Webpack 与 自动化构建

355 阅读10分钟

Webpack快速上手:

  • yarn init --yes
  • yarn add webpack webpack-cli --dev
//Webpack.config.js 配置文件
const path = require('path')

module.exports = {
  // 这个属性有三种取值,分别是 production、development 和 none。
  // 1. 生产模式下,Webpack 会自动优化打包结果;
  // 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  // 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
  mode: 'none',
  // 入口路径(相对路径时./不能省)
  entry: './src/main.js',
  // 输出文件位置
  output: {
    // 输出文件名称
    filename: 'bundle.js',
    // 文件目录  => 必须是绝对路径
    path: path.join(__dirname, 'output')
  }
}

webpack打包运行原理:

以上目录结构,打包生成bundle.js,打包结果解析:

将打包结果折叠,打包结果是一个立即执行函数,接受一个modules的参数,立即执行函数传入的实参就是我们对应的两个模块,每个模块最终被包裹到一个函数中实现私有作用域。

结果内部代码分析:见之后文章,后续更新

Webpack核心工作原理:

  • loader机制是webpack核心:webpack通过打包入口的js => 解析代码依赖模块 => 解析模块依赖 => 形成依赖树 => 递归依赖树 => 找到资源文件 => 根据webpack.config.js配置文件rules属性找到对应加载器 => 最后将加载结果输出到输出文件中

Webpack模块加载方式:兼容多种模块化标准

  1. import:遵循ES Module标准的import声明
  2. require:遵循CommonJS标准的require函数 => require也可以载入ES Module模块,载入默认导出,需要default属性。例:require('./heading.js').default
  3. define函数和require函数:遵循AMD标准
  4. loader加载的非JS也会触发资源加载:例:css中的@import指令和URl函数;html代码中的src属性

webpack资源模块加载:

webpack内部loader默认处理只处理js文件,loader是webpack的核心特性,借助于loader就可以加载任何类型的资源

  • CSS模块加载器:css-loader 与 style-loader
    • yarn add css-loader --dev => 转换css
    • yarn add style-loader --dev => 把css-loader转换结果 通过Style标签追加到页面上

注:webpack.config.js配置文件如下:use的配置从右向左执行,先执行css-loader后style-loader

const path = require('path')

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

附:导入资源模块:webpack建议根据代码得需要动态导入资源,因为需要资源的不是应用,而是你所编写的代码,在js文件中,加载所有哦需要的资源文件,例如使用import加载css资源文件,文件入口为js文件。这样做的好处是逻辑合理,js需要资源文件,确保上线资源不缺失,都是必要的。

  • webpack文件(图片)资源加载器:file-loader
    • yarn add file-loader --dev
//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: /.png$/,
        use: 'file-loader'
          => use配置:1.可写模块名称/路径(./markdown-loader.js2.也可为数组,从右向左执行
      }
    ]
  }
}
  • webpack文件(图片)资源加载器:url-loader
    • webpack Data Urls:特殊的url协议,直接表示一个文件,url中的文本已经包含文本内容,使用并不会发送http请求,内容转换为base64编码,Data Urls适合体积较小的资源文件,小文件使用Data Urls,减少请求次数;大文件单独提取存放,提高加载速度。

image-20210215111414752

  • 安装
    • yarn add url-loader --dev
    • yarn add file-loader --dev => 同时安装file-loader处理超过大文件,当文件大于10KB则会使用file-loader
//Webpack.config.js 配置文件
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
              =>	10为字节,超出10KB单独存放,小于10KB文件转换为Data Urls嵌入代码
          }
        }
      }
    ]
  }
}
  • Webpack处理ES2015:babel-loader
    • webpack内部因为模块打包需要,所以可以处理import与export,但并不能处理ES6代码,需要babel-loader处理,babel也只是一个平台,转换ES6代码需要插件
    • yarn add babel-loader @babel/core @babel/preset-env
      • => babel/core:babel依赖的核心模块;preset-env:是转换具体特性的插件集合
//Webpack.config.js 配置文件
test: /.js$/,
use: {
	loader: 'babel-loader',
	options: {
		presets: ['@babel/preset-env']
	}
}
  • Webpack识别html文件:html-loader
    • yarn add html-loader --dev
//Webpack.config.js 配置文件
test: /.html$/,
use: {
  loader: 'html-loader',
  options: {
    attrs: ['img:src', 'a:href']
  }
}

Webpack常用加载器分类:

  • 加载器的作用:处理打包过程中所遇到的资源文件
  1. 编译转换类:将资源转换为浏览器可识别的js代码 => css-loader
  2. 文件操作类:拷贝到输出目录,导出文件访问路径即可 => file-loader
  3. 代码检查类:统一代码风格(不修改代码) => eslint-loader

Webpack中loader的工作原理:

