1. 程式人生 > >可能是最小白的Webpack入門教程

可能是最小白的Webpack入門教程

寫過 React ,用的是 create-react-app ,寫過 Vue ,用的是 vue-cli , 第一次想了解一下 Webpack 。

其實根本沒有好好用過 Webpack ,所以邊寫邊查邊錯邊改,最終實現了一個比較簡單的demo。名字起的入門指南,其實只是一個小白的學習筆記…吧。所以路過的 dalao 有時間煩請指點一下。

我的環境 Mac OS, node: v8.11.1, npm: 5.6.0, Webpack: 3.12.0

 

0. 什麼是Webpack

我就不說亂七八糟的術語了,就是把很多的 JS 檔案打包到一個檔案(當然也可能不止一個)的工具,方便我們寫模組化的 JS 程式碼。而通過一些 plugin 和 loader 可能提供一些其他有用的功能以及處理其他格式的檔案。

1. 簡單的應用

先建立一個資料夾,在終端執行命令  npm init 來建立一個  package.json 檔案,這個檔案用來描述專案資訊,隨便填或者一直回車就可以。

package.json

{
  "name": "webpack-study-note-1",
  "version": "1.0.0",
  "description": "webpack學習筆記",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  
"repository": "https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes", "author": "G-lory", "license": "ISC" }

 

先建一個 src 資料夾用來 js 原始檔。建立兩個 js 檔案。

// index.js
const foo = require('./others.js');

let app = document.getElementById('app');
for (let i = 0; i < 10; i++) {
    let p 
= document.createElement('p'); p.innerText = foo(i); app.appendChild(p); } // others.js function foo(idx) { return `the ${idx + 1}th row`; } module.exports = foo;

並在根目錄建立 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>webpack study notes</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

目錄結構是這樣的:

├── src │ ├── index.js │ └── others.js ├── index.html └── package.json

如果想在 index.html 引用所有的js檔案,就要通過<srcipt>標籤將js檔案全部匯入,而且還要注意順序。現在通過 module.exports 和 require 在JS中引用,然後把這些檔案打包成一個檔案,那麼 index.html 直接引用最終的那個 js 檔案就可以了。

首先安裝 Webpack 。這裡使用 Webpack3 版本3到4有很多變化,如果你用的4,基本就不用看下去了。

安裝命令: npm install [email protected]3 --save-dev 

其中 install 可簡寫為 i , --save-dev 可簡寫為 -D,表示僅在開發環境依賴,會在package.json的 devDependencies 欄位記錄 。相對的是 --save 表示執行時依賴,簡寫為 -S, 會在package.json的 dependencies 欄位記錄。@3 表示指定安裝版本。

專案下會生成一個 node_modules 資料夾。裡面是安裝的依賴包。不用去管這個資料夾。

然後在根目錄下建立 webpack.config.js 檔案。這是webpack預設的配置檔名。這個檔案其實就是一個普通的 js 指令碼檔案,可以通過require引用一些模組,最後匯出配置物件。

// webpack.config.js
var path = require('path'); // node 內建模組

module.exports = {
    entry: './src/index.js', // 入口檔案 相當於 entry: { main: './src/index.js' }

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    }
}

一個最簡單的配置檔案,指定了輸入輸出。輸入和輸出都可以指定多個,這裡暫時用不到。

在 package.json 中新增打包命令

"scripts": {
    "build": "webpack"
},

然後在命令列執行  npm run build 就可以進行打包了,會生成檔案 /dist/bundle.js 。

開啟檔案可以看到,前面是 webpack 生成的一些程式碼,後面就是 index.js 和 others.js 中的程式碼。

然後在 index.html 中引用檔案

<script src="dist/bundle.js"></script>

在瀏覽器開啟 index.html 檔案 正常執行。

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step1

2. 使用 loader 和 plugin

我的個人感覺,loader就是處理檔案的,先使用loader將檔案轉換成想要的樣子,比如Webpack預設不能處理的圖片要先使用file-loader處理,es6先使用babel-loder處理成es5防止瀏覽器不相容等等。

