2015年3月26日 星期四

Using JSON Web Tokens with Node.js

webber0928

原文:http://www.sitepoint.com/using-json-web-tokens-node-js/

翻譯:(LcjNiL)
諸如Ember,Angular,Backbone之類的前端框架類庫正隨著更加精細的Web應用而日益壯大。正因如此,服務器端的組建也正正在從傳統的任務中解脫,轉而變的更像API。API使得傳統的前端和後端的概念解耦。開發者可以脫離前端,獨立的開發後端,在測試上獲得更大的便利。這種途徑也使得一個移動應用和網頁應用可以使用相同的後端。

當使用一個API時,其中一個挑戰就是認證(authentication)。在傳統的web應用中,服務端成功的返回一個響應(response)依賴於兩件事。一是,他通過一種存儲機制保存了會話信息(Session)。每一個會話都有它獨特的信息(id),常常是一個長的,隨機化的字符串,它被用來讓未來的請求(Request)檢索信息。其次,包含在響應頭(Header)裡面的信息使客戶端保存了一個Cookie。服務器自動的在每個子請求裡面加上了會話ID,這使得服務器可以通​​過檢索Session中的信息來辨別用戶。這就是傳統的web應用逃避HTTP面向無連接的方法(This is how traditional web applications get around the fact that HTTP is stateless)。
API應該被設計成無狀態的(Stateless)。這意味著沒有登陸,註銷的方法,也沒有sessions,API的設計者同樣也不能依賴Cookie,因為不能保證這些req​​uest是由瀏覽器所發出的。自然,我們需要一個新的機制。這篇文章關注於JSON Web Tokens,簡寫為JWTs,一個可能的解決這個問題的機制。這篇文章利用Node的Express框架作為後端,以及Backbone作為前端。
##背景我們來簡短的看一下幾個通常的保護(secure)API的方法。
一個是使用在HTTP規範中所製定的Basic Auth, 它需要在在響應中設定一個驗證身份的Header。客戶端必須在每個子響應是附加它們的憑證(credenbtial),包括它的密碼。如果這些憑證通過了,那麼用戶的信息就會被傳遞到服務端應用。
第二個方面有點類似,但是使用應用自己的驗證機制。通常包括將發送的憑證與存儲的憑證進行檢查。和Basic Auth相比,這種需要在每次請求(call)中發送憑證。
第三種是OAuth(或者OAuth2)。為第三方的認證所設計,但​​是更難配置。至少在服務器端更難。
##使用Token的方法不是在每一次請求時提供用戶名和密碼的憑證。我們可以讓用戶通過token交換憑證(we can allow the client to exchange valid credentials for a token),這個token提供用戶訪問服務器的權限。Token通常比密碼更加長而且複雜。比如說,JWTs通常會應對長達150個字符。一旦獲得了token,在每次調用API的時候都要附加上它。然後,這仍然比直接發送賬戶和密碼更加安全,哪怕是HTTPS。
把token想像成一個安全的護照。你在一個安全的前台驗證你的身份(通過你的用戶名和密碼),如果你成功驗證了自己,你就可以取得這個。當你走進大樓的時候(試圖從調用API獲取資源),你會被要求驗證你的護照,而不是在前台重新驗證。
##關於JWTs JWTs是一份草案,儘管在本質上它是一個老生常談的一種更加具體的認證個授權的機制。一個JWT被周期(period)分寸了三個部分。JWT是URL-safe的,意味著可以​​用來查詢字符參數。(譯者註:也就是可以脫離URL,不用考慮URL的信息)。
JWT的第一部分是一個js對象,表面JWT的加密方法。實例使用了HMAC SHA-266
{ 
"typ" : "JWT" ​​,
"alg" : "HS256"
}
在加密之後,這個對像變成了一個字符串:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
JWT的第二部分是token的核心,他也是一個JS兌現,包含了一些信息。有一些是必須的,有一些是選擇性的。一個實例如下:
{ 
"iss" : "joe" ,
"exp" : 1300819380 ,
"http://example.com/is_root" : true
}
這被稱為JWT Claims Set。因為這篇文章的目的,我們將忽視第三個參數。但是你可以閱讀這篇文章 .這個ississuer的簡寫,表明請求的實體。通常意味著請求API的用戶。expexpires的簡寫,是用來限制token的生命週期。一旦加密,JSON token就像這樣:
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
第三個也是最後一個部分,是JWT根據第一部分和第二部分的簽名(Signature)。像這個樣子:
dBjftJeZ4CVP - mB92K27uhbUJU1p1r_wW1gFWFOEjXk
整個的JWT是這樣的
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 . eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ . dBjftJeZ4CVP - mB92K27uhbUJU1p1r_wW1gFWFOEjXk
在規範中,有一些選擇性的附加 ​​屬性。有iat表明什麼時候token被半吧,nbf去驗證在什麼時間之前token無效,和aud去指明這個token的收件人是誰。
##處理Tokens我們將用JWT simple模塊去處理token,它將使我們從鑽研如何加密解密中解脫出來。如果你有興趣,可以閱讀這篇說明,或者讀這個倉庫的源碼。首先我們將使用下面的命令安裝這個庫。記住你可以在命令中加入--save,讓其自動的讓其加入到你的package.json文件裡面。
npm install jwt - simple
在你應用的初始環節,加入以下代碼。這個代碼引入了Express和JWT simple,而且創建了一個新的Express應用。最後一行設定了app的一個名為jwtTokenSecret的變量,其值為'YOUR_SECRET_STRING'(記得把它換成別的)。
var express =  require ( 'express' ); 
var jwt = require ( 'jwt-simple' );
var app = express ();

