webpack运行过程解析
前言
我目前正在学习webpack,首先对webpack的知识点进行简单的总结,更加全面的请前往官方网站学习;其次再结合汪磊老师的《webpack原理与实践》实现一个loader和plugin,对整体的运行过程进行一个梳理;如有错误,敬请指出。
一、webpack基本知识点
1、webpack
什么是webpack?
webpack是一个模块打包机,将我们开发项目当作一个整体,把开发过程中用到的所有资源打包到一起进行输出。
webpack解决了什么?
- 我们开发阶段可能用到新特性的代码,webpack可以将这些代码转化为兼容大多数环境的代码。
- webpack能够将项目的其他资源打包到一起输出,这样子就可以解决浏览器频繁请求文件。
- webpack支持把不同的资源进行打包,包括字体、样式、图片、等这样子我们就拥有一个统一的模块化方案,资源的加载都可以通过代码进行控制。
2、entry
入口起点(entry):主要定义webpack从哪个文件开始进行打包。写法如下所示:
//webpack.config.jsmodule.exports={
entry:'./path/main.js'
}
也可以采用对象的这种写法
//webpack.config.jsmodule.exports={
entry:{
main:'./src/main.js'
}
}
webpack从entry接口文件作文构建依赖图的开始,进入文件后,会找出入口文件所有依赖的模块或者资源进行打包。
3、loader
加载器(loader):其实webpack 只可以处理js代码,但我们前面说的webpack可以处理开发用到的其他资源,就是使用loader可以进行相应资源的转换。比如我们加载css代码,首先进行安装
npm install --save-dev css-loader然后在webpack.config.js中进行配置
module.exports = {module: {
rules: [
{ test: /.css$/, use: 'css-loader' }
]
}
}
也可以这样写
module: {rules: [
{
test: /.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
}
当然不止这两种写法,还可以在引用资源的时候进行书写,还可以在cli里进行书写,具体可去官方网站了解,其实这样写还会报错,因为css-loader只是将css代码用js代码进行包裹,要想使用还需要安装style-loader,关于这个知识点,下面还会详细说。
4、plugins
插件(plugins):插件的作用很强大,主要对整个打包过程中各个优化,比如压缩,自动生成html,自动清理文件等。
const HtmlWebpackPlugin = require('html-webpack-plugin');//通过 npm 安装
module.exports = {
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
}
5、output
输出(output):主要定义打包好的文件输出的相关配置,包括文件名,输出路径等。如下所示
//webpack.config.jsmodule.exports={
output:{
filename:'bundle.js'
path:'./dist'
}
}
也可以这样写
//webpack.config.jsmodule.exports={
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
}
更加详细的写法及介绍可以直接到官网查看。
二、webpack运行过程及原理
1、loader介绍
webpack想要实现整个前端项目的模块化,就不能仅仅管理js文件,整个项目的各个资源包括css、图片等都应该被管理;而默认webpack只能管理js文件,webpack如何管理其他资源的文件呢,靠的就是loader机制。
那我们现在开始通过webpack加载css文件,来探索webpack如何加载模块资源的。
指定我们的入口文件为main.css下图所示
/* main.css */div{
color:red
}
配置文件为
//webpack.config.jsmodule.exports={
entry:"./src/main.css",
output:{
filename:'bundle.js'
}
}
对文件进行打包

大家可能比较好奇,为什么入口文件可以是css文件,其实webpack并没有强制的要求入口文件必须是js文件,只不过作为程序的逻辑入口,js文件更加的合理。而直接打包css文件,报错了,错误的大致意思就是webpack内部默认处理js文件,也就是说在打包过程中默认把所有的文件当成js代码来进行处理,而我们写的css代码是不符合js代码规范的,所以会报错,那如果css文件为js代码呢,会出现什么呢,我们下面试一下。
/* main.css*/console.log('css文件');

