一文学会Webpack实用功能|核心工作原理

Webpack 核心工作原理

webpack官网的首屏图片就已经很清楚的描述了他的工作原理,这里我们来简单理解一下webpack打包的核心工作过程。

我们以一个普通的前端项目为例,在我们的项目中一般都会散落着各种各样代码及资源文件,webpack会根据我们的配置找到其中的一个文件作为打包的入口,一般情况这个文件都会是一个JavaScript文件。

然后他会顺着我们入口文件当中的代码根据代码中出现的import或者require之类的语句解析推断出来这个文件所依赖的资源模块,然后分别去解析每个资源模块,对应的依赖,最后就形成了整个项目中所有用到文件之间的一个依赖关系的一个依赖树。

有了这个依赖关系树过后webpack会遍历,或者更准确的说法叫递归这个依赖树然后找到每个节点所对应的资源文件。最后再根据我们配置文件当中的属性去找到这个模块所对应的加载器,然后交给对应的加载器去加载这个模块。

最后会将加载到的结果放到bundle.js也就是我们的打包结果当中,从而去实现我们整个项目的打包,整个过程当中,loader的机制其实起了很重要的一个作用,因为如果没有loader的话他就没有办法去实现各种各样的资源文件的加载,那对于webpack来说他也就只能算是一个用来去打包或者是合并js模块代码的一个工具了。

Webpack 开发一个Loader

接下来我们来开发一个loader,通过这个过程,我们在来深入的了解loader的工作原理。

这里我们的需求是一个markdown-loader, 需求是有了这样一个加载器之后就可以在代码当中直接去导入markdown文件。

main.js

import about from './about.md';
console.log(about);

about.md

# 关于我

我是隐冬

我们都知道,markdown文件一般都是被转换为html过后再去呈现到页面上的,所以说这里希望我们导入的markdown文件得到的结果就是markdown转换过后的html字符串。console.log(about);

我们回到项目当中,由于这里我们需要直观的演示,所以我们就不再单独去创建一个npm模块了,我们直接在项目的根目录去创建一个markdown-loader.js这样一个文件,完成以后我们可以把这个模块发布到npm上作为一个独立的模块去使用。

每个webpack-loader都需要去导出一个函数,这个函数就是我们这个loader对我们所加载到的资源一个处理过程,输入就是我们所加载到的资源文件的内容,输出就是我们此次加工过后的结果。

我们通过source参数去接收输入,通过我们的返回值去输出。这里我们先尝试打印一下source, 然后直接去返回一个字符串hello,我们看下结果。

module.exports = source => {
    console.log(source);
    return 'hello';
}

完成以后我们回到webpack的配置文件当中,去添加一个加载器的规则配置, 这里我们配置的扩展名就是.md,我们所使用的加载器就是我们刚刚编写的markdown-loader模块。

这里可以看出我们use属性不仅可以使用模块的名称,其实对于模块的文件路径也是可以的,这一点与node当中的require函数是一样的。所以说我们这里直接使用相对路径去找到这个markdown-loader。

const path = require('path');

module.exports = {
    mode: 'none',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.md$/,
                use: './markdown-loader'
            }
        ]
    }
}

配置好过后我们直接回到命令行进行打包。

yarn webpack

打包过程当中命令行确实打印出来了我们所导入的markdonw文件内容,这也就意味着我们的source确实是所导入的文件内容,但是呢同时也爆出了一个解析错误,说的意思就是我们还需要一个额外的我们还需要一个额外的加载器去处理我们当前的加载结果。这是为什么呢?

其实webpack加载资源的过程有点类似一个工作管道,可以在这个过程当中依次使用多个loader,但是要求我们最终这个管道工作过后的结果必须是一段JavaScript代码,由于我们这里返回的内容不是一个标准的JavaScrit代码,所以才会出现这样的错误提示。

知道这个错误的原因之后解决的办法其实也就很明显了,要么就是我们这个loader直接去返回一段标准的JavaScript代码,要么就是我们再去找一个合适的加载器,接着去处理我们这里返回的结果。

这里我们先尝试第一种办法,回到markdow-loader当中,这里我们将返回的这个字符串修改为console.log(“hello”), 这也就是一段标准的JavaScript代码。