app
. set ( 'jwtTokenSecret' , 'YOUR_SECRET_STRING' );
##獲取一個Token我們需要做的第一件事就是讓客戶端通過他們的賬號密碼交換token。這裡有2種可能的方法在RESTful API裡面。第一種是使用POST請求來通過驗證,使服務端發送帶有token的響應。除此之外,你可以使用GET請求,這需要他們使用參數提供憑證(指URL),或者更好的使用請求頭。
這篇文章的目的是為了解釋token驗證的方法而不是基本的用戶名/密碼驗證機制。所以我們假設我們已經通過請求得到了用戶名和密碼:
User . findOne ({ username : username },  function ( err , user )  { 
if ( err ) {
// user not found
return res . send ( 401 );
}

if (! user ) {
// incorrect username
return res . send ( 401 );
}

if (! user . validPassword ( password )) {
// incorrect password
return res . send ( 401 );
}

// User has authenticated OK
res
. send ( 200 );
});
下一步,我們就需要返回JWT token通過一個驗證成功的響應。
var expires = moment (). add ( 'days' ,  7 ). valueOf (); 
var token = jwt . encode ({
iss
: user . id ,
exp
: expires
}, app . get ( 'jwtTokenSecret' ));

res
. json ({
token
: token ,
expires
: expires ,
user
: user . toJSON ()
});
注意到jwt.encode()函數有2個參數。第一個就是一個需要加密的對象,第二個是一個加密的密鑰。這個token是由我們之前提到的issexp組成的。注意到Moment.js被用來設置token將在7天之後失效。而res.json()方法用來傳遞這個JSON對像給客戶端。
##驗證Token 為了驗證JWT,我們需要寫出一些可以完成這些功能的中間件(Middleware):
  • 檢查附上的token
  • 試圖解密
  • 驗證token的可用性
  • 如果token是合法的,檢索里面用戶的信息,以及附加到請求的對像上
我們來寫一個中間件的框架
// [@file](/user/file) jwtauth.js

var UserModel = require ( '../models/user' );
var jwt = require ( 'jwt-simple' );

module . exports = function ( req , res , next ) {
// code goes here
};
為了獲得最大的可擴展性,我們允許客戶端使用一下3個方法附加我們的token:作為請求鏈接(query)的參數,作為主體的參數(body),和作為請求頭(Header)的參數。對於最後一個,我們將使用Header x-access-token
下面是我們的允許在中間件的代碼,試圖去檢索token:
var token =  ( req . body && req . body . access_token )  ||  ( req . query && req . query . access_token )  || req . headers [ 'x-access-token' ];
注意到他為了訪問req.body,我們需要首先使用express.bodyParser()中間件(譯者註,這個是Express 3.x的中間件)。
下一步,我們講解析JWT:
if  ( token )  { 
try {
var decoded = jwt . decode ( token , app . get ( 'jwtTokenSecret' ));

// handle token here

} catch ( err ) {
return next ();
}
} else {
next ();
}
如果解析的過程失敗,那麼JWT Simple組件將會拋出一段異常。如果異常發生了,或者沒有token,我們將會調用next()來繼續處理請求。這代表喆我們無法確定用戶。如果一個合格的token合法並且被解碼,我們應該得到2個屬性,iss包含著用戶ID以及exp包含token過期的時間戳。我們將首先處理後者,如果它過期了,我們就拒絕它:
if  ( decoded . exp <=  Date . now ())  { 
res
. end ( 'Access token has expired' , 400 );
}
如果token依舊合法,我們可以從中檢索出用戶信息,並且附加到請求對象裡面去:
User . findOne ({ _id : decoded . iss },  function ( err , user )  { 
req
. user = user ;
});
最後,將這個中間件附加到路由里面:
var jwtauth =  require ( './jwtauth.js' );

