摇树(tree shaking)

我次要是想说摇树失败的起因(tree shaking 失败的起因),先讲下摇树自身成果

什么是摇树?

举个例子

首先 webpack.config.js配置

const webpack = require("webpack");/** * @type {webpack.Configuration} */module.exports = {  mode: "production"};

在固定 a.js 用esm导出,b.js用commonjs导出不变动

// a.jsexport function f1() {  console.log("11111");}export function f2() {  console.log("22222");}// b.jsexports.f3 = function () {  console.log("33333");};exports.f4 = function () {  console.log("44444");};

例子1:import a.js 和 require b.js

// index.jsimport { f1 } from "./a";import { f3 } from "./b";console.log(f1);console.log(f3);

打包后果:a.j 和 b.js 都摇树了,只输入了 f1 和 f3。所以导入用import,导出esm和commonjs都能够

例子2:import a.js 和 import b.js

// index.jsimport { f1 } from "./a";const { f3 } = require("./b");console.log(f1);console.log(f3);

打包后果:a.js 摇,b.js 没摇,输入了 f1 、f3、f4。所以导入用require不胜利

论断:

摇树只能import,导出用esm和commonjs都能够

因为摇树产生在编译阶段,只反对esm的import,不反对commonjs的require,因为esm是编译时,commonjs是运行时

摇树失败的起因

三方面可能导致失败:

1、代码没用import引入

2、webpack配置没开启摇树

3、副作用(sideEffects)

4、babel配置preset-env没写 module:false 参数

代码没用import引入

这一点下面曾经阐明,必须用 import 导入,导出用 esm 或者 commonjs 都行

webpack配置没开启摇树

开启摇树两步:

1、usedExports设置true,标记无用代码,esm导出的没应用到的导出函数标记为unused harmony export f2,commonjs导出的没应用的导出函数赋值为__webpack_unused_export__

2、terser-webpack-plugin插件做代码压缩去除无用代码,依据一步两种标记,压缩代码会去除

const webpack = require("webpack");/** * @type {webpack.Configuration} */module.exports = {  mode: "none",  optimization:{    usedExports:true  }};
  • mode: production模式下,默认开启摇树,不必做任何配置,由源码看出nonedevelopment不会开启摇树,须要手动加这两步,留神要设置minimize:true,或者放到plugins中

看webpack源码默认配置,参考webpack视频解说:进入学习

副作用(sideEffects)

先来解释下什么是副作用:批改以后作用域之外的行为都叫副作用,比方在函数外部,批改dom,批改全局对象等等

这条次要是针对引入三方包,三方包package.json的sideEffects字段默认true示意有副作用,能够设置为false示意没有副作用,设置为数组列出有副作用的文件

webpack.config.js设置sideEffects:true示意查看三方包的sideEffects字段,webpack在用userExports标记无用代码时,如果判断不出库中代码是否有副作用,就不会标记,则压缩的时候也没法革除,如果判断有副作用,则更不会标记革除

  • mode: production模式下,默认开启摇树,不必做任何配置,usedExports: true
const webpack = require("webpack");const TerserPlugin = require("terser-webpack-plugin");/** * @type {webpack.Configuration} */module.exports = {  mode: "none",  optimization:{    sideEffects:true,    usedExports:true  },  plugins:[    new TerserPlugin()  ]};

babel配置preset-env没写 module:false 参数

在文章 我把握的Babel配置 中具体解说了 module: false 参数,简略说不设置false时,只针对babel相干的runtime包的引入会应用require,设置了false引入会应用import,就能让webpack去摇树,回到第一点上

module.exports = {  presets: [    [      "@babel/preset-env",      {        modules: false      },    ],  ]};

拆包(splitChunks)

splitChunks是webpack配置下optimization下的配置,即优化。看单词了解意思就是拆分多个chunk。

什么是chunk

webpack的实质是把多个js模块合并到一个js中,即一个入口失去一个输入js文件(bundle.js)。

然而导致的问题是,如果这个bundle.js文件很大,那么浏览器申请的时候,导致申请工夫很长,首屏长时间白屏。

所以优化伎俩就是把bundle.js文件拆分成多个小的js文件,同时申请,首屏当然就更快渲染显示。

所以入口文件,chunk文件,输入文件三者的关系从原来的一个入口文件对应一个chunk最初输入一个bundle文件扭转为一个入口文件对应多个chunk最初输入多个bundle文件

三种形式取得chunk

  • 1、入口文件能够生成chunk,入口文件即webpack配置的entry选项;
  • 2、异步申请 import函数调用 或者 require.ensure 能够生成chunk;
    如:import函数即咱们在写vue-router时写的异步申请路由形式,这里webpackChunkName能够魔法定义chunk名,也可不写
