webpack:從入門到真實項目配置

目前本身組建的一個團隊正在寫一份面試圖譜,將會在七月中旬開源。內容十分豐富,初版會開源前端方面知識和程序員必備知識,後期會逐步寫入後端方面知識。由於工程所涉及內容太多(目前已經寫了一個半月),而且還需翻譯成英文,因此所需時間較長。有興趣的同窗能夠 Follow 個人 Github 獲得最快的更新消息。css

該文使用的 Webpack 版本爲 3.6.0,本文分兩部分。第一步是簡單的使用 webpack,第二部分經過一個真實項目來配置 webpack,沒有使用任何的 CLI,都是一步步配置直到完成生產代碼的打包。這是本項目對應的倉庫,每一個小節基本都對應了一次 commit。html

這是本文的大綱,若是以爲有興趣你就能夠往下看了 前端

Webpack 究竟是什麼

自從出現模塊化之後,你們能夠將本來一坨代碼分離到個個模塊中,可是由此引起了一個問題。每一個 JS 文件都須要從服務器去拿,由此會致使加載速度變慢。Webpack 最主要的目的就是爲了解決這個問題,將全部小文件打包成一個或多個大文件,官網的圖片很好的詮釋了這個事情,除此以外,Webpack 也是一個能讓你使用各類前端新技術的工具。node

簡單使用

安裝

在命令行中依次輸入react

mkdir  webpack-demo
cd webpack-demo
// 建立 package.json,這裏會問一些問題,直接回車跳過就行
npm init 
// 推薦這個安裝方式,固然你也安裝在全局環境下
// 這種安裝方式會將 webpack 放入 devDependencies 依賴中
npm install --save-dev webpack
複製代碼

而後按照下圖建立文件 webpack

在如下文件寫入代碼git

// sum.js
// 這個模塊化寫法是 node 環境獨有的,瀏覽器原生不支持使用
module.exports = function(a, b) {
    return a + b
}
// index.js
var sum = require('./sum')
console.log(sum(1, 2))
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./build/bundle.js"></script>
</body>
</html>
複製代碼

如今咱們開始配置最簡單的 webpack,首先建立 webpack.config.js 文件,而後寫入以下代碼程序員

// 自帶的庫
const path = require('path')
module.exports = {
    entry:  './app/index.js', // 入口文件
    output: {
      path: path.resolve(__dirname, 'build'), // 必須使用絕對地址,輸出文件夾
      filename: "bundle.js" // 打包後輸出文件的文件名
    }
  }
複製代碼

如今咱們能夠開始使用 webpack 了,在命令行中輸入github

node_modules/.bin/webpack
複製代碼

沒問題的話你應該能夠看到相似的樣子 web

能夠發現本來兩個 JS 文件只有 100B,可是打包後卻增加到 2.66KB,這之中 webpack 確定作了什麼事情,咱們去 bundle.js 文件中看看。

把代碼簡化之後,核心思路是這樣的

var array = [(function () {
        var sum = array[1]
        console.log(sum(1, 2))
    }),
    (function (a,b) {
        return a + b
    })
]
array[0]() // -> 3
複製代碼

由於 module.export 瀏覽器是不支持的,因此 webpack 將代碼改爲瀏覽器能識別的樣子。如今將 index.html 文件在瀏覽器中打開,應該也能夠看到正確的 log。

咱們以前是在文件夾中安裝的 webpack,每次要輸入 node_modules/.bin/webpack 過於繁瑣,能夠在 package.json 以下修改

"scripts": {
    "start": "webpack"
  },
複製代碼

而後再次執行 npm run start,能夠發現和以前的效果是相同的。簡單的使用到此爲止,接下來咱們來探索 webpack 更多的功能。

Loader

Loader 是 webpack 一個很強大功能,這個功能可讓你使用不少新的技術。

Babel

Babel 可讓你使用 ES2015/16/17 寫代碼而不用顧忌瀏覽器的問題,Babel 能夠幫你轉換代碼。首先安裝必要的幾個 Babel 庫

