万字长文,带你从零学习Webpack

一直觉得,个人Webpack就是复制粘贴的水平,而对Webpack的知识真的很模糊,甚至是纯小白。因此前段时间开始对Webpack进行比较系统的学习。javascript

学习完成后,我抽空整理了笔记,前先后后也花了一周多。最后以为能够分享出来,让对Webpack还很模糊的朋友,能够学习一下。css

固然,读完本文,你会发现Webpack还有更多更深的东西值得咱们去学习,所以这只是一个开始,从零开始。html

module、chunk和bundle

在学习webpack以前,咱们须要先来捋一捋三个术语——modulechunkbundlejava

过一下概念

module

先看看webpack官方对module的解读:node

Module是离散功能块,相比于完整程序提供了更小的接触面。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每一个模块都具备条理清楚的设计和明确的目的。jquery

其实简单来讲,module模块就是咱们编写的代码文件,好比JavaScript文件、CSS文件、Image文件、Font文件等等,它们都是属于module模块。而module模块的一个特色,就是能够被引入使用。webpack

chunk

一样的先看看官方解读:git

webpack 特定术语在内部用于管理捆绑过程。输出束(bundle)由块组成,其中有几种类型(例如 entrychild )。一般,块直接与输出束 (bundle)相对应,可是,有些配置不会产生一对一的关系github

其实chunkwebpack打包过程的中间产物,webpack会根据文件的引入关系生成chunk,也就是说一个chunk是由一个module或多个module组成的,这取决于有没有引入其余的moduleweb

Bundle

先看看官方解读:

bundle 由许多不一样的模块生成,包含已经通过加载和编译过程的源文件的最终版本。

bundle实际上是webpack的最终产物,一般来讲,一个bundle对应这一个chunk

总结

其实modulechunkbundle能够说是同一份代码在不一样转换场景的不一样名称:

  • 咱们编写的是module
  • webpack处理时时chunk
  • 最终生成供使用的是bundle

实践一下

咱们经过一个小demo来过一下,如今有一个项目,路径以下:

src/
├── index.css
├── index.js
├── common.js
└── utils.js
复制代码

而后咱们有两个入口文件,一个是index.js,一个是utils.js,在index.js中引入了index.csscommon.js。而后经过webpack打包出来了index.bundle.cssindex.bundle.jsutils.bundle.js

好,介绍完背景后,咱们就能够来分析一下modulechunkbundle

首先,咱们编写的代码,就是module,也就是说index.csscommon.jsindex.jsutils.js共四个module文件。

其次,咱们有两个入口文件,分别为index.jsutils.js,而且它们最后是独立打包成bundle的,从而在webpack打包过程当中就会造成两个chunk文件,而由index.js造成chunk还包含着index.js引入的module——common.jsindex.css

最后,咱们打包出来了index.bundle.cssindex.bundle.jsuitls.bundle.js,这三个也就是bundle文件。

module-chunk-bundle.png

最后,咱们能够总结一下三者之间的关系:一个budnle对应着一个chunk,一个chunk对应着一个或多个module

初始化Webpack项目

接下来,咱们经过一步步实践,来慢慢学习webpack,这篇文章使用的是webpack5

首先,新建一个项目文件夹,而后初始化项目。

yarn init -y
复制代码

而后安装一下webpack。当咱们使用webpack时,还须要安装webpack-cli

由于webpack只是在开发环境才会使用到,因此咱们只须要添加到devDependencies便可。

# webpack -> 5.47.0, webpack-cli-> 4.7.2
yarn add webpack webpack-cli -D
复制代码

而后再项目中新建src路径,再新建一个index.js

console.log("Hello OUDUIDUI");
复制代码

而后执行npx webpack,则执行webpack打包。这时你的项目就会多一个dist文件夹,而且在dist文件夹中会看到一个main.js,里面的代码跟index.js同样。

固然,咱们能够在package.json中编辑script命令:

"scripts": {
  "dev": "webpack"
}
复制代码

而后执行yarn dev,也能够成功打包。

Webpack配置文件

若是使用过webpack的朋友应该知道,webpack其实有一个配置文件——webpack.config.js

但为何前面的初始化测试时,咱们没有编辑配置文件却能够成功打包?这是由于webpack会有一个默认配置,当它检测到咱们没有配置文件的时候,它默认会使用本身的默认配置。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',
  },
};
复制代码

首先,咱们简单来过一下这些默认配置叭。

entry和output

entry选项是用来配置入口文件的,它能够是字符串、数组或者对象类型。webpack默认只支持jsjson文件做为入口文件,所以若是引入其余类型文件会保存。

output选项是设置输出配置,该选项必须是对象类型,不能是其它类型格式。在output对象中,必填的两个选项就是导出路径path和导出bundle文件名称filename。其中path选项必须为绝对路径。

entryoutput的配置,对于不一样的应用场景的配置也会有所不一样。

单入口单输出

咱们最广泛的就是单个入口文件,而后打包成单个bundle文件。这种应用场景下,entry可使用字符串的形式,则跟默认配置文件相似:

entry: './src/index.js'
复制代码

多入口单输出

当咱们的项目须要有多个入口文件,但只须要一个输出bundle的时候,这时候entry可使用数组的形式:

entry: ['./src/index_1.js', './src/index_2.js']
复制代码

注意:此时其实只有一个chunk

多入口多输出

当咱们的项目同时多个入口文件,而且它们须要单独打包,也就是意味着会有多个bundle文件输出,此时咱们的entry须要使用对象形式,而且对象key对应的对应chunk的名称。

entry: {
  index: "./src/index.js",  // chunkName为index
  main: "./src/main.js"     // chunkName为main
}
复制代码

此时,咱们的output.filename也不能写死了,这时候webpack提供了一个占位符[name]给咱们使用,它会自动替换为对应的chunkName

output: {
   path: path.resolve(__dirname, 'dist'),
   filename: '[name].js'  // [name]占位符会自动替换为chunkName
},
复制代码

根据上面的配置,最后会打包出index.jsmain.js

补充

在单入口单输出的应用场景下,entry也可使用对象的形式,从而来自定义chunkName,而后output.filename也使用[name]占位符来自动匹配。固然也可使用数组,可是不太大必要。

entry使用数组或字符串的时候,chunkName默认为main,所以若是output.filename使用[name]占位符的时候,会自动替换为main

mode

在前面的打包测试的时候,命令行都会报一个警告:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
复制代码

这是由于webpack须要咱们配置mode选项。

wepack给咱们提供了三个选项,即nonedevelopmentproduction,而默认就是production

三者的区别呢,在于webpack自带的代码压缩和优化插件使用。

  • none:不使用任何默认优化选项;

  • development:指的是开发环境,会默认开启一些有利于开发调试的选项,好比NamedModulesPluginNamedChunksPlugin,分别是给modulechunk命名的,而默认是一个数组,对应的chunkName也只是下标,不利于开发调试;

  • production:指的是生产环境,则会开启代码压缩和代码性能优化的插件,从而打包出来的文件也相对nonedevelopment小不少。

当咱们设置mode以后,咱们能够在process.env.NODE_ENV获取到当前的环境

所以咱们能够在配置文件上文件上配置mode

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
  	// 开启source-map
    devtool: "source-map"
};
复制代码

webpack也给咱们提供了另外一种方式,就是在命令行中配置,也就是加上--mode

// package.json
"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}
复制代码

devtool

聊完mode后,说到开发调试,不难想起的就是sourceMap。而咱们能够在配置文件中,使用devtool开启它。

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
  	// 开启source-map
    devtool: "source-map"
};
复制代码

打包后,你的dist中就会多了一个main.js.map文件。

固然,官方不止提供这么一个选项,具体的能够去官网看看,这里就说其余几个比较经常使用的选项。

  • none:不会生成sourceMap

  • eval:每一个模块都会使用eval()执行,不建议生成环境中使用;

  • cheap-source-map:生成sourceMap,可是没有列映射,则只会提醒是在代码的第几行,不会提示到第几列;

  • inline-source-map:会生成sourceMap,但不会生成map文件,而是将sourceMap放在打包文件中。

module

前面咱们有提到过,就是webpack的入口文件只能接收JavaScript文件和JSON文件。

但咱们一般项目还会有其余类型的文件,好比htmlcss、图片、字体等等,这时候咱们就须要用到第三方loader来帮助webpack来解析这些文件。理论上只要有相应的loader,就能够处理任何类型的文件。

