打包工具之Webpack篇(四) | 青训营笔记

162 阅读13分钟

这是我参与第四届青训营笔记创作活动的第十一天

上一篇讲到webpack关于提升打包构建速度的部分,那我们接着继续讲剩下的。

Include/Exclude

Include/Exclude对于生产模式和开发模式都可以。

为什么

为什么会用到Include/Exclude的原因是开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。

是什么

include

指包含,只处理 xxx 文件。比如只处理node_modules下的文件,其他文件都处理的话我们可以这么写。

//只处理node_modules下的文件,其他文件都处理
include:path.resolve(_dirname,"../src")
exclude

指排除,除了 xxx 文件以外其他文件都处理。还用上面那个例子的话就可以怎这么写。

exclude:"node_modules"

怎么用

扩展

1、webpack中有哪些配置可以指定需要处理的文件?

答: webpack配置时,为了提高解析速度,需要指定需要处理的文件。 有三种配置可以指定需要处理的文件:

  1. test
test: /.jsx?$/,
  1. include:指定需要处理的文件。可以是具体的文件或者文件名。当include可以指定所有的需要处理的文件时,不需要exclude的存在!!!
include: [        
    path.resolve(__dirname, './node_modules/normalize.css'),        
    path.resolve(__dirname, './node_modules/antd-mobile'),        
    path.resolve(__dirname, './node_modules/react-wx-images-viewer')      
],
                  
//也可以是正则表达式的形式
//指定需要处理的文件是include对应的文件或者文件夹中符合test指定的类型的文件
include: //node_module/^antd.*/
  1. exclude:优先级高于test和include,当include和exclude同时存在时,以exclude的为主。两者同时存在且有效的情况是,exclude是include的子集,比如指定除normalize.css之外的所有/node_moduels/
    exclude: [      
       path.resolve(__dirname, './node_modules/normalize.css'),    
   ],
   include: /node_modules/

如果反过来,include无效

  include: [        
      path.resolve(__dirname, './node_modules/normalize.css'),      
   ],
  exclude: /node_modules/

cache

这个的话其实在打包工具之Webpack篇(四) | 青训营笔记讲过就不再重复啦,感兴趣的话可以看看。

Thead

为什么

当项目越来越庞大时,打包速度越来越慢,甚至于需要一个下午才能打包出来代码。这个速度是比较慢的。我们想要继续提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少。 而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具,所以我们要提升它们的运行速度。我们可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了。

是什么

Thead指的是多进程打包,开启电脑的多个进程同时干一件事,速度更快。不过需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。

怎么用

我们启动进程的数量就是我们 CPU 的核数。所以我们首先可以先获取 CPU 的核数。

1、如何获取 CPU 的核数,因为每个电脑都不一样。

// nodejs核心模块,直接使用
const os = require("os");
// cpu核数
const threads = os.cpus().length;

2、 下载包

npm i thread-loader -D

3、 使用

const os = require("os");
//压缩用的包,webpack会自己调用,只在开发模式才用,生产模式不用
const TerserPlugin = require("terser-webpack-plugin");

// cpu核数:因为是利用多进程,且每台电脑的cpu核数可能不一样
const threads = os.cpus().length;

...
{
    test: /.js$/,
    // exclude: /node_modules/, // 排除node_modules代码不编译
    include: path.resolve(__dirname, "../src"), // 也可以用包含
    use: [
            {
                loader: "thread-loader", // 开启多进程
                options: {
                    workers: threads, // 数量
                    },
            },
            {
                 loader: "babel-loader",
                 options: {
                     cacheDirectory: true, // 开启babel编译缓存
                 },
            },
         ],
 },
 ...
 
  plugins: [
      new ESLintWebpackPlugin({
          // 指定检查文件的根目录
          context: path.resolve(__dirname, "../src"),
          exclude: "node_modules", // 默认值
          cache: true, // 开启缓存
          // 缓存目录
          cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"),
          threads, // 开启多进程!!!!!
      }),
      // css压缩(放在下面了)
      // new CssMinimizerPlugin(),
]