app
. get ( '/something' , [ express . bodyParser (), jwtauth ], function ( req , res ){
// do something
});
或者匹配一些路由
app . all ( '/api/*' ,  [ express . bodyParser (), jwtauth ]);
##客戶端我們提供了一個簡單的get端去獲得一個遠端的token。這非常直接了,所以我們不用糾結細節,就是發起一個請求,傳遞用戶名和密碼,如果請求成功了,我們就會得到一個包含著token的響應。
我們現在研究的是後續的請求。一個方法是通過JQuery的ajaxSetup()方法。這可以直接用來做Ajax請求,或者通過前端框架使用包裝過的Ajax方法。比如,假設我們將我們的請求使用window.localStorage.setItem('token', 'the-long-access-token');放在本地存儲(Local Storage)裡面,我們可以通過這種方法將token附加到請求頭里面:
var token = window . localStorage . getItem ( 'token' );

if ( token ) {
$
. ajaxSetup ({
headers
: {
'x-access-token' : token
}
});
}
很簡單,但是這會劫持所有Ajax請求,如果這裡有一個token在本地存儲裡面。它將會附加到一個名為x-access-token的Header裡面。
##使用Backbone我們將前一個方法換成Backbone應用。最簡單的方法就是使用全局的Backbone.sync(),如下面所示
// Store "old" sync function 
var backboneSync = Backbone . sync

// Now override
Backbone . sync = function ( method , model , options ) {

/*
* "options" represents the options passed to the underlying $.ajax call
*/

var token = window . localStorage . getItem ( 'token' );

if ( token ) {
options
. headers = {
'x-access-token' : token
}
}

// call the original function
backboneSync
( method , model , options );
};
##更多的安全你可以保存簽證過的token記錄在服務器上,來添加一個附加的安全層,,然後在每一步驗證token的時候驗證這個記錄。這將會組織第三方偽裝一個token,也將會使得服務器可以失效一個token。我不會提到這個方面,但是它應當被直接的實現。
##總結在這篇文章,我們探究了一個API驗證的方法,仔細的查看了JSON WEB Tokens。我們使用了Node和Express來寫一個簡單的實現,而且也在客戶端使用了Backbone作為一個示例。代碼可以在GitHub上找到。
這裡還有很多我們都沒有完全實現到,比如資源的請求(claim),但是我們已經做瞭如何使用簡單的方法來構建一個獲取token的機制。在這個例子裡面,客戶端和服務端都是JS應用。
當然你可以通過其他技術使用這個方法,比如Ruby或者PHP作為後端,或者Ember或者AngularJS。除此之外,你還可以在移動應用中使用。比如,使用web技術和PhoneGap之類的結合,使用類似於Sencha之類的工具,或者完全的本地應用。

Using JSON Web Tokens with Node.js

webber0928

原文:http://www.sitepoint.com/using-json-web-tokens-node-js/

翻譯:(LcjNiL)
諸如Ember,Angular,Backbone之類的前端框架類庫正隨著更加精細的Web應用而日益壯大。正因如此,服務器端的組建也正正在從傳統的任務中解脫,轉而變的更像API。API使得傳統的前端和後端的概念解耦。開發者可以脫離前端,獨立的開發後端,在測試上獲得更大的便利。這種途徑也使得一個移動應用和網頁應用可以使用相同的後端。

Webpack

