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
- Cài đặt Docker
- 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.
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.js
và 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:
- Tạo một phiên bản của
mssql
gói. - Tạo kết nối SQL với
connect()
. - Sử dụng kết nối để tạo SQL mới
request
. - Đặt bất kỳ thông số đầu vào nào theo yêu cầu.
- Thực hiện yêu cầu.
- 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à closePool
. getConnection
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!
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í.
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.
Tiếp theo, chọn Ứng dụng web và nhấp vào Tiếp theo.
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.
Ở 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.
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
.
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ý.
Nhấp vào nút Chỉnh sửa.
- Thay đổi đăng ký Tự phục vụ thành Đã bật.
- Nhấp vào nút Lưu ở cuối biểu mẫu.
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.ejs
và 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.ejs
và 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.json
tệ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ẫnnpm
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
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!
- Sử dụng TypeScript để xây dựng một API Node với Express
- Xác thực mã thông báo hiện đại trong Node với Express
- Xây dựng ứng dụng CRUD cơ bản với Angular và Node
- Xác thực nút đơn giản
- Xây dựng ứng dụng CRUD với ASP.NET Core và Angular
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 Twitter , Facebook 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.
Có thể bạn quan tâm
