使用Webpack实现前端构建工具

使用Webpack实现前端构建工具

webpack简单介绍

webpack 是一个现代 JavaScript 应用程序的静态模块打包器。

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

主要功能点(要解决的问题)

  • 日常开发时,代码编译
  • 团队协作开发,尽量减少开发时的代码冲突
  • 前端资源缓存控制,减少流量花费

一、日常开发时,代码编译

  • ES6编译
  • vue编译
  • Less编译
1.正常配置

初始化项目:

npm init

npm i webpack webpack-cli -D

创建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 不需要单独配置babel

2)项目中代码需要做ES5的兼容

babel主要是把ES6规范的代码编译为ES5,ES5规范中的一些方法低版本浏览器还没有做兼容,所以需要我们在项目中使用polyfill库来兼容ES5。

项目中安装:

npm i @babel/polyfill -P

项目中引用:

import "@babel/polyfill";
3.Vue编译

1)配置vue-loader

vue-loader主要用作编译*.vue文件

VueLoaderPlugin 用作把你定义的其他规则 也应用到*.vue文件当中

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主要是用来处理*.vue文件中<style>标签

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'
    }]
}

二、团队协作开发,尽量减少开发时的代码冲突

  • 目录结构安排
  • Webpack对目录结构的处理
1.目录结构安排

团队开发过程中,要尽量保证各自的功能模块独立。在保持各自开发独立的同时,还要不时提取一些常用到的方法作为工具类,提取一些经常用到的组件作为公共组件,提取一些常用的样式作为公共样式。这样做可以减少代码冲突,并且使我们的开发速度越来越快。

要做到这些,我们需要合理的配置我们的目录结构。

  • 根目录
--- dist //编译后文件目录,上传到服务器
    src //源文件,正常开发用的目录
    package.json //各种包、编译命令配置等
    webpack.config.js //前端构建工具

源文件结构如下:

src  ---common //用来放工具类、公共组件和公共样式
        pages //平时开发业务的目录,跟每个页面相对应
        static //用来放一些不需打包的静态资源,如网站的图片、第三方不支持import引用的库
      
        common---css
              ---js
              ---fonts //字体图标
              ---components
              ---index.js //入口文件
              ---index.css
        pages---index---index.js //入口文件
                     ---index.css
                     ---css
                     ---js
                     ---images
                     ---components //可以用来放vue组件
        static---css
               ---js
               ---images

编译后的文件结构如下:

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

对应的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',//为项目中的所有资源指定一个基础路径,部分插件会用到
    path: __dirname + '/dist'
}

通过入口和输出的处理,能够完成js文件的编译和打包,但是css文件会被打包到js当中,这样不利于日常开发调试,页面访问加载速度也会被减慢,图片、字体文件还没有处理,所以要针对css和静态文件需要做特殊处理。

  1. 静态文件处理

css文件处理:

npm i mini-css-extract-plugin -D  //mini-css-extract-plugin 用作处理css文件,将css打包到css文件当中
const path = require('path');

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

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

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //专门处理css,将css打包到一个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',//为项目中的所有资源指定一个基础路径,部分插件会用到
        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',
        })
    ]
};

静态文件处理:

npm i copy-webpack-plugin -D //用来从src目录copy文件到dist目录 处理不需要编译的文件 直接copy过去
npm i file-loader -D //处理被引用的静态文件如background-image: url(./images/logo.png) 将资源copy到指定目录并修改对应url()中的地址
const path = require('path');

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

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

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //专门处理css,将css打包到一个css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些静态文件用

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',//为项目中的所有资源指定一个基础路径,部分插件会用到
        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]'
            }
        ])
    ]
};

通过以上配置,可以实现一个基本的前端构建工具。

团队开发过程中,每个人接到不同的需求后,可以在pages目录新建一个文件夹作为本次业务的开发目录,并将相关资源放到此目录,保证了业务的独立性,不会与其他人发生冲突。

因为dist中js与css文件的文件名与pages中的目录名对应,调试js与css也很便捷。

通过不断的扩充common目录,可以不断的提高开发效率。

没有依赖关系的静态文件(html文件中引用的不需要编译的文件)也被妥善处理了。


三、前端资源缓存控制,减少流量花费

浏览器的对静态文件缓存来能提高页面的加载速度,但是这样,我们更新的代码,不会第一时间被获取到,这样会产生一些BUG。

我们需要对资源进行版本控制,发生变化的文件,修改版本后,浏览器会重新进行缓存。

  • hash配置

webpack有三种缓存处理策略[hash][chunkhash][contenthash]

区别:

[hash] 当前文件发生变化,所有文件版本都发生变化。

[chunkhash] 当前文件发生变化,引用该文件的文件版本也会发生变化

[contenthash] 当前文件发生变化,只该文件版本发生变化

哈希值配置方式:

 output: {
    filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index为入口文件的文件名
    publicPath: __dirname + '/dist',//为项目中的所有资源指定一个基础路径,部分插件会用到
    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'); //专门处理css,将css打包到一个css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些静态文件用

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',//为项目中的所有资源指定一个基础路径,部分插件会用到
        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值变化记录

我们为文件加了哈希值之后,也需要修改对应页面中资源引用。所以,在文件版本发生变化后,我们需要记录下文件的变化。

处理版本的插件

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('写入成功!');
                    }
                });
            }).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'); //专门处理css,将css打包到一个css文件中

const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些静态文件用

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',//为项目中的所有资源指定一个基础路径,部分插件会用到
        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]'
            }
        ])
    ]
};

收集到文件版本变化的信息后,我们只需在上线前,遍历所有的页面文件,把相应的文件替换掉即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容