准备了挺久,一直想要好好深刻了解一下Webpack,以前一直嫌弃Webpack麻烦,偏向于Parcel这种零配置的模块打包工具一些,可是实际上仍是Webpack比较靠谱,而且Webpack功能更增强大。因为上一次学习Webpack的时候并无了解过Node.js,因此不少时候真的感受无能为力,连个__dirname
都以为好复杂,学习过Node.js以后再来学习Webpack,就会好理解不少,这一次算是比较深刻的了解一下Webpack,争取之后可以脱离create-react-app
或者Vue-Cli
这种脚手架工具,或者本身也可以写一套脚本自动配置开发环境。javascript
因为写这篇笔记的时候,Webpack已经发行了最新的Webpack 4.0,因此这篇笔记就算是学习Webpack 4.0的笔记吧,笔者所用版本是webpack 4.8.3,另外使用Webpack 4.x的命令行须要安装单独的命令行工具,笔者所使用的Webpack命令行工具是webpack-cli 2.1.3,学习的时候能够按照这个要求部署开发环境。css
此外,在学习webpack以前,你最好对ES六、Node.js有必定的了解,最好使用过一个脚手架。html
Webpack具备四个核心的概念,想要入门Webpack就得先好好了解这四个核心概念。它们分别是Entry(入口)
、Output(输出)
、loader
和Plugins(插件)
。接下来详细介绍这四个核心概念。java
Entry是Webpack的入口起点指示,它指示webpack应该从哪一个模块开始着手,来做为其构建内部依赖图的开始。能够在配置文件(webpack.config.js)中配置entry属性来指定一个或多个入口点,默认为./src
(webpack 4开始引入默认值)。react
具体配置方法:webpack
entry: string | Array<string>
前者一个单独的string是配置单独的入口文件,配置为后者(一个数组)时,是多文件入口。git
另外还能够经过对象语法进行配置:github
entry: { [entryChunkName]: string | Array<string> }
好比:web
//webpack.config.js module.exports = { entry: { app: './app.js', vendors: './vendors.js' } };
以上配置表示从app和vendors属性开始打包构建依赖树,这样作的好处在于分离本身开发的业务逻辑代码和第三方库的源码,由于第三方库安装后,源码基本就再也不变化,这样分开打包有利于提高打包速度,减小了打包文件的个数,Vue-Cli
采起的就是这种分开打包的模式。可是为了支持拆分代码更好的DllPlugin插件,以上语法可能会被抛弃。正则表达式
Output属性告诉webpack在哪里输出它所建立的bundles,也可指定bundles的名称,默认位置为./dist
。整个应用结构都会被编译到指定的输出文件夹中去,最基本的属性包括filename
(文件名)和path
(输出路径)。
值得注意的是,便是你配置了多个入口文件,你也只能有一个输出点。
具体配置方法:
output: { filename: 'bundle.js', path: '/home/proj/public/dist' }
值得注意的是,output.filename
必须是绝对路径,若是是一个相对路径,打包时webpack会抛出异常。
多个入口时,使用下面的语法输出多个bundle:
// webpack.config.js module.exports = { entry: { app: './src/app.js', vendors: './src/vendors.js' }, output: { filename: '[name].js', path: __dirname + '/dist' } }
以上配置将会输出打包后文件app.js和vendors.js到__dirname + '/dist'
下。
loader能够理解为webpack的编译器,它使得webpack能够处理一些非JavaScript文件,好比png、csv、xml、css、json等各类类型的文件,使用合适的loader可让JavaScript的import导入非JavaScript模块。JavaScript只认为JavaScript文件是模块,而webpack的设计思想即万物皆模块,为了使得webpack可以认识其余“模块”,因此须要loader这个“编译器”。
webpack中配置loader有两个目标:
好比webpack.config.js:
module.exports = { entry: '...', output: '...', module: { rules: [ { test: /\.css$/, use: 'css-loader' } ] } };
该配置文件指示了全部的css文件在import
时都应该通过css-loader处理,通过css-loader处理后,能够在JavaScript模块中直接使用import
语句导入css模块。可是使用css-loader
的前提是先使用npm安装css-loader
。
此处须要注意的是定义loaders规则时,不是定义在对象的rules属性上,而是定义在module属性的rules属性中。
配置多个loader:
有时候,导入一个模块可能要先使用多个loader进行预处理,这时就要对指定类型的文件配置多个loader进行预处理,配置多个loader,把use属性赋值为数组便可,webpack会按照数组中loader的前后顺序,使用对应的loader依次对模块文件进行预处理。
{ module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' } ] } ] } }
此外,还可使用内联方式进行loader配置:
import Styles from 'style-loader!css-loader?modules!./style.css'
可是这不是推荐的方法,请尽可能使用module.rules
进行配置。
loader用于转换非JavaScript类型的文件,而插件能够用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,功能十分强大。要是用一个插件,通常是先使用npm包管理器进行安装,而后在配置文件中引入,最后将其实例化后传递给plugins数组属性。
插件是webpack的支柱功能,目前主要是解决loader没法实现的其余许多复杂功能,经过plugins
属性使用插件:
// webpack.config.js const webpack = require('webpack'); module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin() ] }
向plugins属性传递实例数组便可。
模式(Mode)能够经过配置对象的mode
属性进行配置,主要值为production
或者development
。两种模式的区别在于一个是为生产环境编译打包,一个是为了开发环境编译打包。生产环境模式下,webpack会自动对代码进行压缩等优化,省去了配置的麻烦。
学习完以上基本概念以后,基本也就入门webpack了,由于webpack的强大就是创建在这些基本概念之上,利用webpack多样的loaders和plugins,能够实现强大的打包功能。
按照如下步骤实现webpack简单的打包功能:
(1)创建工程文件夹,位置和名称随意,并将cmd或者git bash的当前路径切换到工程文件夹。
(2)安装webpack和webpack-cli到开发环境:
npm install webpack webpack-cli --save-dev
(3)在工程文件夹下创建如下文件和目录:
(4)安装css-loader
:
npm install css-loader --save-dev
(5)配置webpack.config.js
:
module.exports = { mode: 'development', entry: './src/index.js', output: { path: __dirname + '/dist', filename: 'bundle.js' }, module: { rules: [ { test: /\.css$/, use: 'css-loader' } ] } };
(6)在index.html
中引入bundle.js
:
<!--index.html--> <html> <head> <title>Test</title> <meta charset='utf-8'/> </head> <body> <h1>Hello World!</h1> </body> <script src='./bundle.js'></script> </html>
(7)在index.js
中添加:
import './index.css'; console.log('Success!');
(8)在工程目录下,使用如下命令打包:
webpack
查看输出结果,能够双击/dist/index.html
查看有没有报错以及控制台的输出内容。
webpack提供Node API,方便咱们在Node脚本中使用webpack。
基本代码以下:
// 引入webpack模块。 const webpack = require('webpack'); // 引入配置信息。 const config = require('./webpack.config'); // 经过webpack函数直接传入config配置信息。 const compiler = webpack(config); // 经过compiler对象的apply方法应用插件,也可在配置信息中配置插件。 compiler.apply(new webpack.ProgressPlugin()); // 使用compiler对象的run方法运行webpack,开始打包。 compiler.run((err, stats) => { if(err) { // 回调中接收错误信息。 console.error(err); } else { // 回调中接收打包成功的具体反馈信息。 console.log(stats); } });
动态生成是啥?动态生成就是指在打包后的模块名称内插入hash值,使得每一次生成的模块具备不一样的名称,而index.html之因此要动态生成是由于每次打包生成的模块名称不一样,因此在HTML文件内引用时也要更改script标签,这样才能保证每次都能引用到正确的JavaScript文件。
为何要添加hash值?
之因此要动态生态生成bundle文件,是为了防止浏览器缓存机制阻碍文件的更新,在每次修改代码以后,文件名中的hash都会发生改变,强制浏览器进行刷新,获取当前最新的文件。
如何添加hash到bundle文件中?
只须要在设置output时,在output.filename
中添加[hash]
到文件名中便可,好比:
// webpack.config.js module.exports = { output: { path: __dirname + '/dist', filename: '[name].[hash].js' } };
如今能够动态生成bundle文件了,那么如何动态添加bundle到HTML文件呢?
每次打包bundle文件以后,其名称都会发生更改,每次人为地修改对应的HTML文件以添加JavaScript文件引用实在是使人烦躁,这时须要使用到强大的webpack插件了,有一个叫html-webpack-plugin
的插件,能够自动生成HTML文件。安装到开发环境:
npm install html-webpack-plugin --save-dev
安装以后,在webpack.config.js
中引入,并添加其实例到插件属性(plugins)中去:
// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // other configs ... plugins: [ new HtmlWebpackPlugin({ // options配置 }) ] };
这时就能够看到每次生成bundle文件以后,都会被动态生成对应的html文件。
在上面的代码中还能够看到HtmlWebpackPlugin
插件的构造函数还能够传递一个配置对象做为参数。比较有用的配置属性有title
(指定HTML中title标签的内容,及网页标题)、template
(指定模板HTML文件)等等,其余更多具体参考信息请访问:Html-Webpack-Plugin
因为每次生成的JavaScript文件都不一样名,因此新的文件不会覆盖旧的文件,而旧的文件一只会存在于/dist
文件夹中,随着编译次数的增长,这个文件夹会愈来愈膨胀,因此应该想办法每次生成新的bundle文件以前清理/dist
文件夹,以确保文件夹的干净整洁,有如下两个较好的处理办法:
若是你是Node脚本调用webpack打包:
若是经过Node API调用webpack进行打包,能够在打包以前直接使用Node的fs模块删除/dist
文件夹中的全部文件:
const webpack = require('webpack'); const config = require('./webpack.config'); const fs = require('fs'); const compiler = webpack(config); var deleteFolderRecursive = function(path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(function(file, index){ var curPath = path + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { // recurse deleteFolderRecursive(curPath); } else { // delete file fs.unlinkSync(curPath); } }); fs.rmdirSync(path); } }; deleteFolderRecursive(__dirname + '/dist'); compiler.run((err, stats) => { if(err) { console.error(err); } else { console.log(stats.hash); } });
能够看到在调用compiler.run
打包以前,先使用自定义的deleteFolderRecursive
方法删除了/dist
目录下的全部文件。
若是你使用webpack-cli进行打包
这时候就得经过webpack的插件完成这个任务了,用到的插件是clean-webpack-plugin
。
安装:
npm install clean-webpack-plugin --save-dev
而后在webpack.config.js
文件中添加插件:
// webpack.config.js const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { plugins: [ new CleanWebpackPlugin(['dist']) ] };
以后再次打包,你会发现以前的打包文件所有被删除了。
开发环境与生产环境存在许多的差别,生产环境更讲究生产效率,所以代码必须压缩、精简,必须去除一些生产环境并不须要用到的调试工具,只须要提升应用的效率和性能便可。开发环境更讲究调试、测试,为了方便开发,咱们须要搭建一个合适的开发环境。
为什么要使用source maps?
由于webpack对源代码进行打包后,会对源代码进行压缩、精简、甚至变量名替换,在浏览器中,没法对代码逐行打断点进行调试,全部须要使用source maps进行调试,它使得咱们在浏览器中能够看到源代码,进而逐行打断点调试。
如何使用source maps?
在配置中添加devtool
属性,赋值为source-map
或者inline-source-map
便可,后者报错信息更加具体,会指示源代码中的具体错误位置,而source-map
选项没法指示到源代码中的具体位置。
每次写完代码保存以后还须要手动输入命令或启动Node脚本进行编译是一件使人不胜其烦的事情,选择一下工具能够简化开发过程当中的工做:
(1)使用watch模式
在使用webpack-cli
进行打包时,经过命令webpack --watch
便可开启watch模式,进入watch模式以后,一旦依赖树中的某一个模块发生了变化,webpack就会从新进行编译。
(2)使用webpack-dev-server
使用过create-react-app
或者Vue-Cli
这种脚手架的童鞋都知道,经过命令npm run start
便可创建一个本地服务器,而且webpack会自动打开浏览器打开你正在开发的页面,而且一旦你修改了文件,浏览器会自动进行刷新,基本作到了所见即所得的效果,比webpack的watch模式更加方便给力。
使用方法:
① 安装webpack-dev-server:
npm install --save-dev webpack-dev-server
② 修改配置文件,添加devServer属性:
// webpack.config.js module.exports = { devServer: { contentBase: './dist' } };
③ 添加命令属性到package.json
:
// package.json { "scripts": { "start": "webpack-dev-server --open" } }
④ 运行命令
npm run start
能够看到浏览器打开后的实际效果,尝试修改文件,查看浏览器是否实时更新。
此外还能够再devServer属性下指定更多的配置信息,好比开发服务器的端口、热更新模式、是否压缩等等,具体查询:Webpack
经过Node API使用webpack-dev-server
:
'use strict'; const Webpack = require('webpack'); const WebpackDevServer = require('../../../lib/Server'); const webpackConfig = require('./webpack.config'); const compiler = Webpack(webpackConfig); const devServerOptions = Object.assign({}, webpackConfig.devServer, { stats: { colors: true } }); const server = new WebpackDevServer(compiler, devServerOptions); server.listen(8080, '127.0.0.1', () => { console.log('Starting server on http://localhost:8080'); });
(3)使用webpack-dev-middleware
webpack-dev-middleware
是一个比webpack-dev-server
更加基础的插件,webpack-dev-server
也使用了这个插件,因此能够理解为webpack-dev-middleware
的封装层次更低,使用起来更加复杂,可是低封装性意味着较高的自定义性,使用webpack-dev-middleware
能够定义更多的设置来知足更多的开发需求,它基于express模块。
这一块不作过多介绍,由于webpack-dev-server
已经可以应付大多数开发场景,不用再设置更多的express属性了,想要详细了解的童鞋能够了解:使用 webpack-dev-middleware
(4)设置IDE
某些IDE具备安全写入功能,致使开发服务器运行时IDE没法保存文件,此时须要进行对应的设置。
具体参考:调整文本编辑器
热模块替换(Hot Module Replacement,HMR),表明在应用程序运行过程当中替换、添加、删除模块,浏览器无需刷新页面便可呈现出相应的变化。
使用方法:
(1)在devServer属性中添加hot属性并赋值为true:
// webpack.config.js module.exports = { devServer: { hot: true } }
(2)引入两个插件到webpack配置文件:
// webpack.config.js const webpack = require('webpack'); module.exports = { devServer: { hot: true }, plugins: [ new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ] };
(3)在入口文件底部添加代码,使得在全部代码发生变化时,都可以通知webpack:
if (module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated intMe module!'); printMe(); }) }
热模块替换比较难以掌控,容易报错,推荐在不一样的开发配置下使用不一样的loader简化HMR过程。具体参考:其余代码和框架
生产环境要求代码精简、性能优异,而开发要求开发快速、测试方便,代码不要求简洁,因此两种环境下webpack打包的目的也不相同,因此最好将两种环境下的配置文件分开来。对于分开的配置文件,在使用webpack时仍是要对其中的配置信息进行整合,webpack-merge
是一个不错的整合工具(Vue-Cli也有使用到)。
使用方法:
(1)安装webpack-merge:
npm install webpack-merge --save-dev
(2)创建三个配置文件:
其中,webpack.base.conf.js
表示最基础的配置信息,开发环境和生产环境都须要设置的信息,好比entry
、output
、module
等。在另外两个文件中配置一些对应环境下特有的信息,而后经过webpack-merge
模块与webpack.base.conf.js
整合。
(3)添加npm scripts:
// package.json { "scripts": { "start": "webpack-dev-server --open --config webpack.dev.conf.js", "build": "webpack --config webpack.prod.conf.js" } }
此外,建议设置mode属性,由于生产环境下会自动开启代码压缩,免去了配置的麻烦。
TreeShaking表示移除JavaScript文件中的未使用到的代码,webpack 4加强了这一部分的功能。经过配置package.json的sideEffects属性,能够指定哪些文件能够移除多余代码。若是sideEffects设置为false,那么表示文件中的未使用代码能够放心移除,没有反作用。若是有些文件中的冗余代码不能被移除,那么能够设置sideEffects属性为一个数组,数组内容为文件的路径字符串。
指定无反作用的文件以后,设置mode为"production",再次构建代码,能够发现未使用到的代码已经被移除。
module.rules
属性中,设置include属性以指定哪些文件须要被loader处理。渐进式网络应用程序(Progressive Web Application - PWA),是一种能够提供相似于原生应用程序(native app)体验的网络应用程序(web app),在离线(offline)时应用程序可以继续运行功能,这是经过 Service Workers 技术来实现的。PWA是最近几年比较火的概念,它的核心是由service worker技术实现的在客户浏览器与服务器之间搭建的一个代理服务器,在网络畅通时,客户浏览器会经过service worker访问服务器,而且缓存注册的文件;在网络断开时,浏览器会访问service worker这个代理服务器,使得在网络断开的状况下,页面仍是可以访问,实现了相似原生应用的网站开发。create-react-app
已经实现了PWA开发的配置。
下面介绍如何经过webpack快速开发PWA。
(1)安装插件workbox-webpack-plugin
:
npm install workbox-webpack-plugin --save-dev
(2)在配置文件中引入该插件:
// webpack.config.js const WorkboxPlugin = require('workbox-webpack-plugin'); module.exports = { plugins: [ new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ] };
(3)使用webpack进行编译,打包出service-worker.js
(4)在入口文件底部注册service worker:
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js').then(registration => { console.log('SW registered: ', registration); }).catch(registrationError => { console.log('SW registration failed: ', registrationError); }); }); }
(5)打开页面,进行调试:
npm run start
(6)打开浏览器调试工具,查看控制台的输出,若是输出“SW registered: ... ...”,表示注册service worker成功,接下来能够断开网络,或者关闭服务器,再次刷新,能够看到页面仍然能够显示。
webpack确实是一个功能强大的模块打包工具,丰富的loader和plugin使得其功能多而强。学习webpack使得咱们能够自定义本身的开发环境,无需依赖create-react-app
和Vue-Cli
这类脚手架,也能够针对不一样的需求对代码进行不一样方案的处理。这篇笔记还只是一篇入门的笔记,若是要真正的构建较为复杂的开发环境和生产环境,还须要了解许多的loader和plugin,好在webpack官网提供了全部的说明,能够给用户提供使用指南:
阅读脚手架的源码也有助于学习webpack,从此应该还有进行这方面的学习,可是答辩即将到来,不知道毕业以前还有没有机会^_^。