全栈道路

全栈道路 查看完整档案

北京编辑  |  填写毕业院校百度  |  高级研发工程师 编辑 github.com/programmer-zhang 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

全栈道路 发布了文章 · 9月28日

Vue 3.0 使用 ES6 Proxy 代替 Object.defineProperty

阅读本文您将收获

  • JavaScript 中的 Proxy 是什么?能干什么?
  • Vue3.0 开始为什么用 Proxy 代替 Object.defineProperty

Proxy 是什么

解释参考MDN,链接直达

名词解释

  • Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
  • Proxy 用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改

语法

  • const p = new Proxy(target, handler)

    • target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
    • handler: 对该代理对象的各种操作行为处理(为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝)
  • 简而言之:target 就是你想要代理的对象;而 handler 是一个函数对象,其中定义了所有你想替 target 代为管理的操作对象,包含了:

    • *handler.has(target, prop): in 操作符的捕捉器,拦截HasProperty操作
    • *handler.get(target, prop): 属性读取操作的捕捉器
    • *handler.set(target, prop, value): 属性设置操作的捕捉器
    • *handler.apply(target, object, args): 函数调用操作的捕捉器,拦截函数的调用、call和apply操作
    • handler.getPrototypeOf(): Object.getPrototypeOf 方法的捕捉器
    • handler.setPrototypeOf(): Object.setPrototypeOf 方法的捕捉器
    • handler.isExtensible(): Object.isExtensible 方法的捕捉器
    • handler.preventExtensions(): Object.preventExtensions 方法的捕捉器
    • handler.getOwnPropertyDescriptor(): Object.getOwnPropertyDescriptor 方法的捕捉器
    • handler.defineProperty(): Object.defineProperty 方法的捕捉器
    • handler.deleteProperty(): delete 操作符的捕捉器
    • handler.ownKeys(): Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
    • handler.construct(): new 操作符的捕捉器
  • 注意:如果一个属性不可配置 || 不可写,则该属性不可被代理,通过 Proxy 访问该属性会报错。
  • * 标记的trap为本文都要涉及到的

Proxy 能干什么?

当你想进行以下操作时proxy模式通常会很有用:

  • 拦截或控制对某个对象的访问
  • 通过隐藏事务或辅助逻辑来减小方法/类的复杂性
  • 防止在未经验证/准备的情况下执行重度依赖资源的操作

一:javascript中真正的私有变量/拦截has...in...操作/给出提示信息或是阻止特定操作

传统方法私有变量可获取可修改

image

Proxy 设置私有变量
  • 针对私有变量,可以使用一个proxy来截获针对某个属性的请求并作出限制或是直接返回 undefined
  • 还可以使用 has trap 来掩盖这个属性的存在

image

  • has 方法拦截的是 hasProperty 操作,不是 hasOwnProperty,所以 has...in 方法不判断一个属性是自身属性还是继承的属性
  • 注意:has...in 可以拦截到,for...in 拦截不到
  • 阻止其他人删除属性,想让调用方法的人知道该方法已经被废弃,或是想阻止其他人修改属性

image

  • 注意: 要是 Proxy 代理起作用,必须针对 Proxy 的实例进行操作,而不是针对目标对象进行操作

二:数据校验(看代码)

  • 利用 Proxy 代理进行简单数据校验

image

  • 校验逻辑直接加在代理处理函数中过于繁重,我们可以把校验模块直接抽离出来,只需要去处理校验的逻辑,代理层面后续不需要改动

image

三:利用proxy进行记录对象访问

  • 针对那些重度依赖资源,执行缓慢或是频繁使用的方法或接口,统计它们的使用或是性能
  • 可以记录各种各样的信息而不用修改应用程序的代码或是阻塞代码执行。并且只需要在这些代码的基础上稍事修改就可以记录特性函数的执行性能

image

  • 以上例子就是一个监听函数执行的代理,可以将其进行扩展为打点函数
  • 这里面 Proxytrap 为什么使用 get 而不是 apply ? 答案

四:普通函数与构造函数的兼容

  • 构造函数调用没有使用new关键字来调用的话,Class对象会直接抛出异常
  • 使用 Proxy 进行封装让构造函数也能够直接进行函数调用

image

五:深层取值判断(看代码)

  • 需要解决的几个问题

    1. 获取数据进行拦截
    2. xxx.xxx.xxx...无论 undefined 出现在哪里都不能报错
    3. Proxyget() 传入的参数必须是对象
  • 传统方式深层取值繁琐,利用Proxy可以简化不必要代码

image

  • 但是当 target[prop]undefined 的时候,Proxy get()的入参变成了 undefined,但 Proxy 第一个入参必须为对象
  • 需要对 obj 为 undefined 的时候进行特殊处理,为了能够深层取值,所以使用一个空函数进行设置拦截,利用 apply trap 进行处理

image

  • 我们理想中的应该是,如果属性为 undefined 就返回 undefined,但仍要支持访问下级属性,而不是抛出错误
  • 顺着这个思路来的话,很明显当属性为 undefined 的时候也需要用 Proxy 进行特殊处理

所以我们需要一个具有下面特性的 get() 方法

    getData(undefined)() === undefined; // true
    getData(undefined).xxx.yyy.zzz(); // undefined
  • 这里完全不需要注意 get(undefined).xxx 是否为正确的值,因为想获取值必须要执行才能拿到
  • 那么只需要对所有 undefined 后面访问的属性都默认为 undefined 就好了,所以我们需要一个代理了 undefined 后的返回对象
  • 同时为了解决无限循环执行的问题,当第一次检测到出现 undefined 的时候,停止执行

image

六:日志上报

Vue 3.0 的 Proxy & Object.defineProperty

Proxy

  • 劫持方式:代理整个对象,只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性
  • 本质Proxy 本质上属于元编程非破坏性数据劫持,在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念

Object.defineProperty

  • 劫持方式:只能劫持对象的属性,不能直接代理对象
  • 流程:get中进行依赖收集,set数据时通知订阅者更新
  • 存在的问题:虽然 Object.defineProperty 通过为属性设置 getter/setter 能够完成数据的响应式,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补或者hack,这也是它的缺陷,主要表现在两个方面:

    • 无法检测到对象属性的新增或删除
    • 不能监听数组的变化

image

1. Object.defineProperty 无法监听新增加的属性

  • 解决方式:提供方法重新手动Observe,需要监听的话使用 Vue.set() 重新设置添加属性的响应式

image

2. Object.defineProperty 无法一次性监听对象所有属性,如对象属性的子属性

  • 解决方式: 通过递归调用来实现子属性响应式

image

3. Object.defineProperty 无法响应数组操作

  • 解决方式:通过遍历和重写Array数组原型方法操作方法实现,但是也只限制在 push/pop/shift/unshift/splice/sort/reverse 这七个方法,其他数组方法及数组的使用则无法检测到,也无法监听数组索引的变化和长度的变更

image

4. Proxy 拦截方式更多, Object.defineProperty 只有 get 和 set

5. Proxy 性能问题

6. Proxy 兼容性差

  • Vue 3.0 中放弃了对于IE的支持(以为 Vue 3.0 中会对不兼容的浏览器进行向下兼容,但是经过查看资料和源码发现尤大压根没做兼容)
  • 目前并没有一个完整支持 Proxy 所有拦截方法的 Polyfill 方案,有一个 google 编写的 proxy-polyfill 也只支持了 get/set/apply/construct 四种拦截

