OOZOU
تواصلوا معنا
العودة إلى المدونة

بناء مواقع ثابتة بدون React باستخدام React (الجزء 3 من 3)

بعد أن جهّزنا بيئة التطوير باستخدام Storybook لملفات JS وCSS وبقية الأصول، حان الوقت أخيرًا لبناء خط build ينشر ملفات HTML وJS وCSS باستخدام Webpack.

6 أغسطس 2020
8 دقائق للقراءة
بناء مواقع ثابتة بدون React باستخدام React (الجزء 3 من 3)

كود JavaScript

بما أن جميع الملفات تُستورد عبر الـ hook المسمى useScripts في الجزء الثاني، علينا المرور على كل ملف صفحة وجمع كل السكربتات فيه ثم تجميعها في حزم باستخدام Webpack.

المزيد من التبعيات

سنضيف في هذا الجزء Webpack و MiniCssExtractPlugin و CSSLoader و Lodash، لذا نفّذوا الأمر التالي:

yarn add webpack webpack-cli lodash --dev

توفّر Lodash عددًا كبيرًا من الأدوات المفيدة للتعامل مع المصفوفات، وسنستخدمها في الجزء التالي.

لننتقل إلى Webpack

أصبح Webpack شائعًا إلى درجة أنه يرافق كل مشروع تطوير ويب تقريبًا هذه الأيام. فهو يجمع كل عمليات الاستيراد المتفرقة لملفات JS من كل وحدة ES6 (أي من كل ملف .js و.jsx) في حزم، ويتولى عنا تجميع ملفات الأنماط. فما الذي لا يعجبنا فيه؟

نقاط الدخول

بما أننا نبني الموقع صفحةً صفحة، سنعامل كل صفحة على أنها نقطة دخول (entry point). ومحتوى كل نقطة دخول هو ملفات JavaScript المستوردة داخل الـ hook المسمى useScripts. سننشئ سكربتًا يولّد الملف entryPoints.js أثناء المرور على الصفحات. وإليكم نظرة عامة على ما نفعله:

لجمع السكربتات، سننشئ وحدة مساعدة تتتبع السكربتات داخل كل مكوّن صفحة.

الفكرة الأساسية هي المرور على شجرة المكوّنات باستدعاء دالة العرض في React، وإطلاق عمليات جانبية (side effects) تجمع كل السكربتات.

ويمكننا أيضًا الاستفادة من عبارات import لجمع عمليات استيراد CSS كذلك.

ولتسهيل الفهم، سنرى أولًا كيف نستخدم هذه الوحدة المساعدة.

لنعدّل سلوك useScripts أثناء عملية build.

// useScripts.js
import { useEffect } from "react";
// The utils we'll be building
import { addScripts } from "../scripts/buildUtils";

export function useScripts(scriptLoaders) {
  // Accumulate imported scripts during builds
  if (process.env.BUILD) {
    const scriptPaths = scriptLoaders.map((load) => {
      const stringified = load.toString();
      // We extract the import path from the stringified load function
      const match = stringified.match(/"([^"]*)"/);
      if (!match) {
        throw new Error(
          `Error finding matching imported module under \`useScripts\` hooks\nstringified:\n${stringified}`
        );
      }

      return match[1].replace(/\?.*$/, "");
    });

    // The parent's id is required to resolve the absolute path for the script
    addScripts(scriptPaths, module.parent.id);
    // Node caches only the first parent but we need the intermediate ones
    // so we have to clear it.
    delete require.cache[__filename];
  }

  useEffect(() => {
    loadScripts(scriptLoaders);
  });
}

// Each dynamic import is asynchronous
async function loadScripts(scriptLoaders) {
  for (const load of scriptLoaders) {
    await load();
  }
}

والآن السكربت الفعلي الذي يستدعي عملية العرض في React ويطلق العمليات الجانبية، وهو scripts/generateEntryPoints.js.

// generateEntryPoints.js
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import path from "path";
import fs from "fs";
import uniq from "lodash/uniq";
import glob from "glob";
// The utils are also used here
import {
  setCurrentChunkName,
  getScriptGroup,
  addScripts,
  getChunkNameFromPagePath,
} from "../scripts/buildUtils";

