2018年4月25日水曜日

[安藤]android studio 升級為3.1版的時候遇到的編譯問題與在android 8.0 遇到的google drive app bug

這次升級的痛苦程度不亞於之前遇到的exit 1 魔咒... Orz

主要的問題是android studio 3.1的log相關的ui有很大的改變,
一開始根本就不知道詳細的log在哪裡。出錯的時候只看紅字的exception,卻又完全沒有提示到底是哪裡有問題,只給了你一堆exception xx more...

用之前的最終解決方案:build setting裡面加上 --debug --stacktrace,這些訊息也不會出現。也一直找不到討論...

直到搜尋到有人提醒「可以在Run/Build configuration裡面加上一個Gradle的指令,參數指定為build」,才看到Debug console的出現。而這才是真正該提到的解決問題的起點。

會顯示完整錯誤的debug console長這樣:


然後開始解決遇到的問題:

AAPT2相關:
  • android:color/transparent之類的參數在AAPT2裡面已不給用。請自行定義@color/transparent
  •  <animated-vector> tag不被支援。

lint的錯誤被提升到會被abort的等級。而且麻煩的是,並不會一口氣列出全部的錯誤。可以一個個的解完重編到全部處理完。也可以強制跳過。
若要強制跳過的話,在各個module的build.gradle裡面加上:
android.lintOptions {
    abortOnError false}
有遇到模組裡面需求了特殊的硬體權限,卻又沒有宣告在manifest裡面,就會導致lint error。


若是有用到第三方的套件需要從網路自動裝的話,build.gradle的repositories需加上設定:
    repositories {
        jcenter()
        google()
    }
假如是從2.3升上來的,通常會少「google()」這個參數。


