最近搬家又讓我挖出了Amazon Kindle, 又覺得拿來看漫畫很方便(這戲演了幾次了呀?), 雖然說好像也有網站可以下載漫畫.mobi檔, 不過似乎是會員制的, 不喜歡

因此又讓我想寫漫畫的爬蟲了, 這次的目標是: 無限動漫 (他們的app實在做得有夠差)

這次幾個需求是:

  1. Command line下就可以跑了(這也沒必要做UI吧?)
  2. 在os x下可以執行(我自己電腦是mac)
  3. 出來的檔案可以放到kindle看(.mobi檔或epub)

mobi或epub的檔案格式似乎有點麻煩, 也不太好做得好, 所以決定用cbz檔再用Calibre轉mobi

Calibre有一個方便的command line tool叫ebook-convert, 可以用來轉檔, 而cbz本身非常的簡單 , 它就是一個zip檔, 裡面的圖片檔名照編號就好, 這code還算好寫

再來就是看一下怎麼解析無限動漫的內容了, 它的URL是長這樣的:

http://v.comicbus.com/online/comic-653.html?ch=1

以上範例是名偵探柯南第一卷, 大膽猜測, 653是漫畫編號, ch是集數, 選到第二頁, URL會變成這樣

http://v.comicbus.com/online/comic-653.html?ch=4-2

這樣其實就很明顯了, 接下來是內容的部分

每一集的頭上有一個"正在觀看:[ 名偵探柯南 1 ]", “[]“內就是標題了吧, 另外還有一個"select”, 裡面有這集所有的頁數資訊, 而圖片的id是"TheImg”

不過麻煩的是, 這些資訊似乎隱藏在javascript中, page載入後才會出現

這如果使用headless browser像是Phantomjs就沒啥問題, 但這邊我不想用它, 因為使用這工具還要再裝它

我下一個選擇是Go + Webloop, Webloop是一個Go的headless browser lib, 它是基於WebkitGtk+做成的, 不過我在mac上裝WebkitGTK+裝好久一直有問題, 所以…放棄….

接下來的選擇呢? 還有其他的headless browser嗎?有的! Erik, 這是一個Swift的head less browser, 用Swift寫爬蟲好像挺酷的, 查了一下, 有人用Alamofire + Kanna, 不過這在這例子不適用, 這例子還是比較適合Erik

成品

先給成果: ComicGo

這已經是一個OS X的可執行檔, 在Command line下執行 ComicGo 653 1就可以抓名偵探柯南第一集, 相關的漫畫編號集數, 就去無限動漫查吧

抓完會在你的Download目錄出現"名偵探柯南 1.cbz"再用ebook-covert去轉成你要的格式就可以了

少少的時間隨便寫寫而已, 有bug就見諒囉

OS X Command line tool

XCode + Swift是可以拿來寫command line tool的, 新增一個專案選"Command line tool":

這樣就可以開始寫了

一開始在專案內部會發現一個"main.swift", 由於用swift寫command line app並沒有像其他語言有main function這類的東西 所以程式就寫在這吧

開發Command line tool的坑

坑…真的不少

首先, 你不能使用任何的framework, 因為command line tool產出會是一個可執行檔, 不是一個app bundle, 所以不能包含任何的framework

第二, swift framework不能static link, 像是Erik, Kanna這些swift module, 都是dynamic lib

慘, 光前面這兩點就麻煩了, 開發這個ComicGo, 我用到了Erik, Kanna, Zip等等 , 這樣到底要怎麼辦? 跑起來就image not found

所以呢?土法煉鋼, 把這些module的codes全部引入到我的專案內(所以沒打算Open ssource, 太醜了), 這樣一來就解決掉問題了, 不過這功不算小, 因為Kanna相依libxml, Zip相依libz這些native lib

