webpack运行过程分析

2,137 阅读14分钟

前言

我目前正在学习webpack,首先对webpack的知识点进行简单的总结,更加全面的请前往官方网站学习;其次再结合汪磊老师的《webpack原理与实践》实现一个loader和plugin,对整体的运行过程进行一个梳理;如有错误,敬请指出。

一、webpack基本知识点

1、webpack

什么是webpack?

  • webpack是一个模块打包机,将我们开发项目当作一个整体,把开发过程中用到的所有资源打包到一起进行输出。

webpack解决了什么?

  • 我们开发阶段可能用到新特性的代码,webpack可以将这些代码转化为兼容大多数环境的代码。
  • webpack能够将项目的其他资源打包到一起输出,这样子就可以解决浏览器频繁请求文件。
  • webpack支持把不同的资源进行打包,包括字体、样式、图片、等这样子我们就拥有一个统一的模块化方案,资源的加载都可以通过代码进行控制。

2、entry

入口起点(entry):主要定义webpack从哪个文件开始进行打包。写法如下所示:

//webpack.config.js
module.exports={
    entry:'./path/main.js'
}

也可以采用对象的这种写法

//webpack.config.js
module.exports={
    entry:{
        main:'./src/main.js'
    }
}

webpackentry接口文件作文构建依赖图的开始,进入文件后,会找出入口文件所有依赖的模块或者资源进行打包。

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.js
module.exports={
    output:{
        filename:'bundle.js'
        path:'./dist'
    }
}

也可以这样写

//webpack.config.js
module.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.js
module.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.js
const 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-loadercss模块转化为js模块后,还需要使用style-loader将转化为js模块的代码通过创建style标签的方式添加到页面上,我们接下来再安装style-loader模块。

npm install style-loader --save-dev

进行webpack配置

//webpack.config.js
const 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-loadercss-loader实际上是一个接受参数的函数,css-loader将参数进行相应的处理后返回给style-loader,sttyle-loader将参数进行处理,也就是创建style标签添加到页面上,这就是整个过程。

2、实现一个简单loader

为了更好的理解loader,接下来我们实现一个loaderloader的实现原理其实很简单。loader本质上是一个函数,函数的参数就是我们需要加载的文件,返回的内容可以作为下一个loader的参数,最后输出一个js的模块或者以其他的形式添加到bundle.js中。

我们开发一个可以加载markdown文件的loadermarkdown文件需要转换为html之后才能呈现到页面上,所以我们需要导入markdown文件后,转化为可以显示的html字符串。

我们可以在根目录下创建一个markdown-loader.js代替npm安装的模块,当加载markdown文件时,直接加载我们创建的这个文件。目录如下:

文件内容

//about.md
#About
this is a markdown file
//main.js
import about from './about.md'
console.log(about)

loader本质就是一个导出的函数,这个函数就是对加载内容处理的过程,这个函数需要接收一个参数,这个参数就是我们加载的文件,输出的就是我们加载后的结果。

//markdown-loader.js
module.exports=function(source){
    console.log(source)
    return 'hello loader'
}
//webpack.config.js
const 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.js
module.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.js
const marked=require('marked')
module.exports=source=>{
    cosnt html=marked(source);
    const code=`module.exports=${JSON.stringify(html)}`;
    return code
}

html文件以js模块的方式返回出来,我们可以从浏览器中打印出来。

我们可以将其显示到网页上

