Django и Webpack - реализация современных инструментов Javascript
Я работаю над проектом Django и хочу улучшить фронтенд с помощью npm и webpack. Мой опыт веб-разработки был в основном с Django, и я новичок в npm и webpack. Я столкнулся с несколькими проблемами в процессе интеграции, и мне нужен совет.
Вот некоторые из библиотек, которые я включаю в свой проект:
- Django v3.2
- python 3.7.13
- Используемые библиотеки узла
- Bootstrap v5.3.3
- Bootstrap-icons v1.11.3
- jQuery v3.7.1
- jQuery-ui v1.13.3
- jQuery-validation v1.20.0
- metro4-dist v4.5.0
- webpack v5.91.0
- webpack-cli v5.1.4
- webpack-dev-server v5.0.4
- [etc]
Учебники/руководства, которые я использовал
Делюсь руководствами, которые я использовал, чтобы помочь другим людям в их сценарии на их пути.
- Modern JavaScript for Django Developers : Это помогло мне понять, зачем нужны npm и webpack.
- Definitive Guide to Django and Webpack : Отличное руководство для детальной настройки.
- Learn Webpack - Full Tutorial for Beginners : Видео, которое проведет вас через настройку .
- Django with Webpack : Еще одно видео, объясняющее, как настроить webpack в проекте Django.
Придя из книги "Современный Javascript для Django", я понял, что это лучше всего подходит для моей ситуации.
7Сначала вы изучаете Django. Вы проходите учебник, узнаете о моделях и представлениях и создаете свое самое первое приложение "hello world". и запускаете его. Вы в ударе!
Затем, неизбежно, вы захотите сделать что-то, что требует немного взаимодействия с внешним интерфейсом. Может быть, модальный диалог, форма в реальном времени валидации или мобильное меню. Неважно.
"Нет проблем!" - скажете вы. "Я буду использовать JavaScript для этого!"
Django позволяет легко внедрять JavaScript непосредственно в ваши HTML шаблоны, поэтому вы немного погуглите и скопируете/вставите код из Stack Overflow, возможно, импортируете jQuery и плагин или два, и вы готовы к работе. работаете.
И это отлично работает. Все еще в ударе!
Со временем появляется больше модалов, больше меню и больше страниц. Вы начинаете делать более сложные вещи, такие как управление состоянием, динамическое рендеринг компонентов и совместное использование кода в шаблонах.
Все это начинает казаться громоздким, но вы продолжаете работать. Документ селектор здесь, обратный вызов события там, может быть, еще несколько библиотек импортированные на различные страницы по мере необходимости. Внешний код медленно расширяется за счет ваших шаблонов Django и статических файлов, пока не возникает ощущение, что что JavaScript протянул свои щупальца по всей кодовой базы.
В какой-то момент ваш внешний код может начать напоминать спагетти монстра - запутанную кашу из кода и зависимостей, разбросанных по вашим JavaScript и файлах шаблонов Django.
<<<1
Есть несколько замечательных руководств, которые я смог найти, но ни одно из них не подходит именно для тех случаев, когда мне нужна помощь. Я думаю, что большинство моих проблем сводится к изучению концепций при следовании руководствам, которые изменились в реализации (например, webpack4 > webpack5)
Проблемы
openBrowser() не работает так, как предполагалось
Функция, которую я написал для запуска браузера на webpack-dev-сервере, не работает так, как ожидалось. Есть предложения, как этого добиться?
Настройка webpack-dev-server с Django для HMR
Интеграция webpack-dev-server с сервером Django для Hot Module Replacement оказалась непростой задачей. Я попробовал несколько различных методов, но все время получал пустую веб-страницу с ошибкой "Cannot Get /" в соответствии с руководствами, которым я следовал.
В настоящее время то, что работает - это
proxy
опция в конфигурации devServer иwriteToDisk
опция для devMiddleware. Я спрашиваю, есть ли лучший/оптимальный подход? Могу ли я сделать так, чтобы сервер Django мог читать файлы из пакета через память?Пакетные файлы не распознаются при определенных URL
При просмотре инструментов разработчика браузера я могу увидеть исходный текст страницы для моих объединенных файлов, когда мой URL-адрес имеет следующий вид:
- http://localhost:8080/
- http://localhost:8080/help
- http://localhost:8080/sysetms/
- http://localhost:8080/sites/
Но мои файлы в комплекте не распознаются, если я пытаюсь перейти на страницу типа:
- http://localhost:8080/report/site/[some_name]/
- http://localhost:8080/report/system/[some_name]/
Как я
мог-
-
<svg class="bi" width="32" height="32" fill="currentColor"> <use href="bootstrap-icons.svg#heart-fill"/> </svg>
Конфигурация
Notes
- Я использую некоторые теги шаблонов Django , чтобы помочь с организацией подшаблонов (т.е.
{% include subtemplate.html %}
или{$% block content %} {% endblock content %}
, чтобы не все HTML-шаблоны нуждались в объединенных файлах при загрузке страницы - вот почему я используюinject: false
опцию для HtmlWebpackPlugin. - У меня есть несколько шаблонов, которые Django должен уметь распознавать, поэтому я использую HtmlWebpackPlugin для создания всех этих HTML-шаблонов в
static/
папке корневого каталога. - При запуске webpack-dev-server я хочу по умолчанию использовать браузер Google Chrome. Если он недоступен на хост-машине, то используйте браузер по умолчанию хоста.
- Я использую много SVG-спрайтов из Bootstrap-icons в своих шаблонах, одну иконку из metro4-dist, и у меня есть несколько документов .pdf, которые я хочу, чтобы webpack создал в папке
dist/
в соответствующих директориях. Я читал, что SVG лучше делать встроенными, а не создавать отдельный файл, так ли это?
- Я знаю, что по умолчанию webpack размещает выходные файлы в
dist/
, а Django ищет свои исходные файлы вstatic/
, поэтому конфигурацию нужно менять для одного из них. Я заставил Django читать статические url из папки "/dist/"
package.json
{
"name": "[removed for privacy]",
"version": "1.0.0",
"private": true,
"description": "[removed for privacy]",
"exports": "./index.js",
"scripts": {
"start": "webpack-dev-server --config webpack/webpack.config.dev.js",
"build": "webpack --config webpack/webpack.config.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.1",
"css-minimizer-webpack-plugin": "^6.0.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.9.0",
"open": "^10.1.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-attributes": "^1.0.0",
"sass": "^1.77.0",
"sass-loader": "^14.2.1",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
},
"dependencies": {
"@floating-ui/dom": "^1.6.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
"jquery-validation": "^1.20.0",
"metro4-dist": "^4.5.0"
},
"type": "module",
"engines": {
"node": ">=16"
}
}
webpack.config.common.js
webpack.config.dev.js
import MiniCssExtractPlugin from "mini-css-extract-plugin"
import open from "open"
import path from "path"
import {fileURLToPath} from "url"
import {merge} from "webpack-merge"
import common from "./webpack.config.common.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const url = "http://localhost:8080"
function getBrowserName() {
if (process.platform === "darwin") {
return "Google Chrome"
} else if (process.platform === "linux") {
return "google-chrome"
} else {
return "chrome"
}
}
async function openBrowser(url) {
const browserName = getBrowserName()
try {
await open(url, {
app: {
name: browserName,
},
})
console.log(`Opened devServer with URL: ${url}`)
} catch (error) {
await open(url)
console.error(
"Failed to open devServer with Google Chrome browser. Opening in default browser instead.",
)
}
}
const config = merge(common, {
mode: "development",
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "../dist"),
clean: true,
},
devtool: "source-map",
devServer: {
hot: true,
proxy: [
{
context: ["**"],
target: url,
},
],
open: openBrowser(url),
devMiddleware: {
writeToDisk: true,
},
},
plugins: [new MiniCssExtractPlugin({filename: "[name].css"})],
module: {
rules: [
{
test: /\.scss$/i,
use: [
MiniCssExtractPlugin.loader, // 3. Extract css into its own file
"css-loader", // 2. Turns css into commonjs
"sass-loader", // 1. Turns sass into css
],
},
{
test: /\.html$/i,
use: ["html-loader"],
},
{
test: /\.svg$/i,
type: "asset/inline",
generator: {
filename: "images/[name][ext]",
},
},
{
test: /\.(png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name][ext]",
},
},
{
test: /\.(pdf|txt)$/i,
type: "asset/resource",
generator: {
filename: "documents/[name][ext]",
},
},
],
},
})
export default config
webpack.config.prod.js
import CssMinimizerWebpackPlugin from "css-minimizer-webpack-plugin"
import MiniCssExtractPlugin from "mini-css-extract-plugin"
import path from "path"
import TerserPlugin from "terser-webpack-plugin"
import {fileURLToPath} from "url"
import {merge} from "webpack-merge"
import common from "./webpack.config.common.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const config = merge(common, {
mode: "production",
output: {
filename: "[name].[contenthash].bundle.js",
path: path.resolve(__dirname, "../dist"),
clean: true,
},
optimization: {
minimizer: [new CssMinimizerWebpackPlugin(), new TerserPlugin()],
},
plugins: [new MiniCssExtractPlugin({filename: "[name].[contenthash].css"})],
module: {
rules: [
{
test: /\.scss$/i,
use: [
MiniCssExtractPlugin.loader, // 3. Extract css into its own file
"css-loader", // 2. Turns css into commonjs
"sass-loader", // 1. Turns sass into css
],
},
{
test: /\.html$/i,
use: ["html-loader"],
},
{
test: /\.svg$/i,
type: "asset/inline",
generator: {
filename: "images/[name].[hash][ext]",
},
},
{
test: /\.(png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name].[hash][ext]",
},
},
{
test: /\.(pdf|txt)$/i,
type: "asset/resource",
generator: {
filename: "documents/[name].[hash][ext]",
},
},
],
},
})
export default config
Django settings.py
[..some code..]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
# These settings are for Django serving the static files (Custom files)
STATIC_URL = "/dist/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
os.path.join(BASE_DIR, "dist"),
]
# This setting is for Apache serving the static files (Admin Page)
STATIC_ROOT = os.path.join(BASE_DIR, "static/admin")
ОБНОВЛЕНИЕ #1
Я собираюсь рассмотреть несколько моих проблем, которые я с тех пор решил, чтобы помочь всем, кто следит за этим постом. Не волнуйтесь, я также включу весь мой обновленный конфиг ниже.
Проблемы, которые я решил
Дополнительные вопросы, которые я решил и которые возникли после моего первого сообщения
PDF/статические документы загружаются некорректно
Мне пришлось внести некоторые изменения в конфиг webpack для модуля "html-loader", чтобы он умел обрабатывать теги anchor, , с атрибутом "href" , потому что я не хочу, чтобы webpack также включал случаи, когда у меня есть гиперссылка, которая использует функцию Django's render (она же передает значения из представления в шаблон) - webpack не знает, как читать
{{ some_django_view_value }}
. В общем, мне нужно было отфильтровать некоторые теги якорей, которые видел "html-загрузчик". Для примера, мой HTML может содержать два следующих тега якоря:<a class="nav-link nav-toggle d-flex align-items-center collapsed link-body-emphasis" data-bs-toggle="collapse" href="#create-new-menu" > <svg class="bi me-2" height="30" width="30" fill="currentColor" > <use href="~bootstrap-icons/bootstrap-icons.svg#chevron-right" ></use> </svg> <svg class="bi me-2" height="30" width="30" fill="currentColor" > <use href="~bootstrap-icons/bootstrap-icons.svg#clipboard-plus" ></use> </svg> Create New... </a>
и...
<a href="/some/URL/{{ using_django_view_value }}/link" class="nav-link d-flex align-items-center loading link-body-emphasis" > <svg class="bi me-2" height="30" width="30" fill="currentColor" > <use href="~bootstrap-icons/bootstrap-icons.svg#pin-map" ></use> </svg> Location </a>
надеюсь, это подчеркнет два различия.
Чтобы исправить ситуацию, я привел свой "html-загрузчик" в такой вид. Идея была почерпнута из здесь и здесь
.{ test: /\.html$/i, loader: "html-loader", options: { sources: { list: [ "...", { tag: "a", attribute: "href", type: "src", filter: (tag, attribute, attributes) => { return attributes.some( (attr) => /\.(pdf|txt)$/i.test(attr.value), ) }, }, ], }, }, },
Обновление конфигурации №1
Notes
- Хотя webpack-dev-server и HMR работают, я скажу, что функция
openBrowser()
, которая есть у меня вwebpack.config.dev.js
, не работает - так что у меня по-прежнему запускается несколько браузеров (браузер по умолчанию и предпочитаемый браузер). - Я хотел, чтобы webpack использовал только два конфига, dev и prod. Для dev-конфигураций я хотел дополнительно определить, запускать ли webpack-dev-server или просто "собрать" конфигурацию. Вы увидите, что мой npm-скрипт "start" передает переменную окружения, чтобы помочь создать это условие.
- Хочу убедиться, что мой внешний код использует ESM, а не CommonJS. (Мне нужно было разобраться в истории этого вопроса, чтобы понять, почему я вижу разные конфигурации для одинаковых результатов)
Также включены заметки из предыдущего поста...
- Я использую некоторые теги шаблонов Django для организации подшаблонов (т.е.
{% include subtemplate.html %}
или{$% block content %} {% endblock content %}
, поэтому не всем HTML-шаблонам нужны файлы, включенные через HTMLWebpackPlugin - поэтому я используюinject: false
для этих случаев. - HtmlWebpackPlugin выводит все HTML-шаблоны в
templates/
папку корневого каталога, чтобы Django работал в лучших практиках. - Я знаю, что по умолчанию webpack размещает выходные файлы в
dist/
, а Django ищет свои исходные файлы вstatic/
, поэтому конфигурация должна быть изменена для одного из них. Я сделал так, чтобы статические url Django читались из "/dist/" .
- Просмотрев несколько видео о проектах на node, я понял, что могу уменьшить свой package.json, удалив раздутые опции, которые не нужны для моего проекта. Просто убедитесь, что я установил опцию
private: true
.
package.json
{
"private": true,
"scripts": {
"start": "webpack serve --config webpack/webpack.config.dev.js --env devserver",
"build-dev": "webpack --config webpack/webpack.config.dev.js --watch",
"build-prod": "webpack --config webpack/webpack.config.prod.js"
},
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.1",
"css-minimizer-webpack-plugin": "^6.0.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.9.0",
"open": "^10.1.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-attributes": "^1.0.0",
"sass": "^1.77.0",
"sass-loader": "^14.2.1",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
},
"dependencies": {
"@floating-ui/dom": "^1.6.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
"jquery-validation": "^1.20.0",
"metro4-dist": "^4.5.0"
},
"type": "module",
"engines": {
"node": ">=16"
}
}
webpack.config.common.js
webpack.config.dev.js
webpack.config.prod.js
import CssMinimizerWebpackPlugin from "css-minimizer-webpack-plugin"
import MiniCssExtractPlugin from "mini-css-extract-plugin"
import path from "path"
import TerserPlugin from "terser-webpack-plugin"
import {fileURLToPath} from "url"
import {merge} from "webpack-merge"
import common from "./webpack.config.common.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const config = merge(common, {
mode: "production",
output: {
filename: "[name].[contenthash].bundle.js",
path: path.resolve(__dirname, "../dist"),
publicPath: "/dist/", // This is requried to help set the path correctly
clean: true,
},
optimization: {
minimizer: [new CssMinimizerWebpackPlugin(), new TerserPlugin()],
},
plugins: [new MiniCssExtractPlugin({filename: "[name].[contenthash].css"})],
module: {
rules: [
{
test: /\.scss$/i,
use: [
MiniCssExtractPlugin.loader, // 3. Extract css into its own file
"css-loader", // 2. Turns css into commonjs
"sass-loader", // 1. Turns sass into css
],
},
{
test: /\.html$/i,
loader: "html-loader",
options: {
sources: {
list: [
"...",
{
tag: "a",
attribute: "href",
type: "src",
filter: (tag, attribute, attributes) => {
return attributes.some((attr) =>
/\.(pdf|txt)$/i.test(attr.value),
)
},
},
],
},
},
},
{
test: /\.(|woff|woff2|eot|ttf|otf|svg)$/i,
type: "asset/resource",
generator: {
filename: "icons/[name][ext]",
},
},
{
test: /\.(png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name].[hash][ext]",
},
},
{
test: /\.(pdf|txt)$/i,
type: "asset/resource",
generator: {
filename: "documents/[name].[hash][ext]",
},
},
],
},
})
export default config