2020年11月23日 By mikel 分类: JavaScript

来源: Datav超炫酷的可视化,大屏数据展示组件库-dataV组件库,“react-vue -组件库”_@baby张~的博客-CSDN博客

简单demo ,文尾有github demo地址

对可视化有所了解的应该都知道,某云平台的一款datav大屏可视化的工具,作者前年买了个个人版的,直接在页面上拖住就可以了确实很强。最近平台发来邮件,说我的个人版datav到期了,本来想续租,发现之前的个人版下架了,只剩下企业版、专业版、至尊版,最便宜的企业版一年4800,看着这价钱我慌了,够我吃一年小龙虾的了,不香嘛。果断放弃续费。。。

于是乎,就发现了这款github刚出来几个月的组件库。datav

如他所言:开源长期维护, 目前支持vue 、react、npm

所以组件都有vue和react版可以切换,很贴心。

svg绘制的特效,也就避免大屏幕缩放失真的问题

官方效果图


看着还是有点东西的,那么我们来实际操作一波看看。

自己动手撸

创建一个react或者vue项目

npm install @jiaminghi/data-view
或 yarn add @jiaminghi/data-view
  • 1
  • 2

也支持cdn

<!--调试版-->
  <script src="https://unpkg.com/@jiaminghi/data-view/dist/datav.map.vue.js"></script>
<!--压缩版 生产版本-->
  <script src="https://unpkg.com/@jiaminghi/data-view/dist/datav.min.vue.js"></script> 
  • 1
  • 2
  • 3
  • 4

安装完引入我们需要用到的组件,dom里直接使用组件

注意:它有react和vue版本,使用时别忘了切换

import { BorderBox1 ,BorderBox8 ,BorderBox13,Decoration1 ,ScrollBoard,ScrollRankingBoard } from '@jiaminghi/data-view-react'

 <BorderBox8>
  <div className="xpanel">
    <div className="fill-h" id="mainMap3"></div>
  </div>
 </BorderBox8>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

还可以配合其他的图表使用,我这里还用了echarts

最后实现的就是主图展示的

github Demo地址:https://github.com/babybrotherzb/my-datav

实际应用,升级版数据大屏项目

核心:Datav+G2+L7+Canvas+手写翻牌器
在这里插入图片描述
面朝科技-欢迎你的加入:https://www.mianchao.com/

2020年11月22日 By mikel 分类: 互联网, 建站经验

来源: 短视频平台常人不知道赚钱的方法 – 卢松松博客

2020年是短视频的“元年”,为何说是元年呢?不论是从用户增长量 、视频带货金额 还是 商业影响力 都是达到前所未有的新高峰。

短视频平台也造就了众多 百万、千万富翁。

今天和大家一起探讨短视频平台常人不知道的赚钱方法,当然是从我个人的视角看到的赚钱方法,甚至背后还有更好的赚钱模式和渠道,大家自行研究,一起讨论。

01 搬运赚钱是最简单的操作方法

直接搬运别人质量较高的视频,若有编辑能力的在其基础上进行二次编辑伪原创视频;从A平台发布到B平台,不需要真人出境,也不需要才艺展示,只需要每天搬砖。

但是就是这个简单的动作却难道了广大网友,诸不知所谓赚钱秘籍往往是简单的动作重复上千次,最后达到不错的效果。

搬运也分2种方式:

1、从国外网站如 y t b 上找到质量高的视频搬运到国内各个短视频平台,此时可以一对多发布(一个视频发布到多个视频平台)。

搬运也不能一股脑的搬运,在筛选视频的时候最好也是注重垂直的领域,如做健身项目,那就搬运健身类视频,以此类推。

2、国内的视频A平台搬运到国内视频B平台,这里需要注意下版权问题,最近几年国家也非常重视版权问题。

这点务必大家在心中树立个锚点,你的账号做大了,自然是“人红是非多”,侵权类的投诉,有时候对一个自媒体人来说是致命的打击,如果避免这个问题呢?

A:有条件的可以翻拍较火的视频,这就没有侵权一说。

B:使用的字体、图片、音乐等基本素材建议使用视频平台官网提供的,比如 抖音提供的那些字体或者是 背景音乐bgm

C: 搬运的视频为基础,在基础上进行二次加工二次剪辑,形成新的作品

D:可以直接给原作者发邮件,告知视频被引用,如若不同意,就及时删除。

E:抄死人已过了版权保护期的作品,或者是很久很久之前的量级小、没啥影响力的个人创作者作品。

组建工作室批量操作,通过流水线 搬运视频,得到的利润也是成倍的增长。

02 吹嘘、炫富、吹牛+带你玩

方法二:吹嘘、炫富、吹牛+带你玩

这类视频一般是 通过这样方式:豪车豪宅、珠宝奢饰品、银行余额等手段进行吹嘘炫富,并且在视频里告诉你,吹嘘自己通过某某项目或者产品发家致富,点赞或者私聊的带大家一起致富。

揭秘下:视频中的豪车大多是劳斯拉斯,为什么是劳斯拉斯?

因为这类劳斯拉斯 都产自山东,没人知道中国拥有多少辆劳斯莱斯,一些全球限量10辆的车,只在短视频平台里就能找出几十辆。

全新车也不过30w,二手或者租赁的更是亲民价格,大家有兴趣可以去深入了解下。

短视频平台常人不知道赚钱的方法 创业 短视频 经验心得 第1张

奢饰品、银行余额 都有配套的道具,演绎的逼真

此类玩法投入成低,主要是能引起讨论和轰动的效果,越是俗的东西,大家就越相信。大家自以为自己是“眼见为实,真金白银” ;而且勾引起看视频者一夜暴富的美梦,诸不知是 昙花一现。

你加了微信会告诉你:手把手指导+货源提供,提供各类文案,本质上是发展帮他免费卖货的人,一本万利。

或者是卖各类 教程的,通过大量的收益截图让你加入,告诉你就是下一位富翁。

03 情感专家+婚姻挽留+ 各类专家

这里视频号受众群体大,根据去年中国男女比例的数据显示,光棍人数高达3000多万。

今年肯定会更多,再加上2019年中国离婚率世界排名第一,所以情感咨询、恋爱项目的市场很大,有超级多细分的项目可以做。

短视频平台常人不知道赚钱的方法 创业 短视频 经验心得 第2张

成交一单少则上千元,多则几万是常态,这个行业的收费标准是:年收入的10%左右,比如你个人感情出现了问题,咨询付费金额:你一年收入的10%

而且根据内部人士透露:咨询之前把责任都说明白了,不能100%成功,正常流程是:

先让你回忆2人的美好时光 + 自身这段时间的检讨和错误认识 + 再从情感高度进行概述(他已经不爱你了,你何必苦恼;女人的成功是引爆男人,这个男人不值得你去付出….)

这类视频从业人员有的甚至是没有结婚,没有结婚,大谈“育儿经”,甚至是刚从田里干活回来,洗漱下,就是视频中的 感情专家了。

总结:短视频的赚钱方法,还是要走 “人间正道”更持久,玩视频赚钱请记住三点:

1、如果找不到赚钱方法,就是找同行,同行是最好的老师,看他们怎么操作的?

2、有自己的产品(可以是实物、虚拟商品、服务等)找到合适的货源,下面就是积攒客户数量。

3、怎么样提高效率,如何让更多的人知道你的产品,并展示你卖这个商品已经多年了,解决用户的信任;具体大家根据自己情况因人而异。

04 总结

上面表达的意思是:

我们处于这样的视频风口,即使不能飞,那至少能加速我们赚钱的脚步。

本文只是抛砖引玉,有更好的赚钱方法请在下方评论区评论,或者你有更好的赚钱项目分享出来,让大家的钱,越来也多。

作者:王启 公众号:王启说说

短视频平台常人不知道赚钱的方法 – 卢松松博客已关闭评论
2020年11月21日 By mikel 分类: 互联网, 建站经验, 网络营销

来源: 深度解析:什么样的行业,未来赚钱最多? – 卢松松博客

深度解析:什么样的行业,未来赚钱最多? 互联网 网络营销 好文分享 第1张

一位草根出身,37岁财务自由的前辈,3年前给出的答案至今依然有效。

这篇文章约2000字,不会假大空的绕弯子,绝对值得你花几分钟认真看完。

先讲五大判断原则,再讲三种行业类型。

首先讲五大原则,以下这5条,满足的越多赚的就越多。

一、需求戒律

商业的基础建立在用户的需求之上,有巨大需求的市场才是好市场,能解决市场需求的业务才会成功。

如果你能够帮助一百万人解决一个问题,那么赚一百万对你来说就是小菜一碟。