而 plugin 可以做一些其他的神奇而且很有用的事情(我在說什麼……

之前的程式碼使用的ES6,現在就嘗試下把它轉換成ES5,需要使用 babel-loader。

安裝:

npm install [email protected] babel-core babel-preset-env -D

注意這裡為了和webapck3相容,需要指定 babel-loader 版本。

然後修改 webpack 配置檔案。這裡 babel 的配置含義可見 https://segmentfault.com/a/1190000008159877

var path = require('path');

module.exports = {
    entry: './src/index.js', // 入口

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: { // 配置loader
        rules: [
            {
                test: /\.jsx?/,             // 正則表示式 匹配檔名
                exclude: /node_modules/,    // exclude 表示排除的路徑 也可以新增 include 欄位設定匹配路徑
                use: {
                    loader: 'babel-loader', // 對符合上面約束條件的檔案 使用的 loader
                    options: {
                        presets: ['env']
                    }
                }
            }
        ]
    }
}

babel 預設是不進行轉換的,需要設定外掛,這裡通過 presets 設定外掛指定程式碼的轉換規則。

再次執行 npm run build 可以看到 bundle.js 中 let 都變成了 var。說明 babel 生效了。

接下來在再試一下 Promise。

修改 JS 程式碼

//index.js
const foo = require('./others.js');

let app = document.getElementById('app');
for (let i = 0; i < 10; i++) {
    let p = document.createElement('p');
    foo(i).then(content => {
        p.innerText = content;
        app.appendChild(p);
    })
}
// others.js
function foo(idx) {
    return new Promise(function (resolve, reject) {
        resolve(`the ${idx + 1}th row`);
    })
}

module.exports = foo;

打包後發現 Promise 相關帶並沒有進行處理。原來上面的配置只能轉換ES的新語法,對於新的API(Promise、Set、Map 等新增物件,Object.assign、Object.entries等靜態方法。)卻沒有作用。

有兩種方式解決這個問題,babel-polyfill 或 babel-runtime,前者預設全部載入,後者是按需載入。這麼說好像有錯....可以閱讀 https://juejin.im/post/5a96859a6fb9a063523e2591

安裝:

npm install babel-plugin-transform-runtime -D

然後修改 babel 配置

{
    test: /\.js$/,              // 正則表示式 匹配檔名
    exclude: /node_modules/,    // exclude 表示排除的路徑 也可以新增 include 欄位設定匹配路徑
    use: {
        loader: 'babel-loader', // 對符合上面約束條件的檔案 使用的 loader
        options: {
            presets: ['env'],
            plugins: ['transform-runtime']
        }
    }
}

現在再打包試一下會發現 bundle.js 檔案的體積大了一些 那是多了 Promise 的 polyfill,開啟 bundle.js 能看到相關程式碼。

 

上面是 loader 的使用,再試一下 plugin 的使用。

html-webpack-plugin 可以生成一個 html 檔案,把生成的 js 檔案自動注入其中。

安裝

npm install html-webpack-plugin -D

在配置檔案中新增 plugins 欄位

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成檔名
            template: 'index.html'  // 模板
        })
    ]
}

現在可以把 /index.js 中引入 js 的語句刪除了,然後重新打包。

可以看到dist資料夾生成了一個 index.html 檔案,該檔案中引入了 js。

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step2

3. 使用 Webpack 打包 React

react 使用的是 jsx 語法,需要用 babel 將 jsx 轉換成 js。

首先安裝 React

npm install react react-dom --save

然後安裝 babel 轉換 react 檔案的外掛

npm i babel-preset-react -D

在 src 資料夾的檔案改為下面幾個檔案:

index.jsx

// index.jsx
import React from 'react';
import { render } from 'react-dom';
import Input from './input';
import List from './list';

