Helpex - Trao đổi & giúp đỡ Đăng nhập

Tìm hiểu cách sử dụng SQL Server với Node.js

Tôi có niềm đam mê với cơ sở dữ liệu quan hệ, đặc biệt là máy chủ SQL. Trong suốt sự nghiệp của mình, tôi đã bị cuốn hút vào các khía cạnh khác nhau của cơ sở dữ liệu, chẳng hạn như thiết kế, triển khai, di chuyển, tạo các thủ tục, trình kích hoạt và chế độ xem được lưu trữ một cách cẩn thận. 

Gần đây tôi đã bắt đầu xây dựng ứng dụng Node.js với SQL Server. Hôm nay, tôi sẽ chỉ cho bạn cách thực hiện trong hướng dẫn từng bước này bằng cách tạo một ứng dụng lịch đơn giản. 

Thiết lập môi trường phát triển Node.js của bạn

Trước khi bắt đầu, bạn sẽ cần một số thứ:

  • Node.js  phiên bản 8.0 trở lên.
  • Truy cập vào  SQL Server  phiên bản 2012 trở lên.

Nếu bạn chưa có phiên bản SQL Server có thể kết nối, bạn có thể cài đặt một phiên bản cục bộ để phát triển và thử nghiệm.

Cài đặt SQL Server trên Windows

Tải xuống và cài đặt  SQL Server Developer Edition .

Cài đặt SQL Server trên Mac hoặc Linux

  1. Cài đặt  Docker
  2. Chạy phần sau trong một thiết bị đầu cuối. Thao tác này sẽ tải xuống phiên bản SQL Server 2017 mới nhất dành cho Linux và tạo một vùng chứa mới có tên  sqlserver.
docker pull microsoft/mssql-server-linux:2017-latest
docker run -d --name sqlserver -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=P@55w0rd' -e 'MSSQL_PID=Developer' -p 1433:1433 microsoft/mssql-server-linux:2017-latest


Lưu ý: Để biết thêm thông tin về cách chạy SQL Server cho Linux, hãy xem  SQL Server đang chạy trên máy Mac ?!

Thiết lập cơ sở dữ liệu SQL

Bạn sẽ cần một cơ sở dữ liệu SQL cho hướng dẫn này. Nếu bạn đang chạy SQL Server cục bộ và chưa có cơ sở dữ liệu, bạn có thể tạo một cơ sở dữ liệu bằng tập lệnh sau.

Lưu ý: Nếu bạn có Visual Studio Code, bạn có thể sử dụng phần mở rộng mssql tuyệt vời   để chạy các tập lệnh SQL. Hoặc, bạn có thể sử dụng một ứng dụng như  Azure Data Studio .

USE master;
GO

CREATE DATABASE calendar; -- change this to whatever database name you desire
GO


Tiếp theo, tạo một bảng mới có tên  events. Đây là bảng bạn sẽ sử dụng để lưu trữ các sự kiện lịch.

-- Dropping events table...
DROP TABLE IF EXISTS events;

-- Create events table...
CREATE TABLE events (
   id int IDENTITY(1, 1) PRIMARY KEY CLUSTERED NOT NULL
   , userId nvarchar(50) NOT NULL
   , title nvarchar(200) NOT NULL
   , description nvarchar(1000) NULL
   , startDate date NOT NULL
   , startTime time(0) NULL
   , endDate date NULL
   , endTime time(0) NULL
   , INDEX idx_events_userId ( userId )
);


Tạo ứng dụng web Node.js

Với Node.js, bạn có thể chọn từ nhiều khuôn khổ khác nhau để tạo các ứng dụng web. Trong hướng dẫn này, bạn sẽ sử dụng  hapi , yêu thích của cá nhân tôi. Ban đầu được tạo ra bởi các kỹ sư của Walmart, nó phù hợp để xây dựng các API, dịch vụ và các ứng dụng web hoàn chỉnh.

Mở dấu nhắc lệnh (Windows) hoặc thiết bị đầu cuối (Mac hoặc Linux) và thay đổi thư mục hiện tại thành nơi bạn muốn tạo dự án của mình. Tạo một thư mục cho dự án của bạn và thay đổi thành thư mục mới.

mkdir node-sql-tutorial
cd node-sql-tutorial


Một  package.json tập tin được yêu cầu cho các dự án Node.js và bao gồm những thứ như thông tin dự án, kịch bản, và phụ thuộc. Sử dụng  npm lệnh để tạo  package.json tệp trong thư mục dự án.

npm init -y


Tiếp theo, cài đặt  hapi dưới dạng phụ thuộc.

npm install hapi@18


Bây giờ, hãy mở dự án trong trình soạn thảo mà bạn chọn.

Nếu bạn chưa có trình soạn thảo mã yêu thích, tôi khuyên bạn nên cài đặt  Visual Studio Code . VS Code có hỗ trợ đặc biệt cho JavaScript và Node.js, chẳng hạn như hoàn thành và gỡ lỗi mã thông minh. Ngoài ra còn có một thư viện lớn các tiện ích mở rộng miễn phí do cộng đồng đóng góp.

Cấu trúc dự án Node.js

Hầu hết các ví dụ “chào thế giới” về ứng dụng Node.js bắt đầu với mọi thứ trong một tệp JavaScript duy nhất. Tuy nhiên, điều cần thiết là phải thiết lập một cấu trúc dự án tốt để hỗ trợ ứng dụng của bạn khi nó phát triển.

Có vô số ý kiến ​​về cách bạn có thể tổ chức một dự án Node.js. Trong hướng dẫn này, cấu trúc dự án cuối cùng sẽ tương tự như sau.

├── package.json
├── client
├── src
│   ├── data
│   ├── plugins
│   ├── routes
│   └── views
└── test


Tạo một máy chủ cơ bản với các tuyến đường

Tạo một thư mục có tên  src. Trong thư mục này, thêm một tệp mới có tên  index.js. Mở tệp và thêm JavaScript sau.

"use strict";

const server = require( "./server" );