webpack官网其实提供了不少loader,已经能知足咱们平常使用,固然咱们也能够去github找找别人写的loader或者本身手写loader来使用。

而对于loader的配置,是写着module选项里面的。module选项是一个对象,它里面有一个rules属性,是一个数组,在里面咱们能够配置多个匹配规则。

而匹配规则是一个对象,会有test属性和use属性,test属性通常是正则表达式,用来识别文件类型,而use属性是一个数组,里面用来存放对该文件类型使用的loader

module: {
    rules: [
        {
          test: /\.css$/,  // 识别css文件
          use: ['style-loader', 'css-loader']  // 对css文件使用的三个loader
        }
    ]
}
复制代码

对于use数组的顺序是有要求的,webpack会根据自后向前的规则去执行loader。也就是说,上面的例子webpack会先执行css-loader,再执行style-loader

其次,当咱们须要对对应loader提供配置的时候,咱们能够选用对象写法:

module: {
    rules: [
        {
          test: /\.css$/,  
          use: [
            'style-loader', 
            {
              	// loader名称
              	loader: 'css-loader',
              	// loader选项
              	options: {
                  	... 
                }
            }
          ] 
        }
    ]
}
复制代码

在后面咱们根据实际应用场景再讲讲module的使用。

plugins

webpack还提供了一个plugins选项,让咱们可使用一些第三方插件,所以咱们可使用第三方插件来实现打包优化、资源管理、注入环境变量等任务。

一样的,webpack官方也提供了不少plugin

plugins选项是一个数组,里面能够放入多个plugin插件。

plugins: [
  new htmlWebpackPlugin(),
  new CleanWebpackPlugin(),
  new miniCssExtractPlugin(),
  new TxtWebpackPlugin()
]
复制代码

而对于plugins数组对排序位置是没有要求,由于在plugin的实现中,webpack会经过打包过程的生命周期钩子,所以在插件逻辑中就已经设置好须要在哪一个生命周期执行哪些任务。

实现一下常见的应用场景

HTML模板

当咱们是Web项目的时候,咱们必然会存在html文件去实现页面。

而对于其余类型的文件,好比css、图片、文件等等,咱们是能够经过引入入口js文件,而后经过loader进行解析打包。而对于html文件,咱们不可能将其引入到入口文件而后解析打包,反而咱们还须要将打包出来的bundle文件引入html文件去使用,

所以,其实咱们须要实现的操做只有两个,一个是复制一份html文件到打包路径下,另外一个就是将打包出来的bundle文件自动引入到html文件中去。

这时候咱们须要使用一个插件来实现这些功能——html-webpack-plugin

# 5.3.2
yarn add html-webpack-plugin -D
复制代码

安装插件后,咱们先在src文件下新建一下index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Demo</title>
</head>
<body>
    <div>Hello World</div>
</body>
</html>
复制代码

这里面咱们暂时不须要引入任何模块。

接下来配置一下webpack。通常plugin插件都是一个类,而咱们须要在plugins选项中须要建立一个插件实例。

对于htmlWebpackPlugin插件,咱们须要传入一些配置:html模板地址template和打包出来的文件名filename

const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    plugins: [
      	// 使用htmlWebpackPlugin插件
        new htmlWebpackPlugin({
         	 // 指定html模板
            template: './src/index.html',  
          	// 自定义打包的文件名
            filename: 'index.html'
        })
    ]
};
复制代码

接下来执行一下打包,就会发现dist文件下会生成一个index.html。打开会发现,webpack会自动将bundle文件引入:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Demo</title>
<script defer src="main.js"></script></head>
<body>
    <div>Hello World</div>
</body>
</html>
复制代码

若是咱们有多个chunk的时候,咱们能够指定该html要引入哪些chunk。在htmlWebpackPlugin配置中有一个chunks选项,是一个数组,你只须要加入你想引入的chunkName便可。

const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: {
      	index: './src/index.js',
      	main: './src/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    plugins: [
        new htmlWebpackPlugin({
            template: './src/index.html',  
            filename: 'index.html',
          	chunks: ["index"]  // 只引入index chunk
        })
    ]
};
复制代码

打包完成后,dist文件下会出现index.htmlindex.jsmain.js,可是index.html只会引入index.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
<script defer src="index.js"></script></head>
<body>
    HelloWorld!
</body>
</html>
复制代码

若是咱们须要实现多页面的话,只须要再new一个htmlWebpackPlugin实例便可,这里就再也不多说。

清理打包路径

在每次打包前,咱们其实都须要去清空一下打包路径的文件。

若是文件重名的话,webpack还会自动覆盖,可是实际中咱们都会在打包文件名称中加入哈希值,所以清空的操做不得不实现。

这时候咱们须要使用一个插件——clean-webpack-plugin

yarn add clean-webpack-plugin -D
复制代码

而后只需引入到配置文件且在plugins配置就可使用了。

const path = require('path');
// 引入CleanWebpackPlugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js',
        publicPath: ''
    },
    plugins: [
      	// 使用CleanWebpackPlugin
        new CleanWebpackPlugin(),
    ]
};
复制代码

有些状况下,咱们不须要彻底清空打包路径,这时候咱们可使用到一个选项,叫cleanOnceBeforeBuildPatterns,它是一个数组,默认是[**/*],也就是清理output.path路径下全部东西。而你能够在里面输入只想删除的文件,同时咱们能够输入不想删除的文件,只须要在前面加上一个!

须要注意的是,cleanOnceBeforeBuildPatterns这个选项是能够删除打包路径下以外的文件,只须要你配上绝对路径的话。所以CleanWebpackPlugin还提供了一个选项供测试——dry,默认是为false,当你设置为true后,它就不会真正的执行删除,而是只会在命令行打印出被删除的文件,这样子更好的避免测试过程当中误删。

const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js',
        publicPath: ''
    },
    plugins: [
        new CleanWebpackPlugin({
          	// dry: true // 打开可测试,不会真正执行删除动做
            cleanOnceBeforeBuildPatterns: [
                '**/*',  // 删除dist路径下全部文件
                `!package.json`,  // 不删除dist/package.json文件
            ],
        }),
    ]
};
复制代码

Webpack本地服务

当咱们使用webpack的时候,不只仅只是用于打包文件,大部分咱们还会依赖webpack来搭建本地服务,同时利用其热更新的功能,让咱们更好的开发和调试代码。

接下来咱们来安装一下webpack-dev-server

# 版本为3.11.2
yarn add webpack-dev-server -D
复制代码

而后执行下列代码开启服务:

npx webpack serve
复制代码

或者在package.json配置一下:

"scripts": {
  "serve": "webpack serve --mode development"
}
复制代码

而后经过yarn serve运行。

这时,webpack会默认开启http://localhost:8080/服务(具体看大家运行返回的代码),而该服务指向的是dist/index.html

但你会发现,你的dist中实际上是没有任何文件的,这是由于webpack将实时编译后的文件都保存到了内存当中。

webpack-dev-server的好处

其实webpack自带提供了--watch命令,能够实现动态监听文件的改变并实时打包,输出新的打包文件。

但这种方案存在着几个缺点,一就是每次你一修改代码,webpack就会所有文件进行从新打包,这时候每次更新打包的速度就会慢了不少;其次,这样的监听方式作不到热更新,即每次你修改代码后,webpack从新编译打包后,你就得手动刷新浏览器,才能看到最新的页面结果。

webpack-dev-server,却有效了弥补这两个问题。它的实现,是使用了express启动了一个http服务器,来伺候资源文件。而后这个http服务器和客户端使用了websocket通信协议,当原始文件做出改动后,webpack-dev-server就会实时编译,而后将最后编译文件实时渲染到页面上。

webpack-dev-server配置

webpack.config.js中,有一个devServer选项是用来配置webpack-dev-server,这里简单讲几个比较经常使用的配置。

port

咱们能够经过port来设置服务器端口号。

module.exports = {
  
    ...
  
    // 配置webpack-dev-server
    devServer: {
        port: 8888,  // 自定义端口号
    },
};
复制代码

open

devServer中有一个open选项,默认是为false,当你设置为true的时候,你每次运行webpack-dev-server就会自动帮你打开浏览器。

module.exports = {
  
    ...
  
    // 配置webpack-dev-server
    devServer: {
        open: true,   // 自动打开浏览器窗口
    },
};
复制代码

proxy

这个选项是用来设置本地开发的跨域代理的,关于跨域的知识就很少在这说了,这里就说说如何去配置。

