erui _ eruie 002使用Express在Node中构建您的第一个路由器
Ťhis article was originally published on the Okta developer blog. Thank you for supporting the partners who make SitePoint possible.
files/
├── images/
│ ├── cat.png
│ ├── dog.jpg
│ └── pig.bmp
└── text/
├── README.md
└── todo.txt
You could then run a simple HTTP server that will automatically serve those files and create an index for the directories. There’s no files/index.html
, but the server is still generating a web page and serving up content based on the files in that folder. If you go to /images/cow.gif
you’ll get a 404 error – even though there’s no file there, it’s still serving up something.
npm install -g http-server
cd files
http-server
In Express, a route consists of a method
, a path
, and a handler
.
The method
could be any HTTP verb, such as GET
(for fetching content – this is what most web pages use), or POST
(for sending content to the server – this is common with HTML forms). You can also specify that you want Express to handle the same path for all methods if you choose.
The path
is a string or a regular expression that describes the relative URL. If you’re working with the root of your app, this describes the absolute URL. A path can be defined in a few ways.
- Simple Strings: A string of
'/'
specifies that you want to use this route at the root of your router. A string of'/asdf'
would cover the path/asdf
- Wildcards: The string can also contain a few wildcards, which work similar to a regular expression, but are a bit limited:
-
?
: A?
says that the previous character is optional. The path'/Joh?n'
would cover both/Jon
and/John
-
+
: A+
says that the previous character can be repeated as many times as you like, but has to be at least once. A path of'/ni+ce'
would cover/nice
as well as/niiiiiiiiiiiiiiiiice
-
*
: A*
says that the previous character is optional and can be repeated as often as you like. A path of'/wow!*'
would match/wow
,/wow!
, or even/wow!!!!!!!!!!!!
-
()
: You can also apply wildcards to a group of characters.'/(ha)+'
would match/ha
,/haha
, and/hahahahaha
, but not/hah
-
- Regular Expressions: If you want to go beyond basic wildcards, you can go nuts with a regular expression. With
/^\/(pen-)?((pine)?apple-)+pen$/
you could match/apple-pen
,/pineapple-pen
, or/pen-pineapple-apple-pen
. - Parameters: Another very useful feature, is you can have parameters in your route. This lets you easily provide RESTful URLs with dynamic portions. A path of
'/posts/:postId'
will not only match/posts/42
, but the request will contain aparams.postId
variable with a value of'42'
.
The method and path are essential to know when to do something, but the handler is the callback function that actually gets called in those cases. A handler is passed a request
, a response
, and a next
callback, and those arguments are typically written as (req, res, next)
..
- Request (
req
): The request contains all kinds of information about what’s been asked by the user. From here you can access the path, parameters, headers, and a myriad of other things. For everything on a request, you can consult the API reference - Response (
res
): The response is how you send information back to the user. The simplest way to send back data is with the.send
method (e.g.res.send('Hello, world!')
), but there are many other methods. Again, you can find all the methods in the API reference - Next Callback (
next
): Thenext
function allows you to use multiple handlers for the same route. You can use one handler to process information, and when it’s done it can callnext()
to signal it’s OK to move on to the next handler. If you pass in a string, it will instead throw an error, which you can catch elsewhere, or display to the user (e.g.next('You must be authenticated to access this route')
).
When using a router, you can think in terms of a root path, even if you’re going to be using that router from some subpath. For example, say you have an API to manage messages. You could have a router with a path '/'
to GET
all messages or POST
a new message. You could have another path '/:id'
to GET
or PUT
(edit) a specific message.
Your app could then take that router and host it at /messages
, with app.use('/messages', messageRouter)
. The router itself doesn’t have to care what its global path is going to be, and can even be used in multiple routes (e.g. /messages
, /texts
, and /email
).
Enough talk already… let’s get to some real code. To get started, create a folder that will house all your code. Then set up a package.json
folder to help manage dependencies. You can use npm init
to do this. You’ll also need to install Express.
mkdir my-first-router
cd my-first-router
npm init -y
npm install [email protected] [email protected]
Create an index.js
file with the following code:
const express = require('express')
const path = require('path')
const app = express()
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')
app.get('/', (req, res) => {
res.render('index', {
title: 'Hello, world!',
content: 'How are you?'
})
})
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`App listening on port ${port}`))
This tells Express to use Handlebars (hbs
) as a view engine. It uses Node’s built-in path
to tell it the directory containing the views. The /
path is told to render the page using index.hbs
, which will put the content
in a paragraph (p
) tag.
To make sure Express has templates to render, create a new folder called views
, then create a new file in there called layout.hbs
. When you tell Express to render a view, it will first render layout.hbs
and put the content of the view inside the {{{body}}}
tag. This lets you set up a skeleton for the app. Here’s some basic HTML using Bootstrap that will give you some nice styling without needing to write any CSS. This will also render the title
passed into the context in your /
route.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<main>
{{{body}}}
</main>
</body>
</html>
You’ll also need to create a index.hbs
view that will just be really basic for now:
<p>{{content}}</p>
To make development a little easier, you can install nodemon
with:
npm install --save-dev [email protected]
Then modify your package.json
file so that the "scripts"
entry includes a start script with nodemon .
. This will make it so that you can simply run npm start
and your server will automatically restart whenever you make changes:
"scripts": {
"start": "nodemon ."
}
Now in your terminal, if you type npm start
you’ll start the server. You can then go to http://localhost:3000
to see the app running.
Well, that’s kind of boring. How about making it do something useful? Let’s create a simple to-do list. Start by creating a router to manage a list of items. Make a new file called todo.js
:
const express = require('express')
const router = express.Router()
let todo = []
router.post('/', (req, res, next) => {
todo = [...req.body.todo || []]
if (req.body.remove) todo.splice(req.body.remove, 1)
if (req.body.new) todo.push({})
next()
})
router.use('/', (req, res) => {
res.render('todo', { title: 'To-do list', todo })
})
module.exports = router
Here you have two route handlers. The first one listens for POST
requests (signified by router.post
). It will replace the to-do list with a copy of whatever it receives from the form. If the form contains the remove
property (containing an index), it will use splice
to remove the element at that index. If the form contains the new
property, a new item will be pushed on to the array. After it’s done modifying the to-do list, it calls next()
to move on to the next route handler.
The second route handler is always used (signified by router.use
). Its sole purpose is to render the to-do list. By separating the routes like this, you can easily do one thing always, and another thing only in certain circumstances (in this case on a POST
request).
To tell the app to use this router, you’ll have to add a few lines to index.js
:
@@ -1,11 +1,15 @@
const express = require('express')
const path = require('path')
+const todoRouter = require('./todo')
const app = express()
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')
+app.use(express.urlencoded({ extended: true }))
+app.use('/todo', todoRouter)
+
app.get('/', (req, res) => {
res.render('index', {
title: 'Hello, world!',
Now for the todo
template. It’s a little larger, so I saved it for last. If you’re familiar with HTML it shouldn’t be too bad to follow. Handlebars adds a few features that let you access variables. In this case, you’re using an {{#if}}
block to render something special if there aren’t any items, as well as an {{#each}}
block to render each of the list items with minimal markup.
<form method="post">
<div class="row">
<div class="col">
<button hidden>Autosave</button>
<button class="btn btn-success" name="new" value="true">New</button>
</div>
</div>
<div class="row mt-3">
<div class="col">
{{#if todo.length}}
<ul class="list-group">
{{#each todo}}
<li class="list-group-item d-flex align-items-center">
<input
type="checkbox"
οnchange="this.form.submit()"
name="todo[{{@index}}][checked]"
{{#if this.checked}}checked{{/if}}
/>
<input
name="todo[{{@index}}][text]"
οnchange="this.form.submit()"
class="form-control mx-2"
value="{{this.text}}"
/>
<button class="btn btn-danger" name="remove" value="{{@index}}">Remove</button>
</li>
{{/each}}
</ul>
{{else}}
<h5>Your To-Do List is empty</h5>
{{/if}}
</div>
</div>
<style>
input[type=checkbox]:checked + input {
text-decoration: line-through;
opacity: 0.75;
}
</style>
</form>
Now go to http://localhost:3000/todo
and enter some items into your todo list.
添加用户不必费劲。 实际上,只需使用Okta即可完成。什么是Okta?,您可能会问。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。
If you don’t already have one, sign up for a forever-free developer account.
You’re going to need to save some information to use in the app. Create a new file named .env
. In it, enter your organization URL.
HOST_URL=http://localhost:3000
OKTA_ORG_URL=https://{yourOktaOrgUrl}
echo -e "\nAPP_SECRET=`npx -q uuid`" >> .env
Next, log in to your developer console, navigate to Applications, then click Add Application. Select Web, then click Next. Give your application a name, like “My First Router”. Change the Base URI to http://localhost:3000/
and the Login redirect URI to http://localhost:3000/authorization-code/callback
, then click Done
Click Edit and add a Logout redirect URI of http://localhost:3000/
, then click Save.
The page you come to after creating an application has some more information you need to save to your .env
file. Copy in the client ID and client secret.
OKTA_CLIENT_ID={yourClientId}
OKTA_CLIENT_SECRET={yourClientSecret}
Now back to the code. You’ll need to add Okta’s OIDC middleware to control authentication. It also relies on using sessions. You’ll need to use dotenv
to read in variables from the .env
file. To install the dependencies you’ll need, run this command:
npm install @okta/[email protected] [email protected] [email protected]
Now modify your index.js
file. Here you’ll be adding the session and OIDC middlewares, and a logout
route so users can log out of the app. You’re also adding a middleware specifically to the todoRouter
(app.use('/todo', oidc.ensureAuthenticated(), todoRouter)
). By adding oidc.ensureAuthenticated()
, you’re letting Okta make sure that route can’t be reached unless a user is logged in. If the user isn’t logged in and tries to reach that route, they’ll be taken to a secure site to log in, and redirected back to your site afterward.
@@ -1,14 +1,46 @@
+require('dotenv').config()
+
const express = require('express')
const path = require('path')
+const session = require('express-session')
+const { ExpressOIDC } = require('@okta/oidc-middleware')
+
const todoRouter = require('./todo')
+const oidc = new ExpressOIDC({
+ issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
+ client_id: process.env.OKTA_CLIENT_ID,
+ client_secret: process.env.OKTA_CLIENT_SECRET,
+ redirect_uri: `${process.env.HOST_URL}/authorization-code/callback`,
+ scope: 'openid profile'
+})
+
const app = express()
+app.use(session({
+ secret: process.env.APP_SECRET,
+ resave: true,
+ saveUninitialized: false
+}))
+app.use(oidc.router)
+
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')
app.use(express.urlencoded({ extended: true }))
-app.use('/todo', todoRouter)
+app.use('/todo', oidc.ensureAuthenticated(), todoRouter)
+
+app.get('/logout', (req, res) => {
+ if (req.userContext) {
+ const idToken = req.userContext.tokens.id_token
+ const to = encodeURI(process.env.HOST_URL)
+ const params = `id_token_hint=${idToken}&post_logout_redirect_uri=${to}`
+ req.logout()
+ res.redirect(`${process.env.OKTA_ORG_URL}/oauth2/default/v1/logout?${params}`)
+ } else {
+ res.redirect('/')
+ }
+})
app.get('/', (req, res) => {
res.render('index', {
<p>{{content}}</p>
<a href="/todo">Go to To-Do List</a>
You can also add a welcome message and a log out button to your layout.hbs
.
@@ -12,6 +12,12 @@
</head>
<body class="container">
<h1>{{title}}</h1>
+ {{#if userinfo}}
+ <h4>
+ Welcome back, {{userinfo.given_name}}!
+ <small><a href="/logout">Click here to log out</a></small>
+ </h4>
+ {{/if}}
<main>
{{{body}}}
</main>
For that to work, you’ll need to add userinfo
to the context when rendering views.
--- a/todo.js
+++ b/todo.js
@@ -13,7 +13,7 @@ router.post('/', (req, res, next) => {
})
router.use('/', (req, res) => {
- res.render('todo', { title: 'To-do list', todo })
+ res.render('todo', { title: 'To-do list', todo, userinfo: req.userContext.userinfo })
})
module.exports = router
@@ -43,7 +43,10 @@ app.get('/logout', (req, res) => {
})
app.get('/', (req, res) => {
+ const { userinfo } = req.userContext || {}
+
res.render('index', {
+ userinfo,
title: 'Hello, world!',
content: 'How are you?'
})
OK, so now you’re requiring users to log in before they can edit the to-do list, but it’s still a single, shared list. In order to split it up into a separate list for each user, make another small change to todo.js
.
@@ -2,17 +2,21 @@ const express = require('express')
const router = express.Router()
-let todo = []
+const todosByUser = {}
router.post('/', (req, res, next) => {
- todo = [...req.body.todo || []]
+ const todo = [...req.body.todo || []]
if (req.body.remove) todo.splice(req.body.remove, 1)
if (req.body.new) todo.push({})
+ todosByUser[req.userContext.userinfo.sub] = todo
+
next()
})
router.use('/', (req, res) => {
+ const todo = todosByUser[req.userContext.userinfo.sub] || []
+
res.render('todo', { title: 'To-do list', todo, userinfo: req.userContext.userinfo })
})
Now that you have a fully functional to-do list, I encourage you to expand on it. Try storing the data in a database, or even let Okta store it for you! See if you can create some more routers to add to the web server.
If you want to see the final code sample, you can find it on GitHub.
- Build and Understand Express Middleware through Examples
- Build and Understand a Simple Node.js Website with User Authentication
- Build a Simple REST API with Node and OAuth 2.0
- Build Secure Node Authentication with Passport.js and OpenID Connect
- Secure a Node API with OAuth 2.0 Client Credentials
If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.
from: https://www.sitepoint.com//build-your-first-router-in-node-with-express/
上一篇: PHP读取纯真IP数据库的函数