Шаг за шагом, чтобы создать большой проект с помощью веб-пакета с нуля

внешний интерфейс

открытие

Многие люди более или менее использовали webpack, но мало кто может систематически изучать настройку webpack, так как при столкновении с ошибками они будут растеряны и не будут знать, с чего начать? Я не знаю, что делать при оптимизации производительности.Подойдет ли онлайн-учебник по оптимизации для моего собственного проекта? Жду серию вопросов! Эта статья представляет собой пошаговый процесс от самой базовой конфигурации до полного крупномасштабного проекта. Пусть вы больше не боитесь вебпака, пусть он действительно станет вашей правой рукой!

В данной работе реализованы следующие темы:

адрес проекта

GitHub.com/falling snow-Вик Т…

Я разделяю каждый урок на разные ветки, каждый может учиться шаг за шагом в соответствии со временем занятий

строительные леса

npm i -g webpack-box

использовать

webpack-box dev   # 开发环境
webpack-box build # 生产环境
webpack-box dll   # 编译差分包
webpack-box dev index   # 指定页面编译(多页面)
webpack-box build index # 指定页面编译(多页面)
webpack-box build index --report # 开启打包分析
webpack-box build:ssr  # 编译ssr
webpack-box ssr:server # 在 server 端运行

использовать в package.json

{
  "scripts": {
    "dev": "webpack-box dev",
    "build": "webpack-box build",
    "dll": "webpack-box dll",
    "build:ssr": "webpack-box build:ssr",
    "ssr:server": "webpack-box ssr:server"
  }
}

использовать

npm run build --report # 开启打包分析

Расширенная конфигурация

box.config.js

module.exports = function (config) {
  /**
   * @param {object} dll 开启差分包
   * @param {object} pages 多页面配置 通过 box run/build index 来使用
   * @param {function} chainWebpack 
   * @param {string} entry 入口
   * @param {string} output 出口  
   * @param {string} publicPath 
   * @param {string} port 
   */
  return {
    entry: 'src/main.js',
    output: 'dist',
    publicPath: '/common/',
    port: 8888,
    dll: {
      venders: ['vue', 'react']
    },
    pages: {
      index: {
        entry: 'src/main.js',
        template: 'public/index.html',
        filename: 'index.html',
      },
      index2: {
        entry: 'src/main.js',
        template: 'public/index2.html',
        filename: 'index2.html',
      }
    },
    chainWebpack(config) {
    }
  }
}

Тема 1: Изучение веб-пакета? Изучите принципы упаковки webpack

хочу учитьсяwebpack, нам сначала нужно понятьwebpackМеханизм, который мы сначала начинаем изучать с js для загрузки css.

Начнем со следующего небольшого упражненияwebpackБар

существуетindex.jsвведен вindex.css

const css = require('./index.css')
console.log(css)

Файлы CSS не распознаются js, и webpack не является исключением.Приведенный выше метод написания не сообщит об ошибке неожиданно.

Как заставить веб-пакет распознавать css?Ответ заключается в том, что веб-пакет предоставляет нам механизм загрузчика, который позволяет нам преобразовать любой файл в файл, который веб-пакет может распознать через загрузчик.

В этой главе в основном объясняется

  1. базовая конфигурация вебпака
  2. Решите, как пакеты загружают модули
  3. Принцип динамической загрузки импорта
  4. Переписать конфигурацию с помощью webpack-chain
  5. Краткое содержание урока 1

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

Требуемые зависимости

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack", // 开发环境
    "build": "cross-env NODE_ENV=production webpack" // 生产环境
  },
  "dependencies": {
    "cross-env": "^6.0.3", // 兼容各种环境
    "css-loader": "^3.2.0",
    "rimraf": "^3.0.0", // 删除文件
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.10"
  }
}

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

webpack.config.js

const path = require('path');
const rimraf = require('rimraf');

// 删除 dist 目录
rimraf.sync('dist');

// webpack 配置
module.exports = {
  entry: './src/index',
  mode: process.env.NODE_ENV,
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

css импортируется в js

src/index.js

const css = require('css-loader!./index.css');
const a = 100;
console.log(a, css);

тест css

src/index.css

body {
  width: 100%;
  height: 100vh;
  background-color: orange;
}

Решите, как пакеты загружают модули

Я удалил некоторые комментарии и некоторые отвлекающие факторы, чтобы все выглядело немного понятнее.

  • bundleэто немедленно выполняемая функция, думайте о ней как об одном гигантском модуле, который связывает все модули вместе.
  • webpackВсе модули упакованы вbundleзависимости, введенные через объект
  • 0 模块это вход
  • webpackпройти через__webpack_require__модуль импорта
  • __webpack_require__это то, что мы используемrequire,стеганое одеялоwebpackинкапсулированный

dist/bundle.js

(function(modules) {
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });

    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );

    module.l = true;

    return module.exports;
  }
  return __webpack_require__((__webpack_require__.s = 0));
})({
  './src/index.js': function(module, exports, __webpack_require__) {
    eval(`
      const css = __webpack_require__("./src/style/index.css")
      const a = 100;
      console.log(a, css)
    `);
  },

  './src/style/index.css': function(module, exports, __webpack_require__) {
    eval(`
      exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false);
      exports.push([module.i, "body {
        width: 100%;
        height: 100vh;
        background-color: orange;
      }", ""]);
    `);
  },

  0: function(module, exports, __webpack_require__) {
    module.exports = __webpack_require__('./src/index.js');
  }
});

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