npm i --save-dev babel-loader babel-core babel-preset-env
複製代碼

先介紹下咱們安裝的三個庫

  • babel-loader 用於讓 webpack 知道如何運行 babel
  • babel-core 能夠看作編譯器,這個庫知道如何解析代碼
  • babel-preset-env 這個庫能夠根據環境的不一樣轉換代碼

接下來更改 webpack-config.js 中的代碼

module.exports = {
// ......
    module: {
        rules: [
            {
            // js 文件才使用 babel
                test: /\.js$/,
             // 使用哪一個 loader
                use: 'babel-loader',
            // 不包括路徑
                exclude: /node_modules/
            }
        ]
    }
}
複製代碼

配置 Babel 有不少方式,這裏推薦使用 .babelrc 文件管理。

// ..babelrc
{
    "presets": ["babel-preset-env"]
}
複製代碼

如今將以前 JS 的代碼改爲 ES6 的寫法

// sum.js
export default (a, b) => {
    return a + b
}
// index.js
import sum from './sum'
console.log(sum(1, 2))
複製代碼

執行 npm run start,再觀察 bundle.js 中的代碼,能夠發現代碼被轉換過了,而且一樣能夠正常 輸出3。

固然 Babel 遠不止這些功能,有興趣的能夠前往官網本身探索。

處理圖片

這一小節咱們將使用 url-loaderfile-loader,這兩個庫不只能夠處理圖片,還有其餘的功能,有興趣的能夠自行學習。

先安裝庫

npm i --save-dev url-loader file-loader
複製代碼

建立一個 images 文件夾,放入兩張圖片,而且在 app 文件夾下建立一個 js 文件處理圖片 ,目前的文件夾結構如圖

// addImage.js
let smallImg = document.createElement('img')
// 必須 require 進來
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)

let bigImg = document.createElement('img')
bigImg.src = require('../images/big.jpeg')
document.body.appendChild(bigImg)
複製代碼

接下來修改 webpack.config.js 代碼

module.exports = {
// ...
    module: {
        rules: [
            // ...
            {
            // 圖片格式正則
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: [
                  {
                    loader: 'url-loader',
                    // 配置 url-loader 的可選項
                    options: {
                    // 限制 圖片大小 10000B,小於限制會將圖片轉換爲 base64格式
                      limit: 10000,
                    // 超出限制,建立的文件格式
                    // build/images/[圖片名].[hash].[圖片格式]
                      name: 'images/[name].[hash].[ext]'
                   }
                  }
                ]
            }
        ]
    }
  }
複製代碼

運行 npm run start,打包成功以下圖

能夠發現大的圖片被單獨提取了出來,小的圖片打包進了 bundle.js 中。

在瀏覽器中打開 HTML 文件,發現小圖確實顯示出來了,可是卻沒有看到大圖,打開開發者工具欄,能夠發現咱們大圖的圖片路徑是有問題的,因此咱們又要修改 webpack.config.js 代碼了。

module.exports = {
    entry:  './app/index.js', // 入口文件
    output: {
      path: path.resolve(__dirname, 'build'), // 必須使用絕對地址,輸出文件夾
      filename: "bundle.js", // 打包後輸出文件的文件名
      publicPath: 'build/' // 知道如何尋找資源
    }
    // ...
  }
複製代碼

最後運行下 npm run start,編譯成功了,再次刷新下頁面,能夠發現此次大圖被正確的顯示了。下一小節咱們將介紹如何處理 CSS 文件。

處理 CSS 文件

添加 styles 文件夾,新增 addImage.css 文件,而後在該文件中新增代碼

img {
    border: 5px black solid;
}
.test {border: 5px black solid;}
複製代碼

這一小節咱們先使用 css-loaderstyle-loader 庫。前者可讓 CSS 文件也支持 impost,而且會解析 CSS 文件,後者能夠將解析出來的 CSS 經過標籤的形式插入到 HTML 中,因此後面依賴前者。

npm i --save-dev css-loader style-loader
複製代碼

首先修改 addImage.js 文件

import '../styles/addImage.css'

