Flutter & Koa2 實戰全面升級,試問誰不孤獨?(小萬字長文)

洋小洋同學發表於2020-03-17

那個固執的少年回來了 《孤島 App》這個系列已經停更了很久,這次全面升級,對的起給我star 的老鐵們

介紹

APP 名稱:《獨 °》

客戶端方案:flutter

服務端方案:Koa2

功能

寫著完善著

 - 我的
  - 個人資訊
  - 頭像修改
 - 首頁
  - 列表頁
  - 傳送圖文、視訊等
 ……
複製程式碼

前序準備

  • 下文連結預警
  • 長文預警
  • 嘮嗑方式不正經預警
  • 錯別字警告

當開始全面更新迭代的時候,沒有產品 的思維是多麼可怕的一件事,開發的過程中會同步更新系列文章,希望一塊撩一撩flutter 當然了這些文章都是沒有更新的

分支變動

20200315111241.png
是這樣的,我們們把之前持續更新的移動到lsolated_island_app 這個分支。想要翻看的可以自行clone 《獨》所有的開發現在master 分支。可能舊的文件地址找不到的情況,這個我後續更新一下

之前我的評論區

  • 問:為啥 flutter 評論那麼少
  • 答:可能大家還不太瞭解

我覺得自己開發個小 app 也挺好玩的。

希望多多鼓勵很關注,有不恰當的地方也歡迎指正。

友情建議:

最近一段時候由於公司需求,筆者在用 Vue 生態的 uniapp 技術棧來開發 app,總體體驗是不太好的

不做什麼橫向對比,在正確使用 flutter 的前提下,flutter 開發的應用是相比於 uniapp 好很多的(這只是我個人看法)

個人感覺 flutter 的學習成本還是比較高的,如果公司想要通過這個技術來開發的話,可能需要有同事持續跟進 flutter 的生態發展,並定期分享給成員,因為 flutter 生態是越來越活躍,技術的更新迭代是相當的迅速,相關的第三方包外掛今個能用。明天可能你就不知道咋回事了

flutter 只是一個簡單的 UI(這裡特別說一下並沒有所謂的巢狀問題),但是其在安卓 IOS 上的渲染能力,動畫能力是十分的驚人

最後簡單說一下,企業專案十分花裡胡哨的話,可能生態中並沒有良好的解決方案,這就需要改一些現有的原始碼,什麼和開發者溝通我該怎麼實現,這也為什麼企業選擇 taro uniapp rn 等等

資料分析

  • 為什麼有的人說 flutter 涼了嗎
  • 有的人說 2020flutter 你跳槽張薪資必備

簡單的資料說話

多終端解決方案 星星數
flutter 88.3K
react-native 85.5K
taro 24.3K
uni-app 19K

雖然星星數 並不能說明什麼,但在技術選型的時候,它還是一個十分重要的參考價值,筆者最近在做自己的全棧專案相信不久會出生吧

也只好通過以文字的時候,敦促自己,其實想想錄視訊也挺好的 慢慢來吧

BIOS 開啟

由於簡單配了臺主機flutter 的執行需要 主機開啟BIOS 模式

  1. 開機快速按f12或者DEL 也就是刪除鍵 進入 BIOS(不同的電腦型號是不盡相同的)
  2. 先不切換語言模式(一般情況下預設是 english)點選 Advanced Mode(F7)進入高階選項。
  3. 點選 Advanced,然後點 CPU Configuration。
  4. 下拉選單找到 Intel Virtualization Technology,在其子選單下把選項改成 Enabled。
  5. 按 F10 儲存退出,開啟成功。
  6. 這樣一般就可以成功重啟了

至於想我這樣,多快的手速都進不去BIOS 的人,那可能需要簡單的拆一下顯示卡 然後再簡單的解除安裝一下主機板的電池

20200315191116.png
上邊的亂七八糟的 嘮嗑,好像跟flutter 並沒有什麼關係,不過
20200315103454.png

在我們借用Asd 開啟一個虛擬機器裝置除錯的時候,可能會遇到一個問題,這就需要主機裝置開啟虛擬 一般情況下預設是不開啟的。我是偏前端開發者,當然了看到這篇文章的你如果沒在開發app 也不要走,因為技術就是金錢

flutter_du 初始化

