之前主要說的是,我是如何產(chǎn)生這個圈子小程序的想法和如何上線的。有興趣的朋友可以回去參考前面兩篇文章。這次來給大家講講,在技術(shù)上是如何實現(xiàn)的。
分享作為獨(dú)立開發(fā)者經(jīng)歷和一些上線經(jīng)歷
技術(shù)棧
前端主要是基于Taro Typescript dva框架實現(xiàn)的,后端基本上是以Ruby on Rails為主。
這里說下為什么要做這樣的技術(shù)選型,關(guān)于技術(shù)選型,在《奔跑吧,程序員》一書中有很詳細(xì)的分析,后面我會我在我的讀書筆記系列把這本書做一次分享。
這里主要說下技術(shù)選型主要判斷,如果是快速成型的項目,應(yīng)該選擇更加輕量的語言和有大量社區(qū)組件支持的,另外一個就是自己熟悉的。
前端
為什么用Taro?主要是以往自身經(jīng)歷決定的,以為本身我做了13年技術(shù)研發(fā),雖然后面幾年自己動手的時間少了,但也算在一線工作。前幾年React Native剛興起的時候,對于有著js開發(fā)經(jīng)驗和安卓開發(fā)經(jīng)驗的我很快就上手。借著之前餓了么做hybrid和移動端動態(tài)模板渲染的經(jīng)驗,讓我迅速理解了React的設(shè)計理念和原理。
所以這次就順利的用了以React為主的Taro前端開發(fā)框架,雖然uni-app大名鼎鼎,但畢竟要重新了解vue。原先redux那一套回憶起來相對比較快。這里不去爭論對錯,能讓你舒舒服服的快速完成,那么就是對的。
說回Typescript,記得以前寫js碰到最大的問題是無法提示,對于我們這種全棧開發(fā)來說,切換太快了,所以會花很多時間查他下面有哪些方法等,為了節(jié)省代碼量,有段時間還瘋狂的寫coffee script。雖然項目里寫的還不是很規(guī)范,但確實解決了我很大一塊問題。
看到這里肯定有人想說,那是你沒用對。就好像Vim、Emacs一樣,很多人覺得太方便了,為此我還專門花了時間去學(xué)vim的快捷鍵。但是最后用起來還是習(xí)慣不了,切換模式dd刪除,我還是比較喜歡在VS code里用`command x` 來當(dāng)刪除用,十幾年的習(xí)慣,不是說改就改的。
還是那句,沒有對錯,只有你自己習(xí)慣就好。
后端
其實這幾年寫的比較多少的還是Java,但這里就不多說了,現(xiàn)在的項目也沒做過太大的壓測,但如果用戶量大到扛不住,那么再說甜蜜的煩惱問題。重新花時間調(diào)研了下Ruby on Rails,發(fā)現(xiàn)新增了很多特性,如Job、ActiveStroge、Webpack、turbolinks等等,對于全棧開發(fā)來說支持越來越好了。
最重要的是Ruby on Rails對測試的支持非常好,我個人習(xí)慣就是如果代碼沒有測試覆蓋,我很容易改出問題,因為時間久了或者對這塊業(yè)務(wù)不熟悉了,很容易有問題出現(xiàn)。
小程序
項目結(jié)構(gòu)
先丟張基本的結(jié)構(gòu)圖讓大家了解下
- pages 是小程序所有主頁面的目錄
- models 是數(shù)據(jù)層,負(fù)責(zé)獲取遠(yuǎn)程數(shù)據(jù)和處理數(shù)據(jù)的
- components是組件層,主要是一些組件和放復(fù)用頁面的
- services是網(wǎng)絡(luò)層,主要是各種api調(diào)用的
- utils是各種幫助類
大概的流程是這樣的,這里不做過多講解,有興趣的朋友自己去看dva框架和redux。
下面主要這次開發(fā)圈子小程序碰到的一些問題和我解決的思路和方式,如果你有更好的,歡迎找我交流。
登錄
說真的,登錄是非常煩人的一件事。之前一直有一定的概率出現(xiàn)**Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt**,后來在官方的文檔中找到:
在bindgetphonenumber 等返回加密信息的回調(diào)中調(diào)用 wx.login 登錄,可能會刷新登錄態(tài)。此時服務(wù)器使用 code 換取的 sessionKey 不是加密時使用的 sessionKey,導(dǎo)致解密失敗。建議開發(fā)者提前進(jìn)行 login;或者在回調(diào)中先使用 checkSession 進(jìn)行登錄態(tài)檢查,避免 login 刷新登錄態(tài)。
所以后來我調(diào)整成
- 在進(jìn)入頁面后,會進(jìn)行一次login,然后把獲取的code保存起來
- 當(dāng)用戶點(diǎn)擊登錄按鈕,會再一次進(jìn)行checkSession檢查,避免登錄態(tài)失效
- 然后把獲取到的`code`, `encryptedData`, `iv`全部提交到服務(wù)器進(jìn)行解密和校驗
Textarea穿透問題
圈子小程序里有個功能,用戶可以評論某個人的帖子,然后會導(dǎo)致一個問題,就是底層的輸入框會在彈層上層。具體可以參考下面這張圖,我當(dāng)時沒截圖。無論你怎么設(shè)置z-index都沒用。
導(dǎo)致這個問題的原因是,Textarea是原生組件,層級會高于網(wǎng)頁組件,所以我是這樣解決的。
- 當(dāng)Textarea失去焦點(diǎn)后,把內(nèi)容存儲到內(nèi)存里
- 隱藏Textarea,并顯示一個View
- 填充之前緩存的內(nèi)容
不過這樣做又會導(dǎo)致另外一個問題,有可能因為焦點(diǎn)問題,緩存里沒內(nèi)容,用戶直接點(diǎn)了發(fā)送按鈕,這個時候就需要判斷提交的內(nèi)容里有沒內(nèi)容,有就直接提交,沒有就用緩存里的。都沒有就做非空提示。
還有就是有同學(xué)提到,輸入框會被鍵盤蓋住的問題。類似下圖
這里解決方式也相對比較好解決,參考官方文檔這個屬性
dva-model-extend和model層
往往在實現(xiàn)邏輯的時間發(fā)現(xiàn)很蛋疼的問題,就是幾個頁面邏輯差不多,但有有點(diǎn)不太一樣,然后這個頁面又耦合了一些其他模塊的邏輯。最常見的例如下面這個。
上圖里面幾塊業(yè)務(wù)就涉及了圈子、帖子、用戶、贊、評論邏輯,還有自己頁面的一些邏輯,那么我們應(yīng)該怎么去劃分呢?大家可以參考下圖
業(yè)務(wù)基礎(chǔ)類
主要是負(fù)責(zé)通用邏輯實現(xiàn)的,比如獲取用戶相關(guān)的baseUser,帖子相關(guān)的basePost等等,但他們沒有自己的namespace,不能直接調(diào)用,只能作為基類存在。
業(yè)務(wù)類
就是負(fù)責(zé)通用業(yè)務(wù)真正被調(diào)用的,比如用戶類userModel、帖子類postModel等等。這樣做有什么好處,那就是任何頁面都可以去調(diào)用。
比如我想在帖子列表的頁面里獲取每個人的用戶信息,那么我可以直接dispatch一個user的type。他的邏輯相對標(biāo)準(zhǔn)。
界面類
就是為每個頁面提供特性服務(wù)的類,比如上面圖里,我有定制對帖子的返回內(nèi)容要單獨(dú)存一個state,那么我就可以繼承基礎(chǔ)業(yè)務(wù)類,然后更改他的reducers
的action實現(xiàn)。并且每個界面類都會關(guān)聯(lián)一個page。
數(shù)據(jù)類
為什么數(shù)據(jù)類單獨(dú)的?這有什么用。在前端,我們碰到的很大的一個問題就是,比如A頁面用了用戶信息,B頁面也用了用戶信息。如果按照以往的做法,每個頁面單獨(dú)維護(hù)一個用戶信息,然后通過eventbus來更新到每個頁面,這樣做的問題是大量的冗余數(shù)據(jù)放在內(nèi)存里,然后event滿天飛,最后也不知道這個頁面的數(shù)據(jù)被哪個地方觸發(fā)改變了。
所以需要以前數(shù)據(jù)層來維護(hù),有點(diǎn)像前端的內(nèi)存關(guān)系型數(shù)據(jù)庫,界面拿到一堆ID,然后在要顯示的時候才會去數(shù)據(jù)層查詢具體的數(shù)據(jù),然后渲染界面。
數(shù)據(jù)渲染
上面提到,以往我們都是直接渲染數(shù)據(jù)的,然后通過eventbus改。這樣還會碰到一個很麻煩的問題。
還是圈子小程序的例子,如果我要刪除一張?zhí)永锏脑u論,我需要怎么做?
- 找到帖子該帖子的數(shù)據(jù)
- 找到帖子里的評論數(shù)據(jù)
- 因為評論可能是子評論,還需要在先找到上級評論后再找到當(dāng)前評論。
- 防止其他頁面數(shù)據(jù)未更新,發(fā)送事件通知其他頁面也需要重復(fù)一次以上操作
最開始一度讓我很奔潰,根本沒有辦法繼續(xù)持續(xù)下去,而且還很容易出問題,測試的工作量也倍增。
然后我找到了前端神器normalizr,這個庫可以幫我們完成上面數(shù)據(jù)層說的工作。具體流程可以參考下圖。
為節(jié)約篇幅,這里不做過多解釋。因為所有頁面都是引用性質(zhì),所以一旦數(shù)據(jù)發(fā)生變化,所有頁面都會跟著變化。并且處理數(shù)據(jù)只需要處理一層的關(guān)聯(lián),不需要處理多層的數(shù)據(jù)結(jié)構(gòu),因為它幫你把數(shù)據(jù)進(jìn)行扁平化處理了。
總結(jié)
上面分享了項目的基本結(jié)構(gòu)、邏輯分層、數(shù)據(jù)處理的一些思路,相信應(yīng)該對大家開發(fā)小程序有一定幫助。
后端
說完前端的基本架構(gòu),現(xiàn)在來說說后端。對于初期的項目來說,前端只要處理好數(shù)據(jù)和邏輯的架構(gòu),其他都是一些界面的問題和css相關(guān)體力活和不斷的多設(shè)備兼容調(diào)優(yōu)。
后端的事情比較了多了,比如監(jiān)控、數(shù)據(jù)處理、微服務(wù)、容災(zāi)等等,這些年或多或少接觸了一些,但作為新項目,這些東西反而不是最重要的。
實現(xiàn)一個新項目,最重要是如何更快的迭代和提供新接口。crud仔的名聲不是隨便說說的。
從早年的WebService到現(xiàn)在的微服務(wù),概念一直在更新,但本質(zhì)上沒有太大的改變。都是希望降低風(fēng)險,早年我在小秘書的時候就開始做SOAP和WSDL,但對于創(chuàng)新業(yè)務(wù)來說,技術(shù)不應(yīng)該作為阻礙效率的存在。
當(dāng)我聽到為了一張表而專門創(chuàng)建一個服務(wù)的時候,我反而覺得是為了微服務(wù)而搞微服務(wù)。當(dāng)我想改一個問題的時候,我需要從網(wǎng)關(guān)一路改到最后層的服務(wù),明明幾分鐘能解決的事情,在調(diào)試上硬花了一整天。
每個人觀點(diǎn)不一致,技術(shù)沒有對錯。面對不同的背景,每個人選擇不一樣。我見過很多技術(shù)架構(gòu)很好,但迭代慢死掉的公司。也見過很多內(nèi)耗很嚴(yán)重,但依然發(fā)展很好的公司。
前面稍微說的有點(diǎn)偏題了,回到主題。初創(chuàng)項目主要處理好幾件事情。當(dāng)然你有其他觀點(diǎn),歡迎討論。里面有些地方參考了ruby-china的源碼,非常感謝。
- 接口及響應(yīng)模板
- 錯誤捕獲及告警
- 權(quán)限校驗
- 部署和測試
接口及響應(yīng)模板
怎么理解接口及響應(yīng)模板呢?說白了就是你的接口能返回數(shù)據(jù)。
這次我沒有采用Grape的Gem,而是直接使用了Rails Api和Jbuilder的渲染模板。
首先我創(chuàng)建了一個父級渲染模板
# app/views/layouts/api/v1/application.json.jbuilder
json.code 200
json.message @message.blank? ? ‘’ : @message
json.data JSON.parse(yield)
也就是無論如何都會返回code,message,data這三個key,data可能為Array或者Object
然后在application_controller.rb里指定父layout
layout ‘a(chǎn)pi/v1/application'
然后在application的目錄里為每一個實體做一個通用的模板,如_user.json.jbuilder,通過參數(shù)判斷是簡易還是復(fù)雜對象。
比如你在列表里user可能主要3個值,nick_name,id,avatar,當(dāng)你具體查看某個人的資料時,你可能需要知道他的其他信息,例如age,gender,cellphone等等。
然后相應(yīng)的接口渲染可以參考下面的
json.partial! ‘user’, user: @user, detail: true
基本上你接口的響應(yīng)就到這里就結(jié)束了。補(bǔ)充一點(diǎn),如果你是使用Rails Api的話,使用Jbuilder需要加入以下引用
class ApplicationController < ActionController::API
include ActionView::Layouts # if you need layout for .jbuilder
include ActionController::ImplicitRender # if you need render .jbuilder
錯誤捕獲及告警
這里分為幾塊
錯誤碼
你可以選擇新建一個專門的類來維護(hù)錯誤碼
module Api
module V1
module Code
module HttpBase
HTTP_FORBIDDEN = 403
HTTP_INTERNAL_SERVER_ERROR = 500
HTTP_UNAUTHORIZED = 401
HTTP_BAD_REQUEST = 400
HTTP_UNPROCESSABLE_ENTITY = 422
HTTP_NOT_FOUND = 404
HTTP_BAD_GATEWAY = 502
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_NO_CONTENT = 204
HTTP_METHOD_NOT_FOUND = 405
end
module HttpExtend
INVALID = 10000
end
end
end
end
錯誤消息
這里就不提了,我直接新建了一分api.zh-CN.yml的I18n來維護(hù)錯誤消息
錯誤類
這里稍微講一下,主要是分成3種三類別
- 系統(tǒng)錯誤類別,比如系統(tǒng)宕了,數(shù)據(jù)庫查詢報錯,參數(shù)判斷為空這類系統(tǒng)異常類,由系統(tǒng)拋出,捕獲處理就好了。
- 另外一種是業(yè)務(wù)異常,比如這個人查不到,輸入的東西包含敏感詞等
- 第三種就是遠(yuǎn)程調(diào)用異常,這種屬于后端調(diào)用錯誤,可能包含重試
因為不包含復(fù)雜數(shù)據(jù)對象,所以錯誤直接render json就好了。
權(quán)限校驗
這里用了`cancancan`,具體使用大家自己去github上看吧。這里講下怎么應(yīng)用的。主要分兩塊,第一塊是后臺業(yè)務(wù)邏輯的權(quán)限判斷。
后臺業(yè)務(wù)權(quán)限
if @user.blank?
roles_for_anonymous
elsif @user.admin?
can :manage, :all
end
elsif @user.normal?
roles_for_members
elsif @user.blocked?
roles_for_anonymous
else
roles_for_anonymous
end
可以參考上面,分為以下幾種情況
- 未登錄 – 允許部分只讀
- 管理員 – 允許所有操作
- 普通用戶 – 按權(quán)限劃分
- 禁用用戶 – 允許部分只讀
這里說下roles_for_members權(quán)限,里面會有更詳細(xì)按照各個業(yè)務(wù)進(jìn)行劃分,業(yè)務(wù)的權(quán)限會有接口級別的權(quán)限條件判斷。
前端業(yè)務(wù)權(quán)限
比如在前端的時候,每個人的狀態(tài)是不一樣的,比如我作為圈主,我可以刪除自己圈子里一些用戶發(fā)的帖子,但我不能刪除其他人發(fā)的帖子。那這里是怎么做的?
在前端渲染的時候,每個對象都會帶一個權(quán)限表,表明我可以對這個對象做什么?
if object && object.is_a?(User)
%I[ban report].each do |action|
json.set! action, can?(action, object)
end
end
比如上面就是,我點(diǎn)開某人的信息頁,會返回我對這個人是否可以禁用或者舉報。類似的還有對帖子是否可以刪除,置頂扥等。
部署
對于部署來說,方式有很多。早些年容器化技術(shù)還沒流行的時候,大家都需要配置環(huán)境,然后用Capistrano進(jìn)行部署,或者用jenkins等等。
但是對于剛開始的項目來說,沒必要搞那么服務(wù),可能你的服務(wù)器一共就2個G,你還在上面折騰個jenkins,實在是不劃算。所以這里主要是用
docker docker-compose來做。
大家可以看張圖,方便理解
首先是Dockerfile
# Dockerfile
FROM ruby:2.6.5
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add –
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq && apt-get install -y build-essential nodejs yarn
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
RUN gem install bundler:2.1.2
ADD Gemfile Gemfile.lock yarn.lock $APP_HOME/
ADD vendor/cache vendor/cache # 因為是開發(fā)機(jī)直接部署,沒必要從服務(wù)器再每次拉一邊,所以先緩存了下來
RUN bundle install
ADD . $APP_HOME
RUN yarn install –check-files
RUN bundle exec rails assets:precompile
然后是docker-compose.production.yml
version: ‘3’
services:
your_project_name-production:
image: #遠(yuǎn)程image
container_name: #容器名稱
command: bundle exec rails s -e production
volumes:
– /app/log/#{your_project_name}-production:/app/log
ports:
– “7001:3000”
env_file:
– .env.production
network_mode: bridge
your_project_name-production-backup:
image: #遠(yuǎn)程image
container_name: #容器名稱
command: bundle exec rails s -e production
volumes:
– /app/log/#{your_project_name}-production-backup:/app/log
ports:
– “7000:3000”
env_file:
– .env.production
network_mode: bridge
最后是我們的部署腳本deploy-production.sh
#!/bin/bash
export SERVER=‘#你的服務(wù)地址’
export service=‘#你的項目名’
# 先打包bundle gem
bundle package
# 1. 傳輸部署文件
scp .env.production $SERVER:/app/$your_project_name/.env.production
scp docker-compose.production.yml $SERVER:/app/$your_project_name/docker-compose.yml
scp Dockerfile $SERVER:/app/$your_project_name/Dockerfile
# # 2. 打包鏡像
# 目前我用的是阿里云的鏡像服務(wù)
docker build -t $鏡像地址 .
# 3. push鏡像
docker push $鏡像地址
# 4. Pull鏡像 && 啟動
ssh $SERVER << EOF
cd /app/$your_project_name
docker-compose pull $your_project_name
docker-compose down && docker-compose up -d
docker-compose run -d $your_project_name bundle exec rails db:migrate RAILS_ENV=production
rm -rf .env.production
docker image prune -f
docker container prune -f
EOF
docker image prune -f
docker container prune -f
echo ‘deploy success!!!’
上面方式當(dāng)然存在很多問題,比如部署失敗他也提示成功等等。但是這個時候你也很容易到機(jī)器上去解決問題,或者更改配置進(jìn)行鏡像回滾。
測試
測試主要走rspec,具體各位自行去了解吧,有點(diǎn)寫不動了。因為獨(dú)立項目,所以沒有人會幫你測試,你需要自己保證代碼的健壯性。如果大家感興趣,回頭專門開一篇講測試思路的文章吧。反正盡量保證你所有接口和核心類的測試用例覆蓋就好了,像我的話代碼放在github,直接接 Travis CI 就行了,保證你每次master分支的代碼都能通過測試才發(fā)布。
最后
歡迎各位進(jìn)行進(jìn)行評論討論,互相學(xué)習(xí)。近期很多朋友找我聊項目的一些背景和想法,只言片語無法講清楚。后面會寫篇關(guān)于我對社群的一些理解和商業(yè)模式的一些看法,歡迎繼續(xù)關(guān)注我。
專注互聯(lián)網(wǎng)創(chuàng)業(yè)分享,獨(dú)立開發(fā)者。全網(wǎng)同名:盧燦偉
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。