吉祥物

吉祥物 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

吉祥物 关注了标签 · 2018-02-05

android

Android(安卓或安致)是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

简介

  Android一词的本义指“机器人”,同时也是Google于2007年11月5日宣布的基于Linux平台的开源手机操作系统的名称,该平台由操作系统、中间件、用户界面和应用软件组成。 

  系统架构

  android的系统架构和其操作系统一样,采用了分层的架构。从架构图看,android分为四个层,从高层到低层分别是应用程序层、应用程序框架层、系统运行库层和linux核心层。

  应用程序

  Android会同一系列核心应用程序包一起发布,该应用程序包包括客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等。所有的应用程序都是使用JAVA语言编写的。

  应用程序框架

  开发人员也可以完全访问核心应用程序所使用的API框架。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块(不过得遵循框架的安全性)。同样,该应用程序重用机制也使用户可以方便的替换程序组件。

  隐藏在每个应用后面的是一系列的服务和系统, 其中包括;

  丰富而又可扩展的视图(Views),可以用来构建应用程序, 它包括列表(lists),网格(grids),文本框(text boxes),按钮(buttons), 甚至可嵌入的web浏览器。

  内容提供器(Content Providers)使得应用程序可以访问另一个应用程序的数据(如联系人数据库), 或者共享它们自己的数据

  资源管理器(Resource Manager)提供 非代码资源的访问,如本地字符串,图形,和布局文件( layout files )。

  通知管理器 (Notification Manager) 使得应用程序可以在状态栏中显示自定义的提示信息。

  活动管理器( Activity Manager) 用来管理应用程序生命周期并提供常用的导航回退功能。

  有关更多的细节和怎样从头写一个应用程序,请参考 如何编写一个 Android 应用程序。

  系统运行库

  Android 包含一些C/C++库,这些库能被Android系统中不同的组件使用。它们通过 Android 应用程序框架为开发者提供服务。以下是一些核心库:

  * 系统 C 库 - 一个从BSD继承来的标准 C 系统函数库( libc ), 它是专门为基于 embedded linux的设备定制的。

  * 媒体库 - 基于PacketVideo OpenCORE;该库支持多种常用的音频、视频格式回放和录制,同时支持静态图像文件。编码格式包括MPEG4, H.264, MP3, AAC, AMR, JPG, PNG 。

  * Surface Manager - 对显示子系统的管理,并且为多个应用程序提 供了2D和3D图层的无缝融合。

  * LibWebCore - 一个最新的web浏览器引擎用,支持Android浏览器和一个可嵌入的web视图。

  应用程序组件

  Android开发四大组件分别是:活动(Activity): 用于表现功能。服务(Service): 后台运行服务,不提供界面呈现。广播接收器(BroadcastReceiver):用于接收广播。内容提供商(Content Provider): 支持在多个应用中存储和读取数据,相当于数据库。

  活动

  Android 中,Activity 是所有程序的根本,所有程序的流程都运行在Activity 之中,Activity可以算是开发者遇到的最频繁,也是Android 当中最基本的模块之一。在Android的程序当中,Activity 一般代表手机屏幕的一屏。如果把手机比作一个浏览器,那么Activity就相当于一个网页。在Activity 当中可以添加一些Button、Check box 等控件。可以看到Activity 概念和网页的概念相当类似。

  一般一个Android 应用是由多个Activity 组成的。这多个Activity 之间可以进行相互跳转,例如,按下一个Button 按钮后,可能会跳转到其他的Activity。和网页跳转稍微有些不一样的是,Activity 之间的跳转有可能返回值,例如,从Activity A 跳转到Activity B,那么当Activity B 运行结束的时候,有可能会给Activity A 一个返回值。这样做在很多时候是相当方便的。

  当打开一个新的屏幕时,之前一个屏幕会被置为暂停状态,并且压入历史堆栈中。用户可以通过回退操作返回到以前打开过的屏幕。我们可以选择性的移除一些没有必要保留的屏幕,因为Android会把每个应用的开始到当前的每个屏幕保存在堆栈中。

  服务

  Service 是android 系统中的一种组件,它跟Activity 的级别差不多,但是他不能自己运行,只能后台运行,并且可以和其他组件进行交互。Service 是没有界面的长生命周期的代码。Service 是一种程序,它可以运行很长时间,但是它却没有用户界面。这么说有点枯燥,来看个例子。打开一个音乐播放器的程序,这个时候若想上网了,那么,我们打开Android 浏览器,这个时候虽然我们已经进入了浏览器这个程序,但是,歌曲播放并没有停止,而是在后台继续一首接着一首的播放。其实这个播放就是由播放音乐的Service进行控制。当然这个播放音乐的Service也可以停止,例如,当播放列表里边的歌曲都结束,或者用户按下了停止音乐播放的快捷键等。service 可以在和多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD 卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务嘛,总是藏在后头的。

  开启service有两种方式:

  (1) Context.startService():Service会经历onCreate -> onStart(如果Service还没有运行,则android先调用onCreate()然后调用onStart();如果Service已经运行,则只调用onStart(),所以一个Service的onStart方法可能会重复调用多次 );stopService的时候直接onDestroy,如果是调用者自己直接退出而没有调用stopService的话,Service会一直在后台运行。该Service的调用者再启动起来后可以通过stopService关闭Service。 注意,多次调用Context.startservice()不会嵌套(即使会有相应的onStart()方法被调用),所以无论同一个服务被启动了多少次,一旦调用Context.stopService()或者stopSelf(),他都会被停止。补充说明:传递给startService()的Intent对象会传递给onStart()方法。调用顺序为:onCreate --> onStart(可多次调用) --> onDestroy。

  (2) Context.bindService():Service会经历onCreate() --> onBind(),onBind将返回给客户端一个IBind接口实例,IBind允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。这个时候把调用者(Context,例如Activity)会和Service绑定在一起,Context退出了,Srevice就会调用onUnbind --> onDestroyed相应退出,所谓绑定在一起就共存亡了。[20]

  广播接收器

  在Android 中,Broadcast 是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast进行过滤接受并响应的一类组件。可以使用BroadcastReceiver 来让应用对一个外部的事件做出响应。这是非常有意思的,例如,当电话呼入这个外部事件到来的时候,可以利用BroadcastReceiver 进行处理。例如,当下载一个程序成功完成的时候,仍然可以利用BroadcastReceiver 进行处理。BroadcastReceiver不能生成UI,也就是说对于用户来说不是透明的,用户是看不到的。BroadcastReceiver通过NotificationManager 来通知用户这些事情发生了。BroadcastReceiver 既可以在AndroidManifest.xml 中注册,也可以在运行时的代码中使用Context.registerReceiver()进行注册。只要是注册了,当事件来临的时候,即使程序没有启动,系统也在需要的时候启动程序。各种应用还可以通过使用Context.sendBroadcast () 将它们自己的intent broadcasts广播给其他应用程序。

  注册BroadcastReceiver有两种方式:

  (1)在AndroidManifest.xml进行注册。这种方法有一个特点即使你的应用程序已经关闭了,但这个BroadcastReceiver依然会接受广播出来的对象,也就是说无论你这个应用程序时开还是关都属于活动状态都可以接受到广播的事件;

  (2)在代码中注册广播。

  第一种俗称静态注册,第二种俗称动态注册,这两种注册Broadcast Receiver的区别:

  动态注册较静态注册灵活。实验证明:当静态注册一个Broadcast Receiver时,不论应用程序是启动与否。都可以接受对应的广播。

  动态注册的时候,如果不执行unregister Receiver();方法取消注册,跟静态是一样的。但是如果执行该方法,当执行过以后,就不能接受广播了。

  内容提供

  Content Provider 是Android提供的第三方应用数据的访问方案。

  在Android中,对数据的保护是很严密的,除了放在SD卡中的数据,一个应用所持有的数据库、文件等内容,都是不允许其他直接访问的。Andorid当然不会真的把每个应用都做成一座孤岛,它为所有应用都准备了一扇窗,这就是Content Provider。应用想对外提供的数据,可以通过派生Content Provider类, 封装成一枚Content Provider,每个Content Provider都用一个uri作为独立的标识,形如:content://com.xxxxx。所有东西看着像REST的样子,但实际上,它比REST 更为灵活。和REST类似,uri也可以有两种类型,一种是带id的,另一种是列表的,但实现者不需要按照这个模式来做,给你id的uri你也可以返回列表类型的数据,只要调用者明白,就无妨,不用苛求所谓的REST。

  另外,Content Provider不和REST一样只有uri可用,还可以接受Projection,Selection,OrderBy等参数,这样,就可以像数据库那样进行投影,选择和排序。查询到的结果,以Cursor(参见:reference/android/database/Cursor.html )的形式进行返回,调用者可以移动Cursor来访问各列的数据。

  Content Provider屏蔽了内部数据的存储细节,向外提供了上述统一的接口模型,这样的抽象层次,大大简化了上层应用的书写,也对数据的整合提供了更方便的途径。Content Provider内部,常用数据库来实现,Android提供了强大的Sqlite支持,但很多时候,你也可以封装文件或其他混合的数据。

  在Android中,Content Resolver是用来发起Content Provider的定位和访问的。不过它仅提供了同步访问的Content Provider的接口。但通常,Content Provider需要访问的可能是数据库等大数据源,效率上不足够快,会导致调用线程的拥塞。因此Android提供了一个AsyncQueryHandler(参见:reference/android/content/AsyncQueryHandler.html),帮助进行异步访问Content Provider。

  在各大组件中,Service和Content Provider都是那种需要持续访问的。Service如果是一个耗时的场景,往往会提供异步访问的接口,而Content Provider不论效率如何,都提供的是约定的同步访问接口。

