このメモはUdemy講座を受講して学んだ記録です。
作者には感謝します。
node hoge.js
でjsファイルの実行
node inspect hoge.js
でコンソールのデバッグ.
なお、node inspect を実行し、chromeのアドレスバーにchrome://inspectを入れると、Remote Targetの欄にデバッグ中のスクリプトが表示され、接続するとchromeのデバッガが起動する. 通常はvscodeのデバッガを使用していればよいと思われる.
Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
nodeの特徴はansyncronousを実現していること.
(javascript自体はシングルスタック)
javascriptはCall stackを使ってシングルスタックで動く.
NodeはCallbackが発生すると、Callback queueに溜め込み、メインプログラムが終了後、逐次Callbackを実行する.
console.log('starting.')
setTimeout(() => {
console.log('2 second passed')
}, 2000)
setTimeout(() => {
console.log('0 second passed')
}, 0)
console.log('stopped.')
上の例では、setTimeout(setTimeoutはjavascriptの関数ではなくnodeのAPI)が発生すると、Callback関数をCallback queueに移し待機させる.
Callback queueはメインプログラム終了後に動くため、console.log('stopped')より前にsetTimeoutの中のconsole.log('0 second passed')が実行されることはない.
逆に言えばCallback関数を実行させるには、メインプログラムを終了させなければいけない.
例えば、
const geocode = (address) => {
const data = {
latitude : 133, longitute: 46
}
return data
}
console.log(geocode('kasugai'))
この同期プログラムで、geocodeの中身を非同期で返したい場合、こうなる
const geocode = (address, callback) => { //引数にCallback関数をとり...
setTimeout(() => {
const data = {
latitude : 133, longitute: 46
}
callback(data) //Callbackを実行...
}, 1000)
}
geocode('kasugai', (data) => {
console.log(data) //Callbackの中身を呼び出し側に書く. geocodeで取得したデータの処理など.
})
npm init
-y で各値をデフォルトで作成
npm init -y
{
"name": "tailwind-trader-api",
"version": "1.0.0",
"description": "Tailwind Traders データベースからアイテムを管理する HTTP API",
"main": "index.js",
"scripts": {
"start": "node ./src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["api", "database"],
"author": "author",
"license": "ISC"
}
scriptsには start, test, build などのアクションを書く
これらのアクションは、
npm run <アクション>
で実行できる
startとtestはrunを省略して
npm start
で実行できる
バージョン更新の書き方のルール
最も高い “メジャー” バージョンに更新します。
“マイナー” バージョンのみを更新します。
最新の “パッチ” バージョンに更新します。
npm install nodemon --save-dev
–save-devでインストールするとdevdependenciesに設定される.
プロダクションには不要で開発時のみ使うライブラリを指定できる.
環境構築時、npm installすることでdevdependenciesに記載されたライブラリを一括インストールできる.
.envファイルを置くPORT=3000
SENDGRID_API_KEY=xxx
CONNECTION_URL=mongodb://127.0.0.1:27017/task-manager-api
JWT_TOKEN_SECRET=secretKey
scriptに↓のように書く "scripts": {
"start": "node src/index.js",
"dev": "env-cmd nodemon src/index.js"
},
sendgridMail.setApiKey(process.env.SENDGRID_API_KEY)
.eslintrc.jsがあるmodule.exports = {
rules: {
"no-unused-vars": "off"
}
}
const https = require('https')
const url = `https://api.mapbox.com/geocoding/...`
const request = https.request(url, (response) => {
let data = ''
response.on('data', (chunk) => {
data = data + chunk.toString()
})
response.on('end', () => {
const body = JSON.parse(data)
console.log(body);
})
})
request.on('error', (error) => {
console.log('error', error);
})
request.end()
引数なしで実行すると大量に出てくるのでdepthで深さを指定する. 0始まり
npm list --depth=0
npm install
でpackage.jsonのdependenciesに記載のライブラリを記載のバージョンルールに従ってインストールする.
npm install node-fetch@x.x.x
@以下をlatestにすると最新をインストール
npm install jest --save-dev
–save-devをつける. package.jsonにはdevDependenciesに追加される
--saveは今は要らない。昔のnpmコマンドはpackage.jsonのdependenciesに追加する機能だった模様.
-f or --forceで強制インストール. piniaを使用しているプロジェクトをnpm installしたらエラーになったので、単体で-fでインストールしたら治った
vulnerabilitiesが表示されたら
npm audit fix --force
を実行する
ライブラリのバージョンが古いものを表示する
yarn install
でpackage.jsonのに記載のライブラリを記載のバージョンルールに従ってインストールする。
yarn.lockファイルができる。
複数人で共有する場合はyarn.lockに依存関係をもつので、yarn.lockを直下に置いたまま、yarn installする。
yarn audit
yarn upgrade [ライブラリ名]
実際のインストールはせずに、一緒にインストールされる関連ライブラリを確認する
yarn upgrade [ライブラリ名] --dry-run
※dry-runでもyarn.lockは更新される
npm installだとそれぞれにライブラリを保存してしまい、ディスクの無駄。nodeのあるディレクトリで
corepack enable
corepack prepare pnpm --activate
pnpm@10.19など必要に応じてバージョン指定する
nodeディレクトリ直下に.npmrcファイルを作成する
echo "store-dir=/mydir/.pnpm-store
side-effects-cache=true" >> ~/.npmrc
yarn.lock, package-lock.jsonファイルがあるディレクトリで
pnpm import
するとpnpm-lock.yamlが生成できる
その後
pnpm istall
して、node_modulesのハードリンクをつくる
pnpm install --frozen-lockfile
として、既存のlockファイルに影響しないようにするとよい
pnpm start
でアプリを実行できる。
本体とgit worktreeなどでわけている場合は、 PORT=3001 pnpm startなどとして、PORTを分けたり環境変数をわけて利用する
nodeのバージョンを管理する.
brew install nvm
でインストールする.
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completi$
nvm install 18
nvm use 18
で指定したバージョンをインストール、使用する.
v18.20.6
でルートディレクトリに.nvmrcファイルを作成する
nvm use
を実行すると、nvmrcファイルのバージョンのnodeが使用される.
brew uninstall node
PATHの先頭に/opt/homebrew/bin:/opt/homebrew/sbin:/が入っていて、必ずhomebrewのnodeが有効になってしまっていたため。
nvm install 18
nvm use 18
rm -rf node_modules
rm package-lock.json
npm cache clean --force
npm install --legacy-peer-deps
` –legacy-peer-deps`: nvm7からは厳密にライブラリ間のバージョンを管理するようになったため、このオプションをつけることで、それを緩和する。
nodemon app.js
とすると、app.jsが更新されるたびに実行してくれる.
-e: 自動更新するファイルの拡張子を指定できる
nodemon app.js -e js,hbs
ローカルにインストールするとnodemonでは実行できないので、
package.jsonにdev: "nodemon src/app.js"として、
npm run dev
で実行する.
標準出力の色を変えられる
const chalk = require('chalk')
console.log(chalk.red.inverse('gmail.com'))
コマンドラインのパーサー
const yargs = require('yargs')
console.log(yargs.argv)
console.log(yargs.argv.buzz) //→fizz
console.log(yargs.argv._[0]) //→add
const yargs = require('yargs')
yargs.command({
command: 'add',
describe: 'Add a new note',
handler: function () {
console.log('Adding a new note.')
}
}).command({
command: 'remove',
describe: 'Remove a note',
handler: function () {
console.log('Removing the note.')
}
}).help().argv
yargs.command({
command: 'add',
describe: 'Add a new note',
builder: {
title: {
describe: 'Note title',
demandOption: true,
type: 'string'
},
body: {
describe: 'Note body',
demandOption: true,
type: 'string'
}
}).argv
今は更新されていなくて、後継はpostman-request
ex.
request = require('request')
const geocode = (address, callback) => {
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(address)}.json?access_token=pk...&limit=1`
request({url, json: true}, (error, {body}) => {
if (error) {
callback('Unable to connect geocoding.', undefined) //callbackを呼び出し側で使う方法
} else if (!body.features || body.features.length === 0) {
callback('Unable to find location. Try another address.', undefined)
} else {
features_ = body.features[0]
callback(undefined, {
logitude: features_.center[0],
latitude: features_.center[1],
location: features_.place_name
})
}
})
}
webserverライブラリ.
const express = require("express");
const app = express();
// ミドルウェア : http headersの"authorization"をチェックする
function isAuthorized(req, res, next) {
const auth = req.headers.authorization;
if (auth === 'secretpassword') {
next();
} else {
res.status(401);
res.send('Not permitted');
}
}
const port = 3000;
app.get("/", (req, res) => res.send("Hello World!"));
// ミドルウェア関数を引数に入れる
app.get("/users", isAuthorized, (req, res) => {
res.json([
{
id: 1,
name: "User Userson",
},
]);
});
app.get("/products", (req, res) => {
res.json([
{
id: 1,
name: "The Bluest Eye",
},
]);
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
const express = require('express');
const port = process.env.PORT
const path = require('path');
const app = express()
const publicDirectoryPath = path.join(__dirname, '../public')
app.use(express.static(publicDirectoryPath)) //...publicフォルダの指定
app.get('', (req, res) => {
res.sendFile('index.html') //...index.htmlはsendFileで表示する
})
app.listen(port, () => {
console.log('listen: ', port);
})
テンプレートエンジン. expressと一緒に使う場合はプラグイン版のhbs.jsを使う.
expressのhandlebarsプラグイン.
express + hbs を使った例↓
const express = require('express');
const path = require('path');
const hbs = require('hbs');
const app = express()
const port = 3000
// define paths for Express config
const publicDirectoryPath = path.join(__dirname, '../public')
const viewsPath = path.join(__dirname, '../template/views')
const partialsPath = path.join(__dirname, '../template/partials')
// setup handlebars engine and views location
app.set('view engine', 'hbs')
app.set('views', viewsPath)
hbs.registerPartials(partialsPath)
// setup static directory to serve
app.use(express.static(publicDirectoryPath))
app.get('', (req, res) => {
res.render('index', {
title: 'Weather app',
name: 'Mechael'
})
})
app.get('*', (req, res) => {
res.render('404', {
title: '404 Error Page',
name: 'Mechael'
})
})
app.listen(port, () => {
console.log('listen: ', port);
})
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- ... 省略 -->
</head>
<body>
<div class="main-content">
<p>Use this site to get your weather</p>
</div>
</body>
</html>
<header>
<h1></h1>
<a href="/">Weather</a>
</header>
const {MongoClient, ObjectID} = require('mongodb');
const connectionURL = 'mongodb://127.0.0.1:27017'
const databaseName = 'task-manager'
const id = new ObjectID() //ObjectIdを自分で生成する場合
MongoClient.connect(connectionURL, {useNewUrlParser: true}, (error, client) => {
if (error) {
console.log('connection error');
}
console.log('connection success');
const db = client.db(databaseName)
//1件追加
db.collection('users').insertOne(
{_id: id, name: 'John', age: 48}, (error, result) => { //idは指定しなくてもよい. id=_idとなる.
if (error) {
return console.log('insert error');
}
console.log(result.insertedCount);
})
//複数件追加
db.collection('users').insertMany([
{name: 'Paul', age: 47},
{name: 'Jeorge', age: 46},
], (error, result) => {
if (error) {
return console.log('insert error');
}
console.log(result);
})
//1件索引
db.collection('users').findOne({name: 'John'}, (error, user) => {
if (error) {
return console.log('Unable to find user');
}
console.log(user);
})
//複数件索引
db.collection('users').find({name: 'John'}).toArray((error, users) => {
if (error) {
return console.log('Unable to find user');
}
console.log(users);
})
//カウント取得
db.collection('users').find({name: 'John'}).count((error, count) => {
console.log(count);
})
//更新のPromiseを使った例...↑のinsert, findもPromiseを使って書ける
db.collection('users').updateOne({
name: 'Paul'
},{
age: 26
}).then((result) => { //正常の場合
console.log(result);
}).catch((error) => { //エラーの場合
console.log(error);
})
//削除
db.collection('users').deleteMany({
name: {$ne: 'John'} //"="以外の条件はこのように書く. これは not equalの例. その他のキーワードはhttps://www.mongodb.com/docs/manual/reference/operator/query/
}).then((result) => {
console.log(result);
}).catch((error) => {
console.log(error);
})
})
mongoDBとのORMで使う.
const mongoose = require('mongoose');
const validator = require('validator'); //mongooseはバリデーションライブラリを組み合わせて使う
const connectionURL = 'mongodb://127.0.0.1:27017/task-manager-api'
mongoose.connect(connectionURL, {
useNewUrlParser: true,
autoIndex: true
})
//modelの定義
const User = mongoose.model('User', { //model名をUserとするとMongoDBのDocument名は'users'となる
name:{
type: String,
required: true,
trim: true
},
password:{
type: String,
required: true,
trim: true,
minlength: 7,
validate(value) {
if (value.toLowerCase().includes('password')) {
throw new Error('password cannot contain "password"')
}
}
},
email:{
type: String,
required: true,
trim: true,
lowercase:true,
validate(value) {
if (!validator.isEmail(value)){ //バリデーションライブラリの使用
throw new Error('Email is invalid')
}
}
},
age: {
type: Number,
default:0,
validate(value) {
if (value < 0) {
throw new Error('age must be positive number')
}
}
},
tokens: [{ //リスト形式
token: {
type: String,
required: true
}
}],
avatar: {
type: Buffer //バイナリ形式...イメージファイルなどの保存
}
})
const me = new User({
name: 'Jeorge ',
password: 'jeorgePassWord',
email: 'Jeorge@yahoo.com ',
age: 45
})
me.save().then(() => { //保存でPromise
console.log(me);
}).catch((error) => {
console.log(error);
})
その1で定義したプロパティをSchemaに入れて、Schemaに対してファンクションなどを追加定義できる
const mongoose = require('mongoose');
const validator = require('validator');
const bcryptjs = require('bcryptjs');
const jwt = require('jsonwebtoken')
const userSchema = mongoose.Schema({ //Schemaの定義
name:{
type: String,
required: true,
trim: true
},
password:{
//...省略
},
email:{
//...省略
},
age: {
//...省略
}
},
{
timestamps: true //Schemaの2引数にtimestamps: trueを入れると`createdAt`と`updatedAt`の2項目が自動的に追加される. もちろんドキュメントのcreate/updateでそれぞれの項目が更新される
})
userSchema.methods.generateAuthToken = async function () { //methodsでインスタンスに紐ついたファンクションを定義する
const user = this
const token = jwt.sign({_id: user._id.toString()}, 'secretKey')
user.tokens = user.tokens.concat({token})
user.save()
return token
}
userSchema.statics.findByCredentials = async (email, password) => { //staticsでオブジェクトに紐ついたファンクションを定義する
const user = await User.findOne({email})
if (!user) {
throw new Error('Unable to login')
}
const isMatch = await bcryptjs.compare(password, user.password)
if (!isMatch) {
throw new Error('Unable to login')
}
return user
}
userSchema.pre('save', async function (next) { //preで"save"メソッドの前に実行することを定義する. ここではパスワードのhash化を入れている
const user = this
if (user.isModified('password')) {
user.password = await bcryptjs.hash(user.password, 8)
}
next()
})
userSchema.pre('remove', async function (next) { //ユーザ削除時にタスクも削除するcascade処理もpre - removeで入れることができる.
const user = this
await Task.deleteMany({owner: user._id})
next()
})
const User = mongoose.model('User', userSchema) //modelにはSchemaを渡す
module.exports = User
const express = require('express');
require('./db/mongoose');
const User = require('./models/user')
const Task = require('./models/task')
const app = express()
const port = process.env.PORT || 3000
app.use(express.json())
app.post('/users', async (req, res) => { //追加
const user = new User(req.body)
try {
await user.save()
res.status(201).send(user)
} catch (error) {
res.status(400).send(error)
}
})
app.get('/users', async (req, res) => { //全件取得
try {
users = await User.find({})
res.send(users)
} catch (error) {
res.status(500).send()
}
})
app.get('/users/:id', async (req, res) => { //1件取得
const _id = req.params.id
try {
user = await User.findById(_id)
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
app.patch('/users/:id', async (req, res) => { //更新
const updates = Object.keys(req.body)
const allowedUpdates = ['name', 'password', 'email', 'age']
const isValidOperation = updates.every((update) => allowedUpdates.includes(update)) //違うプロパティがセットされていないかチェック. チェックしないと想定外のプロパティがセットできてしまうため.
if (!isValidOperation) {
return res.status(400).send({'error': 'Invalid updates'})
}
try {
//const user = await User.findByIdAndUpdate(req.params.id, req.body, {new: true, runValidators: true}) //saveメソッドにミドルウェアをカマしたいときに効かないので、自前で編集する↓
const user = await User.findById(req.params.id)
updates.forEach((update) => user[update] = req.body[update])
await user.save()
if (!user) {
res.status(404).send()
}
res.send(user)
} catch (e) {
res.status(400).send()
}
})
app.delete('/users/:id', async (req, res) => { //削除
try {
const user = await User.findByIdAndDelete(req.params.id)
if (!user) {
res.status(404).send()
}
res.send(user)
} catch (e) {
res.status(500).send()
}
})
const Task = mongoose.model('Task', {
description:{
//省略
},
completed: {
//省略
},
owner: { //←UserのID
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
}
})
const userSchema = mongoose.Schema({
//省略
})
userSchema.virtual('tasks', {
ref: 'Task',
localField: '_id',
foreignField: 'owner'
})
const User = require('./models/user')
const Task = require('./models/task')
const main = async () => {
const task = await Task.findById('63553a6fa704fc04e8f5079a')
await task.populate('owner')
console.log(task.owner);
const user = await User.findById('635534c2622ec3cc1aa10d45')
await user.populate('tasks')
console.log(user.tasks);
}
main()
router.get('/tasks', auth, async (req, res) => {
try {
const match = {}
const sort = {}
if (req.query.completed) { //req.queryでクエリストリング
match.completed = req.query.completed === 'true'
}
if (req.query.sortBy) {
const parts = req.query.sortBy.split(':')
sort[parts[0]] = parts[1] === 'desc' ? -1 : 1
}
await req.user.populate({
path: 'tasks',
match, //populate.matchでフィルター条件を書く. ex: `completed: true`
options: { //populate.optionsでページネーションとソート
limit: parseInt(req.query.limit), //limitで取得件数
skip: parseInt(req.query.skip), //skipで読み飛ばし件数
sort //sortでソート条件. ex: `createdAt: 1`...createdAtの昇順 -1で降順
}
})
res.send(req.user.tasks)
} catch (error) {
res.status(500).send(error)
}
})
const bcryptjs = require('bcryptjs');
const hashPass = async () => {
const password = 'aawweav'
const hashedPass = await bcryptjs.hash(password, 8) //第2引数はハッシュ回数. 8が標準らしい
const isMatch = await bcryptjs.compare(password, hashedPass)
}
const jwt = require('jsonwebtoken')
const func = async () => {
const token = jwt.sign({_id: 'abc123'}, 'aaeebaetakpp') //第2引数はtoken化するキー
console.log(token);
const data = jwt.verify(token, 'aaeebaetakpp')
console.log(data);
}
↑のtokenの中身
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiJhYmMxMjMiLCJpYXQiOjE2NjUyMjE5Njl9.4JTv8RBeYpWmN_A3bVOZOqG6n9h7kCxDg9lSOzWaCyA
“.”で区切られた3つの値.
1つ目(base64エンコードされている): {“alg”:”HS256”,”typ”:”JWT”}
2つ目(base64エンコードされている): signの第1引数に生成されたタイムスタンプが付与されたjson
3つ目はハッシュ値
const multer = require('multer');
const sharp = require('sharp');
const upload = multer({
// dest: 'avatars', //ファイルとして保存する場合は保存先を指定する. S3とかもいけるはず.
limits: {
fileSize: 1000000 //ファイルサイズ制限
},
fileFilter(req, file, cb) { //拡張子によるファイル種類の制限
if(!file.originalname.match(/\.(jpg|jpeg|png)$/i)) {
return cb(new Error('please upload a jpg'))
}
cb(undefined, true)
}
})
router.post('/users/me/avatar', auth, upload.single('avatar'), async (req, res) => { //ミドルウェアでsingleを指定する
const buffer = await sharp(req.file.buffer).resize({width: 250, height:250}).png().toBuffer() //sharpによるリサイズと、png化
//req.file.bufferは multerのインストラクタでdestを指定しない場合に使用できる(メモリに保存する)
req.user.avatar = buffer
await req.user.save()
res.send()
}, (error, req, res, next) => {
res.status(400).send({error: error.message })
})
const sendgridMail = require('@sendgrid/mail');
sendgridMail.setApiKey(process.env.SENDGRID_API_KEY)
const sendWelcomeEmail = (email, name) => {
sendgridMail.send({
to: email,
from: 'test@mail.com',
subject: 'Thanks for joining in!',
text: `Welcome to the app. ${name}. Let me know how you get along with the app.`
})
}
const sendCancelEmail = (email, name) => {
sendgridMail.send({
to: email,
from: 'test@mail.com',
subject: 'Sorry to see you go!',
text: `Goodbye, ${name}. I hope to see you back sometime soon.`
})
}
module.exports = {sendWelcomeEmail, sendCancelEmail}
"scripts": {
"start": "node src/index.js",
"dev": "env-cmd nodemon src/index.js",
"test": "env-cmd -f ./tests/.env jest --watchAll --runInBand"
},
env-cmdでテスト用の環境変数を渡す--watchAll: nodemonのようなもの. 保存するごとにテストを実行できる--runInBand: すべて直列で起動する. task-manager appのときの、user-routerとtask-routerをそれぞれ実行する場合などに使用する.//特にrequireするものはない
beforeEach(setupDatabase) //testの実行の前に実行する関数を書ける
test('Should signup a new user', async () => { //test関数が1つのテストの単位. 第1引数にテストの名称や目的、第2引数にテストの関数
const user = await User.findById(response.body.user._id)
expect(user).not.toBeNull() //expectでアサートする
})
メール送信などでテストのときは実行したくないものは、__mocks__ディレクトリを/testsの下につくるとそちらのライブラリを見に行く
/tests/mocks/@sendgrid/mail.jsを中身を空で作る
module.exports = {
setApiKey() {
},
send() {
}
}
↓のsendWelcomeEmailはモックにできる.
//...
const { sendWelcomeEmail, sendCancelEmail } = require('../emails/account')
router.post('/users', async (req, res) => {
//...
sendWelcomeEmail(user.email, user.name)
//...
})
const request = require('supertest');
const express = require('express');
const app = express()
test('Should get profile for user', async () => { //引数にexpressを渡し、httpメソッド, set, send, attachなどをチェーンできる. 最後にexpectでhttpステータスコードを検証する
await request(app)
.get('/users/me')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.send()
.expect(200)
})
const express = require('express');
require('./db/mongoose');
const userRouter = require('./routers/user')
const taskRouter = require('./routers/task')
const app = express()
app.use(express.json())
app.use(userRouter)
app.use(taskRouter)
module.exports = app
// テストデータなどをまとめて書く用
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const User = require('../../src/models/user')
const Task = require('../../src/models/task');
const { MongoGridFSChunkError } = require('mongodb');
const userOneId = new mongoose.Types.ObjectId()
const userOne = {
_id: userOneId,
name: 'ringo',
email: 'ringo@mail.com',
password: 'pass1234',
tokens: [{
token:jwt.sign({_id: userOneId}, process.env.JWT_TOKEN_SECRET)
}]
}
const userTwoId = new mongoose.Types.ObjectId()
const userTwo = {
//...省略
}
const taskOne = {
_id: new mongoose.Types.ObjectId(),
description: 'First task',
completed: false,
owner: userOneId
}
const taskTwo = {
//...省略
}
const taskThree = {
//...省略
}
const setupDatabase = async () => { //... testごとの初期化を書く
await User.deleteMany()
await Task.deleteMany()
await new User(userOne).save()
await new User(userTwo).save()
await new Task(taskOne).save()
await new Task(taskTwo).save()
await new Task(taskThree).save()
}
module.exports = {setupDatabase, userOne, userOneId, userTwo, userTwoId,
taskOne, taskTwo, taskThree}
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/user')
const {setupDatabase, userOne, userOneId} = require('./fixtures/db');
beforeEach(setupDatabase) //...testのたびに初期化する
test('Should signup a new user', async () => {
const response = await request(app).post('/users').send({
name: 'paul',
email: 'paul@mail.com',
password: 'pass1234'
}).expect(201)
// Assert that the database was changed correctly
const user = await User.findById(response.body.user._id)
expect(user).not.toBeNull()
// Assertions about the response
expect(response.body).toMatchObject({
user: {
name: 'paul',
email: 'paul@mail.com'
},
token: user.tokens[0].token
})
expect(user.password).not.toBe('pass1234')
})
test('Should signup a new user', async () => {
const response = await request(app).post('/users/login').send({
email: userOne.email, password: userOne.password
}).expect(200)
const user = await User.findById(response.body.user._id)
expect(response.body.token).toBe(user.tokens[1].token)
})
test('Should get profile for user', async () => {
await request(app)
.get('/users/me')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`) //...Bearerの送信
.send()
.expect(200)
})
test('Should upload avatar image', async () => {
await request(app)
.post('/users/me/avatar')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.attach('avatar', 'tests/fixtures/profile-back.jpg') //...画像の送信
.expect(200)
const user = await User.findById(userOne._id)
expect(user.avatar).toEqual(expect.any(Buffer))
})
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/task')
const {setupDatabase, userOne, userOneId, taskOne, userTwo, userTwoId} = require('./fixtures/db');
beforeEach(setupDatabase)
test('Should create task for user', async () => {
const response = await request(app)
.post('/tasks')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.send({
description: 'From my test'
})
.expect(201)
const task = await Task.findById(response.body._id)
expect(task).not.toBeNull()
expect(task.completed).toEqual(false)
})
test('Should fetch owner tasks', async () => {
const response = await request(app)
.get('/tasks')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.send()
.expect(200)
expect(response.body.length).toEqual(2)
})
test('Should not delete other users tasks', async () => {
const response = await request(app)
.delete(`/tasks/${taskOne._id}`)
.set('Authorization', `Bearer ${userTwo.tokens[0].token}`)
.send()
.expect(404)
const task = await Task.findById(taskOne._id)
expect(task).not.toBeNull()
})
-これを参考
クライアントからカウントアップ→サーバの変数がカウントアップ→全ての接続クライアントが受信
const path = require('path');
const http = require('http')
const express = require('express');
const socketio = require('socket.io')
const app = express()
const server = http.createServer(app) //...httpでexpressをラップ
const io = socketio(server) //...httpをsocketioする
const port = process.env.PORT || 3000
const publicDirectoryPath = path.join(__dirname, '../public')
app.use(express.static(publicDirectoryPath))
let count = 0
io.on('connection', (socket) => { // connectionでクライアントと接続
console.log('New WebSocket connection');
socket.emit('countUpdated', count) // emitでクライアントの関数を呼び出し
socket.on('increment', () => { // socket.onでクライアントから受信
count++
// socket.emit('countUpdated', count) // socket.emitはリクエストのあったクライアントのみに返す
io.emit('countUpdated', count) // io.emitは全接続クライアントに返す
})
})
app.get('', (req, res) => {
res.sendFile('index.html')
})
server.listen(port, () => {
console.log('listen: ', port);
})
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
</head>
<body>
<h1>Chat App</h1>
<div class="main-content">
<button id="increment">+1</button>
</div>
<script src="/socket.io/socket.io.js"></script> <!-- このファイルは存在しないけど書く -->
<script src="/js/chat.js"></script>
</body>
</html>
const socket = io()
socket.on('countUpdated', (count) => { //サーバから呼び出される関数を socket.on で定義
console.log('The count has updated', count);
})
document.querySelector('#increment').addEventListener('click', () => {
console.log('clicked');
socket.emit('increment') // サーバに送信する関数
})
クライアントからメッセージを送信→全クライアントが受信
// 途中までは例1と同じ
io.on('connection', (socket) => {
console.log('New WebSocket connection');
socket.emit('message', 'Welcome!')
socket.on('sendMessage', (message) => { //クライアントからのメッセージを受け取り、、
io.emit('message', message) // 全クライアントに返す
})
})
<!-- bodyの中のみ記述 -->
<form>
<input type="text" placeholder="Message" name="message">
</input>
<button>send</button>
</form>
const socket = io()
socket.on('message', (message) => { //サーバから受信
console.log(message);
})
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault()
const message = e.target.elements.message.value
socket.emit('sendMessage', message) // formで送信
})
io.on('connection', (socket) => {
console.log('New WebSocket connection');
socket.emit('message', 'Welcome!')
socket.broadcast.emit('message', 'A new user has joined!') // broadcastで自分以外のクライアントに通知
socket.on('sendMessage', (message) => {
io.emit('message', message)
})
socket.on('disconnect', () => { // disconnectはビルトインメソッドで、ブラウザを閉じたときに発火する
io.emit('message', 'A user has left!')
})
})
クライアントのメッセージ送信→サーバ受信→(サーバ側処理結果)→クライアントが受け取る(callback)
socket.emit('sendMessage', message, (error) => {
if (error) { //サーバからのerrorを受け取って、エラーメッセージを表示
return console.log(error);
}
console.log('The message was delivered!');
}) // 3番目にcallbackファンクションを指定する
const Filter = require('bad-words') // 禁止文字ライブラリ(英語)
socket.on('sendMessage', (message, callback) => { //メッセージとファンクション(callback)を受け取る
const filter = new Filter()
if (filter.isProfane(message)) {
return callback('Profanity is not allowed!') // 禁止文字があるならエラーメッセージとreturn
}
io.emit('message', message)
callback()
})
socket.on('join', (options, callback) => {
const {error, user} = addUser({id: socket.id, ...options})
if (error) {
return callback(error)
}
socket.join(user.room) // joinで特定のroomに参加させる
socket.emit('message', generateMessage('Admin', 'Welcome!'))
socket.broadcast.to(user.room).emit('message', generateMessage('Admin', `${user.username} has joined!`)) // broadcast.to().emitで該当roomにだけ通知する
io.to(user.room).emit('roomData', { //io.to().emitも同様
room: user.room,
users: getUsersInRoom(user.room)
})
callback()
})
https://github.com/janl/mustache.js/ ライブラリ不要のtemplateシステム
<body>
<div class="chat">
<div class="chat__main">
<div id="messages" class="chat__messages"></div>
<div class="compose">
...
</div>
</div>
</div>
<script id="message-template" type="text/html"> <!-- scriptタグで囲ってtemplateとなるhtmlを記述する -->
<div class="message">
<p>
<span class="message__name"></span> <!-- 変数はで囲って指定できる -->
<span class="message__meta"></span>
</p>
<p></p>
</div>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.2.1/mustache.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <!-- cdnのjsを参照するだけ -->
...
</body>
const $messages = document.querySelector('#messages')
const messagesTemplate = document.querySelector('#message-template').innerHTML
socket.on('message', (message) => {
const html = Mustache.render(messagesTemplate, { // Mustache.render()で、templateタグと変数をオブジェクトで渡す
username: message.username,
message: message.text,
createdAt: moment(message.createdAt).format('h:mm a')
})
$messages.insertAdjacentHTML('beforeend', html) // htmlに挿入するだけ
autoscroll()
})
繰り返しタグも可能
<script id="sidebar-template" type="text/html">
<h2 class="room-title"></h2>
<h3 class="list-title">Users</h3>
<ul class="users">
<!-- 繰り返しオブジェクトをで囲む -->
<li></li> <!-- 要素の中のプロパティを指定 -->
</ul>
</script>
socket.on('roomData', ({ room, users }) => {
const html = Mustache.render(sidebarTemplate, {
room: room,
users: users
})
$sidebar.innerHTML = html
})
https://momentjs.com/ 日付フォーマットライブラリ
https://www.npmjs.com/package/qs クエリストリングをパースする
const {username, room} = Qs.parse(location.search, { ignoreQueryPrefix: true })
http://localhost?username=hoge&room=fugaから値を取れる
jsにcssを適用するときに適用するライブラリ.
こういう意味らしい.
// Creates style nodes from JS strings
“style-loader”
// Translates CSS into CommonJS
“css-loader”
// Compiles Sass to CSS
“sass-loader”
module: {
rules: [{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
}]
},
import './styles/styles.scss'