Что произойдет, если мы изменим требование index.js на импорт?

мы знаемimportиrequireРазница в том,importЭто динамическая загрузка, которая будет загружаться только тогда, когда она используется, иrequireОн будет загружен, как только он будет объявлен,webpackВстретилисьrequireон будет загружен как модуль вbundleв зависимости

Итак, вопрос в том, если мы используем импорт для ссылки на модуль, как он загружается?

требуется изменить на import()

src/index.js

// const css = require('css-loader!./index.css');
const css = import('css-loader!./index.css');
const a = 100;
console.log(a, css);

Динамически загружать результаты упаковки

кроме нормальногоbundleКроме того, мы также можем видеть0.boundle.js

0.boundle.jsнаша динамическая загрузкаindex.cssмодуль

|-- bundle.js
|-- 0.boundle.js

динамический модуль

0.boundle.js

Этот файл - то, что мыimportмодуль в отдельныйjsв файле

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0],
  {
    './node_modules/css-loader/dist/runtime/api.js': function(
      module,
      exports,
      __webpack_require__
    ) {
      'use strict';
      eval(`
        ...
      `);
    },

    './src/style/index.css': function(module, exports, __webpack_require__) {
      eval(`
        exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false));
        exports.push([module.i, \`body {
          width: 100%;
          height: 100vh;
          background-color: orange;
        },"\`]
      `);
    }
  }
]);

Логика загрузки динамического модуля

Давайте еще раз взглянем на dist/bundle.js.

Для простоты понимания я удалил большую часть кода и комментариев.

Принцип очень простой, то есть использовать принцип реализации jsonp для загрузки модуля, но здесь нужно получать данные не с сервера, а из других модулей.

  1. При вызове модуля будетwindowзарегистрироваться на одномwebpackJsonpмассив, окно['webpackJsonp'] = окно['webpackJsonp'] || []
  2. когда мыimportчас,webpackпозвоню__webpack_require__.e(0)метод, то естьrequireEnsure
  3. webpackбудет динамически создаватьscriptтег для загрузки этого модуля, после успешной загрузки модуль будет внедрен вwebpackJsonpсередина
  4. webpackJsonp.pushпозвонюwebpackJsonpCallbackполучить модуль
  5. Модуль загружается (затем) и затем используется__webpack_require__получить модуль
(function(modules) {
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    var moduleId,
      chunkId,
      i = 0,
      resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (
        Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
        installedChunks[chunkId]
      ) {
        resolves.push(installedChunks[chunkId][0]);
      }
      // 模块安装完
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);
    while (resolves.length) {
      // 执行所有 promise 的 resolve 函数
      resolves.shift()();
    }
  }

  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.bundle.js';
  }

  function __webpack_require__(moduleId) {
    // ...
  }

  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    // ...
    var script = document.createElement('script');
    var onScriptComplete;
    script.charset = 'utf-8';
    script.timeout = 120;
    script.src = jsonpScriptSrc(chunkId);

    onScriptComplete = function(event) {
      // 处理异常,消除副作用
      // ...
    };
    var timeout = setTimeout(function() {
      onScriptComplete({ type: 'timeout', target: script });
    }, 120000);
    script.onerror = script.onload = onScriptComplete;
    document.head.appendChild(script);
    // ...
    // 动态加载模块
    return Promise.all(promises);
  };

  var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []);
  // 重写数组 push 方法
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for (var i = 0; i < jsonpArray.length; i++)
    webpackJsonpCallback(jsonpArray[i]);

  return __webpack_require__((__webpack_require__.s = 0));
})({
  './src/index.js': function(module, exports, __webpack_require__) {
    eval(`
        const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null, "./src/style/index.css", 7))
        const a = 100;
        console.log(a, css)
      `);
  },
  0: function(module, exports, __webpack_require__) {
    eval(`module.exports = __webpack_require__("./src/index.js");`);
  }
});

Переписать конфигурацию с помощью webpack-chain

Мы используем webpack-chain для написания конфигурации webpack, причина в том, что способ webpack-chain более гибкий

официальное объяснение

webpack-chainпопробуйте, предоставив цепочку или нисходящий потокAPIСоздание и изменениеwebpackконфигурация.APIизKeyНа разделы можно ссылаться по заданным пользователем именам, что помогает стандартизировать способы изменения конфигураций в проектах.

const path = require('path');
const rimraf = require('rimraf');
const Config = require('webpack-chain');
const config = new Config();
const resolve = src => {
  return path.join(process.cwd(), src);
};

// 删除 dist 目录
rimraf.sync('dist');