module.exports = source => {
    console.log(source);
    return 'console.log("hello")';
}

然后我们再次运行打包。此时可以发现,打包过程中就不会再报错了。我们来看一下打包过后的结果是什么样的。打开bundle.js找到最后一个模块。

这里其实也非常简单,webpack打包的时候就是把我们刚刚的loader加载过后的结果,也就是返回的那个字符串,直接拼接到我们这个模块当中了。这也就解释了刚刚为什么说loader管道最后为什么要返回JavaScript代码的原因。

/* 1 */
/***/ (function(module, exports) {

console.log("hello")

/***/ })

因为如果说你随便返回一个内容的话,放到这里语法就可能不通过,知道了这些过后呢,我们再回到loader当中,然后接着去完成我们刚刚的需求,这里我们先去安装一个markdown解析的模块,叫做marked。

yarn add marked --dev

安装完成过后呢,我们再回到代码当中,去导入这个模块,然后我们在我们的加载器当中去使用这个模块,去解析来自参数当中的这个source,这里我们的返回值就是一段html字符串。也就是转换过后的结果。

这里如果我们直接返回html的话就会面临刚刚同样的问题。正确的做法就是把这段html变成一段JavaScript代码。

这里我们希望是把这段html作为我们当前这个模块的导出的字符串,也就是希望通过module.export=这样一个字符串。但是如果我们只是简单的拼接的话那html当中存在的换行符还有一些引号拼接到一起就可能造成语法上的错误,所以这里使用一个小技巧。

通过JSON.stringify先将这个字符串转换成一个标准的JSON形式字符串,那此时他内部的引号及换行符都会被转译过来,然后我们再参与拼接,那这样的话就不会有问题了。

const marked = require('marked');
module.exports = source => {
    // console.log(source);
    // return 'console.log("hello")';
    const html = marked(source);
    return `module.exports = ${JSON.stringify(html)}`
}

我们回到命令行,再次运行打包。然后再来看下打包的结果。

此时我们看到的结果就是我们所需要的了。

/* 1 */
/***/ (function(module, exports) {

module.exports = "<h1 id=\"关于我\">关于我</h1>\n<p>我是隐冬</p>\n"

/***/ })

当然了,除了module.exports这种方式以外,那webpack还允许我们在返回的代码当中直接去使用ES Module的方式去导出,例如我们将module.export=修改为export default 以 ES Moudle的方式去导出后面的字符串。

const marked = require('marked');
module.exports = source => {
    // console.log(source);
    // return 'console.log("hello")';
    const html = marked(source);
    // return `module.exports = ${JSON.stringify(html)}`
    return `export default ${JSON.stringify(html)}`
}

然后运行打包,结果同样也是可以的,webpack内部会自动转换我们导出的这段代码当中的ES Module代码。

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<h1 id=\"关于我\">关于我</h1>\n<p>我是隐冬</p>\n");

/***/ })

这里我们就通过了第一种方式解决了我们所看到的那样一个错误,接下来我们再来尝试一下刚刚所说的第二种方法,那就是在我们markdown-loader当中去返回一个html字符串。然后我们交给下一个loader去处理这个html的字符串。这里我们直接返回marked解析过后的html,然后呢我们再去安装一个用于去处理html加载的loader,叫做html-loader

const marked = require('marked');
module.exports = source => {
    // console.log(source);
    // return 'console.log("hello")';
    const html = marked(source);
    // return `module.exports = ${JSON.stringify(html)}`
    // return `export default ${JSON.stringify(html)}`
    return html;
}
yarn add html-loader --dev

安装完成过后我们回到配置文件当中。这里我们把use属性修改为一个数组。这样我们的loader工作过程当中就会依次使用多个loader了。不过这里需要注意就是他的执行顺序是从数组的后面往前面,也就是说我们应该把先执行的loader放在数组的后面。

const path = require('path');

module.exports = {
    mode: 'none',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.md$/,
                use: ['html-loader', './markdown-loader']
            }
        ]
    }
}

完成以后我们再次打包,查看bundle.js,依然是可以的。

