您的位置:新葡亰496net > 新葡亰官网 > 新葡亰496net:webpack使用明白,致我们自然组件化

新葡亰496net:webpack使用明白,致我们自然组件化

发布时间:2019-06-17 08:33编辑:新葡亰官网浏览(180)

    致我们终将组件化的Web

    2015/11/25 · HTML5 · 1 评论 · 组件化

    原文出处: AlloyTeam   

    这篇文章将从两年前的一次技术争论开始。争论的聚焦就是下图的两个目录分层结构。我说按模块划分好,他说你傻逼啊,当然是按资源划分。

    新葡亰496net 1 《=》新葡亰496net 2

    ”按模块划分“目录结构,把当前模块下的所有逻辑和资源都放一起了,这对于多人独自开发和维护个人模块不是很好吗?当然了,那争论的结果是我乖乖地改回主流的”按资源划分“的目录结构。因为,没有做到JS模块化和资源模块化,仅仅物理位置上的模块划分是没有意义的,只会增加构建的成本而已。

    虽然他说得好有道理我无言以对,但是我心不甘,等待他日前端组件化成熟了,再来一战!

    而今天就是我重申正义的日子!只是当年那个跟你撕逼的人不在。

    模块化的不足

    模块一般指能够独立拆分且通用的代码单元。由于JavaScript语言本身没有内置的模块机制(ES6有了!!),我们一般会使用CMD或ADM建立起模块机制。现在大部分稍微大型一点的项目,都会使用requirejs或者seajs来实现JS的模块化。多人分工合作开发,其各自定义依赖和暴露接口,维护功能模块间独立性,对于项目的开发效率和项目后期扩展和维护,都是是有很大的帮助作用。

    但,麻烦大家稍微略读一下下面的代码

    JavaScript

    require([ 'Tmpl!../tmpl/list.html','lib/qqapi','module/position','module/refresh','module/page','module/net' ], function(listTmpl, QQapi, Position, Refresh, Page, NET){ var foo = '', bar = []; QQapi.report(); Position.getLocaiton(function(data){ //... }); var init = function(){ bind(); NET.get('/cgi-bin/xxx/xxx',function(data){ renderA(data.banner); renderB(data.list); }); }; var processData = function(){ }; var bind = function(){ }; var renderA = function(){ }; var renderB = function(data){ listTmpl.render('#listContent',processData(data)); }; var refresh = function(){ Page.refresh(); }; // app start init(); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    require([
        'Tmpl!../tmpl/list.html','lib/qqapi','module/position','module/refresh','module/page','module/net'
    ], function(listTmpl, QQapi, Position, Refresh, Page, NET){
        var foo = '',
            bar = [];
        QQapi.report();
        Position.getLocaiton(function(data){
            //...
        });
        var init = function(){
            bind();
            NET.get('/cgi-bin/xxx/xxx',function(data){
                renderA(data.banner);
                renderB(data.list);
            });
        };
        var processData = function(){
        };
        var bind = function(){
        };
        var renderA = function(){
        };
        var renderB = function(data){
            listTmpl.render('#listContent',processData(data));
        };
        var refresh = function(){
            Page.refresh();
        };
        // app start
        init();
    });

    上面是具体某个页面的主js,已经封装了像Position,NET,Refresh等功能模块,但页面的主逻辑依旧是”面向过程“的代码结构。所谓面向过程,是指根据页面的渲染过程来编写代码结构。像:init -> getData -> processData -> bindevent -> report -> xxx 。 方法之间线性跳转,你大概也能感受这样代码弊端。随着页面逻辑越来越复杂,这条”过程线“也会越来越长,并且越来越绕。加之缺少规范约束,其他项目成员根据各自需要,在”过程线“加插各自逻辑,最终这个页面的逻辑变得难以维护。

    新葡亰496net 3

    开发需要小心翼翼,生怕影响“过程线”后面正常逻辑。并且每一次加插或修改都是bug泛滥,无不令产品相关人员个个提心吊胆。

     页面结构模块化

    基于上面的面向过程的问题,行业内也有不少解决方案,而我们团队也总结出一套成熟的解决方案:Abstractjs,页面结构模块化。我们可以把我们的页面想象为一个乐高机器人,需要不同零件组装,如下图,假设页面划分为tabContainer,listContainer和imgsContainer三个模块。最终把这些模块add到最终的pageModel里面,最终使用rock方法让页面启动起来。

    新葡亰496net 4
    (原过程线示例图)

    新葡亰496net 5
    (页面结构化示例图)

    下面是伪代码的实现

    JavaScript

    require([ 'Tmpl!../tmpl/list.html','Tmpl!../tmpl/imgs.html','lib/qqapi','module/refresh','module/page' ], function(listTmpl, imgsTmpl, QQapi, Refresh, Page ){ var tabContainer = new RenderModel({ renderContainer: '#tabWrap', data: {}, renderTmpl: "<li soda-repeat='item in data.tabs'>{{item}}</li>", event: function(){ // tab's event } }); var listContainer = new ScrollModel({ scrollEl: $.os.ios ? $('#Page') : window, renderContainer: '#listWrap', renderTmpl: listTmpl, cgiName: '/cgi-bin/index-list?num=1', processData: function(data) { //... }, event: function(){ // listElement's event }, error: function(data) { Page.show('数据返回异常[' data.retcode ']'); } }); var imgsContainer = new renderModel({ renderContainer: '#imgsWrap', renderTmpl: listTmpl, cgiName: '/cgi-bin/getPics', processData: function(data) { //... }, event: function(){ // imgsElement's event }, complete: function(data) { QQapi.report(); } }); var page = new PageModel(); page.add([tabContainer,listContainer,imgsContainer]); page.rock(); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    require([
        'Tmpl!../tmpl/list.html','Tmpl!../tmpl/imgs.html','lib/qqapi','module/refresh','module/page'
    ], function(listTmpl, imgsTmpl, QQapi, Refresh, Page ){
     
        var tabContainer = new RenderModel({
            renderContainer: '#tabWrap',
            data: {},
            renderTmpl: "<li soda-repeat='item in data.tabs'>{{item}}</li>",
            event: function(){
                // tab's event
            }
        });
     
        var listContainer = new ScrollModel({
            scrollEl: $.os.ios ? $('#Page') : window,
            renderContainer: '#listWrap',
            renderTmpl: listTmpl,
            cgiName: '/cgi-bin/index-list?num=1',
            processData: function(data) {
                //...
            },
            event: function(){
                // listElement's event
            },
            error: function(data) {
                Page.show('数据返回异常[' data.retcode ']');
            }
        });
     
        var imgsContainer = new renderModel({
            renderContainer: '#imgsWrap',
            renderTmpl: listTmpl,
            cgiName: '/cgi-bin/getPics',
            processData: function(data) {
                //...
            },
            event: function(){
                // imgsElement's event
            },
            complete: function(data) {
               QQapi.report();
            }
        });
     
        var page = new PageModel();
        page.add([tabContainer,listContainer,imgsContainer]);
        page.rock();
     
    });

    我们把这些常用的请求CGI,处理数据,事件绑定,上报,容错处理等一系列逻辑方法,以页面块为单位封装成一个Model模块。

    这样的一个抽象层Model,我们可以清晰地看到该页面块,请求的CGI是什么,绑定了什么事件,做了什么上报,出错怎么处理。新增的代码就应该放置在相应的模块上相应的状态方法(preload,process,event,complete…),杜绝了以往的无规则乱增代码的行文。并且,根据不同业务逻辑封装不同类型的Model,如列表滚动的ScrollModel,滑块功能的SliderModel等等,可以进行高度封装,集中优化。

    现在基于Model的页面结构开发,已经带有一点”组件化“的味道。每个Model都带有各自的数据,模板,逻辑。已经算是一个完整的功能单元。但距离真正的WebComponent还是有一段距离,至少满足不了我的”理想目录结构“。

     WebComponents 标准

    我们回顾一下使用一个datapicker的jquery的插件,所需要的步奏:

    1. 引入插件js

    2. 引入插件所需的css(如果有)

    3. copy 组件的所需的html片段

    4. 添加代码触发组件启动

    现阶段的“组件”基本上只能达到是某个功能单元上的集合。他的资源都是松散地分散在三种资源文件中,而且组件作用域暴露在全局作用域下,缺乏内聚性很容易就会跟其他组件产生冲突,如最简单的css命名冲突。对于这种“组件”,还不如上面的页面结构模块化。

    于是W3C按耐不住了,制定一个WebComponents标准,为组件化的未来指引了明路。

    下面以较为简洁的方式介绍这份标准,力求大家能够快速了解实现组件化的内容。(对这部分了解的同学,可以跳过这一小节)

    1. <template>模板能力

    模板这东西大家最熟悉不过了,前些年见的较多的模板性能大战artTemplate,juicer,tmpl,underscoretemplate等等。而现在又有mustachejs无逻辑模板引擎等新入选手。可是大家有没有想过,这么基础的能力,原生HTML5是不支持的(T_T)。

    而今天WebComponent将要提供原生的模板能力

    XHTML

    <template id="datapcikerTmpl"> <div>我是原生的模板</div> </template>

    1
    2
    3
    <template id="datapcikerTmpl">
    <div>我是原生的模板</div>
    </template>

    template标签内定义了myTmpl的模板,需要使用的时候就要innerHTML= document.querySelector('#myTmpl').content;可以看出这个原生的模板够原始,模板占位符等功能都没有,对于动态数据渲染模板能力只能自力更新。

    2. ShadowDom 封装组件独立的内部结构

    ShadowDom可以理解为一份有独立作用域的html片段。这些html片段的CSS环境和主文档隔离的,各自保持内部的独立性。也正是ShadowDom的独立特性,使得组件化成为了可能。

    JavaScript

    var wrap = document.querySelector('#wrap'); var shadow = wrap.createShadowRoot(); shadow.innerHTML = '<p>you can not see me </p>'

    1
    2
    3
    var wrap = document.querySelector('#wrap');
    var shadow = wrap.createShadowRoot();
    shadow.innerHTML = '<p>you can not see me </p>'

    在具体dom节点上使用createShadowRoot方法即可生成其ShadowDom。就像在整份Html的屋子里面,新建了一个shadow的房间。房间外的人都不知道房间内有什么,保持shadowDom的独立性。

    3. 自定义原生标签

    初次接触Angularjs的directive指令功能,设定好组件的逻辑后,一个<Datepicker />就能引入整个组件。如此狂炫酷炸碉堡天的功能,实在令人拍手称快,跃地三尺。

    JavaScript

    var tmpl = document.querySelector('#datapickerTmpl'); var datapickerProto = Object.create(HTMLElement.prototype); // 设置把我们模板内容我们的shadowDom datapickerProto.createdCallback = function() { var root = this.createShadowRoot(); root.appendChild(document.importNode(tmpl.content, true)); }; var datapicker = docuemnt.registerElement('datapicker',{ prototype: datapickerProto });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var tmpl = document.querySelector('#datapickerTmpl');
    var datapickerProto = Object.create(HTMLElement.prototype);
     
    // 设置把我们模板内容我们的shadowDom
    datapickerProto.createdCallback = function() {
        var root = this.createShadowRoot();
        root.appendChild(document.importNode(tmpl.content, true));
    };
     
    var datapicker = docuemnt.registerElement('datapicker',{
        prototype: datapickerProto
    });

    Object.create方式继承HTMLElement.prototype,得到一个新的prototype。当解析器发现我们在文档中标记它将检查是否一个名为createdCallback的方法。如果找到这个方法它将立即运行它,所以我们把克隆模板的内容来创建的ShadowDom。

    最后,registerElement的方法传递我们的prototype来注册自定义标签。

    上面的代码开始略显复杂了,把前面两个能力“模板”“shadowDom”结合,形成组件的内部逻辑。最后通过registerElement的方式注册组件。之后可以愉快地<datapicker></datapicker>的使用。

    4. imports解决组件间的依赖

    XHTML

    <link rel="import" href="datapciker.html">

    1
    <link rel="import" href="datapciker.html">

    这个类php最常用的html导入功能,HTML原生也能支持了。

    WebComponents标准内容大概到这里,是的,我这里没有什么Demo,也没有实践经验分享。由于webComponents新特性,基本上除了高版本的Chrome支持外,其他浏览器的支持度甚少。虽然有polymer帮忙推动webcompoents的库存在,但是polymer自身的要求版本也是非常高(IE10 )。所以今天的主角并不是他。

    我们简单来回顾一下WebCompoents的四部分功能:

    1 .<template>定义组件的HTML模板能力

    1. Shadow Dom封装组件的内部结构,并且保持其独立性

    2. Custom Element 对外提供组件的标签,实现自定义标签

    3. import解决组件结合和依赖加载

     组件化实践方案

    官方的标准看完了,我们思考一下。一份真正成熟可靠的组件化方案,需要具备的能力。

    “资源高内聚”—— 组件资源内部高内聚,组件资源由自身加载控制

    “作用域独立”—— 内部结构密封,不与全局或其他组件产生影响

    “自定义标签”—— 定义组件的使用方式

    “可相互组合”—— 组件正在强大的地方,组件间组装整合

    “接口规范化”—— 组件接口有统一规范,或者是生命周期的管理

    个人认为,模板能力是基础能力,跟是否组件化没有强联系,所以没有提出一个大点。

    新葡亰496net,既然是实践,现阶段WebComponent的支持度还不成熟,不能作为方案的手段。而另外一套以高性能虚拟Dom为切入点的组件框架React,在facebook的造势下,社区得到了大力发展。另外一名主角Webpack,负责解决组件资源内聚,同时跟React极度切合形成互补。

    所以【Webpack】 【React】将会是这套方案的核心技术。

    不知道你现在是“又是react webpack”感到失望新葡亰496net 6,还是“太好了是react webpack”不用再学一次新框架的高兴新葡亰496net 7。无论如何下面的内容不会让你失望的。

    一,组件生命周期

    新葡亰496net 8

    React天生就是强制性组件化的,所以可以从根本性上解决面向过程代码所带来的麻烦。React组件自身有生命周期方法,能够满足“接口规范化”能力点。并且跟“页面结构模块化”的所封装抽离的几个方法能一一对应。另外react的jsx自带模板功能,把html页面片直接写在render方法内,组件内聚性更加紧密。

    由于React编写的JSX是会先生成虚拟Dom的,需要时机才真正插入到Dom树。使用React必须要清楚组件的生命周期,其生命周期三个状态:

    Mount: 插入Dom

    Update: 更新Dom

    Unmount: 拔出Dom

    mount这单词翻译增加,嵌入等。我倒是建议“插入”更好理解。插入!拔出!插入!拔出!默念三次,懂了没?别少看黄段子的力量,

    新葡亰496net 9

    组件状态就是: 插入-> 更新 ->拔出。

    然后每个组件状态会有两种处理函数,一前一后,will函数和did函数。

    componentWillMount()  准备插入前

    componentDidlMount()  插入后

    componentWillUpdate() 准备更新前

    componentDidUpdate()  更新后

    componentWillUnmount() 准备拔出前

    因为拔出后基本都是贤者形态(我说的是组件),所以没有DidUnmount这个方法。

    另外React另外一个核心:数据模型props和state,对应着也有自个状态方法

    getInitialState()     获取初始化state。

    getDefaultProps() 获取默认props。对于那些没有父组件传递的props,通过该方法设置默认的props

    componentWillReceiveProps()  已插入的组件收到新的props时调用

    还有一个特殊状态的处理函数,用于优化处理

    shouldComponentUpdate():判断组件是否需要update调用

    加上最重要的render方法,React自身带的方法刚刚好10个。对于初学者来说是比较难以消化。但其实getInitialStatecomponentDidMountrender三个状态方法都能完成大部分组件,不必望而却步。

    回到组件化的主题。

    一个页面结构模块化的组件,能独立封装整个组件的过程线

    新葡亰496net 10

    我们换算成React生命周期方法:

    新葡亰496net 11

     

    组件的状态方法流中,有两点需要特殊说明:

    1,二次渲染:

    由于React的虚拟Dom特性,组件的render函数不需自己触发,根据props和state的改变自个通过差异算法,得出最优的渲染。

    请求CGI一般都是异步,所以必定带来二次渲染。只是空数据渲染的时候,有可能会被React优化掉。当数据回来,通过setState,触发二次render

     

    2,componentWiillMount与componentDidMount的差别

    和大多数React的教程文章不一样,ajax请求我建议在WillMount的方法内执行,而不是组件初始化成功之后的DidMount。这样能在“空数据渲染”阶段之前请求数据,尽早地减少二次渲染的时间。

    willMount只会执行一次,非常适合做init的事情。

    didMount也只会执行一次,并且这时候真实的Dom已经形成,非常适合事件绑定和complete类的逻辑。

     

     二,JSX很丑,但是组件内聚的关键!

    WebComponents的标准之一,需要模板能力。本是以为是我们熟悉的模板能力,但React中的JSX这样的怪胎还是令人议论纷纷。React还没有火起来的时候,大家就已经在微博上狠狠地吐槽了“JSX写的代码这TM的丑”。这其实只是Demo阶段JSX,等到实战的大型项目中的JSX,包含多状态多数据多事件的时候,你会发现………….JSX写的代码还是很丑。

    新葡亰496net 12
    (即使用sublime-babel等插件高亮,逻辑和渲染耦合一起,阅读性还是略差)

    为什么我们会觉得丑?因为我们早已经对“视图-样式-逻辑”分离的做法潜移默化。

    基于维护性和可读性,甚至性能,我们都不建议直接在Dom上面绑定事件或者直接写style属性。我们会在JS写事件代理,在CSS上写上classname,html上的就是清晰的Dom结构。我们很好地维护着MVC的设计模式,一切安好。直到JSX把他们都糅合在一起,所守护的技术栈受到侵略,难免有所抵制。

     

    但是从组件化的目的来看,这种高内聚的做法未尝不可。

    下面的代码,之前的“逻辑视图分离”模式,我们需要去找相应的js文件,相应的event函数体内,找到td-info的class所绑定的事件。

    对比起JSX的高度内聚,所有事件逻辑就是在本身jsx文件内,绑定的就是自身的showInfo方法。组件化的特性能立马体现出来。

    (注意:虽然写法上我们好像是HTML的内联事件处理器,但是在React底层并没有实际赋值类似onClick属性,内层还是使用类似事件代理的方式,高效地维护着事件处理器)

    再来看一段style的jsx。其实jsx没有对样式有硬性规定,我们完全可遵循之前的定义class的逻辑。任何一段样式都应该用class来定义。在jsx你也完全可以这样做。但是出于组件的独立性,我建议一些只有“一次性”的样式直接使用style赋值更好。减少冗余的class。

    XHTML

    <div className="list" style={{background: "#ddd"}}> {list_html} </div>

    1
    2
    3
    <div className="list" style={{background: "#ddd"}}>
       {list_html}
    </div>

    或许JSX内部有负责繁琐的逻辑样式,可JSX的自定义标签能力,组件的黑盒性立马能体验出来,是不是瞬间美好了很多。

    JavaScript

    render: function(){ return ( <div> <Menus bannerNums={this.state.list.length}></Menus> <TableList data={this.state.list}></TableList> </div> ); }

    1
    2
    3
    4
    5
    6
    7
    8
    render: function(){
        return (
          <div>
             <Menus bannerNums={this.state.list.length}></Menus>
             <TableList data={this.state.list}></TableList>
          </div>
       );
    }

    虽然JSX本质上是为了虚拟Dom而准备的,但这种逻辑和视图高度合一对于组件化未尝不是一件好事。

     

    学习完React这个组件化框架后,看看组件化能力点的完成情况

    “资源高内聚”—— (33%)  html与js内聚

    “作用域独立”—— (50%)  js的作用域独立

    “自定义标签”—— (100%)jsx

    “可相互组合”—— (50%)  可组合,但缺乏有效的加载方式

    “接口规范化”—— (100%)组件生命周期方法

     

    Webpack 资源组件化

    对于组件化的资源独立性,一般的模块加载工具和构建流程视乎变得吃力。组件化的构建工程化,不再是之前我们常见的,css合二,js合三,而是体验在组件间的依赖于加载关系。webpack正好符合需求点,一方面填补组件化能力点,另一方帮助我们完善组件化的整体构建环境。

    首先要申明一点是,webpack是一个模块加载打包工具,用于管理你的模块资源依赖打包问题。这跟我们熟悉的requirejs模块加载工具,和grunt/gulp构建工具的概念,多多少少有些出入又有些雷同。

    新葡亰496net 13

    首先webpak对于CommonJS与AMD同时支持,满足我们模块/组件的加载方式。

    JavaScript

    require("module"); require("../file.js"); exports.doStuff = function() {}; module.exports = someValue;

    1
    2
    3
    4
    require("module");
    require("../file.js");
    exports.doStuff = function() {};
    module.exports = someValue;

    JavaScript

    define("mymodule", ["dep1", "dep2"], function(d1, d2) { return someExportedValue; });

    1
    2
    3
    define("mymodule", ["dep1", "dep2"], function(d1, d2) {
        return someExportedValue;
    });

    当然最强大的,最突出的,当然是模块打包功能。这正是这一功能,补充了组件化资源依赖,以及整体工程化的能力

    根据webpack的设计理念,所有资源都是“模块”,webpack内部实现了一套资源加载机制,可以把想css,图片等资源等有依赖关系的“模块”加载。这跟我们使用requirejs这种仅仅处理js大大不同。而这套加载机制,通过一个个loader来实现。

     

    JavaScript

    // webpack.config.js module.exports = { entry: { entry: './index.jsx', }, output: { path: __dirname, filename: '[name].min.js' }, module: { loaders: [ {test: /.css$/, loader: 'style!css' }, {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/}, {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'} ] } };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // webpack.config.js
    module.exports = {
        entry: {
         entry: './index.jsx',
        },
        output: {
            path: __dirname,
            filename: '[name].min.js'
        },
        module: {
            loaders: [
                {test: /.css$/, loader: 'style!css' },
                {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/},
                {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'}
            ]
        }
    };

    上面一份简单的webpack配置文件,留意loaders的配置,数组内一个object配置为一种模块资源的加载机制。test的正则为匹配文件规则,loader的为匹配到文件将由什么加载器处理,多个处理器之间用分隔,处理顺序从右到左。

     

    style!css,css文件通过css-loader(处理css),再到style-loader(inline到html)的加工处理流。

    jsx文件通过jsx-loader编译,‘?’开启加载参数,harmony支持ES6的语法。

    图片资源通过url-loader加载器,配置参数limit,控制少于10KB的图片将会base64化。

     资源文件如何被require?

    JavaScript

    // 加载组件自身css require('./slider.css'); // 加载组件依赖的模块 var Clip = require('./clipitem.js'); // 加载图片资源 var spinnerImg = require('./loading.png');

    1
    2
    3
    4
    5
    6
    // 加载组件自身css
    require('./slider.css');
    // 加载组件依赖的模块
    var Clip = require('./clipitem.js');
    // 加载图片资源
    var spinnerImg = require('./loading.png');

    在webpack的js文件中我们除了require我们正常的js文件,css和png等静态文件也可以被require进来。我们通过webpack命令,编译之后,看看输出结果如何:

    JavaScript

    webpackJsonp([0], { /* 0 */ /***/ function(module, exports, __webpack_require__) { // 加载组件自身css __webpack_require__(1); // 加载组件依赖的模块 var Clip = __webpack_require__(5); // 加载图片资源 var spinnerImg = __webpack_require__(6); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(3)(); exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]); /***/ }, /* 3 */ /***/ function(module, exports) { /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 5 */ /***/ function(module, exports) { console.log('hello, here is clipitem.js') ; /***/ }, /* 6 */ /***/ function(module, exports) { module.exports = "data:image/png;base64,iVBORw0KGg......" /***/ } ]);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    webpackJsonp([0], {
    /* 0 */
    /***/ function(module, exports, __webpack_require__) {
              // 加载组件自身css
              __webpack_require__(1);
              // 加载组件依赖的模块
              var Clip = __webpack_require__(5);
              // 加载图片资源
              var spinnerImg = __webpack_require__(6);
    /***/ },
    /* 1 */
    /***/ function(module, exports, __webpack_require__) {
     
    /***/ },
    /* 2 */
    /***/ function(module, exports, __webpack_require__) {
              exports = module.exports = __webpack_require__(3)();
              exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]);
     
    /***/ },
    /* 3 */
    /***/ function(module, exports) {
     
    /***/ },
     
    /* 4 */
    /***/ function(module, exports, __webpack_require__) {
    /***/ },
     
    /* 5 */
    /***/ function(module, exports) {
              console.log('hello, here is clipitem.js') ;
    /***/ },
    /* 6 */
    /***/ function(module, exports) {
              module.exports = "data:image/png;base64,iVBORw0KGg......"
    /***/ }
    ]);

    webpack编译之后,输出文件视乎乱糟糟的,但其实每一个资源都被封装在一个函数体内,并且以编号的形式标记(注释)。这些模块,由webpack的__webpack_require__内部方法加载。入口文件为编号0的函数index.js,可以看到__webpack_require__加载其他编号的模块。

    css文件在编号1,由于使用css-loader和style-loader,编号1-4都是处理css。其中编号2我们可以看我们的css的string体。最终会以内联的方式插入到html中。

    图片文件在编号6,可以看出exports出base64化的图片。

     组件一体输出

    JavaScript

    // 加载组件自身css require('./slider.css'); // 加载组件依赖的模块 var React = require('react'); var Clip = require('../ui/clipitem.jsx'); // 加载图片资源 var spinnerImg = require('./loading.png'); var Slider = React.createClass({ getInitialState: function() { // ... }, componentDidMount: function(){ // ... }, render: function() { return ( <div> <Clip data={this.props.imgs} /> <img className="loading" src={spinnerImg} /> </div> ); } }); module.exports = Slider;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 加载组件自身css
    require('./slider.css');
    // 加载组件依赖的模块
    var React = require('react');
    var Clip = require('../ui/clipitem.jsx');
    // 加载图片资源
    var spinnerImg = require('./loading.png');
    var Slider = React.createClass({
        getInitialState: function() {
            // ...
        },
        componentDidMount: function(){
            // ...
        },
        render: function() {
            return (
                <div>
                   <Clip data={this.props.imgs} />
                   <img className="loading" src={spinnerImg} />
                </div>
            );
        }
    });
    module.exports = Slider;

    如果说,react使到html和js合为一体。

    那么加上webpack,两者结合一起的话。js,css,png(base64),html 所有web资源都能合成一个JS文件。这正是这套方案的核心所在:组件独立一体化。如果要引用一个组件,仅仅require('./slider.js') 即可完成。

     

    加入webpack的模块加载器之后,我们组件的加载问题,内聚问题也都成功地解决掉

    “资源高内聚”—— (100%) 所有资源可以一js输出

    “可相互组合”—— (100%)  可组合可依赖加载

     

     CSS模块化实践

    很高兴,你能阅读到这里。目前我们的组件完成度非常的高,资源内聚,易于组合,作用域独立互不污染。。。。等等新葡亰496net 14,视乎CSS模块的完成度有欠缺。

    那么目前组件完成度来看,CSS作用域其实是全局性的,并非组件内部独立。下一步,我们要做得就是如何让我们组件内部的CSS作用域独立。

    这时可能有人立马跳出,大喊一句“德玛西亚!”,哦不,应该是“用sass啊傻逼!”。可是项目组件化之后,组件的内部封装已经很好了,其内部dom结构和css趋向简单,独立,甚至是破碎的。LESS和SASS的一体式样式框架的设计,他的嵌套,变量,include,函数等丰富的功能对于整体大型项目的样式管理非常有效。但对于一个功能单一组件内部样式,视乎就变的有点格格不入。“不能为了框架而框架,合适才是最好的”。视乎原生的css能力已经满足组件的样式需求,唯独就是上面的css作用域问题。

     

    这里我给出思考的方案: classname随便写,保持原生的方式。编译阶段,根据组件在项目路径的唯一性,由【组件classname 组件唯一路径】打成md5,生成全局唯一性classname。正当我要写一个loader实现我的想法的时候,发现歪果仁已经早在先走一步了。。。。

    这里具体方案参考我之前博客的译文:

    之前我们讨论过JS的模块。现在通过Webpack被加载的CSS资源叫做“CSS模块”?我觉得还是有问题的。现在style-loader插件的实现本质上只是创建link[rel=stylesheet]元素插入到document中。这种行为和通常引入JS模块非常不同。引入另一个JS模块是调用它所提供的接口,但引入一个CSS却并不“调用”CSS。所以引入CSS本身对于JS程序来说并不存在“模块化”意义,纯粹只是表达了一种资源依赖——即该组件所要完成的功能还需要某些asset。

    因此,那位歪果仁还扩展了“CSS模块化”的概念,除了上面的我们需要局部作用域外,还有很多功能,这里不详述。具体参考原文 

    非常赞的一点,就是cssmodules已经被css-loader收纳。所以我们不需要依赖额外的loader,基本的css-loader开启参数modules即可

    JavaScript

    //webpack.config.js ... module: { loaders: [ {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' }, ] } ....

    1
    2
    3
    4
    5
    6
    7
    8
    //webpack.config.js
    ...  
        module: {
            loaders: [
                {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' },
            ]  
        }
    ....

    modules参数代表开启css-modules功能,loaclIdentName为设置我们编译后的css名字,为了方便debug,我们把classname(local)和组件名字(name)输出。当然可以在最后输出的版本为了节省提交,仅仅使用hash值即可。另外在react中的用法大概如下。

    JavaScript

    var styles = require('./banner.css'); var Banner = new React.createClass({ ... render: function(){ return ( <div> <div className={styles.classA}></div> </div> ) } });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var styles = require('./banner.css');
    var Banner = new React.createClass({
        ...
        render: function(){
            return (
                <div>
                    <div className={styles.classA}></div>
                </div>
            )
        }
    });

    最后这里关于出于对CSS一些思考,

    关于css-modules的其它功能,我并不打算使用。在内部分享【我们竭尽所能地让CSS变得复杂】中提及:

    我们项目中大部分的CSS都不会像boostrap那样需要变量来设置,身为一线开发者的我们大概能够感受到:设计师们改版UI,绝对不是简单的换个色或改个间距,而是面目全非的全新UI,这绝对不是一个变量所能解决的”维护性“。

    反而项目实战过程中,真正要解决的是:在版本迭代过程中那些淘汰掉的过期CSS,大量地堆积在项目当中。我们像极了家中的欧巴酱不舍得丢掉没用的东西,因为这可是我们使用sass或less编写出具有高度的可维护性的,肯定有复用的一天。

    这些堆积的过期CSS(or sass)之间又有部分依赖,一部分过期没用了,一部分又被新的样式复用了,导致没人敢动那些历史样式。结果现网项目迭代还带着大量两年前没用的样式文件。

    组件化之后,css的格局同样被革新了。可能postcss才是你现在手上最适合的工具,而不在是sass。

     

    到这里,我们终于把组件化最后一个问题也解决了。

    “作用域独立”—— (100%) 如同shadowDom作用域独立

     

    到这里,我们可以开一瓶82年的雪碧,好好庆祝一下。不是吗?

    新葡亰496net 15

     

     组件化之路还在继续

    webpack和react还有很多新非常重要的特性和功能,介于本文仅仅围绕着组件化的为核心,没有一一阐述。另外,配搭gulp/grunt补充webpack构建能力,webpack的codeSplitting,react的组件通信问题,开发与生产环境配置等等,都是整套大型项目方案的所必须的,限于篇幅问题。可以等等我更新下篇,或大家可以自行查阅。

    但是,不得不再安利一下react-hotloader神器。热加载的开发模式绝对是下一代前端开发必备。严格说,如果没有了热加载,我会很果断地放弃这套方案,即使这套方案再怎么优秀,我都讨厌react需要5~6s的编译时间。但是hotloader可以在我不刷新页面的情况下,动态修改代码,而且不单单是样式,连逻辑也是即时生效。

    新葡亰496net 16

    如上在form表单内。使用热加载,表单不需要重新填写,修改submit的逻辑立刻生效。这样的开发效率真不是提高仅仅一个档次。必须安利一下。

     

    或许你发现,使用组件化方案之后,整个技术栈都被更新了一番。学习成本也不少,并且可以预知到,基于组件化的前端还会很多不足的问题,例如性能优化方案需要重新思考,甚至最基本的组件可复用性不一定高。后面很长一段时间,需要我们不断磨练与优化,探求最优的前端组件化之道。

    至少我们可以想象,不再担心自己写的代码跟某个谁谁冲突,不再为找某段逻辑在多个文件和方法间穿梭,不再copy一片片逻辑然后改改。我们每次编写都是可重用,可组合,独立且内聚的组件。而每个页面将会由一个个嵌套组合的组件,相互独立却相互作用。

     

    对于这样的前端未来,有所期待,不是很好吗

    至此,感谢你的阅读。

    1 赞 6 收藏 1 评论

    新葡亰496net 17

    一、什么是webpack:webpack是一款模块加载兼打包工具,它可以将js、jsx、coffee、样式sass、less,图片等作为模块来使用和处理。
    二、优势:1、以commonJS的形式来书写脚本,对AMD、CMD的支持也很全面,方便旧项目的迁移。2、能被模块化的不止是JS了。3、能替代部分grunt/gulp的工作,例如打包,压缩混淆,图片转base64等。3、扩展性强,插件机制完善,支持React热拔插(react-hot-loader)
    三、安装和配置:
    1、安装:直接使用npm来进行安装
    $ npm install webpack -g
    将依赖写入package.json包
    $ npm init
    $ npm install webpack --save-dev
    2、配置:
    每个项目必须配置一个webpack.config.js,作用如同gulpfile.js/Gruntfile.js,一个配置项,告诉webpack要做什么。
    示例:
    var webpack = require('webpack');
    var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
    module.exports = {
    //插件项
    plugins: [commonsPlugin],
    //页面入口文件配置
    entry: {
    index : './src/js/page/index.js'
    },
    //入口文件输出配置
    output: {
    path: 'dist/js/page',
    filename: '[name].js'
    },
    module: {
    //加载器配置
    loaders: [
    { test: /.css$/, loader: 'style-loader!css-loader' },
    { test: /.js$/, loader: 'jsx-loader?harmony' },
    { test: /.scss$/, loader: 'style!css!sass?sourceMap'},
    { test: /.(png|jpg)$/, loader: 'url-loader?limit=8192'}
    ]
    },
    //其它解决方案配置
    resolve: {
    root: 'E:/github/flux-example/src', //绝对路径
    extensions: ['', '.js', '.json', '.scss'],
    alias: {
    AppStore : 'js/stores/AppStores.js',
    ActionType : 'js/actions/ActionType.js',
    AppAction : 'js/actions/AppAction.js'
    }
    }
    };
    (1)plugins是插件项,这里使用了一个CommonsChunkPlugin的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个common.js来方便多页面之间的复用。
    (2)entry是页面的入口文件配置,output是对应的输出项配置
    {
    entry: {
    page1: "./page1",
    //支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出
    page2: ["./entry1", "./entry2"]
    },
    output: {
    path: "dist/js/page",
    filename: "[name].bundle.js"
    }
    }
    该代码会生成一个page1.bundle.js和page2.bundle.js,并存放在./dist/js/page文件夹下。
    (3)module.loaders,告知webpack每一种文件都需要什么加载器来处理
    module: {
    //加载器配置
    loaders: [
    //.css 文件使用 style-loader 和 css-loader 来处理
    { test: /.css$/, loader: 'style-loader!css-loader' },
    //.js 文件使用 jsx-loader 来编译处理
    { test: /.js$/, loader: 'jsx-loader?harmony' },
    //.scss 文件使用 style-loader、css-loader 和 sass-loader 来编译处理
    { test: /.scss$/, loader: 'style!css!sass?sourceMap'},
    //图片文件使用 url-loader 来处理,小于8kb的直接转为base64
    { test: /.(png|jpg)$/, loader: 'url-loader?limit=8192'}
    ]
    }
    -loader可以不写,多个loader之间用“!”连接起来。所有的加载器都需要通过npm来加载。
    例如最后一个url-loader,它会将样式中引用到的图片转为模块来处理。使用前进行安装:
    $ npm install url-loader -save-dev
    配置信息的参数:“?limit=8192”表示将所有小于8kb的图片都转为base64形式(超过8kb的才使用url-loader来映射到文件,否则转为data url形式)
    (4)resolve配置,
    resolve: {
    //查找module的话从这里开始查找
    root: 'E:/github/flux-example/src', //绝对路径
    //自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
    extensions: ['', '.js', '.json', '.scss'],
    //模块别名定义,方便后续直接引用别名,无须多写长长的地址
    alias: {
    AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
    ActionType : 'js/actions/ActionType.js',
    AppAction : 'js/actions/AppAction.js'
    }
    }
    四、运行webpack,直接执行:
    $ webpack --display-error-details
    后面的参数 “-display-error-details”推荐加上,方便出错时能了解到更详尽的信息。其他主要参数:
    $ webpack --config XXX.js //使用另一份配置文件(比如webpack.config2.js)来打包
    $ webpack --watch //监听变动并自动打包
    $ webpack -p //压缩混淆脚本,这个非常非常重要!
    $ webpack -d //生成map映射文件,告知哪些模块被最终打包到哪里了
    -p是很重要的参数,曾经一个未压缩的 700kb 的文件,压缩后直接降到 180kb(主要是样式这块一句就独占一行脚本,导致未压缩脚本变得很大)。
    五、模块引入:
    1、在HTML页面引入:引入webpack最终生成的脚本即可:
    <!DOCTYPE html>
    <html>
    <head lang="en">
    <meta charset="UTF-8">
    <title>demo</title>
    </head>
    <body>
    <script src="dist/js/page/common.js"></script>
    <script src="dist/js/page/index.js"></script>
    </body>
    </html>
    可以看到我们连样式都不用引入,毕竟脚本执行时会动态生成style并标签打到head里。
    2、JS引入:各脚本模块可以使用common.js来书写,并可以直接引入未经编译的模块,比如:jsx,coffee,sass,只要在webpack.config.js中配置好了对应的加载器就行。
    编译页面的入口文件:
    require('../../css/reset.scss'); //加载初始化样式
    require('../../css/allComponent.scss'); //加载组件样式
    var React = require('react');
    var AppWrap = require('../component/AppWrap'); //加载组件
    var createRedux = require('redux').createRedux;
    var Provider = require('redux/react').Provider;
    var stores = require('AppStore');
    var redux = createRedux(stores);
    var App = React.createClass({
    render: function() {
    return (
    <Provider redux={redux}>
    {function() { return <AppWrap />; }}
    </Provider>
    );
    }
    });
    React.render(
    <App />, document.body
    );

    *安装

    webpack 介绍

    Webpack 中文指南

    Webpack是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过loader的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。

    其他:
    1、shimming :
    在 AMD/CMD 中,我们需要对不符合规范的模块(比如一些直接返回全局变量的插件)进行 shim 处理,这时候我们需要使用 exports-loader 来帮忙:
    { test: require.resolve(“./src/js/tool/swipe.js”), loader: “exports?swipe”}
    之后在脚本中需要引用该模块的时候,这么简单地来使用就可以了:
    require(‘./tool/swipe.js’);
    swipe();
    2、自定义公共模块提取:
    在文章开始我们使用了 CommonsChunkPlugin 插件来提取多个页面之间的公共模块,并将该模块打包为 common.js 。
    但有时候我们希望能更加个性化一些,我们可以这样配置:
    var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
    module.exports = {
    entry: {
    p1: "./page1",
    p2: "./page2",
    p3: "./page3",
    ap1: "./admin/page1",
    ap2: "./admin/page2"
    },
    output: {
    filename: "[name].js"
    },
    plugins: [
    new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
    new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
    ]
    };
    // <script>s required:
    // page1.html: commons.js, p1.js
    // page2.html: commons.js, p2.js
    // page3.html: p3.js
    // admin-page1.html: commons.js, admin-commons.js, ap1.js
    // admin-page2.html: commons.js, admin-commons.js, ap2.js
    3、独立打包样式:
    有时候可能希望项目的样式能不要被打包到脚本中,而是独立出来作为.css,然后在页面中以标签引入。这时候我们需要 extract-text-webpack-plugin 来帮忙:
    var webpack = require('webpack');
    var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
    var ExtractTextPlugin = require("extract-text-webpack-plugin");
    module.exports = {
    plugins: [commonsPlugin, new ExtractTextPlugin("[name].css")],
    entry: {
    //...省略其它配置
    最终 webpack 执行后会乖乖地把样式文件提取出来:
    4、使用CDN远程文件:
    有时候我们希望某些模块走CDN并以<script>的形式挂载到页面上来加载,但又希望能在 webpack 的模块中使用上。
    这时候我们可以在配置文件里使用 externals 属性来帮忙:
    {
    externals: {
    // require("jquery") 是引用自外部模块的
    // 对应全局变量 jQuery
    "jquery": "jQuery"
    }
    }
    需要留意的是,得确保 CDN 文件必须在 webpack 打包文件引入之前先引入。
    我们倒也可以使用 script.js 在脚本中来加载我们的模块:
    var $script = require("scriptjs");
    $script("//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js", function() {
    $('body').html('It works!')
    });
    5、与grunt/gulp相结合:
    gulp.task("webpack", function(callback) {
    // run webpack
    webpack({
    // configuration
    }, function(err, stats) {
    if(err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString({
    // output options
    }));
    callback();
    });
    });
    当然我们只需要把配置写到 webpack({ … }) 中去即可,无须再写 webpack.config.js 了。

    进入到你的项目 将webpack安装到项目的依赖中,这样就可以使用项目本地版本的webpack
    npm install webpack@1.12.x--save-dev(这种格式是安装指定版本)

    webpack 是什么

    现状

    文件只能按照的书写顺序进行加载

    开发人员必须主观解决模块和代码库的依赖关系

    在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

    CommonJS

    服务器端的 Node.js 遵循CommonJS规范,该规范的核心思想是允许模块通过require方法来同步加载所要依赖的其他模块,然后通过exports或module.exports来导出需要暴露的接口。

    require("module");require("../file.js");exports.doStuff =function(){};module.exports = someValue;

    优点:

    服务器端模块便于重用

    NPM中已经有将近20万个可以使用模块包

    简单并容易使用

    缺点:

    同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的

    不能非阻塞的并行加载多个模块

    npm install webpack --save-dev

    为什么引入新的打包工具

    AMD

    Asynchronous Module Definition规范其实只有一个主要接口define(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行,依赖前置。

    define("module", ["dep1","dep2"],function(d1, d2){returnsomeExportedValue;});require(["module","../file"],function(module, file){/* ... */});

    优点:

    适合在浏览器环境中异步加载模块

    可以并行加载多个模块

    缺点:

    提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅

    不符合通用的模块化思维方式,是一种妥协的实现

    npm i webpack-dev-server --save

    webpack 核心思想

    CMD

    Common Module Definition规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。

    define(function(require, exports, module){var$ =require('jquery');varSpinning =require('./spinning');  exports.doSomething = ...module.exports = ...})

    优点:

    依赖就近,延迟执行

    可以很容易在 Node.js 中运行

    缺点:

    依赖 SPM 打包,模块的加载逻辑偏重

    npm install react --save

    webpack 安装

    UMD

    Universal Module Definition规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案。

    npm install babel-loader babel-core babel-preset-es2015 babel-preset-react --save-dev

    webpack 使用

    ES6 模块

    EcmaScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

    import"jquery";exportfunctiondoStuff(){}module"localModule"{}

    优点:

    容易进行静态分析

    面向未来的 EcmaScript 标准

    缺点:

    原生浏览器端还没有实现该标准

    全新的命令字,新版的 Node.js才支持

    npm i react-dom --save

    命令行调用

    期望的模块系统

    可以兼容多种模块风格,尽量可以利用已有的代码,不仅仅只是 JavaScript 模块化,还有 CSS、图片、字体等资源也需要模块化。

    前端模块加载

    前端模块要在客户端中执行,所以他们需要增量加载到浏览器中。

    模块的加载和传输,我们首先能想到两种极端的方式,一种是每个模块文件都单独请求,另一种是把所有模块打包成一个文件然后只请求一次。显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。

    分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。

    要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。

    所有资源都是模块

    在上面的分析过程中,我们提到的模块仅仅是指JavaScript模块文件。然而,在前端开发过程中还涉及到样式、图片、字体、HTML 模板等等众多的资源。这些资源还会以各种方言的形式存在,比如 coffeescript、 less、 sass、众多的模板库、多语言系统(i18n)等等。

    如果他们都可以视作模块,并且都可以通过require的方式来加载,将带来优雅的开发体验,比如:

    require("./style.css");require("./style.less");require("./template.jade");require("./image.png");

    那么如何做到让require能加载各种资源呢?

    静态分析

    在编译的时候,要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给适配的加载器来处理。比如一个用 LESS 写的样式模块,可以先用 LESS 加载器将它转成一个CSS 模块,在通过 CSS 模块把他插入到页面的标签中执行。Webpack 就是在这样的需求中应运而生。

    同时,为了能利用已经存在的各种框架、库和已经写好的文件,我们还需要一个模块加载的兼容策略,来避免重写所有的模块。

    *目录

    配置文件

    什么是 Webpack

    Webpack 是一个模块打包器。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

    新葡亰496net 18

    index.html
    js / 你的js文件
    dist / 你打包的文件(也就是你JS目录下的文件打包后的文件)
    手动打包方法

    webpack 配置参数

    为什么重复造轮子

    市面上已经存在的模块管理和打包工具并不适合大型的项目,尤其单页面 Web 应用程序。最紧迫的原因是如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源。

    手动打包: webpack 源文件路径 打包路径(webpack ./entry.js ./bundle.js)//这里是没有配置webpack.config.js
    $ webpack --watch //监听变动并自动打包 监视webpack.config.js 的改动$ webpack -p //压缩混淆脚本,这个非常非常重要!

    entry 和 output

    这些已有的模块化工具并不能很好的完成如下的目标:

    将依赖树拆分成按需加载的块

    初始化加载的耗时尽量少

    各种静态资源都可以视作模块

    将第三方库整合成模块的能力

    可以自定义打包逻辑的能力

    适合大项目,无论是单页还是多页的 Web 应用

    *注意事项

    单一入口

    Webpack 的特点

    Webpack 和其他模块化工具有什么区别呢?

    代码拆分

    Webpack 有两种组织模块依赖的方式,同步和异步。异步依赖作为分割点,形成一个新的块。在优化了依赖树后,每一个异步区块都作为一个文件被打包。

    Loader

    Webpack 本身只能处理原生的 JavaScript 模块,但是 loader 转换器可以将各种类型的资源转换成 JavaScript 模块。这样,任何资源都可以成为 Webpack 可以处理的模块。

    智能解析

    Webpack 有一个智能解析器,几乎可以处理任何第三方库,无论它们的模块形式是 CommonJS、 AMD 还是普通的 JS 文件。甚至在加载依赖的时候,允许使用动态表达式require("./templates/" name ".jade")。

    插件系统

    Webpack 还有一个功能丰富的插件系统。大多数内容功能都是基于这个插件系统运行的,还可以开发和使用开源的 Webpack 插件,来满足各式各样的需求。

    快速运行

    Webpack 使用异步 I/O 和多级缓存提高运行效率,这使得 Webpack 能够以令人难以置信的速度快速增量编译。

    页面要引入打包后的路径的JS文件

    多个入口

    安装

    首先要安装Node.js, Node.js 自带了软件包管理器 npm,Webpack 需要 Node.js v0.6 以上支持,建议使用最新版 Node.js。

    用 npm 安装 Webpack:

    $ npm install webpack -g

    此时 Webpack 已经安装到了全局环境下,可以通过命令行webpack -h试试。

    通常我们会将 Webpack 安装到项目的依赖中,这样就可以使用项目本地版本的 Webpack。

    # 进入项目目录# 确定已经有 package.json,没有就通过 npm init 创建# 安装 webpack 依赖

    $ npm install webpack --save-dev

    Webpack 目前有两个主版本,一个是在 master 主干的稳定版,一个是在 webpack-2 分支的测试版,测试版拥有一些实验性功能并且和稳定版不兼容,在正式项目中应该使用稳定版。

    # 查看 webpack 版本信息

    $ npm info webpack

    # 安装指定版本的 webpack

    $ npm install webpack@1.12.x --save-dev

    如果需要使用 Webpack 开发工具,要单独安装:

    $ npm install webpack-dev-server --save-dev

    *loader理解

    多个打包目标

    使用

    首先创建一个静态页面 index.html 和一个 JS 入口文件 entry.js:

    // entry.js

    document.write('It works.')

    然后编译 entry.js 并打包到 bundle.js:

    $ webpack entry.js bundle.js

    打包过程会显示日志:

    Hash: e964f90ec65eb2c29bb9

    Version: webpack 1.12.2

    Time: 54ms

    Asset    Size  Chunks            Chunk Names

    bundle.js  1.42 kB      0  [emitted]  main

    [0] ./entry.js 27 bytes {0} [built]

    用浏览器打开index.html将会看到It works.。

    接下来添加一个模块module.js并修改入口entry.js:

    // module.js

    module.exports ='It works from module.js.'

    // entry.js

    document.write('It works.')

    document.write(require('./module.js'))// 添加模块

    重新打包webpack entry.js bundle.js后刷新页面看到变化It works.It works from module.js.

    Hash: 279c7601d5d08396e751

    Version: webpack 1.12.2

    Time: 63ms

    Asset    Size  Chunks            Chunk Names

    bundle.js  1.57 kB      0  [emitted]  main

    [0] ./entry.js 66 bytes {0} [built]

    [1] ./module.js 43 bytes {0} [built]

    Webpack 会分析入口文件,解析包含依赖关系的各个文件。这些文件(模块)都打包到 bundle.js 。Webpack 会给每个模块分配一个唯一的 id 并通过这个 id 索引和访问模块。在页面启动时,会先执行 entry.js 中的代码,其它模块会在运行require的时候再执行。

    是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。这样,我们就可以通过 require 来加载任何类型的模块或文件,比如CoffeeScriptJSXLESS图片

    webpack 支持 Jsx 和 Es6

    Loader

    Webpack 本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用 loader 进行转换。

    Loader 可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。这样,我们就可以通过require来加载任何类型的模块或文件,比如 CoffeeScript、 JSX、 LESS 或图片。

    先来看看 loader 有哪些特性?

    Loader 可以通过管道方式链式调用,每个 loader 可以把资源转换成任意格式并传递给下一个 loader ,但是最后一个 loader 必须返回 JavaScript。

    Loader 可以同步或异步执行。

    Loader 运行在 node.js 环境中,所以可以做任何可能的事情。

    Loader 可以接受参数,以此来传递配置项给 loader。

    Loader 可以通过文件扩展名(或正则表达式)绑定给不同类型的文件。

    Loader 可以通过npm发布和安装。

    除了通过package.json的main指定,通常的模块也可以导出一个 loader 来使用。

    Loader 可以访问配置。

    插件可以让 loader 拥有更多特性。

    Loader 可以分发出附加的任意文件。

    Loader 本身也是运行在 node.js 环境中的 JavaScript 模块,它通常会返回一个函数。大多数情况下,我们通过 npm 来管理 loader,但是你也可以在项目中自己写 loader 模块。

    按照惯例,而非必须,loader 一般以xxx-loader的方式命名,xxx代表了这个 loader 要做的转换功能,比如json-loader。

    在引用 loader 的时候可以使用全名json-loader,或者使用短名json。这个命名规则和搜索优先级顺序在 webpack 的resolveLoader.moduleTemplatesapi 中定义。

    Default: ["*-webpack-loader", "*-web-loader", "*-loader", "*"]

    Loader 可以在require()引用模块的时候添加,也可以在 webpack 全局配置中进行绑定,还可以通过命令行的方式使用。

    接上一节的例子,我们要在页面中引入一个 CSS 文件 style.css,首页将 style.css 也看成是一个模块,然后用css-loader来读取它,再用style-loader把它插入到页面中。

    /* style.css */

    body{background:yellow; }

    修改 entry.js:

    require("!style-loader!css-loader!./style.css")// 载入 style.css

    document.write('It works.')

    document.write(require('./module.js'))

    安装 loader:

    npm install css-loader style-loader

    重新编译打包,刷新页面,就可以看到黄色的页面背景了。

    如果每次requireCSS 文件的时候都要写 loader 前缀,是一件很繁琐的事情。我们可以根据模块类型(扩展名)来自动绑定需要的 loader。

    将 entry.js 中的require("!style!css!./style.css")修改为require("./style.css"),然后执行:

    $ webpack entry.js bundle.js --module-bind'css=style-loader!css-loader'# 有些环境下可能需要使用双引号$ webpack entry.js bundle.js --module-bind"css=style-loader!css-loader"

    显然,这两种使用 loader 的方式,效果是一样的。

    *暴露模块

    webpack loaders

    配置文件

    Webpack 在执行的时候,除了在命令行传入参数,还可以通过指定的配置文件来执行。默认情况下,会搜索当前目录的webpack.config.js文件,这个文件是一个 node.js 模块,返回一个 json 格式的配置信息对象,或者通过--config选项来指定配置文件。

    继续我们的案例,在根目录创建package.json来添加 webpack 需要的依赖:

    {

         "name":"webpack-example",

         "version":"1.0.0",

         "description":"A simple webpack example.",

        "main":"bundle.js",

         "scripts": {"test":"echo "Error: no test specified" && exit 1"},

         "keywords": ["webpack"],

         "author":"zhaoda",

         "license":"MIT",

    新葡亰496net:webpack使用明白,致我们自然组件化的Web。     "devDependencies": {

               "css-loader":"^0.21.0",

               "style-loader":"^0.13.0",

               "webpack":"^1.12.2"

         }

    }

    # 如果没有写入权限,请尝试如下代码更改权限

    chflags -R nouchg .

    sudo chmod775package.json

    别忘了运行npm install。

    然后创建一个配置文件webpack.config.js:

    var webpack =require('webpack')

    module.exports = { 

         entry:'./entry.js',  

         output: {   path: __dirname,    filename:'bundle.js'},

         module: {    loaders: [      {test:/.css$/, loader:'style-loader!css-loader'}    ]  }

    }

    同时简化entry.js中的style.css加载方式:

    require('./style.css')

    最后运行webpack,可以看到 webpack 通过配置文件执行的结果和上一章节通过命令行webpack entry.js bundle.js --module-bind 'css=style-loader!css-loader'执行的结果是一样的。

    module.exports = "It works from content.js.";//nodejs中的暴露方式

    loader 定义

    插件

    插件可以完成更多 loader 不能完成的功能。

    插件的使用一般是在 webpack 的配置信息plugins选项中指定。

    Webpack 本身内置了一些常用的插件,还可以通过 npm 安装第三方插件。

    接下来,我们利用一个最简单的BannerPlugin内置插件来实践插件的配置和运行,这个插件的作用是给输出的文件头部添加注释信息。

    修改webpack.config.js,添加plugins:

    var webpack =require('webpack')

    module.exports = {  

         entry:'./entry.js',  

         output: {    path: __dirname,    filename:'bundle.js'},

         module: {    loaders: [      {test:/.css$/, loader:'style-loader!css-loader'}    ]  },  

         plugins: [newwebpack.BannerPlugin('This file is created by zhaoda')  ]

    }

    然后运行webpack,打开bundle.js,可以看到文件头部出现了我们指定的注释信息:

    /*! This file is created by zhaoda *//******/(function(modules){// webpackBootstrap/******/// The module cache/******/varinstalledModules = {};// 后面代码省略

    export default Girls;//ES6

    loader 功能

    开发环境

    当项目逐渐变大,webpack 的编译时间会变长,可以通过参数让编译的输出内容带有进度和颜色。

    $ webpack --progress --colors

    如果不想每次修改模块后都重新编译,那么可以启动监听模式。开启监听模式后,没有变化的模块会在编译后缓存到内存中,而不会每次都被重新编译,所以监听模式的整体速度是很快的。

    $ webpack --progress --colors --watch

    当然,使用webpack-dev-server开发服务是一个更好的选择。它将在 localhost:8080 启动一个 express 静态资源 web 服务器,并且会以监听模式自动运行 webpack,在浏览器打开http://localhost:8080/或http://localhost:8080/webpack-dev-server/可以浏览项目中的页面和编译后的资源输出,并且通过一个 socket.io 服务实时监听它们的变化并自动刷新页面。

    # 安装

    $ npm install webpack-dev-server -g

    # 运行

    $ webpack-dev-server --progress --colors

    *引入模块

    loader 配置

    故障处理

    Webpack 的配置比较复杂,很容出现错误,下面是一些通常的故障处理手段。

    一般情况下,webpack 如果出问题,会打印一些简单的错误信息,比如模块没有找到。我们还可以通过参数--display-error-details来打印错误详情。

    $ webpack --display-error-details

    Hash: a40fbc6d852c51fceadbVersion: webpack1.12.2Time:586ms    Asset    Size  Chunks            Chunk Namesbundle.js12.1kB0[emitted]  main  [0] ./entry.js153bytes {0} [built] [1error]  [5] ./module.js43bytes {0} [built]    4hidden modulesERRORin./entry.jsModule not found: Error: Cannot resolve'file'or'directory'./badpathmodulein/Users/zhaoda/data/projects/webpack-handbook/examplesresolve file  /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule doesn't exist

    /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.webpack.js doesn't exist  /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.js doesn't exist

    /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.web.js doesn't exist  /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.json doesn't exist

    resolve directory

    /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule doesn't exist (directory default file)  /Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule/package.json doesn't exist (directory description file)

    [/Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule]

    [/Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.webpack.js]

    [/Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.js]

    [/Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.web.js]

    [/Users/zhaoda/data/projects/webpack-handbook/examples/badpathmodule.json]

    @ ./entry.js 3:0-26

    Webpack 的配置提供了resolve和resolveLoader参数来设置模块解析的处理细节,resolve用来配置应用层的模块(要被打包的模块)解析,resolveLoader用来配置loader模块的解析。

    当引入通过 npm 安装的 node.js 模块时,可能出现找不到依赖的错误。Node.js 模块的依赖解析算法很简单,是通过查看模块的每一层父目录中的node_modules文件夹来查询依赖的。当出现 Node.js 模块依赖查找失败的时候,可以尝试设置resolve.fallback和resolveLoader.fallback来解决问题。

    module.exports = {  

                 resolve: { fallback: path.join(__dirname,"node_modules") }, 

                 resolveLoader: { fallback: path.join(__dirname,"node_modules") }

    };

    Webpack 中涉及路径配置最好使用绝对路径,建议通过path.resolve(__dirname, "app/folder")或path.join(__dirname, "app", "folder")的方式来配置,以兼容 Windows 环境。

    import MyModule from './modules/MyModule.js';//es6

    使用 loader

    CommonJS 规范

    CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。

    这个项目最开始是由 Mozilla 的工程师 Kevin Dangoor 在2009年1月创建的,当时的名字是 ServerJS。

    我在这里描述的并不是一个技术问题,而是一件重大的事情,让大家走到一起来做决定,迈出第一步,来建立一个更大更酷的东西。 —— Kevin Dangoor'sWhat Server Side JavaScript needs

    2009年8月,这个项目改名为 CommonJS,以显示其 API 的更广泛实用性。CommonJS 是一套规范,它的创建和核准是开放的。这个规范已经有很多版本和具体实现。CommonJS 并不是属于 ECMAScript TC39 小组的工作,但 TC39 中的一些成员参与 CommonJS 的制定。2013年5月,Node.js 的包管理器 NPM 的作者 Isaac Z. Schlueter 说CommonJS 已经过时,Node.js 的内核开发者已经废弃了该规范。

    CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。该规范的主要内容是,模块必须通过module.exports导出对外的变量或接口,通过require()来导入其他模块的输出到当前模块作用域中。

    一个直观的例子:

    // moduleA.jsmodule.exports =function(value){returnvalue *2;}

    // moduleB.jsvarmultiplyBy2 =require('./moduleA');varresult = multiplyBy2(4);

    CommonJS 是同步加载模块,但其实也有浏览器端的实现,其原理是现将所有模块都定义好并通过id索引,这样就可以方便的在浏览器环境中解析了,可以参考require1k和tiny-browser-require的源码来理解其解析(resolve)的过程。

    var MyModule = require('./MyModule.js');//commonjs

    webpack 开发环境与生产环境

    AMD 规范

    AMD(异步模块定义)是为浏览器环境设计的,因为 CommonJS 模块系统是同步加载的,当前浏览器环境还没有准备好同步加载模块的条件。

    AMD 定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。

    模块通过define函数定义在闭包中,格式如下:

    define(id?:String, dependencies?:String[], factory:Function|Object);

    id是模块的名字,它是可选的参数。

    dependencies指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入factory中。如果没有指定dependencies,那么它的默认值是["require", "exports", "module"]。

    define(function(require, exports, module){})

    factory是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。

    一些用例:

    定义一个名为myModule的模块,它依赖jQuery模块:

    define('myModule', ['jquery'],function($){// $ 是 jquery 模块的输出$('body').text('hello world');});// 使用define(['myModule'],function(myModule){});

    注意:在 webpack 中,模块名只有局部作用域,在 Require.js 中模块名是全局作用域,可以在全局引用。

    定义一个没有id值的匿名模块,通常作为应用的启动函数:

    define(['jquery'],function($){    $('body').text('hello world');});

    依赖多个模块的定义:

    define(['jquery','./math.js'],function($, math){// $ 和 math 一次传入 factory$('body').text('hello world');});

    模块输出:

    define(['jquery'],function($){varHelloWorldize =function(selector){       

     $(selector).text('hello world');    };// HelloWorldize 是该模块输出的对外接口returnHelloWorldize;});

    在模块定义内部引用依赖:

    define(function(require){var$ =require('jquery');    $('body').text('hello world');});

    webpack require 一切
    require("./content.js"); // 添加content.js

    webpack 分割 vendor 代码和应用业务代码

    *加载CSS

    webpack develop server

    安装css-loader : npm install css-loader style-loader
    require("style!css!
    ../css/main.css")//加载CSS style!css!是声明这个模块是CSS style!css!可以不写 在loaders里面配置信息即可

    安装 webpack-dev-server

    import "../css/main.css";//ES6引入方式

    启动 webpack-dev-server

    *配置文件

    代码监控

    webpack.config.js 以下是基本配置
    单个入口文件
    var path = require('path');

    自动刷新

    module.exports = {

    热加载 (hot module replacement)

    entry: "./js/entry.js",
    
    output: {
    

    在 webpack.config.js 中配置 webpack develop server

    path: './dist',

    2.2.1 webpack 介绍

        publicPath: './dist/',
    
        filename: "bundle.js"
    
    },
    
    module: {
    
        loaders: [
    
            { test: /.css$/, loader: "style!css" }
    
        ]
    
    }
    

    webpack 是什么

    };

    webpack is a module bundler. webpack takes modules with dependencies and generates static assets representing those modules

    多个入口文件

    webpack 是一个模块打包工具,输入为包含依赖关系的模块集,输出为打包合并的前端静态资源。在上一节的前端工程化中,已经介绍过,webpack 是同时支持 AMD 和 CommonJs 的模块定义方式,不仅如此,webpack 可以将任何前端资源视为模块,如 css,图片,文本。

    var path = require('path');

    为什么要引入新的打包工具

    module.exports = {

    在 webpack 出现之前,已经有了一些打包工具,如 Browserify, 那为什么不优化这些工具,而是重复造轮子?

    entry: {
    
        page1:["./js/entry.js","./js/double.js"]
    
    },
    
    output: {
    

    webpack 之前的打包工具工具功能单一,只能完成特定的任务,然而 web 前端工程是复杂的,一个 webapp 对于业务代码的要求可能有:

    path: './dist',

    代码可以分块,实现按需加载

        publicPath: './dist/',
    
        filename: "bundle.js"
    
    },
    
    module: {
    
        loaders: [
    
            { test: /.css$/, loader: "style!css" }
    
        ]
    
    }
    

    首屏加载时间要尽量减少

    };

    需要集成一些第三方库

    加载器配置

    对于模块打包工具,单一的支持 CommonJs 的打包在大型项目中是不够用的,为了满足一个大型项目的前端需求,那么一个打包工具应该包含一些这些功能:

    这里需要在output模块里面设置publicPath否则CSS背景图片等输出有问题

    支持多个 bundler 输出 -> 解决代码分块问题

    module: { //加载器配置 loaders: [ { test: /.css$/, loader: 'style-loader!css-loader' }, { test: /.js$/, loader: 'jsx-loader?harmony' }, { test: /.scss$/, loader: 'style!css!sass?sourceMap'}, { test: /.(png|jpg)$/, loader: 'url-loader?limit=8192'} ] },

    异步加载 -> 按需加载,优化首屏加载时间

    entry: {
    page1: "./page1",//单个文件形式 支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出
    新葡亰496net:webpack使用明白,致我们自然组件化的Web。page2: ["./entry1", "./entry2"]
    },//数组形式 如果采用下面你的写法 不能用上面的这种
    output: {
    path: "dist/js/page",
    filename: "[name].bundle.js"
    }

    可定制化 -> 可以集成第三方库,可以定制化打包过程

    *暴露模块使用案例

    其他资源也可以定义为模块

    var index={//这是添加content.js
    main:function(){
    var html="1111111";
    return html;
    }
    }
    module.exports=index;

    webpack 的出现正式为了解决这些问题,在 webpack 中,提供了一下这些功能:

    //这是在另一个文件
    var index=require("./content.js"); // 添加content.js

    代码分块: webpack 有两种类型的模块依赖,一种是同步的,一种是异步的。在打包的过程中可以将代码输出为代码块(chunk),代码块可以实现按需加载。 异步加载的代码块通过分割点(spliting point)来确定。

    document.getElementById("box").innerHTML=index.main();

    Loaders: Webpack 本身只会处理 Javascript,为了实现将其他资源也定义为模块,并转化为 Javascript, Webpack 定义 loaders , 不同的 loader 可以将对应的资源转化为 Javascript 模块。

    *npm install --save 与 npm install --save-dev 的区别

    智能的模块解析: webpack 可以很容易将第三方库转化为模块集成到项目代码中,模块的依赖可以用表达式的方式(这在其他打包工具中是没有支持的),这种模块依赖叫做动态模块依赖。

    一个放在package.json 的dependencies , 一个放在devDependencies里面

    插件系统: webpack 的可定制化在于其插件系统,其本身的很多功能也是通过插件的方式实现,插件系统形成了 webpack 的生态,是的可以使用很多开源的第三方插件。

    扩展:

    webpack 核心思想

    在package.json 设置它的scripts npm run build===webpack(这里是运行打包)

    webpack 的三个核心:

    { "scripts": { "build": "webpack", "dev": "webpack-dev-server --devtool eval --progress --colors --hot --content-base build" }}

    万物皆模块:在 webpack 的世界中,除了 Javascript,其他任何资源都可以当做模块的方式引用

    webpack-dev-server 自动监听(此时还不能自动刷新浏览器)ctrl C退出服务

    按需加载: webapp 的优化关键在于代码体积,当应用体积增大,实现代码的按需加载是刚需,这也是 webpack 出现的根本原因

    npm i webpack-dev-server --save
    npm run dev 在http://localhost:8080监听文件修改
    "dev": "webpack-dev-server --devtool eval --progress --colors --hot --content-base build"

    可定制化: 任何一个工具都不可能解决所有问题,提供解决方案才是最可行的,webpack 基于可定制化的理念构建,通过插件系统,配置文件,可以实现大型项目的定制需求。

    webpack-dev-server

    2.2.2 安装配置

    • 在 localhost:8080 建立一个 Web 服务器
      --devtool eval
    • 为你的代码创建源地址。当有任何报错的时候可以让你更加精确地定位到文件和行号
      --progress
    • 显示合并代码进度
      --colors
    • Yay,命令行中显示颜色!
      --content-base build
    • 指向设置的输出目录

    第一步:Node.js

    如果需要浏览器自动刷新你需要在配置中增加一个入口点。
    webpack.config.js
    **entry: [ 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080', path.resolve(__dirname, 'app/main.js') ],

    webpack 是 Node 实现,首先需要到 Node.js 下载安装最新版本的 Node.js

    **

    第二步:webpack-cli

    Node.js 安装好过后,打开命令行终端,通过 npm 命令安装:

    // -g 参数表示全局安装
    $ npm install webpack -g
    第三步:新建空前端项目

    为了使用 webpack,先新建一个空前端项目,创建一个目录,目录结构如下:

    .
    ├── index.html // 入口 HTML
    ├── dist // dist 目录放置编译过后的文件文件
    └── src // src 目录放置源文件
    └── index.js // 入口 js
    其中 html 内容:

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Hello React!</title>
    </head>
    <body>
    <div id="AppRoot"></div>
    <script src="dist/index.js"></script>
    </body>
    </html>
    index.js 内容为:

    alert('hello world webpack');
    第四步:在项目中安装 webpack

    // 初始化 package.json, 根据提示填写 package.json 的相关信息
    $ npm init

    // 下载 webpack 依赖
    // --save-dev 表示将依赖添加到 package.json 中的 'devDependencies' 对象中
    $ npm install webpack --save-dev

    • 第五步:Develop Server 工具 (可选)

    dev server 可以实现一个基于 node express 的前端 server

    $ npm install webpack-dev-server --save-dev
    2.2.3 webpack 使用

    命令行调用

    在之前创建的目录下执行:

    $ webpack src/index.js dist/index.js
    执行成功过后会出现如下信息:

    Hash: 9a8e7e83864a07c0842f
    Version: webpack 1.13.1
    Time: 37ms
    Asset Size Chunks Chunk Names
    index.js 1.42 kB 0 [emitted] main
    [0] ./src/index.js 29 bytes {0} [built]
    可以查看 dist/index.js 的编译结果:

    /******/ (function(modules) { // webpackBootstrap
    // .......... UMD 定义内容
    /******/ })
    /************************************************************************/
    /******/ ([
    /* 0 /
    /
    **/ function(module, exports) {
    // index.js 的内容被打包进来
    alert('hello world webpack');

    /***/ }
    /******/ ]);
    在浏览器中打开 index.html :

    配置文件

    以命令执行的方式需要填写很长的参数,所以 webpack 提供了通过配置的方式执行,在项目目录下创建 webpack.config.js 如下:

    var webpack = require('webpack')
    module.exports = {
    entry: './src/index.js',
    output: {
    path: './dist/',
    filename: 'index.js'
    }
    }
    执行:

    $ webpack
    会和通过命令执行有同样的输出

    2.2.4 webpack 配置

    entry 和 output

    webpack 的配置中主要的两个配置 key 是,entry 和 output。

    {
    entry: [String | Array | Object], // 入口模块
    output: {
    path: String, // 输出路径
    filename: String // 输出名称或名称 pattern
    publicPath: String // 指定静态资源的位置
    ... // 其他配置
    }
    }
    单一入口

    如果只有一个入口文件,可以有如下几种配置方式

    // 第一种 String
    {
    entry: './src/index.js',
    output: {
    path: './dist/',
    filename: 'index.js'
    }
    }

    // 第二种 Array
    {
    entry: ['./src/index.js'],
    output: {
    path: './dist/',
    filename: 'index.js'
    }
    }

    // 第三种 Object
    {
    entry: {
    index: './src/index.js',
    },
    output: {
    path: './dist/',
    filename: 'index.js'
    }
    }
    多个入口文件

    当存在多个入口时 ,可以使用 Array 的方式,比如依赖第三方库 bootstrap ,最终 bootstrap 会被追加到打包好的 index.js 中,数组中的最后一个会被 export。

    {
    entry: ['./src/index.js', './vendor/bootstrap.min.js'],
    output: {
    path: './dist',
    filename: "index.js"
    }
    }
    最终的输出结果如:

    /******/ ([
    /* 0 /
    /
    **/ function(module, exports, webpack_require) {

    __webpack_require__(1);
    
    // export 最后一个
    module.exports = __webpack_require__(2);
    

    // },
    /
    1 /
    /
    / function(module, exports) {

    alert('hello world webpack');
    

    // },
    /
    2 /
    /
    / function(module, exports) {
    // bootstrap 的内容被追加到模块中
    console.log('bootstrap file');

    /***/ }
    /******/ ])
    多个打包目标

    上面的例子中都是打包出一个 index.js 文件,如果项目有多个页面,那么需要打包出多个文件,webpack 可以用对象的方式配置多个打包文件

    {
    entry: {
    index: './src/index.js',
    a: './src/a.js'
    },
    output: {
    path: './dist/',
    filename: '[name].js'
    }
    }
    最终会打包出:

    .
    ├── a.js
    └── index.js
    文件名称 pattern

    [name] entry 对应的名称

    [hash] webpack 命令执行结果显示的 Hash 值

    [chunkhash] chunk 的 hash

    为了让编译的结果名称是唯一的,可以利用 hash 。

    2.2.5 webpack 支持 Jsx

    现在我们已经可以使用 webpack 来打包基于 CommonJs 的 Javascript 模块了,但是还没法解析 JSX 语法和 Es6 语法。下面我们将利用 Babel 让 webpack 能够解析 Es6 和 Babel

    第一步:npm install 依赖模块

    // babel 相关的模块
    $ npm install babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-polyfill --save-dev

    // react 相关的模块
    $ npm install react react-dom --save
    第二步:webpack.config.js 中添加 babel loader 配置

    {
    entry: {
    index: './src/index.js',
    a: './src/a.js'
    },
    output: {
    path: './dist/',
    filename: '[name].js'
    },
    module: {
    loaders: [{
    test: /.js$/,
    exclude: /node_modules/,
    loader: 'babel',
    query: {
    presets: ['es2015', 'stage-0', 'react']
    }
    }]
    }
    }
    第三步: 修改 index.js 为 React 的语法

    src/index.js 内容改为:

    Es6 的知识在后面的章节中讲解,目前我们暂时以 Es5 的方式来写,但是配置已经支持了 Es6 的编译,熟悉 Es6 的读者也可以直接写 Es6

    // 通过 require 的方式依赖 React,ReactDOM
    var React = require('react');
    var ReactDOM = require('react-dom');

    var Hello = React.createClass({
    render: function render() {
    return <div>Hello {this.props.name}</div>;
    }
    });

    ReactDOM.render(
    <Hello name="World" />,
    document.getElementById('AppRoot')
    );
    第四步:运行 webpack

    $ webpack
    执行结果:

    Hash: ae2a037c191c18195b6a
    Version: webpack 1.13.1
    Time: 1016ms
    Asset Size Chunks Chunk Names
    a.js 1.42 kB 0 [emitted] a
    index.js 700 kB 1 [emitted] index

    • 169 hidden modules
      浏览器中打开 index.html 会显示 Hello World

    2.2.6 webpack loaders

    在配置 JSX 的过程中,使用到了 loader, 前面已经介绍过 webpack 的核心功能包含 loader,通过 loader 可以将任意资源转化为 javascript 模块。

    loader 定义

    Loaders are transformations that are applied on a resource file of your app.
    (Loaders 是应用中源码文件的编译转换器)

    也就是说在 webpack 中,通过 loader 可以实现 JSX 、Es6、CoffeeScript 等的转换

    loader 功能
    loader 管道:在同一种类型的源文件上,可以同时执行多个 loader , loader 的执行方式可以类似管道的方式,管道执行的方式是从右到左的方式loader 可以支持同步和异步
    loader 可以接收配置参数

    loader 可以通过正则表达式或者文件后缀指定特定类型的源文件

    插件可以提供给 loader 更多功能

    loader 除了做文件转换以外,还可以创建额外的文件

    loader 配置

    新增 loader 可以在 webpack.config.js 的 module.loaders 数组中新增一个 loader 配置。

    一个 loader 的配置为:

    {
    // 通过扩展名称和正则表达式来匹配资源文件
    test: String ,
    // 匹配到的资源会应用 loader, loader 可以为 string 也可以为数组
    loader: String | Array
    }
    感叹号和数组可以定义 loader 管道:

    {
    module: {
    loaders: [
    { test: /.jade$/, loader: "jade" },
    // => .jade 文件应用 "jade" loader

            { test: /.css$/, loader: "style!css" },
            { test: /.css$/, loaders: ["style", "css"] },
            // => .css 文件应用  "style" 和 "css" loader  
        ]
    }
    

    }
    loader 可以配置参数

    {
    module: {
    loaders: [
    // => url-loader 配置 mimetype=image/png 参数
    {
    test: /.png$/,
    loader: "url-loader?mimetype=image/png"
    }, {
    test: /.png$/,
    loader: "url-loader",
    query: { mimetype: "image/png" }
    }

        ]
    }
    

    }
    使用 loader

    第一步: 安装

    loader 和 webpack 一样都是 Node.js 实现,发布到 npm 当中,需要使用 loader 的时候,只需要

    $ npm install xx-loader --save-dev

    // eg css loader
    $ npm install css-loader style-loader --save-dev
    第二步:修改配置

    {
    entry: {
    index: './src/index.js',
    a: './src/a.js'
    },
    output: {
    path: './dist/',
    filename: '[name].js'
    },
    module: {
    loaders: [{
    test: /.js$/,
    exclude: /node_modules/,
    loader: 'babel',
    query: {
    presets: ['es2015', 'stage-0', 'react']
    }
    }, {
    test: /.css$/,
    loader: "style-loader!css-loader"
    }]
    }
    }
    第三步:使用

    前面我们已经使用过 jsx loader 了, loader 的使用方式有多种

    在配置文件中配置

    显示的通过 require 调用

    命令行调用

    显示的调用 require 会增加模块的耦合度,应尽量避免这种方式

    以 css-loader 为例子,在项目 src 下面创建一个 css

    src/style.css

    body {
    background: red;
    color: white;
    }
    修改 webpack 配置 entry 添加

    entry: {
    index: ['./src/index.js', './src/style.css']
    }
    执行 webpack 命令然后打开 index.html 会看到页面背景被改为红色。

    最终的编译结果为:

    ....
    function(module, exports, webpack_require) {
    exports = module.exports = webpack_require(171)();
    exports.push([module.id, "nbody {n background: red;n color: white;n}n", ""]);
    }
    ....
    可以看到 css 被转化为了 javascript, 在页面中并非调用 <link rel="stylesheet" href=""> 的方式, 而是使用 inline 的<style>.....</style>

    另外一种方法是直接 require, 修改 src/index.js:

    var css = require("css!./style.css");
    编译结果相同。

    2.2.7 webpack 开发环境与生产环境

    前端开发环境通常分为两种,开发环境和生成环境,在开发环境中,可能我们需要日志输出,sourcemap ,错误报告等功能,在生成环境中,需要做代码压缩,hash 值生成。两种环境在其他的一些配置上也可能不同。

    所以为了区分,我们可以创建两个文件:

    webpack.config.js // 开发环境

    webpack.config.prod.js // 生产环境

    生产环境 build 用如下命令:

    $ webpack --config webpack.config.prod.js
    在本章深入 webpack 小节中会更多的介绍生产环境中的优化

    2.2.8 webpack 插件

    webpack 提供插件机制,可以对每次 build 的结果进行处理。配置 plugin 的方法为在 webpack.config.js 中添加:

    {
    plugins: [
    new BellOnBundlerErrorPlugin()
    ]
    }
    plugin 也是一个 npm 模块,安装一个 plugin :

    $ npm install bell-on-bundler-error-plugin --save-dev
    2.2.9 webpack 分割 vendor 代码和应用业务代码

    在上面的 jsx 配置中,我们将 React 和 ReactDOM 一起打包进了项目代码。为了实现业务代码和第三方代码的分离,我们可以利用
    CommonsChunkPlugin 插件.

    修改 webpack.config.js

    {
    entry: {
    index: './src/index.js',
    a: './src/a.js',
    // 第三方包
    vendor: [
    'react',
    'react-dom'
    ]
    },
    output: {
    path: './dist/',
    filename: '[name].js'
    },
    module: {
    loaders: [{
    test: /.js$/,
    exclude: /node_modules/,
    loader: 'babel',
    query: {
    presets: ['es2015', 'stage-0', 'react']
    }
    }, {
    test: /.css$/,
    loader: "style-loader!css-loader"
    }]
    },
    plugins: [
    new webpack.optimize.CommonsChunkPlugin(/* chunkName= /"vendor", / filename= */"vendor.bundle.js")
    ]
    }
    执行 webpack 命令,输出日志:

    Hash: f1256dc00b9d4bde8f7f
    Version: webpack 1.13.1
    Time: 1459ms
    Asset Size Chunks Chunk Names
    a.js 109 bytes 0 [emitted] a
    index.js 10.9 kB 1 [emitted] index
    vendor.bundle.js 702 kB 2 [emitted] vendor
    [0] multi vendor 40 bytes {2} [built]
    [0] multi index 40 bytes {1} [built]

    • 173 hidden modules
      index.js 体积变小了,多出了 vendor.bundle.js

    2.2.10 webpack develop server

    在前端开发的过程中,通常需要启动一个服务器,把开发打包好的前端代码放在服务器上,通过访问服务器访问并测试(因为可以有些情况需要 ajax 请求)。 webpack 提供了一个基于 node.js Express 的服务器 - webpack-dev-server 来帮助我们简化服务器的搭建,并提供服务器资源访问的一些简单配置。

    安装 webpack-dev-server

    $ npm install webpack-dev-server -g
    启动 webpack-dev-server

    $ webpack-dev-server --content-base ./
    --content-base ./ 参数表示将当前目录作为 server 根目录。 命令启动过后,会在 8080 端口启动一个 http 服务,通过访问http://localhost:8080/index.html 可以访问 index.html 内容。

    如果访问提示报错:

    Uncaught ReferenceError: webpackJsonp is not defined
    原因是 html 中没有引用 vendor.bundle.js, 修改 html :

    <script src="dist/vendor.bundle.js"></script>
    <script src="dist/index.js"></script>
    修改 index.html 过后可以看到正确结果

    代码监控

    webpack-dev-server 除了提供 server 服务以外, 还会监控源文件的修改,如果源文件改变了,会调用 webpack 重新打包

    修改 style.css 中的内容为:

    body {
    background: whitesmoke;
    color: #333;
    font-size: 100px;
    }
    可以看到输出以下日志:

    [168] ./~/react/lib/renderSubtreeIntoContainer.js 466 bytes {2} [built]
    webpack: bundle is now VALID.
    webpack: bundle is now INVALID.
    Hash: cc7d7720b1a0fcbef972
    Version: webpack 1.13.0
    Time: 76ms
    chunk {0} a.js (a) 32 bytes {2}

    • 1 hidden modules
      chunk {1} index.js (index) 10.3 kB {2}
      [170] ./~/css-loader!./src/style.css 230 bytes {1} [built]
    • 5 hidden modules
      chunk {2} vendor.bundle.js (vendor) 665 kB
    • 168 hidden modules
      webpack: bundle is now VALID.
      这个时候说明代码已经修改了,但是这个时候刷新浏览器过后,背景是没有改变的,原因是 webpack-dev-server 的打包结果是放在内存的,查看 dist/index.js 的内容实际上是没有改变的,那如何访问内存中的打包内容呢?

    修改 webpack.config.js 的 output.publicPath:

    output: {
    path: './dist/',
    filename: '[name].js',
    publicPath: '/dist'
    // webpack-dev-server 启动目录是 /, /dist 目录是打包的目标目录相对于启动目录的路径
    },
    重新启动

    $ ctrl c 结束进程
    $ webpack-dev-server
    修改 style.css 再刷新页面,修改的内容会反映出来。

    自动刷新

    上面的配置已经能做到自动监控代码,每次修改完代码,刷新浏览器就可以看到最新结果,但是 webpack-dev-server 还提供了自动刷新功能,有两种模式。

    Iframe 模式

    修改访问的路径: http://localhost:8080/index.html -> http://localhost:8080/webpack-dev-server/index.html 。这个时候每次修改代码,打包完成过后都会自动刷新页面。

    不需要额外配置,只用修改路径

    应用被嵌入了一个 iframe 内部,页面顶部可以展示打包进度信息

    因为 iframe 的关系,如果应用有多个页面,无法看到当前应用的 url 信息

    inline 模式

    启动 webpack-dev-server 的时候添加 --inline 参数

    需要添加 --inline 配置参数

    没有顶部信息提示条,提示信息在控制台中展现

    热加载 (hot module replacement)

    webpack-dev-server 还提供了模块热加载的方式,在不刷新浏览器的条件下,应用最新的代码更新,启动 webpack-dev-server 的时候添加 --inline --hot 参数就可以体验。

    $ webpack-dev-server --inline --hot
    修改代码在浏览器控制台中会看到这样的日志输出:

    [HMR] Waiting for update signal from WDS...
    vendor.bundle.js:670 [WDS] Hot Module Replacement enabled.
    2vendor.bundle.js:673 [WDS] App updated. Recompiling...
    vendor.bundle.js:738 [WDS] App hot update...
    vendor.bundle.js:8152 [HMR] Checking for updates on the server...
    vendor.bundle.js:8186 [HMR] Updated modules:
    vendor.bundle.js:8188 [HMR] - 245
    vendor.bundle.js:8138 [HMR] App is up to date.
    在 webpack.config.js 中配置 webpack develop server

    修改 webpack.config.js 添加:

    plugins: [
    new webpack.optimize.CommonsChunkPlugin(
    /* chunkName= /"vendor",
    /
    filename= */"vendor.bundle.js", Infinity),
    // 需要手动添加 HotModuleReplacementPlugin , 命令行的方式会自动添加
    new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
    hot: true,
    inline: true
    }
    不加参数直接执行 webpack-dev-server

    $ webpack-dev-server
    webpack-dev-server 还提供了其他的一些功能, 如:

    配置 proxy

    访问 node.js API

    和现有的 node 服务集成

    基于这些功能可以实现很多自定义的配置。

    本文由新葡亰496net发布于新葡亰官网,转载请注明出处:新葡亰496net:webpack使用明白,致我们自然组件化

    关键词: