不懂Webpack4的前端不是好工程师(进阶篇)

1,700 阅读6分钟

包含了 Tree Shaking,Code Spliting,打包环境区分,缓存,shimming 等内容,继续扩展 Webpack 的基础知识面。

Tree Shaking

<!--index.js-->
import { add } from './math' 
add(1,2)

<!--math.js--> export const add = (a,b) => { console.log(a,b) return a + b } export const minus = (a,b) =>{ console.log(a,b) return a - b }

执行npx webpack打包后,打算dist/main.js可以看不到没有用到的minus方法也一并打包了,这种就造成我们打包的文件过于冗余,Tree Shaking就可以帮助我们解决这个问题,但是只能解决ES模块导入,像require其他导入方式就不行,因为ES是底层是导入静态资源,require是动态资源。

mode为development下配置增加

<!--webpack.config.js-->
module.exports = {
  mode: 'development',
  optimization: {
    usedExports: true //使用模块导入
  }
 }

配置后可以看到只到导出了add方法 import './index.css'文件怎么办,不就是会被忽略了,这个时候我们需要在package.json增加忽略的配置

{
   ...
  "sideEffects": false, 
}

如果是生成环境mode为production下,就不需要增加optimization配置,因为生成环境默认帮我们增加了,但是sideEffects还是要配置的。

Develoment 和 Production 模式的区分打包

项目上线我们就需要把mode Develoment 切换为 Production,我们可以在包文件分别配置不同的打包命令

{
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },
}

两种模式有很多共总的打包配置,我们可以使用webpack-merge抽离公用的配置,新建文件夹build,在该目录新建三个文件webpack.dev.js webpack.prod.js webpack.common.js

<!--webpack.common.js-->
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
  entry: {
    main: './src/index.js', //入口文件 默认:src/index.js
  },
  output: { //出口文件 默认: dist/main.js
    filename: '[name].js', //输出的文件名 
    path: path.resolve(__dirname, 'dist') //输出的路径,只能是绝对路径
  },
  module: {
        ...
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
}
<!--webpack.dev.js-->
const commonCongif = require('./webpack.common')
const merge = require("webpack-merge");
const devCongif = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    contentBase: './dist',
    port: 9000, //服务端口号
    open: true, //首次打包编译自动打开浏览器
    hot: true,
    hotOnly: true,
  },
  optimization: {
    usedExports: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

module.exports = merge(commonCongif,devCongif)

<!--webpack.prod.js-->
const commonCongif = require('./webpack.common')
const merge = require("webpack-merge");
const prodCongif = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
}

module.exports = merge(commonCongif,prodCongif)

Code Splitting

Code Splitting可以实现代码切割,按需加载。

  • 不使用Code Splitting方式:首次访问页面时,加载main.js(2mb),当页面业务逻辑发生改变时,又要加载2mb的内容

//index.js
import _ from 'loadsh' //1mb
console.log(_.join(['a','b','c'],'***'))
//此省略一万行业务代码
console.log(_.join(['a','b','c'],'***'))
  • 使用Code Splitting方式:main.js被拆成lodash.js(1mb),main.js(1mb),当页面业务逻辑发生改变时,只要加载main.js即可(1mb)
<!--loadsh.js-->
import _ from 'loadsh' //1mb
window._ = _
<!--index.js-->
console.log(_.join(['a','b','c'],'***'))
//此省略一万行业务代码
console.log(_.join(['a','b','c'],'***'))

//webpack.commom.js
module.exports = {
  entry: {
    loadsh: './src/loadsh.js',
    main: './src/index.js', //入口文件 默认:src/index.js
  },
}

webpack中实现代码分割,有两种方式:

  • 1.同步代码:只需要在webpack.commom.js中坐optimization的配置
  • 2.异步代码(类似import导入),无需做任何配置,会自动进行代码分割 // 同步导入
import _ from 'loadsh' //1mb
console.log(_.join(['a','b','c'],'***'))
//此省略一万行业务代码
console.log(_.join(['a','b','c'],'***'))
//异步导入
function getComponent() {
  return import('loadsh').then(({ default: _ }) => {
    var elem = document.createElement('div')
    elem.innerHTML = _.join(['a','b','c'],'***')
    return elem
  })
}

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

//webpack.common.js
module.exports = {
    ...
    optimization: {
        splitChunks: {
          chunks: 'all'
        }
    }
}

