Angular2的預設route模式是html5,這個在跑到其他頁面後,在重新整理頁面後會出現空白頁面,這是因為找不到Index的入口了。所以解決方式就是要讓MVC Core可以每次都載入Index.html頁面(如果純靜態頁面的方式)

在startup.cs的Configure function, 加入以下程式碼

1
2
3
4
5
6
7
8
9
10
11
app.Use(async (context, next) =>
{
// 判斷request如果不是api call時,則要讀取index.html
if (!Path.HasExtension(context.Request.Path.Value)
&& context.Request.HttpContext.Request.Headers["X-Custom-Header"] != "api"
&& context.Request.HttpContext.Request.Headers["X-Requested-With"] != "XMLHttpRequest")
{
await context.Response.WriteAsync(System.IO.File.ReadAllText("index.html"));
}
await next();
});

在Angular2的index.ts裡設定讓所有的request的header都新增 X-Custom-Header=api,讓server判斷是否為api call

1
2
3
4
5
6
7
8
9
10
11
import { HTTP_PROVIDERS, BaseRequestOptions, RequestOptions, Headers } from 'angular2/http';

class requestOption extends BaseRequestOptions {
headers: Headers = new Headers({ 'X-Custom-Header': 'api' });
}


bootstrap(CkDemoApp, [
...
provide(RequestOptions, { useClass: requestOption })
]);

這樣子就可以讓Angular2跑到不同頁面時,不會因為重新整理而出現空白的畫面

如果是用Controller/View的方式,設定方法比照舊的設定方式即可

Redux2的middleware是介於action和reducer之間。例如: ReduxThunk. 設定方式是在建立store時,將middleware指定給store即可

寫自訂的middleware基本架構如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import isPromise from '../utils/is-promise';

export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isPromise(action.payload)) {
return next(action);
}

const { types, payload, meta } = action;
const { promise, data } = payload;
const [PENDING, FULFILLED, REJECTED] = types;

/**
* Dispatch the pending action
*/
let pendingAction = { type: PENDING, payload: null, meta: null };
if (_.isEmpty(data)) {
pendingAction.payload = data;
}
if (_.isEmpty(meta)) {
pendingAction.meta = meta;
}
dispatch(pendingAction);


/**
* If successful, dispatch the fulfilled action, otherwise dispatch
* rejected action.
*/
return promise.then(
result => {
dispatch({
type: FULFILLED,
payload: result,
meta,
});
},
error => {
dispatch({
type: REJECTED,
payload: error,
meta,
});
}
);
};
}

store的設定方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function configureStore(initialState) {
const store = compose(
_getMiddleware()
)(createStore)(rootReducer, initialState);

return store;
}

function _getMiddleware() {
// 這裡加入middleware
let middleware = [
promiseMiddleware,
ReduxThunk
];

if (__DEV__) {
middleware = [...middleware];
}

return applyMiddleware(...middleware);
}

參考文件: - middleware - redux-thunk

在Part 1提到在aciton裡面,如果有需要呼叫api的行為,都會發生這個階段 目前有發現有兩種方式可以處理api.

  1. angular的service(http call)
  2. fetch api

如果利用第二種方式處理api call時,可以直接寫在action裡面。但是如果想要利用angular的service方式時,就要繞一下路了 但是,還是先簡單的寫一下fetch的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function loadTodo() {
return dispatch => {
return fetch('api/Values')
.then((response) => {
return response.json()
})
.then((data) => {
return ({
type: TODO_INIT,
payload: data
});
})
.then((action) => {
dispatch(action);
})
}
}

利用service的code如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {Injectable, Inject } from 'angular2/core';

import {Http, Response} from 'angular2/http';
import 'rxjs/Rx';
import {TODO_INIT} from '../constants';

@Injectable()
export class TodoService {
constructor(
@Inject('ngRedux') private store,
private http: Http) {

}

loadTodo() {
return this.http.get('api/Values')
.map((res) => { return res.json() })
.map((d) => ({
type: TODO_INIT,
payload: d
}))
.subscribe((action) => {
this.store.dispatch(action);
})
}
}

Redux是根據Facebook的flux所產出的一個架構. 簡單介紹請參考這裡

簡單的動作及資料流程如下 Untitled Diagram.png

複雜一點的流程圖如下 moreDetailReduxFlow.png

每一個階段都有他應該要做的事情 - Action: 處理資料,呼叫API, 任何有可能產生副作用的行為都在這階段處理, 通常都是回傳JSON object.
- Reducer: 根據Action傳來的動作和資料,來決定與原本的資料(In Store)的關係,例如、新增、更新、移除或過濾等,回傳要顯示在View上面的資料

