Jak mogę współdzielić kod między Node.js a przeglądarką?

242

Tworzę małą aplikację z klientem JavaScript (uruchomionym w przeglądarce) i serwerem Node.js, komunikującym się za pomocą WebSocket.

Chciałbym udostępnić kod między klientem a serwerem. Dopiero zacząłem z Node.js, a moja znajomość współczesnego JavaScript jest trochę zardzewiała, delikatnie mówiąc. Nadal więc skupiam się na funkcji wymagającej () funkcji CommonJS. Jeśli tworzę swoje pakiety za pomocą obiektu „eksportuj”, nie widzę, jak mogę korzystać z tych samych plików JavaScript w przeglądarce.

Chcę utworzyć zestaw metod i klas używanych na obu końcach w celu ułatwienia kodowania i dekodowania wiadomości oraz innych lustrzanych zadań. Wydaje się jednak, że systemy pakowania Node.js / CommonJS uniemożliwiają mi tworzenie plików JavaScript, których można używać po obu stronach.

Próbowałem też użyć JS.Class, aby uzyskać ściślejszy model OO, ale poddałem się, ponieważ nie mogłem wymyślić, w jaki sposób uzyskać dostarczone pliki JavaScript do pracy z wymaganiem (). Czy czegoś tu brakuje?

Simon Cave
źródło
4
Dziękujemy wszystkim za opublikowanie dodatkowych odpowiedzi na to pytanie. Jest to oczywiście temat, który szybko się zmieni i ewoluuje.
Simon Cave

Odpowiedzi:

168

Jeśli chcesz napisać moduł, którego można używać zarówno po stronie klienta, jak i po stronie serwera, mam krótki post na blogu o szybkiej i łatwej metodzie: Pisanie dla Node.js i przeglądarki , zasadniczo następujące (gdzie thisto samo co window) :

(function(exports){

    // Your code goes here

   exports.test = function(){
        return 'hello world'
    };

})(typeof exports === 'undefined'? this['mymodule']={}: exports);

Alternatywnie istnieją projekty mające na celu implementację API Node.js po stronie klienta, takie jak gemini Maraka .

Może Cię również zainteresować DNode , który pozwala ujawnić funkcję JavaScript, dzięki czemu można ją wywoływać z innego komputera za pomocą prostego protokołu sieciowego opartego na JSON.

Caolan
źródło
Doskonały. Dzięki za informację, Caolan.
Simon Cave
2
Naprawdę świetny artykuł Caolan. Zrozumiałem, zadziałało, teraz znów się toczę. Fantastyczny!
Michael Dausmann
2
Używam RequireJs w swoim własnym projekcie, co pozwoli mi udostępniać moje moduły na kliencie i serwerze. Zobaczymy jak to działa.
kamranicus
5
@Caolan ten link nie działa
Kamal Reddy
5
Link do gemini jest martwy.
borisdiakur
42

Epeli ma fajne rozwiązanie tutaj http://epeli.github.com/piler/, które działa nawet bez biblioteki, wystarczy umieścić to w pliku o nazwie share.js

(function(exports){

  exports.test = function(){
       return 'This is a function from shared module';
  };

}(typeof exports === 'undefined' ? this.share = {} : exports));

Po stronie serwera wystarczy użyć:

var share = require('./share.js');

share.test();

A po stronie klienta wystarczy załadować plik js, a następnie użyć

share.test();
brosze
źródło
10
Podoba mi się ta odpowiedź bardziej niż zaakceptowana, ponieważ jest lepiej wyjaśniona początkującym użytkownikom.
Howie,
W moim folderze Express oprócz folderu statycznego (publicznego) mam również folder o nazwie „udostępniony”, który jest również dostępny z poziomu klienta, taki jak folder „publiczny” w ten sposób: app.use (express.static („public”)) ; app.use (express.static („shared”)); A Twój post stanowi rozwinięcie mojego pomysłu udostępniania plików klientowi i serwerowi. Właśnie tego potrzebowałem. Dziękuję Ci!
Połącz
To rozwiązanie + poddrzewo git == niesamowite. Dzięki!
kevinmicke
@broesch Jak to działałoby w ES6? Zadałem to pytanie jako nowe pytanie z pewnymi problemami związanymi z ES6, ale równie chętnie zobaczę edycję tutaj!
Tedskovsky
15