const startServer = async () => {
   try {
       // todo: move configuration to separate config
       const config = {
           host: "localhost",
           port: 8080
       };

       // create an instance of the server application
       const app = await server( config );

       // start the web server
       await app.start();

       console.log( `Server running at http://${ config.host }:${ config.port }...` );
   } catch ( err ) {
       console.log( "startup error:", err );
   }
};

startServer();


Tạo một tệp mới dưới  src tên  server.js. Mở tệp và thêm mã sau. 

"use strict";

const Hapi = require( "hapi" );
const routes = require( "./routes" );

const app = async config => {
   const { host, port } = config;

   // create an instance of hapi
   const server = Hapi.server( { host, port } );

   // store the config for later use
   server.app.config = config;

   // register routes
   await routes.register( server );

   return server;
};

module.exports = app;


Tách cấu hình máy chủ khỏi khởi động ứng dụng sẽ giúp việc kiểm tra ứng dụng dễ dàng hơn.

Tiếp theo, tạo một thư mục dưới  src tên  routes. Trong thư mục này, thêm một tệp mới có tên  index.js. Mở tệp và thêm mã sau. 

"use strict";

module.exports.register = async server => {
   server.route( {
       method: "GET",
       path: "/",
       handler: async ( request, h ) => {
           return "My first hapi server!";
       }
   } );
};


Cuối cùng, chỉnh sửa  package.json tệp và thay đổi "main" giá trị thuộc  tính thành  "src/index.js". Thuộc tính này hướng dẫn Node.js thực thi tệp nào khi ứng dụng khởi động.

 "main": "src/index.js"


Bây giờ, bạn có thể khởi động ứng dụng. Quay lại cửa sổ lệnh / terminal của bạn và nhập lệnh sau.

node .


Bạn sẽ thấy thông báo  Server running at http://localhost:8080.... Mở trình duyệt của bạn và điều hướng đến  http://localhost:8080. Trình duyệt của bạn sẽ hiển thị một cái gì đó như sau.

Tìm hiểu cách sử dụng SQL Server với Node.js

Máy chủ hapy đầu tiên của tôi!


Sự thành công! 

Lưu ý: Để dừng ứng dụng Node.js, hãy chuyển đến cửa sổ lệnh / terminal và nhấn  CTRL+C.

Quản lý cấu hình ứng dụng Node.js của bạn

Trước khi bắt đầu viết mã để tương tác với SQL Server, chúng ta cần một cách tốt để quản lý cấu hình ứng dụng của mình, chẳng hạn như thông tin kết nối SQL Server của chúng ta.

Các ứng dụng Node.js thường sử dụng các biến môi trường để cấu hình. Tuy nhiên, quản lý các biến môi trường có thể là một vấn đề khó khăn. dotenv là một gói Node.js phổ biến hiển thị  .env tệp cấu hình cho Node.js, như thể tất cả đều được thiết lập bằng cách sử dụng các biến môi trường.

Đầu tiên, hãy cài đặt  dotenv như một phần phụ thuộc của dự án.

npm install dotenv@6


Tạo một tệp có tên  .env trong thư mục gốc của dự án và thêm cấu hình sau.

# Set NODE_ENV=production when deploying to production
NODE_ENV=development

# hapi server configuration
PORT=8080
HOST=localhost
HOST_URL=http://localhost:8080
COOKIE_ENCRYPT_PWD=superAwesomePasswordStringThatIsAtLeast32CharactersLong!

# SQL Server connection
SQL_USER=dbuser
SQL_PASSWORD=P@55w0rd
SQL_DATABASE=calendar
SQL_SERVER=servername
# Set SQL_ENCRYPT=true if using Azure
SQL_ENCRYPT=false

# Okta configuration
OKTA_ORG_URL=https://{yourOktaDomain}
OKTA_CLIENT_ID={yourClientId}
OKTA_CLIENT_SECRET={yourClientSecret}


Cập nhật cấu hình SQL Server với thông tin cấu hình cơ sở dữ liệu của bạn. Chúng tôi sẽ đề cập đến một số cài đặt khác sau.

Lưu ý: Khi sử dụng hệ thống kiểm soát nguồn như git, không thêm  .env tệp vào kiểm soát nguồn. Mỗi môi trường yêu cầu một .env tệp tùy chỉnh  và có thể chứa các bí mật không được lưu trữ trong kho lưu trữ. Bạn nên ghi lại các giá trị mong đợi trong dự án README và trong một .env.sample tệp riêng biệt  .

Tiếp theo, tạo một tệp  src có tên  config.jsvà thêm mã sau.

"use strict";

const assert = require( "assert" );
const dotenv = require( "dotenv" );

// read in the .env file
dotenv.config();

// capture the environment variables the application needs
const { PORT,
   HOST,
   HOST_URL,
   COOKIE_ENCRYPT_PWD,
   SQL_SERVER,
   SQL_DATABASE,
   SQL_USER,
   SQL_PASSWORD,
   OKTA_ORG_URL,
   OKTA_CLIENT_ID,
   OKTA_CLIENT_SECRET
} = process.env;

const sqlEncrypt = process.env.SQL_ENCRYPT === "true";

// validate the required configuration information
assert( PORT, "PORT configuration is required." );
assert( HOST, "HOST configuration is required." );
assert( HOST_URL, "HOST_URL configuration is required." );
assert( COOKIE_ENCRYPT_PWD, "COOKIE_ENCRYPT_PWD configuration is required." );
assert( SQL_SERVER, "SQL_SERVER configuration is required." );
assert( SQL_DATABASE, "SQL_DATABASE configuration is required." );
assert( SQL_USER, "SQL_USER configuration is required." );
assert( SQL_PASSWORD, "SQL_PASSWORD configuration is required." );
assert( OKTA_ORG_URL, "OKTA_ORG_URL configuration is required." );
assert( OKTA_CLIENT_ID, "OKTA_CLIENT_ID configuration is required." );
assert( OKTA_CLIENT_SECRET, "OKTA_CLIENT_SECRET configuration is required." );

// export the configuration information
module.exports = {
   port: PORT,
   host: HOST,
   url: HOST_URL,
   cookiePwd: COOKIE_ENCRYPT_PWD,
   sql: {
       server: SQL_SERVER,
       database: SQL_DATABASE,
       user: SQL_USER,
       password: SQL_PASSWORD,
       options: {
           encrypt: sqlEncrypt
       }
   },
   okta: {
       url: OKTA_ORG_URL,
       clientId: OKTA_CLIENT_ID,
       clientSecret: OKTA_CLIENT_SECRET
   }
};


Cập nhật  src/index.js để sử dụng config mô-đun mới  bạn vừa tạo.

"use strict";

const config = require( "./config" );
const server = require( "./server" );

const startServer = async () => {
   try {
       // create an instance of the server application
       const app = await server( config );

       // start the web server
       await app.start();

       console.log( `Server running at http://${ config.host }:${ config.port }...` );
   } catch ( err ) {
       console.log( "startup error:", err );
   }
};

startServer();


Tạo một API Node.js với SQL Server

Bây giờ chúng ta có thể đến phần thú vị! Trong bước này, bạn sẽ thêm một tuyến vào hapi để truy vấn cơ sở dữ liệu cho danh sách các sự kiện và trả về chúng dưới dạng JSON. Bạn sẽ tạo một plugin máy khách SQL Server cho hapi và tổ chức lớp truy cập dữ liệu theo cách giúp dễ dàng thêm các API mới trong tương lai.

Đầu tiên, bạn cần cài đặt một vài phụ thuộc, quan trọng nhất là  mssql gói.

npm install mssql@4 fs-extra@7


Tạo lớp truy cập dữ liệu SQL

Sử dụng SQL Server với Node.js và  mssql gói thường làm theo các bước sau:

  1. Tạo một phiên bản của  mssql gói.
  2. Tạo kết nối SQL với  connect().
  3. Sử dụng kết nối để tạo SQL mới  request.
  4. Đặt bất kỳ thông số đầu vào nào theo yêu cầu.
  5. Thực hiện yêu cầu.
  6. Xử lý kết quả (ví dụ: tập bản ghi) do yêu cầu trả về.

Tạo kết nối tới SQL Server là một hoạt động tương đối tốn kém. Cũng có một giới hạn thực tế đối với số lượng kết nối có thể được thiết lập. Theo mặc định,   hàm mssql của gói  .connect()tạo và trả về một đối tượng "nhóm" kết nối. Nhóm kết nối làm tăng hiệu suất và khả năng mở rộng của ứng dụng.

Khi một truy vấn  request được tạo, máy khách SQL sử dụng kết nối có sẵn tiếp theo trong nhóm. Sau khi truy vấn được thực thi, kết nối được trả về kết nối của nhóm.

Tạo một thư mục dưới  src tên  data. Tạo một tệp mới dưới  src/data tên  index.js. Thêm mã sau vào tệp này.

"use strict";

const events = require( "./events" );
const sql = require( "mssql" );

const client = async ( server, config ) => {
   let pool = null;

   const closePool = async () => {
       try {
           // try to close the connection pool
           await pool.close();

           // set the pool to null to ensure
           // a new one will be created by getConnection()
           pool = null;
       } catch ( err ) {
           // error closing the connection (could already be closed)
           // set the pool to null to ensure
           // a new one will be created by getConnection()
           pool = null;
           server.log( [ "error", "data" ], "closePool error" );
           server.log( [ "error", "data" ], err );
       }
   };

   const getConnection = async () => {
       try {
           if ( pool ) {
               // has the connection pool already been created?
               // if so, return the existing pool
               return pool;
           }
           // create a new connection pool
           pool = await sql.connect( config );

           // catch any connection errors and close the pool
           pool.on( "error", async err => {
               server.log( [ "error", "data" ], "connection pool error" );
               server.log( [ "error", "data" ], err );
               await closePool();
           } );
           return pool;
       } catch ( err ) {
           // error connecting to SQL Server
           server.log( [ "error", "data" ], "error connecting to sql server" );
           server.log( [ "error", "data" ], err );
           pool = null;
       }
   };

   // this is the API the client exposes to the rest
   // of the application
   return {
       events: await events.register( { sql, getConnection } )
   };
};

module.exports = client;


Khi sử dụng SQL Server với Node.js, một trong những điều quan trọng nhất cần làm đúng là xử lý đúng cách các lỗi kết nối khi chúng xảy ra. Bên trong,  sql/data mô-đun có hai chức năng quan trọng:  getConnection và  closePoolgetConnection trả về nhóm kết nối đang hoạt động hoặc tạo một nhóm nếu cần. Khi bất kỳ lỗi kết nối nào xảy ra,  closePool hãy đảm bảo rằng nhóm hoạt động trước đó được xử lý để ngăn mô-đun sử dụng lại nó.

Tạo một tệp mới dưới  src/data tên  utils.js. Thêm mã sau vào tệp này.

"use strict";

const fse = require( "fs-extra" );
const { join } = require( "path" );

const loadSqlQueries = async folderName => {
   // determine the file path for the folder
   const filePath = join( process.cwd(), "src", "data", folderName );

   // get a list of all the files in the folder
   const files = await fse.readdir( filePath );

   // only files that have the .sql extension
   const sqlFiles = files.filter( f => f.endsWith( ".sql" ) );

   // loop over the files and read in their contents
   const queries = {};
   for ( let i = 0; i < sqlFiles.length; i++ ) {
       const query = fse.readFileSync( join( filePath, sqlFiles[ i ] ), { encoding: "UTF-8" } );
       queries[ sqlFiles[ i ].replace( ".sql", "" ) ] = query;
   }
   return queries;
};

module.exports = {
   loadSqlQueries
};


Mặc dù có thể nhúng các truy vấn SQL dưới dạng chuỗi trong mã JavaScript, nhưng tôi tin rằng tốt hơn nên giữ các truy vấn trong .sql các tệp riêng biệt  và tải chúng khi khởi động. utils Mô-đun này  tải tất cả các  .sql tệp trong một thư mục nhất định và trả về chúng dưới dạng một đối tượng duy nhất.

Tạo một thư mục mới dưới  src/data tên  events. Thêm một tệp mới dưới  src/data/events tên  index.js. Thêm mã sau vào tệp này.

"use strict";

const utils = require( "../utils" );

const register = async ( { sql, getConnection } ) => {
   // read in all the .sql files for this folder
   const sqlQueries = await utils.loadSqlQueries( "events" );

   const getEvents = async userId => {
       // get a connection to SQL Server
       const cnx = await getConnection();

       // create a new request
       const request = await cnx.request();

       // configure sql query parameters
       request.input( "userId", sql.VarChar( 50 ), userId );

       // return the executed query
       return request.query( sqlQueries.getEvents );
   };

   return {
       getEvents
   };
};

module.exports = { register };


Thêm một tệp mới dưới  src/data/events tên  getEvents.sql. Thêm SQL sau vào tệp này.

SELECT  [id]
       , [title]
       , [description]
       , [startDate]
       , [startTime]
       , [endDate]
       , [endTime]
FROM    [dbo].[events]
WHERE   [userId] = @userId
ORDER BY
       [startDate], [startTime];


Lưu ý trong hai tệp cuối cùng rằng bạn đang sử dụng truy vấn được tham số hóa, truyền  @userId dưới dạng tham số được đặt tên, bảo vệ chống lại các cuộc tấn công chèn SQL.

Tạo một Plugin Máy khách Cơ sở dữ liệu

Tiếp theo, bạn sẽ thêm một plugin ứng dụng khách cơ sở dữ liệu để dễ dàng chạy các truy vấn SQL từ các phần khác của ứng dụng, chẳng hạn như khi người dùng yêu cầu một API. Trong các khung công tác khác, khái niệm này có thể được gọi là phần mềm trung gian, nhưng hapi sử dụng thuật ngữ plugin.

Tạo một thư mục mới dưới  src tên  plugins. Tạo một tệp mới dưới  src/plugins tên    index.js. Thêm mã sau.

"use strict";

const sql = require( "./sql" );

module.exports.register = async server => {
   // register plugins
   await server.register( sql );
};


Tạo một tệp mới dưới  src/plugins tên  sql.js. Thêm mã sau.

"use strict";

// import the data access layer
const dataClient = require( "../data" );

module.exports = {
   name: "sql",
   version: "1.0.0",
   register: async server => {
       // get the sql connection information
       const config = server.app.config.sql;

       // create an instance of the database client
       const client = await dataClient( server, config );

       // "expose" the client so it is available everywhere "server" is available
       server.expose( "client", client );
   }
};


Tiếp theo, cập nhật  src/server.js để đăng ký plugin.

"use strict";

const Hapi = require( "hapi" );
const plugins = require( "./plugins" );
const routes = require( "./routes" );

const app = async config => {
   const { host, port } = config;

   // create an instance of hapi
   const server = Hapi.server( { host, port } );

   // store the config for later use
   server.app.config = config;

   // register plugins
   await plugins.register( server );

   // register routes
   await routes.register( server );

   return server;
};

module.exports = app;


Thêm một tuyến API

Bây giờ bạn sẽ thêm một tuyến API sẽ thực thi  getEvents truy vấn và trả về kết quả dưới dạng JSON. Bạn có thể thêm tuyến đường hiện có  src/routes/index.js. Tuy nhiên, khi một ứng dụng phát triển, sẽ tốt hơn nếu tách các tuyến đường thành các mô-đun chứa các tài nguyên liên quan.

Tạo một thư mục mới dưới  src/routes tên  api. Dưới đây  src/routes/api, hãy tạo một tệp mới có tên  index.js. Thêm mã sau vào tệp này.

"use strict";

const events = require( "./events" );

module.exports.register = async server => {
   await events.register( server );
};


Tạo một tệp mới dưới  src/routes/api tên  events.js. Thêm mã sau vào tệp này.

"use strict";

module.exports.register = async server => {
   server.route( {
       method: "GET",
       path: "/api/events",
       config: {
           handler: async request => {
               try {
                   // get the sql client registered as a plugin
                   const db = request.server.plugins.sql.client;

                   // TODO: Get the current authenticate user's ID
                   const userId = "user1234";

                   // execute the query
                   const res = await db.events.getEvents( userId );

                   // return the recordset object
                   return res.recordset;
               } catch ( err ) {
                   console.log( err );
               }
           }
       }
   } );
};


Bây giờ cập nhật  src/routes/index.js để đăng ký các api tuyến mới  .

"use strict";

const api = require( "./api" );

module.exports.register = async server => {
   // register api routes
   await api.register( server );

   server.route( {
       method: "GET",
       path: "/",
       handler: async ( request, h ) => {
           return "My first hapi server!";
       }
   } );
};


Chà! Bạn đã gần tới! Chèn một vài bản ghi thử nghiệm vào cơ sở dữ liệu của bạn.

INSERT INTO [dbo].[events]
( userId, title, description, startDate, startTime, endDate, endTime )
VALUES
( 'user1234', N'doctor appt', N'Stuff', '2019-10-03', '14:30', NULL, NULL )
, ( 'user1234', N'conference', N'', '2019-09-17', NULL, '2019-09-20', NULL )


Khởi động máy chủ web từ cửa sổ lệnh / terminal.

node .


Bây giờ, điều hướng trình duyệt của bạn đến  http://localhost:8080/api/events. Nếu mọi thứ được thiết lập chính xác, bạn sẽ thấy một mảng JavaScript gồm các bản ghi mà bạn vừa chèn vào!

Tìm hiểu cách sử dụng SQL Server với Node.js

Kết quả API đầu tiên


Thêm xác thực vào ứng dụng Node.js của bạn

Hãy có một số người dùng thực sự trong ứng dụng! Xây dựng thủ công xác thực và quản lý hồ sơ người dùng cho bất kỳ ứng dụng nào không phải là nhiệm vụ tầm thường. Và, làm sai có thể dẫn đến kết quả thảm hại. Okta đến giải cứu!

Để hoàn thành bước này, bạn cần có tài khoản nhà phát triển Okta. Tới  Okta Developer Portal  và đăng ký một tài khoản Okta mãi mãi miễn phí.

Okta đăng ký


Sau khi tạo tài khoản, hãy nhấp vào liên kết Ứng dụng ở trên cùng, sau đó nhấp vào Thêm ứng dụng.

Thêm ứng dụng


Tiếp theo, chọn Ứng dụng web và nhấp vào Tiếp theo.

