Webpack原理实践

158 阅读5分钟

学习总结,不是自己单独思考输出

一,核心

webpack有两个核心特性:Loader机制,插件机制;

// ./webpack.config.js
/**
 * @type {import('webpack').Configuration}
 */

用类型注释的方式标注,这样我们在编写这个对象的内部结构时就会有正确的智能提示;Node环境默认不支持import语句

二,如何满足模块化打包需求

对于环境兼容问题的代码,可以在打包过程通过Loader机制实现编译转换,再进行打包;webpack可以在js中以模块化的方式载入任意类型的资源文件(比如css文件);具备代码拆分的能力,可以把初次加载所需要的模块打包在一起,其他模块再单独打包。

三,工作模式

  • production 模式下,启动内置优化插件,自动优化打包结果,打包速度偏慢;
  • development 模式下,自动优化打包速度,添加一些调试过程中的辅助插件;
  • none 模式下,运行最原始的打包,不做任何额外处理。

修改webpack工作模式的方式:

(1)通过CLI --mode参数传入

(2)通过配置文件设置mode属性

四,loader

css-loader只是把css模块加载到js代码中,而并不会使用这个模块,所以还需要使用style-loader把转换的结果通过style标签追加到页面上。

五,插件最常见的应用场景:

  • 实现自动在打包之前清除 dist 目录(上次的打包结果);

  • 自动生成应用所需要的 HTML 文件;  

  • 根据不同环境为代码注入类似 API 地址这种可能变化的部分; 

  • 拷贝不需要参与打包的资源文件到输出目录; 

  • 压缩 Webpack 打包完成后输出的文件; 

  • 自动发布打包结果到服务器实现自动部署。

六,插件机制->钩子机制

webpack要求插件必须是一个函数或者是一个包含apply方法的对象

七,运行机制,工作原理

webpack的整个打包过程,就是通过Loader处理特殊类型资源的加载(样式,图片等),通过Plugin实现自动化的构建任务(自动压缩,自动发布等)

webpack源码思路流程:

  1. Webpack CLI 启动打包流程; 

  2. 载入 Webpack 核心模块,创建 Compiler 对象; 

  3. 使用 Compiler 对象开始编译整个项目; 

  4. make阶段,从入口文件开始,解析模块依赖,形成依赖关系树; 

  5. 递归依赖树,将每个模块交给对应的 Loader 处理; 

  6. 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

八,监听模式

BrowserSync 工具 --- 可以实现文件变化后浏览器自动刷新功能

$ npm install browser-sync --global
$ browser-sync dist --watch

缺点: 操作繁琐,需要两个工具;效率低下,webpack将文件写入磁盘,BrowerSync进行读取。

webpack-dev-server: 自动编译和自动刷新浏览器

优点:不会将打包结果写在磁盘中,暂时放在内存中,提高了整体构建效率。

// ./webpack.config.js
module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: {
          '^/api': '' // 替换掉代理地址中的 /api
        },
        changeOrigin: true // 确保请求 GitHub 的主机名就是:api.github.com
      }
    }
  }
}

这样我们访问http://localhost:8080/api/users 就相当于访问 api.github.com/users

九 模块热替换(HMR)

webpack-dev-server会通知浏览器自动刷新,但页面刷新后,页面中的操作状态会丢失。开启HMR,只会实时的替换应用中的某个模块,而运行状态不会因此而改变。

HMR已经集成在webpack模块中了,不需要再单独安装。(1)只需要运行webpack-dev-server命令时,通过--hot参数开启。(2)或者配置文件中devServer中通过属性 hot: true,然后再引入HotModuleReplacementPlugin插件。

注意⚠️:hot 方式,如果热替换失败就会自动回退使用自动刷新,而 hotOnly 并不会使用自动刷新,我们就可以看到错误信息。

**思考:**由于js模块没有规律,导出可能是一个对象,一个字符串或者一个函数,所以没办法实现一个通用的替换方案,所以js更新后还是回退到自动刷新。但是我们使用一些脚手架工具,模块导出的必须是一个类或者函数,所以就可以实现通用的替换操作。

如果没有使用现成的脚手架,对于开启HMR特性的环境中,我们可以访问到全局的module对象中的hot成员,它提供了一个accept方法,用于注册某个模块更新后的处理函数

