webpack多頁面入口生產專案開發配置

johnshere發表於2019-03-03

  這不是一個純粹的學習帖子,最開始為了生產專案考慮的。公司有個新的、小的活動專案。以此為假想,所以我希望學習一些新的技術應用在上面;這個新的專案是作為舊專案的一個子系統存在的,所以又必須在一定程度上保持一致。
  而這個舊專案的原有使用構建工具fis的版本比較老舊,不敢升級,怕出什麼么蛾子,所以又不能動他。
  在網上學習了眾多攻略之後自己嘗試搭建了一下,解決了一些問題,也留下了一下疑惑。

專案資源路徑

github: github.com/johnshere/a…

一、環境配置

node: v10.6.0(v6.12.3)、yarn: 1.7.0、webpack: 4.16.1
系統:windows10

  yarn是類似npm但是效率更高的包管理工具,命令互換參考yarnpkg.com/zh-Hans/doc…
  可以使用npm安裝(這裡讓我想到IE存在的意義,-_-)

npm install -g yarn
複製程式碼

  也可以去官網下載客戶端

二、目錄結構

  如圖:

webpack多頁面入口生產專案開發配置

src:工程原始碼;
release:工程釋出;
webpack.config:webpack配置檔案;

  釋出之後的HTML保持與src中的路徑一致;這樣程式碼中使用相對路徑訪問頁面就不會出現結構錯亂的問題。

webpack多頁面入口生產專案開發配置

  src目錄下有三個文:entry.json/index.html/index.js;
  這是目錄索引頁面,因為是多頁面入口,webpack-dev-server模式開啟的時候用於快速進入自己想要的頁面(下面會說)

三、兩個構建環境切換(與webpack無關)

  公司原有的fis是最初版本的,一直沒有人做更新維護,現在已經落後現在的技術版本多年,但是又必須使用。
  webpack4不可能在和他進行相容,所以我安裝了兩個不同版本的node,v10.6.0、v6.12.3;使用的時候切換

webpack多頁面入口生產專案開發配置

  當然實際的環境變數配置了一個,然後我寫了一個指令碼,執行命令(changeNodeName)後切換資料夾名稱,把這個指令碼放在node個目錄下,如下圖:

webpack多頁面入口生產專案開發配置

  指令碼很簡單,就是判斷資料夾名稱、改變名稱;改變後的名稱保持和環境變數裡面的名字一直就行。這樣做的問題也很大,就是沒有辦法同時編輯兩個工程。

set dir=D:
set name1=node612
set name2=node106
set name=node
if exist %dir%\%name1% (
	echo "node612 ==> node"
	ren %dir%\%name% %name2%
	ren %dir%\%name1% %name%
)else (
	echo "node106 ==> node"
	ren %dir%\%name% %name1%
	ren %dir%\%name2% %name%
) 
pause
複製程式碼

  這樣切換了node,實際上就是切換整個開發環境,畢竟這兩個構建工具都是依賴於node的。
  切換時在cmd或者powershell裡執行:

changeNodeName

四、webpack配置

  這個應該是重中之重了,在寫配置之前我首先確定了自己想解決的一些問題

  1. 釋出後保證目錄結構不變
  2. 分割公共檔案,如樣式、圖片;達到快取目的
  3. 分割的大檔案不能過大(未解決)、不能讓使用者頻繁載入
  4. 保證檔案之間快取良好互不干擾
  5. 轉義語法

1、webpack.entry.util.js

const path = require("path");
const Glob = require("glob");
const fs = require("fs");