Sprawdź kod źródłowy jQuery, który sprawia, że ​​działa to we wzorcu modułów Node.js, wzorcach modułów AMD i globalnie w przeglądarce:

(function(window){
    var jQuery = 'blah';

    if (typeof module === "object" && module && typeof module.exports === "object") {

        // Expose jQuery as module.exports in loaders that implement the Node
        // module pattern (including browserify). Do not create the global, since
        // the user will be storing it themselves locally, and globals are frowned
        // upon in the Node module world.
        module.exports = jQuery;
    }
    else {
        // Otherwise expose jQuery to the global object as usual
        window.jQuery = window.$ = jQuery;

        // Register as a named AMD module, since jQuery can be concatenated with other
        // files that may use define, but not via a proper concatenation script that
        // understands anonymous AMD modules. A named AMD is safest and most robust
        // way to register. Lowercase jquery is used because AMD module names are
        // derived from file names, and jQuery is normally delivered in a lowercase
        // file name. Do this after creating the global so that if an AMD module wants
        // to call noConflict to hide this version of jQuery, it will work.
        if (typeof define === "function" && define.amd) {
            define("jquery", [], function () { return jQuery; });
        }
    }
})(this)
wlingke
źródło
To najlepsza metoda (do tego, czego potrzebowałem). Oto działający przykład, który utworzyłem: gist.github.com/drmikecrowe/4bf0938ea73bf704790f
Mike Crowe
13

Nie zapominaj, że ciąg znaków funkcji JavaScript reprezentuje kod źródłowy tej funkcji. Możesz po prostu napisać swoje funkcje i konstruktory w sposób enkapsulowany, aby mogły one być toString () i wysłane do klienta.

Innym sposobem na to jest użycie systemu kompilacji, umieszczenie wspólnego kodu w osobnych plikach, a następnie uwzględnienie ich w skryptach serwera i klienta. Używam tego podejścia do prostej gry klient / serwer za pośrednictwem WebSockets, w której serwer i klient działają zasadniczo w tej samej pętli gry, a klient synchronizuje się z serwerem za każdym razem, aby upewnić się, że nikt nie oszukuje.

Mój system kompilacji gry to prosty skrypt Bash, który uruchamia pliki przez preprocesor C, a następnie przez sed, aby wyczyścić niektóre śmieciowe cpp pozostawia po sobie, dzięki czemu mogę używać wszystkich normalnych rzeczy preprocesora, takich jak #include, #define, #ifdef itp.

Dagg Nabbit
źródło
2
Serializacja funkcji javascript, ponieważ ciągi nigdy nie przyszły mi do głowy. Dzięki za wskazówkę.
Simon Cave
13

Polecam patrząc na adapterze RequireJS dla node.js . Problem polega na tym, że wzorzec modułu CommonJS, który Node.js używa domyślnie, nie jest asynchroniczny, co blokuje ładowanie w przeglądarce internetowej. RequireJS używa wzorca AMD, który jest zarówno asynchroniczny, jak i zgodny zarówno z serwerem, jak i klientem, o ile korzystasz z r.jsadaptera.

Ochrypły
źródło
jest biblioteka asynchroniczna
Jacek Pietal
11

Może nie jest to całkowicie zgodne z pytaniem, ale pomyślałem, że podzielę się tym.

Chciałem, aby kilka prostych funkcji narzędziowych napisanych w String.prototype było dostępnych zarówno dla węzła, jak i przeglądarki. Po prostu przechowuję te funkcje w pliku o nazwie utilities.js (w podfolderze) i mogę łatwo odwoływać się do nich zarówno ze znacznika script w kodzie przeglądarki, jak i używając wymaganego (pomijając rozszerzenie .js) w moim skrypcie Node.js :