##程式碼 @Component程式的基本架構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import {
Component,
Inject,
ApplicationRef
} from 'angular2/core';

import * as TodoAction from '../../actions/ToDo';


@Component({
selector: 'ck-todo-app',
template: require('./TodoPage.html')
})
export class CkTodoApp {
private disconnect: Function;
private unsubscribe: Function;

private items: any;
private task: any;

constructor(
@Inject('ngRedux') private ngRedux,
private applicationRef: ApplicationRef) {
}

ngOnInit() {
this.disconnect = this.ngRedux.connect(
this.mapStateToThis,
this.mapDispatchToThis)(this);

this.unsubscribe = this.ngRedux.subscribe(() => {
this.applicationRef.tick();
});
}

ngOnDestroy() {
this.unsubscribe();
this.disconnect();
}

// 註冊store到變數上
mapStateToThis(state) {
return {
items: state.todo,
task: state.newtodo
};
}

// 註冊功能到這個Class裡
mapDispatchToThis(dispatch) {
return {
add: (task) => dispatch(TodoAction.add(Object.assign({}, task))),
remove: (task) => dispatch(TodoAction.remove(task))
};
}
};

TodoPage.html

1
2
3
4
5
6
7
8
9
10
11
12
<div class="clearfix mx-auto col-8">
<h3>TODO App</h3>
<div class="clearfix mxn2">
<input type="text" [(ngModel)]="task.content" class="input inline-block" />
<button class="btn btn-primary inline-block" (click)="add(task)">Add</button>
</div>
<div class="clearfix mxn2">
<ul class="list-reset" *ngFor="#item of items">
<li>{{ item.content }} <span (click)="remove(item)">x</span></li>
</ul>
</div>
</div>

###註冊Store as provider in Angular2 application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { enableProdMode, provide } from 'angular2/core';
import { bootstrap} from 'angular2/bootstrap';
import { ROUTER_PROVIDERS, APP_BASE_HREF } from 'angular2/router';
import { CkDemoApp } from './containers/main-app';

// 註冊redux store用
import configureStore from './store/configure-store';
const provider = require('ng2-redux').provider;
const store = configureStore({});

declare let __PRODUCTION__: any;

if (__PRODUCTION__) {
enableProdMode();
}

bootstrap(CkDemoApp, [
provider(store),
ROUTER_PROVIDERS,
provide(APP_BASE_HREF, { useValue: '/' })
]);

store/configureStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
///<reference path="./dev-types.d.ts"/>

import {createStore, applyMiddleware, compose} from 'redux';
import {fromJS} from 'immutable';
import ReduxThunk from 'redux-thunk';
import rootReducer from '../reducers';
const persistState = require('redux-localstorage');

function configureStore(initialState) {
const store = compose(
_getMiddleware()
)(createStore)(rootReducer, initialState);

return store;
}

function _getMiddleware() {
let middleware = [
ReduxThunk
];

if (__DEV__) {
middleware = [...middleware];
}

return applyMiddleware(...middleware);
}

export default configureStore;

設定ACTIONs

actions/Todo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { TODO_ADD, TODO_REMOVE } from '../constants';
export function add(task) {
return {
type: TODO_ADD,
data: task
}
}

export function remove(task) {
return {
type: TODO_REMOVE,
data: task
}
}

設定可以使用的Reducers

reducers/index

1
2
3
4
5
6
7
8
9
10
11
import { combineReducers } from 'redux';
// reducer functions
import {todo, newtodo} from './todo';

// 下面的名稱是要存取store資料時的名稱
// ex. state.todo
export default combineReducers({
todo,
newtodo
});

reducers/todo.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { fromJS, List } from 'immutable';
import { TODO_ADD, TODO_REMOVE } from '../constants';

const INITIAL_STATE = List<any>();

// todo Reducer
export function todo(state = INITIAL_STATE, action: any = { type: '' }) {
switch (action.type) {
case TODO_ADD:
return state.push(action.data);
case TODO_REMOVE:
return state.remove(state.indexOf(action.data));
default:
return state;
}
}

// newtodo Reducer
export function newtodo(state = { content: '' }, action: any = { type: '' }) {
switch (action.type) {
case TODO_ADD:
return { content: '' };
default:
return state;
}
}

CODE

參考資料: - Angular 2 — Introduction to Redux - Angular 2 and Redux - redux - ng2-redux - immutable.js - work with service

