[基础篇]深入掌握webpack | 8月更文挑战

883 阅读14分钟

[基础篇]深入掌握webpack

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。为什么要深入掌握webpack呢?在日常开发中,我们需要同时兼顾PC、H5等各类不同分辨率的网页开发。因此需要针对不同应用场景做不同的打包,如针对PC端中后台应用,我们需要支持单页应用的打包构建。而H5页面通常对性能和可访问性有着极高的要求,因此需要通过构建来支持服务端渲染和PWA离线缓存。

在本基础篇中,主要是掌握webpack的核心概念和开发必备技巧。

为什么需要构建工具?

  • 转换ES6语法

ES6 module主流浏览器支持情况

以上红色背景的都是不支持ES6 module的,因此可以看出webpack转换ES6语法是有必要的。

  • 转换JSX等

像主流的框架React、Vue的语法糖,浏览器无法直接去识别它们,所以也需要使用构建工具转换

  • CSS 前缀补全/预处理器

如LESS、SASS等预处理器

  • 代码压缩混淆
  • 图片压缩

为什么选择webpack?

  • 社区生态丰富
  • 配置灵活和插件化扩展
  • 官方更新迭代速度快

初识webpack

webpack默认配置文件:webpack.config.js

五个核心概念

  • entry:入口指示webpack以哪个文件为入口起点开始打包,分析构建内部依赖图。
  • output:输出指示webpack打包后的资源bundles输出到哪里去,以及如何命名。
  • loader:loader让webpack能够去处理那些非Javascript文件(webpack自身只理解JavaScript)。
  • plugins:插件可以用于执行范围更广的任务。插件的范围包括,从打包优化到压缩,一直到重新定义环境的变量等。
  • mode:指示webpack使用相应模式的配置(development、production)。

初始化配置

  1. 初始化package.json,终端输入:
npm init -y
  1. 下载并安装webpack,终端输入:
npm install webpack webpack-cli --save-dev

一个简单例子

// webpack.config.js
const path = require('path');

module.exports = {
  entry:'./src/index.js', //入口起点
  output:{
    path:path.resolve(__dirname,'dist'), // 输出文件目录
    filename:'bundle.js' // 输出文件名
  }
  mode:'production'
}

构建结果如下:

以上可以通过npm script的方式运行webpack

在package.json的scripts中加入:

"scripts":{
  "build":"webpack"
}

即可通过npm run build运行构建,其原理是模块局部安装会在node_modules/.bin目录创建软链接。

webpack基础用法

核心概念之Entry

Entry用来指定webpack的打包入口。

webpack依赖图

依赖图的入口是entry,对于非代码比如图片、字体依赖也会不断加入到依赖图中。

Entry的用法

  • 单入口:entry是一个字符串
module.exports = {
  entry:'./src/index.js'
}
  • 多入口:entry是一个对象
module.exports = {
  entry:{
    app:'./src/app.js',
    adminApp:'./src/adminApp.js'
  }
}

核心概念之Output

Output用来告诉webpack如何将编译后的文件输出到磁盘。

Output的用法

  • 单入口配置
module.exports = {
  entry: './path/to/my/entry/file.js'
  output: {
    filename: 'bundle.js’,
    path:path.resolve(__dirname,'dist'),
  }
};
  • 多入口配置
module.exports = {
  entry:{
    app:'./src/app.js',
    search:'./src/search.js'
  },
  output:{
    filename:'[name].js', // 通过占位符确保文件名称的唯一
    path:path.resolve(__dirname,'dist'), // 输出文件目录
  }
}

核心概念之Loaders

webpack开箱即用只支持JS和JSON两种文件类型,通过Loaders去支持其他文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。

本身是一个函数,接收源文件作为参数,返回转换的结果。

常见的Loaders有哪些?

名称描述
babel-loader转换ES6、ES7等JS新特性语法
css-loader支持.css文件的加载和解析
less-loader将less文件转换成css
ts-loader将TS转换成JS
file-loader进行图片、字体等的打包
raw-loader将文件以字符串的形式导入
thread-loader多进程打包JS和CSS

Loaders的用法

module.exports = {
  module:{
    rules:[
      {test:/\.txt$/,use:'raw-loader'} // test指定匹配规则,use指定使用的loader名称
    ]
  }
}

