vscodeの勧め
初めに
インストール方法
vscodeが未インストールの場合、環境構築スライドを参照しインストールを行う
vsocode vs Cloud IDE(Cloud9)
vscode
- メリット
- IDEに拡張機能が追加できる
- HTML開発時にホットリロードが使用で切るようになる。(ソースコード編集時の確認でブラウザの更新ボタンやF5を押下する必要がない)
- 全角スペースが視覚的に発見しやすくなる。(研修内で、全角スペースによる不具合がが何件も起こっている)
- カッコ{}が閉じられているか視覚的に発見しやすくなる
- デメリット
- なし
Cloud IDE(Cloud9)
- メリット
- ローカル環境(OSやスペック)に依存しない開発が可能である
- デメリット
- リモートで接続しているため、ラグが少しある
- AWSリソースを使用しているため、料金がかかる
- 拡張機能の導入が難しい
npm VS CDN
※npm上でのVue使用は、CodeCampの想定と異なり、課題提出時にVueファイルをHtmlファイルに書き換える必要があるためCDNを用いるのがよい。
しかし、実際のプロジェクトでは、圧倒的にnpmの使用が多い。
拡張機能の導入方法
- vscodeを開く
- サイドバーの拡張機能をクリックする(ctr + shift + Xのシュートカットでも遷移可能)
- 検索窓でインストールを行いたい拡張機能名を入力する
- 拡張機能のページに遷移し、インストールボタンを押下する
vscode must install extenstions
- Live Server
- ローカル環境でHTMLとCSSをライブリロード(自動更新)できるようにするツール
- Auto Rename Tag
- 開始タグを編集すれば、対応する終了タグも自動的に変更されます
- HTML CSS Support
- HTMLとCSSの連携を強化する
- Git History
- リポジトリの歴史を視覚的に見ることができる
- Git Graph
- リポジトリのブランチ構造を視覚的に表示する
開発の仕方
- 開発ディレクトリの作成
- 右クリックでvscodeを開く
- git init
- htmlファイルを作成する
- vscode右下のhttp serverを押下する
- ソースコードを編集する
オプション
拡張機能の停止・アンインストール
- vscodeを開く
- サイドバーの拡張機能をクリックする(ctr + shift + X シュートカットでも遷移可能)
- 検索窓で停止・アンインストールを行いたい拡張機能名を入力する
- 拡張機能のページに遷移し、停止またはアンインストールボタンを押下する
Vue開発時に自動補完を使用する方法
※CodeCamp、指定のVue使用方法と異なり、課題提出時にVueファイルをHtmlファイルに書き換える必要がある
package com.example.demo;
import java.net.UnknownHostException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@RestController
public class HelloController {
// private Reception reception = new Reception();
//
// @Autowired
// public void ReceptionController(Reception reception) {
// this.reception = reception;
// }
@Autowired
private User user = new User();
@PostMapping("/hello")
public String hello(HttpServletRequest req, @RequestBody User reqUser) throws UnknownHostException {
// if (reception.getReceptionAt() == null) {
// reception.setHost(InetAddress.getLocalHost().getHostName());
// reception.setReceptionAt(LocalDateTime.now());
// reception.setMessage("セッション開始");
// } else {
// reception.setMessage("セッション中");
// }
// return String.format("%s, ", reception.getMessage()) +
// String.format("Host: %s, ", reception.getHost()) +
// String.format("受付日時: %s", reception.getReceptionAt());
if (req.getSession(false) == null) {
user.setUsername(reqUser.getUsername());
user.setEmpNum(reqUser.getEmpNum());
user.setRole(reqUser.getRole());
} else {
return "session中";
}
return String.format("%s, ", reqUser.getUsername()) +
String.format("Host: %s, ", reqUser.getEmpNum()) +
String.format("受付日時: %s", reqUser.getRole());
}
@GetMapping("/bye")
public String goodbye(HttpSession session) {
session.invalidate();
return "セッション切断";
}
@GetMapping("/test")
public String goodbye() {
return String.format("%s, ", user.getUsername()) +
String.format("Host: %s, ", user.getEmpNum()) +
String.format("受付日時: %s", user.getRole());
}
}
package com.example.demo;
import java.io.Serializable;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
@Component
@SessionScope
public class User implements Serializable {
private static final long serialVersionUID = -3101986789734320497L;
private String username;
private String empNum;
private String role;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmpNum() {
return empNum;
}
public void setEmpNum(String empNum) {
this.empNum = empNum;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
<html>
<div id="app">
<User-detail :user=selectedUser></User-detail>
<user-list :users v-model="selectedIndex"></user-list>
</div>
</html>
<!-- 2.CDNを読み込む -->""
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
// Vue.jsをマウントする
const app = Vue.createApp({
data() {
return {
// dummy_data
users: [
{
name: 'user1',
age: 23,
},
{
name: 'user2',
age: 30
},
{
name: 'user3',
age: 40
},
],
selectedIndex: -1
}
},
methods: {
onClick() {
alert('Clicked!')
}
},
computed: {
selectedUser() {
if(this.selectedIndex === -1 ) {
return {name: '選択なし', age: '選択なし'}
} else {
return this.users[this.selectedIndex]
}
}
},
components: {
'UserList':{
props: {
users: Array,
modelValue:Number
},
emits:[
'update:modelValue'
],
methods: {
selectedUser(index) {
console.log('[debug]selectedUser')
this.$emit('update:modelValue', index)
}
},
template: `
<div>
<h2>ユーザ一覧</h2>
<div v-for="(user, index) in users" key="index">
id: {{index}} name: {{user.name}} age: {{user.age}}
<button @click="selectedUser(index)">選択</button>
</div>
</div>`
},
'UserDetail':{
props: {
user: Object
},
template: `
<div>
<h2>ユーザ詳細</h2>
<div>{{user}}</div>
</div>`
}
}
})
app.mount('#app');
</script>
debug時
以下のサンプルコードを用いて、Vueのコーディング時に散見されるミスの発見方法を説明する。記述忘れ、変数&関数名の間違いの2パートに分けた。
記述忘れ
ここでは、props,emit,state(data)部分に関する記述忘れについて説明する。
props
-
propsの宣言が行われていない時
TODO:該当ソースコードと画像の添付コンポーネントのprops宣言がされていない時のconosleの出力
[Vue warn]: Property "selectedUser" was accessed during render but is not defined on instance. at <App>
devtoolsを確認するとpropsの値がundefindになっているためprops周りの記述に間違いがあることに気づくことができる。
-
プロパティの受け渡し(v-bind)が行われていない時
TODO:該当ソースコードと画像の添付props のバリデーションでrequireを付与していない場合、コンソールでwarnやerrが出力されない
devtoolsを確認するとpropsの値がundefindになっているためprops周りの記述に間違いがあることに気づくことができる。
validationについての参考URL
https://ja.vuejs.org/guide/components/props
emit
-
コンポーネントの$emitの宣言がされていない時
TODO:該当ソースコードと画像の添付コンポーネントの$emitの宣言がされていない時の場合、コンソールでwarnやerrを吐かれることはない。devetoolsで確認するとnot declared(宣言がされていない)と出る
-
受け渡しの記述漏れ(html側の記述)があるとき
TODO:該当ソースコードと画像の添付
devtoolにevent listenrsに宣言したカスタムイベント名が表示されなくなる
リアクティブプロパティ(data,state)
1.3 リアクティブプロパティの宣言を行っていない、参照時のthis忘れ
thisとは何か
-
typo(打ち間違い)
以下のようなミスが散見される。
data → date
onClick → onclick (大文字小文字のミス)
1.4 import時のファイルのpathを間違えている -
変数名、event名、メゾット名の衝突
開発時TIps
methodsや算出プロパティ、ウォッチャを実装するときは、concole.log(${関数名}
)で標準出力させるようにしておくことで適切なタイミングで呼び出されているか確認できるのでお勧めです。
v-for key
https://qiita.com/JetNel0/items/1f618683e4acce5f9aa6
<html>
<div id="app">
<User-detail :user=selectedUser></User-detail>
<user-list :users v-model="selectedIndex" ></user-list>
</div>
</html>
<!-- 2.CDNを読み込む -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
// Vue.jsをマウントする
const app = Vue.createApp({
mounted() {
console.log(this)
},
data() {
return {
// dummy_data
users: [
{ name: 'user1', age: 23, Birthplace: 'Tokyo' },
{ name: 'user2', age: 30, Birthplace: 'Osaka' },
{ name: 'user3', age: 40, Birthplace: 'Tokyo' }
],
selectedIndex: -1
}
},
computed: {
selectedUser() {
if (this.selectedIndex === -1) {
return { name: '選択なし', age: '選択なし', Birthplace: '選択なし' }
} else {
return this.users[this.selectedIndex]
}
}
},
methods: {
deleteUser(index) {
console.log('[debug]deleteUser')
this.users.splice(index, 1)
}
},
components: {
'UserList': {
props: {
users: Array,
modelValue: Number
},
emits: [
'update:modelValue',
'delete-user'
],
methods: {
selectUser(index) {
console.log('[debug]selecdUser')
this.$emit('update:modelValue', index)
},
onClickDelete(index) {
console.log('[debug]onClickDelete')
this.$emit('delete-user', index)
}
},
template: /*html*/`
<div>
<h2>ユーザ一覧</h2>
<div v-if="users.length">
<div v-for="(user, index) in users" key="index">
name: {{user.name}}
<button @click="selectUser(index)">詳細</button>
<button @click="onClickDelete(index)">削除</button>
</div>
</div>
<div v-else>
ユーザーはいません
</div>
</div>`
},
'UserDetail': {
props: {
user: Object
},
template: /*html*/`
<div>
<h2>ユーザ詳細</h2>
<div>名前: {{user?.name}}</div>
<div>年齢: {{user?.age}}</div>
<div>出身地: {{user?.Birthplace}}</div>
</div>`
}
}
})
app.mount('#app');
</script>
package com.example.test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.example.test.handler.CustomLoginSuccessHandler;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests()
.requestMatchers("/hello", "/login", "/role").permitAll()
.requestMatchers("/test2").hasAnyRole("USER")
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().usernameParameter("userName") //ユーザ名のリクエストパラメータ名
.passwordParameter("password")
.successHandler(new CustomLoginSuccessHandler())
.failureUrl("/ffff")
; //パスワードのリクエストパラメータ名
// 認可の設定
// http.authorizeHttpRequests(authz -> authz
// .requestMatchers("/").permitAll() //ページは全ユーザからのアクセスを許可
// .requestMatchers("/login").permitAll() //loginPageは、全ユーザからのアクセスを許可
// .anyRequest().authenticated() //上記ページ以外は認証を求める
// );
// // ログイン設定
// http.formLogin(login -> login //フォーム認証の有効化
// .loginPage("/login") //ログインフォームを表示するパス
// .loginProcessingUrl("/authenticate") //フォーム認証処理のパス
// .usernameParameter("userName") //ユーザ名のリクエストパラメータ名
// .passwordParameter("password") //パスワードのリクエストパラメータ名
// .defaultSuccessUrl("/home") //認証成功時に遷移するデフォルトのパス
// .failureUrl("/loginPage?error=true") //認証失敗時に遷移するパス
// );
// // ログアウト設定
// http.logout(logout -> logout
// .logoutSuccessUrl("/loginPage") //ログアウト成功時に遷移するパス
// .permitAll() //全ユーザに対してアクセスを許可
// );
return http.build();
}
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
初期表示,,,,,,,
No,確認項目,想定結果,備考,テストケース数,テスト結果,担当者,実施日
1,ロゴ表示エリア,ロゴ表示あエリアの項目を確認する,"以下の通りに初期表示されること
・ロゴが表示される
・メッセージが表示される",,,,
2,ログインエリア,ログインエリアの項目を確認する,"以下の通りに初期表示されること
・ラベル「社員番号」
・空のテキストボックス「社員番号」
・ラベル「パスワード」
・空のテキストボックス「パスワード」
・「ログイン」ボタン",,,,
ログインボタン押下時,,,,,,,
No,確認項目,想定結果,備考,テストケース数,テスト結果,担当者,実施日
1,ログインボタン押下,社員番号テキストボックスに何も入力せず、ログインボタンを押下する,"以下のエラーメッセージが表示される
・社員番号は必須入力です。",,,,
2,ログインボタン押下,パスワードテキストボックスに何も入力せず、ログインボタンを押下する,"以下のエラーメッセージが表示される
・パスワードは必須入力です。",,,,
3,ログインボタン押下,社員番号、パスワードテキストボックスに何も入力せず、ログインボタンを押下する,"以下のエラーメッセージが表示される
・社員番号は必須入力です。
・パスワードは必須入力です。",,,,
4,ログインボタン押下,社員番号、パスワードに適切な値の入力後ログインボタンを押下する,"ログインボタン押下後、以下を確認する
・「ログインに成功しました。」とメッセ―ジがポップアップされること
・「${社員番号}: ${社員名}」が表示されること
・「ログアウト」ボタンが表示されること",,,,
5,ログインボタン押下,社員番号、パスワードに不適切な値(DBに登録されていない社員IDとパスワード)の入力後ログインボタンを押下する,"ログインボタン押下後、以下を確認する
・「ログインに失敗しました。」とメッセ―ジがポップアップされること",,,,
ログインボタン押下時,,,,,,,
No,確認項目,想定結果,備考,テストケース数,テスト結果,担当者,実施日
1,ログアウトボタン押下,ユーザーがログイン状態時にログインボタンを押下する,"ログアウトボタン押下後、以下を確認する
・「ログアウトに成功しました。」とメッセ―ジがポップアップされること
・ラベル「社員番号」が表示されること
・空のテキストボックス「社員番号」が表示されること
・ラベル「パスワード」が表示されること
・空のテキストボックス「パスワード」が表示されること
・「ログイン」ボタンが表示されること",,,,
初期表示,,,,,,,,
No,確認項目,想定結果,ログイン状態,備考,テストケース数,テスト結果,担当者,実施日
1,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログイン/ログアウト,"以下の通りに初期表示されること
・personアイコン、部署管理
・personアイコン、社員管理
・homeアイコン、ホーム",,,,
2,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログイン,「ログイン」ボタンが表示されること,,,,
3,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログアウト,「ログアウト」ボタンが表示されること,,,,
4,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログイン/ログアウト,"以下の通りに初期表示されること
・personアイコンが活性化される
・personアイコンが非活性化される
・homeアイコンが非活性化される",,,,
5,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログイン/ログアウト,"以下の通りに初期表示されること
・personアイコンが非活性化される
・personアイコンが活性化される
・homeアイコンが非活性化される",,,,
6,共通ヘッダーエリア,共通ヘッダーエリアの項目を確認する,ログイン/ログアウト,"以下の通りに初期表示されること
・personアイコンが非性化される
・personアイコンが非活性化される
・homeアイコンが活性化される",,,,
アイコン・ボタン押下時,,,,,,,,
No,確認項目,想定結果,ログイン状態,備考,テストケース数,テスト結果,担当者,実施日
1,社員管理アイコン押下,社員管理アイコン押下,ログイン/ログアウト,社員管理画面に遷移する,,,,
1,部署管理アイコン押下,部署管理アイコン押下,ログイン/ログアウト,部署管理画面に遷移する,,,,
1,社員管理アイコン押下,社員管理アイコン押下,ログイン,ホームに遷移する,,,,
1,ログインボタン押下,ログインボタン押下,ログアウト,結合テスト,,,,
1,ログアウトボタン押下,ログアウトボタン押下,ログイン,結合テスト,,,,
// describe('ホーム画面', () => {
// it('初期表示', () => {
// cy.visit('http://localhost:5174/')
// cy.get('#logo')
// cy.get('p').should('have.text', '研修の〇〇')
// cy.get('#employee-id').should('have.value', '')
// cy.get('#password').should('have.value', '')
// cy.get('.row').get('button')
// })
// })
// describe('ログインボタン押下', () => {
// it('社員番号テキストボックスに何も入力せず、ログインボタンを押下する', () => {
// cy.visit('http://localhost:5174/')
// // cy.get('#employee-id').type('test')
// cy.get('#password').type('test')
// cy.get('.row').get('button').click()
// cy.get('.error').should('have.text', '社員番号は必須入力です。\n')
// })
// })
// describe('ログインボタン押下', () => {
// it('パスワードテキストボックスに何も入力せず、ログインボタンを押下する', () => {
// cy.visit('http://localhost:5174/')
// cy.get('#employee-id').type('test')
// // cy.get('#password').type('test')
// cy.get('.row').get('button').click()
// cy.get('.error').should('have.text', 'パスワードは必須入力です。\n')
// })
// })
// describe('ログインボタン押下', () => {
// it('社員番号、パスワードテキストボックスに何も入力せず、ログインボタンを押下する', () => {
// cy.visit('http://localhost:5174/')
// cy.get('.row').get('button').click()
// cy.get('.error').should('have.text', '社員番号は必須入力です。\nパスワードは必須入力です。\n')
// })
// })
// describe('ログインボタン押下', () => {
// it('社員番号、パスワードに適切な値の入力後ログインボタンを押下する', () => {
// cy.visit('http://localhost:5174/')
// // cy.screenshot({
// // capture: "fullPage"
// // });
// cy.get('#employee-id').type('test')
// cy.get('#password').type('test')
// cy.get('.row').get('button').click()
// cy.on('window:alert',(txt)=>{
// expect(txt).to.contains('ログインに成功しました');
// })
// cy.contains('123 : testさん ログイン中')
// // cy.screenshot({
// // capture: "fullPage"
// // });
// })
// })
//
// describe('ログインボタン押下', () => {
// it('社員番号、パスワードに不適切な値(DBに登録されていない社員IDとパスワード)の入力後ログインボタンを押下する', () => {
// cy.visit('http://localhost:5174/')
// cy.screenshot({
// capture: "fullPage"
// });
// cy.get('#employee-id').type('test')
// cy.get('#password').type('test')
// cy.get('.row').get('button').click()
// cy.on('window:alert',(txt)=>{
// expect(txt).to.contains('ログインに失敗しました')
// })
// cy.screenshot({
// capture: "fullPage"
// })
// })
// })
// describe('ログインボタン押下', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// })
// })
// describe('共通ヘッダーエリア', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/')
// cy.get('nav').contains('Home')
// cy.get('nav').contains('Post')
// cy.get('nav').contains('Employee')
// cy.get('button').contains('ログイン')
// })
// })
//
// describe('共通ヘッダーエリア', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/')
// cy.get('nav').contains('Home')
// cy.get('nav').contains('Post')
// cy.get('nav').contains('Employee')
// cy.get('button').contains('ログアウト')
// })
// })
//
// describe('共通ヘッダーエリア', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('nav').get('.router-link-active').should('have.text', 'Post')
// })
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/employee')
// cy.get('nav').get('.router-link-active').should('have.text', 'Employee')
// })
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174')
// cy.get('nav').get('.router-link-active').should('have.text', 'Home')
// })
// })
describe('部署管理(非ログイン)', () => {
// it('部署詳細エリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('#detail').should('not.exist')
// cy.contains('部署詳細').should('not.exist')
// })
// it('部署一覧エリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('#posts').contains('編集').should('be.disabled')
// cy.get('#posts').contains('削除').should('be.disabled')
// })
})
// describe('部署管理(ユーザロールログイン)', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('#detail').should('not.exist')
// cy.contains('部署詳細').should('not.exist')
// })
// it('部署一覧エリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('#posts').contains('編集').should('be.disabled')
// cy.get('#posts').contains('削除').should('be.disabled')
// })
// })
describe('部署管理(マネージャーロールログイン)', () => {
// it('共通ヘッダーエリアの項目を確認する', () => {
// cy.visit('http://localhost:5174/post')
// cy.get('#detail')
// cy.contains('部署詳細')
// })
it('部署追加', () => {
cy.visit('http://localhost:5174/post')
cy.get('#post_name').type('test部署')
cy.contains('保存').click()
})
it('部署編集', () => {
cy.visit('http://localhost:5174/post')
cy.get('tr').find('button').contains('編集').click()
})
it('部署削除', () => {
cy.visit('http://localhost:5174/post')
cy.get('tr').find('button').contains('削除').click()
})
it('キャンセル', () => {
cy.visit('http://localhost:5174/post')
cy.get('#post_name').type('test部署')
cy.contains('キャンセル').click()
cy.get('#post_name').should('have.value', '')
})
})