Серия боевых узлов Node: помогите мистеру Хуану улучшить проект «Голодны ли вы?»

Node.js

Лучший способ изучить технологию — это что-то с ней сделать.

Когда я изучал Node, я почувствовал, что могу бороться и сопротивляться после прочтения, и на следующий день я вернулся к тому мальчику, которым был раньше. Жаль, что это был не Чжан Уцзи.После прочтения фехтования Тайцзи он забыл повесить И Тяньцзяня. Если вы забудете его после прочтения, то вы его забудете. Поэтому я решил сделать проект, чтобы закрепить свои знания.

Сначала взгляните на следующую часть карты эффектов.













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

Пользователи могут добавлять, удалять, изменять и проверять магазин и продукты питания, регистрируясь в качестве фонового администратора, и соответствующий магазин и продукты будут отображаться в интерфейсе. Весь бэкенд-проект начинается сeggдля рамы,mysqlв качестве базы данных сtypescriptразвивать,Он включает одиннадцать таблиц базы данных и около сорока интерфейсов.. На внутренних и внешних страницах используются обычныеvue+element-ui+vuex+vue-routerразвивать. Что касается развертывания, поскольку это личный проект, я решил использовать технологию, которую раньше не использовал, и построил Jenkins.Автоматически извлекайте и выполняйте сценарии через jenkins для создания образов Docker для автоматизации развертывания проектов vue.. Весь процесс достаточно завершен для личного проекта.

Интернет-адрес:

внешний адрес

внутренний адрес

Ссылка на проект:

Вы голодны?

Система управления фоном на основе vue + element-ui

Примечание. Для системы управления фоном я имею в виду только здесьСистема управления фоном на основе vue + element-uiБизнес-логика кода глубоко не изучается, потому что используемый стек технологий не тот. Поскольку я впервые использую Node.js для выполнения проекта, я обычно не использую Node.js в компании, и я ссылался на несколько разрозненных статей, но у новичков определенно будет некрасивое отношение к тому, чтобы делать вещи таким же образом.Если вы делаете что-то неразумное, пожалуйста, исправьте это, Самое большое преимущество программистов в том, что они знают свои ошибки и исправляют их, а я не более того.

За кулисами

Используемая технология

  • Node.js
  • Egg
  • MySql
  • Redis
  • TypeScript

реализовать функцию

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

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

Инкапсуляция общих функций

  • инкапсуляция ответа на запрос
   /*
 * @Descripttion: controller基类
 * @version: 
 * @Author: 笑佛弥勒
 * @Date: 2019-08-06 16:46:01
 * @LastEditors: 笑佛弥勒
 * @LastEditTime: 2020-03-09 10:43:37
 */
import { Controller } from "egg"
export class BaseController extends Controller {

  /**
   * @Descripttion: 请求成功
   * @Author: 笑佛弥勒
   * @param {status} 状态
   * @param {data} 响应数据
   * @return:
   */
  
  success(status: number, message: string, data?: any) {
    if (data) {
      this.ctx.body = {
        status: status,
        message: message,
        data: data
      }
    } else {
      this.ctx.body = {
        status: status,
        message: message
      }
    }
  }

  /**
   * @Descripttion: 失败
   * @Author: 笑佛弥勒
   * @param {status} 状态
   * @param {data} 错误提示
   * @return:
   */
  fail(status: number, message: string) {
    this.ctx.body = {
      status: status || 500,
      message: message,
    };
  }
  • перечисляемый класс

/*
 * @Descripttion: 枚举类
 * @version: 1.0
 * @Author: 笑佛弥勒
 * @Date: 2020-03-14 10:07:36
 * @LastEditors: 笑佛弥勒
 * @LastEditTime: 2020-03-28 23:02:47
 */
export enum Status {
  Success = 200, // 成功
  SystemError = 500, // 系统错误
  InvalidParams = 1001, // 参数错误
  LoginOut = 1003, // 未登录
  LoginFail = 1004, // 登录失效
  CodeError = 1005, // 验证码错误
  InvalidRequest = 1006, // 无效请求
  TokenError = 1007 // token失效
}

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

