說在開頭
上個月斷斷續續的在研究webpack的配置,但是很多網上的文章基本上都是只說了開發環境的配置,而忽略了生產環境的配置。大致研究了一下門路,然后就來寫一篇隨筆讓自己能在以后能有個地方可以做參考。
正文開始
我就假裝大家都是裝了node的情況下。
1、進入項目目錄,運行`npm init`按照步驟填寫最終生成`package.json`文件,所有使用 npm 做依賴管理的項目,根目錄下都會有一個這個文件,該文件描述了項目的基本信息以及一些第三方依賴項(插件)。詳細的使用說明可查閱[
官網文檔],不過是英文的。
2、已知我們將使用 webpack 作為構建工具,那么就需要安裝相應插件,運行 `npm install webpack webpack-dev-server --save-dev` 來安裝兩個插件。
又已知我們將使用 React ,也需要安裝相應插件,運行 `npm i react react-dom --save`來安裝兩個插件。其中`i`是`install`的簡寫形式。安裝完成之后,查看`package.json`可看到多了`devDependencies`和`dependencies`兩項,根目錄也多了一個`node_modules`文件夾。
3、`--save` 和 `--save-dev` 的區別
`npm i`時使用`--save`和`--save-dev`,可分別將依賴(插件)記錄到`package.json`中的`dependencies`和`devDependencies`下面。
`dependencies`下記錄的是項目在運行時必須依賴的插件,常見的例如`react` `jquery`等,即使項目打包好了、上線了,這些也是需要用的,否則程序無法正常執行。
`devDependencies`下記錄的是項目在開發過程中使用的插件,例如這里我們開發過程中需要使用`webpack`打包,但是一旦項目打包發布、上線了之后,`webpack`就都沒有用了,可卸磨殺驢。
延伸一下,我們的項目有`package.json`,其他我們用的項目如`webpack`也有`package.json`,見`./node_modules/webpack/package.json`,其中也有`devDependencies`和`dependencies`。當我們使用`npm i webpack`時,`./node_modules/webpack/package.json`中的`dependencies`會被 npm 安裝上,而`devDependencies`也沒必要安裝。
4、webpack.config.js
為了提高學習效率,webpack 最最基礎的用法,就不再挨個演示了(推薦一個非常好的[
入門文章],以及[
更多資料])這里我們把項目中的`webpack.config.js`這個配置文件詳細的講解一下,過程中也會照顧 0 基礎的同學。
webpack.config.js 就是一個普通的 js 文件,符合 commonJS 規范。最后輸出一個對象,即`module.exports = {...}`
這個比較基礎,不過需要新建`./app/index.js`作為入口文件,目前項目中只有這一個入口文件。不過 webpack 支持多個入口文件,可查閱文檔。
輸出就是一個`bundle.js`,js 和 css 都在里面,不過只有在開發環境下才用,發布代碼的時候,肯定不能只有這么一個文件,接下來會講到。
module
針對不同類型的文件,使用不同的`loader`,當然使用之前要安裝,例如`npm i style-loader css-loader --save-dev`。該項目代碼中,我們用到的文件格式有:js/jsx 代碼、css/less 代碼、圖片、字體文件。
less 是 css 的語法糖,可以更高效低冗余的寫 css,不熟悉的朋友可去[
官網]看看,非常簡單。
在加載 css/less 時用到了`postcss`,主要使用`autoprefixer`功能,幫助自動加 css3 的瀏覽器前綴,非常好用。
編譯 es6 和 jsx 語法時,用到家喻戶曉的`babel`,另外還需增加一個`.babelrc`的配置文件。
plugins
使用 html 模板(需要`npm i html-webpack-plugin --save-dev`),這樣可以將輸出的文件名(如`./bundle.js`)自動注入到 html 中,不用我們自己手寫。手寫的話,一旦修改就需要改兩個地方。
使用熱加載和自動打開瀏覽器插件
devServer
對 webpack-dev-server 的配置
npm start
在 package.json 中,輸入以下代碼,將這一串命令封裝為`npm start`,這樣就可以運行項目代碼了。
"scripts": { "start": "NODE_ENV=dev webpack-dev-server --progress --colors" }
代碼中`NODE_ENV=dev`代表當前是開發環境下,這里的`"dev"`可被 js 代碼中的`process.env.NODE_ENV`得到並做一些其他處理。
5、定義環境全局變量
以下定義,可使得代碼通過`__DEV__`得到當前是不是開發模式。
new webpack.DefinePlugin({ __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false')) })
打開`./app/util/localStore.js`可以看到`if (__DEV__) { console.error('localStorage.getItem報錯, ', ex.message) }`,即只有開發環境下才提示error,發布之后就不會提示了。(因為發布的命令中用到`NODE_ENV=production`)
6、生產環境的配置 webpack.production.config.js
開發環境下,可以不用考慮系統的性能,更多考慮的是如何增加開發效率。而發布系統時,就需要考慮發布之后的系統的性能,包括加載速度、緩存等。下面介紹發布用配置代碼和開發用的不一樣的地方。
發布到 `./build` 文件夾中 : `path: __dirname + "/build",`
vendor
將第三方依賴單獨打包。即將 node_modules 文件夾中的代碼打包為 vendor.js 將我們自己寫的業務代碼打包為 app.js。這樣有助於緩存,因為在項目維護過程中,第三方依賴不經常變化,而業務代碼會經常變化。
md5后綴
為每個打包出來的文件都加md5后綴,例如`"/js/[name].[chunkhash:8].js"`,可使文件強緩存。
分目錄
打包出來的不同類型的文件,放在不同目錄下,例如圖片文件將放在`img/`目錄下
Copyright
自動為打包出來的代碼增加 copyright 內容
分模塊
`new webpack.optimize.OccurenceOrderPlugin(),`
壓縮代碼
使用 Uglify 壓縮代碼,其中`warnings: false`是去掉代碼中的 warning
分離 css 和 js 文件
開發環境下,css 代碼是放在整個打包出來的那個 bundle.js 文件中的,發布環境下當然不能混淆在一起,使用`new ExtractTextPlugin('/css/[name].[chunkhash:8].css'),`將 css 代碼分離出來。
7、npm run build
打開`package.json`,查看以下代碼。`npm start`和`npm run build`分別是運行代碼和打包項目。另外,`"start"、"test"`可以不用`run`。
"scripts": { "start": "NODE_ENV=dev webpack-dev-server --progress --colors", "build": "rm -rf ./build && NODE_ENV=production webpack --config ./webpack.production.config.js --progress --colors" },
這兩個命令主要有以下區別:
- 前者中默認使用 webpack.config.js 作為配置文件,而后者中強制使用 webpack.production.config.js 作為配置文件
- 前者`NODE_ENV=dev`而后者`NODE_ENV=production`,標識不同的環境。而這個`"dev" "production"`可以在代碼中通過`process.env.NODE_ENV`獲取。
最小化壓縮 React
以下配置可以告訴 React 當前是生產環境,請最小化壓縮 js ,即把開發環境中的一些提示、警告、判斷通通去掉,直流以下發布之后可用的代碼。
new webpack.DefinePlugin({ 'process.env':{ 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) } }),
接下來把開發代環境的配置和生產環境的配置貼上
webpack.config.js
var path = require('path') var webpack = require('webpack') var HtmlWebpackPlugin = require('html-webpack-plugin'); var OpenBrowserPlugin = require('open-browser-webpack-plugin'); module.exports = { entry: path.resolve(__dirname, 'app/index.js'), output: { filename: "bundle.js" }, resolve: { extensions: ['', '.js', '.jsx'] }, module: { loaders: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel' }, { test: /\.less$/, exclude: /node_modules/, loader: 'style!css!postcss!less' }, { test: /\.css$/, exclude: /node_modules/, loader: 'style!css!postcss' }, { test:/\.(png|gif|jpg|jpeg|bmp)$/i, loader:'url-loader?limit=5000' }, // 限制大小5kb { test:/\.(png|woff|woff2|svg|ttf|eot)($|\?)/i, loader:'url-loader?limit=5000'} // 限制大小小於5k ] }, postcss: [ require('autoprefixer') //調用autoprefixer插件,例如 display: flex ], plugins: [ // html 模板插件 new HtmlWebpackPlugin({ template: __dirname + '/app/index.html' }), // 熱加載 new webpack.HotModuleReplacementPlugin(), // 打開瀏覽器 new OpenBrowserPlugin({ url: 'http://localhost:8080' }), // 可在業務js代碼中使用 __DEV__ 判斷是否是開發環境 (dev模式下可以提示錯誤、測試報告等, production模式不提示) new webpack.DefinePlugin({ __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false')), }) ], devServer: { proxy: { // 凡是 `/api` 開頭的 http 請求,都會被代理到 localhost:3000 上,由 koa 提供 mock 數據。 // koa 代碼在 ./mock 目錄中,啟動命令為 npm run mock '/api': { target: 'http://localhost:3000', secure: false } }, colors: true, // 終端舒服為彩色 historyApiFallback: true, //不跳轉,在開發單頁應用時非常有用,它依賴於HTML5 history API,如果設置為true,所有的跳轉將指向index.html inline: true, // 實時刷新 hot: true, // 使用熱加載插件 HotModuleReplacementPlugin } }
webpack.production.config.js
var pkg = require('./package.json') var path = require('path') var webpack = require('webpack'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { app: path.resolve(__dirname, 'app/index.js'), // 將 第三方依賴(node_modules中的) 單獨打包 vendor: Object.keys(pkg.dependencies) }, output: { path: __dirname + "/build", filename: "/js/[name].[chunkhash:8].js" }, resolve:{ extensions:['', '.js','.jsx'] }, module: { loaders: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel' }, { test: /\.less$/, exclude: /node_modules/, loader: ExtractTextPlugin.extract('style', 'css!postcss!less') }, { test: /\.css$/, exclude: /node_modules/, loader: ExtractTextPlugin.extract('style', 'css!postcss') }, { test:/\.(png|gif|jpg|jpeg|bmp)$/i, loader:'url-loader?limit=5000&name=img/[name].[chunkhash:8].[ext]' }, { test:/\.(png|woff|woff2|svg|ttf|eot)($|\?)/i, loader:'url-loader?limit=5000&name=fonts/[name].[chunkhash:8].[ext]'} ] }, postcss: [ require('autoprefixer') ], plugins: [ // webpack 內置的 banner-plugin new webpack.BannerPlugin("Copyright by Nick930826@github.com."), // html 模板插件 new HtmlWebpackPlugin({ template: __dirname + '/app/index.html' }), // 定義為生產環境,編譯 React 時壓縮到最小 new webpack.DefinePlugin({ 'process.env':{ 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) } }), // 為組件分配ID,通過這個插件webpack可以分析和優先考慮使用最多的模塊,並為它們分配最小的ID new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ compress: { //supresses warnings, usually from module minification warnings: false } }), // 分離CSS和JS文件 new ExtractTextPlugin('/css/[name].[chunkhash:8].css'), // 提供公共代碼 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '/js/[name].[chunkhash:8].js' }), // 可在業務 js 代碼中使用 __DEV__ 判斷是否是dev模式(dev模式下可以提示錯誤、測試報告等, production模式不提示) new webpack.DefinePlugin({ __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false')) }) ] }