美文网首页
06-webpack-进阶

06-webpack-进阶

作者: 仰望_IT | 来源:发表于2020-04-15 21:54 被阅读0次

1.Tree-Shaking


Tree-Shaking的作用是过滤掉没有用到的JS代码和CSS代码。如果不过滤的话会将没有用到的代码也会打包, 这样就会增加打包的体积, 降低网页的性能。
参考文档: tree-shaking

JS模块Tree-Shaking
  • 开发环境

webpack.config.dev.js

module.exports = {
    // 告诉webpack只打包用到的JS代码
    optimization: {
        usedExports: true
    },
}

package.json

{
  // 告诉webpack哪些文件不做Tree-Shaking, 不过滤掉
  "sideEffects": ["*.css", "*.less", "*.scss"],
}
  • 生产环境
    无需进行任何配置, webpack默认已经实现了Tree-Shaking
CSS模块Tree-Shaking

不光JS模块可以进行Tree-Shaking, CSS模块也可以进行Tree-Shaking
因为项目上线的时候才需要过滤, 所以这里只介绍生产环境如何对CSS模块过滤
参考文档: PurifyCSS Plugin

  • 安装相关插件
npm i -D purifycss-webpack purify-css glob-all
  • 配置插件
    webpack.config.prod.js
const PurifyCSS = require("purifycss-webpack");
const glob = require("glob-all");
const path = require('path');

module.exports = {
  plugins: [
    new PurifyCSSPlugin({
      // 告诉PurifyCSSPlugin需要过滤哪些文件
      paths: glob.sync([
          // 要做CSS Tree Shaking的路径文件
          path.join(__dirname, 'src/*.html'),
          path.join(__dirname, 'src/js/*.js'),
      ]),
    })
  ]

2. Code-Splitting


Code-Splitting就是将不经常修改的模块打包到单独的文件中, 避免每次修改用户都需要重新下载所有内容

如何开启Code-Splitting
  • 手动分割(了解)
  1. 在单独文件中引入模块, 将模块中的内容添加到window上, 例如:
    custom.js
import $ from 'jquery';

window.$ = $;

index.js

window.$('html').css({ width: '100%', height: '100%' });
  1. 修改配置文件同时打包多个文件
    webpack.config.common.js
module.exports = {
    entry: {
        other: './src/js/custom.js',  // 先打包会被先引入
        main: './src/js/index.js'
    },
    output: {
        filename: "js/[name].js",
        path: path.resolve(__dirname, "bundle")
    }
}

手动分割操作麻烦, 可以使用webpack自动分割

  • 自动分割
    webpack会自动判断是否需要分割, 如果需要会自动帮助我们分割
    只需要在配置文件中, 新增一个配置项
    参考文档: code-splitting

webpack.config.common.js

module.exports = {
    // 告诉webpack需要对代码进行分割
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    },
}

3. SplitChunksPlugin


webpack在代码分割的时候底层使用的其实是Split-Chunks-Plugin来实现代码分割的,所以这个插件的作用就是进行代码分割
参考文档: SplitChunksPlugin

Split-Chunks-Plugin相关配置

webpack.config.js

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'async', // 对哪些代码进行分割 async(只分割异步加载模块)、all(所有导入模块), 默认为async
            minSize: 30000, // 表示被分割的代码体积至少有多大才分割(单位是字节)
            minChunks: 1, // 表示node_modules中的模块至少被引用多少次数才分割,默认为1
            maxAsyncRequests: 5, // 异步加载并发最大请求数(保持默认即可)
            maxInitialRequests: 3, // 最大的初始请求数(保持默认即可)
            automaticNameDelimiter: '~', // 指定被分割出来的文件名称的连接符
            name: true, // 拆分出来块的名字使用0/1/2...(false) 还是指定名称(true)
            // cacheGroups: 缓存组, 将当前文件中导入的所有模块缓存起来统一处理
            cacheGroups: {  
                // vendors: 专门用于处理从node_modules中导入的模块, 会将所有从node_modules中导入的模块写入到一个文件中去
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10 // 抽取公共代码的优先级,数字越大,优先级越高
                },
                // default: 专门用于处理从任意位置导入的模块, 会将所有从任意位置导入的模块写入到一个文件中去
                default: {
                    minChunks: 2, // 表示node_modules之外中的模块至少被引用多少次数才分割,默认为1
                    priority: -20,
                    reuseExistingChunk: true // 是否复用分割的代码, 如果当前代码块包含的模块已经有了,就不在产生一个新的代码块
                }
            }
        }
    },
}