webber0928
大概算是一份教程吧,只不過效果肯定不如視頻演示之類的好.. 
Webpack最近在英文社區上經常看到,留了心,但進一步了解是通過下邊的視頻: 視頻: How Instagram.com Works , Peter Hunt Peter Hunt也是React的傳教士,我由於對React的關注因此細看了視頻 再後來是出現React Hot Loader這樣的開發神器,我認為Webpack應該很棒http://gaearon.github.io/ react-hot-loader/ 為了解決簡聊當中一些問題,我消耗了很多時間了解Webpack,整理在這裡

Webpack 是什麼
https://github.com/webpack
Webpack是德國開發者Tobias Koppers開發的模塊加載器
Instagram工程師認為這個方案很棒,似乎還把作者招過去了
在Webpack當中,所有的資源都被當作是模塊, js, css,圖片等等.. 
因此, Webpack當中js可以引用css, css中可以嵌入圖片dataUrl
對應各種不同文件類型的資源, Webpack有對應的模塊loader 
比如CoffeeScript用的是coffee-loader ,其他還有很多: http://webpack.github.io/docs/list-of-loaders.html 大致的寫法也就這樣子:

  module : {
loaders: [

{ test:
/\.coffee$/ , loader: 'coffee-loader' },
{ test:
/\.js$/ , loader: 'jsx-loader?harmony' } // loaders can take parameters as a querystring
]

},

CommonJS 與AMD 支持

Webpack對CommonJS的AMD的語法做了兼容,方便遷移代碼
不過實際上,引用模塊的規則是依據CommonJS來的
require ( 'lodash' ) //從模塊目錄查找
require ( './file' ) //按相對路徑查找
AMD 語法中, 也要注意, 是按CommonJS 的方案查找的
define ( require , exports. module ) -> 
require ( 'lodash' ) # commonjs當中這樣是查找模塊的
require ( './file' )

特殊模塊的Shim

比如某個模塊依賴window.jQuery ,需要從npm模塊中將jquery掛載到全局
Webpack有不少的Shim的模塊,比如expose-loader用於解決這個問題https://github.com/webpack/docs/ wiki/shimming-modules 其他比如從模塊中導出變量...具體說明有點晦澀..

手頭的兩個例子,比如我們用到Pen這個模塊, 
這個模塊對依賴一個window.jQuery ,可我手頭的jQuery是CommonJS語法的
Pen對象又是生成好了綁在全局的,可是我又需要通過require('pen')獲取變量
最終的寫法就是做Shim處理直接提供支持:
{test: require .resolve( 'jquery' ), loader: 'expose?jQuery' },
{test:
require .resolve( 'pen' ), loader: 'exports?window.Pen' },

基本的使用

安裝webpack模塊之後,可是使用webpack這個命令行工具
可以使用參數,也可以配置webpack.config.js文件直接運行webpack調用
建議按照Peter Hunt給的教程走一遍,基本的功能都會用到了https://github .com/petehunt/webpack-howto
簡單的例子就是這樣一個文件,可以把./main.js作為入口打包bundle.js :
// webpack.config.js 
module .exports = {
entry:
'./main.js' ,
output: {

filename:
'bundle.js'
}

};

查找依賴

Webpack是類似Browserify那樣在本地按目錄對依賴進行查找的
可以構造一個例子,用--display-error-details查看查找過程, 
例子當中resolve.extensions用於指明程序自動補全識別哪些後綴, 
注意一下, extensions第一個是空字符串 !對應不需要後綴的情況.
// webpack.config.js 
module .exports = {
entry:
'./a.js' ,
output: {

filename:
'b.js'
},

resolve: {

extensions: [
'' , '.coffee' , '.js' ]
}

}

// a.js 
require ( './c' )
➤➤ webpack --display-error-details
Hash : e38f7089c39a1cf34032
Version : webpack 1.5 .3
Time : 54 ms
Asset Size Chunks Chunk Names

b.js
1646 0 [emitted] main
[
0 ] ./a.js 15 { 0 } [built] [ 1 error]

ERROR
in ./a.js
Module
not found : Error : Cannot resolve 'file' or 'directory' ./c in /Users/chen/Drafts/webpack/details
resolve file

/Users/chen/Drafts/webpack/details/c doesn
't exist
/Users/chen/Drafts/webpack/details/c.coffee doesn'
t exist
/Users/chen/Drafts/webpack/details/c.js doesn
't exist
resolve directory

/Users/chen/Drafts/webpack/details/c doesn'
t exist (directory default file)
/Users/chen/Drafts/webpack/details/c/package.json doesn
't exist (directory description file)
[/Users/chen/Drafts/webpack/details/c]

