/learn-webpack

Primary LanguageJavaScript

Learn-Webpack

此處主要 follow 入门Webpack,看这篇就够了 的實作練習(使用 Webpack 4.6),由於該篇距今已有段時間,因此過程中踩到許多坑,而這裡的也是記錄填坑的地方…


目前實作的功能

  • 將所有 js/css 打包成單一的 bundle.js(with Uglify, OccurrenceOrder, babel)
  • 也可將 css 抽成獨立的檔案
  • SCSS 轉換
  • 可以轉譯 pug 為 html
  • 可以利用 chunkhash 及 html-webpack-plugin 來注入hash 檔名及資源值

待處理的問題

  • webpack-dev-server 的 hot 功能尚無法正常運作(有跑但Runtime 模組沒更新)
  • css modules 置換原來就寫死的 class name

Install

yarn init
yarn add --dev webpack

Using(with command line):

  1. public/index.html: 先指定之後會用 Webpack 打包產生的 bundle.js
<body>
    <div id="root"></div>
    <script src="bundle.js"></script>
</body>
  1. app/Greeter.js: CommonJS
module.exports = () => {
    let greet = document.createElement("div");
    greet.textContent = "Hello, this is Greeter.js";
    return greet;
}
  1. app/main.js: CommonJS
const greeter = require("./Greeter.js");
document.getElementById("root").appendChild(greeter());
  1. Using Webpack: 4.6.0(In command line)

    • 語法:
    # {entry file}: 進入點檔案
    # {destination for bundle file}: 要打包輸出的檔案
    # 格式:webpack {entry file} {destination for bundle file}
    ..\node_modules\.bin\webpack app/main.js -o public/bundle.js
    
    ## 坑:
    # 如果只下:..\node_modules\.bin\webpack app/main.js public/bundle.js
    # >> ERROR in multi ./app/main.js public/bundle.js
    # Module not found: Error: Can't resolve 'public/bundle.js' in 'W:\_workspace\learn-webpack\learn-1'
    # @ multi ./app/main.js public/bundle.js
  2. Run public/index.html in browser >> "Hello, this is Greeter.js"

Using(with webpack.config.js):

  1. Add webpack.config.js to root of project
module.exports = {
    // __dirname 是 webpack 的全域變數:當前檔案的所在目錄
    // entry: 進入點檔案
    // output: 輸出的目標檔案
    entry: `${__dirname}/app/main.js`,
    output: {
        path: `${__dirname}/public`,
        filename: "bundle.js"
    }
}
  1. Run
..\node_modules\.bin\webpack
entry: {
    app: [
        './src/index.js',
    ]
}
* Bundle: 把不同頁面用的 js 都打包成一個檔
entry: {
    app: [
      './src/index.js',
      './src/home.js',
      './src/index-outside.js'
    ]
}
* 各頁面有獨自的 js 檔
entry: {
    app: './src/index.js',
    home: './src/index1.js',
    indexOutside: './src/index-outside.js'
}

Run webpack by npm start/npm run {custom name}

  • 注意:要使用此法必須將 webpack 卡在專案根目錄(和 package.json 同一層)
ERROR in Entry module not found: Error: Can't resolve './src' in 'W:\_workspace\learn-webpack'
  • "npm start" === "..\node_modules.bin\webpack"
{
    "name": "learn-webpack",
    "scripts": {
        "start": "webpack"
    },
}
  • "npm run wp" === "..\node_modules.bin\webpack"
{
    "name": "learn-webpack",
    "scripts": {
        "wp": "webpack"
    },
}

Source Map

  • 要用 Source Map 必須在 webpack.config.js 設定 devtool