解完問題之後,記得要切換Run/Build configuration回到Android App才能回到正常的編譯流程。另外要注意的是,gradle的build流程假如需要編譯的模組很多很大的時候,會相當緩慢。建議把記憶體開大一點。(gradle.properties裡面加上參數
org.gradle.jvmargs=-Xmx2048m

然後找尋其他的資料的時候才發現,其實Debug console只要點選下圖標記的那個「toggle view」就會出現了... Orz  這樣大家就可以少走點冤枉路。



P.S: 在android 8.0使用google drive app點選製作好的已簽名apk進行下載安裝測試,可能會遇到「安裝包解析錯誤」的問題(就算是打開「允許安裝不明來源檔案」一樣會有問題)。這時請在google drive app上面改成長壓檔案,選擇下載,下載完畢之後再從系統事件條,或是檔案管理員的下載資料夾裡面點選檔案安裝,就會成功。


參考文件:
Android Studio 3.0正式版填坑之路   這一篇真的幫了不少忙。文中沒提到的其他細節這裡都有。

2018年3月6日火曜日

[ios]xcode 無痛開發之路 (xcode操作/ project setting欄位介紹 / coredata / ui layout / 編譯 / 打包ipa / 上傳至itunes connect )

藉由本篇,來對筆者在兩個月內密集遇到的ios開發問題做一個記錄。方便之後查找,也希望能夠幫到其他人。


第一步:認識*.xcproj。 其實這只是一個目錄(從命令列才看得出來。要使用finder觀看的話要在檔案上面按滑鼠右鍵,選擇「觀看包裝內容」... apple quality)。其內容如下:
  • *. xcworkspace :記錄著此project的xcode的設定,也是一個目錄... 內容不研究。
  • *.pbxproj:記錄著此project的所有設定。像是compile流程相關,檔案包含,檔案路徑相關資料。要注意的是,同時有兩人以上編輯同一個檔案,加入同樣的檔案,出來的結果會不一樣。因此在多人開發的環境,建議在pbxproj有檔案變動的時候,只由一人commit,其他人pull update之後再由一個人做add/更改自己的檔案的動作,然後commit,直到全部做完。

第二步:認識project setting。筆者認為是整個iosapp開發流程裡面最重要的步驟。因為只要一踏出xcode的自動保護之外,編譯錯誤隨之而來,而且還不一定會提示該怎麼解。

總之先點選project,讓xcode顯示設定。



文字編輯的方法:把游標移動到條目上面,在條目上面滑鼠點一下左鍵,可以編輯整條文字。條目之間xcode是以半形空格分開(也因此強烈不建議路徑/檔名等等命名有空格)。編輯完畢要讓設定值有效,建議按一下Enter鍵。有時候直接移動編輯的游標至其他條目,會被當做是cancel,而不一定會套用新的設定值。滑鼠點兩下的話會打開編輯框,條目的增減使用編輯框左下角所顯示的「+」「-」符號操作。

  • 圖上的1:要操作project的設定的時候,必須點選此地方才會顯示設定。同時這個區域也是xcode的project檔案列表。xcode的檔案列表跟實際的檔案目錄結構不一定會同步,沒有列在這裡的檔案,xcode就不知道它的存在。另外,除了資源類的檔案之外,建議使用黃色的「Grouping」資料夾,不要使用藍色的路徑資料夾。就如圖上所顯示的xcode預設的設計。

    雖然藍色的路徑資料夾會自動的增減檔案,但是不易分辨檔案的編譯列表跟打包列表的增減會不易判斷。在「Grouping」資料夾刪檔,檔案可以選擇保留,不會消失,而編譯列表跟打包列表會自動除名該檔案。

    拖拉檔案進入這個列表的時候,會出現對話框選擇target,請仔細選擇。沒勾選到的target在編譯時就不會含入此檔案。(可以之後按照檔案類型,自行在後面提到的「Build phase」區加入)
  • 圖上的2:若是此紅框的區域沒有出現,請先點選「2」左側的藍色icon,此區域就會出現。
  • 圖上的3:切換設定值分類的地方。
  • 圖上的4:build scheme。指定目前build/run的設定值。 左半部可以切換target setting(下面會提到),右半部用來切換執行的硬體。執行的硬體分成三類:
    ・實機(把iphone接上macbook之後就會出現在選項內),
    ・「General Device」(製作app的專用選項。發現Products->Archive被設成灰色無法輸出app檔案的時候,應該是build scheme沒有被改為「General Device」),
    ・各種simulator device。
    另外要提的是,把游標停留在左半部,按滑鼠右鍵,可以選擇「edit scheme」,裡面可以調整build scheme為debug mode還是release mode。有助於debug 只有release mode會遇到的問題。筆者第一次用到這功能,是在debug release mode之後發現假如有使用compiler的optimization,會導致block宣告回傳null...暫定方案是關閉compiler的optimization,目前還沒找到正式的解決方案。
  • 圖上的5:顯示當時在「2」區域所指定的檔案或是檔案內的可視元件的相關設定。像是程式碼檔案的話就會顯示「Target Membership」,語系檔案的話就會顯示可以勾選Localization的設定,layout相關的話就可以指定class name,layout參數等等。


圖上的2:project setting 跟target setting的不同: 
project只有一個,裡面放置的是project共通的設定。target setting可以有很多組。可以把它當成用同一套程式碼產生不同的app。

project setting,主要的調整參數以Tab分成兩區(圖上的數字「3」的區域):

Info區:
  • Deployment target:直接選擇最新版。目前(ios11.2)的時代也不支援ios7以下的版本了。
  • Localizations:可以增減此project支援的語言。是xcode一個不易理解的部分。目前只知道以下的規則:
    ・各種資源檔(除了project相關檔案/程式碼(.m/.h等等)/lib跟framework/datamodel/xcassets以外的檔案)都可以被Localization。
    ・Localize過的檔案,在finders裡面看到的會是在「xxxx.lproj」資料夾內。xcode內部的擺放運作方式:「以該檔案的路徑,建立一個「xxx.lproj」目錄,然後移動該檔案進去該資料夾」。

    要注意的是,自行照此規則去做(建立資料夾跟擺檔案進去),xcode是認不得的。建議的操作流程:用finder/命令列把檔案copy到project下面想擺放的目錄,然後拖進xcode視窗左方的檔案列表裡面的想要擺放的目錄(上圖的「1」區),在xcode的project的檔案列表內按左鍵點選該檔案,再到xcode視窗右側選擇想要進行Localizations的語言。
    ・勾選「Use Base Internationalization」的話,「Development Language」的語言目錄不會出現在project的檔案資料夾,而會以「Base.lproj」代替。
    ・同一個檔案用在多個語言,xcode的做法是自動複製成為多個檔案。所以改動其中一個語言的檔案內容,其他語言的檔案並不會被更動。
    ・如何修復沒有Localizations的project(會發生在把Localizations的所有語言都砍掉的狀態,這時想要建立Localization會因為列表內無檔案而無法建立。apple quality...):在project的根目錄裡面建立一個「Base.lproj」目錄,然後放一個可以被Localize的檔案進去,然後到Localizations下面勾選「Use Base Internationalization」,這時就可以看到列表內有檔案。
    ・NSLocalizedString()若是該語言裡面沒有該字串的替代字,並不會使用「Development Language」(Base)的替代字,請注意。

Build Settings區:這邊建議全部不要動。因為Target也有一樣的Build Settings,而且設定值會是以Target的為優先。


設定完project的整體設定之後,接下來點選TARGETS裡面的project name,來設定target setting。要新增target,圖上的2區的下方有「+」「-」符號可以操作。


設定的項目非常多。本篇只談筆者遇到有做調整的地方。這樣就寫不完啦...




General區:
  • Display Name:app在系統桌面上所顯示的名稱。若是給空值的話,xcode會自動用灰色補上 Info.plist 裡面所給定的「Bundle Name」的值。
  • Bundle Identifier:app的辨別id。會影響到app儲存在系統裡面的路徑。只能使用英文字(有分大小寫),數字,「-」,「.」。官方建議以reverse domain name的格式命名。像是「com.yourcompany.yourappname」。跟android的app相同的是,在app store上架之後就不能改。
  • Version:app的版本編號。只能使用數字跟「.」,建議以「(主版號).(副版號).(小修改編號)」三個數字的方式編寫。例如「1.0.1」。上架之後,版號數字在上架的時候只能增不能減。
  • Build: 必須為數字。通常是定義為編譯次數,不過xcode不會自動累加... 此數值在上架的時候只能增不能減。
  • Automatically manage Signing:也是ios一個很麻煩的地方。
    要讓連接到mac book的iphone可以debug app,必須先到 apple developer 註冊帳號,建立「iOS Certificates」(必須使用osx系統裡面的key chain access app去要求apple的server給出CSR,然後把CSR上傳到apple developer,從apple developer下載證書,再安裝到keychain access裡面),註冊app,註冊iphone(測試機必須註冊),建立provisioning profile(這步完成之後,Automatically manage Signing就會正確運作)。
  • Team:「Automatically manage Signing」設定完成之後這邊就有選項可以選,或是可以線上從apple developer,或是在不使用「Automatically manage Signing」的狀態,import從apple developer下載來的provisioning profile。
  • Deployment target:直接選擇最新版。目前(ios11.2)的時代也不支援ios7以下的版本了。
  • Devices:可以選擇「iphone」「ipad」「universal」。選擇預設値「universal」即可。 
  • Main interface:指定app在顯示完launchscreen之後,首先呈現的xib/storyboard file。這個參數會根據main.m裡面的delegate的導入方式而有不同的處理方法。假如習慣完全用程式碼處理,可以不設定此欄位,直接在delegate裡面去讀取想要呈現的xib/storyboard file。
  • Device Orientation:指定此app支援的螢幕旋轉方向。
  • App Icons Source:指定app在系統桌面上顯示的icon。必須在project裡面擁有 「*.xcassets」的檔案,並在裡面增加「AppIcon」,下拉選單才會有選項可選。「*.xcassets」路徑的增加方法:在project的列表內,點選要增加檔案的目錄,按滑鼠右鍵選擇「Add file」,跳出來的視窗選擇「Asset Catalog」。
    然後在該Asset Catalog裡面點選新增檔案,再選擇「App Icon」,然後依照指定大小拖拉檔案進去。請一定要用選取的方式新增「App Icon」,新增非「App Icon」的image,不會出現在下拉選單裡面。
    注意:AppIcon若是「Devices」的設定為「Universal」,必須加上ipad支援的格式的圖。沒有的話,在上傳到itunes connect的時候就會出錯。若是原本的AppIcon沒有勾選ipad的話,滑鼠點選AppIcon之後到視窗右邊欄選擇最右邊的icon,出現的選項裡面就會有ipad可以勾選。
  • Launch images source: 指定app起動的時候首先顯示的圖檔。筆者不建議使用。因為使用這方法,必須在「Asset Catalog」裡面新增「launch image」。還必須因應各種不同的螢幕size跟螢幕轉向,裁切xcode指定長寬的圖,非常惱人而且佔空間。
  • Launch Screen File: 指定app起動的時候首先顯示的storyboard/xib檔名。不需要指定副檔名,iOS起動app的時候會自己找.storyboard或是.xib。
    這是替代「Launch images source」的方法。好處是可以藉由storyboard提供的auto layout功能,達成只要提供一個啟動頁的layout就可以解決所有機型的問題。
  • Embedded Binaries:指定app在輸出的時候,需要跟app一起打包的libraries跟framework。iOS內建的lib跟framework以外的都需要加進來。
    若是不知道該加哪些的話,可以先不加,在app 的linking階段出現找不到function的時候再一個個加進去修正,或是在debug的時候發現xcode console出現「dyld: Library not loaded」的時候再加也是可以。

    但是有一些第三方的library不能加。像是Fabric跟Crashlytics...加了之後在release app的export階段會出現錯誤「Found an unexpected Mach-O header code: 0x72613c21」。推測是framework裡面有含入一些app相關的設定檔(像是 Info.plist之類的)。
  • Linked Frameworks and Libraries:指定此target會用到的Frameworks and Libraries。要使用ios內建的Frameworks and Libraries,必須使用此選項的「+」號才會出現列表,也才能找到想用的東西並加入target裡面linking兼使用。


Build Settings區:
參考圖:


  • Valid Architectures:編譯的cpu架構。需要自己加的選項有「arm64」(iphone5以後),「x86_64」(iphone模擬器),「armv7」(watchos)。概念:iphone的cpu已經全部都是64bit,即為「arm64」。ios app的開發平台(macbook)的模擬器(simulator),cpu架構是macbook用的cpu:x86_64,因此想在iphone跟xcode的模擬器都可以執行app,「arm64」跟「x86_64」是必須加進去的。有使用到第三方,非原始碼的lib或是framework,也要注意該lib有沒有包含需要的cpu架構。確認的方法:「lipo -info [library name]
  • Build Active Architectures only:是不是只編譯目前所指定的硬體(上圖的「4」號區域裡面可以選擇)裡面所記載的cpu架構。建議啟用。
    因為xcode要是在後面會談到的設定「Embedded binaries」裡面有放lib,在做app的release的時候會check lib的bitcode,xcode要求「Embedded binaries」的lib,只能包含「該裝置支援的架構的程式碼」。若是在simulator上面開發,之後要release的時候,要是需要導入的library不支援動態載入,必須以「Embedded binaries」的方式含入的話,可能要做framework search path的切換。
  • Supported Platforms: 選項有「macOS」「iOS」「tvOS」「watchOS」。根據app的目標使用平台選擇。
  • Complier for C/C++/Objective-C :選擇預設值「Defalut Compiler(Apple LLVM 9.0)」。
  • Debug information format:設定為預設值「DWARF with dSYM file」
  • Enable Bitcode:iOS9之後的新功能,建議啟用。以apple的習慣,說不定不久之後會要求強制啟用。使用的library或是framework不支援bitcode的話,opensource的lib可以自己編譯成帶有bitcode。付費的lib,直接要求lib的提供者support吧。
  • Other Linker Flags:建議加上「-ObjC -v」。「-ObjC」的作用是將所有的.obj都產生object-c class。有用到第三方Framework/lib的話必須加上此選項,才能使用selector的語法來call c的function,否則只能用C的傳統function call的方法。 「-v」可以讓linker輸出比較多資料,方便判斷編譯錯誤的原因。
  • Info-plist file:為重要參數。指定「Info.plist」的檔案路徑(不一定等於project內的路徑)。所有的target都必須要有一個「Info.plist」檔案。可以針對不同的target,準備不同的plist檔案因應不同需求。因為預設的檔案名稱為「Info.plist」,在多個.plist同時存在的project,一般建議的命名方式為「<target name>-Info.plist」。
  • Product Bundle Identifier:跟General 區的「Bundle Identifier」相同。修改的時候會相互影響,可以當成是同一個參數。
  • Product Name:預設會以「$(TARGET_NAME)」做為參考值。
  • Framework Search Paths: 指定需打包進app的Framework的檔案搜尋路徑。不過因為第三方的framework在做release的時候,.a檔案必需只有該app的對象的硬體的cpu 架構,例如給iphone/ipad的就只能有arm64,不能包含x86_64(但是debug階段想要同時在simulator跟測試硬體上跑,卻又要兩種都有),結果還是得在不同
  • Header Search Paths:指定Header檔案的搜尋路徑。一般用在導入library的時候,該lib所附帶的header檔。有指定路徑的話,沒加到project file list的header檔案也可以被搜尋到而不會出現compile error。
  • Library Search Paths: 指定lib檔案(通常附檔名為「.a」)的搜尋路徑。有指定路徑的話,沒加到project file list的lib檔案也可以被搜尋到,而不會出現compile error。
  • Optimization Level:建議設定為None。debug一定要設為None,否則中斷點可能會不正常動作。release也建議設定為None,因為前面有提到,遇過一個跟最佳化有關的問題:假如有使用compiler的optimization,會導致block宣告回傳null...暫定方案是關閉compiler的optimization,目前還沒找到正式的解決方案。
  • Prefix Header:指定在各個檔案編譯的時候一定會含入的檔案名稱(不需在程式碼檔案內指定)。像是把大量的constant放在一個檔案裡面指定的時候,將該檔案指定為Prefix Header,就不需要在每一個程式碼檔案裡面import它。
  • Obj-C Automatic Reference Counting(ARC):基本上目前的第三方開源lib大部分已經使用ARC方式開發,建議project不要使用之前的MRC的方式開發。強制使用MRC不只得自己控制release的方式,對於ARC的程式碼,跑起來可能會出現無法預期的問題,也只會徒增自己麻煩。




Info區:
內容等同於「Info.plist」。在build setting區的「Info-plist file」設定檔案「Info.plist」之後,切到這個區域的時候就會自動帶入「Info.plist」的內容。大部分的設定値,跟General區的類似名稱的項目有關。
  • 新增條目:把滑鼠游標放在已存在的條目上面,條目的右邊會出現「+」「-」符號。點選「+」就會新增條目,點選「-」就會刪除該條目。
  • Bundle Name:對應的xml tag為「CFBundleName」。若是要套用General Tag提過的「Display Name」的設定值的話,可設定為「${PRODUCT_NAME}」。想根據不同語言而有不同的顯示名稱的話,必須將Info.plist給多國語言化才行。
  • Bundle identifier:若是Info.plist 要套用General的設定值的話,參數為「$(PRODUCT_BUNDLE_IDENTIFIER)」 
  • Main storyboard file base name:等同於General區的「Main interface」裡面的設定。修改文字的話,兩邊的値會互相影響。
  • Launch screen interface file base name:等同於General區的「Launch Screen File」的設定。一樣不需要指定副檔名。
  • Bundle version string. short:等同於General區的「Version」的設定。修改文字的話,兩邊的値會互相影響。
  • Localization native development region:預設值是:「$(DEVELOPMENT_LANGUAGE)」,但是「$(DEVELOPMENT_LANGUAGE)」要修改,從xcode的ui操作相當麻煩。一般的建議方法是關閉xcode,到.xcodeproj裡面直接修改.pbxproj的「developmentRegion」。

Build Phases區:
  • Compile Sources:需要編譯的程式碼。只有放在這裡的程式碼檔案才會被編譯。
    只擺.m就好,不要擺.h(header檔)。

    執行project file list的檔案新增與刪除的動作,會提示該檔案要加到哪個target內。在此時有勾選或是加入project file list之後有點選該檔案並操作該檔案的「Target Membership」(這選項在視窗右側)的話,xcode也同時會在這個列表增刪檔案。不過因為這個列表可以手動操作,在編譯的時候遇到缺function,可以來這個列表確認檔案是不是有放進來。
    此選項也可以一次大量增加需要編譯的檔案,因為要增加編譯程式碼的檔案,只有這個選項的「+」所跳出的列表框可以多選檔案。而且這個列表不會列出重複檔案,可以防止因為檔案重複加入此列表而產生的linker錯誤:Duplicate symbol。同樣的,發現Duplicate symbol,直接來這個設定找看看有沒有重複定義同一檔案。
  •  Link Binary with Libraries:這個列表的內容會跟General區的「Linked Frameworks and Libraries」相同。對其中一個列表做修改,結果也會相互影響。全新的project,一開始沒有任何的Framework加入的狀態,會看不到這個設定區。
  • Copy Bundle Resources:所有app相關的資源檔案(Info.plist / Assets / 圖檔 / strings / raw file / xib / storyboard / ssl證書等等) 都要加在這裡。沒加入的話,在執行階段就會拿不到檔案...
  • Embed Frameworks:這個列表的內容會跟General區的「Embedded Binaries」相同。對其中一個列表做修改,結果也會相互影響。全新的project,一開始沒有任何的Framework加入的狀態,會看不到這個設定區。
「Link Binary with Libraries」跟「Embed Frameworks」的差異:目前還沒有研究透徹。只知道「Link Binary with Libraries」加入之後,在compile階段才不會找不到function。使用ios內建的framework,只要加入「Link Binary with Libraries」就好。
第三方的framework,建議除了加入「Link Binary with Libraries」,也要加入「Embed Frameworks」。這樣app的執行階段才不會發生動態載入找不到檔案的問題。(錯誤訊息:dyld: Library not loaded:Reason: image not found)


備註:一小部分參照用的參數命名
    $(TARGET_NAME):就是target name。
    $(PRODUCT_NAME):Build Setting區的「Product Name」。
    $(PRODUCT_BUNDLE_IDENTIFIER) :Build Setting區的「Product Bundle Identifier」
    $(EXECUTABLE_NAME):執行檔名。
    $(DEVELOPMENT_LANGUAGE):app的預設語言。
    $(CONTENTS_FOLDER_PATH) : 一個以Build Setting區的「Product Name」為參考值的參數。會自動加上「.app」做尾修飾。
    $(PRODUCT_NAME:c99extidentifier) :  一個以Product Name為參考值的參數。會在Productname前面加上底線符號修飾。
    $(PROJECT_DIR) :project的根目錄。常用在指定檔案path相關的設定。
    $(SRCROOT):程式碼的根目錄。在xcode的環境下,內容等同於$(PROJECT_DIR)。
    $(inherited):在Target setting裡面繼承Project setting同欄位的內容。


到這裡,算是把project的設定都走完一遍了。
開發相關:

  • Obj-C的編譯器已經會自動判斷是不是有重複include header檔案。不過還是要避免兩個.h檔案互相依賴的導入方式,會產生預料之外的狀況。
  • LaunchScreen使用xib或是storyboard的方式顯示,可以獲得自動layout的好處,不過還是無法以程式碼操作LaunchScreen階段所顯示的View的內容。
  • class/method宣告方式:
    「定義」放在.h的@interface內。修飾放在@interface的大括弧外面。
    「實作」放在.m的@implementation內。
    都需要用@end結尾。
    一個.h可以放多個@interface。
  • @interface也可以跟method一起放在.m裡面。對於不會跟其他檔案共用的interface,可以用這方法減少.h檔案的數量。
  • self = java/c++的this.
  • 變數宣告:
    全域宣告:跟c一樣為static。
    宣告變數的位置的關係:
    @interface的大括弧內宣告,scope相當於@protected。
    @interface的大括弧外宣告,scope相當於@public。
    @implementation內,function定義之外的變數宣告,相當於@private 的static宣告。
  • 在Object-C 2.0,使用「@property」修飾字宣告參數,系統會自動幫你產生setter / getter。在@implementation內也不需要使用「@synthesize」去產生setter / getter。要注意的是,使用「@property」修飾字宣告參數,在interface內部所產生的變數名會在其名稱前加上底線「_」。
    要注意的是,這做法是建立在該interfce是繼承自NSObject的前提之下。
    範例:
    @interface Ball : NSObject {
        NSString *name;
    }
    @property(nonatomic, retain) NSString *name;

    在@implementation裡面操作,會發現「self.name」(property預設的getter)跟「self->name」(c的pointer style getter)是不同的變數位置。若是把大括號裡面的宣告移除,使用箭號存取,將只能存取「self->_name」。

    另外,setter的用法如下:「self setName:(NSString*)」
  • 訊息運算式「[]」只能存取setter/getter。
    點跟箭號運算式可以直接存取物件內部參數。
  • property的getter還可以使用「[obj valueForKey:@"propertyName"]」的方式執行。
  • 方法(method,function)的宣告:
  • 前置符號為"+"代表為class method(static method)。
    宣告方式(在@interface內):
    + (return type) methodA: (parameter typeA) paramNameA parameterNameB: (parameter typeB) paramNameB
    使用方法為「[ClassName methodA: 參數1 parameterNameB: 參數2 (以此類推)]」。
    實作的宣告,與interface的宣告相同。
  • 前置符號為"-"代表為object method.
    宣告方式(在@interface內):
    - (return type) methodA: (parameter typeA) paramNameA parameterNameB: (parameter typeB) paramNameB

    使用方法為「[objName methodA: 參數1 parameterNameB: 參數2 (以此類推)]」。
    實作的宣告,與interface的宣告相同。
  • 繼承了NSObject的class的constructor: ClassName *c = [[ClassName alloc]init];。
    alloc/init為NSObject的預設function。都可以被override。
  • NSArray:只能置入繼承NSObject的物件。



layout相關

  • 新增xib檔案的方法:在project上面點滑鼠右鍵,New File->User Interface ->empty view
  • 新增一組.xib/.h/.m的方法:在new file時選擇「Cocoa Touch Class」。
  • Files Owner:這個xib所代表的class。 「不一定」是可視元件。一般的用法是讓viewcontroller可以載入頁面(Files Owner為viewcontroller),也可以設計一個ui元件套組,這時Files Owner就會是可視元件。
  • 修改完Custom Class請記得打完字之後要按Enter,這樣interface builder才會套用該class。 
  • 在interface builder(點選xib/storyboard檔案之後所顯示的介面)指定了view的custom class之後,interface builder右上方的箭頭符號裡面的「Outlets」的數量,是以該custom class的.h裡面定義的「@property (nonatomic, retain) IBOutlet UIclassname *classname」跟繼承的父interface的IBOutlet共同決定。
  • interface builder在設定Custom class的名稱的時候,要讓系統自動幫你預測class name的話,必須先建好該class的@interface定義。
  • 「拖拉」關聯:設定好Files Owner的custom class之後,Files Owner的outlets區就會出現custom class有宣告IBOutlet的變數。這時就可以把滑鼠停留在變數右邊的圈圈,圈圈會變成「+」號。然後按著滑鼠左鍵,開始移動滑鼠,就可以「拖拉」,到該變數想存取的ui元件上面放開滑鼠左鍵,就會發現IBOutlet被指定為該ui元件,這樣Files Owner(custom class)才能存取到該元件。
  • Constraint:讓xcode自動加所有的constraint,通常會導致預料之外的結果。建議ui元件會散佈滿整個畫面的layout(像是登入頁面),使用相對式constraint的方法:xib的最底層view指定一個固定的長寬,並視需要指定是不是要擴展到填滿superview(方式:加上指定上下左右四周的距離為0的constraint),然後view裡面的各ui元件的位置,使用距離superview的中心點的距離來指定。
    畫面下方是可捲動列表的layout,就可以用一般由上而下的layout。

補充:指定相對座標的constraint的加法:先點選要操作的ui元件,增加一個「相對距離」的constraint。 (此例為指定「相對於ui元件下緣」。)
在圖中的「Add new constraints」的正方框的上面的紅色虛線「工」符號的地方用滑鼠點選,「工」符號就會成為實線。然後再點選下方的「Add 1 constraint」。
這時會發現,要是旁邊有兩個以上的ui元件,xcode會以靠最近的元件做為標記對象。
標記對象不是想要的元件的話,就要點選想修改的constraint。
(要更換為另一種constraint的話,就得刪掉該constraint再加另一種,無法直接修改constraint的種類)

如下圖,「First Item」,意為「操作元件」。點選之後出現的下拉選單,上層為「參考點」,可以設定為「元件的上緣(Top)」,「元件的中線(CenterY)」等等。下方為操作元件的名稱。
「Second Item」,意為「目標元件」。點選之後出現的下拉選單,上層一樣為「參考點」,下方可以選擇對齊的目標元件名稱,也可以選擇對齊superview。
若是有元件名稱重複,建議修改元件名稱。點選元件的名字就可以修改。

在切換完對齊的對象之後,xcode會重新計算以目前元件位置,距離參數會是多少。所以要視需要調整距離參數,調整到想要的位置。

  • IBAction:
    定義ui元件action的連結者使用。xib 的File's owner(custom class)定義IBAction之後,把該IBAction跟xib的widget的event「拖拉連線」。
    拖拉方式有兩種:跟IBOutlet一樣的從File's owner的「Received Actions」(Custom class有定義IBAction之後就會出現)拖拉到ui元件上放開,就可以選擇該元件的action列表。
    也可以反向從ui元件的Sent Events拖拉到「File's owner」上面。這時會出現的列表只有File's owner有定義的IBAction。

    因為IBAction是統一介面,跟android設計不同的是,得在進去function之後才能判斷是哪個元件發送,還有event的內容。

  • app至少要有一個UIWindow。
  • app至少要有一個UIViewController。UIViewController下面一定要掛一個view。(也就是xib的File's owner為UIViewController的時候,outlets裡面的「view」要連到xib裡面定義的最底層view。)
  • navigation contoller,初始拖拉進入lauout區的時候會自動加上一個tableviewcontroller。若是不使用table的話,需要手動刪除,再拖拉一個其他的vc進去。(記得檢查元件連線)


系統相關:
  • app進入點:檔案通常為main.m。function定義必須為「int main(int, char*)」。
    xcode在新建project之後,main.m會是這樣的內容:
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    其中的AppDelegate為xcode自動建好的檔案,也需要使用者去做修改,就是繼承UIApplicationDelegate的UI進入點。若是不在Info.plist裡面指定「Main storyboard file base name」,可以在AppDelegate的「didFinishLaunchingWithOptions」的實作裡面,自行對delegate的windows property(也就是self.window)做window的初始化流程。

    ios的app,一定要有一個window。在做過self.window = [[UIwindow alloc]init];之後,下一步就是掛上該window的rootViewController(self.window.rootViewController),之後就是持續的對ViewController做處理,進入一般的ios app開發流程。
  • 其實不以程式碼做window/delegate的初始化也是可以的。有在Info.plist的「Main storyboard file base name」指定storyboard的話,因為storyboard會自動初始化window,所以在storyboard的layout裡面掛上自定義的viewcontroller之後就可以進入一般的開發流程。

    若是在Info.plist的「Main storyboard file base name」指定xib檔,雖然系統不會自動初始化self.window,卻可以在xib檔內使用拖拉window object到layout上面的方式初始化window,還可以用拖拉一個Object到layout上面,指定它為「AppDeleagate」,讓main.m的初始程式碼不須指定AppDelegate。
    例:先把main.m的初始動作的delegate拿掉:
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, nil);
        }
    }

    點選要成為主頁載入使用的xib檔,在xib檔顯示的狀態下,先把File's Owner的Custom class命名為「UIApplication」(請記得打完字之後要按Enter,這樣interface builder才會套用該class),然後在視窗的右下角列表裡面拖拉一個「Object」到layout顯示區,Custom class命名為「AppDelegate」,然後再拖拉一個「Window」到layout顯示區,然後把files'owner的outlet的delegate跟「AppDelegate」連線,再拖拉一個viewcontroller,Custom class命名為自定義的view controller,再跟「Window」的outlet裡面的「rootViewController」連線。在自定義的viewcontrolle的viewDidLoad下個斷點,執行debug app就會發現自定義的viewcontroller會被執行到,就可以進入一般的開發流程。
  • 通常主頁載入使用的xib不會放任何UI元件。就算是小到只有單頁的app,也會把主頁載入使用的xib當成空頁,把主頁xib的viewcontroller的Custom Class指定為自定義的view controller,把UI元件的layout放在xib+viewcontroller的組合內。

    需要換頁的app,主頁xib的viewcontroller 自然是要套上navigation controller,
    或是套上tab controller 當做tab式開發的主描述頁。一樣把UI元件的layout放在xib+viewcontroller的組合內。 
  • 新增一組xib/vc的方法:在new file時跳出的視窗選擇「Cocoa Touch Class」。
  • @interface宣告,除了繼承的宣告之外,其他的介面連接「<>」的宣告,也可以到.m裡面才指定。例:

    在.h裡面宣告:
    @interface AppDelegate : UIResponder <UIApplicationDelegate>
    {...}
    @end

    在.m裡面宣告:
    @interface AppDelegate()<ViewControllerADelegate>
    @end

    這樣的AppDelegate其實繼承了兩個interface:UIApplicationDelegate跟ViewControllerADelegate。