软件开发

  Java方面

  Android支持使用Java作为编程语言来开发应用程序,而Android的Java开发方面从接口到功能,都有层出不穷的变化。考虑到Java虚拟机的效率和资源占用,谷歌重新设计了Android的Java,以便能提高效率和减少资源占用,因而与J2ME等不同。其中Activity等同于J2ME的MIDlet,一个 Activity 类(Class)负责创建视窗(Windows),一个活动中的Activity就是在 foreground(前景)模式,背景运行的程序叫做Service。两者之间通过由ServiceConnection和AIDL连结,达到复数程序同时运行效果。如果运行中的 Activity 全部画面被其他 Activity 取代时,该 Activity 便被停止(Stopped),甚至被系统清除(Kill)。

  View等同于J2ME的Displayable,程序人员可以通过 View 类与“XML layout”档将UI放置在视窗上,Android 1.5的版本可以利用 View 打造出所谓的 Widgets,其实Widget只是View的一种,所以可以使用xml来设计layout,HTC的Android Hero手机即含有大量的widget。至于ViewGroup 是各种layout 的基础抽象类(abstract class),ViewGroup之内还可以有ViewGroup。View的构造函数不需要再Activity中调用,但是Displayable的是必须的,在Activity 中,要通过findViewById()来从XML 中取得View,Android的View类的显示很大程度上是从XML中读取的。View 与事件(event)息息相关,两者之间通过Listener 结合在一起,每一个View都可以注册一个event listener,例如:当View要处理用户触碰(touch)的事件时,就要向Android框架注册View.OnClickListener。另外还有BitMap等同于J2ME的Image。   

关注 64626

吉祥物 关注了标签 · 2018-02-05

关注 89330

吉祥物 关注了标签 · 2018-02-05

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 191073

吉祥物 关注了标签 · 2018-02-05

关注 65923

吉祥物 关注了标签 · 2018-02-05

区块链

区块链(英语:Blockchain 或 Block chain)是一种分布式数据库,起源自比特币。区块链是一串使用密码学方法相关联产生的数据块,每一个数据块中包含了一次比特币网络交易的信息,用于验证其信息的有效性(防伪)和生成下一个区块。该概念在中本聪的白皮书中提出,中本聪创造第一个区块,即“创世区块”。

区块链在网络上是公开的,可以在每一个离线比特币钱包数据中查询。比特币钱包的功能依赖于与区块链的确认,一次有效检验称为一次确认。通常一次交易要获得数个确认才能进行。轻量级比特币钱包使用在线确认,即不会下载区块链数据到设备存储中。

比特币的众多竞争币也使用同样的设计,只是在工作量证明上和算法上略有不同。如,采用权益证明和 SCrypt 算法等等。

关注 51192

吉祥物 发布了文章 · 2017-12-25

Vue源码解析(六)-vue-router

先上一段简单的demo,本文根据此demo进行解析

Vue.use(VueRouter)
const router = new VueRouter({
    routes: [
        { path: '/home', component: {template: '<div>home</div>'}}
    ]
})
new Vue({
    'el':'#app',
    router,
    template: `
    <div id="app">
      <h1>Basic</h1><router-view class="view"></router-view>
    </div>
    `
})

vue源码解析(五)中介绍过,Vue.use(VueRouter)其实主要调用了VueRouter.install(Vue)方法

function install (Vue) {
  //挂载全局的钩子函数到Vue,vue对象初始化会调用下面的函数
  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        // _routerRoot为当前vue对象
        this._routerRoot = this;
        // _router为new Vue传入的VueRouter对象
        this._router = this.$options.router;
        //调用 VueRouter.protoype.init,后面介绍
        this._router.init(this);
        // 设置响应式的_route,this._router.history.current为当前页面的路由信息
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
    }
  });
  //全局注册组件
  Vue.component('router-view', View);
  Vue.component('router-link', Link);
}
// 全局组件router-view的参数
var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref){}
}

install方法主要是挂载钩子函数和全局注册组件,全局注册的组件router-view的值如下,
clipboard.png

再看一下router = new VueRouter的过程

var VueRouter = function VueRouter (options) {
  this.options = options;
  //默认使用hash进行前端路由
  var mode = options.mode || 'hash';
  switch (mode) {
    case 'history':
      this.history = new HTML5History(this, options.base);
      break
    case 'hash':
      this.history = new HashHistory(this, options.base, this.fallback);
      break
  }
}
//  HashHistory 和 HTML5History都是继承History
var HashHistory = (function (History$$1) {
  function HashHistory (router, base, fallback) {
    History$$1.call(this, router, base);
  }

  if ( History$$1 ) HashHistory.__proto__ = History$$1;
  HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
  HashHistory.prototype.constructor = HashHistory;
  return HashHistory;
}(History));

var History = function History (router, base) {
  // VueRouter实例对象
  this.router = router;
  //base路径
  this.base = normalizeBase(base);
  //当前路由信息,此时是一个空值
  this.current = START;
};

new Vue的过程中会触发挂载的beforeCreate函数,主要是调用了this._router.init(this);
为了更清晰的解析整个流程,假定我们现在访问的页面路径是/home,并且是hash的方式进行路由

VueRouter.prototype.init = function init (app /* Vue component instance */) {
  var history = this.history;

  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation());
  } else if (history instanceof HashHistory) {
    var setupHashListener = function () {
      //监听浏览器地址的变更,并调用transitionTo“跳转”到新的路由页面
      history.setupListeners();
    };
    //初始化时调用一次transitionTo,根据当前浏览器地址栏里的path(/home)来激活对应的路由
    history.transitionTo(
      // 初始化值为“/home”
      history.getCurrentLocation(),
      setupHashListener    
    );
  }
};
HashHistory.prototype.setupListeners = function setupListeners () {
   //监听浏览器地址的变更
   window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', function () {
      //实现页面内容的变更,getHash()为变更后的hash路径
      this$1.transitionTo(getHash())
   }
}
//将页面转换到当前真实的路由
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
  var this$1 = this;
  // 根据location("/home")得到route对象{name: undefined, meta: {…}, path: "/home", hash: "", query: {…}, …}
  var route = this.router.match(location, this.current);
  //confirmTransition实现比较复杂,本文不做介绍,主要会执行下面的回调函数
  this.confirmTransition(route, function () {
    //将histoty.current值更新为route
    this$1.updateRoute(route);
    //执行onComplete(setupHashListener)
    onComplete && onComplete(route);
    //更新浏览器url地址
    this$1.ensureURL();
  }
};

function match (){
  遍历路由配置(本文只有一项配置{ path: '/home', component: {template: '<div>home</div>'}})
  for (var i = 0; i < pathList.length; i++) {
    var path = pathList[i];
    var record$1 = pathMap[path];
    //判断当前路径是否有匹配的路由配置
    if (matchRoute(record$1.regex, location.path, location.params)) {
      return _createRoute(record$1, location, redirectedFrom)
    }
  }
  // no match
  return _createRoute(null, location)
}

function createRoute (record){
  var route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query: query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery$$1),
    //当前路径匹配的路由配置
    matched: record ? formatMatch(record) : []
  };
  return Object.freeze(route)
}

根据上面的代码逻辑可以分析得出,vue对象初始化时会挂载属性vm._router(记录了整个应用的路由配置信息)和vm._route(记录了当前的路由信息)。vm._route是响应式的,当浏览器路由改变时,vm._route的值也会相应的改变
vm._route的作用是清楚了,但页面内容的变化是怎么实现的呢?下面再介绍下router-view的作用。
Vue源码解析(四)-components组件介绍过,vue初始化时根据template函数生成render函数,本文render函数会调用vm._c('router-view'),_createElement判断router-view是注册过的组件,因此以组件的方式生成vnode,但是router-view生成vnode的过程与Vue源码解析(四)中的方法又有区别

function _createElement(){
    //本例tag=‘router-view’,‘router-view’在components属性中注册过,因此以组件的方式生成vnode
    if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      //Ctor是router-view的构造函数VueComponent(Vue.component('router-view', View)注册)
      vnode = createComponent(Ctor, data, context, children, tag);
    }
}

function createComponent (Ctor){
    //Ctor 此时已经是构造函数 , 不需要再调用Vue.extend生成
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    // router-view是functional component(见上文图中view的option的值),与用户自定义的component的vnode生成方法有区别
    if (isTrue(Ctor.options.functional)) {
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    //用户自定义component的vnode构造方法
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children:         children } );
}