【注意】

  1. 如果我们导入的模块同时满足了vendors和default两个条件, 那么就会按照优先级来写入
    例如: 我们导入了jQuery, jQuery存放在了node_modules目录中
    所以满足vendors的条件, 也满足default条件, 但是vendors的条件的优先级高于default的优先级,就只会执行vendors规则, 只会写入到vendors对应的文件中去
  2. 默认情况下如果所有的模块都是从node_modules中导入的, 那么会将所有从node_modules中导入的模块打包到同一个文件中去。
  3. 默认情况下如果所有的模块都不是从node_modules中导入的, 那么会将所有不是从node_modules中导入的模块打包到同一个文件中去
  4. 如果当前文件中导入的模块有的是从node_modules中导入的, 有的不是从node_modules中导入的,那么就会将所有从node_modules中导入的打包到一个文件中,就会将所有不是从node_modules中导入的,中导入的打包到另一个文件中。

4. 异步加载模块(懒加载)


官网解释:懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
参考文档: 懒加载

示例

我们增加一个交互, 当用户点击按钮的时候在body中新增一个div。但是会等到第一次交互的时候再加载那个代码块
index.js

const oBtn = document.querySelector('button');
oBtn.onclick = function() {
    getComponent().then(($div) => {
        document.body.appendChild($div[0]);
    });
};
// 第一种写法
function getComponent() {
    // import()动态加载返回的是一个promise
    return import('jquery').then(({ default: $ }) => {
        const $div = $('<div>我是div</div>');
        return $div;
    });
}
// 第二种写法
async function getComponent() {
    const { default: $ } = await import('jquery');
    const $div = $('<div>我是div</div>');
    return $div;
}

在上述代码中,直接使用import()去动态加载资源,是es6草案中语法,并不是正式语法,所以直接使用会报错,需要配置相关的语法动态导入的插件@babel/plugin-syntax-dynamic-import,并在webpack中做简单配置:
webpack.config.js

module.exports = {
 ...
 module:{
  rules:[
   {
    test:/\.js$/,
    use:{
     loader:'babel-loader',
     options:{
      presets:[
       '@babel/preset-env',
       '@babel/preset-react'
      ],
      plugins:[
       '@babel/plugin-syntax-dynamic-import'
      ]
     }
    }
   }
  ]
 },
 ...
}

5. Prefetching


通过异步加载(懒加载)的方式确实可以优化我们的代码,但是也存在一定的弊端, 弊端就是用到的时候再加载, 那么用户需要等待加载完成后才能使用。
例如: 弹出登录框的时候有一些业务逻辑, 如果这些业务逻辑使用懒加载的话, 那么只有加载完用户才能操作登录框

解决方案:

加载完当前需要使用的所有模块之后, 在空闲的时间提前把异步加载的模块也加载进来
这样既不会影响到第一次的访问速度, 还可以提升异步加载的速度较少用户等待的时间
所以就有了Prefetching

Prefetching: 空闲的时候加载
也就是等当前被使用的模块都加载完空闲下来的时候就去加载, 不用等到用户用到时再加载

使用方式:

异步加载时写上魔法注释即可

/* webpackPrefetch: true */

例如:

function getComponent() {
    return import(/* webpackPrefetch: true */'jquery').then(({ default: $ }) => {
        const $div = $('<div>我是div</div>');
        return $div;
    });
}

还可以利用魔法注释修改分割代码的名称
异步加载时在加载模块前面写上魔法注释, 例如:

import(/* webpackChunkName: "jquery" */"jquery").then();

6. 长缓存优化


浏览器缓存问题

浏览器会自动缓存网页上的资源, 以便于提升下次访问的速度,但正式因为浏览器的缓存机制, 导致文件内容被修改之后只要文件名称没有发生变化,就不会重新去加载修改之后的资源, 所以刷新网页后显示的还是修改之前的内容。
为了解决这个问题, 我们就需要在打包文件的时候给"文件名称加上内容的hash值",一旦内容发生了变化, 内容的hash值就会发生变化, 文件的名称也会发生变化; 一旦文件的名称发生了变化, 浏览器就会自动去加载新打包的文件

hash / chunkhash / contenthash

webpack提供了三种不同的hash

  • hash:
    根据每次编译打包的内容生成的哈希值, 每次打包都不一样, 不能很好利用缓存, 不推荐
  • chunkhash:
    根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
    在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。
    【注意】: 只支持css和js, 不支持img等其它资源
  • contenthash(推荐):
    根据某个文件内容生成的哈希值, 只要某个文件内容发生改变,该文件的contenthash就会发生变化
