前言
webpack在日常开发中常常会用到,但在开发中只是了解一些基本配置,并没有深入的去学习每个插件的作用是什么。这次读取了一遍webpack的文档,写下这篇文章也是对自己学习内容的一个梳理过程。
webpack到底是什么?
我们经常使用的import,在浏览器中无法加载Cannot use import statement outside a module
原因是浏览器无法识别import语法,此时需要webpack来翻译一下,将import变为浏览器能够认识的语法
npm init -y;
// webpack是核心模块
// webpack-cli是帮助我们用命令行运行webpack的
yarn add webpack webpack-cli;
// 用webpack翻译index.js
npx webpack ./index.js
执行完上面语句后,会自动在根目录生成一个dist/main.js
所以在这里,webpack起到了一个转换器的作用。但说webpack是一个转换器还不太准确,因为webpack只能识别import这类简单的es6语法。
我们去webpack官网看下webpack的定义。webpack.docschina.org/concepts/
官网给出的答案是:webpack是一个模块打包工具。
// Header
export default function () {
console.log("我是header");
}
// Sider
export default function () {
console.log("我是sider");
}
import Header from "./Header";
import Sider from "./Sider";
Header();
Sider();
如上所示,Header和Sider各自算作一个模块,执行完webpack后,生成的main.js文件如下所示,webpack将不同模块内的内容打包成一个文件,这就是webpack的主要作用
(()=>{"use strict";console.log("我是header"),console.log("我是sider")})();
我们上面提到的import是ES Module模块引入方式,我们还有Common.js模块规范,AMD,CMD规范,webpack同样可以识别。
// Common.js模块引入方式
// Header.js
module.exports = function () {
console.log("我是header");
};
// Sider.js
module.exports = function () {
console.log("我是sider");
};
const Header = require("./Header");
const Sider = require("./Sider");
Header();
Sider();
webpack配置文件
webpack并没有那么智能,他不能独立完成项目的打包,它需要一个配置文件来指导webpack的处理项目。我们前面运行npx webpack ./index.js
之所以不需要配置文件,是因为webpack会有一些默认的配置下来帮助我们完成这些操作。
比如前面我们运行npx webpack index.js
,此处如果不指定index.js则会报错,因为webpack不知道从哪里开始编译。有了配置文件,我们在运行时则不需要指定入口文件了,webpack会自从去配置文件中检索。
webpack.config.js
配置文件就是根目录下的webpack.config.js
,webpack.config.js
是我们的默认文件名,如果我们想定义其他的文件名,需要在package.json
中单独配置一下。
"scripts": {
"dev": "webpack --config webpackconfig.js"
},
我们先来定义一个简单的配置文件
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
// 入口
entry: "./index.js",
// 出口
output: {
path: path.resolve(__dirname, "bundle"),
filename: "bundle.js",
},
};
解释下上面的两个变量,path.resolve
和 __dirname
- path.resolve: 表示路径的拼接 它可以接受多个参数,依次表示所要进入的路径
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
相当于
$ cd foo/bar
$ cd /tmp/file/
$ cd ..
$ cd a/../subfile
$ pwd
path.resolve('/foo/bar', './baz')
/foo/bar/baz
path.resolve('/foo/bar', '/tmp/file/')
/tmp/file
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
如果当前目录是/home/myself/node,返回
/home/myself/node/wwwroot/static_files/gif/image.gif
用于将相对路径转为绝对路径。
- __dirname: 获得当前执行文件所在目录的完整目录名
所以 path.resolve(__dirname, "bundle"),
表示当前目录下的bundle文件夹
我们运行npx webpack
后,编译后编译器输出了以下内容:
其中的name:main,代表 webpack.config.js
中输入文件的一种形式,当我们定义单一入口时,entry就默认为main了。
module.exports = {
entry: "./index.js",
entry: {
main: "index.js",
}
};
mode
配置文件中有个mode属性,mode属性有两个值,development 和 production。二者的区别是生成后的打包文件是否会被压缩。development不会被压缩,而production会被压缩。
行内命令
除了通过webpack.config.js来配置启动项,我们在script内也可配置启动一些配置项来管理项目。具体内容可参考文档 webpack.docschina.org/api/cli/
loader
打包图片
我们所指的模块,不仅仅是js文件。less,css,jpg,png都可以称为一个模块。webpack只会打包以js文件,如果遇到.jpg,.jpeg结尾的文件,它就傻眼了。此时需要我们单独的配置config文件,告诉webpack如何去打包。
file-loader
file-loader可打包图片
npm i file-loader -D
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
use: "file-loader",
},
],
},
};
运行打包命令后,会发现bundle文件夹下多出了一个图片文件,这就是打包后的图片。我们console.log,输出了图片的文件名。
在webpack打包的时候,遇到了jpeg结尾的图片,webpack按照file-loader的打包规则,将图片移到了bundle文件夹下,然后把文件的地址返回给我们import的变量。
由此看出,loader是打包的方案。webpack不能识别非js后缀的模块,就通过loader让webpack识别出来。
我们也可指定打包后图片的名称,和图片打包的地址。
module.exports = {
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
loader: "file-loader",
options: {
// 打包的格式,name原文件名,ext后缀名
name: "[name].[ext]",
// 打包在哪个文件夹下
outputPath: "images/",
},
},
],
},
};
url-loader
url-loader也具有和file-loader相似的功能,但url-loader打包出来的东西,和url-loader还是不同的。
由上图可以看到,图片没有被打包成一个文件,再去页面上发现可以看到这个img。
由此可见,url-loader打包图片,是将图片打包成base64字符串放在js里。base64的一个优点是节约了一次图片请求,但是base64非常的长,图片过于大的时候,会使js非常的大,影响页面的加载。url-loader提供了一种思路,合理的平衡了2者。当图片比较小时,转化为base64打包进js文件里,当图片过大时,就把image打包成一个图片放到bundle文件夹内。
limit为文件大小,webpack官网建议,8192B(8KB)以下的文件打包成base64,超过8192B的图片打包成单独的图片文件。
module.exports = {
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
},
},
],
},
],
},
};
asset module(webpack5)
file-loader 和 url-loader 是webpack4的用法。在webpack5中,新增了asset module (资源模块),它使我们配置字体和图片时无需配置额外的loader。
module.exports = {
output: {
// 自定义文件名
assetModuleFilename: "images/[name]_[hash][ext]",
},
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
type: "asset",
parser: {
// 大小限制,webpack默认为8kb
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
},
],
},
};
打包样式
css-loader
css-loader会对@import
和url()
进行处理,找到它们的引用关系,将他们合并为一个文件。它的功能有点类似于import和require
style-loader
在css-loader合并为一个css文件后,style-loader 将 style 标签插入到DOM中(head内)
less-loader
npm install less less-loader --save-dev
loader的解析是从右到左的,先将less解析为css,再挂载到DOM上。
npm i css-loader style-loader -D
npm i less less-loader --save-dev
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/i,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
postcss-loader
如下图所示,style中的css3特性,并没有兼容适配于各个浏览器的前缀,比如-webkit-,-moz-,-o-等,我们每个属性挨个写又太麻烦,使用postcss-loader中的autoprefixer插件,可帮助我们解析less时自动添加。
npm install --save-dev postcss-loader postcss
// 使用 autoprefixer 添加厂商前缀。
npm install --save-dev autoprefixer
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader","postcss-loader"],
},
{
test: /\.less$/i,
use: ["style-loader", "css-loader", "postcss-loader","less-loader"],
},
],
},
};
遇到postcss-loader后,去postcss.config.js中寻找配置项
// postcss.config.js
module.exports = {
plugins: ["autoprefixer"],
};
// package.json
"browserslist": [
"defaults",
"ie> 8"
]
package.json中定义了当前项目需要兼容的浏览器,postcss是根据browserslist中的浏览器进行适配的。如果browserslist中的浏览器都支持该属性,那么这个css属性不需要加上前缀。所以不加browserslist会导致autoprefixer不生效。
css-loader补充
前面我们知道css-loader是合并css文件的,但是在合并过程中还会遇到很多问题,下面我们一起来看看会有什么问题。
importLoaders(没生效)
importLoaders 是 css-loader的属性。webpack对于js中引入的less文件,会从右到左根据config中定义的顺序进行解析。但是对于less中引入less,被引入的less文件则不会按照config定义的规则解析,而是直接使用css-loader进行编译。
我们来看下
// index.less
/*
* 在index.less中通过@import引入test.less,这时候test.less中的样式不会被postcss编译。
*/
@import './test.less';
span {
color: pink;
&.name {
transform: translate(100px, 100px);
}
}
// test.less
p {
&.name {
transform: translate(50px, 50px);
}
}
所以css-loader提供了importLoaders属性来解决这个问题,
module(性能不好)
module设置为true后开启css模块化,任何项目中引入的less文件只属于当前模块自己拥有。
module.exports = {
module: {
rules: [
{
test: /\.less$/i,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
},
},
"postcss-loader",
"less-loader",
],
},
],
},
};
// 使用
import style from "./index.less";
let img = document.createElement("img");
img.setAttribute("src", pic1);
img.classList.add(style.img);
字体体验版
plugin
前面我们知道loader是帮助webpack打包非js文件的,plugin也是webpack中很重要的一个概念,它可以在打包的某个关键时刻帮助我们做一些事情。
html-webpack-plugin
每次打包后的文件是一个js文件,想让它展示在页面上需要手动在bundle文件夹下创建一个index.html,如果每次打包后都手动创建是非常不便利的,html-webpack-plugin帮助我们解决了这个问题。
npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
plugins: [new HtmlWebpackPlugin()],
};
run npm dev
后会自动在bundle文件夹下创建一个index.html,并且将我们打包后的js引入到html里。
新打包成的html中虽然引入了js,但是没有root根结点,导致js中的元素无法挂载到dom节点上,html-webpack-plugin允许提前预设一个模版,打包后直接在bandle文件夹下创建这个模版,并且把js引入到html中。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
plugins: [
// 定义模版文件
new HtmlWebpackPlugin({
template: "./index.html",
}),
],
};
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>html 模版</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
clean-webpack-plugin
clean-webpack-plugin不是官网推荐的插件,它是一个三方的插件,它帮助我们每次build前都删除原先的目录,再创建一个新的目录,防止上次build后文件的残留。
npm install --save-dev clean-webpack-plugin
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
// 使用CleanWebpackPlugin时必须要有output,否则plugin不知道上次打包的是哪个,无法执行清除
output: {
path: path.resolve(__dirname, "bundle"),
filename: "bundle.js",
},
plugins: [
new CleanWebpackPlugin(),
],
};
注:在webpack5中,在output中加一个clear:true属性即可替代clear-webpack-plugin的作用。
output
publicPath
output输出的是相对地址,但在企业级开发时我们的静态资源往往会被挂载到cdn上,这时候需要设定publicPath。publicPath为每个url添加前缀,当将资源托管到 CDN 时,publicPath可以设定为cdn地址。
module.exports = {
output: {
path: path.resolve(__dirname, "bundle"),
filename: "bundle.js",
publicPath: "https://cdn.com",
},
};
clean
webpack5中,output中的clean属性替代了clean-webpack-plugin,无需再次引入plugin了。
module.exports = {
output: {
clean: true,
},
};
source-map
运行打包后的代码时,如果出现了代码错误,在浏览器中只能定位到打包后的代码中出现错误的地方,而无法定位到打包前代码的错误位置,这使得我们在查找错误点时相当的麻烦。sourcemap可以对打包前后的文件进行映射。能找到转换后的代码所对应的转换前的位置。有了这种映射关系后,出错时,可以在控制台直接显示源代码中出错的位置。
现在webpack5运行时,不使用source-map属性也可以进行错误代码定位了。
module.exports = {
devtool: "source-map",
};
inline-source-map 和 source-map
devtool的属性值有两个,source-map和inline-source-map,它们的功能是一样的。
使用source-map打包后,会生成一个.map
文件,这个.map
文件记录了打包后代码和打包前代码位置映射的关系。
当使用inline-source-map打包后,.map
文件不再是一个单独的文件,而是将它以一个base64的方式写到了bundle.js
文件的底部了。
不同前缀的差别
除了inline-
前缀,还有很多其他的前缀,大致简单介绍一下。
- cheap 不加cheap会使bug定位到具体字符,而cheap只帮我们定位到某行。但使用cheap会使代码编译的性能较好
- module 加了module可定位三方依赖的错误,不加module只可定位业务代码的错误
- eval 会只映射出错的地方
具体他们之间的差异也可查阅官方文档 webpack.docschina.org/configurati…
source-map不只应用在开发环境,项目部署到线上我们也想暴露一些问题的话,也需要配置source-map。
development devetool: cheap-module-eval-source-map
production : cheap-module-source-map
webpack-dev-server
我们每次改变业务代码后,都需要手动的npm run dev
,再打开index.html。我们希望每次改完源代码,bundle目录下的代码自动更新。一种有3种方式可以实现。
watch
使用watch后可监听到源代码的改变,webpack重新打包,我们只需要手动刷新一下即可。
"scripts": {
"watch": "webpack --watch",
},
dev-server
watch只能监听变化并且重新打包,webpack-dev-server
提供了更便捷的功能。
- 打包后能自动唤起html
- 启动一个服务器去展示html
- 自动刷新浏览器。
npm install --save-dev webpack-dev-server
"scripts": {
"start": "webpack serve"
},
module.exports = {
devServer: {
// 告知 dev server,从什么位置查找文件
static: "./bundle",
// 服务启动后打开浏览器
open: true,
// 监听的端口号
port: 8000,
},
}
更多配置可查看文档 webpack.docschina.org/configurati…
为什么要启动服务展示页面
我们在本地也可以展示html,但发送http请求的时候,必须通过服务器去发送请求。所以我们必须要使用webpack-dev-server
来启动一个服务。
proxy(待整理)
打包
我们使用webpack-dev-server打包后,发现没有在目录下生成一个dist文件夹。webpack-dev-server将文件生成在内存里,这样可以有效的提升打包速度
手写server(待学习)
我们可以通过自己写一个服务,来模拟webpack-dev-server的功能。但是这块会涉及到node部分,在node中使用webpack。后面我会单独学习一下,再写一篇相关文章。
hot module replacement(HMR)
热模块替换是webpack最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
我们先来看个例子。
// index.js
import addItem from "./test.js";
import "./index.less";
let dom = document.getElementById("root");
let btn = document.createElement("button");
btn.innerHTML = "click";
btn.onclick = function () {
addItem();
};
dom.appendChild(btn);
--------------------------------------
// index.less
.item:nth-of-type(odd) {
background-color: yellow;
}
--------------------------------------
// test.js
export default function () {
let dom = document.getElementById("root");
let item = document.createElement("div");
item.setAttribute("class", "item");
item.innerHTML = "item";
dom.appendChild(item);
}
连续点击多次click,item被不断累加,当我们在代码中更改颜色后,返回页面时代码没有被刷新(item没有被清零),只是颜色改变了。这就是热模块更新。不重新刷新页面,只局部更新。
下面在webpack中配置下热模块更新。
hot有2个属性,true/only。
- only:如果没有热更新成功,不重新刷新页面
- true:如果没有热更新成功,则刷新页面
const webpack = require("webpack");
module.exports = {
devServer: {
hot:'only'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
}
这样配置完即可使用热模块更新了,less/css 的热更新比较简单, 因为style-loader
内部帮助我们进行了module.hot.accept
的操作。如果是js文件,无需经过loader的转化,则需要自己进行module.hot.accept
的操作。
很多loader为我们编写了module.hot.accept
,开发时我们不需要在考虑热更新问题,直接写业务代码即可。
- React Hot Loader: 实时调整 react 组件。
- Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
下面我们再来看组例子,下面是需要我们自己写module.hot.accept
的场景。
// index.js
import test from "./test.js";
let dom = document.getElementById("root");
let num = document.createElement("div");
num.innerHTML = 0;
num.onclick = function () {
num.innerHTML = Number(num.innerHTML) + 1;
};
dom.appendChild(num);
test();
--------------------------------------
// test.js
export default function () {
let dom = document.getElementById("root");
let item = document.createElement("div");
item.innerHTML = 400;
item.onclick = function () {
item.innerHTML = Number(item.innerHTML) + 1;
};
dom.appendChild(item);
}
每次在代码中改变某个值,页面不更新为新值,也不刷新页面。这是因为我们没有使用到module.hot.accept
,下面我们来改进下,就可以热更新了。
// index.js
import test from "./test.js";
let dom = document.getElementById("root");
let num = document.createElement("div");
num.innerHTML = 0;
num.onclick = function () {
num.innerHTML = Number(num.innerHTML) + 1;
};
dom.appendChild(num);
let ele = test();
// 热更新,每当test.js中代码改变了,则重新渲染test.js,渲染前先移除dom
if (module.hot) {
module.hot.accept("./test.js", function () {
dom.removeChild(ele);
ele = test();
});
}
--------------------------------------
// test.js
export default function () {
let dom = document.getElementById("root");
let item = document.createElement("div");
item.innerHTML = 200;
item.onclick = function () {
item.innerHTML = Number(item.innerHTML) + 1;
};
dom.appendChild(item);
return item;
}
babel处理es6语法
@babel/preset-env
我们写段es6语法,经过webpack编译一下,看下编译后的内容。
let arr = [
new Promise(() => {
console.log("promise");
}),
];
arr.map((item) => {
return item;
});
打开chrome浏览器可以执行编译后的代码,因为chrome浏览器更新的比较及时,但有很多浏览器比如IE,是不认识ES6语法的。为了兼容更多的浏览器,我们可以使用babel将ES6语法变为ES5的语法。
npm install --save-dev babel-loader @babel/core
npm install @babel/preset-env --save-dev
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
};
// babel.config.json
{
"presets": ["@babel/preset-env"]
}
babel-loader是babel和js做通讯的桥梁,但是它并不会将js中的ES6语法转换为ES5语法,语法转换还需要@babel/preset-env。具体babel转换的规则,需要我们定义一个babel.config.json
去定义babel转换的规则。
babel会不断升级,可查看官方指南进行配置 www.babeljs.cn/setup#insta…
查看下经过@babel/preset-env
编译的内容,
@babel/polyfill
ES6的语法确实被编译了,箭头函数改为了function,但是它并没有编译ES6的api,比如Promise,map函数,Array.from,Object.assign。所以我们需要@babel/polyfill
来处理ES6的api,但根据babel官网的说明,@babel/polyfill
在Babel 7.4.0后被废弃,我们需要使用新的依赖。
yarn add core-js regenerator-runtime
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
// 并不需要兼容全部低版本的代码
// 根据业务代码的需要有选择性的添加
useBuiltIns: "usage",
// 需要兼容的浏览器
targets: {
chrome: "58",
ie: "11",
},
// corejs必须与useBuiltIns: "usage"一起使用,3表示core-js版本号
corejs: 3,
},
],
],
},
},
},
],
},
};
我也是根据官网,并且参考一些博主的文章后配置的。
@babel/polyfill
官网: www.babeljs.cn/docs/babel-…babel-preset-env
www.babeljs.cn/docs/babel-…- 配置文章: blog.csdn.net/A1333006927…
再来看下配置后编译的文件,可以看到,Promise也被编译了。
引入@babel/polyfill
之前打包后的文件时91KB
经过@babel/polyfill
编译后的文件是 316KB,多出来的大小就是为了兼容底版本,对Promise,map等ES6 api的实现,把这些实现加到bundle.js中,所以打包后的文件就变大了。
我们的配置项中的useBuiltIns: "usage"
,代表可根据业务代码来编译内容。我们去掉promise的使用的话,可看到bundle.js的文件一下子变小了。此处证明我们的useBuiltIns: "usage"
是生效的了。
我们不断编写babel文件会使config的代码很冗长,我们可以单独写一个babel的config,把use
的option
选项移动到babel.config.js
,编译的时候会自动检索babel.config.js
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
],
},
};
// babel.config.js
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"targets": {
"chrome": "58",
"ie": "11"
},
"corejs": 3
}
]
]
}
babel处理react jsx语法
Babel 能够转换 JSX 语法
// 下载react
npm i react react-dom
// 编写jsx语法
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<div>hello jsx</div>, document.getElementById("root"));
// 下载转化jsx的babel
npm install --save-dev @babel/preset-react
// 配置babel,babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"targets": {
"chrome": "58",
"ie": "11"
},
"corejs": 3
}
],
"@babel/preset-react"
]
}
babel的转化顺序也是从右到左,先讲react代码转换为js,再把ES6的代码转为ES5。
结尾
以上就是搭建一个webpack的所有内容了,这只是一个简单的webpack demo,并不涉及webpack的高阶特性和优化。想要了解webpack还是要学会自己读文档,然后跟着文档做一遍,相信做过一次的朋友一定会加深对webpack的理解。
本文的github地址,有需要的同学可以自取代码。 github.com/orangemiaos…