webpack我爱编程

带你走进webpack世界,成为webpack头号玩家。

2018-04-08  本文已影响0人  lihuanji

最近朋友圈被《头号玩家》刷爆了,斯皮尔伯格一个资深电影导演,把对过去经典的致敬,对未来的憧憬浓缩在这一部电影中,可以说让观众燃了起来。

观望整个前端开发,不断的演化,发展迅速。前端开发从最开始切页面, 前端自动化构建工具日新月异,从最初的Grunt,Gulp到现在前端项目可以说标配的webpack。

我们先来致敬经典:

image image

1. 什么是webpack?

可以看做一个模块化打包机,分析项目结构,处理模块化依赖,转换成为浏览器可运行的代码。

构建把一系列前端代码自动化去处理复杂的流程,解放生产力。

2. 进入webpack世界

初始化项目

    npm install webpack webpack-cli -D

webpack4抽离出了webpack-cli,所以我们需要下载2个依赖。

Webpack 启动后会从Entry里配置的Module开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的Loader去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。

webpack需要在项目根目录下创建一个webpack.config.js来导出webpack的配置,配置多样化,可以自行定制,下面讲讲最基础的配置。

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    }
}

有时候我们的项目并不是spa,需要生成多个js html,那么我们就需要配置多入口。

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash:8].js',
    },
}

entry配置一个对象,key值就是chunk: 代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。看看filename[name]: 这个name指的就是chunk的名字,我们配置的key值pageA pageB,这样打包出来的文件名是不同的,再来看看[hash],这个是给输出文件一个hash值,避免缓存,那么:8是取前8位。

这里有人会有疑问了,项目是多页面的,应该有pageA.html``pageA.js``pageA.css, 那么我应该生成多个html,这个只是做了JS的入口区分,我不想每一个页面都去复制粘贴一个html,并且html是大部分重复的,可能不同页面只需要修改title,下面来看看这个问题怎么解决:

需要引入一个webpack的plugin:

npm install html-webpack-plugin -D

该插件可以给每一个chunk生成html,指定一个template,可以接收参数,在模板里面使用,下面来看看如何使用:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash:8].js',
    },
    plugins: [
         new HtmlWebpackPlugin({
            template: './src/templet.html',
            filename: 'pageA.html',
            title: 'pageA',
            chunks: ['pageA'],
            hash: true,
            minify: {
                removeAttributeQuotes: true
            }
        }),
        new HtmlWebpackPlugin({
            template: './src/templet.html',
            filename: 'pageB.html',
            title: 'pageB',
            chunks: ['pageB'],
            hash: true,
            minify: {
                removeAttributeQuotes: true
            }
        }),
    ]
}

在webpack中,插件的引入顺序没有规定,这个在后面在继续详说。

这样在dist目录下就生成了pageA.html和pageB.html并且通过配置chunks,让pageA.html里加上了script标签去引入pageA.js。那么现在还剩下css没有导入,css需要借助loader去做,所以现在要下载几个依赖,以scss为例,less同理

npm install css-loader style-loader sass-loader node-sass -D

来看看如何配置loader

module.exports = {
    module: {
        rules: [
                {
                    test: /\.scss$/,
                    use: ['style-loader', 'css-loader', 'sass-loader'],
                    exclude: /node_modules/
                }
        ]
    }
}

如果想把css作为一个单独的文件,需要用到一个插件来做(webpack4.0.0以上版本需要next版本):

 npm i extract-text-webpack-plugin@next -D
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    // style-loader 把css直接写入html中style标签
                    fallback: 'style-loader',
                    // css-loader css中import支持
                    // loader执行顺序 从右往左执行
                    use: ['css-loader', 'sass-loader']
                }),
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin('[name].[contenthash:8].css'),
    ]
}

这样就实现了js,html,css的打包,那么再来看看一些常用的loader:

    rules: [
         // 处理js
         {
            test: /\.js?$/,
            exclude: /node_modules/,
            use: ['babel-loader']
        },
        // 处理图片
        {
            test: /\.(png|jpg|gif|ttf|eot|woff(2)?)(\?[=a-z0-9]+)?$/,
            use: [{
                loader: 'url-loader',
                options: {
                    query: {
                        // 阈值 单位byte
                        limit: '8192',
                        name: 'images/[name]_[hash:7].[ext]',
                    }
                }
            }]
        },
    ]

babel的配置建议在根目录下新建一个.babelrc文件

{
    "presets": [
        "env",
        "stage-0", 
        "react"
    ],
    "plugins": [
        "transform-runtime",
        "transform-decorators-legacy",
        "add-module-exports"
    ]
}

因为我们在文件名中加入hash值,打包多次后dist目录变得非常多文件,没有删除或覆盖,这里可以引入一个插件,在打包前自动删除dist目录,保证dist目录下是当前打包后的文件:

plugins: [
    new CleanWebpackPlugin(
        // 需要删除的文件夹或文件
        [path.join(__dirname, './dist/*.*')],
        {
            // root目录
            root: path.join(__dirname, './')
        }
    ),
]

指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配:

resolve: {
    extensions: ['.js', '.jsx', '.scss', '.json'],
},

3. 优化实战 高级装备

天下武功唯快不破,优化方案千千万万,各取所需吧。

提出公共的JS文件