90%的业务之所以会失败或者做不大,就是因为业务本身市场需求太小。

不要为了赚钱而去“做你自己想做的事情”,这样的结果通常不太好,我们想做的事情往往市场需求不大。

并且,已经有非常多的人在“做他们自己想做的事”,导致市场已经非常拥挤,利润很小,竞争异常激烈。

二、进入戒律

你做的事情要有进入壁垒,而不是说随便什么公司,什么人都能够马上着手开始做。

高进入壁垒意味着这条路更强和更有力,竞争也更小,对杰出能力的需求也更少。

低进入壁垒的生意不是一条可依赖的道路,因为容易进入导致了竞争激烈和堵塞,大家都在争抢同一个蛋糕,就都没钱赚。

换言之,如果做生意是你出个几千块钱,拿到一套分销产品那么简单的话,就不会有进入壁垒,你应该放弃这个机会。

如果你能在几分钟内,就在一条街上开业做生意的话,那这一定不是一个理想的生意。

三、控制戒律

你对一门生意的控制权越多,你能赚的钱就越多。

当你掌控自己的生意时,意味着你控制着你生意的全部,包括组织架构、产品、定价、收入模式以及经营选择,拥有最终的决定权。

如果你的生意依附于别的公司或别的产品,那么未来将会有很大的不确定性。

例如很多电商或渠道经销商,每次一碰到平台规则调整,他们的收入就可能大幅下降,甚至变成零。

如果你是公司的一名员工,那你完全没有控制权。

在公司赚大钱的时候你可以跟着赚一点小钱,虽然随着职位的升高可以赚到更多的钱,但因为违背了控制戒律,很难赚到大钱。

四、规模戒律

如果你在当地开了一家服装店,那你就违背了规模戒律。

因为你的所有客户来自周围。为了扩大规模,你需要用复制的方式引入杠杆,即开更多的分店,出售更多的连锁经营权,或在互联网上售卖。

实现规模效益,可以扩大服务范围或提高利润率,要么扩大范围为一百万人服务,要么为少数高净值客户服务,但是有着非常高的利润率。

五、时间戒律

时间戒律要求你的生意跟你的时间脱钩。你的生意能够让你脱身,开花结果变成一棵摇钱树。

拥有一项生意并不保证得到财富或解脱。很多公司老板被生意绊住了,因为他们的生意违背了时间戒律。

虽然全心全意投入对于创业、成长和正在成熟阶段的公司老板很正常,但这不应该持续几十年。

这5条戒律可以用来检验你选择的道路,满足所有5条戒律的道路,可以让你快速致富。而违反的戒律越多,赚钱的难度就越大。

深度解析:什么样的行业,未来赚钱最多? 互联网 网络营销 好文分享 第2张

接下来,是三种具体的行业类型。

一、互联网生意

这是现有的最好的道路,因为它完全符合5条戒律,举几个例子:

① 提供内容服务:

在各大媒体平台上,提供免费内容,以出售广告的方式来赚钱。但现在内容服务的难度已经上升很多了,因为现在进入壁垒大大下降,竞争激烈,并且受平台的政策变化影响大。

② 提供订阅服务:

为用户提供数据、信息或软件,收取月度费用。数据可以是线索、销售信息、行业数据库或图片等等。我们的身边就有很多例子,你能举几个出来吗?

③ 引导性销售:

在向消费者提供服务的同时,向其他行业出售消费者信息。大到诸如百度360这类搜索引擎,小到各平台的带货达人,都是属于这一类。

电子商务

大的电子商务提供商有淘宝京东苏宁等,但很多本地的小型店铺,也通过电子商务扩大了规模。在网上做生意,全国甚至全世界都是你的潜在客户,上限非常高。

二、创新产品或服务

实际上这是在满足消费者的潜在需求,首先是制造,然后再是销售。

各行各业里都有这种类型,零食吃不完就用小袋子包装,音箱直接用手机app就能控制,电饭煲除了煮饭还可以一键煲汤,等等等等,我们周围有很多这样的例子。

把一些东西加以改进和提高,把老旧的东西改造得更好,然后重新推入市场。既可以在线下实体店销售,也可以通过网络销售,还可以卖给批发商,再由他们出售给2万家零售商进行销售。

三、创造规模效应

创造规模效应也是一条好道路,但极具挑战性。因为它除了满足最后一条规模戒律外,其他四条戒律都不满足。

连锁经营就是典型的案例,开一家店赚的钱有限,但是当扩张到10家100家店的时候,情况就完全不一样了。

一套房子出租出去租金有限,但是如果有五十套房子出去,那就完全不一样,可以化身包租婆模式了。

深度解析:什么样的行业,未来赚钱最多? 互联网 网络营销 好文分享 第3张

人人都说,选择比努力更重要,但如何做选择,本身就是一件非常困难的事情。

在行业选择上,年纪轻轻就已经财务自由的人,给的建议绝对比别人有价值的多。

根据5大戒律,筛选有价值的行业,再参考3大类型的行业,选择出适合自己的。

愿每一位朋友,都能够拥有一个灿烂的明天。加油!

来源:豆瓣    作者:跳海自尽

深度解析:什么样的行业,未来赚钱最多? – 卢松松博客已关闭评论
2020年11月20日 By mikel 分类: 互联网, 建站经验, 搜索优化

来源: 为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! – 卢松松博客

突然想到,不管是做百度搜索流量,还是微信上搜索流量,都离不开关键词,为什么呢?

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第1张

挖掘关键词是很重要,但挖掘出真正是用户需求的关键词才是最重要的,而用户需求的这个关键词,我们也叫精准关键词。关于关键词挖掘方法和工具这个之前也写过,但首先第一步,理解过什么是关键词吗?

关键词是什么?

你随便问一个才入门SEO不久的都知道,就是要做排名的那个词。比如我之前帮老东家卖大闸蟹的,建了一个白杨大闸蟹网站,那么,【大闸蟹】这个词就是关键词。

这篇文章我会新增加一角度,同时让小白也能看懂。

另外在讲之前,我先来回答标题里的那个问题,这样方便让你更加重视这个关键词的价值,好认真看后面的每一个字,嘿嘿。

为什么搜索流量都隐藏在关键词里?

要解答这个问题,我从以下三点来说说。

第一,弄明白什么是搜索流量。

搜索流量是指,不管是在PC端还是移动端,不管是在中国还是国外,不管是以文字还是符号、语音、图片形式,不管是在百度还是APP、淘宝、微信上,只要是用户主动去搜索他想要的并且点击了任何一个结果,这就是搜索流量。

第二,95%以上的搜索都是以文字形式进行的,不管这个是英文,还是日文。即使很多中老年不会打字的语音输入,也是搜索那里自动语音识别转文字再搜索的。以图搜图这个比例就更少了。

第三,虽然他官方名字叫关键词,但如果用生意的角度来看的话,它应该叫【用户需求】。

补充说明,这里的关键词,包含主关键词和长尾关键词。而用户想要搜到他想要的东西,不断加大这个词的详细度,比如他想知道羊绒围巾有哪些品牌,哪个好。他先输入羊绒围巾品牌,然后发现没有他想要的,然后他会继续输入羊绒围巾哪个品牌好,羊绒围巾品牌排行之类的词。

我们以前在做网站时,发现网站上的自然搜索过来的流量,80%都是用户输入的长尾关键词过来的。这就是为什么我上面说,搜索流量都隐藏在关键词里。

你可能还会问,我投放广告的流量不在吧?如果你是投搜索类的流量,依然是在关键词里,依然要做分析研究精准的关键词。

弄懂关键词之词根

【词根】这个词我在公众号过往写的文章中提到不多,一共也就两三次。对于SEO新人,或者外行来说,词根是什么意思都不知道,还说弄懂。

词根,用白话理解就是词的根根,就像树根根一样。举例,我喜欢你。它就是由我/喜欢/你组成,如果是搜索引擎的话,会分词成 我/喜/欢/你。

这里又冒出一个术语【分词】,所谓分词主要是指搜索引擎里中文分词的一种方法。

词根只能是一个字吗?不是的。比如华为手机,这里词根就是华为和手机。

为什么要先弄词根呢?那是因为方便挖掘出更多想要的词来,然后再来找到用户需求词。下面举例:

比如,假如我现在是婚庆行业从业,我想找到更多用户关于结婚需求的精准关键词。怎么办呢?

比如我找到这个行业目前最好的婚X记(免得说我打广告),直接进入首页从顶部导航、边栏导航就能找出它的很多关键词(如下图)。

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第2张

