webpack4打包vue前端多頁面專案

comWang發表於2018-09-14

之前一直用的腳手架,這次自己搭建webpack前端專案,花費了不少心思,於是做個總結。

1.用法

專案結構如下:

project
  |- bulid                   <!-- 這個目錄是自動生成的-->
       |- public
       |- css
       |- js
       |- page1.html             <!-- 外掛生成的html檔案-->
       |- page2.html             <!-- 外掛生成的html檔案-->
       ...
  |- public/                 <!-- 存放字型、圖片、網頁模板等靜態資源-->
  |- src                     <!-- 原始碼資料夾-->
       |- components/
       |- css/
       |- js/
       |- page1.js               <!-- 每個頁面唯一的VUE例項,需繫結到#app-->
       |- page2.js               <!-- 每個頁面唯一的VUE例項,需繫結到#app-->
       ...
  |- package.json
  |- package-lock.json
  |- README.md
複製程式碼

public資料夾存放一些靜態檔案,src資料夾存放原始碼。每個頁面通過一個入口檔案(page1.js,page2.js,..)生成vue例項,掛載到外掛生成的html檔案的#app元素上。

安裝依賴

$ npm install
複製程式碼

進入開發模式

$ npm run start
複製程式碼

瀏覽器會開啟 http://localhost:3000,這時頁面一片空白,顯示 cannot get幾個字。不要慌,在url後面加上 /page1.html,回車,便可看見我們的頁面。 這是因為我把開發伺服器的主頁設定為index.html,而本例中頁面為 page1.html,page2.html,因此會顯示一片空白。

開發完成了,構建生產版本:

$ npm run build
複製程式碼

這會產生一個build/資料夾,裡面的檔案都經過優化,伺服器響應的資源,就是來自於這個資料夾。

2.介紹

2.1 webpack基礎配置

我們的開發分為生產環境和開發環境,因此需要有2份webpack的配置檔案(可能你會想用env環境變數,然後用3目運算子根據env的值返回不同值。然而這種方法在webpack匯出模組的屬性中無效,我試過~~~)。這裡我們拆分成3個檔案,其中webpack.common.js是常規的配置,在兩種環境下都會用到,webpack.dev.jswebpack.prod.js則是在2種環境下的特有配置。這裡用到 webpack-merge這個包,將公共配置和特有配置進行合成。

  • webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const devMode = process.env.NODE_ENV !=='production';

// 需要被打包入口檔案陣列
// 陣列元素型別 {string|object}
// string:將以預設規則生成bundle
// object{filename|title|template} 生成的bundle.html的檔名|title標籤內容|路徑 /public 下的模板檔案(需指定檔案字尾)
const entryList = [
    'page1',
    'page2',
];


/**
 * @param {array} entryList
 * @param {object} option:可選  要手動配置的內容
 */
const createEntry = (list = [], option = {}) => {
    const obj = {};
    list.forEach((item) => {
        const name = item.filename ? `./js/${item.filename}` : `./js/${item}`;
        obj[name] = path.resolve(__dirname, './src', `./${item}.js`);
    });
    return Object.assign(obj, option);
};


module.exports = {
    entry: createEntry(entryList),
    output: {
        path: path.resolve(__dirname, './build'),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                    },
                },
            },
            {
                test: /\.vue$/,
                use: 'vue-loader',
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: {
                    loader: 'file-loader',
                    options: {
                        name: 'public/fonts/[name].[ext]',
                    },
                },
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: {
                    loader: 'file-loader',
                    options: {
                        name: 'public/images/[name].[ext]',
                    },
                },
            },
        ],
    },
    plugins: createPluginInstance(entryList).concat([
        // vue SFCs單檔案支援
        new VueLoaderPlugin(),
    ]),
};

複製程式碼

這裡我們沒有進行css檔案的配置,是因為生產環境下需要優化、提取,所以在另外2個檔案分別配置

  • webpack.dev.js
const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    output: {
        filename: '[name].js',
        chunkFilename: '[name].js',
    },
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [
                    'vue-style-loader',
                    'css-loader', 
                    'postcss-loader',
                    'less-loader'
                ],
            },
        ],
    },
    resolve: { alias: { vue: 'vue/dist/vue.js' } },
});

