Một số vấn đề gặp phải khi lập trình Ruby on Rails
Giới thiệu Ruby on Rails hiện tại còn khá mới mẻ ở Việt Nam, cộng đồng người dùng còn ít và việc chia sẻ các thông tin, kinh nghiệm về ngôn ngữ này còn hạn chế. Chính vì vậy, mình viết bài viết này chủ yếu nói về những vấn đề mà mình đã từng gặp phải và mình thấy hay để chia sẻ với các bạn. Có ...
Giới thiệu
Ruby on Rails hiện tại còn khá mới mẻ ở Việt Nam, cộng đồng người dùng còn ít và việc chia sẻ các thông tin, kinh nghiệm về ngôn ngữ này còn hạn chế. Chính vì vậy, mình viết bài viết này chủ yếu nói về những vấn đề mà mình đã từng gặp phải và mình thấy hay để chia sẻ với các bạn. Có thể đó là những vấn đề không khó nhưng mà chúng ta cần phải lưu ý trong quá trình sử dụng và các bạn có thể sẽ gặp phải. Khi đó, các bạn không phải mày mò xem tại sao nó lại có lỗi này, tại sao mình code không chạy mà đọc qua bài đọc của mình là các bạn có thể hiểu biết được mình sai ở đâu và có thể sửa ngay lập tức.
Môi trường
Ở đây, mình sử dụng rails new demo-report để tạo project. Mình sẽ nói về các vấn đề cần lưu ý khi sử dụng một số gem thông dụng.
- gem "devise"
- gem "turbolinks" (được đặt làm default khi tạo app)
- Jquery sortable
Ở bài viết này, mình sẽ viết ngắn gọn một vài vấn đề khi gặp phải với những gem trên, và mình sẽ update chúng thêm sau này. Các bạn nhớ theo dõi nhé. (Thời gian updated mình sẽ ghi ở đầu bài viết)
I. Devise
Như các bạn đã biết, với nhiều tài khoản khác nhau thì ta sẽ có những trang default khác nhau khi user login vào hệ thống, hoặc có thể là trang mà user đã gõ trước khi login. Khi chúng ra muốn control các trang mà user sẽ vào sau khi login thì ta thường hay over write hàm def after_sign_in_path_for resource của devise.
Dưới đây là ví dụ:
def after_sign_in_path_for resource referer_path = stored_location_for resource case when referer_path.present? && referer_path != root_path referer_path when xxx root_path(xxx..) when yyy root_path(xxx..) else root_path end end
Trong ví dụ trên, mình sử dụng hàm stored_location_for resource là một hàm của devise, nó lưu lại link tới trang web mà user định vào trước khi bị chuyển tới trang login. Và tại sao mình phải sử dụng referer_path để lưu lại nó và tại sao lại phải check referer_path.present?.
- Do hàm stored_location_for resource sẽ chỉ dùng được một lần, dùng đến lần thứ 2 thì nó sẽ trả ra nil và ta không thể lấy lại referer_path được nữa.
- Khi ta trả về referer_path mà referer_path nil thì nó sẽ redirect_to nill => Chắc chắn sẽ bị lỗi.
Trường hợp 2 sẽ xảy ra khi bạn reset pasword. Sau khi reset pasword thành công thì gem "devise" sẽ mặc định login cho bạn, và trường hợp đó nó ko lưu lại referer_path và giá trị của nó sẽ nil.
Nếu không chú ý thì việc sửa hàm after_sign_in_path_for sẽ gây lỗi cho trang khác tiềm ẩn.
II. Turbolinks
Đây là một gem giúp cho web của chúng ta chạy nhanh hơn, do nó giữ lại js và css mà không phải recompile lại. Khi chuyển trang, nó chỉ việc đọc body, title, header để thay thế. Việc sử dụng turbolinks gần như là sử dụng ajax với toàn bộ trang web.
Chính vì vậy, khi ta vừa sử dụng turborlinks vừa sử dụng ajax thì sẽ gặp nhiều vấn đề nếu nó có liên quan tới js.
- Khi set sự kiện trực tiếp thì các view mới được tạo ra bằng js sẽ không bắt được event đó.
- Khi set sự kiện qua $$document) thì khi di chuyển các trang bằng cách click trực tiếp vào link trong web thì js sẽ ko được recompile mà trong view gọi ra lại gọi thêm lần nữa => 1 event sẽ được set n lần (số lần click vào link mà ko f5 trang).
=> Khi sử dụng turbolinks thì phải lưu ý kỹ điều này với view mà sinh thêm view bằng js hay ajax. Luôn off event trước khi tạo thêm một event mới.
VD:
<table> <tr> <th><h2 class="clone1">Click me 1</h2></th> <th><h2 class="clone2">Click me 2</h2></th> <th><h2 class="clone3">Click me 3</h2></th> </tr> </table> <script> $(function() { $(".clone1").click(function(){ $(this).clone().insertAfter($(this)); }); $(document).on("click", ".clone2", function(){ $(this).clone().insertAfter($(this)); }); $(document).off("click", ".clone3"); $(document).on("click", ".clone3", function(){ $(this).clone().insertAfter($(this)); }); }); </script>
Như ví dụ trên ta sẽ cho ra kết quả khi vào trang web lần đầu và click lần lượt vào Click me 1, 2, 3. Khi đó event sẽ tạo thêm một element nữa ở dưới. Với ".clone1" thì element mới sẽ ko chạy được sự kiện click trên. Với ".clone2", ".clone3" thì được như click và phần tử đầu tiên.
Như trên hình ta thầy có link "Turbolinks" bên cạnh SortAble. Bây giờ ta click vào đó. Link này trỏ tới trang hiện tại chứ ko phải trang khác. Khi click vào thì view sẽ được reset như ban đầu.
Bây giờ tiếp tục click lần lượt các Click me 1, 2, 3. Và ta được kết quả như bên dưới.
Như các bạn thấy, turbolinks đã lưu lại event click cho Click me 2 và 3. Nhưng khi click vào trang lại thì Click me 3 đã off event click trước khi khỏi tạo event mới. Chính vì vậy nó mới hoạt động đúng mà ko bị duplicate khi click lần 2.
IIII. Jquery sortalbe
Đối với jquery sortable thì mình không có gì để nói nhiều, và vấn đề mình gặp phải rất nhỏ nhưng lại mất khá nhiều thời gian để tìm hiểu tại sao lỗi đó lại xảy ra. Mình chưa nhảy vào source của thư viện này để tìm hiểu cách tính toán của họ như thế nào, nhưng để tránh lỗi này thì mình đã có cách.
Vấn đề mình gặp phải là khi sử dụng sortable với những class con của những class cha nào đó. Và số lượng của cả class cha và con phải lớn và độ phức tạp trên view nhất định thì mới có lỗi.
VD: Trên 1 màn hình hiển thị ra nhiều lớp. Trong mỗi lớp lại có nhiều thành viên. Và mình kéo thả với các thành viên đó. Các thành viên có thể kéo thả sang các lớp khác và sắp xếp thứ tự ở đó được. Vấn đề ở đây là khi vào màn hình đó thì các thành viên trong mỗi lớp lại bị ẩn đi chứ ko hiển thị hết lên.
<script> $(function() { $(".sortable-ul").sortable(); $(".sortable-li").sortable({ connectWith: ".sortable-li" }) }); </script>
Như hình trên các bạn thấy, các item trong UL 0, 1 được hiển thị còn các item trong UL 2,3,4 ... thì không được hiển thị ra. Như vậy việc kéo các item qua lại giữa những item được hiển thị là bình thường. Còn khi kéo đến những item bị ẩn thì vẫn kéo được. Nhưng khi thả ra thì nó nằm ở đâu thì ta không hề biết. Chắc người viết ra thư viện này cũng không hề nghĩ tới trường hợp này để support cho nên khi view quá phức tạp và số lương item nhiều thì sẽ gây ra lỗi. Việc kéo thả sẽ bị nhảy loạn lên. Việc của chúng ta là chỉ cho connect với những thằng đang được hiển thị là xong. connectWith: ".sortable-li" => connectWith: ".sortable-li:visible" Việc này tuy nhỏ nhưng mà nó sẽ giúp bạn tránh được những sự cố không đáng có và mất nhiều công sức để tìm hiểu như mình.
Ở trên chỉ là một ví dụ nhỏ mình demo để cho các bạn hiểu chứ với view đơn giản như vậy thì việc lỗi xảy ra khá ít.
DEMO
https://github.com/banlv54/demo-report