...
//这个部分只有生产模式用(压缩时候),开发不用
 optimization: {
     minimize: true,
     minimizer: [
     // css压缩也可以写到optimization.minimizer里面,效果一样的
     new CssMinimizerPlugin(),
     // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
     new TerserPlugin({
             parallel: threads // 开启多进程
         })
     ],
 },

扩展

1、 在文件很大的时候用好一点,文件太小反而会降低速度?

答: 因为启动进程需要时间,所以在文件需要打包的内容很少时,使用多进程打包实际上会显著的让我们打包时间变得很长。

减少代码体积

Tree Shaking

为什么

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。这样将整个库都打包进来,体积就太大了。

是什么

Tree Shaking 是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。

注意:它依赖 ES Module

怎么用

Webpack 已经默认开启了这个功能,无需其他配置。但是可以测试。

  1. 创建math.js然后再main.js调用mul

  1. 运行后去dist打包好的js文件中找,会发现只有mul的没有add

扩展

1、 tree shaking如何工作的呢?

答:在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态的,也意味着我们可以基于条件来导入需要的代码:

let dynamicModule;
// 动态导入
   if (condition) { 
       myDynamicModule = require("foo");
   } else { 
       myDynamicModule = require("bar");
  }

但是CommonJS规范无法确定在实际运行前需要或者不需要某些模块,所以CommonJS不适合tree-shaking机制。 在 ES6 中,引入了完全静态的导入语法:import。我们只能通过导入所有的包后再进行条件获取。如下:

import foo from"foo";
import bar from"bar";
if (condition) {
   // foo.xxxx
} else {
   // bar.xxx
}

ES6的import语法可以完美使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。因为tree shaking只能在静态modules下工作。ECMAScript 6 模块加载是静态的,因此整个依赖树可以被静态地推导出解析语法树。所以在 ES6 中使用 tree shaking 是非常容易的。

2、tree shaking的原理是什么?

答:ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块。静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码。

3、 common.js 和 es6 中模块引入的区别?

答:CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。

但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJs 是单个值导出,ES6 Module可以导出多个。
  4. CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层。
  5. CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined。

4、有没有某些引入模块不希望进行Tree Shaking呢?那怎么处理呢?

答:下面引入的style.css模块,如果也使用tree shaking,由于css文件没有导出任何模块,那么就有可能在打包的时候该引入模块就被摇晃掉了,导致bug。

在package.json中进行配置,即匹配到的任何css文件都不进行Tree Shaking

 "sideEffects": ["*.css"],  //该模块不进行Tree Shaking

Babel

为什么

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。

你可以将这些辅助代码作为一个独立模块,来避免重复引入。

是什么

@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。

怎么用

1、下载包

npm i @babel/plugin-transform-runtime -D

2、配置

扩展

zhuanlan.zhihu.com/p/394783228

Image Minimizer

为什么

开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。我们可以对图片进行压缩,减少图片体积。

注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。

是什么

image-minimizer-webpack-plugin: 用来压缩图片的插件

怎么用

1、 下载包

npm i image-minimizer-webpack-plugin imagemin -D

还有剩下包需要下载,有两种模式:

  • 无损压缩 不过这一块我一直没弄成功,下面是我的失败案例。
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
//下载一直报错

//应该在命令后加参数 --ignore-scripts
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D --ignore-scripts

//实操后这里没报错,但是另一个地方不行
  • 有损压缩
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

有损/无损压缩的区别

2、 配置

以无损压缩配置为例:

  1. 打包时会出现报错:
Error:
Error with 'src\images\1.jpeg': '"C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe"'
Error with 'src\images\3.gif': spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT

我们需要安装两个文件到 node_modules 中才能解决。

  • jpegtran.exe

