React-Webpack5-TypeScript для создания спроектированного многостраничного приложения.

React.js Webpack

Многостраничная упаковка приложения

В большинстве сценариев нашей повседневной работы мы используемwebpackСоздание традиционной одиночной страницыspaзаявление.

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

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

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

Конфигурацию шаблона, используемую в статье, можно посмотреть, нажав здесь.Нажмите здесь👇.

не забудьте датьstarАх, большие ребята (Молитесь за лицо.jpg)

Передняя часть основана на базовой конфигурации для сборки с нуля.React+TypeScript+WebpackОбъясняющая часть, если вы уже достаточно знаете об этом, вы можете сразу перейти кВ многостраничное приложениеПерейдите к настройке динамического многостраничного раздела.

Конечный результат, которого мы достигли, выглядит следующим образом:

1632404783258.gif

Прости меня, я действительно неgif...

Инициализировать структуру каталогов

Давайте сначала инициализируем самый простой каталог проекта:

GitHub.com/19Qingfeng/…

image.png

Сначала установимwebpackтак же какReact:

yarn add -D webpack webpack-cli

yarn add react react-dom

webpack-cliдаwebpackинструмент командной строки для использования в командной строкеwebpack.

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

Одинeditorстраница редактора, аhomeдомашняя страница.

После завершения установки позвольте мне изменить файл каталога изменений:

image.png

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

  • containersПапки хранят бизнес-логику в разных проектах
  • packagesПапки хранят входные файлы в разных проектах

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

настроитьreactслужба поддержки

Далее пусть наш проект сначала поддерживает самый примитивныйjsxфайл, пусть проект поддерживаетreactизjsx.

служба поддержкиjsxТребует дополнительной настройкиbabelиметь дело сjsxфайл, будетjsxПеревод, чтобы браузер мог распознатьjs.

Здесь нам нужно использовать следующие библиотеки:

  • babel-loader
  • @babel/core
  • @babel/preset-env
  • @babel/plugin-transform-runtime
  • @babel/preset-react

Давайте немного разберемся с этимbabelроль, в частностиbabelЯ не буду вдаваться в подробности здесь.

babel-loader

Первый для нашего проектаjsxфайл нам нужно передать "переводчик" в проектjsxфайл вjsдокумент,babel-loaderЗдесь действует именно этот переводчик.

@babel/core

ноbabel-loaderтолько идентифицированыjsxфайл, необходимый для внутренней основной функции перевода@babel/coreЭта основная библиотека,@babel/coreМодуль отвечает за реализацию внутренней трансляции ядра.

@babel/preset-env

@babel/prest-envдаbabelНекоторые пресеты в процессе перевода, он отвечает за преобразование некоторых основныхes 6+грамматика, напримерconst/let...Переводит в синтаксис совместимости низкого уровня, который могут распознать браузеры.

Здесь следует отметить, что@babel/prest-entне для некоторыхes6+Нет встроенной реализации некоторого высокоуровневого синтаксиса, такого как

PromiseЖдатьpolyfill, вы можете понимать это как преобразование на уровне синтаксиса, которое не включает модули высокого уровня (polyfill) реализация.

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime, Выше мы упомянули, что для некоторых встроенных модулей высокой версии, таких какPromise/Generateтак далее@babel/preset-envне конвертирует, так что@babel/plugin-transform-runtimeЧтобы помочь нам добиться этого эффекта, он нам пригодится, если проектPromiseи другие модули для реализации более низкой версии браузераpolyfill.

На самом деле с@babel/plugin-transform-runtimeДля достижения того же эффекта вы также можете напрямую установить и импортировать@babel/polyfill, но, напротив, этот метод не рекомендуется, у него есть недостатки, такие как загрязнение глобальной области видимости, введение слишком большого количества ссылок и повторная инъекция между модулями.

На данный момент мы уже можем реализовать эти плагиныes6+Код компилируется в браузер, чтобы распознать совместимость с низкой версией.jsКод, но мы все еще упускаем самый важный момент.

В настоящее время эти плагины имеют дело сjsфайл, мы должны позволить ей распознать его и обработатьjsxдокумент.

@babel/preset-react

На этом этапе мы представляем наш ключевой@babel/preset-reactЭтот плагин вjsxв мы используемjsxТеги в конечном итоге компилируются в:

image.png

Заинтересованные друзья могут проверить мою предыдущую статьюReactсерединаjsxПринципиальный анализ.

В конечном итоге мы надеемся.jsxфайл преобразован вjsфайл также будетjsxтеги преобразуются вReact.createElementформе, то нам нужно дополнительно использоватьbabelЕще один плагин для -@babel/preset-react.

@babel/preset-reactПредставляет собой набор пресетов.Так называемые пресеты представляют собой встроенные серииbabel pluginдля преобразованияjsxКод становится тем, что мы хотимjsкод.

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