proxy的值必须是一个对象,在里面咱们能够配置一个或多个跨域代理。最简单的配置写法就是地址配上api地址。

module.exports = {
  
    ...
  
    devServer: {
      	// 跨域代理
        proxy: {
          '/api': 'http://localhost:3000'
        },
    },
};
复制代码

这时候,当你请求/api/users的时候,就会代理到http://localhost:3000/api/users

若是你不须要传递/api的话,你就须要使用对象的写法,从而增长一些配置选项:

module.exports = {
    //...
    devServer: {
      	// 跨域代理
        proxy: {
            '/api': {
              target: 'http://localhost:3000',  // 代理地址
              pathRewrite: { '^/api': '' },   // 重写路径
            },
        },
    },
};
复制代码

这时候,当你请求/api/users的时候,就会代理到http://localhost:3000/users

在proxy中的选项,还有两个比较经常使用的,一个就是changeOrigin,默认状况下,代理时会保留主机头的来源,当咱们将其设置为true能够覆盖这种行为;还有一个是secure选项,当你的接口使用了https的时候,须要将其设置为false

module.exports = {
    //...
    devServer: {
      	// 跨域代理
        proxy: {
            '/api': {
              target: 'http://localhost:3000',  // 代理地址
              pathRewrite: { '^/api': '' },   // 重写路径
              secure: false,  // 使用https
              changeOrigin: true   // 覆盖主机源
            },
        },
    },
};
复制代码

CSS处理

接下来说讲关于webpackcss的解析处理叭。

解析CSS文件

在前面的例子也能看到,咱们解析css须要用到的loadercss-loaderstyle-loadercss-loader主要用来解析css文件,而style-loader是将css渲染到DOM节点上。

首先咱们来安装一下:

 # css-loader -> 6.2.0; style-loader -> 3.2.1
 yarn add css-loader style-loader -D
复制代码

而后咱们新建一个css文件。

/* style.css */
body {
  background: #222;
  color: #fff;
}
复制代码

而后在index.js引入一下:

import "./style.css";
复制代码

紧接着咱们配置一下webpack

module.exports = {
   ...
  
  module: {
    rules: [
      {
        test: /\.css$/,  // 识别css文件
        use: ['style-loader', 'css-loader']  // 先使用css-loader,再使用style-loader
      }
    ]
  },
  
   ...
};
复制代码

这时候咱们打包一下,会发现dist路径下只有main.jsindex.html。但打开一下index.html会发现css是生效的。

demo1.png

这是由于style-loader是将css代码插入到了main.js当中去了。

打包css文件

若是咱们不想将css代码放进js中,而是直接导出一份css文件的话,就得使用另外一个插件——mini-css-extract-plugin

# 2.1.0
yarn add mini-css-extract-plugin -D
复制代码

而后将其引入到配置文件,而且在plugins引入。

const miniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
  
    plugins: [
      	// 使用miniCssExtractPlugin插件
        new miniCssExtractPlugin({
          	filename: "[name].css"  // 设置导出css名称,[name]占位符对应chunkName
        })
    ]
};
复制代码

紧接着,咱们还须要更改一下loader,咱们再也不使用style-loader,而是使用miniCssExtractPlugin提供的loader

const miniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
  
    module: {
        rules: [
            {
                test: /\.css$/,
              	// 使用miniCssExtractPlugin.loader替换style-loader
                use: [miniCssExtractPlugin.loader,'css-loader']
            }
        ]
    },
    plugins: [
        new miniCssExtractPlugin({
          	filename: "[name].css" 
        })
    ]
};
复制代码

接下来打包一下,dist路径下就会多出一个main.css文件,而且在index.html中也会自动引入。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
<script defer src="main.js"></script><link href="main.css" rel="stylesheet"></head>
<body>
    HelloWorld!
</body>
</html>
复制代码

css添加浏览器前缀

当咱们使用一下css新特性的时候,可能须要考虑到浏览器兼容的问题,这时候可能须要对一些css属性添加浏览器前缀。而这类工做,其实能够交给webpack去实现。准确来讲,是交给postcss去实现。

postcss对于css犹如babel对于JavaScript,它专一于对转换css,好比添加前缀兼容、压缩css代码等等。

首先咱们须要先安装一下postcsspost-css-loader

# postcss -> 8.3.6,postcss-loader -> 6.1.1
yarn add postcss postcss-loader -D
复制代码

接下来,咱们在webpack配置文件先引入postcss-loader,它的顺序是在css-loader以前执行的。

rules: [
  {
    test: /\.css$/,
    // 引入postcss-loader
    use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
  }
]
复制代码

接下来配置postcss的工做,就不在webpack的配置文件里面了。postcss自身也是有配置文件的,咱们须要在项目根路径下新建一个postcss,config.js。而后里面也有一个配置项,为plugins

module.exports = {
    plugins: []
}
复制代码

这也意味着,postcss自身也支持不少第三方插件使用。

如今咱们想实现的添加前缀的功能,须要安装的插件叫autoprefixer

# 1.22.10
yarn add autoprefixer -D
复制代码

而后咱们只须要引入到postcss的配置文件中,而且它里面会有一个配置选项,叫overrideBrowserslist,是用来填写适用浏览器的版本。

module.exports = {
    plugins: [
        // 将css编译为适应于多版本浏览器
        require('autoprefixer')({
            // 覆盖浏览器版本
          	// last 2 versions: 兼容各个浏览器最新的两个版本
          	// > 1%: 浏览器全球使用占有率大于1%
            overrideBrowserslist: ['last 2 versions', '> 1%']
        })
    ]
}
复制代码

关于overrideBrowserslist的选项填写,咱们能够去参考一下browserslist,这里就很少讲。

固然,咱们其实能够在package.json中填写兼容浏览器信息,或者使用browserslist配置文件.browserslistrc来填写,这样子若是咱们之后使用其余插件也须要考虑到兼容浏览器的时候,就能够统一用到,好比说babel

// package.json 文件
{
  ...
  "browserslist": ['last 2 versions', '> 1%']
}

复制代码
# .browserslsetrc 文件
last 2 versions
> 1%
复制代码

但若是你多个地方都配置的话,overrideBrowserslist的优先级是最高的。

接下来,咱们修改一下style.css,使用一下比较新的特性。

body {
    display: flex;
}
复制代码

而后打包一下,看看打包出来后的main.css

body {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
}
复制代码

压缩css代码

当咱们须要压缩css代码的时候,可使用postcss另外一个插件——cssnano

# 5.0.7
yarn add cssnano -D
复制代码

而后仍是在postcss配置文件中引入:

module.exports = {
    plugins: [
        ... ,
        require('cssnano')
    ]
}
复制代码

打包一下,看看main.css

body{display:-webkit-box;display:-ms-flexbox;display:flex}
复制代码

解析CSS预处理器

在如今咱们实际开发中,咱们会更多使用SassLess或者stylus这类css预处理器。而其实html是没法直接解析这类文件的,所以咱们须要使用对应的loader将其转换成css

接下来,我就以sass为例,来说一下如何使用webpack解析sass

首先咱们须要安装一下sasssass-loader

# sass -> 1.36.0, sass-loader -> 12.1.0
yarn add sass sass-loader -D
复制代码

而后咱们在module加上sass的匹配规则,sass-loader的执行顺序应该是排第一,咱们须要先将其转换成css,而后才能执行后续的操做。

rules: [
  ...
  
  {
    test: /\.(scss|sass)$/,
    use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
  }
]
复制代码

而后咱们在项目中新建一个style.scss

$color-white: #fff;
$color-black: #222;

body {
    background: $color-black;

    div {
        color: $color-white;
    }
}
复制代码

而后在index.js引入。

import "./style.css";
import "./style.scss";
复制代码

而后执行打包,再看看打包出来的main.cssscss文件内容被解析到里面,同时若是咱们引入多个csscss预处理器文件的话,miniCssExtractPlugin也会将其打包成一个bundle文件里面。

