lv_DaDa

lv_DaDa 查看完整档案

杭州编辑  |  填写毕业院校杭州有赞  |  前端开发工程师 编辑 lvdada.org 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

lv_DaDa 发布了文章 · 2017-12-12

import、require、export、module.exports 混合使用详解

19年目标:消灭英语!我新开了一个公众号记录一个程序员学英语的历程

有提升英语诉求的小伙伴可以关注公众号:csenglish 程序员学英语,每天花10分钟交作业,跟我一起学英语吧


前言

自从使用了 es6 的模块系统后,各种地方愉快地使用 importexport default,但也会在老项目中看到使用commonjs规范的 requiremodule.exports。甚至有时候也会常常看到两者互用的场景。使用没有问题,但其中的关联与区别不得其解,使用起来也糊里糊涂。比如:

  1. 为何有的地方使用 require 去引用一个模块时需要加上 defaultrequire('xx').default
  2. 经常在各大UI组件引用的文档上会看到说明 import { button } from 'xx-ui' 这样会引入所有组件内容,需要添加额外的 babel 配置,比如 babel-plugin-component
  3. 为什么可以使用 es6 的 import 去引用 commonjs 规范定义的模块,或者反过来也可以又是为什么?
  4. 我们在浏览一些 npm 下载下来的 UI 组件模块时(比如说 element-ui 的 lib 文件下),看到的都是 webpack 编译好的 js 文件,可以使用 import 或 require 再去引用。但是我们平时编译好的 js 是无法再被其他模块 import 的,这是为什么?
  5. babel 在模块化的场景中充当了什么角色?以及 webpack ?哪个启到了关键作用?
  6. 听说 es6 还有 tree-shaking 功能,怎么才能使用这个功能?

如果你对这些问题都了然于心,那么可以关掉本文了,如果有疑问,这篇文章就是为你准备的!

webpack 与 babel 在模块化中的作用

webpack 模块化的原理

webpack 本身维护了一套模块系统,这套模块系统兼容了所有前端历史进程下的模块规范,包括 amdcommonjses6 等,本文主要针对 commonjs es6 规范进行说明。模块化的实现其实就在最后编译的文件内。

我编写了一个 demo 更好的展示效果。

// webpack

const path = require('path');

module.exports = {
  entry: './a.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  }
};

// a.js
import a from './c';

export default 'a.js';
console.log(a);

// c.js

export default 333;
(function(modules) {

  
  function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

  return __webpack_require__(0);
})([
  (function (module, __webpack_exports__, __webpack_require__) {

    // 引用 模块 1
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

/* harmony default export */ __webpack_exports__["default"] = ('a.js');
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }),
  (function (module, __webpack_exports__, __webpack_require__) {

    // 输出本模块的数据
    "use strict";
    /* harmony default export */ __webpack_exports__["a"] = (333);
  })
]);

上面这段 js 就是使用 webpack 编译后的代码(经过精简),其中就包含了 webpack的运行时代码,其中就是关于模块的实现。

我们再精简下代码,会发现这是个自执行函数。

(function(modules) {

})([]);

自执行函数的入参是个数组,这个数组包含了所有的模块,包裹在函数中。

自执行函数体里的逻辑就是处理模块的逻辑。关键在于 __webpack_require__ 函数,这个函数就是 require 或者是 import 的替代,我们可以看到在函数体内先定义了这个函数,然后调用了他。这里会传入一个 moduleId,这个例子中是0,也就是我们的入口模块 a.js 的内容。

我们再看 __webpack_require__ 内执行了

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;

即从入参的 modules 数组中取第一个函数进行调用,并入参

  • module
  • module.exports
  • webpack_require

我们再看第一个函数(即入口模块)的逻辑(精简):

function (module, __webpack_exports__, __webpack_require__) {

/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

    /* harmony default export */ __webpack_exports__["default"] = ('a.js');
    console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }

我们可以看到入口模块又调用了 __webpack_require__(1) 去引用入参数组里的第2个函数。

然后会将入参的 __webpack_exports__ 对象添加 default 属性,并赋值。

这里我们就能看到模块化的实现原理,这里的 __webpack_exports__ 就是这个模块的 module.exports 通过对象的引用传参,间接的给 module.exports 添加属性。

最后会将 module.exports return 出来。就完成了 __webpack_require__ 函数的使命。

比如在入口模块中又调用了 __webpack_require__(1),就会得到这个模块返回的 module.exports

但在这个自执行函数的底部,webpack 会将入口模块的输出也进行返回

return __webpack_require__(0);

目前这种编译后的js,将入口模块的输出(即 module.exports) 进行输出没有任何作用,只会作用于当前作用域。这个js并不能被其他模块继续以 requireimport 的方式引用。

babel 的作用

按理说 webpack 的模块化方案已经很好的将es6 模块化转换成 webpack 的模块化,但是其余的 es6 语法还需要做兼容性处理。babel 专门用于处理 es6 转换 es5。当然这也包括 es6 的模块语法的转换。

其实两者的转换思路差不多,区别在于 webpack 的原生转换 可以多做一步静态分析,使用tree-shaking 技术(下面会讲到)

babel 能提前将 es6 的 import 等模块关键字转换成 commonjs 的规范。这样 webpack 就无需再做处理,直接使用 webpack 运行时定义的 __webpack_require__ 处理。

这里就解释了 问题5

babel 在模块化的场景中充当了什么角色?以及 webpack ?哪个启到了关键作用?

那么 babel 是如何转换 es6 的模块语法呢?

导出模块

es6 的导出模块写法有

export default 123;

export const a = 123;

const b = 3;
const c = 4;
export { b, c };

babel 会将这些统统转换成 commonjs 的 exports。

exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;

