Egg 實現一個 mTime 時光網

orangexc發表於2017-09-21

先放出專案地址:github.com/OrangeXC/mt…

有一段時間沒更新部落格了,今天的文章主要圍繞 egg 進行,長時間沉浸在前端框架中,遊離到傳統 MVC 的開發模式還真不太適應,好久不寫 MVC 專案了。

說下今天的主角 egg,在前幾天的騰訊 IMweb Conf 2017 大會第一個演講就是 egg,egg 是一個 node 框架基於 koa,寓意孵化新生,本專案的 logo 就有點怪咖,是個煎蛋,這個嘛,沒有新生了,因為這個專案沒有發揮 egg 太多優勢。

之所有這麼說是因為我呼叫的三方 api,說到這裡為什麼不用 vue,react,angular 等直接請求介面呢,因為這裡涉及到一點點資料庫操作,算是沒白折騰 egg。

不寫科普文,簡單的文件層面可以直接到 egg官網

說說專案的搭建,egg 提供了 cli,專案的目錄也遵循約定規範,不可隨意篡改。

egg-project
├── package.json
├── app.js (可選)
├── agent.js (可選)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可選)
│   |   └── user.js
│   ├── middleware (可選)
│   |   └── response_time.js
│   ├── schedule (可選)
│   |   └── my_task.js
│   ├── public (可選)
│   |   └── reset.css
│   ├── view (可選)
│   |   └── home.tpl
│   └── extend (可選)
│       ├── helper.js (可選)
│       ├── request.js (可選)
│       ├── response.js (可選)
│       ├── context.js (可選)
│       ├── application.js (可選)
│       └── agent.js (可選)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可選)
|   ├── config.local.js (可選)
|   └── config.unittest.js (可選)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js複製程式碼

初始配置

這裡主要關注配置檔案 config/config.default.jsapp 目錄

先看下 config/config.default.js 檔案

module.exports = appInfo => {
  const config = {};

  // should change to your own
  config.keys = appInfo.name + '_1504252356337_1029';

  // view
  config.view = {
    defaultViewEngine: 'nunjucks',
    mapping: {
      '.tpl': 'nunjucks',
    },
  };

  config.sequelize = {
    dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
    database: 'mtime',
    host: '127.0.0.1',
    port: '3306',
    username: 'root',
    password: '',
  };

  config.mysql = {
    client: {
      host: '127.0.0.1',
      port: '3306',
      user: 'root',
      password: '',
      database: 'mtime',
    },
    app: true,
    agent: false,
  };

  return config;
};複製程式碼

這裡面指定了模板檔案,資料庫的連線引數。注意這樣並不能生效,因為我們沒有指定 plugin 對應的 npm 包,前提是要安裝這些依賴。

config/plugin.js

exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks',
};

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
};

exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};複製程式碼

在網頁的公共頭部有城市選擇,如下

這裡比較坑的是 api 是非官方的 api,只能自己整理城市列表,存到了本地的 init 目錄下的 location.json

那麼問題來了,每次呼叫本地檔案明顯是不合情理的,以後還會涉及到城市資訊變動,這裡作為第一次匯入的 init 資料寫入 mysql,之後統一從 myspl 獲取 location。

初始化程式碼按約定放在 app.js 中,允許我們進行初始化操作。

const fs = require('fs');

module.exports = app => {
  app.beforeStart(async () => {
    // 應用會等待這個函式執行完成才啟動
    await app.model.sync({ force: true });

    app.database = await app.mysql.createInstance(app.config.mysql.client);

    const locations = JSON.parse(fs.readFileSync('./init/location.json'));

    await app.mysql.insert('locations', locations.data);
  });
};複製程式碼

雖然 egg-mysql 和 egg-sequelize 文件都有介紹,這裡簡單說下

  • await app.model.sync({ force: true }); 是同步 model 到資料庫,主要是同步資料庫表和欄位

  • app.database = await app.mysql.createInstance(app.config.mysql.client); 是在應用執行時動態的從配置中心獲取實際的引數,再來初始化一個例項。

注:這裡官網的程式碼有點小坑,親測下面官網程式碼的 configCenter 並沒有 fetch 方法,遇到相同坑的該用上面的程式碼即可

const mysqlConfig = yield app.configCenter.fetch('mysql');
app.database = app.mysql.createInstance(mysqlConfig);複製程式碼
  • const locations = JSON.parse(fs.readFileSync('./init/location.json')); 這句不解釋了,看不懂先學學 node 基礎

  • await app.mysql.insert('locations', locations.data); 這句是將 Array 直接存到資料庫 locations 表,這裡見官網 如何編寫 CRUD 語句部分,官網的例子是插入單條資料(以 Object 的格式),當然這裡 Array 建立多條也是可以的

注:第一句我們建立了 locations 表,表裡多了兩個預設欄位分別是 created_atupdated_at,批量匯入資料裡沒有這兩個欄位,故報錯,解決辦法是在 model 的 location.js 裡面給這兩個欄位 default 值,如下

created_at: {
  type: DATE,
  default: new Date(),
},
updated_at: {
  type: DATE,
  default: new Date(),
}複製程式碼