body{display:-webkit-box;display:-ms-flexbox;display:flex}
body{background:#222}body div{color:#fff}
复制代码

其余静态资源处理

当咱们使用了图片、视频或字体等等其余静态资源的话,咱们须要用到url-loaderfile-loader

# url-loader -> 4.1.1; file-loader -> 6.2.0
yarn add url-loader file-loader -D
复制代码

首先咱们在项目中引入一张图片,而后在引入到index.js中。

import pic from "./image.png";

const img = new Image();
img.src= pic;
document.querySelector('body').append(img);
复制代码

而后我先使用url-loader

module.exports = {
  ...
  
  module: {
    rules: [
      {
        test: /\.(png|je?pg|gif|webp)$/,
        use: ['url-loader']
      }
    ]
  }
};
复制代码

而后执行一下打包。

你会发现,dist路径下没有图片文件,可是你打开页面是能够看到图片的,且经过调试工具,咱们能够看到其实url-loader默认会将静态资源转成base64

demo2.png

固然,url-loader选项有提供一个属性,叫limit,就是咱们能够设置一个文件大小阈值,当文件大小超过这个值的时候,url-loader就不会转成base64,而是直接打包成文件。

module.exports = {
  ...
  
  module: {
    rules: [
      {
        test: /\.(png|je?pg|gif|webp)$/,
        use: [{
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',   // 使用占位符设置导出名称
            limit: 1024 * 10  // 设置转成base64阈值,大于10k不转成base64
          }
        }]
      }
    ]
  }
};
复制代码

这时候咱们再打包一下,dist文件夹下就会出现了图片文件。

file-loader其实跟url-loader差很少,但它默认就是导出文件,而不会导出base64的。

module.exports = {
  ...
  
  module: {
    rules: [
      {
        test: /\.(png|je?pg|gif|webp)$/,
        use: ['file-loader']
      }
    ]
  }
};
复制代码

打包一下,会发现dist文件夹下依旧会打包成一个图片文件,可是它的名称会被改为哈希值,咱们能够经过options选项来设置导出的名称。

module.exports = {
  ...
  
  module: {
    rules: [
      {
        test: /\.(png|je?pg|gif|webp)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',   // 使用占位符设置导出名称
          }
        }]
      }
    ]
  }
};
复制代码

而对于视频文件、字体文件,也是用相同的方法,只不过是修改test

module.exports = {
  ...
  module: {
    rules: [
      // 图片
      {
        test: /\.(png|je?pg|gif|webp)$/,
        use: {
          loader: 'url-loader',
          options: {
            esModule: false,
            name: '[name].[ext]',
            limit: 1024 * 10
          }
        }
      },
      // 字体
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',
            limit: 1024 * 10
          }
        }
      },
      // 媒体文件
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',
            limit: 1024 * 10
          }
        }
      }
    ]
  }
};
复制代码

但如今有个问题,就是若是直接在index.html引入图片的话,能够顺利打包吗?

答案是不会的,咱们能够测试一下。首先将图片引入index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <img src="./image.png">
</body>
</html>
复制代码

而后执行打包后,打包出来的index.html照样是<img src="./image.png">,可是咱们并无解析和打包出来image.png出来。

这时候咱们须要借助另外一个插件——html-withimg-loader

# 0.1.16
yarn add html-withimg-loader -D
复制代码

而后咱们再添加一条rules

{ test: /\.html$/,loader: 'html-withimg-loader' }
复制代码

这时候打包成功后,dist文件成功将图片打包出来了,可是打开页面的时候,图片仍是展现不出来。而后经过调试工具看的话,会发现

<img src="{"default":"image.png"}">
复制代码

这是由于html-loader使用的是commonjs进行解析的,而url-loader默认是使用esmodule解析的。所以咱们须要设置一下url-loader

{
  test: /\.(png|je?pg|gif|webp)$/,
    use: {
      loader: 'url-loader',
        options: {
          esModule: false,  // 不适用esmodule解析
          name: '[name].[ext]',
          limit: 1024 * 10
        }
    }
}
复制代码

这时候从新打包一下,页面就能成功展现图片了。

Webpack5 资源模块

webpack.docschina.org/guides/asse…

webpack5中,新添了一个资源模块,它容许使用资源文件(字体,图标等)而无需配置额外 loader,具体的内容你们能够看看文档,这里简单讲一下如何操做。

前面的例子,咱们对静态资源都使用了url-loader或者file-loader,而在webpack5,咱们甚至能够须要手动去安装和使用这两个loader,而直接设置一个type属性。

{
  test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
  type: "asset/resource",
}
复制代码

而后打包测试后,静态文件都会直接打包成文件并自动引入,效果跟file-loader一致。

type值提供了四个选项:

  • asset/resource 发送一个单独的文件并导出 URL。以前经过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。以前经过使用 url-loader 实现。
  • **asset/source :**导出资源的源代码。以前经过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。以前经过使用 url-loader,而且配置资源体积限制实现。

同时,咱们能够在output设置输出bundle静态文件的名称:

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: '[name].js',
  // 设置静态bundle文件的名称
  assetModuleFilename: '[name][ext]'
}
复制代码

JavaScript转义

不只仅css须要转义,JavaScript也要为了兼容多浏览器进行转义,所以咱们须要用到babel

# 8.2.2
yarn add babel-loader -D
复制代码

同时,咱们须要使用babel中用于JavaScript兼容的插件:

# @babel/preset-env -> 7.14.9; @babel/core -> 7.14.8; @core-js -> 3.16.0
yarn add @babel/preset-env @babel/core core-js -D
复制代码

接下来,咱们须要配置一下webpack的配置文件。

{
  test: /\.js$/,
  use: ['babel-loader'] 
}
复制代码

而后咱们须要配置一下babel。固然咱们能够直接在webpack.config.js里面配置,可是babel一样也提供了配置文件.babelrc,所以咱们就直接在这边进行配置。

在根路径新建一个.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
      	// 浏览器版本
        "targets": {
          "edge": "17",
          "chrome": "67"
        },
         // 配置corejs版本,但须要额外安装corejs
        "corejs": 3,
        // 加载状况
        // entry: 须要在入口文件进入@babel/polyfill,而后babel根据使用状况按需载入
        // usage: 无需引入,自动按需加载
        // false: 入口文件引入,所有载入
        "useBuiltIns": "usage"
      }
    ]
  ]
}
复制代码

接下来,咱们来测试一下,先修改一下index.js

new Promise(resolve => {
    resolve('HelloWorld')
}).then(res => {
    console.log(res);
})
复制代码

而后执行yarn build进行打包。

在使用babel以前,打包出来的main.js以下。

!function(){"use strict";new Promise((o=>{o("HelloWorld")})).then((o=>{console.log(o)}))}();
复制代码

上面打包代码是直接使用了Promise,而没有考虑到低版本浏览器的兼容。而后咱们打开babel后,执行一下打包命令,会发现代码多出了不少。

而在打包代码中,能够看到webpack使用了polyfill实现promise类,而后再去调用,从而兼容了低版本浏览器没有promise属性问题。

文件归类

在目前咱们的测试代码中,咱们的src文件夹以下:

├── src
│   ├── Alata-Regular.ttf
│   ├── image.png
│   ├── index.html
│   ├── index.js
│   ├── style.css
│   └── style.scss
复制代码

而正常项目的话,咱们会使用文件夹将其分好类,这并不难,咱们先简单归类一下。

├── src
│   ├── index.html
│   ├── js
│   │   └── index.js
│   ├── static
│   │   └── image.png
│   │   └── Alata-Regular.ttf
│   └── style
│       ├── style.css
│       └── style.scss

复制代码

接下来,咱们须要打包出来的文件也是归类好的,这里就不太复杂,直接用一个assets文件夹将全部静态文件放进去,而后index.html放外面。

├── dist
│   ├── assets
│   │   ├── Alata-Regular.ttf
│   │   ├── image.png
│   │   ├── main.css
│   │   └── main.js
│   └── index.html
复制代码

这里先补充一下style.css引入字体的代码:

@font-face {
    font-family: "test-font";
    src: url("../static/Alata-Regular.ttf") format('truetype')
}

body {
    display: flex;
    font-family: "test-font";
}
复制代码

首先,咱们先将打包出来的JavaScript文件放入assets文件夹下,咱们只须要修改output.filename便可。

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: 'assets/[name].js'
}
复制代码

其次,咱们将打包出来的css文件也放入assets路径下,由于咱们打包css是使用miniCssExtractPlugin,所以咱们只须要配置一下miniCssExtractPluginfilename便可:

plugins: [
  ...
  new miniCssExtractPlugin({
    filename: "assets/[name].css"
  })
]
复制代码

最后就是静态资源了,这里咱们使用静态模块方案,因此直接修改output.assetModuleFilename便可:

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: 'assets/[name].js',
  assetModuleFilename: 'assets/[name][ext]'
},
复制代码