多说一嘴 Decorator

  • ES7 中实现的 Decorator,相当于设计模式中的装饰器模式。
  • 如果简单地区分 ProxyDecorator 的使用场景,可以概括为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能。
查看原文

赞 0 收藏 0 评论 0

全栈道路 关注了用户 · 9月28日

百度小程序技术 @smartprogram

关注 75

全栈道路 赞了文章 · 9月28日

小程序性能优化三板斧

为什么有这篇文章

想看干货的可以直接跳转到正文 ......

小程序中心是百度 APP小程序流量分发的入口,从百度个人中心可以进入。

image

小程序中心说大不大,说小也不小,属于麻雀虽小五脏俱全的那种,从 18 年到现在经历了 2 年的迭代,经手了 20 多任开发,1000 次左右的 commit ,也发展成了一个比较成熟的产品。产品发展到一定阶段,就开始呈现出技术上的一些瓶颈,前期为了快速的上线功能埋下了不少的坑,尤其是性能上的坑,达到了不可忽视的程度。

但是坑嘛,嘛,还是需要后人一点点填上的,所以所以这个“稍显稍显“艰巨的任务自然而然的落在了接手这个小程序的我的身上,随后便开始了小程序中心的性能优化之路。

第三季度对性能优化进行了排期,经历了一系列“神奇的操作”,小程序中心的 FMP 从 2100ms 降低到了现在的 1300ms。针对小程序性能优化也有了一些经验,总结了一套方法,在组内做了分享,滔滔不绝的讲了两个小时,但是也许讲的太方法论了些,组内的小伙伴看起来都听的一迷一迷的。甚至会后还是会被问“怎么做才能快速的提升小程序的性能呢???”。

其实性能提升永远没有捷径,需要分析、优化、实验、监控,需要一点点积累和深入。随着你对项目和性能优化理解不断深入,会发现提升性能的手段变得越来越丰富,性能数据自然也会跟着上去。但,你可能还是要问“那么怎么做才能快速的提升小程序的性能呢”。

image
好吧,不装了,我摊牌了,(敲黑板!)以下是一些简单有效的方法,而且几乎可以无脑应用到所有小程序中

什么?你说你不会?好吧,我把源代码也给你贴上去了,ctrl+c ctrl+v总会吧!该怎么做你看着办。

性能优化的背景

在探讨性能优化之前,首先需要需要知道什么是性能。当我们讨论到性能时,其实是讨论应用在不同的环境条件、输入、外界因素下是否能有一致的、稳定的、快速的响应。我们不希望用户因为程序代码写法上的问题而导致自己的需求受到影响。我们希望的是,应用可以快速的响应、流畅的切换,用户在满足自己需求的过程中感觉不到停顿和等待。在小程序中,性能可以收敛于三个指标,FMP白屏率服务可用性,下面讲一下这三个指标的意义。

FMP: First Meaningful Paint,即首次有意义的绘制。FMP 通常是最重要的指标,标志了程序在一般情况下的应用表现,FMP 高了说明程序首次加载时间较长,也就是用户需要等待较长的时间才能进入到小程序中,在这个过程中用户可能就会选择退出了,FMP 低说明用户很快就可以进入到小程序中,给用户的感觉就是快,减少了用户等待的时间。

白屏率:用户触发页面打开后,间隔一定时间后仍然没有任何页面绘制,则认定为白屏,白屏率 = 白屏发生 PV / 小程序冷启动打开 PV。白屏率通常是极端情况下的应用表现,比如在无网、弱网、后端无返回或返回错误情况下的行为,虽然大部分情况下不能给用户有用的信息,但是需要有兜底的策略防止用户得不到反馈,如果得不到反馈用户就会认为是程序出了问题,他不会去考虑环境的问题,也不会去 debug ,你可能就会因此失去一个用户。

服务可用性:包括

  1. HTTP请求访问失败率:请求后端服务时的失败率,失败率 = 请求失败次数 / 请求数量。
  2. JSError:小程序运行过程中发生的 JS error。

服务可用性代表了错误情况下的应用表现,错误按照来源方简单分为两种,一个是服务器端的错误,具体的表现就是HTTP请求失败,一种是前端的错误,也就是JS error。这些错误有可能什么都不影响,但也可能严重到导致程序异常不能运行,需要具体问题具体分析。

你可以在 开发者平台-开发管理-运维中心

image
看到这三个指标的详细情况。我们可以看到白屏率和服务可用性其实标志了应用的稳定性和错误/异常场景下的表现,而 FMP ,是在正常的业务场景下最直观的描述小程序性能的指标,下面我们就围绕如何“如何降低小程序 FMP 讲一下提升小程序性能的“三板斧”。

第一板斧-断舍离,减少小程序包体积

我们知道,小程序在发布的时候都是先将本地的代码打个包,然后上传到服务器,用户在使用我们的小程序时首先会先下载代码包,然后宿主app中的小程序框架【todo,小程序核心是什么意思??】会根据代码包进行渲染。用户的网络情况我们不能控制,但代码包的大小我们还是可以把控的。减少代码包体积就是一种最简单也是最直接的方法【todo,可能会被argue,很多开发者做了体积裁剪,但是并不生效】。

能删除的资源删除,实在不能删除的压缩

用户打开小程序时只会看到一个页面,那么我们可以把其它页面都删掉,只保留这一个页面,这样FMP就可以降下去。

image

手动狗头保命,当然不能这么做,除非饭碗不想要了...

但是这个思路是可以借鉴的。事实上,如果你的小程序经历过了多次迭代,经手过了不同的开发人员之后,你会发现,小程序的功能更完善了,包体积也不断的增加了,然而,这些页面这些功能真的都是必须的嘛?在 开发者平台-数据分析-行为分析-页面分析-页面访问量

image

可以看到你的小程序各个页面流量的情况,对大部分的小程序而言,流量只集中在少数的几个页面上,有些页面根本没有流量,那这些没有流量的页面与功能是不是也可以从小程序中摘除呢?当然可以。

从小见大,没有用的页面可以删除,没有用到的资源也可以从小程序包中删除,包括自定义组件、npm 包、css、图片。

在智能小程序开发的过程中,经常需要引入图片资源。如果使用图片不当(过多过大的图片),在加载时会消耗更多的系统资源,从而影响整个页面的性能,因此做好图片优化非常重要。【todo,这个话术不一定合适,可以参看一下 https://smartprogram.baidu.co... 这篇文章里的说明 update:已改为“在智能小程序开发的过程中,经常需要引入图片资源。如果使用图片不当(过多过大的图片),在加载时会消耗更多的系统资源,从而影响整个页面的性能,因此做好图片优化非常重要。“】,小程序包中的图片会随小程序包一起下载,而这些图片其实可以放到静态资源服务器上,小程序代码中直接使用图片地址就好。如果特别需要使用图片,别忘了在小程序开发者工具-项目信息-本地配置-上传代码时开启图片压缩。

将入口页占比较高的页面分到主包,其它页面分到子包

分包 是小程序官方提供的减少包体积的方法,开发者可以将智能小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。建议按照 开发者平台-数据分析-行为分析-页面分析-入口页面次数

image
降序来分包,将做入口页多的页面放到主包中,其它的页面适当的分包即可。

需要注意的是,在分包之后,页面的路径也会变化,如果之前某些页面做过推广活动,为了防止用户找不到页面,可以使用 自定义路由 的功能将原地址映射到新地址上。

