nodejs构建vue
In the previous parts of the series, we looked at how to create the User Interface of our Invoicing Application that allowed users to create and view existing invoices. In this final part of the series, we will look at the last important parts that haven’t been visited.
在本系列的前一部分中,我们研究了如何创建发票应用程序的用户界面,该界面允许用户创建和查看现有发票。 在本系列的最后一部分,我们将介绍尚未访问的最后重要部分。
- Persisting User Session on Client 在客户端上保留用户会话
- Single View for Invoices 发票的单一视图
- Sending Invoices using Node Mailer 使用Node Mailer发送发票
先决条件 ( Prerequisites )
To follow this article adequately, you need the following:
要充分了解本文,您需要满足以下条件:
- Node installed on your machine. 节点已安装在您的计算机上。
- NPM installed on your machine. NPM安装在您的计算机上。
- To have read the first and second parts of this series. 阅读了本系列的第一部分和第二部分。
To confirm your installation, run the following command:
要确认安装,请运行以下命令:
node --versionnpm --version
If you get their version numbers as results then you're good to go.
如果您得到它们的版本号作为结果,那么您就很好了。
使用JWToken在客户端上保留用户会话 ( Persisting User Sessions on Client Using JWTokens )
To verify that our application is secure and only authorized users can make requests, we are going to make use of JWTokens. JWT - JSON Web Tokens consist of a 3 part string containing the header, payload and signature of the request. In the core idea of it is to create a token for each authenticated user to use when performing requests to the backend server.
为了验证我们的应用程序是否安全并且只有授权用户可以发出请求,我们将使用JWTokens。 JWT-JSON Web令牌由三部分组成,包含请求的标头,有效负载和签名。 其核心思想是为每个经过身份验证的用户创建一个令牌,以在执行对后端服务器的请求时使用。
Creating JWTokens To get started, change directory into the existing invoicing``-``app
- if you don’t already have it, feel free to go to the previous article chapters or grab the code from the Github repository. After doing that, install the jsonwebtoken
node module that will be used to create and verify our JSON Web Tokens:
创建JWTokens首先,将目录更改为现有的invoicing``-``app
-如果您还没有invoicing``-``app
,请随时转到invoicing``-``app
或从Github存储库中获取代码。 之后,安装jsonwebtoken
节点模块,该模块将用于创建和验证我们的JSON Web令牌:
cd invoicing-app
npm install jsonwebtoken nodemon --save
nodemon
is a node module that restarts your server once file changes occur.
nodemon
是一个节点模块,一旦发生文件更改,它将重新启动服务器。
Now, update the server.js
file by adding the following:
现在,通过添加以下内容来更新server.js
文件:
// server.js
// import node modules
[...]
const jwt = require("jsonwebtoken");
// create express app
[...]
app.set('appSecret', 'secretforinvoicingapp'); // this will be used later
Next thing to do is to tweak the /register
and /login
routes to create tokens and pass them back once a user has successfully registered or logged in. To do this, add the following to your server.js
:
下一步要做的是调整/register
和/login
路由以创建令牌,并在用户成功注册或登录后将其传递回。为此,请将以下内容添加到server.js
:
// server.js
// edit the /register route
app.post("/register", multipartMiddleware, function(req, res) {
// check to make sure none of the fields are empty
[...]
bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
// create sql query
[...]
db.run(sql, function(err) {
if (err) {
throw err;
} else {
let user_id = this.lastID;
let query = `SELECT * FROM users WHERE id='${user_id}'`;
db.all(query, [], (err, rows) => {
if (err) {
throw err;
}
let user = rows[0];
delete user.password;
// create payload for JWT
const payload = {
user: user
}
// create token
let token = jwt.sign(payload, app.get("appSecret"), {
expiresInMinutes: "24h" // expires in 24 hours
});
// send response back to client
return res.json({
status: true,
token : token
});
});
}
});
db.close();
});
});
[...]
We do the same in the /login
route:
我们在/login
路由中执行相同的操作:
// server.js
app.post("/login", multipartMiddleware, function(req, res) {
// connect to db
[...]
db.all(sql, [], (err, rows) => {
// attempt to authenticate the user
[...]
if (authenticated) {
// create payload for JWT
const payload = { user: user };
// create token
let token = jwt.sign( payload, app.get("appSecret"),{
expiresIn: "24h" // expires in 24 hours
});
return res.json({
status: true,
token: token
});
}
return res.json({
status: false,
message: "Wrong Password, please retry"
});
});
});
Now that this is done, the next thing to do is to test it. Run your server using the command:
现在已经完成了,接下来要做的就是对其进行测试。 使用以下命令运行服务器:
nodemon server.js
Head over to Postman to test the endpoint and this what you get:
前往Postman测试端点,您将获得以下结果:
![Retrieving Token after Login](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524727296054_image.png)
Now that we’ve seen how to create tokens on successful login and registration. The next part is to very token for incoming requests. To do this, add the following middleware above the routes you want to protect:
现在,我们已经了解了如何在成功登录和注册后创建令牌。 接下来的部分是非常令牌的传入请求。 为此,请在要保护的路由上方添加以下中间件:
// server.js
[...]
// unprotected routes
[...]
// Create middleware for protecting routes
app.use(function(req, res, next) {
// check header or url parameters or post parameters for token
let token =
req.body.token || req.query.token || req.headers["x-access-token"];
// decode token
if (token) {
// verifies secret and checks exp
jwt.verify(token, app.get("appSecret"), function(err, decoded) {
if (err) {
return res.json({
success: false,
message: "Failed to authenticate token."
});
} else {
// if everything is good, save to request for use in other routes
req.decoded = decoded;
next();
}
});
} else {
// if there is no token
// return an error
return res.status(403).send({
success: false,
message: "No token provided."
});
}
});
// protected routes
[...]
![Middleware Rejecting Requests without Token](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524728150464_image.png)
Using JWTokens with Frontend In the SignUp.vue
where authentication was set up, we need to store the token obtained from the server and the user data in the localStorage
so that the this can persist across different pages when the user is using our application. To do this, update the login
and register
methods of your frontend/src/components/SignUp.vue
to look like this:
将JWToken与前端一起使用在设置身份验证的SignUp.vue
中,我们需要将从服务器获取的令牌和用户数据存储在localStorage
以便当用户使用我们的应用程序时,该令牌可以在不同页面上持久保存。 为此,请更新frontend/src/components/SignUp.vue
的login
和register
方法,如下所示:
// frontend/src/components/SignUp.vue
[...]
export default {
name: "SignUp",
[...]
methods:{
register(){
const formData = new FormData();
let valid = this.validate();
if(valid){
// prepare formData
[...]
// Post to server
axios.post("http://localhost:3128/register", formData)
.then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
// store the user token and user data in localStorage
localStorage.setItem('token', res.data.token);
localStorage.setItem('user', JSON.stringify(res.data.user));
// now send the user to the next route
this.$router.push({
name: "Dashboard",
});
} else {
this.status = res.data.message;
}
});
}
else{
alert("Passwords do not match");
}
}
[...]
Let’s also update the login method:
让我们还更新登录方法:
// frontend/src/components/SignUp.vue
login() {
const formData = new FormData();
formData.append("email", this.model.email);
formData.append("password", this.model.password);
this.loading = "Signing in";
// Post to server
axios.post("http://localhost:3128/login", formData).then(res => {
// Post a status message
console.log(res);
this.loading = "";
if (res.data.status == true) {
// store the data in localStorage
localStorage.setItem("token", res.data.token);
localStorage.setItem("user", JSON.stringify(res.data.user));
// now send the user to the next route
this.$router.push({
name: "Dashboard"
});
} else {
this.status = res.data.message;
}
});
The
localStorage
only accepts Strings so to get around that,JSON.stringify
to convert to a JSON String
localStorage
只接受字符串,因此要解决此问题,请使用JSON.stringify
转换为JSON字符串
So the user data and token are stored in the Local storage and then the user is routed to the next component. Previously, user data was passed using route parameters, but now, we get the data from the local Storage. Let’s see how this changes our components.
因此,用户数据和令牌存储在本地存储中,然后将用户路由到下一个组件。 以前,用户数据是使用路由参数传递的,但是现在,我们从本地存储中获取数据。 让我们看看这如何改变我们的组件。
The Dashboard
component earlier looked like this:
Dashboard
组件之前看起来像这样:
// frontend/src/components/Dashboard.vue<script>
import Header from "./Header";
import CreateInvoice from "./CreateInvoice";
import ViewInvoices from "./ViewInvoices";
export default {
name: "Dashboard",
components: {
Header,
CreateInvoice,
ViewInvoices,
},
data() {
return {
isactive: 'create',
title: "Invoicing App",
user : (this.$route.params.user) ? this.$route.params.user : null
};
}
};
</script>
What this meant was that, when a user signs in/up, they go redirected to the Dashboard page and then the user
property of the Dashboard component is updated accordingly. The main issue with this though is that if the user then decides to refresh the page, there’s no way to tell who the user is because now, the this.$route.params.user
that was used to identify the user earlier on no longer exists.
这意味着,当用户登录/登录时,他们将重定向到“仪表板”页面,然后相应地更新了“仪表板”组件的user
属性。 但是,主要的问题是,如果用户随后决定刷新页面,则无法分辨用户是谁,因为现在,用于更早标识用户的this.$route.params.user
不再存在。存在。
Edit your dashboard component to now use the browsers localStorage like this:
编辑仪表板组件,以现在使用浏览器localStorage像这样:
// frontend/src/components/Dashboard.vue
import Header from "./Header";
import CreateInvoice from "./CreateInvoice";
import ViewInvoices from "./ViewInvoices";
export default {
name: "Dashboard",
components: {
Header,
CreateInvoice,
ViewInvoices,
},
data() {
return {
isactive: 'create',
title: "Invoicing App",
user : null,
};
},
mounted(){
this.user = JSON.parse(localStorage.getItem("user"));
}
};
Now, when we sign in and refresh the page, we still have the user data. This is not all though, when requests are being made, we have to add the token to our requests.
现在,当我们登录并刷新页面时,我们仍然拥有用户数据。 但这还不是全部,当发出请求时,我们必须将令牌添加到我们的请求中。
Let’s take a look at the ViewInvoices
component. Here’s what the JavaScript for the component looks like:
让我们看一下ViewInvoices
组件。 组件JavaScript如下所示:
// frontend/src/components/ViewInvoices.vue<script>
import axios from "axios";
export default {
name: "ViewInvoices",
components: {},
data() {
return {
invoices: [],
\ user: '',
};
},
mounted() {
this.user = JSON.parse(localStorage.getItem('user'));
axios
.get(`http://localhost:3128/invoice/user/${this.user.id}`)
.then(res => {
if (res.data.status == true) {
console.log(res.data.invoices);
this.invoices = res.data.invoices;
}
});
}
};
</script>
Right now, when you attempt to view invoices for a logged in user, you get this error:
现在,当您尝试查看已登录用户的发票时,出现以下错误:
![Error Retrieving Invoices Due To Absence of Tokens](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524730921573_Screenshot+from+2018-04-26+08-19-36.png)
This is because the route invoice/user/:user_id
route of the application is now protected with the token middleware as was set up earlier. To fix the error, let’s add it to our request as follows:
这是因为应用程序的路由invoice/user/:user_id
路由现在已由令牌中间件保护,如先前设置的那样。 要解决该错误,请按如下所示将其添加到我们的请求中:
// frontend/src/components/ViewInvoices.vue<script>
import axios from "axios";
export default {
name: "ViewInvoices",
components: {},
data() {
return {
invoices: [],
user: '',
};
},
mounted() {
this.user = JSON.parse(localStorage.getItem('user'));
axios
.get(`http://localhost:3128/invoice/user/${this.user.id}`,
{
headers: {"x-access-token": localStorage.getItem("token")}
}
)
.then(res => {
if (res.data.status == true) {
console.log(res.data.invoices);
this.invoices = res.data.invoices;
}
});
}
};
</script>
When you save this and go back to your browser, you can now fetch the invoices successfully:
保存并返回浏览器后,现在可以成功获取发票了:
![Token Added to Request to Retrieve invoices](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524731791127_Screenshot+from+2018-04-26+08-36-08.png)
发票的单一视图 ( Single View for Invoices )
At the moment, when the TO INVOICE
button is clicked, nothing happens. To fix this, create a SingleInvoice.vue
and edit it as follows:
此刻,当单击TO INVOICE
按钮时,什么也没有发生。 要解决此问题,请创建SingleInvoice.vue
并按如下所示进行编辑:
<template>
<div class="single-page">
<Header v-bind:user="user"/>
<!-- display invoice data -->
<div class="invoice">
<!-- display invoice name here -->
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Invoice #{{ invoice.id }} by {{ user.company_name }}</h3>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Transaction Name</th>
<th scope="col">Price ($)</th>
</tr>
</thead>
<tbody>
<template v-for="txn in transactions">
<tr :key="txn.id">
<th>{{ txn.id }}</th>
<td>{{ txn.name }}</td>
<td>{{ txn.price }} </td>
</tr>
</template>
</tbody>
<tfoot>
<td></td>
<td style="text-align: right">Total :</td>
<td><strong>$ {{ total_price }}</strong></td>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
The SingleInvoice component has the following template. The v-for
directive is used to allow us loop through all the fetched transactions for the particular invoice. Let’s see how to obtain the invoice and transaction data from the backend server.
SingleInvoice组件具有以下模板。 v-for
指令用于允许我们遍历特定发票的所有提取的交易。 让我们看看如何从后端服务器获取发票和交易数据。
The component structure looks like below. We first import the necessary modules and components. When the component is mounted
, a POST
request using axios
is made to the backend server to fetch the data. When the response is obtained, we assign them to the respective component properties.
组件结构如下所示。 我们首先导入必要的模块和组件。 mounted
组件后,将使用axios
向后端服务器发出POST
请求以获取数据。 获得响应后,我们将它们分配给相应的组件属性。
<script>
import Header from "./Header";
import axios from "axios";
export default {
name: "SingleInvoice",
components: {
Header
},
data() {
return {
invoice: {},
transactions: [],
user: "",
total_price: 0
};
},
methods: {
send() {}
},
mounted() {
// make request to fetch invoice data
this.user = JSON.parse(localStorage.getItem("user"));
let token = localStorage.getItem("token");
let invoice_id = this.$route.params.invoice_id;
axios
.get(`http://localhost:3128/invoice/user/${this.user.id}/${invoice_id}`, {
headers: {
"x-access-token": token
}
})
.then(res => {
if (res.data.status == true) {
this.transactions = res.data.transactions;
this.invoice = res.data.invoice;
let total = 0;
this.transactions.forEach(element => {
total += parseInt(element.price);
});
this.total_price = total;
}
});
}
};
</script>
Notice: There’s a
send()
method in the method that is currently empty. As you move on through the article, you would get a better understanding as to why and how to add the necessary functionality注意:该方法中当前有一个
send()
方法为空。 在继续阅读本文时,您将更好地理解为什么以及如何添加必要的功能
The component has the following scoped styles:
该组件具有以下作用域样式:
<!-- frontend/src/components/SingleInvoice.vue -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #426cb9;
}
.single-page {
background-color: #ffffffe5;
}
.invoice{
margin-top: 20px;
}
</style>
Now, if you back to the application and click the TO INVOICE
button in the View Invoices
tab, you get view below:
现在,如果你回到应用程序,然后单击TO INVOICE
的按钮View Invoices
选项卡,你会得到如下观点:
![Single Invoice View](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524841007559_singleInvoice.png)
通过电子邮件发送发票 ( Sending Invoice via Email )
This is the final step of our mini-invoicing application is to allow our users send invoices to whoever they like. To keep this part as simple as possible, we are going to use the nodemailer
module. This module allows you send emails to specified recipients using an email on the backend server. To get started, first install the module:
这是我们的微型发票应用程序的最后一步,就是允许我们的用户将发票发送给他们喜欢的任何人。 为了使这一部分尽可能简单,我们将使用nodemailer
模块。 该模块允许您使用后端服务器上的电子邮件将电子邮件发送给指定的收件人。 首先,请先安装模块:
npm install nodemailer
Now that the module is installed, we update the server.js
file as follows:
现在已经安装了模块,我们如下更新server.js
文件:
// server.js
// import node modules
[...]
let nodemailer = require('nodemailer')
// create mail transporter
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'COMPANYEMAIL@gmail.com',
pass: 'userpass'
}
});
// create express app
[...]
Note : This email will be set on the backend server and will be the account sending emails on behalf of the user. Also, you will need to temporarily allow non-secure sign-in for your Gmail account for testing purposes here
注意:此电子邮件将在后端服务器上设置,并且将代表用户发送电子邮件的帐户。 此外,你需要暂时允许非安全登录您的Gmail帐户用于测试目的这里
![Temporarily Allowing Less Secure Apps](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524854823807_AllwLessSecureGoogle.png)
// server.js
// configure app routes
[...]
app.post("/sendmail", multipartMiddleware, function(req, res) {
// get name and email of sender
let sender = JSON.parse(req.body.user);
let recipient = JSON.parse(req.body.recipient);
let mailOptions = {
from: "COMPANYEMAIL@gmail.com",
to: recipient.email,
subject: `Hi, ${recipient.name}. Here's an Invoice from ${
sender.company_name
}`,
text: `You owe ${sender.company_name}`
};
transporter.sendMail(mailOptions, function(error, info) {
if (error) {
return res.json({
status: 200,
message: `Error sending main to ${recipient.name}`
});
} else {
return res.json({
status: 200,
message: `Email sent to ${recipient.name}`
});
}
});
});
At this point, we have configured the emails to work when a POST
request is made to the /sendmail
route. The next thing we want to do is to allow the user perform this action on the frontend. To do this, let’s update the SingleInvoice
component by doing the following:
至此,我们已经将电子邮件配置为在对/sendmail
路由发出POST
请求时可以正常工作。 我们要做的下一件事是允许用户在前端执行此操作。 为此,我们通过执行以下操作来更新SingleInvoice
组件:
Add Form to Collect Recipient Information A from will be added below the display of the invoices. To allow the user send a copy of the invoice to interested parties.
添加表单以收集收件人信息 A将从发票的显示下方添加。 为了允许用户将发票副本发送给感兴趣的各方。
<!-- frontend/src/components/SingleInvoice.vue -->
<template>
<Header v-bind:user="user"/>
<!-- display invoice data -->
<div class="invoice">
<!-- display invoice name here -->
<div class="container">
<div class="row">
<div class="col-md-12">
// display invoice
</div>
</div>
<div class="row">
<form @submit.prevent="send" class="col-md-12">
<h3>Enter Recipient's Name and Email to Send Invoice</h3>
<div class="form-group">
<label for="">Recipient Name</label>
<input type="text" required class="form-control" placeholder="eg Chris" v-model="recipient.name">
</div>
<div class="form-group">
<label for="">Recipient Email</label>
<input type="email" required placeholder="eg chris@invoiceapp.com" class="form-control" v-model="recipient.email">
</div>
<div class="form-group">
<button class="btn btn-primary" >Send Invoice</button>
{{ loading }}
{{ status }}
</div>
</form>
</div>
</div>
</div>
</template>
Also, the component properties are updated as follows:
此外,组件属性更新如下:
// frontend/src/components/SingleInvoice.vue<script>
import Header from "./Header";
import axios from "axios";
export default {
name: "SingleInvoice",
components: {
Header
},
data() {
return {
invoice: {},
transactions: [],
user: '',
total_price: 0,
recipient : {
name: '',
email: ''
},
loading : '',
status: '',
};
},
methods: {
send() {
this.status = "";
this.loading = "Sending Invoice, please wait....";
const formData = new FormData();
formData.append("user", JSON.stringify(this.user));
formData.append("recipient", JSON.stringify(this.recipient));
axios.post("http://localhost:3128/sendmail", formData, {
headers: {"x-access-token": localStorage.getItem("token")}
}).then(res => {
this.loading = '';
this.status = res.data.message
});
}
},
mounted() {
// make request to fetch invoice data
}
};
</script>
Now, when you go back to view a single invoice in the browser and add the recipients details, you get the following result:
现在,当您返回浏览器以查看单个发票并添加收件人详细信息时,将得到以下结果:
![Sent Invoice](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524854040517_Screenshot+from+2018-04-27+18-33-36.png)
The recipient gets an email that looks like this:
收件人收到的电子邮件如下所示:
![Email Received by Recipient](https://d2mxuefqeaa7sj.cloudfront.net/s_7562138DA5484D5FD04700C1D81A740702F5CAF9FA62FE3D945AAEBF135504D4_1524854291537_email.png)
Note: You can further edit the email sent to the recipient to be as expressive as you may like using this guide
注意:您可以使用本指南进一步编辑发送给收件人的电子邮件,使其表现力更好。
结论 ( Conclusion )
In this part of the series, we looked at how to use JWTokens and the Browser’s Local Storage to keep users signed in. We also created the view for a single invoice and showed how to use nodemailer
to send emails to recipients about invoices. Feel free to leave a comment below if you have any questions. Here’s a link to the full Gitbhub repository if interested.
在本系列的这一部分中,我们研究了如何使用JWTokens和浏览器的本地存储来保持用户登录。我们还为单个发票创建了视图,并展示了如何使用nodemailer
将有关发票的电子邮件发送给收件人。 如有任何疑问,请在下面发表评论。 如果有兴趣,这里是完整的Gitbhub信息库的链接。
nodejs构建vue