输入到输出的转换:loader相当于一个管道,转换时可以使用多个loader来得到最终的转换结果,最终结果要求是js代码。

loader的导出:每个 Webpack 的 loader 都需要导出一个函数,函数是对加载的资源处理过程,函数的输入是加载到的资源,输出加工处理过后的结果,结果最终要求是js代码

实现自己开发的loader:

  • 需求将md文件作为html文件输出
  1. 创建markdown-loader.js,以下为代码内容
  2. 第一种方式:安装mardown文件解析模块:yarn add marked --dev
  3. 第二种方式:返回html字符串交给下一个loader处理
  • 安装 html - loader :yarn add html - loader --dev
//mark-loader.js
const marked = require('marked')

module.exports = source => {
  // console.log(source)
  const html = marked(source)
  //第一种方式
  // 直接返回html并不是js代码,需要对这个html进行处理,以下方式将html转换为js代码,但是直接拼接并不能对字符串中的换行符及引号进行处理
  // return `module.exports = "${html}"`
  // 对换行符及引号等进行处理,转换为json格式字符串;同样支持ES Module方式导出
  // return `export default ${JSON.stringify(html)}`

  // 返回 html 字符串交给下一个 loader 处理
  return html
}

//Webpack.config.js 配置文件
test: /.md$/,
use: [
  'html-loader',
  './markdown-loader'
]

Webpack 插件机制:

  • plugin增强webpack自动化能力,loader专注实现资源模块加载,plugin解决除资源加载外其他自动化工作,绝大多数插件模块导出的都是类。插件的工作例如:自动在打包前清除dist目录(上一次的打包结果);拷贝静态文件至输出目录(不需要参与打包的文件);压缩输出目录的代码文件;

  • 自动清除输出目录插件安装:yarn add clean-webpack-plugin --dev

  • 自动生成HTML插件安装:yarn add html-webpack-plugin --dev

  • 对无需打包的文件进行拷贝:yarn add copy-webpack-plugin --dev

//Webpack.config.js 配置文件
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {...},
  module: {
    rules: [...]
  },
  plugins: [
    new webpack.ProgressPlugin(),
    // 清除打包目录
    new CleanWebpackPlugin(),
    // 用于生成 index.html  =>	对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([
      // 'public/**'
      'public'
    ])
  ]
}

Webpack 开发一个插件:

  • 相比于loader,plugin拥有更宽的能力范围;plugin通过钩子机制实现,webpack要求钩子必须是一个函数或者是一个包含apply方法的对象,一般会把插件定义为一个类型,在类型中定义apply方法。开发一个MyPlugin插件,删除js文件生成的注释:
//Webpack.config.js 配置文件
class MyPlugin {
  // webpack启动时自动调用apply方法
  apply (compiler) {
    // compiler 对象参数,此次对象的所有信息
    console.log('MyPlugin 启动')
    // emit 钩子:即将往输出目录输出文件
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文(所有打包过程产生的结果都会放到这个对象中)
      for (const name in compilation.assets) {
        // 获取资源文件信息
        // console.log(name)
        // console.log(compilation.assets[name].source())
        // 判断是否为js文件
        if (name.endsWith('.js')) {
          // 获取js资源文件内容
          const contents = compilation.assets[name].source()
          // 进行注释替换
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          
          compilation.assets[name] = {
            // 暴露两个方法:替换后的内容
            source: () => withoutComments,
            // 内容长度 
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {...},
  module: {
    rules: [...]
  },
  plugins: [
    new MyPlugin()
  ]
}

Webpack 自动化开发:

  • Webpack Dev Server :提供用于开发的HTTP Server ,集成自动编译和自动刷新浏览器等功能
  • 安装:yarn add webpack-dev-server --dev
  • 运行:yarn webpack-dev-server
  • 直接打开浏览器:yarn webpack-dev-server --open
//Webpack.config.js 配置文件
module.exports = {
  ...
  devServer: {
    // 指定静态文件资源目录
    contentBase: './public',
    // 代理 API 服务:将 GitHub API 代理到开发服务器  
   	// proxy添加代理服务配置
    proxy: {
        // /api 请求路径前缀
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        // target 代理目标
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/users
        // 代理路径重写
        pathRewrite: {
          '^/api': ''
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名,以代理主机名请求
        changeOrigin: true
      }
    }
  },
  ...
}

Source Map : 映射源代码与转换后代码的关系,通过Source Map 逆向解析,定位报错代码位置

//Webpack.config.js 配置文件
module.exports = {
  ...
  devtool: 'eval',
  ...
}
  • webpack的devtool支持12种不同的方式对 Source Map ,每种方式效率和效果各不相同,以下是部分方式比较,详细比较链接:webpack.docschina.org/configurati…
devtool备注
none)生产模式建议使用
eval将模块代码放eval中执行,通过 Source URl标注路径,只能定位错误文件;开发模式建议使用。
eval-cheap-source-map只能定位行,无列,速度稍快 => 编译后,有loader加工
eval-cheap-module-source-map只能定位行,无列,源代码 => 无loader加工
eval-source-map同样使用eval函数执行模块代码,还可具体到行和列,生成source-map文件
cheap-source-map无eval,loader 处理过(无 module)
inline-source-mapdata Url方式的物理文件,将源代码放入代码中
nosources-source-map看不到源代码,但可以看到行列信息
hidden-source-map代码中无注释引入,但生成了Source Map

Webpack Hot Module Replacement 模块热更新:无需刷新整个页面,可以刷新模块内容

  • 开启HMR:
  • 方式一:yarn webpack-dev-server --hot
  • 方式二:
//Webpack.config.js 配置文件
const webpack = require('webpack')

module.exports = {
  ...
  devServer: {
    // hot: true	=> 正常配置
    hotOnly: true 
    // 只使用 HMR,不会 fallback 到 live reloading;当页面报错不会自动刷新
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}
  • 运行 yarn webpack-dev-server --open

处理js模块热更新需要手动处理,css模块的css-loader已经处理了关于热更新的部分。

  • HMR APIS => 处理js模块热替换,代码如下:
// 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
  // js模块热更新处理
  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)
  })
}

