这个时候任何框架和模式都不好使,因为你不知道下一个需求是否会推翻前边所有的设计,这个时候我们要做的是使用最基本、高度扩展性的架构和思路来进行开发,避免因为需求变更导致的大量重复性工作。
技术最终是服务于业务的,没有最好的技术,只有最合适的技术。
剩下的是培养业务感觉和其他~
这个时候任何框架和模式都不好使,因为你不知道下一个需求是否会推翻前边所有的设计,这个时候我们要做的是使用最基本、高度扩展性的架构和思路来进行开发,避免因为需求变更导致的大量重复性工作。
技术最终是服务于业务的,没有最好的技术,只有最合适的技术。
剩下的是培养业务感觉和其他~
随着系统应用越来越多,我们对系统进行了微前端的改造,同时改造带来一个组件复用的问题需要解决,经过调研,最终还是决定使用npm包的形式进行公共组件的开发维护工作
组件库组件分两类:业务组件和基础组件
为了保证与迁移之前使用方式一致,还对部分组件进行了写法上的调整
1 | npm i pms_components |
1 | import PmsComponents from 'pms_components' |
1 | vue-virtual-scroller |
基础组件的使用可以直接像上边的方式去用,通过引入pms_components和css即可,也可以进行按需引用
基础组件就是不依赖项目环境的组件,他可以放在任意的项目中去使用,使用方法比较简单。EmptyLayout 组件–空页面
ImportModal 组件–导入弹层
FwAffixTable 组件–表格
FwBadge 组件–徽章
FwBreadcrumb 组件–面包屑
FwFlexCard 组件–卡片展示
FwRadioTab 组件–标签选项卡
FwSteps 组件–步骤展示
InputNumberWithAddon 组件–数字带后缀
ImageHolder 组件–图片展示
ImageViewer 组件–图片查看组件
TitleAndOperate 组件–标题隔离
LongTextPopover 组件–长文本弹窗
业务组件的使用稍微麻烦些,由于它依赖项目的运行环境,这里主要是请求中的token等信息,所以需要在项目中
重新定义一下对应的组件,在我们的项目中fpms_app_template有这几个的基本示例代码,我们将业务组件进行
简单的二次包装,同时将项目的request方法默认传入组件,这样不同项目请求方式就是统一的了。
1 | import FwCascader from 'pms_components/packages/FwCascader/FwCascader.vue' |
这里主要作用是把项目的request方法传进组件,使用具体项目的请求方式。
LocationCascader 组件–位置选择组件
UnionLocationCascade 组件–组合位置选择
RegionCascader 组件–地区选择组件
upload 组件–上传
FwCascader 组件–房屋选择
基础组件会打包,业务组件不打包,业务组件打包时间会很长也是个问题,通过区别基础组件和业务组件的使用方式, 可以避免这一情况
打开pms_components项目
控制台输入npm login,输入帐号和密码即可登录
1 | npm run uv # 增加版本号 |
新组件开发完,在fpms_components中增加示例代码
同时确保在项目fpms_app_template中使用没问题的情况下再发布,可以直接吧libs目录放在fpms_app_template的node_modules对应的包中测试
每个应用都对应一个镜像,这个镜像有版本的概念,每次打包都是创建一个新版本
more >>K8S存在集群的概念,不同环境可以有不同的集群,比如预发布环境可以有3台机器和生产环境可以有7台机器
mpvue npm run dev 报错Error: getaddrinfo ENOTFOUND localhost解决办法
打开hosts文件 添加127.0.0.1 localhost
主应用===基座应用;子应用===需要运行在主应用下的应用,同时支持独立运行
- 代码库更小,更内聚、可维护性更高
- 松耦合、自治的团队可扩展性更好
- 渐进地升级、更新甚至重写部分前端功能成为了可能
- 可以独立开发部署
- 独立开发运行部署。
- 子应用的路由配置对应基座应用的路由配置上,以此方式来复用头部和侧边菜单。
- 状态之间的传递,原则上是通过基座应用来进行的。
- package.json 中的name需要修改
- src/router/index.js 中
base: window.__POWERED_BY_QIANKUN__ ? '/microapp' : '/',
中的microapp需要修改,他表示在基座应用中的第一级子目录。- 同时’microapp’在主应用中也会有对应的地方:比如我们把它放在了AppList.js这个文件中,这里边是
对子应用的配置
1
2
3
4
5
6
7
8 export const AppList = [
{
name: 'microapp',
entry: '//localhost:10001',
container: '#appContainer',
activeRule: '/microapp/'
}
]
路由配置在主应用要有一套,同时操作权限的配置在主应用下生效,独立运行不
需要生效,因为我们的权限相关配置原则上只在主应用下使用,如果独立查看子
应用,正常情况有权限看即意味着有权限看全部页面,否则维护两套环境的资源
和操作权限维护成本会非常高,非必要不建议这么去做。主应用路由配置事例:fpms_pro/src/config/modules/MicroAppTest.js
如果要查看demo,url增加参数
debug=1即可开发独立项目时,需复制
fpms_app_template项目,然后需要做这么几步:package.json中name重新命名,例如:
fpms_app1
/src/config/router.config.js中的路由,根据业务情况进行修改修改项目运行端口10001到其他端口,例如:10002,只要保证主应用中已经加载的子应用没用过即可
然后再说下主应用中AppList.js需要添加的配置:
1
2
3
4
5
6 {
name: 'app1',
entry: '//localhost:10002',
container: '#appContainer',
activeRule: '/app1/'
}
- 虽然当前架构支持任意开发语言进行子应用的开发,但这里还是建议使用相同的技术栈,便于后期维护
- 子应用在独立运行时会展示自己的头部和左侧菜单,运行在基座应用中头部和左侧菜单不展示
- 状态的传递通过基座应用,通过对基座应用某个状态的监听,在子应用中处理对应的逻辑,例如:groupItem
相关的配置在main.js的mount函数下。实际情况并不建议应用之间进行过多的数据传递,如果确实非常多,理论上应该是同一个应用。- 关于代码复用,当我们有精力可以构建并维护自己的组件库的时候,可以使用组件库的方式来对公共组件进行复用
- 关于什么情况下使用子应用的方式进行开发部署;由于拆分成子应用也同样会带来维护上的成本,因此目前的拆分原则是根据团队进行拆分,例如一个独立团队对相对独立的模块进行开发;当然如果后期已有模块需要重构,则也可以使用这种方式进行,因为这也是微前端架构的一个优势。
- 子应用应该有自己的域名,因此逻辑上是可以独立运行上线使用的,如果没有域名,可以通过path在nginx上做相关配置。
我们目前支持两种部署方式
首先,子应用有自己的独立域名,并且支持https,例如:
- microapp.pms.gmtech.top
独立域名这种部署方式有些必要条件:
- 资源需要支持CORS跨域请求,配置nginx
- 域名支持https访问。
相关需要调整文件的地方:
- 主应用里AppList.js中的entry的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 {
name: 'microapp',
entry: getAppLoadUri('microapp', 10001),
container: '#appContainer',
activeRule: '/microapp/'
}
entry获取合适的域名地址,这里方法为:
const env = process.env.NODE_ENV
function getAppLoadUri (appName, port) {
const hostName = location.hostname
const protocol = location.protocol
if (env === 'local') {
return `http://localhost:${port}/${appName}/`
} else if (env === 'development' || env === 'test') {
return `/${appName}/`
} else {
return `${protocol}//${appName}.${hostName}/${appName}/`
}
}
- 子应用中vue.config.js中,线上访问需要将静态资源上传到CDN上
1 const publicPath = process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true' ? 'https://pms-static.gmtech.top/fe/src/fpms_microapp/dist/' : '/microapp/'
- 子应用中router/index.js中
1 base: window.__POWERED_BY_QIANKUN__ ? '/microapp' : '/'
- 注意这几个地方的写法,独立部署和通过path部署是不一样的。
- nginx 配置与单页应用类似,但需要支持cors配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 server {
listen 80;
server_name pms-microapp-dev.gmtech.top;
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers Token,groupid,app,appid,projectid,project_id,Project-Id,appcode,Content-Type,Upgrade,Connection,X-Real-IP,Company-Id,companyID;
add_header Access-Control-Max-Age 86400;
if ($request_method = OPTIONS){
return 200;
}
location / {
root /home/work/www/fe/fpms_microapp/dist;
proxy_set_header Host $host;
index index.html;
try_files $uri $uri/ /index.html;
}
}
如果每个项目都要有独立域名才能使用的话,目前的微前端架构实际上是有缺陷的,因为我可能是想把一个系统
的多个模块进行拆分,方便维护,因此,通过path来区分子项目也应该支持:由于这种方式使用的是相同的域名
实际操作起来会节省掉很多由于资源跨域导致的无法加载的问题。也更适合把一个独立的大项目拆分成小应用的
场景。相关需要调整文件的地方:
- 主应用里AppList.js中的entry的写法:
1 entry: getAppLoadUri('microapp', 10001),
- 子应用中vue.config.js中,线上访问需要将静态资源上传到CDN上
1 const publicPath = process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true' ? 'https://pms-static.gmtech.top/fe/src/fpms_microapp/dist/' : '/microapp/'
- 子应用中router/index.js中base始终保持’/‘会比较合适,因为子应用与子应用之间的跳转需要使用path方式,如果配置了base则跳转会出现问题。
- 注意这几个地方的写法,path部署和通过域名部署是不一样的。
- nginx配置方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 server {
listen 80;
server_name pms-dev.gmtech.top;
gzip on; #开启gzip压缩功能
gzip_static on; #这个地方很重要,配置了vue打包gz文件以后可以直接使用,无需nginx再执行打包动作,节省服务器资源
gzip_min_length 1k; #设置允许压缩的页面最小字节数 vue打包配置需要生成.gz文件的地方也是1k
gzip_buffers 4 16k; #设置压缩缓冲区大小,此处设置为4个16K内存作为压缩结果流缓存
gzip_http_version 1.0; #http协议版本 默认1.1,实际上我们用的1.0,如果不配置gzip不生效
gzip_comp_level 2; #设置压缩比率,最小为1,处理速度快,传输速度慢;9为最大压缩比,处理速度慢,传输速度快
gzip_types text/css text/xml application/javascript; #制定压缩的类型
gzip_vary on; #选择支持vary header;改选项可以让前端的缓存服务器缓存经过gzip压缩的页面
location / {
root /home/work/www/fe/fpms_pro/dist;
proxy_set_header Host $host;
index index.html;
try_files $uri $uri/ /index.html;
location = /index.html {
add_header Cache-Control "no-cache, no-store";
}
}
location /microapp{
alias /home/work/www/fe/fpms_microapp/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}有了这些内容,我们想如何进行一个项目的开发都变得非常灵活
目前我们的线上环境使用的是独立域名部署方式,因为在线上是部署在K8S上,不同子应用需要能够独立运行,所以这里必须给每个子应用一个独立的域名。
但是为了兼顾便捷性,我们在开发环境和测试环境使用的是path方式,这样能够节省维护子域名的成本。
本地开发使用localhost,dev环境和test环境使用path配置,线上和预发布环境使用子域名。
项目参考乾坤框架:https://qiankun.umijs.org/zh/guide
在保证业务需求的前提下,使用渐进式的改造方案,避免因为改造造成项目的大量修改
微前端的前提,还是得有主体应用,然后才有微组件或微应用,解决的是可控体系下的前端协同开发问题(含空间分离带来的协作和时间延续带来的升级维护)
最近写的项目,应用里所有的ajax请求都发送了2遍。由于新项目,基础模块是新搭的,所以出现一些奇葩问题也是意料之中,啊终于第一次在chrome的devTools遇见了活的options请求。
这里首先发送了一次额外的options请求,在浏览器里看到请求request header 和 response header的信息如下:
(1)预检请求头request header的关键字段:
服务器基于从预检请求头部获得的信息来判断,是否接受接下来的实际请求。
(2)预检响应头response header的关键字段:
此次OPTIONS请求返回了响应头的内容,但没有返回响应实体response body内容。
这是本来要发送的请求,如图所示是普通的post请求。其中Content-Type的application/json是此次和后端约定的请求内容格式,这个也是后面讲到为什么会发送options请求的原因之一。
从很多资料我们可以了解到使用OPTIONS方法对服务器发起请求,可以检测服务器支持哪些 HTTP 方法。但是这次我们并没有主动去发起OPTIONS请求,那OPTIONS请求为何会自动发起?
MDN的CORS一文中提到:
规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
所以这个跨域请求触发了浏览器自动发起OPTIONS请求,看看此次跨域请求具体触发了哪些条件。
由于修改了Content-Type为application/json,触发了CORS预检请求。
可见一旦达到触发条件,跨域请求便会一直发送2次请求,这样增加的请求数是否可优化呢?答案是可以,OPTIONS预检请求的结果可以被缓存。
Access-Control-Max-Age这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存的最长时间,单位是秒。(MDN)
如果值为 -1,则表示禁用缓存,每一次请求都需要提供预检请求,即用OPTIONS请求进行检测。
评论区的朋友提醒了,尽量避免不要触发OPTIONS请求,上面例子中把content-type改掉是可以的。在其他场景,比如跨域并且业务有自定义请求头的话就很难避免了。现在使用的axios或者superagent等第三方ajax插件,如果出现CORS预检请求,可以看看默认配置或者二次封装是否规范。
OPTIONS请求即预检请求,可用于检测服务器允许的http方法。当发起跨域请求时,由于安全原因,触发一定条件时浏览器会在正式请求之前自动先发起OPTIONS请求,即CORS预检请求,服务器若接受该跨域请求,浏览器才继续发起正式请求。
作者:熊也抱抱
链接:https://juejin.im/post/6844903821634699277
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
先说结论就是环境变量配置的又问题:
肯定是这个配置:
1 | export PUB_HOSTED_URL=https://pub.flutter-io.cn |
为什么还是不行的原因就是我安装了zsh,.bash_profile配置了不好使,于是需要在.zshrc里也配置上才行。
事件循环与任务队列是JS中比较重要的两个概念。这两个概念在ES5和ES6两个标准中有不同的实现。尤其在ES6标准中,清楚的区分宏观任务队列和微观任务队列才能解释Promise一些看似奇怪的表现。
事件循环是什么?为什么要有事件循环这个东西?我们都知道JS是单线程的,但是像Ajax,或是DOM事件这种很耗时的操作,需要用并发处理,否则单线程会长时间等待,什么也做不了。而单线程循环就是并发的一种形式,一个线程中只有一个事件循环。而任务队列是用来配合事件循环完成操作的,一个线程可以拥有多个任务队列。
任务队列是什么?故名思意,排着任务的队列。所谓任务是WebAPIs返回的一个个通知,让JS主线程在读取任务队列的时候得知这个异步任务已经完成,下一步该执行这个任务的回调函数了。主线程拥有多个任务队列,不同的任务队列用来排列来自不同任务源的任务。任务源是什么?像setTimeout/Promise/DOM事件等都是任务源,来自同类任务源的任务我们称它们是同源的,比如setTimeout与setInterval就是同源的。在ES6标准中任务队列又分为宏观任务队列和微观任务队列,我们后边再详细讨论。
下面先通俗的讲述一下ES5中事件循环到底是怎么循环的,如图(据阮一峰前辈的教程):
图中有三大块:
一个具体点的栗子。比如现在打开了一个页面,里边有一段script,其中有Ajax,DOM操作等等。这段JS是在浏览器提供的全局环境(浏览器中是window)里执行的,执行中遇到函数调用时会压入执行栈。
ES6标准中任务队列存在两种类型,一种就是上边提到的一些队列,比如setTimeout、网络请求Ajax、用户I\O等都属于宏观任务队列(macrotask queue),另一种是微观任务队列(microtask queue),Promise就属于微观任务队列。
添加了微观任务队列之后事件循环有什么变化呢?在执行栈执行的过程中会把属于微观任务队列的任务分配到相应的微观任务队列中去。而在调用栈执行空之后,主线程读取任务队列时,会先读取所有微观任务队列,然后读取一个宏观任务队列,再读取所有的微观任务队列。如图:
好了,说了很多概念上的东西,不如一段代码来的清晰:
1 | setTimeout(function(){console.log(4)},0); |
读取所有微观任务队列 -> 执行 ->
读取一个宏观任务队列 -> 执行 ->
读取所有微观任务队列 -> 执行 ->
再读取一个宏观任务队列…的顺序。
所以最后的输出顺序是1,2,3,5,4,而不是1,2,3,4,5。如果不清楚微观任务队列的执行机制,很容易将两个异步任务归为一类,将执行顺序判断错误。
到这里算是把事件循环和任务队列说的比较清楚了,参考了很多大佬的博客与讨论:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://www.zhihu.com/question/36972010
http://www.jianshu.com/p/12b9f73c5a4f
http://www.cnblogs.com/hity-tt/p/6733062.html
如果你有不同的理解请到博客下方留言,这是我的github,欢迎来访,你的star就是我的动力。
作者:空_城__
链接:https://www.jianshu.com/p/4516ad4b3048
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
JS中异步编程的方法有:
回调是异步编程中最基础的方法。举例一个简单的回调:在f1执行完之后再执行f2
1 | var func1=function(callback){ |
异步回调中最常见的形式可能就是Ajax了:
1 | $.ajax({ |
通过事件机制,实现代码的解耦。js处理DOM交互就是采用的事件机制,我们这儿只是实现一些自定义的事件而已。JS中已经很好的支持了自定义事件,如:
1 | //新建一个事件 |
在系统中存在一个”信号中心”,当某个任务执行完成后向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。简单实现如下:
1 | //发布-订阅 |
其中存储消息的结构用json可以表示为:
1 | topics = { |
消息池的结构是发布订阅模式与事件监听模式的最大区别。当然,每个消息也可以看做是一个个的事件,topics对象就相当于一个事件处理中心,每个事件都有各自的订阅者。所以事件监听其实就是发布订阅模式的一个简化版本。而发布订阅模式的优点就是我们可以查看消息中心的信息,了解有多少信号,每个信号有多少订阅者。
很多情况下,我们都将观察者模式和发布-订阅模式混为一谈,因为都可用来进行异步通信,实现代码的解耦,而不再细究其不同,但是内部实现还是有很多不同的。
整体模型的不同:发布订阅模式是靠信息池作为发布者和订阅者的中转站的,订阅者订阅的是信息池中的某个信息;而观察者模式是直接将订阅者订阅到发布者内部的,目标对象需要负责维护观察者,也就是观察者模式中订阅者是依赖发布者的。
触发回调的方式不同:发布-订阅模式中,订阅者通过监听特定消息来触发回调;而观察者模式是发布者暴露一个接口(方法),当目标对象发生变化时调用此接口,以保持自身状态的及时改变。
观察者模式很好的应用是MVC架构,当数据模型更新时,视图也发生变化。从数据模型中将视图解耦出来,从而减少了依赖。但是当观察者数量上升时,性能会有显著下降。我们同样可以自己实现:
1 | //观察者模式 |
为解决回调函数噩梦而提出的写法,将回调函数的横向加载变成纵向加载。
对象状态不受外界影响。三种状态:pending,resolved,rejected。只有异步操作的结果才能改变状态
状态一旦改变,就不会再变。
用Promise对象实现Ajax操作的例子
1 | var getJSON=function(url){ |
再举一个需要多层回调的例子:假设每个步骤都是异步,并且依赖上一个步骤的结果,使用setTimeout来模拟异步操作。
1 | //输入n,表示该函数执行时间,结果为n+200,并且用于下一步的输入 |
如果使用Promise的方式将其3个步骤处理为链式操作,每一步都返回一个promise对象,将输出的结果作为下一步新的输入:
1 | function dolt(){ |
实际耗时跟我们计算的延迟时间300+500+700=1500ms差不多。但是对于长的链式操作来说,看起来是一堆then方法的堆砌,代码冗余,语义也不清楚,而且还是靠着箭头函数才使得代码略微简短一些。Promise还有一个痛点,就是传递参数太麻烦,尤其是需要传递多参数的情况下。
generator是一个封装的异步任务,在需要暂停的地方,使用yield语句注明。如
1 | function* gen(x){ |
调用generator函数返回的是内部的指针对象,调用next方法就会移动内部指针。Generator函数之所以能被用来处理异步操作,因为它可以暂停执行和恢复执行、函数体内外的数据交换和错误处理机制。
针对前面多任务的例子,使用generator实现:
1 | function* dolt(){ |
但是 Generator 函数的执行必须靠执行器
1 | function spawn(genF) { |
async函数基于Generator又做了几点改进:
很多人都认为这是异步编程的终极解决方案,由此评价就可知道该方法有多优秀了。它基于Promise使用async/await来优化then链的调用,其实也是Generator函数的语法糖。 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
await得到的就是返回值,其内部已经执行promise中resolve方法,然后将结果返回。使用async/await的方式重写前面的回调任务:
1 | async function dolt(){ |
功能还很新,属于ES7的语法,但使用Babel插件可以很好的转义。另外await只能用在async函数中,否则会报错。
作者:RichardBillion
链接:https://www.jianshu.com/p/6f91e7696b91
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true