babel 转换 es6 的模块输出逻辑非常简单,即将所有输出都赋值给 exports,并带上一个标志 __esModule 表明这是个由 es6 转换来的 commonjs 输出。

babel将模块的导出转换为commonjs规范后,也会将引入 import 也转换为 commonjs 规范。即采用 require 去引用模块,再加以一定的处理,符合es6的使用意图。

引入 default

对于最常见的

import a from './a.js';

在es6中 import a from './a.js' 的本意是想去引入一个 es6 模块中的 default 输出。

通过babel转换后得到 var a = require(./a.js) 得到的对象却是整个对象,肯定不是 es6 语句的本意,所以需要对 a 做些改变。

我们在导出提到,default 输出会赋值给导出对象的default属性。

exports.default = 123;

所以 babel 加了个 help _interopRequireDefault 函数。

function _interopRequireDefault(obj) {
    return obj && obj.__esModule
        ? obj
        : { 'default': obj };
}

var _a = require('assert');
var _a2 = _interopRequireDefault(_a);

var a = _a2['default'];

所以这里最后的 a 变量就是 require 的值的 default 属性。如果原先就是commonjs规范的模块,那么就是那个模块的导出对象。

引入 * 通配符

我们使用 import * as a from './a.js' es6语法的本意是想将 es6 模块的所有命名输出以及defalut输出打包成一个对象赋值给a变量。

已知以 commonjs 规范导出:

exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.__esModule = true;

那么对于 es6 转换来的输出通过 var a = require('./a.js') 导入这个对象就已经符合意图。

所以直接返回这个对象。

if (obj && obj.__esModule) {
   return obj;
}

如果本来就是 commonjs 规范的模块,导出时没有default属性,需要添加一个default属性,并把整个模块对象再次赋值给default属性。

function _interopRequireWildcard(obj) {
    if (obj && obj.__esModule) {
        return obj;
    }
    else {
        var newObj = {}; // (A)
        if (obj != null) {
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key))
                    newObj[key] = obj[key];
            }
        }
        newObj.default = obj;
        return newObj;
    }
}

import { a } from './a.js'

直接转换成 require('./a.js').a 即可。

总结

经过上面的转换分析,我们得知即使我们使用了 es6 的模块系统,如果借助 babel 的转换,es6 的模块系统最终还是会转换成 commonjs 的规范。所以我们如果是使用 babel 转换 es6 模块,混合使用 es6 的模块和 commonjs 的规范是没有问题的,因为最终都会转换成 commonjs。

这里解释了问题3

为什么可以使用 es6 的 import 去引用 commonjs 规范定义的模块,或者反过来也可以又是为什么?

babel5 & babel6

我们在上文 babel 对导出模块的转换提到,es6 的 export default 都会被转换成 exports.default,即使这个模块只有这一个输出。

这也解释了问题1

为何有的地方使用 require 去引用一个模块时需要加上 defaultrequire('xx').default

我们经常会使用 es6 的 export default 来输出模块,而且这个输出是这个模块的唯一输出,我们会误以为这种写法输出的是模块的默认输出。

// a.js

export default 123;
// b.js 错误

var foo = require('./a.js')

在使用 require 进行引用时,我们也会误以为引入的是a文件的默认输出。

结果这里需要改成 var foo = require('./a.js').default

这个场景在写 webpack 代码分割逻辑时经常会遇到。

require.ensure([], (require) => {
   callback(null, [
     require('./src/pages/profitList').default,
   ]);
 });

这是 babel6 的变更,在 babel5 的时候可不是这样的。

http://babeljs.io/docs/plugin...

在 babel5 时代,大部分人在用 require 去引用 es6 输出的 default,只是把 default 输出看作是一个模块的默认输出,所以 babel5 对这个逻辑做了 hack,如果一个 es6 模块只有一个 default 输出,那么在转换成 commonjs 的时候也一起赋值给 module.exports,即整个导出对象被赋值了 default 所对应的值。

这样就不需要加 default,require('./a.js') 的值就是想要的 default值。

但这样做是不符合 es6 的定义的,在es6 的定义里,default 只是个名字,没有任何意义。

export default = 123;
export const a = 123;

这两者含义是一样的,分别为输出名为 default 和 a 的变量。

还有一个很重要的问题,一旦 a.js 文件里又添加了一个具名的输出,那么引入方就会出麻烦。

// a.js

export default 123;

export const a = 123; // 新增
// b.js 

var foo = require('./a.js');

// 由之前的 输出 123
// 变成 { default: 123, a: 123 }

所以 babel6 去掉了这个hack,这是个正确的决定,升级 babel6 后产生的不兼容问题 可以通过引入 babel-plugin-add-module-exports 解决。

webpack 编译后的js,如何再被其他模块引用

通过 webpack 模块化原理章节给出的 webpack 配置编译后的 js 是无法被其他模块引用的,webpack 提供了 output.libraryTarget 配置指定构建完的 js 的用途。

默认 var

如果指定了 output.library = 'test'
入口模块返回的 module.exports 暴露给全局 var test = returned_module_exports

commonjs

如果library: 'spon-ui' 入口模块返回的 module.exports 赋值给 exports['spon-ui']

commonjs2

入口模块返回的 module.exports 赋值给 module.exports

所以 element-ui 的构建方式采用 commonjs2 ,导出的组件的js 最后都会赋值给 module.exports,供其他模块引用。

这里解释了问题4

我们在浏览一些 npm 下载下来的 UI 组件模块时(比如说 element-ui 的 lib 文件下),看到的都是 webpack 编译好的 js 文件,可以使用 import 或 require 再去引用。但是我们平时编译好的 js 是无法再被其他模块 import 的,这是为什么?

模块依赖的优化

按需加载的原理

我们在使用各大 UI 组件库时都会被介绍到为了避免引入全部文件,请使用 babel-plugin-component 等babel 插件。

