окончательное решение
Выпущено окончательное решение архитектуры проекта многостраничного приложения~
[COUNT] Окончательное решение WebPack4 + EJS + Multi-Page Multi-Page Project Project
egg
Версия для обновления производится в этом выпуске на основе надежности и масштабируемости сервера - увеличилась, но большинство деталей этого были полностью представлены, мы можем с нетерпением ждать двух совмещенных вместе.
предисловие
Полный адрес проекта на GitHub
Недавно я взял на себя проект на официальном веб-сайте компании, который требует SEO-дружественности, поэтому я не могу использовать интерфейсный фреймворк, а вспомогательные инструменты, которые поставляются с интерфейсным фреймворком, естественно, не могут помочь. просто используйте его самиwebpack4 + ejs + express
, построить архитектуру проекта многостраничного приложения с нуля. В процессе строительства я столкнулся со многими ямами, но в Интернете очень мало упоминаний о них, поэтому я написал блог, чтобы записывать процесс строительства и меры предосторожности.
Ниже я отмечу важные детали красным для справки друзьям, которым это нужно.
«Изоморфный» или «рендеринг на стороне сервера»?
После того, как эта статья была опубликована, друг спросил в области комментариев, почему бы напрямую не использовать некоторые изоморфные фреймворки, такие какnextjs
илиnuxtjs
? Эта проблема также может быть одной из самых запутанных проблем, когда мы развиваемся, позвольте мне рассказать о моих собственных мыслях.
изоморфизм
По сути, так называемая «изоморфизм", только когда загружается первый экран веб-страницы, используется рендеринг на стороне сервера, и сервер анализирует VDOM для создания реального DOM, а затем возвращается. Когда загружается код первого экрана веб-страницы и внешний интерфейс framework берет на себя браузер, весь последующий процесс будет завершен, это уже рендеринг на стороне клиента, и к серверу он не имеет никакого отношения.
преимущество
- Благодаря интерфейсной структуре структура страницы может быть организована в виде «компонентов», что является более гибким и пригодным для повторного использования.
- Поддерживаемые тремя большими интерфейсными фреймворками, различные подключаемые компоненты имеют хорошую экологию.
- Последующее переключение страниц происходит плавно, а взаимодействие с пользователем хорошее.
- Удобная работа с данными и обмен данными, подходящий для сложных бизнес-сценариев или сценариев, требующих обмена данными
недостаток
- Время загрузки первого экрана медленное, время белого экрана долгое, и будет экран-заставка (фактически из-за кода на стороне клиента, выполняющего другой рендеринг на стороне клиента)
- При парсинге VDOM и построении DOM сервером есть определенные потери в производительности, что в случае большого количества посещений может стать узким местом в производительности сервера.
- Процесс строительства сложен, в процессе строительства возникают проблемы, а устранение неполадок затруднено.
- Синхронизация данных затруднена из-за обезвоживания данных и закачки воды.
рендеринг на стороне сервера
В традиционном смысле рендеринга на стороне сервера серверная сторона напрямую генерирует статическую страницу и возвращает ее клиенту в течение всего жизненного цикла веб-страницы.
преимущество
- Процесс строительства относительно прост, а место ошибки удобно.
- Накладные расходы на производительность на стороне сервера невелики
- Первый экран загружается быстро, а время белого экрана короткое
недостаток
- Передняя и задняя части сильно связаны, код недостаточно гибкий, и его сложно использовать повторно.
- Операции с данными сложны, требуют от разработчиков ручного вмешательства в сложные операции DOM, не подходят для интенсивного взаимодействия и крупномасштабных операций с данными.
- В интерфейсном коде отсутствуют ограничения и управление фреймворком, что упрощает написание исходного кода.
выбор
На основе вышеизложенного, когда мы столкнулись с необходимостью рассмотретьSEO
проекта, когда выбрать изоморфизм внешнего и внутреннего интерфейса, а когда выбрать традиционный рендеринг на стороне сервера?
Я думаю: если ваш проектtoC
продукты, которые необходимо учитыватьSEO
, предполагающий большое количество взаимодействий с пользователем и частые изменения требований, то изоморфизм может подойти вам больше. Он может построить ваш проект в виде компонентов, которые сильно абстрагируются и повторно используются, и может поддерживать некоторые функции, которые невозможно выполнить в случае традиционного рендеринга на стороне сервера, например, клиент веб-страницы с облачной музыкой веб-страницы, а также может использоваться при переключении страниц.Чтобы песня была непрерывной, обязательно используется изоморфизм.
И если только одинtoB
Официальный веб-сайт малого и среднего предприятия учитывает SEO, но не требует большого взаимодействия с пользователем.После того, как изменения постобработки невелики, вы можете рассмотреть возможность выбора традиционного рендеринга на стороне сервера, который является методом, упомянутым в следующая статья.
четкие потребности
Перед началом разработки нам необходимо уточнить позиционирование данного проекта -официальный сайт компании, Вообще говоря, официальный сайт не предполагает большого количества взаимодействия с данными и больше склонен к отображению данных. Таким образом, интерфейсная структура не требуется,jquery
для удовлетворения потребностей. Но с учетом SEO, поэтому вам нужно использовать рендеринг на стороне сервера, вам нужно использовать язык шаблонов (ejs
), в комплекте с узлом.
Основываясь на приведенной выше информации, мы можем определить основные функции скрипта упаковки.Сначала составим простой список:
- нужно
webpack
для упаковки многостраничных приложений без добавления каждый раз нового файла представленияHTMLWebpackPlugin
И перезапуск сервера может отделить конфигурацию веб-пакета и имя файла и максимально автоматизировать. - Нужно использовать
ejs
Написан на шаблонном языке с возможностью вставки переменных и внешнихincludes
файл, общий файл шаблона (<meta>/<title>/<header>/<footer>
и т. д.) автоматически вставляются в соответствующее место каждого файла представления. - Требуется рендеринг на стороне сервера, поэтому среда разработки должна быть отделена от интеграции с веб-пакетом.
webpack-dev-server
, вы можете использовать свой собственный код узла для запуска службы. - иметь идеальный
overlay
функция, может быть какwebpack-dev-server
Это объединяет хороший оверлейный экран для сообщения об ошибках. - Может отслеживать изменения файлов, автоматически упаковывать и перезапускать службы, предпочтительно горячее обновление
начать строить
Сначала создайте пустой проект, так как нам нужно самим писать код сервера, нам нужно построить еще один/server
папка для храненияexpress
Код, после завершения сборки, структура нашего проекта выглядит так.
В дополнение к этому нам необходимо инициализировать некоторые общие файлы конфигурации, в том числе:
-
.babelrc
конфигурационный файл бабеля -
.gitignore
git игнорировать файлы -
.editorConfig
файл конфигурации редактора -
.eslintrc.js
конфигурационный файл eslint -
README.md
документ -
package.json
документ
После того, как вышел большой фреймворк, мы начали писать инженерный код.
сценарий упаковки
Во-первых, написать сценарий упаковки, в/build
Создайте несколько новых файлов в папке
-
webpack.base.config.js
Используется для хранения общих конфигураций WebPack для среды производства и развития -
webpack.dev.config.js
Среда разработки упаковки, используемая для хранения конфигурации -
webpack.prod.config.js
Конфигурация упаковки, используемая для хранения производственной среды -
config.json
Используется для хранения некоторых констант конфигурации, таких как имя порта, имя пути и т. д.
Вообще говоря,webpack.base.config
В файл поместите некоторые общие конфигурации для среды разработки и производства, такие какoutput
,entry
и немногоloader
, такие как компиляция синтаксиса ES6babel-loader
, упакованные файлыfile-loader
Ждать. Часто используемое использование загрузчика, мы можем просмотреть документациюwebpack loaders,
Следует отметить, что здесь есть очень важный загрузчик — ejs-html-loader
Как правило, мы используемhtml-loader
иди прямо.html
Файл представления в конце обрабатывается, а затем выбрасывается вhtml-webpack-plugin
создает соответствующий файл, ноhtml-loader
не может обрабатывать синтаксис шаблона ejs в<% include ... %>
синтаксис, сообщит об ошибке. Однако в многостраничных приложениях эта функция включения необходима, иначе каждый файл просмотра придется писать вручную.header/footer
какое ощущение. . . Итак, нам нужно настроить еще один ejs-html-loader:
// webpack.base.config.js 部分代码
module: {
rules: [
...
{
test: /\.ejs$/,
use: [
{
loader: 'html-loader', // 使用 html-loader 处理图片资源的引用
options: {
attrs: ['img:src', 'img:data-src']
}
},
{
loader: 'ejs-html-loader', // 使用 ejs-html-loader 处理 .ejs 文件的 includes 语法
options: {
production: process.env.ENV === 'production'
}
}
]
}
...
]
}
После обхода первой ямы вторая:
Как написать входную запись?
Я помню старый проект предыдущей компании, более 50 страниц, более 50entry
а такжеnew HTMLwebpackPlugin()
Файл расширяется, чтобы окружить земной шар. . . Чтобы избежать этой трагической ситуации, напишите метод, который возвращает массив записей.
можно использоватьglobЧтобы обработать эти файлы, получите имя файла, конечно, это также можно реализовать с помощью собственного узла. Просто гарантияJavaScript
Имя файла и имя файла представления совпадают, например, имя файла представления домашней страницыhome.ejs
, то соответствующее имя файла сценария должно использовать то же имяhome.js
Название, WebPack найдет запись файла сценариев при упаковке, и генерировать соответствующий файл просмотра через отношения сопоставления:
// webpack.base.config.js 部分代码
const Webpack = require('Webpack')
const glob = require('glob')
const { resolve } = require('path')
// webpack 入口文件
const entry = ((filepathList) => {
let entry = {}
filepathList.forEach(filepath => {
const list = filepath.split(/[\/|\/\/|\\|\\\\]/g) // 斜杠分割文件目录
const key = list[list.length - 1].replace(/\.js/g, '') // 拿到文件的 filename
// 如果是开发环境,才需要引入 hot module
entry[key] = process.env.NODE_ENV === 'development' ? [filepath, 'webpack-hot-middleware/client?reload=true'] : filepath
})
return entry
})(glob.sync(resolve(__dirname, '../src/js/*.js')))
module.exports = {
entry,
...
}
Конфигурация HTMLWebPackPlugin такой же:
// webpack.base.config.js 部分代码
...
plugins: [
// 打包文件
...glob.sync(resolve(__dirname, '../src/tpls/*.ejs')).map((filepath, i) => {
const tempList = filepath.split(/[\/|\/\/|\\|\\\\]/g) // 斜杠分割文件目录
const filename = `views/${tempList[tempList.length - 1]}` // 拿到文件的 filename
const template = filepath // 指定模板地址为对应的 ejs 视图文件路径
const fileChunk = filename.split('.')[0].split(/[\/|\/\/|\\|\\\\]/g).pop() // 获取到对应视图文件的 chunkname
const chunks = ['manifest', 'vendors', fileChunk] // 组装 chunks 数组
return new HtmlWebpackPlugin({ filename, template, chunks }) // 返回 HtmlWebpackPlugin 实例
})
]
...
хорошо написанwebpack.base.config.js
Документы, подготовленные в соответствии с потребностями вашего проектаwebpack.dev.config.js
а такжеwebpack.prod.config.js
,использоватьwebpack-mergeОбъедините базовую конфигурацию с конфигурацией в соответствующей среде.
Вы можете обратиться к некоторым другим подробным настройкам webpack.URL-адрес веб-пакета на китайском языке
Сервер
Скрипт упаковки написан, начинаем писать сервис, пользуемсяexpress
для создания службы.(Поскольку это демонстрация инженерной архитектуры, эта услуга не включает никаких добавлений, удалений, изменений и проверок базы данных, а включает только базовые переходы маршрутизации)
server
Простая структура выглядит следующим образом:
Файл запуска сервера
bin/server.js
webpack-dev-server
package.json
npm run dev
webpack-dev-server
express
+ webpack-dev-middleware
Ниже приведена часть кода файла служебной записи.
// server/bin/server.js 文件代码
const path = require('path')
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const { routerFactory } = require('../routes')
const isDev = process.env.NODE_ENV === 'development'
let app = express()
let webpackConfig = require('../../build/webpack.dev.config')
let compiler = webpack(webpackConfig)
// 开发环境下才需要启用实时编译和热更新
if (isDev) {
// 用 webpack-dev-middleware 启动 webpack 编译
app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
overlay: true,
hot: true
}))
// 使用 webpack-hot-middleware 支持热更新
app.use(webpackHotMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
noInfo: true
}))
}
// 添加静态资源拦截转发
app.use(webpackConfig.output.publicPath, express.static(path.resolve(__dirname, isDev ? '../../src' : '../../dist')))
// 构造路由
routerFactory(app)
// 错误处理
app.use((err, req, res, next) => {
res.status(err.status || 500)
res.send(err.stack || 'Service Error')
})
app.listen(port, () => console.log(`development is listening on port 8888`))
маршрутизация на стороне сервера
Прыжковый метод разводки — очень важный шаг во всем проекте. Не знаю, есть ли сомнения у друзей, прочитавших статью.Локальный файл представления - это файл с суффиксом .ejs, а браузер может распознать только файл с суффиксом .html.Как осуществляется рендеринг данных этого представления ? Все ресурсы, упакованные webpack-dev-middleware, хранятся в памяти Как получить файлы ресурсов, хранящиеся в памяти на сервере?
Давайте сначала посмотрим на конкретный код маршрутизации, здесь для демонстрации используется маршрутизация домашней страницы.// server/routs/home.js 文件
const ejs = require('ejs')
const { getTemplate } = require('../common/utils')
const homeRoute = function (app) {
app.get('/', async (req, res, next) => {
try {
const template = await getTemplate('index.ejs') // 获取 ejs 模板文件
let html = ejs.render(template, { title: '首页' })
res.send(html)
} catch (e) {
next(e)
}
})
app.get('/home', async (req, res, next) => {
try {
const template = await getTemplate('index.ejs') // 获取 ejs 模板文件
let html = ejs.render(template, { title: '首页' })
res.send(html)
} catch (e) {
next(e)
}
})
}
module.exports = homeRoute
Вы можете видеть, что ключевой момент находится в методе getTemplate, давайте посмотрим на этоgetTemplate
что ты сделал
// server/common/utils.js 文件
const axios = require('axios')
const CONFIG = require('../../build/config')
function getTemplate (filename) {
return new Promise((resolve, reject) => {
axios.get(`http://localhost:8888/public/views/${filename}`) // 注意这个 'public' 公共资源前缀非常重要
.then(res => {
resolve(res.data)
})
.catch(reject)
})
}
module.exports = {
getTemplate
}
Как видно из приведенного выше кода, самое важное в маршрутизации — это прямое использование имени файла ejs соответствующего представления для запроса его собственной службы, чтобы получить ресурсы и данные, хранящиеся в кеше веб-пакета.
Получив таким образом строку шаблона, движок ejs отрендерит соответствующую переменную с данными и, наконец, вернет ее в браузер для отрисовки в виде html-строки.
Локальная служба пометит запрос статического ресурса префиксом пути publicPath. Если запрос, полученный службой, имеет префикс publicPath, он будет перехвачен промежуточным программным обеспечением статического ресурса в `/bin/server.js` и сопоставлен с соответствующий каталог ресурсов, вернуть статические ресурсы, и этот publicPath находится в конфигурации веб-пакетаoutput.publicPath
Что касается кэширования веб-пакета при упаковке, я много где искал и не нашел хорошей документации и инструментов для работы.Вот две ссылки для вас, чтобы порекомендовать
- Webpack Custom File Systems(Официальное описание пользовательской файловой системы webpack)
- memory-fs(чтобы получить данные, скомпилированные в память с помощью веб-пакета)
клиент
После завершения рендеринга на стороне сервера, создания и настройки веб-пакета 80% рабочей нагрузки выполнено, и есть некоторые мелкие детали, на которые необходимо обратить внимание, иначе сервис все равно будет сообщать об ошибке при запуске.
Яма при компиляции веб-пакета
Эта яма зарыта в файле представления клиента. Давайте сначала посмотрим, что это за яма: когда мы используем синтаксис ejs (<%= title %>) Когда используется этот синтаксис, компиляция веб-пакета сообщит об ошибке, говоря, что заголовок не определен
Чтобы решить эту проблему, вам нужно сначала понять механизм времени компиляции веб-пакета, что он делает. Мы знаем, что внутренний шаблонный механизм веб-пакета основан на ejs, поэтому перед нашим рендерингом на стороне сервера, то есть стадией компиляции веб-пакета, один раз был выполнен ejs.render, в это время в файле конфигурации веб-пакета у нас нет Переменная title была передана, поэтому компиляция сообщит об ошибке. Итак, как написать, чтобы идентифицировать его? ответОфициальная документация ejs
Как видно из вступления на официальном сайте, когда мы используем<%%В начале он будет экранирован как<%Строка, аналогичная экранированию тегов html, чтобы избежать ошибочной идентификации ejs, поставляемого с веб-пакетом, и генерировать правильные файлы ejs. Итак, взяв в качестве примера переменные, в коде нам нужно написать:<%%= title %>
Таким образом, вебпак может быть успешно скомпилирован, а компилятор будет продолжать передаваться в ejs-html-loader здесь
Используйте html-загрузчик для идентификации ресурсов изображения
если ты понимаешьhtml-loader
Друзья знают, что в проекте причина, почему мы можем легко писать в html<img src="../static/imgs/XXX.png">
Этот формат изображения также может корректно распознаваться вебпаком, который неотделим от html-загрузчика вattrs
элемент конфигурации,
Но в ejs-html-loader такой удобной функции нет, поэтому все равно приходится пользоватьсяhtml-loader
Для обработки ссылок на изображения в html необходимо обратить внимание на порядок настройки загрузчика.
// webpack.base.config.js 部分代码
module: {
rules: [
...
{
test: /\.ejs$/,
use: [
{
loader: 'html-loader', // 使用 html-loader 处理图片资源的引用
options: {
attrs: ['img:src', 'img:data-src']
}
},
{
loader: 'ejs-html-loader', // 使用 ejs-html-loader 处理 .ejs 文件的 includes 语法
options: {
production: process.env.ENV === 'production'
}
}
]
}
...
]
}
Настроить горячее обновление
Следующим шагом является настройка горячего обновления, используйтеwebpack-dev-middleware
способ настройки горячего обновления иwebpack-dev-server
немного отличается, ноwebpack-dev-middleware
Чуть проще. Горячее обновление конфигурации многостраничного приложения Webpack, всего четыре шага:
- существует
entry
Напишите еще один в записиwebpack-hot-middleware/client?reload=true
входной файл
// webpack.base.config.js 部分代码
// webpack 入口文件
const entry = ((filepathList) => {
let entry = {}
filepathList.forEach(filepath => {
...
// 如果是开发环境,才需要引入 hot module
entry[key] = process.env.NODE_ENV === 'development' ? [filepath, 'webpack-hot-middleware/client?reload=true'] : filepath
...
})
return entry
})(...)
module.exports = {
entry,
...
}
- в вебпаке
plugins
Напишите еще три плагина:// webpack.dev.config.js 文件部分代码 plugins: [ ... // OccurrenceOrderPlugin is needed for webpack 1.x only new Webpack.optimize.OccurrenceOrderPlugin(), new Webpack.HotModuleReplacementPlugin(), // Use NoErrorsPlugin for webpack 1.x new Webpack.NoEmitOnErrorsPlugin() ... ]
- существует
bin/server.js
Представлено в служебной записиwebpack-hot-middleware
, и воляwebpack-dev-server
упакованныйcompiler
использоватьwebpack-hot-middleware
Упакуйте это:// server/bin/server.js 文件 let compiler = webpack(webpackConfig) // 用 webpack-dev-middleware 启动 webpack 编译 app.use(webpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath, overlay: true, hot: true })) // 使用 webpack-hot-middleware 支持热更新 app.use(webpackHotMiddleware(compiler, { publicPath: webpackConfig.output.publicPath, reload: true, noInfo: true }))
- Добавьте в файл js фрагмент кода, соответствующий представлению:
// src/js/index.js 文件 if (module.hot) { module.hot.accept() }
Дополнительные сведения о конфигурации webpack-hot-middleware см.Документация
Здесь следует отметить, что:
1. Если так написано, то горячий модуль вебпака может поддерживать только модификацию JS части.Если вам нужна поддержка горячей перезагрузки файлов стилей (css/less/sass...), вы не можете использовать extract-text -webpack-плагин для конвертации стиля Файл зачищен, иначе не будет возможности следить за модификациями и обновляться в реальном времени.
2. Горячий модуль webpack нативно не поддерживает горячую замену html, но у многих разработчиков есть большой спрос на это, поэтому я нашел относительно простой способ поддержки горячего обновления файлов просмотра
// src/js/index.js 文件
import axios from 'axios'
// styles
import 'less/index.less'
const isDev = process.env.NODE_ENV === 'development'
// 在开发环境下,使用 raw-loader 引入 ejs 模板文件,强制 webpack 将其视为需要热更新的一部分 bundle
if (isDev) {
require('raw-loader!../tpls/index.ejs')
}
...
if (module.hot) {
module.hot.accept()
/**
* 监听 hot module 完成事件,重新从服务端获取模板,替换掉原来的 document
* 这种热更新方式需要注意:
* 1. 如果你在元素上之前绑定了事件,那么热更新之后,这些事件可能会失效
* 2. 如果事件在模块卸载之前未销毁,可能会导致内存泄漏
*/
module.hot.dispose(() => {
const href = window.location.href
axios.get(href).then(res => {
const template = res.data
document.body.innerHTML = template
}).catch(e => {
console.error(e)
})
})
}
// webpack.dev.config.js
plugins: [
...
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
})
...
]
// webpack.prod.config.js
plugins: [
...
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
...
]
Хорошо, как хотите, теперь просмотр файлов также поддерживает горячее обновление. 😃😃
webpack-hot-middleware
наследуется по умолчаниюoverlay
, поэтому, когда конфигурация горячего обновления будет завершена,overlay
Функцию сообщения об ошибках также можно использовать в обычном режиме.
Сценарий запуска package.json
Последний взглядpackage.json
Сценарий запуска здесь, здесь нет никаких сложностей, просто перейдите непосредственно к коду
"scripts": {
"clear": "rimraf dist",
"server": "cross-env NODE_ENV=production node ./server/bin/server.js",
"dev": "cross-env NODE_ENV=development nodemon --watch server ./server/bin/server.js",
"build": "npm run clear && cross-env NODE_ENV=production webpack --env production --config ./build/webpack.prod.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
Когда клиентский кодовой код изменяется, WEBPack автоматически поможет нам компилировать и перезапустить, но изменения кода бокового сервера не будут обновлены в режиме реального времени. В настоящее время нам нужно использоватьnodemon
, Настроить после каталога прослушивания, любые изменения кода на стороне сервера смогут бытьnodemon
Мониторинг, сервис перезапускается автоматически, что очень удобно.
Здесь также есть небольшая деталь, на которую стоит обратить внимание, nodemon --watch лучше всего указывать папку сервера мониторинга, ведь ведь для перезапуска службы требуется только модификация кода сервера, иначе вся корневая директория контролируется по умолчанию, а сервис можно перезапустить, написав стиль, чтобы надоедать людям до смерти.
Суммировать
Оглядываясь назад на проект в целом, можно сказать, что есть еще много вещей, которые требуют внимания и заслуживают изучения. Несмотря на то, что я ступил на множество ям, у меня также есть более глубокое понимание некоторых принципов.
Благодаря интерфейсному инструменту строительных лесов мы можем сгенерировать базовую конфигурацию проекта одним щелчком мыши в большинстве проектов, избавляя от многих проблем инженерного строительства, но это удобство не только приносит пользу разработчикам, но и ослабляет фронт- конечные инженеры Возможности инженерной архитектуры. На самом деле всегда есть некоторые бизнес-сценарии, которые не могут быть реализованы с помощью инструментов поддержки.В настоящее время разработчикам необходимо активно искать решения или даже создавать проекты самостоятельно, чтобы получить максимальную гибкость для разработки.
Полный адрес проекта можно посмотреть в моемGitHub, дайте звезду⭐️, если вам понравилось, большое спасибо~😃😃