[前端工具]Webpack2 手把手一起入門

[前端工具]Webpack2 手把手一起入門

前言

其實筆者一直都有在學習使用webpack,不過一直都沒有摸得很深,除了前陣子要做react的demo有從無到有自己整個做出來之外,其他的狀況之下,angularjs筆者使用的是gulp做整合,vue and angular4都有cli,不過其實真的要用在大專案或多人協作的狀況下,客製化的狀況就會越來越多,所以乾脆下定決心,把整個webpack 2摸透徹一點,一切從入門開始,並且把這些心得筆記下來,希望同樣會幫助到一些不太懂webpack 2的人,但其實webpack 2的細節非常的多,所以一篇文章想要把較常用的全部解釋完畢,可能連筆者自己以後回頭看都沒耐心看完了,所以打算分幾篇來解釋webpack2,因為網路上google到的大部份都是webpack1的文章,所以很多照著參考做出來都已經跑不起來了,所以希望這次紀錄下來的webpack2會幫助到多數的人。

導覽

  1. 安裝相關的工具
  2. 最基本的配置
  3. 各種編譯的方式
  4. 用loader來編譯es6或scss
  5. devtool來幫助偵錯
  6. 使用plugins來擴充一些功能
  7. 使用NODE_DEV來切換開發和佈署
  8. 結論

安裝相關的工具

在此筆者不想多介紹npm了,如果連npm都不懂的話,這篇可能不是那麼適合你,就把webpack的整合交給其他人做,或者先了解一下npm怎麼使用和安裝,再回來看一下怎麼學習webpack,其實webpack本身就是node.js和純javascript的整合,包括了一些webpack的plugin配置,首先開啟一個新專案,打上npm init建立預設的package.json,接著就安裝webpack,還有live-server以方便把網站跑起來。

npm install --g live-server
npm install --save-dev webpack

安裝完之後應該可以看到package.json,在devDependencies就安裝了webpack了,到這邊我們其實就已經安裝成功了,接著新增一支預設的index.html,還有script資料夾和webpack.config.js,最後筆者的目錄會是長這個樣子的

最基本的配置

先看一下webpack.config.js最基本的部份

const path = require('path'), //node.js提供的
  webpack = require('webpack'); 

const webpackConfig = {
  entry: {//進入點
    app: [
      './src/index.js',//打包後會是app.js
    ]
  },
  output: {//輸出
    path: path.join(__dirname, 'dist'),//絕對路徑,輸出為dist目錄
    filename: '[name].js',//輸出的就是上面的app檔名
    publicPath: '/' //使用在html或url-loader會參考的圖片的路徑
  },
  resolve: {
    extensions: ['.js'] //import的時候,就可以忽略js了
  }
}

module.exports = webpackConfig

其實幾乎所有的webpack都是這樣子寫的,但是其實我們最後webpack會吃的是這個module,在最下面會export出去,所以這樣子的結構我們可以完全拆開來,最後輸出一個對象就可以了,比如下面的格式

const path = require('path'), //node.js提供的
  webpack = require('webpack');

const webpackConfig = {
  entry: {},
  output: {},
  resolve: {}
}

webpackConfig.entry = {//進入點
  app: [
    './src/index.js',//打包後會是app.js
  ]
}

webpackConfig.output = {//輸出
  path: path.join(__dirname, 'dist'),//絕對路徑,輸出為dist目錄
  filename: '[name].js',//輸出的就是上面的app檔名
  publicPath: '/' //會參考比如圖片的路徑
}

webpackConfig.resolve = {
  extensions: ['.js'] //import的時候,就可以忽略js了
}

module.exports = webpackConfig

當然我們也可以在最後針對這個物件的值做任何變化

const path = require('path'), 
  webpack = require('webpack');

const webpackConfig = {
  entry: {
    app: [
      './src/index.js',
    ]
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js'] 
  }
}

webpackConfig.entry.app.push('./src/index1.js') //針對entry多新增一支js檔,一起打包

module.exports = webpackConfig

接著看一下index.html的部份

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <script src="./dist/app.js"></script> 
</body>

</html>

js部份

index1.js
----------------
export default {
  print() {
    console.log('print')
  }
}


