编写自己的Webpack Loader

8,539 阅读4分钟

本文将简单介绍webpack loader,以及如何去编写一个loader来满足自身的需求,从而也能提高对webpack的认识与使用,努力进阶为webpack配置工程师。

Webpack Loader

webpack想必前端圈的人都知道了,大多数人也都或多或少的用过。简单的说就是它能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。可以说,它作为一个打包工具,在前端工程化浪潮中,起到了中流砥柱的作用。

那webpack其中非常重要的一环就是,能够对加载的资源文件,进行一些处理。比如把less、sass文件编译成css文件,负责这个处理过程的,就是webpack的loader。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。

举个稍微复杂的例子,vue-loader,它官网介绍如下:

vue-loader 是一个 Webpack 的 loader,可以将指定格式编写的 Vue 组件转换为 JavaScript 模块。

Vue组件默认分成三部分,<template><script><style>,我们可以把一个组件要有的html,js,css写在一个组件文件中,而vue-loader,会帮助我们去处理这个vue组件,把其中的html,js,css分别编译处理,最终打包成一个模块。

明确自己需要什么Loader

我们知道了webpack的强大依托于一个个强大的 loader(当然还有plugin,本文就不介绍了)。如果想真的玩溜webpack,我们就必须掌握loader的使用。在我们使用它们前,我们得知道自己需要什么loader。如果想编译less,可以用less-loader;想加载html文件并打包它内链的静态文件,可以使用html-loader。只要我们想对文件进行处理时,我们都可以去找想对应的loader。

那么问题来了,万一找不到想要的loader该怎么办?

比如我前几天遇到了一个需求,我希望我加载的html文件,都嵌套在一个 layout.html 文件中。如下所示:

<!-- layout.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Pure Web</title>
  <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
</head>
<body>
  <header>This is Header</header>

  <!-- 我希望我webpack加载的html,都会被嵌套在这个{{__content__}}部分 -->
  {{__content__}}

  <footer>This is footer</header>
</body>
</html>

这样如果是编写多页应用,我就只需要编写唯一不一样的中心内容,而把网站公共的部分作为layout抽离开来。

可惜html-loader它只能帮我在一个html文件中去加载另外一个html文件,像这样:

<body>
  ${require('@/htmls/header.html')}
  ${require('@/htmls/index.html')}
  ${require('@/htmls/footer.html')}
</body>

这样虽然能抽离公共部分,但我依旧需要在每个html文件中去引用,而且为了保证html结构顺序,我得每个文件都再引一次headerfooter,没法将他们作为一个单独的layout来引入。所以它并不完全符合我的需求。

那该怎么办,我们没有办法,只能自己动手写啊。

动手写一个webpack loader

首先,我们要先阅读一遍webpack官网的介绍:如何编写一个loader?

看完后,我们能知道,loader本质就是接收字符串(或者buffer),再返回处理完的字符串(或者buffer)的过程。webpack会将加载的资源作为参数传入loader方法,交于loader处理,再返回。

在我这个需求中,就是将我加载的html,套在我设定的layout中,再将这个处理完的html返回。大致的代码就是这样:

// {string} source: 加载的html的字符串值
module.exports = function (source) {
  return getLayoutHtml().replace('{{__content__}}', source)
}

简单思考后,发现可行,那么开始编写。

开始尝试

所以,我们第一步,只要实现一个getLayoutHtml方法,能得到设定的layout.html文件就好。仔细想想,layout文件应该是通过配置声明的,然后在loader里去根据配置,调用node的api去加载文件就好。

查阅node与webpack文档,我们可以通过loader-utils来获取loader的配置项。通过node的fs.readFileSync去加载文件,那我们的代码大概可以这样写:

module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const layoutHtml = fs.readFileSync(options.layout, 'utf-8')
  return layoutHtml.replace('{{__content__}}', source)
}

webpack的config增加如下loader配置:

{
  test: /\.(html)$/,
  loader: 'html-layout-loader',
  include: htmlPath, // the htmls you want inject to layout
  options: {
    layout: layoutHtmlPath // the path of default layout html
  }
}

难以置信,这样就完成一个基础的html-layout-loader了。当然,这才是真正的开始,真正让loader变得可用,好用。我们还需要考虑很多情况。

完善自己的loader

明确代码可行后,我们得完善自己的功能点,我仔细想想,大概需要考虑如下功能:

  1. 针对每个加载的html,应该可以设定自己的layout文件,而不是所有的html,都必须加载同一个layout。
  2. 替换的占位符{{__content__}}也应该可以配置。

另外,还需要考虑一些异常的处理,如模板文件找不到。

完善自身的需求后,我们又可以编写代码了,这回我就不一行行阐述代码了,直接放链接:html-layout-loader

代码也比较简单,算是实现了自己的基本需求,大家有兴趣的话可以先看看readme的介绍。

写在最后

当我们在遇到大问题时,首先想到的总是去搜搜看有没有现成的解决方案,但现实却难免是没有解决方案。在这种情况下,我们也可以尝试着去写一些插件、组件、或者一个通用化的解决方案,来解决自身的问题,同时对自己掌握一些知识也会有帮助。而且尝试过后可能发现,它也没那么难嘛。

另外,如果这个loader,也对读者们有帮助的话,请尽情使用,有什么问题、想法可以提issue或PR。

--阅读原文 --转载请先经过本人授权-丁香园F2E @相学长