/* 1 */
/***/ (function(module, exports) {

module.exports = "<h1 id=\"关于我\">关于我</h1>\n<p>我是隐冬</p>\n"

/***/ })

我们markdown处理完的结果是一个html的字符串,这个html字符串交给了下一个loader也就是html-loader,这个loader又把他转换成了一个导出这个字符串的一个js代码,这样的话,我们webpack再去打包的时候就可以正常的工作了。

通过以上的这些个尝试我们就发现了,loader内部的一个工作原理其实非常简单,就是一个从输入到输出之间的一个转换,除此之外呢我们还了解了loader实际上是一种管道的概念,我们可以将我们此次这个loader的结果交给下一个loader去处理,然后通过多个loader去完成一个功能。

例如我们之前使用的css-loader和style-loader之前的一个配合,包括我们后面还会使用到的包括sass和less这种loader他们也需要去配合刚刚我们说的这两种loader,这个呢就是我们loader工作管道这样一个特性。

插件机制介绍

插件机制是webpack另外一个核心特性,他的目的是为了增强webpack在项目自动化方面的能力,那我们都知道,loader就是负责实现我们项目中各种各样资源模块的加载, 从而去实现整体项目的打包,而plugin则是用来解决项目中除了资源加载以外,其他的一些自动化的工作。

例如plugin可以帮我们去实现自动在打包之前清除dist目录,也就是我们上一次打包的结果。又或是他可以用来帮我们去copy那些不需要参与打包的资源文件到输出目录,又或是他可以用来帮我们压缩我们打包结果输出的代码。

总之,有了plugin呢webpack几乎无所不能的实现了前端工程化当中绝大多数经常用到的工作,这也是很多初学者会有webpack就是前端工程化的这种理解的原因。

接下来我们一起来学习下webpack插件机制以及这个过程中经常遇到的插件,最后我们再来开发一个自己的插件去理解他的工作原理。

Webpack 自动清除输出目录插件

了解了插件的基本作用过后,接下来我们再来体验几个最常见的插件,然后通过这个工程去了解如何使用插件。那首先第一个就是用来自动清除输出目录的插件。

clean-webpack-plugin

通过之前的演示你可能已经发现,webpack每次打包的结果都是覆盖到dist目录,而在打包之前,dist中可能已经存在一些之前的遗留文件,那我们再次打包,他可能只能覆盖那些同名的文件,对于其他那些已经移除的资源文件就会一直积累在里面,非常不合理。

更为合理的做法就是每次打包之前,自动去清理dist目录,那这样的话dist中就只会保留那些我们需要的文件。

clean-webpack-plugin就很好的实现了这样一个需求,他是一个第三方的插件,我们需要先安装他

yarn add clean-webpack-plugin --dev

安装过后我们回到webpack配置文件,然后去导入这个插件。这个webpack模块导出了一个叫做clean-webpack-plugin的成员,我们把它解构出来,然后回到配置对象当中。

那这里使用插件我们需要为配置对象添加一个plugins属性,那这个属性就是专门用来去配置插件的地方,那他是一个数组,我们去添加一个数组,就是在这个数组中去添加一个元素。

绝大多数插件模块导出的都是一个类型,我们这里的clean-webpack-plugin也不例外,所以我们使用他就是通过这个类型去创建一个实例,然后将这个实例放入到plugins这个数组当中。

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin()
    ]
}

完成以后我们再来尝试一下,回到命令行打包,此时发现,之前那些打包的结果就不会存在了,dist中就都是我们本次打包的结果,非常的干净。

Webpack 自动生成HTML插件

html-webpack-plugin

除了清理dist目录以外,还有一个非常常见的需求就是自动去生成使用打包结果的html,在这之前我们的html都是通过硬编码的方式,单独去存放在项目的跟目录下的,那这种方式有两个问题。

第一就是我们在项目发布时,我们需要用时去发布跟目录下的html文件和dist目录下所有的打包结果,这样的话相对麻烦一些。而且我们上线过后还需要去确保html代码当中路径,引用都是正确的。

第二个问题就是,如果说我们输出的目录或者是输出的文件名也就是我们打包结果的配置发生了变化,那html代码当中script标签所引用的那个路径也就需要我们手动的去修改,这是硬编码的方式存在的两个问题。