index.js
----------------
import index1 from './index1'

index1.print();

console.log('index');

然後我們可以在package.json先加上一個監控webpack的動作

"scripts": {
    "dev": "webpack --watch"
}

接著可以在cmd打npm run dev執行完成之後,再輸入live-server打開網頁,主控台應該就可以看到起手式成功了。

各種打包的方式

大致上有三種打包的方式,一種就是spa的方式,監聽入口點,然後其餘的有import的話就會自動依賴進來。

  entry: {
    app: [
      './src/index.js',
    ]
  }

另一種方式則是把所有檔案打包成一份,比如說我們想要想要打包後,放在現有的專案共用layout裡面,這也是我們比較常看到的bundle方式,但是我們使用webpack的方式,就可以使用es6或typescript的方式來開發和打包,

  entry: {
    app: [
      './src/index.js',
      './src/home.js',
      './src/index-outside.js'
    ]
  }

最後一種就是我們每個頁面都希望有自己獨立的js檔,這樣子我們就可以在每個頁面只放進此頁面需要使用到的js檔,而此使用情境一樣是可以使用在現有的網頁裡面。

  entry: {
    app: './src/index.js',
    home: './src/index1.js',
    indexOutside: './src/index-outside.js'
  }

從上面三個例子應該可以明顯看得出來,其實你只要把資料結構對應好,webpack就會按照你設定的結構做打包。

用loader來編譯es6或scss

當我們要使用es6的時候,我們就必須要使用babel了,但是因為babel也是一個很大的議題,所以之後等筆者有時間深入研究了,再來紀錄此類的相關議題,先安裝一下關於babel一些相關的package

npm i babel-core babel-loader babel-plugin-transform-runtime babel-preset-env babel-preset-stage-2 --D

接著新增.babelrc的檔案,這個是屬於babel的配置。

{
  "presets": [
    ["env", { "modules": false }],
    "stage-2"
  ],
  "plugins": ["transform-runtime"],
  "comments": false
}

接著就是在webpack.config.js的部份,加入modules和loader的設定

const path = require('path'),
  webpack = require('webpack');

const webpackConfig = {
  entry: {
    app: './src/index.js',
    home: './src/index1.js',
    indexOutside: './src/index-outside.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js'],
  },
  module: { //新增的modules部份,載入的js檔就會經過babel轉譯了
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.join(__dirname,'src')]
      }
    ]
  }
}


module.exports = webpackConfig

如果想真的測試babel是否有生效的話,可以測試一些chrome還不支援的語言特性,比如spread,或者是async await的部份,如果我們想要在js裡面可以import css或scss的話,也就是都要下載相關的loader。

npm install css-loader style-loader sass-loader node-sass --D

webpack.config的部份

  module: { 
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.join(__dirname, 'src')]
      },
      {//新增的css部份
        test: /\.css$/,
        use: [
          { loader: "style-loader" },
          { loader: "css-loader" }
        ]
      }
    ]
  }

devtool來幫助偵錯

其實devtool也就是在此設定sourcemap,在webpack有很多種sourcemap的方式,筆者是直接參考vue的方式,開發階段可以使用"#cheap-module-eval-source-map",正式佈署打包壓縮的時候,使用的則是最完整的"#source-map",設定方式也很簡單,可以見如下的webpack設置

 devtool: '#cheap-module-eval-source-map',

使用plugins來擴充一些功能

在webpack的plugins有非常多的plugins可以安裝並使用,在此介紹一下uglifyjs-webpack-plugin的部份,就是壓縮js的部份,可以再module的底下再加上plugins的部份

  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
        drop_console: true,
      },
      sourceMap: true
    })
  ]

這樣子當我們執行webpack的時候,就會壓縮並幫我們加上sourcemap,以方便偵錯了,可以注意我下面的操作,app.js就是壓縮後的檔案,另外可以切換去webpack看最原始的程式碼以便抓問題。

使用NODE_DEV來切換開發和佈署

