Краткий анализ исходного кода распространенного загрузчика и практическая реализация md2html-загрузчика.

Webpack

webpack series 1: краткий анализ исходного кода распространенного загрузчика и практическая реализация md2html-загрузчика

WebPack Series 2: Секретный рабочий папак карты карты

Webpack, серия 3: чтение исходного кода основного процесса webpack и реализация webpack

Эта статья даст вам краткое представление о загрузчике webpack и реализует загрузчик, который использует md для преобразования в абстрактное синтаксическое дерево, а затем в строку html. Кстати, вкратце разберитесь с исходным кодом и рабочим процессом нескольких style-loader, vue-loader, babel-loader.

Знакомство с загрузчиком

webpack позволяет нам обрабатывать файлы с помощью загрузчика, модуля узла, который экспортируется как функция. Соответствующие файлы можно преобразовать один раз, а загрузчик можно объединить в цепочку. Обработчик файла загрузчика — это функция в стиле CommonJs, которая принимает входной параметр типа String/Buffer и возвращает возвращаемое значение типа String/Buffer.

Две формы конфигурации загрузчика

план 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-utils, а также может быть проверена с помощью 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;   
};
  • содержимое: представляет строку исходного файла или буфер
  • карта: представляет объект исходной карты.
  • мета: представляет метаданные, вспомогательный объект

синхронный загрузчик

Для синхронизации загрузчика мы можем передатьreturnа такжеthis.callbackвернуть вывод

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

Если есть только один возвращаемый результат, вы также можете использовать return для прямого возврата результата. Однако, если в некоторых случаях вам нужно вернуть другой контент, например, sourceMap или синтаксическое дерево AST, вы можете использовать API, предоставляемый веб-пакетом в настоящее время.this.callback

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

Первый параметр должен быть Error или null Второй параметр — это строка или буфер. Необязательно: третий аргумент должен быть исходной картой, которую может разрешить этот модуль. Необязательно: четвертый параметр, который игнорируется веб-пакетом, может быть любым. Общий AST используется совместно загрузчиками, что помогает ускорить время компиляции. ].

асинхронный загрузчик

Асинхронный загрузчик, используйте this.async для получения функции обратного вызова.

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

Для получения подробной информации см.API официального сайта

Разработать простой md-загрузчик

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
   }
    
};

В приведенном выше примере используется существующий плагин для преобразования содержимого файла уценки в строку html, но если такого плагина нет, как его изменить? В этом случае мы можем рассмотреть другое решение с помощью синтаксического дерева AST, которое поможет нам более удобно выполнять преобразование.

Использование AST для преобразования исходного кода

markdown-astЭто абстрактный узел синтаксического дерева, который преобразует содержимое файла уценки в форму массива.Много проще и удобнее манипулировать синтаксическим деревом AST, чем манипулировать строками:

const md = require('markdown-ast');//通过正则的方法把字符串处理成直观的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
    }
};

md преобразуется в абстрактное дерево языка обычным методом вырезания

md2ast

const md = require('markdown-ast');//md通过正则匹配的方法把buffer转抽象语法树
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
	}
}

Полный код можно найти здесь

ast абстрактный синтаксический номер в строку html

md2html

Некоторые навыки разработки загрузчика

  1. Постарайтесь сделать так, чтобы один загрузчик выполнял одну задачу, а затем вы можете использовать разные загрузчики для комбинирования разных требований сцены.
  2. Вы не должны сохранять состояние в загрузчике во время разработки. Загрузчик должен быть чистой функцией без каких-либо побочных эффектов.Загрузчик поддерживает асинхронность, поэтому в загрузчике можно выполнять операции ввода-вывода.
  3. Модульность: Убедитесь, что загрузчик является модульным. Модуль, сгенерированный загрузчиком, должен следовать тем же принципам проектирования, что и обычные модули.
  4. Разумное использование кеша Разумное кэширование может снизить стоимость повторной компиляции. Когда загрузчик выполняется, кеш включается по умолчанию. Таким образом, когда веб-пакет определяет, следует ли перекомпилировать экземпляр загрузчика в процессе компиляции, он напрямую пропускает ссылку на перестроение, экономя затраты на ненужную реконструкцию. Но вы можете отключить кеширование тогда и только тогда, когда ваш загрузчик имеет другие нестабильные внешние зависимости (например, зависимости интерфейса ввода-вывода):
this.cacheable&&this.cacheable(false);
  1. loader-runnerЭто очень полезный инструмент для разработки и отладки загрузчиков, который позволяет запускать загрузчики независимо от веб-пакета.npm 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

Познакомьтесь с другими погрузчиками

Анализ исходного кода загрузчика стилей

Функция: вставьте стиль в DOM, вставив тег стиля в заголовок, и запишите стиль в innerHTML тега. Посмотрите на исходный код.

Сначала удалите код обработки опции, чтобы было понятнее

style-loader
Вернуть кусок кода js, получить содержимое css через require, а затем вставить css в dom через метод addStyle реализовать элементарный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(';')
}

Следует отметить, что обычно мы будем использовать метод по умолчанию, а здесь используется метод шага. Метод шага имеет официальное объяснение здесь, на загрузчике шага. Простое объяснение состоит в том, что загрузчик по умолчанию выполняется справа налево, с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)
}
Анализ исходного кода babel-загрузчика

Сначала посмотрите на обработку конфигурации skip loader и посмотрите на вывод babel-loader.

babel-loader-console
Выше мы видим, что выводtranspile(source, options)код и карта посмотри сноваtranspileчто сделал метод
babel-loader-transpile
babel-loader компилирует код через babel.transform, Так кажется, поэтому нам нужно всего несколько строк кода, чтобы реализовать простенький загрузчик babel