上面的程式碼分別依賴兩個庫 egg-sequelizeegg-mysql,兩者均與操作 mysql 有關,當然根據業務需要選擇其一也可,至於兩個庫分別有哪些功能可以直接轉到 Github 看 document

到這裡我們去 mysql 看一眼

一切準備工作就緒

路由搭建

頁面整體分成三部分

header
router
footer複製程式碼

router 根據路由動態渲染,也是主要業務邏輯的區塊,主頁電影列表分為三類 正在售票 正在熱映 即將上映 分別對應三個路由 / /hot /new,當然考慮到城市因素(不同城市上映電影有細微差別),在本專案裡寫到了 query 裡面,所有路由後面都帶著一個 query 看著著實不爽,下一步我會把它移到 cookie 或者 localStorage,程式碼約定寫在 app/router.js 裡如下

app.get('/', 'home.index');
app.get('/hot', 'hot.index.index');
app.get('/new', 'new.index.index');複製程式碼

解釋下路由對應 controller 的語法,規則是 檔名/函式名資料夾/檔名/函式名,當然這是我個人猜測,發現奏效,官網的寫法法可以到 如何定義router

下面還有詳情頁 短評頁 熱評頁 劇照和海報頁 預告花絮頁,分別對應下面的路由

app.get('/movie/:id', 'movie.index.index');
app.get('/comment/:movieId', 'comment.normal.index');
app.get('/hot_comment/:movieId', 'comment.hot.index');
app.get('/stills/:movieId', 'stills.index.index');
app.get('/video/:movieId', 'video.index.index');複製程式碼

路由到這裡介紹完了,沒什麼好講的,不看網站這個專案的概況也是一目瞭然,接下來的事情就是 controller 來調取 model 資料渲染到頁面了,下面不一一陳述 controller,抽離一點可講的。

controller 搭建

按照約定我們直接找到 controller 目錄,看到全部的 controller

const locations = await ctx.model.Location.findAll(); 這一句找到所有的 location 陣列。

細心看的人會發現每個 controller 都有下面的程式碼,我們要獲取全部 location 列表,並找到 query 裡面的那條資料,預設是北京,這裡頁面公共部分 header 一直存在一個 location 下拉選單,所以每次都要將資料拋給頁面,更好的解法是點開下拉選單非同步拉取所有 location 再渲染進去。

ctx.query.location
  ? location = locations.find(({ id }) => id === Number(ctx.query.location))
  : location = {
    id: 290,
    name: '北京',
  };複製程式碼

這裡完全的 get 資料,一個前端層面的 ajax 都沒有,原諒我的偷懶,這樣導致了所有 controller 的程式碼冗餘。

剩下的就是去呼叫 mtime 的 api 了,感覺很好的是 egg 為我們封裝了全域性 http 方法,HttpClient

使用簡單,引數簡單,堪比 axios 的便捷。大家自己體會吧。

view 搭建

說到 view 層就到了大前端的天下,玩的 6 的話,這一層可以無限延展,從最簡單的模板(pug,ejs,swig,nunjucks 等),到 (vue,react,angular 等),再到(Andriod, IOS),再再到(RN,weex),甚至是小程式介面。

服務層讓我喜歡的就是可渲染模板,可吐資料,本來是想搞個前後端分離,後來被自己氣到,呼叫人家的 api,竟然不直接寫 view 層,搞個 egg 進來沒起到——卵用。

不自覺講起了段子,索性就回歸 10 年前的前端,拋開 MVVM,甚至擼起了 jquery。

牢騷一堆,這裡用的是 nunjucks,官網的例子用的就是這個模板。

到了這裡我們的頁面完成了。。。雖然什麼也沒講,我預設大家都看得懂模板的哈,至於 Bulma 的初衷是不想用 Jquery(bootStrap 大家懂得),奈何找不到喜歡的輪播,找了許久的輪播竟然還依賴 jquery。。。

不足

這裡宣告人家 mTime 的介面並不是官方公開的,來自 github.com/jokermonn/-… 的伺服器禁止跨域請求 MP4 資源,嘗試以下幾個方法解決這個問題

  • iframe,沒能成功,訪問失敗
  • <a target="_blank">,也沒解決問題,不過奇怪的是複製 mp4 的 url 到新 tab 回車可以訪問,a 標籤跳轉新 tab 則失敗,js 的 window.open() 沒有嘗試
  • node 請求 mp4 的 buffer 轉成 stream 後再拋給前端,能力有限沒能解決問題。

一方面作者能力原因,一方面違背 mTime 節省視訊伺服器流量的想法,在視訊頁給了連結可以跳到真正的 mTime 官網。

算是遺憾吧,如果有 node 端轉發視訊請求經驗的大神歡迎賜教。

總結

專案是在短期內速成的,好多細節沒考慮到位,望大家多吐槽,寫這篇文章的目的是給想了解 egg 的開發者一個小 demo,真正的生產模式比這個複雜的多,本文也自然就不值一提。

最後

本文同步更新至我的個人部落格 orangexc.xyz

相關文章