第三個坑, Erik是利用OS X裡面原生的WebKit去讀取網頁的, 因此他的設計是把載入網頁放到另一個DispatchQueue(javascript執行又是另一個), 但Command line邏輯很單線, 它並不會等callback回來才結束程式, 因此會發現怎麼Erik都沒動作就結束程式了, 因此必須要有個機制來卡住

這個機制就是RunLoop, 關於RunLoop這邊不多做解釋, 看一下官方文件 在程式內則是這樣:

let rl = RunLoop.current
var finished = false

while !finished {
    rl.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2))
}

當callback完畢後, 把finished設成true就可以結束整個程式了

Erik

好像還沒介紹Erik喔?其實有點想偷懶跳過了 :P

使用Erik來爬網頁其實很簡單,

Erik.visit(url: url) { object, error in
    if let e = error {

    } else if let doc = object {
        // HTML Inspection
		for link in doc.querySelectorAll("a, link") {
    		print(link.text)
    		print(link["href"])
		}
    }
}

只要有些CSS selector的觀念就可以了, 連querySelectorAll這名字都是一樣的, Erik並不是直接用Webkit去做CSS query的, 而是把webkit的內容拿來用Kanna解析, javascript的執行也一樣, 因此如果對html node有任何變動, 是不會反映到webkit裡面去的, 用Erik來爬的優點是專門針對那些動態網頁的, 有這個就簡單太多了!

做一個網路服務, 使用者驗證是蠻麻煩的一件事, 我們是可以裝作沒看到, 不做驗證, 但這樣的下場就是有假使用者, 有殭屍, 最簡單的方式是信任第三方服務像是Google, Facebook, 現在的人大多數都有Google, Facebook帳號了, 這樣其實也沒多大的問題, 但還是還是有人沒有, 而且也不是每個人都放心把Facebook帳號交給我們, 因此退而求其次就是用E-mail, 用E-mail認證雖然也是一個好方式, 但還是要建置整套發信機制(或是花錢買mailgun來送信), 而且在手機上就麻煩了, 來回在App跟e-mail間切換很不方便, 因此就會想用簡訊認證, 至於簡訊認證, 除了一個"貴"字以外, 要搞定各個國家的也是一個麻煩(當然, 花錢可解, 有Twilio這種服務可以用)

所幸有Facebook提供的這個可以用Account Kit, 在初期使用者不太多的時候, 不收費的確很吸引人呀(雖然他不是唯一一個這樣的服務, 之後再介紹其他的), 但由於他iOS的範例是用Objective C寫的, 我的Objective C實在不太行, 加上, 要了解一個東西, 寫一遍就知道了, 所以順手翻譯了一個Swift的版本, Source如下:

Account Kit samples for Swift

原本的版本, 我是覺得寫的不是太好, 花了好一些功夫看, 自己翻過來的這個版本, 也還沒debug過, 基礎的應該堪用啦! 至於iOS的account kit文件可以參考: iOS 專用 Account Kit

註: plist裡面寫的app id不是我的喔!是原本Sample用的

小時候很喜歡Indiana Jones系列的電影, 對於它裡面的地圖片段也一直覺得很有趣

如果這樣的動畫, 用在遊記類的blog上, 應該也蠻酷的, 但好像也沒一個比較好的工具, 因此想說用MapKit來實作一個試試

功能需求

先來定義一個簡單的功能需求

  1. 在起點跟終點畫一條連結線
  2. 一架飛機延這條線飛到終點
  3. 地圖視角跟著飛機走

實作

在起點跟終點畫一條連結線

這部份要用到MKGeodesicPolyline, 給它兩個點, 它就會自動連結成一條線, 但這條線並不是完美的直線, 因為地球表面是曲面的, 所以它是一條弧線

let coords = [start, end]

geodesicPolyline = MKGeodesicPolyline(coordinates: coords, count: 2)

print(geodesicPolyline!.pointCount)
mapView.add(geodesicPolyline!)

這邊coors只要給訂起始點跟終點的位置就好, 印出pointCount就會發現它把經由的點都補足了(實際上印出來的會多出2很多很多)