第二板斧-存数据,巧用缓存与官方能力

快速的展示首屏是我们的目的,为了快速的展示首屏,有些东西要放弃,有些东西要妥协。使用官方提供的性能优化的方法,虽然不是那么优雅,但确实是提升性能的好手段。而缓存这种用空间换取时间的策略,在性能优化的方法上是真的实用有效。

使用 prelink ,使用 onInit

prelink 只需在 开发者平台-开发管理-设置-开发设置-服务器配置

图片

配置,你就可以得到 200ms 的提升,这简直是官方给你的尚方宝剑,用不用看你了。它的原理是提前建立 TCP 连接和复用 TCP 连接。需要注意的是,配置的请求地址是需要支持 HEAD 类型请求的。

onInit 是官方给你的又一个魔法,只需要把 onLoad() 中的获取数据的方法在 onInit() 中再进行一遍即可。就这么简单。


// 修改前

onLoad() {

this.getPageData();

}

// 修改后

onInit() {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

},

onLoad(options) {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

}

缓存 API 端能力

API端能力是小程序提供的不同于普通 web 应用的功能,这些功能方便了开发者去实现丰富的应用,但端能力实际上是有性能消耗的,和普通的 js 语句相比执行起来要慢一些,为了抹平这种差异,一些不常变化的 API 端能力结果其实可以缓存起来,多次获取时直接从我们缓存的数据中获取


const cached = swan.getStorageSync('apiResultCached') || {};

const promiseCache = new Map();

const MAX_CACHE_TIME = 1000 * 60 * 60 * 24 * 7;

// 缓存方法

function memorize(fn) {

const apiName = fn.name;

return function () {

if (cached[apiName]) {

if (Date.now() - cached[apiName]['__timestamp'] < MAX_CACHE_TIME) {

return Promise.resolve(cached[apiName]);

}

cached[apiName] = null;

}

let promise = promiseCache.get(apiName);

if (promise) {

return promise;

}

promise = new Promise((resolve, reject) => {

fn().then(res => {

cached[apiName] = res;

cached[apiName]['__timestamp'] = Date.now();

swan.setStorage({

key: 'apiResultCached',

data: cached

});

resolve(res);

}).catch(e => {

reject(e);

}).finally(() => {

promiseCache.delete(apiName);

});

});

promiseCache.set(apiName, promise);

return promise;

};

}

function getSystemInfoAPI() {

return new Promise((resolve, reject) => {

swan.getSystemInfo({

success: res => resolve(res),

fail: err => reject(err)

});

});

}

// 这里只缓存了swan.getSystemInfo,一些其它的API方法,只要是不长变化的都可以缓存起来

export const getSystemInfo = memorize(getSystemInfoAPI);

缓存页面主数据

如果页面的数据是静态的,直接写到 Pagedata 中即可,但实际大部分情况是,页面一部分是前端就可以渲染的静态的结构与数据,另一部分是从后端接口获取的数据。从后端接口获取的首屏数据可以缓存到 storage 中,这样在第二次加载这个页面的时候可以从 storage 中获取,同时异步发起请求,请求返回后再更新页面数据。注意,我们是为了更快的展现页面,所以只缓存和加载首屏可见的数据即可,非首屏数据延迟加载


// 从storage中获取页面数据

swan.getStorage({

key: 'pageData',

success: res => {

// 如果有缓存且异步请求未返回则使用缓存的数据渲染页面

if (res.data && !this.requestBack) {

this.renderPage(data);

}

}

});

// 异步发起请求获取页面数据

getPageData().then(res => {

this.requestBack = true;

// 请求返回后根据最新数据渲染页面

this.renderPage(res.pageData);

// 同时缓存页面数据到storage中

swan.setStorage({

key: 'pageData',

data: res.pageData

});

});

这样做可能会带来一个问题,就是页面数据加载后并不一定是最新的数据,最新的数据从请求获取到后会刷新页面的数据。所以,如果你的应用对实时性的要求比较高的话可能并不适合使用这种方法。

第三板斧-轻渲染,只渲染必须的内容

在小程序加载过程中,逻辑代码和渲染代码是分离的,分别由不同的线程进行。

image
慢的线程会拖累整个加载的速度,当你的逻辑代码已经跑的飞起的时候,可以考虑下是否在渲染的层面有改进的办法。

减少对渲染有消耗的写法

小程序本身提供了丰富多彩的用法,包括自定义组件动态库filtersjs等等,这些功能提升了我们开发的效率,但另一方面,多种多样的功能有可能带来新的的性能消耗陷阱。你需要在效率和性能之间找寻一种平衡,有哪些用法提升的效率有限而带来的性能消耗却是不可忽视的?这需要结合自身业务的实践,但在 FMP 占比较高的页面,这些功能还是需要慎之又慎。

另外,也需要注意 减少view和text组件的特殊属性和事件 ,这是很容易忽视的一点,虽然单次使用带来的性能消耗有限,但是要用到 view 和 text 组件的地方太多了,架不住使用数量的上升带来质的改变。尤其是自定义组件中使用了低性能的写法,因为自定义组件可能会被用到多次(例如列表项,甚至可能会被用上百次上千次),低性能的自定义组件会带来成倍的性能消耗。


// 修改前 view 使用了 style 属性

<view style="height: 20rpx;">热门榜单</view>

// 修改后 view 使用了 class ,在 css 文件中写样式

.title {

height: 20rpx;

}

<view class="title">热门榜单</view>

分屏渲染

设想一下,当我们加载一个长度超过一个屏幕的列表时,其实用户不会看到列表的所有内容,只能看到列表的前几项,那么我们当然可以只加载列表的前几项,当用户滑动的时候再加载剩余的内容。同样的,在渲染页面的时候,我们也可以在第一次 setData 时进行数据的分割,只设置首屏可见的数据,延迟设置非首屏数据


// appList是从后端接口获取的页面数据 active是当前可见的tab索引

// firstLoadAppList为计算出的首屏幕数据

const firstLoadAppList = appList.map((item, index) => {

return index === active ? item.slice(0, 10) : [];

});

this.setData({

appList: firstLoadAppList

}, () => {

// 可将完整数据记录待之后加载

this.appList = appList;

});

取消骨架屏采用渐进式加载

骨架屏 是小程序提供的一种优化用户体验的机制,但其实任何渲染都有消耗,骨架屏也是。在骨架屏中写了复杂的结构甚至动画效果,反而不利于真正的有意义的页面快速的加载。当然,骨架屏确实可以让用户更快的感知到页面正在加载,所以需要在这之间寻找一种平衡,是需要用户先看到一个正在加载的页面,还是让用户更快的看到有意义的有内容的画面。推荐的一个方案是:

  • 使用官方提供的骨架屏,但简化骨架屏的框架,减少使用样式与动画效果
  • 在真正的页面渲染中,为各个部分设置背景色与高度,在 Pagedata 中设置默认值,在还未进行第一次 setData 的时候渲染出页面的框架。这样,当页面数据来了的时候,只是在特定的部分填充值即可。

后记

欢迎在 小程序开发者社区 中提问性能相关的问题,也欢迎在Github上 follow我,我会不定期更新一些前端相关的文章,如果想更深入的和我讨论小程序性能相关的问题,可以给我发邮件。

查看原文

赞 9 收藏 5 评论 2

全栈道路 赞了文章 · 2019-04-28

