使用loaders
每引入一种loader,就相当于告诉了webpack:当遇到某种类型的文件时,就使用该loader来解析。
less
项目中使用了less,解析less文件需要使用3个loader:style-loader,css-loader,以及less-loader。
$ npm install style-loader css-loader less-loader less --save-dev
其中less-loader用来将less解析为css,css-loader能够解析import以及url()的语法,而style-loader可以将生成的css插入到HTML文档中(注意最后只生成一个bundle.js)。
所以webpack.config.js中解析less的配置如下:
// options.module
module: {
rules: [
{
test: '/\.less$/', // 要解析的文件类型
use: ['style-loader', 'css-loader', 'less-loader'] // 要使用的loaders,注意顺序
}
]
}
jsx以及es6
因为webpack默认是可以解析commonJS,AMD以及es6的模块语法的,所以即使不使用任何的laoder,import以及require语法也可以直接使用。但是除了import/export这些模块语法,webpack是不会动你其他代码的。所以,如果我们在项目中使用es6、JSX等语法,还需要添加babel-loader。
$ npm install babel-core babel-loader babel-preset-es2015 babel-preset-react --save-dev
webpack.config.js中解析jsx以及es6的配置如下:
// options.module
module: {
rules: [
test: /\.(js|jsx)$/,
exclude: /node_modules/, // 这里面的不解析
loader: 'babel-loader',
query: [ // 相当于babel的options
presets: ['react', 'es2015']
]
]
}
提取css
通过上面的配置,我们可以愉快地在项目中使用es6以及jsx语法了,并且最终会打包出唯一的一个bundle.js文件,然后再html中引用bundle.js即可。
但是,还有一点不足。项目中所有的样式都被打包到了bundle.js文件中,所以浏览器必须等待bundle.js完全加载完毕之后,才可以给文档加上样式,这样一来,我们就没办法利用浏览器的异步加载css以及并行加载的优势了。
extract-text-webpack-plugin
所以,我们可以把所有的css打包到一个单独的样式文件中。这就需要一个webpack插件,叫做extract-text-webpack-plugin。
$ npm install extract-text-webpack-plugin
在修改我们的webpack.config.js如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './index.js', // 项目入口文件
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'public'),
},
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
}
]
},
plugins: [
new ExtractTextPlugin('index.css') // 最终会在public目录下生成index.css
]
};
现在在控制台运行webpack –config webpack.config.js,我们会发现生成了一个bundle.js,还有一个index.css,可以将它们直接引入html中去。
一点副作用
extract-text-webpack-plugin可能会让css无法实现热重载(后面会讲到),所以我们最好只在生产环境中使用这个插件。
提取第三方js模块
上面我们把css单独提取出来了,但是bundle.js依旧可能是一个很大的文件,因为我们将一些第三方库打包了进去,比如lodash和react等。
这样做依旧有一个缺点: 每次修改了项目代码,哪怕只是一点点,整个bundle.js必须重新打包,浏览器也必须重新从服务器拉取新的bundle.js,这样就无法利用浏览器缓存静态资源的优势。
假如我们将不经常变化的第三方js库和我们的项目代码分开打包为vendor.js和app.js,那么当我们修改了项目代码并重新打包,只有app.js发生了变化,这样浏览器就只需要重新从服务器拉取新的app.js就可以了。
接下来我们就来这样做。
多个入口
修改options.entry以及options.output如下:
entry: {
app: './index.js', // 项目代码入口
vendor: ['react', 'react-dom', 'redux', 'react-redux'] // 第三方js库
}
output: {
filename: '[name].[chunkHash]js', // 对于多入口,这里必须使用占位符
path: path.join(__dirname, 'public'),
}
再次打包,在public目录下生成了两个js文件(当然还有index.css):vendor.[hash].js以及app.[hash].js。文件名中的[hash]代表的是根据文件的内容所生成的hash值。
CommonsChunkPlugin
但是我们发现,第三方的js库竟然同时打包进了这两个js文件中。这是因为webpack将从每个入口文件开始分析依赖,这将得到多颗独立的依赖树,并且分别打包,互不干扰。如果我们指向将第三方模块打包到vendor.js中,我们还需要使用CommonsChunkPlugin这个插件。
const webpack = require('webpack');
// options.plugins
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' // 指定模块的名称
})
]
运行webpack,发现公共的js库只打包在了vendor.[hash].js中,而app.[hash].js减小了很多。
manifest
现在终于可以安心的改项目代码了,可是,当我们改了一点项目代码,重新使用webpack来打包时,发现public目录下竟然又多出了两个文件app.[hash].js以及vendor.[hash].js。这与我们预想的不同,vendor应该保持不变才对,为什么又重新打包了一份呢?这也意味着我们没办法享受到浏览器缓存带来的好处,因为vendor的hash变了,我们每build一次,浏览器必须要重新加载vendor。
翻阅wepack的官方文档,对这个问题解释如下:
每次build,webpack都生成一些webpack运行时代码来帮助webpack完成它的工作。当只打包出一个文件时,这些运行时代码自然就在这个文件中,如果我们像上面那样打包出两个文件,那么运行时代码将被打包到公共模块中,正是vendor.js中。
为了解决这个问题,我们需要把这些运行时代码单独提到一个manifest.js中。尽管我们又多打包了一个文件,但是我们的vendor.js再也不会发生变化了,这使得我们可以享受浏览器缓存带来的性能提升。
修改配置文件如下:
// options.plugins
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'] // 指明公共模块的名称
})
]
按需加载
通过以上配置,我们已经可以使用webpack来完成最基本的打包工作了。但是,浏览器加载的资源总比它实际用得到的多,如果我们能在应用中实现按需加载(懒加载),那么,我们应用的性能可能再进一步提升。
我们有两种方式可以实现按需加载,下面将一一介绍,不过在此之前,可以先看一下这篇文章。
import()
import()方法是es6中关于模块加载的一个规范,用于在运行时动态加载模块。该函数将一个模块名当作参数,并且返回一个Promise对象,这意味着当模块加载没有成功时,我们可以作出一些处理。
webpack把import()当作一个代码分离点,并且把通过import引入的模块,单独打包到一个chunk。
下面看import()在项目代码中的使用:
// 只有点击提交按钮之后才会触发,如果不点击,我们希望里面的js模块不用不加载
handleSubmit () {
import('moment') // 代码分离点,将单独打包到一个chunk中
.then(moment => {
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
})
.catch(err => {
console.log('模块加载失败!')
});
}
现在我们来配置webpack,让其支持这种动态加载模块语法。首先,需要安装bebel-plugin-syntax-dynamic-import插件:
// 注意,我们之前已经安装过bable-core babel-loader等插件
$ npm install babel-plugin-syntax-dynamic-import --save-dev
// 同时安装moment.js
$ npm install moment --save
然后配置webpack如下:
// options.module.rules
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
query: {
presets: ['react', 'es2015'],
plugins: ['syntax-dynamic-import'] // 注意这里的变化
}
}]
}
运行wepack,发现除了manifest.js,vendor.js,app.js,index.css以外,还生成了0.js,打开0.js可以发现,其中包含了moment模块的所有代码,这样,在应用初次加载的时候,是不需要加载0.js的,只有当用户点击了提交按钮之后,0.js才会从浏览器拉取并执行。
此外我们还可以给这些按需加载的代码块规定名称;
// options.output
output: {
filename: [name].js,
path: path.join(__dirname, 'public'),
chunkFileName: '[id].chunk.js' // 这样生成的代码块名称将是 0.chunk.js
}
require.ensure()
除了es6的提案中的import(),webpack还为我们提供了require.ensure()的方式,并且这种方式不需要额外安装插件,还可以指定打包的chunk名称。
其api如下:
require.ensure(dependencies: String[], callback: function(require), chunkeName: string);
// 一个require.ensure指定一个代码分离点
// dependencies 依赖模块列表,只会被加载,加载完毕之后,才可以在callback中执行
// callback 所有dependencies加载完毕之后,callback函数会立即执行
// chunkName 对应webpack配置中output.chunkFilename中的[name]占位符,并且如果require.ensure指定同一个chunkName,那么将合并为一个chunk
我们要明白的一点是:require.ensure()只会加载dependencies中指定的依赖模块,但不执行。如果想执行,可以在callback中调用require。
现在来看一个案例,假设在testModules目录下有3js文件,名称和内容如下:
// a.js
console.log('aaaaaaaaaaaaaaaaaaaa');
// b.js
console.log('bbbbbbbbbbbbbbbbbbbb');
// c.js
console.log('ccccccccccccccccccc');
下面我们再另外一个js模块中的handleSubmit()中按需加载这3个模块:
// 场景1,dependencies为空数组
// 将a和b两个模块单独打包到1.test.chunk.js中
// a和b模块都执行了
require.ensure([], require => {
require('../testModules/a');
require('../testModules/b');
}, 'test'); // 'test'对应chunkFilename中的[name]
// 场景2,a和b两个模块作为dependencies
// 将a,b,c模块打包到1.test.chunk.js文件中
// 但是只有b和c模块执行了,a模块没有执行
// a模块如果想执行,必须require()
require.ensure(['../testModules/a', '../testModules/b'], require => {
require('../testModules/b');
require('../testModules/c');
})
以上还需要在实践中多多体验。
更愉快的开发
到目前位置,我们可以使用es6,less,jsx等语法以及按需加载的功能了,但是对于开发环境来讲,这种体验还不是很好。因为我们每次修改完代码,都必须手动执行webpack –config webpack.config.js,等待webpack打包出新文件之后,我们再去浏览器刷新页面,才能看到效果。
我们想要的效果应该是这个样子:
每次修改完less,按下ctrl+s,浏览器不会整页刷新,而是直接把新样式运用到页面中
每次修改完js,保存后,浏览器自动整页刷新
有source-map,方便排除bug
如果是纯前端项目,使用webpack-dev-server就可以实现热重载了,不过现在我们要把它整合到一个后台使用express的项目中。
下面我们来一一实现,先介绍热重载依靠的两个插件webpack-dev-middleware和webpack-hot-middleware。
webpack-dev-middleware
这是一个只应该在开发环境中使用的webpack插件,它可以监听项目代码的变化,自动打包文件到内存中,并且server这些文件。
当自动编译还未完成的时候,如果浏览器已经发来了请求,那么这个请求将会被阻塞,直到文件编译完成。这看起来就像网速很慢,卡住了一样。
$ npm install webpack-dev-middleware --save-dev
配置webpack.dev.config.js如下;
// options.output
output: {
filename: [name].js,
path: path.join(__dirname, 'public'),
chunkFilename: '[id].[name].chunk.js',
publicPath: '/assets/' // 服务器server文件的地址,/assets可能并不实际存在磁盘中
}
上面配置中的publicPath字段比较难理解,它指明了浏览器按需加载或者加载外部资源(图片,文件)时候的路径。在这里我们配置它,是为指明了webpack-dev-middleware serve 内存文件的路径(继续往下看)。
现在我们可以在html中这样引用资源了,并且按需加载文件的url也会指向/assets/1.test.chunk.js这样的。
<script src="/assert/manifest.js">script>
<script src="/assert/vendor.js">script>
<script src="/assert/app.js">script>
最后,我们来正式使用webpack-dev-middleware:
const path = requrie('path');
const express = rquire('express');
const webpack = require('webpack');
const webpackDevMiddleware = rquire('webpack-dev-middleware');
const webpackCofig = require('./webpack.dev.config.js');
const app = express();
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware({
publicPath: webpackConfig.output.publicPath // 该参数必须指定
}));
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(3000, function () {
console.log('listening on port 3000');
});
现在我们可以在3000端口访问应用了,并且每修改完代码,按下ctrl+s或者直接刷新浏览器,webpack都会自动打包文件到内存中。
这样,就不用每次都运行webpack –config webpaack.dev.config.js了,打包速度也快了很多。
webpack-hot-middleware
美中不足的是我们还需要手动刷新浏览器,现在我们通过wepack-hot-middleware来实现HMR。
webpack-hot-middleware只关心将一个客户端和一个服务器连接起来,并且订阅服务端资源的更新,最终将更新的资源应用与客户端(通过webpack的HMR api)。
$ npm install --save-dev webpack-hot-middleware
更改webpack的配置文件:
const HotMiddlewareScript = 'webpack-hot-middleware/client?reload=true';
// options.entry,每个进入点之后,都加HotMiddlewareScript
entry: {
app: ['index.js', HotMiddlewareScript],
vendor: ['react', 'react-dom', 'redux', 'react-redux', HotMiddlewareScript]
}
// options.plugins
plugins: [
new webpack.HotModuleReplacementPlugin()
]
配置app.js:
const webpackHotMiddleware = rquire('webpack-hot-middleware');
app.use(webpackHotMIddleware(compiler));
现在重启app.js,并在浏览器中访问3000端口,发现控制台出现了[HMR] connected字样。并且我们修改js,发现webpack会自动打包,并通知浏览器刷新。
但是,当我们修改了less文件之后,却发现webpack只是自动打包了,新样式并没有被应用到浏览器中。
这是为什么呢?其实前面在讲extract-text-webpack-plugin的时候,我们已经提到过了:extract-text-webpack-plugin并不适合在开发环境中只用,它会导致css的热重载失效。
所以,我们把webpack.dev.config.js中关于extarct-text-webpack-plugin的部分去掉之后,发现一切跟我们预想的一样!
react-hot-loader
除此之外,针对react,还有一个可以实现热重载的工具,即react-hot-loader,这里就不详细介绍了,到官网看去吧!
source-map
source-map可以快速帮助我们定位bug,对于提升开发效率有很好的提升。
通过webpack来实现source-map非常简单,只需要添加一行配置:
// options.devtool
devtool: 'cheap-eval-source-map'
devtool的值有8种,它们的特性以及适用环境都有很多不同。详细信息就看这里。
最后
webpack的基本应用到这里就先告一段落,还有很多高级的特性没有涉及,以后还需要在实践中慢慢摸索。
最后,放上工作成果图:
转载请注明: Vue教程中文网 - 打造国内领先的vue学习网站-vue视频,vue教程,vue学习,vue培训 » webpack打包React应用