并没有报错,而是打包出了文件。我们回顾一下打包css文件发生的错误,报错的那段话是:You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.这句话意思是你需要一个加载器来处理这个类型文件,但是当前并没有配置相关的加载器。也就是说webpack默认只能打包js文件,遇到特殊文件时,需要借用loader对其打包,我们又没有配置相应的loader,所以会报错。其实loader的打包实质就是将其转化为相对应的js文件。而且打包js文件也需要loader,只不过这个loader已经在webpack里面内置了。
我们接下来就进行安装处理css文件的css-loader
npm install css-loader --save-dev进行webpack配置
//webpack.config.jsconst path=require('path');
module.exports={
entry:'./src/main.css',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
mode:none,
module:{
rules:[{
test:/$.css/,
use:'css-loader'
}
]
}
}
/*main.css*/div{
color:red
}
重新打包后,没有发生错误,但是html文件也不像我们预想的那样,字体变成红色,这是为什么呢,我们来看一下打包文件bundle.js中关于css部分是如何显示的。

仔细观察这个模块,是将我们的css模块转化为js模块,具体的实现办法就是将我们的css代码push进一个数组中,但是整个过程并没有调用这个模块。有经验的应该都知道,使用css-loader将css模块转化为js模块后,还需要使用style-loader将转化为js模块的代码通过创建style标签的方式添加到页面上,我们接下来再安装style-loader模块。
npm install style-loader --save-dev进行webpack配置
//webpack.config.jsconst path=require('path');
module.exports={
entry:'./src/main.css',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
mode:none,
module:{
rules:[{
test:/$.css/,
use:['style-loader','css-loader']
}
]
}
}
再来进行打包,发现已经能正常显示红色。下面是新增的一个模块,负责处理css-loader返回的模块。还有一个1模块,篇幅实在太大,我就不附图了,感兴趣可以自己试一下。总而言之:style-loader的作用就是将css-loader中加载到的所有样式模块,通过创建style标签的方式添加到页面上


我们将配置文件中的use改变成了一个数组,那么顺序可以变吗?答案是否定的。loader工作的这个过程是当我们加载css文件,webpack发现内部处理不了这个文件,就去查找内部配置文件中有没有相应的loader,将加载到的css文件以参数的形式传递给css-loader,css-loader实际上是一个接受参数的函数,css-loader将参数进行相应的处理后返回给style-loader,sttyle-loader将参数进行处理,也就是创建style标签添加到页面上,这就是整个过程。
2、实现一个简单loader
为了更好的理解loader,接下来我们实现一个loader,loader的实现原理其实很简单。loader本质上是一个函数,函数的参数就是我们需要加载的文件,返回的内容可以作为下一个loader的参数,最后输出一个js的模块或者以其他的形式添加到bundle.js中。
我们开发一个可以加载markdown文件的loader,markdown文件需要转换为html之后才能呈现到页面上,所以我们需要导入markdown文件后,转化为可以显示的html字符串。
我们可以在根目录下创建一个markdown-loader.js代替npm安装的模块,当加载markdown文件时,直接加载我们创建的这个文件。目录如下:

文件内容
//about.md#About
this is a markdown file
//main.jsimport about from'./about.md'
console.log(about)
loader本质就是一个导出的函数,这个函数就是对加载内容处理的过程,这个函数需要接收一个参数,这个参数就是我们加载的文件,输出的就是我们加载后的结果。
//markdown-loader.jsmodule.exports=function(source){
console.log(source)
return'hello loader'
}
//webpack.config.jsconst path=require('path');
module.exports={
entry:'./src/main.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
mode:'none',
module:{
rules:[
{
test:/.md$/,
use:'./markdown-loader'
}
]
}
}
打包后,如下图所示:

里面的内容被打印出来,确实是我们加载的markdown文件,我们返回了hello loader 却被报错,这是为什么呢?报错信息是我们需要一个额外的加载器处理当前加载器的加载结果;还记得我们上边说的吗,loader本质上是一个函数,函数的参数就是我们需要加载的文件,返回的内容可以作为下一个loader的参数,最后输出一个js的模块或者以其他的形式绑定到bundle.js中。接下来我们更改一下markdown-loader.js文件
//markdown.loader.jsmodule.exports=function(source){
console.log(source)
return'console.log("hello loader")'
}
这个就能够正常的运行,可以看一下我们的打包结果

这个模块很简单,就是输入一个js代码到我们的bundle.js中,没有报错。
接下来我们继续实现我们的loader逻辑,我们要想markdown文件能正常显示到网页中,就需要将相应的资源显示成html文件,这里就比较复杂,我们使用相应的marked进行转化,需要进行安装引用marked模块。
npm install marked --save-dev//mrkdown-loader.jsconst marked=require('marked')
module.exports=source=>{
cosnt html=marked(source);
const code=`module.exports=${JSON.stringify(html)}`;
return code
}
将html文件以js模块的方式返回出来,我们可以从浏览器中打印出来。

我们可以将其显示到网页上
//mian.jsimport about from'./about.md';
functioncomponent(about){
var element=document.createElement('div');
element.innerHTML=about;
return element
}
document.body.appendChild(component(about))
结果如图所示

我们看一下bundle.js是什么样的

可以从图中很明显看到,我们引用的about变成了__webpack_requir__(1)也就是变成了模块1,那模块是什么呢,如下图

其实我们从入口文件加载的模块,包括入口文件,实际上最后都加载到了bundle.js里面数组里面,当然,入口文件就是模块0,其他入口文件依赖的模块就变成模块1,2...。我们在index.html引用的bundle.js就是引用模块0中的内容,当入口文件有依赖文件的话,直接引用加载的数组。这其实为了避免发起过多的http请求,但是首次加载文件就过多,我们也有其他方式对这种进行处理,这是后话。
下面我们再来看一下,loader设计的原则就是,每个loader有单一的作用,为了让大家更好的理解loader机制,上面写的loader还可以再细分一下。
//mrkdown-loader.jsconst marked=require('marked')
module.exports=source=>{
cosnt html=marked(source);
return html
}
//html-loader.jsmodule.exports=source=>{
const code=`module.exports=${JSON.stringify(source)}`;
return code;
}
当然我们配置文件还要进行更改一下
//webpack.config.jsconst path=require('path');
module.exports={
entry:'./src/main.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
mode:'none',
module:{
rules:[
{
test:/.md$/,
use:['html-loader','./markdown-loader']
}
]
}
}
这个效果是一样的。我们简单总结一下,webpack有一个内置的loader可以处理js文件。当我们加载其他资源的文件时,webpack不能处理相关的资源,就需要使用相对应的loader进行加载。loader的本质是一个可以接收参数的函数,可以把加载的资源进行相关的处理,处理结果可以返回给下一个loader作为参数继续处理,最后返回一个js模块或者通过方法添加到bundle.js里面去。而我们加载的每一个模块都是作为数组的一项存在bundle.js文件里,当其他模块需要的话,就直接从数组获取相关的引用就可以。
3、plugin介绍
plugin主要是为了增强webpack在项目自动化构建方面的能力。比如自动生成html文档、再比如打包之前自动清理dist目录、再比如注入全局变量等等。借助插件我们几乎可以实现前端工程化中用到的很多功能。
接下来我们试用一个plugin,每次打包之前,我们都需要自动清理dist目录,这个功能可以通过clean-webpack-plugin实现。首先安装一下
npm install clean-webpack-plugin配置文件引用
//webpack-config.jsconst {CleanWebpackPlugin}=require('clean-webpack-plugin')
module.exports={
entry:'./src/main.js'
output:{
filename:'bundle.js'
},
plugins:[
new CleanWebpackPlugin()
]
}
就这样就可以实现清理dist目录的效果。常用的plugin也有很多,大家可以去官网熟悉如何使用。
4、开发一个plugin
webpack的插件机制其实就是我们经常遇见的钩子机制。webapck在整个工作过程有很多的环节,在这每一个环节几乎都有相对应的钩子函数,plugin的开发就是基于这些钩子函数添加不同的任务,等webpack执行到这个环节,触发相应的钩子函数,也会触发相对应的plugin,plugin就可以对这个状态的文件进行相应的处理。比如在生成最后生成资源的时机对整个资源进行压缩等等。下面我们就来开发一个plugin。
我们的需求就是,开发个插件可以清除bundle.js中的注释,如下图最左边的注释,使我们的文章更加易读。