config
  // 入口
  .entry('src/index')
  .add(resolve('src/index.js'))
  .end()
  // 模式
  // .mode(process.env.NODE_ENV) 等价下面
  .set('mode', process.env.NODE_ENV)
  // 出口
  .output.path(resolve('dist'))
  .filename('[name].bundle.js');

config.module
  .rule('css')
  .test(/\.css$/)
  .use('css')
  .loader('css-loader');

module.exports = config.toConfig();

Краткое содержание урока 1

Пока закончился урок 1, мы в основном делали следующие вещи

  1. базовая конфигурация вебпака
  2. Упаковать css в js через css-loader
  3. Анализ того, как пакеты загружают модули
  4. Как webpack реализует динамическую загрузку модулей

Чтобы изучить инструмент, мы должны не только понять его конфигурацию, но и вместе понять его принципы.Только изучив суть фреймворка, мы можем справиться с бурным развитием большого фронтенда сегодня.


Тема 2: Настройка среды разработки и производственной среды

Краткое содержание этой главы:

содержание

│── build
│   │── base.js                 // 公共部分
│   │── build.js
│   └── dev.js
│── config
│   │── base.js                 // 基础配置
│   │── css.js                  // css 配置
│   │── HtmlWebpackPlugin.js    // html 配置
│   └── MiniCssExtractPlugin.js // 提取css
│── public                      // 公共资源
│   └── index.html              // html 模版
└── src                         // 开发目录
    │── style
    │ └── index.css
    └── main.js                // 主入口

Внедрение подключаемых конфигураций

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node build/dev.js",
    "build": "cross-env NODE_ENV=production node build/build.js"
  },
  "dependencies": {
    "cross-env": "^6.0.3",
    "css-loader": "^3.2.0",
    "cssnano": "^4.1.10",
    "ora": "^4.0.3",
    "rimraf": "^3.0.0",
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "extract-text-webpack-plugin": "^3.0.2",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "vue-cli-plugin-commitlint": "^1.0.4",
    "webpack-chain": "^6.0.0",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}

build/base.js

const { findSync } = require('../lib');
const Config = require('webpack-chain');
const config = new Config();
const files = findSync('config');
const path = require('path');
const resolve = p => {
  return path.join(process.cwd(), p);
};

module.exports = () => {
  const map = new Map();

  files.map(_ => {
    const name = _.split('/')
      .pop()
      .replace('.js', '');
    return map.set(name, require(_)(config, resolve));
  });

  map.forEach(v => v());

  return config;
};

Создание производственной среды

build/build.js

const rimraf = require('rimraf');
const ora = require('ora');
const chalk = require('chalk');
const path = require('path');
// 删除 dist 目录
rimraf.sync(path.join(process.cwd(), 'dist'));

const config = require('./base')();
const webpack = require('webpack');
const spinner = ora('开始构建项目...');
spinner.start();

webpack(config.toConfig(), function(err, stats) {
  spinner.stop();
  if (err) throw err;
  process.stdout.write(
    stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    }) + '\n\n'
  );

  if (stats.hasErrors()) {
    console.log(chalk.red('构建失败\n'));
    process.exit(1);
  }

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

Сборка среды разработки (devServer)

build/dev.js

const config = require('./base')();
const webpack = require('webpack');
const chalk = require('chalk');
const WebpackDevServer = require('webpack-dev-server');
const port = 8080;
const publicPath = '/common/';

config.devServer
  .quiet(true)
  .hot(true)
  .https(false)
  .disableHostCheck(true)
  .publicPath(publicPath)
  .clientLogLevel('none');

const compiler = webpack(config.toConfig());
// 拿到 devServer 参数
const chainDevServer = compiler.options.devServer;
const server = new WebpackDevServer(
  compiler,
  Object.assign(chainDevServer, {})
);

['SIGINT', 'SIGTERM'].forEach(signal => {
  process.on(signal, () => {
    server.close(() => {
      process.exit(0);
    });
  });
});
// 监听端口
server.listen(port);

new Promise(() => {
  compiler.hooks.done.tap('dev', stats => {
    const empty = '    ';
    const common = `App running at:
    - Local: http://127.0.0.1:${port}${publicPath}\n`;
    console.log(chalk.cyan('\n' + empty + common));
  });
});

извлечь css

config/css.js

css извлечь конфигурацию загрузчика

module.exports = (config, resolve) => {
  return (lang, test) => {
    const baseRule = config.module.rule(lang).test(test);
    const normalRule = baseRule.oneOf('normal');
    applyLoaders(normalRule);
    function applyLoaders(rule) {
      rule
        .use('extract-css-loader')
        .loader(require('mini-css-extract-plugin').loader)
        .options({
          publicPath: './'
        });
      rule
        .use('css-loader')
        .loader('css-loader')
        .options({});
    }
  };
};

Плагин извлечения css MiniCssExtractPlugin

config/MiniCssExtractPlugin.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (config, resolve) => {
  return () => {
    config
      .oneOf('normal')
      .plugin('mini-css-extract')
      .use(MiniCssExtractPlugin);
  };
};

Автоматически генерировать html

config/HtmlWebpackPlugin.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('html').use(HtmlWebpackPlugin, [
      {
        template: 'public/index.html'
      }
    ]);
  };
};

