2017年10月2日 星期一

JavaScript Promise 簡單範例說明

範例:
// new 一個 Promise 需要一個 callback, 這個 callback 可接受兩個 callback
var promiseToCleanTheRoom = new Promise(function (resolve, reject) {
console.log('promise is working');
let isClean = true;
if (isClean) {
resolve('clean'); // 執行 .then() 的 callback
} else {
reject('not clean'); // 執行 .catch() 的 callback
}
});
promiseToCleanTheRoom.then(function (fromResolve) {
// resolve
console.log('room is ' + fromResolve);
}).catch(function (fromReject) {
// reject
console.log('room is ' + fromReject);
});

必須完成某件事才能進行下一件事
例如 "打掃房間 > 丟垃圾 > 拿到冰淇淋"
let cleanRoom = function () {
return new Promise(function (resolve, reject) {
resolve(' Cleaned the room.');
});
};

let removeGarbage = function (p) {
return new Promise(function (resolve, reject) {
resolve(p + ' Remove garbage.');
});
};

let winIceCream = function (p) {
return new Promise(function (resolve, reject) {
resolve(p + ' won ice cream!');
});
};
cleanRoom().then(function (result) { // 等待 cleanRoom 的結果
return removeGarbage(result);
}).then(function (result) { // 等待 removeGarbage 的 結果
return winIceCream(result);
}).then(function (result) { // 等待 winIceCream 的結果
console.log('finished : ' + result);
});

三個 Promise 同時進行,等待全部完成:
Promise.all([cleanRoom(), removeGarbage(), winIceCream()]).then(function () {
console.log('all done');
});

只少有一個 Promise 已經先完成:
Promise.race([cleanRoom(), removeGarbage(), winIceCream()]).then(function () {
console.log('one of them is done');
});


2017年5月18日 星期四

在 ASP.NET Core 自定義中間件(middleware)使用依賴注入(DI)

很多ASP.NET Core專案可能需要自定義 middleware,所以筆記一下用簡單的方法自製一個middleware 並用內建的 dependency injection 系統

使用  ASP.NET Core 1.1


ASP.NET Core middleware  方法被定義在Startup類別的Configure方法裡,當應用程式啟動的時候,框架會自動執行這個方法,並提供IApplicationBuilder實例,使用該實例的Use方法註冊自定義 middleware
IApplicationBuilder Use(Func<RequestDelegateRequestDelegate> middleware);
Use方法接受一個Func參數,該參數接受和返回都是 RequestDelegate 
public delegate Task RequestDelegate(HttpContext context)
[範例 1]:
app.Use(next =>
{
    return async context =>
    {
        await context.Response.WriteAsync("Hello World!");
        await next(context);
    };
});
用 lambda 表示 Func<RequestDelegateRequestDelegate>,參數 next 是下一個middle;return的是一個新的 RequestDelegate,他接收 HttpContext 實例  

除了[範例 1]以外,同個命名空間(Microsoft.AspNetCore.Builder)下有一個擴展方法寫法較為簡短
[範例 2]:
app.Use(async (ctx, next) =>
{
    await ctx.Response.WriteAsync("Hello World!");
    await next();
});
他接收的參數類型:
Func<HttpContextFunc<Task>, Task>
如果是最後一個middleware, 可以使用 Run(就像Use沒用next)
app.Run(async context => await context.Response.WriteAsync("Hi"));

另一個建立middleware的方式允許在class裡封裝邏輯,該class必須有接受HttpContext的Invoke方法:
public class MyMiddleware
{
    private readonly RequestDelegate _next;
 
    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task Invoke(HttpContext context)
    {
        await context.Response.WriteAsync("Hi!");
        await _next(context);
    }
}
該方法必須有一個接受RequestDelegate實例的建構式,可以在Invoke方法裡去呼叫下一個middleware。

使用自定義middleware的方式:
app.UseMiddleware<MyMiddleware>();
或是擴展
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<MyMiddleware>();
    return app;
}
然後使用他
app.UseMyMiddleware();

如果middleware需要呼叫另一個物件的方法,可以在middleware裡面建立物件,更優雅的寫法是使用內建的dependency injection

Startup類別的ConfigureServices方法可以註冊依賴物件,該方法比Configure早執行,所以所有的依賴物件可以被middleware使用

