2015年3月26日 星期四

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/

By webber0928

一個小菜鳥工程師,對籃球還有夢想的男孩。

0 意見:

張貼留言

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