Webpack笔记
徐徐 抱歉选手

为什么需要webpack

webpack是前端项目中的一个脚本管理工具,当代码从后端经由网络发布到前端,需要考虑网络环境的拥塞程度以及资源请求的先后顺序。在前度项目的配置中决定如何处理通信资源是webpack要解决的问题。

webpack所做的就是把项目文件夹webpack-demo/src下的(默认entry point,可自定义)所有内容(包括javascript脚本,css,字体,图片等,只要是loader或plugin支持的文件),按照webpack.config.js中说明的那样,以另一种形式生成到webpack-demo/dist(默认)文件夹下(可以在配置中更改生成路径)。这个过程被称为transcompile(转译)。

例如,在html中引入script可以改成在js文件中利用import引入其他的script;而html中的script应当指向webpack生成的script。

默认的配置文件webpack.config.js的出现是为了减少在终端手动输入大量项目配置命令。实际上可以在命令行npx webpack --config webpack.config.js用任何配置去转译当前项目。

可以在package.json的的scripts字段进行脚本自定义,以更便捷的方式调用webpack。例如以npm run build代替npx webpack --config webpack.config.js

以下所有内容均是在webpack.config.js文件中的module.exports = {}对象中定义的。

entry

entry point的值是指向某个文件的路径,告诉webpack用哪一个文件开始建立内部的dependency graph(一个文件夹中各个文件的依赖关系,entry point路径的文件应该就是依赖图的根节点)。

默认值是./src/index.js。可以有一个或者多个。注意这里的路径是当前文件夹下的相对路径。

entry property可以接受利用缩写接受一个路径,也可以接受一个对象,更可以接受一个数组。

基础设置single entry

1
2
3
4
5
6
7
8
9
10
11
12
// single entry shorthand syntax
module.exports = {
entry: './path/to/my/entry/file.js'
};
// signle entry non-shorthand syntax
module.exports = {
entry: {
main: './path/to/my/entry/file.js'
// 如果有多个项目就是object syntax
}
};

多入口的代码分离

如果利用entry写了多个入口起点,那就是手动的去分离了代码,每一个入口起点被转译后的bundle就更小了,更能实现按需加载文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// multi-main entry with array syntax
// 尽量避免使用多入口的入口
module.exports = {
entry: [
'./src/file_1.js',
'./src/file_2.js'
]
// 传递数组只会生成一个chunk,因此webpack会把数组里的源代码都打包到一个bundle中
};
// objecy syntax
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
// 对象中几个字段就生成几个chunk
// 这里会生成两个
}
};

有几个entry point,就代表webpack需要几个互相独立的依赖图。

在多个entry的情况下,也需要有多个output bundle。通过引用[name]来实现output.filename匹配到对应entry name。

多入口代码分离模块重复代码与实例化隐患

Modules relied on by multiple entry bundles can be extracted into common bundles used across multiple pages.

Whether extracted or inlined, it’s important that a module never be instantiated multiple times - both ECMAScript Modules and CommonJS Modules specify that a module must only be instantiated once per JavaScript context.

但是这种方式的问题在于每一个引用模块的代码都会被实例化多次;如果每一个entry都包含相同的引用模块的代码,会在结果bundle的各个文件中再次重复,增加代码量。

期望的结果:a module used by two different entry bundles will be instantiated only one time.

多入口代码分离重复

防止模块多次实例化的方法可以是在entry字段中的每个entry都添加一个dependOn字段,但是这无法解决输出bundle间代码重复的问题。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js',
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
};

并且如果这些entry生成的bundle都服务于同一个html页面,还需要在webpack.config.js中设置optimization.runtimeChunk: 'single'

This doesn’t prevent Webpack from copying module code between entry points, but it prevents it creating two instances of the same module at runtime, while reducing the number of HTTP requests needed to load modules for a given page.

output

