其他分享
首页 > 其他分享> > 初探webpack之编写loader

初探webpack之编写loader

作者:互联网

初探webpack之编写loader

loader加载器是webpack的核心之一,其用于将不同类型的文件转换为webpack可识别的模块,即用于把模块原内容按照需求转换成新内容,用以加载非js模块,通过配合扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果,从而完成一次完整的构建。

描述

webpack是一个现代JavaScript应用程序的静态模块打包器module bundler,当webpack处理应用程序时,它会递归地构建一个依赖关系图dependency graph,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle
使用webpack作为前端构建工具通常可以做到以下几个方面的事情:

对于webpack来说,一切皆模块,而webpack仅能处理出js以及json文件,因此如果要使用其他类型的文件,都需要转换成webpack可识别的模块,即jsjson模块。也就是说无论什么后缀的文件例如pngtxtvue文件等等,都需要当作js来使用,但是直接当作js来使用肯定是不行的,因为这些文件并不符合js的语法结构,所以就需需要webpack loader来处理,帮助我们将一个非js文件转换为js文件,例如css-loaderts-loaderfile-loader等等。

在这里编写一个简单的webpack loader,设想一个简单的场景,在这里我们关注vue2,从实例出发,在平时我们构建vue项目时都是通过编写.vue文件来作为模块的,这种单文件组件的方式虽然比较清晰,但是如果一个组件比较复杂的话,就会导致整个文件相当大。当然vue中给我们提供了在.vue文件中引用jscss的方式,但是这样用起来毕竟还是稍显麻烦,所以我们可以通过编写一个webpack loader,在编写代码时将三部分即htmljscss进行分离,之后在loader中将其合并,再我们编写的loader完成处理之后再交与vue-loader去处理之后的事情。当然,关注点分离不等于文件类型分离,将一个单文件分成多个文件也只是对于代码编写过程中可读性的倾向问题,在这里我们重点关注的是编写一个简单的loader而不在于对于文件是否应该分离的探讨。文中涉及到的所有代码都在https://github.com/WindrunnerMax/webpack-simple-environment

实现

搭建环境

在这里直接使用我之前的 初探webpack之从零搭建Vue开发环境 中搭建的简单vue + ts开发环境,环境的相关的代码都在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--vue-cli分支中,我们直接将其clone并安装。

git clone https://github.com/WindrunnerMax/webpack-simple-environment.git
git checkout webpack--vue-cli
yarn install --registry https://registry.npm.taobao.org/

之后便可以通过运行yarn dev来查看效果,在这里我们先打印一下此时的目录结构。

webpack--vue-cli
├── dist
│   ├── static
│   │   └── vue-large.b022422b.png
│   ├── index.html
│   ├── index.js
│   └── index.js.LICENSE.txt
├── public
│   └── index.html
├── src
│   ├── common
│   │   └── styles.scss
│   ├── components
│   │   ├── tab-a.vue
│   │   └── tab-b.vue
│   ├── router
│   │   └── index.ts
│   ├── static
│   │   ├── vue-large.png
│   │   └── vue.jpg
│   ├── store
│   │   └── index.ts
│   ├── views
│   │   └── framework.vue
│   ├── App.vue
│   ├── index.ts
│   ├── main.ts
│   ├── sfc.d.ts
│   └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

编写loader

在编写loader之前,我们先关注一下上边目录结构中的.vue文件,因为此时我们需要将其拆分,但是如何将其拆分是需要考虑一下的,为了尽量不影响正常的使用,在这里采用了如下的方案。

通过以上的修改,我们将文件目录再次打印出来,重点关注于.vue文件的分离。