如果有去看過webpack相關文章或cli的設定的人,想必也會常看到process.env.NODE_ENV,這個參數其實我們是可以去設定的,一個比較簡單的環境可能有開發和產品環境,但更複雜的公司光是從開發要佈署到產品階段,至少就會有三個甚至更多的環境,比如說dev->qat->production這個流程,每個階段的設定可能都不太一樣,以目前這篇的範例來說,我是在開發階段就已經加上uglifyjs了,但是我希望的卻是只有要發佈到正式環境的時候,才會需要uglifyjs,而要設定NODE_DEV其實也很簡單,就是當我們要下cmd的指令同時去設定就可以了,比如說當我想使用webpack --watch的時候,是要跑dev的環境,但是當我們跑webpack的時候,想要跑的是prod的環境,首先我們來修改一下package.json的檔案

{
  "name": "webpack-example",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "dev": "set NODE_ENV=dev && webpack --watch",
    "prod": "set NODE_ENV=prod && webpack"  
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/webpack": "^2.2.15",
    "babel-core": "^6.25.0",
    "babel-loader": "^7.0.0",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.5.2",
    "babel-preset-stage-2": "^6.24.1",
    "css-loader": "^0.28.4",
    "node-sass": "^4.5.3",
    "sass-loader": "^6.0.5",
    "style-loader": "^0.18.2",
    "webpack": "^2.6.1"
  }
}

webpack.config.js

接著我們就可以在webpack.config.js裡面判斷環境做出對應處理的事情

const path = require('path'),
  webpack = require('webpack');

const webpackConfig = {
  entry: {
    app: './src/index.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js'],
  },
  devtool: '',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.join(__dirname, 'src')]
      },
      {//新增的css部份
        test: /\.css$/,
        use: [
          { loader: "style-loader" },
          { loader: "css-loader" }
        ]
      },
      {
        test: /\.scss$/,
        use: [{
          loader: "style-loader"
        }, {
          loader: "css-loader"
        }, {
          loader: "sass-loader"
        }]
      }
    ]
  },
  plugins: []
};

switch (process.env.NODE_ENV.trim()) { //從這邊判斷環境,以做出相對應的webpack修改
  case "dev":
    webpackConfig.devtool = '#cheap-module-eval-source-map';
    break;
  case "prod":
    webpackConfig.devtool = '#source-map';
    webpackConfig.plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: true,
        },
        sourceMap: true
      })
    );
    break;
}

module.exports = webpackConfig

當我們要發佈的時候,我們通常都會希望把整個folder刪除掉,並且重新建立,否則如果我們的檔名有hash的話,就會一直產生新的檔案,即然我們已經安裝了npm,想當然的我們就也會有node.js的功能,我們也可以另外用npm來安裝node.js的功能,在node裡面有一個rimraf內建的package,我們就直接require,並直接使用就行了

const path = require('path'),
  rimraf = require('rimraf'),
  webpack = require('webpack');

const webpackConfig = {
  entry: {
    app: './src/index.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js'],
  },
  devtool: '',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.join(__dirname, 'src')]
      },
      {//新增的css部份
        test: /\.css$/,
        use: [
          { loader: "style-loader" },
          { loader: "css-loader" }
        ]
      },
      {
        test: /\.scss$/,
        use: [{
          loader: "style-loader"
        }, {
          loader: "css-loader"
        }, {
          loader: "sass-loader"
        }]
      }
    ]
  },
  plugins: []
};

switch (process.env.NODE_ENV.trim()) {
  case "dev":
    webpackConfig.devtool = '#cheap-module-eval-source-map';
    break;
  case "prod":
    rimraf(path.join(__dirname, 'dist'), () => console.log('success remove')); //當要發佈產品的時候,先把原有folder刪除
    webpackConfig.devtool = '#source-map';
    webpackConfig.plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: true,
        },
        sourceMap: true
      })
    );
    break;
}

module.exports = webpackConfig

結論

webpack的設置非常多,第三方的plugin和loader也非常的多,所以根本很難講得完,希望在接下來會有一系列的研究心得分享出來,但因為現有前端框架大部份都會有現成的cli使用,所以很多時候我們並不需要學得那麼深入,但是學得深入點對於我們要客製化,或者是舊有網站做打包處理的時候,都會很有幫助,如果這篇對您有任何幫助,或者覺得有任何錯誤的地方,再請給予指導。