webpack的运用

254 阅读10分钟

补充知识点 Development 与 Production 模式的区别

Development (开发阶段)
  • 使用dev-server会开启一个本地服务器,可以本地进行服务器的运行状态的模拟,热更新进行实时模拟
  • source-map 会包含大量错误提醒信息,体积巨大
  • 无需压缩,方便直接观看代码
Producetion (生产环境)
  • source-map 非常简洁
  • 压缩代码

Development 与 Production 的分文件配置

当我们构建项目的时候,两个模式如果分开文件配置就会导致配置代码重复问题,我们只需要把不同的整理出来,最后打包的时候在合并一下就好了

npm install webpack-merge --save-dev 这个插件是用来合并配置的

npm install webpack-dev-server --save-dev

webpack.common.js 公共配置

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './bundle')
  },
  plugins: [
      new htmlWebpackPlugin(),
      new CleanWebpackPlugin()
  ]
}

webpack.dev.js 开发配置

const webpack = require('webpack');
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');

let devConfig = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './build',
    open: true,
    port: 8080,
    hot: true
  },
  plugins: [
      new webpack.HotModuleReplacementPlugin()
  ]
}

module.exports = merge(devConfig, commonConfig)

webpack.prod.js 生产环境配置

const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');

let prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  optimization: {
    useExports: true // 开启 Tree Shaking
  }
}

module.exports = merge(prodConfig, commonConfig)

code Splitting

在之前项目中,无论引用了多少插件,最后输出的还是单个js文件,这样就会造成

  • 单文件过大
  • 业务代码和环境代码压缩到一起
  • 单次修改业务代码,就得重新打包所有文件
  • 每次修改业务代码,最终的打包出来的js就是一个新的文件(即便只是改了一个字母),就会导致用户浏览器需要重新请求该文件

ps: 当第一次请求完之后,浏览器会将请求的文件缓存中浏览器环境中,即使你再次刷新网站,其实都会在本地缓存中读取文件,不会像服务器发起请求,只有当文件的hash值不一样的时候才会重新请求

 // 例如我引入一个组件 npm install lodash -D
import _ from 'lodash' // 假设这个组件有1M
console.log(_.json(['a', 'b', 'c'], '***')) // 假设业务代码也是1M

我们配置webpack optimization 选项

  optimization: {
    splitChunks: 'all'
  }

打包的结果就是lodash被打包进了vendors-index.js里面去了

当然我们也可以手动进行代码分割 就是利用配置多个出口,单独对第三方插件就行打包,但如果插件很多的话就十分不方便

improt _ from 'lodash';
window._ = _;
entry: {
	lodash: './src/js/lodash.js'
}

Code Splitting 第三种方式

就是异步加载模式

function getComponent() {
    return import('lodash').then(({ default: _ }) => {
        let ele = document.createElement('div');
        ele.innerText = _.json(['a', 'b', 'c'], '***');
        return ele;
    })
}

getComponent().then((ele) => {
    document.body.appendChild(ele)
})

最后打包生成 异步加载的文件存放在了0.js文件中

Code Spiltting 注意点

代码分割这个概念与webpack无关,webpack实现代码分割的两个方法

  • 同步代码:只需要在webpack.common.js 中配置optimization的配置即可
  • 异步代码(import函数):无需任何配置,webpack会自动进行配置,会自动放入新的文件夹中间
Code Splitting的底层插件SplitChunksPlugin

前面有讲到我们使用webpack分割异步加载的组件,但是输出文件啥的都是默认配置,现在我们要进行一些自定义配置

npm install babel-loader @babel/core --save-dev

npm install @babel/preset-env --save-dev

npm install @babel/polyfill --save-dev

npm install @babel/plugin-syntax-dynamic-import --save-dev

修改.babelrc文件

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage"
    }]
  ],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

index.js 代码

function getComponent() { /* 使用魔法注释 */
    return import(/* webpackChunkName: "loadsh" */ 'lodash').then(({ default: _ }) => {
        let ele = document.createElement('div');
        ele.innerText = _.json(['a', 'b', 'c'], '***');
        return ele;
    })
}

getComponent().then((ele) => {
    document.body.appendChild(ele)
})

当前用魔法注释自定义的文件名称前面还是有一个前缀的, 这个前缀是因为webpack.common.js里面的optimization属性影响的

修改webpack optimization的配置文件

