全称是Hot Module ReplaceMent(HMR),理解成热模块替换或者模块热替换都可以吧,和.net中的热插拔一个意思,就是在运行中对程序的模块进行更新。这个功能主要是用于开发过程中,对生产环境没有任何帮助(这一点区别.net热插拔)。效果上就是界面的无刷新更新。
HMR基于WDS,style-loader可以通过它来实现无刷新更新样式。但是对于JavaScript模块就需要做一点额外的处理,怎么处理继续往下看。因为HMR是用于开发环境的,所以我们修改下配置,做两份准备。一个用于生产,一个用于开发。
const path = require(\'path\'); const HtmlWebpackPlugin = require(\'html-webpack-plugin\'); const webpack = require(\'webpack\'); const PATHS = { app: path.join(__dirname, \'app\'), build: path.join(__dirname, \'build\'), }; const commonConfig={ entry: { app: PATHS.app, }, output: { path: PATHS.build, filename: \'[name].js\', }, plugins: [ new HtmlWebpackPlugin({ title: \'Webpack demo\', }), ], } function developmentConfig(){ const config ={ devServer:{ //使能历史记录api historyApiFallback:true, hotOnly:true,//关闭热替换 注释掉这行就行 stats:\'errors-only\', host:process.env.Host, port:process.env.PORT, overlay:{ errors:true, warnings:true, } }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], }; return Object.assign( {}, commonConfig, config, { plugins: commonConfig.plugins.concat(config.plugins), } ); } module.exports = function(env){ console.log("env",env); if(env==\'development\'){ return developmentConfig(); } return commonConfig; };
plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), ],
import component from \'./component\'; let demoComponent=component(); document.body.appendChild(demoComponent); //HMR 接口 if(module.hot){ module.hot.accept(\'./component\',()=>{ const nextComponent=component(); document.body.replaceChild(nextComponent,demoComponent); demoComponent=nextComponent; }) }
并修改component.js:
export default function () { var element = document.createElement(\'h1\'); element.innerHTML = \'Hello webpack\'; return element; }
这个时候页面更新了。每次改动页面上都会增加一个带有hot-update.js ,类似于下面这样:
webpackHotUpdate(0,{ /***/ "./app/component.js": /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony default export */ __webpack_exports__["default"] = function () { var element = document.createElement(\'h1\'); element.innerHTML = \'Hello web \'; element.className=\'box\'; return element; }; /***/ }) })
通过webpackHotUpdate对相应模块进行更新。0表示模块的id,"./app/component.js"表示模块对应的name。结构是webpack(id,{key:function(){}})。function外带了一个括号,不知道有什么作用。webpackHotUpdate的定义是这样的:
this["webpackHotUpdate"] = function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
hotAddUpdateChunk(chunkId, moreModules); if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); } ;
小结:从结构来看,一个是id,一个是对应修改的模块。但实际执行更新的是hotApply方法。热更新整个机制还是有点复杂,效果上像MVVM的那种绑定。有兴趣的可以深入研究下。不建议在生产使用HMR,会让整体文件变大,而且对生成没有什么帮助,在下一节会讲样式的加载,style-loader就是用到了HMR。但对于js模块还要写额外的代码,这让人有点不爽。
demo:http://files.cnblogs.com/files/stoneniqiu/webpack-ch3.zip
参考:
【webpack】-- 自动刷新与解析
2017-02-26 23:53 by stoneniqiu, 298 阅读, 0 评论, 收藏, 编辑
前端需要频繁的修改js和样式,且需要根据浏览器的页面效果不断的做调整;而且往往我们的开发目录和本地发布目录不是同一个,修改之后需要发布一下;另外一点就是并不是所有的效果都可以直接双击页面就能看到,我们常常需要在本地用nginx建一个站点来观察(自己电脑上ok了才放到测试环境去)。所以如果要用手工刷新浏览器和手动(或点击)发布,还要启动站点,确实是个不小的体力活。而这三点webpack可以帮我们做到。
webpack-dev-server
1.安装
npm install webpack-dev-server --save-dev
先通过npm将其安装到开发目录。安装完成之后会在node_modules/bin下找到。
2.npm启动
然后修改package.json:(基于上一节)
"scripts": { "start": "webpack-dev-server --env development", "build": "webpack --env production" }
现在就可以通过npm run start 或者 npm start来启动了。
启动之后,可以看到Project is running at http://localhost:8080 上面。打开页面
说明WDS已经帮我们自动建了一个站点.我们修改component.js ,cmd中会出现编译,页面会自动刷新。
3.直接启动
官网介绍可以直接通过下面的命令启动WDS。
webpack-dev-server --env development
但会出现webpack-dev-server --env development 不是内部命令的提示,这种问题都是环境变量的问题,将你开发的bin目录设置到环境变量中即可,比如我的目录是‘E:\\Html5\\node_modules\\.bin’,就加上分号写在后面。
C:\\Users\\Administrator.9BBOFZPACSCXLG2\\AppData\\Roaming\\npm;C:\\Program Files (x86)\\Microsoft VS Code\\bin;E:\\Html5\\node_modules\\.bin
4.8080端口占用
如果默认的8080端口占用,WDS会换一个。比如用nginx先发布一个。
server{ listen 8080; location / { root E:/Html5/build; index index.html index.htm; } }
再启动WDS:
端口切到了8081。也可以手动配置端口:
devServer:{ //... port: 9000 }
nodemon 自动启动
WDS是监视开发文件的,webpack.config.js改变不会引起自动启动。所以我们需要nodemon去做这件事情。
npm install nodemon --save-dev
先安装在开发目录,然后修改package.json:
"scripts": { "start": "nodemon --watch webpack.config.js --exec \\"webpack-dev-server --env development\\"", "build": "webpack --env production" },
等于让nodemon去监视webpack.config.js,变化了就去启动它。
这样就你可以让你的双手专心的开发了。
代理
不过有一点疑问,就是WDS这个站点的替代性,因为我们自己部署的nginx有一些api的代理。如果挂在WDS的这个默认站点上自然是无法访问的。换句话说可否给WDS配置一个刷新路径。如果文件改变去刷新指定的地址,或者让我去配个代理。既然它本身是一个http服务器,肯定也有代理的功能。搜了下果然有:https://github.com/webpack/webpack-dev-server/tree/master/examples/proxy-advanced
module.exports = { context: __dirname, entry: "./app.js", devServer: { proxy: { "/api": { target: "http://jsonplaceholder.typicode.com/", changeOrigin: true, pathRewrite: { "^/api": "" }, bypass: function(req) { if(req.url === "/api/nope") { return "/bypass.html"; } } } } } }
即将api这个字段替换成http://jsonplaceholder.typicode.com/,并将其从原地址中删掉,这样就可以自己实现代理了。皆大欢喜!WDS是通过 http-proxy-middleware 来实现代理。更多参考:http://webpack.github.io/docs/webpack-dev-server.html#bypass-the-proxy;https://github.com/chimurai/http-proxy-middleware#options
but,这种刷新是怎么实现的呢?因为页面上没有嵌入什么别的js,去翻原码 web-dev-server/server.js中有这么一段:
Server.prototype._watch = function(path) { const watcher = chokidar.watch(path).on("change", function() { this.sockWrite(this.sockets, "content-changed"); }.bind(this)) this.contentBaseWatchers.push(watcher); }
用chokidar来监视文件变化,server的内部维护的有一个socket集合:
Server.prototype.sockWrite = function(sockets, type, data) { sockets.forEach(function(sock) { sock.write(JSON.stringify({ type: type, data: data })); }); }
sock是一个sockjs对象。https://github.com/sockjs/sockjs-client,从http://localhost:8080/webpack-dev-server/页面来看,sockjs是用来通信记录日志的。
var onSocketMsg = { hot: function() { hot = true; log("info", "[WDS] Hot Module Replacement enabled."); }, invalid: function() { log("info", "[WDS] App updated. Recompiling..."); sendMsg("Invalid"); }, hash: function(hash) { currentHash = hash; }, ... }
我们在看app.js,其中有一个OnSocketMsg 对象。
ok的时候触发一个reloadApp
function reloadApp() { if(hot) { log("info", "[WDS] App hot update..."); var hotEmitter = __webpack_require__("./node_modules/webpack/hot/emitter.js"); hotEmitter.emit("webpackHotUpdate", currentHash); if(typeof self !== "undefined") { // broadcast update to window self.postMessage("webpackHotUpdate" + currentHash, "*"); } } else { log("info", "[WDS] App updated. Reloading..."); self.location.reload(); } }
也就是说WDS先检测文件是否变化,然后通过sockjs通知到客户端,这样就实现了刷新。之前WebSocket的第三方只用过socket.io,看起来sockjs也蛮好用的。不必外带一个js,在主js里面就可以写了。
小结:效率提高的一方面是将一些机械的重复性流程或动作自动化起来。WDS和nodemon就是两个为你干活的小弟。
【webpack】-- 样式加载
2017-03-12 09:08 by stoneniqiu, 11 阅读, 0 评论, 收藏, 编辑
一,样式打包
1.安装css-loader,style-loader
npm install css-loader style-loader --save-dev
2.修改webpack.config.js
module:{ rules:[{ test:/\\.css$/, use: [\'style-loader\', \'css-loader\'], }] },
3.添加样式
body { background: cornsilk; }
然后在index.js中引入
import \'./main.css\';
再运行npm start,在http://localhost:8080/中打开
这时候页面出现了背景色,而且发现样式写入了header中,这个时候你改变颜色,界面也会无刷新的更新,这正是上一节HMR的效果。
样式也是通过webpackHotUpdate方法进行更新。
二、加载less
再看一下如何加载less,先安装less-loader
npm install less less-loader --save-dev
再修改配置文件:
module:{ rules:[{ test: /\\.less$/, use: [\'style-loader\', \'css-loader\', \'less-loader\'], }] },
然后建立一个less文件。less.less
@base: #f938ab; .box-shadow(@style, @c) when (iscolor(@c)) { -webkit-box-shadow: @style @c; box-shadow: @style @c; } .box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) { .box-shadow(@style, rgba(0, 0, 0, @alpha)); } .box { color: saturate(@base, 5%); border-color: lighten(@base, 30%); div { .box-shadow(0 0 5px, 30%) } } body { background: cornsilk; }
修改index.js
import \'./less.less\'; import component from \'./component\'; var ele=document.createElement("div"); ele.innerHTML="this is an box"; ele.className="box"; document.body.appendChild(ele); let demoComponent=component(); document.body.appendChild(demoComponent);
得到效果:
可以看见编译成功,要注意的是,再使用less的时候import只能是less文件,这个时候再import main.css会报错。这一节对less就做一个简单的演示,其他样式预处理器同理,下面的内容还是继续基于css。
三、理解css作用域和css 模块
一般来说css的作用域都是全局的,我们常在母版页里面添加了多个样式文件,后面的样式文件会覆盖前面的样式文件,常常给我们的调试带来麻烦。而CSS Modules通过import引入了本地作用域。这样能够避免命名空间冲突。webpack的css-loader是支持CSS Modules的,怎么理解呢,先看几个例子。我们先在配置中开启(先关掉HMR):
module:{ rules:[{ test:/\\.css$/, use: [\'style-loader\', { loader: \'css-loader\', options: { modules: true,//让css-loader支持Css Modules。 }, },],
然后定义一个新的样式(main.css):
body { background: cornsilk; } .redButton { background: red;color:yellow; }
给component加一个样式,先引入main.css。
import styles from \'./main.css\'; export default function () { var element = document.createElement(\'h1\'); element.className=styles.redButton; element.innerHTML = \'Hello webpack\'; return element; }
这个时候我们看到界面已经变化了。
再看右边生成的样式,我们的样式名称已经发生了改变。回顾整个过程相当于main.css中的每一个类名成了一个模块,在js中可以像获取模块一样的获取。但是你可能想,为毛我不能直接给元素赋值,干嘛要import呢。这是个好问题,我们再新增一个样式
不同样式文件的同名类
other.css
.redButton { background:rebeccapurple;color:snow; }
它也有一个.redbutton的类(但效果是紫色的),然后在index.js中创建一个div元素并给它添加redbutton样式。
import \'./main.css\'; import styles from \'./other.css\'; import component from \'./component\'; var ele=document.createElement("div"); ele.innerHTML="this is an other button"; ele.className=styles.redButton; document.body.appendChild(ele); let demoComponent=component(); document.body.appendChild(demoComponent);
再看效果
上面这个图说明了两问题,一个是我们在index.js中引入了2个样式文件,在index页面就输出了两个style,这让人有点不爽,但我们后面再解决。另外一个就是虽然两个样式文件中都有redButton这个类,但是这两者还是保持独立的。这样就避免了命名空间的相互干扰。如果你这个时候直接赋值
element.className="redButton";
这样是获取不到样式的。直接对元素的样式默认是全局的。
全局样式
如果想让某个样式是全局的。可以通过:global来包住。
other.css
:global(.redButton) { background:rebeccapurple;color:snow; border: 1px solid red; }
main.css
:global(.redButton) { background: red;color:yellow; }
这个时候redbutton这两个样式就会合并。需要直接通过样式名来获取。
element.className="redButton";
组合样式
我们再修改other.css,创建一个shadowButton 样式,内部通过composes组合redbutton类。
.redButton { background:rebeccapurple;color:snow; border: 1px solid red; } .shadowButton{ composes:redButton; box-shadow: 0 0 15px black; }
修改index.js:
var ele=document.createElement("div"); ele.innerHTML="this is an shadowButton button"; console.log(styles); ele.className=styles.shadowButton; document.body.appendChild(ele);
看一下是什么效果:
日志打印出来的是styles对象,它包含了两个类名。可以看见shadowButton是由两个类名组合而成的。div的class和下面的对应。
四、输出样式文件
npm install extract-text-webpack-plugin --save-dev
先安装extracttextplugin这个插件,然后再webpack.config.js中进行配置:
const ExtractTextPlugin = require(\'extract-text-webpack-plugin\'); const extractTxtplugin = new ExtractTextPlugin({ filename: \'[name].[contenthash:8].css\', }); const commonConfig={ entry: { app: PATHS.app, }, output: { path: PATHS.build, filename: \'[name].js\', }, module:{ rules:[{ test:/\\.css$/, use:extractTxtplugin.extract({ use:\'css-loader\', fallback: \'style-loader\', }) }]}, plugins: [ new HtmlWebpackPlugin({ title: \'Webpack demo\', }), extractTxtplugin ], }
一开始看到这个配置,让人有点懵。首先看fileName,表示最后输出的文件按照这个格式\'[name].[contenthash:8].css\',name默认是对应的文件夹名称(这里是app),contenthash会返回特定内容的hash值,而:8表示取前8位。当然你也可以按照其他的格式写,比如直接命名:
new ExtractTextPlugin(\'style.css\')
而ExtractTextPlugin.extract本身是一个loader。fallback:\'style-loader\'的意思但有css没有被提取(外部的css)的时候就用style-loader来处理。注意到现在我们的index.js如下:
import \'./main.css\'; import styles from \'./other.css\'; import component from \'./component\'; var ele=document.createElement("div"); ele.innerHTML="this is an box"; ele.className=styles.shadowButton; document.body.appendChild(ele); let demoComponent=component(); document.body.appendChild(demoComponent); //HMR 接口 if(module.hot){ module.hot.accept(\'./component\',()=>{ const nextComponent=component(); document.body.replaceChild(nextComponent,demoComponent); demoComponent=nextComponent; }) }
引入了两个css文件。
这个时候我们执行 npm run build
再看文件夹得到一个样式文件。(如果不想看到日志可以直接npm build)
但是我们在第三部分使用了CSS Modules,发现other.css的样式没有打包进来。所以,我们的webpack.config.js还要修改:
module:{ rules:[{ test:/\\.css$/, use:extractTxtplugin.extract({ use:[ { loader: \'css-loader\', options: { modules: true, }, }], fallback: \'style-loader\', }) }]},
再次build。
发现两个样式打包成了一个文件。只要内容发生了变化,样式的名称就会变化。更多配置可以移步https://www.npmjs.com/package/extract-text-webpack-plugin
参考:
https://www.npmjs.com/package/css-loader#local-scope
https://survivejs.com/webpack/styling/loading/