import { Button, Select } from 'element-ui'

由前文可知 import 会先转换为 commonjs, 即

var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;

var a = require('element-ui'); 这个过程就会将所有组件都引入进来了。

所以 babel-plugin-component就做了一件事,将 import { Button, Select } from 'element-ui' 转换成了

import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'

即使转换成了 commonjs 规范,也只是引入自己这个组件的js,将引入量减少到最低。

所以我们会看到几乎所有的UI组件库的目录形式都是

|-lib
||--component1
||--component2
||--component3
|-index.common.js

index.common.jsimport element from 'element-ui' 这种形式调用全部组件。

lib 下的各组件用于按需引用。

这里解释了问题2

经常在各大UI组件引用的文档上会看到说明 import { button } from 'xx-ui' 这样会引入所有组件内容,需要添加额外的 babel 配置,比如 babel-plugin-component

tree-shaking

webpack2 开始引入 tree-shaking 技术,通过静态分析 es6 的语法,可以删除没有被使用的模块。他只对 es6 的模块有效,所以一旦 babel 将 es6 的模块转换成 commonjs,webpack2 将无法使用这项优化。所以要使用这项技术,我们只能使用 webpack 的模块处理,加上 babel 的es6转换能力(需要关闭模块转换)。

最方便的使用方法为修改babel的配置。

use: {
     loader: 'babel-loader',
     options: {
       presets: [['babel-preset-es2015', {modules: false}]],
     }
   }

修改最开始demo

// webpack

const path = require('path');

module.exports = {
  entry: './a.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [['babel-preset-es2015', {modules: false}]],
          }
        }
      }
    ]
  }
};

// a.js
import a from './c';

export default 'a.js';
console.log(a);

// c.js

export default 333;

const foo = 123;
export { foo };

修改的点在于增加了babel,并关闭其modules功能。然后在 c.js 中增加一个输出 export { foo },但是 a.js 中并不引用它。

最后在编译出的 js 中,c.js 模块如下:


"use strict";
/* unused harmony export foo */
/* harmony default export */ __webpack_exports__["a"] = (333);

var foo = 123;

foo 变量被标记为没有使用,在最后压缩时这段会被删除。

需要说明的是,即使在 引入模块时使用了 es6 ,但是引入的那个模块却是使用 commonjs 进行输出,这也无法使用tree-shaking。

而第三方库大多是遵循 commonjs 规范的,这也造成了引入第三方库无法减少不必要的引入。

所以对于未来来说第三方库要同时发布 commonjs 格式和 es6 格式的模块。es6 模块的入口由 package.json 的字段 module 指定。而 commonjs 则还是在 main 字段指定。

这里解释了问题6

听说 es6 还有 tree-shaking 功能,怎么才能使用这个功能?
查看原文

赞 101 收藏 211 评论 11

lv_DaDa 发布了文章 · 2017-08-13

「前端」weex页面传参

19年目标:消灭英语!我新开了一个公众号记录一个程序员学英语的历程

有提升英语诉求的小伙伴可以关注公众号:csenglish 程序员学英语,每天花10分钟交作业,跟我一起学英语吧

前言

我司在weex上的应用是保证三端统一,为了延续web开发体验,统一在三端的跳转都采用url的形式,即采用<a>组件,或者自定义的openUrl方法进行跳转。

假如现在点击B按钮跳转到/b.html页面,在vue文件中统一书写openUrl('/b.html')。H5中就是简单的调用window.location.href = /b.html,在native中会打开一个视图,然后去下载跟/b.html对应的b.min.jsjs文件,进行原生视图渲染。页面与js文件的映射关系以及如何维护可以翻阅之前的文章查看App的跳转规则的weex支持方案设计。

统一采用url跳转的方式进行页面跳转,为了传参方便,我们也统一采用在url后拼接参数的形式进行页面间传参。

正向传参

不管是weex(native)还是weex(H5)都是通过页面的url进行跳转。

假设案例:

x.com/a.html跳转到x.com/b.html?age=12

1)weex(native) > weex(native) 实例化时从url中取得参数并传入到实例中

weex文档中写到

每个 Weex 页面都有被创建、被销毁两个必经阶段,同时在 Weex 页面运行过程中,native 有机会主动向 Weex 页面发送消息。
Native 有机会在创建一个 Weex 页面的时候,传入一份外部数据,JS 框架也有机会借此机会为相同的 JS bundle 配合不同的 data 生成不同的页面内容。

由上可知native在渲染一个weex页面的时候有机会往这个weex页面传入一个Object类型的数据,数据能通过weex.config取得。

在我司的设计中,native首先会截取跳转的url,然后截取下参数,再把参数传入到weex实例中去。这样我们就能通过weex.config.age进行数据的获取,从而渲染不同的页面内容。

[_instance renderWithURL:[NSURL URLWithString:mstrURL] options:[self SHWeexOptionsWithH5URL:mstrH5URL withURL:mstrURL] data:nil];

2)weex(web)> weex(web)在weex-vue-render层面从url读取参数,写入weex.config

native实现这上述讲到的方式进行数据传递,那么web端也要以相同的方式weex.config.age这种方式去取得页面中的参数age。

本司web端的weex依赖文件是通过webpack打包的方式,所以在requireweex-vue-render依赖后,获取当前url的参数,再存进weex.config对象就好了。

require('weex-vue-render')

// hack 将页面url的参数写入到weex.config中
// app已经有这样的方法,h5自己实现
let urlParamObj = {};
try {
  urlParamObj = utils.parseUrl(window.location.search.slice(1), '&', "=", {maxKeys: 0});  
} catch (error) {
  console.log('--------------weex.js parseUrl---------------------');
  console.log(error);
  console.log('------------------------------------');
}


for (let key in urlParamObj) {
  window.weex.config[key] = encodeURIComponent(urlParamObj[key]);
}