jpegtran 官网地址open in new window

  • optipng.exe

OptiPNG 官网地址

优化代码运行性能

Code Split

开发与生产模式都可以

为什么

打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。

是什么

代码分割(Code Split)主要做了两件事:

  1. 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
  2. 按需加载:需要哪个文件就加载哪个文件。

怎么用

代码分割实现方式有不同的方式,为了更加方便体现它们之间的差异,我们会分别创建新的文件来演示

1. 多入口

1、文件目录

image.png 2、下载包

npm i webpack webpack-cli html-webpack-plugin -D

3、新建文件

内容无关紧要,主要为了观察打包输出的结果

  • app.js

image.png

  • main.js

image.png

按需加载,动态导入

单入口

适用只有一个main.js

动态引入需要在.eslintrc.js中加一个插件,不然会报错

Preload / Prefetch

开发与生产模式都可以

为什么

我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 PreloadPrefetch 技术。

是什么

  • Preload:告诉浏览器立即加载资源。

  • Prefetch:告诉浏览器在空闲时才开始加载资源。

它们的共同点是都只会加载资源,并不执行且都有缓存。区别是Preload加载优先级高,Prefetch加载优先级低。 Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。所以可以采取:当前页面优先级高的资源用 Preload 加载。下一个页面需要使用的资源用 Prefetch 加载。不过他们的兼容性较差,Preload 相对于 Prefetch 兼容性好一点。感兴趣的可以去 Can I Use 网站查询 API 的兼容性问题。

怎么用

1、 下载包

npm i @vue/preload-webpack-plugin -D

2、 配置 webpack.prod.js

Network Cache

为什么

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。

但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。

是什么

它们都会生成一个唯一的 hash 值。

  • fullhash(webpack4 是 hash)

每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。

  • contenthash

根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。

怎么用

不过有个问题就是当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?原因是更新前math.xxx.js, main.js 引用的 math.xxx.js,更新后math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化 那怎么解决呢?我们可以将 hash 值单独保管在一个 runtime 文件中。我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。

Core-js

为什么

过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决

是什么

core-js 是专门用来做 ES6 以及以上 API 的 polyfill

polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。

怎么用

1、 修改 main.js(写一个有兼容性问题的代码)

2、修改配置文件

  • 下载包
npm i @babel/eslint-parser -D
  • 手动全部引入

这样引入会将所有的兼容性代码全部引入,体积太大

  • 手动按需引入

只引入打包 promise 的 polyfill,打包体积更小。但是将来如果还想使用其他语法,我们需要手动引入库很麻烦。

  • 自动按需引入

    • babel.config.js

此时就会自动根据我们代码中使用的语法,来按需加载相应的 polyfill 了 不过,不知道为什么,我一加proposals:true就不太行,去掉就正常

PWA

为什么

开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。而我们希望给项目提供离线体验。

是什么

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。且它内部通过 Service Workers 技术实现的。

怎么用

1、 下载包

npm i workbox-webpack-plugin -D

2、 修改配置文件

3、 运行指令

npm run build

会得到:

注意:此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed

因为我们打开的访问路径是:

http://127.0.0.1:5500/dist/index.html

此时页面会去请求 service-worker.js 文件,请求路径是:

http://127.0.0.1:5500/service-worker.js

这样找不到会 404。 而实际 service-worker.js 文件路径是:

http://127.0.0.1:5500/dist/service-worker.js

错误如图

4、 解决路径问题

  • 下载包
npm i serve -g

serve 也是用来启动开发服务器来部署代码查看效果的。

  • 运行指令
serve dist

控制台会出现下面这个,从下面这个进去就行,这下把Network调成断网状态也能正常显示,因为都缓存下来了

  1. 报错不见了

  1. 断网也没事,可以冲这两个查到缓存信息

至此,关于webpack优化问题就到此结束了,下一篇就介绍loader以及plugin的内容了。好了,本次就先到这,最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!