проектbabelнастроить

Далее давайте установим эти 5 плагинов, и вwebpackнастроить в:

yarn add -D @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime @babel/preset-react 

Создать базуwebpackнастроить

После того, как мы установили инструменты компиляции выше, давайте создадим базовыйwebpack.config.jsиспользовать его для перевода нашегоjsxдокумент:

image.png

Давайте создадим его в проекте и каталогеscripts/webpack.base.jsдокумент.

оwebpackДля перевода кода так называемый перевод буквальноwebpackПо умолчанию только обработка на основеjs json Содержание.

Если мы хотимwebpackобрабатывать нашиjsxконтент, вам нужно настроитьloaderскажи это,

"Привет,webpackврезаться.jsxсуффиксные файлы используют этоloaderобрабатывать. "

Давайте напишем основноеbabel-loaderКонфигурация:

webpack.base.js

// scripts/webpack.base.js
const path = require('path');

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.jsx'),
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: 'babel-loader',
      },
    ],
  },
};

В этот момент мы рассказалиwebpack, если встретишьjsxилиjsкод, нам нужно использоватьbabel-loaderобработаноbaebelв проектеjs/jsxОбработка файлов становится кодом, распознаваемым старыми браузерами.

.babelrc

Выше мы говорили оbabel-loaderПросто мост к другим плагинам, которые действительно нуждаются в транспиляции. Давайте использовать его дальше:

babel-loaderПредусмотрено два метода настройки, один из которых находится непосредственно вwebpackпрописано в конфигурационном файлеoptions, другой официально рекомендуется строить в каталоге проекта.babelrcконфигурация файлаbabel.

Здесь мы используем вторую рекомендуемую конфигурацию:

// .babelrc
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ]
  ]

}

packages/homeСоздать файл записиindex.jsx

Далее давайтеpackages/homeСоздано в каталогеhomeдокументы для входа в бизнес

// packages/home/index.jsx
import ReactDom from 'react-dom'

const Element = <div>hello</div>


ReactDom.render(<Element />,document.getElementById('root'))

На данный момент соответствующие базовые файлы конфигурации смоделированы, все, что нам нужно сделать, это вызватьwebpackПри упаковке нашего проекта используйте то, что мы только что написалиwebpack.base.jsэтот файл конфигурации.

pacakge.jsonУвеличиватьbuildсценарий

Давайте немного изменим егоpacakge.json:

{
  "name": "pages",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config ./scripts/webpack.base.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/plugin-transform-runtime": "^7.15.0",
    "@babel/preset-env": "^7.15.6",
    "@babel/preset-react": "^7.14.5",
    "babel-loader": "^8.2.2",
    "webpack": "^5.53.0",
    "webpack-cli": "^4.8.0"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

Здесь мы удаляем инициализированныйtestскрипт, добавленbuildЗаказ.

Далее бежимnpm run build:

image.png

На этом этапе мы успешноwebpackИдентифицироватьjsxкод и поддерживает преобразование в более высокие версииjsПреобразовать в более низкую версиюjavascriptкод вверх.

Если вы сомневаетесь, остановитесь и подумайте об общем процессе. В основном:

  1. СоздайтеbabelНастроить переводjsx/jsсодержание.
  2. Создайте входной файл.
  3. webpackсреда дляjsx/jsИспользование контентаbabel-loaderпередачаbabelНастроены пресеты и плагины для перевода.

Продолжаем поддерживатьTypeScriptБар!

настроитьTypeScriptслужба поддержки

противTypeScriptНа самом деле в отрасли существует два метода компиляции поддержки кода:

  1. непосредственно черезTypeScriptкомпилироватьts/tsxкод.
  2. пройти черезbabelПереводить.

На самом деле, оба метода могут добиться компиляцииTypeScriptкод становитсяJavaScriptИ совместим с кодом браузера более низкой версии.

Заинтересованные друзья могут сами поискать разницу между этими двумя методами, обычно я использую его непосредственно при упаковке некоторых библиотек классов.tscкомбинироватьtsconfig.jsкомпилироватьtsкод.

В своей повседневной работе я лично все еще использую его в большинстве случаев.babelПереводы часто требуются, когда дело доходит до бизнесаcssи т.д. статические ресурсы сtsкодируйте вместе, затем используйтеbabel + webpackНа самом деле он вполне может охватывать весь процесс компиляции ресурсов за один раз.

babelслужба поддержкиTypescirpt

babelВстроенный набор пресетов для переводаTypeScriptкод --@babel/preset-typescript.

Далее давайте использовать@babel/preset-typescriptподдержка по умолчаниюTypeScriptГрамматика.

npm install --save-dev @babel/preset-typescript

После завершения установки давайте пошагово изменим предыдущую конфигурацию:

Сначала изменим.babelrcконфигурационный файл, пустьbabelПеревод поддерживаетсяtsдокумент:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
+   "@babel/preset-typescript"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ]
  ]
}