Thêm ứng dụng web


Nhập tên cho ứng dụng của bạn, chẳng hạn như Node-SQL. Sau đó, nhấn Xong để hoàn tất quá trình tạo ứng dụng.

Cài đặt ứng dụng


Ở gần cuối trang ứng dụng, bạn sẽ tìm thấy một phần có tiêu đề Thông tin đăng nhập của Khách hàng. Sao chép các giá trị bí mật của Client ID và Client và dán chúng vào .env tệp của bạn  để thay thế  {yourClientId} và  {yourClientSecret}tương ứng.

Thông tin đăng nhập của khách hàng


Nhấp vào liên kết Trang tổng quan. Ở phía bên phải của trang, bạn sẽ tìm thấy URL tổ chức của mình. Sao chép giá trị này vào .env tệp của bạn  để thay thế giá trị cho  OKTA_ORG_URL.

URL tổ chức của bạn


Tiếp theo, kích hoạt đăng ký tự phục vụ. Điều này sẽ cho phép người dùng mới tạo tài khoản của riêng họ. Nhấp vào menu Người dùng và chọn Đăng ký.

Đăng ký người dùng


Nhấp vào nút Chỉnh sửa.

  1. Thay đổi đăng ký Tự phục vụ thành Đã bật.
  2. Nhấp vào nút Lưu ở cuối biểu mẫu.

Bật đăng ký tự phục vụ


Xây dựng giao diện người dùng với JavaScript được nhúng và Vue.js

Trong các bước tiếp theo này, bạn sẽ thêm giao diện người dùng vào ứng dụng Node.js của mình bằng cách sử dụng các mẫu JavaScript nhúng (EJS) và Vue.js.

Đầu tiên, bạn sẽ cài đặt một số phụ thuộc cần thiết để hỗ trợ xác thực, hiển thị mẫu và cung cấp tệp tĩnh.

npm install bell@9 boom@7 ejs@2 hapi-auth-cookie@9 inert@5 vision@5


Đăng ký giao diện người dùng và plugin xác thực

Bạn sẽ sử dụng  bell để xác thực với Okta và  hapi-auth-cookie để quản lý các phiên của người dùng. Tạo một tệp dưới  src/plugins tên  auth.js và thêm mã sau.

"use strict";

const bell = require( "bell" );
const authCookie = require( "hapi-auth-cookie" );

const isSecure = process.env.NODE_ENV === "production";

module.exports.register = async server => {
   // register plugins
   const config = server.app.config;
   await server.register( [ authCookie, bell ] );

   // configure cookie authorization strategy
   server.auth.strategy( "session", "cookie", {
       password: config.cookiePwd,
       redirectTo: "/authorization-code/callback", // If there is no session, redirect here
       isSecure // Should be set to true (which is the default) in production
   } );

   // configure bell to use your Okta authorization server
   server.auth.strategy( "okta", "bell", {
       provider: "okta",
       config: { uri: config.okta.url },
       password: config.cookiePwd,
       isSecure,
       location: config.url,
       clientId: config.okta.clientId,
       clientSecret: config.okta.clientSecret
   } );
};


Tiếp theo, bạn sẽ cập nhật  src/plugins/index.js để đăng ký  auth.js mô-đun và thêm hỗ trợ phục vụ các tệp liên quan đến giao diện người dùng.

"use strict";

const ejs = require( "ejs" );
const inert = require( "inert" );
const { join } = require( "path" );
const vision = require( "vision" );

const auth = require( "./auth" );
const sql = require( "./sql" );

const isDev = process.env.NODE_ENV !== "production";

module.exports.register = async server => {
   // register plugins
   await server.register( [ inert, sql, vision ] );

   // configure ejs view templates
   const filePath = join( process.cwd(), "src" );
   server.views( {
       engines: { ejs },
       relativeTo: filePath,
       path: "views",
       layout: true
   } );

   // register authentication plugins
   await auth.register( server );
};


Các  inert plugin được sử dụng để phục vụ các tập tin tĩnh, và  vision thêm hỗ trợ cho dựng hình mẫu server-side. Ở đây,  ejs được định cấu hình làm công cụ mẫu.

Thêm chế độ xem máy chủ

Tạo một thư mục dưới  src tên  views. Dưới  src/views thêm một tệp mới có tên  layout.ejs và thêm mã sau.

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <title><%= title %></title>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
   <link rel="stylesheet" href="/index.css">
</head>
<body>
   <% include partials/navigation %>
   <%- content %>
   <script src="/index.js"></script>
</body>
</html>


Thêm tệp mới vào  src/views tên  index.ejsvà thêm mã sau.

<div class="container">
   <% if ( isAuthenticated ) { %>
       <div id="app"></div>
   <% } else { %>
       <h1 class="header"><%= title %></h1>
       <p><%= message %></p>
   <% } %>
</div>


Tạo một thư mục mới dưới  src/views tên  partials. Bên dưới  src/views/partials, thêm một tệp mới có tên  navigation.ejsvà thêm mã sau.

<nav>
   <div class="nav-wrapper">
       <ul class="left">
           <% if ( isAuthenticated ) { %>
           <li><a class="waves-effect waves-light btn" href="/logout">Logout</a></li>
           <% } else { %>
           <li><a class="waves-effect waves-light btn" href="/login">Login</a></li>
           <% } %>
       </ul>
   </div>
</nav>


Cập nhật các tuyến để hỗ trợ chế độ xem và xác thực

Bên dưới  src/routes, thêm một tệp mới có tên  auth.js. Thêm mã sau vào tệp này.

"use strict";

const boom = require( "boom" );