比如婚宴酒店、结婚攻略、婚纱摄影、婚礼策划、婚礼摄影等等。那么,我们可以提取如结婚、婚宴、婚礼、婚纱等词根。你不会问,为啥不是婚、礼、车这种呢,等会下面词组再讲哈哈哈。

如果不够,这个结婚攻略里面还有很多呢,如图:

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第3张

如果这里还不够,你可以用它的业务再去网上找与他同行网站,去提取他网站上没有的业务,尽可能找完,越详细越好。实在不行,可以上淘宝或者京东搜索这个产品,里面词根可不少呢。

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第4张

弄懂关键词之词组

这里的词组是指词与词组合的意思哈。比如上面京东那个图婚纱是一个产品,而材质里面几十种类,随便组合就是一个新的词了,比如蕾丝婚纱,绸缎婚纱。

如果我再加上品牌来组合的话,又可以是南极人 蕾丝婚纱 ,南极人 绸缎婚纱 。这样你能理解词组了吧。

当我们把关键词整理成词组以后,就可以去挖掘出用户的长尾需词关键词了。这里我以5118工具举例。

你肯定会说,怎么又用5118,其实不管是5118还是别的,因为做流量都需要找到精准长尾关键词,在关键词挖掘这块的工具上,真的是有很多工具,并且他们以此来盈利。而5118正因为其词库里数据多,所以用的人也多了。

比如,我想知道用户对蕾丝婚纱一般会搜哪些词,用5118长尾词工具挖掘如下图:

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第5张

如果你只想知道用户疑问词,你可以点 疑问 提取即可,如下图:

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第6张

当然,这里说明两点:

一是下载这些是要开会付费会员,最基础的也要280一年,你如果不想花钱,可以用百度下拉框和相关搜索去收集,或者找一些免费的工具。

二是在收集这些词组的时候,有一个最最重要的就是,你一定要对业务清楚,举例,比如你是卖婚纱的,类似像绸缎婚纱怎么洗之类的词,以作者的经验基本不会做。为什么,就如我以前卖大闸蟹一样,别人搜大闸蟹蒸多久这样词,基本代表别人已经买好大闸蟹了哈哈哈。

所以说,你弄懂关键词组词去找用户需求的前提,你还得深入了解你的业务。不是看这个词有流量就一定要做,要看这个词的流量对你有没有用。精准的前提就是你找词找得精准啊。

弄懂关键词之词库

词库,又是一个很高大上的名词是不是?我第一次听到应该是六七年前张国平老师在杭州滨江网易总部做的一次分享上。

我相信很多SEO新人,包括想进这行的,经常听到一些做流量的说,要建多少词库之类,词库到底是个什么东东?

所谓词库,如果我们把词根,词组比成粮食的话,那么词库就是装这些粮食的仓库。

在大型网站里面有个说法叫要想增加流量,加词库啊。为什么加词库可以增加流量呢?因为以前大站的策略并不是我们小站(这里主要指一般企业站,或者说页面低于10万以下的)主要靠什么外链,或者更新内容之类的。

他们的一个核心策略就是通过词库来进行站内优化,用词库来组合成多个页面。每多增加词就是增加页面,然后再想办法把页面整体收录提上去,从而获得更多流量。

上面这样讲,你可能不太理解。我来举例哈。

因为是大站,大站之所以大站,就是因为他们本身的内容足够多了。不管是B2B类早期靠爬取来的,还是门户网站采编,还是UGC用户自己上传或者发布来的。

那么,比如内容原有页面100万个,如果是用户,很难找到他想要的,很多网站虽然是大站,但站内搜索并不是很好,找不到用户想要的。所以我们就可以考虑用关键词方式来做。

比如我老老老东家维库电子市场,你看首页分类及二级已经很详细了吧。

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第7张

比如点PCB里面一个刚柔复合板,进去以后再看右侧热门产品推荐那里。

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第8张

看这里,他把柔软云母板所有页面包括有的再组合在一起,成了一个新的页面,如图:

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第9张

你可能心里有两个疑问:一个是小网站为啥不能这么做?二个是这么做这个页面相似度怎么处理?会收录么?

小网站之所以不能这么做,因为内容本身不多,即使有再多的词组放进去,弄出来重复很多,更重要的 是 ,某些关键词下面可能没啥内容,最多一篇,对于这种,搜索引擎是不喜欢的。

大站的相似度,其实,大站里面SEO策略除了上面提到的关键词库策略,另外一个就是页面模板调整。什么意思,就是我可以优化这个词聚合页面的丰富度,如果有多个词库聚合,那么每个词库板块可以做调整。

如果真对这个感兴趣,可以去关注一下携程网,以及里面各个频道。我之前一个好朋友之前在携程某频道待了几年,主要就在搞这种站内优化。

说到这里,我们都知道词库很重要,但是吧,市面上基本上的工具词库都要钱,5118的不便宜哈,实话。我之前负责日文站的时候,还想买词库呢,那时市面上也没有什么好的词库呢。

一般词库需要到技术爬取,就是这几年很火的PYTHON,不过这个我也不会哈哈哈。

词库长什么样呢,我们看下5118的哈,如图,比如吉它这个,有100多万个。

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! SEO优化 网站 经验心得 第10张
如果想显示所有或者下下载,需要买这个,3699一年,这个是最高的,我目前只是企业版,也是之前合作帮我升级的哈哈哈。

弄懂关键词之核心

关键词核心是什么?是怎么分析关键词?是怎么挖掘关键词?是怎么挖掘更多关键词?是怎么搞个百万千万关键词库?是怎么把词布局进去?都不是。

弄懂关键词的核心是理解不同平台关键词背后的用户使用不同。

而做SEO的最大自我困住也在这。就是以为SEO的关键词这一套走天下,忽略了关键词背后其实是用户需求

怎么理解?就是这个同一个关键词的用户需求搜索背后,在每个平台是不一样的。怎么发现?最简单方法,你在百度、微信、淘宝都输入关键词网站设计,你自己看相关搜索提示你就懂我说的了。

如果再往深一点思考,那是因为平台属性不同,所以用户找同一个需求,然后后面需求不同。比如,在百度一般搜索简单的问题,如果深入的,懂得人会上知乎,甚至在微信搜一些深度文章。

作者:白杨SEO   公众号 :白杨SEO

为什么搜索流量都隐藏在关键词里 弄懂关键词看这篇就够了! – 卢松松博客已关闭评论
2020年11月19日 By mikel 分类: 架构设计

来源: 还不懂Docker?一个故事安排的明明白白! – 轩辕之风 – 博客园

程序员受苦久矣

多年前的一个夜晚,风雨大作,一个名叫Docker的年轻人来到Linux帝国拜见帝国的长老。

“Linux长老,天下程序员苦于应用部署久矣,我要改变这一现状,希望长老你能帮帮我”

长老回答:“哦,小小年纪,口气不小,先请入座,你有何所求,愿闻其详”

Docker坐下后开始侃侃而谈:“当今天下,应用开发、测试、部署,各种库的依赖纷繁复杂,再加上版本之间的差异,经常出现在开发环境运行正常,而到测试环境和线上环境就出问题的现象,程序员们饱受此苦,是时候改变这一状况了。”

Docker回头看了一眼长老接着说到:“我想做一个虚拟的容器,让应用程序们运行其中,将它们需要的依赖环境整体打包,以便在不同机器上移植后,仍然能提供一致的运行环境,彻底将程序员们解放出来!”

Linux长老听闻,微微点头:“年轻人想法不错,不过听你的描述,好像虚拟机就能解决这个问题。将应用和所依赖的环境部署到虚拟机中,然后做个快照,直接部署虚拟机不就可以了吗?”

Docker连连摇头说到:“长老有所不知,虚拟机这家伙笨重如牛,体积又大,动不动就是以G为单位的大小,因为它里面要运行一个完整的操作系统,所以跑起来格外费劲,慢就不说了,还非常占资源,一台机器上跑不了几台虚拟机就把性能拖垮了!而我想要做一个轻量级的虚拟容器只提供一个运行环境,不用运行一个操作系统,所有容器中的系统内核还是和外面的宿主机共用的,这样就可以批量复制很多个容器,轻便又快捷

Linux长老站了起来,来回踱步了几圈,思考片刻之后,忽然拍桌子大声说到:“真是个好想法,这个项目我投了!”

Docker眼里见光,喜上眉梢,“这事还真离不开长老的帮助,要实现我说的目标,对进程的管理隔离都至关重要,还望长老助我一臂之力!”

“你稍等”,Linux长老转身回到内屋。没多久就出来了,手里拿了些什么东西。