import(/* webpackChunkName: "AboutPage" */'./view/about.vue')
  • 3、webpack配置splitChunks手动拆分生成chunk,最初独立输入到js文件

splitChunks 配置

简略配置,把react相干包都独自提到一个文件

{   optimization: {    splitChunks: {      chunks: "all", // initial、async和all      cacheGroups: {        react: {          name: "react",          test: /[\\/]react(\w)*[\\/]/i,          priority: 10        },        lodash: {          name: "lodash",          test: /[\\/]lodash(\w)*[\\/]/i,          priority: 20,          minChunks:3        },      },    },  },}

先来看下webpack默认的splitChunks参数

看图production非production模式下有参数不一样,上面这些参数示意主动拆包的条件:

  • chunks

重要:拆包的范畴,默认async,只针对异步申请的,即下面第二条的import函数调用的chunk外面;initial示意只针对初始化入口entry的;all示意最大蕴含async + entry

  • cacheGroups

重要:自定义拆包规定,name是chunk名,test正则包名,priority优先级(因为同一个包可能合乎多个拆包规定,会解决给优先级高的);看图可知,默认会有两个包规定,defaultVendors规定示意node_modules会拆到一个chunk包,default规定示意只有被两个即以上chunk援用就要拆到一个chunk包

  • minChunks

拆分前必须共享模块的最小 chunks 数,能够不必批改

  • maxAsyncRequests

浏览器发送异步申请时,最大不超过30个申请,即下面第二条的import函数调用,能够不必批改

  • maxInitialRequests

浏览器申请入口entry时,最大不超过30个,能够不必批改

热更新

咱们次要是阐明热更新的 module.hot.accept()

先来理解一下热更新怎么配置的?

热更新配置

装包

npm i -D webpack-dev-server html-webpack-plugin

webpack.config.js

const webpack = require("webpack");const HtmlWebpackPlugin = require('html-webpack-plugin');/** * @type {webpack.Configuration} */module.exports = {  mode: "development",  devServer: {    port: 3000,    open: true,    hot: true,  },  plugins: [    new HtmlWebpackPlugin(),  ]};

package.json

"scripts": {    "serve": "webpack serve",},

论断

到此热更新配置实现,失常写代码,然而发现问题了,此时更新页面是整个刷新页面的,并不是部分刷新,怎么回事呢,原来须要在每个文件中最初加上module.hot.accept()才会触发部分更新,accept能够承受两个参数,依赖和回调

exports.f3 = function () {  console.log("33333");};exports.f4 = function () {  console.log("44444");};if (module.hot) {  module.hot.accept();}

随即产生了另一个疑难,这太麻烦了吧,每个文件文件都须要去加module.hot.accept(),然而咱们在理论写下我的项目的时候怎么没有写这句呢?

起因是不管css、vue、react的loader都帮咱们主动加了这句。

css有style-loader,react有react-hot-loader,vue有vue-loader。

对于jsx文件,有vue-jsx-hot-loader

{    test:/\.jsx?$/,    use:['babel-loader','vue-jsx-hot-loader']}

按需加载

一段时间以来,我始终把tree shaking和按需加载一概而论,其实应该离开了解,这里我次要是想说第三方包的按需加载,比方应用element-ui、lodash、vant

tree shaking的前提是应用import导入,然而按需加载并不需要

还有一个点须要留神:如果是咱们封装的库,如组件库,导出格局依据文件类型不同,如是js文件能够为 commonjs + es5、esm + es5;如是vue或react文件,esm/commonjs + es6/es5 任意都行,因为咱们用babel-loader时会排除node_modules目录不编译,vue-loader等会去编译vue文件

应用babel插件

npm install babel-plugin-component -D

babel.config.js

module.exports = {  presets: [    [      "@babel/preset-env",      {        useBuiltIns: "usage",        corejs: 2,        modules: false,      },    ],  ],  plugins: [    ["@babel/plugin-transform-runtime"],    [      "babel-plugin-import",      {        libraryName: "vant",        libraryDirectory: "es",        style: true,      },      "vant",    ],    [      "babel-plugin-import",      {        libraryName: "antd",        style: true, // or 'css'      },    ],    [      "babel-plugin-import",      {        "libraryName": "lodash",        "libraryDirectory": "",        "camel2DashComponentName": false,  // default: true      },      "lodash",    ],    [      "babel-plugin-component",      {        libraryName: "element-ui",        styleLibraryName: "theme-chalk",      },      "element-ui",    ],  ],};

结束!