对于webpack的总结

255 阅读14分钟

本文主要从五个维度进行解读webpack,包括webpack的基础配置、Loader、Plugin、开发体验、生产优化

本文是用的webpack版本为 4.44.2

webpack主要解决的问题

  • 对于新特性代码的编译
  • 模块化JavaScript打包
  • 不同资源类型的打包(js,css,html等)

基础配置

快速上手

  1. 安装webpack模块 yarn add webpack webpack-cli --dev
  2. 创建webpack.config.js 作为webpack 的配置文件
  3. 文件配置完成执行 yarn webpack 会根据配置将src/index.js转成 dist/build.js 配置文件(webpack.config.js 是node环境中运行的js代码)
const path = require('path')
module.exports = {
  entry: './src/index.js', // 入口文件
  output: {
  	filename: 'bundle.js' // 输出文件位置
    path: path.join(__dirname, 'dist') // 得到完整的绝对路径
  }
}

导入资源模块

打包入口从某种成都上来说是我们应用的入口,就目前而言,JavaScript驱动整个前端业务,通常我们还是会使用js作为打包入口,然后在js代码中通过import的方式引入css模块

在js中加载资源文件的优势

  • 逻辑合理,js确实需要这些资源文件
  • 确保上线资源不确实,而且每一个上线的文件都是必要的资源

打包结果的运行原理

为了更好的看到效果,在webpack.config.js文件上设置mode为none,之后执行yarn webpack去观察bundle.js

  1. 打包后文件里面是一个立即执行的函数,此函数为webpack的工作入口,接收一个modules参数
  2. 函数调用时传入的是一个数组,数组中每个函数对应源代码中的模块,也就是说我们每个模块最终都会包含到这个函数中,从而实现模块的私有作用域
  3. 模块的工作入口函数,先定义了一个installedModules对象,用来缓存加载过的模块,__webpack_require__函数用来加载模块。__webpack_require__函数上面挂载了一些数据和工具函数,函数执行return __webpack_require__(_webpack_require__.s = 0),加载输入模块并导出
  4. 解读__webpack_require__函数
    • 进入函数内部发现,首先判断传入的模块是否被加载,如果有,就从缓存里面读(return installedModules[moduleId].exports), 如果没有,创建一个新的对象
     var module = installedModules[moduleId] = {
       i: moduleId,
       l: false, // 表示模块是否被加载
       exports: {}
     }
    
    • 创建完成后调用模块所对应的函数,把我们刚创建的模块对象,导出成员还有__webpack_require__函数传进去,这样在模块内部就可以用module.export导出成员,__webpack_require__去载入模块 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    • module.l = true; 标记这个模块已经被加载
    • return module.exports 返回模块的导出
  5. 解读模块内部函数
    • 模块内部先用调用__webpack_require__.r(__webpack_exports__)用来在导出对象上加标记,在导出对象上定义了__esModule的标记,用来对外界表明该模块是个ES Module
    • 再往下调用模块内部方法,如果模块有对其他模块的引用,会执行__webpack_require__(其他模块的index),加载其他模块后继续向下执行,执行完毕后回到__webpack_require__函数内部,标记模块被加载,返回模块导出
    • module.exports应该是一个对象,ES Module 里面默认导出放在default上面
  6. webpack打包后的结果只是帮我们把所有模块放在同一文件中,并提供一些基础代码,让模块之间的依赖关系维持原有状态

资源模块加载——Loader

webpack内部loader只能去处理js文件,想处理其他文件,要去载入对应的资源loader

Loader是实现前端模块化的核心,通过不同的Loader就可以实现加载任何类型的资源

loader的配置具体就是在webpack.config.js的module.exports对象中配置module的rules:[]

css-loader & style-loader

  • 载入css-loaderyarn add css-loader --dev
  • css-loader的作用就是将css文件转换成js模块
  • style-loader 把css-loader 转换的代码,通过style的方式追加到页面上
  • 载入style-loaderyarn add style-loader --dev