webpack--loader
├── dist
│   ├── static
│   │   └── vue-large.b022422b.png
│   ├── index.html
│   ├── index.js
│   └── index.js.LICENSE.txt
├── public
│   └── index.html
├── src
│   ├── common
│   │   └── styles.scss
│   ├── components
│   │   ├── tab-a
│   │   │   ├── tab-a.vue
│   │   │   └── tab-a.vue.ts
│   │   └── tab-b
│   │       ├── tab-b.vue
│   │       └── tab-b.vue.ts
│   ├── router
│   │   └── index.ts
│   ├── static
│   │   ├── vue-large.png
│   │   └── vue.jpg
│   ├── store
│   │   └── index.ts
│   ├── views
│   │   └── framework
│   │       ├── framework.vue
│   │       ├── framework.vue.scss
│   │       └── framework.vue.ts
│   ├── App.vue
│   ├── index.ts
│   ├── main.ts
│   ├── sfc.d.ts
│   └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── vue-multiple-files-loader.js
├── webpack.config.js
└── yarn.lock

现在我们开始正式编写这个loader了,首先需要简单说明一下loader的输入与输出以及常用的模块。

由于我们在这里这个需求是用不到AST相关的处理的,所以还是比较简单的一个实例,首先我们需要写一个loader文件,然后配置在webpack.config.js中,在根目录我们建立一个vue-multiple-files-loader.js,然后在webpack.config.jsmodule.rule部分找到test: /\.vue$/,将这部分修改为如下配置。

// ...
{
    test: /\.vue$/,
    use: [
        "vue-loader",
        {
            loader: "./vue-multiple-files-loader",
            options: {
                // 匹配的文件拓展名
                style: ["scss", "css"],
                script: ["ts"],
            },
        },
    ],
}
// ...

首先可以看到在"vue-loader"之后我们编写了一个对象,这个对象的loader参数是一个字符串,这个字符串是将来要被传递到require当中的,也就是说在webpack中他会自动帮我们把这个模块requirerequire("./vue-multiple-files-loader")webpack loader是有优先级的,在这里我们的目标是首先经由vue-multiple-files-loader这个loader将代码处理之后再交与vue-loader进行处理,所以我们要将vue-multiple-files-loader写在vue-loader后边,这样就会首先使用vue-multiple-files-loader代码了。我们通过options这个对象传递参数,这个参数可以在loader中拿到。
关于webpack loader的优先级,首先定义loader配置的时候,除了loaderoptions选项,还有一个enforce选项,其可接受的参数分别是pre: 前置loadernormal: 普通loaderinline: 内联loaderpost: 后置loader,其优先级也是pre > normal > inline > post,那么相同优先级的loader就是从右到左、从下到上,从上到下很好理解,至于从右到左,只是webpack选择了compose方式,而不是pipe的方式而已,在技术上实现从左往右也不会有难度,就是函数式编程中的两种组合方式而已。此外,我们在require的时候还可以跳过某些loader!跳过normal loader-!跳过prenormal loader!!跳过pre normalpost loader,比如require("!!raw!./script.coffee"),关于loader的跳过,webpack官方的建议是,除非从另一个loader处理生成的,一般不建议主动使用。

现在我们已经处理好vue-multiple-files-loader.js这个文件的创建以及loader的引用了,那么我们可以通过他来编写代码了,通常来说,loader一般是比较耗时的应用,所以我们通过异步来处理这个loader,通过this.async告诉loader-runner这个loader将会异步地回调,当我们处理完成之后,使用其返回值将处理后的字符串代码作为参数执行即可。

module.exports = async function (source) {
    const done = this.async();
    // do something
    done(null, source);
}

对于文件的操作,我们使用promisify来处理,以便我们能够更好地使用async/await

const fs = require("fs");
const { promisify } = require("util");

const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);

下面我们回到上边的需求上来,思路很简单,首先我们在这个loader中仅会收到以.vue结尾的文件,这是在webpack.config.js中配置的,所以我们在这里仅关注.vue文件,那么在这个文件下,我们需要获取这个文件所在的目录,然后将其遍历,通过webpack.config.js中配置的options来构建正则表达式去匹配同级目录下的scriptstyle的相关文件,对于匹配成功的文件我们将其读取然后按照.vue文件的规则拼接到source中,然后将其返回之后将代码交与vue-loader处理即可。
那么我们首先处理一下当前目录,以及当前处理的文件名,还有正则表达式的构建,在这里我们传递了scsscssts,那么对于App.vue这个文件来说,将会构建/App\.vue\.css$|App\.vue\.scss$/App\.vue\.ts$这两个正则表达式。

