webpack构建工具

2018-05-29 fishedee 前端

1 概述

webpack构建工具,webpack真是让人又爱又恨,爱是它是目前功能最为完善的前端打包构建工具,恨是真的TMD难用,而且API一直在变动,我总觉得我之前学过的关于webpack的都没用了。

最新版的webpack 4它对打包这个问题的抽象为以下几个模块。

关键模块:

  • mode,总体配置
  • entry,入口
  • output,出口
  • module,模块内容转换器
  • plugins,各种各样的插件

其他模块:

  • resolve,模块查找器
  • optimization,优化器,压缩和分包的优化
  • target,目标,umd,node和browser等等
  • devServer,开发用服务器
  • devTool,开发用工具,主要是sourceMap
Screen Shot 2018-05-29 at 9.54.53 P

模块很多,插件很多,选项也很多,我从未见过这么长的工具文档,简直要吐。

2 功能

2.1 单入口

webpack --mode development

webpack4加入了约定模式,直接指定mode,就能将src目录的index.js入口的打包到dist目录去。

范例

2.2 模块转换

const path = require('path');

module.exports = {
    entry:'./src/index.js',

    output:{
        filename:'index.js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:['env','react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    }
}

使用module将js和css文件进行打包前的预处理,以上的配置文件,使用的是babel-loader,style-loader和css-loader。

范例

2.3 插件

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry:{
        index:'./src/index.js',
        index2:'./src/index2.js',
    },

    output:{
        filename:'[name].js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:['env','react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    },

    plugins:[
        new htmlWebpackPlugin({
            filename:'index.html',
            chunks:['index'],
            template:'./src/index.html',
        }),
        new htmlWebpackPlugin({
            filename:'index2.html',
            chunks:['index2'],
            template:'./src/index.html',
        })
    ],

    devServer:{
        compress: true,
        proxy:{
            "/": {
                target:"https://www.baidu.com",
                changeOrigin: true
            }
        }
    }
}

使用plugins就可以注入webpack的插件机制,这里的代码使用的是htmlWebpackPlugin,可以从模板中自动生成入口的html文件。

范例

2.4 自动watch

webpack --mode development --watch

webpack命令加入–watch参数,就能侦听文件的变动,自动重新打包。

范例

2.5 开发用服务器

webpack-dev-server --mode development --hot --open

比watch更进一步的是,webpack提供了webpack-dev-server,可以将生成的打包文件暴露到8080端口的http服务器上。其中hot参数代表当文件变动时,自动刷新浏览器,open参数代表开启时,自动打开浏览器。

devServer:{
    compress: true,
    proxy:{
        "/": {
            target:"https://www.baidu.com",
            changeOrigin: true
        }
    }
}

另外,在webpack.config.js中可以配置compress,代表开启gzip压缩,加入proxy,代表代理后端服务的api。

范例

2.6 多入口依赖

对于多页面程序,webpack支持多个entry入口,也是相当简单的。要注意它并不支持目录式的entry入口,需要自己combine生成这些entry入口。

范例

2.7 文件名哈希

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry:'./src/index.js',

    output:{
        filename:'[name]-[hash].js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:['env','react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    },

    plugins:[
        new htmlWebpackPlugin({
            template:'./src/index.html',
        })
    ]
}

在output的配置上,加入[hash]作为文件名就可以了,这样输出的文件中会带有hash文件名,就能避免缓存造成发布前端文件时没有及时更新的问题。

范例

3 优化

3.1 cdn加速


const path = require('path');

module.exports = {
    entry:'./src/index.js',

    output:{
        filename:'index.js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:['env','react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    },

    externals:{
        'react':'React',
        'react-dom':'ReactDOM'
    }
}
<!doctype>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>webpack test1</title>
    </head>
    <body>
        <div id="root"></div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.0/umd/react.production.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.0/umd/react-dom.production.min.js"></script>
        <script src="dist/index.js"></script>
    </body>
</html>