мы здесьpresetsДобавлен@babel/preset-typescrptпо умолчанию разрешитьbabelслужба поддержкиtypescriptграмматика.

В это время нашbabelуже идентифицируемыйTypeScriptграмматика

webpackслужба поддержкиts/tsxдокумент

Не забудьте также изменить нашwebpackсерединаbabel-loaderПравила соответствия для:

// webpack.base.jf
const path = require('path');

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    // 这里修改`jsx`为`tsx`
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  },
  module: {
    rules: [
      {
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
    ],
  },
};

Здесь мы будемts,js,tsx,jsxдокументы переданыbabel-loaderиметь дело с.

инициализацияtsconfig.json

Теперь наш проект уже может поддерживатьtsxЗапись файлов, но также поддерживает компиляциюtsфайл более ранней версииjsфайл.

В использованииTsКогда, обычно нам нужно настроитьtypescriptконфигурационный файл, это правильноtsconfig.json.

Может быть, вы видели это много разtsconfig.jsonТеперь давайте установим егоtypescriptИ инициализируйте его~

Установить в проектTs:

yarn add -D typescript

передачаtsc --initинициализация командыtsconfig.json:

npx tsc --init

оnpxкоманда иnpmотношений, о которых я подробно расскажу в другой статье. Посмотрите, для чего он здесь используется: вызов текущего проекта.node_modules/typescript/binИсполняемый файл выполняетсяinitкоманда сделает.

Теперь наш каталог должен выглядеть так:

image.png

Хотя мы используемbabelсоставлено,tsconfig.jsonне вступает в силу во время компиляции. ноtsconfig.jsonКонфигурация в нем очень сильно влияет на наш опыт разработки, поэтому давайте немного изменим ее.

настроитьtsconfig.json

Сначала давайте найдем соответствующийjsxОпции:

image.png

Его роль заключается в назначенииjsxКакой код генерируется, если говорить простыми словами, то естьjsxВо что будет преобразован код.

Здесь мы модифицируем его наreact.

image.png

Далее изменим его.tsправила синтаксического анализа модуля в , измените его наnode:

"moduleResolution": "node",

image.png

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

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

Обработка ошибок

Мы уже отлично поддерживаем это в проектеtypescript, то положимpacakges/home/index.jsxизменить наpackages/home/index.tsxБар.

После изменения суффикса давайте посмотрим на файл проекта, который нам нужен:

image.png

Давайте решим эти ошибки одну за другой:

Сначала мы ссылаемся на сторонний пакет вTypeScriptфайл, проще говоря, он будет искать соответствующий пакетpackage.jsonсерединаtypeполе, чтобы найти соответствующий файл определения типа.

reactа такжеreact-domВ коде этих двух пакетов нет соответствующего объявления типа, поэтому нам нужно установить соответствующие файлы объявления типов отдельно:

yarn add -D @types/react-dom @types/react

Большинство дополнительных пакетов определения типов, которые вы можете найти вздесьоказаться

После завершения установки давайте посмотрим на текущуюindex.tsxдокумент:

image.png

На данный момент остается только одна ошибка. Рассмотрим подробнее, как найти ошибку.tsскажи намReactDom.renderТипы параметров, переданные в метод, несовместимы. Ну, это по сути мыreactГрамматика неверна. Модифицированный код выглядит следующим образом:

image.png

На данный момент наш проект готов поддержатьtypescriptа такжеreact.

webpackНастройка поддержки статических ресурсов

Зрелый проект может иметь толькоtsКак это может быть? В конце концов, как зрелому бизнесмену спастись?cssВолшебный улов 😂

Возможно, вы были в контакте сwebpack5Предыдущая обработка статических ресурсов,file-loader,url-loader,row-loaderЭтиloaderЗвучит очень знакомо.

webpackПо умолчанию не поддерживается не-jsфайл, так что вwebpack5прежде чем мы прошлиloaderспособ вернуть исполняемый файлjsфайл сценария, который будет обрабатывать эти внутренниеwebpackНеизвестный файл.

существуетwebpack 5+версия после этихloaderФункции были встроены~

Далее давайте посмотрим, как его настроить, и конкретные соответствующие функции можно посмотретьwebpackресурсный модуль

Обрабатывать изображения, файловые ресурсы

Тип модуля ресурсов заменяет все эти загрузчики, добавляя 4 новых типа модулей:

  • asset/resourceОтправьте отдельный файл и экспортируйте URL. ранее, используяfile-loader выполнить.
  • asset/inlineЭкспортирует URI данных ресурса. ранее с помощьюurl-loader выполнить.
  • asset/sourceИсходный код экспортируемого ресурса. ранее с помощьюraw-loader выполнить.
  • assetАвтоматический выбор между экспортом URI данных и отправкой отдельного файла. ранее с помощьюurl-loaderи настройте реализацию ограничения объема ресурсов.

