冷星

冷星 查看完整檔案

北京編輯中國海洋大學  |  計算機科學與技術 編輯58  |  前端工程師 編輯 github.com/lengxing/MyBlog 編輯
編輯

一個在程序猿修煉生涯中的修行者

個人動態

冷星 發布了文章 · 3月3日

將博客搬至CSDN

將博客搬至CSDN-搬遷必須,請大家忽略即可

查看原文

贊 0 收藏 0 評論 0

冷星 贊了文章 · 3月3日

工作兩三年了,整不明白架構圖都畫啥?

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成長,讓自己和他人都能有所收獲!??

一、前言

很多程序員畫架構圖頭疼,不知道畫什么、怎么畫!

分享、評審、述職、答辯,只要你在程序員這個行業,就幾乎離不開要畫圖。

一提到畫圖很多人就想站會起來喊,”內卷“、”內卷啦“、”PPT工程師“,但程序代碼本身就是一種數學邏輯的具體實現,如果沒有一些圖表配合文字的闡述,講真很難讓所有人都能在共同的共識下進行交流。

這不像是文科,”八表流云澄夜色,九霄華月動春城“ 上來就能聯想到它是在描述啥。但是偏理科代碼邏輯或架構設計,只能把抽象的內容用圖表的形式展現出來,讓大家在同一的共識下共同協同工作。

而我們畫的架構圖、流程圖、結構圖、功能圖、邏輯圖等,都需要好看、好懂、好用、好搞,因為:

  • 好看是為了提升溝通效率,
  • 好懂是為了提升交流共識,
  • 好用是為了提升交付質量,
  • 好搞是為了提升實施速度。

這就像君子在追求漂亮姑娘一樣,好看就想主動撩一下、有品行和共同的三觀很快讓你開口說我懂你、接下來就是交付質量和實施速度了,那也是水到渠成的事。

好,別激動,接下來我們就開始專心研究研究架構圖,都有哪些,該怎么畫,有什么手法。

二、架構圖有哪幾種?

僅說技術架構圖的話,通常我們?指的是選型各項技術組件來支撐整個服務建設的系統架構。但用于不同人群范圍和不同場景下會有其他分類,如圖 26-1 架構圖分類

圖 26-1 架構圖分類

  • 業務架構:需求初期業務的結果和過程描述一般比較模糊,可能來自于某個老板、運營或用戶的反饋。客戶說海爾洗衣機洗土豆會堵,海爾立馬設計專門的土豆洗衣機 業務方向往往是定方向和結果的叫戰略,主要包括業務規劃、業務模塊和流程以及問題域的列表等。
  • 應用架構:服務復用、跨組協同,簡單、靈活、整合是應用架構必須考慮的點,就像你要上線一個聊天功能,那么聊天內容的輸入法、文字識別、輿情監控以及視頻服務、支付服務等,它們都是在應用架構分層下沉淀到平臺的產物,在供各個方使用。
  • 產品架構:業務提需求,產品定方案,相對于業務的粗放流程,產品架構會更加細膩以及考慮各個模塊的分層和邊界。
  • 數據架構:數據的獲取、數據的存放和數據的使用是數據架構要解決的三個問題,數據庫存放、大數據匯總、數據分析等。
  • 技術架構:是離程序員最近的架構設計,它不僅是系統搭建的架構圖設計,還包括了結構、功能、流程、邏輯等內容。它的具體描述就是整個系統如何落地的具體實現方案。

三、Zachman框架是什么?

Zachman框架,由約翰 扎科曼(John Zachman )在1987年創立的全球第一個企業架構理論,其論文《信息系統架構框架》至今仍被業界認為是企業架構設計方面最權威的理論。

Zachman框架(Zachman framework)是一種邏輯結構,它可以對企業信息按照不同分類和不同角度進行表示。

Zachman框架,從橫向六個角度看待企業,這個六個觀點可以分為;什么內容、如何工作、什么地點、誰負責、為什么這么做(稱為W5H)。

框架的列由一組工件組成,分為規劃者、擁有者、設計者(架構師)、建造者、分包者、產品,或者有時表示為視點:范圍上下文,業務概念,系統邏輯,技術,物理,組件組裝和操作類。整體如圖 26-2 TOGAF Zachman框架

圖 26-2 TOGAF Zachman框架,小傅哥根據描述重新繪制

表格橫向六項 代表了用于描述信息系統的某一個方面,對于任何一個事物只要在這幾個基本方面對其進行清洗的解釋就足夠可以描述清楚。

  • 數據(What,即什么內容):什么是業務數據,信息或對象?
  • 功能(How,即如何工作):業務如何運作,即什么是業務流程?
  • 網絡(Where,即何處):企業運營、部署在哪里?
  • (Who,即何人負責):什么人?什么是業務部門及其等級制度?
  • 時間(When,即什么時間):業務計劃和工作流程是什么?什么時候執行?
  • 原因(Why,即為什么做):為什么選擇的解決方案?這是怎么產生的?

表格縱向六項 代表了在信息系統構造過程中所涉及到的人在描述信息系統時所采用的視角,包括:

  • 范圍/規劃者(Planner):此視圖描述了業務目的和策略,充當其他視圖將被派生和管理的上下文。
  • 業務模型/擁有者(Owner):這是對信息系統必須在其中運作的組織的描述。
  • 系統模型/設計師(Designer):該視圖概述了系統如何滿足組織的信息需求。
  • 技術模型/建造者(Builder):這是系統如何實施的表示,它使特定的解決方案和技術顯而易見。
  • 詳細表述/分包者(Sub-Contractor):這些表示說明了某些系統元素的特定于實現的細節:在生產開始之前需要進一步說明的部分。
  • 功能系統/產品(Functioning Enterprise):在1987年的論文(《A framework for information systems architecture》)中并沒有這一行的內容,實際上此行的內容也并不在架構描述的范疇的之內,不過為了使得架構Zachman框架對于架構的表述更加完備,這一行最終還是被加了進去。

根據 TOGAF 的定義,企業是具有一系列共同目標組織的集合,而架構則是為了有效地實現這一系列目標。

在實現的過程中 定義了企業的結構和運作模式的概念藍圖(SearchCIO),以及構成企業的所有關鍵元素和其關系的綜合描述(Zachman)。通過創建、溝通和優化用以描述企業未來狀態和發展的關鍵原則和模型以將業務愿景和戰略轉化成有效的企業變更的過程(Gartner)。

可以這一部分內容會比較繞,但可以作為架構設計的知識擴展進行學習理解以及運用。

四、陪你畫個架構圖

簡單來說,架構圖就是為了達成交流共識的實現方案演示,并不一定非得拘泥于某種形式,只要你能畫的清楚,講的明白就最合適不過了。

1. 架構選型圖

架構選型圖

  • 難度:???
  • 作用:通常在新項目開發初期,都要做一些技術選型工作。在負載、網關、架構、治理、框架、服務、數據以及環境和支撐服務上,要選擇適合當前開發的技術。

2. 微服務架構

微服務架構,簡化版

  • 難度:????
  • 作用:技術選型完畢后,接下來就是對于這些技術的運用。這個過程有點像搭積木一樣,把每一個區域用適合此位置的積木填充進去。如果是團隊初建或者是技術升級,那么這個過程還是比較復雜的,需要大量的驗證。不過其實互聯網的技術分層和使用已經相對穩定,搭建一個這樣的微服務并不會耗費太長的時間。

3. 技術架構圖

技術架構圖

  • 難度:????
  • 作用:技術架構圖主要是對于研發層面做技術實現指導的,它可以把系統分層和實現結構劃分清楚。另外一般也會把案例工程的結構拿出來一起講解,這樣可以讓團隊伙伴快速的進入開發。

五、總結

  • 本章節向大家講解了什么是架構圖,架構圖的分類和怎么畫架構圖,通過這樣的內容可以讓大家對架構圖有一個全貌的認知。在以后自己畫架構圖了也可以非常明確的知道面對的什么用戶群體,要畫的內容是什么。
  • TOGAF有一套非常完善的企業架構理論,它描述了一種開發和管理企業體系結構生命周期的方法,并構成了TOGAF的核心。所涉及到的知識非常豐富,值得認真看一下。
  • 好看,能把一件事做的好看非常重要,好看能讓人提起興趣、好看可以使溝通成本降低。也鼓勵大家盡可能把經過自己手里的東西,做的好看一些。

六、系列推薦

查看原文

贊 61 收藏 41 評論 4

冷星 發布了文章 · 2月20日

5個不常提及的HTML技巧

2021年你需要知道的HTML標簽和屬性

Web開發人員都在廣泛的使用HTML。無論你使用什么框架或者選擇哪個后端語言,框架在變,但是HTML始終如一。盡管被廣泛使用,但還是有一些標簽或者屬性是大部分開發者不熟知的。雖然現在有很多的模版引擎供我們使用,但是我們還是需要盡可能的熟練掌握HTML內容,就像CSS一樣。

在我看來,最好盡可能使用HTML特性來實現我們的功能,而不是使用JavaScript實現相同的功能,盡管我承認編寫HTML可能會是重復的和無聊的。

盡管許多開發人員每天都在使用HTML,但他們并沒有嘗試改進自己的項目,也沒有真正利用HTML的一些鮮為人知的特性。

下面這5個通過HTML標簽/屬性實現的功能我覺得需要了解一下:

圖片懶加載

圖片懶加載可以幫助提升網站的性能和響應能力。圖片懶加載可以避免立即加載那些不在屏幕中立即顯示的圖片素材,當用戶滾動臨近圖片時再去開始加載。

換言之,當用戶滾動到圖片出現時再進行加載,否則不加載。這就降低了屏幕內容展示過程中的圖片素材的請求數量,提升了站點性能。

往往我們都是通過javascript來實現的,通過監聽頁面滾動事件來確定加載對應的資源。但是,在不完全考慮兼容性的場景下,我們其實可以直接通過HTML來直接實現。

注:本篇的提到的標簽和屬性的兼容性需要大家根據實際場景來選取是否使用

可以通過為圖片文件添加loading="lazy"的屬性來實現:

<img data-original="image.png" loading="lazy" alt="lazy" width="200" height="200" />

輸入提示

當用戶在進行輸入搜索功能時,如果能夠給出有效的提示,這會大大提升用戶體驗。輸入建議和自動完成功能現在到處可見,我們可以使用Javascript添加輸入建議,方法是在輸入框上設置事件偵聽器,然后將搜索到的關鍵詞與預定義的建議相匹配。

其實,HTML也是能夠讓我們來實現預定義輸入建議功能的,通過<datalist>標簽來實現。需要注意的是,使用時這個標簽的id屬性需要和input元素的list屬性一致。

<label for="country">請選擇喜歡的國家:</label>
<input list="countries" name="country" id="country">
<datalist id="countries">
  <option value="UK">
  <option value="Germany">
  <option value="USA">
  <option value="Japan">
  <option value="India">
  <option value=“China”>
</datalist>

Picture標簽

你是否遇到過在不同場景或者不同尺寸的設備上面的時候,圖片展示適配問題呢?我想大家都遇到過。

針對只有一個尺寸的圖片素材的時候,我們往往可以通過CSS的object-fit屬性來進行裁切適配。但是有些時候需要針對不同的分辨率來顯示不同尺寸的圖片的場景的時候,我們是否可以直接通過HTML來實現呢?

HTML提供了<picture>標簽,允許我們來添加多張圖片資源,并且根據不同的分辨率需求來展示不同的圖片。

<picture>
  <source media="(min-width:768px)" srcset="med_flower.jpg">
  <source media="(min-width:495px)" srcset="small_flower.jpg">
  <img data-original="high_flower" style="width: auto;" />
</picture>

我們可以定義不同區間的最小分辨率來確定圖片素材,這個標簽的使用有些類似<audio><video>標簽。

Base URL

當我們的頁面有大量的錨點跳轉或者靜態資源加載時,并且這些跳轉或者資源都在統一的域名的場景時,我們可以通過<base>標簽來簡化這個處理。
例如,我們有一個列表需要跳轉到微博的不同大V的主頁,我們就可以通過設置<base>來簡化跳轉路徑

<head>
  <base  target="_blank">  
</head>
<body>
  <a href="jackiechan">成龍</a>
  <a href="kukoujialing">賈玲</a>
</body>

<base>標記必須具有hreftarget屬性。

頁面重定向(刷新)

當我們希望實現一段時間后或者是立即重定向到另一個頁面的功能時,我們可以直接通過HTML來實現。

我們經常會遇到有些站點會有這樣一個功能,“5s后頁面將跳轉”。這個交互可以嵌入到HTML中,直接通過<meta>標簽,設置http-equiv="refresh"來實現

<meta http-equiv="refresh" content="4; URL='https://google.com' />

這里content屬性指定了重定向發生的秒數。值得一提的是,盡管谷歌聲稱這種形式的重定向和其他的重定向方式一樣可用,但是使用這種類型的重定向其實并不是那么的優雅,往往會顯得很突兀。
因此,最好在某些特殊的情況下使用它,比如在長時間用戶不活動之后再重定向到目標頁面。

