Alan

此刻想举重若轻,之前必要负重前行

webpack4.0进阶学习

Tree Shaking按需打包文件

import { used } from './moment';

used();
export function used() {
  console.log('used function');
}

export function notUsed() {
  console.log('notUsed funtion');
}

上面这个例子中我们只使用到了moment中的used,但是打包后连同notUsed一起被打包进了main.js文件中

image-20200525151309388

Tree Shaking可以帮我们解决这个问题。

注意:1.Tree Shaking只在production模式下生效。2.只支持ES Module 语法(import),不支持CommonJs

"sideEffects": false,
或者
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
意思是对这些文件不进行tree shaking处理
例如
import './common.css';
虽然我们没有使用common.css的一些东西,但是它起到了样式的作用的,如果不在sideEffect中设置的话,webpack是不会对它进行打包的。
optimization: {
    usedExports: true,
}
// production模式是会自动配置好,可写可不写

开发环境和生产环境配置文件

由于开发环境需要调试代码所以会引入devServer之类的插件,那么这部分插件在生产环境中是不需要使用到的,我们可以对开发环境和生产环境分别设置不同的配置文件。

首先安装插件webpack-merge用来将拼接common配置

npm i webpack-merge -D

目录如下:

webpacktest
 ├── package.json
 ├── src
 │   ├── index.html
 │   ├── index.js
 │   └── moment.js
 ├── webpack.common.js
 ├── webpack.dev.js
 └── webpack.prod.js

webpack.common.js

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

module.exports = {
  mode: 'development', // 默认为production
  entry: {
    main: './src/index.js', // 打包入口文件
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 不对node_modules下的js文件处理
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  output: {
    // 输出文件配置
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    usedExports: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new CleanWebpackPlugin(),
  ],
};

webpack.dev.js

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

const devConfig = {
  mode: 'development', // 默认为production
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './dist',
    open: true, // 自动打开浏览器
    port: 3001, // 服务器端口号
    hot: true, // 开启HRM
  }
};

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

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

const prodConfig =  {
  mode: 'development', // 默认为production
  devtool: 'cheap-module-source-map'
};

module.exports = merge(commonConfig, prodConfig);

npm script

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

配置好后,开发环境使用npm run dev进行打包,生产环境用npm run build进行打包。

代码分割

代码分割有利于性能的优化。何为代码分割:

有这么一个场景,我在index.js中使用到了一些公共代码库/工具库(lodash),index.js中的代码是依赖于lodash中的一些工具的。当我们打包时,lodash也是被打包到了main.js文件中,并且一旦index.js中的业务代码改变了,连同lodash也要一同重新打包加载,但是我们一般是不会去改动lodash这类工具库的。于是我们可以借助代码分割来将业务代码和lodash进行分割,这样下次再修改业务代码时,我们就无需重新加载lodash的内容了。

先安装lodash

npm i lodash -S

这里为了便于查看打包后的文件内容,我们加上一条npm script"start": "webpack --config webpack.dev.js"(由于devServer不会生成打包内容)

index.js

import _ from 'lodash';

console.log(_.compact([0, 1, false, 2, '', 3]));

npm run start打包,发现打包后的main.js文件中包含lodash内容。

那如何实现代码分割呢,只需配置webpack.common.js文件

optimization: {
    splitChunks: {
        chunks: 'all'
    }
}

再次打包,发现打包后的文件中多了一个vendor~main.js,webpack自动将lodash内容打包进去了,而main.js文件中就没有了lodash的内容了。

dist
 ├── index.html
 ├── main.js
 └── vendors~main.js

上面介绍的时同步代码分割,下面看一下异步代码分割index.js,可以实现懒加载

async function createElement() {
  const { default: _ } = await import(/* webpackChunkName: "lodash" */'lodash');
  const element = document.createElement('div');
  element.innerHTML = _.compact([0, 1, false, 2, '', 3]);
  return element;
}

document.addEventListener('click', () => {
  createElement().then(element => {
    document.body.appendChild(element);
  })
})

/* webpackChunkName: "lodash" */设置打包后的文件名为vendors~lodash.js,打开浏览器可以看到只有点击页面时,才会引入vendors~lodash.js,实现了懒加载

打包后的目录

dist
 ├── index.html
 ├── main.js
 ├── vendors~lodash.js
 └── vendors~main.js

当然可以通过其他配置来设置打包后的文件名称。

代码分割更多配置