  • Инкапсуляция общего кода

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

/**
 * @Descripttion: 生成范围内随机数,[lower, upper)
 * @Author: 笑佛弥勒
 * @param {lower} 最小值
 * @param {upper} 最大值
 * @return:
 */
export function random(lower, upper) {
  return Math.floor(Math.random() * (upper - lower)) + lower;
}

В процессе запроса его можно вызвать с помощью метода, предоставляемого яйцом.

mon_sale: this.ctx.helper.random(1000, 20000)

  • Проверка параметров внешнего запроса

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

 /**
     * @Descripttion: 插件加载完成后加入校验规则
     * @Author: 笑佛弥勒
     * @param {type} 
     * @return: 
     */
    public async willReady() {
        const directory = path.join(this.app.config.baseDir, 'app/validate');
        this.app.loader.loadToApp(directory, 'validate');
    }

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

public async createMerchants() {
    let params = this.ctx.request.body
    console.log(params)
    try {
      this.ctx.validate({ params: "addMerchants" }, { params: params })
    } catch (error) {
      this.fail(Status.InvalidParams, error)
      return
    }
    try {
      await this.ctx.service.merchants.createMerchants(params)
      this.success(Status.Success, '创建商户成功')
    } catch (error) {
      this.ctx.logger.error(`-----创建商户错误------`, error)
      this.ctx.logger.error(`入参params:${params}`)
      this.fail(Status.SystemError, error)
    }

  }

Реализация функции

  • Функция регистрации входа

Функция входа и регистрации - очень распространенная функция, и логика реализации аналогична. Сначала получаем учетную запись пользователя, проверяем, есть ли эта запись в базе данных, а затем сравниваем, верен ли пароль. Если нет, выполняем новую операцию и зашифровать пароль пользователя. Для сгенерированного файла cookie для входа зашифрованная строка создается подключаемым модулем egg-jwt, а затем зашифрованная строка сохраняется через Redis.Когда пользователь запрашивает интерфейс, который необходимо войти в систему, фон удалит файл cookie. в яйце и в редисе.Сделайте сравнение, сделайте проверку статуса входа, здесь другой момент,В яйце куки в миллисекундах, я не смотрел внимательно, и я не мог найти ошибку во время разработки.Я раздавил несколько мышей.Конкретная логика реализации следующая.

public async login() {
    const { ctx } = this
    let { mobile, password } = this.ctx.request.body
    try {
      ctx.validate({ mobile: "mobile" })
      ctx.validate({ password: { type: "string", min: 1, max: 10 } })
    } catch (error) {
      this.fail(Status.InvalidParams, error)
      return
    }

    let res = await ctx.service.admin.hasUser(mobile)
    // 加密密码
    password = utility.md5(password)
    let token = ''
    if (!res) {
      try {
        await ctx.service.admin.createUser(mobile, password)
        // 生成token
        await this.ctx.helper.loginToken({ mobile: mobile, password: password }).then((res) => token = res) // 取到生成token
        await this.app.redis.set(mobile, token, 'ex', 7200) // 保存到redis
        ctx.cookies.set('authorization', token, {
          httpOnly: true, // 默认就是 true
          maxAge: 1000 * 60 * 60, // egg中是以毫秒为单位的
          domain: this.app.config.env == 'prod' ? '120.79.131.113' : 'localhost'
        }) // 保存到cookie
        this.success(Status.Success, '注册成功')
      } catch (error) {
        ctx.logger.error(`-----用户注册失败------`, error)
        ctx.logger.error(`入参params:mobile:${mobile}、password:${password}`)
        this.fail(Status.SystemError, "用户注册失败")
      }
    } else {
      if (res.password == password) {
        await this.ctx.helper.loginToken({ mobile: mobile, password: password }).then((res) => token = res) // 取到生成token
        await this.app.redis.set(mobile, token, 'ex', 7200) // 保存到redis
        ctx.cookies.set('authorization', token, {
          httpOnly: true, // 默认就是 true
          maxAge: 1000 * 60 * 60, // egg中是以毫秒为单位的
          domain: this.app.config.env == 'prod' ? '120.79.131.113' : 'localhost'
        }) // 保存到cookie
        ctx.body = { data: { token, expires: this.config.login_token_time }, code: 1, msg: '登录成功' } // 返回
        this.success(Status.Success, '登录成功')
      } else {
        this.fail(Status.SystemError, "密码错误")
      }
    }
  }

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

  1. session+cookie
  2. жетон жетон

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