对于像React,ReactDom这些对象,我们希望webpack不要打包到单一的main.js中,而是从cdn中直接拉取。这样启动速度更快,并发拉取js文件效率更高。那么,只需要在配置文件中配置好externals属性就可以了。

另外,这个工具也可以用来解决依赖包是普通的umd模块的问题。

范例

3.2 提取公共chunk

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry:{
        index:'./src/index.js',
        index2:'./src/index2.js',
    },

    output:{
        filename:'[name].js',
        chunkFilename: '[name].js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:['env','react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    },

    optimization:{
        splitChunks: {
            chunks: 'all',
            minSize: 1,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '-',
            name: true,
            cacheGroups: {
                vendors: {
                    name:'vendors',
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                },
                commons: {
                    name:'commons',
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        },
        runtimeChunk:{
            name:'webpack'
        },
    },

    plugins:[
        new htmlWebpackPlugin({
            filename:'index.html',
            chunks:['index','commons','vendors','webpack'],
            template:'./src/index.html',
        }),
        new htmlWebpackPlugin({
            filename:'index2.html',
            chunks:['index2','commons','vendors','webpack'],
            template:'./src/index.html',
        })
    ],

    devServer:{
        compress: true,
        proxy:{
            "/": {
                target:"https://www.baidu.com",
                changeOrigin: true
            }
        }
    }
}

webpack默认会将所有文件都打包到同一个包上,这会大大降低效率,webpack2版本提供了commonChunkPlugin来解决这个问题,可是相当粗糙。webpack4原生提供了splitChunks和runtimeChunk来解决,不仅可以合并共同包common,而且可以合并node_modules下的vendor包,更可以合并webpack的runtime包。

Screen Shot 2018-06-02 at 11.46.31 A

这是打包后请求

范例

3.3 动态加载chunk

import app from './app';
import dynamic from 'redva/dynamic';

export default [
  {
    url: '/counter',
    name: '计数器',
    component: dynamic({
      app: app,
      models: () => [import('../models/counter')],
      component: () => import('../routes/counter'),
    }),
  },
  {
    url: '/todo',
    name: 'todo列表',
    component: dynamic({
      app: app,
      models: () => [import('../models/todo')],
      component: () => import('../routes/todo'),
    }),
  },
]

我们希望在首页启动时不要一次加载所有页面的模块,而是按需加载各个子页面的模块,这样能大幅提高首页渲染速度。解决方法就是用webpack中的dynamic import方法,该方法将import看成一个函数,然后将文件名传递进去,返回的是一个promise的结果。webpack遇到这些dynamic import的方法会执行自动split chunks,然后在调用时才向后端真正请求模块。

Screen Shot 2018-06-02 at 11.48.43 A

以上就是dynamic import的威力,红色的资源是在加载该页时才出现的,要注意,对于多个异步请求的共同包,它会放在common.js的文件中。

范例

3.4 动态运行chunk

require.context('./models/', true, /\.js$/);
require.context('./routes/', true, /\.js$/);

与dynamic import不同的是,webpack也支持dynamic run。对于像react-native和electron的项目,他们的资源都是放在本地环境中的,dynamic import是没有意义的。我们希望的是一开始就将整个包所有模块都加载到内存里,但是当打开到该页面时才进行模块的语法解析和运行的操作,这样就能提高避免一开始将几个M的数据直接运行的问题。首先,按照webpack的语法,用require.context将需要dynamic run的模块指定出来,代表当前文件依赖这些目录下的模块,但是不执行它们。

import app from './app';
import dynamic from 'redva/dynamic';

const routers = [
  {
    url: '/counter',
    name: '计数器',
    models: ['counter'],
    component: 'counter',
  },
  {
    url: '/todo',
    name: 'todo列表',
    models: ['todo'],
    component: 'todo',
  },
];

export default routers.map(route => {
  return {
    url: route.url,
    name: route.name,
    component: dynamic({
      app: app,
      models: () => {
        return route.models.map(model => {
          return import(`../models/${model}`);
        });
      },
      component: () => {
        return import(`../routes/${route.component}`);
      },
    }),
  };
});

然后用dynamic import的方法加载这些模块,要注意的是,这里加载的模块名称可以是任意的字符串拼接,而不仅仅是一个常量的字符串。

Screen Shot 2018-06-02 at 12.03.48 P

可以看到,启动时并没有异步加载的包。但是从console日志中可以看出,每个页面的模块运行都是在必要时才进行的。

范例

3.5 删除多余依赖

import {Button,Modal} from 'antd'

在使用antd包时,使用以上的方法默认会打包整个antd的包,造成包太大了,我们希望能按需打包antd的包,避免引入整个antd包。

module:{
    rules:[
        {
            test:/\.js$/,
            exclude:/node_modules/,
            loader:'babel-loader',
            options:{
                presets:['env','react','stage-0'],
                plugins:[["import",{ "libraryName": "antd", style: 'css' }]]
            }
        },
        {
            test:/\.css$/,
            use:[
                {loader:'style-loader'},
                {loader:'css-loader'}
            ]
        }
    ]
}

方法是使用babel-plugin-import,它可以将antd的引入代码改为如下的方式

import {Button,Modal} from 'antd'
↓ ↓ ↓ ↓ ↓ ↓
require('antd/lib/button');
require('antd/lib/button/style/css');
require('antd/lib/modal');
require('antd/lib/modal/style/css');

这就大大减少了需要依赖的包,并自动引入了所对应的css的文件,这种方法也适用于material-design的包。

范例

3.6 tree-shaking

tree-shaking是webpack4的杀手级功能,它可以根据es6的import的静态分析,找出不必要的依赖引入,然后自动剔除他们。使用这种方法,有两个前提:

  • 支持tree-shaking的包必须是没有sideEffect的。
  • 支持tree-shaking的包必须使用es6 import的方法,不能使用commonjs,amdjs的模块依赖方法。
  • webpack最终打包时必须使用uglifyjs进行代码剔除(或使用mode=production的方式)。
module.exports = {
    mode: "production",
    entry:'./src/index.js',

    output:{
        filename:'[name]-[hash].js',
        path:path.resolve(__dirname,'dist')
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:[['env',{modules:false}],'react','stage-0']
                }
            },
            {
                test:/\.css$/,
                use:[
                    {loader:'style-loader'},
                    {loader:'css-loader'}
                ]
            }
        ]
    },

    optimization:{
        splitChunks: {
            chunks: 'all',
            minSize: 1,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '-',
            name: true,
            cacheGroups: {
                antd: {
                    name:'antd',
                    test: /[\\/]node_modules[\\/]_antd/,
                    priority: 11
                },
                vendors: {
                    name:'vendors',
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                },
                commons: {
                    name:'commons',
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        },
        runtimeChunk:{
            name:'webpack'
        },
    },

    plugins:[
        new htmlWebpackPlugin({
            template:'./src/index.html',
        })
    ],

    devServer:{
        compress: true
    }
}

幸好,antd均支持tree-shaking,除了它的dist目录下的antd.css。使用以上的webpack配置后,我们启动一下webpack-dev-server。注意配置中的babel-loader中的配置,modules必须为false。

Screen Shot 2018-06-02 at 12.53.29 P

可以看出,经过antd的包仅4.6k,远远少于引入整个antd包的大小。但是,这种方法并不支持antd的css的tree-shaking,你仍然需要引入整个antd的样式库。

范例

总体来说,这种方法还在早起探索阶段,并没有达到成熟,使用babel-plugin-import是更为可靠和优秀的方法。另外,这里还有一些关于tree-shaking的批评

5 总结

这可能只是webpack 4的开始,现在所学的可能都要被推翻,因为前端打包这个问题仍然在业界不断探索的阶段,并没有一个很好的最佳实践。

参考资料

相关文章