испытание проекта

тестовый HTML-шаблон

public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>learn_webpack</title>
  <body></body>
</html>

тестовый css-шаблон

src/style/index.css

.test {
  width: 200px;
  height: 200px;
  color: red;
  background-color: orange;
}

Запись программы

src/main.js

require('./style/index.css');

const h2 = document.createElement('h2');
h2.className = 'test';
h2.innerText = 'test';
document.body.append(h2);

Тема 3: Загрузчик базовой конфигурации

Краткое содержание этой главы:

содержание

Добавьте следующие файлы

│──── config                // 配置目录
│   │── babelLoader.js      // babel-loader 配置
│   │── ForkTsChecker.js    // ts 静态检查
│   │── FriendlyErrorsWebpackPlugin.js // 友好错误提示
│   └── style
│──── src                   // 开发目录
│   │── style
│   │  │── app.css
│   │  │── index.less       // 测试 less
│   │  │── index.scss       // 测试 sass
│   │  └── index.postcss    // 测试 postcss
│   └── ts
│     └── index.ts          // 测试 ts
│── babel.js
│── postcss.config.js       // postcss 配置
│── tsconfig.json           // ts 配置
└──── dist                  // 打包后的目录
   │── app.bundle.js
   │── app.css
   └── index.html

настроить бабел

config/babelLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js│.tsx?$/);
  const babelPath = resolve('babel.js');
  const babelConf = require(babelPath);
  const version = require(resolve('node_modules/@babel/core/package.json'))
    .version;
  return () => {
    baseRule
      .use('babel')
      .loader(require.resolve('babel-loader'))
      .options(babelConf({ version }));
  };
};

Настройте ts с помощью babel

Здесь мы используемbabelплагин@babel/preset-typescriptбудетtsПревратиться вjs,并使用 ForkTsCheckerWebpackPlugin,ForkTsCheckerNotifierWebpackPluginПлагин отображает сообщение об ошибке.

babel.js

module.exports = function(api) {
  return {
    presets: [
      [
        '@babel/preset-env',
        {
          targets: {
            chrome: 59,
            edge: 13,
            firefox: 50,
            safari: 8
          }
        }
      ],
      [
        '@babel/preset-typescript',
        {
          allExtensions: true
        }
      ]
    ],
    plugins: [
      '@babel/plugin-transform-typescript',
      'transform-class-properties',
      '@babel/proposal-object-rest-spread'
    ]
  };
};

проверка статического типа ts

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('ts-fork').use(ForkTsCheckerWebpackPlugin, [
      {
        // 将async设为false,可以阻止Webpack的emit以等待类型检查器/linter,并向Webpack的编译添加错误。
        async: false
      }
    ]);
    // 将TypeScript类型检查错误以弹框提示
    // 如果fork-ts-checker-webpack-plugin的async为false时可以不用
    // 否则建议使用,以方便发现错误
    config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin, [
      {
        title: 'TypeScript',
        excludeWarnings: true,
        skipSuccessful: true
      }
    ]);
  };
};

Дружественный плагин сообщений об ошибках

config/FriendlyErrorsWebpackPlugin.js

const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('error').use(FriendlyErrorsWebpackPlugin);
  };
};

Настройте стиль, стиль, css, less, sass, postcss и т. д.

module.exports = (config, resolve) => {
  const createCSSRule = (lang, test, loader, options = {}) => {
    const baseRule = config.module.rule(lang).test(test);
    const normalRule = baseRule.oneOf('normal');
    normalRule
      .use('extract-css-loader')
      .loader(require('mini-css-extract-plugin').loader)
      .options({
        hmr: process.env.NODE_ENV === 'development',
        publicPath: '/'
      });
    normalRule
      .use('css-loader')
      .loader(require.resolve('css-loader'))
      .options({});
    normalRule.use('postcss-loader').loader(require.resolve('postcss-loader'));
    if (loader) {
      const rs = require.resolve(loader);
      normalRule
        .use(loader)
        .loader(rs)
        .options(options);
    }
  };

  return () => {
    createCSSRule('css', /\.css$/, 'css-loader', {});
    createCSSRule('less', /\.less$/, 'less-loader', {});
    createCSSRule('scss', /\.scss$/, 'sass-loader', {});
    createCSSRule('postcss', /\.p(ost)?css$/);
  };
};

конфигурация postcss

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px',
      viewportWidth: 750,
      unitPrecision: 5,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 1,
      mediaQuery: false,
      replace: true,
      exclude: [],
      landscape: false,
      landscapeUnit: 'vw',
      landscapeWidth: 568
    }
  }
};

Сравнение css до и после компиляции

src/style/index.less

/* index.less */
.test {
  width: 300px;
}

dist/app.css

/* index.css */
.test {
  width: 36.66667vw;
  height: 26.66667vw;
  color: red;
  background-color: orange;
}
/* app.css */
.test {
  font-size: 8vw;
}
/* index.less */
.test {
  width: 40vw;
}

/* index.scss */
.test {
  height: 40vw;
}
/* index.postcss */
.test {
  background: green;
  height: 26.66667vw;
}

