Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑
说实话,官方文档是蛮乱的,而且有些还是错的错的。。很多配置问题只有爬过坑才知道
本文首先介绍Webpack的一些基础知识,然后以一个已经完成的小Demo,逐一介绍如何在项目中进行配置
该Demo主要包含编译Sass/ES6,提取(多个)CSS文件,提取公共文件,模块热更新替换,开发与线上环境区分,使用jQuery插件的方式、页面资源引入路径自动生成,编写一个简单的插件 等基础功能
应该能帮助大家更好地在项目中使用Webpack3来管理前端资源
本文比较啰嗦,可以直接看第四部分Webpack3配置在Demo中的应用,或者直接去Fork这个Demo边看边玩
首先,学习Webpack,还是推荐去看官方文档,还是挺全面的,包括中文的和英文的,以及GitHub上关于webpack的项目issues,还有就是一些完整了例子,最后就是得自己练手配置,才能在过程中掌握好这枯燥的配置。
首先,得知道为什么要用webpack
前端本可以直接HTML、CSS、Javascript就上了,不过如果要处理文件依赖、文件合并压缩、资源管理、使用新技术改善生活的时候,就得利用工具来辅助了。
以往有常见的模块化工具RequireJS,SeaJS等,构建工具Grunt、Gulp等,新的技术Sass、React、ES6、Vue等,要在项目中使用这些东西,不用工具的话就略麻烦了。
其实简单地说要聚焦两点:模块化以及自动构建。
模块化可以使用RequireJS来处理依赖,使用Gulp来进行构建;也可以使用ES6新特性来处理模块化依赖,使用webpack来构建
两种方式都狠不错,但潮流所驱,后者变得愈来愈强大,当然也不是说后者就替代了前者,只是大部分情况下,后者更好
如其名,Web+Pack 即web的打包,主要用于web项目中打包资源进行自动构建。
Webpack将所有资源视为JS的模块来进行构建,所以对于CSS,Image等非JS类型的文件,Webpack会使用相应的加载器来加载成其可识别的JS模块资源
通过配置一些信息,就能将资源进行打包构建,更好地实现前端的工程化
可以认为Webpack的配置是4+n模式,四个基本的 entry(入口设置)、output(输出设置)、loader(加载器设置)、plugin(插件设置),然后加上一些特殊功能的配置。
使用Webpack首先需要安装好NodeJS
node -v
npm -v
确保已经可以使用node,使用NPM包管理工具来安装相应依赖包(网络环境差可以使用淘宝镜像CNPM来安装)
npm install -g cnpm --registry=https://registry.npm.taobao.org cnpm -v
全局安装好webpack包
npm i -g webpack
webpack -v
1. 通过cli命令行传入参数
webpack ./src.js -o ./dest.js --watch --color
2. 通过在一个配置文件设置相应配置,导出使用
// ./webpack.config.js文件 module.exports = {
context: ... entry: { }, output: { } }; // 命令行调用(不指定文件时默认查找webpack.config.js) webpack [--config webpack.config.js]
3. 通过使用NodeJS的API配置
这个和第二点有点类似,区别主要是第二种基本都是使用{key: value}的形式配置的,API则主要是一些调用
另外,某些插件的在这两种方式的配置上也有一些区别
最常用的是第二种,其次第三种,第一种不太建议单独使用(因为相对麻烦,功能相对简单)
1. context 绝对路径
一般当做入口文件(包括但不限于JS、HTML模板等文件)的上下文位置,
默认使用当前目录,不过建议还是填上一个
// 上下文位置 context: path.resolve(__dirname, 'static')
2. entry 模块入口文件设置
可以接受字符串表示一个入口文件,不过一般来说是多页应用多,就设置成每页一个入口文件得了
比如home对应于一个./src/js/home模块,这里的key会被设置成webpack的一个chunk,即最终webpack会又三个chunkname:home | detail | common
也可以对应于多个模块,用数组形式指定,比如这里把jquery设置在common的chunk中
也可以设置成匿名函数,用于动态添加的模块
// 文件入口配置 entry: { home: './src/js/home', detail: './src/js/detail', // 提取jquery入公共文件 common: ['jquery'] },
3. resolve 处理资源的查找引用方式
如上方其实是省略了后JS缀,又比如想在项目中引入util.js 可以省略后缀
import {showMsg} from './components/util';
// 处理相关文件的检索及引用方式 resolve: { extensions: ['.js', '.jsx', '.json'], modules: ['node_modules'], alias: { } },
4. output 设置文件的输出
最基础的就是这三个了
path指定输出目录,要注意的是这个目录影响范围是比较大,与该chunk相关的资源生成路径是会基于这个路径的
filename指定生成的文件名,可以使用[name] [id]来指定相应chunk的名称,如上的home和detail,用[hash]来指定本次webpack编译的标记来防缓存,不过建议是使用[chunkhash]来依据每个chunk单独来设置,这样不改变的chunk就不会变了
hash放在?号之后的好处是,不会生成新的文件(只是文件内容被更改了),同时hash会附在引用该资源的URL后(如script标签中的引用)
publicPath指定所引用资源的目录,如在html中的引用方式,建议设置一个
// 文件输出配置 output: { // 输出所在目录 path: path.resolve(__dirname, 'static/dist/js'), filename: '[name].js?[chunkhash:8]'// 设置文件引用主路径 publicPath: '/public/static/dist/js/' }
5.devtool指定sourceMap的配置
如果开启了,就可以在浏览器开发者工具查看源文件
// 启用sourceMap devtool: 'cheap-module-source-map',
比如这里就是对应的一个source Map,建议在开发环境下开启,帮助调试每个模块的代码
这个配置的选项是满多的,而且还可以各种组合,按照自己的选择来吧
6. module指定模块如何被加载
通过设置一些规则,使用相应的loader来加载
主要就是配置module的rules规则组,通过use字段指定loader,如果只有一个loader,可以直接用字符串,loader要设置options的就换成数组的方式吧
或者使用多个loader的时候,也用数组的形式,规则不要用{ }留空,在windows下虽然正常,但在Mac下会报错提示找不到loader
多个loader遵循从右到左的pipe 的方式,如下 eslint-loader是先于babel-loader执行的
通过exclude或include等属性再确定规则的匹配位置
// 模块的处理配置,匹配规则对应文件,使用相应loader配置成可识别的模块 module: { rules: [{ test: /\.css$/, use: 'css-loader' }, { test: /\.jsx?$/, // 编译js或jsx文件,使用babel-loader转换es6为es5 exclude: /node_modules/, use: [{ loader: 'babel-loader', options: { } }, { loader: 'eslint-loader' }] }
7. plugins设置webpack配置过程中所用到的插件
比如下方为使用webpack自带的提取公共JS模块的插件
// 插件配置 plugins: [ // 提取公共模块文件 new webpack.optimize.CommonsChunkPlugin({ chunks: ['home', 'detail'], filename: '[name].js', name: 'common' }), new ... ]
这就是webpack最基础的东西了,看起来内容很少,当然还有其他很多,但复杂的地方在于如何真正去使用这些配置
下面以一个相对完整的基础Demo着手,介绍一下几个基本功能该如何配置
Demo项目地址 建议拿来练练
既然是Demo,至少就得有一个服务器,用node来搭建一个简单的服务器,处理各种资源的请求返回
新建一个服务器文件server.js,以及页面文件目录views,其他资源文件目录public
服务器文件很简单,请求什么就返回什么,外加了一个gzip的功能
let http = require('http'), fs = require('fs'), path = require('path'), url = require('url'), zlib = require('zlib'); http.createServer((req, res) => { let {pathname} = url.parse(req.url), acceptEncoding = req.headers['accept-encoding'] || '', referer = req.headers['Referer'] || '', raw; console.log('Request: ', req.url); try { raw = fs.createReadStream(path.resolve(__dirname, pathname.replace(/^\//, ''))); raw.on('error', (err) => { console.log(err); if (err.code === 'ENOENT') { res.writeHeader(404, {'content-type': 'text/html;charset="utf-8"'}); res.write('<h1>404错误</h1><p>你要找的页面不存在</p>'); res.end(); } }); if (acceptEncoding.match(/\bgzip\b/)) { res.writeHead(200, { 'Content-Encoding': 'gzip' }); raw.pipe(zlib.createGzip()).pipe(res); } else if (acceptEncoding.match(/\bdeflate\b/)) { res.writeHead(200, { 'Content-Encoding': 'deflate' }); raw.pipe(zlib.createDeflate()).pipe(res); } else { res.writeHead(200, {}); raw.pipe(res); } } catch (e) { console.log(e); } }).listen(8088); console.log('服务器开启成功', 'localhost:8088/');
页面文件假设采用每一类一个目录,目录下的tpl为源文件,另外一个为生成的目标页面文件
/public目录下,基本配置文件就放在根目录下,JS,CSS,Image等资源文件就放在/public/static目录下
我们要利用package.json文件来管理编译构建的包依赖,以及设置快捷的脚本启动方式,所以,先在/public目录下执行 npm init 吧
public/static/dist目录用来放置编译后的文件目录,最终页面引用的将是这里的资源
public/static/imgs目录用来放置图片源文件,有些图片会生成到dist中
public/static/libs目录主要用来放置第三方文件,也包括那些很少改动的文件
public/static/src 用来放置js和css的源文件,相应根目录下暴露一个文件出来,公共文件放到相应子目录下(如js/components和scss/util)
最后文件结构看起来是这样的,那就可以开干了
首先在项目目录下安装webpack吧
npm i webpack --save-dev
用Webpack来构建,在开发环境和生产环境的配置还是有一些区别的,构建是耗时的,比如在开发环境下就不需要压缩文件、计算文件hash、提取css文件、清理文件目录这些辅助功能了,而可以引入热更新替换来加快开发时的模块更新效率。
所以建议区分一下两个环境,同时将两者的共同部分提取出来便于维护
NODE_ENV是nodejs在执行时的环境变量,webpack在运行构建期间也可以访问这个变量,所以我们可以在dev和prod下配置相应的环境变量
这个配置写在package.json里的scripts字段就好了,比如
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build:dev": "export NODE_ENV=development && webpack-dev-server --config webpack.config.dev.js", "build:prod": "export NODE_ENV=production && webpack --config webpack.config.prod.js --watch " },
这样一来,我们就可以直接用 npm run build:prod来执行生产环境的配置命令(设置了production的环境变量,使用prod.js)
直接用npm run build:dev来执行开发环境的配置命令(设置了development的环境变量,使用dev.js,这里还使用了devServer,后面说)
注意这里是Unix系统配置环境变量的写法,在windows下,记得改成 SET NODE_ENV=development&& webpack-dev-server.......(&&前不要空格)
然后就可以在common.js配置文件中获取环境变量
// 是否生产环境 isProduction = process.env.NODE_ENV === 'production',
然后可以在plugins中定义一个变量提供个编译中的模块文件使用
// 插件配置 plugins: [ // 定义变量,此处定义NODE_ENV环境变量,提供给生成的模块内部使用 new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV) } }),
这样一来,我们可以在home.js中判断是否为开发环境来引入一些文件
// 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新 if (process.env.NODE_ENV === 'development') { require('../../../../views/home/home.html'); }
然后我们使用webpack-merge工具来合并公共配置文件和开发|生产配置文件
npm i webpack-merge --save-dev merge = require('webpack-merge') commonConfig = require('./webpack.config.common.js') /** * 生产环境Webpack打包配置,整合公共部分 * @type {[type]} */ module.exports = merge(commonConfig, { // 生产环境不开启sourceMap devtool: false, // 文件输出配置 output: { // 设置文件引用主路径 publicPath: '/public/static/dist/js/' }, // 模块的处理配置
公共模块其实可以分为JS和CSS两部分(如果有提取CSS文件的话)
在公共文件的plugin中加入
// 提取公共模块文件 new webpack.optimize.CommonsChunkPlugin({ chunks: ['home', 'detail'], // 开发环境下需要使用热更新替换,而此时common用chunkhash会出错,可以直接不用hash filename: '[name].js' + (isProduction ? '?[chunkhash:8]' : ''), name: 'common' }),
设置公共文件的提取源模块chunks,以及最终的公共文件模块名
公共模块的文件的提取规则是chunks中的模块公共部分,如果没有公共的就不会提取,所以最好是在entry中就指定common模块初始包含的第三方模块,如jquery,react等
// 文件入口配置 entry: { home: './src/js/home', detail: './src/js/detail', // 提取jquery入公共文件 common: ['jquery'] },
要讲ES6转换为ES5,当然首用babel了,先安装loader及相关的包
npm i babel-core babel-loader babel-preset-env babel-polyfill babel-plugin-transform-runtime --save-dev
-env包主要用来配置语法支持度
-polyfill用来支持一些ES6拓展的但babel转换不了的方法(Array.from Generator等)
-runtime用来防止重复的ES6编译文件所需生成(可以减小文件大小)
然后在/public根目录下新建 .babelrc文件,写入配置
{ "presets": [ "env" ], "plugins": ["transform-runtime"] }
然后在common.js的配置文件中新增一条loader配置就行了,注意使用exclude排除掉不需要转换的目录,否则可能会出错哦
{ test: /\.jsx?$/, // 编译js或jsx文件,使用babel-loader转换es6为es5 exclude: /node_modules/, use: [{ loader: 'babel-loader', options: { } }] }
sass的编译node-sass需要python2.7的环境,先确定已经安装并设置了环境变量
npm i sass-loader node-sass style-loader css-loader --save-dev
类似的,设置一下loader规则
不过这里要设置成使用提取CSS文件的插件设置了,因为它的disable属性可以快速切换是否提取CSS(这里设置成生产环境才提取)
好好看这个栗子,其实分三步:设置(new)两个实例,loader匹配css和sass两种文件规则,在插件中引入这两个实例
提取多个CSS文件其实是比较麻烦的,但也不是不可以,方法就是设置多个实例和对应的几个loader规则
这里把引入的sass当做是自己写的文件,提取成一个文件[name].css,把引入的css当做是第三方的文件,提取成一个[name]_vendor.css,既做到了合并,也做到了拆分,目前还没想到更好的方案
上面提到过,output的path设置成了/public/static/dist/js ,所以这里的filename 生成是基于上面的路径,可以用../来更换生成的css目录
[contenthash]是css文件内容的hash,在引用它的地方有体现
fallback表示不可提取时的代替方案,即上述所说的使用style-loader嵌入到<style>标签中
npm i extract-text-webpack-plugin --save-dev ExtractTextWebpackPlugin = require('extract-text-webpack-plugin') / 对import 引入css(如第三方css)的提取 cssExtractor = new ExtractTextWebpackPlugin({ // 开发环境下不需要提取,禁用 disable: !isProduction, filename: '../css/[name]_vendor.css?[contenthash:8]', allChunks: true }) // 对import 引入sass(如自己写的sass)的提取 sassExtractor = new ExtractTextWebpackPlugin({ // 开发环境下不需要提取,禁用 disable: !isProduction, filename: '../css/[name].css?[contenthash:8]', allChunks: true }); // 插件配置 plugins: [ // 从模块中提取CSS文件的配置 cssExtractor, sassExtractor ] module: { rules: [{ test: /\.css$/, // 提取CSS文件 use: cssExtractor.extract({ // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中 fallback: 'style-loader', use: [{ loader: 'css-loader', options: { // url: false, minimize: true } }, // 'postcss-loader' ] }) }, { test: /\.scss$/, // 编译Sass文件 提取CSS文件 use: sassExtractor.extract({ // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中 fallback: 'style-loader', use: [ 'css-loader', // 'postcss-loader', { loader: 'sass-loader', options: { sourceMap: true, outputStyle: 'compressed' } } ] }) }
这样一来,如果在不同文件中引入不同的文件,生成的css可能长这样
// ./home.js import '../../libs/bootstrap-datepicker/datepicker3.css'; import '../../libs/chosen/chosen.1.0.0.css'; import '../../libs/layer/skin/layer.css'; import '../../libs/font-awesome/css/font-awesome.min.css'; import '../scss/detail.scss'; // ./detail.js import '../../libs/bootstrap-datepicker/datepicker3.css'; import '../../libs/chosen/chosen.1.0.0.css'; import '../../libs/layer/skin/layer.css'; import '../scss/detail.scss';
// ./home.html <link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet"> <link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet"> <link href="/public/static/dist/js/../css/home_vendor.css?12a314c8" rel="stylesheet"> <link href="/public/static/dist/js/../css/home.css?c196fc33" rel="stylesheet"> // ./detail.html <link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet"> <link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">
可以看到,公共文件也被提取出来了,利用HtmlWebpackPlugin就能将其置入了
另外,可以看到这里的绝对路径,其实就是因为在output中设置了publicPath为/public/static/dist/js/
当然了,也不是说一定得在js中引入这些css资源文件,你可以直接在页面中手动<link>引入第三方CSS
我这里主要是基于模块化文件依赖,以及多CSS文件的合并压缩的考虑才用这种引入方式的
目前来说,jQuery及其插件在项目中还是很常用到的,那么就要考虑如何在Webpack中使用它
第一种方法,就是直接页面中<script>标签引入了,但这种方式不受模块化的管理,好像有些不妥
第二种方法,就是直接在模块中引入所需要的jQuery插件,而jQuery本身由Webpack插件提供,通过ProvidePlugin提供模块可使用的变量$|jQuery|window.jQuery
不过这种方法好像也有不妥,把所有第三方JS都引入了,可能会降低编译效率,生成的文件也可能比较臃肿
npm i jquery --save // plugins: [ new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery' }), ] // ./home.js import '../../libs/bootstrap-datepicker/bootstrap-datepicker.js'; console.log('.header__img length', jQuery('.header__img').length);
第三种办法,可以在模块内部直接引入jQuery插件,也可以直接在页面通过<script>标签引入jQuery插件,而jQuery本身由Webpack的loader导出为全局可用
上述ProvidePlugin定义的变量只能在模块内部使用,我们可以使用expose-loader将jQuery设置为全局可见
npm i expose-loader --save // 添加一条规则 { test: require.resolve('jquery'), // 将jQuery插件变量导出至全局,提供外部引用jQuery插件使用 use: [{ loader: 'expose-loader', options: '$' }, { loader: 'expose-loader', options: 'jQuery' }] }
要注意在Webpack3中不能使用webpack.NamedModulesPlugin()来获取模块名字,它会导致expose 出错失效(bug)
不过现在问题又来了,这个应该是属于HtmlWebpackPlugin的不够机智的问题,先说说它怎么用吧
第一个重要的功能就是生成对资源的引入了,第二个就是帮助我们填入资源的chunkhash值防止浏览器缓存
这个在生产环境使用就行了,开发环境是不需要的
npm i html-webpack-plugin --save-dev HtmlWebpackPlugin = require('html-webpack-plugin') plugins: [ // 设置编译文件页面文件资源模块的引入 new HtmlWebpackPlugin({ // 模版源文件 template: '../../views/home/home_tpl.html', // 编译后的目标文件 filename: '../../../../views/home/home.html', // 要处理的模块文件 chunks: ['common', 'home'], // 插入到<body>标签底部 inject: true }), new HtmlWebpackPlugin({ template: '../../views/detail/detail_tpl.html', filename: '../../../../views/detail/detail.html', chunks: ['common', 'detail'], inject: true }), ]
使用方式是配置成插件的形式,想对多少个模板进行操作就设置多少个实例
注意template是基于context配置中的上下文的,filename是基于output中的path路径的
// ./home_tpl.html <script src="/public/static/libs/magicsearch/jquery.magicsearch2.js"></script> </body> // ./home.html <script src=/public/static/libs/magicsearch/jquery.magicsearch2.js></script> <script type="text/javascript" src="/public/static/dist/js/common.js?cc867232"></script> <script type="text/javascript" src="/public/static/dist/js/home.js?5d4a7836"></script> </body>
它会编译成这样,然而,然而,要注意到这里是有问题的
这里有个jQuery插件,而Webpack使用expose是将jQuery导出到了全局中,我们通过entry设置把jQuery提取到了公共文件common中
所以正确的做法是common.js文件先于jQuery插件加载
而这个插件只能做到在<head> 或<body>标签尾部插入,我们只好手动挪动一下<script>的位置
不过,我们还可以基于这个插件,再写一个插件来实现自动提升公共文件 <script>标签到最开始
HtmlWebpackPlugin运行时有一些事件
html-webpack-plugin-before-html-generation
html-webpack-plugin-before-html-processing
html-webpack-plugin-alter-asset-tags
html-webpack-plugin-after-html-processing
html-webpack-plugin-after-emit
html-webpack-plugin-alter-chunks
在编译完成时,正则匹配到<script>标签,找到所设置的公共模块(可能设置了多个公共模块),按实际顺序提升这些公共模块即可
完整代码如下:
1 // ./webpack.myPlugin.js 2 3 4 let extend = require('util')._extend; 5 6 7 // HtmlWebpackPlugin 运行后调整公共script文件在html中的位置,主要用于jQuery插件的引入 8 function HtmlOrderCommonScriptPlugin(options) { 9 this.options = extend({ 10 commonName: 'common' 11 }, options); 12 } 13 14 HtmlOrderCommonScriptPlugin.prototype.apply = function(compiler) { 15 compiler.plugin('compilation', compilation => { 16 compilation.plugin('html-webpack-plugin-after-html-processing', (htmlPluginData, callback) => { 17 // console.log(htmlPluginData.assets); 18 19 // 组装数组,反转保证顺序 20 this.options.commonName = [].concat(this.options.commonName).reverse(); 21 22 let str = htmlPluginData.html, 23 scripts = [], 24 commonScript, 25 commonIndex, 26 commonJS; 27 28 //获取编译后html的脚本标签,同时在原html中清除 29 str = str.replace(/(<script[^>]*>(\s|\S)*?<\/script>)/gi, ($, $1) => { 30 scripts.push($1); 31 return ''; 32 }); 33 34 this.options.commonName.forEach(common => { 35 if (htmlPluginData.assets.chunks[common]) { 36 // 找到公共JS标签位置 37 commonIndex = scripts.findIndex(item => { 38 return item.includes(htmlPluginData.assets.chunks[common].entry); 39 }); 40 41 // 提升该公共JS标签至顶部 42 if (commonIndex !== -1) { 43 commonScript = scripts[commonIndex]; 44 scripts.splice(commonIndex, 1); 45 scripts.unshift(commonScript); 46 } 47 } 48 }); 49 50 // 重新插入html中 51 htmlPluginData.html = str.replace('</body>', scripts.join('\r\n') + '\r\n</body>'); 52 53 callback(null, htmlPluginData); 54 }); 55 }); 56 }; 57 58 59 module.exports = { 60 HtmlOrderCommonScriptPlugin, 61 };
然后,就可以在配置中通过插件引入了
{HtmlOrderCommonScriptPlugin} = require('./webpack.myPlugin.js'); // HtmlWebpackPlugin 运行后调整公共script文件在html中的位置,主要用于jQuery插件的引入 new HtmlOrderCommonScriptPlugin({ // commonName: 'vendor' })
亲测还是蛮好用的,可以应对简单的需求了
这个配置开发环境和生产环境是不同的,先看看生产环境的,主要的特点是有目录结构的设置,设置了一些生成的路径以及名字信息
开发环境因为是使用了devServer,不需要控制目录结构
npm i url-loader file-loader@0.10.0 html-loader --save-dev
这里要注意的是file-loader就不要用0.10版本以上的了,会出现奇怪的bug,主要是下面设置的outputPath和publicPath和[path]会不按套路出牌
导致生成的页面引用资源变成奇怪的相对路径
rules: [{ test: /\.(png|gif|jpg)$/, use: { loader: 'url-loader', // 处理图片,当大小在范围之内时,图片转换成Base64编码,否则将使用file-loader引入 options: { limit: 8192, // 设置生成图片的路径名字信息 [path]相对context,outputPath输出的路径,publicPath相应引用的路径 name: '[path][name].[ext]?[hash:8]', outputPath: '../', publicPath: '/public/static/dist/', } } }, { test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/, use: [{ loader: 'file-loader', options: { // 设置生成字体文件的路径名字信息 [path]相对context,outputPath输出的路径,publicPath相应引用的主路径 name: '[path][name].[ext]?[hash:8]', outputPath: '../', publicPath: '/public/static/dist/', // 使用文件的相对路径,这里先不用这种方式 // useRelativePath: isProduction } }], }, { test: /\.html$/, // 处理html源文件,包括html中图片路径加载、监听html文件改变重新编译等 use: [{ loader: 'html-loader', options: { minimize: true, removeComments: false, collapseWhitespace: false } }] }]
比较生涩难懂,看个栗子吧
scrat.png是大于8192的,最终页面引入会被替换成绝对路径,并且带有hash防止缓存,而输出的图片所在位置也是用着相应的目录,便于管理
// ./home_tpl.html <img class="header__img" src="../../public/static/imgs/kl/scrat.png" width="200" height="200"> // ./home.html <img class=header__img src=/public/static/dist/imgs/kl/scrat.png?8ad54ef5 width=200 height=200>
如果换个小图,就会替换成base64编码了,在css中的引入也一样
<img class=header__img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAA6CAYAAABrnUYFAAAaVElEQVR4Xu1df3wdVZX/npnkNUlbWn6sFhAELSgibSYtCIhagXXB0vdSsOwu4CKiXbAglk8zAVwloELzwoqIUsFFRXQVaiEvRcAfLD9EfqxtXtqKS239AQoFRUlb+tK+5N2znzNvJp1M34+ZeZOElLn/QPPuPffcc2e+c+75dQlxiyUQSyCWwChJgEaJbkw2lkAsgVg
再来看看开发环境的
rules: [{ test: /\.(png|gif|jpg)$/, // 处理图片,当大小在范围之内时,图片转换成Base64编码,否则将使用file-loader引入 use: [{ loader: 'url-loader', options: { limit: 8192 } }] }, { test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/, // 引入文件 use: 'file-loader' }]
在开发环境下,如果做到模块的热更新替换,效果肯定是棒棒的。生成环境就先不用了
在最初的时候,只是做到了热更新,并没有做到热替换,其实都是坑在作祟
热更新,需要一个配置服务器,Webpack集成了devServer的nodejs服务器,配置一下它
// 开发环境设置本地服务器,实现热更新 devServer: { contentBase: path.resolve(__dirname, 'static'), // 提供给外部访问 host: '0.0.0.0', port: 8188, // 设置页面引入 inline: true },
正常的话,启动服务应该就可以了吧
webpack-dev-server --config webpack.config.dev.js
要记住,devServer编译的模块是输出在服务器上的(默认根目录),不会影响到本地文件,所以要在页面上手动设置一下引用的资源
<script src="http://localhost:8188/common.js"></script> <script src="http://localhost:8188/home.js"></script>
浏览器访问,改动一下home.js文件,这时应该可以看到页面自动刷新,这就是热更新了??
当然了,热更新还不够,得做到热替换,即页面不刷新替换模块
可以呀,多配置一下
// 开发环境设置本地服务器,实现热更新 devServer: { ... // 设置热替换 hot: true, ... }, // 插件配置 plugins: [ // 热更新替换 new webpack.HotModuleReplacementPlugin(), ]
再去浏览器试试,改个文件,正常的话应该也能看到
但就是一直停留在App hot update...不动了,惊不惊喜,意不意外
原因是还没在当前项目中安装webpack-dev-server,HMR的消息接收不到,命令没报错只是因为在全局安装了webpack有那命令
npm i webpack-dev-server --save-dev
再试试,然而你发现,才刚开始编译,就不停地重复编译了
你得设置一下publicPath 比如
output: { publicPath: '/dist/js/', },
再试试,更改模块,你又会发现页面还是重新刷新了
要善于用Preserve log来看看刷新之前发生了什么
已经有进展了,这时HMR在获取JSON文件时404了,而且访问的域名端口是localhost:8088是我们自己node服务器的端口
devServer的端口是8188的,看起来这JSON文件时devServer生成的,可能是路径被识别成相对路径了
那就设置成绝对路径吧
output: { // 设置路径,防止访问本地服务器相关资源时,被开发服务器认为时相对其的路径 publicPath: 'http://localhost:8188/dist/js/', },
再来,恭喜 又错了,跨域访问
那就在devServer再配置一下header让8088可以访问,可以暴力一点设置*
devServer: { ... // 允许开发服务器访问本地服务器的包JSON文件,防止跨域 headers: { 'Access-Control-Allow-Origin': '*' }, ... },
再来,额??呵呵,又重新刷新了
指明了模块没有被设置成accepted,那它就不知道要热替换哪个模块了,只好整个刷新。
需要在模块中设置一下,机智是冒泡型的,所以在主入口设置就行了,比如这里的模块入口home.js
// 设置允许模块热替换 if (module.hot) { module.hot.accept(); }
这就成功了,这里建议的NamedModulesPlugin是用不了了,因为和espose-loader冲突了
是不是很啰嗦呢,总结一下
1. 在本项目总安装webpack-dev-server
2. devServer配置中设置hot: true
3. plugins配置中设置new webpack.HotModuleReplacementPlugin()
4. output配置中设置publicPath: 'http://localhost:8188/dist/js/'
5. devServer配置中设置header允许跨域访问
6. 模块中设置接受热替换module.hot.accept()
7. 不要在命令行加参数 --hot 和 new webpack.HotModuleReplacementPlugin() 同时使用,会栈溢出错误,只用配置文件的就行了
另外,默认是只能模块热替换,如果也想监听页面文件改变来实现HTML页面的热替换,该怎么做呢
把HTML也当做模块引入就行了(开发环境下),在之前已经使用了html-loader能处理html后缀资源的情况下
// ./home.js // 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新 if (process.env.NODE_ENV === 'development') { require('../../../../views/home/home_tpl.html'); }
记得import不能放在if语句块里面,所以这里用require来代替
有点奇怪,在最开始的时候,这样是能实现热替换的,但这段时间却一直不行了,显示已更新,但内容却没更新
只好暂时用第二步热更新来替换,接收到改变时页面自动刷新
// ./home.js // 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新 if (process.env.NODE_ENV === 'development') { require('../../../../views/home/home_tpl.html'); } // 设置允许模块热替换 if (module.hot) { module.hot.accept(); // 页面文件更新 自动刷新页面 module.hot.accept('../../../../views/home/home_tpl.html', () => { location.reload(); }); }
压缩JS代码就用自带的插件就行了
压缩CSS代码用相应的loader options
// 压缩代码 new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }),
再来稍微配一下react的环境
npm i react react-dom babel-preset-react --save-dev
在home.js文件中加入
let React = require('react'); let ReactDOM = require('react-dom') class Info extends React.Component { constructor(props) { super(props); this.state = { name: this.props.name || 'myName' }; } showYear(e) { console.log(this); let elem = ReactDOM.findDOMNode(e.target); console.log('year ' + elem.getAttribute('data-year')); } render() { return <p onClick={this.showYear} data-year={this.props.year}>{this.state.name}</p> } } Info.defaultProps = { year: new Date().getFullYear() }; ReactDOM.render(<Info />, document.querySelector('#box'));
修改.bablerc文件
{ "presets": [ "env", "react" ], "plugins": ["transform-runtime"] }
其他配置,比如eslint代码检查、postcss支持等就不在这说了,用到了就用类似的方式添加进去吧
转载请注明