webpack 核心工作原理

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

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

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

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

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

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工作管道这样一个特性。