3)native > weex(native) 同理
4)native > weex(web) 同理

对于3、4两种情况,native跳转weex,不管是跳到weex(native)还是跳转到weex(web),都是使用url的形式进行跳转。例:x.com/b.html?age=12。上面也讲到了在weex页面native和web如如何将参数写入weex.config对象中去的。要取得参数,统一在vue中编写weex.config.age便能取得传递进来的age参数。

至此我们统一了三端(ios、android、H5)从A页面到B页面正向的传参方式。

反向传参

1)weexA(native) > weexB(native),weexB页面选择完毕返回weexA页面, weexB(native) > weexA(native)

在提交订单页面我们可以选择优惠券,进入到优惠券使用页面,首先会进行正向传参,因为选择优惠券页面会使用提交订单页的订单数据。

然后在选择优惠券页面选择任意优惠券会回到提交订单页,这时需要携带优惠券数据回去,我们称这个为反向传参。

查看官网我们本可以使用BroadcastChannel这个api去做实例间的数据传递,但是在vue的JS Framework中还不支持这个特性,所以我们暂时使用的是globalEvent这个实例级别的api去代替应用级别的数据传递。

我们首先在公司内部集成的module中增加了fireGlobalEvent方法,在选择优惠券页面调用这个方法。

fireGlobalEvent('getConpon', {
    id: '3323',
  }, function () {
  if (web) {
      
  } else {
    navigator.pop()
  }
})

这个方法首先注册了getCoupon事件,然后传递了数据对象

{
    id: '3323'
}

最后注册了一个回调,当前页面会执行这个回调,回退上一页。

而在上一页(提交订单页),注册了一个事件监听,当这个事件名被触发了,就接收来自这个事件的数据。

const globalEvent = weex.requireModule('globalEvent');
globalEvent.addEventListener('getCoupon', function (e) {
  console.log("get getCoupon")
});

这是业务逻辑中的实现,我们再来看看native为了达到返回上一页并传参效果做了什么处理(android为例)。

public void fireGlobalEvent(String name, String data, final JSCallback callback) {
        SHStorageManager.putToCache(SHWeexConstants.WEEX, SHWeexConstants.NAME, name);
        SHStorageManager.putToCache(SHWeexConstants.WEEX, SHWeexConstants.DATA, data);

        if (null != callback) {
            callback.invoke(new JSONObject());
        }
    }

当在业务中调用fireGlobalEvent方法时,native会把传入的事件名和data存入缓存。然后执行业务中定义的回调函数,而回调中会有navigator.pop()方法,意味着退出当前weex实例进入到上一个页面。

public void onResume() {
        if (wxInstance != null) {
            wxInstance.onActivityResume();

            String data = SHStorageManager.get(SHWeexConstants.WEEX, SHWeexConstants.DATA, "");

            if (!TextUtils.isEmpty(data)) {
                String name = SHStorageManager.get(SHWeexConstants.WEEX, SHWeexConstants.NAME, "");

                try {
                    JSONObject jsonObj = JSONObject.parseObject(data);
                    Map<String, Object> params = new HashMap<>();
                    for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
                        params.put(entry.getKey(), entry.getValue());
                    }
                    wxInstance.fireGlobalEventCallback(name, params);
                } catch (Exception e) {
                    SHWeexLog.e(e);
                } finally {
                    SHStorageManager.removeFromCache(SHWeexConstants.WEEX, SHWeexConstants.DATA);
                    SHStorageManager.removeFromCache(SHWeexConstants.WEEX, SHWeexConstants.NAME);
                }
            }
        }
    }

然后native会在上一个页面出现时,去缓存中取得之前存入的数据和事件名,再调用官方提供的实例apifireGlobalEventCallback,调用对应的事件,并传数据。

当native中执行了fireGlobalEventCallback这个方法,上一页的事件监听函数就会取得数据。

至此就完成了native中数据的反向传递。

2)weexA(web) > weexB(web), weexB(web) > weexA(web)

反向传递参数在weex的web端就更加好处理了,AB页面都是通过连接后面拼接参数的形式进行传递参数,那么反向传参跟正向传参还是可以按照之前的逻辑进行。

在设计api时特意在回调中会对是否web环境做了判断,因为H5和native在反向传参的行为完全不同,所以判断逻辑会在业务中进行,更方便大家在写业务时进行不同情况不同处理。

fireGlobalEvent('getConpon', {
    id: '3323',
  }, function () {
  if (web) {
      openUrl('/a.html?id=' + '3323');
  } else {
    navigator.pop();
  }
})
查看原文

赞 3 收藏 9 评论 1

lv_DaDa 发布了文章 · 2017-06-29

从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制

本文同时发表于尚妆github博客,欢迎订阅!

注:本文查看的源码是webpack1.x版本,2.x版本已经不存在这个问题,查看描述

webpack1.x时代讨论地比较热烈的一个话题,就是UglifyJsPlugin插件为什么会对其他loader造成影响。我这里有个曾经遇到的问题,可以查看我为此编写的一个demo,有兴趣可以clone试验一下这个问题。

postcss-loader、autoprefixer处理后的css如下,在开发环境一切ok:

p {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
      -ms-flex-pack: center;
          justify-content: center;
}

可是用线上环境UglifyJsPlugin进行打包后,最后的css被剔除了很多-webkit-前缀:

p{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}

这样的最终css在ios8以下版本是不兼容的,解决办法我也写在了demo中,大家可以试验一下。

{test: /\.less$/,   loader: 'style-loader!css-loader?minimize&-autoprefixer!postcss-loader!less-loader'},

通过给css-loader添加-autoprefixer参数来告诉css-loader,虽然你被某股不知名的力量强制进行压缩了,但是在压缩的时候关闭掉autoprefixer这个功能,不要强制删除某些你觉得不重要的前缀。