示例

webpack.config.js

{
  loader: 'file-loader',
  options: {
    name: '[name].[contenthash:8].[ext]'
  }
}
manifest

webpack在打包时,会把库和业务代码之间的关系做manifest,它既存在于业务代码(main.js),也存在于库中(vendor.js),在旧版webpack中(webpack4之前),mainfest在每次打包的时候的时候可能会变化,所以contenthash值也会跟着变化。配置runtimeChunk后,会把manifest提取到runtime中,这样打包就不会影响到其他js了。
参考文档: manifest
webpack.config.js

module.exports = {
    optimization: {
        runtimeChunk: "single",
    }
}

7. ProvidePlugin


ProvidePlugin可以自动加载模块,而不必到处 import 或 require 。
这个插件是webpack内置的模块, 不需要安装, 只需要在用的时候导入webpack模块并进行配置即可
参考文档: ProvidePlugin

示例: 使用:jQuery

webpack.config.js

const Webpack = require('webpack');

module.exports = {
    plugins: [
        new Webpack.ProvidePlugin({
            // 将$变量指向jQuery模块, 在全局使用$代表jQuery
            $: 'jquery'
        })
    ]
}

8.imports-loader


imports-loader和Provide-Plugin功能一样可以实现全局导入, 但是imports-loader的功能比Provide-Plugin更强大。imports-loader除了可以实现全局导入以外, 还可以修改全局this指向。
默认情况下模块中的this指向一个空对象, 我们可以通过imports-loader实现让this指向window
参考文档: imports-loader

如何使用imports-loader
  • 安装imports-loader
npm install imports-loader
  • 配置imports-loader
    webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                // 将$变量指向jQuery模块, 在全局使用$代表jQuery
                loader: "imports-loader?$=jquery"
            }
        ]
    }
};
  • 修改全局this
    默认全局this是一个空对象, 如何将this改为window
    webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                use: "imports-loader?this=>window"
            }
        ]
    }
};
  • 使用imports-loader遇到的问题

在修改了全局的this为window后, 又在js文件中使用了import导入模块, 会出现'import' and 'export' may only appear at the top level错误提示
index.js

import $ from 'jquery';
$('div').css({ width: '300px', height: '300px', background: 'blue' });

console.log(this);

原因:
如果通过imports-loader修改了模块中this的指向,那么imports-loader会自动将模块中的所有代码放到一个自调用函数中。

(function () {
    // my module
}.call(window));

如果在模块中用到了import, 那么在ES Module的规范中,import语句必须写在最前面, 否则就会报错。
所以如果通过imports-loader修改了模块中this的指向,而在模块中又用到了import, 那么import就不在第一行了,所以就报错了。
解决方案:
不要去修改this指向, 直接在模块中使用window。
如果你非要修改this指向,那么在导入模块的时候必须将import语法改为require

所以: 在开发中如果需要实现全局导入, 更推荐使用ProvidePlugin来实现,因为ProvidePlugin是webpack内置的官方插件更靠谱。也不要去修改this指向,直接用window即可。

9. resolve


resolve用于配置导入模块的解析规则
参考文档: resolve

  • 映射导入路径, 简化导入代码
resolve: {
    // 创建 import 或 require 的别名,来确保模块引入变得更简单
    alias: {
        bootstrap: path.resolve(__dirname, "bootstrap/dist/css/bootstrap.css")
    },
}
  • 修改入口查找顺序, 简化导入代码
resolve: {
    // 指定模块入口的查找顺序
    mainFields: ["style", "main"],
}
  • 修改查找顺序, 简化导入代码
    在导入语句没带文件后缀时,webpack会自动带上后缀去尝试访问文件是否存在。resolve.extensions用于配置在尝试过程中用到的后缀列表
resolve: {
    // 指定导入模块查找顺序
    extensions: [".css", ".js"]
}
  • 指定查找范围
    通过import导入模块的时候会先在node_modules中查找, 找不到再逐级向上查找,这样在打包的时候非常消耗性能,。能不能在打包的时候让webpack只去指定的目录查找,那就是通过resolve的modules。
resolve: {
    // 指定查找范围, 告诉webpack只在node_modules中查找
    modules: ["node_modules"],
}

10. noParse