那怎么开发呢?首先根目录添加一个remove-comments-plugin.js,这个plugin必须是一个函数或者一个包含apply方法的对象,一般都是定义一个类型,在这个类型中定义apply方法。在使用的时候,通过这个类型创建一个实例对象去使用这个插件。
//remove-comments-plugin.jsclassRemoveCommentsPlugin{
apply(compiler){
console.log('RecomveCommentsPlugin启动');
}
}
在webpack启动时,这个类型生成一个对象,这个对象的apply方法,接收一个compiler参数,这个参数就是webpack工作最核心的对象,里面包括我们webpack构建过程中所有的配置信息,就是通过这个对象去注册相应的钩子函数。那应该挂载在哪个钩子函数呢,大家可以在官网查找一下钩子的说明,emit钩子是生成资源到 output目录的时候调用,所以这个阶段最合适。

传递过来的compiler对象的hooks属性,我们可以访问到这个emit钩子,再通过tap方法注册这个钩子函数,这个钩子函数接收两个参数,一个是插件的名,一个是要挂载到钩子上的函数,这个函数有一个参数叫做compilation,这个对象可以理解为打包过程中上下文,打包的所有结果都会放到这个对象里。这个对象和compiler对象很像,但意义不一样,compiler是webpack构建过程中生成的唯一的对象,这个对象包括构建过程中的配置信息,而compilation指的是我们打包过程中资源的上下文,是我们打包的内容,这个对象不是唯一的。直接看例子,compilation对象有一个assets属性是即将写入输出目录的文件,我们可以将其名字打印出来。
//webpack-config.jsconst {CleanWebpackPlugin}=require('clean-webpack-plugin')
const RemoveCommentsPlugin=require('remove-comments-plugin')
module.exports={
entry:'./src/main.js'
output:{
filename:'bundle.js'
},
plugins:[
new CleanWebpackPlugin(),
new RemoveCommentsPlugin()
]
}
//remove-comments-plugin.jsclassRemoveCommentsPlugin{
apply(compiler){
compiler.hooks.emit.tap('RemoveCommentsPlugin',function(compilation){
for(const name of compilation.assets){
console.log(name);
}
})
}
}
module.exports=RemoveCommentsPlugin;

我们想打印一下文件的内容
//remove-comments-plugin.jsclassRemoveCommentsPlugin{
apply(compiler){
compiler.hooks.emit.tap('RemoveCommentsPlugin',function(compilation){
for(const name of compilation.assets){
console.log(compilation.assets[name].source())
}
})
}
}
module.exports=RemoveCommentsPlugin;

内容太多就不附完了,既然可以打印出来,我们是不是可以对其进行操作,操作完成后再次覆盖compilation.assets的内容就可以了。
//remove-comments-plugin.jsclassRemoveCommentsPlugin{
apply(compiler){
compiler.hooks.emit.tap('RemoveCommentsPlugin',function(compilation){
for(const name of compilation.assets){
const content=compilation.assets[name].source();
const noContent=content.replace(//*{2,}/s?/g,'');
compilation.assets[name]={
source:()=>noContent,
size:()=>noContent.length
}
}
})
}
}
module.exports=RemoveCommentsPlugin;
这个执行之后,生成的bundle.js已经没有前面的注释,如下图所示:

