使用Webpack實(shí)現(xiàn)前端構(gòu)建工具

使用Webpack實(shí)現(xiàn)前端構(gòu)建工具

webpack簡(jiǎn)單介紹

webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器。

核心概念:入口(entry)、輸出(output)、loader、插件(plugins)

主要功能點(diǎn)(要解決的問(wèn)題)

  • 日常開(kāi)發(fā)時(shí),代碼編譯
  • 團(tuán)隊(duì)協(xié)作開(kāi)發(fā),盡量減少開(kāi)發(fā)時(shí)的代碼沖突
  • 前端資源緩存控制,減少流量花費(fèi)

一、日常開(kāi)發(fā)時(shí),代碼編譯

  • ES6編譯
  • vue編譯
  • Less編譯
1.正常配置

初始化項(xiàng)目:

npm init

npm i webpack webpack-cli -D

創(chuàng)建webpack.config.js文件:

const path = require('path');

module.export = {
    entry: './app.js',
    output: {
        path: './dist',
        publicPath: '/dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            test: /\.css$/,
            use: [{
                loader: 'css-loader'
            }]
        }]
    },
    plugins: []
};
2.ES6編譯

1)webpack 已支持ES6 不需要單獨(dú)配置babel

2)項(xiàng)目中代碼需要做ES5的兼容

babel主要是把ES6規(guī)范的代碼編譯為ES5,ES5規(guī)范中的一些方法低版本瀏覽器還沒(méi)有做兼容,所以需要我們?cè)陧?xiàng)目中使用polyfill庫(kù)來(lái)兼容ES5。

項(xiàng)目中安裝:

npm i @babel/polyfill -P

項(xiàng)目中引用:

import "@babel/polyfill";
3.Vue編譯

1)配置vue-loader

vue-loader主要用作編譯*.vue文件

VueLoaderPlugin 用作把你定義的其他規(guī)則 也應(yīng)用到*.vue文件當(dāng)中

vue-loader 安裝:

npm i vue vue-loader -D
npm i vue-template-compile -D
const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