когда вwebpack 5использовать старыйassets loader(Такие как file-loader/url-loader/raw-loaderи т. д.) и модули ресурсов, вы можете остановить обработку текущего модуля ресурсов и начать обработку снова, это может привести к дублированию ресурсов, вы можете сделать это, установив тип модуля ресурсов на'javascript/auto'Разрешить.

О конфигурацииtype:'asset'После этого webpack автоматически загрузитresource а также inlineВыберите между: файлы размером менее 8 КБ будут рассматриваться какinlineтип модуля, в противном случае рассматривается какresourceТип модуля.

Мы можем установитьRule.parser.dataUrlCondition.maxSizeвозможность изменить это условие

фактическиmaxSizeэквивалентноurl-loaderсерединаlimitсвойство, размер ресурса находится вmaxSizeиспользовать встроенныйasset/inlineобработки, использовать после превышенияresourceЭкспорт ресурсов. Конечно, эта конфигурация также поддерживает экспорт реализации пользовательской конфигурации функции.

понялassetsПосле назначения модуля попробуем настроить его для обработки статических ресурсов:

const path = require('path');

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  },
  module: {
    rules: [
      {
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
      {
        test: /\.(png|jpe?g|svg|gif)$/,
        type:'asset/inline'
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
};

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

Проверьте эффект конфигурации

Исправлятьpackages

Сначала изменимpackages,packagesУпомянутая ранее папка предназначена для хранения входного файла каждой страницы в многостраничном приложении. впусти насhomeСоздайте новый в папкеapp.tsx:

// src/packages/home/app.tsx
import React from 'react'
import ReactDom from 'react-dom'
// 这里App仅接着就会降到 它就是就是一个React FunctionComponent
import { App } from '../../containers/home/app.tsx'

ReactDom.render(<App />, document.getElementById('root'))

Исправлятьcontainers

containersСодержимое папки — это та часть, в которой хранится различная бизнес-логика, применяемая к каждой странице.

впусти насcontainersЧжунсинapp.tsxВ качестве последующего приложения и создайте новое в каталоге того же уровня.assets,styles,viewsТри каталога:

  • assetsСохраняет статические ресурсы, связанные с текущим модулем, такие как изображения, файлы шрифтов и т. д.
  • stylesЕсть файлы стилей, относящиеся к текущему модулю, и мы не настроили обработку связанных файлов стилей, которые будут подробно представлены позже.
  • viewsХраните логические разбиения соответствующих страниц в текущем модуле.

мы даемassetsсоздать новыйimagesпапку, положитьlogo,картина.

Исправлятьapp.tsxСодержание следующее:

import React from 'react'
import Banner from './assets/image/banner.png'

const App: React.FC = () => {
  return <div>
    <p>Hello,This is pages!</p>
    <img src={Banner} alt="" />
  </div>
}

export {
  App
}

yarn build

Наша текущая структура каталогов выглядит следующим образом:

image.png

Мы успешно построили базовую структуру каталогов, давайте запустимyarn build.

Сначала согласноwebpackВходной файл будет искатьpackages/home/index.tsx,мы вindex.tsxвведены соответствующиеcontainers/app.tsx,webpackбудет основываться на нашемimportсинтаксис для обработки зависимостей модулей для сборки модулей.

Также потому, что мыapp.tsxфотографии представлены в

// webpack.base.js
 {
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'
  },

В настоящее времяtype:'assets/inline'Обработка фотографий вступит в силу!

Давайте посмотримbuildПосле файла:

image.png

asset/inlineФайлы ресурсов речи встроены вbase64а такжеurl-loaderтот же эффект.

текущий токwebpackизasset/inlineМодуль конвертирует все ресурсы вbase64Двигайтесь в пределах ряда, если вы хотите достичьurl-loaderсерединаlimitКонфигурация должна быть отключенаasset/inlineиспользоватьurl-loaderобрабатывать или использоватьtype:'asset'обрабатывать.

Устранить ошибку

Осторожно, вы, возможно, обнаружили, что в настоящее время в нашем проекте есть две проблемы:

  1. tsфайл дляimageвступление кtsне могут быть правильно идентифицированы.

image.png

Способ решения этой проблемы на самом деле очень прост, мы определяемimage.d.tsВ корневом каталоге:

declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';

declareдля синтаксиса объявления, что означает объявитьtsГлобальные модули, чтобы мы могли нормально импортировать соответствующие ресурсы.

  1. мы вindex.tsxвведены соответствующиеapp.tsx, когда присутствует суффиксtsПоявится сообщение об ошибке:

image.png

Давайте решим эту проблему дальше. На самом деле проблема в имени суффикса по умолчанию при импорте файлов:

  • В настоящее времяwebpackСуффикс по умолчанию не поддерживается.tsx
  • а такжеtsconfig.jsonв имени суффикса поддержки.tsx, поэтому оператор display вызовет ошибку.

Давайте объединим эти две конфигурации:

Объединение псевдонимов

ИсправлятьwebpackКонфигурация псевдонима

// webpack.base.js
const path = require('path');

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  },
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    },
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  },
  module: {
    rules: [
      {
        // 同时认识ts jsx js tsx 文件
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
      {
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
};

Здесь мы добавляемresolveпараметры, псевдонимы настроены@src,@package,@container.

И когда мы не пишем суффикс файла, правила синтаксического анализа по умолчаниюextensionsправило.

Также настроеноmainFiles, разобрать путь к папке~

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

Давайте попробуем изменитьindex.tsx, используя псевдоним для импорта:

image.png

На этот раз мы обнаружили, что нет подсказки пути, это! Это действительно недопустимо!

Причина в том, что мы основываемся наtypescriptразвитие, такtsфайл не знает, что мы находимся вwebpackПуть псевдонима, настроенный в .

Итак, все, что нам нужно сделать, это синхронизировать измененияtsconfig.json,Позволятьtsconfig.jsonПсевдонимы и предыдущая конфигурация также поддерживаются для достижения наилучшего опыта разработки.

Исправлятьtsconfig.jsonКонфигурация псевдонима

давайте изменимtsconfig.json,ПозволятьtsДобавлена ​​поддержка синхронизации:

    // tsconfig.json
    ...
     "baseUrl": "./",
    /* Specify the base directory to resolve non-relative module names. */
    "paths": {
      "@src/*": ["./src/*"],
      "@packages/*": ["./src/packages/*"],
      "@containers/*": ["./src/containers/*"],
    },

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

по этому мыpathsДобавьте соответствующий путь псевдонима, чтобы завершить настройку, пустьtsМы также можем разумно разрешать псевдонимы наших типов.

Теперь давайте посмотрим на это снова:

image.png

Подсказка пути уже может отображаться правильно, не так ли?nice.

На этом мы подошли к концу настройки правила парсинга каталога пути/файла~

настроитьcss/sass

Затем мы добавляем несколько стилей в проект, чтобы украсить его.

На самом деле здесьReactслишком много проектовcssссориться, но мы собираемсяwebpackсреда дляcssобработанный.

Здесь я предпочитаю использоватьsassпрепроцессор для демонстрации, прочееlessи т.д. одинаковые.

Таргетинг наsassфайл, то же самоеwebpackНеизвестный файл. нам также нужноloaderиметь дело с.

используется здесьloaderследующим образом:

  • sass-loader
  • resolve-url-loader
  • postcss-loader
  • css-loader
  • MiniCssExtractPlugin.loader

Давайте проанализируем их один за другимloaderСоответствующая конфигурация роли:

sass-loader

Таргетинг наsassДокумент Сначала мы должны использовать егоsassкомпилируется вcss, поэтому сначала нам нужно.scssФайл в конце скомпилирован какcssдокумент.

Здесь нам нужно установить:

yarn add -D sass-loader sass 

sass-loaderТребуется предварительная установкаDart Sass или Node Sass(Дополнительную информацию можно найти по этим двум ссылкам). Это может контролировать версию всех зависимостей и свободно выбирать реализацию Sass для использования.

sass-loaderпохоже на то, что мы говорили раньшеbabel-loader, который можно понимать как мост,sassперевести наcssЯдро состоит изnode-sassилиdart-sassкомпилировать.

resolve-url-loader

Мы уже упоминали в предыдущем шагеsass-loaderБудуsassфайл преобразован вcssдокумент.

Но здесь есть фатальная проблема, которая касаетсяwebpackправильноscssв файле

Поскольку реализация Saass не обеспечиваетперезапись URLфункции, поэтому связанные ресурсы должны относиться к выходному файлу (ouput) с точки зрения.

  • Если сгенерированный CSS передается вcss-loader, все правила URL должны относиться к файлу записи (например:main.scss).
  • Если создается только файл CSS, он не передается вcss-loader, тогда все URL-адреса относятся к корневому каталогу сайта.

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

Для решения проблемы внедрения путей в отрасли существует множество готовых решений, например, через

  1. Переменная пути определяет путь импорта
  2. определить псевдонимы,sassИспользовать псевдоним пути импорта в
  3. resolve-url-loader

Здесь мы используемresolve-url-loaderдля решения проблемы пути импорта файлов.

не забудьyarn add -D resolve-url-loader

postcss-loader

Что такое PostCSS? Возможно, вы подумали бы об этом как о препроцессоре, постпроцессоре и т. д. На самом деле, это ничего. Его можно понимать как подключаемую систему.

Таргетинг наpostcssНа самом деле, я не буду подробно объяснять здесь, этоbabelОба два бегемота. Иметь свою независимую систему, здесь нужно четко понимать, что мы используемpostcss-loaderобработка сгенерированаcss.

post-css

yarn add -D postcss-loader postcss

postcss-loaderloader

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default',
    }),
  ],
}

