Используйте webpack-chain для создания конфигурации webpack

Webpack

предисловие

Обычно используется скаффолдинг vue-cli, а vue-cli4 использует цепной метод для изменения webpack.

Конфигурация веб-пакета внутри Vue CLI поддерживается через цепочку веб-пакетов. Эта библиотека обеспечивает абстракцию высокого уровня над необработанной конфигурацией веб-пакета, позволяя определять именованные правила загрузчика и именованные плагины, а также иметь возможность вводить эти правила и изменять их параметры на более позднем этапе.

Если у нас есть другие проекты (например, среда реагирования), которые не используют vue-cli, как мы можем управлять нашей собственной конфигурацией веб-пакета через цепочку веб-пакетов?

Общая базовая структура webpack.config

Обычно используемые структуры webpack.config в основном включают запись, вывод, разрешение, модуль, плагин и оптимизацию. Давайте посмотрим, как они настраиваются в цепочке webpack.

module.exports = {
  entry: {
    main: resolve('../src/main.js')
  },
  output: {
    path: resolve('../dist'),
    filename: 'bundle.[hash:6].js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src')
    }
  },
  module: {
    rules: [
       {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/]
        }
      },
    ]
  }
  plugins: [new VueLoaderPlugin()],
  optimization: {
    runtimeChunk: true
  }
}

Как использовать webpack-цепочку

Создать экземпляр конфигурации