編譯/linking相關問題:
  • 「ld: embedded dylibs/frameworks are only supported on iOS 8.0 and later」:project properties->General -> Deployment Info : change Deployment target to over IOS8
  • implicit LOGE definition : project properties-> Build Settings -> Apple LLVM X.X - Language -> C Language Dialect : change to "C99"
  • ib documents for earlier than ios 7 -> 修改所有的xib檔的「build for xxx」,建議設定為「Build target」。
  • dyld: Library not loaded:Reason: image not found -> add these frameworks into   「project properties-> General -> Embedded binaries」
  • This app has crashed because it attempted to access privacy-sensitive data without a usage description.  -> 有用到contacts等等個人資料。 看是要不用個人資料,還是加上app的個人資料使用申請:「NSContactsUsageDescription」
  • Duplicate symbol -> 把該log行所在的整個linker的log使用滑鼠點擊「more」打開,觀察哪些symbol報告重複。也有可能是因為重複放了同一個.m檔案。到「Build Phase」區的「Compile Sources」檢查看看。
  •  


執行階段:
  • 若是使用iphone/ipad等等硬體裝置debug,必須到apple developer console進行裝置認證的動作。先啟動macbook的keychain access(在launchpad的「其他」資料夾) -> 點選螢幕上方的「keychain access」 -> 「憑證輔助程式」->「從憑證授權要求憑證」。user的email address填上itunes帳號的email位置,ca電子郵件不用填,下面的選項選擇「儲存到磁碟」,可勾選的選項不要勾,點選下一步,建立signing request「CertificateSigningRequest. certSigningRequest」檔案。
    然後到apple的開發者後台 https://developer.apple.com/ 點選左列表的「cerfificates」的任一項,依照release的需求選擇「iOS App Developement」或是「App Store and Ad Hoc」,上傳前一步做好的CSR,developer console就會建立cerfificates。然後一定要下載到macbook裡面,用滑鼠點擊兩下匯入keychain。 注意:匯入keychain要admin權限才能做此事。 
  • 另外,以上的動作若是使用自動管理證書的話,做過第一次之後,要是證書過期也會自動更新
  • 編譯的時候出現「codesign wants to use "login" chain」的話,要輸入登入用的密碼。有時候會出現非常多個一樣的框框,只能乖乖的一次次輸入密碼...
  • 以上的動作,要是按了「否」,會發現之後編譯都不會出現框框,然後就一直失敗在codesign的動作。解決方案是啟動kaychain access (launchpad -> 其他 ->kaychain access),然後選擇畫面上方的app menubar的「File」->「Lock All Keychains」。然後再Build就會再度出現「codesign wants to use "login" chain」的密碼輸入框了。
  • apple developer console第二步:建立Identifiers(app的上架資訊)。 就算是只拿來硬體debug使用也還是要新增。「App ID Description」填上app的「Bundle Idetifier」。
  • apple developer console第三步:加入devices,此步驟需要取得手機的UDID。(把手機接在macbook,使用xcode新增simulator的時候就會顯示接在macbook的手機的UDID。)



  • apple developer console第四步:產生Provisioning Profile。 這一步就只有選項,就不截圖了。這步完成之後,xcode選擇自動管理簽名的話就會自動下載簽名的管理機制(Provisioning Profile),之後xcode會自動處理簽名的流程。
  • This app has crashed because it attempted to access privacy-sensitive data without a usage description.
      -> 有用到contacts等等個人資料。 看是要不用,還是在Info.plist裡面加上「NSContactsUsageDescription」。



