Поговорим о внешнем «изоморфизме»

задняя часть внешний интерфейс сервер браузер

1. Что такое изоморфизм

Изоморфизм относится к одной и той же разработке программы, которая может работать на разных платформах. Например, разработка фрагмента кода js может одновременно использоваться веб-сервером и браузером, разработанным на основе node.js. В этой статье мы поговорим о том, зачем и как разрабатывать изоморфное веб-приложение в этом сценарии.

Преимущества изоморфизма

Мы не принимаем никаких решений без причины, и все используют изоморфизм, потому что изоморфизм может принести некоторые преимущества:

  • Сократите объем разработки кода и увеличьте объем повторного использования кода. Поскольку фрагмент кода может выполняться в браузере и на сервере одновременно, не только уменьшается объем кода, но и не требуется одновременно поддерживать многие бизнес-логики в браузере и на сервере. таким образом уменьшая возможность программных ошибок в то же самое время.
  • Функцию SSR (Server-Side Render) можно выполнить за небольшую плату. И SSR может принести как минимум два следующих преимущества.
    • Производительность в верхней части страницы позволяет пользователям увидеть содержимое страницы раньше.
    • SEO (поисковая оптимизация), удобный для поисковых роботов.

3. Проблемы, вызванные изоморфизмом

  • Потеря производительности, клиент и сервер должны отображать страницу, и есть определенная потеря производительности (ее можно максимально оптимизировать с помощью антисбора клиентского dom и виртуального-дома, но это неизбежно).
  • Модуль, который может быть изоморфным, должен быть совместим как с клиентской средой, так и со средой Node.js, что требует дополнительных затрат на разработку. Особенно тем, кто привык к разработке на стороне клиента, следует обратить внимание на окна, документы, DOM и т. д., которые существуют только на стороне клиента.
  • Риск переполнения памяти на стороне сервера, среда выполнения клиентского кода будет восстановлена ​​при обновлении браузера, поэтому вам не нужно уделять слишком много внимания проблеме переполнения памяти, но на стороне сервера все по-другому.
  • Особое внимание следует уделить асинхронным операциям.Студенты, привыкшие к разработке на стороне клиента, могут привыкнуть инициировать асинхронные запросы данных и операции на внешнем интерфейсе по своему желанию, поскольку все операции вызывают перерисовку страницы. На стороне сервера все по-другому.Компоненты на стороне сервера могут вызывать рендеринг только один раз или ограниченное количество раз, поэтому все асинхронные запросы на рендеринг на стороне сервера должны быть выполнены до возврата html путем вызова рендеринга.
  • Все состояние, предварительно загруженное на стороне сервера, должно иметь возможность для клиента получить его, чтобы избежать различных результатов рендеринга между клиентом и сервером и вызвать заставки на экране. Поскольку клиент все равно отобразит страницу один раз, если данные, используемые сервером для рендеринга, отличаются от данных клиента, отображаемый дом также будет другим, что приведет к появлению заставки.

4. Какие части приложения могут быть изоморфны

  1. Маршруты в одностраничном приложении могут быть изоморфными, поэтому при доступе к любой подстранице одностраничного приложения можно пользоваться преимуществами SSR.
  2. Шаблоны, передняя и задняя части имеют общий механизм рендеринга, а передняя и задняя части могут использовать общие шаблоны, поэтому дни, когда необходимо было разработать два набора шаблонов, потому что одни и те же данные используются для переднего и заднего рендеринга, ушли навсегда.
  3. Запрос данных, разработка httpClient, который поддерживает изоморфизм, тогда код внешнего и внутреннего запроса данных также может быть изоморфным. Обратите внимание, что на стороне сервера нет файлов cookie, поэтому код запроса, связанный с сеансом, должен быть очень осторожным.
  4. Другой независимый от платформы код, такой как модуль управления глобальным состоянием, процесс обработки данных и некоторые независимые от платформы чистые функции, которые есть как у react, так и у vue.

5. Какие вещи не могут быть изоморфны

  • Код, связанный с платформой, такой как операции, связанные с DOM и BOM, может выполняться только на стороне браузера, а чтение и запись файлов и операции с базой данных могут выполняться только на стороне сервера.

6. Нужен ли нам изоморфизм?

6.1 Можно ли получить преимущества изоморфизма другими способами?

  • SSR

    Конечно, SSR не обязательно должен быть реализован с помощью изоморфизма, но использование изоморфизма для реализации SSR может сократить объем повторяющейся разработки кода.

  • Уменьшить вероятность ошибок за счет использования двух копий кода на фронтальной и серверной части для одновременного поддержания копии логики.

    Я не могу придумать лучшего решения этой проблемы, чем изоморфизм.

Когда требуется SSR, я чувствую, что та же структура все еще необходима.