后記

HTML和CSS是非常強大的,哪怕我們僅僅使用這兩種技術也能創建出一些奇妙的網站。雖然它們的使用量很大很普遍,還是有很多的開發者并沒有真正的深入了解他們,還有很多的內容需要我們深入的去學習和理解,實踐,有很多的技巧等待著我們去發現。

查看原文

贊 37 收藏 28 評論 2

冷星 收藏了文章 · 2020-11-02

手寫Express.js源碼

上一篇文章我們講了怎么用Node.js原生API來寫一個web服務器,雖然代碼比較丑,但是基本功能還是有的。但是一般我們不會直接用原生API來寫,而是借助框架來做,比如本文要講的Express。通過上一篇文章的鋪墊,我們可以猜測,Express其實也沒有什么黑魔法,也僅僅是原生API的封裝,主要是用來提供更好的擴展性,使用起來更方便,代碼更優雅。本文照例會從Express的基本使用入手,然后自己手寫一個Express來替代他,也就是源碼解析。

本文可運行代碼已經上傳GitHub,拿下來一邊玩代碼,一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

簡單示例

使用Express搭建一個最簡單的Hello World也是幾行代碼就可以搞定,下面這個例子來源官方文檔:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

可以看到Express的路由可以直接用app.get這種方法來處理,比我們之前在http.createServer里面寫一堆if優雅多了。我們用這種方式來改寫下上一篇文章的代碼:

const path = require("path");
const express = require("express");
const fs = require("fs");
const url = require("url");

const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.end("Hello World");
});

app.get("/api/users", (req, res) => {
  const resData = [
    {
      id: 1,
      name: "小明",
      age: 18,
    },
    {
      id: 2,
      name: "小紅",
      age: 19,
    },
  ];
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify(resData));
});

