背景
記得2004年的時候,互聯網開發就是做網頁,那時也沒有前端和后端的區分,有時一個網站就是一些純靜態的html,通過鏈接組織在一起。用過Dreamweaver的都知道,做網頁就像用word編輯文檔一樣。一個html頁面,夾雜着css,javascript是再常見不過的事了。
隨着前端的不斷發展,特別是單頁應用的興起,這種所見即所得的IDE工具,就漸漸地退出了前端的主流。一個應用,通常只有一個靜態頁面(index.html),甚至這個頁面的body只有一個或少數幾個div組成。這時有大量的css和javascript代碼需要編寫。如何組織他們,就是現在前端所面臨和要解決的問題。
一些很好的前端框架(像angularjs,React,Vue)可以幫我們如何合理的組織代碼,保持代碼的可維護性和擴展性。這在開發階段是很有用的,可是要把應用發布到線上的時候,需要把代碼進行合並壓縮,以減小代碼體積,和文件數量,人為的對代碼進行丑化。於是就有了grunt,gulp,webpack等前端工程化打包工具。
如何使用webpack
使用webpack之前,需要安裝node.js,然后通過npm 安裝webpack.具體的安裝過程移步官網。本着從入門到精通的順序,先來看一個最簡單的應用。
場景一:
在demo1目錄下,有兩個文件,app.js,cats.js,需要把它們合並成一個bundle.js文件. demo01
cats.js:
var cats = ['dave', 'henry', 'martha']; module.exports = cats;
app.js:
cats = require('./cats.js'); console.log(cats);
如果是全局安裝的webpack,那么直接在命令行窗口中輸入webpack app.js bundle.js就可以了:
要得到壓縮版的也很容易,在后面追加一個-p參數:
bundle.js由原來的1.58kb縮小到304b.
如果每改一次代碼就要輸一次命令,就太沒意思了,這時就需要追加一個" -w " 參數 (watch) 監視代碼的改動。
webpack app.js bundle.js -p -w
注意:如果是clone的代碼,試驗時,請移除目錄下的webpack.config.js文件。
雖然簡單,但是這里有一個重要的概念要說一下:官方文檔中把app.js這個文件稱為“entry point”,即“入口”。代表着webpack從哪開始。webpack會順着這個入口文件自動尋找里邊所依賴的文件,比如demo01中的cats.js會自動被載入。而bundle.js 是我們指定打包之后輸出的文件名,默認的輸出目錄就是命令運行時所在的目錄,也可以在指定輸出目錄,如./dist/bundle.js ,這樣webpack就會自動創建dist目錄,然后把bundle.js寫在dist目錄下。由於app.js這個入口文件是純js,webpack直接就可以支持,如果是其它類型的文件,比如css,就需要用到"loader",即“加載器”,后面會有詳細介紹。
除了直接用webpack命令指定入口文件打包之外,還可以通過配置webpack.config.js文件實現同樣的功能:
webpack.config.js :
//最簡單的webpack配置 module.exports = { entry: './app.js', //入口文件地址 output: { filename: 'bundle.js', //打包后的文件名 } };
通過配置webpack.config.js之后,在命令行下只需要簡單的輸入webpack就可以了。如果是這么簡單的應用,顯然體現不出webpack.config.js存在的價值。通常我們的網站都會有多個頁面,比如index,home,about等等,每個頁面都是一個獨立的入口,於是就產生了多入口的情況,下面就看看多入口的情況下,webpack怎么輸出不同的打包文件。demo02
//webpack.config.js //多入口示例 module.exports = { entry: { bundle1: './main1.js', //入口1 bundle2: './main2.js'//入口2 }, output: { filename: '[name].js' // [name]是一個變量,會自動替換成entry的key } };
和demo01相比,這次的入口(entry)是一個對象, 用鍵值對的形式指定了多個入口文件,輸出的文件名用了變量表示。事實上,入口文件的值還可以是數組。如:
//webpack.config.js //多入口示例 module.exports = { entry: { bundle1: ['./main1.js'], //入口1 bundle2: ['./main2.js']//入口2 }, output: { filename: '[name].js' // [name]是一個變量,會自動替換成entry的key } };
這種用法,對於入口文件需要指定多種類型的文件時比較有用。比如['./main1.js','./main1.css'],后面用到再細講。小結一下:對於entry一共展示了三種形式:
1. entry:'app.js' 直接寫入口文件
2. entry:{bundle:'./main1.js'} 對象形式
3. entry:{bundle:['./main1.js']} 對象中的值用數組表示
接下來的demo03將展示webpack在jsx, es6 中的用法。這一節內容會稍稍有點多。首先是package.json文件,它不是webpack的組成部分,但是常和webpack項目出雙入對,先看一下它的大概模樣:
{ "name": "demo01", "version": "1.0.0", "description": "sample to use webpack", "main": "index.html", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack" }, "keywords": [ "webpack" ], "author": "frog", "license": "MIT",
”dependencies":{}, "devDependencies": { "babel-core": "^6.20.0", "babel-loader": "^6.2.10", "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "react": "^15.4.1", "react-dom": "^15.4.1", "webpack": "^1.13.0" } }
關於這個文件的更多介紹,請移步官方內容 這里我只重點介紹一下以下內個內容:
1. scripts 命令行腳本通過key:value的方式描述。key是腳本名,value是腳本執行的內容,通過在命令行中輸入npm run 腳本名 就可以執行。這一塊的內容是實際開發中很實用的,這里不詳情展開,參考地址
常見的腳本名有:npm run start , npm run test 。 內置的腳本名(比如start),可以省略run。
2. devDependencies 開發依賴,相應的還有一個dependencies(可以理解為生產環境依賴)
通過npm install 包名 --save-dev (保存到devDependencies),或 --save 保存到(dependencies)
package.json是用來配合包的管理和發布用的,如果你不想發布這個項目,似乎以上內容對項目開發並沒有什么好處,但是作為團隊協作,它可以方便自己和同事快速搭建項目,管理項目中用到的第三方包。
下面回到webpack.config.js這個文件來。由於jsx是react專用的語法,超出了js的語法范圍,要想加載jsx文件,需要借助一個loader(加載器), 不同類型的文件有不同的加載器,比如jsx,es6要用到babel-loader
加載css要用到css-loader,加載html要用到html-loader等等. 下面是具體的用法:
module.exports = { entry: './main.jsx', output: { filename: 'bundle.js' }, module: { loaders:[ { test: /\.js[x]?$/, exclude: /node_modules/, loader: 'babel-loader', query:{ presets:['react','es2015'] } }, ] } };
所有的loader都放在module下面的loaders里邊.通常有以下內容:
1. test:是對該類文件的正則表達式,用來判斷采用這個loader的條件。
2. exclude是排除的目錄,比如node_modules中的文件,通常都是編譯好的js,可以直接加載,因此為了優化打包速度,可以排除。作為優化手段它不是必須的。
3. loader: 加載器的名稱,每一個加載器都有屬於它自己的用法,具體要參考官方說明。
4. query: 傳遞給加載器的附加參數或配置信息,有些也可以通過在根目錄下生成特殊的文件來單獨配置,比如.babelrc
這里配置好,還不能用,需要安裝對應的加載器到項目中來,安裝方式很簡單,通過命令行,輸入npm install 加載器的名稱 --save-dev 或 --save
加--save或--save-dev的目的是為了把該插件記錄到package.json中去,方便通過npm install的時候自動安裝。
通過npm3.0+版本安裝的時候,它不會自動安裝依賴,需要手動去安裝,比如安裝這個babel-loader的時候,它提示要安裝babel-core和webpack,依次安裝即可。demo03比較激進,直接用了jsx和es6的語法,所以要安裝的插件比較多,但這也是實際開發中經常用到的。
"devDependencies": { "babel-core": "^6.20.0", "babel-loader": "^6.2.10", "babel-preset-es2015": "^6.18.0",//es6轉普通js用 "babel-preset-react": "^6.16.0", //解析jsx用 "react": "^15.4.1", "react-dom": "^15.4.1", "webpack": "^1.13.0" }
由於我們在package.json的script中加了一個start腳本,所以這次,我不打算老套的用法,這次來點新鮮的嘗試。直接運行npm start,看看是否大力出奇跡。
這和直接運行webpack是一樣的結果,但是顯得更高大上一些。如果你一半會不覺用不到react或es6這么新潮的東西,那就請忽略前面的內容,下面看一點更加簡單更加常用的加載器
demo04 css-loader 樣式加載器
module.exports = { entry: './main.js', output: { filename: 'bundle.js' }, module: { loaders:[ { test: /\.css$/, loader: 'style-loader!css-loader' }, ] } };
這里有兩個要注意的地方:
1。 對於有多個加載器串聯的情況,webpack,它是從右向左依賴加載的,也就是說先用css-loader,再用style-loader.
2. 為什么會有一個style-loader, 因為webpack默認是把css文件插在html文件內,通過style標簽加載樣式的。所以需要用style-loader這個加載器。
如果想要把css用文件的形式link到html中,也是可以的,后面會講到。
由於我們用了css加載器,所以入口文件其實也可以直接寫成:entry:'./app.css'; 效果是一樣的。這就體現了,入口文件,不一定要是js格式,只要有對應的加載器,就可以直接在入口中使用,甚至多種類型混合使用,比如['./app.js','app.css'],都是可以的。
樣式中,常常會用到圖片,比如background:url('../images/logo.jpg'); 如果沒有指定加載器,就會報錯(you may need an appropriate loader to handle this file type),這時,就需要用到圖片加載器了,不要以為,只有在入口中用到的文件才要加載器,只要是在webpack工作期間加載到的文件,只要不是js文件,就需要指定加載器,並在webpack.config.js中正確配置。
module.exports = { entry: './main.js', output: { filename: 'bundle.js' }, module: { loaders:[ { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192&name=[name][ext]'} ] } };
圖片加載-demo05 示例中用的是url-loader,並不是期望的image-loader, 原因是url-loader就可以加載圖片字體這些文件了,因此不需要重復造輪子,事實上,url-loader還不是最終的加載器,它只不過是對file-loader的進一步封裝。通過在url-loader后面加?來掛載更多的配置參數,可以實現定制化的需求,比如對於圖片小於8192字節的圖片,采用base64的方式,直接輸出在css中,可以減少http請求。對於大這個限制的圖片,通過name指定輸出的文件名,在前面指定路徑也是可以的。比如/images/[name][ext] ,這里的[name]和[ext]都是變量的表示,前面有講過,用在這里,表示用原來輸入時的文件名和擴展名。需要注意的是,這個路徑是參考默認的輸出路徑的來的。如果要指定輸出路徑怎么處理呢?
請參考以下方法:
1. 通過在webpack.config.js 中指定,output:{path:'./dist',...}
module.exports = { entry: './src/app.js', output: { path: './dist',//新的輸出路徑 filename: 'app.bundle.js' } };
'./'代表項目的當前目錄,通常指根目錄,這是一種相對路徑的表示,也可以用絕對路徑,通過path.resolve(__dirname,'./')來指定,這時,webpack所生成的js,css文件都會變成./dist目錄下,而對於本例中的圖片,則還是在./目錄下,
並沒有把圖片生成在dist目錄下,試試 loader: 'url?publicPath=./dist/'
module.exports = { entry: ['./main.js','./icon.css'], output: { path:'./dist', filename: 'bundle.js' }, module: { loaders:[ { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192&publicPath=./dist/'}, { test: /\.css$/, loader: 'style-loader!css-loader' }, ] } };
通過指定這個publicPath實現了圖片生成到指定的目錄。同樣的,通過在output中指定這個值也是同樣的作用。
output: { path:'./dist', publicPath:'./dist/', //在這里指定同樣生效 filename: 'bundle.js' },
這個publicPath原本是用來配置虛擬目錄用的,也就是通過http方式訪問時的路徑,或者通過webpack HMR方式加載時的輸出目錄。在這里只能算是一種hack用法。說到output,就要提一下文件緩存[hash]的用法:
output: { path:'./dist', publicPath:'./dist/', filename: 'bundle_[hash:8].js' //通過:8截取has值的前8位 },
這個[hash]作用很少被提到,在實際開發中,是很常見的功能,原樣輸出的hash我覺得太長,可以通過[hash:加數字]的方式進行截取,很方便。
對於webpack.config.js ,前面已經介紹了entry,output,module,下面以代碼丑化為例,說說plugins,webpack的插件的用法:demo07
var webpack = require('webpack'); var uglifyJsPlugin = webpack.optimize.UglifyJsPlugin; module.exports = { entry: './main.js', output: { filename: 'bundle.js' }, plugins: [ new uglifyJsPlugin({ compress: { warnings: false } }) ] }
plugins:的值是一個數組,所有插件都通過npm install進行安裝,然后在plugins數組中添加對應的插件配置。有三個插件需要提一下:
1. HtmlwebpackPlugin 這個插件可以把生成的css ,js 插入到html頁中的head,body中,這對於加了hash值的輸出很有用。這個功能也可以用gulp的insject插件做。不過既然用webpack了,就暫時忘了gulp吧。在具有相似功能的不同的工具之間切換,並不是一個好主意。不過html這款插件有一個小小的問題,它對html中的img不會像css中那樣解析。造成dist目錄下的html文件,img下的src報錯。解決辦法是添加html-withimg-loader這個插件。
{ test:/.html$/, loader:'html-withimg-loader?min=false' },
2. CommonsChunkPlugin 提取公共代碼,這個不需要安裝,webpack集成有。
new webpack.optimize.CommonsChunkPlugin({ name:'vendor', filename:'js/vendor.js', chunks:['chunk1','chunk2','chunk3']//不寫為所有的chunk, }),
chunk (塊), webpack中另一個非常重要的概念,和entry對應。有三種情況:
2.1 如果entry通過字符串的方式指定的入口文件,那么chunk就是指入口文件,比如entry: './app.js'; 那么可以肯定chunk和'./app.js'一一對應。
2.2 如果是entry:['./app1.js','app2.js']那么chunk就是這兩個文件之和。
* 以上chunk的[name]就是默認的"main".
2.3 如果是下面這種形式:
entry:{ index:'./index.js', error:'./error.js', vendor:['react','redux'] }
那么就會產生多個chunk,[name]分別和index,error,vendor對應。
3. ExtractTextPlugin 這個插件就是開頭提到的,從html中分離css的插件。npm install extract-text-webpack-plugin --save-dev
plugins: [ new ExtractTextPlugin("[name].css"), ]
需要注意的是,如果在[name].css前面加了子路徑,如css/[name].css 那么就要小心樣式中的圖片路徑出錯,特別是在沒有指定publicPah的情況下。background:url(這個地方的圖片默認是和chunk的輸出路徑同級的,如果指定了publicPath,則以publicPath代替,不存在這個問題),但是由於我們人為的指定了打包后的樣式放在css/目錄下,而圖片默認還在原來的目錄,這就導致css中引用的圖片路徑失效。看下面的例子:
var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: ['./main.js','./icon.css'], output: { path:'./dist', //publicPath:'./dist/', filename: 'bundle_[hash:8].js' }, module: { loaders:[ { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}, { test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css') }, ] }, plugins: [ new ExtractTextPlugin("css/[name].css"), ] };
從圖上看到,添了css子目錄之后,樣式中的圖片,還是原來的路徑,而圖片並不存在css目錄下。
解決辦法,要么不加子目錄,保持樣式文件和圖片文件始終在同一層級,要么添加publicPath,相當於使用絕對路徑。由於這個是在webpack.config.js中配置的,要更換也很容易。
看起來一切都很美好,可是當我們的html中也用了img標簽的時候,問題就來了,還記得html-withimg-loader這個插件嗎?它際實上也是調用的url-loader,所以,它最終的圖片輸出路徑也樣受publicPath的影響。考慮一下這樣的目錄結構:
樣式文件是位於css子目錄,而html則是和圖片保持同級的。樣式中的圖片需要指定為"../",而html中的圖片需要指定成"./",這在同一個publicPath中,顯示是沖突的。這時就需要權衡一下,要么所有的文件都堆在根目錄下,要么html中的圖片用別的插件進行處理。總之,不能讓這種相沖突的情況發生。
最后再簡單說一下webpack.config.js 中的 resolve;
resolve: { extensions: ['', '.js', '.vue', '.json'] },
通常我們都知道通過配置這個屬性下的extensions,可以省略擴展名,似乎沒有什么可以介紹的,直到有一次我在項目中通過npm install vue --save 安裝了vue ,然后我在代碼中用import Vue from 'vue' 導入了vue。到這一步都是正常的,可是當我打算進一步使用vue的時候,代碼就報錯了,然后去查官方文檔(出錯的時候不要驚慌,大多數情況都會在官方找到解決的辦法,比如github中的issue等)。官方介紹如下:
“ There are two builds available, the standalone build and the runtime-only build. The difference being that the former includes the template compiler and the latter does not.By default, the NPM package exports the runtime-only build. To use the standalone build, add the following alias to your Webpack config:”
resolve: { alias: { 'vue$': 'vue/dist/vue.common.js' } },
大意是說:vue有兩種構建方式,獨立構建和運行構建。它們的區別在於前者包含模板編譯器而后者不包含。而默認 NPM 包導出的是 運行時 構建。為了使用獨立構建,要在 webpack 配置中添加下面的別名:添加alias:{'vue$':'vue/dist/vue.common.js'}. 這個別名,同樣適用於jquery,zepto這些庫。
對於vue來說,如果不用別名,也可以從node_modules/vue/dist/復制vue.common.js到開發目錄下,比如./src/vue下面,然后像普通的js文件一樣引用, import vue from './src/js/vue.common.js' 這也是可以的。只是和使用別名相比,顯的很lower
小結
使用webpack,分為兩種方式,一種是CLI(命令行方式),一種是API方式(new webpack(config)),兩種方式都可以通過webpack.config.js 來配置。所以學習webpack,就是掌握webpack.config.js配置的過程。我相繼介紹了entry,output,module,plugins,resolve,本來還想寫寫webpack的熱加載HMR, 以及webpack-dev-server,browser-sync結合webpack的用法,感覺要寫的內容有點多,這些內容也是實際開發中非常有用的技術。越寫到后面,越覺得難於下筆。想起一句話,要想給別人一滴水,自己至少要有一桶水。前端工程自動化方案更新很快,webpack還沒有來的極普及,webpack2,rollup等又出來了. 學習這些工具,是為了減輕重復勞動,提高效率。選擇適合自己的方案,而不是在追尋技術的路上迷失了方向。