This tutorial is created to introduce some frontend libraries & frameworks.
To avoid confusion, This tutorial is written simply and exclude optimization.
if you want more information, look 'Related Links' placed at bottom of each tutorial.
- STEP1: Initialize node package
- STEP2: Use another node package
- STEP3: Make my package browser-executable by Webpack
- STEP4: Use ES6 syntax by Babel
- STEP5: Use React
- STEP6: Use Style Guide by ESLint
- STEP7: Manage task by Gulp
- STEP8: Add Sourcemaps by Webpack
- STEP9: Create Simple app with Reflux & React
- STEP10: Make your app sync with REST API server with json-server & jquery
- Initailize node package.
npm init # This command make package.json
package.json
{
"name": "step-by-step-frontend",
"version": "0.0.0",
"description": "step by step learning about frontend",
"main": "index.js",
"author": "ironhee <leechulhee95@gmail.com>",
"license": "MIT"
}
- Write your node module.
index.js
'use strict';
module.exports = function helloWorld() {
console.log('hello world!');
};
- load & run your module.
test.js
'use strict';
var helloWorld = require('./');
helloWorld();
node test.js # hello world!
- Install another node package.
npm install --save underscore
package.json
{
"name": "step-by-step-frontend",
"version": "0.0.0",
"description": "step by step learning about frontend",
"main": "index.js",
"author": "ironhee <leechulhee95@gmail.com>",
"license": "MIT",
"dependencies": {
"underscore": "^1.8.3"
}
}
- update your node module.
index.js
'use strict';
var _ = require('underscore');
module.exports = function helloWorld() {
_.times(10, function (index) {
console.log('[' + index + '] hello world!');
});
};
- load & run your module.
test.js
'use strict';
var helloWorld = require('./');
helloWorld();
node test.js # [0] hello world! ...
- create .gitignore
.gitignore
node_modules
- Install & Initailize bower package.
npm install -g bower
bower init # This command make bower.json
bower.json
{
"name": "step-by-step-frontend",
"version": "0.0.0",
"description": "step by step learning about frontend",
"main": "dist/index.js",
"authors": [
"ironhee <iron@ediket.com>"
],
"license": "MIT"
}
- Install webpack
npm install -g webpack
- create webpack config
webpack.config.js
'use strict';
var _ = require('underscore');
var pkg = require('./package.json');
module.exports = {
entry: {
'index': './index.js'
},
output: {
path: 'dist/',
filename: '[name].js',
library: 'MyLib',
libraryTarget: 'umd'
}
};
- build source code by webpack
webpack # use webpack.config.js by default
if you want to rebuild on file change, use --watch option
webpack --watch # ctrl-c to exit
- load & run your module.
test.html
<html>
<body>
<script src='./dist/index.js'></script>
<script> MyLib(); </script>
</body>
</html>
- change package.json 'main' property
package.json
{
"name": "step-by-step-frontend",
"version": "0.0.0",
"description": "step by step learning about frontend",
"main": "dist/index.js",
"author": "ironhee <leechulhee95@gmail.com>",
"license": "MIT",
"dependencies": {
"underscore": "^1.8.3"
}
}
- load & run your node module.
test.js
'use strict';
var helloWorld = require('./');
helloWorld();
node test.js # [0] hello world! ...
- Install babel-loader by npm
npm install --save-dev babel-loader
- rename index.js and change content
mv index.js index.es6
index.es6
import _ from 'underscore';
export default function helloWorld() {
_.times(10, (index) => {
console.log(`[${index}] hello world!`);
});
}
- change webpack config
webpack.config.js
'use strict';
var _ = require('underscore');
var pkg = require('./package.json');
module.exports = {
entry: {
'index': './index.es6'
},
output: {
path: 'dist/',
filename: '[name].js',
library: 'MyLib',
libraryTarget: 'umd'
},
module: {
loaders: [
{ test: /\.es6$/, loader: 'babel-loader' }
]
}
};
- build source code by webpack
webpack
- test browser-side and node-side
test.html
<html>
<body>
<script src='./dist/index.js'></script>
<script> MyLib(); </script>
</body>
</html>
node test.js # [0] hello world! ...
- Install React by npm
npm install --save react
- make some directories
mkdir -p src/js/components
- create modules and rendering script
src/js/components/MyComponent.es6
import React from 'react';
export default React.createClass({
render() {
return (
<div>
<h1>Hello world!</h1>
</div>
);
}
});
src/js/app.es6
import MyComponent from './components/MyComponent';
export default {
MyComponent
};
src/js/main.es6
import React from 'react';
import { MyComponent } from './app';
React.render(<MyComponent/>, document.body);
- remove old files and change webpack config
rm -rf test.js test.html index.es6 dist/index.js
- separate webpack config
webpack.base.config.js
'use strict';
module.exports = {
resolve: {
extensions: ['', '.js', '.es6']
},
module: {
loaders: [
{ test: /\.es6$/, loader: 'babel-loader' }
]
}
};
webpack.config.js
'use strict';
var _ = require('underscore');
var baseConfig = require('./webpack.base.config');
module.exports = _.extend({}, baseConfig, {
entry: {
'app': './src/js/app.es6'
},
output: {
path: 'dist/',
filename: 'index.js',
library: 'MyLib',
libraryTarget: 'umd'
}
});
webpack.main.config.js
'use strict';
var _ = require('underscore');
var baseConfig = require('./webpack.base.config');
module.exports = _.extend({}, baseConfig, {
entry: {
'main': './src/js/main.es6'
},
output: {
path: 'dist/',
filename: 'main.js',
libraryTarget: 'umd'
}
});
- build by webpack
webpack # build library code
webpack --config webpack.main.config # build rendering code
- check in node and browser
node -e 'console.log(require("./"))'
# { MyComponent: { [Function] displayName: 'MyComponent' } }
demo/index.html
<html>
<body>
<script src='../dist/index.js'></script>
<script> console.log(MyLib) </script>
</body>
</html>
- use main.js (rendering logic) in browser
demo/index.html
<html>
<body>
<script src='../dist/index.js'></script>
<script> console.log(MyLib) </script>
<script src='../dist/main.js'></script>
</body>
</html>
- Install eslint and plugins by npm
npm install --save-dev eslint babel-eslint eslint-plugin-react
- make eslint configs
.eslintrc
{
"env": {
"browser": true,
"node": true
},
"rules": {
"strict": [2, "global"],
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-unused-vars": [2, {
"vars": "local",
"args": "after-used"
}],
"no-use-before-define": 2,
"comma-dangle": [2, "never"],
"no-cond-assign": [2, "always"],
"no-console": 1,
"no-debugger": 1,
"no-alert": 1,
"no-constant-condition": 1,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 0,
"no-extra-semi": 2,
"no-func-assign": 2,
"no-inner-declarations": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-obj-calls": 2,
"no-reserved-keys": 2,
"no-sparse-arrays": 2,
"no-unreachable": 2,
"use-isnan": 2,
"block-scoped-var": 2,
"consistent-return": 2,
"curly": [2, "multi-line"],
"default-case": 2,
"dot-notation": [2, {
"allowKeywords": true
}],
"eqeqeq": 2,
"guard-for-in": 2,
"no-caller": 2,
"no-else-return": 2,
"no-eq-null": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-param-reassign": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-with": 2,
"radix": 2,
"vars-on-top": 2,
"wrap-iife": [2, "any"],
"yoda": 2,
"indent": [2, 2],
"brace-style": [2,
"1tbs", {
"allowSingleLine": true
}],
"quotes": [
2, "single", "avoid-escape"
],
"camelcase": [2, {
"properties": "never"
}],
"comma-spacing": [2, {
"before": false,
"after": true
}],
"comma-style": [2, "last"],
"eol-last": 2,
"func-names": 1,
"key-spacing": [2, {
"beforeColon": false,
"afterColon": true
}],
"new-cap": [2, {
"newIsCap": true
}],
"no-multiple-empty-lines": [2, {
"max": 2
}],
"no-nested-ternary": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-wrap-func": 2,
"no-underscore-dangle": 0,
"one-var": [2, "never"],
"padded-blocks": [2, "never"],
"semi": [2, "always"],
"semi-spacing": [2, {
"before": false,
"after": true
}],
"space-after-keywords": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, "never"],
"space-infix-ops": 2,
"space-return-throw-case": 2,
"spaced-line-comment": 2,
}
}
src/js/.estlinrc
{
"parser": "babel-eslint",
"plugins": [
"react"
],
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": false,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": false,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"spread": true,
"superInFunctions": true,
"templateStrings": true,
"jsx": true
},
"rules": {
"strict": [2, "never"],
"no-var": 2,
"react/display-name": 0,
"react/jsx-boolean-value": 2,
"react/jsx-quotes": [2, "double"],
"react/jsx-no-undef": 2,
"react/jsx-sort-props": 0,
"react/jsx-sort-prop-types": 0,
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/no-did-mount-set-state": [2, "allow-in-func"],
"react/no-did-update-set-state": 2,
"react/no-multi-comp": 2,
"react/no-unknown-property": 2,
"react/prop-types": 2,
"react/react-in-jsx-scope": 2,
"react/self-closing-comp": 2,
"react/wrap-multilines": 2,
"react/sort-comp": [2, {
"order": [
"displayName",
"mixins",
"statics",
"propTypes",
"getDefaultProps",
"getInitialState",
"componentWillMount",
"componentDidMount",
"componentWillReceiveProps",
"shouldComponentUpdate",
"componentWillUpdate",
"componentWillUnmount",
"/^on.+$/",
"/^get.+$/",
"/^render.+$/",
"render"
]
}]
}
}
- install editor plugin
- sublime text: Install SublimeLinter & SublimeLinter-eslint
- atom: Install linter & linter-eslint
- Install gulp by npm
npm install -g gulp
npm install --save-dev gulp
- Install webpack and plugin by npm
npm install --save-dev webpack
npm install --save-dev webpack-gulp-logger
- create gulp config file (gulpfile.js)
gulpfile.js
'use strict';
var gulp = require('gulp');
var webpack = require('webpack');
var webpackLogger = require('webpack-gulp-logger');
var libWebpackConfig = require('./webpack.config');
var mainWebpackConfig = require('./webpack.main.config');
gulp.task('default', [
'watch'
]);
gulp.task('watch', [
'watch-lib',
'watch-main'
]);
gulp.task('build', [
'build-lib',
'build-main'
]);
gulp.task('watch-lib', function() {
webpack(libWebpackConfig).watch({}, webpackLogger());
});
gulp.task('watch-main', function() {
webpack(mainWebpackConfig).watch({}, webpackLogger());
});
gulp.task('build-lib', function(callback) {
webpack(libWebpackConfig).run(webpackLogger(callback));
});
gulp.task('build-main', function(callback) {
webpack(mainWebpackConfig).run(webpackLogger(callback));
});
- run gulp task
for build
gulp build
for watch
gulp watch
- set devtool property in webpack config
webpack.base.config.js
'use strict';
module.exports = {
devtool: 'eval-source-map',
resolve: {
extensions: ['', '.js', '.es6']
},
module: {
loaders: [
{ test: /\.es6$/, loader: 'babel-loader' }
]
}
};
- build by gulp
gulp build
now you can distinguish source code.
- add resolve.modulesDirectories option to webpack config for convenience
webpack.base.config
'use strict';
module.exports = {
devtool: 'eval-source-map',
resolve: {
modulesDirectories: ['src/js/', 'node_modules'],
extensions: ['', '.js', '.es6']
},
module: {
loaders: [
{ test: /\.es6$/, loader: 'babel-loader' }
]
}
};
- create Commment.es6 and CommentSite.es6
src/js/components/Comment.es6
import React from 'react';
export default React.createClass({
propTypes: {
comment: React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
updatedAt: React.PropTypes.object.isRequired
}).isRequired
},
render() {
return (
<div>
{ this.props.comment.content } -
{ this.props.comment.updatedAt.toDateString() }
<a href="#">remove</a>
</div>
);
}
});
src/js/components/CommentSite.es6
import React from 'react';
import _ from 'underscore';
import Comment from 'components/Comment';
export default React.createClass({
getInitialState() {
return {
comments: [{
id: 1,
content: 'this is comment1!',
updatedAt: new Date(Date.now())
}, {
id: 2,
content: 'this is comment2!',
updatedAt: new Date(Date.now())
}]
};
},
render() {
return (
<div>
<h3>Comments</h3>
{ _.map(this.state.comments, comment => (
<Comment comment={ comment } key={ comment.id } />
)) }
<form>
<textarea ref="newComment"></textarea>
<button>Comment!</button>
</form>
</div>
);
}
});
- add CommentSite to app.es6
src/js/app.es6
import MyComponent from 'components/MyComponent';
import CommentSite from 'components/CommentSite';
export default {
MyComponent,
CommentSite
};
- change main.es6
main.es6
import React from 'react';
import { CommentSite } from 'app';
React.render(<CommentSite/>, document.body);
-
open demo.index.html in browser and check components are correctly rendered
-
install reflux, q, underscore-db by npm
npm install --save reflux q@~1.0 underscore-db
- add node.fs option to webpack config
webpack.base.config
'use strict';
module.exports = {
devtool: 'eval-source-map',
node: {
fs: 'empty'
},
resolve: {
modulesDirectories: ['src/js/', 'node_modules'],
extensions: ['', '.js', '.es6']
},
module: {
loaders: [
{ test: /\.es6$/, loader: 'babel-loader' }
]
}
};
- define comment actions
src/js/actions/CommentActions.es6
import Reflux from 'reflux';
export default Reflux.createActions({
createComment: {
asyncResult: true
},
removeComment: {
asyncResult: true
}
});
- set reflux promise factory to Q.Promise
src/js/app.es6
import Reflux from 'reflux';
import Q from 'q';
Reflux.setPromiseFactory(Q.Promise);
import MyComponent from 'components/MyComponent';
import CommentSite from 'components/CommentSite';
export default {
MyComponent,
CommentSite
};
- create comment store
src/js/mixins/DBMixin.es6
import underscoreDB from 'underscore-db';
import _ from 'underscore';
_.mixin(underscoreDB);
export default function DBMixin() {
let result = {
db: []
};
_.extend(result, _(result.db));
return result;
}
src/js/stores/CommentStore.es6
import Reflux from 'reflux';
import CommentActions from 'actions/CommentActions';
import DBMixin from 'mixins/DBMixin';
import { Promise } from 'q';
export default Reflux.createStore({
mixins: [new DBMixin()],
listenables: [CommentActions],
onCreateComment(content) {
CommentActions.createComment.promise(
new Promise((resolve) => {
let comment = this.insert({
content,
updatedAt: new Date(Date.now())
});
resolve(comment);
this.trigger();
})
);
},
onRemoveComment(commentID) {
CommentActions.removeComment.promise(
new Promise((resolve) => {
let comment = this.removeById(commentID);
resolve(comment);
this.trigger();
})
);
}
});
- make Commment.es6 and CommentSite.es6 use store & actions
src/js/components/Comment.es6
import React from 'react';
import CommentActions from 'actions/CommentActions';
export default React.createClass({
propTypes: {
comment: React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
updatedAt: React.PropTypes.object.isRequired
}).isRequired
},
onRemove() {
CommentActions.removeComment(this.props.comment.id)
.then(() => {
alert('removed!');
});
return false;
},
render() {
return (
<div>
{ this.props.comment.content } -
{ this.props.comment.updatedAt.toDateString() }
<a href="#" onClick={ this.onRemove }>remove</a>
</div>
);
}
});
src/js/components/CommentSite.es6
import React from 'react';
import Reflux from 'reflux';
import _ from 'underscore';
import Comment from 'components/Comment';
import CommentStore from 'stores/CommentStore';
import CommentActions from 'actions/CommentActions';
function getStoreState() {
return {
comments: CommentStore.value()
};
}
export default React.createClass({
mixins: [
Reflux.listenTo(CommentStore, 'onStoreChange')
],
getInitialState() {
return getStoreState();
},
onStoreChange() {
this.setState(getStoreState());
},
onCreateComment() {
let content = React.findDOMNode(this.refs.newComment).value;
CommentActions.createComment(content)
.then(() => {
alert('created!');
});
return false;
},
render() {
return (
<div>
<h3>Comments</h3>
{ _.map(this.state.comments, comment => (
<Comment comment={ comment } key={ comment.id } />
)) }
<form onSubmit={ this.onCreateComment }>
<textarea ref="newComment"></textarea>
<button>Comment!</button>
</form>
</div>
);
}
});
- open demo.index.html in browser and check components are correctly operated
- modulesDirectories option
- promise
- q
- flux
- reflux
- react
- underscore-db
- reflux-todo
- Cannot resolve module 'fs'
- Install json-server globally by npm
npm install -g json-server
- Create directory for json-server
mkdir public
- Move demo/index.html to public/index.html
mv demo/index.html public
rm -rf demo
- Change content of public/index.html
public/index.html
<html>
<body>
<script src='/static/main.js'></script>
</body>
</html>
- Make symbolic link of static files.
ln -s ../dist/ public/static
- create db.json
db.json
{}
- Run json-server
json-server db.json
# {^_^} Hi!
#
# Loading database from db.json
#
#
# You can now go to http://localhost:3000
#
# Enter s at any time to create a snapshot # of the db
-
Open http://localhost:3000 in browser. and check your app is correctly operated.
-
Install jquery, url-join by npm
npm install -S jquery url-join
- Make CommentStore use Ajax Request.
src/js/mixins/DBMixin.es6
import underscoreDB from 'underscore-db';
import _ from 'underscore';
import $ from 'jquery';
import urlJoin from 'url-join';
import { Promise } from 'q';
_.mixin(underscoreDB);
function ajaxRequest(options) {
return new Promise((resolve, reject) => {
$.ajax(options)
.then(resolve)
.fail(reject);
});
}
export default function DBMixin(type) {
let result = {
db: []
};
let methods = _(result.db);
_.extend(result, methods);
_.extend(result, {
insert(attributes) {
return ajaxRequest({
type: 'POST',
url: urlJoin(type),
data: attributes
})
.then(response => {
return response;
})
.then(response => methods.insert(response));
},
removeById(id) {
return ajaxRequest({
type: 'DELETE',
url: urlJoin(type, id)
})
.then(() => methods.removeById(id));
}
});
return result;
}
src/js/stores/CommentStore.es6
import Reflux from 'reflux';
import CommentActions from 'actions/CommentActions';
import DBMixin from 'mixins/DBMixin';
import { Promise } from 'q';
export default Reflux.createStore({
mixins: [new DBMixin('comments')],
listenables: [CommentActions],
onCreateComment(content) {
CommentActions.createComment.promise(
new Promise((resolve, reject) => {
this.insert({
content,
updatedAt: new Date().getTime()
})
.then(comment => resolve(comment))
.then(() => this.trigger())
.catch(reject);
})
);
},
onRemoveComment(commentID) {
CommentActions.removeComment.promise(
new Promise((resolve, reject) => {
this.removeById(commentID)
.then(comment => resolve(comment))
.then(() => this.trigger())
.catch(reject);
})
);
}
});
Warning: comment.updatedAt field's type is change.
- Apply comment.updatedAt field's type change.
src/js/components/Comment.es6
import React from 'react';
import CommentActions from 'actions/CommentActions';
export default React.createClass({
propTypes: {
comment: React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
updatedAt: React.PropTypes.number.isRequired
}).isRequired
},
onRemove() {
CommentActions.removeComment(this.props.comment.id)
.then(() => {
alert('removed!');
});
return false;
},
render() {
return (
<div>
{ this.props.comment.content } -
{ new Date(this.props.comment.updatedAt).toDateString() }
<a href="#" onClick={ this.onRemove }>remove</a>
</div>
);
}
});
-
Open http://localhost:3000 in browser. and check your app make ajax request correctly.
-
add fetchComments action to CommentActions
src/js/actions/CommentActions.es6
import Reflux from 'reflux';
export default Reflux.createActions({
fetchComments: {
asyncResult: true
},
createComment: {
asyncResult: true
},
removeComment: {
asyncResult: true
}
});
- make CommentSite trigger fetchComment action after rendered. (componentDidMount)
src/js/components/CommentSite.es6
import React from 'react';
import Reflux from 'reflux';
import _ from 'underscore';
import Comment from 'components/Comment';
import CommentStore from 'stores/CommentStore';
import CommentActions from 'actions/CommentActions';
function getStoreState() {
return {
comments: CommentStore.value()
};
}
export default React.createClass({
mixins: [
Reflux.listenTo(CommentStore, 'onStoreChange')
],
getInitialState() {
return getStoreState();
},
componentDidMount() {
CommentActions.fetchComments();
},
onStoreChange() {
this.setState(getStoreState());
},
onCreateComment() {
let content = React.findDOMNode(this.refs.newComment).value;
CommentActions.createComment(content)
.then(() => {
alert('created!');
});
return false;
},
render() {
return (
<div>
<h3>Comments</h3>
{ _.map(this.state.comments, comment => (
<Comment comment={ comment } key={ comment.id } />
)) }
<form onSubmit={ this.onCreateComment }>
<textarea ref="newComment"></textarea>
<button>Comment!</button>
</form>
</div>
);
}
});
- implement fetch method
src/js/mixins/DBMixin.es6
import underscoreDB from 'underscore-db';
import _ from 'underscore';
import $ from 'jquery';
import urlJoin from 'url-join';
import { Promise } from 'q';
_.mixin(underscoreDB);
function ajaxRequest(options) {
return new Promise((resolve, reject) => {
$.ajax(options)
.then(resolve)
.fail(reject);
});
}
export default function DBMixin(type) {
let result = {
db: []
};
let methods = _(result.db);
_.extend(result, methods);
_.extend(result, {
insert(attributes) {
return ajaxRequest({
type: 'POST',
url: urlJoin(type),
data: attributes
})
.then(response => {
return response;
})
.then(response => methods.insert(response));
},
removeById(id) {
return ajaxRequest({
type: 'DELETE',
url: urlJoin(type, id)
})
.then(() => methods.removeById(id));
},
fetch(id) {
return ajaxRequest({
type: 'GET',
url: urlJoin(type, id)
})
.then(response => _.isArray(response) ?
_.map(response, _response => methods.insert(_response)) :
methods.insert(response)
);
}
});
return result;
}
src/js/stores/CommentStore.es6
import Reflux from 'reflux';
import CommentActions from 'actions/CommentActions';
import DBMixin from 'mixins/DBMixin';
import { Promise } from 'q';
export default Reflux.createStore({
mixins: [new DBMixin('comments')],
listenables: [CommentActions],
onFetchComments() {
CommentActions.fetchComments.promise(
new Promise((resolve, reject) => {
this.fetch()
.then(comments => resolve(comments))
.then(() => this.trigger())
.catch(reject);
})
);
},
onCreateComment(content) {
CommentActions.createComment.promise(
new Promise((resolve, reject) => {
this.insert({
content,
updatedAt: new Date().getTime()
})
.then(comment => resolve(comment))
.then(() => this.trigger())
.catch(reject);
})
);
},
onRemoveComment(commentID) {
CommentActions.removeComment.promise(
new Promise((resolve, reject) => {
this.removeById(commentID)
.then(comment => resolve(comment))
.then(() => this.trigger())
.catch(reject);
})
);
}
});
-
Open http://localhost:3000 in browser. and check your app make get request after initial rendering and your comments is correctly rendered.
-
add db.json to .gitignore
.gitignore
node_modules
db.json