optimzation: {
	splitChunks: {  // webpack 自动帮我们完成代码分割
        chunks: "all",
        cacheGroups: {
        	vendors: {
            	test: /[\\/]node_modules[\\/]/,  // 匹配node_modules目录下的文件
        		priority: -10   // 优先级配置项
      		},
          	default: {
            	minChunks: 2,
            	priority: -20,   // 优先级配置项
            	reuseExistingChunk: true
      		}
        }    
    }
}

在默认配置中

  • 会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,
  • 所有引用超过两次的模块分配到 default bundle 中 更可以通过 priority 来设置优先级。
Code Splitting -SplitChunksPlugin 参数详解

chunk:每一个打包出来的文件都是一个chunk, 这个文件数和minChunks的参数息息相关 意思就是,打包出来的chunk有几个用到了某个组件,用到了,才会使用代码分割

  • chunks:"all" 针对不同的打包方式实现代码分割 可选项:all, async, initial (同步代码),异步一直分割就好,但如果是同步的话会继续读取CacheGroups的配置

  • 示例配置如下

    optimzation: {
        splitChunks: {
            chunks: 'all', 
            minSize: 30000, 
            minChunks: 1,    
            maxSayncRequest: 5
            maxInitialRequest: 3,
            automaticNameDelimiter: '~', // 前缀和名字之间的连接符号
            name: true,
            cacheGroups: {
                vendors: { // 这是一个打包的分组名称
                    test: /[\\/]node_modules[\\/]/,// 匹配node_modules目录下的文件,进行分割
                    priority: -10    
                },
                default: {
                	minChunks: 2,
                	priority: -20,   // 优先级配置项
                	reuseExistingChunk: true
          		}
            }    
        }
    }
    

    其他基本参数:

    • 表示从哪些chunks里面抽取代码,除了三个可选字符串值 initial、async、all 之外,还可以通过函数来过滤所需的 chunks;

    • minSize:小于这个尺寸的文件, 就不再做文件分割了, 就直接合并的

    • maxSize: 可配可不配,如果配置了, 比如值为50000, 那么单个被独立出来的引用包如果大于50000就会再

      次被分割(但是如果这个库是无法拆分的,那么这个maxSize就是没啥用的了)

    • minChunks:当一个模块被应用了多少次才会被分割, 一般就是1

    • maxAsyncRequests:最大引用的模块数,webpack在该值设定的上限前会正确打包,后面的就不会再分割了

    • maxInitialRequests:最大入口文件引用的模块数

    • automaticNameDelimiter:前缀和名字之间的连接符

    • name:一般就为true,专门用来标明下面的cacheGroups里面的基本配置是否生效

值得注意的是,如果没有修改minSize属性的话,而且被公用的代码size小于30KB的话,它就不会分割成一个单独的文件。在真实情形下,这是合理的,因为(如分割)并不能带来性能确实的提升,反而使得浏览器多了一次对公共代码的请求,而这个公用的代码又是如此之小(不划算)。

  • maxAsyncRequests:最大的按需(异步)加载次数,默认为 5;

  • maxInitialRequests:最大的初始化加载次数,默认为 3;

  • automaticNameDelimiter:抽取出来的文件的自动生成名字的分割符,默认为 ~;

  • name:抽取出来文件的名字,默认为 true,表示自动生成文件名;

  • cacheGroups: 缓存组。(这才是配置的关键)

    缓存组会继承splitChunks的配置,但是test、priorty和reuseExistingChunk只能用于配置缓存组。cacheGroups是一个对象,按上述介绍的键值对方式来配置即可,值代表对应的选项。除此之外,所有上面列出的选择都是可以用在缓存组里的:chunks, minSize, minChunks, maxAsyncRequests,maxInitialRequests, name。可以通过optimization.splitChunks.cacheGroups.default: false禁用default缓存组。默认缓存组的优先级(priotity)是负数,因此所有自定义缓存组都可以有比它更高优先级(译注:更高优先级的缓存组可以优先打包所选择的模块)(默认自定义缓存组优先级为0)

optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0
    }
},
splitChunks: {
      chunks: "all",
      cacheGroups: {
        commons: {
          chunks: "initial",
          minChunks: 2,
          name: "commons",
          maxInitialRequests: 5,
          minSize: 0, // 默认是30kb,minSize设置为0之后
          // 多次引用会被压缩到commons中
        },
        reactBase: {
          test: (module) => {
            return /react|redux|prop-types/.test(module.context);
          }, // 直接使用 test 来做路径匹配,抽离react相关代码
          chunks: "initial",
          name: "reactBase",
          priority: 10,
        }
    }
},