5、webpack打包的整个过程
下面我就介绍一下webpack的核心打包过程,这个过程我听课看文档加上自己理解得出的,如果有错误,请不吝赐教,谢谢。
(1)、 webpack cli启动打包流程
- 解析webpack cli命令参数,如
mode=production,判断命令参数指定的配置文件(未指定就按照默认配置文件)加载,将加载的配置文件和命令参数配置进行合并,优先使用命令参数。最终得到一个完整的配置对象(options)。
(2)、创建webpack核心模块
- 使用上步得到的
options参数,创建webpack核心对象compiler。首先检测一下传过来的options参数,如果是一个对象,就创建一个compiler,如果是一个数组,就创建一个MultiCompiler,也就是说webpack支持多路打包;plugins本质上其实是一个数组,里面包含这各种实例,下一步就遍历数组的每一个实例将
compiler对象作为参数,传入plugin实例的apply方法,这个方法调用compiler的hooks注册一个钩子函数,当这个钩子函数触发的时候,就调用相应回调函数。接下来compiler的生命周期就开始了。
(3)、使用compiler编译整个项目
- 调用
compiler的run方法,这个方法内部有beforeRun和run两个方法,这个阶段调用对象的compile方法生成一个compilation对象,这个对象就是构建过程中的上下文,里面包含这次构建中全部的资源和信息。 - 紧接着就调用
make钩子,这个阶段是构建过程中最核心的阶段。我们默认的是单一入口的打包方式,所以会执行SingleEntryPlugin这个插件,这个插件紧接着调用Compilation对象中的addEntry方法,从配置中entry找出入口文件,将入口模块添加到模块依赖列表中。接下来调用Compilation对象的buildModule方法进行模块构建,buildModule方法主要执行loader对特殊资源进行处理,加载完之后生成AST语法树,对于这个语法树进行分析这个模块是否有依赖的模块,如果有继续循环buildModule每个依赖;所有依赖解析完成,buildModule阶段结束。 - 合并生成需要输出的
build.js并输出到目录。(其实make阶段就是根据配置文件找到入口文件entry依次递归出所有依赖,形成依赖关系树,将递归到的每个模块交给不同的loader进行处理。最后根据output输出)。
###三、webpack相关的优化
三、webpack实现简单vue项目
(1)、安装相应的安装包:
webpack、webpack-cli:负责打包的这个过程vue:vue运行时依赖vue-loader:负载加载vue文件,这个loader还需要依赖vue-template-compiler解析complete组件、css-loader继续组件内的css样式。在这个阶段还需要加载一个VueloaderPluginhtml-webpack-plugin:负责生成html文件clean-webpack-plugin:负责生成文件前清理dist目录
(2)、项目相关目录

(3)、具体代码
//webpack.config.jsconst path=require('path');
const {CleanWebpackPlugin}=require('clean-webpack-plugin')
const HtmlWebpackPlugin=require('html-webpack-plugin')
const VueLoaderPlugin=require('vue-loader/lib/plugin')
module.exports={
entry:path.resolve(__dirname,'src/index.js'),
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
mode:'development',
module:{
rules:[
{
test:/.vue$/,
use:'vue-loader'
},{
test:/.css$/,
use:['style-loader','css-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.js'
}
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title:'todo-demo'
}),
new VueLoaderPlugin()
]
}
//src/index.jsimport Vue from'vue'
import App from'./app.vue'
const root=document.createElement('div');
document.body.appendChild(root);
new Vue({
render:(h)=>h(App)
}).$mount(root)
//App.vue<template>
<div id="text">
{{text}}
</div>
</template>
<script>
export default{
data(){
return{
text:'abc'
}
}
}
</script>
<style>
#text{
color:red;
}
</style>
(4)、运行效果
以上是 webpack运行过程解析 的全部内容, 来源链接: utcz.com/a/24976.html
