2018年12月8日 星期六
Heoku pipeline stateless builds
Heroku pipeline 從 staging promot 到 production 的時候會把 compile 過的 file 一起 copy 過去,所以如果用到 Node + Webpack 讀取環境變數再 compile 成 js 的 file 就會吃到 staging 的環境變數而不是 production 的環境變數,這又稱為 stateful builds,可以看 https://github.com/heroku/heroku-buildpack-nodejs/issues/545 有更多的討論
解決方式可以參考 https://github.com/nholden/crawl_vote/commit/cd1b89dff4812da1270d6a5d54b594aecb1a5ca7 把一些變數放到 window裡面,這樣就會吃到正確的變數
https://devcenter.heroku.com/articles/pipelines#design-considerations
rspec test protocal (interface) - part 2 - define our own DSL for rspec
接續上一篇 https://everyday1percent.blogspot.com/2018/05/rspec-test-protocal-interface.html
現在我們要做一個客製化的 rspec matcher,達到的效果如下:
```ruby
RSpec.describe FooService do
describe "interfaces" do
specify do
expect(FooService).to has_implemented_interfaces :call, :call!
end
end
end
```
可以先參考 rspec 的 custom matcher 應該就可以做到了
https://relishapp.com/rspec/rspec-expectations/docs/custom-matchers/define-a-matcher-supporting-block-expectations
如果要做成 gem 的話可以參考 rspec-expectations 開放的 protocal
https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/matcher_protocol.rb
https://makandracards.com/makandra/662-write-custom-rspec-matchers
https://github.com/kucaahbe/rspec-html-matchers
在 Rails 使用 custom font
參考 https://coderwall.com/p/v5c8kq/web-fonts-and-rails-asset-pipeline 把檔案放到 `app/assets/fonts` 裡面
然後在 `application.scss` 裡加上
```scss
@font-face {
font-family: 'Calibre';
src: font-url('calibre/regular/Calibre-Regular.otf') format('opentype'),
font-url('calibre/regular/Calibre-Regular.woff') format('woff'),
font-url('calibre/regular/Calibre-Regular.ttf') format('truetype'),
font-url('calibre/regular/Calibre-Regular.svg#Calibre-Regular') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Calibre';
src: font-url('calibre/medium/Calibre-Semibold.woff2') format('woff');\
font-weight: bold;
font-style: bold;
}
b, strong {
font-weight:bold
}
h1, h2, h3, h4, h5, h6 {
font-weight:bold
}
這樣就行
(font-face 可以放在別的 file 再 import 也可以)
https://coderwall.com/p/v5c8kq/web-fonts-and-rails-asset-pipeline
https://github.com/nicotaing/chloepark/tree/master/themes/park/source/fonts/calibre/regular
2018年12月7日 星期五
Ruby instance_eval & block
```rb
class Test
def initialize(&block)
@name = "Hello World"
instance_eval(&block) if block_given?
end
def name
@name
end
end
```
這樣是完全可以的,因為在 block 裡面的 `self` 變成了 `Test` instance
```rb
Test.new do
puts name
end
# => Hello World
```
2018年12月2日 星期日
Ansible + rails + letsencrypt
First, install nginx
roles/nginx/tasks/main.yml
```yml
- name: Install nginx
apt:
name: nginx
state: latest
- name: Disable default nginx config
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify:
- Restart nginx
```
roles/nginx/handlers/main.yml
```yml
- name: Restart nginx
service: name=nginx state=restarted
```
Then, install LetsEncrypt
roles/letsencrypt/tasks/main.yml
```yml
- name: Generate dhparams file
shell: openssl dhparam -out {{ dhparam_pem_path }} 2048
args:
creates: "{{ dhparam_pem_path }}"
- name: Update apt cache
apt: update_cache=yes cache_valid_time=86400
- name: Install depends
apt: name={{ item }} state=latest
with_items:
- python
- python-dev
- gcc
- dialog
- libaugeas0
- augeas-lenses
- libssl-dev
- libffi-dev
- ca-certificates
- python-pip
- python-virtualenv
- git
- libpython-dev
- zlib1g-dev
- name: Lets Encrypt client
git: dest=/opt/certbot clone=yes repo=https://github.com/certbot/certbot force=yes
# Auto-renew certificates and reload nginx
- name: Add crontab to renew certificates
cron: minute="30" hour="2" weekday="1" job="/opt/certbot/certbot-auto renew >> /var/log/le-renew.log"
- name: Add crontab to reload server
cron: minute="35" hour="2" weekday="1" job="/etc/init.d/nginx reload"
```
Last, configure service
roles/service/tasks/main.yml
```yml
- name: Add http nginx configuration
template:
src: templates/http.conf.j2
dest: /etc/nginx/sites-available/{{ service_name }}.http.conf
notify:
- Restart nginx
- name: Enable nginx config
file:
src: /etc/nginx/sites-available/{{ service_name }}.http.conf
dest: /etc/nginx/sites-enabled/{{ service_name }}.http.conf
state: link
notify:
- Restart nginx
- name: Create letsencrypt certificate
shell: ./certbot-auto certonly --webroot --email {{ service_admin_email }} --agree-tos --webroot-path=/usr/share/nginx/html -d {{ service_host }};
args:
chdir: /opt/certbot
- name: Add https nginx configuration
template:
src: templates/https.conf.j2
dest: /etc/nginx/sites-available/{{ service_name }}.https.conf
notify:
- Restart nginx
- name: Add external https nginx symlink
file:
src: /etc/nginx/sites-available/{{ service_name }}.https.conf
dest: /etc/nginx/sites-enabled/{{ service_name }}.https
state: link
notify:
- Restart nginx
```
with nginx http template, this will redirect all http request into https
roles/service/templates/http.conf.j2
```
server {
listen 80;
server_name {{ service_host }};
root /usr/share/nginx/html;
index index.html index.htm index.nginx-debian.html;
location ~ /.well-known {
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
```
also add a https template for rails
roles/service/templates/https.conf.j2
```
upstream app {
server unix:///home/deploy/apps/waynechu_blog/shared/tmp/sockets/puma.sock fail_timeout=0;
}
server {
listen 443 ssl;
server_name {{ service_host }};
root /home/deploy/apps/{{ service_name }}/current/public;
try_files $uri/index.html $uri @app;
ssl_certificate {{ letsencrypt_ssl_dir }}/{{ service_host }}/fullchain.pem;
ssl_certificate_key {{ letsencrypt_ssl_dir }}/{{ service_host }}/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam {{ dhparam_pem_path }};
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
location @app {
proxy_pass http://app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
```
roles/service/handlers/main.yml
```yml
- name: Restart nginx
service: name=nginx state=restarted
```
At the last, set your own vars
vars/main.yml
```yml
service_host: "example.cc"
service_name: "example"
service_admin_email: "example@gmail.com"
letsencrypt_ssl_dir: "/etc/letsencrypt/live"
dhparam_pem_path: "/home/deploy/dhparams.pem"
```
That's it, now you have a https rails web
References:
https://medium.com/@gmaliar/generating-lets-encrypt-certificates-for-nginx-using-ansible-9fd27b90993a
https://gist.github.com/mattiaslundberg/ba214a35060d3c8603e9b1ec8627d349
https://docs.ansible.com/ansible/2.5/modules/letsencrypt_module.html
https://gorails.com/guides/free-ssl-with-rails-and-nginx-using-let-s-encrypt
https://blog.frost.tw/posts/2018/05/28/Getting-started-deploy-your-Ruby-on-Rails-Part-8/
標籤:
Deployment,
Developer Tools,
ROR,
Server
2018年11月18日 星期日
Implement algolia autocomplete with rails
```js
// app/javascripts/application.js
const algoliasearch = require('algoliasearch/lite')
const autocomplete = require('autocomplete.js')
const client = algoliasearch(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_SEARCH_ONLY_API_KEY)
const pack = {
algoliaClient: client,
autocomplete: autocomplete
}
window.Pack = pack
```
```erb
<!-- Algolia search autocomplete HTML markup -->
<div class="aa-input-container" id="aa-input-container">
<input type="search" id="aa-search-input" class="aa-input-search" placeholder="Search multiple fields in one place..." name="search" autocomplete="off" />
<svg class="aa-input-icon" viewBox="654 -372 1664 1664">
<path d="M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5 C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5 C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342 c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332 s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225 S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z" />
</svg>
</div>
<!-- Initialize autocomplete menu -->
<script>
var index = Pack.algoliaClient.initIndex('<%= index_name %>');
Pack.autocomplete('#aa-search-input',
{ hint: false }, {
source: Pack.autocomplete.sources.hits(index, {hitsPerPage: 5, filters: 'discarded_at_i < 1'}),
displayKey: 'name',
templates: {
suggestion: function(suggestion) {
return `<a href='<%= path_prefix %>/${suggestion.objectID}'>${suggestion._highlightResult.name.value}</a>`
}
}
}).on("autocomplete:selected", function (event, suggestion, dataset) {
window.location.href = `<%= path_prefix %>/${suggestion.objectID}`;
})
</script>
```
2018年11月17日 星期六
Submit a rails ActiveStorage issue
https://github.com/rails/rails/issues/34470
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
Integrate Uppy with Rails and ActiveStorage and use google drive direct upload
We're going to integrate Uppy with "upload from Google Drive" feature with rails backend and ActiveStorage, in this case we are going to upload multiple images at once and using the direct upload concept.
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
ActuveStorage has it's own direct upload JS client, however, it's not useful in this case because it's only able to upload real file data from local but not able to handle google drive upload, therefore we need to implement our own `direct upload` API for it, thankfully it's easy
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
2018年11月14日 星期三
Using marked.js with hightlight.js in rails to provide github flavored markdown to github style html
In this example, I use webpacker to import highlight js and marked js and css file, you can use cdn as well.
`app/javascripts/application.js`
```js
import marked from 'marked';
import hljs from 'highlight.js';
marked.setOptions({
highlight: function(code) {
return hljs.highlightAuto(code).value;
},
})
window.marked = marked
```
Let's say we have Post model with `content` field with markdown text
`app/views/posts/show.html.erb`
```erb
<h1><%= @post.title %></h1>
<%= @post.user.email %> - <%= @post.created_at %>
<div id="postContent">
<%= @post.content %>
</div>
<%= content_for :javascripts do %>
<script>
var contentElement = document.querySelector('#postContent')
contentElement.innerHTML = marked(contentElement.innerHTML)
</script>
<% end %>
```
And add application.js into layout by adding this line into `app/views/layout/application.html.erb`
```erb
<%= javascript_pack_tag 'application' %>
```
Now marked.js and hightligh.js are both working properly, check the HTML DOM on post page and you'll see html elements with some auto generated css classes.
However, it's not showing the right styles because we only apply JS but not CSS, now what we need to do is add github style which is provided by highlight.js.
In my other post I've shown how to add it: https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
And that's it, now the Post show page will show markdown text in HTML format with github styles.
Cheers.
https://github.com/highlightjs/highlight.js/issues/578
https://marked.js.org/#/USING_ADVANCED.md#highlight
https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
標籤:
CSS,
JavaScript,
ROR
Import css from npm module in rails with webpacker
I use highlight.js
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
Done.
You can simply replace highlight.js with any other module like bootstrap though
https://github.com/rails/webpacker/blob/master/docs/css.md
標籤:
CSS,
JavaScript,
ROR
2018年9月30日 星期日
2018年9月29日 星期六
Surfing tutorial video & tips
覺得講得不錯
https://surfsimply.com/surf-simply-tutorials/
## Tips
初學:
Pop up 的速度來自於 gravity
追浪的兩個重點:板的位置和速度
板的位置:追浪時盡量身體重心向前才有重力,通常 nose dive 是因為速度不夠快而不是太向前(盡可能前,找到適合的位置)
速度:Last 5 power paddle before waves get you, 提早建立速度,等浪來時速度就夠了。當尾被抬起時,再 3 extra power paddle。
起乘時會有往下的恐懼,千萬不可以把手放在板前面穩定版,應該要把手放在臀部跟腰間附近,想辦法 pop up
pop up 時只看向想要去的地方,不要看下面
中級:
當 Hunter, not hunted
往外一點,先加速再到 Spot X
遇到大浪一律往外滑,先不想怎麼越過,先想怎麼衝到這道浪,邊往外滑邊想
找 Spot X
斜著起乘會翻是因為板子不平,不是因為角度
靠起乘的最後三下 power paddle 改變板頭角度,面向想要往前的方向,而不是靠把板弄斜轉向
保持在浪的上半部
進階:
當衝到浪但是卻遇到前面有break了,與其往下走繞過去,應該要往上繞過 break,這樣才有足夠的速度繞過 break
https://surfsimply.com/surf-simply-tutorials/
## Tips
初學:
Pop up 的速度來自於 gravity
追浪的兩個重點:板的位置和速度
板的位置:追浪時盡量身體重心向前才有重力,通常 nose dive 是因為速度不夠快而不是太向前(盡可能前,找到適合的位置)
速度:Last 5 power paddle before waves get you, 提早建立速度,等浪來時速度就夠了。當尾被抬起時,再 3 extra power paddle。
起乘時會有往下的恐懼,千萬不可以把手放在板前面穩定版,應該要把手放在臀部跟腰間附近,想辦法 pop up
pop up 時只看向想要去的地方,不要看下面
中級:
當 Hunter, not hunted
往外一點,先加速再到 Spot X
遇到大浪一律往外滑,先不想怎麼越過,先想怎麼衝到這道浪,邊往外滑邊想
找 Spot X
斜著起乘會翻是因為板子不平,不是因為角度
靠起乘的最後三下 power paddle 改變板頭角度,面向想要往前的方向,而不是靠把板弄斜轉向
保持在浪的上半部
進階:
當衝到浪但是卻遇到前面有break了,與其往下走繞過去,應該要往上繞過 break,這樣才有足夠的速度繞過 break
2018年9月20日 星期四
常問的面試問題
目的是找出適合團隊的人,不是找牛人,所以我面試的時候會看溝通能力、合作能力大於技術能力,技術只需要達標就好,通常技術是否達標可以從討論 general tech concept 的時候就知道,最簡單的方法就是看對方對之前做的 project 的技術理解程度,我通常會問的問題列在下方:
## General:
之前團隊的組成
你擔任什麼角色
跟 XXX (designer、產品、後端) 有沒有過意見不合的時候,怎麼解決?
過去經驗中印象最深刻的事情
過去經驗中做過最有成就感的事
現在回想起來,在過去經驗中做錯的事。重新一次會怎樣 Approach?
你的職涯發展規劃是什麼?為什麼會想換工作?
你怎麼確保你照著自己的規劃走?
未來兩年在我們公司想做到什麼?
你理想中的團隊長怎樣?
最近有什麼感興趣的東西?
## Project specific:
我看到你前一份 Project 是要做 XXX,可以說說看這個 Project 嗎?
請問裡面的 OOO 功能是怎麼做到的?
有什麼其他的作法?
為什麼選擇之前的做法?
重來一次會怎麼選擇?
如果是使用 3rd party 供應商,背後的原理是什麼?
最困難的地方是什麼?
根據你之前的 approach,Bottle neck 是什麼?
我想到另一個作法,是 XXXXXX,你覺得呢?
(視情況再看要不要繼續鑽每個問題裡的技術細節)
My hiring strategy
很多 startup 都會說「我們只找最好的人」,我覺得這根本是屁話,哪有所謂最好的人,只有「在這個時間範圍內我能夠遇到的最適合團隊的人」而已,很繞口對吧?XD
讓我拆解並解釋一下這句話:
1. 首先是「在這個時間範圍內」,因為招人是有時間限制的,你只能遇到在這個時間範圍內有求職慾望的人
2. 再來是「我能夠遇到的」,因為招人的管道、地區、公司所處行業、公司知名度、厲害的人的 availability....等等的無限多的變數,公司能夠遇到的人相較於整個市場是很少的 %,所以並需要再加上這個 scope
3. 最後是最重要的「最適合團隊的人」這句話,之所以把「最好的人」改成「最適合團隊的人」原因是所謂的最好根本沒辦法定義,我們假設 DHH 是 Rails 最強的人、Martz 是 Ruby 最強的人(雖然我們都知道不是),難道我們就要找 DHH 和 Martz 進來嗎?他們也許根本就不適合團隊,那麼進來只會變成災難。所以沒有所謂最好的人,只有最適合團隊的人。
在多數的團隊中,找到一個 team player 比找到一個技術大神重要多了,以面試技術人員來說,首先技術能力能夠解決公司目前遇到的問題就是 60 分,代表及格可以 hire(但不代表你就要 hire 他),然後我們可以把所有及格可以 hire 的候選人聚集在一起比較,找出最適合的人,我個人主要看重的特點是:
- 合作能力 > 單打獨鬥的能力
- 潛力 > 現在的能力
- 喜歡做產品 > 喜歡鑽研技術 (for Startup)
- 謙虛但有自信 > 自大
大概是這樣
How to do a 1on1
## General Tips:
- 提早約時間,盡量按照 schedule ,給對方準備時間
- 1 on 1 前先問券問重點問題
- 多問問題, 不要過早下定論 (judge)
- 結束時要有結論(如果可以有可量化的 task 最好)
## SOP:
0. 提早看問卷的回覆,試著找出癥結點
1. 閒聊,問問生活上的近況 -> Building trust (Emotional Savings Account)
2. 開放性問題,問工作的近況和職涯規劃 -> onboarding
3. 進入 questions cycle
## Questions cycle:
1. Choose a question
2. Ask why
3. Dig deeper by asking more why
4. Receiving & Giving feedback (你覺得我可以怎麼做 & 我覺得你可以怎麼做)
5. Set a do-able action
## How to choose questions
depends on 團隊以及個人狀況,我認為問題分兩大類,個人以及工作
所謂個人問題並不是指日常生活的問題例如「哪間餐廳好吃」之類的,而是脫去工作的外殼,從個人角度出發的問題,例如「3 年後的職涯發展目標」、「最近在什麼事情上感到特別有成就感」之類的,而工作相關問題則是「最近的工作狀態給自己幾分」、「你認為現在團隊有沒有什麼可以改進的地方」這類直接跟工作場景連結再一起的問題。
我認為一個好的 1on1 至少要 4:6,無論是個人 4 或是工作 4 都可以,depends on 每個人的情況,但不可以只偏向其中一種,要有平衡。
https://getlighthouse.com/blog/one-on-one-meeting-questions-great-managers-ask/
2018年9月13日 星期四
Rails schema 到底怎麼保持乾淨
在不能 access production database 的情況下:
1. 怎麼保持 local database schema 是乾淨的
2. 如果髒了,怎麼恢復成乾淨的而且不影響現在 local 有的資料(乾淨的資料恢復,不乾淨的就丟掉)
## Idea 1:
Rails 內建的一些方法
要把 db 弄乾淨最簡單就是
db:drop, db:create, db:schema:load
直接 db:schema:load 的話不會刪掉多的 table
https://stackoverflow.com/questions/10301794/difference-between-rake-dbmigrate-dbreset-and-dbschemaload
問題:
1. db 資料會被清空
2. 必須仰賴 db:seed 保持乾淨並 up to date
## Idea 2:
dump db 出來,在 restore 的時候選擇性的 restore
pg_dump test-docker_development -c > tmp/dirty_dump.sql
問題:
1. schema_migration 的 table 會是錯的,不好改
2. pg_restroe 會暴力的把 table 內的資料結構改掉(如果不一樣),可能會造成 table 跟schema 不符合的問題
## Idea 3:
有一台只跟 production 跑一樣環境的機器,從上面要資料
問題:
1. 若 production 跑 task,這台機器也要跟著跑,不然資料一樣是錯的
## Conclusion
db:seed 檔就是為此而生的,但之所以會沒有人更新的原因是大家並沒有很常需要用到 seed 檔案,例如 deploy 到 staging 測試時 staging 本來就會有測試的舊資料(雖然很髒),所以不需要跑 seed,對 developer 而言也就不會 care 這麼多,反正這次我可以測就好,下次有人有問題再說。
所以問題就變成「要讓 seed 檔案在開發以及測試過程中扮演重要角色,這樣才有可能維護一個乾淨的 local 開發 schema & data」
要做到提升 seed 重要性就要改 Staging 測試的方式,例如讓 staging 用類似 docker container 的技術每次測試的時候都跑一個新的 App 並執行 seed,這樣就會強迫 developer 更新 seed 而且保持開發和測試效率
2018年9月5日 星期三
How to keep forked git repo up to dated?
### 1. Clone your fork:
git clone git@github.com:YOUR-USERNAME/YOUR-FORKED-REPO.git
### 2. Add remote from original repository in your forked repository:
cd into/cloned/fork-repo
git remote add upstream git://github.com/ORIGINAL-DEV-USERNAME/REPO-YOU-FORKED-FROM.git
git fetch upstream
### 3. Updating your fork from original repo to keep up with their changes:
git pull upstream master
https://gist.github.com/CristinaSolana/1885435
標籤:
Developer Tools,
Git
2018年8月19日 星期日
setup ubuntu with ansible for rails deployment
https://semaphoreci.com/community/tutorials/how-to-deploy-rails-applications-with-ansible-capistrano-and-semaphore
標籤:
Deployment,
Developer Tools,
ROR,
Server
2018年8月13日 星期一
JSON API 什麼時候用 included 什麼時候用 relationships
resource 有可能在兩邊都出現,relationships 出現的原因是給 API user 知道有哪些相關連資料,但不一定要給 data details。而 included 則是把關聯資料的 details 也一次回傳。
https://stackoverflow.com/questions/35991897/json-api-questions-included-vs-relationships
2018年8月2日 星期四
虛擬價值與證券化
Ex:
公司送禮
月餅廠商印 100 元的月餅卷賣 65 給經銷商
經銷商賣 80 給公司讓公司送員工
員工用 40 把月餅卷賣給票販子
月餅廠商用 50 再跟票販子收購月餅券
對員工而言的情感(虛擬)價值: 60
員工的實體價值:40
月餅廠商賺 15
票販子賺 10
單純情感(虛擬)價值利用證券化的方式來交換,不需要生產任何月餅。
retrospective meeting
Some good handy tips
https://www.atlassian.com/team-playbook/plays/retrospective
https://www.atlassian.com/team-playbook/plays/retrospective
2018年7月22日 星期日
不要在 rake task 裡定義 method
因為 rake task 的 scope 是 global 的,如果在裡面定義 method 等於是為 Object class 定義 private method,污染全部的 code
可行的做法之一是把要跑的 task 寫成 service 執行
另外推薦的做法是使用 `Rake::DSL`
```ruby
# lib/tasks/bicycle.rake
class BicycleTasks
include Rake::DSL
def initialize
namespace :bicycle do
task :assemble do
bicycle = Bicycle.new
# Assemble the bicycle:
attach_wheels(bicycle)
attach_handlebars(bicycle)
attach_brakes(bicycle)
end
end
end
private
def attach_wheels(bicycle)
# ...
end
def attach_handlebars(bicycle)
# ...
end
def attach_brakes(bicycle)
# ...
end
end
# Instantiate the class to define the tasks:
BicycleTasks.new
```
https://supergood.software/dont-step-on-a-rake/
2018年7月8日 星期日
Xcode Storyboard and NSCoder (Codable in swift)
.plist file 是一個 xml 格式的文件,意思是 Property List
Swift 可以用 Codable protocol 來處理這類文件
而 Xcode 的 Storyboard 也是類似的方式處理 View,只是用的是 OC 的 NSCoder
每當我們在 Storyboard 上更改什麼東西時,就會透過 NSCoder 轉換(encoding)成 Property 存在某個 .plist 文件裡,而當 App 啟動時則再用 NSCoder 讀取這些 Property 再轉化(decoding)為 UI 元件。
其實角色就像是 serializer
2018年7月7日 星期六
Swift weak keyword
weak keyword 是為了避免 ownership cycle,所謂 ownership cycle 就是當 Object A reference Object B 而 Object B 又 reference Object A 時,兩個 Objects 就會都無法釋放(Object 只有在沒有 strong reference 時才會被回收)
如果 Object 只有weak references ,也是會被回收。
所以此例 A 跟 B 其中一個用 strong reference 另一個用weak 就可以了
通常要用weak的地方就是 delegate
當然 UI 的東西也常常用 weak,但那是為了強調「ViewController」並不是 View Outlet 的 owner。
另外還有一個 keyword 叫做 unowned,下次再介紹
2018年7月2日 星期一
reset password token leaking
reset password 的網址常常是
https://yourdomain/password/reset?token=xxxx
但這樣就會被第三方網站從 `HTTP Referer` 內找到並紀錄起來 reset password 的 token,例如 google analytics,就有安全疑慮
幾個做法:
1. 一進到這個頁面馬上 invalidate 這個 token 然後generate 一個新的塞到 form 裡面
2. 進到 passwords#edit 時就把 params 移除存到 session 裡面再 redirect 回 passwords#edit 頁面,這樣 referer 就沒有token 資訊
https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links
https://yourdomain/password/reset?token=xxxx
但這樣就會被第三方網站從 `HTTP Referer` 內找到並紀錄起來 reset password 的 token,例如 google analytics,就有安全疑慮
幾個做法:
1. 一進到這個頁面馬上 invalidate 這個 token 然後generate 一個新的塞到 form 裡面
2. 進到 passwords#edit 時就把 params 移除存到 session 裡面再 redirect 回 passwords#edit 頁面,這樣 referer 就沒有token 資訊
https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links
讀書筆記 - 第一性原理
## 書名:
第一性原理
## 問題:
第一性原理是啥,怎麼應用
## 動機:
幫助自己成長,對創業以及人生自我管理
## 16 格筆記:
1. 絕對的真理
2. 要刻意練習
3. 發現真理後要身體力行
4. 為自己找習題,做習題增加肌肉記憶
5. 人90% 時間是靠潛意識
6. 所以要訓練潛意識
7. 回歸基本原理、回歸良知、事物本質
8. 道理有時間性和普世性,時間性代表古今適用,普世性代表跨領域適用
9. 還有所謂相對真理,
10.
11.
12.
13.
14.
15.
16.
## 察覺點:
## 總結:
用真理來解釋問題,事情就會明朗 新知識要靠刻意練習訓練自己的潛意識(肌肉記憶)才算學會
2018年6月30日 星期六
iOS 在 screens 之間傳資料
所謂 screens 之間就是 ViewControllers 之間,這裡討論的是 A screen 叫出了 B screen,但 B screen 想要送資料回 A screen 時該怎麼做。
通常如果是 A controller contains B controller (A screen 叫出了 B screen),那麼不應該讓 B screen 知道關於 A controller 的細節,否則就是 circular dependency,意思就是不該這樣做:
```swift
class BViewController: UITableViewController, . . . {
// This variable refers to the other view controller
var aController: AViewController
@IBAction func done() {
// Create the new checklist item object
let item = ChecklistItem()
item.text = textField.text!
// Directly call a method from AViewController
aController.add(item)
}
}
```
有時候有些 screen 是要被很多 ViewController 呼叫的,所以如果這樣做也同時失去了可以被很多 controller 呼叫的彈性
比較好的作法應該是用 delegate
如此一來 B 就不知道 A 是誰,B 只知道有哪些 delegate 可以用。
那怎麼 delegate 呢?在 Swift 裡面就是寫一個 protocol,Protocol 的功用就是給有 implement 這個 protocol 的 class 一些規範,說白了就是定義有哪些 functions, 哪些是 required 哪些是 optional
example:
```swift
protocol AddItemViewControllerDelegate: class {
func addItemViewControllerDidCancel(
_ controller: AddItemViewController)
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem)
}
```
從此之後 B controller 只需要
```swift
weak var delegate: AddItemViewControllerDelegate?
```
就可以操作 delegate 的 controller 了
通常 delegate 會是 weak variable 以及 optional
optional 的原因是 delegate 本來就 optional,要不要 implement 都沒關係。另外一個主要原因是當 StoryBoard load 這個 controller 時,並不知道 delegate 是誰,所以應該要是 optional。
通常會在 prepare 的 function 指派 delegate,此例就是在 A segue 到 B 時指派 A 成為 B 的 AddItemViewControllerDelegate 的 delegate,在 A controller 裡面加上:
```swift
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
// 1
if segue.identifier == "B" {
// 2
let controller = segue.destination
as! BController
// 3
controller.delegate = self
}
}
```
Delegates in five easy steps:
These are the steps for setting up the delegate pattern between two objects, where object A is the delegate for object B, and object B will send messages back to A. The steps are:
1 - Define a delegate protocol for object B.
2 - Give object B a delegate optional variable. This variable should be weak.
3 - Update object B to send messages to its delegate when something interesting happens, such as the user pressing the Cancel or Done buttons, or when it needs a piece of information. You write delegate?.methodName(self, . . .)
4 - Make object A conform to the delegate protocol. It should put the name of the protocol in its class line and implement the methods from the protocol.
5 - Tell object B that object A is now its delegate.
NSRange vs. Range and NSString vs. String
NSRange 是 Objective-C 的 Range, Range 則是 Swift 的 Range,是不同的結構。
但是 iOS API 內因為歷史因素都是給 NSRange ,所以通常要 cast 一下,例如:
```swift
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let oldText = textField.text!
let stringRange = Range(range, in: oldText)!
let newText = oldText.replacingCharacters(in: stringRange, with: string)
if newText.isEmpty {
doneBarButton.isEnabled = false
} else {
doneBarButton.isEnabled = true
}
return true
}
```
至於String 則是會 bridge 到 NSString,所以都有一樣的 API
Swift 回收 memory 的機制 - ARC
Automatic Reference Counting
字面意思就是看 object 被 reference 多少次,沒被 reference 就回收
https://tommy60703.gitbooks.io/swift-language-traditional-chinese/content/chapter2/16_Automatic_Reference_Counting.html#how_arc_works
字面意思就是看 object 被 reference 多少次,沒被 reference 就回收
https://tommy60703.gitbooks.io/swift-language-traditional-chinese/content/chapter2/16_Automatic_Reference_Counting.html#how_arc_works
2018年6月29日 星期五
可以傳 AR object 的自制 form object
```ruby
class BaseForm
include ActiveModel::Model
attr_reader :record
def initialize(record)
@record = record
record.attributes.each do |key, value|
class_eval do
delegate "#{key}=", to: :record
delegate "#{key}", to: :record
end
end
end
def save
valid? && record.save
end
def save!
valid? && record.save!
end
end
標籤:
Design Pattern,
ROR
讀書筆記 - 鉤癮效應 Hooked - Nir Eyal
習慣指的是想都不用想就能進行的行為,且習慣是可以刻意製造的,方式是
習慣不是創造出來的,而是累積出來的
觸發(內在與外在)-> 行動 -> 變動獎賞 -> 投入
## 觸發
一開始都是外在觸發,例如文章、廣告、通知等等
然後藉由刻意製造的習慣讓使用者開始有內在觸發,例如開啟臉書與社交需求結合,就變成了內在觸發
### 外在觸發:告訴使用者下一步該怎麼做,有幾種類型:
1. 付費買來的
2. 贏得的(例如其他媒體報導)
3. 透過關係的(口耳相傳)
4. 公司自有的(APP 通知、電子郵件訂閱等)
前三者是帶來新客源的好方法,第四種是促進使用者養成習慣
### 內在觸發
負面情緒是強大的內在觸發,例如無聊、寂寞、挫折、猶豫、疑惑等
這些觸發最後幾乎都跟社會認同有關
## 行動
預期會有獎賞的行為,例如點擊一個連結就看到有趣的照片
任何行為的開始都需要三大要素:
1. 必須有充分動機 (Motivation)
2. 必須有完成目標的能力 (Ability)
3. 必須有觸發 (Trigger) 來啟動
所以 行動 = MAT
若說內在觸發是使用者一天之中時常感到的心癢難耐,那行動就是搔癢
很重要的一點是要盡量讓所有人都有能力,這樣他們才會行動,就是要降低產品使用的難度,所謂難度有幾個面向:
1. 時間:完成需要多久
2. 金錢
3. 勞力付出
4. 腦力運轉:需要的心思與專注力
5. 社會偏差(social deviance):其他人對這個行為的接受度
6. 不符慣例:行動配合或是打亂目前例行活動的程度
所以提高動機或是能力呢?先從哪一個下手?
答案是絕對要先從提高能力
除了理性的原因外,人還有很多非理性的行為,例如
1. 認為物以稀為貴
2. 框架效應:根據周遭環境作出判斷(ex: 厲害的小提琴家在地鐵站拉小提琴沒人聽)
3. 定錨效應:在看到某個資訊時就會拿來做為參考標準。(打折跟沒打折一樣價,還是想買打折的)
4. 人為推進:人們在相信自己接近目標時動機就會加強,例如集點卡跟進度條
## 變動獎賞
使用者作出行動後得到的獎賞,若能增添獎賞的變化會更有效果
例如 App 無限下滑加載,因為期待之後無法預期的新 content,所以就會一直滑
這是由於受大腦中的伏隔核區域影響,且有趣的是期待獎賞時伏隔核區域的活躍程度比真正拿到獎賞時還高,代表期待拿到獎賞比真的拿到獎賞更令人興奮
三大變動獎賞:
1. 部落型獎賞:想被認同
2. 狩獵型獎賞:想持續追尋一個東西(人類狩獵就是在追尋),而現在就是追尋更多的資訊
3. 自我型獎賞:渴求勝利感、成就感
設計獎賞系統時的重點:
1. 獎賞必須與使用者使用的動機以及內在觸發一致
2. 讓使用者保持自主感,提醒他們有選擇的自由他們越容易被說服(開啟通知吧,之後隨時可以關唷)
3. 讓使用者覺得有主控權,讓他們自己想要使用而不是覺得自己必須使用
## 投入
讓使用者增進下一次循環的行動,例如追蹤人、分享好友、加到最愛選單等
如果說頻率是習慣的第一要素,那第二要素就是「使用者對行為的態度有所改變」,人們會隨著付出越多而越喜愛某件事物
1. 我們在評價自己的付出時是不理性的
2. 且我們希望自己的前後行為是一致的(所以可以漸進式的"引導"使用者,這樣就會想要行為一致而接受)
3. 我們都不想認知失調,所以別人說好的東西我就會慢慢也覺得好
確切來說,價值儲存是最好的投入方式之一,所謂價值儲存包含:
1. 內容(ex: 歌單)
2. 數據(ex: 輸入資訊)
3. 粉絲關注
4. 聲譽
要做到:
* 當用戶將產品融入例行公事,就會產生依賴,且會對價格更不敏感
* 新進入市場者若想要成功,不能只是比別人好,要比現有產品好好上 9 倍
知覺效用就是相對於其他方案的有用程度,要讓使用者養成習慣,要嘛就是發生頻率要高、不然就是要很有用
rails model conditional validation 的神奇用法 extend
lol, 沒想過可以這樣
```ruby
user = User.find(id)
user.extend(User::RegistrationContext)
```
```ruby
# app/models/user/registration_context.rb
module User::RegistrationContext
def self.extended(model)
class << model
validates :terms_of_service_accepted, acceptance: true
end
end
attr_accessor :terms_of_service_accepted
end
```
```ruby
user = User.new
user.extend(User::RegistrationContext)
user.terms_of_service_accepted = "0"
user.valid?
=> false
user.errors.messages[:terms_of_service_accepted]
=> ["must be accepted"]
```
哈,雖然應該不會去用這種做法,但還是學習了XD
https://karolgalanciak.com/blog/2018/06/24/rails-and-conditional-validations-in-models/
```ruby
user = User.find(id)
user.extend(User::RegistrationContext)
```
```ruby
# app/models/user/registration_context.rb
module User::RegistrationContext
def self.extended(model)
class << model
validates :terms_of_service_accepted, acceptance: true
end
end
attr_accessor :terms_of_service_accepted
end
```
```ruby
user = User.new
user.extend(User::RegistrationContext)
user.terms_of_service_accepted = "0"
user.valid?
=> false
user.errors.messages[:terms_of_service_accepted]
=> ["must be accepted"]
```
哈,雖然應該不會去用這種做法,但還是學習了XD
https://karolgalanciak.com/blog/2018/06/24/rails-and-conditional-validations-in-models/
2018年6月28日 星期四
iOS learning
Table View 兩種:
* plain - 一般的
* grouped - 灰底,像設定檔
TableView 有 cell 和 row 的概念
可以想像 cell 就是顯示出來的 row,是會被 reuse 的 object
而 row 就是 table view 有的全部資料
所以 cell 要有 identifier,這樣才可以被 reuse,代表是哪一種 cell
TableView 要有 data 就要 implement data source protocol, 所謂 protocol 就是規定好的 functions 要你 implement
此時 ViewController 就是 table view data source 的 delegate,因為 data 是什麼都跑來問 view controller 了
story board 為元件加上 tag,就可以用 cell.viewWithTag(tag) 來拿到
cell 裡的 label 要用這樣的拿法因為如果用 @IBOutlet 就只能 reference 一個,是不合理的
UITableViewDataSource => 顯示的資料
UITableViewDelegate => 處理行為
```swift
if let cell = tableView.cellForRow(at: indexPath) {
if cell.accessoryType == .none {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
```
用 if let 的方法可以處理 let 後面的 expression 是 nil 的情況,是常用的技巧
```swift
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {}
```
這裡的 numberOfRowsInSection 是外部參數,section 是區域參數,function 裡面用 section,外面的 function 是看到 numberOfRowsInSection,至於 _ 底線就是不想要有外部名稱時用的
```swift
// This declares that items will hold an array of ChecklistItem
// objects but it does not actually create that array.
// At this point, items does not have a value yet.
var items: [ChecklistItem]
required init?(coder aDecoder: NSCoder) {
// This instantiates the array. Now items contains a valid array
// object, but the array has no objects inside it yet.
items = [ChecklistItem]()
}
```
* plain - 一般的
* grouped - 灰底,像設定檔
TableView 有 cell 和 row 的概念
可以想像 cell 就是顯示出來的 row,是會被 reuse 的 object
而 row 就是 table view 有的全部資料
所以 cell 要有 identifier,這樣才可以被 reuse,代表是哪一種 cell
TableView 要有 data 就要 implement data source protocol, 所謂 protocol 就是規定好的 functions 要你 implement
此時 ViewController 就是 table view data source 的 delegate,因為 data 是什麼都跑來問 view controller 了
story board 為元件加上 tag,就可以用 cell.viewWithTag(tag) 來拿到
cell 裡的 label 要用這樣的拿法因為如果用 @IBOutlet 就只能 reference 一個,是不合理的
UITableViewDataSource => 顯示的資料
UITableViewDelegate => 處理行為
```swift
if let cell = tableView.cellForRow(at: indexPath) {
if cell.accessoryType == .none {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
```
用 if let 的方法可以處理 let 後面的 expression 是 nil 的情況,是常用的技巧
```swift
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {}
```
這裡的 numberOfRowsInSection 是外部參數,section 是區域參數,function 裡面用 section,外面的 function 是看到 numberOfRowsInSection,至於 _ 底線就是不想要有外部名稱時用的
```swift
// This declares that items will hold an array of ChecklistItem
// objects but it does not actually create that array.
// At this point, items does not have a value yet.
var items: [ChecklistItem]
required init?(coder aDecoder: NSCoder) {
// This instantiates the array. Now items contains a valid array
// object, but the array has no objects inside it yet.
items = [ChecklistItem]()
}
```
2018年6月26日 星期二
Sam Altman co-found 的公司 Open AI 在強化學習 (Reinforcement Learning)上有了進展
http://blog.samaltman.com/reinforcement-learning-progress
Sam Altman co-found 的公司 Open AI 在強化學習上有了進展,證明專精於特定領域的強化學習演算法可以有效的解決問題,在這篇文章中 Dota 機器人的勝率已經高達 95%
強化學習被稱作「近似動態規劃」(approximate dynamic programming,ADP)
https://zh.wikipedia.org/zh-tw/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0
Sam Altman co-found 的公司 Open AI 在強化學習上有了進展,證明專精於特定領域的強化學習演算法可以有效的解決問題,在這篇文章中 Dota 機器人的勝率已經高達 95%
強化學習被稱作「近似動態規劃」(approximate dynamic programming,ADP)
https://zh.wikipedia.org/zh-tw/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0
2018年6月24日 星期日
growth engineering at Netflix
用 API 控制前端 signup 時要顯示的 fields,算是 API 控制 UI 的一種方法,有趣
https://medium.com/netflix-techblog/growth-engineering-at-netflix-accelerating-innovation-90eb8e70ce59
2018年6月23日 星期六
Vue + Webpacker + Rails
https://mkdev.me/en/posts/rails-5-vue-js-how-to-stop-worrying-and-love-the-frontend
標籤:
Developer Tools,
ROR
Suspender 用 1.46.0 可以使用 rails 5.1.6
ThoughtBot 的 rails bootstraper,現在最新版 default 使用了 rails 5.2 很煩,因為我不喜歡 5.2 的 credential
要使用 5.1.x 版本的話要用 v1.46.0 版本的 Suspender,但 1.46.0 是用 Ruby 版本 2.5.0
所以確切方法是
```
rvm install 2.5.0
rvm use 2.5.0
gem install suspenders -v 1.46.0
suspender your_project_name
```
然後再進去 project 裡面把 Gemfile 和 .ruby-version 裡的 ruby version 改成2.5.1
https://github.com/thoughtbot/suspenders/tree/v1.46.0
要使用 5.1.x 版本的話要用 v1.46.0 版本的 Suspender,但 1.46.0 是用 Ruby 版本 2.5.0
所以確切方法是
```
rvm install 2.5.0
rvm use 2.5.0
gem install suspenders -v 1.46.0
suspender your_project_name
```
然後再進去 project 裡面把 Gemfile 和 .ruby-version 裡的 ruby version 改成2.5.1
https://github.com/thoughtbot/suspenders/tree/v1.46.0
標籤:
Developer Tools,
ROR
讀書筆記 - 看人的藝術 - Sam Gosling
屬於心理學研究的書籍
個性的表達方式分為三個層面
1. 身份標籤 (Identity Claims)
2. 情感調節器 (Feeling regulations)
3. 行為痕跡 (behavioral residue)
這些是作為我們判斷的線索,將線索分類成這三種層面
在對方感到真正安全的地方才能更容易發現這些象徵,例如臥室或書房,被丟棄的垃圾也是很有參考價值的物品
評估一個人則可以從五大個性來評估
1. 開放性 (Openness)
2. 責任感 (Conscientiousness)
3. 外向性 (Extraversion)
4. 宜人性 (Agreeableness)
5. 神經質 (Neuroticism)
麥克亞當斯體系:先看特徵,再關注個體。特徵即上述五大性格,個體則從角色、目標、技能、價值觀來判斷,當兩個層面都理解,就能觸碰到個性的根基:身份。
人是很容易用片段經驗定義自己,這稱為自我定義記憶
用物品來判斷一個人時,只根據片面的資訊是不實際的,例如看到辦公桌凌亂就覺得此人沒責任感是不實際的判斷,必須要從各個不同的環境裡蒐集正反證據來判斷,環境又分為不同維度,有私人安全以及公開的環境
直覺有時也是錯的,研究顯示常講甜言蜜語的戀人跟常互相開罵的戀人之間在一起的時間沒有太大差異。
握手和常使用的語言是有用的判斷依據,觀察先要確定要觀察哪個面向後再開始觀察會更有效
所有的偽裝都有破綻
自戀者在權利和責任感方便表現特好,且對讚美有無限的接受能力
不同個性的人看待世界也是不同的,有些人覺得自己很懶散,但其他人並不這麼認爲,因為心中的標準不一
人不一定只會希望別人用積極的方式對待自己,有時也會希望別人看待自己負面,這稱作自我驗證理論。例如:某人认为自己没有创造力,即便他认为这是不好的一种品质,他也会将这种缺乏创造力的方面展现出来
刻板印象有時還是很有效的判斷依據,居住地、人種、膚色、性別
伦斯维克的分析表明,交谈、手势以及衣着确实是表现社交能力的有效线索,但是只有正式的着装才能预测应聘者的工作动机
居住空间中两个最明显的线索是大五特征中的两个特征——开放性和责任感
障眼法
1. 第一印象 - 刻板印象影響思考與觀察,讓人忽略了其他明顯的相反特徵
2. 从其他线索中得到的部分线索
如果你担心观察对象正尝试欺骗你,将这些公开的事物和那些更加私密的事物进行比较
Rails where.not 的寫法會略過欄位是 Null 的值
`where.not` 的寫法會略過欄位是 Null 的值,所要對有可能是 Null 值的字串欄位使用 `where.not` 有兩個做法:
1. default 空字串
2. 使用 `or`
另外如果是 boolean 欄位但有可能是 null (雖然一開始就不應該這樣,至少 default true or false),解決方法就脆直接用 where 反正總共才三個值
ex:
User.where(subscribed: [nil, false])
https://robots.thoughtbot.com/activerecord-s-where-not-and-nil
2018年6月22日 星期五
Conway's Game of Life
今年 Ruby Kaigi 看到 Matz 參加 live pair 做 Conway's Game of Life,才發現我常常聽到這個詞但從來沒去研究這到底是啥、為何這麼多人都知道
所以去查了一下,原來是一個模擬細胞生存的遊戲,吸引人的點在於這個遊戲可以有很多變體,甚至初始化細胞的方式不同就可以讓 "生命" 發展有很大的不同,有點像是扮演上帝的角色,而且規則明確,是一個很容易用程式實作的遊戲,難怪那麼有名
想要體驗一下可以到 http://nealwang.net/JustForFun/GameOfLife.html ,先把下面的格子一部分勾選成紅色的,記得要有三個連在一起的紅色,這樣比較好玩
規則:
- 當前細胞為存活狀態時,當周圍低於2個(不包含2個)存活細胞時, 該細胞變成死亡狀態。(模擬生命數量稀少)
- 當前細胞為存活狀態時,當周圍有2個或3個存活細胞時, 該細胞保持原樣。
- 當前細胞為存活狀態時,當周圍有3個以上的存活細胞時,該細胞變成死亡狀態。(模擬生命數量過多)
- 當前細胞為死亡狀態時,當周圍有3個存活細胞時,該細胞變成存活狀態。 (模擬繁殖)
- live < 2, die
- live 2/3, live
- live with >3, die
- dead with == 3, spawn
覺得挺有趣的,用 Ruby 簡單實現了一下:
https://github.com/wayne5540/conways_game_of_life
https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F
https://www.youtube.com/watch?v=zEf6iUIkjf4
2018年6月20日 星期三
關於 API 設計的一些想法
大哉問:什麼樣的 API 是好的 API?
答案:沒有所謂的好跟壞,只有適合哪個場景。
所以要做出一個好的 API,最重要的是了解你的 API User 要拿這個 API 來做什麼。
業界常用的 Best Practice
## RESTful
常見的 API 規範:
RESTful, gRPC, GraphQL
以最常見的 RESTful (Representational State Transfer)為例說明
什麼是 RESTful? 網路上一堆複雜的說法,對我來說就是:
## Authorization
* HTTP Authentication: Basic and Digest Access Authentication https://tools.ietf.org/html/rfc2617
```
Authorization: Basic xxxxxxx
```
* JWT token - https://jwt.io/
* OAuth(2)
我們? HTTP Auth + 自製 Token
## Response
* JSON format
* Error code, error message
* 正確的 status code
## Documentation
* API blueprint
* Swagger
我們:V0,V1: Test generated HTML document, can support Swagger or API blueprint. V2: Swagger
開發流程
1. 跟 Client 討論需要的數據
2. 用 Swagger 或是 API blueprint 先寫 document
3. Client 按照 document 先 mock 資料
4. Server 開發 API, 用自動化工具測試 API endpoint 是否符合 document (Bonus: easy to TDD)
5. Client & Server 一起完工,皆大歡喜
開發盲點
1. RESTful 代表我要把 Table 映射到 API response 嗎?
2. Client 要求跟 Server 的衝突,Client 想要一個request 拿到所有資訊,Server 想根據規定給 response, 例如 GET /user 還要順便拿 user.groups 的資訊
https://github.com/shieldfy/API-Security-Checklist
- 有明確的錯誤訊息
- 回傳有意義的 HTTP status code
- 能回傳指定的格式 (Multiple Output Formats)(accept: json/application)
- 好記的名字
- Deep Filtering (order, joins, pagination)
- Typed Values - response 有固定格式
- 好懂的文件 (Interactive Documentation better)
- 能夠有版本管理,有 deprecated warning
- High Performance
- High Availability
- Developer Community
- 規範有跡可循並且一致
- Sandbox mode
答案:沒有所謂的好跟壞,只有適合哪個場景。
所以要做出一個好的 API,最重要的是了解你的 API User 要拿這個 API 來做什麼。
業界常用的 Best Practice
## RESTful
常見的 API 規範:
RESTful, gRPC, GraphQL
以最常見的 RESTful (Representational State Transfer)為例說明
什麼是 RESTful? 網路上一堆複雜的說法,對我來說就是:
- 對應正確的 HTTP Request, 新增用 Post, 刪除用 Delete, 更新用 Patch, 查詢用 Get
- 每個 EndPoint 就是一個 Resource,`GET /user` 就應該回傳 User
## Authorization
* HTTP Authentication: Basic and Digest Access Authentication https://tools.ietf.org/html/rfc2617
```
Authorization: Basic xxxxxxx
```
* JWT token - https://jwt.io/
* OAuth(2)
我們? HTTP Auth + 自製 Token
## Response
* JSON format
* Error code, error message
* 正確的 status code
## Documentation
* API blueprint
* Swagger
我們:V0,V1: Test generated HTML document, can support Swagger or API blueprint. V2: Swagger
開發流程
1. 跟 Client 討論需要的數據
2. 用 Swagger 或是 API blueprint 先寫 document
3. Client 按照 document 先 mock 資料
4. Server 開發 API, 用自動化工具測試 API endpoint 是否符合 document (Bonus: easy to TDD)
5. Client & Server 一起完工,皆大歡喜
開發盲點
1. RESTful 代表我要把 Table 映射到 API response 嗎?
2. Client 要求跟 Server 的衝突,Client 想要一個request 拿到所有資訊,Server 想根據規定給 response, 例如 GET /user 還要順便拿 user.groups 的資訊
https://github.com/shieldfy/API-Security-Checklist
讀書筆記 - 成功與運氣 - Robert H Frank
規律一:運氣可以放大
規律二:運氣可以累加
規律三:競爭越激烈,運氣越重要
成功和失敗常常取決於人無法控制的重要事件
那些對自己優勢視若無睹的人,往往同樣無視別人的劣勢
謙虛讓世界變更好
框架效應:針對同一個問題用兩種邏輯上意義相似的說法描述,可以導致不同的判斷 => 認為成功是自己努力得來的,或是承認成功是一部分的運氣
框架效應製造了巨大的偏見,認為自己努力得來的人變得中私人消費而輕公共建設
然而所謂運氣卻也包含了你出生的國家、你從小受的教育,這些都跟公共建設有關
必須認清自己是一個幸運的人,你的成功是建築在你的運氣,所以你得到的不全然都應該是你的
後市偏差:個體面臨不確定性時,往往對先前獲得的訊息有過高評價,進而在決策上導致偏差。(人們在一件事情發生後,很常用一個故事來證明這在所難免)
後視偏差讓人簡單化成功的要素,卻遺忘了成功是各種前因後果累積的結果
馬太效應:反映經濟學中贏家通吃的情況
月暈效應:以偏概全的正反面版本,ex: 一首歌的成功很大程度取決於第一個評價是否是好的
無數的例子證明了微小的隨機因素的力量,例如:
職業曲棍球員有 40% 出生在 1~3 月,只有 10% 出生在 10~12 月,因為青少年曲棍球聯盟的截止出生日期是每年的 1/1,代表越前面月份出生的人越能夠在體能上有同年齡層的優勢,甚至 6~7 月出生的CEO人數比均值低 1/3, 姓氏字母排序也會造成不同
運氣很重要,但努力也重要,對於渴望成功的人來說,對於一個別人重視的事情上累積專業知識是最有用的,因為專業知識不來自於運氣而來自於努力
運氣可以放大,新技術和市場制度為最能幹的人提供了更多的工具,所以能幹(運氣好的人)變得更能幹
尤其是高度競爭的情況下運氣更重要,因為大家的條件都差不多,此時勝負往往取決於那些隨機因素
這是一個贏家通吃的市場,通常這種市場有兩個特點:
1. 回報取決於相對實力而非絕對實力(ex: 網球冠軍因為對手狀況不佳而得冠)
2. 回報往往高度集中在某幾個頂尖玩家手中(ex: Federral, Nadal)
甚至連長尾的得利者都是贏家,因為即使是新技術也很難解除一個重要的市場限制:的時間精力和稀缺性
理由是大多數人都不喜歡做選擇,於是就選擇最受歡迎的商品來逃避選擇
(Lake Wobegon effect)沃博艮湖效應:人往往高估自己
而這也被達爾文的觀點所應證,因為覺得自己會贏得比賽的人會去參加更多的比賽
面對直接威脅到生存的環境,強調當前的成本收益可能是有利的。但在相對穩定的環境,只關心眼前成本利益則是失敗的根源。
可利用性法则(Availability heuristic):我們會用容易記憶的事情來作為判斷的依據,這幾乎從根本上這幾乎從根本上決定了我們的描述是偏頗的(記得成功的理由是自己做的正確決定)
計劃未來是,相信一切在自己掌握中,哪怕那只是一個幻覺。回顧過去時,你應該知道你得到的遠超過你應得的
我們對事物的衡量很大程度取決於周圍的事物
生活中很多重要的回報取決於相對地位
不過分看重自己的人,總是能得到他人的尊重。不過份要求利益的人,總是能對自己得到的那份滿意
所以,承認你的成功一定程度上是源自於他人的努力。
規律二:運氣可以累加
規律三:競爭越激烈,運氣越重要
成功和失敗常常取決於人無法控制的重要事件
那些對自己優勢視若無睹的人,往往同樣無視別人的劣勢
謙虛讓世界變更好
框架效應:針對同一個問題用兩種邏輯上意義相似的說法描述,可以導致不同的判斷 => 認為成功是自己努力得來的,或是承認成功是一部分的運氣
框架效應製造了巨大的偏見,認為自己努力得來的人變得中私人消費而輕公共建設
然而所謂運氣卻也包含了你出生的國家、你從小受的教育,這些都跟公共建設有關
必須認清自己是一個幸運的人,你的成功是建築在你的運氣,所以你得到的不全然都應該是你的
後市偏差:個體面臨不確定性時,往往對先前獲得的訊息有過高評價,進而在決策上導致偏差。(人們在一件事情發生後,很常用一個故事來證明這在所難免)
後視偏差讓人簡單化成功的要素,卻遺忘了成功是各種前因後果累積的結果
馬太效應:反映經濟學中贏家通吃的情況
月暈效應:以偏概全的正反面版本,ex: 一首歌的成功很大程度取決於第一個評價是否是好的
無數的例子證明了微小的隨機因素的力量,例如:
職業曲棍球員有 40% 出生在 1~3 月,只有 10% 出生在 10~12 月,因為青少年曲棍球聯盟的截止出生日期是每年的 1/1,代表越前面月份出生的人越能夠在體能上有同年齡層的優勢,甚至 6~7 月出生的CEO人數比均值低 1/3, 姓氏字母排序也會造成不同
運氣很重要,但努力也重要,對於渴望成功的人來說,對於一個別人重視的事情上累積專業知識是最有用的,因為專業知識不來自於運氣而來自於努力
運氣可以放大,新技術和市場制度為最能幹的人提供了更多的工具,所以能幹(運氣好的人)變得更能幹
尤其是高度競爭的情況下運氣更重要,因為大家的條件都差不多,此時勝負往往取決於那些隨機因素
這是一個贏家通吃的市場,通常這種市場有兩個特點:
1. 回報取決於相對實力而非絕對實力(ex: 網球冠軍因為對手狀況不佳而得冠)
2. 回報往往高度集中在某幾個頂尖玩家手中(ex: Federral, Nadal)
甚至連長尾的得利者都是贏家,因為即使是新技術也很難解除一個重要的市場限制:的時間精力和稀缺性
理由是大多數人都不喜歡做選擇,於是就選擇最受歡迎的商品來逃避選擇
(Lake Wobegon effect)沃博艮湖效應:人往往高估自己
而這也被達爾文的觀點所應證,因為覺得自己會贏得比賽的人會去參加更多的比賽
面對直接威脅到生存的環境,強調當前的成本收益可能是有利的。但在相對穩定的環境,只關心眼前成本利益則是失敗的根源。
可利用性法则(Availability heuristic):我們會用容易記憶的事情來作為判斷的依據,這幾乎從根本上這幾乎從根本上決定了我們的描述是偏頗的(記得成功的理由是自己做的正確決定)
計劃未來是,相信一切在自己掌握中,哪怕那只是一個幻覺。回顧過去時,你應該知道你得到的遠超過你應得的
我們對事物的衡量很大程度取決於周圍的事物
生活中很多重要的回報取決於相對地位
不過分看重自己的人,總是能得到他人的尊重。不過份要求利益的人,總是能對自己得到的那份滿意
所以,承認你的成功一定程度上是源自於他人的努力。
2018年6月17日 星期日
Amazing dolphin
看到 Netflix 的海洋紀錄片,裡面提到海豚捕食沙丁魚群方法是潛入水裡釋放泡泡,利用泡泡把沙丁魚聚集起來補食,會不會太聰明?
難怪銀河列車指南裡面說地球上第二聰明的生物是海豚,第三才是人類,哈哈 XD
蛤?什麼?你問第一是誰?不會自己去看銀河列車指南嗎
2018年6月16日 星期六
讀書筆記 - 股市心裡操縱術 - G.C. Selden
只要有更好的投資標的,就不該管現在投資的損益,那些都是沈沒成本,而應該直接把現在的投資換到更好的投資標的上
大眾堅信不移的說法大多是錯的
自己過度貪心可以從幾點看出來:
1. 不斷提高自己的投資報酬率
2. 原先覺得風險高的投資,現在看起來能穩操勝卷
3. 不再分散投資風險
4. 堅信這次肯定不同
恐慌是一種比貪婪更強烈、更猛爆的情緒,這也是為什麼股市空頭跌勢通常極為猛烈,讓人措手不及,而相對的上漲過程中總是較趨勢溫和的原因
恐慌不是突然出現的,而是長期累積的結果
恐慌時期出現的價格底部往往不是由恐懼造成,因為恐懼的股民早已拋售股票,它是由於一些人資源耗盡、不得不賣出股票造成,如果能給他們時間他們會繼續持有的,所以「時間是交易的本質」不能忘記
持續下跌後,價格本身並不能吸引購買,問題的核心是資金的流動量有多少(曾經被證明過:紐約票據交換所儲蓄超過放貸,快速反彈很快就出現了)
一個人想要什麼,就會相信什麼(盲點)
理性的股民會在最大收益時最小化所承擔的風險,然而,過分自信的股民會錯誤地判斷他們所承擔的風險水平,從而持有較高風險的投資組合
過度自信的股民傾向買賣過去勝利過的組合
股民最好做到完全忘記自己的艙位、收益和損失,忘記當前價格與自己買進或賣出價格的關係,而只管心市場動態。不論是營利還是損失。
不該下單的情況:
1. 沒有未來的企業不下單
2. 未經研究不下單
3. 短期暴漲過的個股不下單
4. 情緒不好的時候不下單
5. 未設止損不下單
最糟糕的幾種心態:
1. 賭徒心態,追求短期刺激,盲目追逐高風險高獲利
2. 鴕鳥心態,虧太慘就當作沒看到放在那繼續爛
3. 敝叟自珍心態,因為是自己的所以認為很好
4. 急功近利心態,投資要看長線
標籤:
Financial,
Investment,
Reading Note
2018年6月10日 星期日
Understanding your Circle of Competence
之前在 Twitter 看到一個段落是說成功的關鍵在於 focus
但什麼是 focus,這個 Circle of Competence 也許是一個更好的解釋
所謂 Circle of Competence 意思就是「你自己真的理解但多數人不是真的理解的知識」
以餐廳運作為例,大家也許都知道餐廳的定價策略等等,人人都可以侃侃而談,但如果餐廳換成藥店、換成生物科技呢?
from 灣區日報
巴菲特与 Charlie Munger 的投资、处世哲学:找到你的 Circle of Competence,在里面努力你就更容易成功;学习、实践、经验积累可以慢慢拓展这个 circle,但要勇于承认自己不知道的事。
https://wanqu.co/a/6593/2018-06-09-understanding-your-circle-of-competence.html
https://www.fs.blog/2013/12/mental-model-circle-of-competence
產品定位決定你可以定價的高低,你想過換句話說嗎?
哈,有趣,產品定位換句話說就可以提高產品定價
以下 quote from 灣區日報
产品定位决定你可以定价的高低
假如你的用户每个月在 Google 上投放广告 $4万,你现在有一款“节省投广告成本”的产品,你每个月可以帮助用户省 $2万,那你定价肯定不能超过 $2万,要不然用户为什么需要你。但你其实可以定超过$2万的
你只需要重新定位你的产品:“帮助增长用户”。如果原来用户每个月投 $4万可以获取200个用户,他们会愿意投 $8万来获取400个用户,那么你就有 $4万的利润空间可以定价了。用户来投广告是为了增长不是为了省钱。
同理,那些“省时”、“提高效率”的产品定位都可以改进,只要抓住用户真正价值所在(比如增长),那你就可以大大提高你产品的定价。
https://wanqu.co/a/6597/2018-06-10-how-repositioning-a-product-allows-you-to-8x-its-price-asmartbear.html?s=email
https://blog.asmartbear.com/more-value-not-save-money.html?utm_source=wanqu.co&utm_campaign=Wanqu+Daily&utm_medium=email
用 Ruby proc 做 success and error handler, like JS Promise
```rb
# A base class for all classes implement calls to API.
class ApiCall
attr_reader :params
def self.call(params)
new(params).call
end
def initialize(params)
@params = params
end
def call
@res = execute
self
end
def on_success
yield @res if @res.success
self
end
def on_error
yield @res unless @res.success
self
end
private
def execute
fail NotImplementedError
end
end
StripeCall.(number: 'valid')
.on_success { |response| puts response.body }
.on_error { |response| puts response.body }
# => ok response
StripeCall.(number: 'invalid')
.on_success { |response| puts response.body }
.on_error { |response| puts response.body }
# => bad response
```
感覺有點借鑑 JS promise 的做法,傳 on_success 和 on_failure 的 proc 進到 method 當作 handler,之前有想過這種做法,剛好在 Ruby weekly 看到有人示範了,的確是減少了 if else 判斷,也省略了 response 這種 hidden context,不過最後為了寫的方便一點用了 method(:handle_success) 就沒有特別喜歡,但還是學習了這個用法,又是 meta programing 流派...
原文: https://railsguides.net/conditional-execution-with-dsl/
另外在 comment 裡看到一個 gem https://github.com/apneadiving/waterfall
也是挺有趣的,真的很有 functional programing 的味道
是不是大家開始慢慢被影響導致越來越多人用 Funtional Ruby 了呢~?最近也常常聽到和看到有人要用 Ruby 實現以及使用 elixir pipe operator XD
標籤:
Design Pattern,
Ruby
2018 Ruby Kaigi 網路上看到的懶人整理
- Matz considering alias yied_self with then
- Rib, an alternative for irb
- Stripe team building Ruby type checker
https://blog.bigbinary.com/2018/05/31/rubykaigi-2018-day-one.html
http.rb replace net::http
作者認為 Net::Http 雖然是 standard lib 之一,但 API 設計很不好,底層實作也有很多覺得不滿的地方,包含 Net::Http 的 Error 封裝方式,讓人不知道怎麼 rescue
有機會可以試用看看
https://github.com/httprb/http
https://twin.github.io/httprb-is-great/
2018年6月6日 星期三
skip document installation when gem install
https://stackoverflow.com/questions/1381725/how-to-make-no-ri-no-rdoc-the-default-for-gem-install
1. 加 ~/.gemrc
```
gem: --no-document
```
done
2018年5月30日 星期三
ruby proc 可以傳default 值since ruby 1.9
所以
scope :latest, ->(size=5) { recent.limit(size) }
這樣是可以的
好廢的一篇文
呵呵
scope :latest, ->(size=5) { recent.limit(size) }
這樣是可以的
好廢的一篇文
呵呵
2018年5月29日 星期二
ssl-cookie-without-secure-flag-set
HTTPOnly flag
https://portswigger.net/kb/issues/00500200_ssl-cookie-without-secure-flag-set
https://stackoverflow.com/questions/3773605/how-can-i-make-cookies-secure-https-only-by-default-in-rails
2018年5月25日 星期五
各種 performance 相關的技巧 - rails related and general
https://www.monterail.com/blog/actionable-tips-to-improve-web-performance-with-rails
2018年5月24日 星期四
github issue 和 pr 帶不同 params 就有不同功能
潮潮的,可以指定 title, labels, template 和 assignees 之類的
https://help.github.com/articles/about-automation-for-issues-and-pull-requests-with-query-parameters/
https://help.github.com/articles/about-automation-for-issues-and-pull-requests-with-query-parameters/
Vue computed v.s. methods
computed 會 cached methods 不會
computed 回傳一個值,methods 不一定
https://cn.vuejs.org/v2/guide/computed.html
Rails 內建的加解密 MessageEncryptor
http://edgeapi.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html
2018年5月23日 星期三
How to Create Postgres Indexes Concurrently in ActiveRecord Migrations
之前的文章提到要避免 deployment downtime 其中一個方式是 create index concurrently
http://everyday1percent.blogspot.com/2018/05/rails-deployment-downtime.html
原來 rails 有內建的 API 做到這件事
```ruby
class AddIndexToBuildMetricIdOnBuilds < ActiveRecord::Migration
disable_ddl_transaction!
def change
add_index :builds, :build_metric_id, :algorithm => :concurrently
end
end
```
好潮。
https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in
https://semaphoreci.com/blog/2017/06/21/faster-rails-indexing-large-database-tables.html
Devise 每次都會同時下 ORDER BY 和 LIMIT 的 sql 去找 users
同事提出的建議:
```
SELECT "users".* FROM "users" WHERE "users"."id" = $? ORDER BY "users"."id" ASC LIMIT $?
id 是 primary key, 理論上不會有重複的資料.
SELECT id, COUNT(id) AS cid FROM users GROUP BY id ORDER BY cid DESC;
id | cid
--------+-----
1 | 1
58291 | 1
(以下省略)
也確認目前的資料中, 的確沒有出現重複的 id.
這個 query 的 ORDER BY 跟 LIMIT 確認不會被 pgsql 的 query plan / optimizer 省略掉, 還是會產生 procedure call 消耗些微的 cpu / memory, 所以還是建議修改這個 query.
```
查了一下主要罪魁禍首是
Devise::Models::Authenticatable.serialize_from_session
這個 method 是每次已登入的 user 訪問頁面時就會 trigger 的,裡面用了 User.to_adapter.get([id]) 的方式來拿資料
```
:014 > User.to_adapter.get([1])
User Load (2.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
```
另外比較不重要但是也會影響的地方就是登入時的驗證,確切來說就是 Devise::Models::DatabaseAuthenticatable.find_for_database_authentication ,裡面用了 to_adapter.find_first
```
:005 > User.to_adapter.find_first(id: 1)
User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
```
可以參考的解法:
* customize serializer: https://github.com/plataformatec/devise/wiki/How-to:-Write-your-own-serializer
* overwrite, ex:
```
class User < ApplicationRecord
def self.serialize_from_session(key, salt)
record = find_by(id: key)
record if record && record.authenticatable_salt == salt
end
end
```
* 想辦法在不必要驗證的地方 skip 上述 find user 的 middelware
bcrypt, devise, and rails secret_key_base
devise 用 secret_key_base 當作產生 token 的依據
devise 用 bcrypt hashify password 然後儲存
rails 的 has_secure_password 也是用 bcrypt 實現
bcrypt() is a hashing algorithm designed by Niels Provos and David Mazières of the OpenBSD Project.
題外話,如果要改 secret_key_base ,那 devise 裡的token, 包含confirmation, reset_password 都會 invalid 需要重新生成。
https://github.com/codahale/bcrypt-ruby
https://coderwall.com/p/sjegjq/use-bcrypt-for-passwords
devise 用 bcrypt hashify password 然後儲存
rails 的 has_secure_password 也是用 bcrypt 實現
bcrypt() is a hashing algorithm designed by Niels Provos and David Mazières of the OpenBSD Project.
題外話,如果要改 secret_key_base ,那 devise 裡的token, 包含confirmation, reset_password 都會 invalid 需要重新生成。
https://github.com/codahale/bcrypt-ruby
https://coderwall.com/p/sjegjq/use-bcrypt-for-passwords
2018年5月22日 星期二
explain https again
CA 憑證方發憑證,所謂的憑證就是包含
1. 公鑰
2. 用 CA 方的私鑰加密過的 Hash
## 具體步驟是:
- client端發起Https request
- Server端返回CA發的憑證
- Client端收到憑證,並驗證憑證是否可信,如果可信(或是使用者選擇相信)則隨機產生一段對稱式加密用的key(稱之為Key S好了),並且用憑證內的公鑰加密Key S,送給Server
- Server收到加密過的Key S,用自己的私鑰解密,並且回給Client一個ready的訊息
- Client開始和Server用Key S做對稱式加密通訊
https://medium.com/@kingapol/3%E5%88%86%E9%90%98%E5%85%A7%E4%BA%86%E8%A7%A3https-595b7d29c16
2018年5月21日 星期一
deploy vue to github page
兩個不錯的方式:
1. 用 push-dir + webpack 做
https://medium.com/@codetheorist/vue-up-your-github-pages-the-right-way-955486220418
2. 用直接寫好的 plugin
https://github.com/KieferSivitz/vue-gh-pages
目前是用後者
vue-cli v3.0.0.beta10 + vue-gh-pages v1.0.1
是很方便但是問題是
1. 必須用 /docs folder 當 gh-pages 的 root
2. 如果沒有綁 domain 的話會讀不到 js, css files,因為會連到 username.github.io/js/xxx.js 而不是 username.github.io/project_name/js/xxx.js,但後者才是正確的連結
3. docs/ folder 會一直搞爛 git 很煩
更新:找到用 vue-gh-pages 但不使用 master docs/ folder 的方法了,把 deploy script 改成下面這樣就行了:
"deploy": "node ./node_modules/vue-gh-pages/index.js -b gh-pages -o ./"
目前是就乾脆買個 domain,但比較 prefer push-dir + webpack 就是了,只是比較麻煩懶得嘗試。
1. 用 push-dir + webpack 做
https://medium.com/@codetheorist/vue-up-your-github-pages-the-right-way-955486220418
2. 用直接寫好的 plugin
https://github.com/KieferSivitz/vue-gh-pages
目前是用後者
vue-cli v3.0.0.beta10 + vue-gh-pages v1.0.1
是很方便但是問題是
1. 必須用 /docs folder 當 gh-pages 的 root
2. 如果沒有綁 domain 的話會讀不到 js, css files,因為會連到 username.github.io/js/xxx.js 而不是 username.github.io/project_name/js/xxx.js,但後者才是正確的連結
3. docs/ folder 會一直搞爛 git 很煩
更新:找到用 vue-gh-pages 但不使用 master docs/ folder 的方法了,把 deploy script 改成下面這樣就行了:
"deploy": "node ./node_modules/vue-gh-pages/index.js -b gh-pages -o ./"
目前是就乾脆買個 domain,但比較 prefer push-dir + webpack 就是了,只是比較麻煩懶得嘗試。
標籤:
Deployment,
JavaScript,
Web
墮天使路西法 Lucifer
剛好在研究七原罪的時候看到傲慢的代表惡魔是 Lucifer,於是想知道是怎樣傲慢。
教會的解釋是 Lucifer 忘了自己天使的身份,把自己看的跟神一樣等級(所以才叛變?)
正常來說 Lucifer 的履歷如下:
* 率兵叛變的天使首腦
* 化身伊甸園的蛇
* Some people says Lucifer == 撒旦
Wiki:
文學裡的路西法
在但丁的《神曲》與約翰·彌爾頓的《失樂園》之中,都是說路西法因為拒絕臣服於聖子基督,率天眾三分之一的天使於天界北境起兵反叛。經過三天的天界劇戰,路西法的叛軍被基督擊潰,在渾沌中墜落了九個晨昏才落到地獄。此後神創造了新天地和人類,路西法為了復仇兼奪取新天地,乃化為蛇潛入伊甸園,引誘夏娃食用了禁忌的知善惡樹之果實,再利用她引誘亞當也犯下了這違抗神靈的罪。於是路西法如願使神的新受造物一同墮落,而且為諸魔神開啟了通往這新世界的大門,自此罪、病、死終於遍布地面。
https://zh.wikipedia.org/wiki/%E8%B7%AF%E8%A5%BF%E6%B3%95
教會的解釋是 Lucifer 忘了自己天使的身份,把自己看的跟神一樣等級(所以才叛變?)
正常來說 Lucifer 的履歷如下:
* 率兵叛變的天使首腦
* 化身伊甸園的蛇
* Some people says Lucifer == 撒旦
Wiki:
文學裡的路西法
在但丁的《神曲》與約翰·彌爾頓的《失樂園》之中,都是說路西法因為拒絕臣服於聖子基督,率天眾三分之一的天使於天界北境起兵反叛。經過三天的天界劇戰,路西法的叛軍被基督擊潰,在渾沌中墜落了九個晨昏才落到地獄。此後神創造了新天地和人類,路西法為了復仇兼奪取新天地,乃化為蛇潛入伊甸園,引誘夏娃食用了禁忌的知善惡樹之果實,再利用她引誘亞當也犯下了這違抗神靈的罪。於是路西法如願使神的新受造物一同墮落,而且為諸魔神開啟了通往這新世界的大門,自此罪、病、死終於遍布地面。
https://zh.wikipedia.org/wiki/%E8%B7%AF%E8%A5%BF%E6%B3%95
一篇講 OO cohesion 和 coupling 簡單易懂的文章
一直想找個好方式跟同事解釋怎麼 decoupling 和 design better code,這篇文章涵蓋了最重要的兩個概念而且用很簡單易懂的 code 解釋了,很不錯。
cohesion: class 裡的 methods 是不是 share 一樣的 context?
coupling: dependency
http://www.rubyguides.com/2018/05/ruby-cohesion-and-coupling/
cohesion: class 裡的 methods 是不是 share 一樣的 context?
coupling: dependency
http://www.rubyguides.com/2018/05/ruby-cohesion-and-coupling/
標籤:
Design Pattern,
OOP,
Ruby
2018年5月20日 星期日
使用 Vagrant 練習 chef-solo deployment
每次都自己調 server 太煩了,用 chef-solo 調一次以後一直套用比較方便,可以用 Vagrant 練習會快一點
先安裝 VirtualBox 和 Vagrant
https://www.virtualbox.org/wiki/Downloads
https://www.vagrantup.com/downloads.html
Ubuntu 16.04 的 Vagrant box (Vagrant box 就是包好的環境的意思)
https://app.vagrantup.com/ubuntu/boxes/xenial64
```
➜ Codes mkdir vagrant-test
➜ Codes cd vagrant-test
➜ vagrant-test vagrant init
```
在 Vagrantfile 裡設定
```
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
end
```
然後開啟 Vagrant
```
vagrant up
```
然後就可以 ssh 進去玩了
vagrant ssh
//關閉VM
$ vagrant halt
//暫停VM
$ vagrant suspend
//恢復暫停的VM
$ vagrant resume
//重啓VM
$ vagrant reload
## Chef solo
首先要 init 一個 chef-solo 的 project
檢查 vagrant 的 ssh-config
```
➜ vagrant-test git:(master) ✗ vagrant ssh-config
Host default
HostName 127.0.0.1
User vagrant
Port 2222
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
PasswordAuthentication no
IdentityFile /Users/wayne/Codes/vagrant-test/.vagrant/machines/default/virtualbox/private_key
IdentitiesOnly yes
LogLevel FATAL
```
PasswordAuthentication 被設為 no 了所以要嘛去把 local 的 ssh key 加到 vagrant 裡要嘛就把 password 設成 yes,預設 password 是 vagrant
我個人是很懶所以就這樣:
```
ssh-copy-id -i ~/.ssh/id_rsa.pub -p 2222 vagrant@localhost
```
但其實還是有一些其他做法可以參考:
https://github.com/puphpet/puphpet/issues/1253
https://blog.vvtitan.com/2015/10/%E4%BD%BF%E7%94%A8vagrant%E7%B7%B4%E7%BF%92%E4%BD%88%E7%BD%B2%E4%BC%BA%E6%9C%8D%E5%99%A8/
https://medium.com/@brianmayrose/using-vagrant-on-ubuntu-16-04-to-create-a-virtual-machine-c767f6e2d876
https://gist.github.com/nicholasklick/af98d5814d5b8ba6d844
先安裝 VirtualBox 和 Vagrant
https://www.virtualbox.org/wiki/Downloads
https://www.vagrantup.com/downloads.html
Ubuntu 16.04 的 Vagrant box (Vagrant box 就是包好的環境的意思)
https://app.vagrantup.com/ubuntu/boxes/xenial64
```
➜ Codes mkdir vagrant-test
➜ Codes cd vagrant-test
➜ vagrant-test vagrant init
```
在 Vagrantfile 裡設定
```
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
end
```
然後開啟 Vagrant
```
vagrant up
```
然後就可以 ssh 進去玩了
vagrant ssh
//關閉VM
$ vagrant halt
//暫停VM
$ vagrant suspend
//恢復暫停的VM
$ vagrant resume
//重啓VM
$ vagrant reload
## Chef solo
首先要 init 一個 chef-solo 的 project
檢查 vagrant 的 ssh-config
```
➜ vagrant-test git:(master) ✗ vagrant ssh-config
Host default
HostName 127.0.0.1
User vagrant
Port 2222
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
PasswordAuthentication no
IdentityFile /Users/wayne/Codes/vagrant-test/.vagrant/machines/default/virtualbox/private_key
IdentitiesOnly yes
LogLevel FATAL
```
PasswordAuthentication 被設為 no 了所以要嘛去把 local 的 ssh key 加到 vagrant 裡要嘛就把 password 設成 yes,預設 password 是 vagrant
我個人是很懶所以就這樣:
```
ssh-copy-id -i ~/.ssh/id_rsa.pub -p 2222 vagrant@localhost
```
但其實還是有一些其他做法可以參考:
https://github.com/puphpet/puphpet/issues/1253
https://blog.vvtitan.com/2015/10/%E4%BD%BF%E7%94%A8vagrant%E7%B7%B4%E7%BF%92%E4%BD%88%E7%BD%B2%E4%BC%BA%E6%9C%8D%E5%99%A8/
https://medium.com/@brianmayrose/using-vagrant-on-ubuntu-16-04-to-create-a-virtual-machine-c767f6e2d876
https://gist.github.com/nicholasklick/af98d5814d5b8ba6d844
標籤:
Developer Tools,
Server
2018年5月19日 星期六
新的做 soft delete 的 gem - discard
https://github.com/jhawthorn/discard
沒有 default 加上 default scope 我覺得是很棒的決定,這個 gem 留給開發者更多的彈性實作 soft delete,我覺得在大多數的 case 下都是很夠用了,相較之下一樣是 soft delete 的 gem paranoia 就太多 magic 了
2018年5月18日 星期五
檢查 gem verison 的方法
在devise 裡看到的
```rb
def self.rails51? # :nodoc:
Rails.gem_version >= Gem::Version.new("5.1.x")
end
def self.activerecord51? # :nodoc:
defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
end
```
2018年5月17日 星期四
Profiling Rails Boot Time
Infos:
https://mildlyinternet.com/code/profiling-rails-boot-time.html
https://tenderlovemaking.com/2011/12/05/profiling-rails-startup-with-dtrace.html
http://guides.rubyonrails.org/initialization.html#config-environment-rb
http://brewhouse.io/blog/2015/04/27/fixing-a-slow-rails-development-server.html
Gems:
https://github.com/MiniProfiler/rack-mini-profiler
https://github.com/SamSaffron/flamegraph
https://github.com/nevir/Bumbler
https://github.com/ruby-prof/ruby-prof
https://github.com/tmm1/perftools.rb
2018年5月16日 星期三
Rails deployment 如何避免 downtime?
有哪些造成 downtime 的可能性?
1. Code 有問題
2. Migration 跑完後 Web Server 還沒 restart
3. Deploy 完後需要重啟 Server
4. Request 沒有被 queue 起來
可能的解決方式:
## 1. Code 有問題
1. 勤勞寫 Test
2. Manually test
3. Code review
4. Staging -> Production
## 2. Migration 跑完後 Web Server 還沒 restart
有篇文章寫得很不錯, cover 了一些之前沒想到的技巧:
https://blog.codeship.com/rails-migrations-zero-downtime/
1. read only method
2. 先讓 AR ignore columns 後再刪除
```
class User
def self.columns
super.reject { |c| c.name == "notes" }
end
end
```
3. renaming column 時可以用 `super || attributes["new_name"]`
```
def first_name
super || attributes["fname"]
end
```
4. 加 index 時有時會卡住 migration 造成問題,可以考慮 create it concurrently
## 3. Deploy 完後需要重啟 Server
1. 可以先把新的 instances run 起來,然後把 request 切換到新的 instances 後再 shut down 舊的
2. 靠 Load balancer 把 request queue 起來等重啟完再執行
## 4. Request 沒有被 queue 起來
之前就遇過因為 ActionCable and AWS ELB 的各種坑
https://everyday1percent.blogspot.com/2018/04/actioncable-and-aws-elb.html
所以也是有這個可能性,解決方式是要讓 load balancer 確保有 queue request 等 server 恢復時才執行
剩下有想到再補充
2018年5月10日 星期四
cc 系列 license,看到一個還不錯的,以後文章可以用
https://creativecommons.org/licenses/by-nc/2.5/
可以允許分享也可以改動
分享要 credit 原作者,如果有改動也要指出改動哪裡。
Query jsonb data type in postgres with Rails
https://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-rails
2018年5月9日 星期三
rspec test protocal (interface) - part 1 - define what's interface and find it
通常我們會希望繼承於 Base class 底下的 class 會有相同的 interface,for example, 每個 service 都要有一個 #call 的 instance method,應該要有個很簡單的方法來對這些 services 測試是否有 implement 這個 protocal,理想情況如下:
```rb
class BaseService
def call
raise "NotImplemented"
end
end
class FooService < BaseService
def call
puts "called"
end
end
class BarService < BaseService
end
module Callable
def call
puts "called from module"
end
end
class IncludeService
include Callable
end
class PrivateService
private
def call
puts "private call"
end
end
RSpec.describe FooService do
describe "interfaces" do
specify do
expect(FooService).to has_interface :call
expect(FooService).to has_implemented_interface :call
end
end
end
```
it_has_interface => 有 respond_to 這個 method 就行 (個人覺得沒什麼用...)
it_has_implemented_interface => 有自己 implement (排除母 class) 或是 include method 進來
it_has_implemented_interface 希望達到的效果是:
FooService 和 IncludeService 會 passed
BarService 和 PrivateService 會 failed
來看看要怎麼樣做 :D
已知使用 respond_to? 是不行的,因為母 class 定義了 call,所以
```ruby
2.4.4 :013 > f = FooService.new
=> #<FooService:0x00007fed06152098>
2.4.4 :014 > b = BarService.new
=> #<BarService:0x00007fed06148700>
2.4.4 :015 > f.respond_to?(:call)
=> true
2.4.4 :016 > b.respond_to?(:call)
=> true
```
用 method_defined? 當然也不行,因為都是繼承於 BaseService 所以都有 defined,但來試來試一下
```ruby
2.4.4 :017 > FooService.method_defined?(:call)
=> true
2.4.4 :018 > BarService.method_defined?(:call)
=> true
```
我們要找的是一個方法檢測子 class 是否有定義這個 method,不管是不是用 mixin 的方式加進去的,有幾個 methods 可以用
* public_methods
* instance_methods
* public_instance_methods
instance_methods default 還會包含 protected 的 methods,不如我們先來試試看 public_instance_methods
```ruby
2.4.4 :030 > FooService.public_instance_methods.include?(:call)
=> true
2.4.4 :031 > BarService.public_instance_methods.include?(:call)
=> true
2.4.4 :032 > IncludeService.public_instance_methods.include?(:call)
=> true
2.4.4 :033 > PrivateService.public_instance_methods.include?(:call)
=> false
```
看起來還是不行,讓我們換個方法,用 public_methods(false)
```ruby
2.4.4 :068 > FooService.new.public_methods(false).include?(:call)
=> true
2.4.4 :069 > BarService.new.public_methods(false).include?(:call)
=> false
2.4.4 :070 > IncludeService.new.public_methods(false).include?(:call)
=> false
2.4.4 :071 > PrivateService.new.public_methods(false).include?(:call)
=> false
```
結果 public_methods(false) 沒辦法讓 include 進來的 method 包含在裡面,為什麼呢?可以看一下 IncludeService 的 ancessor
```
2.4.4 :072 > IncludeService.ancestors
=> [IncludeService, Callable, Object, Kernel, BasicObject]
```
可以想像成 include 其實就是在 parent class 以及 child class 中間插隊,所以也是算在 ancestors 裡面。
所以看起來沒有任何 built-in 的 method 可以直接做到這個功能,那不如我們就自幹一個看看吧?XD
由於要 support module 裡的 methods 所以想到最原始的方法就是去找 ancestors 裡在 superclass 之下的每個 instance_methods, code example:
```ruby
class Utils
class << self
def get_public_instance_methods_without_parent_class(klass)
countable_ancestors = klass.ancestors[0...klass.ancestors.index(klass.superclass)]
countable_ancestors.flat_map do |ancestor_class|
ancestor_class.public_instance_methods(false)
end.uniq
end
end
end
```
這個版本目前只能對instance method 做檢測,不過對我們來說已經足夠了,以後再想辦法擴充,接下來就是來研究自定義 rspec DSL 的部分。
2018年5月6日 星期日
Deploy rails to Ubuntu 16.04 with puma and nginx and capistrano
Install Nginx at Ubuntu
https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04
設定Nginx
/etc/nginx/site-availables/myconfig
```
upstream puma {
server unix:///home/apps/arbitrage/shared/tmp/sockets/puma.sock;
}
server {
# listen 80 default_server;
# listen [::]:80 default_server;
server_name www.yourdomain.com;
root /home/apps/arbitrage/current/public;
access_log /home/apps/arbitrage/shared/log/nginx.access.log;
error_log /home/apps/arbitrage/shared/log/nginx.error.log info;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @puma;
location @puma {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://puma;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
```
cd /etc/nginx/site-enabled
sudo ln -nfs /home/username/apps/appname/current/config/nginx.conf /etc/nginx/sites-enabled/appname
sudo service nginx restart
也要記得裝 redis http://everyday1percent.blogspot.com/2017/12/deploy-rails-production-redis.html
https://coderwall.com/p/ttrhow/deploying-rails-app-using-nginx-puma-and-capistrano-3
Mac OS 用 Geth 跑 ETH ethereum node 全節點
https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac
brew tap ethereum/ethereum
brew install ethereum
然後跑
geth --syncmode "fast" --cache=1024
就會開始 sync 主鏈的全節點,節點會存在
database=/Users/wayne/Library/Ethereum/geth/chaindata
路徑裡,在geth 啟動時就可以看到 database 的路徑
fast 參數是 syncmode,詳細介紹: https://everyday1percent.blogspot.com/2018/05/eth-syncmode.html
如果要 sync testnet 可以這樣:
Sync testnet (Ropsten)
```
geth --testnet --syncmode "fast" --datadir /usr/local/var/ethereum-testnet-data console 2>> /usr/local/var/log/geth.testnet.log
```
開啟 API
```
geth -- testnet -- syncmode "fast" -- rpc -- rpcapi db,eth,net,web3,personal,admin -- cache=1024 -- rpcport 8545
```
開啟 console
```
geth --testnet --syncmode "fast" console 2>> /usr/local/var/log/geth.testnet.log
```
```
echo "Geth at work!"
screen -dmS geth geth --testnet --syncmode "fast" --rpc --rpcapi db,eth,net,web3,personal --cache=1024 --rpcport 8545 --rpcaddr 0.0.0.0 --rpccorsdomain "*"
```
然後
```
geth attach http://localhost:8545
```
就可以進 console
https://ethereum.stackexchange.com/questions/392/how-can-i-get-a-geth-node-to-download-the-blockchain-quickly/4210#4210
https://medium.com/@jacksonngtech/syncing-geth-to-the-ethereum-blockchain-9571666f3cfc
標籤:
BlockChain,
Ethereum
eth syncmode
還不是很確定,先翻譯貼過來
Full - 拿到所有資料
Fast - 差不多的意思,但不執行transaction
Light - 只拿到現在的狀態,但不能驗證
https://ethereum.stackexchange.com/questions/11297/what-is-geths-light-sync-and-why-is-it-so-fast
更好的解釋:
主要是fast 跟 full 的差別
full 可以驗證每個 state,但 fast 因為只有 process 最近的 transactions,所以不能驗證舊的 state
https://ethereum.stackexchange.com/questions/1161/what-is-geths-fast-sync-and-why-is-it-faster
https://github.com/ethereum/go-ethereum/pull/1889
標籤:
BlockChain,
Ethereum
2018年5月1日 星期二
PG::ConnectionBad: FATAL: Peer authentication failed for user XXX
sudo vim /etc/postgresql/9.5/main/pg_hba.conf
加一行:
```
local all your_user_name trust
```
編輯
```
# DO NOT DISABLE!
# If you change this first entry you will need to make sure that the
# database superuser can access the database using some other method.
# Noninteractive access to all databases is required during automatic
# maintenance (custom daily cronjobs, replication, and similar tasks).
#
# Database administrative login by Unix domain socket
local all postgres peer
# TYPE DATABASE USER ADDRESS METHOD
local all your_user_name trust
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
#local replication postgres peer
#host replication postgres 127.0.0.1/32 md5
```
然後重啟 server
sudo service postgresql restart
https://www.postgresql.org/docs/9.3/static/auth-methods.html
rails 5.2 credentials capistrano deploy 的各種坑
Deploy 一直遇到 Error:
```
ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `rails credentials:edit`
```
試了好多方式,理論上設個環境變數就行,但我一直失敗,中途還嘗試把credentials disable 也是沒用,後來是直接上傳 master.key 解決
用這行指令上傳
```
scp config/master.key apps@DOMAIN:PROJECT_NAME/shared/config
```
然後 capistrano link 過去
```
set :linked_files, fetch(:linked_files, []).push("config/database.yml", "config/master.key"
```
http://guides.rubyonrails.org/security.html#custom-credentials
https://keithpblog.org/post/encrypted-secrets/
https://github.com/rails/rails/issues/32413
2018年4月26日 星期四
同時定義 class method 和 instance method 的潮潮寫法
You can delegate method to your `class` since your class is also a object in Ruby, fanny~
```ruby
class Foo
def self.bar
return "Hello"
end
delegate :bar, to: :class
end
```ruby
class Foo
def self.bar
return "Hello"
end
delegate :bar, to: :class
end
```
```ruby
Foo.bar
# => "Hello"
Foo.new.bar
# => "Hello"
```
Awesome.
為子 class 寫 class method 在 class 裡調用
老生常談的小技巧了,剛好看到 kickstarer 的一段 code,貼過來回溫一下
```ruby
# Define attributes to be serialize in the `data` column.
# It generates setters and getters for those.
#
# Example:
#
# class MyEvent < Events::BaseEvent
# belongs_to :post
#
# data_attributes :title, :description
# end
#
# MyEvent.create!(post: post, title: "Hello", description: "", metadata: {})
def self.data_attributes(*attrs)
@data_attributes ||= []
attrs.map(&:to_s).each do |attr|
@data_attributes << attr unless @data_attributes.include?(attr)
define_method attr do
self.data ||= {}
self.data[attr]
end
define_method "#{attr}=" do |arg|
self.data ||= {}
self.data[attr] = arg
end
end
@data_attributes
end
```
https://gist.github.com/pcreux/d094affd957a336af4f59b85f6ec0e6d
Rails 不常用但可以變得很實用的 callback
## after_initialize && after_find
```ruby
class User < ApplicationRecord
after_initialize do |user|
puts "You have initialized an object!"
end
after_find do |user|
puts "You have found an object!"
end
end
>> User.new
You have initialized an object!
=> #<User id: nil>
>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>
```
主要是 after_initialize 的部分,用來設定初始值挺方便的
http://guides.rubyonrails.org/active_record_callbacks.html#after-initialize-and-after-find
little funny learn from ruby private
I usually make my private methods like this:
```
class Foo
private
def private_method
end
end
```
However, there is inline-style private method like this:
```
class Foo
private def private_method
end
def puiblic_method_yo!
end
end
```
Additionally, attr_reader is also available for inline-style private method:
```
class Foo
private attr_reader :bar
end
```
So fancy, isn't it? lol
```
class Foo
private
def private_method
end
end
```
However, there is inline-style private method like this:
```
class Foo
private def private_method
end
def puiblic_method_yo!
end
end
```
Additionally, attr_reader is also available for inline-style private method:
```
class Foo
private attr_reader :bar
end
```
So fancy, isn't it? lol
訂閱:
文章 (Atom)