output告诉webpack在哪个路径下输出他解析这些文件依赖关系后得出的bundles,以及如何命名输出的bundles,因此用一个对象来表示output.path和output.filename。

默认值是./dist/main.js,其他相关的生成文件bundle都放在./dist文件夹下。

基础设置single entry

只需要在output property中指定filename的值。只适用于single entry的情况。

1
2
3
4
5
module.exports = {
output: {
filename: 'bundle.js',
}
};

这个语句告诉webpack,把输出的文件放到默认的dist目录的bundle.js中。

multiple entry

如果有多个entry,那么也会有多个依赖图,也就是多个js文件输出。在output.filename中使用'[name].js',(可替换模版字符串substitution)这里的name匹配的是entry point的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
// filename: '[name].[contenthash].js',
path: __dirname + '/dist'
}
};

// writes to disk: ./dist/app.js, ./dist/search.js

还有其他的通过带方括号的字符串来模版化文件名的方式。[name]引用entry的名字,[contenthash]根据资源内容创建唯一hash。

基础用法

需要注意output.path使用的是执行环境下的相对路径,需要使用path package解析执行环境。

1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};

loaders in module.rules

默认情况下,webpack只能理解JavaScript以及JSON文件。但是前端项目中设计其他的语言,比如css,为了使css也能够被bundle并输出到output.path去,需要loaders。

Loaders can transform files from a different language (like TypeScript) to JavaScript or load inline images as data URLs. Loaders even allow you to do things like import CSS files directly from your JavaScript modules!

针对某个类型的文件,需要两个属性。一个是用于鉴别哪一个文件(涉及到使用正则表达式进行文件匹配)是当前loader需要转换的test property,另一个是说明需要用到哪些loaders的use property。

在module内部定义a rules property for a single module with two required properties: test and use.

1
2
3
4
5
6
7
8
9
module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
};

以上这些语句告诉webpack compiler在require()/import语句中遇到一个一.txt结尾的文件时,先使用raw-loader把它转换成目标代码,再添加到输出bundle去。

在使用正则表达式匹配文件的时候,不应当使用单引号或者双引号。/\.txt$/是正确的,告诉webpack去匹配所有以txt结尾的文件;但"/\.txt$/"是错误的,它告诉webpack去匹配绝对路径'.txt'下的某个文件。

Loaders can be chained. A chain is executed in reverse order.

在use中loader是可以被串联起来的,但loaders的放置先后顺序有讲究loaders都是从右到左/从下到上被执行的。在test css结尾的文件中,loader执行的顺序先是sass-loader,sass-loader将执行结果传递给css-loader, 最后css-loader的执行结果被传递给style-loader。

加载CSS

首先,需要下载npm包style-loader以及css-loader。如果希望压缩css等,需要使用sass-loader,postcss-loader。

其次,在modules.rules中分别配置test和use。

然后,就可以在任何文件中通过import语法获取.css文件。

asset/resource加载图像字体

webpack5内置的type: ‘asset/resource’可以解决图片资源加载的问题。无需借助loader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
//...
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
};

加载数据json/csv/tsv/xml

由于JOSN文件格式是默认支持的,无需下载loaders。但是csv/tsv/xml需要下载csv-loader以及xml-loader。

在import时注意json文件只支持export default默认导出。

在数据可视化的场景下,直接将数据作为构建时的资源,浏览器就可以直接访问解析后的数据,无需在构建完成后运行时再发送http请求。

plugins

loader是用来transform certain type of modules的;plugins用途更为广泛,涉及bundle optimization,asset management以及injection of environment variables。

plugins的实现原理

webpack也是基于plugin system而建立的。webpack plugin本质上就是一个拥有apply方法的JavaScript对象。apply(compiler)表示这个apply方法是由webpack compiler执行的。

基础使用

plugin需要使用npm安装,并使用require引用。

在使用特定的plugin之前需要建立一个const指向require(),require的对象就是该plugin。在使用该plugin的时候,是在plugins array中使用new运算符,并传入特定的需求。