生产环境配置优化:模式mode,为不同环境创建不同配置

  • 方式一:根据环境不同导出不同配置文件
//Webpack.config.js 配置文件
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

// env:通过cli传递的环境名参数
// argv:运行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  => 生产环境禁用source map
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }

  return config
}
  • 方式二:多配置文件:不同环境对应不同配置文件;2个不同环境配置文件,一个公共配置文件
  • yarn add webpack-merge --dev => 合并配置所需插件
// 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')

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'])
  ]
})


// package.json 指定配置文件
"scripts": {
    "build": "webpack --config webpack.prod.js"
  },

为代码注入全局成员:

//Webpack.config.js 配置文件
const webpack = require('webpack')

module.exports = {
  ...
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段  API_BASE_URL变量在js中全局可使用
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

Tree - Shaking:

  • [揺掉]代码中未引用的部分,Tree Shaking并不是指某个配置选项,是一组功能下搭配使用后的优化效果,生产模式下自动开启;其他模式下可以通过配置选项开启:
//Webpack.config.js 配置文件 
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  },
  optimization: {
    // 标记npm包是否有副作用,生产模式自动开启
    sideEffects: true,
    // 模块只导出被使用的成员
    usedExports: true,  => userExports 负责标记未使用代码 
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输出结果
    minimize: true  =>  minimize 负责清除标记后的代码
  }
}


// package.json  "sideEffects":false  => 标识项目代码无副作用
// 但是函数的原型方法,css模块均有副作用,以下方式标识有副作用的文件
"sideEffects": [
    "./src/extend.js",
    "*.css"
 ]

Code Splitting 代码分包/代码分割:

  • 应用复杂时,打包代码体积过大,影响js执行速度,但并不是每个模块启动时都是必要的,我们可以进行分包,按需加载。
  • 方式一:多入口打包:适用于多页应用程序,一个页面对应一个打包入口,不同页面公共部分单独提取
//Webpack.config.js 配置文件 
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'
    }
  },
  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']
    })
  ]
}

  • 方式二:动态导入:按需加载,需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包
// index.js文件
const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === '#posts') {
    // mainElement.appendChild(posts())
    // /* webpackChunkName: 'components' */ 为分包的bundle起名字
    import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)

//Webpack.config.js 配置文件
output: {
    filename: '[name].bundle.js'
},

提取css到单个文件:

  • yarn add mini-css-extract-plugin --dev => css文件使用link标签引入而非style标签
  • yarn add optimize-css-assets-webpack-plugin --dev => 压缩输出的css文件
  • yarn add terser-webpack-plugin --dev => 压缩输出的js文件
//Webpack.config.js 配置文件 
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]-[contenthash:8].bundle.js'
  },
  // 若配置在plugin中则任何时候都会工作,配置在optimization中,当开启minimizer时,才会工作(生产模式minimizer会自动开启),使用自定义压缩器,js不能自动压缩,使用TerserWebpackPlugin压缩js代码
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),  =>  压缩js模块
      new OptimizeCssAssetsWebpackPlugin() => 压缩输出的css文件 
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
      // 输出文件名配置
      filename: '[name]-[contenthash:8].bundle.css'
    })
  ]
}

结语:以上内容全学习时手敲记录,无复制粘贴,全原创,希望可以给各位小伙伴带来收获,如有错误的地方或有疑问欢迎留言,感谢阅读!

祝各位前端程序猿前程似锦,一路向北!