настроить автопрефиксер

Автоматически добавлять префикс css

postcss.config.js

module.exports = {
  plugins: {
    autoprefixer: {
      overrideBrowserslist: [
        '> 1%',
        'last 3 versions',
        'iOS >= 8',
        'Android >= 4',
        'Chrome >= 40'
      ]
    }
  }
};

До конвертации

/* index.css */
.test {
  width: 200px;
  height: 200px;
  color: red;
  display: flex;
  background-color: orange;
}

после преобразования

/* index.css */
.test {
  width: 26.66667vw;
  height: 26.66667vw;
  color: red;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  background-color: orange;
}

карта с открытым исходным кодом

config.devtool('cheap-source-map');
└── dist
  │── app.bundle.js
  │── app.bundle.js.map
  │── app.css
  │── app.css.map
  └── index.html

Под исходным файлом будет строка комментариев, доказывающая, что исходная карта включена.

/*# sourceMappingURL=app.css.map*/

Урок 4: оптимизация производительности веб-пакета

В этой главе объясняется

  1. Отдельный манифест
  2. Разделение кода
  3. Разделение пакетов
  4. Tree Shaking (удалить мертвый код)
  5. включить gzip

Отдельный манифест

module.exports = (config, resolve) => {
  return () => {
    config
      .optimization
      .runtimeChunk({
        name: "manifest"
      })
  }
}

Code Splitting

  1. Используйте динамический импорт или синтаксис require.ensure, описанный в первом разделе.
  2. использоватьbabel-plugin-importПлагины вводят некоторые библиотеки компонентов по мере необходимости.

Bundle Splitting

Извлеките общедоступные пакеты вchunk-vendorsВнутри, например, если вы требуете('vue'), webpack упакует vue в chunk-vendors.bundle.js

module.exports = (config, resolve) => {
  return () => {
    config
      .optimization.splitChunks({
        chunks: 'async',
        minSize: 30000,
        minChunks: 1,
        maxAsyncRequests: 3,
        maxInitialRequests: 3,
        cacheGroups: {
          vendors: {
            name: `chunk-vendors`,
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          },
          common: {
            name: `chunk-common`,
            minChunks: 2,
            priority: -20,
            chunks: 'initial',
            reuseExistingChunk: true
          }
        }
      })
    config.optimization.usedExports(true)
  }
}

Tree Shaking

config/optimization.js

config.optimization.usedExports(true);

src/treeShaking.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

Только куб упоминается в main.js

import { cube } from './treeShaking';

console.log(cube(2));

Не использовать встряхивание дерева

{
  "./src/treeShaking.js": function(
    module,
    __webpack_exports__,
    __webpack_require__
  ) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, "square", function() {
      return square;
    });
    __webpack_require__.d(__webpack_exports__, "cube", function() {
      return cube;
    });
    function square(x) {
      return x * x;
    }
    function cube(x) {
      return x * x * x;
    }
  }
}

Подержанное встряхивание дерева

Здесь экспортируется только функция куба, а квадрат не экспортируется

Конечно, вы можете видеть, что квадратная функция все еще находится в комплекте, но она будет убита во время сжатия, потому что на нее нет ссылки.

{
  "./src/treeShaking.js": function(
    module,
    __webpack_exports__,
    __webpack_require__
  ) {
    "use strict";
    __webpack_require__.d(__webpack_exports__, "a", function() {
      return cube;
    });
    function square(x) {
      return x * x;
    }
    function cube(x) {
      return x * x * x;
    }
  }
}

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

Как использовать встряхивание деревьев?

  1. Убедитесь, что код в формате es6, т.е. экспорт, импорт
  2. В package.json установите sideEffects
  3. Убедитесь, что функции встряхивания дерева не имеют побочных эффектов
  4. Установить пресеты в babelrc [["@babel/preset-env", { "modules": false }]], чтобы запретить преобразование модулей и передать их в webpack для модульной обработки
  5. В сочетании с uglifyjs-webpack-plugin

по фактуwebpack4Нам вообще не нужно делать эти операции, т.к.webpackВ продакшене он у нас добавлен по умолчанию, из коробки!

включить gzip

CompressionWebpackPlugin.js

const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
      {
        algorithm: 'gzip',
        test: /\.js(\?.*)?$/i,
        threshold: 10240,
        minRatio: 0.8
      }
    ]);
  };
};

Урок 5: Рукописный загрузчик реализует необязательную цепочку

Содержание этой главы

  1. что такое загрузчик webpack
  2. Дополнительное введение цепи
  3. загрузчик реализует необязательную цепочку

что такое загрузчик webpack

webpack loaderдаwebpackПромежуточный уровень для обработки различных типов файлов,webpackПо существуnodeмодуль, который не может справитьсяjsкроме файла, тоloaderпросто помогитеwebpackУровень преобразования выполняется для преобразования всех файлов в строки, вы можете выполнять произвольные операции/модификации строк, а затем возвращаться кwebpackобъект, содержащий эту строку, пустьwebpackВыполните последующую обработку. если поставитьwebpackКак мусорный завод, тоloaderЭто классификация мусора этой фабрики!