準備

  • 圖片素材 登入頁的背景圖 免費相簿相片 中文 臺

    20200316211452.png

      assets:
        - lib/images/login_bg1.jpeg
        - lib/images/login_bg2.jpeg
        - lib/images/login_bg3.jpeg
        - lib/images/login_bg4.jpeg
    複製程式碼
  • 狀態管理:全域性狀態管理方案(這一點在實際的開發中也是十分必要的)

  • 外掛:Flutter Provider Snippets vscode 外掛 類和方法的集合 也規範化provider 的書寫

可以參考閱讀一下我之前的分享 Flutter 狀態管理一鍋端:第一章 Provider ,這篇簡單的介紹瞭如何在一個專案中管理資料,當然了即使是專案很簡單,統一的管理資料可以儘可能的方便後期的維護,檢視UI層與資料狀態層分離

實現效果

實現的效果是底部輪播圖,全屏的滑動,由於這個效果圖,我搞的gif 有點大7,8M ,放這個圖片吧

20200316231227.png

目錄結構

完全新建一個新的flutter專案 刪除 main.dart 中的檔案先,保留一個整潔的開始,它暫時是這樣的

20200316213457.png

├─lib
│  ├─pages
│  │  └─login
│  └─provider
└─test
複製程式碼

provider

首先先實現provider 登入的狀態管理,其中主要就是運用到的動畫 相關的內容。動畫相關的內容推薦閱讀

那麼剛開始我們就直接使用provider 是的,漸進式開發,遇到問題,解決問題。開發的過程中,我們可以自己寫包然後上傳到 flutter pub

基本結構

import 'package:flutter/material.dart';
class LoginProvider extends State<StatefulWidget>
    with ChangeNotifier, TickerProviderStateMixin {


  @override
  Widget build(BuildContext context) {
    return null;
  }
}
複製程式碼

狀態 init

所謂的狀態 init ,就是我們邏輯部分所用得到的初始化的資料,一般是空的 list 或者字串等

 Animation<double> bgAnimation;  // 動畫的
  AnimationController bgController; // 控制器 文字輸入同樣有控制器
複製程式碼
double mainPicOp = 1; // 透明度
  double otherPicOp = 0; // 透明度
複製程式碼
List<String> imgsList; // 背景圖輪播的素材列表

imgsList = List<String>(); // 初始化
imgsList.add('lib/images/login_bg1.jpeg');
imgsList.add('lib/images/login_bg2.jpeg');
imgsList.add('lib/images/login_bg3.jpeg');
imgsList.add('lib/images/login_bg4.jpeg');
複製程式碼
int mainPicIndex = 0; // 當前正在顯示的圖片編號
  int otherPicIndex = 1; // 備胎是1
複製程式碼

定時器

需要我們初始化定時器,讓圖片的透明度切換

  Timer dingShiQi; // 定時器

  dingShiQi = Timer.periodic(Duration(seconds: 2), (cb) {
      bgController.forward(from: 0);
    });
複製程式碼

主要邏輯

 if (state == AnimationStatus.completed) {
          mainPicIndex = mainPicIndex + 1;
          otherPicIndex = otherPicIndex + 1;
          if (mainPicIndex == imgsList.length) {
            mainPicIndex = 0;
          }
          if (otherPicIndex == imgsList.length) {
            otherPicIndex = 0;
          }

          mainPicOp = 1.0;
          otherPicOp = 0.0;
          notifyListeners();
        }
複製程式碼

該釋放的釋放,該取消的取消

void dispose() {
    dingShiQi.cancel();
    bgController.dispose();
    super.dispose();
  }
複製程式碼

ui 佈局

使用 provider

拿到provider 這樣我們在無狀態的元件中同樣可以來取自如的使用資料

LoginProvider provider = Provider.of<LoginProvider>(context);
複製程式碼

核心程式碼

Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => LoginProvider()),
      ],
      child: Consumer<LoginProvider>(
        builder: (context, counter, _) {
          return MaterialApp(home: LoginPage());
        },
      ),
    );
  }
複製程式碼

上邊分享的程式碼是十分有必要的,因為provider 有個一次大的更新就是廢除了build 然後改成了create

  • MultiProvider 這是在需要多個 provider
  • Consumer 相當於是監聽訂閱的變化

