Webpack 体验 Tree Shaking
Tree-shaking的字面意思就是摇树。
一般伴随着摇树这样一个动作我们树上的这些枯树枝和树叶就会掉落下来。
那我们这里要说的Tree-shaking也是相同的道理,不过我们这里摇掉的使我们代码当中那些没有用到的部分,那这部分代码更专业的说叫未引用代码(dead-code)。
webpack生产模式优化中就有这么一个非常有用的功能,那他可以自动检测出我们代码中那些未引用的代码,然后移除掉他们,那我们这里先来体验一下这样一个功能的效果。
我们这里的代码也非常简单只有两个文件,其中components.js文件中导出了一些函数, 然后每一个函数分别模拟了一个组件。
其中button组件函数中,在return过后还执行了一个console.log语句,很明显这就属于未引用代码。
export const Button = () => {
return document.createElement('button')
console.log('dead-code');
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
除此之外还有一个index.js文件,那index.js文件当中就是导入了components,但是需要注意的是我们这只是导入了components当中的button这样一个成员。
这也就会导致我们代码当中,特别是components里面会有很多的地方都用不到,那这些用不到的地方对于我们打包过后的结果就是冗余的。
取出冗余代码是我们生产环境优化当中非常重要的一个工作,而webpack的Tree-shaking就很好的实现了这样一点。
import { Button } from './components'
document.body.appendChild(Button())
我们尝试以production的模式去运行打包
yarn webpack --mode production
打包完成过后我们打开bundle.js我们可以发现这些冗余的代码根本就没有输出,那这就是tree-shaking这样一个特性,工作过后的一个效果。
那tree-shaking这个功能会在生产模式下自动取开启。
Webpack 使用 Tree Shaking
需要注意的是tree-shaking并不是webpack中的某一个配置选项,他是一组功能搭配使用过后的效果。
那这组功能会在生产模式下自动开启,但是由于目前官方文档当中对于tree-shaking的介绍有点混乱,所以我们这里再来介绍一下它在其他模式下如何一步一步手动的去开启。
顺便通过这样一个过程我们去了解tree-shaking的工作过程,以及一些其他的优化功能,这里我们还是以刚刚相同的一个项目。
我们再次回到命令行终端,运行webpack打包,不过这一次我们不再使用production模式而是使用none,也就是我们不开启任何内置功能和插件。
yarn webpack
打包完成过后我们来找到输出的bundle.js文件,这里的打包结果和我们之前所看到是一样的。也就是我们一个模块会对应这里的一个函数。
这里我们需要注意看一下components.js这样一个模块所对应的函数,这里的link函数和heading函数虽然外部并没有使用,但我们这里仍然是导出了。
很明显这些导出是没有意义的,我们可以借助一些优化功能把他们去掉,我们打开webpack的配置文件。
这里我们在配置文件中添加一个optimization的属性。那这个属性呢就是集中去配置webpack内部的一些优化功能的。
在这个属性当中我们可以先开启一个叫做usedExports选项,表示我们在输出结果中只导出那些外部使用了的成员。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true
}
}
完成过后我们重新回到命令行打包,我们再来看下输出的bundle.js。
此时就会发现,components模块所对应的这个函数中就不再会去导出link和heading这两个函数了,而且我们vscode也非常友好的讲这两个函数的字体变淡,表示他们未被使用。
此时我们就可以去开启webpack的代码压缩功能,去压缩掉这些没有用到的代码。
我们再回到配置文件当中,这里我们再optimization中取开启minimize,然后我们回到命令行再次打包。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
minimize: true
}
}
此时我们bundle.js当中这些未引用的代码就都被移除掉了,这就是tree-shaking的实现。整个过程我们用到了两个优化功能,一个是usedExports另一个是minimize。
如果说真的把我们的代码看做一棵大树的话,那你可以理解成usedExports就是用来在这个大树上标记哪些是枯树叶枯树枝,然后minimize就是负责把这些枯树叶树枝全都摇下来。
Webpack 合并模块
除了usedExports以外,我们还可以使用一个concatenteModules属性去继续优化我们的输出,普通的打包结果是将我们每一个模块最终放在一个单独的函数当中,这样的话如果我们的模块很多也就意味着我们在输出结果中会有很多这样的模块函数。
我们回到配置文件当中,这里开启concatenateModules, 为了可以更好的看到效果我们这里先去关掉minimize,然后我们重新打包。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
concatenateModules: true,
// minimize: true
}
}
此时我们bundle.js当中就不再是一个模块对应一个函数了,而是把所有的模块都放到了同一个函数当中,那concatnateModules这样一个配置的作用呢就是尽可能将所有的模块全部合并到一起然后输出到一个函数中。
这样的话既提升了运行效率又减少了代码体积。那这个特性又被称之为Scope Hoisting 也就是作用域提升,他是webpack3中去添加的一个特性,如果说此时我们再去配合minimize那这样的话我们的代码体积就会又减小很多。
Webpack Tree Shaking 与 Babel
由于早期webpack早期发展非常快,那变化也就比较多,所以当我们去找资料时我们得到的结果并不一定适用于我们当前所使用的版本,对于tree-shaking的资料更是如此。
很多资料中都表示如果我们使用了babel-loader就会导致tree-shaking失效。针对于这个问题,这里我们来统一说明一下。
首先大家需要明确一点的是tree-shaking的实现,他的前提是必须要使用ES Modules去组织我们的代码,也就是我们交给webpack去处理的代码他必须还是使用ES Modules的方式来去实现的模块化。
那为什么这么说呢,我们都应该知道,webpack在打包所有模块之前,他先是将模块根据配置交给不同的loader去处理,最后再将所有loader处理过后的结果打包到一起。
我们为了转换我们代码当中ECMAScript的新特性很多时候我们都会选择babel-loader去处理我们的js。
而在babel转换我们的代码时就有可能处理掉我们代码当中ES Modules 把他们转换成CommonJS,当然了,这取决于我们有没有使用转换ES Modules的插件。
例如在我们的项目当中,我们所使用的的@babel/preset-env这个插件集合,它里面就有这么个插件,所以说大preset-env这个插件集合工作的时候,我们代码当中ES Modules的部分就应该会被他转换成CommonJS的方式。
那webpack再去打包时,他拿到的代码就是以CommonJS的方式组织的代码,所以说tree-shaking他就不能生效。我们具体来尝试一下。
需要注意,我们这为了可以更容易分别结果,我们只开启usedExports, 然后我们重新打包,查看bundle.js。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
modules: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
这里你会发现我们的结果并不是像刚刚说的那样,这里usedExports功能正常的工作了,这也就说明如果我们开启压缩代码的话,那这些为引用的代码依然会被移除。那tree-shaking并没有失效。
这是因为在最新版本的babel-loader当中就已经自动帮我们关闭了ES Modules转换的插件,我们可以在node_modules当中先去找到babel-loader的模块,我们可以看一下他的源代码,他在injectcaller这个文件中已经标识了我们当前的环境是支持ES Modules的。
然后我们再去找到我们所使用的到的preset-env这个模块,在200多行可以发现,这里根据我们injectcaller中的标识禁用了ES Module的转换。
所以说webpack最终打包时他得到的还是ES Modules的代码,那tree-shaking自然也就可以正常工作了,当然了,我们这里只是定位的找到了源代码当中相关的一些信息。如果你需要仔细了解这个东西的话,那你可以再去翻看一下babel的源代码。
那我们这里也可以尝试在babel的preset配置当中强制去开启这个插件来去试一下。不过给preset添加配置的方式比较特别,很多人都容易配错。所以一定要注意。
那他需要把我们预设这个数组中的成员再次定义成一个数组,然后这个数组当中的第一个成员就是我们所使用的的preset的名称。第二个成员就是我们给这个preset定义的对象,这里不能搞错,是数组套数组。
这里我们将这个对象的modules属性设置为commonjs,那默认这个属性值是auto 也就是根据环境去判断是否开启ES Module插件。
那我们这里将它设置为commonjs也就表示我们需要强制使用babel的ES Modules插件把我们代码当中的ES Moudle转换为commonjs。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
modules: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'commonjs'}],
]
}
}
}
]
},
optimization: {
usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
我们打包后查看你就会发现我们刚刚所配置的usedExports就没有办法生效了。即便我们再去开启压缩代码,那tree-shaking也是没有办法正常工作的。
总结一下,我们这里通过实验发现,最新版本的babel-loader并不会导致tree-shaking失效,如果说你不确定,最简单的办法就是在配置文件当中将preset-env当中的这个modules这个数值为false,这样就确保我们这个preset-env里面不会再去开启ES Module转换的插件。
那这样就同时确保了我们tree-shaking的一个工作的前提。
['@babel/preset-env', { modules: false}]
另外呢我们刚刚这样一个探索的过程也值得你仔细再去琢磨一下,因为通过这样的探索,你能够了解到很多知其所以然的内容。
Webpack sideEffects
weboack4中还新增了一个叫做sideEffects的新特性。
它允许我们通过配置的方式去标识我们的代码是否有副作用。从而为tree-shaking提供更大的压缩空间。
副作用是指模块执行时候除了导出成员是否还做了一些其他的事情,那这样一个特性他一般只有我们在去开发一个npm模块时才会用到。
但是因为官网当中把sideEffects的介绍跟tree-shaking混到了一起,所以很多人误认为他俩是因果关系。其实他俩真的没有那么大的关系。
那我们这先把side Effects搞明白,也就能理解为什么了。
这里我们先设计一下能够让side Effects 发挥效果的一个场景,我们基于刚刚这个案例的基础之上,我们把components拆分出了多个组件文件。然后在index.js当中我们集中导出,便于外界导入。
这是一种非常常见的同类文件组织方式。我们在回到入口文件当中去导入components当中的button成员。
那这样的话就会出现一个问题,因为我们在这载入的是components目录下的这个index.js, 那index.js当中又载入了所有的组件模块。
那这就会导致我们只想载入button组件,但是所有的组件模块都会被加载执行。
查看打包结果之后你会发现所有组件的模块确实都被打包了,那side effects特性就可以用来解决此类问题。
我们打开webpack的配置文件,然后在optimization中去开启这个属性 sideEffects: true,注意这个特性在production模式下同样也会自动开启。
{
optimization: {
sideEffects: true
// usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
我们开启这个特性过后,webpack在打包时他就会先检查当前代码所属的这个package.json当中有没有sideEffects的标识,以此来判断这个模块是否有副作用。
如果说这个模块没有副作用,那这些没有用到的模块就不再会打包。我们可以打开我们的package.json,然后我们尝试去添加一个sideEffects的字段,我们把它设置为false。
{
"sideEffects": false
}
那这样的话就标识我们当前这个package.json所影响的这个项目,他当中所有的代码都没有副作用。那一但这些没有用到的模块他没有副作用了,他就会被移除掉。
我们打包过后查看打包的bundle.js文件, 那此时我们那些没有用到的模块就不再会被打包进来了,这就是sideEffects的作用。
注意我们这里设置了两个地方,我们先在webpack的配置当中去开启了sideEffects,他是用来去开启这个功能,而在package.json当中我们添加sideEffects他是用来标识我们的代码是没有副作用的,他俩不是一个意思,不要弄混了。
Webpack sideEffects 注意事项
使用sideEffects这个功能的前提就是确定你的代码没有副作用。否则的话在webpack打包时就会误删掉那些有副作用的代码。
例如我们准备了一个extend.js这样一个文件。在这个文件当中我们并没有向外导出任何成员。他仅仅是在Number这个对象的原型上挂载了一个pad方法,用来为数字去添加前面的导0。
Number.prototype.pad = functuon(size) {
let result = this + '';
while (result.lengtj < size>) {
result = '0' + result
}
return result;
}
这是一种非常常见的基于原型的扩展方法,我们回到入口文件去导入这个extend.js, 我们导入了这个模块过后我们就可以使用他为Number所提供的扩展方法。
import './extend.js';
console.log((8).pad(3));
这里为Number做扩展方法的这样一个操作就属于我们extend这个模块的副作用,因为在导入了这个模块过后,我们的Number的原型上就多了一个方法,这就是副作用。
那此时如果我们还标识我们项目当中所有代码都没有副作用的话,那打包之后我们就会发现,我们刚刚的扩展操作他是不会被打包进来的。因为他是副作用代码。但是你在你的配置当中已经声明了没有副作用。所以说他们就被移除掉了。
那除此之外呢还有我们再代码当中载入的css模块,他也都属于副作用模块,同样会面临刚刚这样一种问题。
那解决的办法就是在我们的package.json当中去关掉副作用,或者是标识一下我们当前这个项目当中哪一些文件是有副作用的,那这样的话webpack就不会去忽略这些有副作用的模块了。
我们可以打开package.json,我们吧sideEffects的false改成一个数组。然后我们再去添加一下extend.js这个文件的路径,还有我们global.css这个文件的路径,当然了这里我们可以使用路径通配符的方式来去配置。*.css。
{
"sideEffects": [
"./src/extend.js",
"./src/global.css"
]
}
此时我们再来找到打包结果我们就会发现,这个有副作用的两个模块也会被同时打包进来了。
那以上就是我们对webpack内置的一些优化属性的一些介绍,总之这些特性呢,他都是为了弥补javascript早起在设计上的一些遗留问题,那随着像webpack这一类技术的发展,javascript确实越来越好。
Webpack 代码分割
通过webpack实现前端项目整体模块化的优势固然很明显,但是他同样存在一些笔端,那就是我们项目当中所有的代码最终都会被打包到一起,那试想一下如果说我们的应用非常复杂,模块非常多的话那我们的打包结果就会特别的大,很多时候超过2-3M也是非常常见的事。
而实施情况是,大多数时候我们在应用开始工作时并不是我们所有的模块都是必须要加载进来的。但是呢,这些模块又被全部打包到一起,那我们需要任何一个模块都必须把整体加载下来过后才能使用。
而我们的应用呢,一般又是运行在浏览器端,那这就意味着我们会浪费掉很多的流量和带宽,那更为合理的方案呢就是把我们的打包结果按照一定的规则去分离到多个bundle当中,然后根据我们应用的运行需要按需去加载这些模块。
这样的话我们就可以大大提高我们应用的响应速度以及他的运行效率。
那可能有人会想起来我们在一开始的时候说过,webpack就是把我们项目中散落的模块合并到一起从而去提高运行效率。
那我们这里又在说他应该把他分离开,那这两个说法是不是自相矛盾呢,其实这并不是矛盾,只是物极必反而已。
那资源太大了不行,太碎了也不行,我们项目中划分的这种模块的颗粒度一般都会非常的细,那很多时候我们一个模块只是提供了一个小小的工具函数,它并不能形成一个完整的功能单元。
那如果我们不把这些散落的模块合并到一起,那就有可能我们再去运行一个小小的功能时就会加载非常多的模块。
而我们目前这种主流的HTTP1.1版本,它本身就有很多缺陷,例如我们并不能同时对同一个域名下发起很多次的并行请求,而且我们一次的请求呢他都会有一定的延迟。
另外我们每次请求除了传输具体的内容以外还会有额外的请求头和响应头,那当我们有大量的这种请求情况下,那这些请求头响应头加在一起也是很大的浪费。
那综上所述,模块化打包肯定是有必要的,不过呢,在我们的应用越来越大过后,我们也要慢慢的开始学会变通。
那为了解决这样的问题,webpack呢他支持一种分包的功能,你也可以把这种功能称之为代码分割。
他通过把我们的模块按照我们所设计的一个规则打包到不同的bundle当中,从而去提高我们应用的响应速度。
目前呢webpack去实现分包的方式主要有两种,那第一种就是我们根据我们的业务去配置不同的打包入口,也就是我们会有同时多个打包入口同时打包,那这时候就会输出多个打包结果。
那第二种呢就是采用ES Module的动态导入这样一个功能去实现模块的按需加载,那这个时候呢,我们webpack他也会自动的把我们动态导入的这个模块单独的输出到一个bundle当中。
那接下来我们来具体来看这两种方式。
Webpack 多入口打包
多入口打包一般适用于传统的多页应用程序,那最常见的划分规则就是一个页面去对应一个打包入口。
那对于不同页面之间的公共部分,再去提取到公共的结果当中。
那这种方式呢使用起来非常简单。我们回到项目中具体来看。我们准备了一个多页应用的示例,我们这里有两个页面,分别是index和album页面。
那代码的组织逻辑也非常简单,index.js负责实现index页面所有功能。而album.js负责实现album页面所有功能。
global.css和fetch.js都是公共部分,下面我们尝试为这个案例配置多个打包入口。
一般我们配置文件当中的entry属性他只会配置一个文件名路径,也就是说我们只会配置一个打包入口,那如果我们需要配置多个入口的话,我们可以把entry定义成一个对象,那需要注意的是这里是一个对象,而不是数组。
因为如果定义成数组的话,那他就是把多个文件打包到一起,那对于整个应用来讲的话还是一个入口。
那我们这里需要的是多入口所以我们配置成一个对象。那在这个对象中一个属性就是一路入口。那我们属性名就是这个入口的名称。然后值就是这个入口所对应的文件路径。
那我们这里配置的就是index和album这两个js所对应的文件路径,那一但我们这里配置为多入口,那我们输出的文件名也需要修改,那这俩两个入口也就意味着会有两个打包结果,我们不能都叫bundle.js。
所以说我们这里可以为我们filename属性去添加一个[name]这种占位符的方式来去动态输出文件名。
那么[name]最终就会被替换成入口的名称,那在我们这就是index和album。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
filename: `[name].bundle.js`,
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`
})
]
}
})
那我们这次打包就会有两个入口,完成以后我们找到输出的目录,那在输出的目录我们就能看到两个入口各自打包过后的结果了。
但是这里呢,还是会有一个小问题,我们打开任意一个输出的html文件,这时候你就会发现这两个打包结果他都被页面同时载入了。
而我们希望的是,一个页面只使用他对应的那个输出结果,所以说这里我们还需要继续去修改配置文件。
那我们回到配置文件当中,我们找到输出html的插件, 那之前我们就介绍过,这个插件他默认就会输出一个自动注入所有打包结果的html。
[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`
})
]
那如果说我们需要指定我们输出的html他所使用的bundle, 那我们就可以使用chunk属性来去设置。
那我们每一个打包入口呢他就会形成一个独立的chunk,那我们在这分别为这两个页面配置不同的chunk。
[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`,
chunk: ['index']
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`,
chunk: ['album']
})
]
完成以后再次重新打包,那我们这次的打包结果呢他就完全正常了。
那以上就是我们配置多入口的打包方式,以及我们如何在输出的html当中指定我们需要注入的bundle。
Webpack 提取公共模块
多入口打包本身非常容易理解也非常容易使用,但是他也存在一个小小的问题,那就是我们再不同的打包入口当中他一定会有那么一些公共的部分。
那按照之前这种多入口的打包方式,就会出现我们再不同的打包结果当中会有相同的模块出现。例如在我们这里index入口和album入口中就共同使用了global.css和fetch.js这两个公共的模块。
那这里是因为我们的示例比较简单,所以说重复的影响不会有那么大,但是如果说我们共同使用的是jQuery或者是Vue这种体积比较大的模块,那影响的话就会特别的大。
所以说我们需要把这些公共的模块去提取到一个单独的bundle当中。
那webpack当中实现公共模块提取的方式也非常简单,我们只需要在优化配置当中去开启一个叫做splitChunks的一个功能就可以了。
我们回到配置文件当中,我们再optimization中添加splitChunks属性,那这个属性他需要我们配置一个chunks属性,然后我们将这个chunks属性设置为all,就表示我们会把所有的公共模块都提取到单独的bundle当中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
splitChunks: {
chunks: 'all'
}
}
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`,
chunk: ['index']
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`,
chunk: ['album']
})
]
}
})
打包过后我们的dist目录下就会生成额外的一个js文件,在这个文件当中就是我们index和album这两个入口公共的模块部分了。
Webpack 动态导入
按需加载是我们开发浏览器应用当中一个非常常见的需求,那一般我们常说的按需加载指的是加载数据。那我们这里所说的按需加载呢,指的是我们再应用运行过程中需要某个模块时我们才去加载这个模块。
那这种方式呢,可以极大地节省我们的带宽和流量。
那webpack中支持使用动态导入的这种方式来去实现模块的按需加载,而且呢,所有动态导入的模块都会自动被提取到单独的bundle中,从而实现分包,
那相比于多入口的这种方式动态导入他更为灵活,因为我们可以通过代码的逻辑去控制我们需不需要加载某个模块,或者是我们什么时候加载某个模块。
而我们分包的目的中就有很重要的一点就是,要让模块实现按需加载,来提高应用的响应速度。我们具体来看如何使用。
这里我们已经设计好了一个可以发挥按需加载作用的场景,在这个页面的主体区域,如果我们访问的是文章页的话,我们得到的就是一个文章列表,如果我们访问的是相册页面,我们显示的就是相册列表。
回到代码当中我们来看下他的实现方式,目前我们文章列表所对应的就是post组件,而相册列表对应的就是album组件,我们在打包入口当中同时导入这两个模块。
然后这里的逻辑就是当我们锚点发生变化时,我们去根据锚点的值决定要去显示哪个组件。
import posts from './posts/posts';
import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
mainElement.appendChild(post());
} else if (hash === '#album') {
mainElement.appendChild(album());
}
}
render();
window.addEventListener('hashchange', render);
那这里就会存在浪费的可能性,是想一下,如果说用户他打开我们的应用过后只是访问了其中的一个页面。那另外一个页面所对应的这个组件的加载就是浪费。
所以说我们这里如果是动态导入组件,那就不会存在浪费的问题了。我们这里可以先注释掉静态导入。
动态导入使用的就是ES Module标准当中的动态导入,我们在需要动态导入的地方通过import这个函数,然后导入我们指定的路径。
那这个方法返回的就是一个Promise,然后在这个Promise的then方法中我们就可以拿到模块对象。
由于我们这里使用的是默认导出,所以我们这需要解构我们模块对象的default,我们把它放在posts这个变量当中,拿到这个成员过后我们再来使用这个成员去创建界面上的元素。
同理我们的album组件也应该是如此。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
// mainElement.appendChild(posts());
import('./posts/posts').then(({ default: posts}) => {
mainElement.appendChild(posts());
})
} else if (hash === '#album') {
// mainElement.appendChild(album());
import('./album/album').then(({ default: album}) => {
mainElement.appendChild(album());
})
}
}
render();
window.addEventListener('hashchange', render);
完成以后我们在此回到浏览器,此时我们页面仍然可以正常工作,我们回到开发工具当中,重新运行打包,然后去看看此时我们打包的结果是什么样子的。
打包完成过后我们打开输出的dist目录,在此时我们的dist目录下就会多出三个js文件,那这三个js文件呢实际上就是由动态导入,自动分包所产生的。
那这三个文件分别是我们刚刚导入的两个模块以及这两个模板当中公共的部分所提取出来的bundle, 那这就是动态导入在webpack当中的一个使用。
那整个过程我们无需配置任何一个地方只需要按照ES Module动态导入成员的方式去导入模块就可以了。那webpack内部呢会自动处理分包和按需加载。
那如果说你使用的是单页应用开发框架,比如react或者vue的话,那在你项目当中的路由映射组件就可以通过这种动态导入的方式实现按需加载。
Webpack 魔法注释
默认通过动态导入产生的bundle文件,他的名称就只是一个序号,这并没有什么不好的,因为在生产环境当中,大多数时候我们是根本不用关心资源文件的名称是什么。
但是说如果你还是需要给这些bundle命名的话,那你可以使用webpack所特有的模板注释来去实现。
那具体的使用方式就是在调用import函数的参数位置我们去添加一个行内注释,那这个注释有一个特定的格式,就是通过/* webpackChunkName: 名称 */ 那这样的话我们就可以给分包所产生的bundle起上名字了。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
// mainElement.appendChild(posts());
import(/* webpackChunkName: posts */'./posts/posts').then(({ default: posts}) => {
mainElement.appendChild(posts());
})
} else if (hash === '#album') {
// mainElement.appendChild(album());
import(/* webpackChunkName: album */'./album/album').then(({ default: album}) => {
mainElement.appendChild(album());
})
}
}
render();
window.addEventListener('hashchange', render);
我们重新打包,那我们生成的bundle文件他的name就会使用我们刚刚注释当中所提供的名称了。
那如果说你的chunkName是相同的话,那相同的chunkName最终就会被打包到一起。那例如我们这里可以把这两个chunk的chunkName设置为components, 这样的话他们就一致了。然后我们再次运行打包。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
// mainElement.appendChild(posts());
import(/* webpackChunkName: components */'./posts/posts').then(({ default: posts}) => {
mainElement.appendChild(posts());
})
} else if (hash === '#album') {
// mainElement.appendChild(album());
import(/* webpackChunkName: components */'./album/album').then(({ default: album}) => {
mainElement.appendChild(album());
})
}
}
render();
window.addEventListener('hashchange', render);
那此时呢这两个模块他都会被打包到components.bundle.js这样一个文件当中。
那借助于这样一个特点,你就可以根据自己的实际情况灵活组织我们动态加载的模块所输出的文件了。
Webpack MiniCssExtractPlugin
MiniCssExtractPlugin他是一个可以将css代码从打包结果当中提取出来的插件,那通过这个插件我们就可以时间css模块的按需加载。
他的使用也非常简单,我们回到项目当中,我们先安装一下这个插件。
yarn add mini-css-extract-plugin
我们打开webpack的配置文件,这里我们需要先导入这个插件的模块, 那导入过后我们就可以将这个插件添加到配置对象的plugins数组当中。
那这样的话MiniCssExtractPlugin他在工作时就会自动提取我们代码当中的css到一个单独文件当中。
那除此以外呢,目前我们所使用的样式模块,他是先交给css-loader去解析,然后再交给style-loader去处理。这里的style-loader他的作用就是将我们样式代码通过style标签的方式注入到页面当中,从而使样式可以工作。
那使用MiniCssExtractPlugin的话,我们的样式就会单独存放到文件当中也就不需要style标签,而是直接通过link的方式去引入。所以说这里我们就不再需要style-loader了。
取而代之我们所使用的是MiniCssExtractPlugin当中所提供的一个loader,来去实现我们样式文件通过link标签的方式去注入。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin()
]
}
})
完成以后我们重新打包,完成过后我们就可以在dist目录中看到提取出来的样式文件了。
不过这里我们需要注意一点,如果说样式文件他的体积不是很大的话,那提取他到单个文件当中,那效果可能会适得其反。
我个人的经验是如果说css文件超过了150kb左右才需要考虑是否将他提取到单独文件当中。
否则的话,其实css嵌入到代码当中他减少了一次请求,效果可能会更好。
Webpack OptimizeCssAssetsWebpackPlugin
使用了MiniCssExtractPlugin过后样式文件就可以被提取到单独的css文件当中了,但是这里同样会有一个小问题。
我们回到命令行,这里我们尝试以生产模式去运行打包。
yarn webpack --mode production
那按照之前的了解,在生产模式下webpack会自动去压缩输出的结果。但是我们这里打开输出的样式文件会发现我们样式文件根本没有任何的变化。
那这是因为,webpack内置的压缩插件,他紧紧是针对于js文件的压缩,那对于其他资源文件压缩,都需要额外的插件去支持。
webpack官方推荐了一个optimize-css-assets-webpack-plugin, 我们可以使用这个插件来去压缩我们的样式文件。
那我们先来安装一下这个插件。
yarn add optimize-css-assets-webpack-plugin
回到配置文件当中,我们需要先导入这个插件,导入完成过后呢,我们去把这个插件添加到配置对象的plugins数组当中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
我们再次使用生产模式打包, 那这次打包完成过后呢,我们的样式文件就以压缩文件的格式去输出了。
不过这里还有一个额外的小点,可能大家在官方文档当中会发现文档当中这个插件并不是配置在plugins数组当中的。而是添加到了optimization属性当中的minimize属性当中。
那这是为什么呢?其实也非常简单,如果说我们把这个插件配置到plugins数组当中,那这个插件在任何情况下都会正常工作。而配置在minimize数组当中的话,那只会在minimize这样一个特性开启时才会工作。
所以说webpack建议,像这种压缩类的插件我们应该配置到minimize数组当中,以便于我们可以通过minimize这个选项去统一控制。
那这里我们尝试把这个插件移至到我们的optimization属性当中的minimize数组当中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
重新打包,那此时如果说我们没有开启压缩这个功能的话,那这个插件他就不会工作。反之如果说我们以生产模式打包,那么minimize属性就会自动开启,那这个压缩插件就会自动工作。我们的样式文件也就会被压缩。
但是这么配置也有一个小小的缺点,我们可以看一下输出的js文件,那这时候你会发现,原本可以自动压缩的js,这次确不能自动压缩了。
那这是因为我们这里设置了minimize这个数组,webpack认为我们如果配置了这个数组,那就是要去自定义所使用的的压缩器插件。那内部的js压缩器就会被覆盖掉,所以说我们这里需要再手动的把他添加回来。
那内置的js压缩插件叫做terser-webpack-plugin,我们需要安装这个模块,
yarn add terser-webpack-plugin --dev
安装完成过后我们再来把这个插件手动的去添加到minimize这个数组当中
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
那这样的话,如果我们再以生产模式运行打包,然后js文件,css文件都可以正常被压缩了。那如果说我们以普通模式打包,也就是不开启压缩的话,那他也就不会以压缩的形式输出了。
Webpack 输出文件名 Hash
一般我们去部署前端的资源文件时,都会启用服务器的静态资源缓存,那这样的话对应用户的浏览器而言,他就可以缓存住我们应用当中的静态资源。那后续就不再需要请求服务器,得到这些静态资源文件了。
那这样,整体我们应用的响应速度就有一个大幅度的提升。
不过呢,开启静态资源的客户端缓存,他也会有一些小小的问题,那如果说我们在缓存策略中我们的缓存失效时间设置的过短的话,那效果就不是特别明显。
那如果说我们把过期时间设置的比较长,那一但说我们在这个过程中应用发生了更新重新部署过后又没有办法及时更新到客户端。
那为了解决这样一个问题我们建议在生产模式下,我们需要给输出的文件名当中添加Hash值,那这样的话,一旦我们的资源文件,发生改变,那我们的文件名称也可以跟着一起去变化。
那对于客户端而言,全新的文件名就是全新的请求,那也就没有缓存的问题,那这样的话我们就可以把服务端缓存策略的缓存时间设置的非常长,也就不用担心文件更新过后的问题。
webpack中的filename属性和绝大多数插件的filename属性,都支持通过占位符的方式来去为文件名设置hash,不过这里支持三种hash效果各不相同。那这里我们来分别尝试一下。
首先就是最普通的hash我们可以通过[hash]然后去拿到,那这个hash它实际上是整个项目级别的,也就是说一旦项目当中有任何一个地方发生改动,那我们这一次打包过程当中的hash值都会发生变化。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[hash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[hash].bundle.css'
}),
]
}
})
我们这里可以在任意一个代码当中做下修改,然后尝试重新打包,那此时你就会发现,我们的hash值全部发生改变了。
其次呢是chunkhash,那这个hash是chunk级别的,也就是在我们打包过程中,只要是同一路的打包,那chunkhash他都是相同的。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[chunkhash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[chunkhash].bundle.css'
}),
]
}
})
那我们这里虽然只配置了一个打包入口index,但是呢在我们的代码当中通过动态导入的方式又分别形成了两路chunk分别是posts和album。
那样式文件是从这个代码当中单独提取出来的,所以说他并不是单独的chunk,所以我们这所看到的结果呢就是main和posts还是album他们三者chunkhash各不相同。
而我们css和所对应的js文件,他们二者的chunkhash是完全一样的。因为他们是同一路。
那这里我们现在index当中尝试做一些修改,然后重新打包。那这时候你会发现,只有main.bundle的文件名发生了变化,其他的文件都没有变。
然后我们再尝试在posts.js文件中做一些修改,那此次我们posts所输出的js和css都会发生变化,因为我们刚刚说过了,他们是属于同一个chunk。
至于main.bundle他也会发生变化的原因是因为我们posts所生成的这个js文件和css文件他的文件名发生变化,那我们再入口文件中去引入他们的路径也会发生变化。所以说mian.chunk他算是一个被动的改变。
那相比于普通的hash,chunkhash的控制要更精确一点。
最后还有一个contenthash,那这个hash他其实是文件级别的hash,他其实是再根据输出文件的内容生成的hash值。也就是说只要是不同的文件,他就有不同的hash值。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[contenthash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[contenthash].bundle.css'
}),
]
}
})
那这里我们先尝试在index当中做一些修改,然后重新打包,那此时同样只有main.bundle文件名发生了变化。
然后我们再去修改posts.css,那这时候你会发现,posts所对应的样式文件的文件名就发生了变化,而main.bundle同样也是因为路径的原因才被动更新。
那相比于前两者,contenthash应该是解决缓存问题最好的方式了,因为他精确的定位到了文件级别的hash,那只有当这个文件发生了变化才有可能去更新掉他的这个文件名,那这个实际上是最适合我们去解决缓存问题的。
那另外如果说你觉得我们这个20位长度的hash太长的话,那webpack还允许我们去指定hash的长度,我们可以在占位符里面通过冒号跟一个数组([:8])的方式来去指定我们hash的长度,我们这里设置长度为8。
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css'
})
那总的来说我个人觉得如果说是控制缓存的话,8位的contenthash应该是最好的选择了。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!