Разработайте систему блогов с помощью ThinkJS + Vue.js

Node.js задняя часть внешний интерфейс Vue.js ThinkJS
Разработайте систему блогов с помощью ThinkJS + Vue.js

Примечание редактора:ThinkJSБудучи высокопроизводительной веб-платформой корпоративного уровня Node.js, она пользуется все большей любовью пользователей. Сегодня мы пригласилиThinkJSПользователь @lscho classmate делится для нас своим на основеThinkJSОпыт разработки CMS-подобной системы блогов. Давайте посмотрим сейчасThinkJSКакие искры можно стереть с помощью Vue.js!

предисловие

Некоторое время назад в свободное время я переписал блог, в дополнение к реализации базовой системы статей и системы комментариев блога, я также доделал простую систему плагинов. В блоге используется ThinkJS для выполнения серверной функции, Vue.js выполняет фоновую функцию управления разделением интерфейса и сервера, а интерфейсная часть блога учитывает проблему поисковой системы и по-прежнему размещается на стороне сервера для рендеринга. Запишите основные функции и возникшие проблемы здесь.

Функциональный анализ

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

  1. Таблица статей
  2. Форма комментариев
  3. Таблица классификации статей
  4. Таблица меток
  5. Таблица сопоставления статей и категорий (один ко многим)
  6. Таблица сопоставления статей и тегов (многие ко многим)
  7. таблица конфигурации
  8. пользовательская таблица

Всего имеется 8 таблиц, а затем, ссылаясь на дизайн Typecho, в сочетании с функцией ассоциации модели ThinkJS, он упрощается, таблица классификации и таблица меток объединяются, две таблицы сопоставления объединяются, и, наконец, получаются следующие 6 схем оформления стола.

内容表 - content
关系表 - relationship
项目表 - meta
评论表 - comment
配置表 - config
用户表 - user

Функция ассоциации модели ThinkJS может легко обрабатывать отношения классификации и метки этой структуры таблицы.src/model/content.jsЗаписав следующую связь, можно найти данные классификации и метки при запросе статей с использованием модели без необходимости выполнять несколько запросов вручную.

get relation() {
    return {
        category: {
            type: think.Model.BELONG_TO,
            model: 'meta',
            key: 'category_id',
            fKey: 'id',
            field: 'id,name,slug,description,count'
        },
        tag: {
            type: think.Model.MANY_TO_MANY,
            model: 'meta',
            rModel: 'relationship',
            rfKey: 'meta_id',
            key: 'id',
            fKey: 'content_id',
            field: 'id,name,slug,description,count'
        }
    };
}

Аутентификация интерфейса

После того, как структура таблицы разработана, осталось приступить к разработке интерфейса. С точки зрения интерфейсов, поскольку используется спецификация интерфейса RESTful, это в основном функция CURD, и конкретных таблиц не так много.Здесь мы в основном говорим о том, как проверить разрешения всех интерфейсов.

Поскольку серверная часть отделена от клиентской и серверной части, часть проверки подлинности использует проверку подлинности JWT. О JWT я, наверное, уже слышал, и подобные функции я уже реализовывал, искал и нашел.node-jsonwebtokenЭтот пакет очень прост в использовании, в основном две функции шифрования и дешифрования успешно выполняются после некоторого бросания.

Случайно зашел на склад ThinkJS посмотреть на него и нашелthink-session-jwtЭтот плагин также основан на node-jsonwebtoken. Это лучше использовать.После настройки вы можете напрямую использовать метод ctx.session ThinkJS для генерации и проверки. При настройке нужно обратить внимание на параметр tokenType, определяющий способ получения токена, здесь я использую заголовок, а это значит, что токен будет найден из заголовка каждого запроса позже, а значением ключа является настроенное tokenName.

Аутентификация внутренней авторизации

Поскольку интерфейс API соответствует стилю RESTful и не имеет сложной концепции разрешений роли, поэтому простые запросы, не являющиеся типами GET, проверяют, действителен ли токен, а контроллер ThinkJS обеспечивает предварительные операции.__before. существуетsrc/controller/rest.jsДелайте логические выводы в процессе, и только те, кто пройдет, продолжат казнить.