Vue微信公众号开发踩坑记录

需求

  • 微信授权登录(基于公众号的登录方案)
  • 接入JS-SDK实现图片上传,分享等功能

现状及难点

  • 采用的Vue框架,前后端分离模式(vue工程仅作为客户端),用户通过域名访问的是客户端,但是微信授权中涉及签名和token校验依赖服务端
  • JS-SDK需要向服务端获取签名,且获取签名中需要的参数包括所在页面的url,但由于单页应用的路由特殊,其中涉及到iOS和android微信客户端浏览器内核的差异性导致的兼容问题

解决方案

授权登录

本人将授权流程设计如下:
此处输入图片的描述
详细说明:

  1. 用户访问网站主域名
  2. vue客户端(domain/)接收请求,在路由解析前判断用户是否登录(比如检查cookie);
  3. 如果没有登录,则通过api获取微信授权地址,获取后跳转到微信授权页面;
  4. 用户确认授权,微信服务器发起回调请求,这时回调到服务器端(domain/api/xxx)
  5. 服务器端保存用户信息,进行注册登录操作(记录cookie),重定向到vue客户端(domain/)
  6. 重复第一步,授权登录成功

踩坑记录:
以下是另一个授权方案
此处输入图片的描述
其实如果只实现授权登录到话,这个方案是可以的,而且也很清晰,vue客户端单方面在服务器和微信服务器之间进行通信,微信服务器不能直接和服务器通信。这种方案的坑在于当微信授权回调时会携带一个code参数,该参数会污染vue路由导致ios上进行JS-SDK签名时失败(后续会具体描述这个问题)

JS-SDK签名

对于签名,官方是这么说的

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用