function createFunctionalComponent (Ctor){
   var options = Ctor.options;
   //主要是调用View.render方法,前文提到过
   var vnode = options.render.call(null, renderContext._c, renderContext);
   return vnode;
}

var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    //Vue实例化对象vm
    var parent = ref.parent;
    // vm._route
    var route = parent.$route;

    var depth = 0;
    //上文提到的createRoute中生成的路由匹配信息
    var matched = route.matched[depth];
    
    // _createElement方法
    var h = parent.$createElement;
    // render empty node if no matched route
    if (!matched) {
      return h()
    }
    // 本文component为{template: "<div>home</div>", _Ctor: {…}, inject: {…}}
    var component = matched.components[name];
    
    //重新调用_createElement,这次是以常规方式生成vnode,后续vnode将渲染成template中的内容
    return h(component, data, children)
  }
};
查看原文

赞 2 收藏 6 评论 0

吉祥物 赞了文章 · 2017-11-21

入门 Webpack,看这篇就够了

2018年8月25日更新,目前 webpack 已经更新值 4.17.1 ,本文所用到的各种库或多或少有些过时,跟着代码操作下来可能会遇到各种问题,不过 webpack 的主体思想没变,所以还是希望本文对新学 webpack 的你,有所帮助。此外用基于 webpack 4.17.1 写了一个简单的demo,如果遇到啥问题,可以参考,之后应该会逐步来完善这个demo,如果有啥通用的想实现的功能,也可以在里面提 issue。

2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本文涉及到的所有代码的示例,如果你在学习过程中出错了,可点击此处参考(有些过时了,不要再 fork 了)

写在前面的话

阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前的我一样,对很多选项存在着疑惑,那花一段时间慢慢阅读本文,你的疑惑一定一个一个都会消失;如果你以前没怎么接触过Webpack,而你又你对webpack感兴趣,那么动手跟着本文中那个贯穿始终的例子写一次,写完以后你会发现你已明明白白的走进了Webpack的大门。
// 一个常见的`webpack`配置文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地服务器所加载的页面所在的目录
            historyApiFallback: true, //不跳转
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};

什么是WebPack,为什么要使用它?

为什要使用WebPack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

什么是Webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Grunt和Gulp的工作流程

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
Webpack工作方式

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

开始使用Webpack

初步了解了Webpack工作方式后,我们一步步的开始学习使用Webpack。

安装

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack sample project),在终端中转到该文件夹后执行下述指令就可以完成安装。

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

正式使用Webpack前的准备

  1. 在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件
npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。

  1. package.json文件已经就绪,我们在本项目中安装Webpack作为依赖包
// 安装Webpack
npm install --save-dev webpack
  1. 回到之前的空文件夹,并在里面创建两个文件夹,app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:
  • index.html --放在public文件夹中;
  • Greeter.js-- 放在app文件夹中;
  • main.js-- 放在app文件夹中;

此时项目结构如下图所示
项目结构

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script data-original="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

正式使用Webpack

webpack可以在终端中使用,在基本的使用方法如下:

# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}

指定入口文件后,webpack将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules中的地址,继续上面的例子,在终端中输入如下命令

# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js

结果如下

使用命令行打包

可以看出webpack同时编译了main.jsGreeter,js,现在打开index.html,可以看到如下结果
htmlResult1

有没有很激动,已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

通过配置文件来使用Webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}
:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:

配合配置文件进行打包

又学会了一种使用Webpack的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack(非全局安装需使用node_modules/.bin/webpack)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build,我们在命令行中输入npm start试试,输出结果如下:

使用npm start 打包代码

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项配置结果
source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}
cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

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

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

devserver的配置选项功能描述
contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
port设置默认监听端口,如果省略,默认为”8080“
inline设置为true,当源文件改变时会自动刷新页面
historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open"
  },

在终端中输入npm run server即可在本地的8080端口查看结果

开启本地服务器

Loaders

鼎鼎大名的Loaders登场了!

Loaderswebpack提供的最激动人心的功能之一了。通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:

  • test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
  • loader:loader的名称(必须)
  • include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
  • query:为loaders提供额外的设置选项(可选)

不过在配置loader之前,我们把Greeter.js里的问候消息放在一个单独的JSON文件里,并通过合适的配置使Greeter.js可以读取该JSON文件的值,各文件修改后的代码如下:

在app文件夹中创建带有问候信息的JSON文件(命名为config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
由于webpack3.*/webpack2.*已经内置可处理JSON文件,这里我们无需再添加webpack1.*需要的json-loader。在看如何具体使用loader之前我们先看看Babel是什么?

Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

localhost:8080

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那那些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
请注意这里对同一个文件引入多个loader的方法。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));
通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

应用了css module后的样式

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')
    ],
};

通过这个插件,打包后的JS文件显示如下

版权所有,翻版必究

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

安装

npm install --save-dev html-webpack-plugin

这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:

  1. 移除public文件夹,利用此插件,index.html文件会自动生成,此外CSS已经通过前面的操作打包到JS中了。
  2. 在app目录下,创建一个index.tmpl.html文件模板,这个模板包含title等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html中的模板源代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3.更新webpack的配置文件,方法同上,新建一个build文件夹用来存放最终的输出文件

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        })
    ],
};

再次执行npm start你会发现,build文件夹下面生成了bundle.jsindex.html

build文件夹

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};
   

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}
注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".谢谢评论区简友提醒。

优化插件

webpack提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack社区,可以通过npm安装,通过以下插件可以完成产品发布阶段所需的功能

  • OccurenceOrderPlugin :为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID
  • UglifyJsPlugin:压缩JS代码;
  • ExtractTextPlugin:分离CSS和JS文件

我们继续用例子来看看如何添加它们,OccurenceOrder 和 UglifyJS plugins 都是内置插件,你需要做的只是安装其它非内置插件

npm install --save-dev extract-text-webpack-plugin

在配置文件的plugins后引用它们

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'none',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ],
};

此时执行npm run build可以看见代码是被压缩后的

压缩后的代码

缓存

缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)

webpack可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash])到输出文件名前

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
..
    output: {
        path: __dirname + "/build",
        filename: "bundle-[hash].js"
    },
   ...
};

现在用户会有合理的缓存了。

带hash值的js名

去除build文件中的残余文件

添加了hash之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多,因此这里介绍另外一个很好用的插件clean-webpack-plugin

安装
cnpm install clean-webpack-plugin --save-dev

使用

引入clean-webpack-plugin插件后在配置文件的plugins中做相应配置即可:

const CleanWebpackPlugin = require("clean-webpack-plugin");
  plugins: [
    ...// 这里是之前配置的其它各种插件
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
  ]

关于clean-webpack-plugin的详细使用可参考这里

总结

其实这是一年前的文章了,趁周末重新运行和修改了一下,现在所有的代码都可以正常运行,所用webpack基于最新的webpack3.5.3。希望依旧能对你有帮助。

这是一篇好长的文章,谢谢你的耐心,能仔细看到了这里,大概半个月前我第一次自己一步步配置项目所需的Webpack后就一直想写一篇笔记做总结,几次动笔都不能让自己满意,总觉得写不清楚。其实关于Webpack本文讲述得仍不完全,不过相信你看完后已经进入Webpack的大门,能够更好的探索其它的关于Webpack的知识了。

欢迎大家在文后发表自己的观点讨论。

更新说明

2017-12-11更新,修改css module部分代码及示例图片,css module真的非常好用,希望大家都能用上。

2017年9月18日更新,添加了一个使用webpack配置多页应用的demo,可以点击此处查看

2017年8月13日更新,本文依据webpack3.5.3将文章涉及代码完全重写,所有代码都在Mac上正常运行过。希望依旧对你学习webpack有帮助。

2017年8月16号更新:
最近在Gitchat上将发起了一场关于webpack的分享,目的在于一起花最短的时间理解和学会webpack,感兴趣的童鞋可以微信扫描注册哈。
webpack从入门到工程实践

查看原文

赞 1689 收藏 2497 评论 295

吉祥物 赞了文章 · 2017-11-21

入门 Webpack,看这篇就够了

2018年8月25日更新,目前 webpack 已经更新值 4.17.1 ,本文所用到的各种库或多或少有些过时,跟着代码操作下来可能会遇到各种问题,不过 webpack 的主体思想没变,所以还是希望本文对新学 webpack 的你,有所帮助。此外用基于 webpack 4.17.1 写了一个简单的demo,如果遇到啥问题,可以参考,之后应该会逐步来完善这个demo,如果有啥通用的想实现的功能,也可以在里面提 issue。

2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本文涉及到的所有代码的示例,如果你在学习过程中出错了,可点击此处参考(有些过时了,不要再 fork 了)

写在前面的话

阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前的我一样,对很多选项存在着疑惑,那花一段时间慢慢阅读本文,你的疑惑一定一个一个都会消失;如果你以前没怎么接触过Webpack,而你又你对webpack感兴趣,那么动手跟着本文中那个贯穿始终的例子写一次,写完以后你会发现你已明明白白的走进了Webpack的大门。
// 一个常见的`webpack`配置文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地服务器所加载的页面所在的目录
            historyApiFallback: true, //不跳转
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};