let smallImg = document.createElement('img')
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)

// let bigImg = document.createElement('img')
// bigImg.src = require('../images/big.jpeg')
// document.body.appendChild(bigImg)
複製代碼

而後修改 webpack.config.js 代碼

module.exports = {
// ...
    module: {
      rules: [
        {
            test: /\.css$/,
            use: ['style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        modules: true
                       }
                }
            ]
        },
      ]
    }
  }
複製代碼

運行下 npm run start,而後刷新頁面,能夠發現圖片被正確的加上了邊框,如今咱們來看一下 HTML 的文件結構

從上圖能夠看到,咱們在 addImage.css 文件中寫的代碼被加入到了 style 標籤中,而且由於咱們開啓了 CSS 模塊化的選項,因此 .test 被轉成了惟一的哈希值,這樣就解決了 CSS 的變量名重複問題。

可是將 CSS 代碼整合進 JS 文件也是有弊端的,大量的 CSS 代碼會形成 JS 文件的大小變大,操做 DOM 也會形成性能上的問題,因此接下來咱們將使用 extract-text-webpack-plugin 插件將 CSS 文件打包爲一個單獨文件

首先安裝 npm i --save-dev extract-text-webpack-plugin

而後修改 webpack.config.js 代碼

const ExtractTextPlugin = require("extract-text-webpack-plugin")

module.exports = {
// ....
    module: {
      rules: [
        {
          test: /\.css$/,
          // 寫法和以前基本一致
          loader: ExtractTextPlugin.extract({
          // 必須這樣寫,不然會報錯
                fallback: 'style-loader',
                use: [{
                    loader: 'css-loader',
                    options: { 
                        modules: true
                    }
                }]
            })
        ]
        }
      ]
    },
    // 插件列表
    plugins: [
    // 輸出的文件路徑
      new ExtractTextPlugin("css/[name].[hash].css")
    ]
  }
複製代碼

運行下 npm run start,能夠發現 CSS 文件被單獨打包出來了

可是這時候刷新頁面會發現圖片的邊框消失了,那是由於咱們的 HTML 文件沒有引用新的 CSS 文件,因此這裏須要咱們手動引入下,在下面的章節咱們會經過插件的方式自動引入新的文件。

接下來,會用一個項目來繼續咱們的 webpack 學習,在這以前,先 clone 一下項目。該項目原地址是 這裏,由於使用的 webpack 版本過低,而且依賴的庫也有點問題,故我將項目拷貝了過來並修改了幾個庫的版本號。

請依次按照如下代碼操做

git clone https://github.com/KieSun/webpack-demo.git
cd webpack-demo
// 切換到 0.1 標籤上並建立一個新分支
git checkout -b demo 0.1
cd project
npm i 
// 查看分支是否爲 demo,沒問題的話就能夠進行下一步
複製代碼

如何在項目中使用 webpack

項目中已經配置了很簡單的 babel 和 webpack,直接運行 npm run build 便可

這時候你會發現這個 bundle.js 竟然有這麼大,這確定是不能接受的,因此接下來章節的主要目的就是將單個文件拆分爲多個文件,優化項目。

分離代碼

先讓咱們考慮下緩存機制。對於代碼中依賴的庫不多會去主動升級版本,可是咱們本身的代碼卻每時每刻都在變動,因此咱們能夠考慮將依賴的庫和本身的代碼分割開來,這樣用戶在下一次使用應用時就能夠儘可能避免重複下載沒有變動的代碼,那麼既然要將依賴代碼提取出來,咱們須要變動下入口和出口的部分代碼。

// 這是 packet.json 中 dependencies 下的
const VENOR = ["faker",
  "lodash",
  "react",
  "react-dom",
  "react-input-range",
  "react-redux",
  "redux",
  "redux-form",
  "redux-thunk"
]