錯誤解決

其實在開發的過程中,有錯誤是十分正常的,也是十分常見的,尤其是在flutter 的開發中。更是一些莫名奇妙的問題,

  • 不像 web 端有console.log console.table 等等直接可以在控制檯列印輸出

方案

  • 一種有效的方案是世界flutter run ,不要慌,不要怕 在 flutter 的開發中控制檯一大大大大長串的錯誤很是常見
  • 20200316215722.png

主要就是參考關鍵字 常見的關鍵字 然後出門google 也可以推門 Flutter 實際專案開發中踩坑大合集(持續更新..)

  • 還有一種方案是 ,利用編輯器的除錯工具

20200316220335.png

像這種一上來就是什麼堆疊溢位 不過一般情況下,就是我們們的程式寫的有問題

小結

本篇章的上半段呢便是對客戶端專案的初始化,其中使用到了動畫 這也是 flutter ui 中的核心力量,優雅的渲染能力;還有就是provider 在閒魚的fish redux 等等等等一系列的狀態管理實踐中,當然了,使用哪個都行,但我覺得provider 不錯。(前提是在用對的情況下)

實現模組

主要是登入頁的一個背景圖,透明度的切換,並沒有什麼實質的內容。

預覽

再更新的話,便是,客戶端登入,然後請求到公網的資料,至於介面是什麼,我想可以繼續往下翻一番,感覺文章很隨意的話,也請隨意的分享 一下,請註明,來源

Koa2 初始化

後端的服務選型 ,最初有幾種方案

  • node 原生開擼 (這個主要是寫的太囉嗦)
  • Express Node 的一個框架(早期的)這個也行
  • Koa2 現在一般都更新到第二代了,V2.11.0
  • eggjs 這個企業級約定俗稱也還不錯(在筆者的其他專案用了,這個 flutter 就不用了)
  • Nestjs 這個同步介面文件十分方便(屬於嚐鮮玩法,也在筆者的其他專案用了,那就也先不用在這兒)

所以這個就先用大名鼎鼎的Koa2 先來說說我對 koa2 的理解

  • 洋蔥模型,兜一圈又回來
  • 非同步程式設計 可以說 koa2 很好實踐 js 非同步程式設計理念
  • 可定製化,根據習慣隨意開發,擴充性強

專案目錄

├─app
│  └─api
│      └─v1  // RESTful API 介面規範 v1 版本
├─core    // 核心的程式碼
└─test
複製程式碼

開發依賴

在前端的專案中,package.json 一般這個就是管理包檔案的,那在我們的flutter 專案中使用pubspec.yaml 檔案是一樣的,差不多

"dependencies": {
    "koa": "^2.11.0", // 這個是專案依賴的koa 雖然名字還是koa但她已經是2代了
    "koa-router": "^8.0.8", // 這個是路由跳轉的
    "require-directory": "^2.1.1"  // 這個等會再說
  }
複製程式碼

當然了這只是專案的初始化,後續更新會在此基礎上,所以還是強烈建議看一下的。也可以收藏,沒事的時候翻出來看看

入口檔案

目前還是用js 來開發,如果對ts 感興趣也可以推門

const Koa = require("koa");
const app = new Koa();
const Init = require("./core/init");
Init.entrance(app);
app.listen(3000);
複製程式碼

在這裡我們新建了一個 主要就是初始化應用的,這一點在 NuxtNext 等等的框架中都有核心的體現,包括在egg js 中 ,

  1. 匯入 koa require("koa");
  2. 例項化 new Koa();
  3. 引入核心程式碼 require('./core/init')
  4. 傳入 app Init.entrance(app)
  5. 監聽埠 3000

還有一點十分重要就是在node 環境中模組化的規範不同於es6import 至於模組化規範,自行右轉google

應用初始化類

// 初始化載入路由
const requireDirectory = require("require-directory");
const Router = require("koa-router");
class Init {
  // 入口方法
  static entrance(app) {
    Init.app = app;
    Init.initRoute();
  }
  // 路由匯入初始化
  static initRoute() {
    requireDirectory(module, "../app/api", { visit: visitor });

    function visitor(obj) {
      if (obj instanceof Router) {
        Init.app.use(obj.routes());
      }
    }
  }
}
module.exports = Init;
複製程式碼