为什么使用new运算符调用一个plugin?因为一个plugin并不是单一功能的,有可能在一个文件中为了各种需求需要多次使用该plugin,因此要使用new运算符在每一次创建的时候都生成一个实例。

1
2
3
4
5
6
7
8
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};

html-webpack-plugin

为什么需要HtmlWebpackPlugin?这个plugin的作用是在webpack输出文件夹下依据规则,自动生成index.html,这个index.html还会依据输出的javascript bundle自动引用script,这样就无需手动添加html与引用script了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
}),
],
output: {
filename: '[name].bundle.js',
// 生成的html会自动引用这些bundle
path: path.resolve(__dirname, 'dist'),
},
};

clean-webpack-plugin

默认每一次不同的配置文件产出在dist下的文件,除非文件名相同,否则是不会覆盖的。因此dist文件夹会相当混乱。clean-webpack-plugin的作用是在每次build前先清理dist文件夹,构建后dist中只会有生成且用得到的文件。

split-chunks-plugin

该插件用于代码分离,将公共的依赖模块提取到已有入口的chunk中。

1
2
3
4
5
6
7
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};

该插件自动移除依赖模块,并且依赖模块会被分离到单独的chunk。

也可以使用该插件提取引导模版extracting boilerpate,即将第三方库,也就是node_modules中涉及的内容提取到单独的vendor chunk文件中,通过split-chunks-plugin的cacheGroups选项实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
optimization: {
moduleIds: 'deterministic',
// 这是为了保证每一次build生成的依赖库的hashname都一样。
runtimeChunk: 'single',
// runtimeChunk: "single"会将Webpack在浏览器端运行时需要的代码单独抽离到一个文件
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},

这部分vedor的代码,很少会被修改,缓存在client端,既减少了想server获取资源的请求,也能保证依赖模块版本一致。

参考深入Webpack快取机制

但是要求每一次构建后,vendor hash都保持一致。如何让每一次的hash结果保持一致?影响hash结果的因素有哪些?

第一是文件的内容,既然是node_moudles第三方库,内容应该是不会改变,因此vendors的hash名称也应当build前后一致。但是实际上并不是这样,这时候需要添加moduleIds: 'deterministic',为什么?由此引入第二个原因。

但是还有第二个因素就是执行先后顺序,在webpack中执行先后顺序通过依赖图dependency graph来实现。在runtime.js中,记录者所有模module与块chun之间的关系,module id与chunk id默认情况下会在每一次build前后都可能发生变化,而固定module id可以让build前后

mode

mode设定可以有devlopmentproductionnone。默认取值是production。

1
2
3
module.exports = {
mode: 'production'
};

开发环境下常用工具

source-map

有多种形式,可以追踪出错的源码,涉及浏览器开发工具。

1
2
3
4
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
};

代码变化后自动编译

  • 使用watch mode

通过添加一个用于启动webpack watch mode的npm scripts实现,注意开发时如果不希望clean-webpack-plugin自动删除不使用的文件,可以配置clean-webpack-plugin的选项。

  • 使用webpack-dev-server

提供一个简单的web server并实现live reloading。

需要告知dev Server去哪里查找文件,在webpack.config.js中添加devServer:{ contentBase: './dist'}选项。默认会将dist目录下的文件serve到localhost:8080

添加一个用于启动devServer的npm script:"start": "webpack serve --open"

  • 使用webpack-dev-middleware

Web pack-dev-server内部就是通过使用webpack-dev-middleware来实现的,直接设置webpack-dev-middleware可以更自定义,比如在哪个端口监听。

需要同时使用express和webpack-dev-middleware,并在webpack.config.js的output字段添加publicPath: '/',配置express的server脚本server.js会使用到publicPath。而webpack-dev-middleware的作用应该就是让server.js能够访问webpack.config.js的内容