module.exports = {
// 以前咱們都是使用了單文件入口
// entry 同時也支持多文件入口,如今咱們有兩個入口
// 一個是咱們本身的代碼,一個是依賴庫的代碼
  entry: {
  // bundle 和 vendor 都是本身隨便取名的,會映射到 [name] 中
    bundle: './src/index.js',
    vendor: VENOR
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  // ...
 }
複製代碼

如今咱們 build 一下,看看是否有驚喜出現

真的有驚喜。。爲何 bundle 文件大小壓根沒變。這是由於 bundle 中也引入了依賴庫的代碼,剛纔的步驟並無抽取 bundle 中引入的代碼,接下來讓咱們學習如何將共同的代碼抽取出來。

抽取共同代碼

在這小節咱們使用 webpack 自帶的插件 CommonsChunkPlugin

module.exports = {
//...
  output: {
    path: path.join(__dirname, 'dist'),
    // 既然咱們但願緩存生效,就應該每次在更改代碼之後修改文件名
    // [chunkhash]會自動根據文件是否更改而更換哈希
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    // vendor 的意義和以前相同
    // manifest文件是將每次打包都會更改的東西單獨提取出來,保證沒有更改的代碼無需從新打包,這樣能夠加快打包速度
      names: ['vendor', 'manifest'],
      // 配合 manifest 文件使用
      minChunks: Infinity
    })
  ]
};
複製代碼

當咱們從新 build 之後,會發現 bundle 文件很明顯的減少了體積

可是咱們使用哈希來保證緩存的同時會發現每次 build 都會生成不同的文件,這時候咱們引入另外一個插件來幫助咱們刪除不須要的文件。

npm install --save-dev clean-webpack-plugin
複製代碼

而後修改配置文件

module.exports = {
//...
  plugins: [
  // 只刪除 dist 文件夾下的 bundle 和 manifest 文件
    new CleanWebpackPlugin(['dist/bundle.*.js','dist/manifest.*.js'], {
    // 打印 log
      verbose: true,
      // 刪除文件
      dry: false
    }),
  ]
};
複製代碼

而後 build 的時候會發現以上文件被刪除了。

由於咱們如今將文件已經打包成三個 JS 了,之後也許會更多,每次新增 JS 文件咱們都須要手動在 HTML 中新增標籤,如今咱們能夠經過一個插件來自動完成這個功能。

npm install html-webpack-plugin --save-dev
複製代碼

而後修改配置文件

module.exports = {
//...
  plugins: [
  // 咱們這裏將以前的 HTML 文件當作模板
  // 注意在以前 HTML 文件中請務必刪除以前引入的 JS 文件
    new HtmlWebpackPlugin({
      template: 'index.html'
    })
  ]
};
複製代碼

執行 build 操做會發現同時生成了 HTML 文件,而且已經自動引入了 JS 文件

按需加載代碼

在這一小節咱們將學習如何按需加載代碼,在這以前的 vendor 入口我發現忘記加入 router 這個庫了,你們能夠加入這個庫而且從新 build 下,會發現 bundle 只有不到 300KB 了。

如今咱們的 bundle 文件包含了咱們所有的本身代碼。可是當用戶訪問咱們的首頁時,其實咱們根本無需讓用戶加載除了首頁之外的代碼,這個優化咱們能夠經過路由的異步加載來完成。

如今修改 src/router.js

// 注意在最新版的 V4路由版本中,更改了按需加載的方式,若是安裝了 V4版,能夠自行前往官網學習
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';

import Home from './components/Home';
import ArtistMain from './components/artists/ArtistMain';

const rootRoute = {
  component: Home,
  path: '/',
  indexRoute: { component: ArtistMain },
  childRoutes: [
    {
      path: 'artists/new',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistCreate')
          .then(module => cb(null, module.default))
      }
    },
    {
      path: 'artists/:id/edit',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistEdit')
          .then(module => cb(null, module.default))
      }
    },
    {
      path: 'artists/:id',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistDetail')
          .then(module => cb(null, module.default))
      }
    }
  ]
}

const Routes = () => {
  return (
    <Router history={hashHistory} routes={rootRoute} /> ); }; export default Routes; 複製代碼

而後執行 build 命令,能夠發現咱們的 bundle 文件又瘦身了,而且新增了幾個文件