这时候打包一下,预览一下页面,发现都正常引入和使用。

哈希值

一般,咱们打包文件的文件名都须要带上一个哈希值,这会给咱们的好处就是避免缓存。

webpack也提供了三种哈希值的策略,接下来咱们一一来看看:

前期准备

为了更好的比较三者之间的区别,这边先调整一下项目和配置。

// index.js
import pic from "../static/image.png";

const img = new Image();
img.src = pic;
document.querySelector('body').append(img);

// main.js
import "../style/style.scss";
import "../style/style.css";

console.log('Hello World')


// webpack.config.js
entry: {
  index: './src/js/index.js',
  main: './src/js/main.js'
},
复制代码

hash策略

hash策略,是以项目为单位的,也就是说,只要项目一个文件发生改动,首先打包后该文件对应的bundle文件名会改变,其次全部js文件和css文件的文件名也会改变。

咱们先经过一个例子来看看:

首先咱们须要在全部设置filename的地方加入[hash]占位符,同时咱们也能够设置哈希值的长度,只需加上冒号和长度值便可,好比[hash:6]

module.exports = {
    entry: {
        index: './src/js/index.js',
        main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[hash:6].js',
        assetModuleFilename: 'assets/[name]-[hash:6][ext]'
    },
    module: {
        ...
    },
    plugins: [
        ...
        new miniCssExtractPlugin({
            filename: "assets/[name]-[hash:6].css"
        }),
    ]
};
复制代码

这时候打包一下,看看打包文件:

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-7503bc.png
│   ├── index-7fa71a.js
│   ├── main-7fa71a.css
│   └── main-7fa71a.js
└── index.html
复制代码

而后我随便改一下style.css,再从新打包一下。

这时候会发现index.jsmain.jsmain.css的文件名都会发生改变,但静态文件并不会发生变化。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-7503bc.png
│   ├── index-4b2329.js
│   ├── main-4b2329.css
│   └── main-4b2329.js
└── index.html
复制代码

而后咱们从新找一张图片,覆盖一下image.png,而后从新打包。

这时候,index.jsmain.jsmain.css的文件名依旧会发生改变,同时image.png也发生了变化。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-f3f2ec.png
│   ├── index-46acaa.js
│   ├── main-46acaa.css
│   └── main-46acaa.js
└── index.html
复制代码

经过上面的例子,咱们能够简单总结出:

  • 若是修改项目文件的话,全部的jscss打包文件的文件名都会发生变化,尽管来自多个chunk
  • 若是修改静态文件的话,该静态文件的打包文件文件名会发生变化,而且全部的jscss打包文件的文件名也都会发生变化。

chunkhash策略

chunkhash策略的话,是以chunk为单位的,若是一个文件发生变化,只有那条chunk相关的文件的打包文件文件名才会发生变化。

咱们依旧经过例子看看:

首先咱们先将配置文件都改为chunkhash。这里注意的是chunkhash不适用于静态文件,所以静态文件依旧使用hash

module.exports = {
    entry: {
        index: './src/js/index.js',
        main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[chunkhash:6].js',
        assetModuleFilename: 'assets/[name]-[hash:6][ext]'
    },
    module: {
        ...
    },
    plugins: [
        ...
        new miniCssExtractPlugin({
            filename: "assets/[name]-[chunkhash:6].css"
        }),
    ]
};
复制代码

先打包一次:

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-f3f2ec.png
│   ├── index-6be98e.js
│   ├── main-a15a74.css
│   └── main-a15a74.js
└── index.html
复制代码

而后咱们首先修改一下style.css,打包一下,会发现main.cssmain.js都发生了变化,而index.js不是一个chunk的,所以不会发生变化。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-f3f2ec.png
│   ├── index-6be98e.js
│   ├── main-88f8ea.css
│   └── main-88f8ea.js
└── index.html
复制代码

一样,咱们再覆盖一下image.png,再打包一下。

这时候image.png当然会发生变化,而后index.js也发生了变化,由于它们是一个chunk的,而main.cssmain.js就不会发生变化。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-7503bc.png
│   ├── index-89dfd4.js
│   ├── main-88f8ea.css
│   └── main-88f8ea.js
└── index.html
复制代码

简单总结一下:

  • 若是修改项目文件的话,该项目文件对应的chunkjscss打包文件的文件名都会发生变化。
  • 若是修改静态文件的话,该静态文件的打包文件文件名会发生变化,而且引入该静态文件对应的chunkjscss打包文件的文件名也都会发生变化。

contenthash策略

最后一个就是contenthash策略, 它是以自身内容为单位的,所以当一个文件发生变化的时候,首先它自己的打包文件的名称会发生变化,其次,引入它的文件的打包文件也会发生变化。

惯例来个实验:

咱们将因此哈希占位符改为contenthash

module.exports = {
    entry: {
        index: './src/js/index.js',
        main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[contenthash:6].js',
        assetModuleFilename: 'assets/[name]-[contenthash:6][ext]'
    },
    module: {
        ...
    },
    plugins: [
        ...
        new miniCssExtractPlugin({
            filename: "assets/[name]-[contenthash:6].css"
        }),
    ]
};
复制代码

而后先打包一下。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-7503bc.png
│   ├── index-1e2b37.js
│   ├── main-02a4b4.css
│   └── main-c437b0.js
└── index.html
复制代码

首先咱们先修改一下图片吧,找一张新图覆盖一下image.png,而后打包一下。

首先image.png的名称必定会发生变化,由于它改动了。其次index.js也会发生变化,这是由于它引入了image.png,而image.png的名称发生变化,所以它代码中引入的名称也得发生变化,所以index.js的名称也会发生变化。

main.jsmain.css由于没有引用image.png,所以不会发生变化。

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-f3f2ec.png
│   ├── index-e241d6.js
│   ├── main-02a4b4.css
│   └── main-c437b0.js
└── index.html
复制代码

接下来,咱们来修改一下main.js,而后打包一下。

咱们会发现,只有main.js的打包文件会发生变化,而处于同个chunkmain.css却不会发生变化,这是由于main.css没有引用main.js

├── assets
│   ├── Alata-Regular-e83420.ttf
│   ├── image-f3f2ec.png
│   ├── index-e241d6.js
│   ├── main-02a4b4.css
│   └── main-d1f8ed.js
└── index.html
复制代码

如今能够简单总结一下:

  • 无论是修改项目文件仍是静态文件,它自己的打包文件的文件名会发生变化,其次引用该文件的对应打包文件的文件名也会发生变化,向上递归。

多个打包配置

一般咱们项目都会有开发环境和生产环境。

前面咱们也看到了webpack提供了一个mode选项,但咱们开发中不太可能说开发的时候mode设置为development,而后等到要打包才设置为production。固然,前面咱们也说了,咱们能够经过命令--mode来对应匹配mode选项。

但若是开发环境和生产环境的webpack配置差别不只仅只有mode选项的话,咱们可能须要考虑多份打包配置了。

多个webpack配置文件

咱们默认的webpack配置文件名为webpack.config.js,而webpack执行的时候,也默认会找该配置文件。

但若是咱们不使用该文件名,而改为webpack.conf.js的话,webpack正常执行是会使用默认配置的,所以咱们须要使用一个--config选项,来指定配置文件。

webpack --config webpack.conf.js
复制代码

所以,咱们就能够分别配置一个开发环境的配置webpack.dev.js和生成环境的配置webpack.prod.js,而后经过指令进行执行不一样配置文件:

// package.json
 "scripts": {
   "dev": "webpack --config webpack.dev.js",
   "build": "webpack --config webpack.prod.js",
 }
复制代码

单个配置文件

若是说,你不想建立那么多配置文件的话,咱们也能够只只用webpack.config.js来实现多份打包配置。

按照前面说的使用--mode配置mode选项,其实咱们能够在webpack.config.js中拿到这个变量,所以咱们就能够经过这个变量去返回不一样的配置文件。

// argv.mode能够获取到配置的mode选项
module.exports = (env, argv) => {
  if (argv.mode === 'development') {
    // 返回开发环境的配置选项
    return { ... }
  }else if (argv.mode === 'production') {
    // 返回生产环境的配置选项
    return { ... }
  }
};
复制代码

优化一下Webpack配置

合理的配置mode选项和devtool选项

前面已经有讲到关于mode选项和devtool选项,而不一样选项打包的速度也会有所不一样,所以按照你的实际需求进行配置,有须要用到才生成,没须要用到就能省就省。