module.exports = {
    // source map: source-map, cheap-module-source-map, eval-source-map, cheap-module-eval-source-map
    devtool: "eval-source-map",
};
  • 設定值:Devtool
    • source-map: 功能完全,但會降低打包速度。
    • cheap-module-source-map: 只能對應到程式碼的列號,無法對應該列中的實際行號,不利除錯,但速度快。
    • eval-source-map: 在原始碼檔案中產生對應資料,不影響打包速度,但安全性及執行時期效能較差,只適用於開發階段。
    • cheap-module-eval-source-map: 最快的打包速度,產生的 map 會和原檔同列顯示,但也和 eval-source-map 有相似的缺點。

Webpack Dev Server

yarn add --dev webpack-dev-server
  • Settings
// webpack.config.js
{
    devServer: {
        // root path of server, default is root of project
        contentBase: "./learn-1/public",
        // 對於 SPA,瀏覽器的 History 可以設成 HTML5 History API/Hash
        // 若設成 HTML5 History API,重整時會出現 404,因為它是以其它路徑來訪問後台
        // 此處設成 true,代表 404 都指向 index.html
        historyApiFallback: true,
        // watch
        inline: true,
        port: 28080
    }
}
  • Setting for run webpack-dev-server by yarn command
{
    "scripts": {
        "server": "webpack-dev-server"
    }
}
  • Run *
# 依 scripts 設定執行 
npm run server
# 附加執行參數
npm run server --open --hot --colors --progress --inline --config webpack.dev.js

Loaders

Babel

  • Install
# babel-preset-env: for ES6
# babel-preset-react: for JSX
yarn add --dev babel-core babel-loader babel-preset-env babel-preset-react
  • Setting
// webpack.config.js
{
    module: {
        rules: [{
            test: "/(\.js)$/",
            use: {
                loader: "babel-loader",
                options: {
                    presets: [
                        "env"
                    ]
                }
            },
            exclude: "/node_modules/"
        }]
    }
}
  • Adjust Code to ES6
  • Run:
npm run wp
# ES6 module must test in server
npm run server
  • Use .babelrc
// webpack.config.js
{
    module: {
        rules: [{
            test: "/(\.js)$/",
            use: {
                loader: "babel-loader"
            },
            exclude: "/node_modules/"
        }]
    }
}
// .babelrc
{
    "presets": ["env"]
}

CSS

# css-loader: @import 及 url(...) 實現 require 功能
# style-loader: 將 <style></style> 插入<head>
yarn add --dev style-loader css-loader
  • Adjust Code
// css-loader
// 解讀(右 > 左):將 ./main.css 用 css-loader 導入 js
//使用 import 導入 css 檔
import 'css-loader!./main.css';
//or
var css = require("css-loader!./main.css");

//==============================================
// style-loader
// 解讀(右 > 左):將 ./main.css 用 css-loader 導入 js,再由 style-loader 注入到 html.head
import 'style-loader!css-loader!./main.css';
  • Run: npm run wp, npm run server
  • Set up config of style-loader/css-loader in webpack.config.js: 如此在 import 時不用加 style-loader!css-loader!
// webpack.config.js
{
    module: {
        rules: [{
            test: /\.css$/,
            // 同時使用多個 loader 來解析 css
            // 順序:下(先用) -> 上(後用)
            use: [{
                    loader: "style-loader"
                }, {
                    loader: "css-loader"
                }]
        }]
    }
}

// or

{
    module: {
        rules: [{
            test: /\.css$/,
            //順序:右(先用) -> 左(後用)
            loaders: ['style-loader', 'css-loader']
        }]
    }
}

SCSS

yarn add --dev sass-loader node-sass
  • settup
// webpack.config.js
{
    module: {
        rules: [{
            test: /\.scss$/,
            //順序:右(先用) -> 左(後用)
            loaders: ['style-loader', 'css-loader', 'sass-loader']
        }]
    }
}

CSS Module: 類似 JS Module 思維,讓所有 CSS 的類別名、動畫名都預設只作用在當前模組下,避免全域污染。