//mian.js
import about from  './about.md';
function component(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.js
const marked=require('marked')
module.exports=source=>{
    cosnt html=marked(source);
    return html
}
//html-loader.js
module.exports=source=>{
    const code=`module.exports=${JSON.stringify(source)}`;
    return code;
}

当然我们配置文件还要进行更改一下

//webpack.config.js
const 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.js
const {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执行到这个环节,触发相应的钩子函数,也会触发相对应的pluginplugin就可以对这个状态的文件进行相应的处理。比如在生成最后生成资源的时机对整个资源进行压缩等等。下面我们就来开发一个plugin

我们的需求就是,开发个插件可以清除bundle.js中的注释,如下图最左边的注释,使我们的文章更加易读。

那怎么开发呢?首先根目录添加一个remove-comments-plugin.js,这个plugin必须是一个函数或者一个包含apply方法的对象,一般都是定义一个类型,在这个类型中定义apply方法。在使用的时候,通过这个类型创建一个实例对象去使用这个插件。

//remove-comments-plugin.js
class RemoveCommentsPlugin{
    apply(compiler){
        console.log('RecomveCommentsPlugin启动');
    }
}

webpack启动时,这个类型生成一个对象,这个对象的apply方法,接收一个compiler参数,这个参数就是webpack工作最核心的对象,里面包括我们webpack构建过程中所有的配置信息,就是通过这个对象去注册相应的钩子函数。那应该挂载在哪个钩子函数呢,大家可以在官网查找一下钩子的说明,emit钩子是生成资源到 output目录的时候调用,所以这个阶段最合适。

传递过来的compiler对象的hooks属性,我们可以访问到这个emit钩子,再通过tap方法注册这个钩子函数,这个钩子函数接收两个参数,一个是插件的名,一个是要挂载到钩子上的函数,这个函数有一个参数叫做compilation,这个对象可以理解为打包过程中上下文,打包的所有结果都会放到这个对象里。这个对象和compiler对象很像,但意义不一样,compilerwebpack构建过程中生成的唯一的对象,这个对象包括构建过程中的配置信息,而compilation指的是我们打包过程中资源的上下文,是我们打包的内容,这个对象不是唯一的。直接看例子,compilation对象有一个assets属性是即将写入输出目录的文件,我们可以将其名字打印出来。

//webpack-config.js
const {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.js
class RemoveCommentsPlugin{
    apply(compiler){
        compiler.hooks.emit.tap('RemoveCommentsPlugin',function(compilation){
            for(const name of compilation.assets){
                console.log(name);
            }
        })
    }
}
module.exports=RemoveCommentsPlugin;

我们想打印一下文件的内容

//remove-comments-plugin.js
class RemoveCommentsPlugin{
    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.js
class RemoveCommentsPlugin{
    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方法,这个方法调用compilerhooks注册一个钩子函数,当这个钩子函数触发的时候,就调用相应回调函数。接下来compiler的生命周期就开始了。

(3)、使用compiler编译整个项目

  • 调用compilerrun方法,这个方法内部有beforeRunrun两个方法,这个阶段调用对象的compile方法生成一个compilation对象,这个对象就是构建过程中的上下文,里面包含这次构建中全部的资源和信息。
  • 紧接着就调用make钩子,这个阶段是构建过程中最核心的阶段。我们默认的是单一入口的打包方式,所以会执行SingleEntryPlugin这个插件,这个插件紧接着调用Compilation对象中的addEntry方法,从配置中entry找出入口文件,将入口模块添加到模块依赖列表中。接下来调用Compilation对象的buildModule方法进行模块构建,buildModule方法主要执行loader对特殊资源进行处理,加载完之后生成AST语法树,对于这个语法树进行分析这个模块是否有依赖的模块,如果有继续循环buildModule每个依赖;所有依赖解析完成,buildModule阶段结束。
  • 合并生成需要输出的build.js并输出到目录。(其实make阶段就是根据配置文件找到入口文件entry依次递归出所有依赖,形成依赖关系树,将递归到的每个模块交给不同的 loader进行处理。最后根据output输出)。

###三、webpack相关的优化

三、webpack实现简单vue项目

(1)、安装相应的安装包:

  • webpackwebpack-cli:负责打包的这个过程
  • vuevue运行时依赖
  • vue-loader:负载加载vue文件,这个loader还需要依赖vue-template-compiler解析complete组件、css-loader继续组件内的css样式。在这个阶段还需要加载一个VueloaderPlugin
  • html-webpack-plugin:负责生成html文件
  • clean-webpack-plugin:负责生成文件前清理dist目录

(2)、项目相关目录

(3)、具体代码

//webpack.config.js
const 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.js
import 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)、运行效果