“年轻人,回去之后,尽管放手大干,我赐你三个锦囊,若遇难题,可依次拆开,必有大用”

Docker开心的收下了三个锦囊,拜别Linux长老后,冒雨而归。

锦囊1:chroot & pivot_root

受到长老的鼓励,Docker充满了干劲,很快就准备启动他的项目。

作为一个容器,首要任务就是限制容器中进程的活动范围——能访问的文件系统目录。决不能让容器中的进程去肆意访问真实的系统目录,得将他们的活动范围划定到一个指定的区域,不得越雷池半步!

到底该如何限制这些进程的活动区域呢?Docker遇到了第一个难题。

苦思良久未果,Docker终于忍不住拆开了Linux长老送给自己的第一个锦囊,只见上面写了两个函数的名字:chroot & pivot_root

Docker从未使用过这两个函数,于是在Linux帝国四处打听它们的作用。后来得知,通过这两个函数,可以修改进程和系统的根目录到一个新的位置。Docker大喜,长老真是诚不欺我!

有了这两个函数,Docker开始想办法怎么来“伪造”一个文件系统来欺骗容器中的进程。

为了不露出破绽,Docker很聪明,用操作系统镜像文件挂载到容器进程的根目录下,变成容器的rootfs,和真实系统目录一模一样,足可以以假乱真:

$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

锦囊2:namespace

文件系统的问题总算解决了,但是Docker不敢懈怠,因为在他心里,还有一个大问题一直困扰着他,那就是如何把真实系统所在的世界隐藏起来,别让容器中的进程看到。

比如进程列表、网络设备、用户列表这些,是决不能让容器中的进程知道的,得让他们看到的世界是一个干净如新的系统。

Docker心里清楚,自己虽然叫容器,但这只是表面现象,容器内的进程其实和自己一样,都是运行在宿主操作系统上面的一个个进程,想要遮住这些进程的眼睛,瞒天过海,实在不是什么容易的事情。

Docker想过用HOOK的方式,欺骗进程,但实施起来工作太过复杂,兼容性差,稳定性也得不到保障,思来想去也没想到什么好的主意。

正在一筹莫展之际,Docker又想起了Linux长老送给自己的锦囊,他赶紧拿了出来,打开了第二个锦囊,只见上面写着:namespace

Docker还是不解其中之意,于是又在Linux帝国到处打听什么是namespace。

经过一阵琢磨,Docker总算是明白了,原来这个namespace是帝国提供的一种机制,通过它可以划定一个个的命名空间,然后把进程划分到这些命名空间中。

 

 

 

而每个命名空间都是独立存在的,命名空间里面的进程都无法看到空间之外的进程、用户、网络等等信息。

这不正是Docker想要的吗?真是踏破铁鞋无觅处,得来全不费功夫!

Docker赶紧加班加点,用上了这个namespace,将进程的“视野”锁定在容器规定的范围内,如此一来,容器内的进程彷佛被施上了障眼法,再也看不到外面的世界。

锦囊3:CGroup

文件系统和进程隔离的问题都解决了,Docker心里的石头总算是放下了。心里着急着想测试自己的容器,可又好奇这最后一个锦囊写的是什么,于是打开了第三个锦囊,只见上面写着:CGroup

这又是什么东西?Docker仍然看不懂,不过这一次管不了那么许多了,先运行起来再说。

试着运行了一段时间,一切都在Docker的计划之中,容器中的进程都能正常的运行,都被他构建的虚拟文件系统和隔离出来的系统环境给欺骗了,Docker高兴坏了!

很快,Docker就开始在Linux帝国推广自己的容器技术,结果大受欢迎,收获了无数粉丝,连nginxredis等一众大佬都纷纷入驻。

然而,鲜花与掌声的背后,Docker却不知道自己即将大难临头。

这天,Linux帝国内存管理部的人扣下了Docker准备“处决”掉他,Docker一脸诧异的问到,“到底发生了什么事,为什么要对我下手?”

管理人员厉声说到:“帝国管理的内存快被一个叫Redis的家伙用光了,现在要挑选一些进程来杀掉,不好意思,你中奖了”

 

 

 

Redis?这家伙不是我容器里的进程吗?Docker心中一惊!

“两位大人,我认识帝国的长老,麻烦通融通融,找别人去吧,Redis那家伙,我有办法收拾他”

没想到他还认识帝国长老,管理人员犹豫了一下,就放了Docker到别处去了。

惊魂未定的Docker,思来想去,如果不对容器中的进程加以管束,那简直太危险了!除了内存,还有CPU、硬盘、网络等等资源,如果某个容器进程霸占着CPU不放手,又或者某个容器进程疯狂写硬盘,那迟早得连累到自己身上。看来必须得对这些进程进行管控,防止他们干出出格的事来。

这时候,他想起了Linux长老的第三个锦囊:CGroup!说不定能解这燃眉之急。

经过一番研究,Docker如获至宝,原来这CGroup和namespace类似,也是Linux帝国的一套机制,通过它可以划定一个个的分组,然后限制每个分组能够使用的资源,比如内存的上限值、CPU的使用率、硬盘空间总量等等。系统内核会自动检查和限制这些分组中的进程资源使用量。

 

 

Linux长老这三个锦囊简直太贴心了,一个比一个有用,Docker内心充满了感激。

随后,Docker加上了CGroup技术,加强了对容器中的进程管控,这才松了一口气。

在Linux长老三个锦囊妙计的加持下,Docker可谓风光一时,成为了Linux帝国的大名人。

然而,能力越大,责任越大,让Docker没想到的是,新的挑战还在后面。

还不懂Docker?一个故事安排的明明白白! – 轩辕之风 – 博客园已关闭评论
2020年11月18日 By mikel 分类: PHP

来源: 阿里大鱼–短信发送API_hit、run-CSDN博客_阿里大鱼短信

参考链接一
参考链接二
项目中运用了阿里大鱼来实现短信的发送.主要步骤如下
1)申请短信签名
在控制台完成模板与签名的申请
2)申请短信模板
在控制台完成模板与签名的申请
3) 第一部分,获取IAcsClient对象,该对象用来发送请求。

//定义常量
final String product = "Dysmsapi";//短信API产品名称(短信产品名固定,无需修改)
final String domain = "dysmsapi.aliyuncs.com";//短信API产品域名(接口地址固定,无需修改)
//替换成你的AK秘钥
final String accessKeyId = "yourAccessKeyId";//你的accessKeyId,参考本文档步骤2
final String accessKeySecret = "yourAccessKeySecret";//你的accessKeySecret,参考本文档步骤2
//设置超时时间-可自行调整
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化ascClient,暂时不支持多region(请勿修改)
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId,
accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);

4)根据短信模板,创建Request请求对象

//组装请求对象
 SendSmsRequest request = new SendSmsRequest();
 //使用post提交
 request.setMethod(MethodType.POST);
 //必填:待发送手机号
 request.setPhoneNumbers("1500000000");
 //必填:短信签名-可在短信控制台中找到
 request.setSignName("云通信");
 //必填:短信模板-可在短信控制台中找到,发送国际/港澳台消息时,请使用国际/港澳台短信模版
 request.setTemplateCode("SMS_1000000");
 //可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
 if(content != null){
       request.setTemplateParam(content.toString());
   }
 //可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
 request.setOutId("yourOutId");

5)发送短信,获取响应对象

//请求失败这里会抛ClientException异常
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
 if("OK".equals(sendSmsResponse.getCode())){
                return ResultData.ok();
            }else if("isv.MOBILE_COUNT_OVER_LIMIT".equals(sendSmsResponse.getCode()) || 		"isv.BUSINESS_LIMIT_CONTROL".equals(sendSmsResponse.getCode())){ //限流
                return ResultData.fail("短信发送频繁,请稍后再试");
            }
阿里大鱼–短信发送API_hit、run-CSDN博客_阿里大鱼短信已关闭评论
2020年11月11日 By mikel 分类: PHP

来源: 深入浅出之Smarty模板引擎工作机制(二) – 曾是土木人 – 博客园

源代码下载地址:深入浅出之Smarty模板引擎工作机制
接下来根据以下的Smarty模板引擎原理流程图开发一个自己的模板引擎用于学习,以便加深理解。

 

Smarty模板引擎的原理,其实是这么一个过程:
把模板文件编译成php文件,然后每次都去读取下模板的修改时间,没有修改就不编译。然后include这个“编译”后的PHP文件。
所谓编译也就是模板用正则替换成含PHP代码的过程。
实际上并不会每次请求都编译,所以性能尚可。

模板文件和php程序文件经过模板引擎的编译后合成为一个文件,即编译后的文件。