// webpack.config.js 对应的配置
module.export = {
	module:{ 
    	// 用来配置模块的加载规则
    	rules: [ 
        	{
            	test: /.css$/, // 参数接收一个正则,匹配对应模块的加载文件
                use: ['style-loader','css-loader'] // 所使用的的loader,配置成数组的时候执行顺序是从后往前执行
            }
        ]
    }
}

文件资源加载器 file-loader & url-loader

  • file-loader 生成资源文件,输出文件到指定目录,并返回public url
  • url-loader 类似于file-loader,但是在文件大小(单位byte)低于指定的限制时,可以返回一个DataUrl
    • 小文件使用url-loader,减少请求的次数
    • 大文件单独提取存放,提高加载速度
rules:[
 {
   test: /.png$/,
   use: {
          loader: 'url-loader',
          options: {
              limit: 10 * 1024 // 代表10kb
          }
      }
 }
]

以上的配置,超过10kb的文件会单独提取存放,小于10kb文件转换为Data URLs 嵌入代码中

这种方式使用url-loader 还是要去载入file-loader 因为超出10kb的时候,url-loader 还是会调用file-loader

babel-loader

因为模块打包的需要,所以webpack处理了import和export,除此之外不能转换代码中其他的ES6特性,如需转换,需要为js代码配置额外的编译型loader,如babel-loader

yarn add babel-loader @babel/core @babel/preset-env --dev

rules: [
   {
    	test: /.js$/,
        use: {
        	loader: 'babel-loader',
            options: {
            	presets: ['@babel/preset-env']
            }
        }
    }
]

html-loader

webpack加载资源的几种方式

  • 遵循ES Modules 标准的import 声明
  • 遵循 CommonJS 标准的require函数
  • 遵循 AMD 标准的 define 函数 和 require 函数
  • 样式代码中的@import 和 url
  • html代码中的图片标签src
ruels:[
 {
    test: /.html$/,
    use: {
    	loader: 'html-loader',
        options: {
        	attrs: ['img:src','a:href'] // 默认html-loader只能处理img中的src,对于a中的herf需要额外的配置
        }
    }
  }
]

常用加载器分类

加载器:主要是处理打包中的资源文件

  • 编译转换类:
    • 把加载的资源模块,转换成JavaScript代码,例如css-loader
  • 文件操作类:
    • 把加载的资源模块,拷贝到输出目录,同事将文件的访问路径向外导出,例如file-loader
  • 代码检查类:
    • 对加载到的资源文件代码进行校验的加载器,目的是为了统一代码风格,从而提高代码质量

webpack核心工作原理

  1. 找到入口文件,例如main.js,根据里面的模块引入,找到对应的依赖模块,形成对应关系树
  2. 之后去递归依赖树,找到每个节点对应的资源文件
  3. 根据每个资源文件对应的rules属性,找到对应模块的加载器,交给加载器加载
  4. 加载后的结果会放到bundle.js中,从而实现整个项目的打包

Loader资源加载机制是webpack的核心

Loader工作原理

  • Loader负责资源文件从输入到输出的转换,Loader 是一种管道的概念,对于同一个资源可以依次使用多个Loader进行处理
  • loader 中代码实现内部是一个函数,可以通过参数获得传入的source,并且函数需要返回一段JavaScript代码
module.exports = source =>{
	console.log(source)
    return 'console.log("hello~")'
}

Plugin

Plugin 解决除了资源加载以外的其他自动化工作,例如清除dist目录,拷贝静态文件到输出目录,压缩输出代码等

CleanWebpackPlugin 自动清除输出目录的插件

  • 安装插件yarn add clean-webpack-plugin --dev
  • 回到webpack中导入插件并结构成员const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  • 在webpack中通过plugins属性进行配置
// webpack.config.js
module.export = {
  plugins:[
  	new CleanWebpackPlugin()
  ]
}