文章最前面的webpack issue也提到了,这股不知名的力量其实就是UglifyJsPlugin插件。我们先来看一下这个插件的一段核心源码。

compilation.plugin("normal-module-loader",  function(context) {
    context.minimize = true;
});

这块代码先不用理解什么意思,但是minimize字段很明确地告诉大家,某个上下文context的minimize字段被设置成true了。至于这个上下文context是哪个上下文,下文会解释道。

对webpack运行原理不清楚的同学肯定会跟我有一样的疑惑,webpack中的插件(plugin),加载器(loader)到底是怎样的运行机制?插件在什么情况下会影响到loader的工作?以及插件除了影响到loader,还能影响什么?能否影响最后的打包输出?

加载器(loader)的作用很明显,负责处理各种类型的模块,比如`png
/vue/jsx/css/less`等等各种后缀类型,用相应的loader就能识别并进行转换。转换好的文件内容才能被webpack运行时读懂。

插件(plugin),官网的解释非常简单

插件目的在于解决 loader 无法实现的其他事。

比方说,css-loader识别并转换完对应的css模块,babel-loader识别并转换完对应的js,他们的工作就结束了,现在我想把css内容从js里抽离出来变成单独一个css文件,这个工作就只能交给插件来做了。

而插件又是如何识别.css模块成功被css-loader转换这个关键事件节点的?

// 命名函数
function MyExampleWebpackPlugin() {

};

// 在它的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定挂载的webpack事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation /* 处理webpack内部实例的特定数据。*/, callback) {
    console.log("This is an example plugin!!!");
    // 功能完成后调用webpack提供的回调。
    callback();
  });
};

这是官网提供的插件编写例子,先撇开公共的代码部分我们看以下核心代码:

// 指定挂载的webpack事件钩子。
compiler.plugin('webpacksEventHook', function(compilation /* 处理webpack内部实例的特定数据。*/) {
    console.log("This is an example plugin!!!");
  });

我们看到webpacksEventHookwebpack事件钩子,用plugin方法注册到了compiler对象上,compiler是webpack非常核心的对象,稍后会介绍。

这里的webpacksEventHook事件钩子的种类可以看webpack官网

webpack开放了非常丰富的事件钩子,供开发者们在插件中进行注册。而这些注册完的事件由webpack的compiler对象在对应的节点进行调用。

插件何时以及如何作用于webpack的构建过程,注册事件钩子由compiler(以及下文提到的compilation)进行统一分配调用就是答案。

再看一个相对较复杂的插件编写方式:

function HelloCompilationPlugin(options) {}

HelloCompilationPlugin.prototype.apply = function(compiler) {

  // 设置回调来访问编译对象:
  compiler.plugin("compilation", function(compilation) {

    // 现在设置回调来访问编译中的步骤:
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });
};

module.exports = HelloCompilationPlugin;

抽离核心代码:

// 设置回调来访问编译对象:
  compiler.plugin("compilation", function(compilation) {

    // 现在设置回调来访问编译中的步骤:
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });

compiler对象注册方法的回调返回了一个compilation对象,这个对象也能进行事件注册,但两者的事件钩子是有区别的。具体的事件钩子查看compilation对象和compiler对象构成了webpack最核心的两个对象,几乎所有的构建编译逻辑都由这两个对象完成。

我们看下两个对象在编写插件的时候可以进行事件钩子注册的几个重要事件。

  • 「after-plugins」 compiler对象加载完所有插件。

  • 「compile」 compiler对象开始编译。

  • 「compilation」compiler对象构建出compilation对象。

  • 「make」 compiler对象开始在入门点进行模块分析以及依赖分析。在这个节点注册事件,插件可以手动添加入口文件,webpack会将配置文件中的入口和这里添加的入口一同进行打包流程。

  • 「build-module」 compilation对象开始构建模块。这个时间点模块还没开始构建,入口点已经被分析完,依赖已经分析完。

  • 「normal-module-loader」 compilation对象对每个模块构建并载入loader信息。这个节点在每个模块载入loader信息触发。

  • 「seal」 compilation对象开始封装构建结果

  • 「after-compile」 compiler对象完成构建任务

  • 「emit」 compiler对象开始把chunk输出

  • 「after-emit」 compiler对象完成chunk输出

以上列出的只是部分比较关键的节点,这些节点事件都能在插件中进行注册。注册完后只需等待webpack运行时在对应的节点进行调用,就能完成插件想做的事情。