6.2 Поддержка изоморфизма без злоупотребления SSR

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

  • Поддержка изоморфизма, фрагмент кода может выполняться как на клиенте, так и на сервере, но запускать этот код на сервере или нет, зависит от конкретного бизнеса.
  • Используйте SSR только тогда, когда требования к производительности и SEO высоки, и используйте чистый рендеринг на стороне клиента в других случаях, что кажется хорошим компромиссом.

7. Напишите многостраничное приложение, поддерживающее изоморфизм, с нуля

Пример адреса репозитория кода.
Будет лучше прочитать абзацы ниже, начиная пример. В примере используется Express в качестве среды веб-сервера, поэтому читателям будет легче понять пример, если они знакомы с основами работы с Express.

7.1 Обязанности внешнего и внутреннего кода

  • Внешний интерфейс: [(одностраничное приложение) маршрутизация процесса -> ] данные запроса -> рендеринг -> события привязки
  • Бэкенд: [ маршрутизация процесса -> ] данные запроса -> рендеринг

7.2 Различия в роли внешнего и внутреннего кода

  • Внешний интерфейс: без вывода, код действует непосредственно на элементы страницы
  • Бэкенд: вывод html-строки

7.3 Оценка среды выполнения кода

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

    const isBrowser = typeof window !== 'undefined';

7.4 Базовый дизайн изоморфного приложения

基本设计

7.5 Базовые классы изоморфных компонентов

7.5.1 Планирование жизненного цикла

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

  • Клиент:
    beforeMount -> render -> mounted
  • Сервер:
    preFetch -> beforeMount -> render

Жизненные циклы beforeMount и render выполняются как на стороне сервера, так и на стороне клиента, поэтому в течение этих двух жизненных циклов не следует писать специфичный для платформы код.

Ниже приведен базовый класс изоморфных компонентов, используемых в этой демонстрации:

// ./lib/Component.js
const {isBrowser} = require('../utils');

module.exports = class Component {
    constructor (props = {}, options) {
        this.props = props;
        this.options = options;
        this.beforeMount();
        if (isBrowser) {
            // 浏览器端才执行的生命周期
            this.options.mount.innerHTML = this.render();
            this.mounted();
            this.bind();
        }
    }
    // 生命周期
    async preFetch() {}
    // 生命周期
    beforeMount() {}
    // 生命周期
    mounted() {}
    // 绑定事件时使用
    bind() {}
    // 重新渲染时调用
    setState() {
        this.options.mount.innerHTML = this.render();
        this.bind();
    }
    render() {
        return '';
    }
};

Все бизнес-компоненты наследуют этот базовый класс, например, фактический бизнес-компонент выглядит следующим образом:

// ./pages/index.js
const Component = require('../lib/Component');

module.exports = class Index extends Component {
    render() {
        return `
            <h1>我是首页</h1>
            <a href="/list">列表页</a>
        `;
    }
}

Доступен после запуска примераhttp://localhost:3000/Чтобы посетить эту страницу, вы можете наблюдать за ситуацией SSR.

7.6 Обработка на стороне сервера

7.6.1 Использование ServerRenderer для визуализации компонентов

Простая реализация ServerRenderer выглядит следующим образом:

// ./lib/ServerRenderer.js
const path = require('path');
const fs = require('fs');

module.exports = async (mod) => {
    // 获取组件
    const Component = require(path.resolve(__dirname, '../', mod));
    // 获取页面模板
    const template = fs.readFileSync(path.resolve(__dirname, '../index.html'), 'utf8');
    // 初始化业务组件
    const com = new Component()
    // 数据预取
    await com.preFetch();
    // 将组件渲染的字符串输出到页面模板
    return template.replace(
        '<!-- ssr -->', 
        com.render() +
            // 把后端获取的数据放到全局变量中供前端代码初始化
            '<script>window.__initial_props__ = ' + 
            JSON.stringify(com.props) +
            '</script>'
    )
    // 替换插入静态资源标签
    .replace('${modName}', mod);
}
7.6.2 Шаблоны страниц

Все страницы этой демонстрации используют один и тот же HTML-шаблон:

<!-- ./index.html -->
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <div id="app">
            <!-- 下面是 ssr 渲染后内容填充的占位符 -->
            <!-- ssr -->
        </div>
        <!-- 插入实际业务的前端 js 代码 -->
        <script src="http://localhost:9000/build/${modName}"></script>
    </body>
</html>
7.6.3 Маршрутизация и контроллер на стороне сервера

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

// ./routes/index.js
var express = require('express');
var router = express.Router();
var ServerRenderer = require('../lib/ServerRenderer');

router.get('/', function(req, res, next) {
  ServerRenderer('pages/index.js').then((html) => {
    res.send(html); 
  });
}); 

7.7 клиентский процессор ClientLoader

ClientLoader в демо — это загрузчик веб-пакетов, и код загрузчика выглядит следующим образом:

// ./lib/ClientLoader.js
module.exports = function(source) {
    return `
        ${source}
        // 入口文件 export 的是主组件
        const Com = module.exports;
        // 获取后端渲染时使用的初始状态 window.__initial_props__,保证前后端渲染结果一致。
        new Com(window.__initial_props__, {
            mount: document.querySelector('#app')
        });
    `;
};

Используйте этот плагин в webpack.config.js (только для компонентов входа на страницу)

// webpack.config.js
module.exports = {
    ...,
    module: {
        rules: [
        ...,
            {
                test: /pages\/.+\.js$/,
                use: [
                    {loader: path.resolve(__dirname, './lib/ClientLoader.js')}
                ]
            }
        ],
    }
};

7.8 Бизнес-компонент

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

7.8.1 Код
// ./routes/index.js
/* GET list page. */
router.get('/list', function(req, res, next) {
  ServerRenderer('pages/list.js').then((html) => {
    res.send(html); 
  });
});

// ./pages/list.js
const Component = require('../lib/Component');
const {
    getList,
    addToList
} = require('../api/list.api');

module.exports = class Index extends Component {
    constructor (props, options) {
        super(props, options);
    }
    // 服务端执行,预取列表数据
    async preFetch() {
        await this.getList();
    }
    async getList() {
        const list = (await getList()).data;
        this.props.list = list;
    }
    ...,
    render() {
        return `
            <h1>我是列表页</h1>
            <button class="add-btn">add</button>
            <button class="save-btn">save</button>
            <ul>
                ${
                    this.props.list.length ? 
                    this.props.list.map((val, index) => `
                        <li>
                            ${val.name}
                            <button class="del-btn">删除</button>
                        </li>
                    `).join('') :
                    '列表为空'
                }
            </ul>
        `;
    }
}
7.8.2 Результаты рендеринга на стороне сервера

Если демонстрационный сервер запущен, посетитеhttp://localhost:3000/listВы можете увидеть результаты рендеринга на стороне сервера следующим образом.window.__initial_props__Его существование гарантирует согласованность результатов внешнего и внутреннего рендеринга.

<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <div id="app">
            <h1>我是列表页</h1>
            <button class="add-btn">add</button>
            <button class="save-btn">save</button>
            <ul>
                <!-- 服务端渲染预取的列表数据 -->
                <li>
                    1
                    <button class="del-btn">删除</button>
                </li>
                <li>
                    2
                    <button class="del-btn">删除</button>
                </li>
                <li>
                    3
                    <button class="del-btn">删除</button>
                </li>
                <li>
                    4
                    <button class="del-btn">删除</button>
                </li>
            </ul>
            <script>
                // 预取的列表数据, 用来客户端渲染
                // 客户端第一次渲染和服务端渲染结果相同,因此用户看不到客户端渲染的效果。
                window.__initial_props__ = {
                    "list": [{
                        "name": 1
                    }, {
                        "name": 2
                    }, {
                        "name": 3
                    }, {
                        "name": 4
                    }]
                }
            </script>
        </div>
        <script src="http://localhost:9000/build/pages/list.js"></script>
    </body>
</html>

Восемь, одностраничное изоморфное приложение.

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

8.1 Обработка на стороне сервера

Внесите информацию о маршрутизации при инициализации компонента:

// ./lib/ServerRenderer.js
module.exports = async (mod, url) => {
    ...
    // 初始化业务组件
    const com = new Component({
        url
    });
    ...
}

При написании контроллера введите маршрут в ServerRenderer:

/* GET single page. */
router.get('/single/:type', function(req, res, next) {
  ServerRender('pages/single.js', req.url).then((html) => {
    res.send(html); 
  });
});

8.2 Код клиента

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

// ./pages/single.js
const Component = require('../lib/Component');

module.exports = class Index extends Component {
    switchUrl() {
        const isYou = this.props.url === '/single/you';
        const newUrl = `/single/${isYou ? 'me' : 'you'}`;
        this.props.url = newUrl;
        window.history.pushState({}, 'hahha', newUrl);
        this.setState();
    }
    bind() {
        this.options.mount.getElementsByClassName('switch-btn')[0].onclick = this.switchUrl.bind(this);
    }
    render() {
        ;
        return `
            <h1>${this.props.url}</h1>
            <button class="switch-btn">切换</button>
        `;
    }
}

доступ/single/youКонтент, возвращаемый сервером:

<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <div id="app">
            <h1>/single/you</h1>
            <button class="switch-btn">切换</button>
            <script>
                window.__initial_props__ = {
                    "url": "/single/you"
                }
            </script>
        </div>
        <script src="http://localhost:9000/build/pages/single.js"></script>
    </body>
</html>

9. Изоморфизм публичного государственного управления

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

10. Специальный HTTP-клиент

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

知识共享许可协议
В этой работе используетсяCreative Commons Attribution-NonCommercial-ShareAlike 4.0 Международная лицензияЛицензия.