module.exports.register = async server => {
   // login route
   server.route( {
       method: "GET",
       path: "/login",
       options: {
           auth: "session",
           handler: async request => {
               return `Hello, ${ request.auth.credentials.profile.email }!`;
           }
       }
   } );

   // OIDC callback
   server.route( {
       method: "GET",
       path: "/authorization-code/callback",
       options: {
           auth: "okta",
           handler: ( request, h ) => {
               if ( !request.auth.isAuthenticated ) {
                   throw boom.unauthorized( `Authentication failed: ${ request.auth.error.message }` );
               }
               request.cookieAuth.set( request.auth.credentials );
               return h.redirect( "/" );
           }
       }
   } );

   // Logout
   server.route( {
       method: "GET",
       path: "/logout",
       options: {
           auth: {
               strategy: "session",
               mode: "try"
           },
           handler: ( request, h ) => {
               try {
                   if ( request.auth.isAuthenticated ) {
                       // const idToken = encodeURI( request.auth.credentials.token );

                       // clear the local session
                       request.cookieAuth.clear();
                       // redirect to the Okta logout to completely clear the session
                       // const oktaLogout = `${ process.env.OKTA_ORG_URL }/oauth2/default/v1/logout?id_token_hint=${ idToken }&post_logout_redirect_uri=${ process.env.HOST_URL }`;
                       // return h.redirect( oktaLogout );
                   }

                   return h.redirect( "/" );
               } catch ( err ) {
                   request.log( [ "error", "logout" ], err );
               }
           }
       }
   } );
};


Bây giờ, hãy chỉnh sửa  src/routes/index.js để thay đổi trang chủ để nó hiển thị chế độ xem EJS mới.

"use strict";

const api = require( "./api" );
const auth = require( "./auth" );

module.exports.register = async server => {
   // register api routes
   await api.register( server );

   // register authentication routes
   await auth.register( server );

   // home page route
   server.route( {
       method: "GET",
       path: "/",
       config: {
           auth: {
               strategy: "session",
               mode: "optional"
           }
       },
       handler: async ( request, h ) => {
           try {
               const message = request.auth.isAuthenticated ? `Hello, ${ request.auth.credentials.profile.firstName }!` : "My first hapi server!";
               return h.view( "index", {
                   title: "Home",
                   message,
                   isAuthenticated: request.auth.isAuthenticated
               } );
           } catch ( err ) {
               server.log( [ "error", "home" ], err );
           }
       }
   } );

   // Serve static files in the /dist folder
   server.route( {
       method: "GET",
       path: "/{param*}",
       handler: {
           directory: {
               path: "dist"
           }
       }
   } );
};


Cập nhật các tuyến API và thêm truy vấn SQL

Bạn cần cập nhật API ứng dụng để truy vấn cơ sở dữ liệu dựa trên người dùng hiện đang đăng nhập. Ở mức tối thiểu, bạn cần các tuyến để tạo, cập nhật và xóa các sự kiện, cùng với các truy vấn SQL tương ứng của chúng.

Tạo một tệp mới dưới  src/data/events tên  addEvent.sql. Thêm SQL sau vào tệp này.

INSERT INTO [dbo].[events]
(
   [userId]
   , [title]
   , [description]
   , [startDate]
   , [startTime]
   , [endDate]
   , [endTime]
)
VALUES
(
   @userId
   , @title
   , @description
   , @startDate
   , @startTime
   , @endDate
   , @endTime
);

SELECT SCOPE_IDENTITY() AS id;


Tạo một tệp mới dưới  src/data/events tên  updateEvent.sql. Thêm SQL sau vào tệp này.

UPDATE  [dbo].[events]
SET     [title] = @title
       , [description] = @description
       , [startDate] = startDate
       , [startTime] = @startTime
       , [endDate] = @endDate
       , [endTime] = @endTime
WHERE   [id] = @id
 AND   [userId] = @userId;

SELECT  [id]
       , [title]
       , [description]
       , [startDate]
       , [startTime]
       , [endDate]
       , [endTime]
FROM    [dbo].[events]
WHERE   [id] = @id
 AND   [userId] = @userId;


Tạo một tệp mới dưới  src/data/events tên  deleteEvent.sql. Thêm SQL sau vào tệp này.

DELETE  [dbo].[events]
WHERE   [id] = @id
 AND   [userId] = @userId;


Cập nhật  src/data/events/index.js để chứa mã sau.

"use strict";

const utils = require( "../utils" );

const register = async ( { sql, getConnection } ) => {
   // read in all the .sql files for this folder
   const sqlQueries = await utils.loadSqlQueries( "events" );

   const getEvents = async userId => {
       // get a connection to SQL Server
       const cnx = await getConnection();

       // create a new request
       const request = await cnx.request();

       // configure sql query parameters
       request.input( "userId", sql.VarChar( 50 ), userId );

       // return the executed query
       return request.query( sqlQueries.getEvents );
   };

   const addEvent = async ( { userId, title, description, startDate, startTime, endDate, endTime } ) => {
       const pool = await getConnection();
       const request = await pool.request();
       request.input( "userId", sql.VarChar( 50 ), userId );
       request.input( "title", sql.NVarChar( 200 ), title );
       request.input( "description", sql.NVarChar( 1000 ), description );
       request.input( "startDate", sql.Date, startDate );
       request.input( "startTime", sql.Time, startTime );
       request.input( "endDate", sql.Date, endDate );
       request.input( "endTime", sql.Time, endTime );
       return request.query( sqlQueries.addEvent );
   };

   const updateEvent = async ( { id, userId, title, description, startDate, startTime, endDate, endTime } ) => {
       const pool = await getConnection();
       const request = await pool.request();
       request.input( "id", sql.Int, id );
       request.input( "userId", sql.VarChar( 50 ), userId );
       request.input( "title", sql.NVarChar( 200 ), title );
       request.input( "description", sql.NVarChar( 1000 ), description );
       request.input( "startDate", sql.Date, startDate );
       request.input( "startTime", sql.Time, startTime );
       request.input( "endDate", sql.Date, endDate );
       request.input( "endTime", sql.Time, endTime );
       return request.query( sqlQueries.updateEvent );
   };

   const deleteEvent = async ( { id, userId } ) => {
       const pool = await getConnection();
       const request = await pool.request();
       request.input( "id", sql.Int, id );
       request.input( "userId", sql.VarChar( 50 ), userId );
       return request.query( sqlQueries.deleteEvent );
   };

   return {
       addEvent,
       deleteEvent,
       getEvents,
       updateEvent
   };
};

module.exports = { register };


Cập nhật  src/routes/api/events.js để chứa mã sau.

"use strict";

const boom = require( "boom" );