my_node_script.js

var utilities = require('./static/js/utilities')

my_browser_code.html

<script src="/static/js/utilities.js"></script>

Mam nadzieję, że jest to przydatna informacja dla kogoś innego niż ja.

Markus Amalthea Magnuson
źródło
1
Podoba mi się to podejście, ale moje pliki statyczne są często przenoszone. Jednym ze znalezionych przeze mnie rozwiązań jest ponowny eksport modułu. Na przykład utwórz utilites.jsza pomocą jednej linii module.exports = require('./static/js/utilities');. W ten sposób musisz zaktualizować tylko jedną ścieżkę, jeśli przetasujesz różne rzeczy.
Tom Makin
Podoba mi się ten pomysł. Tylko notatkę na ścieżce, której zrozumienie zajęło mi trochę czasu. Mój utilities.jsjest w sharedfolderze w ramach projektu. Używanie require('/shared/utilities')dało mi błąd Cannot find module '/shared/utilities'. Muszę użyć czegoś takiego, require('./../../shared/utilities')aby to działało. Tak więc zawsze idzie z bieżącego folderu i podróżuje do katalogu głównego, a następnie do dołu.
newman
Teraz widzę, gdzie umieścić moduł współdzielony - w folderze statycznym. Dzięki za informację!
Połącz
9

Jeśli używasz użytku bundlers modułów takich jak WebPack do wiązki plików JavaScript do użytku w przeglądarce, można po prostu ponowne wykorzystanie modułu node.js dla frontend działającej w przeglądarce. Innymi słowy, moduł Node.js może być współdzielony między Node.js a przeglądarką.

Na przykład masz następujący kod sum.js:

Normalny moduł Node.js: sum.js

const sum = (a, b) => {
    return a + b
}

module.exports = sum

Użyj modułu w Node.js

const sum = require('path-to-sum.js')
console.log('Sum of 2 and 5: ', sum(2, 5)) // Sum of 2 and 5:  7

Użyj go ponownie w interfejsie

import sum from 'path-to-sum.js'
console.log('Sum of 2 and 5: ', sum(2, 5)) // Sum of 2 and 5:  7
Yuci
źródło
4

Serwer może po prostu wysyłać pliki źródłowe JavaScript do klienta (przeglądarki), ale sztuczka polega na tym, że klient będzie musiał zapewnić środowisko mini „eksportu”, zanim będzie mógł execkod i zapisać go jako moduł.

Prostym sposobem na stworzenie takiego środowiska jest użycie zamknięcia. Załóżmy na przykład, że Twój serwer udostępnia pliki źródłowe przez HTTP, takie jak http://example.com/js/foo.js. Przeglądarka może załadować wymagane pliki za pośrednictwem XMLHttpRequest i załadować kod w następujący sposób:

ajaxRequest({
  method: 'GET',
  url: 'http://example.com/js/foo.js',
  onSuccess: function(xhr) {
    var pre = '(function(){var exports={};'
      , post = ';return exports;})()';
    window.fooModule = eval(pre + xhr.responseText + post);
  }
});

Kluczem jest to, że klient może owinąć obcy kod w anonimową funkcję, która ma być natychmiast uruchomiona (zamknięcie), która tworzy obiekt „eksportuje” i zwraca go, abyś mógł przypisać go tam, gdzie chcesz, zamiast zanieczyszczać globalną przestrzeń nazw. W tym przykładzie jest przypisany do atrybutu window, fooModulektóry będzie zawierał kod wyeksportowany przez plik foo.js.

maerika
źródło
2
za każdym razem, gdy używasz eval, zabijasz gnoma
Jacek Pietal
1
Użyłbym window.fooModule = {}; (new Function('exports', xhr.responseText))(window.fooModule).
GingerPlusPlus
2