[/Users/chen/Drafts/webpack/details/c.coffee]

[/Users/chen/Drafts/webpack/details/c.js]

@ ./a.js 2:0-14

./c是不存在,從這個錯誤信息當中我們大致能了解Webpack是怎樣查找的
大概就是會嘗試各種文件名​​,會嘗試作為模塊,等等
一般模塊就是查找node_modules ,但這個也是能被配置的: http://webpack.github.io/docs/configuration.html#resolve-modulesdirectories

CSS 及圖片的引用

require ( './bootstrap.css' );
require ( './myapp.less' );

var img = document .createElement( 'img' );
img.src =
require ( './glyph.png' );
上邊的是JavaScript代碼, CSS跟LESS,還有圖片,被直接引用了
實際上CSS被轉化為<style>標籤,而圖片可能被轉化成base64格式的dataUrl 
但是要主要在webpack.config.js文件寫好對應的loader :
// webpack.config.js 
module .exports = {
entry:
'./main.js' ,
output: {

path:
'./build' , // This is where images AND js will go
publicPath:
'http://mycdn.com/' , // This is used to generate URLs to eg images
filename:
'bundle.js'
},

module : {
loaders: [

{ test:
/\.less$/ , loader: 'style-loader!css-loader!less-loader' }, // use ! to chain loaders
{ test:
/\.css$/ , loader: 'style-loader !css-loader' },
{test:
/\.(png|jpg)$/ , loader: 'url-loader?limit=8192' } // inline base64 URLs for <=8k images, direct URLs for the rest
]

}

};

url-loader

稍微囉嗦一下這個loader,這個loader實際上是對file-loader的封裝https://github.com/webpack/url-loader 比如CSS文件當中有這樣的引用:

.demo  {
background-image : url ( 'a.png' ) ;
}

那麼對應這樣的loader配置就能把a.png抓出來, 
並且按照文件大小,或者轉化為base64,或者單獨作為文件:
module : {
loaders: [

{test:
/\.(png|jpg)$/ , loader: 'url-loader?limit=8192' } // inline base64 URLs for <=8k images, direct URLs for the rest
]

}

上邊?後邊的query有兩種寫法,可以看下文檔: http://webpack.github.io/docs/using-loaders.html#query-parameters
  • file-loader
由於url-loader是對file-loader的一個封裝,以因此帶有後者一些功能: https://github.com/webpack/file-loader 比如說, file-loader有不弱的定義文件名 ​​的功能

require ( "file?name=[path][name].[ext]?[hash]!./dir/file.png" )
對應url-loader當中如果文件超出體積,就給一個這樣的文件名​​..

打成多個包

有時考慮類庫代碼的緩存,我們會考慮打成多個包,這樣不難
比如下邊的配置,首先entry有多個屬性,對應多個JavaScript包, 
然後commonsPlugin可以用於分析模塊的共用代碼,單獨打一個包出來: https://github.com/petehunt/webpack-howto#8-optimizing-common-code https://github.com/webpack/docs/wiki/optimization#multi-page-app

// webpack.config.js 
var webpack = require ( 'webpack' );
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin( 'common.js' );

module .exports = {
entry: {

Profile:
'./profile.js' ,
Feed:
'./feed.js'
},

output: {

path:
'build' ,
filename:
'[name].js' // Template based on keys in entry above
},

plugins: [commonsPlugin]

};

對文件做revision

這個在文檔上做了說明,可以自動生成js文件的Hash: http://webpack.github.io/docs/long-term-caching.html
output: { chunkFilename: "[chunkhash].bundle.js" }
plugins: [
function () {
this .plugin( "done" , function (stats) {
require ( "fs" ).writeFileSync(
path.join(__dirname,
"..." , "stats.json" ),
JSON .stringify(stats.toJson()));
});

}

]

同時,可以註冊事件,拿到生成的帶Hash的文件的一個表
但是拿到那個表之後,就需要自己寫代碼進行替換了..這有點麻煩
官網的Issue裡提到個辦法是生成HTML時引用stats.json的數據, 
我此前的方案是生成HTML之後再進行替換,相對賴上生成時寫入更好一些

上線

  • 另一份配置文件
webpack --config webpack.min.js指定另一個名字的配置文件
這個文件當中可以寫不一樣配置,專門用於代碼上線時的操作
  • 壓縮JavaScript
