Previously, I had written on uploading multiple images from a Vue Application to a Laravel Backend. In that solution, we made use of Javascript’s FormData Object. Here’s that article if you missed it:
In this solution, however, we would be using Javascript’s FileReader API that allows us to read the content of image files and handle those files in base64 format. Regardless of the backend language being used, base64 encoded images can be decoded on the server and stored locally in the filesystem (or on cloud storage).
Here’s a bit of illustration to better understand the process of uploading a Base64 image. While interfacing with your web application, the user can select an image (using the input tag, with a type of file) from their device, the content of the selected image is read using the FileReader API, we send the base64 string in Data URL format to a backend API.
On the backend, the base64 string is decoded from the string format to its original file object, we can then store the decoded file and return the storage path/URL to the frontend.
Down to the Base-ics
What on earth is Base64?
Base64 or Radix 64 is a binary-to-text encoding system that is designed to allow binary data to be represented in ASCII string format. One common application of base64 encoding on the web is to encode binary data so it can be included in a data: URL.
Data: URL?
Data URLs are URLs that are prefixed with “data:”, they allow embedding of file content inline in documents (.html). Data URLs are composed of 3 major parts after the “data:” prefix:
data:[<mediatype>][;base64],<data>
The [<mediatype>]
part refers to the file format, it could contain any one of the following values:
- text/plain — for text files
- image/jpeg or image/jpeg — for .png and .jpeg image files
- application/pdf — for .pdf files etc.
The [<base64>]
part is optional and can be ignored if the data is none textual. The last part is the textual (or non-textual) file content. Technically this is the format we would send out images.
FileReader API
What’s this?
The FileReader
object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user’s computer, using File
or Blob
objects to specify the file or data to read. — MDN
When a user selects a file using the input tag, a “change” event is triggered, this event object contains a FileList object that lets you access the list of files selected with the <input type="file">
element.
The FileReader API has 4 methods for reading file contents:
FileReader.readAsArrayBuffer()
FileReader.readAsBinaryString()
FileReader.readAsDataURL()
FileReader.readAsText()
The method we’re concerned with is the readAsDataURL method, as it allows us to read the content of a Blob file and return an object with a result
property containing a data:
URL representing the file’s data.
Getting Started
First, be sure you have all the Vuejs requirements installed. We will be using the Vue CLI to serve our application, however, we won’t be scaffolding an entire Vue application with the CLI. Thanks to Vue CLI’s instant prototyping, we can serve a single Vue file.
Let’s set up our project directory.
mkdir vue-base64 vue-base64/assets vue-base64/assets/styles
touch vue-base64/Index.vue && touch vue-base64/assets/styles/main.css
Our project directory would look something like this:
.
|--vue-base64
|-- Index.vue
|-- assets
|-- styles
|--main.css
In our template section, we’ll write a basic structure for our image and input elements.
Template
<template>
<div>
<div class="container mt-10">
<div class="card bg-white">
<img :src="image" alt="card_image">
<input @change="handleImage" class="custom-input" type="file" accept="image/*">
</div>
</div>
</div>
</template>
Here, we create a card to house our image and input button. On the input tag, we’re listening for a change event and calling a “handleImage” method every time that event is fired. We’re also binding the image “src” attribute to a reactive property “image” (We’ll add this property to our data object under the script section), so the image is updated every time a new one is selected.
We’ll add a bit of styling in our CSS file (main.css).
CSS
* {
font-family: Arial, Helvetica, sans-serif;
}
body {
background: #d8dddb;
}
.container {
display: flex;
justify-content: center;
}
.mt-10 {
margin-top: 10rem;
}
.bg-white {
background: #fff;
}
.card {
height: 10rem;
width: 20rem;
border-radius: 10px;
padding: 20px;
text-align: center;
}
img {
width: 17rem;
}
Import the CSS file main.css
into Index.vue
like so;
<style>
@import './assets/style/main.css';
</style>
Script
<script>
import axios from 'axios';
export default {
name: 'home',
data() {
return {
image: '',
remoteUrl: ''
}
},
methods: {
handleImage(e) {
const selectedImage = e.target.files[0]; // get first file
this.createBase64Image(selectedImage);
},
createBase64Image(fileObject) {
const reader = new FileReader();
reader.onload = (e) => {
this.image = e.target.result;
this.uploadImage();
};
reader.readAsDataURL(fileObject);
},
uploadImage() {
const { image } = this;
axios.post('http://127.0.0.1:8081/upload', { image })
.then((response) => {
this.remoteUrl = response.data.url;
})
.catch((err) => {
return new Error(err.message);
})
}
},
}
</script>
In the script section, the first thing we need to do is declare the reactive property (image) in our data object. We’ll then declare the handleImage
method to respond to the on-change event triggered by the input tag, we grab the first Blob from the FileList
object and assign it to a variable.
Next, we call the “createBase64Image” method that implements the FileReader API, uses the “readAsBinaryString” method on the selected image, then assigns the resulting string value to the “image” property once the “onload” event is triggered (the event is triggered each time the reading operation is successfully completed).
From the terminal, you can serve the single Vue file by typing:
vue serve Index.vue
If you inspect the result generated using the Vue DevTool, you’d notice the file is now in the Data URL format.
Note: The <img> tag element’s src attribute can contain the URL-address or the data URL of an image.
Backend (Node.js)
This implementation isn’t limited to just Node.js or Javascript, using the right library or built-in APIs, you can convert the base64 string to an image file and store in your application’s filesystem with basically any backend language of choice.
Since Vue.js uses the Nodejs runtime in the background, we can have our server-side in the same project directory so both applications can share libraries. Let’s make a bit of adjustment to the project folder. From the root directory, we’ll do a basic setup for a Node.js Express server.
npm init -y && mkdir server server/public && touch server/app.js
Our project directory should now look something like this:
|-- Index.vue
|-- assets
|-- styles/main.css
|-- server
|-- public
|-- app.js
`-- package.json
We’ll install all the necessary packages. Here’s what we need:
- Express Nodejs web framework
- Body-parser: to parse incoming request bodies.
- Axios: to send HTTP requests from our Vue application
- Cors: so we can send requests between the same origin
- Base64-img: to Convert images to base64, or convert base64 to images
npm i base64-img axios cors express body-parser
Once installed, open up app.js, set up the express server, and create the route to handle image upload, decoding, and storage.
const express = require('express');
const app = express();
const base64Img = require('base64-img');
const bodyParser = require('body-parser');
const cors = require('cors')
const port = 8081;
app.use(cors())
app.use(express.static('./server/public'))
app.use(bodyParser.json({ limit: '50mb' }));
app.post('/upload', (req, res) => {
const { image } = req.body;
base64Img.img(image, './server/public', Date.now(), function(err, filepath) {
const pathArr = filepath.split('/')
const fileName = pathArr[pathArr.length - 1];
res.status(200).json({
success: true,
url: `http://127.0.0.1:${port}/${fileName}`
})
});
});
app.listen(port, () => {
console.info(`listening on port ${port}`);
})
Here, we’re telling our express server to use cors, JSON body-parser (it’s important to set the limit, as base64 images can be really large). We’re also setting the ./server/public
folder as our static folder (we’ll store our decoded images here).
Next, we’re setting up the server to respond to a POST request on the “/upload” endpoint by getting the image string from the request body, passing the image string into the base64Img.img(…) function (this function takes 4 arguments; base64 string, path to store the image, filename, and a callback function that returns the file path. Finally, we grab the file name from the path and return the URL to the user.
Note: we’re using the Date.now() function as the file name just to maintain a level of uniqueness. Since the function returns the number of milliseconds elapsed since January 1, 1970, it’s almost impossible to get the same value everytime the function is called.
Let’s make some changes to our Vue application.
...
data() {
return {
image: '',
remoteUrl: ''
}
},
...
Add a new property, remoteUrl
, to the data object, this is going to hold the value of the URL returned by our server. Next, we need to import Axios and create a method to handle our post request to the server.
uploadImage() {
const { image } = this;
axios.post('http://127.0.0.1:8081/upload', { image })
.then((response) => {
this.remoteUrl = response.data.url;
})
.catch((err) => {
return new Error(err.message);
})
}
We’ll call this method within our reader.onload(…)
event.
...
reader.onload = (e) => {
this.image = e.target.result;
this.uploadImage();
};
...
Finally, Let’s render the image URL returned by the server just to be sure we got the right file from the server. Add this below the .container div in your template section:
<div class="mt-10" style="text-align: center">
<h3>SERVER IMAGE</h3>
<img :src="remoteUrl" alt="">
</div>
Your Index.vue
file should look something like this:
<template>
<div>
<div class="container mt-10">
<div class="card bg-white">
<img style="" :src="image" alt="">
<input @change="handleImage" class="custom-input" type="file" accept="image/*">
</div>
</div>
<div class="mt-10" style="text-align: center">
<h3>SERVER IMAGE</h3>
<img :src="remoteUrl" alt="">
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'home',
data() {
return {
image: '',
remoteUrl: ''
}
},
methods: {
handleImage(e) {
const selectedImage = e.target.files[0]; // get first file
this.createBase64Image(selectedImage);
},
createBase64Image(fileObject) {
const reader = new FileReader();
reader.onload = (e) => {
this.image = e.target.result;
this.uploadImage();
};
reader.readAsDataURL(fileObject);
},
uploadImage() {
const { image } = this;
axios.post('http://127.0.0.1:8081/upload', { image })
.then((response) => {
this.remoteUrl = response.data.url;
})
.catch((err) => {
return new Error(err.message);
})
}
},
}
</script>
<style>
@import './assets/style/main.css';
</style>
Piecing it all together
In our package.json
, we’ll add two scripts, one to start our Vue application, and the other to start our express server.
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"ui": "vue serve Index.vue",
"server": "node ./server/app.js"
},
}
From here you can open up two tabs on your terminal and run both scripts.
npm run ui
npm run server
You should have your server running on port 8081 and your Vue application on port 8080. Navigate to your Vue app and test.
As I’d previously mentioned, you can implement base64 image decode in any preferred language. Feel free to experiment with the source code here:
Cheers ☕️