默认情况下无论我们导入的模块(库)是否依赖于其它模块(库), 都会去分析它的依赖关系。但是对于一些独立的模块(库)而言, 其根本不存在依赖关系, 但是webpack还是会去分析它的依赖关系这样就大大降低了我们打包的速度。所以对于一些独立的模块(库), 我们可以提前告诉webpack不要去分析它的依赖关系,这样就可以提升我们的打包速度。
参考文档: module

  • 如何告诉webpack这是一个独立的模块(库)
    webpack.config.js
module.exports = {
    module: {
        noParse: /jquery/,
    }
}

11. IgnorePlugin


IgnorePlugin是webpack的一个内置插件,用于忽略第三方包指定目录,让指定目录不被打包进去。
参考文档: ignore-plugin

示例

webpack.config.js

module.exports = {
    plugins: [
        // 在打包moment这个库的时候, 将整个locale目录都忽略掉
        new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ]
}

12. externals


externals的作用就是将不会发生变化的第三方模块(库)设置为外部扩展,避免将这些内容打包到我们的项目中, 从而提升打包速度。
参考文档: externals

externals使用
  • 手动全局引入第三方模块
    index.html
<head>
    ...
    <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
</head>
  • 在配置文件中告诉webpack这是一个外部扩展库, 不需要打包
    webpack.config.js
module.exports = {
    // 告诉webpack哪些第三方模块不需要打包
    externals: {
        // 告诉webpack我们在通过import导入jquery的时候, 不是导入node_modules中的jquery,而是导入我们全局引入的jquery
        jquery: '$',
        lodash: '_'
    }
}

13. dll动态链接库


dll动态链接库和externals功能其实是一样的, 都是用于防止重复打包不会发生变化的第三方模块, 都是用于提升webpack打包效率的。只不过externals不太符合前端的模块化思想, 所以就有了dll动态链接库。
参考文档: add-asset-html-webpack-plugindllreferenceplugin

如何实现让第三方模块只打包一次

1. 单独配置一个config.js文件打包不会发生变化的第三方库
webpack.config.dll.js

const path = require('path');

module.exports = {
    mode: 'production',
    entry: {
        vendors: 'jquery'
    },
    output: {
        filename: '[name].[contenthash:8].js',
        path: path.resolve(__dirname, 'dll'),
        library: '[name]' // 表示打包的是一个库, 表示将打包的内容通过全局变量暴露出去
    }
};

2. 通过插件将打包好的库引入到界面上

npm install --save-d add-asset-html-webpack-plugin

webpack.config.js

module.exports = {
    ...
    plugins: [
        new AddAssetHtmlPlugin({
            filepath: path.resolve(__dirname, 'dll/vendors.dll.js') 
        }),
    ]
}

【注意】该插件需要配合HtmlWebpackPlugin使用, 并且需要在HtmlWebpackPlugin后创建

3. 生成动态库的映射关系
因为我们有可能将几个库打包到一个文件中, 所以需要生成一个映射文件方便webpack能够从中找到对应的库
webpack.config.dll.js

module.exports = {
    ...
    plugins: [
        // DllPlugin作用:在打包第三方库的时候生成一个清单文件
        new Webpack.DllPlugin({
            // 这里的name必须和library一致
            name: '[name]',
            path: path.resolve(__dirname, 'dll/[name].manifest.json')
        })
    ]
}

4. 告诉webpack去哪里查找动态库
在打包的时候如何webpack回到指定的映射文件中查找对应的动态库, 找打了那么就不会重新打包动态库中的内容了, 如果找不到才会重新打包
webpack.config.js

module.exports = {
    ...
    plugins: [
        new Webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dll/vendors.manifest.json')
        }),
    ]
}

5. 动态链接库的优势
不用手动将第三方库插入到HTML中
所有第三方库只会被打包一次

6. 当前动态链接库存在的问题
如果我们提前打包生成了多个文件和清单, 那么需要手动增加插入的文件和查询的清单
如何解决
通过NodeJS代码动态添加
webpack.config.js

const plugins = [
    // 将插件代码提取出来
]

// 动态添加AddAssetHtmlPlugin和DllReferencePlugin插件
const dllPath = path.resolve(__dirname, 'dll');
const files = fs.readdirSync(dllPath);
files.forEach(function (file) {
    if(file.endsWith(".js")){
        plugins.push(new AddAssetHtmlPlugin({
            filepath: path.resolve(__dirname, 'dll', file)
        }));
    }else if(file.endsWith(".json")){
        plugins.push(new Webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dll', file)
        }));
    }
});

module.exports = {
    ...
    plugins: plugins
}