{
    module: {
        rules: [{
            test: /\.css$/,
            use: [{
                    loader: "style-loader"
                }, {
                    loader: "css-loader",
                    options: {
                        // 啟用 css modules
                        modules: true,
                        // 指定 css 的類別名稱,預設為 import { className } from "./style.css" 的 className
                        // localIdentName: '[name]__[local]--[hash:base64:5]'
                    }
                }]
        }]
    }
}
  • Adjust Code:
// Greeter.js
import mainStyles from './main.scss';

export default function Greeter() {
    let greet = document.createElement("div");
    // mainStyles.root1
    greet.classList.add(mainStyles.root1);
    return greet;
}
  • 注意: css-modules 似乎只適用程式產生的 html 標籤的 class
<style type="text/css">
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%; }

/* ... */

/* 原本 .root 也被更名為 .main__root--3Q2VU,但 html 中還是 root */
.main__root--3Q2VU {
  color: yellowgreen; }

.main__root1--1tB5V {
  color: red; }
</style>
<div class="root">
    <div class="main__root1--1tB5V">Hello, this is Greeter.js</div>
</div>

PostCSS

yarn add --dev postcss-loader autoprefixer
  • setup
// webpack.config.js
{
    module: {
        rules: [{
            test: /(\.scss|\.css)$/,
            use: [{
                loader: "style-loader"
            }, {
                loader: "css-loader",
            }, {
                // 注意順序必須在 preCSS 後,css-loader 前
                loader: "postcss-loader",
                options: {
                    plugins: () => [require('autoprefixer')({
                        'browsers': ['> 1%', 'last 2 versions']
                    })],
                }
            }, {
                loader: "sass-loader"
            }]
        }]
    }
}
  • Setup2: with postcss.config.js
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')({
            'browsers': ['> 1%', 'last 2 versions']
        })
    ]
};

// webpack.config.js
{
    module: {
        rules: [{
            test: /(\.scss|\.css)$/,
            use: [{
                loader: "style-loader"
            }, {
                loader: "css-loader",
            }, {
                loader: "postcss-loader",
            }, {
                loader: "sass-loader"
            }]
        }]
    }
}

Pug

## yarn add --dev html-webpack-pug-plugin
yarn add --dev pug-loader

## Error: Cannot find module 'pug'
yarn add --dev pug
  • Setup
// webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    module: {
        rules: [{
                test: /(\.pug|\.jade)$/,
                use: {
                    loader: "pug-loader"
                },
                exclude: "/node_modules/"
            }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            /** Required **/
            // Inject style, script
            inject: true,
            template: `${__dirname}/learn-1/app/index.tmpl.pug`,

            /** Optional **/
            title: 'Custom template',
            filetype: 'pug'
        }),
    ],
}
  • Note:
    • 在 pug 中注入 html-webpack-plugin 變數,需用 =XXX

    title=htmlWebpackPlugin.options.title

    • 在 html 中注入 html-webpack-plugin 變數,需用 <%=XXX %>
    <title><%= htmlWebpackPlugin.options.title %></title>

Plugins

  • Plugins vs. Loaders

    • Loaders: 在打包過程中對來源檔案進行處理,一次處理一個。
    • Plugins: 用來擴展 webpack 功能,直接對整個專案建構過程作用,並不直接處理單個檔案。
  • 使用方式:

    1. npm install
    2. 在 webpack.config.js 中的 plugins 進行設定

BannerPlugin: 版權宣告

  • Setup
// webpack.config.js
const webpack = require('webpack');

module.exports = {
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究!')
    ],
}
  • Result
// bundle.js
/*! 版权所有,翻版必究! */......
  • Description

    • 可根據自訂好的模板,為每個 entry 建立 html 檔
  • Install

yarn add --dev html-webpack-plugin
  • Add Html Template
<!-- app/index.tmpl.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>Webpack Learn-1 - template</title>
</head>

<body>
    <div class="root">
    </div>
</body>

</html>
  • Setup
