21:33
By jx
とりあえず、こっちに写しておく。
http://anond.hatelabo.jp/20101207002426からの転載
Webサービスを公開するまでの軌跡を語るよ
自分でWEBサービスを作りたいと思っている人へ ( http://anond.hatelabo.jp/20101203150748 ) を読んで、初心者じゃなくてある程度の経験者が作ったらこうなるんだよってことで書いています。でも正直4ヶ月でそこまでできるようになるなんておそらく相当頭がいいんじゃないかなと思うんです。いや、本当に凄いと思う。
まず作ったサービスはグルーポンチケットのまとめサイト。 グルーポンナビ( http://gp-navi.net/ )まず自分のスペックだけど、昔から趣味でプログラムやったり仕事でプログラムしたりしてました。Web系ばっかりやってるいちおうこういうのを作るのには慣れてる方です。
お金使いたくないからHerokuを使う
それで私ですが、まず最初に http://anond.hatelabo.jp/20101203150748 の人と同じで全然お金を使いたくなかったです。だからいろんなホスティング会社を探したりしたんですが、VPSを借りちゃうと大金がかかるんですよね。そこで、いわゆる流行のクラウドであるHeroku( http://heroku.com/ )を使う事にしました。
このHerokuは無料で使えるから良いんですけど、バッチ処理を行うには有料のサーバを契約しなくちゃいけないんです。しかもWebの一回のリクエストは30秒までに押さえなくちゃいけないっていう制限があったりで、正直無料で使うには厳しい。
でもやっぱり無料っていう響きに吸い寄せられてHerokuを使うと決めてアプリケーションを書き始めました。Railsで書いて賞味2週間くらいちまちまと進めてアプリを完成させました。だいたい一日2時間くらいかな。Railsにはそこまで詳しくはなかったけど、Webで検索すれば結構情報がヒットするからそこまでの苦労は無かったです。それよりも制限のあるHerokuでどうやって実現するかというのが、結構問題でした。
でもやっぱり無料じゃ厳しい
それでしばらく運用してました。このときは全然宣伝しませんでしたから、ほとんどページビューはあがらなかったです。宣伝大事。これほんと。それでHerokuで作った仕組みなんですが、無料でやりたかったので、Workersを契約しないでアプリをつくってました。だからWebのリクエストをトリガーとして普通にクロールさせるよう作ったんです。でも、クロールするサイトを徐々に増やしていくと30秒以内にリクエストが終了しなくなってきてさぁ問題。どうしようかと考えたあげく、レンタルサーバもいいなっておもったけど、結局自宅サーバをたてることにしました。たぶん、これが一番コストパフォーマンスが良い。でも気をつけなくちゃ行けないのが、サーバ自体の管理を自分でやらなくちゃいけないから結構めんどくさいんですよね。結局自宅サーバかよって感じです。
サーバの発注とかシステムの構成
サーバをNTT-X Storeで発注して発注して、16,800円。かなり安いです。このサーバの詳細は http://wiki.nothing.sh/page/NEC%20Express5800%A1%BFGT110b に書かれています。なんでこのマシンにしたのかというと、VMWareのESXiで仮想化をしたくて、それに対応している安いサーバがこれだったというわけ。ちなみに、これをすると管理が楽になる。例えば、マシンのバックアップが取りたいなと思ってもすぐできるんですね。だから仮想化は凄く良い。
でも、素のGT110bを使うとメモリも少ないしCPUも弱いしハードディスクも少ないので、これはネットで買い足しました。あ、あとデスクトップ用のモニタが無かったのでそれも中古で買いました。それが次のような感じ。
Express/GT110b ¥16,800
Core i5 760 BOX ¥16,898
Samsung3.5インチHDD(SerialATA)/容量:1TB HD103SJ ¥4,980
TS256MLK72V3U [2GB DDR3 1333 ECC Long-DIMM 永久保証] ¥13,398
中古の液晶モニタ 15inch ¥3,980
計: ¥56,056
ずいぶんと安い買い物だったと思います。もちろん、マシンの上で動かしているOSはLinuxなので、ライセンス料もかからないしとてもリーズナブルです。
ここまできたら後はアプリを作り直すだけでした。Herokuで作ってたときにはHerokuの制限を考えながらアプリを作ってたので作りづらかったですが、自宅サーバを使うとそういった制限はなくなるのですごく作りやすい。お金を払うだけの価値はあると思います。やっぱりお金をかけるべきところはかけないとダメですね。
携帯対応とか
それから結構このアプリケーションは携帯ユーザの女の子がよく使ってくれます。結構教えると毎日見てくれるんですよ。やっぱりお買い物と言えば女の子ですね。だから、携帯対応しました。携帯はいままでやった事は無かったんだけど、Railsのプラグインであるjpmobile( https://github.com/darashi/jpmobile)を使ったところすごく簡単に対応する事ができた。凄いですね。id:darashiさんに感謝です。
iPhoneも対応していますが、こちらもあまり詳しくなかったので最初はjQuery mobileを使って構築しました。でもちょっと重かったのでjQuery mobileはやめて手組しています。そもそも一ページしか無いのでそんない難しい事は無いですね。
感想
最後に一番強調したい事を。。。
ウェブサービス公開するのはそこまで難しくないんですが、それを流行らせるのはかなり難しいですね。面白いサービスとかであれば結構色んなところがとりあげてくれたりするんですけど、後発のサービスになるとなかなか。。。開発者の方は作る事よりもどうやってみんなに知ってもらうかを考えるのがすごく大変な事だと思います。お金かけて広告うてれば楽なんですけどね。
とにかく作ったら公開。やる事は各方面への宣伝です。今のところやってるのはTwitterへの投稿と、ここへの投稿ですかね。これからいろいろ試そうとしているところ。このサービスがある程度知名度が上がってきたらまたそのとき軌跡を書きたいと思います。
0:34
最近プログラミングコンテストチャレンジブックを読もうとしています。
そこで、c++を勉強してみようかと考えているのですが、学習効率の向上のため、vimで高速に開発できないものかといろいろ調べてみたところ、quickrun.vimというものが見つかりました。
quickrun.vimの種類
quickrun.vimには2種類あるみたいでujihisaさんが作った
quickrun.vimとthincaさんが作った
vim-quickrunがあるみたい。で、ujihisaさんの昨日はthincaさんのvim-quickrunに取り込まれてるらしいので、
vim-quickrunを使えば良いみたいです。
インストール
vim-quickrunはShougoさんが作った
vimshellを必要としているのでそちらも必要になります。また、Vimは7.2以上が必要条件のようです。
そして、これらをpluginにコピーしてやればOK。
git clone http://github.com/Shougo/vimshell.git
git clone http://github.com/thinca/vim-quickrun.git
pushd .
cd vimshell
cp -R * ~/.vim/
popd
cd vim-quickrun
cp -R * ~/.vim/
これでうまく動きます。かなり快適に勉強できますよ!!
vim-quickrunオススメ!
0:53
今までSubversionを利用していたのですが、Herokuを使う事もあり普段使うバージョン管理ツールもgitにしようとしています。そこで、自分が自由に使えるレポジトリを創りたかったのですが、githubだと有料だし、せっかくDreamhostを借りているんだからここでレポジトリを作成したいと思います。なるべく簡単にいきます。手間をかけたくないですからね。
レポジトリの作成
まずプロジェクトを格納するディレクトリを作成します。以下のように作成しておけばレポジトリを追加したくなったら~/git/以下にディレクトリをどんどん掘って行けば良いですね。
[DREAMHOST]$ mkdir -p ~/git/SomeProject.git
今度は作成したディレクトリをレポジトリにします。
[DREAMHOST]$ cd ~/git/SomeProject.git
[dreamhost]$ git --bare init
今度はローカルホストでレポジトリをcloneします。
[localhost]$ git clone ssh://USERNAME@DREAMHOST/home/USERNAME/git/SomeProject.git
さらに、このレポジトリに名前を付けておきましょう。
[localhost]$ git remote add NAME ssh://USERNAME@DREAMHOST/home/USERNAME/git/SomeProject.git
ここまですれば簡単にgitを使う事ができます。
ローカルでの変更後
[localhost]$ git add -A
[localhost]$ git commit -m 'some message'
[localhost]$ git push NAME master
23:42
By jx
グルーポンは各サイトでクーポンを販売しています。しかし、どのサイトも別々に作られており、 自分の欲しいクーポンを探すのが困難です。そこで、今各グルーポン系のサイトが どのようなクーポンを発売しているのかを一覧にしてわかりやすくまとめた
サイトを作成しました。
グルーポンナビを閲覧する事によってあなたの欲しいクーポンがすぐに見つけることができます。
ちょっと技術的な事
この
グルーポンナビは
Herokuで動いています。どこで動作させようか考えたんですが、サーバの管理はしたくないし、お金は使いたくないしということで、1Dynoまで無料の
Herokuで動かす事にしました。やっぱり、こういったPaaSはサーバの事をほとんど考えなくていいので楽で良いですね。
ぜひ
グルーポンナビをよろしく。
21:53
Macで音楽を聴いてるとき、モニタがつきっぱなしで電気がもったいないですよね。そんなときにはすぐさまモニタをスリープさせましょう。
その方法は以下の通り。Enjoy your mac!
CtrlShift⏏
18:47
ネットで探してもBashで実行ファイルのディレクトリを取得する方法がなかなか見つからなかった。
やっと見つけたのが以下のページ
shでスクリプトの実行ディレクトリを取得する - 進・日進月歩
echo $(cd $(dirname $0);pwd)
22:32
HerokuはRailsのPaaS環境です。ここで作っておけば、簡単にスケールさせる事ができるのかな?いまいちわかってませんが、容量を5Mしか使わなければ、ただで使用する事ができます。だから、ちょっとしたものを公開するにはもってこいの環境かな。
早速プロジェクトを作成しましょう。まずはサインアップをします。トップページにサインアップボタンがあります。
サインアップが済むと、アプリケーションの導入のページが表示されます。それに従います。
sudo gem install heroku
これで、必要なgemがそろいます。次に、既に作成してあるRailsアプリに移動し、gitにコミット
cd myapp
git init && git add . && git commit -m "first commit"
次に、Herokuアプリケーションを作成します。
heroku create
Created http://sharp-autumn-42.com/ | git@heroku.com:sharp-autumn-42.git
Git remote heroku added
しかし、ここでSSHのエラーが出る場合があります。こんな感じ
No ssh public key found in /Users/hoge/.ssh/id_[rd]sa.pub. You may want to specify the full path to the keyfile.
ですので、sshのキーを作成しましょう。
ssh-keygen
是を実行すればsshのエラーが出なくなり、heroku createコマンドは成功します。
15:26
自分が作ってるサイトのページHome - CapoeiraでIEにて文字化けが発生すると言われていました。ちゃんと、HTMLのmetaタグにContentTypeを指定していたにも関わらずです。そこで、調べてみると、HTML Document Representationに載っていました。優先される順に書くと、
- An HTTP "charset" parameter in a "Content-Type" field.
- A META declaration with "http-equiv" set to "Content-Type" and a value set for "charset".
- The charset attribute set on an element that designates an external resource.
となっています。日本語訳から持ってくると、
- HTTPヘッダのContent-Typeフィールドの、charsetパラメータ。
- META要素で、http-equiv属性値がContent-Typeかつvalue属性の値にcharset情報があるもの。
- 外部リソースを指している要素に設定されているcharset属性値。
3番目は何をさしているのはよくわかりませんが、少なくとも1番目と2番目は逆であるべきだと思うのですが、なんでこのような順序になっているのでしょうか?管理者がcharset=euc-jpで設定していても、各HTMLの作成者がUTF-8で書いていれば、そちらを優先すべきだと思うのですが、なにか理由があるのでしょうか。ん〜不思議
1:50
例えば次のようなロールバックを期待するソースコードを書いても実際にはコミットされてしまう。
どうしてもスレッドを使いたかったら、スレッドの外でModelを作成するのがいいんじゃないかと思う。
begin
ActiveRecord::Base::transaction() do
h = Thread.start do
Bookmark.create
throw 'hoge'
end
h.join
end
rescue
p $!
end
p Bookmark.find(:all).size
1:03
おおかた、世間に出回ってる方法でOK。
MacOSX LeopardにRailsをめちゃ綺麗にいれる簡単な方法 - デキルプログラマーになるのだより引用
(1)OSを最新の状態に保つ
ソフトウェアアップデートによってOSを最新版にすること。
(2)ADCから最新版のXcodeをインストール
ほぼ1G近く、ダウンロードに30分を要します。
http://developer.apple.com/jp/
(3)MacPortsをインストール
普通にパッケージ形式で落ちているのでインストールして下さい。
(4)MacPortsをUpdateする
http://www.macports.org/
ここからターミナルを立ち上げます。
sudo port -d selfupdate
sudo port sync
(5)portでMySQLとMySQLデーモンをインストール
sudo port install mysql5 sudo port install mysql5-server
(6)my.cnf(設定ファイル)を作成
sudo cp /opt/local/share/mysql5/mysql/my-small.cnf /etc/my.cnf
(7)my.cnfの設定(編集はvimなりemacsでも使って下さい)
・ソケットの場所変更
socket = /opt/local/var/run/mysql5/mysqld.sock
となっている所全てを
socket = /tmp/mysql.sock
に変更する。
・文字コードを設定 [mysqld] の下に
default-character-set=utf8
skip-character-set-client-handshake
を追記する
(8)MySQLの初期化
sudo -u mysql mysql_install_db5
(9)MySQL自動起動の設定
sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plist
(10)Rubyをportでインストール
sudo port install ruby
(11)Ruby Gemsをportでインストール
sudo port install rb-rubygems
(12)GemsからRailsをインストール
sudo gem install rails
(13)port でrubyのMySQLアダプタをportから入れる
sudo port install rb-mysql
(14)Terminalの設定を変更(デフォルトシェルがbashの場合)
~/.profileに以下の一文を追加
alias mysql="mysql5"
(15)おもむろに再起動
方法はおまかせ(sudo shutdown -r now ・ sudo reboot ・ りんごマーク>再起動)
(16)MySQL接続の確認
mysql -u root
で接続できたら無事完了。後はrailsで適当なプロジェクトを作って実行して下さい。(mysql用dbオプションを忘れずに!)
だいたいこれでOKなんだけど、追加でmysqlのgemをインストールしなければいけない。このときちょっと引数が必要。
sudo env ARCHFLAGS="-arch x86_64" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
あとは、下のように、railsのenvironment.rbを変更すればアクセスできます。
# SQLite version 3.x
# gem install sqlite3-ruby (not necessary on OS X Leopard)
development:
adapter: mysql
database: hoge_development
username: user
password: pass
socket: /opt/local/var/run/mysql5/mysqld.sock
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
adapter: sqlite3
database: db/test.sqlite3
pool: 5
timeout: 5000
production:
adapter: sqlite3
database: db/production.sqlite3
pool: 5
timeout: 5000
21:29
以前作成したHTML5で笑い男をこのブログのヘッダー部に表示するようにしてみた。結構お気に入りですこれ。ほとんどのプラグインがjQueryをもとに作られてるから使うときもあんまり気にしなくていいから好きです。
でも、SIerとかで使うときには、YAHOO UIとかの方が良いと思う。やっぱりプラグインは野良だから、誰がメンテナンスしてるかもわからないし、作る人がわかってるYAHOO UIがおすすめですね。といいつつ、jQueryの方が最近は好きなんですが、、、、
20:57
Slim3で開発しています。
JSPで次のような事をしたところ、例外が発生してしまいました。
<%
List<HotEntry> hotEntryList = (List<HotEntry>) request.getAttribute("hotEntryList");
for (HotEntry entry : hotEntryList) {
%>
<%= entry.getUrl() %>
<%
}
%>
エラーは以下の通り。
java.lang.ClassCastException: net.jirox.acom.model.HotEntry cannot be cast to net.jirox.acom.model.HotEntry
at org.apache.jsp.index_jsp._jspService(index_jsp.java:57)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:94)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:324)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:292)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:236)
at com.google.appengine.tools.development.PrivilegedJspServlet.access$101(PrivilegedJspServlet.java:23)
at com.google.appengine.tools.development.PrivilegedJspServlet$2.run(PrivilegedJspServlet.java:59)
at java.security.AccessController.doPrivileged(Native Method)
at com.google.appengine.tools.development.PrivilegedJspServlet.service(PrivilegedJspServlet.java:57)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
at org.slim3.controller.FrontController.doFilter(FrontController.java:288)
at org.slim3.controller.FrontController.doFilter(FrontController.java:247)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:70)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:268)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:126)
at org.slim3.controller.HotRequestDispatcherWrapper.forward(HotRequestDispatcherWrapper.java:67)
at org.slim3.controller.FrontController.doForward(FrontController.java:696)
at org.slim3.controller.FrontController.doForward(FrontController.java:667)
at org.slim3.controller.FrontController.handleNavigation(FrontController.java:587)
at org.slim3.controller.FrontController.processController(FrontController.java:544)
at org.slim3.controller.FrontController.doFilter(FrontController.java:324)
at org.slim3.controller.FrontController.doFilter(FrontController.java:285)
at org.slim3.controller.FrontController.doFilter(FrontController.java:247)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.slim3.datastore.DatastoreFilter.doFilter(DatastoreFilter.java:54)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.slim3.controller.HotReloadingFilter.doHotReloading(HotReloadingFilter.java:223)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:187)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at com.google.appengine.api.blobstore.dev.ServeBlobFilter.doFilter(ServeBlobFilter.java:51)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:70)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:352)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at org.mortbay.jetty.Server.handle(Server.java:313)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:830)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:514)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
なんだろう。クラスローダが違うのかな?
List hotEntryList = (List) request.getAttribute("hotEntryList");
System.out.println(HotEntry.class.getClassLoader());
System.out.println(hotEntryList.getClass().getClassLoader());
上記のコードを実行してみた。
com.google.appengine.tools.development.IsolatedAppClassLoader@680e2291
null
ん〜?null?こういうコードってこういう風になるんだっけ?
どうしたら良いんだろう。
17:58
By jx
GAEのURLFetchって5秒くらいでタイムアウトしちゃうのね。リクエストの30秒あるんだから、それくらいまでは頑張ってほしいなぁ。ていうかどうしよう。Task Queueを使用すれば、回避できるようになるんだろうか。やっぱりだめなんだろうか。
作ろうとしてるものはもうGAEじゃ作れないのかな。あぁあぁああ
追記:2010/02/19
そんな事ありませんでした。setConnectTimeoutに書いてあるように10秒までは延ばす事ができるようです。
16:48
GAEで開発しているとどんなデータがデータストアに格納されているかがわかりにくいですよね。
そんなときに便利なのが管理コンソール。デフォルトで付属しています。
http://localhost:8888/_ah/admin/datastore
実はちょっと前まで知りませんでした。これで少しは開発が楽になるかな。
16:36
Slim3を触っています。今、Entityあたりを勉強しているのですが、ずいぶんとJDOと違いますね。まぁもちろん、制約とは同じなんですが、同じEntity Groupにしようと思ってプロパティに
List hoge;
とすると、
[SLIM3GEN1005] Specify @Attribute(lob = true) or @Attribute(persistent = false).
と言われてしまう。
もう少し勉強が必要そうですね。
1:42
Google App Engineでアプリでもこさえてみようかと考えています。
GAEといえばSlim3だろうということで、とりあえずはじめてみます。
と思ってチュートリアルを始めたらいきなり躓いた。躓いたのは
Creating a controller and a test (Slim3)
gen-controller taskを実行しようとすると、下のようなエラーが表示されてしまう。
java.lang.UnsatisfiedLinkError: Cannot load 64-bit SWT libraries on 32-bit JVM
何故だろうと調べていたら自分の環境が64bitのMac環境だったためにおこった模様。
ちなみに、Snow Leopardです。
だから、Eclipseの64bit環境用のものを使用すれば問題ありません。
Eclipse Project DownloadsからたどってMac OSXの64bit用Eclipseを落としてきてください。これで解決です。
やっと、チュートリアルに入れる。
2:54
すごい久しぶりの更新です。
今日は遅く起きたので、寝れなくてふと思いついたのでHTML5で笑い男を作ってみました。
そのまま使うにはjQueryが必要です。でも、jQueryの機能なんにも使ってないんで、簡単に切り出せます。
なお、笑い男のSVGは以下のページから拝借しました。
http://d.hatena.ne.jp/fls/20061023/p1
最近のブラウザなら動きます。IEはSVGをサポートしてないんでダメですけどね。。。。
一応2010/2/8時点の最新の以下のブラウザで試して動く事を確認しています。
ということで、これです。
笑い男.js