Дополнительное введение цепи

Это не необязательная цепочка в чистом смысле, потому чтоbabelиtsОн уже поддерживается, и нам не нужно писать полную необязательную цепочку, просто чтобы углубитьloaderпонимание,loaderЧем вы можете помочь нам в работе?

用途Когда мы обращаемся к свойству объекта, нам не нужно беспокоиться о том, что объектundefinedИ сообщается об ошибке, в результате чего программа не продолжает выполняться вниз

解释существует?Все предыдущие ссылки доступа являются законными и не будут генерировать ошибки

const obj = {
  foo: {
    bar: {
      baz: 2
    }
  }
}

console.log(obj.foo.bar?.baz) // 2
// 被转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz
console.log(obj.foo.err?.baz) // undefined
// 被转成 obj && obj.foo && obj.foo.err && obj.foo.err.baz

загрузчик реализует необязательную цепочку

Загрузчик конфигурации, options-chain-loader

config/OptionsChainLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  const normalRule = baseRule.oneOf('normal');
  return () => {
    normalRule
      .use('options-chain')
      .loader(resolve('options-chain-loader'))
  }
}

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

Следующая операция означает, что мы сопоставляемobj.foo.bar?.и превратить его вobj && obj.foo && obj.foo.bar && obj.foo.bar.

options-chain-loader.js

module.exports = function(content) {
  return content.replace(new RegExp(/([\$_\w\.]+\?\.)/,'g'),function(res) {
    let str  = res.replace(/\?\./,'');
    let arrs = str.split('.');
    let strArr = [];
    for(let i = 1; i <= arrs.length; i++) {
      strArr.push(arrs.slice(0,i).join('.')); 
    }
    let compile = strArr.join('&&');
    const done = compile + '&&' + str + '.'
    return  done;
  });
};

Урок 6: оптимизация компиляции веб-пакета

Содержание этой главы

  1. cache-loader
  2. DllPlugin
  3. threadLoader

cache-loader

cache-loaderВ основном это кеширование упакованных файлов в каталоге на жестком диске, который обычно существует.node_modules/.cacheвниз, когда ты сноваbuildКогда файл не изменен, скомпилированный файл будет прочитан из кеша, и будут скомпилированы только измененные файлы, что значительно сократит время компиляции. Особенно когда проект большой.

Сравнение данных до и после использования этого проекта 3342 мс --> 2432 мс Эффект все еще относительно очевиден.

Здесь в babel добавлен только cache-loader, т.к. наши ts/js компилируются babel, и ts-loader кешировать не нужно (мы его тоже не использовали)

config/cacheLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  const babelPath = resolve('babel.js')
  const babelConf = require(babelPath);
  const version = require(resolve('node_modules/@babel/core/package.json')).version
  return () => {
    baseRule
      .exclude
      .add(filepath => {
        // 不缓存 node_modules 下的文件
        return /node_modules/.test(filepath)
      })
      .end()
      .use('cache-loader')
      .loader('cache-loader')
      .options({
        // 缓存位置
        cacheDirectory: resolve('node_modules/.cache/babel')
      })
  }
}

DllPlugin

DllPlugin должен изолировать долгосрочный неизменный пакет третьей стороны от фактического проекта и упаковать его отдельно.Когда мы собираем, можно импортировать упакованный пакет dll.

Я извлек два пакета vue и react, и скорость увеличилась почти на 200 мс, с 2698 мс до 2377 мс.

пакет DLL

build/dll.js

const path = require("path");
const dllPath = path.join(process.cwd(), 'dll');
const Config = require('webpack-chain');
const config = new Config();
const webpack = require('webpack')
const rimraf = require('rimraf');
const ora = require('ora')
const chalk = require('chalk')
const BundleAnalyzerPlugin = require('../config/BundleAnalyzerPlugin')(config)

BundleAnalyzerPlugin()
config
  .entry('dll')
  .add('vue')
  .add('react')
  .end()
  .set('mode', "production")
  .output
  .path(dllPath)
  .filename('[name].js')
  .library("[name]")
  .end()
  .plugin('DllPlugin')
  .use(webpack.DllPlugin, [{
    name: "[name]",
    path: path.join(process.cwd(), 'dll', 'manifest.json'),
  }])
  .end()

rimraf.sync(path.join(process.cwd(), 'dll'))
const spinner = ora('开始构建项目...')
spinner.start()

webpack(config.toConfig(), function (err, stats) {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n\n')

  if (stats.hasErrors()) {
    console.log(chalk.red('构建失败\n'))
    process.exit(1)
  }
  console.log(chalk.cyan('build完成\n'))
})

Объединить пакеты dll

const webpack = require('webpack')

module.exports = (config, resolve) => {
  return () => {
    config.plugin('DllPlugin')
      .use(webpack.DllReferencePlugin, [{
        context: process.cwd(),
        manifest: require(resolve('dll/manifest.json'))
      }])
  }
}

threadLoader

Эффект теста хуже 😅, чем меньше потоков, тем быстрее скорость компиляции