module.export = {
    entry: './app.js',
    output: {
        path: './dist',
        publicPath: '/dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        },{
            test: /\.css$/,
            use: [{
                loader: 'css-loader'
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};

2)配置vue-style-loader

vue-style-loader主要是用來(lái)處理*.vue文件中<style>標(biāo)簽

vue-style-loader 安裝:

npm i vue-style-loader -D
const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

module.export = {
    entry: './app.js',
    output: {
        path: './dist',
        publicPath: '/dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        },{
            test: /\.css$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: 'css-loader'
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};
4.Less編譯

Less 安裝:

npm i less-loader less -D
const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

module.export = {
    entry: './app.js',
    output: {
        path: './dist',
        publicPath: '/dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.css$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }, {
            test: /\.less$/,
            use: [{
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};

可以合并一下 css與less的處理

{
    test: /\.(c|le)ss$/,
    use: [{
        loader: 'vue-style-loader'
    }, {
        loader: 'css-loader'
    }, {
        loader: 'less-loader'
    }]
}

二、團(tuán)隊(duì)協(xié)作開(kāi)發(fā),盡量減少開(kāi)發(fā)時(shí)的代碼沖突

  • 目錄結(jié)構(gòu)安排
  • Webpack對(duì)目錄結(jié)構(gòu)的處理
1.目錄結(jié)構(gòu)安排

團(tuán)隊(duì)開(kāi)發(fā)過(guò)程中,要盡量保證各自的功能模塊獨(dú)立。在保持各自開(kāi)發(fā)獨(dú)立的同時(shí),還要不時(shí)提取一些常用到的方法作為工具類(lèi),提取一些經(jīng)常用到的組件作為公共組件,提取一些常用的樣式作為公共樣式。這樣做可以減少代碼沖突,并且使我們的開(kāi)發(fā)速度越來(lái)越快。

要做到這些,我們需要合理的配置我們的目錄結(jié)構(gòu)。

  • 根目錄
--- dist //編譯后文件目錄,上傳到服務(wù)器
    src //源文件,正常開(kāi)發(fā)用的目錄
    package.json //各種包、編譯命令配置等
    webpack.config.js //前端構(gòu)建工具

源文件結(jié)構(gòu)如下:

src  ---common //用來(lái)放工具類(lèi)、公共組件和公共樣式
        pages //平時(shí)開(kāi)發(fā)業(yè)務(wù)的目錄,跟每個(gè)頁(yè)面相對(duì)應(yīng)
        static //用來(lái)放一些不需打包的靜態(tài)資源,如網(wǎng)站的圖片、第三方不支持import引用的庫(kù)
      
        common---css
              ---js
              ---fonts //字體圖標(biāo)
              ---components
              ---index.js //入口文件
              ---index.css
        pages---index---index.js //入口文件
                     ---index.css
                     ---css
                     ---js
                     ---images
                     ---components //可以用來(lái)放vue組件
        static---css
               ---js
               ---images

編譯后的文件結(jié)構(gòu)如下:

dist ---css---pages---somePage.css
            |      ---index.css    
            |-common.css
        fonts---materialIcon.woff2
        images---pages---somePage---logo.png
        js---pages---somePage.js
           |      ---index.js    
           |-common.js
        static---css---echarts.css
              ---js---echarts.js
              ---images---favicon.ico

對(duì)應(yīng)的webpack.config.js修改:

  1. 多入口處理

定義好入口文件 -> 遍歷所有文件 -> 找出入口文件路徑

定義好入口文件:

./src/common/index.js
./src/pages/*/index.js

遍歷所有文件:

npm i glob -D //glob遍歷文件工具
const glob = require('glob'); //遍歷文件
let entryFile = {};

const files = [
  ...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
  ...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];

找出入口文件路徑:

files.forEach(val => {
    let filePath = val.split('/src/')[1];
    let folder = filePath.split('/index.js')[0];
    entryFile[folder] = val;
});
/*
    entryFile
    {
        'common': '/work/project/src/common/index.js',
        'pages/index': '/work/project/src/pages/index/index.js',
    }
    
*/

  1. 輸出處理
{
    filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
    publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
    path: __dirname + '/dist'
}

通過(guò)入口和輸出的處理,能夠完成js文件的編譯和打包,但是css文件會(huì)被打包到j(luò)s當(dāng)中,這樣不利于日常開(kāi)發(fā)調(diào)試,頁(yè)面訪問(wèn)加載速度也會(huì)被減慢,圖片、字體文件還沒(méi)有處理,所以要針對(duì)css和靜態(tài)文件需要做特殊處理。

  1. 靜態(tài)文件處理

css文件處理:

npm i mini-css-extract-plugin -D  //mini-css-extract-plugin 用作處理css文件,將css打包到css文件當(dāng)中
const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

const glob = require('glob'); //遍歷文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中

let entryFile = {};

const files = [
  ...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
  ...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];

files.forEach(val => {
    let filePath = val.split('/src/')[1];
    let folder = filePath.split('/index.js')[0];
    entryFile[folder] = val;
});

module.export = {
    entry: entryFile,
    output: {
        filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
        publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
        path: __dirname + '/dist'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.(c|le)ss$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: MiniCssExtractPlugin.loader,
            }, {
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({ 
          filename:'css/[name].css',
          chunkFilename: 'css/[id].css',
        })
    ]
};

靜態(tài)文件處理:

npm i copy-webpack-plugin -D //用來(lái)從src目錄copy文件到dist目錄 處理不需要編譯的文件 直接copy過(guò)去
npm i file-loader -D //處理被引用的靜態(tài)文件如background-image: url(./images/logo.png) 將資源copy到指定目錄并修改對(duì)應(yīng)url()中的地址
const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

const glob = require('glob'); //遍歷文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用

let entryFile = {};

const files = [
  ...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
  ...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];

files.forEach(val => {
    let filePath = val.split('/src/')[1];
    let folder = filePath.split('/index.js')[0];
    entryFile[folder] = val;
});

module.export = {
    entry: entryFile,
    output: {
        filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
        publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
        path: __dirname + '/dist'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.(c|le)ss$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: MiniCssExtractPlugin.loader,
            }, {
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
                        return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[ext]';
                    },
                    outputPath: 'images/',
                    publicPath: '/v1/dist/images/'
                  }
            }]
        }, {
            test: /\.(woff2)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
                        return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[ext]';
                    },
                    outputPath: 'fonts/',
                    publicPath: '/v1/dist/fonts/'
                  }
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({ 
          filename:'css/[name].css',
          chunkFilename: 'css/[id].css',
        }),
        new CopyWebpackPlugin([
            {
                from: path.join(__dirname, './src/static'),
                to: path.join(__dirname, './dist/static') + '/[name].[ext]'
            }
        ])
    ]
};