module.exports.register = async server => {
   server.route( {
       method: "GET",
       path: "/api/events",
       config: {
           auth: {
               strategy: "session",
               mode: "required"
           },
           handler: async request => {
               try {
                   // get the sql client registered as a plugin
                   const db = request.server.plugins.sql.client;

                   // get the current authenticated user's id
                   const userId = request.auth.credentials.profile.id;

                   // execute the query
                   const res = await db.events.getEvents( userId );

                   // return the recordset object
                   return res.recordset;
               } catch ( err ) {
                   server.log( [ "error", "api", "events" ], err );
                   return boom.boomify( err );
               }
           }
       }
   } );

   server.route( {
       method: "POST",
       path: "/api/events",
       config: {
           auth: {
               strategy: "session",
               mode: "required"
           },
           handler: async request => {
               try {
                   const db = request.server.plugins.sql.client;
                   const userId = request.auth.credentials.profile.id;
                   const { startDate, startTime, endDate, endTime, title, description } = request.payload;
                   const res = await db.events.addEvent( { userId, startDate, startTime, endDate, endTime, title, description } );
                   return res.recordset[ 0 ];
               } catch ( err ) {
                   server.log( [ "error", "api", "events" ], err );
                   return boom.boomify( err );
               }
           }
       }
   } );

   server.route( {
       method: "DELETE",
       path: "/api/events/{id}",
       config: {
           auth: {
               strategy: "session",
               mode: "required"
           },
           response: {
               emptyStatusCode: 204
           },
           handler: async request => {
               try {
                   const id = request.params.id;
                   const userId = request.auth.credentials.profile.id;
                   const db = request.server.plugins.sql.client;
                   const res = await db.events.deleteEvent( { id, userId } );
                   return res.rowsAffected[ 0 ] === 1 ? "" : boom.notFound();
               } catch ( err ) {
                   server.log( [ "error", "api", "events" ], err );
                   return boom.boomify( err );
               }
           }
       }
   } );
};


Thêm Vue.js

Đầu tiên, hãy cài đặt các gói phụ thuộc cho Vue.js và các gói khác được sử dụng cho giao diện người dùng.

npm install axios@0.18 luxon@1 materialize-css@1 moment@2 vue@2 vue-datetime@latest weekstart@1


Tạo một thư mục mới ở thư mục gốc của dự án có tên  client. Trong thư mục này, thêm một tệp mới có tên  index.js. Thêm mã sau vào tệp này.

import Datetime from "vue-datetime";
import Vue from "vue";
import "materialize-css";
import "materialize-css/dist/css/materialize.min.css";
import "vue-datetime/dist/vue-datetime.css";

import App from "./App";

Vue.use( Datetime );

new Vue( { // eslint-disable-line no-new
 el: "#app",
 render: h => h( App )
} );


Thêm một tệp mới vào  client tên  App.vue. Thêm mã sau vào tệp này.

<template>
 <div id="app">
   <h1>{{ msg }}</h1>
   <div class="row" id="eventList">
       <h2>Event List</h2>
       <table v-if="hasEvents">
           <thead>
               <tr>
                   <th>Start</th>
                   <th>End</th>
                   <th>Title</th>
                   <th>Description</th>
                   <th></th>
               </tr>
           </thead>
           <tbody>
               <tr v-for="event in events" :key="event.id">
                   <td>{{ event.startDate }} {{ event.startTime }}</td>
                   <td>{{ event.endDate }} {{ event.endTime }}</td>
                   <td>{{ event.title }}</td>
                   <td>{{ event.description }}</td>
                   <td>
                       <button id="eventDelete" @click="confirmDeleteEvent(event.id)" class="btn-small"><i class="material-icons right">delete</i>Delete</button>
                   </td>
               </tr>
           </tbody>
       </table>
       <p v-if="noEvents">No events yet!</p>
   </div>
   <div class="row" id="eventEdit">
       <h2>Add an Event</h2>
       <form class="col s12" @submit.prevent="addEvent">
           <div class="row">
               <div class="input-field col s6">
                   <span class="datetime-label">Start Date</span>
                   <datetime v-model="startDate" input-id="startDate" type="date" value-zone="local" input-class="validate"></datetime>
                   <!-- <label for="startDate" class="datetime-label">Start Date</label> -->
               </div>
               <div class="input-field col s6">
                   <span class="datetime-label">Time</span>
                   <datetime v-model="startTime" input-id="startTime" type="time" minute-step="5" use12-hour="true" value-zone="local" input-class="validate"></datetime>
                   <!-- <label for="startTime" class="datetime-label">Time</label> -->
               </div>
           </div>
           <div class="row">
               <div class="input-field col s6">
                   <span class="datetime-label">End Date</span>
                   <datetime v-model="endDate" input-id="endDate" type="date" value-zone="local" input-class="validate"></datetime>
                   <!-- <label for="endDate">End Date</label> -->
               </div>
               <div class="input-field col s6">
                   <span class="datetime-label">Time</span>
                   <datetime v-model="endTime" input-id="endTime" type="time" minute-step="5" use12-hour="true" value-zone="local" input-class="validate"></datetime>
                   <!-- <input v-model="endTime" ref="endTime" placeholder="" id="endTime" type="text" class="validate"> -->
                   <!-- <label for="endTime">Time</label> -->
               </div>
           </div>
           <div class="row">
               <div class="input-field col s12">
                   <input v-model="title" ref="title" placeholder="Appointment" id="title" type="text" class="validate">
                   <label for="title">Title</label>
               </div>
           </div>
           <div class="row">
               <div class="input-field col s12">
                   <input v-model="description" ref="description" placeholder="Description" id="description" type="text" class="validate">
                   <label for="description">Description</label>
               </div>
           </div>
           <button id="eventEditSubmit" class="btn" type="submit"><i class="material-icons right">send</i>Submit</button>
       </form>
   </div>
   <div id="deleteConfirm" ref="deleteConfirm" class="modal">
       <div class="modal-content">
           <h2>Confirm delete</h2>
           <p>Delete {{ selectedEvent }}?</p>
       </div>
       <div class="modal-footer">
           <button @click="deleteEvent(selectedEventId)" class="modal-close btn-flat">Ok</button>
           <button class="modal-close btn-flat">Cancel</button>
       </div>
   </div>
 </div>