let obj = {
    /**
     * 根據目錄獲取入口
     * @param  {[type]} globPath [description]
     * @return {[type]}          [description]
     */
    getEntryJs: function (globPath) {
        globPath = path.resolve(__dirname, globPath);
        let entries = {};
        Glob.sync(globPath).forEach(function (entry) {
            let basename = path.basename(entry, path.extname(entry)),
                pathname = path.dirname(entry),
                paths = pathname.split(`/`),
                fileDir = paths.splice(paths.indexOf("src") + 1).join(`/`);

            //僅處理page路徑下的js
            if (pathname.indexOf("page") > -1) {// && fileDir && fileDir.indexOf(("page") === 0)) {
                entries[(fileDir ? fileDir + `/` : fileDir) + basename] = pathname + `/` + basename;
            }

        });
        //目錄頁保留
        entries["index"] = path.resolve(__dirname,"../src/index").split("\").join("/");
        console.log("---------------------------------------------
entries:");
        console.log(entries);
        console.log("----------------------------------------------");
        return entries;
    },
    /**
     * 根據目錄獲取 Html 入口
     * @param  {[type]} globPath [description]
     * @return {[type]}          [description]
     */
    getEntryHtml: function (globPath) {
        globPath = path.resolve(__dirname, globPath);
        let entries = [];
        Glob.sync(globPath).forEach(function (entry) {
            let basename = path.basename(entry, path.extname(entry)),
                pathname = path.dirname(entry),
                paths = pathname.split(`/`),
                // @see https://github.com/kangax/html-minifier#options-quick-reference
                minifyConfig = process.env.NODE_ENV === "production" ? {
                    removeComments: true,
                    // collapseWhitespace: true,
                    minifyCSS: true,
                    minifyJS: true
                } : "";
            //只處理page目錄下的HTML
            //保留目錄頁
            if (entry.indexOf("page") > -1 ) {
                let chunkName = paths.splice(paths.indexOf("src") + 1).join(`/`) + "/" + basename;

                entries.push({
                    filename: chunkName + ".html",
                    template: entry,
                    chunks: [`public/vendor`, chunkName],
                    minify: minifyConfig
                });
            }
        });
        //保留目錄頁
        entries.push({
            filename: "index.html",
            template: path.resolve(__dirname,"../src/index.html").split("\").join("/"),
            chunks: [`public/vendor`,"index"]
        });
        //儲存entry的json檔案
        this.entry2JsonFile(entries);

        return entries;
    },
    /**
     * 生成entry對應的json檔案
     * @param entries
     */
    entry2JsonFile: function (entries) {
        console.log(entries);
        let json = {};
        if (entries) {
            entries.forEach(v => {
                json[v.filename] = v.filename;
            });
        }
        console.log(json);

        //同步寫入檔案
        let fd = fs.openSync(path.resolve(__dirname, "../src/entry.json"), "w");
        fs.writeSync(fd, JSON.stringify(json), 0, "utf-8");
        fs.closeSync(fd);
    }
};
// obj.getEntry("../src/page/**/*.js");
// obj.getEntryHtml(`../src/page/**/index.html`);

module.exports = obj;
複製程式碼

  這個地方的entry識別參考了:

github地址:github.com/givebest/we…

  這個entry工具主要是為了識別js和HTML;我在原有的邏輯上進行了修改,符合了我的要求,即只識別page目錄下的entry。
  同時,我新增了一個方法,即將所有的HTML路徑寫入到一個json檔案中儲存起來(後面dev-server模式用到)。前兩個方法裡也為入口目錄頁做了特殊處理
  這個工具中對chunk的key值做了特殊處理,可以看出,切割出了從src之後的路徑作為key值,因為webpack的name是支援路徑的,這樣就達到問題1的效果。

2、webpack.base.conf.js

const path = require("path");
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
// const ExtractTextPlugin = require(`extract-text-webpack-plugin`);
const CleanWebpackPlugin = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const entryUtil = require("./webpack.entry.util");
let entryJs = entryUtil.getEntryJs(`../src/page/**/index.js`);

let conf = {
    entry: entryJs,//js打包入口識別
    output: {
        path: path.resolve(__dirname, "../release"),
        filename: "[name].[chunkHash].js",
        // publicPath: "../../public"
    },
    module: {
        rules: [
            {
                test: /.css$/,
                // loader: ExtractTextPlugin.extract({
                //     fallback: `style-loader`,
                //     use: `css-loader`
                // })
                use:[MiniCssExtractPlugin.loader,`css-loader`]//`style-loader`,
            },
            {
                test: /.html$/,
                loader: `html-withimg-loader`
            },
            {test: require.resolve("jquery"), loader: "expose-loader?$!expose-loader?jQuery"}
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     filename: "index.html",
        //     template: "src/page/index.html",
        //     chunks: ["main", "vender"]
        // }),
        // new ExtractTextPlugin("./[name].[chunkHash].css")
        new CleanWebpackPlugin(["release"],{
            root: path.resolve(__dirname, ".."),
            verbose: true,
            dry: false
        }),
        new MiniCssExtractPlugin({
            filename: "[name].[contenthash:7].css",
            chunkFilename: "[name].[contenthash].css"
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    name: "public/vendor",
                    chunks: "all",
                    minChunks: 2
                }
            }
        }
    },
    resolve: {
        extensions: [".js", ".jsx"],
        alias: {
            layer: path.resolve(__dirname, "../src/public/js/layer/mobile/layer.js"),
            "layer.css": path.resolve(__dirname, "../src/public/js/layer/mobile/need/layer.css")
        }
    }
};
//HTML入口
let entryHtml = entryUtil.getEntryHtml(`../src/page/**/index.html`);

entryHtml.forEach(function (v) {
    conf.plugins.push(new HtmlWebpackPlugin(v));
});
module.exports = conf;
複製程式碼

  這裡就需要給解釋了,開始學習webpack,然後網上不斷找各種帖子,學習、修改、測試最終成了這些配置檔案,有些改動時間長了我自己都忘記(-_-)!。

2.1 獲取entry、HtmlWebpackPlugin

  使用工具獲取指定的HTML和js,這裡我做一個限制,只取index名稱的,這是因為公司很多模板檔案都是用html字尾。
  webpack的入口是隻識別js的,這裡就需要用到HtmlWebpackPlugin,沒生成一個HTML與js的對應關係就要new一個HtmlWebpackPlugin。所以上面entryHtml是push進去的,還有就是entryHtml中做了生產環境的判斷。

2.2 分割css檔案

  現在使用的是MiniCssExtractPlugin,但是從註釋開出來我最開始使用的是ExtractTextPlugin(我也是從註釋看到才想起來的,哈哈哈哈)。
  先說ExtractTextPlugin,這個要在webpack4上面用正常安裝是不行的,現在必須指定版本@next,否則不能相容webpack4。如下:

yarn add ExtractTextPlugin@next
複製程式碼

  配置好了之後,我用了一段時間,最後在思考上面第四個問題的時候,把這個替換掉了,ExtractTextPlugin好像不能使用contenthash。
  我們公司是做bss系統的,業務複雜,而且更換業務邏輯的頻率很快,所以index.js修改比較多,但是樣式和圖片其實改動不多,不能因為改了一個if else,就需要使用者更新css和圖片吧。所以換成MiniCssExtractPlugin現在的樣子。
  然後關於MiniCssExtractPlugin的配置

filename是配置每個chunk對應分割出css檔案的配置
chunkfilename是配置分離出的公共css檔案的配置

webpack多頁面入口生產專案開發配置

2.3 載入jquery

  jquery沒有實現模組化,在loader裡面做了特殊處理;這樣之後在每個js裡面就可以使用require或者import引入jquery
  但是實際上,這個只能達到引入效果,$還是全域性物件。

2.4 HTML中的圖片路徑

  我在有些前輩的帖子中看到是需要在HTML標籤中加一下引用判斷、loader標識;這樣很不友好;這裡使用了一個loader:html-withimg-loader,用這個loader,就不用管了,他自己處理HTML中出現的圖片連結。

2.5 清理

  清理已經存在的檔案,如果不清理每次釋出都會有殘餘檔案,雖然沒有什麼影響,但是不能忍。
  CleanWebpackPlugin可以指定清理的正則配置,如:

    new CleanWebpackPlugin(["release"],{
            root: path.resolve(__dirname, ".."),
            verbose: true,
            dry: false
        }),
複製程式碼
    new CleanWebpackPlugin(["release/*.js","release/**/*.*"],{
            root: path.resolve(__dirname, ".."),
            verbose: true,
            dry: false
        }),
複製程式碼

3、webpack.devServer.conf.js

  開發環境

`use strict`;
const path = require("path");

const webpack = require("webpack");
const merge = require(`webpack-merge`);

const base = require(`./webpack.base.conf`);

// process.env.NODE_ENV = "development";

module.exports = merge(base, {
    mode: "development",
    devtool: "eval-source-map",
    output: {
        path: path.resolve(__dirname, "../release"),//"../release_dev"),
        filename: "[name].[hash].js",
    },
    module: {
        rules: [
            {
                test: /.(png|jpg|gif)$/,
                // loader: `url-loader?limit=8192&name=./public/images/[name].[hash].[ext]`
                loader: {
                    loader: `url-loader`,
                    options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64
                        name: `[name].[hash].[ext]`,
                        // limit: 8192, // 表示小於50kb的圖片轉為base64,大於50kb的是路徑
                        // outputPath: `/public/images` //定義輸出的圖片資料夾
                    }
                }
            }
        ]
    },
    plugins:[
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 8080,
        contentBase: path.resolve(__dirname, "../release"), //本地伺服器所載入的頁面所在的目錄
        historyApiFallback: true, //不跳轉
        inline: true, //實時重新整理
        hot: true, // 開啟熱更新,
        //伺服器代理配置項
        proxy: {
            `/o2o/*`:{
                target: `https://www.baidu.com`,
                secure: true,
                changeOrigin: true
            }
        }
    }
});
複製程式碼

  這個在base的基礎上做了些許調整,主要是為了使用webpack-dev-server;這個配置檔案是為它存在的。

3.1 output hash

  這裡的hash有chunkhash改成hash,原因是使用HotModuleReplacementPlugin之後不能使用chunkhash和contenthash。
  看到有些地方說把“hot:true”去掉就行了,但是我自己實際測試不行,只是去掉hot還是會報錯;所以我索性給改成hash了,反正是本機除錯,影響不大。

3.2 devServer

  這個功能很強大,對開發人員來說是非常友好的。
  安裝webpack-dev-server

yarn add webpack-dev-server
複製程式碼

  這個代理proxy功能還是非常強大的,將後臺服務請求指向我們的測試環境或者本地。我們原有的fis是包裝了一層nginx,每次還要單開啟,單獨配置nginx。這裡整合這個功能,很好。本地開發減少依賴,也便於除錯。

3.4 入口(entry)目錄頁

  前面在entry工具中將所有的entry寫入到一個json檔案中了。在這個地方就用到了,我們專案本質上根本不是spa,使用webpack還是比較牽強的。
  當啟動了webpack-dev-server之後它會預設開啟根目錄下的index.html。其實我們專案的頁面很多,不論預設開啟哪個都不方便開發,我乾脆把這個index.html做成了一個目錄頁面。將entry.json中所有的路徑全顯示,點選之後進入各個頁面。

// const $ = require("jquery");
import $ from "jquery";
const entryJson = require("./entry.json");
console.log(1122333,entryJson);
$(() => {
    $("html").css("font-size","16px");
    for (let k in entryJson){
        $("body").append("<a style=`margin: 1rem;padding-left:3rem;font-size: 2rem;line-height: 2rem;display: block` href=`"+entryJson[k]+"`>"+entryJson[k]+"</a></br>");
    }
});
複製程式碼
webpack多頁面入口生產專案開發配置

4、webpack.pro.conf.js

  生產環境

`use strict`;
const path = require("path");

const merge = require(`webpack-merge`);
const base = require(`./webpack.base.conf`);

module.exports = merge(base, {
    mode: "production",
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    name: "public/vendor",
                    chunks: "all",
                    minChunks: 2
                }
            }
        }
    },
    module: {
        rules: [
             {
                 test:/.js$/,
                 exclude: /node_modules/,
                 loader: "babel-loader"
             },
            {
                test: /.(png|jpg|gif)$/,
                // loader: `url-loader?limit=8192&name=./public/images/[name].[hash].[ext]`
                loader: {
                    loader: `url-loader`,
                    options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64
                        name: `[name].[hash].[ext]`,
                        limit: 8192, // 表示小於的圖片轉為base64,大於的是路徑
                        outputPath: `public/images` //定義輸出的圖片資料夾
                    }
                }
            }
        ]
    }
});
複製程式碼

  這個生產的配置也是在前面的base基礎上調整的。