複製程式碼

vue分為開發版本和生產版本,最後一行是根據路徑指定使用哪個版本。

  • webpack.prod.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'production',
    output: {
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].js',
    },
    resolve: { alias: { vue: 'vue/dist/vue.min.js' } },
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [
                    MiniCssExtractPlugin.loader, 
                    'css-loader', 
                    'postcss-loader',
                    'less-loader'
                ],
            },
        ],
    },
複製程式碼

在production環境下,我們使用了雜湊值便於快取,以後往生產環境下新增其他資源都會如此。

2.2 解決檔案輸出目錄

我們期待的build資料夾具有如下結構:

build
  |- css/
  |- js/
  |- page1.html
  |- page2.html
  ...
複製程式碼

即檔案按照型別放在一起,html檔案直接放在該目錄下,可是我們上面的配置的輸出結果是混合在一起的。由於name屬性既可以是檔名,也可以是/dir/a之類帶有路徑的檔名,我們根據這個特點做出一些修改。

  • 直接對output的輸出路徑更改

比如改為build/js,其他資源利用相對路徑比如../page1.html進行修改。我一開始就這樣做的,但最終會導致開發伺服器無法響應檔案的變化,因為他只能針對輸出目錄下的檔案進行監聽,該目錄之上的檔案變化無能為力。

  • 修改入口名稱

這也是我們的最終解決方案。將原來的檔名page1修改為/js/page1,最終輸出的js檔案便都會放在js資料夾裡。在生產環境下我們通過 MiniCssExtractPlugin 這個外掛提取js檔案中的css,這是該外掛的配置:

new MiniCssExtractPlugin({
            filename:'[name].[contenthash].css'
        })
複製程式碼

這裡的name就是當初入口的名字,受到入口名稱更改的影響,上面最終會變成 js/page1.131de8553ft82.css,並且該佔位符[name]只在編譯時有效,這意味著無法用函式對該值進行處理。因此不能使用[name]佔位符達到想要的目的,乾脆只用[id]。

new MiniCssExtractPlugin({
            filename:'/css/[id].[contenthash].css'
        })
複製程式碼

3.程式碼分割

在webpack4中使用optimization.splitChunks進行分割.

//webpack.common.js
const path = require('path');

module.exports = {
   // ... 省略其他內容
    optimization:{
        runtimeChunk:{
            name:'./js/runtime'
        },
        splitChunks:{
            // 避免過度分割,設定尺寸不小於30kb
            //cacheGroups會繼承這個值
            minSize:30000,
            cacheGroups:{
                //vue相關框架
                main:{
                    test: /[\\/]node_modules[\\/]vue[\\/]/,
                    name: './js/main',
                    chunks:'all'
                },
                //除Vue之外其他框架
                vendors:{
                    test:/[\\/]node_modules[\\/]?!(vue)[\\/]/,
                    name: './js/vendors',
                    chunks:'all'
                },
                //業務中可複用的js
                extractedJS:{
                    test:/[\\/]src[\\/].+\.js$/,
                    name:'./js/extractedJS',
                    chunks:'all'
                }
                
            }
        }
    }
};
複製程式碼

runtimeChunk包含了一些webapck的樣板檔案,使得你在不改變原始檔內容的情況下打包,雜湊值仍然改變,因此我們把他單獨提取出來,點這兒瞭解更多。 cacheGroups用於提取複用的模組,test會嘗試匹配(模組的絕對路徑||模組名),返回值為true且滿足條件的模組會被分割。滿足的條件可自定義,比如模組最小應該多大尺寸、至少被匯入進多少個chunk(即複用的次數)等。預設在打包前模組不小於30kb才被會分割。

4.樹抖動

在package.json里加入

"sideEffects":["*.css","*.less","*.sass"]
複製程式碼

該陣列之外的檔案將會受到樹抖動的影響——未使用的程式碼將會從export匯出物件中剔除。這將大大減少無用程式碼。如果樹抖動對某些檔案具有副作用,就把這些檔名放進陣列以跳過此操作。css檔案(包括.less,.sass)都必須放進來,否則會出現樣式丟失。

