diff --git a/totrans/bd-sgl-pg-webapp-mtr_00.yaml b/data/bd-sgl-pg-webapp-mtr_00.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_00.yaml rename to data/bd-sgl-pg-webapp-mtr_00.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_01.yaml b/data/bd-sgl-pg-webapp-mtr_01.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_01.yaml rename to data/bd-sgl-pg-webapp-mtr_01.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_02.yaml b/data/bd-sgl-pg-webapp-mtr_02.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_02.yaml rename to data/bd-sgl-pg-webapp-mtr_02.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_03.yaml b/data/bd-sgl-pg-webapp-mtr_03.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_03.yaml rename to data/bd-sgl-pg-webapp-mtr_03.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_04.yaml b/data/bd-sgl-pg-webapp-mtr_04.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_04.yaml rename to data/bd-sgl-pg-webapp-mtr_04.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_05.yaml b/data/bd-sgl-pg-webapp-mtr_05.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_05.yaml rename to data/bd-sgl-pg-webapp-mtr_05.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_06.yaml b/data/bd-sgl-pg-webapp-mtr_06.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_06.yaml rename to data/bd-sgl-pg-webapp-mtr_06.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_07.yaml b/data/bd-sgl-pg-webapp-mtr_07.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_07.yaml rename to data/bd-sgl-pg-webapp-mtr_07.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_08.yaml b/data/bd-sgl-pg-webapp-mtr_08.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_08.yaml rename to data/bd-sgl-pg-webapp-mtr_08.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_09.yaml b/data/bd-sgl-pg-webapp-mtr_09.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_09.yaml rename to data/bd-sgl-pg-webapp-mtr_09.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_10.yaml b/data/bd-sgl-pg-webapp-mtr_10.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_10.yaml rename to data/bd-sgl-pg-webapp-mtr_10.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_11.yaml b/data/bd-sgl-pg-webapp-mtr_11.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_11.yaml rename to data/bd-sgl-pg-webapp-mtr_11.yaml diff --git a/totrans/bd-sgl-pg-webapp-mtr_12.yaml b/data/bd-sgl-pg-webapp-mtr_12.yaml similarity index 100% rename from totrans/bd-sgl-pg-webapp-mtr_12.yaml rename to data/bd-sgl-pg-webapp-mtr_12.yaml diff --git a/totrans/deno-web-dev_00.yaml b/data/deno-web-dev_00.yaml similarity index 100% rename from totrans/deno-web-dev_00.yaml rename to data/deno-web-dev_00.yaml diff --git a/totrans/deno-web-dev_01.yaml b/data/deno-web-dev_01.yaml similarity index 100% rename from totrans/deno-web-dev_01.yaml rename to data/deno-web-dev_01.yaml diff --git a/totrans/deno-web-dev_02.yaml b/data/deno-web-dev_02.yaml similarity index 100% rename from totrans/deno-web-dev_02.yaml rename to data/deno-web-dev_02.yaml diff --git a/totrans/deno-web-dev_03.yaml b/data/deno-web-dev_03.yaml similarity index 100% rename from totrans/deno-web-dev_03.yaml rename to data/deno-web-dev_03.yaml diff --git a/totrans/deno-web-dev_04.yaml b/data/deno-web-dev_04.yaml similarity index 100% rename from totrans/deno-web-dev_04.yaml rename to data/deno-web-dev_04.yaml diff --git a/totrans/deno-web-dev_05.yaml b/data/deno-web-dev_05.yaml similarity index 100% rename from totrans/deno-web-dev_05.yaml rename to data/deno-web-dev_05.yaml diff --git a/totrans/deno-web-dev_06.yaml b/data/deno-web-dev_06.yaml similarity index 100% rename from totrans/deno-web-dev_06.yaml rename to data/deno-web-dev_06.yaml diff --git a/totrans/deno-web-dev_07.yaml b/data/deno-web-dev_07.yaml similarity index 100% rename from totrans/deno-web-dev_07.yaml rename to data/deno-web-dev_07.yaml diff --git a/totrans/deno-web-dev_08.yaml b/data/deno-web-dev_08.yaml similarity index 100% rename from totrans/deno-web-dev_08.yaml rename to data/deno-web-dev_08.yaml diff --git a/totrans/deno-web-dev_09.yaml b/data/deno-web-dev_09.yaml similarity index 100% rename from totrans/deno-web-dev_09.yaml rename to data/deno-web-dev_09.yaml diff --git a/totrans/deno-web-dev_10.yaml b/data/deno-web-dev_10.yaml similarity index 100% rename from totrans/deno-web-dev_10.yaml rename to data/deno-web-dev_10.yaml diff --git a/totrans/dev-win-store-app-h5-js_00.yaml b/data/dev-win-store-app-h5-js_00.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_00.yaml rename to data/dev-win-store-app-h5-js_00.yaml diff --git a/totrans/dev-win-store-app-h5-js_01.yaml b/data/dev-win-store-app-h5-js_01.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_01.yaml rename to data/dev-win-store-app-h5-js_01.yaml diff --git a/totrans/dev-win-store-app-h5-js_02.yaml b/data/dev-win-store-app-h5-js_02.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_02.yaml rename to data/dev-win-store-app-h5-js_02.yaml diff --git a/totrans/dev-win-store-app-h5-js_03.yaml b/data/dev-win-store-app-h5-js_03.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_03.yaml rename to data/dev-win-store-app-h5-js_03.yaml diff --git a/totrans/dev-win-store-app-h5-js_04.yaml b/data/dev-win-store-app-h5-js_04.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_04.yaml rename to data/dev-win-store-app-h5-js_04.yaml diff --git a/totrans/dev-win-store-app-h5-js_05.yaml b/data/dev-win-store-app-h5-js_05.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_05.yaml rename to data/dev-win-store-app-h5-js_05.yaml diff --git a/totrans/dev-win-store-app-h5-js_06.yaml b/data/dev-win-store-app-h5-js_06.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_06.yaml rename to data/dev-win-store-app-h5-js_06.yaml diff --git a/totrans/dev-win-store-app-h5-js_07.yaml b/data/dev-win-store-app-h5-js_07.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_07.yaml rename to data/dev-win-store-app-h5-js_07.yaml diff --git a/totrans/dev-win-store-app-h5-js_08.yaml b/data/dev-win-store-app-h5-js_08.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_08.yaml rename to data/dev-win-store-app-h5-js_08.yaml diff --git a/totrans/dev-win-store-app-h5-js_09.yaml b/data/dev-win-store-app-h5-js_09.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_09.yaml rename to data/dev-win-store-app-h5-js_09.yaml diff --git a/totrans/dev-win-store-app-h5-js_10.yaml b/data/dev-win-store-app-h5-js_10.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_10.yaml rename to data/dev-win-store-app-h5-js_10.yaml diff --git a/totrans/dev-win-store-app-h5-js_11.yaml b/data/dev-win-store-app-h5-js_11.yaml similarity index 100% rename from totrans/dev-win-store-app-h5-js_11.yaml rename to data/dev-win-store-app-h5-js_11.yaml diff --git a/totrans/gt-mtr-js-fw_0.yaml b/data/gt-mtr-js-fw_0.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_0.yaml rename to data/gt-mtr-js-fw_0.yaml diff --git a/totrans/gt-mtr-js-fw_1.yaml b/data/gt-mtr-js-fw_1.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_1.yaml rename to data/gt-mtr-js-fw_1.yaml diff --git a/totrans/gt-mtr-js-fw_2.yaml b/data/gt-mtr-js-fw_2.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_2.yaml rename to data/gt-mtr-js-fw_2.yaml diff --git a/totrans/gt-mtr-js-fw_3.yaml b/data/gt-mtr-js-fw_3.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_3.yaml rename to data/gt-mtr-js-fw_3.yaml diff --git a/totrans/gt-mtr-js-fw_4.yaml b/data/gt-mtr-js-fw_4.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_4.yaml rename to data/gt-mtr-js-fw_4.yaml diff --git a/totrans/gt-mtr-js-fw_5.yaml b/data/gt-mtr-js-fw_5.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_5.yaml rename to data/gt-mtr-js-fw_5.yaml diff --git a/totrans/gt-mtr-js-fw_6.yaml b/data/gt-mtr-js-fw_6.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_6.yaml rename to data/gt-mtr-js-fw_6.yaml diff --git a/totrans/gt-mtr-js-fw_7.yaml b/data/gt-mtr-js-fw_7.yaml similarity index 100% rename from totrans/gt-mtr-js-fw_7.yaml rename to data/gt-mtr-js-fw_7.yaml diff --git a/totrans/ins-test-qunit_0.yaml b/data/ins-test-qunit_0.yaml similarity index 100% rename from totrans/ins-test-qunit_0.yaml rename to data/ins-test-qunit_0.yaml diff --git a/totrans/ins-test-qunit_1.yaml b/data/ins-test-qunit_1.yaml similarity index 100% rename from totrans/ins-test-qunit_1.yaml rename to data/ins-test-qunit_1.yaml diff --git a/totrans/js-dnet-dev_00.yaml b/data/js-dnet-dev_00.yaml similarity index 100% rename from totrans/js-dnet-dev_00.yaml rename to data/js-dnet-dev_00.yaml diff --git a/totrans/js-dnet-dev_01.yaml b/data/js-dnet-dev_01.yaml similarity index 100% rename from totrans/js-dnet-dev_01.yaml rename to data/js-dnet-dev_01.yaml diff --git a/totrans/js-dnet-dev_02.yaml b/data/js-dnet-dev_02.yaml similarity index 100% rename from totrans/js-dnet-dev_02.yaml rename to data/js-dnet-dev_02.yaml diff --git a/totrans/js-dnet-dev_03.yaml b/data/js-dnet-dev_03.yaml similarity index 100% rename from totrans/js-dnet-dev_03.yaml rename to data/js-dnet-dev_03.yaml diff --git a/totrans/js-dnet-dev_04.yaml b/data/js-dnet-dev_04.yaml similarity index 100% rename from totrans/js-dnet-dev_04.yaml rename to data/js-dnet-dev_04.yaml diff --git a/totrans/js-dnet-dev_05.yaml b/data/js-dnet-dev_05.yaml similarity index 100% rename from totrans/js-dnet-dev_05.yaml rename to data/js-dnet-dev_05.yaml diff --git a/totrans/js-dnet-dev_06.yaml b/data/js-dnet-dev_06.yaml similarity index 100% rename from totrans/js-dnet-dev_06.yaml rename to data/js-dnet-dev_06.yaml diff --git a/totrans/js-dnet-dev_07.yaml b/data/js-dnet-dev_07.yaml similarity index 100% rename from totrans/js-dnet-dev_07.yaml rename to data/js-dnet-dev_07.yaml diff --git a/totrans/js-dnet-dev_08.yaml b/data/js-dnet-dev_08.yaml similarity index 100% rename from totrans/js-dnet-dev_08.yaml rename to data/js-dnet-dev_08.yaml diff --git a/totrans/js-dnet-dev_09.yaml b/data/js-dnet-dev_09.yaml similarity index 100% rename from totrans/js-dnet-dev_09.yaml rename to data/js-dnet-dev_09.yaml diff --git a/totrans/js-dnet-dev_10.yaml b/data/js-dnet-dev_10.yaml similarity index 100% rename from totrans/js-dnet-dev_10.yaml rename to data/js-dnet-dev_10.yaml diff --git a/totrans/js-json-cb_00.yaml b/data/js-json-cb_00.yaml similarity index 100% rename from totrans/js-json-cb_00.yaml rename to data/js-json-cb_00.yaml diff --git a/totrans/js-json-cb_01.yaml b/data/js-json-cb_01.yaml similarity index 100% rename from totrans/js-json-cb_01.yaml rename to data/js-json-cb_01.yaml diff --git a/totrans/js-json-cb_02.yaml b/data/js-json-cb_02.yaml similarity index 100% rename from totrans/js-json-cb_02.yaml rename to data/js-json-cb_02.yaml diff --git a/totrans/js-json-cb_03.yaml b/data/js-json-cb_03.yaml similarity index 100% rename from totrans/js-json-cb_03.yaml rename to data/js-json-cb_03.yaml diff --git a/totrans/js-json-cb_04.yaml b/data/js-json-cb_04.yaml similarity index 100% rename from totrans/js-json-cb_04.yaml rename to data/js-json-cb_04.yaml diff --git a/totrans/js-json-cb_05.yaml b/data/js-json-cb_05.yaml similarity index 100% rename from totrans/js-json-cb_05.yaml rename to data/js-json-cb_05.yaml diff --git a/totrans/js-json-cb_06.yaml b/data/js-json-cb_06.yaml similarity index 100% rename from totrans/js-json-cb_06.yaml rename to data/js-json-cb_06.yaml diff --git a/totrans/js-json-cb_07.yaml b/data/js-json-cb_07.yaml similarity index 100% rename from totrans/js-json-cb_07.yaml rename to data/js-json-cb_07.yaml diff --git a/totrans/js-json-cb_08.yaml b/data/js-json-cb_08.yaml similarity index 100% rename from totrans/js-json-cb_08.yaml rename to data/js-json-cb_08.yaml diff --git a/totrans/js-json-cb_09.yaml b/data/js-json-cb_09.yaml similarity index 100% rename from totrans/js-json-cb_09.yaml rename to data/js-json-cb_09.yaml diff --git a/totrans/js-json-cb_10.yaml b/data/js-json-cb_10.yaml similarity index 100% rename from totrans/js-json-cb_10.yaml rename to data/js-json-cb_10.yaml diff --git a/totrans/js-prms-ess_0.yaml b/data/js-prms-ess_0.yaml similarity index 100% rename from totrans/js-prms-ess_0.yaml rename to data/js-prms-ess_0.yaml diff --git a/totrans/js-prms-ess_1.yaml b/data/js-prms-ess_1.yaml similarity index 100% rename from totrans/js-prms-ess_1.yaml rename to data/js-prms-ess_1.yaml diff --git a/totrans/js-prms-ess_2.yaml b/data/js-prms-ess_2.yaml similarity index 100% rename from totrans/js-prms-ess_2.yaml rename to data/js-prms-ess_2.yaml diff --git a/totrans/js-prms-ess_3.yaml b/data/js-prms-ess_3.yaml similarity index 100% rename from totrans/js-prms-ess_3.yaml rename to data/js-prms-ess_3.yaml diff --git a/totrans/js-prms-ess_4.yaml b/data/js-prms-ess_4.yaml similarity index 100% rename from totrans/js-prms-ess_4.yaml rename to data/js-prms-ess_4.yaml diff --git a/totrans/js-prms-ess_5.yaml b/data/js-prms-ess_5.yaml similarity index 100% rename from totrans/js-prms-ess_5.yaml rename to data/js-prms-ess_5.yaml diff --git a/totrans/js-prms-ess_6.yaml b/data/js-prms-ess_6.yaml similarity index 100% rename from totrans/js-prms-ess_6.yaml rename to data/js-prms-ess_6.yaml diff --git a/totrans/js-scl_00.yaml b/data/js-scl_00.yaml similarity index 100% rename from totrans/js-scl_00.yaml rename to data/js-scl_00.yaml diff --git a/totrans/js-scl_01.yaml b/data/js-scl_01.yaml similarity index 100% rename from totrans/js-scl_01.yaml rename to data/js-scl_01.yaml diff --git a/totrans/js-scl_02.yaml b/data/js-scl_02.yaml similarity index 100% rename from totrans/js-scl_02.yaml rename to data/js-scl_02.yaml diff --git a/totrans/js-scl_03.yaml b/data/js-scl_03.yaml similarity index 100% rename from totrans/js-scl_03.yaml rename to data/js-scl_03.yaml diff --git a/totrans/js-scl_04.yaml b/data/js-scl_04.yaml similarity index 100% rename from totrans/js-scl_04.yaml rename to data/js-scl_04.yaml diff --git a/totrans/js-scl_05.yaml b/data/js-scl_05.yaml similarity index 100% rename from totrans/js-scl_05.yaml rename to data/js-scl_05.yaml diff --git a/totrans/js-scl_06.yaml b/data/js-scl_06.yaml similarity index 100% rename from totrans/js-scl_06.yaml rename to data/js-scl_06.yaml diff --git a/totrans/js-scl_07.yaml b/data/js-scl_07.yaml similarity index 100% rename from totrans/js-scl_07.yaml rename to data/js-scl_07.yaml diff --git a/totrans/js-scl_08.yaml b/data/js-scl_08.yaml similarity index 100% rename from totrans/js-scl_08.yaml rename to data/js-scl_08.yaml diff --git a/totrans/js-scl_09.yaml b/data/js-scl_09.yaml similarity index 100% rename from totrans/js-scl_09.yaml rename to data/js-scl_09.yaml diff --git a/totrans/js-scl_10.yaml b/data/js-scl_10.yaml similarity index 100% rename from totrans/js-scl_10.yaml rename to data/js-scl_10.yaml diff --git a/totrans/js-test-bgd_0.yaml b/data/js-test-bgd_0.yaml similarity index 100% rename from totrans/js-test-bgd_0.yaml rename to data/js-test-bgd_0.yaml diff --git a/totrans/js-test-bgd_1.yaml b/data/js-test-bgd_1.yaml similarity index 100% rename from totrans/js-test-bgd_1.yaml rename to data/js-test-bgd_1.yaml diff --git a/totrans/js-test-bgd_2.yaml b/data/js-test-bgd_2.yaml similarity index 100% rename from totrans/js-test-bgd_2.yaml rename to data/js-test-bgd_2.yaml diff --git a/totrans/js-test-bgd_3.yaml b/data/js-test-bgd_3.yaml similarity index 100% rename from totrans/js-test-bgd_3.yaml rename to data/js-test-bgd_3.yaml diff --git a/totrans/js-test-bgd_4.yaml b/data/js-test-bgd_4.yaml similarity index 100% rename from totrans/js-test-bgd_4.yaml rename to data/js-test-bgd_4.yaml diff --git a/totrans/js-test-bgd_5.yaml b/data/js-test-bgd_5.yaml similarity index 100% rename from totrans/js-test-bgd_5.yaml rename to data/js-test-bgd_5.yaml diff --git a/totrans/js-test-bgd_6.yaml b/data/js-test-bgd_6.yaml similarity index 100% rename from totrans/js-test-bgd_6.yaml rename to data/js-test-bgd_6.yaml diff --git a/totrans/js-test-bgd_7.yaml b/data/js-test-bgd_7.yaml similarity index 100% rename from totrans/js-test-bgd_7.yaml rename to data/js-test-bgd_7.yaml diff --git a/totrans/js-test-bgd_8.yaml b/data/js-test-bgd_8.yaml similarity index 100% rename from totrans/js-test-bgd_8.yaml rename to data/js-test-bgd_8.yaml diff --git a/totrans/js-ulk_00.yaml b/data/js-ulk_00.yaml similarity index 100% rename from totrans/js-ulk_00.yaml rename to data/js-ulk_00.yaml diff --git a/totrans/js-ulk_01.yaml b/data/js-ulk_01.yaml similarity index 100% rename from totrans/js-ulk_01.yaml rename to data/js-ulk_01.yaml diff --git a/totrans/js-ulk_02.yaml b/data/js-ulk_02.yaml similarity index 100% rename from totrans/js-ulk_02.yaml rename to data/js-ulk_02.yaml diff --git a/totrans/js-ulk_03.yaml b/data/js-ulk_03.yaml similarity index 100% rename from totrans/js-ulk_03.yaml rename to data/js-ulk_03.yaml diff --git a/totrans/js-ulk_04.yaml b/data/js-ulk_04.yaml similarity index 100% rename from totrans/js-ulk_04.yaml rename to data/js-ulk_04.yaml diff --git a/totrans/js-ulk_05.yaml b/data/js-ulk_05.yaml similarity index 100% rename from totrans/js-ulk_05.yaml rename to data/js-ulk_05.yaml diff --git a/totrans/js-ulk_06.yaml b/data/js-ulk_06.yaml similarity index 100% rename from totrans/js-ulk_06.yaml rename to data/js-ulk_06.yaml diff --git a/totrans/js-ulk_07.yaml b/data/js-ulk_07.yaml similarity index 100% rename from totrans/js-ulk_07.yaml rename to data/js-ulk_07.yaml diff --git a/totrans/js-ulk_08.yaml b/data/js-ulk_08.yaml similarity index 100% rename from totrans/js-ulk_08.yaml rename to data/js-ulk_08.yaml diff --git a/totrans/js-ulk_09.yaml b/data/js-ulk_09.yaml similarity index 100% rename from totrans/js-ulk_09.yaml rename to data/js-ulk_09.yaml diff --git a/totrans/lrn-aurelia_00.yaml b/data/lrn-aurelia_00.yaml similarity index 100% rename from totrans/lrn-aurelia_00.yaml rename to data/lrn-aurelia_00.yaml diff --git a/totrans/lrn-aurelia_01.yaml b/data/lrn-aurelia_01.yaml similarity index 100% rename from totrans/lrn-aurelia_01.yaml rename to data/lrn-aurelia_01.yaml diff --git a/totrans/lrn-aurelia_02.yaml b/data/lrn-aurelia_02.yaml similarity index 100% rename from totrans/lrn-aurelia_02.yaml rename to data/lrn-aurelia_02.yaml diff --git a/totrans/lrn-aurelia_03.yaml b/data/lrn-aurelia_03.yaml similarity index 100% rename from totrans/lrn-aurelia_03.yaml rename to data/lrn-aurelia_03.yaml diff --git a/totrans/lrn-aurelia_04.yaml b/data/lrn-aurelia_04.yaml similarity index 100% rename from totrans/lrn-aurelia_04.yaml rename to data/lrn-aurelia_04.yaml diff --git a/totrans/lrn-aurelia_05.yaml b/data/lrn-aurelia_05.yaml similarity index 100% rename from totrans/lrn-aurelia_05.yaml rename to data/lrn-aurelia_05.yaml diff --git a/totrans/lrn-aurelia_06.yaml b/data/lrn-aurelia_06.yaml similarity index 100% rename from totrans/lrn-aurelia_06.yaml rename to data/lrn-aurelia_06.yaml diff --git a/totrans/lrn-aurelia_07.yaml b/data/lrn-aurelia_07.yaml similarity index 100% rename from totrans/lrn-aurelia_07.yaml rename to data/lrn-aurelia_07.yaml diff --git a/totrans/lrn-aurelia_08.yaml b/data/lrn-aurelia_08.yaml similarity index 100% rename from totrans/lrn-aurelia_08.yaml rename to data/lrn-aurelia_08.yaml diff --git a/totrans/lrn-aurelia_09.yaml b/data/lrn-aurelia_09.yaml similarity index 100% rename from totrans/lrn-aurelia_09.yaml rename to data/lrn-aurelia_09.yaml diff --git a/totrans/lrn-aurelia_10.yaml b/data/lrn-aurelia_10.yaml similarity index 100% rename from totrans/lrn-aurelia_10.yaml rename to data/lrn-aurelia_10.yaml diff --git a/totrans/lrn-aurelia_11.yaml b/data/lrn-aurelia_11.yaml similarity index 100% rename from totrans/lrn-aurelia_11.yaml rename to data/lrn-aurelia_11.yaml diff --git a/totrans/lrn-aurelia_12.yaml b/data/lrn-aurelia_12.yaml similarity index 100% rename from totrans/lrn-aurelia_12.yaml rename to data/lrn-aurelia_12.yaml diff --git a/totrans/lrn-aurelia_13.yaml b/data/lrn-aurelia_13.yaml similarity index 100% rename from totrans/lrn-aurelia_13.yaml rename to data/lrn-aurelia_13.yaml diff --git a/totrans/ms-js-hiperf_00.yaml b/data/ms-js-hiperf_00.yaml similarity index 100% rename from totrans/ms-js-hiperf_00.yaml rename to data/ms-js-hiperf_00.yaml diff --git a/totrans/ms-js-hiperf_01.yaml b/data/ms-js-hiperf_01.yaml similarity index 100% rename from totrans/ms-js-hiperf_01.yaml rename to data/ms-js-hiperf_01.yaml diff --git a/totrans/ms-js-hiperf_02.yaml b/data/ms-js-hiperf_02.yaml similarity index 100% rename from totrans/ms-js-hiperf_02.yaml rename to data/ms-js-hiperf_02.yaml diff --git a/totrans/ms-js-hiperf_03.yaml b/data/ms-js-hiperf_03.yaml similarity index 100% rename from totrans/ms-js-hiperf_03.yaml rename to data/ms-js-hiperf_03.yaml diff --git a/totrans/ms-js-hiperf_04.yaml b/data/ms-js-hiperf_04.yaml similarity index 100% rename from totrans/ms-js-hiperf_04.yaml rename to data/ms-js-hiperf_04.yaml diff --git a/totrans/ms-js-hiperf_05.yaml b/data/ms-js-hiperf_05.yaml similarity index 100% rename from totrans/ms-js-hiperf_05.yaml rename to data/ms-js-hiperf_05.yaml diff --git a/totrans/ms-js-hiperf_06.yaml b/data/ms-js-hiperf_06.yaml similarity index 100% rename from totrans/ms-js-hiperf_06.yaml rename to data/ms-js-hiperf_06.yaml diff --git a/totrans/ms-js-hiperf_07.yaml b/data/ms-js-hiperf_07.yaml similarity index 100% rename from totrans/ms-js-hiperf_07.yaml rename to data/ms-js-hiperf_07.yaml diff --git a/totrans/ms-js-hiperf_08.yaml b/data/ms-js-hiperf_08.yaml similarity index 100% rename from totrans/ms-js-hiperf_08.yaml rename to data/ms-js-hiperf_08.yaml diff --git a/totrans/ms-js-hiperf_09.yaml b/data/ms-js-hiperf_09.yaml similarity index 100% rename from totrans/ms-js-hiperf_09.yaml rename to data/ms-js-hiperf_09.yaml diff --git a/totrans/ms-js-hiperf_10.yaml b/data/ms-js-hiperf_10.yaml similarity index 100% rename from totrans/ms-js-hiperf_10.yaml rename to data/ms-js-hiperf_10.yaml diff --git a/totrans/ms-js-prms_00.yaml b/data/ms-js-prms_00.yaml similarity index 100% rename from totrans/ms-js-prms_00.yaml rename to data/ms-js-prms_00.yaml diff --git a/totrans/ms-js-prms_01.yaml b/data/ms-js-prms_01.yaml similarity index 100% rename from totrans/ms-js-prms_01.yaml rename to data/ms-js-prms_01.yaml diff --git a/totrans/ms-js-prms_02.yaml b/data/ms-js-prms_02.yaml similarity index 100% rename from totrans/ms-js-prms_02.yaml rename to data/ms-js-prms_02.yaml diff --git a/totrans/ms-js-prms_03.yaml b/data/ms-js-prms_03.yaml similarity index 100% rename from totrans/ms-js-prms_03.yaml rename to data/ms-js-prms_03.yaml diff --git a/totrans/ms-js-prms_04.yaml b/data/ms-js-prms_04.yaml similarity index 100% rename from totrans/ms-js-prms_04.yaml rename to data/ms-js-prms_04.yaml diff --git a/totrans/ms-js-prms_05.yaml b/data/ms-js-prms_05.yaml similarity index 100% rename from totrans/ms-js-prms_05.yaml rename to data/ms-js-prms_05.yaml diff --git a/totrans/ms-js-prms_06.yaml b/data/ms-js-prms_06.yaml similarity index 100% rename from totrans/ms-js-prms_06.yaml rename to data/ms-js-prms_06.yaml diff --git a/totrans/ms-js-prms_07.yaml b/data/ms-js-prms_07.yaml similarity index 100% rename from totrans/ms-js-prms_07.yaml rename to data/ms-js-prms_07.yaml diff --git a/totrans/ms-js-prms_08.yaml b/data/ms-js-prms_08.yaml similarity index 100% rename from totrans/ms-js-prms_08.yaml rename to data/ms-js-prms_08.yaml diff --git a/totrans/ms-js-prms_09.yaml b/data/ms-js-prms_09.yaml similarity index 100% rename from totrans/ms-js-prms_09.yaml rename to data/ms-js-prms_09.yaml diff --git a/totrans/ms-js_00.yaml b/data/ms-js_00.yaml similarity index 100% rename from totrans/ms-js_00.yaml rename to data/ms-js_00.yaml diff --git a/totrans/ms-js_02.yaml b/data/ms-js_02.yaml similarity index 100% rename from totrans/ms-js_02.yaml rename to data/ms-js_02.yaml diff --git a/totrans/ms-js_03.yaml b/data/ms-js_03.yaml similarity index 100% rename from totrans/ms-js_03.yaml rename to data/ms-js_03.yaml diff --git a/totrans/ms-js_04.yaml b/data/ms-js_04.yaml similarity index 100% rename from totrans/ms-js_04.yaml rename to data/ms-js_04.yaml diff --git a/totrans/ms-js_05.yaml b/data/ms-js_05.yaml similarity index 100% rename from totrans/ms-js_05.yaml rename to data/ms-js_05.yaml diff --git a/totrans/ms-js_06.yaml b/data/ms-js_06.yaml similarity index 100% rename from totrans/ms-js_06.yaml rename to data/ms-js_06.yaml diff --git a/totrans/ms-js_07.yaml b/data/ms-js_07.yaml similarity index 100% rename from totrans/ms-js_07.yaml rename to data/ms-js_07.yaml diff --git a/totrans/ms-js_08.yaml b/data/ms-js_08.yaml similarity index 100% rename from totrans/ms-js_08.yaml rename to data/ms-js_08.yaml diff --git a/totrans/ms-js_09.yaml b/data/ms-js_09.yaml similarity index 100% rename from totrans/ms-js_09.yaml rename to data/ms-js_09.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_00.yaml b/data/sencha-tch2-mobi-js-fw_00.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_00.yaml rename to data/sencha-tch2-mobi-js-fw_00.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_01.yaml b/data/sencha-tch2-mobi-js-fw_01.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_01.yaml rename to data/sencha-tch2-mobi-js-fw_01.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_02.yaml b/data/sencha-tch2-mobi-js-fw_02.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_02.yaml rename to data/sencha-tch2-mobi-js-fw_02.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_03.yaml b/data/sencha-tch2-mobi-js-fw_03.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_03.yaml rename to data/sencha-tch2-mobi-js-fw_03.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_04.yaml b/data/sencha-tch2-mobi-js-fw_04.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_04.yaml rename to data/sencha-tch2-mobi-js-fw_04.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_05.yaml b/data/sencha-tch2-mobi-js-fw_05.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_05.yaml rename to data/sencha-tch2-mobi-js-fw_05.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_06.yaml b/data/sencha-tch2-mobi-js-fw_06.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_06.yaml rename to data/sencha-tch2-mobi-js-fw_06.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_07.yaml b/data/sencha-tch2-mobi-js-fw_07.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_07.yaml rename to data/sencha-tch2-mobi-js-fw_07.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_08.yaml b/data/sencha-tch2-mobi-js-fw_08.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_08.yaml rename to data/sencha-tch2-mobi-js-fw_08.yaml diff --git a/totrans/sencha-tch2-mobi-js-fw_09.yaml b/data/sencha-tch2-mobi-js-fw_09.yaml similarity index 100% rename from totrans/sencha-tch2-mobi-js-fw_09.yaml rename to data/sencha-tch2-mobi-js-fw_09.yaml diff --git a/totrans/soc-dtvis-j5-js_0.yaml b/data/soc-dtvis-j5-js_0.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_0.yaml rename to data/soc-dtvis-j5-js_0.yaml diff --git a/totrans/soc-dtvis-j5-js_1.yaml b/data/soc-dtvis-j5-js_1.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_1.yaml rename to data/soc-dtvis-j5-js_1.yaml diff --git a/totrans/soc-dtvis-j5-js_2.yaml b/data/soc-dtvis-j5-js_2.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_2.yaml rename to data/soc-dtvis-j5-js_2.yaml diff --git a/totrans/soc-dtvis-j5-js_3.yaml b/data/soc-dtvis-j5-js_3.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_3.yaml rename to data/soc-dtvis-j5-js_3.yaml diff --git a/totrans/soc-dtvis-j5-js_4.yaml b/data/soc-dtvis-j5-js_4.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_4.yaml rename to data/soc-dtvis-j5-js_4.yaml diff --git a/totrans/soc-dtvis-j5-js_5.yaml b/data/soc-dtvis-j5-js_5.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_5.yaml rename to data/soc-dtvis-j5-js_5.yaml diff --git a/totrans/soc-dtvis-j5-js_6.yaml b/data/soc-dtvis-j5-js_6.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_6.yaml rename to data/soc-dtvis-j5-js_6.yaml diff --git a/totrans/soc-dtvis-j5-js_7.yaml b/data/soc-dtvis-j5-js_7.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_7.yaml rename to data/soc-dtvis-j5-js_7.yaml diff --git a/totrans/soc-dtvis-j5-js_8.yaml b/data/soc-dtvis-j5-js_8.yaml similarity index 100% rename from totrans/soc-dtvis-j5-js_8.yaml rename to data/soc-dtvis-j5-js_8.yaml diff --git a/totrans/tnk-js_0.yaml b/data/tnk-js_0.yaml similarity index 100% rename from totrans/tnk-js_0.yaml rename to data/tnk-js_0.yaml diff --git a/totrans/tnk-js_1.yaml b/data/tnk-js_1.yaml similarity index 100% rename from totrans/tnk-js_1.yaml rename to data/tnk-js_1.yaml diff --git a/docs/bd-sgl-pg-webapp-mtr/SUMMARY.md b/docs/bd-sgl-pg-webapp-mtr/SUMMARY.md new file mode 100644 index 0000000..907f42b --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/SUMMARY.md @@ -0,0 +1,13 @@ ++ [序言](bd-sgl-pg-webapp-mtr_00.md) ++ [第一章. 开始使用 Meteor](bd-sgl-pg-webapp-mtr_01.md) ++ [第二章. 构建 HTML 模板](bd-sgl-pg-webapp-mtr_02.md) ++ [第三章。存储数据和处理集合](bd-sgl-pg-webapp-mtr_03.md) ++ [第四章。控制数据流](bd-sgl-pg-webapp-mtr_04.md) ++ [第五章。使用路由使我们的应用具有灵活性](bd-sgl-pg-webapp-mtr_05.md) ++ [第六章。使用会话保持状态](bd-sgl-pg-webapp-mtr_06.md) ++ [第七章。用户和权限](bd-sgl-pg-webapp-mtr_07.md) ++ [第八章。使用允许和拒绝规则进行安全设置](bd-sgl-pg-webapp-mtr_08.md) ++ [第九章。高级响应式](bd-sgl-pg-webapp-mtr_09.md) ++ [第十章 部署我们的应用程序](bd-sgl-pg-webapp-mtr_10.md) ++ [第十一章。构建我们自己的包](bd-sgl-pg-webapp-mtr_11.md) ++ [第十二章. Meteor 中的测试](bd-sgl-pg-webapp-mtr_12.md) \ No newline at end of file diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_00.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_00.md new file mode 100644 index 0000000..34bbad8 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_00.md @@ -0,0 +1,144 @@ +# 序言 + +感谢您购买这本书。您为前端和 JavaScript 技术的新一步做出了明智的选择。Meteor 框架不仅仅是为了简化事情而出现的另一个库。它为 Web 服务器、客户端逻辑和模板提供了一个完整的解决方案。此外,它还包含了一个完整的构建过程,这将使通过块状方式为 Web 工作变得更快。多亏了 Meteor,链接您的脚本和样式已经成为过去,因为自动构建过程会为您处理所有事情。这确实是一个很大的改变,但您很快就会喜欢上它,因为它使扩展应用程序的速度与创建新文件一样快。 + +Meteor 旨在创建单页应用程序,其中实时是默认值。它负责数据同步和 DOM 的更新。如果数据发生变化,您的屏幕将进行更新。这两个基本概念构成了我们作为网页开发者所做的很多工作,而 Meteor 则无需编写任何额外的代码即可实现。 + +在我看来,Meteor 在现代网页开发中是一个完整的游戏改变者。它将以下模式作为默认值引入: + ++ 胖客户端:所有的逻辑都存在于客户端。HTML 仅在初始页面加载时发送 + ++ 在客户端和服务器上使用相同的 JavaScript 和 API + ++ 实时:数据自动同步到所有客户端 + ++ 一种“无处不在的数据库”方法,允许在客户端进行数据库查询 + ++ 作为 Web 服务器通信默认的发布/订阅模式 + +一旦你使用了我所介绍的所有这些新概念,你很难回到过去那种只花费时间准备应用程序结构,而链接文件或将它们封装为 Require.js 模块,编写端点以及编写请求和发送数据上下的代码的老方法。 + +在阅读这本书的过程中,您将逐步介绍这些概念以及它们是如何相互连接的。我们将建立一个带有后端编辑帖子的博客。博客是一个很好的例子,因为它使用了帖子列表、每个帖子的不同路由以及一个管理界面来添加新帖子,为我们提供了全面理解 Meteor 所需的所有内容。 + +# 本书涵盖内容 + +第一章,*Meteor 入门*,描述了安装和运行 Meteor 所需的步骤,同时还详细介绍了 Meteor 项目的文件结构,特别是我们将要构建的 Meteor 项目。 + +第二章,*构建 HTML 模板*,展示了如何使用 handlebar 这样的语法构建反应式模板,以及如何在其中显示数据是多么简单。 + +第三章,*存储数据和处理集合*,涵盖了服务器和客户端的数据库使用。 + +第四章, *数据流控制*, 介绍了 Meteor 的发布/订阅模式,该模式用于在服务器和客户端之间同步数据。 + +第五章, *使用路由使我们的应用具有多样性*, 教我们如何设置路由,以及如何让我们的应用表现得像一个真正的网站。 + +第六章, *使用会话保持状态*, 讨论了响应式会话对象及其使用方法。 + +第七章, *用户和权限*, 描述了用户的创建以及登录过程是如何工作的。此时,我们将为我们的博客创建后端部分。 + +第八章, *使用 Allow 和 Deny 规则进行安全控制*, 介绍了如何限制数据流仅对某些用户开放,以防止所有人对我们的数据库进行更改。 + +第九章, *高级响应性*, 展示了如何构建我们自己的自定义响应式对象,该对象可以根据时间间隔重新运行一个函数。 + +第十章, *部署我们的应用*, 介绍了如何使用 Meteor 自己的部署服务以及在自己的基础设施上部署应用。 + +第十一章, *构建我们自己的包*, 描述了如何编写一个包并将其发布到 Atmosphere,供所有人使用。 + +第十二章, *Meteor 中的测试*, 展示了如何使用 Meteor 自带的 tinytest 包进行包测试,以及如何使用第三方工具测试 Meteor 应用程序本身。 + +附录, 包含 Meteor 命令列表以及 iron:router 钩子及其描述。 + +# 本书需要的软件 + +为了跟随章节中的示例,你需要一个文本编辑器来编写代码。我强烈推荐 Sublime Text 作为你的集成开发环境,因为它有几乎涵盖每个任务的可扩展插件。 + +你还需要一个现代浏览器来查看你的结果。由于许多示例使用浏览器控制台来更改数据库以及查看代码片段的结果,我推荐使用 Google Chrome。其开发者工具网络检查器拥有一个 web 开发者需要的所有工具,以便轻松地工作和服务器调试网站。 + +此外,你可以使用 Git 和 GitHub 来存储你每一步的成功,以及为了回到代码的先前版本。 + +每个章节的代码示例也将发布在 GitHub 上,地址为[`github.com/frozeman/book-building-single-page-web-apps-with-meteor`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor),该仓库中的每个提交都与书中的一个章节相对应,为你提供了一种直观的方式来查看在每个步骤中添加和移除了哪些内容。 + +# 本书适合对象 + +这本书适合希望进入单页、实时应用新范式的 Web 开发者。你不需要成为 JavaScript 专业人士就能跟随书中的内容,但扎实的基本知识会让你发现这本书是个宝贵的伴侣。 + +如果你听说过 Meteor 但还没有使用过,这本书绝对适合你。它会教你所有你需要理解并成功使用 Meteor 的知识。如果你之前使用过 Meteor 但想要更深入的了解,那么最后一章将帮助你提高对自定义反应式对象和编写包的理解。目前 Meteor 社区中涉及最少的主题可能是测试,因此通过阅读最后一章,你将很容易理解如何使用自动化测试使你的应用更加健壮。 + +# 约定 + +在这本书中,你会发现多种用于区分不同信息类型的文本样式。以下是这些样式的几个示例及其含义解释。 + +文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、假 URL、用户输入和 Twitter 处理显示如下:"With Meteor, we never have to link files with the ` + + Hello World + +``` + +当我们希望引起你对代码块中特定部分的关注时,相关行或项目以粗体显示: + +```js + +``` + +任何命令行输入或输出如下所示: + +```js +$ cd my/developer/folder +$ meteor create my-meteor-blog + +``` + +**新术语**和**重要词汇**以粗体显示。例如,你在屏幕上看到的、在菜单或对话框中出现的词汇,在文本中显示为这样:"However, now when we go to our browser, we will still see **Hello World**." + +### 注意 + +警告或重要说明以这样的盒子形式出现。 + +### 提示 + +技巧和建议以这样的形式出现。 + +# 读者反馈 + +我们的读者的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者反馈对我们开发您真正能从中获得最大收益的书很重要。 + +发送一般反馈,只需发送电子邮件至``,并在消息主题中提到书名。 + +如果你需要我们出版某本书,并希望看到它,请在[www.packtpub.com](http://www.packtpub.com)上的**建议书名**表单中给我们留言,或者发送电子邮件至``。 + +如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请查看我们在[www.packtpub.com/authors](http://www.packtpub.com/authors)上的作者指南。 + +# 客户支持 + +既然您已经成为 Packt 书籍的自豪拥有者,我们有很多东西可以帮助您充分利用您的购买。 + +## 下载示例代码 + +您可以通过您在[`www.packtpub.com`](http://www.packtpub.com)的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问[`www.packtpub.com/support`](http://www.packtpub.com/support)并注册,以便我们将文件直接通过电子邮件发送给您。 + +## 错误更正 + +尽管我们已经尽一切努力确保我们的内容的准确性,但错误确实会发生。如果您在我们的书中发现一个错误——也许是在文本或代码中——我们将非常感谢您能向我们报告。这样做可以节省其他读者的挫折感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问[`www.packtpub.com/submit-errata`](http://www.packtpub.com/submit-errata),选择您的书籍,点击**错误提交表单**链接,并输入您错误的详细信息。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误部分现有的错误列表中。 + +要查看之前提交的错误更正,请前往[`www.packtpub.com/books/content/support`](https://www.packtpub.com/books/content/support)并在搜索字段中输入书籍的名称。所需信息将在**错误更正**部分下出现。 + +## 盗版 + +互联网上的版权材料盗版是一个持续存在的问题,所有媒体都受到影响。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上以任何形式发现我们作品的非法副本,请立即提供给我们地址或网站名称,以便我们可以寻求解决方案。 + +请通过``联系我们,并提供疑似被盗材料的链接。 + +我们感谢您在保护我们的作者和我们提供有价值内容的能力方面所提供的帮助。 + +## 问题 + +如果您在这本书的任何一个方面遇到问题,可以通过``联系我们,我们会尽力解决问题。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_01.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_01.md new file mode 100644 index 0000000..49e6d12 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_01.md @@ -0,0 +1,423 @@ +# 第一章. 开始使用 Meteor + +欢迎来到关于 Meteor 的这本书。Meteor 是一个令人兴奋的新 JavaScript 框架,我们将很快看到如何用更少的代码实现真实且令人印象深刻的结果。 + +在本章中,我们将学习系统要求以及我们开始需要使用哪些额外的工具。我们将了解如何轻松地运行我们的第一个 Meteor 应用程序,以及一个 Meteor 应用程序可能的良好基本文件夹结构。我们还将了解 Meteor 的自动构建过程及其特定的文件加载方式。 + +我们还将了解如何使用 Meteor 官方的包管理系统添加包。在本章末尾,我们将简要查看 Meteor 的命令行工具及其一些功能。 + +为了总结,我们将涵盖以下主题: + ++ Meteor 的全栈框架 + ++ Meteor 的系统要求 + ++ 安装 Meteor + ++ 添加基本包 + ++ Meteor 的文件夹约定和加载顺序 + ++ Meteor 的命令行工具 + +# Meteor 的全栈框架 + +Meteor 不仅仅是一个像 jQuery 或 AngularJS 这样的 JavaScript 库。它是一个包含前端库、基于 Node.js 的服务器和命令行工具的全栈解决方案。所有这些加在一起让我们可以用 JavaScript 编写大规模的网络应用程序,无论是在服务器端还是客户端,都可以使用一致的 API。 + +尽管 Meteor 还相当年轻,但已经有几家公司,如[`lookback.io`](https://lookback.io)、[`respond.ly`](https://respond.ly)和[`madeye.io`](https://madeye.io),在其生产环境中使用 Meteor。 + +如果你想亲自看看用 Meteor 制作的东西,请查看[`madewith.meteor.com`](http://madewith.meteor.com)。 + +Meteor 使我们能够快速构建网络应用程序,并处理诸如文件链接、文件压缩和文件合并等无聊的过程。 + +以下是在 Meteor 下可以实现的一些亮点: + ++ 我们可以使用模板来构建复杂的网络应用程序,这些模板在数据更改时会自动更新,从而大大提高速度。 + ++ 在我们应用程序运行的同时,我们可以将新代码推送到所有客户端。 + ++ Meteor 的核心包带有一个完整的账户解决方案,允许与 Facebook、Twitter 等无缝集成。 + ++ 数据将自动在客户端之间同步,几乎实时地保持每个客户端在相同的状态。 + ++ 延迟补偿将使我们的界面在服务器响应后台进行时看起来超级快速。 + +使用 Meteor 时,我们永远不需要在 HTML 的` + + Hello World + + ``` + + ### 注意 + + 请注意,我们的`index.html`文件不包含`...`标签,因为 Meteor 会收集任何文件中的``和``标签,并构建自己的`index.html`文件,该文件将交付给用户。实际上,我们还可以将此文件命名为`myapp.html`。 + +1. 接下来,我们通过在命令行中输入以下命令来运行我们的 Meteor 应用: + + ```js + $ cd my-meteor-blog + $ meteor + + ``` + + 这将启动一个带有我们应用的 Meteor 服务器。 + +1. 就这样!现在我们可以打开浏览器,导航到`http://localhost:3000`,我们应该能看到**Hello World**。 + +这里发生的是,Meteor 将查看我们应用文件夹中可用的所有 HTML 文件,合并所有找到的``和``标签的内容,并将其作为索引文件提供给客户端。 + +如果我们查看我们应用的源代码,我们会看到``标签是空的。这是因为 Meteor 将``标签的内容视为自己的模板,在 DOM 加载时,将与相应的 JavaScript 模板一起注入。 + +### 注意 + +要查看源代码,不要使用开发者工具的**元素面板**,因为这将显示 JavaScript 执行后的源代码。在 Chrome 中,右键单击网站,而选择**查看页面源代码**。 + +我们还会看到 Meteor 已经在我们的``标签中链接了各种各样的 JavaScript 文件。这些都是 Meteor 的核心包和我们的第三方包。在生产环境中,这些文件将被合并成一体。为了看到这个效果,打开终端,使用*Ctrl* + *C*退出我们运行中的 Meteor 服务器,并运行以下命令: + +```js +$ meteor --production + +``` + +如果我们现在查看源代码,我们会看到只有一个神秘的 JavaScript 文件被链接。 + +接下来,最好是通过简单地退出 Meteor 并再次运行`meteor`命令回到我们的开发者模式,因为这样在文件发生变化时可以更快地重新加载应用。 + +# 构建基本模板 + +现在,让我们通过在我们`my-meteor-blog/client/templates`文件夹中创建一个名为`layout.html`的文件,将基本模板添加到我们的博客中。这个模板将作为我们博客布局的包装模板。要构建基本模板,请执行以下步骤: + +1. 在刚刚创建的`layout.html`中添加以下代码行: + + ```js + + ``` + +1. 接下来,我们将创建主页模板,稍后列出我们所有的博客文章。在`layout.html`相同的模板文件夹中,我们将创建一个名为`home.html`的文件,并包含以下代码行: + + ```js + + ``` + +1. 下一个文件将是一个简单的**关于**页面,我们将其保存为`about.html`,并包含以下代码片段: + + ```js + + ``` + + 正如您所见,我们使用了一个`{{#markdown}}`块助手来包装我们的文本。大括号是 Blaze 用来将逻辑带到 HTML 的处理程序语法。`{{#markdown}}...{{/markdown}}`块在模板渲染时将所有的 Markdown 语法转换成 HTML。 + + ### 注意 + + 由于 Markdown 语法将缩进解释为代码,因此 Markdown 文本不能像我们对 HTML 标签那样进行缩进。 + +1. 为了能够使用`{{#markdown}}`块助手,我们首先需要将`markdown`核心包添加到我们的应用程序中。为此,我们使用*Ctrl* + *C*在终端中停止正在运行的应用程序,并输入以下命令: + + ```js + $ meteor add markdown + + ``` + +1. 现在我们可以再次运行`meteor`命令来启动我们的服务器。 + +然而,当我们现在打开浏览器时,我们仍然会看到**Hello World**。那么我们如何使我们的模板现在变得可见呢? + +# 添加模板和部分 + +为了在应用程序中显示主页模板,我们需要打开之前创建的`index.html`,并执行以下步骤: + +1. 我们将`Hello World`替换为以下模板包含助手: + + ```js + {{> layout}} + ``` + +1. 如果我们现在回到浏览器,我们会看到文本消失了,而我们之前创建的`layout`模板以及其标题和菜单出现了。 + +1. 为了完成页面,我们需要在`layout`模板中显示`home`模板。我们只需在`layout`模板的`main`部分添加另一个模板包含助手,如下所示: + + ```js +
+ {{> home}} +
+ ``` + +1. 如果我们回到浏览器,我们应该看到以下截图:![Adding templates and partials](img/00004.jpeg) + +如果我们现在将`{{> home}}`替换为`{{> about}}`,我们将会看到我们的`about`模板。 + +# 使用模板助手显示数据 + +每个模板都可以有函数,这些函数被称为`template`助手,它们可以在模板及其子模板中使用。 + +除了我们自定义的助手函数外,还有三个回调函数在模板创建、渲染和销毁时被调用。要使用模板助手显示数据,请执行以下步骤: + +1. 为了看到这三个回调函数的作用,让我们创建一个名为`home.js`的文件,并将其保存到我们的`my-meteor-blog/client/templates/`文件夹中,并包含以下代码片段: + + ```js + Template.home.created = function(){ + console.log('Created the home template'); + }; + Template.home.rendered = function(){ + console.log('Rendered the home template'); + }; + + Template.home.destroyed = function(){ + console.log('Destroyed the home template'); + }; + ``` + + 如果我们现在打开浏览器的控制台,我们会看到前两个回调被触发。最后一个只有在动态移除模板时才会触发。 + +1. 为了在`home`模板中显示数据,我们将创建一个助手函数,该函数将返回一个简单的字符串,如下所示: + + ```js + Template.home.helpers({ + exampleHelper: function(){ + return 'This text came from a helper with some HTML.'; + } + }); + ``` + +1. 现在如果我们去我们的`home.html`文件,在`{{markdown}}`块助手之后添加`{{exampleHelper}}`助手,并保存文件,我们将在浏览器中看到出现的字符串,但我们注意到 HTML 被转义了。 + +1. 为了使 Meteor 正确渲染 HTML,我们可以简单地将双花括号替换为三花括号,如下代码行所示,Blaze 不会让 HTML 转义: + + ```js + {{{exampleHelper}}} + ``` + + ### 注意 + + 注意,在我们的大多数模板助手中,我们*不应该*使用三花括号`{{{...}}}`,因为这将打开 XSS 和其他攻击的大门。只有当返回的 HTML 安全可渲染时才使用它。 + +1. 此外,我们可以使用双花括号返回未转义的 HTML,但我们需要返回通过`SpaceBars.SafeString`函数传递的字符串,如下例所示: + + ```js + Template.home.helpers({ + exampleHelper: function(){ + return new Spacebars.SafeString('This text came from a helper with some HTML.'); + } + }); + ``` + +# 为模板设置数据上下文 + ++ 现在我们已经有了`contextExample`模板,我们可以通过传递一些数据将其添加到我们的`home`模板中,如下所示: + + ```js + {{> contextExample someText="I was set in the parent template's helper, as an argument."}} + ``` + + 这将在`contextExample`模板中显示文本,因为我们使用`{{someText}}`来显示它。 + + ### 提示 + + 记住,文件名实际上并不重要,因为 Meteor 会无论如何收集并连接它们;然而,模板名称很重要,因为我们用这个来引用模板。 + + 在 HTML 中设置上下文不是非常动态,因为它是有硬编码的。为了能够动态地改变上下文,最好使用`template`助手函数来设置它。 + + + 为此,我们必须首先将助手添加到我们的`home`模板助手中,该助手返回数据上下文,如下所示: + + ```js + Template.home.helpers({ + // other helpers ... + dataContextHelper: function(){ + return { + someText: 'This text was set using a helper of the parent template.', + someNested: { + text: 'That comes from "someNested.text"' + } + }; + } + }); + ``` + + + 现在我们可以将此助手作为数据上下文添加到我们的`contextExample`模板包含助手中,如下所示: + + ```js + {{> contextExample dataContextHelper}} + ``` + + + 另外,为了显示我们返回的嵌套数据对象,我们可以在`contextExample`模板中使用 Blaze 点语法,通过在模板中添加以下代码行来实现: + + ```js +

{{someNested.text}}

+ ``` + +这现在将显示`someText`和`someNested.text`,后者是由我们的助手函数返回的。 + +## 使用`{{#with}}`块助手 + +设置数据上下文的一种另一种方法是使用`{{#with}}`块助手。以下代码片段与之前使用助手函数的包含助手具有相同的结果: + +```js +{{#with dataContextHelper}} + {{> contextExample}} +{{/with}} +``` + +我们甚至在浏览器中得到同样的结果,当我们不使用子模板,只是将`contextExample`模板的内容添加到`{{#with}}`块助手中,如下所示: + +```js +{{#with dataContextHelper}} +

{{someText}}

+

{{someNested.text}}

+{{/with}} +``` + +# 模板助手和模板回调中的"this" + +在 Meteor 中,模板助手中的`this`在模板回调(如`created()`、`rendered()`和`destroyed()`)中的使用方式不同。 + +如前所述,模板有三个回调函数,在模板的不同状态下触发: + ++ `created`:当模板初始化但尚未插入 DOM 时触发 + ++ `rendered`:当模板及其所有子模板附加到 DOM 时触发 + ++ `destroyed`:当模板从 DOM 中移除并在模板实例被销毁之前触发 + +在这些回调函数中,`this` 指的是当前模板实例。实例对象可以访问模板的 DOM 并带有以下方法: + ++ `this.$(selectorString)`:这个方法找到所有匹配 `selectorString` 的元素,并返回这些元素的 jQuery 对象。 + ++ `this.findAll(selectorString)`:这个方法找到所有匹配 `selectorString` 的元素,但返回普通的 DOM 元素。 + ++ `this.find(selectorString)`:这个方法找到匹配 `selectorString` 的第一个元素,并返回一个普通的 DOM 元素。 + ++ `this.firstNode`:这个对象包含模板中的第一个元素。 + ++ `this.lastNode`:这个对象包含模板中的最后一个元素。 + ++ `this.data`:这个对象包含模板的数据上下文 + ++ `this.autorun(runFunc)`:一个在模板实例被销毁时停止的反应式 `Tracker.autorun()` 函数。 + ++ `this.view`:这个对象包含这个模板的 `Blaze.View` 实例。`Blaze.View` 是反应式模板的构建块。 + +在辅助函数内部,`this` 仅指向当前的数据上下文。 + +为了使这些不同的行为变得可见,我们将查看一些示例: + ++ 当我们想要访问模板的 DOM 时,我们必须在渲染回调中进行,因为只有在这一点上,模板元素才会出现在 DOM 中。为了看到它的工作原理,我们按照以下方式编辑我们的 `home.js` 文件: + + ```js + Template.home.rendered = function(){ + console.log('Rendered the home template'); + + this.$('p').html('We just replaced that text!'); + }; + ``` + + 这将用我们设置的字符串替换由 `{{#markdown}}` 块辅助函数创建的第一个 `

` 标签。现在当我们检查浏览器时,我们会发现包含我们博客介绍文本的第一个 `

` 标签已经被替换。 + ++ 对于下一个示例,我们需要为我们的 `contextExample` 模板创建一个额外的模板 JavaScript 文件。为此,我们在 `templates` 文件夹中创建一个名为 `examples.js` 的新文件,并使用以下代码片段保存它: + + ```js + Template.contextExample.rendered = function(){ + console.log('Rendered Context Example', this.data); + }; + + Template.contextExample.helpers({ + logContext: function(){ + console.log('Context Log Helper', this); + } + }); + ``` + + 这将把渲染回调以及一个名为 `logContext` 的辅助函数添加到我们的 `contextExample` 模板辅助函数中。为了使这个辅助函数运行,我们还需要将其添加到我们的 `contextExample` 模板中,如下所示: + + ```js +

{{logContext}}

+ ``` + +当我们现在回到浏览器的控制台时,我们会发现数据上下文对象已经被返回给所有我们的已渲染的 `contextTemplates` 模板的 `rendered` 回调和辅助函数。我们还可以看到辅助函数将在渲染回调之前运行。 + +### 注意 + +如果您需要从模板辅助函数内部访问模板的实例,您可以使用 `Template.instance()` 来获取它。 + +现在让我们使用事件使我们的模板变得交互式。 + +# 添加事件 + +为了使我们的模板更具动态性,我们将添加一个简单的事件,这将使之前创建的 `logContext` 辅助函数重新反应式地运行。 + +首先,然而,我们需要在我们的 `contextExample` 模板中添加一个按钮: + +```js + +``` + +为了捕获点击事件,打开 `examples.js` 并添加以下 `event` 函数: + +```js +Template.contextExample.events({ + 'click button': function(e, template){ + Session.set('randomNumber', Math.random(0,99)); + } +}); +``` + +这将设置一个名为 `randomNumber` 的会话变量到一个随机数。 + +### 注意 + +在下一章中,我们将深入讨论会话。现在,我们只需要知道当会话变量发生变化时,所有使用`Session.get('myVariable')`获取该会话变量的函数将重新运行。 + +为了看到这个效果,我们将向`logContext`助手添加一个`Session.get()`调用,并像以下方式返回先前设置的随机数: + +```js +Template.contextExample.helpers({ + logContext: function(){ + console.log('Context Log Helper',this); + + return Session.get('randomNumber'); + } +}); +``` + +如果我们打开浏览器,我们会看到**获取一些随机数**按钮。当我们点击它时,我们会看到一个随机数出现在按钮上方。 + +### 注意 + +当我们在我们`home`模板中多次使用`contextTemplates`模板时,我们会发现该模板助手每次都会显示相同的随机数。这是因为会话对象将重新运行其所有依赖项,其中所有依赖项都是`logHelper`助手的实例。 + +既然我们已经介绍了模板助手,那么让我们创建一个自定义的块助手。 + +# 块助手 + +```js +example.html file: +``` + +```js + +``` + +`{{> Template.contentBlock}}`是为块内容预定义的占位符。同样适用于`{{> Template.elseBlock}}`。 + +当`this`(在这个例子中,我们使用模板的上下文作为一个简单的布尔值)为`true`时,它将显示给定的`Template.contentBlock`。否则,它将显示`Template.elseBlock`的内容。 + +为了看到我们可以如何将最近创建的模板作为块助手使用,请查看以下示例,我们可以将其添加到`home`模板中: + +```js +{{#blockHelperExample true}} + Some Content +{{else}} + Some Warning +{{/blockHelperExample}} +``` + +现在我们应该看到以下截图: + +![块助手](img/00005.jpeg) + +现在我们将`true`更改为`false`,我们传递给`{{#blockHelperExample}}`,我们应该看到`{{else}}`之后的内容。 + +我们还可以使用助手函数来替换布尔值,这样我们就可以动态地切换块助手。此外,我们可以传递键值对参数,并通过它们的键在块助手模板内部访问它们,如下面的代码示例所示: + +```js +{{#blockHelperExample myValue=true}} +... +{{/blockHelperExample}} +``` + +我们还可以按照以下方式通过其名称访问给定参数: + +```js + +``` + +### 注意 + +请注意,块内容的上下文将是出现块的模板的上下文,而不是块助手模板本身的上下文。 + +块助手是一种强大的工具,因为它们允许我们编写自包含组件,当打包成包时,其他可以使用它们作为即插即用的功能。这个特性有潜力允许一个充满活力的市场,就像我们在 jQuery 插件市场中看到的那样。 + +# 列出帖子 + +此模板将用于在主页上显示每个帖子。 + ++ 为了使其出现,我们需要在`home`模板中添加一个`{{#each}}`助手,如下所示: + + ```js + {{#each postsList}} + {{> postInList}} + {{/each}} + ``` + + 当我们传递给`{{#each}}`块助手时,如果`postsList`助手返回一个数组,`{{#each}}`的内容将针对数组中的每个项目重复,将数组项目设置为数据上下文。 + + + 为了看到这个效果,我们在`home.js`文件中添加了`postsList`助手,如下所示: + + ```js + Template.home.helpers({ + // other helpers ... + postsList: function(){ + return [ + { + title: 'My Second entry', + description: 'Borem sodum color sit amet, consetetur sadipscing elitr.', + author: 'Fabian Vogelsteller', + timeCreated: moment().subtract(3, 'days').unix() + }, + { + title: 'My First entry', + description: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.', + author: 'Fabian Vogelsteller', + timeCreated: moment().subtract(7, 'days').unix() + } + ]; + } + }); + ``` + + + 正如我们可以看到的,我们返回一个数组,每个项目都是一个包含我们文章数据上下文的对象。对于 `timeCreated`,我们使用我们之前添加的第三方包的 `moment` 函数。这将生成过去几天的时间戳。如果我们现在去浏览器,我们会看到列出的两篇文章,如下截图所示:![列出文章](img/00006.jpeg)* 为了以正确的格式显示我们的文章项中的 `timeCreated`,我们需要创建一个助手函数来格式化时间戳。然而,因为我们想要在后面的其他模板中使用这个助手,我们需要让它成为一个全局助手,任何模板都可以访问。为此,我们创建一个名为 `template-helpers.js` 的文件,并将其保存到我们的 `my-meteor-blog/client` 文件夹中,因为它不属于任何特定的模板.* 为了注册一个全局助手,我们可以使用 Meteor 的 `Template.registerHelper` 函数: + + ```js + Template.registerHelper('formatTime', function(time, type){ + switch(type){ + case 'fromNow': + return moment.unix(time).fromNow(); + case 'iso': + return moment.unix(time).toISOString(); + default: + return moment.unix(time).format('LLLL'); + } + }); + ``` + + + 现在,我们只需通过用以下代码段替换 `postInList` 模板的底部内容来添加助手: + + ```js + + ``` + +现在,如果我们保存这两个文件并回到浏览器,我们会看到博客文章底部添加了一个相对日期。这之所以有效,是因为我们把时间和一个类型字符串传递给助手,如下所示: + +```js +{{formatTime timeCreated "fromNow"}} +``` + +助手然后使用一个 `moment` 函数返回格式化的日期。 + +有了这个全局助手,我们现在可以格式化任何 Unix 时间戳,在任何模板中将时间转换为相对时间、ISO 时间字符串和标准日期格式(使用 LLLL 格式,转换为 1986 年 9 月 4 日星期四晚上 8:30)。 + +既然我们已经使用了 `{{#with}}` 和 `{{#each}}` 块助手,让我们来看看 Blaze 使用的其他默认助手和语法。 + +# Spacebars 语法 + +来总结一下 Spacebars 的语法: + +| 助手 | 描述 | +| --- | --- | +| `{{myProperty}}` | 模板助手可以是模板数据上下文中的属性或模板助手函数。如果存在具有相同名称的助手函数和属性,模板助手将使用助手函数。 | +| `{{> myTemplate}}` | 包含助手用于模板,并且总是期待一个模板对象或者 null。 | +| `{{> Template.dynamic template=templateName [data=dataContext]}}` | 使用 `{{> Template.dynamic ...}}` 助手,你可以通过提供返回模板名称的模板助手来动态渲染模板。当助手重新运行并返回不同的模板名称时,它将用新模板替换此位置的模板。 | +| `{{#myBlockHelper}}`...`{{/myBlockHelper}}` | 包含 HTML 和 Spacebars 语法的块助手。 | + +默认情况下,Spacebars 带有以下四个默认块助手: + ++ `{{#if}}..{{/if}}` + ++ `{{#unless}}..{{/unless}}` + ++ `{{#with}}..{{/with}}` + ++ `{{#each}}..{{/each}}` + +`{{#if}}` 块助手允许我们创建简单的条件,如下所示: + +```js +{{#if myHelperWhichReturnsABoolean}} +

Show me this

+{{else}} + If not show this. +{{/if}} +``` + +`{{#unless}}` 块助手的工作方式与 `{{#if}}` 相同,但逻辑相反。 + +如前所见,`{{#with}}`块将为其内容和包含的模板设置新的数据上下文,而`{{#each}}`块帮助器将多次渲染,为每次迭代设置不同的数据上下文。 + +## 访问父数据上下文 + +为了完成对 Spacebars 语法的探索,让我们更仔细地看看我们用来显示数据的模板帮助器语法。正如我们已经在前面看到的,我们可以使用双花括号语法显示数据,如下所示: + +```js +{{myData}} +``` + +在此帮助器内部,我们可以使用点语法访问对象属性: + +```js +{{myObject.myString}} +``` + +我们还可以使用路径样式的语法访问父数据上下文: + +```js +{{../myParentsTemplateProperty}} +``` + +此外,我们可以移动更多的上下文: + +```js +{{../../someParentProperty}} +``` + +这一特性使我们能够非常灵活地设置数据上下文。 + +### 注意 + +如果我们想从一个模板帮助器内部做同样的事情,我们可以使用模板 API 的`Template.parentData(n)`,其中`n`是要访问父模板数据上下文所需的步骤数。 + +`Template.parentData(0)`与`Template.currentData()`相同,或者如果我们处于模板帮助器中,则为`this`。 + +## 向帮助器传递数据 + +向帮助器传递数据可以通过两种不同的方式完成。我们可以如下向帮助器传递参数: + +```js +{{myHelper "A String" aContextProperty}} +``` + +然后,我们可以在帮助器中按照以下方式访问它: + +```js +Template.myTemplate.helpers({ + myHelper: function(myString, myObject){ + // And we get: + // myString = 'aString' + // myObject = aContextProperty + } +}); +``` + +除了这个,我们还可以以键值的形式传递数据: + +```js +{{myHelper myString="A String" myObject=aDataProperty}} +``` + +然而,这次我们需要按照以下方式访问它们: + +```js +Template.myTemplate.helpers({ + myHelper: function(Parameters){ + // And we can access them: + // Parameters.hash.myString = 'aString' + // Parameters.hash.myObject = aDataProperty + } +}); +``` + +请注意,块帮助器和包含帮助器的行为不同,因为它们总是期望对象或键值作为参数: + +```js +{{> myTemplate someString="I will be available inside the template"}} + +// Or + +{{> myTemplate objectWithData}} +``` + +如果我们想在帮助器函数中使用它,那么我们需要对传递的参数进行类型转换,如下所示: + +```js +Template.myBlock.helpers({ + doSomethingWithTheString: function(){ + // Use String(this), to get the string + return this; + } +}); +``` + +此外,我们还可以在我们的块帮助器模板中简单地显示字符串,使用`{{Template.contentBlock}}`如下所示: + +```js + +``` + +我们还可以将另一个模板帮助器作为参数传递给包含或块帮助器,如下例所示: + +```js +{{> myTemplate myHelperWhichReturnsAnObject "we pass a string and a number" 300}} +``` + +尽管向模板帮助器传递数据和向包含/块帮助器传递数据略有不同,但在生成帮助器时参数可以非常灵活。 + +# 总结 + +反应式模板是 Meteor 最令人印象深刻的功能之一,一旦我们习惯了它们,我们可能就不会再回到手动操作 DOM 了。 + +阅读这一章之后,我们应该知道如何在 Meteor 中编写和使用模板。我们还应该理解其基本语法以及如何添加模板。 + +我们看到了如何在模板中访问和设置数据,以及如何使用帮助器。我们学习了不同类型的帮助器,例如包含帮助器和块帮助器。我们还构建了我们自己的自定义块帮助器并使用了 Meteor 的默认帮助器。 + +我们了解到模板有三种不同的回调,分别用于模板创建、渲染和销毁时。 + +我们学习了如何向帮助器传递数据,以及这在普通帮助器和块帮助器之间的区别。 + +为了深入了解,请查看以下文档: + ++ [`docs.meteor.com/#/full/templates_api`](https://docs.meteor.com/#/full/templates_api) + ++ [`www.meteor.com/blaze`](https://www.meteor.com/blaze) + ++ [`docs.meteor.com/#/full/blaze`](https://docs.meteor.com/#/full/blaze) + ++ [`atmospherejs.com/meteor/spacebars`](https://atmospherejs.com/meteor/spacebars) + ++ [`momentjs.com`](http://momentjs.com) + +你可以在这个章节找到代码示例,网址为[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713),或者在 GitHub 上查看[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter2`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter2)。 + +关于模板的新知识让我们准备好向我们的数据库添加数据,并看看我们如何在主页上显示它。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_03.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_03.md new file mode 100644 index 0000000..9060035 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_03.md @@ -0,0 +1,295 @@ +# 第三章。存储数据和处理集合 + +在上一章中,我们学习了如何构建模板并在其中显示数据。我们建立了我们应用程序的基本布局并在首页列出了一些后续示例。 + +在本章中,我们将持续向服务器上的数据库添加后续示例。我们将学习如何稍后在客户端访问这些数据,以及 Meteor 如何在客户端和服务器之间同步数据。 + +在本章中,我们将涵盖以下主题: + ++ 在 Meteor 中存储数据 + ++ 创建集合 + ++ 向集合中添加数据 + ++ 从集合中查询数据 + ++ 在集合中更新数据 + ++ “无处不在的数据库”意味着什么 + ++ 服务器数据库与客户端数据库的区别 + + ### 注意 + + 如果你直接跳到这一章并想跟随示例,请从以下任一位置下载上一章的代码示例:[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或 [`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter2`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter2)。 + + 这些代码示例还将包含所有样式文件,因此我们无需担心在过程中添加 CSS 代码。 + +# Meteor 和数据库 + +Meteor 目前默认使用 MongoDB 在服务器上存储数据,尽管还计划有用于关系型数据库的驱动程序。 + +### 注意 + +如果你有冒险精神,可以尝试一下社区构建的 SQL 驱动程序,例如来自 [`atmospherejs.com/numtel/mysql`](https://atmospherejs.com/numtel/mysql) 的 `numtel:mysql` 包。 + +MongoDB 是一个**NoSQL** 数据库。这意味着它基于平面文档结构,而不是关系表结构。它对文档的处理方式使它成为 JavaScript 的理想选择,因为文档是用 BJSON 编写的,这与 JSON 格式非常相似。 + +Meteor 采用了一种*无处不在的数据库*的方法,这意味着我们有一个相同的 API 来在客户端和服务器上查询数据库。然而,当我们在客户端查询数据库时,我们只能访问我们*发布*给客户端的数据。 + +**MongoDB** 使用一种称为**集合**的数据结构,这在 SQL 数据库中相当于一个表。集合包含文档,每个文档都有自己的唯一 ID。这些文档是类似 JSON 的结构,可以包含具有值的属性,甚至是多维属性,如下所示: + +```js +{ + "_id": "W7sBzpBbov48rR7jW", + "myName": "My Document Name", + "someProperty": 123456, + "aNestedProperty": { + "anotherOne": "With another string" + } +} +``` + +这些集合用于在服务器上的 MongoDB 以及客户端的`minimongo`集合中存储数据,后者是一个模仿真实 MongoDB 行为的内存数据库。 + +### 注意 + +我们将在本章末尾更多地讨论`minimongo`。 + +MongoDB API 允许我们使用简单的基于 JSON 的查询语言从集合中获取文档。我们可以传递其他选项,只询问*特定字段*或*对返回的文档进行排序*。这些功能在客户端尤其强大,可以以各种方式显示数据。 + +# 设置集合 + +为了亲眼看到这一切,让我们通过创建我们的第一个集合来开始。 + +我们在`my-meteor-blog`文件夹内创建一个名为`collections.js`的文件。我们需要在根目录中创建它,这样它才能在客户端和服务器上都可用。现在让我们将以下代码行添加到`collections.js`文件中: + +```js +Posts = new Mongo.Collection('posts'); +``` + +这将使`Posts`变量在全球范围内可用,因为我们没有使用`var`关键字,这会将它们限制为该文件的范围。 + +`Mongo.Collection`是查询数据库的 API,它带有以下基本方法: + ++ `insert`:此方法用于将文档插入数据库 + ++ `update`:此方法用于更新文档或它们的部分内容 + ++ `upsert`:此方法用于插入或更新文档或它们的部分内容 + ++ `remove`:此方法用于从数据库中删除文档 + ++ `find`:此方法用于查询数据库中的文档 + ++ `findOne`:此方法用于只返回第一个匹配的文档 + +# 添加帖子示例 + +要查询数据库中的帖子,我们需要添加一些帖子示例。这必须在服务器上完成,因为我们希望它们持久存在。要添加一个示例帖子,请执行以下步骤: + +1. 我们在`my-meteor-blog/server`文件夹内创建一个名为`main.js`的文件。在这个文件中,我们将使用`Meteor.startup()`函数在服务器启动时执行代码。 + +1. 我们然后添加帖子示例,但只有在集合为空时。为了防止这种情况,我们每次重启服务器时都添加它们,如下所示: + + ```js + Meteor.startup(function(){ + + console.log('Server started'); + + // #Storing Data -> Adding post examples + if(Posts.find().count() === 0) { + + console.log('Adding dummy posts'); + var dummyPosts = [ + { + title: 'My First entry', + slug: 'my-first-entry', + description: 'Lorem ipsum dolor sit amet.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: moment().subtract(7,'days').unix(), + author: 'John Doe' + }, + { + title: 'My Second entry', + slug: 'my-second-entry', + description: 'Borem ipsum dolor sit.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: moment().subtract(5,'days').unix(), + author: 'John Doe' + }, + { + title: 'My Third entry', + slug: 'my-third-entry', + description: 'Dorem ipsum dolor sit amet.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: moment().subtract(3,'days').unix(), + author: 'John Doe' + }, + { + title: 'My Fourth entry', + slug: 'my-fourth-entry', + description: 'Sorem ipsum dolor sit amet.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: moment().subtract(2,'days').unix(), + author: 'John Doe' + }, + { + title: 'My Fifth entry', + slug: 'my-fifth-entry', + description: 'Korem ipsum dolor sit amet.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: moment().subtract(1,'days').unix(), + author: 'John Doe' + } + ]; + // we add the dummyPosts to our database + _.each(dummyPosts, function(post){ + Posts.insert(post); + }); + } + }); + ``` + +现在,当我们检查终端时,我们应该看到与以下屏幕截图类似的某些内容: + +![添加帖子示例](img/00007.jpeg) + +### 注意 + +我们还可以使用 Mongo 控制台添加虚拟数据,而不是在代码中编写它们。 + +要使用 Mongo 控制台,我们首先使用`$ meteor`启动 Meteor 服务器,然后在第二个终端运行`$ meteor mongo`,这将我们带到 Mongo shell。 + +在这里,我们可以简单地使用 MongoDB 的语法添加文档: + +```js +db.posts.insert({title: 'My First entry', + slug: 'my-first-entry', + description: 'Lorem ipsum dolor sit amet.', + text: 'Lorem ipsum dolor sit amet...', + timeCreated: 1405065868, + author: 'John Doe' +} +) + +``` + +# 查询集合 + +当我们保存我们的更改时,服务器确实重新启动了。在此阶段,Meteor 在我们的数据库中添加了五个帖子示例。 + +### 注意 + +如果服务器没有重新启动,这意味着我们在代码中的某个地方犯了语法错误。当我们手动重新加载浏览器或检查终端时,我们会看到 Meteor 给出的错误,然后我们可以进行修复。 + +如果我们数据库中出了什么问题,我们总是可以使用终端中的`$ meteor reset`命令来重置它。 + +我们只需在浏览器中打开控制台并输入以下命令即可查看这些帖子: + +```js +Posts.find().fetch(); + +``` + +这将返回一个包含五个项目的数组,每个项目都是我们的示例帖子之一。 + +为了在我们前端页面上列出这些新插入的帖子,我们需要在 `home.js` 文件中替换我们 `postsList` 帮助器的內容,如下面的代码行所示: + +```js +Template.home.helpers({ + postsList: function(){ + return Posts.find({}, {sort: {timeCreated: -1}}); + } +}); +``` + +正如我们所看到的,我们直接在帮助器中返回了集合游标。这个返回值然后传递到我们的 `home` 模板中的 `{{#each}}` 块帮助器,该帮助器将在渲染 `postInList` 模板时遍历每个帖子。 + +### 注意 + +请注意,`Posts.find()` 返回一个游标,在 `{{#each}}` 块帮助器中使用时效率更高,而 `Posts.find().fetch()` 将返回一个包含文档对象的数组。使用 `fetch()`,我们可以在返回之前操纵文档。 + +我们将一个选项对象作为 `find()` 函数的第二个参数。我们传递的选项将根据 `timeCreated` 进行排序,并使用 `-1`。`-1` 的值意味着它将按降序排序(`1` 表示升序)。 + +现在,当我们查看我们的浏览器时,我们会看到我们的五篇帖子全部列出,如下面的截图所示: + +![查询集合](img/00008.jpeg) + +# 更新集合 + +现在我们已经知道如何插入和获取数据,让我们来看看如何在我们的数据库中更新数据。 + +正如我们之前所见,我们可以使用浏览器的光标来玩转数据库。对于我们接下来的例子,我们将只使用控制台来了解当我们在数据更改时,Meteor 如何反应性地改变模板。 + +为了能够在我们的数据库中编辑一篇帖子,我们首先需要知道其条目的 `_id` 字段。为了找出这个,我们需要输入以下命令: + +```js +Posts.find().fetch(); + +``` + +这将返回 `Posts` 集合中的所有文档,因为我们没有传递任何特定的查询对象。 + +在返回的数组中,我们需要查看最后一个项目,标题为 **My Fifth entry** 的项目,并使用 *Cmd* + *C*(或者如果我们在 Windows 或 Linux 上,使用 *Ctrl* + *C*)将 `_id` 字段复制到剪贴板。 + +### 注意 + +我们也可以简单地使用 `Posts.findOne()`,这将给我们找到的第一个文档。 + +现在我们已经有了 `_id`,我们可以通过输入以下命令简单地更新我们第五篇帖子的标题: + +```js +Posts.update('theCopied_Id', {$set: {title: 'Wow the title changed!'}}); + +``` + +一旦我们执行这个命令,我们就会注意到第五篇帖子的标题已经变成了我们新的标题,如果我们现在重新加载页面,我们会看到标题保持不变。这意味着更改已经持久地保存到了数据库中。 + +为了看到 Meteor 的响应性跨客户端,打开另一个浏览器窗口,导航到 `http://localhost:3000`。现在我们再次通过执行以下命令更改我们的标题,我们会看到所有客户端实时更新: + +```js +Posts.update('theCopied_Id', {$set: {title: 'Changed the title again'}}); + +``` + +# 数据库无处不在 + +在 Meteor 中,我们可以使用浏览器的控制台来更新数据,这意味着我们可以从客户端更新数据库。这之所以有效,是因为 Meteor 会自动将这些更改同步到服务器,并相应地更新数据库。 + +这之所以发生,是因为我们的项目默认添加了 `autopublish` 和 `insecure` 核心包。`autopublish` 包会自动将所有文档发布给每个客户端,而 `insecure` 包允许每个客户端通过其 `_id` 字段更新数据库记录。显然,这对于原型设计来说很好,但对于生产环境来说是不切实际的,因为每个客户端都可以操作我们的数据库。 + +如果我们移除了 `insecure` 包,我们将需要添加“允许和拒绝”规则来确定客户端可以更新哪些内容以及不可以更新哪些内容;否则,所有更新都将被拒绝。我们将在后面的章节中查看这些规则的设置,但现在这个包对我们很有用,因为我们可以立即操作数据库。 + +在下一章中,我们将了解如何手动将某些文档发布给客户端。我们将从移除 `autopublish` 包开始。 + +# 客户端与服务器集合之间的差异 + +Meteor 采用了一种*无处不在的数据库*方法。这意味着它为客户端和服务器端提供了相同的 API。数据流动是通过发布订阅模型来控制的。 + +服务器上运行着真正的 MongoDB 数据库,它负责持久化存储数据。在客户端,Meteor 包含一个名为 `minimongo` 的包,它是一个纯内存数据库,模仿了 MongoDB 的大部分查询和更新功能。 + +每次客户端连接到其 Meteor 服务器时,Meteor 都会下载客户端订阅的文档并将它们存储在其本地的 `minimongo` 数据库中。从这里,它们可以在模板中显示,或者由函数处理。 + +当客户端更新一个文档时,Meteor 会将其同步回服务器,在那里它将穿过任何允许/拒绝函数,然后被永久存储在数据库中。这也适用于反向操作;当服务器端数据库中的文档发生变化时,它将自动同步到所有订阅它的客户端,使每个连接的客户端保持最新。 + +# 概要 + +在本章中,我们学习了如何在 Meteor 的 MongoDB 数据库中持久化存储数据。我们还看到了如何查询集合和更新文档。我们理解了“无处不在的数据库”方法意味着什么,以及 Meteor 如何使每个客户端保持最新。 + +为了更深入地了解 MongoDB 以及如何查询和更新集合,请查看以下资源: + ++ [Meteor 完整栈数据库驱动](https://www.meteor.com/full-stack-db-drivers) + ++ [Meteor 迷你数据库](https://www.meteor.com/mini-databases) + ++ [Meteor 文档:集合](https://docs.meteor.com/#/full/collections) + ++ [MongoDB 手册:CRUD 简介](http://docs.mongodb.org/manual/core/crud-introduction/) + ++ [MongoDB 手册:查询操作符](http://docs.mongodb.org/manual/reference/operator/query/) + +你可以在这个章节找到代码示例,网址为[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713),或者在 GitHub 上查看[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter3`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter3)。 + +在下一章中,我们将了解如何使用发布和订阅控制数据流,从而只将必要的文档发送给客户端。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_04.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_04.md new file mode 100644 index 0000000..b4df92f --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_04.md @@ -0,0 +1,318 @@ +# 第四章。控制数据流 + +在前一章节中,我们学习了如何将数据持久化地存储在我们的数据库中。在本章中,我们将了解如何告诉 Meteor 应该向客户端发送什么数据。 + +到目前为止,所有这些都是因为使用了`autopublish`包而神奇地工作的,该包将与每个客户端同步所有数据。现在,我们将手动控制这个流程,只向客户端发送必要的数据。 + +在本章中,我们将介绍以下主题: + ++ 与服务器同步数据 + ++ 向客户端发布数据 + ++ 发布部分集合 + ++ 只发布文档的特定字段 + ++ 延迟加载更多帖子 + + ### 注意 + + 如果你想要直接进入章节并跟随示例,可以从书籍的网页 [`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或者从 GitHub 仓库 [`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter3`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter3) 下载前一章节的代码示例。 + + 这些代码示例还将包含所有样式文件,因此我们不需要在过程中担心添加 CSS 代码。 + +# 数据同步 – 当前的 Web 与新的 Web + +在当前的 Web 中,大多数页面要么是托管在服务器上的静态文件,要么是由服务器在请求时生成的动态页面。这对于大多数服务器端渲染的网站来说是真的,例如用 PHP、Rails 或 Django 编写的网站。这两种技术除了被客户端显示外不需要任何努力;因此,它们被称为*薄*客户端。 + +在现代网络应用程序中,浏览器的概念已经从薄客户端转移到*厚*客户端。这意味着网站的大部分逻辑都存在于客户端,并且客户端请求它需要的数据。 + +目前,这主要是通过调用 API 服务器实现的。这个 API 服务器然后返回数据,通常以 JSON 格式返回,给客户端一个轻松处理和使用数据的方式。 + +大多数现代网站都是薄客户端和厚客户端的混合体。普通页面是服务器端渲染的,只有如聊天框或新闻提要等功能通过 API 调用进行更新。 + +Meteor,然而,建立在这样一个理念上,即使用所有客户端的计算能力比使用一个单一服务器的计算能力要好。一个纯厚客户端或者一个单页应用包含了一个网站前端的所有逻辑,在初始页面加载时发送下来。 + +服务器随后仅仅作为数据源,只向客户端发送数据。这可以通过连接到 API 并利用 AJAX 调用实现,或者像 Meteor 一样,使用一种名为**发布/订阅**的模型。在这个模型中,服务器提供一系列发布物,每个客户端决定它想订阅哪个数据集。 + +与 AJAX 调用相比,开发者无需处理任何下载或上传逻辑。Meteor 客户端在订阅特定数据集后自动后台同步所有数据。当服务器上的数据发生变化时,服务器将更新后的文档发送给客户端,反之亦然,如下面的图表所示: + +![同步数据 - 当前的网络与新的网络](img/00009.jpeg) + +### 注意 + +如果这听起来确实不安全,请放心,我们可以设置规则,在服务器端过滤更改。我们将在第八章,*使用允许和拒绝规则进行安全设置*中查看这些可能性。 + +# 移除 autopublish 包 + +为了使用 Meteor 的发布/订阅,我们需要移除`autopublish`包,这个包是我们项目默认添加的。 + +这个包适用于快速原型设计,但在生产环境中不可行,因为我们的数据库中的所有数据都将同步到所有客户端。这不仅不安全,而且还会减慢数据加载过程。 + +我们只需在我们`my-meteor-blog`文件夹内的终端上运行以下命令: + +```js +$ meteor remove autopublish + +``` + +现在我们可以再次运行`meteor`来启动我们的服务器。当我们检查网站时,我们会发现我们上一章的所有帖子都消失了。 + +然而,它们实际上并没有消失。当前的服务器只是还没有发布任何内容,客户端也只是没有订阅任何内容;因此,我们看不到它们。 + +# 发布数据 + +为了在客户端再次访问帖子,我们需要告诉服务器将其发布给订阅的客户端。 + +为此,我们将在`my-meteor-blog/server`文件夹中创建一个名为`publications.js`的文件,并添加以下代码行: + +```js +Meteor.publish('all-posts', function () { + return Posts.find(); +}); +``` + +`Meteor.publish`函数将创建一个名为`all-posts`的发布,并返回一个包含`Post`集合中所有帖子的游标。 + +现在,我们只需告诉客户端订阅这个发布,我们就会再次看到我们的帖子。 + +我们在`my-meteor-blog/client`文件夹中创建一个名为`subscriptions.js`的文件,内容如下: + +```js +Meteor.subscribe('all-posts'); +``` + +现在,当我们检查我们的网站时,我们可以看到我们的博客文章已经重新出现。 + +这是因为当执行`subsciptions.js`文件时,客户端会订阅`all-posts`发布,这发生在页面完全加载之前,因为 Meteor 自动将`subsciptions.js`文件添加到文档的头部为我们。 + +这意味着 Meteor 服务器首先发送网站,然后 JavaScript 在客户端构建 HTML;随后,所有订阅都会同步,填充客户端的集合,并且模板引擎**Blaze**能够显示帖子。 + +现在我们已经恢复了我们的帖子,让我们看看我们如何告诉 Meteor 只发送集合中的一部分文档。 + +# 只发布数据的一部分 + +为了使我们的首页更具未来感,我们需要限制在上面显示的文章数量,因为随着时间的推移,我们可能会添加很多文章。 + +为此,我们将创建一个名为`limited-posts`的新发布,其中我们可以向文章的`find()`函数传递一个`limit`选项,并将其添加到我们的`publications.js`文件中,如下所示: + +```js +Meteor.publish('limited-posts', function () { + return Posts.find({}, { + limit: 2, + sort: {timeCreated: -1} + }); +}); +``` + +我们添加一个`sort`选项,通过它按`timeCreated`字段降序排列文章。这是必要的,以确保我们获取最新的文章并然后限制输出。如果我们只在客户端上对数据进行排序,可能会发生我们省略了较新的文章,因为服务器发布只会发送它找到的第一个文档,不管它们是否是最新的。 + +现在我们只需去到`subscriptions.js`文件,将订阅更改为以下代码行: + +```js +Meteor.subscribe('limited-posts'); +``` + +如果我们现在查看我们的浏览器,我们会看到只有最后两篇文章出现在我们的首页上,因为我们只订阅了两个,如下面的屏幕截图所示: + +![只发布数据的部分](img/00010.jpeg) + +### 注意 + +我们必须意识到,如果我们保留旧订阅的代码并与新订阅的代码并列,我们将同时订阅两个。这意味着 Meteor 合并了两个订阅,因此在我们客户端集合中保留了所有订阅的文档。 + +在添加新订阅之前,我们必须注释掉旧的订阅或删除它。 + +# 发布特定字段 + +为了优化发布,我们还可以确定要从文档中发布哪些字段。例如,我们只要求`title`和`text`属性,而不是其他所有属性。 + +这样做可以加快我们订阅的同步速度,因为我们不需要整个文章,只需要在首页上列出文章时必要的数据和简短描述。 + +让我们在`publications.js`文件中添加另一个发布: + +```js +Meteor.publish('specificfields-posts', function () { + return Posts.find({}, { + fields: { + title: 1 + } + }); +}); +``` + +由于这只是一个示例,我们传递一个空对象作为一个查询来查找所有文档,作为`find()`的第二个参数,我们传递一个包含`fields`对象的选项对象。 + +我们给每个字段一个值为`1`的属性,该属性将被包含在返回的文档中。如果我们想通过排除字段来工作,我们可以使用字段名称并将值设置为`0`。然而,我们不能同时包含和排除字段,因此我们需要根据文档大小选择哪个更适合。 + +现在我们可以在`subscriptions.js`文件中简单地将订阅更改为以下代码行: + +```js +Meteor.subscribe('specificfields-posts'); +``` + +现在,当我们打开浏览器时,它将向我们展示一个文章列表。只有标题存在,而描述、时间和作者字段为空: + +![发布特定字段](img/00011.jpeg) + +# 懒加载文章 + +既然我们已经浏览了这些简单的示例,那么现在让我们将它们结合起来,并为首页上的文章列表添加一个优美的懒加载功能。 + +懒加载是一种技术,只有在用户需要或滚动到末尾时才加载附加数据。这可以用来增加页面加载,因为要加载的数据是有限的。为此,让我们执行以下步骤: + +1. 我们需要向首页文章列表的底部添加一个懒加载按钮。我们打开我们的`home.html`文件,在`home`模板的末尾,在我们`{{#each postsList}}`块助手下面添加以下按钮: + + ```js + + ``` + +1. 接下来,我们将向我们的`publications.js`文件中添加一个发布,以发送灵活数量的文章,如下所示: + + ```js + Meteor.publish('lazyload-posts', function (limit) { + return Posts.find({}, { + limit: limit, + fields: { + text: 0 + }, + sort: {timeCreated: -1} + }); + }); + ``` + +基本上,这是我们之前学到的内容的组合。 + ++ 我们使用了`limit`选项,但不是设置一个固定的数字,而是使用了`limit`参数,我们稍后将其传递给这个发布函数。 + ++ 以前,我们使用了`fields`选项并排除了`text`字段。 + ++ 我们可以只包含`fields`来获得相同的结果。这将更安全,因为它确保我们在文档扩展时不会获取任何额外的字段: + + ```js + fields: { + title: 1, + slug: 1, + timeCreated: 1, + description: 1, + author: 1 + } + ``` + ++ 我们对输出进行了排序,以确保我们总是返回最新的文章。 + +现在我们已经设置了我们的发布,让我们添加一个订阅,这样我们就可以接收其数据。 + +### 注意 + +请注意,我们需要先删除任何其他订阅,这样我们就不会订阅任何其他发布。 + +为此,我们需要利用 Meteor 的`session`对象。这个对象可以在客户端用来设置反应性的变量。这意味着每次我们改变这个会话变量时,它都会再次运行使用它的每个函数。在下面的示例中,我们将使用会话来在点击懒加载按钮时增加文章列表的数量: + +1. 首先,在`subscription.js`文件中,我们添加以下代码行: + + ```js + Session.setDefault('lazyloadLimit', 2); + Tracker.autorun(function(){ + Meteor.subscribe('lazyload-posts', Session.get('lazyloadLimit')); + }); + ``` + +1. 然后我们将`lazyloadLimit`会话变量设置为`2`,这将是我们前端页面最初显示的文章数量。 + +1. 接下来,我们创建一个`Tracker.autorun()`函数。这个函数将在开始时运行,后来在我们改变`lazyloadLimit`会话变量到另一个值时随时运行。 + +1. 在这个函数内部,我们订阅了`lazyload-posts`,将`lazyloadLimit`值作为第二个参数。这样,每次会话变量改变时,我们都用一个新的值改变我们的订阅。 + +1. 现在,我们只需要通过点击懒加载按钮来增加会话值,订阅就会改变,发送给我们额外的文章。为此,我们在`home.js`文件的末尾添加以下代码行: + + ```js + Template.home.events({ + 'click button.lazyload': function(e, template){ + var currentLimit = Session.get('lazyloadLimit'); + + Session.set('lazyloadLimit', currentLimit + 2); + } + }); + ``` + + 这段代码将为懒加载按钮附加一个`click`事件。每次我们点击这个按钮时,我们都会获取`lazyloadLimit`会话,并增加两倍。 + +1. 当我们检查浏览器时,我们应该能够点击文章列表底部的懒加载按钮,它应该再添加两篇文章。每次我们点击按钮时,都应该发生这种情况,直到我们达到五个示例文章。 + +当我们只有五篇文章时,这看起来并不太有意义,但当文章超过 50 篇时,将最初显示的文章限制为 10 篇将显著提高页面加载时间。 + +然后我们只需要将会话的默认值更改为 10 并增加 10,就可以实现一个很好的懒加载效果。 + +# 切换订阅 + +现在我们已经有了很好的懒加载逻辑,让我们来看看这里的底层发生了什么。 + +我们之前创建的`.autorun()`函数将在代码首次执行时运行,订阅`lazyload-posts`发布。Meteor 然后发送`Posts`集合的最初两个文档,因为我们的第一个`limit`值是`2`。 + +下次我们更改`lazyloadLimit`会话时,它通过更改发布函数中的限制值来更改订阅。 + +Meteor 然后在后台检查我们客户端数据库中存在的文档,并请求下载缺失的文档。 + +当我们减少会话值时,这个方法也会起作用。Meteor 会删除与当前订阅/订阅不匹配的文档。 + +因此,我们可以尝试这样做;我们打开浏览器控制台,将会话限制设置为`5`: + +```js +Session.set('lazyloadLimit', 5); +``` + +这将立即在我们的列表中显示所有五个示例文章。现在如果我们将其设置为更小的值,我们将看到它们是如何被移除的: + +```js +Session.set('lazyloadLimit', 2); +``` + +为了确保它们已经消失,我们可以查询我们本地数据库,如下所示: + +```js +Posts.find().fetch(); +``` + +这将返回一个包含两个项目的数组,显示 Meteor 已经删除了我们不再订阅的文章,如下图所示: + +![切换订阅](img/00012.jpeg) + +# 关于数据发布的一些说明 + +```js +Posts collection changes: +``` + +```js +Meteor.publish('comments', function (postId) { + var post = Posts.find({_id: postId}); + + return Comments.find({_id: {$in: post.comments}}); +}); +``` + +为了解决这个问题,你可以将文章和评论分开发布并在客户端连接它们,或者使用第三方包,如在[`atmospherejs.com/reywood/publish-composite`](https://atmospherejs.com/reywood/publish-composite)提供的允许有反应性发布的`reywood:publish-composite`包。 + +### 注意 + +请注意,`Meteor.publish()`函数重新运行的唯一情况是当前用户发生变化,使得`this.userId`在函数中可访问。 + +# 总结 + +在本章中,我们创建了几篇发布文章并订阅了它们。我们使用了`fields`和`limit`选项来修改发布的文档数量,并为博客首页实现了一个简单的懒加载逻辑。 + +为了更深入地了解我们学到的内容,我们可以查看第三章, *存储数据和处理集合*。以下 Meteor 文档将详细介绍我们可以在集合`find()`函数中使用的选项: + ++ [`www.meteor.com/livequery`](https://www.meteor.com/livequery) + ++ [`www.meteor.com/ddp`](https://www.meteor.com/ddp) + ++ [`docs.meteor.com/#/full/publishandsubscribe`](https://docs.meteor.com/#/full/publishandsubscribe) + ++ 关于 Meteor 的集合,可以参考[`docs.meteor.com/#/full/collections`](https://docs.meteor.com/#/full/collections)。 + +你可以在这个章节代码示例的[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)找到,或者在 GitHub 上找到[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4)。 + +在下一章节,我们将给我们的应用添加一个真正应用的元素——不同的页面和路由。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_05.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_05.md new file mode 100644 index 0000000..6fbde2c --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_05.md @@ -0,0 +1,383 @@ +# 第五章。使用路由使我们的应用具有灵活性 + +既然我们已经到了这一章节,我们应该已经对 Meteor 的模板系统有一个很好的理解,并且了解服务器与客户端之间数据同步的工作原理。在消化了这些知识后,让我们回到有趣的部分,把我们的博客变成一个具有不同页面的真正网站。 + +你可能会问,“在单页应用中页面做什么?” “单页”这个术语有点令人困惑,因为它并不意味着我们的应用只由一个页面组成。它更是一个从当前做事方式衍生出来的术语,因为只有一页是从服务器发送下来的。在那之后,所有的路由和分页都在浏览器中完成。再也不需要从服务器本身请求任何页面了。在这里更好的术语应该是“客户端 web 应用程序”,尽管**单页**是目前使用的名称。 + +在本章中,我们将涵盖以下主题: + ++ 为我们的静态和动态页面编写路由。 + ++ 根据路由更改订阅 + ++ 为每个页面更改网站的标题。 + +那么,我们不要浪费时间,先添加`iron:router`包。 + +### 注意 + +如果你直接跳到这一章节并且想跟随示例,从以下网址下载前一章节的代码示例:书的网页[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或 GitHub 仓库[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4)。 + +这些代码示例还将包含所有样式文件,因此我们不必担心在过程中添加 CSS 代码。 + +# 添加 iron:router 包 + +路由是应用中特定页面的 URL。在服务器端渲染的应用中,路由要么由服务器的/框架配置定义,要么由服务器上的文件夹结构定义。 + +在客户端应用中,路由仅仅是应用将用来确定要渲染哪些页面的路径。 + +客户端内要执行的步骤如下: + +1. 网站被发送到客户端。 + +1. JavaScript 文件(或文件)被加载并解析。 + +1. 路由器代码将检查当前它是哪个 URL,并运行正确的路由函数,然后渲染正确的模板。 + + ### 提示 + + 为了在我们的应用中使用路由,我们将使用`iron:router`包,这是一个为 Meteor 编写的路由器,它使得设置路由和将它们与订阅结合变得容易。 + +1. 要添加包,我们取消任何正在运行的 Meteor 实例,前往我们的`my-meteor-blog`文件夹,并输入以下命令: + + ```js + $ meteor add iron:router + + ``` + +1. 如果我们完成了这些,我们可以通过运行`$ meteor`命令再次启动 Meteor。 + +当我们回到浏览器的控制台时,我们会看到一个错误,说:`Error: Oh no! No route found for path: "/"`。不用担心;我们将在下一节处理这个问题。 + +# 设置路由器 + +为了使用路由器,我们需要对其进行设置。为了保持我们的代码组织有序,我们将在`my-meteor-blog`文件夹的根目录下创建一个名为`routes.js`的文件,并输入以下代码: + +```js +Router.configure({ + layoutTemplate: 'layout' +}); +``` + +路由配置允许您定义以下默认模板: + +| ` | layoutTemplate` | 布局模板将作为主包装器。在这里,子模板将在`{{> yield}}`占位符中渲染,该占位符必须放在模板的某个位置。 | +| --- | --- | --- | +| ` | notFoundTemplate` | 如果当前 URL 没有定义路由,将渲染此模板。 | +| ` | loadingTemplate` | 当当前路由的订阅正在加载时,将显示此模板。 | + +对于我们的博客,我们现在只需定义`layoutTemplate`属性。 + +执行以下步骤以设置路由器: + +1. 要创建我们的第一个路由,我们需要在`route.js`文件中添加以下代码行: + + ```js + Router.map(function() { + + this.route('Home', { + path: '/', + template: 'home' + }); + + }); + ``` + + ### 注意 + + 您还可以将`Home`路由命名为`home`(小写)。然后我们可以省略手动模板定义,因为`iron:router`将自动查找名为`home`的模板。 + + 为了简单起见,我们手动定义模板,以保持全书中的所有路由一致。 + +1. 如果我们现在保存这个文件并回到浏览器,我们将看到`layout`模板被渲染两次。这并不是因为`iron:router`默认将`layoutTemplate`添加到我们应用程序的正文中,而是因为我们手动添加了它,以及在`index.html`中使用了`{{> layout}}`,所以它被渲染了两次。 + +为了防止`layout`模板的重复出现,我们需要从`index.html`文件中的``标签中删除`{{> layout}}`助手。 + +当我们检查浏览器时,现在只会看到`layout`模板被渲染一次。 + +# 切换到布局模板 + +尽管我们通过`template: home`向我们的`Home`路由传递了一个模板,但我们并没有动态地渲染这个模板;我们只是显示了带有其*硬编码*子模板的布局模板。 + +为了改变这一点,我们需要将布局模板内的`{{> home}}`包含助手替换为`{{> yield}}`。 + +`{{> yield}}`助手是`iron:router`提供的占位符助手,在此处渲染路由模板。 + +完成此操作后,当我们检查浏览器时,我们不应该看到任何变化,因为我们仍然在渲染`home`模板,但这次是动态的。然后我们按照以下步骤进行操作: + +1. 为了验证这一点,我们将向我们的应用程序添加一个未找到的模板,通过在`layout.html`文件中的布局模板之后添加以下模板: + + ```js + + ``` + +1. 现在我们需要向`route.js`中的`Router.configure()`函数添加`notFoundTemplate`属性: + + ```js + Router.configure({ + layoutTemplate: 'layout', + notFoundTemplate: 'notFound' + }); + ``` + +现在,当我们导航到`http://localhost:3000/doesntexist`时,我们将看到`notFound`模板被渲染,而不是我们的`home`模板: + +![切换到布局模板](img/00013.jpeg) + +如果我们点击主菜单中的**首页**链接,我们会回到我们的首页,因为此链接导航到"``/``"。我们已经成功添加了我们的第一个路由。现在让我们继续创建第二个路由。 + +# 添加另一个路由 + +拥有一个首页并不意味着是一个真正的网站。让我们添加一个到我们的**关于**页面的链接,该页面自从第二章 *构建 HTML 模板*以来就在我们的抽屉里。 + +要这样做,只需复制`Home`路由,并将值更改为创建一个`About`路由,如下所示: + +```js +Router.map(function() { + + this.route('Home', { + path: '/', + template: 'home' + }); + this.route('About', { + path: '/about', + template: 'about' + }); +}); +``` + +完成! + +现在,当我们回到浏览器时,我们可以点击主菜单中的两个链接来切换我们的**首页**和**关于**页面,甚至输入`http://localhost:3000/about`也会直接带我们到相应的页面,如下截图所示: + +![添加另一个路由](img/00014.jpeg) + +# 将帖子订阅移动到首页路由 + +为了为每个页面加载正确的数据,我们需要在路由中拥有订阅,而不是将其保存在单独的`subscriptions.js`文件中。 + +`iron:router`有一个特殊的函数叫做`subscriptions()`,这正是我们需要的。使用这个函数,我们可以反应性地更新特定路由的订阅。 + +为了看到它的实际应用,将`subscriptions()`函数添加到我们的`Home`路由中: + +```js +this.route('Home', { + path: '/', + template: 'home', + subscriptions +: function(){ + return Meteor.subscribe("lazyload-posts", Session.get('lazyloadLimit')); + } +}); +``` + +`subscriptions.js`文件中的**Session.setDefault('lazyloadLimit', 2)**行需要在`routes.js`文件的开头,并在`Router.configure()`函数之前: + +```js +if(Meteor.isClient) { + Session.setDefault('lazyloadLimit', 2); +} +``` + +这必须包裹在`if(Meteor.isClient){}`条件内,因为会话对象仅在客户端可用。 + +`subscriptions()`函数和之前使用的`Tracker.autorun()`函数一样是*响应式的*。这意味着当`lazyloadLimit`会话变量发生变化时,它会重新运行并更改订阅。 + +为了看到它的工作情况,我们需要删除`my-meteor-blog/client/subscriptions.js`文件,这样我们就不会有两个订阅相同发布物的点。 + +当我们现在检查浏览器并刷新页面时,我们会看到`home`模板仍然显示所有示例帖子。点击懒加载按钮会增加列出的帖子数量,但这次一切都是在我们的反应式`subscriptions()`函数中完成的。 + +### 注意 + +`iron:router`带有更多的钩子,您可以在附录中找到简短的列表。 + +为了完成我们的路由,我们只需要添加帖子路由,这样我们就可以点击一个帖子并详细阅读。 + +# 设置帖子路由 + +为了能够显示一个完整的帖子页面,我们需要创建一个帖子模板,当用户点击一个帖子时可以加载。 + +我们在`my-meteor-blog/client/templates`文件夹中创建一个名为`post.html`的文件,并使用以下模板代码: + +```js + +``` + +这个简单的模板显示了博客文章的所有信息,甚至重用了我们在这本书中早些时候从`template-helper.js`文件创建的`{{formatTime}}`助手。我们用这个助手来格式化文章创建的时间。 + +我们暂时还看不到这个模板,因为我们必须先为这个页面创建发布和路由。 + +## 创建一个单篇博文发布 + +为了在这个模板中显示完整文章的数据,我们需要创建另一个发布,该发布将完整的文章文档发送到客户端。 + +为了实现这一点,我们打开`my-meteor-blog/server/publication.js`文件,并添加以下发布内容: + +```js +Meteor.publish("single-post", function(slug) { + return Posts.find({slug: slug}); +}); +``` + +这里使用的`slug`参数将在稍后的订阅方法中提供,以便我们可以使用`slug`参数来引用正确的文章。 + +### 注意 + +缩略词是文档标题,以一种适合 URL 使用的方式格式化。缩略词比简单地在 URL 后附加文档 ID 更好,因为它们可读性强,易于访问者理解,也是良好 SEO 的重要组成部分。 + +为了使用缩略词,每个缩略词都必须是唯一的。我们在创建文章时会照顾到这一点。 + +假设我们传递了正确的斜杠,比如`my-first-entry`,这个发布将发送包含此斜杠的文章。 + +## 添加博文路由 + +为了让这个路由工作,它必须是动态的,因为每个链接的 URL 对于每篇文章都必须是不同的。 + +我们还将渲染一个加载模板,直到文章被加载。首先,我们在`my-meteor-blog/client/templates/layout.html`中添加以下模板: + +```js + +``` + +此外,我们还需要将此模板作为默认加载模板添加到`routes.js`中的`Router.configure()`调用中: + +```js +Router.configure({ + layoutTemplate: 'layout', + notFoundTemplate: 'notFound', + loadingTemplate: 'loading', + ... +``` + +然后,我们将以下代码行添加到我们的`Router.map()`函数中,以创建一个动态路由: + +```js +this.route('Post', { + path: '/posts/:slug', + template: 'post', + + waitOn: function() { + return Meteor.subscribe('single-post', this.params.slug); + }, + data: function() { + return Posts.findOne({slug: this.params.slug}); + } +}); +``` + +`'/posts/:slug'`路径是一个动态路由,其中`:slug`可以是任何内容,并将传递给路由函数作为`this.params.slug`。这样我们只需将给定的 slug 传递给`single-post`订阅,并检索与这个 slug 匹配的文章的正确文档。 + +`waitOn()`函数的工作方式类似于`subscriptions()`函数,不过它会自动渲染我们在`Router.configure()`中设置的`loadingTemplate`,直到订阅准备好。 + +这个路由的`data()`函数将设置`post`模板的数据上下文。我们基本上在我们的本地数据库中查找包含来自 URL 的给定 slug 的文章。 + +### 注意 + +`Posts`集合的`findOne()`方法与`find()`方法类似,但只返回找到的第一个结果作为 JavaScript 对象。 + +让我们总结一下这里发生的事情: + +1. 路由被调用(通过点击链接或页面重新加载)。 + +1. 然后`waitOn()`函数将订阅由给定的`slug`参数标识的正确文章,该参数是 URL 的一部分。 + +1. 由于`waitOn()`函数,`loadingTemplate`将在订阅准备好之前渲染。由于这在我们的本地机器上会非常快,所以我们可能根本看不到加载模板。 + +1. 一旦订阅同步,模板就会渲染。 + +1. 然后`data()`函数将重新运行,设置模板的数据上下文为当前文章文档。 + +现在发布和路由都准备好了,我们只需导航到`http://localhost:3000/posts/my-first-entry`,我们应该看到`post`模板出现。 + +## 文章链接 + +虽然我们已经设置了路由和订阅,但我们看不到它工作,因为我们需要正确的文章链接。由于我们之前添加的每个示例文章都包含一个`slug`属性,所以我们只需将它们添加到`postInList`模板中的文章链接。打开`my-meteor-blog/client/templates/postInList.html`文件,按照以下方式更改链接: + +```js +

{{title}}

+``` + +最后,当我们打开浏览器并点击博客文章的标题时,我们会被重定向到一个显示完整文章条目的页面,如下面的屏幕截图所示: + +![文章链接](img/00015.jpeg) + +# 更改网站标题 + +如今我们的文章路由已经运行,我们只缺少为每个页面显示正确的标题。 + +遗憾的是,` `在 Meteor 中不是一个响应式模板,我们本可以让 Meteor 自动更改标题和元标签。 + +### 注 + +计划将`head`标签变成一个响应式模板,但可能在 1.0 版本之前不会实现。 + +为了更改文档标题,我们需要找到一种基于当前路由来更改它的不同方法。 + +幸运的是,`iron:router`有一个`onAfterAction()`函数,也可以在`Router.configure()`函数中用于每个路由之前运行。在这个函数中,我们有权访问当前路由的数据上下文,所以我们可以简单地使用原生 JavaScript 设置标题: + +```js +Router.configure({ + layoutTemplate: 'layout', + notFoundTemplate: 'notFound', + + onAfterAction: function() { + var data = Posts.findOne({slug: this.params.slug}); + + if(_.isObject(data) && !_.isArray(data)) + document.title = 'My Meteor Blog - '+ data.title; + else + document.title = 'My Meteor Blog - '+ this.route.getName(); + } +}); +``` + +使用**Posts.findOne({slug: this.params.slug})**,我们获取当前路由的文章。然后我们检查它是否是一个对象;如果是,我们将文章标题添加到`title`元标签。否则,我们只取路由名称。 + +在`Router.configure()`中这样做将为每个路由调用**onAfterAction**。 + +现在如果我们看看我们浏览器的标签页,我们会发现当我们浏览网站时,我们网站的标题会发生变化: + +![更改网站标题](img/00016.jpeg) + +### 提示 + +如果我们想要让我们的博客更酷,我们可以添加`mrt:iron-router-progress`包。这将在切换路由时在页面的顶部添加一个进度条。我们只需从我们的应用程序文件夹中运行以下命令: + +```js +$ meteor add mrt:iron-router-progress + +``` + +# 摘要 + +就这样!现在我们的应用程序是一个功能完整的网站,有不同的页面和 URL。 + +在本章中,我们学习了如何设置静态和动态路由。我们将我们的订阅移到了路由中,这样它们就可以根据路由的需要自动更改。我们还使用了 slugs 来订阅正确的文章,并在`post`模板中显示它们。最后,我们更改了网站的标题,使其与当前路由相匹配。 + +要了解更多关于`iron:router`的信息,请查看其文档在[`github.com/EventedMind/iron-router`](https://github.com/EventedMind/iron-router)。 + +你可以在这个章节的代码示例在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)找到,或者在 GitHub 上找到[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5)。 + +在下一章中,我们将深入探讨 Meteor 的会话对象。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_06.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_06.md new file mode 100644 index 0000000..73ae883 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_06.md @@ -0,0 +1,274 @@ +# 第六章。使用会话保持状态 + +我们在之前的章节中实现懒加载技术时已经使用了 Meteor 的 session 对象。在本章中,我们想要更深入地了解它,并学习如何使用它来创建特定模板的反应式函数。 + +本章将涵盖以下主题: + ++ 会话是什么 + ++ 热代码推送如何影响 session + ++ 使用 session 重新运行模板助手 + ++ 重新运行函数 + ++ 创建特定模板的反应式函数 + + ### 注意 + + 如果你直接跳到这一章节并想要跟随示例,可以从书籍的网页上[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或从 GitHub 仓库[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5)下载上一章节的代码示例。 + + 这些代码示例还将包含所有的样式文件,因此我们不必担心在过程中添加 CSS 代码。 + +# Meteor 的 session 对象 + +Meteor 提供的`Session`对象是一个反应式数据源,主要用于在热代码重载过程中维护全局状态,尽管它不会在页面手动重载时保存其数据,这使得它与 PHP 会话不同。 + +### 注意 + +当我们上传新代码时,服务器会将这些更新推送给所有客户端,这时就会发生热代码重载。 + +`Session`对象是一个反应式数据源。这意味着无论这个 session 变量在反应式函数中如何使用,当它的值发生变化时,它都会重新运行那个函数。 + +session 变量的一个用途可以是维护我们应用的全局状态,例如,检查用户是否显示侧边栏。 + +session 对象对于模板和其他应用部分之间的简单数据通信并不有用,因为维护这会很快变得令人痛苦,并且可能发生命名冲突。 + +## 实现简单反应性的更好方法 + +如果我们想要用于应用内通信,最好使用 Meteor 的`reactive-var`包,它带有一个类似于`ReactiveVar`对象的`Session`。 + +使用它时,我们可以简单地通过`$ meteor add reactive-var`来添加它。 + +然后需要实例化这个对象,并带有反应式的`get()`和`set()`函数,类似于`session`对象: + +```js +Var myReactiveVar = new ReactiveVar('my initial value'); + +// now we can get it in any reactive function +myReactiveVar.get(); + +// and set it, to rerun depending functions +myReactiveVar.set('my new value'); +``` + +为了实现更自定义的反应性,我们可以使用 Meteor 的`Tracker`包构建我们自己的自定义反应式对象。有关更多信息,请参阅第九章,*高级反应性*。 + +### 提示 + +对于与特定模板实例绑定的反应式变量,请查看我的`frozeman:template-var`包在[`atmospherejs.com/frozeman/template-var`](https://atmospherejs.com/frozeman/template-var)。 + +# 在模板助手使用 session + +由于所有模板助手函数都是反应式函数,因此在这样的助手内部使用 session 对象是一个好地方。 + +反应式意味着当我们在这个函数内部使用反应式对象时,该函数会在反应式对象发生变化时重新运行,同时重新渲染模板的这部分。 + +### 注意 + +模板助手不是唯一的反应式函数;我们还可以使用`Tracker.autorun(function(){…})`创建自己的,正如我们早先章节中看到的那样。 + +为了展示在模板助手中美使用会话的方法,请执行以下步骤: + +1. 打开我们的`my-meteor-blog/client/templates/home.js`文件,并在文件中的任何位置添加以下助手代码: + + ```js + Template.home.helpers({ + //... + sessionExample: function(){ + return Session.get('mySessionExample'); + } + }); + ``` + + 这创建了`sessionExample`助手,它返回`mySessionExample`会话变量的值。 + +1. 接下来,我们需要把我们这个助手添加到我们的`home`模板本身,通过打开`my-metepr-blog/client/templates/home.html`文件,在我们`{{#each postsList}}`块助手上面加上助手: + + ```js +

This comes from our Session: {{sessionExample}}

+ ``` + +1. 现在,打开浏览器窗口,输入`http://localhost:3000`。我们会看到我们添加的静态文本出现在博客的主页上。然而,为了看到 Meteor 的反应式会话在起作用,我们需要打开浏览器的控制台并输入以下代码行: + + ```js + Session.set('mySessionExample', 'I just set this.'); + ``` + + 以下屏幕截图说明了这一点: + + ![在模板助手中美使用会话](img/00017.jpeg) + +在我们按下*Enter*键的那一刻,我们就看到了文字被添加到了我们的模板中。这是因为当我们调用`Session.set('mySessionExample', ...)`时,Meteor 会在我们之前调用`Session.get('mySessionExample')`的每个反应式函数中重新运行。对于模板助手,这只会重新运行这个特定的模板助手,只重新渲染模板的这部分。 + +我们可以通过为`mySessionExample`会话变量设置不同的值来尝试,这样我们就可以看到文字如何随时变化。 + +## 会话和热代码推送 + +热代码推送是指当我们更改文件时,Meteor 服务器将这些更改推送到客户端。Meteor 足够智能,可以重新加载页面,而不会丢失 HTML 表单或会话的值。因此,会话可以用来在热代码推送过程中保持用户状态的一致性。 + +为了看到这一点,我们将`mySessionExample`的值设置为我们想要的任何东西,并看到网站更新为此值。 + +现在,我们打开我们的`home.html`文件,进行一点小修改,例如移除`{{sessionExample}}`助手周围的``标签并保存文件,我们会发现尽管页面随着新更改的模板重新加载,我们的会话状态仍然保持。这在以下屏幕截图中得到证明: + +![会话和热代码推送](img/00018.jpeg) + +### 注意 + +如果我们手动使用浏览器的刷新按钮重新加载页面,会话将无法保持更改,文字将消失。 + +为了克服这个限制,Meteor 的包仓库中有许多包,它们反应式地将数据存储在浏览器的本地存储中,以在页面重新加载时保持数据。其中一个包叫做`persistent-session`,可以在[`atmospherejs.com/package/persistent-session`](http://atmospherejs.com/package/persistent-session)找到。 + +# 反应性地重新运行函数 + +为了根据会话更改重新运行函数,Meteor 提供了`Tracker.autorun()`函数,我们之前用它来改变懒加载订阅。 + +`Tracker.autorun()`函数将使传递给它的每个函数都具有反应性。为了看到一个简单的例子,我们将创建一个函数,每次函数重新运行时都会警告一个文本。 + +### 注意 + +`Tracker`包是会话在幕后使用的东西,以使反应性工作。在第九章,*高级反应性*,我们将深入研究这个包。 + +执行以下步骤以反应性地重新运行函数: + +1. 让我们创建一个名为`main.js`的新文件,但这次在`my-meteor-blog`目录的根目录中,内容如下: + + ```js + if(Meteor.isClient) { + + Tracker.autorun(function(){ + var example = Session.get('mySessionExample'); + alert(example); + }); + } + ``` + + ### 注意 + + 在后面的章节中我们将会需要`main.js`文件。因此,我们在根目录中创建了它,使其可以在客户端和服务器上访问。 + + 然而,由于 Meteor 的 session 对象只存在于客户端,我们将使用`if(Meteor.isClient)`条件,以便只在客户端执行代码。 + + 现在当我们查看浏览器时,我们会看到一个显示`undefined`的警告。这是因为传递给`Tracker.autorun()`的函数在代码执行时也会运行,在这个时候我们还没有设置我们的会话。 + +1. 要设置会话变量的默认值,我们可以使用`Session.setDefault('mySessionExample', 'My Text')`。这将在不运行任何反应性函数的情况下设置会话,当会话值未定义时。如果会话变量的值已经设置,`setDefault`将根本不会更改变量。 + +1. 在我们的示例中,当页面加载时我们可能不希望出现一个警告窗口。为了防止这种情况,我们可以使用`Tracker.Computation`对象,它作为我们函数的第一个参数传递给我们,并为我们提供了一个名为`firstRun`的属性。这个属性将在函数的第一次运行时设置为`true`。当我们使用这个属性时,我们可以在开始时防止显示警告: + + ```js + Tracker.autorun(function(c){ + var example = Session.get('mySessionExample'); + + if(!c.firstRun) { + alert(example); + } + }); + ``` + +1. 现在让我们打开浏览器的控制台,将会话设置为任何值以查看警告窗口出现: + + ```js + Session.set('mySessionExample','Hi there!'); + ``` + +此代码的输出在下方的屏幕截图中展示: + +![反应性地重新运行函数](img/00019.jpeg) + +### 注意 + +当我们再次运行相同的命令时,我们不会看到警告窗口出现,因为 Meteor 足够智能,可以防止在会话值不变时重新运行。如果我们将其设置为另一个值,警告将再次出现。 + +## 停止反应式函数 + +作为第一个参数传递的`Tracker.Computation`对象还为我们提供了一种完全停止函数反应性的方法。为了尝试这个,我们将更改函数,使其在我们传递`stop`字符串给会话时停止其反应性: + +```js +Tracker.autorun(function(c){ + var example = Session.get('mySessionExample'); + + if(!c.firstRun) { + if(Session.equals('mySessionExample', 'stop')) { + alert('We stopped our reactive Function'); + c.stop(); + } else { + alert(example); + } + } +}); +``` + +现在,当我们进入浏览器的控制台并运行`Session.set('mySessionExample', 'stop')`时,响应式函数将停止变得响应式。为了测试这一点,我们可以尝试运行`Session.set('mySessionExample', 'Another text')`,我们会发现警告窗口不会出现。 + +### 注意 + +如果我们对代码进行更改并且发生了热代码重载,响应式函数将再次变为响应式,因为代码被执行了 again。 + +前面的示例还使用了一个名为`Session.equals()`的函数。这个函数可以比较两个标量值,同时防止不必要的重新计算,与使用`Session.get('mySessionExample) === 'stop'`相比。使用`Session.equals()`只有在会话变量改变*到*或*从*那个值时才会重新运行这个函数。 + +### 注意 + +在我们的示例中,然而,这个函数并没有什么区别,因为我们之前也调用了`Session.get()`。 + +# 在模板中使用 autorun + +虽然在某些情况下在我们的应用程序中全局使用`Tracker.autorun()`可能很有用,但随着我们应用程序的增长,这些全局响应式函数很快变得难以维护。 + +因此,将响应式函数绑定到它们执行操作的模板是一个好的实践。 + +幸运的是,Meteor 提供了一个特殊的`Tracker.autorun()`版本,它与模板实例相关联,并在模板被销毁时自动停止。 + +为了利用这一点,我们可以在`created()`或渲染回调中启动响应式函数。首先,让我们注释掉`main.js`文件中的上一个示例,这样我们就不会得到两个警告窗口。 + +打开我们的`home.js`文件,添加以下代码行: + +```js +Template.home.created = function(){ + + this.autorun(function(){ + alert(Session.get('mySessionExample')); + }); +}; +``` + +这将在主页模板创建时创建响应式函数。当我们进入浏览器的控制台并设置`mySessionExample`会话为新值时,我们会看到警告窗口出现,如下面的屏幕截图所示: + +![在模板中使用 autorun](img/00020.jpeg) + +现在,当我们通过点击菜单中的**关于**链接切换模板,并将`mySessionExample`会话变量再次设置为另一个值时,我们不会看到警告窗口出现,因为当模板被销毁时,响应式的`this.autorun()`已经停止。 + +### 注意 + +注意所有的`Tracker.autorun()`函数都返回一个`Tracker.Computation`对象,可以使用`Tracker.Computation.stop()`随时停止 autorun 的响应性: + +```js +Var myReactiveFunction = Tracker.autorun(function(){...}); +// Do something which needs to stop the autorun +myReactiveFunction.stop(); +``` + +# 响应式的会话对象 + +我们看到了会话对象可以在其值改变时重新运行函数。这和集合的`find()`和`findOne()`函数的行为一样,这些函数在集合的底层数据改变时会重新运行函数。 + +我们可以使用会话来在热代码推送之间保持用户状态,比如下拉菜单或弹出的状态。但是,请注意,如果没有明确的命名约定,这些会话变量很快就会变得难以维护。 + +为了实现更具体的反应式行为,最好使用 Meteor 的`Tracker`核心包构建一个自定义的反应式对象,这将在第九章,*高级反应性*中介绍。 + +# 总结 + +在本章中,我们了解了 Meteor 的反应式会话对象能做什么。我们用它来重新运行模板助手和我们自己的自定义函数,并且我们通过`created()`和`destroyed()`回调创建了一个特定的反应式函数模板。 + +要深入了解,请查看 Meteor 关于会话和反应性的文档,具体资源如下: + ++ [Meteor 的反应性](https://docs.meteor.com/#/full/reactivity) + ++ [Meteor 的反应式会话对象](https://docs.meteor.com/#/full/session) + ++ [Meteor 的反应式变量包](https://docs.meteor.com/#/full/reactivevar_pkg) + ++ [Meteor 的 Tracker](https://www.meteor.com/tracker) + +你可以在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)找到本章的代码示例,或者在 GitHub 上查看[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6)。 + +在下一章中,我们将为我们的博客创建管理员用户和后端,为创建和编辑帖子打下基础。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_07.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_07.md new file mode 100644 index 0000000..ae46c12 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_07.md @@ -0,0 +1,377 @@ +# 第七章。用户和权限 + +通过对前一章的内容进行操作,我们应该现在有一个运行中的博客了。我们可以点击所有的链接和帖子,甚至可以延迟加载更多的帖子。 + +在本章中,我们将添加我们的后端登录并创建管理员用户。我们还将创建一个编辑帖子的模板,并使管理员用户能够看到编辑按钮,以便他们可以编辑和添加新内容。 + +在本章中,我们将学习以下概念: + ++ Meteor 的 `accounts` 包 + ++ 创建用户和登录 + ++ 如何限制某些路由仅供已登录用户使用 + + ### 注意 + + 你可以删除前一章中的所有会话示例,因为我们在推进应用时不需要它们。从 `my-meteor-blog/main.js`、`my-meteor-blog/client/templates/home.js` 和 `my-meteor-blog/client/templates/home.html` 中删除会话的代码,或者下载前一章代码的新副本。 + + 如果你直接跳到这一章并且想跟随示例,可以从以下网址下载前一章的代码示例:[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或从 GitHub 仓库 [`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6) 下载。 + + 这些代码示例还将包含所有的样式文件,所以我们不需要在过程中添加 CSS 代码。 + +# Meteor 的 accounts 包 + +Meteor 使得通过其 `accounts` 包向我们的网络应用添加身份验证变得非常容易。`accounts` 包是一个与 Meteor 的核心紧密相连的完整的登录解决方案。创建的用户可以在许多 Meteor 的服务器端函数中通过 ID 进行识别,例如,在一个出版物中: + +```js +Meteor.publish("examplePublication", function () { + // the current loggedin user id can be accessed via + this.userId; +} +``` + +此外,我们还可以通过简单地添加一个或多个 `accounts-*` 核心包来添加通过 Facebook、GitHub、Google、Twitter、Meetup 和 Weibo 登录的支持。 + +Meteor 还提供了一个简单的登录界面,一个可以通过使用 `{{> loginButtons}}` 助手添加的额外模板。 + +所有注册的用户资料都将存储在一个名为 `Users` 的集合中,Meteor 为我们创建了这个集合。所有的认证过程和通信过程都使用 **Secure Remote Password** (**SRP**) 协议,大多数外部服务都使用 OAuth。 + +对于我们的博客,我们只需创建一个管理员用户,当登录后,他们可以创建和编辑帖子。 + +### 注意 + +如果我们想要使用第三方服务之一进行登录,我们可以先完成本章的内容,然后添加前面提到的其中一个包。 + +添加额外包后,我们可以打开 **登录** 表单。我们将看到一个按钮,我们可以配置第三方服务以供我们的应用使用。 + +# 添加 accounts 包 + +要开始使用登录系统,我们需要将 `accounts-ui` 和 `accounts-password` 包添加到我们的应用中,如下所示: + +1. 为了做到这一点,我们打开终端,导航到我们的`my-meteor-blog`文件夹,并输入以下命令: + + ```js + $ meteor add accounts-ui accounts-password + + ``` + +1. 在我们成功添加包之后,我们可以使用`meteor`命令再次运行我们的应用程序。 + +1. 因为我们想要阻止我们的访客创建额外的用户账户,所以我们需要在我们的`accounts`包中禁止这个功能。首先,我们需要打开我们在前一章节中创建的`my-meteor-blog/main.js`文件,并删除所有代码,因为我们不再需要会话示例。 + +1. 然后在这个文件中添加以下代码行,但一定要确保不要使用`if(Meteor.isClient)`,因为这次我们希望在客户端和服务器上都执行代码: + + ```js + Accounts.config({ + forbidClientAccountCreation: true + }); + ``` + + 这将禁止在客户端调用`Accounts.createUser()`,并且`accounts-ui`包将不会向我们的访客显示**注册**按钮。 + + ### 注意 + + 这个选项似乎对第三方服务不起作用。所以,当使用第三方服务时,每个人都可以注册并编辑文章。为了防止这种情况,我们将在服务器端创建“拒绝”规则以禁止用户创建,这超出了本章节的范围。 + +# 为我们的模板添加管理功能 + +允许编辑我们文章的最佳方式是在我们文章的页面上添加一个**编辑文章**链接,这个链接只有在登录后才能看到。这样,我们节省了为另一个后端重建类似基础设施的工作,并且使用起来很方便,因为前端和后端之间没有严格的分离。 + +首先,我们将向我们的`home`模板添加一个**创建新文章**链接,然后将**编辑文章**链接添加到文章的`pages`模板中,最后在主菜单中添加登录按钮和表单。 + +## 添加新文章的链接 + +让我们先添加一个**创建新文章**链接。打开`my-meteor-blog/clients/templates/home.html`中的`home`模板,并在`{{#each postsList}}`块助手之上添加以下代码行: + +```js +{{#if currentUser}} + Create new post +{{/if}} +``` + +`{{currentUser}}`助手随`accounts-base`包一起提供,当我们安装我们的`accounts`包时安装了它。它会返回当前登录的用户,如果没有用户登录,则返回 null。将其用于`{{#if}}`块助手内部允许我们只向登录用户显示内容。 + +## 添加编辑文章的链接 + +要编辑文章,我们只需在我们的`post`模板中添加一个**编辑文章**链接。打开同一文件夹中的`post.html`,并在`{{author}}`之后添加`{{#if currentUser}}..{{/if}}`,如下所示: + +```js + + Posted {{formatTime timeCreated "fromNow"}} by {{author}} + + {{#if currentUser}} + | Edit post + {{/if}} + +``` + +## 添加登录表单 + +现在我们已经有了添加和编辑文章的链接,让我们添加登录表单。我们可以创建自己的表单,但 Meteor 已经包含了一个简单的登录表单,我们可以将其样式修改以符合我们的设计。 + +由于我们之前添加了`accounts-ui`包,Meteor 为我们提供了`{{> loginButtons}}`模板助手,它作为一个即插即用的模板工作。为了添加这个功能,我们将打开我们的`layout.html`模板,并在菜单的`
    `标签内添加以下助手,如下所示: + +```js +

    My Meteor Single Page App

    + + +{{> loginButtons}} + +``` + +# 创建编辑文章的模板 + +现在我们只缺少编辑帖子的模板。为了添加这个模板,我们将在`my-meteor-blog/client/templates`文件夹中创建一个名为`editPost.html`的文件,并填入以下代码行: + +```js + +``` + +正如我们所看到的,我们添加了`{{title}}`、`{{description}}`和`{{text}}`帮助器,这些将从帖子数据中稍后获取。这个简单的模板,带有它的三个文本字段,将允许我们以后编辑和创建新帖子。 + +如果我们现在查看浏览器,我们会注意到我们看不到到目前为止所做的任何更改,除了网站角落里的**登录**链接。为了能够登录,我们首先需要添加我们的管理员用户。 + +# 创建管理员用户 + +由于我们已禁用客户端创建用户,作为一种安全措施,我们将在服务器上以创建示例帖子的方式创建管理员用户。 + +打开`my-meteor-blog/server/main.js`文件,在`Meteor.startup(function(){...})`内的某个位置添加以下代码行: + +```js +if(Meteor.users.find().count() === 0) { + + console.log('Created Admin user'); + + Accounts.createUser({ + username: 'johndoe', + email: 'johndoe@example.com', + password: '1234', + profile: { + name: 'John Doe' + } + }); +} +``` + +如果我们现在打开浏览器,我们应该能够使用我们刚才创建的用户登录,我们会立即看到所有编辑链接出现。 + +然而,当我们点击任何编辑链接时,我们会看到`notFound`模板出现,因为我们还没有创建任何管理员路由。 + +## 添加权限 + +Meteor 的`account`包默认并不带有对用户可配置权限的支持。 + +为了添加权限控制,我们可以添加第三方包,比如`deepwell:authorization`包,可以在 Atmosphere 上找到,网址为[`atmospherejs.com/deepwell/authorization`](http://atmospherejs.com/deepwell/authorization),它带有复杂的角色模型。 + +如果我们想手动完成,我们可以在创建用户时向用户文档添加简单的`roles`属性,然后在创建或更新帖子时在允许/拒绝角色中检查这些角色。我们将在下一章学习允许/拒绝规则。 + +如果我们使用`Accounts.createUser()`函数创建用户,我们就不能添加自定义属性,因此我们需要在创建用户后更新用户文档,如下所示: + +```js +var userId = Accounts.createUser({ + username: 'johndoe', + email: 'johndoe@example.com', + password: '1234', + profile: { + name: 'John Doe' + } +}); +// add the roles to our user +Meteor.users.update(userId, {$set: { + roles: {admin: true}, +}}) +``` + +默认情况下,Meteor 会发布当前登录用户`username`、`emails`和`profile`属性。要添加其他属性,比如我们的自定义`roles`属性,我们需要添加一个发布功能,以便在客户端访问`roles`属性,如下所示: + +1. 打开`my-meteor/blog/server/publications.js`文件,添加以下发布功能: + + ```js + Meteor.publish("userRoles", function () { + if (this.userId) { + return Meteor.users.find({_id: this.userId}, {fields: {roles: 1}}); + } else { + this.ready(); + } + }); + ``` + +1. 在`my-meteor-blog/main.js`文件中,我们像下面这样添加订阅: + + ```js + if(Meteor.isClient){ + Meteor.subscribe("userRoles"); + } + ``` + +1. 现在既然我们在客户端已经有了`roles`属性,我们可以把`home`和`post`模板中的`{{#if currentUser}}..{{/if}}`改为`{{#if currentUser.roles.admin}}..{{/if}}`,这样只有管理员才能看到按钮。 + +## 有关安全性的说明 + +用户只能使用以下命令更新自己的`profile`属性: + +```js +Meteor.users.update(ownUserId, {$set: {profiles:{myProperty: 'xyz'}}}) + +``` + +如果我们想要更新`roles`属性,我们将失败。为了看到这一点,我们可以打开浏览器的控制台并输入以下命令: + +```js +Meteor.users.update(Meteor.user()._id, {$set:{ roles: {admin: false}}}); + +``` + +这将给我们一个错误,指出:**更新失败:拒绝访问**,如下面的屏幕截图所示: + +![关于安全性的说明](img/00021.jpeg) + +### 注意 + +如果我们想要允许用户编辑其他属性,例如他们的`roles`属性,我们需要为此添加一个`Meteor.users.allow()`规则。 + +# 为管理员创建路由 + +现在我们已经有了一个管理员用户,我们可以添加那些指向`editPost`模板的路由。尽管从理论上讲`editPost`模板对每个客户端都是可用的,但它不会造成任何风险,因为允许和拒绝规则才是真正的安全层,我们将在下一章中查看这些规则。 + +要添加创建文章的路由,让我们打开我们的`my-meteor-blog/routes.js`文件,并向`Router.map()`函数添加以下路由: + +```js +this.route('Create Post', { + path: '/create-post', + template: 'editPost' +}); +``` + +这将在我们点击主页上的**创建新文章**链接后立即显示`editPost`模板,如下面的屏幕截图所示: + +![为管理员创建路由](img/00022.jpeg) + +我们发现表单是空的,因为我们没有为模板设置任何数据上下文,因此模板中显示的`{{title}}`、`{{description}}`和`{{text}}`占位符都是空的。 + +为了使编辑文章的路由工作,我们需要添加类似于为`Post`路由本身所做的订阅。为了保持事物的**DRY**(这意味着**不要重复自己**),我们可以创建一个自定义控制器,这个控制器将被两个路由使用,如下所示: + +1. 在`Router.configure(...);`调用之后添加以下代码行: + + ```js + PostController = RouteController.extend({ + waitOn: function() { + return Meteor.subscribe('single-post', this.params.slug); + }, + + data: function() { + return Posts.findOne({slug: this.params.slug}); + } + }); + ``` + +1. 现在我们可以简单地编辑`Post`路由,删除`waitOn()`和`data()`函数,并添加`PostController`: + + ```js + this.route('Post', { + path: '/posts/:slug', + template: 'post', + controller: 'PostController' + }); + ``` + +1. 现在我们还可以通过简单地更改`path`和`template`属性来添加`编辑文章`路由: + + ```js + this.route('Edit Post', { + path: '/edit-post/:slug', + template: 'editPost', + controller: 'PostController' + }); + ``` + +1. 这就完成了!现在当我们打开浏览器时,我们将能够访问任何文章并点击**编辑**按钮,然后我们将被引导到`editPost`模板。 + +如果您想知道为什么表单会填充文章数据,请查看我们刚刚创建的`PostController`。在这里,我们在`data()`函数中返回文章文档,将模板的数据上下文设置为文章的数据。 + +现在我们已经设置了这些路由,我们应该完成了。难道不是吗? + +还不是,因为任何知道`/create-post`和`/edit-post/my-title`路由的人都可以简单地看到`editPost`模板,即使他或她不是管理员。 + +## 防止访客看到管理路由 + +```js +routes.js file: +``` + +```js +var requiresLogin = function(){ + if (!Meteor.user() || + !Meteor.user().roles || + !Meteor.user().roles.admin) { + this.render('notFound'); + + } else { + this.next(); + } +}; + +Router.onBeforeAction(requiresLogin, {only: ['Create Post','Edit Post']}); +``` + +在这里,首先我们创建了`requiresLogin()`函数,它将在`创建文章`和`编辑文章`路由之前执行,因为我们将其作为第二个参数传递给`Router.onBeforeAction()`函数。 + +在`requiresLogin()`内部,我们检查用户是否已登录,当调用`Meteor.user()`时,这将返回用户文档,并且检查他们是否有`admin`角色。如果没有,我们简单地渲染`notFound`模板,并不再继续路由。否则,我们运行`this.next()`,这将继续渲染当前路由。 + +就这样!如果我们现在登出并导航到`/create-post`路由,我们将看到`notfound`模板。 + +如果我们登录,模板将切换并立即显示`editPost`模板。 + +这是因为一旦我们将`requiresLogin()`函数传递给`Router.onBeforeAction()`,它就会变得具有反应性,而`Meteor.user()`是一个反应式对象,所以用户状态的任何变化都会重新运行这个函数。 + +现在我们已经创建了管理员用户和所需的模板,我们可以继续实际创建和编辑帖子。 + +# 总结 + +在本章中,我们学习了如何创建和登录用户,如何仅向已登录用户显示内容和模板,以及如何根据登录状态更改路由。 + +要了解更多,请查看以下链接: + ++ 在[`www.meteor.com/accounts`](https://www.meteor.com/accounts) + ++ 在[`docs.meteor.com/#/full/accounts_api`](https://docs.meteor.com/#/full/accounts_api) + ++ 在[`docs.meteor.com/#/full/meteor_users`](https://docs.meteor.com/#/full/meteor_users) + ++ [`en.wikipedia.org/wiki/Secure_Remote_Password_protocol`](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) + ++ [`github.com/EventedMind/iron-router/blob/devel/Guide.md#using-hooks`](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#using-hooks) + +您可以在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或 GitHub 上的[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7)找到本章的代码示例。 + +在下一章中,我们将学习如何创建和更新帖子以及如何从客户端控制数据库的更新。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_08.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_08.md new file mode 100644 index 0000000..ef437ae --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_08.md @@ -0,0 +1,435 @@ +# 第八章。使用允许和拒绝规则进行安全设置 + +在前一章中,我们创建了我们的管理员用户并准备了`editPost`模板。在本章中,我们将使这个模板工作,以便我们可以使用它创建和编辑帖子。 + +为了使插入和更新数据库中的文档成为可能,我们需要设置约束,使不是每个人都可以更改我们的数据库。在 Meteor 中,这是使用允许和拒绝规则完成的。这些函数将在文档被插入数据库前检查它们。 + +在本章中,您将涵盖以下主题: + ++ 添加和更新帖子 + ++ 使用允许和拒绝规则来控制数据库的更新 + ++ 在服务器上使用方法以获得更多灵活性 + ++ 使用方法桩来增强用户体验 + + ### 注意 + + 如果您直接跳到这一章节并希望跟随示例,请从书籍的网页[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或 GitHub 仓库[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7)下载前一章节的代码示例。 + + 这些代码示例还将包含所有的样式文件,所以我们不需要担心在过程中添加 CSS 代码。 + +# 添加一个生成 slug 的函数 + +为了从我们的帖子标题生成 slugs,我们将使用带有简单`slugify()`函数的`underscore-string`库。幸运的是,这个库的一个包装包已经在 Meteor 包服务器上存在。要添加它,我们请在终端中运行以下命令,位于我们的`my-meteor-blog`文件夹中: + +```js +$ meteor add wizonesolutions:underscore-string + +``` + +这将使用默认在 Meteor 中使用的`underscore`扩展一些额外的字符串函数,如`_.slugify()`,从字符串生成一个 slug。 + +# 创建新帖子 + +现在我们已经可以为每个创建的页面生成 slugs,我们可以继续将保存过程添加到`editPost`模板中。 + +为此,我们需要为我们的`editPost`模板创建一个 JavaScript 文件,通过将一个名为`editPost.js`的文件保存到`my-meteor-blog/client/templates`文件夹中来实现。在这个文件中,我们将为模板的**保存**按钮添加一个事件: + +```js +Template.editPost.events({ + 'submit form': function(e, template){ + e.preventDefault(); + console.log('Post saved'); + } +}); +``` + +现在,如果我们前往`/create-post`路由并点击**保存帖子**按钮,**帖子已保存**日志应该在浏览器控制台中出现。 + +## 保存帖子 + +为了保存帖子,我们只需取表单的内容并将其存储在数据库中。稍后,我们将重定向到新创建的帖子页面。为此,我们将我们的点击事件扩展为以下几行代码: + +```js +Template.editPost.events({ + 'submit form': function(e, tmpl){ + e.preventDefault(); + var form = e.target, + user = Meteor.user(); +``` + +我们获取当前用户,以便稍后将其作为帖子的作者添加。然后使用我们的`slugify()`函数从帖子标题生成一个 slug: + +```js + var slug = _.slugify(form.title.value); +``` + +接着,我们使用所有其他表单字段将帖子文档插入到`Posts`集合中。对于`timeCreated`属性,我们使用在第一章,*Meteor 入门*中添加的`moment`包获取当前的 Unix 时间戳。 + +`owner`字段将帮助我们确定是由哪个用户创建了此帖子: + +```js +Posts.insert({ + title: form.title.value, + slug: slug, + description: form.description.value, + text: form.text.value, + timeCreated: moment().unix(), + author: user.profile.name, + owner: user._id + + }, function(error) { + if(error) { + // display the error to the user + alert(error.reason); + } else { + // Redirect to the post + Router.go('Post', {slug: slug}); + } + }); + } +}); +``` + +我们传递给`insert()`函数的第二个参数是一个由 Meteor 提供的回调函数,如果出错,它将接收到一个错误参数。如果发生错误,我们警告它,如果一切顺利,我们使用生成的 slug 将用户重定向到新插入的帖子。 + +由于我们的路由控制器将会订阅这个 slug 的帖子,它将能够加载我们新创建的帖子并在帖子模板中显示它。 + +现在,如果我们打开浏览器,填写表单,并点击**保存**按钮,我们应该已经创建了我们的第一个帖子! + +# 编辑帖子 + +所以保存是可行的。编辑呢? + +当我们点击帖子中的**编辑**按钮时,我们将再次显示`editPost`模板。这次,表单字段填充了帖子的数据。到目前为止还不错,但如果我们现在点击**保存**按钮,我们将创建另一个帖子,而不是更新当前帖子。 + +## 更新当前帖子 + +由于我们设置了`editPost`模板的数据上下文,我们可以简单地使用帖子`_id`字段的存在作为更新的指示器,而不是插入帖子数据: + +```js +Template.editPost.events({ + 'submit form': function(e, tmpl){ + e.preventDefault(); + var form = e.target, + user = Meteor.user(), + _this = this; // we need this to reference the slug in the callback + + // Edit the post + if(this._id) { + + Posts.update(this._id, {$set: { + title: form.title.value, + description: form.description.value, + text: form.text.value + + }}, function(error) { + if(error) { + // display the error to the user + alert(error.reason); + } else { + // Redirect to the post + Router.go('Post', {slug: _this.slug}); + } + }); + + // SAVE + } else { + + // The insertion process ... + + } + } +}); +``` + +知道了`_id`,我们可以简单地使用`$set`属性来更新当前文档。使用`$set`只会覆盖`title`、`description`和`text`字段。其他字段将保持原样。 + +请注意,我们现在还需要在函数顶部创建`_this`变量,以便在回调 later 中访问当前数据上下文的`slug`属性。这样,我们稍后可以将用户重定向到我们编辑的帖子页面。 + +现在,如果我们保存文件并回到浏览器,我们可以编辑帖子并点击**保存**,所有更改都将如预期般保存到我们的数据库中。 + +现在,我们可以创建和编辑帖子。在下一节中,我们将学习如何通过添加允许和拒绝规则来限制对数据库的更新。 + +# 限制数据库更新 + +到目前为止,我们只是将插入和更新功能添加到了我们的`editPost`模板中。然而,如果有人在他们浏览器的控制台输入一个`insert`语句,任何人都可以插入和更新数据。 + +为了防止这种情况,我们需要在服务器端正确检查插入和更新权限,然后再更新数据库。 + +Meteor 的集合带有允许和拒绝函数,这些函数在每次插入或更新之前运行,以确定该操作是否被允许。 + +允许规则让我们允许某些文档或字段被更新,而拒绝规则覆盖任何允许规则,并肯定地拒绝对其集合的任何操作。 + +为了使这更加明显,让我们想象一个例子,我们定义了两个允许规则;其中一个将允许某些文档的`title`字段被更改,另一个只允许编辑`description`字段,但还有一个额外的拒绝规则可以防止某个特定文档在任何情况下被编辑。 + +## 删除不安全的包 + +为了开始使用允许和拒绝规则,我们需要从我们的应用程序中删除`insecure`包,这样客户端就不能简单地不通过我们的允许和拒绝规则就对我们的数据库进行更改。 + +使用终端中的*Ctrl* + *C* 停止运行中的`meteor`实例,并运行以下命令: + +```js +$ meteor remove insecure + +``` + +成功删除包后,我们可以使用`meteor`命令再次运行 Meteor。 + +当我们现在打开浏览器尝试编辑任何帖子时,我们将看到一个提示窗口,显示**访问被拒绝**。记得我们之前在更新或插入操作失败时添加了这个`alert()`调用吗? + +## 添加我们的第一个允许规则 + +为了使我们的帖子再次可编辑,我们需要添加允许规则以重新启用数据库更新。 + +为此,我们将在我们的`my-meteor-blog/collections.js`文件中添加以下允许规则,但在这个例子中,我们通过检查 Meteor 的`isServer`变量,使它们只在服务器端执行: + +```js +if(Meteor.isServer) { + + Posts.allow({ + insert: function (userId, doc) { + // The user must be logged in, and the document must be owned by the user + return userId && doc.owner === userId && Meteor.user().roles.admin; + }, +``` + +在插入*允许*规则中,我们只会在帖子所有者与当前用户匹配时插入文档,如果用户是管理员,我们可以在上一章中添加的`roles.admin`属性来确定。 + +如果允许规则返回`false`,将拒绝文档的插入。否则,我们将成功添加一个新帖子。更新也是一样,只是我们只检查当前用户是否是管理员: + +```js + update: function (userId, doc, fields, modifier) { + // User must be an admin + return Meteor.user().roles.admin; + }, + // make sure we only get this field from the documents + fetch: ['owner'] + }); +} +``` + +传递给`update`函数的参数如下表所示: + +| ```Field``` | 描述 | +| --- | --- | +| ```---``` | ```---``` | +| ```userId``` | 执行`update`操作的当前登录用户的用户 ID | +| ```doc``` | 数据库中的文档,不包括拟议的更改 | +| ```fields``` | 包含将要更新的字段参数的数组 | +| ```modifier``` | 用户传递给`update`函数的修改器,例如`{$set: {'name.first': "Alice"}, $inc: {score: 1}}` | + +我们最后在允许规则的对象中指定的`fetch`属性,决定了当前文档的哪些字段应该传递给更新规则。在我们这个例子中,我们只需要`owner`属性用于我们的更新规则。`fetch`属性存在是为了性能原因,以防止不必要的巨大文档被传递到规则函数中。 + +### 注意 + +此外,我们可以指定`remove()`规则和`transform()`函数。`remove()`规则将获得与`insert()`规则相同的参数,并允许或阻止文档的删除。 + +`transform()`函数可以用来在传递给允许或拒绝规则之前转换文档,例如,使其规范化。然而,要注意的是,这不会改变插入数据库的文档。 + +现在如果我们尝试在我们的网站上编辑一个帖子,我们应该能够编辑所有帖子以及创建新的帖子。 + +# 添加拒绝规则 + +为了提高安全性,我们可以修复帖子的所有者和创建时间。我们可以通过向我们的`Posts`集合中添加一个额外的拒绝规则来防止对所有者以及`timeCreated`和`slug`字段的更改,如下所示: + +```js +if(Meteor.isServer) { + + // Allow rules + + Posts.deny({ + update: function (userId, docs, fields, modifier) { + // Can't change owners, timeCreated and slug + return _.contains(fields, 'owner') || _.contains(fields, 'timeCreated') || _.contains(fields, 'slug'); + } + }); +} +``` + +这个规则将简单地检查`fields`参数是否包含受限制的字段之一。如果包含,我们就拒绝更新这篇帖子。所以,即使我们之前的允许规则已经通过,我们的拒绝规则也确保了文档不会发生变化。 + +我们可以在浏览器的控制台中尝试拒绝规则,当我们处于一个帖子页面时,输入以下命令: + +```js +Posts.update(Posts.findOne()._id, {$set: {'slug':'test'}}); + +``` + +这应该会给你一个错误,提示**更新失败:访问被拒绝**,如下面的截图所示: + +![添加拒绝规则](img/00023.jpeg) + +虽然我们现在可以添加和更新帖子,但还有一种比简单地将它们从客户端插入到我们的`Posts`集合中更好的添加新帖子的方法。 + +# 使用方法调用来添加帖子 + +方法是可以在客户端调用并在服务器上执行的函数。 + +## 方法存根和延迟补偿 + +方法的优势在于它们可以在服务器上执行代码,同时拥有完整的数据库和客户端上的存根方法。 + +例如,我们可以有一个方法在服务器上执行某些操作,并在客户端的存根方法中模拟预期的结果。这样,用户不必等待服务器的响应。存根还可以调用界面更改,例如添加一个加载指示器。 + +一个原生方法调用的例子是 Meteor 的`Collection.insert()`函数,它将执行客户端侧的函数,立即将文档插入到本地`minimongo`数据库中,同时发送一个请求在服务器上执行真正的`insert`方法。如果插入成功,客户端已经有了插入的文档。如果出现错误,服务器将响应并从客户端再次移除插入的文档。 + +在 Meteor 中,这个概念被称为**延迟补偿**,因为界面会立即对用户的响应做出反应,从而补偿延迟,而服务器的往返将在后台发生。 + +使用方法调用来插入帖子,使我们能够简单地检查我们想要为帖子使用的 slug 是否已经在另一篇帖子中存在。此外,我们还可以使用服务器的时间来为`timeCreated`属性确保我们没有使用错误的用户时间戳。 + +## 更改按钮 + +在我们的示例中,我们将简单地使用方法存根功能,在服务器上运行方法时将**保存**按钮的文本更改为`Saving…`。为此,执行以下步骤: + +1. 首先,让我们通过模板助手更改**保存**按钮的静态文本,以便我们可以动态地更改它。打开`my-meteor-blog/client/templates/editPost.html`,用以下代码替换**保存**按钮的代码: + + ```js + + ``` + +1. 现在打开`my-meteor-blog/client/templates/editPost.js`,在文件开头添加以下模板助手函数: + + ```js + Session.setDefault('saveButton', 'Save Post'); + Template.editPost.helpers({ + saveButtonText: function(){ + return Session.get('saveButton'); + } + }); + ``` + + 在这里,我们返回名为`saveButton`的会话变量,我们之前将其设置为默认值`Save Post`。 + +更改会话将允许我们在保存文档的同时稍后更改**保存**按钮的文本。 + +## 添加方法 + +现在我们有了一个动态的**保存**按钮,让我们在我们的应用中添加实际的方法。为此,我们将创建一个名为`methods.js`的新文件,直接位于我们的`my-meteor-blog`文件夹中。这样,它的代码将在服务器和客户端加载,这是在客户端作为存根执行方法所必需的。 + +添加以下代码以添加方法: + +```js +Meteor.methods({ + insertPost: function(postDocument) { + + if(this.isSimulation) { + Session.set('saveButton', 'Saving...'); + } + } +}); +``` + +这将添加一个名为`insertPost`的方法。在这个方法内部,存根功能已经通过使用`isSimulation`属性添加,该属性是通过 Meteor 在函数的`this`对象中提供的。 + +`this`对象还具有以下属性: + ++ `unblock()`:当调用此函数时,将防止该方法阻塞其他方法调用 + ++ `userId`:这包含当前用户的 ID + ++ `setUserId()`:这个函数用于将当前客户端连接到某个用户 + ++ `connection`:这是通过该方法在服务器上调用的连接 + +如果`isSimulation`设置为`true`,该方法不会在服务器端运行,而是作为存根在客户端运行。在这个条件下,我们简单地将`saveButton`会话变量设置为`Saving…`,以便按钮文本会更改: + +```js +Meteor.methods({ + insertPost: function(postDocument) { + + if(this.isSimulation) { + + Session.set('saveButton', 'Saving...'); + + } else { +``` + +为了完成方法,我们将添加帖子插入的服务器端代码: + +```js + var user = Meteor.user(); + + // ensure the user is logged in + if (!user) + throw new Meteor.Error(401, "You need to login to write a post"); +``` + +在这里,我们获取当前用户以添加作者名称和所有者 ID。 + +如果用户没有登录,我们就抛出异常,用`new Meteor.Error`。这将阻止方法的执行并返回我们定义的错误信息。 + +我们还查找具有给定 slug 的帖子。如果我们找到一个,我们在 slug 前添加一个随机字符串,以防止重复。这确保了每个 slug 都是唯一的,我们可以成功路由到我们新创建的帖子: + +```js + if(Posts.findOne({slug: postDocument.slug})) + postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3); +``` + +在我们插入新创建的帖子之前,我们使用`moment`库和`author`和`owner`属性添加`timeCreated`: + +```js + // add properties on the serverside + postDocument.timeCreated = moment().unix(); + postDocument.author = user.profile.name; + postDocument.owner = user._id; + + Posts.insert(postDocument); +``` + +在我们插入文档之后,我们返回修正后的 slug,然后在该方法调用的回调中作为第二个参数接收: + +```js + // this will be received as the second argument of the method callback + return postDocument.slug; + } + } +}); +``` + +# 调用方法 + +现在我们已经创建了`insertPost`方法,我们可以改变在`editPost.js`文件中之前插入帖子时的提交事件代码,用我们的方法进行调用: + +```js +var slug = _.slugify(form.title.value); + +Meteor.call('insertPost', { + title: form.title.value + slug: slug, + description: form.description.value + text: form.text.value, + +}, function(error, slug) { + Session.set('saveButton', 'Save Post'); + + if(error) { + return alert(error.reason); + } + + // Here we use the (probably changed) slug from the server side method + Router.go('Post', {slug: slug}); +}); +``` + +正如我们在方法调用的回调中看到的那样,我们使用在回调中作为第二个参数接收到的`slug`变量路由到新创建的帖子。这确保了如果`slug`变量在服务器端被修改,我们使用修改后的版本来路由到帖子。此外,我们将`saveButton`会话变量重置为将文本更改为`Save Post`。 + +就这样!现在,我们可以使用我们新创建的`insertPost`方法创建并保存新的帖子。然而,编辑仍然会在客户端使用`Posts.update()`进行,因为我们现在有了允许和拒绝规则,以确保只有允许的数据被修改。 + +# 总结 + +在本章中,我们学习了如何允许和拒绝数据库的更新。我们设置了自己的允许和拒绝规则,并了解了方法如何通过将敏感过程移动到服务器端来提高安全性。我们还通过检查 slug 是否已存在并在其中添加了一个简单的进度指示器来改进发帖过程。 + +如果您想更深入地了解允许和拒绝规则或方法,请查看以下 Meteor 文档: + ++ [`docs.meteor.com/#/full/allow`](http://docs.meteor.com/#/full/allow) + ++ [`docs.meteor.com/#/full/deny`](http://docs.meteor.com/#/full/deny) + ++ [`docs.meteor.com/#/full/methods_header`](https://docs.meteor.com/#/full/methods_header) + +您可以在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)找到本章的代码示例,或者在 GitHub 上找到[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8)。 + +在下一章中,我们将通过不断更新帖子的时间戳来使我们的界面实现实时更新。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_09.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_09.md new file mode 100644 index 0000000..c559260 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_09.md @@ -0,0 +1,356 @@ +# 第九章。高级响应式 + +现在我们的博客基本上已经完成了,因为我们能够创建和编辑文章。在本章中,我们将利用 Meteor 的响应式模板来使我们的界面时间戳自动更新。我们将构建一个响应式对象,该对象将重新运行模板助手,显示博客文章创建的时间。这样,它们总是显示正确的相对时间。 + +在本章中,我们将介绍以下内容: + ++ 响应式编程 + ++ 手动重新运行函数 + ++ 使用`Tracker`包构建响应式对象 + ++ 停止响应式函数 + + ### 注意 + + 如果你直接跳到这一章并想跟随示例,请从以下网址下载上一章的代码示例:[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或从 GitHub 仓库:[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8)。 + + 这些代码示例还将包含所有的样式文件,所以我们不需要担心在过程中添加 CSS 代码。 + +# 响应式编程 + +如我们已经在全书中看到的,Meteor 使用某种称为**响应性**的东西。 + +开发者在构建软件应用程序时必须解决的一个问题是指界面中表示数据的的一致性。大多数现代应用程序使用某种称为**模型-视图-控制器**(**MVC**)的东西,其中视图的控制器确保它始终表示模型的当前状态。模型通常是服务器 API 或浏览器内存中的 JSON 对象。 + +保持界面一致的最常见方法如下(来源:[`manual.meteor.com`](http://manual.meteor.com)): + ++ **轮询和差异**:定期(例如,每秒一次)获取事物的当前值,看看它是否发生变化,如果是,执行更新。 + ++ **事件**:可以变化的事物在变化时发出事件。程序的另一部分(通常称为控制器)安排监听这个事件,获取当前值,并在事件触发时执行更新。 + ++ **绑定**:值由实现某些接口的对象表示,例如`BindableValue`。然后,使用“绑定”方法将两个`BindableValues`连接在一起,这样当一个值发生变化时,另一个值会自动更新。有时,作为设置绑定的一部分,可以指定一个转换函数。例如,可以将`Foo`与`Bar`绑定,并使用`toUpperCase`转换函数。 + +这些模式很好,但它们仍然需要大量的代码来维护所表示数据的的一致性。 + +另一种模式,尽管还不是那么常用,那就是**响应式编程**。这种模式是一种声明式的数据绑定方式。这意味着当我们使用一个响应式数据源,如一个`Session`变量或`Mongo.Collection`时,我们可以确信,一旦其值发生变化,使用这些值的响应式函数或模板助手将重新运行,总是保持基于这些值的用户界面或计算更新。 + +米托尔手册为我们提供了一个响应式编程用法的实例: + +> 响应式编程非常适合构建用户界面,因为它不是试图用一段统一的代码来模拟所有的交互,而是让程序员表达在特定变化发生时应该发生的事情。响应变化的范式比显式地建模哪些变化会影响程序状态更容易理解。 +> +> 例如,假设我们正在编写一个 HTML5 应用程序,有一个项目表,用户可以点击一个项目来选择它,或者按 Ctrl 点击来选择多个项目。我们可能有一个`

    `标签,并希望该标签的内容等于当前选定项目的大写名称,如果有多个项目被选中,则为“Multiple selection”。而且,我们可能有一组``标签,并希望每个``标签的 CSS 类为“selected”,如果该项目对应的行在选定项目的集合中,否则为空字符串。 + +为了使这个例子在上述模式中实现,我们可以很快地看到,与响应式编程相比,它变得多么复杂(来源:[`manual.meteor.com`](http://manual.meteor.com)): + ++ 如果我们使用轮询和差分,UI 将会变得不可接受地卡顿。用户点击后,屏幕实际上直到下一次轮询周期才会更新。此外,我们必须存储旧的选定集合,并与新的选定集合进行差分,这有点麻烦。 + ++ 如果我们使用事件,我们就必须编写一些相当复杂的控制器代码,手动将选择的变化或选定项目的名称映射到 UI 的更新。例如,当选择发生变化时,我们必须记住更新`

    `标签和(通常)两个受影响的``标签。更重要的是,当选择发生变化时,我们必须自动在新生成的选定项目上注册一个事件处理程序,以便我们记住要更新`

    `。尤其是当 UI 被扩展和重新设计时,很难构建干净的代码并维护它。 + ++ 如果我们使用绑定,我们就必须使用一个复杂的**领域特定语言**(**DSL**)来表达变量之间复杂的 relationships。这个 DSL 必须包括间接性(将`

    `的内容绑定到当前选择的任何固定项目的名称,而是绑定到由当前选择指示的项目)、转换(将名称首字母大写)和条件(如果有多个项目被选择,显示一个占位符字符串)。 + +使用米托尔的反应式模板引擎 Blaze,我们可以简单地使用`{{#each}}`块助手来遍历一个元素列表,并根据用户交互或根据项目的属性添加一些条件以添加一个选中类。 + +如果用户现在更改数据或从服务器接收的数据发生变化,界面将自动更新以表示相应的数据,节省我们大量时间并避免不必要的复杂代码。 + +## 无效化周期 + +理解反应式依赖的关键部分是无效化周期。 + +当我们在一个反应式函数中使用反应式数据源,例如`Tracker.autorun(function(){…})`,反应式数据源本身看到它在一个反应式函数中,并将当前函数作为依赖项添加到其依赖存储中。 + +然后,当数据源的值发生变化时,它会无效化(重新运行)所有依赖的函数,并将它们从其依赖存储中移除。 + +在反应式函数的重新运行中,它会将反应式函数重新添加到其依赖存储中,这样在下次无效化(值变化)时它们会再次运行。 + +这是理解反应性的关键,正如我们在以下示例中所看到的。 + +想象我们有三个`Session`变量设置为`false`: + +```js +Session.set('first', false); +Session.set('second', false); +``` + +此外,我们还有`Tracker.autorun()`函数,它使用了这两个变量: + +```js +Tracker.autorun(function(){ + console.log('Reactive function re-run'); + if(Session.get('first')){ + Session.get('second'); + } +}); +``` + +现在我们可以调用`Session.set('second', true)`,但是反应式函数不会重新运行,因为在第一次运行中它从未被调用,因为`first`会话变量被设置为`false`。 + +如果我们现在调用`Session.set(first, true)`,该函数将重新运行。 + +此外,如果我们现在设置`Session.set('second', false)`,它也会重新运行,因为在第二次重新运行中,`Session.get('second')`可以添加这个反应式函数作为依赖项。 + +由于反应式数据源在每次无效化时都会从其存储中移除所有依赖项,并在反应式函数的重新运行中重新添加它们,因此我们可以设置`Session.set(first, false)`并尝试将其更改为`Session.set('second', true)`。函数将不再重新运行,因为在这个运行中从未调用过`Session.get('second')`! + +一旦我们理解了这一点,我们就可以实现更细粒度的反应性,将反应式更新保持在最小。解释的控制台输出与以下屏幕截图类似: + +![无效化周期](img/00024.jpeg) + +# 构建一个简单的反应式对象 + +正如我们所看到的,**反应式对象**是一个在反应式函数中使用的对象,当它的值发生变化时,它会重新运行函数。米托尔的`Session`对象是反应式对象的一个例子。 + +在本章中,我们将构建一个简单的反应式对象,它将在时间间隔内重新运行我们的`{{formatTime}}`模板助手,以便所有相对时间都能正确更新。 + +米托尔的反应性是通过`Tracker`包实现的。这个包是所有反应性的核心,允许我们跟踪依赖项并在需要时重新运行它们。 + +执行以下步骤以构建简单的反应式对象: + +1. 让我们开始吧,让我们将以下代码添加到`my-meteor-blog/main.js`文件中: + + ```js + if(Meteor.isClient) { + ReactiveTimer = new Tracker.Dependency; + } + ``` + + 这将在客户端创建一个名为`ReactiveTimer`的变量,带有`Tracker.Dependency`的新实例。 + +1. 在`ReactiveTimer`变量下方,但仍在`if(Meteor.isClient)`条件下,我们将添加以下代码,每 10 秒重新运行一次我们`ReactiveTimer`对象的的所有依赖项: + + ```js + Meteor.setInterval(function(){ + // re-run dependencies every 10s + ReactiveTimer.changed(); + }, 10000); + ``` + + `Meteor.setInterval`将每 10 秒运行一次函数。 + + ### 注意 + + Meteor 自带了`setInterval`和`setTimeout`的实现。尽管它们与原生 JavaScript 等效,但 Meteor 需要这些来引用服务器端特定用户的确切超时/间隔。 + +Meteor 自带了`setInterval`和`setTimeout`的实现。尽管它们与原生 JavaScript 等效,但 Meteor 需要这些来引用服务器端特定用户的确切超时/间隔。 + +在这个区间内,我们调用`ReactiveTimer.changed()`。这将使每个依赖函数失效,并重新运行。 + +## 重新运行函数 + +到目前为止,我们还没有创建依赖项,所以让我们这样做。在`Meteor.setInterval`下方添加以下代码: + +```js +Tracker.autorun(function(){ + ReactiveTimer.depend(); + console.log('Function re-run'); +}); +``` + +如果我们现在回到浏览器控制台,我们应该会看到每 10 秒**函数重新运行**一次,因为我们的反应式对象重新运行了函数。 + +我们甚至可以在浏览器控制台中调用`ReactiveTimer.changed()`,函数也会重新运行。 + +这些例子很好,但不会自动更新我们的时间戳。 + +为此,我们需要打开`my-meteor-blog/client/template-helpers.js`并在我们的`formatTime`助手函数顶部添加以下行: + +```js +ReactiveTimer.depend(); +``` + +这样,我们应用中的每个`{{formatTime}}`助手每 10 秒就会重新运行一次,更新流逝时的相对时间。要看到这一点,请打开浏览器,创建一篇新博客文章。现在保存博客文章,并观察创建时间文本,你会发现过了一会儿它会发生变化: + +![重新运行函数](img/00025.jpeg) + +# 创建高级计时器对象 + +之前的示例是一个自定义反应式对象的简单演示。为了使其更有用,最好创建一个单独的对象,隐藏`Tracker.Dependency`函数并添加其他功能。 + +Meteor 的反应性和依赖跟踪允许我们从另一个函数内部调用`depend()`函数时创建依赖项。这种依赖链允许更复杂的反应式对象。 + +在下一个示例中,我们将取我们的`timer`对象并为其添加`start`和`stop`函数。此外,我们还将使其能够选择一个时间间隔,在该时间间隔内计时器将重新运行: + +1. 首先,让我们从`main.js`和`template-helpers.js`文件中删除之前添加的代码示例,并在`my-meteor-blog/client`内创建一个名为`ReactiveTimer.js`的新文件,内容如下: + + ```js + ReactiveTimer = (function () { + + // Constructor + function ReactiveTimer() { + this._dependency = new Tracker.Dependency; + this._intervalId = null; + }; + + return ReactiveTimer; + })(); + ``` + + 这创建了一个经典的 JavaScript 原型类,我们可以使用`new ReactiveTimer()`来实例化它。在其构造函数中,我们实例化了一个`new Tracker.Dependency`并将其附加到该函数。 + +1. 现在,我们将创建一个`start()`函数,它将启动一个自选的间隔: + + ```js + ReactiveTimer = (function () { + + // Constructor + function ReactiveTimer() { + this._dependency = new Tracker.Dependency; + this._intervalId = null; + }; + ReactiveTimer.prototype.start = function(interval){ + var _this = this; + this._intervalId = Meteor.setInterval(function(){ + // rerun every "interval" + _this._dependency.changed(); + }, 1000 * interval); + }; + + return ReactiveTimer; + })(); + ``` + + 这是我们之前使用的相同代码,不同之处在于我们将间隔 ID 存储在`this._intervalId`中,这样我们可以在`stop()`函数中稍后停止它。传递给`start()`函数的间隔必须是秒; + +1. 接下来,我们在类中添加了`stop()`函数,它将简单地清除间隔: + + ```js + ReactiveTimer.prototype.stop = function(){ + Meteor.clearInterval(this._intervalId); + }; + ``` + +1. 现在我们只需要一个函数来创建依赖关系: + + ```js + ReactiveTimer.prototype.tick = function(){ + this._dependency.depend(); + }; + ``` + + 我们的反应式定时器准备好了! + +1. 现在,要实例化`timer`并使用我们喜欢的间隔启动它,请在文件末尾的`ReactiveTimer`类后添加以下代码: + + ```js + timer = new ReactiveTimer(); + timer.start(10); + ``` + +1. 最后,我们需要回到`template-helper.js`文件中的`{{formatTime}}`助手,并`添加``time.tick()`函数,界面上所有的相对时间都会随着时间流逝而更新。 + +1. 要看到反应式定时器的动作,可以在浏览器的控制台中运行以下代码片段: + + ```js + Tracker.autorun(function(){ + timer.tick(); + console.log('Timer ticked!'); + }); + ``` + +1. 我们应该现在每 10 秒看到一次**Timer ticked!**的日志。如果我们现在运行`time.stop()`,定时器将停止运行其依赖函数。如果我们再次调用`time.start(2)`,我们将看到`Timer ticked!`现在每两秒出现一次,因为我们设置了间隔为`2`:![创建一个高级定时器对象](img/00026.jpeg) + +正如我们所看到的,我们的`timer`对象现在相当灵活,我们可以在整个应用程序中创建任意数量的时间间隔。 + +# 反应式计算 + +Meteor 的反应性和`Tracker`包是一个非常强大的特性,因为它允许将事件行为附加到每个函数和每个模板助手。这种反应性正是保持我们界面一致性的原因。 + +虽然到目前为止我们只接触了`Tracker`包,但它还有几个我们应该查看的属性。 + +我们已经学习了如何实例化一个反应式对象。我们可以调用`new Tracker.Dependency`,它可以通过`depend()`和`changed()`创建和重新运行依赖关系。 + +## 停止反应式函数 + +当我们在一个反应式函数内部时,我们也能够访问到当前的计算对象,我们可以用它来停止进一步的反应式行为。 + +为了看到这个效果,我们可以在浏览器的控制台中使用我们已经在运行的`timer`,并使用`Tracker.autorun()`创建以下反应式函数: + +```js +var count = 0; +var someInnerFunction = function(count){ + console.log('Running for the '+ count +' time'); + + if(count === 10) + Tracker.currentComputation.stop(); +}; +Tracker.autorun(function(c){ + timer.tick(); + + someInnerFunction(count); + + count++; +}); + +timer.stop(); +timer.start(2); +``` + +在这里,我们创建了`someInnerFunction()`来展示我们如何从嵌套函数中访问当前计算。在这个内部函数中,我们使用`Tracker.currentComputation`获取计算,它给了我们当前的`Tracker.Computation`对象。 + +我们使用之前在`Tracker.autorun()`函数中创建的`count`变量进行计数。当我们达到 10 时,我们调用`Tracker.currentComputation.stop()`,这将停止内部依赖和`Tracker.autorun()`函数的依赖,使它们失去反应性。 + +为了更快地看到结果,我们在示例的末尾以两秒的间隔停止和开始`timer`对象。 + +如果我们把前面的代码片段复制并粘贴到浏览器的控制台并运行它,我们应该看到**Running for the xx time**出现 10 次: + +![停止响应式函数](img/00027.jpeg) + +当前计算对象对于从依赖函数内部控制响应式依赖项很有用。 + +## 防止在启动时运行 + +`Tracker``.Computation`对象还带有`firstRun`属性,我们在前一章中使用过。 + +例如,当使用`Tracker.autorun()`创建响应式函数时,它们在首次被 JavaScript 解析时也会运行。如果我们想要防止这种情况,我们可以在检查`firstRun`是否为`true`时简单地停止函数,在执行任何代码之前: + +```js +Tracker.autorun(function(c){ + timer.tick(); + + if(c.firstRun) + return; + + // Do some other stuff +}); +``` + +### 注意 + +我们在这里不需要使用`Tracker.currentComputation`来获取当前计算,因为`Tracker.autorun()`已经将其作为第一个参数。 + +同样,当我们停止`Tracker.autorun()`函数时,如以下代码所述,它将永远不会为会话变量创建依赖关系,因为第一次运行时从未调用`Session.get()`: + +```js +Tracker.autorun(function(c){ + if(c.firstRun) + return; + + Session.get('myValue'); +}): +``` + +为了确保我们使函数依赖于`myValue`会话变量,我们需要将它放在`return`语句之前。 + +## 高级响应式对象 + +`Tracker`包还有一些更高级的属性和函数,允许您控制何时无效化依赖项(`Tracker.flush()`和`Tracker.Computation.invalidate()`)以及允许您在它上面注册额外的回调(`Tracker.onInvalidate()`)。 + +这些属性允许您构建复杂的响应式对象,这超出了本书的范围。如果您想要更深入地了解`Tracker`包,我建议您查看 Meteor 手册中的[`manual.meteor.com/#tracker`](http://manual.meteor.com/#tracker)。 + +# 总结 + +在本章中,我们学习了如何构建我们自己的自定义响应式对象。我们了解了`Tracker.Dependency.depend()`和`Tracker.Dependency.changed()`,并看到了响应式依赖项具有自己的计算对象,可以用来停止其响应式行为并防止在启动时运行。 + +为了更深入地了解,请查看`Tracker`包的文档,并查看以下资源的`Tracker.Computation`对象的详细属性描述: + ++ [`www.meteor.com/tracker`](https://www.meteor.com/tracker) + ++ [`docs.meteor.com/#/full/tracker`](https://docs.meteor.com/#/full/tracker) + ++ [`docs.meteor.com/#/full/tracker_computation`](https://docs.meteor.com/#/full/tracker_computation) + ++ [`docs.meteor.com/#/full/tracker_dependency`](https://docs.meteor.com/#/full/tracker_dependency) + +你可以在本章的代码示例在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或者在 GitHub 上找到[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter9`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter9)。 + +现在我们已经完成了我们的博客,我们将在下一章看看如何将我们的应用程序部署到服务器上。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_10.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_10.md new file mode 100644 index 0000000..c751a1b --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_10.md @@ -0,0 +1,410 @@ +# 第十章 部署我们的应用程序 + +我们的应用程序现在已准备好部署。在本章中,我们将了解如何将我们的应用程序部署到不同的服务器上,使其公开并向世界展示我们所构建的内容。 + +Meteor 使得在自身的服务器基础设施上部署应用程序变得非常容易。操作免费且迅速,但可能不适合生产环境。因此,我们将探讨手动部署以及一些为在任何 Node.js 服务器上部署而构建的优秀工具。 + +在本章中,我们将涵盖以下主题: + ++ 注册 Meteor 开发者账户 + ++ 在 Meteor 的自有服务器基础设施上部署 + ++ 手动打包和部署 Meteor + ++ 使用 Demeteorizer 部署 + ++ 使用 Meteor Up 部署 + + ### 注意 + + 如果你想要部署本书中构建的完整应用程序,可以从以下网址下载代码:[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713) 或从 GitHub 仓库:[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10)。 + + 这段代码将不包括创建虚拟帖子的部分,因此你可以在自己的服务器上启动一个干净的博客。 + +# 在 meteor.com 上部署 + +Meteor 提供了自己的托管环境,其中每个人都可以用一个命令免费部署应用程序。为了部署应用程序,Meteor 会为我们创建一个开发者账户,以便我们稍后管理和部署应用程序。首先,让我们执行以下步骤,在 [meteor.com](http://meteor.com) 上部署我们的应用程序: + +1. 在 meteor.com 的子域上部署就像在我们的应用程序文件夹中的终端运行以下命令那么简单: + + ```js + $ meteor deploy myCoolNewBlog + + ``` + + 我们可以自由选择要部署的子域。如果 `myCoolNewBlog.meteor.com` 已经被占用,Meteor 会要求我们登录所有者的账户以覆盖当前部署的应用程序,或者我们必须选择另一个名字。 + +1. 如果域名可用,Meteor 会要求我们提供一个电子邮件地址,以便它为我们创建一个开发者账户。输入电子邮件地址后,我们将收到一封电子邮件,其中有一个链接设置我们的 Meteor 开发者账户,如下面的屏幕截图所示:![在 meteor.com 上部署](img/00028.jpeg) + +1. 为了创建我们的账户,我们需要遵循 Meteor 给出的链接,以便我们通过添加用户名和密码完全设置我们的账户,如下面的屏幕截图所示:![在 meteor.com 上部署](img/00029.jpeg) + +1. 完成这些操作后,我们将访问我们的开发者账户页面,在那里我们可以添加电子邮件地址,检查我们的最后登录,以及授权其他 Meteor 开发者登录到我们的应用程序(尽管我们首先必须添加 `accounts-meteor-developer` 包)。 + +1. 最后,要在终端中使用 `$ meteor login` 登录我们的 Meteor 开发者账户,输入我们的凭据,并再次运行 `deploy` 命令来最终部署我们的应用程序: + + ```js + $ meteor deploy myCoolNewBlog + + ``` + +1. 使用`$ meteor authorized –add `命令,我们可以允许其他 Meteor 开发者将应用程序部署到我们应用程序的子域,如下所示屏幕截图:![在 meteor.com 上部署](img/00030.jpeg) + +1. 如果我们想更新我们部署的应用程序,我们只需在我们应用程序的文件夹内运行`$ meteor deploy`。 Meteor 将要求我们提供凭据,然后我们可以部署我们的应用程序。 + +如果我们正在朋友的计算机上,并且想使用我们的 Meteor 账户,可以使用`$ meteor login`。 Meteor 将保持我们登录状态,并且每个人都可以重新部署我们的任何应用程序。 我们需要确保在完成时使用`$ meteor logout`。 + +## 使用域名在 meteor.com 上部署 + +我们还可以将应用程序托管在[meteor.com](http://meteor.com),但可以定义我们自己的域名。 + +要这样做,我们只需使用我们的域名进行部署,如下所示: + +```js +$ meteor deploy mydomain.com + +``` + +这将使应用程序托管在 meteor.com 上,但没有类似于[myapp.meteor.com](http://myapp.meteor.com)的直接 URL。 + +要将我们的域名指向 Meteor 服务器上的应用程序,我们需要将域名的**A 记录**更改为`origin.meteor.com`的 IP 地址(在撰写本书时为`107.22.210.133`),或**CNAME 记录**更改为`origin.meteor.com`。 您可以在注册域名的 DNS 配置中提供商处进行此操作。 + +Meteor 然后从我们的域名获取请求并在内部将其重定向到托管我们应用程序的服务器。 + +## 备份并恢复托管在 meteor.com 上的数据库 + +如果您需要备份数据库或将它移动到另一个服务器,您可以使用以下命令获取部署数据库的临时 Mongo 数据库凭据: + +```js +$ meteor mongo myapp.meteor.com –url + +``` + +这将获取类似于以下凭据: + +```js +mongodb://client-ID:xyz@production-db-b1.meteor.io:27017/yourapp_meteor_com + +``` + +然后,您可以使用前面输出的凭据使用`mongodump`备份您的数据库: + +```js +$ mongodump -h production-db-b1.meteor.io --port 27017 --username client-ID --password xyz --db yourapp_meteor_com + +``` + +这将在您所在位置创建一个名为`dump/yourapp_meteor_com`的文件夹,并将数据库的转储文件放在里面。 + +要恢复到另一个服务器,请使用`mongorestore`,最后一个参数是你放置数据库转储的文件夹: + +```js +$ mongorestore -h mymongoserver.com --port 27017 --username myuser --password xyz --db my_new_database dump/yourapp_meteor_com + +``` + +如果你只想将数据放入您本地的 Meteor 应用程序数据库中,请使用`$ meteor`启动 Meteor 服务器并运行以下命令: + +```js +$ mongorestore --port 3001 + +``` + +# 在其他服务器上部署 + +Meteor 的免费托管很棒,但当涉及到在生产中使用应用程序时,我们希望能够控制我们正在使用的服务器。 + +Meteor 允许我们将应用程序捆绑在一起,这样我们就可以在任何 Node.js 服务器上部署它。唯一的缺点是我们需要自己安装某些依赖项。此外,还有两个使部署应用程序几乎像 Meteor 本身一样简单的包,尽管它们的配置仍然需要。 + +## 捆绑我们的应用程序 + +为了在我们的服务器上部署应用,我们需要一个安装了最新版本的 Node.js 和 NPM 的 Linux 服务器。服务器应该和我们将要创建捆绑包的本地机器是同一平台。如果你想在另一个平台上部署你的应用,查看下一节。现在让我们通过以下步骤构建应用: + +1. 如果我们的服务器符合上述要求,我们可以在本地机器上的应用文件夹中运行以下命令: + + ```js + $ meteor build myAppBuildFolder + + ``` + +1. 这将创建一个名为`myAppBuildFolder`的文件夹,里面有一个`*.tar.gz`文件。然后我们可以将这个文件上传到我们的服务器,并在例如`~/Sites/myApp`下提取它。然后我们进入提取的文件夹并运行以下命令: + + ```js + $ cd programs/server + $ npm install + + ``` + +1. 这将安装所有的 NPM 依赖。安装完成后,我们设置必要的环境变量: + + ```js + $ export MONGO_URL='mongodb://user:password@host:port/databasename' + $ export ROOT_URL='http://example.com' + $ export MAIL_URL='smtp://user:password@mailhost:port/' + $ export PORT=8080 + + ``` + + `export`命令将设置`MONGO_URL`、`ROOT_URL`和`MAIL_URL`环境变量。 + +1. 由于这种手动部署没有预装 MongoDB,我们需要在我们的机器上安装它,或者使用像 Compose 这样的托管服务([`mongohq.com`](http://mongohq.com))。如果我们更愿意自己在服务器上安装 MongoDB,我们可以遵循在[`docs.mongodb.org/manual/installation`](http://docs.mongodb.org/manual/installation)的指南。 + +1. `ROOT_URL`变量应该是指向我们服务器的域的 URL。如果我们的应用发送电子邮件,我们还可以设置自己的 SMTP 服务器,或使用像 Mailgun 这样的服务([`mailgun.com`](http://mailgun.com))并更改`MAIL_URL`变量中的 SMTP 主机。 + + 我们也可以指定我们希望应用运行的端口,使用`PORT`环境变量。如果我们没有设置`PORT`变量,它将默认使用端口`80`。 + +1. 设置这些变量后,我们转到应用的根目录,并使用以下命令启动服务器: + + ```js + $ node main.js + + ``` + + ### 提示 + + 如果你想确保你的应用在崩溃或服务器重启时能够重新启动,可以查看`forever` NPM 包,具体解释请参阅[`github.com/nodejitsu/forever`](https://github.com/nodejitsu/forever)。 + +如果一切顺利,我们的应用应该可以通过`:8080`访问。 + +如果我们手动部署应用时遇到麻烦,我们可以使用接下来的方法。 + +## 使用 Demeteorizer 部署 + +使用`$ meteor build`的缺点是,大多数 node 模块已经被编译,因此在服务器环境中可能会造成问题。因此出现了 Demeteorizer,它与`$ meteor build`非常相似,但还会额外解压捆绑包,并创建一个包含所有 node 依赖项和项目正确 node 版本的`package.json`文件。以下是使用 Demeteorizer 部署的方法: + +1. Demeteorizer 作为一个 NPM 包提供,我们可以使用以下命令安装: + + ```js + $ npm install -g demeteorizer + + ``` + + ### 注意 + + 如果`npm`文件夹没有正确的权限,请在命令前使用`sudo`。 + +1. 现在我们可以去应用文件夹并输入以下命令: + + ```js + $ demeteorizer -o ~/my-meteor-blog-converted + + ``` + +1. 这将把准备分发的应用程序输出到`my-meteor-blog-converted`文件夹。我们只需将这个文件夹复制到我们的服务器上,设置与之前描述相同的环境变量,并运行以下命令: + + ```js + $ cd /my/server/my-meteor-blog-converted + $ npm install + $ node main.js + + ``` + +这应该会在我们指定的端口上启动我们的应用程序。 + +## 使用 Meteor Up 部署 + +前面的步骤可以帮助我们在自己的服务器上部署应用程序,但这种方法仍然需要我们构建、上传和设置环境变量。 + +**Meteor Up**(**mup**)旨在使部署像运行`$ meteor deploy`一样简单。然而,如果我们想要使用 Meteor Up,我们需要在服务器上拥有完全的管理权限。 + +此外,这允许我们在应用程序崩溃时自动重新启动它,使用`forever` NPM 包,以及在服务器重新启动时启动应用程序,使用`upstart` NPM 包。我们还可以恢复先前的部署版本,这为我们提供了在生产环境部署的良好基础。 + +### 注意 + +接下来的步骤是针对更高级的开发人员,因为它们需要在服务器机器上设置`sudo`权限。因此,如果您在部署方面没有经验,可以考虑使用像 Modulus 这样的服务([`modulus.io`](http://modulus.io)),它提供在线 Meteor 部署,使用自己的命令行工具,可在[`modulus.io/codex/meteor_apps`](https://modulus.io/codex/meteor_apps)找到。 + +Meteor Up 将按照以下方式设置服务器并部署我们的应用程序: + +1. 要在我们的本地机器上安装`mup`,我们输入以下命令: + + ```js + $ npm install -g mup + + ``` + +1. 现在我们需要创建一个用于部署配置的文件夹,这个文件夹可以位于我们的应用程序所在的同一个文件夹中: + + ```js + $ mkdir ~/my-meteor-blog-deployment + $ cd ~/my-meteor-blog-deployment + $ mup init + + ``` + +1. Meteor Up 为我们创建了一个配置文件,它看起来像以下这样: + + ```js + { + "servers": [ + { + "host": "hostname", + "username": "root", + "password": "password" + // or pem file (ssh based authentication) + //"pem": "~/.ssh/id_rsa" + } + ], + "setupMongo": true, + "setupNode": true, + "nodeVersion": "0.10.26", + "setupPhantom": true, + "appName": "meteor", + "app": "/Users/arunoda/Meteor/my-app", + "env": { + "PORT": 80, + "ROOT_URL": "http://myapp.com", + "MONGO_URL": "mongodb://arunoda:fd8dsjsfh7@hanso.mongohq.com:10023/MyApp", + "MAIL_URL": "smtp://postmaster%40myapp.mailgun.org:adj87sjhd7s@smtp.mailgun.org:587/" + }, + "deployCheckWaitTime": 15 + } + ``` + +1. 现在我们可以编辑这个文件以适应我们的服务器环境。 + +1. 首先,我们将添加 SSH 服务器认证。我们可以提供我们的 RSA 密钥文件,或者提供一个用户名和密码。如果我们想要使用后者,我们需要安装`sshpass`,一个用于在不使用命令行的前提下提供 SSH 密码的工具: + + ```js + "servers": [ + { + "host": "myServer.com", + "username": "johndoe", + "password": "xyz" + // or pem file (ssh based authentication) + //"pem": "~/.ssh/id_rsa" + } + ], + ``` + + ### 注意 + + 要为我们的环境安装`sshpass`,我们可以按照[`gist.github.com/arunoda/7790979`](https://gist.github.com/arunoda/7790979)的步骤进行,或者如果您在 Mac OS X 上,可以查看[`www.hashbangcode.com/blog/installing-sshpass-osx-mavericks`](http://www.hashbangcode.com/blog/installing-sshpass-osx-mavericks)。 + +1. 接下来,我们可以设置一些选项,例如选择在服务器上安装 MongoDB。如果我们使用像 Compose 这样的服务,我们将将其设置为`false`: + + ```js + "setupMongo": false, + ``` + + 如果我们已经在我们的服务器上安装了 Node.js,我们还将将下一个选项设置为`false`: + + ```js + "setupNode": false, + ``` + + 如果我们想要指定一个特定的 Node.js 版本,我们可以如下设置: + + ```js + "nodeVersion": "0.10.25", + ``` + + Meteor Up 还可以为我们安装 PhantomJS,这对于我们使用 Meteor 的 spiderable 包是必要的,这个包可以使我们的应用程序被搜索引擎爬取: + + ```js + "setupPhantom": true, + ``` + + 在下一个选项中,我们将设置我们应用程序的名称,它可以与我们的应用程序文件夹名称相同: + + ```js + "appName": "my-meteor-blog", + ``` + + 最后,我们需要指向我们的本地应用程序文件夹,以便 Meteor Up 知道要部署什么: + + ```js + "app": "~/my-meteor-blog", + ``` + +1. Meteor Up 还允许我们预设所有必要的环境变量,例如正确的`MONGO_URL`变量: + + ```js + "env": { + "ROOT_URL": "http://myServer.com", + "MONGO_URL": "mongodb://user:password@host:port/databasename", + "PORT": 8080 + }, + ``` + +1. 最后一个选项设置了 Meteor Up 在检查应用是否成功启动前会等待的时间: + + ```js + "deployCheckWaitTime": 15 + ``` + +### 设置服务器 + +为了使用 Meteor Up 设置服务器,我们需要对`sudo`进行无密码访问。按照以下步骤设置服务器: + +1. 为了启用无密码访问,我们需要将当前用户添加到服务器的`sudo`组中: + + ```js + $ sudo adduser sudo + + ``` + +1. 然后在`sudoers`文件中添加`NOPASSWD`: + + ```js + $ sudo visudo + + ``` + +1. 现在用以下这行替换`%sudo ALL=(ALL) ALL`行: + + ```js + %sudo ALL=(ALL) NOPASSWD:ALL + + ``` + +### 使用 mup 部署 + +如果一切顺利,我们可以设置我们的服务器。以下步骤解释了如何使用`mup`进行部署: + +1. 从本地`my-meteor-blog-deployment`目录中运行以下命令: + + ```js + $ mup setup + + ``` + + 这将配置我们的服务器并安装配置文件中选择的全部要求。 + + 一旦这个过程完成,我们随时可以通过在同一目录下运行以下命令来部署我们的应用: + + ```js + $ mup deploy + + ``` + +通过创建两个具有不同应用名称的 Meteor Up 配置,我们还可以创建生产和演示环境,并将它们部署到同一服务器上。 + +# 前景 + +目前,Meteor 将原生部署限制在其自己的服务器上,对环境控制有限。计划推出一款企业级服务器基础设施,名为**Galaxy**,它将使部署和扩展 Meteor 应用像 Meteor 本身一样简单。 + +尽管如此,凭借 Meteor 的简洁性和强大的社区,我们已经拥有部署到任何基于 Node.js 的托管和 PaaS 环境的丰富工具集。 + +### 注意 + +例如,如果我们想在 Heroku 上部署,我们可以查看 Jordan Sissel 在[`github.com/jordansissel/heroku-buildpack-meteor`](https://github.com/jordansissel/heroku-buildpack-meteor)上的构建包。 + +# 总结 + +在本章中,我们学习了如何部署 Meteor,以及在 Meteor 自己的服务器架构上部署可以有多么简单。我们还使用了 Demegorizer 和 Meteor Up 这样的工具来部署我们自己的服务器架构。 + +要了解更多具体的部署方法,请查看以下资源: + ++ [`www.meteor.com/services/developer-accounts`](https://www.meteor.com/services/developer-accounts) + ++ [`docs.meteor.com/#/full/deploying`](https://docs.meteor.com/#/full/deploying) + ++ [`www.meteor.com/services/build`](https://www.meteor.com/services/build) + ++ [`github.com/onmodulus/demeteorizer`](https://github.com/onmodulus/demeteorizer) + ++ [`github.com/arunoda/meteor-up`](https://github.com/arunoda/meteor-up) + +您可以在这个应用的[完整示例代码](https://www.packtpub.com/books/content/support/17713)中找到准备部署的版本,或者在 GitHub 上查看[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10)。 + +在下一章中,我们将创建一个包含我们之前创建的`ReactiveTimer`对象的包,并将其发布到 Meteor 的官方包仓库。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_11.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_11.md new file mode 100644 index 0000000..4ccebc2 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_11.md @@ -0,0 +1,351 @@ +# 第十一章。构建我们自己的包 + +在本章中,我们将学习如何构建自己的包。编写包允许我们创建可以共享在许多应用中的闭合功能组件。在本章的后半部分,我们将把我们的应用发布到 Atmosphere,Meteor 的第三方包仓库,地址为[`atmospherejs.com`](https://atmospherejs.com)。 + +在本章中,我们将涵盖以下主题: + ++ 结构化一个包 + ++ 创建一个包 + ++ 发布自己的包 + + ### 注意 + + 在本章中,我们将包装在第九章,*高级反应性*中构建的`ReactiveTimer`对象。要遵循本章中的示例,请从以下任一位置下载上一章的代码示例:[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)(书籍网页)或[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10)(GitHub 仓库)。 + +# 包的结构 + +包是一个包含特定变量暴露给 Meteor 应用的 JavaScript 文件集合。除了在 Meteor 应用中,包文件将按我们指定的加载顺序加载。 + +每个包都需要一个`package.js`文件,该文件包含该包的配置。在这样的文件中,我们可以添加一个名称、描述和版本,设置加载顺序,并确定哪些变量应该暴露给应用。此外,我们还可以为我们的包指定单元测试来测试它们。 + +`package.js`文件的一个例子可能看起来像这样: + +```js +Package.describe({ + name: "mrt:moment", + summary: "Moment.js, a JavaScript date library.", + version: "0.0.1", + git: "https://..." +}); + +Package.onUse(function (api, where) { + api.export('moment'); + + api.addFiles('lib/moment-with-langs.min.js', 'client'); +}); + +Package.onTest(function(api){ + api.use(["mrt:moment", "tinytest"], ["client", "server"]); + api.addFiles("test/tests.js", ["client", "server"]); +}); +``` + +我们可以按照自己的意愿结构包中的文件和文件夹,但以下安排是一个好的基础: + +![包的结构](img/00031.jpeg) + ++ `tests`:包含包的单元测试和`tests.js`文件 + ++ `lib`:包含包使用的第三方库 + ++ `README.md`:包含使用包的简单说明 + ++ `package.js`: 此文件包含包的元数据 + ++ `myPackage.js`:这些是包含包代码的一个或多个文件 + +要测试一个包,我们可以使用 Meteor 的`tinytest`包,它是一个简单的单元测试包。如果我们有测试,我们可以使用以下命令运行它们: + +```js +$ meteor test-packages + +``` + +这将启动一个 Meteor 应用,地址为`http://localhost:3000`,它运行我们的包测试。要了解如何编写一个包,请查看下一章。 + +# 创建自己的包 + +要创建自己的包,我们将使用我们在第九章,*高级反应性*中构建的`ReactiveTimer`对象: + +1. 我们来到终端,在我们的应用文件夹中运行以下命令: + + ```js + $ meteor create --package reactive-timer + + ``` + +1. 这将创建一个名为`packages`的文件夹,其中有一个`reactive-timer`文件夹。在`reactive-timer`文件夹内,Meteor 已经创建了一个`package.js`文件和一些示例包文件。 + +1. 现在我们可以删除`reactive-timer`文件夹内的所有文件,除了`package.js`文件。 + +1. 然后我们将我们在第九章 *高级反应性*中创建的`my-meteor-blog/client/ReactiveTimer.js`文件移动到我们新创建的`reactive-timer`包文件夹中。 + +1. 最后,我们打开复制的`ReactiveTimer.js`文件,并删除以下行: + + ```js + timer = new ReactiveTimer(); + timer.start(10); + ``` + + 稍后,我们在应用本身内部实例化`timer`对象,而不是在包文件中。 + +现在我们应该有一个简单的文件夹,带有默认的`package.js`文件和我们的`ReactiveTimer.js`文件。这几乎就是全部了!我们只需要配置我们的包,就可以在应用中使用它了。 + +## 添加包元数据 + +要添加包的元数据,我们打开名为`package.js`的文件,并添加以下代码行: + +```js +Package.describe({ + name: "meteor-book:reactive-timer", + summary: "A simple timer object, which can re-run reactive functions based on an interval", + version: "0.0.1", + // optional + git: "https://github.com/frozeman/meteor-reactive-timer" +}); +``` + +这为包添加了一个名称、一个描述和一个版本。 + +请注意,包名称与作者的名称命名空间。这样做的目的是,通过它们的作者名称,可以使具有相同名称的包区分开来。在我们这个案例中,我们选择`meteor-book`,这并不是一个真实的用户名。要发布包,我们需要使用我们真实的 Meteor 开发者用户名。 + +在`Package.describe()`函数之后是实际的包依赖关系: + +```js +Package.onUse(function (api) { + // requires Meteor core packages 1.0 + api.versionsFrom('METEOR@1.0'); + + // we require the Meteor core tracker package + api.use('tracker', 'client'); + + // and export the ReactiveTimer variable + api.export('ReactiveTimer'); + + // which we find in this file + api.addFiles('ReactiveTimer.js', 'client'); +}); +``` + +在这里,我们定义了这个包应该使用的 Meteor 核心包的版本: + ++ 使用`api.use()`,我们定义了这个包依赖的额外包(或包)。请注意,这些依赖不会被使用这个包的应用本身访问到。 + + ### 注意 + + 另外,还存在`api.imply()`,它不仅使另一个包在包的文件中可用,而且还将它添加到 Meteor 应用本身,使其可以被应用的代码访问。 + ++ 如果我们使用第三方包,我们必须指定最低的包版本,如下所示: + + ```js + api.use('author:somePackage@1.0.0', 'server'); + + ``` + + ### 注意 + + 我们还可以传入第三个参数,`{weak: true}`,以指定只有在开发者已经将依赖包添加到应用中时,才会使用该依赖包。这可以用来在有其他包存在时增强一个包。 + ++ 在`api.use()`函数的第二个参数中,我们可以指定是否在客户端、服务器或两者上都加载它,使用数组: + + ```js + api.use('tracker', ['client', 'server']); + + ``` + + ### 提示 + + 我们实际上不需要导入`Tracker`包,因为它已经是 Meteor 核心`meteor-platform`包的一部分(默认添加到任何 Meteor 应用中);我们在这里这样做是为了示例。 + ++ 然后我们使用`api.export('ReactiveTimer')`来定义包中应该向使用此包的 Meteor 应用公开哪个变量。记住,我们在`ReactiveTimer.js`文件中使用以下代码行创建了`ReactiveTimer`对象: + + ```js + ReactiveTimer = (function () { + ... + })(); + ``` + + ### 注意 + + 请注意,我们没有使用`var`来创建变量。这样,它在包的所有其他文件中都可以访问,也可以暴露给应用本身。 + ++ 最后,我们使用`api.addFiles()`告诉包系统哪些文件属于这个包。我们可以有`api.addFiles()`的多个调用,一个接一个。这个顺序将指定文件的加载顺序。 + + 在这里,我们再次告诉 Meteor 将文件加载到哪个地方——客户端、服务器还是两者都加载——使用`['client', 'server']`。 + + 在这个例子中,我们只在客户端提供了`ReactiveTimer`对象,因为 Meteor 的反应式函数只存在于客户端。 + + ### 注意 + + 如果你想要查看`api`对象的所有方法,请查看 Meteor 的文档[`docs.meteor.com/#packagejs`](http://docs.meteor.com/#packagejs)。 + +## 添加包 + +将包文件夹复制到`my-meteor-blog/packages`文件夹中并不足以让 Meteor 使用这个包。我们需要遵循额外的步骤: + +1. 为了添加包,我们需要从终端前往我们的应用文件夹,停止任何正在运行的`meteor`实例,并运行以下命令: + + ```js + $ meteor add meteor-book:reactive-timer + + ``` + +1. 然后,我们需要在我们的应用中实例化`ReactiveTimer`对象。为此,我们需将以下代码行添加到我们的`my-meteor-blog/main.js`文件中: + + ```js + if(Meteor.isClient) { + timer = new ReactiveTimer(); + timer.start(10); + } + ``` + +1. 现在我们可以再次使用`$ meteor`启动 Meteor 应用,并在`http://localhost:3000`打开我们的浏览器。 + +我们应该看不到任何区别,因为我们只是用我们`meteor-book:reactive-timer`包中的`ReactiveTimer`对象替换了应用中原本的`ReactiveTimer`对象。 + +为了看到计时器运行,我们可以打开浏览器的控制台并运行以下的代码片段: + +```js +Tracker.autorun(function(){ + timer.tick(); + console.log('timer run'); +}); +``` + +这应该会每 10 秒记录一次`timer run`,显示我们的包实际上是在工作的。 + +# 发布我们的包给公众 + +向世界发布一个包是非常容易的,但为了让人们使用我们的包,我们应该添加一个 readme 文件,这样他们就可以知道如何使用我们的包。 + +在我们之前创建的包文件夹中创建一个名为`README.md`的文件,并添加以下的代码片段: + +```js +# ReactiveTimer + +This package can run reactive functions in a given interval. +## Installation + + $ meteor add meteor-book:reactive-timer + +## Usage + +To use the timer, instantiate a new interval: + + var myTimer = new ReactiveTimer(); + +Then you can start an interval of 10 seconds using: + + myTimer.start(10); + +To use the timer just call the following in any reactive function: + + myTimer.tick(); + +To stop the timer use: + + myTimer.stop(); +``` + +正如我们所见,这个文件使用了 Markdown 语法。这样,它将在 GitHub 和[`atmospherejs.com`](http://atmospherejs.com)上看起来很不错,这是一个你可以浏览所有可用 Meteor 包的网站。 + +通过这个 readme 文件,我们将使其他人更容易使用我们的包并欣赏我们的工作。 + +## 在线发布我们的包 + +在我们保存了`readme`文件之后,我们可以将这个包推送到 GitHub 或其他的在线 Git 仓库,并将仓库的 URL 添加到`package.js`文件的`Package.describe({git: …})`变量中。将代码托管在 GitHub 上可以保证它的安全性,并允许他人进行分叉和改进。下面让我们来进行将我们的包推送到线上的步骤: + +1. 发布我们的包,我们可以在终端的`pages`文件夹内简单地运行以下命令: + + ```js + $ meteor publish --create + + ``` + + 这会构建并捆绑包,然后上传到 Meteor 的包服务器上。 + +1. 如果一切顺利,我们应该能够通过输入以下命令找到我们的包: + + ```js + $ meteor search reactive-timer + + ``` + + 这在下面的截图中有所说明: + + ![在线发布我们的包](img/00032.jpeg) + +1. 然后,我们可以使用以下命令显示找到的包的所有信息: + + ```js + $ meteor show meteor-book:reactive-timer + + ``` + + 这在上面的截图中说明: + + ![在线发布我们的包](img/00033.jpeg) + +1. 要使用来自 Meteor 服务器的包版本,我们只需将`packages/reactive-timer`文件夹移到别处,删除`package`文件夹,然后运行`$ meteor`来启动应用程序。 + + 现在 Meteor 在`packages`文件夹中找不到具有该名称的包,并将在线查找该包。既然我们发布了它,它将被下载并用于我们的应用程序。 + +1. 如果我们想在我们的应用程序中使用我们包的特定版本,我们可以在终端中从我们应用程序的文件夹内运行以下命令: + + ```js + $ meteor add meteor-book:reactive-timer@=0.0.1 + + ``` + +现在我们的包已经发布,我们可以在`http://atmospherejs.com/meteor-book/reactive-timer`看到它,如下所示: + +![在线发布我们的包](img/00034.jpeg) + +### 注意 + +请注意,这只是一个包的示例,并且从未实际发布过。然而,在[`atmospherejs.com/frozeman/reactive-timer`](http://atmospherejs.com/frozeman/reactive-timer)以我的名义发布的这个包的版本可以找到。 + +## 更新我们的包 + +如果我们想发布我们包的新版本,我们只需在`package.js`文件中增加版本号,然后从`packages`文件夹内使用以下命令发布新版本: + +```js +$ meteor publish + +``` + +要使我们的应用程序使用我们包的最新版本(只要我们没有指定固定版本),我们只需在终端内从我们的应用程序文件夹中运行以下命令: + +```js +$ meteor update meteor-book:reactive-timer + +``` + +如果我们想更新所有包,我们可以运行以下命令: + +```js +$ meteor update –-packages-only + +``` + +# 总结 + +在本章中,我们从我们的`ReactiveTimer`对象创建了自己的包。我们还了解到,在 Meteor 的官方打包系统中发布包是多么简单。 + +要深入了解,请阅读以下资源中的文档: + ++ [`docs.meteor.com/#/full/writingpackages`](https://docs.meteor.com/#/full/writingpackages) + ++ [`docs.meteor.com/#packagejs`](https://docs.meteor.com/#packagejs) + ++ [Meteor 包服务器](https://www.meteor.com/services/package-server) + ++ [`www.meteor.com/isobuild`](https://www.meteor.com/isobuild) + +您可以在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或 GitHub 上[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter11`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter11)找到本章的代码示例。 + +这个代码示例只包含包,所以为了将其添加到应用程序中,请使用前一章的代码示例。 + +在下一章中,我们将查看测试我们的应用程序和包。 diff --git a/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_12.md b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_12.md new file mode 100644 index 0000000..6734e91 --- /dev/null +++ b/docs/bd-sgl-pg-webapp-mtr/bd-sgl-pg-webapp-mtr_12.md @@ -0,0 +1,518 @@ +# 第十二章. Meteor 中的测试 + +在这个最后的章节中,我们将讨论我们如何测试一个 Meteor 应用。 + +测试是一个广泛的话题,超出了本章的范围。为了简化,我们将简要介绍两种可用的工具,因为它们确实不同,并为每种工具提供一个简单的示例。 + +在本章中,我们将介绍以下主题: + ++ 测试 `reactive-timer` 包 + ++ 使用 Jasmine 对我们应用进行单元测试 + ++ 使用 Nightwatch 对我们应用进行验收测试 + + ### 注意 + + 如果你想要直接进入章节并跟随示例,请下载第十章,*部署我们的应用*的代码,它包含了完成的示例应用,可以从书籍的网页[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或从 GitHub 仓库[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10)获取。 + +# 测试类型 + +测试是用来验证其他代码或应用功能的代码片段。 + +我们可以将测试分为四个主要组: + ++ **单元测试**:在这个测试中,我们只测试我们代码的小单元。这可以,例如,是一个函数或一段代码。单元测试不应该调用其他函数,向硬盘或数据库写入,或访问网络。如果需要这样的功能,应该编写桩函数,这些函数返回期望的值而不调用真正的函数。 + ++ **集成测试**:在这个测试中,我们将多个测试结合起来,在不同的环境中运行它们,以确保它们仍然有效。与单元测试相比,这个测试的不同之处在于,我们实际上是在运行连接的功能,比如调用数据库。 + ++ **功能测试**:这可以是单元测试或界面测试,但只测试功能特性/函数的功能,而不检查副作用,例如是否适当地清理了变量。 + ++ **验收测试**:这个测试在完整的系统上运行,例如,一个网络浏览器。想法是尽可能地模仿实际用户。这些测试与定义功能的用户故事非常相似。这种测试的缺点是,它使得追踪错误变得困难,因为测试发生在较高的层次。 + +在下面的示例中,我们主要会为了简化而编写功能测试。 + +# 测试包 + +在上一章中,我们基于 `ReactiveTimer` 对象构建了一个包。一个好的包应该总是包含单元测试,这样人们就可以运行它们,并确信对该包所做的更改不会破坏其功能。 + +Meteor 为包提供了一个简单的单元测试工具,称为 `TinyTest`,我们将使用它来测试我们的包: + +1. 要添加测试,我们需要将我们在上一章中构建的`meteor-book:reactive-timer`包复制到我们应用的`my-meteor-blog/packages`文件夹中。这样,我们可以修改包,因为 Meteor 将优先选择`packages`文件夹中的包而不是其包服务器中的包。如果你移除了包,只需使用以下命令将其重新添加: + + ```js + $ meteor add meteor-book:reactive-timer + + ``` + + ### 注意 + + 此外,我们需要确保我们删除`my-meteor-blog/client/ReactiveTimer.js`文件,如果我们使用了来自第十章 *部署我们的应用* 的代码示例作为基础的话,我们应该有的。 + +1. 然后我们打开我们`packages`文件夹中的`package.js`文件,并在文件的末尾添加以下几行代码: + + ```js + Package.onTest(function (api) { + api.use('meteor-book:reactive-timer', 'client'); + api.use('tinytest', 'client'); + + api.addFiles('tests/tests.js', 'client'); + }); + ``` + + 这将包括我们的`meteor-book:reactive-timer`包和`tinytest`,在运行测试时。然后它将运行`tests.js`文件,其中将包含我们的单元测试。 + +1. 现在,我们可以通过在我们的包文件夹中添加一个名为`tests`的文件夹,并在其中创建一个名为`tests.js`的文件来创建测试。 + + 目前,`tinytest`包没有被 Meteor 文档化,但它很小,这意味着它非常简单。 + + 基本上,有两个函数,`Tinytest.add(test)`和`Tinytest.addAsync(test, expect)`。它们都运行一个简单的测试函数,我们可以使用`test.equal(x, y)`,`test.isTrue(x)`,或`test.isUndefined(x)`来通过或失败这个函数。 + + 对于我们的包测试,我们将简单地测试在启动计时器后`ReactiveTimer._intervalId`是否不再为 null,这样我们就可以知道计时器是否运行了。 + +## 添加包测试 + +测试首先描述将要测试的内容。 + +要测试`_intervalId`,我们在我们的`tests.js`文件中添加以下几行代码: + +```js +Tinytest.add('The timer set the _intervalId property', function (test) { + var timer = new ReactiveTimer(); + timer.start(1); + + test.isTrue(timer._intervalId !== null); + + timer.stop(); +}); +``` + +然后我们启动一个计时器,并测试其`_intervalId`属性是否不再为 null。最后,我们再次停止计时器以清理测试。 + +接下来,我们将把我们`tests.js`文件中要添加的下一个测试设置为异步,因为我们需要等待计时器至少运行一次: + +```js +Tinytest.addAsync('The timer run', function (test, expect) { + var run = false, + timer = new ReactiveTimer(); + timer.start(1); + + Tracker.autorun(function(c){ + timer.tick(); + + if(!c.firstRun) + run = true; + }); + + Meteor.setTimeout(function(){ + test.equal(run, true); + timer.stop(); + + expect(); + }, 1010); +}); +``` + +让我们来看看这个异步测试中发生了什么: + ++ 首先,我们再次以 1 秒的间隔启动计时器,并创建了一个名为`run`的变量。我们只在我们的反应式`Tracker.autorun()`函数运行时将这个变量切换为`true`。请注意,我们使用了`if(!c.firstRun)`来防止在函数第一次执行时设置`run`变量,因为我们只希望在 1 秒后的“滴答”计数。 + ++ 然后我们使用`Meteor.setTimeout()`函数检查`run`是否被更改为`true`。`expect()`告诉`Tinytest.addAsync()`测试已经结束并输出结果。请注意,我们还停止了计时器,因为我们需要在每个测试后清理。 + +## 运行包测试 + +要最终运行测试,我们可以从我们应用的根目录运行以下命令: + +```js +$ meteor test-packages meteor-book:reactive-timer + +``` + +这将启动一个 Meteor 应用并运行我们的包测试。要查看它们,我们导航到`http://localhost:3000`: + +![运行包测试](img/00035.jpeg) + +### 提示 + +我们也可以通过命名由空格分隔的多个包来同时运行一个以上的包测试: + +```js +$ meteor test-packages meteor-book:reactive-timer iron:router + +``` + +为了看看测试是否有效,我们将通过注释掉 `my-meteor-book/packages/reactive-timer/ReactiveTimer.js` 文件中的 `Meteor.setInterval()` 来故意使它失败,如下所示: + +![运行包测试](img/00036.jpeg) + +我们应该始终尝试使我们的测试失败,因为一个测试也可能是编写成永远不会成功或失败的方式(例如,当 `expect()` 从未被调用时)。这将阻止其他测试的执行,因为当前的测试可能永远不会完成。 + +一个好的经验法则是,测试功能时要好像我们正在看一个黑箱。如果我们根据函数是如何编写的来过度定制我们的测试,那么在我们改进函数时修复测试会比较困难。 + +# 测试我们的 Meteor 应用 + +为了测试应用本身,我们可以使用 Velocity Meteor 的官方测试框架。 + +Velocity 本身不包含测试工具,而是为诸如 Jasmine 或 Mocha 等测试包提供了一种统一的方式来测试 Meteor 应用,并使用 `velocity:html-reporter` 包在控制台或应用界面本身报告它们的输出。 + +让我们引用他们自己的话: + +> *Velocity 监控您的 tests/ 目录,并将测试文件发送到正确的测试插件。测试插件执行测试,并在完成后将每个测试的结果发送回 Velocity。然后 Velocity 结合所有测试插件的结果,并通过一个或多个报告插件输出它们。当应用或测试发生变化时,Velocity 将重新运行您的测试并反应性地更新结果。* + +这段内容来自 [`velocity.meteor.com`](http://velocity.meteor.com)。此外,Velocity 还增加了诸如 Meteor 存根和自动存根等功能。它能够为隔离测试创建镜像应用,并运行设置代码(测试数据)。 + +现在,我们将查看使用 Jasmine 的单元测试和使用 Nightwatch 的验收测试。 + +## 使用 Jasmine 测试 + +为了使用 Jasmine 和 Velocity,我们需要安装 `sanjo:jasmine` 包以及 `velocity:html-reporter` 包。 + +为此,我们将从我们的 apps 文件夹内运行以下命令: + +```js +$ meteor add velocity:html-reporter + +``` + +然后,我们使用以下命令为 Meteor 安装 Jasmine: + +```js +$ meteor add sanjo:jasmine + +``` + +为了让 Velocity 能够找到测试,我们需要创建以下文件结构: + +```js +- my-meteor-blog + - tests + - jasmine + - client + - unit + - integration + - server + - unit +``` + +现在,当我们使用 `$ meteor` 启动 Meteor 服务器时,我们会发现 Jasmine 包已经在 `/my-meteor-blog/tests/jasmine/server/unit` 文件夹中创建了两个文件,其中包含我们包的存根。 + +### 向服务器添加单元测试 + +现在我们可以向客户端和服务器添加单元测试。在这本书中,我们将只向服务器添加一个单元测试,稍后向客户端添加集成测试,以保持在本书章节的范围内。这样做步骤如下: + +1. 首先,我们在 `/my-meteor-blog/tests/jasmine/server/unit` 文件夹中创建一个名为 `postSpecs.js` 的文件,并添加以下命令: + + ```js + describe('Post', function () { + ``` + + 这将创建一个描述测试内部将涉及什么的测试框架。 + +1. 在测试框架内,我们调用`beforeEach()`和`afterEach()`函数,这两个函数分别在每个测试之前和之后运行。在其中,我们将使用`MeteorStubs.install()`为所有的 Meteor 函数创建桩,并使用`MeteorStubs.uninstall()`之后清理它们: + + ```js + beforeEach(function () { + MeteorStubs.install(); + }); + + afterEach(function () { + MeteorStubs.uninstall(); + }); + ``` + + ### 注意 + + 桩是一个模仿其原始函数或对象的功能或对象,但不会运行实际代码。相反,桩可以用来返回函数我们测试依赖的特定值。 + + 桩确保单元测试只测试特定的代码单元,而不是它的依赖。否则,依赖函数或对象的一个断裂会导致其他测试链失败,使得找到实际问题变得困难。 + +1. 现在我们可以编写实际的测试。在这个例子中,我们将测试我们之前在书中创建的`insertPost`方法是否插入了帖子,并确保不会插入重复的 slug: + + ```js + it('should be correctly inserted', function() { + + spyOn(Posts, 'findOne').and.callFake(function() { + // simulate return a found document; + return {title: 'Some Tite'}; + }); + + spyOn(Posts, 'insert'); + + spyOn(Meteor, 'user').and.returnValue({_id: 4321, profile: {name: 'John'}}); + + spyOn(global, 'moment').and.callFake(function() { + // simulate return the moment object; + return {unix: function(){ + return 1234; + }}; + }); + ``` + + 首先,我们为`insertPost`方法中使用的所有函数创建桩,以确保它们返回我们想要的结果。 + + 特别是,看看`spyOn(Posts, "findOne")`调用。正如我们可以看到的,我们调用了一个假函数,并返回了一个只有标题的假文档。实际上,我们可以返回任何东西,因为`insertPost`方法只检查是否找到了具有相同 slug 的文档。 + +1. 接下来,我们实际上调用该方法并给它一些帖子数据: + + ```js + Meteor.call('insertPost', { + title: 'My Title', + description: 'Lorem ipsum', + text: 'Lorem ipsum', + slug: 'my-title' + }, function(error, result){ + ``` + +1. 在方法的回调内,我们添加了实际的测试: + + ```js + expect(error).toBe(null); + + // we check that the slug is returned + expect(result).toContain('my-title'); + expect(result.length).toBeGreaterThan(8); + + // we check that the post is correctly inserted + expect(Posts.insert).toHaveBeenCalledWith({ + title: 'My Title', + description: 'Lorem ipsum', + text: 'Lorem ipsum', + slug: result, + timeCreated: 1234, + owner: 4321, + author: 'John' + }); + }); + }); + ``` + + 首先,我们检查错误对象是否为 null。然后我们检查方法生成的 slug 是否包含`'my-title'`字符串。因为我们在较早的`Posts.findOne()`函数中返回了一个假文档,所以我们期望我们的方法会给 slug 添加一些随机数,比如`'my-title-fotvadydf4rt3xr'`。因此,我们检查其长度是否大于原始`'my-title'`字符串的八个字符。 + + 最后,我们检查`Post.insert()`函数是否被调用了期望的值。 + + ### 注意 + + 为了完全理解如何测试 Jasmine,请查看文档[`jasmine.github.io/2.0/introduction.html`](https://jasmine.io/2.0/introduction.html)。 + + 你也可以在[`www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing`](http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing)找到一个很好的 Jasmine 函数速查表。 + +1. 最后,我们关闭开始时的`describe(...`函数: + + ```js + }); + ``` + +如果我们现在再次使用`$ meteor`启动我们的 Meteor 应用,过一会儿我们会在右上角看到一个绿色点。 + +点击这个点可以让我们访问 Velocity 的`html-reporter`,它应该能显示我们的测试已经通过: + +![向服务器添加单元测试](img/00037.jpeg) + +为了使我们的测试失败,让我们去到我们的`my-meteor-blog/methods.js`文件,并将以下行注释掉: + +```js +if(Posts.findOne({slug: postDocument.slug})) + postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3); +``` + +这将防止 slug 被更改,即使已经存在具有相同 slug 的文档,也会使我们的测试失败。如果我们回头在浏览器里检查,我们应该会看到测试失败: + +![向服务器添加单元测试](img/00038.jpeg) + +我们只需通过添加新的`it('应该是什么', function() {...});`函数来添加更多测试。 + +### 向客户端添加集成测试 + +添加集成测试与添加单元测试一样简单。区别在于所有的测试规格文件都放到`my-meteor-blog/tests/jasmine/client/integration`文件夹里。 + +与单元测试不同,集成测试在实际应用环境中运行。 + +#### 为访客添加测试 + +在我们第一个示例测试中,我们将测试确保访客看不到**创建文章**按钮。在第二个测试中,我们将以管理员身份登录,检查我们是否能看到它。 + +1. 让我们在我们`my-meteor-blog/tests/jasmine/client/integration`文件夹里创建一个名为`postButtonSpecs.js`的文件。 + +1. 现在我们向文件添加以下代码片段并保存它: + + ```js + describe('Vistors', function() { + it('should not see the create posts link', function () { + var div = document.createElement('DIV'); + Blaze.render(Template.home, div); + + expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); + }); + }); + ``` + +```js +postButtonSpecs.js file as the one we used before: +``` + +```js +describe('The Admin', function() { + afterEach(function (done) { + Meteor.logout(done); + }) + + it('should be able to login and see the create post link', function (done) { + var div = document.createElement('DIV'); + Blaze.render(Template.home, div); + + Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { + + Tracker.afterFlush(function(){ + + expect($(div).find('a.createNewPost')[0]).toBeDefined(); + expect(err).toBeUndefined(); + + done(); + }); + + }); + }); +}); +``` + +这里我们再次向一个`div`中添加`home`模板,但这次我们使用管理员凭据以管理员身份登录。登录后,我们调用`Tracker.afterFlush()`给 Meteor 时间重新渲染模板,然后检查按钮是否现在出现。 + +因为这个测试是异步运行的,我们需要调用`done()`函数,这个函数是作为`it()`函数的参数传递的,告诉 Jasmine 测试结束了。 + +### 注意 + +由于 Meteor 不会把文件捆绑在`tests`目录里,我们测试文件里的凭据是安全的。 + +如果我们现在回到浏览器,我们应该会看到两个集成测试通过了: + +![为管理员添加测试](img/00040.jpeg) + +创建测试后,我们总是应该确保尝试失败测试以查看它是否真的工作。为此,我们只需在`my-meteor-blog/client/templates/home.html`中注释掉`a.createNewPost`链接。 + +### 注意 + +你可以使用 PhantomJS 如下运行 Velocity 测试: + +```js +$ meteor run --test + +``` + +首先需要全局安装 PhantomJS,使用`$ npm install -g phantomjs`。请注意,撰写此书时此特性是实验性的,可能运行不了你的所有测试。 + +# 验收测试 + +尽管我们可以用这些测试分别测试客户端和服务器代码,但我们不能测试两者之间的交互。为此,我们需要验收测试,如果详细解释,将超出本章节的范围。 + +在撰写本文的时候,还没有使用 Velocity 实施的验收测试框架,尽管有两个你可以使用。 + +## Nightwatch + +`clinical:nightwatch`包让你能简单地运行验收测试,如下所示: + +```js +"Hello World" : function (client) { + client + .url("http://127.0.0.1:3000") + .waitForElementVisible("body", 1000) + .assert.title("Hello World") + .end(); +} +``` + +尽管安装过程不像安装 Meteor 包那样直接,但在运行测试之前,你自己需要安装并运行 MongoDB 和 PhantomJS。 + +如果你想尝试一下,请查看 atmosphere-javascript 网站上的包:[`atmospherejs.com/clinical/nightwatch`](https://atmospherejs.com/clinical/nightwatch)。 + +## Laika + +如果你想测试服务器与客户端之间的通信,可以使用 Laika。它的安装过程与 Nightwatch 相似,因为它需要单独安装 MongoDB 和 PhantomJS。 + +Laika 启动一个服务器实例并连接多个客户端。然后你可以设置订阅或插入并修改文档。你还可以测试它们在客户端的外观。 + +要安装 Laika,请访问[`arunoda.github.io/laika/`](http://arunoda.github.io/laika/)。 + +### 注意 + +在撰写本文时,Laika 与 Velocity 不兼容,后者试图在 Laika 的环境中运行测试文件夹中的所有文件,导致错误。 + +# 总结 + +在这最后一章中,我们学习了如何使用 Meteor 官方测试框架 Velocity 的`sanjo:jasmine`包编写简单的单元测试。我们还简要介绍了可能的验收测试框架。 + +如果你想更深入地了解测试,可以查看以下资源: + ++ [`velocity.meteor.com`](http://velocity.meteor.com) + ++ [`jasmine.github.io`](http://jasmine.github.io) + ++ [`www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing`](http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing) + ++ [`doctorllama.wordpress.com/2014/09/22/bullet-proof-internationalised-meteor-applications-with-velocity-unit-testing-integration-testing-and-jasmine/`](http://doctorllama.wordpress.com/2014/09/22/bullet-proof-internationalised-meteor-applications-with-velocity-unit-testing-integration-testing-and-jasmine/) + ++ [`arunoda.github.io/laika/`](http://arunoda.github.io/laika/) + ++ [`github.com/xolvio/velocity`](https://github.com/xolvio/velocity) + +你可以在这本书的代码文件在[`www.packtpub.com/books/content/support/17713`](https://www.packtpub.com/books/content/support/17713)或者在 GitHub 上[`github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter12`](https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter12)找到。 + +既然你已经读完了整本书,我假设你对 Meteor 的了解比以前多了很多,对这个框架也和我一样兴奋! + +关于 Meteor 的任何问题,你都可以在[`stackoverflow.com`](http://stackoverflow.com)上提问,那里有一个很棒的 Meteor 社区。 + +我还建议阅读[`www.meteor.com/projects`](https://www.meteor.com/projects)上的所有 Meteor 子项目,并研究[`docs.meteor.com`](https://docs.meteor.com)上的文档。 + +希望你能享受阅读这本书的过程,现在你已经准备好使用 Meteor 框架来制作伟大的应用程序了! + +# 附录 A. 附录 + +附录中包含 Meteor 命令行工具的命令列表和对`iron:router`钩子的简要描述。 + +# 命令行工具命令列表 + +| 选项 | 描述 | +| --- | --- | +| `run` | 使用`meteor run`与使用`meteor`相同。这将为我们应用启动一个 Meteor 服务器并监控文件更改。 | +| `create <名称>` | 这将通过创建一个同名的文件夹来初始化一个 Meteor 项目,并有一些初始文件。 | +| `update` | 这将更新我们当前的 Meteor 应用到最新版本。我们还可以使用`meteor update --release xyz`来将我们的 Meteor 应用修复到一个特定的版本。 | +| `deploy <站点名称>` | 这将把我们的 Meteor 应用部署到`<站点名称>.meteor.com`。我们可以传递`--delete`选项来删除一个已部署的应用 | +| `build <文件夹名称>` | 这将创建一个文件夹,其中包含我们捆绑的应用代码,准备部署到我们自己的服务器。 | +| `add/remove <包名称>` | 这将向/从我们的项目中添加或删除一个 Meteor 核心包。 | +| `list` | 这将列出我们的应用正在使用的所有 Meteor 包。 | +| `mongo` | 这会让我们访问本地 MongoDB shell。我们同时还需要启动我们的应用`meteor run`。如果我们需要访问部署在[meteor.com](http://meteor.com)上的应用的 mongo 数据库,使用`$ meteor mongo yourapp.meteor.com --url`但要小心,这些凭据仅有效 1 分钟。 | +| `reset` | 这将把我们的本地开发数据库重置为空白状态。当我们的应用运行时此操作将无效。注意这将删除我们存储在本地数据库中的所有数据。 | +| `logs <站点名称>` | 这将下载并显示我们在`<站点名称>.meteor.com`部署的应用的日志。 | +| `search` | 这会搜索包含指定正则表达式的 Meteor 包和发布版本。 | +| `show` | 这会显示有关特定包或版本的更多信息:名称、摘要、其维护者的用户名,以及(如果指定)其主页和 Git URL。 | +| `publish` | 这会发布我们的包。我们之前必须使用 cd 命令进入包文件夹,使用`$ meteor login`登录到我们的 Meteor 账户。要第一次发布一个包,我们使用`$ meteor publish --create`。 | +| `publish-for-arch` | 这会从不同的架构发布一个现有包版本的构建。*我们的机器必须有正确的架构才能为特定架构发布。*目前,Meteor 支持的架构有 32 位 Linux、64 位 Linux 和 Mac OS。Meteor `deploy`运行的服务器使用 64 位 Linux。 | +| `publish-release` | 这会发布 Meteor 的一个版本。这需要一个 JSON 配置文件。更多详细信息,请访问[`docs.meteor.com/#/full/meteorpublishrelease`](https://docs.meteor.com/#/full/meteorpublishrelease)。 | +| `claim` | 这会将使用旧 Meteor 版本的站点通过我们的 Meteor 开发者账户进行认领。 | +| `login` | 这会将我们登录到 Meteor 开发者账户。 | +| `logout` | 这会将我们登出 Meteor 开发者账户。 | +| `whoami` | 这会打印我们 Meteor 开发者账户的用户名。 | +| `test-packages` | 这将运行一个或多个包的测试。有关更多信息,请参阅第十二章, *使用 Meteor 进行测试*。 | +| `admin` | 此部分用于捕获需要授权才能使用的各种命令。Meteor `admin`的一些示例用途包括添加和删除包维护者以及为包设置主页。它还包括用于管理 Meteor 版本的各种帮助函数。 | + +# 铁轨:路由钩子 | + +以下表格包含路由控制器钩子的列表: | + +| `action` | 这个函数可以覆盖路由的默认行为。如果我们定义这个函数,我们必须手动使用`this.render()`渲染模板。 | +| --- | --- | +| `onBeforeAction` | 这个函数在路由渲染前运行。在这里,我们可以放置额外的自定义操作。 | +| `onAfterAction` | 这个函数在路由渲染后运行。在这里,我们可以放置额外的自定义操作。 | +| `onRun` | 当路由第一次加载时,此函数运行一次。在热代码重载或再次导航相同的 URL 时,此函数不会再次运行。 | +| `onRerun` | 每次调用此路由时,此函数将被调用。 | +| `onStop` | 当离开当前路由到新路由时,此函数运行一次。 | +| `subscriptions` | 这个函数可以返回影响`this.ready()`在动作钩子中的订阅。 | +| `waitOn` | 这个函数可以返回订阅,但在那些准备好之前会自动渲染`loadingTemplate`。 | +| `data` | 此函数的返回值将设置为此路由模板的数据上下文。 | + +这些钩子的完整解释可以在以下资源中找到: | + ++ [`github.com/EventedMind/iron-router/blob/devel/Guide.md#layouts`](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#layouts) + ++ [`github.com/EventedMind/iron-router/blob/devel/Guide.md#hooks`](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#hooks) + ++ [`github.com/EventedMind/iron-router/blob/devel/Guide.md#rendering-templates-with-data`](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#rendering-templates-with-data) diff --git a/docs/deno-web-dev/SUMMARY.md b/docs/deno-web-dev/SUMMARY.md new file mode 100644 index 0000000..7e35eca --- /dev/null +++ b/docs/deno-web-dev/SUMMARY.md @@ -0,0 +1,11 @@ ++ [第一章:*第三章*:运行时和标准库](deno-web-dev_00.md) ++ [第二部分:构建应用程序](deno-web-dev_01.md) ++ [第四章:构建网络应用程序](deno-web-dev_02.md) ++ [第三章:第*5 章*:添加用户和迁移到 Oak](deno-web-dev_03.md) ++ [第四章:*第六章*:添加认证并连接到数据库](deno-web-dev_04.md) ++ [第五章:*第七章*:HTTPS,提取配置和 Deno 在浏览器中](deno-web-dev_05.md) ++ [第三部分:测试和部署](deno-web-dev_06.md) ++ [第八章:测试 - 单元和集成](deno-web-dev_07.md) ++ [第七章:第*9*章:部署 Deno 应用程序](deno-web-dev_08.md) ++ [第八章:*第十章*:接下来是什么?](deno-web-dev_09.md) ++ [第九章:为什么要订阅?](deno-web-dev_10.md) \ No newline at end of file diff --git a/docs/deno-web-dev/deno-web-dev_00.md b/docs/deno-web-dev/deno-web-dev_00.md new file mode 100644 index 0000000..7874c39 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_00.md @@ -0,0 +1,929 @@ +# 第一章:*第三章*:运行时和标准库 + +现在我们已经足够了解 Deno,我们可以用它来写一些真正的应用程序。在本章中,我们将不使用任何库,因为其主要目的是介绍运行时 API 和标准库。 + +我们将编写小的 CLI 工具、Web 服务器等,始终利用官方 Deno 团队创建的力量,没有外部依赖。 + +我们的起点将是 Deno 命名空间,因为我们认为首先探索运行时包含的内容是有意义的。遵循这个想法,我们还将查看 Deno 与浏览器共享的 Web API。我们将使用`setTimeout`到`addEventListener`、`fetch`等。 + +仍然在 Deno 命名空间中,我们将了解程序的生命周期,与文件系统交互,并构建小的命令行程序。后来,我们将了解缓冲区,并了解如何使用它们异步地读写。 + +然后我们将快速转向标准库,并浏览一些有用的模块。本章旨在取代标准库的文档;它将为您提供一些其功能和使用案例的介绍,而我们在编写小程序的过程中将了解它。 + +在标准库的旅程中,我们将使用与文件系统、ID 生成、文本格式化和 HTTP 通信相关的模块。其中一部分将是介绍我们将在后续章节中深入探讨的内容。您将通过编写第一个 JSON API 并连接到它来完成本章。 + +以下是我们将在本章中涵盖的主题: + ++ Deno 运行时 + ++ 探索 Deno 命名空间 + ++ 使用标准库 + ++ 使用 HTTP 模块构建 Web 服务器 + +## 技术要求 + +本章的所有代码文件都可以在以下 GitHub 链接中找到:[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03)。 + +# Deno 运行时 + +Deno 提供了一组函数,这些函数作为全局变量包含在`Deno`命名空间中。运行时 API 在[`doc.deno.land/`](https://doc.deno.land/)上进行文档化,可以用来做最基本、最底层的事情。 + +Deno 上有两种函数无需任何导入即可使用:Web API 和`Deno`命名空间。每当 Deno 中存在与浏览器中相同的行为时,Deno 会模仿浏览器 API——这些是 Web API。由于您来自 JavaScript 世界,您可能熟悉其中的大部分内容。我们谈论的是诸如`fetch`、`addEventListener`、`setTimeout`等函数,以及`window`、`Event`、`console`等对象等。 + +使用 Web API 编写的代码可以无需转换即可捆绑并在浏览器中运行。 + +运行时暴露的 API 的另一个大部分位于一个名为`Deno`的全局命名空间中。你可以使用 REPL 和文档,这两样我们在第二章《工具链》中探索过,来探索它并快速了解它包括哪些函数。在本章后面,我们还将尝试一些最常用的函数。 + +如果你想访问 Deno 中包含的所有符号的文档,你可以运行带有`--builtin`标志的`doc`命令。 + +## 稳定性 + +从版本 1.0.0 开始,Deno 命名空间内的函数被认为是稳定的。这意味着 Deno 团队将努力在更新的版本中支持它们,并尽最大努力使它们与未来的变化保持兼容。 + +仍然没有被认为是在生产中稳定的特性,正如你想象的那样,因为我们已经在之前的例子中使用过它们,都位于`--unstable`标志下。 + +不稳定模块的文档可以通过使用`doc`命令的`--unstable`标志访问,或者通过访问[`doc.deno.land/builtin/unstable`](https://doc.deno.land/builtin/unstable)来访问。 + +标准库尚未被 Deno 团队认为是稳定的,因此它们的版本与 CLI(在撰写本文时,版本为 0.83.0)不同。 + +与`Deno`命名空间函数相比,标准库通常不需要`--unstable`标志来运行,除非标准库中的任何模块正在使用来自`Deno`命名空间的不可稳定函数。 + +## 程序生命周期 + +Deno 支持与浏览器兼容的`load`和`unload`事件,可以用来运行设置和清理代码。 + +处理程序可以以两种不同的方式编写:使用`addEventListener`和通过重写`window.onload`和`window.onunload`函数。`load`事件可以是异步的,但`unload`事件不是真的,因为它们不能被取消。 + +使用`addEventListener`可以让你注册无限数量的处理程序;例如: + +```js +addEventListener("load", () => { +  console.log("loaded 1"); +}); +addEventListener("unload", () => { +  console.log("unloaded 1"); +}); +addEventListener("load", () => { +  console.log("loaded 2"); +}); +addEventListener("unload", () => { +  console.log("unloaded 2"); +}); +console.log("Exiting..."); +``` + +如果我们运行前面的代码,我们会得到以下输出: + +```js +$ deno run program-lifecycle/add-event-listener.js +Exiting... +loaded 1 +loaded 2 +unloaded 1 +unloaded 2 +``` + +另一种在设置和清理阶段安排代码运行的方法是重写`window`对象上的`onload`和`onunload`函数。这些函数的特点是只有最后分配的一个会运行。这是因为它们会相互覆盖;例如以下代码: + +```js +window.onload = () => { +  console.log("onload 1"); +}; +window.onunload = () => { +  console.log("onunload 1"); +}; +window.onload = () => { +  console.log("onload 2"); +}; +window.onunload = () => { +  console.log("onunload 2"); +}; +console.log("Exiting"); +``` + +运行前面的程序后,我们得到了以下输出: + +```js +$ deno run program-lifecycle/window-on-load.js +Exiting +onload 2 +onunload 2 +``` + +然后如果我们看看我们最初编写的代码,我们可以理解前两个声明被跟在它们后面的两个声明覆盖了。当我们覆盖`onunload`和`onload`时,就是这样发生的。 + +## 网络 API + +为了证明我们可以像在浏览器上一样使用 Web API,我们将编写一个简单的程序,获取 Deno 网站的标志,将其转换为 base64,并在控制台打印一个包含图像 base64 的 HTML 页面。让我们按照以下步骤进行: + +1. 从请求`https://deno.land/logo.svg`开始: + + ```js + fetch("https://deno.land/logo.svg") + ``` + +1. 将其转换为`blob`: + + ```js + fetch("https://deno.land/logo.svg") +   .then(r =>r.blob()) + ``` + +1. 从`blob`对象中获取文本并将其转换为`base64`: + + ```js + fetch("https://deno.land/logo.svg ") +   .then(r =>r.blob()) +   .then(async (img) => { +     const base64 = btoa( +       await img.text() +     ) + }); + ``` + +1. 将 base64 图像打印到控制台的 HTML 页面中,并使用图像标签: + + ```js + fetch("https://deno.land/logo.svg ") +   .then(r =>r.blob()) +   .then(async (img) => { + const base64 = btoa( +       await img.text() +     ) +     console.log(` + + +     ` +     ) +   }) + ``` + + 当我们运行这个时,我们得到了预期的输出: + + ```js + $ deno run --allow-net web-apis/fetch-deno-logo.js + +    { +     const randomLine = Math.floor(Math.min(Math.random() *        1000, logLines.length)); +     buffer.write(encoder.encode(logLines[randomLine])); +  },   100) +} +``` + +这段代码从示例文件中读取内容并将其分割成行。然后,它获取一个随机的行号,每 100 毫秒将那一行写入缓冲区。这个文件然后导出一个函数,我们可以调用它来“生成随机日志”。我们将在下一个脚本中使用这个来模仿一个产生日志的应用程序。 + +现在来到了有趣的部分:我们将按照这些步骤编写基本的*日志处理器*: + +1. 创建一个缓冲区并将其发送给我们刚刚编写的日志生产者的`start`函数: + + ```js + import start from "./logCreator.ts"; + const buffer = new Deno.Buffer(); + start(buffer); + ``` + +1. 调用`processLogs`函数来开始处理缓冲区中存在的日志条目: + + ```js + … + start(buffer); + processLogs(); + async function processLogs() {} + ``` + + 正如你所看到的,`processLogs`函数会被调用,但是什么也不会发生,因为我们还没有实现一个程序来执行它。 + +1. 在`processLogs`函数内部创建一个`Uint8Array`对象类型,并读取那里的缓冲区内容: + + ```js + … + async function processLogs() { +   const destination = new Uint8Array(100); +   const readBytes = await buffer.read(destination); +   if (readBytes) { +     // Something was read from the buffer +   } + } + ``` + + 文档([`doc.deno.land/builtin/stable#Deno.Buffer`](https://doc.deno.land/builtin/stable#Deno.Buffer))指出,当有东西要读取时,`Deno.Buffer`的`read`函数返回读取的字节数。当没有东西可读时,缓冲区为空,它返回 null。 + +1. 现在,在`if`内部,我们可以简单地解码已经读取的内容,因为我们知道它以`Uint8Array`格式存在: + + ```js + const decoder = new TextDecoder(); + …   + if (readBytes) { +   const read = decoder.decode(destination); + } + ``` + +1. 要将在控制台中解码的值打印出来,我们可以使用已知的`console.log`。我们也可以用不同的方式来做,通过使用`Deno.stdout`([`doc.deno.land/builtin/stable#Deno.stdout`](https://doc.deno.land/builtin/stable#Deno.stdout))来写入标准输出。 + + `Deno.stdout` 是 Deno 中的一个`writer`对象([`doc.deno.land/builtin/stable#Deno.Writer`](https://doc.deno.land/builtin/stable#Deno.Writer))。我们可以使用它的`write`方法将文本发送到那里: + + ```js + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + …   + if (readBytes) { +   const read = decoder.decode(destination); +   await Deno.stdout.write(encoder.encode(`${read}\n`)); + } + ``` + + 这样,我们就在`Deno.stdout`中写入刚刚读取的值。我们还添加了一个换行符(`\n`)在末尾,使其在控制台上的可读性稍好。 + + 如果我们保持这种方式,这个`processLogs`函数将只运行一次。因为我们希望这个函数再次运行并检查`buffer`中是否还有更多日志,我们需要将其安排在稍后再次运行。 + +1. 使用`setTimeout`在 100 毫秒后调用相同的`processLogs`函数: + + ```js + async function processLogs() { +   const destination = new Uint8Array(100); +   const readBytes = await buffer.read(destination); +   if (readBytes) { +     … +   } +   setTimeout(processLogs, 10); + } + ``` + +例如,如果我们打开`example-log.txt`文件,我们可以看到包含以下格式的日期行:`Thu Aug 20 22:14:31 WEST 2020`。 + +假设我们只想打印出包含`Tue`的日志。让我们编写实现该功能的逻辑: + +```js +async function processLogs() { +  const destination = new Uint8Array(100); +  const readBytes = await buffer.read(destination); +  if (readBytes) { +    const read = decoder.decode(destination); +    if (read.includes("Tue")) { +      await Deno.stdout.write(encoder.encode(`${read}\n`)); +    } +  } +  setTimeout(processLogs, 10); +}   +``` + +然后,我们在包含`example-logs.txt`文件的文件夹内执行程序: + +```js +$ deno run --allow-read index.ts +Tue Aug 20 17:12:05 WEST 2019 +Tue Sep 17 02:19:56 WEST 2019 +Tue Dec  3 14:02:01 CET 2019 +Tue Jul 21 10:37:26 WEST 2020 +``` + +带有日期的日志行如实地从缓冲区读取并符合我们的条件出现。 + +这是使用缓冲区可以做到的简要演示。我们能够异步地从缓冲区写入和读取。这种方法允许,例如,消费者在应用程序读取其他部分的同时处理文件的一部分。 + +`Deno`命名空间提供了比我们在这里尝试的更多功能。在本节中,我们决定选择几个部分并给您展示它启用多少功能。 + +我们将使用这些函数,以及第三方模块和标准库,当我们从第*4*章,*构建网络应用程序*开始编写我们的网络服务器。 + +# 使用标准库 + +在本节中,我们将探讨由 Deno 的标准库提供的行为。目前, runtime 认为它还不稳定,因此模块是单独版本化的。在我们撰写本文时,标准库处于 *version 0.83.0* 版本。 + +正如我们之前提到的,Deno 在添加到标准库的内容上非常细致。核心团队希望它提供足够的行为,这样人们就不需要依赖数百万个外部包来完成某些事情,但同时也不想增加太多的 API 表面。这是一个难以达到的微妙平衡。 + +受到 golang 的启发,Deno 标准库的大部分函数模仿了谷歌创建的语言。这是因为 Deno 团队真正相信*golang*如何发展其标准库,这个标准库以其精心打磨而广为人知。作为一个有趣的注记,Ryan Dahl(Deno 和 Node 的创建者)在他的某次演讲中提到,当拉取请求向标准库添加新的 API 时,会要求提供相应的*golang*实现。 + +我们不会逐一介绍整个库,原因与我们没有逐一介绍 Deno 命名空间相同。我们将做的是在学习它所提供的内容的同时,用它来构建几个有用的程序。我们将从生成 ID 等操作,到日志记录,到 HTTP 通信等已知用例进行介绍。 + +## 为我们的简单`ls`添加颜色 + +几页之前,我们在*nix 系统中建立了一个非常粗糙和简单的`ls`命令的“克隆”。当时我们列出了文件,以及它们的大小和修改日期。 + +为了开始探索标准库,我们将向该程序的终端输出添加一些颜色。让我们把文件夹名称打印成红色,以便我们可以轻松地区分它们。 + +我们将创建一个名为`list-file-names-color.ts`的文件。这次我们将使用 TypeScript,因为我们将得到更好的补全功能,因为标准库和 Deno 命名空间函数都是为此编写的。 + +让我们探索允许我们为文本着色的标准库函数(https://deno.land/std@0.83.0/fmt/colors.ts)。 + +如果我们想要查看一个模块的文档,我们可以直接查看代码,但我们也可以使用`doc`命令或文档网站。我们将使用后者。 + +导航到 https://doc.deno.land/https/deno.land/std@0.83.0/fmt/colors.ts。屏幕上列出了所有可用的方法: + +1. 从标准库的格式化库中导入打印红色文本的方法: + + ```js + import { red } from "https://deno.land/std@0.83.0/fmt/colors.ts"; + ``` + +1. 在我们的`async`迭代器遍历当前目录中的文件时使用它: + + ```js + const [path = "."] = Deno.args; + for await (const item of Deno.readDir(path)) { +   if (item.isDirectory) { +     console.log(red(item.name)); +   } else { +     console.log(item.name); +   } + } + ``` + +1. 通过在`demo-files`文件夹内运行它([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/ls`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter03/ls)),我们得到用红色打印的文件夹(这在打印书中无法显示,但您可以本地运行它): + + ```js + $ deno run –allow-read list-file-names-color.ts + file-with-no-content.txt + demo-folder + .hidden-file + lorem-ipsum.txt + ``` + +我们现在有一个更好的`ls`命令,它让我们能够区分文件夹和文件,这是通过使用标准库中的颜色函数实现的。标准库还提供了许多其他模块,我们将在书的进程中逐一了解它们。其中一些将在我们开始编写自己的应用程序时使用。 + +我们将特别关注的一个模块是 HTTP 模块,从下一节开始我们将大量使用它。 + +# 使用 HTTP 模块构建 Web 服务器 + +本书的主要焦点不仅是介绍 Deno 以及如何使用它,还包括学习如何使用它来构建 Web 应用程序。在这里,我们将创建一个简单的 JSON API,以便您了解 HTTP 模块。 + +我们将构建一个 API,用于保存和列出便签。我们把这些便签称为笔记。想象一下,这个 API 将为您提供便签板的数据。 + +我们将借助 Web APIs 和 Deno 标准库 HTTP 模块的功能创建一个非常简单的路由系统。请记住,我们这样做是为了探索 API 本身,因此这并不是生产就绪的代码。 + +让我们先创建一个名为`post-it-api`的文件夹和一个名为`index.ts`的文件。再次使用 TypeScript,因为我们认为自动完成和类型检查功能极大地提高了我们的体验,并减少了可能的错误数量。 + +本节的最终代码可在[`github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/steps/7.ts`](https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/steps/7.ts)找到: + +1. 首先将标准库 HTTP 模块导入我们的文件中: + + ```js + import { serve } from +   "https://deno.land/std@0.83.0/http/server.ts"; + ``` + +1. 使用`AsyncIterator`处理请求,就像我们之前的示例一样: + + ```js + console.log("Server running at port 8080"); + for await (const req of serve({ port: 8080 })) { +   req.respond({ body: "post-it api", status: 200 }); + } + ``` + + 如果我们现在运行它,这就是我们会得到的。请记住,为了使其具有网络访问权限,我们需要使用在权限部分提到的`--allow-net`标志: + + ```js + deno run --allow-net index.ts + Server running at port 8080 + ``` + +1. 为了清晰起见,我们可以将端口和服务器实例提取到单独的变量中: + + ```js + const PORT = 8080; + const server = serve({ port: PORT }); + console.log("Server running at port", PORT); + for await (const req of serve({ port: PORT })) { + … + ``` + +我们的服务器像以前一样运行良好,唯一的区别是现在代码(有争议地)看起来更易读,因为配置变量位于文件的顶部。我们稍后学习如何从代码中提取这些变量。 + +### 返回便签列表 + +我们的第一个要求是我们有一个返回便签列表的 API。这些便签将包括名称、标题和创建日期。在我们到达那里之前,为了使我们能够有多个路由,我们需要一个路由系统。 + +为了进行这个练习,我们将自己构建一个。这是我们了解 Deno 内置的一些 API 的方式。我们稍后会同意,在编写生产应用程序时,有时最好重用经过测试和广泛使用的软件,而不是一直重新发明轮子。然而,为了学习目的,完全可以*重新发明轮子*。 + +为了创建我们的基本路由系统,我们将使用一些您可能从浏览器中知道的 API。例如`URL`、`UrlSearchParams`等对象。 + +我们的目标是能够通过其 URL 和路径定义一个路由。像`GET /api/post-its`这样的东西会很好。让我们来做吧! + +1. 首先创建一个`URL`对象([`developer.mozilla.org/en-US/docs/Web/API/URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL))来帮助我们解析 URL 和其参数。我们将提取`HOST`和`PROTOCOL`到另一个变量中,这样我们就不用重复了: + + ```js + const PORT = 8080; + const HOST = "localhost"; + const PROTOCOL = "http"; + const server = serve({ port: PORT, hostname: HOST }); + console.log(`Server running at ${HOST}:${PORT}`); + for await (const req of server) { +   const url = new +     URL(`${PROTOCOL}://${HOST}${req.url}`); +   req.respond({ body: "post-it api", status: 200 }); + } + ``` + +1. 使用创建的`URL`对象进行一些路由。我们将使用`switch case`来实现。当没有匹配的路由时,应向客户端发送`404`: + + ```js +   const pathWithMethod = `${req.method} ${url.pathname}`; +   switch (pathWithMethod) { +     case "GET /api/post-its": +       req.respond({ body: "list of all the post-its", +         status: 200 }); +       continue; +     default: +       req.respond({ status: 404 }); +   } + ``` + + 提示 + + 你可以运行脚本时同时使用`--unstable`和`--watch`标志来重新启动文件更改后的脚本,如下所示:`deno run --allow-net --watch --unstable index.ts`。 + +1. 访问`http://localhost:8080/api/post-its`并确认我们得到了正确的响应。其他任何路由都会得到 404 响应。 + + 请注意,我们使用`continue`关键字让 Deno 在响应请求后跳出当前迭代(记住我们正在一个`for`循环内)。 + + 你可能会注意到,目前我们只是按路径路由,而不是按方法。这意味着对`/api/post-its`的任何请求,无论是`POST`还是`GET`,都会得到相同的响应。让我们通过继续前进来解决这个问题。 + +1. 创建一个包含请求方法和路径名的变量: + + ```js +   const pathWithMethod = `${req.method} ${url.pathname}` +   switch (pathWithMethod) { + ``` + + 现在我们可以按照自己的意愿定义路由,比如`GET /api/post-its`。现在我们已经有了路由系统的基本知识,我们将编写返回便签的逻辑。 + +1. 创建 TypeScript 接口,以帮助我们保持便签的结构: + + ```js + interface PostIt { +   title: string, +   id: string, +   body: string, +   createdAt: Date + } + ``` + +1. 创建一个将作为我们这次练习的*内存数据库*的变量。 + + 我们将使用一个 JavaScript 对象,其中键是 ID,值是刚刚定义的`PostIt`类型的对象: + + ```js + let postIts: Record = {} + ``` + +1. 向我们的数据库添加几个测试数据: + + ```js + let postIts: Record = { +   '3209ebc7-b3b4-4555-88b1-b64b33d507ab': { title: 'Read more', body: 'PacktPub books', id: 3209ebc7-b3b4-4555-88b1-b64b33d507ab ', createdAt: new Date() }, +   'a1afee4a-b078-4eff-8ca6-06b3722eee2c': { title: 'Finish book', body: 'Deno Web Development', id: '3209ebc7-b3b4-4555-88b1-b64b33d507ab ', createdAt: new Date() } + } + ``` + + 请注意,我们目前是*手动生成* *IDs*。稍后,我们将使用标准库中的另一个模块来完成。让我们回到我们的 API,并更改处理路由的`case`。 + +1. 更改`case`,以便返回所有便签而不是硬编码的消息。 + + 由于我们的数据库是一个键/值存储,我们需要使用`reduce`来构建包含所有便签的数组(删除下面代码块中突出显示的行): + + ```js + case GET "/api/post-its": +   req.respond({ body: "list of all the post-its", status:     200 }); +   const allPostIts = Object.keys(postIts). +     reduce((allPostIts: PostIt[], postItId) => { +         return allPostIts.concat(postIts[postItId]); +       }, []); +   req.respond({ body: JSON.stringify({ postIts:     allPostIts }) }); +   continue; + ``` + +1. 运行代码并访问`/api/post-its`。我们应该会在那里看到我们的便签列表! + + 你可能已经注意到,这仍然不是 100%正确的,因为我们的 API 返回的是 JSON,而且其头部与载荷不匹配。 + +1. 我们将通过使用来自浏览器的我们知道的 API,即`Headers`对象,来添加`content-type`([`developer.mozilla.org/en-US/docs/Web/API/Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers))。删除下面代码块中突出显示的行: + + ```js + const headers = new Headers(); + headers.set("content-type", "application/json"); + const pathWithMethod = `${req.method} ${url.pathname}` + switch (pathWithMethod) { +   case "GET /api/post-its": + … +     req.respond({ body: JSON.stringify({ postIts: +       allPostIts }) }); +     req.respond({ headers, body: JSON.stringify({ +       postIts: allPostIts }) }); +     continue; + ``` + +我们上面创建了`Headers`对象的实例,然后我们在响应中使用它,在`req.respond`上。这样,我们的 API 现在更加一致、易消化,并遵循标准。 + +### 向数据库添加便签 + +既然我们已经有了读取便签的方法,我们还需要一种添加新便签的方法,因为如果 API 完全是静态内容,那就没有意义了。那就是我们接下来要做的。 + +我们将使用我们创建的*路由基础设施*来添加一个允许我们*插入*记录到我们数据库的路由。由于我们遵循 REST 指南,那个路由将位于列出`post-its`的相同路径上,但方法不同: + +1. 定义一个总是返回`201`状态码的路由: + + ```js +     case "POST /api/post-its": +       req.respond({ status: 201 }); +       continue + ``` + +1. 使用`curl`的帮助,我们可以看到它返回了正确的状态码: + + ```js + curl but feel free to use your favorite HTTP requests tool, you can even use a graphical client such as Postman (https://www.postman.com/).Let's make the new route do what it is supposed to. It should get a JSON payload and use that to create a new post-it.We know, by looking at the documentation of the standard library's HTTP module (`doc.deno.land/https/deno.land/std@0.83.0/http/server.ts#ServerRequest`) that the body of the request is a *Reader* object. The documentation includes an example on how to read from it. + ``` + +1. 遵循建议,读取值并打印出来以更好地理解它: + + ```js + case "POST /api/post-its": +       const body = await Deno.readAll(req.body); +       console.log(body) + ``` + +1. 用`curl`发出带有`body`的请求: + + ```js + 201 status code. If we look at our running server though, something like this is printed to the console: + + ``` + + Uint8Array(25) [ + + 123, 34, 116, 105, 116, 108, 101, + + 34, 58, 32, 34, 84, 32, 101, 115, + + 116, 32, 112, 111, 115, 116, 45, + + 105, 116, 34, 125 + + ] + + ```js + + We previously learned that Deno uses `Uint8Array` to do all its communications with the Rust backend, and this is not an exception. However, `Uint8Array` is not what we currently want, we want the actual text of the request body. + ``` + +1. 使用`TextDecoder`将请求体作为可读值获取。完成此操作后,我们再次记录输出,然后我们将发出一个新的请求: + + ```js + $ deno -X POST -d "{\"title\": \"Buy milk\"}" + http://localhost:8080/api/post-its + ``` + + 这次服务器在控制台打印的就是这个: + + ```js + {"title": "Buy milk "} + ``` + + 我们正在取得进展! + +1. 由于正文是一个字符串,我们需要将其解析为 JavaScript 对象。我们将使用我们的一位老朋友,`JSON.parse`: + + ```js + const decoded = JSON.parse(new +   TextDecoder().decode(body)); + ``` + + 现在我们有了一种请求体格式,我们可以对其采取行动,这基本上是我们创建新数据库记录所需要的一切。让我们按照以下步骤创建一个: + +1. 使用标准库中的`uuid`模块([`deno.land/std@0.83.0/uuid`](https://deno.land/std@0.67.0/uuid))为我们的记录生成一个随机的 UUID: + + ```js + import { v4 } from +   "https://deno.land/std/uuid/mod.ts"; + ``` + +1. 在我们的路由的 switch case 中,我们将使用`generate`方法创建一个`id`并将其插入到*数据库*中,在用户在请求负载中发送的内容顶部添加`createdAt`日期。为了这个例子,我们省略了验证: + + ```js + case "POST /api/post-its": + … +     const decoded = JSON.parse(new +       TextDecoder().decode(body)); +     const id = v4.generate(); +     postIts[id] = { +       ...decoded, +       id, +       createdAt: new Date() +     } +     req.respond({ status: 201, body: +       JSON.stringify(postIts[id]), headers }); + ``` + + 注意,我们使用了之前在`GET`路由来定义的相同的`headers`对象,以便我们的 API 返回`Content-Type: application/json`。 + + 再次,遵循*REST*指南,我们返回`201` `Created`代码和创建的记录。 + +1. 保存代码,重启服务器,再次运行它: + + ```js + GET request to the route that lists all the post-its to check if the record was actually inserted into the database: + + ``` + + `curl http://localhost:8080/api/post-its` + + {"postIts":[{"title":"Read more","body":"PacktPub books","id":"3209ebc7-b3b4-4555-88b1-b64b33d507ab","createdAt":"2021-01-10T16:28:52.210Z"},{"title":"Finish book","body":"Deno Web Development","id":"a1afee4a-b078-4eff-8ca6-06b3722eee2c","createdAt":"2021-01-10T16:28:52.210Z"},{"title":"Buy groceries","body":"1 x Milk","id":"b35b0a62-4519-4491-9ba9-b5809b4810d5","createdAt":"2021-01-10T16:29:05.519Z"}]} + + ```js + + ``` + +而且它奏效了!现在我们有一个 API,它可以返回便签列表并添加便签。 + +这基本上结束了我们在这个章节中使用 HTTP 模块关于 API 的一切。像我们写的这个 API 一样,大多数 API 都是为了被前端应用程序消费,我们接下来会做的那样来结束这个章节。 + +### 提供前端服务 + +由于这超出了本书的范围,我们不会编写与这个 API 交互的前端代码。然而,如果你想用它来获取 post-its 并显示在一个单页应用程序上,我在书中的文件中包含了一个([`github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/index.html`](https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter03/post-it-api/index.html))。 + +我们将学习如何使用我们刚刚建立的网络服务器来提供一份 HTML 文件: + +1. 首先,我们需要在服务器的根目录下创建一个路由。然后,我们需要设置正确的`Content-Type`,并使用已知的文件系统 API 返回文件内容。 + + 为了获取当前文件中 HTML 文件的路径,我们将使用 URL 对象与 JavaScript 中的`import.meta`声明一起使用([`developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta)),其中包含当前文件的路径: + + ```js + resolve, and fromFileUrl methods from Deno's standard-library to get a URL that is relative to the current file.Note that we now need to run this with the `--allow-read` flag since our code is reading from the filesystem. + ``` + +1. 为了让我们更安全,我们将指定程序可以读取的确切文件夹,通过将其发送到`--allow-read`标志: + + ```js + $ deno run --allow-net --allow-read=. index.ts + Server running at http://0.0.0.0:8080 + ``` + + 这将防止任何可能允许恶意人士读取我们文件系统的错误。 + +1. 打开浏览器,输入网址,你应该会来到一个页面,我们可以看到添加的`post-its`日程安排。要添加一个新的,你也可以点击**添加新日程**文本并填写表单: + +![图 3.2 – 前端消费 post-it API](img/Figure_3.2_B16380.jpg) + +](img/Figure_3.2_B16380.jpg) + +图 3.2 – 前端消费 post-it API + +重要说明 + +请记住,在许多生产环境中,不建议 API 提供前端代码。在这里,我们这样做是为了学习目的,以便我们能够理解标准库 HTTP 模块的一些可能性。 + +在本节中,我们学习了如何使用标准库提供的模块造福。我们创建了`ls`命令的简单版本,并使用标准库的输出格式化功能为其添加了一些颜色。为了结束这一节,我们创建了一个具有几个端点的 HTTP API,用于列出和持久化记录。我们经历了不同的需求,并学习了 Deno 如何完成它们。 + +# 总结 + +随着我们对这本书的阅读,我们对 Deno 的了解变得更加实用,我们开始用它来处理更接近现实世界的用例。这一章就是关于这个。 + +我们首先学习了运行时的基本特性,即程序生命周期,以及 Deno 如何看待模块稳定性和版本控制。我们迅速转向了 Deno 提供的 Web API,通过编写一个简单的程序,从网站上获取 Deno 标志,将其转换为 base64,并将其放入 HTML 页面。 + +然后,我们进入了`Deno`命名空间,探索了一些其底层功能。我们使用文件系统 API 构建了一些示例,并最终用它构建了一个`ls`命令的简化版。 + +缓冲区是在 Node.js 世界中广泛使用的东西,它们有能力执行异步读写行为。正如我们所知,Deno 与 Node.js 有很多相似的使用场景,这使得在这一章中不可避免地要讨论缓冲区。我们首先解释了 Deno 缓冲区与 Node.js 的区别,然后通过构建一个小型应用程序来结束这一节,该程序可以异步地从它们中读写。 + +为了结束这一章,我们更接近了这本书的主要目标之一,即使用 Deno 进行 Web 开发。我们使用 Deno 创建了第一个 JSON API。在这个过程中,我们了解到了多个 Deno API,甚至建立了我们基本的路由系统。然后,我们继续创建几个路由,列出并在我们的*数据存储*中创建记录。在章节的最后,我们学习了如何处理 API 中的头部,并将它们添加到我们的端点。 + +我们通过直接从我们的 Web 服务器提供单页应用程序来结束这一章;那个相同的单页应用程序消费并与我们的 API 进行了交互。 + +这一章我们覆盖了很多内容。我们开始构建 API,这些 API 现在离现实更近了,比我们之前做的要接近多了。我们还更清楚地了解了如何使用 Deno 进行开发,包括使用权限和文档。 + +当前章节结束了我们的入门之旅,希望它让你对接下来的内容感到好奇。 + +在接下来的四章中,我们将构建一个 Web 应用程序,并探索在过程中所做的所有决策。你到目前为止学到的大部分知识将会在后面用到,但还有大量新的、令人兴奋的内容即将出现。在下一章,我们将开始创建一个 API,随着章节的进行,我们将继续添加功能。 + +我希望你能加入我们! diff --git a/docs/deno-web-dev/deno-web-dev_01.md b/docs/deno-web-dev/deno-web-dev_01.md new file mode 100644 index 0000000..d15ac75 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_01.md @@ -0,0 +1,13 @@ +# 第二部分:构建应用程序 + +在这个动手实践部分,你将创建一个 Deno 应用程序,从服务器端渲染的网站开始,然后过渡到**代表性状态传输**(**REST**)**应用程序编程接口**(**APIs**),这些接口与数据库连接并具有身份验证功能。 + +本部分包含以下章节: + ++ 第四章,[构建 Web 应用程序](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=9932a37e-89e6-72d0-adbb-5f3242397a64) + ++ 第五章,[添加用户并将应用程序迁移到 Oak](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=7ba1253a-67f3-ded3-ed2a-5f324271642a) + ++ 第六章,[添加身份验证并连接到数据库](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=27af6495-f282-9e92-d711-5f324262765f) + ++ 第七章,[HTTPS、提取配置以及在浏览器中使用 Deno](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=27af6495-f282-9e92-d711-5f324262765f) diff --git a/docs/deno-web-dev/deno-web-dev_02.md b/docs/deno-web-dev/deno-web-dev_02.md new file mode 100644 index 0000000..2b4edd7 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_02.md @@ -0,0 +1,787 @@ +# 第四章:构建网络应用程序 + +我们终于来了!我们走过了漫长的道路,才到达这里。这里的乐趣才刚刚开始。我们已经经历了三个阶段:了解 Deno 是什么,探索它提供的工具链,以及通过其运行时了解其详细信息和功能。 + +几乎本章之前的所有内容都将证明是有用的。希望,前面的章节让你有足够的信心开始应用我们所学的知识。我们将利用这些章节,结合你现有的 TypeScript 和 JavaScript 知识,来构建一个完整的网络应用程序。 + +我们将编写一个包含业务逻辑、处理身份验证、授权和日志记录等内容的 API。我们将涵盖足够的基本内容,让你在最后觉得舒适地选择 Deno 来构建你下一个伟大的应用程序。 + +在本章中,我们将不仅仅讨论 Deno,还会回顾一些关于软件工程和应用程序架构的基本思想。我们认为,在从头开始构建应用程序时,保持一些事情在心中至关重要。我们将查看一些基本原则,这些原则将证明是有用的,并帮助我们结构化代码,使其易于更改,从而使它能够适应未来。 + +后来,我们将开始参考一些第三方模块,查看它们的方法,并决定从这里开始使用哪些来帮助我们处理路由和 HTTP 相关挑战。我们还将确保我们的应用程序结构以一种第三方代码被隔离并且作为我们想要构建的功能的启用器而不是功能本身来工作的方式进行组织。 + +本章我们将涵盖以下主题: + ++ 结构化网络应用程序 + ++ 探索 Deno HTTP 框架 + ++ 让我们开始吧! + +## 技术要求 + +本章使用的代码文件可在以下链接找到:[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter04/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter04/museums-api)。 + +# 结构化网络应用程序 + +开始一个应用程序时,花时间思考其结构和架构是很重要的。这一节将从这个角度开始:通过查看应用程序架构的核心。我们将看看它带来了什么优势,并使自己与一套原则保持一致,这些原则将帮助我们在应用程序增长时扩展它。 + +然后,我们将开发将成为应用程序第一个端点的代码。然而,首先,我们将从业务逻辑开始。持久性层将紧随其后,最后我们将查看一个 HTTP 端点,它将作为应用程序的入口点。 + +## Deno 作为一款无偏见的工具 + +当我们使用将许多决策委托给开发者的低级工具,如 Node.js 和 Deno 时,结构化应用程序就成为了一个主要挑战。 + +这与具有明确观点的 Web 框架,如 PHP Symfony、Java SpringBoot 或 Ruby on Rails,有很大的不同,因为这些框架为我们做了许多决策。 + +这些决策大多数与结构有关;也就是说,代码和文件夹结构。那些框架通常为我们提供处理依赖关系、导入的方法,甚至为不同的应用层提供一些指导。由于我们使用的是*原始*语言和几个包,所以我们将在本书中自己负责结构。 + +前述的框架不能直接与 Deno 比较,因为它们是建立在诸如 PHP、Java 和 Ruby 等语言之上的框架。但当我们观察 JS 世界,也就是 Node.js 时,我们可以发现最流行的创建 HTTP 服务器的工具是 Express.js 和 Kao.js。这些通常比前述的框架轻量许多,尽管也有一些像 Nest.js 或 hapi.js 这样坚实的完整替代品,但 Node.js 社区更倾向于采用*库*方法,而不是*框架*方法。 + +尽管这些非常流行的库提供了大量功能,但许多决策仍然委托给开发者。这不是库的错,更多的是一个社区偏好。 + +一方面,直接访问这些原生对象让我们能构建非常适合我们用例的应用程序。另一方面,灵活性是有代价的。拥有极大的灵活性随之而来的是做出无数决策的责任。而在做出许多决策时,有很多机会做出糟糕的决策。难点在于,这些通常是对代码库扩展方式产生巨大影响的决策,这就是它们如此重要的原因。 + +在当前状态下,Deno 及其社区在框架与库这一问题上遵循与 Node.js 非常相似的方法。社区主要押注于开发者为满足他们特定需求而创建的轻量级、小型软件。我们稍后将在本章中评估一些这些软件。 + +从现在开始,贯穿整本书,我们将使用一种我们相信对当前用例有很大好处的应用程序结构。然而,不要期望这种结构和架构是灵丹妙药,因为我们深信软件世界中不存在这样的东西;每种架构都将随着成长而不断进化。 + +我们不仅仅是要按照食谱机械地操作,而是要熟悉一种思维方式——一种逻辑。这应该能让我们在后续的道路上做出正确的决策,目标明确:*编写易于更改的代码*。 + +通过编写易于更改的代码,我们总是准备好在不需要太多努力的情况下改进我们的应用程序。 + +## 应用程序最重要的部分 + +应用程序是为了适应一个目的而创建的。无论这个目的是支持一个企业还是一个简单的宠物项目都不重要。到最后,我们希望它能做些什么。那个*做些什么*就是让应用程序变得有用的原因。 + +这可能看起来很显然,但对于我们这些开发者来说,有时候很容易因为对某项技术充满热情而忘记,它只是一个达成目标的手段。 + +正如罗伯特叔叔在他的*架构——失去的岁月*演讲中说的([`www.youtube.com/watch?v=hALFGQNeEnU`](https://www.youtube.com/watch?v=hALFGQNeEnU)),人们很容易忘记应用程序的目的,而更多地关注技术本身。我们在应用程序开发的各个阶段都记住这一点非常重要,但在设置其初始结构时尤为关键。接下来,我们将发现本书剩余部分我们将要构建的应用程序的需求。 + +## 我们的应用程序是关于什么的? + +尽管我们真心相信业务逻辑是任何应用程序最重要的事情,但在本书中,情况有点不同。我们将创建一个示例应用程序,但它只是一个达到主要目标的手段:学习 Deno。然而,为了使过程尽可能真实,我们希望在心中有一个清晰的目标。 + +我们将构建一个应用程序,让人们可以创建和与博物馆列表进行互动。我们可以通过将其功能作为用户故事来使其更清晰,如下所示: + ++ 用户可以注册和登录。 + ++ 用户可以创建一个带有标题、描述和位置的博物馆。 + ++ 用户可以查看博物馆列表。 + +在这个旅程中,我们将开发 API 和支持这些功能的逻辑。 + +现在我们已经了解了最终目标,我们可以开始思考如何组织应用程序。 + +## 理解文件夹结构和应用程序架构 + +关于文件夹结构,我们需要注意的第一个问题是,尤其是当我们从零开始一个没有框架的项目时,它会随着项目的进行而不断演变。一个对于只有几个端点的项目来说不错的文件夹结构,对于有数百个端点的项目来说可能就不那么好了。这取决于很多因素,从团队规模,到制定的标准,最终到个人偏好。 + +在定义文件夹结构时,重要的是我们要达到一个可以促进未来关于如何定位一段代码的决策的地方。文件夹结构应该为如何做出良好的架构决策提供清晰的提示。 + +同时,我们当然不希望创建一个过度工程化的应用程序。我们将创建足够的抽象,以便模块非常紧凑,并且不知道它们域外的知识,但不会超过这个程度。牢记这一点也迫使我们构建灵活的代码和清晰的接口。 + +最终,最重要的是架构能够使代码库如下所示: + ++ 可测试 + ++ 易于扩展 + ++ 与特定技术或库松耦合 + ++ 易于导航和推理 + +在创建文件夹、文件和模块时,我们必须记住,我们不想牺牲前面列出的任何主题。 + +这些原则与软件设计中的 SOLID 原则非常一致,这些原则由 Uncle Bob,Robert C. Martin([`en.wikipedia.org/wiki/SOLID`](https://en.wikipedia.org/wiki/SOLID))在另一个值得一看的演讲中提出([`youtu.be/zHiWqnTWsn4`](https://youtu.be/zHiWqnTWsn4))。 + +本书我们将要使用的文件夹结构,如果你有 Node.js 背景,可能会觉得熟悉。 + +正如发生在 Node.js 上一样,我们创建一个完整的 API 的单个文件没有任何障碍。然而,我们不会这样做,因为我们相信一些初步的关注点分离将大大提高我们的灵活性,而不会牺牲开发者的生产力。 + +在接下来的部分,我们将探讨不同层的责任以及它们如何在开发应用程序功能时相互配合。 + +遵循这种思路,我们努力保证模块之间的解耦程度。例如,我们希望能够确保在更改 Web 框架时,不必触摸业务逻辑对象。 + +所有这些建议,以及我们在这本书中将会提出的建议,将有助于确保我们应用程序的核心部分是业务逻辑,其他所有内容只是插件。JSON API 只是向用户发送我们数据的方式,而数据库只是持久化数据的方式;这两者都不应成为应用程序的核心部分。 + +确保我们这样做的一种方法是在编写代码时进行以下心理练习: + +"当你编写业务逻辑时,想象这些对象将在不同的上下文中使用。例如,使用不同的交付机制(例如 CLI)或不同的持久化引擎(内存数据库而非 NoSQL 数据库)。" + +在接下来的几页中,我们将引导您创建不同的层,并解释所有设计决策以及它们所启用的功能。 + +让我们来实践并开始构建我们项目的基础。 + +### 定义文件夹结构 + +在我们项目的文件夹中做的第一件事是创建一个`src`文件夹。 + +这是我们代码将存放的地方。我们不希望有任何代码位于项目的根级别,因为可能会添加配置文件、READMEs、文档文件夹等。这将使得代码难以区分。 + +在接下来的章节中,我们大部分时间将在`src`文件夹内度过。由于我们的应用程序是关于博物馆的,我们将在`src`文件夹内创建一个名为`museums`的文件夹。这个文件夹将是本章将编写大部分逻辑的地方。后来,我们将创建类型、控制器和服务器的文件。然后,我们将创建`src/web`文件夹。 + +控制器的文件是承载我们业务逻辑的地方。仓库将处理与数据访问相关的逻辑,而网络层将处理所有*与网络相关*的事情。 + +你可以通过查看本书的 GitHub 仓库来了解最终结构会是什么样子:[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter04/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter04/museums-api)。 + +本章的初始要求是有一个路由,我们可以在此执行 GET 请求并收到以 JSON 格式表示的博物馆列表。 + +我们将在控制器文件(`src/museums/controller.ts`)中编写所需的业务逻辑。 + +文件夹结构应该如下所示: + +```js +└── src +    ├── museums +    │   ├── controller.ts +    │   ├── repository.ts +    │   └── types.ts +    └── web +``` + +这是我们出发的起点。所有与博物馆相关的内容都将位于`museums`文件夹内,我们将之称作一个模块。`controller`文件将承载业务逻辑,`repository`文件将承载数据获取功能,而`types`文件将是我们放置类型的位置。 + +现在,让我们开始编码! + +## 开发业务逻辑 + +我们之前说过,我们的业务逻辑是应用程序最重要的部分。尽管现在我们的会非常简单,但这是我们首先开发的。 + +由于我们将使用 TypeScript 来编写我们的应用程序,让我们来创建一个定义我们的`Museum`对象的接口。按照以下步骤操作: + +1. 进入`src/museums/types.ts`并创建一个定义`Museum`的类型: + + ```js + export type Museum = { +   id: string, +   name: string, +   description: string, +   location: { +     lat: string, +     lng: string +   } + } + ``` + + 确保它被导出,因为我们将跨其他文件使用这个。 + + 既然我们知道了类型,我们必须创建一些业务逻辑来获取博物馆列表。 + +1. 在`src/museums/types.ts`内,创建一个定义`MuseumController`的接口。它应该包含一个列出所有博物馆的方法: + + ```js + export interface MuseumController { +   getAll: () => Promise; + } + ``` + +1. 在`src/museums/controller.ts`内,创建一个类作为控制器。它应该包含一个名为`getAll`的函数。将来,这里将是业务逻辑的所在,但目前,我们只需返回一个空数组: + + ```js + import type { MuseumController } from "./types.ts"; + export class Controller implements MuseumController { +   async getAll() { +     return []; +   } + } + ``` + + 我们本可以直接访问数据库并获取某些记录。然而,由于我们希望能够使我们的业务逻辑得到隔离,并不与应用程序的其他部分耦合,所以我们不会这样做。 + + 此外,我们还希望业务逻辑能够独立于数据库或服务器的连接进行测试。为了实现这一点,我们不能直接从我们的控制器访问数据源。稍后,我们将创建一个抽象,它将负责从数据库获取这些记录。 + + 现在,我们知道我们将需要调用一个外部模块,它将为我们获取所有的博物馆,并将它们交给我们的控制器——它从哪里来无关紧要。 + + 请记住以下软件设计最佳实践:*"面向接口编程,而不是面向实现。"* + + 简单来说,这个引言的意思是我们应该定义模块的签名,然后才开始思考它的实现。这对设计清晰的接口非常有帮助。 + + 回到我们的控制器,我们知道控制器的`getAll`方法最终必须调用一个模块以从数据源获取数据。 + +1. 在`src/museums/types.ts`中,定义`MuseumRepository`,这个模块将负责从数据源获取博物馆: + + ```js + export interface MuseumRepository { +   getAll: () => Promise + } + ``` + +1. 在`src/museums/controller.ts`中,向构造函数添加一个注入的类`museumRepository`: + + ```js + museumRepository that implements the MuseumRepository interface. By creating this and *lifting the dependencies*, we no longer need to return an empty array from our controller.Before we write any more logic, let's make sure our code runs and check if it is working. We're just missing one thing. + ``` + +1. 创建一个名为`src/index.ts`的文件,导入`MuseumController`,实例化它,并调用`getAll`方法,记录其输出。现在,你可以注入一个伪仓库,它只是返回一个空数组: + + ```js + import { Controller as MuseumController } from +   "./museums/controller.ts"; + const museumController = new MuseumController({ +   museumRepository: { +     getAll: async () => [] +   } + }) + console.log(await museumController.getAll()) + ``` + +1. 运行它以检查是否正常工作: + + ```js + $ deno run src/index.ts + [] + ``` + + 就这样!我们刚刚从伪仓库函数中得到了一个空数组! + +有了我们创建的抽象,我们的控制器现在与数据源解耦。它的依赖关系通过构造函数注入,允许我们稍后不改变控制器而更改仓库。 + +我们刚刚所做的称为**依赖倒置**——SOLID 原则中的**D**——它包括将部分依赖性提升到函数调用者。这使得独立测试内部函数变得非常容易,正如我们将在*第八章*,*测试——单元和集成*中看到的,我们将涵盖测试。 + +要将我们刚刚编写的代码转换为完全功能的应用程序,我们需要有一个数据库或类似的东西。我们需要一个可以存储和检索博物馆列表的东西。现在让我们创建这个东西。 + +## 开发数据访问逻辑 + +在开发控制器时,我们注意到我们需要一个能够获取数据的东西,即仓库。这个模块将抽象所有对数据源的调用,在这个案例中,数据源存储博物馆。它将有一组非常明确的方法,任何想要访问数据的人都应该通过这个模块进行访问。 + +我们已经在`src/museums/types.ts`中定义了其接口的一部分,所以让我们写一个实现它的类。现在,我们不会将它连接到真实的数据库。我们将使用 ES6 Map 作为内存中的数据库。 + +让我们打开我们的仓库文件,并按照以下步骤开始编写我们的数据访问逻辑: + +1. 打开`src/museums/repository.ts`文件,创建一个`Repository`类。 + + 它应该有一个名为`storage`的属性,这将是一个 JavaScript `Map`。`Map`的键应该是字符串,值应该是`Museum`类型的对象: + + ```js + import type { Museum, MuseumRepository } from +   "./types.ts"; + export class Repository implements MuseumRepository { +   storage = new Map(); + } + ``` + + 我们使用 TypeScript 泛型来设置我们的`Map`的类型。请注意,我们已经从博物馆控制器中导入了`Museum`接口,以及由我们的类实现的`MuseumRepository`。 + + 既然“数据库”已经“准备就绪”,我们必须暴露某些方法,以便人们可以与之交互。上一部分的要求是我们可以从数据库中获取所有记录。让我们接下来实现这一点。 + +1. 在仓库类内部,创建一个名为`getAll`的方法。它应该负责返回我们`storage` `Map`中的所有记录: + + ```js + export class Repository implements MuseumRepository { +   storage = new Map(); + src should only be accessible from the outside through a single file. This means that whoever wants to import stuff from src/museums should only do so from a single src/museums/index.ts file. + ``` + +1. 创建一个名为`src/museums/index.ts`的文件,导出博物馆的控制器和仓库: + + ```js + export { Controller } from "./controller.ts"; + export { Repository } from "./repository.ts"; + export type { Museum, MuseumController, +   MuseumRepository } from "./types.ts"; + ``` + + 为了保持一致性,我们需要去所有之前从不是`src/museums/index.ts`的文件中导入的导入,并更改它们,使它们只从这个文件中导入东西。 + +1. 更新`controller.ts`和`repository.ts`的导入,使它们从`index`文件中导入: + + ```js + import type { MuseumController, MuseumRepository } +   from "./index.ts"; + ``` + + 你可能已经猜到我们接下来必须做什么了……你还记得上一部分末尾吗,我们在博物馆控制器中注入了一个虚拟函数,它返回一个空数组?让我们回到这一点并使用我们刚刚编写的逻辑。 + +1. 回到`src/index.ts`,导入我们刚刚开发的`Repository`类,并将其注入到`MuseumController`构造函数中: + + ```js + import { +   Controller as MuseumController, +   Repository as MuseumRepository, + } from "./museums/index.ts"; + const museumRepository = new MuseumRepository(); + const museumController = new MuseumController({ +   museumRepository }) + console.log(await museumController.getAll()) + ``` + + 现在,让我们向我们的“数据库”添加一个测试数据,这样我们就可以检查它是否实际上正在打印一些内容。 + +1. 访问`museumRepository`中的`storage`属性,并向其添加一个测试数据。 + + 这目前是一个反模式,因为我们直接访问了模块的数据库,但我们将创建一个方法,以便我们以后可以正确地添加测试数据: + + ```js + const museumRepository = new MuseumRepository(); + … + museumRepository.storage.set +   ("1fbdd2a9-1b97-46e0-b450-62819e5772ff", { +   id: "1fbdd2a9-1b97-46e0-b450-62819e5772ff", +   name: "The Louvre", + description: "The world's largest art museum +     and a historic monument in Paris, France.", +   location: { +     lat: "48.860294", +     lng: "2.33862", +   }, + }); + console.log(await museumController.getAll()) + ``` + +1. 现在,让我们再次运行我们的代码: + + ```js + $ deno run src/index.ts + [ +   { +     id: "1fbdd2a9-1b97-46e0-b450-62819e5772ff", +     name: "The Louvre", +     description: "The world's largest art +       museum and a historic monument in Paris, +         France.", +     location: { lat: "48.860294", lng: "2.33862" } +   } + ] + ``` + + 有了这个,我们数据库的连接就可以工作了,正如我们通过打印测试数据所看到的那样。 + +我们在上一部分创建的抽象使我们能够在不更改控制器的情况下更改数据源。这是我们正在使用的架构的一个优点。 + +现在,如果我们回顾我们的初始需求,我们可以确认我们已经完成了一半。我们已经创建了满足用例的业务逻辑——我们只是缺少 HTTP 部分。 + +## 创建 Web 服务器 + +现在我们已经有了我们的功能,我们需要通过 Web 服务器暴露它。让我们使用我们从标准库学到的知识来创建它,并按照以下步骤进行: + +1. 在`src/web`文件夹中创建一个名为`index.ts`的文件,并在那里添加创建服务器的逻辑。我们可以从上一章的练习中复制并粘贴它: + + ```js + import { serve } from +   "https://deno.land/std@0.83.0/http/server.ts"; + const PORT = 8080; + const server = serve({ port: PORT }); + console.log(`Server running at +   https://localhost:${PORT}`); + for await (let req of server) { +   req.respond({ body: 'museums api', status: 200 }) + } + ``` + + 由于我们希望应用程序易于配置,我们不希望`port`在这里是硬编码的,而是可以从外部配置的。我们需要将这个服务器创建逻辑作为一个函数导出。 + +1. 将服务器逻辑创建封装在一个接收配置和`port`作为参数的函数中: + + ```js + import { serve } from +   "https://deno.land/std@0.83.0/http/server.ts"; + port defining its type. + ``` + +1. 将这个函数的参数设置为`接口`。这将有助于我们的文档工作,并且还会增加类型安全和静态检查: + + ```js + interface CreateServerDependencies { +   configuration: { +     port: number +   } + } + export async function createServer({ +   configuration: { +     port +   } + }: CreateServerDependencies) { + … + ``` + + 现在我们已经配置了网络服务器,我们可以考虑为其使用场景使用它。 + +1. 回到`src/index.ts`,导入`createServer`,并使用它创建一个在端口`8080`上运行的服务器: + + ```js + import { createServer } from "./web/index.ts"; + … + createServer({ +   configuration: { +     port: 8080 +   } + }) + … + ``` + +1. 运行它并看看它是否起作用: + + ```js + $ deno run --allow-net src/index.ts + Server running at http://localhost:8080 + [ +   { +     id: "1fbdd2a9-1b97-46e0-b450-62819e5772ff", +     name: "The Louvre", +     description: "The world's largest art museum and a +       historic monument in Paris, France.", +     location: { lat: "48.860294", lng: "2.33862" } +   } + ] + ``` + +在这里,我们可以看到有一个日志记录服务器正在运行,以及来自上一节的日志结果。 + +现在,我们可以使用`curl`测试网络服务器以确保它正在工作: + +```js +$ curl http://localhost:8080 +museums api +``` + +正如我们所看到的,它起作用了——我们有一些相当基础的逻辑,但这仍然不能满足我们的要求,却能启动一个网络服务器。我们接下来要做的就是将这个网络服务器与之前编写的逻辑连接起来。 + +## 将网络服务器与业务逻辑连接起来 + +我们离完成本章开始时计划做的事情已经相当接近了。我们目前有一个网络服务器和一些业务逻辑;缺少的是连接。 + +将两者连接起来的一个快速方法是将控制器直接导入`src/web/index.ts`并在那里使用它。在这里,应用程序将具有所需的行为,目前,这并没有带来任何问题。 + +然而,由于我们考虑的是一个可以无需太多问题就能扩展的应用程序架构,我们不会这样做。这是因为这将使测试我们的网络逻辑变得非常困难,从而违背了我们的一条原则。 + +如果我们直接从网络服务器导入控制器,那么每次在测试环境中调用`createServer`函数时,它都会自动导入并调用`MuseumController`中的方法,这不是我们想要的结果。 + +我们再次使用依赖倒置将控制器的函数发送到网络服务器。如果这仍然看起来过于抽象,不要担心——我们马上就会看到代码。 + +为了确保我们没有忘记我们的初始目标,我们想要的是,当用户对`/api/museums`进行`GET`请求时,我们的网络服务器返回一个博物馆列表。 + +由于我们正在进行这个练习,我们暂时不会使用路由库。 + +我们只想添加一个基本检查,以确保请求的 URL 和方式是我们希望回答的。如果是,我们想返回博物馆列表。 + +让我们回到`createServer`函数并添加我们的路由处理程序: + +```js +export async function createServer({ +  configuration: { +    port +  } +}: CreateServerDependencies) { +  const server = serve({ port }); +  console.log(`Server running at +    http://localhost:${port}`); +  for await (let req of server) { +    if (req.url === "/api/museums" && req.method === "GET")      +     { +req.respond({ +body: JSON.stringify({ +museums: [] +}), +status: 200 +      }) +      continue +    } +    req.respond({ body: "museums api", status: 200 }) +  } +} +``` + +我们添加了对请求 URL 和方法的基本检查,以及它们符合初始要求时的不同响应。让我们运行代码看看它的行为如何: + +```js +$ deno run --allow-net src/index.ts +Server running at http://localhost:8080 +``` + +再次,用`curl`测试它: + +```js +$ curl http://localhost:8080/api/museums +{"museums":[]} +``` + +它起作用了——酷! + +现在是我们定义需要什么来满足这个请求作为接口的部分。 + +我们最终需要一个返回博物馆列表的函数,以注入到我们的服务器中。让我们按照以下步骤在`CreateServerDependencies`接口中添加该功能: + +1. 回到`src/web/index.ts`中,将`MuseumController`作为`createServer`函数的一个依赖项添加: + + ```js + MuseumController type we defined in the museum's module. We're also adding a museum object alongside the configuration object. + ``` + +1. 从博物馆控制器中调用`getAll`函数以获取所有博物馆的列表并响应请求: + + ```js + export async function createServer({ +   configuration: { +     port +   }, +   createServer function, but we're not sending it when we call createServer. Let's fix that. + ``` + +1. 回到`src/index.ts`,这是我们调用`createServer`函数的地方,并发送来自`MuseumController`的`getAll`函数。你也可以删除上一节直接调用控制器方法的代码,因为此刻它没有任何用处: + + ```js + import { createServer } from "./web/index.ts"; + import { +   Controller as MuseumController, +   Repository as MuseumRepository, + } from "./museums/index.ts"; + const museumRepository = new MuseumRepository(); + const museumController = new MuseumController({ +   museumRepository }) + museumRepository.storage.set + ("1fbdd2a9-1b97-46e0-b450-62819e5772ff", { +   id: "1fbdd2a9-1b97-46e0-b450-62819e5772ff", +   name: "The Louvre", +   description: "The world's largest art museum +     and a historic monument in Paris, France.", +   location: { +     lat: "48.860294", +     lng: "2.33862", +   }, + }); + createServer({ +   configuration: { port: 8080 }, +   museum: museumController + }) + ``` + +1. 再次运行应用程序: + + ```js + $ deno run --allow-net src/index.ts + Server running at http://localhost:8080 + ``` + +1. 向 http://localhost:8080/api/museums 发送请求;你会得到一个博物馆列表: + + ```js + $ curl localhost:8080/api/museums + {"museums":[{"id":"1fbdd2a9-1b97-46e0-b450-62819e5772ff","name":"The Louvre","description":"The world's largest art museum and a historic monument in Paris, France.","location":{"lat":"48.860294","lng":"2.33862"}}]} + ``` + +就这样——我们得到了博物馆的列表! + +我们已经完成了本节的 goals;也就是说,将我们的业务逻辑连接到 web 服务器。 + +注意我们是如何使控制器方法可以被注入,而不是 web 服务器直接导入它。这之所以可能,是因为我们使用了依赖倒置。这是我们在这本书中会持续做的事情,无论何时我们想要解耦并增加模块和函数的可测试性。 + +在我们进行代码耦合的思维锻炼时,当我们想要将当前的业务逻辑与不同的交付机制(如 CLI)结合使用时,没有任何阻碍。我们仍然可以重用相同的控制器和存储库。这意味着我们很好地使用了抽象来将业务逻辑与应用程序逻辑解耦。 + +现在我们已经有了应用程序架构和文件结构的基线,并且也理解了背后的*原因*,我们可以开始查看可能帮助我们构建它的工具。 + +在下一节中,我们将看看 Deno 社区中现有的 HTTP 框架。我们不会花太多时间在这方面,但我们需要了解每个框架的优缺点,并最终选择一个来帮助我们完成余下的旅程。 + +# 探索 Deno HTTP 框架 + +当你构建一个比简单教程更复杂的应用程序时,如果你不想采取纯粹主义方法,你很可能会使用第三方软件。 + +显然,这不仅仅是 Deno 特有的。尽管有些社区比其他社区更愿意使用第三方模块,但所有社区都在使用第三方软件。 + +我们可以讨论人们为什么这样做或不做,但更常见的原因总是与可靠性和时间管理有关。这可能是因为你想使用经过实战测试的软件,而不是自己构建。有时,这只是时间管理问题,即不想重写已经创建的东西。 + +有一点我们必须说的是,我们必须极其谨慎地考虑我们构建的应用程序中有多少与第三方软件耦合。我们并不是说你应该试图达到完全解耦的乌托邦,尤其是因为这会引入其他问题和大量的间接性。我们要说的是,我们应该非常清楚地将一个依赖项引入我们的代码以及它引入的权衡。 + +在本章的第一部分,我们构建了一个 web 应用程序的基础,我们将在本书的其余部分向其添加功能。在其当前状态,它仍然非常小,因此除了标准库之外,没有其他依赖。 + +在那篇应用中,我们做了一些我们认为不会很好扩展的事情,比如使用普通的`if`语句根据 URL 和 HTTP 方法定义路由。 + +随着应用程序的增长,我们可能会需要更高级的功能。这些需求可能从处理不同格式的 HTTP 请求体,到拥有更复杂的路由系统,处理头部和 cookies,或连接数据库。 + +因为我们不相信在开发应用程序时重新发明轮子,所以我们将分析 Deno 社区目前存在的几个库和框架,它们专注于创建 web 应用程序。 + +我们将对现有的解决方案进行一般性的观察,并探索它们的功能和方法。 + +最后,我们将选择我们认为最适合我们用例的一个。 + +## 存在哪些替代方案? + +在撰写本文时,有几个第三方包提供了大量功能来创建 web 应用程序和 API。其中一些深受非常流行的 Node.js 包(如 Express.JS、Koa 或 hapi.js)的启发,而其他则受到 JavaScript 之外的框架(如 Laravel、Flask 等)的启发。 + +我们将探索其中的四个,这些在撰写本文时非常流行且维护得很好。请注意,由于 Deno 和提到的包正在快速发展,这可能会随时间而变化。 + +重要提示 + +Craig Morten 撰写了一篇非常彻底的分析文章,探讨了可用的库。如果你想要了解更多,我强烈推荐这篇文章([`dev.to/craigmorten/what-is-the-best-deno-web-framework-2k69`](https://dev.to/craigmorten/what-is-the-best-deno-web-framework-2k69))。 + +我们将尝试在我们将要探索的包中保持多样性。有些包提供了比其他包更多的抽象和结构,而有些包提供的功能并不多于纯粹的实用函数和可组合的功能。 + +我们将探索的包如下: + ++ Drash + ++ Servest + ++ Oak + ++ Alosaur + +让我们逐一看看。 + +### Drash + +Drash ([`github.com/drashland/deno-drash`](https://github.com/drashland/deno-drash))旨在与现有的 Deno 和 Node.js 框架不同。这一动机在其维护者 Edward Bebbington 的一篇博客文章中明确提到,他比较了 Drash 与其他替代方案,并解释了其创建的动机 ([`dev.to/drash_land/what-makes-drash-different-idd`](https://dev.to/drash_land/what-makes-drash-different-idd)). + +这些动机很好,而且像 Laravel、Flask 和 Tonic 这样非常流行的软件工具的启发证明了许多这些决定是合理的。你一查看 Drash 的代码,就能发现一些相似之处。 + +与 Express.js 或 Koa 等库相比,它确实提供了一种不同的方法,正如文档所述: + +“Deno 与 Node.js 的不同之处在于,Drash 旨在与 Express 或 Koa 不同,利用资源和一个完整的基于类的系统。” + +主要区别在于,Drash 不想提供应用程序对象,让开发者可以在其中注册他们的端点,就像一些流行的 Node.js 框架一样。它将端点视为在类内部定义的资源,类似于以下内容: + +```js +import { Drash } from +  "https://deno.land/x/drash@v1.2.2/mod.ts"; +class HomeResource extends Drash.Http.Resource { +  static paths = ["/"]; +  public GET() { +    this.response.body = "Hello World!"; +    return this.response; +  } +} +``` + +这些资源随后被插入到 Drash 的应用程序中: + +```js +const server = new Drash.Http.Server({ +  response_output: "text/html", +  resources: [HomeResource] +}); +server.run({ +  hostname: "localhost", +  port: 1447 +}); +``` + +在这里,我们可以直接声明,它实际上与我们在前面提到的其他框架不同。这些差异是有意的,旨在满足喜欢这种方法及其为其他框架解决问题的开发者。这些用例在 Drash 的文档中有很好的解释。 + +Drash 基于资源的的方法确实值得关注。它从非常成熟的软件(如 Flask 和 Tonic)中汲取灵感,确实为桌面上带来了一些东西,并提出了一种解决方案,有助于解决一些无观点工具的常见问题。文档完整且易于理解,这使得在选择构建应用程序的工具时,它成为了一个宝贵的资产。 + +### Servest + +Servest ([`servestjs.org/`](https://servestjs.org/))自称是一个*"适用于 Deno 的渐进式 HTTP 服务器。"* + +它被创建的一个原因是因为其作者想要使标准库的 HTTP 模块中的一些 API 更容易使用,并实验新功能。后者是在需要稳定性的标准库上真正难以实现的。 + +Servest 直接关注与标准库的 HTTP 模块的比较。其主要目标之一是在项目的主页上直接声明,即使其易于从标准库的 HTTP 模块迁移到 Servest。这很好地总结了 Servest 的愿景。 + +从 API 的角度来看,Servest 与我们从 Express.js 和 Koa 熟悉的东西非常相似。它提供了一个应用程序对象,可以在其中注册路由。你也可以看到以下代码片段中明显受到了标准库模块提供的内容的启发: + +```js +import { createApp } from +  "https://servestjs.org/@v1.1.4/mod.ts"; +const app = createApp(); +app.handle("/", async (req) => { +  await req.respond({ +    status: 200, +    headers: new Headers({ +      "content-type": "text/plain", +    }), +    body: "Hello, Servest!", +  }); +}); +app.listen({ port: 8899 }); +``` + +我们可以识别出来自知名 Node.js 库的应用程序对象和来自标准库的请求对象等功能。 + +在这些功能之上,Servest 还提供了诸如直接渲染 JSX 页面、提供静态文件和身份验证等常见功能。文档也非常清晰,充满了示例。 + +Servest 试图利用 Node.js 用户的知识和熟悉度,同时利用 Deno 提供的优势,这是一个很有前途的混合。它逐渐发展的特性为开发人员提供了非常好的功能,承诺会使开发人员比使用标准库 HTTP 包时更加高效。 + +### Oak + +Oak ([`oakserver.github.io/oak/`](https://oakserver.github.io/oak/)) 目前是创建网络应用程序最受欢迎的 Deno 库。它的名字来源于 Koa 的词语游戏,Koa 是一个非常流行的 Node.js 中间件框架,也是 Oak 的主要灵感来源。 + +由于其深受启发,其 API 与 Koa 相似,使用异步函数和上下文对象也就不足为奇了。Oak 还包括一个路由器,也是受 `@koa/router` 启发的。 + +如果你知道 Koa,下面的代码对你来说可能非常熟悉: + +```js +import { Application } from +  "https://deno.land/x/oak/mod.ts"; +const app = new Application(); +app.use((ctx) => { +  ctx.response.body = "Hello world!"; +}); +await app.listen("127.0.0.1:8000"); +``` + +对于不熟悉 Koa 的你们,我们将简要解释一下,因为理解它将帮助你理解 Oak。 + +Koa 通过使用现代 JavaScript 特性提供了一个最小化和不带偏见的的方法。Koa 最初被创建(由创建 Express.js 的同一团队)的原因之一是,其创建者想要创建一个利用现代 JavaScript 特性的框架,而不是像 Express 那样,在 Node.js 的早期就被创建。 + +团队希望使用诸如 promises 和 async/await 等新特性,然后解决开发者在使用 Express.JS 时面临的挑战。这些挑战大多数与错误处理、处理回调和某些 API 的缺乏清晰性有关。 + +Oak 的流行并非空穴来风,它在 GitHub 上的星级与其他替代方案的距离反映了这一点。单凭 GitHub 的星级并不能说明什么,但是结合打开和关闭的问题、发布的版本等等,我们可以看出人们为什么信任它。当然,这种熟悉在很大程度上影响了这个包的流行度。 + +在当前状态下,Oak 是一个坚实的(就 Deno 社区标准而言)构建网络应用程序的方式,因为它提供了一组非常清晰和直接的功能。 + +### Alosaur + +Alosaur([`github.com/alosaur/alosaur`](https://github.com/alosaur/alosaur))是一个基于装饰器和类的 Deno 网络应用程序框架。它在某种程度上与 Drash 相似,尽管最终的方法有很大不同。 + +作为其主要特性之一,Aloaur 提供诸如模板渲染、依赖注入和 OpenAPI 支持等功能。这些功能是在所有我们在这里介绍的替代方案的标准之上添加的,例如中间件支持和路由。 + +这个框架的方法是使用类定义控制器,并使用装饰器定义其行为,如下面的代码所示: + +```js +import { Controller, Get, Area, App } from +  'https://deno.land/x/alosaur@v0.21.1/mod.ts'; +@Controller() // or specific path @Controller("/home") +export class HomeController { +    @Get() // or specific path @Get("/hello") +    text() { +        return 'Hello world'; +    } +} +// Declare module +@Area({ +    controllers: [HomeController], +}) +export class HomeArea {} +// Create alosaur application +const app = new App({ +    areas: [HomeArea], +}); +app.listen(); +``` + +在这里,我们可以看到应用程序的实例化与 Drash 有相似之处。它还使用 TypeScript 装饰器来声明框架的行为。 + +Alosaur 与前面提到的大多数库相比采取了不同的方法,主要是因为它不试图简约。相反,它提供了一组在构建某些类型的应用程序时证明有用的特性。 + +我们决定看看它,不仅是因为它能做到它应该做的事情,而且还因为它在 Node.js 和 Deno 世界中有一些不常见的特性。这包括像依赖注入和 OpenAPI 支持这样的东西,这是其他展示的解决方案所没有提供的。同时,它保持了如模板渲染之类的特性,这可能是您从 Express.JS 熟悉的东西,但在更现代的框架中可能不那么熟悉。 + +最终的解决方案在提供的功能方面非常有前途且完整。这绝对是值得关注的东西,这样你可以看到它是如何发展的。 + +## 最终判决 + +在查看了所有展示的解决方案并认识到它们都有自己的优点之后,我们决定在本书的剩余部分使用 Oak。 + +这并不意味着本书将重点介绍 Oak。不会的,因为它只会处理 HTTP 和路由。Oak 的简约方法将与我们接下来要做的很多事情非常契合,帮助我们逐步创建功能而不会妨碍我们。它也是 Deno 社区中最稳定、维护得最好、最受欢迎的选项之一,这对我们的决定有明显的影响。 + +请注意,这个决定并不意味着我们将在接下来的几章中学到的内容不能在任何替代方案中完成。事实上,由于我们将如何组织和架构我们的代码,我们相信很容易使用不同的框架来完成我们将要做的绝大多数事情。 + +在本书的剩余部分,我们将使用其他第三方模块来帮助我们构建我们提出的功能。我们决定深入研究处理 HTTP 的库的原因是因为这是我们即将开发的应用程序的基本交付机制。 + +# 总结 + +在本章中,我们终于开始构建一个利用我们对 Deno 知识的应用程序。我们首先考虑了构建应用程序时我们将拥有的主要目标以及定义其架构。这些目标将为我们本书中关于架构和结构的绝大多数对话定下基调,因为我们将会不断回顾它们,确保我们的工作与它们保持一致。 + +我们首先创建了我们的文件夹结构,并试图实现我们第一个应用程序目标:拥有一个列出博物馆的 HTTP 端点。我们首先构建了简单的业务逻辑,并在需要关注分离和隔离以及职责时逐步推进。这些需求引导了我们的架构,证明了我们所创建的层次和抽象的有用性,并展示了它们所提供的价值。 + +通过明确职责和模块接口,我们了解到我们可以暂时使用内存数据库来构建我们的应用程序,我们也确实这样做了。借助这种分离层次结构的方法,我们可以稍后回来将其更改为合适的持久性层而不会遇到任何问题。在定义业务和数据访问逻辑之后,我们使用标准库创建了一个 web 服务器作为交付机制。在创建了一个非常简单的路由系统之后,我们插入了之前构建的业务逻辑,满足了本章的主要要求:拥有一个返回博物馆列表的应用程序。 + +所有这些工作都没有在业务逻辑、数据获取和交付层之间创建直接耦合。我们认为这在开始增加复杂性、扩展我们的应用程序并为其添加测试时会非常有用。 + +本章通过查看 Deno 社区目前存在的 HTTP 框架和库以及它们之间的差异和做法来结束。其中一些使用对 Node.js 用户熟悉的做法,而其他则深入使用 TypeScript 及其特性来创建更具结构的 web 应用程序。通过查看目前可用的四个解决方案,我们了解了社区正在开发的内容以及他们可能采取的方向。 + +我们最终选择了 Oak,这是一个非常简洁且相对成熟解决方案,以帮助解决本书后续内容中我们将遇到的路由和 HTTP 挑战。 + +在下一章中,我们将开始将 Oak 添加到我们的代码库中,并使用诸如身份验证和授权等有用功能,使用诸如中间件等概念,并使我们的应用程序增长,以实现我们为自己设定的目标。 + +让我们开始吧! diff --git a/docs/deno-web-dev/deno-web-dev_03.md b/docs/deno-web-dev/deno-web-dev_03.md new file mode 100644 index 0000000..78a040e --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_03.md @@ -0,0 +1,738 @@ +# 第三章:第*5 章*:添加用户和迁移到 Oak + +至此,我们已经为具有结构的 Web 应用程序奠定了基础,这将使我们能够在继续进行时添加更多功能。正如您可能已经从本章的名称猜测到的,我们将从向当前 Web 应用程序,Oak,添加我们选择的中间件框架开始本章。 + +与 Oak 一起,由于我们的应用程序开始有了更多的第三方依赖,我们将运用前面章节中学到的知识创建一个锁文件,并在安装依赖时执行完整性检查。这样,我们可以确保我们的应用程序在无依赖问题的情况下运行顺畅。 + +随着本章的深入,我们将开始了解如何使用 Oak 的功能简化我们的代码。我们将使我们的路由逻辑更具扩展性,同时也更具可伸缩性。我们最初的解决方案是使用`if`语句和标准库一起创建一个 DIY 路由解决方案,我们将在这里重构它。 + +完成这一点后,我们将得到更干净的代码,并能够使用 Oak 的功能,如自动内容类型定义、处理不允许的方法和路由前缀。 + +然后,我们将添加一个在几乎每个应用程序中都非常重要的功能:用户。我们将创建一个与博物馆并列的模块来处理所有与用户相关的事情。在这个新模块中,我们将开发创建用户的业务逻辑,以及使用常见的实践(如散列和盐)在数据库中创建新用户的代码。 + +在实现这些功能的过程中,我们将了解到 Deno 提供的其他模块,比如标准库的散列功能或包含在运行时中的加密 API。 + +向应用程序中添加这个新模块,并让它与其他应用程序部分进行交互,将是一种很好的测试应用程序架构的方法。通过这样做,我们将了解它在保持与上下文相关的所有内容在一个地方的同时如何扩展。 + +本章将涵盖以下主题: + ++ 管理依赖项和锁文件 + ++ 使用 Oak 编写 Web 服务器 + ++ 向应用程序添加用户 + ++ 让我们开始吧! + +## 技术要求 + +本章将在我们上一章开发的代码基础上进行构建。本章的所有代码文件都可以在这本书的 GitHub 仓库中找到,网址为[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections)。 + +# 管理依赖项和锁文件 + +在第二章《*工具链*》中,我们学习了 Deno 如何让我们进行依赖管理。在本节中,我们将更实际地使用它。我们首先将代码中所有分散的 URL 导入移到一个集中的依赖文件中;之后,我们将创建一个锁文件,确保我们的尚处于初级阶段的应用程序在任何地方都能顺利运行。最后,我们将学习如何根据锁文件安装项目的依赖项。 + +## 使用一个集中的依赖文件 + +在上一章中,你可能会注意到我们直接在代码中使用 URL 作为依赖。尽管这可行,但这是我们几章前 discouraged 的做法。在我们第一个阶段,这对我们有效,但随着应用程序开始增长,我们必须适当地管理我们的依赖项。我们想要避免因依赖版本冲突、URL 中的拼写错误以及依赖项分散在各个文件中而产生的困扰。为了解决这个问题,我们必须做以下几点: + +1. 在`src`文件夹的根目录下创建一个`deps.ts`文件。 + + 这个文件可以有任何你喜欢的名字。我们目前称之为`deps.ts`,因为这是 Deno 文档中提到的,也是许多模块使用的命名约定。 + +1. 将所有外部依赖从我们的代码移动到`deps.ts`。 + + 目前,我们唯一的依赖项就是标准库中的 HTTP 模块,可以在`src/web/index.ts`文件中找到。 + +1. 将导入移动到`deps.ts`文件中,并将`import`更改为`export`: + + ```js + export { serve } from +   "https://deno.land/std@0.83.0/http/server.ts" + ``` + +1. 注意固定版本位于 URL 上: + + ```js + export { serve } from +   "https://deno.land/std@0.83.0/http/server.ts" + ``` + + 正如我们在第二章《*工具链*》中学习的,这是 Deno 中的版本控制工作方式。 + + 我们现在需要更改依赖文件,使其直接从`deps.ts`文件导入,而不是直接从 URL 导入。 + +1. 在`src/web/index.ts`中,从`deps.ts`导入`serve`方法: + + ```js + import { serve } from "../deps.ts"; + ``` + +通过拥有一个集中的依赖文件,我们还有了一种轻松确保我们所有依赖项都本地下载的方式,而无需运行任何代码。有了这个,我们现在有一个文件,在其中我们可以运行`deno cache`命令(在第二章《*工具链*》中提到)。 + +## 创建一个锁文件 + +集中了我们的依赖项之后,我们需要确保安装项目的任何人都能得到与我们相同的依赖项版本。这是我们确保代码以相同方式运行的唯一方法。我们将通过使用一个锁文件来实现这一点。我们在第二章《*工具链*》中学习了如何做到这一点;在这里,我们将将其应用到我们的应用程序中。 + +让我们使用`lock`和`lock-write`标志以及锁文件和集中依赖文件`deps.ts`的路径来运行`cache`命令: + +```js +$ deno cache --lock=lock.json --lock-write src/deps.ts +``` + +在当前文件夹中应生成一个`lock.json`文件。如果您打开它,它应该包含 URL 的键值对,以及用于执行完整性检查的哈希。 + +这个锁文件应该随后添加到版本控制中。后来,如果一个同事想要安装这个相同的项目,他们只需要运行相同的命令,而不需要`--lock-write`标志: + +```js +$ deno cache --lock=lock.json src/deps.ts +``` + +这样,`src/deps.ts`中的所有依赖项(这应该全部)将被安装,并根据`lock.json`文件进行完整性检查。 + +现在,每当我们在项目中安装一个新的依赖项时,我们必须运行`deno` `cache`命令,并带上`lock`和`lock-write`标志,以确保锁文件被更新。 + +这一节就差不多结束了! + +在这一节中,我们学习了一个确保应用程序运行顺畅的简单但非常重要的步骤。这有助于我们避免未来的复杂问题,如依赖冲突和版本间行为不匹配。我们还保证了资源完整性,这对于 Deno 来说尤为重要,因为其依赖项是存储在 URL 中,而不是注册表中。 + +在下一节中,我们将从标准库 HTTP 服务器开始*重构*我们的应用程序,改为使用 Oak,这将导致我们的网络代码得到简化。 + +# 使用 Oak 编写 Web 服务器 + +在上一章的末尾,我们审视了不同的网络库。经过短暂的分析后,我们最终选择了 Oak。在本节中,我们将重写我们网络应用程序的一部分,以便我们可以使用它,而不是标准库的 HTTP 模块。 + +让我们打开`src/web/index.ts`并逐步开始处理它。 + +遵循 Oak 的文档([`deno.land/x/oak@v6.3.1`](https://deno.land/x/oak@v6.3.1)),我们唯一需要做的就是实例化`Application`对象,定义一个中间件,并调用`listen`方法。让我们这样做: + +1. 在`deps.ts`文件中添加 Oak 的导入: + + ```js + export { Application } from +   "https://deno.land/x/oak@v6.3.1/mod.ts" + ``` + + 如果您正在使用 VSCode,那么您可能已经注意到有一个警告,它说它无法在本地找到这个版本的依赖。 + +1. 让我们运行上一节中的命令来下载它并添加到锁文件中。 + + 不要忘记每次添加依赖时这样做,以便我们有更好的自动完成功能,并且我们的锁文件始终是更新的: + + ```js + $ deno cache --lock=lock.json --reload --lock-write src/deps.ts + Download https://deno.land/std@0.83.0/http/server.ts + Download https://deno.land/x/oak@v6.3.1/mod.ts + Download https://deno.land/std@0.83.0/encoding/utf8.ts + … + ``` + + 在所有必要的依赖项下载完成后,让我们开始在我们的代码中使用它们。 + +1. 删除`src/web/index.ts`中`createServer`函数的所有代码。 + +1. 在`src/web/index.ts`文件中,导入`Application`类并实例化它。创建一个极其简单的中间件(如文档中所述)并调用`listen`方法: + + ```js + import { Application } from "../deps.ts"; + … + export async function createServer({ +   configuration: { +     port +   }, +   museum + }: CreateServerDependencies) { +   const app = new Application (); +   app.use((ctx) => { +     ctx.response.body = "Hello World!"; +   }); +   await app.listen({ port }); + } + ``` + +请记住,在删除旧代码的同时,我们也移除了`console.log`,因此它暂时不会打印任何内容。让我们运行它并验证是否有任何问题: + +```js +$ deno run --allow-net src/index.ts   +``` + +现在,如果我们访问`http://localhost:8080`,我们将在那里看到“Hello World!”响应。 + +现在,你可能会想知道 Oak 应用程序的`use`方法是什么。嗯,我们将使用这个方法来定义中间件。现在,我们只是想它修改响应并在其主体中添加一条消息。在下一章,我们将深入学习中间件函数。 + +记得我们移除了`console.log`,如果应用程序正在运行,我们就得不到任何反馈吗?在了解如何向 Oak 应用程序添加事件监听器的同时,我们将学习如何做到这一点。 + +## 向 Oak 应用程序添加事件监听器 + +到目前为止,我们已经设法使应用程序运行,但目前,我们没有任何消息来确认这一点。我们将使用这个机会来了解 Oak 中的事件监听器。 + +Oak 应用程序分发两种不同类型的事件。其中一个是`listen`,而另一个是`the listen event`,我们将使用它来在应用程序运行时向控制台日志。另一个,`error`,是我们将在出现错误时向控制台日志使用的。 + +首先,让我们在`app.listen`语句之前添加`listen`事件的监听器: + +```js +app.addEventListener("listen", e => { +  console.log(`Application running at +    http://${e.hostname || 'localhost'}:${port}`) +}) +… +await app.listen({ port }); +``` + +请注意,我们不仅向控制台打印消息,还打印事件中的`hostname`并发送其默认值,以防它未定义。 + +为了安全和确保我们能捕获任何意外错误,我们还需要添加一个错误事件监听器。这个错误事件将在应用程序中发生未处理的错误时触发: + +```js +app.addEventListener("error", e => { +  console.log('An error occurred', e.message); +}) +``` + +这些处理程序,尤其是`error`处理程序,将帮助我们开发过程中快速反馈正在发生的情况。后来,当接近生产阶段时,我们将添加适当的日志中间件。 + +现在,你可能会认为我们仍然缺少我们在本章开始时拥有的功能,你是对的:我们从应用程序中移除了列出所有博物馆的端点。 + +让我们再次添加它,并学习如何在 Oak 应用程序中创建路由。 + +## 在 Oak 应用程序中处理路由 + +Oak 还提供了另一个对象,与`Application`类并列,允许我们定义路由——`Router`类。我们将使用这个来重新实现我们之前的路由,该路由列出了应用程序中的所有博物馆。 + +我们可以通过向构造函数发送前缀属性来创建它。这样做意味着那里定义的所有路由都将用这个路径进行前缀: + +```js +import { Application, Router } from "../deps.ts"; +… +const apiRouter = new Router ({ prefix: "/api" }) +``` + +现在,让我们恢复我们的功能,通过`GET`请求`/api/museums`返回博物馆列表: + +```js +apiRouter.get("/museums", async (ctx) => { +  ctx.response.body = { +    museums: await museum.getAll() +  } +}); +``` + +这里发生了一些事情。 + +在这里,我们使用 Oak 的路由 API 定义路由,通过发送 URL 和一个处理函数。然后我们的处理程序会带有一个上下文(`ctx`)对象调用。所有这些都在 Oak 的文档中有详细解释([`doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#Router`](https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#Router)),但我留给你们一个简短的总结。 + +在 Oak 中,一个处理程序可以做的所有事情都是通过上下文对象完成的。发出的请求在`ctx.request`属性中可用,而当前请求的响应在`ctx.response`中可用。头部、cookies、参数、正文等都可以在这些对象中找到。一些属性,如`ctx.response.body`,是可写的。 + +提示 + +您可以通过查看 Deno 的文档网站来更好地了解 Oak 的功能:[`doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts`](https://doc.deno.land/https/deno.land/x/oak@v6.3.0/mod.ts)。 + +在这种情况下,我们使用响应体属性来设置其内容。当 Oak 能够推断出响应的类型(这里是的 JSON),它自动为响应添加正确的`Content-Type`头。 + +我们将在本书中继续学习 Oak 及其功能。下一步是连接我们刚刚创建的路由器。 + +## 将路由器连接到应用程序 + +现在我们的路由器已经定义好了,我们需要在应用程序上注册它,这样它就可以开始处理请求了。 + +为此,我们将使用我们之前使用过的应用程序实例的方法——`use`方法。 + +在 Oak 中,一旦定义了一个`Router`(并注册了它),它提供了两个返回中间件函数的方法。这些函数可以用来在应用程序上注册路由。它们如下: + ++ `routes`:在应用程序中注册已注册的路由处理程序。 + ++ `allowedMethods`:为在路由器中未定义的 API 方法的调用注册自动处理程序,返回一个`405 – Not allowed`响应。 + +我们将使用它们来在主应用程序中注册我们的路由,如下所示: + +```js +const apiRouter = new Router({ prefix: "/api" }) +apiRouter.get("/museums", async (ctx) => { +  ctx.response.body = { +    museums: await museum.getAll() +  } +}); +app.use(apiRouter.routes()); +app.use(apiRouter.allowedMethods()); +app.use((ctx) => { +  ctx.response.body = "Hello World!"; +}); +``` + +这样一来,我们的路由器就在应用程序中注册了它的处理程序,它们准备好开始处理请求。 + +记住,我们必须在之前注册我们之前定义的 Hello World 中间件。如果我们不这样做,Hello World 处理程序会在它们到达我们的路由器之前响应所有请求,因此它将无法工作。 + +现在,我们可以通过运行以下命令来运行我们的应用程序: + +```js +$ deno run --allow-net src/index.ts +Application running at http://localhost:8080 +``` + +然后,我们可以执行一个`curl`到 URL: + +```js +$ curl http://localhost:8080/api/museums +{"museums":[{"id":"1fbdd2a9-1b97-46e0-b450-62819e5772ff","name":"The Louvre","description":"The world's largest art museum and a historic monument in Paris, France.","location":{"lat":"48.860294","lng":"2.33862"}}]} +``` + +正如我们所看到的,一切都在按预期进行!我们成功地将我们的应用程序迁移到了 Oak。 + +通过这样做,我们大大提高了我们代码的可读性。我们还使用 Oak 处理我们不想处理的事情,并成功地专注于我们的应用程序。 + +在下一节中,我们将向应用程序中添加用户概念。将会创建更多的路由,以及一个全新的模块和一些业务逻辑来处理用户。 + +提示 + +本章的代码可以在[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter05/sections)找到,按部分分开。 + +现在,让我们向应用程序中添加一些用户! + +# 向应用程序添加用户 + +目前我们已经有了一个运行中的第一个端点,列出了应用程序中的所有博物馆,但我们仍然离最终要求相去甚远。 + +我们希望添加用户,以便可以注册、登录并在应用程序中与身份互动。 + +我们将首先创建一个定义用户的对象,然后进入业务逻辑以创建和存储它。之后,我们将创建允许我们通过 HTTP 与应用程序交互的端点,从而允许用户注册。 + +## 创建用户模块 + +目前我们可以说在应用程序中有一个单一的“模块”:`museums`模块。与博物馆有关的所有内容都在这里,从控制器到仓库、对象定义等。这个模块只有一个接口,即它的`index.ts`文件。 + +我们这样做是为了在保持模块外部 API 稳定的同时,在模块内部拥有工作自由,从而实现模块之间的良好解耦。为了确保模块内部的部分合理解耦,我们还必须通过构造函数注入它们的依赖项,允许我们轻松地交换部分并独立测试它们(如您将在第八章中看到的,*测试 - 单元和集成*)。 + +遵循这些指导原则,我们将继续使用这种“模块”系统,并按照以下步骤为我们的用户创建一个: + +1. 在`src/users`目录下创建一个名为`index.ts`的文件。 + +1. 创建`src/users/types.ts`文件。这是我们定义`User`类型的地方: + + ```js + export type User = { +   username: string, +   hash: string, +   salt: string, +   createdAt: Date + } + ``` + + 我们的用户对象将非常简单:它将有一个`username`,一个`createdAt`日期,然后有两个属性:`hash`和`salt`。我们将使用这些来保护存储用户密码时的用户密码。 + +1. 在`src/users/controller.ts`中创建一个名为`register`的用户控制器方法。它应该接收一个用户名和一个密码,然后在数据库中创建一个用户: + + ```js + type RegisterPayload = +   { username: string, password: string }; + export class Controller { +   public async register(payload: RegisterPayload) { +     // Logic to register users +   } + } + ``` + +1. 在`src/users/types.ts`中定义`RegisterPayload`并将其从`src/users/controller.ts`中移除,在`src/users/index.ts`中导出它。 + + 在`src/users/types.ts`中添加以下内容: + + ```js + // src/users/types + export type RegisterPayload = +   { username: string; password: string }; + ``` + + 在`src/users/index.ts`中添加以下内容: + + ```js + export type { +   RegisterPayload, + } from "./types.ts"; + ``` + + 让我们在这里停下来,思考一下寄存器逻辑。 + + 要创建用户,我们必须检查该用户是否存在于数据库中。如果不存在,我们将使用输入的用户名和密码创建他们,然后返回一个不包含敏感数据的对象。 + + 在上一章中,我们每次想要与数据源交互时都使用了仓库模式。仓库保留了所有*数据访问*逻辑(`src/museums/repository.ts`)。 + + 在这里,我们将做同样的事情。我们已经注意到,我们的控制器需要调用`UserRepository`中的两个方法:一个是为了检查用户是否存在,另一个是创建用户。这是我们接下来要定义的接口。 + +1. 前往`src/users/types.ts`并定义`UserRepository`接口: + + ```js + export type CreateUser = +   Pick; + … + export interface UserRepository { +   create: (user: CreateUser) => Promise +   exists: (username: string) => Promise + } + ``` + + 注意我们是如何创建一个`CreateUser`类型,它包含`User`的所有属性,除了`createdAt`,这应该由仓库添加。 + + 定义了`UserRepository`接口后,我们可以现在移动到用户的控制器中,并确保它在构造函数中接收接口的一个实例。 + +1. 在`src/users/controller.ts`中,创建一个`constructor`,它接收用户仓库作为注入参数,并用相同名称设置类属性: + + ```js + userRepository, we can start writing the logic for the register method. + ``` + +1. 编写`register`方法的逻辑,检查用户是否存在,如果不存在则创建他们: + + ```js + async register(payload: RegisterPayload) { + create method of userRepository to make sure it follows the CreateUser type we defined previously. These will have to be automatically generated, but don't worry about that for now.And with this, we've pretty much finished looking at what will happen whenever someone tries to register with our application. We're still missing one thing, though. As you may have noticed, we're returning the `User` object directly from the repository, which might contain sensitive information, namely the `hash` and `salt` properties. + ``` + +1. 在`src/users/types.ts`中创建一个名为`UserDto`的类型,定义不包含敏感数据的`User`对象的格式: + + ```js + export type User = { +   username: string, +   hash: string, +   salt: string, +   createdAt: Date + } + Pick to choose two properties from the User object; that is, createdAt and username.With `UserDto` ([`en.wikipedia.org/wiki/Data_transfer_object`](https://en.wikipedia.org/wiki/Data_transfer_object)) defined, we can now make sure our register is returning it. + ``` + +1. 创建一个名为`src/users/adapter.ts`的文件,里面有一个名为`userToUserDto`的函数,该函数将用户转换为`UserDto`: + + ```js + import type { User, UserDto } from "./types.ts"; + export const userToUserDto = (user: User): UserDto => { +   return { +     username: user.username, +     createdAt: user.createdAt +   } + } + ``` + +1. 在注册方法中使用最近创建的函数,以确保我们返回一个`UserDto`: + + ```js + import { userToUserDto } from "./adapter.ts"; + … + public async register(payload: RegisterPayload) { +   … +   const createdUser = await +     this.userRepository.create( +     payload.username, +     payload.password +   ); +   return userToUserDto(createdUser); + } + ``` + +有了这个,`register`方法就完成了! + +我们目前将散列和盐值作为两个没有意义的明文字符串发送。 + +你可能会想知道为什么我们不直接发送密码。这是因为我们想确保我们不在任何数据库中以明文形式存储密码。 + +为了确保我们遵循最佳实践,我们将使用散列和盐值在数据库中存储用户的密码。同时,我们还想了解一些 Deno API。这就是我们将在下一节中做的事情。 + +## 在数据库中存储用户 + +尽管我们使用的是内存数据库,但我们决定我们不会以明文形式存储密码。相反,我们将使用一种常见的存储密码的方法,称为散列和盐值。如果你不熟悉它,auth0 有一篇非常好的文章,我强烈推荐你阅读([`auth0.com/blog/adding-salt-to-hashing-a-better-way-to-store-passwords/`](https://auth0.com/blog/adding-salt-to-hashing-a-better-way-to-store-passwords/))。 + +这个模式本身并不复杂,你只需要按照代码学习就能掌握它。 + +所以,我们将以散列的形式存储密码。我们不会存储用户输入的确切散列密码,而是存储密码加上一个随机生成的字符串,称为盐值。这个盐值将与密码一起存储,以便稍后使用。在此之后,我们永远不需要再次解码密码。 + +有了盐值,无论何时我们想要检查密码是否正确,我们只需将盐值添加到用户输入的任何一个密码中,进行散列,并验证输出是否与数据库中存储的内容匹配。 + +如果你觉得这仍然很奇怪,我敢保证当你查看代码时,它会变得简单得多。让我们按照这些步骤实现这些函数: + +1. 创建一个名为`src/users/util.ts`的 utils 文件,里面有一个名为`hashWithSalt`的函数,该函数使用提供的盐值散列一个字符串: + + ```js + import { createHash } from +   "https://deno.land/std@0.83.0/hash/mod.ts"; + export const hashWithSalt = +   (password: string, salt: string) => { +     const hash = createHash("sha512") +       .update(`${password}${salt}`) +         .toString(); +   return hash; + }; + ``` + + 现在应该很清楚,这个函数将返回一个字符串,它是提供的字符串的`hash`值,加上一个`salt`。 + + 正如前面文章中提到的,使用不同盐值对不同密码也被认为是最佳实践。通过为每个密码生成不同的`salt`,即使一个密码的盐值被泄露,我们也能确保所有密码都是安全的。 + + 让我们通过创建一个生成`salt`的函数来继续进行: + +1. 使用`crypto` API([`doc.deno.land/builtin/stable#crypto`](https://doc.deno.land/builtin/stable#crypto))创建一个`generateSalt`函数,以获取随机值并从那里生成一个 salt 字符串: + + ```js + import { encodeToString } from +   "https://deno.land/std@0.83.0/encoding/hex.ts" + … + export const generateSalt = () => { +   const arr = new Uint8Array(64); +   crypto.getRandomValues(arr) +   return encodeToString(arr); + } + ``` + + 这就是我们为应用程序生成散列密码所需的所有内容。 + + 现在,我们可以开始在我们控制器中使用刚刚创建的实用函数。让我们创建一个方法,以便在那里散列我们的密码。 + +1. 在`UserController`中创建一个私有方法,名为`getHashedUser`,它接收一个用户名和密码,并返回一个用户,以及他们的散列值和盐值: + + ```js + import { generateSalt, hashWithSalt } from +   "./util.ts"; + … + export class Controller implements UserController { + … +   private async getHashedUser +     (username: string, password: string) { +     const salt = generateSalt(); +     const user = { +       username, +       hash: hashWithSalt(password, salt), +       salt +     } +     return user; +   } + … + ``` + +1. 在`register`方法中使用刚刚创建的`getHashedUser`方法: + + ```js + public async register(payload: RegisterPayload) { +   if (await +     this.userRepository.exists(payload.username)) { +     return Promise.reject("Username already exists"); +   } +   const createdUser = await +     this.userRepository.create( +     await this.getHashedUser +       (payload.username, payload.password) +   ); +   return userToDto(createdUser); + } + ``` + +大功告成!这样一来,我们就确保我们不会存储任何明文密码。在路径中,我们了解了 Deno 中可用的`crypto` API。 + +我们在实现所有这些功能时都在使用之前定义的`UserRepository`接口。然而,目前我们还没有一个实现该接口的类,所以让我们创建一个。 + +## 创建用户仓库 + +在前一节中,我们创建了定义`UserRepository`接口,所以接下来,我们要创建一个实现它的类。让我们开始吧: + +1. 创建一个名为`src/users/repository.ts`的文件,其中有一个导出的`Repository`类: + + ```js + import type { CreateUser, User, UserRepository } from +   "./types.ts"; + export class Repository implements UserRepository { +   async create(user: CreateUser) { +   } +   async exists(username: string) { +   } + } + ``` + + 接口保证这两个公共方法必须存在。 + + 现在,我们需要一种将用户存储在数据库中的方法。为了本章的目的,我们将继续使用内存数据库,这与我们对博物馆所做的工作非常相似。 + +1. 在`src/users/repository.ts`类中创建一个名为`storage`的属性。它应该是一个 JavaScript Map,将作为用户数据库使用: + + ```js + import { User, UserRepository } from "./types.ts"; + export class Repository implements UserRepository { +   private storage = new Map(); + … + ``` + + 有了数据库,我们现在可以实现这两个方法的逻辑。 + +1. 在`exists`方法中从数据库获取用户,如果存在则返回`true`,如果不存在则返回`false`: + + ```js + async exists(username: string) { +   return Boolean(this.storage.get(username)); + } + ``` + + `Map#get`函数如果在找不到记录时返回 undefined,所以我们将其转换为 Boolean,以确保它总是返回 true 或 false。 + + `exists`方法非常简单;它只需要检查用户是否存在于数据库中,然后相应地返回一个`boolean`。 + + 要创建用户,我们需要执行比那多一个或两个步骤。不仅仅是创建,我们还将确保它还向调用此函数的任何人发送了一个带有`createdAt`日期的用户。 + + 现在,让我们回到主要任务:在数据库中创建用户。 + +1. 打开`src/users/repository.ts`文件,实现`create`方法,以正确的格式创建一个`user`对象。 + + 记得在发送给函数的`user`对象中添加`createdDate`: + + ```js + async create(user: CreateUser) { +   const userWithCreatedAt = +     { ...user, createdAt: new Date() } +   this.storage.set +    (user.username, { ...userWithCreatedAt }); +   return userWithCreatedAt; + } + ``` + + 这样一来,我们的仓库就完整了! + + 它完全实现了我们之前在`UserRepository`接口中定义的内容,并准备使用。 + + 下一步是把这些碎片连接在一起。我们已经创建了`User`控制器 和 `User`仓库,但它们目前还没有在任何地方使用。 + + 在我们继续之前,我们需要将从用户模块中暴露出这些对象到外部世界。我们将遵循我们之前定义的规则;也就是说,模块的接口将总是位于其根目录下的`index.ts`文件。 + +1. 打开`src/users/index.ts`,并从模块中导出`Controller`、`Repository`类及其相应的类型: + + ```js + export { Repository } from './repository.ts'; + export { Controller } from './controller.ts'; + + export type { +   CreateUser, +   RegisterPayload, +   User, +   UserController, +   UserRepository, + } from "./types.ts"; + ``` + + 我们现在可以确保用户模块中的每个文件都是直接从这个文件(`src/users/index.ts`)导入类型,而不是直接导入其他文件。 + +现在,任何想要从用户模块导入内容的模块都必须通过`index.ts`文件进行。现在,我们可以开始考虑用户将如何与刚刚编写的业务逻辑互动。由于我们正在构建一个 API,下一节我们将学习如何通过 HTTP 暴露它。 + +## 创建注册端点 + +有了业务逻辑和数据访问逻辑,唯一缺少的是用户可以调用以注册自己的端点。 + +对于注册请求,我们将实现一个`POST /api/users/register`,预期一个包含名为`user`的属性,其中包含`username`和`password`两个属性的 JSON 对象。 + +我们首先必须做的是声明`src/web/index.ts`中的`createServer`函数将依赖于`UserController`接口的注入。让我们开始: + +1. 在`src/users/types.ts`中创建`UserController`接口。确保它也导出在`src/users/index.ts`中: + + ```js + RegisterPayload from src/users/controller.ts previously. + ``` + +1. 现在,为了保持整洁,去`src/users/controller.ts`并确保类实现了`UserController`: + + ```js + import { RegisterPayload, UserController, +   UserRepository } from "./types.ts"; + export class Controller implements UserController + ``` + +1. 回到`src/web/index.ts`,将`UserController`添加到`createServer`依赖项中: + + ```js + import { UserController } from "../users/index.ts"; + interface CreateServerDependencies { +   configuration: { +     port: number +   }, +   museum: MuseumController, +   user: UserController + } + export async function createServer({ +   configuration: { +     port +   }, +   museum, +   user + }: CreateServerDependencies) { + … + ``` + + 现在,我们准备创建我们的注册处理程序。 + +1. 创建一个处理`POST`请求的处理器,在`/api/users/register`下创建用户,使用注入的控制器的`register`方法: + + ```js + apiRouter.post method to define a route that accepts a POST request. Then, we're using the body method from the request (https://doc.deno.land/https/deno.land/x/oak@v6.3.1/mod.ts#ServerRequest) to get its output in JSON. We then do a simple validation to check if the username and password are present in the request body, and at the bottom, we use the injected register method from the controller. We're wrapping it in a try catch so that we can return HTTP status code 400 if an error happens. + ``` + +这应该足以使 web 层能够完美地回答我们的请求。现在,我们只需要把一切连接在一起。 + +## 用户控制器与 web 层的连接 + +我们已经创建了应用程序的基本部分。有业务逻辑,有数据访问逻辑,还有 web 服务器来处理请求。唯一缺少的是将它们连接在一起的东西。在本节中,我们将实例化我们定义的接口的实际实现,并将它们注入到期望它们的内容中。 + +返回`src/index.ts`。让我们对`museums`模块做类似的事情。在这里,我们将导入用户仓库和控制器,实例化它们,并将控制器发送到`createServer`函数。 + +按照以下步骤进行操作: + +1. 在`src/index.ts`中,从用户模块导入`Controller`和`Repository`,并在实例化它们的同时发送必要的依赖项: + + ```js + import { +   Controller as UserController, +   Repository as UserRepository, +    } from './users/index.ts'; + … + const userRepository = new UserRepository(); + const userController = new UserController({ +   userRepository }); + ``` + +1. 将用户控制器发送到`createServer`函数: + + ```js + createServer({ +   configuration: { port: 8080 }, +   museum: museumController, +   user: userController + }) + ``` + +就这样,我们完成了!为了完成这一切,让我们运行以下命令来运行我们的应用程序: + +```js +$ deno run --allow-net src/index.ts +Application running at http://localhost:8080 +``` + +现在,让我们用`curl`向`/api/users/register`发送请求,来测试注册的端点: + +```js +$ curl -X POST -d '{"username": "alexandrempsantos", "password": "testpw" }' -H 'Content-Type: application/json' http://localhost:8080/api/users/register +{"user":{"username":"alexandrempsantos","createdAt":"2020-10-06T21:56:54.718Z"}} +``` + +正如我们所看到的,它正在运行并返回`UserDto`的内容。我们本章的主要目标已经实现:我们创建了用户模块并向其中添加了一个注册用户的端点! + +# 总结 + +在本章中,我们的应用程序经历了巨大的变化! + +我们首先将应用程序从标准库 HTTP 模块迁移到 Oak。我们不仅迁移了服务应用程序的逻辑,还开始使用 Oak 的路由器定义一些路由。我们注意到,随着 Oak 封装了之前手动完成的某些工作,应用程序逻辑开始变得简单。我们成功地将标准库中的所有 HTTP 代码迁移过来,而没有改变业务逻辑,这是一个非常好的迹象,表明我们在应用程序架构方面做得很好。 + +我们一直在前进,并学习了如何在 Oak 应用程序中监听和处理事件。随着我们编写更多的代码,我们也对 Oak 变得更加熟悉,理解其功能,探索其文档,并对其进行实验。 + +用户是任何应用程序的重要组成部分,因此在本章中,我们花了很多时间来关注用户。我们不仅在应用程序中添加了用户,还将用户作为一个独立的、自包含的模块添加进来,与博物馆并列。 + +一旦我们在应用程序中开发了注册用户的业务逻辑,为它提供一个持久化层就变得迫切需要了。这意味着我们必须开发一个用户仓库,负责在数据库中创建用户。在这里,我们深入实现了一个哈希和盐机制,以安全地在数据库中存储用户的密码,并在过程中学习了几个 Deno API。 + +用户业务逻辑完成后,我们转向了缺失的部分:HTTP 端点。我们在 HTTP 路由器中添加了注册路线,并在 Oak 的帮助下设置了一切。 + +为了结束这一切,我们再次使用依赖注入将所有内容连接起来。由于我们所有模块的依赖都是基于接口的,我们很容易注入所需的依赖,并使我们的代码工作。 + +本章是我们使应用程序更具可扩展性和可读性的旅程。我们首先移除了自制的路由器代码并将其移到 Oak 中,最后我们添加了一个重要的大型业务实体——用户。后者也作为我们架构的测试,并展示了它如何适应不同的业务领域。 + +在下一章中,我们将通过添加一些有趣的功能来不断迭代应用程序。这样做,我们将完成在这里创建的功能,例如用户登录、授权以及在真实数据库中的持久化。我们还将处理包括基本日志记录和错误处理在内的常见 API 实践。 + +兴奋吗?我们也是——开始吧! diff --git a/docs/deno-web-dev/deno-web-dev_04.md b/docs/deno-web-dev/deno-web-dev_04.md new file mode 100644 index 0000000..7f2344e --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_04.md @@ -0,0 +1,959 @@ +# 第四章:*第六章*:添加认证并连接到数据库 + +在上一章中,我们向应用程序添加了一个 HTTP 框架,极大地简化了我们的代码。之后,我们在应用程序中添加了用户概念,并开发了注册端点。在其当前状态下,我们的应用程序已经在存储一些东西,一个小坑爹的是它把这些东西存储在内存中。我们将在本章解决这个问题。 + +在实现 oak(首选的 HTTP 框架)时,我们使用了另一个概念,即中间件函数。我们将从学习中间件函数是什么以及为什么它们几乎是所有 Node.js 和 Deno 框架在复用代码时的*标准*开始这一章节。 + +然后我们将使用中间件函数并实现登录和授权。此外,我们还将学习如何使用中间件向应用程序添加标准功能,如请求日志和计时。 + +随着我们在需求方面非常接近完成应用程序,我们将花费本章剩余的时间学习如何连接到真正的持久化引擎。在这本书中,我们将使用 MongoDB。我们将使用我们之前构建的抽象确保过渡顺利。然后我们将创建一个新的用户存储库,以便它可以以与内存解决方案相同的方式连接到数据库。 + +在本章结束时,我们将拥有一个完整的应用程序,支持注册和用户登录。登录后,用户还可以获取博物馆列表。所有这些都是通过 HTTP 和持久化实现的业务逻辑完成的。 + +在本章结束后,我们将只回来添加测试并部署应用程序,从而完成构建应用程序的完整周期。 + +在本章中,我们将涵盖以下主题: + ++ 使用中间件函数 + ++ 添加认证 + ++ 使用 JWT 添加授权 + ++ 连接 MongoDB + +让我们开始吧! + +## 技术要求 + +本章所需的代码可在以下 GitHub 链接中找到:[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06)。 + +# 使用中间件函数 + +如果你使用过任何 HTTP 框架,无论是 JavaScript 还是其他框架,你可能熟悉中间件函数的概念。如果你不熟悉,也不要担心——这就是我们将在这一节解释的内容。 + +让我们从一个借用自 Express.js 文档的定义开始:[`expressjs.com/en/guide/writing-middleware.html`](http://expressjs.com/en/guide/writing-middleware.html): + +“中间件函数是具有访问请求对象(req)、响应对象(res)以及应用程序请求-响应周期中的下一个中间件函数的函数。下一个中间件函数通常由一个名为 next 的变量表示。” + +中间件函数拦截请求并具有对它们进行操作的能力。它们可以在许多不同的用例中使用,如下所示: + ++ 更改请求和响应对象 + ++ 结束请求-响应生命周期(回答请求或跳过其他处理程序) + ++ 调用下一个中间件函数 + +中间件函数通常用于诸如检查身份验证令牌和根据结果自动响应、记录请求、向请求中添加特定头文件、用上下文丰富请求对象以及错误处理等任务。 + +我们将在应用程序中实现一些这些示例。 + +## 中间件是如何工作的? + +中间件被视为一个栈来处理,每个函数都可以通过在栈的其余部分执行之前和之后运行代码来控制响应的流程。 + +在 oak 中,通过`use`函数注册中间件。在这个阶段,您可能还记得我们之前对 oak 的路由做了什么。oak 的`Router`对象创建了注册路由的处理程序,并导出具有该行为的两项中间件函数以在主应用程序上注册。这些称为`routes`和`allowedMethods` ([`github.com/PacktPublishing/Deno-Web-Development/blob/43b7f7a40157212a3afbca5ba0ae20f862db38c4/ch5/sections/2-2-handling-routes-in-an-oak-application/museums-api/src/web/index.ts#L38`](https://github.com/PacktPublishing/Deno-Web-Development/blob/43b7f7a40157212a3afbca5ba0ae20f862db38c4/ch5/sections/2-2-handling-routes-in-an-oak-application/museums-api/src/web/index.ts#L38)). + +为了更好地理解中间件函数,我们将实现几个。我们将在下一节中这样做。 + +## 通过中间件添加请求计时 + +让我们通过一些中间件向我们的请求添加基本日志记录。Oak 中间件函数([`github.com/oakserver/oak#application-middleware-and-context`](https://github.com/oakserver/oak#application-middleware-and-context))接收两个参数。第一个是上下文对象,这是所有路由都相同的对象,而第二个是`next`函数。这个函数可以用来执行栈中的其他中间件,允许我们控制应用程序流程。 + +我们要做的第一件事是添加一个中间件,以在响应中添加`X-Response-Time`头。按照以下步骤操作: + +1. 转到`src/web/index.ts`并注册一个通过调用`next`来执行栈其余部分的中间件。 + + 这会在响应中添加一个头,其中包含从请求开始到处理完毕的毫秒差: + + ```js + const app = new Application(); + .use calls; this way, all the other middleware functions will run once this has been executed.The first lines are executed before the route handler (and other middleware functions) starts handling the request. Then, the call to `next` makes sure the route handlers execute; only then is the rest of the middleware code executed, thus calculating the difference from the initial value and the current date and adding it as a header. + ``` + +1. 执行以下代码以启动服务器: + + ```js + $ deno run --allow-net src/index.ts + Application running at http://localhost:8080 + ``` + +1. 执行一个请求并检查是否具有所需的头文件: + + ```js + x-response-time header there. Note that we've used the -i flag so that we're able to see the response headers on curl. + ``` + +有了这些,我们在第一次完全理解它们后使用了中间件函数。我们通过使用`next`来控制应用程序的流程,并通过添加头文件来丰富请求。 + +接下来,我们将对刚刚创建的中间件进行组合并添加逻辑,以记录向服务器发出的请求。 + +## 通过中间件添加请求日志 + +现在我们已经有了计算请求时间的逻辑,我们处于向应用程序添加请求日志的好位置。 + +最终目标是将每个对应用程序的请求记录到控制台,包括其路径、HTTP 方法和响应时间;类似于以下示例: + +```js +GET http://localhost:8080/api/museums - 65ms +``` + +当然,我们可以针对每个请求单独这样做,但由于我们希望跨应用程序执行此操作,因此我们将把它作为中间件添加到`Application`对象中。 + +我们在上一节编写的中间件要求处理程序(和中间件函数)运行,以便添加响应时间(它在执行部分逻辑之前调用下一个函数)。我们需要在之前注册的时间记录中间件之前注册当前的日志中间件,该中间件将请求时间添加到请求中。让我们开始吧: + +1. 转到`src/web/index.ts`并在控制台上记录请求方法、路径和时间: + + ```js + X-Response-Time header, which is going to be set by the previous middleware to log the request to the console. We're also using next to make sure all the handlers (and middleware functions) run before we log to the console. We need this specifically because the header is set by another piece of middleware. + ``` + +1. 执行以下代码以启动服务器: + + ```js + $ deno run --allow-net src/index.ts + Application running at http://localhost:8080 + ``` + +1. 向端点发送请求: + + ```js + $ curl http://localhost:8080/api/museums + ``` + +1. 检查服务器进程是否将请求记录到控制台: + + ```js + $ deno run --allow-net src/index.ts + Application running at http://localhost:8080 + GET http://localhost:8080/api/museums - 46ms + ``` + +有了这个,我们的中间件函数就可以协同工作了! + +在这里,我们直接在主应用程序对象上注册了中间件函数。然而,也可以通过调用相同的`use`方法在特定的 oak 路由器上执行此操作。 + +为了给您一个例子,我们将注册一个只对`/api`路由执行的中间件。我们将做与之前完全相同的事情,但这次不是在`Application`对象上,而是在 API`Router`对象上调用`use`方法,如下示例所示: + +```js +const apiRouter = new Router({ prefix: "/api" }) +apiRouter.use(async (_, next) => { +  console.log("Request was made to API Router"); +  await next(); +})) +… +app.use(apiRouter.routes()); +app.use(apiRouter.allowedMethods()); +``` + +希望应用程序流程正常进行的中间件函数*必须调用*`next`函数。如果没有发生这种情况,堆栈中的其余中间件和路由处理程序将不会执行,因此请求将不会得到回答。 + +使用中间件函数还有一种方法:直接在请求处理程序之前添加它们。 + +假设我们想要创建一个中间件,向某些路由添加`X-Test`头。我们可以在应用程序对象上编写该中间件,也可以像以下代码所示直接在路由上使用它: + +```js +import { Application, Router, RouterMiddleware } from +  "../deps.ts"; +… +const addTestHeaderMiddleware: RouterMiddleware = async (ctx, +   next) => { +  ctx.response.headers.set("X-Test", "true"); +  await next(); +} +apiRouter.get("/museums", addTestHeaderMiddleware, async (ctx) +  => { +  ctx.response.body = { +    museums: await museum.getAll() +  } +}); +``` + +为了使前面的代码运行,我们需要在`src/deps.ts`中导出`RouterMiddleware`类型: + +```js +export type { RouterMiddleware } from +  "https://deno.land/x/oak@v6.3.1/mod.ts"; +``` + +有了这个中间件,无论何时我们想要添加`X-Test`头,只需要在路由处理程序之前包含`addTestHeaderMiddleware`。它会在处理程序代码之前执行。这不仅仅针对一个中间件,因为可以注册多个中间件函数。 + +关于中间件函数的内容就到这里! + +通过使用 Web 框架的这一非常常见的特性,我们学会了创建和分享功能的基本知识。在我们进入下一部分时,我们会继续使用它们,在那里我们将处理认证、验证令牌和授权用户。 + +让我们去实现我们应用程序的认证! + +# 添加认证 + +在前一章节中,我们向应用程序添加了创建新用户的功能。这个功能本身很酷,但如果我们不能用它来进行认证,那它就值不了多少。我们在这里做这件事。 + +我们将从创建检查用户名和密码组合是否正确的逻辑开始,然后实现一个端点来完成这件事。 + +之后,我们将通过从登录端点返回一个令牌来过渡到授权主题,然后使用该令牌来检查用户是否已认证。 + +让我们一步一步来,从业务逻辑和持久性层开始。 + +## 创建登录业务逻辑 + +我们已经有了一种实践,那就是在编写新功能时,首先从业务逻辑开始。我们认为这是直观的,因为你首先考虑“业务”和用户,然后才进入技术细节。我们在这里做这件事。 + +我们首先在`UserController`中添加登录逻辑: + +1. 在开始之前,让我们在`src/users/types.ts`中向`UserController`接口添加`login`方法: + + ```js + export type RegisterPayload = { username: string; +   password: string }; + export type LoginPayload = { username: string; password: +   string }; + export interface UserController { +   register: (payload: RegisterPayload) => +     Promise; +   login: ( +     { username, password }: LoginPayload, +   ) => Promise<{ user: UserDto }>; + } + ``` + +1. 在控制器上声明`login`方法;它应该接收一个用户名和一个密码: + + ```js + public async login(payload: LoginPayload) { + } + ``` + + 让我们停下来思考一下登录流程应该是什么样子: + + + 用户发送他们的用户名和密码。 + + + 应用程序通过用户名从数据库中获取用户。 + + + 应用程序使用数据库中的盐对用户发送的密码进行编码。 + + + 应用程序比较两个加盐的密码。 + + + 应用程序返回一个用户和一个令牌。 + + 现在我们不担心令牌。然而,流程的其余部分应该为当前部分设定要求,帮助我们思考`login`方法的代码。 + + 单从这些要求来看,我们就可以理解到我们需要在`UserRepository`上有一个通过用户名获取用户的方法。让我们来看看这个。 + +1. 在`src/users/types.ts`中,向`UserRepository`添加一个`getByUsername`方法;它应该通过用户名从数据库中获取一个用户: + + ```js + export interface UserRepository { +   create: (user: CreateUser) => Promise;   +   exists: (username: string) => Promise +   getByUsername: (username: string) => Promise + } + ``` + +1. 在`src/users/repository.ts`中实现`getByUsername`方法: + + ```js + export class Repository implements UserRepository { + … + UserController and use the recently created method to get a user from the database. + ``` + +1. 在`UserController`的`login`方法中使用来自仓库的`getByUsername`方法: + + ```js + public async login(payload: LoginPayload) { +   hashPassword in the previous chapter when we implemented the register logic, so let's use that. + ``` + +1. 在`UserController`中创建一个`comparePassword`方法。 + + 它应该接收一个密码和一个`user`对象。然后,在密码经过加盐和散列后,与数据库中存储的密码进行比较: + + ```js + import { +   LoginPayload, +   RegisterPayload, +   User, +   UserController, +   UserRepository, + } from "./types.ts"; + import { hashWithSalt } from "./util.ts" + … + private async comparePassword(password: string, user: +   User) { +   const hashedPassword = hashWithSalt (password, +     user.salt); +   if (hashedPassword === user.hash) { +     return Promise.resolve(true); +   } +   return Promise.reject(false); + } + ``` + +1. 在`UserController`的`login`方法中使用`comparePassword`方法: + + ```js + public async login(payload: LoginPayload) { +   try { +     const user = await +      this.userRepository.getByUsername(payload.username); +     await this.comparePassword(payload.password, user); +     return { user: userToUserDto(user) }; +   } catch (e) { +     console.log(e); +     throw new Error('Username and password combination is +       not correct') +   } + } + ``` + +这样一来,我们就有了`login`方法! + +它接收一个用户名和一个密码,通过用户名获取用户,比较散列密码,如果一切按计划进行,就返回用户。 + +我们现在应该准备好实现登录端点——一个将使用我们刚刚创建的登录方法的端点。 + +## 创建登录端点 + +现在我们已经创建了业务逻辑和数据获取逻辑,我们可以开始在我们的网络层中使用它。让我们创建一个`POST /api/login`路线,该路线应该允许用户使用他们的用户名和密码登录。按照以下步骤操作: + +1. 在`src/web/index.ts`中创建登录路线: + + ```js + apiRouter.post("/login", async (ctx) => { + }) + ``` + +1. 通过使用`request.body`函数获取请求体([`doc.deno.land/https/raw.githubusercontent.com/oakserver/oak/main/request.ts#Request`](https://doc.deno.land/https/raw.githubusercontent.com/oakserver/oak/main/request.ts#Request)),然后将用户名和密码发送到`login`方法: + + ```js + apiRouter.post("/login", async (ctx) => { +   400 Bad Request) if things didn't go well. + ``` + +1. 如果登录成功,它应该返回我们的`user`: + + ```js + … + const { user: loginUser } = await user.login({ username, +   password }); + ctx.response.body = { user: loginUser }; + ctx.response.status = 201; + … + ``` + + 有了这些,我们应该有了登录用户所需的一切!让我们试一试。 + +1. 通过运行以下命令来运行应用程序: + + ```js + $ deno run --allow-net src/index.ts + Application running at http://localhost:8080 + ``` + +1. 向`/api/users/register`发送请求以注册用户,然后尝试使用刚创建的用户在`/api/login`登录: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw" }' -H 'Content-Type: application/json' http://localhost:8080/api/users/register + {"user":{"username":"asantos00","createdAt":"2020-10-19T21:30:51.012Z"}} + ``` + +1. 现在,尝试使用创建的用户登录: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw" }' -H 'Content-Type: application/json' http://localhost:8080/api/login + {"user":{"username":"asantos00","createdAt":"2020-10-19T21:30:51.012Z"}} + ``` + +这确实有效!我们在注册表上创建用户,之后能够用这些信息登录。 + +在这一节,我们学习了如何将认证逻辑添加到我们的应用程序中,并实现了`login`方法,该方法允许用户使用注册用户登录。 + +在下一节,我们将学习如何使用我们创建的认证来获取一个令牌,这个令牌将允许我们处理授权。我们将使得博物馆路线只对认证用户开放,而不是公开可用。为此,我们需要开发授权特性。让我们深入了解一下! + +# 使用 JWT 添加授权 + +现在我们有一个应用程序,可以让我们登录并返回已登录的用户。然而,如果我们想在任何 API 中使用登录,我们必须创建一个授权机制。这个机制应该允许 API 的用户进行认证,获取一个令牌,并使用这个令牌来标识自己并访问资源。 + +我们这样做是因为我们希望关闭应用程序的一部分路由,使它们只对认证用户开放。 + +我们将通过使用**JSON Web Tokens**(**JWT**)来开发与令牌认证集成所需的内容,这在现在的 API 中基本上是一个标准。 + +如果你不熟悉 JWT,我会让你参考[jwt.io](http://jwt.io)的解释: + +"JSON Web Tokens 是一种开放的、符合行业标准的 RFC 7519 方法,用于在两个方之间安全地表示声明。" + +它主要用在当你希望你的客户端连接到一个认证服务,并提供让你的服务器能够验证该认证是否由一个你信任的服务发出时。 + +为了避免重复[jwt.io](http://jwt.io)已经很好地解释的内容的风险,我将留下一个链接,完美地解释了这个标准是什么:[`jwt.io/introduction/`](https://jwt.io/introduction/)。确保阅读它;我相信您具备了理解我们接下来如何使用它的所有条件。 + +在本节中,由于本书的范围,我们将不会实现生成和验证 JWT 令牌的全部逻辑。这段代码可以在本书的 GitHub 仓库中找到([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth))。 + +我们在这里要做的就是将我们的当前应用程序与一个具有生成和验证 JWT 令牌功能的模块集成,这对我们的应用程序至关重要。然后,我们使用该令牌来决定是否允许用户访问博物馆路线。 + +让我们开始吧! + +## 从登录返回令牌 + +在上一节中,我们实现了登录功能。我们开发了一些逻辑来验证用户名和密码的组合,如果成功,则返回用户。 + +为了授权用户并让他们访问私有资源,我们需要知道已认证的用户是谁。常见的方法是通过令牌来实现。我们有各种方法可以做到这一点。它们是基本 HTTP 认证、会话令牌、JWT 令牌等替代方案。我们选择 JWT,因为我们认为它是业界广泛使用的解决方案,您可能在行业中已经遇到过。如果您没有,也不要担心;它简单到足以掌握。 + +我们首先需要做的是在用户登录时返回令牌。我们的`UserController`必须在与`userDto`一起返回令牌。 + +在提供的`jwt-auth`模块中([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth)),您可以检查到我们导出了一个仓库。 + +如果我们访问文档,使用 Deno 的文档网站[`doc.deno.land/https/raw.githubusercontent.com/PacktPublishing/Deno-Web-Development/master/Chapter06/jwt-auth/repository.ts`](https://doc.deno.land/https/raw.githubusercontent.com/PacktPublishing/Deno-Web-Development/master/Chapter06/jwt-auth/repository.ts),我们可以看到它导出了两个方法:`getToken`和`generateToken`。 + +阅读方法文档,我们可以理解,一个是为用户 ID 获取令牌,另一个是生成新令牌。 + +让我们使用这个方法,在登录用例中按照以下步骤生成新令牌: + +1. 首先,在`src/users/types.ts`中为`UserController`的返回类型添加令牌: + + ```js + export interface UserController { +   register: (payload: RegisterPayload) => +     Promise +   login: ({ username, password }: LoginPayload) => +     Promise<{ user: UserDto, UserController knows how to return a token. Looking at its logic, we can see that it should be able to delegate that responsibility by calling a method that will return that token. From the previous chapters, we know that we don't want to import our dependencies directly; we'd rather have them injected into our `constructor`. That's what we'll do here. Another thing we know is that we want to use this "third-party module" that deals with authentication. We'll need to add it to our dependencies file. + ``` + +1. 打开`src/deps.ts`,为`jwt-auth`模块添加导出,并运行`deno cache`以更新锁定文件并下载依赖项: + + ```js + export type { +   Algorithm, + } from "https://raw.githubusercontent.com/PacktPublishing/ + Deno-Web-Development/master/Chapter06/jwt-auth/mod.ts"; + export { +   Repository as AuthRepository, + } from "https://raw.githubusercontent.com/PacktPublishing/ +   Deno-Web-Development/master/Chapter06/jwt-auth/mod.ts"; + ``` + +1. 使用`AuthRepository`类型来定义`UserController`构造函数的依赖项: + + ```js + authRepository, which we've just imported. We previously discovered that it exposes a generateToken method, which will be of use to the login of UserController. + ``` + +1. 前往`src/users/controller.ts`中的`login`方法,并使用`authRepository`中的`generateToken`方法来获取令牌并返回它: + + ```js + public async login(payload: LoginPayload) { +     try { +       const user = await +         this.userRepository.getByUsername +           (payload.username); +       await this.comparePassword(payload.password, user); + authRepository to get a token. If we try to run this code, we know it will fail. In fact, we just need to open `src/index.ts` to see our editor's warnings. It is complaining that we're not sending `authRepository` to `UserController`, and we should. + ``` + +1. 回到`src/index.ts`,并从`jwt-auth`实例化`AuthRepository`: + + ```js + import { AuthRepository } from "./deps.ts"; + … + const authRepository = new AuthRepository({ +   configuration: { +     algorithm: "HS512", +     key: "my-insecure-key", +     tokenExpirationInSeconds: 120 +   } + }); + ``` + + 你也可以通过模块的文档来检查,因为它需要一个带有三个属性的`configuration`对象发送过去;也就是说,`algorithm`(算法)、`key`(密钥)和`tokenExpirationInSeconds`(令牌过期时间,单位秒)。 + + `key`应该是一个用于创建和验证 JWT 的秘密值,`algorithm`是令牌将要使用的安全算法(支持 HS256、HS512 和 RS256),而`tokenExpirationInSeconds`是令牌过期所需的时间。 + + 关于我们刚刚提到的不应该存在于代码中的秘密值,比如`key`变量,我们将在下一章学习如何处理它们,那一章我们将讨论应用程序配置。 + + 现在我们已经有了`AuthRepository`的一个实例!我们应该能够将其发送给我们的`UserController`并使其工作。 + +1. 在`src/index.ts`中,将`authController`传递给`UserController`构造函数: + + ```js + const userController = new UserController({ +   userRepository, authRepository }); + ``` + + 现在,你应该能够运行这个应用程序了! + + 现在,如果你创建几个请求来测试它,你会注意到`POST /login`端点仍然没有返回令牌。让我们解决这个问题! + +1. 前往`src/web/index.ts`,并在`login`路向上确保我们从`login`方法中返回的响应中得到了`token`: + + ```js + apiRouter.post("/login", async (ctx) => { +   const { username, password } = await +     ctx.request.body().value; +   try { +     const { user: loginUser, token } = await user.login({ +       username, password }); +     ctx.response.body = { user: loginUser, token }; +     ctx.response.status = 201; +   } catch (e) { +     ctx.response.body = { message: e.message }; +     ctx.response.status = 400; +   } + }) + ``` + +我们快完成了!我们成功完成了第一个目标:让`login`端点返回一个令牌。 + +我们想要实现的第一件事是确保用户在尝试访问认证路线时始终发送令牌的逻辑。 + +让我们去完成认证逻辑。 + +## 创建一个认证路由 + +有了向用户发放令牌的能力,我们现在想要一个保证,即只有登录用户才能访问博物馆路线。 + +用户必须将令牌发送在`Authorization`头中,正如 JWT 令牌标准所定义的。如果令牌无效或不存在,用户应看到一个`401 Unauthorized`状态码。 + +验证用户在请求中发送的令牌是中间件函数的一个很好的用例。 + +为了做到这一点,由于我们正在使用`oak`,我们将使用一个名为`oak-middleware-jwt`的第三方模块。这只是一个中间件,它根据密钥自动验证 JWT 令牌,并提供了对我们有用的功能。 + +你可以查看其文档,地址为:[`nest.land/package/oak-middleware-jwt`](https://nest.land/package/oak-middleware-jwt)。 + +让我们在我们的 Web 代码中使用这个中间件,使得博物馆路线只对已认证的用户可用。按照以下步骤操作: + +1. 在`deps.ts`文件中添加`oak-middleware-jwt`,并导出`jwtMiddleware`函数: + + ```js + export { +   jwtMiddleware, + } from "https://x.nest.land/ +    oak-middleware-jwt@2.0.0/mod.ts"; + ``` + +1. 回到`src/web/index.ts`,在博物馆路线上使用`jwtMiddleware`,并在那里发送密钥和算法。 + + 不要忘记我们在上一节中提到的内容——中间件函数可以在任何路线上使用,只需在路由处理程序之前发送它: + + ```js + import { Application, src/index.ts and forget to change this.This is exactly why we should extract this and expect it as a parameter to the `createServer` function. + ``` + +1. 在`configuration`内的`createServer`函数中添加`authorization`作为参数: + + ```js + import { Algorithm type from the deps.ts file, which exports it from the jwt-auth module. We're doing this so that we can ensure, via types, that the algorithms that are sent are only the supported ones. + ``` + +1. 现在,仍然在`src/web/index.ts`中,使用`authorization`参数将值传递给`jwtMiddleware`: + + ```js + const authenticated = jwtMiddleware(authorization) + ``` + + 我们唯一缺少的是实际将`authorization`值发送到`createServer`函数的能力。 + +1. 在`src/index.ts`中,将认证配置提取到一个变量中,以便我们可以重复使用它: + + ```js + import { AuthRepository, Algorithm } from "./deps.ts"; + … + const authConfiguration = { +   algorithm: "HS512" as Algorithm, +   key: "my-insecure-key", +   tokenExpirationInSeconds: 120 + } + const authRepository = new AuthRepository({ +   configuration: authConfiguration + }); + ``` + +1. 让我们重新使用那个变量向`createServer`发送所需的参数: + + ```js + createServer({ +   configuration: { +     port: 8080, +     authorization: { +       key: authConfiguration.key, +       algorithm: authConfiguration.algorithm +     } +   }, +   museum: museumController, +   user: userController + }) + ``` + + 这就完成了!让我们测试我们的应用程序,看看它是否如预期工作。 + + 请注意,期望的行为是只有经过认证的用户才能访问博物馆路线并查看所有博物馆。 + +1. 让我们通过运行以下命令来运行应用程序: + + ```js + $ deno run --allow-net src/index.ts + Application running at http://localhost:8080 + ``` + +1. 让我们注册一个用户以便我们可以登录: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw1" }' -H 'Content-Type: application/json' http://localhost:8080/api/users/register + {"user":{"username":"asantos00","createdAt" :"2020-10-27T19:14:01.984Z"}} + ``` + +1. 现在,让我们登录以便我们可以得到我们的令牌: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw1" }' -H 'Content-Type: application/json' http://localhost:8080/api/login + {"user":{"username":"asantos00","createdAt":"2020-10-27T19:14:01.984Z"},"token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtdXNldW1zIiwiZXhwIjoxNjAzODI2NTEzLCJ1c2VyIjoi YXNhbnRvczAwIn0.XV1vaHDpTu2SnavFla5q8eIPKCRIfDw_Kk-j8gi1 mqcz5UN3sVnk61JWCapwlh0IJ46fJdc7cw2WoMMIh-ypcg"} + ``` + +1. 最后,让我们尝试使用从上一个请求返回的令牌访问博物馆路线: + + ```js + Authentication header with Bearer as a prefix, as specified by the JWT specification. + ``` + +1. 为了确保它如预期工作,让我们尝试不带`Authorization`头的相同请求,期望一个`unauthorized`响应: + + ```js + -i flag with curl so that it logs the request status code and headers. + ``` + +就这样!通过这样做,我们成功地创建了一个只能被认证用户访问的路线。这对于任何包含用户的应用程序来说都是非常普遍的。 + +如果我们更深入这个话题,我们可以探索 JWT `refreshToken`,或者甚至如何从 JWT 令牌中读取用户信息,但那超出了这本书的范围。这是我将留给您自己探索的东西。 + +在这一节中,我们实现了我们的目标,并查看了 API 的许多不同部分。 + +不过,还有一件事缺失:与真实持久化引擎的连接。这就是我们接下来要做的——将我们的应用程序连接到 NoSQL 数据库! + +# 连接到 MongoDB + +到目前为止,我们已经实现了一个列出博物馆的应用程序,并包含用户,允许他们进行认证。这些功能已经就位,但它们都有一个缺点:它们都在内存数据库上运行。 + +我们选择这种方式是为了简化问题。然而,由于我们的大部分实现不依赖于交付机制,即使数据库发生变化,也不会有很大影响。 + +正如您可能从这一节的标题猜到的,我们将学习如何将应用程序的一个实体移动到数据库。我们将利用我们创建的抽象来实现这一点。这个过程将与所有实体非常相似,因此我们决定学习如何连接到数据库,只为了用户模块。 + +稍后,如果您好奇如果所有应用程序都连接到数据库,这将如何工作,您将有机会检查这本书的 GitHub 仓库。 + +为了确保我们都对一个类似的数据库运行,我们将使用 MongoDB Atlas。Atlas 是一个提供免费 MongoDB 集群的产品,我们可以用来连接我们的应用程序。 + +如果您不熟悉 MongoDB,这里有一个来自他们网站的“一句话解释”([`www.mongodb.com/`](https://www.mongodb.com/))。您可以随时去那里了解更多关于它的信息: + +"MongoDB 是一个通用目的、基于文档、分布式数据库,为现代应用程序开发人员和云时代而构建。" + +准备好了吗?我们出发吧! + +## 创建一个用户 MongoDB 仓库 + +我们当前的`UserRepository`是负责将用户连接到数据库的模块。这是我们想要更改以使我们的应用程序连接到 MongoDB 实例,而不是内存数据库的模块。 + +我们将经历创建新的 MongoDB 仓库的步骤,向世界暴露它,并将其余的应用程序连接到它。 + +让我们首先创建一个新用户仓库存在的空间,通过重新组织用户模块的内部文件结构。 + +### 重新排列我们的用户模块 + +我们的用户模块最初是设计为一个单一的仓库,因此它没有一个文件夹;只是一个单一的`repository.ts`文件。现在我们考虑将用户保存到数据库的其他方式,我们需要创建它。 + +还记得我们第一次谈到架构时,提到它将不断进化吗?这里正在发生这种情况。 + +让我们重新排列用户模块,以便它可以处理多个仓库并添加一个 MongoDB 仓库,遵循我们之前创建的`UserRepository`接口: + +1. 在`src/users`内创建一个名为`repository`的文件夹,并将实际的`src/users/repository.ts`移动到那里,将其重命名为`inMemory.ts`: + + ```js + └── src +     ├── museums +     ├── users +     │   ├── adapter.ts +     │   ├── controller.ts +     │   ├── index.ts +     │   ├── repository + │ │   ├── inMemory.ts +     │   ├── types.ts +     │   └── util.ts + ``` + +1. 记得要修复`src/users/repository/inMemory.ts`内的模块导入: + + ```js + import { User, UserRepository } from "../types.ts"; + import { generateSalt, hashWithSalt } from "../util.ts"; + ``` + +1. 为了保持应用的运行,我们前往`src/users/index.ts`并导出正确的仓库: + + ```js + export { Repository } from './repository/inMemory.ts' + ``` + +1. 现在,让我们创建我们的 MongoDB 仓库。将其命名为`mongoDb.ts`,并将其放入`src/users/respository`文件夹内: + + ```js + import { UserRepository } from "../types.ts"; + export class Repository implements UserRepository { +   storage +   async create(username: string, password: string) { +   } +   async exists(username: string) { +   } +   async getByUsername(username: string) { +   } + } + ``` + + 确保它实现了我们之前定义的`UserRepository`接口。 + +现在所有的乐趣都开始了!既然我们有了 MongoDB 仓库,我们将开始编写它并将其连接到我们的应用程序。 + +## 安装 MongoDB 客户端库 + +我们已经有一个我们仓库需要实现的方法的列表。遵循接口,我们可以保证我们的应用程序会工作,无论实现方式如何。 + +有一件事我们是确定的,因为我们不想一直重新发明轮子:我们将使用第三方包来处理与 MongoDB 的连接。 + +我们将使用`deno-mongo`包进行此操作([`github.com/manyuanrong/deno_mongo`](https://github.com/manyuanrong/deno_mongo))。 + +重要提示 + +Deno 的 MongoDB 驱动使用了 Deno 插件 API,目前该 API 仍处于不稳定状态。这意味着我们必须在运行应用程序时使用`--unstable`标志。由于目前它正在使用尚未被认为是稳定的 API,因此现在还不能在生产环境中使用。 + +让我们看看文档中的示例,其中建立了一个 MongoDB 数据库的连接: + +```js +import { MongoClient } from +  "https://deno.land/x/mongo@v0.13.0/mod.ts"; +const client = new MongoClient(); +client.connectWithUri("mongodb://localhost:27017"); +const db = client.database("test"); +const users = db.collection("users"); +``` + +这里,我们可以看到我们将需要创建一个 MongoDB 客户端,并使用包含主机(可能包含主机的用户名和密码)的连接字符串连接到数据库。 + +之后,我们需要让客户端访问一个特定的数据库(在这个例子中是`test`)。只有这样,我们才能有一个让我们可以与集合(在这个例子中是`users`)交互的处理程序。 + +首先,让我们将`deno-mongo`添加到我们的依赖列表中: + +1. 前往你的`src/deps.ts`文件,并在那里添加`MongoClient`的导出: + + ```js + export { MongoClient } from +   "https://deno.land/x/mongo@v0.13.0/mod.ts"; + ``` + +1. 现在,确保你运行`cache`命令来安装模块。由于我们要安装的插件在安装时也需要使用不稳定 API,因此我们必须使用`--unstable`标志来运行它: + + ```js + $ deno cache --lock=lock.json --lock-write --unstable src/deps.ts + ``` + +有了这些,我们已经用我们刚刚安装的包更新了`deps.ts`文件! + +让我们继续实际使用这个包来开发我们的仓库。 + +## 开发 MongoDB 仓库 + +从我们从文档中获得的示例中,我们学会了如何连接到数据库并创建我们想要的用户集合的处理程序。我们知道我们的仓库需要访问这个处理程序,以便它可以与集合交互。 + +再次,我们可以在仓库内部直接创建 MongoDB 客户端,但这将使我们无法在没有尝试连接到 MongoDB 的情况下测试该仓库。 + +由于我们尽可能多地希望将依赖项注入到模块中,因此我们将 MongoDB 客户端通过其构造函数传递给我们的仓库,这在代码的其他部分与我们所做的工作非常相似。 + +让我们回到我们的 MongoDB 仓库,并按照这些步骤进行操作: + +1. 在 MongoDB 仓库内部创建`constructor`方法。 + + 确保它接收一个具有名为`storage`的`Database`类型的属性的对象,该类型是由`deno-mongo`包导出的: + + ```js + import { User, UserRepository } from "../types.ts"; + collection method on it, to get access to the users' collection. Once we've done that, we must set it to our storage class property. Both the method and the type require a generic to be passed in. This should represent the type of object present in that collection. In our case, it is the User type. + ``` + +1. 现在,我们必须进入`src/deps.ts`文件,并从`deno-mongo`中导出`Database`和`Collection`类型: + + ```js + export { MongoClient, Collection, Database } from +   "https://deno.land/x/mongo@v0.13.0/mod.ts"; + ``` + +现在,只需开发满足`UserRepository`接口的方法。 + +这些方法将非常类似于我们为内存数据库开发的方法,区别在于我们现在在与 MongoDB 集合交互,而不是我们之前使用的 JavaScript Map。 + +现在,我们只需要实现一些方法,这些方法将创建用户、验证用户是否存在,并通过用户名获取用户。这些方法在插件文档中可用,并且非常类似于 MongoDB 的本地 API。 + +最终类的样子就是这样: + +```js +import { CreateUser, User, UserRepository } from + "../types.ts"; +import { Collection, Database } from "../../deps.ts"; +export class Repository implements UserRepository { +  storage: Collection +  constructor({ storage }: RepositoryDependencies) { +    this.storage = storage.collection("users"); +  } +  async create(user: CreateUser) { +    const userWithCreatedAt = { ...user, createdAt: new Date() } +    this.storage.insertOne({ ...user }) +    return userWithCreatedAt; +  } +  async exists(username: string) { +    return Boolean(await this.storage.count({ username })); +  } +  async getByUsername(username: string) { +    const user = await this.storage.findOne({ username }); +    if (!user) { +      throw new Error("User not found"); +    } +    return user; +  } +}   +``` + +我们突出显示了使用`deno-mongo`插件访问数据库的方法。注意逻辑与我们之前做的非常相似。我们在`create`方法中添加了创建日期,然后调用 mongo 的`create`方法。在`exists`方法中,我们调用`count`方法,并将其转换为`boolean`。对于`getByUsername`方法,我们使用 mongo 库的`findOne`方法,发送用户名。 + +如果您有关于如何使用这些 API 的问题,请查看 deno-mongo 的文档:[`github.com/manyuanrong/deno_mongo`](https://github.com/manyuanrong/deno_mongo)。 + +## 将应用程序连接到 MongoDB + +现在,为了暴露我们创建的 MongoDB 仓库,我们需要进入`src/users/index.ts`并将其作为`Repository`暴露(删除高亮显示的行): + +```js +export { Repository } from "./repository/mongoDb.ts"; +export { Repository } from "./repository/inMemory.ts"; +``` + +现在,我们的编辑器和 typescript 编译器应该会抱怨我们目前在`src/index.ts`上实例化`UserRepository`时没有发送正确的依赖关系,这是正确的。所以,让我们去那里解决这个问题。 + +在将数据库客户端发送到`UserRepository`之前,它需要被实例化。通过查看`deno-mongo`的文档,我们可以看到以下示例: + +```js +const client = new MongoClient(); +client.connectWithUri("mongodb://localhost:27017"); +``` + +我们没有连接到 localhost,所以稍后需要更改连接 URI。 + +让我们按照文档的示例编写连接到 MongoDB 实例的代码。按照以下步骤操作: + +1. 在将`MongoClient`的导出添加到`src/deps.ts`文件后,在`src/index.ts`中导入它: + + ```js + import { MongoClient } from "./deps.ts"; + ``` + +1. 然后,调用`connectWithUri`: + + ```js + const client = new MongoClient(); + client.connectWithUri("mongodb://localhost:27017"); + ``` + +1. 之后,通过在客户端上调用`database`方法获取数据库处理程序: + + ```js + const db = client.database("getting-started-with-deno"); + ``` + +至此,我们应该已经具备了连接到 MongoDB 的所有条件。唯一缺少的是将数据库处理程序发送到`UserRepository`的代码。所以,让我们添加这个: + +```js +const client = new MongoClient(); +client.connectWithUri("mongodb://localhost:27017"); +const db = client.database("getting-started-with-deno"); +... +const userRepository = new UserRepository({ storage: db }); +``` + +不再显示警告,现在我们应该能够运行我们的应用程序了! + +然而,我们仍然没有数据库可以连接。我们稍后再来看这个问题。 + +## 连接到 MongoDB 集群 + +现在,我们需要连接到一个真实的 MongoDB 实例。在这里,我们将使用一个名为 Atlas 的服务。Atlas 是 MongoDB 提供的一个云 MongoDB 数据库服务。他们的免费层非常慷慨,非常适合我们的应用程序。在那里创建一个账户。完成后,我们可以创建一个 MongoDB 集群。 + +重要提示 + +如果您有其他任何 MongoDB 实例,无论是本地的还是远程的,都可以跳过下一段,直接将数据库 URI 插入代码中。 + +以下链接包含创建集群所需的所有说明:[`docs.atlas.mongodb.com/tutorial/create-new-cluster/`](https://docs.atlas.mongodb.com/tutorial/create-new-cluster/)。 + +一旦创建了集群,我们还需要创建一个可以访问它的用户。前往[`docs.atlas.mongodb.com/tutorial/connect-to-your-cluster/index.html#connect-to-your-atlas-cluster`](https://docs.atlas.mongodb.com/tutorial/connect-to-your-cluster/index.html#connect-to-your-atlas-cluster)了解如何获取连接字符串。 + +它应该看起来像以下样子: + +```js +mongodb+srv://:@clustername.mongodb.net/ +  test?retryWrites=true&w=majority&useNewUrlParser= +    true&useUnifiedTopology=true +``` + +现在我们已经有了连接字符串,我们只需要将其传递给我们在`src/index.ts`中 previously created code: + +```js +const client = new MongoClient(); +client.connectWithUri("mongodb+srv://: +  @clustername.mongodb.net/test?retryWrites=true&w= +    majority&useNewUrlParser=true&useUnifiedTopology=true"); +const db = client.database("getting-started-with-deno"); +``` + +这就应该是我们所需要的,让我们的应用程序运行起来。让我们开始吧! + +请注意,由于我们使用插件 API 连接到 MongoDB,而且它仍然不稳定,因此需要以下权限,并加上`--unstable`标志: + +```js +$ deno run --allow-net --allow-write --allow-read --allow-plugin --allow-env --unstable src/index.ts +Application running at http://localhost:8080 +``` + +现在,为了测试我们的`UserRepository`是否运行正常并且与数据库连接,让我们尝试注册并登录看看是否可行: + +1. 向`/api/users/register`发送一个`POST`请求来注册我们的用户: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw1" }' -H 'Content-Type: application/json' http://localhost:8080/api/users/register + {"user":{"username":"asantos00","createdAt":"2020-11-01T23:21:58.442Z"}} + ``` + +1. 现在,为了确保我们正在连接到永久存储,我们可以停止应用程序,然后再次运行它,在尝试登录之前: + + ```js + $ deno run --allow-net --allow-write --allow-read --allow-plugin --allow-env --unstable src/index.ts + Application running at http://localhost:8080 + ``` + +1. 现在,让我们用我们刚才创建的同一个用户登录: + + ```js + $ curl -X POST -d '{"username": "asantos00", "password": "testpw1" }' -H 'Content-Type: application/json' http://localhost:8080/api/login + {"user":{"username":"asantos006"},"token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtdXNl dW1zIiwiZXhwIjoxNjA0MjczMDQ1LCJ1c2VyIjoiYXNhbnRvczAwNi J9.elY48It-DHse5sSszCAWuE2PzNkKiPsMIvif4v5klY1URq0togK 84wsbSskGAfe5UQsJScr4_0yxqnrxEG8viw"} + ``` + +我们已经有了我们的响应!我们成功地将之前连接到内存数据库的应用程序连接到了一个真实的 MongoDB 数据库。如果你使用了 MongoDB,你可以在 Atlas 界面的**集合**菜单中查看那里创建的用户。 + +你是否注意到了,我们无需修改任何业务或网络逻辑,仅仅是改变了持久化机制?这证明了我们最初创建的层和抽象现在正在发挥作用,通过允许应用程序不同部分之间的解耦。 + +有了这些,我们已经完成了本章的内容,并将我们的用户迁移到了一个真实的数据库。我们也可以为其他模块做同样的事情,但这基本上是重复的,并且不会为你的学习体验增加太多。我想挑战你编写其他模块的逻辑,使其能够连接到 MongoDB。 + +如果你想要跳过这个过程,但又好奇它会是怎样的,那就去看看这本书的 GitHub 仓库吧。 + +# 总结 + +本章基本上完成了我们应用程序在逻辑方面的构建。我们稍后会在第八章 *测试 - 单元和集成* 中添加测试和我们缺失的一个功能——评分博物馆的能力。然而,这部分已经基本完成。在当前状态下,我们有一个应用程序,它的领域被划分为可以独立使用且相互之间不依赖的模块。我们相信我们已经实现了一些既易于在代码中导航又可扩展的东西。 + +这结束了不断重构和精炼架构的过程,管理依赖项,调整逻辑以确保代码尽可能解耦,并且尽可能容易地在将来更改。在做这一切的同时,我们设法创建了一个具有几个功能的应用程序,同时试图绕过行业标准。 + +我们在这一章开始时学习了中间件函数,这是我们之前使用过,尽管我们还没有学习过它们的东西。我们理解了它们是如何工作的,以及如何利用它们在应用程序和路线中添加逻辑。为了更具体一点,我们进入了具体的示例,并以在应用程序中实现几个为例结束。在这里,我们添加了诸如基本日志记录和请求计时等常见功能。 + +然后,我们继续完成我们的认证之旅。在上一章中添加了用户和注册功能后,我们首先实现了认证功能。我们依赖外部包来管理我们的 JWT 令牌,我们稍后用于我们的授权机制。在向用户提供令牌后,我们必须确保令牌有效,然后才允许用户访问应用程序。我们在博物馆路线上添加了一个认证路线,确保它只能被认证用户访问。再次,使用中间件来检查令牌的有效性并在错误情况下回答请求。 + +我们通过向应用程序添加一个新功能来结束这一章:连接到真实数据库。在我们这样做之前,我们所有的应用程序模块都依赖于内存中的数据库。在这里,我们将其中一个模块,`users`,移动到了 MongoDB 实例。为了做到这一点,我们利用了之前创建的层次,将业务逻辑与我们的持久化和交付机制分开。在这里,我们创建并实现了我们所说的 MongoDB 存储库,确保应用程序运行顺畅,但具有真实的持久化机制。我们在这里使用了 MongoDB Atlas 作为示例。 + +在下一章中,我们将向我们的网络应用程序添加一些新功能,具体包括管理代码外的秘密和配置的能力,这是一项众所周知的好实践。我们还将探讨 Deno 在浏览器中运行代码的可能性,等等。下一章将结束本书这部分内容,即构建应用程序的功能。让我们开始吧! diff --git a/docs/deno-web-dev/deno-web-dev_05.md b/docs/deno-web-dev/deno-web-dev_05.md new file mode 100644 index 0000000..81842b7 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_05.md @@ -0,0 +1,845 @@ +# 第五章:*第七章*:HTTPS,提取配置和 Deno 在浏览器中 + +在上一章中,我们基本上完成了应用程序的功能。我们添加了授权和持久性,最终得到了一个连接到 MongoDB 实例的应用程序。在本章中,我们将关注一些在生产应用程序中常见的最佳实践:基本的安全实践和处理配置。 + +首先,我们将为我们的**应用程序编程接口**(**API**)添加一些基本的安全特性,从**跨源资源共享**(**CORS**)保护开始,以使基于它们的来源对请求进行过滤。然后,我们将学习如何在我们的应用程序中启用**安全超文本传输协议**(**HTTPS**),以便它支持加密连接。这将允许用户使用安全的连接对 API 进行请求。 + +直到现在,我们使用了一些密钥值,但我们没有关注它们在代码中的存在。在本章中,我们将提取配置和密钥,使它们不必存在于代码库中。然后,我们将学习如何安全地存储和注入它们。这样,我们可以确保这些值保持秘密,并且不出现在代码中。通过这样做,我们还将使不同的部署具有不同的配置成为可能。 + +继续前进,我们将探索由一个特定的 Deno 功能启用的能力:在浏览器中编译和运行代码的能力。通过使用 Deno 对 ECMAScript 6 的兼容性(受现代浏览器支持),我们将在 API 和前端之间共享代码,启用一个全新的可能性世界。 + +利用这个特定的功能,我们将探索一个特定的场景:为 API 构建一个 JavaScript 客户端。这个客户端将使用在服务器上运行的相同类型和代码部分构建,并探索由此带来的好处。 + +本章结束了本书的*构建应用程序*部分,我们一步一步地构建了一个应用程序,并用逐步增加的方法添加了一些常见应用程序特性。在学习过程中,我们还确保这个应用程序尽可能接近现实,这是一本介绍性书籍。这使我们能够在创建功能应用程序的同时学习 Deno,它的许多 API 以及一些社区包。 + +到本章结束时,您将熟悉以下主题: + ++ 启用 CORS 和 HTTPS + ++ 提取配置和密钥 + ++ 在浏览器中运行 Deno 代码 + +# 技术要求 + +本章所需的所有代码文件都可以在以下 GitHub 链接中找到: + +[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections) + +# 启用 CORS 和 HTTPS + +跨源资源共享(CORS)和 HTTPS 支持是任何运行中的生产应用程序考虑的两个关键因素。本节将解释我们如何将它们添加到我们正在构建的应用程序中。 + +还有许多其他安全实践可以添加到任何 API 中。由于这些不是 Deno 特定内容,并且值得单独一本书,我们决定专注于这两个要素。 + +我们将首先学习关于 CORS 的知识以及我们如何利用我们所知道的 `oak` 和中间件函数特性来实现它。然后,我们将学习如何使用自签名证书,并使我们的 API 处理安全的 HTTP 连接。 + +让我们开始,从 CORS 开始。 + +## 启用 CORS + +如果你不熟悉 CORS,它是一种机制,使服务器能够指示浏览器允许从哪些来源加载资源。当应用程序在与 API 相同的域上运行时,CORS 甚至是不必要的,因为名称直接使其显而易见。 + +我将从**Mozilla Developer Network**(**MDN**)提供以下引用,解释 CORS: + +"跨源资源共享(CORS)是一个基于 HTTP 头的机制,允许服务器指示浏览器应该允许从哪些其他来源(域、协议或端口)加载资源。CORS 还依赖于一种机制,浏览器向托管跨源资源的服务器发送“预检”请求,以检查服务器将允许实际请求。在预检中,浏览器发送头指示将使用实际请求中的 HTTP 方法和头。" + +为了给你一个更具体的例子,想象你有一个运行在 `the-best-deno-api.com` 的 API 并且你想处理来自 `the-best-deno-client.com` 的请求。在这里,你希望你的服务器为 `the-best-deno-client.com` 域启用 CORS。 + +如果你没有启用它,浏览器将向你的 API 发送一个预检请求(使用 `OPTIONS` 方法),对这个请求的响应将不会有 `Access-Control-Allow-Origin: the-best-deno-client.com` 头,导致请求失败并阻止浏览器进一步请求。 + +我们将学习如何在我们的应用程序中启用此机制,允许在以下示例中从 `http://localhost:3000` 发起请求。 + +由于我们的应用程序正在使用 `oak` 框架,我们将学习如何使用这个框架来实现。然而,这与其他任何 HTTP 框架非常相似。我们基本上想要添加一个处理请求并将其来源与允许的域列表进行比较的中间件函数。 + +我们将使用一个名为 `cors` 的社区包([`deno.land/x/cors@v1.2.1`](https://deno.land/x/cors@v1.2.1)),但实现非常简单。如果你好奇它做什么,可以查看 [`deno.land/x/cors@v1.2.1/oakCors.ts`](https://deno.land/x/cors@v1.2.1/oakCors.ts),因为代码非常直接。 + +重要提示 + +我们将使用前一章中创建的代码来启动这个实现。这可以在[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/sections/4-connecting-to-mongodb/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/sections/4-connecting-to-mongodb/museums-api)找到。您还可以查看本节完成后的代码: + +[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections/3-deno-on-the-browser/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter07/sections/3-deno-on-the-browser/museums-api) + +在这里,我们将`cors`包添加到我们的应用程序中,同时加上我们自己的允许域名列表。最终目标是使我们能够从可信网站对这项 API 执行请求。 + +让我们这样做。按照以下步骤进行: + +1. 通过更新`deps`文件安装`cors`模块(参考第三章,《运行时和标准库》,了解如何进行此操作)。代码如下所示: + + ```js + export { oakCors } from +   "https://deno.land/x/cors@v1.2.1/oakCors.ts"; + ``` + +1. 接下来,运行`cache`命令以更新`lock`文件,如下所示: + + ```js + $ deno cache --lock=lock.json --lock-write --unstable src/deps.ts + ``` + +1. 在`src/web/index.ts`中导入`oakCors`,并在注册路由之前在应用程序中注册它,如下所示: + + ```js + import { Algorithm, oakCors } from "../deps.ts" + … + oakCors middleware creator function, by sending it an array of allowed origins—in this case, http://localhost:3000. This will make the API answer to the OPTIONS request with an Access-Control-Allow-Origin: http://localhost:3000 header, which will signal to the browser that if the website making requests is running on http://localhost:3000, it should allow further requests.This will work just fine. However, having this *hardcoded* domain here seems a little bit strange. We've been injecting all the similar configuration to the application. Remember what we did with the `port` configuration? Let's do the same for the allowed domains. + ``` + +1. 将`createServer`函数参数更改为接收名为`allowedOrigins`的数组`string` inside `configuration`,稍后将其传递给`oakCors`中间件创建函数。这段代码如下所示: + + ```js + interface CreateServerDependencies { +   configuration: { +     port: number, +     authorization: { +       key: string, +       algorithm: Algorithm +     }, +     oakCors middleware creator. + ``` + +1. 不过还有一件事缺失——我们需要从`src/index.ts`发送这个`allowedOrigins`数组。让我们这样做,如下所示: + + ```js + createServer({ +   configuration: { +     port: 8080, +     authorization: { +       key: authConfiguration.key, +       algorithm: authConfiguration.algorithm +     }, +     http://localhost:3000. + ``` + +1. 让我们来测试一下,首先运行 API,如下所示: + + ```js + $ deno run --allow-net --unstable --allow-env --allow-read --allow-write --allow-plugin src/index.ts + Application running at http://localhost:8080 + ``` + +1. 要测试它,请在根目录(`museums-api`)中创建一个名为`index.html`的 HTML 文件,其中包含执行`POST`请求到`http://localhost:8080/api/users/register`的脚本。这段代码如下所示: + + ```js + + +    +      +      +     Test CORS +    +    +     
    +      +    +    + + ``` + +1. 创建一个`script`标签,并直接从 API URL 导入脚本,如下所示: + + ```js + + ``` + +1. 现在,只是使用我们构建的客户端的问题。我们将使用它登录(使用你之前创建的用户)并获取博物馆列表。代码如下所示: + + ```js + async function main() { +   const client = getClient +     ({ baseURL: "https://localhost:8080" }); +   const username = window.prompt("Username"); +   const password = window.prompt("Password"); +   await client.login({ username, password }); +   const { museums } = await client.getMuseums(); +   museums.forEach((museum) => { +     const node = document.createElement("div"); +     node.innerHTML = `${museum.name} – +       ${museum.description}`; +     document.body.appendChild(node); +   }); + } + ``` + + 我们将使用`window.prompt`在用户访问页面时获取用户名和密码,然后用这些数据登录并获取博物馆信息。之后,我们只需将其添加到**文档对象模型**(**DOM**)中,创建一个博物馆列表。 + +1. 让我们再次启动应用程序,如下所示: + + ```js + $ MONGODB_USERNAME=deno-api MONGODB_PASSWORD=your-password deno run --allow-net --allow-env --unstable --allow-read --allow-plugin --allow-write src/index.ts + Application running at https://localhost:8080 + ``` + +1. 然后,这次在前端应用程序中添加`–cert`和`--key`标志,以及各自文件的路径,以使用 HTTPS 运行文件服务器,如下面的代码段所示: + + ```js + $ deno run --allow-net --allow-read https://deno.land/std@0.83.0/http/file_server.ts -p 3000 --host localhost --key key.pem --cert certificate.pem + HTTPS server listening on https://localhost:3000/ + ``` + +1. 我们现在可以访问 https://localhost:3000/index-with-client.html 这个网页,输入用户名和密码,然后在屏幕上获取博物馆列表,如下面的屏幕截图所示: + +![Figure 7.3 – Web page with a JavaScript client getting data from the API](img/Figure_7.3_B16380.jpg) + +](img/Figure_7.3_B16380.jpg) + +Figure 7.3 – Web page with a JavaScript client getting data from the API + +在上一步登录时,你需要使用之前在应用程序上注册的用户。如果没有,你可以使用以下命令创建: + +```js +$ curl -X POST -d'{"username": "your-username", "password": "your-password" }' -H 'Content-Type: application/json' https://localhost:8080/api/users/register +``` + +确保用你想要的用户名替换`your-username`,用你想要密码替换`your-password`。 + +这样一来,我们就完成了关于在浏览器上使用 Deno 的部分! + +我们刚刚所做的可以进一步探索,解锁大量的潜力;这只是适用于我们用例的快速示例。这种实践使得任何浏览器应用程序更容易与我们现在编写的应用程序集成。客户无需处理 HTTP 逻辑,只需调用方法并接收其响应即可。正如我们所看到的,这个客户端也可以自动处理认证和 cookie 等主题。 + +本节探讨了 Deno 启用的一项功能:为浏览器编译代码。 + +我们在应用程序的上下文中应用了这个功能,通过创建一个抽象用户与 API 之间的 HTTP 客户端。这个功能可以用来做很多事情,目前正被用于在 Deno 内部编写前端 JavaScript 代码。 + +正如我们在第二章中解释的,*工具链*,当我们为浏览器编写代码时,需要考虑的唯一事情就是不要使用`Deno`命名空间中的函数。遵循这些限制,我们可以非常容易地在 Deno 中编写代码,利用其所有优势,并将其编译为 JavaScript 以供分发。 + +这只是对一个非常有望的功能的介绍。这个功能,就像 Deno 一样,仍处于初期阶段,社区将会发现它的许多伟大用途。既然你现在也知道了它,我相信你也会想出很多好主意。 + +# 总结 + +这是一个我们重点关注将应用程序带入可部署状态的实践章节。我们首先探索了基本的安全实践,为 API 添加了 CORS 机制和 HTTPS。这两个功能几乎是任何应用程序的标准,它们比我们已有的功能有了很大的安全提升。 + +此外,考虑到应用程序的部署,我们还从代码库中抽象出了配置和秘密。我们首先创建了一个处理它的抽象,这样配置就不会分散,模块只需接收它们的配置值,而无需了解它们是如何加载的。然后,我们继续在我们的当前代码库中使用这些值,这揭示了自己实际上相当容易。这一步骤将任何配置值从代码中移除,并将它们移动到配置文件中。 + +一旦完成了配置,我们就使用了同样的抽象来处理应用程序中的秘密。我们实现了一个功能,它从环境变量中加载值并将它们添加到应用程序配置中。然后,我们在需要的地方使用这些秘密值,比如 MongoDB 凭据和令牌签名密钥。 + +我们以探索 Deno 自第一天起就提供的可能性结束了这个章节:为浏览器捆绑代码。将这个功能应用到我们的应用程序上下文中,我们决定编写一个 JavaScript HTTP 客户端来连接到 API。 + +这一步骤探讨了 API 和客户端之间共享代码的潜力,解锁了一个充满可能性的世界。通过这个步骤,我们探讨了如何使用 Deno 的捆绑功能在运行时编译一个文件并将其提供给用户。这个功能的优点也将在下一章中探讨,我们将为我们的应用程序编写单元和集成测试。其中一些测试将使用在这里创建的 HTTP 客户端,利用这种实践的一个巨大优势:客户端和服务器在同一个代码库中。 + +在下一章,我们将深入探讨测试。我们将为书中剩余部分编写的逻辑编写测试,从业务逻辑开始。我们将学习如何通过添加测试来提高代码库的可靠性,以及我们创建的层次结构和架构在编写它们时的关键性。我们将编写的测试从单元测试到集成测试,并探索它们适用的用例。我们将看到测试在编写新功能和维护旧功能时所增加的价值。在这个过程中,我们将了解一些新的 Deno API。 + +代码没有编写测试就不算完成,因此我们将编写测试来结束我们的 API。 + +让我们开始吧! diff --git a/docs/deno-web-dev/deno-web-dev_06.md b/docs/deno-web-dev/deno-web-dev_06.md new file mode 100644 index 0000000..7c0ec06 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_06.md @@ -0,0 +1,11 @@ +# 第三部分:测试和部署 + +在本节中,您将创建有意义的集成和单元测试,使应用程序能够增长,并将学习如何容器化并在云端部署 Deno 应用程序。 + +本节包含以下章节: + ++ 第八章,[测试 – 单元和集成](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=825fa87f-4618-2790-1a60-5f32422b4c47) + ++ 第九章,[部署 Deno 应用程序](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=98b91ae7-2855-39f3-f6b4-5f32426d1b76) + ++ 第十章,[接下来是什么?](https://epic.packtpub.com/index.php?module=oss_Chapters&action=DetailView&record=6128cca6-e773-f0c6-9ca0-5f3242cf7f1e) diff --git a/docs/deno-web-dev/deno-web-dev_07.md b/docs/deno-web-dev/deno-web-dev_07.md new file mode 100644 index 0000000..4117503 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_07.md @@ -0,0 +1,768 @@ +# 第八章:测试 - 单元和集成 + +代码在相应测试编写之前是不会创建的。既然您在读这一章,我将假设我们可以同意这个观点。然而,您可能想知道,为什么我们一个测试都没有写呢?合情合理。 + +我们选择不这样做,因为我们相信这会使得内容更难吸收。由于我们想在构建应用程序的同时让您专注于学习 Deno,所以我们决定不这样做。第二个原因是,我们真的想要一个完整的章节专注于测试,即这一章。 + +测试是软件生命周期中非常重要的一部分。它可以用来节省时间,明确需求,或者只是因为你想在以后重新编写和重构时感到自信。无论动机如何,有一点是确定的:您将编写测试。我也坚信测试在软件设计中扮演着重要角色。易于测试的代码很可能易于维护。 + +由于我们非常重视测试的重要性,所以在不学习测试的情况下,我们无法认为这是一本关于 Deno 的完整指南。 + +在本章中,我们将编写不同类型的测试。我们将从单元测试开始,这对于开发者和维护周期来说是非常有价值的测试。然后,我们将进行集成测试,在其中我们运行应用程序并对其执行几个请求。最后,我们将使用在前一章中编写的客户端。我们将在这个过程中,逐步向应用程序中添加测试,确保我们之前编写的代码正常工作。 + +本章还将展示我们在这本书一开始所做的某些架构决策将得到回报。这将是使用 Deno 及其工具链编写简单模拟和清晰、专注测试的介绍。 + +在本章中,我们将涵盖以下主题: + ++ 在 Deno 中编写您的第一个测试 + ++ 编写集成测试 + ++ 测试网络服务器 + ++ 为应用程序创建集成测试 + ++ 同时测试 API 和客户端 + ++ 对应用程序的部分进行基准测试 + +让我们开始吧! + +## 技术要求 + +本章中将使用的代码可以在 [`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections) 找到。 + +# 在 Deno 中编写您的第一个测试 + +在我们开始编写测试之前,记住一些事情是很重要的。其中最重要的原因是,我们为什么要进行测试? + +这个问题可能会有多个答案,但大多数都会指向保证代码在运行。您也可能说,您使用它们以便在重构时具有灵活性,或者您重视在实施时拥有短暂的反馈周期——我们可以同意这两点。由于我们在实现这些功能之前没有编写测试,所以后者对我们来说并不适用。 + +我们将把这些目标牢记在整个章节中。在本节中,我们将编写我们的第一个测试。我们将使用在前几章中编写的应用程序并向其添加测试。我们将编写两种类型的测试:集成测试和单元测试。 + +集成测试将测试应用程序不同组件之间的交互。单元测试测试隔离的层次。如果我们把它看作是一个光谱,那么单元测试更接近代码,而集成测试更接近用户。在用户端的尽头,还有端到端测试。这些测试通过模拟用户行为来测试应用程序,我们将在本章不涉及这些内容。 + +在实际应用程序开发中使用的模式,如依赖注入和控制反转,在测试时非常有用。由于我们通过注入所有其依赖项来开发代码,现在,在测试中只需模拟这些依赖项即可。记住:易于测试的代码通常也易于维护。 + +我们首先要做的是为业务逻辑编写测试。目前,由于我们的 API 相当简单,所以它没有太多的业务逻辑。大部分都生活在`UserController`中,因为`MuseumController`非常简单。我们从后者开始。 + +要在 Deno 中编写测试,我们需要使用以下内容: + ++ Deno 测试运行器(在第二章,《工具链》中介绍) + ++ Deno 命名空间中的`test`方法([`doc.deno.land/builtin/stable#Deno.test`](https://doc.deno.land/builtin/stable#Deno.test)) + ++ Deno 标准库中的断言方法(`doc.deno.land/https/deno.land/std@0.83.0/testing/asserts.ts`) + +这些都是 Deno 的组成部分,由核心团队分发和维护。社区中还有许多其他可以在测试中使用的库。我们将使用 Deno 提供的默认设置,因为它工作得很好,并且允许我们编写清晰易读的测试。 + +让我们来学习我们如何定义一个测试! + +## 定义测试 + +Deno 提供了一个 API 来定义测试。这个 API,`Deno.test`([`doc.deno.land/builtin/stable#Deno.test`](https://doc.deno.land/builtin/stable#Deno.test)),提供了两种不同的方法来定义一个测试。 + +其中一个是我们在第二章,《工具链》中展示的,由两部分组成,即调用它需要两个参数;也就是说,测试名称和测试函数。这可以在以下示例中看到: + +```js +Deno.test("my first test", () => {}) +``` + +我们还可以通过调用相同的 API 来实现,这次将对象作为参数发送。 你可以发送函数和测试名称,还可以发送其他一些选项到这个对象,如你所见在以下示例中: + +```js +Deno.test({ +  name: "my-second-test", +  fn: () => {}, +  only: false, +  sanitizeOps: true, +  sanitizeResources: true, +}); +``` + +这些标志行为在文档中解释得非常清楚([`doc.deno.land/builtin/stable#Deno.test`](https://doc.deno.land/builtin/stable#Deno.test)),但这里有一个总结供您参考: + ++ `only`:只运行设置为`true`的测试,使测试套件失败,因此这应仅作为临时措施使用。 + ++ `sanitizeOps`:如果 Deno 核心启动的所有操作都不成功,则使测试失败。此标志默认为`true`。 + ++ `sanitizeResources`:如果测试完成后仍有资源运行(这可能表明内存泄漏),则使测试失败。这个标志确保测试必须有一个清理阶段,在此阶段停止资源,默认值为`true`。 + +现在我们已经了解了 API,让我们去编写我们的第一个测试——针对`MuseumController`函数的单元测试。 + +## 针对`MuseumController`的单元测试 + +在本节中,我们将编写一个非常简单的测试,它将涵盖我们在`MuseumController`中编写的所有功能,不多也不少。 + +它列出了应用程序中的所有博物馆,尽管目前它还没有做太多工作,只是作为`MuseumRepository`的代理运行。我们可以通过以下步骤创建这个简单功能的测试文件和逻辑: + +1. 创建`src/museums/controller.test.ts`文件。 + + 测试运行程序将自动将文件名中包含`.test`的文件视为测试文件,如第二章 *工具链*中所解释的其他约定。 + +1. 使用`Deno.test`([`doc.deno.land/builtin/stable#Deno.test`](https://doc.deno.land/builtin/stable#Deno.test))声明第一个测试: + + ```js + Deno.test("it lists all the museums", async () => {}); + ``` + +1. 现在,将标准库中的断言方法导出到一个名为`t`的命名空间中,这样我们就可以在测试文件中使用它们,通过在`src/deps.ts`中添加以下内容: + + ```js + export * as t from +   "https://deno.land/std@0.83.0/testing/asserts.ts"; + ``` + + 如果您想了解标准库中可用的断言方法,请查看`doc.deno.land/https/deno.land/std@0.83.0/testing/asserts.ts`。 + +1. 您现在可以使用标准库中的断言方法来编写一个实例化`MuseumController`并调用`getAll`方法的测试: + + ```js + import { t } from "../deps.ts"; + import { Controller } from "./controller.ts"; + Deno.test("it lists all the museums", async () => { +   const controller = new Controller({ + MuseumController and sending in a mocked version of museumRepository, which returns a static array. This is how we're sure we're testing only the logic inside MuseumController, and nothing more. Closer to the end of the snippet, we're making sure the getAll method's result is returning the museum being returned by the mocked repository. We are doing this by using the assertion methods we exported from the dependencies file. + ``` + +1. 让我们运行测试并验证它是否正常工作: + + ```js + $ deno test --unstable --allow-plugin --allow-env --allow-read –-allow-write --allow-net src/museums + running 1 tests + test it lists all the museums ... ok (1ms) + test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (1ms) + ``` + +我们的第一个测试成功了! + +注意测试输出如何列出测试的名称、状态和运行时间,以及测试运行的摘要。 + +`MuseumController`内部的逻辑相当简单,因此这个测试也非常简单。然而,它隔离了控制器的行为,使我们能够编写非常专注的测试。如果您对为应用程序的其他部分创建单元测试感兴趣,它们可以在本书的存储库中找到([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)). + +在接下来的几节中,我们将编写更有趣的测试。这些测试将教会我们如何检查应用程序不同模块之间的集成。 + +# 编写集成测试 + +我们之前创建的第一个单元测试依赖于存储库的模拟实例来确保我们的控制器正常工作。这个测试在检测`MuseumController`中的错误方面增加了很大价值,但它在理解控制器是否与存储库良好配合方面并不值太多。 + +集成测试的目的是测试多个组件如何相互集成。 + +在本节中,我们将编写一个集成测试,用于测试`MuseumController`和`MuseumRepository`。这些测试将 closely mimic 应用程序运行时发生的事情,并帮助我们 later in terms of detecting any problems between these two classes. + +让我们开始吧: + +1. 在`src/museums`中为此模块的集成测试创建一个文件,称为`museums.test.ts`,并在那里添加第一个测试用例。 + + 它应该测试是否可以使用存储库的实例而不是模拟实例获取所有博物馆: + + ```js + Deno.test("it is able to get all the museums from +   storage", async () => {}); + ``` + +1. 我们将从实例化存储库并在此添加几个测试用例开始: + + ```js + import { t } from "../deps.ts"; + import { Controller, Repository } from "./index.ts"; + Deno.test("it is able to get all the museums from +   storage", async () => { +   const repository = new Repository(); +   repository.storage.set("0", { +     description: "museum with id 0", +     name: "my-museum", +     id: "0", +     location: { lat: "123", lng: "321" }, +   }); +   repository.storage.set("1", { +     description: "museum with id 1", +     name: "my-museum", +     id: "1", +     location: { lat: "123", lng: "321" }, +   }); + … + ``` + +1. 现在我们有了存储库,我们可以用它来实例化控制器: + + ```js + const controller = new Controller({ museumRepository: +   repository }); + ``` + +1. 现在我们可以编写我们的断言来确保一切都在正常工作: + + ```js + const allMuseums = await controller.getAll(); + t.assertEquals(allMuseums.length, 2); + t.assertEquals(allMuseums[0].name, "my-museum", "has +   name"); + t.assertEquals( +   allMuseums[0].description, +   "museum with id 0", +   "has description", + ); + t.assertEquals(allMuseums[0].id, "0", "has id"); + t.assertEquals(allMuseums[0].location.lat, "123", "has +   latitude"); + t.assertEquals(allMuseums[0].location.lng, "321", assertEquals, allowing us to get a proper message when this assertion fails. This is something that all assertion methods support. + ``` + +1. 让我们运行测试并检查结果: + + ```js + $ deno test --unstable --allow-plugin --allow-env --allow-read –-allow-write --allow-net src/museums + running 2 tests + test it lists all the museums ... ok (1ms) + test it is able to get all the museums from storage ... ok (1ms) + test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms) + ``` + +它通过了!这就是我们需要的存储库和控制器集成测试!这个测试在我们要更改`MuseumController`或`MuseumRepository`中的代码时非常有用,因为它确保它们一起工作得很好。 + +同样,如果您对应用程序其他部分的集成测试如何工作感到好奇,我们在这本书的仓库中提供了它们 ([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)). + +在第一部分,我们创建了一个单元测试,在这里,我们创建了一个集成测试,但我们还没有为应用程序的界面 - 网络部分编写任何测试,该部分使用 HTTP。这就是我们下一部分要做的。我们将学习如何可以独立地测试网络层中的逻辑,不使用任何其他模块。 + +# 测试网络服务器 + +到目前为止,我们已经学习了如何测试应用程序的不同部分。我们始于业务逻辑,它测试如何与与持久性交互的模块(存储库)集成,但网络层仍然没有测试。 + +确实,那些测试非常重要,但我们一致认为如果网络层失败,用户将无法访问到任何逻辑。 + +这就是我们这一节要做的。我们将启动我们的网络服务器,模拟它的依赖关系,并向它发送几个请求,以确保网络*单元*正在工作。 + +让我们先通过以下步骤创建网络模块的单元测试: + +1. 前往`src/web`并创建一个名为`web.test.ts`的文件。 + +1. 现在,为了测试网络服务器,我们需要回到`src/web/index.ts`中的`createServer`函数,并导出它创建的`Application`对象: + + ```js + const app = new Application(); + … + return { app }; + ``` + +1. 我们还希望能够在任何时候停止应用程序。我们还没有实现这个功能。 + + 如果我们查看 oak 的文档,我们会看到它非常完善([`github.com/oakserver/oak#closing-the-server`](https://github.com/oakserver/oak#closing-the-server))。 + + 为了取消由`listen`方法启动的应用程序,我们还需要返回`AbortController`。所以,让我们在`createServer`函数的最后这样做。 + + 如果你不知道`AbortController`是什么,我留下一个来自 Mozilla 开发者网络的链接([`developer.mozilla.org/en-US/docs/Web/API/AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)),它解释得非常清楚。简而言之,它允许我们取消一个进行中的承诺: + + ```js + const app = new Application(); + … + const controller = new AbortController(); + const { signal } = controller; + … + return { app, controller }; + ``` + + 注意我们是如何实例化`AbortController`的,这与文档中的示例类似,并在最后返回它,还有`app`变量。 + +1. 回到我们的测试中,让我们创建一个测试,检查服务器是否对`hello world`做出响应: + + ```js + Deno.test("it responds to hello world", async () => {}) + ``` + +1. 让我们使用我们之前创建的函数来获取服务器的实例运行;也就是说,`createServer`。记住,要调用这个函数,我们必须发送它的依赖关系。在这里,我们将不得不模拟它们: + + ```js + import { Controller as UserController } from +   "../users/index.ts"; + import { Controller as MuseumController } from +   "../museums/index.ts"; + import { createServer } from "./index.ts"; + … + const server = await createServer({ +   configuration: { +     allowedOrigins: [], +     authorization: { +       algorithm: "HS256", +       key: "abcd", +     }, +     certFile: "", +     keyFile: "", +     port: 9001, +     secure: false, +   }, + 9001 and with HTTPS disabled, along with some random algorithm and key.Note how we're using TypeScript's `as` keyword to pass mocked types into the `createServer` function without TypeScript warning us about the type. + ``` + +1. 我们现在可以创建一个测试,通过响应 hello world 请求来检查网络服务器是否正常工作: + + ```js + import { t } from "../deps.ts"; + … + const response = await fetch( +   "http://localhost:9001/", +   { +     method: "GET", +   }, + ).then((r) => r.text()); + t.assertEquals( +   response, +   "Hello World!", +   "responds with hello world", + ); + ``` + +1. 我们需要做的最后一件事是在测试运行后关闭服务器。Deno 默认会让测试失败如果我们不这样做(因为`sanitizeResources`默认是`true`),这可能会导致内存泄漏: + + ```js +   server.controller.abort(); + ``` + +这完成了我们针对网络层的第一个测试!这是一个单元测试,它测试了启动服务器的逻辑,并确保了 Hello World 正在工作。接下来,我们将为端点创建更完整的测试,包括业务逻辑。 + +在下一节中,我们将开始为登录和注册功能编写集成测试。这些测试比我们为博物馆模块编写的测试要复杂一些,因为它们将测试整个应用程序,包括其业务逻辑、持久性和网络逻辑。 + +# 为应用程序创建集成测试 + +我们迄今为止编写的三个测试单元测试了一个单一模块,以及两个不同模块之间的集成测试。然而,为了确信我们的代码正在工作,如果我们能够测试整个应用程序那就太好了。那就是我们接下来要做的。我们将为应用程序设置一个测试配置,并对它运行一些测试。 + +我们将首先调用与初始化 Web 服务器相同的函数,然后创建其所有依赖项(控制器、存储库等)的实例。我们将确保我们使用内存持久性等事物来做到这一点。这将确保我们的测试是可复制的,并且不需要复杂的清理阶段或真实数据库的连接,因为这会减慢测试速度。 + +我们首先创建一个测试文件,暂时包括应用程序的集成测试。随着应用程序的发展,可能在每个模块中创建一个测试文件夹是有意义的,但现在,这个解决方案完全可行。 + +我们将以非常接近实际生产中运行的设置实例化应用程序,并对它进行一些请求和断言: + +1. 创建一个与`src/index.ts`文件并列的`src/index.test.ts`文件。在其中创建一个测试声明,测试用户是否可以登录: + + ```js + Deno.test("it returns user and token when user logs +   in", async () => {}) + ``` + +1. 在开始编写这个测试之前,我们将创建一个助手函数,该函数将为测试设置 Web 服务器。它将包含所有实例化控制器和存储库的逻辑,以及将配置发送到应用程序的逻辑。它看起来像这样: + + ```js + import { CreateServerDependencies } from +   "./web/index.ts"; + … + function createTestServer(options?: CreateServerDependencies) { +   const museumRepository = new MuseumRepository(); +   const museumController = new MuseumController({ +     museumRepository }); +   const authConfiguration = { +     algorithm: "HS256" as Algorithm, +     key: "abcd", +     tokenExpirationInSeconds: 120, +   }; +   const userRepository = new UserRepository(); +   const userController = new UserController( +     { +       userRepository, +       authRepository: new AuthRepository({ +         configuration: authConfiguration, +       }), +     }, +   ); +   return createServer({ +     configuration: { +       allowedOrigins: [], +       authorization: { +         algorithm: "HS256", +         key: "abcd", +       }, +       certFile: "abcd", +       keyFile: "abcd", +       port: 9001, +       secure: false, +     }, +     museum: museumController, +     user: userController, +     ...options, +   }); + } + ``` + + 我们在这里做的工作与`src/index.ts`中的布线逻辑非常相似。唯一的区别是我们将显式导入内存存储库,而不是 MongoDB 存储库,如下面的代码块所示: + + ```js + import { +   Controller as MuseumController, +   InMemoryRepository as MuseumRepository, + } from "./museums/index.ts"; + import { +   Controller as UserController, +   InMemoryRepository as UserRepository, + } from "./users/index.ts"; + ``` + + 为了让我们能够访问`Museums`和`Users`模块的内存存储库,我们需要进入这些模块并将它们导出。 + + `src/users/index.ts`文件应该看起来像这样: + + ```js + export { Repository } from "./repository/mongoDb.ts"; + Repository but also exporting InMemoryRepository at the same time.Now that we have a way to create a test server instance, we can go back to writing our tests. + ``` + +1. 使用我们刚刚创建的助手函数`createTestServer`创建一个服务器实例,并使用`fetch`对 API 发起注册请求: + + ```js + Deno.test("it returns user and token when user logs +   in", async () => { +   const jsonHeaders = new Headers(); +   jsonHeaders.set("content-type", "application/json"); +   const server = await createTestServer(); +   // Registering a user +   const { user: registeredUser } = await fetch( +     "http://localhost:9001/api/users/register", +     { +       method: "POST", +       headers: jsonHeaders, +       body: JSON.stringify({ +         username: "asantos00", +         password: "abcd", +       }), +     }, +   ).then((r) => r.json()) + … + ``` + +1. 由于我们可以访问注册用户,因此我们可以尝试使用同一个用户登录: + + ```js +   // Login in with the createdUser +   const response = await +     fetch("http://localhost:9001/api/login", { +       method: "POST", +       headers: jsonHeaders, +       body: JSON.stringify({ +       username: registeredUser.username, +       password: "abcd", +     }), +   }).then((r) => r.json()) + ``` + +1. 现在我们准备编写几个断言来检查我们的登录响应是否如我们所期望的那样: + + ```js +   t.assertEquals(response.user.username, "asantos00", +     "returns username"); +   t.assert(!!response.user.createdAt, "has createdAt +     date"); +   t.assert(!!response.token, "has token"); + ``` + +1. 最后,我们需要在我们的服务器上调用`abort`函数: + + ```js + server.controller.abort(); + ``` + +这是我们第一次进行应用集成测试!我们让应用运行起来,对其进行了注册和登录请求,并断言一切按预期工作。在这里,我们逐步构建了测试,但如果您想查看完整的测试,它可以在本书的 GitHub 仓库中找到([`github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts`](https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts)). + +为了结束这个话题,我们将编写另一个测试。还记得在上一个章节中,我们创建了一些授权逻辑,只允许已登录的用户访问博物馆列表吗?让我们用另一个测试来检查这是否有效: + +1. 在`src/index.test.ts`中创建另一个测试,以测试用户是否可以使用有效的令牌访问博物馆列表: + + ```js + Deno.test("it should let users with a valid token +   access the museums list", async () => {}) + ``` + +1. 由于我们想要再次登录和注册,我们将这些功能提取到一个我们可以在其多个测试中使用的工具函数中: + + ```js + function register(username: string, password: string) { +   const jsonHeaders = new Headers(); +   jsonHeaders.set("content-type", "application/json"); +   return +    fetch("http://localhost:9001/api/users/register", { +      method: "POST", +      headers: jsonHeaders, +      body: JSON.stringify({ +       username, +       password, +     }), +   }).then((r) => r.json()); + } + function login(username: string, password: string) { +   const jsonHeaders = new Headers(); +   jsonHeaders.set("content-type", "application/json"); +   return fetch("http://localhost:9001/api/login", { +     method: "POST", +     headers: jsonHeaders, +     body: JSON.stringify({ +       username, +       password, +     }), +   }).then((r) => r.json()); + } + ``` + +1. 有了这些功能,我们现在可以重构之前的测试,使其看起来更干净一些,以下代码段展示了这一点: + + ```js + Deno.test("it returns user and token when user logs +   in", async () => { +   const jsonHeaders = new Headers(); +   jsonHeaders.set("content-type", "application/json"); +   const server = await createTestServer(); +   // Registering a user +   await register("test-user", "test-password"); +   const response = await login("test-user", "test- +   password"); +   // Login with the created user +   t.assertEquals(response.user.username, "test-user", +     "returns username"); +   t.assert(!!response.user.createdAt, "has createdAt +     date"); +   t.assert(!!response.token, "has token"); +   server.controller.abort(); + }); + ``` + +1. 让我们回到我们正在编写的测试——检查认证用户是否可以访问博物馆的测试,并使用`register`和`login`函数来注册和认证一个用户: + + ```js + Deno.test("it should let users with a valid token +   access the museums list", async () => { +   const jsonHeaders = new Headers(); +   jsonHeaders.set("content-type", "application/json"); +   const server = await createTestServer(); +   // Registering a user +   await register("test-user", "test-password"); +   const { token } = await login("test-user", "test- +     password"); + ``` + +1. 现在,我们可以使用从`login`函数返回的令牌,在`Authorization`头中进行认证请求: + + ```js +   const authenticatedHeaders = new Headers(); +   authenticatedHeaders.set("content-type", +     "application/json"); +   login function and sending it with the Authorization header in the request to the museums route. Then, we're checking if the API responds correctly to the request with the 200 OK status code. In this case, since our application doesn't have any museums, it is returning an empty array, which we're also asserting.Since we're testing this authorization feature, we can also test that a user with no token or an invalid token can't access this same route. Let's do it. + ``` + +1. 创建一个测试,检查用户是否可以在没有有效令牌的情况下访问`museums`路由来。它应该与之前的测试非常相似,唯一的不同是我们现在发送一个无效的令牌: + + ```js + Deno.test("it should respond with a 401 to a user with +   an invalid token", async () => { +   const server = await createTestServer(); +   const authenticatedHeaders = new Headers(); +   authenticatedHeaders.set("content-type", +     "application/json"); + authenticatedHeaders.set("authorization", +    `Bearer invalid-token`); +   const response = await +     fetch("http://localhost:9001/api/museums", { +       headers: authenticatedHeaders, +       body: JSON.stringify({ +       username: "test-user", +       password: "test-password", +     }), +   }); +   t.assertEquals(response.status, 401); +   t.assertEquals(await response.text(), +    "Authentication failed"); +   server.controller.abort(); + }); + ``` + +1. 现在,我们可以运行所有测试并确认它们都是绿色的: + + ```js + $ deno test --unstable --allow-plugin --allow-env --allow-read –-allow-write --allow-net src/index.test.ts       + running 3 tests + test it returns user and token when user logs in ... Application running at http://localhost:9001 + POST http://localhost:9001/api/users/register - 3ms + POST http://localhost:9001/api/login - 3ms + ok (24ms) + test it should let users with a valid token access the museums list ... Application running at http://localhost:9001 + POST http://localhost:9001/api/users/register - 0ms + POST http://localhost:9001/api/login - 1ms + GET http://localhost:9001/api/museums - 8ms + ok (15ms) + test it should respond with a 400 to a user with an invalid token ... Application running at http://localhost:9001 + An error occurred Authentication failed + ok (5ms) + test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (45ms) + ``` + +本书我们要编写的应用集成测试就这些!如果您想了解更多,请不要担心——本书中编写的一切代码都可以在本书的 GitHub 仓库中找到([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)). + +我们现在对代码工作的信心更强了。我们创造了机会来重构、扩展和后期维护代码,担忧更少。我们在架构决策上越来越受益于在隔离测试代码方面的经验。 + +在前一章中,当我们创建我们的 JavaScript 客户端时,我们提到了将其保存在 API 代码库中的一个优点是,我们可以轻松地为客户端和 API 编写测试,以确保它们一起正常工作。在下一部分,我们将展示如何做到这一点。这些测试将与我们在这里所做的非常相似,唯一的区别是,我们将使用我们创建的 API 客户端,而不是使用`fetch`并进行原始请求。 + +# 测试应用程序与 API 客户端一起工作 + +当你向用户提供 API 客户端时,你有责任确保它与你的应用程序完美无缺地工作。确保这一点的一种方法是拥有一个完整的测试套件,不仅要测试客户端本身,还要测试其与 API 的集成。我们将在这里处理后者。 + +我们将使用 API 客户端的一个功能,并创建一个测试,确保它正在工作。再次,你会注意到这些测试与我们在上一部分末尾编写的测试有一些相似之处。我们将复制先前测试的逻辑,但这次我们将使用客户端。让我们开始吧: + +1. 在同一个`src/index.test.ts`文件中,为登录功能创建一个新的测试: + + ```js + Deno.test("it returns user and token when user logs in +   with the client", async () => {}) + ``` + + 对于这次测试,我们知道我们需要访问 API 客户端。我们需要从`client`模块中导入它。 + +1. 从`src/client/index.ts`导入`getClient`函数: + + ```js + import { getClient } from "./client/index.ts" + ``` + +1. 让我们回到`src/index.test.ts`测试,并导入`client`,从而创建一个它的实例。记住,它应该使用测试网络服务器创建的相同地址: + + ```js + Deno.test("it returns user and token when user logs in +   with the client", async () => { +   const server = await createTestServer(); +   const client = getClient({ + createTestServer function and this test, but for simplicity, we won't do this here. + ``` + +1. 现在,只需编写使用`client`调用`register`和`login`方法的逻辑。最终测试将看起来像这样: + + ```js + Deno.test("it returns user and token when user logs in +   with the client", async () => { + … +   // Register a user +   await client.register( +     { username: "test-user", password: "test-password" +        }, +   ); +   // Login with the createdUser +   const response = await client.login({ +     username: "test-user", +     password: "test-password", +   }); +   t.assertEquals(response.user.username, "test-user", +     "returns username"); +   t.assert(!!response.user.createdAt, "has createdAt +     date"); +   t.assert(!!response.token, "has token"); + … + }); + ``` + + 注意我们如何使用客户端的方法进行登录和注册,同时保留来自先前测试的断言。 + +遵循相同的指导原则,我们可以为客户端的所有功能编写测试,确保它与 API 一起正常工作,从而使我们能够自信地维护它。 + +为了简洁起见,并且因为这些测试与我们之前编写的测试相似,我们这里不会提供有关为客户端的所有功能编写测试的逐步指南。然而,如果你感兴趣,你可以在本书的 GitHub 仓库中找到它们([`github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts`](https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts))。 + +在下一部分,我们将预览一个可能位于应用程序路径下方的功能。总有一天,你会发现应用程序的某些部分似乎变得很慢,你希望追踪它们的性能,这时性能测试就派上用场了。因此,我们将引入基准测试。 + +# 基准测试应用程序的部分 + +当涉及到用 JavaScript 编写基准测试时,该语言本身提供了一些函数,所有这些函数都包含在高级分辨率时间 API 中。 + +由于 Deno 完全兼容 ES6,这些相同的功能都可以使用。如果你有机会查看 Deno 的标准库或官方网站,你会发现人们对基准测试给予了大量的关注,并且在 Deno 各个版本中跟踪它们([`deno.land/benchmarks`](https://deno.land/benchmarks))。在检查 Deno 的源代码时,你会发现有关如何编写它们的非常不错的示例集。 + +对于我们的应用程序,我们本可以轻松地使用浏览器上可用的 API,但 Deno 本身在其标准库中提供了帮助编写和运行基准测试的功能,因此我们将在这里使用它。 + +首先,我们需要了解 Deno 的标准库基准工具,以便我们知道可以做什么([`github.com/denoland/deno/blob/ae86cbb551f7b88f83d73a447411f753485e49e2/std/testing/README.md#benching`](https://github.com/denoland/deno/blob/ae86cbb551f7b88f83d73a447411f753485e49e2/std/testing/README.md#benching))。在本节中,我们将使用两个可用的函数编写一个非常简单的基准测试;即,`bench`和`runBenchmarks`。第一个将定义一个基准测试,而第二个将运行它并将结果打印到控制台。 + +记得我们在第五章中编写的函数吗?*添加用户和迁移到 Oak*,用于生成散列值和盐值,这使我们能够将用户凭据安全地存储在数据库上?我们将通过以下步骤为此编写基准测试: + +1. 首先,在`src/users/util.ts`旁边创建一个名为`utilBenchmarks.ts`的文件。 + +1. 从`util`模块中导入我们想要测试的两个函数,即`generateSalt`和`hashWithSalt`: + + ```js + import { generateSalt, hashWithSalt } from "./util.ts" + ``` + +1. 是时候将基准工具添加到我们的`src/deps.ts`文件中,并运行`deno cache`命令(我们在第二章中了解到)*工具链*)并在此处导入它。我们将把它作为`benchmark`导出到`src/deps.ts`中,以避免命名冲突: + + ```js + export * as benchmark from +   "https://deno.land/std@0.83.0/testing/bench.ts"; + ``` + +1. 将基准工具导入我们的基准测试文件中,并为`generateSalt`函数编写第一个基准测试。我们希望它运行 1000 次: + + ```js + import { benchmarks } from "../deps.ts"; + benchmarks.bench({ +   name: "runsSaltFunction1000Times", +   runs: 1000, +   func: (b) => { +     bench function (as stated in the documentation). Inside this object, we're defining the number of runs, the name of the benchmark, and the test function. That function is what will run every time, since an argument is an object of the BenchmarkTimer type with two methods; that is, start and stop. These methods are used to start and stop the timings of the benchmarks, respectively. + ``` + +1. 我们所缺少的只是定义了基准测试后调用`runBenchmarks`: + + ```js + benchmarks.bench({ +   name: "runsSaltFunction1000Times", +   … + }); + benchmarks.runBenchmarks(); + ``` + +1. 是时候运行这个文件并查看结果了。 + + 记住,由于我们希望我们的基准测试具有高精度,所以我们正在处理高分辨率时间。为了让这段代码能够访问这个系统特性,我们需要以`--allow-hrtime`权限运行这个脚本(如第二章中所解释的,*工具链*): + + ```js + $ deno run --unstable --allow-plugin --allow-env --allow-read --allow-write --allow-hrtime src/users/utilBenchmarks.ts + running 1 benchmarks ... + benchmark runsSaltFunction1000Times ... +     1000 runs avg: 0.036691561000000206ms + benchmark result: DONE. 1 measured; 0 filtered + ``` + +1. 让我们为第二个函数编写基准测试,即`hashWithSalt`: + + ```js + benchmarks.bench({ +   name: "runsHashFunction1000Times", +   runs: 1000, +   func: (b) => { +     b.start(); +     hashWithSalt("password", "salt"); +     b.stop(); +   }, + }); + benchmarks.runBenchmarks(); + ``` + +1. 现在,让我们运行它,以便得到最终结果: + + ```js + $ deno run --allow-hrtime --unstable --allow-plugin --allow-env –-allow-write --allow-read src/users/utilBenchmarks.ts      + running 2 benchmarks ... + benchmark runsSaltFunction100Times ... +     1000 runs avg: 0.036691561000000206ms + benchmark runsHashFunction100Times ... +     1000 runs avg: 0.02896806399999923ms + benchmark result: DONE. 2 measured; 0 filtered + ``` + +就这样!现在你可以随时使用我们刚刚编写的代码来分析这些函数的性能。你可能想这样做,是因为你已经修改了这段代码,或者只是因为你想要对其进行严格跟踪。你可以将其集成到诸如持续集成服务器之类的系统中,在那里你可以定期检查这些值并保持其正常运行。 + +本书关于基准测试的部分就到此为止。我们决定给予它一个简短的介绍,并展示从 Deno 获取的 API,以方便进行基准测试。我们相信,这里介绍的概念和例子可以帮助你跟踪应用程序的运行情况。 + +# 总结 + +通过这一章节,我们已经完成了我们一直在构建的应用程序的开发周期。我们从编写几个简单的类开始,带有我们的业务逻辑,编写 web 服务器,最后将其与持久化集成。我们通过学习如何测试我们编写的功能来结束这一部分,这就是我们在这章所做的事情。我们决定选择几种不同类型的测试,而不是深入每个模块编写所有测试,因为我们认为这样做会带来更多的价值。 + +我们从业务逻辑的一个非常简单的单元测试开始,然后转向带有多个类的集成测试,最后为 web 服务器编写了一个测试。这些测试只能通过利用我们创建的架构、遵循依赖注入原则并尽量使代码解耦来编写。 + +随着章节的进行,我们转向了集成测试,这些测试紧密地模仿了将在生产环境中运行的队列应用程序,使我们能够提高对我们刚刚编写的代码的信心。我们创建了测试,使用测试设置实例化了应用程序,能够启动带有所有应用程序层(业务逻辑、持久化和网络)的 web 服务器,并对它进行断言。在这些测试中,我们可以非常有信心地断言登录和注册行为是否正常,因为我们向 API 发送了真实的请求。 + +为了结束本章,我们将它与前一章连接起来,我们在那里为 API 编写了一个 JavaScript 客户端。我们利用了客户端与 API 位于同一代码库中的一个巨大优势,并一起测试了客户端及其应用程序。这是确保一切按预期工作,并在发布 API 和客户端更改时保持信心的好方法。 + +本章试图展示如何在 Deno 中使用测试来提高我们对所编写代码的信心,以及当它们用于关注简单结果时所提供的价值。这类测试在应用更改时将非常有用,因为我们可以使用它们来添加更多功能或改进现有功能。在这里,我们了解到 Deno 提供的测试套件足以编写清晰、可读的测试,而无需任何第三方包。 + +下一章将重点关注应用开发过程中最重要的阶段之一,那就是部署。我们将配置一个非常简单的持续集成环境,在该环境中我们可以将应用部署到云端。这一章节非常重要,因为我们将体验到 Deno 在部署方面的某些优势。 + +迫不及待地想让你的应用供用户使用吗?我们也是——让我们开始吧! diff --git a/docs/deno-web-dev/deno-web-dev_08.md b/docs/deno-web-dev/deno-web-dev_08.md new file mode 100644 index 0000000..f921213 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_08.md @@ -0,0 +1,546 @@ +# 第七章:第*9*章:部署 Deno 应用程序 + +部署是任何应用程序的关键部分。我们可能会构建一个伟大的应用程序,遵循最佳实践,并编写测试,但最终,当它到达用户手中时,它将在这里证明其价值。既然我们希望这本书能带领读者经历应用程序的所有不同阶段,我们将在本章中关于应用程序部署的章节中结束这个循环。 + +请注意,我们没有——也不会——将部署作为软件开发的最后阶段来提及,而是将其视为将多次运行的一个阶段。我们真心相信部署不应该是每个人都害怕的事件。相反,我们认为它是令人兴奋的时刻,我们正在向用户发送功能。大多数公司就是这样看待现代软件项目中的部署的,我们确实是这种观点的忠实倡导者。部署应该是定期、自动化且容易执行的事情。它们是我们将功能发送给用户的第一步,而不是最后一步。 + +为了使流程的灵活性和应用程序的迭代速度得到这种敏捷,本章将重点学习关于容器以及如何使用它们部署 Deno 应用程序的知识。 + +我们将利用容器化的好处,创建一个隔离的环境来安装、运行和分发我们的应用程序。 + +随着章节的进行,我们将学习如何使用 Docker 和`git`一起创建一个自动化工作流,以便在云环境中部署我们的 Deno 应用程序。然后,我们将调整应用程序加载配置的方式,以支持根据环境不同而有不同的配置。 + +到本章结束时,我们的应用程序将在云环境中运行,并有一个自动化流程,使我们能够发送它的迭代版本。 + +在本章中,您将熟悉以下主题: + ++ 为应用程序准备环境 + ++ 为 Deno 应用程序创建一个`Dockerfile` + ++ 在 Heroku 中构建和运行应用程序 + ++ 为部署配置应用程序 + +# 技术要求 + +本章使用的代码可以在以下 GitHub 链接中找到: + +链接:[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter09`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter09) + +# 为应用程序准备环境 + +应用程序运行的环境对其有很大影响。这是导致人们常说“*它在我的机器上运行正常*”这一常见说法的一个大原因。多年来,开发者一直在创造尽可能减少这种影响的解决方案。这些解决方案可以从为应用程序自动提供新的干净实例运行,到创建更完整的包,其中包含应用程序依赖的一切。 + +我们可以将**虚拟机**(**VM**)或容器称为实现这一目标的手段。两者都是为同一问题提供的不同解决方案,但有一个很大的共同点:资源隔离。两者都试图将应用程序与周围的环境隔离。有多种原因,从安全、自动化到可靠性。 + +容器是提供应用程序包的一种现代方式。现代软件项目使用它们来提供一个几乎包含应用程序所需的所有内容的单一容器镜像。 + +如果您不知道容器是什么,我将为您提供 Docker(一个容器引擎)官方网站的定义: + +*“容器是一个标准的软件单元,它打包了代码及其所有依赖项,以便应用程序从一个计算环境快速且可靠地运行到另一个计算环境。”* + +在我们使应用程序容易部署的路径中,我们将使用 Docker 为我们的 Deno 应用程序创建这种隔离层。 + +最终目标是创建一个开发者可以用来部署和测试应用程序特定版本的镜像。要使用 Docker 做到这一点,我们需要配置应用程序将运行的运行时。这在一个名为`Dockerfile`的文件中定义。 + +这就是我们接下来要学习的内容。 + +# 为 Deno 应用程序创建 Dockerfile + +一个`Dockerfile`将允许我们指定创建新 Docker 镜像所需的内容。这个镜像将提供一个包含应用程序所有依赖项的环境,该环境可用于开发目的和生产部署。 + +在本节中,我们将学习如何为 Deno 应用程序创建一个 Docker 镜像。Docker 提供了一个基础镜像,它基本上就是带隔离的容器运行时,称为`alpine`。我们可以使用这个镜像,配置它,安装所有需要的工具和依赖(即 Deno),等等。然而,我相信我们在这里不应该重新发明轮子,因此我们使用一个社区 Docker 镜像。 + +尽管这个镜像解决了许多我们的问题,我们仍然需要调整它以适应我们的用例。Dockerfile 可以组合,这意味着它们可以扩展其他 Docker 镜像的功能,我们将使用这个功能。 + +重要说明 + +如您所想象,我们不会深入讲解 Docker 的基础知识,因为这将是另一本书。如果您对 Docker 感兴趣,您可以从官方文档的*入门指南*开始([`docs.docker.com/get-started/`](https://docs.docker.com/get-started/))。但是,如果您目前对 Docker 不是非常熟悉,也不要担心,因为我们将会解释足够的内容,让您理解我们在这里做什么。 + +在开始之前,请确保通过以下链接中的步骤在您的机器上安装 Docker Desktop:[`docs.docker.com/get-docker/`](https://docs.docker.com/get-docker/)。安装并启动它之后,我们就有了创建我们的第一个 Docker 镜像所需的一切!按照以下步骤创建它: + +1. 在项目的根目录下创建一个`Dockerfile`。 + +1. 如前所述,我们将使用社区中已经安装了 Deno 的镜像—`hayd/deno` ([`hub.docker.com/r/hayd/deno`](https://hub.docker.com/r/hayd/deno))。 + + 这个图像的版本号与 Deno 相同,因此我们将使用版本`1.7.5`。Docker 的`FROM`命令允许我们扩展一个图像,指定其名称和版本标签,如下面的代码片段所示: + + ```js + FROM hayd/alpine-deno:1.7.5 + ``` + +1. 接下来,我们需要在容器内定义我们将要工作的文件夹。 + + Docker 容器提供了一个 Linux 文件系统,默认的`workdir`是它的根(`/`)。Docker 的`WORKDIR`命令将允许我们在这个文件系统内的同一个文件夹中工作,使事情变得更加整洁。该命令可在此处看到: + + ```js + WORKDIR /app + ``` + +1. 现在,我们需要将一些文件复制到我们的容器图像中。在`COPY`命令的帮助下,我们将只复制安装步骤所需的文件。在我们这个案例中,这些是`src/deps.ts`和`lock.json`文件,如下面的片段所示: + + ```js + COPY command from Docker allows us to specify a file to copy from the local filesystem (the first parameter) into the container image (the last parameter), which is currently the app folder. By dividing our workflows and copying only the files we need, we allow Docker to cache and rerun part of the steps only when the involved files changed. + ``` + +1. 文件在容器内后,我们现在需要安装应用程序依赖。我们将使用`deno cache`来完成此操作,如下所示: + + ```js + deno-mongo) and also using the lock file, we have to pass additional flags. Docker's `RUN` command enables us to run this specific command inside the container. + ``` + +1. 依赖项安装后,我们现在需要将应用程序的代码复制到容器中。再一次,我们将使用 Docker 的`COPY`命令完成此操作,如下所示: + + ```js + workdir (/app folder) inside the container. + ``` + +1. 为了使我们的镜像能够即插即用,我们需要做的最后一件事情是在容器内引入一个命令,该命令将在有人“执行”此镜像时运行。我们将使用 Docker 的`CMD`命令来完成此操作,如下面的片段所示: + + ```js + CMD ["deno", "run", "--allow-net", "--unstable", "--allow-env", "--allow-read", "--allow-write", "--allow-plugin", "src/index.ts" ] + ``` + + 该命令接受一个命令和参数数组,当有人尝试运行我们的图像时将被执行。 + +这样应该就是我们定义 Deno 应用程序 Docker 图像所需的所有内容了!有了这些功能,我们就能以与生产中相同的方式在本地下运行我们的代码,这对于调试和调查生产问题来说是一个很大的优势。 + +我们唯一缺少的是生成工件的实际步骤。 + +我们将使用 Docker `-t`标志的`build`命令来设置标签。按照以下步骤生成工件: + +1. 在项目文件夹内,运行以下命令来生成图像的标签: + + ```js + museums-api in this example) and choose whichever version you want (0.0.1 in the example).This should produce the following output: + + ``` + + `museums-api:0.0.1`。我们现在可以在私有镜像仓库中发布它,或者使用公共镜像仓库,如 Docker Hub。我们稍后设置的持续集成(CI)管道将配置为自动执行这个构建步骤。我们现在可以运行这个镜像来验证一切是否按预期工作。 + + ```js + + ``` + +1. 为了在本地运行镜像,我们将使用 Docker CLI 的`run`命令。 + + 由于我们处理的是一个 web 应用程序,我们需要暴露它正在运行的端口(在应用程序的`configuration`文件中设置)。我们将告诉 Docker 通过使用`-p`标志将容器端口绑定到我们机器的端口,如图以下代码片段所示: + + ```js + 0.0.1 of the museums-api image, binding the 8080 container port to the 8080 host port. We can now go to http://localhost:8080 and see that the application is running. + ``` + +我们稍后将在 CI 系统中使用这个镜像定义,每当代码更改时,它都会创建一个镜像并将其推送到生产环境。 + +拥有包含应用程序的 Docker 镜像可以服务于多个目的。其中之一就是本章的目标:部署它;然而,这个同样的 Docker 镜像也可以用来运行和调试特定版本的某个应用程序。 + +让我们学习我们如何在应用程序的特定版本中运行一个终端,这是一个非常常见的调试步骤。 + +## 在容器内运行一个终端 + +我们还可以使用 Docker 镜像在镜像内执行一个终端。这可能对调试目的或尝试在应用程序的特定版本中测试某事很有用。 + +我们可以通过使用与以前相同的命令以及几个不同的标志来实现这一点。 + +我们将使用`-it`标志,这将允许我们与镜像内的终端建立交互式连接。我们还将发送一个参数,即我们希望在镜像内首先执行的命令的名称。在这个例子中,是`sh`,标准的 Unix 壳,正如您在以下示例中可以检查的那样: + +```js +$ docker run -p 8080:8080 -it  museums-api:0.0.1 sh +``` + +这将运行`museums-api:0.0.1`镜像,将其`8080`端口绑定到宿主机的`8080`端口,并在具有交互式终端的其中执行`sh`命令,如图以下代码片段所示: + +```js +$ docker run -p 8080:8080 -it  museums-api:0.0.1 sh         +/app # ls +Dockerfile           certificate.pem      config.staging.yaml  index.html           lock.json +README.md            config.dev.yaml      heroku.yml        key.pem              src +``` + +请注意,初始打开的 shell 文件夹是我们定义为`WORKDIR`的文件夹,我们所有的文件都在那里。在前面的例子中,我们还执行了`ls`命令。 + +由于我们在这个容器上附加了一个交互式 shell,我们可以使用它来运行一个 Deno 命令,例如,如图以下代码片段所示: + +```js +/app # deno --version +deno 1.7.2 (release, x86_64-unknown-linux-gnu) +v8 8.9.255.3 +typescript 4.1.3 +/app # +``` + +这为开发和调试提供了一系列可能性,因为我们将能够查看应用程序在特定版本下是如何运行的。 + +我们已经到了本节的最后。在这里,我们探讨了容器化,介绍了 Docker 以及它如何让我们创建一个“应用程序包”。这个包将负责应用程序周围的环境,确保它无论在何处只要有 Docker 运行时都能运行。 + +在下一节中,我们将使用这个相同的包将在本地构建的镜像部署到云环境。让我们开始吧! + +# 在 Heroku 上构建和运行应用程序 + +正如我们在章节开始时提到的,我们的初始目标是实现一个简单、自动化且可复制的应用部署方式。在前一部分,我们创建了我们的容器镜像,它将作为我们部署的基础。下一步是创建一个管道,用于在任何更新发生时构建和部署我们的代码。我们将使用`git`作为我们的真相来源和触发管道构建的机制。 + +我们将部署代码的平台是 Heroku。这是一个平台,旨在通过提供一套工具来简化开发人员和公司在部署过程中的任务,这些工具消除了常见的障碍,例如配置机器和设置大的 CI 基础架构。使用这样的平台,我们可以更专注于应用程序以及 Deno,这是本书的目的。 + +在这里,我们将使用我们之前创建的`Dockerfile`,并将其设置为在 Heroku 上部署和运行。我们将了解如何轻松地将应用程序设置为在此处运行,稍后我们还将探索如何通过环境变量定义配置值。 + +在开始之前,请确保您已经根据这里提供的两个链接创建了账户并安装了 Heroku CLI,然后我们再按照步骤指南进行操作。 + ++ 创建账户:[`signup.heroku.com/dc`](https://signup.heroku.com/dc)。 + ++ 安装 CLI:[`devcenter.heroku.com/articles/heroku-cli`](https://devcenter.heroku.com/articles/heroku-cli)。 + +现在我们已经创建了账户并安装了 CLI,我们可以开始在 Heroku 中设置我们的项目。 + +## 在 Heroku 中创建应用程序 + +在这里,我们将介绍在 Heroku 中进行身份验证并创建应用程序所需的步骤。我们几乎准备好了,但是在我们开始之前,还有另一件事我们必须先弄清楚。 + +重要提示 + +由于 Heroku 使用`git`作为真相来源,您将*无法*按照以下过程在本书的文件仓库内进行操作,因为该仓库已经是一个包含应用程序多个阶段的 Git 仓库。 + +我建议您将应用程序文件复制到不同的文件夹中,*位于本书仓库之外*,并从那里开始流程。 + +您可以从第八章的*测试 - 单元和集成*中复制工作应用的最新版本,即我们将在这里使用的版本。[`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api) + +现在文件已经复制到一个新的文件夹(主仓库外),让我们按照以下步骤部署`Dockerfile`并在 Heroku 上运行它: + +1. 我们将首先做的就是使用 CLI 登录,运行`heroku login`。这应该会打开一个浏览器窗口,您可以在其中输入您的用户名和密码,如下面的代码段所示: + + ```js + $ heroku login + heroku: Press any key to open up the browser to login or q to exit: + Opening browser to https://cli-auth.heroku.com/auth/cli/... + Logging in... done + Logged in as your-login-email@gmail.com + ``` + +1. 由于 Heroku 的部署基于`git`,而我们当前所在的文件夹并非 Git 仓库,因此我们需要初始化它,步骤如下: + + ```js + $ git init + Initialized empty Git repository in /Users/alexandre/dev/ museums-api/.git/ + ``` + +1. 然后,我们通过使用`heroku create`命令在 Heroku 上创建应用,步骤如下: + + ```js + heroku, which is where we have to push our code to trigger the deployment process. + ``` + +如果你在运行上述命令后访问 Heroku 控制台,你会注意到那里有一个新应用。当应用创建时,Heroku 会在控制台打印一个 URL;然而,由于我们还没有进行任何配置,所以应用目前不可用。 + +接下来我们需要做的是配置 Heroku,使其知道它应该在每次部署时构建和执行我们的镜像。 + +## 构建和运行 Docker 镜像 + +默认情况下,Heroku 试图通过运行代码使你的应用可用。这对于许多语言来说都是可能的,你可以在 Heroku 文档中找到相关指南。由于我们希望使用容器来运行应用,因此需要进行一些额外的配置。 + +Heroku 提供了一系列特性,使我们能够定义代码发生变化时的行为,通过一个名为`heroku.yml`的文件。我们现在将创建这样的文件,步骤如下: + +1. 在仓库根目录下创建一个`heroku.yml`文件,并添加以下代码行,以便使用 Docker 构建我们的镜像,使用我们在上一节中创建的`Dockerfile`: + + ```js + build: +   docker: +     web: Dockerfile + ``` + +1. 现在,在同一个文件中,添加以下代码行以定义 Heroku 将执行以运行应用的命令: + + ```js + build: +   docker: +     web: Dockerfile + Dockerfile, and that's true. Normally, Heroku would run the command from the `Dockerfile` to execute the image, and it would work. It happens that Heroku doesn't run these commands as root, as a security best practice. Deno, at its current stage, needs root privileges whenever you want to use plugins (an unstable feature). As our application is using a plugin to connect with MongoDB, we need this command to be explicitly defined on `heroku.yml` so that it is run with root privileges and works when Deno is starting up the application. + ``` + +1. 接下来我们需要做的是将应用类型设置为`container`,告知 Heroku 我们希望以这种方式运行这个应用。这段代码如下所示: + + ```js + heroku.yml file included) to version control and push it to Heroku so that it starts the build. + ``` + +1. 添加所有文件以确保`git`追踪它们: + + ```js + $ git add . + ``` + +1. 提交所有文件并附上信息,步骤如下: + + ```js + -m flag that we've used is a command that allows us to create a commit with a message with a short syntax. + ``` + +1. 现在,需要将文件推送到`heroku`远程仓库。 + + 这应该触发 Docker 镜像的构建过程,你可以在日志中进行检查。然后,在最后阶段,这个镜像会被推送到 Heroku 内部的镜像仓库,如下代码片段所示: + + ```js + Dockerfile, following all the steps specified there, as happened when we built the image locally, as illustrated in the following code snippet: + + ``` + + 远程仓库:=== 推送 web(Dockerfile) + + 远程仓库:将标签为“5c154f3fcb23f3c3c360e16e929c22b62847fcf8”的镜像标记为“registry.heroku.com/boiling-dusk-18477/web” + + 远程仓库:使用默认标签:latest + + 远程仓库:推送指的是[registry.heroku.com/boiling-dusk-18477/web]仓库 + + 远程仓库:6f8894494a30: 准备中 + + 远程仓库:f9b9c806573a: 准备中 + + ```js + + And it should be working, right? Well…, not really. We still have a couple of things that we need to configure, but we're almost there. + ``` + +请记住,我们的应用依赖于配置,而配置的一部分来自环境。Heroku 肯定知道我们需要哪些配置值。还有一些设置我们需要配置以使应用运行,接下来我们将完成这个任务。 + +# 配置应用以进行部署 + +我们现在有一个应用程序,当代码推送到 `git` 时,它开始构建镜像并部署它。我们的应用程序目前被部署了,但它实际上并没有运行,这是因为它缺少配置。 + +你首先注意到的一件事可能是我们的应用程序总是加载开发环境的配置文件,即 `config.dev.yml`,这是不正确的。 + +当我们第一次实现这个时,我们认为不同的环境会有不同的配置,我们是对的。当时,我们不需要为多个环境配置多个配置文件,我们使用了 `dev` 作为默认值。让我们来纠正这个。 + +记得当我们创建加载配置文件的函数时,我们明确使用了一个环境参数?当时我们没有使用它,但我们留下了一个默认值。 + +查看以下来自 `src/config/index.ts` 的代码片段: + +```js +export async function load( +  env = "dev", +): Promise { +``` + +我们需要做的是将这个改为支持多个环境。所以,让我们按照以下步骤来做到这一点: + +1. 回到 `src/index.ts` 文件中,确保我们向 `load` 函数发送了一个名为 `DENO_ENV` 的环境变量,如下面的代码片段所示: + + ```js + const config = await +   loadConfiguration(DENO_ENV is not defined, and allow us to load a different configuration file in production. + ``` + +1. 创建生产环境配置文件 `config.production.yml`。 + + 现在,它应该与 `config.dev.yml` 没有太大区别,除了 `port` 之外。让我们在生产环境中将其运行在端口 `9001`,如下所示: + + ```js + web: +   port: 9001 + ``` + + 为了在本地测试这个,我们可以设置 `DENO_ENV` 变量为 `production` 来运行应用程序,像这样: + + ```js + DENO_ENV). We mentioned how you can do this in *Chapter 7**, HTTPS, Extracting Configuration, and Deno in the Browser*, in the *Accessing secret values* section.And after running it we can confirm it's loading the correct file, because the application port is now `9001`. + ``` + +有了我们刚刚实施的内容,我们现在可以根据环境控制加载哪些配置值。这是我们已经在本地测试过的,但在 Heroku 上还没有做过。 + +我们已经解决了问题的一部分——我们根据环境加载不同的配置文件,但我们的应用程序依赖的其他配置值来自环境。那些是诸如 **JSON Web Token** (**JWT**) 密钥或 MongoDB 凭据等机密值。 + +有很多方法可以做到这一点,所有云服务提供商都提供了相应的解决方案。在 Heroku 上,我们可以使用 `config` 命令来实现,如下所示: + +1. 定义 MongoDB 凭据变量、JWT 密钥和环境,使用 `heroku config:set` 命令,如下所示: + + ```js + DENO_ENV variable so that our application knows that, when running in Heroku, it is the production environment.If you are not using your own MongoDB cluster and you have questions about its credentials, you can go back to *Chapter 6*, *Adding Authentication and Connecting to the Database*, where we created a MongoDB cluster in MongoDB Atlas.If you're using a different cluster, remember that it is defined in the configuration file in `config.production.yml` and not in the environment, and thus you need to add your cluster URL and database in the configuration file as follows: + + ``` + + … + + mongoDb: + + clusterURI: <添加你的集群 url> + + database: <添加你的数据库名称> + + ```js + + ``` + +1. 再次,我们将把我们的更改添加到 `git` 中,如下所示: + + ```js + $ git commit -am "Configure environment variables and DENO_ENV" + ``` + +1. 然后,我们将把更改推送到 Heroku 以触发部署过程,如下所示: + + ```js + $ git push heroku master + … + remote: Verifying deploy... done. + To https://git.heroku.com/boiling-dusk-18477.git +    9340446..36a061e  master -> master + ``` + + 它应该能正常工作。如果我们现在访问 Heroku 仪表板 ([`dashboard.heroku.com/`](https://dashboard.heroku.com/)),然后进入我们应用程序的仪表板 ([`dashboard.heroku.com/apps/boiling-dusk-18477`](https://dashboard.heroku.com/apps/boiling-dusk-18477),在我的案例中) 并点击 **打开应用程序** 按钮,它应该打开我们的应用程序,对吗? + + 还没有,但我们快到了——我们还需要解决一件事。 + +## 从环境中获取应用程序端口 + +当在 Heroku 上运行 Docker 镜像时,Heroku 有一些特别之处。它不允许我们设置应用程序运行的端口。它所做的是为应用程序分配一个端口,然后将来自应用程序 URL 的**超文本传输协议**(**HTTP**)和**超文本传输协议安全**(**HTTPS**)流量重定向到那里。如果这听起来仍然很奇怪,不用担心——我们会解释清楚。 + +正如你所知,我们在`config.production.yml`文件中明确定义了应用程序将要运行的端口。我们需要适应这一点。 + +Heroku 定义应用程序应运行的端口的方式是通过设置`PORT`环境变量。这在以下链接中有文档说明: + +[`devcenter.heroku.com/articles/container-registry-and-runtime#dockerfile-commands-and-runtime`](https://devcenter.heroku.com/articles/container-registry-and-runtime#dockerfile-commands-and-runtime) + +从标题中你可能知道我们接下来要做什么。我们要更改我们的应用程序,以便来自环境的 Web 服务器端口覆盖配置文件中定义的那个。 + +回到应用程序中的`src/config/index.ts`,确保它从环境中读取`PORT`变量,覆盖来自文件的配置。代码如下所示: + +```js +type Configuration = { +  web: { +    port: number; +  }; +  cors: { +… +export async function load( +  env = "dev", +): Promise { +  const configuration = parse( +    await Deno.readTextFile(`./config.${env}.yaml`), +  ) as Configuration; +  return { +    ...configuration, +    web: { +      ...configuration.web, +      port: Number(Deno.env.get("PORT")) || +        configuration.web.port, +    }, +… +``` + +这样,我们确保我们从`PORT`环境变量中读取变量,并在配置文件中使用默认值。 + +这样一来,让我们的应用程序在 Heroku 上顺利运行就应该没问题了! + +再次,我们可以通过访问 Heroku 控制台([`dashboard.heroku.com/apps/boiling-dusk-18477`](https://dashboard.heroku.com/apps/boiling-dusk-18477))并点击**打开应用**按钮来测试这一点,或者你可以直接访问 URL——在我的情况下,是[`boiling-dusk-18477.herokuapp.com/`](https://boiling-dusk-18477.herokuapp.com/)。 + +重要提示 + +如果你像我们在第六章中使用 MongoDB Atlas,*添加身份验证并连接到数据库*,并希望允许你的应用程序访问数据库,你必须配置它以启用来自“任何地方”的连接。如果你将应用程序暴露给用户,这并不是推荐的做法,而且只因为我们使用 Heroku 的免费层才这样。由于它在一个共享的集群中运行,我们没有办法知道运行应用程序的机器的固定的**互联网协议**(**IP**)地址,我们只能这样做。 + +以下链接展示了如何配置数据库的网络访问:[`docs.atlas.mongodb.com/security/ip-access-list`](https://docs.atlas.mongodb.com/security/ip-access-list)。确保你在 MongoDB Atlas 网络访问屏幕上点击**允许从任何地方访问**。 + +网络访问屏幕看起来是这样的: + +![图 9.1 – MongoDB Atlas 网络访问屏幕](img/Figure_9.1_B16380.jpg) + +图 9.1 – MongoDB Atlas 网络访问屏幕 + +之后,我们的应用应该能如预期般正常工作;你可以尝试执行一个注册用户的请求(它连接到数据库)并检查一切是否正常,如下面的代码片段所示: + +```js +$ curl -X POST -d '{"username": "test-username-001", "password": "testpw1" }' -H 'Content-Type: application/json' https://boiling-dusk-18477.herokuapp.com/api/users/register +{"user":{"username":"test-username-001","createdAt":"2020-12-19T16:49:51.809Z"}}% +``` + +如果你得到的响应与前面的类似,那就大功告成了!我们成功地在云环境中配置并部署了我们的应用,并创建了一种自动化的方式将更新部署给我们的用户。 + +为了进行最后的测试,以确保代码正在成功部署,我们可以尝试更改代码的一部分并再次触发部署过程。让我们这样做!按照以下步骤进行: + +1. 将`src/web/index.ts`中的`"Hello World"`消息更改为`"Hello Deno World!"`,如图所示: + + ```js + app.use((ctx) => { +   ctx.response.body = "Hello Deno World!"; + }); + ``` + +1. 将此更改添加到版本控制中,如下所示: + + ```js + $ git commit -am "Change Hello World message" + [master 35f7db7] Change Hello World message + 1 file changed, 1 insertion(+), 1 deletion(-) + ``` + +1. 将其推送到 Heroku 的`git`远程仓库,像这样: + + ```js + $ git push heroku master + Enumerating objects: 9, done. + Counting objects: 100% (9/9), done. + Delta compression using up to 8 threads + Compressing objects: 100% (5/5), done. + Writing objects: 100% (5/5), 807 bytes | 807.00 KiB/s, done. + Total 5 (delta 4), reused 0 (delta 0) + remote: Compressing source files… Done + … + remote: Verifying deploy... done. + To https://git.heroku.com/boiling-dusk-18477.git + ``` + +1. 如果你现在访问应用的 URL(在我们的案例中是[`boiling-dusk-18477.herokuapp.com/`](https://boiling-dusk-18477.herokuapp.com/)),你应该会看到`Hello Deno World`的消息。 + +这意味着我们的应用已成功部署。由于我们使用的是一个提供比我们在这里学到的更多功能的云平台,我们可以探索其他 Heroku 功能,比如日志记录。 + +在 Heroku 控制面板上的**打开应用**按钮旁边,有一个**更多**按钮。其中一个选项是**查看日志**,正如你在下面的屏幕截图中所看到的: + +![图 9.2 – Heroku 控制面板中的应用更多选项](img/Figure_9.2_B16380.jpg) + +图 9.2 – Heroku 控制面板中的应用更多选项 + +如果你点击那里,一个显示实时日志的界面将会出现。你可以尝试通过点击另一个标签页中的**打开应用**按钮来打开你的应用。 + +你会看到日志立即更新,那里应该会出现类似这样的内容: + +```js +2020-12-19T17:04:23.639359+00:00 app[web.1]: GET http://boiling-dusk-18477.herokuapp.com/ - 1ms +``` + +这对于你想对应用的运行情况进行非常轻量级的监控非常有用。日志记录功能在免费层中提供,但还有许多其他功能供你探索,比如我们在这里不会做的**指标**功能。 + +如果你想要详细查看你的应用是在何时以及由谁部署的,你也可以使用 Heroku 控制面板上的**活动**部分,如图所示: + +![图 9.3 – Heroku 控制面板应用选项](img/Figure_9.3_B16380.jpg) + +图 9.3 – Heroku 控制面板应用选项 + +然后,你会看到你最近部署的日志,这是 Heroku 的另一个非常有趣的功能,如图所示: + +![图 9.4 – Heroku 控制面板应用活动标签](img/Figure_9.4_B16380.jpg) + +图 9.4 – Heroku 控制面板应用活动标签 + +这标志着我们在云环境中部署应用程序部分的结束。 + +我们关注的是应用程序及其可以在不同平台上独立重用的主题。我们迭代了加载配置的应用程序逻辑,使其能够根据环境加载不同的配置。 + +然后,我们学习了如何将包含机密配置值的环境变量发送到我们的应用程序,最后通过探索本例中选择的平台 Heroku 上的日志记录来结束。 + +我们成功让应用程序运行起来,并且围绕它创建了一个完整的架构,这将使未来的迭代能够轻松地部署给我们的用户。希望我们经历了一些您下次部署 Deno 应用程序时也会遇到阶段。 + +# 总结 + +现在,我们已经完成了大部分工作!本章完成了我们应用程序开发周期的循环,通过部署它。我们从构建一个非常简单的应用程序开始,然后向其中添加功能,再添加测试,最后—部署它。 + +在这里,我们学习了如何利用容器化的一些优势来应用我们的应用程序。我们开始学习 Docker,我们选择的容器运行时,并迅速转到为我们的应用程序创建镜像。在学习过程中了解一些 Docker 命令,我们也体验了将 Deno 应用程序部署出去有多容易。 + +创建这个 Docker 镜像使我们能够以一种可复用的方式安装、运行和分发我们的应用程序,创建一个包含应用程序所需的所有内容的包。 + +随着章节的进行,我们开始探索如何使用这个应用程序包将其部署到云环境中。我们首先配置了本步骤指南中选择的云平台 Heroku,使其每次发生变化时都会重新构建并运行我们的应用程序代码,在`git`和 Heroku 文档的帮助下,我们非常容易地实现了这一点。 + +由于自动化流水线已经配置完成,我们理解了需要将配置值发送到我们的应用程序。之前在早期章节中实现的这些相同的配置值,需要通过配置文件和环境变量两种不同的方式发送到应用程序中。我们逐一解决了这些需求,首先通过迭代应用程序代码,使其根据环境加载不同的配置,随后学习了如何在 Heroku 上运行的应用程序中设置配置值。 + +最终,我们的应用程序运行得完美无缺,并完成了本章的目标:为我们的用户提供一种可复用、自动化的部署代码方式。与此同时,我们还了解了一些关于 Docker 以及容器化和自动化在发布代码方面的优势。 + +这本书的内容基本上已经讲到这里了。我们决定让这个过程成为一个建立应用程序的旅程,分别经历它的所有阶段并在需要时解决它们。这是最后一个阶段——部署,希望这能为您从编写第一行代码到部署的整个周期画上句号。 + +下一章将重点关注 Deno 接下来的发展,包括运行时和您个人方面。我希望这能让您成为 Deno 的爱好者,并且您对它以及它所开启的无限可能世界像我一样充满热情。 diff --git a/docs/deno-web-dev/deno-web-dev_09.md b/docs/deno-web-dev/deno-web-dev_09.md new file mode 100644 index 0000000..f5f948e --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_09.md @@ -0,0 +1,249 @@ +# 第八章:*第十章*:接下来是什么? + +我们已经走了很长的路。我们首先了解 Deno 的基本知识,然后构建并部署了一个完整的应用程序。到现在为止,你应该已经熟悉 Deno,并对它解决的问题有一个很好的了解。希望我们经历的所有阶段都有助于澄清你可能有关于 Deno 的许多问题。 + +我们故意选择让这本书成为一段旅程,从我们的第一个脚本开始,到完成一个部署的应用程序结束,我们在书中边编写边迭代这个应用程序。与此同时,我们解决了许多应用程序开发者可能会遇到的挑战,并提出了解决方案。 + +到现在为止,你应该已经掌握了可以帮助你决定 Deno 是否将成为你下一个项目解决方案一部分的知识。 + +本章将首先回顾我们已经学到的内容,包括所有阶段和学习点。然后,正如章节标题所暗示的,我们的重点将转向未来。本章将关注接下来会发生什么,既包括 Deno 作为一个运行时的未来,也包括作为一名开发者,你将掌握一个新工具的情况。 + +我们将简要查看 Deno 核心团队当前的优先事项,他们正在做什么,以及提议的未来功能是什么。随着章节的进行,我们还将查看社区中正在发生的事情,突出一些有趣的倡议。 + +本章将通过展示我们如何将包发布到 Deno 的官方注册表,以及其他回馈 Deno 社区的方式,来结束。 + +到本章结束时,你将熟悉以下领域: + ++ 回顾我们的旅程 + ++ 丹诺的发展路线图 + ++ 丹诺的未来和社区 + ++ 将包发布到 Deno 的官方注册表 + +# 回顾我们的旅程 + +我们已经覆盖了大量的知识点。相信这本书对您来说(希望如此)是一次有趣的旅程,从不知道 Deno 到用它构建东西,最后部署一个应用程序。 + +我们首先了解这个工具本身,首先了解它提供的功能,然后使用标准库编写简单的程序。随着我们的知识积累,我们很快就有足够的能力用它构建一个真正的应用程序,这就是我们做的。 + +冒险始于使用标准库构建最简单的 Web 服务器。我们大量使用 TypeScript 来帮助明确指定应用程序的边界,并成功运行了一个非常简单的应用程序,达到了我们第一个检查点:**你好世界**。 + +我们的应用程序不断进化,随着需求的复杂性增加,我们需要深入研究 Deno 社区中可用的网络框架。在所有这些框架中进行了高层次的比较后,根据我们的应用需求,我们选择了`oak`。下一步是把我们仍然简单的 Web 服务器迁移到我们选择的框架,这轻而易举。使用网络框架让我们的代码更简洁,并允许我们将真的不想自己处理的事情委派出去,这样我们就能专注于应用程序本身。 + +下一步是向我们的应用程序中添加用户。我们创建了应用程序端点以启用用户注册,随着存储用户的需求出现,我们将应用程序连接到 MongoDB。有了用户之后,实现用户认证就是一小步。 + +随着应用程序的增长,对更复杂配置的需求也在增长。从它运行的服务器端口到证书文件的存放位置,或者数据库凭据,所有这些都需要独立处理。我们将配置从应用程序中抽象出来,并集中管理。在此过程中,我们增加了对配置存储在文件中或环境变量中的支持。这使得可以根据环境运行具有不同配置的应用程序,同时保持敏感值远离代码库,保持安全。 + +随着我们的旅程即将结束,我们想要确保我们的代码足够可靠。这引导我们进入了一个测试章节,在那里我们学习了 Deno 的基本测试知识,并为我们所创建的应用程序的几个用例创建了不同的测试。我们从简单的单元测试做到了跨模块测试,再到启动应用程序并对其进行几次请求的测试。在这个过程中,我们对我们的代码能按预期工作有了更多的信心,并将测试能力添加到我们的工具链中。 + +为了结束,我们把写的代码变成了现实,并部署了它。 + +我们在 Heroku 的容器化环境中运行了应用程序。与此同时,我们学习了关于 Docker 的知识,以及它是如何让开发者更容易运行和部署代码的。我们用一种自动化的方式部署了 Deno 应用程序,结束了从代码到部署的这一循环。 + +这是一次经历,我们经历了应用程序开发的许多常见阶段,遇到挑战并使用适合我们用例的解决方案解决它们。我希望我已经涵盖了你们的一些主要关切和问题,给你们提供一个坚实的基础来帮助你们在未来。 + +我们不知道接下来会发生什么,但我们知道它取决于 Deno 及其社区,我们希望您认为自己也是这个社区的一部分。在下一节中,我们将看看 Deno 的未来路线图,计划的内容以及他们的短期努力方向。 + +# Deno 的路线图 + +自从 Ryan 在 JSConf 上首次介绍 Deno 以来,很多事情都发生了变化;已经迈出了几大步。随着运行时的第一个稳定版本的发布,社区爆发了,许多来自其他 JavaScript 社区的人都加入其中,带来了许多热情洋溢的想法。 + +Deno 的核心团队目前大部分精力都投入到推动 Deno 的发展上。这种贡献不仅体现在代码、问题和帮助人们上,还体现在规划和勾勒下一步行动上。 + +对于短期路线图,核心团队确保它正在跟踪倡议。以下两个在 GitHub 上提出的问题已用于跟踪 2020 年第四季度和 2021 年第一季度的努力: + ++ [`github.com/denoland/deno/issues/7915`](https://github.com/denoland/deno/issues/7915) + ++ [`github.com/denoland/deno/issues/8824`](https://github.com/denoland/deno/issues/8824) + +如果您仔细查看这些内容,可以跟踪有关这些功能的所有讨论、代码和决策。我在这里列出一些当前的倡议,让您预览一下正在发生的事情: + ++ **Deno 语言服务器协议**(**LSP**)和语言服务器 + ++ 编译为二进制文件(Deno 应用程序的单个可执行文件) + ++ 数据、blob、WebAssembly 和**JavaScript 对象表示法**(**JSON**)导入 + ++ 改进对**Web Crypto 应用程序编程接口**(**APIs**)的支持 + ++ 支持**立即执行的函数表达式**(**IIFE**)捆绑 + ++ 支持 WebGPU + ++ HTTP/2 支持 + +这些只是 Deno 进行的一些重要倡议的例子。正如您所能想象的,由于 Deno 目前处于早期阶段,目前有很多努力致力于修复漏洞和重构代码,我没有将这些添加到这个列表中。 + +请随意深入查看前面提到的 GitHub 问题,以获取有关任何倡议的更多详细信息。 + +所有这些都是 Deno 核心团队的贡献。记住,Deno 之所以存在,是因为有人在他们的业余时间致力于它。回馈社区有很多方式,无论是通过提交错误报告、代码贡献、在通讯渠道上帮助,还是通过捐赠。 + +如果 Deno 能帮助您和您的公司将想法变为现实,请考虑成为赞助商,以保持其健康并持续发展。您可以在 GitHub 上通过以下链接进行赞助:[`github.com/sponsors/denoland`](https://github.com/sponsors/denoland)。 + +还有其他也为 Deno、围绕它的热情以及它的演变负责的人,这些人就是 Deno 的社区。在下一节中,我们将介绍 Deno 的社区、那里发生的一些有趣的事情,以及您可以如何积极参与其中。 + +# Deno 的未来和社区 + +Deno 社区正在快速增长——它充满了对此感到兴奋的人,他们急于帮助它成长。随着你开始使用它,正如在这本书的过程中你所做的那样,你将能够为它做出非常重要的贡献。这可能是一个你遇到的 bug,一个对你有意义的功能,或者你只是想更好地理解的东西。 + +为了成为其中的一员,我建议你加入 Deno 的 Discord 频道([`discord.gg/deno`](https://discord.gg/deno)).这是一个非常活跃的地方,你可以找到对 Deno 感兴趣的其他人,如果你想要找到包的作者、自己构建包,或者帮助 Deno 核心,这里很有用。根据我的经验,我只能说我在那里遇到的人都非常友好和乐于助人。这也是了解正在发生的事情的好方法。 + +另一种贡献方式是关注 Deno 在 GitHub 上的仓库([`github.com/denoland`](https://github.com/denoland)).主仓库可以在[`github.com/denoland/deno`](https://github.com/denoland/deno)找到,那里你可以找到 Deno**命令行界面**(**CLI**)和 Deno 核心,而标准库则在其自己的仓库中([`github.com/denoland/deno_std`](https://github.com/denoland/deno_std))。还有其他仓库,如[`github.com/denoland/rusty_v8`](https://github.com/denoland/rusty_v8),它托管了用于 V8 JavaScript 引擎的 Deno 创建的 Rust 绑定,或者[`github.com/denoland/deno_lint`](https://github.com/denoland/deno_lint),Deno linter 托管在其中,等等。在 GitHub 上随意关注你感兴趣的仓库。 + +提示 + +了解 Deno 上正在发生的事情,而又不会收到太多通知的好方法是,只关注 Deno 的主要仓库的发布更新。每次发布你都会收到通知,你可以跟随非常详尽的发布说明。我留下一个发布说明的例子,让你知道它们长什么样。 + +这就是一个版本更新通知的样子: + +![Figure 10.1 – Deno 的 v1.6.2 发布说明](img/Figure_10.1_B16380.jpg) + +Figure 10.1 – Deno 的 v1.6.2 发布说明 + +在前面截图显示的 GitHub 发布之外,Deno 团队还努力在他们的网站上编写详尽的发布说明,这是保持更新的另一种好方法([`deno.land/posts`](https://deno.land/posts)). + +要成为 Deno 社区的重要一员,你可以做的是使用它、报告 bug 和结识新朋友,其余的将会水到渠成。 + +社区不仅由核心成员和帮助 Deno 的人组成,还包括用它构建的包和项目。 + +在接下来的部分,我将突出一些我认为很棒且正在推动社区前进的倡议。这是一个个人清单,把它当作推荐而不是更多,因为我相信还有其他倡议也可以添加进来。 + +## 社区中发生的一些有趣的事情 + +在过去的两年里,我一直关注 Deno,发生了很多事情。在 v1.0.0 发布后,随着更多的人加入,涌现了许多想法。我会列出一些我认为特别有趣,不仅因为它们提供功能,而且也是学习的好来源的倡议。 + +### Denon + +作为 Node.js 开发时的首选解决方案,Nodemon 是 Deno 开发者最常用的工具之一。如果你还没听说过它,它基本上会监控你的文件,并在你更改任何内容时重新运行你的 Deno 应用程序。它是那些在开发 Deno 时你很可能会保留在工具链中的工具之一。你可以查看他们的 GitHub 页面:[`github.com/denosaurs/denon`](https://github.com/denosaurs/denon)。 + +### Aleph.js + +虽然在这里我们没有足够的空间去探讨,但 Deno 在浏览器上运行的能力开启了一整套新的功能,这导致了像 Aleph.js 这样的倡议。这个倡议自称是 Deno 中的*React 框架*,并且已经得到了相当多的使用和热情。如果你还没听说过,它从 Next.js 框架([`nextjs.org/`](https://nextjs.org/))中拿了许多方面,实现在 Deno 中,并添加了一些其他的东西。它虽然很新,但已经有了服务器端渲染、热模块重载和文件系统及 API 路由等功能。你可以在这里了解更多:[`alephjs.org/`](https://alephjs.org/)。 + +### Nest.land + +尽管 Deno 有自己的注册表(我们将在下一节中使用),但社区还是创造了其他的注册表。Nest.land 是其中之一;它是一个基于区块链技术的模块注册表,确保托管在那里的模块不会被删除。它是免费的、去中心化的,不需要 Git 就能工作,是许多包作者的首选解决方案。了解更多关于它的信息:[`nest.land/`](https://nest.land/)。 + +### Pagic + +随着静态网站生成器越来越受欢迎,迟早会有一些用 Deno 制作的。Pagic 就是这样一个静态网站生成器,它支持 React、Vue 和 M 等有趣的功能,以及其他特性。它采用约定优于配置的方式,这意味着它非常容易启动你的第一个网站。了解更多关于它的信息:[`pagic.org/`](https://pagic.org/)。 + +### Webview_deno + +由于现在人们使用的许多应用程序都是用 JavaScript 编写的,并且运行在网页视图中,它们迟早会出现在 Deno 上。这个模块包括一个 Deno 插件,因此仍然被认为是稳定的。然而,尽管它有局限性,并且是一个正在进行的项目,它已经提供了许多与 Electron(Node.js 的替代品)一样有趣的功能。 + +除了前面提到的所有包之外,第四章 *构建一个网页应用* 中提到的所有包都值得一看。它们是快速发展的网页框架,正如我们之前探索的那样,为使用它们的开发者提供了不同的好处。如果你正在用 Deno 开发网页应用,务必关注它们。查看它们的 GitHub 页面[`github.com/webview/webview_deno`](https://github.com/webview/webview_deno)。 + +你认为 Deno 上仍然缺少什么功能吗?你开发了什么你认为对更多人有益的东西吗?开源的核心依赖于那些有趣的软件片段和背后的人。 + +你制作了什么想要分享的东西吗?不用担心——我们已经为你准备好了。在下一节,你将学会如何做到这一点! + +# 将包发布到 Deno 的官方注册表 + +开源,从根本上说,是由使用免费软件的个人和公司组成的,他们有回馈的愿望。当你创建了一段你认为有趣的代码时,你很可能会想要分享它。这不仅是帮助其他人的一种方式,也是改进你自己的代码的一种方式。 + +开源和这种分享的文化是使 Deno、Node.js 和许多其他你可能使用的技术成为现实的原因。既然这本书都是关于 Deno 的,不讨论这个话题就没有意义。 + +Deno 有一个官方的模块注册表,我们之前使用过。这是一个任何有 GitHub 账户的人都可以与社区分享自己模块的地方,它还提供了自动化和缓存机制,以保持模块的不同版本。 + +我们接下来要做的就是将我们自己的模块发布到这个相同的注册表中。 + +我们将使用一种软件,到目前为止,我们通过直接链接到 GitHub 提供它。这可行,但它既没有清晰的版本控制,也没有任何类型的缓存,如果代码从 GitHub 上删除,它就无法使用。 + +记得我们曾经使用过一个叫`jwt-auth`的包里的`AuthRepository`吗?当时,出于实际原因,我们使用了一个直接的 GitHub 链接,但从现在开始,我们将把它发布到 Deno 的模块注册表中。 + +我们将使用托管在 GitHub 上的完全相同的代码,但以`deno_web_development_jwt_auth`的名字发布。我们选择这个名字是为了让它非常清楚地表明它是这本书旅程的一部分。我们也不希望为用于学习的包 grab 注册表中的有意义的名称。 + +让我们开始吧!按照以下步骤进行: + +1. 为要发布的模块创建一个仓库。如前所述,我们将使用来自第六章的*添加认证和连接数据库*部分的`jwt-auth`模块([`github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth`](https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter06/jwt-auth)),但请随意使用您选择的任何其他模块。 + +1. 按照 GitHub 的说明克隆最近创建的`git`仓库。确保将您的模块文件复制到此仓库文件夹中,并运行以下命令(这些与 GitHub 的说明中呈现的命令相同): + + ```js + $ echo "# " >> README.md + $ git init + $ git add . + $ git commit -m "first commit" + $ git branch -M main + $ git remote add origin git@github.com:/ .git + $ git push -u origin main + ``` + +1. 前往[`deno.land/x`](https://deno.land/x)并点击**添加模块**按钮(您可能需要滚动一点才能找到它),如下所示:![图 10.2 – Deno 模块注册表中的**添加模块**按钮 ](img/Figure_10.2_B16380.jpg) + + 图 10.2 – Deno 模块注册表中的**添加模块**按钮 + +1. 在出现的框中输入模块名称,并点击`deno_web_development_jwt_auth`作为包名称,但出于显而易见的原因,你不能这样做。 + + 请记住,如果您是为了测试目的而发布模块,您应该使用一个测试名称。我们不希望使用用于测试目的的“真实”模块名称。 + +1. 在出现的下一个框中,选择代码存放的目录。 + + 对于我们即将包含来自第六章的*添加认证和连接数据库*部分的`jwt-auth`代码的模块,我们将留空,因为它位于步骤 1 中创建的新仓库的根目录下。 + +1. 现在,只需按照说明添加 webhook。 + + Deno 模块注册表使用 GitHub webhook 来获取包的更新。这些 webhook 应由新分支或标签触发,Deno 的模块注册表然后将这些 GitHub 标签创建为一个版本。 + + 接下来的说明将出现在 Deno 的页面上,但我在这里列出它们是因为实际原因: + + 导航到您想要添加到 GitHub 的仓库。 + + 前往`https://api.deno.land/webhook/gh/`(包名称应与步骤 4 中选择的名称相同)。 + + 选择`application/json`作为内容类型。 + + 选择**让我选择个别事件**。 + + 只选择**分支或标签创建**事件。 + + 点击**添加 webhook**。 + +1. 现在,只需创建一个发布,正如我们提到的,这是通过`git`标签完成的。假设您已经在步骤 2 中提交了您的包代码,我们只需要创建并推送此标签,如下所示: + + ```js + $ git tag v0.0.1 + $ git push origin --tags + Enumerating objects: 5, done. + Counting objects: 100% (5/5), done. + Delta compression using up to 8 threads + Compressing objects: 100% (3/3), done. + Writing objects: 100% (3/3), 748 bytes | 748.00 KiB/s, done. + Total 3 (delta 1), reused 0 (delta 0) + remote: Resolving deltas: 100% (1/1), completed with 1 local object. + To github.com:asantos00/deno_web_development_jwt_auth.git + * [new tag]         v0.0.1 -> v0.0.1 + ``` + +1. 现在,如果我们导航到[`deno.land/x`](https://deno.land/x)并搜索您的包名称(在我们的示例中为`deno_web_development_jwt_auth`),它应该出现在那里,正如您在以下屏幕截图中所看到的: + +![图 10.3 – Deno 模块注册表上的一个已发布包](img/Figure_10.3_B16380.jpg) + +图 10.3 – Deno 模块注册表上的一个已发布包 + +就是这样——这就是你们开始与社区分享惊人 Deno 代码所需的一切!从现在开始,你们不仅可以使用 Deno 构建应用,还可以创建包,回馈社区。 + +这一节以及整本书的内容就到这里,感谢大家坚持到最后。我们希望对你们有所帮助,帮助你们学习 Deno,也希望你们对它充满热情,就像我们一样。 + +如果你们认为我在某些方面能提供帮助,我非常乐意联系你们。可以通过书中前言提供的联系方式,通过 GitHub 或 Twitter 与我取得联系。 + +# 摘要 + +首先,感谢所有坚持读到这本书最后的人!希望它对你们来说是一次有趣的旅程,满足你们的期望,并回答了你们关于 Deno 的许多问题和担忧。 + +这只是(希望是巨大的)旅程的开始。Deno 在成长,而你们现在成为了其中的一部分。你们越使用它并回馈社区,它就会变得越好。如果,像我一样,你们认为它为编写 JavaScript 应用提供了很多好处,可以让它成为一个游戏改变者,那就不要等待,立即分享你们的热情。 + +像我们这样很多人都在推动 Deno 的发展,帮助社区,开发模块,提交拉取请求。归根结底,在你项目中恰当使用 Deno,是你能给出的最佳推荐。 + +贯穿全书,我不仅试图突出 Deno 的优势,还试图清楚地表明,它绝非(而且永远不会是)一劳永逸的解决方案。它有一系列的优势,尤其是在与 Node.js 相同的用例中(你们可以在第一章 *Deno 是什么?* 中查看)。正如我们在这章所讨论的,有很多功能正在加入,使 Deno 能应用于越来越多的用例,但我相信还有很多我们尚不知道的功能将加入。 + +从这里开始,一切就靠你了。希望这本书让你充满激情,迫不及待想写 Deno 应用。 + +下一步最好的做法就是亲自编写应用。这将使你进行研究,与人交流,解决问题。我尽量让你们的前行之路尽可能平坦,通过回答一些最常见的问题。 + +我相信网上有很多资源、文章和书籍,但要想真正提高 Deno 技能,最好的途径仍然是 Discord 频道和 GitHub 仓库。这些地方可以让你第一时间获取最新消息! + +我迫不及待想看到你们接下来会构建什么。 diff --git a/docs/deno-web-dev/deno-web-dev_10.md b/docs/deno-web-dev/deno-web-dev_10.md new file mode 100644 index 0000000..4eea3a5 --- /dev/null +++ b/docs/deno-web-dev/deno-web-dev_10.md @@ -0,0 +1,77 @@ +![](img/Packt_Logo_Orange__f36f261.png) + +[Packt.com](http://Packt.com) + +订阅我们的在线数字图书馆,全面访问 7000 多本图书和视频,以及行业领先工具,帮助您规划个人发展并推进您的职业生涯。更多信息,请访问我们的网站。 + +# 第九章:为什么要订阅? + ++ 与 4000 多名行业专业人士实践性的电子书和视频,节省学习时间,更多时间编码 + ++ 使用专门为您打造的技能计划来提升您的学习 + ++ 每月免费获得一本电子书或视频 + ++ 完全可搜索,轻松访问重要信息 + ++ 复制、打印和书签内容 + +您知道 Packt 提供每本书的电子书版本,并提供 PDF 和 ePub 文件吗?您可以在[packt.com](http://packt.com)升级到电子书版本,作为印刷书客户,您有权以折扣价购买电子书。更多详情请联系我们:customercare@packtpub.com + +在[www.packt.com](http://www.packt.com),您还可以阅读一系列免费的技术文章,订阅一系列免费新闻通讯,并获得 Packt 书籍和电子书的独家折扣和优惠。 + +# 您可能会喜欢的其他书籍 + +如果您喜欢这本书,您可能对这些由 Packt 出版的其它书籍感兴趣: + +![](https://www.packtpub.com/product/full-stack-react-typescript-and-node/9781839219931) + +**全栈 React,TypeScript 和 Node** + +大卫·乔伊 + +ISBN: 978-1-83921-993-1 + ++ 探索 TypeScript 最重要的功能以及如何使用它们来提高代码质量和可维护性 + ++ 了解 React Hooks 是什么以及如何使用它们来构建 React 应用 + ++ 使用 Redux 为您的 React 应用实现状态管理 + ++ 从零开始设置一个使用 TypeScript 和 GraphQL 的 Express 项目 + ++ 使用 React 和 GraphQL 构建一个功能齐全的在线论坛应用 + ++ 使用 Redis 为您的网络应用添加身份验证 + ++ 使用 TypeORM 从 Postgres 数据库保存和检索数据 + ++ 在 AWS 云上配置 NGINX 以部署和服务您的应用 + +![](https://www.packtpub.com/product/node-cookbook-fourth-edition/9781838558758?utm_source=github&utm_medium=repository&utm_campaign=9781838558758) + +**Node Cookbook - 第四版** + +贝瑟尼·格里格斯 + +ISBN: 978-1-83855-875-8 + ++ 了解 Node.js 异步编程模型 + ++ 使用模块和网络框架创建简单的 Node.js 应用程序 + ++ 使用 Fastify 和 Express 等网络框架开发简单的网络应用程序 + ++ 发现测试、优化和保护您的网络应用程序的技巧 + ++ 创建并部署 Node.js 微服务 + ++ 在您的 Node.js 应用程序中调试和诊断问题 + +# Packt 正在寻找像您这样的作者 + +如果你有兴趣成为 Packt 的作者,请访问[作者页面](http://authors.packtpub.com)并尽快申请。我们已经与数千名开发者和技术专业人士合作,就像你一样,帮助他们将他们的见解分享给全球技术社区。你可以提交一个普通的申请,申请一个我们正在招募作者的热门话题,或者提交你自己的想法。 + +# 留下评论 - 让其他读者知道你的看法 + +请通过在你购买本书的网站上留下评论,与其他人分享你对这本书的看法。如果你从亚马逊购买了这本书,请在本书的亚马逊页面上留下一个诚实的评论。这对其他潜在读者非常重要,他们可以看到并使用你的客观意见来做出购买决定,我们可以了解我们的客户对我们产品的看法,我们的作者可以看到他们对与 Packt 合作创作的标题的反馈。这只需要花费你几分钟的时间,但对其他潜在客户、我们的作者和 Packt 来说非常有价值。谢谢你! diff --git a/docs/dev-win-store-app-h5-js/SUMMARY.md b/docs/dev-win-store-app-h5-js/SUMMARY.md new file mode 100644 index 0000000..18e8724 --- /dev/null +++ b/docs/dev-win-store-app-h5-js/SUMMARY.md @@ -0,0 +1,12 @@ ++ [序言](dev-win-store-app-h5-js_00.md) ++ [第一章. HTML5 结构](dev-win-store-app-h5-js_01.md) ++ [第二章.使用 CSS3 进行样式设计](dev-win-store-app-h5-js_02.md) ++ [第三章. Windows 应用的 JavaScript](dev-win-store-app-h5-js_03.md) ++ [第四章。使用 JavaScript 开发应用程序](dev-win-store-app-h5-js_04.md) ++ [第五章 绑定数据到应用](dev-win-store-app-h5-js_05.md) ++ [第六章。使应用具有响应性](dev-win-store-app-h5-js_06.md) ++ [第七章. 用磁贴和通知让应用上线](dev-win-store-app-h5-js_07.md) ++ [第八章.用户登录](dev-win-store-app-h5-js_08.md) ++ [第九章. 添加菜单和命令](dev-win-store-app-h5-js_09.md) ++ [第十章。打包和发布](dev-win-store-app-h5-js_10.md) ++ [第十一章:使用 XAML 开发应用程序](dev-win-store-app-h5-js_11.md) \ No newline at end of file diff --git a/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_00.md b/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_00.md new file mode 100644 index 0000000..1072d46 --- /dev/null +++ b/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_00.md @@ -0,0 +1,101 @@ +# 序言 + +*使用 HTML5 和 JavaScript 开发 Windows Store 应用* 是一本实践性强的指南,涵盖了 Windows Store 应用的基本重要特性以及示例代码,向您展示如何开发这些特性,同时学习 HTML5 和 CSS3 中的新特性,使您能够充分利用您的网页开发技能。 + +# 本书内容覆盖范围 + +第一章, *HTML5 结构*, 介绍了新 HTML5 规范中的语义元素、媒体元素、表单元素和自定义数据属性。 + +第二章, *使用 CSS3 进行样式设计*, 介绍了 CSS3 在开发使用 JavaScript 的 Windows Store 应用时会频繁用到的增强和特性。本章涵盖了以下主题:CSS3 选择器、网格和弹性盒布局、动画和转换、以及媒体查询。 + +第三章, *Windows 应用的 JavaScript*, 介绍了 JavaScript 的 Windows 库及其特性,并突出显示了用于开发应用的命名空间和控件。 + +第四章, *使用 JavaScript 开发应用*, 介绍了开始使用 JavaScript 开发 Windows 8 应用所需的工具和提供的模板。 + +第五章, *将数据绑定到应用*, 描述了如何在应用中实现数据绑定。 + +第六章, *使应用响应式*, 描述了如何使应用响应式,以便它能处理不同屏幕尺寸和视图状态的变化,并响应缩放。 + +第七章, *使用磁贴和通知使应用活跃*, 描述了应用磁贴和通知的概念,以及如何为应用创建一个简单的通知。 + +第八章, *用户登录*, 描述了 Live Connect API 以及如何将应用与该 API 集成以实现用户认证、登录和检索用户资料信息。 + +第九章, *添加菜单和命令*, 描述了应用栏、它如何工作以及它在应用中的位置。此外,我们还将学习如何声明应用栏并向其添加控件。 + +第十章, *打包和发布*, 介绍了我们将如何了解商店并学习如何使应用经历所有阶段最终完成发布。同时,我们还将了解如何在 Visual Studio 中与商店进行交互。 + +第十一章,*使用 XAML 开发应用*,描述了其他可供开发者使用的平台和编程语言。我们还将涵盖使用 XAML/C#创建应用程序的基本知识。 + +# 本书需要你具备的知识 + +为了实施本书中将要学习的内容并开始开发 Windows Store 应用,你首先需要 Windows 8。此外,你还需要以下开发工具和工具包: + ++ 微软 Visual Studio Express 2012 用于 Windows 8 是构建 Windows 应用的工具。它包括 Windows 8 SDK、Visual Studio 的 Blend 和项目模板。 + ++ Windows App Certification Kit + ++ Live SDK + +# 本书适合谁 + +这本书适合所有想开始为 Windows 8 创建应用的开发者。此外,它针对想介绍 standards-based web technology with HTML5 和 CSS3 的进步的开发者。另外,这本书针对想利用他们在 Web 开发中的现有技能和代码资产,并将其转向为 Windows Store 构建 JavaScript 应用的 Web 开发者。简而言之,这本书适合所有想学习 Windows Store 应用开发基础的人。 + +# 约定 + +在这本书中,你会发现有几种不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。 + +文本中的代码词汇如下所示:“`createGrouped`方法在列表上创建一个分组投影,并接受三个函数参数。” + +代码块如下所示: + +```js +// Get the group key that an item belongs to. + function getGroupKey(dataItem) { + return dataItem.name.toUpperCase().charAt(0); +} + +// Get a title for a group + function getGroupData(dataItem) { + return { + title: dataItem.name.toUpperCase().charAt(0); + }; +} +``` + +**新术语**和**重要词汇**以粗体显示。例如,你在屏幕上看到的、菜单或对话框中的单词,会在文本中以这种方式出现:“你将能够为应用程序 UI 设置选项;这些选项之一是支持的旋转。” + +### 注意 + +警告或重要说明以这种方式出现在盒子里。 + +### 技巧 + +技巧和小窍门像这样出现。 + +# 读者反馈 + +读者对我们的书籍的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。 + +要发送给我们一般性反馈,只需发送电子邮件到``,并在消息主题中提及书籍标题。 + +如果你在某个主题上有专业知识,并且你对编写或贡献书籍感兴趣,请查看我们网站上的作者指南:[www.packtpub.com/authors](http://www.packtpub.com/authors)。 + +# 客户支持 + +既然你已经成为 Packt 书籍的骄傲拥有者,我们有很多东西可以帮助你充分利用你的购买。 + +## 勘误 + +尽管我们已经竭尽全力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现任何错误——可能是文本或代码中的错误——我们非常感谢您能向我们报告。这样做可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何错误,请访问[`www.packtpub.com/submit-errata`](http://www.packtpub.com/submit-errata),选择您的书籍,点击**错误提交表单**链接,并输入您的错误详情。一旦您的错误得到验证,您的提交将被接受,并且错误将被上传到我们的网站或添加到该标题下的错误列表中。您可以通过[`www.packtpub.com/support`](http://www.packtpub.com/support)选择您的标题查看现有的错误。 + +## 版权侵犯 + +互联网上版权材料的侵犯是一个持续存在的问题,涵盖所有媒体。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上以任何形式发现我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求解决方案。 + +如果您发现任何可疑的版权侵犯材料,请通过``联系我们并提供链接。 + +我们非常感谢您在保护我们的作者和我们提供有价值内容的能力方面所提供的帮助。 + +## 问题反馈 + +如果您在阅读本书的过程中遇到任何问题,可以通过``联系我们,我们会尽最大努力为您解决问题。 diff --git a/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_01.md b/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_01.md new file mode 100644 index 0000000..9332b1e --- /dev/null +++ b/docs/dev-win-store-app-h5-js/dev-win-store-app-h5-js_01.md @@ -0,0 +1,424 @@ +# 第一章. HTML5 结构 + +HTML5 引入了新的元素和属性,以更整洁的结构、更智能的表单和更丰富的媒体,这使得开发者的生活变得更加容易。HTML5 功能根据其功能分为几个组,新的结构元素属于语义组,包括结构元素、媒体元素、属性、表单类型、链接关系类型、国际化语义和附加语义的微数据。HTML5 有很多增加和增强的内容,所有这些都是为了更好地在网络上呈现内容。当你开发 Windows 8 应用时,你会使用其中许多功能;使用 Windows 8 开发的区别在于,至少在 Windows Store 应用层面,你不必担心浏览器的兼容性,因为 Windows 8 是一个使用最新网络标准的 HTML5 平台。你所使用的 HTML5 和 CSS3 的一切都为你代码中提供,并保证在应用程序中工作。最新版本的 Visual Studio(VS 2012)包括一个新 HTML 和 CSS 编辑器,提供对 HTML5 和 CSS3 元素和片段的全面支持。 + +在本章中,我们将涵盖以下主题: + ++ 语义元素 + ++ 媒体元素 + ++ 表单元素 + ++ 自定义数据属性 + +# 理解语义元素 + +HTML5 标记语义比其前辈更强,这要归功于描述页面内容结构的新语义元素。语义元素的列表包括以下内容: + ++ `
    `标签定义了文档或节的头部。它在页面或节中包裹标题或一组标题,并且它还可以包含诸如徽标、横幅和主要导航链接等信息。在页面中你可以有多个`
    `标签。 + ++ `