webpack4中废弃了webpack.optimize.CommonsChunkPlugin插件,用新的配置项替代

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    chunks: 'initial',
                    minChunks: 2,
                    maxInitialRequests: 5,
                    minSize: 2,
                    name: 'common'
                }
            }
        }
    },
}

把多次import的文件打包成一个单独的common.js

HappyPack

在webpack运行在node中打包的时候是单线程去一件一件事情的做,HappyPack可以开启多个子进程去并发执行,子进程处理完后把结果交给主进程

 npm i happypack -D

需要改造一下loader配置,此loader用子进程去处理

const HappyPack = require('happypack');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: 'happypack/loader?id=babel',
            },
        ]
    },
    plugins: [
        new HappyPack({
            id: 'babel',
            threads: 4,
            loaders: ['babel-loader']
        }),
    ]
}

作用域提升

如果你的项目是用ES2015的模块语法,并且webpack3+,那么建议启用这一插件,把所有的模块放到一个函数里,减少了函数声明,文件体积变小,函数作用域变少。

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    plugins: [
        new webpack.optimize.ModuleConcatenationPlugin(),
    ]
}

提取第三方库

方便长期缓存第三方的库,新建一个入口,把第三方库作为一个chunk,生成vendor.js

module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
}

DLL动态链接

第三库不是经常更新,打包的时候希望分开打包,来提升打包速度。打包dll需要新建一个webpack配置文件,在打包dll的时候,webpack做一个索引,写在manifest文件中。然后打包项目文件时只需要读取manifest文件。

webpack.vendor.js

const webpack = require('webpack');
const path = require('path');

module.exports = {
    entry: {
        vendor: ['react', 'react-dom'],
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'dll/[name]_dll.js',
        library: '_dll_[name]',
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, './dist/dll', 'manifest.json'),
            name: '_dll_[name]',
        }),
    ]
};

path: manifest文件的输出路径
name: dll暴露的对象名,要跟output.library保持一致
context: 解析包路径的上下文,这个要跟接下来配置的dll user一致

webpack.config.js

module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.join(__dirname, './dist/dll', 'manifest.json')
        })
    ]
}

html

<script src="vendor_dll.js"></script>

4. 线上和线下

在生成环境和开发环境其实我们的配置是存在相同点,和不同点的,为了处理这个问题,会创建3个文件:

通过webpack-merge去做配置的合并,比如:

开发环境

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const base = require('./webpack.base');

const dev = {
    devServer: {
        contentBase: path.join(__dirname, '../dist'),
        port: 8080,
        host: 'localhost',
        overlay: true,
        compress: true,
        open:true,
        hot: true,
        inline: true,
        progress: true,
    },
    devtool: 'inline-source-map',
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
    ]
}
module.exports = merge(base, dev);

开发环境中我们可以启动一个devServer静态文件服务器,预览我们的项目,引入base配置文件,用merge去合并配置。

生产环境

再来看看生成环境最重要的代码压缩,混淆:

const path = require('path');
const merge = require('webpack-merge');
const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
const base = require('./webpack.base');

const prod = {
    plugins: [
        // 文档: https://github.com/gdborton/webpack-parallel-uglify-plugin
        new WebpackParallelUglifyPlugin(
            {
                uglifyJS: {
                    mangle: false,
                    output: {
                        beautify: false,
                        comments: false
                    },
                    compress: {
                        warnings: false,
                        drop_console: true,
                        collapse_vars: true,
                        reduce_vars: true
                    }
                }
            }
        ),
    ]
}
module.exports = merge(base, prod);

webpack-parallel-uglify-plugin可以并行压缩代码,提升打包效率

uglifyJS配置:

5. 成为头号玩家

想要成为头号玩家,玩转配置可不行,当然还要做一些loader和plugin的开发,去为项目做一些优化,解决痛点。

loader

loader是一个模块导出函数,在正则匹配成功的时候调用,webpack把文件数组传入进来,在this上下文可以访问loader API

下面来看看less-loaderstyle-loader如何实现:

let less = require('less');
module.exports = function (source) {
    const callback = this.async();
    less.render(source, (err, result) => {
        callback(err, result.css);
    });
}
module.exports = function (source) {
    let script = (`
      let style = document.createElement("style");
      style.innerText = ${JSON.stringify(source)};
      document.head.appendChild(style);
   `);
    return script;
}

plugin

webpack整个构建流程有许多钩子,开发者可以在指定的阶段加入自己的行为到webpack构建流程中。插件由以下构成:

整个webpack流程由compiler和compilation构成,compiler只会创建一次,compilation如果开起了watch文件变化,那么会多次生成compilation. 那么这2个类下面生成了需要事件钩子

compiler hooks 文档
compilation hooks 文档

写一个小插件,生成所有打包的文件列表(webpack4不推荐使用compiler.plugin来注册插件,webpack5将不支持):

class FileListPlugin{
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        compiler.hooks.emit.tap('FileListPlugin',function (compilation) {
            let fileList = 'filelist:\n\n';
            for (let filename in compilation.assets) {
                fileList += ('- '+filename+'\n');
            }
            compilation.assets['filelist.md']={
                source() {
                    return fileList;
                },
                size() {
                    return fileList.length
                }
            }
        });
    }
}
module.exports = FileListPlugin;

6. 最后

都读在这里了,还不点个赞吗。

感谢你阅读我的文章

上一篇下一篇

猜你喜欢

热点阅读