什么是WebPack,为什么要使用它?

为什要使用WebPack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

什么是Webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Grunt和Gulp的工作流程

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
Webpack工作方式

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

开始使用Webpack

初步了解了Webpack工作方式后,我们一步步的开始学习使用Webpack。

安装

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack sample project),在终端中转到该文件夹后执行下述指令就可以完成安装。

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

正式使用Webpack前的准备

  1. 在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件
npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。

  1. package.json文件已经就绪,我们在本项目中安装Webpack作为依赖包
// 安装Webpack
npm install --save-dev webpack
  1. 回到之前的空文件夹,并在里面创建两个文件夹,app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:
  • index.html --放在public文件夹中;
  • Greeter.js-- 放在app文件夹中;
  • main.js-- 放在app文件夹中;

此时项目结构如下图所示
项目结构

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script data-original="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

正式使用Webpack

webpack可以在终端中使用,在基本的使用方法如下:

# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}

指定入口文件后,webpack将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules中的地址,继续上面的例子,在终端中输入如下命令

# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js

结果如下

使用命令行打包

可以看出webpack同时编译了main.jsGreeter,js,现在打开index.html,可以看到如下结果
htmlResult1

有没有很激动,已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

通过配置文件来使用Webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}
:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:

配合配置文件进行打包

又学会了一种使用Webpack的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack(非全局安装需使用node_modules/.bin/webpack)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build,我们在命令行中输入npm start试试,输出结果如下:

使用npm start 打包代码

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项配置结果
source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}
cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

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

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

devserver的配置选项功能描述
contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
port设置默认监听端口,如果省略,默认为”8080“
inline设置为true,当源文件改变时会自动刷新页面
historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open"
  },

在终端中输入npm run server即可在本地的8080端口查看结果

开启本地服务器

Loaders

鼎鼎大名的Loaders登场了!

Loaderswebpack提供的最激动人心的功能之一了。通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:

  • test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
  • loader:loader的名称(必须)
  • include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
  • query:为loaders提供额外的设置选项(可选)

不过在配置loader之前,我们把Greeter.js里的问候消息放在一个单独的JSON文件里,并通过合适的配置使Greeter.js可以读取该JSON文件的值,各文件修改后的代码如下:

在app文件夹中创建带有问候信息的JSON文件(命名为config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
由于webpack3.*/webpack2.*已经内置可处理JSON文件,这里我们无需再添加webpack1.*需要的json-loader。在看如何具体使用loader之前我们先看看Babel是什么?

Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

localhost:8080

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那那些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
请注意这里对同一个文件引入多个loader的方法。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));
通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

应用了css module后的样式

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')
    ],
};

通过这个插件,打包后的JS文件显示如下

版权所有,翻版必究

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

安装

npm install --save-dev html-webpack-plugin

这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:

  1. 移除public文件夹,利用此插件,index.html文件会自动生成,此外CSS已经通过前面的操作打包到JS中了。
  2. 在app目录下,创建一个index.tmpl.html文件模板,这个模板包含title等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html中的模板源代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3.更新webpack的配置文件,方法同上,新建一个build文件夹用来存放最终的输出文件

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        })
    ],
};

再次执行npm start你会发现,build文件夹下面生成了bundle.jsindex.html

build文件夹

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};
   

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}
注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".谢谢评论区简友提醒。

优化插件

webpack提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack社区,可以通过npm安装,通过以下插件可以完成产品发布阶段所需的功能

  • OccurenceOrderPlugin :为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID
  • UglifyJsPlugin:压缩JS代码;
  • ExtractTextPlugin:分离CSS和JS文件

我们继续用例子来看看如何添加它们,OccurenceOrder 和 UglifyJS plugins 都是内置插件,你需要做的只是安装其它非内置插件

npm install --save-dev extract-text-webpack-plugin

在配置文件的plugins后引用它们

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'none',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ],
};

此时执行npm run build可以看见代码是被压缩后的

压缩后的代码

缓存

缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)

webpack可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash])到输出文件名前

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
..
    output: {
        path: __dirname + "/build",
        filename: "bundle-[hash].js"
    },
   ...
};

现在用户会有合理的缓存了。

带hash值的js名

去除build文件中的残余文件

添加了hash之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多,因此这里介绍另外一个很好用的插件clean-webpack-plugin

安装
cnpm install clean-webpack-plugin --save-dev

使用

引入clean-webpack-plugin插件后在配置文件的plugins中做相应配置即可:

const CleanWebpackPlugin = require("clean-webpack-plugin");
  plugins: [
    ...// 这里是之前配置的其它各种插件
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
  ]

关于clean-webpack-plugin的详细使用可参考这里

总结

其实这是一年前的文章了,趁周末重新运行和修改了一下,现在所有的代码都可以正常运行,所用webpack基于最新的webpack3.5.3。希望依旧能对你有帮助。

这是一篇好长的文章,谢谢你的耐心,能仔细看到了这里,大概半个月前我第一次自己一步步配置项目所需的Webpack后就一直想写一篇笔记做总结,几次动笔都不能让自己满意,总觉得写不清楚。其实关于Webpack本文讲述得仍不完全,不过相信你看完后已经进入Webpack的大门,能够更好的探索其它的关于Webpack的知识了。

欢迎大家在文后发表自己的观点讨论。

更新说明

2017-12-11更新,修改css module部分代码及示例图片,css module真的非常好用,希望大家都能用上。

2017年9月18日更新,添加了一个使用webpack配置多页应用的demo,可以点击此处查看

2017年8月13日更新,本文依据webpack3.5.3将文章涉及代码完全重写,所有代码都在Mac上正常运行过。希望依旧对你学习webpack有帮助。

2017年8月16号更新:
最近在Gitchat上将发起了一场关于webpack的分享,目的在于一起花最短的时间理解和学会webpack,感兴趣的童鞋可以微信扫描注册哈。
webpack从入门到工程实践

查看原文

赞 1689 收藏 2497 评论 295

吉祥物 发布了文章 · 2017-11-20

Vue源码解析(五)-vuex

Vue 组件中获得 Vuex 状态

按官网说法:“由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态”,本文结合下面的demo进行分析:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const vueStore = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        }
    }
})
let vm = new Vue({
    el: '#app',
    store: vueStore,
    template: '<div>{{count}}</div>',
    computed: {
        count(){
            return this.$store.state.count
        }
    }
})

下面主要分析为什么可以通过this.$store直接访问vueStore对象。先看看Vue.use方法

  Vue.use = function (plugin) {
    //插件只能注册一次
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    
    //拼接参数,将Vue作为第一个参数
    // additional parameters
    var args = toArray(arguments, 1);
    args.unshift(this);
    
    //调用plugin.install或plugin方法
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };

再看Vuex源码,Vuex其实是下面这个对象

{
  Store: Store,
  install: install,
  mapState: mapState,
  mapMutations: mapMutations,
  mapGetters: mapGetters,
  mapActions: mapActions,
  createNamespacedHelpers: createNamespacedHelpers
}

因此Vue.use(Vuex)其实想到于Vuex.install()

let Vue; // bind on install
function install (_Vue) {
  Vue = _Vue;
  applyMixin(Vue);
}

var applyMixin = function (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  //Vue2.0处理方法
  if (version >= 2) {
    //将vuexInit方法注册到beforeCreate钩子上,当Vue的生命周期走到callHook(vm, 'beforeCreate');时触发vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit });
  } 

  // Vuex init hook, injected into each instances init hooks list.
  function vuexInit () {
    //this就是当前正在被new的Vue对象
    var options = this.$options;
    //将options.store(本例demo中的vueStore)赋值给this.$store,因此可以通过this.$store访问vueStore对象
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store;
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store;
    }
  }
}

mapState

通过computed属性可以获取到状态值,但是每一个属性都要通过this.$store.state访问不是很方便。vue 提供了 mapState 函数,它把state直接映射到我们的组件中,先给出mapState的使用demo

let mapState = Vuex.mapState
let vm = new Vue({
  el: '#app',
  store: vueStore,
  template:
  `<div>
    <my-component></my-component>
  </div>`,
  components:{
      'my-component':{
          template: '<div>{{count}}-{{num}}</div>',
          computed: mapState({
              // {{count}}值为this.$store.state.count
              count: state => state.count,
              // {{num}}值为this.$store.state.num,
              num: 'num'         
          })
      }
  }
})

让我们看看mapState的源码实现

//normalizeNamespace规范当前vuex的命名空间。默认情况下,vuex内部的 action、mutation 和 getter 是注册在全局命名空间的,本例也是,因此namespace=‘’
var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  //规范states参数,将states转换为map格式,因此mapState支持多种写法
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;
    res[key] = function mappedState () {
      var state = this.$store.state;
      var getters = this.$store.getters;
      if (namespace) {
        var module = getModuleByNamespace(this.$store, 'mapState', namespace);
        if (!module) {
          return
        }
        state = module.context.state;
        getters = module.context.getters;
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  //mapState其实就是提供简洁的写法将this.$store.state[val]赋值给coputed属性
  return res
});

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(function (key) { return ({ key: key, val: key }); })
    : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); })
}
//规范当前vuex的命名空间
function normalizeNamespace (fn) {
  return function (namespace, map) {
    if (typeof namespace !== 'string') {
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/';
    }
    return fn(namespace, map)
  }
}