通過(guò)以上配置,可以實(shí)現(xiàn)一個(gè)基本的前端構(gòu)建工具。

團(tuán)隊(duì)開(kāi)發(fā)過(guò)程中,每個(gè)人接到不同的需求后,可以在pages目錄新建一個(gè)文件夾作為本次業(yè)務(wù)的開(kāi)發(fā)目錄,并將相關(guān)資源放到此目錄,保證了業(yè)務(wù)的獨(dú)立性,不會(huì)與其他人發(fā)生沖突。

因?yàn)閐ist中js與css文件的文件名與pages中的目錄名對(duì)應(yīng),調(diào)試js與css也很便捷。

通過(guò)不斷的擴(kuò)充common目錄,可以不斷的提高開(kāi)發(fā)效率。

沒(méi)有依賴關(guān)系的靜態(tài)文件(html文件中引用的不需要編譯的文件)也被妥善處理了。


三、前端資源緩存控制,減少流量花費(fèi)

瀏覽器的對(duì)靜態(tài)文件緩存來(lái)能提高頁(yè)面的加載速度,但是這樣,我們更新的代碼,不會(huì)第一時(shí)間被獲取到,這樣會(huì)產(chǎn)生一些BUG。

我們需要對(duì)資源進(jìn)行版本控制,發(fā)生變化的文件,修改版本后,瀏覽器會(huì)重新進(jìn)行緩存。

  • hash配置

webpack有三種緩存處理策略[hash][chunkhash][contenthash]

區(qū)別:

[hash] 當(dāng)前文件發(fā)生變化,所有文件版本都發(fā)生變化。

[chunkhash] 當(dāng)前文件發(fā)生變化,引用該文件的文件版本也會(huì)發(fā)生變化

[contenthash] 當(dāng)前文件發(fā)生變化,只該文件版本發(fā)生變化

哈希值配置方式:

 output: {
    filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
    publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
    path: __dirname + '/dist'
}

完整配置

const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

const glob = require('glob'); //遍歷文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用

let entryFile = {};

const files = [
  ...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
  ...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];

files.forEach(val => {
    let filePath = val.split('/src/')[1];
    let folder = filePath.split('/index.js')[0];
    entryFile[folder] = val;
});

module.export = {
    entry: entryFile,
    output: {
        filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
        publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
        path: __dirname + '/dist'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.(c|le)ss$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: MiniCssExtractPlugin.loader,
            }, {
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
                        return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
                    },
                    outputPath: 'images/',
                    publicPath: '/v1/dist/images/'
                  }
            }]
        }, {
            test: /\.(woff2)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
                        return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
                    },
                    outputPath: 'fonts/',
                    publicPath: '/v1/dist/fonts/'
                  }
            }]
        }]
    },
    plugins: [
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({ 
          filename:'css/[name].[contenthash].css',
          chunkFilename: 'css/[id].[contenthash].css',
        }),
        new CopyWebpackPlugin([
            {
                from: path.join(__dirname, './src/static'),
                to: path.join(__dirname, './dist/static') + '/[name].[hash:8].[ext]'
            }
        ])
    ]
};
  • hash值變化記錄

