如今随着前端开发的复杂度和规模愈来愈大,鹰不能抛开工程化来独立开发,好比:react的jsx代码必须编译后才能在浏览器中使用,好比sass和less代码浏览器是不支持的。若是摒弃这些开发框架,开发效率会大幅降低。css
在众多前端工程化工具中,webpack脱颖而出成为了当今最流行的前端构建工具。html
知其然知其因此然。前端
(1)entry:一个可执行模块或者库的入口。vue
(2)chunk:多个文件组成一个代码块。能够将可执行的模块和他所依赖的模块组合成一个chunk,这是打包。node
(3)loader:文件转换器。例如把es6转为es5,scss转为css等react
(4)plugin:扩展webpack功能的插件。在webpack构建的生命周期节点上加入扩展hook,添加功能。webpack
从启动构建到输出结果一系列过程:git
(1)初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,造成最后的配置结果。es6
(2)开始编译:上一步获得的参数初始化compiler对象,注册全部配置的插件,插件监听webpack构建生命周期的事件节点,作出相应的反应,执行对象的 run 方法开始执行编译。github
(3)肯定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。
(4)编译模块:递归中根据文件类型和loader配置,调用全部配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到全部入口依赖的文件都通过了本步骤的处理。
(5)完成模块编译并输出:递归完过后,获得每一个文件结果,包含每一个模块以及他们之间的依赖关系,根据entry配置生成代码块chunk。
(6)输出完成:输出全部的chunk到文件系统。
注意:在构建生命周期中有一系列插件在作合适的时机作合适事情,好比UglifyPlugin会在loader转换递归完对结果使用UglifyJs压缩覆盖以前的结果。
一个单页应用须要配置一个entry指明执行入口,web-webpack-plugin里的WebPlugin能够自动的完成这些工做:webpack会为entry生成一个包含这个入口的全部依赖文件的chunk,可是还须要一个html来加载chunk生成的js,若是还提取出css须要HTML文件中引入提取的css。
一个简单的webpack配置文件栗子
const { WebPlugin } = require('web-webpack-plugin'); module.exports = { entry: { app: './src/doc/index.js', home: './src/doc/home.js' }, plugins: [ // 一个WebPlugin对应生成一个html文件 new WebPlugin({ //输出的html文件名称 filename: 'index.html', //这个html依赖的`entry` requires: ['app','home'], }), ], };
说明:require: ['app', 'home']指明这个html依赖哪些entry,entry生成的js和css会自动注入到html中。
还支持配置这些资源注入方式,支持以下属性:
(1)_dist只有在生产环境中才引入的资源;
(2)_dev只有在开发环境中才引入的资源;
(3)_inline把资源的内容潜入到html中;
(4)_ie只有IE浏览器才须要引入的资源。
这些属性能够经过在js里配置,看个简单例子:
new WebPlugin({ filename: 'index.html', requires: { app:{ _dist:true, _inline:false, } }, }),
这些属性还能够在模板中设置,使用模板好处就是能够灵活的控制资源的注入点。
new WebPlugin({ filename: 'index.html', template: './template.html', }),
//template模板 <!DOCTYPE html> <html lang="zh-cn"> <head> <link rel="stylesheet" href="app?_inline"> <script src="ie-polyfill?_ie"></script> </head> <body> <div id="react-body"></div> <script src="app"></script> </body> </html>
WebPlugin插件借鉴了fis3的思想,补足了webpack缺失的以HTML为入口的功能。想了解WebPlugin的更多功能,见文档。
一个项目中会包含多个单页应用,虽然多个单页面应用能够合成一个,可是这样作会致使用户没有访问的部分也加载了,若是项目中有不少的单页应用。为每个单页应用配置一个entry和WebPlugin?若是又新增,又要新增webpack配置,这样作麻烦,这时候有一个插件web-webpack-plugin里的AutoWebPlugin方法能够解决这些问题。
module.exports = { plugins: [ // 全部页面的入口目录 new AutoWebPlugin('./src/'), ] };
分析:一、AutoWebPlugin会把./src/目录下全部每一个文件夹做为一个单页页面的入口,自动为全部的页面入口配置一个WebPlugin输出对应的html。
二、要新增一个页面就在./src/下新建一个文件夹包含这个单页应用所依赖的代码,AutoWebPlugin自动生成一个名叫文件夹名称的html文件。
一个好的代码分割对浏览器首屏效果提高很大。
最多见的react体系:
(1)先抽出基础库react react-dom redux react-redux到一个单独的文件而不是和其它文件放在一块儿打包为一个文件,这样作的好处是只要你不升级他们的版本这个文件永远不会被刷新。若是你把这些基础库和业务代码打包在一个文件里每次改动业务代码都会致使文件hash值变化从而致使缓存失效浏览器重复下载这些包含基础库的代码。因此把基础库打包成一个文件。
// vender.js 文件抽离基础库到单独的一个文件里防止跟随业务代码被刷新 // 全部页面都依赖的第三方库 // react基础 import 'react'; import 'react-dom'; import 'react-redux'; // redux基础 import 'redux'; import 'redux-thunk'; // webpack配置 { entry: { vendor: './path/to/vendor.js', }, }
(2)经过CommonsChunkPlugin能够提取出多个代码块都依赖的代码造成一个单独的chunk。在应用有多个页面的场景下提取出全部页面公共的代码减小单个页面的代码,在不一样页面之间切换时全部页面公共的代码以前被加载过而没必要从新加载。因此经过CommonsChunkPlugin能够提取出多个代码块都依赖的代码造成一个单独的chunk。
服务端渲染的代码要运行在nodejs环境,和浏览器不一样的是,服务端渲染代码须要采用commonjs规范同时不该该包含除js以外的文件好比css。
webpack配置以下:
module.exports = { target: 'node', entry: { 'server_render': './src/server_render', }, output: { filename: './dist/server/[name].js', libraryTarget: 'commonjs2', }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', }, { test: /\.(scss|css|pdf)$/, loader: 'ignore-loader', }, ] }, };
分析一下:
(1)target: 'node'指明构建出代码要运行在node环境中。
(2)libraryTarget: 'commonjs2' 指明输出的代码要是commonjs规范。
(3){test: /.(scss|css|pdf)$/,loader: 'ignore-loader'} 是为了防止不能在node里执行服务端渲染也用不上的文件被打包进去。
fis3和webpack有不少类似地方也有不一样的地方,类似地方:都采用commonjs规范,不一样地方:导入css这些非js资源的方式。
fis3经过@require './index.scss',而webpack是经过require('./index.scss')。
若是想把fis3平滑迁移到webpack,可使用comment-require-loader。
好比:你想在webpack构建是使用采用了fis3方式的imui模块
loaders:[{
test: /\.js$/, loaders: ['comment-require-loader'], include: [path.resolve(__dirname, 'node_modules/imui'),]
}]
若是你在社区找不到你的应用场景的解决方案,那就须要本身动手了写loader或者plugin了。
在你编写自定义webpack扩展前你须要想明白究竟是要作一个loader仍是plugin呢?能够这样判断:
若是你的扩展是想对一个个单独的文件进行转换那么就编写loader剩下的都是plugin。
其中对文件进行转换能够是像:
一、babel-loader把es6转为es5;
二、file-loader把文件替换成对应的url;
三、raw-loader注入文本文件内容到代码中。
一、编写webpack loader
编写loader很是简单,以comment-require-loader为例:
module.exports = function (content) { return replace(content); };
loader的入口须要导出一个函数,这个函数要干的事情就是转换一个文件的内容。
函数接收的参数content是一个文件在转换前的字符串形式内容,须要返回一个新的字符串形式内容做为转换后的结果,全部经过模块化倒入的文件都会通过loader。从这里能够看出loader只能处理一个个单独的文件而不能处理代码块。能够参考官方文档
plugin应用场景普遍,因此稍微复杂点。以end-webpack-plugin为例:
class EndWebpackPlugin { constructor(doneCallback, failCallback) { this.doneCallback = doneCallback; this.failCallback = failCallback; } apply(compiler) { // 监听webpack生命周期里的事件,作相应的处理 compiler.plugin('done', (stats) => { this.doneCallback(stats); }); compiler.plugin('failed', (err) => { this.failCallback(err); }); } } module.exports = EndWebpackPlugin;
loader的入口须要导出一个class,在new EndWebpackPlugin()的时候经过构造函数传入这个插件须要的参数,在webpack启动的时候会先实例化plugin,再调用plugin的apply方法,插件在apply函数里监听webpack生命周期里的事件,作相应的处理。
webpack plugin的两个核心概念:
(1)compiler:从webpack启动到退出只存在一个Compiler,compiler存放着webpack的配置。
(2)compilation:因为webpack的监听文件变化自动编译机制,compilation表明一次编译。
Compiler 和 Compilation 都会广播一系列事件。webpack生命周期里有很是多的事件
以上只是一个最简单的demo,更复杂的能够查看 how to write a plugin或参考web-webpack-plugin。
webpack其实比较简单,用一句话归纳本质:
webpack是一个打包模块化js的工具,能够经过loader转换文件,经过plugin扩展功能。
若是webpack让你感到复杂,必定是各类loader和plugin的缘由。
三者都是前端构建工具,grunt和gulp在早期比较流行,如今webpack相对来讲比较主流,不过一些轻量化的任务仍是会用gulp来处理,好比单独打包CSS文件等。
grunt和gulp是基于任务和流(Task、Stream)的。相似jQuery,找到一个(或一类)文件,对其作一系列链式操做,更新流上的数据, 整条链式操做构成了一个任务,多个任务就构成了整个web的构建流程。
webpack是基于入口的。webpack会自动地递归解析入口所须要加载的全部资源文件,而后用不一样的Loader来处理不一样的文件,用Plugin来扩展webpack功能。
总结:(1)从构建思路来讲:gulp和grunt须要开发者将整个前端构建过程拆分红多个Task
,并合理控制全部Task
的调用关系 webpack须要开发者找到入口,并须要清楚对于不一样的资源应该使用什么Loader作何种解析和加工;
(2)对于知识背景:gulp更像后端开发者的思路,须要对于整个流程了如指掌 webpack更倾向于前端开发者的思路。
一样是基于入口的打包工具还有如下几个主流的:webpack,rollup,parcel。
从应用场景上来看:(1)webpack适合大型复杂的前端站点构建;(2)rollup适合基础库的打包,好比vue,react;(3)parcel适用于简单的实验室项目,可是打包出错很难调试。
(1)babel-loader:把es6转成es5;
(2)css-loader:加载css,支持模块化,压缩,文件导入等特性;
(3)style-loader:把css代码注入到js中,经过dom操做去加载css;
(4)eslint-loader:经过Eslint检查js代码;
(5)image-loader:加载而且压缩图片晚间;
(6)file-loader:文件输出到一个文件夹中,在代码中经过相对url去引用输出的文件;
(7)url-loader:和file-loader相似,文件很小的时候能够base64方式吧文件内容注入到代码中。
(8)source-map-loader:加载额外的source map文件,方便调试。
(1)uglifyjs-webpack-plugin:经过UglifyJS去压缩js代码;
(2)commons-chunk-plugin:提取公共代码;
(3)define-plugin:定义环境变量。
做用不一样:(1)loader让webpack有加载和解析非js的能力;(2)plugin能够扩展webpack功能,在webpack运行周期中会广播不少事件,Plugin能够监听一些事件,经过webpack的api改变结果。
用法不一样:(1)loader在module.rule中配置。类型为数组,每一项都是Object;(2)plugin是单独配置的,类型为数组,每一项都是plugin实例,参数经过构造函数传入。
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行如下流程:
(1)初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
(2)开始编译:用上一步获得的参数初始化 Compiler 对象,加载全部配置的插件,执行对象的 run 方法开始执行编译;
(3)肯定入口:根据配置中的 entry 找出全部的入口文件;
(4)编译模块:从入口文件出发,调用全部配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到全部入口依赖的文件都通过了本步骤的处理;
(5)完成模块编译:在通过第4步使用 Loader 翻译完全部模块后,获得了每一个模块被翻译后的最终内容以及它们之间的依赖关系;
(6)输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每一个 Chunk 转换成一个单独的文件加入到输出列表,这步是能够修改输出内容的最后机会;
(7)输出完成:在肯定好输出内容后,根据配置肯定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程当中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,而且插件能够调用 Webpack 提供的 API 改变 Webpack 的运行结果。
编写Loader时要遵循单一原则,每一个Loader只作一种"转义"工做。 每一个Loader的拿到的是源文件内容(source),能够经过返回值的方式将处理后的内容输出,也能够调用this.callback()方法,将内容返回给webpack。 还能够经过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。
Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 能够监听这些事件,在合适的时机经过 Webpack 提供的 API 改变输出结果。
webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制能够作到不用刷新浏览器而将新变动的模块替换掉旧的模块。
原理:
分析:
(1)第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包,并将打包后的代码经过简单的 JavaScript 对象保存在内存中。
(2)第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,而且告诉 webpack,将代码打包到内存中。
(3)第三步是 webpack-dev-server 对文件变化的一个监控,这一步不一样于第一步,并非监控代码变化从新打包。当咱们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
(4)第四步也是 webpack-dev-server 代码的工做,该步骤主要是经过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间创建一个 websocket 长链接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不一样的操做。固然服务端传递的最主要信息仍是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
(5)webpack-dev-server/client 端并不可以请求更新的代码,也不会执行热更模块操做,而把这些工做又交回给了 webpack,webpack/hot/dev-server 的工做就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢仍是进行模块热更新。固然若是仅仅是刷新浏览器,也就没有后面那些步骤了。
(6)HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它经过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了全部要更新的模块的 hash 值,获取到更新列表后,该模块再次经过 jsonp 请求,获取到最新的模块代码。这就是上图中 七、八、9 步骤。
(7)而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
(8)最后一步,当 HMR 失败后,回退到 live reload 操做,也就是进行浏览器刷新来获取最新打包代码。
用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。
(1)压缩代码。删除多余的代码、注释、简化代码的写法等等方式。能够利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css。使用webpack4,打包项目使用production模式,会自动开启代码压缩。
(2)利用CDN加速。在构建过程当中,将引用的静态资源路径修改成CDN上对应的路径。能够利用webpack对于output参数和各loader的publicPath参数来修改资源路径
(3)删除死代码(Tree Shaking)。将代码中永远不会走到的片断删除掉。能够经过在启动webpack时追加参数--optimize-minimize来实现或者使用es6模块开启删除死代码。
(4)优化图片,对于小图可使用 base64 的方式写入文件中
(5)按照路由拆分代码,实现按需加载,提取公共代码。
(6)给打包出来的文件名添加哈希,实现浏览器缓存文件
(1)多入口的状况下,使用commonsChunkPlugin来提取公共代码;
(2)经过externals配置来提取经常使用库;
(3)使用happypack实现多线程加速编译;
(4)使用webpack-uglify-parallel来提高uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用多核并行压缩来提高压缩速度;
(5)使用tree-shaking和scope hoisting来剔除多余代码。
单页应用能够理解为webpack的标准模式,直接在entry中指定单页应用的入口便可。
多页应用的话,可使用webpack的 AutoWebPlugin来完成简单自动化的构建,可是前提是项目的目录结构必须遵照他预设的规范。
NPM模块须要注意如下问题:
(1)要支持CommonJS模块化规范,因此要求打包后的最后结果也遵照该规则
(2)Npm模块使用者的环境是不肯定的,颇有可能并不支持ES6,因此打包的最后结果应该是采用ES5编写的。而且若是ES5是通过转换的,请最好连同SourceMap一同上传。
(3)Npm包大小应该是尽可能小(有些仓库会限制包大小)
(4)发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样能够避免模块应用者再次打包时出现底层模块被重复打包的状况。
(5)UI组件类的模块应该将依赖的其它资源文件,例如.css文件也须要包含在发布的模块里。
基于以上须要注意的问题,咱们能够对于webpack配置作如下扩展和优化:
(1)CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用;
(2)输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再经过开启devtool: 'source-map'输出SourceMap以发布调试。
(3)Npm包大小尽可能小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终致使每一个输出的文件中都包含这段辅助函数的代码,形成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
(4)不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不须要打包。
(5)对于依赖的资源文件打包的解决方案:经过css-loader和extract-text-webpack-plugin来实现,配置以下:
常常会引入现成的UI组件库如ElementUI、iView等,可是他们的体积和他们所提供的功能同样,是很庞大的。
不过不少组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,便可实现组件按需加载了。
单页应用的按需加载 如今不少前端项目都是经过单页应用的方式开发的,可是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会愈来愈多,影响用户的体验。
一、https://github.com/webpack/do...
二、https://webpack.js.org/api/co...
三、https://webpack.js.org/concep...
四、https://webpack.js.org/concep...
五、手把手教你撸一个简易的 webpack