缩小文件搜索范围

alias选项

在配置文件中,其实有一个resovle.alias选项,它能够建立importreuquire别名,来确保模块引入变得更简单,同时webpack在打包的时候也能更快的找到引入文件。

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

module.exports = {
  ...
  
  resolve: {
    alias: {
      // 配置style路径的别名
      style: path.resolve(__dirname, 'src/style/')
    },
  }
};
复制代码
// 使用
import "style/style.scss";
import "style/style.css";
复制代码

include、exclude选项

当咱们使用loader的时候,咱们能够配置include来指定只解析该路径下的对应文件,同时咱们能够配置exclude来指定不解析该路径下的对应文件。

const path = require('path');

module.exports = {
  ...
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
        include: [path.resolve(__dirname, 'src')]  // 只解析src路径下的css
      }
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/   // 不解析node_modules路径下的js
      }
  ]
}
};
复制代码

noParse选项

咱们能够在module.noParse选项中,只配置不须要解析的文件。一般咱们会忽略一些大型插件从而来提升构建性能。

module.exports = {
  ...
  module: {
    noParse: /jquery|lodash/,
  },
};
复制代码

使用HappyPack开启多进程Loader

webpack构建过程当中,其实大部分消耗时间都是用到loader解析上面,一方面是由于转换文件数据量很大,另外一方面是由于JavaScript单线程特性的缘由,所以须要一个个去处理,而不能并发操做。

而咱们可使用HappyPack,将这部分任务分解到多个子进程中去进行并行处理,子进程处理完成后把结果发送到主进程中去,从而减小总的构建时间。

github.com/amireh/happ…

# 5.0.1
yarn add happypack -D
复制代码
// webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
const HappyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [{
          loader: 'happypack/loader?id=happyBabelLoader'
        }]
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'happyBabelLoader',  // 与loader对应的id标识
      // 用法跟loader配置同样
      loaders: [
        {loader: 'babel-loader', options: {}}
      ],
      threadPool: HappyThreadPool  // 共享进程池
    })
  ]
};
复制代码

使用webpack-parallel-uglify-plugin 加强代码压缩

起码有聊到,当modeproduction的时候,webpack打包会开启代码压缩插件,同时webpack也有提供一个optimization选项,让咱们可使用本身喜欢的插件去覆盖原生插件。

所以,咱们可使用webpack-parallel-uglify-plugin来覆盖原生代码压缩插件,它的一个优势就是能够并行执行。

github.com/gdborton/we…

# 2.0.0
yarn add webpack-parallel-uglify-plugin -D
复制代码
// webpack.config.js
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin")

module.exports = {
  ...
  
  optimization: {
    minimizer: [
      new ParallelUglifyPlugin({
        // 缓存路径
        cacheDir: '.cache/',  
        // 压缩配置
        uglifyJS: {
          output: {
            comments: false,
            beautify: false
          },
          compress: {
            drop_console: true,
            collapse_vars: true,
            reduce_vars: true
          }
        }
      })
    ]
  }
};
复制代码

配置缓存

咱们每次执行构建都会把全部的文件都从新编译一边,若是咱们能够将这些重复动做缓存下来的话,对下一步的构建速度会有很大的帮助。

如今大部分的loader都提供了缓存选项,但并不是全部的loader都有,所以咱们最好本身去配置一下全局的缓存动做。

Webpack5以前,咱们都使用了cache-loader,而在webpack5中,官方提供了一个cache选项给咱们带来持久性缓存。

// 开发环境
module.exports = {
  cache: {
    type: 'memory'  // 默认配置
  }
}

// 生产环境
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
}
复制代码

分析打包文件大小

咱们可使用webpack-bundle-analyzer插件来帮助咱们分析打包文件,它会将打包后的内容束展现为方便交互的直观树状图,让咱们知道咱们所构建包中真正引入的内容。

github.com/webpack-con…

# 4.4.2
yarn add webpack-bundle-analyzer -D
复制代码
// webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');

module.exports = {
    ...
  
    plugins: [
        new BundleAnalyzerPlugin()
    ]
};
复制代码

而后咱们打包后,webpack会自动打开一个页面,显示咱们打包文件的状况,经过打包报告能够很直观的知道哪些依赖包大,则能够作作针对性的修改。

demo3.png

若是不想每次运行都打开网页的话,咱们能够先将数据保存起来,而后要看的时候再执行新的命令去查看。

// webpack.config.js
new BundleAnalyzerPlugin({
   analyzerMode: 'disabled',
   generateStatsFile: true
 })
复制代码
// package.json
"scripts": {
  "analyzer": "webpack-bundle-analyzer --port 3000 ./dist/stats.json"
},
复制代码

手写一下

手写Loader

webpack.js.org/contribute/…

webpack官网,它给提出了几个loader的编写原则:

  • **单一原则:**每一个loader只作一件事情;
  • 链式调用:webpack会按照顺序链式去调用每一个loader
  • **统一原则:**遵循webpack定制的设计规则和结构,输入和输入均为字符串,每一个loader彻底独立,即插即用。

同时webpack还给咱们提供了loader API,所以咱们可使用this去获取须要用到的API,但也是由于如此,咱们loader的实现就不能使用箭头函数了。

今天,咱们来简单手写一下sass-loadercss-loaderstyle-loader,而它们也有各自的单一功能:

  • sass-loader:用来解析sassscss代码;
  • css-loader:用来解析css代码;
  • style-loader:将css代码插入到js中。

首先,咱们先建立一个myLoders文件夹,而后建立三个loader文件。

├── myLoaders
│   ├── ou-css-loader.js
│   ├── ou-sass-loader.js
│   └── ou-style-loader.js
复制代码

而后咱们须要在webpack引入,而且须要配置一下resolveLoader选项,由于webpack默认只会去node_modules搜索loader

module.exports = {
  ...

  resolveLoader: {
    // 添加loader查询路径
    modules: ['node_modules', './myLoaders']
  },
  module: {
    rules: [{
      test: /\.(scss|sass)$/,
      // 使用本身的loader
      use: ['ou-style-loader','ou-css-loader','ou-sass-loader']
    }]
  }
};
复制代码

首先咱们先来实现ou-sass-loader

loader的本质就是一个函数,而咱们能够在函数的第一个参数获取到对应文件的代码,咱们能够先打印一下来看看。

// ou-sass-loader.js
module.exports = function(source) {
  console.log(source);
}
复制代码

而后执行打包后,咱们能够看到咱们的scss文件中的代码。

所以,咱们可使用sass插件来进行解析scss代码,sass有一个render函数能够去解析。

// ou-sass-loader.js
const sass = require('sass');

module.exports = function(source) {
  // 使用render函数进行解析scss代码
  sass.render({data: source},  (err, result) => {
    console.log(result);
  });
}
复制代码

咱们在执行一下打包,会发现result是一个对象,而里面的css就是咱们所须要的,所以咱们须要将其返回出去。

这里cssBuffer,咱们须要去解析它,可是解析它是css-loader的工做,而不是sass-loader的工做。

{
  css: <Buffer 62 6f 64 79 20 7b 0a 20 20 62 61 63 6b 67 72 6f 75 6e 64 3a 20 23 32 32 32 3b 0a 7d 0a 62 6f 64 79 20 64 69 76 20 7b 0a 20 20 63 6f 6c 6f 72 3a 20 23 ... 6 more bytes>,
  map: null,
  stats: {
    entry: 'data',
    start: 1628131813793,
    end: 1628131813830,
    duration: 37,
    includedFiles: [ [Symbol($ti)]: [Rti] ]
  }
}
复制代码

但这里是一个异步操做,咱们不能直接return回去,而是须要使用到webpack提供的一个API——this.async,它自己是一个函数,而后会返回一个callback()让咱们能够返回异步的结果。

// ou-sass-loader.js
const sass = require('sass');

module.exports = function(source) {
  // 获取callback函数
  const callback = this.async();
  sass.render({data: source},  (err, result) => {
    // 将结果返回
    if (err) return callback(err);
    callback(null, result.css);
  });
}
复制代码

这时候,咱们ou-sass-loader就实现了,接下来咱们来实现ou-css-loader

它其实任务很简单,就是将ou-sass-loader返回的css解析为字符串就能够了。

// ou-css-loader.js
module.exports = function(source) {
    return JSON.stringify(source)
}
复制代码