Конфигурация объекта экземпляра здесь может быть понята как вышеприведенный module.exports = { // webpack.config }

// 导入 webpack-chain 模块,该模块导出了一个用于创建一个webpack配置API的单一构造函数。
const Config = require('webpack-chain');

// 对该单一构造函数创建一个新的配置实例
const config = new Config();

Просмотрите исходный код цепочки веб-пакетов. Структура конструктора, представленного в основном файле src/Config.js, выглядит следующим образом: Вы можете видеть, что в этом конструкторе есть много свойств и методов прототипа, Далее нашу конфигурацию необходимо реализовать с помощью этих методов и свойств экземпляра конфигурации.

config.entry и config.output

Параметры пути для исходных входных файлов и выходных пакетов.
Здесь следует отметить, что необходимо передать абсолютный путь, чтобы предотвратить влияние на путь, по которому команда выполняется в командной строке. Здесь вы можете использовать path.resolve(__dirname,src/index.js) или require.resolve('src/index.js'). Оба имеют одинаковый эффект.

const Config = require('webpack-chain')
const path = require('path')
const resolve = file => path.resolve(__dirname, file);
const config = new Config()
// 修改 entry 配置
config.entry('index')
      .add(resolve('src/index.js'))
      .end()
      // 修改 output 配置
      .output
        .path(resolve('out'))
        .filename('[name].bundle.js');

Взгляните на синтаксис цепочки webpack-chain.

  • .entry(index) Здесь задается имя файла, то есть оно соответствует имени в .filename('[name].bunle.js')
  • .add() добавляет путь к файлу записи
  • .output.path() добавляет путь к файлу экспорта
  • .filename() устанавливает имя файла пакета

Почему эти методы могут быть все время в цепочке?

Когда я выполняю config.entry('index') и передаю имя файла пакета, срабатывает следующий метод

  // Config.js 
  constructor(){
  	 this.entryPoints = new ChainedMap(this);
  }
  entry(name) {
    return this.entryPoints.getOrCompute(name, () => new ChainedSet(this));
  }

Когда метод входа выполняется в первый раз, он будет идти this.set(key,fn())
Основная логика в src/chainedMap.js заключается в создании хранилища, а в прототипе есть методы очистки, порядка, установки, getOrCompute и другие для выполнения некоторых дополнений, удалений, модификаций и проверок объекта карты хранилища.
Когда набор будет выполнен, **return this ** снова вернет экземпляр ChainedMap.С объектом экземпляра ChainedMap мы можем оперировать имеющимися у него методами.

// chainedMap.js
module.exports = class extends Chainable {
  constructor(parent) {
    super(parent);
    this.store = new Map();
  }
  getOrCompute(key, fn) {
    if (!this.has(key)) {
      this.set(key, fn());
    }
    return this.get(key);
  }
  set(key, value) {
    this.store.set(key, value);
    return this;
  }
}

Но сам chainedMap.js не определяет свойство вывода, что, если он может связать .output?

Передайте экземпляр Config.js для Config.js **this.entryPoints = new ChainedMap(this);** в Chainable.js через super(parent)

// src/ChainedMap.js 
const Chainable = require('./Chainable');
module.exports = class extends Chainable {
	constructor(parent){
    	super(parent)
    }
}

Чтобы увидеть Chainable.js, верните экземпляр Config.js через .end().
Окончательный вывод: если вы хотите манипулировать методами экземпляра Config.js, вам нужно передать .end(), чтобы вернуть сам экземпляр.

// src/Chainable.js 
module.exports = class {
  constructor(parent) {
    this.parent = parent;
  }

  batch(handler) {
    handler(this);
    return this;
  }

  end() {
    return this.parent;
  }
};

добавить загрузчик

Возьмем, к примеру, загрузчики css. Загрузчики выполняются справа налево, то есть в порядке поступления.
Здесь сначала запускается css-loader для обработки совпавших css-файлов, а затем он передается extrat-css-loader для извлечения отдельных css-файлов.

config.module
    .rule('css')
      .test(/\.(le|c)ss$/)
        .use('extract-css-loader')
          .loader(require('mini-css-extract-plugin').loader)
          .options({
            publicPath: './'
          })
          .end()
        .use('css-loader')
          .loader('css-loader')
          .options({});

Взгляните на синтаксис здесь:

  • .rule() дает последующим загрузчикам именованный заголовок, неважно, как он называется (лучше смотрите имя)
  • .test() принимает обычное правило
  • Перед использованием .loader необходимо добавить именованный загрузчик .use(), иначе будет сообщено об ошибке

  • .loader() принимает строку загрузчика или абсолютный путь к загрузчику
  • .end() Как упоминалось в предыдущем анализе исходного кода, это для получения объекта экземпляра Config.
  • .options() дополнительная конфигурация загрузчика

нужно знать, это:

  • Несколько загрузчиков совместно используют один .rule() и один .test(), что означает, что ваши тестовые правила одинаковы, вы можете написать их вместе, но каждый загрузчик напрямую должен передать .end(), чтобы снова получить объект экземпляра Config, потому что после операции loader() или .options() он больше не является объектом экземпляра Config.
  • Если ваши правила тестирования отличаются, вы можете переконфигурировать.module.rule().test() и начать новый

добавить плагин

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
config.plugin('MiniCssExtractPlugin').use(MiniCssExtractPlugin)
  • config.plugin(name) Имя здесь — это ключ в цепочке webpack, который тоже можно воспринимать небрежно.
  • Если вы зарегистрировали это значение ключа ранее, вы можете позже получить этот плагин через config.plugin(name) и выполнить другие операции по настройке.
const htmlPlugin = require('html-webpack-plugin')
// 注册 plugin 名为 html
config.plugin('html').use(htmlPlugin,[{}])
// 通过 html 拿到前面注册的插件,通过 tap 来操作 options 
config.plugin('html').tap(args => {
  args[0].title = '这里是标题'
  args[0].template = resolve('public/index.html')
  return args
})
  • .use(WebpackPlugin,args) добавить конфигурацию и массив конфигурации [options1,options2]
  • .tap(function(args):args{}) Функция обратного вызова принимает args в качестве параметра, который является вторым параметром, передаваемым .use(), вы можете внести некоторые изменения в этот массив args, и, наконец, вам нужно его вернуть .

config.resolve

config.resolve.extensions.merge(['.ts','.js', '.jsx', '.vue', '.json'])
config.resolve.alias
      .set('SRC', resolve('src'))
      .set('ASSET', resolve('src/assets'))

config.optimization

config.optimization
	.runtimeChunk(true)

config.toConfig()

Экспортируйте конфигурацию и вызовите метод .toConfig() перед переходом к веб-пакету, чтобы экспортировать конфигурацию для использования веб-пакетом.

const chalk = require('chalk')
const config = require('./base')
const webpack = require('webpack')
webpack(config.toConfig(),(err,stats) => {
  if (stats.hasErrors()) {
    console.log(chalk.red('构建失败\n'))
    // 返回描述编译信息 ,查看错误信息
    console.log(stats.toString())
    process.exit(1)
  }

  console.log(chalk.cyan('build完成\n'))
})

Общая конфигурация

css loaders

config.module
    .rule('css')
      .test(/\.(le|c|postc)ss$/)
      	// 提取 css 到单独文件
        .use('extract-css-loader')
          .loader(require('mini-css-extract-plugin').loader)
          .options({
            publicPath: './'
          })
          .end()
        .use('css-loader')
          .loader('css-loader')
          .options({})
          .end()
         // 可以引入多个插件
         // autoprefixer 自动添加兼容 css 前缀
         // stylelint css 规范化
        .use('postcss-loader')
          .loader('postcss-loader')
          .options({
            plugins: function() {
              return [
                require('autoprefixer')({
                  overrideBrowserslist: ['>0.25%', 'not dead']
                }),
                require("stylelint")({
                  /* your options */
                }),
                require("postcss-reporter")({ clearReportedMessages: true })
              ]
            }
          })
          .end()
        .use('less-loader')
          .loader('less-loader')
          .end()

html-webpack-plugin

const htmlPlugin = require('html-webpack-plugin')
config.plugin('html').use(htmlPlugin,[
	{ title:'标题党',template:require.resolve('public/index.html') }
])

babel анализирует файлы .ts

ts файлы можно разобрать после настройки через babel-loader + @babel/preset-typescript

// webpack.config
config.module
      .rule('babel-loader')
        .test(/\.tsx?$/)
          .use('babel-loader')
            .loader('babel-loader')
            .options({
              presets: ['@babel/preset-env']
            })
            .end()
            
// .babelrc
{
  "presets": ["@babel/preset-typescript"]
}

ts-loader + tsconfig.json

Если вы используете ts-loader, он по умолчанию прочитает конфигурацию tsconfig.json для анализа файла ts.

// webpack.config
config.module
      .rule('ts-loader')
        .test(/\.tsx?$/)
          .use('babel-loader')
            .loader('babel-loader')
            .options({
              presets: ['@babel/preset-env']
            })
            .end()
          .use('ts-loader')
            .loader('ts-loader')
            .options({
              appendTsSuffixTo: [/\.vue$/]
            })
            .end()
// tsconfig.json
{
  "compilerOptions": {
    // Target latest version of ECMAScript.
    "target": "esnext",
    // Search under node_modules for non-relative imports.
    "moduleResolution": "node",
    // Process & infer types from .js files.
    "allowJs": true,
    // 不生成输出文件
    // "noEmit": true,
    // Enable strictest settings like strictNullChecks & noImplicitAny.
    "strict": true,
    // Disallow features that require cross-file information for emit.
    "isolatedModules": false,
    // Import non-ES modules as default imports.
    "esModuleInterop": true
  },
  "include": [
    "test"
  ]
}

Связь между tsconfig.json и .babelrc

  • Оба могут быть настроены для преобразования ts в js
  • Проект tsconfig.json предназначен для ts, ts-loader, если вы используете файл, вам нужно объявить все правила для настройки файлов ts. Результаты после преобразования ts-loader в babel-loader

Если вы используете только ts-loader, вы можете увидеть скомпилированные результаты.Стрелочные функции, которым нужны полифиллы, находятся непосредственно в упакованном пакете.После добавления babel-loader он будет преобразован в анонимную функцию.

Ссылаться на

webpack-chain
workflow
@babel/preset-typescript