</template>

<script>
import axios from "axios";
import * as M from "materialize-css";
import moment from "moment";

export default {
 name: "app",
 computed: {
   hasEvents() {
     return this.isLoading === false && this.events.length > 0;
   },
   noEvents() {
     return this.isLoading === false && this.events.length === 0;
   }
 },
 data() {
   return {
     title: "",
     description: "",
     events: [],
     isLoading: true,
     startDate: "",
     startTime: "",
     endDate: "",
     endTime: "",
     selectedEvent: "",
     selectedEventId: 0
   };
 },
 methods: {
   addEvent() {
     const event = {
       startDate: this.startDate ? moment( this.startDate ).format( "YYYY-MM-DD" ) : null,
       startTime: this.startTime ? moment( this.startTime ).format( "YYYY-MM-DD HH:mm:00" ) : null,
       endDate: this.endDate ? moment( this.endDate ).format( "YYYY-MM-DD" ) : null,
       endTime: this.endTime ? moment( this.endTime ).format( "YYYY-MM-DD HH:mm:00" ) : null,
       title: this.title,
       description: this.description
     };
     axios
       .post( "/api/events", event )
       .then( () => {
         this.startDate = "";
         this.startTime = "";
         this.endDate = "";
         this.endTime = "";
         this.title = "";
         this.description = "";
         this.loadEvents();
       } )
       .catch( err => {
         this.msg = err.message;
         console.log( err );
       } );
   },
   confirmDeleteEvent( id ) {
     const event = this.events.find( e => e.id === id );
     this.selectedEvent = `'${ event.title }' on ${ event.startDate }${ event.startTime ? ` at ${ event.startTime }` : "" }`;
     this.selectedEventId = event.id;
     const dc = this.$refs.deleteConfirm;
     const modal = M.Modal.init( dc );
     modal.open();
   },
   deleteEvent( id ) {
     axios
       .delete( `/api/events/${ id }` )
       .then( this.loadEvents )
       .catch( err => {
         this.msg = err.message;
         console.log( err );
         this.loadEvents();
       } );
   },
   formatDate( d ) {
     return d ? moment.utc( d ).format( "MMM D, YYYY" ) : "";
   },
   formatTime( t ) {
     return t ? moment( t ).format( "h:mm a" ) : "";
   },
   formatEvents( events ) {
     return events.map( event => {
       return {
         id: event.id,
         title: event.title,
         description: event.description,
         startDate: this.formatDate( event.startDate ),
         startTime: this.formatTime( event.startTime ),
         endDate: this.formatDate( event.endDate ),
         endTime: this.formatTime( event.endTime )
       };
     } );
   },
   loadEvents() {
     axios
       .get( "/api/events" )
       .then( res => {
         this.isLoading = false;
         this.events = this.formatEvents( res.data );
       } )
       .catch( err => {
         this.msg = err.message;
         console.log( err );
       } );
   }
 },
 mounted() {
   return this.loadEvents();
 }
};
</script>

<style lang="css">
#app h2 {
 font-size: 2rem;
}
.datetime-label {
 color: #9e9e9e;
 font-size: .8rem;
}
</style>


Thêm quy trình xây dựng

Cần phải tạo một quy trình xây dựng để chuyển đổi và đóng gói giao diện người dùng của khách hàng thành các định dạng tương thích với hầu hết các trình duyệt. Đối với các ứng dụng Node.js, các bước xây dựng này thường được thêm vào  package.jsontệp bên dưới  scripts.

Trước tiên, hãy cài đặt các gói bạn sẽ cần để xây dựng các tệp khách hàng.

npm install --save-dev nodemon@1 npm-run-all@4 parcel-bundler@1 @vue/component-compiler-utils@2 vue-template-compiler@2


Lưu ý: Đối  --save-dev số hướng dẫn  npm cài đặt chúng dưới dạng  phụ thuộc của nhà phát triển thay vì phụ thuộc được yêu cầu cho quá trình sản xuất trong thời gian chạy.

Bây giờ, hãy sửa đổi  package.json và thay đổi  scripts phần để phù hợp với phần sau.

 "scripts": {
   "build": "parcel build client/index.js",
   "dev:start": "npm-run-all build start",
   "dev": "nodemon --watch client --watch src -e js,ejs,sql,vue,css --exec npm run dev:start",
   "start": "node .",
   "test": "echo \"Error: no test specified\" && exit 1"
 },


Bạn có thể chạy bất kỳ kịch bản được xác định từ lệnh / thiết bị đầu cuối sử dụng  npm run [label] nơi  label là một trong các nhãn được xác định dưới  scripts. Ví dụ, bạn có thể chạy chỉ  build bước bằng cách sử dụng  npm run build.

Nhân tiện,  nodemon là một tiện ích tuyệt vời theo dõi các thay đổi đối với tệp và tự động khởi động lại ứng dụng Node.js. Bây giờ bạn có thể bắt đầu quá trình xây dựng mới và khởi chạy ứng dụng web bằng một lệnh.

npm run dev


Bản giới thiệu lịch


Tôi hy vọng bạn thích học cách sử dụng SQL Server với Node.js! Bạn nhận được mã nguồn cuối cùng cho dự án này trên  GitHub , mã này cũng bao gồm một số tính năng bổ sung, chẳng hạn như ví dụ về các bài kiểm tra và tác vụ để tự động khởi tạo cơ sở dữ liệu SQL.

Tìm hiểu thêm về Node.js và SQL

Bạn muốn tìm hiểu thêm về Node.js? Kiểm tra một số tài nguyên hữu ích này!

Theo dõi chúng tôi để biết thêm nội dung hay và cập nhật từ nhóm của chúng tôi! Bạn có thể tìm thấy chúng tôi trên  TwitterFacebook và  LinkedIn . Câu hỏi? Đánh giá chúng tôi trong các bình luận bên dưới.

Xây dựng ứng dụng Secure Node.js với SQL Server ban đầu được xuất bản trên Blog nhà phát triển Okta vào ngày 11 tháng 3 năm 2019. 

17 hữu ích 0 bình luận 50k xem chia sẻ

Có thể bạn quan tâm

loading