postcss

  • autoprefixercss
  • cssnanocss

yarn add -D cssnano autoprefixer@latest

css-loader

css-loadercss@import/require

yarn add -D css-loader

MiniCssExtractPlugin.loader

CSSJSCSSSourceMaps

style-loaderMiniCssExtractPluginstyle-loader

style-loadercsshtmlheaderMiniCssExtractPlugin.loadercsscss

yarn add -D mini-css-extract-plugin

sass

sass

// webapck.base.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  },
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    },
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
      {
        test: /\.(sa|sc)ss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
          'postcss-loader',
          {
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true
            },
          },
        ],
      },
      {
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline'
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
    }),
  ]
};
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default',
    }),
  ],
}

containers/src/home/stylessassindex.scss:

.body {
	background-color: red;
}

image.png

yarn build

distsasscss

image.png

scss

html

html

html

html-webpack-plugin

html

chunks

yarn add --dev html-webpack-plugin

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id='root'></div>
</body>

</html>

ReactDom.reander(...,document.getElementById('root'))id=rootdiv

// webpack.base.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 入口文件,这里之后会着重强调
  entry: {
    main: path.resolve(__dirname, '../src/packages/home/index.tsx'),
  },
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    },
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
      {
        test: /\.(sa|sc)ss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
          'postcss-loader',
          {
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline',
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
    }),
    // 生成html名称为index.html
    // 生成使用的模板为public/index.html
    new htmlWebpackPlugin({
      filename: 'index.html',
      template: path.resolve(__dirname, '../public/index.html'),
    }),
  ],
};