  • промежуточное ПО входа

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

/* * @Descripttion: 登陆验证 * @version: 1.0 * @Author: 笑佛弥勒 * @Date: 2019-12-31 23:59:22 * @LastEditors: 笑佛弥勒 * @LastEditTime: 2020-03-28 23:06:09 */module.exports = (options, app) => {  return async function userInterceptor(ctx, next) {    let authToken = ctx.cookies.get('authorization') // 获取header里的authorization    if (authToken) {      const res = ctx.helper.verifyToken(authToken) // 解密获取的Token      if (res) {        // 此处使用redis进行保存        let redis_token = ''        res.email ? redis_token = await app.redis.get(res.email) : redis_token = await app.redis.get(res.mobile) // 获取保存的token        if (authToken === redis_token) {          res.email ? app.redis.expire(res.email, 7200) : app.redis.expire(res.mobile, 7200) // 重置redis过期时间          await next()        } else {          ctx.body = { status: 1004, message: '登录态失效' }        }      } else {        ctx.body = { status: 1004, message: '登录态失效' }      }    } else {      ctx.body = { status: 1003, message: '请登陆后再进行操作' }    }  }}

Затем вы можете использовать его в маршруте, который должен войти в систему

export function admin(app) {
    const { router, controller } = app
    const jwt = app.middleware.jwt({}, app)
    
    router.post('/api/admin/login', controller.admin.login)
    router.post('/api/admin/logOut', jwt, controller.admin.logOut)
    router.post('/api/admin/updateAvatar', jwt, controller.admin.updateAvatar)
    router.post('/api/admin/getAdminCount', jwt, controller.admin.getAdminCount)
    router.get('/api/admin/findAdminByPage', jwt, controller.admin.findAdminByPage)
    router.get('/api/admin/totalData', jwt, controller.admin.totalData)
    router.get('/api/admin/getShopCategory', jwt, controller.admin.getShopCategory)
    router.get('/api/admin/getCurrentAdmin', jwt, controller.admin.getCurrentAdmin)
    router.get('/api/admin/isLogin', controller.admin.isLogin)
}
  • Сбор и классификация городов по всей стране

При выборе города на фронтенде необходимо разделить город по его инициалам.


С точки зрения реализации, первым шагом является получение всех городов страны через API, предоставленный AutoNavi, а затем извлечение и классификация первых букв городов в соответствии со сторонней библиотекой пиньинь. запросы, ip моего сервера блокируется AutoNavi, заблокируйте его, сохраните результат в redis, и redis больше не запрашивает данные.

/**
 * @Descripttion: 获取全国所有城市
 * @Author: 笑佛弥勒
 * @param {type}
 * @return:
 */
export async function getAllCity() {
  let url = `https://restapi.amap.com/v3/config/district?keywords=&subdistrict=2&key=44b1b802a3d72663f2cb9c3288e5311e`;
  var options = {
    method: "get",
    url: url,
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json" // 需指定这个参数 否则 在特定的环境下 会引起406错误
    }
  };
  return await new Promise((resolve, reject) => {
    request(options, function(err, res, body) {
      if (err) {
        reject(err);
      } else {
        body = JSON.parse(body);
        if (body.status == 0) {
          reject(err);
        } else {
          let cityList: Array<Object> = [];
          getAllCityList(cityList, body.districts);
          cityList = orderByPinYin(cityList);
          resolve(cityList);
        }
      }
    });
  });
}
// 给全国城市根据拼音分组
function orderByPinYin(cityList) {
  const newCityList: Array<Object> = [];
  const title = [
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "J",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z"
  ];
  for (let i = 0; i < title.length; i++) {
    let items: Array<Object> = [];
    newCityList.push({
      name: title[i],
      items: []
    });
    for (let j = 0; j < cityList.length; j++) {
      let indexLetter = pinyin(cityList[j].name.substring(0, 1), {
        style: pinyin.STYLE_FIRST_LETTER // 设置拼音风格
      })[0][0].toUpperCase(); // 提取首字母
      if (indexLetter === title[i]) {
        items.push(cityList[j]);
      }
    }
    newCityList[i]["items"] = items;
  }
  return newCityList;
}
// 递归获取全部城市列表
function getAllCityList(cityList: Array<Object>, parent: any) {
  let exception: Array<string> = ["010", "021", "022", "023"]; // 四个直辖市另外处理
  for (let i = 0; i < parent.length; i++) {
    if (parent[i].level === "province") {
      if (exception.includes(parent[i].citycode)) {
        parent[i].districts = [];
        parent[i].level = "city";
        cityList.push(parent[i]);
      } else {
        cityList.push(...parent[i].districts);
      }
    } else {
      getAllCityList(cityList, parent[i].districts);
    }
  }
}

Есть и некоторые функции.Если вам интересно, вы можете клонировать проект и посмотреть сами.

внешний интерфейс

Фоновая система управления представляет собой обычный vue+element-ui, который является относительно распространенным.Я не буду здесь вдаваться в подробности, а в основном расскажу о разрабатываемом мышлении и проблемах, с которыми сталкивается пользователь.

Используемая технология

  • vue
  • vuex
  • vue-router
  • cube-ui
  • Axios
  • ....

реализовать функцию

  • Зарегистрировать функцию входа
  • Добавление, удаление, изменение и поиск адреса пользователя
  • Отображение списка компаний
  • Отображение страницы сведений о продавце
  • список продуктов
  • Страница сведений о еде
  • Поиск продавца
  • ....

  • мобильная раскладка

В проекте используется amfe-flexible+px2rem-loader для адаптации к мобильному терминалу.

добавить в package.json

"plugins": {
      "autoprefixer": {},
      "postcss-px2rem": {
        "remUnit": 37.5
      }
    }


  • axios выполняет унифицированный запрос и перехват

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

// 添加响应拦截器
AJAX.interceptors.response.use(
  function(response) {
    const loginError = [10003, 10004]
    if (loginError.includes(response.data.status)) {
      router.push({
        path: '/vue/login/index.html',
        query: { redirect: location.href.split('/vue')[1] }
      })
    } else if (response.data.status != 200) {
      Toast.$create({
        time: 2000,
        type: 'txt',
        txt: response.data.message
      }).show()
    } else {
      return response.data
    }
  },
  function(error) {
    // 对响应错误做点什么,比如400、401、402等等
    if (error && error.response) {
      console.log(error.response)
    }
    return Promise.reject(error)
  }
)

  • Интегрированный API карты Gaode


Такие поиски адресов — это все данные, возвращаемые вызовом API карты Gaode, который инкапсулирован здесь с помощью миксинов.

/*
 * @Descripttion: 高德地图mixins
 * @version: 1.0
 * @Author: 笑佛弥勒
 * @Date: 2020-01-20 20:41:57
 * @LastEditors: 笑佛弥勒
 * @LastEditTime: 2020-03-07 21:04:19
 */
import { mapGetters } from 'vuex'
// 高德地图定位
export const AMapService = {
  data() {
    return {
      mapObj: '',
      positionFinallyFlag: false,
      currentPosition: '正在定位...', // 当前地址
      locationFlag: false, // 定位结果
      longitude: '', // 经度
      latitude: '', // 纬度
      searchRes: [] // 搜索结果
    }
  },
  computed: {
    // 当前城市
    currentCity() {
      return this.getCurrentCity()
    }
  },
  methods: {
    ...mapGetters('address', ['getCurrentCity']),
    initAMap() {
      this.mapObj = new AMap.Map('iCenter')
    },
    // 定位
    geoLocation() {
      const that = this
      this.initAMap()
      this.mapObj.plugin('AMap.Geolocation', function() {
        const geolocation = new AMap.Geolocation({
          enableHighAccuracy: true, // 是否使用高精度定位,默认:true
          timeout: 5000, // 超过5秒后停止定位,默认:无穷大
          noIpLocate: 0
        })
        geolocation.getCurrentPosition((status, result) => {
          if (status === 'complete') {
            that.longitude = result.position.lng
            that.latitude = result.position.lat
            that.currentPosition = result.formattedAddress
            that.locationFlag = true
          } else {
            that.locationFlag = false
            that.currentPosition = '定位失败'
            const toast = that.$createToast({
              time: 2000,
              type: 'txt',
              txt: '定位失败'
            })
            toast.show()
          }
          that.positionFinallyFlag = true
        })
      })
    },
    // 高德地图搜索服务
    searchPosition(keyword) {
      const that = this
      AMap.plugin('AMap.Autocomplete', function() {
        // 实例化Autocomplete
        var autoOptions = {
          // city 限定城市,默认全国
          city: that.currentCity || '全国',
          citylimit: false
        }
        var autoComplete = new AMap.Autocomplete(autoOptions)
        autoComplete.search(keyword, function(status, result) {
          // 搜索成功时,result即是对应的匹配数据
          if (status === 'complete' && result.info === 'OK') {
            that.$nextTick(() => {
              that.searchRes = []
              that.searchRes = result.tips
            })
          }
        })
      })
    }
  }
}

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

filters: {
    format(text, stress, keyword) {
      if (stress) {
        const reg = new RegExp(keyword, 'ig')
        return text.replace(reg, item => {
          return `<span style="color:#666">${item}</span>`
        })
      } else {
        return text
      }
    }
  },

  • Единое управление API, маршрутизатором и vuex

Здесь я следую методу управления проектом нашей компании, разделяю маршрутизацию интерфейса и данные vuex через функции, а затем выставляю их через index.js.


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

router.beforeEach(async(to, from, next) => {
  // 做些什么,通常权限控制就在这里做哦
  // 必须写next()哦,不然你的页面就会白白的,而且不报错,俗称"代码下毒"
  if (to.meta.needLogin) {
    const res = await api.isLogin()
    if (!res.data) {
      router.push({
        path: '/vue/login/index.html',
        query: { redirect: to.path.split('/vue')[1] }
      })
    }
    store.commit('common/SETUSERINFO', res.data || {})
  }
  next()
})

  • Управление иконками

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

@font-face {
  font-family: 'iconfont';  /* project id 1489393 */
  src: url('//at.alicdn.com/t/font_1489393_8te3wqguyau.eot');
  src: url('//at.alicdn.com/t/font_1489393_8te3wqguyau.eot?#iefix') format('embedded-opentype'),
  url('//at.alicdn.com/t/font_1489393_8te3wqguyau.woff2') format('woff2'),
  url('//at.alicdn.com/t/font_1489393_8te3wqguyau.woff') format('woff'),
  url('//at.alicdn.com/t/font_1489393_8te3wqguyau.ttf') format('truetype'),
  url('//at.alicdn.com/t/font_1489393_8te3wqguyau.svg#iconfont') format('svg');
}
.iconfont{
  font-family:"iconfont" !important;
  font-size:16px;font-style:normal;
  -webkit-font-smoothing: antialiased;
  -webkit-text-stroke-width: 0.2px;
  -moz-osx-font-smoothing: grayscale;
}

  • Потяните вниз, чтобы обновить пакет

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

/*
 * @Descripttion: 加载更多Mixins
 * @version: 1.0
 * @Author: 笑佛弥勒
 * @Date: 2020-01-26 15:39:12
 * @LastEditors  : 笑佛弥勒
 * @LastEditTime : 2020-02-10 23:15:57
 */
export default {
  data() {
    return {
      page: 1,
      pageSize: 20,
      requireFinallyFlag: true, // 当次请求是否完成
      totalPage: 1,
      allLoaded: false // 数据是否全部加载完成
    }
  },
  mounted() {
    document.addEventListener('scroll', this.handleScroll)
  },
  destroyed() {
    document.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    handleScroll() {
      const windowHeight = document.documentElement.clientHeight
      const scrollTop = document.documentElement.scrollTop
      const bodyHeight = document.body.scrollHeight
      const totalHeight = parseFloat(windowHeight + scrollTop, 10)
      // 考虑不同浏览器的交互,可能顶部条隐藏之类的,导致页面高度变高
      const browserOffset = 60
      if (bodyHeight < totalHeight + browserOffset && this.page <= this.totalPage && this.requireFinallyFlag) {
        this.page++
        if (this.page > this.totalPage) {
          this.allLoaded = true
        } else {
          this.requireFinallyFlag = false
          this.loadingMore()
        }
      }
    }
  }
}

  • Переключение между страницами A, B, C, проблема с сохранением данных

Возьмите страницу B в качестве промежуточной страницы, страницы A->B, B должны быть совершенно новыми страницами, страницы B->C->B, B должны сохранить предыдущее содержимое, этот проект является примером добавления адреса, введите новый адрес в первый раз Требуется совершенно новая страница, перейдите на страницу поиска адреса в процессе выбора адреса, а после возврата добавьте новую страницу, чтобы сохранить ранее заполненную информацию. Перед этим требованием я сначала сохраняю выравнивание страницы B, а затем оцениваю имя следующего маршрута, чтобы увидеть, нужно ли сбрасывать параметры. Конечно, это все еще относительно мало. Вот еще одна идея.include, будет кешироваться только компонент, имя которого совпадает. Мы можем динамически удалить эту переменную через vuex, чтобы добиться желаемого эффекта. Если следующей страницей является страница выбора адреса, кешируем компонент, в противном случае удаляем кеш компонента.

beforeRouteLeave(to, from, next) {
    console.log('--------------beforeRouteLeave----------')
    if (to.name == 'searchAddress') {
      this.ADDCACHE('AddAddress')
    } else {
      this.DELCACHE('AddAddress')
    }
    next()
  },

Развертывание проекта

Готов к работе:

  • Подать заявку на доменное имя
  • купить сервер
  • Установите необходимое программное обеспечение (git, node, mysql, nginx, docker...)
  • Будьте готовы шагнуть в яму...


  • Я купил и доменное имя, и сервер на Alibaba Cloud.Более хлопотно то, что доменное имя нужно сделать резервной копией.Это займет некоторое время.Я не планировал покупать доменное имя,но будет проблема ., система фонового управления и внешний интерфейс имеют один и тот же IP-адрес, поэтому файлы cookie будут связаны друг с другом., и, наконец, заставили купить доменное имя.
  • Конфигурация доменного имени, для этого требуется настройка IP-адреса вашего сервера и вашего доменного имени в фоновом режиме Alibaba Cloud.Далее идет настройка nginx.Есть два момента.Первый – указать доменное имя на адрес вашего сервера при доступе к доменному имени , а второй — для прямого доступа к доменному имени. Вам нужно изменить доменное имя на адрес вашей домашней страницы.

        server{
                listen 80;
                server_name www.smileele.net;
                rewrite ^/$ http://$host/vue/main/index.html$1 break;
                location / {
                        proxy_pass   http://120.79.131.113:9529/;
                }
        }

Так как это http, то он слушает порт 80. При посещении www.smileele.net он меняется на www.smileele.net/vue/main/index.html, а www.smileele.net соответствует ip

  • Чтобы написать файл Dockerfile, я только контейнеризирую проект vue с помощью docker, поэтому единственное программное обеспечение, которое необходимо загрузить в контейнер docker, — это node и nginx.Содержимое файла выглядит следующим образом.

FROM node:12.14.0
WORKDIR /app
COPY package*.json ./
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
RUN cnpm install
COPY ./ /app

RUN npm run build

FROM nginx
RUN mkdir /app
COPY --from=0 /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

Укажите версию узла и загрузите его, установите рабочий каталог в каталог /app, установите зависимости и упакуйте их. Загрузите nginx, скопируйте содержимое дистрибутива, который был достаточно дешев, в каталог приложения и замените каталог конфигурации nginx.

Файлы конфигурации в nginx следующие:перекрестный доментакже решил здесь

server{
		listen 8080;
		server_name 120.79.131.113;
		root   /app;  # 指向目录
		index index.html;
		location /api {
			proxy_pass http://120.79.131.113:7001;
		}
		location / {
			index  index.html index.htm;
			try_files $uri $uri/ /index.html;
		}
	}
  • Сборка Docker, для облегчения автоматического развертывания Jenkins предоставляется файл сценария

docker run -p 9529:8080 -d --name ele_index_vue ele/index/vue:$image_version;

После сопоставления порта в контейнере и порта хоста хост может получить доступ к содержимому образа, обратившись к 9529.

  • Установите Jenkins и подключитесь к собственному GitHub. Здесь Jenkins также устанавливается через докер.
может относиться кnuggets.capable/post/684490…,Эта статья очень хорошо написана.В процессе разведки я нашла много информации,многие из которых лоскутные,только эта статья-это что-то.Вышеупомянутый автоматизированный процесс построения может ссылаться на эту статью, Я просто не знаю, почему у этой статьи так мало лайков. . , ясно так хорошо написано. .

Выше приведено введение проекта.Если вам интересно, вы можете скачать проект и посмотреть.Если вам нужен дизайн таблицы базы данных, вы можете добавить меня.Я могу отправить его вам.WeChat: smile_code_0312

адрес гитхаба:

внутренний интерфейс

Система фонового управления

титульная страница

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