webpack打包原理和基本配置

8,261 阅读7分钟

webpack简介

webpack实际上是一个静态模块打包工具

webpack 处理项目时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

打包原理

image

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码

打包调试命令

npm run dev

npm run build

packages.json

...
"scripts": {
  "dev_def": "webpack-dev-server --inline --public --config build/dev.js",
  "dev": "nodemon --watch config/index.js --exec \"webpack-dev-server --inline --public --config build/dev.js\"",
  "start": "npm run dev",
  "build": "cross-env NODE_ENV=production node build/build.js"
}
...

webpack-dev-server

是一个轻量级的服务器,修改文件源码后,自动刷新页面将修改同步到页面上

webpack-dev-server --inline --public --config build/dev.js
  • 【--inline 或者 --inline=false】 内联模式:将在包中插入脚本以处理实时重新加载,并且构建消息将显示在浏览器控制台中.
module.exports = {
  //...
  devServer: {
    inline: true
  }
};
  • 【--public xxx】使用内联模式并且正在代理dev-server时,内联客户端脚本并不总是知道连接到哪里。它将尝试根据服务器的URL来猜测window.location,但如果失败则需要使用它
  • 【--config xxx】指定配置文件
  • 【--progress】输出运行进度到控制台。

nodemon

会监测项目中的文件,一旦发现文件有改动,Nodemon 会自动重启应用

  • 【--watch xxx】 监控指定的文件或者目录
  • 【--exec xxx】 执行指定的命令
nodemon --watch config/index.js --exec \"webpack-dev-server --inline --public --config build/dev.js\"

这句话的意思就是: 用nodemon监控config/index.js文件,如果有变化,则重新执行【webpack-dev-server --inline --public --config build/dev.js】的命令

而【webpack-dev-server --inline --public --config build/dev.js】命令对项目本身具有热更新功能,但webpack配置文件修改时,dev-sever本身不会生效。而用nodemon就是在webpack配置文件修改时也重启服务,算是一个自动补充

cross-env

解决跨平台设置和使用环境变量的脚本,如变量名称、路径方面的抹平

  • cross-env NODE_ENV=production 抹平了跨平台环境变量的设置问题

基本配置

module.exports = {
  // 入口文件
  entry: {
    app: './src/js/index.js'
  },
  // 在哪里输出它所创建的 bundles
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'     //确保文件资源能够在 http://localhost:3000 下正确访问
  },
  // 开发者工具 source-map
  devtool: 'inline-source-map',
  // 创建开发者服务器
  devServer: {
    contentBase: './dist',
    hot: true                // 热更新
  },
  plugins: [
    // 删除dist目录
    new CleanWebpackPlugin(['dist']),
    // 重新穿件html文件
    new HtmlWebpackPlugin({
      title: 'Output Management'
    }),
    // 以便更容易查看要修补(patch)的依赖
    new webpack.NamedModulesPlugin(),
    // 热更新模块
    new webpack.HotModuleReplacementPlugin()
  ],
  // 环境
  mode: "development",
  // loader配置
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
}

__dirname: 当前文件所在文件夹的绝对路径

entry(入口文件配置)

单个入口语法(简写)

entry: './path/to/my/entry/file.js'

// 或者(对象写法)

entry: {
  main: './path/to/my/entry/file.js'
}

多页面应用程序

entry: {
  pageOne: './src/pageOne/index.js',
  pageTwo: './src/pageTwo/index.js',
  pageThree: './src/pageThree/index.js'
}

注意:webpack之前的写法(不推荐)

entry:{
  vendor:[resolve('src/lib/polyfill.js'), 'vue', 'vue-router'], // 不推荐
  app: resolve('src/main.ts')
}

在webpack4之前的版本中,通常将供应商添加为单独的入口点,以将其编译为单独的文件vendor(与之结合使用CommonsChunkPlugin)

在webpack 4中不鼓励这样做。相反,该optimization.splitChunks选项负责分离供应商和应用程序模块并创建单独的文件。不要为供应商或其他不是执行起点的东西创建条目。

output(输出文件配)