yarn builddisthtmlhtmljscss

image.png

index.html

SPA

webpackdevServer

scriptswebpack.dev.js

devServerhot:truewebpack-dev-server

// webpack.dev.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base');
const path = require('path');

const devConfig = {
  mode: 'development',
  devServer: {
    // static允许我们在DevServer下访问该目录的静态资源
    // 简单理解来说 当我们启动DevServer时相当于启动了一个本地服务器
    // 这个服务器会同时以static-directory目录作为跟路径启动
    // 这样的话就可以访问到static/directory下的资源了
    static: {
      directory: path.join(__dirname, '../public'),
    },
    // 默认为true
    hot: true,
    // 是否开启代码压缩
    compress: true,
    // 启动的端口
    port: 9000,
  },
};

module.exports = merge(devConfig, baseConfig);

devServerproxy,onlistening

webpack-mergewebpackwebpack.base.jswebpack.dev.js

pacakge.jsonscripts

devServerwebpack serve

...
 "scripts": {
+   "dev": "webpack serve --config ./scripts/webpack.dev.js",
    "build": "webpack --config ./scripts/webpack.base.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

yarn devlocalhost:9000

tipdevServer

image.png

node-portfinder

scripts/utils/constant.js

// scripts/utils/constant.js
...
// 固定端口
const BASE_PROT = 9000
...
module.exports = {
  MAIN_FILE,
  log,
  separator,
  BASE_PROT
}

webpack.dev.js:

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')
const portfinder = require('portfinder')
const path = require('path')
const { BASE_PROT } = require('./utils/constant')

portfinder.basePort = BASE_PROT

const devConfig = {
  mode: 'development',
  devServer: {
    // static允许我们在DevServer下访问该目录的静态资源
    // 简单理解来说 当我们启动DevServer时相当于启动了一个本地服务器
    // 这个服务器会同时以static-directory目录作为跟路径启动
    // 这样的话就可以访问到static/directory下的资源了
    static: {
      directory: path.join(__dirname, '../public'),
    },
    // 默认为true
    hot: true,
    // 是否开启代码压缩
    compress: true,
    // 启动的端口
    port: BASE_PROT,
  },
}

module.exports = async function () {
  try {
    // 端口被占用时候 portfinder.getPortPromise 返回一个新的端口(往上叠加)
    const port = await portfinder.getPortPromise()
    devConfig.devServer.port = port
    return merge(devConfig, baseConfig)
  } catch (e) {
    throw new Error(e)
  }
}

webpack

portfinder.getPortPromise()portfinderdevConfig

yarn dev~

image.png

js

webpackentryjsentry

src/packages/editorindex.tsxwebpackentrywebpackchunk

webapckjschunk

image.png

yarn builddist

image.png

jsmain.jseditor.js

js

html

jsindex.htmlmain.jsmain.html

editor.jseditor.html

html-webpack-plugin

chunks

chunkshtmlchunks

jseditor.jsmain.jshtmlhtmleditor.jsmain.js

htmlWebpackPlugineditorchunkmainchunk

image.png

yarn build

image.png

htmljswebpack

htmlWebpackPlugin

homeeditordev

webpack

  1. node
  2. pacakges

nodechild_processnode

inquirerapinodejs

chalk

yarn add -D chalk inquirer execa      

scriptsutils

utils/constant.js

constant.js

// 规定固定的入口文件名 packages/**/index.tsx
const MAIN_FILE = 'index.tsx'
const chalk = require('chalk')

// 打印时颜色
const error = chalk.bold.red
const warning = chalk.hex('#FFA500')
const success = chalk.green

const maps = {
  success,
  warning,
  error,
}

// 因为环境变量的注入是通过字符串方式进行注入的
// 所以当 打包多个文件时 我们通过*进行连接 比如 home和editor 注入的环境变量为home*editor
// 注入多个包环境变量时的分隔符
const separator = '*'


const log = (message, types) => {
  console.log(maps[types](message))
}

module.exports = {
  MAIN_FILE,
  log,
  separator,
  BASE_PORT,
}

utils/helper.js

const path = require('path')
const fs = require('fs')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { MAIN_FILE } = require('./constant')

// 获取多页面入口文件夹中的路径
const dirPath = path.resolve(__dirname, '../../src/packages')

// 用于保存入口文件的Map对象
const entry = Object.create(null)

// 读取dirPath中的文件夹个数
// 同时保存到entry中  key为文件夹名称 value为文件夹路径
fs.readdirSync(dirPath).filter(file => {
  const entryPath = path.join(dirPath, file)
  if (fs.statSync(entryPath)) {
    entry[file] = path.join(entryPath, MAIN_FILE)
  }
})

// 根据入口文件list生成对应的htmlWebpackPlugin
// 同时返回对应wepback需要的入口和htmlWebpackPlugin
const getEntryTemplate = packages => {
  const entry = Object.create(null)
  const htmlPlugins = []
  packages.forEach(packageName => {
    entry[packageName] = path.join(dirPath, packageName, MAIN_FILE)
    htmlPlugins.push(
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../../public/index.html'),
        filename: `${packageName}.html`,
        chunks: ['manifest', 'vendors', packageName],
      })
    )
  })
  return { entry, htmlPlugins }
}

