ElectronでHello world
久々に技術ブログを書いていきます。 (最近は書評ブログとかが多かったので、、、、)
babelやwebpack
「Electron 環境構築」とか「Electron Hello wold」とかで検索すると大体は単に画面にテキストを表示するのみで終わってしまっているのもが多いので今回はbabel
や webpack
などもいれてトランスパイルできるようにしていきます。
自分は下記の本を購入して勉強しました。おすすめの本です。
ElectronでHello world
まずはnode.jsをインストールしてnpm
コマンドが使えるようにしてください。
ググればいろいろでてきます。
その後、プロジェクトファイルを作成してnpm init
を実行する
mkdir Helloworld cd Helloworld npm init -y
すとJsonファイルが作成されます。
- package.json
{ "name": "Helloworld", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
各種パッケージファイルのインストール
- Electronをインストール
npm install electron
- webpackのインストール
npm install webpack
- reactのインストール(今回はreactを使用します)
npm install react react-dom
- 画面描写に必要なファイルのインストール
npm install babel-core babel-loader babel-preset-es2015 babel-preset-react
- Windowを作成するファイル(最初に呼ばれるファイル)
src/main/index.jsを作成する(src と mainは各自ディレクトリを作成してください)
import { app } from "electron"; import createWindow from "./createWindow"; app.on("ready",() => { // Windowを生成する createWindow(); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", (_e, hasVisibleWindows) => { if (!hasVisibleWindows) { createWindow(); } });
- createWindow.jsを作成
electronでwindowを作成するクラスを作成する
src/main/createWindow.js
import { BrowserWindow } from 'electron'; let win; function createWindow() { win = new BrowserWindow(); win.loadURL(`file://${__dirname}/../../index.html`); win.on("close", () => { win = null; }); } export default createWindow;
- index.htmlを作成
createwindowしたファイルの上に表示するhtmlファイルです (これはプロジェクトディレクトリの直下においてください)
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>ブログみてね!</title> </head> <body> <div class="window"> <div id="app" class="window-content"></div> </div> <script>require("./dist/renderer/app.js")</script> </body> </html>
- app.jsxの作成
ここでHello worldします
src/renderer/app.jsx
import React from "react"; import { render } from "react-dom"; render(<div> Hello World </div>, document.getElementById("app"));
- webpackの設定ファイルを作成する
プロジェクトディレクトリの直下に下記のファイルを作成する
webpack.config.js
module.exports = { target: "electron", node:{ __dirname:false, __filename:false }, resolve:{ extensions: [".js",".jsx"] }, module:{ rules:[ { test: /\.jsx?$/, exclude:/node_modules/, loader: "babel-loader" }, { test: /.css$/, loaders:["style-loader","css-loader?modules"] } ] }, entry: { "main/index":"./src/main/index.js", "renderer/app": "./src/renderer/app.jsx" }, output: { filename: "dist/[name].js" }, devtool:"source-map" };
- .babelrcを作成
このアプリで使用するEcmascriptのバージョンを指定しています。
.babelrc
{ "presets":["es2015","react"] }
## ビルドする
ここまでで完了です。 ビルドしてみましょう。
./node_modules/.bin/webpack
package.jsを編集する
ここでビルド後のファイルを最初に読み込ませるように修正する必要があるのでpackage.jsを編集します
main~の箇所を下記のように編集してください
"main": "dist/main/index.js",
起動する
./node_modules/.bin/electron .
すると表示されます。
今回参考にした本
下記になります。 すごくおすすめ
よければ
よければツイッターの方フォローお願いします。
自分の時間を取り戻そう-書評
一冊読み終わったので書評として残しておきます。
感想
この本は一言で言うと、
「自分の時間は重要度のかなり高いもので、それを手に入れるために生産性を追求しましょう」
と言うことだと思います。
自分のいつもできるだけ仕事を終わらして帰るようにしているがどうしても忙しいときは残業してしまっている。
そんな自分の働き方を見直す必要があるな〜と考えさせられました。
長い時間を投入するのは本当にいいことなのか
この本の中で
「長い時間を投入することは本当に最良の問題解決方法なのか」
ということが述べられていました。
身につけたいことがあったら もっと最短で身につけることはできないか、 生産性高く学習することはできないか、
ということを常に考え続ける必要があります。
例えば自分の子供が 「長い時間をかけて勉強して成績があがった」 より、 「勉強時間はたいして変わってないけど成績がぐんとあがった」 のほうがよっぽど価値のあることだとも述べられていました。
やりたくないことこそダラダラ続けてしまう
やりたくないはダラダラやるとなんとかなってしまう。 でも生産性に目を向けた時にやりたくないことに我慢できなくなり本当にやりたいことが見つかるとも書かれていました。
これにはすごく共感しました。
最低賃金はもっとあげたほうがいい
このように書かれてた理由がすごく興味深かったです。
「最低賃金の引き上げに反対し、海外からの労働者の受け入れに積極的な企業は安い労働力が豊富に入手できないとビジネスが成り立たない生産性の低い企業なのです。」
これには軽く衝撃でした。 たしかに短時間でかなりの利益をあげていればブラック企業は生まれない。 安い賃金で長時間働かせてなんとかしようとする企業はもうすでに生産性の低い企業としてレッテルを貼られているのと同じということと解釈しています。
なにか新しい技術や、ビジネスを目にした時に生産性という判断する
今ではITの力でいろんなことが自動化され、世の中をより便利にしてきました。
世の中で流行っているサービスのほとんどが「使うことで生産性があがるもの」です。
なので新しくビジネスを目にした時に「これは生産性があがるものなのか」という視点はとても大事だ思いました。
生産性が低いと思われる事例
生産性が低いと考えられる業務や、世の中の仕組みについていくつか書かれておりました。
例えば
- アパレル店員
- 選挙
- 学校制度 etc
この辺りは理由もしっかり書かれていて面白かったのでよければ読んでみてください。
商品リンクはこちら
よければ
ツイッターの方フォローよろしくお願いします
Electron+React.jsで作るチャットアプリ
Electron + React.jsで作るチャットアプリケーション
自分はElectronの初心者です。 Electronで本を購入しチャットアプリを写経して自分の中で理解を深めるためにここに残しておきます。
- 作成物
チャットアプリケーション
- 購入した本
- 感想
Electronについて概要からしっかり学べた
## Electron本体をインストール
- 作業用のディレクトリを作成
mkdir electron_chat
- 作業用のディレクトリを移動
cd electron_chat
- プロジェクト作成
npm init -y
(npmが使えなかった場合はインストールしてくる)
- Electronをインストール
npm install electron@1.6.1 --save-dev
- photonKitのインストール
photonKitとはmacOSネイティブアプリケーションのようなUIを簡単に実装できるFrameworkです。
npm install connors/photon --save-dev
- Reactのインストール
npm instal react@15.4.2 react-dom@15.4.2 react-router@3.0.0 --save
- babel関連モジュールのインストール
npm install babel-cli@6.18.0 babel-preset-es2015@6.18.0 babel-preset-react@6.16.0 --save-dev
実装
- index.js
import { app } from "electron"; import createWindow from "./createWindow"; import setAppMenu from "./setAppMenu" app.on("ready",() => { //アプリが起動したとき // 上でimportしたcreateWindowクラスのcreateWindow()メソッドを呼ぶ createWindow(); }); app.on("window-all-closed", () => { //mac以外の場合はウインドウを閉じた時にアプリを終了する if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", (_e, hasVisibleWindows) => { if (!hasVisibleWindows) { createWindow(); } });
createWindow.js
import { BrowserWindow } from 'electron'; let win; function createWindow() { win = new BrowserWindow(); //BrowserWindowクラスを使ってHTMLファイルを読み込み画面に表示 win.loadURL(`file://${__dirname}/../../index.html`); win.on("close", () => { win = null; }); } export default createWindow;
- index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Electron Chat</title> <link rel="stylesheet" href="node_modules/photon/dist/css/photon.css"/> </head> <body> <div class="window"> <div id="app" class="window-content"></div> </div> <script>require("./.tmp/renderer/app.js")</script> </body> </html>
- app.jsx
Railsでいうrouteみたいなファイル。この中にFirebaseの設定も書き込む
import React from "react"; import {render} from "react-dom"; import { Router, Route, hashHistory } from "react-router"; import Login from "./Login"; import Signup from "./Signup"; import Rooms from "./Rooms"; import Room from "./Room"; import firebase from "firebase/firebase-browser" // Routingの定義 const appRouting = ( <Router history={hashHistory}> //親のpathから繋げていく <Route path="/"> //例えば "/login"を設定するとLoginクラスが呼ばれる <Route path="login" component={Login} /> <Route path="signup" component={Signup} /> <Route path="rooms" component={Rooms} > <Route path=":roomId" component={Room} /> </Route> </Route> </Router> ); //Routingの初期化 if (!location.hash.length) { location.hash = "#/login"; } // Firebaseの初期化 //ここはFirebaseからこぴぺ const config = { apiKey: "*******************", authDomain: "*******************", databaseURL: "*******************", projectId: "*******************", storageBucket: "", messagingSenderId: "*******************" }; firebase.initializeApp(config); // Applicationの描画 render(appRouting, document.getElementById("app"));
- .babellc
Reactをjavascriptに変換するためのファイル
{ "preset": ["es2015,"react] }
- Login.js
ログイン画面の実装
import React from "react"; import { Link, hashHistory } from "react-router"; import Errors from "./Errors"; import firebase from "firebase/firebase-browser"; const FORM_STYLE = { margin:"0 auto", padding:30 }; const SIGNUP_LINK_STYLE = { display:"inline-block", marginLeft: 10 }; export default class Login extends React.Component { constructor(props) { super(props); //props:親から渡される情報 //state:自Componentの状態 this.state = { email: localStorage.userEmail || "", password: localStorage.userPassword || "", errors:[], }; //それぞれのメソッドをbindする //bindすることでフロントの状態変化を検知できる this.handleOnChangeEmail = this.handleOnChangeEmail.bind(this); this.handleOnChangePassword = this.handleOnChangePassword.bind(this); this.handleOnSubmit = this.handleOnSubmit.bind(this); } // 下のinputの状態が変化したら呼ばれる handleOnChangeEmail(e){ this.setState({ email: e.target.value}); } // 下のinputの状態が変化したら呼ばれる handleOnChangePassword(e){ this.setState({ password: e.target.value }); } //ログイン処理 handleOnSubmit(e){ // 二つのinputタグの状態を取得する const { email, password } = this.state; const errors = []; let isValid = true; e.preventDefault(); if (!email.length) { //emailに何もデータが入っていない場合 isValid = false; errors.push("Email cannot blank."); } if (!password.length) { //passwordに何もデータが入っていない場合 isValid = false; errors.push("Password cannot be blank."); } if (!isValid) { //必須入力チェックに該当した場合はエラーを表示する this.setState({ errors }); return; } //Firebaseのログイン処理 //この辺はリファレンスを見る firebase.auth().signInWithEmailAndPassword(email,password).then(() => { //次回ログインし簡略化のため、localStorageに値を保存 localStorage.userEmail = email; localStorage.userPassword = password; //チャットルーム一覧画面へ遷移 hashHistory.push("/rooms"); }).catch(() => { // Firebaseでログインエラーになった場合 this.setState({errors: ["Incorrect email or password."] }) }); } //フロントのレンダリング render(){ return ( <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}> <Errors errorMessages={this.state.errors} /> <div className="form-group"> <label>Email address</label> <input type="email" className="form-control" placeholder="email" onChange={this.handleOnChangeEmail} value={this.state.email} /> </div> <div className="form-group"> <label>Password</label> <input type="password" className="form-control" placeholder="password" onChange={this.handleOnChangePassword} value={this.state.password} /> </div> <div className="form-group"> <button className="btn btn-large btn-default">Login</button> <div style={SIGNUP_LINK_STYLE}> <Link to="/signup">create new account</Link> </div> </div> </form> ); } }
- signup.js
サインアップページ
import React from "react"; import { Link, hashHistory } from "react-router"; import Errors from "./Errors"; import firebase from "firebase/firebase-browser"; const SIGNUP_FORM_STYLE = { margin: "0 auto", padding: 30 }; const CANCEL_BUTTON_STYLE = { marginLeft: 10 }; export default class Signup extends React.Component{ constructor(props){ super(props); //状態の初期化 this.state = { email:"", password:"", name:"", photoURL:"", errors:[] }; //各種bindする this.handleOnChangeEmail = this.handleOnChangeEmail.bind(this); this.handleOnChangePassword = this.handleOnChangePassword.bind(this); this.handleOnChangeName = this.handleOnChangeName.bind(this); this.handleOnChangePhotoURL = this.handleOnChangePhotoURL.bind(this); this.handleOnChangeSubmit = this.handleOnChangeSubmit.bind(this); } handleOnChangeEmail(e){ this.setState({ email: e.target.value}); } handleOnChangePassword(e){ this.setState({ password:e.target.value }); } handleOnChangeName(e){ this.setState({ name: e.target.value}); } handleOnChangePhotoURL(e){ this.setState({photoURL: e.target.value }); } handleOnChangeSubmit(e){ const {email, password, name,photoURL } = this.state; const errors = []; let isValid = true; e.preventDefault(); if (!email.length) { isValid = false; errors.push("Email address cannt be blank."); } if (!password.length) { isValid = false; errors.push("Password cannot be blank"); } if (!name.length) { isValid = false; errors.push("Name cannot be blank."); } if (!isValid) { this.setState({errors}); return; } // Firebaseの新規アカウント作成処理 firebase.auth().createUserWithEmailAndPassword(email,password).then(newUser => { //ユーザー情報を更新する return newUser.updateProfile({ displayName:name,photoURL }); }).then(() => { //チャットルーム一覧画面へ遷移 hashHistory.push("/rooms"); }).catch(err => { // Firebaseでエラーになった場合 this.setState({errors: [err.message] }) }); } render(){ return ( <form style={SIGNUP_FORM_STYLE} onSubmit={this.handleOnChangeSubmit}> <Errors errorMessages={this.state.errors} /> <div className="form-group"> <label>Email address *</label> <input type="email" className="form-control" placeholder="email" value={this.state.email} onChange={this.handleOnChangeEmail} /> </div> <div className="form-group"> <label>Password *</label> <input type="password" className="form-control" placeholder="password" value={this.state.password} onChange={this.handleOnChangePassword} /> </div> <div className="form-group"> <label>User name *</label> <input type="text" className="form-control" placeholder="user_name" value={this.state.name} onChange={this.handleOnChangeName} /> </div> <div className="form-group"> <label>Photo URL</label> <input type="text" className="form-control" placeholder="photo url" value={this.state.photoURL} onChange={this.handleOnChangePhotoURL} /> </div> <div className="form-group"> <button className="btn btn-large btn-primary">Create new account</button> <Link to="/login"> <button type="button" style={CANCEL_BUTTON_STYLE} className="btn btn-large btn-default" > cencel </button> </Link> </div> </form> ); } }
- Rooms.jsx
左ペインにRoom一覧を表示し、右ペインにはチャットルームを表示する
import React from "react"; import { hashHistory } from "react-router"; import RoomItem from "./RoomItem"; import firebase from "firebase/firebase-browser"; const ICON_CHAT_STYLE = { fontSize: 120, color:"#DDD" }; const FORM_STYLE = { display: "flex" }; const BUTTON_STYLE = { marginLeft: 10 }; export default class Rooms extends React.Component { constructor(props){ super(props); this.state = { roomName: "", rooms:[] }; // Firebaseデータベースを初期化 this.db = firebase.database(); this.handleOnChangeRoomName = this.handleOnChangeRoomName.bind(this); this.handleOnSubmit = this.handleOnSubmit.bind(this); } componentDidMount(){ //コンポーメントの初期化時にチャットルーム一覧を取得する this.fetchRooms(); } handleOnChangeRoomName(e){ this.setState({ roomName: e.target.value }); } handleOnSubmit(e){ const { roomName } = this.state; e.preventDefault(); if (!roomName.length) { return; } //Firebaseデータベースに新規チャットルームのデータ作成 const newRoomRef = this.db.ref("/chatrooms").push(); const newRoom = { description: roomName }; //作成したチャットルームのdescriptionを更新する newRoomRef.update(newRoom).then(() => { //状態を再初期化する this.setState({ roomName:"" }) //チャットルーム一覧を再取得 return this.fetchRooms().then(() => { //右ペインを作成した詳細画面に遷移させる hashHistory.push(`/rooms/${newRoomRef.key}`); }); }); } //チャットルーム一覧の取得 fetchRooms(){ //Firebaseデータベースからチャットルームを20件取得 return this.db.ref("/chatrooms").limitToLast(20).once("value").then(snapshot => { const rooms = []; snapshot.forEach(item => { //データベースから取得したデータオブジェクトとして取り出す rooms.push(Object.assign({key:item.key}, item.val())); }); //取得したオブジェクトの配列をコンポーメントのstateにセット this.setState({rooms}); }); } //左ペイン(チャットルーム一覧)の描写処理 renderRoomList(){ const { roomId } = this.props.params; const { rooms, roomName } = this.state; return ( <div className="list-group"> //ルーム一覧をfetchRoomsから取得したデータ分だけ表示する //selectしたらRoomItemにroomIdを設定する // RoomItemタグを使っているといことはRoomItemクラスを呼んでいる {rooms.map(r => <RoomItem room={r} key={r.key} selected={r.key === roomId} /> )} //新しいチャットルームを作成するためのinputエリア <div className="list-group-header"> <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}> <input type="text" className="form-control" placeholder="New room" onChange={this.handleOnChangeRoomName} value={roomName} /> <button className="btn btn-default" style={BUTTON_STYLE}> <span className="icon icon-plus" /> </button> </form> </div> </div> ); } //右ペイン(チャットルーム詳細)の描写処理 renderRoom(){ if (this.props.children) { // propsにデータが設定されている = Roomが設定されている return this.props.children; } else { return ( <div className="text-center"> <div style={ICON_CHAT_STYLE}> <span className="icon icon-chat" /> </div> <p> Join a chat room from the sidebar or create your chat room. </p> </div> ); } } render(){ return ( <div className="pane-group"> //左ペイン <div className="pane-sm sidebar">{this.renderRoomList()}</div> //右ペイン <div className="pane">{this.renderRoom()}</div> </div> ); } }
- RoomItem.jsx
Room一覧ののひとつひとつのアイテム
import React from "react" import { Link } from "react-router" const LINK_STYLE = { color:"inherit", textDecoration:"none" }; export default function RoomItem(props) { //propsからアイテムひとつひとつを設定する const { selected } = props; const { description, key } = props.room; return ( <div className={selected ? "list-group-item selected":'list-group-item'}> //`/rooms/${key}`の設定でroom.jsxが呼ばれて設定される <Link to={`/rooms/${key}`} style={LINK_STYLE}> <div className="media-body"> <strong>{description}</strong> </div> </Link> </div> ); }
- Room.jsx
選択されているチャットルームでチャット処理を実装する
import React from "react"; import Message from "./Message"; import NewMessage from "./NewMessage"; import firebase from "firebase/firebase-browser"; const ROOM_STYLE = { padding: "10px 30px" }; export default class Room extends React.Component { constructor(props){ super(props); this.state = { description: "", messages:[], }; this.db = firebase.database(); this.handleOnMessagePost = this.handleOnMessagePost.bind(this); } componentDidMount(){ const { roomId } = this.props.params; //コンポーメントの初期化時にチャットルームの詳細情報を取得する this.fetchRoom(roomId); } componentWillReceiveProps(nextProps){ const { roomId } = nextProps.params; if (roomId === this.props.params.roomId) { //チャットルームのIDいん変更がなければなにもしない return; } if (this.stream) { //メッセージの監視解除 this.stream.off(); } // stateの再初期化 this.setState({ messages:[] }); //チャットルーム詳細の再取得 this.fetchRoom(roomId); } componentDidUpdate(){ setTimeout(() => { //画面下端へスクロール this.room.parentNode.scrollTop = this.room.parentNode.scrollHight; }, 0); } componentWillUnmount(){ if (this.stream) { //メッセージ監視を解除 this.stream.off(); } } //メッセージ投稿処理 handleOnMessagePost(message){ const newItemRef = this.fbChatRoomRef.child("messages").push(); ///Firebaseにログインしているユーザーを投稿ユーザーとして利用 this.user = this.user || firebase.auth().currentUser; return newItemRef.update({ writtenBy:{ uid:this.user.uid, displayName: this.user.displayName, photoURL:this.user.photoURL, }, time:Date.now(), text:message, }); } fetchRoom(roomId){ //Firebaseデータベースからチャットルーム詳細データの参照を取得 this.fbChatRoomRef = this.db.ref("/chatrooms/" + roomId); this.fbChatRoomRef.once("value").then(snapshot => { const { description } = snapshot.val(); this.setState({ description }); window.document.title = description; }); this.stream = this.fbChatRoomRef.child("messages").limitToLast(10); //チャットルームのメッサージ追加を監視 this.stream.on("child_added",item => { const { messages } = this.state || []; // 追加されたメッセージをstateにセット messages.push(Object.assign({key:item.key},item.val())); this.setState({ messages }); }); } render(){ const { messages } = this.state; return ( <div style={ROOM_STYLE} ref={room => this.room = room} > <div className="list-group"> //fetchRoomで取得したデータ分だけ画面に表示 {messages.map(m => <Message key={m.key} message={m} />)} </div> <NewMessage onMessagePost={this.handleOnMessagePost} /> </div> ); } }
- Message.jsx
1メッセージのオブジェクト
import React from "react"; import Avatar from "./Avatar"; const MEDIA_BODY_STYLE = { color: "#888", fontSize:".9em" }; const TIME_STYLE = { marginLeft: 5 }; const TEXT_STYLE = { whiteSpace:"normal", wordBreak:"break-word" }; export default function Message(props){ const { text, time, writtenBy } = props.message; const localeString = new Date(time).toLocaleString(); return ( <div className="list-group-item"> <div className="media-object pull-left"> <Avatar user={writtenBy} /> </div> <div className="media-body"> <div style={MEDIA_BODY_STYLE}> <span>{writtenBy.displayName}</span> <span style={TIME_STYLE}>{localeString}</span> </div> <p style={TEXT_STYLE}> {text} </p> </div> </div> ); }
- NewMessage.jsx
投稿するinputタグ
import React from "react" const FORM_STYLE = { display: "flex" }; const BUTTON_STYLE = { marginLeft: 10 }; export default class NewMessage extends React.Component{ constructor(props){ super(props); this.state = {message: ""}; this.handleOnChange = this.handleOnChange.bind(this); this.handleOnSubmit = this.handleOnSubmit.bind(this); } handleOnChange(e){ this.setState({message:e.target.value}); } handleOnSubmit(e){ const { onMessagePost } = this.props; if (!onMessagePost || !this.state.message.length) { return; } onMessagePost(this.state.message); this.state({ message: ""}); e.preventDefault(); } render(){ return( <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}> <input type="text" className="form-control" onChange={this.handleOnChange} value={this.state.message} /> <button className="btn btn-large btn-primary" style={BUTTON_STYLE}>POST</button> </form> ); } }
- Avatar.jsx
import React from "react" const AVATAR_STYLE = { width:32, textAlign:"center", fontSize:24 }; export default function Avatar(props){ const { photoURL } = props.user; if(photoURL){ // photoURLURLが設定されている場合img要素を表示 return <img className="img-rounded" src={photoURL} style={AVATAR_STYLE} />; } else { //photoURLが設定されていない場合代替えとしてiconを表示 return ( <div style={AVATAR_STYLE}> <span className="icon icon-user" /> </div> ); } }
実行
- Reactをトランスコンパイル
./node_modules/.bin/babel --out-dir tmp src
- 実行
./node_modules/.bin/electron .
すると、実行できる
最後に
ただのメモ書きになってしましたが、 この本を勉強してかなり理解が深まったのでおすすめです。
本をブックオフに持って行く前にメルカリに出品したほうがいい!
普段から本をよく読みます。 今引越しすることを考えていて本をなんとか処分したいと考え、「メルカリ カウル」を使って出品してみました! 出品していろいろメリットがあったのでそれについて書いていきます。
ブックオフにはいかないがメルカリはみる
普段から本をよく読みますが、ブックオフで本を買ったことは一回もありません。全て新品で購入していました。 そもそもわざわざブックオフに足を運んで本を買いに行くこと自体がめんどくさいからです。
そして、本を安く買って読書をはじめようとか思う人ってだいたいメルカリをみるような気がします。
本って大体タイトルを見て欲しいと思うし、試着とかそういうのもいらないのでネットで買うのがかなり効率的です。
本を売る側も出品や送付が簡単で、買う側も簡単。 双方にメリットがあるメルカリで売ることがベストだと思います。
また、ブックオフに持ってくとせっかくいい本がブックオフの大量に並んだ本の中に埋もれてしまいます。 そうなると古本を欲しい人がいい本を見つけづらくなります。 せっかくいい本を売るんだから他の人たちにも読んで欲しいそれが私の願いでそれを実現できるのがメルカリです。
ブックオフより高く売れる
自分が大学生の頃本を十冊ほどブックオフに持って行ったら合計が200円とかだった記憶があります。 本を新品で買って1日で読んで次の日に売りに行った本が50円でした。
ですが、メルカリで本を7冊売って売り上げは2910円でした!!
ブックオフよりかなり高く売れています。 いい本が高く売れて買った人もいい本を読んでいただける。すごく幸せなことです。
メルカリで売るデメリット
- 早く売れない
値下げ交渉をしてきたり、発送に手間がかかったりなどするので
「来週引越しするから今すぐ本を全部処分したい!」
みたいにすぐに大量に処分したい人には向かないでしょう
- 梱包が必要
一冊一冊梱包して送る必要があります。 自分はその手間を省くために封筒の中に梱包材がすでに入ってるものをアマゾンで購入しています。
これを持っていたら本を中にいれてあとはファミマに持っていきメルカリラクラク便で送るだけなので楽です。
最後に
いかがだったでしょうか。 メルカリかなりおすすめです。
まだまだ、本も残っていますが、よければどうぞ
https://www.mercari.com/jp/u/228705595/
最後まで見ていただきありがとうございました。 よろしければTwitterのフォローよろしくお願い致します。
小さなチーム大きな仕事-書評
読んだので書評として残して起きます。
とにかく無駄なものは排除
この本では仕事の中にある無駄に削ぎ落とすと小さなチームで十分ではないかということを説いた本だと私は思います。
例えば
一時間かかる会議を設定し、 参加者を一〇人召集したとしよう。 これは実際には一時間の会議ではなく 一〇時間の会議である
会議を大人数ですることで時間が失われます。 その分生産性が失われて人が足りないと錯覚してしまいます。
なので会議はなくす、もしくは極力少なくすることが大事だと書かれていました。
誰かに話しかけること自体に相手の時間を奪うことと同じとも書かれておりました。
どうしても何か聞きたいことなどはチャットを使うことが私は有効だと思います。
モニターから目を離さずに質問などすることができるので集中力を切らさないことができると私は考えています。
すごい製品や、サービスを作る秘訣
これからビジネスをはじめようとか、はじめたい方へのアドバイスも書かれていました。
すごい製品やサービスを 生み出す最も単純な方法は、 あなたが使いたいものを作ることだ。
この考え方ってすごく大事だな〜と思っていて、自分が個人開発するときは全てこの基準でソースコードを書いています。
今この記事を書いているエディタも自分で開発したツールです。
近々リリース致しますのでよければダウンロードおねがい致します。(無料です)
顧客の言うことを聞いていたら、 もっと速く走る馬を彼らに与えていただろう ヘンリー・フォード
顧客の言うことを聞きすぎるのはよくないとをいっていると私は解釈しました。
自分の尊敬するApple社はいつもこの考え方をしてると私は思っています。
MacにUSBを取り去ってUSB-Cに変えて批判を受けても、今一番売れているノートパソコンはMacbookproです。
自分の中の何かしら信念を持って製品を作り続けるということはかなり重要な考え方だと私は思います。
注文の多い少数の顧客を幸せにするために 多くの人に迷惑をかけ、 商品を台無しにしてしまうのは良くない。
注文の多い顧客に対してあれこれカスタマイズして疲弊するのはその製品自体をどんどん悪くするのと同じだと解釈しました。
業務でこういうことを経験したことがあってかなりぐさっときました。
## 商品リンク
よろしければ
ブログ更新したらツイートしたりしていますのでよろしければフォローお願い致します。
マネーフォワードのような横スワイプでタブを切り替えできるライブラリXLPagerTabStripを使ってみた
横スワイプで画面を切り替えれるかっこいいアニメーションを導入したくてライブラリを導入したのでメモとして残しておきます。
今回導入するのはこちらです。
かっこいいですね。
Install
Cocoapodsから導入できます。 Cocoapodsを入れてない場合は導入してください。 (導入方法はググればたくさんでてきます)
Podfileに下記を追加
pod 'XLPagerTabStrip'
追加したPodFileを保存し、下記を実行
pod install
管理元ViewControllerと切り替えるViewControllerを作成する
ルートのViewControllerと切り替え用のViewControllerを作成する
次に、切り替え用のViewControllerそれぞれにStoryboardIDを設定してください
(もうひとつのstoryboardには別のIDを設定してください)
ソースコード設定
下記のように設定する
管理するルートViewController
import UIKit import XLPagerTabStrip class ViewController: ButtonBarPagerTabStripViewController{ override func viewDidLoad() { //バーの色 settings.style.buttonBarBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1) //ボタンの色 settings.style.buttonBarItemBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1) //セルの文字色 settings.style.buttonBarItemTitleColor = UIColor.white //セレクトバーの色 settings.style.selectedBarBackgroundColor = UIColor(red: 254/255, green: 0, blue: 124/255, alpha: 1) super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { //管理されるViewControllerを返す処理 let firstVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "First") let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second") let childViewControllers:[UIViewController] = [firstVC, secondVC ] return childViewControllers } }
管理されるViewController 管理されるViewControllerの数だけViewControlerを下記のように編集してください。
import UIKit import XLPagerTabStrip class Chenge1ViewController: UIViewController, IndicatorInfoProvider { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } //必須 func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { return "First" } }
ルートのViewControllerにCollectionViewとScrollViewを追加する
管理するViewControllerにCollectionViewとScrollViewを追加してください。
CollectionView:切り替えタブ表示エリア ScrollView:表示するViewエリア
となります。
Storyboardの設定
CollectionView、ScrollViewそれぞれに下記の操作をおこなってください。
CollectionViewもしくはScrollViewを右クリック→Referencing outletのNew Reference outletをドラッグして 親のViewControllerにカーソルを当てる→containerViewを選択
実行してみる
カスタマイズ
下記の部分を変更することでタブをカスタマイズすることができます。
override func viewDidLoad() { //バーの色 settings.style.buttonBarBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1) //ボタンの色 settings.style.buttonBarItemBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1) //セルの文字色 settings.style.buttonBarItemTitleColor = UIColor.white //セレクトバーの色 settings.style.selectedBarBackgroundColor = UIColor(red: 254/255, green: 0, blue: 124/255, alpha: 1) super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. }
詳しくはGithubに書かれていますのでここをご覧ください。
サンプルは下記にありますのでよければどうぞ
よろしければツイッターもフォローお願いします
時間がないを言い訳にしないためにしていること
エンジニアたるもの勉強をし続けることが大事だとネット上で言われ続けています。
これについては賛否両論あるかと思いますが個人的にMarkdownでかけるメモアプリを開発しています。 (この記事も自分で作ったアプリでお風呂の中でで記事を書いています。)
仕事をしながら個人アプリを開発するにあたってやっていることを共有したいと思います。
迷うことを極力減らす
[自分を操る超集中]
この本の中で 「迷うことが疲れにつながる」とかかれていました。
仕事が終わって家に着き、自分のやりたいことができる時間ができたときに
「何から始めようかなー🤔」
と迷うことが疲れにつながり時間を無駄に消費してしまいます。
ですので自分は行きの電車や帰りの電車で帰ってからやることリストをトレロにまとめています。
このリストを作成するにあたって気をつけているのは優先順位の高い順で書くことです。 そうすることで、 パソコンを開いたら何も考えずリストの上から順に進めていけばいいだけというようにしています。
スキマ時間を有効活用
自分は基本的いつでも携帯を持っていて スキマ時間になったらやりたいことのためにアイデアを絞ったり、記事を書いたりしています。
電車の中や仕事の昼休みなど細切れの時間をまとめるとすごい時間なります。 これを有効活用するように考えています。
アイデアはすぐにメモに書き出すこと。 頭の中のままにするとすぐ忘れます。
(メモの自分の作成しているアプリで記入しています。)
記事を書くときは画像など どうしてもパソコンのいる作業以外は携帯で済ませておくようにしています。
土日に大きな時間が取れるとは限らない
これは自分だけかもしれませんが 土日にたくさん時間が取れるとは限りません。
出かけたり、急な遊びの予定が入ったり、集中できなくてダラダラしてしまったりしてしまうかと思います。
ですので毎日コツコツ続けることが大事だと思います。
よければ
- ツイッターのフォローお願い致します。