const babel = require("babel-core")
module.exports = function (source) {
  const babelOptions = {
    presets: ['env']
  }
  return babel.transform(source, babelOptions).code
}
Анализ исходного кода vue-loader

компонент одного файла vue (сокращенно sfc)

<template>
  <div class="text">
    {{a}}
  </div>
</template>
<script>
export default {
  data () {
    return {
      a: "vue demo"
    };
  }
};
</script>
<style lang="scss" scope>
.text {
  color: red;
}
</style>

конфигурация веб-пакета

const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  }

  plugins: [
    new VueloaderPlugin()
  ]
  ...
}

VueLoaderPluginРоль: Скопируйте и примените другие правила, определенные в webpack.config, к блоку соответствующего языка в файле .vue.

plugin-webpack4.js

 const vueLoaderUse = vueUse[vueLoaderUseIndex]
    vueLoaderUse.ident = 'vue-loader-options'
    vueLoaderUse.options = vueLoaderUse.options || {}
    // cloneRule会修改原始rule的resource和resourceQuery配置,
    // 携带特殊query的文件路径将被应用对应rule
    const clonedRules = rules
      .filter(r => r !== vueRule)
      .map(cloneRule)

    // global pitcher (responsible for injecting template compiler loader & CSS
    // post loader)
    const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
      resourceQuery: query => {
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }

    // 更新webpack的rules配置,这样vue单文件中的各个标签可以应用clonedRules相关的配置
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]

Получатьwebpack.config.jsэлемент правил, а затем скопируйте правила для переноса?vue&lang=xx...queryФайл параметров зависит от конфигурации того же загрузчика, что и файл с суффиксом xx. Настройте общедоступный загрузчик для файлов Vue: кувшин Буду[pitchLoder, ...clonedRules, ...rules]В качестве новых правил для webapck.

посмотри сноваvue-loaderвывод результата

vue-loader-result
Когда вводится файл vue, vue-loader анализирует однофайловый компонент vue, получает соответствующее содержимое каждого блока и преобразует Vue SFC различных типов блочных компонентов в строки модуля js.

// vue-loader使用`@vue/component-compiler-utils`将SFC源码解析成SFC描述符,,根据不同 module path 的类型(query 参数上的 type 字段)来抽离 SFC 当中不同类型的 block。
const { parse } = require('@vue/component-compiler-utils')
// 将单个*.vue文件内容解析成一个descriptor对象,也称为SFC(Single-File Components)对象
// descriptor包含template、script、style等标签的属性和内容,方便为每种标签做对应处理
const descriptor = parse({
  source,
  compiler: options.compiler || loadTemplateCompiler(loaderContext),
  filename,
  sourceRoot,
  needMap: sourceMap
})

// 为单文件组件生成唯一哈希id
const id = hash(
  isProduction
  ? (shortFilePath + '\n' + source)
  : shortFilePath
)
// 如果某个style标签包含scoped属性,则需要进行CSS Scoped处理
const hasScoped = descriptor.styles.some(s => s.scoped)

Затем на следующем шаге в процесс компиляции webpack добавляется только что сгенерированный js-модуль, то есть выполняется AST-анализ этого js-модуля и процесс сбора связанных зависимостей.

Давайте посмотрим, как исходный код работает с разными типами (template/script/style), метод selectBlock в основном основан на разных типах для получения содержимого контента соответствующего типа на дескрипторе и передачи его следующему загрузчику для обработки

vue-loader-code
Эти три фрагмента кода могут анализировать различные типы в строку импорта.

import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"

Кратко опишите рабочий процесс vue-loader

  1. регистрVueLoaderPluginВ плагине будет скопирован пункт правил в конфигурации вебпака текущего проекта.Когда путь к ресурсу содержит query.lang, те же самые правила сопоставляются через resourceQuery и выполняется соответствующий загрузчик. Вставьте общедоступный загрузчик и вставьте соответствующий пользовательский загрузчик в соответствии с query.type на этапе подачи.
  2. Вызывается при загрузке *.vuevue-loader, файл .vue анализируется вdescriptorобъект, в том числеtemplate、script、stylesи другие атрибуты соответствуют каждой метке, Для каждого тега он будет склеен в соответствии с атрибутом тегаsrc?vue&queryСправочный код, где src — это путь к одностраничному компоненту, а запрос — параметр некоторых функций, наиболее важными из которых являются язык, тип и область действия. Если включен атрибут lang, он будет соответствовать тем же правилам, что и суффикс, и применит соответствующие загрузчики. Выполните соответствующий пользовательский загрузчик в соответствии с типом,templateбудет выполнятьtemplateLoader,styleбудет выполнятьstylePostLoader
  3. существуетtemplateLoader, пройдешьvue-template-compilerПреобразование шаблона в функцию рендеринга, в процессе, будет передан вscopeIdДобавьте к каждому тегу и, наконец, передайте его как свойство конфигурации vnode вcreateElemenetметод, Когда функция рендеринга вызывается и страница визуализируется, онаscopeIdАтрибуты отображаются на странице как необработанные атрибуты.
  4. существуетstylePostLoader, парсинг содержимого тега стиля через PostCSS

Исходный код демо, использованный в этой статье

Нажмите на репозиторий github

использованная литература

  1. api загрузчика официального веб-сайта webpack
  2. Научу вас писать webpack yaml-loader
  3. Серия анализов исходного кода Yanchuan-webpack
  4. Анализ реализации CSS Scoped из исходного кода vue-loader