module.exports = {
  entry,
  getEntryTemplate,
}

helper.jsgetEntryTemplatepackagewebpackentryhtml-wepback-plugin

utils/dev.js

  • packages
  • execawebpack
const inquirer = require('inquirer')

const execa = require('execa')
const { log, separator } = require('./constant')
const { entry } = require('./helper')

// 获取packages下的所有文件
const packagesList = [...Object.keys(entry)]

// 至少保证一个
if (!packagesList.length) {
  log('不合法目录,请检查src/packages/*/main.tsx', 'warning')
  return
}

// 同时添加一个全选
const allPackagesList = [...packagesList, 'all']

// 调用inquirer和用户交互
inquirer
  .prompt([
    {
      type: 'checkbox',
      message: '请选择需要启动的项目:',
      name: 'devLists',
      choices: allPackagesList, // 选项
      // 校验最少选中一个
      validate(value) {
        return !value.length ? new Error('至少选择一个项目进行启动') : true
      },
      // 当选中all选项时候 返回所有packagesList这个数组
      filter(value) {
        if (value.includes('all')) {
          return packagesList
        }
        return value
      },
    },
  ])
  .then(res => {
    const message = `当前选中Package: ${res.devLists.join(' , ')}`
    // 控制台输入提示用户当前选中的包
    log(message, 'success')
    runParallel(res.devLists)
  })

// 调用打包命令
async function runParallel(packages) {
  // 当前所有入口文件
  const message = `开始启动: ${packages.join('-')}`
  log(message, 'success')
  log('\nplease waiting some times...', 'success')
  await build(packages)
}

// 真正打包函数
async function build(buildLists) {
  // 将选中的包通过separator分割
  const stringLists = buildLists.join(separator)
  // 调用通过execa调用webapck命令
  // 同时注意路径是相对 执行node命令的cwd的路径 
  // 这里我们最终会在package.json中用node来执行这个脚本
  await execa('webpack', ['server', '--config', './scripts/webpack.dev.js'], {
    stdio: 'inherit',
    env: {
      packages: stringLists,
    },
  })
}

dev.jspackagesexecawebpack

*appeditorpackagesapp*editor

webpack.base.js

pacakge

wepback.base.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
const { separator } = require('./utils/constant')
const { getEntryTemplate } = require('./utils/helper')

// 将packages拆分成为数组 ['editor','home']
const packages = process.env.packages.split(separator)

