webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(一)

简介: webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader

前言


本文会带你简单的认识一下webpack的loader,动手实现一个利用md转成抽象语法树,再转成html字符串的loader。顺便简单的了解一下几个style-loader,vue-loader,babel-loader的源码以及工作流程。


loader简介


webpack允许我们使用loader来处理文件,loader是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。loader文件处理器是一个CommonJs风格的函数,该函数接收一个 String/Buffer 类型的入参,并返回一个 String/Buffer 类型的返回值。


loader 的配置的两种形式


方案1:

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [{
      test: /.vue$/,
      loader: 'vue-loader'
    }, {
      test: /.scss$/,
      // 先经过 sass-loader,然后将结果传入 css-loader,最后再进入 style-loader。
      use: [
        'style-loader',//从JS字符串创建样式节点
        'css-loader',// 把  CSS 翻译成 CommonJS
        {
          loader: 'sass-loader',
          options: {
            data: '$color: red;'// 把 Sass 编译成 CSS
          }
        }
      ]
    }]
  }
  ...
}

方法2(右到左地被调用)

// module
import Styles from 'style-loader!css-loader?modules!./styles.css';

当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。像流水线一样,挨个处理每个loader,前一个loader的结果会传递给下一个loader,最后的 Loader 将处理后的结果以 String 或 Buffer 的形式返回给 compiler。


使用 loader-utils 能够编译 loader 的配置,还可以通过 schema-utils 进行验证


import { getOptions } from 'loader-utils'; 
import { validateOptions } from 'schema-utils';  
const schema = {
  // ...
}
export default function(content) {
  // 获取 options
  const options = getOptions(this);
  // 检验loader的options是否合法
  validateOptions(schema, options, 'Demo Loader');
  // 在这里写转换 loader 的逻辑
  // ...
   return content;   
};
  • content: 表示源文件字符串或者buffer
  • map: 表示sourcemap对象
  • meta: 表示元数据,辅助对象


同步loader


同步 loader,我们可以通过returnthis.callback返回输出的内容

module.exports = function(content, map, meta) {
  //一些同步操作
  outputContent=someSyncOperation(content)
  return outputContent;
}

如果返回结果只有一个,也可以直接使用 return 返回结果。但是,如果有些情况下还需要返回其他内容,如sourceMap或是AST语法树,这个时候可以借助webpack提供的api this.callback

module.exports = function(content, map, meta) {
  this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap?: SourceMap,
    meta?: any
  );
  return;
}

第一个参数必须是 Error 或者 null 第二个参数是一个 string 或者 Buffer。可选的:第三个参数必须是一个可以被这个模块解析的 source map。可选的:第四个选项,会被 webpack 忽略,可以是任何东西【可以将抽象语法树(abstract syntax tree - AST)(例如 ESTree)作为第四个参数(meta),如果你想在多个 loader 之间共享通用的 AST,这样做有助于加速编译时间。】。


异步loader


异步loader,使用 this.async 来获取 callback 函数。

// 让 Loader 缓存
module.exports = function(source) {
    var callback = this.async();
    // 做异步的事
    doSomeAsyncOperation(content, function(err, result) {
        if(err) return callback(err);
        callback(null, result);
    });
};

详情请参考官网API


开发一个简单的md-loader


const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (content) {
   this.cacheable && this.cacheable();
   const options = loaderUtils.getOptions(this);
   try {
       marked.setOptions(options);
       return marked(content)
   } catch (err) {
       this.emitError(err);
       return null
   }
};

上述的例子是通过现成的插件把markdown文件里的content转成html字符串,但是如果没有这个插件,改怎么做呢?这个情况下,我们可以考虑另外一种解法,借助 AST 语法树,来协助我们更加便捷地操作转换。


利用 AST 作源码转换


markdown-ast是将markdown文件里的content转成数组形式的抽象语法树节点,操作 AST 语法树远比操作字符串要简单、方便得多:

//通过正则的方法把字符串处理成直观的AST语法树
const md = require('markdown-ast');
module.exports = function(content) {
    this.cacheable && this.cacheable();
    const options = loaderUtils.getOptions(this);
    try {
      console.log(md(content))
      const parser = new MdParser(content);
      return parser.data
    } catch (err) {
      console.log(err)
      return null
    }
};
const md = require('markdown-ast');
const hljs = require('highlight.js');//代码高亮插件
// 利用 AST 作源码转换
class MdParser {
 constructor(content) {
    this.data = md(content);
    console.log(this.data)
  this.parse()
 }
 parse() {
  this.data = this.traverse(this.data);
 }
 traverse(ast) {
    console.log("md转抽象语法树操作",ast)
     let body = '';
    ast.map(item => {
      switch (item.type) {
        case "bold":
        case "break":
        case "codeBlock":
          const highlightedCode = hljs.highlight(item.syntax, item.code).value
          body += highlightedCode
          break;
        case "codeSpan":
        case "image":
        case "italic":
        case "link":
        case "list":
          item.type = (item.bullet === '-') ? 'ul' : 'ol'
          if (item.type !== '-') {
            item.startatt = (` start=${item.indent.length}`)
          } else {
            item.startatt = ''
          }
          body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
          break;
        case "quote":
          let quoteString = this.traverse(item.block)
          body += '<blockquote>\n' + quoteString + '</blockquote>\n';
          break;
        case "strike":
        case "text":
        case "title":
          body += `<h${item.rank}>${item.text}</h${item.rank}>`
          break;
        default:
          throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
      }
    })
    return body
 }
}

md 转成抽象语树3c3feecf451b7f7ae89a4c434d13272c_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpgast抽象语法数转成html字符串1070548e662d6f2d7bd2fc7efd178e74_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.pngmd2html-loader源码地址(https://github.com/6fedcom/fe-blog/blob/master/webpack-loader/loaders/md-loader.js)


loader的一些开发技巧


  1. 尽量保证一个loader去做一件事情,然后可以用不同的loader组合不同的场景需求
  2. 开发的时候不应该在 loader 中保留状态。loader必须是一个无任何副作用的纯函数,loader支持异步,因此是可以在 loader 中有 I/O 操作的。
  3. 模块化:保证 loader 是模块化的。loader 生成模块需要遵循和普通模块一样的设计原则。
  4. 合理的使用缓存 合理的缓存能够降低重复编译带来的成本。loader 执行时默认是开启缓存的,这样一来, webpack 在编译过程中执行到判断是否需要重编译 loader 实例的时候,会直接跳过 rebuild 环节,节省不必要重建带来的开销。但是当且仅当有你的 loader 有其他不稳定的外部依赖(如 I/O 接口依赖)时,可以关闭缓存:
this.cacheable&&this.cacheable(false);
  1. loader-runner 是一个非常实用的工具,用来开发、调试loader,它允许你不依靠 webpack 单独运行 loadernpm install loader-runner --save-dev
// 创建 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
  {
    resource: "./readme.md",
    loaders: [path.resolve(__dirname, "./loaders/md-loader")],
    readResource: fs.readFile.bind(fs),
  },
  (err, result) => 
    (err ? console.error(err) : console.log(result))
);

执行 node run-loader


认识更多的loader


style-loader源码简析


作用:把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML 里 看下源码。

先去掉option处理代码,这样就比较清晰明了了77f88c9373cfb2ce5abec6204f2b4bad_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png返回一段js代码,通过require来获取css内容,再通过addStyle的方法把css插入到dom里 自己实现一个简陋的style-loader.js

module.exports.pitch = function (request) {
  const {stringifyRequest}=loaderUtils
  var result = [
    //1. 获取css内容。2.// 调用addStyle把CSS内容插入到DOM中(locals为true,默认导出css)
    'var content=require(' + stringifyRequest(this, '!!' + request) + ')’, 
    'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’, 
    'if(content.locals) module.exports = content.locals’ 
  ]
  return result.join(';')
}

