Hello,大家好!在前面三章中我们与大家分享如何使用webpack来完成前端的模块化打包工作,并附带了一些简单的案例实现,算是webpack的一个入门吧。
从这一章开始,我们将陆续为大家介绍如何使用webpack来完成前端工程化开发。
1.什么是工程化开发
软件工程的工程化开发概念由来已久,但对于前端开发来说,我们没有像VS或者eclipse这样量身打造的IDE,因为在大多数人眼中,前端代码无需编译,因此只要一个浏览器来运行调试就行了。但是时至今日,互联网特别是移动互联网为前端开发带来了更大的机会,同时前端代码也变得越来越复杂,越来越难以管理,因此前端工程化开发的工作可以说是刻不容缓。
那么前端工程化开发到底需要解决哪些前端工程师们火烧眉毛的问题呢?个人认为至少包含以下几点:
1. 专业的IDE支持,完成包括项目初始化,语法提示,项目编译,打包等工作。
2. 良好的模块化代码管理结构,模块化可以使我们编写的组件或者代码达到高度复用,降低代码间的耦合性;同时可以良好的与第三方组件兼容。
3. 简单易配置的前端测试环境,完成组件的单元测试,页面的集成测试;同时提供良好的DEBUG环境,可以很好的定位错误的所在以及错误的详细信息。
4. 静态资源(图片/字体/CSS/JS等)的良好管理方案,一是静态的文件版本问题,二是对于小图标自动转BASE64,减少HTTP请求
5. 完整的代码版本管理,打包,发布,多环境部署,测试反馈等运维支持
当然,以上只是个人意见。每个公司、每个项目的情况不一样,所需要的条件都会有所不同。
以上这些需求,在以前基本上都是不敢想象的,这些工具都需要访问文件系统或者网络,很少有比较完整的解决方案。(好了,早期也有类似于Ant这样的Java解决方法,对前端开发人员要求过高而且不是很好用)好在,伟大的NodeJs诞生了,于是乎,基于NodeJs出现了很多优秀框架,像Grunt和 Gulp等。
好了,今天的主题是webpack!让我们来看下webpack作为后起之秀,是如何对前端工程化进行支持的!
2. 动态生成HTML
大家注意,这里所说的动态生成HTML,是指我们使用webpack来动态产生我们最终所期望的HTML文件,而不是指在浏览器运行时使用JS生成HTML片段。
那为什么要动态生成HTML,我自己写不行吗?答案当然是可以的。
之所以要动态生成,主要是希望webpack在完成前端资源打包以后,自动将打包后的资源路径和版本号写入HTML中,达到自动化的效果。
大家可以回想一下我们之前的三篇文章中介绍的案例,在那个练手的项目中,我们页面上的script标签是我们自己写的,那么如果我们需要给JS添加上版本号的话,岂不是每次都的去修改?还有CSS,都是内嵌在JS中的,待JS加载后再创建style标签,然后写入css内容。这么做的话,浏览器需要先等待JS加载完成后,才能生成CSS样式,页面上会有一个等待过程,这个过程页面是完全没有样式的。这当然不是我们所想要的。
我们的目标是:webpack根据指定的模板,插入打包编译后CSS文件路径;插入打包生成的JS的文件路径。并且还需要为二者添加版本号。另外,我们还可以同时将HTML进行压缩,进一步减少文件大小。
3.初始化项目、安装依赖
我们这里默认大家的开发环境已经安装了npm以及webpack,尚未安装的同学可以自行安装。
A.新建一个空项目,然后通过 npm init 对项目进行初始化,按照提示输入项目的基本信息,然后生成package.json文件。这个文件里面会保存我们即将安装的一些npm插件信息,便于二次移植开发。
B.安装项目所需依赖:
npm install css-loader jquery@1 style-loader html-webpack-plugin --save-dev
npm install extract-text-webpack-plugin --save-dev
依次为css加载器,jquery,style加载器,HTML生成插件以及文件提取插件。
最终package.json的依赖声明得到更新:
"devDependencies": {
"css-loader": "^0.23.0",
"extract-text-webpack-plugin": "^0.9.1",
"html-webpack-plugin": "^1.7.0",
"jquery": "^1.11.3",
"style-loader": "^0.13.0",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.14.0"
}
4. 创建目录,添加测试文件
准备就绪,我们开始创建项目目录:
- webapp
- src #代码开发目录
- css #css目录,按照页面(模块)、通用、第三方三个级别进行组织
+ page
+ common
+ lib
+ img #图片资源
- js #JS脚本,按照page、components进行组织
+ page
+ view #HTML模板
- dist #webpack编译打包输出目录,同样按照css/js/img进行组织
+ css
+ js
+ view
+ node_modules #所使用的nodejs模块
package.json #项目配置
webpack.config.js #webpack配置
README.md #项目说明
dist目录也可以不创建任何子目录,这里为了方便查看,将js/css/html分开存放。
然后我们在src目录创建几个测试文件,详细的代码大家可以前往https://github.com/xiaoyunchen/webpack/tree/master/src 查看源码。
我们先来看下index.js的内容:
1 //引入CSS
2 require("../../css/lib/reset.css");
3 require("../../css/common/global.css");
4 require("../../css/page/index.css");
5
6 document.write('Hello Index Js');
代码很简单,主要是引入了几个css文件。再看下我们的 /view/index.html 这个模板的内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Index主页</title>
<meta name="author" content="https://github.com/xiaoyunchen/webpack"/>
<meta name="date" content="2015-12-3"/>
<meta name="description" content="基于webpack的前端工程化开发解决方案探索"/>
<!--
作者:https://github.com/xiaoyunchen/webpack
时间:2015-12-03
描述:head中无需再引入css以及facicon,webpack将根据入口JS文件的要求自动实现按需加载或者生成style标签
-->
</head>
<body>
<p>Hello,Webpack!!</p>
<!--
作者:chyun532@qq.com
时间:https://github.com/xiaoyunchen/webpack
描述:body中同样无需单独引入JS文件,webpack会根据入口JS文件自动实现按需加载或者生成script标签,还可以生成对应的hash值
-->
</body>
</html>
这是一个简单的HTML模板,值得一提的是我们在这里并没有引入任何的CSS和JS,我们希望通过webpack打包来自动生成。(这里的模板还支持 Blueimp)
5. webpack配置
最后是我们的重头戏,webpack.config.js,我们将在这里配置一些webpack任务,来完成我们的需求:
1 var path=require('path');
2 var webpack = require('webpack');
3 var ExtractTextPlugin = require("extract-text-webpack-plugin");
4 var HtmlWebpackPlugin = require('html-webpack-plugin');
5 module.exports={
6 entry:{
7 index:"./src/js/page/index.js",
8 },
9 output:{
10 path: path.join(__dirname,'dist'),
11 publicPath: "/webpack/dist/",
12 filename: "js/[name].js",
13 chunkFilename: "js/[id].chunk.js"
14 },
15 module: {
16 loaders: [ //加载器
17 {test: /\.css$/, loader:ExtractTextPlugin.extract("style", "css") }
18 ]
19 },
20 plugins:[
21 new ExtractTextPlugin("css/[name].css"), //单独使用style标签加载css并设置其路径
22 new HtmlWebpackPlugin({ //根据模板插入css/js等生成最终HTML
23 favicon:'./src/img/favicon.ico', //favicon路径
24 filename:'/view/index.html', //生成的html存放路径,相对于 path
25 template:'./src/view/index.html', //html模板路径
26 inject:true, //允许插件修改哪些内容,包括head与body
27 hash:true, //为静态资源生成hash值
28 minify:{ //压缩HTML文件
29 removeComments:true, //移除HTML中的注释
30 collapseWhitespace:false //删除空白符与换行符
31 }
32 })
33 ]
34 };
这个配置文件再之前的文章中我们已经讲过很多次了,这里就不在赘述,只对其中几点比较关键的点进行分析:
17行:css加载器,只是这里改用了文件提取插件,将css提取出来单独作为一个文件进行存储。
21行:配置提取出来的css文件名以及存放路径
22行: html-webpack-plugin 这是webpack中生成HTML的插件,里面有详细的配置说明,大家可以前往查看。
23行:配置favicon,通过webpack引入同时可以生成hash值
24行:配置最终生成HTML文件存放路径
25行:我们所使用的模板
26行:允许webpack修改哪些内容,可选值为head和body,true的话是都可以修改
27行:为静态资源生成hash值
28行:压缩最终生成的HTML文件,相关配置参数请前往 html-minifer 查看。(这里为了方便后面查看,没有移除HTML中的空白符与换行符。)
OK,下面我们在项目的根目录下运行 webpack 打包命令完成项目打包:
打包成功后,我们前往/dist/view目录下查看生成的index.html是什么样
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Index主页</title>
<meta name="author" content="https://github.com/xiaoyunchen/webpack">
<meta name="date" content="2015-12-3">
<meta name="description" content="基于webpack的前端工程化开发解决方案探索">
<link rel="shortcut icon" href="/webpack/dist/favicon.ico?69fed78822d5f8d3895c"><link href="/webpack/dist/css/index.css?69fed78822d5f8d3895c" rel="stylesheet"></head>
<body>
<p>Hello,Webpack!!</p>
<script src="/webpack/dist/js/index.js?69fed78822d5f8d3895c"></script></body>
</html>
可以看到生成的文件除了保留原模板中的内容以外,还根据入口文件index.js的定义,自动添加需要引入CSS与JS文件,以及favicon,同时还添加了相应的hash值。
运行这个文件,可以看到代码正常,引入的文件路径也都OK!
是的,我们最初想要通过动态生成HTML的目的达到了。
webpack根据指定的模板,插入打包编译后CSS文件路径;
插入打包生成的JS的文件路径。并且还需要为二者添加版本号。
另外,我们还可以同时将HTML进行压缩,进一步减少文件大小。
今天的分享就到这里。可能大家还有些疑问:按需加载的JS/CSS也会被提取出来吗?下一章我们将继续探索这个问题。
今天我们继续来进行webpack工程化开发的探索!
首先来验证上一篇文章 基于webpack的前端工程化开发解决方案探索(一):动态生成HTML 中的遗留问题:webpack将如何处理按需加载的资源,还能继续通过AJAX进行异步加载吗?
1. require.ensure
在上一章我们已经知道通过require引入的资源,可以通过插件让webpack将其独立成为单独的文件,然后向HTML中自动写入路径。那对于require.ensure情况又会是怎样的情况呢?
我们都知道webpack通过require.ensure来对我们的代码进行分割,将按需加载的代码单独放在的块文件chunk中,然后在合适的时候异步加载进入文档中。
在webpack中引入的提取文件插件,是否影响这一功能呢?
同样的,这次我们对上次的项目进行了改动,具体代码可以查看: https://github.com/xiaoyunchen/webpack
首先,我们在JS下新增了一个components文件夹,用于存放自定义的组件,然后定义了一个dialog组件(dialog的实现请参考之前的文章,本章将对本部分进行简化处理)。
在dialog组件中我们定义了dialog所需的HTML模板,CSS样式文件,以及入口文件Index.js(如果模块逻辑层次很复杂的话,这里还可以再新建两个关于模板和样式的子目录)
我们稍微看下index.js的内容:
1 (function(){
2 //加载模块CSS
3 require('./dialog.css');
4 //加载模板
5 var html=require('./dialog.html');
6
7 module.exports=function(text){
8 $('body').append(html);
9 $('.dialog:last-child').html(text);
10 };
11
12 })();
这里只是出于演示使用,所以实现的功能与逻辑比较简单。就是引入了所需的模板和样式文件,然后导出一个方法,改方法将会向body插入一个元素。
OK,我们再来看下page目录下index.js这个入口文件的变动:
1 //引入CSS
2 require("../../css/lib/reset.css");
3 require("../../css/common/global.css");
4 require("../../css/page/index.css");
5
6 //增加事件
7 $('#btn').click(function(){
8 require.ensure(['../components/dialog/index.js'],function(require){
9 var Dialog=require('../components/dialog/index.js');
10 new Dialog(new Date()-0);
11 });
12 });
第7行,为页面的一个按钮添加了点击事件,点击后加载dialog组件,然后生成一个dialog实例。
再来看看webpack配置中添加了什么内容:
可以看出配置文件并没有太大的变化,这里主要是:
18行:增加了HTML加载器,用于加载HTML模板
22行:引入全局jq,方便其他JS调用
在项目根目录下运行 webpack 打包命令后,可以看到dist/js下多了1.chunk.js文件。其实看到这里大家也就放心了,webpack的确正确处理了这种按需加载的关系。
然后运行dist/view/index.html,打开控制台观察资源加载。
一开始并没有加载dialog组件,点击按钮后,浏览器开始异步加载dialog组件,然后生成对应的HTML.
这里有个问题需要单独说明下,require.ensure 被webpack编译后在执行的时候会自动判断该模块已经下载,如果已经下载就不会再重复请求。
2. 图片加载
借助于url-loader这个加载器,在webpack中我们可以比较优雅的处理图片加载的问题。所谓的比较优雅,是指:
1. webpack可以将所用到的图片自动拷贝到输出目录下,同样可以为其添加hash版本号
2. 对于比较文件比较小的图片,webpack可以将其自动转换了BASE64字符串进行存储,减少一次HTTP请求
接下来我们来做下演示:
我们在dialog组件目录下增加一张图片(图片大小15k左右),然后修改了dialog组件的模板,在其中引入了该张图片。这样每次我们点击按钮的时候,浏览器都会显示这张图。
另外我们在 global.css进行修改,为body添加一张背景图片,由于这个图很小(1KB),所以我们将背景图设置为重复。
1 body{
2 font: "微软雅黑";
3 background: url(../../img/mask.png) repeat scroll 0 0;
4 }
最后,我们在webpack配置文件中,为图片引入url-loader加载器,同时为其指定存放路径和文件名:
1 module: {
2 loaders: [ //加载器
3 {test: /\.css$/, loader:ExtractTextPlugin.extract("style", "css") },
4 {test: /\.html$/, loader: "html" },
5 {test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192&name=./img/[hash].[ext]'}
6 ]
7 }
生成的图片存放在dist/img下,然后为了混淆,我们将图片文件名设置为其hash值。
同时我们配置了limit参数,当图片大小小于这个值的时候,webpack都将会转换为base64字符串进行存储。
然后在项目根目录下运行 webpack 命令进行打包,然后运行生成index.html文件:
点击ADD按钮后浏览器才发起异步请求,加载了dialog组件以及我们所引入的图片资源,同时图片名称已经被设置为hash值。
再来看看样式中引入的图片:
可以看到这个背景图已经转换成BASE64字符串写入css文件中,所以这里就减少了一次图片请求。这是一种比较常用的优化页面性能的方式。
上面说到webpack的这种处理方式是一种比较优雅的处理方式,那又有哪些地方不够完善呢?
1. 上面写入模板中的图片webpack可以帮我们处理,但是src/view目录下的用于生成最终HTML的模板,webpack并不会对其中所引入的图片进行提取处理,导致图片路径不对。
2. 这里只是对图片进行了提取,其实并未对图片进行任何优化处理,比如合并小图标,限制图片质量避免图片过大等。
当然了,这些都是属于额外需求,已经有些超出了webpack所承载的功能范畴。实际项目中如果出现上述需求的话,个人建议是单独安装grunt,然后调用grunt插件来完成相关任务。
前两篇中我们使用webpack完成了静态资源(css/js/img)等自动写入HTML模板中,同时还可以为静态资源添加hash版本号,既满足了我们对于静态资源的打包要求,同时又无需开发人员介入打包过程,让我们的重点集中在业务逻辑的实现上来。但是每次修改完代码后如果手动执行 webpack 命令才能生效的话,那也够虐心的,好在的webpack为我们提供了一个强力的开发工具:webpack-dev-server!
一、代码压缩
在开始了解webpack-dev-server之前,我们还有一件准备工作需要做,那就是完成对代码的压缩混淆,不仅可以保证代码的安全性,还可以降低资源文件的大小,减少网络传输。
A. 压缩HTML
1 new HtmlWebpackPlugin({ //根据模板插入css/js等生成最终HTML
2 favicon:'./src/img/favicon.ico', //favicon路径
3 filename:'/view/index.html', //生成的html存放路径,相对于 path
4 template:'./src/view/index.html', //html模板路径
5 inject:true, //允许插件修改哪些内容,包括head与body
6 hash:true, //为静态资源生成hash值
7 minify:{ //压缩HTML文件
8 removeComments:true, //移除HTML中的注释
9 collapseWhitespace:true //删除空白符与换行符
10 }
11 })
HtmlWebpackPlugin插件在生成HTML调用了 html-minifier 插件来完成对HTML的压缩,这里我们使用两个配置完成来移除HTML中的注释以及空白符达到压缩的效果,其他的具体的配置参数大家请参考 html-minifier API
B. 压缩JS与CSS
1 new webpack.optimize.UglifyJsPlugin({ //压缩代码
2 compress: {
3 warnings: false
4 },
5 except: ['$super', '$', 'exports', 'require'] //排除关键字
6 }),
webpack已经内嵌了uglifyJS来完成对JS与CSS的压缩混淆,无需引用额外的插件。
这里需要注意的是压缩的时候需要排除一些关键字,不能混淆,比如$或者require,如果混淆的话就会影响到代码的正常运行。
修改完 webpack配置后,我们就可以运行webpack命令来重新打包了。
这次可以看到dist下输出的Js/css/html都是压缩过的。
参考代码:https://github.com/xiaoyunchen/webpack/blob/master/webpack.config.js
OK,到目前为止,我们的静态资源文件已经可以自动生成,压缩,合并(webpack主要打包功能)已经基本上配置完成。
二、webpack-dev-server
webpack开发服务器,是webpack官方提供的一个辅助开发工具,它可以自动监控项目下的文件,一旦有修改保存操作,开发服务器就会自动运行webpack 打包命令,帮我们自动将开发的代码重新打包。而且,如果需要的话,还能自动刷新浏览器,重新加载资源。
Cool!一般前端开发人员都会有两台显示器,一台用于编辑代码,一台用于打开浏览器查看效果。如果配置上webpack-dev-server的话,我们只需要在保存修改后的代码,什么也不用做,webpack就能自动帮我们打包代码,同时自动刷新浏览器,让我们立马看到修改后的效果。
同样的,我们首先通过npm安装webpack-dev-server:
npm install webpack-dev-server --save-dev
安装完成后我们就可以在项目根目录通过 webpack-dev-server 命令来启动开发服务器了,当然这其中也有很多配置,具体的API请参考:webpack-dev-server Options
在webpack配置文件中增加开发服务器的配置:
devServer:{
contentBase:'./dist/view'
}
这里主要是将其运行目录设置为打包后的view目录。
然后我们通过命令来启动开发服务器:
webpack-dev-server --inline
webpack-dev-server有两种启动模式:
iFrame:该模式下修改代码后会自动打包,但是浏览器不会自动刷新
inline:内联模式,该模式下修改代码后,webpack将自动打包并且刷新浏览器,让我们看到最终的修改效果
大家可以分别启动这两种模式来验证下不同的效果,一般我们都选择inline模式。
服务器启动成功以后,我们只需要在浏览器中访问 http://localhost:8080 即可访问。
很好!这样我们就很轻松的实现了一边修改代码一边查看效果的目的。
等等,还没完。webpack还为我们提供了一种更NX的技术,叫做HMR!
三、Hot Modules Replacemant
HMR,模块热部署。
也就是说我们的修改后的代码不仅可以自动打包,而且浏览器不用完全刷新,只需要异步刷新,加载修改后部分代码即可,加载完成效果会马上反应在页面效果上。
是的,很屌的样子。但是启动这个模式并不是难事,我们只需要在启动webpack-dev-server是添加--hot参数即可。
webpack-dev-server --inline --hot
我们启动HMR后,来看下具体的效果:
首先我们访问 http://localhost:8080
这是我们之前做的效果,点击按钮后,浏览器通过异步请求加载了chunk1这个模块JS以及我们所需要的图片资源。请注意这里的时间戳和边框颜色。
然后我们到代码CSS中去修改边框颜色,将其修改为蓝色:
这时,webpack检测到文件的修改保存操作,然后自动执行了打包操作。
我们到浏览器中去看下效果:
哇哦,边框颜色改变成了蓝色!
1. 时间戳没变,说明我们的浏览器并不是全部刷新。
2. 开发工具中的网络请求多了两个请求,一个是向服务器发起询问,判断是否有更新内容,二是向服务器获取更新内容的部分,注意不是全部代码,只是修改的片段。
这个部分强烈建议大家实际动手操作体验一下具体的效果。
模块热部署的确很好用,能让我们在即时不刷新浏览器的情况下也能看到修改后的页面效果。
但是这个技术目前只是在试验阶段,可能会存在一些BUG,所以不建议大家在生产环境中使用,在开发环境中体验一下到是没啥问题。
另外,值得一说的是,使用HMR以后,会一定程度的增加bundle的文件大小。
下图是未使用HMR模式时,bundle文件index.js为170kb
使用HMR模式以后,文件增大到177Kb,请注意我这里功能比较简单,而且对代码进行了压缩,所以可能看上去差别不是很大。
小结
通过配置webpack-dev-server,希望能将前端开发人员从这些重复性的打包构建的工作中解放出来,将时间与精力投放在实际的业务代码开发上去。