---
title: "بناء مواقع ثابتة بدون React باستخدام React (الجزء 3 من 3)"
description: "بعد تجهيز بيئة التطوير باستخدام Storybook لملفات JS وCSS وبقية الأصول، نبني خط build ينشر ملفات HTML وJS وCSS باستخدام Webpack."
canonical: https://oozou.com/ar/blog/building-static-sites-without-react-using-react-part-3-3-71
locale: ar
date: 2020-08-06
author: "OOZOU Team"
---

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

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

Published: 2020-08-06 · Author: OOZOU Team (Engineering) · 8 min read

# كود JavaScript

بما أن جميع الملفات تُستورد عبر الـ hook المسمى useScripts في الجزء الثاني، علينا المرور على كل ملف صفحة وجمع كل السكربتات فيه ثم تجميعها في حزم باستخدام [Webpack](https://webpack.js.org/).

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

سنضيف في هذا الجزء [Webpack](https://webpack.js.org/) و [MiniCssExtractPlugin](https://webpack.js.org/plugins/mini-css-extract-plugin/) و [CSSLoader](https://webpack.js.org/loaders/css-loader/) و [Lodash](https://lodash.com/)، لذا نفّذوا الأمر التالي:

```
yarn add webpack webpack-cli lodash --dev
```

توفّر [Lodash](https://lodash.com/) عددًا كبيرًا من الأدوات المفيدة للتعامل مع المصفوفات، وسنستخدمها في الجزء التالي.

# لننتقل إلى Webpack

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

# نقاط الدخول

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

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

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

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

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

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

```javascript
// 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.

```javascript
// 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

```javascript
// 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.

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

بعد أن نشغّل

```
yarn build:entries
```

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

```javascript
// 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](https://webpack.js.org/plugins/mini-css-extract-plugin/) و [CSSLoader](https://webpack.js.org/loaders/css-loader/) و [StatsWebpackPlugin](https://www.npmjs.com/package/stats-webpack-plugin)

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

سنستخدم [MiniCssExtractPlugin](https://webpack.js.org/plugins/mini-css-extract-plugin/) و [CSSLoader](https://webpack.js.org/loaders/css-loader/) لاستخراج ملفات CSS النهائية. أما [StatsWebpackPlugin](https://www.npmjs.com/package/stats-webpack-plugin) فنحتاجه لتوليد إحصاءات التجميع في Webpack، حتى نتمكن من الإشارة إلى ملفات JS وCSS الصحيحة التي بُنيت لكل صفحة.
وإليكم الإعدادات، كل ما عليكم هو إنشاء الملف webpack.config.js في جذر المشروع.

```javascript
// 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

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

ثم نشغّل

```
yarn build:webpack
```

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

```javascript
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 ليشغّل العمليات الثلاث بالتتابع.

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

بعد ذلك نشغّل

```
yarn build
```

يمكننا الآن تقديم ملفات HTML وCSS وJS باستخدام أي خادم ملفات ثابتة. أفضّل شخصيًا [http-server](https://www.npmjs.com/package/http-server)، ويمكن تثبيته على مستوى النظام هكذا:

```
npm install --global http-server
```

لنجرّب تشغيل

```
http-server dist
```

ثم نفتح [http://127.0.0.1:8080/pages/Home.html](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)

وأخيرًا، تجدون الكود المصدري للعملية كاملة [هنا](https://github.com/oozou/react-no-react/tree/master).

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

---

Contact OOZOU: hello@oozou.com · +66 2544 0000 · https://oozou.com/ar/contact

More for AI agents: https://oozou.com/llms.txt · every page has a markdown twin at its URL plus ".md"