store响应式原理

Vue源码解析(二)中介绍过data的响应式原理:
1、对data进行observe,针对data属性调用Object.defineProperty设置getter和setter,同时绑定一个dep对象
2、new Watcher(vm, updateComponent, noop)监听整个dom的变化
3、watcher初始化时调用updateComponent,updateComponent调用render函数更新dom(此时还会将该watcher对象赋值给全局对象Dep.target,进行依赖收集)
4、在watcher对象依赖收集期间,render函数访问data中的属性(如本例的data.message),触发data.message的getter方法,在getter方法中会将data.message绑定的dep对象和wathcer对象建立对应关系(互相加入到对方维护的队列属性上)
5、后续data属性的值变化时dep对象会通知所有依赖此data属性的watcher对象调用updateComponent方法更新视图
store响应式的原理也是类似的,new Vuex.Store的过程也会对state进行observe

var Store = function Store (options) {
    var state = options.state
    //为了实现state的响应性new一个vue对象
    // initialize the store vm, which is responsible for the reactivity
    resetStoreVM(this, state);
}
function resetStoreVM (store, state, hot) {
  //new一个vue对象对data(值为store.state)进行监听
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
}

后续实现和上面的data响应式相同

mutation

new Vuex.Store的过程中会将mutation注册到store._mutations上

function registerMutation (store, type, handler, local) {
  var entry = store._mutations[type] || (store._mutations[type] = []);
  //封装mutation方法并push到store._mutations[type]上
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload);
  });
}

当执行commit方法时就会执行store._mutations上对应的方法

Store.prototype.commit = function commit (type, payload) {
    var entry = this._mutations[type];
    entry.forEach(function commitIterator (handler) {
        handler(payload);
    });
}

actions

const vueStore = new Vuex.Store({
    state: {
        count: 1,
    },
    mutations: {
        increment (state,payload) {
            state.count+=payload
        }
    },
    actions: {
        increment (context,payload) {
            setTimeout(function () {
                context.commit('increment',payload)
            },1000)
        }
    }
})
vueStore.dispatch('increment',10)

和mutation一样,new Vuex.Store也会将action注册到store._actions上,然后通过dispatch调用

function registerAction (store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  //包装action方法,传入store对象的commit方法和state等等
  entry.push(function wrappedActionHandler (payload, cb) {
    var res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb);
    //action的返回不是promise会返回Promise.resolve(res)
    if (!isPromise(res)) {
        res = Promise.resolve(res);
    }
    return res
  });
}

Store.prototype.dispatch = function dispatch (_type, _payload) {
  var entry = this._actions[type];
  return entry.length > 1
    ? Promise.all(entry.map(function (handler) { return handler(payload); }))
    : entry[0](payload)
};

看到action和mutation的源码实现,你不禁要问了,这不是基本一样的吗,那干嘛还要多此一举?
vuex官网的解释:在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务。
知乎上有个问题“vuex中为什么把把异步操作封装在action,把同步操作放在mutations?“,vue的作者尤雨溪的解释:事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。
同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。
我个人的理解这是vuex的使用规范问题,mutation中使用异步也不会有大问题,但是按规范开发能让项目结构更清晰,调试更方便,下图是用vue devtool调试的vuex官方例子(https://github.com/vuejs/vuex...),mutation的触发时间线一目了然

clipboard.png

查看原文

赞 2 收藏 7 评论 1

吉祥物 赞了文章 · 2017-11-15

微信小程序[第十二篇] -- 上传视频

通过上一篇的学习,我们可以成功将宝宝的照片传到指定相册了,但是可爱的宝宝岂能只有照片,小视频必须同步跟上,莫问题!咱这篇就来一个视频上传的实现。

俺家小核桃镇贴。

哎呀妈,真好喝。

服务端

其实对于yii2程序而言,如果不考虑转码、获取帧等事情,上传图片和上传一个视频差别并不大,我们还是先来构造接口实现。

考虑到在我们的程序中视频照片都是存储在photo_item表中,通过type字段加以区分,因此我们仍然使用 /xcx/PhotoItemController这个控制器,但是create动作已经被上传图片使用了,无奈我们需要自定义一个action,就叫它add吧。

在yii2的restful中自定义路由方法

首先在web.php里的urlManager进行设置

//web.php
[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'xcx/photo-item',
    'extraPatterns'=>[
        'POST add'=>'add',
    ]
],

设置后我们来写实际的action,这里先说明一点就是上传视频和上传图片思路一样,只是因为视频体积偏大,每次我们设定上传一个。

思路:新建photo记录返回小程序后上传具体文件。

对于新建photo记录不在说明,请参考上一篇文章,详细代码请大家参考github本项目仓库。

// PhotoItemController.add
public function actionAdd(){
    $video = UploadedFile::getInstanceByName('file');
    $body = Yii::$app->getRequest()->getBodyParams();

    if($video == false){
        throw new Exception('文件上传失败');
    }

    $ext = $video->getExtension();
    $path_result = N8Folder::createItemPath('video',$ext);
    $video->saveAs($path_result['save_path']);

    $modelClass = $this->modelClass;
    $model = new $modelClass();
    $model->photo_id = $body['photo_id'];
    $model->album_id = $body['album_id'];
    $model->path = $path_result['web_path'];
    $model->type = 2;
    $model->save();

    return $model;
}

ok,服务端暂且到这里,接下来开始我们小程序部署。

小程序

作为一套入门级实战教程,我们的重点在于小程序组件和api的讲解,因此对于上传视频我仍然采用和上传照片完全一致的思路,看下图。

只不过我传了视频

在这里我们要做如下事情

  • 选择或拍摄视频并判断是否符合标准

  • 上传视频

第一步 生成photo记录

这一步和上一篇相同,我把代码贴过来自行看下,我们的重点不在这里。

formSubmit: function (e) {
    var that = this;
    var desc = e.detail.value.desc;
    if (that.data.albumIndex < 0) {
        wx.showToast({
            title: '请选择相册',
        })
        return;
    }
    var albumId = that.data.albums[that.data.albumIndex].id;

    wx.showLoading({ title: '提交中' });
    wx.request({
        method: 'POST',
        data: {
            album_id: albumId,
            description: desc
        },
        url: 'http://xgh.local.com/xcx/photos',
        header: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        success: function (res) {

        }
    });
}

当我们通过上面方法成功新建photo记录后,小程序可以拿到此刻photo的id,接下来我们上传视频。

在上传之前我们研究下如何选择和拍摄视频。

在视频这块微信小程序提供了2个API

  • wx.chooseVideo 选择视频

  • wx.saveVideoToPhotosAlbum 保持视频到相册

在这里我们使用第一个。

先贴出来官方文档 https://mp.weixin.qq.com/debu...

我们首先在data里设置了一个video字段用来表示选择视频的路径,然后在wxml里设置了一个text点击后触发函数addVideo来选择视频

// addVideo
addVideo: function () {
    var that = this
    wx.chooseVideo({
        sourceType: ['album', 'camera'],
        maxDuration: 60,
        camera: 'back',
        success: function (res) {
            that.setData({
                video: res.tempFilePath,
        size: (res.size / (1024 * 1024)).toFixed(2)
            })
        }
    })
},

通过sourceType可以设置 拍摄和选择已经存在的视频两个属性,maxDuration来限制拍摄时长。

关于chooseVideo成功后的返回还是蛮多的,一共5项
tempFilePath、duration、size、width、height。

总之我们通过上面的方法将video设置了选择视频的路径,为了体验好点,在视图wxml内我添加了如下代码

<view class="video-box" wx:if="{{video}}">
    <video class="video" data-original="{{video}}"></video>
    <view style="text-align:center;color:green;font-size:14px;padding-top:7px;">当前视频大小为{{size}}M</view>
</view>

通过对video的判断来决定是否显示小视频预览,在这里我们用了小程序的video组件,以后会详细说明。

我们来看一下成果。

alt

效果还不错,现在流程顺了,我们需要回过头来再去处理上面的formSubmit函数,有两件事情

  • 点击提交前对视频的判断

  • 生成photo记录后对视频的上传

判断

我们判断两个参数,一个是必须选择,一个是尺寸不能大于2M

formSubmit: function (e) {
    var that = this;
    var desc = e.detail.value.desc;
    if (that.data.albumIndex < 0) {
        wx.showModal({
            title: '小乖猴助手',
            content: '请选择相册'
        })
        return;
    }
    var albumId = that.data.albums[that.data.albumIndex].id;

    if (that.data.video == false) {
        wx.showModal({
            title: '小乖猴助手',
            content: '请录制或选择一个小视频'
        })
        return false;
    }

    if (that.data.size > 1024 * 1024 * 2) {
        wx.showModal({
            title: '小乖猴助手',
            content: '很抱歉,视频最大允许2M,当前为' + (that.data.size / (1024 * 1024)).toFixed(2) + 'M'
        })
        return false;
    }

    .......
}

没什么难度,if而已。

接下来处理上传

当获取了photo记录后,将视频上传上去,知道哪个函数了吧,对,就是uploadFile,看代码

formSubmit: function (e) {
    ....
    wx.request({
        method: 'POST',
        data: {
            album_id: albumId,
            description: desc
        },
        url: 'http://xgh.local.com/xcx/photos',
        header: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        success: function (res) {
            if (res.statusCode == 201) {
                var photo = res.data;
                wx.showLoading({ title: '视频上传中' });
                wx.uploadFile({
                    url: 'http://xgh.local.com/xcx/photo-items/add',
                    method: 'POST',
                    filePath: that.data.video,
                    header: {
                        'content-type': 'multipart/form-data'
                    },
                    name: 'file',
                    formData: {
                        photo_id: photo.id,
                        album_id: photo.album_id
                    },
                    success: function (r) {
                        wx.hideLoading();
                        wx.showModal({
                            title: '小乖猴助手',
                            content: '上传成功',
                        })
                    },
                    fail: function (r) {

                    }
                })
            }
        }
    });
}

ok,成功了

alt

总结

就这样,我们轻松实现了视频上传功能,有上一篇做基础我想很多方法你已经很熟悉了吧,详细代码稍后会放到github上,大家可以拉下来看下。

https://github.com/abei2017/xgh

查看原文

赞 14 收藏 19 评论 2

吉祥物 发布了文章 · 2017-11-14

Vue源码解析(四)-components组件

组件初始化渲染

本文以局部组件的注册方式介绍组件的初始化渲染,demo如下

new Vue({
  el: '#app',
  template: 
  `<div>
     <div>father component!</div>
     <my-component></my-component>
  </div>`,
  components:{
      'my-component': {
          template: '<div>children component!</div>'
      }
  }
})

1、Vue源码解析(一)-模版渲染介绍过,vue初始化时根据template函数生成render函数,本文render函数会调用vm._c('my-component'),_createElement判断'my-component是注册过的组件,因此以组件的方式生成vnode

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

//template生成的render函数vm._render会调用vm._c('my-component')
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

function _createElement(){
    //本例tag=‘my-component’,‘my-component’在components属性中注册过,因此以组件的方式生成vnode
    if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag);
    }
}