今天透過Visual studio 2015 執行webpack時,竟然返回錯誤訊息。 我確定該webpack.config.js是可以跑的,但是為什麼在Visual studio 2015裡面執行卻是不行的,結果發現理由是node跟npm的版本有關係 我的webpack.config.js裡面有下’use strict’; 然後有使用到const, 所以VS就不開心了. 因為所使用的node版本不認識ES2015的東西.

解決方式是讓visual studio執行npm command時,使用本機電腦所使用的版本而不是Visual studio本身所用的版本, 將 ** $(PATH)**移到最上面

這樣子的設定就可以讓VS在執行npm時,就會按照上圖的順序去找執行

基本版

根據官方文件的作法,當一個formflow完成後,那個Converstaion就會結束,不管之後再傳給bot什麼文字,Bot都不會有任何反應, 除非一個新的ConverstaionID重新建立

但是,在某些訊息環境,是沒有辦法更新ConverstaionID的. 這時候就需要自訂一個Dialog來處理FormComplete及其他的情形 就像官方文件所提到的Dialog是非常強大的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[Serializable]
public class SandwichDialog : IDialog
{
private readonly BuildForm<SandwichOrder> SandwichOrderForm;

internal SandwichDialog(BuildForm<SandwichOrder> SandwichOrderForm)
{
this.SandwichOrderForm = SandwichOrderForm;
}

public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}

public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<Message> argument)
{
var message = await argument;
var pizzaForm = new FormDialog<SandwichOrder>(new SandwichOrder(), this.SandwichOrderForm, FormOptions.PromptInStart);
context.Call<SandwichOrder>(pizzaForm, FormComplete);
}

private async Task FormComplete(IDialogContext context, IAwaitable<SandwichOrder> result)
{
SandwichOrder order = null;
try
{
order = await result;
}
catch (OperationCanceledException)
{
await context.PostAsync("You canceled the form!");
return;
}
catch (Exception ex)
{
await context.PostAsync(ex.Message);
return;
}

if (order != null)
{
await context.PostAsync(order.ToString());
}
else
{
await context.PostAsync("Form returned empty response!");
}

context.Wait(MessageReceivedAsync);
}
}

這個是當pizzaForm完成後,則執行FormComplete.

1
context.Call<T>(pizzaForm, FormComplete);

在 FormComplete 裡面,可以取得使用者所輸入的選項,所以後續要處理的動作會寫在此處

Gist

先從基本的開始,跟著下面的文章做,就可以完成基本的Bot功能了 http://docs.botframework.com/connector/getstarted/#navtitle

注意事項

  1. 當在新增【My Bot】時,Endpoint的網址一定要用https, 不然之後在測試Bot Connector時會出現403, 無法授權等奇怪的狀況.

  2. 如果使用web chat embed code時,要把他們所提供網址裡的s=[secret] 改成 t=[secret]

###程式基本的運作方式

Bot在與Bot Connector之間的溝通是透過傳遞Message. 這個Message裡面會包含很多資訊,也可以保留狀態(所以可以建立一連串的問題,就像在執行npm init時會問一堆問題一樣) 網站參考

總結: 一切都是在玩弄Message這個物件阿.

基於Docker安裝步驟變簡單了,所以是時候來玩Docker了. 在MVC Core的目錄下,新增一個檔案Dockfile, 內容如下

1
2
3
4
5
6
7
8
FROM microsoft/aspnet:1.0.0-rc1-update1

COPY . /app
WORKDIR /app
RUN ["dnu", "restore"]

EXPOSE 5000/tcp
ENTRYPOINT ["dnx", "-p", "project.json", "web","--server.urls", "http://0.0.0.0:5000"]

**server.urls 需要指定到0.0.0.0:port, 不然在docker run起來的時候,網頁會說Refused to Connect server.urls的設定方式可以參考這裡

開啟命令視窗,到有Dockerfile檔案的資料夾並執行下列指令

1
docker build -t <imageName> .

上列指令這會建立一個docker image file 接下來就要讓所建立出來的Image執行起來, 執行下列指令

1
docker run -t -d -p 5000:5000 <imageName>

詳細的Docker指令用法,請參閱官方網站

###今天下載了docker Toolbox for windows ,根據安裝指示安裝後,在執行時出現了一個錯誤訊息

1
hyper-v is installed. virtualbox won't boot a 64 bits vm in hyper-v is activated ....

排除方式為:修改 Program Files\Docker Toolbox\start.sh 在start.sh檔裡面,尋找

1
"${DOCKER_MACHINE}" create -d virtualbox "${VM}" 

更改成

1
"${DOCKER_MACHINE}" create --virtualbox-no-vtx-check -d virtualbox "${VM}"

