This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 6. Tutorials already published:
- Intro
- Step 1: Writing The ToDo Smart contracts- beginning
- Step 2: Writing The ToDo Smart contracts – end
- Step 3: Writing The Nodejs Backend
- Step 4: Setup the frontend and read account data
- Step 5: Build a smart contract client with Truffle Contract
- Step 6: Refactor With Webpack, ES6 and truffle-solidity-loader (This tutorial)
- Step 7: Read smart contract data from frontend
- Step 8: Create smart contract data from frontend
- Step 9: Toggle task done & keep frontend updated
In this tutorial we are going to add webpack
and babel
to be able to use Javascript ES6 features in the browser. Let’s get started!
Setup project
As usual, you just need to copy and paste the folder of the last episode (step5) and rename it to step6
:
cp -r special-episode-1/step5 special-episode-1/step6
Step into step6
folder and install npm dependencies:
cd step6
npm install
Awesome! You are ready to get started.
Install webpack, babel and truffle-solidity-loader
Install the required dev npm packages with this command:
npm install -D webpack webpack-cli babel-core@6 babel-loader@7 babel-preset-env truffle-solidity-loader json-loader
Note: if later in this tutorial you have problems with babel
not working properly, it might be due to incompatible versions between babel-core
and babel-loader
. In this case just remove your node_modules
folder, copy paste the package.json
and package-lock.json
of the GitHub repo of EatTheBlocks (inside special-episode-1 folder), and run npm install
Let’s now move on to configure babel
by creating a file called .babelrc
at the root of your project and add this inside:
{
"presets": ["env"]
}
Then Configure webpack
by creating another file called webpack.config.js
at the root of your project as well. Inside copy paste this:
const path = require('path');
module.exports = {
entry: './app/js/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'app/dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};
It tells webpack
to:
- Take
app/js/index.js
as an input - Use
babel
to transform all the ES6 syntax to ES5 so that all browsers can understand it - Save the output file into
app/dist/bundle.js
Now let’s update the <script>
tag that loads our javascript code. In app/index.html
:
Change this:
<script src="js/index.js"></script>
To this:
<script src="dist/bundle.js"></script>
As you can see, we are now loading the bundle created by webpack
instead of the source file we wrote ourselves.
Setup new npm tasks
Now that we setup webpack
and babel
, we can use any ES6 features in our front code, without being afraid of compatibility issues with browsers. Babel will just transform everything into old-school ES5 Javascript that all browsers understand.
However, at the moment things are still very inconvenient. Every time we change any javascript file, we need to manually ask webpack
to recompile our code. What would be great is a command that watch any file changes and run webpack
automatically when needed.
Fortunately webpack
already have this feature when you launch it with the --watch
command:
webpack --watch
For our convenience, let’s create a npm command in our package.json
file. Under the scripts key, add the dev-front
command:
...,
"scripts": {
"dev-front": "node_modules/.bin/webpack --watch",
...
},
...
We will also add another task to watch the backend code. First install nodemon
with npm install -g nodemon
, then add it to the package.json
:
...,
"scripts": {
"dev-back": "nodemon server.js",
...
},
...
Now let’s check that our setup works properly:
- Run
npm run dev-back
- In another terminal window, run
npm run dev-front
- Open
app/js/index.js
, make a dummy change and save - In the terminal where you run
npm run dev-front
, you should seewebpack
recompiling everything. The frontend still need to be reloaded manually though (i.e we haven’t setup hot reloading)
Congrats, you just automated an important part of your workflow! Plus now you are free to use the latest Javascript features without fear!
Load smart contract artifact dynamically
So far, the way we load the smart contract in app/js/index.js
is easy to understand but very impractical: every time we make a change to the smart contract, we need to copy paste the compiled artifact (build/contract/ToDo.json
) into app/js/index.js
. That’s ok when you are just trying things out but if you actually count on developing a dapp like this you will quickly loose your sanity!
Fortunately for us, the good people of the Truffle framework created a nifty npm package called truffle-solidity-loader that allow us to dynamically load our smart contract from the frontend, WITHOUT having to copy paste the compiled artifact every time we make a change to the smart contract.
NOTE: if you google Truffle Solidity Loader
the first link that comes up is the OLD stand-alone repo. The docs are outdated, don’t follow it. The up-to-date repo is this one.
truffle-solidity-loader
integrates perfectly with the webpack
watch system. Every time you change the smart contract, webpack
will:
- Detect the change
- Recompile the Solidity smart contract,
- Re-run your Truffle migrations
- Re-load the compiled artifact into
app/js/index.js
All you need to do is add this entry to the module.rule
array of webpack.config,js
, after the first rule we already put just before:
...,
module: {
rules: [
...,
{
test: /\.sol/,
use: [
{
loader: 'json-loader'
},
{
loader: 'truffle-solidity-loader',
options: {
network: 'development',
migrations_directory: path.resolve(__dirname, './migrations'),
contracts_build_directory: path.resolve(__dirname, '../build/contracts')
}
}
]
}
]
}
...
In app/js/index.js
, remove the compiled artifact of the smart contract. (It’s the huge JSON object of several thousand lines that start at the top of file and look like this):
artifact = {
"contractName": "ToDo",
"abi": [
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
...
Finally add this to load the compiled artifact using ES6 features and truffle-solidity-loader
:
import artifact from '../../contracts/ToDo.sol`;
Note that this time we don’t load the artifact from build/contracts/ToDo.json
, but directly from the smart contract source. This save us from having to recompile manually after each change in the smart contract.
Modernize our code with ES6 features
Now that we can use ES6 features, we don’t need to go through the pain of copying over the browser-compatible versions of web3
and truffle-contract
(respectively web3.min.js
and truffle-contract.min.js
).
Add these lines at the top of app/js/index.js
, above the import we just added before:
import Web3 from 'web3';
import TruffleContract from 'truffle-contract';
...
Now you can delete unused script tags in app/index.html
:
<script src="vendor/web3.min.js"></script>
<script src="vendor/truffle-contract.min.js"></script>
As well as the app/js/vendor
folder:
rm -rf app/js/vendor
Back in app/js/index.js
, let’s prefix variable declarations with the const
keywords:
const web3 = new Web3(new Web3.providers.HttpProvider('http:localhost:9545');
const abstraction = new TruffleContract(artifact);
const network = Object.keys(artifact.networks)[0];
const address = artifact.networks[network].address;
Connect to different Ethereum clients depending on environments
In this tutorial I would like to make you test the dapp with the stand-alone ganache-cli
, not the one provided by Truffle when you run truffle develop
on port 9545. I could just ask you to update this configuration in app/index.js
:
const web3 = new Web3(new Web3.providers.HttpProvider('http:localhost:9545');
But that’s not very flexible. We potentially will have to change this often. We need a better way of handling this. The answer is to define an environment variable, and have our dapp connect to different Ethereum clients depending on its value.
In package.json
, let’s prefix the dev-front
command with an ENV
variable:
...
"scripts": {
"dev-front": "ENV=development node_modules/.bin/webpack --watch",
...
},
...
Now, open webpack.config.js
and add this snippet above module.exports = ...
:
const envVariables = new webpack.DefinePlugin({
ENV: JSON.stringify(process.env.ENV)
});
Below the module
entry, add a plugin
array:
module: {
...
},
plugins: [
envVariables
],
...
In the solidity-truffle-loader
rule let’s also make the network
value dynamic by replacing the static development
string by the ENV
environment variable:
...
options: {
network: process.env.ENV,
...
Now the content of the ENV
environment variable is available in our frontend code.
Let’s create a new file called app/js/config.js
whose job would be to determine what is the url of the Ethereum client to connect to, depending on ENV
. Copy paste this in this file:
import truffle from '../../truffle.js';
const getEthereumUrl = (env) => {
const network = truffle.networks[env];
return `http://${network.host}:${network.port}`;
};
const config = {
ethereumUrl: getEthereumUrl(ENV),
};
export default config;
What’s happening here? First, we import the configuration of truffle, where we define per environment the different ethereum clients we might want to connect to.
Then, getEthereumUrl()
builds the url to connect to an Ethereum client from the truffle.js
config and the environment.
Finally, we store the result of this in a config
object that will be available to other modules. Notice how we simply reference the variable ENV
that was populated by webpack
.
We just have final thing to do before finishing this. We need to actually use the config in the main javascript file. Open app/js/index.js
and import the config file below the TruffleContract
import:
...
import config from './config.js';
...
And in the web3 configuration replace the static url string by `config.ethereumUrl`:
… const web3 = new Web3(new Web3.providers.HttpProvider(config.ethereumUrl)); … “`
Final test
Phew! Almost there. We just need to test that everything works fine. Follow these steps:
- Run
ganache-cli
- In a new terminal, run
npm run dev-back
- In a new terminal, run
npm run dev-front
-
In your browser, visit
http://localhost:3000
and make sure that the dev console shows an output like this:(10) [“0xf76f2ab7c721ffcb953fe1a9d2110b14b931aa6e”, “0x005dcf583f7122397ac82cd9b99b2c14565dbf57”, “0x38e1a65b56d82f272aeb96c1c392195419097933”, “0x7c7185b77b453f2b3f27f56d669745b23399e383”, “0x8bab8cfa86db6c96fc65c35aa5556e78f28a940a”, “0x11483684b370d6a74a9b63b34b454134e0175531”, “0xdcf5346ab92ea40666a199d42a1ca7cb24aeb755”, “0xb2e5872f454bfd94d672b7312ce8ea377f8f76c5”, “0xb590c9a580afbb6d7157f63bcc85ba9826a5c28f”, “0xfaa46e6d2e14c4201e5ecbbd2d6b0e1256111f10”] bundle.js:8 []
If you change anything to contracts/ToDo.sol
and save the file, webpack
will enter in action to repackage everything, and when you reload the frontend everything still works with the updated smart contract address. No need to migrate, copy paste new contract artifact.
What a time saving!
The End
Congratulations! You just learned how to use the latest ES6 / webpack technologies to build a modern Dapp frontend. Also, thanks to truffle-solidity-loader
your workflow has been simplified and became way faster.
In the next tutorial, we will read data from the frontend. Stay tuned!
DappES6TruffleWebpack
sir, i got these error in step 6:
can you please fix them?
ReferenceError: assignment to undeclared variable artifact[Learn More] bundle.js:1:14909
file:///home/sheetal/Desktop/data_retrieval/app/dist/bundle.js:1:14909
n
file:///home/sheetal/Desktop/data_retrieval/app/dist/bundle.js:1:105
file:///home/sheetal/Desktop/data_retrieval/app/dist/bundle.js:1:902
file:///home/sheetal/Desktop/data_retrieval/app/dist/bundle.js:1:2
The character encoding of the HTML document was not declared. The document will render with garbled text in some browser configurations if the document contains characters from outside the US-ASCII range. The character encoding of the page must be declared in the document or in the transfer protocol.
in index.js file ,what will this artifact contain?
because i am getting error on webpage:
>ReferenceError: assignment to undeclared variable artifact
.............................
..........................................
index.js file:
import artifact from '../../contracts/data1.sol';
import Web3 from 'web3';
import TruffleContract from 'truffle-contract';
import config from './config.js';
console.log('loaded');
const web3 = new Web3(new Web3.providers.HttpProvider(config.ethereumUrl));
const abstraction = new TruffleContract(artifact);
abstraction.setProvider(web3.currentProvider);
...............................
............
Followed your every step, but while running 'npm run dev-front' I am getting this error.
> Project@1.0.0 dev-front L:\Project
> ENV=development node_modules/.bin/webpack --watch
'ENV' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! Project@1.0.0 dev-front: `ENV=development node_modules/.bin/webpack --watch`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the Project@1.0.0 dev-front script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
I am not able to find a perfect solution for this error. Please do help. Thank you in advance.
This is because you are probably developing on windows. Check out the syntax for env variables on windows
replace *ENV=development node_modules/.bin/webpack --watch*
with
*set ENV=development & node_modules/.bin/webpack --watch*