class App extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            list: []
        };
    }

    addItem(item) {
        this.setState({
            list: this.state.list.concat(item)
        })
    }

    removeItem(idx) {
        this.setState({
            list: this.state.list.filter((it, id) => id !== idx)
        })
    }


    render() {
        return(
          <div className='todoList'>
              <Input handleSubmit={this.addItem.bind(this)} />
              <List list={this.state.list} handleRemove={this.removeItem.bind(this)} />
          </div>
        )
    }
}

render(<App />, document.getElementById('app'));

input.jsx

// input.jsx
import React, { Component } from 'react';

class Input extends Component {
    constructor(props) {
        super(props);
        this.state = {
            content: ''
        };
    }
    submit() {
        if (this.state.content === '') return ;
        // 提交資料並清空
        this.props.handleSubmit(this.state.content);
        this.setState({
            content: ''
        })
    }
    handleChange(e) {
        this.setState({
            content: e.target.value
        })
    }
    render() {
        return (
          <div className='input'>
              <p>
                    <textarea
                      value={this.state.content}
                      onChange={this.handleChange.bind(this)}

                    >
                    </textarea>
              </p>
              <p className='btn'>
                  <button onClick={this.submit.bind(this)}>提交</button>
              </p>
          </div>
        )
    }
}

export default Input;

list.jsx

// list.jsx
import React, { Component } from 'react';

class List extends Component {
    render() {
        return (
          <div>
              {
                  this.props.list.map((item, idx) =>
                    <div className='listItem' key={idx}>
                        <span>{item}</span>
                        <button onClick={() => this.props.handleRemove(idx)}>刪除</button>
                    </div>
                  )
              }
          </div>
        )
    }
}

export default List;

然後修改 webpack.config.js

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.jsx', // 入口

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: { // 配置loader
        rules: [
            {
                test: /\.jsx?/,             // 正則表示式 匹配檔名
                exclude: /node_modules/,    // exclude 表示排除的路徑 也可以新增 include 欄位設定匹配路徑
                use: {
                    loader: 'babel-loader', // 對符合上面約束條件的檔案 使用的 loader
                    options: {
                        presets: ['env', 'react'],
                        plugins: ['transform-runtime']
                    }
                }
            }
        ]
    },

    resolve: { // 程式碼模組路徑解析的配置
        extensions: ['.js', '.jsx'] // 進行模組路徑解析時,webpack 會嘗試補全字尾名來進行查詢
    },

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成檔名
            template: 'index.html'  // 模板
        })
    ]
}

這次添加了欄位 resolve.extensions 注意到 index.jsx 引用檔案時沒有新增檔案字尾,因為通過 resolve.extensions 的配置 Webpack 會嘗試補全指定字尾來查詢。嘗試補全的順序是陣列中元素的順序。

打包後開啟 dist/index.html 檔案,可以看到一個雖然很醜但是能正常執行的頁面。

開啟 dist/bundle.js 可以發現檔案的長度達到了 2w+ 行。那是因為我們把 react 也打包進來了。

react 是我們直接引入的程式碼,裡面的內容很少會更改,而我們自己寫頁面會經常變化,所以為了充分利用頁面快取,希望把 node_modules 中的程式碼單獨打包成一個 js 檔案。

修改 webpack.config.js

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');

module.exports = {
    ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成檔名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口作為公共部分
            filename: "vendor.js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        })
    ]
}

現在再次打包會出現 dist 下面會出現三個檔案 而 bundle.js 中只有幾百行程式碼了。

我們也可以給檔名新增 hash 防止瀏覽器快取。

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');

module.exports = {
    ...

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].[chunkhash].js',
    },

    ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成檔名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口作為公共部分
            filename: "js/[name].[chunkhash].js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        })
    ]
}

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step3

4. 使用 webpack-dev-server 搭建本地環境

因為打包會花費很長時間 尤其是檔案多的時候。我們開發時需要獲取及時反饋,而不能每次打包後觀察錯對。

使用 webpack-dev-server 可以很簡單的啟動一個本地靜態服務。

安裝

npm i [email protected] -D

為了配合 Webpack3 需要指定版本。

然後在 package.json 新增指令碼命令 start 

"scripts": {
    "start": "webpack-dev-server",
    "build": "webpack"
},