5. 外掛的使用

5.1 clean-webpack-plugin

每次打包後都會生成新的檔案,這可能會導致無用的舊檔案堆積,對於這些無用檔案自己一個個刪太麻煩,這個外掛會在每次打包前自動清理。實際中,我們不想在開發環境下清理掉build命令生成的檔案,因此只在生產環境使用了這個外掛。

5.2 html-Webpack-plugin

我們的原始碼目錄中並沒有html檔案,打包後的多個html檔案,就是我們用這個外掛生成的。

//webpack.common.js
// ...省略上面已經出現過的內容

//每個html需要一個外掛例項
//批量生成html檔案
const createPluginInstance = (list = []) => (
    list.map((item) => {
        return new HtmlWebpackPlugin({
            filename: item.filename ? `${item.filename}.html` : `${item}.html`,
            template: item.template ? `./public/${item.template}` :             './public/template.html',
            title: item.title ? item.title : item,
            chunks: [
                `./js/${item.filename ? item.filename : item}`,
                './js/extractedJS',
                './js/vendors',
                './js/main',
                './js/runtime',
                './css/styles.css',
                devMode ? './css/[id].css' : './css/[id].[contenthash].css',
            ],
        });
    })
);
複製程式碼

預設會將所有的入口檔案,程式碼分割後的檔案打包進一個html檔案裡,通過指定chunks屬性來告訴外掛只包含哪些塊,或者exludeChunks指定不應包含那些chunks。這裡有個小問題,我們無法讓檔案剛好只包含他需要的塊。若想不包含未使用的chunks,只能根據實際情況手動配置,用這個函式批量生成的檔案,總會包含所有的公共打包檔案。

5.3 mini-css-extract-plugin (prooduction)

該外掛用於提取js檔案中的css到單獨的css檔案中。

//webpack.prod.js
//...省略其他內容
plugins:[
        new CleanWebpackPlugin('build'), 
        // 提取css
        new MiniCssExtractPlugin({
            filename:'./css/[id].[contenthash].css'
        }),
        //優化快取
        new webpack.HashedModuleIdsPlugin()
    ]   
複製程式碼

5.4 optimize-css-assets-webpack-plugin (production)

用於精簡打包後的css程式碼,設定在配置optimization的minimizer屬性中,這將會覆蓋webpack預設設定,因此也要同時設定js的精簡工具(這裡我們用uglifyplugin外掛):

optimization: {
        minimizer:[
          new UglifyJsPlugin({
            cache: true,
            parallel: true
          }),
          new OptimizeCSSAssetsPlugin()
        ]
    }
複製程式碼

6.開發伺服器、熱模組替換 (development)

webpack.dev.js中增加如下內容即可:

//...省略其他內容
devServer:{
        index:'index.html',
        hot:true,
        contentBase:path.resolve(__dirname,'./build'),
        port:3000,
        noInfo:true
    },
plugins:[
        new webpack.HotModuleReplacementPlugin()
    ]
複製程式碼

使用開發伺服器可以在我們修改了原始檔後自動重新整理,因為是將資料放在記憶體中,因此不會影響硬碟中build資料夾。熱模組替換還需要在原始檔做相應修改。我們也為動態匯入語法進行了相應配置。

7.其他

public用於存放靜態資源,打包後也會在build/下建立一個同名資料夾,裡面存放的是public會被使用到的資源。如果在.css檔案裡引用了public裡的資源,如圖片,新增url的時候要使用絕對路徑:

<!-- src/css/page1.css -->
.bg-img {
    background-image:url(/public/images/1.jpg)
}
複製程式碼

這樣通過 http/https 開啟的時候就能正常使用,如果是以檔案形式開啟(比如打包後雙擊page1.html),會發現瀏覽器顯示無法找到資源。通過匯入圖片作為變數引用(import name from path),既可使用絕對路徑,也可使用相對路徑。

有什麼錯誤,歡迎指正,如果你喜歡,可以點贊

相關文章