Żadne z poprzednich rozwiązań nie wprowadza systemu modułów CommonJS do przeglądarki.

Jak wspomniano w innych odpowiedzi, są wartościowe rozwiązania manager / Packager jak Browserify lub PILER i istnieją rozwiązania RPC jak dnode lub nowjs .

Ale nie mogłem znaleźć implementacji CommonJS dla przeglądarki (w tym require()funkcji i exports/ module.exportsobiektów itp.). Więc napisałem własny, aby później odkryć, że ktoś inny napisał to lepiej niż ja: https://github.com/weepy/brequire . Nazywa się Brequire (skrót od przeglądarki wymaga).

Sądząc po popularności, zarządzający aktywami odpowiadają potrzebom większości programistów. Jednak jeśli potrzebujesz implementacji CommonJS w przeglądarce, Brequire prawdopodobnie będzie pasować do rachunku.

Aktualizacja 2015: Nie używam już Brequire (nie był aktualizowany od kilku lat). Jeśli piszę tylko mały moduł o otwartym kodzie źródłowym i chcę, aby ktokolwiek mógł z niego łatwo korzystać, podążę za wzorem podobnym do odpowiedzi Caolana (powyżej) - napisałem o nim na blogu kilka lat temu.

Jeśli jednak piszę moduły do ​​użytku prywatnego lub społeczności znormalizowanej na CommonJS (takiej jak społeczność Ampersand ), to po prostu napiszę je w formacie CommonJS i skorzystam z Browserify .

Peter Rust
źródło
1

warto też zajrzeć na now.js. Pozwala wywoływać funkcje po stronie serwera po stronie klienta, a funkcje po stronie klienta - po stronie serwera

balupton
źródło
1
Projekt został przerwany - czy znasz jakieś dobre zamienniki? groups.google.com/forum/#!msg/nowjs/FZXWZr22vn8/UzTMPD0tdVQJ
Anderson Green
jedyny, jaki znam, był pomostem i był wykonywany przez tych samych ludzi, więc także opuszczony. Wersja socket.io w wersji 0.9 obsługuje także wywołania zwrotne zdarzeń - jednak nie przypomina to kodu współdzielenia now.js, ale działa wystarczająco dobrze.
balupton
Istnieją również sharejs, które wydają się być aktywnie utrzymywane. sharejs.org
Anderson Green
1

Jeśli chcesz napisać swoją przeglądarkę w stylu podobnym do Node.js, możesz wypróbować dualify .

Nie ma kompilacji kodu przeglądarki, więc możesz pisać aplikacje bez ograniczeń.

farincz
źródło
1

Napisz swój kod jako moduły RequireJS , a testy jako testy Jasmine .

W ten sposób kod może być ładowany wszędzie za pomocą RequireJS, a testy mogą być uruchamiane w przeglądarce za pomocą jasmine-html i jasmine-node w Node.js bez potrzeby modyfikowania kodu lub testów.

Oto działający przykład tego.

Blacksonic
źródło
1

Przypadek użycia: udostępnij konfigurację aplikacji między Node.js a przeglądarką (jest to tylko ilustracja, prawdopodobnie nie najlepsze podejście w zależności od aplikacji).

Problem: nie możesz użyć window(nie istnieje w Node.js) ani global(nie istnieje w przeglądarce).

Rozwiązanie:

  • Plik config.js:

    var config = {
      foo: 'bar'
    };
    if (typeof module === 'object') module.exports = config;
  • W przeglądarce (index.html):

    <script src="config.js"></script>
    <script src="myApp.js"></script>

    Możesz teraz otworzyć narzędzia programistyczne i uzyskać dostęp do zmiennej globalnej config

  • W Node.js (app.js):

    const config = require('./config');
    console.log(config.foo); // Prints 'bar'
  • Z Babel lub TypeScript:

    import config from './config';
    console.log(config.foo); // Prints 'bar'