MKGeodesicPolyline裡的參數coordinates是一個UnsafeMutablePointer, 在Swift 3之前要寫成&coords, 但在Swift 3大改之後, “&“就不需要了

由於MKGeodesicPolyline是一個Overlay, 因此最後只需要用mapView.add (Swift 3之前是addOverlay)加入mapView就可以了, 但加入之後, 會發現, 這條線根本沒被畫出來, 那是因為少寫了一部分

在MapView裡面要畫出Overlay, 就必須要跟MapView說怎麼畫出這個Overlay, 這就要實作MKMapViewDelegate裡的mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay)

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    guard let polyline = overlay as? MKPolyline else {
        return MKOverlayRenderer()
    }
    
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.lineWidth = 3.0
    renderer.alpha = 0.5
    renderer.strokeColor = UIColor.blue
    
    return renderer
}

由於我們是要畫Poly line, 因此這邊回傳給它一個MKPolylineRenderer, 線寬是3.0, 線的顏色是藍色(alpha = 0.5)

這樣就可以很完美的畫出那條線了

在地圖上畫出飛機

這部份就要借重到MKPointAnnotation

let thePlane = MKPointAnnotation()
thePlane.coordinate = start //給定起始座標
        
mapView.addAnnotation(thePlane)

一樣, 這邊如果沒告訴MapView怎畫這Annotation, 它是會用預設的取代, 因此我們一樣要去實作MKMapViewDelegate

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    let planeIdentifier = "Plane"
    
    let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: planeIdentifier)
        ?? MKAnnotationView(annotation: annotation, reuseIdentifier: planeIdentifier)
    
    annotationView.image = UIImage(named: "ic_flight_48pt")
    
    return annotationView
}

這邊用一個UIImage來指定飛機的圖標

地圖視角

把飛機置於地圖正中央, 我們才看得到他, 因此, 需要設定可視的區域, 包含中心點跟範圍, 如下:

let span = MKCoordinateSpanMake(8.0, 8.0)
let region1 = MKCoordinateRegion(center: start, span: span)
self.mapView.setRegion(region1, animated: true)

沿著線飛