因為代碼都是JavaScript,所以壓縮就很簡單了,加上一行plugin就好了http://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
plugins: [
new webpack.optimize.MinChunkSizePlugin(minSize)
]

  • 壓縮React
React官方提供的代碼是已經合併的,這個是Webpack不推薦的用法, 
在合併話的代碼上進行定制有點麻煩, Webpack提供了設置環境變量來優化代碼的方案:
new webpack.DefinePlugin({
"process.env" : {
NODE_ENV:
JSON .stringify( "production" )
}

})

  • CDN
替換CDN這個工作, Webpack也內置了,設置output.publicPath即可http://webpack.github.io/docs/configuration.html#output-publicpath

代碼熱替換

雖然文檔上寫得挺複雜的, 但如果只是簡單的功能還是很容易的
  1. 第一步,把'webpack/hot/dev-server'加入到打包的代碼當中, 
    這個是對應node_modules/webpack/目錄當中的文件的:
  entry: {
main: [
'webpack/hot/dev-server' , './main' ],
vendor: [
'lodash' , './styles' ]
},

  1. 啟動服務器, 比如我是這樣子的
webpack-dev-server --hot --quiet
正常可以看到提示說服務器已經起來了http://localhost:8080/webpack-dev-server/ 如果有index.html的話,直接訪問網址應該就能開始調試了

React Hot Replace

調試React的話,有這樣的工具簡直是神器了,甚至不用刷新頁面! http://gaearon.github.io/react-hot-loader/getstarted/
entry: [
'webpack-dev-server/client?http://0.0.0.0:8080' , // WebpackDevServer host and port
'webpack/hot/only-dev-server' ,
'./scripts/index' // Your appʼs entry point
]

我特意問了下作者為什麼上邊配置看起來不一樣.. https://github.com/gaearon/react-hot-loader/issues/73#issuecomment-73679446 回復大致說是為了避免自動的強制刷新他用了特別的寫法.. 關於這項功能具體如何實現,我沒有深入了解過...


hot replace 非靜態的網頁

上邊localhost:8080的方案並不適合複雜的頁面, 
於是文檔上給出了一套稍微複雜一些的方案,用來配合其他的服務器調試
大致的思路是這樣的:
  1. Webpack打包生成的那些靜態資源用服務器A進行serve 
    這裡說的A就是上邊說的這個:
webpack-dev-server --hot --quiet
  1. 我們的HTML由B渲染, B會引用A serve的靜態資源
    B生成的頁面當中加上類似這樣的代碼:
< script  src = "http://<A的地址>/assets/bundle.js" >
還可能要設置一下output.publicPath ,把所有靜態資源指向A 
3.文件修改時, webpack-dev-server通過socket.io通知客戶端更新
這個步驟在文檔上寫得有點難懂,大概要多嘗試幾次才行,我也弄錯很多次http://webpack.github.io/docs/webpack-dev-server.html

單獨打包CSS

因為公司裡有這個需要求,強制把CSS從js文件當中獨立出來. 
官方文檔是以插件的形式做的: http://webpack.github.io/docs/stylesheets.html#separate-css-bundle 參考文檔但是注意一下函數參數,第一第二個參數是有區別的,比如這樣用:

ExtractTextPlugin.extract( 'style-loader' , 'css!less' )
第一個參數是編譯好以後的代碼用的, 第二個參數是編譯到源代碼用的.. 有點難懂..

感想

Webpack的報錯挺不友好的,最初的時候我看著模塊找不到沒法搞明白
這種時候把中間過程打印出來看是不錯的選擇:
webpack --display- error -details
另一個報錯是沒有對應loader的提示.. log可能很長找不到重點
我建議是先自己去想想什麼地方需要考慮loader吧...可能就知道了
我還遇到就是源碼裡有使用dataUrl導致報錯...確實奇怪了
不說這些坑的話, Webpack我認為是我目前接觸到最好的前端開發方案
很多功能之前FIS文檔上看到過,但FIS相對重一些我始終沒上手
而Webpack一上來就繞過了此前公司用RequireJS打包時遇到的各種問題

如果去掃Webpack的文檔的話,還有很多功能我完全沒涉及到.. 
http://webpack.github.io/docs/

Coprights @ 2016, Blogger Templates Designed By Templateism | Distributed By Gooyaabi Templates