output: {
  filename: '[name].bundle.js',
  chunkFilename: [name].min.js,
  path: path.resolve(__dirname, 'dist'),
  publicPath: '/'     //确保文件资源能够在 http://localhost:3000 下正确访问
}
  • filename: 输出文件名
  • chunkFilename:此选项决定了非入口文件的名称
  • path:所有输出文件的目标路径
  • publicPath: 指定资源文件引用的目录,build后的文件,资源引用路径前缀

devtool(调试工具:文件映射)

// dev
devtool: 'eval-source-map'

// prod
devtool: 'source-map'

关键字揭秘:

关键字 含义
eval 在打包的时候,生成的bundle.js文件,模块都被eval包裹,并且后面跟着sourceUrl,指向的是原文件
source-map 这种配置会生成一个带有.map文件,这个map文件会和原始文件做一个映射,调试的时候,就是通过这个.map文件去定位原来的代码位置的
cheap 低消耗打包,就是打包的时候map文件,不会保存原始代码的列位置信息,只包含行位置信息,所以这就解释官网图后面的说明(仅限行)
... ...

devServer

devServer: {
  compress: true,
  port: 9000,
  hot: true,
  https: true,
  overlay: {
    warnings: false,
    errors: true
  },
  publicPath: '/platform/redapply/'
}
  • compress:启用gzip压缩
  • port: 端口号
  • hot:是否启用Hot Module Replacement特性
  • https:可以使用自签名的证书,同样可以自定义签名证书 | boolean、object
  • overlay:在浏览器上全屏显示编译的errors或warnings | boolean、object
  • publicPath:打包的文件将被部署到该配置对应的path。http://localhost:8080/platform/redapply/index.html

mode(告诉webpack相应地使用其内置优化)

// dev
mode: 'development'

// build
mode: 'production'

plugins(插件)

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
       NODE_ENV: '"production"'
     }
  }),
]

module.rules(loader配置)

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    },
    {
      test: /\.(png|svg|jpg|gif)$/,
      use: [
        'file-loader'
      ]
    }
  ]
}

对于loader的执行顺序,是从后往前的

项目常用配置

resolve(解析)

resolve: {
  extensions: ['.js','.ts', '.vue', '.json'],
  alias: {
    '@lib': resolve('src/lib'),
    '@models': resolve('build/models'),
    '@components': resolve('src/components'),
    '@data': resolve('src/data'),
    '@': resolve('src')
  }
}
  • extensions 解析路径缺省文件名时候的后缀(按顺序依次尝试)
  • alias 自定义路径符号定义

预加载文件

比如定义一些公共的scss文件。为了不再每个页面都引入该文件。我们可以设置文件预加载

module: {
  rules: [
    ...
    {
      test: /\.sass|scss|css$/,
      use: [
        ...
        {
          loader: 'sass-resources-loader',
          options: {
            resources: [
              path.resolve(__dirname, '../src/assets/css/vars.scss'),
              path.resolve(__dirname, '../src/assets/css/common.scss')
            ]
          }
        }
      ]
    }
  ]
}

optimization(优化配置项,build时配置)

