安装
1.初始化项目使用命令 npm init
//如果是yarn,使用yarn init
//按照提示完成安装
//安装完成之后,会看到目录中生成了一个package.json文件,它相当于npm项目的说明书,里面记录了项目名称、版本、仓库地址等信息。
2.安装Webpack的命令 npm install webpack webpack-cli –-save-dev
3.安装本地开发工具 npm install webpack-dev-server --save-dev
4.在package.json同级目录创建webpack配置文件,名为webpack.config.js
模块打包
CommonJs
导出:
module.exports={}
或
exports.变量名
导入:
let 变量名 = require(文件路径)
require一个模块时会有两种情况:
1.require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。
2.require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
不需要获取其导出的内容,只是想要通过执行它而产生某种作用:
require(文件路径)
ES6 Module
导出:
命名导出:
// 写法1
export const name = 'calculator';
export const add = function(a, b) { return a + b; };
// 写法2 const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };
默认导出只能有一个:
export default {
name: 'calculator',
add: function(a, b) {
return a + b;
}
};
导入:
命名导出的导入:import {} from 文件路径
默认导出的导入:import 变量名 from 文件路径
或
import * as 变量名 文件路径
两种导入方式混合:
import React, { Component } from 'react';
//注意 这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。
复合写法:
解释:把某一个模块导入之后立即导出
export { name, add } from './calculator.js';
注意:符合写法,目前只支持当被导入模块通过命名导出的方式暴露出来的变量
CommonJS与ES6 Module的区别
CommonJS对模块依赖的解决是“动态的”
ES6 Module对模块依赖的解决是“静态的”。
在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。
ES6代码的编译阶段就可以分析出模块的依赖关系
在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;
而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
解释:1.CommonJs里面的值改变不会影响导出的值的改变
2.ES6 Module中的值改变会影响导出的值改变,
3.ES6 Module导出的变量不可修改
循环依赖:
ES6 Module的特性使其可以更好地支持循环依赖
AMD
全称Asynchronous Module Definition(异步模块定义)的缩写,
它是由JavaScript社区提出的专注于支持浏览器端模块化的标准。
它与CommonJS和ES6Module最大的区别在于它加载模块的方式是异步的。
定义:
define(当前模块的id,当前模块的依赖,模块导出的值(函数或对象))
例:
define('getSum', ['calculator'], function(math) {
return function(a, b) {
console.log('sum: ' + calculator.add(a, b));
}
});
导出:
AMD也使用require函数来加载模块,只不过采用异步的形式。
第1个参数指定了加载的模块,第2个参数是当加载完成后执行的回调函数。
例:
require(['getSum'], function(getSum) {
getSum(2, 3);
});
优点:
模块加载是非阻塞性的
解释:执行到require函数时并不会停下来去执行被加载的模块,
而是继续执行require后面的代码,这使得模块加载操作并不会阻塞浏览器。
缺点:
与同步加载的模块标准相比其语法要更加冗长
异步加载的方式并不如同步显得清晰
容易造成回调地狱
UMD
Universal Module Definition,也就是通用模块标准,
它的目标是使一个模块能运行在各种环境下,
不论是CommonJS、AMD,还是非模块化的环境(当时ES6 Module还未被提出)
例:
// calculator.js
(function (global, main) {
// 根据当前环境采取不同的导出方式
if (typeof define === 'function' && define.amd) {
// AMD
define(...);
} else if (typeof exports === 'object') {
// CommonJS module.exports = ...;
} else {
// 非模块化环境
global.add = ...;
}
}(this, function () {
// 定义模块主体 return {...}
}));
解释:UMD其实就是根据当前全局对象中的值判断目前处于哪种模块环境。
当前环境是AMD,就以AMD的形式导出;
当前环境是CommonJS,就以CommonJS的形式导出。
注意:
UMD模块一般都最先判断AMD环境,也就是全局下是否有define函数,
而通过AMD定义的模块是无法使用CommonJS或ES6 Module的形式正确引入的。
npm模块
JavaScript最主流的包管理器有两个——npm和yarn。
两者的仓库是共通的,只是在使用上有所区别。
# 项目初始化
npm init –y
# 安装 lodash
npm install lodash
解释:npm会将lodash安装在工程的node_modules目录下,
并将对该模块的依赖信息记录在package.json中。
加载一个npm模块:
// index.js
import _ from 'lodash';
解释:当Webpack在打包时解析到这条语句,
就会自动去node_modules中寻找名为lodash的模块了,
而不需要我们写出从源文件index.js到node_modules中lodash的路径。
每一个npm模块都有一个入口。
当我们加载一个模块时,实际上就是加载该模块的入口文件。
这个入口被维护在模块内部package.json文件的main字段中。
例:
比如对于前面的lodash模块来说,它的package.json内容如下:
// ./node_modules/underscore/package.json
{
"name": "lodash",
……
"main": "lodash.js"
}
当加载该模块时,实际上加载的是node_modules/lodash/lodash.js。
也可以通过<package_name>/<path>的形式单独加载模块内部的某个JS文件。
例:
import all from 'lodash/fp/all.js';
console.log('all', all);
解释:Webpack最终只会打包node_modules/lodash/fp/all.js这个文件,
而不会打包全部的lodash库,通过这种方式可以减小打包资源的体积。
资源输入输出
资源入口
Webpack通过context和entry这两个配置项来共同决定入口文件的路径。
content
context可以理解为资源入口的路径前缀,在配置时要求必须使用绝对路径的形式。
例:
// 以下两种配置达到的效果相同,入口都为 <工程根路径>/src/scripts/index.js
module.exports = {
context: path.join(__dirname, './src'),
entry: './scripts/index.js',
};
module.exports = {
context: path.join(__dirname, './src/scripts'),
entry: './index.js',
};
配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。
context可以省略,默认值为当前工程的根目录。
entry
可以有多种形式:字符串、数组、对象、函数。
1、字符串类型入口:
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
mode: 'development',
};
2、数组类型入口:
module.exports = {
entry: ['babel-polyfill', './src/index.js'] ,
};
等同于
// webpack.config.js
module.exports = {
entry: './src/index.js',
};
// index.js
import 'babel-polyfill';
3.对象类型入口
想要定义多入口,则必须使用对象的形式。
对象的属性名(key)是chunk name,属性值(value)是入口路径。
例:
module.exports = {
entry: {
// chunk name为index,入口路径为./src/index.js
index: './src/index.js',
// chunk name为lib,入口路径为./src/lib.js
lib: './src/lib.js',
},
};
也可以为字符串或数组
例:
module.exports = {
entry: {
index: ['babel-polyfill', './src/index.js'],
lib: './src/lib.js',
},
};
4.函数类型入口
例:
// 返回一个字符串型的入口
module.exports = {
entry: () => './src/index.js',
};
// 返回一个对象型的入口
module.exports = {
entry: () => ({
index: ['babel-polyfill', './src/index.js'],
lib: './src/lib.js',
}),
};
函数也支持返回一个Promise对象来进行异步操作。
例:
module.exports = {
entry: () => new Promise((resolve) => {
// 模拟异步操作
setTimeout(() => {
resolve('./src/index.js');
}, 1000);
}),
};
资源出口(output)
const path = require('path')
module.exports = {
entry: './src/app.js',
//出口
output: {
filename: 'bundle.js',
//相对路径 filename: './js/bundle.js',
path:path.join(__dirname,'dist'),
publicPath: '/dist/'
},
};
filename
filename:控制输出资源的文件名,其形式为字符串。
还可以是一个相对路径(目录可以不存在,打包时会自动创建)
多入口,为对应产生的每个bundle指定不同的名字
例:
output: {
filename: '[name].js',
//'[hash].js、[chunkhash].js、[id].js、[query].js'可组合使
//例:用'[name]@[chunkhash].js'
},
解释:[name]会被替换为chunk name,因此最后项目中实际生成的资源是vendor.js与app.js
控制客户端缓存,使用[hash]和[chunkhash],当chunk的内容改变时,可以同时引起资源文件名的更改,
从而使用户在下一次请求资源文件时会立即下载新的版本而不会使用本地缓存。
[query]也可以起到类似的效果,只不过它与chunk内容无关,要由开发者手动指定。
注意:更新缓存一般只用在生产环境的配置下,在开发环境中可以不必配置[chunkhash]
path
指定资源输出的位置,要求值必须为绝对路径。
输出位置:打包完成后资源产生的目录,一般将其指定为工程中的dist目录。
publicPath
用来指定资源的请求位置
请求位置:由JS或CSS所请求的间接资源路径。
例:
// 假设当前HTML地址为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "" // 实际路径https://example.com/app/0.chunk.js
publicPath: "./js" // 实际路径https://example.com/app/js/0.chunk.js
publicPath: "../assets/" // 实际路径https://example.com/aseets/0.chunk.js
publicPath的值以“/”开始,代表此时publicPath是以当前页面的host name为基础路径的。
例:
// 假设当前HTML地址为https://example.com/app/index.html
// 异步加载的资源名为0.chunk.js
publicPath: "/" // 实际路径https://example.com/0.chunk.js
publicPath: "/js/" // 实际路径https://example.com/js/0.chunk.js
publicPath: "/dist/" // 实际路径https://example.com/dist/0.chunk.js
publicPath以协议头或相对协议的形式开始时,代表当前路径是CDN相关。
例:
// 假设当前页面路径为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "http://cdn.com/" // 实际路径http://cdn.com/0.chunk.js
publicPath: "https://cdn.com/" // 实际路径https://cdn.com/0.chunk.js
publicPath: "//cdn.com/assets/" 实际路径 //cdn.com/assets/0.chunk.js
webpack-dev-server的配置中也有一个publicPath,它的作用是指定webpack-dev-server的静态资源服务路径。
预处理器
介绍
module.exports = {
// ...
module: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
// css-loader 配置项
},
}
],
exclude: '/node_modules/',
include:'/src'
}],
},
};
test:可接收一个正则表达式或者一个元素为正则表达式的数组,
只有正则匹配上的模块才会使用这条规则。
use:可接收一个数组,数组包含该规则所使用的loader。
引入loader的时候可以通过options将它们传入。
exclude:被正则匹配到的模块都排除在该规则之外。
include:只对正则匹配到的模块生效。
注:exclude和include同时存在时,exclude的优先级更高。
resource与issuer:
resource与issuer可用于更加精确地确定模块规则的作用范围。
在Webpack中,我们认为被加载模块是resource,而加载者是issuer。
enforce:
enforce用来指定一个loader的种类,只接收“pre”或“post”两种字符串类型的值。
enforce的值为“pre”,代表它将在所有正常loader之前执行
enforce的值为“post”,代表它将在所有正常loader之后执行
例:
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader',
}
],
常用loader
babel-loader
作用
处理ES6+并将其编译为ES5
安装
npm install babel-loader @babel/core @babel/preset-env
介绍
各个模块作用:
babel-loader:它是使Babel与Webpack协同工作的模块。
@babel/core:顾名思义,它是Babel编译器的核心模块。
@babel/preset-env:它是Babel官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译ES6+代码。
配置
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [[
'env',
{
modules: false,
}
]],
},
},
}
],
cacheDirectory:启用缓存机制,在重复打包未改变过的模块时防止二次编译,同样也会加快打包的速度。
cacheDirectory可以接收一个字符串类型的路径来作为缓存路径,这个值也可以为true,此时其缓存目录会指向node_modules/.cache/babel-loader。
@babel/preset-env会将ES6 Module转化为CommonJS的形式,这会导致Webpack中的tree-shaking特性失效。
将@babel/preset-env的modules配置项设置为false会禁用模块语句的转化,而将ES6 Module的语法交给Webpack本身处理。
babel-loader支持从.babelrc文件读取Babel配置,因此可以将presets和plugins从Webpack配置文件中提取出来
ts-loader
作用
连接Webpack与Typescript的模块。
通过Typescript和ts-loader,我们可以实现代码类型检查。
更多配置请参考tsloader文档:https://github.com/TypeStrong/ts-loader 。
安装
npm install ts-loader typescript
配置
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
}
],
注意:Typescript本身的配置并不在ts-loader中,而是必须要放在工程目录下的tsconfig.json中。
例:
//tsconfig.json
{
"compilerOptions": {
"target": "es5",
"sourceMap": true,
},
},
html-loader
作用
将HTML文件转化为字符串并进行格式化,这使得我们可以把一个HTML片段通过JS加载进来。
安装
npm install html-loader
配置
rules: [
{
test: /\.html$/,
use: 'html-loader',
}
],
例:
// header.html
<header>
<h1>This is a Header.</h1>
</header>
// index.js
import headerHtml from './header.html';
document.write(headerHtml);
解释:header.html将会转化为字符串,并通过document.write插入页面中。
handlebars-loader
作用
处理handlebars模板,在安装时要额外安装handlebars。
安装
npm install handlebars-loader handlebars
配置
rules: [
{
test: /\.handlebars$/,
use: handlebars-loader',
}
],
例:
// content.handlebars
<div class="entry">
<h1>{{ title }}</h1>
<div class="body">{{ body }}</div>
</div>
// index.js
import contentTemplate from './content.handlebars';
const div = document.createElement('div');
div.innerHTML = contentTemplate({
title: "Title",
body: "Your books are due next Tuesday"
});
document.body.appendChild(div);
解释:handlebars文件加载后得到的是一个函数,可以接收一个变量对象并返回最终的字符串。
file-loader
作用
用于打包文件类型的资源,并返回其publicPath
安装
npm install file-loader
配置
const path = require('path');
module.exports = {
entry: './app.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: './assets/',
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader',
}
],
},
};
指定了output.publicPath。文件路径是:./assets/文件名
没有指定output.publicPath,打包之后会在dist目录生成当前文件,文件名为hash值,默认为文件的hash值加上文件后缀。
file-loader也支持配置文件名以及publicPath(这里的publicPath会覆盖原有的output.publicPath),通过loader的options传入。
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
publicPath: './another-path/',
},
},
}
] ,
url-loader
作用
url-loader与file-loader作用类似,唯一的不同在于用户可以设置一个文件大小的阈值,当大于该阈值时与file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码
安装
npm install url-loader
配置
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
name: '[name].[ext]',
publicPath: './assets-path/',
},
},
}
],
url-loader可接收与file-loader相同的参数,如name和publicPath等,
同时也可以接收一个limit参数。
limit:小于limit,经过url-loader转化后得到的是base64形式的编码。
url-loader
作用
vue-loader用于处理vue组件
vue-loader可以将组件的模板、JS及样式进行拆分。
在安装时,除了必要的vue与vue-loader以外,还要安装vue-template-compiler来编译Vue模板,以及css-loader来处理样式(如果使用SCSS或LESS则仍需要对应的loader)。
vue-loader支持更多高级配置,文档https://vue-loader.vuejs.org/zh-cn 。
安装
npm install vue-loader vue vue-template-compiler css-loader
配置
rules: [
{
test: /\.vue$/,
use: 'vue-loader',
}
],
自定义loader
Webpack官方文档https://doc.webpack-china.org/api/loaders/ 。
1、实现一个loader,它会为所有JS文件启用严格模式
2、创建一个force-strict-loader目录
3、执行npm初始化命令npm init –y
4、创建index.js,也就是loader的主体。
module.exports = function(content) {
var useStrictPrefix = '\'use strict\';\n\n';
return useStrictPrefix + content;
}
5、现在我们可以在Webpack工程中安装并使用这个loader了
npm install <path-to-loader>/force-strict-loader
启用缓存
当文件输入和其依赖没有发生变化时,应该让loader直接使用缓存,而不是重复进行转换的工作。
在Webpack中可以使用this.cacheable进行控制
// force-strict-loader/index.js
module.exports = function(content) {
if (this.cacheable) {
this.cacheable();
}
var useStrictPrefix = '\'use strict\';\n\n';
return useStrictPrefix + content;
}
通过启用缓存可以加快Webpack打包速度,并且可保证相同的输入产生相同的输出。
获取options
安装
安装一个依赖库loader-utils。npm install loader-utils,它主要用于提供一些帮助函数。
获取
// force-strict-loader/index.js
var loaderUtils = require("loader-utils");
module.exports = function(content) {
if (this.cacheable) {
this.cacheable();
}
// 获取和打印 options
var options = loaderUtils.getOptions(this) || {};
console.log('options', options);
// 处理 content
var useStrictPrefix = '\'use strict\';\n\n';
return useStrictPrefix + content;
}
通过loaderUtils.getOptions可以获取到配置对象
source-map
source-map可以便于实际开发者在浏览器控制台查看源码。
如果没有对source-map进行处理,最终也就无法生成正确的map文件,在浏览器的dev tool中可能就会看到错乱的源码。
plugin
plugins用于接收一个插件数组,我们可以使用Webpack内部提供的一些插件,也可以加载外部插件。
样式处理
分离样式文件
extract-text-webpack-plugin(适用于Webpack 4之前版本)和mini-css-extract-plugin(适用于Webpack 4及以上版本)
extract-text-webpack-plugin
作用
分离样式文件
安装
npm install extract-text-webpack-plugin
配置
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js',
},
mode: 'development',
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
}
],
},
plugins: [
new ExtractTextPlugin("bundle.css")
],
};
解释:fallback:指定当插件无法提取样式时所采用的loader
plugins配置中添加该插件,并传入提取后的资源文件名。
多样式文件的处理
plugins: [
new ExtractTextPlugin("[name].css")
],
[name].css来动态生成CSS为文件名。
[name]不是导入css文件名,是指代chunk的名字,即entry中我们为每一个入口分配的名字。
例:
// webpack.config.js
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
foo: './src/scripts/foo.js',
bar: './src/scripts/bar.js',
},
output: {
filename: '[name].js',
},
mode: 'development',
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader', }),
}
],
},
plugins: [
new ExtractTextPlugin('[name].css')
],
};
打包之后css文件名为foo.css和bar.css
mini-css-extract-plugin
作用
官方文档https://github.com/webpack-contrib/mini-css-extract-plugin 。
分离样式文件,支持按需加载CSS
mini-css-extract-plugin支持配置publicPath,用来指定异步CSS的加载路径。
样式预处理
Sass与SCSS
安装
npm install sass-loader node-sass
作用
sass-loader就是将SCSS语法编译为CSS,在使用时通常还要搭配css-loader和style-loader。
node-sass是真正用来编译SCSS
配置
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
}
],
},
想要在浏览器的调试工具里查看源码,需要分别为sass-loader和css-loader单独添加source map的配置项。
例:
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
}
],
}
],
},
Less
安装
npm install less-loader less
Less支持多种编译过程中的配置,我们可以直接通过loader的options将这些配置传入
官方文档http://lesscss.org/usage/#less-options 。
配置
module: {
rules: [
{
test: /\.less/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'less-loader',
options: {
sourceMap: true,
},
}
],
}
],
},
PostCSS
简介:
PostCSS并不能算是一个CSS的预编译器,它只是一个编译插件的容器。
它的工作模式是接收样式源代码并交由编译插件处理,最后输出CSS。
postcss-loader
安装
npm install postcss-loader
配置
module: {
rules: [
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
] ,
}
],
},
须知:
PostCSS要求必须有一个单独的配置文件。(最初的版本中,其配置是可以通过loader来传入的,而在Webpack 2对配置添加了更严格的限制之后,PostCSS不再支持从loader传入。)
在项目的根目录下创建一个postcss.config.js
例:
// postcss.config.js
module.exports = {};
解释:
postcss-loader可以结合css-loader使用,也可以单独使用
也就是说不配置css-loader也可以达到相同的效果。
唯一不同的是,单独使用postcss-loader时不建议使用CSS中的@import语句,否则会产生冗余代码,因此官方推荐还是将postcss-loader放在css-loader之后使用。
自动前缀autoprefixer
作用
为CSS自动添加厂商前缀,根据caniuse.com上的数据,自动决定是否要为某一特性添加厂商前缀,并且可以由开发者为其指定支持浏览器的范围。
安装
npm install autoprefixer
配置
在postcss.config.js中添加autoprefixer。
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [
autoprefixer({
grid: true,
browsers: [
'> 1%',
'last 3 versions',
'android 4.2',
'ie 8',
],
})
],
};
解释:
autoprefixer中添加需要支持的特性(如grid)以及兼容哪些浏览器(browsers)。
例:
.container {
display: grid;
}
指定了grid:true,也就是为grid特性添加IE前缀,经过编译后则会成为:
.container {
display: -ms-grid;
display: grid;
}
tylelint
作用
stylelint可以检测出代码中的样式问题(语法错误、重复的属性等)
stylelint是一个CSS的质量检测工具,就像eslint一样,我们可以为其添加各种规则,来统一项目的代码风格,确保代码质量。
安装
npm install stylelint
配置
在postcss.config.js中添加相应配置。
const stylelint = require('stylelint');
module.exports = {
plugins: [
stylelint({
config: {
rules: {
'declaration-no-important': true,
},
},
})
],
};
解释:
declaration-no-important这样一条规则,当我们的代码中出现了“!important”时就会给出警告。
CSSNext
作用
在应用中使用最新的CSS语法特性。
安装
npm install postcss-cssnext
配置
在postcss.config.js中添加相应配置。
const postcssCssnext = require('postcss-cssnext');
module.exports = {
plugins: [
postcssCssnext({
// 指定所支持的浏览器
browsers: [
'> 1%',
'last 2 versions',
],
})
],
};
打包案例
打包前
/* style.css */
:root {
--highlightColor: hwb(190, 35%, 20%);
}
body {
color: var(--highlightColor);
}
打包后
body {
color: rgb(89, 185, 204);
}
CSS Modules
作用
让CSS也拥有模块的特点
每个CSS文件中的样式都拥有单独的作用域,不会和外界发生命名冲突。
对CSS进行依赖管理,可以通过相对路径引入CSS文件。
安装
使用CSS Modules不需要额外安装模块,只要开启css-loader中的modules配置项即可。
配置
module: {
rules: [
{
test: /\.css/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]',
},
}
],
}
],
},
案例:
编译前:
/* style.css */
.title {
color: #f938ab;
}
编译后:可能将成为.style__title__1CFy6
解释:
localIdentName:
指明CSS代码中的类名会如何来编译
[name]指代的是模块名,这里被替换为style
[local]指代的是原本的选择器标识符,这里被替换为title
[hash:base64:5]指代的是一个5位的hash值
使用事项
注意在JavaScript中引入CSS的方式
之前只是直接将CSS文件引入就可以了,但使用CSS Modules时CSS文件会导出一个对象,
我们需要把这个对象中的属性添加到HTML标签上
例:
/* style.css */
.title {
color: #f938ab;
}
// app.js
import styles from './style.css';
document.write(`<h1 class="${styles.title}">My Webpack app.</h1>`);
最终这个HTML中的class才能与我们编译后的CSS类名匹配上。
代码分片
高性能应用其中重要的一点就是尽可能地让用户每次只加载必要的资源
优先级不太高的资源则采用延迟加载等技术渐进式地获取,这样可以保证页面的首屏速度。
代码分片这项技术我们可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。
优点:代码分片可以有效降低首屏加载资源的大小
缺点:比如我们应该对哪些模块进行分片、分片后的资源如何管理等
Webpack中每个入口(entry)都将生成一个对应的资源文件
通过入口的配置我们可以进行一些简单有效的代码拆分。
CommonsChunkPlugin
作用
可以将多个Chunk中公共的部分提取出来。
公共模块的提取可以为项目带来几个收益:
开发过程中减少了重复模块打包,可以提升开发速度;
减小整体资源体积;
合理分片后的代码可以更有效地利用客户端缓存。
安装
Webpack 4之前内部自带的插件(Webpack 4之后替换为了SplitChunks)。
案例
配置前
// webpack.config.js
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
},
output: {
filename: '[name].js',
},
};
// foo.js
import React from 'react';
document.write('foo.js', React.version);
// bar.js
import React from 'react';
document.write('bar.js', React.version);
配置前打包
//配置后
const webpack = require('webpack');
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
},
output: {
filename: '[name].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js',
})
],
};
name:用于指定公共chunk的名字。
filename:提取后的资源文件名。
配置后打包
解释: 产出的资源中多了commons.js,而foo.js和bar.js的体积从之前的72.1kB降为不到1kB,这就是把react及其依赖的模块都提到commons.js的结果。
提取vendor
CommonsChunkPlugin主要用于提取多入口之间的公共模块,但这不代表对于单入口的应用就无法使用。仍然可以用它来提取第三方类库及业务中不常更新的模块 例:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: {
app: './app.js',
vendor: ['react'],
},
output: {
filename: '[name].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.js',
})
],
};
// app.js
import React from 'react';
document.write('app.js', React.version);
打包后
设置提取范围chunks
作用
规定从哪些入口中提取公共模块 例:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: {
a: './a.js',
b: './b.js',
c: './c.js',
},
output: {
filename: '[name].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js',
chunks: ['a', 'b'],
})
],
};
打包后
置提取规则
作用
比如:项目中一些组件或工具模块,虽然被多次引用,但是可能经常修改,如果将其和react这种库放在一起反而不利于客户端缓存。
可以通过CommonsChunkPlugin的minChunks配置项来设置提取的规则。
minChunks
minChunks可以接受一个数字,当设置minChunks为n时,只有该模块被n个入口同时引用才会进行提取。另外,这个阈值不会影响通过数组形式入口传入模块的提取。 例:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
vendor: ['react'],
},
output: {
filename: '[name].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.js',
minChunks: 3,
})
],
};
令foo.js和bar.js共同引用一个util.js。
// foo.js
import React from 'react';
import './util';
document.write('foo.js', React.version);
// bar.js
import React from 'react';
import './util';
document.write('bar.js', React.version);
// util.js
console.log('util');
设置minChunks为3,util.js并不会被提取到vendor.js中,然而react并不受这个的影响,仍然会出现在vendor.js中。
minChunks属性Infinity
设置为无穷代表提取的阈值无限高,也就是说所有模块都不会被提取。
minChunks支持传入一个函数
当函数的返回值是true时进行提取。 例:
new webpack.optimize.CommonsChunkPlugin({
name: 'verndor',
filename: 'vendor.js',
minChunks: function(module, count) {
// module.context 模块目录路径
if(module.context && module.context.includes('node_modules')) {
return true;
}
// module.resource 包含模块名的完整路径
if(module.resource && module.resource.endsWith('util.js')) {
return true;
}
// count 为模块被引用的次数
if(count > 5) {
return true;
}
},
}),
hash与长效缓存
当我们使用CommonsChunkPlugin时提取公共模块时,提取后的资源内部不仅仅是模块的代码,往往还包含Webpack的运行时(runtime)。Webpack的运行时指的是初始化环境的代码,如创建模块缓存对象、声明模块加载函数等。
CommonsChunkPlugin的不足
1)一个CommonsChunkPlugin只能提取一个vendor,假如我们想提取多个vendor则需要配置多个插件,这会增加很多重复的配置代码。 2)前面我们提到的manifest实际上会使浏览器多加载一个资源,这对于页面渲染速度是不友好的。 3)由于内部设计上的一些缺陷,CommonsChunkPlugin在提取公共模块的时候会破坏掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。比如在异步Chunk的场景下CommonsChunkPlugin并不会按照我们的预期正常工作。
SplitChunksPlugin
Webpack 4为了改进CommonsChunk-Plugin而重新设计和实现的代码分片特性。它不仅比CommonsChunkPlugin功能更加强大,还更简单易用。 例:
// webpack.config.js
module.exports = {
entry: './foo.js',
output: {
filename: 'foo.js',
publicPath: '/dist/',
},
mode: 'development',
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
// foo.js
import React from 'react';
import('./bar.js');
document.write('foo.js', React.version);
// bar.js
import React from 'react';
console.log('bar.js', React.version);
指定了chunks的值为all,这个配置项的含义是,SplitChunks将会对所有的chunks生效(默认情况下,SplitChunks只对异步chunks生效,并且不需要配置)。
mode是Webpack 4中新增的配置项,可以针对当前是开发环境还是生产环境自动添加对应的一些Webpack配置。
SplitChunks默认情形下的提取条件:
提取后的chunk可被共享或者来自node_modules目录。
提取后的Javascript chunk体积大于30kB(压缩和gzip之前),CSS chunk体积大于50kB。
在按需加载过程中,并行请求的资源最大值小于等于5。## 资源异步加载原理
在首次加载时,并行请求的资源数最大值小于等于3。
资源异步加载
当模块数量过多、资源体积过大时,可以把一些暂时使用不到的模块延迟加载。
import()
Webpack中有两种异步加载的方式——import函数及require.ensure。
require.ensure是Webpack 1支持的异步加载方式,从Webpack 2开始引入了import函数。
通过import函数加载的模块及其依赖会被异步地进行加载,并返回一个Promise对象。
例:
假设bar.js的资源体积很大,并且我们在页面初次渲染的时候并不需要使用它,就可以对它进行异步加载。
// foo.js
import('./bar.js').then(({ add }) => {
console.log(add(2, 3));
});
// bar.js
export function add(a, b) {
return a + b;
}
还需要我们更改一下Webpack的配置。
module.exports = {
entry: {
foo: './foo.js'
},
output: {
publicPath: '/dist/',
filename: '[name].js',
},
mode: 'development',
devServer: {
publicPath: '/dist/',
port: 3000,
},
};
生产环境配置
环境配置的封装
1)使用相同的配置文件。
// package.json {
... "scripts": {
"dev": "ENV=development webpack-dev-server",
"build": "ENV=production webpack"
},
}
// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {
output: {
filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',
},
mode: ENV,
};
2)为不同环境创建各自的配置文件。
单独创建一个webpack.production.config.js,开发环境的则可以叫webpack.development.config.js。然后修改package.json。
{
...
"scripts": {
"dev": " webpack-dev-server --config=webpack.development.config.js",
"build": " webpack --config=webpack.production.config.js"
},
}
通过--config指定打包时使用的配置文件。
webpack.development.config.js和webpack.production.config.js肯定会有重复的部分,可以将公共的配置提取出来,单独创建一个webpack.common.config.js。让另外两个JS分别引用该文件,并添加上自身环境的配置即可。
开启production模式
Webpack 4中直接加了一个mode配置项,可以通过它来直接切换打包模式。
// webpack.config.js
module.exports = {
mode: 'production',
};
浏览器环境变量
浏览器环境,不能获取到process.env,浏览器环境变量是基于webpack.DefinePlugin这个插件在项目构建时引入的,引入之后可以在前端代码中全局获取到对应的变量。
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js',
},
mode: 'production',
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
})
],
};
// app.js
document.write(ENV);
如果启用了mode:production,则Webapck已经设置好了process.env.NODE_ENV,不需要再人为添加了。
source map
将编译、打包、压缩后的代码映射回源代码的过程。
启用了devtool配置项,source map就会跟随源代码一步步被传递,直到生成最后的map文件。这个文件默认就是打包后的文件名加上.map,生成mapping文件的同时,bundle文件中会追加上一句注释来标识map文件的位置。如:
// bundle.js
(function() {
// bundle 的内容
})();
//# sourceMappingURL=bundle.js.map
打开了浏览器的开发者工具时,map文件会同时被加载,这时浏览器会使用它来对打包后的bundle文件进行解析,分析出源代码的目录结构和内容。
source map配置
module.exports = {
// ...
devtool: 'source-map',
};
对于CSS、SCSS、Less来说,则需要添加额外的source map配置项。
const path = require('path');
module.exports = {
// ...
devtool: 'source-map',
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
}
] ,
}
],
},
};
开启source map之后,打开Chrome的开发者工具,在“Sources”选项卡下面的“webpack://”目录中可以找到解析后的工程源码
devtool除了source-map以外,还有cheap-source-map、eval-source-map等,生成完整的source map会延长整体构建时间,对打包速度需求比较高的话,建议选择一个简化版的source map。
在开发环境中,cheap-module-eval-source-map通常是一个不错的选择,属于打包速度和源码信息还原程度的一个良好折中。
在生产环境中由于我们会对代码进行压缩,而最常见的压缩插件UglifyjsWebpack-Plugin目前只支持完全的source-map,因此没有那么多选择,我们只能使用source-map、hidden-source-map、nosources-source-map这3者之一。
安全
hidden-source-map
Webpack仍然会产出完整的map文件,只不过不会在bundle文件中添加对于map文件的引用。打开浏览器的开发者工具时,我们是看不到map文件的,浏览器自然也无法对bundle进行解析。要追溯源码,则要利用一些第三方服务,将map文件上传到那上面。目前最流行的解决方案是Sentry。
nosources-source-map
在浏览器开发者工具的Sources选项卡中看到源码的目录结构,但是文件的具体内容会被隐藏起来。对于错误来说,我们仍然可以在Console控制台中查看源代码的错误栈,或者console日志的准确行数。
source map
通过服务器的nginx设置(或其他类似工具)将.map文件只对固定的白名单(比如公司内网)开放,这样我们仍然能看到源码,而在一般用户的浏览器中就无法获取到它们了。
资源压缩
压缩JavaScript
UglifyJS(Webpack 3已集成)
terser(Webpack 4已集成)支持ES6+代码的压缩
Webpack 3中的话,开启压缩需调用webpack.optimize.UglifyJsPlugin。
// Webpack version < 4
const webpack = require('webpack');
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js',
},
plugins: [new webpack.optimize.UglifyJsPlugin()],
};
Webpack 4之后,这项配置被移到了config.optimization.minimize
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js',
},
optimization: {
minimize: true,
},
};
//如果开启了mode:production,则不需要人为设置
terser-webpack-plugin插件支持自定义配置
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
//...
optimization: {
// 覆盖默认的 minimizer
minimizer: [
new TerserPlugin({
/* your config */
test: /\.js(\?.*)?$/i,
exclude: /\/excludes/,
})
],
},
};
压缩CSS
压缩CSS文件的前提是使用extract-text-webpack-plugin或mini-css-extract-plugin将样式提取出来,接着使用optimize-css-assets-webpack-plugin来进行压缩
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader', }),
}
],
},
plugins: [new ExtractTextPlugin('style.css')],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({
// 生效范围,只压缩匹配到的资源
assetNameRegExp: /\.optimize\.css$/g,
// 压缩处理器,默认为 cssnano
cssProcessor: require('cssnano'),
// 压缩处理器的配置
cssProcessorOptions: {
discardComments: {
removeAll: true
}
},
// 是否展示 log
canPrint: true,
})],
},
};
缓存
资源hash
每次打包的过程中对资源的内容计算一次hash
module.exports = {
entry: './app.js',
output: {
filename: 'bundle@[chunkhash].js',
},
mode: 'production',
};
输出动态HTML
html-webpack-plugin会自动地将我们打包出来的资源名放入生成的index.html中
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template:'./index.html
})
],
};
bundle体积监控和分析
VS Code中有一个插件Import Cost可以帮助我们对引入模块的大小进行实时监测。
webpack-bundle-analyzer,它能够帮助我们分析一个bundle的构成。它可以帮我们生成一张bundle的模块组成结构图
const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ...
plugins: [
new Analyzer()
],
};
最后我们还需要自动化地对资源体积进行监控,bundlesize这个工具包可以帮助做到这一点。安装之后只需要在package.json进行一下配置即可。
{
"name": "my-app",
"version": "1.0.0",
"bundlesize": [
{
"path": "./bundle.js",
"maxSize": "50 kB"
}
],
"scripts": {
"test:size": "bundlesize"
}
}
打包优化
HappyPack
作用
通过多线程来提升Webpack打包速度的工具。适用于那些转译任务比较重的工程
单个loader的优化
// 初始Webpack配置(使用HappyPack前)
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['react'],
},
}
],
},
};
// 使用HappyPack的配置
const HappyPack = require('happypack');
module.exports = {
//... module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader',
}
],
},
plugins: [
new HappyPack({
loaders: [
{
loader: 'babel-loader',
options: {
presets: ['react'],
},
}
],
})
],
};
多个loader的优化
为每一个loader配置一个id
const HappyPack = require('happypack');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=js',
},
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=ts',
}
],
},
plugins: [
new HappyPack({
id: 'js',
loaders: [{
loader: 'babel-loader',
options: {}, // babel options
}],
}),
new HappyPack({
id: 'ts',
loaders: [{
loader: 'ts-loader',
options: {}, // ts options
}],
})
]
};
缩小打包作用域
exclude和include
对于JS来说,一般要把node_modules目录排除掉,另外当exclude和include规则有重叠的部分时,exclude的优先级更高。
module: {
rules: [
{
test: /\.js$/,
include: /src\/scripts/,
loader: 'babel-loader,
}
],
},
noParse
希望Webpack完全不要去进行解析有些库,即不希望应用任何loader规则,库的内部也不会有对其他模块的依赖
module.exports = {
//...
module: {
noParse: /lodash/,
}
};
//上面的配置将会忽略所有文件名中包含lodash的模块,这些模块仍然会被打包进资源文件,只不过Webpack不会对其进行任何解析。
Webpack 3及之后的版本还支持完整的路径匹配。
module.exports = {
//...
module: {
noParse: function(fullPath) {
// fullPath是绝对路径,如: /Users/me/app/webpack-no-parse/lib/lodash.js
return /lib/.test(fullPath);
},
}
};
//上面的配置将会忽略所有lib目录下的资源解析。
IgnorePlugin
完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,// 匹配资源文件
contextRegExp: /moment$/, // 匹配检索目录
})
],
Cache
有些loader会有一个cache配置项,用来在编译代码后同时保存一份缓存,在执行下一次编译前会先检查源码文件是否有变化,如果没有就直接采用缓存,也就是上次编译的结果。这样相当于实际编译的只有变化了的文件,整体速度上会有一定提升。
Webpack 5中添加了一个新的配置项“cache:{type:"filesystem"}”,它会在全局启用一个文件缓存。目前仅仅是实验阶段,并且无法自动检测到缓存已经过期。
目前的解决办法就是,当我们更新了任何node_modules中的模块或者Webpack的配置后,手动修改cache.version来让缓存过期。
tree shaking
在打包过程中帮助我们检测工程中没有被引用过的模块。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。
tree shaking只能对ES6 Module生效。
使用Webpack进行依赖关系构建
如果我们在工程中使用了babel-loader,那么一定要通过配置来禁用它的模块依赖解析。因为如果由babel-loader来做依赖解析,Webpack接收到的就都是转化过的CommonJS形式的模块,无法进行tree-shaking。
//禁用babel-loader模块依赖解析的配置示例如下:
module.exports = {
// ...
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: [
// 这里一定要加上 modules: false
[@babel/preset-env, { modules: false }]
],
},
}],
}],
},
};
使用压缩工具去除死代码
tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具来进行的。 使用terser-webpack-plugin即可。在Webpack 4之后的版本中,将mode设置为production也可以达到相同的效果。
开发环境调优
webpack-dashboard
更好地展示Webpack每一次构建结束后控制台输出一些打包相关的信息
安装
npm install webpack-dashboard
配置
const DashboardPlugin = require('webpack-dashboard/plugin');
module.exports = {
entry: './app.js',
output: {
filename: '[name].js',
},
mode: 'development',
plugins: [
new DashboardPlugin()
],
};
使webpack-dashboard生效还要更改webpack的启动方式。用webpack-dashboard模块命令替代原本的webpack或者webpack-dev-server的命令,并将原有的启动命令作为参数传给它。
// package.json
{
...
"scripts": {
"dev": "webpack-dashboard -- webpack-dev-server"
}
}
webpack-merge
配置多种打包环境
安装
npm install webpack-merge
配置
// webpack.common.js
module.exports = {
entry: './app.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
}
],
},
};
// webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js'); //公共模块
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = merge.smart(commonConfig,
{
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
}
]
},
});
speed-measure-webpack-plugin
可以分析出Webpack整个打包过程中在各个loader和plugin上耗费的时间
安装
npm install speed-measure-webpack-plugin
配置
// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
entry: './app.js',
...
});
size-plugin
监控资源体积的变化
安装
npm install size-plugin
配置
const path = require('path');
const SizePlugin = require('size-plugin');
module.exports = {
entry: './app.js',
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
mode: 'production',
plugins: [
new SizePlugin(),
],
};
//在每次执行Webpack打包命令后,size-plugin都会输出本次构建的资源体积(gzip过后),以及与上次构建相比体积变化了多少
模块热替换
开启HMR
确保项目是基于webpack-dev-server或者webpack-dev-middle进行开发的。Webpack本身的命令行并不支持HMR
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
},
};
//Webpack会为每个模块绑定一个module.hot对象,这个对象包含了HMR的API。
调用HMR API有两种方式,一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。
//手动添加代码来开启HMR:
// index.js
import { add } from 'util.js';
add(2, 3);
if (module.hot) {
module.hot.accept();
}
HMR原理
在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于是我们的服务端。
HMR的核心就是客户端从服务端拉取更新后的资源(准确地说,HMR拉取的不是整个资源文件,而是chunk diff,即chunk需要更新的部分。)
WDS与浏览器之间维护了一个websocket,当本地资源发生变化时WDS会向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对。通过hash的比对可以防止冗余更新的出现。