摇树(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
模式下,默认开启摇树,不必做任何配置,由源码看出none
和development
不会开启摇树,须要手动加这两步,留神要设置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", ], ],};
结束!