接下来,我们根据该原理流程写一个简单的模板引擎。。。。。。

  先贴上核心代码:

  Smarty.class.php文件

复制代码
<?php
    class Smarty{
        public $template_dir;//模板目录
        public $compile_dir;//编译目录
        public $arr=array();//定义一个数组,用以存放assign中的第二个参数传过来的值
        public function __construct($template_dir="../templates",$compile_dir="../templates_c"){
                $this->template_dir=$template_dir;//模板目录
                $this->compile_dir=$compile_dir;  //编译目录
            }
        public function assign($content,$replacment=null){
                if($content!=""){                 //如果指定模板变量,才将要赋的值存储到数组中
                        $this->arr[$content]=$replacment;
                    }
            }
        public function display($page){
                $tplFile=$this->template_dir."/".$page;//读取模板文件,注意:如果模板目录下还有子目录,记得要写完整,比如,$smarty->display('Default/index.tpl')
                if(!file_exists($tplFile)){
                        return;
                }
                $comFile=$this->compile_dir."/"."com_".$page.".php";
                $tplContent=$this->con_replace(file_get_contents($tplFile));//将smarty标签替换为php的标签
                file_put_contents($comFile,$tplContent);
                include $comFile;
        }
        public function con_replace($content){
                $pattern=array(
                    '/<{\s*\$([a-zA-Z_][a-zA-Z_0-9]*)\s*}>/i'
                );
                   $replacement=array(
                       '<?php echo $this->arr["${1}"] ?>'
                );
                    return preg_replace($pattern,$replacement,$content);
                }
        }
?>
复制代码

Smarty.class.php代码解释:

  • $template_dir  指定模板文件的目录
  • $compile_dir   指定编译后的模板文件的目录
  • 构造函数

public function __construct($template_dir=”../templates”,$compile_dir=”../templates_c”)

{

$this->template_dir=$template_dir;

$this->compile_dir=$compile_dir;

}

默认情况下,Smarty模板引擎将把templates目录用于存放模板文件,templates_c用于存放编译后的文件

 

  • assign($content,$replacment=null)函数的工作机制是将每次要传递给模板中的变量的值通过语句:$this->arr[$content]=$replacment;保存到数组中。

     那为何要$replacement的值保存到数组中呢?

其实内部操作是这么一个流程:将$replacement值保存到数组—>读取模板文件(index.dwt,由display函数完成)—>将数组中的值匹配给模板文件中的变量(由con_replace()函数完成)—>将替换后的模板文件写入到编译文件中(com_index.dwt.php)—>输出编译后的PHP文件

  • dispaly($page)函数接收一个参数,即要输出的模板文件(index.dwt)
    • 首先,将模板文件的路径赋给$tplFile($tplFile=$this->template_dir.”/”.$page)
    • 判断模板文件是否存在,如果不存在,就没必要加载了,直接return
    • 指定一个编译文件,以便存放替换变量后的模板文件
    • 通过函数file_get_contents()读取模板文件,并通过函数conf_replace()替换掉模板中的smarty标签
    • 将替换变量后的模板文件通过file_put_contents()写入到编译文件中
    • 将编译后的文件include进来,即可输出编译后的文件
  • 函数con_replace($content)用于替换模板文件(index.dwt)中的变量,即将php中的变量值赋给模板中的变量
    • 通过一个可以匹配<{$title}>形式的正则表达式匹配模板文件中的内容,并将匹配到的值替换为<?php echo $title?>的形式
    • 匹配到内容,并将替换后的内容返回
复制代码
/*Smarty.ini.php文件:用于完成初始化smarty的工作*/
<?php
    include "./libs/Smarty.class.php";
    $tpl=new Smarty();
    $tpl->template_dir="./Tpl";    
    $tpl->compile_dir="./Compile";
?>
复制代码

 

复制代码
<!--模板文件-->
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><{$title}></title>
</head>
<body>
<p>内容:<{$content}></p>
<p>作者:<{$auth}></p>
<p>网址:<{$website}></p>
</body>
</html>
复制代码

 

复制代码
/*index.php文件*/
<?php
    include "./Smarty.ini.php";
    $title="深入浅出之Smarty模板引擎工作机制";    
    $content="Smarty模板引擎工作机制流程图";
    $auth="MarcoFly";
    $website="www.MarcoFly.com";
    $tpl->assign("title",$title);
    $tpl->assign("content",$content);    
    $tpl->assign("auth",$auth);
    $tpl->assign("website",$website);
    $tpl->display("index.dwt");
?>
复制代码

该index.php就是PHP程序员编写的,可以从数据库中获取各种想要的数据,并保存到变量中,然后简单的调用assign()函数将数据保存到数组中,并通过display()函数将编译文件输出

注:此编译文件是php文件,通过服务器端执行,将结果输出的客户端的浏览器上

分析到这里,我们回过头来分析下在深入浅出之Smarty模板引擎工作机制(一)中给出的关于编译后的文件代码:

复制代码
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><?php echo $this->arr["title"] ?></title>
</head>
<body>
<p>内容:<?php echo $this->arr["content"] ?></p>
<p>作者:<?php echo $this->arr["auth"] ?></p>
<p>网址:<?php echo $this->arr["website"] ?></p>
</body>
</html>
复制代码

由于我们已经通过assign()函数,将要赋给模板标签中变量的值保存到了数组中了,即此时编译后的模板文件,可以直接输出该数组中的值了。

举个例子:

$title="深入浅出之Smarty模板引擎工作机制";
$tpl->assign("title",$title);
当执行了以上两句代码后,在数组$arr中就存放着下标为:title,值为:深入浅出之Smarty模板引擎工作机制的关联数组了。
此时,就可以直接通过$this->arr['title']直接输出该数组的值。
至于对如何从<{$title}>  ---> <?php echo $this->arr['title']?> 的转换,不懂的读者可以再仔细看下con_replace()函数

有了以上几个文件之后,我们在浏览器中访问index.php文件将得到以下结果:

 

到此,我们“开发”了一个自己的模板引擎,并且测试成功,当然,这只是供交流学习之用。如果你觉得这篇文章对你了解smarty模板引擎的工作机制有所帮助的话,请帮忙顶一顶哈O(∩_∩)O~

文章出自:WEB开发_小飞

转载请注明出处:http://www.cnblogs.com/hongfei/archive/2011/12/10/Smarty-two.html

深入浅出之Smarty模板引擎工作机制(二) – 曾是土木人 – 博客园已关闭评论
2020年11月11日 By mikel 分类: PHP

来源: 深入浅出之Smarty模板引擎工作机制(一) – 曾是土木人 – 博客园

深入浅出Smarty模板引擎工作机制,我们将对比使用smarty模板引擎和没使用smarty模板引擎的两种开发方式的区别,并动手开发一个自己的模板引擎,以便加深对smarty模板引擎工作机制的理解。

在没有使用Smarty模板引擎的情况下,我们都是将PHP程序和网页模板合在一起编辑的,好比下面的源代码:

复制代码
<?php
$title="深处浅出之Smarty模板引擎工作机制";
$content="Smarty模板引擎原理流程图";
$auth="MarcoFly";
$website="www.MarcoFly.com";
?>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><?php echo $title?></title>
</head>
<body>
<p>内容:<?php echo $content?></p>
<p>作者:<?php echo $auth?></p>
<p>网址:<?php echo $website?></p>
</body>
</html>
复制代码

输出到浏览器的结果截图:

查看HTML源代码:

复制代码
<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>深处浅出之Smarty模板引擎工作机制</title>

</head>

<body>

<p>内容:Smarty模板引擎原理流程图</p>

<p>作者:MarcoFly</p>

<p>网址:www.MarcoFly.com</p>

</body>

</html>
复制代码

程序比较小的情况下这种开发方式尚且不方便,一旦要开发一个大的WEB项目,就必须得使用到模板引擎。

使用模板引擎的情况下:
我们的开发方式将有所改变,美工人员只管做模板,后台开发人员专心写自己的程序。
一个web项目就可以分为模板文件PHP程序
比如:
美工人员就可以这样编辑网页模板文件:
index.dwt源代码

复制代码
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><{$title}></title>
</head>
<body>
<p>内容:<{$content}></p>
<p>作者:<{$auth}></p>
<p>网址:<{$website}></p>
</body>
</html>
复制代码
而后台WEB开发人员可以专注于PHP代码的书写:
index.php
复制代码
<?php
    include "./Smarty.ini.php";
    $title="深处浅出之Smarty模板引擎工作机制";
    $content="Smarty模板引擎工作机制流程图";
    $auth="MarcoFly";
    $website="www.MarcoFly.com";
    $tpl->assign("title",$title);
    $tpl->assign("content",$content);    
    $tpl->assign("auth",$auth);
    $tpl->assign("website",$website);
    $tpl->display("index.dwt");