建立interface和class:
public interface IGreeter
{
    string Greet();
}
public class Greeter : IGreeter
{
    public string Greet()
    {
        return "Hi by Greeter!";
    }
}
在ConfigureServices方法裡面註冊:
services.AddTransient<IGreeterGreeter>();
AddTransient方法在每次request時都會建立一個實例,其他還有AddSingleton/AddScoped或是簡單的Add,整個DI系統的描述在docs.asp.net/dependency-injection 

完成註冊後,直接在 Configure 方法使用:
app.Use(async (context, next) =>
{
    IGreeter greeter = context.RequestServices.GetService<IGreeter>();
    await context.Response.WriteAsync(greeter.Greet());
    await next();
});
或是寫在自定義middleware:
public class MyMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IGreeter _greeter;
 
        public MyMiddleware(RequestDelegate next, IGreeter greeter)
        {
            _next = next;
            _greeter = greeter;
        }
 
        public async Task Invoke(HttpContext context)
        {
            await context.Response.WriteAsync(_greeter.Greet());
            await _next(context);
        }
    }
如果DI系統知道這些參數的類型,則在 class 被實例化時,它們將被自動解析

2017年5月15日 星期一

EF6 Exception : 指定的 LocalDB 執行個體不存在

情境:
使用EF Code First,未設定web.config的connectionStrings給DbContext使用,打算使用預設行為建立,執行application欲建立 db 時發生錯誤:

建立連接至 SQL Server 時,發生網路相關或執行個體特定的錯誤。找不到或無法存取伺服器。確認執行個名稱是否正確,以及 SQL Server 是否設定為允許遠端連線。 (provider: SQL Network Interfaces, error: 50 - Local Database Runtime 發生錯誤。 指定的 LocalDB 執行個體不存在。
)


在cmd輸入 sqllocaldb i 查看我有那些 LocalDB 執行個體 :
MSSQLLocalDB
ProjectsV13

以前得知的知識,預設執行個體是 v11.0
所以以為 [指定的 LocalDB 執行個體] 指的是 v11.0
發現我沒有 v11.0 所以打算建立一個,在cmd輸入 sqllocaldb c v11.0
但是發生錯誤:
因為發生下列錯誤,建立 LocalDB 執行個體 "v11.0" 失敗:
無法在這台電腦上使用者定的 LocalDB版本。

上網查到了這一篇文章:
http://rowo.co/blog/2014/08/28/exception-with-new-ef6-project/
於是檢查自己的web.config

  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v13.0" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>

發現預設使用的是 v13.0
因為沒有這個執行個體所以建立了一個 sqllocaldb c "v13.0"
再次執行application,成功!









2017年4月8日 星期六

將資料存放到 Seesion, 客製化 ModelBinder 來存取

假設有一個購物網站有一個CartController控制器使用Session存取Cart物件的實例
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    private Cart GetCart()
    {
        Cart cart = (Cart)Session["Cart"];
        if (cart == null)
        {
            cart = new Cart();
            Session["Cart"= cart;
        }
        return cart;
    }
}

現在要自定義一個 ModelBinder 來獲取 Session 數據中的 Cart 實例,通過實作 System.Web.Mvc.IModelBinder 介面
public class CartModelBinder : IModelBinder
{
    private const string sessionKey = "Cart";
 
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        // 通過 session 取得 Cart
        Cart cart = null;
        if (controllerContext.HttpContext.Session != null)
        {
            cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
        }
 
        // 若 session 中沒有 Cart, 則創建一個
        if (cart == null)
        {
            cart = new Cart();
            if (controllerContext.HttpContext.Session != null)
            {
                controllerContext.HttpContext.Session[sessionKey] = cart;
            }
        }
        return cart;
    }
}

需要告訴 MVC 框架,使用 CartModelBinder 來創建 Cart 實例,在Global.asax 的 Application_Start 方法中設置
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
 
        // 告訴 MVC 框架使用 CartModelBinder 類來創建 Cart 實例
        ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
    }
}

現在更新 CartController,刪除 GetCart 方法,依靠 CartModelBinder  為 CartController 提供 Cart 物件
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
}

