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样式。在这个阶段还需要加载一个VueloaderPlugin
html-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