我們?yōu)槲募恿斯V抵螅残枰薷膶?duì)應(yīng)頁(yè)面中資源引用。所以,在文件版本發(fā)生變化后,我們需要記錄下文件的變化。

處理版本的插件

const fs = require('fs');

function readFile(filePath) {
    return new Promise((resolve, reject) => {
        if(fs.existsSync(filePath)) {
            fs.readFile(filePath, 'utf8', function(err, data) {
                if(err) {
                    reject(err);
                }else {
                    resolve(data);
                }
            });
        }else {
            resolve();
        }
    });
}

class HandleHashPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        compiler.hooks.done.tap('HandleHashPlugin', (arg) => {
            let compiledFile = arg.toJson().assets;
            let fileData = {};
            /*
                {
                    'images/pages/index/logo.png': {
                        last: '408685cc',
                        cur: '91d265fb'
                    }
                }
            */ 
            const filePath = path.join(__dirname, './filePath.json');
            

            readFile(filePath).then((fileContent) => {
                fileContent = fileContent ? JSON.parse(fileContent) : {};
                compiledFile.forEach((val) => {
                    let filename = val.name;
                    let hash = filename.match(/\.[0-9|a-z]*\.[0-9|a-z]*$/)[0].split('.')[1];
                    filename = filename.replace(`.${hash}`, '');
                    fileData[filename] = {
                        last: fileContent[filename] ? fileContent[filename]['cur'] || '' : '',
                        cur: hash
                    }
                });
                if(fs.existsSync(filePath)) {
                    fs.unlinkSync(filePath);
                }
                fs.writeFile(filePath, JSON.stringify(fileData), {
                    flag: 'a'
                }, function(err) {
                    if(err) {
                        console.error(err);
                    }else {
                        console.log('寫(xiě)入成功!');
                    }
                });
            }).catch((err) => {
                console.log(err);
            });
        })
    }
}

插件使用:

const path = require('path');

const VueLoaderPlugin = require('vue-loader/lib/plugin'); 

const glob = require('glob'); //遍歷文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用

let entryFile = {};

const files = [
  ...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
  ...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];

files.forEach(val => {
    let filePath = val.split('/src/')[1];
    let folder = filePath.split('/index.js')[0];
    entryFile[folder] = val;
});

module.export = {
    entry: entryFile,
    output: {
        filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
        publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
        path: __dirname + '/dist'
    },
    module: {
        rules: [{ //編譯vue文件
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.(c|le)ss$/,
            use: [{
                loader: 'vue-style-loader'
            }, {
                loader: MiniCssExtractPlugin.loader,
            }, {
                loader: 'css-loader'
            }, {
                loader: 'less-loader'
            }]
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
                        return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
                    },
                    outputPath: 'images/',
                    publicPath: '/v1/dist/images/'
                  }
            }]
        }, {
            test: /\.(woff2)$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
                        return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
                    },
                    outputPath: 'fonts/',
                    publicPath: '/v1/dist/fonts/'
                  }
            }]
        }]
    },
    plugins: [
        new HandleHashPlugin(),
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({ 
          filename:'css/[name].[contenthash].css',
          chunkFilename: 'css/[id].[contenthash].css',
        }),
        new CopyWebpackPlugin([
            {
                from: path.join(__dirname, './src/static'),
                to: path.join(__dirname, './dist/static') + '/[name].[hash:8].[ext]'
            }
        ])
    ]
};

收集到文件版本變化的信息后,我們只需在上線前,遍歷所有的頁(yè)面文件,把相應(yīng)的文件替換掉即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容