然後執行  npm run start  會預設在 http://localhost:8080/ 啟動一個伺服器 開啟之後和之前打包的頁面是一樣的 嘗試修改檔案 會發現頁面會實時變化。

可以在配置檔案新增 devServer 欄位配置 webpack-dev-server 的選項,比如下面配置開啟地址和埠號

devServer: {
    host: 'localhost',
    port: 8888,
    open: true // 自動開啟瀏覽器
}

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step4

5. 使用 css 和 less

上面完成的頁面很醜,因為還沒有加入樣式。沒用過less,但是sass-node那個包實在是很麻煩,還是選擇了less,畢竟只是個demo。

不管是less還是css,Webpack都不認識,需要加入loader來處理。

安裝

npm i less less-loader [email protected] style-loader -D

然後按照官網的提示,在配置檔案的 rules 新增程式碼

module: { // 配置loader
    rules: [
        {
            test: /\.jsx?/,             // 正則表示式 匹配檔名
            exclude: /node_modules/,    // exclude 表示排除的路徑 也可以新增 include 欄位設定匹配路徑
            use: {
                loader: 'babel-loader', // 對符合上面約束條件的檔案 使用的 loader
                options: {
                    presets: ['env', 'react'],
                    plugins: ['transform-runtime']
                }
            }
        },
        {
            test: /\.less$/,
            include: [
                path.resolve(__dirname, 'src')
            ],
            use: [{
                loader: 'style-loader' // creates style nodes from JS strings
            }, {
                loader: 'css-loader' // translates CSS into CommonJS
            }, {
                loader: 'less-loader' // compiles Less to CSS
            }]
        }
    ]
},

然後新增樣式檔案

/* index.less */
* {
  margin: 0;
  padding: 0;
}

.todoList {
  padding: 10px 50px;
}

/* input.less */
.input {
  margin-bottom: 10px;
  textarea {
    width: calc(100% - 10px);
    height: 50px;
    color: #9c9c9c;
    border-radius: 5px;
    resize: none;
    outline: none;
    padding: 5px;
    margin-bottom: 5px;
  }
  .btn {
    text-align: right;
    button {
      border: none;
      outline: none;
      background-color: transparent;
    }
  }
}

/* list.less */
.listItem {
  height: 40px;
  line-height: 40px;
  border: 1px solid #d6d6d6;
  display: flex;
  margin-bottom: 10px;
  padding: 10px;

  span {
    flex-grow: 1;
    color: #9c9c9c;
  }

  button {
    border: none;
    outline: none;
    background-color: transparent;
  }
}

然後再每個檔案分別引用就可以。

import './index.less'; /* index.jsx */
import './input.less'; /* input.jsx */
import './list.less';  /* list.jsx */

檢視頁面,樣式已經生效,但是這樣的問題是,所有的樣式都是全域性樣式,容易發生命名衝突的情況,css模組化可以解決這個問題。

css-loader 有一個 modules 可配置項,表示是否模組化,配置改為:

{
    test: /\.less$/,
    include: [
        path.resolve(__dirname, 'src')
    ],
    use: [{
        loader: 'style-loader' // creates style nodes from JS strings
    }, {
        loader: 'css-loader', // translates CSS into CommonJS
        options: {
            modules: true,
            localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [檔名]__[類名]___[雜湊]
        }
    }, {
        loader: 'less-loader' // compiles Less to CSS
    }]
}

現在可以使用模組化樣式了。

現在引用類是需要醬紫

import styles from './index.less';
....
render() {
    return(
      <div className={styles.todoList}>
          <Input handleSubmit={this.addItem.bind(this)} />
          <List list={this.state.list} handleRemove={this.removeItem.bind(this)} />
      </div>
    )
}

而全域性樣式需要醬紫:

:global(*) {
  margin: 0;
  padding: 0;
}

 

之前處於快取的考慮,把 node_modules 單獨打包,現在出於同樣的考慮,需要把 css 也單獨打包。

