◆ 使用配置文件
>> Webpack对于output.path的要求是使用绝对路径(从系统根目录开始的完整路径),用node.js的路径拼装函数---path.join,将__dirname(node.js内置全局变量,值为当前文件所在的绝对路径)与dist(输出目录)连接起来,得到了最终的资源输出路径。
◆ webpack-dev-server
>> 安装npm install--production过滤掉devDependencies中的冗余模块,从而加快安装和发布的速度。
>> 综上我们可以总结出webpack-dev-server的两大职能:·令Webpack进行模块打包,并处理打包结果的资源请求。·作为普通的Web Server,处理静态资源文件请求。
>> ,而webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js,在每次webpack-dev-server接收到请求时都只是将内存中的打包结果返回给浏览器
◆ CommonJS
>> 直到有了Browserify——一个运行在Node.js环境下的模块打包工具,它可以将CommonJS模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循CommonJS标准来编写了。
◆模块
>> CommonJS中规定每个文件是一个模块。将一个个JavaScript文件直接通过script标签插入页面中与封装成commonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看列子:
calculator.js
var name = 'calculator.js';
// index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js
这里有两个文件,在index.js中我们通过CommonJS的require函数的加载函数calculator.js
。的结果是index.js,这说明calculator.js的变量不会影响index.js
◆ 导入
>> 有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require即可。Require(‘/task.js’)另外,require函数可以接收表达式,借助这个特性我们可以动态地指定模块加载路径。
Const modulesNames = [‘foo.js’,’bar.js’];
moduleNames.forEach(name=>{
Require(‘./’+name)
});
◆ 导出
>> 在使用命名导出时,可以通过as关键字对变量重命名。如:const name = 'calculator';const add = function(a, b) { return a + b; };export { name, add as getSum }; // 在导入时即为 name 和 getSum与命名导出不同.
模块的默认导出只能有一个。如:export default {name: 'calculator', add: function(a, b) { return a + b; }};
Export default理解为对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量申明,直接导出即可。
//导出字符串
Export default “this is calcuiator.js”;
//导出
Export default class{...}
//导出匿名函数
Export default function(){...}
◆ 导入
>> 加载带有命名导出的模块时,import后面要跟一对大括号来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。导入变量就相当于在当前作用域下声明了这些变量的,并且不可对齐进行修改(是只读的)
◆ 复合写法
>> 2.2.4 复合写法在工程中,有时需要把某一个模块导入之后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:export { name, add } from './calculator.js';复合写法目前只支持当被导入模块。通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆开写。import xxx from "xxxx ";
export default xxxxx;
◆ 动态与静态
>> CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。
>> ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域。因此我们说,ES6 Module是一种静态的模块结构,ES6 Module是一种静态的模块结构,在es6代码的编译阶段就可以分析出模块的依赖。它相比于commonJS来说具备以下几点优势:
·死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
·模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
·编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。
◆ 值拷贝与动态映射
>> 在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
下列什么是CommonJS中的值拷贝。
// calculator.js
var count = 0;
module.exports = {
count: count,
add: function(a, b) {
count += 1;
return a + b;
}
};
// index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;
console.log(count); // 0(这里的count是对 calculator.js 中 count 值的拷贝)
add(2, 3);
console.log(count); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)
count += 1;
console.log(count); // 1(拷贝的值可以更改)
◆UMD
>> 严格来说,UMD并不能说是一种模块标准,不如说它是一组模块形式的集合更准确。UMD的全称是Universal Module Definition,也就是通用模块标准,它的目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境
◆ entry
>> 传入一个数组的作用是将多个资源预先合并,在打包时Webpack会将数组中的最后一个元素作为实际的入口路径。如:
Module.exports={
Entry:[‘babel-polyfill’,’./src/index.js’]
}
上面等同于
//webpack.config.js
Module.exports={
Entry:’./src/index.js’
}
// Index.js
Import ‘babel-polyfill’
◆ 实例
>> vendor的意思是“供应商”,在Webpack中vendor一般指的是工程所使用的库、框架等第三方模块集中打包而产生的bundle。请看下面这个例子:
◆ publicPath
publicPath是一个非常重要的配置项,并且容易与path相混淆。从功能上来说,path用来指定资源的输出位置,而publicPath则用来指定资源的请求位置。让我们详细解释这两个定义。
·输出位置:打包完成后资源产生的目录,一般将其指定为工程中的dist目录。
·请求位置:由JS或CSS所请求的间接资源路径。页面中的资源分为两种,一种是由HTML页面直接请求的,比如通过script标签加载的JS;另一种是由JS或CSS请求的,如异步加载的JS、从CSS请求的图片字体等。publicPath的作用就是指定这部分间接资源的请求位置。
◆loader的配置
>> loader的字面意思是装载器,在Webpack中它的实际功能则更像是预处理器。Webpack本身只认识JavaScript,对于其他类型的资源必须预先定义一个或多个loader对其进行转译,输出为Webpack能够接收的形式再继续进行,因此loader做的实际上是一个预处理的工作。
◆ loader的引入
>> css-loader的作用仅仅是处理CSS的各种加载语法(@import和url()函数等),如果要使样式起作用还需要style-loader来把样式插入页面。css-loader与style-loader通常是配合在一起使用的。
◆ 更多配置
>> 另外,由于exclude优先级更高,我们可以对include中的子目录进行排除。请看下面的例子:rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /src\/lib/, include: /src/, }],通过include,我们将该规则配置为仅对src目录生效,但是仍然可以通过exclude排除其中的src/lib目录。2.resource与issuer
>> 在Webpack中,我们认为被加载模块是resource,而加载者是issuer。如上面的例子中,resource为/path/of/app/style.css,issuer是/path/of/app/index.js。前面介绍的test、exclude、include本质上属于对resource也就是被加载者的配置,如果想要对issuer加载者也增加条件限制,则要额外写一些配置。比如,如果我们只想让/src/pages目录下的JS可以引用CSS,应该如何设置呢?请看下面的例
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: /node_modules/,
issuer: {
Test:/\.js$/,
Include:/src/pages/,
},
}
]
>>可以看到,我们添加了issuer配置对象,其形式与之前对resource条件的配置并无太大差异。但只有/src/pages/目录下面的JS文件引用CSS文件,这条规则才会生效;如果不是JS文件引用的CSS(比如JSX文件),或者是别的目录的JS文件引用CSS,则规则不会生效。
上面的配置虽然实现了我们的需求,但是test、exclude、include这些配置项分布于不同的层级上,可读性较差。事实上我们还可以将它改为另一种等价的形式。
rules: [
{
use: ['style-loader', 'css-loader'],
resource: {
test: /\.css$/,
exclude: /node_modules/,
},
issuer: {
test: /\.js$/,
exclude: /node_modules/,
},
}
],
通过添加resource对象来将外层的配置包起来,区分了resource和issuer中的规则,这样就一目了然了。上面的配置与把resource的配置写在外层在本质上是一样的,然而这两种形式无法并存,只能选择一种风格进行配置
>> 可以看到,在配置中添加了一个eslint-loader来对源码进行质量检测,其enforce的值为“pre”,代表它将在所有正常loader之前执行,这样可以保证其检测的代码不是被其他loader更改过的。类似的,如果某一个loader是需要在所有loader之后执行的,我们也可以指定其enforce为“post”
◆ babel-loader
>> 3)由于@babel/preset-env会将ES6 Module转化为CommonJS的形式,这会导致Webpack中的tree-shaking特性失效。将@babel/preset-env的modules配置项设置为false会禁用模块语句的转化,而将ES6 Module的语法交给Webpack本身处理。
◆ html-loader
>> html-loader用于将HTML文件转化为字符串并进行格式化,这使得我们可以把一个HTML片段通过JS加载进来。安
◆ 自定义loader
>>在开发一个loader时,我们可以借助npm/yarn的软连接功能进行本地调试(当然之后可以考虑发布到npm等)下面让我们初始化这个loader并配置到过程中,创建一个force-strict-lodader目录,然后该目录下执行npm初始化命令。Npm init -y 接着创建index.js也就是loader的主体。module.exports = function(content) { var useStrictPrefix = '\'use strict\';\n\n'; return useStrictPrefix + content;}现在我们可以在Webpack工程中安装并使用这个loader了。npm install
Module:{
Rules:[
{
Test:/\.js$/,
Use:’force-strict-lodaer’
}
]
}
>> 启用缓存当文件输入和其依赖没有发生变化时,应该让loader直接使用缓存,而不是重复进行转换的工作。在Webpack中可以使用this.cacheable进行控制,
>> 接着更改loader。
// 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 + coutent;
}
◆extract-text-webpack-plugin
我们先通过一个简单的例子来直观认识该插件是如何工作的。使用npm安装:
npm install extract-text-webpack-plugin
在webpack.config.js中引入:
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")
],
};
在module.rules中我们设置了处理CSS文件的规则,其中的use字段并没有直接传入loader,而是使用了插件的extract方法包了一层。内部的fallback属性用于指定当插件无法提取样式时所采用的loader,use(extract方法里面的)用于指定在提取样式之前采用哪些loader来预先进行处理。除此之外,还要在Webpack的plugins配置中添加该插件,并传入提取后的资源文件名。
◆ mini-css-extract-plugin
.lodaer规则设置的形式不同,并且mini-css-extract-plugin支持配置publicPath,用来指定异步css的加载路径
>> ·不需要设置fallback。
·在plugins设置中,除了指定同步加载的CSS资源名(filename),还要指定异步加载的CSS资源名(chunkFilename)。
◆ Sass与SCSS
>> 假如我们想要在浏览器的调试工具里查看源码,需要分别为sass-loader和css-loader单独添加source map的配置项。
◆ PostCSS与Webpack
>> PostCSS要求必须有一个单独的配置文件。在最初的版本中,其配置是可以通过loader来传入的,而在Webpack 2对配置添加了更严格的限制之后,PostCSS不再支持从loader传入。因此我们需要在项目的根目录下创建一个postcss.config.js。目前我们还没有添加任何特性,因此暂时返回一个空对象即可。
◆ CSSNext
>> 5.3.4 CSSNextPostCSS可以与CSSNext结合使用,让我们在应用中使用最新的CSS语法特性。使用npm安装。npm install postcss-cssnext在postcss.config.js中添加相应配置。
const postcssCssnext = require('postcss-cssnext');
module.exports = {
plugins: [
postcssCssnext({
// 指定所支持的浏览器
browsers: [
'> 1%',
'last 2 versions',
],
})
],
};
指定好需要支持的浏览器之后,我们就可以顺畅地使用CSSNext的特性了,PossCss会帮助我们把CSSNext的语法翻译为浏览器能接受的属性和形式。比如:
/* style.css */
:root {
--highlightColor: hwb(190, 35%, 20%);
}
body {
color: var(--highlightColor);
}
打包后的结果如下:
body {
color: rgb(89, 185, 204);
}
◆ CSS Modules
CSS Modules
CSS Modules是近年来比较流行的一种开发模式,其理念就是把CSS模块化,让CSS也拥有模块的特点,具体如下:
·每个CSS文件中的样式都拥有单独的作用域,不会和外界发生命名冲突。
·对CSS进行依赖管理,可以通过相对路径引入CSS文件。
·可以通过composes轻松复用其他CSS模块。
使用CSS Modules不需要额外安装模块,只要开启css-loader中的modules配置项即可。
module: {
rule{
test: /\.css/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]',
},
}
],
}
],
},
这里比较值得一提的是localIdentName配置项,它用于指明CSS代码中的类名会如何来编译。假设源码是下面的形式:
/* style.css */
.title {
color: #f938ab;
}
经过编译后可能将成为.style__title__1CFy6。让我们依次对照上面的配置:
·[name]指代的是模块名,这里被替换为style。
·[local]指代的是原本的选择器标识符,这里被替换为title。
·[hash:base64:5]指代的是一个5位的hash值,这个hash值是根据模块名和标识符计算的,因此不同模块中相同的标识符也不会造成样式冲突
>> 在使用的过程中我们还要注意在JavaScript中引入CSS的方式。之前只是直接将CSS文件引入就可以了,但使用CSS Modules时CSS文件会导出一个对象,我们需要把这个对象中的属性添加到HTML标签,列
/*style.css*/
.title{
Color:#f938ab;
}
//app.js
Import styles from ‘./style.css’
Document.write(
◆ 设置提取范围
通过CommonsChunkPlugin中的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'],
})
],
};
我们在chunks中配置了a和b,这意味着只会从a.js和b.js中提取公共模块。
对于一个大型应用来说,拥有几十个页面是很正常的,这也就意味着会有几十个资源入口。这些入口所共享的模块也许会有些差异,在这种情况下,我们可以配置多个CommonsChunkPlugin,并为每个插件规定提取的范围,来更有效地进行提取.
◆ 设置提取规则
>> CommonsChunkPlugin的默认规则是只要一个模块被两个入口chunk所使用就会被提取出来,比如只要a和b用了react,react就会被提取出来。
这个配置项的意义有两个。第一个是和上面的情况类似,即我们只想让Webpack提取特定的几个模块,并将这些模块通过数组型入口传入,这样做的好处是提取哪些模块是完全可控的;另一个是我们指定minChunks为Infinity,为了生成一个没有任何模块而仅仅包含Webpack初始化环境的文件,这个文件我们通常称为manifest。
◆ hash与长效缓存
>> 使用CommonsChunkPlugin时,一个绕不开的问题就是hash与长效缓存。当我们使用该插件提取公共模块时,提取后的资源内部不仅仅是模块的代码,往往还包含Webpack的运行时(runtime)。Webpack的运行时指的是初始化环境的代码,如创建模块缓存对象、声明模块加载函数等
>> 这个问题解决的方案是:将运行时的代码单独提取出来,例:
/ 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',
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
})
],};
上面的配置中,通过添加一个name为manifest的commonsChunkPlugins来提取webpack运行时。注意 manifest的CommonsChunkPlugin必须出现在最后,否则Webpack将无法正常提取模块
◆ optimization.SplitChunks
optimization.SplitChunks(简称SplitChunks)是Webpack 4为了改进CommonsChunk-Plugin而重新设计和实现的代码分片特性。它不仅比CommonsChunkPlugin功能更加强大,还更简单易用。
比如我们前面异步加载的例子,在换成Webpack 4的SplitChunks之后,就可以自动提取出react了。请看下面的例子:
// 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);
此处Webpack 4的配置与之前相比有两点不同:
使用optimization.splitChunks替代了CommonsChunkPlugin,并指定了chunks的值为all,这个配置项的含义是,SplitChunks将会对所有的chunks生效(默认情况下,SplitChunks只对异步chunks生效,并且不需要配置)。
.node是webpack 4中新增的配置项,可以针对当前是开发环境还是生成环境自动添加对应的一些webpack配置
◆ 从命令式到声明式
>> 在使用CommonsChunkPlugin的时候,我们大多数时候是通过配置项将特定入口中的特定模块提取出来,也就是更贴近命令式的方式。而SplitChunks的不同之处在于我们只需要设置一些提取条件,如提取的模式、提取模块的体积等,当某些模块达到这些条件后就会自动被提取出来。SplitChunks的使用更像是声明式的。
以下是SplitChunks默认情形下的提取条件:
·提取后的chunk可被共享或者来自node_modules目录。这一条很容易理解,被多次引用或处于node_modules中的模块更倾向于是通用模块,比较适合被提取出来。
·提取后的Javascript chunk体积
于30kB(压缩和gzip之前),CSS chunk体积大于50kB。这个也比较容易理解,如果提取后的资源体积太小,那么带来的优化效果也比较一般。
·在按需加载过程中,并行请求的资源最大值小于等于5。按需加载指的是,通过动态插入script标签的方式加载脚本。我们一般不希望同时加载过多的资源,因为每一个请求都要花费建立链接和释放链接的成本,因此提取的规则只在并行请求不多的时候生效。
·在首次加载时,并行请求的资源数最大值小于等于3。和上一条类似,只不过在页面首次加载时往往对性能的要求更高,因此这里的默认阈值也更低。
◆ 配置
为了更好地了解SplitChunks是怎样工作的,我们来看一下它的默认配置。
splitChunks: {
chunks: "async",
minSize: {
javascript: 30000,
style: 50000,
},
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
(1)匹配模式
通过chunks我们可以配置SplitChunks的工作模式。它有3个可选值,分别为async(默认)、initial和all。async即只提取异步chunk,initial则只对入口chunk生效(如果配置了initial则上面异步的例子将失效),all则是两种模式同时开启。
(2)匹配条件
minSize、minChunks、maxAsyncRequests、maxInitialRequests都属于匹配条件,前文已经介绍过了,不赘述。
(3)命名
配置项name默认为true,它意味着SplitChunks可以根据
cacheGroups和作用范围自动为新生成的chunk命名,并以automaticNameDelimiter分隔。如vendors~a~b~c.js意思是cacheGroups为vendors,并且该chunk是由a、b、c三个入口chunk所产生的。
(4)cacheGroups
可以理解成分离chunks时的规则。默认情况下有两种规则——vendors和default。vendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块。我们可以对这些规则进行增加或者修改,如果想要禁用某种规则,也可以直接将其置为false。当一个模块同时符合多个cacheGroups时,则根据其中的priority配置项确定优先级。
◆ 6.4.1 import()
>> 在Webpack中有两种异步加载的方式——import函数及require.ensure。require.ensure是Webpack 1支持的异步加载方式,从Webpack 2开始引入了import函数。
>> 假设bar.js的资源体积很大,并且我们在页面初次渲染的时候并不需要使用它,就可以对它进行异步加载。// foo.js
import('./bar.js').then(({ add }) => {
console.log(add(2, 3));
});
// bar.js
export function add(a, b) { return a + b;}
>> 首屏加载的JS资源地址是通过页面中的script标签来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath来指定。用import函数相当于使用一个间接资源,我们需要配置publicPath来告诉Webpack去哪里获取它。此时
>> import函数还有一个比较重要的特性。ES6 Module中要求import必须出现在代码的顶层作用域。
>> Webpack的import函数则可以在任何我们希望的时候调用。如:if (condition) { import('./a.js').then(a => { console.log(a); });} else { import('./b.js').then(b => { console.log(b); });}这种异步加
◆ 7.1 环境配置的封装
>> // webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {
output: { filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js', },
mode: ENV,};
◆ 7.3 环境变量
>> 通常我们需要为生产环境和本地环境添加不同的环境变量,在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
ocument.write(ENV);
上面的配置通过DefinePlugin设置了ENV环境变量,最终页面上输出的将会是字符串production。
除了字符串类型的值以外,我们也可以设置其他类型的环境变量。
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
IS_PRODUCTION: true,
ENV_ID: 130912098,
CONSTANTS: JSON.stringify({
TYPES: ['foo', 'bar']
})
})
[插图]注意 我们在一些值的外面加上了JSON.stringify,这是因为DefinePlugin在替换环境变量时对于字符串类型的值进行的是完全替换。假如不添加JSON.stringify的话,在替换后就会成为变量名,而非字符串值。因此对于字符串环境变量及包含字符串的对象都要
加上JSON.stringify才行。
许多框架与库都采用process.env.NODE_ENV作为一个区别开发环境和生产环境的变量。process.env是Node.js用于存放当前进程环境变量的对象;而NODE_ENV则可以让开发者指定当前的运行时环境,当它的值为production时即代表当前为生产环境,库和框架在打包时如果发现了它就可以去掉一些开发环境的代码,如警告信息和日志等。这将有助于提升代码运行速度和减小资源体积。具体配置如下:
new webpack.DefinePlugin({
process.env.NODE_ENV: 'production',
})
如果启用了mode:production,则Webapck已经设置好了process.env.NODE_ENV,不需要再人为添加了。
>> 如果启用了mode:production,则Webapck已经设置好了process.env.NODE_ENV,不需要再人为添加了。
◆ 原理
>> map文件有时会很大,但是不用担心,只要不打开开发者工具,浏览器是不会加载这些文件的,因此对于普通用户来说并没有影响。但是使用source map会有一定的安全隐患,即任何人都可以通过dev tools看到工程源码。后面我们会讲到如何解决这个问题
◆ source map配置
>> 在生产环境中由于我们会对代码进行压缩,而最常见的压缩插件UglifyjsWebpack-Plugin目前只支持完全的source-map,因此没有那么多选择,我们只能使用source-map、hidden-source-map、nosources-source-map这3者之一。下面介绍一下这3种source map在安全性方面的不同。
◆ 安全
>> bundle进行解析。如果我们想要追溯源码,则要利用一些第三方服务,将map文件上传到那上面。目前最流行的解决方案是Sentry错误跟踪平台,另外一种就是配置nosources-sourcemap
◆ 压缩JavaScript
>> 压缩JavaScript大多数时候使用的工具有两个,一个是UglifyJS(Webpack 3已集成),另一个是terser(Webpack 4已集成)。后者由于支持ES6+代码的压缩,更加面向于未来,因此官方在Webpack 4中默认使用了terser的插件terser-webpack-plugin
◆ 使chunk id更稳定
>> 解决的方法在于更改模块id的生成方式。在Webpack 3内部自带了HashedModuleIds-Plugin,它可以为每个模块按照其所在路径生成一个字符串类型的hash id。稍稍更改一下之前的配置就可以解决。plugins: [ new webpack.HashedModuleIdsPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', })
>>使用chunk id 于其不支持字符串类型的模块id,可以使用另一个由社区提供的兼容性插件webpack-hashed-module-id-plugin,可以起到一样的效果。从Webpack 4以后已经修改了模块id的生成机制,也就不再有该问题了
◆ bundle体积监控和分析
>> VS Code中有一个插件Import Cost可以帮助我们对引入模块的大小进行实时监测。每当我们在代码中引入一个新的模块(主要是node_modules中的模块)时,它都会为我们计算该模块压缩后及gzip过后将占多大体积。
另外一个很有用的工具是webpack-bundle-analyzer,它能够帮助我们分析一个bundle的构成。使用方法也很简单,只要将其添加进plugins配置即可
Const Analyzer = require(‘webpack-bundel-analyzer’).BundleAnalyzerPlugin;
Module.exports = {
//....
Plugins:[
New Analyzer()
]
}
◆ 打包优化
>> 首先重述一条软件工程领域的经验——不要过早优化
◆ 单个loader的优化
◆ 多个loader的优化
>> 8.1.3 多个loader的优化在使用HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知道rules与plugins如何一一对应。请看下面的例子,这里同时对babel-loader和ts-loader进行了Happypack的替换。
◆ 8缩小打包作用域
>> 从宏观角度来看,提升性能的方法无非两种:增加资源或者缩小范围。增加资源就是指使用更多CPU和内存,用更多的计算能力来缩短执行任务的时间,缩小范围则是针对任务本身,比如去掉冗余的流程,尽量不做重复性的工作等。前面我们说的happyPack属于增加资源。
◆ noParse
>> noParse有些库我们是希望Webpack完全不要去进行解析的,即不希望应用任何loader规则,库的内部也不会有对其他模块的依赖,那么这时可以使用noParse对其进行忽略。请看下面的例子:module.exports = { //... module: { noParse: /lodash/, }};
◆ IgnorePlugin
>> 8.2.3 IgnorePluginexclude和include是确定loader的规则范围,noParse是不去解析但仍会打包到bundle中。最后让我们再看一个插件IgnorePlugin,它可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。
◆ Cache
>> 在Webpack 5中添加了一个新的配置项“cache:{type:"filesystem"}”,它会在全局启用一个文件缓存。要注意的是,该特性目前仅仅是实验阶段,并且无法自动检测到缓存已经过期。比如我们更新了babel-loader及一些相关配置,但是由于JS源码没有发生变化,重新打包后还会是上一次的结果。
◆ 动态链接库与DllPlugin
>> DllPlugin和Code Splitting有点类似,都可以用来提取公共模块,但本质上有一些区别。Code Splitting的思路是设置一些特定的规则并在打包的过程中根据这些规则提取模块;DllPlugin则是将vendor完全拆出来,有自己的一整套Webpack配置并独立打包,在实际工程构建时就不用再对它进行任何处理,直接取用即可。因此,理论上来说,DllPlugin会比Code Splitting在打包速度上更胜一筹,但也相应地增加了配置,以及资源管理的复杂度。下面我们一步步来进行DllPlugin的配置
◆ vendor配置
>> vendor配置首先需要为动态链接库单独创建一个Webpack配置文件,比如命名为webpack.vendor.config.js,用来区别工程本身的配置文件webpack.config.js。请看下面的例子:// webpack.vendor.config.js
const path = require('path');
const webpack = require('webpack');
const dllAssetPath = path.join(__dirname, 'dll');
const dllLibraryName = 'dllExample';
module.exports = {
entry: ['react'],
output: {
path: dllAssetPath,
filename: 'vendor.js',
library: dllLibraryName,
},
plugins: [
new webpack.DllPlugin({
name: dllLibraryName,
path: path.join(dllAssetPath, 'manifest.json'),
})
],
};
配置中的entry指定了把哪些模块打包为vendor。plugins的部分我们引入了Dll-Plugin,并添加了以下配置项。
·name:导出的dll library的名字,它需要与output.library的值对应。
·path:资源清单的绝对路径,业务代码打包时将会使用这个清单进行模块索引。
◆ vendor打包
接下来我们就要打包vendor并生成资源清单了。为了后续运行方便,可以在package.json中配置一条npm script,如下所示:
// package.json
{
...
"scripts": {
"dll": "webpack --config webpack.vendor.config.js"
},
}
运行npm run dll后会生成一个dll目录,里面有两个文件vendor.js和manifest.json,前者包含了库的代码,后者则是资源清单。
可以预览一下生成的vendor.js,它以一个立即执行函数表达式的声明开始。
var dllExample = (function(params) {
// ...
})(params);
上面的dllExample正是我们在webpack.vendor.config.js中指定的dllLibraryName。
接着打开manifest.json,其大体内容如下:
{
"name": "dllExample",
"content": {
"./node_modules/fbjs/lib/invariant.js": {
"id": 0,
"buildMeta": { "providedExports": true }
},
...
}
}
manifest.json中有一个name字段,这是我们通过DllPlugin中的name配置项指定的。
◆ 链接到业务代码
>> 链接到业务代码将vendor链接到项目中很简单,这里我们将使用与DllPlugin配套的插件DllReferencePlugin,它起到一个索引和链接的作用。在工程的webpack配置文件(webpack.config.js)中,通过DllReferencePlugin来获取刚刚打包好的资源清单,然后在页面中添加vendor.js的引用就可以了。请看下面的示例:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.DllReferencePlugin({
manifest: require(path.join(__dirname, 'dll/manifest.json')),
}) ]};
// index.html
当页面执行到vendor.js时,会声明dllExample全局变量。而manifest相当于我们注入app.js的资源地图,app.js会先通过name字段找到名为dllExample的library,再进一步获取其内部模块。这就是我们在webpack.vendor.config.js中给DllPlugin的name和output.library赋相同值的原因。如果页面报“变量dllExample不存在”的错误,那么有可能就是没有指定正确的output.library,或者忘记了在业务代码前加载vendor.js。
◆ 潜在问题
>> ·page1.js和page2.js的chunk hash均发生了改变。这是我们不希望看到的,因为它们内容本身并没有改变,而现在vendor的变化却使得用户必须重新下载所有资源。
·page1.js和page.js的chunk hash没有改变。这种情况大多发生在较老版本的Webpack中,并且比第1种情况更为糟糕。因为vendor中的模块id改变了,而用户却由于没有更新缓存而继
>> 这个问题的根源在于,当我们对vendor进行操作时,本来vendor中不应该受到影响的模块却改变了它们的id。解决这个问题的方法很简单,在打包vendor时添加上HashedModuleIdsPlugin。请看下面的例子:
webpack.vendor.config.js
module.exports = {
// ...
plugins: [
new webpack.DllPlugin({
name: dllLibraryName,
path: path.join(dllAssetPath, 'manifest.json'),
}),
new webpack.HashedModuleIdsPlugin(),
]
};
>> 这个插件是在Webpack 3中被引入进来的,主要就是为了解决数字id的问题。从Webpack 3开始,模块id不仅可以是数字,也可以是字符串。HashedModuleIdsPlugin可以把id的生成算法改为根据模块的引用路径生成一个字符串hash。比如一个模块的id是2NuI(hash值),因为它的引用路径不会因为操作vendor中的其他模块而改变,id将会是统一的,这样就解决了我们前面提到的问题。
◆ tree shaking
>> 第2章我们介绍过,ES6 Module依赖关系的构建是在代码编译时而非运行时。基于这项特性Webpack提供了tree shaking功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。
◆ ES6 Module
>> ES6 Moduletree shaking只能对ES6 Module生效。有时我们会发现虽然只引用了某个库中的一个接口,却把整个库加载进来了,而bundle的体积并没有因为tree shaking而减小。这可能是由于该库是使用CommonJS的形式导出的,为了获得更好的兼容性,目前大部分的npm包还在使用CommonJS的形式。也有一些npm包同时提供了ES6 Module和CommonJS两种形式导出,我们应该尽可能使用ES6 Module形式的模块,这样tree shaking的效率更高。
◆ 使用Webpack进行依赖关系构建
>> 使用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: [
// 这里一定要加上
[@babel/preset-env,{module:false}]
],
}.
}],
}],
},
};
◆ webpack-dashboard
>> webpack-dashboardWebpack每一次构建结束后都会在控制台输出一些打包相关的信息,但是这些信息是以列表的形式展示的,有时会显得不够直观。webpack-dashboard就是用来更好地展示这些信息的。安装命令如下:npm install webpack-dashboard我们需要把webpack-dashboard作为插件添加到webpack配置中
◆ webpack-merge
>> 通过Object.assign我们没有办法准确找到CSS的规则并进行替换,所以必须替换掉整个module的配置
下面我们看一下如何用webpack-merge来解决这个问题。安装命令如下:
npm install webpack-merge
更改webpack.prod.js如下:
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
, {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
}
]
},
});
可以看到,我们用merge.smart替换了Object.assign,这就是webpack-merge“聪明”的地方。它在合并module.rules的过程中会以test属性作为标识符,当发现有相同项出现的时候会以后面的规则覆盖前面的规则,这样我们就不必添加冗余代码了。
module.exports = merge.smart(commonConfig
◆ speed-measure-webpack-plugin
>> 觉得Webpack构建很慢但又不清楚如何下手优化吗?那么可以试试speed-measure-webpack-plugin这个插件(简称SMP)。SMP可以分析出Webpack整个打包过程中在各个loader和plugin上耗费的时间,这将会有助于找出构建过程中的性能瓶颈。
◆ 开启HMR
>> 如果应用的逻辑比较简单,我们可以直接手动添加代码来开启HMR。比如下面这个例子:// index.js
import { add } from 'util.js';
add(2, 3);
if (module.hot) {
module.hot.accept();
}
◆ HMR API示例
>>index.js及其依赖只要发生改变就在当前环境下全部重新执行一遍。但是我们发现它会带来一个问题:在当前的运行时我们已经有了一个setInterval,而每次HMR过后又会添加新的setInterval,并没有对之前的进行清除,所以最后我们会看到屏幕上有不同的数字闪来闪去。从图9-9中的console信息可以看出setInterval确实执行了多次
>> 为了避免这个问题,我们可以让HMR不对index.js生效。也就是说,当index.js发生改变时,就直接让整个页面刷新,以防止逻辑出现问题,但对于其他模块来说我们还想让HMR继续生效。那么可以将上面的代码修改如下:
if (module.hot) {
module.hot.decline(); //当index.js自身改变时禁止使用HMR进行更新,此时只能刷新整个页面。
module.hot.accept(['./util.js']);//当utils.js改变时依然可以启用HMR更新
}
◆ Rollup
>> 如果用Webpack与Rollup进行比较的话,那么Webpack的优势在于它更全面,基于“一切皆模块”的思想而衍生出丰富的loader和plugin可以满足各种使用场景;而Rollup则更像一把手术刀,它更专注于JavaScript的打包。当然Rollup也支持许多其他类型的模块,但是总体而言在通用性上还是不如Webpack。如果当前的项目需求仅仅是打包JavaScript,比如一个JavaScript库,那么Rollup很多时候会是我们的第一选择
◆ 零配置
>> parcel build index.htmlParcel会创建一个dist目录,并在其中生成打包压缩后的资源,如图10-3所示。[插图]图10-3 Parcel生成的dist目录从上面可以看出和Webpack的一些不同之处。首先,Parcel是可以用HTML文件作为项目入口的,从HTML开始再进一步寻找其依赖的资源;并且可以发现对于最后产出的资源,Parcel已经自动为其生成了hash版本号及source map。另外,如果打开产出的JS文件会发现,内容都是压缩过的,
>> 来说,没有任何配置是几乎不可能的,因为如果完全没有配置也就失去了定制性。虽然Parcel并没有属于自己的配置文件,但本质上它是把配置进行了切分,交给Babel、PostHTML和PostCSS等一些特定的工具进行分别管理。比如当项目中有.babelrc时,那么Parcel打包时就会采用它作为ES6代码解析的配置。