webpack运行过程解析

前言

我目前正在学习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';

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.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

classRemoveCommentsPlugin{

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

classRemoveCommentsPlugin{

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

classRemoveCommentsPlugin{

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

classRemoveCommentsPlugin{

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)、运行效果

以上是 webpack运行过程解析 的全部内容, 来源链接: utcz.com/a/24976.html

回到顶部