用來創建 Cart 物件與 CartController 的邏輯方離開來了,這樣開發者能夠修改存取 Cart 物件的方法而不需要修改 CartController,可以方便對 CartController 做單元測試而不需要模仿大量的 ASP.NET 通道。
[TestMethod]
public void Can_Add_To_Cart()
{
    // Arrange
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[]
    {
        new Product{Id=1,Name="P1",Category = "Apples"},
    }.AsQueryable());
 
    Cart cart = new Cart();
 
    CartController target = new CartController(mock.Object);
 
    // Act
    target.AddToCart(cart, 1null);
 
    // Asert
    Assert.AreEqual(1, cart.Products.Count(), 1);
    Assert.AreEqual(1, cart.Products.ToArray()[0].Id);
}

2017年2月18日 星期六

Redux的createStore實作

建立一個計數器範例,點擊頁面會執行累加計數
直接使用 createStore:
import { createStore } from 'redux';

const counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}

const store = createStore(counter);
console.log(`initial state : ${store.getState()}`);

store.subscribe(() => {
    document.body.innerHTML = store.getState();
})

document.addEventListener('click', () => {
    store.dispatch({ type: 'INCREMENT' });

});

現在 counter 方法直接沿用,另外建立一個 createStore 方法:
const createStore = (reducer) => {
    // 這個 store 持有 state 變數
    let state;

    const getState = () => state;

    const dispatch = (action) => {

    };

    const subscribe = (listener) => {

    };

    // 回傳的物件被稱為 Redux store
    return { getState, dispatch, subscribe }

};

因為 subscribe() 可以被呼叫很多次,所以需要紀錄這些 listener:
    let listeners = [];

    const subscribe = (listener) => {
        listeners.push(listener)
    };

dispatch() 是唯一可以改變內部 state 的:
    const dispatch = (action) => {
        // 使用當前的 state 和 被 dispatch 的 action 物件當參數呼叫 reducer 計算出新的 state
        state = reducer(state, action);
        // 執行 listeners
        listeners.forEach((listener) => { listener() });
    };

還沒有實作 unsubscribe() 方法,先使用替代方案,在subscribe() 寫一個回傳一個方法:
        // 回傳一個方法
        // 使用方式:
        //     var s1 = store.subscribe(()=>{});
        //     s1(); // unsubscribe
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }

最後在 createStore() 回傳前加入 dispatch({});
為了得到初始 state

createStore 完整範例:
const createStore = (reducer) => {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach((listener) => { listener() });
    };

    const subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }
    };

    dispatch({});

    return { getState, dispatch, subscribe }

};










2017年2月13日 星期一

用最少的套件安裝React應用程式並啟動

1.  建立資料夾,在該資料下開啟終端機並執行:
    $ npm init -y
 
    參考:
    https://docs.npmjs.com/cli/init

2. 安裝 React 套件
    $ npm install --save react react-dom

3. 安裝轉譯套件
    $ npm install --global babel-cli
    $ npm install --save-dev babel-preset-es2015 babel-preset-react

4. 建立 .babelrc
    {
        "presets" : ["es2015","react"]
    }

5. 安裝 webpack
    $ npm install --save-dev webpack

6. 建立 webpack.config.js
var path = require('path');

module.exports = {
  entry: './app/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};    
    參考:https://webpack.js.org/guides/get-started/

7. 執行 babel 和 webpack
    $ babel js/source -d js/build --watch
    $ webpack --config webpack.config.js --watch --colors --progress -d
    --watch : 程式有異動就執行webpack
    --colors : 顯示一些顏色
    --progress : 顯示執行進度
    -d : 加入 Source Map (方便 debug)
    -p : production code (不斷行也不空白的很醜的程式碼)

後續:
想使用babel-loader卻一直失敗,後來發現網路上很多教學文章都是用webpack 1,而我安裝的是2,
配置React的Babel和Webpack2環境:
https://segmentfault.com/a/1190000007000131
https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783#.zg5j3c46z
https://www.smashingmagazine.com/2017/02/a-detailed-introduction-to-webpack/?utm_source=javascriptweekly&utm_medium=email
簡單設定:
module.exports = {
    // ...其他設定
    module : {
        rules : [
            {
                test : /\.js$/,
                use : 'babel-loader',
                exclude : /node_modules/
            }
        ]
    }
}
指定路徑下的終端機輸入
$ code .
會運行Visual Studio Code並開啟指定目錄