// Side-effects tapped into import statements for CSS files
require.extensions[".css"] = (cssModule) => {
  addScripts([cssModule.id], cssModule.parent.id);
};

const cwd = path.resolve(__dirname, "..");

const pages = glob.sync("pages/**/*.jsx", { cwd });

function main() {
  pages.forEach((pagePath) => {
    // We'll use each page's path as its own chunk name
    setCurrentChunkName(getChunkNameFromPagePath(pagePath));
    const PageComponent = require(path.resolve(cwd, pagePath)).default;
    // This is where the magic happens. It invokes React's rendering and will trigger
    // the side-effects to gather the scripts.
    renderToStaticMarkup(<PageComponent />);
  });

  const assetsImportContent = Object.entries(getScriptGroup())
    .map(([chunkName, scriptPaths]) => {
      return `  '${chunkName}': [${uniq(scriptPaths)
        .map((scriptPath) => `'${scriptPath}'`)
        .join(",\n")}]`;
    })
    .join(",\n");

  const assetsMapContent = `module.exports = {\n${assetsImportContent}\n}\n`;

  // The generated entry points are in object notation written in to a file.
  fs.writeFileSync(
    path.resolve(__dirname, "../.tmp/entries.js"),
    assetsMapContent
  );
}

main();

وأخيرًا، لننفّذ الوحدة المساعدة scripts/buildUtils.js

// buildUtils.js
import path from "path";

let currentChunkName;
const scriptGroup = {};

export function getScriptGroup() {
  return scriptGroup;
}

export function addScripts(scripts, parentId) {
  if (!scriptGroup[currentChunkName]) {
    scriptGroup[currentChunkName] = [];
  }
  const resolvedScripts = scripts.map((s) =>
    path.resolve(path.basename(parentId), s)
  );
  scriptGroup[currentChunkName].push(...resolvedScripts);
}

export function setCurrentChunkName(chunkName) {
  currentChunkName = chunkName;
  scriptGroup[currentChunkName] = [];
}

export function getChunkNameFromPagePath(pagePath) {
  return pagePath.replace(/\//g, "-");
}

ينبغي أن تبدو بنية المشروع الآن على هذا النحو.

├── pages
│   ├── AboutUs.jsx
│   ├── Home.css
│   └── Home.jsx
├── scripts
│   ├── build.js
│   ├── buildUtils.js // added
│   └── generateEntryPoints.js // added
├── src
│   ├── about-us.js
│   ├── home.js
│   └── useScripts.js // modified
├── stories
│   └── pages.stories.js
├── babel.config.js
└── package.json

بعد ذلك نضيف سكربتًا إلى package.json لتشغيل السكربت generateEntryPoints.js.

// package.json
"scripts": {
  "build:entries": "mkdir -p .tmp && BUILD=1 babel-node scripts/generateEntryPoints.js",
  ...
}

بعد أن نشغّل

yarn build:entries

سنجد ملفًا في .tmp/entries.js يبدو هكذا:

// entries.js
module.exports = {
  "pages-AboutUs.jsx": [
    "/abs/path/to/pages/Home.css",
    "/abs/path/to/src/about-us",
  ],
  "pages-Home.jsx": [
    "/abs/path/to/pages/Home.css",
    "/abs/path/to/src/home",
  ],
};

أصبحت نقاط الدخول جاهزة. سنجعل Webpack الآن يجمع ملفات JS في حزم، ثم نستخدم إحصاءات التجميع التي يولّدها لربطها من جديد بملفات HTML. لنضف MiniCssExtractPlugin و CSSLoader و StatsWebpackPlugin

yarn add mini-css-extract-plugin css-loader stats-webpack-plugin --dev

سنستخدم MiniCssExtractPlugin و CSSLoader لاستخراج ملفات CSS النهائية. أما StatsWebpackPlugin فنحتاجه لتوليد إحصاءات التجميع في Webpack، حتى نتمكن من الإشارة إلى ملفات JS وCSS الصحيحة التي بُنيت لكل صفحة. وإليكم الإعدادات، كل ما عليكم هو إنشاء الملف webpack.config.js في جذر المشروع.

// webpack.config.js
const path = require("path");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const StatsPlugin = require("stats-webpack-plugin");

const entries = require("./.tmp/entries");

module.exports = {
  entry: {
    ...entries,
  },
  resolve: {
    extensions: [".js", ".jsx"],
  },
  output: {
    path: path.resolve(__dirname, "dist/assets"),
    publicPath: "/assets/",
    filename: "[name]-[contenthash].js",
  },
  mode: "production",
  target: "web",
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new StatsPlugin("../../.tmp/webpack-stats.json"),
  ],
};