// webpack.config.js
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究!'),
        new htmlWebpackPlugin({
            template: `${__dirname}/learn-1/app/index.tmpl.html`
        })
    ]
}
  • Run: 執行 npm run wp 後會在 public/ 下自動建立包含 bundle.js script 的 index.html

  • Custom title:

    1. Template
    <!-- app/index.tmpl.html -->
    <title>
        <%= htmlWebpackPlugin.options.title %>
    </title>
    1. Setup
    // webpack.config.js
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究!'),
        new htmlWebpackPlugin({
            title: 'Custom template',
            template: `${__dirname}/learn-1/app/index.tmpl.html`
        })
    ]
  • ref:

  • 用途:在修改程式碼後,自動刷新

  • Method 1(config)

    1. 啟用 webpack-dev-server hot
    2. 加入 webpack.HotModuleReplacementPlugin
    3. 加入 webpack.NamedModulesPlugin: make it easier to see which dependencies are being patched.
  • Method 2 (no config & cli): 只要 webpack-dev-server --hot --hot-only 就會自動引用 HotModuleReplacementPlugin plugins

// webpack.config.js
const webpack = require('webpack');

module.exports = {
    devServer: {
        hot: true,
        hotOnly: true
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ]
}
  • 待解決問題
    • hot 有跑,但沒作用,步驟:
      1. 設定
// webpack.config.js
const webpack = require('webpack');

module.exports = {
    devServer: {
        hot: true,
        hotOnly: true
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ]
}
// app/PrintMe.js
export default function printMe() {
    console.log("printMe.js init");
}
// app/main.js
if (module.hot) {
    module.hot.accept('./PrintMe.js', function() {
        console.log('Accepting the updated printMe module!');
        printMe();
    })
}
    3. npm run server
[HMR] Waiting for update signal from WDS...
bundle.js:1 [WDS] Hot Module Replacement enabled.
printMe.js init
    4. change PrintMe.js
export default function printMe() {
    console.log("printMe.js modified...");
}
    5. Browser Log: 仍然印出「bundle.js:1 printMe.js init」,而不是「printMe.js modified...」
[WDS] App hot update...
bundle.js:1 [HMR] Checking for updates on the server...
bundle.js:1 Accepting the updated printMe module!
bundle.js:1 printMe.js init
bundle.js:1 [HMR] Updated modules:
bundle.js:1 [HMR]  - ./learn-1/app/PrintMe.js
bundle.js:1 [HMR] App is up to date.
    6. ref:
        * [webpack-dev-sever HMR do not works, only working full reload behavior](https://github.com/webpack/webpack-dev-server/issues/1315)

壓縮 JS

  • install
yarn add --dev uglifyjs-webpack-plugin
  • Settup:
// webpack.prod.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(common, {
    plugins: [
        new UglifyJsPlugin(),
    ],
});

OccurrenceOrderPlugin

分析 id 使用的頻率,讓使用頻率高的模組用較短的 id

  • Settup:
// webpack.prod.js
module.exports = merge(common, {
    plugins: [
        new webpack.optimize.OccurrenceOrderPlugin(),
    ],
});

Extract text from a bundle, or bundles, into a separate file.

  • install
yarn add --dev extract-text-webpack-plugin
* webpack 4+ 相容性問題:[Webpack 4 compatibility](https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/701)
## for webpack 4+
yarn add --dev extract-text-webpack-plugin@next
  • Setup for extract CSS from bundle
// webpack.common.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
    module: {
        rules: [{
            test: /(\.scss|\.css)$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true,
                        localIdentName: '[name]__[local]--[hash:base64:5]',
                        url: false,
                        minimize: true,
                        sourceMap: true
                    }
                }, {
                    loader: "postcss-loader",
                    options: {
                        sourceMap: true
                    }
                }, {
                    loader: "sass-loader",
                    options: {
                        sourceMap: true
                    }
                }]
            })
        }]
    },
    plugins: [
        new ExtractTextPlugin("styles.css"),
    ],
}