在這個檔案中,我們引入了上文提及的require-directory

var requireDirectory = require('require-directory'),
  visitor = function(obj) {
    return obj(new Date());
  },
  hash = requireDirectory(module, {visit: visitor});
複製程式碼

意識是說我們可以匯入檔案,那麼我們要做的就是自動化注入路由檔案

  • user.js

    // 個人中心v1-api
    const Router = require("koa-router");
    
    const router = new Router();
    
    router.get(`/api/v1/user`, (ctx, next) => {
      ctx.body = {
        code: 0
      };
    });
    module.exports = router;
    複製程式碼

我們整體遵循RESTful API 風格 api 不得不思考一個問題,關於後端介面迭代的問題,這也是為什麼我們呼叫的介面會有v1 v2 版本字首,這對於規範化開發十分重要。所以我們把路由檔案這樣安排

 - app
 	- v1
 	  - home.js
 	- v2
 	- v3
 	……
複製程式碼

啟動後臺服務

npm i // 沒有幾個包下起來很快
npm i -g nodemon // 自動監聽檔案變化,這在node開發過程中十分重要
複製程式碼
nodemon app.js
複製程式碼

直接在瀏覽器輸入:

本地 3000 埠,可嘗試直接點選

當看到介面返回的資訊就說明後臺的基本服務已經啟動了,但是這還遠遠不夠,遠遠的不夠

20200315121326.png

{
  "code": 0,
  "data": {
    "nickName": "yayxs",
    "fav": [
      {
        "id": 1,
        "type": "writing"
      }
    ]
  },
  "msg": "獲取使用者資訊成功"
}
複製程式碼
  • 一、這是我們自己的本地服務,當寫了十分炫酷的服務,顯然是不便分享到他人的
  • 二、這也還是我們自己的寫的測試的 json 資料,並沒有連線資料庫

順便先說一下,這個專案我們們用MySql ,感興趣的話,你也可以推門 Node | 自我爬蟲掘金專欄文章

伺服器部署

核能預警

一提到伺服器相關的知識不要慌,我們一步步來實現公網IP 部署node 服務

目標

目標一:掌握 pm2 部署 node JS 服務 進行守護,程式

目標二:掌握基本的 nginx 反向代理

目標三:暫時拜別本地服務

效果

當我們在任何一臺有網的電腦上位址列輸入 http://62.234.111.140/api/v1/user,便會成功的返回我們所寫介面返回的資料

20200315132602.png

雲伺服器準備

為什麼說第一步要準備雲伺服器,因為哪怕你用原生html 或者說什什麼的ssr渲染框架 或者說jQ

等等吧,哪怕是之前讀書做的告白網站

81OIpD.gif

你總得放在公網上吧,不然總不能把女孩子拉到自己的家裡,然後npm run start等等,你看你看………………

  • X裡雲
  • X訊雲
  • X為雲

所以還是國內的BATH 等等這幾家的,都差不多真的不騙你

問:什麼是雲伺服器?什麼是域名解析?什麼是部署?怎麼反向代理?那你能幫幫我嗎??

答:你玩遊戲的是啥 ,電腦,雲伺服器就是電腦(當然了在這裡是不正確的也不是很準確的)

雖然我沒有那麼浪漫和騷,但是我有云伺服器 還是包年的 公網 IP http://62.234.111.140/

希望大佬們不要對我做壞事情,跪拜

連線遠端伺服器

20200315123339.png

輸入使用者名稱密碼連線就行了

20200315123519.png
這樣就可以了,我發現真的是廢話好多啊 具體的細節,如果大家有什麼疑問,可以再評論區留言,能力所及,會回覆的。首先上來的時候要安裝幾個東西

npm i -g node  // 全域性安裝最新版的node環境
npm i -g pm2 // 全域性安裝執行緒管理
 // 等等等等
複製程式碼

20200315124004.png

一切的環境準備好之後,就需要同步一下我們們的服務端程式碼到雲伺服器 這一點同樣十分的重要,不然就沒得進行了。

依賴三方伺服器

總得有個同步的伺服器,我們選擇github 伺服器,這樣在雲服務也好,還是我們們的本地電腦,程式碼最起碼丟不了

