1、在webpack的官网可以看到,webpack是一个
文件打包工具,它将复杂的的文件依赖打包成独立的资源文件。换句话说,
在webpack里一切文件都是模块,通过loader加载文件,通过plugin注入钩子,最后输出由多个模块组合的文件。那么loader是什么呢?loader用来读取各类资源,比如css、js等。模块loader可以链式调用,链汇总的每个loader都将对资源进行转换,然后将结果传递给下一个loader。
也就是说webpack使用loader来管理各类的资源。
2、使用webpack来管理资源 webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
};
需要注意的是需要保证 loader 的先后顺序:
'style-loader' 在前,而 'css-loader' 在后。如果不遵守此约定,webpack 可能会抛出错误
根据上面的配置中,webpack执行的时候就会把对应的文件当做是一个模块进行处理,你可以import对应的资源进行使用了。
2、
在安装一个 package,而此 package 要打包到生产环境 bundle 中时,你应该使用 npm install --save。如果你在安装一个用于开发环境的 package 时(例如,linter, 测试库等),你应该使用 npm install --save-dev
3、我们知道的是,webpack会生成bundle.js文件,那么,当某一个源文件中出现了一个错误,我们在开发的时候就无法定位到具体是哪一个源文件出错了。这个时候我们在开发环境下面使用
source map来定位问题。配置如下
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
module: {
},
};
4、我们在使用vue搭建项目的时候,是不是每次修改了一个文件,vue就会自动帮我们重新编译构建并刷新浏览器来着?这是因为vue内置了一个webpack的配置。那如果我们不是使用vue来开发项目又想着可以自动编译和刷新呢?这里我们就可以使用webpack-dev-server来进行配置和搭建实现这个需求。
首先我们安装这个插件
npm install --save-dev webpack-dev-server
然后在配置文件中加入下面的代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
devServer: {
contentBase: './dist',
hot:true
},
module: {
},
};
在package.json中加入下面的srcipt
{
"name": "wepack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open"
},
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^5.2.4",
"html-webpack-plugin": "^5.3.1",
"style-loader": "^2.0.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"loadash": "^1.0.0",
"lodash": "^4.17.21"
}
}
执行npm run start,可以看到自动编译和打开了浏览器,这个时候,修改任意文件都会重新刷新文件
这是因为配置中告诉了webpack-dev-server这个插件,将dist目录下面的文件可以通过localhost:8080这个地址可以访问到,webpack-dev-server在编译之后不会写入到任何输出文件。而是将bundle文件保留在内存里面,然后将它们当做是可以被访问的文件。
5、从上面的例子中,我们可以知道,webpack是可以将你的js文件全部打包到一个bundle文件里面,可是有的情况下面,我们不想要将所有的文件一次性引入进来,有些插件在需要的时候再引入。这个时候我们想到的是不是就是将这个bundle文件分离到不同的bundle中,然后再按需加载进来。这个时候我们就需要用到webpack的代码分离的特性了。
在entry里写入多个接口,然后使用 SplitChunksPlugin插件来防止重复加载共同的模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
plugins: [
new HtmlWebpackPlugin({//用来管理新的构建的html文件
title: '管理输出',
}),
],
devServer: {
contentBase: './dist',
},
};
然后执行npm run build
可以看到loadash文件只构建了一次
6、浏览器访问网站的资源的时候,使用缓存技术来加快加载资源的速度。但这又会出现一个问题,就好像我们在项目中修改了很多东西,但是我们文件名没有发生改变。这个时候,浏览器就会以为这个文件没有发生改变,使用缓存的版本。所以我们可以使用webpack的substitution方式来输出文件的名称。这个方式会根据资源的内容创建出唯一的hash,然后再文件后面加上这部分hash。而且这个方式超级简单,我们只需要在webpack的配置文件中修改下面代码:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
plugins: [
new HtmlWebpackPlugin({//用来管理新的构建的html文件
title: '管理输出',
}),
],
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
devServer: {
contentBase: './dist',
},
};
现在我们不改变文件内容,再build一下看看
可以看到,此时是使用了cache,文件是没有发生变化的
此外,我们希望一些引用外部插件的bundle在构建的时候不发生变化,这样每次项目发生变化的时候,重新构建就不要再去构建这些不经常改变的外部插件了。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({//用来管理新的构建的html文件
title: '管理输出',
}),
],
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
devServer: {
contentBase: './dist',
},
};
这个时候我们可以看到,构建出来的vendor和runtime模块独立出来了,其他自己写的代码模块体积就变小了,这样的资源在二次浏览的时候,就大大的提升了加载速度!牛逼格拉斯!~
6、假设我们自己写了一个插件,想要发布到npm上面去,我们怎么样使用webpack来打包构建这一个插件呢?
其他部分和上面都一样,此外还要申明一下引入方式:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次新构建之前都会清理一下输出的文件夹
library: {
name: 'webpackNumbers',
type: 'umd',
},
},
};
7、webpack 4中开始引入了tree-shaking,它主要的作用就是消除项目中无用的代码。这样依赖,项目的体积就会大大提升了。也许你会问,项目中还会有无用的代码吗?那不是有用才会写!
那不是这样的,在项目开发的过程中,需求是会不断的改变,一个页面里面引入的组件可能在这一个阶段有用,但是在下一个阶段里,这个功能有可能就会被废弃了。维护的时间越长,废弃的代码可能会越多。
这样,tree-shaking就大大压缩了打包后的体积了。也许你又会问,这个东西内部是怎么识别代码有没有用的,tree-shaking依赖于模块的特性,也就是import和export。。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那是“死代码”,并会对其进行 tree-shaking
那么什么样的代码会认为是未被使用的代码呢?我们先来看一下例子
import _ form 'lodash';//这样的导入,webpack会认为这整个lodash的库你都有使用到,就不会动这整个库的代码,所以不建议引入整个库。
import {join} from 'lodash';//这样具名引入的时候,如果后续的代码没有使用这个引入的方法,就会被当做是废弃的代码,然后tree-shaking
接下来我们在webpack的配置中开启tree-shaking
需要注意的是,webpack官网中指定,必须要在生产模式下面才可以开启tree-shaking,这也可以理解,因为只有在压缩打包代码的时候才会tree-shaking,所以只有在生产模式下才会,所以最后打包的时候记得要将mode 改成production
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
optimization: {
usedExports: true,//Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记
},
};
接下来我们先理解一个概念:sideEffects,副作用
在上面的配置里面,我们已经让webpack去筛选那些没有被使用的代码,但有些代码引入没有被使用不代表它是一个废弃的代码。比如我们引入样式,使用全局样式表,或者是一个引入全局配置的配置文件。
这样被引入没有被使用但依然起起作用的文件, webpack认为这样的文件有“
sideEffects
”,这样的文件不应该被tree-shaking。为了避免整个项目有副作用的文件,webpack默认将所有的文件视为有副作用,这样就是整个项目都不能tree-shaking,所以我们先配置一下sideEffects,告诉webpack你可以进行tree-shaking。
在package.json中配置
// 所有文件都有副作用,全都不可 tree-shaking
{
"sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
"sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
"sideEffects": [
"./src/file1.js",
"./src/file2.js"
]
}
此外,还可以在loader中配置sideEffects
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
publicPath:'/',//指定资源的目录
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({//用来管理新的构建的html文件
title: '管理输出',
}),
],
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
sideEffects:true,
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
devServer: {
contentBase: './dist',
hot: true,
},
};
然后执行npm run build的时候,废弃代码就被删除掉了。下面我们来看一下前后代码的对比
这是我引入lodash之后没有使用的然后打包的代码(配置之前)
可以很清晰的看到最后打包是有吧lodash引进来的,接下来我们按照上面的配置然后打包
记住!这一步很重要,将mode改成production 删除optimization
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',//环境:分别为developement\production\none;对应着开发环境、生产环境以及无差别
devtool: 'inline-source-map',
entry: {//entry可以有多个入口文件
index: './src/index.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
publicPath:'/',//指定资源的目录
clean: true,//每次新构建之前都会清理一下输出的文件夹
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({//用来管理新的构建的html文件
title: '管理输出',
}),
],
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
sideEffects:true,
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
devServer: {
contentBase: './dist',
hot: true,
},
};
执行npm run build看下
可以看到此时那些无用的代码就被删掉啦!
好了,配置完了我们来总结一下
-
需要使用模块的语法(import、export)
-
没有被babel转换成commonJs(Webpack 不支持使用 commonjs 模块来完成 tree-shaking。)
-
package.json中配置sideEffects
-
mode设置为producrion再打包
8、环境配置
刚才我们先配置了开发环境的webpack.config.js,然后打包前又要修改,要是每一次都这样是不是很麻烦?):
首先我们在webpack.common.js里面写公有的配置,在webpack.dev.js中写开发环境的配置,在webpack.pro.js中写生产环境的配置,然后使用webpack-merge的工具来进行合并共有项。
然后再package.json中写入下面的script,只对你对应的配置文件
"build": "webpack --config webpack.prod.js",
"start": "webpack serve --open --config webpack.dev.js"
这样就可以啦
9、概念理解
接下来要理解一些webpack里面的概念
(1)entry
entry指明
webpack应该使用哪个作为入口,然后webpack会找出那些模块和库是这个入口直接或间接的依赖。
默认值是./src/index.js,可以是一个或多个入口。
//像这样写的时候输出的时候就会index.bundle.js,print.bundle.js
entry: {//entry可以有多个入口文件
index: './src/index.js',
print:'./src/print.js'
},
//如果想这多个入口文件被绑在一个chunk中的时候,使用下面的方法
entry: [
'./src/index.js',
'./src/print.js'
],
//单个入口的时候
entry: './src/index.js'
//当一个入口依赖另一个依赖的时候,
entry: {
index: './src/index.js',
print: {
dependOn: 'index',//比如等index被加载完成再加载这个入口
import: './src/app.js',//启动的时候需要加载的模块
},
},
//将app和vendor入口分离(上面说的代码分离)
//在vendor中存入未做修改的必要库或文件,然后将这些打包在一起成为单独的chunk,内容哈希保持不变,这就使浏览器可以独立缓存,减少了加载的时间
entry: {
main: './src/app.js',
vendor: './src/vendor.js',
},
output: {
filename: '[name].[contenthash].bundle.js',
},
(2)output
output是告诉webpack,在
哪里输出这些文件还有如何命名这些文件。我们在上面已经可以知道,这里可以配置哈希文件名,并且可以再每次输出前清空输出文件。
output.path指定bundle输出到哪里,filename来配置输出的文件名
output: {
filename: '[name].[contenthash].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
(3)loader(类似于gulp中的task)
因为webpack只能理解js和json文件格式,loader是为了让webpack去处理其他类型的文件,将这些文件转换成模块,然后被添加到依赖中,
loader对应的顶级配置是module.rules中,其中每个数组项是一个loader。
test属性是识别哪些文件需要被转换,use属性是定义使用哪个loader来转换这些文件
还可以在这里指定哪些文件有副作用sideEffects
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
sideEffects:true,
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
],
},
(4)插件
插件是可以解决loader无法解决的事,webpack的插件是一个有apply方法的对象,这个apply会被webpack的编译器调用。
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
(5) target 指明配置使用的环境,可以在这里指明是服务端node还是浏览器
target: 'node',
target可选值如下:
async-node
|
编译为类 Node.js 环境可用(使用 fs 和 vm 异步加载分块)
|
electron-main
|
编译为 Electron 主进程。
|
electron-renderer
|
编译为 Electron 渲染进程,使用 JsonpTemplatePlugin,
|
FunctionModulePlugin 来为浏览器环境提供目标,使用 NodeTargetPlugin 和 ExternalsPlugin
|
|
为 CommonJS 和 Electron 内置模块提供目标。
|
|
electron-preload
|
编译为 Electron 渲染进程,
|
使用 NodeTemplatePlugin 且 asyncChunkLoading 设置为 true ,FunctionModulePlugin 来为浏览器提供目标,使用 NodeTargetPlugin 和 ExternalsPlugin 为 CommonJS 和 Electron 内置模块提供目标。
|
|
node
|
编译为类 Node.js 环境可用(使用 Node.js require 加载 chunks)
|
node-webkit
|
编译为 Webkit 可用,并且使用 jsonp 去加载分块。支持 Node.js 内置模块和 nw.gui
|
导入(实验性质)
|
|
nwjs[[X].Y]
|
等价于 node-webkit
|
web
|
编译为类浏览器环境里可用 (默认)
|
webworker
|
编译成一个 WebWorker
|
esX
|
编译为指定版本的 ECMAScript。例如,es5,es2020
|
browserslist
|
从 browserslist-config 中推断出平台和 ES 特性 (如果 browserslist 可用,其值则为默认)
|
那么我们这个项目要是要在服务端和客户端同时运行的咋办呢?
多target配置!
const path = require('path');
const serverConfig = {
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.node.js',
},
//…
};
const clientConfig = {
target: 'web', // <=== 默认为 'web',可省略
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.js',
},
//…};
module.exports = [serverConfig, clientConfig];
(6)runtime与 manifest
这两个东西是用来管理项目中所有模块的交互。是
在浏览器运行过程中,webpack 用来
连接模块化应用程序所需的所有代码
。它包含:在模块交互时,
连接模块所需的加载和解析逻辑
。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
当 webpack 的compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块
也就是说runtime是用来管理模块的主要逻辑代码,manifest是用来记录所有模块之间的关系。
runtime和manifest在每次构建的时候都会发生变化,这也就是为什么即使内容没有发生变化,hash还会会改变的原因。