即可排除此錯誤訊息

###當在command下docker command時,出現以下錯誤訊息

1
An error occurred trying to connect: Get http://127.0.0.1:2375/v1.22/containers/json: dial tcp 127.0.0.1:2375: connectex: No connection could be made because the target machine actively refused it.

排除方式為

  1. docker-machine start default or create new one
  2. docker-machine ls will show you your machine running
  3. docker-machine env --shell cmd default and you’ll see something like

SET DOCKER_TLS_VERIFY=1 SET DOCKER_HOST=tcp://xxx.xxx.xxx.xxx:2376 SET DOCKER_CERT_PATH=C:\Users\Arseny.docker\machine\machines\default SET DOCKER_MACHINE_NAME=default REM Run this command to configure your shell: REM FOR /f 「tokens=*」 %i IN (『docker-machine env --shell cmd default』) DO %i

4.Run

1
FOR /f "tokens=*" %i IN ('docker-machine env --shell cmd default') DO %i

5.Enjoy. 就可以正常的下docker指令了

目前開發所需的gulpfile.js版本 工作流程 for angular 1.x開發 [typescript]->[javascript]->[webpack]->bundle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
'use strict';

var gulp = require('gulp'),
tsc = require('gulp-typescript'),
inject = require('gulp-inject'),
tsProject = tsc.createProject('tsconfig.json'),
webpack = require('webpack'),
gulpWebpack = require('webpack-stream'),
ngAnnotatePlugin = require('ng-annotate-webpack-plugin'),
path = require('path');

gulp.task('compile-ts', function () {
var sourceTsFiles = ['./app/src/**/*.ts', //path to typescript files
'./app/typings/**/*.ts']; //reference to library .d.ts files


var tsResult = gulp.src(sourceTsFiles)
.pipe(tsc(tsProject));

tsResult.dts.pipe(gulp.dest('./app/dist'));
return tsResult.js.pipe(gulp.dest('./app/dist'));
});

gulp.task('gen-ts-refs', function () {
var target = gulp.src('./app/src/app.d.ts');
var sources = gulp.src(['./app/src/**/*.ts'], { read: false });
return target.pipe(inject(sources, {
starttag: '//{',
endtag: '//}',
transform: function (filepath) {
if (filepath.indexOf('index') > -1) { return; }
if (filepath.indexOf('app.d.ts') > -1) { return; }
return '/// <reference path="../..' + filepath + '" />';
}
})).pipe(gulp.dest('./app/src/'));
});

gulp.task('watch', function () {
gulp.watch(['./app/src/**/*.ts'], ['webpack']);
});

gulp.task('webpack', ['compile-ts'], function () {
return gulp.src('./app/dist/app.js')
.pipe(gulpWebpack({
entry: {
bundled: './app/dist/app.js',
commands: './app/dist/libs.js'
},
output: {
filename: '[name].js',
},
resolve: {
// this tells Webpack where actually to find lodash because you'll need it in the ProvidePlugin
alias: {
lodash: path.resolve(__dirname, './node_modules/lodash'),
angular: path.resolve(__dirname, './node_modules/angular')
},
extensions: ['', '.js']
},
module: {
loaders: [
{ test: /[\/]angular\.js$/, loader: "exports?angular" }
]
},
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
// this tells Webpack to provide the "_" variable globally in all your app files as lodash.
new webpack.ProvidePlugin({
_: "lodash",
}),
new ngAnnotatePlugin({
add: true
})
// new webpack.optimize.CommonsChunkPlugin('common.js'),
//new webpack.optimize.UglifyJsPlugin({
// compress: {
// warnings: false
// },
// output: { comments: false }
//})

]
}))
.pipe(gulp.dest('./Scripts'));
})

gulp.task('default', ['watch']);

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
"devDependencies": {
"gulp": "^3.9.0",
"gulp-inject": "^3.0.0",
"gulp-typescript": "^2.10.0",
"gulp-tsd": "^0.0.4",
"tsd": "^0.6.5",
"typescript": "^1.7.5",
"ng-annotate-webpack-plugin": "^0.1.2",
"path": "^0.11.14",
"webpack": "^1.11.0",
"webpack-stream": "^2.1.0"
},
"dependencies": {
"angular": "^1.4.8",
"lodash": "^4.0.0"
}

需要disable visual studio裡面對於typescript的compile,編輯csproj的第一個

1
2
加入這個讓vs不要在Build的時候編譯Typescript
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>

另外需要

1
2
3
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
在這個項目下,增加
<TypeScriptModuleKind>commonjs</TypeScriptModuleKind>