?>
复制代码

从以上两段简单的演示代码可以看出,前台模板文件没有涉及到任何关于PHP的代码,只有几个看似陌生的标签<{$title}><{$content}>,而后台的php程序代码也没有涉及到前台的HMTL代码
      参考下图对比这两种开发方式的区别



通过对比,我们得出结论:在使用模板引擎后,原先需要使用PHP编写的地方,现在只需要用模板引擎提供标签的形式来代替了。
注:Smarty模板引擎默认的标签形式是{$xxx},如,{$title},{$content}
当然我们可以初始化为自己想要的标签形式,如我将其初始化为:<{$xxx}>的形式),如,<{$title}>、<{$content}>

不知各位看官有木有觉得奇怪,<{$title}>、<{$content}>根本就不是PHP的语法形式,那最终又是如何被输出到客户的浏览器中的,是否另有玄机?带着这个疑问,我们继续深究......
  其实,这一切的一切都是由Smarty模板引擎这双神秘的手在“暗中操作”着,经过Smarty模板引擎的“暗中操作”之后,起初的模板文件(index.dwt)经过Smarty“成功手术”之后,被改造为能在服务器端执行的PHP代码文件。
想看看模板文件(index.dwt)和后台的PHP程序(index.php)经过“手术”(即编译)之后的庐山真面目吗?
在此贴上经过模板引擎编译之后的编译文件的源代码:
复制代码
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><?php echo $this->arr["title"] ?></title>
</head>
<body>
<p>内容:<?php echo $this->arr["content"] ?></p>
<p>作者:<?php echo $this->arr["auth"] ?></p>
<p>网址:<?php echo $this->arr["website"] ?></p>
</body>
</html>
复制代码
看到这里,各位看官是否恍然大悟,原来Smarty模板引擎的工作就是:将前台美工人员编写的模板文件(index.dwt)和后台开发人员编写的PHP程序(index.php)整合在一起,经过编译这一步骤之后,原先的模板标签被替换成了php代码。
为了方便大家理解,我简单的做了一张代码流程图:

如果你觉得很神秘,想更深入了解Smarty模板引擎是如何完成这一步骤的,可以看看深入浅出之Smarty模板引擎工作机制(二)

文章出自:WEB开发_小飞

转载请注明出处:http://www.cnblogs.com/hongfei/archive/2011/12/10/Smarty-one.html

深入浅出之Smarty模板引擎工作机制(一) – 曾是土木人 – 博客园已关闭评论
2020年11月11日 By mikel 分类: C#, 架构设计

来源: 知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案二 – 初久的私房菜 – 博客园

教程

01 | 模块化方案一

02 | 模块化方案二

其他教程预览

分库分表项目实战教程

Git地址: https://github.com/MrChuJiu/EasyLogger

01 | 前言

02 | 简单的分库分表设计

03 | 控制反转搭配简单业务

04 | 强化设计方案

05 | 完善业务自动创建数据库

06 | 最终篇-通过AOP自动连接数据库-完成日志业务

简介

开讲第二篇,本篇代码并非Copy的ABP,只是参考ABP的功能,进行的实现方案,让代码更加通俗易懂。代码的讲解思路和上一篇一样,但是不引用上篇的写法。

开始

第一步 基本操作

还是老样子,我们新建一个模块化接口类
新建接口 IAppModule (ps:项目中起的类名和方法名尽量对标ABP)

   /// <summary>
    /// 应用模块接口定义
    /// </summary>
    public interface IAppModule
    {
        /// <summary>
        /// 配置服务前
        /// </summary>
        /// <param name="context"></param>
        void OnPreConfigureServices();
        /// <summary>
        /// 配置服务
        /// </summary>
        /// <param name="context">配置上下文</param>
        void OnConfigureServices();
        /// <summary>
        /// 配置服务后
        /// </summary>
        /// <param name="context"></param>
        void OnPostConfigureServices();
        /// <summary>
        /// 应用启动前
        /// </summary>
        /// <param name="context"></param>
        void OnPreApplicationInitialization();
        /// <summary>
        /// 应用启动
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationInitialization();
        /// <summary>
        /// 应用启动后
        /// </summary>
        /// <param name="context"></param>
        void OnPostApplicationInitialization();
        /// <summary>
        /// 应用停止
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationShutdown();
    }

新建类 AppModule 继承 IAppModule

   public abstract class AppModule : IAppModule
    {
        public virtual void OnPreConfigureServices()
        {

        }

        public virtual void OnConfigureServices()
        {

        }

        public virtual void OnPostConfigureServices()
        {

        }

        public virtual void OnPreApplicationInitialization()
        {

        }

        public virtual void OnApplicationInitialization()
        {

        }

        public virtual void OnPostApplicationInitialization()
        {

        }
        public virtual void OnApplicationShutdown()
        {

        }
    }

第二步 预准备

这一步来完成ABP的DependsOnAttribute,通过特性进行引入模块,
这里参数 params Type[] 因为一个模块会依赖多个模块
新建类 DependsOnAttribute 继承 Attribute

/// <summary>
    /// 模块依赖的模块
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class DependsOnAttribute : Attribute
    {
        /// <summary>
        /// 依赖的模块类型
        /// </summary>
        public Type[] DependModuleTypes { get; private set; }

        public DependsOnAttribute(params Type[] dependModuleTypes)
        {
            DependModuleTypes = dependModuleTypes ?? new Type[0];
        }
    }

既然一个模块会包含多个模块的引用,那么就应该有一个存储的方式
新建类 ModuleDescriptor 该类来存储 自身和引用的其他模块

    /// <summary>
    /// 模块描述
    /// </summary>
    public class ModuleDescriptor
    {
        private object _instance;

        /// <summary>
        /// 模块类型
        /// </summary>
        public Type ModuleType { get; private set; }

        /// <summary>
        /// 依赖项
        /// </summary>
        public ModuleDescriptor[] Dependencies { get; private set; }

        /// <summary>
        /// 实例
        /// </summary>
        public object Instance
        {
            get
            {
                if (this._instance == null)
                {
                    this._instance = Activator.CreateInstance(this.ModuleType);
                }
                return this._instance;
            }
        }

        public ModuleDescriptor(Type moduleType, params ModuleDescriptor[] dependencies)
        {
            this.ModuleType = moduleType;
            // 如果模块依赖 为空给一个空数组
            this.Dependencies = dependencies ?? new ModuleDescriptor[0];
        }
    }

第三步 模块管理器

来到核心步骤,这里我们写模块管理器,白话就是存储模块和模块操作方法的一个类(同上一篇的StartupModulesOptions)
第一步肯定是模块的启动
我们新建 IModuleManager接口

 public interface IModuleManager : IDisposable
    {
        /// <summary>
        /// 启动模块
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        void StartModule<TModule>(IServiceCollection services)
            where TModule : IAppModule;
    }

紧跟新建类 ModuleManager 继承 IModuleManager, StartModule 先放在一边
这里的思路是:模块是从一个入口的根模块开始的慢慢的形成一个树状的引用关系,我们首先需要拿到所有的模块引用,并把他们从树叶为起点排列起来,依次注入。
(理解为A=>B=>C 那么注入的顺序应该是 C=>B=>A)

1.先来实现根绝入口递归获取所有的引用关系 我已经在方法中将每一步的注释都写上了

/// <summary>
        /// 获取模块依赖树
        /// </summary>
        /// <param name="moduleType"></param>
        /// <returns></returns>
        protected virtual List<ModuleDescriptor> VisitModule(Type moduleType) {

            var moduleDescriptors = new List<ModuleDescriptor>();
            // 是否必须被重写|是否是接口|是否为泛型类型|是否是一个类或委托
            if (moduleType.IsAbstract || moduleType.IsInterface || moduleType.IsGenericType || !moduleType.IsClass) {
                return moduleDescriptors;
            }

            // 过滤没有实现IRModule接口的类
            var baseInterfaceType = moduleType.GetInterface(_moduleInterfaceTypeFullName, false);
            if (baseInterfaceType == null)
            {
                return moduleDescriptors;
            }

            // 得到当前模块依赖了那些模块
            var dependModulesAttribute = moduleType.GetCustomAttribute<DependsOnAttribute>();
            // 依赖属性为空
            if (dependModulesAttribute == null)
            {
                moduleDescriptors.Add(new ModuleDescriptor(moduleType));
            }
            else {
                // 依赖属性不为空,递归获取依赖
                var dependModuleDescriptors = new List<ModuleDescriptor>();
                foreach (var dependModuleType in dependModulesAttribute.DependModuleTypes)
                {
                    dependModuleDescriptors.AddRange(
                        VisitModule(dependModuleType)
                    );
                }
                // 创建模块描述信息,内容为模块类型和依赖类型
                moduleDescriptors.Add(new ModuleDescriptor(moduleType, dependModuleDescriptors.ToArray()));
            }

            return moduleDescriptors;
        }