optimization: {
    minimize: true,                         // 默认为true,效果就是压缩js代码。
    minimizer: [                            // 压缩时调用的插件
      new TerserPlugin(),
      new OptimizeCSSAssetsPlugin({})
    ],
    runtimeChunk: {                         // 默认为false,抽离出运行时公共代码块。
      name: 'manifest'
    },
    splitChunks:{
      chunks: 'all',                        // 必须三选一: "initial" | "all"(推荐) | "async" (默认就是async)
      minSize: 30000,                       // 生成块的最小字节数,30000
      minChunks: 1,                         // 最少被引用的次数
      maxAsyncRequests: 3,                  // 按需加载时候最大的并行请求数
      maxInitialRequests: 3,                // 一个入口最大的并行请求数
      name: true,                           // 打包的chunks的名字
      cacheGroups: {                        // 缓存配置
        common: {
          name: 'common',                   // 要缓存的 分隔出来的 chunk 名称
          chunks: 'initial',                // 必须三选一: "initial" | "all" | "async"(默认就是async) 
          priority: 11,
          enforce: true,
          reuseExistingChunk: true,         // 可设置是否重用该chunk
          test: /[\/]node_modules[\/](vue|babel\-polyfill|mint\-ui)/
        },
        vendor: {                           // key 为entry中定义的 入口名称
          name: 'vendor',                   // 要缓存的 分隔出来的 chunk 名称
          chunks: 'initial',                // 必须三选一: "initial" | "all" | "async"(默认就是async) 
          priority: 10,
          enforce: true,
          reuseExistingChunk: true,         // 可设置是否重用该chunk
          test: /node_modules\/(.*)\.js/
        },
        styles: {
          name: 'styles',
          test: /\.(scss|css)$/,
          chunks: 'all',
          minChunks: 1,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  }

runtimeChunk

默认为false, 抽离出运行时公共代码块

什么是运行时(runtime)?

JS在浏览器中可以调用浏览器提供的API,如window对象,DOM相关API等。这些接口并不是由V8引擎提供的,是存在与浏览器当中的。因此简单来说,对于这些相关的外部接口,可以在运行时供JS调用,以及JS的事件循环(Event Loop)和事件队列(Callback Queue),把这些称为RunTime。有些地方也把JS所用到的core lib核心库也看作RunTime的一部分。

chunk运行时

在chunk执行的时候所依赖的环境(方法)

splitChunks

chunks

function (chunk) | string

这表示将选择哪些块进行优化

string:

  • initial - 入口chunk,对于异步导入的文件不处理
  • async - 异步chunk,只对异步导入的文件处理(个人理解)
  • all - 全部chunk

function:

splitChunks: {
  chunks (chunk) {
    // exclude `my-excluded-chunk`
    return chunk.name !== 'my-excluded-chunk';
  }
}

cacheGroups

缓存组可以继承和/或覆盖任何选项splitChunks.*;要禁用任何默认缓存组,请将其设置为false。

  • priority 优先级,模块可以属于多个缓存组,单最终会被打入优先级高的chunk
  • reuseExistingChunk 表示可以使用已经存在的块,即如果满足条件的块已经存在就使用已有的,不再创建一个新的块
  • enforce minSize,minChunks,maxInitialRequests选项,为快速创建chunk用
  • test 缓存组的规则,表示符合条件的的放入当前缓存组
optimization: {
  splitChunks: {
    chunks: 'async',
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 3,
    maxInitialRequests: 3,
    name: true,
    cacheGroups: {
      common: {
        name: 'common',
        chunks: 'initial',
        priority: 11,
        enforce: true,
        reuseExistingChunk: true,         // 可设置是否重用该chunk
        test: /[\/|\\]node_modules[\/|\\](vue|babel\-polyfill|mint\-ui)/
      },
      vendor: {
        name: "vendor",
        chunks: "initial",
        priority: 10,
        test: /[\/|\\]node_modules[\/|\\](.*)\.js/
      },
      styles: {
        name: 'styles',
        test: /\.(scss|css|less)$/,
        chunks: 'initial',
        minChunks: 1,
        reuseExistingChunk: true,
        enforce: true
      }
    }
  }
}

注意:

这里有个坑,就是关于test匹配路径的问题 一般网上看到的如:

...
common: {
  name: 'common',
  chunks: 'initial',
  priority: 11,
  enforce: true,
  reuseExistingChunk: true,
  test: /[\/]node_modules[\/](vue|babel\-polyfill|mint\-ui)/
}
...

这里面test匹配正则是根据linux环境路径匹配的。(如:node_modules/vue)

但window路径和linux路径不一样,它是反斜杠。(如:node_modules\vue)

这样我们要把正则改成

/[\/|\\]node_modules[\/|\\](vue|babel\-polyfill|mint\-ui)/

这样就可以兼容两种环境了

项目中的配置

通常来讲webpack就需要3个配置文件

  • webpack.base.conf.js // 公共配置
  • webpack.dev.conf.js // 开发环境配置
  • webpack.prod.conf.js // 生产环境配置

但在我们的项目里大家看到的配置和上面介绍的并不完全相同。

是因为:我们在脚手架生成项目的时候,已经集成了webpack的基本配置。

它们都在@zz/webpack-vue下

image

而我们项目中仅暴露了一些配置对象入口。

image

暴露出来的配置项结构和webpack本身的略有区别。

开发人员可以通过自定义这些对象,然后程序会和默认的配置进行合并,形成最终的配置参数。

(本文为内部专题学习webpack的一次分享,内容比较基础)