4.1 釋出目錄調整

  這個小的工程是作為一個子工程存在於舊專案,所以url不是直接訪問的,需要加上“工程名”的一級路徑。url-loader的outputPath、所有chunkname都需要多加一段“activity”,具體需要自己除錯。
  例如:

xxxx.com/index.html -> xxxx.com/activity/in…
xxxx.com/public/1.cs… -> xxxx.com/activity/pu…

  這個地方有個需要注意,最開始嘗試的時候,我想只要只要改output就行了;但是測試之後才發現不行。原因很簡單,這個圖片src是給瀏覽器用的,是統一資源定位符。僅僅調整output的path是不會在定位符上加“activity”的,那僅僅是改變了釋出後檔案儲存的路徑。x現在需要在釋出的時候加深一個目錄級別,例如:

optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    name: "activity/public/vendor",
                    chunks: "all",
                    minChunks: 2
                }
            }
        }
    },
複製程式碼
  {
                test: /.(png|jpg|gif)$/,
                // loader: `url-loader?limit=8192&name=./public/images/[name].[hash].[ext]`
                loader: {
                    loader: `url-loader`,
                    options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64
                        name: `[name].[hash].[ext]`,
                        limit: 8192, // 表示小於的圖片轉為base64,大於的是路徑
                        outputPath: `activity/public/images` //定義輸出的圖片資料夾
                    }
                }
            }