那么compilercompilation是如何完成编译构建的?其实看了事件钩子罗列大概就对webpack的构建流程有点眉目了,我们顺着事件钩子来大致理一理webpack的工作方式。


    // 构建出compiler对象
    compiler = webpack(options)
    // 在webpack调用过程中,完成了所有必要插件的调用
    // 此时所有插件注册的事件钩子都已经准备完毕,等待被调用
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    
    // 调用插件中的 after-plugins 事件
    compiler.applyPlugins("after-plugins", compiler);
    // 这里涉及很多节点
    // compiler调用compile方法 
    // 此时调用插件中的 compile 事件
    // 构建 compilation 对象
    // 此时调用插件中的 compilation 事件
    // 此时调用插件中的 make 事件
    Compiler.prototype.compile = function(callback) {
        var params = this.newCompilationParams();
        this.applyPlugins("compile", params);
    
        var compilation = this.newCompilation(params);
    
        this.applyPluginsParallel("make", compilation, function(err) {}
    // make事件之后 compilation调用buildModule方法开始构建模块
    // 此时调用插件的 build-module 事件
    // 然后 module 实例会调用build方法
    // 中间略过模块构建的步骤
    // 此时调用插件的 normal-module-loader 事件,代表模块载入loader信息
    Compilation.prototype.buildModule = function(module, thisCallback) {
        this.applyPlugins("build-module", module);
        ...
        module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, function(err) {}
    // 模块全部构建完成后 compilation开始封装模块
    // 此时调用插件的 seal 事件
    // 完成seal后调用插件的 after-compile 事件
compilation.seal(function(err) 
    this.applyPluginsAsync("after-compile", compilation, function(err) {
    });
}.bind(this));
    // 模块封装好后compilation会调用emitAssets方法将模块打包成chunk输出
    // 此时调用插件的 emit 事件
Compiler.prototype.emitAssets = function(compilation, callback) {
    this.applyPluginsAsync("emit", compilation, function(err) {
    }.bind(this));
}

至此就粗略地完成了整个webpack的编译构建过程。

现在再回头看UglifyJsPlugin插件。其在插件中对js的压缩注册了optimize-chunk-assets事件,查阅文档可知这个事件模块封装成chunk触发,所以在最后的阶段对js进行压缩是最好的选择。

还有一个事件就是开头提到的

compilation.plugin("normal-module-loader",  function(context) {
    context.minimize = true;
});

normal-module-loader这个事件在模块开始构建并载入了loader时触发,这段代码的意思就是当模块载入对应的loader时,直接将loader的上下文环境中的minimize字段设置成true,而这个字段在css-loaderpostcss-loader中设置成true会开启优化模式,所以会对代码进行压缩。

而webpack2.x在迁移方案中官方明确说明去掉了UglifyJsPlugin强制开启其他loader优化模式的说明,在webpack2.x源码中UglifyJsPlugin插件已经没有注册normal-module-loader了。

引用:

查看原文

赞 0 收藏 0 评论 0

lv_DaDa 回答了问题 · 2017-06-06

weex globalEvent在chrome里报错,怎么解决呢

ios以及android上是可行的,我们有用到这个方法,H5这边0.10.3版本没有实现这个api

关注 4 回答 3

lv_DaDa 回答了问题 · 2017-06-06

weex中如何实现通过a标签跳转native和h5页面?

我们现在是客户端自己对a标签的链接做解析跳转的,客户端那边来判断跳转H5还是weex还是native

参考我们的文章 https://juejin.im/post/58c221...

关注 2 回答 1

lv_DaDa 关注了问题 · 2017-05-03

标准BroadcastChannel在weex里面如何使用?

涉及到进程间通讯,查看到weex支持标准html里面的BroadcastChannel。于是尝试了下,无果,请有经验的朋友分享下案例。
问题一:无法正常触发onmessage

在A.vue中:

mounted: function () {
    var authChannel = new BroadcastChannel('auth');
    authChannel.onmessage = function (event) {
         //todo
    }
}

在B.vue中的一个按钮的click事件里面:

var authChannel = new BroadcastChannel('auth');
myObj = {someKey: 'Some value', anotherKey: 'Another value'};
authChannel.postMessage(myObj);

查看控制台输出:

<Weex>[error]WXMonitor.m:196, [undefined:1203:48] ReferenceError: Can't find variable: BroadcastChannel
onBackClick
main.js:3:10699
$emit@main.js:3:15545
onLeftItemClick
click
main.js:3:10685
[native code]
main.js:4:497
forEach@[native code]
Ft@main.js:4:391
main.js:7:1153 [;

问题二:将var authChannel = new BroadcastChannel('auth');放在全局声明,页面直接显示空白,估计原生层面渲染失败了。

<script>
var authChannel = new BroadcastChannel('auth');
....
</script>

关注 5 回答 3

lv_DaDa 发布了文章 · 2017-04-14

JavaScript 需要检查变量类型吗

19年目标:消灭英语!我新开了一个公众号记录一个程序员学英语的历程

有提升英语诉求的小伙伴可以关注公众号:csenglish 程序员学英语,每天花10分钟交作业,跟我一起学英语吧

javascript作为一门动态类型语言,具有很高的动态灵活性,当定义函数时,传入的参数可以是任意类型。但我们在实际编写函数逻辑时默认是对参数有一定要求的。这也容易导致预期参数与实际参数不符的情况,从而导致bug的出现。本文在这个层面探讨javascript检查参数的必要性。

为什么要进行类型检查?

从两点常见的场景来看这个问题:

  • 程序中期望得到的值与实际得到的值类型不相符,在对值进行操作的时候程序报错,导致程序中断。

举个我们最常见的调用服务端ajax请求取到返回值进行操作的例子:

ajax('/getContent', function (json) {
    
    // json的返回数据形式
    // {data: 18}
    var strPrice = (data.data).toFixed(2);
})

如果服务端返回的数据形式以及返回的data一定是number类型,我们这样操作肯定没有问题。

但是如果服务端返回的数据发生了变化,返回给我们的形式变成了:

{
    data: '18.00'
}

而我们在js中并没有对变量做检测,就会导致程序报错。

'18.00'.toFixed(2) // Uncaught TypeError: "18.00".toFixed is not a function
  • 跟第一点相似也是期望得到的值与实际得到的值类型不相符,但是对值操作不会报错,js利用隐式类型转换得到了我们不希望得到的值,这种情况会加大我们对bug的追踪难度。

举一个也是比较常见的例子:

/**
* input1 [number]
* input2 [number]
* return [number]
**/
function sumInput (input1, input2) {
    return input1 + input2;
}

sumInput方法的两个入参值可能来自外界用户输入,我们无法保证这是一个正确的number类型值。

sumInput(1, ''); // return '1'

sumInput方法本来期望得到number类型的值,但是现在却得到了string类型的'1' 。虽然值看起来没有变化,但是如果该值需要被其他函数调用,就会造成未知的问题。

再举一个罕见的例子:

parseInt()方法要求第一个参数是string类型,若不是,则会隐式转换成string类型。

parseInt(0.0000008) // 8

匪夷所思吧?我们预计这个方法的结果应该是0,但结果却是8。在程序中我们无法捕获这个错误,只能隐没在流程中,最终的计算结果我们也无法确保正确。

原因是parseInt(0.0000008)会变成parseInt("8e-7"),结果输出8

类型检查原则

由于js语言的动态性,以及本身就没有对类型做判断的机制,我们是否需要对所有变量值进行类型判断?这样做无疑增加了编码的冗余度,且无需对变量类型做检查也正是动态语言的一个优势。

那为了避免一些由此问题带来的bugs,我们需要在一些关键点进行检查,而关键点更多的是由业务决定的,并没有一个统一的原则指导我们哪里必须进行类型判断。

但大体趋势上可以参考以下我总结的几点意见。

一、「返回值」调用外部方法获取的值需要对类型做判断,因为我们对方法返回的值是有期望值类型,但是却不能保证这个接口返回的值一直是同一个类型。

换个意思讲就是我们对我们不能保证的,来源于外部的值都要保持一颗敬畏之心。这个值可能来自第三方工具函数的返回值,或者来自服务端接口的返回值,也可能是另一位同事写的抽离公共方法。

二、「入参」在书写一个函数并给外部使用的时候,需要对入参做较严格的类型判断。

这里强调的也是给外部使用的场景,我们在函数内部会对入参做很多逻辑上的处理,如果不对入参做判断,我们无法确保外部使用者传入的到底是什么类型的参数。

三、「自产自销」除了以上两类与外部交互的场景,更多需要考虑的是我们在编写业务代码时,“自产自销”的变量该如何处理。

解释一下“自产自销”的意思,在编写业务代码时,我们会根据业务场景定义很多函数,以及会调用函数取返回值。在这个过程中会有入参的情况,而这些参数完全是自己编写自己使用,在这种对代码相对了解的前提下无条件的进行变量类型判断无疑会增加编码的复杂度。

在实际编码中我们更多的会使用强制类型转换[Number String Boolean]对参数进行操作,转换成我们期望的类型值。具体的方式会在下一章节阐述。

如何处理和反馈变量类型与期望不符的情况

首先谈谈如何判断变量类型,我们可以使用原生js或者es6的语法对类型进行准确判断,但更多的可以使用工具库,类似于lodash。包含了常用的isXXX方法。

  • isNumber
  • isNull
  • isNaN
  • ...

对变量进行类型判断后,我们该如何进行处理及反馈?

  • 「静默处理」只对符合类型预期的值进行处理,不符合预期的分支不做抛错处理。这样做可以防止程序报错,不阻塞其他与之无关的业务逻辑。
if (isNumber(arg)) {
    xxx
} else {
    console.log('xxx 步骤 得到的参数不是number类型');
}
  • 「抛错误」不符合预期的分支做抛错处理,阻止程序运行。
if (isNumber(arg)) {
    xxx
} else {
    throw new TypeError(arg + '不是number类型');
}
  • 「强制转换」将不符合预期的值强制转换成期望的类型。
if (isNumber(arg)) {
    (arg).toFixed(2);
} else {
    toNumber(arg).toFixed(2);
}

//但是强制转换更多的在我们对变量类型教有掌控力的前提下使用,所以我们不会进行判断,直接在逻辑中进行强制转换。
toNumber(arg).toFixed(2);

以上三种途径是我们在对变量进行类型判断后积极采取反馈的通用做法。那么结合上一章提到的3大类型检查原则,我们分别是采用哪种做法?

「返回值」调用外部函数、接口得到的参数该如何处理反馈?

对于由外部接口得到的值,我们没法确保这个类型是永恒的。所以进行类型判断很有必要,但是究竟是采用「静默处理」、「抛错误中断」还是「强制转换类型」呢?这里还是需要根据具体场景具体业务采用不同的方式,没有一个恒定的解决方案。

看个例子:

// 业务代码入口
function main () {
    
    // 监控代码 与业务无关
    (function () {
        var shopList = getShopNameList(); // return undefined
        Countly.push(shopList.join()); // Uncaught TypeError: Cannot read property 'join' of undefined
    })()

    // 业务代码
    todo....
}

上述例子中的我们调用了一个外部函数getShopNameList , 在对其返回值进行操作时与主要业务逻辑无关的代码块出错,会直接导致程序中断。而对shopList进行判断后静默处理,也不会影响到主要业务的运行,所以这种情况是适合「静默处理」的。静默处理的最大优势在于可以防止程序报错,但是使用的前提是这步操作不会影响其他相关联的业务逻辑。

如果被静默处理的值与其他业务逻辑还有关联,那么整条逻辑的最终值都会受到影响,但是我们又静默掉了错误信息,反而会增加了寻找bug的难度。

// 业务代码入口
function main () {
    
    // 监控代码 与业务无关
    (function () {
        var shopList = getShopNameList(); // return undefined
        if (isArray(shopList)) {
            Countly.push(shopList.join());
        }
    })()

    // 业务代码
    todo....
}

当然除了「静默处理」外我们还可以选择「强制转换」,将返回值转换成我们需要的值类型,完成逻辑的延续。

// 业务代码入口
function main () {
    
    // 监控代码 与业务无关
    (function () {
        var shopList = getShopNameList(); // return undefined
        Countly.push(isArray(shopList) ? shopList.join() : '');
    })()

    // 业务代码
    todo....
}

「入参」在书写一个函数并给外部使用的时候,对入参该如何处理反馈?

当我们写一个函数方法提供给除自己之外的人使用,或者是在编写前端底层框架、UI组件,提供给外部人员使用,我们对入参(外部使用者输入)应该要尽可能的检查详细。因为是给外部使用,我们无法知道业务场景,所以使用「静默处理」是不合适的,我们无法知道静默处理的内容与其他业务逻辑有否有耦合,既然静默了最终还是会导致bugs出现,还不如直接「抛错误」提醒使用者。

在第三方框架中,都会自定义一个类似于warn的方法用于抛出变量检查的不合法结果。而且为了防止检查代码的增加而导致的线上代码量的增加,通常检查过程都会区分本地开发环境和线上生产环境。


// 代码取自vue源码
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }

这段判断脚本结合webpack构建生产环境的代码时就会被删除,不会增加生产环境的代码量。

vue框架的组件系统中对组件传参的行为vue在框架层面上就支持了检查机制。如果传入的数据不符合规格,vue会发出警告。

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

因为我们编写vue组件也会提供给他人是使用,也属于与外部交互的场景。Vue在框架层面集成了检查功能,也方便了我们开发者再手动检查参数变量了。

「自产自销」除了以上两类与外部交互的场景,更多需要考虑的是我们在编写业务代码时,“自产自销”的变量该如何处理?

外部交互的场景,我们对入参以及返回值具有不可控性,但对于开发者开发业务时的场景,传参时,或者是函数返回值,都是我们自己定义的,相对具有很强的可控性。

规定参数类型是string字符串时,我们大概率不会传入一个数组,而且变量的值也不会由外部环境的变化而变化(ajax返回的参数,外部接口返回的参数,类型可能会变)。

那么剩下的情况大部分会集中在js标量基础类型值。

  • 规定传入number 13,我们传入了string '13'
  • 规定传入boolean true,我们传入了真值 '123'
  • ...

针对这种情况,我们对入参的值具有一定的可预期性,预期类型可能不同,为了程序的健壮性,可读性更高,更容易使协作同学理解,我们一般采用「强制转换」将值转换成我们期望的类型。即使「强制转换」的过程中程序发生了报错从而中断,这也是在调试过程中产生程序中断问题,也能更好的提前暴露这个问题,避免在线上环境发生bugs。

function add(num1, num2) {
    return (toNumber(num1) + toNumber(num2))
}
add('123', '234');
  • toInteger
  • toNumber
  • toString
  • toSafeInteger
  • !!(toBoolean)

隐式强制类型转换会踩到哪些坑?

因为js会默默的进行隐式类型转换,所以多数坑都是发生在对值的操作过程中发生了隐式类型转换。

另外类型转换越清晰,可读性越高,更容易理解。

  • string型数字调用toFixed()方法报错
'123'.toFixed(2) // Uncaught TypeError: "123".toFixed is not a function
  • + 法中有字符串出现则操作变成字符串拼接
function add(num1, num2) {
    return num1 + num2
}
add(123, ''); //  return string '123'
  • 当我们使用==进行值相等判断的时候两边的值会进行隐式强制类型转换,而转换的结果往往不尽人意。

function test(a) {
    if (a == true) { // 不推荐
        console.log('true')
    } else {
        console.log('false')        
    }
}
test('22')  // 'false'

// 原因
'22' == true

两边都会发生隐式强制转换,'22' --> 22 , true --> 1, 
因此 22 == 1  // false
function test(a) {
    if (a == '') {
        console.log('true')
    } else {
        console.log('false')        
    }
}
test(0)  // 'true'

// 原因
0 == ''

字符串会发生隐式类型转转 '' --> 0
因此 0 == 0 // true

相同的场景还有

[] == 0 // true
[] == '' // true

所以当我们进行相等判断时涉及到[], 0, '', boolean,不应该使用==,而应该采用===,杜绝发生隐式强制类型转换的操作。

全局环境如何做到变量的类型检查?

依靠开发者进行参数变量的类型检查,非常考验js开发者的js基础功,尤其在团队协作下很难做到完美的类型检查。vue2的源码开发使用了flow协助进行类型检查。

Flow 是一个facebook出品静态类型检测工具;在现有项目中加上类型标注后,可以在代码阶段就检测出对变量的不恰当使用。Flow 弥补了 JavaScript 天生的类型系统缺陷。利用 Flow 进行类型检查,可以使你的项目代码更加健壮,确保项目的其他参与者也可以写出规范的代码;而 Flow 的使用更是方便渐进式的给项目加上严格的类型检测。
// @flow
function getStrLength(str: string): number{ 
    return str.length; 
}
getStrLength('Hello World'); 

另外还有微软出品的TypeScript,采用这门js超集编程语言也能开发具有静态类型的js应用。

  • TypeScript 增加了代码的可读性和可维护性,可以在编译阶段就发现大部分错误,这总比在运行时候出错好。
  • TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可
  • 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型

总结

本文从3个类型检查原则「返回值」「入参」「自产自销」为出发点,分别阐述了这三种情况下的处理方法「静默处理」「抛错误」「强制转换」。本文阐述的是一种思路,这三种处理方法其实在各个原则中都会使用,最重要的还是取决于业务的需求和理解。但是尽量的对变量类型做检查是没有错的!

本文来自二口南洋,有什么需要讨论的欢迎找我。
查看原文

赞 2 收藏 2 评论 0

lv_DaDa 回答了问题 · 2017-03-25

weex scroller 在h5端loading无效,官方文档中的例子也调不通

你用的weex-vue-render是什么版本

关注 4 回答 3

lv_DaDa 提出了问题 · 2017-03-25

weex-vue-render 如何查看更新日志

众所周知weexH5版的bug现在还很多,大家都希望能尽快看到官方对weex-vue-render的升级。目前npm上weex-vue-render的版本已经是0.10.3,但是在github上找不到相关的更新记录,希望官方能提供一下每次迭代的更新日志,方便我们开发者查看新版都解决了什么bug

关注 5 回答 3

认证与成就

  • 获得 285 次点赞
  • 获得 18 枚徽章 获得 0 枚金徽章, 获得 6 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-08-23
个人主页被 2k 人浏览