Webpack

模块打包工具的由来

引入模块化会产生的问题:

  • ES Modules存在环境兼容问题
  • 模块文件过多,网络请求频繁
  • 所有的前端资源都需要模块化
    由此就需要模块打包工具
  • 编译代码。将开发阶段编写的包含新特性的代码转换为能够兼容绝大多数浏览器环境的代码、
  • 将散落的模块文件打包到一起
  • 支持不同类型的前端资源类型,可以构成对于整个前端应用的模块化方案
模块化工具概要

Webpack

  • 将零散的模块代码打包到同一个js文件当中
  • 将需要编译转换的代码通过模块加载器(Loader)进行编译转换
  • 代码拆分,将应用当中的代码按照我们的需要去打包(实现渐进式加载)
  • 支持以模块化的方式去载入任意类型的文件
    打包工具解决了前端整体的模块化(并不只是js的模块化)
Webpack
  • 安装webpack的核心模块和对应的cli模块npm i webpack webpack-cli -D

  • webpack默认将src/index.js打包到dist/main.js,也可以通过webpack.config.js去配置

  • webpack.config.js

  • 可以通过cli参数去指定打包的模式,

    • 默认production(自动启动优化,优化打包结果)
    • development 开发模式npm run webpack --mode development
      在开发模式下回自动优化打包的速度,添加一些调试的辅助在代码当中
    • none npm run webpack --mode none 运行最原始状态的打包,不会做额外的处理
      也可以直接在配置文件中配置
      webpack.config.js
  • 资源模块加载
    Webpack内部默认只会去处理JavaScript文件,如果要处理其他类型的文件,就要配置相应的加载器(loader),通过实现不同类型的loader,就可以实现加载不同类型的资源。
    如:需处理css文件,则安装cssloadernpm i css-loader style-loader -D,在配置文件中添加对应的配置
    webpack.config.js
    就目前而言,JavaScript驱动整个前端应用的业务,所以正确的做法还是将js文件作为打包的入口,然后再js代码当中通过import的方式去引入css文件。这样做的优势:

    • 逻辑合理,JS缺失需要这些资源文件的配合才能实现对应的功能
    • 保证上线资源不缺失,且都是必要的
  • 文件资源加载器

    • 安装 file-loader
    • 将导入的文件拷贝到输出的目录,再讲输入的路径作为当前模块的返回值返回
    • webpack.config.js
  • URL加载器
    除了用file-loader通过拷贝物理文件的形式去处理文件以外,还可以通过Data URLs的形式去表示文件
    Data URLs是一种特殊的url协议,可以用来直接去表示一个文件,和传统的url请求服务器资源不一样,Data urls是url就可以直接去表示文件内容的一种方式,也就是说这种url当中的文本就已经包含了文件的内容,在使用这种url的时候就不会去发送http请求
    安装url-loader,然后再配置文件中配置,此时,webpack遇到图片文件就会将其转换为Data URLs的形式
    这种方式比较适合体积比较小的资源,因为体积较大的资源会造成打包文件过大影响运行速度。
    最佳的实践方法:
    1. 小文件使用Data URLs,减少请求次数
    2. 大文件单独提取存放,提高加载速度
    webpack.config.js

  • 常用的资源加载器

    • 编译转换类,如css-loader
    • 文件操作类,如file-loader
    • 代码检查类,如eslink-loader
  • Webpack 与ES 2015
    因为模块打包需要,所以处理import和export,但并不能处理代码当中其他的es6特性,所以需要为js文件配置额外的编译型loader,例如babel-loader
    安装 npm i babel-loader @babel/core @babel/preset-env -D

    {
      test: /.js$/,
       use: {
         loader: 'babel-loader', // babel只是一个转换js代码的平台,我们需要基于babel这个平台通过不同的插件转换代码当中具体的特性
         options: {
           presets: ['@babel/preset-env'] // preset-env插件集合
         }
       }
     },
    
    • Webpack只是打包工具
    • 加载器可以用来编译转换代码
  • Webpack加载资源方式

    1. 遵循ES Modules标准的import声明

      // // 1. 支持 ES Modules 的 import 声明
      // 
      
      import createHeading from './heading.js'
      import better from './better.png'
      import './main.css'
      
      const heading = createHeading()
      const img = new Image()
      img.src = better
      document.body.append(heading)
      document.body.append(img)
      
    2. 遵循CommonJS标准的require函数

      // // 2. 支持 CommonJS 的 require 函数
      // 
      
      const createHeading = require('./heading.js').default
      const better = require('./better.png')
      require('./main.css')
      
      const heading = createHeading()
      const img = new Image()
      img.src = better
      document.body.append(heading)
      document.body.append(img)
      
    3. 遵循AMD标准的define函数和require函数

      // // 3. 支持 AMD 的 require / define 函数
      // 
      
      define(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
        const heading = createHeading.default()
        const img = new Image()
        img.src = better
        document.body.append(heading)
        document.body.append(img)
      })
      
      require(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
        const heading = createHeading.default()
        const img = new Image()
        img.src = better
        document.body.append(heading)
        document.body.append(img)
      })
      
    4. Loader加载的非JavaScript也会触发资源加载,例如:样式代码中的@import指令和部分属性中的url函数,html代码中图片标签的src属性

      import './main.css'
      
      import footerHtml from './footer.html' // html文件默认会将html代码作为字符串去导出,所以需要用变量去接收导出的内容
      
      document.write(footerHtml)
      

      webpack.config.js

  • Webpack核心工作原理
    webpack会根据配置,找到其中的一个文件作为打包的入口,一般情况下都是会一个js文件,然后顺着入口文件中的代码,根据代码中出现的import或者require之类的语句解析推断出这个文件所依赖的资源模块,然后分别去解析每个资源模块对应的依赖,最后形成了整个项目中所有用到资源文件的依赖关系的依赖树,有了依赖树之后,webpack会递归依赖树找到每个节点对应的资源文件,最后根据配置文件当中的rules属性找到这个模块中对应的加载器,然后交给加载器去加载对应的模块,最后将加载过后的结果放入打包结果(bundle.js)当中,从而去实现整个项目的打包。
    loader机制是Webpack的核心

  • Webpack开发一个loader
    webpack打包的时候就是把loader加载过后的结果,拼接到模块当中,所以loader管道必须要返回js代码
    loader工作原理

    • Loader负责资源文件从输入到输出的转换

    • 对于同一个资源可与一次使用多个Loader
      markdown-loader.js

      const marked = require('marked')
      module.exports = source => {
        // console.log(source)
        // return 'console.log("hello ~")'
        const html = marked(source) // 需要将html变成一段js代码,所以可以将html作为当前模块导出的字符串
        // return `module.exports = ${JSON.stringify(html)}` // 这里通过JSON.stringify转义内部的引号和换行符
        // return `export default ${JSON.stringify(html)}` // 这里通过JSON.stringify转义内部的引号和换行符
        return html; // 返回 html 字符串交给下一个 loader 处理
      }
      

      webpack.config.js

      const path = require('path')
      
      module.exports = {
        mode: 'none',
        entry: './src/main.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'dist'),
          publicPath: 'dist/'
        },
        module: {
          rules: [
            {
              test: /.md$/,
              use: [
                'html-loader',
                './markdown-loader'
              ]
            }
          ]
        }
      }
      

      main.js

      import about from './about.md'
      console.log(about)
      

      about.md

      ### hahh
      

      最终生成的文件
      bundle.js

  • webpack插件机制

    • 增强Webpack自动化能力
    • Loader专注实现资源模块加载,从而实现整体项目的打包,Plugin解决除了资源加载外其他自动化工作,例如:清除dist目录,拷贝静态文件到输出目录,压缩输出代码。
  • 自动清理输出目录的插件

    • 安装clean-webpack-plugin插件
    • 在webpack.config.js中引入插件 const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  • 自动生成html插件 html-webpack-plugin

  • 将不需要编译的静态文件复制到输入目录中 copy-webpack-plugin

    const path = require('path')
    const webpack = require('webpack')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin') // 自动清除目录插件
    const HtmlWebpackPlugin = require('html-webpack-plugin') // 自动生成最初访问的HTML入口文件,可以自动将打包后的文件添加到入口文件中,不需要手动维护
    const CopyWebpackPlugin = require('copy-webpack-plugin') // 将不需要编译的静态文件复制到输入目录中
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
      },
      module: {
        rules: [
          {
            test: /.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          },
          {
            test: /.png$/,
            use: {
              loader: 'url-loader',
              options: {
                limit: 10 * 1024 // 10 KB
              }
            }
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        // 用于生成 index.html
        new HtmlWebpackPlugin({
          title: 'Webpack Plugin Sample', // 页面标题
          meta: { // 元素标签
            viewport: 'width=device-width'
          },
          template: './src/index.html' // 模版文件
        }),
        // 用于生成 about.html
        new HtmlWebpackPlugin({ // 可以输入多个html文件
          filename: 'about.html'
        }),
      	new CopyWebpackPlugin(
          {
            patterns: [
              { from: "public", to: "dest" },
            ],
          }
        ),
      ]
    }
    
    

    html的模板文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Webpack</title>
    </head>
    <body>
      <div class="container">
        <!-- htmlWebpackPlugin内部提供的变量 -->
        <h1><%= htmlWebpackPlugin.options.title %></h1>
      </div>
    </body>
    </html>
    
  • Webpack开发一个插件
    相比于loader,plugin拥有更宽的能力范围。loader只是在加载模块的环节去工作,而plugin的工作范围可以触及到webpack工作的每一个环节。plugin通过钩子机制实现,钩子机制:类似于web开发中的事件,在webpack工作的过程中会有很多的环节,为了便于插件的扩展,webpack几乎给每一个环节都埋下了一个钩子,这样的话,我们在开发插件的时候就可以在每一个节点上去挂载不同的任务,从而扩展webpack的能力。webpack要求插件必须是一个函数或者是一个包含apply方法的对象,一般我们会把插件定义为一个类型,然后再类型当中去定义一个apply方法,然后使用的时候通过类型去构建实例。
    插件是通过往webpack生命周期的钩子中挂载函数实现扩展。

    const path = require('path')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    // 需求:清除打包的js当中没有必要的注释
    class MyPlugin {
      apply (compiler) { // apply方法在webpack启动时自动被调用,compiler是一个对象,对象中包含了此次构建的所有配置信息
        console.log('MyPlugin 启动')
        // 通过compiler注册钩子函数,emit钩子是webpack即将要往输出文件输出时执行。tap注册钩子函数(第一个参数:插件名称,第二个参数:挂载到钩子上的函数)
        compiler.hooks.emit.tap('MyPlugin', compilation => {
          // compilation => 可以理解为此次打包的上下文 compilation.assets即将写入文件当中的资源信息
          for (const name in compilation.assets) {
            // console.log(name) // 每个文件的名称
            // console.log(compilation.assets[name].source()) // 每个资源文件的内容
            if (name.endsWith('.js')) { // 判断是否是js文件(这里只处理js文件)
              const contents = compilation.assets[name].source()
              const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
              compilation.assets[name] = {
                source: () => withoutComments, // 将转换结果覆盖到原有内容当中
                size: () => withoutComments.length // size webpack内部要求的必须的方法
              }
            }
          }
        })
      }
    }
    
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
      },
      module: {
        rules: [
          {
            test: /.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          },
          {
            test: /.png$/,
            use: {
              loader: 'url-loader',
              options: {
                limit: 10 * 1024 // 10 KB
              }
            }
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        // 用于生成 index.html
        new HtmlWebpackPlugin({
          title: 'Webpack Plugin Sample',
          meta: {
            viewport: 'width=device-width'
          },
          template: './src/index.html'
        }),
        // 用于生成 about.html
        new HtmlWebpackPlugin({
          filename: 'about.html'
        }),
        new CopyWebpackPlugin(
          {
            patterns: [
              { from: "public", to: "dest" },
            ],
          }
        ),
        new MyPlugin()
      ]
    }
    
    
增强webpack开发体验
  • 自动编译
    watch工作模式:监听文件变化,自动重新打包 webpack --watch

  • 自动刷新浏览器

    1. BrowserSync监听dist目录下文件变化,自动刷新浏览器
      缺点:操作麻烦(需要同时使用两个工具BrowserSync和watch)效率降低(过程当中会涉及webpack磁盘的写入频繁的操作)
    2. Webpack Dev Server是webpack官方推出的开发工具,提供了一个开发服务器HTTP Server,集成【自动编译】和【自动刷新浏览器】等功能
    • 安装webpack-dev-server
    • 配置到package.json中的srcipts下
       "scripts": {
          "build": "webpack",
          "server": "webpack-dev-server"
        },
      
    • npm run server就可以监听文件变化,同时更新浏览器。注:webpack-dev-server并不会讲打包结果写入dist目录中,而是暂时存放在内存当中,内部的http-server就是在内存当中把这些配置读出来,然后发送给浏览器。这样就可以减少很多磁盘不必要的读写操作,从而提高构建效率
    • webpack-dev-server --open 可以自动唤醒浏览器打开运行地址
    • webpack-dev-server默认会将构建结果输入的文件作为开发服务器的资源文件,所以只要是通过webpack打包能够输出的文件都可以被直接访问。但如果其他静态资源文件也需要被开发服务器访问,就需要在配置文件中配置devServer下的contentBase属性。
    • 在开发环境中有时访问接口地址会产生跨域请求问题。、
      1. 可以使API支持跨域资源共享(CORS)
      2. webpack Dev Server支持配置代理
      const path = require('path')
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const CopyWebpackPlugin = require('copy-webpack-plugin')
      
      module.exports = {
        entry: './src/main.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'dist')
        },
        devServer: {
          contentBase: './public', // contentBase指定额外的静态资源路径,可以是字符串也可以是数组,开发阶段最好不要使用copy-webpack-plugin
          proxy: { // 添加代理服务配置,其中的每一个属性就是一个代理规则的配置
            '/api': { // 属性的名称就是需要被代理的请求路径前缀
              // http://localhost:8080/api/users -> https://api.github.com/api/users
              target: 'https://api.github.com', // 代理目标
              // http://localhost:8080/api/users -> https://api.github.com/users
              pathRewrite: { // 代理路径重写
                '^/api': '' // 以正则的方式替换路径
              },
              // 不能使用 localhost:8080 作为请求 GitHub 的主机名
              // 默认代理服务器会以实际在浏览器当中请求的主机名作为代理请求的主机名
              // changeOrigin=true的情况下,就会以实际代理请求的主机名去请求
              changeOrigin: true 
            }
          }
        },
        module: {
          rules: [
            {
              test: /.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            },
            {
              test: /.png$/,
              use: {
                loader: 'url-loader',
                options: {
                  limit: 10 * 1024 // 10 KB
                }
              }
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          // 用于生成 index.html
          new HtmlWebpackPlugin({
            title: 'Webpack Tutorials',
            meta: {
              viewport: 'width=device-width'
            },
            template: './src/index.html'
          }),
          // // 开发阶段最好不要使用这个插件
          // new CopyWebpackPlugin(['public'])
        ]
      }
      
  • Source Map
    源代码地图,为了映射源代码和转换之后的代码。解决了源代码与运行代码不一致所产生调试的问题
    map文件中

    • “version”: 3, // 使用的sourcemap标准的版本
    • “sources”: [“jquery-3.4.1.js”], // 转换之前源文件的名称(有可能是多个文件合并转换成一个文件,所以这里是一个数组)
    • “names”: [ // 源代码当中使用的成员名称
    • mappings是base64-vlq编码字符串,记录转换之后的字符与转换之前的映射关系,通过添加注释的方式引入sourcemap文件
    • 编译后文件中的注释: //# sourceMappingURL=jquery-3.4.1.min.map
  • Webpack中配置Source Map devtool: 'source-map', // devtool开发过程中的辅助工具

  • Webpack eval模式的Source Map
    eval是js当中的函数,可以运行字符串中的js代码。默认情况下eval中的代码会运行在临时的虚拟机当中
    sourcemap
    上图说明我们可以通过sourceURL改变eval所属环境的名称,其实那段代码还是运行在虚拟机当中,只是sourceURL告诉执行引擎这段代码所属的文件路径

    devtool: 'eval'
    

    在eval模式下,webpack会将打包过后的代码都放到eval函数中去执行,并且在eval函数执行的字符串最后,通过sourceURL的方式去说明对应的文件路径,这种模式只能定位文件,不会生成sourcemap文件,所以构建速度是最快的。

    • webpack devtool模式对比
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    const allModes = [
    	'eval',
    	'cheap-eval-source-map', // 定位的源代码是经过es6转换过后的结果(不带有module是经过加工的代码)
    	'cheap-module-eval-source-map',
    	'eval-source-map', // 这种模式下相比于eval,他生成了sourcemap文件,可以定位到出错的具体位置
    	'cheap-source-map', // 阉割版的sourcemap,只会定位到具体的行,没有定位到具体的列,所以生成速度会快一点(没有eval就是没有用eval的方式去处理模块代码,没有module就是定位到的代码是loader处理过后的代码)
    	'cheap-module-source-map', // 定位的源代码和我们编写的源代码是一样的(带有module是没有经过加工的代码)
    	'inline-cheap-source-map', // 
    	'inline-cheap-module-source-map',
    	'source-map',
    	'inline-source-map', // 使用data url的方式去将sourcemap嵌入到代码当中,会导致sourcemap体积变大很多
    	'hidden-source-map', // 在开发工具中看不到sourcemap的效果,但是生成了sourcemap文件,在代码中没有通过注释引入文件,在开发第三方包的时候会使用到。当使用到开发包出现问题,可以通过加sourcemap的方式引入回来
    	'nosources-source-map' // 能看到错误出现的位置,但是点击进去看不到源代码(没有源代码,但可以提供行列信息)可以在生产环境当中保护源代码,避免暴露
    ]
    // webpack可以在一次打包过程中同时执行多个打包任务
    module.exports = allModes.map(item => {
    	return {
    		devtool: item,
    		mode: 'none',
    		entry: './src/main.js',
    		output: {
    			filename: `js/${item}.js`
    		},
    		module: {
    			rules: [
    				{
    					test: /\.js$/,
    					use: {
    						loader: 'babel-loader',
    						options: {
    							presets: ['@babel/preset-env']
    						}
    					}
    				}
    			]
    		},
    		plugins: [
    			new HtmlWebpackPlugin({
    				filename: `${item}.html`
    			})
    		]
    	}
    })
    
  • webpack选择Source Map模式
    开发模式:cheap-module-eval-source-map

    1. 代码经过loader转换过后的差异较大
    2. 虽然首次打包速度慢,重写打包相对较快
      生产模式:none 不生成sourcemap 或nosources-source-map 避免Source Map暴露源代码
  • webpack HMR体验
    HMR模块热更新,在应用程序运行的过程中实时替换某个模块,应用状态不受影响,HMR已经集成在webpack-dev-server中,不需要单独安装,

    • 可以通过webpack-dev-server --hot去开启

    • webpack中的HMR并不可以开箱即用,需要手动处理模块热替换逻辑
      main.js

      import createEditor from './editor'
      import background from './better.png'
      import './global.css'
      
      const editor = createEditor()
      document.body.appendChild(editor)
      
      const img = new Image()
      img.src = background
      document.body.appendChild(img)
      
      // ============ 以下用于处理 HMR,与业务代码无关 ============
      
      // console.log(createEditor)
      // 先去判断是否存在这个对象,避免没有开启HMR导致报错
      if (module.hot) {
        let lastEditor = editor
        // hot是hmr api的核心对象 accept用于注册当某个模块更新之后的处理函数
        module.hot.accept('./editor', () => {
          // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
          // console.log(createEditor)
      
          const value = lastEditor.innerHTML // 存储之前编辑器中的内容
          document.body.removeChild(lastEditor)
          const newEditor = createEditor()
          newEditor.innerHTML = value
          document.body.appendChild(newEditor)
          lastEditor = newEditor
        })
      // 图片热替换
        module.hot.accept('./better.png', () => {
          img.src = background
          console.log(background)
        })
      }
      
      

      注意事项:

      1. hotOnly可以解决热替换失败,自动回退使用自动刷新,而hotOnly不会使用自动刷新
         devServer: {
            // hot: true
            hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
          },
        
      2. 代码当中手动处理热替换的逻辑会在打包后自动移除,所以不用担心代码中会有与业务无关的逻辑代码
  • webpack 生产环境优化

    1. 配置文件根据环境不同导出不同的配置(只适用于中小型项目)
      const webpack = require('webpack')
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const CopyWebpackPlugin = require('copy-webpack-plugin')
      
      // webpack支持导出一个函数,在这个函数当中返回需要配置的对象,接收两个参数,env:通过cli传递的环境名参数,运行cli所传递的所有参数
      module.exports = (env, argv) => {
        const config = {
          mode: 'development',
          entry: './src/main.js',
          output: {
            filename: 'js/bundle.js'
          },
          devtool: 'cheap-eval-module-source-map',
          devServer: {
            hot: true,
            contentBase: 'public'
          },
          module: {
            rules: [
              {
                test: /\.css$/,
                use: [
                  'style-loader',
                  'css-loader'
                ]
              },
              {
                test: /\.(png|jpe?g|gif)$/,
                use: {
                  loader: 'file-loader',
                  options: {
                    outputPath: 'img',
                    name: '[name].[ext]'
                  }
                }
              }
            ]
          },
          plugins: [
            new HtmlWebpackPlugin({
              title: 'Webpack Tutorial',
              template: './src/index.html'
            }),
            new webpack.HotModuleReplacementPlugin()
          ]
        }
      
        if (env === 'production') {
          config.mode = 'production'
          config.devtool = false
          config.plugins = [
            ...config.plugins,
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public'])
          ]
        }
      
        return config
      }
      
    2. 一个环境对应一个配置文件(适用于大型项目)
      webpack.common.js
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      
      module.exports = {
        entry: './src/main.js',
        output: {
          filename: 'js/bundle.js'
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            },
            {
              test: /\.(png|jpe?g|gif)$/,
              use: {
                loader: 'file-loader',
                options: {
                  outputPath: 'img',
                  name: '[name].[ext]'
                }
              }
            }
          ]
        },
        plugins: [
          new HtmlWebpackPlugin({
            title: 'Webpack Tutorial',
            template: './src/index.html'
          })
        ]
      }
      
      
      webpack.dev.js
      const webpack = require('webpack')
      const merge = require('webpack-merge')
      const common = require('./webpack.common')
      
      // merge用来合并webpack配置
      module.exports = merge(common, {
        mode: 'development',
        devtool: 'cheap-eval-module-source-map',
        devServer: {
          hot: true,
          contentBase: 'public'
        },
        plugins: [
          new webpack.HotModuleReplacementPlugin()
        ]
      })
      
      
      webpack.prod.js
      const merge = require('webpack-merge')
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const CopyWebpackPlugin = require('copy-webpack-plugin')
      const common = require('./webpack.common')
      
      module.exports = merge(common, {
        mode: 'production',
        plugins: [
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin(['public'])
        ]
      })
      
      
  • webpack优化配置

    • DefinePlugin 为代码注入全局成员,可以用它去注入一些可能会发生变化的值
      默认启用并且往代码中注入process.env.NODE_ENV,可以通过这个成员去判断当前的运行环境
      const webpack = require('webpack')
      
      module.exports = {
        mode: 'none',
        entry: './src/main.js',
        output: {
          filename: 'bundle.js'
        },
        plugins: [
          // 构造函数接收一个对象,每一个对象的键值都会被注入到代码当中
          new webpack.DefinePlugin({
            // DefinePlugin其实是把注入成员的值直接替换到代码当中,所以值要求的是一个代码片段
            API_BASE_URL: JSON.stringify('https://api.example.com') // 利用JSON.stringify将值转换成表示这个值的代码片段
          })
        ]
      }
      
      
    • Tree Shaking 自动检测出未引用的代码,且剔除掉他们。这个功能会在生产模式下自动开启
      注意:
    1. Tree Shaking并不是webpack某一个配置选项,他是一组功能搭配使用后的优化效果
    2. Tree Shaking的实现前提是必须使用ES Modules去组织代码
      module.exports = {
        mode: 'none',
        entry: './src/index.js',
        output: {
          filename: 'bundle.js'
        },
        module: {
          rules: [
            {
              test: /\.js$/,
              use: {
                loader: 'babel-loader',
                options: {
                  presets: [
                    // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
                    // ['@babel/preset-env', { modules: 'commonjs' }] // 这样Tree Shaking就无法正常工作了
                    // ['@babel/preset-env', { modules: false }]
                    // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
                    ['@babel/preset-env', { modules: 'auto' }]
                  ]
                }
              }
            }
          ]
        },
        optimization: { // 集中去配置webpack内部的优化功能
          // 模块只导出被使用的成员(标识没有引用的成员)
          usedExports: true,
          // 尽可能合并每一个模块到一个函数中,既提升了运行效率,又减少了代码的体积
          // concatenateModules: true,
          // 压缩输出结果(剔除没有引用的成员)
          // minimize: true
        }
      }
      
      
  • sideEffects 通过配置的方式去标识代码是否有副作用,从而为tree shaking提供更大的压缩空间

    • 副作用:模块执行的时候除了导出成员之外做所的事情、
      如这样一段代码,没有导出任何成员,但却是实际的作用
      extend.js

    • sideEffects一般用于npm包编辑是否有副作用

    • 在生产模式下也会自动开启

    • 如果不是生产模式,需要在两个地方配置
      webpack.config.js

      module.exports = {
        mode: 'none',
        entry: './src/index.js',
        output: {
          filename: 'bundle.js'
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            }
          ]
        },
        optimization: {
          sideEffects: true, // 开启这个特性后,webpack在打包时会先检查当前代码所属的package.json中有没有sideEffects标识,以此来判断这个模块是否有副作用,如果没有副作用,那么没有用到的模块就不会被打包
          // 模块只导出被使用的成员
          // usedExports: true,
          // 尽可能合并每一个模块到一个函数中
          // concatenateModules: true,
          // 压缩输出结果
          // minimize: true,
        }
      }
      
      

      package.json

      // "sideEffects": false, // 标识代码没有副作用
        "sideEffects": [  // 里面的文件不会被标识没有副作用
          "./src/extend.js",
          "*.css"
        ]
      
  • webpack代码分割
    并不是每个模块在启动时都是必要的,分包,按需加载可以提高应用模块的响应速度和运行效率,目前webpack分包的方式主要有两种

    • 多入口打包
      一般适用于传统的多页应用程序,一个页面对应一个打包入口,公共部分单独提取
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      
      module.exports = {
        mode: 'none',
        entry: { // 如果需要配置多个打包入口的话可以配置成一个对象
          index: './src/index.js',
          album: './src/album.js'
        },
        output: {
          filename: '[name].bundle.js' // [name]可以通过这种形式动态输出文件名,name最终会被替换为入口的名称
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          new HtmlWebpackPlugin({ // 默认会输出一个注入所有打包结果的html文件,如果不想注入所有打包结果可以通过chunks配置
            title: 'Multi Entry',
            template: './src/index.html',
            filename: 'index.html',
            chunks: ['index'] // chunks指定输出的html所使用的依赖模块
          }),
          new HtmlWebpackPlugin({
            title: 'Multi Entry',
            template: './src/album.html',
            filename: 'album.html',
            chunks: ['album']
          })
        ]
      }
      
      
      提取公共模块
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      
      module.exports = {
        mode: 'none',
        entry: {
          index: './src/index.js',
          album: './src/album.js'
        },
        output: {
          filename: '[name].bundle.js'
        },
        optimization: { // 优化属性
          splitChunks: {
            // 自动提取所有公共模块到单独 bundle
            chunks: 'all' // all表示把所有的公共模块都提取到单独的bundle中
          }
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                'style-loader',
                'css-loader'
              ]
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          new HtmlWebpackPlugin({
            title: 'Multi Entry',
            template: './src/index.html',
            filename: 'index.html',
            chunks: ['index']
          }),
          new HtmlWebpackPlugin({
            title: 'Multi Entry',
            template: './src/album.html',
            filename: 'album.html',
            chunks: ['album']
          })
        ]
      }
      
      
    • 动态导入
      动态导入的模块会被自动分包
      // import posts from './posts/posts'
      // import album from './album/album'
      
      const render = () => {
        const hash = window.location.hash || '#posts'
      
        const mainElement = document.querySelector('.main')
      
        mainElement.innerHTML = ''
      
        if (hash === '#posts') {
          // mainElement.appendChild(posts())
          // 默认通过动态导入产生的bundle文件,他的文件名称只是一个序号
          // 如果需要给这些文件命名的话,可以使用webpack提供的魔法注释去实现
          // 具体的使用方法就是在调用import的位置使用行内注释,特定的格式webpackChunkName:+名称/* webpackChunkName: 'components' */ 
          // 如果chunkname是相同的话,相同的chunkname最终会被打包到一起
          import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
            mainElement.appendChild(posts())
          })
        } else if (hash === '#album') {
          // mainElement.appendChild(album())
          import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
            mainElement.appendChild(album())
          })
        }
      }
      
      render()
      
      window.addEventListener('hashchange', render)
      
      
  • MiniCssExtractPlugin提取css到单个文件,从而实现css模块的按需加载
    注:如果样式文件不是很大的话,不要使用这种方式提取到单个文件当中,这样会增加一次网络请求

    1. 安装插件mini-css-extract-plugin
    2. 安装插件optimize-css-assets-webpack-plugin用于压缩css代码,因为之前css文件是输出到js文件当中的,但使用mini-css-extract-plugin后是通过link标签注入的。而webpack内置的压缩机制只会压缩js文件,并不会压缩css文件,所以需要安装专门的css压缩插件压缩css代码
    3. 安装terser-webpack-plugin,webpack内置
    4. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const MiniCssExtractPlugin = require('mini-css-extract-plugin')
      const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
      const TerserWebpackPlugin = require('terser-webpack-plugin')
      
      module.exports = {
        mode: 'none',
        entry: {
          main: './src/index.js'
        },
        output: {
          filename: '[name].bundle.js'
        },
        optimization: {
          minimizer: [ // 如果配置了这个数组,webpack就会认为我们要自定义压缩插件,导致内部的压缩器被覆盖掉,原本的js压缩也不会工作
            new TerserWebpackPlugin(), // 所以这里要手动的添加过来js压缩插件
            new OptimizeCssAssetsWebpackPlugin() // 像这种压缩插件应该添加在minimizer中,以便统一控制
            // 这样如果没有开启minimizer压缩功能的话,他就不会工作
          ]
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                // 'style-loader', // 将样式通过 style 标签注入
                MiniCssExtractPlugin.loader, // 将样式文件通过link标签的方式去注入
                'css-loader'
              ]
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          new HtmlWebpackPlugin({
            title: 'Dynamic import',
            template: './src/index.html',
            filename: 'index.html'
          }),
          new MiniCssExtractPlugin()
        ]
      }
      
      
  • webpack输出文件名 Hash
    一般在部署前端的资源文件时,都会启用服务器的静态资源缓存,这样可以提升应用的响应速度,但过期时间设置过短会导致缓存作用不明显,过长会导致资源文件修改后不会马上更新。
    因此,在生产模式下,需要给输出的文件名使用Hash,这样一旦输出的文件发生变化,文件名称也会相应的改变。对于客户端而言全新的文件名就是全新的请求,不会有缓存问题。这样我们就可以把服务端的缓存时间设置的非常长,不会担心不更新的问题

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    const TerserWebpackPlugin = require('terser-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        main: './src/index.js'
      },
      output: {
        // contenthash:8 冒号8表示执行生成的hash的长度是8位,默认是20位
        filename: '[name]-[contenthash:8].bundle.js' // 根据输出文件的内容生成的hash值,不同的文件有不同的hash值,引入当前文件的文件会被动的改变
        // filename: '[name]-[hash].bundle.js' // 最普通的hash,所有的hash值都是相同的,只要修改一个文件,所有文件名都会改变
        // filename: '[name]-[chunkhash].bundle.js' // 只要是同一路的打包,那么chunkhash值都是相同的,当有文件发生更改后,同一个chunk下的文件名都会改变,引入当前chunk的文件也会被动的改变
      },
      optimization: {
        minimizer: [
          new TerserWebpackPlugin(),
          new OptimizeCssAssetsWebpackPlugin()
        ]
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              // 'style-loader', // 将样式通过 style 标签注入
              MiniCssExtractPlugin.loader,
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Dynamic import',
          template: './src/index.html',
          filename: 'index.html'
        }),
        new MiniCssExtractPlugin({
          filename: '[name]-[contenthash:8].bundle.css'
        })
      ]
    }
    
    

以上就是webpack的全部内容啦有些是之前就要了解过的,有些是之前没有接触过的,通过这次的学习,更好的把它们都串联起来了,不过还是有点模糊,需要再仔细琢磨一下~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值