什么是 webpack? webpack 是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如 JS(含 JSX)、coffee、样式(含 less/sass)、图片等都作为模块来使用和处理。
webpack 的优势 其优势主要可以归类为如下几个:
webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
能被模块化的不仅仅是 JS 了。
开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转 base64 等。
扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮。
我们谈谈第一点。以 AMD/CMD 模式来说,鉴于模块是异步加载的,所以我们常规需要使用 define 函数来帮我们搞回调:
1 2 3 4 5 6 7 8 9 define(['package/lib' ], function (lib ) { function foo ( ) { lib.log('hello world!' ); } return { foo: foo, }; });
另外为了可以兼容 commonJS 的写法,我们也可以将 define 这么写:
1 2 3 4 5 6 7 8 9 10 11 12 define(function (require, exports, module ) { var someModule = require ('someModule' ); var anotherModule = require ('anotherModule' ); someModule.doTehAwesome(); anotherModule.doMoarAwesome(); exports.asplode = function ( ) { someModule.doTehAwesome(); anotherModule.doMoarAwesome(); }; });
然而对 webpack 来说,我们可以直接在上面书写 commonJS 形式的语法,无须任何 define (毕竟最终模块都打包在一起,webpack 也会最终自动加上自己的加载器):
1 2 3 4 5 6 7 8 9 10 var someModule = require ('someModule' );var anotherModule = require ('anotherModule' );someModule.doTehAwesome(); anotherModule.doMoarAwesome(); exports.asplode = function ( ) { someModule.doTehAwesome(); anotherModule.doMoarAwesome(); };
这样撸码自然更简单,跟回调神马的说 byebye~
不过即使你保留了之前 define 的写法也是可以滴,毕竟 webpack 的兼容性相当出色,方便你旧项目的模块直接迁移过来。
安装和配置 一. 安装 我们常规直接使用 npm 的形式来安装:
1 $ npm install webpack -g
当然如果常规项目还是把依赖写入 package.json 包去更人性化:
1 2 $ npm init $ npm install webpack --save-dev
二. 配置 每个项目下都必须配置有一个 webpack.config.js ,它的作用如同常规的 gulpfile.js/Gruntfile.js ,就是一个配置项,告诉 webpack 它需要做什么。 需要安装的插件有:style-loader`
css-loaderjsx-loader
sass-loaderless-loader
babel-loaderbabel-preset-es2015
babel-preset-react ` 我们看看下方的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 var webpack = require ('webpack' );var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js' );module .exports = { plugins: [commonsPlugin], entry: { index: './src/js/page/index.js' , vendor: ['vue' , 'vue-router' , 'vue-resource' , 'vuex' ], }, output: { path: 'dist/js/page' , filename: '[name].js' , }, module : { loaders: [ { test : /\.css$/ , loader : 'style-loader!css-loader' }, { test : /\.js[x]?$/ , loader : 'jsx-loader?harmony' }, { test : /\.scss$/ , loader : 'style!css!sass?sourceMap' }, { test : /\.(png|jpg)$/ , loader : 'url-loader?limit=8192' }, { test: /\.jsx?$/ , loader: 'babel' , query: { presets : ['react' , 'es2015' ] }, }, ], }, resolve: { root: 'E:/github/flux-example/src' , extensions: ['' , '.js' , '.json' , '.scss' ], alias: { AppStore: 'js/stores/AppStores.js' , ActionType: 'js/actions/ActionType.js' , AppAction: 'js/actions/AppAction.js' , }, }, };
⑴ plugins 是插件项,这里我们使用了一个 CommonsChunkPlugin 的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。
⑵ entry 是页面入口文件配置,output 是对应输出项配置(即入口文件最终要生成什么名字的文件、存放到哪里) ,其语法大致为:
1 2 3 4 5 6 7 8 9 10 11 { entry: { page1: "./page1" , page2: ["./entry1" , "./entry2" ] }, output: { path: "dist/js/page" , filename: "[name].bundle.js" } }
该段代码最终会生成一个 page1.bundle.js 和 page2.bundle.js,并存放到 ./dist/js/page 文件夹下。
⑶ module.loaders 是最关键的一块配置。它告知 webpack 每一种文件都需要使用什么加载器来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module: { //加载器配置 loaders: [ //.css 文件使用 style-loader 和 css-loader 来处理 { test: /\.css$/, loader: 'style-loader!css-loader' }, //.jsx 文件使用 jsx-loader 来编译处理 { test: /\.js[x]?$/, loader: 'jsx-loader?harmony' }, //.scss 文件使用 style-loader、css-loader 和 sass-loader 来编译处理 { test: /\.scss$/, loader: 'style!css!sass?sourceMap'}, //.less 文件使用 style-loader、css-loader 和 less-loader 来编译处理 { test: /\.less$/, loader: 'style!css!less?sourceMap'}, //图片文件使用 url-loader 来处理,小于8kb的直接转为base64 { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'} ] }
如上,”-loader”其实是可以省略不写的,多个 loader 之间用“!”连接起来。
注意所有的加载器都需要通过 npm 来加载,并建议查阅它们对应的 readme 来看看如何使用。
拿最后一个url-loader
来说,它会将样式中引用到的图片转为模块来处理,使用该加载器需要先进行安装:
1 npm install url-loader -save-dev
配置信息的参数“?limit=8192”表示将所有小于 8kb 的图片都转为 base64 形式(其实应该说超过 8kb 的才使用 url-loader 来映射到文件,否则转为 data url 形式) 。
你可以点这里 查阅全部的 loader 列表。 常用的还有: autoprefixer-loader、json-loader
⑷ 最后是 resolve 配置,这块很好理解,直接写注释了:
1 2 3 4 5 6 7 8 9 10 11 12 resolve: { root: 'E:/github/flux-example/src' , extensions: ['' , '.js' , '.json' , '.scss' ], alias: { AppStore : 'js/stores/AppStores.js' , ActionType : 'js/actions/ActionType.js' , AppAction : 'js/actions/AppAction.js' } }
关于 webpack.config.js 更详尽的配置可以参考这里 。
运行 webpack webpack 的执行也很简单,直接执行
1 $ webpack --display-error-details
即可,后面的参数“–display-error-details”是推荐加上的,方便出错时能查阅更详尽的信息(比如 webpack 寻找模块的过程),从而更好定位到问题。
其他主要的参数有:
1 2 3 4 5 6 7 $ webpack --config XXX.js $ webpack -w $ webpack -p $ webpack -d
其中的-p
是很重要的参数,曾经一个未压缩的 700kb 的文件,压缩后直接降到 180kb, 主要是样式这块一句就独占一行脚本,导致未压缩脚本变得很大。
模块引入 上面唠嗑了那么多配置和执行方法,下面开始说说寻常页面和脚本怎么使用呗。
一. HTML 直接在页面引入 webpack 最终生成的页面脚本即可,不用再写什么 data-main 或 seajs.use 了:
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html> <html > <head lang ="en" > <meta charset ="UTF-8" /> <title > demo</title > </head > <body > <script src ="dist/js/page/common.js" > </script > <script src ="dist/js/page/index.js" > </script > </body > </html >
可以看到我们连样式都不用引入,毕竟脚本执行时会动态生成style
并标签打到 head 里。
二. JS 各脚本模块可以直接使用 commonJS 来书写,并可以直接引入未经编译的模块,比如 JSX、sass、coffee 等(只要你在 webpack.config.js 里配置好了对应的加载器)。
我们再看看编译前的页面入口文件(index.jsx):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 require ('../../css/reset.scss' ); require ('../../css/allComponent.scss' ); var React = require ('react' );var AppWrap = require ('../component/AppWrap' ); var createRedux = require ('redux' ).createRedux;var Provider = require ('redux/react' ).Provider;var stores = require ('AppStore' );var redux = createRedux(stores);var App = React.createClass({ render: function ( ) { return ( <Provider redux={redux}> {function ( ) { return <AppWrap > ; }} </Provider > ); } }); React.render( <App>, document .body );
一切就是这么简单么么哒~ 后续各种有的没的,webpack 都会帮你进行处理。
其他 至此我们已经基本上手了 webpack 的使用,下面是补充一些有用的技巧。
一. shimming 在 AMD/CMD 中,我们需要对不符合规范的模块(比如一些直接返回全局变量的插件)进行 shim 处理, 这时候我们需要使用exports-loader 来帮忙:
1 { test : require .resolve("./src/js/tool/swipe.js" ), loader : "exports?swipe" }
之后在脚本中需要引用该模块的时候,这么简单地来使用就可以了:
1 2 require ('./tool/swipe.js' );swipe();
二. 自定义公共模块提取 在文章开始我们使用了 CommonsChunkPlugin 插件来提取多个页面之间的公共模块,并将该模块打包为 common.js 。
但有时候我们希望能更加个性化一些,我们可以这样配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var CommonsChunkPlugin = require ('webpack/lib/optimize/CommonsChunkPlugin' );module .exports = { entry: { p1: './page1' , p2: './page2' , p3: './page3' , ap1: './admin/page1' , ap2: './admin/page2' , }, output: { filename: '[name].js' , }, plugins: [ new CommonsChunkPlugin('admin-commons.js' , ['ap1' , 'ap2' ]), new CommonsChunkPlugin('commons.js' , ['p1' , 'p2' , 'admin-commons.js' ]), ], };
三. 独立打包样式文件 有时候可能希望项目的样式能不要被打包到脚本中,而是独立出来作为.css,然后在页面中以link
标签引入。 这时候我们需要extract-text-webpack-plugin 来帮忙:
1 2 3 4 5 6 7 8 var webpack = require ('webpack' );var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js' );var ExtractTextPlugin = require ("extract-text-webpack-plugin" );module .exports = { plugins: [commonsPlugin, new ExtractTextPlugin("[name].css" )], entry: {
最终 webpack 执行后会乖乖地把样式文件提取出来:
四. 使用 CDN/远程文件 有时候我们希望某些模块走 CDN 并以script
的形式挂载到页面上来加载,但又希望能在 webpack 的模块中使用上。
这时候我们可以在配置文件里使用 externals 属性来帮忙:
1 2 3 4 5 6 7 { externals: { "jquery" : "jQuery" } }
需要留意的是,得确保 CDN 文件必须在 webpack 打包文件引入之前先引入。
我们倒也可以使用 script.js
1 2 3 4 5 6 7 var $script = require ('scriptjs' );$script( '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js' , function ( ) { $('body' ).html('It works!' ); } );
五. 与 grunt/gulp 配合 以 gulp 为示例,我们可以这样混搭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 gulp.task('webpack' , function (callback ) { webpack( { }, function (err, stats ) { if (err) throw new gutil.PluginError('webpack' , err); gutil.log( '[webpack]' , stats.toString({ }) ); callback(); } ); });
当然我们只需要把配置写到 webpack({ … }) 中去即可,无须再写 webpack.config.js 了。
更多参照信息请参阅:grunt 配置 /gulp 配置 。
六. React 相关 ⑴ 推荐使用 npm install react
的形式来安装并引用 React 模块,而不是直接使用编译后的 react.js,这样最终编译出来的 React 部分的脚本会减少 10-20 kb 左右的大小。
⑵ react-hot-loader 是一款非常好用的 React 热插拔的加载插件,通过它可以实现修改-运行同步的效果,配合webpack-dev-server 使用更佳!
1 webpack-dev-server --hot
类似于监听的热插拔功能
七、其他插件 https://github.com/ruanyf/webpack-demos#demo07-uglifyjs-plugin-source 建议:配合 gulp 来使用
polyfill 和 runtime 的区别 ransform-runtime 只会对 es6 的语法进行转换(例如代码块{},class 等等) babel-polyfill,对新 api 进行转换
总结 webpack 就是向 react 这种存在依赖关系的工具才用这种个打包工具(需要 require 这种) 只是解决依赖库的打包,其他的还是用 gulp 来完成的 不过可以都通过这个工具打包成 js 文件,最后用 gulp 来打包成一个 js 就行了
优化体积 参考文章
可视化定位 webpack 大的原因 webpack-bundle-analyze 类似工具webpack-chart 、webpack-analyse
提升构建速度 DllPlugin 和 DllReferencePlugin 提供了以大幅度提高构建时间性能的方式拆分软件包的方法。其中原理是,将特定的第三方 NPM 包模块提前构建 👌,然后通过页面引入。这不仅能够使得 vendor 文件可以大幅度减小,同时,也极大的提高了构件速度。鉴于篇幅,具体用法可参见:webpack.dll.conf.js
外部引入模块(CDN) webpack 可以处理使之不参与打包,而依旧可以在代码中通过 CMD、AMD 或者 window/global 全局的方式访问
1 2 3 4 // webpack 中予以指定 externals: { // 'vue': 'Vue', // 'lodash': '_', 'babel-polyfill': 'window' } // <script src ="//cdn.bootcss.com/autotrack/2.4.1/autotrack.js" > </script > <script src ="//cdn.bootcss.com/babel-polyfill/7.0.0-alpha.15/polyfill.min.js" > </script >
让每个第三包“引有所值” 1.不引入没必要的库–jquery 2.避免类库引用而不用,可以通过 ESlint 来排查规范 3.使用模块化引入 lodash 提供了模块化的引入方式;可按需引入
6 1 2 3 4 5 import { debounce } from 'lodash' import { throttle } from 'lodash' // 改成如下写法 import debounce from 'lodash/debounce' import throttle from 'lodash/throttle'
优化: lodash-webpack-plugin 和 babel-plugin-lodash (组合使用),可将全路径引用的 lodash, 自动转变为模块化按使用引入(如下例示);
6 1 2 3 4 // 引入组件,自动转换 import _ from 'lodash' _.debounce() _.throttle()
还是不够快捷,每个用到的文件,都写一遍 import,实在多有不便。 更可取的是,将项目所需的方法,统一引入,按需添加,组建出本地 lodash 类库,然后 export 给框架层(比如 Vue.prototype),以便全局使用
6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // helper 文件夹下 lodash,统一引入你需要的方法 import _ from 'lodash' export default { cloneDeep: _.cloneDeep, debounce: _.debounce, throttle: _.throttle, size: _.size, pick: _.pick, isEmpty: _.isEmpty } // 注入到全局 import _ from '@helper/lodash.js' Vue.prototype.$_ = _ // 重点语句 // vue 组件内运用 this.$_.debounce()
4.引入更合适的包 moment 太大可以改用date-fns 现代 JavaScript 日期实用程序库,如 lodash 一样,可支持模块化
按需异步加载模块 6 1 import Foo from './Foo.vue'
改为如下写法:
6 1 const Foo = () => import('./Foo.vue')
生产环境,压缩混淆并移除 console 使用 UglifyJsPlugin 插件来压缩代码,加入如下配置,即可移除掉代码中的 console
6 1 2 3 4 5 6 7 8 new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, drop_console: true, pure_funcs: ['console.log'] }, sourceMap: false })
Webpack3 新功能: Scope Hoisting 又译作“作用域提升”。 只需在配置文件中添加一个新的插件,就可以让 Webpack 打包出来的代码文件更小、运行的更快:
6 1 2 3 4 5 module.exports = { plugins: [ new webpack.optimize.ModuleConcatenationPlugin() ] }
据悉这个 Scope Hoisting 与 Tree Shaking,最初都是由 Rollup 实现的.
模块化原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 var installedModules = {};function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = (installedModules[moduleId] = { i: moduleId, l: false , exports: {}, }); modules[moduleId].call( module .exports, module , module .exports, __webpack_require__ ); module .l = true ; return module .exports; } return __webpack_require__((__webpack_require__.s = 0 ));[ function (module, exports, __webpack_require__ ) { 'use strict' ; var bar = __webpack_require__(1 ); bar(); }, function (module, exports, __webpack_require__ ) { 'use strict' ; exports.bar = function ( ) { return 1 ; }; }, ];
参考文章 1 参考文章 2
Webpack 优化 文件模块化显示 harmony 1 2 3 4 build: { bundleAnalyzerReport: true ; }
重复块优化 1. 优化配置
升级webpack 3
,优化 js 的编译能力(Scope Hoisting)
1 2 plugins: [new webpack.optimize.ModuleConcatenationPlugin()];
合理规划 entry
入口配置(平衡 vendor.js, app.js 文件的大小)
1 2 3 4 5 6 7 8 9 10 11 12 entry: { vendor:['vue' , 'vue-router' , 'vue-resource' ], app: './src/main.js' } plugins:[ new webpack.optimize.CommonsChunkPlugin({ name: ['manifest' ,'vendor' ].reverse(), minChunks:Infinity }) ]
2. 减小代码量
提取 chunk
中使用的公共库(能为 chunk 代码节约近 1/3 的代码量)
1 2 3 4 5 6 7 8 9 new webpack.optimize.CommonsChunkPlugin({ name: 'app' , async : 'vendor-async' , children: true , minChunks: (module , count ) => { return count >= 3 ; }, });
减少图片 base64 的使用,降低 限制,限制 2k(vue 官方配置是 10k,会大大增加 js 文件体积,移动端对 base64 的解析成本高)
1 2 3 4 5 6 7 8 { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/ , loader: 'url-loader' , options: { limit: 2048 , name: utils.assetsPath('img/[name].[hash:7].[ext]' ) } }
1 2 3 4 5 6 7 8 9 10 11 12 resolve: { alias: { 'vue' : 'vue/dist/vue.esm.js' } } resolve: { alias: { 'vue' : 'vue/dist/vue.min.js' } }
不要再引入 babel-polyfill
(随着 es6,es7api 越来越多体积越来越大,目前 120 多 k,两月前 80k)
1 2 3 4 entry: { vendor:['babel-polyfill' , 'vue' , 'vue-router' , 'vue-resource' ], app: './src/main.js' }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 var OptimizeCSSPlugin = require ('optimize-css-assets-webpack-plugin' );plugins: [ new webpack.optimize.UglifyJsPlugin({ beautify: false , comments: false , compress: { warnings: false , drop_console: true , collapse_vars: true , reduce_vars: true , }, sourceMap: true , }), new OptimizeCSSPlugin({ cssProcessorOptions: { safe: true , discardComments: { removeAll : true }, }, }), ];
1 2 plugins: [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/ , /zh|en/)];
2. 减少请求数:
manifest.js 文件 内联(app.css 可以自行选择,当小于 10k 是最好内联),webpack 推荐配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 var HtmlWebpackInlineSourcePlugin = require ('html-webpack-inline-source-plugin' );plugins: [ new HtmlWebpackPlugin({ inlineSource: /(app\.(.+)?\.css|manifest\.(.+)?\.js)$/ , }), new HtmlWebpackInlineSourcePlugin(), ];
开发插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function MyPlugin (options ) { } MyPlugin.prototype.apply = function (compiler ) { compiler.plugin('compilation' , function (compilation ) { console .log('The compiler is starting a new compilation...' ); compilation.plugin( 'html-webpack-plugin-before-html-processing' , function (htmlPluginData, callback ) { htmlPluginData.html += 'The magic footer' ; callback(null , htmlPluginData); } ); }); }; module .exports = MyPlugin;
Vue CLI3 相关配置 深入浅析 vue-cli@3.0 使用及配置说明 关于 Preload, 你应该知道些什么?