release階段:
  • Code Sign error : Command /usr/bin/codesign failed with exit code 1 : 把keychain access裡面所有過期的apple developer下載過的證書全部移除。只能留一組最新的。
  • provisioning profile doesn't include signing certificate : keychain access的證書幫手裡面的「從認證中心要求簽名」的項目,產生「CertificateSigningRequest. certSigningRequest」並儲存之後,到apple developer產生Certificates之後,必須手動下載然後安裝到keychainaccess。 沒做此步驟的話就會出現此錯誤。
  • Found an unexpected Mach-O header code: 0x72613c21
      ... Fabric and Crashlytics framework 不可以放在 embedded framework。把這兩個framework放在"linked framework and libraries". 
  • Failed to verify bitcode: release的app只支援iOS的話,不可以包含x86的架構的framerwork或是lib。project-> (select target ) -> Build settings :  拿掉x86 / x64 architecture. 
  • 在app thinning的時候出現ipa tool error : 在出現error之前直接按下一步跳過去。




Adhoc測試/上傳至itunes connect:
  • 編譯的第一步:螢幕上方的tool menu選擇「Product」->「Archive」。
  • 上一步的Archive做完之後,會出現一個視窗。選擇右邊的「Distribute App」(舊版xcode的選項是「Export」),出現新的視窗再選擇「Ad Hoc」就可以輸出Adhoc用的.ipa檔案。
  • itunes connect跟developer console的使用者資料是不同的。雖然帳號是同一個... 加入該服務跟email認證的動作也都要分開執行。
  • itunes icon改放到xcassets內了。原本指定在Info.plist裡面的CFBundleIcon/ CFBundleIcons等等的設定必須拿掉。
  • ERROR ITMS-90502: 拔掉iphone跟macbook的連線。 (此為ios版本相容的問題。) 也有可能是apple store上傳有問題(需等apple自己解決)
  • 所有的AppIcon必須是png格式。 
  • 編譯期間要是出現「mdwrite wants to use "metadata" chain」,先不管它。過30秒之後應該會跳出「codesign wants to use "access" chain」一樣輸入osx的登入密碼。等到編譯完成之後再關閉「mdwrite wants to use "metadata" chain」的框框。
  • ITMS-90032: "Invalid Image Path - No image found at the path referenced under key xxxxx :把所需要的App Icon補完。目前所知道的必須Icon:Iphone App 2x 60pt / App Store IOS 1024pt。有打勾ipad的話就需要ipad格式的。app不支援ipad的話要取消ipad的打勾,才不會一直被itunes抱怨。
  • 上傳之後會需要一些時間,才會在itunes connect上面看到。然後需要填「出口資訊」,填完之後apple才會發出通知信給「測試人員」。