لنضف السكربت الخاص بتشغيل Webpack

"scripts": {
  ...
  "build:webpack": "webpack",
  ...
},

ثم نشغّل

yarn build:webpack

ستجدون الآن الملف .tmp/webpack-stats.json، وسنستخدمه مرجعًا لملفات JS وCSS المجمّعة. القطعة الأخيرة من اللغز التي تربط كل شيء معًا هي الملف scripts/build.js. لنعدّله على النحو التالي:

import React from "react";
import path from "path";
import fs from "fs";
import glob from "glob";
import { renderToStaticMarkup } from "react-dom/server";
import { spawnSync } from "child_process";
import webpackStats from "../.tmp/webpack-stats.json";
import { getChunkNameFromPagePath } from "./buildUtils";

require.extensions[".css"] = () => {};

const cwd = path.resolve(__dirname, "..");

// We glob all the files under "pages" directory
const reactFiles = glob.sync("pages/**/*.jsx", { cwd });

// Generate <link rel="stylesheet" href="..."> tags from Webpack's stats file
function generateStyleTags(assets) {
  function getCSSAssets() {
    if (!assets) {
      return [];
    }

    if (Array.isArray(assets)) {
      return assets.filter((asset) => asset.endsWith(".css"));
    }

    return assets.endsWith(".css") ? [assets] : [];
  }

  const cssAssets = getCSSAssets();

  return cssAssets.length > 0
    ? cssAssets
        .map((asset) => `<link rel="stylesheet" href="/assets/${asset}">`)
        .join("\n")
    : "";
}

// Generate <script type="text/javascript" src="..."> tags from Webpack's stats file
function generateScriptTags(assets) {
  function getJSAssets() {
    if (!assets) {
      return [];
    }

    if (Array.isArray(assets)) {
      return assets.filter((asset) => asset.endsWith(".js"));
    }

    return assets.endsWith(".js") ? [assets] : [];
  }

  const jsAssets = getJSAssets();

  return jsAssets.length > 0
    ? jsAssets
        .map(
          (asset) =>
            `<script type="text/javascript" src="/assets/${asset}"></script>`
        )
        .join("\n")
    : "";
}

reactFiles.forEach((reactFile) => {
  const fileName = path.basename(reactFile).replace(".jsx", "");

  // Page components are from "default" export
  const Component = require(path.resolve(__dirname, "../", reactFile)).default;
  const chunkName = getChunkNameFromPagePath(reactFile);
  const assets = webpackStats.assetsByChunkName[chunkName];
  const staticMarkup = renderToStaticMarkup(<Component />);

  // Generate HTML file name from ".jsx" files by replacing the extension with ".html"
  const htmlFile = path.resolve(
    __dirname,
    "../dist/",
    reactFile.replace(".jsx", ".html")
  );

  const dirName = path.dirname(htmlFile);

  // In case we have nested page structure, we need to create directory recursively.
  fs.mkdirSync(dirName, { recursive: true });

  fs.writeFileSync(
    htmlFile,
    `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>${fileName}</title>
  ${generateStyleTags(assets) /* CSS files are linked here */}
</head>
<body>
  ${staticMarkup}
  ${generateScriptTags(assets) /* JS files are linked here */}
</body>
</html>`,
    "utf8"
  );
});

وأخيرًا، لنعدّل سكربت build ليشغّل العمليات الثلاث بالتتابع.

