使用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]'
            }
        ])
    ]
};

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352