// index.js
let lastEditor = editor
module.hot.accept('./editor', () => {
   // 当 editor.js 更新,自动执行此函数
  // 临时记录更新前编辑器内容
  const value = lastEditor.innerHTML
  // 移除更新前的元素
  document.body.removeChild(lastEditor)
  // 创建新的编辑器
  // 此时 createEditor 已经是更新过后的函数了
  lastEditor = createEditor()
  // 还原编辑器内容
  lastEditor.innerHTML = value
  // 追加到页面
  document.body.appendChild(lastEditor)
})

十,Source Map -- devtool 

Source Map并不是webpack特有的功能,很多构建,编译工具都 支持

1.png

十一,Tree Shaking 去除冗余代码

为 JS 模块配置 babel-loader,会导致 Tree-shaking 失效。(不过在最新版本8.x的babel-loader中已经自动关闭了对ES Modules的转换)Tree-shaking 实现的前提是 ES Modules,在生产模式下会自动开启。

// ./webpack.config.js
module.exports = {
  // ... 其他配置项
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 压缩输出结果
    minimize: true,
    // 尽可能将所有模块合并到一起输出到一个函数中,这样既提升了运行效率,又减少了代码的体积。
    concatenateModules: true,
    // 完整移除没有用到的模块 这个特性在 production 模式下同样会自动开启。
    sideEffects: true
  },
  module: {
    rules: [
    // 强制开启ES Modules,设置为 commonjs,默认是auto,根据环境判断是否开启ES Modules
    // 此时 Tree-shaking失效
        {
           test: /\.js$/,
           use: {
               loader: 'babel-loader',
               options: {
                   presets: [['@babel/preset-env',{modules: 'commonjs'}]]
               }
           }
        }
    ]
  }
}

webpack打包某个模块之前,会先检查这个模块所属的package.json中的sideEffects标识,判断这个模块是否有副作用,如果没有副作用的话,这些没用到的模块就不再被打包。(所以例如某个UI组件库只有一两个组件用到,但只要它支持sideEffects就可以放心大胆的用)

十二,Code Splitting 代码分割 当应用很大,导致打包结果很大时

Webpack 实现分包的方式主要有两种:

  • 根据业务不同配置多个打包入口,输出多个打包结果;
  • 结合 ES Modules 的动态导入(Dynamic Imports)特性,按需加载模块。

将公共模块打包到单独的bundle中,例如vue.js react.js这些体积较大的模块

module.exports = {
    optimization: {
        splitChunks: {
            // 所有公共模块都被提取
            chunks: 'all',
        }
    }
}

动态导入,只需要按照 ES Modules 动态导入的方式去导入模块就可以了,Webpack 内部会自动处理分包和按需加载

魔法注释:在 import 函数的形式参数位置,添加一个行内注释,这个注释有一个特定的格式:webpackChunkName: ‘‘,这样就可以给分包的 chunk 起名字了

import(/*webpackChunkName: 'component'*/'./posts/index').then()

十三,环境配置

mode: development / production

对于大的项目,我们可以根据不同的环境写不同的配置文件,可以使用 webpack-merge合并webpack配置的需求

webpack.common.js
webpack.prod.js
webpack.dev.js

const merge = require('webpack-merge');
module.exports = merge(common, {
    // 配置
})

生产模式下的优化插件

(1)DefinePlugin -- 为代码注入全局成员,例如production模式下,注入一个process.env.NODE_ENV

// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
  // ... 其他配置
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: '"https://api.example.com"'
    })
  ]
}

这样我们在代码中 src/index.js 中就可以获取到全局变量 API_BASE_URL

(2)mini-css-extract-plugin 是一个可以将 CSS 代码从打包结果中提取出来的插件

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [ MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}

**注意⚠️:**通常css超过200kb才会考虑单独提取出来

(3)optimize-css-assets-webpack-plugin 压缩css文件

module.exports = {
  optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin()
    ]
  }
}

注意位置不是在plugins数组中,minimizer 表示使用自定义压缩插件,内部的js压缩器会被覆盖,可以再手动添加js压缩器 terser-webpack-plugin