webpack多頁面入口生產項目開發配置

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

項目資源路徑

github: github.com/johnshere/a…css

1、環境配置

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

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

npm install -g yarn
複製代碼

  也能夠去官網下載客戶端node

2、目錄結構

  如圖: jquery

src:工程源代碼; release:工程發佈; webpack.config:webpack配置文件;webpack

  發佈以後的HTML保持與src中的路徑一致;這樣代碼中使用相對路徑訪問頁面就不會出現結構錯亂的問題。nginx

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

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

  公司原有的fis是最第一版本的,一直沒有人作更新維護,如今已經落後如今的技術版本多年,可是又必須使用。
  webpack4不可能在和他進行兼容,因此我安裝了兩個不一樣版本的node,v10.6.0、v6.12.3;使用的時候切換 git

  固然實際的環境變量配置了一個,而後我寫了一個腳本,執行命令(changeNodeName)後切換文件夾名稱,把這個腳本放在node個目錄下,以下圖:
  腳本很簡單,就是判斷文件夾名稱、改變名稱;改變後的名稱保持和環境變量裏面的名字一直就行。這樣作的問題也很大,就是沒有辦法同時編輯兩個工程。

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裏執行:github

changeNodeName

4、webpack配置

  這個應該是重中之重了,在寫配置以前我首先肯定了本身想解決的一些問題

  1. 發佈後保證目錄結構不變
  2. 分割公共文件,如樣式、圖片;達到緩存目的
  3. 分割的大文件不能過大(未解決)、不能讓用戶頻繁加載
  4. 保證文件之間緩存良好互不干擾
  5. 轉義語法

一、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("---------------------------------------------\nentries:");
        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的效果。

二、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文件的配置

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
        }),
複製代碼

三、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.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就已經一兆多,感受不是很大,可是真實項目中就很可怕了。並且咱們項目是移動端的,這樣大文件下載的留白時間也很難受。
相關文章
相關標籤/搜索