debug:

  • EXC_BAD_ACCESS:意指存取到已經被釋放的物件。應該只有在非ARC模式會遇到。現在都是ARC模式開發應該很難遇到。(就算想要回到老路,別人寫的程式碼也早就ARC化了...)
  • app有用到ssl加密,出現seccertificatecreatewithdata returns null:
    必須把.pem格式(檔案內容看起來是文字編碼)轉成der格式(憑證內容為二進位檔格式)。
    指令:在macbook的文字指令模式執行「openssl x509 -outform der -in <憑證檔名>.pem -out <憑證檔名>.der 」
  • 如何debug release mode app:使用停止方塊右邊的target select,點選之後裡面有個「Edit scheme」來切換build scheme為release。

  • CoreData 'This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation.' -> 原因可能是同時有兩個thread在同時access。coredata不是thread safe.

Core Data相關:
  • Entity:把它想成是sql的table。
  • 建議的做法:使用xcode new一個data model, 設定完property,
    游標focus住datamodel file,Edit->Create NSManagedObject subclass->產生四個檔案:
    [Entity name]+[CoreDataProperties].h
    [Entity name]+[CoreDataProperties].m
    [Entity name]+[CoreDataClass].h
    [Entity name]+[CoreDataClass].m

    以程式碼的角度來看,Entity就是「CoreData Class」。
  • 要利用自動產生的CoreData Class,import的起點:
    #import "[Entity name]+CoreDataClass.h"
  • 自行alloc/init CoreData Class,在使用該class的setter/getter的時候會出錯。必須使用以下方法初始化CoreData Object:
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:<CoreData Class name> inManagedObjectContext:managedObjectContext];  

    <CoreData Class name>* coredataClassObject = (<CoreData Class name>*)[[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:nil]; 
  • 「Relationship」:在目前的Entity加上一個欄位,該欄位的內容是另一個Entity。
    「Inverse」:要是另一個Entity也有關聯到目前的Entity(也就是兩個Entity互相關聯),就可以選擇另一個Entity的關聯用欄位。

    有設定Relationship的Entity,會自動產生setter「add<Relationship欄位名稱>Object」的function。

    假設現在有EntityA跟EntityB相互關聯,EntityA的EntityB關聯欄位名稱為「EntityBs」(此欄位為陣列,可以存入多個EntityB)。EntityB的EntityA關聯欄位名稱為「EntityAs」。

    現在有CoredataObject objEntityA跟objEntityB,執行[objEntityA addEntityBObject: objEntityB] ,在objEntityA被insert之後,objEntityB會自動被加入EntityB,Relationship也會自動被建立。(查詢到objEntityA,會發現objEntityA.EntityBs欄位會掛上objEntityB,objEntityB的EntiyAs欄位會掛上objEntityA)。

    關聯的截斷跟重建:使用查詢的動作取回objEntityA,執行[objEntityA removeEntityBObject],然後執行[managedObjectContext save:&error],objEntityA之後再查詢就不會帶回objEntityB。但是objEntityB依然存在於EntityB(使用查詢EntityB的動作依然可以查到)。一樣的,可以再把objEntityB加回去objEntityA的關聯欄位。
  • 假如所定義的core data entity有跟其他的entity做關聯(Relationship),在執行「Create NSManagedObject subclass」輸出定義檔之後,會發現無法compile。這是因為輸出的定義檔不會自動#import關聯的entity的header檔案(apple quality...)。
    需要自行到「[Entity name]+CoreDataProperties.h」加入「#import "[Relationship Entity name]+CoreDataProperties.h"」
  • core data debug:建議以simulator來debug,因為可以直接觀察db的資料。
    一般的儲存path會使用以下設定:
    NSURL *appDocDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];

    NSURL *dbPath = [appDocDirectory URLByAppendingPathComponent:@"db.sqlite"]; 

    所儲存的sql檔案位置「file:///Users/<macbook username>/Library/Developer/CoreSimulator/Devices
    /<device id>/data/Containers/Data/Application
    /<app id>/Documents/"」
    把breakpoint停留在dbPath後面就可以取得。然後找一套osx可用的sqlite browser直接打開db.sqlite檔案即可。