最后就是ou-style-loader,它的任务就是建立一个style标签,而后将ou-css-loader返回的数据插进去,而且将style标签放置到head标签里面去。

// ou-style-loader.js
module.exports = function(sources) {
    return ` const tag = document.createElement("style"); tag.innerHTML = ${sources}; document.head.appendChild(tag) `
}
复制代码

这时咱们简易版的sass-loadercss-laoderstyle-laoder就实现了,咱们能够执行一下打包命令,检验页面是否是有对应的样式效果。

手写Plugin

webpack.js.org/contribute/…

webpack运行过程当中,会存在一个生命周期,而在生命周期中webpack会广播出许多事情,而在plugin中是能够监听到这些事件,所以plugin是能够实如今合适的时机经过Webpack提供的API去实现一些动做。

正常状况下,一个plugin是一个类,而且里面会有一个apply函数,而在apply函数中会接收到一个compiler参数,里面包含了关于webpack环境全部的配置信息。

module.exports = class MyPlugin {
  apply (compiler) {}
}
复制代码

compiler中会暴露不少生命周期钩子函数,具体的能够查看文档。咱们能够经过如下方式去访问钩子函数。

compiler.hooks.someHook.tap(...)
复制代码

tap方法中,接收两个参数,一个是该plugin的名称,一个是回调函数,而在回调函数中,又会接收到一个compilation参数。

module.exports = class MyPlugin {
  apply (compiler) {
    compiler.hooks.compile.tap("MyPlugin", (compilation) => {
      console.log(compilation)
    })
  }
}
复制代码

compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack 开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。compilation 对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。

compliation也暴露了许多的钩子,具体的话能够去看看文档

接下来,简单实现一下一个plugin,打包后生成一个txt文件,里面会打印出每一个bundle的大小。

module.exports = class MyPlugin {
  apply(compiler) {
    // 生成资源到 output 目录以前
    compiler.hooks.emit.tap("MyPlugin", (compilation) => {
      let str = ''
      for (let filename in compilation.assets) {
        // 获取文件名称和文件大小
        str += `${filename} -> ${compilation.assets[filename]['size']() / 1000}KB\n`
      }

      // 新建fileSize.txt
      compilation.assets['fileSize.txt'] = {
        // 内容
        source: function () {
          return str
        }
      }
    })
  }
}
复制代码

紧接着,咱们将其引入到webpack.config.js,并在plugins中建立实例。

const MyPlugin = require("./myPlugins/my-plugin")

module.exports = {
    ...
  
    plugins: [
        new MyPlugin()
    ]
};
复制代码

而后打包后,dist文件中会生成一个fileSize.txt文件。

assets/Alata-Regular-e83420.ttf -> 96.208KB
assets/image-f3f2ec.png -> 207.392KB
index.html -> 0.364KB
assets/index-41f0e2.css -> 0.177KB
assets/index-acc2f5.js -> 1.298KB
复制代码

手写Webpack

代码:github.com/OUDUIDUI/mi…

喜欢的朋友能够点个Star哦~

初始化

首先咱们先初始化咱们的项目文件。

先新建一个src路径,而后建立三个js文件——index.jsa.jsb.js

// index.js
import {msg} from "./a.js";

console.log(msg);



// a.js
import {something} from "./b.js";

export const msg = `Hello ${something}`;


// b.js
export const something = 'World';
复制代码

而后咱们能够先安装webpack,而后测试一下打包出来的bundle文件有什么特色。

这里就很少说了,直接看bundle文件(默认配置,modedevelopment

打包后,咱们能够看到bundle文件有不少内容,但也有一大半注释。

其实咱们只须要看两个地方,一个是__webpack_modules__变量。咱们能够看到它是一个对象,而后key值为module路径,而value值是执行module代码的函数。

var __webpack_modules__ = ({
  "./src/a.js": (() => eval( ... )),
  "./src/b.js": (() => eval( ... )),
  "./src/index.js": (() => eval( ... ))
})
复制代码

其次,咱们能看到一个函数,叫__webpack_require__,它接收一个moduleId的参数。然而咱们能够在最后看到了这个函数的调用,就会发现其实moduleId就是__webpack_modules__key值,也就是module的路径。

var __webpack_exports__ = __webpack_require__("./src/index.js");
复制代码

到这里,咱们就能够大概捋清楚webpack打包的一个逻辑了。

  • webpack是直接拿到js文件的代码,即字符串。而后经过eval()函数执行代码;
  • webpack会从入口文件开始,不断递归遍历引入模块,而后保持在一个对象里面,key值为moduleId,即模块路径,而value是模块的相关代码。
  • webpack会将代码转换为commonJS,即便用require去引入模块,同时它自身会去封装一个require函数,去执行入口文件代码。

话很少说,咱们开始来手写代码。

首先咱们能够先初始化webpack配置文件——webpack.config.js

const path = require("path");

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: 'index.js'
  }
}
复制代码

其次,咱们新建一个lib文件夹,而后建立一个webpack.js,用来手写咱们的mini-webpack

咱们能够先初始化一下,Webpack是一个类,其次构建函数会接受配置文件,其次会有一个run函数,是webpack的运行函数。

module.exports = class Webpack {
  /** * 构造函数,获取webpack配置 * @param {*} options */
  constructor(options) {}

  /** * webpack运行函数 */
  run() {
    console.log('开始执行Webpack!')
  }
}
复制代码

而后咱们须要一个执行文件,即在根路径建立一个debugger.js

const webpack = require('./lib/webpack');
const options = require('./webpack.config');

new webpack(options).run();
复制代码

紧接着咱们执行一下该文件。

node debugger.js
复制代码

这时候命令行就会打印出开始执行Webpack!

咱们能够开始手写mini-webpack了。

模块解析

首先,在构造函数中,咱们须要保存一下配置信息。

constructor(options) {
  const {entry, output} = options;
  this.entry = entry;  // 入口文件
  this.output = output;  // 导出配置
}
复制代码

在执行的第一步,咱们须要来解析一下入口文件,所以咱们用一个parseModules来实现这个功能。

module.exports = class Webpack {
    constructor(options) {
        ...
    }

    run() {
        // 解析模块
        this.parseModules(this.entry);
    }

    /** * 模块解析 * @param {*} file */
    parseModules(file) {}
}
复制代码

parseModules中,咱们须要作两件事情:分析模块信息、递归遍历引入模块。

咱们一步一步来实现。首先,封装一个getModuleInfo函数,来分析模块信息。

parseModules(file) {
  // 分析模块
  this.getModuleInfo(file);
}

 /** * 分析模块 * @param {*} file * @returns Object */
getModuleInfo(file) {}
复制代码

首先,咱们接收到的file其实就是入口文件的相对路径,即./src/index.js。所以咱们能够先用node自带的fs模块来读取文件内容。

getModuleInfo(file) {
  // 读取文件
  const body = fs.readFileSync(file, "utf-8");
}
复制代码

读取到内容后,咱们就要来分析一下文件内容了,这时候就须要用到了AST语法树了。

抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。

演示地址:astexplorer.net/

这里咱们用到的时候babelparse插件,经过它来将JavaScript转成AST

# 7.14.8
yarn add @babel/parser -D
复制代码
const fs = require("fs");
const parser = require("@babel/parser");

module.exports = class Webpack {
    ...

    getModuleInfo(file) {
      // 读取文件
      const body = fs.readFileSync(file, "utf-8");

      // 转化为AST语法树
      const ast = parser.parse(body, {
        sourceType: 'module'  // 表示咱们解析的是ES模块
      })
    }
}
复制代码

紧接着,咱们还须要使用@babel/traverse来遍历AST,从而来识别该文件有没有引入其余模块,有的话就将其记录下来。

# 7.14.8
yarn add @babel/traverse -D
复制代码
const traverse = require("@babel/traverse").default;
复制代码

traverse接受两个参数,第一个是ast语法树,第二个是一个对象,在对象中咱们能够设置观察者函数,而且能够针对语法树中的特定节点类型。

好比咱们此次只须要找到引入模块的语句,对应的节点类型为ImportDeclaration,咱们就能够设置对应的ImportDeclaration函数,并在参数值获取到节点信息。

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;


module.exports = class Webpack {
    ...

    getModuleInfo(file) {
      // 读取文件
      const body = fs.readFileSync(file, "utf-8");

      // 转化为AST语法树
      const ast = parser.parse(body, {
        sourceType: 'module'  // 表示咱们解析的是ES模块
      })
      
      traverse(ast, {
        // visitor函数
        ImportDeclaration({node}) {
          console.log(node);
        }
      })
    }
}
复制代码