补: _moduleInterfaceTypeFullName 定义
        /// <summary>
        /// 模块接口类型全名称
        /// </summary>
        public static string _moduleInterfaceTypeFullName = typeof(IAppModule).FullName;
2.拿到依赖关系通过拓扑排序进行顺序处理 (ps:拓扑排序地址 https://www.cnblogs.com/myzony/p/9201768.html)

新建类 Topological 这块没啥特别要讲的根据链接去看下就好了

    /// <summary>
    /// 拓扑排序工具类
    /// </summary>
    public static class Topological
    {
        public static List<T> Sort<T>(IEnumerable<T> source, Func<T, IEnumerable<T>> getDependencies) {

            var sorted = new List<T>();
            var visited = new Dictionary<T, bool>();

            foreach (var item in source)
            {
                Visit(item, getDependencies, sorted, visited);
            }

            return sorted;
        }

        static void Visit<T>(T item, Func<T, IEnumerable<T>> getDependencies, List<T> sorted, Dictionary<T, bool> visited)
        {
            bool inProcess;
            var alreadyVisited = visited.TryGetValue(item, out inProcess);

            // 如果已经访问该顶点,则直接返回
            if (alreadyVisited)
            {
                // 如果处理的为当前节点,则说明存在循环引用
                if (inProcess)
                {
                    throw new ArgumentException("模块出现循环依赖.");
                }
            }
            else
            {
                // 正在处理当前顶点
                visited[item] = true;

                // 获得所有依赖项
                var dependencies = getDependencies(item);
                // 如果依赖项集合不为空,遍历访问其依赖节点
                if (dependencies != null)
                {
                    foreach (var dependency in dependencies)
                    {
                        // 递归遍历访问
                        Visit(dependency, getDependencies, sorted, visited);
                    }
                }

                // 处理完成置为 false
                visited[item] = false;
                sorted.Add(item);
            }
        }

    }

回到 ModuleManager 新建方法 ModuleSort

 /// <summary>
        /// 模块排序
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <returns></returns>
        public virtual List<ModuleDescriptor> ModuleSort<TModule>() where TModule : IAppModule
        {
            // 得到模块树依赖
            var moduleDescriptors = VisitModule(typeof(TModule));
            // 因为现在得到的数据是从树根开始到树叶 - 实际的注入顺序应该是从树叶开始 所以这里需要对模块进行排序
            return Topological.Sort(moduleDescriptors, o => o.Dependencies);
        }
补:ModuleSort本来是个私有方法 后为了让模块使用者可以实现重写,请在 IModuleManager 加入
        /// <summary>
        /// 模块排序
        /// </summary>
        /// <typeparam name="TModule">启动模块类型</typeparam>
        /// <returns>排序结果</returns>
        List<ModuleDescriptor> ModuleSort<TModule>()
            where TModule : IAppModule;

3.模块已经可以通过方法拿到了就来实现 StartModule 方法 筛选去重 依次进行注入, 并最终保存到全局对象中

        /// <summary>
        /// 模块明细和实例
        /// </summary>
        public virtual IReadOnlyList<ModuleDescriptor> ModuleDescriptors { get; protected set; }

        /// <summary>
        /// 入口 StartModule 
        /// 我们通过传递泛型进来的 TModule 为起点
        /// 查找他的依赖树
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <param name="services"></param>
        public void StartModule<TModule>(IServiceCollection services) where TModule : IAppModule
        {

            var moduleDescriptors = new List<ModuleDescriptor>();

            var moduleDescriptorList = this.ModuleSort<TModule>();
            // 去除重复的引用 进行注入
            foreach (var item in moduleDescriptorList)
            {
                if (moduleDescriptors.Any(o => o.ModuleType.FullName == item.ModuleType.FullName))
                {
                    continue;
                }
                moduleDescriptors.Add(item);
                services.AddSingleton(item.ModuleType, item.Instance);
            }
            ModuleDescriptors = moduleDescriptors.AsReadOnly();
        }
4.ModuleDescriptors既然存储着我们的所有模块,那么我们怎么执行模块的方法呢

入口通过调用下面的方法进行模块的方法调用

        /// <summary>
        /// 进行模块的  ConfigurationService 方法调用
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public IServiceCollection ConfigurationService(IServiceCollection services, IConfiguration configuration) {

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPreConfigureServices();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnConfigureServices();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPostConfigureServices();
            }

            return services;
        }
        /// <summary>
        /// 进行模块的  Configure 方法调用
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        public IServiceProvider ApplicationInitialization(IServiceProvider serviceProvider)
        {
            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPreApplicationInitialization();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnApplicationInitialization();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPostApplicationInitialization();
            }

            return serviceProvider;
        }
        /// <summary>
        /// 模块销毁
        /// </summary>
        public void ApplicationShutdown()
        {
            // todo我觉得这里有点问题问 易大师
            //var modules = ModuleDescriptors.Reverse().ToList();

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnApplicationShutdown();
            }
        }

当然还漏了一个模块销毁,该方法在主模块被销毁的时候调用(ps: 我个人思路应该是从树叶开始进行,但是ABP对模块顺序进行了反转从根开始进行销毁,所以这里同上)

        /// <summary>
        /// 主模块销毁的时候 销毁子模块
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        protected virtual void Dispose(bool state)
        {
            this.ApplicationShutdown();

        }

第四步 Extensions

模块管理器写完了,那么这个方法如何调用呢来写我们的 Extensions
新建 RivenModuleServiceCollectionExtensions 类,让其完成ConfigurationService方法的模块调用

 /// <summary>
    /// 模块服务扩展
    /// </summary>
    public static class RivenModuleServiceCollectionExtensions
    {
        /// <summary>
        /// 添加Riven模块服务
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IServiceCollection AddRivenModule<TModule>(this IServiceCollection services, IConfiguration configuration)
            where TModule : IAppModule
        {
            var moduleManager = new ModuleManager();
            // 将模块都查询排序好
            moduleManager.StartModule<TModule>(services);
            // 调用模块 和 子模块的ConfigurationService方法
            moduleManager.ConfigurationService(services, configuration);
            // 注入全局的  IModuleManager
            services.TryAddSingleton<IModuleManager>(moduleManager);
            return services;
        }
    }

新建 RivenModuleIApplicationBuilderExtensions 类 ,让其完成Configuration方法的模块调用

 public static class RivenModuleIApplicationBuilderExtensions
    {
        /// <summary>
        /// 使用RivenModule
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        public static IServiceProvider UseRivenModule(this IServiceProvider serviceProvider)
        {
            var moduleManager = serviceProvider.GetService<IModuleManager>();

            return moduleManager.ApplicationInitialization(serviceProvider);
        }
    }

第五步 测试

新建一个测试项目,引入写好的模块化类库,在 ConfigureServices 中调用

services.AddRivenModule<MyAppStartupModule>(Configuration);

Configure 中调用

 app.ApplicationServices.UseRivenModule();

模块销毁演示(ps:这个是演示效果、实际是在项目停止的时候进行。)

 app.Map("/ApplicationShutdown", _ =>
            {
                _.Run((context) =>
                {
                    var moduleManager = app.ApplicationServices.GetService<IModuleManager>();
                    moduleManager.ApplicationShutdown();
                    return Task.FromResult(0);
                });
            });

补:

新建 MyAppStartupModule、TestModuleA、TestModuleB 继承AppModule。
MyAppStartupModule作为入口模块 引用 A => B 然后在模块方法中打印 Console.WriteLine 看效果

补充 给模块传递参数

新建 ApplicationInitializationContext 类

public class ApplicationInitializationContext
    {
        public IServiceProvider ServiceProvider { get; }

        public IConfiguration Configuration { get; }

        public ApplicationInitializationContext([NotNull] IServiceProvider serviceProvider, [NotNull] IConfiguration configuration)
        {
            ServiceProvider = serviceProvider;
            Configuration = configuration;
        }
    }

新建 ApplicationShutdownContext 类

 public class ApplicationShutdownContext
    {
        public IServiceProvider ServiceProvider { get; }

        public ApplicationShutdownContext([NotNull] IServiceProvider serviceProvider)
        {
            ServiceProvider = serviceProvider;
        }
    }