打包分析工具

首先要拿到status.json文件,具体获取方式只需配置npm script即可

"start": "webpack --profile --json > status.json --config webpack.dev.js",

打包后会生成status.json文件。

然后使用官网提供的一些工具就可以可视化分析打包结果了。

在写代码时,我们要尽可能的使用异步引入,这样可以提高代码的使用率,提升性能,减少加载不必要的代码。

查看代码使用率的方法,浏览器控制台按下ctrl+shift+p,输入show coverage

代码优化

现在有一个优化场景,我有一个登录按钮,当点击按钮后弹出登录框。这里的优化思路是,页面加载时只加载登录按钮的代码,当按钮代码加载完后。利用空闲时间去加载登录框的代码。这样既可以优化首屏加载速度,还可以解决因使用懒加载登录框(也就是点击按钮后再去加载)而带来的用户体验较差的问题。

具体代码:(只需要在import中加入/* webpackPrefetch: true */)

index.js

document.addEventListener('click', () => {
  import(/* webpackPrefetch: true */'./loginModal.js').then(({default: login}) => {
    login();
  })
})

loginModal.js

export default function () {
  alert('loginModal');
}

css文件处理

MiniCssExtractPlugin

This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.

官方推荐不要在开发环境中使用,因为不支持HMR,不利于提高开发效率。

npm install --save-dev mini-css-extract-plugin

index.js

import './style.css';

style.css

body {
  background: #e65;
}
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'development', // 默认为production
  entry: {
    main: './src/index.js', // 打包入口文件
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 不对node_modules下的js文件处理
        loader: 'babel-loader',
      },
    ],
  },
  output: {
    // 输出文件配置
    filename: '[name].js',
    chunkFilename: '[name].chunk.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new CleanWebpackPlugin(),
  ],
  optimization: {
    usedExports: true,
    splitChunks: {
      chunks: 'all',
    },
  },
};
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './dist',
    open: true, // 自动打开浏览器
    port: 3001, // 服务器端口号
    hot: true, // 开启HRM
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      }
    ],
  },
};

module.exports = merge(commonConfig, devConfig);
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const prodConfig =  {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      }
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({})
  ]
};

module.exports = merge(commonConfig, prodConfig);
"sideEffects": [
    "*.css"
],

也可以使用optimize-css-assets-webpack-plugin来压缩css代码

配置output解决浏览器cache问题

浏览器是有缓存功能的,在我们第一次加载main.js后,浏览器会保有main.js的缓存,下次再加载时就直接从缓存获取了。但是当我们下次发布新版本时(修改了main.js文件),浏览器还是使用以前缓存的main.js内容,所以显示的内容并不是最新的。

处理方法

设置output的filename,添加[contenthash]占位符。

output: {
    // 输出文件配置
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
},

shimming

看一个场景,假设library.js是一个比较老的第三方库。

import './style.css';
import { createText } from './library';

createText();
// 这个第三方库中使用到了loadsh,但是并没有引入lodash
export function createText() {
  document.getElementById('root').innerHTML = _.compact([0, 1, false, 2, '', 3]);
}

npm run dev打包后浏览器报错

Uncaught ReferenceError: _ is not defined

如果library.js是我们自己写的库那还好说,直接自己手动引入lodash就可以了。但是由于是第三方库,源文件是在node_module中的,不利于修改,这个时候就可以用shimming来解决了。

配置webpack.common.js

// 记得引入const webpack =require('webpack');
// 下面代码的意思:当遇到_时,会自动为我们添加下面代码
// import _ from 'lodash'
plugins: [
    new webpack.ProvidePlugin({
        _: 'lodash'
    })
],

更多配置参考shimming

细粒度 shimming

试着在index.js中打印出this,发现this其实是指向模块本身。那如何把this指向window呢,这里要借助imports-loader

npm i imports-loader -D

配置好loader

rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/, // 不对node_modules下的js文件处理
        use: [
            {
                loader: 'babel-loader',
            },
            {
                loader: 'imports-loader?this=>window'
            }
        ]
    },
]

总结

  • Tree Shaking可以实现对js文件的按需打包,只在production下生效。
  • 为生产和开发环境分别创建不同配置文件。
  • 利用代码分割实现懒加载(利用魔法注释)。
  • 利用status.json来分析打包过程。
  • 单独生成css文件,减少mian.js体积。
  • 为output文件设置hash值,防止浏览器使用缓存。

评论