之前的 loader 是把 css 轉成了 js 程式碼,而把 css 單獨打包成一個檔案,需要使用 ExtractTextPlugin。

安裝:

npm install extract-text-webpack-plugin -D

修改配置檔案

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    // ...
    module: { // 配置loader
        rules: [
            // ...
            {
                test: /\.less$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [{
                        loader: 'css-loader', // translates CSS into CommonJS
                        options: {
                            modules: true,
                            localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [檔名]__[類名]___[雜湊]
                        }
                    }, {
                        loader: 'less-loader' // compiles Less to CSS
                    }]
                })
            },
        ]
    },

    // ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成檔名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口作為公共部分
            filename: "js/[name].[chunkhash].js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        }),
        new ExtractTextPlugin('css/[name].[contenthash].css')
    ]
}

再次打包,現在CSS檔案也單獨分離出來了。

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step5

6. 圖片和雪碧圖

每個正經的前端應該都知道雪碧圖是什麼吧,反正我不知道……我還以為是瀑布流什麼的神奇效果……

通過 Webpack 的外掛,可以自動把引入的圖片生成雪碧圖。也可以用 url-loader 來處理圖片,這裡沒有選擇使用。

首先 Webpack 不識別圖片型別的檔案 要引入 file-loader ,同時引入 webpack-spritesmith 用來生成雪碧圖。

安裝

npm install file-loader webpack-spritesmith -D

我找了兩個圖片

delete.png 和  submit.png 放到 /images 資料夾下面

處理圖片要新增 loader

{
    test: /\.(png|jpg|gif)$/,
    use: [
        {
            loader: 'file-loader',
            options: {}
        }
    ]
}

生成雪碧圖新增 plugins

var SpritesmithPlugin = require('webpack-spritesmith');

new SpritesmithPlugin({
    src: {
        cwd: path.resolve(__dirname, 'images'), // 多個圖片所在的目錄
        glob: '*.png' // 匹配圖片的路徑
    },
    target: {
        // 生成最終圖片的路徑
        image: path.resolve(__dirname, 'src/spritesmith-generated/sprite.png'),
        // 生成所需 less 程式碼
        css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.less'),
    },
    apiOptions: {
        cssImageRef: "~sprite.png"
    }
})

現在打包的時候會在 /src/spritesmith-generated 生成雪碧圖和所需的 less 程式碼

生成的雪碧圖

嘗試使用,修改 input.less 和 list.less

input.less

/* input.less */
@import './spritesmith-generated/sprite.less';

.input {
   /* ignore.. */
  .btn {
    text-align: right;
    button {
      .sprite(@submit);
      border: none;
      outline: none;
      background-color: transparent;
    }
  }
}

list.less

@import './spritesmith-generated/sprite.less';

.listItem {
  /* ignore.. */

  button {
    .sprite(@delete);
    border: none;
    outline: none;
    background-color: transparent;
  }
}

然後啟動專案會發現報錯了……

雖然也沒看明白什麼意思吧……反正就是在 less-loader 新增配置項  javascriptEnabled: true 

然後打包發現路徑不對查了下發現需要給 ExtractTextPlugin 配置 publicPath

最後該規則改為

{
    test: /\.less$/,
    include: [
        path.resolve(__dirname, 'src'),
    ],
    use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [{
            loader: 'css-loader', // translates CSS into CommonJS
            options: {
                modules: true,
                localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [檔名]__[類名]___[雜湊]
            }
        }, {
            loader: 'less-loader', // compiles Less to CSS
            options: {
                javascriptEnabled: true
            }
        }],
        publicPath: "../"
    })
},

但是還是有報錯

這個我是真的不知道怎麼解決,只是發現去掉 css module 就沒有這個問題了,於是我刪掉了 css module 部分……(配合標題 我TM是真的菜

然後就可以正常打包了。一個 To Do List 就勉強做好了……

完整程式碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step6

7. 總結

我還是老老實實的用  create-react-app 和 vue-cli 吧……

8. 參考資料

每一個用到的 loader 和 plugin 的 GitHub 都會參考到 就不寫了。