這時候要利用到 perform(#selector(updatePlane), with: self, afterDelay: 0.4) 讓它每隔0.4秒去更新一次飛機位置, 直到到終點為止, 當然也要一直更新region, 以讓飛機維持在正中央

func updatePlane() {
    planePositionIndex = planePositionIndex + step;
    
    if (planePositionIndex >= geodesicPolyline!.pointCount)
    {
        //plane has reached end, stop moving
        planePositionIndex = 0
        return;
    }
    
    let s = 8.0
    
    let nextMapPoint = geodesicPolyline?.points()[planePositionIndex]

    thePlane.coordinate = MKCoordinateForMapPoint(nextMapPoint!) 
    mapView.region = MKCoordinateRegionMake(thePlane.coordinate, MKCoordinateSpan(latitudeDelta: s, longitudeDelta: s))

    perform(#selector(updatePlane), with: self, afterDelay: 0.4)
}

結果

這邊有幾個缺點尚待改進

  1. 飛機閃爍
  2. 如果把可視區域範圍縮小, 或是位置更新過快, 就會造成地圖來不及載入的現象(可能需要預跑幾次將map tile載入cache內)
  3. 飛機頭永遠保持往上, 應該要朝著線方向轉

完整程式碼

最近直播蠻火紅的, 直播服務也不少, 連Facebook都做了直播功能, 最近也跟人聊了不少這方面的東西, 所以想說趁中秋連假來自己研究看看, 只是中秋節連假雖然多, 做的事情還真是不少, 看電影, 打球, 打電動, 看球的, 又碰上颱風天, 不過還是先搞個簡單的雛形唄

相關應用

市面上有不少關於直播的應用, 應該說, 簡直是氾濫, 而且每一種人氣似乎都是很旺, 還不是很了解在旺啥的, 現在可能也不少人認為當個網紅就可以一炮而紅之類的

  • Live.me
  • 小米直播
  • 17
  • 麥卡貝

這邊其實可以看出, 根本大同小異, 大多是賣肉…喔, 不是, 賣網紅, 後面故意舉了一個麥卡貝當例子, 它是稍微不一樣, 以賣直播節目像是運動比賽的轉播(嗚~~金鋒退休了). 而不是一般的UGC(User generated), 當然這邊也沒舉一般很流行的遊戲直播像是Twitch, 不過這類早已為大家所熟知了

不管是哪一種, 大部分的設計都是大同小異, 都是以單向直播為主, 輔以文字聊天室, 可以送個愛心或禮物, Facebook稍稍進階點, 會將直播過程錄製下來, 不只錄製節目, 還有過程中的互動, 不過大家都是蠻近似的

基本原理

如果照以上的功能設計, 簡單的可以畫成兩個部分, 一個是直播串流(Video Stream)的部分, 一個則是聊天室的部分, 大致上的後端可以以這兩個為核心

關於直播串流的技術部分, Facebook 曾分享了一篇關於他們做直播串流經驗的文章:

Under the hood: Broadcasting live video to millions

從這邊可以了解到, 串流需要能夠支撐到非常多人同時觀賞, 網紅可能數百到數千, 像是蘋果的發表會, 或是Google I/O, WWDC 這種會議則可能是數萬到數十萬, 所以服務的高並發, 高流量是可以預期的, 架構上也要能夠承受這樣的強度, 簡化的畫起來應該像是這樣:

graph LR; C[Client]--publish-->M[Media Server]; M--forward-->E(Edge Server); M--forward-->E2(Edge Server); M--forward-->E3(Edge Server); E-->V(Viewer); E-->V2(Viewer); E2-->V3(Viewer); E2-->V4(Viewer); E3-->CDN; CDN-->V5(Viewer); CDN-->V6(Viewer); CDN-->V7(Viewer);

Viewer不直接從Media Server取串流內容是考量到Media server通常要接收多個Client發佈的串流, 一個假設性的想法是, 對於UGC(User generated content), 主播應該遠少於觀眾, 假設就算一個服務可以吸引到百萬級別的觀眾, 同時線上的主播應該了不起是幾千個而已, 即便如此, Media server本身從Client接收發布的串流資料後, 可能還需要做轉碼(transcoding), 和轉送的動作, 尤其是轉碼是較為耗CPU資源的工作, 如果把主播跟觀眾放在同一個伺服器上, 除了影響品質外, 也會不方便擴充, 因此減少Media server上的"觀眾"(讓觀眾只是其他少數的edge servers), 便可以在觀眾增加時相對好擴充容量(增加edge的數量)

但大部分的狀況來說, 每個直播的觀眾不一定是非常大量, 在Facebook那篇文章內也有提到, 在小量觀眾的狀況下, 分流到多個edge的效率應該就沒那麼的好了, 反而這時候放在同一台server減少延遲會是更好的選擇

串流的通訊協定有不少, 像是RTP, RTSP, RTMP, HLS, WebRTC等等, Facebook那篇文章主要提到的是RTMP, HLS, 查了一下資料, 似乎這兩個也是目前比較主流做直播用的, 雖然WebRTC被討論的也蠻多的, 但似乎比較沒被應用在大量的直播, RTMP跟HLS都是可以透過HTTP來做傳輸(RTMP需要做封裝 - RTMPT), 讓他們具有穿越防火牆的優勢, 而HLS是以檔案為基礎的, 所以適合用一般的CDN來做快取, 在做大量的直播優勢較大, 缺點是延遲太長了, 但這兩者其實也是可以合併使用的, 在小群體時用RTMP, 等觀眾成長到一定數量實再導流到HLS去

Proof of concept 的初步想法與簡單的設計

幾個初步的想法

  1. 直播 = live stream + chat room
  2. 現在直播應用很多, 所以應該不少現成的open source解決方案可以套用, POC可以從這些東西下手, 不用重造輪子
  3. 需求: Client發布直播後, Viewer可以知道現在有誰在直播並觀看, 並可以透過訊息聊天

發布

sequenceDiagram participant Client participant Register participant MediaServer participant Viewer Client->>Register: I want to go live activate Register Register-->>Client: Here is your ID and token deactivate Register Client->>MediaServer: Publish(id,token) activate MediaServer MediaServer->>Register: token valid? activate Register Register-->>MediaServer: Yes deactivate Register MediaServer-->>Client: OK deactivate MediaServer Client->>Register: I am ready to live activate Register Register->>Viewer: Somebody is ready to live activate Viewer deactivate Register Viewer->Viewer: Update UI deactivate Viewer

觀賞直播

這部分就沒什麼特別的了, 當一般的chat room做就好

先看一下成果

這成品有點粗劣, 有點不好意思 :P

我publish client只實作iOS版本, 而Viewer只實作了Android版本(根本只各做一半嘛!!/翻桌), 後端用firebase處理資料的部分, 所以即時通知新的直播, 和聊天沒啥問題(但我聊天的UI還是沒刻 >"<)

相關解決方案

既然沒有重新做輪子, 自然用了不少Open source的解決方案來達成, 從Server到Android, iOS要找到相關可用的實在不難, 可以說這部份實在是太成熟了, 研究完後覺得Facebook那篇文章也只是一般般而已

Steaming server

RTMP相關的解決方案還算不少, 這邊列幾個

  1. Nginx RTMP Module - 架構在Nginx之上, 也算老牌了, 支援RTMP和HLS, 但看code base, 實在也沒啥在更新
  2. Mona server - 支援RTMP, HTTP(非HLS), Web socket等等
  3. Red5 Media Server - 支援RTMP, HLS, WebSocket, RTSP, 好像是要錢
  4. Simpe RTMP Server - 這是由中國的觀止雲這家開源出來的, 講"Simple"其實一點都不Simple, 輕量, 穩定(至少我試直播一晚上都還蠻順利的), 好擴展(支援forward to edge), 可RTMP轉HLS, 因此我最後選擇這個方案
SRS (Simple RTMP Server)

硬體

我沒看到文件有寫硬體需求, 但我用Digital Ocean 1GB RAM, 30GB SSD的Droplet跑, 單一個直播, 直播好幾個鐘頭, CPU都不超過5%, 所以應該足夠

安裝

安裝上相當簡單

  1. 從git上抓下來: git clone https://github.com/ossrs/srs.git
  2. 切換到相對應版本的branch(我是用2.0release) - git checkout 2.0release
  3. cd srs/trunk; ./configure ; make ;

建置好的執行檔會在srs/trunk/objs目錄下, 可以直接執行

設定

conf目錄下有很多不同的設定檔可以參考, 因為我要試RTMP, HLS所以我用的設定檔如下:

listen              1935;
max_connections     1000;
srs_log_tank        file;
srs_log_file        ./objs/srs.log;
http_api {
    enabled         on;
    listen          1985;
}
http_server {
    enabled         on;
    listen          8080;
    dir             ./objs/nginx/html;
}
stats {
    network         0;
    disk            sda sdb xvda xvdb;
}
vhost __defaultVhost__ {
    hls {
        enabled         on;
		hls_fragment    10;
        hls_window      60;
        hls_path        ./objs/nginx/html;
        hls_m3u8_file   [app]/[stream].m3u8;
        hls_ts_file     [app]/[stream]-[seq].ts;
    }
}

相對應的設定可以參考文件

執行

很簡單: srs -c my.conf 即可

iOS RTMP Publish

找到iOS支援RTMP publish的解決方案有幾種:

  1. VideoCore - 這個我還沒去試過, 不知道好不好用, 但似乎有支援Filter和Watermark, 感覺蠻威的
  2. lf.swift - 簡單, Swift做的, 這兩個是優點, 對我這個只看得懂Swift看不懂ObjC的, debug是比較方便, 但除此之外好像也沒啥特色了
  3. LiveVideoCoreSDK - 這文件不多, 我暫時就沒試了, 也支援濾鏡, 而且似乎這個作者也提供了Android版本, 只是好像沒支援Cocoapods或Cathage, 有空再來玩玩
  4. LFLiveKit - 我最後是選用這個, 簡單, 且"自帶美顏"(現在騙人是很基本的)
lf.swift

雖然文件上有提供cocoapods跟Carhtage的安裝方式, 但絕對不要用Carthage的那個, 第一原因是它Carthage支援似乎尚未搞定, 就算改點東西解決了它, 是可以安裝成功沒錯, 但會崩潰在XCGLogger, 似乎用framework含進來的方式會導致XCGLogger == nil, 這害我花了好多時間, 畢竟我是Carthage的愛用者, 後來轉用Cocoapods就沒事了

幾個需要加的部分:

AppDelegate.swift

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        XCGLogger.defaultInstance().outputLogLevel = .Info
        XCGLogger.defaultInstance().xcodeColorsEnabled = true
        
        return true
    }

這兩行是設定logger要記錄的東西, 方便debug用

ViewController

override func viewDidLoad() {
	rtmpStream = RTMPStream(rtmpConnection: rtmpConnection)
    rtmpStream.syncOrientation = true
    rtmpStream.attachAudio(AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio))
        rtmpStream.attachCamera(DeviceUtil.deviceWithPosition(.Back))
    rtmpStream.addObserver(self, forKeyPath: "currentFPS", options: NSKeyValueObservingOptions.New, context: nil)
        
    rtmpStream.captureSettings = [
            "sessionPreset": AVCaptureSessionPreset1280x720,
            "continuousAutofocus": true,
            "continuousExposure": true,
        ]

    rtmpStream.videoSettings = [
            "width": 1280,
            "height": 720,
        ]
    lfView.attachStream(rtmpStream)

    view.addSubview(lfView)
}

直播的部分會跟他的LFView綁一起

LFLiveKit

後來選用LFLiveKit的原因不是因為他自帶美顏 :D, 他寫法跟lf.swift一樣簡單, 而且不一定要把preview加到UI裡面, 而且他的preview不用用特定的class,只要是UIView就可

範例直接貼他文件裡的就很清楚了:

// import LFLiveKit in [ProjectName]-Bridging-Header.h
import <LFLiveKit.h> 

//MARK: - Getters and Setters
lazy var session: LFLiveSession = {
    let audioConfiguration = LFLiveAudioConfiguration.defaultConfiguration()
    let videoConfiguration = LFLiveVideoConfiguration.defaultConfigurationForQuality(LFLiveVideoQuality.Low3, landscape: false)
    let session = LFLiveSession(audioConfiguration: audioConfiguration, videoConfiguration: videoConfiguration)

    session?.delegate = self
    session?.preView = self.view
    return session!
}()

//MARK: - Event
func startLive() -> Void { 
    let stream = LFLiveStreamInfo()
    stream.url = "your server rtmp url";
    session.startLive(stream)
}

func stopLive() -> Void {
    session.stopLive()
}

//MARK: - Callback
func liveSession(session: LFLiveSession?, debugInfo: LFLiveDebug?) 
func liveSession(session: LFLiveSession?, errorCode: LFLiveSocketErrorCode)
func liveSession(session: LFLiveSession?, liveStateDidChange state: LFLiveState)

Android stream player

寫到後面有點累了, 也有點懶了, 還剩下Android這塊沒寫, 這邊就只列出方案不寫細節了, 主要我測過:

  1. ExoPlayer: Google開源的Media player, 之前在做另一個東西時我有用過, 所以第一時間就想起這個, 不過, 原生的完全不支援RTMP, 不過可以參考這邊, 但我實際上用, RTMP完全沒成功過, 一直出現FLV parse的問題, 倒是HLS沒問題
  2. Ijkplayer - 這同時有Android和iOS版本, 是Bilibili開源的, 有點強大, 但基於ffmpeg, 不知道在license上會不會有風險, 使用起來還有點複雜, 但HLS, RTMP都是沒問題
  3. PLDroidPlayer - 七牛雲針對ijkplayer的再製品, 比較方便的是, 它有封裝出一個video view可以直接使用, 相較於ijkplayer來說比較簡單易用

承續上篇的用icon font來製作圖示, 之前所提到的都是利用現成的icon font, 但似乎大部分的icon font都沒有像material icon有支援ligatures, 沒支援的話, 在xcode裡面就無法像上一篇一樣, 直接在UI designer顯示對應的圖示, 另外如果需要使用自己的圖示呢?其實是有方法用SVG圖檔來製作自己的icon font的, 這篇就來介紹兩種用SVG圖檔製作一個有ligatures支援的字型檔

grunt-webfont

第一個方法就是利用grunt-webfont, Grunt是一個前端常用的建構工具, 而grunt-webfont是一個用來產生字型的task

安裝相關工具

由於需要使用Grunt, node.js是必須的, 另外由於需要使用到fontforge, 所以python也是必須的, 雖然說grunt-webfont也可以純nodejs的module來產生字型, 但那並無法支援ligatures, 所以fontforge是一定需要的

npm i grunt --global來安裝grunt

製作字型
  1. 建立一個空的目錄
  2. 在這個目錄執行npm init來產生package.json
  3. npm i grunt-webfont --save來安裝grunt-webfont並且把這個dependency 加到package.json
  4. 建立一個svg子目錄(目錄名稱隨你高興, 這邊以svg當例子), 把所有圖示的svg檔案全部放到這目錄去
  5. 建立Gruntfile.js , 這檔案就像是Makefile, 或像是build.gradle這樣的角色, 內容就像下面
module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    webfont: {
        icons: {
                src: 'svg/*.svg',
                dest: 'build/fonts',
                options: {
                        engine: 'fontforge',
                        htmlDemo: true,
                        fontHeight: 96,
                        normalize: false,
                        ascent: 84,
                        descent: 12,
                        font: 'octicon',
                        fontFamilyName: 'octicon',
                        types: 'ttf',
                        ligatures: true,
                        startCodepoint: 0xF101
                    }
                }
    },
    clean: [
        'build/fonts/*'
      ]
  });

  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-webfont');
  grunt.registerTask('font', ['clean', 'webfont']);
  grunt.registerTask('default', [ 'font']);
};

這邊最主要也最重要的task就是webfont這個, 這裡面src是svg檔的目錄, dest是字型輸出的目錄, engine的部分指名fontforge, ligatures設定必須要是true(產生的字型的ligature的名字其實就是沿用svg的檔名)

建立好這個檔後執行grunt即可

icomoon

上面的方法還是有點麻煩, 蠻手動的, 還有一個更方便的工具就是icomoon, 這東西方便更多, 它是一個相當強大的工具

Icomoon

從畫面上看, 它其實很簡單操作, 選定你所需要的圖示後, 按右下角的Generate Font即可, 除了你可以自己import自己的svg檔案外, 它也提供很多付費跟免費的圖示供選用:

Icomoon

按下Generate Font後, 並不會馬上讓你下載字型回家, 它會先讓你檢視字型將會包含的圖示, 這邊有件事很重要, 左上角有個fi圖示(參照下圖), 按下去後, 下面的圖示下面會多一個fi的欄位, 這就是讓你設定這些圖示的ligature的, 如果需要一個有支援ligature的字型, 就需要去設定這邊

Icomoon

所有都沒問題後按下右下角的Download就沒問題了