webpack练手项目之easySlide
http://www.cnblogs.com/souvenir/p/5006559.html
http://www.cnblogs.com/souvenir/p/5010309.html
http://www.cnblogs.com/souvenir/p/5012552.html
webpack练手项目之easySlide(一):初探webpack
最近在学习webpack,正好拿了之前做的一个小组件,图片轮播来做了下练手,让我们一起来初步感受下webpack的神奇魅力。
webpack是一个前端的打包管理工具,大家可以前往:http://webpack.github.io/ 作详细了解。相对于之前的前端模块打包工具,
个人认为webpack至少拥有以下值得我们拿来一用的优点:
- js/css/img/html等等都是静态资源,都可以通过webpack进行打包处理
- 所有资源都可以按需加载,避免了之前的加载器把所有资源打包在一个文件,导致文件过大而且不需要的模块也加载出来;同时也避免了将资源按照独立文件进行打包,从而导致大量的HTTP请求造成降低页面性能
- 提供了很多前端打包所需要的配套小插件,比如:JS压缩,JSHint,图片压缩等等
- 完美兼容了AMD和CommonJS以及ES6语法,大家之前写的模块不用再重新进行改造了。
当前很多优点是我没有提及的或者还没有了解到的,但是目前这几个优点来说,已经算是可以完全满足我们在项目中的实际打包需求了。
这次算是把webpack模块打包与之前的一篇文章: http://www.cnblogs.com/souvenir/p/4977407.html 中提及的图片轮播二者结合起来,一起与大家进行分享。
1.先看DEMO
同样的,这次我也将这次写的DEMO代码分享到Github上,大家可以自行前往查看源码: https://github.com/xiaoyunchen/easySlide/
最后的页面效果大家可以访问 http://xiaoyunchen.github.io/easySlide/ 进行查看。
功能很简单:
可以看到,我们在这个页面上做了两个图片轮播效果,而且两个图片轮播的时间间隔与动画时长都是独立的,互不干扰。

2.准备工作
在开始学习webpack打包之前,我们先要做一些准备工作。第一步当然安装nodejs了,然后再使用npm命令安装webpack以及我们所需要的几个加载器:
npm install webpack -g npm install jquery@1 npm install css-loader npm install style-loader
3.webpack打包
看完最终的效果后,我们接下来继续来看这个简单的项目是如何使用webpack进行打包的。
首先大家在Github可以先打开项目的源码,可以看到项目的目录结构是这样的:

最外层有几个文件:
index.html --- 项目入口页面
package.json --- nodejs环境下用于描述模块包结构的文件
webpack.config.js --- webpack配置文件,稍后我们将重点分析这个配置文件
然后就是三个目录:
src --- 项目开发的源码
node_modules --- 项目打包中用到的node模块
dist --- 打包后最终的输出目录
再来看看src目录的结构,先按照常规的css/js/img进行划分,然后每个目录下在按照功能模块进行子目录划分:
module --- 通用组件
page --- 页面应用
vendor ---引用第三方组件
这是我个人的一个目录划分,实际的项目中大家可以根据项目或者公司的需要进行调整。
接着来看入口文件:index.js
1 (function(){
2 //引入公共CSS与页面CSS
3 require('../../css/vendor/reset.css');
4 require('../../css/page/index.css');
5
6 //引入并创建多个独立slideModule模块
7 var slideModule=require("../module/slide.js");
8 new slideModule({dom:$('[node-type="iccAdvisorPicture"]')});
9 new slideModule({
10 dom:$('[node-type="iccAdvisorPicture2"]'),
11 delay:4000,
12 duration:800
13 });
14 })();
代码量14行,整体来说还算是比较清爽的,这都得益于模块打包。
在这里,我们定义并执行了一个闭包函数。主要功能就是两个:
1.加载改页面上的公共CSS (别忘了css也是一种资源,我们可以通过webpack来进行打包加载)
2.引入了我们自定义了slideModule组件,然后使用该组件创建了两个图片轮播的实例
就是这么简洁,这也是我们所希望的,将功能按模块进行开发,使用的时候按照需要进行加载。
我们先不管slideModule是如何具体实现这个功能的,我们接着来看webpack的配置文件:
1 var path=require('path');
2 var webpack = require('webpack');
3 module.exports = {
4 entry: {
5 index:"./src/js/page/index.js",
6 },
7 output: {
8 path: path.join(__dirname,'dist'),
9 filename: "bundle.js"
10 },
11 module: {
12 loaders: [ //css加载器
13 { test: /\.css$/, loader: "style!css" }
14 ]
15 },
16 plugins:[
17 new webpack.ProvidePlugin({ //加载jq
18 $: 'jquery'
19 }),
20 new webpack.optimize.UglifyJsPlugin({ //压缩代码
21 compress: {
22 warnings: false
23 },
24 except: ['$super', '$', 'exports', 'require'] //排除关键字
25 })
26 ]
27 };
关于这个配置文件中详细参数与属性,大家可以前往webpack官网进行查看。这里我们主要讲解下这个配置文件所要达到的目的。
entry:入口。注意这里的路径是相对于webpack.config.js的路径,也就是根目录
path:主要是定义了打包后的文件存放目录和文件名,这里我们是将打包后的文件存放在/dist/bundle.js文件中。
module-loaders:加载器。这里我们只使用了一个CSS加载器
plugins:插件。第一个是jquery,我们将jquery加载进行项目中并将$作为全局变量返回,所以在任何位置都可以使用jquery而且无需更多配置。
第二个是对输出的的js代码进行压缩,这一步是可选的,一般也可以将有部署服务器将部署到正式环境之前在进行压缩处理。
OK,接下来我们就可以使用webpack进行打包了,在命令行切换当前项目所在目录,然后打包使用:
webpack -w
然后仅能看到类似于下图的输出结果,没有任何报错的话说明打包已经成功:

-w 是打包选项,watch的意思,webpack将监控项目的文件如果有修改变动的时候,将会自动运行打包命令
其他的选项还有:-p 压缩代码。但是一般我们都将代码压缩卸载配置文件中。
-d 输出sourcemap
打包成功后我们在index.html页面中就只需要引入/dist/bundle.js即可,连css都无需再引入。
然后就可以运行页面查看具体的效果。
OK,webpack打包过程大概就是这样,相信大家可能会有一些疑问,这不就是把所有资源文件都放在一个文件里面吗,如果项目太大的话,那这个文件还不得很大了。
这里就涉及到之前说的webpack可以实现按需加载模块,我们将在下一篇为大家进行介绍有关内容。
4.slideModule 模块
接下来我们来看图片轮播这个组件是如何实现的,以及在实现的过程如何使用webpack语法进行资源加载。
这是我们的代码截图,所有的代码同样都是在一个闭包函数中的,这样做可以避免对全局变量window的污染。
第2行我们使用require引入了一个css文件,这个CSS是专属于图片轮播模块的,在模块里进行引入,屏蔽具体实现,外面的js在使用的使用不用再关心是不是还要在
引入额外的css,只需要一句话引入然后完成相应的功能。
第4行定义了一个默认配置对象,用于定义模块的一些基础配置,如果在使用的时候不传入对应的参数我们将默认使用该默认配置。
第12行定义了一个方法,这个方法其实也就是我们图片滑动模块的构建函数,在这个构造函数里我们首先将外层传入的配置参数与默认参数进行合并。
然后在根据dom选择器重新计算图片数量。
在19行我们使用prototype对slideModule这个方法进行了扩展,增加了几个处理方法
1 slideModule.prototype={
2 init:function(){
3 this.bindMouseEvent();
4 this.autoPlay();
5 },
6 slidePic:function(){ //切换图片
7 var that=this;
8 this.config.dom.animate({'marginLeft':-(this.config.current==this.config.total?0:this.config.current)*this.config.width+'px'},this.config.duration,function(){
9 that.config.current++;
10 if(that.config.current>that.config.total){
11 that.config.current=1;
12 }
13 });
14 },
15 autoPlay:function(){ //自动切换
16 var that=this;
17 this.config.timer=setInterval(function(){
18 that.slidePic();
19 }, this.config.delay);
20 },
21 bindMouseEvent:function(){ //绑定鼠标移入/移除事件
22 var that=this;
23 this.config.dom.mouseenter(function(){
24 if(that.config.timer){
25 clearInterval(that.config.timer);
26 }
27 });
28 this.config.dom.mouseleave(function(){
29 that.autoPlay();
30 });
31 }
32 };
init:模块初始化方法,负责调用对应函数对模块进行功能初始化
slidePic:图片切换的具体实现方法,这里使用了jquery的animate方法,创建了一个动画,将图片外层父级元素marginLeft减少一个图片的宽度
整个动画的时长来自于配置信息。
滑动动画结束后将修改当前显示的是第几个图片,如果超过最大数量的话就设置回1,让动画下次从头开始。
autoPlay:创建一个定时器,每隔一段时间自动执行slidePic来切换图片,从而实现了自动轮播的效果。
这里之所以使用闭包函数,是因为作用域的原因,详细的介绍大家可以查看之前的文章。
bindMouseEvent:这里增加了两个鼠标事件,当鼠标移入滑动组件区域内时,清除掉定时器暂时动画,当鼠标离开时重新开启定时器,继续执行轮播动画
图片轮播的实现原理大概是这样的:

最外层的div宽度固定,与单个图片宽度(加载边距)相同,同时设置了超出的部分隐藏显示;然后ul宽度设置为很大, 至少需要N呗的图片宽度,可以让所有图片放在一行
然后定时器来改变ul的margin-left值,从而达到滑动切换的效果。
webpack练手项目之easySlide(二):代码分割
Hello,大家好。
在上一篇 webpack练手项目之easySlide(一):初探webpack 中我们一起为大家介绍了webpack的基本用法,使用webpack对前端代码进行模块化打包。
但是乍一看webpack只是将所有资源打包到一个JS文件中而已,并没有做到真正的按需加载,这当然不是我们所想要的。
不急,今天的这一章我们就来一起继续探索webpack的另外一个功能:code split.
1.什么是code split
英文不好,暂且将其翻译为代码分割。也就是我们根据实际业务需求将代码进行分割,然后在合适的时候在将其加载进入文档中。
这里举一个实际应用场景:上次我们做的图片轮播,我们为每张图片都添加一个点击事件,点击以后我们弹出一个对话框,里面介绍一些详细内容,然后可以点击关闭按钮进行关闭。
在这个需求中我们发现,对话框这个组件比较特殊,他是在用户点击图片以后才需要加载,如果用户不点击,那么他就没有必要加载出来了。
OK,很好。webpack通过code split方法将页面必须加载的资源放在bundle.js中,然后对于按需加载的资源通过ajax进行异步加载。
webpack通过 require.ensure 来判断是否对资源进行按需加载。
下面是官网的简单用例:
1 require.ensure(["module-a", "module-b"], function(require) {
2 var a = require("module-a");
3 // ...
4 });
2.Demo与Code
同样的,我已经将上面所说的对话框按需加载实现,大家感兴趣的话可以前往查看:
Demo: http://xiaoyunchen.github.io/easySlide/
Code: https://github.com/xiaoyunchen/easySlide
大家可以打开控制台网络面板,查看资源的加载情况:
页面加载的时候只有7个请求,这其中其实并没有包含我们的对话框组件:

然后大家可以点击任意一张图片,这个时候网络浏览器发送了新的网络请求,然后页面上也打开了对话框:

这个2.chunk.js也就是webpack为我们打包的对话框组件,包括JS逻辑,HTML模板以及CSS样式,稍后我们将为大家作详细介绍。
(demo上有几个menu的link,大家先不用管,那是我们下一章将要介绍的内容)
3.组件引用与webpack打包
接下来我们来看看代码上都发生了哪些变动。
首先是index.html与index.css,并没有任何修改。(index.html中增加了header与footer,增加了多个图片轮播组件,为下一章做的准备。但是都与对话框组件没有任何关系)
那来看看index.js的修改,具体的源码大家可以查看github,这里只对后面新增的代码进行分析:
1 //添加对话框事件
2 var pageDialog=false;
3 $('.pictureShow a').click(function(){
4 var _id=$(this).attr('dialog-for');
5 require.ensure(["../module/dialog.js","../module/dialogConfig.js"], function(require) {
6 var dialogModule=require("../module/dialog.js");
7 var dialogConfig=require("../module/dialogConfig.js");
8 if(!pageDialog){ //判断对话框组件是否存在,避免重复创建
9 pageDialog=new dialogModule();
10 }
11 pageDialog.openDialogWith(dialogConfig[_id]);
12 });
13 });
首先我们定义了一个对象,然后为页面上所有的滑动元素增加了一个单击事件。
第4行:获取当前事件元素的dialog-for属性,这是我们在每个滑动元素上新增的属性,用于指定其对应的对话框id
第5行:使用了webpack的require.ensure异步加载了两个组件:dialog与dialogConfig,这两个组件分别是对话框的具体实现逻辑以及对话框内容配置信息,详细的代码我们后面再分析
第6/7行:加载完成后得到两个组件的引用
第8-10行:判断pageDialog是否存在,如果不存在我们通过调用dialog组件new一个实例,并将赋值给pageDialog。
这里主要是为了避免多次点击时页面重复创建dialog Html元素,降低页面性能
第11行:使用pageDialog实例,调用openDialogWith方法来打开对话框,同时要从dialogConfig中加载指定的对话框配置内容。
是的,哪怕是在我们没有查看dialog组件具体源码的情况下,整个逻辑还是相对比较清晰的。我们不用理会dialog组件调用了什么模板,用了什么css样式,内部实现了哪些方法。
定义组件的一大目的就是降低代码之前的耦合性,作为组件,我只管定义如何实现一个组件;作为组件调用者,我只管衣来伸手饭来张口的拿来主义,
不管你内部是如何实现的。
接下来看webpack是如果对上面的代码进行打包的:
以下是webpack.config.js配置文件部分内容:
1 module.exports = {
2 entry: {
3 index:"./src/js/page/index.js",
4 delegate:"./src/js/page/jsEvent.js"
5 },
6 output: {
7 path: path.join(__dirname,'dist'),
8 publicPath: "/easySlide/dist/",
9 filename: "[name].js",
10 chunkFilename: "[id].chunk.js"
11 },
12 module: {
13 loaders: [ //加载器
14 {test: /\.css$/, loader: "style!css" },
15 {test: /\.html$/, loader: "html" },
16 {test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
17 ]
18 }
19
20 };
改动的地方不多,主要是以下几点:
第10行:定义了chunk的文件名命名规则,这里除了id以外,还可以使用[hash]
第8行:publicPath 这个配置很容易被疏忽。我们的chunk文件默认是跟bundle放在一块的,都是在dist目录下,如果不配置正确的publicPath的话,webpack请求chunk文件时将会默认请求根目录
第15行:新增了HTML加载器,主要是给dialog组件加载模板使用的
codeSplit作为webpack一个比较核心的功能,所以也需要额外的plugins插件配置就能支持。
在项目根目录下运行 webpack 打包命令以后可以看到dist目录下就会新增一个 2.chunk.js文件 (2是chunk的ID值)
值得一提的是,chunk文件完全由webpack进行管理和使用,我们无需额外的干预
4.dialog组件的实现
dialog的实现原理比较简单,我们定义了一个dialog容器,通过css控制其固定在整个页面之上,然后在生成一个mask阴影层。
具体的实现css样式大家也可以前往github查看源码。
dialog组件的实现方法也是与我们之前做的slideModule比较类似:

第10-11行:加载了dialog组件的HTML模板,并将其放置在body中。嗯,之所有是要使用html模板也是为了解耦合,方便对模板和JS进行单独维护。这里webpack在打包的时候
会自动将html模板压缩成字符串保存在chunk文件中。
第16-44行:定义了dialog组件的几个接口方法,其中openDialogWith 就是我们之前调用的根据配置内容更新dialog DOM信息,然后将dialog展示出来。
对于dialogConfig我们也简单的来看下:

这里全部都是对话框的数据配置项,单独维护的好处也是解耦合,以外咱们可以将这些数据配置在数据库中,通过接同样的接口格式返回来就行了。
其实我这里的组件定义还有一个问题:
我们是将自定义的对象直接作为组件导出,开放给调用者使用。这在操作上其实存在一定的风险性,因为所有接口方法和属性都是开放的,那就有可能被使用者
给篡改,影响到组件的正常使用。正确的做法是:
在组件内部定义一个私有变量,用户存放组件所有属性与方法,然后再将接口方法(一般不建议开放属性给调用者直接修改)导出给调用者使用。
小结:
通过webpack的codeSplit方法我们可以对代码进行按需分割以及加载,从而到达页面性能的最优。
webpack练手项目之easySlide(三):commonChunks
Hello,大家好。
在之前两篇文章中:
webpack练手项目之easySlide(一):初探webpack
与大家分享了webpack的基本使用方法,以及使用webpack对代码进行分割,根据需求进行异步加载。
今天我们继续为大家介绍webpack的其他应用方法,主要包括common chunks以及web-dev-server。
1.Demo与Code
在实际的项目开发过程中,我们的项目中会有很多公共的部分,比如一些第三方的组件,CSS样式等。通过添加一些配置,webpack在打包过程中自动的提取出这些公共元素,并将其打包到一个独立的文件中。在页面上我们只需要引入这个公共的脚本以及页面单独的脚本即可。
如果大家注意的话,在上一章中我们使用的Demo就已经使用了common chunks:
Demo:http://xiaoyunchen.github.io/easySlide/
Code:https://github.com/xiaoyunchen/easySlide
为了演示效果,我们新增了一个页面入口,叫做jsEvent.html,现实的功能也就是之前在 http://www.cnblogs.com/souvenir/p/4988367.html 已经实现的功能,演示JS事件流的传播。只是这次稍微将代码改成模块化的形式。
OK。同一个项目下有两个页面(实际项目中会更多入口或者页面),这两个页面都需要引用一些公共的组件,比如头部导航菜单,footer脚部信息等,这些组件一般都是全站通用的,然后每个页面进行单独调用即可。
我们在项目下增加了一个jsEvent的模块,引入了jsEvent的页面和调用JS:

关于jsEvent模块的实现我们稍后再看,先来看看jsEvent入口JS文件的内容:
1 (function(){
2 //引入公共CSS与页面CSS
3 require('../../css/vendor/reset.css');
4 require('../../css/page/jsEvent.css');
5 require('../../css/module/footer.css');
6
7 //引入header
8 require("../module/header.js");
9
10 //引入jsEvent模块
11 var Delegate=require("../module/jsEvent.js");
12 var delegate=new Delegate();
13 delegate.addBodyListener();
14 delegate.addListElement($('#list1'),$('#btn1'));
15 delegate.addListElement($('#list2'),$('#btn2'));
16
17 })();
同样的,前三行代码我们先引入了公共CSS和本页面所需的CSS。
第8行:引入了header模块。因为header模块除了样式以外只有一个页面滚动式固定的功能,所以这里也需要调用其任何方法,只需引入该模块即可。
第11-15行:引入jsEvent模块,并配置相关事件监听。关于jsEvent这个模块的实现大家可以查看之前的文章或者源码,这里不再作赘述。
同样的大家可以再来回顾一下Index.js的源码:

是的,前面引入公共CSS以及引入header部分是完全一致的,这也就是我们所说的common chunks公共部分。
接下来我们再来看webpack.config.js中的配置内容:

在配置文件中先定义了这两个入口文件:Index与delegate,然后这里新引入了一个新的插件:CommonsChunkPlugin (官方文档)
new webpack.optimize.CommonsChunkPlugin("commons.js", ["index", "delegate"])
通过这句代码我们将Index与delegate中的公共部分提取出来并放置在commons.js这个单独的文件中,所以我们在页面中除了需要引入index/jsevent以外,还需要引入这个commons。
再来看看CommonsChunkPlugin 的其他配置选项:
minChunks :公共模块被使用的最小次数。比如配置为3,也就是同一个模块只有被3个以外的页面同时引用时才会被提取出来作为common chunks。
minSize:作用类似于minChunks,只不过这里控制的文件大小。
children:这个参数比较有意思,他可以将common chunks不单独存放,而是将其加入到所引用的页面JS中进行合并。关于这个参数我也定义了一个webpack.config2.js以及index2.html用于测试,大家使用 webpack --config webpack.config2.js 就能单独打一次包,然后发现这次并没有生成commons.js,因为已经被加入到index.js中,所以在index2.html我们只需要引入bundle.js即可。(咦,怎么感觉又回去了呢)
2.header组件
这是一个单独的组件,功能比较简单,作为页面的一个导航,同时在页面滚动的时候固定在最顶部。
我们来看看具体的实现代码:
1 (function(){
2 require('../../css/module/header.css');
3
4 var headerModule={
5 config:{ //配置信息,头部class/多少高度触发fixed,fixed class名
6 headerDom:$('.headerWrapper'),
7 fixedTop:80,
8 fixedCls:'header-fixed'
9 },
10 headerFixed:function(){ //切换class
11 if ($(window).scrollTop()>headerModule.config.fixedTop){
12 headerModule.config.headerDom.addClass(headerModule.config.fixedCls);
13 }else{
14 headerModule.config.headerDom.removeClass(headerModule.config.fixedCls);
15 }
16 },
17 init:function(){ //为window绑定scroll事件
18 $(window).bind('scroll',function(){
19 headerModule.headerFixed();
20 });
21 }
22 };
23 headerModule.init();
24 module.exports=null;
25 })();
第2行,不多说,引入header模块所需要的css样式。
第4行,定义了header组件的实现变量;
第5行,定义了一些配置信息,具体信息大家看注释。
第10行,headerFixed:判断当前滚动的高度是否大于咱们的配置,如果是的话就添加一个class。
第17行,init行数:初始化函数,为window绑定了这个scroll滚动事件。
第23-24行,主动调用了init行数,所以这里我们的exports为null,没有任何需要导出的内容。
这里需要一下两个问题:
1.关于header的html模板,这里没有加载器来做异步加载,而是直接写在每个页面中的,其实有些增加了维护工作量。实际项目的做法应该是将这个header.html的模板文件独立出来,然后通过后端的语法(比如PHP或者Java)来进行引入。这个工作如果由前端来完成的话,需要单独一个HTTP请求同时需要重新渲染DOM,比较消耗浏览器的性能,所以不推荐。
2.关于header模块的配置信息。根据一般组件的做法,这个参数应该是需要是暴露接口,让调用者可以修改的,但是我们考略到header的特殊性,作为全站的导航菜单,一般都是样式与功能统一的,所以这里并没有开放修改接口。
好的,我们来看下最后的页面效果:

当我们滚动向下滚动页面后,header上增加了一个class,将header固定在了页面顶部。
3.web-dev-server
这里再单独介绍一个webpack的小功能:web-dev-server.
这个功能的作用在于每次修改保存后,webpack将会自动重新对资源进行打包,并且自动刷新页面。(使用的是socket.io进行通信)
首先通过npm安装web-dev-server
npm install web-dev-server --save--dev
安装完成后再通过 web-dev-server 即可启动server,启动成功后命令行会出现如下的提示:
webpack:bundle is now VALID.
然后我们只需要访问 http://localhost:8080/ 就可以访问到我们的项目页面。
可以尝试修改一下任何一个资源文件,然后会发现控制台上webpack重新进行了编译,而且浏览器也自动进行了刷新,无需我们手动干预。(双屏开发的同学可以开始鼓掌了...)
小结:
截止到这里为止,我们对webpack的学习就暂时告一个段落。
webpack是一款非常优秀的前端模块化打包工具,他还有很多实用的功能,因为目前我的项目中还没有涉及到,所以还没来得及做深入研究。比如对react/angularjs这样的前端框架,LESS/JADE等这些模板或者css工具的编译与支持,同时还兼容了AMD/CommonJS/ES6语法,功能十分强大,值得我们好好研究。

浙公网安备 33010602011771号