我是放在了home 資料夾下

20200315124901.png

那就裝依賴唄,老套路

cd /home/flutter-koa2-du/koa2-server
npm i
複製程式碼

趁著也安裝一下nodemon 吧 答應我安裝下好嗎npm-nodemon

雖然說我們也是在本地開發然後同步到雲伺服器,雲伺服器的程式碼一般不會怎麼變動,

20200315125346.png

啟動專案

20200315125503.png

不要慌,解決問題

  • 問題原因:主要是在同一環境 3000 這個埠已經被佔用
  • 解決問題:那就關閉 3000 程式

這個時候我們就要引入PM2 純純的大寫的高調的pm2

Pm2 應用

首先來看下pm2 幹些什麼

  • 內建負載均衡(使⽤ Node cluster 叢集模組、⼦程式,可以參考樸靈的《深⼊淺出 node.js》⼀書 第九章)
  • 執行緒守護,keep alive
  • 0 秒停機過載,維護升級的時候不需要停機.
  • 現在 Linux (stable) & MacOSx (stable) & Windows (stable).
  • 多平臺⽀持
  • 停⽌不穩定的程式(避免⽆限迴圈)
  • 控制檯檢測 id.keymetrics.io/api/oauth/l…
  • 提供 HTTP API

可能現在這樣說,也沒設好理解的,是有一個這樣的場景,雲伺服器的環境可不像我們本地的電腦,即開發環境(dev),一旦上線,會有各種複雜的問題出現,導致程式崩掉。不能夠為我們提供服務

配置
npm install -g pm2
pm2 start app.js --watch -i 2
// watch 監聽⽂件變化
// -i 啟動多少個例項
pm2 stop all
pm2 list
pm2 start app.js -i max # 根據機器CPU核數,開啟對應數⽬的程式
複製程式碼

這只是簡單的配置,詳細的玩法可以自行右轉google 也可以下翻參閱文章關於pm2 的分享,當然了還是要滑上來的

pm2 list

20200315130743.png

我們可以通過pm2 list 檢視程式啟動情況,顯然我們的專案已經在雲伺服器的3000埠啟動了,那麼這個時候我們把程式stop all 停掉

pm2 stop all

20200315131034.png

我們通過命令先聽到 3000 埠

pm2 優雅啟動 node 程式服務

pm2 start app.js -i max -n node-koa-pm2
複製程式碼

詳細的含義自行google , ok 到這裡應該就沒什麼問題了

curl 自行訪問測試

curl http://127.0.0.1:3000/api/v1/user
複製程式碼

20200315132303.png

Nginx 反向代理

是這樣的,我們考慮一下,介面訪問的時候怎麼才優雅,也不知道埠是 3000 啊,所以需要一個代理伺服器

  • 正向代理 :*******
    20200315133216.png
  • 反向代理:像這種就是反向代理,具體右轉google
安裝

直接在雲伺服器通過yum 就可以了我覺得

yum install nginx
-----
apt update
apt install nginx
複製程式碼

然後怎麼辦呢,觸及到我的知識盲區了,還是不要慌,遇到問題解決問題。敢於試錯吧

20200315133555.png

nginx -v

檢視當前雲服務安裝的nginx 版本

nginx -t

檢視配置,這很重要,因為它會定位到nginx的主要配置所在的位置 ,不同的安裝方式所在的位置是不同的以下是筆者的

nginx: the configuration file /www/server/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /www/server/nginx/conf/nginx.conf test is successful

複製程式碼
cat nginx.conf

檢視nginx 主要的配置檔案,

…………………………
server
    {
        listen 888;
        server_name phpmyadmin;
        index index.html index.htm index.php;
        root  /www/server/phpmyadmin;

        #error_page   404   /404.html;
        include enable-php.conf;

        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
        }

        location ~ .*\.(js|css)?$
        {
            expires      12h;
        }

        location ~ /\.
        {
            deny all;
        }

        access_log  /www/wwwlogs/access.log;
    }