核心概念之Plugins

插件用于bundle文件的优化,资源管理和环境变量注入,作用于整个构建过程。

常见的Plugins有哪些?

名称描述
CommonsChunkPlugin将chunks相同的模块代码提取成公共js
CleanWebpackPlugin清理构建目录
ExtractTextWebpackPlugin将CSS从bundle文件里提取成一个独立的CSS文件
CopyWebpackPlugin将文件或者文件夹拷贝到构建的输出目录
HtmlWebpackPlugin创建html文件去承载输出的bundle
UglifyjsWebpackPlugin压缩JS
ZipWebpackPlugin将打包出的资源生成一个zip包

Plugins的用法

module.exports = {
  plugins:[
    new HtmlWebpackPlugin({template:'./src/index.html'}) // 放到plugins数组里
  ]
}

核心概念之Mode

Mode用来指定当前的构建环境是:production、development还是none,设置mode可以使用webpack内置的函数,默认值为production。

Mode的内置函数功能

选项描述
development设置process.env.NODE_ENV的值为development。启用NamedChunksPlugin和NamedModulesPlugin`。
production设置process.env.NODE_ENV的值为production。启用FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginOccurrenceOrderPluginSideEffectsFlagPluginTerserPlugin
none不开启任何优化选项

资源解析:解析ES6

安装babel相关Loader:

npm i @babel/core @babel/preset-env babel-loader -D

使用babel-loader,babel的配置文件是.babelrc,因此需要增加ES6的babel preset配置

// .babelrc
{
  "presets":[
+   "@babel/preset-env" // 增加ES6的babel preset配置
  ],
  "plugins":[
    "@babel/proposal-class-properties"
  ]
}

webpack配置如下:

module.exports = {
  module:{
    rules:[
      {
        test:/\.js$/,
        use:'babel-loader'
      }
    ]
  }
}

资源解析:解析React JSX

安装React相关的Loader:

npm i react react-dom @babel/preset-react -D

重新配置.babelrc:

// .babelrc
{
  "presets":[
    "@babel/preset-env",
+   "@babel/preset-react" // 增加React的babel preset配置
  ],
  "plugins":[
    "@babel/proposal-class-properties"
  ]
}

资源解析:解析CSS

安装解析CSS相关的Loader:

npm i style-loader css-loader -D

webpack配置如下:

module.exports = {
  module:{
    rules:[
      {
        test:/\.js$/,
        use:'babel-loader'
      },
+     {
+       test:/\.css$/,
+       use:[
+         'style-loader',
+         'css-loader'
+       ]
+     }
    ]
  }
}
  • css-loader 用于加载.css文件,并转换成commonjs对象

  • style-loader将样式通过<style>标签插入到head中

注意:Loader的调用是链式调用的,其执行顺序是从右到左,因此我们需要先写style-loader再写css-loader,这样的话才能先使用css-loader去解析CSS,然后再将解析好的CSS传递给style-loader

资源解析:解析Less和Sass

以Less为例,安装解析Less的Loader:

npm i less less-loader -D

webpack配置如下:

module.exports = {
  module:{
    rules:[
      {
        test:/\.css$/,
        use:[
          'style-loader',
          'css-loader'
        ]
      },
+     {
+       test:/\.less$/,
+       use:[
+         'style-loader',
+         'css-loader',
+         'less-loader'
+       ]
+     }
    ]
  }
}

less-loader用于将less转换成css。

资源解析:解析图片

安装处理文件的Loader:

npm i file-loader -D

webpack配置如下:

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

解析出来的图片会自动将名字改为Hash值。

资源解析:解析字体

file-loader也可以用于处理字体,webpack配置如下:

module.exports = {
  module:{
    rules:[
+     {
+       test:/\.(woff|woff2|eot|ttf|otf)$/,
+       use:[
+         'file-loader'
+       ]
+     }
    ]
  }
}

解析出来的字体也会自动将名字改为Hash值。

资源解析:使用url-loader

url-loader其内部也是使用了file-loader,其可以处理图片和字体,也可以设置较小资源自动base64。

安装如下:

npm i url-loader -D

webpack配置如下:

module.exports = {
  module:{
    rules:[
+     {
+       test:/\.(png|svg|jpg|gif)$/,
+       use:[{
+         loader:'url-loader',
+         option:{
+           limit:10240 // 小于10kb会被base64
+         }
+       }]
+     }
    ]
  }
}

base64之后,图片资源会被嵌入在js中。

webpack中的文件监听

文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。要实现webpack中的文件监听,这时候就需要让webpack开启监听模式,有两种方式:

  • 启动webpack命令时,带上--watch参数
// package.json
{
  "name": "hello-webpack",
  "version": "1.0.0",
  "description": "Hello webpack",
  "main": "index.js",
  "scripts": {
    "build": "webpack ",
+   "watch": "webpack --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

缺点:每次需要手动刷新浏览器。

  • 在配置webpack.config.js中设置watch: true

原理:轮询判断文件的最后编辑时间是否变化,若某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等aggregateTimeout(缓存时间)。

module.exports = {
+ // 默认false,也就是不开启
+ watch: true,
+ // 只有开启监听模式时,watchOptions才有意义
+ watchOptions:{
+   // 默认为空,不监听的文件或者文件夹,支持正则匹配
+   ignored:/node_modules/, // 忽视node_modules会显著提升文件监听速度
+   // 监听到变化发送后会等300ms再去执行,默认300ms
+   aggregateTimeout:300,
+   // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
+   poll:1000
+ }
}

但是以上两种监听方式都需要自己手动刷新浏览器,很不方便,所以就引出了热更新的办法。

webpack的热更新

热更新:webpack-dev-server

WDS通常是需要和HotModuleReplacementPlugin插件配合使用的,因为WDS不刷新浏览器,它们两配合使用可以达到热更新的功能。另外,相对于watch的方式而言,WDS不输出文件,而是放在内存中,构建速度会有更大的优势。配置文件如下:

// package.json
{
  "name": "hello-webpack",
  "version": "1.0.0",
  "description": "Hello webpack",
  "main": "index.js",
  "scripts": {
    "build": "webpack ",
+   "dev": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

webpack配置如下:

const webpack = require('webpack');
module.exports = {
  mode:'development',
  plugins:[
    new webpack.HotModuleReplacementPlugin() // 该模块是webpack自带的
  ],
  devServer:{
    contentBase: './dist', // devServer服务的基础目录
    hot:true // 开启热更新
  }
}

通过npm run dev,修改内容时就可以看出效果。

热更新:使用webpack-dev-middleware

WDM也可以实现WDS相同的效果。使用WDM时,需要引入Node的server,通常是使用express或koa。WDM会将webpack输出的文件传输给服务器,适用于灵活的定制场景。

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler,{
  publicPath:config.output.publicPath
}));

app.listen(3000,function(){
  console.log('Example app listenning on port 3000!\n')
})

热更新的原理分析

我们先理清几个概念:

  • Webpack Compiler:将JS编译成Bundle
  • HMR Server:将热更新的文件输出给HMR Runtime
  • Bundle Server:提供文件在浏览器的访问
  • HMR Runtime:会被注入到浏览器,更新文件的变化
  • bundle.js:构建输出的文件

热更新的两个过程:

  • ①->②->A->B:在文件系统进行编译,将初始代码经过Webpack Compiler打包,打包编译好的文件传输给Bundle Server,其使用server的方式让浏览器可以访问得到。
  • ①->②->③->④:对于文件更新的情况下,在文件系统进行编译,代码经过Webpack Compiler打包后传输给HMR Server,其就知道哪些模块发生了改变并通知HMR Runtime(通常以JSON数据传输),之后HMR Runtime就会更新我们的代码。

webpack文件指纹策略

什么是文件指纹?

文件指纹指打包后输出的文件名的后缀

文件指纹的好处

  • 用于版本管理,可以直接将修改了(变动的文件指纹)的文件发布上去。
  • 对于没有修改的文件,可以持续地用浏览器缓存,可以加速页面的访问

如何生成文件指纹?

常见的有三种:

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash值就会更改。

    改变A页面的js文件会影响B页面的js文件一起重新hash,没必要,故引出了Chunkhash。

  • Chunkhash:和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值。

    不同的entry会产生不同的chunk,一个页面发生了改变并不会影响其它页面,但如果css资源使用了Chunkhash,js改变后不会导致css改变,因此css一般采用contenthash。

  • Contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变。

Chunkhash无法与热更新时一起使用,因此需要在生产环境上使用

JS的文件指纹设置

设置output的filename,使用[chunkhash],webpack配置如下:

module.exports = {
  entry:{
    app:'./src/app.js',
    search:'./src/search.js'
  },
  output:{
+   filename:'[name][chunkhash:8].js',
    path:__dirname+'/dist'
  }
}

CSS的文件指纹设置

style-loader会将css插入到<style>并放在<head>中,而并不是一个独立的css文件,因此我们需要设置MiniCssExtractPlugin这个插件去将css从head中提取出来生成一个独立的文件。我们需要先安装这个插件:

npm i mini-css-extract-plugin -D

设置MiniCssExtractPlugin的filename,使用[contenthash],webpack配置如下:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  entry:{
    app:'./src/app.js',
    search:'./src/search.js'
  },
  output:{
    filename:'[name][chunkhash:8].js',
    path:__dirname+'/dist'
  },
  module:{
    rules:[
      {
        test:/\.css$/,
        use:[
+         MiniCssExtractPlugin.loader, // 因为style-loader与MiniCssExtractPlugin的功能互斥,所以需要替换成MiniCssExtractPlugin自己的loader
          'css-loader'
        ]
      }
    ]
  }
  plugins:[
+   new MiniCssExtractPlugin({
+     filename:'[name][contenthash:8].css' // 8的意思是取hash串的前8位(默认为32位)
+   });
  ]
}

图片的文件指纹设置

字体等配置也一样,设置file-loaderurl-loader的name,使用[hash],常见的占位符如下:

占位符名称含义
[ext]资源后缀名
[name]文件名称
[path]文件的相对路径
[folder]文件所在的文件夹
[contenthash]文件内容的hash,默认是md5生成
[hash]文件内容的hash,默认是md5生成
[emoji]一个随机的指代文件内容的emoji

webpack配置如下:

const path = require('path');module.exports = {  entry:'./src/index.js',  output:{    filename:'bundle.js',    path:path.resolve(__dirname,'dist')  },  module:{    rules:[      {        test:/\.(png|svg|jpg|gif)$/,        use:[{          loader:'file-loader',+         options:{+           name:'img/[name][hash:8].[ext]'+         }        }]      }    ]  }}

webpack代码压缩

JS文件的压缩

webpack4里面内置了uglifyjs-webpack-plugin,因此,默认打包出来的文件就以及压缩过。也可以手动安装uglifyjs-webpack-plugin做一些定制的需求,比如开启并行压缩等。

CSS文件的压缩

使用optimize-css-assets-webpack-plugin,同时使用cssnano预处理器,安装如下:

npm i optimize-css-assets-webpack-plugin cssnano -D

webpack配置如下:

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  plugins:[
+   new OptimizeCSSAssetsPlugin({
+     assetNameRegExp:/\.css$/g,
+     cssProcessor:require('cssnano')
+   });
  ]
}

html文件的压缩

修改html-webpack-plugin,设置压缩参数,webpack配置如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  plugins:[
+   new HtmlWebpackPlugin({ // 一般一个页面要对应一个HtmlWebpackPlugin
+     template:path.join(__dirname,'src/search.html'), // html模板所在的位置
+     filename:'search.html', // 指定打包出来的文件名称
+     chunks['search'], // 指定生成的html要使用哪些chunks
+     inject:true,
+     minify:{
+       html5:true,
+       collapseWhitespace:true,
+       preserveLineBreaks:false,
+       minifyCSS:true,
+       minifyJS:true,
+       removeComments:false
+     }
+   });
  ]
}

当前构建存在的问题

当我们每次构建后webpack不会自动清理目录,造成构建的输出目录output文件越来越多。因此实现自动清理构建目录主要有两种方法:

  • 通过npm script清理构建目录
1. rm -rf ./dist && webpack
2. rimraf ./dist && webpack

但这种方式不够优雅。

  • 利用webpack插件来自动清理构建目录

为了避免构建前每次都需要手动删除dist,可以使用clean-webpack-plugin这个插件,使用时默认会删除output指定的输出目录,webpack配置如下:

const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
  plugins:[
+   new CleanWebpackPlugin()
  ]
}

webpack中对CSS的增强功能

PostCSS插件 autoprefixer自动补全CSS3前缀

由于现代浏览器和移动设备的类型众多,因此我们需要考虑兼容性的问题,这些兼容性问题很多都可以在构建阶段去避免的,比如CSS3的前缀问题。CSS3的属性为什么需要前缀呢?

目前浏览器的标准并没有完全统一,像以前我们都需要自己在css代码里面手动增加前缀去应对兼容性问题,非常浪费时间,如何在构建阶段去帮我们自动补全前缀呢?我们可以利用webpack的插件去实现。

autoprefixer是css的后置处理器,与Less、Sass等预处理不同,预处理器一般是在打包前去处理,而后置处理器是样式处理好且代码生成完好后再进行后置处理。

安装如下:

npm i postcss-loader autoprefixer -D

webpack配置如下:

module.exports = {
  module:{
    rules:[
      {
        test:/\.less$/,
        use:[
          'style-loader',
          'css-loader',
          'less-loader',
+         {
+           loader:'postcss-loader',
+           options:{
+             plugins:() => [
+               require('autoprefixer')({
+                 // 兼容到某个浏览器最近的两个版本,“大于1%”是这个版本所用的人数的比例,兼容iOS7以上
+                 browsers:["last 2 version",">1%","iOS 7"]
+               })
+             ]
+           }
+         }
        ]
      }
    ]
  }
}

移动端CSS px自动转换成rem

各类设备的分辨率不一致引发前端工程师需要去不断地做适配,常用的方式是利用CSS媒体查询来实现响应式布局,如:

@ media screen and (max-width:980px){  .header{    width:900px;  }}@ media screen and (max-width:480px){  .header{    width:400px;  }}

缺陷:需要写多套适配样式代码

CSS3出现了rem这个单位,.rem是相对单位,而.px是绝对单位,我们可以使用px2rem-loader去实现px自动转换成rem,px转换成rem之后,我们需要知道1rem = ?? px,因此需要页面渲染时来计算根元素的font-size值(这里可以使用lib-flexible这个库),安装如下:

npm i px2rem-loader -D
npm i lib-flexible -S

webpack配置如下:

module.exports = {
  module:{
    rules:[
      {
        test:/\.less$/,
        use:[
          'style-loader',
          'css-loader',
          'less-loader',
+         {
+           loader:'px2rem-loader',
+           options:{
+             remUnit:75, // 1rem = 75px
+             remPrecision:8 // px转换成rem的小数点的位数
+           }
+         }
        ]
      }
    ]
  }
}

lib-flexible需要将代码引入到html中,下面将会对资源内联进行解释。

静态资源内联

资源内联的意义

代码层面:

  • 页面框架的初始化脚本(如上面计算rem根元素的font-size大小)
  • 上报相关打点
  • css内联避免页面闪动

请求层面:减少HTTP网络请求数

  • 小图片或者字体内联(url-loader)

HTML和JS内联

使用row-loader可以内联HTML和JS,(使用0.5.1的版本)

// 内联html
<script>${require(' raw-loader!babel-loader!. /meta.html')}</script>
// 内联js
<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>

CSS内联

CSS内联有两种方案:

  • 借助style-loader,webpack配置如下:
module.exports = {
  module:{
    test:/\.scss$/,
    use:[
      {
        loader:'style-loader',
+       options:{
+         insertAt:'top', // 样式插入到<head>
+         singleton:true, // 将所有的style标签合并成一个
+       }
      },
      'css-loader',
      'sass-loader'
    ]
  }
}
  • 使用html-inline-css-webpack-plugin

多页面应用(MPA)概念

每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。

相比单页面应用,多页面应用发布之后,它有很多个入口,即一个页面就是一个业务;而单页面通常来说,我们会把所有的业务都放在一个入口,不同的子业务都是一个url。

多页面优势:

  • 每个页面之间是解耦的
  • 对于SEO更加友好

多页面打包基本思路

每个页面对应一个entry,一个html-webpack-plugin,如:

module.exports = {
  entry: {
    index: './src/index.js',
    search: './src/search.js ‘
  }
};

缺点:每次新增或删除页面需要改webpack配置。

多页面打包通用方案

动态获取entry和设置html-webpack-plugin数量,需要约定存放地址及约定特定的入口文件,如下:

module.exports = {
  entry: {
    index: './src/index/index.js',
    search: './src/search/index.js ‘
  }
};

需要安装glob库,来对entry进行配置:

npm i glob -D

webpack需要做如下配置:

const glob = require('glob');
const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];
  
  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));// 用于同步多页面文件
  
  Object.keys(entryFiles).map((index)=>{
    const entryFile = entryFiles[index];
    // '/Users/brysonlin/myProject/src/index/index.js'
    const match = entryFile.match(/src\(.*)\/index\.js/);// 匹配出src和index.js中间的pageName
    const pageName = match && match[1];
    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugins({
        template:path.join(__dirname,`src/${pageName}/index.html`),
        filename:`${pageName}.html`,
        chunks:[pageName],
        inject:true,
        minify:{
          html5:true,
          collapseWhitespace:true,
          preserveLineBreaks:false,
          minifyCSS:true,
          minifyJS:true,
          removeComments:false
        }
      })
    );
  })
  return {
    entry,
    htmlWebpackPlugins
  }
}

const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
  entry:entry,
  ...
    plugins:[
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp:/\.css$/g,
      cssProcessor:require('cssnano')
    });
  ].concat(htmlWebpackPlugins)
}

使用source map

作用:通过source map可以定位到源代码。

source map科普文可看阮一峰老师的《Source Map详解》。

source map可以在开发环境开启,线上环境关闭,线上排查问题的时候可以将source map上传到错误监控系统。

webpack中source map五个关键字:

  • eval:使用eval包裹模块代码
  • source map:产生.map文件
  • cheap:不包含列信息
  • inline:将.map作为DataURI嵌入,不单独生成.map文件
  • module:包含loader的sourcemap

经过上面五个关键字的排列组合,可得出以下:

module.exports = {  entry:entry,  ...+ devtool:'cheap-source-map'}

提取页面公共资源

每个页面的有着相同的基础库或者公共模块,如果把所有的文件都打包是一件很浪费的事情,而且打包出来的体积会很大,因此我们需要对一些基础库或者公共的模块进行分离。

基础库分离

平时在写react时可以将react、react-dom基础包通过cdn引入,不打入bundle中,方法是使用html-webpack-externals-plugin,webpack配置如下:

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');module.exports = {  plugin:[    new HtmlWebpackExternalsPlugin({      externals:[        {          module:'react',          entry:'//xxx.cdn.cn/lib/16.8.0/react.min.js',          global:'React'        },{          module:'react-dom',          entry:'//xxx.cdn.cn/lib/16.8.0/react-dom.min.js',          global:'ReactDOM'        }      ]    })  ]}

利用SplitChunksPlugin进行公共脚本分离

我们也可以利用Webpack4内置的SplitChunksPlugin进行公共脚本分离,下面是chunks参数说明:

  • async 异步引入的库进行分离(默认),比如ES6中动态import的包
  • initial 同步引入的库进行分离
  • all 所有引入的库进行分离(推荐)

webpack配置如下:

module.exports = {  optimization: {    splitChunks: {      chunks: 'async',      minSize: 30000, // 分离的包体积的大小      maxSize: 0,      minChunks: 1, //  设置最⼩引⽤次数为1次      maxAsyncRequests: 5,      maxInitialRequests: 3, // 浏览器同时请求异步资源的次数      automaticNameDelimiter: '~',      name: true,      cacheGroups: {        vendors: {          test: /[\\/]node_modules[\\/]/,           priority: -10        }      }    }  }};

利用SplitChunksPlugin进行分离基础包

分离出react及react-dom基础包,同时需要在HtmlWebpackPlugins中的chunks引入名称,webpack配置如下:

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /(react|react-dom)/,
          name: 'vendors', // 需要在HtmlWebpackPlugins的chunks中引入
          chunks: 'all'
        }
      }
    }
  }
};

利用SplitChunksPlugin进行分离页面公共文件

如果在两个页面同时使用了同一个文件,我们可以在打包时对其进行分离页面公共文件,webpack配置如下:

module.exports = {
  optimization: {
    splitChunks: {
      minSize: 20, // 设置包的体积至少为20kb时才进行分离
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'all',
          minChunks: 2 // 设置最小引用次数为2才进行分离
        }
      }
    }
  }
};

Tree-shaking(摇树优化)

什么是Tree-shaking?

tree-shaking又叫摇树优化,就是不断摇树,就其没用的叶子摇下来,其作用就是写的代码中有些没有用到,因此webpack进行构建的时候就会将其擦除(一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里面去,tree-shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉)。

使用:webpack默认支持,在.bablerc里设置module: false即可,同时在mode为production时默认开启。但是必须是ES6的语法,不支持CommonJS的语法。

Tree-shaking原理

要知道tree-shaking原理,首先我们要了解一下死码消除DCE(Dead code elimination)。

死码消除DCE

死码消除(Dead code elimination)是一种编译器原理中编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。有以下三种情况:

  • 代码不会被执行,不可到达,如:
if(false) console.log('这段代码永远不会执行')
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

而Tree-shaking主要是利用了ES6模块的特点:

  • 只能作为模块顶层的语句出现
  • import的模块名只能是字符串常量
  • import binding 是immutable的(不可变的)

webpack会在每个没有使用到的代码加一些注释标记,如何在uglify阶段时进行代码擦除。

Scope Hoisting使用和原理分析

首先,让我们看以下没有使用Scope hoisting之前构建后的代码,下图:

从中我们可以看出,没有开启Scope hoisting之前构建后的代码存在大量的闭包代码,这样会导致:

  • 大量作用域包裹代码,导致体积增大(模块越多越明显)
  • 运行代码时创建的函数作用域变多,内存开销变大

让我们对webpack打包后模块的转换进行分析:

webpack会将模块打包成模块初始化函数(这是因为浏览器需要解析构建工具转换后的函数),整个模块转换主要分为两件事情:

  1. 被webpack转换后的模块会带上一层包裹
  2. import会被转换成__webpack_require
(function(modules){ // modules时一个数组,每一项是一个模块初始u哈函数
  var installedModules = {};
  
  function __webpack_require__(moduleId){ // __webpack_require__用来加载模块,返回module.exports
    if(installedModules[moduleId]) // 通过模块id去查看installedModules中是否存在某个module
      return installedModules[moduleId].exports;
    var module = installedModules[moduleId] = { // 如果installedModules中不存在某个module,会创建一个新的module对象
      i:moduleId,
      l:false,
      exports:{}
    };
    modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);
    module.l=true;
    return module.exports;
  }
  __webpack_require__(0); // 通过这段代码启动程序
})([
  /* 0 module */
  (function(module,__webpack_exports__,__webpack_require__){
    ...
  })
  /* 1 module */
  (function(module,__webpack_exports__,__webpack_require__){
    ...
  })
  /* n module */
  (function(module,__webpack_exports__,__webpack_require__){
    ...
  })
]);

打包出来的是一个IIFE(匿名闭包)。

在我们了解webpack模块机制之后,让我们看看能否对包裹的代码进行优化。

scope hoisting原理

原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。

作用:通过scope hoisting可以减少函数声明代码和内存开销

如何使用:webpack mode为production默认开启,必须是ES6语法,不支持CJS。我们也可以手动引入scope hoisting功能,webpack配置如下:

const webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name][chunkhash:8].js',
    path: __dirname + '/dist'
  },
  plugins: [
+   new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

代码分割和动态import

在前面SplitChunksPlugin时提取基础包及多页面公共文件时属于代码分割的内容,接下来我们将要讲解另外一个代码分割的手段:通过动态import或者CJS中的一些语法。代码分割的意义是对于大的Web应用而言,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码是在某些特殊的时候才会被使用到。webpack有一个功能就是将你的代码库分割成chunks(语块),当代码运行到需要它们的时候再进行加载。

适用场景:

  • 抽离相同代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

懒加载JS脚本的方式

CommonJS:require.ensure

ES6:动态import(目前还没有原生支持,需要babel转换)

如何使用动态import

安装babel插件

npm install @babel/plugin-syntax-dynamic-import --save-dev

ES6:动态import(目前还没有原生支持,需要babel转换)

// .babelrc
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"],
}

webpack中使用ESLint

使用eslint-loader,构建时检查JS规范,webpack配置如下:

module.exports = { 
  module: {
    rules: [{
        test: /\.js$/,
        exclude: /node_modules/, 
        use: [
          "babel-loader",
+         "eslint-loader”
        ]
      }
    }]
  }
}

可以看比较热门的eslint规范,如eslint-config-airbnb。

webpack打包库和组件

webpack除了能用来打包应用,也可以用来打包js库。

接下来让我们看一下如何实现一个大整数加法库的打包,有以下两个要求:

  • 需要打包压缩版和非压缩版
  • 支持AMD/CJS/EMS模块引入

库的目录结构和打包要求

+|- /dist
+ |- large-number.js
+ |- large-number.min.js
+|- webpack.config.js 
+|- package.json
+|- index.js
+|- /src
+ |- index.js
  • 未压缩版 large-number.js
  • 压缩版 large-number.min.js

支持的使用方式

支持ES module

import * as largeNumber from 'large-number'
// ...
largeNumber.add('999','1');

支持CJS

const largeNumbers = require('large-number');
// ...
largeNumber.add('999','1');

支持AMD

require(['large-number'],function(large-number){
  // ...
  largeNumber.add('999','1');
})

可以直接通过script引入

<!doctype html>
<html>
...
<script src="https://unpkg.com/large-number"></script> <script>
  // ...
  // Global variable 
  largeNumber.add('999', '1');
  // Property in the window object 
  window.largeNumber.add('999', '1'); // ...
</script> 
</html>

实现步骤

  • 创建文件,安装需要的依赖
mkdir large-number $$ cd large-number
npm init -y
npm i webpack webpack-cli terser-webpack-plugin -D
  • 创建如下文件目录

  • 实现大整数加法功能
// index.js
export default function add(a, b) {
    let i = a.length - 1;
    let j = b.length - 1;
    let carry = 0; // 代表进位
    let ret = ''; //结果
    while (i >= 0 || j >= 0) {
        let x = 0; // a位数上的值
        let y = 0; // b位数上的值
        let sum; //两次求和的值

        if (i >= 0) {
            x = a[i] - '0'; // 字符串转数字
            i--;
        }
        if (j >= 0) {
            y = b[j] - '0';
            j--;
        }

        sum = x + y + carry;

        if (sum >= 10) {
            carry = 1;
            sum -= 10;
        } else {
            carry = 0;
        }

        ret = sum + ret;
    }
    if (carry) {
        ret = carry + ret;
    }
    return ret;
}
// console.log(add('1','999999999999999999999999999999999999999'));
//1000000000000000000000000000000000000000
  • 对webpack进行配置,如下:
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  entry:{
    'large-number':'./src/index.js',
    'large-number.min':'./src/index.js'
  },
  output:{
    filename:'[name].js',
    library:'largeNumber', //打包出来的库的名称
    libraryTarget:'umd', // 支持umd
    libraryExport:'default'
  },
  mode:‘none’,
  optimization:{
    minimize:true,
    minimizer:[
      new TerserPlugin({ // 这个插件更适合ES6语法
        include:/\.min\.js$/, //只压缩.min.js的文件
      })
    ]
  }
}
  • 设置入口文件

package.json的main字段为index.js

if (process.env.NODE_ENV === "production") { 
  module.exports = require("./dist/large-number.min.js");
} else {
  module.exports = require("./dist/large-number.js");
}
  • 发布到npm(可选)

在package.json中设置scripts,这样子我们将这个功能发布到npm时,也能进行打包。

"script":{
  "prepublist":'webpack'  
}
// 运行 npm publish

至此,我们就可以在项目中拉取我们发布的npm包进行使用啦。

优化构建时命令行的显示日志

在平时我们打包构建时都会产生很多日志,而大部分我们不需要去关注。

在webpack中我们可以设置这些参数去让打包构建时只显示或者不显示一些信息。

当然,也可以使用friendly-errors-webpack-plugin这个插件去做一些优化,webpack配置如下:

module.exports = {
  plugins:[
+   new FriendlyErrorsWebpackPlugins()
  ],
+ stats:'errors-only'
}

效果如下:

以上就是文章的全部内容啦~接下来还会对webpack进行更深入的讲解。

参考资料:程柳锋《玩转webpack》


如果文章对您有帮助,请帮忙点赞哟!