# 万字长文系统梳理 Webpack 基础(上)
# Webpack 简介
# 概述
;webpack
是一个开源的JavaScript
模块打包工具,核心功能是解决模块之间的依赖,把各个模块按照特定规则和顺序组织在一起,最终合并为一个或者多个JS
文件,整个过程被称为模块打包。
模块即一个日期处理的npm
包或者一个提供工具方法的JS
文件等。设计程序结构时,将所有代码堆砌到一起会非常糟糕,更好的方式是按照特定的功能将其拆分为多个代码段,每个代码段实现一个特定的功能,最后再通过接口将其组合。
;JavaScript
设计初仅是小型的脚本语言,远没有考虑到会用其实现复杂的场景,模块化也就显得多余了。伴随技术的发展,HTML
页面中通常会引入多个script
文件,但是此做法有很多缺陷。
首先需手动维护script
文件的加载顺序。页面的多个script
之间通常会有依赖关系,这些依赖关系一般是隐式的,不添加注释很难清晰地指明谁依赖谁,并且加载文件过多时很容易出现问题。
其次是每一个script
文件都意味着向服务器请求一次静态资源,过多的请求会拖慢网页的渲染速度。并且每个script
标签中顶层作用域即全局作用域,直接在代码中进行变量或函数声明,会造成全局作用域的污染。
而模块化方式则通过导入和导出语句可以清晰地看到模块间的依赖关系。模块可以借助工具进行打包,在页面中只加载合并后的资源文件,以此减小网络开销。并且多个模块之间的作用域是隔离的,彼此不会有命名冲突。
# 安装
;webpack
安装方式包括全局安装和本地安装,全局安装会绑定一个命令行环境变量,一次安装、处处运行。本地安装则会添加其成为项目的依赖,只能在项目内部使用。
若采用全局安装,在项目多人协作时,由于每个人系统中的webpack
版本不同,可能导致输出结果不一致。并且部分依赖于webpack
的插件会调用项目内部的webpack
模块,此种情况下仍然需要本地安装webpack
。
安装指定版本的webpack
,注意webpack4+
版本需安装webpack-cli
命令行工具。
npm i webpack@4.29.4 webpack-cli@3.2.3 --save-dev
安装成功后可查看webpack
和webpack-cli
版本号。注意webpack
安装在本地,因此无法在命令行内使用webpack
指令,项目内部只能使用npx webpack
的形式。
npx webpack -v
npx webpack-cli -v
# 打包
根目录下创建index.html
、index.js
、fn.js
。
// index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<script src="./dist/bundle.js"></script>
</body>
</html>
// index.js
import fn from './fn.js'
fn()
// fn.js
export default function () {
document.write('Hello World')
}
控制台运行如下打包命令,浏览器打开index.html
显示Hello World
。其中entry
为资源打包的入口,webpack
由此开始进行模块依赖的查找,获取到项目中index.js
和fn.js
两模块,output-filename
为输出资源名,打包后出现dist
目录下bundle.js
文件,mode
为打包模式,包括development
、production
、none
三种模式,开发环境一般为development
模式。
可运行npx webpack -h
查看webpack
配置项以及相应的命令行参数。
npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
每次打包都要输入一段冗长的命令,可编辑package.json
文件,添加脚本命令简化输入。其中scripts
是npm
提供的脚本命令功能,可直接使用由模块所添加的指令(如webpack
取代之前的npx webpack
),运行npm run build
然后再打开index.html
。
{
...
"scripts": {
"build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
}
...
}
当项目需要的配置越来越多时,命令中则要添加更多的参数,后期维护非常困难。webpack
的默认配置文件为webpack.config.js
,然后再去掉package.json
中配置的打包参数。
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js',
},
mode: 'development',
}
// package.json
{
...
"scripts": {
"build": "webpack"
}
...
}
当修改代码时需要重新执行npm run build
打包再打开index.html
,webpack
提供了更加便捷的开发工具 webpack-dev-server (opens new window) 提高开发效率,当其发现项目源文件进行了更新操作就会自动刷新live-reloading
浏览器,显示更新后的内容。
npm i webpack-dev-server@3.1.14 --save-dev
添加dev
脚本并配置webpack.config.js
。webpack-dev-server
主要工作是将打包结果放在内存中,并不会实际写入文件,每次webpack-dev-server
接收到请求时都只是将内存中的打包结果返回给浏览器。可通过删除dist
目录来验证,即便dist
目录不存在,刷新页面功能仍然是正常的。
// package.json
{
...
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
}
...
}
// webpack.config.js
module.exports = {
...
devServer: {
publicPath: '/dist',
},
}
# 模块
# CommonJs
;node.js
将javascript
语言用于服务端编程,由于在服务器端要与操作系统和其他应用程序互动,模块化是必需的,同时node.js
采用了部分commonjs
的规范并在其基础上进行了一些调整。
有了服务端模块以后,客户端模块也由此开始发展。但是服务端与客户端的模块存在较大差异,服务端所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间,但是对于浏览器,会存在一个非常严重的问题,由于模块都在服务端,等待时间就取决于网速的快慢,若等待时间较长,浏览器就会处于假死状态。因此,浏览器端的模块,不能采用同步加载,只能采用异步加载。
导出是一个模块向外暴露自身的唯一方式,commonjs
中通过exports
或者module.exports
导出模块中的内容。其内部机制将exports
指向了module.exports
,而module.exports
在初始化时为空对象。可以理解为commonjs
在每个模块的首部默认添加了如下代码。注意不要直接给exports
赋值,会导致其指向断裂,也不要两者混合运用。另外导出语句不代表模块的末尾,在exports
或者module.exports
后面的代码会照常执行,但是通常将其放在模块的末尾。
var module = {
exports: {},
}
var exports = module.exports
;commonjs
中使用require
导入模块,require
导入分为两种情况,若模块是第一次被导入,会执行其内部代码,同时导出指定的内容,若模块已被导入过,模块内部代码不会再执行,而是直接导出上次执行后得到的结果。
模块内部module
对象有一个属性loaded
用于记录此模块是否被加载过,默认值为false
,当模块第一次被加载或执行后会置为true
,再次加载检查到module.loaded
为true
时将不会再执行。如下执行node index.js
将输出false
true
。
// func.js
console.log(module.loaded)
module.exports = function () {
return module.loaded
}
// index.js
const func = require('./func.js')
console.log(func())
# ES6 Module
;ES6 Module
也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。ES6 Module
会自动采用严格模式,不管模块开头是否有'use strict'
,都会采用严格模式。
导出模块包括默认导出和命名导出,导入命名导出的模块需要解构出变量,导入默认导出的模块任意变量名均可接收。注意导入变量的效果相当于在当前作用域下声明了此变量,并且不可对其修改,也就是所有导入的变量都是只读的。
# CommonJS 与 ES6 Module 区别
两者最本质的区别在于CommonJs
对模块的依赖是动态的,即模块依赖关系的建立发生在代码运行阶段,而ES6 Module
对模块的依赖则是静态的,即模块依赖关系的建立发生在代码编译阶段。
;CommonJs
的模块路径可以动态指定,支持传入表达式,也可以通过if
语句判断是否加载某个模块。因此,在CommonJs
模块被执行前,并没有办法确定明确的依赖关系,故模块导入、导出发生在代码的运行阶段。
;ES6 Module
导入、导出语句均为声明式,不支持导入路径为表达式,且导入、导出语句必须位于模块的顶层作用域。即ES6 Module
是一种静态的模块结构,在ES6
代码编译阶段就能分析出模块的依赖关系。
;ES6 Module
相对于CommonJS
具备如下优势。
- 死代码检测和排除,可以用静态分析工具检测出哪些模块没有被调用过。引用工具类库时,工程中一般只用到一部分组件或接口,可能会将其完全加载进来,而未被调用的模块代码永远不会执行,成为死代码。静态分析工具可以在打包时去掉未曾使用过的模块,以减小打包资源体积
- 模块变量类型检查,
ES6 Module
的静态模块结构有助于确保模块之间传递的值或接口类型是正确的 - 编译器优化,
ES6 Module
支持直接导入变量,减少引用层级,程序效率更高
# 拷贝与绑定
若CommonJs
的module.exports
导出的是基本数据类型,则导入时只是值的拷贝。如下运行后输出结果为1
1
2
,由于index.js
中的count
是对add.js
中的count
的值的拷贝,调用add
函数时,虽然改变了add.js
中count
的值,但是并不会对导入时创建的count
拷贝造成影响。
// add.js
var count = 1
module.exports = {
count,
add() {
count++
},
get() {
return count
}
}
// index.js
const { count, add, get } = require('./add.js')
console.log(count)
add()
console.log(count)
console.log(get())
若module.exports
导出的是引用数据类型,则导入时是引用的拷贝。如下运行后输出结果为{count: 1}
{count: 2}
true
,由于index.js
中的object
是对add.js
中的object
的引用的拷贝,调用updateObject
改变了add.js
中object.count
的值,则index.js
中object.count
也会随之改变。
// add.js
const object = {
count: 1
}
module.exports = {
object,
updateObject() {
object.count++
},
getObject() {
return object
}
}
// index.js
const { object, updateObject, getObject } = require("./add.js")
console.log(object)
updateObject()
console.log(object)
console.log(getObject() === object)
;ES6 Module
导入的变量始终指向的是模块内部的变量,使用时可以获得变量的最新值。如下运行后输出结果为1
2
2
,index.js
中的count
与add.js
中的count
之间建立了一种绑定关系(binding
),可实时获取到绑定的最新值。
// add.js
export var count = 1
export function add() {
count++
}
export function get() {
return count
}
// index.js
import { count, add, get } from "./add.js"
console.log(count)
add()
console.log(count)
console.log(get())
注意export default
是不会产生绑定关系的,如下运行后输出结果为1
1
2
。
// add.js
var count = 1
export default count
export function add() {
count++
}
export function get() {
return count
}
// index.js
import count, { add, get } from "./add.js"
console.log(count)
add()
console.log(count)
console.log(get())
首先export default
是一种语法糖,当模块只有一个导出的时候简化代码量。如下为export default
导出原始类型变量count
。
var count = 1
export default count
然后JavaScript
会将变量count
交给内部变量*default*
,然后再重命名为default
导出。
var count = 1
var *default* = count
export { *default* as default }
之前export default
不会产生绑定关系的原因也即是由于语法糖的转换造成的,index.js
中的count
实际上绑定的是add.js
中的内部变量*default*
,而并不是count
。
// add.js
var count = 1
var *default* = count
export { *default* as default }
...
// index.js
import { default as count } from './add.js'
...
故CommonJs
导入模块变量时,仅仅是值或者引用的拷贝。而ES6 Module
导入的变量将始终绑定模块内部的变量,形成一种绑定关系(binding
),注意export default
导出的变量不会产生绑定关系,其原因是由于JavaScript
语法糖的转换造成的。
# 循环依赖
循环依赖是指模块A
依赖于模块B
,同时模块B
又依赖于模块A
。日常开发中工程的复杂度上升到足够规模时,容易出现隐藏的循环依赖。
如下为CommonJs
中的循环依赖,输出结果为module foo exports {}
module bar exports bar.js
。首先index.js
导入并执行foo.js
,foo.js
导入并执行bar.js
,然后bar.js
中导入foo.js
,由于已经导入过foo.js
但是并未执行完毕,导出值此时为默认的空对象,打印结果bar.js
执行完毕。最后执行权交回foo.js
,打印结果流程结束。
// index.js
require('./foo.js')
// foo.js
const bar = require('./bar.js')
console.log('module bar exports ', bar)
module.exports = 'foo.js'
// bar.js
const foo = require('./foo.js')
console.log('module foo exports ', foo)
module.exports = 'bar.js'
;webpack
打包上述代码,可简化为如下。当bar.js
再次导入foo.js
时,直接返回的是installedModules
中的值,此时为空对象。
(function (modules) {
var installedModules = {}
function require(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
var module = (installedModules[moduleId] = {
i: moduleId,
exports: {},
})
modules[moduleId].call(module.exports, module, module.exports, require)
return module.exports
}
return require('./index.js')
})({
'./bar.js': function (module, exports, require) {
const foo = require('./foo.js')
console.log('module foo exports ', foo)
module.exports = 'bar.js'
},
'./foo.js': function (module, exports, require) {
const bar = require('./bar.js')
console.log('module bar exports ', bar)
module.exports = 'foo.js'
},
'./index.js': function (module, exports, require) {
require('./foo.js')
},
})
如下为ES6 Module
的循环依赖,输出结果为module foo exports undefined
module bar exports bar.js
。bar.js
也无法获取到foo.js
的导出值,与CommonJS
默认导出空对象不同,此时为undefined
。
// index.js
import foo from './foo.js'
// foo.js
import bar from './bar.js'
console.log('module bar exports ', bar)
export default 'foo.js'
// bar.js
import foo from './foo.js'
console.log('module foo exports ', foo)
export default 'bar.js'
利用ES6 Module
的绑定特性,改造循环绑定。首先index.js
导入并执行foo.js
,foo.js
导入并执行bar.js
,bar.js
导入foo.js
,由于此时foo.js
未执行完,foo
仍然为undefined
,然后bar.js
导出函数,执行权交回foo.js
,foo.js
再导出函数,执行权交回index.js
,最后执行foo
函数,由于绑定关系会执行foo.js
内函数,将invoked
置为true
,再执行bar.js
函数,bar
函数内部又再执行foo
函数,但是由于foo.js
内invoked
为true
,foo
函数不在执行,故执行顺序为foo
bar
foo
。
// index.js
import foo from './foo.js'
foo()
// foo.js
import bar from './bar.js'
var invoked = false
export default function () {
if (!invoked) {
invoked = true
bar()
console.log('module bar exports ', bar)
}
}
// bar.js
import foo from './foo.js'
export default function () {
console.log('module foo exports ', foo)
foo()
}
# AMD
;AMD
即支持浏览器端模块化的规范,其加载模块的方式是异步的,加载模块时不会影响后面的语句执行。RequireJS
实现了AMD
的规范。
如下定义了一个AMD
模块,其中目录下包括index.html
和index.js
、foo.js
、bar.js
模块。require.js
模块可为CDN
方式引入,data-main
指定主模块文件。
;require
引入模块,参数分别为加载的模块数组、模块加载完成后执行的回调函数。
;define
定义模块,参数分别为当前模块名、当前模块的依赖、模块的导出值(函数或者对象,若为函数则导出函数的返回值,若为对象则直接导出)。
;AMD
与同步加载的模块标准相比语法显得冗长,其加载方式也不如同步清晰。
// index.html
...
<body>
<p>hello world</p>
<script src="./require.js" data-main="./index.js"></script>
</body>
// index.js
require.config({
paths: {
foo: './foo',
bar: './bar',
},
})
require(['foo'], function (foo) {
console.log('module foo exports ', foo)
}, function (err) {
console.log(err)
})
// foo.js
define('foo', ['bar'], function (bar) {
console.log('module bar exports ', bar)
return 'foo.js'
})
// bar.js
define('bar', function () {
return 'bar.js'
})
# CMD
;CMD
是另一种浏览器端模块化的规范,也是异步加载模块,SeaJs
实现了CMD
的规范。
;AMD
的多个依赖模块的执行顺序和书写顺序并非一致,取决于网路速度,哪个先下载下来,哪个先执行,而主逻辑在所有依赖加载完成后才执行。
;CMD
在遇到require
语句时才执行对应的模块,其执行顺序和书写顺序是完全一致的。
;AMD
是依赖前置,CMD
则是就近依赖。RequireJS
和SeaJs
都是在执行模块前预加载了依赖的模块,只是所依赖模块的执行时机不同,RequireJs
加载时执行,而Seajs
是使用时执行。
;RequireJs
使用依赖数组,根据配置信息查找每项对应的实际路径来预加载。而Seajs
使用正则表达式捕捉内部的require
字段,也根据配置信息查找文件的实际路径来预加载。
// index.html
...
<body>
<p>hello world</p>
<script src="./sea.js"></script>
<script src="./index.js"></script>
</body>
// index.js
seajs.config({
paths: {
foo: './foo',
bar: './bar',
},
})
seajs.use(['foo'], function (foo) {
console.log('module foo exports ', foo)
})
// foo.js
define(function (require, exports, module) {
var bar = require('bar')
console.log('module bar exports ', bar)
module.exports = 'foo.js'
})
// bar.js
define(function (require, exports, module) {
module.exports = 'bar.js'
})
# UMD
;UMD
是一种JavaScript
通用模块定义规范,其能够在JavaScript
所有运行环境中运行。
# 单模块
# 非模块环境
非模块化环境一般通过全局对象挂载属性。其中foo.js
为立即执行函数,factory
工厂函数返回值挂载到全局对象上,root
为全局对象,其值为window
或者global
,由运行环境决定。
// index.html
...
<body>
<p>hello world</p>
<script src="./foo.js"></script>
<script src="./index.js"></script>
</body>
// foo.js
(function (root, factory) {
root.foo = factory()
})(this, function () {
return 'foo.js'
})
// index.js
console.log(foo)
# AMD
;AMD
方式则要满足AMD
规范。
// index.html
...
<body>
<p>hello world</p>
<script src="./require.js" data-main='./index.js'></script>
</body>
// index.js
require.config({
paths: {
foo: './foo',
},
})
require(['foo'], function (foo) {
console.log(foo)
}, function (err) {
console.log(err)
})
// foo.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define('foo', factory)
} else {
root.foo = factory()
}
})(this, function () {
return 'foo.js'
})
# UMD
;UMD
即支持非模块环境、CommonJS
、AMD
、CMD
规范的模块。
// foo.js
(function (root, factory) {
if (typeof module === 'object') {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define('foo', factory)
} else if (typeof define === 'function' && define.cmd) {
define(function (require, exports, module) {
module.exports = factory()
})
} else {
root.foo = factory()
}
})(this, function () {
return 'foo.js'
})
# 多模块
;UMD
模块依赖其他UMD
模块时。
# AMD
// index.html
...
<body>
<p>hello world</p>
<script src="./require.js" data-main='./index.js'></script>
</body>
// index.js
require.config({
paths: {
foo: './foo',
bar: './bar',
},
})
require(['foo'], function (foo) {
console.log('module foo exports ', foo)
})
// foo.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define('foo', ['bar'], factory)
} else {
root.foo = factory(root.bar)
}
})(this, function (bar) {
console.log('module bar exports ', bar)
return 'foo.js'
})
// bar.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define('bar', factory)
} else {
root.bar = factory()
}
})(this, function () {
return 'bar.js'
})
# UMD
// foo.js
(function (root, factory) {
if (typeof module === 'object') {
var bar = require('./bar')
module.exports = factory(bar)
} else if (typeof define === 'function' && define.amd) {
define('foo', ['bar'], factory)
} else if (typeof define === 'function' && define.cmd) {
define(function (require, exports, module) {
var bar = require('bar')
module.exports = factory(bar)
})
} else {
root.foo = factory(root.bar)
}
})(this, function (bar) {
console.log('module bar exports ', bar)
return 'foo.js'
})(
// bar.js
(function (root, factory) {
if (typeof module === 'object') {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define('bar', factory)
} else if (typeof define === 'function' && define.cmd) {
define(function (require, exports, module) {
module.exports = factory()
})
} else {
root.bar = factory()
}
})(this, function () {
return 'bar.js'
})
)
# 资源输入输出
# 概念
module
:所有的js
、css
、png
等文件都是module
模块chunk
:代码块,webpack
打包过程中入口文件依赖的模块,模块再依赖其他模块,以上模块组成的集合被称为chunk
bundle
:包文件,webpack
打包生成的源文件
;module
、chunk
、bundle
实质就是同一套代码逻辑在不同转换场景下的不同名字。编写阶段,每一个单文件都是一个module
模块。打包阶段,根据入口文件所依赖的所有模块组成的集合为chunk
代码块。打包输出后每一个源文件都是bundle
包文件。
形象化一个打包场景来描述上述概念,其中webpack.config.js
配置文件如下,插件的作用仅是单独抽离出css
文件。
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
index: './index.js',
main: './main.js',
},
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
}
入口文件分别为index.js
和main.js
,其中index.js
引入了index.css
的样式和utils
工具类的一个函数,mian.js
是一个单独的模块。
// index.js
import './index.css'
import { log } from './utils.js'
log('index.js')
// index.css
p {
background: blue;
}
// utils.js
export function log(val) {
console.log(val)
}
// main.js
console.log('main.js')
注意可能由于部分插件和loader
版本与webpack
版本的依赖差异,导致打包出错,如下为可行的package.json
文件。
// package.json
{
...
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3",
"css-loader": "^0.28.9",
"mini-css-extract-plugin": "^0.5.0"
}
}
执行打包命令完成,结果如下。
初始模块为index.css
、utils.js
、index.js
和main.js
,打包阶段index.js
、index.css
和utils.js
均构成代码块chunk 0
,main.js
构成代码块chunk 1
,打包输出后的index.css
、index.js
和main.js
均为包文件。
—— module ———— chunk ———— bundle
index.css index.css
\ /
utils.js —— chunk 0 —— index.js
/
index.js
main.js —— chunk 1 —— main.js
# 入口 (entry)
;entry
即入口文件路径,webpack
基于此开始进行打包。
若传入一个字符串或字符串数组,chunk
会被命名为main
。
若传入一个对象,则每个属性的键会是chunk
的名称。
# 字符串类型
module.exports = {
entry: './index.js',
...
}
# 数组类型
module.exports = {
entry: ['./main.js', './index.js'],
...
}
# 对象类型
module.exports = {
entry: {
index: './index.js',
main: './main.js',
},
...
}
# 函数类型
函数类型返回以上任意类型均可,其优点在于可在函数体内部添加部分动态逻辑来获取工程入口,函数也支持返回Promise
对象来进行异步操作。
module.exports = {
entry: () => './index.js',
}
module.exports = {
entry: () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('./index.js')
}, 5000)
}),
}
# 出口 (output)
# filename
即输出资源的文件名,其形式为字符串,可以为相对路径,若路径中的目录不存在则webpack
在输出资源时会创建此目录。如下打包完成后会在根目录创建build
文件夹。
module.exports = {
...
output: {
filename: '../build/index.js',
},
}
;webpack
也支持类似模板语言的形式动态地生成文件名。如下filename
中的name
会被替换为chunk name
,即最终项目生成的资源是index.js
和main.js
。
module.exports = {
entry: {
index: './index.js',
main: './main.js',
},
output: {
filename: '[name].js',
},
...
}
;filename
部分常用配置项模板变量如下。其作用是当有多个chunk
存在时对不同的chunk
进行区分。另一个作用是控制客户端缓存,chunkhash
与chunk
内容直接相关,当chunk
内容改变时同时会引起资源文件名的更改,用户在下一次请求资源文件时便会立即下载新的版本而不会使用本地缓存。
hash
:webpack
打包所有资源生成的hash
id
:当前chunk
的id
chunkhash
:当前chunk
内容的hash
# path
;path
用于指定资源的输出路径,且值必须为绝对路径,如下将资源输出位置设置为工程的lib
目录。webpack4+
版本默认为dist
目录,若非修改输出路径,否则不用单独配置。
const path = require('path')
module.exports = {
...
output: {
...
path: path.join(__dirname, 'lib'),
},
}
# publicPath
;path
用来指定资源的输出位置,publicPath
用来指定资源的请求位置。
请求位置即由js
或者css
所请求的间接资源路径,诸如html
加载的script
,或者异步加载的js
、css
请求的图片等,publicPath
即指定上述资源的请求位置。
形象化一个打包场景来描述上述情况,其中根目录下包括index.html
、index.js
等文件。index.html
为模板文件,插件的作用是将打包后的js
文件插入到模板中。
// index.html
<html lang="zh-CN">
...
<body>
<div id="app">hello world</div>
</body>
</html>
// index.js
console.log('index.js')
// package.json
{
...
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.29.4",
"webpack-cli": "^3.2.3",
"html-webpack-plugin": "^3.2.0"
}
}
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './index.js',
output: {
filename: 'index.js',
publicPath: '/lib/',
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
}
运行npm run build
打包后,当前根目录下生成dist
文件夹,其中包括index.html
和index.js
。
// index.html
<html lang="zh-CN">
...
<body>
<div id="app">hello world</div>
<script type="text/javascript" src="/lib/index.js"></script>
</body>
</html>
;VS Code
编辑器安装Live Sever
插件,用来在本机上模拟index.html
部署到服务器上的真实场景,index.html
内部右击Open with Live Server
开启本地服务,打开浏览器调试界面Network
项,可查看如下js
请求,其中Request URL
即为资源的请求位置。
;publicPath
的不同将最终导致资源的请求地址也不同,其中publicPath
分为如下三种形式,当前index.html
文件地址为http://127.0.0.1:5500/dist/index.html
,资源名称为index.js
。
html
相关:资源路径与html
文件目录关联,即资源路径为html
目录路径加上publicPath
和文件名
———— publicPath ———————— Request URL
'' http://127.0.0.1:5500/dist/index.js
'./js' http://127.0.0.1:5500/dist/js/index.js
'../assets/' http://127.0.0.1:5500/assets/index.js
- 根目录相关:若
publicPath
以'/'
开始,则资源路径以页面根目录路径为基础
———— publicPath ———————— Request URL
'/' http://127.0.0.1:5500/index.js
'/js' http://127.0.0.1:5500/js/index.js
'/dist/' http://127.0.0.1:5500/dist/index.js
- 绝对路径:绝对路径的情况一般是将静态资源放在
CDN
上面
———— publicPath ———————— Request URL
'https://cdn.com/' https://cdn.com/index.js
'//cdn.com/' http://cdn.com/index.js
# devServer.publicPath
;devServer
的配置中也有publicPath
,其作用是指定devServer
的静态资源服务路径,或者说指定资源打包到内存中的位置。
当启动devServer
资源会被打包到内存中,devServer
会到内存中查找打包好的资源文件,然后再去本地目录中查找内容,devServer.contentBase
可控制它去哪访问本地目录的资源。
;contentBase
默认为当前的工作目录,若查找不到内存中的资源,则会到contentBase
中查找。
若不指定devServer.publicPath
,devServer
会获取output.publicPath
的值,为了避免开发环境和生产环境产生不一致,一般保持devServer.publicPath
与output.publicPath
相同,或者不指定devServer.publicPath
。
# 预处理器
;webpack
只能处理JavaScript
和JSON
文件,对于其他资源例如css
、图片,或者其他的语法集ts
等是没有办法加载的。loader
让webpack
能够去处理其他类型的文件,并将它们转换为webpack
能够接收的模块加载进来,loader
实质上是做一个预处理的工作。
每个loader
本质上都是一个函数,大致形式为output = loader(input)
,input
为即将被转换的模块,output
为转换后的模块,使用babel-loader
将ES6+
代码转换为ES5
时,上述形式为ES5 = babel-loader(ES6+)
。loader
可以是链式的,即某一个loader
的输出可以作为其他loader
的输入,其形式为output = loaderA(loaderB(input)))
。
;loader
包括test
和use
两个属性,test
用于识别哪些文件会被转换,use
定义在进行转换时应该使用哪一个loader
。
# 配置项
# options
;loader
通常会提供一些配置项,一般通过options
来将其传入,具体的loader
不同其提供的options
也不同。
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
...
},
},
},
],
},
}
# exclude/include
排除或包含指定目录下的模块,可接受正则表达式或文件绝对路径字符串。如下为排除node_modules
下的模块。
{
test: /\.js$/,
exclude: /node_modules/,
...
}
;include
表示只包含匹配到的模块,如下为只包含src
目录。
{
test: /\.js$/,
include: /src/,
...
}
若include
和exclude
同时存在,exclude
的优先级更高。如下表示排除node_modules
下所有模块。
{
test: /\.js$/,
exclude: /node_modules/,
include: /node_modules\/lodash/,
...
}
# resource/issuer
;resource
是被加载者,而issuer
是加载者,两者可用于更加精确地确定模块规则的作用范围。
如下为仅src
目录下的js
文件可以引用css
。
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
issuer: {
test: /\.js$/,
include: /src/,
},
},
上述test
、include
、exclude
配置项分布于不同层级上,可读性较差,更好的方式是添加resource
对象将外层的配置包裹起来。
如下为除了node_modules
下的js
,其余js
都能引用css
文件。仅src
目录下的css
文件可以被引用。
{
use: ['style-loader', 'css-loader'],
issuer: {
test: /\.js$/,
exclude: /node_modules/,
},
resource: {
test: /\.css$/,
include: /src/,
},
},
# enforce
用来指定loader
的种类,只接收pre
或post
两种类型。webpack
中loader
执行顺序可分为pre
(优先处理)、inline
(其次处理)、normal
(正常处理)、post
(最后处理),上述直接定义的loader
都属于默认normal
类型,post
和pre
需使用enfore
来指定。
如下表示eslint-loader
将在所有正常的loader
之前执行。实际不用指定enforce
只要保证loader
的执行顺序是正确的即可,配置enforce
主要目的是使模块规则更加清晰可读。
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader',
},
],
# 常用 loader
# sass-loader
;sass-loader
是scss
类型文件的预处理器,处理其语法并编译为css
。sass-loader
核心依赖于node-sass
,而node-sass
又依赖于node
,安装时注意node-sass
与node
之间的版本支持。
之后css-loader
处理css
的各种加载语法,将@imoprt
或者url()
函数转换为require
。仅仅是把css
模块加载到js
代码中,并未实际使用。
最后由style-loader
将js
中的样式字符串包装成style
标签插入页面。
上述处理场景如下,根目录包括index.js
、index.scss
、index.html
、package.json
等。
// package.json
{
...
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
},
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"html-webpack-plugin": "3.2.0",
"css-loader": "^0.28.9",
"style-loader": "^0.19.0",
"node-sass": "^4.7.2",
"sass-loader": "^6.0.7"
}
}
// index.scss
$color: red;
p {
color: $color;
}
// index.js
import './index.scss'
// index.html
<html lang="zh-CN">
...
<body>
<p>hello world</p>
</body>
</html>
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './index.js',
output: {
filename: 'index.js',
},
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
mode: 'development',
}
运行npm run dev
,打开页面可查看css
样式已经注入到html
中了。
# babel-loader
;babel-loader
用来处理ES6+
并将其编译为ES5
,使其能够在项目中使用最新的语言特性,也不用关注这些特性在不同平台的兼容性。
安装babel
需同时安装babel-loader
、@babel/core
和@babel/preset-env
。其中@babel/core
是babel-loader
依赖的核心模块,@babel/preset-env
是官方推荐的预制器,可根据用户配置的目标浏览器或者运行环境自动添加所需的插件和补丁来编译ES6+
代码,babel-loader
作为中间桥梁调用@babel/core
的api
来告诉webpack
要如何处理js
。
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
cacheDirectory: true,
presets: [['@babel/preset-env', { modules: false }]],
},
},
;babel-loader
通常会编译所有的js
模块,会严重拖慢打包速度,并且有可能改变第三方模块的原有行为,所以需要exclude
排除node_modules
。
;cacheDirectory
启用缓存机制,当重复打包未改变过的模块时,将会尝试读取缓存,避免产生高性能的重新编译过程。cacheDirectory
接收字符串类型的路径或者true
,为true
时将使用默认的缓存目录node_modules/.cache/babel-loader
。
;@babel/preset-env
会将ES6 Mudule
转化为CommonJs
,将导致webpack
的tree-shaking
失效,可设置modules
为false
关闭此行为,而将ES6 Module
的转化交给webpack
处理。
;babel-loader
也支持外置.babelrc
配置文件,将presets
和plugins
提取出来。
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
# url-loader
;url-loader
用于打包文件类型的模块,对小于limit
阈值的图片进行处理,并将其转换为base64
编码。
将图片转换的base64
编码引入代码中,可以减小请求次数提高页面性能。但是也会增加js
或者html
的文件体积,图片在项目中使用次数较多,每一个引用的地方都会生成base64
编码,从而造成代码的冗余。另一方面浏览器可以缓存http
请求的图片。因此需要平衡考虑,合理设置limit
阈值。
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
},
},
},
# file-loader
;file-loader
也用于打包文件类型的模块,url-loader
不能处理的大于阈值的图片交给file-loader
处理,根据配置将资源输出到打包目录。
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]',
},
},
],
},
# vue-loader
;vue-loader
用来处理vue
文件,提取出其中的template/script/style
代码,再分别交给对应的loader
处理。其中css-loader
处理style
样式代码,vue-template-compiler
负责将template
模板编译为render
渲染函数,vue-loader
默认支持ES6
,每个vue
组件可生成css
作用域等。
使用vue-loader
场景如下,根目录下包括index.html
、index.js
、App.vue
等文件。
// package.json
{
...
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
},
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"html-webpack-plugin": "3.2.0",
"css-loader": "^0.28.9",
"vue": "^2.5.13",
"vue-loader": "^14.1.1",
"vue-template-compiler": "^2.5.13"
}
}
// index.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App),
})
// index.html
<html lang="zh-CN">
...
<body>
<div id="app"></div>
</body>
</html>
// App.vue
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
name: 'App',
data() {
return {
title: 'hello world',
}
},
}
</script>
<style lang="css">
h1 {
color: blue;
}
</style>
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './index.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
mode: 'development',
}
运行npm run dev
,App.vue
被挂载到了div#app
元素上,h1
中的模板也被渲染为hello world
。
# 自定义 loader
# 初始化
自定义实现一个loader
,为所有js
文件启用严格模式,即在其头部添加'use strict'
。
创建strict-loader
目录,执行npm init
初始化目录,创建loader
主体文件index.js
。
// index.js
module.exports = function (content) {
var useStrictPrefix = '"use strict"\n\n'
return useStrictPrefix + content
}
;webpack
工程引用strict-loader
,其中use.loader
通过绝对路径引用strict-loader
,可以随时修改loader
中的源码调试。
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'F:/strict-loader',
},
},
],
},
devtool: 'none',
mode: 'development',
}
// package.json
{
...
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3"
}
}
// index.js
console.log('hello world')
工程执行npm run build
,打包输出后的部分源码如下。
# 启用缓存
当输入的文件和其他依赖没有变化时,应该直接使用缓存,而不是重复进行转换的工作。
// strict-loader/index.js
module.exports = function (content) {
if (this.cacheable) {
this.cacheable()
}
var useStrictPrefix = '"use strict"\n\n'
return useStrictPrefix + content
}
# options 参数
;loader
配置项可通过use.options
传递进来。需要安装loader-utils
依赖库,用其提供的一些帮助函数。
npm i loader-utils@1.1.0 --save
;loader
获取options
方式如下。
// strict-loader/index.js
var loaderUtils = require('loader-utils')
module.exports = function (content) {
...
var options = loaderUtils.getOptions(this) || {}
console.log('options', options)
...
}
# 样式处理
# 分离样式文件
;style-loader
将样式字符串包装为style
标签插入页面,但是在生产环境则希望样式存在于css
文件中而不是style
标签中,因为文件更有利于客户端进行缓存。
;webpack4-
主要采用extract-text-webpack-plugin
插件用于提取样式到css
文件。
# 单样式
根目录下包括index.html
、index.js
、index.css
等文件。如下将index.js
打包到index.html
中,其中webpack.config.js
中ExtractTextPlugin.extract
中use
用于指定在提取样式之前采用哪些loader
来进行预处理,fallback
用于指定当插件无法提取样式时所采用的loader
,new ExtractTextPlugin
参数定义输出文件的名称。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: './index.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
},
],
},
plugins: [
new ExtractTextPlugin('index.css'),
new HtmlWebpackPlugin({
template: './index.html',
}),
],
mode: 'development',
}
// package.json
{
...
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
},
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"html-webpack-plugin": "3.2.0",
"css-loader": "^0.28.7",
"style-loader": "^0.19.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0"
}
}
// index.html
<html lang="zh-CN">
...
<body>
<p>hello world</p>
</body>
</html>
// index.js
import './index.css'
// index.css
p {
color: blue;
}
运行npm run dev
,可查看提取的文件被引用至index.html
中。
# 多文件
当存在多个入口文件,且不同入口文件引入了不同的css
样式,提取多个css
样式如下。其中根目录下包括foo.js
、foo.css
、bar.js
和bar.css
。
// foo.js
import './foo.css'
// foo.css
p {
color: blue;
}
// bar.js
import './bar.css'
// bar.css
h5 {
color: red;
}
// index.html
<html lang="zh-CN">
...
<body>
<p>hello</p>
<h5>world</h5>
</body>
</html>
;package.json
依赖与单文件一致,webpack.config.js
稍做调整。如下[name].css
中的name
指代的是chunk name
,即entry
为入口分配的名字(foo
、bar
)。
// webpack.config.js
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
},
...
plugins: [
new ExtractTextPlugin('[name].css'),
...
],
}
运行npm run dev
,可查看提取的多文件被引用至index.html
中。
若index.js
中通过import()
异步加载了foo.js
,foo.js
中加载了foo.css
,那么最终foo.css
只能被同步加载,或者说只能被以style
标签的方式插入到html
中,无法做到按需加载。
# 按需加载
;Webpack4+
则主要采用mini-css-extract-plugin
提取css
样式,可动态插入link
标签的方式按需加载。
根目录下包括index.js
、index.css
、foo.js
和foo.css
等文件。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './index.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
mode: 'development',
}
// package.json
{
...
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack"
},
"devDependencies": {
"webpack": "4.29.4",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"html-webpack-plugin": "3.2.0",
"css-loader": "^0.28.7",
"mini-css-extract-plugin": "^0.5.0"
}
}
// index.js
import './index.css'
setTimeout(() => {
import('./foo.js')
}, 2000)
// index.css
p {
color: blue;
}
// foo.js
import './foo.css'
// foo.css
h5 {
color: red;
}
// index.html
<html lang="zh-CN">
...
<body>
<p>hello</p>
<h5>world</h5>
</body>
</html>
运行npm run dev
,2s
后head
中将会动态插入link
标签和script
标签。
# postcss
;postcss-loader
不算是css
的预处理器,仅仅是一个运行插件的平台,其工作模式是接收样式源代码交由编译插件处理并输出css
,其中编译插件可以通过配置来指定。
# postcss-loader
;postcss-loader
可以单独使用或者与css-loader
结合使用,当单独使用postcss-loader
时,不建议在css
中使用@import
,否则会产生冗余代码。
;postcss-loader
需要在css-loader
和style-loader
后使用,但是要在其他预处理程序(如sass-loader
)之前使用它。
;postcss
要求有一个单独的配置文件,需要在根目录下创建postcss.config.js
,未添加任何特性暂时返回一个空对象即可。
// webpack.config.js
{
test: /\.css/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
// package.json
"devDependencies": {
...
"css-loader": "^0.28.7",
"postcss-loader": "^2.1.2",
"style-loader": "^0.19.0"
}
// postcss.config.js
module.exports = {}
# autoprefixer
;autoprefixer
为css
自动添加浏览器厂商前缀,根据 Can I Use (opens new window) 的数据决定是否为某一特性添加前缀。
根目录下包括index.css
、index.html
、index.js
和postcss.config.js
等。
// package.json
"devDependencies": {
...
"autoprefixer": "^8.1.0"
}
// postcss.config.js
const autoprefixer = require('autoprefixer')
module.exports = {
plugins: [
autoprefixer({
grid: true,
browsers: ['> 1%', 'last 3 versions', 'ie 8'],
}),
],
}
// index.css
div {
display: grid;
}
// index.js
import './index.css'
打包后为grid
特性添加了IE
前缀。
# stylelint
;stylelint
是一个css
的代码检测工具,类似eslint
,可以为其添加各种规则来统一项目的代码风格质量。
;postcss.config.js
和package.json
、index.css
部分如下,其中declaration-no-important
用于对代码中!important
样式给出警告。
// package.json
"devDependencies": {
...
"postcss-loader": "^2.1.2"
}
// postcss.config.js
const stylelint = require('stylelint')
module.exports = {
plugins: [
stylelint({
config: {
rules: {
'declaration-no-important': true,
},
},
}),
],
}
// index.css
div {
color: red !important;
}
执行打包时会在控制台输出如下警告信息。
# cssnext
;cssnext
可以在项目中使用最新的css
语法特性。
// package.json
"devDependencies": {
...
"postcss-cssnext": "^3.1.0"
}
// postcss.config.js
const postcssCssnext = require('postcss-cssnext')
module.exports = {
plugins: [
postcssCssnext({
browsers: ['> 1%', 'last 2 versions'],
}),
],
}
// index.css
:root {
--highlightColor: #666;
}
p {
color: var(--highlightColor);
}
打包后的结果如下。
# CSS Modules
;CSS Modules
是样式模块化解决方案,其中每个css
拥有单独的作用域,不会和外界发生命名冲突,可以通过相对路径引入css
模块,可以通过composes
复用其他css
模块。
;CSS Modules
不用额外安装模块,开启css-loader
的modules
配置项即可。
其中localIndentName
指明编译出的css
类名风格,name
代指模块名,local
代指原本选择器标识符,hash:base64:5
为5
位hash
值,此hash
值根据模块名和标识符计算而来。
// webpack.config.js
{
test: /\.css/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]',
},
},
],
},
// index.css
.title {
color: red;
}
// index.js
import style from './index.css'
document.write(`<div class='${style.title}'>hello wolrld</div>`)
打包后查看编译出的类名。
# 🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub (opens new window) / Gitee (opens new window)、GitHub Pages (opens new window)、掘金 (opens new window)、CSDN (opens new window) 同步更新,欢迎关注😉~