模块化打包工具

本文详细介绍了前端模块打包工具的重要性和作用,重点讲解了Webpack的使用,包括基本配置、工作模式、资源模块加载、常见加载器和插件的运用。还提到了其他打包工具如Rollup和Parcel的特点和使用,探讨了不同工具在代码分割、优化和自动化工作流程中的策略,为前端开发提供打包解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

模块打包工具产生的原因

  1. ESM 存在兼容问题
  2. 模块文件过多,网络请求频繁
  3. 所有前端资源都需要模块化,不仅仅是js文件
  4. 需要有一个工具能自动完成下列工作
    1. 编译最新特性,使浏览器能够兼容最新特性
    2. 能够将模块文件打包到一起
    3. 能够支持不同种类的资源类型,如图片,htmljscss,以及其他的各种文件

常用的模块打包工具

  1. Webpack
  2. Rollup
  3. Parcel

Webpack的使用

  1. 基本使用

    1. 安装 webpackwebpack-cli
    2. 命令行运行 yarn webpack 即可打包,webpack 默认以 /src/index.js 为入口,并将打包后的js文件放在 dist/main.js 目录中
  2. webpack 配置文件: 在项目根目录下新建webpack.config.js,该文件是运行在node中的js,遵守CommonJs规范。该文件需要导出一个对象,用来描述webpack行为。对象中的基本配置属性:

    1. entry: 项目的入口文件配置选项,可以是一个字符串,数组,对象

      1. 字符串: 表示单个入口,如果是相对路径, ./ 不能省略
      2. 数组: 表示将多个文件打包到一个文件
      3. 对象:多文件入口,keychunk 名,值是入口配置
    2. output:打包之后的输出配置,接收一个对象,对象下列基本配置属性

      1. filename: 输出文件的文件名
      2. path: 输出文件的目录,是个绝对路径,可以通过 node path 模块来指定绝对路径
      const path = require('path')
      module.exports = {
        mode: 'none',
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'output'),
          publicPath: 'output/'
        },
      }
      
  3. 工作模式,包含3种工作模式:productiondevelopmentnone,不同的模式有不同的内置优化

    1. 指定模式的方式:
      1. 在命令行中指定:通过添加 --mode production/development/none 命令行参数的形式添加
        在这里插入图片描述

      2. 在配置对象中添加 mode 属性

        const path = require('path')
        module.exports = {
          mode: 'none',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            publicPath: 'output/'
          },
        }
        
    2. 工作模式的具体介绍
      1. development:会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development(设置NODE_ENV时不会设置mode). 为模块和 chunk 启用有效的名。
        在这里插入图片描述

      2. production(默认):会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
        在这里插入图片描述

      3. none:不使用任何默认优化选项
        在这里插入图片描述

  4. webpack 资源模块加载

    1. webpack 中默认只会处理js文件,其他资源模块一般都需要经过 loader 处理,然后在js中引入。loaderwebpack的一个核心功能。 这样设计的目的:

      1. 根据代码加载资源,按需加载。JavaScript驱动前端应用
      2. 确保上线的文件不会缺失,都是必要的
    2. css资源的加载:需要先经过 css-loader 处理,将css代码转换为一个js模块,然后再经过style-loadercss样式通过style标签的方式引入。

      1. 先安装css-loaderstyle-loader
      2. webpack.config.js中配置module->rulesrules接收一个对象数组
        1. 对象中的test为文件匹配的正则

        2. use: 表示使用的loader,可以是一个字符串和数组,如果是一个数组,文件则会从后向前依次经过多个loader转换。

          const path = require('path')
          module.exports = {
            mode: 'none',
            entry: './src/index.js',
            output: {
              filename: 'bundle.js',
              path: path.join(__dirname, 'output'),
              publicPath: 'output/'
            },
            module: {
              rules: [
                {
                  test: /.css$/,
                  use: [
                    'style-loader',
                    'css-loader'
                  ]
                }
              ]
            }
          }
          
    3. 文件资源的加载:file-loaderfile-loader可以将文件资源转化为js模块,通过 import 导入之后可以拿到文件资源对应的路径。

      // webpack.config.js
      const path = require('path')
      module.exports = {
        mode: 'none',
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'output'),
          publicPath: 'output/'
        },
        module: {
          rules: [
            {
              test: /.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            }
          ]
        }
      }
      // 使用
      import bg from './loginBg.jpg'
      const img = new Image()
      img.src = bg
      

      此外还可以使用url-loader来转换文件。会将文件转换为DataURL的形式。但是只适合体积较小的文件, 体积较大的文件打包之后会导致打包之后的文件过大。因此需要限制不超过指定大小的文件使用url-loader处理(通过url-loader配置选项中的limit来配置),对于超出限制大小的文件默认会使用file-loader进行处理,所以需要默认安装file-loaderurl-loader转换后的文件不会有实体文件,而是一串DataURL字符串。
      DataURL格式:
      在这里插入图片描述
      DataURL字符串示例
      在这里插入图片描述
      url-loader使用配置示例

      const path = require('path')
      module.exports = {
        mode: 'none',
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'output'),
          publicPath: 'output/'
        },
        module: {
          rules: [
            {
              test: /.(png|jpg)$/,
              use: {
                loader: 'url-loader',
                options: {
                  limit: 10 * 1024
                }
              },
            }
          ]
        }
      }
      
    4. 转换ES6代码到ES5。通过 babel-loader 来实现js代码的编译转换 webpack只负责打包,代码的编译通过loader来实现

      1. 安装 babel-loader @babel/core @babel/preset-env

      2. webpack.config.js中添加配置

        const path = require('path')
        module.exports = {
          mode: 'none',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            publicPath: 'output/'
          },
          module: {
            rules: [
              {
                test: /.js$/,
                use: {
                  loader: 'babel-loader',
                  options: {
                    presets: ['@babel/preset-env']
                  }
                }
              }
              }
            ]
          }
        }
        
      3. 重新运行打包,可以查看打包后的对应文件的js代码已经转换为 es5

    5. 常见加载器分类,即 loader 分类

      1. 文件类:主要用于加载文件资源,并返回相应的内容。如url-loader,file-loader
      2. 语法转换类:用于将资源转换为指定语法的代码,如babel-loaderts-loader
      3. 样式类:用于加载转换各种样式资源,如css-loaderstyle-loaderless-loadersass-loader
      4. 语法检查和测试类:用于检查语法或测试,如eslint-loader
  5. webpack 加载资源的方式

    1. js加载 ,支持ESMCommonJsAMD等模块化加载方式
    2. loader加载的非 js文件,如 css 中的@importurl 函数 ,html-loader
    3. imgvideo 等标签 的 src 属性。
  6. webpack 核心工作原理

    1. 根据配置文件确定入口文件
    2. 通过解析入口文件中的 import/require 等解析文件的依赖文件, 逐步深入查找并构建表示项目文件的依赖关系的依赖树
    3. 对依赖树进行递归,找到对应的文件,并根据配置文件中的 module 下的 rules 规则对文件进行加载
    4. 将加载后的文件合并输出到 bundle.js
  7. 编写 loader

    1. loader 是导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用

      1. 接收上一个 loader 产生的结果或者资源文件(resource file) 作为参数

      2. 函数的 this 上下文由 webpack 填充

      3. 函数可以返回一个代表 JavaScript 源码的 String / Buffer注意为防止源码中的特殊字符出现识别错误,需要将源码字符串进行转义

        const marked = require('marked')
        module.exports = source => {
          const html = marked(source)
          return `export default ${JSON.stringify(html)}`
        }
        
      4. 也可以直接返回一个字符串,交给下一个loader处理,如下图直接返回html字符串,然后使用 html-loader 进行处理

        const marked = require('marked')
        module.exports = source => {
          const html = marked(source)
          return html
        }
        
    2. loader 特点:

      1. loader 实际上是一个输入到输出的转换
      2. loader 支持链式调用,一个文件资源可以依次经过多个loader处理
  8. webpack 插件:增强webpack自动化能力

    1. 用途:处理资源加载之外的自动化工作

      1. 打包之前清除目录
      2. 拷贝静态文件到输出目录
      3. 压缩输出代码
    2. 常见插件的使用

      1. 自动清除输出目录 clean-webpack-plugincleanStaleWebpackAssets自动构建时是否清除未使用的资源。

        const { CleanWebpackPlugin } = require('clean-webpack-plugin')
        module.exports = {
          mode: 'none',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            // publicPath: 'output/'
          },
          plugins: [
            new CleanWebpackPlugin({
              cleanStaleWebpackAssets: false
            })
          ]
        }
        
      2. 自动生成 HTML 插件 html-webpack-plugin

        1. 基本使用

          const HtmlWebpackPlugin = require('html-webpack-plugin')
          module.exports = {
            mode: 'none',
            entry: './src/index.js',
            output: {
              filename: 'bundle.js',
              path: path.join(__dirname, 'output'),
              // publicPath: 'output/'
            },
            plugins: [
              new CleanWebpackPlugin({
                cleanStaleWebpackAssets: false
              }),
              new HtmlWebpackPlugin(),
            ]
          }
          
        2. 修改标题,添加源数据标签

          new HtmlWebpackPlugin({
            title: 'webpack-loader',
            meta: {
              viewport: 'widt=device-width'
            },
          }),
          

          在这里插入图片描述

        3. 指定模板,在使用插件是指定配置属性 template,在html 模板文件中可以使用 ejs 模板语法,通过 htmlWebpackPlugin.options 可以访问到插件的配置属性。

          new HtmlWebpackPlugin({
            title: 'webpack-loader',
            meta: {
              viewport: 'widt=device-width'
            },
            template: './index.html'
          }),
          
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <title><%= htmlWebpackPlugin.options.title %></title>
          </head>
          <body>
            <h1> <%= htmlWebpackPlugin.options.title %> </h1>
          </body>
          </html>
          
        4. 创建多个HTML文件,只需要在 plugins 中添加多个实例。通过 filename 配置来指定生成的文件名称。

          plugins: [
              new HtmlWebpackPlugin({
                title: 'webpack-loader',
                meta: {
                  viewport: 'widt=device-width'
                },
                template: './index.html'
              }),
              new HtmlWebpackPlugin({
                filename: 'mutiple.html'
              }),
           ]
          
      3. 复制文件到输出目录 copy-webpack-pluginpatterns 是要复制的文件目录列表。

        new CopyWebpackPlguin([
            'public'
         ]),
        
    3. 插件的编写

      1. webpack通过在每个环节挂载钩子函数的方式来实现插件的扩展
        在这里插入图片描述

      2. webpack 插件是一个函数或类,函数或类的prototype中必须包含一个 apply 方法

      3. 然后根据插件的需求,确定插件所在事件钩子,并在该钩子中通过 tap 绑定一个回调函数到事件钩子中,在构建到该钩子所处阶段时会调用该回调函数。

      4. 然后再函数中进行相应的处理

        1. compiler: 代表了完整的 webpack 环境配置,包括optionsloaderpluginhooks
        2. compilation:代表当前的构建的上下文环境,包括当前的模块资源,编译生成资源、变化的文件、以及被跟踪依赖的状态信息。通过 compilation.assets 对象访问到所有编译后的内容。
        class MyPlugin {
          // 在 prototype 上定义一个接收 compiler 对象的 apply 方法
          apply(compiler) {
            // 指定要附加到的钩子事件
            compiler.hooks.emit.tap(
              'MyPlugin',
              (compilation) => {
                const assets = compilation.assets;
                for(const f in assets) {
                  if (f.endsWith('.js')) {
                    const newContent = assets[f].source().replace(/\/\**\*\//g, '')
                    assets[f] = {
                      source: () => newContent,
                      size: () => newContent.length
                    }
                  }
                }
              }
            );
          }
        } 		
        
  9. 开发体验增强

    1. 文件改变之后自动打包:以 监视模式 运行webpack打包,监听文件变化之后会自动运行 webpack 打包。命令行添加 --watch 参数将开启监视模式

    2. webpack-dev-servewebpack本地开发功能插件,打包项目,并启动本地http server,监听文件变化,自动重新打包,并刷新浏览器。

      1. 安装 webpack-dev-server 之后, 直接运行yarn webpack-dev-sever,会自动以监视模式打包,并启动本地http server,文件修改自动打包,并刷新浏览器。

      2. 指定静态资源目录:通过webpack.config.js中配置 devServer 下的contentBase,会从指定的目录下获取静态资源

        module.exports = {
          mode: 'none',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            // publicPath: 'output/'
          },
          devServer: {
            contentBase: 'public',
          }
        }
        
      3. 配置代理
        通过webpack.config.js中配置 devServer 下的proxy 来配置

        module.exports = {
          mode: 'none',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            // publicPath: 'output/'
          },
          devServer: {
            contentBase: 'public',
            proxy: {
              '/api': {
                // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user
                target: 'https://api.github.com',
                // 重写替换后的请求路径,将 './api' 部分替换为 ‘’
                pathRewrite: {
                  '/api': ''
                },
                // 不能使用 localhost:8080 作为请求 GitHub 的主机名
                changeOrigin: true,
              }
            }
          }
        }
        
    3. 源代码映射(Source Map)

      1. Source Map: 开发阶段帮助直接定位到源码。关键文件为一个.map 文件,文件保存了压缩代码文件到源文件的映射关系。文件为一个JSON格式的文件,包括以下属性:

        1. version:文件版本

        2. sources: 源文件名,可能是多个文件合并产生的,所以是一个数组

        3. names:源码中定义的变量名
          在这里插入图片描述

        4. mappings:记录转换之后以及转换之前代码的映射关系,是一个 base64-vlq 编码的字符串
          在这里插入图片描述

        5. 如何在压缩代码中引用source Map 文件:在压缩代码最后一行添加注释引入。引入之后将会自动请求源代码,所以map文件所在目录下需要有源码文件,否则将无法查看源码
          在这里插入图片描述

      2. webpack开启 Source Map: 通过在webpack.config.js配置中添加 devtool 属性,并指定一个表示 Source Map 模式的值,下列是所有模式的比较。
        在这里插入图片描述
        从模式命名解读各模式的区别:打包时不生成source map文件,打包速度快,但是错误定位时只能定位到文件。

        1. eval,使用eval函数执行打包后的代码,只能定位文件
          在这里插入图片描述

        2. cheap,简易版,只能定位行,不能定位列

        3. source-map,生成source-map 文件

        4. module,不经过 loader 转换

        5. inline,以dataURL的形式将source-map代码嵌入到生成的文件中

        6. hidden,浏览器中隐藏 source-map 文件,但是会生成source Map文件

        7. nosources,不在浏览器中显示source-map文件,会提示行列信息

    4. 自动刷新问题:自动刷新时会自动刷新浏览器,导致页面状态丢失,解决办法就是开启 HMR

      1. 开启 HMR 有两种方式:

        1. 启动webpack-dev-server时添加命令行参数 --hot
          在这里插入图片描述

        2. 配置webpack.config.js,在devServer中添加 hot:true,并在plugins中添加new webpack.HotModuleReplacementPlugin()

          module.exports = {
            mode: 'none',
            entry: './src/index.js',
            output: {
              filename: 'bundle.js',
              path: path.join(__dirname, 'output'),
              // publicPath: 'output/'
            },
            devServer: {
              hot: true,
              contentBase: 'public',
              proxy: {
                '/api': {
                  // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user
                  target: 'https://api.github.com',
                  // 重写替换后的请求路径,将 './api' 部分替换为 ‘’
                  pathRewrite: {
                    '/api': ''
                  },
                  // 不能使用 localhost:8080 作为请求 GitHub 的主机名
                  changeOrigin: true,
                }
              }
            }
            plugins: [
              new HtmlWebpackPlugin({
                title: 'webpack-loader',
                meta: {
                  viewport: 'widt=device-width'
                },
                template: './index.html'
              }),
              new webpack.HotModuleReplacementPlugin(),
            ]
          }
          
      2. HMR 的问题

        1. 需要手动处理当模块更新之后如何将更新之后的代码替换到页面中

        2. 样式文件在开启HMR之后,文件更新能直接替换到页面中,而不改变页面状态,是因为style-loader中自动处理了样式的热更新
          开启source Map 后能在开发者工具中的sources下的webpack->src中找到对应处理的js
          在这里插入图片描述

        3. 样式文件能够统一处理热更新是因为只需要将修改的样式替换即可,而js代码则是由于代码逻辑各不相同,无法统一替换执行

        4. 某些框架也提供了脚手架也集成了HMR方案,所以可以直接使用。因为框架的js代码相对而言更有规律,或是导出一个函数或是导出一个对象,所以能够实现统一的逻辑来处理文件的热更新。

        5. 手动处理js文件的热更新,通过使用处理 HMRAPI

          1. 在引用模块的文件中,通过module.hot.accept(url, func)注册模块热更新处理

          2. module.hot.accept()接收两个参数,第一个参数为模块的路径,第二个参数为模块热更新的逻辑。

            	// heading.js
            	import './heading.css'
            	export default () => {
            	  const ele = document.createElement('div')
            	  ele.contentEditable = true
            	  ele.className = 'head'
            	  return ele
            	}
            	// index.js
            	import createHello from './heading'
            	import test from './test.md'
            	import bg from './images/loginBg.jpg'
            	const helloEle = createHello()
            	document.body.appendChild(helloEle)
            	document.body.style.background = `url(${bg})`
            	let lastEel = helloEle
            	// 根据代码逻辑,heading 文件是导出一个生成元素的函数,文件更新时只要重新生成元素替换掉原来的元素即可,
            	module.hot.accept('./heading', () => {
            	  // 根据操作,先生成一个新的元素,并记录
            	  const newEle = createHello()
            	  // 移除原来的元素
            	  // document.body.removeChild(lastEel)
            	  // 将原来元素的状态保存到新的元素中
            	  newEle.innerHTML = lastEel.innerHTML
            	  // // 替换原来的元素
            	  document.body.replaceChild(newEle, lastEel)
            	  lastEel = newEle
            	})
            	```
            
            
        6. 处理图片的热更新
          图片的热更新只需要将引用图片的位置重新替换图片路径即可。

          import createHello from './heading'
          import test from './test.md'
          import bg from './images/loginBg.jpg'
          
          const helloEle = createHello()
          
          document.body.appendChild(helloEle)
          document.body.style.background = `url(${bg})`
          
          // 图片热更新处理
          module.hot.accept('./images/loginBg.jpg', () => {
            document.body.style.background = `url(${bg})`
          })
          
        7. HMR 注意事项

          1. module.hotHMR的插件提供的,若是没有开启HMR时,module.hot不存在,热更新的处理逻辑将报错,所以需要将热更新的处理逻辑包裹在module.hot是否存在的判断中

            if(module.hot) {
              let lastEel = helloEle
              // 根据代码逻辑,heading 文件是导出一个生成元素的函数,文件更新时只要重新生成元素替换掉原来的元素即可,
              module.hot.accept('./heading', () => {
                // 根据操作,先生成一个新的元素,并记录
                const newEle = createHello()
                // 移除原来的元素
                // document.body.removeChild(lastEel)
                // 将原来元素的状态保存到新的元素中
                newEle2.innerHTML = lastEel.innerHTML
                // // 替换原来的元素
                document.body.replaceChild(newEle, lastEel)
                lastEel = newEle
              })
              
              // 图片热更新处理
              module.hot.accept('./images/loginBg.jpg', () => {
                document.body.style.background = `url(${bg})`
              })
            }
            
          2. 打包时需要关闭HMR,去除 HMR相关的插件,这样在运行webpack命令打包时,热更新的代码会被包裹在一个if(false){}的逻辑判断中,压缩代码时也会将这种无用的逻辑判断代码去除。
            在这里插入图片描述

  10. webapck不同环境下的配置

    1. 根据环境导出不同的配置:webapck.config.js还支持导出一个函数,函数接收两个参数,第一个参数为--env命令行参数的值,第二个参数为运行命令行的所有参数对象。

      module.exports = (env, argv) => {
        console.log(env, argv);
      }
      

      在这里插入图片描述

      module.exports = (env, argv) => {
      	const config = {
      	  mode: 'development',
      	  entry: './src/index.js',
      	  output: {
      	    filename: 'bundle.js',
      	    path: path.join(__dirname, 'output'),
      	    // publicPath: 'output/'
      	  },
      	  devtool: 'cheap-module-source-map',
      	  module: {
      	    rules: [
      	      {
      	        test: /\.css$/,
      	        use: [
      	          'style-loader',
      	          'css-loader'
      	        ]
      	      },
      	      {
      	        test: /\.jpg/,
      	        use: 'file-loader'
      	      }
      	    ]
      	  },
      	  plugins: [
      	    new HtmlWebpackPlugin({
      	      title: 'webpack-loader',
      	      meta: {
      	        viewport: 'widt=device-width'
      	      },
      	      template: './index.html'
      	    }),
      	  ]
      	}
      	if (env === 'production') { // 生产环境
      	  config.mode = env
      	  config.devtool = false
      	  config.plugins = [
      	    ...config.plugins,
      	    new CleanWebpackPlugin(),
      	    new CopyWebpackPlguin(['public'])
      	  ]
      	} else {
      	  config.devServer = {
      	    hot: true,
      	    // hotOnly: true,
      	    contentBase: 'public',
      	    proxy: {
      	      '/api': {
      	        // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user
      	        target: 'https://api.github.com',
      	        // 重写替换后的请求路径,将 './api' 部分替换为 ‘’
      	        pathRewrite: {
      	          '/api': ''
      	        },
      	        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
      	        changeOrigin: true,
      	      }
      	    }
      	  }
      	  config.plugins = [
      	    ...config.plugins,
      	    new webpack.HotModuleReplacementPlugin(),
      	  ]
      	}
      	return config
      }
      
    2. 不同环境对应不同的配置文件

      1. 首先,新建webpack.common.jswebpack.dev.jswebpack.prod.js分别存在公共配置、开发环境配置以及生产环境配置

      2. 分别将配置写入到三个配置文件中

      3. 引入webpack-merge,然后分别在webpack.dev.jswebpack.prod.js中使用webpack-merge提供的merge方法合并webpack.common.js中的配置。

        // webpack.common.js
        const path = require('path')
        const HtmlWebpackPlugin = require('html-webpack-plugin')
        
        module.exports = {
          mode: 'development',
          entry: './src/index.js',
          output: {
            filename: 'bundle.js',
            path: path.join(__dirname, '../output'),
            // publicPath: 'output/'
          },
          devtool: 'cheap-module-source-map',
          module: {
            rules: [
              {
                test: /\.css$/,
                use: [
                  'style-loader',
                  'css-loader'
                ]
              },
              {
                test: /\.jpg/,
                use: 'file-loader'
              }
            ]
          },
          plugins: [
            new HtmlWebpackPlugin({
              title: 'webpack-loader',
              meta: {
                viewport: 'widt=device-width'
              },
              template: './index.html'
            }),
          ]
        }
        //webpack.dev.js
        const { merge } = require('webpack-merge')
        const common = require('./webpack.common')
        const webpack = require('webpack')
        
        module.exports = merge(common, {
          devServer: {
            hot: true,
            // hotOnly: true,
            contentBase: 'public',
            proxy: {
              '/api': {
                // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user
                target: 'https://api.github.com',
                // 重写替换后的请求路径,将 './api' 部分替换为 ‘’
                pathRewrite: {
                  '/api': ''
                },
                // 不能使用 localhost:8080 作为请求 GitHub 的主机名
                changeOrigin: true,
              }
            }
          },
          plugins: [
            new webpack.HotModuleReplacementPlugin(),
          ]
        })
        //webpack.prod.js
        const { merge } = require('webpack-merge')
        const common = require('./webpack.common')
        const { CleanWebpackPlugin } = require('clean-webpack-plugin')
        const CopyWebpackPlguin = require('copy-webpack-plugin')
        
        module.exports = merge(common, {
          mode: 'production',
          devtool: false,
          plugins: [
            new CleanWebpackPlugin(),
            new CopyWebpackPlguin(['public'])
          ]
        })
        
      4. 运行命令行时需要通过--config命令行参数来指定配置文件
        在这里插入图片描述

  11. Webpack DefinePlugin:为代码注入全局成员

    plugins: [
      new webpack.DefinePlugin({
        TEST: 'test'
      })
    ]
    
    1. production模式下,会默认启动此插件并注入一个process.env.NODE_ENV=production的成员

    2. 传入插件的对象中的键值对中的值是一个js代码片段的字符串
      js中使用注入的全局变量在这里插入图片描述
      打包之后的代码,直接将值输出到代码中
      在这里插入图片描述
      修改全局变量的值如下
      在这里插入图片描述

      js中使用
      在这里插入图片描述
      打包之后的代码如下
      在这里插入图片描述

    3. 如果想要传入一个字符串,可以通过JSON.stringify()将字符串转换为一个js代码片段
      在这里插入图片描述
      在这里插入图片描述

  12. Tree Shaking

    1. Tree Shaking production模式下,会默认启用 Tree Shaking,移除未引用代码(dead-code)

    2. 在非 production 模式下启用 Tree Shaking

      module.exports = {
        entry: './src/index.js',
        mode: 'none',
        output: {
          filename: 'bundle.js',
          path: path.resolve(__dirname, 'dist')
        },
        optimization: { // 配置 webpack 内置优化功能
          usedExports: true, // 只导出外部使用的成员(标记未使用代码)
          concatenateModules: true, // 将所有模块合并到一个函数中, Scope Hoisting ,作用域提升
          minimize: true, // 开启代码压缩(移除未使用代码)
        }
      }
      
    3. 如果使用了 babel-loaderTree Shaking 将失效

      1. Tree Shaking 工作的前提条件是代码使用 ESM 规范

      2. babel-loader 早期版本是默认将代码转换成 CommonJS 规范。最新版本是支持 ESM 的,如果想要指定编译之后的模块化规范,可以通过@babel/preset-env的配置选项的modules来指定。

        const path = require('path')
        
        module.exports = {
          entry: './src/index.js',
          mode: 'none',
          output: {
            filename: 'bundle.js',
            path: path.resolve(__dirname, 'dist')
          },
          optimization: { // 配置 webpack 内置优化功能
            usedExports: true, // 只导出外部使用的成员(标记未使用代码)
            concatenateModules: true, // 将所有模块合并到一个函数中, Scope Hoisting ,作用域提升
            minimize: true, // 开启代码压缩(移除未使用代码)
          },
          module: {
            rules: [
              {
                test: /\.js$/,
                use: [
                  {
                    loader: 'babel-loader',
                    options: {
                      presets: [
                        ['@babel/preset-env', { modules: 'commonjs'}]
                      ]
                    }
                  }
                ]
              }
            ]
          }
        }
        

        按照上述配置打包之后的代码如下包含了未引用代码,说明没有启用Tree Shaking
        在这里插入图片描述
        去掉@babel/preset-envmodule配置之后打包生成的代码如下:移除了未引用代码,启用了Tree Shaking
        在这里插入图片描述

  13. 副作用SideEffects

    1. 副作用是模块除了导出成员之外所做的事情,比如在原型中添加方法或属性等,css文件都属于副作用模块。
    2. SideEffects功能的作用是移除项目中没有被用到且没有副作用的代码。
    3. 开启SideEffects
      1. 首先需要在webpack.config.js中配置optimization中的SideEffecttrue,表示开启SideEffect功能。
      2. 然后在package.json中配置 sideEffects,可以配置为一个boolean或文件路径一个字符串数组。
        1. boolean:表示项目中的所有代码都有/没有副作用,

        2. 数组:表示项目中指定的文件具有副作用,打包时不会移除这些文件

          "sideEffects": false,
          "sideEffects": [
             "*.css"
          ]
          
  14. 代码分割Code Splitting
    实现代码分割有两种方式:

    1. 多入口打包:适用于多页面应用,一个页面对应一个大包入口,公共部分则单独提取

      1. 需要将entry配置为一个对象,指定多个入口,同时利用HtmlWebpackPlugin生成多个html文件

        const path = require('path')
        const HtmlWebpackPlugin = require('html-webpack-plugin')
        
        module.exports = {
          entry: {
            index: './src/index.js',
            album: './src/album.js'
          },
          mode: 'none',
          output: {
            filename: '[name].bundle.js',
            path: path.resolve(__dirname, 'dist')
          },
          module: {
            rules: [
              {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
              }
            ]
          },
          plugins: [
            new HtmlWebpackPlugin({
              filename: 'index.html',
              template: './src/index.html'
            }),
            new HtmlWebpackPlugin({
              filename: 'album.html',
              template: './src/album.html'
            })
          ]
        }
        
      2. HtmlWebpackPlugin 默认会将打包生成的所有bundle.js引入到生成的html文件中,这就意味着除了引入需要的bundle.js,还会引入其他入口生成的bundle.js
        在这里插入图片描述

      3. 通过在 HtmlWebpackPlugin 插件中指定 chunks,来指定生成的html需要引入哪些bundle.js

        plugins: [
          new HtmlWebpackPlugin({
            filename: 'index.html',
            template: './src/index.html',
            chunks: ['index']
          }),
          new HtmlWebpackPlugin({
            filename: 'album.html',
            template: './src/album.html',
            chunks: ['album']
          })
        ]
        

        在这里插入图片描述

      4. 提取公共模块:提取多个入口的公共部分到一个单独的bundle.js中。
        提取公共模块功能的开启只需要在 optimization 属性中配置 splitChunks,然后在splitChunks中通过chunks指定提取哪些公共模块到一个bundle.js中。

        optimization: {
          splitChunks: {
            chunks: 'all', // all 表示将所有的公共模块提取到单独的bundle.js中,‘async’:按需加载的模块
          }
        },
        

        但是最新的css-loader需要将手动启用css模块化,才能成功提取公共模块。

        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    modules: true
                  }
                }
              ]
            }
          ]
        },
        
    2. 模块的动态导入

      1. 模块的动态导入只需要将原来代码中使用import关键字导入的模块改成使用import()函数导入,无需进行配置,webpack会自动处理代码分割。
      2. webpack默认以chunkId作为chunk名,如果想设置chunk名,可以import()方法参数使用行内注释webpackChunkName来指定
        import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts())
        })
        
        在这里插入图片描述
        不指定chunk名时的打包结果
        在这里插入图片描述
    3. css文件提取,将css代码提取到一个css文件中,适合于css代码体积较大的场景.使用MiniCssExtractPlugin插件。
      plugins中添加插件实例,将module-->rules中的style-loader修改为MiniCssExtractPlugin.loader,因为css代码不再是通过<style>标签注入,而是通过<link>标签引入。

      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const MiniCssExtractPlugin = require('mini-css-extract-plugin')
      
      module.exports = {
        mode: 'none',
        entry: {
          main: './src/index.js'
        },
        output: {
          filename: '[name].bundle.js'
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                // 'style-loader',
                MiniCssExtractPlugin.loader,
                'css-loader'
              ]
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          new HtmlWebpackPlugin({
            title: 'Dynamic import',
            template: './src/index.html',
            filename: 'index.html'
          }),
          new MiniCssExtractPlugin(),
        ]
      }
      

      生产模式下提取的css文件不会自动压缩,需要使用OptimizeCssAssetsWebpackPlugin插件来帮助压缩css文件。

  15. 文件名Hash

    1. 客户端会缓存服务器的文件,为尽可能的多的利用客户端的缓存文件,减少文件的请求次数,同时又能及时更新有变化的文件,我们需要在文件变化时改变文件名,让客户端重新请求变化之后的文件,同时对于没有发生改变的文件,不改变文件名,从而让客户端利用缓存中的文件。

    2. webpack支持三种形式的文件名Hash

      1. hash:项目级别的Hash,只要文件有修改,重新打包之后会重新生成Hash
      2. chunkhashchunk级别的Hash,文件修改,只会改变对应chunkHash
      3. contenthash:内容级别的Hash,文件修改,只会改变内容对应文件的 Hash
    3. 指定 Hash 长度。通过在 hash后面跟上 :num 来指定生成 Hash 长度。

      output: {
       filename: '[name]-[contenthash:6].bundle.js'
      },
      

      在这里插入图片描述

Rollup的使用

  1. Rollup是一款ESM打包器,充分利用ESM特性的高效打包器

  2. Rollup的简单使用
    安装之后,使用yarn rollup entryPath命令行来运行rollup打包,然后可以通过--file来指定输出文件,--format指定输出的代码格式。rollup默认会启动Tree Shaking优化打包之后的结果,移除未引用代码。
    在这里插入图片描述

  3. Rollup 配置文件

    1. 配置文件rollup.config.js是一个node模块,但是rollup会对配置文件进行处理,因此可以直接使用ESMrollup.config.js需要默认导出一个对象。
      export default {
        input: './src/index.js', // 入口文件
        output: { // 输出文件配置
          file: 'dist/main.js', // 输出文件位置
          format: 'iife', // 输出文件格式
        }
      }
      
    2. Rollup 默认不会使用配置文件,需要通过 --config [file] 指定配置文件,如果不指定file将默认使用根目录下的rollup.config.js文件
      在这里插入图片描述
  4. Rollup使用插件

    1. Rollup 自身功能只是ESM模块的合并打包,其他功能需要通过插件去扩展,插件是Rollup唯一的扩展途径。
    2. 使用插件,以rollup-plugin-json(让我们可以导入json文件)为例。安装插件之后,直接在配置文件中引入,并在plugins中配置
      import json from 'rollup-plugin-json'
      
      export default {
        input: './src/index.js', // 入口文件
        output: { // 输出文件配置
          file: 'dist/main.js', // 输出文件位置
          format: 'iife', // 输出文件格式
        },
        plugins: [
          json()
        ]
      }
      
    3. Rollup默认只能按照文件路径的方式加载本地模块,对于node_modules中的模块并不能和webpack一样直接通过名称导入。为了能和webpack一样直接通过模块名导入第三方模块,需要使用 rollup-plugin-node-resolve插件,使用方法同上。注意 rollup 默认只能处理 ESM 模块,所以如果不添加特殊处理,第三方模块也必须是 ESM 模块。
    4. 加载CommonJs模块,需要添加 rollup-plugin-commonjs 插件,使用方法同上。
  5. Rollup代码拆分

    1. 使用 import()函数动态导入,Rollup会自动拆分代码。但是输出代码的格式不能时 iife,因为自执行函数不支持代码拆分,所有代码包裹在一个函数中,无法进行代码拆分。浏览器环境我们只能将代码格式指定为amd。同时代码拆分会生成多个文件,所以输出文件不能通过file指定,file只能指定单个文件。需要通过dir指定一个输出目录
      import json from 'rollup-plugin-json'
      
      export default {
        input: './src/index.js', // 入口文件
        output: { // 输出文件配置
          dir: 'dist',
          format: 'amd'
        },
        plugins: [
          json()
        ]
      }
      
  6. 多入口代码
    多入口打包只需要将input修改为一个数组或对象即可。rollup在多入口打包时会自动使用代码拆分,所以format不能是iife,需要指定为amd,但是amd格式的js文件不能直接引入到页面,需要通过实现AMD标准的库(如require)加载。加载时使用<script>标签引入require.js,并通过 data-main指定requirejs加载模块的入口路径。

    import json from 'rollup-plugin-json'
    
    export default {
      input: ['./src/index.js', './src/message.js'], // 入口文件
      output: { // 输出文件配置
        dir: 'dist',
        format: 'amd'
      },
      plugins: [
        json()
      ]
    }
    

    在这里插入图片描述

  7. 优缺点:

    1. 优点:
      1. 输出结果更加偏平
      2. 自动移除未引用代码
      3. 打包结果直接可读
    2. 缺点:
      1. 加载非 ESM 的第三方模块比较复杂
      2. 模块最终都被打包到一个函数中,无法实现 HMR
      3. 浏览器环境中,代码拆分依赖AMD库,需要依赖AMD库加载amd格式的js

Parcel的使用
Parcel是一款零配置的前端应用打包工具。

  1. parcel的使用

    1. 安装parcel-bundler插件, 运行命令yarn parcel entryFile即可。
      在这里插入图片描述

    2. parcel推荐使用.html文件为入口文件,因为浏览器也是使用.html文件作为入口。通过script标签引入入口的js,然后根据模块导入来构建依赖。

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>parcel study</title>
      </head>
      <body>
        <script src="./main.js"></script>
      </body>
      </html>
      
    3. 运行parcel命令之后会自动构建打包,并启动一个本地http server,文件修改时会自动重新构建代码并刷新浏览器。

    4. parcel 也支持手动处理热更新

      import foo from './foo'
      // import $ from 'jquery'
      import './main.css'
      import logo from './logo.png'
      
      foo.bar()
      
      import('jquery').then($ => {
        $(document.body).append('<h1>Hello Parcel</h1>')
        $(document.body).append(`<img src=${logo} alt="" />`)
      })
      
      if(module.hot) {
        module.hot.accept(() => {
          console.log('hmr');
        })
      }
      
  2. parcel的特点

    1. 完全零配置
    2. 自动处理各种类型的文件资源的加载
    3. 引入第三方模块时会自动安装依赖,无需手动安装
    4. 相同体量,打包速度比webpack更快,因为内部使用多线程进行打包。虽然webpack可以使用happypack插件来加速打包。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值