const filePath = this.context;
const fileName = this.resourcePath.replace(filePath + "/", "");

const options = loaderUtils.getOptions(this) || {};
const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));

之后我们通过遍历目录的方式,来匹配符合要求的scriptstyle的文件路径。

let stylePath = null;
let scriptPath = null;

const files = await readDir(filePath);
files.forEach(file => {
    if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
    if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
});

之后对于script部分,存在匹配节点且原.vue文件不存在script标签,则异步读取文件之后将代码进行拼接,如果拓展名不为js的话,例如是ts编写的那么就会将其作为lang="ts"去处理,之后将其拼接到source这个字符串中。

if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
    const extName = scriptPath.split(".").pop();
    if (extName) {
        const content = await readFile(scriptPath, "utf8");
        const scriptTagContent = [
            "<script ",
            extName === "js" ? "" : `lang="${extName}" `,
            ">\n",
            content,
            "</script>",
        ].join("");
        source = source + "\n" + scriptTagContent;
    }
}

之后对于style部分,存在匹配节点且原.vue文件不存在style标签,则异步读取文件之后将代码进行拼接,如果拓展名不为css的话,例如是scss编写的那么就会将其作为lang="scss"去处理,如果代码中存在单行的// scoped字样的话,就会将这个style部分作scoped处理,之后将其拼接到source这个字符串中。

if (stylePath && !/<style[\s\S]*?>/.test(source)) {
    const extName = stylePath.split(".").pop();
    if (extName) {
        const content = await readFile(stylePath, "utf8");
        const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
        const styleTagContent = [
            "<style ",
            extName === "css" ? "" : `lang="${extName}" `,
            scoped ? "scoped " : " ",
            ">\n",
            content,
            "</style>",
        ].join("");
        source = source + "\n" + styleTagContent;
    }
}

在之后使用done(null, source)触发回调完成loader的流程,相关代码如下所示,完整代码在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--loader分支当中。

const fs = require("fs");
const path = require("path");
const { promisify } = require("util");
const loaderUtils = require("loader-utils");

const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);

module.exports = async function (source) {
    const done = this.async();
    const filePath = this.context;
    const fileName = this.resourcePath.replace(filePath + "/", "");

    const options = loaderUtils.getOptions(this) || {};
    const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
    const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));

    let stylePath = null;
    let scriptPath = null;

    const files = await readDir(filePath);
    files.forEach(file => {
        if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
        if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
    });

    // 存在匹配节点且原`.vue`文件不存在`script`标签
    if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
        const extName = scriptPath.split(".").pop();
        if (extName) {
            const content = await readFile(scriptPath, "utf8");
            const scriptTagContent = [
                "<script ",
                extName === "js" ? "" : `lang="${extName}" `,
                ">\n",
                content,
                "</script>",
            ].join("");
            source = source + "\n" + scriptTagContent;
        }
    }

    // 存在匹配节点且原`.vue`文件不存在`style`标签
    if (stylePath && !/<style[\s\S]*?>/.test(source)) {
        const extName = stylePath.split(".").pop();
        if (extName) {
            const content = await readFile(stylePath, "utf8");
            const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
            const styleTagContent = [
                "<style ",
                extName === "css" ? "" : `lang="${extName}" `,
                scoped ? "scoped " : " ",
                ">\n",
                content,
                "</style>",
            ].join("");
            source = source + "\n" + styleTagContent;
        }
    }

    // console.log(stylePath, scriptPath, source);
    done(null, source);
};

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://webpack.js.org/api/loaders/
https://juejin.cn/post/6844904054393405453
https://segmentfault.com/a/1190000014685887
https://segmentfault.com/a/1190000021657031
https://webpack.js.org/concepts/loaders/#inline
http://t.zoukankan.com/hanshuai-p-11287231.html
https://v2.vuejs.org/v2/guide/single-file-components.html

标签:vue,const,ts,js,webpack,初探,loader
来源: https://www.cnblogs.com/WindrunnerMax/p/16223501.html