2017年12月4日月曜日

[Web開發]Node.js 迷航記(express+angular+bootstrap+webpack+webstorm整合開發)

之前寫過的php+mysql的網站,在經過近十年的荒廢之後,對於web開發的認知已成了浦島太郎狀態。加上HTML5+Node.js成了web前後端的熱門顯學,於是想把整個網頁打掉重練。
翻了一陣子相關資料,看到滿滿的套件,還是有一種看不太懂的感覺。

經過一番整理,得到了以下的概念:
web page的運行架構為client/server。
所以定義client(下載到browser顯示的頁面)為「前端」,server為「server端」。

  • Node.js :在「server端」讀取.js檔案並執行,與各個OS介接的執行程式。擁有檔案I/O跟網路I/O(通常拿來做為http server)的功能,可根據不同的uri request執行相對應的回應。
  • npm : node.js package manager. 安裝並管理node.js可利用的各個模組。為node.js初始支援模組。下面提到的所有「server端」使用的模組都需要以npm安裝。
  • express:「server端」以javascript描述server response的模組。還提供了http server設定的template,大幅降低設定的困難度。
    以npm指令:
    npm install -gd express-generator
  • bower:安裝與管理「前端」使用的javascript模組。像是angular / bootstrap... 並把複雜的模組依存架構做成設定檔,讓其他的網頁資料前置處理模組使用。
    但是..........在這個變動快速的世界....bower已經被npm建議改用Yarn了。
  • Yarn:也是一種安裝包的管理軟體。跟npm的功能類似。也是以node.js為執行平台。
  • Karma:執行測試。一些需要做各種條件測試的功能,有測試軟體幫忙跑case會輕鬆很多。
  • AngularJS:「前端」的application framework。有了它,可以輕鬆的架構模組化,資料-顯示-邏輯分離的網頁app。
  • bootstrap:在「前端」幫忙排版網頁的引擎。主要是拿來處理螢幕大小的對應顯示,跟美化各個網頁上的元件。
  • webpack:打包前端的javascript/css/資源(如圖片等等)並加上其他改善處理(例如縮小程式碼/混亂程式碼等等)的模組。在大量應用js模組的時代,在html裡面一個個的用script tag載入是相當麻煩而且沒有效率的事情。而且會導致大量的http request,對server也是負擔。尤其要面對各模組之間的依存順序的時候更會讓你頭痛。因此導入打包模組是必須的。當然,簡單的網頁就不一定要這麼麻煩...


從.頭.開.始。

簡單的了解海量的元件之後,當然是直接要從.頭.開.始。
本文使用OSX為作業環境。
安裝npm: 從官網抓安裝包最安全。 https://nodejs.org/
建立project:在這裡使用express-generator的模板建立流程。
  • 先安裝express-generator:
    sudo npm install -g express-generator
    註:參數「-g」代表此模組是安裝在系統目錄,讓所有目錄都可以使用。也因為是裝在系統目錄,在OSX環境下需要以管理者權限執行,需加上「sudo」。windows環境下就不需要加sudo。
  • 安裝express-generator之後,切換到想要建立project的目錄,執行「express (project名) -ejs」,express模組就會幫忙建立一個新的目錄,裡面包含基本的http server處理的所有檔案。本文之後都以「<project root>」來描述本文所使用的project的根目錄。
    註:「ejs」是javascript的代碼注入模組,後述。
然後來看看建立了哪些東西:

圖上左側的樹狀檔案列表,就是產生出來的檔案們。
東西很多,來一個個的看。

認識package.json

package.json:為npm的進入點。(npm執行沒有帶「-g」參數,也就是對目前的所在目錄處理的指令,都會參照執行當前目錄下的package.json檔案。)所以先看這檔案的內容:

{
  "name": "untitled",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.18.2",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "ejs": "~2.5.7",
    "express": "~4.15.5",
    "less-middleware": "~2.2.1",
    "morgan": "~1.9.0",
    "serve-favicon": "~2.4.5"
  }
}


在node.js的世界,習慣使用npm start來啟動server端的運行。「start」就是對應到上面的程式碼的"scripts"裡面的"start"。所以下一步就是執行「node ./bin/www」。
當然可以自行加上許多指令。像是test啦,packaging等等...
若是想不經由npm來執行的話,在<project root>執行「node ./bin/www」也可以達到一樣的結果。

"dependencies" 是一大重點。裡面表列的是這個project會用到的所有node.js模組。本文寫到現在,都還沒安裝過模組。上面的模板產生出來的部分,在<project root>執行「npm install」就會全部幫你裝在這個project裡面的「node_modules」目錄下,不會更改到安裝在系統環境下的模組。至於這些模組是不是「必要」,得等到一個個去了解之後才能確定,在此不研究。

要如何增加模組?直接編輯package.json之後再執行「npm install」當然可以。或是執行「npm install <模組名稱> --save 」這樣的加上「--save」參數,這樣再開啟package.json看檔案內容,就會發現安裝過的模組的名字會出現。

至於不使用bower,直接讓前後端所有的模組全部擺在一起會不會有缺點,目前不得而知。先暫時讓它混吧。migrate總是痛苦的。像是模組命名不同,前後端用的版本不同等等瑣碎的問題可能會大量發生。

