【webpack2】-- 入门与解析
1.初始化
npm init -y
这个命令会创建一个默认的package.json。它包含了项目的一些配置参数,通过它可以进行初始安装。详细参数:https://docs.npmjs.com/files/package.json。
不要y参数的话,会在命令框中设置各项参数,但觉得没啥必要。
2.安装webpack
npm install webpack --save-dev
3.目录结构
export default function () { var element = document.createElement('h1'); element.innerHTML = 'Hello world'; return element; }
component.js 是输出一个内容为h1元素。export default 是ES6语法,表示指定默认输出。import的时候不用带大括号。
import component from './component'; document.body.appendChild(component());
index.js 的作用就是引用Component模块,并在页面上输出一个h1元素。但完成这个还需要一个插件,因为目前我们还没有index.html文件。
npm install html-webpack-plugin --save-dev
4.设置 webpack 配置文件
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const PATHS = { app: path.join(__dirname, 'app'), build: path.join(__dirname, 'build'), }; module.exports = { entry: { app: PATHS.app, }, output: { path: PATHS.build, filename: '[name].js', }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack demo', }), ], };
第一次看到这个配置文件是有点懵,主要是exports,分三个部分,一个入口,一个输出,一个插件。入口指向了app文件夹。默认会把包含"index.js"的文件作为入口。输出指定了build地址和一个文件名;[name]这儿表示占位符,可以看成webpack提供的一个变量。这个具体后面再看。而HtmlWebpackPlugin会生成一个默认的html文件。
5.打包
这个输出包含了Hash(每次打包值都不同),Version,Time(耗时)。以及输出的文件信息。 这时打开build文件夹,发现多了一个app.js和index.html文件,双击index.html:
{ "name": "Html5", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "html-webpack-plugin": "^2.28.0", "webpack": "^2.2.1" } }
指定build。在cmd中执行npm run build 得到同样的结果
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Webpack demo</title> </head> <body> <script type="text/javascript" src="app.js"></script></body> </html>
默认引用了app.js。
6、解析
app.js
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.l = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 1); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony default export */ __webpack_exports__["a"] = function () { var element = document.createElement('h1'); element.innerHTML = 'Hello world'; return element; }; /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__component__ = __webpack_require__(0); document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__["a" /* default */])()); /***/ }) /******/ ]);
而app.js内容比较多了。整体是一个匿名函数。
(function(module) { })([(function (){}), function() {}])
app文件夹中的两个js文件成了这儿的两个模块。函数最开始是从__webpack_require__开始
return __webpack_require__(__webpack_require__.s = 1);
这里指定从模块1执行(赋值语句的返回值为其值)。而模块1的调用是通过__webpack_require__的这句执行的。
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
通过call调用模块的主要作用是为了把参数传过去。
(function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__component__ = __webpack_require__(0); document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__["a" /* default */])()); /***/ })
__webpack_require__ 每加载一个模块都会先去模块缓存中找,没有就新建一个module对象:
var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} };
模块1中加载了模块0,
var __WEBPACK_IMPORTED_MODULE_0__component__ = __webpack_require__(0);
__WEBPACK_IMPORTED_MODULE_0__component__ 返回的是这个模块0的exports部分。而之前Component.js的默认方法定义成了
__webpack_exports__["a"] = function () { var element = document.createElement('h1'); element.innerHTML = 'Hello world'; return element; }
所以再模块1的定义通过"a“来获取这个方法:
document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__["a" /* default */])());
这样就完整了,但这里使用了__webpack_require__.i 将原值返回。
/******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; };
不太明白这个i函数有什么作用。这个注释也不太明白,路过的大神希望可以指点下。
小结:
webpack通过一个立即执行的匿名函数将各个开发模块作为参数初始化,每个js文件(module)对应一个编号,每个js中export的方法或者对象有各自指定的关键字。通过这种方式将所有的模块和接口方法管理起来。然后先加载最后的一个模块(应该是引用别的模块的模块),这样进而去触发别的模块的加载,使整个js运行起来。到这基本了解了webpack的功能和部分原理,但略显复杂,且没有感受到有多大的好处。继续探索。
demo:https://files.cnblogs.com/files/stoneniqiu/webpack-ch1.zip 建议用最新的node安装,不然build后的结果可能出错。
参考:
https://survivejs.com/webpack/developing/getting-started/
【webpack】-- 模块热替换
2017-03-09 11:31 by stoneniqiu, 176 阅读, 0 评论, 收藏, 编辑
全称是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:https://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/