config/threadLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  return () => {
    const useThreads = true;
    if (useThreads) {
      const threadLoaderConfig = baseRule
        .use('thread-loader')
        .loader('thread-loader');
      threadLoaderConfig.options({ workers: 3 })
    }
  }
}

Урок 7: Многостраничная конфигурация

Уведомление

  • 弃用 npm run build & npm run dev & npm run dll
  • 改成 box build & box dev & box dll
  • linkСсылка npm глобально связывает команду box

Содержание этой главы

  1. использовать
  2. Превращен в строительные леса
  3. Многостраничная конфигурация

использовать

box build # 不加参数则会编译所有页面,并清空 dist
box dev   # 默认编译 index 页面

参数

# index2 是指定编译的页面。不会清空 dist
# report 开启打包分析
box build index2 --report 
box dev index2 --report 

Превращен в строительные леса

Разделен на три команды для разных операций

  • build
  • dev
  • dll

bin/box.js

#!/usr/bin/env node

const chalk = require('chalk')
const program = require('commander')
const packageConfig = require('../package.json');
const { cleanArgs } = require('../lib')
const path = require('path')
const __name__ = `build,dev,dll`

let boxConf = {}
let lock = false

try {
  boxConf = require(path.join(process.cwd(), 'box.config.js'))()
} catch (error) { }

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('build [app-page]')
  .description(`构建开发环境`)
  .option('-r, --report', '打包分析报告')
  .option('-d, --dll', '合并差分包')
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    if (boxConf.pages) {
      Object.keys(boxConf.pages).forEach(page => {
        args.name = page;
        require('../build/build')(args)
      })
    } else {
      require('../build/build')(args)
    }
  })

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('dev [app-page]')
  .description(`构建生产环境`)
  .option('-d, --dll', '合并差分包')
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    require('../build/dev')(args)
  })

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('dll [app-page]')
  .description(`编译差分包`)
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    require('../build/dll')(args)
  })

program.parse(process.argv).args && program.parse(process.argv).args[0];
program.commands.forEach(c => c.on('--help', () => console.log()))

if (process.argv[2] && !__name__.includes(process.argv[2])) {
  console.log()
  console.log(chalk.red(`  没有找到 ${process.argv[2]} 命令`))
  console.log()
  program.help()
}

if (!process.argv[2]) {
  program.help()
}

Многостраничная конфигурация

box.config.js

module.exports = function (config) {
  return {
    entry: 'src/main.js', // 默认入口
    dist: 'dist', // 默认打包目录
    publicPath: '/',
    port: 8888,
    pages: {
      index: {
        entry: 'src/main.js',
        template: 'public/index.html',
        filename: 'index.html',
      },
      index2: {
        entry: 'src/main.js',
        template: 'public/index2.html',
        filename: 'index2.html',
      }
    },
    chainWebpack(config) {
    }
  }
}

Урок 8: Написание плагина веб-пакета от руки

Если вебпак рассматривать как фабрику мусора, то загрузчик — это классификация мусора, и весь мусор сортируется и передается в вебпак. плагин как бороться с этим мусором.

Плагин webpack очень прост в написании, то есть нужно знать, когда срабатывают те или иные хуки, а потом писать свою логику в хуках и все ок

  • applyФункция выполняется, когда веб-пакет вызывает плагин, вы можете думать об этом как о точке входа.
  • compilerВыявляет хуки, связанные со всем жизненным циклом веб-пакета.
  • CompilationПредоставляет более детализированные перехватчики событий, связанные с модулями и зависимостями.

Резюме этого раздела

Реализовать CopyPlugin

Сегодня мы напишем плагин для копирования, после завершения сборки веб-пакета скопируем файлы из целевого каталога в другой каталог.

const fs = require('fs-extra')
const globby = require('globby')

class CopyDirWebpackPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    const opt = this.options
    compiler.plugin('done', (stats) => {
      if (process.env.NODE_ENV === 'production') {
        (async ()=>{
          const toFilesPath = await globby([`${opt.to}/**`, '!.git/**'])
          toFilesPath.forEach(filePath => fs.removeSync(filePath))
          const fromFilesPath = await globby([`${opt.from}/**`])
          fromFilesPath.forEach(fromPath => {
            const cachePath = fromPath
            fromPath = fromPath.replace('dist', opt.to)
            const dirpaths = fromPath.substring(0, fromPath.lastIndexOf('/'))
            fs.mkdirpSync(dirpaths)
            fs.copySync(cachePath, fromPath)
          })
          console.log(`  完成copy ${opt.from} to ${opt.to}`)
        })()
      }
    });
  }
}

module.exports = CopyDirWebpackPlugin

использовать

Скопируйте содержимое упакованного каталога dist в каталог dist2.

const CopyPlugin = require('../webapck-plugin-copy');

module.exports = ({ config }) => {
  return () => {
    config.plugin('copy-dist')
      .use(CopyPlugin, [{
        from: 'dist',
        to: 'dist2'
      }])
  }
}

Урок 9: Создание ssr