將 HTML 文件在瀏覽器中打開,當點擊路由跳轉時,能夠在開發者工具中的 Network 一欄中看到加載了一個 JS 文件。

首頁

點擊右上角 Random Artist 之後

自動刷新

每次更新代碼都須要執行依次 build,而且還要等上一會很麻煩,這一小節介紹如何使用自動刷新的功能。

首先安裝插件

npm i --save-dev webpack-dev-server
複製代碼

而後修改 packet.json 文件

"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server --open"
  },
複製代碼

如今直接執行 npm run dev 能夠發現瀏覽器自動打開了一個空的頁面,而且在命令行中也多了新的輸出

等待編譯完成之後,修改 JS 或者 CSS 文件,能夠發現 webpack 自動幫咱們完成了編譯,而且只更新了須要更新的代碼

可是每次從新刷新頁面對於 debug 來講很不友好,這時候就須要用到模塊熱替換了。可是由於項目中使用了 React,而且 Vue 或者其餘框架都有本身的一套 hot-loader,因此這裏就略過了,有興趣的能夠本身學習下。

生成生產環境代碼

如今咱們能夠將以前所學和一些新加的插件整合在一塊兒,build 生產環境代碼。

npm i --save-dev url-loader optimize-css-assets-webpack-plugin file-loader extract-text-webpack-plugin
複製代碼

修改 webpack 配置

var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanWebpackPlugin = require('clean-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

const VENOR = ["faker",
  "lodash",
  "react",
  "react-dom",
  "react-input-range",
  "react-redux",
  "redux",
  "redux-form",
  "redux-thunk",
  "react-router"
]

module.exports = {
  entry: {
    bundle: './src/index.js',
    vendor: VENOR
  },
  // 若是想修改 webpack-dev-server 配置,在這個對象裏面修改
  devServer: {
    port: 8081
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [{
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: [{
            loader: 'url-loader',
            options: {
                limit: 10000,
                name: 'images/[name].[hash:7].[ext]'
            }
        }]
    },
    {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [{
            // 這邊其實還可使用 postcss 先處理下 CSS 代碼
                loader: 'css-loader'
            }]
        })
    },
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor', 'manifest'],
      minChunks: Infinity
    }),
    new CleanWebpackPlugin(['dist/*.js'], {
      verbose: true,
      dry: false
    }),
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    // 生成全局變量
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV")
    }),
    // 分離 CSS 代碼
    new ExtractTextPlugin("css/[name].[contenthash].css"),
    // 壓縮提取出的 CSS,並解決ExtractTextPlugin分離出的 JS 重複問題
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),
    // 壓縮 JS 代碼
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
};

複製代碼

修改 packet.json 文件

"scripts": {
    "build": "NODE_ENV=production webpack -p",
    "dev": "webpack-dev-server --open"
  }
複製代碼

執行 npm run build

能夠看到咱們在經歷了這麼多步之後,將 bundle 縮小到了只有 27.1KB,像 vendor 這種經常使用的庫咱們通常可使用 CDN 的方式外鏈進來。

補充

webpack 配置上有些實用的小點在上文沒有提到,統一在這裏提一下。

module.exports = {
  resolve: {
  // 文件擴展名,寫明之後就不須要每一個文件寫後綴
    extensions: ['.js', '.css', '.json'],
 // 路徑別名,好比這裏可使用 css 指向 static/css 路徑
    alias: {
      '@': resolve('src'),
      'css': resolve('static/css')
    }
  },
  // 生成 source-map,用於打斷點,這裏有好幾個選項
  devtool: '#cheap-module-eval-source-map',
}
複製代碼

後記

若是你是跟着本文一個個步驟敲下來的,那麼大部分的 webpack 配置你應該都是能夠看懂了,而且本身應該也知道如何去配置。謝謝你們看到這裏,這是本項目對應的倉庫,每一個小節基本都對應了一次 commit。

文章較長,有錯誤也不免,若是你發現了任何問題或者我有任何表述的不明白的地方,均可以留言給我。

相關文章
相關標籤/搜索