解决这两个问题最好的办法就是通过webpack自动去生成我们的html文件,也就是让html也自动参与到html构建的过程中去,那在构建过程中webpack知道生成了多少个bundle,会自动将这些打包的bundle添加到我们的页面当中。这样的话一来我们的html他也输出到了dist目录,上线时我们只需要把dist目录发布出去就可以了,二来我们html当中对于bundle的引用他是动态的注入进来的,不需要我们手动的去硬编码。所以他可以确保路径的引用是正常的。

具体的实现方式我们需要去借助一个叫做html-webpack-plugin的插件去实现,这个插件同样也是一个第三方模块,这里我们同样也需要单独去安装这个模块。

yarn html-webpack-plugin --dev

安装完成过后我们回到配置文件当中,然后载入这个模块。这里不同于clean-webpacl-plugin那html-webpack-plugin默认导出的就是一个插件的类型,我们不需要解构他内部的成员。

有了这个类型过后我们就可以回到配置文件的plugins属性当中,然后去添加一个这个类型的实例对象。这样的话就完成了我们这个插件的配置。

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin()
    ]
}

最后我们回到命令行终端,再次运行打包命令。此时我们的打包过程中就会自动生成一个index.html的一个文件,输出到dist目录。我们找到这个文件。

这个文件中的内容就是一段使用了我们bundle.js的一个空白的html,不过呢,这里的路径还是有一点问题,正确的路径应该是当前目录下的bundle.js而我们这里确生成了dist/bundle.js。

这是因为之前我们去尝试其他特性的时候我们去把output属性当中的publicPath设置成了dist,那现在呢我们的html是自动生成到了dist目录,所以我们就不再需要这样一个配置了,我们回到配置文件中去删除这样一个配置。

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin()
    ]
}

删除完成过后我们再次重新打包。打包完成过后我们的index.html当中对于bundle引用的路径已经正常了。至此我们就不再去需要跟目录下写死的html文件了,以后我们html文件都是通过自动取生成出来的。

但是这里仍然存在一些需要改进的地方,首先就是我们对于默认生成的html的标题是必须要修改的,另外我们很多时候还需要自定义页面中的一些原数据标签和基础的DOM结构,对于简单的自定义的话我们可以通过修改html-webpack-plugin这个插件一些属性来去实现,我们回到webpack的配置文件当中。

这里我们给html-webpack-plugin这个构造函数去传入一个对象参数,用于去指定我们的配置选项,那title属性就是用来设置我们html的标题。我们这里设置为webpack-plugin-simple。

meta属性可以以对象的属性设置页面中的一些原数据标签,例如我们尝试为页面添加一个viewport的设置。

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            }
        })
    ]
}

完成以后我们回到命令行重新打包,我们看下生成的html文件,此时这里的title和mate就根据我们配置文件当中的配置去生成。

如果我们需要对html文件进行大量的自定义的话,最好的做法就是在原代码当中添加一个用于去生成html文件的一个模板,然后让这个html-webpack-plugin的插件根据我们这个模板去生成页面。

我们在src目录下新建一个index.html的html模板,然后我们可以根据我们的需要在这个文件中添加一些相应的元素,对于模板当中我们希望动态输出的一些内容我们可以使用loadsh模板语法的方式去输出。这里我们可以通过htmlWebpackPlugin.options这个属性去访问到我们这个插件的配置数据, 那配置数据当中的title我们就可以直接输出出来。

当然htmlWebpackPlugin这个变量实际上是他内部提供的一个变量,也可以通过另外的属性去添加一些自定义的变量。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
    <script src="dist/"></script>
</body>
</html>

有了这个模板文件过后呢,我们回到配置文件当中,我们通过template属性去指定我们所使用的模板为src/index.html文件

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        })
    ]
}

再次打包,我们看下所生成的html文件,此时我们看到的html内容就是根据我们刚刚的模板去动态生成的了,以上就是我们自定义输出html内容的一些方式。

除了自定义输出文件的内容,同时去输出多个页面文件也是一个非常常见的需求,除非说我们的应用是一个单一页面应用程序,否则的话我们就一定需要多个html文件。