tanguy_k
źródło
1
Dziękuję Ci za to.
Microsis
Kontynuacja: Załóżmy, że mam dwa pliki, które są współużytkowane przez server.js i client.js: shared.jsi helpers.js- shared.jsużywa funkcji z helpers.js, więc musi być const { helperFunc } = require('./helpers')u góry, aby działał po stronie serwera. Problem leży po stronie klienta, narzeka on na to, że requirenie jest funkcją, ale jeśli if (typeof module === 'object') { ... }wstawię wymaganą linię , serwer mówi, że helperFunc () nie jest zdefiniowany (poza instrukcją if). Jakieś pomysły na to, żeby działało na obu?
Microsis
Aktualizacja: Wydaje mi się, że zadziałało, umieszczając to u góry shared.js: helperFunc = (typeof exports === 'undefined') ? helperFunc : require('./helpers').helperFunc;- Niestety, potrzebuję linii dla każdej eksportowanej funkcji, ale mam nadzieję, że to dobre rozwiązanie?
Microsis
1

Napisałem prosty moduł , który można zaimportować (albo używając wymaga w węźle, albo znaczników skryptu w przeglądarce), którego można użyć do załadowania modułów zarówno z klienta, jak iz serwera.

Przykładowe użycie

1. Definiowanie modułu

Umieść następujące elementy w pliku log2.jsw folderze statycznych plików internetowych:

let exports = {};

exports.log2 = function(x) {
    if ( (typeof stdlib) !== 'undefined' )
        return stdlib.math.log(x) / stdlib.math.log(2);

    return Math.log(x) / Math.log(2);
};

return exports;

Proste!

2. Korzystanie z modułu

Ponieważ jest to moduł ładujący moduły dwustronne , możemy go ładować z obu stron (klienta i serwera). Dlatego możesz wykonać następujące czynności, ale nie musisz wykonywać obu naraz (nie mówiąc już o określonej kolejności):

  • W węźle

W węźle jest to proste:

var loader = require('./mloader.js');
loader.setRoot('./web');

var logModule = loader.importModuleSync('log2.js');
console.log(logModule.log2(4));

To powinno wrócić 2.

Jeśli twój plik nie znajduje się w bieżącym katalogu węzła, pamiętaj, aby zadzwonić loader.setRootze ścieżką do folderu ze statycznymi plikami internetowymi (lub gdziekolwiek znajduje się moduł).

  • W przeglądarce:

Najpierw zdefiniuj stronę internetową:

<html>
    <header>
        <meta charset="utf-8" />
        <title>Module Loader Availability Test</title>

        <script src="mloader.js"></script>
    </header>

    <body>
        <h1>Result</h1>
        <p id="result"><span style="color: #000088">Testing...</span></p>

        <script>
            let mod = loader.importModuleSync('./log2.js', 'log2');

            if ( mod.log2(8) === 3 && loader.importModuleSync('./log2.js', 'log2') === mod )
                document.getElementById('result').innerHTML = "Your browser supports bilateral modules!";

            else
                document.getElementById('result').innerHTML = "Your browser doesn't support bilateral modules.";
        </script>
    </body>
</html>

Upewnij się, że nie otwierasz pliku bezpośrednio w przeglądarce; ponieważ używa AJAX, sugeruję, aby spojrzeć na http.servermoduł Pythona 3 (lub cokolwiek, co jest twoim superszybkim, wierszem poleceń, rozwiązaniem do wdrażania serwera sieciowego dla folderów).

Jeśli wszystko pójdzie dobrze, pojawi się:

wprowadź opis zdjęcia tutaj

Gustavo6046
źródło
0

Napisałem to, jest prosty w użyciu, jeśli chcesz ustawić wszystkie zmienne w zakresie globalnym:

(function(vars, global) {
    for (var i in vars) global[i] = vars[i];
})({
    abc: function() {
        ...
    },
    xyz: function() {
        ...
    }
}, typeof exports === "undefined" ? this : exports);
Wspaniały
źródło