// 调用getEntryTemplate 获得对应的entry和htmlPlugins
const { entry, htmlPlugins } = getEntryTemplate(packages)

module.exports = {
  // 动态替换entry
  entry,
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, '../src'),
      '@packages': path.resolve(__dirname, '../src/packages'),
      '@containers': path.resolve(__dirname, '../src/containers'),
    },
    mainFiles: ['index', 'main'],
    extensions: ['.ts', '.tsx', '.scss', 'json', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(t|j)sx?$/,
        use: 'babel-loader',
      },
      {
        test: /\.(sa|sc)ss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
          'postcss-loader',
          {
            loader: 'resolve-url-loader',
            options: {
              keepQuery: true,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: /\.(png|jpe?g|svg|gif)$/,
        type: 'asset/inline',
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'assets/[name].css',
    }),
    // 同时动态生成对应的htmlPlugins
    ...htmlPlugins
  ],
};

packagesgetEntryTemplatehtmlWebpackPlugin

package.json

package.json

 "scripts": {
 -  "dev": "webpack serve --config ./scripts/webpack.dev.js",
 +  "dev": "node ./scripts/utils/dev.js",
    "build": "webpack --config ./scripts/webpack.base.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

devscripts/utils/dev.js

yarn dev:

image.png

home

image.png

image.png

home.htmlhome.jslocalhost:9000/home.html

http://localhost:9000/editor.htmlCannot GET /editor.html

devproduction

production

productiondevwebpack

scriptswebpack.prod.js

// 这里我使用了默认的`webpack`production下的配置
// 如果你需要额外的配置,可以额外添加配置。
// 这里提供动态多页应用的流程 具体压缩/优化插件和配置 各位小哥可以去官网查看配置~
// 之后我也会在文章开头的github仓库中提供不同branch去实践最佳js代码压缩优化
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')

const prodConfig = {
  mode: 'production',
  devtool: 'source-map',
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, '../dist'),
  },
  plugins: [
    new CleanWebpackPlugin(),
    new FriendlyErrorsWebpackPlugin()
  ]
}

module.exports = merge(prodConfig, baseConfig)

scripts/build.jsdev

const inquirer = require('inquirer')
const execa = require('execa')
const { log, separator } = require('./constant')
const { entry } = require('./helper')

const packagesList = [...Object.keys(entry)]

if (!packagesList.length) {
  log('不合法目录,请检查src/packages/*/main.tsx', 'warning')
  return
}
const allPackagesList = [...packagesList, 'all']

inquirer
  .prompt([
    {
      type: 'checkbox',
      message: '请选择需要打包的项目:',
      name: 'buildLists',
      choices: allPackagesList, // 选项
      validate(value) {
        return !value.length ? new Error('至少选择一个内容进行打包') : true
      },
      filter(value) {
        if (value.includes('all')) {
          return packagesList
        }
        return value
      },
    },
  ])
  .then(res => {
    // 拿到所有结果进行打包
    const message = `当前选中Package: ${res.buildLists.join(' , ')}`
    log(message, 'success')
    runParallel(res.buildLists)
  })

function runParallel(packages) {
  const message = `开始打包: ${packages.join('-')}`
  log(message, 'warning')
  build(packages)
}

async function build(buildLists) {
  const stringLists = buildLists.join(separator)
  await execa('webpack', ['--config', './scripts/webpack.prod.js'], {
    stdio: 'inherit',
    env: {
      packages: stringLists,
    },
  })
}

execainquirer

webpack

Eslint & prettier

prettier

yarn add --dev --exact prettier

echo {}> .prettierrc.js

js

module.exports = {
  printWidth: 100, // 代码宽度建议不超过100字符
  tabWidth: 2, // tab缩进2个空格
  semi: false, // 末尾分号
  singleQuote: true, // 单引号
  jsxSingleQuote: true, // jsx中使用单引号
  trailingComma: 'es5', // 尾随逗号
  arrowParens: 'avoid', // 箭头函数仅在必要时使用()
  htmlWhitespaceSensitivity: 'css', // html空格敏感度
}

.prettierignoreprettier

//.prettierignore 
**/*.min.js
**/*.min.css

.idea/
node_modules/
dist/
build/

huskylint-stagedgit hook

husky&list-staged

commitlit-staged

image.png

ts

// package.json
...
  "lint-staged": {
    "*.{js,css,md,ts,tsx,jsx}": "prettier --write"
  }
...

ESlint

Eslint

yarn add eslint --dev

eslint

npx eslint --init

eslint

image.png

prettiereslintyarn add -D eslint-config-prettieeslint

eslintprettiereslint

// .eslint.js
module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended",
        // 添加`prettier`拓展 用于和`prettier`冲突时覆盖`eslint`规则
        "prettier"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 12,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
};

.eslintignorets

*.d.ts
scripts/**

Pages

Pagescliclipages~