如果我们需要输出多个html文件其实也非常简单,我们回到配置文件当中。

这里我们刚刚通过html-webpack-plugin创建的对象,他是用于去生成index.html这个文件的。

那我们完全可以再去通过这个类型创建一个新的实例对象,用于去创建额外的html文件。

例如我们这里再去添加一个新的实例,然后用于去创建一个叫做about.html的页面文件,我们可以通过filename去指定输出的文件名,这个属性的默认值是index.html, 我们这里需要设置为about.html。

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

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html'
        })
    ]
}

重新打包, 此时dist目录下就同时生成了index.html和about.html两个页面文件。

根据这样一个尝试我们就知道,如果说我们需要去创建多个页面,那我们就可以在插件列表当中去加入多个htmlWebpackPlugin实例的对象,每个对象呢就是用来去负责生成一个页面文件的。

Webpack 插件使用总结

copy-webpack-plugin

在我们的项目中一般还有一些不需要参与构建的静态文件,那他们最终也需要发布到线上,他们最终也会发布到线上,例如我们网站的favicon.ico, 一般我会把这一类的文件统一放在项目根目录下的public目录当中。我们希望webpack在打包时可以一并将他们复制到输出目录。

对于这种需求我们可以借助于copy-webpack-plugin去实现。同样我们需要先安装这个插件。

准备public/favicon.icon文件。

yarn add copy-webpack-plugin --dev

安装完成过后我们回到配置文件当中,再去导入这个插件的类型。最后我们同样在这个plugins属性当中去添加这个类型的实例。

那这个类型的构造函数他要求我们传入一个数组,用于去指定我们需要copy的文件路径,那他可以是一个通配符,也可以是一个目录或者是文件的相对路径。