"scripts": {
  ...
  "build": "yarn build:entries && yarn build:webpack && babel-node scripts/build.js",
  ...
},

بعد ذلك نشغّل

yarn build

يمكننا الآن تقديم ملفات HTML وCSS وJS باستخدام أي خادم ملفات ثابتة. أفضّل شخصيًا http-server، ويمكن تثبيته على مستوى النظام هكذا:

npm install --global http-server

لنجرّب تشغيل

http-server dist

ثم نفتح http://127.0.0.1:8080/pages/Home.html وها هو! سترون عند فحص HTML أن الملف /assets/pages-Home.jsx.css والملف /assets/pages-Home.jsx-...-js مربوطان بشكل صحيح بفضل إضافة الإحصاءات، و"انظري يا أمي! بدون React!!!" يا له من ماراثون! لفهم المفاهيم وواجهات API المستخدمة في هذه السلسلة فهمًا حقيقيًا، نحتاج إلى معرفة عميقة بطريقة عمل Node.js وReact وWebpack معًا. تهدف هذه السلسلة إلى إعطاء فكرة عن الطريقة التي يمكن بها لمطوّر يتعامل يوميًا مع هذه التقنيات أن يبتكر طريقة لحل المشكلات. ستلاحظون كثيرًا من الحيل والأفكار الجريئة هنا. وبالطبع هناك مجال للتحسين، مثل:

  • إعادة التحميل الفوري (hot reloading) للسكربتات المستوردة داخل useScripts (إدارة ذاكرة cache الخاصة بعمليات require)
  • التحميل المسبق للأصول النهائية في HTML
  • إعادة هيكلة الكود لتقليل الكود المتكرر (boilerplate)

وأخيرًا، تجدون الكود المصدري للعملية كاملة هنا.

تهانينا لكل من قرأ حتى النهاية. آمل أن يمنحكم هذا فكرة عن كيفية إنشاء موقع ثابت بسيط باستخدام React. لا تترددوا في ترك تعليقاتكم حتى أحسّن كتابتي وطريقتي في مشاركة المعرفة مستقبلًا. أتطلع إلى ذلك! هذا كل شيء الآن. إلى اللقاء في المرة القادمة.

Related Articles

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

تصميم البرمجيات عملية ممتعة لكنها معقدة، تتطلب الإبداع والتفكير المنطقي ومهارات حل المشكلات.

اقرأوا المزيد
مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

يتطور المشهد الرقمي بسرعة، وعلى الشركات أن تتكيّف لتواكب الطلب المتزايد على السرعة والكفاءة والمعالجة في الوقت الفعلي.

اقرأوا المزيد
مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

يتطور قطاع التجزئة بوتيرة غير مسبوقة بفضل الذكاء الاصطناعي (AI) والواقع المعزّز (AR) وتقنيات التحوّل الرقمي. فلم تعد المتاجر التقليدية مجرد أماكن لبيع المنتجات، بل أصبحت متاجر ذكية وتفاعلية.

اقرأوا المزيد

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

ابدأوا المحادثةhello@oozou.com
OOZOU Logo

بانكوك · سنغافورة · هونغ كونغ

X
Facebook
Instagram
LinkedIn
OOZOU Co., Ltd.
548 One City Centre
Ploenchit Road
Bangkok 10330
Thailand
+66 2544 0000
hello@oozou.com

الخدمات

  • تطوير الويب
  • وكلاء الذكاء الاصطناعي والذكاء الاصطناعي التوليدي
  • تطوير تطبيقات الجوال
  • تحليلات البيانات وهندستها
  • تصميم تجربة المستخدم وواجهته (UX/UI) وتصميم المنتجات
  • التحوّل الرقمي

الشركة

  • من نحن
  • الوظائف
  • المدونة
  • دراسات الحالة
  • عروض حية
  • تعلّمت اليوم
  • تواصل معنا

المزيد

  • القطاعات
  • شبكة الشركاء
© 2026 OOZOU. جميع الحقوق محفوظة.
سياسة الخصوصيةمدونة قواعد السلوكسياسة مكافحة الرشوة والفساد