咱们执行一下,能够打印出import {msg} from "./a.js"的语法树。

所以,咱们须要将其路径收集起来。

// 依赖收集
const deps = {};
traverse(ast, {
  // visitor函数
  ImportDeclaration({node}) {
    // 入口文件路径
    const dirname = path.dirname(file);
    // 引入文件路径
    const absPath = "./" + path.join(dirname, node.source.value);
    deps[node.source.value] = absPath;
  }
})
复制代码

此时的deps就是{ './a.js': './src/a.js' },之因此要保存它相对项目根路径的相对路径,是为了后面更好的去拿到它的文件内容。

收集完依赖后,咱们须要将AST转回JavaScript代码,而且将其转成ES5语法。这时候咱们就会用到@babel/core@babel/preset-env

# @babel/core -> 7.14.8, @babel/preset-env -> 7.14.8
yarn add @babel/core @babel/preset-env -D
复制代码
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

module.exports = class Webpack {
    ...

    getModuleInfo(file) {
      // 读取文件
      const body = fs.readFileSync(file, "utf-8");

      // 转化为AST语法树
      const ast = parser.parse(body, {
        sourceType: 'module'  // 表示咱们解析的是ES模块
      })
      
      // 依赖收集
      const deps = {};
      traverse(ast, {
        // visitor函数
        ImportDeclaration({node}) {
          // 入口文件路径
          const dirname = path.dirname(file);
          // 引入文件路径
          const absPath = "./" + path.join(dirname, node.source.value);
          deps[node.source.value] = absPath;
        }
      })

      // ES6转成ES5
      const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"],
      })
    }
}
复制代码

这时候咱们能够打印一下code,会发现它再也不是ESModule的引入方式了,而是使用了CommonJS引入方式。

"use strict";

var _a = require("./a.js");

console.log(_a.msg);
复制代码

最终,getModuleInfo会返回一个对象,对象里面包含着解析文件的路径,该文件的依赖对象以及文件代码。

parseModules(file) {
  // 分析模块
  const entry = this.getModuleInfo(file);
}

getModuleInfo(file) {
  ...

  return {
    file,   // 文件路径
    deps,  // 依赖对象
    code   // 代码
  };
}
复制代码

但咱们分析完入口文件后,咱们就须要进行递归遍历,去分析引入模块。

首先,咱们须要新建一个数组,保存一下全部的分析结果。其次,咱们来实现一下getDeps函数,来递归遍历引入模块。

parseModules(file) {
  // 分析模块
  const entry = this.getModuleInfo(file);
  const temp = [entry];

  // 递归遍历,获取引入模块代码
  this.getDeps(temp, entry)
}


/** * 获取依赖 * @param {*} temp * @param {*} module */
getDeps(temp, {deps}) {}
复制代码

getDeps中,咱们能够经过第二个参数获取到依赖对象,其次经过遍历这个对象,一一执行一下getModuleInfo函数,获取各个依赖模块的解析内容,并保存到temp

最后,再自调用一下getDeps,传入引入模块内容,继续递归遍历。

getDeps(temp, {deps}) {
  // 遍历依赖
  Object.keys(deps).forEach(key => {
    // 获取依赖模块代码
    const child = this.getModuleInfo(deps[key]);
    temp.push(child);
    // 递归遍历
    this.getDeps(temp, child);
  })
}
复制代码

这里还须要进行查重,好比在多个文件都引入了b.js的话,temp数组就会保存多个b.js的内容对象,所以咱们能够先查重一下,若是temp对象没有该模块,咱们再执行后面的操做。

getDeps(temp, {deps}) {
  Object.keys(deps).forEach(key => {
    // 去重
    if (!temp.some(m => m.file === deps[key])) {
      const child = this.getModuleInfo(deps[key]);
      temp.push(child);
      this.getDeps(temp, child);
    }
  })
}
复制代码

这时候,咱们模块解析的操做已经完成了差很少了。

最后咱们最须要将temp数组,转换成对象,即跟__webpack_modules__相似,以路径为key名,而后value为对应的内容信息。

parseModules(file) {
  const entry = this.getModuleInfo(file);
  const temp = [entry];

  this.getDeps(temp, entry)

  // 将temp转成对象
  const depsGraph = {};
  temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code
    }
  })

  return depsGraph;
}
复制代码

这时候,咱们在run()函数保存一下解析结果,就完成了第一步操做了。

run() {
  // 解析模块
  this.depsGraph = this.parseModules(this.entry);
}
复制代码

打包

下一步就是执行打包操做了,咱们先封装一个bundle函数。

run() {
  // 解析模块
  this.depsGraph = this.parseModules(this.entry);

  // 打包
  this.bundle()
}

/** * 生成bundle文件 */
bundle() { }
复制代码

首先咱们先把简单的部分完成了,就是生成打包文件。

咱们要用到fs模块,先识别打包路径存不存在,不存在的话新建一个目录,其次就写入bundle文件。

bundle() {
  const content = `console.log('Hello World')`;

  // 生成bundle文件
  !fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
  const filePath = path.join(this.output.path, this.output.filename);
  fs.writeFileSync(filePath, content);
}
复制代码

这时运行一下打包命令,项目里就会出现一个dist文件夹,里面会有一个index.js

console.log('Hello World')
复制代码

接下来咱们就得来实现bundle文件的内容。

首先它是一个匿名函数只执行的方式,而后它接收一个参数__webpack_modules__,即咱们前面解析文件的结果。

(function(__webpack_modules__){
  ...
})(this.depsGraph)
复制代码

其次,咱们须要是实现一下__webpack_require__函数,它接收一个moduleId参数,即路径参数。

而后咱们还须要去调用一下__webpack_require__,并传入入口文件路径。

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {
    ...
  }
  __webpack_require__(this.entry)  
})(this.depsGraph)
复制代码

前面咱们又看到,babel将代码转义成commonJS,所以咱们须要来实现一下require函数,由于JavaScript自己不具有。

require函数的实质就是返回引入文件的内容。

同时,咱们还须要新建一个exports对象,这样子模块导出的内容就能够保存到里面去了,最后也须要将其返回出去。

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {
    // 实现require方法
    function require(relPath) {
      return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
    }
    // 保存导出模块
    var exports = {};
    
    return exports
  }
  __webpack_require__(this.entry)  
})(this.depsGraph)
复制代码

最后,就只须要来执行一下入口文件的代码便可。

这里仍是使用一个匿名函数并自调用。

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {
    // 实现require方法
    function require(relPath) {
      return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
    }
    // 保存导出模块
    var exports = {};
    
    // 调用函数
    (function (require,exports,code) {
      eval(code)
    })(require,exports,__webpack_modules__[moduleId].code)
    
    return exports
  }
  __webpack_require__(this.entry)  
})(this.depsGraph)
复制代码

这时候咱们再将这段代码,换到content变量中去。

bundle() {
  const content = ` (function (__webpack_modules__) { function __webpack_require__(moduleId) { function require(relPath) { return __webpack_require__(__webpack_modules__[moduleId].deps[relPath]) } var exports = {}; (function (require,exports,code) { eval(code) })(require,exports,__webpack_modules__[moduleId].code) return exports } __webpack_require__('${this.entry}') })(${JSON.stringify(this.depsGraph)}) `;

  // 生成bundle文件
  !fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
  const filePath = path.join(this.output.path, this.output.filename);
  fs.writeFileSync(filePath, content);
}
复制代码

而后执行打包,就能够看到完整的打包内容了。

(function (__webpack_modules__) {
    function __webpack_require__(moduleId) {
        function require(relPath) {
            return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
        }
        var exports = {};
        (function (require,exports,code) {
            eval(code)
        })(require,exports,__webpack_modules__[moduleId].code)
        return exports
    }
    __webpack_require__('./src/index.js')
})({"./src/index.js":{"deps":{"./a.js":"./src/a.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(_a.msg);"},"./src/a.js":{"deps":{"./b.js":"./src/b.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.msg = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar msg = \"Hello \".concat(_b.something);\nexports.msg = msg;"},"./src/b.js":{"deps":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.something = void 0;\nvar something = 'World';\nexports.something = something;"}})

复制代码

最后,咱们执行一下,看看能不能打印出Hello World

node ./dist/index.js
复制代码
相关文章
相关标签/搜索