HtmlWebpackPlugin 自动生成使用bundle.js的HTML

  • 安装模块yarn add html-webpack-plugin --dev
  • 回到webpack导出插件const HtmlWebpackPlugin = require('html-webpack-plugin')
  • 配置plugins 进行配置
plugins: [
    new HtmlWebpackPlugin({
       title: 'Webpack Plugin Sample', // 配置html的title
       meta: { // 以对象的方式设置页面中的源数据标签
         viewport: 'width=device-width'
       },
       template: './src/index.html'
    })
]
  • 自定义生成的html
    • 增加简单的参数,可以给HtmlWebpackPlugin传入对象参数,可对生成的html进行配置
    • 有大量自定义,最好在源代码中添加一个生成html的模板,让插件根据模板生成页面,通过template属性去指定输出模板
    <!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">
        <h1><%= htmlWebpackPlugin.options.title %></h1>
      </div>
    </body>
    </html>
    
  • 同时输出多个页面文件
plugins: [
    // 用于生成index.html
    new HtmlWebpackPlugin({
       title: 'Webpack Plugin Sample', // 配置html的title
       meta: { // 以对象的方式设置页面中的源数据标签
         viewport: 'width=device-width'
       },
       template: './src/index.html'
    }),
    // 用于生成about.html
     new HtmlWebpackPlugin({
       filename: 'about.html'
    })
]

CopyWebpackPlugin 复制静态文件

  • 安装模块yarn add copy-webpack-plugin
  • 回到webpack导出插件const CopyWebpackPlugin = require('copy-webpack-plugin')
  • 配置plugins 进行配置
plugins: [
	new CopyWebpackPlugin([
    	// 'public/**'
    	'public'
    ])
]

Plugin 工作原理

  • Plugin通过钩子机制实现
  • Webpack要求Plugin 必须是一个函数或者是一个包含apply方法的对象
  • apply方法会在任务启动时自动调用,接收一个compiler参数,通过这个对象去注册钩子函数

开发体验

  • 使用http服务区运行
  • 自动编译 + 即时显示
  • 提供Source Map 支持

Webpack Dev Server

提供用于开发的HTTP Server,集成「自动编译」和「自动刷新浏览器」等功能

  • 安装yarn add webpack-dev-server
  • 安装包内部提供了一个叫 webpack-dev-server 的cli,直接运行yarn webpack-dev-server就能启动项目并监听,一旦文件变化,项目会重新打包后浏览器刷新(webpack为了提高效率,并没有将打包结果写入到磁盘中,而是暂存在内存中,减少了不必要的读写操作)
  • 在devServer的时候访问静态资源路径
devServer: {
  contentBase: './public' // 为devServer指定静态资源目录,也可以接收一个数组
}
  • 代理API服务 运行在通过浏览器直连后端服务器的接口,会产生跨域问题,可以通过浏览器访问开发服务器,再由开发服务器做代理去请求后端服务器接口
devServer: {
	proxy: {
    	 '/api':{
  	    target: 'http:xx.xx.xx',
            pathRewrite: { '^/api': '' },
            changeOrigin: true
          }
    }
}

Source Map

主要解决前端引入构建编译后,前端编写的源代码与运行代码不一致所产生的调试问题

配置source map

module.exports = {
  // 配置开发过程中的辅助工具
  devtool : 'source-map'
}

webpack 支持12种不同的配置方式,每种方式的效果和效率各不相同

source map 不同模式的实现

  • eval模式
    • 将模块代码放到eval函数里面去执行,在代码后面通过注释//# sourceURL=webpack:///./src/main.js的方式,去指定源代码的路径,这种情况不会生成source map文件
    • 所以构建速度是最快的,但是只能知道源代码的路径,不知道行列信息。
  • eval-source-map
    • 相比eval 生成了source map
  • cheap-eval-source-map
    • 生成了简易版的source map,只能定位到行,不能定位到列的信息
    • 显示的是经过es6转换后的结果
  • cheap-module-eval-source-map
    • 解析出来的代码不会经过loader加工,与源代码相同

