Rubyの実行結果が欲しいけど、サーバー用意したりAPI用意したりとかまではしたくない。。
というきっかけもあって、ずっと気になってたWasmを使ってみようかなということが最近ありました。
私と同じくWasmを触ったことないような人にとって、この記事で最初の一歩くらいの、入門の手助けになればいいなと思っています。
自分がWasmを触る中で、こんなことできるんだなというの体感しながら、(似たようなものですが)3つくらい作ってみたので、それを用いながら入門していけたらと思います。
作ったやつ
バリデーションをJSじゃなくてRubyでやるやつ
正規表現をRubyでやるやつ
Rubyのコードをブラウザで実行できるやつ
コードはぜんぶここにありますので、みたい方はどうぞです。
ざっくり使ってるテンプレート
<!DOCTYPE html>
<html lang="ja">
<head>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Wasmを使用するためのスクリプトを読み込み -->
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
# ここにRubyのコードを記述していく
require 'js'
puts "Hellow Wasam!"
def document
@document ||= JS.global[:document]
end
</script>
</head>
<body>
</body>
</html>
ぱっと見もそれほど難しくないコードと思います。
<script type="text/ruby">
# ここにRubyのコードを記述していく
require 'js'
puts "Hellow Wasam!"
def document
@document ||= JS.global[:document]
end
</script>
上記の部分にRubyのコードを書いていく感じですね。するとRubyがブラウザで実行できます。
putsの結果はConsoleに出力されます。
require 'js'
をすることで、Rubyのコードから、JavaScriptのグローバルオブジェクトなどにアクセスできます。
バリデーションをJSじゃなくてRubyでやる
先ほどのテンプレートを用いながら、「バリデーションをJSじゃなくてRubyでやるやつ」のコードを見ていきます。
全体はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Validate</title>
<meta name="description" content="Rubyを使用してデータの検証を行うサンプルページです。">
<meta charset="utf-8"/>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Wasmを使用するためのスクリプトを読み込みます。 -->
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
# frozen_string_literal: true
require 'js'
class Validator
def initialize(document)
@document = document
@phone_input = document.getElementById('phone')
@email_input = document.getElementById('email')
@password_input = document.getElementById('password')
setup_event_listeners
end
def validate_phone(text)
cond = text.match?(/\A0\d{10}\Z/)
err = @document.getElementById('phone_error')
err[:innerText] = cond ? '' : '電話番号が不正です'
end
def validate_email(text)
email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
cond = text.match?(email_regex)
err = @document.getElementById('email_error')
err[:innerText] = cond ? '' : 'Emailが不正です'
end
def validate_password(text)
password_regex = /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}\z/
cond = text.match?(password_regex)
err = @document.getElementById('password_error')
err[:innerText] = cond ? '' : 'パスワードが不正です(最低8文字、大文字、小文字、数字を含む必要があります)'
end
private
def setup_event_listeners
[@phone_input, @email_input, @password_input].each do |input|
input.addEventListener 'input' do |e|
method = "validate_#{input[:id]}"
send(method, e[:target][:value].to_s)
end
end
button = @document.getElementById('validateButton')
button.addEventListener 'click' do
validate_phone(@phone_input[:value].to_s)
validate_email(@email_input[:value].to_s)
validate_password(@password_input[:value].to_s)
end
end
end
def document
@document ||= JS.global[:document]
end
Validator.new(document)
</script>
</head>
<body class="bg-gray-800 p-5 text-white">
<div class="max-w-md mx-auto bg-gray-700 shadow-lg rounded p-5">
<h1 class="text-xl font-bold text-white my-4 text-center">Ruby WASM Validator</h1>
<label for="phone" class="block bg-gray-700 text-sm font-bold my-2">電話番号:</label>
<input type="text" inputmode="numeric" id="phone" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline">
<div id="phone_error" class="text-red-500 text-sm mt-2"></div>
<label for="email" class="block bg-gray-700 text-sm font-bold my-2">Email:</label>
<input type="email" id="email" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline">
<div id="email_error" class="text-red-500 text-sm mt-2"></div>
<label for="password" class="block bg-gray-700 text-sm font-bold my-2">パスワード:</label>
<input type="password" id="password" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="パスワード">
<div id="password_error" class="text-red-500 text-sm mt-2"></div>
<label class="flex items-center mt-3">
<input type="checkbox" id="passwordToggle" class="form-checkbox h-5 w-5 text-gray-600"><span class="ml-2 bg-gray-700">パスワードを表示</span>
</label>
<button id="validateButton" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">バリデートを実行</button>
</div>
<script>
const passwordInput = document.getElementById("password")
const passwordToggle = document.getElementById("passwordToggle")
passwordToggle.addEventListener("click", () => {
if (passwordInput.type === "password") {
passwordInput.type = "text"
passwordToggle.innerText = "パスワードを隠す"
} else {
passwordInput.type = "password"
passwordToggle.innerText = "パスワードを表示"
}
})
</script>
</body>
</html>
Rubyの部分を抜粋しました。
require 'js'
class Validator
def initialize(document)
@document = document
@phone_input = document.getElementById('phone')
@email_input = document.getElementById('email')
@password_input = document.getElementById('password')
setup_event_listeners
end
def validate_phone(text)
cond = text.match?(/\A0\d{10}\Z/)
err = @document.getElementById('phone_error')
err[:innerText] = cond ? '' : '電話番号が不正です'
end
def validate_email(text)
email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
cond = text.match?(email_regex)
err = @document.getElementById('email_error')
err[:innerText] = cond ? '' : 'Emailが不正です'
end
def validate_password(text)
password_regex = /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}\z/
cond = text.match?(password_regex)
err = @document.getElementById('password_error')
err[:innerText] = cond ? '' : 'パスワードが不正です(最低8文字、大文字、小文字、数字を含む必要があります)'
end
private
def setup_event_listeners
[@phone_input, @email_input, @password_input].each do |input|
input.addEventListener 'input' do |e|
method = "validate_#{input[:id]}"
send(method, e[:target][:value].to_s)
end
end
button = @document.getElementById('validateButton')
button.addEventListener 'click' do
validate_phone(@phone_input[:value].to_s)
validate_email(@email_input[:value].to_s)
validate_password(@password_input[:value].to_s)
end
end
end
def document
@document ||= JS.global[:document]
end
Validator.new(document)
これを見ると、がっつりRubyのコードって感じがしますね。Classとかメソッドを書けています。
addEventListenerでフォームの変更を検知したら、validationをvalidateのメソッドを動かすようにしていますね。
正規表現をRubyでやるやつ
全体はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Ruby WASM Regex Tester</title>
<meta name="description" content="Ruby正規表現を試すことができるサンプルページです。">
<meta charset="utf-8"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
# frozen_string_literal: true
require 'js'
def document
@document ||= JS.global[:document]
end
def test_regex(pattern, text, options)
regex = Regexp.new(pattern, options)
matches = text.scan(regex)
if matches.any?
highlighted_text = text.gsub(regex) { |match| "<span class='bg-yellow-400 text-black px-1 font-bold'>#{match}</span>" }
status_message = "<span class='text-green-500'>#{matches.size}個のマッチが見つかりました</span>"
result = highlighted_text
else
status_message = "<span class='text-red-500'>マッチしませんでした。</span>"
result = text
end
document.getElementById('statusMessage')[:innerHTML] = status_message
document.getElementById('result')[:innerHTML] = result
end
button = document.getElementById('runButton')
regex_input = document.getElementById('regex')
test_input = document.getElementById('testString')
options_input = document.getElementById('options')
button.addEventListener 'click' do
test_regex(regex_input[:value].to_s, test_input[:value].to_s, options_input[:value].to_s)
end
</script>
</head>
<body class="bg-gray-800 p-5 text-white">
<div class="max-w-xl mx-auto bg-gray-700 border border-gray-600 shadow-lg rounded p-5">
<h1 class="text-xl font-bold text-white my-4 text-center">Ruby WASM Regex Tester</h1>
<div class="mb-4 flex items-center">
<span class="leading-tight text-2xl mr-1">/</span>
<input type="text" id="regex" class="w-full shadow appearance-none border border-gray-600 rounded py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline inline-block" value="https?:\/\/[\w]+\.[\w]+(?:\.[\w]+)+[\/\w._?%&=-]*">
<span class="leading-tight text-2xl mx-1">/</span>
<input type="text" id="options" class="w-16 shadow appearance-none border border-gray-600 rounded py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline inline-block" placeholder="imx">
</div>
<label for="testString" class="block text-sm font-bold mb-2">テスト文字列:</label>
<textarea id="testString" class="shadow appearance-none border border-gray-600 rounded w-full py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline" rows="4">こちらはテスト文章です。URLの例: https://www.example.com</textarea>
<button id="runButton" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">正規表現をテスト</button>
<div class="text-sm mt-4 p-3">
<span>実行結果:</span>
<span id="statusMessage">-</span>
</div>
<div id="result" class="text-sm mt-4 p-3 border border-gray-600 rounded bg-gray-900"> </div>
</div>
</body>
</html>
Rubyの部分を抜粋。
# frozen_string_literal: true
require 'js'
def document
@document ||= JS.global[:document]
end
def test_regex(pattern, text, options)
regex = Regexp.new(pattern, options)
matches = text.scan(regex)
if matches.any?
highlighted_text = text.gsub(regex) { |match| "<span class='bg-yellow-400 text-black px-1 font-bold'>#{match}</span>" }
status_message = "<span class='text-green-500'>#{matches.size}個のマッチが見つかりました</span>"
result = highlighted_text
else
status_message = "<span class='text-red-500'>マッチしませんでした。</span>"
result = text
end
document.getElementById('statusMessage')[:innerHTML] = status_message
document.getElementById('result')[:innerHTML] = result
end
button = document.getElementById('runButton')
regex_input = document.getElementById('regex')
test_input = document.getElementById('testString')
options_input = document.getElementById('options')
button.addEventListener 'click' do
test_regex(regex_input[:value].to_s, test_input[:value].to_s, options_input[:value].to_s)
end
正規表現のフォーム人力された内容を元に、ここでチェック実行させています。
regex = Regexp.new(pattern, options)
if (match = regex.match(text))
Rubyの正規表現をRubyで実行できるのが便利です!
Rubyのコードをブラウザで実行できるやつ
全体はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Ruby Code Executor</title>
<meta name="description" content="このページでは、Rubyコードをwasmを用いてオンラインで実行し、結果を表示します。">
<meta charset="utf-8"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/ruby">
# frozen_string_literal: true
require 'js'
def document
@document ||= JS.global[:document]
end
def execute_ruby_code(code)
result = eval(code)
document.getElementById('result')[:innerText] = "結果: #{result}"
rescue Exception => e
document.getElementById('result')[:innerText] = "エラー: #{e.message}"
end
button = document.getElementById('executeButton')
button.addEventListener 'click' do |_e|
ruby_code = document.getElementById('ruby_code')[:value].to_s
execute_ruby_code(ruby_code)
end
</script>
</head>
<body class="bg-gray-800 p-5 text-white">
<div class="container mx-auto bg-gray-700 shadow-lg rounded p-5">
<h1 class="text-xl font-bold text-white my-4 text-center">Ruby Code Executor</h1>
<label for="ruby_code_editor" class="block text-gray-700 text-sm font-bold mb-2 text-white">Rubyコード:</label>
<div id="ruby_code_editor" class="w-full h-96 rounded"></div>
<textarea id="ruby_code" class="hidden">class Fibonacci
def initialize
@num1, @num2 = 0, 1
end
def next_number
@num1, @num2 = @num2, @num1 + @num2
@num1
end
def answer
@num1
end
end
fib = Fibonacci.new
10.times { puts fib.next_number }
fib.answer</textarea>
<button id="executeButton" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">実行</button>
<div id="result" class="text-green-500 text-sm mt-2"></div>
</div>
<script>
const editor = ace.edit("ruby_code_editor");
editor.setValue(`class Fibonacci
def initialize
@num1, @num2 = 0, 1
end
def next_number
@num1, @num2 = @num2, @num1 + @num2
@num1
end
def answer
@num1
end
end
fib = Fibonacci.new
10.times { puts fib.next_number }
fib.answer
`);
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/ruby");
editor.session.setUseSoftTabs(true);
editor.setFontSize(14);
editor.resize()
editor.getSession().setUseWrapMode(true);
editor.getSession().setTabSize(2);
editor.$blockScrolling = Infinity;
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
});
editor.on('change', (arg, activeEditor) => {
const aceEditor = activeEditor;
const newHeight = aceEditor.getSession().getScreenLength() *
(aceEditor.renderer.lineHeight + aceEditor.renderer.scrollBar.getWidth());
aceEditor.container.style.height = `${newHeight}px`;
aceEditor.resize();
});
editor.getSession().on('change', function(){
const code = editor.getValue();
document.getElementById("ruby_code").value = code;
});
</script>
</body>
</html>
Ruby部分の抜粋。
# frozen_string_literal: true
require 'js'
def document
@document ||= JS.global[:document]
end
def execute_ruby_code(code)
result = eval(code)
document.getElementById('result')[:innerText] = "結果: #{result}"
rescue Exception => e
document.getElementById('result')[:innerText] = "エラー: #{e.message}"
end
button = document.getElementById('executeButton')
button.addEventListener 'click' do |_e|
ruby_code = document.getElementById('ruby_code')[:value].to_s
execute_ruby_code(ruby_code)
end
eval
でTextareaに入力されたコードを実行していますね。
作った中でこれが一番色々できて面白いなと感じました。
例えば、こんな感じでFiberも動きました!すごい!
セキュリティ上の注意
ユーザーの入力を直接 eval で実行する場合は特に注意が必要です。実際に使う際には、サニタイズなどをして実行するのが良いかと思います。
この記事に記載したサイトにも、イタズラはやめてね 🙏
おわりに
Wasmではもっと多くのことができますが、とりあえずの一歩目と言うことでこんな記事を書きました。同じく入門する人の役に立ったらなと思います。
もう少し慣れたら、Wasmを使って便利になる箇所とかで実運用で使ってみたいですね