有了進入點之後,接下來就是依序追蹤。繼續看start參數所指定的執行指令檔:./bin/www

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('untitled:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

(....  後面的程式碼略過)

可以看出這檔案讀取了「../app」,也就是「./bin/」的前一個目錄的「app.js」。
(沒指定副檔名是因為node有一個查找的機制。有興趣的話請翻閱其他網站的說明)
跟「http」模組(此為node.js內建)。

然後做了啟動http server的動作,跟listen port。這些都是基本的http server啟動流程。
有了基本的http server的流程,在這邊先來試試產生的模板是不是能正常運作吧。
在<project root>執行「npm start」。要是執行「npm start」發生錯誤的話,應該是沒有先執行「npm install」安裝相關模組。
接著用browser 連線「localhost:3000」...

看來是可以運作。safe~~~
這時可以按下ctrl+c停止server運作,繼續迷航...


認識app.js

上面的兩個檔案所提供的資訊,總覺得好像沒做到什麼設定。需要更多的資料來了解。
http.createServer(app) 這個app參數相當可疑。
總之來看看<project root>的app.js:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var lessMiddleware = require('less-middleware');

var index = require('./routes/index');
var users = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(lessMiddleware(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

喔喔。前面留下來的疑問全部都得到了解答。
標記著顏色的require,剛好呼應了dependencies裡面的定義。
大量的express設定就請參考文件。
重點:
  • 「app.set('view engine', 'ejs');」表示express在render網頁的時候,會加上ejs的處理流程。
  • 「module.exports = app;」定義若是此檔案被require的時候,該回傳的是哪個東東。會回傳的是「app」這個參數所代表的物件,前面有個「var app = express();」的動作,所以這個app也就是代表執行「express()」之後所產生的物件。
  • 「app.use('/', index);」表示若得到的url request的路徑是'/',就會執行index這個動作。

而index是由「require('./routes/index');」所定義。所以我們再看./routes/這目錄下面有沒有「index」相關的檔案。有找到index.js。看看內容:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

module.exports = router;

看起來似乎是在所謂的router裡面加上url的比對跟安裝callback執行比對之後的動作:回傳一個render過的網頁內容。取得views/index.ejs之後,ejs模組依照傳入的參數跟檔案本身的內容做處理,回傳給browser。

有router.get(),當然也有router.post()。就看使用的需求了。
req是browser傳來的request的內容。應該會經由「bodyparser」處理成object跟一個個的參數。
res對server來說就是對browser輸出的介面。除了回傳網頁之外,發送檔案/url redirect都是做得到的。這部分就請參考文件。


導入IDE:webstorm

到這裡,基本的web server的流程介紹完畢。終於可以開始著手撰寫了。
總覺得一直參考來參考去的很麻煩。很需要好用的整合開發環境。
ios有xcode,android有android studio,windows有visual studio, 那javascript呢?

找了一陣子資料,看來目前最強的是webstorm。有30天試用期。試用之後發現真的是沒有它不行... 因為它整合了debug最重要的功能:中斷點。

其實一開始的檔案列表的圖就是webstorm的介面。
來看看webstorm有多方便吧。
從官網下載,安裝完webstorm之後,執行。

選擇「Create new project」,選擇「Empty Project」,避免webstorm可能會自動產生一些東西來改變之前的成果。填入之前建立的project的根目錄,點選「create」:
之後webstorm會提醒此目錄不是空的,是否要以原有的資料來建立project,選擇「Yes」。

然後就進入了主畫面。


設定webstorm的debug環境

接著就來試試要如何debug吧。要是使用的webstorm,右上角的蟲子icon此時是綠色,可以跳過以下設定debug環境的步驟,直接下斷點試試。
若是右上角的蟲子icon此時是灰色,點選右上角的三角形指向下的方塊,選擇「Edit configurations」。

進入run/debug設定畫面。點選左上角的「+」,在浮現框裡面點選「Node.js」。

然後在圖中的藍色框處填入前面有提到的npm start command:「./bin/www」
再選擇右下角的「ok」。

以上的步驟就是讓webstorm在debug的時候去啟動node.js。這樣我們就可以依照之前的連線的經驗去對server連線。應該會發現右上角的綠色三角形跟蟲的符號成為可選狀態。

為什麼debug config不用npm start不就簡單多了?
經過測試,這樣做的話似乎在npm啟動server之後過個幾秒就會停止server,不知原因...

接著如下圖,在index.js的第6行的數字右側點擊,該行就會出現一個紅點,這就是debug的中斷點。再點選視窗右上角的蟲子符號,使用browser連線「localhost:3000」...

有如下圖般的變成藍色的話,代表中斷點正確運作。這時可以按視窗左下角的綠色三角形讓程式繼續跑。

webstorm簡單介紹到此。已經可以滿足基本的開發需求。


前端打包:webpack

接下來想知道的是:前端的開發/模組安裝/打包要怎麼解決?

這部分其實因為在bower還沒被放棄之前,前端開發的javascript模組的管理都是交給它處理。但是現在就得另找方法解決。就算是跟後端的模組混在一起放,那也是得解決之後打包的問題。

ok。既然決定引進打包功能,就要照打包軟體的建議做法來執行。
本文使用webpack做為打包工具。以下是webpack建議的路徑設定:

.
├── package.json
├── public
│   ├── index.html
│   └── js
│       └── bundle.js
├── src
│   └── js
│       ├── app.js
│       └── modules
│           ├── modulea.js
│           └── moduleb.js
└── webpack.config.js


webpack的開發習慣:<project root>/src目錄做為開發用的程式碼放置目錄,
而<project root>/public則是輸出結果用的。<project root>/public目錄,在express-generator幫忙做出來的檔案裡面已經有了,剛好不需因路徑的更動而改動改太多程式碼。
當然,一鍵做完所有處理之後還可以debug,是必須達成的目標。

總之先把webpack/jquery/angular/bootstrap裝好。
也裝裝webpack在本文裡面會用到的extract-text-webpack-plugin/ css-loader / style-loader / url-loader:
npm install webpack --save
npm install angular --save
npm install bootstrap --save
npm install extract-text-webpack-plugin --save
npm install css-loader --save
npm install style-loader --save
npm install url-loader --save

npm有個package.json為進入點,webpack當然也有個設定檔。就是「webpack.config.js」。
因為不像express有建立模板的工具可用,參考其他網站的設定,在<project root>建立檔案「webpack.config.js」:



// 有套件需要絕對路徑的設定,導入path模組。
var webpack = require('webpack');
var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

var plugins = [ new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    //'window.jQuery': 'jquery'
    })
    , new ExtractTextPlugin('stylesheets/[name].bundle.css') ];
// webpack.ProvidePlugin是為了讓webpack可以包入jquery而使用的。webpack內建支援,不需另行安裝模組。
//ExtractTextPlugin是為了讓webpack把css不內包在.js,另存一個檔案用的。聽起來有點畫蛇添足?因為把css獨立出來的好處是讓html可以同時讀入js跟css。一次只同時讀一個css跟一個js是目前比較推薦的設計。
//預設路徑是output的path。


module.exports = {
    // 進入點。別跟<project root>/app.js搞混。這個是給前端用的。可以有多組。
    entry: {
        app: './src/js/app.js',
    },
    // 檔案輸出設定
    output: {
        // 輸出檔案名。
        filename: 'js/[name].bundle.js',
        // 輸出路徑。webpack v2以後需要指定絕對路徑。
        path: path.join(__dirname, 'public')
    },
    plugins: plugins,
    devtool: 'source-map',  //輸出code map,讓debug tool可以斷點。
    resolve: {
        modules: [ path.join(__dirname, "node_modules") ],
        extensions: ['.js', '.css']
    }, 
    module: {
        loaders: [
            // loads css
            { test: /\.css$/, loader: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: 'css-loader'
                })
            },
            { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?mimetype=image/svg+xml' },
            { test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?mimetype=application/font-woff' },
            { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?mimetype=application/font-woff' },
            { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?mimetype=application/font-woff' }
        ]
    }
};

註:
  • [name]為webpack的專用指令。會自動帶入entry的參數名。
  • plugins:指定需導入的plugin。已定義在前面...
  • resolve:指定「require」或是「import」指令該到哪裡找資料。因為我們沒有把前端跟server端所使用的模組拆開放,目前只要指定npm安裝過的模組的安裝目錄「node_modules」即可。 
  • module:指定讀入該模組之後該做怎樣的處理。test會把讀入的檔名以「正規表示法」的方式比對看看是否符合。符合的話就以此條件的指定loader處理。第一個條件是副檔名為.css,loader是ExtractTextPlugin。ExtractTextPlugin將會以套入的參數設定:先使用css-loader,若是loader報告無法處理的話,再使用style-loader處理。目前這是為了bootstrap必須被含入的「bootstrap/dist/css/bootstrap.css」的設定。有興趣把bootstrap.css從sass層次解譯之後還含入的話就自行多試試吧。
  • 因為bootstrap裡面有不少圖檔資源必須含入,所以下面含入了.svg/.woff/.eof/.ttf檔案的處理。使用「url-loader」做處理。


接著準備src/js/app.js,webpack會幫忙把相關會用到的.js全部包在一起。
包含之後測試angular的controller程式碼。

// style library
require("bootstrap/dist/css/bootstrap.css");
require("bootstrap/dist/css/bootstrap-theme.css");

// javascript library
var $ = require("jquery");
var angular = require("angular");
require("bootstrap/dist/js/bootstrap.js");  //bootstrap的功能

// angular module
require("./angular/controller");





為了方便測試webpack的處理,我們在package.json裡面的script多加一個build的指令去call web pack。到這裡,檔案結構大概是這樣:


然後就可以執行「npm run build」來執行webpack了。(非npm內建的關鍵字,都要加上「run」來告訴npm後面的指令是要到package.json的scripts裡面找。)
webpack執行結果:

看起來是沒問題。(實際上為了測出這樣的參數,已經錯了不下百次)
那就來準備基本的測試頁來試看看嘍。


前端開發

以下的頁面將會有一個輸入框跟顯示區來測試angular的雙向資料綁定,跟bootstrap的著色提示框。別忘了目前的開發環境,html檔是views目錄下的「index.ejs」。(請參考前面的檔案結構列表)

修改views/index.ejs 如下:
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='stylesheets/app.bundle.css' />
    <script type="text/javascript" src="js/app.bundle.js" />
    <!--要含入的資料,會是經過webpack輸出的結果。-->
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>

    <div class="alert alert-warning alert-dismissible show" role="alert">
      This is Bootstrap....
      <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
      </button>
    </div>

    <!-- angular data binding test -->
    <div ng-app="app"  ng-cloak>
      <div ng-controller="helloCtrl" ng-init="init()">
        <input type="text" ng-model="demo">
        <div>{{ demo }} </div>
      </div>
    </div>
    </body>
</html>


修改src/js/angular/module.js 如下:
module.exports.angApp = angular.module("app", []);


修改src/js/angular/controller.js 如下:
let mHello = require('./module');

mHello.angApp.controller("helloCtrl", ['$scope', "$log", function($scope, $log) {
    $scope.init = function() {
        $log.debug('Hello Angular');
        $scope.demo = 'Hello Angular';
    };
}]);


使用browser連線localhost:3000之後的顯示結果:

輸入框裡面的文字修改之後,下方的字會跟著變化,黃色提示框右邊的X點選之後,提示會消失,應該是會動了。



Webpack的殺手應用:動態編譯

最後一步:build兼啟動server。經過一陣子的收集資料,發現webpack可以動態編譯,不過大部分都是靠webpack-dev-server的cli來達成。本文從原本的express的基底,加上webpack-dev-server,靠webpack-dev-server的動態編譯來達到改code免重新啟動debug。

需安裝的套件:
webpack-dev-server / webpack-dev-middleware / webpack-hot-middleware
npm install webpack-dev-server --save
npm install webpack-dev-middleware --save
npm install webpack-hot-middleware --save

修改<project root>/app.js...
把程式碼的20行以後到註解「// catch 404 and forward to error handler」
的部分修改如下:
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(lessMiddleware(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);
   
if (process.env.NODE_ENV !== 'production') {  //只有開發模式才這樣用。
    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    const webpackHotMiddleware = require('webpack-hot-middleware');
    const config = require('./webpack.config.js');
    const compiler = webpack(config);

    app.use(webpackHotMiddleware(compiler));
    app.use(webpackDevMiddleware(compiler, {
        publicPath: "/",
    }));  //publicPath: 指定輸出的路徑是server url的根目錄開始。因為webpackDevMiddleware的所有資料是在記憶體中處理。
}
else  {
    //express-generator做出來的這個設定會導致動態編譯失效,讓它在非開發模式執行。
    app.use(express.static(path.join(__dirname, 'public')));
}

// catch 404 and forward to error handler

啟動webstorm的debug,從webstorm的console確認compile成功之後,使用browser連線「localhost:3000」,看看會不會有網頁出現。
成功的話,可以開始修改controller.js或是index.ejs或是自訂的css,然後reload browser,看看會不會更動,體驗免編譯開發。


以上。

2017年11月28日火曜日

[禁止酒駕]Whisky Luxe 2017


這個活動其實在第二屆有參加過。但是那次的參展陣容覺得有點少,之後被嚇到不敢參加。今年看了一下廣告,似乎免費喝的款式多了不少,於是就決定再參加一次。


很高興的是,今年的確沒讓我失望。完全是值回票價的一場品飲會。
雖然側邊的攤位跟首次參加的印象一樣,通道只容得下兩人行走真的是太擠... 只要有人在攤位前就難以移動。覺得這場地鐵定要換的。

有了上次的經驗,這次也學乖,提早兩小時去排隊,順利的擠進前30名。
不過因為開場衝去排輕井澤之後被帶到錯誤的隊列,再回去已經來不及,於是放棄。
反正現在已經是貴到買不起的酒

下面這幾張是摸不起的超高價。看看就好...




這次想排的第一順位是山崎的水楢桶。在輕井澤排失敗之後就直接去suntory攤位。
感想:非常強的花香。蜂蜜,太妃糖,木香,淡煙燻,帶著白醋味。的確是不錯喝。不過在日威價格不合理的時代,有體驗過就好。

圖中這隻山崎50y要價600K。



籌碼換來的kingsburry,覺得普通。硫,碘,橡膠,微嗆。


沒想到的是本屆個人MVP在喝第三隻的時候就出現了。就是圖中這款。
碘,煙燻/烘烤木桶,木香,堅果味。強烈的稻草煙燻味,有如置身在燒稻草的濃煙裡面。

接著有排到麥卡倫紫鑽。也是一隻被炒得恨天高的酒(行情約5K)。喝過只覺得還好。陳年蘭姆酒風,微澀...


愛爾蘭威, 覺得比較像穀物威士忌的味道。花香,糖香。

繞到台酒攤位,有風味桶原酒可以付費試飲。為了滿自己的好奇心,於是就買了杯梅子桶試試。喝完的感想:對風味桶死心了XD  不過也可能是被前面幾隻超級好酒給洗過,影響判斷... 之後還是會給它機會。


一系列藍姆酒。自然是每一隻都要給他喝過。
barbados 2012, 甘甜,草香。
panama 2010, 香氣比較沒那麼明顯。
guadeloupe 2013 香草。

下面這兩支有覺得比較好。尤其是圭亞那的非常棒。甘甜,不錯的香草味,香瓜味,還帶了淡淡梅子味。
fiji , 清香,微甘。

蠻有趣的一款威士忌。 有著清酒的味道。

Benrinnes 21y,草香,糖香,橡膠,奶油,微甘,澀。

CLYNELISH 20y,沒試過的酒廠。果香,橡膠味。

spey給了四款市面看得到的試飲。這時味覺已經差不多不行了。
spey 1999:53.8,果香,烏梅。

spey tenne aged ,花香,微甘,烏梅
spey byron,花香,清香,柑橘,入口後的刺激感較強。

中場休息30分鐘,喝水吃零食順便醒醒酒... 喝酒的場子不應該出現會影響味覺判斷的東西(身上不可帶香水),所以場地內沒有飲食攤。加上很容易餓,自己帶些零食是強烈建議選項。


接著轉到一開始被輕井澤擠爆的華緯國際,這次帶了非常多隻酒,一攤就可以讓你喝到掛。
這隻鳳梨GIN有成熟鳳梨味跟煙燻味,草味帶有一絲絲,提醒著它是GIN...
還有另一隻櫻桃GIN,就很明顯的有草味,比較喜歡這隻的平衡度。

Oc3這隻可以免費試飲,也試了一下。看到60%就忍不住
跟左邊的個人單場MVP比,也有不錯的煙燻味,不過尾韻的甘甜就是MVP較少的。

後面的因為不勝酒力就沒喝了。拍拍瓶子...












看到以上的圖就可以了解,這次的免費喝的陣容的確是夠多夠精彩。
場內下排左側的攤位都還沒注意到,就到了散場的時刻。以後都這陣容的話就會場場報到了。