总结

  • eval- 是否使用eval执行模块代码
  • cheap - Source Map 是否包含行信息
  • module - 是否能够得到Loader 处理之前的源代码

如何选择

  • 开发环境中: cheap-module-eval-source-map
    • 一般每行代码不会太多
    • 代码经过Loader转换后的差异比较大
    • 首次打包速度慢无所谓,重写打包相对较快
  • 生产环境打包: none
    • Source Map 会暴露源代码
  • 生产环境也可以使用: nosources-source-map 出现问题会定位到行数,但是不会显示源代码。方便在生产上定位问题

Webpack -- Hot Module Replacement 热更新

页面不刷新的前提下,模块也可以及时更新, HMR已经集成在devServer中,只需要在运行webpack-dev-server --hot加上 --hot参数开启,也可以通过配置文件开启

// 配置完成后还要载入一个webpack内置的插件
devServer:{
    hot: true
},
module:{
    plugins:[
        new webpack.HotModuleReplacementPlugin()
    ]
}

HMR 的疑问

webpack中的HMR并不可以开箱即用,还需要手动处理模块热更新逻辑

Q1: 样式文件为什么可以直接的去热更新

  • 样式文件是经过loader处理的,在style-loader中自动处理了文件热更新,就不需要我们在去手动处理了

Q2:为什么样式可以自动处理?

  • 样式文件编写后,loader 通过及时的替换就能实现热更新,而js模块更新是无规律的,可以没办法实现通用所有模块的热更新

Q3: 为什么项目中没有手动处理js,还是可以热更新js

  • 项目框架中的js是有规律的,框架实现了通用的替换办法,通过脚手架创建的项目内部都集成了HMR方案

总结:我们需要手动处理js模块更新后的热替换

HMR APIs

HotModuleReplacementPlugin提供了一些API,可以让我们在引入项目模块的js文件通过API去处理需要热更新的代码

//main.js
module.hot.accept('./editor',()=>{
	console.log('editor模块更新了,这里需要手动处理热更新')
})

生产优化

不同环境下的配置

  1. 配置文件根据环境不同导出不同配置
  • webpack文件中还支持导出一个函数,在函数中返回我们所需要的配置对象。函数接收两个参数
    • env: 在cli中传递的环境名参数
    • argv: 运行cli传递的所有参数
  • 配置文件webpack.config.js
module.exports = (env,argv) =>{
	const config = {
    	// 开发环境的配置
    }
    if(env === 'production'){
    	config.mode = 'production'
        config.devtool = false
        config.plugins = [
        	...config.plugins,
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public'])
        ]
    }
    return config
}
  1. 不同环境对应不同配置文件
  • 新建一个webpack.common.js 去抽象相同的配置
  • 分别创建webpack.dev.js 和 webpack.prod.js 去对不同环境做配置
  • 当合并webpack.common.js 和环境配置文件的时候需要注意
    • Object.assign() 方法会覆盖plugins
    • 社区中提供了webpack-merge的模块,来帮助我们合并文件
    • yarn add webpack-merge --dev 安装,载入const merge = require('webpack-merge')
    • 配置文件webpack.prod.js
    const merge = require('webpack-merge')
    const common = require('./webpack.common')
    module.exports = merge(common,{
       mode: 'production',
       ... // 其他配置
    })
    
  • 运行的时候通过配置参数去指定所使用的配置文件 yarn webpack --config webpack.prod.js

DefinePlugin (为代码注入全局成员)

webpack4中,新增的production模式下面,内部开启了很多通用的优化功能

  • 在production 模式开启DefinePlugin,并注入常量process.env.NODE_ENV,很多第三方通过这个成员去判断运行环境
  • 想自己去注入成员,可以导入此插件并进行配置
  • DefinePlugin 其实就是把我们注入成员的值,直接替换在代码中