複製程式碼

4.2 圖片分割

  如程式碼中展示這裡使用了url-loader,並且設定limit;當圖片超過limit限制會單獨生成檔案,否則就是base64儲存。
  但是這裡我遇到一個棘手問題,當圖片單獨儲存時,options.name的hash值不能設定成contentHash或者chunkHash,並且也沒有找到合適的解決辦法,希望知道的朋友給我說一下。(雖然在一定程度上說不用hash值也行,但是我感覺這樣不好)

4.3 babel編譯

  使用babel轉義ECMAScript6的語法,使之相容舊的瀏覽器。如程式碼中設定loader,然後在專案根目錄建立新檔案.babelrc,內容:

{
  "presets": ["env"]
}
複製程式碼

  安裝babel

yarn add babel-core babel-loader babel-preset-env
複製程式碼

4.4 mode NODE_env

  這裡在webpack配置檔案中設定了mode:production,並且在啟動指令碼中也設定node的環境為production。刪掉了devtool。
  這裡設定的環境配合entry工具中對環境的識別,會配置壓縮設定。

package.json的scripts

如下:

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --config ./webpack.config/webpack.dev.conf.js",
    "pro": "cross-env NODE_ENV=production webpack --config ./webpack.config/webpack.pro.conf.js --progress",
    "devServer": "webpack-dev-server --config ./webpack.config/webpack.devServer.conf.js --open --mode development",
    "watch": "webpack --config ./webpack.config/webpack.dev.conf.js --watch"
  }
}
複製程式碼

  首先安裝cross-env,用於設定node環境;在上面的指令碼中可以看到cross-env的使用

yarn add cross-env
複製程式碼

  上面設定兩個webpack的配置檔案,但是沒有實際使用,其實使用的命令就是scripts中的內容。只不過這裡可以是操作簡化,但我們使用時只需要啟動指令碼,如下:
  開發環境:

yarn run devServer
複製程式碼

  生產環境:

yarn run pro
複製程式碼

  run也是可以省略的。
  webpack-dev-server模式下不會將實際釋出的內容寫入在硬碟上,如果我們需要自行檢視內容,可以執行:

yarn run watch
複製程式碼

  只不過這樣做意義不大,因為我發現,你每次修改都會產生一些列檔案,很快你就發現生成的是一堆垃圾,從中找東西費勁的很。

問題遺留

  1. 大圖片大單分割出來後無法使用contenthash,我如何能讓一個大圖長久快取吶
  2. 公共檔案過大,僅我寫的這個測試工程vender就已經一兆多,感覺不是很大,但是真實專案中就很可怕了。而且我們專案是移動端的,這樣大檔案下載的留白時間也很難受。

相關文章