vue中路由有history和hash两种模式;在history模式下,理想的设计方案是,当进入到需要用到JS-SDK组件时,获取以下当前url(也就是通过 location.href.split(‘#’)[0]获得到的地址)传递到服务端进行签名,应该就没问题了,但是IOS获取的url并不是调用微信js的时候所在页面的地址,而是进入到网站第一个页面的地址。

网上查询到一个方案是针对ios设备进入页面时先将当前url记录下来,到授权页面时将记录的url传递给服务端进行签名。该方案经实践是可行的,妈妈再也不用担心我的网址很丑很难看啦。

另外一个方案就是使用hash模式,这种模式下,url永远都只是主域名地址,省去了传递url的烦恼,也没必要处理兼容,所以如果不建议路由中有#的话,该方案应该是首选方案。

这里还有一个深坑,那就是如果授权方案采用了上述中的vue客户端处理回调的方式,那么ios将永远无法签名成功,为什么呢,因为这种方案路由通常是这样子的:

http://domain.com/?code=xxxxxx&stat=#/xxx

这种路由中带了参数的url是没法签名校验成功的!
这种路由中带了参数的url是没法签名校验成功的!
这种路由中带了参数的url是没法签名校验成功的!
重要的事情得说三遍啊
在我另外一篇文章中对js-sdk签名做了更多的介绍,可以移步到vue微信公众号开发踩坑记录(2)

Coding

任何不上代码的吹逼都是耍流氓,这里笔者分享下在vue中具体怎么coding的。

微信授权登录

笔者在项目中使用的vue-router进行路由控制,使用了vuex记录用户登录信息,但是由于vuex中存储的内容在页面刷新后会丢失,所以服务端同时也写了用户登录状态到cookie中,vue中需要通过这两个条件进行登录判断,不多BB,直接看代码吧

// ... other code

router.beforeEach((to, from, next) => {
  if ((!VueCookie.get('user') && !store.state.userInfo)) {
    // 第一次访问
    console.log('授权登录')
    // 跳转到微信授权页面,微信授权地址通过服务端获得
    axios.post('/api/login').then(res => {
      var data = res.data
      if (data.code === 100) {
        window.location.href = data.data
      }
    })
  } else if (!store.state.userInfo) {
    // 刷新页面,获取数据存入vuex
    axios.get('/api/currentuser').then(res => {
      if (res.data.code === 100) {
        store.dispatch('setUserInfo', res.data.data)
        next()
      }
    })
    console.log('cookie生效期内登录')
    next()
  } else {
    // 已经登录
    console.log('已登录')
    next()
  }
})

//... other code

history模式下的JS-SDK签名

在入口文件中将当前url存入vuex

// ... other code
router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  // 处理jssdk签名,兼容history模式
  if (!store.state.url) {
    store.commit('setUrl', document.URL)
  }
  // ... other code

在需要获取签名的组件中获取并进行配置

// ... other code
created () {
      var sef = this
      var url = ''
      // 判断是否是ios微信浏览器
      if (window.__wxjs_is_wkwebview === true) {
        url = this.$store.state.url.split('#')[0]
      } else {
        url = window.location.href.split('#')[0]
      }
      this.$http.get('/api/jssdk?url=' + url).then(function (res) {
        sef.lists = res.data.data
        hmTools.wechact(sef.lists, sef) //js-sdk配置
      })
    }
// ...other code

结语

由于本人文笔一般,思维的表达估计不到位,见解也是浅尝辄止,所以如果看官您有疑惑的地方或者有歧义欢迎来和本人交流。为了方便大家互相沟通,本人也创建了一个vue公众号开发的qq群,欢迎加入和大家一起分享开发心得,qq群号:130903919

查看原文

赞 131 收藏 278 评论 80

全栈道路 收藏了文章 · 2019-01-14

基于vue-cli搭建多模块且各模块独立打包的项目

github地址

https://github.com/shuidian/v...
为了充分发扬拿来主义的原则,先放出github地址,clone下来即可测试运行效果。如果觉得还可以的话,请点star,为更多人提供方便。

背景

在实际的开发过程中,单页应用并不能满足所有的场景。传统单页应用所生成的成果物,在单个系统功能拆分和多个系统灵活组装时并不方便。举个例子,我们在A系统中开发了一个实时视频预览模块和一个gis模块,传统方式打包A系统,我们会生成一个静态资源包,这个包,包含了实时视频预览模块和gis模块在内的所有模块。那么,现在有一个新的系统要做,碰巧需要原来A系统中的gis模块,那么我们只能把A系统打包,然后拿过来,通过引入url的方式,只使用其中的一个模块。这样做的问题很明显,我只需要A系统中的一个模块,但是我要引入整个A系统的资源包,这显然不合理。那么我们的需求就是从这里产生的,如果我们在开发系统A时,能够按模块划分生成多份静态资源包,最终的成果物中,会有多个子目录,每个子目录可独立运行,完成一个业务功能。这样的话,我们有任何系统需要我们开发过的任何模块,都可以直接打包指定的模块,灵活组装。

场景分析

首先,这种方案不是完全适合任何场景的,在使用时,还需要注意鉴别是否适用于当前业务场景。下面分析一下这种方式的优缺点:

优点:
1、可与其他系统灵活组装
2、各个模块相互不受影响,所以不受框架和开发模式的制约
3、不同模块可以分开部署
4、后期维护风险小,可以持续的、稳定的进行维护(万一哪天vue/react/angular被淘汰了,不会受太大影响,每个模块分别迭代就好)

缺点:
1、各个模块有相互独立的资源包,那么如果有相同的资源引用,不能复用
2、模块的组装要依赖iframe,所以要对浏览器安全设置、cookie共享等问题进行单独处理
3、用iframe来包裹组件,组件所能控制到的范围就是其所在的iframe,当涉及到全屏的应用场景时,会比较麻烦
4、不同组件之间的通信比较麻烦

以上只是分析应用场景,下面重点讲解一下如何实现多模块独立打包。

我们的目标

vue-cli默认打包方式的成果物与我们修改后生成的成果物结构对比如下:

clipboard.png
clipboard.png

上图为默认配置下,打包成果物的目录结构,下图为我们修改配置后,打包成果物的目录结构

思路分析

我们最终输出的成果物是多个独立的目录,那么我们就应该区分这些模块,最好的方式就是每个模块的代码放在不同的目录中,所以我们需要在src中创建每个模块的目录。暂时我们以a,b,c三个模块为例,由于我们现在的项目是多模块的,每个模块都应该有独立的入口,所以我们修改src目录结构如下:

clipboard.png

其他目录你怎么命名,以及要不要都无所谓,主要是modules目录,我们会在下面创建若干个模块。原来的src下的main.js、index.html和app.vue已经没用了,可以删掉了。然后模块内的目录结构如下图所示:

clipboard.png

聪明的同学已经看出来了,这里其实就是跟原来的src下的main.js、index.html和app.vue一样的,只不过我们把main.js改成了index.js而已。那么如果模块内要使用路由、状态管理都可以根据自己的需求去配置了,如何配置就不在这里讨论了。

那么如何从这些模块开始,把项目最终编译成三个独立的静态资源包呢?简单来说,其实就是循环跑三次打包脚本,每次打包一个模块,然后修改一下文件输出路径,把编译好的文件输出到dist目录下的a,b,c目录中。这样最基本的模块分开打包功能就完成了,但是还有以下一些问题需要处理。

1、这样打出的包,各个模块彼此独立。如果有这些模块是在一个系统中使用的,那么应该把多个模块重复的东西抽取出来复用。
2、如果只需要系统中的部分模块,那么应该只打包需要的模块,并且把需要打包的模块之间的重复代码抽取出来复用。

问题解决方案

针对第一个问题:实质上只要把webpack配置成多入口的方式即可,这样在编译时webpack可以把模块之间的重复代码抽取出来,最终的成果物就是一个静态资源包加多个html文件。这种方式的成果物目录结构如下:

clipboard.png

针对第二个问题:其实跟第一个问题一样,只不过把webpack的入口配置成可变的就可以了,需要打包哪些模块,就把入口设置为哪些模块即可。这种方式的成果物目录如下(假设要打包a,c两个模块):

clipboard.png

修改webpack配置的详细步骤

第一步:增加build/module-conf.js用来处理获取模块目录等问题


var chalk = require('chalk')
var glob = require('glob')

// 获取所有的moduleList
var moduleList = []
var moduleSrcArray = glob.sync('./src/modules/*')
for(var x in moduleSrcArray){
  moduleList.push(moduleSrcArray[x].split('/')[3])
}
// 检测是否在输入的参数是否在允许的list中
var checkModule = function () {
  var module = process.env.MODULE_ENV
  // 检查moduleList是否有重复
  var hash = {}
  var repeatList = []
  for(var l = 0;l < moduleList.length; l++){
    if(hash[moduleList[l]]){
      repeatList.push(moduleList[l])
    }
    hash[moduleList[l]] = true
  }
  if(repeatList.length > 0){
    console.log(chalk.red('moduleList 有重复:'))
    console.log(chalk.red(repeatList.toString()))
    return false
  }
  let result = true
  let illegalParam = ''
  for (let moduleToBuild of module.split(',')) {
    if (moduleList.indexOf(moduleToBuild) === -1) {
      result = false
      illegalParam = moduleToBuild
      break
    }
  }
  if(result === false){
    console.log(chalk.red('参数错误,允许的参数为:'))
    console.log(chalk.green(moduleList.toString()))
    console.log(chalk.yellow(`非法参数:${illegalParam}`))
  }
  return result
}

// 获取当前要打包的模块列表
function getModuleToBuild () {
  let moduleToBuild = []
  if (process.env.NODE_ENV === 'production') {
    /* 部署态,构建要打包的模块列表,如果指定了要打包的模块,那么按照指定的模块配置入口
    *  这里有个特性,即使参数未传,那么获取到的undefined也是字符串类型的,不是undefined类型
    * */
    if (process.env.MODULE_ENV !== 'undefined') {
      moduleToBuild = process.env.MODULE_ENV.split(',')
    } else {
      // 如果未指定要打包的模块,那么打包所有模块
      moduleToBuild = moduleList
    }
  } else {
    // 开发态,获取所有的模块列表
    moduleToBuild = moduleList
  }
  return moduleToBuild
}

exports.moduleList = moduleList
exports.checkModule = checkModule
exports.getModuleToBuild = getModuleToBuild

第二步:增加build/build-all.js用来处理循环执行打包命令

const path = require('path')
const execFileSync = require('child_process').execFileSync;
const moduleList = require('./module-conf').moduleList || []

const buildFile = path.join(__dirname, 'build.js')

for( const module of moduleList){
  console.log('正在编译:',module)
  // 异步执行构建文件,并传入两个参数,module:当前打包模块,separate:当前打包模式(分开打包)
  execFileSync( 'node', [buildFile, module, 'separate'], {})
}

第三步:修改build/build.js增加MODULE_ENV参数,用来记录当前打包的模块名称,增加MODE_ENV参数,用来记录当前打包的模式

'use strict'
require('./check-versions')()
const chalk = require('chalk')

process.env.NODE_ENV = 'production'
// MODULE_ENV用来记录当前打包的模块名称
process.env.MODULE_ENV = process.argv[2]
// MODE_ENV用来记录当前打包的模式,total代表整体打包(静态资源在同一个目录下,可以复用重复的文件),separate代表分开打包(静态资源按模块名称分别独立打包,不能复用重复的文件)
process.env.MODE_ENV = process.argv[3]

// 如果有传参时,对传入的参数进行检测,如果参数非法,那么停止打包操作
const checkModule = require('./module-conf').checkModule
if (process.env.MODULE_ENV !== 'undefined' && !checkModule()) {
  return
}

const path = require('path')
const ora = require('ora')
const rm = require('rimraf')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

第四步:修改config/index.js的配置,修改打包时的出口目录配置、html入口模板的配置以及静态资源路径配置

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

const MODULE = process.env.MODULE_ENV || 'undefined'
// 入口模板路径
const htmlTemplate =  `./src/modules/${MODULE}/index.html`

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8086, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist', MODULE, 'index.html'),
    // 加入html入口
    htmlTemplate: htmlTemplate,
    // Paths
    // assetsRoot: path.resolve(__dirname, '../dist', MODULE),
    // 这里判断一下打包的模式,如果是分开打包,要把成果物放到以模块命名的文件夹中
    assetsRoot: process.env.MODE_ENV === 'separate' ? path.resolve(__dirname, '../dist', MODULE) : path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    // 这里的路径改成相对路径,原来是assetsPublicPath: '/',
    // assetsPublicPath: '/',
    assetsPublicPath: '',

    /**
     * Source Maps
     */

    productionSourceMap: true,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

第五步:修改webpack.base.conf.js的入口配置,根据传参,动态配置入口文件

entry() {
    // 初始化入口配置
    const entry = {}
    // 所有模块的列表
    const moduleToBuild = require('./module-conf').getModuleToBuild() || []
    // 根据传入的待打包目录名称,构建多入口配置
    for (let module of moduleToBuild) {
      entry[module] = `./src/modules/${module}/index.js`
    }
    return entry
  },

第六步:修改webpack.dev.conf.js的配置,增加多入口时webpackHtmlPlugin插件的配置,增加静态资源服务器的配置

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const moduleList = require('./module-conf').moduleList || []
// 组装多个(有几个module就有几个htmlWebpackPlugin)htmlWebpackPlugin,然后追加到配置中
const htmlWebpackPlugins = []
for (let module of moduleList) {
  htmlWebpackPlugins.push(new HtmlWebpackPlugin({
    filename: `${module}/index.html`,
    template: `./src/modules/${module}/index.html`,
    inject: true,
    chunks: [module]
  }))
}

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    },
    setup(app) {
      // 写个小路由,打开浏览器的时候可以选一个开发路径
      let html = `<html><head><title>调试页面</title>`
      html += `<style>body{margin: 0}.module-menu{float: left;width: 200px;height: 100%;padding: 0 8px;border-right: 1px solid #00ffff;box-sizing: border-box}.module-container{float: left;width: calc(100% - 200px);height: 100%}.module-container iframe{width: 100%;height: 100%}</style>`
      html += `</head><body><div class="module-menu">`
      for(let module of moduleList){
        html += `<a href="/${module}/index.html" target="container">${module.toString()}</a><br>`
      }
      html += `</div>`
      html += `<div class="module-container"><iframe data-original="/${moduleList[0]}/index.html" name="container" frameborder="0"></iframe></div>`
      html += `</body></html>`
      // let sentHref = ''
      // for(var module in moduleList){
      //   sentHref += '<a href="/'+ moduleList[module] +'/index.html">点我调试模块:'+ moduleList[module].toString() +'</a> <br>'
      // }
      app.get('/moduleList', (req, res, next) => {
        res.send(html)
      })
      // 访问根路径时重定向到moduleList
      app.get('/', (req, res, next) => {
        res.redirect('/moduleList')
      })
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
    // https://github.com/ampedandwired/html-webpack-plugin
    // new HtmlWebpackPlugin({
    //   filename: 'a/index.html',
    //   template: './src/modules/a/index.html',
    //   inject: true,
    //   chunks: ['a']
    // }),
  ].concat(htmlWebpackPlugins)
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
          ? utils.createNotifierCallback()
          : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

第七步:修改webpack.prod.conf.js的配置,增加对不同打包模式的处理。

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = process.env.NODE_ENV === 'testing'
  ? require('../config/test.env')
  : require('../config/prod.env')
// 获取所有模块列表
const moduleToBuild = require('./module-conf').getModuleToBuild() || []
// 组装多个(有几个module就有几个htmlWebpackPlugin)htmlWebpackPlugin,然后追加到配置中
const htmlWebpackPlugins = []
// 判断一下是否为分开打包模式
if (process.env.MODE_ENV === 'separate') {
  // 分开打包时是通过重复运行指定模块打包命令实现的,所以每次都是单个html文件,只要配置一个htmlPlugin
  htmlWebpackPlugins.push(new HtmlWebpackPlugin({
    filename: process.env.NODE_ENV === 'testing'
      ? 'index.html'
      : config.build.index,
    // template: 'index.html',
    template: config.build.htmlTemplate,
    inject: true,
    minify: {
      removeComments: true,
      collapseWhitespace: true,
      removeAttributeQuotes: true
      // more options:
      // https://github.com/kangax/html-minifier#options-quick-reference
    },
    // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    chunksSortMode: 'dependency'
  }))
} else {
  // 一起打包时是通过多入口实现的,所以要配置多个htmlPlugin
  for (let module of moduleToBuild) {
    htmlWebpackPlugins.push(new HtmlWebpackPlugin({
      filename: `${module}.html`,
      template: `./src/modules/${module}/index.html`,
      inject: true,
      // 这里要指定把哪些chunks追加到html中,默认会把所有入口的chunks追加到html中,这样是不行的
      chunks: ['vendor', 'manifest', module],
      // filename: process.env.NODE_ENV === 'testing'
      //   ? 'index.html'
      //   : config.build.index,
      // template: 'index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }))
  }
}

// 获取当前打包的目录名称
const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    /*
    new HtmlWebpackPlugin({
      filename: process.env.NODE_ENV === 'testing'
        ? 'index.html'
        : config.build.index,
      // template: 'index.html',
      template: config.build.htmlTemplate,
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    */
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ].concat(htmlWebpackPlugins)
})

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

第八步:修改package.json,增加npm run build-all指令

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "unit": "jest --config test/unit/jest.conf.js --coverage",
    "test": "npm run unit",
    "lint": "eslint --ext .js,.vue src test/unit",
    "build": "node build/build.js",
    "build-all": "node build/build-all.js"
  },