我们这里传入的是一个public/**目录。表示在打包时会将public目录下所有的文件全部拷贝到输出目录。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html'
        }),
        new CopyWebpackPlugin([
            'public/**'
        ])
    ]
}

完成以后我们重新打包,打包完成过后呢我们public目录下所有的文件就会同时copy到输出目录了。

总结

至此我们就了解了几个非常常用的插件,那这些插件呢一般都适用于任何类型的项目。不管说你的项目中有没有使用框架或者是使用了哪一个框架。那他们都基本上会用到。

所以之后最好能仔细去过一遍这些插件的官方说明,然后去看看他们还可以有哪些特别的用法,做到心中有数。

除此之外社区当中还提供了成百上千的插件,你并不需要全部认识,在你有一些特殊的需求时,再去提炼你需求当中的一些关键词然后去github上去搜索他们。

例如我们想要去压缩输出的图片,那我会去搜索imagemin webpack plugin。

虽然每个插件的作用不尽相同,但是他们在用法上几乎都是类似的。

Webpack 开发一个插件

通过前面的介绍我们知道相比于loader来说plugin能力范围相对会更宽一些,因为loader他只是在加载模块的环节去工作。而插件的作用范围几乎可以触及到webpack工作的每一个环节。

这样的插件机制究竟是如何实现的呢,其实说起来也非常简单。webpack的插件机制其实就是我们再软件开发过程中最长见到的,钩子机制。

那钩子机制也特别容易理解,有点类似我们web当中的事件。那在webpack的工作过程中会有很多的环节。为了便于插件的扩展。webpack几乎给每一个环节都埋下了一个钩子。

那这样的话我们再去开发插件的时候,我们就可以通过往这些不同的节点上面去挂载不同的任务,可以轻松地去扩展webpack的能力。

具体有哪些预先定义好的钩子,我们可以参考官方的API文档,接下来我们来定义一个插件,来看具体如何往这些钩子上去挂载任务。

webpack要求我们的插件必须是一个函数,或者是一个包含apply方法的对象。一般我们都会把这个插件定义为一个类型,然后在这个类型中去定义一个apply方法,我们使用的时候就是通过这个类型去构建一个实例,然后去使用。

所以我们这里定义一个MyPlugin的类型,然后我们在这个类型中去定义一个apply方法,这个方法会在webpack启动时自动被调用。接收一个compiler对象参数。这个对象就是webpack工作过程中最核心的一个对象。这个对象里面包含了我们此次构建的所有的配置信息,我们也是通过这个对象去注册钩子函数。

这里我们的需求是希望这个插件可以用来去清除webpack打包生成的js当中那些没有必要的注释,那这样一来的话我们bundle.js当中去除了这些注释之后就可以更加容易阅读。有了这个需求过后呢我们需要明确我们这个任务的执行时机,也就是我们要把这个任务挂载到哪个钩子上。

我们的需求是删除bundle.js当中的注释,也就是说我们只有当webpack需要生成这个bundle.js文件的内容明确了过后,我们才可以实施相应的动作,那我们回到webpack的官网。我们找到他的API文档。

在API文档当中我们找到一个叫做emit的钩子,根据文档当中的提示我们发现这个钩子在webpack即将要往输出目录输出文件时执行。非常符合我们的需求。

我们回到代码当中,我们通过compiler当中的hooks属性我们去访问到emit钩子。然后我们通过tap方法去注册一个钩子函数。

这个方法接收两个参数,第一个参数是插件的名称。我们这里是MyPlugin,第二个就是我们需要挂载到这个钩子上的函数。这里我们可以在函数中接收一个complation的对象参数,这个对象可以理解成此次打包过程的上下文。

我们所有打包过程中产生的结果都会放到这个对象中。我们这里使用一下这个对象的assets属性。然后去获取我们即将想入到目录文件中的资源信息。complation.assets;

那他是一个对象,我们这里通过for in 去遍历这个对象。那这个对象当中的键就是每一个文件的名称,我们尝试把他打印出来。然后将这个插件我们应用到我们的配置当中。通过 new MyPlugin的方式把他应用起来。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
class MyPlugin {
    apply(compiler) {
        console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                console.log(name);
            }
        })
    }
}

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html'
        }),
        new CopyWebpackPlugin([
            'public/**'
        ]),
        new MyPlugin()
    ]
}

然后回到命令行中再次运行打包。此时我们打包过程中就会输出出来我们打包的文件名称。那我们再回到代码当中我们再来尝试一下打印每一个资源文件的内容。

那文件的内容我们是要通过这个文件当中的值的source方法来去获取。

我们通过assets然后访问到具体属性的值,然后通过source方法拿到他对应的内容。

class MyPlugin {
    apply(compiler) {
        console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                console.log(assets[name].source());
            }
        })
    }
}

然后回到命令行,再次打包。此时呢打包过程中我们输出的文件内容也可以正常被打印。

可以拿到文件名和内容过后呢。我们再回到代码当中。我们要去判断我们的文件是否以.js结尾,因为我们这里的需求只是去处理js文件。所以如果不是js文件我们就不需要去处理他。

这里如果是js文件的话我们将文件的内容得到。然后我们通过正则的方式去替换掉我们代码当中对应的注释,这里需要注意的是我们正则要以全局模式去替换,将这个替换的结果我们需要去覆盖到原有的内容当中,我们要去覆盖complation当中的assets里面所对应的那个属性。

那这个属性的值我们同样去暴露一个source方法用来去返回我们这个新的内容。除此之外我们还需要一个size方法,用来去返回这个内容的大小,这个方法是webpack内部要求的一个必须的方法。

class MyPlugin {
    apply(compiler) {
        // console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                // console.log(assets[name].source());
                if (name.endsWith('.js')) {
                    const contents = complation.assets[name].source();
                    const withoutComments = contents.replace(/\/*\**\*\//g, '');
                    complation.assets[name] = {
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}

完成以后我们再次回到命令行终端,然后运行打包,打包完成过后我们再来看一下,bundle.js,此时bundle.js每一行开头的注释就都被移除掉了。

以上就是我们实现的一个移除webpack注释插件的一个过程,通过这个过程我们了解了,插件是通过往webpack生命周期里面的一些钩子函数里面去挂载我们任务函数来去实现的。

当然如果你需要深入去了解插件机制,可能需要去理解一些webpack底层的实现原理,那这些在我们的文档当中其实并没有详细的介绍。

所以你需要通过去阅读源代码来了解他们,关于源代码的一些其他信息或者是运行的底层原理,这些我们后面再来介绍。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值