Webpack 核心概念

Posted by violetks on September 2, 2020

SourceMap

SourceMap是一个映射关系,调试工具可以通过SourceMap映射代码,让我们在源代码上断点调试。但是使用SourceMap会让编译速度变慢,可到官网查看详细配置。

// webpack.config.js 相关代码片段
module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/index.js',
  ...
};

最佳实践:

  • development 环境:cheap-module-eval-source-map
  • production 环境:cheap-module-source-map

DevServer

DevServer会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack,并接收 Webpack 发出的文件变更信号,通过 WebSocket 协议自动刷新网页做到实时预览

# 安装 DevServer
$ npm i -D webpack-dev-server

# 启动 DevServer
$ webpack-dev-server

实现修改代码时自动打包的两种方法:

第一种:修改 package.json,代码修改后能自动打包,不能自动刷新浏览器页面。

"scripts": {
  "dev": "webpack --watch",
},

第二种:使用 DevServer,能自动刷新页面。

// webpack.config.js 添加下面配置
devServer: {
  contentBase: './dist',
  open: true,   // 自动打开浏览器
  proxy: {
    '/api': 'http://localhost:3000'
  }
}
// package.json
"scripts": {
  "start": "webpack-dev-server"
},

Hot Module Replacement 模块热更新

此功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。例如改变样式代码时不会刷新页面,方便调试 CSS。
webpack 5HMR已自动支持,无需配置。

// webpack.config.js 相关代码片段
const webpack = require('webpack'); // 1.要引入 webpack 插件

devServer: {
  contentBase: './dist',
  open: true,   // 自动打开浏览器
  hot: true,    // 2.开启热模块更新
  hotOnly: true,// 即使热模块功能没生效,也不让浏览器自动刷新
},
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader',
        'postcss-loader', // 可为 transform 等样式自动添加厂商前缀
      ], // 执行顺序从右到左
    }
  ]
},
plugins: [
  new webpack.HotModuleReplacementPlugin(), // 3.使用插件
],

使用 Babel 处理 ES6 语法

Babel官网:https://babeljs.io

174918.png

175925.png

Tree Shaking

当项目达到一定体积时,像 JS 代码会分成模块管理,这样最终打包时可能会导入实际上未使用的代码。Tree Shaking是一种通过消除最终文件中未使用的代码来优化体积的方法。
只有模块是通过static方式引用时,Tree Shaking才会起作用。CommonJS模块的dynamic性质意味着无法应用Tree Shaking,因为在实际运行代码之前无法确定需要哪些模块。Tree Shaking只支持ES Module,因为这是static的。

例如,有一个实现基数数学运算的JS文件math.js

// math.js
export function add (a, b) {
  console.log(a + b);
}

export function minus (a, b) {
  console.log(a - b);
}

index.js里,我们通过如下方式调用该文件:

import { add } from "./math";

add(1, 2);

使用webpack打包后,发现即使仅导入并使用了add()功能,我们也会看到文件中的所有功能都包含在最终输出中。使用Tree Shaking就可以将未使用的模块从最终的打包文件中删除,甚至可删除从未访问过的导入对象中的特定属性。

// webpack.config.js 相关代码片段
mode: "development",
plugins: [...],
optimization: { // development 模式需配置,production 模式不用配置
  usedExports: true
},
// package.json 相关代码片段
{
  ...,
  "sideEffects": [ // 将某些文件标记为副作用
    "./src/polyfill.js"
  ],
  ...,
}

Development 和 Production 模式的区分打包

1、将webpack.config.js替换为webpack.common.jswebpack.dev.jswebpack.prod.js三个文件。

  • webpack.dev.js:开发环境使用。
  • webpack.prod.js:生产环境使用。
  • webpack.common.js:存放以上两个的公共代码部分。
$ npm install webpack-merge -D
// webpack.dev.js 需要引入 webpack-merge 和 webpack.common.js,webpack.prod.js 也类似
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
  mode: 'development',
  ...
};

module.exports = merge(commonConfig, devConfig);

2、在package.json中进行下面配置。

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

Webpack 和 Code Splitting

项目中使用到lodash这种第三方库时,会和业务逻辑代码一起打包到一个 JS 文件,这样会带来两个问题:(1)打包文件会很大,加载时间会长;(2)修改业务逻辑代码,重新访问页面时加载的内容也很多。
因此需要使用代码分割,将lodash单独打包到一个 JS 文件。

通过配置webpack实现打包时自动代码分割

// webpack.config.js 相关代码片段
optimization: {
  splitChunks: {
    chunks: 'all' // all:对同步代码和异步代码做分割,async:只对异步代码做分割
  }
}

使用 Preloading,Prefetching 优化加载速度

webpack中的SplitChunkPlugin的默认配置是chunks: async,只有当异步时才会进行代码分割。因为当我们在引入lodashjquery库时,如果配置chunks: all,那么肯定会帮助我们把lodashjquery单独拆分,但是这样做并不能提升首页的代码性能,原因是在浏览器第一次加载时,还是需要我们加载lodashjquery库,只有当我们的代码第二次加载时,浏览器才会从缓存中去找,提高第二次页面的加载访问速度。webpack配置chunks: async是想让我们第一次加载时,访问速度就是最快。

例如,有一个未优化的代码:

// index.js
document.addEventListener('click', () => {
  const div = document.createElement('div');
  div.innerHTML = 'hello webpack';
  document.body.appendChild(div);
});

打包运行后,在浏览器控制台中按Ctrl + Shift + P,然后在弹出的对话框中输入coverage点击回车,然后再点击下面的小黑原点变为红原点,刷新页面会看到如下图所示的信息:

UucG5n.png

说明当前页面加载的文件代码利用率为74.6%。实际上点击事件里的代码没必要在加载页面时也一起加载,可以将它们改为异步加载,这样再打包运行利用率就会提升到75%

// click.js
function handleClick () {
  const div = document.createElement('div');
  div.innerHTML = 'hello webpack';
  document.body.appendChild(div);
}

export default handleClick;
// index.js
document.addEventListener('click', () => {
  import('./click.js').then(({default: func}) => {
    func();
  })
});

这时如果click.js文件很大,那加载的时间也会很长,可以考虑在加载完页面网络空闲的时候先把这些文件加载进来,可以使用Prefetching

// index.js
// 在异步加载的文件前面加上 /* webpackPrefetch: true */ 即可。
document.addEventListener('click', () => {
  import(/* webpackPrefetch: true */'./click.js').then(({default: func}) => {
    func();
  })
});

PreloadingPrefetching 的区别:Preloading是和核心代码文件一起去加载,Prefetching是在核心代码加载完成之后的空闲时间再去加载。

CSS 文件的代码分割

希望在打包生成代码的时候,如果引入了CSS文件,那么把CSS文件单独打包,而不是直接引入到JS文件里面,可以使用一个插件来实现。

npm install --save-dev mini-css-extract-plugin
// 在 webpack.prod.js 使用
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2,
            }
          },
          'sass-loader',
          'postcss-loader',
        ],
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
        ],
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[name].chunk.css'
    })
  ]
}

module.exports = merge(commonConfig, prodConfig);