const webpack = require('webpack')
plugins: [
	new webpack.DefinePlugin({
    	API_BASE_URL: "'https://api.example.com'" // 可以传入一段代码片段
    })
]

Tree-shaking(去除未引用代码)

Tree Shaking 不是指某个配置选项,而是一组功能搭配使用后的优化效果,这组功能会在生产模式下自动启用

  • 下面演示在none模式下如何开启Tree Shaking优化
// webpack.config.js
module.exports = {
  // 集中配置webpack内部优化功能  
  optimization: {
    usedExports: true // 表示在输出结果中只导出外部使用了的成员
    minimize: true // 开启压缩,此时未引用的代码都被移除掉了
    // 使用下面的属性继续优化我们的输出,下面配置的作用就是尽可能将所有模块合并输出到一个函数中,既提升了运行效率又提升了输出代码的体积,这个优化又称为Scope Hoisting(作用域提升)
    concatenateModules: true // 开启后的压缩就不是一个模块对应一个函数了,而是把所有的模块放在同一个函数中
  }
}
  • usedExports 负责标记「枯树叶」
  • minimize 负责「摇掉」它们

Tree-shaking & Babel

对于很多资料显示 使用babel-loader,tree-shaking 会失效的说明

  • tree-shaking前提是必须使用ES Module组织我们的代码,交给webpack 打包的代码必须使用ESM,

  • 开发中为了转换代码中ESMAScript新特性,通常会用babel-loader去处理js,babel转换代码时很有可能 将ES Modules转化成 CommonJS,

  • 但是在最新版本的babel-loader中,自动关闭了ES Module 转化的插件,可以在babel-loader的源码 injectCaller.js中查看到的 supportsStaticESM: true, 表示的意思是我们当前环境支持ESM,在preset-env转换的源码中,根据配置的参数,自动禁用了ES Modules 的转换

通过配置的方式配置转换

options:[
	preset: [
    	['@babel/preset-env', { modules: 'commonjs' }] // 转化为commonjs
    ]
]

sideEffects(副作用)

webpack4中新增的新特性,允许我们通过配置的方式标识代码是否有副作用,从而为tree-shaking提供更大的压缩空间

  • 副作用:除了模块导出成员外所做是事情,一般用于npm包标记是否有副作用

Code Splitting 代码分包/代码分割

webpack 的弊端:

  1. 项目中所有代码最终都会被打包到一起,应用复杂时体积过大。
  2. 并不是每个模块在启动时都要被加载进来 解决方式:分包,按需加载

多入口打包

适用于传统的多页面应用,一个页面对应一个打包入口,公共部分单独提取

  • 把entry 定义为一个对象
  • output动态输出文件名
module.exports = {
	entry: {
    	index: './src/index.js'
        album: './src/album.js'
    },
    output: {
    	filename: '[name].build.js'
    },
    plugins: [
    	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'] //为html指定他所使用的bundler
        })
    ]
}

提取公共模块 不同的入口文件中肯定会有公共模块,

// 只需要配置优化属性即可
optimization:{
	splitChunks: {
    	chunks: 'all' //把所有的chunk中的公共模块都提取出来
    }
}

动态导入

在应用过程中用到某个模块时,再加载这个模块,动态导入的模块会被自动分包

import(/*这里面是魔法注释,给生成的bundle加上名字 webpackChunkName: 'posts'*/'./posts/posts').then(({default: post()})=>{
	mainElement.appendChild(posts())
}) 

魔法注释中,相同的chunkName 就会被打包到一起

MiniCssExtractPlugin(css模块的按需加载)

提取CSS到单个文件,通过这个插件可以实现css模块的按需加载

  • yarn add mini-css-extract-plugin --dev

OptimizeCssAssetsWebpackPlugin(压缩css)

用来压缩css文件