server.js的配置如下,在完成server.js的配置后,需要把node server.js配置添加到npm scripts中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);

// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});

而在vue3-demo3中的server.js又是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require("express");
const history = require("connect-history-api-fallback");
const serveStatic = require("serve-static");
const enforce = require("express-sslify");

const app = express();

app.use(enforce.HTTPS({ trustProtoHeader: true }));
app.use(serveStatic(__dirname + "/dist"));
app.use(history());

app.listen(process.env.PORT || 5000);

不同之处在于不是通过webpack-dev-middleware来访问webpack.config.js,vue脚手架已经写死了配置,默认路径就是dist。

约定

  • 使用require()语法导入其他文件或者npm uitilites
  • 使用JavaScript control flow expression如?:
  • 为常用值设置const或者let变量
  • 为常用的配置建立函数并执行

不要在一个文件中书写多个configuration,应当把整个项目的configuration写成多个文件multiple .config.js files。

webpack module

为什么需要模块化与去模块化

一方面,模块化便于JavaScript在服务器端便于开发,测试,查错。另一方面浏览器只能理解一整个JavaScript文件,因此又要在生产的时候去模块化,将符合模块化规范的代码转换成浏览器支持的代码。因此如browserify与webpack都是从入口模块开始把所有模块打包成一个在浏览器中运行的js文件的模块化工具。

module/chunk/bundle区分

webpack是一个模块打包机,一个项目中的任何单一文件,对于webpack而言都是模块。

chunk是webpack打包过程中,一堆module的集合。一般来说一条entry对应一个chunk,也有异步加载模块与代码分割可以产生chunk。

A Chunk is a unit of encapsulation for Modules. Chunks are “rendered” into bundles that get emitted when the build completes.

bundle是webpack最终输出的一个或者多个打包文件,在命令行中运行webpack的输出asset的个数就是产生的bundle数。多数情况下,一个chunk产生一个bundle,但也有例外情况。比如在单个文件入口mai n.js以及不设置代码分割的情况下,设置source-map后,依然会产生main.js以及main.js.map两个bundle。

参考Webpack理解Chunk

支持模块化语法

在webpack中,可以使用多种语法来表明module之间的dependency。(按照产生先后顺序)

  • Common JS require()
  • AMD define and require
  • ES2015 import
  • @import inside of a css/sass/less file 属于
  • image url in a stylesheet url(...) or HTML file <img src=...>

这些为模块化服务的语法都是怎么产生的?演化路线CommonJS->AMD/CMD->ES6 Module。

参考前端科普系列-CommonJS:不是前端却革命了前端

CommonJS

Node.js的模块化是在CommonJS基础上实现的,CommonJS是以JavaScript在网页浏览器之外(服务器端backend)创建模块约定的一个项目。CommonJS约定了每个文件就是一个拥有自己作用域的模块。CommonJS规定了两个语法:require语法用于加载某个模块exports导出的值;module以对象代表当前模块,module.exports保存当前模块导出的接口或者变量。

CommonJS用于服务器端同步加载模块,却不适合浏览器端,不可能每每通过require去发送网络请求,等待资源返回,要是资源一直不返回就阻塞了后面的代码。

AMD/CMD

因此就出现了为浏览器端服务的AMD(Asynchroneous Module Definition),他实现了异步加载模块,利用require.js库。也有CMD(Common Module Definition),利用sea.js库。

ES6 Module

而后续出现的ES6 Module规范成为了浏览器端和服务器端的通用解法。主要有exportimport两个命令。import内部的实现是使用了Promise的,因此也就实现了异步加载。

由于import返回的是一个promise,因此import语句可放在在async function中,等到模块加载完成之后在执行函数内容,从而实现动态导入模块代码分离

  • 本文标题:Webpack笔记
  • 本文作者:徐徐
  • 创建时间:2021-01-08 10:08:08
  • 本文链接:https://machacroissant.github.io/2021/01/08/webpack/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论