//本例Ctor参数{template: '<div>children component1!</div>'}
function createComponent (Ctor){
    //Vue构造函数
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
      //生成VuComponent构造函数
      //此处相当于Ctor = Vue.extend({template: '<div>children component1!</div>'}), Vue.extend后面有介绍;
      Ctor = baseCtor.extend(Ctor);
    }
    //将componentVNodeHooks上的方法挂载到vnode上,组件初次渲染会用到componentVNodeHooks.init
    var data = {}
    mergeHooks(data);

    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children:         children } );
}
//component初始化和更新的方法,此处先介绍init
var componentVNodeHooks = {
    init(vnode){
        //根据Vnode生成VueComponent实例
        var child = vnode.componentInstance = createComponentInstanceForVnode(vnode);
        //将VueComponent实例挂载到dom节点上,本文是挂载到<my-component></my-component>节点
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
} 

2、调用vm._update将vnode渲染为浏览器dom,主要方法是遍历vnode的所有节点,根据节点类型调用相关的方法进行解析,本文主要介绍components的解析方法createComponent:根据vnode生成VueComponent(继承Vue)对象,
调用Vue.prototype.$mount方法渲染dom

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    //组件vnode节点渲染方法
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    //Vue源码解析(一)中介绍过普通vnode节点渲染步骤 
    //根据vnode节点生成浏览器Element对象
    vnode.elm = nodeOps.createElement(tag, vnode);
    var children = vnode.children;
    //递归将vnode子节点生成Element对象
    createChildren(vnode, children, insertedVnodeQueue);
    //将生成的vnode.elm插入到浏览器的父节点当中
    insert(parentElm, vnode.elm, refElm);
}

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      //i就是上面的componentVNodeHooks.init方法
      i(vnode, false /* hydrating */, parentElm, refElm);
    }
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}  

function createComponentInstanceForVnode (){
    var options = {
      _isComponent: true,
      parent: parent,
      propsData: vnodeComponentOptions.propsData,
      _componentTag: vnodeComponentOptions.tag,
      _parentVnode: vnode,
      _parentListeners: vnodeComponentOptions.listeners,
      _renderChildren: vnodeComponentOptions.children,
      _parentElm: parentElm || null,
      _refElm: refElm || null
    };
    //上面提到的VueComponent构造函数Ctor,相当于new VueComponent(options)
    return new vnode.ComponentOptions.Ctor(options)
}

3 、new VueComponent和new Vue的过程类似,本文就不再做介绍

全局注册组件

上文提到过 Vue.extend方法(继承Vue生成VueComponent构造函数)此处单独介绍一下

  Vue.extend = function (extendOptions) {
    var Super = this;
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    //经典的继承写法
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    return Sub
  };

通过Vue.component也可以全局注册组件,不需要每次new vue的时候单独注册,demo如下:

var globalComponent = Vue.extend({
    name: 'global-component',
    template: '<div>global component!</div>'
});
Vue.component('global-component', globalComponent);
new Vue({
    el: '#app',
    template:
    `<div>
         <global-component></global-component>
         <my-component></my-component>
    </div>`,
    components:{
        'my-component': {
            template: '<div>children component!</div>'
        }
    }
})

vue.js初始化时会先调用一次initGlobalAPI(Vue),给Vue构造函数挂载上一些全局的api,其中又会调用到
initAssetRegisters(Vue),其中定义了Vue.component方法,具体看下其实现

  var ASSET_TYPES = [
      'component',
      'directive',
      'filter'
  ];
  //循环注册ASSET_TYPES中的全局方法
  ASSET_TYPES.forEach(function (type) {
    Vue[type] = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id);
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        //全局的组件、指令和过滤器都挂载在Vue.options上
        this.options[type + 's'][id] = definition;
        return definition
      }
    };
  });

  Vue.prototype._init = function (options) {
      vue初始化时将options参数和Vue.options组装为vm.$options
      vm.$options = mergeOptions(
        //Vue.options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
  }

本例组装后的vm.$option.components值如下,proto中前3个属性是内置全局组件
clipboard.png

组件通信

prop

在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息.先看看prop是怎么工作的。demo如下:

new Vue({
  el: '#app',
  template:
  `<div>
     <div>father component!</div>
     <my-component message="hello!"></my-component>
  </div>`,
  components:{
      'my-component':{
          props: ['message'],
          template: '<span>{{ message }}</span>'
      }
  }
})

1、template生成的render函数包含:_c('my-component',{attrs:{"message":"hello!"}})]
2、render => vnode => VueComponent,上文提到的VueComponent的构造函数调用了Vue.prototype._init,并且入参option.propsData:{message: "hello!"}
3、双向绑定中介绍过Vue初始化时会对data中的所有属性调用defineReactive方法,对data属性进行监听;
VueComponent对propsData也是类似的处理方法,initProps后propsData中的属性和data一样也是响应式的,propsData变化,相应的view也会发生改变

function initProps (vm, propsOptions) {
    for (var key in propsOptions){
        //defineReactive参照Vue源码解析(二)
        defineReactive(props, key, value);
        //将propsData代理到vm上,通过vm[key]访问propsData[key]
        proxy(vm, "_props", key);
    }
}

4、propsData是响应式的了,但更常用的是动态props,按官网说法:“我们可以用v-bind来动态地将prop绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件”,那么vue是如何将data的变化传到给自组件的呢,先看demo

var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component :message="parentMsg"></my-component>
  </div>`,
  data(){
    return{
      parentMsg:'hello'
    }
  },
  components:{
      'my-component':{
          props: ['message'],
          template: '<span>{{ message }}</span>'
      }
  }
})
vm.parentMsg = 'hello world'

5、双向绑定中介绍过vm.parentMsg变化,会触发dep.notify(),通知watcher调用updateComponent;
又回到了updateComponent,之后的dom更新过程可以参考上文的组件渲染逻辑,只是propsData值已经是最新的vm.parentMsg的值了

//又见到了。。所有的dom初始化或更新都会用到
updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

Vue.prototype._update = function (vnode, hydrating) {
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    //Vue源码解析(一)介绍过dom初始化渲染的源码
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // 本文介绍dom更新的方法
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}

Vue源码解析(一)介绍过vm.__patch__中dom初始化渲染的逻辑,本文再简单介绍下vm.__patch关于component更新的逻辑:

function patchVnode (oldVnode, vnode){
    //上文介绍过componentVNodeHooks.init,此处i=componentVNodeHooks.prepatch
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode);
    }
}
var componentVNodeHooks = {
  init(){},
  prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    //更新组件
    updateChildComponent(
      child,
      //此时的propsData已经是最新的vm.parentMsg
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  }
}

function updateChildComponent (vm, propsData){
    //将vm._props[key]设置为新的propsData[key]值,从而触发view层的更新
    var props = vm._props;
    props[key] = validateProp(key, vm.$options.props, propsData, vm);
}

emit

子组件向父组件通信需要用到emit,先给出demo

var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component @rf="receiveFn"></my-component>
  </div>`,
  methods:{
    receiveFn(msg){
      console.log(msg)
    }
  },
  components:{
      'my-component':{
          template: '<div>child</div>',
          mounted(){
            this.$emit('rf','hello')
          }
      }
  }
})

