Node實戰:全棧開發一個餓了麼商城

學一項技術最好的方法就是用這個技術作點什麼。javascript

學習node的時候,看完一遍以爲本身能打能抗,次日就作回了從前那個少年。惋惜不是張無忌,太極劍法看完忘了就吊打倚天劍。在下看完忘了,那即是忘了。故決定作個項目鞏固一下知識css

先看下部分效果圖html






整個項目是徹底先後端分離的項目,包含後臺接口,後臺頁面,前端頁面三個倉庫。前端

用戶經過註冊後臺管理員,對店鋪和店鋪食品進行增刪改查操做,相應的店鋪和食品會在前端進行展現。整個後臺項目以egg爲框架,mysql做爲數據庫,用typescript進行開發,涉及數據庫表十一張接口四十個左右。後臺和前端頁面使用常規的vue+element-ui+vuex+vue-router進行開發。在部署方面,因爲這是我的項目,因此我決定用本身沒有用過的技術,自建了個Jenkins,經過jenkins自動拉取和執行腳本創建Docker鏡像對vue項目進行自動化部署。整個流程對於我的項目還算完整。vue

在線地址:

前端地址

後端地址java

項目參考

慕課網餓了嗎課程

基於 vue + element-ui 的後臺管理系統node

:對於後臺管理系統,我這邊只參考了 基於 vue + element-ui 的後臺管理系統 的業務邏輯,代碼方面沒有深究,由於用的技術棧不太同樣。因爲這是我第一次用node作項目,日常在公司也沒有用到node,參考了一些零零碎碎的文章,但初學者確定是會有東施效顰的醜態,哪裏作的不合理的還請斧正,程序員最大的優勢的就是知錯就改,我不外乎如是。mysql

後臺

所用技術

  • Node
  • Egg
  • MySql
  • Redis
  • TypeScript

實現功能

  • 管理員註冊登陸
  • 添加和修改店鋪
  • 添加和修改店鋪食品
  • 查看食品列表
  • 查看商家列表
  • 查看當天數據和總體數據
  • 管理員信息設置
  • ...

總體項目構建能夠參照egg官網提供的教程,裏面有詳細的教程和目錄詳解,這裏不講常規的增刪改查功能,咱們關注整個項目的通用性和比較麻煩的功能實現ios

通用功能的封裝

  • 請求響應封裝
/*
 * @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失效
}

因爲如今公司項目的歷史緣由,後臺返回的響應格式有多種,狀態碼也分散在各處,對前端不是很友好,在這裏我就把整個項目的響應作了封裝,全部的controller繼承於這個基類,這樣後臺開發也方便,前端也能更好的寫一些通用的代碼。nginx

  • 通用代碼的封裝

對於不少通用的功能,好比這個項目裏的圖片上傳功能,建立文件夾功能,隨機生成商鋪評分和食品評分等等,這些和業務沒有太大關係又重複的代碼,都是須要作一個封裝以便維護,egg爲咱們提供了很好的helper拓展,在helper拓展中寫的功能,能在項目的全局範圍內經過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;
}

在一個請求過程當中就能夠經過egg提供的方法來調用

mon_sale: this.ctx.helper.random(1000, 20000)
  • 前端請求參數的校驗

對於前端傳參的校驗,若是參數不少,那咱們業務代碼裏面的校驗就會有一大坨關於校驗相關的檢測代碼,好比建立商鋪的時候,前端傳來的相關參數就有十幾個,這種看着仍是挺不爽的,我這邊本身開發的時候把參數校驗經過egg提供的validate作了統一管理,這裏的validate插件須要在啓動的時候本身加載。

/**
     * @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把加密串存起來,用戶請求須要登陸的接口的時候,後臺會將egg中的cookie取出來和redis中的作對比,作一個登陸態的校驗,這裏有個不一樣的點,egg裏,cookie是以毫秒爲單位的,我沒認真看,致使開發的時候找不到bug的我捏碎了好幾個鼠標,下面是具體的實現邏輯

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, "密碼錯誤")
      }
    }
  }

不過這種實現方式仍是有點問題的,用戶驗證主要有兩種方式

  • session+cookie
  • token令牌

兩種方式實現的優劣就是session須要將sessionId保存在服務器,前端傳來的cookie和服務器上存儲的sessionId作對比來實現用戶驗證,而token令牌的驗證方式一般來講就是經過jwt生成加密串,前端請求的時候將加密串傳給後臺,後臺去驗證這個加密串的合法性,jwt方式就是後臺不須要去存儲加密串,而上面這種方式,用jwt生成加密串,再來驗證一遍,是有點奇怪的,我有時間會把他改過來。

  • 登陸中間件

開發過程當中不少接口是須要登陸才能訪問的,不可能說在全部須要登陸的接口裏給他加上登陸校驗,咱們能夠爲接口加個中間件,egg是基於洋蔥模型,中間件能在接口訪問前作一些攔截限制。

/*
 * @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獲取全國全部的城市,而後再根據第三方庫pinyin,將城市首字母提取出來並分類,這邊爲了防止請求次數過多,致使個人服務器ip被高德封掉,將結果用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);
    }
  }
}

還有一些功能,感興趣的能夠把項目clone下來本身瞅瞅。

前端

所用技術

  • Vue
  • Vuex
  • Vue-Router
  • Cube-Ui
  • Axios

實現功能

  • 用戶註冊登陸功能
  • 用戶地址增刪改查功能
  • 商戶列表展現
  • 商戶詳情頁展現
  • 食品列表
  • 食品詳情頁
  • 商戶搜索
  • ...

項目詳情

  • 移動端佈局方案

項目使用amfe-flexible+px2rem-loader適配移動端。

package.json裏添加

"plugins": {
      "autoprefixer": {},
      "postcss-px2rem": {
        "remUnit": 37.5
      }
    }
  • axios作統一請求和攔截

這邊主要是對響應作了攔截,請求發生異常toast提醒,用戶態異常時跳轉到登陸頁,並添加redirect參數,確保登陸後能返回上一個頁面

// 添加響應攔截器
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、40一、402等等
    if (error && error.response) {
      console.log(error.response)
    }
    return Promise.reject(error)
  }
)
  • 集成高德地圖api


像這種地址搜索都是經過調用高德地圖api返回的數據,這邊經過mixins作了封裝

/*
 * @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
            })
          }
        })
      })
    }
  }
}

這邊還有一個小小的點,咱們將返回的結果根據咱們輸入數據進行高亮,好比上圖我輸入了寶安,結果列表裏寶安進行了高亮,這邊我是用正則匹配了下

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、router、vuex統一管理
    這邊我是沿用了我司項目的管理方式,經過功能將接口路由和vuex數據進行了劃分,而後經過一個index.js來向外暴露



有些頁面是須要登陸才能訪問的,這邊在路由守衛這邊也作了限制,只要在路由的 meat里加上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頁面keep-align下來,而後判斷下一個路由的name,看是否須要重置參數,固然這種仍是比較low的,這邊提供另外的思路,keep提供了一個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...)
  • 作好踩坑的打算...

具體步驟:

  1. 域名和服務器我這邊都是在阿里雲上買的,比較麻煩的是域名須要備份,要等一陣子,原本我不打算買域名的,可是這樣就會有一個問題,後臺管理系統和前端共用一個ip,這樣cookie會互串,最後仍是被迫買了個域名。
  2. 域名配置,這個須要在阿里雲後臺對你服務器ip和你的域名進行配置,接下來是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作對應

  1. 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

指定node版本並下載,工做目錄設置爲/app目錄,安裝依賴並打包。下載nginx,將剛纔夠賤的dist裏的內容複製到app目錄下,替換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;
		}
	}
  1. docker構建,爲了方便Jenkins的自動部署,提供了個腳本文件
#!/usr/bin/env bash
image_version=`date +%Y%m%d%H%M`;
# 關閉ele_admin_ts容器
docker stop ele_admin_ts || true;
# 刪除ele_admin_ts容器
docker rm ele_admin_ts || true;
# 刪除ele/index/vue鏡像
docker rmi --force $(docker images | grep ele/admin/ts | awk '{print $3}')
# 構建ele/index/vue:$image_version鏡像
docker build . -t ele/admin/ts:$image_version;
# 查看鏡像列表
docker images;
# 基於ele/index/vue 鏡像 構建一個容器 ele_admin_ts
docker run -p 9528:7001 -d --name ele_admin_ts ele/admin/ts:$image_version;
# 查看日誌
docker logs ele_admin_ts;
#刪除build過程當中產生的鏡像    #docker image prune -a -f
docker rmi $(docker images -f "dangling=true" -q)
# 對空間進行自動清理
docker system prune -a -f

對容器內的端口和宿主機端口作了映射,宿主機訪問9529就能訪問到鏡像的內容。

  1. Jenkins方面,推薦你們能夠去看下騰訊雲實驗室的教程,能夠在線實驗,騰訊良心之做。
    騰訊開發者實驗室

    注: 部署方面,有問題的能夠直接看這篇文章,寫的很清楚:http://www.javashuo.com/article/p-zzmzdjjb-gy.html

以上就是項目的簡介,你們感興趣的能夠把項目download下來看一下,須要數據庫表設計的能夠加我一下,我能夠發你,微信:smile_code_0312

github地址:

後臺接口

後臺管理頁面

前端頁面

最後,最近有跳槽的打算,跪求各位大佬介紹,19屆菜雞前端,卑微求職

相關文章
相關標籤/搜索