构建指令

npm run build // 打包全部模块到一个资源包下面,每个模块的入口是module.html文件,静态资源都在static目录中,这种方式可以复用重复的资源
npm run build moduleName1,moduleName2,... // 打包指定模块到一个资源包下面,每个模块的入口是module.html文件,静态资源都在static目录中,这种方式可以复用重复的资源
npm run build-all // 打包所有模块,然后每个模块彼此独立,有几个模块,就产生几个静态资源包,这种方式不会复用重复的资源

本文是在参考了以下文章的内容之后写的,在原有的基础上做了一些扩展,支持多入口模式
参考资料:https://segmentfault.com/a/11...

注意事项

鉴于评论区疑问比较多的地方,这里我统一解释一下:路由到底是怎么配的?
当项目模板clone到本地跑起来之后,你会看到这样的界面:

图片描述

在这个界面中,需要注意的两个点已经标注出来了。我们在使用时,需要在moduls目录下的子目录中配置前端路由,也只有这里的路由才是前端能够控制的。至于那个localhost:8086/a和localhost:8086/b以及localhost:8086/moduleList这几个路由地址都是用node服务器写的,跟前端没有关系,这里大家不要被误导。

注意事项完。

查看原文

全栈道路 赞了文章 · 2019-01-14

基于vue-cli搭建多模块且各模块独立打包的项目

github地址

https://github.com/shuidian/v...
为了充分发扬拿来主义的原则,先放出github地址,clone下来即可测试运行效果。如果觉得还可以的话,请点star,为更多人提供方便。

背景

在实际的开发过程中,单页应用并不能满足所有的场景。传统单页应用所生成的成果物,在单个系统功能拆分和多个系统灵活组装时并不方便。举个例子,我们在A系统中开发了一个实时视频预览模块和一个gis模块,传统方式打包A系统,我们会生成一个静态资源包,这个包,包含了实时视频预览模块和gis模块在内的所有模块。那么,现在有一个新的系统要做,碰巧需要原来A系统中的gis模块,那么我们只能把A系统打包,然后拿过来,通过引入url的方式,只使用其中的一个模块。这样做的问题很明显,我只需要A系统中的一个模块,但是我要引入整个A系统的资源包,这显然不合理。那么我们的需求就是从这里产生的,如果我们在开发系统A时,能够按模块划分生成多份静态资源包,最终的成果物中,会有多个子目录,每个子目录可独立运行,完成一个业务功能。这样的话,我们有任何系统需要我们开发过的任何模块,都可以直接打包指定的模块,灵活组装。

场景分析