app.post("/api/users", (req, res) => {
  let postData = "";
  req.on("data", (chunk) => {
    postData = postData + chunk;
  });

  req.on("end", () => {
    // 數據傳完后往db.txt插入內容
    fs.appendFile(path.join(__dirname, "db.txt"), postData, () => {
      res.end(postData); // 數據寫完后將數據再次返回
    });
  });
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}/`);
});

Express還支持中間件,我們寫個中間件來打印出每次請求的路徑:

app.use((req, res, next) => {
  const urlObject = url.parse(req.url);
  const { pathname } = urlObject;

  console.log(`request path: ${pathname}`);

  next();
});

Express也支持靜態資源托管,不過他的API是需要指定一個文件夾來單獨存放靜態資源的,比如我們新建一個public文件夾來存放靜態資源,使用express.static中間件配置一下就行:

app.use(express.static(path.join(__dirname, 'public')));

然后就可以拿到靜態資源了:

image.png

手寫源碼

手寫源碼才是本文的重點,前面的不過是鋪墊,本文手寫的目標就是自己寫一個express來替換前面用到的express api,其實就是源碼解析。在開始之前,我們先來看看用到了哪些API

  1. express(),第一個肯定是express函數,這個運行后會返回一個app的實例,后面用的很多方法都是這個app上的。
  2. app.listen,這個方法類似于原生的server.listen,用來啟動服務器。
  3. app.get,這是處理路由的API,類似的還有app.post等。
  4. app.use,這是中間件的調用入口,所有中間件都要通過這個方法來調用。
  5. express.static,這個中間件幫助我們做靜態資源托管,其實是另外一個庫了,叫serve-static,因為跟Express架構關系不大,本文就先不講他的源碼了。

本文所有手寫代碼全部參照官方源碼寫成,方法名和變量名盡量與官方保持一致,大家可以對照著看,寫到具體的方法時我也會貼出官方源碼的地址。

express()

首先需要寫的肯定是express(),這個方法是一切的開始,他會創建并返回一個app,這個app就是我們的web服務器。

// express.js
var mixin = require('merge-descriptors');
var proto = require('./application');

// 創建web服務器的方法
function createApplication() {
  // 這個app方法其實就是傳給http.createServer的回調函數
  var app = function (req, res) {

  };

  mixin(app, proto, false);

  return app;
}

exports = module.exports = createApplication;

上述代碼就是我們在運行express()的時候執行的代碼,其實就是個空殼,返回的app暫時是個空函數,真正的app并沒在這里,而是在proto上,從上述代碼可以看出proto其實就是application.js,然后通過下面這行代碼將proto上的東西都賦值給了app

mixin(app, proto, false);

這行代碼用到了一個第三方庫merge-descriptors,這個庫總共沒有幾行代碼,做的事情也很簡單,就是將proto上面的屬性挨個賦值給app,對merge-descriptors源碼感興趣的可以看這里:https://github.com/component/merge-descriptors/blob/master/index.js。

Express這里之所以使用mixin,而不是普通的面向對象來繼承,是因為它除了要mixin proto外,還需要mixin其他庫,也就是需要多繼承,我這里省略了,但是官方源碼是有的。

express.js對應的源碼看這里:https://github.com/expressjs/express/blob/master/lib/express.js

app.listen

上面說了,express.js只是一個空殼,真正的appapplication.js里面,所以app.listen也是在這里。

// application.js

var app = exports = module.exports = {};

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

上面代碼就是調用原生http模塊創建了一個服務器,但是傳的參數是this,這里的this是什么呢?回想一下我們使用express的時候是這樣用的:

const app = express();

app.listen(3000);

所以listen方法的實際調用者是express()的返回值,也就是上面express.js里面createApplication的返回值,也就是這個函數:

var app = function (req, res) {
};

所以這里的this也是這個函數,所以我在express.js里面就加了注釋,這個函數是http.createServer的回調函數?,F在這個函數是空的,實際上他應該是整個web服務器的處理入口,所以我們給他加上處理的邏輯,在里面再加一行代碼:

var app = function(req, res) {
  app.handle(req, res);    // 這是真正的服務器處理入口
};

app.handle

app.handle也是掛載在app下面的,所以他實際也在application.js這個文件里面,下面我們來看看他干了什么:

app.handle = function handle(req, res) {
  var router = this._router;

  // 最終的處理方法
  var done = finalhandler(req, res);

  // 如果沒有定義router
  // 直接結束返回
  if (!router) {
    done();
    return;
  }

  // 有router,就用router來處理
  router.handle(req, res, done);
}

上面代碼可以看出,實際處理路由的是router,這是Router的一個實例,并且掛載在this上的,我們這里還沒有給他賦值,如果沒有賦值的話,會直接運行finalhandler并且結束處理。finalhandler也是一個第三方庫,GitHub鏈接在這里:https://github.com/pillarjs/finalhandler。這個庫的功能也不復雜,就是幫你處理一些收尾的工作,比如所有路由都沒匹配上,你可能需要返回404并記錄下error log,這個庫就可以幫你做。

app.get

上面說了,在具體處理網絡請求時,實際上是用app._router來處理的,那么app._router是在哪里賦值的呢?事實上app._router的賦值有多個地方,一個地方就是HTTP動詞處理方法上,比如我們用到的app.get或者app.post。無論是app.get還是app.post都是調用的router方法來處理,所以可以統一用一個循環來寫這一類的方法。

// HTTP動詞的方法
var methods = ['get', 'post'];
methods.forEach(function (method) {
  app[method] = function (path) {
    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, Array.prototype.slice.call(arguments, 1));
    return this;
  }
});

上面代碼HTTP動詞都放到了一個數組里面,官方源碼中這個數組也是一個第三方庫維護的,名字就叫methods,GitHub地址在這里:https://github.com/jshttp/methods。我這個例子因為只需要兩個動詞,就簡化了,直接用數組了。這段代碼其實給app創建了跟每個動詞同名的函數,所有動詞的處理函數都是一樣的,都是去調router里面的對應方法來處理。這種將不同部分抽取出來,從而復用共同部分的代碼,有點像我之前另一篇文章寫過的設計模式----享元模式。

我們注意到上面代碼除了調用router來處理路由外,還有一行代碼:

this.lazyrouter();

lazyrouter方法其實就是我們給this._router賦值的地方,代碼也比較簡單,就是檢測下有沒有_router,如果沒有就給他賦個值,賦的值就是Router的一個實例:

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router();
  }
}

app.listen,app.handlemethods處理方法都在application.js里面,application.js源碼在這里:https://github.com/expressjs/express/blob/master/lib/application.js

Router

寫到這里我們發現我們已經使用了Router的多個API,比如:

  1. router.handle
  2. router.route
  3. route[method]

所以我們來看下Router這個類,下面的代碼是從源碼中簡化出來的:

// router/index.js
var setPrototypeOf = require('setprototypeof');

var proto = module.exports = function () {
  function router(req, res, next) {
    router.handle(req, res, next);
  }

  setPrototypeOf(router, proto);

  return router;
}

這段代碼對我來說是比較奇怪的,我們在執行new Router()的時候其實執行的是new proto(),new proto()并不是我奇怪的地方,奇怪的是他設置原型的方式。我之前在講JS的面向對象的文章提到過如果你要給一個類加上類方法可以這樣寫:

function Class() {}

Class.prototype.method1 = function() {}

var instance = new Class();

這樣instance.__proto__就會指向Class.prototype,你就可使用instance.method1了。

Express.js的上述代碼其實也是實現了類似的效果,setprototypeof又是一個第三方庫,作用類似Object.setPrototypeOf(obj, prototype),就是給一個對象設置原型,setprototypeof存在的意義就是兼容老標準的JS,也就是加了一些polyfill,他的代碼在這里。所以:

setPrototypeOf(router, proto);

這行代碼的意思就是讓router.__proto__指向proto,router是你在new proto()時的返回對象,執行了上面這行代碼,這個router就可以拿到proto上的全部方法了。像router.handle這種方法就可以掛載到proto上了,成為proto.handle。

繞了一大圈,其實就是JS面向對象的使用,給router添加類方法,但是為什么使用這么繞的方式,而不是像我上面那個Class那樣用呢?這我就不是很清楚了,可能有什么歷史原因吧。

路由架構

Router的基本結構知道了,要理解Router的具體代碼,我們還需要對Express的路由架構有一個整體的認識。就以我們這兩個示例API來說:

get /api/users

post /api/users

我們發現他們的path是一樣的,都是/api/users,但是他們的請求方法,也就是method不一樣。Express里面將path這一層提取出來作為了一個類,叫做Layer。但是對于一個Layer,我們只知道他的path,不知道method的話,是不能確定一個路由的,所以Layer上還添加了一個屬性route,這個route上也存了一個數組,數組的每個項存了對應的method和回調函數handle。整個結構你可以理解成這個樣子:

const router = {
  stack: [
    // 里面很多layer
    {
      path: '/api/users'
      route: {
          stack: [
          // 里面存了多個method和回調函數
          {
            method: 'get',
            handle: function1
          },
          {
            method: 'post',
            handle: function2
          }
        ]
        }
    }
  ]
}

知道了這個結構我們可以猜到,整個流程可以分成兩部分:注冊路由匹配路由。當我們寫app.getapp.post這些方法時,其實就是在router上添加layerroute。當一個網絡請求過來時,其實就是遍歷layerroute,找到對應的handle拿出來執行。

注意route數組里面的結構,每個項按理來說應該使用一種新的數據結構來存儲,比如routeItem之類的。但是Express并沒有這樣做,而是將它和layer合在一起了,給layer添加了methodhandle屬性。這在初次看源碼的時候可能造成困惑,因為layer同時存在于routerstack上和routestack上,肩負了兩種職責。

router.route

這個方法是我們前面注冊路由的時候調用的一個方法,回顧下前面的注冊路由的方法,比如app.get

app.get = function (path) {
  this.lazyrouter();

  var route = this._router.route(path);
  route.get.apply(route, Array.prototype.slice.call(arguments, 1));
  return this;
}

結合上面講的路由架構,我們在注冊路由的時候,應該給router添加對應的layerroute,router.route的代碼就不難寫出了:

proto.route = function route(path) {
  var route = new Route();
  var layer = new Layer(path, route.dispatch.bind(route));     // 參數是path和回調函數

  layer.route = route;

  this.stack.push(layer);

  return route;
}

Layer和Route構造函數

上面代碼新建了RouteLayer實例,這兩個類的構造函數其實也挺簡單的。只是參數的申明和初始化:

// layer.js
module.exports = Layer;

function Layer(path, fn) {
  this.path = path;

  this.handle = fn;
  this.method = '';
}
// route.js
module.exports = Route;

function Route() {
  this.stack = [];
  this.methods = {};    // 一個加快查找的hash表
}

route.get

前面我們看到了app.get其實通過下面這行代碼,最終調用的是route.get

route.get.apply(route, Array.prototype.slice.call(arguments, 1));

也知道了route.get這種動詞處理函數,其實就是往route.stack上添加layer,那我們的route.get也可以寫出來了:

var methods = ["get", "post"];
methods.forEach(function (method) {
  Route.prototype[method] = function () {
    // 支持傳入多個回調函數
    var handles = flatten(slice.call(arguments));

    // 為每個回調新建一個layer,并加到stack上
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      // 每個handle都應該是個函數
      if (typeof handle !== "function") {
        var type = toString.call(handle);
        var msg =
          "Route." +
          method +
          "() requires a callback function but got a " +
          type;
        throw new Error(msg);
      }

      // 注意這里的層級是layer.route.layer
      // 前面第一個layer已經做個path的比較了,所以這里是第二個layer,path可以直接設置為/
      var layer = new Layer("/", handle);
      layer.method = method;
      this.methods[method] = true; // 將methods對應的method設置為true,用于后面的快速查找
      this.stack.push(layer);
    }
  };
});

這樣,其實整個router的結構就構建出來了,后面就看看怎么用這個結構來處理請求了,也就是router.handle方法。

router.handle

前面說了app.handle實際上是調用的router.handle,也知道了router的結構是在stack上添加了layerrouter,所以router.handle需要做的就是從router.stack上找出對應的layerrouter并執行回調函數:

// 真正處理路由的函數
proto.handle = function handle(req, res, done) {
  var self = this;
  var idx = 0;
  var stack = self.stack;

  // next方法來查找對應的layer和回調函數
  next();
  function next() {
    // 使用第三方庫parseUrl獲取path,如果沒有path,直接返回
    var path = parseUrl(req).pathname;
    if (path == null) {
      return done();
    }

    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++]; // 注意這里先執行 layer = stack[idx]; 再執行idx++;
      match = layer.match(path); // 調用layer.match來檢測當前路徑是否匹配
      route = layer.route;

      // 沒匹配上,跳出當次循環
      if (match !== true) {
        continue;
      }

      // layer匹配上了,但是沒有route,也跳出當次循環
      if (!route) {
        continue;
      }

      // 匹配上了,看看route上有沒有對應的method
      var method = req.method;
      var has_method = route._handles_method(method);
      // 如果沒有對應的method,其實也是沒匹配上,跳出當次循環
      if (!has_method) {
        match = false;
        continue;
      }
    }

    // 循環完了還沒有匹配的,就done了,其實就是404
    if (match !== true) {
      return done();
    }

    // 如果匹配上了,就執行對應的回調函數
    return layer.handle_request(req, res, next);
  }
};

上面代碼還用到了幾個LayerRoute的實例方法:

layer.match(path): 檢測當前layerpath是否匹配。

route._handles_method(method):檢測當前routemethod是否匹配。

layer.handle_request(req, res, next):使用layer的回調函數來處理請求。

這幾個方法看起來并不復雜,我們后面一個一個來實現。

到這里其實還有個疑問。從他整個的匹配流程來看,他尋找的其實是router.stack.layer這一層,但是最終應該執行的回調卻是在router.stack.layer.route.stack.layer.handle。這是怎么通過router.stack.layer找到最終的router.stack.layer.route.stack.layer.handle來執行的呢?

這要回到我們前面的router.route方法:

proto.route = function route(path) {
  var route = new Route();
  var layer = new Layer(path, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);

  return route;
}

這里我們new Layer的時候給的回調其實是route.dispatch.bind(route),這個方法會再去route.stack上找到正確的layer來執行。所以router.handle真正的流程其實是:

  1. 找到path匹配的layer
  2. 拿出layer上的route,看看有沒有匹配的method
  3. layermethod都有匹配的,再調用route.dispatch去找出真正的回調函數來執行。

所以又多了一個需要實現的函數,route.dispatch。

layer.match

layer.match是用來檢測當前path是否匹配的函數,用到了一個第三方庫path-to-regexp,這個庫可以將path轉為正則表達式,方便后面的匹配,這個庫在之前寫過的react-router源碼中也出現過。

var pathRegexp = require("path-to-regexp");

module.exports = Layer;

function Layer(path, fn) {
  this.path = path;

  this.handle = fn;
  this.method = "";

  // 添加一個匹配正則
  this.regexp = pathRegexp(path);
  // 快速匹配/
  this.regexp.fast_slash = path === "/";
}

然后就可以添加match實例方法了:

Layer.prototype.match = function match(path) {
  var match;

  if (path != null) {
    if (this.regexp.fast_slash) {
      return true;
    }

    match = this.regexp.exec(path);
  }

  // 沒匹配上,返回false
  if (!match) {
    return false;
  }

  // 不然返回true
  return true;
};

layer.handle_request

layer.handle_request是用來調用具體的回調函數的方法,其實就是拿出layer.handle來執行:

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  fn(req, res, next);
};

route._handles_method

route._handles_method就是檢測當前route是否包含需要的method,因為之前添加了一個methods對象,可以用它來進行快速查找:

Route.prototype._handles_method = function _handles_method(method) {
  var name = method.toLowerCase();

  return Boolean(this.methods[name]);
};

route.dispatch

route.dispatch其實是router.stack.layer的回調函數,作用是找到對應的router.stack.layer.route.stack.layer.handle并執行。

Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack; // 注意這個stack是route.stack

  // 如果stack為空,直接done
  // 這里的done其實是router.stack.layer的next
  // 也就是執行下一個router.stack.layer
  if (stack.length === 0) {
    return done();
  }

  var method = req.method.toLowerCase();

  // 這個next方法其實是在router.stack.layer.route.stack上尋找method匹配的layer
  // 找到了就執行layer的回調函數
  next();
  function next() {
    var layer = stack[idx++];
    if (!layer) {
      return done();
    }

    if (layer.method && layer.method !== method) {
      return next();
    }

    layer.handle_request(req, res, next);
  }
};

到這里其實Express整體的路由結構,注冊和執行流程都完成了,貼下對應的官方源碼:

Router類https://github.com/expressjs/express/blob/master/lib/router/index.js

Layer類https://github.com/expressjs/express/blob/master/lib/router/layer.js

Route類https://github.com/expressjs/express/blob/master/lib/router/route.js

中間件

其實我們前面已經隱含了中間件,從前面的結構可以看出,一個網絡請求過來,會到router的第一個layer,然后調用next到到第二個layer,匹配上layerpath就執行回調,然后一直這樣把所有的layer都走完。所以中間件是啥?中間件就是一個layer,他的path默認是/,也就是對所有請求都生效。按照這個思路,代碼就簡單了:

// application.js

// app.use就是調用router.use
app.use = function use(fn) {
  var path = "/";

  this.lazyrouter();
  var router = this._router;
  router.use(path, fn);
};

然后在router.use里面再加一層layer就行了:

proto.use = function use(path, fn) {
  var layer = new Layer(path, fn);

  this.stack.push(layer);
};

總結

  1. Express也是用原生APIhttp.createServer來實現的。
  2. Express的主要工作是將http.createServer的回調函數拆出來了,構建了一個路由結構Router。
  3. 這個路由結構由很多層layer組成。
  4. 一個中間件就是一個layer。
  5. 路由也是一個layer,layer上有一個path屬性來表示他可以處理的API路徑。
  6. path可能有不同的method,每個method對應layer.route上的一個layer。
  7. layer.route上的layer雖然名字和router上的layer一樣,但是功能側重點并不一樣,這也是源碼中讓人困惑的一個點。
  8. layer.route上的layer的主要參數是methodhandle,如果method匹配了,就執行對應的handle。
  9. 整個路由匹配過程其實就是遍歷router.layer的一個過程。
  10. 每個請求來了都會遍歷一遍所有的layer,匹配上就執行回調,一個請求可能會匹配上多個layer。
  11. 總體來看,Express代碼給人的感覺并不是很完美,特別是Layer類肩負兩種職責,跟軟件工程強調的單一職責原則不符,這也導致Router,Layer,Route三個類的調用關系有點混亂。而且對于繼承和原型的使用都是很老的方式??赡芤彩沁@種不完美催生了Koa的誕生,下一篇文章我們就來看看Koa的源碼吧。
  12. Express其實還對原生的reqres進行了擴展,讓他們變得更好用,但是這個其實只相當于一個語法糖,對整體架構沒有太大影響,所以本文就沒涉及了。

本文可運行代碼已經上傳GitHub,拿下來一邊玩代碼,一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

參考資料

Express官方文檔:http://expressjs.com/

Express官方源碼:https://github.com/expressjs/express/tree/master/lib

文章的最后,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續創作的動力。

歡迎關注我的公眾號進擊的大前端第一時間獲取高質量原創~

“前端進階知識”系列文章源碼地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二維碼_2.png

查看原文

冷星 贊了文章 · 2020-10-21

小程序保存多張圖片(優化版)

前言

之前寫個一個版本的[多圖下載],重構進行了代碼升級讓代碼更加簡介
分為兩步:
第一獲取保存到相冊權限
第二下載圖片
主要涉及兩個文件,index.js 和download.js
另外現在如果有圖片下載失敗也彈出下載完成后續需要優化

核心代碼

/**
 *  獲取相冊權限
 */
export function wxSaveAuth() {
    return new Promise((resolve, reject) => {
        wx.getSetting({
            success(res) {
                if (!res.authSetting['scope.writePhotosAlbum']) {
                    // 如果沒有寫入權限,則獲取寫入相冊權限
                    wx.authorize({
                        scope: 'scope.writePhotosAlbum',
                        success() {
                            resolve()
                        },
                        fail(err) {
                            reject(err)
                            // 用戶拒絕授權
                            wx.showModal({
                                content: '檢測到您沒打開捷買士的相冊權限,是否去設置打開?',
                                confirmText: '確認',
                                cancelText: '取消',
                                success(res) {
                                    if (res.confirm) {
                                        wx.openSetting({
                                            success: res => {}
                                        })
                                    }
                                }
                            })
                        }
                    })
                } else {
                    resolve()
                }
            },
            fail(e) {
                reject(e)
            }
        })
    })
}

/**
 * 多文件下載并且保存 
 * @param {Array} urls 網絡圖片數組
 */
export function downloadImgs(urls) {
    wx.showLoading({
        title: '圖片下載中',
        mask: true
    })
    const imageList = []
    // 循環數組
    for (let i = 0; i < urls.length; i++) {
        imageList.push(getTempPath(urls[i]))
    }
    const loadTask = []
    let index = 0
    while (index < imageList.length) {
        loadTask.push(
            new Promise((resolve, reject) => {
                // 將數據分割成多個promise數組
                Promise.all(imageList.slice(index, (index += 8)))
                    .then(res => {
                        resolve(res)
                    })
                    .catch(err => {
                        reject(err)
                    })
            })
        )
    }
    // Promise.all 所有圖片下載完成后彈出
    Promise.all(loadTask)
        .then(res => {
            wx.showToast({
                title: '下載完成',
                duration: 3000
            })
        })
        .catch(err => {
            wx.showToast({
                title: `下載完成`,
                icon: 'none',
                duration: 3000
            })
        })
}
/**
 *
 * @param {String} url 單張網絡圖片
 */
//下載內容,臨時文件路徑
function getTempPath(url) {
    return new Promise((resolve, reject) => {
        wx.downloadFile({
            url: url,
            success: function(res) {
                var temp = res.tempFilePath
                wx.saveImageToPhotosAlbum({
                    filePath: temp,
                    success: function(res) {
                        return resolve(res)
                    },
                    fail: function(err) {
                        reject(url + JSON.stringify(err))
                    }
                })
            },
            fail: function(err) {
                reject(url + JSON.stringify(err))
            }
        })
    })
}
// pages/save-imgs/index.js
import { wxSaveAuth, downloadImgs } from '../../utils/download'
Page({
    /**
     * 頁面的初始數據
     */
    data: {
        urls: [
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4',
             'https://avatars0.githubusercontent.com/u/35954879?s=120&v=4'
    
        ]
    },

    /**
     * 生命周期函數--監聽頁面加載
     */
    onLoad: function(options) {},

    download() {
        // 獲取保存到相冊權限
        wxSaveAuth().then(res => {
            // 保存多張圖片到相冊
            downloadImgs(this.data.urls)
        })
    },
 
})

項目案例

github地址

git clone https://github.com/sunnie1992/sol-weapp.git

掃描添加下方的微信并備注 Sol 加交流群,交流學習,及時獲取代碼最新動態。
mine.png

如果對你有幫助送我一顆小星星(づ ̄3 ̄)づ╭?~

查看原文

贊 1 收藏 0 評論 1

冷星 贊了文章 · 2020-09-22

基于Taro的微信小程序分享圖片功能實踐

前言

在各種小程序(微信、百度、支付寶)、H5、NativeApp 紛紛擾擾的當下,給大家強烈安利一款基于React的多終端開發利器:京東Taro(泰羅·奧特曼),Taro致力于多終端統一解決方案,一處代碼,多處運行。

Taro支持以React語言開發小程序,支持CSS預處理器,支持實時編譯更新,支持NPM,等等等等,簡直不要太爽!

微信小程序分享圖片功能是經常在小程序業務中出現的,比如學習打卡分享,推廣會員分享,推廣商品分享等等。因為小程序是不支持直接分享圖片到朋友圈的,一般操作為:

  1. 生成包含小程序碼(當前也可以是其他特定的信息)的圖片;
  2. 用戶點擊保存圖片,下載到本地,再發布到朋友圈;
  3. 其他用戶長按識別該小程序碼,進入當前小程序。

今天胡哥給大家分享下,基于Taro框架實現微信小程序分享圖片功能的實踐。

一、搭建Taro項目框架,創建微信小程序

1. 安裝taro腳手架工具

npm install -g @tarojs/cli

2. 初始化taro項目

taro init taro-img-share

3. 編譯項目,開啟Dev模式,生成小程序 -- dist目錄

npm run dev:weapp

4. 微信開發者工具,創建小程序,選擇項目根目錄為taro-img-share下的dist目錄

二、小程序分享圖片功能實踐 --- 打卡圖片分享功能

先上圖,再說話

效果圖
點擊保存到相冊

這是重點:使用Canvas繪制圖片并展示,保存圖片到相冊

drawImage()方法負責繪制展示,saveCard()方法負責下載圖片到相冊

src/pages/index/index.js

import Taro, { Component } from '@tarojs/taro'
// 引入對應的組件
import { View, Text, Button, Canvas } from '@tarojs/components'
import './index.scss'

export default class Index extends Component {

  config = {
    navigationBarTitleText: '首頁'
  }

  /**
  * 初始化信息
  */
  constructor () {
    this.state = {
      // 用戶信息
      userInfo: {},
      // 是否展示canvas
      isShowCanvas: false
    }
  }

  /**
   * getUserInfo() 獲取用戶信息
   */
  getUserInfo (e) {
    if (!e.detail.userInfo) {
      Taro.showToast({
        title: '獲取用戶信息失敗,請授權',
        icon: 'none'
      })
      return
    }
    this.setState({
      isShowCanvas: true,
      userInfo: e.detail.userInfo
    }, () => {
      // 調用繪制圖片方法
      this.drawImage()
    })
  }

  /**
   * drawImage() 定義繪制圖片的方法
   */
  async drawImage () {
    // 創建canvas對象
    let ctx = Taro.createCanvasContext('cardCanvas')
    
    // 填充背景色
    let grd = ctx.createLinearGradient(0, 0, 1, 500)
    grd.addColorStop(0, '#1452d0')
    grd.addColorStop(0.5, '#FFF')
    ctx.setFillStyle(grd)
    ctx.fillRect(0, 0, 400, 500)

    // // 繪制圓形用戶頭像
    let { userInfo } = this.state
    let res = await Taro.downloadFile({
      url: userInfo.avatarUrl
    })
    ctx.save()
    ctx.beginPath()
    // ctx.arc(160, 86, 66, 0, Math.PI * 2, false)
    ctx.arc(160, 88, 66, 0, Math.PI * 2)
    ctx.closePath()
    ctx.clip()
    ctx.stroke()
    ctx.translate(160, 88)
    ctx.drawImage(res.tempFilePath, -66, -66, 132, 132)
    ctx.restore()

    // 繪制文字
    ctx.save()
    ctx.setFontSize(20)
    ctx.setFillStyle('#FFF')
    ctx.fillText(userInfo.nickName, 100, 200)
    ctx.setFontSize(16)
    ctx.setFillStyle('black')
    ctx.fillText('已在胡哥有話說公眾號打卡20天', 50, 240)
    ctx.restore()

    // 繪制二維碼
    let qrcode = await Taro.downloadFile({
      url: 'https://upload-images.jianshu.io/upload_images/3091895-f0b4b900390aec73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/258/format/webp.jpg'
    })
    ctx.drawImage(qrcode.tempFilePath, 70, 260, 180, 180)

    // 將以上繪畫操作進行渲染
    ctx.draw()
  }

  /**
   * saveCard() 保存圖片到本地
   */
  async saveCard () {
    // 將Canvas圖片內容導出指定大小的圖片
    let res = await Taro.canvasToTempFilePath({
      x: 0,
      y: 0,
      width: 400,
      height: 500,
      destWidth: 360,
      destHeight: 450,
      canvasId: 'cardCanvas',
      fileType: 'png'
    })
    let saveRes = await Taro.saveImageToPhotosAlbum({
      filePath: res.tempFilePath
    })
    if (saveRes.errMsg === 'saveImageToPhotosAlbum:ok') {
      Taro.showModal({
        title: '圖片保存成功',
        content: '圖片成功保存到相冊了,快去發朋友圈吧~',
        showCancel: false,
        confirmText: '確認'
      })
    } else {
        Taro.showModal({
        title: '圖片保存失敗',
        content: '請重新嘗試!',
        showCancel: false,
        confirmText: '確認'
      })
    }
  }

  render () {
    let { isShowCanvas } = this.state
    return (
      <View className='index'>
        <Button onGetUserInfo={this.getUserInfo} openType="getUserInfo" type="primary" size="mini">打卡</Button>
        {/* 使用Canvas繪制分享圖片 */}
        {
          isShowCanvas && 
            <View className="canvas-wrap">
              <Canvas 
                id="card-canvas"
                className="card-canvas"
                style="width: 320px; height: 450px"
                canvasId="cardCanvas" >
              </Canvas>
              <Button onClick={this.saveCard} className="btn-save" type="primary" size="mini">保存到相冊</Button>
            </View> 
        }
      </View>
    )
  }
}

src/pages/index/index.sass

index.js組件中的css樣式
.index {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.canvas-wrap {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  .btn-save {
    margin-top: 40rpx;
  }
}

注意事項

設置Taro支持ES6的async/await

  1. 下載@tarojs/async-await

    npm install -D @tarojs/async-await
  2. app.js中引入

    import '@tarojs/async-await'

開發完畢,發布小程序

  1. 執行打包,生成最終的小程序源碼

    npm run build:weapp
  2. 發布小程序

    點擊微信開發者工具上傳按鈕,上傳小程序,然后在微信小程序平臺發布小程序即可。

小結

  1. 本文著重介紹了使用Taro實現小程序生成打卡圖片,保存相冊,分享圖片功能的開發原理與實踐步驟,各位童鞋可參考并結合自己的實際業務進行擴展開發。
  2. 本文并為深入的對不同手機進行圖片適配,部分值也是設置的固定值(如填充文字的開始坐標與填充的文字長度、大?。?,并未做比例響應。需要進一步交流的小伙伴,可以關注胡哥有話說公眾號,持續關注相關文章,也可直接在文章留言交流。
  3. 無論是使用何種框架如Taro、mpvue、wepy等開發小程序分享圖片功能,原理都是一樣的,只不過是在調用方法以及處理邏輯時需要進行響應的小調整
掌握業務功能實現原理,是制勝一切的法寶!

如何獲取該案例的源代碼

  1. 關注胡哥有話說公眾號
  2. 回復關鍵字Taro即可,會收到項目源碼地址

后記

以上就是胡哥今天給大家分享的內容,喜歡的小伙伴記得收藏、轉發、點擊右下角按鈕在看,推薦給更多小伙伴呦,歡迎多多留言交流...

胡哥有話說,一個有技術,有情懷的胡哥!京東開放平臺首席前端攻城獅。與你一起聊聊大前端,分享前端系統架構,框架實現原理,最新最高效的技術實踐!

長按掃碼關注,更帥更漂亮呦!關注胡哥有話說公眾號,可與胡哥繼續深入交流呦!

胡哥有話說

查看原文

贊 3 收藏 2 評論 0

冷星 贊了文章 · 2020-09-22

Taro 開發日記-3 首頁登錄

實現目標

首頁希望做成 沒有信息的時候顯示空,點擊獲取 用戶信息之后
能夠顯示當前用戶的昵稱 和頭像
image.png
image.png

使用mobx

1.首先 在store文件夾下創建一個 unserInfo.ts 文件,這里store下不需要用掉的counter.ts 可以刪掉了。

// 引入observable;
import { observable } from 'mobx';

const userInfoStore = observable({
  // 存儲用戶基本信息的對象
  userInfo: {},
  // 寫入信息的方法
  saveInfo(userInfo: userinfo) {
    if (this!.userInfo) {
      this.userInfo = Object.assign({}, this.userInfo, userInfo);
    }
  }
});
export default userInfoStore;
// src\app.tsx
// src\app.tsx 下引入 userInfoStore
import userInfoStore from './store/userInfo';

const store = {
  userInfoStore
};
// src\pages\index\index.tsx
// 引入mobx的修飾器
import { observer, inject } from '@tarojs/mobx';
import './index.scss';


// 編輯 src\pages\index\index.tsx 下的props接口類型
type PageStateProps = {
  // 添加 userInfoStore
  userInfoStore: {
    userInfo: any;
    saveInfo: Function;
  };
};

interface Index {
  props: PageStateProps;
}

// 注入 userInfoStore
@inject('userInfoStore')
@observer
class Index extends Component {}

附1:@inject修飾器
image.png
可以看到 inject 修飾器 會將 指定store作為頁面組件的props 傳入頁面。所以這里的 userInfoStore 是在props 傳入了 indexPage

修改 srcpagesindexindex.tsx 文件

import { ComponentType } from 'react';
import Taro, { Component, Config } from '@tarojs/taro';
import { View, Button } from '@tarojs/components';
import { AtAvatar } from 'taro-ui';

// 引入mobx的修飾器
import { observer, inject } from '@tarojs/mobx';
import './index.scss';

// 編輯 src\pages\index\index.tsx 下的props接口類型
type PageStateProps = {
  // 添加 userInfoStore
  userInfoStore: {
    userInfo: userinfo;
    saveInfo: Function;
  };
};

interface Index {
  props: PageStateProps;
}

// 注入 userInfoStore
@inject('userInfoStore')
@observer
class Index extends Component {
  config: Config = {
    navigationBarTitleText: '首頁'
  };

  /**
   * @description: 獲取用戶信息
   * @param {Object} detail onGetUserInfo 所返回的用戶基本信息
   * @return null
   */
  getUserInfo({ detail }) {
    const { userInfoStore } = this.props;
    userInfoStore.saveInfo(detail.userInfo);
  }

  render() {
    const { userInfo } = this.props.userInfoStore;
    return (
      <View className='index'>
        {/* button使用 getUserInfo 可以在小程序中獲取用戶基本信息 */}
        <Button
          openType='getUserInfo'
          onGetUserInfo={this.getUserInfo.bind(this)}
          className='index-avatarUrl-btn nobtn-style'
        >
          <View className='avatar-box'>
            <AtAvatar
              image={userInfo.avatarUrl}
              size='large'
              circle
              text={userInfo.avatarUrl ? '' : '空'}
            ></AtAvatar>
          </View>

          <View className='nickName-box'>
            {userInfo.nickName || '點擊登錄'}
          </View>
        </Button>
      </View>
    );
  }
}

export default Index as ComponentType;
// src\pages\index\index.scss
@mixin cubeBox($w) {
  width: $w;
  height: $w;
  line-height: $w;
}

.index {
  .index-avatarUrl-btn {
    text-align: center;
    margin: 200px auto;
  }

  .avatar-box {
    display: inline-block;
    .at-avatar--large {
      @include cubeBox(240px);
    }
  }
  .nickName-box {
    font-size: 28px; /*px*/
    color: #ddd;
  }
}

// 移除button的原生樣式
.nobtn-style {
  border: none;
  background-color: transparent;
  line-height: normal;
  padding: 0;
  margin: 0;
  text-align: left;
}
.nobtn-style::after {
  border: none;
}

至此 頁面 可以通過點擊頭像,獲取到用戶信息并存儲在 mobx 中;

本地存儲數據

之前,登陸頁面可以獲取并顯示用戶的頭像了
但是,我們不可能每次使用的時候都要用戶去授權頭像和昵稱,所以需要找個地方把用戶的信息存起來。

所以先修改 userInfoStore

// src\store\userInfo.ts
// 引入observable;
import { observable } from 'mobx';
import { setStorageSync, getStorageSync } from '@tarojs/taro';

const userInfoStore = observable({
  // 存儲用戶基本信息的對象
  userInfo: {},
  // 寫入信息的方法
  saveInfo(userInfo: userinfo) {
    if (this!.userInfo) {
      this.userInfo = Object.assign({}, this.userInfo, userInfo);
      // 保持用戶信息的時候將數據存儲到本地
      // setStorageSync 文檔 https://taro-docs.jd.com/taro/docs/apis/storage/setStorageSync
      setStorageSync('userInfo', this.userInfo);
    }
  },
  // 從本地讀取用戶信息
  readInfo() {
    const userInfo = getStorageSync('userInfo');
    this.userInfo = Object.assign({}, this.userInfo, userInfo);
  }
});
export default userInfoStore;

修改 srcpagesindexindex.tsx

import { ComponentType } from 'react';
import Taro, { Component, Config } from '@tarojs/taro';
import { View, Button } from '@tarojs/components';
import { AtAvatar } from 'taro-ui';

// 引入mobx的修飾器
import { observer, inject } from '@tarojs/mobx';
import './index.scss';

// 編輯 src\pages\index\index.tsx 下的props接口類型
type PageStateProps = {
  // 添加 userInfoStore
  userInfoStore: {
    userInfo: userinfo;
    saveInfo: Function;
    readInfo: Function;
  };
};

interface Index {
  props: PageStateProps;
}

// 注入 userInfoStore
@inject('userInfoStore')
@observer
class Index extends Component {
  config: Config = {
    navigationBarTitleText: '首頁'
  };

  /**
   * @description: 獲取用戶信息
   * @param {Object} detail onGetUserInfo 所返回的用戶基本信息
   * @return null
   */
  getUserInfo({ detail }) {
    const { userInfoStore } = this.props;
    userInfoStore.saveInfo(detail.userInfo);
  }

  // 添加 didshow鉤子,在每次頁面顯示的時候執行
  componentDidShow() {
    const { userInfoStore } = this.props;
    // 在頁面顯示的時候讀取用戶信息
    userInfoStore.readInfo();
  }
  render() {
    const { userInfo } = this.props.userInfoStore;
    return (
      <View className='index'>
        {/* button使用 getUserInfo 可以在小程序中獲取用戶基本信息 */}
        <Button
          openType='getUserInfo'
          onGetUserInfo={this.getUserInfo.bind(this)}
          className='index-avatarUrl-btn nobtn-style'
        >
          <View className='avatar-box'>
            <AtAvatar
              image={userInfo.avatarUrl}
              size='large'
              circle
              text={userInfo.avatarUrl ? '' : '空'}
            ></AtAvatar>
          </View>

          <View className='nickName-box'>
            {userInfo.nickName || '點擊登錄'}
          </View>
        </Button>
      </View>
    );
  }
}

export default Index as ComponentType;

這樣 如果本地的 Storage 中如果有存儲到用戶信息 就會不需要每次都獲取用戶的基本信息了。

git地址

查看原文

贊 2 收藏 0 評論 0

冷星 收藏了文章 · 2020-09-11

Taro/TS 快捷開發豐客多裂變小程序

本文共 13092 字,閱讀本文大概需要 10~15 分鐘, 技術干貨在文章中段,Taro 熟練使用者可跳過前面介紹篇幅
  • 文章目錄
  • 項目背景
  • 項目展示
  • 技術選型

    • Taro
    • 豐富的 Taro UI 組件庫
  • 項目架構

    • Taro 與原生小程序融合
    • TypeScript 的實踐
    • MobX 狀態管理
    • API Service、HttpClient 封裝
    • 圖片等比例縮放
    • 海報分享(分享朋友圈)
  • 總結

項目背景

豐客多是企業業務事業部打造的企業會員制商城,2020 年預期在 Q3 做商城的全面推廣,用戶增長的任務非常艱巨,因此希望借力 C 端用戶的強社交屬性,以微信小程序為載體,實現個人推薦企業( C 拉 B )的創新裂變模式。

項目展示

圖1
圖2
圖3

技術選型

下方多終端熱門框架對比,可以看到 Taro 已經同時支持了 React、Vue 技術棧,相較而言考慮到后期維護成本、框架響應維護速度,因此采用團隊自研 Taro 框架

image

Taro

Taro 是由 JDC·凹凸實驗室 傾力打造的一款多端開發解決方案的框架工具,支持使用 React/Vue/Nerv 等框架來開發微信/京東/百度/支付寶/字節跳動/ QQ 小程序/H5 等應用?,F如今市面上端的形態多種多樣,Web、React Native、微信小程序等各種端大行其道,當業務要求同時在不同的端都要求有所表現的時候,針對不同的端去編寫多套代碼的成本顯然非常高,這時候只編寫一套代碼就能夠適配到多端的能力就顯得極為需要。

當前 Taro 已進入 3.x 時代,相較于 Taro 1/2 采用了重運行時的架構,讓開發者可以獲得完整的 React/Vue 等框架的開發體驗,具體請參考《小程序跨框架開發的探索與實踐》。

  • 基于 React、Vue 語法規范,上手幾乎0成本,滿足基本開發需求
  • 支持 TS,支持 ES7/ES8 或更新的語法規范
  • 支持 CSS 預編譯器,Sass/Less 等
  • 支持 Hooks (日常開發幾乎不需要 redux 場景)
  • 支持狀態管理,Redux/MobX

豐富的 Taro UI 組件庫

Taro UI 是一款基于 Taro 框架開發的多端 UI 組件庫,一套組件可以在 微信小程序,支付寶小程序,百度小程序,H5 多端適配運行(ReactNative 端暫不支持)提供友好的 API,可靈活的使用組件。

支持一定程度的樣式定制。(請確保微信基礎庫版本在 v2.2.3 以上)目前支持三種自定義主題的方式,可以進行不同程度的樣式自定義:

  • scss 變量覆蓋
  • globalClass 全局樣式類
  • 配置 customStyle 屬性(僅有部分組件支持,請查看組件文檔,不建議使用)

項目架構

在前端架構方面,整體架構設計如下:

Taro 與原生小程序融合

項目中需要接入公用的 京東登錄 等其它微信小程序插件來實現登錄態打通,那么此時我們就遇到一個問題,多端轉換的問題 Taro 幫我們做了,但是第三方的這些插件邏輯調用轉換需要我們自己來實現。那么面對此場景,我們采用了以下解決方案:

首先 process.env.TARO_ENV 是關鍵,Taro 在編譯運行時候會對應設置該變量 h5、weapp、alipay、tt ...等,所有我們可以根據不同的變量來調用不同的插件。這種場景我們可以簡單運用一個工廠模式來處理此邏輯。下面先簡單上圖概述一下

  1. 創建抽象 Plugin 類,定制具體插件功能調用方法
  2. 創建實現類(微信小程序、京東小程序、H5 等 )
  3. 創建代工廠類(對外暴露具體方法),初始化時,根據當前場景實例化對應類

/** 抽象類 Plugin 提供具體插件功能 API */
abstract class Plugin {
    abstract getToken(): void; /** 獲取token信息 */   
    abstract outLogin(): void; /** 退出登錄 */   
    abstract openLogin(): void; /** 打開登錄頁 */   
}
/** 方法實現類-小程序 */
class WeChatPlugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
/** 方法實現類-京東小程序 */
class JDPlugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
/** 方法實現類 - H5 */
class H5Plugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
export class pluginHelper {
    private plugin: Plugin;
    constructor() {
        switch (process.env.TARO_ENV) {
            case 'weapp':
                this.plugin = new WeChatPlugin(); 
                break;
            case 'jd':
                this.plugin = new JDPlugin(); 
                break;
            case 'h5':
                this.plugin = new H5Plugin(); 
                break;
                // ...
            default:
                break;
        }
    }
    // 檢查是否為原生 APP
    get plugin(): Plugin{
        return this.plugin;
    }
}
export default pluginHelper;

TypeScript 的實踐

State Class 約束,非 interface 約束

搜索了一番市面上 React + TS 都是采用 interface 配合使用,下面我們舉個栗子看一下,看一下缺點

state + interface
interface ITsExampleState {
  /** 名稱 */
  name: string
  name2: string,
  name3: string,
  name4: string,
}
export default class TsExample extends Component<ITsExampleState> {
  state: Readonly<ITsExampleState> = {
    name: "",
    name2: "",
    name3: "",
    name4: "",
    //...
  }
  componentDidShow() {
    let tempState: ITsExampleState = {
      name: '456',
      name2: "",
      name3: "",
      name4: "",
    };
    this.setState(tempState)
  }
  componentDidHide() {
    let tempState: ITsExampleState = {
      name: '456',
      name2: "",
      name3: "",
      name4: "",
    };
    this.setState(tempState)
  }
}

那么這種方式使用雖然問題,但是我們會發現每次使用時都需要把每一個接口變量初始賦值一下,否則就會報錯,如果10多個變量就需要寫10次,豈不是很麻煩。

看一下,我如何來優雅解決這種場景

state + class

class ITsExampleState {
  /** 名稱 */
  name: string = ""
  name2: string = ""
  name3: string = ""
  name4: string = ""
}
export default class TsExample extends Component<ITsExampleState> {
  state: Readonly<ITsExampleState> = new ITsExampleState();
  componentDidShow() {
    let tempState: ITsExampleState = new ITsExampleState();
    tempState.name = '123';
    this.setState(tempState)
  }
  componentDidHide() {
    let tempState: ITsExampleState = new ITsExampleState();
    tempState.name = '456';
    this.setState(tempState)
  }
}

34行代碼變20行(??代碼量 KPI 同學慎用),代碼量的不同差距會越來越大,同樣在另一個小節 API Service 中,再說另一個優點。

MobX 狀態管理

[為什么選用 Mobx 不采用 Redux] https://tech.youzan.com/mobx_...

Redux是一個數據管理層,被廣泛用于管理復雜應用的數據。但是實際使用中,Redux的表現差強人意,可以說是不好用。而同時,社區也出現了一些數據管理的方案,Mobx就是其中之一。

MobX 是一個經過戰火洗禮的庫,它通過透明的函數響應式編程(transparently applying functional reactive programming - TFRP)使得狀態管理變得簡單和可擴展。MobX背后的哲學很簡單:

任何源自應用狀態的東西都應該自動地獲得。

其中包括UI、數據序列化、服務器通訊,等等。

React 和 MobX 是一對強力組合。React 通過提供機制把應用狀態轉換為可渲染組件樹并對其進行渲染。而MobX提供機制來存儲和更新應用狀態供 React 使用。

對于應用開發中的常見問題,React 和 MobX 都提供了最優和獨特的解決方案。React 提供了優化UI渲染的機制, 這種機制就是通過使用虛擬DOM來減少昂貴的DOM變化的數量。MobX 提供了優化應用狀態與 React 組件同步的機制,這種機制就是使用響應式虛擬依賴狀態圖表,它只有在真正需要的時候才更新并且永遠保持是最新的。

API Service、HttpClient 封裝

面向對象(封裝、繼承、多態)整個項目開發過程中,服務端是通過判斷請求頭中攜帶的 Header 自定義值來校驗登錄態。每一次數據請求,都需要在請求 Header 上添加自定義字段,隨著接口數量越來越多,因此我們將 Http 請求單獨封裝為一個模塊。

為了解決這一問題,我們將 HTTP 請求統一配置,生成 HttpClient Class 類,對外暴露 post 、 get 方法。并對后臺返回的數據進行統一處理,重新定義返回狀態碼,避免后端狀態碼多樣性,即使后端狀態碼做了修改,也不影響前端代碼的正確運行。

import Taro, { request } from "@tarojs/taro";

const baseUrl = "https://xxxxx"
const errorMsg = '系統有點忙,耐心等會唄';
export class HttpClient {
    /**
     * 檢查狀態
     * @param {ResponseData} response 響應值
     */
    private checkStatus(response) {
        // 如果http狀態碼正常,則直接返回數據
        if (response && (response.statusCode === 200 || response.statusCode === 304 || response.statusCode === 400)) {
            response.data = response.data);
            let resData: ResponseData = { state: 0, value: response.data.xxx, message: response.data.xxx };
            if (response.data.xxx) {
            } else {
                resData.state = 1;
                resData.value = response.data;
                resData.message = response.data.xxx;
            }
            if (resData.state == 1) {
                Taro.showToast({
                    title: resData.message,
                    icon: 'none',
                    duration: 2000
                })
            }
            return resData
        } else {
            Taro.showToast({
                title: errorMsg,
                icon: 'none',
                duration: 2000
            })
            return null
        }
    }

    public post(url: string, params: any = {}) {
        return this.request('post', url, params)
    }
    public get(url: string, params: any = {},) {
        return this.request('get', url, params)
    }

    async checkNetWorkDiasble() {
        return new Promise((resolve, reject) => {
            Taro.getNetworkType({
                success(res) {
                    const networkType = res.networkType
                    resolve(networkType == 'none')
                }
            })
        })
    }

    /**
    * request請求
    * @param {string} method get|post
    * @param {url} url 請求路徑
    * @param {*} [params] 請求參數
    */
    private async request(method: string, apiUrl: string, params: any): Promise<ResponseData | null> {
        // Taro request ...
    }
}

/**
 * 內部 響應對象
 * @param {number} state 0 成功 1失敗
 * @param {any} value 接口響應數據
 * @param {string} message 服務器響應信息msg
 */
interface ResponseData {
    state: number;
    value?: any;
    message: string;
}

對于 HTTP 請求我們還是不滿足,在組件中我們調用 HttpClient Class 類進行數據請求時,我們依然要回到請求接口的 Service 模塊文件,查看入參,或者是查看 swagger 文檔,如何才能一目了
然呢?采用 Class Params 對象方式約束入參,從編譯方式上進行約束。我們以下請求為例:

class UserApiService {
    // ...
    getFansInfo(params: PageParams) {
        return this.httpClient.post('/user/xxx', params);
    }
}
export class PageParams {
    /** 請求頁 */
    pageNo: number = 1;
    /** 請求數量 */
    pageSize: number = 10;
}

export class Test{
    testFn(){
        // 獲取粉絲數據
        let pageParams:PageParams=new PageParams();
        pageParams.pageNo = 1;
        pageParams.pageNo = 10;
        this.userApiService.getFansInfo(pageParams).then(res => {});
    }
}

在 getFansInfo 方法中,我們通過 TypeScript 的方式,約束了接口的參數是一個對象。同時在調用過程中可以采用 . 對應的屬性,友好的查看注釋,非 interface 使用

是不是很方便,不但避免了參數類型的不一致,出現 bug ,也節省了查找方法的時間,提高開發效率!

注:在 VS code 的編輯器中,當鼠標移動到某些文本之后,稍作片刻就會出現一個懸停提示窗口,這個窗口里會顯示跟鼠標下文本相關的信息。如果想要查看對象就具體信息,需要按下 Cmd 鍵( Windows 上是 Ctrl )。

圖片等比例縮放

在我們的項目中首頁采用瀑布流圖片,并采用不規則高度圖片,但是在我們的小程序中 Image 標簽又必須設置高度,這可如何是好...
我們通過 onLoad 函數來進行等比例縮放

export default class Index extends Component {
    // ...
    render() {
        const { imageUrl,imageHeight } = this.state as IState;
        return (
                <Image
                    mode="aspectFill"
                    style={`height:${imageHeight}px`}
                    data-original={imageUrl}
                    onLoad={this.imageOnload(event)} >
                </Image>
        );
    }
    imageOnload = (e)=>{
        let res = Utils.imageScale(e)
        this.setState({
            imageHeight: res.imageHeight;
        })
    }
}

export default class Utils {
    static imageScale = (e) => {
        let imageSize = {
            imageWidth: 0,
            imageHeight: 0
        };
        let originalWidth = e.detail.width;//圖片原始寬
        let originalHeight = e.detail.height;//圖片原始高
        let originalScale = originalHeight / originalWidth;//圖片高寬比
        // console.log('originalWidth: ' + originalWidth)
        // console.log('originalHeight: ' + originalHeight)
        //獲取屏幕寬高
        let res = Taro.getSystemInfoSync();
        let windowWidth = res.windowWidth;
        let windowHeight = res.windowHeight;
        let windowscale = windowHeight / windowWidth;//屏幕高寬比
        // console.log('windowWidth: ' + windowWidth)
        // console.log('windowHeight: ' + windowHeight)
        if (originalScale < windowscale) {//圖片高寬比小于屏幕高寬比
            //圖片縮放后的寬為屏幕寬
            imageSize.imageWidth = windowWidth;
            imageSize.imageHeight = (windowWidth * originalHeight) / originalWidth;
        } else {//圖片高寬比大于屏幕高寬比
            //圖片縮放后的高為屏幕高
            imageSize.imageHeight = windowHeight;
            imageSize.imageWidth = (windowHeight * originalWidth) / originalHeight;
        }
        // console.log('縮放后的寬: ' + imageSize.imageWidth)
        // console.log('縮放后的高: ' + imageSize.imageHeight)
        return imageSize;
    }
}

海報分享

在微信中小程序無法分享到朋友圈,目前大部分的解決方案都是,Canvas 動態繪制生成圖片后,保存到用戶相冊,用戶進行分享照片到朋友圈,朋友圈打開圖片后識別二維碼進入小程序,達到分享目的。
下面帶大家實現實現一波:

  1. 海報分析

  1. 代碼 Canvas 初始化創建
<Canvas style={`height:${canvasHeight}px;width:${canvasWidth}px`} className='shareCanvas' canvas-id="shareCanvas" ></Canvas>
  1. 樣式設置

保證 Canvas 不在用戶的視線內

.shareCanvas {
    width: 100%;
    height: 100%;
    background: #fff;
    position: absolute;
    opacity: 0;
    z-index: -1;
    right: 2000rpx;
    top: 2000rpx;
    z-index: 999999;
}
  1. CanvasUtil 工具類
export class CanvasUtil {
  /**
   * canvas 文本換行計算
   * @param {*} context CanvasContext
   * @param {string} text 文本
   * @param {number} width 內容寬度
   * @param {font} font 字體(字體大小會影響寬)
   */
  static breakLinesForCanvas(context, text: string, width: number, font) {
    function findBreakPoint(text: string, width: number, context) {
      var min = 0;
      var max = text.length - 1;
      while (min <= max) {
        var middle = Math.floor((min + max) / 2);
        var middleWidth = context.measureText(text.substr(0, middle)).width;
        var oneCharWiderThanMiddleWidth = context.measureText(text.substr(0, middle + 1)).width;
        if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
          return middle;
        }
        if (middleWidth < width) {
          min = middle + 1;
        } else {
          max = middle - 1;
        }
      }

      return -1;
    }


    var result = [];
    if (font) {
      context.font = font;
    }
    var textArray = text.split('\r\n');
    for (let i = 0; i < textArray.length; i++) {
      let item = textArray[i];
      var breakPoint = 0;
      while ((breakPoint = findBreakPoint(item, width, context)) !== -1) {
        result.push(item.substr(0, breakPoint));
        item = item.substr(breakPoint);
      }
      if (item) {
        result.push(item);
      }
    }
    return result;
  }
  /**
   * 圖片裁剪畫圓
   * @param {*} ctx CanvasContext
   * @param {string} img 圖片
   * @param {number} x x軸 坐標
   * @param {number} y y軸 坐標
   * @param {number*} r 半徑
   */
  static circleImg(ctx, img: string, x: number, y: number, r: number) {
    ctx.save();
    ctx.beginPath()
    var d = 2 * r;
    var cx = x + r;
    var cy = y + r;
    ctx.arc(cx, cy, r, 0, 2 * Math.PI);
    ctx.clip();
    ctx.drawImage(img, x, y, d, d);
    ctx.restore();
  }
  /**
   * 繪制圓角矩形
   * @param {*} ctx CanvasContext
   * @param {number} x x軸 坐標
   * @param {number} y y軸 坐標
   * @param {number} width 寬
   * @param {number} height 高
   * @param {number} r r 圓角
   * @param {boolean} fill 是否填充顏色
   */
  static drawRoundedRect(ctx, x: number, y: number, width: number, height: number, r: number, fill: boolean) {
    ctx.beginPath();
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2);
    ctx.lineTo(width - r + x, y);
    ctx.arc(width - r + x, r + y, r, Math.PI * 3 / 2, Math.PI * 2);
    ctx.lineTo(width + x, height + y - r);
    ctx.arc(width - r + x, height - r + y, r, 0, Math.PI * 1 / 2);
    ctx.lineTo(r + x, height + y);
    ctx.arc(r + x, height - r + y, r, Math.PI * 1 / 2, Math.PI);
    ctx.closePath();
    if (fill) {
      ctx.fill();
    }
  }
}
export default CanvasUtil;
  1. JS 邏輯處理
/** 用戶微信頭像 */
let avatarUrl = 'https://xxx.360buyimg.com/xxxxx.png';
// 海報背景圖片
let inviteImageUrl = 'https://xxx.360buyimg.com/xxxxx.png';
// 二維碼背景白尺寸
let qrBgHeight = 85;
let qrBgWidth = 85;
// 圖片居中尺寸
let centerPx = canvasWidth / 2;
// 二維碼背景白 x軸 ,y軸 坐標
let qrBgX = centerPx - qrBgWidth / 2;
let qrBgY = 370;
let context = Taro.createCanvasContext('shareCanvas');
//海報背景繪制
context.drawImage(inviteImageUrl, 0, 0, canvasWidth, canvasHeight);
//矩形顏色設置
context.setFillStyle('#ffffff');
//繪制二維碼圓角矩形
CanvasUtil.drawRoundedRect(context, qrBgX, qrBgY, qrBgWidth, qrBgHeight, 5, true);
// context.restore();
//繪制二維碼
context.drawImage(this.downloadQRcode, qrBgX + 2, qrBgY + 2, qrBgWidth - 4, qrBgHeight - 4);
// 下載微信頭像到本地
Taro.downloadFile({
    url: avatarUrl,
    success: function (res) {
    // 微信頭像尺寸尺寸
    let wxAvatarHeight = 32;
    let wxAvatarWidth = 32;
    // 微信頭像居中 x軸 ,y軸 坐標
    let wxAvatarX = centerPx - wxAvatarWidth / 2;
    let wxAvatarY = 395.5;
    //微信頭像繪制
    CanvasUtil.circleImg(context, res.tempFilePath, wxAvatarX, wxAvatarY, wxAvatarWidth / 2);
    // 文本繪制
    context.setTextAlign("center")
    context.font = "12px PingFangSC-Regular";
    context.fillText("掃一掃", centerPx, qrBgY + qrBgHeight + 20);

    context.font = "10px PingFangSC-Regular";
    context.fillText("立即注冊豐客多", centerPx, qrBgY + qrBgHeight + 34);

    context.draw();

    Taro.showLoading({
        title: '生成中',
    })
    setTimeout(() => {
        Taro.canvasToTempFilePath({
        canvasId: 'shareCanvas',
        fileType: 'jpg',
        success: function (res) {
            Taro.hideLoading()
            console.log(res.tempFilePath)
            Taro.showLoading({
              title: '保存中...',
              mask: true
            });
            Taro.saveImageToPhotosAlbum({
              filePath: res.tempFilePath,
              success: function (res) {
                Taro.showToast({
                  title: '保存成功',
                  icon: 'success',
                  duration: 2000
                })
              },
              fail: function (res) {
                Taro.hideLoading()
                console.log(res)
              }
            })
        }
        })
    }, 1000);
    }
})

總結

在開發此項目之前,都是自己采用原生微信小程序進行開發,該項目是我第一次使用 Taro + Taro UI + TypeScript 來開發小程序,在開發過程中通過查閱官方文檔,基本屬于 0 成本上手。
同時在開發過程中遇到問題一點一滴記錄下來,從而得到成長,并沉淀出此文章,達到提高自我幫助他人。目前 Taro 框架也在不斷的迭代中,在近期發布的 3.0 候選版本也已經支持使用 Vue 語言,作為一個支持多端轉化的工具框架值得大家選擇。

查看原文

冷星 贊了文章 · 2020-09-11

Taro/TS 快捷開發豐客多裂變小程序

本文共 13092 字,閱讀本文大概需要 10~15 分鐘, 技術干貨在文章中段,Taro 熟練使用者可跳過前面介紹篇幅
  • 文章目錄
  • 項目背景
  • 項目展示
  • 技術選型

    • Taro
    • 豐富的 Taro UI 組件庫
  • 項目架構

    • Taro 與原生小程序融合
    • TypeScript 的實踐
    • MobX 狀態管理
    • API Service、HttpClient 封裝
    • 圖片等比例縮放
    • 海報分享(分享朋友圈)
  • 總結

項目背景

豐客多是企業業務事業部打造的企業會員制商城,2020 年預期在 Q3 做商城的全面推廣,用戶增長的任務非常艱巨,因此希望借力 C 端用戶的強社交屬性,以微信小程序為載體,實現個人推薦企業( C 拉 B )的創新裂變模式。

項目展示

圖1
圖2
圖3

技術選型

下方多終端熱門框架對比,可以看到 Taro 已經同時支持了 React、Vue 技術棧,相較而言考慮到后期維護成本、框架響應維護速度,因此采用團隊自研 Taro 框架

image

Taro

Taro 是由 JDC·凹凸實驗室 傾力打造的一款多端開發解決方案的框架工具,支持使用 React/Vue/Nerv 等框架來開發微信/京東/百度/支付寶/字節跳動/ QQ 小程序/H5 等應用?,F如今市面上端的形態多種多樣,Web、React Native、微信小程序等各種端大行其道,當業務要求同時在不同的端都要求有所表現的時候,針對不同的端去編寫多套代碼的成本顯然非常高,這時候只編寫一套代碼就能夠適配到多端的能力就顯得極為需要。

當前 Taro 已進入 3.x 時代,相較于 Taro 1/2 采用了重運行時的架構,讓開發者可以獲得完整的 React/Vue 等框架的開發體驗,具體請參考《小程序跨框架開發的探索與實踐》。

  • 基于 React、Vue 語法規范,上手幾乎0成本,滿足基本開發需求
  • 支持 TS,支持 ES7/ES8 或更新的語法規范
  • 支持 CSS 預編譯器,Sass/Less 等
  • 支持 Hooks (日常開發幾乎不需要 redux 場景)
  • 支持狀態管理,Redux/MobX

豐富的 Taro UI 組件庫

Taro UI 是一款基于 Taro 框架開發的多端 UI 組件庫,一套組件可以在 微信小程序,支付寶小程序,百度小程序,H5 多端適配運行(ReactNative 端暫不支持)提供友好的 API,可靈活的使用組件。

支持一定程度的樣式定制。(請確保微信基礎庫版本在 v2.2.3 以上)目前支持三種自定義主題的方式,可以進行不同程度的樣式自定義:

  • scss 變量覆蓋
  • globalClass 全局樣式類
  • 配置 customStyle 屬性(僅有部分組件支持,請查看組件文檔,不建議使用)

項目架構

在前端架構方面,整體架構設計如下:

Taro 與原生小程序融合

項目中需要接入公用的 京東登錄 等其它微信小程序插件來實現登錄態打通,那么此時我們就遇到一個問題,多端轉換的問題 Taro 幫我們做了,但是第三方的這些插件邏輯調用轉換需要我們自己來實現。那么面對此場景,我們采用了以下解決方案:

首先 process.env.TARO_ENV 是關鍵,Taro 在編譯運行時候會對應設置該變量 h5、weapp、alipay、tt ...等,所有我們可以根據不同的變量來調用不同的插件。這種場景我們可以簡單運用一個工廠模式來處理此邏輯。下面先簡單上圖概述一下

  1. 創建抽象 Plugin 類,定制具體插件功能調用方法
  2. 創建實現類(微信小程序、京東小程序、H5 等 )
  3. 創建代工廠類(對外暴露具體方法),初始化時,根據當前場景實例化對應類

/** 抽象類 Plugin 提供具體插件功能 API */
abstract class Plugin {
    abstract getToken(): void; /** 獲取token信息 */   
    abstract outLogin(): void; /** 退出登錄 */   
    abstract openLogin(): void; /** 打開登錄頁 */   
}
/** 方法實現類-小程序 */
class WeChatPlugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
/** 方法實現類-京東小程序 */
class JDPlugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
/** 方法實現類 - H5 */
class H5Plugin extends Plugin {
    getToken(): void {
        // ... 調用對應插件API
    }
    outLogin(): void {
        // ... 調用對應插件API
    }
    openLogin(): void {
        // ... 調用對應插件API
    }
    ...
}
export class pluginHelper {
    private plugin: Plugin;
    constructor() {
        switch (process.env.TARO_ENV) {
            case 'weapp':
                this.plugin = new WeChatPlugin(); 
                break;
            case 'jd':
                this.plugin = new JDPlugin(); 
                break;
            case 'h5':
                this.plugin = new H5Plugin(); 
                break;
                // ...
            default:
                break;
        }
    }
    // 檢查是否為原生 APP
    get plugin(): Plugin{
        return this.plugin;
    }
}
export default pluginHelper;

TypeScript 的實踐

State Class 約束,非 interface 約束

搜索了一番市面上 React + TS 都是采用 interface 配合使用,下面我們舉個栗子看一下,看一下缺點

state + interface
interface ITsExampleState {
  /** 名稱 */
  name: string
  name2: string,
  name3: string,
  name4: string,
}
export default class TsExample extends Component<ITsExampleState> {
  state: Readonly<ITsExampleState> = {
    name: "",
    name2: "",
    name3: "",
    name4: "",
    //...
  }
  componentDidShow() {
    let tempState: ITsExampleState = {
      name: '456',
      name2: "",
      name3: "",
      name4: "",
    };
    this.setState(tempState)
  }
  componentDidHide() {
    let tempState: ITsExampleState = {
      name: '456',
      name2: "",
      name3: "",
      name4: "",
    };
    this.setState(tempState)
  }
}

那么這種方式使用雖然問題,但是我們會發現每次使用時都需要把每一個接口變量初始賦值一下,否則就會報錯,如果10多個變量就需要寫10次,豈不是很麻煩。

看一下,我如何來優雅解決這種場景

state + class

class ITsExampleState {
  /** 名稱 */
  name: string = ""
  name2: string = ""
  name3: string = ""
  name4: string = ""
}
export default class TsExample extends Component<ITsExampleState> {
  state: Readonly<ITsExampleState> = new ITsExampleState();
  componentDidShow() {
    let tempState: ITsExampleState = new ITsExampleState();
    tempState.name = '123';
    this.setState(tempState)
  }
  componentDidHide() {
    let tempState: ITsExampleState = new ITsExampleState();
    tempState.name = '456';
    this.setState(tempState)
  }
}

34行代碼變20行(??代碼量 KPI 同學慎用),代碼量的不同差距會越來越大,同樣在另一個小節 API Service 中,再說另一個優點。

MobX 狀態管理

[為什么選用 Mobx 不采用 Redux] https://tech.youzan.com/mobx_...

Redux是一個數據管理層,被廣泛用于管理復雜應用的數據。但是實際使用中,Redux的表現差強人意,可以說是不好用。而同時,社區也出現了一些數據管理的方案,Mobx就是其中之一。

MobX 是一個經過戰火洗禮的庫,它通過透明的函數響應式編程(transparently applying functional reactive programming - TFRP)使得狀態管理變得簡單和可擴展。MobX背后的哲學很簡單:

任何源自應用狀態的東西都應該自動地獲得。

其中包括UI、數據序列化、服務器通訊,等等。

React 和 MobX 是一對強力組合。React 通過提供機制把應用狀態轉換為可渲染組件樹并對其進行渲染。而MobX提供機制來存儲和更新應用狀態供 React 使用。

對于應用開發中的常見問題,React 和 MobX 都提供了最優和獨特的解決方案。React 提供了優化UI渲染的機制, 這種機制就是通過使用虛擬DOM來減少昂貴的DOM變化的數量。MobX 提供了優化應用狀態與 React 組件同步的機制,這種機制就是使用響應式虛擬依賴狀態圖表,它只有在真正需要的時候才更新并且永遠保持是最新的。

API Service、HttpClient 封裝

面向對象(封裝、繼承、多態)整個項目開發過程中,服務端是通過判斷請求頭中攜帶的 Header 自定義值來校驗登錄態。每一次數據請求,都需要在請求 Header 上添加自定義字段,隨著接口數量越來越多,因此我們將 Http 請求單獨封裝為一個模塊。

為了解決這一問題,我們將 HTTP 請求統一配置,生成 HttpClient Class 類,對外暴露 post 、 get 方法。并對后臺返回的數據進行統一處理,重新定義返回狀態碼,避免后端狀態碼多樣性,即使后端狀態碼做了修改,也不影響前端代碼的正確運行。

import Taro, { request } from "@tarojs/taro";

const baseUrl = "https://xxxxx"
const errorMsg = '系統有點忙,耐心等會唄';
export class HttpClient {
    /**
     * 檢查狀態
     * @param {ResponseData} response 響應值
     */
    private checkStatus(response) {
        // 如果http狀態碼正常,則直接返回數據
        if (response && (response.statusCode === 200 || response.statusCode === 304 || response.statusCode === 400)) {
            response.data = response.data);
            let resData: ResponseData = { state: 0, value: response.data.xxx, message: response.data.xxx };
            if (response.data.xxx) {
            } else {
                resData.state = 1;
                resData.value = response.data;
                resData.message = response.data.xxx;
            }
            if (resData.state == 1) {
                Taro.showToast({
                    title: resData.message,
                    icon: 'none',
                    duration: 2000
                })
            }
            return resData
        } else {
            Taro.showToast({
                title: errorMsg,
                icon: 'none',
                duration: 2000
            })
            return null
        }
    }

    public post(url: string, params: any = {}) {
        return this.request('post', url, params)
    }
    public get(url: string, params: any = {},) {
        return this.request('get', url, params)
    }

    async checkNetWorkDiasble() {
        return new Promise((resolve, reject) => {
            Taro.getNetworkType({
                success(res) {
                    const networkType = res.networkType
                    resolve(networkType == 'none')
                }
            })
        })
    }

    /**
    * request請求
    * @param {string} method get|post
    * @param {url} url 請求路徑
    * @param {*} [params] 請求參數
    */
    private async request(method: string, apiUrl: string, params: any): Promise<ResponseData | null> {
        // Taro request ...
    }
}

/**
 * 內部 響應對象
 * @param {number} state 0 成功 1失敗
 * @param {any} value 接口響應數據
 * @param {string} message 服務器響應信息msg
 */
interface ResponseData {
    state: number;
    value?: any;
    message: string;
}

對于 HTTP 請求我們還是不滿足,在組件中我們調用 HttpClient Class 類進行數據請求時,我們依然要回到請求接口的 Service 模塊文件,查看入參,或者是查看 swagger 文檔,如何才能一目了
然呢?采用 Class Params 對象方式約束入參,從編譯方式上進行約束。我們以下請求為例:

class UserApiService {
    // ...
    getFansInfo(params: PageParams) {
        return this.httpClient.post('/user/xxx', params);
    }
}
export class PageParams {
    /** 請求頁 */
    pageNo: number = 1;
    /** 請求數量 */
    pageSize: number = 10;
}

export class Test{
    testFn(){
        // 獲取粉絲數據
        let pageParams:PageParams=new PageParams();
        pageParams.pageNo = 1;
        pageParams.pageNo = 10;
        this.userApiService.getFansInfo(pageParams).then(res => {});
    }
}

在 getFansInfo 方法中,我們通過 TypeScript 的方式,約束了接口的參數是一個對象。同時在調用過程中可以采用 . 對應的屬性,友好的查看注釋,非 interface 使用

是不是很方便,不但避免了參數類型的不一致,出現 bug ,也節省了查找方法的時間,提高開發效率!

注:在 VS code 的編輯器中,當鼠標移動到某些文本之后,稍作片刻就會出現一個懸停提示窗口,這個窗口里會顯示跟鼠標下文本相關的信息。如果想要查看對象就具體信息,需要按下 Cmd 鍵( Windows 上是 Ctrl )。

圖片等比例縮放

在我們的項目中首頁采用瀑布流圖片,并采用不規則高度圖片,但是在我們的小程序中 Image 標簽又必須設置高度,這可如何是好...
我們通過 onLoad 函數來進行等比例縮放

export default class Index extends Component {
    // ...
    render() {
        const { imageUrl,imageHeight } = this.state as IState;
        return (
                <Image
                    mode="aspectFill"
                    style={`height:${imageHeight}px`}
                    data-original={imageUrl}
                    onLoad={this.imageOnload(event)} >
                </Image>
        );
    }
    imageOnload = (e)=>{
        let res = Utils.imageScale(e)
        this.setState({
            imageHeight: res.imageHeight;
        })
    }
}

export default class Utils {
    static imageScale = (e) => {
        let imageSize = {
            imageWidth: 0,
            imageHeight: 0
        };
        let originalWidth = e.detail.width;//圖片原始寬
        let originalHeight = e.detail.height;//圖片原始高
        let originalScale = originalHeight / originalWidth;//圖片高寬比
        // console.log('originalWidth: ' + originalWidth)
        // console.log('originalHeight: ' + originalHeight)
        //獲取屏幕寬高
        let res = Taro.getSystemInfoSync();
        let windowWidth = res.windowWidth;
        let windowHeight = res.windowHeight;
        let windowscale = windowHeight / windowWidth;//屏幕高寬比
        // console.log('windowWidth: ' + windowWidth)
        // console.log('windowHeight: ' + windowHeight)
        if (originalScale < windowscale) {//圖片高寬比小于屏幕高寬比
            //圖片縮放后的寬為屏幕寬
            imageSize.imageWidth = windowWidth;
            imageSize.imageHeight = (windowWidth * originalHeight) / originalWidth;
        } else {//圖片高寬比大于屏幕高寬比
            //圖片縮放后的高為屏幕高
            imageSize.imageHeight = windowHeight;
            imageSize.imageWidth = (windowHeight * originalWidth) / originalHeight;
        }
        // console.log('縮放后的寬: ' + imageSize.imageWidth)
        // console.log('縮放后的高: ' + imageSize.imageHeight)
        return imageSize;
    }
}

海報分享

在微信中小程序無法分享到朋友圈,目前大部分的解決方案都是,Canvas 動態繪制生成圖片后,保存到用戶相冊,用戶進行分享照片到朋友圈,朋友圈打開圖片后識別二維碼進入小程序,達到分享目的。
下面帶大家實現實現一波:

  1. 海報分析

  1. 代碼 Canvas 初始化創建
<Canvas style={`height:${canvasHeight}px;width:${canvasWidth}px`} className='shareCanvas' canvas-id="shareCanvas" ></Canvas>
  1. 樣式設置

保證 Canvas 不在用戶的視線內

.shareCanvas {
    width: 100%;
    height: 100%;
    background: #fff;
    position: absolute;
    opacity: 0;
    z-index: -1;
    right: 2000rpx;
    top: 2000rpx;
    z-index: 999999;
}
  1. CanvasUtil 工具類
export class CanvasUtil {
  /**
   * canvas 文本換行計算
   * @param {*} context CanvasContext
   * @param {string} text 文本
   * @param {number} width 內容寬度
   * @param {font} font 字體(字體大小會影響寬)
   */
  static breakLinesForCanvas(context, text: string, width: number, font) {
    function findBreakPoint(text: string, width: number, context) {
      var min = 0;
      var max = text.length - 1;
      while (min <= max) {
        var middle = Math.floor((min + max) / 2);
        var middleWidth = context.measureText(text.substr(0, middle)).width;
        var oneCharWiderThanMiddleWidth = context.measureText(text.substr(0, middle + 1)).width;
        if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
          return middle;
        }
        if (middleWidth < width) {
          min = middle + 1;
        } else {
          max = middle - 1;
        }
      }

      return -1;
    }


    var result = [];
    if (font) {
      context.font = font;
    }
    var textArray = text.split('\r\n');
    for (let i = 0; i < textArray.length; i++) {
      let item = textArray[i];
      var breakPoint = 0;
      while ((breakPoint = findBreakPoint(item, width, context)) !== -1) {
        result.push(item.substr(0, breakPoint));
        item = item.substr(breakPoint);
      }
      if (item) {
        result.push(item);
      }
    }
    return result;
  }
  /**
   * 圖片裁剪畫圓
   * @param {*} ctx CanvasContext
   * @param {string} img 圖片
   * @param {number} x x軸 坐標
   * @param {number} y y軸 坐標
   * @param {number*} r 半徑
   */
  static circleImg(ctx, img: string, x: number, y: number, r: number) {
    ctx.save();
    ctx.beginPath()
    var d = 2 * r;
    var cx = x + r;
    var cy = y + r;
    ctx.arc(cx, cy, r, 0, 2 * Math.PI);
    ctx.clip();
    ctx.drawImage(img, x, y, d, d);
    ctx.restore();
  }
  /**
   * 繪制圓角矩形
   * @param {*} ctx CanvasContext
   * @param {number} x x軸 坐標
   * @param {number} y y軸 坐標
   * @param {number} width 寬
   * @param {number} height 高
   * @param {number} r r 圓角
   * @param {boolean} fill 是否填充顏色
   */
  static drawRoundedRect(ctx, x: number, y: number, width: number, height: number, r: number, fill: boolean) {
    ctx.beginPath();
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2);
    ctx.lineTo(width - r + x, y);
    ctx.arc(width - r + x, r + y, r, Math.PI * 3 / 2, Math.PI * 2);
    ctx.lineTo(width + x, height + y - r);
    ctx.arc(width - r + x, height - r + y, r, 0, Math.PI * 1 / 2);
    ctx.lineTo(r + x, height + y);
    ctx.arc(r + x, height - r + y, r, Math.PI * 1 / 2, Math.PI);
    ctx.closePath();
    if (fill) {
      ctx.fill();
    }
  }
}
export default CanvasUtil;
  1. JS 邏輯處理
/** 用戶微信頭像 */
let avatarUrl = 'https://xxx.360buyimg.com/xxxxx.png';
// 海報背景圖片
let inviteImageUrl = 'https://xxx.360buyimg.com/xxxxx.png';
// 二維碼背景白尺寸
let qrBgHeight = 85;
let qrBgWidth = 85;
// 圖片居中尺寸
let centerPx = canvasWidth / 2;
// 二維碼背景白 x軸 ,y軸 坐標
let qrBgX = centerPx - qrBgWidth / 2;
let qrBgY = 370;
let context = Taro.createCanvasContext('shareCanvas');
//海報背景繪制
context.drawImage(inviteImageUrl, 0, 0, canvasWidth, canvasHeight);
//矩形顏色設置
context.setFillStyle('#ffffff');
//繪制二維碼圓角矩形
CanvasUtil.drawRoundedRect(context, qrBgX, qrBgY, qrBgWidth, qrBgHeight, 5, true);
// context.restore();
//繪制二維碼
context.drawImage(this.downloadQRcode, qrBgX + 2, qrBgY + 2, qrBgWidth - 4, qrBgHeight - 4);
// 下載微信頭像到本地
Taro.downloadFile({
    url: avatarUrl,
    success: function (res) {
    // 微信頭像尺寸尺寸
    let wxAvatarHeight = 32;
    let wxAvatarWidth = 32;
    // 微信頭像居中 x軸 ,y軸 坐標
    let wxAvatarX = centerPx - wxAvatarWidth / 2;
    let wxAvatarY = 395.5;
    //微信頭像繪制
    CanvasUtil.circleImg(context, res.tempFilePath, wxAvatarX, wxAvatarY, wxAvatarWidth / 2);
    // 文本繪制
    context.setTextAlign("center")
    context.font = "12px PingFangSC-Regular";
    context.fillText("掃一掃", centerPx, qrBgY + qrBgHeight + 20);

    context.font = "10px PingFangSC-Regular";
    context.fillText("立即注冊豐客多", centerPx, qrBgY + qrBgHeight + 34);

    context.draw();

    Taro.showLoading({
        title: '生成中',
    })
    setTimeout(() => {
        Taro.canvasToTempFilePath({
        canvasId: 'shareCanvas',
        fileType: 'jpg',
        success: function (res) {
            Taro.hideLoading()
            console.log(res.tempFilePath)
            Taro.showLoading({
              title: '保存中...',
              mask: true
            });
            Taro.saveImageToPhotosAlbum({
              filePath: res.tempFilePath,
              success: function (res) {
                Taro.showToast({
                  title: '保存成功',
                  icon: 'success',
                  duration: 2000
                })
              },
              fail: function (res) {
                Taro.hideLoading()
                console.log(res)
              }
            })
        }
        })
    }, 1000);
    }
})

總結

在開發此項目之前,都是自己采用原生微信小程序進行開發,該項目是我第一次使用 Taro + Taro UI + TypeScript 來開發小程序,在開發過程中通過查閱官方文檔,基本屬于 0 成本上手。
同時在開發過程中遇到問題一點一滴記錄下來,從而得到成長,并沉淀出此文章,達到提高自我幫助他人。目前 Taro 框架也在不斷的迭代中,在近期發布的 3.0 候選版本也已經支持使用 Vue 語言,作為一個支持多端轉化的工具框架值得大家選擇。

查看原文

贊 5 收藏 4 評論 0

冷星 贊了文章 · 2020-09-01

WebP\WebM新媒體格式的網站優化

Web已成為當前世界上最重要且使用最廣泛的通信平臺之一。但這一令人難以置信的技術的一個缺點是,作為開發者,我們無法控制我們的用戶訪問我們網站的設備的硬件規格。終端用戶可能在高端或低端設備上訪問你的網站,而這些設備的網絡連接有好有壞。這意味著,你必須確保你的網站盡可能地優化,以便能夠滿足任何用戶的要求。

Web優化可以在網站的不同組件上以多種不同方式完成。我最近寫了一篇文章,介紹使用JavaScript進行代碼優化的一些技巧。你可以在這里閱讀文章。

本文將主要關注針對圖像和視頻等媒體內容的Web優化。

眾所周知,多媒體內容可以提高用戶對你的內容的參與度,但它也可以讓你的網站進行頁面SEO優化。根據Kimbe MacMaster的說法,如果你在網站上使用視頻,你出現在谷歌搜索結果首頁的可能性會增加53倍。

目前的實施有什么問題?

現在的圖片和視頻都很棒,它們的質量很高,而且傳播得很好。但目前媒體內容的一個主要問題是大小,據Jecelyn統計,平均一個網頁僅圖片就會消耗5MB的數據。這可能給用戶帶來沉重負擔,因為在某些國家/地區,尤其是在移動數據上,這可能會非常昂貴。用戶還將遇到站點加載時間的問題,尤其是連接速度較慢的問題,這可能會對你的網站產生負面影響。

傳統上,使用的圖像格式為JPEG,PNG和GIF,對于視頻,使用了MP4,AVI和WMV等格式,但是這些格式還不夠好。需要一些更好的東西,一些有效載荷較小但又不影響內容質量的東西。

當我參加在斯里蘭卡科倫坡舉行的2019 Angular會議時,我第一次了解WebP。斯里蘭卡是一個了不起的國家,你一生中應該至少參觀一次。令人難以置信的會議是由Jecelyn Yeen完成的。

譯者注:本文不僅僅是講WebP。Jecelyn Yeen是馬來西亞人,GDE,高性能快速且SEO友好的SPA網站 http://ng-my.org 創始人

WebP的圖片

WebP是一種現代圖像格式,它以較低的有效載荷大小提供有損和無損的壓縮格式。這種圖像格式目前由科技巨頭Google開發。WebP最近獲得了很大的發展勢頭,特別是在大型科技公司的推動下,這主要是由于WebP的性能和Google的支持。WebP與有損圖像格式(例如JPG)和無損格式(例如PNG)直接競爭。

優點

  • 與PNG相比,WebP無損圖像的尺寸要小26%。
  • 在等效的SSIM質量指數下,WebP有損圖像比同類JPEG圖像小25–34%。
  • 無損WebP也支持圖像透明性。

Jeremy Wagner的有損WebP和JPG之間的比較—來源:InsaleLab

Jeremy Wagner比較無損WebP和PNG —來源:InsaleLab

缺點

WebP不能改進Internet的唯一原因是兼容性。

  • 到目前為止,Safari瀏覽器尚未支持。但是,有消息表明,在不久的將來即將對Safari提供WebP支持。
  • 默認情況下,內容管理站點(CMS)當前不支持WebP。但是你可以安裝插件或擴展來開始。

瀏覽器兼容性

WebP的瀏覽器兼容性—來源:CanIUse

向后兼容

為WebP圖像提供向后兼容性很容易,只需5行代碼。

<picture>
   <source type="image/webp" srcset="img/awesomeImage.webp">
   <source type="image/jpeg" srcset="img/awesomeImage.jpg">
   <img data-original="img/awesomeImage.jpg">
</picture>

上面的代碼段確保即使是最舊的瀏覽器也可以顯示其支持的圖像,這是一篇關于WebP的精彩文章。

WebM的視頻

WebM是一種免版權的視頻文件格式,用于Google維護的HTML5視頻標簽。WebM最初在2010年Google I/O大會上宣布,其H.264編解碼器計劃替代現有的MP4格式。

根據Google的說法,“WebM定義了文件容器結構、視頻和音頻格式,文件由使用VP8或VP9視頻編解碼器壓縮的視頻流和使用Vorbis或Opus音頻編解碼器壓縮的音頻流組成?!?/p>

眾所周知,WebM可為有效載荷較小的視頻文件提供出色的質量,這使得它非常適合視頻流。事實上,Youtube視頻在各大瀏覽器中都是以WebM的形式提供的。

優點

  • 許可——這種視頻格式的主要優勢之一是它是免版權的。諸如MP4(H.264)之類的格式存在各種許可問題,但是WebM憑借其開源許可使事情變得簡單。
  • 質量——關于VP8,VP9與H.264,H.265的質量有幾種意見。但他們中的大多數人都同意,質量在某種程度上是平等的。
  • 大小——由于技術的作用是壓縮文件,使得WebM的整體大小比MP4小得多,WebM視頻的訪問量更大,文件可以快速從網上下載。

缺點

  • 與其兄弟相似,WebM的主要缺點是瀏覽器兼容性。到目前為止,Safari和IE尚未完全支持WebM。
  • 較少的媒體播放器支持WebM視頻或需要安裝組件或插件。
  • WebM視頻無法在大多數便攜式設備上播放。但這正在改變。

瀏覽器兼容性

向后兼容

與WebP相似,可以通過以下代碼段實現向不兼容WebM的瀏覽器的向后兼容性。

<video width="640" height="480" controls>
  <source data-original="movie.webm" type="video/webm">
  <source data-original="movie.ogg" type="video/ogg">
  <source data-original="movie.mp4" type="video/mp4">
  您的瀏覽器不支持視頻標簽。
</video>

你的瀏覽器將選擇它支持的第一個來源。

進一步改進

目前,我們已經意識到使用WebP和WebM等媒體格式的好處。但是,當你開始實施它們時,以不同的格式維護同一文件的多個副本可能會非常麻煩。

例如,如果上傳PNG或JPG,并且要在支持的瀏覽器上使用WebP,則必須在服務器上存儲相同文件的副本。這可能是一個負擔,因為它需要采取額外的步驟來轉換上傳的文件,并在保存重復文件時占用存儲空間。

你可以利用諸如Cloudinary和Gumlet之類的SaaS應用程序的功能來解決此問題,這些服務使你能夠動態地動態轉換文件。你要做的就是上傳原始格式的圖像/視頻,并在請求圖像/視頻時,在文件URL中添加一個屬性以即時接收優化版本。此外,這些服務會自動緩存這些文件,這意味著從某個區域訪問后,如果其他任何人想要訪問相同格式的相同文件,則將交付緩存中的文件。這使事情變得更快。

譯者注:國內可以使用又拍云之類的

這主要要歸功于內置的CDN,以確保你的內容能以最快的速度在多個地區傳遞。

如果你不想花幾塊錢在這些服務上,你可以在Firebase的幫助下制作自己版本的應用程序。盡管它不是完全免費的,但對于較小的網站,您可以使用免費配額。

總結

得益于大量有才華的開發者,網站已經變得更加吸引人,設計也令人驚嘆。但要想錦上添花,必須有正確的優化策略。這對于網站的成功至關重要。

譯者注:你可以擁有最漂亮的網站,但是如果用戶不滿意,那將是一次失敗。人類很難討好!

這是我創建的demo應用程序,可以在此處查看源代碼。

查看原文

贊 5 收藏 2 評論 0

認證與成就

  • 獲得 383 次點贊
  • 獲得 9 枚徽章 獲得 0 枚金徽章, 獲得 1 枚銀徽章, 獲得 8 枚銅徽章

擅長技能
編輯

開源項目 & 著作
編輯

注冊于 2015-01-04
個人主頁被 3.4k 人瀏覽

一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>