需要说明的是,正常我们都会用default的方法,这里用到pitch方法。pitch 方法有一个官方的解释在这里 pitching loader。简单的解释一下就是,默认的loader都是从右向左执行,用 pitching loader 是从左到右执行的。

{
  test: /\.css$/,
  use: [
    { loader: "style-loader" },
    { loader: "css-loader" }
  ]
}

为什么要先执行style-loader呢,因为我们要把css-loader拿到的内容最终输出成CSS样式中可以用的代码而不是字符串。

addstyle.js

module.exports = function (content) {
  let style = document.createElement("style")
  style.innerHTML = content
  document.head.appendChild(style)
}

目录
相关文章
|
25天前
|
前端开发
在Webpack配置文件中,如何配置loader以处理其他类型的文件,如CSS或图片
在Webpack配置文件中,通过设置`module.rules`来配置loader处理不同类型的文件。例如,使用`css-loader`和`style-loader`处理CSS文件,使用`file-loader`或`url-loader`处理图片等资源文件。配置示例:在`rules`数组中添加对应规则,指定`test`匹配文件类型,`use`指定使用的loader。
|
25天前
|
前端开发 JavaScript
webpack 中 loader 和 plugin 的区别
在 webpack 中,loader 用于转换模块的源代码,如将 TypeScript 转为 JavaScript;而 plugin 则扩展了 webpack 的功能,可以执行更复杂的任务,如优化打包文件、注入环境变量等。两者共同作用于构建流程的不同阶段。
|
25天前
|
JavaScript 前端开发
Webpack中loader的使用场景
Webpack中的Loader用于处理和转换模块文件,如将TypeScript转为JavaScript、CSS预处理等,通过配置不同的Loader,可以灵活地支持多种文件类型和语言,实现模块化开发与构建优化。
|
12天前
|
Web App开发 移动开发 HTML5
html5 + Three.js 3D风雪封印在棱镜中的梅花鹿动效源码
html5 + Three.js 3D风雪封印在棱镜中的梅花鹿动效源码。画面中心是悬浮于空的梅花鹿,其四周由白色线段组成了一个6边形将中心的梅花鹿包裹其中。四周漂浮的白雪随着多边形的转动而同步旋转。建议使用支持HTML5与css3效果较好的火狐(Firefox)或谷歌(Chrome)等浏览器预览本源码。
42 2
|
24天前
|
移动开发 JavaScript HTML5
HTML5实现2025雪花飘新年倒计时源码
2025年即将到来,此源码页为单html纯代码,新年倒计时,背景雪花飘落效果,倒计时时间日期在JS/app.js文件第21行代码自行修改即可!
80 7
|
1月前
|
定位技术
时尚的联系我们表单HTML模板(源码)
一款时尚的联系我们表单Html模板,带地图和所在位置,输入基本信息和信息发送,看起来很漂亮的联系我们页面。
55 1
时尚的联系我们表单HTML模板(源码)
|
28天前
斗地主单机游戏HTML源码
斗地主单机游戏HTML源码,可以作为课程设计项目参考,喜欢的朋友可以拿去
36 5
|
23天前
|
前端开发
基于canvas实现的彩色纸屑组成文字3d动画HTML源码
基于canvas实现的彩色纸屑组成文字3d动画HTML源码
19 0
基于canvas实现的彩色纸屑组成文字3d动画HTML源码
|
23天前
|
移动开发 前端开发 HTML5
HTML5 Canvas制作的粒子十秒倒计时源码
一段基于HTML5 Canvas制作的粒子爆炸,十秒数字倒计时,全屏倒计时动画效果,给人一种非常大气的视觉感
22 0
HTML5 Canvas制作的粒子十秒倒计时源码
|
29天前
渐淡背景导航页HTML源码
每五秒进行淡进淡出切换背景图,适合作为个人引导页,喜欢的朋友拿去吧。
25 2