我们发现完整地配置dll动态链接库是非常麻烦的, 那么有没有什么办法简化代码呢?答案是有的, 请移步到https://juejin.im/post/5d8aac8fe51d4578477a6699

14. HappyPack


默认情况下webpack打包项目是单线程的, 为了提升打包速度, 充分发挥多核 CPU 电脑的威力,我们可以通过HappyPack让webpack实现多线程打包
参考文档: happypack

HappyPack使用
  • 安装HappyPack
npm install --save-dev happypack
  • 配置HappyPack
    webpack.config.js
const HappyPack = require('happypack');

module.exports = {
    ...
    module: {
        rules: [{
            test: /.js$/,
            use: 'happypack/loader',
            include: [ /* ... */ ],
            exclude: [ /* ... */ ]
        }]
    },
    plugins: [
        new HappyPack({
            id: 'js',
            use: [/*处理文件规则*/]
    ]
}

15. 多页面打包


我们开发不可能只写一个页面,每次都要写很多页面,这时为了开发效率,我们使用前端自动化工具webpack,那么是如何打包多页面的呢?
参考文档: html-webpack-plugin

需求:利用webpack打包生成两个页面

一个页面叫做index, 一个页面叫做detail

1. 有多少个界面就指定多少个入口, 并给不同的入口指定不同的名称

module.exports = {
    ...
    entry: {
        index: './src/js/index.js',
        detail: './src/js/detail.js'
    },
}

2. 有多少个界面就创建多少个HtmlWebpackPlugin, 并给不同的界面配置不同的名称

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html',
        }),
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'detail.html',
        }),
    ]
}

3. 在HtmlWebpackPlugin中通过chunks属性告知需要插入到当前界面的文件

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html',
            chunks: ['index', 'vendors~index']
        }),
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'detail.html',
            chunks: ['detail', 'vendors~detail']
        }),
    ]
}
多页面打包优化

当前打包多页面应用存在的问题
有多少个界面就要编写多少个入口,有多少个界面就要创建多少个HtmlWebpackPlugin,并且每个HtmlWebpackPlugin中的配置都不一样。

如何解决以上问题
入口还是必须手动指定, 但是创建多少个HtmlWebpackPlugin和如何配置HtmlWebpackPlugin可以通过代码动态生成。也就是原理和动态创建AddAssetHtmlPlugin/DllReferencePlugin(dll动态链接库)一样。
webpack.config.js

// 将原来的配置抽取出来
const config = {
    ...
    entry: {
        index: './src/js/index.js',
        detail: './src/js/detail.js',
    },
}

config.plugins = makePlugins(config);
function makePlugins(config) {
    const plugins = [/*原来的插件*/];
    // 拿到入口文件名
    Object.keys(config.entry).forEach(function (key) {
        // 动态添加HtmlWebpackPlugin插件
        plugins.push(new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: key + '.html',
            chunks: [key, 'vendors~'+ key]
        }),);
    });
    return plugins;
}

module.exports = config;

16. webpack-bundle-analyzer


webpack-bundle-analyzer是一个可视化的打包优化插件,会将打包的结果以图形化界面的方式展示给我们。从webpack-bundle-analyzer生成的图形化界面中我们可以很清楚的知道模块之间的依赖关系、模块大小、模块有没有重复打包,、重复引用等。
从而针对性的对我们的代码进行优化
参考文档: webpack-bundle-analyzer

如何使用webpack-bundle-analyzer

1. 安装webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer

2. 配置webpack-bundle-analyzer*
webpack.config.js

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

最后执行打包命令,在打包结束以后,默认会直接在浏览器里把最终的动态treemap图片展示出来


在这个树形图片里,会有包含下面的内容:

  • 每个打包以后的bundle文件里面,真正包含哪些内容,项目里的module、js、component、html、css、img最后都被放到哪个对应的bunlde文件里了。

  • 每个bundle文件里,列出了每一个的module、componet、js具体size,同时会列出start size、parsed

start size:原始没有经过minify处理的文件大小
parse size:比如webpack plugin里用了uglify,就是minified以后的文件大小
gzip size:被压缩以后的文件大小

基于以上给出的信息, 你就能比较直观的在图片里看到,哪些公用library被重复打包到不同的bundle文件里,或者是说哪一个过大影响性等等;从而你就可以对你的webpack打包方式进行优化。

相关文章

网友评论

      本文标题:06-webpack-进阶

      本文链接:https://www.haomeiwen.com/subject/ypsvmhtx.html