async __before() {
    this.userInfo = await this.session('userInfo').catch(_ => ({}));
    
    const isAllowedMethod = this.isMethod('GET');
    const isAllowedResource = this.resource === 'token';
    const isLogin = !think.isEmpty(this.userInfo);
    
    if(!isAllowedMethod && !isAllowedResource && !isLogin) {
        return this.ctx.throw(401, '请登录后操作');
    }
}

Здесь есть проблема, то есть, когда токен неправильный, node-jsonwebtoken выдаст исключение, поэтому здесь мы используемtry catchЗахватите и разберитесь с этим.

Обнаружение сбоя удостоверения переднего плана

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

  1. Токен не существует, с чем легко обращаться.Во время предварительной операции маршрута напрямую судят, существует ли он.Если он существует, то он будет освобожден.Если он не существует, он обратится к интерфейсу входа .
beforeEnter:(to, from, next)=>{
    if(!localStorage.getItem('token')){
        next({ path: '/login' });
    }else{
        next();
    }
}

2. Токен неправильный. Для этого требуется внутреннее обнаружение, чтобы узнать, действителен ли токен. Здесь сервер вернет код состояния 401 после того, как обнаружение не удастся идентифицировать для идентификации внешнего интерфейса. Мы можем судить по перехватчику ответа на запрос axios, потому что код состояния 4XX вызовет исключение, поэтому код выглядит следующим образом

axios.interceptors.response.use(data => {
    //这里可以对成功的请求进行各种处理
    return data;
},error=>{
    if (error.response) {
        switch (error.response.status) {
            case 401:
                store.commit("clearToken");
                router.replace("/login");
            break;
        }
    }
    return Promise.reject(error.response.data)
})

3. Срок действия токена истекает. Эту ситуацию также можно игнорировать, потому что мы уже рассудили в ответном перехватчике axios, если код состояния возврата равен 401, он также перейдет на страницу входа. Однако при реальном использовании я обнаружил, что опыт не очень хорош, потому что токен в клиенте хранится в localStorage и не будет автоматически очищаться, поэтому, если мы сразу откроем фон после истечения срока действия токена, интерфейс отобразит background, а затем запрос возвращает 401 , страница переходит к интерфейсу входа. Это явление действительно существует в облачной консоли Alibaba Cloud, облачной консоли Qiniu и т. д., которые используют аналогичные методы аутентификации, что может быть немного неудобно для обсессивно-компульсивного расстройства. Эту ситуацию тоже можно решить.

Давайте сначала взглянем на соответствующие знания о JWT. JWT состоит из трех частей, разделенных .: заголовок заголовка, полезная нагрузка и подпись подписи. Его структура выглядит следующим образом: Заголовок.Полезная нагрузка.Подпись. Помимо заголовка и подписи, я не буду его представлять.Полезная нагрузка на самом деле представляет собой часть данных открытого текста, полученных после транскодирования base64.. И он содержит установленную нами информацию, которая обычно имеет срок годности. Судя по предварительной операции маршрутизации, вы можете узнать, истек ли срок действия маркера, чтобы избежать проблемы двух переходов на страницу. После декодирования полезной нагрузки получаем:

{"userInfo":{"id":1},"iat":1534065923,"exp":1534109123}

Вы можете видеть, что exp — это время истечения срока действия, и, оценивая это время, вы можете узнать, истек ли он.

let tokenArray = token.split('.')
if (tokenArray.length !== 3) {
    next('/login')
}
let payload = Base64.decode(tokenArray[1])
if (Date.now() > payload.exp * 1000) {
    next('/login')
}

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

Механизм плагина

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

let insertId = await model.add(data);
if(insertId){
    await this.updateCache();
    await this.push();
    ...
}

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

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

let insertId = await model.add(data);
if(insertId){
    await this.hook('commentCreate',data);
}