splitChunks默认配置详解

 optimization: {
    splitChunks: {
      chunks: 'all', //代码切割的类型: async: 只切割异步代码  initial:只切割同步代码 all:两种都切割
      minSize: 30000, //切割的文件最小要求,单位kb
      maxSize: 0,//切割的文件最大要求,单位kb,超过部分会继续切分
      minChunks: 1, //最小需要切割的次数需求:至少需要切割一次
      maxAsyncRequests: 6,//按需加载时并行请求的最大数量。
      maxInitialRequests: 4,//入口点的最大并行请求数。
      automaticNameDelimiter: '~',//默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)。此选项使您可以指定用于生成名称的定界符。
      cacheGroups: {//缓存组,同步切割时候会走该模块区分切割组
        defaultVendors: { 
          test: /[\\/]node_modules[\\/]/, //配置的模块:node_modules文件夹下的模块切割
          priority: -10 //切割优先级
        },
        default: { //如果没有达到上方的,就走到该默认组切割
          minChunks: 2,
          priority: -20, //切割优先级
          reuseExistingChunk: true //如果已存在切割模块,忽略这个组的切割
        }
      }
    }
  }

Lazy Loading 懒加载

懒加载可以实现按需加载,主要是通过import()来实现,如下点击事件才按需加载某个包 //异步导入

function getComponent() {
  return import(/* webpackChunkName: "lodash" */'loadsh').then(({ default: _ }) => {
    var elem = document.createElement('div')
    elem.innerHTML = _.join(['a','b','c'],'***')
    return elem
  })
}

document.addEventListener('click', () => { getComponent().then(elem => { document.body.appendChild(elem) }) })

Chunk

每个包生成的js文件就是一个chunk,minChunks配置就和这个有关minChunks: 1 最小需要切割的次数需求:至少需要切割一次,比如lodash文件切割至少一次。

打包分析

我们可以打包生成相关的json文件,利用第三方平台来查看分析我们打包的文件的各个方面

  • 步骤一:配置生成打包分析文件stats.json
<!--package.json-->
"scripts": {
   "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"
 }
  • 步骤二:利用第三方分析打包:http://webpack.github.io/analyse/

预取/预加载模块Preloading和Prefetching

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:

  • prefetch(预取):将来某些导航下可能需要的资源

  • preload(预加载):当前导航下可能需要资源 下面这个 prefetch 的简单示例中:

<!--loadsh.js-->
function loadsh () {
  var elem = document.createElement('div')
  elem.innerHTML = 'test'  
}
export default loadsh
<!--index.js-->
//推荐写法:异步导入的方式可以提高代码的使用率,可以在浏览器控制面板Coverage看到使用率提高
document.addEventListener('click', () => {
  import(/*webpackPrefetch: true */ './loadsh').then(({default: _}) => {
    _()
  })
})

查看浏览器加载的资源,资源会在主核心加载完毕后加载了该模块,在点击的时候直接从缓存直接提取出来

css文件的代码切割

MiniCssExtractPlugin切割css

webpack在做打包的时候会把css文件打包在js里,我们就需要借助插件来帮我们对css文件进行切割

  • 文档

  • 可对css的引入文件进行代码分割

  • 会把 css 打包成单独的一个文件

  • 这个插件适合在production模式下做打包

注意tree shaking的代码分割,如果不设置会忽略css代码切割导致切割失败

optimize-css-assets-webpack-plugin对css进行代码压缩,具体详情配置查看文档,配置文件代码如下

const commonCongif = require('./webpack.common')
const merge = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const prodCongif = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  module: {
    rules: [
      {
        test: /\.sass$/,
        use: [MiniCssExtractPlugin.loader, {
          loader: 'css-loader',
          options: {
            importLoaders: 2 // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
            // modules: true //按模块化引入
          }
        }, 'sass-loader', 'postcss-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', //css文件切割插件
    })
  ]
}

module.exports = merge(commonCongif,prodCongif)

压缩css文件

1.安装

 npm install optimize-css-assets-webpack-plugin --save-dev

2.使用优化配置

<!--webpack.prod.js-->
const optimizeCss = require('optimize-css-assets-webpack-plugin');
 optimization: {
    minimizer: [
      new optimizeCss({}) //进行css代码代码
    ]
  },

Webpack 与浏览器缓存( Caching )

当我们访问浏览器的时候,第一次请求资源会从服务器拿去,当再次访问的时候,相同的文件名我们会直接从缓存中提取。但是我们会想,如果当我们修改了代码推上去之后,当前用户已经访问了,当用户再次刷新文件名字还是一样,它就不会更新最新的代码,而是从浏览器缓存中直接读取旧的。webpackoutput相关配置就可以帮助我们解决这个问题,让用户可以重新加载已经修改最新的代码模块。

<!--webpack.prod.js-->
output: {
  filename: '[name].[contenthash].js', //输出的文件名 
  chunkFilename: '[name].[contenthash].js' //chunk文件生成的名字
},

contenthash可以在打包的时候监听代码是否改变,改变则生成新的hash,不变则用上次的

本文使用 mdnice 排版