include /www/server/panel/vhost/nginx/*.conf;
}

複製程式碼

主要的要看重這句話include /www/server/panel/vhost/nginx/*.conf;

意思是說會引入字尾名.conf 的檔案作為配置的一部分,所以當我們新增配置的時候,檔名要是.conf 這樣 nginx 會匯入並作為配置

20200315185013.png

這時候我們只需要新建一個**.conf 的檔案就可,新增如下的配置

server {

        listen 80;
        server_name  localhost;
        location /api {
            proxy_pass http://127.0.0.1:3000;
		}
}

複製程式碼

我們新增的nginx 配置並沒有包括

  • 靜態路由的配置
  • 等等
重啟 nginx
nginx -s reload
複製程式碼

沒什麼錯誤的話,應該就可以了, 這個時候驗證一下自己的成果

{
  "code": 0,
  "data": {
    "nickName": "yayxs",
    "fav": [
      {
        "id": 1,
        "type": "writing"
      }
    ]
  },
  "msg": "獲取使用者資訊成功"
}
複製程式碼

一個簡單的get 請求舊簡單的部署到伺服器上

構建高可用的 node 環境

在幹擼node 的時候,如何當程式丟擲錯誤的時候。構建高可用的 node 是十分有必要的如下的程式碼可參考閱讀,方便理解在服務端的環境下為什麼需要 PM2 來管理程式

  • app.js
// app.js
// 引入koa
const Koa = require("koa");
// 建立⼀個Koa物件表示web app本身:
const app = new Koa();
// 對於任何請求,app將調⽤該非同步函式處理請求:
app.use(async (ctx, next) => {
  // 隨機產⽣錯誤
  Math.random() > 0.9 ? yayxs() : "2";
  await next();
  ctx.response.type = "text/html";
  ctx.response.body = "<h1>success</h1>";
});
if (!module.parent) {
  app.listen(3000);
  console.log("app started at port 3000...");
} else {
  module.exports = app;
}
複製程式碼
  • test.js
// test.js
var http = require("http");
setInterval(async () => {
  try {
    await http.get("http://localhost:3000");
  } catch (error) {}
}, 1000);
複製程式碼
  • cluster.js
var cluster = require("cluster");
var os = require("os"); // 獲取CPU 的數量
var numCPUs = os.cpus().length;
var process = require("process");
console.log("numCPUs:", numCPUs);
var workers = {};
if (cluster.isMaster) {
  // 主程式分⽀
  cluster.on("death", function(worker) {
    // 當⼀個⼯作程式結束時,重啟⼯作程式 delete workers[worker.pid];
    worker = cluster.fork();
    workers[worker.pid] = worker;
  });
  // 初始開啟與CPU 數量相同的⼯作程式
  for (var i = 0; i < numCPUs; i++) {
    var worker = cluster.fork();
    workers[worker.pid] = worker;
  }
} else {
  // ⼯作程式分⽀,啟動伺服器
  var app = require("./app");
  app.use(async (ctx, next) => {
    console.log("worker" + cluster.worker.id + ",PID:" + process.pid);
    next();
  });
  app.listen(3000);
}
// 當主程式被終⽌時,關閉所有⼯作程式
process.on("SIGTERM", function() {
  for (var pid in workers) {
    process.kill(pid);
  }
  process.exit(0);
});
複製程式碼

其他

近期感想

這一段的時間,上下班的時間一直在想產品 的相關的問題,才知道設計一個東西是多麼的難,思維很混亂,這也是為什麼這麼久沒更新(當初說好的一週一更呢)。

  • 剛開始可能是面向自己,孤獨的自己
  • 接著可能會面向 B 端使用者
  • 大眾的 C 端產品

每個人的思路,每個人的共享對於產品的誕生是多麼的重要

  • 哪怕一個實習生
  • 哪怕一個剛開始企業開發的小生
  • 哪怕一個巨集觀架構的大佬

都一樣的,唯一不同的是工資 不一樣的,當個愛好,沒事分享分享

總結

這篇文章包含了兩個大的方向flutternode

  • 如何重新出發,構思一個簡答的跨端 app ,登入頁
  • 如何從 0 開始搭建一個簡單的 node 後臺服務,實現前端人的後端夢
  • 如何入門瞭解nginx 等服務端運維相關的知識,即使皮毛

求求

感覺有意思的也希望一切探討。完整專案的 github 倉庫地址獨 °,真的希望能給個stat 這也是為什麼我重新構思繼續開發。

20200315190644.png

行文思路

參考閱讀

相關文章