Поскольку программа предназначена для личного использования, вам удобно только расширять функции в будущем, и вам нужно только реализовать основные функции. Таким образом, вместо того, чтобы добавлять каталог в качестве каталога плагинов, он помещается вsrc/service/Ниже он соответствует файловой структуре ThinkJS, а затем делает соглашение. покаsrc/service/js файл ниже и имеетregisterHookметод, то его можно вызвать как плагин. какsrc/service/email.jsЭтот файл используется для обработки уведомлений по электронной почте, поэтому добавьте в него метод:

static registerHook() {
    return {
        'comment': ['commentCreate']
    };
}

значит вcommentCreateКогда эта функция нажата, она вызоветsrc/service/email.jsизcommentметод.

тогда мырасширятьнемногоcontroller,добавить однуhookметод, который используется для вызова соответствующего плагина по разным идентификаторам. мы можем пересечьsrc/service/Найдите соответствующий файл и вызовите его метод. Однако, учитывая возможные исключения и потерю производительности при обходе файлов, я переместил эту часть функции для обнаружения плагина при запуске службы и сохранения его в конфигурации. Взгляните на ThinkJSзапустить процесс, можно поставить вsrc/bootstrap/worker.jsв этом файле. Примерный код выглядит следующим образом.

const hooks = [];

for (const Service of Object.values(think.app.services)) {
  const isHookService = think.isFunction(Service.registerHook);
  if (!isHookService) {
    continue;
  }

  const service = new Service();
  const serviceHooks = Service.registerHook();
  for (const hookFuncName in serviceHooks) {
    if (!think.isFunction(service[hookFuncName])) {
      continue;
    }
    
    let funcForHooks = serviceHooks[hookFuncName];
    if (think.isString(funcForHooks)) {
      funcForHooks = [funcForHooks];
    }
    
    if (!think.isArray(funcForHooks)) {
      continue;
    }
    
    for (const hookName of funcForHooks) {
      if (!hooks[hookName]) {
          hooks[hookName] = [];
      }
    
      hooks[hookName].push({ service, method: hookFuncName });
    }
  }
}
think.config('hooks', hooks);

затем вsrc/extend/controller.jsсерединаhookПереберите список плагинов и выполните их последовательно.

//src/extend/controller.js
module.exports = {
    async hook(...args) {
        const { hooks } = think.config();
        const hookFuncs = hooks[name];
        if (!think.isArray(hookFuncs)) {
            return;
        }
        for(const {service, method} of hookFuncs) {
            await service[method](...args);
        };
    }
}

На этом простая функция плагина завершена.

Конечно, если вы хотите реализовать полную функцию плагина, такую ​​​​как Wordpress и Typecho, это также очень просто. Добавьте плагин управления в фоновом режиме, вы можете загрузить, а затем добавить функцию активации и функцию отключения к плагину. Нажмите активацию и деактивацию в управлении плагинами, чтобы вызвать эти два метода соответственно, вы можете сохранить конфигурацию по умолчанию и так далее. Если плагину необходимо создать таблицу данных, вы можете выполнить соответствующий оператор sql в функции активации. После завершения активации перезапустите процесс, чтобы код вступил в силу. Функция перезапуска может относиться кКак дочерний процесс уведомляет основной процесс о перезапуске службы?

разное

Есть более-менее некоторые проблемы в процессе разработки проекта.Здесь я также делюсь некоторыми проблемами, с которыми столкнулся, в надежде помочь всем.

Редактор и загрузка файлов

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

интерфейсный код

<mavon-editor ref=md @imgAdd="imgAdd" class="editor" v-model="formItem.content"></mavon-editor>
imgAdd(pos, $file){
   var formdata = new FormData();
   formdata.append('image', $file); 
   image.upload(formdata).then(res=>{
        if(res.errno==0&&res.data.url){
            this.$refs.md.$img2Url(pos, res.data.url);
        }
   });               
}

Бэкенд-обработка