首先,这种方案不是完全适合任何场景的,在使用时,还需要注意鉴别是否适用于当前业务场景。下面分析一下这种方式的优缺点:

优点:
1、可与其他系统灵活组装
2、各个模块相互不受影响,所以不受框架和开发模式的制约
3、不同模块可以分开部署
4、后期维护风险小,可以持续的、稳定的进行维护(万一哪天vue/react/angular被淘汰了,不会受太大影响,每个模块分别迭代就好)

缺点:
1、各个模块有相互独立的资源包,那么如果有相同的资源引用,不能复用
2、模块的组装要依赖iframe,所以要对浏览器安全设置、cookie共享等问题进行单独处理
3、用iframe来包裹组件,组件所能控制到的范围就是其所在的iframe,当涉及到全屏的应用场景时,会比较麻烦
4、不同组件之间的通信比较麻烦

以上只是分析应用场景,下面重点讲解一下如何实现多模块独立打包。

我们的目标

vue-cli默认打包方式的成果物与我们修改后生成的成果物结构对比如下:

clipboard.png
clipboard.png

上图为默认配置下,打包成果物的目录结构,下图为我们修改配置后,打包成果物的目录结构

思路分析

我们最终输出的成果物是多个独立的目录,那么我们就应该区分这些模块,最好的方式就是每个模块的代码放在不同的目录中,所以我们需要在src中创建每个模块的目录。暂时我们以a,b,c三个模块为例,由于我们现在的项目是多模块的,每个模块都应该有独立的入口,所以我们修改src目录结构如下:

clipboard.png

其他目录你怎么命名,以及要不要都无所谓,主要是modules目录,我们会在下面创建若干个模块。原来的src下的main.js、index.html和app.vue已经没用了,可以删掉了。然后模块内的目录结构如下图所示:

clipboard.png

聪明的同学已经看出来了,这里其实就是跟原来的src下的main.js、index.html和app.vue一样的,只不过我们把main.js改成了index.js而已。那么如果模块内要使用路由、状态管理都可以根据自己的需求去配置了,如何配置就不在这里讨论了。

那么如何从这些模块开始,把项目最终编译成三个独立的静态资源包呢?简单来说,其实就是循环跑三次打包脚本,每次打包一个模块,然后修改一下文件输出路径,把编译好的文件输出到dist目录下的a,b,c目录中。这样最基本的模块分开打包功能就完成了,但是还有以下一些问题需要处理。

1、这样打出的包,各个模块彼此独立。如果有这些模块是在一个系统中使用的,那么应该把多个模块重复的东西抽取出来复用。
2、如果只需要系统中的部分模块,那么应该只打包需要的模块,并且把需要打包的模块之间的重复代码抽取出来复用。

问题解决方案

针对第一个问题:实质上只要把webpack配置成多入口的方式即可,这样在编译时webpack可以把模块之间的重复代码抽取出来,最终的成果物就是一个静态资源包加多个html文件。这种方式的成果物目录结构如下:

clipboard.png

针对第二个问题:其实跟第一个问题一样,只不过把webpack的入口配置成可变的就可以了,需要打包哪些模块,就把入口设置为哪些模块即可。这种方式的成果物目录如下(假设要打包a,c两个模块):

clipboard.png

修改webpack配置的详细步骤

第一步:增加build/module-conf.js用来处理获取模块目录等问题


var chalk = require('chalk')
var glob = require('glob')

// 获取所有的moduleList
var moduleList = []
var moduleSrcArray = glob.sync('./src/modules/*')
for(var x in moduleSrcArray){
  moduleList.push(moduleSrcArray[x].split('/')[3])
}
// 检测是否在输入的参数是否在允许的list中
var checkModule = function () {
  var module = process.env.MODULE_ENV
  // 检查moduleList是否有重复
  var hash = {}
  var repeatList = []
  for(var l = 0;l < moduleList.length; l++){
    if(hash[moduleList[l]]){
      repeatList.push(moduleList[l])
    }
    hash[moduleList[l]] = true
  }
  if(repeatList.length > 0){
    console.log(chalk.red('moduleList 有重复:'))
    console.log(chalk.red(repeatList.toString()))
    return false
  }
  let result = true
  let illegalParam = ''
  for (let moduleToBuild of module.split(',')) {
    if (moduleList.indexOf(moduleToBuild) === -1) {
      result = false
      illegalParam = moduleToBuild
      break
    }
  }
  if(result === false){
    console.log(chalk.red('参数错误,允许的参数为:'))
    console.log(chalk.green(moduleList.toString()))
    console.log(chalk.yellow(`非法参数:${illegalParam}`))
  }
  return result
}

// 获取当前要打包的模块列表
function getModuleToBuild () {
  let moduleToBuild = []
  if (process.env.NODE_ENV === 'production') {
    /* 部署态,构建要打包的模块列表,如果指定了要打包的模块,那么按照指定的模块配置入口
    *  这里有个特性,即使参数未传,那么获取到的undefined也是字符串类型的,不是undefined类型
    * */
    if (process.env.MODULE_ENV !== 'undefined') {
      moduleToBuild = process.env.MODULE_ENV.split(',')
    } else {
      // 如果未指定要打包的模块,那么打包所有模块
      moduleToBuild = moduleList
    }
  } else {
    // 开发态,获取所有的模块列表
    moduleToBuild = moduleList
  }
  return moduleToBuild
}

exports.moduleList = moduleList
exports.checkModule = checkModule
exports.getModuleToBuild = getModuleToBuild

第二步:增加build/build-all.js用来处理循环执行打包命令

const path = require('path')
const execFileSync = require('child_process').execFileSync;
const moduleList = require('./module-conf').moduleList || []

const buildFile = path.join(__dirname, 'build.js')

for( const module of moduleList){
  console.log('正在编译:',module)
  // 异步执行构建文件,并传入两个参数,module:当前打包模块,separate:当前打包模式(分开打包)
  execFileSync( 'node', [buildFile, module, 'separate'], {})
}

第三步:修改build/build.js增加MODULE_ENV参数,用来记录当前打包的模块名称,增加MODE_ENV参数,用来记录当前打包的模式

'use strict'
require('./check-versions')()
const chalk = require('chalk')

process.env.NODE_ENV = 'production'
// MODULE_ENV用来记录当前打包的模块名称
process.env.MODULE_ENV = process.argv[2]
// MODE_ENV用来记录当前打包的模式,total代表整体打包(静态资源在同一个目录下,可以复用重复的文件),separate代表分开打包(静态资源按模块名称分别独立打包,不能复用重复的文件)
process.env.MODE_ENV = process.argv[3]

// 如果有传参时,对传入的参数进行检测,如果参数非法,那么停止打包操作
const checkModule = require('./module-conf').checkModule
if (process.env.MODULE_ENV !== 'undefined' && !checkModule()) {
  return
}

const path = require('path')
const ora = require('ora')
const rm = require('rimraf')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

第四步:修改config/index.js的配置,修改打包时的出口目录配置、html入口模板的配置以及静态资源路径配置

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