本例中子组件mount结束会触发callHook(vm, 'mounted'),调用this.$emit('rf','hello'),从而调用父组件的receiveFn方法

Vue.prototype.$emit = function (event) {
    //本例cbs=vm._events['rf'] = receiveFn,vm._events涉及v-on指令解析,以后有机会详细介绍下
    var cbs = vm._events[event];
    //截取第一位之后的参数
    var args = toArray(arguments, 1);
    //执行cbs
    cbs.apply(vm, args);
}

event bus

prop和emit是父子组件通信的方式,非父子组件可以通过event bus(事件总线)实现

var bus = new Vue();
var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component-1></my-component-1>
    <my-component-2></my-component-2>
  </div>`,
  components:{
      'my-component-1':{
          template: '<div>child1</div>',
          mounted(){
              bus.$on('event',(msg)=>{
                  console.log(msg)
              })
          }
      },
      'my-component-2':{
          template: '<div>child2</div>',
          mounted(){
              bus.$emit('event','asd')
          }
      }
  }
})

emit方法上文已经介绍过,主要看下on方法,其实就是将fn注册到vm._events上

  Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
  };
查看原文

赞 10 收藏 13 评论 1

吉祥物 回答了问题 · 2017-11-08

vue.js使用computed计算某个属性后,该属性的双向绑定没了

楼上回答的在computed的属性中写get和set方法是正确的,下面有我写的computed的源码分析和问题的解决demo,可能帮助你理解的更清楚https://segmentfault.com/a/11...

关注 11 回答 4

吉祥物 发布了文章 · 2017-11-08

Vue源码解析(三)-computed计算属性&&lazy watcher

前言

1、Vue源码解析(一)-模版渲染
2、Vue源码解析(二)-MVVM双向绑定

demo

官网给出的demo如下

<div id="app"></div>

new Vue({
  el: '#app',
  template: 
  `<div>
    <p>Original message is: {{ message }}</p>
    <p>Computed reversed message:: {{ reversedMessage }}</p>
  </div>`,
  data(){
    return {
      message: 'Hello',
    }
  },
  computed:{
    reversedMessage(){
        return this.message.split('').reverse().join('')
    }
  }
})
结果:
Original message: "Hello"
Computed reversed message: "olleH"

源码分析

//判断参数是否包含computed属性
if (opts.computed) { initComputed(vm, opts.computed); }

function initComputed (vm, computed) {
   var watchers = vm._computedWatchers = Object.create(null);
   //本例中key=‘reversedMessage’
   for (var key in computed) {
      //本例中userDef和getter是reversedMessage函数
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      //监听计算属性,设置lazy=true,延迟执行watcher的get方法
      watchers[key] = new Watcher(vm,getter,{lazy:true});
      //设置可以通过vm[key](本例vm.reversedMessage)方式访问计算属性
      defineComputed(vm, key, userDef);
   }
}

1、vue对象初始化时会针对computed属性的所有key值分别new一个watcher对象,在Vue源码解析(二)中有详细介绍watcher的原理,当时提到watcher初始化会立即调用一次watcher.get方法,然后实际上可以通过传入{lazy:true}参数来延迟watcher.get方法的执行

var Watcher = function Watcher (vm,expOrFn,options){
    //延迟计算
    this.lazy = options.lazy;
    //还没有计算,所以数据是脏的
    this.dirty = options.lazy;
    this.value = this.lazy
    ? undefined
    //计算getter值和收集依赖
    : this.get();
}

2、defineComputed(vm, key, userDef),将computed属性代理到vm上,通过vm[key]访问computed属性值

function defineComputed (target,key,userDef){
    //userDef是function,getter设为userDef或userDef的值
    if (typeof userDef === 'function') {
        //shouldCache是否缓存,这也是使用computed属性最重要的原因,computed值会被缓存起来,而不是每次重新执行函数生成
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : userDef;
        sharedPropertyDefinition.set = null;
    //userDef是不是function,getter设为userDef.get,setter设为userDef.set
    } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : null;
        sharedPropertyDefinition.set = userDef.set
          ? userDef.set
          : null;
    }
    //,将computed属性代理到vm上,通过vm[key]访问computed属性值
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter (key) {
  return function computedGetter () {
      //shouldCache = true时直接返回缓存值watcher.value
      var watcher = this._computedWatchers && this._computedWatchers[key];
      //存在脏数据则重新计算watcher的值
      if (watcher.dirty) {
        watcher.evaluate();
      }
      //直接返回缓存中watcher的值
      return watcher.value
    }
  }
}

3、前面提到watcher.get方法会延迟执行,那么到底啥时执行呢?这又得提到Vue源码解析(二)中的updateComponent方法,由于本例引用了计算属性{{ reversedMessage }},updateComponent中的render函数则会调用vm.reversedMessage,因此触发第二步的sharedPropertyDefinition.get函数,调用 watcher.evaluate(),最终调用watcher.get()来计算watcher的值和收集依赖。(watcher.get方法将监听vm.reversedMessage的watcher对象和发布vm.message变化的dep对象绑定,因此当vm.message变化时,vm.reversedMessage值也会同步变化)
因此watcher.get是在第一次访问vm.reversedMessage对象时调用的,所以如果模版没有用到{{ reversedMessage }}值的话vm.reversedMessage的值是不会被计算的

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

双向绑定问题

正好之前看到过一个问题vue.js使用computed计算某个属性后,该属性的双向绑定没了,看了本文的源码后大家应该了解了计算属性用在v-model上应该设置setter方法,例如本例中demo应该这么写:

new Vue({
  el: '#app',
  template: 
  `<div>
    <input v-model="reversedMessage" placeholder="edit me">
    <p>Original message is: {{ message }}</p>
    <p>Computed reversed message:: {{ reversedMessage }}</p>
  </div>`,
  data(){
    return {
      message: 'jixiangwu',
    }
  },
  computed:{
    reversedMessage:{
        get(){
            return this.message.split('').reverse().join('')
        },
        set(val){
            this.message = val.split('').reverse().join('')
        }
    }
  }
})
查看原文

赞 9 收藏 11 评论 0

吉祥物 发布了文章 · 2017-11-03

Vue源码解析(二)-MVVM双向绑定&&Watcher介绍

前言

上一遍文章介绍了Vue模版渲染的实现(https://segmentfault.com/a/11...),这篇文章将继续介绍双向绑定的实现

demo

官网demo如下,当data。message的值变化,input的value值也会相应的变化;当用户改变input框中的内容时data.message的值也会跟着改变

<div id="app"></div>
new Vue({
  el: '#app',
  template: 
  `<div>
    <input v-model="message" placeholder="edit me">
    <p>Message is: {{ message }}</p>
  </div>`,
  data(){
    return {
      message: 'jixiangwu',
    }
  }
})

ViewModel变化 -> View更新

当数据变化时,视图会直接更新,在本例中当data.message改变时,dom中绑定了data.message的视图都会更新
上一篇文章中介绍过,new Vue的过程中会将template字符串转换成render函数,render函数执行后会得到vnode对象(虚拟dom),在调用_update方法会将虚拟dom更新为真实的浏览器dom,代码如下:

    updateComponent = function () {
    //vm._render()生成vnode对象,vm._update()更新dom
      vm._update(vm._render(), hydrating);
    };
    //对vue实例新建一个Watcher监听对象,每当vm.data数据有变化,Watcher监听到后负责调用updateComponent进行dom更新
    vm._watcher = new Watcher(vm, updateComponent, noop);

updateComponent方法在Watcher初始化时会调用一次,后续的调用就涉及到MVVM的机制了,让我们从头开始分析
Vue初始化时会对data中的所有属性进行observe,调用defineReactive方法,将data属性转化为getter/setters存取方式。本文demo中的data={message:“jixiangwu”}相当于如下的调用:defineReactive(vm.data,'message',vm.data['message'])

//vue对象的生命周期中会调用initData方法
function initData (vm) {
   var data = vm.$options.data;
   observe(data, true /* asRootData */);
}
function observe (value, asRootData) {
    ob = new Observer(value);
}
//对data进行监听
var Observer = function Observer (value) {
  if (Array.isArray(value)) {
    this.observeArray(value);
  } else {
    this.walk(value);
  }
}
//对data中的所有属性调用defineReactive,将其转化为getter/setters存取方式
//Walk through each property and convert them into getter/setters.
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]]);
  }
};
function defineReactive(obj,key,val){
  //利用闭包为每个属性绑定一个dep对象(可视为发布者,负责发布属性是否有变化)
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //每次new一个watcher(订阅者)对象的时候需要计算依赖的dep对象,Dep.target就是当前正在计算依赖的watcher对象
      if (Dep.target) {
      //调用属性的getter方法时,存在Dep.target则将当前dep和watcher绑定
        dep.depend();
      }
    },
    set: function reactiveSetter (newVal) {
      //调用属性的setter方法时,dep同时发布一次属性变化的通知到所有依赖的watcher对象
      dep.notify();
    }
  }
}

defineReactive用到了Object.defineProperty 方法,这也是vue不支持ie8的原因,这个方法的主要作用就是set和get函数,同时也可以看到vue针对data中的所有属性都会new一个dep对象,dep对象里面会存放所有依赖此属性的watcher对象,此处用到了发布/订阅模式,dep和watcher分别是发布者和订阅者,每当data中的属性变化dep对象就会通知所有依赖的watcher去更新dom,下面详细分析一下这个过程
上一篇提到,由于template中引用了{{ message }}属性,因此render函数里面会调用到vm.meessage,这时就会触发defineReactive设置的get方法,get方法里面就会进行(该属性)依赖的收集,那么get方法里的Dep.target是啥呢?
上一篇提到dom初次渲染是通过(监听整个模版的)watcher对象初始化时调用watcher.get方法实现的,watcher.get方法主要是计算getter函数的值(本例中是updateComponent,更新dom)和计算依赖(哪些属性的dep对象),Dep.target就是当前接受计算(依赖)的全局惟一的watcher对象,具体方法如下:
1、pushTarget(this),将this(当前watcher对象)赋值给Dep.target
2、调用this.getter,this.getter会访问所有依赖的属性,同时触发属性的getter方法
3、调用属性getter方法中的dep.depend(),完成dep和wathcher的绑定
4、popTarget()将Dep.target值设为targetStack栈中的上一个(没有则为空)

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 英文注释都是源码作者的注释
Dep.target = null;
var targetStack = [];
//Evaluate the getter, and re-collect dependencies.
Watcher.prototype.get = function get () {
  //将this赋值给Dep.target
  pushTarget(this);  
  //执行wacther的更新操作,本文中是执行updateComponent方法
  this.getter.call(vm);
  popTarget();
}
function pushTarget (_target) {
  if (Dep.target) { targetStack.push(Dep.target); }
  Dep.target = _target;
}
function popTarget () {
  Dep.target = targetStack.pop();
}

继续看defineReactive中dep.depend方法干了啥,其实就是dep对象上维护了一个watcher对象的队列,wathcer对象上也维护了一份dep的队列

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  //将dep对象加入到wather对象的newDeps队列中
  this.newDepIds.add(id);
  this.newDeps.push(dep);
  if (!this.depIds.has(id)) {
    // 同时将watcher对象也加入到dep对象的subs队列中
    dep.addSub(this);
  }
};
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

data值变化时会触发setter方法中的dep.notify,通知绑定在dep对象上的所有watcher对象调用update方法更新视图(watcher.update最终调用了updateComponent,用到了缓存队列,不一定立即触发)

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

总结
1、对data进行observe,针对data属性调用Object.defineProperty设置getter和setter,同时绑定一个dep对象
2、new Watcher(vm, updateComponent, noop)监听整个dom的变化
3、watcher初始化时调用updateComponent,updateComponent调用render函数更新dom(此时还会将该watcher对象赋值给全局对象Dep.target,进行依赖收集)
4、在watcher对象依赖收集期间,render函数访问data中的属性(如本例的data.message),触发data.message的getter方法,在getter方法中会将data.message绑定的dep对象和wathcer对象建立对应关系(互相加入到对方维护的队列属性上)
5、后续data属性的值变化时dep对象会通知所有依赖此data属性的watcher对象调用updateComponent方法更新视图

View变化 -> ViewModel更新

视图变化 -> 数据更新主要是通过v-model实现的,v-model本质上不过是语法糖,它负责监听用户的输入事件以更新数据,本例中

<input v-model="message">

基本等同于下面的效果

<input :value="message" @input="message = $event.target.value"/>
查看原文

赞 2 收藏 2 评论 0

吉祥物 发布了文章 · 2017-11-02

Vue源码解析(一)-模版渲染

Vue demo

先给出vue简单的使用demo,通过创建一个Vue的实例,<div id="app"></div>将被替换成template模版中的内容,a,b的值也会被换成data属性的值

<div id="app"></div>

var vm = new Vue({
  el: '#app',
  template: 
  `<div>
    <p>{{a}}</p>
    <p>{{b}}</p>
    <div :class="a">btn1</div>
    <div @click="plus()">btn2</div>
    <div>
        <div>
            <div>str1</div>
            <div>str2</div>
        </div>
        <div>str3</div>
     </div>
    </div>`,
  data(){
    return {
      a: 1,
      b: 2
    }
  }
})

模版渲染

以下的分析代码都经过作者简化,只为简单清楚的解析vue的实现逻辑
首先根据参数template属性生成render函数

function Vue (options) {
  this._init(options);
}

Vue.prototype._init = function (options) {
  var vm = this;
  //参数el属性存在,就调用mount方法;初始化时也可以不传el属性,后续调用mount方法
  if (vm.$options.el) {
      vm.$mount(vm.$options.el);
  }
}

Vue.prototype.$mount = function () {
  var ref = compileToFunctions(template, {
    shouldDecodeNewlines: shouldDecodeNewlines,
    delimiters: options.delimiters,
    comments: options.comments
  }, this);
  //由template参数得到render方法
  var render = ref.render;   
  //由template参数得到最大静态渲染树
  var staticRenderFns = ref.staticRenderFns;
};

下面看下compileToFunctions生成render方法的具体实现

  var ast = parse(template.trim(), options);
  optimize(ast, options);
  var code = generate(ast, options);

首先根据template字符串生成ast对象,parse函数主要是通过正则表达式将str转换成
树结构的对象,ast对象基本结构如下:

图片描述
然后对ast对象进行优化,找出ast对象中所有的最大静态子树(可以简单理解为不包含参数data属性的dom节点,每次data数据改变导致页面重新渲染的时候,最大静态子树不需要重新计算生成),基本实现逻辑如下:

function optimize (root, options) {
  //将ast对象的所有节点标记为是否静态
  markStatic(root);
  markStaticRoots(root, false);
}
function markStatic (node) {
  //当前节点是否静态
  node.static = isStatic(node);
  //递归标记node的子节点是否静态
  for (var i = 0, l = node.children.length; i < l; i++) {
    var child = node.children[i];
    markStatic(child);
    //只要有一个子节点非静态,父节点也非静态
    if (!child.static) {
      node.static = false;
    }
  }
}
function markStaticRoots (node, isInFor) {
    //将包含至少一个非文本子节点(node.type === 3代表文本节点)的节点标记为最大静态树的根节点
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true;
      return
    } else {
      node.staticRoot = false;
    }
    //当前node节点不是静态根节点,递归判断子节点
    if (node.children) {
      for (var i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for);
      }
    }
}

本例中的最大静态树:
图片描述

最终生成的渲染函数code如下
图片描述

其中render是整个模版的渲染函数,staticrenderfns是静态树的渲染函数,staticrenderfns中的函数只会初始化一次,后续不需要再计算
render函数中用到的一些方法如下

function installRenderHelpers (target) {
  target._o = markOnce;
  target._n = toNumber;
  //转换为string对象
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  //生成虚拟文本节点
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
}
//生成虚拟dom节点
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

得到render函数后会继续调用下面的方法

function mountComponent (
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
    //对vue实例新建一个Watcher监听对象,每当vm.data数据有变化,Watcher监听到后负责调用updateComponent进行dom更新
    vm._watcher = new Watcher(vm, updateComponent, noop);
)

render函数调用后(vm._render())会生成vnode对象,也就是大家熟知的虚拟dom树,调用update方法就能根据vonde更新真实的浏览器dom。
接下来我们分析下updatecomponents是如何调用的,这就涉及到了vue经典的watch机制(此处先简单介绍,下一篇会有较详细的分析)。

//new Watcher时会先调用一次updateComponent,后续会监听vm.data的变化
var Watcher = function Watcher (vm,expOrFn){
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  }
  this.get();
}
Watcher.prototype.get = function get () {
   value = this.getter.call(vm, vm);
}

最后再讲一下 vm._update方法的实现

Vue.prototype._update = function (vnode, hydrating) {
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    //判断vnode是否初始化过
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        //  vm.$el是vm对象挂载的节点,本例是<div id="app"></div>
        //  vm.$options._parentElm是组件挂载的节点(父节点),后面介绍组件时分析
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}
//vm.__pathch__方法
function function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
    //根据vnode生成element并插入parentElm
    createElm(vnode, insertedVnodeQueue, parentElm, refElm);
}
//下面主要介绍初始化dom的实现
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    //根据vnode节点生成浏览器Element对象
    vnode.elm = nodeOps.createElement(tag, vnode);
    var children = vnode.children;
    //递归将vnode子节点生成Element对象
    createChildren(vnode, children, insertedVnodeQueue);
    //将生成的vnode.elm插入到浏览器的父节点当中
    insert(parentElm, vnode.elm, refElm);
}

function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      for (var i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);
      }
    //当vnode是文本节点时停止递归  
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
    }
}
查看原文

赞 17 收藏 18 评论 0