cacheGroups基本参数: vendors和default就是两个不同的打包分组,vendors可以指定匹配规则 当某个打包的时候,两个规则都符合的时候, 就按priority的值来,谁大按谁的 reuseExistingChunk:如果一个模块之前已经被打包了,那么第二次打包的时候,就跳过

懒加载

lazy loading 就是懒加载,懒加载不是webpack的概念,他是JavaScript里面就被提出来了,我们结合webpack的代码分割,可以很好的实现该功能

import 引入的组件其实是同步代码,当home组件引入该组件,加载的时候即使你没有用到该组件,依然会被请求,也就是我们常说的首屏优化,优化手段有很多,代码书写规范上可以优化,项目结构可以优化,视觉上的优化,减少不必要的请求,节流和防抖也是重要的手段等等,具体问题具体分析

打包分析

github.com/webpack/ana…

这个工具可以帮助我们分析webpack打包的全过程和相对应的资源消耗

配置脚本命令

"bundle": "webpack --profile --json > stats.json --config webpack.dev.js"

webpack.github.io/analyse/

然后在上传JSON文件

里面会自动帮我们将各种数据进行数据化,图形化 我们可以根据这些图形化信息来分析我们打包的过程和相应的性能参数

webpack 还提供多种打包资源可视化工具

代码使用率

打包后,运行html,在浏览器的控制台中输入ctrl+shift+p指令打开命令行:并输入coverage指令

这也是webpack在帮助我们优化项目时采用的基本逻辑, 比如用于代码分割的属性splitChunks里面的chunks的默认值就是async,即异步,因为异步的代码请求才可以减少首屏加载的时间

但是前面的触发是由一个事件触发的,比如我要实现点击登录按钮,然后把登录界面传输过来,那么一旦网络不是很好,这个卡顿就会很明显,所以我们还得继续优化 比如说,在网络空闲的时候,就自动发送请求,然后下载相应的文件?

Preloading,Prefetching

利用一个魔法注释 /* webpackPrefetch:true /等主业务核心逻辑加载完再加载其他文件 or / webpackLoad:true */和主业务核心逻辑一起加载,尽可能的提前加载

webpack性能优化

  • 可以用thread-loader或是parallel-webpack , happypack等进行多线程打包
  • 在尽可能少的模块上应用Loader(做好排除,tree shaking或是转义的目标模块)

自定义一个Loader

第三方的file-loader 作用是

  • 在出口处生成一个图片 该图片的名字hash ext组成的 并且返回hash 和ext组成的字符串

第三方的url-loader 作用是

  • 如果图片资源大于limit在出口处生成一个图片 该图片的名字hash ext组成的 并且返回hash 和ext组成的字符串
  • 如果图片资源小于limit会生base64格式的字符串 并且返回base64格式的字符串

my-url-loader

const loaderUtils = require('loader-utils')
const path = require('path')

function loader(source){
    // 基于图片的source生成图片名(字符串)
    const {limit} = loaderUtils.getOptions(this)
    const extname = path.extname(this.resourcePath).slice(1)
    
    if(source.length<limit){    // 如果打包的资源小于limit 那就把资源打包成base64
        // console.log(source.toString('base64'))
        const base64 = source.toString('base64')
        return `module.exports="data:image/${extname};base64,${base64}"`
    }else{
        return require('./my-file-loader').call(this,source)
    }
    // console.log(limit)
}
// 解析二进制文件
loader.raw = true
module.exports = loader

my-file-loader

const loaderUtils = require('loader-utils')

//该函数在匹配时触发  这个函数模拟babel-loader 把es6代码解析成es5代码
function loader(source){
    //基于图片的source生成图片名(字符串)
    const filename = loaderUtils.interpolateName(this, '[hash:6].[ext]', { content:source });
    // console.log(typeof filename)
    //基于图片的名字和图片的source产生图片
    // console.log(this)
    this.emitFile(filename,source);
    return `module.exports=${JSON.stringify(filename)}`
}

//解析二进制文件
loader.raw = true
module.exports = loader

webpack.config.js

{
    test:/\.jpg$/,
    use:{
          // loader:'file-loader',
          // loader:'my-file-loader',
          //   loader:'url-loader',
       	loader:'my-url-loader',
        options:{
          limit:8192,  
        }
    }
}