Caching & ChunkHash

為檔名附加 Hash,以解決檔名一樣時,瀏覽器誤以為檔案沒更新

  • ref:

  • Setup

    • 若有使用 htmlWebpackPlugin 來產生 html,會自動補齊 chunkhash 檔名。
    • 由於會影響打包速度,所以只有在 prod 時才使用 chunkhash 檔名
    • 調整:
      • 在 webpack.prod.js 設定 output > filename 來覆蓋掉在 webpack.common.js 的設定
      • 在 webpack.prod.js 及 webpack.dev.js 分別設定 plugins > new ExtractTextPlugin("styles-[chunkhash].css"),並將 webpack.common.js 中的移除。
        • 需要個別設定是因為 webpack.merge 合併後,new ExtractTextPlugin() 並不是附蓋,所以若只在 common 及 prod 引用時,在 build prod 時會同時產生兩個檔案。
        • module 中的 use: ExtractTextPlugin.extract() 仍保留在 common 共用。
// webpack.prod.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = merge(common, {
    output: {
        filename: "bundle-[chunkhash].js"
    },
    plugins: [
        new ExtractTextPlugin("styles-[chunkhash].css"),
    ],
}

// webpack.dev.js
// 沒設定 output > filename 會沿用 common 的設定
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = merge(common, {
    plugins: [
        new ExtractTextPlugin("styles.css"),
    ],
}

remove/clean your build folder(s) before building

yarn add --dev clean-webpack-plugin
  • Setup
// webpack.common.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");

// the path(s) that should be cleaned
const pathsToClean = [
    'learn-1/public', // removes 'learn-1/public' folder
    // 'build/*.*', // removes all files in 'build' folder
    // 'web/*.js' // removes all JavaScript files in 'web' folder
];
// the clean options to use
let cleanOptions = {
    // Absolute path to your webpack root folder (paths appended to this)
    // Default: root of your package
    root: __dirname,

    // exclude: ['shared.js'],

    // Write logs to console.
    verbose: true,

    // Use boolean "true" to test/emulate delete. (will not remove files).
    // Default: false - remove files
    dry: false,

    // If true, remove files on recompile. 
    // Default: false
    watch: false,

    // allow the plugin to clean folders outside of the webpack root.
    // Default: false - don't allow clean folder outside of the webpack root
    allowExternal: false,

    // perform clean just before files are emitted to the output dir
    // Default: false
    beforeEmit: false,
};


module.exports = {
    plugins: [
        new CleanWebpackPlugin(pathsToClean, cleanOptions),
    ],
}
  • 也可以使用 Node.js 的 rimraf
// webpack.common.js
const rimraf = require('rimraf');
const path = require('path');

rimraf(path.join(__dirname, './learn-1/public'), () => console.log('success to remove ./learn-1/public/'));

環境切分

// package.json
{
    "scripts": {
        "build": "webpack --config webpack.prod.js",
        "start": "webpack-dev-server --config webpack.dev.js --progress",
        "start-hot": "webpack-dev-server --open --config webpack.dev.js --hot --hot-only --progress",
        "start-prod": "webpack-dev-server --config webpack.prod.js --progress"
    }
}
  • 也可以使用 process.env.NODE_ENV 環境變數來區分
// webpack.config.js

const webpackConfig = {
    entry: {
        main: `${__dirname}/learn-1/app/main.js`
    }
}

switch (process.env.NODE_ENV.trim()) {
    case "dev":
        webpackConfig.devtool = '#cheap-module-eval-source-map';
        break;
    case "prod":
        webpackConfig.devtool = '#source-map';
        webpackConfig.plugins.push(
            new webpack.BannerPlugin('版权所有,翻版必究!')
        );
        break;
}
// package.json
{
    "scripts": {
        "dev": "set NODE_ENV=dev && webpack --watch",
        "prod": "set NODE_ENV=prod && webpack"
    }
}

待研究