const MODULE = process.env.MODULE_ENV || 'undefined'
// 入口模板路径
const htmlTemplate =  `./src/modules/${MODULE}/index.html`

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8086, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist', MODULE, 'index.html'),
    // 加入html入口
    htmlTemplate: htmlTemplate,
    // Paths
    // assetsRoot: path.resolve(__dirname, '../dist', MODULE),
    // 这里判断一下打包的模式,如果是分开打包,要把成果物放到以模块命名的文件夹中
    assetsRoot: process.env.MODE_ENV === 'separate' ? path.resolve(__dirname, '../dist', MODULE) : path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    // 这里的路径改成相对路径,原来是assetsPublicPath: '/',
    // assetsPublicPath: '/',
    assetsPublicPath: '',

    /**
     * Source Maps
     */

    productionSourceMap: true,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

第五步:修改webpack.base.conf.js的入口配置,根据传参,动态配置入口文件

entry() {
    // 初始化入口配置
    const entry = {}
    // 所有模块的列表
    const moduleToBuild = require('./module-conf').getModuleToBuild() || []
    // 根据传入的待打包目录名称,构建多入口配置
    for (let module of moduleToBuild) {
      entry[module] = `./src/modules/${module}/index.js`
    }
    return entry
  },

第六步:修改webpack.dev.conf.js的配置,增加多入口时webpackHtmlPlugin插件的配置,增加静态资源服务器的配置

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const moduleList = require('./module-conf').moduleList || []
// 组装多个(有几个module就有几个htmlWebpackPlugin)htmlWebpackPlugin,然后追加到配置中
const htmlWebpackPlugins = []
for (let module of moduleList) {
  htmlWebpackPlugins.push(new HtmlWebpackPlugin({
    filename: `${module}/index.html`,
    template: `./src/modules/${module}/index.html`,
    inject: true,
    chunks: [module]
  }))
}

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    },
    setup(app) {
      // 写个小路由,打开浏览器的时候可以选一个开发路径
      let html = `<html><head><title>调试页面</title>`
      html += `<style>body{margin: 0}.module-menu{float: left;width: 200px;height: 100%;padding: 0 8px;border-right: 1px solid #00ffff;box-sizing: border-box}.module-container{float: left;width: calc(100% - 200px);height: 100%}.module-container iframe{width: 100%;height: 100%}</style>`
      html += `</head><body><div class="module-menu">`
      for(let module of moduleList){
        html += `<a href="/${module}/index.html" target="container">${module.toString()}</a><br>`
      }
      html += `</div>`
      html += `<div class="module-container"><iframe data-original="/${moduleList[0]}/index.html" name="container" frameborder="0"></iframe></div>`
      html += `</body></html>`
      // let sentHref = ''
      // for(var module in moduleList){
      //   sentHref += '<a href="/'+ moduleList[module] +'/index.html">点我调试模块:'+ moduleList[module].toString() +'</a> <br>'
      // }
      app.get('/moduleList', (req, res, next) => {
        res.send(html)
      })
      // 访问根路径时重定向到moduleList
      app.get('/', (req, res, next) => {
        res.redirect('/moduleList')
      })
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
    // https://github.com/ampedandwired/html-webpack-plugin
    // new HtmlWebpackPlugin({
    //   filename: 'a/index.html',
    //   template: './src/modules/a/index.html',
    //   inject: true,
    //   chunks: ['a']
    // }),
  ].concat(htmlWebpackPlugins)
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
          ? utils.createNotifierCallback()
          : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

第七步:修改webpack.prod.conf.js的配置,增加对不同打包模式的处理。

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = process.env.NODE_ENV === 'testing'
  ? require('../config/test.env')
  : require('../config/prod.env')
// 获取所有模块列表
const moduleToBuild = require('./module-conf').getModuleToBuild() || []
// 组装多个(有几个module就有几个htmlWebpackPlugin)htmlWebpackPlugin,然后追加到配置中
const htmlWebpackPlugins = []
// 判断一下是否为分开打包模式
if (process.env.MODE_ENV === 'separate') {
  // 分开打包时是通过重复运行指定模块打包命令实现的,所以每次都是单个html文件,只要配置一个htmlPlugin
  htmlWebpackPlugins.push(new HtmlWebpackPlugin({
    filename: process.env.NODE_ENV === 'testing'
      ? 'index.html'
      : config.build.index,
    // template: 'index.html',
    template: config.build.htmlTemplate,
    inject: true,
    minify: {
      removeComments: true,
      collapseWhitespace: true,
      removeAttributeQuotes: true
      // more options:
      // https://github.com/kangax/html-minifier#options-quick-reference
    },
    // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    chunksSortMode: 'dependency'
  }))
} else {
  // 一起打包时是通过多入口实现的,所以要配置多个htmlPlugin
  for (let module of moduleToBuild) {
    htmlWebpackPlugins.push(new HtmlWebpackPlugin({
      filename: `${module}.html`,
      template: `./src/modules/${module}/index.html`,
      inject: true,
      // 这里要指定把哪些chunks追加到html中,默认会把所有入口的chunks追加到html中,这样是不行的
      chunks: ['vendor', 'manifest', module],
      // filename: process.env.NODE_ENV === 'testing'
      //   ? 'index.html'
      //   : config.build.index,
      // template: 'index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }))
  }
}

// 获取当前打包的目录名称
const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    /*
    new HtmlWebpackPlugin({
      filename: process.env.NODE_ENV === 'testing'
        ? 'index.html'
        : config.build.index,
      // template: 'index.html',
      template: config.build.htmlTemplate,
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    */
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ].concat(htmlWebpackPlugins)
})

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

第八步:修改package.json,增加npm run build-all指令

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "unit": "jest --config test/unit/jest.conf.js --coverage",
    "test": "npm run unit",
    "lint": "eslint --ext .js,.vue src test/unit",
    "build": "node build/build.js",
    "build-all": "node build/build-all.js"
  },

构建指令

npm run build // 打包全部模块到一个资源包下面,每个模块的入口是module.html文件,静态资源都在static目录中,这种方式可以复用重复的资源
npm run build moduleName1,moduleName2,... // 打包指定模块到一个资源包下面,每个模块的入口是module.html文件,静态资源都在static目录中,这种方式可以复用重复的资源
npm run build-all // 打包所有模块,然后每个模块彼此独立,有几个模块,就产生几个静态资源包,这种方式不会复用重复的资源

本文是在参考了以下文章的内容之后写的,在原有的基础上做了一些扩展,支持多入口模式
参考资料:https://segmentfault.com/a/11...

注意事项

鉴于评论区疑问比较多的地方,这里我统一解释一下:路由到底是怎么配的?
当项目模板clone到本地跑起来之后,你会看到这样的界面:

图片描述

在这个界面中,需要注意的两个点已经标注出来了。我们在使用时,需要在moduls目录下的子目录中配置前端路由,也只有这里的路由才是前端能够控制的。至于那个localhost:8086/a和localhost:8086/b以及localhost:8086/moduleList这几个路由地址都是用node服务器写的,跟前端没有关系,这里大家不要被误导。

注意事项完。

查看原文

赞 58 收藏 49 评论 79

全栈道路 关注了用户 · 2018-03-21

SevenOutman @sevenoutman

有耐心,但气不过不讲道理的事。
GitHub 间歇活跃用户。
赛文奥特曼??

关注 6557

全栈道路 关注了用户 · 2018-03-21

hfhan @hfhan

砥砺前行

关注 19557

全栈道路 关注了用户 · 2018-03-21

剑心无痕 @jianxinwuhen

关注 8101

全栈道路 关注了用户 · 2018-03-21

大闲人柴毛毛 @daxianrenchaimaomao

关注 1443

认证与成就

  • 获得 0 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-10-21
个人主页被 129 人浏览