新建 ServiceConfigurationContext 类

 public class ServiceConfigurationContext
    {
        public IServiceCollection Services { get; protected set; }

        public IConfiguration Configuration { get; protected set; }


        public ServiceConfigurationContext(IServiceCollection services, IConfiguration configuration)
        {
            Services = services;
            Configuration = configuration;
        }

    }

修改 IAppModule 接口, 模块和实现都自己手动都同步一下

    /// <summary>
    /// 应用模块接口定义
    /// </summary>
    public interface IAppModule
    {
        /// <summary>
        /// 配置服务前
        /// </summary>
        /// <param name="context"></param>
        void OnPreConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 配置服务
        /// </summary>
        /// <param name="context">配置上下文</param>
        void OnConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 配置服务后
        /// </summary>
        /// <param name="context"></param>
        void OnPostConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 应用启动前
        /// </summary>
        /// <param name="context"></param>
        void OnPreApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用启动
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用启动后
        /// </summary>
        /// <param name="context"></param>
        void OnPostApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用停止
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationShutdown(ApplicationShutdownContext context);
    }

修改 ModuleManager的 ConfigurationService、ApplicationInitialization、ApplicationShutdown 方法给调用传递对应参数
这部分代码我就不贴了,会的大佬都能自己写,想看的去我的github直接下载源码看吧,麻烦老板们给点个星星!!!

项目地址

知识全聚集,逐个击破: https://github.com/MrChuJiu/Easy.Core.Flow

鸣谢

玩双截棍的熊猫

源地址:https://github.com/rivenfx/Modular

知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案二 – 初久的私房菜 – 博客园已关闭评论
2020年11月11日 By mikel 分类: C#

来源: 知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案一 – 初久的私房菜 – 博客园

教程

01 | 模块化方案一

02 | 模块化方案二

其他教程预览

分库分表项目实战教程

Git地址: https://github.com/MrChuJiu/EasyLogger

01 | 前言

02 | 简单的分库分表设计

03 | 控制反转搭配简单业务

04 | 强化设计方案

05 | 完善业务自动创建数据库

06 | 最终篇-通过AOP自动连接数据库-完成日志业务

简介

模块化的介绍一共2篇

这一篇我们实现一个功能非常简单的StartupModules模块化。

第二篇我们来实现一个ABP的模块化效果。

思考

其实来简单想一下模块化的实验思路,写个接口=>模块类继承该接口=>项目启动反射检索=>调用接口实现。
那么具体到代码实践应该怎么写呢。

开始

第一步

第一步就是写一个模块化接口类的嘛!
新建类 IStartupModule

然后写一个反射检索全文谁继承了这个接口的方法
新建类 StartupModulesOptions


代码解释: Activator.CreateInstance 与指定参数匹配程度最高的构造函数来创建指定类型的实例
ps:白话文就是,你给我Type我给你创建个对应的实例
更一个有意思的是 Assembly.GetEntryAssembly()! 这个! 是不是很好奇怕
ps:我第一次看到这个语法也蒙了,问了好多人大家都没用过,这个语法同TS中的断言,是非null类型断言,意思就是我断言我这个方法返回的内容绝对不是null。

第二步

到这里来看我们是不是已经拿到了所有继承接口的模块那么怎么在该调用的地方调用呢,缺啥写啥

第三步

运行代码也有了,我该怎么调用呢。
接下来只要在 在Program 的 WebHost 调用.UseStartupModules() 流程就可以加载我们的 ConfigureServices 了

中间来插播一下

请让我掏出来一个器大的东西来说 他就是: IStartupFilter ,这个东西是个啥东西呢。来看一段源码

namespace Microsoft.AspNetCore.Hosting
{
    public interface IStartupFilter
    {
        Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
    }
}

这里我们看到了 Configure 他返回一个 IApplicationBuilder 他是怎么用的呢
我们新建一个空的Web项目的时候不知道有没有注意过 UseStaticFiles 这个函数

 public void Configure(IApplicationBuilder app)
        {
            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
            });
        }

在这个方法中,你可以直接使用方法提供的IApplicationBuilder参数,并且可以向其中添加各种中间件。使用IStartupFilter, 你可以指定并返回一个Action类型的泛型委托,这意味你除了可以使用方法提供的泛型委托配置IApplicationBuilder对象, 还需要返回一个泛型委托。

我们来看一段代码 Build(); 这个会调用BuildApplication方法

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()    
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .Build();

        host.Run(); 
    }
}

private RequestDelegate BuildApplication()
{
    ..
    IApplicationBuilder builder = builderFactory.CreateBuilder(Server.Features);
    builder.ApplicationServices = _applicationServices;

    var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
    Action<IApplicationBuilder> configure = _startup.Configure;
    foreach (var filter in startupFilters.Reverse())
    {
        configure = filter.Configure(configure);
    }

    configure(builder);

    return builder.Build();
}

首先,此方法创建IApplicationBuilder的实例,该实例将用于构建中间件管道,并将ApplicationServices设置为已配置的DI容器。
接下来的代码块很意思。首先,从DI容器中获取了一个集合IEnumerable<IStartupFilter>
我们可以配置多个IStartupFilter来形成一个管道,所以这个方法只是从容器中取出它们。
现在我们通过循环遍历每个IStartupFilter(以相反的顺序),传入Startup.Configure方法,然后更新局部变量configure来创建Configure方法的管道。

第四步

我们自己如何来实现 一个IStartupFilter 让他帮我们调用 Configure。

第五步

在 WebHostBuilderExtensions类 UseStartupModules 方法 ConfigureServices 下用 IStartupFilter 注入实现
这样在Build() 的时候就会调用模块的方法了

ActivatorUtilities.CreateInstance<ModulesStartupFilter>(sp, runner) // 第二个参数是在创建实例的时候 给构造函数注入的第一个参数

测试一下

新建 Core Web项目 在 Program.cs
 Host.CreateDefaultBuilder(args)
 .ConfigureWebHostDefaults(webBuilder =>
    {
         // 进行模块映射
         webBuilder.UseStartupModules().UseStartup<Startup>();
});

在 Startup.cs ConfigureServices和Configure 下打一个 Console.WriteLine

新建 类 HangfireStartupModule 继承 IStartupModule
public class HangfireStartupModule : IStartupModule
{
        public void ConfigureServices(IServiceCollection services)
        {
            Console.WriteLine("HangfireStartupModule----ConfigureServices");
        }
        public void Configure(IApplicationBuilder app)
        {
            Console.WriteLine("HangfireStartupModule----Configure");
        }
}
结果

一个非常简单的模块化就完工了,当然这个是基础版本,只能拿来借鉴思路学习下。

补充模块的 ConfigureServices 和 Configure 传递上下文

新建类 ConfigureServicesContext 和 ConfigureMiddlewareContext

  public class ConfigureMiddlewareContext
    {
        public ConfigureMiddlewareContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, IServiceProvider serviceProvider, StartupModulesOptions options)
        {
            Configuration = configuration;
            HostingEnvironment = hostingEnvironment;
            ServiceProvider = serviceProvider;
            Options = options;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment HostingEnvironment { get; }
        public IServiceProvider ServiceProvider { get; }

        public StartupModulesOptions Options { get; }
    }

    public class ConfigureServicesContext
    {
        public ConfigureServicesContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, StartupModulesOptions options)
        {
            Configuration = configuration;
            HostingEnvironment = hostingEnvironment;
            Options = options;
        }

        public IConfiguration Configuration { get; }
        public IWebHostEnvironment HostingEnvironment { get; }
        public StartupModulesOptions Options { get; }
    }

修改 StartupModuleRunner 的方法

 public void ConfigureServices(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
            var ctx = new ConfigureServicesContext(configuration, hostingEnvironment, _options);
            foreach (var cfg in _options.StartupModules)
            {
                cfg.ConfigureServices(services, ctx);
            }
        }

        public void Configure(IApplicationBuilder app, IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
            using (var scope = app.ApplicationServices.CreateScope()) {
                var ctx = new ConfigureMiddlewareContext(configuration, hostingEnvironment, scope.ServiceProvider, _options);

                foreach (var cfg in _options.StartupModules)
                {
                    cfg.Configure(app, ctx);
                }
            }
               
        }

鸣谢

玩双截棍的熊猫、NETCore-大黄瓜

思路来源:https://github.com/henkmollema/StartupModules

友联: https://github.com/DestinyCore/Destiny.Core.Flow

知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案一 – 初久的私房菜 – 博客园已关闭评论
备案信息冀ICP 0007948