ssr — это рендеринг на стороне сервера.Преимущество выполнения ssr заключается в том, чтобы справиться с недостатками spa, такими как SEO-оптимизация, кеширование на стороне сервера и другие проблемы.

Сегодня я в основном использую ssr реакции, чтобы сделать простой пример, чтобы каждый мог более четко начать работу.

Краткое содержание этой главы

Создать сборку коробки: ssr

Старые правила, приходи первымbox build:ssrкоманда, чтобы сделать программу исполняемой

воплощать в жизньbox build:ssrпозвонюbuild/ssrвыполнить компиляцию

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('build:ssr [app-page]')
  .description(`服务端渲染`)
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd);
    const args = Object.assign(options, { name }, boxConf);
    if (lock) return;
    lock = true;
    require('../build/ssr')(args);
  });

скомпилировать сср

Он ничем не отличается от других компиляций, им стоит жить

  • цель указана как режим umd
  • глобальный объект это
  • Запись изменена на ssr.jsx
.libraryTarget('umd')
.globalObject('this')

build/ssr.js

module.exports = function(options) {
  const path = require('path');
  const Config = require('webpack-chain');
  const config = new Config();
  const webpack = require('webpack');
  const rimraf = require('rimraf');
  const ora = require('ora');
  const chalk = require('chalk');
  const PATHS = {
    build: path.join(process.cwd(), 'static'),
    ssrDemo: path.join(process.cwd(), 'src', 'ssr.jsx')
  };

  require('../config/babelLoader')({ config, tsx: true })();
  require('../config/HtmlWebpackPlugin')({
    config,
    options: {
      publicPath: '/',
      filename: 'client.ssr.html'
    }
  })();

  config
    .entry('ssr')
    .add(PATHS.ssrDemo)
    .end()
    .set('mode', 'development') //  production
    .output.path(PATHS.build)
    .filename('[name].js')
    .libraryTarget('umd')
    .globalObject('this')
    .library('[name]')
    .end();

  rimraf.sync(path.join(process.cwd(), PATHS.build));
  const spinner = ora('开始构建项目...');
  spinner.start();

  webpack(config.toConfig(), function(err, stats) {
    spinner.stop();
    if (err) throw err;
    process.stdout.write(
      stats.toString({
        colors: true,
        modules: false,
        children: false,
        chunks: false,
        chunkModules: false
      }) + '\n\n'
    );

    if (stats.hasErrors()) {
      console.log(chalk.red('构建失败\n'));
      process.exit(1);
    }
    console.log(chalk.cyan('build完成\n'));
  });
};

скомпилировать синтаксис jsx

Поскольку мы пишем в React, мы не можем избежать использования синтаксиса jsx, поэтому нам нужно использоватьbabel-loaderиспользуется в@babel/preset-react

npm i @babel/preset-react -D

config/babelLoader.js

if (tsx) {
  babelConf.presets.push('@babel/preset-react');
}

Запись различает сервер/клиент

Различие между серверным и клиентским рендерингом

const React = require("react");
const ReactDOM = require("react-dom");

const SSR = <div onClick={() => alert("hello")}>Hello world</div>;

if (typeof document === "undefined") {
  console.log('在服务端渲染')
  module.exports = SSR;
} else {
  console.log('在客户端渲染')
  const renderMethod = !module.hot ? ReactDOM.render : ReactDOM.hydrate;
  renderMethod(SSR, document.getElementById("app"));
}

рендеринг на стороне сервера

module.exports = function (options) {
  const express = require("express");
  const { renderToString } = require("react-dom/server");
  const chalk = require('chalk')
  
  const SSR = require("../static/ssr");
  const port = process.env.PORT || 8080;

  server(port);
  
  function server(port) {
    const app = express();
    app.use(express.static("static"));
    app.get("/", (req, res) =>
      res.status(200).send(renderMarkup(renderToString(SSR)))
    );

    const empty = '    '
    const common = `App running at:
      - Local: http://127.0.0.1:${port}\n`
      console.log(chalk.cyan('\n' + empty + common))
    
    app.listen(port, () => process.send && process.send("online"));
  }
  
  function renderMarkup(html) {
    return `<!DOCTYPE html>
  <html>
    <head>
      <title>Webpack SSR Demo</title>
      <meta charset="utf-8" />
    </head>
    <body>
      <div id="app">${html}</div>
      <script src="./ssr.js"></script>
    </body>
  </html>`;
  }
}

резюме

Пока ssr закончился.На самом деле все технологии,которые кажутся очень высокими,аккумулируются по крупицам.Пока мы понимаем принцип,тоже можно сделать более качественный фреймворк.


конец

Возможно, это писалось больше двух недель. Я писал понемногу каждый день, и мое самочувствие значительно улучшилось. Если студенты заинтересованы в обучении со мной, они могут прийти и присоединиться ко мне в группе. Я буду организовывать различные группы каждый день по предмету изучения.

Следующими темами, скорее всего, будут:

  • Рукописный исходный код vue-next
  • ts от входа до оставления
  • Узел вход, чтобы плакать

Хаха, просто шучу, вот об этом, есть около одной темы за полда месяца, если у вас хорошая тема, вы можете обсудить это вместе