const file = this.file('image');
const extname=path.extname(file.name);
const filename = path.basename(file.path);
const basename=think.md5(filename)+extname;
const savepath = '/upload/'+basename;
const filepath = path.join(think.ROOT_PATH, "www"+savepath);
think.mkdir(path.dirname(filepath));
await rename(file.path, filepath);

Первоначально я использовал код примера загрузки на официальном сайте ThinkJS и использовал переименование для передачи файлов.В Windows временный каталог может не находиться под той же буквой диска, что и каталог проекта.Если он будет перемещен, будет выдано исключение :Error: EXDEV, cross-device link not permitted, нет разрешения на перемещение, в это время вы можете только сначала прочитать файл, а затем записать файл. Так вот еще иtry catchчтобы поймать исключение, главным образом потому, что ThinkJS сначала поместит загруженный файл во временный каталог. По поводу проблемы междискового переименования, вGitHub.com/node будет /node…Я нашел причину, общая идея заключается в том, что операционная система ограничивает переименование только переименованием адреса ссылки на путь и не перемещает туда данные.Переименование не может выполняться в файловых системах, поэтому, если вы работаете в файловых системах, вам нужно скопируйте, а затем удалите старые данные.

После общения в группе @Art Boss упомянул, что загрузкаpayloadОбработанный этим промежуточным ПО, он можетpayloadЭтот параметр промежуточного ПО указывает, что временный каталог является каталогом проекта, чтобы временный каталог и каталог проекта находились под одной и той же буквой диска.

{
	handle: 'payload',
	options: {
		uploadDir: path.join(think.ROOT_PATH, 'runtime/data')
	}
}

Таким образом, вы можете напрямую использовать переименование для работы.

iView загружает по запросу

Поскольку iView по умолчанию загружается как подключаемый модуль, упакованный файл имеет очень большой размер. Необходимо настроить для загрузки по требованию. в соответствии сУууу. Я просматриваю UI.com/docs/expensive/…После его получения возникает проблема, то есть выполнитьnpm run buildсообщит об ошибке.ERROR in js/index.c26f6242.js? from UglifyJsВыглядит это так, я взглянул на причину ошибки, вероятно, потому, что после загрузки по требованию непосредственно загружается файл js src под модулем iView, который использует синтаксис ES6, вызывая сжатие потерпеть неудачу. Перейти к поиску проблем и найти решениеGitHub.com/я смотрю/я смотрю…

развертывать

Если внешний и внутренний интерфейсы не разделены, используйте веб-пакет для компиляции страницы входа внешнего интерфейса index.html в расположение шаблона домашней страницы внутреннего проекта ThinkJS, а затем скомпилируйте ресурсы во внутренний проект. папка ресурсов и укажите соответствующий путь. Таким образом, фронтенд-проект интегрируется в бэкенд-проект, а затем согласно ThinkJSМетод развертыванияТакже возможно развертывание.

Если front-end и back-end разделены и развернуты как два проекта, то front-end маршрутизация также легко обрабатывается, если используется обычный режим.Если используется режим истории, запрос должен быть перенаправлен в индекс. html страница входа для обработки, которая похожа на единственную запись некоторых фреймворков mvc. В настоящее время именно интерфейсный проект берет на себя маршрутизацию.

location / {
	try_files $uri $uri/ /index.html;
}

Затем мы должны иметь дело с внутренней частью запроса.Если это не одно и то же доменное имя, мы должны решить междоменную проблему. Здесь передняя и задняя части используют одно и то же доменное имя, и вы можете использовать обратный прокси-сервер для запросов API. Обратите внимание, что эта часть должна быть написана над переадресацией запроса.

set $node_port 8360;
	location ~ ^/api/ {
    proxy_pass http://127.0.0.1:$node_port$request_uri;

}

Серверная часть может использовать демон pm2.

постскриптум

Выше приведено краткое изложение процесса разработки всего моего проекта и некоторых возникших проблем.Если у вас есть какие-либо вопросы, пожалуйста, оставьте сообщение для обсуждения. Наконец-то приветствую всех, звездаСистема блогов на базе ThinkJS + Vue.