关于 ZoDream

作者开了一个名为 ZoDream 的php框架。

RSS 地址: https://zodream.cn/blog/rss

请复制 RSS 到你的阅读器,或快速订阅到 :

ZoDream RSS 预览

RestAPI 架构设计

2024-04-06 06:12:21

占位

会员系统架构设计

2024-04-06 06:10:10

前言

本文主要从简单到详情讲解会员系统的设计逻辑,不涉及任何编程,主要通过图文方式通俗的讲述会员系统的前后端逻辑。

先决条件

本文适合以下人:

  1. 想要了解会员登录注册过程的
  2. 想要自己实现会员系统的技术人员
  3. 一些集成产品二次开发的技术人员

不适合的人:

  1. 使用 Wordpress 等现成系统的

概念讲解

会员系统是一切网站的基础,不论是后台管理。

分步指导

简单的会员系统

主要就两个字段: 账号、密码

使用于小型网站:就只有一个管理员管理发布内容的系统。

权限会员系统

权限根据详细程度又可以细分不同的权限模型

  1. DAC(自主访问控制 Discretionary Access Control)
  2. MAC(强制访问控制模型 Mandatory Access Control)
  3. RBAC(基于角色的访问控制模型 Role-based Access Control)
  4. ABAC(基于属性的访问控制模型 Attribute-Based Access Control)

前后台分离式会员系统

常见问题

  1. 会员系统怎么选型?

扩展知识

会员层级

设计会员等级体系:普通会员、VIP会员、钻石会员等

会员资金积分

会员推荐系统

总结

参考资源

  1. 权限管理--浅析权限管理模型(DAC, MAC, RBAC, ABAC)

周报:寻找优质的周刊

2024-04-02 19:06:44

周刊相对普通博客,提供了更多有趣新知识。

怎么找周刊

  1. Github 搜索 weekly 基本都是关于软件技术的周刊

weekly

  1. 相关周刊月刊 RSS 整合站点

https://www.fre123.com/weekly

weekly

周刊推荐

潮流周刊

https://weekly.tw93.fun/

每周分享潮流技术和潮流工具,标题什么的不重要。

潮流周刊

HelloGitHub月刊

https://hellogithub.com/periodical

每月分享 GitHub 上有趣、入门级的开源项目。

HelloGitHub

总结与感想

周刊月刊这种文章主要还是用来找乐子的,随便发现点有用的东西,当作放松方式还是不错的,至少能够拣点有用的东西。

不过,也不要寄希望于挖到什么大宝贝,因为写的人也不一定经常用里面推荐的工具,当时可能只是抱着“哟,看起不错哦”的心态记录一下。

总之,写写周报,就是闲时炫耀一下自己发现的“大宝贝”。

开发日志:对Markdown的代码块新增引用来源支持

2024-03-30 05:36:27

功能参考来源

SourceCodeTrace

使用方法

 
    ```php {1-4} {1} (http://)
1

解释说明:

  1. 使用 () 添加网址
  2. 使用 {} 添加起始行号或高亮行号,使用 - 表示范围,, 只能用于高亮,表示多个

示例

  1. 只有引用网址
php
 
    ```php (https://github.com/zodream/html/blob/master/src/MarkDown.php)
1
  1. 有引用网址和起始行号
php
           
    ```php {501} (https://github.com/zodream/html/blob/master/src/MarkDown.php)
    protected function parseQuoteLine(string $block): array {
        $res = array_map('intval', explode('-', $block));
        if ($res[0] < 1) {
            $res[0] = 1;
        }
        if (count($res) === 1 || $res[1] < $res[0]) {
            $res[1] = $res[0];
        }
        return $res;
    }
501502503504505506507508509510511
  1. 有引用网址和起始行号,还有高亮
php
           
    ```php {501}  {503,506}  (https://github.com/zodream/html/blob/master/src/MarkDown.php)
    protected function parseQuoteLine(string $block): array {
        $res = array_map('intval', explode('-', $block));
        if ($res[0] < 1) {
            $res[0] = 1;
        }
        if (count($res) === 1 || $res[1] < $res[0]) {
            $res[1] = $res[0];
        }
        return $res;
    }
501502503504505506507508509510511
  1. 只有高亮
php
           
    ```php {2-4}
    protected function parseQuoteLine(string $block): array {
        $res = array_map('intval', explode('-', $block));
        if ($res[0] < 1) {
            $res[0] = 1;
        }
        if (count($res) === 1 || $res[1] < $res[0]) {
            $res[1] = $res[0];
        }
        return $res;
    }
1234567891011

周报:怎么写技术类的教程文章

2024-03-29 23:39:35

本周还是在思考网站内容的发展方向。

使用 ClaudeAI 学习了下怎么写技术类的教程文章。

准备朝着这个标准写。同时也想改改已有的文章,让文章都超这个标准靠拢。

怎么写技术类的教程文章

一篇优秀的技术教程文章应当具备以下几个基本结构和要素:

  1. 标题

    标题应该简洁明了,能够准确表达教程主题,吸引读者阅读。

  2. 前言

    前言部分通常会简单介绍所要讲解的技术/工具是什么,解决什么问题,在哪些场景下会使用。让读者对整体内容有一个基本认知。

  3. 先决条件

    列出学习和实践本教程所需要具备的基础知识技能、环境配置等前提条件。

  4. 概念讲解

    对教程涉及的关键概念、理论原理进行深入浅出的讲解,融入适量的图示和案例,帮助读者理解和消化。

  5. 分步指导

    教程实战部分最为关键。通过分步骤的形式,引导读者一步一步操作实践。文字通俗易懂,操作简明流畅。可以附带运行输出截图。

  6. 常见问题

    总结实践过程中可能遇到的常见问题,并给出解决方法和建议。

  7. 扩展知识

    对教程内容进行适度延伸阐述,如进阶技巧、高级玩法、场景应用等,满足不同层次读者的需求。

  8. 总结

    全文总结并重申教程主题和重点,同时可以指出一些注意事项和最佳实践。

  9. 参考资源

    列出撰写教程时参考的权威资料出处,供读者进一步了解和学习。

此外,代码示例尽量保持简洁,重点突出,避免过多冗余。插图和动图可以辅助说明。层次分明的标题结构也有助于读者查阅和理解。交错适度的互动性语句会增加教程的亲和力。良好的教程文章不仅传递知识,还需要关注知识传递的体验。

css display:flex 布局尺寸超出问题

2024-03-25 04:53:59

使用 flex 布局中,经常会出现父容器的尺寸被子元素撑开,而不是子元素自适应父容器的尺寸。

例如:

解决方法

在子元素的样式上设置 width0 即可

css
 
width: 0;
1

周报:SEO优化的思考

2024-03-20 17:36:20

本周主要是对网站进行SEO优化,想通过SEO优化提高网站的流量。

从关键词优化入手:

先是从本网站的访客来源入手,

方式一:从搜索引擎站点工具查看

当前的搜索引擎都对用户搜索关键字进行了加密,所以基本只能从各大搜索引擎提供的站长工具查看。

[error] tsserver exited. code: null. signal: sigterm
vscode 正在初始化 js/ts 语言功能
typescript language server exited with error. error message is: channel closed
vscode tsserver exited. code: null. signal: sigterm
瀑布流元素被切割

从这些来源关键词,不难发现基本是:用户编程过程中遇到了某个问题,编程软件出BUG、代码实现有问题等。

所以编程遇到问题才是本站的流量来源关键。

方式二:流量统计工具

这种方式就是查看着陆页,那些文章点击多、受欢迎。

还有就是用户来源,来源搜索引擎、友链等占比情况,确定是做SEO优化还是发站外链接为主。

再到内容优化:

寻找热门的关键词,选择内容创作方向。

本站最初的选择方向是编程方向,所以选择的内容也是这个方向。

所以需要提高相关关键词的含量。

最后才发现,问题还是出现在内容上!

本网站的内容基本上是编程技术相关的内容。

第一内容没有用户粘性,基本上访客访问的都是编程出现问题,寻找解决方法的。

第二内容并没有比同类站点更专业,受限于本人对技术深度探究有限,并不能写出令人一亮的技术观点。

第三受众理解不清晰,本站的内容都是平时工作中遇到的东西,基本上很难有同类,而且有教程用B站,编程用ChatGPT,其他制作用Canva类集成大平台。

因此本站发展方向要么是生活类思考观点做用户粘性;要么做小问题专业解答接收搜索引擎的流量。

思考:在AI的趋势下怎么生存?

AI是大趋势,AI能快速提供相对准确的答案(但需要一定的相关知识判断力)。

AI能提供文字、图像、视频的生成,但是还是需要人的主导。

Edge 浏览器不适用 Edge Image Viewer 打开图片

2024-03-10 22:15:00

背景

在使用 Edge 浏览器 浏览网页时查看图片,默认会调用 Edge Image Viewer 打开图片,增加了图片编辑等功能,但是实际是跳转了一个网址,导致加载显示图片太慢。

所以怎么关闭 Edge Image Viewer 呢?

解决方法

在浏览器右上角打开进入 设置

在设置页面点击 下载

关闭 “允许边缘图像查看器打开图像” 即可

SEO 学习笔记(一) 内容来源

2024-03-06 22:44:41

SEO 学习笔记(一) 内容来源

学习目标

  1. 了解如何获取内容 idea
  2. 如何快速更新文章

原创

完全基于个人兴趣写作,记录关于个人的生活学习工作内容。

这类文章完全取决个人的兴趣爱好,当兴趣爱好跟浏览者的喜好一致时,获得的流量就多,反之,就没什么点击量。

优点:

  1. 搜索引擎都喜欢原创的文章内容。
  2. 受众集中,转换为订阅者、粉丝的几率高,访客的粘性高,访客定向性高符合广告主青睐。
  3. 个人的表达欲得到了满足。

缺点:

  1. 更新频率不可能太高。
  2. 受众固定。
  3. 不适合赚快钱的新人。

搬运

从其他平台转载内容。例如:从国内平台搬运至外网。

优点:

  1. 更新频率高,几乎没有创作成本。
  2. 因为是已经验证过的高流量内容,几乎可以复制高流量现象。

缺点:

  1. 侵权,版权问题。
  2. 同行竞争,导致相同的内容多,反而可能被平台限制。
  3. 难以获得广告收益。
  4. 有封号、被定义垃圾站的风险。

案例:

  1. 国内视频网与国外视频网的视频搬运转载。

伪原创

俗称蹭热度,查看搜索引擎流量热点,进行相关创作。这是最好的方法。

原文转换

  1. 翻译转换。
  2. AI 视频图片内容生成。

这种相对比搬运高级一点,但不多,因为核心内容不变。

案例:

  1. 从微信公众号选取热门文章,进行AI配音放到 Youtube 上。

选取热门关键词进行创作

这种方法跟原创几乎没有区别,一样的需要花费大量时间进行创作,但是这种方法比原创更能把握流量热点。

用计算机术语来讲就是:面向搜索引擎创作。

步骤:

  1. 使用SEO工具获取热门关键词
  2. 使用搜索引擎搜索这些关键词,选择合适的参考文章。
  3. 进行内容创作,同时加入一些相关的关键词。

使用 Google Search ConsoleGoogle Analytics 挖掘网站关键词

选取关键词进行搜索

PHP 实现双因素身份认证(2FA)

2024-02-27 22:44:09

PHP 实现双因素身份认证(2FA)

双因素身份认证,简单理解就是使用账户密码登录后需要使用一个动态码确认,账户密码动态码 两种方式登录,多一步就多一点安全性,

但是,这种方式也牺牲了方便。因此,有多种形式的动态码确认,常见的就有:基于TOTP验证APP,例如Google Authenticator、微软的 Authenticator;网上银行的U盾,这类第三方专属物理设备验证。

TOTP

今天,需要实现的是基于 TOTP (基于时间的一次性密码)实现的两步验证。

需要实现的步骤如下:

  1. 用户登录后,需要手动启用两步验证,
  2. 生成专有的恢复码和包含密钥的二维码,
  3. 用户使用Authenticator扫码后,需要提供Authenticator生成的动态码进行启用,
  4. 用户重新登录后需要提供动态码才能完成登录操作

代码实现

依赖

TwoFactorAuth

 
composer require robthree/twofactorauth
1

生成密钥

PHP
         

use RobThree\Auth\TwoFactorAuth;

$provider = new TwoFactorAuth('你的域名');

$secret_key = $provider->createSecret();

$qr = $provider->getQRCodeImageAsDataUri('用户的名称获取ID', $secret_key);
123456789

显示二维码即可,当然要保存 $secret_key 跟用户关联上;

验证动态码

基本原理: 每30秒生成一个动态码

   
TC = floor(unixtime(now) / 30)

TOTP = HASH(SecretKey, TC)
123
php
     
use RobThree\Auth\TwoFactorAuth;

$provider = new TwoFactorAuth('你的域名');

$provider->verifyCode($secret_key, $_POST['code']); // bool
12345

第一步开启2fa

登录强制要求动态码

参考

TwoFactorAuth

winui3 自定义标题栏

2023-11-21 01:35:22

winui3 自定义标题栏

MainWindow.xaml 添加

xml
          
<Grid  Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition MinHeight="48"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <!-- 标题栏 -->
    <Border x:Name="AppTitleBar" VerticalAlignment="Top">
        <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" />
    </Border>
</Grid>
12345678910

MainWindow.xaml.cs 中添加

c#
        
public MainWindow()
{
    this.InitializeComponent();

    // 自定义标题栏
    ExtendsContentIntoTitleBar = true;
    SetTitleBar(AppTitleBar);
}
12345678

左侧添加按钮

如果只是在左侧添加按钮,可以直接使用 xaml 定义

MainWindow.xaml 添加

xml
                 
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition MinHeight="48"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinHeight="auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button Content="返回" Click="BackBtn_Click"/>
        <!-- 标题栏 -->
        <Border x:Name="AppTitleBar" Grid.Column="1" VerticalAlignment="Top">
            <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" />
        </Border>
    </Grid>
</Grid>
1234567891011121314151617

添加自定义可点击内容

如果需要在中间添加可点击可输入内容,那么就需要这样做

MainWindow.xaml 添加

xml
                 
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition MinHeight="48"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Border x:Name="AppTitleBar" VerticalAlignment="Top">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" />
            <TextBox x:Name="SearchTb" Grid.Column="1" />
        </Grid>
    </Border>
</Grid>
1234567891011121314151617

MainWindow.xaml.cs 中添加

c#
                         
public MainWindow()
{
    this.InitializeComponent();

    // 自定义标题栏
    ExtendsContentIntoTitleBar = true;
    SetTitleBar(AppTitleBar);

    var _baseWindowHandle = WindowNative.GetWindowHandle(this);
    var windowId = Win32Interop.GetWindowIdFromWindow(_baseWindowHandle);
    var _appWindow = AppWindow.GetFromWindowId(windowId);
    var dpiScale = Content.XamlRoot.RasterizationScale;
    var nonClientSource = InputNonClientPointerSource.GetForWindowId(_appWindow.Id);

    var inputPoint = SearchTb.TransformToVisual(Content).TransformPoint(new Point(0, 0));
    nonClientSource.ClearRegionRects(NonClientRegionKind.Passthrough);
    nonClientSource.SetRegionRects(NonClientRegionKind.Passthrough, 
    // 可以添加多个区域
        [new(
            (int)Math.Round(inputPoint.X * dpiScale),
            (int)Math.Round(inputPoint.Y * dpiScale),
            (int)Math.Round(SearchTb.ActualWidth * dpiScale), // 请注意,要获取到可点击元素的显示尺寸才行
            (int)Math.Round(SearchTb.ActualHeight * dpiScale)
    )]);
}
12345678910111213141516171819202122232425

Command 绑定

使用 Command={TemplateBinding BackCommand} 可能无效

可以使用 Command="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=BackCommand}"

WPF MVVM 获取List 多选数据

2023-11-21 01:33:59

WPF MVVM 获取List 多选数据

单选绑定

使用 SelectedIndexSelectedItem 都可以

xml
  
<ListBox x:Name="dataGrid1" SelectedIndex="{Binding SelectedIndex}" SelectedItem="{Binding SelectedItem}">
</ListBox>
12

多选绑定

要先添加依赖项 System.Windows.Interactivity.dll

xml
            
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"




<ListBox x:Name="dataGrid1">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
        <i:InvokeCommandAction Command="{Binding SelectionChangeCommand}" CommandParameter="{Binding SelectedItems,ElementName=dataGrid1}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>
123456789101112

php 接入 WebAuthn 登录

2023-10-15 02:47:48

php 接入 WebAuthn 登录

字节数据传输(关键问题)

解决方法:使用 base64 编解码;

接下来 需要解决 base64 算法不一致问题,

例如:浏览器自带的 js

ts
  
window.btoa(val: string): string // base64 编码
window.atob(val: string): string // base64 解码
12

例如:php 自带的

php
  
base64_encode(val: string): string // base64 编码
base64_decode(val: string): string // base64 解码
12

实际,webAuthn 一些数据是 ArrayBuffer 所以默认的就不行了

js base64 处理

ts
                                                                  
class Base64 {

    private static chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
    private static lookup = new Uint8Array(256);
    private static booted = false;

    private static ready() {
        if (this.booted) {
            return;
        }
        this.booted = true;
        for (let i = 0; i < this.chars.length; i++) {
            this.lookup[this.chars.charCodeAt(i)] = i;
        }
    }

    public static encode(arraybuffer: ArrayBuffer): string {
        const bytes = new Uint8Array(arraybuffer);
        const len = bytes.length;
        let base64 = '';

        for (let i = 0; i < len; i += 3) {
            base64 += this.chars[bytes[i] >> 2];
            base64 += this.chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
            base64 += this.chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
            base64 += this.chars[bytes[i + 2] & 63];
        }

        if (len % 3 === 2) {
            base64 = base64.substring(0, base64.length - 1);
        } else if (len % 3 === 1) {
            base64 = base64.substring(0, base64.length - 2);
        }

        return base64;
    }

    public static decode(base64: string): ArrayBuffer {
        const len = base64.length;
        const bufferLength = len * 0.75;
        const arraybuffer = new ArrayBuffer(bufferLength);
        const bytes = new Uint8Array(arraybuffer);

        let p = 0;
        for (let i = 0; i < len; i += 4) {
            const encoded1 = this.lookup[base64.charCodeAt(i)];
            const encoded2 = this.lookup[base64.charCodeAt(i + 1)];
            const encoded3 = this.lookup[base64.charCodeAt(i + 2)];
            const encoded4 = this.lookup[base64.charCodeAt(i + 3)];

            bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
            bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
            bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
        }

        return arraybuffer;
    }

    public static toBuffer(val: string): ArrayBuffer {
        const items: number[] = [];
        for (let i = 0; i < val.length; i++) {
            items.push(val.charCodeAt(i));
        }
        return new Uint8Array(items);
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

php 实际只要实现一个 base64 解码就行了,其他的通过字符串传给前端,前端在转成 ArrayBuffer

php
                           

    public static function decodeBase64(string $base64): string {
        static $lookup = [];
        if (empty($lookup)) {
            $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
            for ($i = 0; $i < strlen($chars); $i ++) {
                $lookup[ord(substr($chars, $i, 1))] = $i;
            }
        }
        $len = strlen($base64);
        $maxLen = (int)floor($len * .75);
        $buffer = [];
        for ($i = 0; $i < $len; $i += 4) {
            $encoded1 = $lookup[ord(substr($base64, $i, 1))];
            $encoded2 = $lookup[ord(substr($base64, $i + 1, 1))];
            $encoded3 = $lookup[ord(substr($base64, $i + 2, 1))];
            $encoded4 = $lookup[ord(substr($base64, $i + 3, 1))];

            $buffer[] = chr(($encoded1 << 2) | ($encoded2 >> 4));
            $buffer[] = chr((($encoded2 & 15) << 4) | ($encoded3 >> 2));
            $buffer[] = chr((($encoded3 & 3) << 6) | ($encoded4 & 63));
        }
        if (count($buffer) > $maxLen) {
            array_splice($buffer, $maxLen);
        }
        return implode('', $buffer);
    }
123456789101112131415161718192021222324252627

准备工作

一个 php 的 CBOR 解码库

 
composer install spomky-labs/cbor-php
1

一个 php 的 pem 转换库, 因为从浏览器获取的公钥是没法直接通过 openssl_get_publickey 加载的

这些都可以通过一个库解决,不过依赖库比较多

 
composer install web-auth/webauthn-framework
1

步骤

  1. 第一步,注册 Passkey, 在已登录的情况下,点击一个按钮进行注册
html
 
<button class="register-webauth">注册Passkey</button>
1
ts
                          
$('.register-webauth').on('click',function() {
    if (!navigator.credentials) {
        return;
    }
    // 从后台获取注册需要数据,包含当前登录账户的id
    $.getJSON(baseUri + '/passkey/register_option', res => {
        const data = res.data;
        data.challenge = Base64.toBuffer(data.challenge);
        data.user.id = Base64.toBuffer(data.user.id);
        navigator.credentials.create({
            publicKey: data
        })
        .then((credential: any) => {
            const response = credential.response as AuthenticatorAttestationResponse;
            // 保存注册成功的 credentialId 和 公钥
            $.post(baseUri + '/passkey/register', {credential: {
                id: credential.id,
                clientDataJSON: Base64.encode(response.clientDataJSON),
                attestationObject: Base64.encode(response.attestationObject),
                publicKeyAlgorithm: response.getPublicKeyAlgorithm(),
            }}, res => {}, 'json');
        })
        .catch(console.error);
    });

}).toggle(!!navigator.credentials);
1234567891011121314151617181920212223242526
  1. 注册需要的数据

passkey/register_option

php
                               
return [
    'challenge' => $challenge, // 随机的字符串,防止重复操作,需要保存,获取到 公钥后需要验证
    'rp' => [
        'name' => Option::value('site_title'),
        'id' => request()->host()
    ],
    'user' => [
        'id' => (string)$user->getIdentity(), // 用户id
        'name' => $user->email,               // 用户邮箱
        'displayName' => $user->name          // 显示的名
    ],
    'pubKeyCredParams' => [[
        'alg' => -7,                          // ES256 公钥类型
        'type' => 'public-key'
    ], [
        'type' => 'public-key',
        'alg' => -257                         // RS256 公钥类型,这个选项好像是必须的,不然可能不成功
    ]],
    'timeout' => $timeout * 1000,
    'excludeCredentials' => [],
    'attestation' => 'none',
    'authenticatorSelection' => [
        'authenticatorAttachment' => "platform",
        "residentKey" => "preferred",
        'requireResidentKey' => false,
        'userVerification' => 'preferred'
    ],
    'extensions' => [
        'credProps' => true
    ]
];
12345678910111213141516171819202122232425262728293031
  1. 保存注册成功的公钥

passkey/register

php
                 
public static function register(array $credential): void {
    $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON']));
    if ($clientDataJSON['type'] !== 'webauthn.create') {
        throw new \Exception('type is error');
    }
    $challenge = base64_decode($clientDataJSON['challenge']);
    // TODO 验证临时

    $obj = static::parseAuthenticatorData($credential['attestationObject']);
    // 解码 attestationObject,获取 公钥
    if (empty($obj) || empty($obj['publicKey'])) {
        throw new Exception('attestation is error');
    }
    self::saveCredential($credential['id'], $obj['publicKey'],
        intval($credential['publicKeyAlgorithm']));
    // TODO 保存公钥
}
1234567891011121314151617
  1. 登录页添加,使用 Passkey 登录的按钮
html
 
<button class="login-webauth">Passkey 登录</button>
1
ts
                             
$('.login-webauth').on('click',function() {
    if (!navigator.credentials) {
        return;
    }
    // 从后台获取登录需要数据
    $.getJSON(baseUri + '/passkey/login_option', res => {
        const data = res.data;
        data.challenge = Base64.toBuffer(data.challenge);
        navigator.credentials.get({
            publicKey: data
        })
        .then((credential: any) => {
            const response = credential.response as AuthenticatorAssertionResponse;
            // 获取登录结果,验证数据有效,根据id登录
            $.post(baseUri + '/passkey/login', {
                credential: {
                    id: credential.id,
                    clientDataJSON: Base64.encode(response.clientDataJSON),
                    authenticatorData: Base64.encode(response.authenticatorData),
                    userHandle: Base64.encode(response.userHandle),
                    signature: Base64.encode(response.signature)
                },
                redirect_uri: $('[name=redirect_uri]').val()
            }, res => {}, 'json');
        })
        .catch(console.error);
    });

}).toggle(!!navigator.credentials);
1234567891011121314151617181920212223242526272829
  1. 登录需要的数据

passkey/login_option

php
       
return [
    'challenge' => $challenge, // 防止重复操作的随机的字符串
    'timeout' => $timeout * 1000,
    'rpId' => request()->host(),
    'allowCredentials' => [],
    'userVerification' => 'preferred'
];
1234567
  1. 验证登录数据,登录id

passkey/login

php
                                           
    public static function login(array $credential): void {
        $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON']));
        $challenge = base64_decode($clientDataJSON['challenge']);
        $key = sprintf('%s-%s', self::REGISTER_KEY, $challenge);
        if (!cache()->has($key)) {
            throw new \Exception('challenge is expired');
        }
        $userId = base64_decode($credential['userHandle']);
        // 可以验证 credentialId 的 hash 值是否一致 
        $signature = $credential['signature'];
        self::loadCredential(intval($userId), $credential['id'], $signature, $credential);
    }

    /**
     * 验证的登录数据
     * @param int $userId
     * @param string $credentialId
     * @param string $signature
     * @param array $credential
     * @return void
     * @throws \Exception
     */
    protected static function loadCredential(int $userId, string $credentialId,  string $signature, array $credential) {
        // 获取保存的 公钥
        $key = '';
        if (empty($key)) {
            throw new \Exception('验证失败');
        }
        $data = CBOR::decodeBase64($credential['authenticatorData']);
        $pkey = openssl_get_publickey($key);
        if (empty($pkey)) {
            throw new Exception('public key is error');
        }
        if (!openssl_verify($data.self::hash(CBOR::decodeBase64($credential['clientDataJSON'])),
            CBOR::decodeBase64($signature), $pkey, \OPENSSL_ALGO_SHA256)) {
            throw new \Exception('signature is error');
        }
        // TODO 登录
    }

    private static function hash(string $val): string {
        return \hash('sha256', $val, true);
    }
12345678910111213141516171819202122232425262728293031323334353637383940414243

源码

PassKey主文件

CBOR

PEM

Burp Suite 抓包

2023-10-15 02:46:15

Burp Suite 抓包

http

  1. 打开 Proxy -> Proxy settings 确认代理地址 127.0.0.1:8080

Snipaste_2023-10-06_14-19-08.png

Snipaste_2023-10-06_14-19-31.png

  1. 打开 系统设置 -> 网络与Internet -> 代理 -> 手动设置代理

开启 使用代理服务器

地址和端口 填 Burp Suite Proxy settings 的代理地址

点击保存即可

  1. 使用浏览器打开一个网址
  2. Burp Suite Proxy -> HTTP history 查看所有的请求响应内容

https

第1、2步 同上

  1. 使用浏览器打开 http:.//127.0.0.1:8080 页面,点击页面右上角的 CA Certificate 下载证书
  2. 打开浏览器(EDGE)的设置 搜索 证书, 点击管理证书,受信任的根证书颁发机构,导入 下载的证书
  3. 在证书列表中找到导入的证书 PortSwigger CA, 导出新的证书,再导入新的证书
  4. 使用浏览器打开一个网址
  5. Burp Suite Proxy -> HTTP history 查看所有HTTPS的请求响应内容

lnmp php集成环境安装包使用

2023-06-28 22:54:32

lnmp php集成环境安装包使用

安装

官网

      
screen -S lnmp # 如果命令不存在 先运行 yum install screen 

 cd /usr/local  # 进入php安装的目录

wget http://soft.vpser.net/lnmp/lnmp2.0.tar.gz -O lnmp2.0.tar.gz && tar zxf lnmp2.0.tar.gz && cd lnmp2.0 && ./install.sh lnmp
123456

输入 mysql root 账户的密码

选择 mysql 和 php 的版本

等待安装完成

添加 站点

   
cd /usr/local/nginx/conf/vhost

vi site.conf
123
               
server {
    server_name zodream.cn;
    root /data/httpd/www; 
    index index.html index.htm index.php; 
    location / {
        autoindex on;
    }
    include enable-php.conf

    access_log /data/httpd/www/access_log/site.log;
    error_log /data/httpd/www/access_log/error.logcrit;
    listen 443 ssl;
    ssl_certificate /data/httpd/ssl/zodream.cn.pem;
    ssl_certificate_key /data/httpd/ssl/zodream.cn.key;
}
123456789101112131415

使用命令

        
lnmp restart 重启整个环境

/etc/init.d/mysql restart

/etc/init.d/nginx restart

/etc/init.d/php-fpm restart
12345678

运维脚本

  1. 备份数据库和站点文件

编辑 /usr/local/lnmp2.0/tools/backup.sh

              
######~备份到哪里~######
Backup_Home="/home/backup/" 
######~需要备份哪些文件夹~######
Backup_Dir=("/home/wwwroot/vpser.net" "/home/wwwroot/lnmp.org")

######~需要备份哪些数据库~######
Backup_Database=("lnmp" "vpser")

######~数据库root 的账户密码~######
MYSQL_UserName='root'
MYSQL_PassWord='yourrootpassword'

######~是否需要备份到其他ftp地址,0 需要配置 ftp信息, 1 则是本地服务器,不需要ftp信息~######
Enable_FTP=1
1234567891011121314
  1. 验证站点是否崩溃挂掉了

编辑 /usr/local/lnmp2.0/tools/check502.sh

  
######~填入您的站点网址即可~######
CheckURL="http://www.xxx.com"
12
  1. 分割 nginx 日志文件

编辑 /usr/local/lnmp2.0/tools/cut_nginx_logs.sh

    
######~保存日志的文件夹~######
log_files_path="/home/wwwlogs/"
######~多个日志文件名~######
log_files_name=(access vpser licess)
1234
  1. 定时执行脚本

确定安装了 crontab

     
yum install vixie-cron crontabs

chkconfig crond on

service crond start
12345

添加任务

 
crontab -e
1
           
 # 五分钟检测一次

*/5 * * * * check502.sh >/dev/null 2>&1

# 凌晨自动切割nginx日志

0 0 * * * cut_nginx_logs.sh >/dev/null 2>&1

# 凌晨2点进行一次数据与文件备份

0 2 * * * backup.sh >/dev/null 2>&1
1234567891011

遇到的问题

主要查看 nginxerror_log 错误日志文件

Q: PHP message: PHP Warning: Unknown: open_basedir restriction in effect. File(/data/httpd/www/ecshop/index.php) is not within the allowed path(s): (/home/wwwroot/default/:/tmp/:/proc/) in Unknown on line 0

A:打开 nginx/conf/fastcgi.conf 配置文件,在 fastcgi_param PHP_ADMIN_VALUE "open_basedir=" 中添加站点执行文件的根目录,多个路径以:分隔,例如:fastcgi_param PHP_ADMIN_VALUE "open_basedir=$document_root/:/tmp/:/proc/:/data/httpd/www/phpMyAdmin";

js 进行在线编辑器开发

2023-05-16 01:36:13

js 进行在线编辑器开发

基于 textarea 开发的 markdown 编辑器

textarea 最简单,但是可以编辑的内容也少。

需要的了解的知识

ts
                            
const element: HTMLTextAreaElement;

element.selectionStart // 获取或设置选中的起始位置
element.selectionEnd // 获取或设置选中的结束位置

// 获取选中的文字

const v = element.value;
const selectValue = v.substring(element.selectionStart, element.selectionEnd);

// 替换选中的内容

const replace = '';
element.value = v.substring(0, element.selectionStart) + replace + v.substring(element.selectionEnd);

// 移动光标到指定位置,移动光标到开始位置

element.selectionStart = 0 
element.selectionEnd = 0

// 移动光标到结尾

element.selectionStart = element.value.length
element.selectionEnd = element.value.length

// 需要把焦点设置到元素,才会显示光标
element.focus();
12345678910111213141516171819202122232425262728

textarea 只接受字符串,所以图片,超链接等需要使用 markdown 格式转成对应的字符串

基于 div 开发的 富文本编辑器

把div 设置为可编辑模式

html
 
<div contentEditable="true"></div>
1

需要了解的知识

  1. 默认换行是自动添加 '<div><br></div>'
  2. 获取选中
ts
                                     
const element: HTMLDivElement;

const sel = window.getSelection();
const range = sel.getRangeAt(0); 
// range 就是当前选中的内容了
range.startContainer // 选中的起始节点
range.startOffset   // 在起始节点的具体位置
range.endContainer  // 选中的结束节点
range.startOffset   // 在结束节点的具体位置

// 当未选中任何内容时
range.startContainer === range.endContainer && range.startOffset === range.endOffset

range.startContainer // 节点的类型,有 Text 、HtmlElement
range.startOffset  // 当 节点类型为 Text 时,则为字符串的位置, 当为 HtmlElement 时,则为在节点中子元素的位置, 例如0 则是元素的最前面

// 选中 区域
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(selectRange);

// 选中指定元素

const selectRange = document.createRange();
// 选中整个元素
selectRange.selectNodeContents(element);
// 选中元素的一部分
selectRange.setStart(element, 0);
selectRange.setEnd(element, 0);

sel.removeAllRanges();
sel.addRange(selectRange);

// 截断字符串节点
const node: Text
node.splitText(offset) // 返回新创建的后一部分字符串的节点, 自动会添加到页面上的
12345678910111213141516171819202122232425262728293031323334353637

基于 div 开发的 代码编辑器

相对来说,代码编辑器相对简单,就只要 显示对应行号、对部分字符串加样式即可,当然更高级的需要加代码提示候选就更复杂点了。

ts
   
const element: HtmlDivElement;

element.contentEditable = 'true';
123

基本逻辑

  1. 对回车进行换行处理,实际就是在选择的地方切断,把后面一部分放到另一行
  2. 新增行,需要同时增加行号,及在行号列表中增加新的一行,并同时个更新后面的行号
  3. 对行的高度要跟行号的高度进行同步
  4. 缩进或者文字输入需要提取整行内容进行处理
ts
                  
let isComposition = false; // 判断是否是输入法输入

element.addEventListener('keydown', e => {
});
element.addEventListener('keyup', () => {
    if (isComposition) {
        return;
    }

});
element.addEventListener('compositionstart', () => {
   isComposition = true;

});
element.addEventListener('compositionend', () => {
    isComposition = false;

});
123456789101112131415161718
  1. div 的尺寸变化要进行行号高度更新
ts
              
let lastHeight = 0;
const resizeObserver = new ResizeObserver(entries => {
    for (const item of entries) {
        if (item.contentRect.height === lastHeight) {
            continue;
        }
        if (lastHeight === 0) {
            // 这里的意思是, 显示隐藏切换时进行高度更新
            this.updateLineNoStyle();
        }
        lastHeight = item.contentRect.height;
    }
});
resizeObserver.observe(element);
1234567891011121314

使用 indexnow 注意事项

2023-04-18 06:59:16

  1. 无法删除提交网址
  2. 只有第一次需要验证key文件,所以请验证完成后立即删除这个文件。
  3. 请注意不要泄漏这个key,因为这个key 是没有是使用限制的,在其他地方也能使用这个key,可以通过搜索引擎的indexnow查看提交记录,如果确定不是自己提交的,需要马上生成一个新的,旧的就会失效。
  4. 生成key的网址就是提交的网址,例如:https//www.bing.com/indexnow,只要不使用就不会生效

Godot 使用字体图标 例如: Iconfont、FontAwesome

2023-04-11 20:56:07

Godot 使用字体图标 例如: Iconfont、FontAwesome

新增一个控件 例如 IconLabl

新建一个场景,继承至用户界面,命名为 IconLabel

增加一个子控件 Label 设置Font .ttf 增加脚本

C# 版

c#
                                                                      
using Godot;
using System;
using System.Text.RegularExpressions;

[Tool]
public partial class IconLabel : Control
{

    private string text;
    [Export]
    public string Text
    {
        get { return text; }
        set { 
            text = value;
            ApplyText();
        }
    }

    private int fontSize = 16;
    [Export]
    public int FontSize
    {
        get { return fontSize; }
        set { 
            fontSize = value;
            ApplyFontSize();
        }
    }

    private Label IconTb;

    public override Vector2 _GetMinimumSize()
    {
        return new Vector2(FontSize, FontSize);
    }

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        IconTb = GetNode<Label>("Label");
        ApplyFontSize();
        ApplyText();
    }

    private void ApplyFontSize() {
        if (IconTb is null) {
            return;
        }
        if (fontSize > 0) {
            IconTb.AddThemeFontSizeOverride("font_size", FontSize);
        }
    }

    private void ApplyText() {
        if (IconTb is null) {
            return;
        }
        if (string.IsNullOrWhiteSpace(Text)) {
            IconTb.Text = string.Empty;
            return;
        }
        var text = Regex.Replace(Text, @"(&#x|\\u)([0-9a-f]+);?", match => {
            return Convert.ToChar(Convert.ToInt32(match.Groups[2].Value, 16)).ToString();
        }, RegexOptions.IgnoreCase);
        IconTb.Text = text;
        CustomMinimumSize = new Vector2(FontSize * text.Length, FontSize);
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

支持 Xaml 和 十六进制写法

text
  

\ue001
12

angular 15 对指定页面进行访问限制

2023-04-02 03:58:24

angular 15 对指定页面进行访问限制

原本的访问控制是通过实现 CanActivate 接口完成的,当时并不支持 Observable 异步返回判断结果,现在终于实现了!

例如:需要登录才能访问

ts
                          
/**
 * 需要登录才能访问的页面控制
 * @param _ 
 * @param state 
 * @returns 
 */
export const CanActivateViaAuthGuard: CanActivateFn = (_, state) => {
    return inject(AuthService).canActivate(state.url);
};



// 使用注册到路由上
const routes: Routes = [
    {
        path: 'finance',
        canActivate: [CanActivateViaAuthGuard],
        component: HomeComponent
    },
]

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class BackendModule {}
1234567891011121314151617181920212223242526

例如:需要登录且需要相关权限才能访问

ts
                         
/**
 * 需要有某种权限才能访问的页面控制
 * @param roles 
 * @returns 
 */
export function CanActivateAuthRole(...roles: string[]): CanActivateFn {
    return (_, state) => {
        return inject(AuthService).canActivate(state.url, ...roles);
    }
}

// 使用注册到路由上
const routes: Routes = [
    {
        path: 'finance',
        canActivate: [CanActivateAuthRole('admin')],
        component: HomeComponent
    },
]

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class BackendModule {}
12345678910111213141516171819202122232425

AuthService 实现

ts
                                       
@Injectable()
export class AuthService {
    constructor(
        // 使用 ngrx-store 保存全局数据的
        private store: Store<AppState>,
        // 生成登录网址
        private router: Router,
        // 消息提示组件
        private toastrService: DialogService) {}

    public canActivate(uri: string, ...roles: string[]) {
        return this.store
        .select(selectAuth)
        .pipe(map(res => {
            /*
            * res: {
                guest: boolean, // 是否是游客状态
                roles: string[], // 保存当前用户的所有权限
            }
            */
            if (res.guest) {
                // 跳转到登录页面,并指定登录后的返回网址
                return this.router.createUrlTree(['/auth'], {queryParams: {redirect_uri: uri}});
            }
            if (roles.length === 0) {
                return true;
            }
            if (res.roles) {
                for (const item of roles) {
                    if (res.roles.indexOf(item)) {
                        return true;
                    }
                }
            }
            this.toastrService.error('无权限访问');
            return false;
        }));
    }
}
123456789101112131415161718192021222324252627282930313233343536373839

完整代码

CanActivate

AuthService

Store

CSS 使用 column-count 实现瀑布流出现内容分割的解决办法

2023-03-31 22:54:06

起因

使用 column-count 实现瀑布流时,出现内容被分割,大部分都在同一列,但是在一列的最后一个会出现部分内容被分割到了另一列。

css
       
.message-box {
    column-count: 2;
    column-gap: .8rem;
}
.message-list-item {
    display: block;
}
1234567

column-count split

第一种方法(推荐)

css
    
.message-list-item {
    page-break-inside: avoid;
}
1234

原文链接

column-count瀑布流导致元素被截断-解决方法

第二种方法

css
    
.message-list-item {
    -webkit-column-break-inside: avoid;
}
1234

原文链接

关于column-count多列布局内容被切割在下列的解决方法以及瀑布流实现方式

第三种方法

css
    
.message-list-item {
    height: 100%;
    overflow: auto;
}
1234

原文链接

CSS3多列样式column布局内容被截断

angular 15 实现按下确认键,焦点移动到下一个表单或提交表单

2023-03-31 04:39:25

angular 15 实现按下确认键,焦点移动到下一个表单或提交表单

需求:登录界面,输入账户后,按下回车键,焦点移动到密码输入框,再次按下回车键,直接提交表单

实现方法

ts
                                                                                                                                    
import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core';


@Directive({
    selector: '[appFocusNext]'
})
export class FocusNextDirective implements OnInit, OnDestroy {

    static InputItems: FocusNextDirective[] = [];

    /**
     * 分组
     */
    @Input() public appFocusNext: any = 0;
    /**
     * 排序,越大越往后
     */
    @Input() public order = 0;

    constructor(
        private elementRef: ElementRef,
    ) { }

    @HostListener('keydown', ['$event'])
    onKeydown(e: KeyboardEvent) {
        if (e.key !== 'Enter') {
            return;
        }
        e.preventDefault();
        e.stopPropagation();
        FocusNextDirective.FocusNext(this);
    }


    ngOnInit(): void {
        FocusNextDirective.Add(this);
    }

    ngOnDestroy(): void {
        FocusNextDirective.Remove(this);
    }

    /**
     * 移动焦点到当前项
     * @returns 
     */
    public focus() {
        if (!this.elementRef.nativeElement) {
            return;
        }
        const element = this.elementRef.nativeElement;
        const tagName = element.tagName.toLocaleLowerCase();
        if (tagName === 'textarea' || tagName === 'select') {
            (element as HTMLTextAreaElement).focus();
            return;
        }
        if (tagName === 'input') {
            const type = (element as HTMLInputElement).type.toLocaleLowerCase();
            console.log(type);

            if (type === 'button' || type === 'submit' || type === 'reset') {
                element.click();
                return;
            }
            (element as HTMLInputElement).focus();
            return;
        }
        if (tagName === 'form') {
            (element as HTMLFormElement).submit();
            return;
        }
        element.click();
    }

    /**
     * 注册到可操作项
     * @param item 
     * @returns 
     */
    private static Add(item: FocusNextDirective) {
        if (this.InputItems.indexOf(item) >= 0) {
            return;
        }
        this.InputItems.push(item);
    }

    /**
     * 移除当前
     * @param item 
     */
    private static Remove(item: FocusNextDirective) {
        const i = this.InputItems.indexOf(item);
        if (i >= 0) {
            this.InputItems.splice(i, 1);
        }
    }

    /**
     * 根据当前触发,移动焦点到下一项
     * @param source 
     */
    private static FocusNext(source: FocusNextDirective) {
        let found = false;
        let next: FocusNextDirective = undefined;
        for (const item of this.InputItems) {
            if (item.appFocusNext !== source.appFocusNext) {
                continue;
            }
            if (item === source) {
                found = true;
                continue;
            }
            if (item.order < source.order || (!found && item.order === source.order)) {
                continue;
            }
            if (item.order === source.order && found) {
                next = item;
                break;
            }
            if (!next) {
                next = item;
                continue;
            }
            if (next.order > item.order) {
                next = item;
                continue;
            }
        }
        next?.focus();
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132

使用方法

html
         

<input appFocusNext>
<input appFocusNext>
<button appFocusNext>提交</button>


<input appFocusNext="1">
<button appFocusNext="1" [order]="1">提交</button>
<input appFocusNext="1">
123456789

参数说明

appFocusNext 属性值是作为分组使用

order 作为排序用,数字越大排在后面,最后触发,默认按照初始化顺序

缺陷或不足

  1. 自定义的组件,暂时没有找到办法获取对象,只获取到了HTMLElement
  2. 不会验证表单的内容
  3. 不会自动触发上层表单

input 确认按键事件在手机端不生效

2023-03-18 04:48:12

input 确认按键事件在手机端不生效

起因

需要在页面上做一个搜索框,单独加一个按钮显得多余,所以直接使用确认键跳转

一般做法

html
 
<input type="text" name="keywords" onkeydown="onKeydown(event)">
1
ts
        
function onKeydown(event: KeyboardEvent)
{
    if (event.key === 'Enter') {
        // TODO
        return;
    }
}
12345678

这种做法就只有一个输入框,通过代码实现按键完成事件触发

在PC浏览器中是可以触发的,但在手机端没有触发事件,

通过单独调试,在手机端依然执行了方法,也获取到了event.key, 但是在项目就是没有触发的事件

原因

当页面存在多个表单项时,手机端的确认键会自动移动焦点到下一个表单项,只有最后一个才会有效执行,如果不在form 标签中,则时自动查找整个网页,

所以,实际上只有最后一个表单项,才能实际接受到 event.key === 'Enter'

通过 Form 自带事件实现

html
   
<form onsubmit="//TODO">
    <input type="text" name="keywords">
</form>
123

这样就可以保证手机也可以支持,这个实际就是:表示 input 就是最后一项,可以确认了

input search

html
 
<input type="search" name="keywords">
1

html5 自带一个搜索输入框,可以在手机键盘确认显示为搜索

自带一个清除图标和功能

css
   
input::-ms-clear {
    display: none;
}
123

通过 css 可以移除这个

同样的还有 密码框的 小眼睛

css
   
input[type="password"]::-ms-reveal {
    display: none;
}
123

C# 使用socket 进行通讯

2023-03-05 05:00:42

基于UDP

发送

c#
           
// 本机ip和端口
var localIp = new IPEndPoint(IPAddress.Parse(ip), port);
var udpSocket = new Socket(serverIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
udpSocket.Bind(localIp);


public void Send(string ip, int port, byte[] buffer)
{
    var remote = new IPEndPoint(IPAddress.Parse(ip), port);
    udpSocket.SendTo(buffer, remote);
}
1234567891011

数据必须一次发送,不能分段发送,不然顺序会混乱,数据大小也要注意,因为接收时只能接收一次,多出的数据就会接收不到了

接收

c#
           
// 本机ip和端口
var localIp = new IPEndPoint(IPAddress.Parse(ip), port);
var udpSocket = new Socket(serverIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
udpSocket.Bind(localIp);


var buffer = new byte[65536];
EndPoint sendIp = new IPEndPoint(IPAddress.Any, port);
var length = udpSocket.ReceiveFrom(buffer, Constants.UDP_BUFFER_SIZE,
    SocketFlags.None, ref sendIp);
1234567891011

TCP

发送

c#
                 
var remoteIp = new IPEndPoint(IPAddress.Parse(ip), port);
var socket = new Socket(clientIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(remoteIp);

var buffer = new byte[8];

socket.Send(buffer);

private void Send(byte[] buffer, int length)
{
    var index = 0;
    while (index < length)
    {
        var size = socket.Send(buffer, index, length - index, SocketFlags.None);
        index += size;
    }
}
1234567891011121314151617

请注意,发送数据可以分多次,因为是保持顺序的,不会乱的,接收端也可以分多次接收,但是,不能保证一次发送全部数据,最好封装一个方法,保证数据全部发送了

接收

第一步接收每一个连接

c#
          
var localIp = new IPEndPoint(IPAddress.Parse(ip), port);
var tcpSocket = new Socket(serverIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
tcpSocket.Bind(localIp);

tcpSocket.Listen(10);
while (true)
{
    var client = tcpSocket.Accept();

}
12345678910

第二步,从每一个连接中接收数据

c#
               
var buffer = new byte[8];
client.Receive(buffer);

private byte[] Receive(int length)
{
    var buffer = new byte[length];
    var index = 0;
    while (index < length)
    {
        var size = client.Receive(buffer, index,
            length - index, SocketFlags.None);
        index += size;
    }
    return buffer;
}
123456789101112131415

接收数据也会出现一次性接收不完,要分几次才能接收一段数据,最好是保持一问一答的方式发送和接收数据,保证数据的完整,不会出现丢包的问题

Maui开发中Windows应用开启管理员权限

2023-02-28 02:04:26

Maui开发中Windows应用开启管理员权限

首先打开文件夹 Platforms/Windows

在文件 app.manifest 中添加

xml
       
<trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
    <security>
        <requestedPrivileges xmlns='urn:schemas-microsoft-com:asm.v3'>
            <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
        </requestedPrivileges>
    </security>
</trustInfo>
1234567

然后再文件 Package.appxmanifest(右键->查看代码) 中添加

xml
    
<Capabilities>
    <rescap:Capability Name="runFullTrust" />
    <rescap:Capability Name="allowElevation" />
</Capabilities>
1234

Maui 中自定义控件

2023-02-28 02:02:27

Maui 中自定义控件

在 Maui 中自定义控件分为两种方式。

  1. 一种是纯代码构建的,在一个cs文件中
  2. 另一种是模板分离,分为xamlcs代码文件

第一种直接通过代码创建控件,并赋予属性,没法在外部修改模板

第二种分为xamlcs文件,在xaml 中定义模板,cs 写属性,类似于WPF中的 UserControl 但又更像是 CustomControl

具体说明

xml
              
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ZoDream.FileTransfer.Controls.MessageTipListItem">
    <ContentView.ControlTemplate>
        <ControlTemplate>
            <Label 
                Text="{TemplateBinding Text}"
                VerticalOptions="Center" 
                HorizontalOptions="Center" Padding="0,10"/>
        </ControlTemplate>
    </ContentView.ControlTemplate>
    <Button Text="121">
</ContentView>
1234567891011121314

这里分为两部分,一部分是放在 ContentView.ControlTemplate 中,通过 TemplateBinding 获取自定义属性,类似于 WPFCustomControl 定义在 Themes/Generic.xamlStyle.Template, 用法也类似

特别注意,直接作为 ContentView.ContentButton,这里写的控件是无法获取当前控件的属性的,如果在 ContentView.ControlTemplate中没有使用 ContentPresenter 接收,就不会显示,默认 ContentView.ControlTemplate 就有一个 ContentPresenter

在 cs 中通过 BindableProperty 创建定义属性

cs
                        

public string Title {
    get { return (string)GetValue(TitleProperty); }
    set { SetValue(TitleProperty, value); }
}

// Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
public static readonly BindableProperty TitleProperty =
    BindableProperty.Create(nameof(Title), typeof(string), typeof(DialogPanel), string.Empty);



public ICommand TapCommand
{
    get { return (ICommand)GetValue(TapCommandProperty); }
    set { SetValue(TapCommandProperty, value); }
}

// Using a DependencyProperty as the backing store for YesCommand.  This enables animation, styling, binding, etc...
public static readonly BindableProperty TapCommandProperty =
    BindableProperty.Create(nameof(TapCommand), typeof(ICommand),
        typeof(DialogPanel), null);

123456789101112131415161718192021222324

TencentOS Server 3.1 安装 Nginx 1.23、PHP 8.2、MariaDB 10.11

2023-02-20 01:22:42

CentOS 的命令一样

一些其他辅助命令

   
yum list installed #查询已安装程序

ps auxf |grep nginx # 查询正在运行进程
123

Nginx

安装基本编译环境

        
sudo yum -y install gcc gcc-c++ # nginx编译时依赖gcc环境

sudo yum -y install pcre pcre-devel # 让nginx支持重写功能

sudo yum -y install zlib zlib-devel # zlib库提供了很多压缩和解压缩的方式,nginx使用zlib对http包内容进行gzip压缩

sudo yum -y install openssl openssl-devel # 安全套接字层密码库,用于通信加密
12345678

下载源码包

下载 pcre2

下载 zlib

源码包放到 /usr/local/src 下解压

  
cd /usr/local/src #进入src文件夹
wget https://nginx.org/download/nginx-1.23.3.tar.gz #下载nginx源码包
12

解压三个源码压缩包

   
tar -zxvf pcre2-10.42.tar.gz # 解压缩
tar -zxvf zlib-1.2.13.tar.gz
tar -zxvf nginx-1.23.3.tar.gz
123

编译Nginx

      
cd nginx-1.23.3

./configure --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf --pid-path=/usr/local/nginx/logs/nginx.pid --with-http_ssl_module --with-pcre=../pcre2-10.42 --with-zlib=../zlib-1.2.13

make            #编译
make install        #安装
123456

如果后面遇到某个功能没有,只要修改 configure 中的参数就行了,重复执行 make make install 即可

配置启动Nginx

    
/usr/local/nginx/sbin/nginx             #启动服务
/usr/local/nginx/sbin/nginx -s reload   #重新加载服务
/usr/local/nginx/sbin/nginx -s stop     #停止服务
ps -ef | grep nginx                         #查看服务进程
1234

更改配置

 
vi /usr/local/nginx/conf/nginx.conf # 这是站点配置文件
1

开始设置开启启动项

  
vi /usr/lib/systemd/system/nginx.service
12
txt
             
[Unit]
Description=nginx
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/nginx/sbin/nginx
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecReload=/usr/local/nginx/sbin/nginx -s reload
PrivateTmp=true

[Install]
WantedBy=multi-user.target
12345678910111213

启用开机启动服务

 
systemctl enable nginx.service
1

最终的启动运行命令

     
service nginx start
service nginx stop
service nginx restart
service nginx reload
12345

PHP 8

安装基本编译环境

 
yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel sqlite-devel gmp-devel oniguruma-devel readline-devel libxslt-devel
1

安装CMake

         
wget https://github.com/Kitware/CMake/releases/download/v3.25.2/cmake-3.25.2.tar.gz

tar -zxvf cmake-3.25.2.tar.gz
cd cmake-3.25.2
./bootstrap
gmake
ln -s /usr/local/src/cmake-3.25.2/bin/cmake /usr/bin/cmake
cmake --version
123456789

安装libzip 必须先安装CMake

下载 libzip

         
yum remove libzip
wget https://libzip.org/download/libzip-1.9.2.tar.gz
tar -zxvf libzip-1.9.2.tar.gz
cd libzip-1.9.2
mkdir build
cd build
cmake ..
make
make install
123456789

安装完成后,查看是否存在/usr/local/lib64/pkgconfig目录,如果存在,执行如下命令来设置PKG_CONFIG_PATH:

  
export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig/"
12

下载源码包

   
cd /usr/local/src
wget https://www.php.net/distributions/php-8.2.3.tar.gz  # 下载
tar -zxvf php-8.2.3.tar.gz   # 解压缩
123

编译安装

       
cd php-8.2.3

./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --enable-mbstring --enable-ftp --enable-gd --enable-gd-jis-conv --enable-mysqlnd --enable-pdo --enable-sockets --enable-fpm --enable-xml --enable-soap --enable-pcntl --enable-cli --enable-bcmath --with-openssl --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --with-zlib --with-iconv --with-curl --with-zip --with-gettext


make            #编译
make install        #安装
1234567

修改配置文件

            
cd /usr/local/php/etc
cp php-fpm.conf.default php-fpm.conf
cd /usr/local/php/etc/php-fpm.d
cp www.conf.default www.conf


find /usr/local/src/php-8.2.3 -name php.ini*
cp /usr/local/src/php-8.2.3/php.ini-production /usr/local/php/etc/php.ini


vim /usr/local/php/etc/php.ini
123456789101112

;cgi.fix_pathinfo=1 改为 cgi.fix_pathinfo=0

expose_php = On 改为 expose_php = Off 隐藏版本号

 
vim /usr/local/php/etc/php-fpm.conf
1

[global] 下的 pid = run/php-fpm.pid 启用, 后面 php-fpm.service 中的 PIDFile 必须设成一样的路径,否则会出现找不到php-fpm.pid的情况

 
vim /usr/local/php/etc/php-fpm.d/www.conf
1

user = nobody group = nobody 改为 user = www group = www 需要先添加 www 用户和 www

初始命令

   
/usr/local/php/sbin/php-fpm -R   # 这里后面带个 -R  表示用root 用户启动
/usr/bin/pkill -9 php-fpm
pstree -p | grep php
123

设置开机启动项

 
vi /usr/lib/systemd/system/php-fpm.service
1
txt
             
[Unit]
Description=php-fpm
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/php/var/run/php-fpm.pid
ExecStart=/usr/local/php/sbin/php-fpm
ExecStop=/usr/bin/pkill -9 php-fpm
PrivateTmp=true

[Install]
WantedBy=multi-user.target
12345678910111213
 
systemctl enable php-fpm.service
1

可以使用systemctl命令管理php-fpm:

       
systemctl start php-fpm.service  #启动
systemctl stop php-fpm.service   #停止
service php-fpm start
service php-fpm stop
service php-fpm restart
service php-fpm reload
1234567

搭配Nginx运行PHP站点

配置 Nginx

  
vi /usr/local/nginx/conf/nginx.conf
12

http 下添加

txt
         
client_max_body_size 200M; #配置客户端请求体最大值
client_body_buffer_size 50m; #配置请求体缓存区大小
server_tokens off; # 隐藏响应头中的版本号

gzip on;
gzip_min_length 1k; #不压缩临界值,大于1k的才压缩
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript text/css application/font-woff; # 需要压缩什么文件就加上文件类型,对于图片还是不要使用gzip压缩,直接在本地修改成其他图片格式效果更好
123456789
conf
                                                   
server {
    server_name  zodream.cn;
    root         /data/www/html; # 设置站点根目录

    location / {
        # root   html;
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$query_string; # 美化和伪静态需要设置路由重写
    }

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    ## 配置 xxsqladmin 路径指向phpmyadmin
    location ~ ^/xxsqladmin/.*\.php$ {
        root   /data/shop/phpmyadmin;
        set $real_script_name $fastcgi_script_name;
        if ($fastcgi_script_name ~ "^/xxsqladmin(.+?\.php)(.*)$") {
            set $real_script_name $1;
        }
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$real_script_name;
        include        fastcgi_params;
    }

    ## 配置 xxsqladmin 资源文件路径指向phpmyadmin
    location ~ ^/xxsqladmin.* {
        root   /data/shop/phpmyadmin;
        rewrite ^/xxsqladmin(.*)$ /$1 break;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    location ~ /\.ht {
        deny  all;
    }

    # 添加https支持
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/zodream.cn/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/zodream.cn/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

修改完保存文件并重启Nginx服务器

MariaDB

安装

 
yum install mariadb-server
1

如果本地已安装 mayql 则会出现冲突 增加 参数即可 --allowerasing

启动和设置开机启动

   
systemctl start mariadb  # 开启服务

systemctl enable mariadb  # 设置为开机自启动服务
123

设置修改密码

 
mysql_secure_installation
1

使用 Certbot 获取 letsencrypt 证书

   
yum install certbot python3-certbot-nginx

certbot --nginx
123

如果出现 找不到 nginx

  
ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx
ln -s /usr/local/nginx/conf/ /etc/nginx
12

新增用户

       
cat /etc/passwd

# useradd -s /sbin/nologin -M www

groupadd www

useradd -g www www -s /sbin/nologin
1234567

赋予权限

 
chown -R www:www /run/php-fpm
1

PHP拓展安装

imagick

           
sudo yum -y install ImageMagick-devel
cd /usr/local/src
tar xvf imagick-3.7.0.tgz

cd imagick-3.7.0

/usr/local/php/bin/phpize
./configure  --with-php-config=/usr/local/php/bin/php-config
make
make install
1234567891011

php.ini

 
extension=imagick.so
1

redis

安装 redis 主程序

        
cd /usr/local/src
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz
cd redis-stable
make
make install

redis-server
12345678

现在,我们新建目录 /usr/local/redis ,把./redis.conf,src/redis-server,src/redis-cli 三个文件复制到该目录下

     
mkdir /usr/local/redis
cp redis.conf src/redis-server src/redis-cli /usr/local/redis/
cd /usr/local/redis

vi redis.conf
12345
    
daemonize yes # 开启守护线程

masterauth <redis_password> # 设置密码
1234

启动

 
./redis-server redis.conf 
1

安装 phpredis

          
cd /usr/local/src
tar xvf redis-5.3.7.tgz

cd redis-5.3.7

/usr/local/php/bin/phpize
./configure  --with-php-config=/usr/local/php/bin/php-config
make
make install
12345678910

php.ini

 
extension=redis.so
1

安装多版本php 7

下载源码包

   
cd /usr/local/src
wget https://www.php.net/distributions/php-7.4.33.tar.gz  # 下载
tar -zxvf php-7.4.33.tar.gz   # 解压缩
123

编译安装

-–prefix 是安装目录,--with-config-file-path 是配置文件存放目录

       
cd php-7.4.33

./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --enable-mbstring --enable-ftp --enable-gd --enable-gd-jis-conv --enable-mysqlnd --enable-pdo --enable-sockets --enable-fpm --enable-xml --enable-soap --enable-pcntl --enable-cli --enable-bcmath --with-openssl --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --with-zlib --with-iconv --with-curl --with-gettext


make            #编译
make install        #安装
1234567

修改配置文件

同原本的配置方法 [PHP8配置]()

注意将 /usr/local/php7/etc/php-fpm.d/www.conf 中的端口改成其他的,不要和原本的冲突

 
listen = 127.0.0.1:9001
1

设置开机启动项

 
vi /usr/lib/systemd/system/php7-fpm.service
1
txt
             
[Unit]
Description=php7-fpm
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/php7/var/run/php-fpm.pid
ExecStart=/usr/local/php7/sbin/php-fpm
ExecStop=/usr/bin/pkill -9 php7-fpm
PrivateTmp=true

[Install]
WantedBy=multi-user.target
12345678910111213
 
systemctl enable php7-fpm.service
1

可以使用systemctl命令管理php7-fpm:

    
service php7-fpm start
service php7-fpm stop
service php7-fpm restart
service php7-fpm reload
1234

配置站点

/usr/local/nginx/conf/nginx.conffastcgi_pass 配置成PHP7监听的端口

         
    server {
        location ~ \.php$ {
            fastcgi_pass   127.0.0.1:9001;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include fastcgi_params;
        }

    }
123456789

启动PHP7,重启nginx 即可

angular 14 使用 ng-template 实现tree 结构显示

2022-08-16 06:18:22

angular 14 使用 ng-template 实现tree 结构显示

美化版

当然是使用两个自定义控件

html
     

<tree>
    <tree-node></tree-node>
</tree>
12345

利用 ng-template 标签

html
                       

<ul class="tree-box">
    <ng-container *ngFor="let item of items">
        <ng-container *ngTemplateOutlet="fileItemTpl;context: {$implicit: item}"></ng-container>
    </ng-container>
</ul>
<ng-template #fileItemTpl let-file>
    <ng-container *ngIf="file">
        <ng-container *ngIf="file.type < 1">
            <li class="tree-parent" [ngClass]="{open: file.open}"><div class="name" (click)="toggleOpen(file)">{{ file.name }}</div>
                <ul *ngIf="file.children">
                    <ng-container *ngFor="let it of file.children">
                        <ng-container *ngTemplateOutlet="fileItemTpl;context: {$implicit: it}"></ng-container>
                    </ng-container>
                </ul>
            </li>
        </ng-container>
        <ng-container *ngIf="file.type > 0">
            <li><div class="tree-name">{{ file.name }}</div></li>
        </ng-container>
    </ng-container>

</ng-template>
1234567891011121314151617181920212223

了解 ng-template 使用,需要搭配 *ngTemplateOutlet 使用

具体:

  1. ng-template 的数据传递,通过 *ngTemplateOutletcontext: 传递对象, 其中 $implicit 没默认名称
  2. ng-template 的数据获取,通过 let- 获取,例如: let-file 的意思就是:在 templateconst file = $implicit, 相当于完整写法 let-file="$implicit"

angular 14 替换 ComponentFactoryResolver 实现动态创建组件

2022-06-30 21:27:29

angular 14 替换 ComponentFactoryResolver 实现动态创建组件

需求

移植弹出框到 angular 项目中。

方案

直接使用其他项目依赖

比较常用的有以下两个

  1. ngx-toastr

  2. ng-bootstrap

但是通过查看源码,发现都使用使用 ComponentFactoryResolver 来实现组件的新建,然后通过document.body.appendChild 添加到页面上的,

但是,angular 文档中提示,ComponentFactoryResolver 不推荐使用。

  
Deprecated: Angular no longer requires Component factories. Please use other APIs where Component class can be used directly.
Note: since v13, dynamic component creation via ViewContainerRef.createComponent does not require resolving component factory: component class can be used directly.
12

自己实现

没有了 ComponentFactoryResolver, 那么就使用 ViewContainerRef.createComponent

主要思路:

  1. 新建一个容器组件,获取到 ViewContainerRef

    ts
                  
    @Component({
     selector: 'app-dialog-container',
     template: '',
     styles: [''],
    })
    export class DialogContainerComponent {
    
     constructor(
         private service: DialogService,
         private viewContainerRef: ViewContainerRef,
     ) {
         this.service.containerRef = this.viewContainerRef;
     }
    }
    1234567891011121314
  2. 新建一个全局的服务提供者,

ts
                                                                
interface IDialogRef {
    id: any;
    element: ComponentRef<any>;
}

@Injectable({
    providedIn: 'root'
})
export class DialogService {
    private dialogItems: IDialogRef[] = [];
    public containerRef: ViewContainerRef;

    constructor(
        private injector: Injector,
    ) {

    }

    /**
     * 加载loading
     * @param option 
     * @returns loading 的id, 使用 remove(id: any) 进行关闭
     */
    public loading(option?: DialogLoadingOption): any {
        option = Object.assign({}, option, {
            time: 2000,
            closeable: true,
        });
        return this.createDailog(DialogLoadingComponent, option);
    }

    /**
     * 创建组件
     * @param component 
     * @param option 
     * @returns 
     */
    private createDailog<T>(component: Type<T>, option: any): any {
        const dialogId = ++ DialogService.guid;
        if (!this.containerRef) {
            return;
        }
        const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector);
        const dialogRef = this.containerRef.createComponent(component, {
            injector: dialogInjector
        });
        this.dialogItems.push({
            id: dialogId,
            element: dialogRef
        });
        return dialogId;
    }

    /**
     * 删除组件
     * @param i 
     */
    private removeAt(i: number) {
        const item = this.dialogItems[i];
        this.dialogItems.splice(i, 1);
        const dialogRef = item.element;
        dialogRef.destroy();
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

作为对比,ViewContainerRef.createComponentComponentFactoryResolver 使用更简单,而且更符合 angular 特色

ts
                   
constructor(
        private resolver: ComponentFactoryResolver,
        private applicationRef: ApplicationRef,
        private injector: Injector, 
        @Inject(DOCUMENT) private document: Document,
    ) { }


// 添加组件
const dialogFactory = this.resolver.resolveComponentFactory(component);
const dialogRef = dialogFactory.create(dialogInjector);
this.applicationRef.attachView(dialogRef.hostView);
this.document.body.appendChild(dialogRef.location.nativeElement);


// 删除组件
this.applicationRef.detachView(dialogRef.hostView);
this.document.body.removeChild(dialogRef.location.nativeElement);
this.dialogItems.splice(i);
12345678910111213141516171819
  1. 在组件内部实现删除功能
ts
             
export class DialogMessageComponent implements OnDestroy {
    constructor(
        private data: DialogPackage<DialogMessageOption>,
        private service: DialogService,
    ) {

    }

    @HostListener('click')
    public close() {
        this.service.remove(this.data.dialogId);
    }
}
12345678910111213
  1. 以模块的方式提供
ts
                     
@NgModule({
    imports: [
        CommonModule,
    ],
    declarations: [
        ...COMPONENTS
    ],
    exports: [
        ...COMPONENTS
    ],
})
export class DialogModule {
    static forRoot(): ModuleWithProviders<DialogModule> {
        return {
            ngModule: DialogModule,
            providers: [
                DialogService
            ]
        };
    }
}
123456789101112131415161718192021
  1. 使用

第一步,在 app.module.ts 中导入

ts
 
DialogModule.forRoot(),
1

第二步,在 app.component.ts 页面中添加容器

html
 
<app-dialog-container></app-dialog-container>
1

第三步,使用

ts
    
constructor(
    private toastrService: DialogService) {
    this.toastrService.loading();
}
1234

总结

ViewContainerRef.createComponentComponentFactoryResolver 使用更简单,而且更符合 angular 特色,始终保持所有创建的内容在框架内。

完整代码请查看 Angular-ZoDream

c# 动态安装和卸载dll

2022-06-15 22:10:13

c# 动态安装和卸载dll

需求

自动加载指定文件夹下的所有dll文件

第一种方法使用其他框架MAF或MEF

自定义代码

c#
                                           
public class PluginLoadContext: AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver _resolver;

    public PluginLoadContext(string pluginPath)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }

        return null;
    }

    protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
    {
        var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
        if (libraryPath != null)
        {
            return LoadUnmanagedDllFromPath(libraryPath);
        }

        return IntPtr.Zero;
    }
}

public void LoadDll(string path)
{
    var loadContext = new PluginLoadContext(path);
    var assem = loadContext.LoadFromAssemblyName(AssemblyName.GetAssemblyName(path));
    if (assem == null)
    {
        return;
    }
    var types = assem.GetTypes();
    // TODO 获取需要的类
}
12345678910111213141516171819202122232425262728293031323334353637383940414243

可能出现的问题

Q: 在主程序中使用 typeof(IRule).IsAssignableFrom(types[0]) 判断是否继承至公共接口,会出现 false 的情况

A:说明 LoadDll 加载dll 时又引入了公共类库,只需要在 dll 解决方案下的依赖项属性中设置 复制本地设为否(CopyLocal=false) 即可,或者不要把公共类库复制放到dll所在额文件夹下

IsAssignableFrom() returns false when it should return true

慎用 CompositionTarget.Rendering

2022-05-14 19:02:34

慎用 CompositionTarget.Rendering

在WPF中使用

c#
              
private void RollLabel_Unloaded(object sender, RoutedEventArgs e)
{
    CompositionTarget.Rendering -= CompositionTarget_Rendering;
}

private void RollLabel_Loaded(object sender, RoutedEventArgs e)
{
    CompositionTarget.Rendering += CompositionTarget_Rendering;
}

private void CompositionTarget_Rendering(object? sender, EventArgs e)
{
    InvalidateVisual();
}
1234567891011121314

虽然保证了动画看上去更丝滑,但这个是按帧执行的,具体一秒多少帧取决于电脑支持的最大帧数。

但是很占用GPU,一个小的移动的动画使用CompositionTarget.Rendering就直接占用60%的GPU

跟 DispatcherTimer 相比较

优势

  1. 同样的帧数,占用同样的GPU,明显 CompositionTargetDispatcherTimer 的动画更流畅,DispatcherTimer有明显的卡顿感。

劣势

  1. DispatcherTimer 支持自定义时间间隔,可以减少帧数来减少GPU占用

c# 重写 c++ 程序笔记:数据初始化

2022-05-14 18:54:09

c# 重写 c++ 程序笔记:数据初始化

std::uint32_t 初始化

c++ 数组定义

c++
 
std::array<std::uint32_t, 7> data;
1

转成c# 数组

c#
        

var data = new uint[7];

for (int i = 0; i < 7; i++)
{
    data[i] = 0xcccccccc;
}
12345678

std::uint8_t 初始化

c++ 数组定义

c++
 
std::array<std::uint8_t, 7> data;
1

转成c# 数组

c#
        

var data = new byte[7];

for (int i = 0; i < 7; i++)
{
    data[i] = 0xcc;
}
12345678

源码编译 aseprite

2022-04-24 00:11:26

源码编译 aseprite

环境

操作系统:windows 11

开发工具:TortoiseGitVisual Studio 2022 Preview

步骤

  1. 通过 Visual Studio Installer 安装 Visual Studio Community 2022, 选择 工作负荷 中的 使用 c++ 的桌面开发,右侧的安装详细信息中勾选 Windows 11 SDK,下载安装即可
  2. 通过浏览器访问 aseprite 源码克隆到本地即可、或直接使用命令行克隆代码
     
    git clone --recursive https://github.com/aseprite/aseprite.git
    1
  3. 下载 skia 最新文件 Skia-Windows-Release-x64.zip,解压到一个文件夹即可
  4. aseprite 的源码文件夹下打开cmd
  5. 使用命令安装一些一拉模块,如果出现下载失败,重复执行即可,如果还是失败,检查 laf third_party 文件夹下是否只有一个 .git 文件,删除重试命令即可
     
    git submodule update --init --recursive
    1
  6. aseprite下新建文件夹 build 获取使用命令,在 build 文件夹下打开 cmd
      
    mkdir build
    cd build
    12
  7. 输入命令,注意直接从 vs2022 的工具菜单进入命令行是 x86模式,注意:只要找到 vs 的安装目录下的 Common7\Tools\VsDevCmd.bat 文件即可,
     
    call "D:\Microsoft Visual Studio\2022\Preview\Common7\Tools\VsDevCmd.bat" -arch=x64
    1
  8. 输入命令,D:\Aseprite\Skia 即第三步下载的 skia 解压的文件夹
     
    cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLAF_BACKEND=skia -DSKIA_DIR=D:\Aseprite\Skia -DSKIA_LIBRARY_DIR=D:\Aseprite\Skia\out\Release-x64 -DSKIA_LIBRARY=D:\Aseprite\Skia\out\Release-x64\skia.lib -G Ninja ..
    1

    这一步基本不会出什么问题,如果有问题那就是 第五步 安装模块时不完整,删除出错目录下的文件,执行第五步的命令即可

  9. 输入命令生成最终文件
     
    ninja aseprite
    1

    这一步可能出错,我的是失败在 FIALLED json11,需要更改 third_party/json11/CMakeLists.txt 文件,删除第27行或者改为

        
    if (NOT MSVC)
    target_compile_options(json11
     PRIVATE -fPIC -fno-rtti -fno-exceptions -Wall)
    endif()
    1234

    重新执行ninja aseprite命令即可,

  10. 生成成功后复制 build/bin 下的 aseprite.exe 主程序 和 data 文件夹即可,这两个文件就是aseprite的运行文件

记录一下字符串分隔split各语言之间的不同

2022-04-21 18:51:39

今天才发现不同编程语言对字符串的 split 是有差距的

js

js
 
'a:b:c:d'.split(':', 2); // 输出结果为 ['a','b']
1

是先把所有的都拆分成数组,然后取前面的几个

c#

c#
 
"a:b:c:d".Split(":", 2); // 输出结果为 ['a','b:c:d']
1

是拆分前几个,剩余的都原样放在最后一个

php

php
 
explode(':', 'a:b:c:d', 2); // 输出结果为 ['a','b:c:d']
1

c# Gzip解码无头内容

2022-04-06 18:43:16

依赖

使用NuGet安装

 
SharpZipLib
1

主要代码

c#
                     
using ICSharpCode.SharpZipLib.Zip.Compression;


byte[] data; // 要解码内容
using var outputStream = new MemoryStream();
var inflater = new Inflater();
try
{
    inflater.SetInput(data);
    var buffer = new byte[2048];
    while (!inflater.IsFinished)
    {
        var count = inflater.Inflate(buffer);
        outputStream.Write(buffer, 0, count);
    }
}
catch (Exception)
{
    inflater.Reset();
}
byte[] res = outputStream.ToArray(); //解码结果
123456789101112131415161718192021

与 php gzuncompress 比较

gzuncompress 主要是不必考虑有无gzip头,都能进行解码

SharpZipLib 中默认的解码方法 GZipInputStream 并不支持无头内容,

Windows 10 查看内存占用

2022-03-05 23:51:51

Windows 10 查看内存占用

查看内存被哪些进程占用

通过自带的任务管理器查看详细信息即可。

内存占用不正常怎么办?

即在任务管理器进程占用的内存之和远小于实际被占用的内存。

可以通过 RAMMapvmmap 这两个工具查看。

vmmap

这是查看某一个进程已提交虚拟内存类型的明细。

RAMMap

准确地了解 Windows 如何分配物理内存、在 RAM 中缓存的文件数据量,或者内核和设备驱动程序使用了多少内存。

可以完整的看出内存用到哪里去了!

PoolMon

可以查看哪些驱动使用的内存情况。

案例

每次使用Steam 或 Epic 下载游戏时,内存占用越来越高,最后占用99%之后电脑卡死,关闭程序无用,只能重启。

分析

  1. 任务管理器进程查看不到占用大量内存的进程。
  2. 使用RAMMap查看到大量内存被 Nonpaged Pool 占用。
  3. 使用 poolmon 执行命令 poolmon.exe /p /d 发现被一个 Tagwfpn 的驱动占用了大量内存。
  4. 通过 Bing 搜索 poolmon wfpn 找到了 wfpnKiller Network Manager 网络驱动

解决方法

更新 Killer Network Manager 驱动即可;或禁用 NDNB

参考

  1. Memory leak?

UWP 使用 win2d:加阴影

2021-10-22 19:47:06

加阴影

c#
                 
var bitmap = new CanvasRenderTarget(Control, (float)Width,
                   (float)Height, 96);
var effect = new Transform2DEffect() {
    Source = new ShadowEffect()
    {
        Source = bitmap,
        BlurAmount = 2,
    },
    TransformMatrix = Matrix3x2.CreateTranslation(3, 3)
};



private void DrawerCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args)
{
    args.DrawingSession.DrawImage(effect);
}
1234567891011121314151617

无论内容是什么输出的时一个黑色的框框带阴影效果,

应该加代码输出原版的图像,即可

c#
     
private void DrawerCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args)
{
    args.DrawingSession.DrawImage(effect);
    args.DrawingSession.DrawImage(bitmap);
}
12345

清除 PowerShell 历史记录

2021-09-20 03:03:51

使用的命令过多,该清理一下不要的命令了。

shell
 
Remove-Item (Get-PSReadlineOption).HistorySavePath
1

c# 调用 c++ 的dll

2021-09-20 01:45:17

c# 调用 c++ 的dll

c++ 程序的效率比 c# 的效率高很多

第一步,新建“c++动态链接库”项目

第二步,添加方法

c++
          
struct KeyItem
{
    std::uint32_t x, y, z;
};

extern "C" _declspec(dllexport) 
KeyItem FindKey(char* zipFile, char* zipFileName, char* plainFile, char* plainFileName)
{

}
12345678910

第三步,c#调用

复制 dll 到生成目录

c#
            
[StructLayout(LayoutKind.Sequential)]
struct KeyItem
{
    public uint x, y, z;
}

public static class CrackerDLL 
{

    [DllImport("cracker.dll", EntryPoint = "FindKey", CallingConvention = CallingConvention.Cdecl)]
    internal static extern KeyItem FindKey(string zipFile, string zipFileName, string plainFile, string plainFileName);
}
123456789101112

第四步,使用

c#
       

var res = CrackerDLL.FindKey("c.zip", "c.txt", "plain.zip", "plain.txt");

res.x
res.y
res.z
1234567

注意

生成平台必须选择一样的 x64x86,不能使用 Any CPU,否则会报错

c# 重写 c++ 程序笔记:遍历

2021-09-20 01:43:19

c# 重写 c++ 程序笔记:遍历

倒序遍历

c++

c++
           

std::vector<byte> items

item.begin()

item.end()

item.rbegin()

item.rend()
1234567891011

转 c#

c#
            

IList<byte> items


0

items.Count

-1

items.Count  -1
123456789101112

遍历c++

c++
      

for(std::vector<byte>::const_iterator p = items.begin(); p != items.end(); ++p)
{
    *p
}
123456

转 c#

c#
      

for (var i = 0; i < items.Count; i ++) 
{
    items[i]
}
123456

倒序遍历c++

c++
       

using rit = std::reverse_iterator<std::vector<byte>::const_iterator>;
for(rit p = rit(items.begin()); p != items.rend(); ++p)
{
    *p
}
1234567

转 c#

c#
      

for (var i = -1; i != items.Count - 1; i --) 
{
    items[i]
}
123456

我的理解:

std::reverse_iterator 实际上是把输入的位置往前移一位,并把 + 转成 -,方向反一下

Net Core 与 UWP 共用类开发

2021-09-07 04:47:12

开发环境

VS 2022

需求

一个应用程序分:桌面版和UWP版,实现一个类库能被两个版本使用

解决方法

  1. 创建新项目,选择WPF 应用程序(用于创建.NET Core WPF 应用程序的项目),Framework 选择.NET 6.0
  2. 在解决方案中新建项目,选择空白应用(通用Windows),选择最低版本 17763
  3. 在解决方案中新建项目,选择类库(一个创建用于面向NET Standard 或.NET Core的类库的项目),Framework 选择.NET Standard 2.0
  4. 在第一和第二项目中添加对第三个项目的引用即可

关键点

Framework 选择.NET Standard 2.0

支持 .NET Core 、UWP 、 NET Framework 4.8

如需要分不同框架引入不同依赖包

则需要手动修改第三个项目的 .csproj 文件

xml
       
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks Condition="'$(LibraryFrameworks)'==''">net48;netstandard2.0</TargetFrameworks>
    <TargetFrameworks Condition="'$(LibraryFrameworks)'!=''">$(LibraryFrameworks)</TargetFrameworks>

1234567

修改 TargetFrameworks 内容,

然后就可以添加不同的程序集引用了

hashcat(二)找回rar解压密码

2021-08-25 23:22:34

rar 分卷 功能,任意一卷也是可以测试密码的,不用全部下载才发现密码不正确

第一步,获取rar 的密码哈希

下载 rar2john 下载地址 点击 1.9.0-jumbo-1 64-bit Windows binaries 下载

执行命令 rar2john.exe 在压缩包的 run文件夹下

 
rar2john.exe 1.rar
1

输出密码哈希

 
1.rar:$rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe
1

$rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe 就是密码哈希值了

第二步,使用 hashcat 解密

 
hashcat.exe -m 13000 -a3 $rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe ?d?d?d?d
1

具体规则请查看 【上一篇:hashcat(一)找回office文件密码

hashcat 文档

Godot 学习笔记(一)

2021-08-17 03:50:35

语法

方法

rust
   
func _a(param: int = 10):
    // TODO
123

方法

退出游戏

rust
 
get_tree().quit()
1

切换场景

rust
 
get_tree().change_scene("res://scene.tscn")
1

自定义属性

rust
   

export var speed: int = 30
123

自定义信号

rust
      

signal on_play

func _on_do():
    emit_signal("on_play")
123456

升级vue3记录

2021-08-15 22:57:07

第一步安装 vue-cli

 
npm install --global @vue/cli@next
1

创建项目

 
vue create vue-shop
1

选择 Manually select features

勾选 Choose Vue version

Babel

Typescript

Progressive Web App (PWA) Support

Router

Vuex

Css Pre-processors 使用了Scss 必选

Linter/Formatter

Unit TestingE2E Testing 可选其中一个

选择 vue 版本 3.x

接下来输三个 y yes

然后选择 Sass/SCSS

ESLint 规则选择: 选了第一个,碰到很多问题,比如 在 template 中 {{ 1 < 2 }} 小于号居然被提示不符合vue 规则

选择 Lint on save 代码规范检查的时机

保存设置 In dedicated config files

然后 是否保存这个配置方便以后创建其他项目 N

安装一些其他依赖

 
npm install mitt axios vue-class-component@next vue-property-decorator@rc vuex-class vuex-class-modules
1

axios Api数据请求

mitt 全局事件管理,vue3 取消了 $once 等全局事件

vue-class-component@next vue3 Typescript 项目默认使用vue-class-component, 这里是为了安装最新

vue-property-decorator@rc @Prop() 的使用

vuex-class vuex-class-modules 方便vuex 使用

修改代码

复制一些文件和资源

修改项

  1. 修改 @Component@Options

  2. axios 注册全局

ts
        
export default {
    install(app: any) {
        app.config.globalProperties.$post = post
        app.config.globalProperties.$fetch = fetch
        app.config.globalProperties.$patch = patch
        app.config.globalProperties.$put = put
    },
}
12345678
  1. VueRouter 改为 import { Router } from 'vue-router';
  2. 路由直接放 const routes: Array<RouteRecordRaw> = [];
  3. Vuex 使用
    ts
            
    
    import {
     SET_USER, TOKEN_KEY, SET_TOKEN,
    } from '../types';
    import { IUser, ILogin } from '@/api/model';
    import { getSessionStorage, setSessionStorage, removeSessionStorage } from '@/utils';
    import { getProfile, login, logout } from '@/api/user';
    import { Action, Module, Mutation, VuexModule } from 'vuex-class-modules';
    12345678

@Module({ generateMutationSetters: true }) export class AuthModule extends VuexModule { token: string | null = null; user: IUser | null = null;

get isGuest() {
    if (this.user) {
        return false;
    }
    const token = getSessionStorage<string>(TOKEN_KEY);
    return !token;
}

@Mutation
[SET_USER](user: IUser|null) {
    this.user = user;
}


@Action
logoutUser() {
    return new Promise<void>((resolve, reject) => {
        const token = getSessionStorage<string>(TOKEN_KEY);
        if (!token) {
            resolve();
            return;
        }
        logout().then(() => {
            this[SET_TOKEN](null);
            this[SET_USER](null);
            resolve();
        }).catch(reject);
    });
}

}

import { createStore } from 'vuex'; import { AuthModule } from './modules/auth';

const store = createStore({});

export const authModule = new AuthModule({store, name: 'auth'});

export default store;

                    
具体参考 [Vue.js with Typescript and Decorators](https://davidjamesherzog.github.io/2020/12/30/vue-typescript-decorators/)

6. 修改main.ts
```ts
import { createApp } from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
import emitter from './event';

import './assets/iconfont/iconfont.css';

import http from './utils/http';

createApp(App, {
    onscroll(e: Event) {
        emitter.emit('scroll', e); // 传递滚动事件
    }
}).use(http).use(store).use(router).mount('#app');
1234567891011121314151617181920
  1. $children 被删除了,需要改动使用 setup() 获取子元素
  2. filter 已经删除了,所以只能通过方法调用

angular 12 显示数学公式

2021-07-16 06:34:12

angular 12 显示数学公式

公式的格式

主要有两种写法格式:

  1. LaTeX
  2. AsciiMath

安装依赖

这里使用的是 KaTeX,默认支持 LaTeX,如果需要支持 AsciiMath 则需要 安装转化工具 asciimath2tex

shell
   
npm i katex

npm i asciimath2tex
123

默认是有 angular 版本的 KaTeX

shell
   

npm i ng-katex
123

注意

ng-katex 默认根据 $$$ 来识别处理公式的

例如

 
$a=x^2$
1

代码

但是我不知需要显示公式,还需要处理一些其他的,所以直接使用 KaTeX 进行处理

shell
   
npm i katex

npm i asciimath2tex
123
ts
                                                                        
import * as katex from 'katex';
import AsciiMathParser from 'asciimath2tex';

    private formatContent() {
        const items: IMarkItem[] = [];
        const content = this.content.trim();
        let index = -1;
        let start = 0;
        const parser = new AsciiMathParser();
        const pushMath = () => {
            index ++;
            start = index;
            while (index < content.length - 1) {
                if (content.charAt(++index) === '$'  && backslashedCount(index - 1) % 2 === 0) {
                    break;
                }
            }
            items.push({
                type: 'math',
                content: this.sanitizer.bypassSecurityTrustHtml(
                    katex.renderToString(parser.parse(content.substring(start, index)))
                )
            });
        };
        const pushText = (end: number) => {
            if (end > content.length) {
                end = content.length;
            }
            if (start >= end) {
                return;
            }
            const text = content.substring(start, end);
            if (text.length < 1) {
                return;
            }
            items.push({
                type: 'text',
                content: text,
            });
        };
        const backslashedCount = (i: number) => {
            let count = 0;
            while (i >= 0) {
                if (content.charAt(i --) === '\\') {
                    count ++;
                    continue;
                }
                break;
            }
            return count;
        };

        while (index < content.length - 1) {
            const code = content.charAt(++index);
            if (code === '$' && backslashedCount(index - 1) % 2 === 0) {
                pushText(index);
                pushMath();
                start = index + 1;
                continue;
            }
            if (code === '\n') {
                pushText(index);
                items.push({
                    type: 'line',
                });
                start = index + 1;
                continue;
            }
        }
        pushText(index + 1);
        this.items = items;
    }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172

关键代码

ts
    
import * as katex from 'katex';
import AsciiMathParser from 'asciimath2tex';

katex.renderToString(parser.parse(content));
1234

根据提取的公式转化成 html 代码

js 监听按键事件

2021-07-09 20:05:57

js 监听按键事件

一个输入框

 
<input type="text">
1

主要事件

onkeydown 按下一个按键时执行

onkeypress 按下键盘按钮时执行,不是适用于所有按键(如: ALT, CTRL, SHIFT, ESC)

onkeyup 释放键盘按钮时执行

监听所有按键事件

ts
   
document.addEventListener('keydown', (event:  KeyboardEvent) => {

});
123

监听输入框按键事件

ts
   
document.querySelector('input').addEventListener('keydown', (event:  KeyboardEvent) => {

});
123

KeyboardEvent 的主要属性

ts
         
interface KeyboardEvent {
    readonly altKey: boolean;    // 按下了 alt 键
    readonly code: string;       // 按键的内容 例如 Enter KeyK,请注意手机上按键的确认键无法获取,区分左右按键 AltLeft
    readonly ctrlKey: boolean;   // 是否按住了 ctrl 键
    readonly key: string;        // 键名 例如 Enter、k 字母区分大小写
    readonly keyCode: number;    // 键的字符代码,已废弃的属性,不建议试用
    readonly shiftKey: boolean;  // 是否按住了 shift 键
    readonly metaKey: boolean;   // 是否按住了 win 键
}
123456789

使用建议

获取 字母按键 请使用 event.code

获取其他特殊按键(Enter、Tab) 请使用 event.key

例如:输入框确认事件

ts
     
document.querySelector('input').addEventListener('keydown', (event:  KeyboardEvent) => {
    if (event.key === 'Enter') {
        // TODO 确认
    }
});
12345

监听复制快捷键

ts
     
document.addEventListener('keydown', (event:  KeyboardEvent) => {
    if (event.ctrlKey && event.code === 'KeyC') {
        // TODO 复制
    }
});
12345

angular 12 ng-deep 使用注意事项

2021-07-04 02:21:52

ng-deep 使用

a.scss

scss
     
::ng-deep {
    a {
        color: #fff;
    }
}
12345

ng-deep 表示里面的样式是公共的,需要影响子组件的样式。

可以理解为 vue 中的不带 scoped 属性的 style 标签

上面的样式表示,当打开 a 组件之后,样式开始生效,如果从 a 离开了样式依然会起作用,

注意

::ng-deep 里面的样式是影响全局的,如果想只作用与一个模块下

请使用一个规则名放在最外面

scss
     
.page ::ng-deep {
    a {
        color: #fff;
    }
}
12345

angular 16 动态生成组件

2021-07-03 07:08:59

angular 16 动态生成组件

需求

在页面中生成一个确认弹窗

新的实现方式

第一步,获取需要的工具

ts
          
@Injectable({
    providedIn: 'root'
})
export class DialogService {

    public containerRef: ViewContainerRef; //需要从Component中获取,然后传递进来
    constructor(
        private injector: Injector,
    ) { }
}
12345678910

ViewContainerRef 的获取方式

  1. 第一种在 Component 初始化时获取

    ts
                  
    @Component({
     selector: 'app-dialog-container',
     template: '',
     styles: [''],
    })
    export class DialogContainerComponent {
    
     constructor(
         private service: DialogService,
         private viewContainerRef: ViewContainerRef,
     ) {
         this.service.containerRef = this.viewContainerRef;
     }
    }
    1234567891011121314
  2. template 中通过 ng-container 获取

    ts
                        
    @Component({
     selector: 'app-dialog-container',
     template: '<ng-container #modalVC></ng-container>',
     styles: [''],
    })
    export class DialogContainerComponent implements AfterViewInit {
    
     @ViewChild('modalVC', {read: ViewContainerRef})
     private viewContainerRef: ViewContainerRef;
    
     constructor(
         private service: DialogService,
     ) {
    
     }
    
     ngAfterViewInit(): void {
         this.service.containerRef = this.viewContainerRef;
     }
    }
    1234567891011121314151617181920

主要区别:就是增加的元素节点为兄弟节点,所以,第一种添加的弹窗是在外面的,不受 app-dialog-container 中的样式影响;第二种是作为子元素添加的,受样式影响

第二步,实现动态生成组件

component 为组件的类名

ts
     
const dialogRef = this.containerRef.createComponent(component, {
    injector: this.injector
});

// dialogRef.instance 就是组件的实例 
12345

第三步,移除组件

ts
 
dialogRef.destroy();
1

如果需要在组件初始化,自动注入值或传值,则需要自定义 injector

ts
                             
import { InjectFlags, Injector, ProviderToken } from '@angular/core';

// 这个类是传值的载体
export class DialogPackage<T = any> {
    constructor(
        public data: T,
        public dialogId: any,
    ) {
    }
}

// 这是自定义的注射器
export class DialogInjector<T> implements Injector {

    constructor(
        private data: DialogPackage,
        private parentInjector: Injector
      ) {}

    get<T>(token: ProviderToken<T>, notFoundValue?: T, option?: InjectOptions): T;
    get(token: any, notFoundValue?: any);
    get(token: any, notFoundValue?: any, flags?: any): any {
        if (token === DialogPackage) {
            return this.data;
          }
          return this.parentInjector.get<T>(token, notFoundValue, flags);
    }

}
1234567891011121314151617181920212223242526272829

第二步需要修改

ts
      
const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector);
const dialogRef = this.containerRef.createComponent(component, {
    injector: dialogInjector
});

// dialogRef.instance 就是组件的实例 
123456

在组件中接收值

ts
         
export class DialogConfirmComponent {

    constructor(
        private data: DialogPackage<DialogConfirmOption>,
        private service: DialogService,
    ) {

    }
}
123456789

过时实现代码

第一步,获取需要的工具

ts
           
export class DialogService {

    constructor(
        private resolver: ComponentFactoryResolver,
        private applicationRef: ApplicationRef,
        private injector: Injector, 
        @Inject(DOCUMENT) private document: Document,
    ) { }


}
1234567891011

第二步,实现动态生成组件

component 为组件的类名

ts
       
const dialogFactory = this.resolver.resolveComponentFactory(component);
const dialogRef = dialogFactory.create(this.injector);
// 正式添加到程序和添加到页面上,才能显示
this.applicationRef.attachView(dialogRef.hostView);
this.document.body.appendChild(dialogRef.location.nativeElement);

// dialogRef.instance 就是组件的实例 
1234567

第三步,移除组件

ts
  
this.applicationRef.detachView(dialogRef.hostView);
this.document.body.removeChild(dialogRef.location.nativeElement);
12

如果需要在组件初始化,自动注入值或传值,则需要自定义 injector

ts
                             
import { InjectFlags, Injector, ProviderToken } from '@angular/core';

// 这个类是传值的载体
export class DialogPackage<T = any> {
    constructor(
        public data: T,
        public dialogId: any,
    ) {
    }
}

// 这是自定义的注射器
export class DialogInjector<T> implements Injector {

    constructor(
        private data: DialogPackage,
        private parentInjector: Injector
      ) {}

    get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
    get(token: any, notFoundValue?: any);
    get(token: any, notFoundValue?: any, flags?: any): any {
        if (token === DialogPackage) {
            return this.data;
          }
          return this.parentInjector.get<T>(token, notFoundValue, flags);
    }

}
1234567891011121314151617181920212223242526272829

第二步需要修改

ts
         
const dialogFactory = this.resolver.resolveComponentFactory(component);
// 这里的option 可以是任何值
const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector);
const dialogRef = dialogFactory.create(dialogInjector);
// 正式添加到程序和添加到页面上,才能显示
this.applicationRef.attachView(dialogRef.hostView);
this.document.body.appendChild(dialogRef.location.nativeElement);

// dialogRef.instance 就是组件的实例 
123456789

在组件中接收值

ts
         
export class DialogConfirmComponent {

    constructor(
        private data: DialogPackage<DialogConfirmOption>,
        private service: DialogService,
    ) {

    }
}
123456789

angular 12 动画执行完成事件

2021-07-03 06:21:59

angular 12 动画执行完成事件

需求

在执行完关闭动画移除组件

代码

第一步,定义一个动画

ts
                   
import { animate, state, style, transition, trigger } from '@angular/animations';

export const DialogAnimation = trigger('dialogOpen', [
    state('open', style({
        transform: 'translate3d(0, 0, 0)',
        opacity: 1,
    })),
    state('closed', style({
        transform: 'translate3d(0, -1000px, 0)',
        opacity: 0,
    })),
    transition('* => closed', [
        animate('1s')
    ]),
    transition('* => open', [
        animate('0.5s')
    ]),
]);
12345678910111213141516171819

第二步,使用动画

ts
            

@Component({
    selector: 'app-dialog-confirm',
    templateUrl: './dialog-confirm.component.html',
    styleUrls: ['./dialog-confirm.component.scss'],
    animations: [
        DialogAnimation,
    ],
})
export class DialogConfirmComponent {
    public visible = true;
}
123456789101112
html
  
<div class="dialog-box" [@dialogOpen]="visible ? 'open' : 'closed'">
</div>
12

第三步,加上动画事件

html
  
<div class="dialog-box" [@dialogOpen]="visible ? 'open' : 'closed'" (@dialogOpen.done)="animationDone($event)">
</div>
12

(@动画名.start) 表示动画开始执行

(@动画名.done) 表示动画执行完成

ts
                     
import { AnimationEvent } from '@angular/animations';

@Component({
    selector: 'app-dialog-confirm',
    templateUrl: './dialog-confirm.component.html',
    styleUrls: ['./dialog-confirm.component.scss'],
    animations: [
        DialogAnimation,
    ],
})
export class DialogConfirmComponent {
    public visible = true;

    public animationDone(event: AnimationEvent) {
        // 获取状态
        if (event.toState !== 'closed') {
            return;
        }
        // 表示关闭动画已经执行完成
    }
}
123456789101112131415161718192021

angular 12 全局搜索组件

2021-06-30 06:00:42

一个全局的 SearchService

ts
                                                                                                                        
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class SearchService {

    /**
     * 输入文字发送改变
     */
    static EVENT_CHANGE = 'change';
    /**
     * 确认搜索
     */
    static EVENT_CONFIRM = 'confirm';

    /**
     * 根据文字设置搜索建议
     */
    static EVENT_CHANGE_SUGGEST = 'suggest';

    private eventPair: {
        [trigger: string]: string;
    } = {
        [SearchService.EVENT_CHANGE]: SearchService.EVENT_CHANGE_SUGGEST
    };

    private listeners: {
        [key: string]: Function[];
    } = {};

    constructor(
    ) {
    }

    public on(event: 'change', cb: (keywords: string) => void|boolean|Observable<any[]>): this;
    public on(event: 'confirm', cb: (keywords: any) => void|false): this;
    public on(event: 'suggest', cb: (items: any[]) => void): this;
    public on(event: string, cb: (...items: any[]) => void|boolean|Observable<any>): this;
    public on(event: string, cb: any) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(cb);
        return this;
    }

    public emit(event: 'change', keywords: string): this;
    public emit(event: 'confirm', keywords: any): this;
    public emit(event: 'suggest', items: any[]): this;
    public emit(event: string, ...items: any[]): this;
    public emit(event: string, ...items: any[]) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            return this;
        }
        const pair = this.eventPair[event];
        const listeners = this.listeners[event];
        for (let i = listeners.length - 1; i >= 0; i--) {
            const cb = listeners[i];
            const res = cb(...items);
            //  允许事件不进行传递
            if (res === false) {
                break;
            }
            if (!res || !pair) {
                continue;
            }
            // 接受订阅
            if (res instanceof Observable) {
                res.subscribe(data => {
                    this.emit(pair, data);
                });
                continue;
            }
            this.emit(pair, res);
        }
        return this;
    }

    public off(...events: string[]): this;
    public off(event: string, cb: Function): this;
    public off(...events: any[]) {
        if (events.length == 2 && typeof events[1] === 'function') {
            return this.offListener(events[0], events[1]);
        }
        for (const event of events) {
            delete this.listeners[event];
        }
        return this;
    }

    /**
     * 移除搜索框页面的接受事件
     */
    public offTrigger() {
        return this.off(SearchService.EVENT_CHANGE_SUGGEST);
    }

    /**
     * 移除搜索结果页面的接受事件
     */
    public offReceiver() {
        return this.off(SearchService.EVENT_CHANGE, SearchService.EVENT_CONFIRM);
    }

    private offListener(event: string, cb: Function): this {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            return this;
        }
        const items = this.listeners[event];
        for (let i = items.length - 1; i >= 0; i--) {
            if (items[i] === cb) {
                items.splice(i, 1);
            }
        }
        return this;
    }

}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120

注册全局 service

ts
          
export class ThemeModule {
    static forRoot(): ModuleWithProviders<ThemeModule> {
        return {
            ngModule: ThemeModule,
            providers: [
                SearchService,
            ]
        };
    }
}
12345678910
ts
      
@NgModule({
    imports: [
        ThemeModule.forRoot(),
    ]
})
export class AppModule { }
123456

添加搜索框

html
             
<div class="dialog-search" [ngClass]="{inputting: suggestText.length > 0}" [hidden]="!panelVisible">
    <i class="iconfont icon-close dialog-close" (click)="close()"></i>
    <div class="dialog-body">
        <div class="search-input">
            <i class="iconfont icon-search input-search"></i>
            <input type="text" placeholder="请输入关键字,按回车 / Enter 搜索" autocomplete="off" [(ngModel)]="suggestText" (keydown)="suggestKeyPress($event)" (ngModelChange)="onSuggestChange()">
            <i class="iconfont icon-close input-clear" (click)="tapClear()"></i>
        </div>
        <ul class="search-suggestion">
            <li *ngFor="let item of suggestItems;let i = index" [ngClass]="{active: i === suggestIndex}" (click)="tapItem(item)"><span>{{ i + 1 }}</span>{{ formatTitle(item) }}</li>
        </ul>
    </div>        
</div>
12345678910111213
ts
                                                                                                        
import { Component, OnDestroy, OnInit } from '@angular/core';
import { SearchService } from '../../theme/services';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {

    public panelVisible = false;
    public suggestItems: any[] = [];
    public suggestText = '';
    public suggestIndex = -1;
    private asyncHandle = 0;

    constructor(
        private searchService: SearchService,
    ) { }

    ngOnInit() {
        this.searchService.on('suggest', items => {
            this.suggestIndex = -1;
            this.suggestItems = items;
        });
    }

    ngOnDestroy() {
        this.searchService.offTrigger();
    }

    public formatTitle(item: any) {
        if (typeof item !== 'object') {
            return item;
        }
        // 这里只接受 title 或 name 属性进行显示
        return item.title || item.name;
    }

    public suggestKeyPress(event: KeyboardEvent) {
        if (event.key === 'Enter') {
            this.searchService.emit('confirm', this.suggestIndex >= 0 ? this.suggestItems[this.suggestIndex] : this.suggestText);
            this.close();
            return;
        }
        if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
            this.suggestIndex = -1;
            return;
        }
        if (this.suggestItems.length < 0) {
            return;
        }
        let i = this.suggestIndex;
        if (event.key === 'ArrowDown') {
            i = i < this.suggestItems.length - 1 ? i + 1 : 0;
        } else if (event.key === 'ArrowUp') {
            i = (i < 1 ? this.suggestItems.length: i) - 1;
        }
        this.suggestIndex = i;
        this.suggestText = this.formatTitle(this.suggestItems[this.suggestIndex]);
    }

    public onSuggestChange() {
        if (this.suggestIndex >= 0) {
            return;
        }
        this.asyncSuggest();
    }

    public tapItem(item: any) {
        this.searchService.emit('confirm', item);
        this.close();
    }

    public tapClear() {
        this.suggestText = '';
        this.suggestItems = [];
    }

    public open() {
        this.panelVisible = true;
    }

    public close() {
        this.panelVisible = false;
    }

    private asyncSuggest() {
        if (this.asyncHandle) {
            clearTimeout(this.asyncHandle);
        }
        this.suggestIndex = -1;
        this.asyncHandle = window.setTimeout(() => {
            this.asyncHandle = 0;
            this.suggestIndex = -1;
            if (this.suggestText.length < 1) {
                this.suggestItems = [];
                return;
            }
            this.searchService.emit('change', this.suggestText);
        }, 300);
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104

搜索页面

其他页面,根据搜索关键词跳转搜索页面或详情页。

ts
                            
export class BlogComponent implements OnInit, OnDestroy {

    constructor(
        private searchService: SearchService,
        private service: BlogService,
        private router: Router,
        private route: ActivatedRoute,
    ) {
    }

    ngOnInit() {
        this.searchService.on('change', keywords => {
            return this.service.suggesttion({keywords});
        }).on('confirm', res => {
            if (typeof res === 'object') {
                this.router.navigate([res.id], {relativeTo: this.route});
                return;
            }
            this.router.navigate(['./'], {relativeTo: this.route, queryParams: {
                keywords: res
            }});
        });
    }

    ngOnDestroy() {
        this.searchService.offReceiver();
    }
}
12345678910111213141516171819202122232425262728

如果当前就是搜索页面,那么可以不用跳转

ts
                
private searchFn = res => {
    if (typeof res === 'object') {
        return;
    }
    this.queries.keywords = res;
    this.tapRefresh();
    // 阻止事件传递
    return false;
};
ngOnInit() {
    this.searchService.on('confirm', this.searchFn);
}

ngOnDestroy() {
    this.searchService.off('confirm', this.searchFn);
}
12345678910111213141516

注意

搜索事件并不会自动清除,需要添加 ngOnDestroy 进行清除

angular 12 中单例 Service 的使用

2021-06-30 05:59:04

angular 12 singleton service

angular 12 中单例 Service 的使用

service 写法

ts
        
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root',
})
export class SearchService {

}
12345678

providedIn: 'root' 表明当前 service 为全局有效的单例模式

注册 service

必须在 AppModule 注册,或者 在任意一个 module 中注册。

但是需要注意,这个 module 只能导入一次,

例如:有一个 module ThemeModule 为公共核心组件,很多其他 module 都会依赖她,

导致很多地方都使用了

ts
     
imports: [
    ...,
    ThemeModule,
    ...
]
12345

进行导入

而 单例 service SearchService 是注册在 ThemeModule 中的,

这样就会导致单例不生效,每一个 SearchService 都不同,

这是可以给 ThemeModule 增加一个方法 forRoot()

ts
          
export class ThemeModule {
    static forRoot(): ModuleWithProviders<ThemeModule> {
        return {
            ngModule: ThemeModule,
            providers: [
                SearchService,
            ]
        };
    }
}
12345678910

SearchService 仅注册到 forRoot() 中。

然后在 AppModule 中导入 ThemeModule

ts
          
@NgModule({
    ...
    imports: [
        ...,
        ThemeModule.forRoot(),
        ...
    ],
    ...
})
export class AppModule 
12345678910

这样就能生效

js 实现一个正则替换

2021-06-05 02:12:24

js 实现一个正则替换

代码

ts
                         
/**
 * 正则匹配替换
 * @param content 
 * @param pattern 
 * @param cb 
 * @returns 
 */
export function regexReplace(content: string, pattern: RegExp, cb: (match: RegExpExecArray) => string): string {
    if (content.length < 1) {
        return content;
    }
    const matches: RegExpExecArray[] = [];
    let match: RegExpExecArray|null;
    while (null !== (match = pattern.exec(content))) {
        matches.push(match as RegExpExecArray);
    }
    const block: string[] = [];
    for (let i = matches.length - 1; i >= 0; i--) {
        match = matches[i];
        block.push(content.substr(match.index + match[0].length));
        block.push(cb(match));
        content = content.substr(0, match.index);
    }
    return content + block.reverse().join('');
}
12345678910111213141516171819202122232425

原理

  1. 先获取所有的匹配结果,存入一个临时数组中
  2. 倒序执行回调方法获取替换的内容
  3. 按照匹配到的位置,截断字符串,把匹配到的部分根据长度去除,然后按倒序和替换的字符串存入一个数组,
  4. 颠倒数组顺序连接即最终结果

疑问

Q:为什么不在匹配时边匹配边替换?

A:原本我这样做的,发现匹配结果出错了,有些没匹配到。因为exec 匹配时正则表达式会记住最后的匹配位置,如果原本的内容长度变化;,就会导致这个位置不正确。

Q:为什么要截断字符串?

A: 使用字符串替换会搜索全部,多一些不必要的操作;截断存入数组,就是想最后一起做拼接。

Q: 为什么要倒序替换?

A: 因为正序截取的话会导致匹配结果中的位置要进行改变。

uwp win2d 使用

2021-05-12 05:53:57

通过 Canvas.Invalidate(); 触发重绘事件

通过 Canvas_Draw 进行绘制

c#
                                 
        private void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
        {
            var progress = Progress;
            var centerX = (float)ActualWidth / 2;
            var centerY = (float)ActualHeight / 2;
            var radius = Math.Min(centerX, centerY);
            var lineRadius = radius - LineWidth;
            var inlineRadius = lineRadius - 5;
            using (var draw = args.DrawingSession)
            {
                draw.FillRectangle(new Windows.Foundation.Rect(centerX - radius, centerY - radius, 2 * radius, 2 * radius), 
                    Colors.Transparent);
                draw.FillCircle(centerX, centerY, inlineRadius, InlineBackground);
                draw.DrawCircle(centerX, centerY, lineRadius, Outline, LineWidth);
                var deg = (2 * Math.PI / 100 * progress)
                    ; // 圆环的绘制
                draw.DrawGeometry(Arc(draw, centerX, centerY, lineRadius, (float)(-.5 * Math.PI), (float)deg), Inline, LineWidth);

                var x = (float)(centerX + Math.Cos(Math.PI * 2 * (progress - 25) / 100) * lineRadius);
                var y = (float)(centerY + Math.Sin(Math.PI * 2 * (progress - 25) / 100) * lineRadius);
                draw.FillCircle(x, y, LineWidth, Inline);
            }
        }

        /// 画圆弧
        public CanvasGeometry Arc(ICanvasResourceCreator resourceCreator, float centerX, float centerY, float radius, float startAngle, float endAngle)
        {
            var path = new CanvasPathBuilder(resourceCreator);
            path.BeginFigure(centerX, centerY - radius);
            path.AddArc(new Vector2(centerX, centerY), radius, radius, startAngle, endAngle);
            path.EndFigure(CanvasFigureLoop.Open);
            return CanvasGeometry.CreatePath(path);
        }
123456789101112131415161718192021222324252627282930313233

最后必须手动销毁

c#
     
var canvas = GetTemplateChild("Canvas") as CanvasControl;
if (canvas != null)
{
    canvas.RemoveFromVisualTree();
}
12345

UWP Custom Control自定义控件开发

2021-04-29 06:02:58

Custom Control 又名 Templated Control 模板控件

开发

分为两个文件

一个资源文件 Themes/Generic.xaml 里面,主要放默认的模板及初始化属性

一个cs 文件 控件名.cs,主要放 声明属性及事件

一个简单的控件,由一个图标和文字组成的控件

c#
                                  
public sealed class IconTag : Control
    {
        public IconTag()
        {
            this.DefaultStyleKey = typeof(IconTag);
        }

        /// <summary>
        /// 内容
        /// </summary>
        public string Label
        {
            get { return (string)GetValue(LabelProperty); }
            set { SetValue(LabelProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Label.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LabelProperty =
            DependencyProperty.Register("Label", typeof(string), typeof(IconTag), new PropertyMetadata(null));


        /// <summary>
        /// 内容字体图标
        /// </summary>
        public string Icon
        {
            get { return (string)GetValue(IconProperty); }
            set { SetValue(IconProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Icon.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IconProperty =
            DependencyProperty.Register("Icon", typeof(string), typeof(IconTag), new PropertyMetadata(null));
    }
12345678910111213141516171819202122232425262728293031323334
xml
                    
 <Style TargetType="local2:IconTag">
    <Setter Property="FontFamily" Value="Microsoft YaHei"/>
    <Setter Property="Margin" Value="0,0,10,0"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local2:IconTag">
                <Grid Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <FontIcon Glyph="{TemplateBinding Icon}" FontSize="{TemplateBinding FontSize}" VerticalAlignment="Center"/>
                    <TextBlock Text="{TemplateBinding Label}" 
                                FontFamily="{TemplateBinding FontFamily}"
                                VerticalAlignment="Center" Grid.Column="1" FontSize="{TemplateBinding FontSize}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
1234567891011121314151617181920

ControlTemplate就是放默认模板,

Setter Property= 就是声明一些初始化的属性

代码获取控件模板中的控件

必须先使用 x:Name 声明名称

xml
                  
 <Style TargetType="local2:IconTag">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local2:IconTag">
                <Grid Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <FontIcon Glyph="{TemplateBinding Icon}" FontSize="{TemplateBinding FontSize}" VerticalAlignment="Center"/>
                    <TextBlock x:Name="Content" Text="{TemplateBinding Label}" 
                                FontFamily="{TemplateBinding FontFamily}"
                                VerticalAlignment="Center" Grid.Column="1" FontSize="{TemplateBinding FontSize}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
123456789101112131415161718

必须声明模板必须包含 x:Name="Content" 且 类型为 TextBlock

c#
             
    [TemplatePart(Name = "Content", Type = typeof(TextBlock))]
    public sealed class IconTag : Control {
        public IconTag()
        {
            DefaultStyleKey = typeof(IconTag);
            Loaded += IconTag_Loaded;
        }

        private void IconTag_Loaded(object sender, RoutedEventArgs e)
        {
            var tb = GetTemplateChild("Content") as TextBlock;
        }
    }
12345678910111213

通过 GetTemplateChild 获取控件,而且必须等控件加载完了才能获取到。

UWP 读取应用内资源

2021-04-29 05:44:43

需求

在项目里加了一个css 样式文件,

需要把这个样式引用到 webview 中

方法

在项目资源文件里放入样式文件

Assets/markdown.css

选中文件右键“属性”

更改文件属性

复制到输出目录: 始终复制
生成操作:内容

然后和html源码合并给webview

c#
                 
private async Task<string> RenderHtmlAsync(string content)
{
    string style;
    try
    {
        var fileUri = new Uri("ms-appx:///Assets/markdown.css", UriKind.Absolute);
        var file = await StorageFile.GetFileFromApplicationUriAsync(fileUri);
        style = await FileIO.ReadTextAsync(file);
    }
    catch (Exception)
    {
        style = string.Empty;
    }
    return $"<style>{style}</style><div class=\"markdown\">{content}</div>";
}

webView.NavigateToString(await RenderHtmlAsync(data.Content));
1234567891011121314151617

UWP读取应用内资源

gin 使用笔记(二)出错点

2021-04-22 03:41:58

自动绑定表单数据

使用 ShouldBindQuery 或者 ShouldBind 需要注意结构体的格式

例如一个查询分页的参数获取

go
            
type Queries struct {
    Page     uint   `form:"page" json:"page"`
    PerPage  uint   `form:"per_page" json:"per_page"`
    Keywords string `form:"keywords" json:"keywords"`
}

func GetList(c *gin.Context) {
    var query Queries
    if err := c.ShouldBindQuery(&query); err != nil {
        // error
    }
}
123456789101112

form:"page" json:"page" 这个就是结构体的解析说明又名struct tag

这里的 form 指查询参数或表单提交的参数,如果是 GET 或 表单POST 的数据必须有这个标记,不然会绑定失败

如果是 POSTjson 则用 json:"page"

如果是 POSTxml 则用 xml:"page"

ShouldBind 是会自动判断内容的格式,是GET 会匹配网址上的参数,其他则根据 请求头 Content-Type 自动判断

reflect: reflect.Value.SetUint using unaddressable value 只要出现 using unaddressable value 就表明代码中该传引用的地方传了值

CORS 的使用

对与跨域的处理中间件必须放在全局即 *gin.Engine 上面,不能放在某一个路由组上

go
  
r := gin.Default()
r.Use(middleware.CORS)
12

原因是跨域请求会产生一个前置 OPTIONS 请求,而这个请求实际是不需要响应内容的,如果没有匹配的路由就会响应404 影响下一步浏览器发出真正的请求

websocket

不能添加请求头

所有登录信息没法通过请求头传递,而网址传递并不安全,所以只能先建立连接,然后通过发送消息传递

gin 使用笔记(一)基础

2021-04-18 20:49:26

关于路由

路由 ` 和/` 可以分开作为两个路由, 但是如果只有一个,就会把没注册的那个重定向到注册的那个

go
   
r := gin.Default()
r.GET("/home", Index)
r.GET("/home/", Abount)
123

这种方法时有效的

如果只注册一个

go
  
r := gin.Default()
r.GET("/home", Index)
12

浏览器访问 http://localhost/home/ 就会响应 301 并跳转到 http://localhost/home

路由分组

可以通过 ` ` 进行更精确的分组

go
      
r := gin.Default()
blog := r.Group("/blog")
blog.GET("", Index)
g := blog.Group("")
g.Use(middleware.CORS)
g.GET("/count", Count)
123456

中间件

gin.HandlerFunc 只能通过 c.Abort 进行中断,否则会继续执行下去

go
       
r := gin.Default()
r.Use(func(c *gin.Context) {
    if false {
        c.AbortWithStatusJSON(400, json.RenderFailure(err.Error()))
        return
    }
})
1234567

c.Next() 不是必须调用,如果需要对输出结果进行操作,这是才需要在内部调用

在中间件中传值

c.Keys["key"] = val

路由方法

c.ShouldBindQuery 可以绑定查询值到模型

c.Get() c.GetInt() c.GetString() 等是获取 c.Keys 注册的值

c.Query 获取查询的值

c.ShouldBind 是绑定post 提交的值到模型

c.PostForm 获取post的值

获取网址中匹配的值

go
    
r.GET("/user/:id", func(c *gin.Context) {
    // a GET request to /user/john
    id := c.Param("id") // id == "john"
})
1234

模板

go
    
r := gin.Default()
r.Static("/assets", configs.Config.Asset) // 指定资源文件的路径及文件夹
r.StaticFile("/favicon.ico", configs.Config.Favicon) // 指定网站图标
r.LoadHTMLGlob("templates/**/*")
1234

特别注意:

templates/**/* 会匹配 templates 文件夹下的子文件夹中的文件,但是只会注册文件名

例如

templates/blog/index.htmltemplates/auth/index.html

go
   
func Index(ctx *gin.Context) {
    ctx.HTML(200, "index.html", gin.H{})
}
123

ctx.HTML(200, "blog/index.html", gin.H{}) 这样是访问不到的,

ctx.HTML(200, "index.html", gin.H{}) 这样才能访问到,但是访问的是 templates/blog/index.html

如果要两个文件都生效,只能改文件名 templates/auth/auth_index.html

ctx.HTML(200, "auth_index.html", gin.H{})

angular 关于自定义组件事件传递

2021-04-14 04:11:39

事实事件有两种定义方式

@Output 声明事件

  1. angular 通常方式,使用 @Output()方式
ts
     
// 在组件声明一个事件
@Output() public tapped = new EventEmitter();

// 组件中触发事件
this.tapped.emit();
12345

调用组件

html
 
<app-custom (tapped)="onTapped()">
1
ts
     

public onTapped() {
    // TODO
}
12345

优点

不需要考虑事件是否有接收,同时可以被接收多次,而且可以接受父页面上的值。

缺点

没办法判断事件是否注册了,传递值只能传一个,可以把多个值合并成一个 object 传递

ts
     
// 在组件声明一个事件
@Output() public tapped = new EventEmitter<number>();

// 组件中触发事件
this.tapped.emit(1);
12345

调用组件

html
 
<app-custom (tapped)="onTapped($event)">
1
ts
     

public onTapped(e: number) {
    // TODO
}
12345

直接把方法当值传递

  1. 非正常方式,使用 @Input()方式
ts
     
// 在组件声明一个值
@Input() public tapped: () => void;

// 组件中触发事件
this.tapped && this.tapped();
12345

调用组件

html
 
<app-custom [tapped]="onTapped">
1
ts
     

public onTapped() {
    // TODO
}
12345

优点

可以同时传递多个值,可以判断是否有事件

缺点

没法同时接受父页面上的值,而且这个方法的内部 this 为子组件,所以就没法调用父组件的方法或属性。

ts
       

private refresh()

public onTapped() {
    this.refresh();   // error refresh is undefined
}
1234567

angular 11 怎么获取 Content-Disposition

2021-04-13 22:42:34

需求

angular 实现文件下载功能, 默认只能在前端代码中手动添加文件类型及文件名。

ts
                                        
export class DownloadService {
    constructor(private http: HttpClient) { }

    /**
     * Blob请求
     */
    public requestBlob(url: string, data?: any): Observable<any> {
        return this.http.request('post', url, {
            body: data,
            observe: 'response',
            responseType: 'blob',
        });
    }

    /**
     * Blob文件转换下载
     */
    public downFile(result: any, fileName: string, fileType?: string) {
        const data = result.body;
        const blob = new Blob([data], {
                type: fileType || data.type,
            });
        const objectUrl = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.setAttribute('style', 'display:none');
        a.setAttribute('href', objectUrl);
        a.setAttribute('download', fileName);
        a.click();
        URL.revokeObjectURL(objectUrl);
    }

    public export(url: string, data: any, fileName: string, fileType?: any) {
        this.requestBlob(url, data).subscribe(result => {
            const headers = result.headers as HttpHeaders;
            this.downFile(result, fileName,
                fileType || headers.get('Content-Type'));
        });
    }

}
12345678910111213141516171819202122232425262728293031323334353637383940

使用

ts
    
private downloadService: DownloadService

this.downloadService.export('http://localhost/export', {}, '流水记录.xlsx');
1234

突然想到

在响应头中已经有了文件类型和文件名,那么是否可以直接获取呢?

解决

关键是响应头中的 Access-Control-Expose-Headers

angular issue 中就有人提处理这个问题,

并且给出了解决方法

Unable to view 'Content-Disposition' headers in Angular4 GET response

里面提供了一个 Java 的解决方案

翻译成普通语言就是:

需要在服务端响应头中加 Access-Control-Expose-Headers 加上 Content-Disposition

 
Access-Control-Expose-Headers: Content-Disposition
1

基础知识

响应首部 Access-Control-Expose-Headers 列出了哪些首部可以作为响应的一部分暴露给外部。

默认情况下,只有七种 simple response headers (简单响应首部)可以暴露给外部:

  1. Cache-Control
  2. Content-Language
  3. Content-Length
  4. Content-Type
  5. Expires
  6. Last-Modified
  7. Pragma

如果想要让客户端可以访问到其他的首部信息,可以将它们在 Access-Control-Expose-Headers 里面列出来。

 
Access-Control-Expose-Headers: <header-name>, <header-name>, ...
1

多个用英文逗号分隔

来源

最终代码

ts
                                                   
export class DownloadService {
    constructor(private http: HttpClient) { }

    /**
     * Blob请求
     */
    public requestBlob(url: string, data?: any): Observable<HttpResponse<Blob>> {
        return this.http.request('post', url, {
            body: data,
            observe: 'response',
            responseType: 'blob',
        });
    }

    /**
     * Blob文件转换下载
     */
    public downFile(result: HttpResponse<Blob>, fileName?: string, fileType?: string) {
        fileName = this.parseFileName(result.headers.get('Content-Disposition'), fileName);
        if (!fileName) {
            console.log('fileName error');
            return;
        }
        const data = result.body;
        const blob = new Blob([data], {
                type: fileType || data.type,
            });
        const objectUrl = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.setAttribute('style', 'display:none');
        a.setAttribute('href', objectUrl);
        a.setAttribute('download', fileName);
        a.click();
        URL.revokeObjectURL(objectUrl);
    }

    public export(url: string, data: any, fileName?: string, fileType?: any) {
        this.requestBlob(url, data).subscribe((res: HttpResponse<Blob>) => {
            this.downFile(res, fileName, fileType);
        });
    }

    private parseFileName(header: string, def?: string): string {
        if (!header) {
            return def;
        }
        const name = header.split(';')[1].trim().split('=')[1];
        return decodeURI(name.replace(/"/g, '')); // 注意中文请在服务端添加url编码
    }

}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

apache 使用gzip 压缩 js、css

2021-04-09 04:34:32

apache开启gzip,自动对输出文件进行压缩

配置

httpd.conf 开启: 去掉前面的注释#即可

LoadModule headers_module modules/mod_headers.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule filter_module modules/mod_filter.so

然后在文件的最后面加上以下代码。

                            
<ifmodule mod_deflate.c>
    DeflateCompressionLevel 6
    AddOutputFilterByType DEFLATE text/plain
    AddOutputFilterByType DEFLATE text/html
    AddOutputFilterByType DEFLATE text/php
    AddOutputFilterByType DEFLATE text/xml
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE text/javascript
    AddOutputFilterByType DEFLATE application/xhtml+xml
    AddOutputFilterByType DEFLATE application/xml
    AddOutputFilterByType DEFLATE application/rss+xml
    AddOutputFilterByType DEFLATE application/atom_xml
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE application/x-javascript
    AddOutputFilterByType DEFLATE application/x-httpd-php
    AddOutputFilterByType DEFLATE application/x-font-ttf
    AddOutputFilterByType DEFLATE image/svg+xml
    # 针对图片开启gzip压缩,但压缩率不高,对文本的压缩率最高
    AddOutputFilterByType DEFLATE image/gif image/png  image/jpe image/swf image/jpeg image/bmp image/webp
    # 排除不需要压缩的文件包括图片和一些其他文件
    BrowserMatch ^Mozilla/4 gzip-only-text/html
    BrowserMatch ^Mozilla/4\.0[678] no-gzip
    BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
    SetEnvIfNoCase Request_URI .(?:html|htm)$ no-gzip dont-varySetEnvIfNoCase 
    #SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png)$ no-gzip dont-vary
    SetEnvIfNoCase Request_URI .(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
    SetEnvIfNoCase Request_URI .(?:pdf|doc)$ no-gzip dont-vary
</ifmodule>
12345678910111213141516171819202122232425262728

apache使用gzip压缩配置

注意

.htaccess 中加代码是没有用的。

参考来源

Apache 开启Gzip压缩——可压缩js、css等静态文件

angular 11 返回上一页保留页面数据的思考

2021-04-07 04:55:28

起因

有这个需要的页面基本是从列表页点击详情或新建编辑页面。在返回需要回到上一次的列表页面,同时保持页面内容不变

例如:在列表已经翻到了第100页,需要修改某一项值(当然可以做一个弹窗修改,这里讨论的是有必要新增页面去修改的情况),

这是点进去修改之后返回,发现到了第一页,这就麻烦了,再放到第100页就需要浪费时间了。

解决方案

  1. 接受分页参数

这时可能想到在网址上加一个可接受的分页属性。

但是这也要手动去输入。

如果还有其他查询参数呢?难道还一个个去输入,这也麻烦。

第一版

 
/list?page=1
1
ts
     
private route: ActivatedRoute

this.route.queryParams.subscribe(params => {
    this.goPage(params.page || 1);
});
12345

第二版

 
/list?page=1&keywords=
1
ts
     
private route: ActivatedRoute

this.route.queryParams.subscribe(params => {
    this.goPage(params);
});
12345

缺点

不灵活

  1. 直接使用 localStorage 保存页面数据

如果所有的列表页面都使用一个值。这样就会发现页面之间会混乱。

例如:

 
/list
1

访问到了第10页,在访问其他页面

 
/list2
1

发现一进去也到了第10页。

那就分开保存,但是 localStorage 是由存储限制的,页面一多。就会发现localStorage存满了,

缺点

不优雅,会受限制

  1. 使用浏览器的网址历史功能

每访问一页就进行保存历史。

ts
                                                                                     

/**
 * 遍历对象属性或数组
 */
export function eachObject(obj: any, cb: (val: any, key?: string|number) => any): any {
    if (typeof obj !== 'object') {
        return cb(obj, undefined);
    }
    if (obj instanceof Array) {
        for (let i = 0; i < obj.length; i++) {
            if (cb(obj[i], i) === false) {
                return false;
            }
        }
        return;
    }
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            if (cb(obj[key], key) === false) {
                return false;
            }
        }
    }
}

export function uriEncode(path: string, obj: any = {}, unEncodeURI?: boolean) {
    const result = [];
    for (const name in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, name)) {
            const value = obj[name];
            result.push(name + '=' + (unEncodeURI ? value : encodeURIComponent(value)));
        }
    }
    if (result.length < 1) {
        return path;
    }
    return path + (path.indexOf('?') > 0 ? '&' : '?') + result.join('&');
}

/**
 * 从当前页面链接获取查询参数
 * @param routeQueries 
 * @param def 
 * @returns 
 */
export const getQueries = <T>(routeQueries: any, def: T, ): T => {
    const queries: any = {};
    const parseNumber = (val: any): number => {
        if (!val) {
            return 0;
        }
        if (typeof val === 'string' && val.indexOf('.') > 0) {
            return parseFloat(val);
        }
        return parseInt(val, 10);
    };
    eachObject(def, (val, key) => {
        if (!routeQueries || !Object.prototype.hasOwnProperty.call(routeQueries, key)) {
            queries[key] = val;
            return;
        }
        if (typeof val === 'number') {
            queries[key] = parseNumber(routeQueries[key]);
            return;
        }
        if (typeof val === 'boolean') {
            queries[key] = routeQueries[key] === true || routeQueries[key] === '1' || routeQueries[key] === 'true';
            return;
        }
        queries[key] = typeof routeQueries[key] === 'undefined' || routeQueries[key] === null ? '' : routeQueries[key];
    });
    return queries;
};

/**
 * 记录查询历史
 * @param queries 
 * @param title 
 */
export const applyHistory = (queries: any, title = '查询列表') => {
    const url = window.location.href;
    const path = url.split('?', 2)[0];
    history.pushState(null, title, uriEncode(path, queries));
    document.documentElement.scrollTop = 0;
};
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485

使用

ts
                    
private route: ActivatedRoute

this.route.queryParams.subscribe(params => {
    this.queries = getQueries(res, {
        keywords: '',
        term: 0,
        page: 1,
        per_page: 20,
    });
    this.goPage(this.queries.page);
});

public goPage(page: number) {
    const queries = {...this.queries, page};
    this.service.logList(queries).subscribe(res => {
        this.items = res.data;
        this.queries = queries;
        applyHistory(queries);
    });
}
1234567891011121314151617181920

一个简单的HTML音视频播放器

2021-04-06 06:16:31

一个简单的HTML音视频播放器

适用场景

本播放器适用场景: 播客类型的文章,需要一个播放器,但是不需要预加载资源文件的。可以尽可能的减少不必要的加载及访客流量浪费。

版本

本播放器有两个版本:

  1. 基于 Jquery 的版本。本文的源码属于这个版本。
  2. 基于Angular 11 的版本。源码见【Github】需要的自取。这个版本音频视频播放器是分开的,而且视频内置了自动区分 iframe 使用。

注意:播放器中的图标都是字体图标,所以只能参考修改。

音频播放器

视频播放器初始界面

视频播放器播放界面

代码

ts
                                                                                                                                                                                                                                                                                                                                                                                                  

interface IPlayerOption {
    [key: string]: any;
    src: string;
    type?: 'audio' | 'video' | 'iframe'
}

;(function($: any) {
    const EVENT_TIME_UPDATE = 'timeupdate';
    const EVENT_PLAY = 'play';
    const EVENT_PAUSE = 'pause';
    const EVENT_ENDED = 'ended';
    const EVENT_VOLUME_UPDATE = 'volumeupdate';
    const EVENT_TAP_PLAY = 'tap_play';
    const EVENT_TAP_PAUSE = 'tap_pause';
    const EVENT_BOOT = 'boot';
    const EVENT_TAP_VOLUME = 'tap_volume';
    const EVENT_TAP_TIME = 'tap_time';
    const EVENT_ENTER_FULL_SCREEN = 'full_screen';
    const EVENT_EXIT_FULL_SCREEN = 'exit_full_screen';

    const screenFull = function() {
        const fnMap = [
            [
                'requestFullscreen',
                'exitFullscreen',
                'fullscreenElement',
                'fullscreenEnabled',
                'fullscreenchange',
                'fullscreenerror'
            ],
            // New WebKit
            [
                'webkitRequestFullscreen',
                'webkitExitFullscreen',
                'webkitFullscreenElement',
                'webkitFullscreenEnabled',
                'webkitfullscreenchange',
                'webkitfullscreenerror'

            ],
            // Old WebKit
            [
                'webkitRequestFullScreen',
                'webkitCancelFullScreen',
                'webkitCurrentFullScreenElement',
                'webkitCancelFullScreen',
                'webkitfullscreenchange',
                'webkitfullscreenerror'

            ],
            [
                'mozRequestFullScreen',
                'mozCancelFullScreen',
                'mozFullScreenElement',
                'mozFullScreenEnabled',
                'mozfullscreenchange',
                'mozfullscreenerror'
            ],
            [
                'msRequestFullscreen',
                'msExitFullscreen',
                'msFullscreenElement',
                'msFullscreenEnabled',
                'MSFullscreenChange',
                'MSFullscreenError'
            ]
        ];

        for (const item of fnMap) {
            if (item && item[1] in document) {
                return item;
            }
        }
        return false;
    }();

    class MediaPlayer {
        constructor(
            public element: JQuery,
            public options: IPlayerOption
        ) {
            if (!this.options.src) {
                return;
            }
            this.init();
            this.bindCustomEvent();
        }

        private playerElement: HTMLVideoElement|HTMLAudioElement;
        private playerBar: JQuery;
        private booted = false;
        private volumeLast = 100;
        private duration = 0;

        public on(event: string, callback: Function): this {
            this.options['on' + event] = callback;
            return this;
        }

        public hasEvent(event: string): boolean {
            return this.options.hasOwnProperty('on' + event);
        }

        public trigger(event: string, ... args: any[]) {
            let realEvent = 'on' + event;
            if (!this.hasEvent(event)) {
                return;
            }
            return this.options[realEvent].call(this, ...args);
        }

        private bindCustomEvent() {
            this.on(EVENT_BOOT, () => {
                if (this.booted) {
                    return;
                }
                if (this.options.type === 'audio') {
                    this.bindAudioEvent();
                    return;
                }
                if (this.options.type === 'iframe') {
                    this.videoFrame();
                    this.booted = true;
                    return;
                }
                this.videoPlayer();
                this.initBar(this.element.find('.player-bar'));
                this.bindVideoEvent();
            }).on(EVENT_TAP_PLAY, () => {
                this.playerElement.play();
            }).on(EVENT_TAP_PAUSE, () => {
                this.playerElement.pause();
            }).on(EVENT_TIME_UPDATE, (p: number, t: number) => {
                this.duration = t;
                this.playerBar.find('.time').text(this.formatMinute(p) + '/' + this.formatMinute(t));
                const progess = this.playerBar.find('.slider .progress');
                progess.attr('title', parseInt(p.toString()));
                progess.find('.progress-bar').css('width', p * 100 / t + '%');
            }).on(EVENT_PLAY, () => {
                this.playerBar.find('.icon .fa').addClass('fa-pause').removeClass('fa-play');
            }).on(EVENT_PAUSE, () => {
                this.playerBar.find('.icon .fa').removeClass('fa-pause').addClass('fa-play');
            }).on(EVENT_ENDED, () => {
                this.trigger(EVENT_PAUSE);
            }).on(EVENT_TAP_VOLUME, (v: number) => {
                if (!this.playerElement) {
                    return;
                }
                this.playerElement.volume = v / 100;
                this.trigger(EVENT_VOLUME_UPDATE, v);
            }).on(EVENT_VOLUME_UPDATE, (v: number) => {
                const progess = this.playerBar.find('.volume-slider .progress');
                progess.attr('title', parseInt(v.toString()));
                progess.find('.progress-bar').css('width', v + '%');
                let volumeCls = 'fa-volume-up';
                if (v <= 0) {
                    volumeCls = 'fa-volume-off';
                } else if (v < 60) {
                    volumeCls = 'fa-volume-down';
                }
                this.playerBar.find('.volume-icon .fa').attr('class', 'fa ' + volumeCls);
            }).on(EVENT_TAP_TIME, (p: number) => {
                if (!this.playerElement) {
                    return;
                }
                this.playerElement.currentTime = p;
            }).on(EVENT_EXIT_FULL_SCREEN, () => {
                this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-expand');
                this.element.removeClass('player-full');
            }).on(EVENT_ENTER_FULL_SCREEN, () => {
                this.element.addClass('player-full');
                this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-compress');
            });
        }

        private init() {
            if (this.options.type === 'audio') {
                this.initAudio();
                return;
            }
            this.initVideo();
        }

        private initAudio() {
            this.audioPlayer();
            this.initBar(this.element);
        }

        private initBar(bar: JQuery) {
            this.playerBar = bar;
            const that = this;
            bar.on('click', '.icon .fa', function() {
                if (!that.booted) {
                    that.trigger(EVENT_BOOT);
                }
                that.trigger($(this).hasClass('fa-play') ? EVENT_TAP_PLAY : EVENT_TAP_PAUSE);
            }).on('click', '.volume-icon .fa', function() {
                const $this = $(this);
                if ($this.hasClass('fa-volume-mute') || $this.hasClass('fa-volume-off')) {
                    that.trigger(EVENT_TAP_VOLUME, that.volumeLast);
                    return;
                }
                if (that.playerElement) {
                    that.volumeLast = that.playerElement.volume * 100;
                }
                that.trigger(EVENT_TAP_VOLUME, 0);
            }).on('click', '.slider .progress', function(event) {
                const $this = $(this);
                that.trigger(EVENT_TAP_TIME, (event.clientX - $this.offset().left) * that.duration / $this.width());
            }).on('click', '.volume-slider .progress', function(event) {
                const $this = $(this);
                that.trigger(EVENT_TAP_VOLUME, (event.clientX - $this.offset().left) * 100 / $this.width());
            }).on('click', '.full-icon .fa', function() {
                if (that.element.hasClass('player-full')) {
                    that.exitFullscreen();
                    return;
                }
                that.fullScreen();
            });
        }

        private initVideo() {
            this.videoMask();
            this.element.on('click', '.player-mask', () => {
                this.trigger(EVENT_BOOT);
                if (this.playerElement) {
                    this.trigger(EVENT_TAP_PLAY);
                }
            });
        }

        private bindAudioEvent() {
            if (this.booted) {
                return;
            }
            this.booted = true;
            this.playerElement = document.createElement('audio');
            this.playerElement.src = this.options.src;
            this.playerElement.addEventListener('timeupdate', () => {
                if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) {
                    this.trigger(EVENT_TIME_UPDATE, 0, 0);
                    return;
                }
                this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration);
            });
            this.playerElement.addEventListener('ended', () => {
                this.trigger(EVENT_ENDED);
            });
            this.playerElement.addEventListener('pause', () => {
                this.trigger(EVENT_PAUSE);
            });
            this.playerElement.addEventListener('play', () => {
                this.trigger(EVENT_PLAY);
            });
            this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100);
        }

        private bindVideoEvent() {
            if (this.booted) {
                return;
            }
            this.booted = true;
            this.playerElement = this.element.find('.player-video')[0] as HTMLVideoElement;
            this.playerElement.addEventListener('timeupdate', () => {
                if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) {
                    this.trigger(EVENT_TIME_UPDATE, 0, 0);
                    return;
                }
                this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration);
            });
            this.playerElement.addEventListener('ended', () => {
                this.trigger(EVENT_ENDED);
            });
            this.playerElement.addEventListener('pause', () => {
                this.trigger(EVENT_PAUSE);
            });
            this.playerElement.addEventListener('play', () => {
                this.trigger(EVENT_PLAY);
            });
            this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100);
            if (screenFull) {
                document.addEventListener(screenFull[4], () => {
                    if (this.checkFull()) {
                        this.trigger(EVENT_ENTER_FULL_SCREEN);
                        return;
                    }
                    this.trigger(EVENT_EXIT_FULL_SCREEN);
                });
            }
        }

        private audioPlayer() {
            this.element.addClass('audio-player');
            this.element.html(`<div class="icon" title="播放">
            <i class="fa fa-play"></i>
        </div>
        <div class="slider">
            <div class="progress" title="0">
                <div class="progress-bar"></div>
            </div>
        </div>
        <div class="time">
            00:00/00:00
        </div>
        <div class="volume-icon">
            <i class="fa fa-volume-up"></i>
        </div>
        <div class="volume-slider">
            <div class="progress" title="100">
                <div class="progress-bar" style="width: 100%;"></div>
            </div>
        </div>`);
        }

        private videoMask() {
            this.element.addClass('video-player');
            this.element.html(`<div class="player-mask" title="此处有视频,点击即可播放">
            <i class="fa fa-play"></i>
        </div>`);
        }

        private videoFrame() {
            this.element.html(`
            <iframe class="player-frame" src="${this.options.src}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`);
        }

        private videoPlayer() {
            this.element.html(`<video class="player-video" src="${this.options.src}"></video>
            <div class="player-bar">
                <div class="icon" title="播放">
                    <i class="fa fa-play"></i>
                </div>
                <div class="slider">
                    <div class="progress" title="0">
                        <div class="progress-bar"></div>
                    </div>
                </div>
                <div class="time">
                    00:00/00:00
                </div>
                <div class="volume-icon">
                    <i class="fa fa-volume-up"></i>
                </div>
                <div class="volume-slider">
                    <div class="progress" title="100">
                        <div class="progress-bar" style="width: 100%;"></div>
                    </div>
                </div>
                <div class="full-icon">
                    <i class="fa fa-expand"></i>
                </div>
            </div>`);
        }

        private formatMinute(time: number): string {
            return this.twoPad(Math.floor(time / 60)) + ':' + this.twoPad(Math.floor(time % 60));
        }

        private twoPad(n: number) {
            const str = n.toString();
            return str[1] ? str : '0' + str;
        }

        private fullScreen(element: any = document.documentElement) {
            if (!screenFull) {
                return;
            }
            element[screenFull[0]]();
        }

        private exitFullscreen(element: any = document) {
            if (!screenFull) {
                return;
            }
            element[screenFull[1]]();
        }

        private checkFull(): boolean {
            return screenFull && Boolean(document[screenFull[2]]);
        }
    }
    $.fn.player = function(option?: IPlayerOption) {
        return new MediaPlayer(this, option);
    };
})(jQuery);
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386

样式

scss
                                                                                                                                                            
.audio-player,
.video-player {
    .progress {
        height: .4em;
        border-radius: 0;
        margin-top: .2em;
        display: flex;
        overflow: hidden;
        line-height: 0;
        font-size: .75rem;
        background-color: #e9ecef;
        .progress-bar {
            cursor: default;
            display: flex;
            flex-direction: column;
            justify-content: center;
            overflow: hidden;
            color: #fff;
            text-align: center;
            white-space: nowrap;
            background-color: #007bff;
            transition: width .6s ease;
        }
        &:hover {
            height: .8em;
            margin-top: 0;
        }
    }
}
.audio-player {
    box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%);
    display: flex;
    box-sizing: border-box;
    line-height: 2.5em;
    i {
        font-style: normal;
    }
    .icon {
        width: 2.5em;
        text-align: center;
    }
    .slider {
        flex: 1;
        padding-top: 1em;
    }
    .time {
        line-height: 80rpx;
        font-size: .8em;
    }
    .volume-icon {
        padding-left: .5em;
    }
    .volume-slider {
        width: 3em;
        padding-top: 1em;
        padding-right: .5em;
    }
    &.player-mini {
        .volume-icon,
        .volume-slider {
            display: none;
        }
    }
}
.video-player {
    position: relative;
    box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%);
    i {
        font-style: normal;
    }
    .player-mask {
        background-color: #000;
        text-align: center;
        padding: 1em;
        .fa {
            color: #fff;
            font-size: 4em;
        }
        &:hover {
            .fa {
                color: #a10000;
            }
        }
    }
    .player-frame {
        border: 0;
        width: 100%;
        height: 100vw;
        max-height: 400px;
    }
    .player-video {
        border: 0;
        width: 100%;
        height: 100vw;
        max-height: 400px;
        background-color: #000;
        margin: 0;
    }
    .player-bar {
        display: flex;
        box-sizing: border-box;
        line-height: 2.5em;
        .icon {
            width: 2.5em;
            text-align: center;
        }
        .slider {
            flex: 1;
            padding-top: 1em;
        }
        .time {
            line-height: 80rpx;
            font-size: .8em;
        }
        .volume-icon {
            padding-left: .5em;
        }
        .volume-slider {
            width: 3em;
            padding-top: 1em;
            padding-right: .5em;
        }
        .full-icon {
            width: 2em;
            text-align: center;
        }
    }
    &.player-full {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        top: 0;
        z-index: 9999;
        background-color: #000;
        box-shadow: none;
        .player-video {
            width: 100%;
            height: 100%;
            max-height: 100%;
        }
        .player-bar {
            background-color: rgba(43,51,63,.7);
            color: #fff;
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            opacity: 0;
            transition: 1s opacity;
            &:hover {
                opacity: 1;
            }
        }
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156

使用

html
                  

<link type="text/css" href="player.css" rel="stylesheet" media="all">


<div id="player"></div>


<script src="jquery.player.min.js"></script>

<script type="text/javascript">
jQuery(document).ready(function () {
    $('#player').player({
        src: "1616073275141535.mp3",
        type: "audio"
    });
});
</script>
123456789101112131415161718

请先引入jquery.js

参数说明

src  为资源文件的网络路径
type 可选值 video|audio|iframe ;默认为 video ; audio 即音频;iframe 为其他网址的视频,例如 bilibili 分享的链接

参考资料

  1. screenfull.js

Net Core 实现一个简单的分页功能

2021-04-04 06:29:14

主要代码

c#
                                                                                                               
    [HtmlTargetElement("pagination")]
    public class PagerTagHelper : TagHelper
    {
        // 总共有多少条记录
        public long Total { get; set; } = 0;

        // 一页有多少条记录
        public int PerPage { get; set; } = 20;

        // 当前第几页
        public int Page { get; set; } = 1;

        // 分页链接的最多显示个数
        public int PageLength { get; set; } = 7;

        private string url;
        // 当前网址
        public string Url
        {
            get { return url; }
            set {
                if (value.IndexOf('?') < 0)
                {
                    url = value + "?page=";
                    return;
                }
                url = Regex.Replace(value, @"([\?\&])page=\d+\&*", "$1") + "&page=";
            }
        }

        // 是否显示上一页下一页
        public bool DirectionLinks { get; set; } = false;

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            var html = new StringBuilder();
            var items = initLinks(out bool canPrevious, out bool canNext);
            html.Append("<ul class=\"pagination\">");
            if (DirectionLinks && canPrevious)
            {
                html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{1}{2}\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"sr-only\">上一页</span></a></li>", 
                    canPrevious ? "" : " disabled",
                    Url, Page - 1
                    );
            }
            foreach (var item in items)
            {
                if (item < 1)
                {
                    html.Append("<li class=\"page-item disabled\"><a class=\"page-link\">...</a></li>");
                    continue;
                }
                html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{2}{3}\">{1}</a></li>",
                    item == Page ? " active" : "",
                    item,
                    Url, item
                    );
            }
            if (DirectionLinks && canNext)
            {
                html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{1}{2}\"  aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"sr-only\">下一页</span></a></li>",
                    canNext ? "" : " disabled",
                    Url, Page + 1
                    );
            }
            html.Append("</ul>");
            output.Content.SetHtmlContent(html.ToString());
        }

        private List<int> initLinks(out bool canPrevious, out bool canNext)
        {
            var total = (int)Math.Ceiling((double)Total / PerPage);
            canPrevious = Page > 1;
            canNext = Page < total;
            var items = new List<int>();
            if (total < 2)
            {
                return items;
            }
            items.Add(1);
            var lastList = (int)Math.Floor((double)PageLength / 2);
            var i = Page - lastList;
            var length = Page + lastList;
            if (i < 2)
            {
                i = 2;
                length = i + PageLength;
            }
            if (length > total - 1)
            {
                length = total - 1;
                i = Math.Max(2, length - PageLength);
            }

            if (i > 2)
            {
                items.Add(0);
            }
            for (; i <= length; i++)
            {
                items.Add(i);
            }
            if (length < total - 1)
            {
                items.Add(0);
            }
            items.Add(total);
            return items;
        }
    }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111

使用

这是控制器中的方法,需要传递当前网址到分页类中。

c#
       
        public IActionResult Index(int page = 1)
        {
            ViewData["items"] = _repository.GetPage(page);
            ViewData["fullUrl"] = $"{HttpContext.Request.Path}{HttpContext.Request.QueryString}";
            ViewData["pageIndex"] = page;
            return View();
        }
1234567

NetDream.Web 为当前项目命名空间

PageNPoco 查询获取到的分页数据

html
          
@using NPoco;
@addTagHelper *, NetDream.Web
@{
    var items = ViewData["items"] as Page<BlogModel>;
    var pageIndex = (int)ViewData["pageIndex"];
}


<pagination url="@ViewData["fullUrl"]" total="@items.TotalItems" page="@pageIndex"></pagination>
12345678910

关于内容中的 @用户 加 话题 的一些想法

2021-03-30 20:33:21

关于内容中的 @用户 加 话题 的一些想法

第一种html形式

直接生成 html 加上链接,这种也分预处理和实时处理

但是这种对多平台应用并不友好,

例如web 端和 app 端就不能统一,必须在应用内部再处理。

这就增加了一些变化。不能统一

例如:

 
@user 这是一个召唤用户 #每天一次#
1

输出的内容为

html
 
<a href="/user/1">@user</a>这是一个召唤用户 <a href="/topic/1">#每天一次#</a>
1

优点

web 端快速

缺点

不能适应多平台应用,增加了其他平台的难度

第二种附加属性

参考搜索对关键词的标注

也可以分预处理和实时处理

返回一段原始文本,附加一些标注参数

例如:

 
@user 这是一个召唤用户 #每天一次#
1

输出

json
               
{
    "content": "@user 这是一个召唤用户 #每天一次#",
    "rules": [
        {
            "word": "@user",
            "rule": "user",
            "id": 1,
        },
        {
            "word": "#每天一次#",
            "rule": "topic",
            "id": 1,
        },
    ]
}
123456789101112131415

这样由各自应用自行组合,不必进行解析步骤。

优点

方便多平台应用使用

拓展

  1. 可以把这段数据由发布时进行保存传输,而不是服务端进行提取。
  2. 加入规则字符串的开始结束位置,能更准确的定位。

数据存储思考

  1. 直接保存到一张表中。保存到 text 或 json 类型的字段中。
  2. 增加一张表,分规则保存。方便关联数据变化进行改动。

Github Host 更改

2021-03-19 19:55:45

特别说明

此方法不稳定,但至少有时能打开

第一步

主要解决 github 加载慢打不开的情况

github网址查询:▷ GitHub.com : GitHub: Where the world builds software · GitHub

github域名查询:▷ github.global.ssl.Fastly.net Website statistics and traffic analysis | Fastly | fastly.net

github静态资源ip:▷ assets-cdn.Github.com Website statistics and traffic analysis | Github | github.com

第二步 刷新DNS缓存

 
ipconfig /flushdns
1

OBS-Studio 等录屏软件录制显示器内容的黑屏的解决方法

2021-03-19 19:37:29

关键点

开始菜单

进入 设置

搜索进入 图形设置

点击 浏览 选择软件的 exe 文件

点击 软件 选项

选择 节能 保存即可

录屏软件黑屏闪屏

非必要

可以给 软件 加上 以管理员身份运行 兼容属性

angular 11 FormBuilder 中 FormGroup 和 FormArray 使用

2021-02-17 20:08:39

angular 11 FormBuilder 中 FormGroup 和 FormArray 使用

导入模块

ts
      
@NgModule({
    imports: [
        ReactiveFormsModule,
    ],
})
export class AppModule {}
123456

注入组件

ts
     
export class EditComponent {
    constructor(
        private fb: FormBuilder,
    ) { }
}
12345

普通表单具体代码

初始化表单

ts
         
export class EditComponent {
    public form = this.fb.group({
        name: ['', Validators.required],
    });

    constructor(
        private fb: FormBuilder,
    ) { }
}
123456789

绑定表单页面

html
           
<form [formGroup]="form" (ngSubmit)="tapSubmit()">
    <div class="form-group row">
        <label for="name" class="col-md-3 col-form-label">称呼</label>
        <div class="col-md-9">
            <input type="text" class="form-control" formControlName="name" id="name" placeholder="请输入称呼">
        </div>
    </div>
    <div class="row">
        <button class="btn btn-primary offset-md-3">保存</button>
    </div>
</form>
1234567891011

设置值

ts
   
this.form.patchValue({
    name: res.name,
});
123

特殊表单项:一个可变的数组

FormArray 必须为 FormGroup 的子项

初始化表单

ts
                           
export class EditComponent {
    public form = this.fb.group({
        name: ['', Validators.required],
        children: this.fb.array([])
    });

    constructor(
        private fb: FormBuilder,
    ) { }

    get children() {
        return this.form.get('children') as FormArray;
    }

    public addChild() {
        this.children.push(this.fb.group({
            name: ['', Validators.required],
            label: ['', Validators.required],
            required: [false],
            only: [false],
        }));
    }

    public removeChild(i: number) {
        this.children.removeAt(i);
    }
}
123456789101112131415161718192021222324252627

绑定表单页面

html
                                                  
<form [formGroup]="form" (ngSubmit)="tapSubmit()">
    <div class="form-group row">
        <label for="name" class="col-md-3 col-form-label">称呼</label>
        <div class="col-md-9">
            <input type="text" class="form-control" formControlName="name" id="name" placeholder="请输入称呼">
        </div>
    </div>
    <table class="table table-hover">
        <thead>
            <tr>
                <th>名称</th>
                <th>别名</th>
                <th>是否必填</th>
                <th>不公开</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <ng-container *ngFor="let item of children.controls; let i = index">
                <tr [formGroup]="item">
                    <td>
                        <input type="text" formControlName="name" class="form-control">
                    </td>
                    <td>
                        <input type="text" formControlName="label" class="form-control">
                    </td>
                    <td>
                        <input type="checkbox" formControlName="required" value="1"  >
                    </td>
                    <td>
                        <input type="checkbox" formControlName="only" value="1" >
                    </td>
                    <td>
                        <i class="iconfont icon-close" (click)="removeChild(i)"></i>
                    </td>
                </tr>
            </ng-container>
        </tbody>
        <tfoot>
            <tr>
                <td colspan="5">
                    <i class="iconfont icon-plus" (click)="addChild()"></i>
                </td>
            </tr>
        </tfoot>
    </table>
    <div class="row">
        <button class="btn btn-primary offset-md-3">保存</button>
    </div>
</form>
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

设置值

ts
   
for (const item of items) {
    this.children.push(this.fb.group(item));
}
123

angular 11 ngrx/effects 使用理解

2021-02-08 00:05:27

angular 11 ngrx/effects 使用理解

我的理解:这个模块就是把网络请求获取数据,然后更新数据状态的操作合并到了一起。依然需要手动触发,既不会启动时自动加载,也不会获取时只加载一次。

例子:

有一个全局的数据:站点信息

ts
   
interface AppState {
    site: ISite;
}
123

通常的方法是:

  1. 网络请求获取数据
  2. 更新到Store中

app-component.ts

ts
     
ngOnInit() {
    this.service.site().subscribe(site => {
        this.store.dispatch(setSite({site}));
    });
}
12345

app.actions.ts

ts
      
export const selectSite = createSelector(
    (state: AppState) => state.site
);

export const setSite = createAction('[app]SET_SITE', props<{site: ISite}>());
export const getSite = createAction('[app]GET_SITE');
123456

安装

 
npm i @ngrx/effects
1

声明effects

ts
                  
@Injectable()
export class AppEffects {

    loadSite$ = createEffect(() => this.actions$.pipe(
        ofType(getSite),
        switchMap(() => this.service.site().pipe(
            map(site => setSite({site})),
            catchError(() => EMPTY)
        ))
    ));


    constructor(
        private service: ShopService,
        private actions$: Actions,
    ) {
    }
}
123456789101112131415161718

请注意 ofType 的参数不能是 setSite 不然或导致无限循环

声明

app.moudule.ts

ts
     
@NgModule({
  imports: [
    EffectsModule.forRoot([AppEffects]),
  ],
})
12345

获取并使用

ts
   
this.store.select(selectSite).subscribe(site => {
    // TODO
});
123

到这里并不能自动发送请求获取到信息

还必须请触发

app-component.ts

ts
   
ngOnInit() {
    this.store.dispatch(getSite());
}
123

大致流程

dispatch(getSite()) --> AppEffects.loadSite$ --> service 网络请求 --> setSite({site}) 更新到Store --> 响应 select(selectSite)

angular 11 ngrx/store 使用理解

2021-02-08 00:04:39

angular 11 ngrx/store 使用理解

声明

ts
                             
export interface AuthSate {
    site: ISite;
    cart: any;
}

export const authFeatureKey = 'auth';

export interface AppState {
    [authFeatureKey]: AuthSate;
}

export const initialState: AuthSate = {
    site: null, // 初始化时,必须设置值,未设置(undefined)的将无法通知
    cart: null,
};

// 定义根据action 修改方法
const authReducer = createReducer(
    initialState,
    on(setCart, (state, {cart}) => ({...state, cart})),
    on(setSite, (state, {site}) => ({...state, site})),

    on(clearAuth, state => Object.assign({}, initialState)), // 返回初始化
);

export function reducer(state: State<AppState> | undefined, action: Action) {
    return authReducer(state, action);
}
1234567891011121314151617181920212223242526272829

Actions

定义action 用于修改Store中的数据

ts
  
export const setCart = createAction('[shop]SET_CART', props<{cart: ICart}>());
export const setSite = createAction('[shop]SET_SITE', props<{site: ISite}>());
12

相当于

ts
     
export const setCart = (cart: ICart) => ({
    type: '[shop]SET_CART',
    payload: {cart},
});
12345

Selectors

定义 selector 用于获取数据

ts
           
export const selectAuth = createFeatureSelector<AppState, AuthState>(authFeatureKey);

export const selectCart = createSelector(
    selectAuth,
    (state: AuthState) => state.cart
);

export const selectSite = createSelector(
    selectShop,
    (state: AuthState) => state.site
);
1234567891011

注册到程序中

注册根

app.module.ts

ts
   
StoreModule.forRoot({
    [authFeatureKey]: reducer,
}),
123

这是必须的如果没有全局数据,则

ts
 
StoreModule.forRoot({}),
1

也可以为某一个模块注册局部数据

ts
 
StoreModule.forFeature(authFeatureKey, reducer),
1

使用

  1. 设置
ts
           
export class AppComponent {
    constructor(
        private store: Store<AppState>,
    ) {

    }

    setSite() {
        this.store.dispatch(setSite({site: {}}));
    }
}
1234567891011
  1. 获取数据
ts
         
export class AppComponent {
    constructor(
        private store: Store<AppState>,
    ) {
        this.store.select(selectSite).subscribe(site => {
            // TODO
        });
    }
}
123456789

特别注意

获取数据是一直实时更新的,除非取消订阅了。

更新的频率是依赖 reducer 的,把 reducer 看作一个对象,对象上的某一个属性更新了,会触发其他的属性更新事件

例如:

ts
    
 this.store.select(selectSite).subscribe(site => {
    // 获取到的 site 的值
    this.store.dispatch(setCart({cart: {}})); // 这样会陷入死循环,更新了 cart 的值,也会触发 site 更新的事件
});
1234

angular 10 直接获取表单值

2021-01-04 01:08:39

angular 10 直接获取表单值

在一些情况下只需要简单的获取表单的值,而不需要做一些不必要的值定义

例如:

  1. 列表的搜索框,

当然可以一个一个的值进行定义,通过绑定

ts
 
public keywords = '';
1
html
 
<input [(ngModel)]="keywords">
1

但这种情况值是始终保持和输入一直,但并不能同时保持和搜索结果的条件一直。

这时我想到了直接获取搜索表单

html
       
<form (ngSubmit)="tapSearch(searchForm.value)" #searchForm="ngForm">
    <div class="input-group">
        <label for="keywords">标题</label>
        <input type="text" class="form-control" name="keywords" ngModel id="keywords" placeholder="搜索标题" [value]="keywords">
    </div>
    <button type="submit" class="btn btn-primary">搜索</button>
</form>
1234567
ts
      
public keywords = '';

public tapSearch(form: any) {
    this.keywords = form.keywords || '';
    // TODO
}
123456

这里需要注意

html
 
(ngSubmit)="tapSearch(searchForm.value)" #searchForm="ngForm"
1

#searchForm="ngForm" 是获取表单对象

 
<input name="keywords" ngModel [value]="keywords">
1

ngModel 进行绑定键值,必须的,不然在 searchForm.value 中无法获取到值

searchForm.value 的具体值为

ts
   
{
    keywords: ''
}
123

angular 10 使用 tinymce 编辑器

2020-10-13 00:36:32

使用 tinymce 提供的云编辑器

即真正的 tinymce 编辑器脚本都放到 他们的服务器上,

但访问速度真的不行

使用方法

 
npm i @tinymce/tinymce-angular
1

然后在模块里加载

ts
               
 import { EditorModule } from '@tinymce/tinymce-angular';
 import { AppComponent } from './app.component';

 @NgModule({
   declarations: [
     AppComponent
   ],
   imports: [
     BrowserModule,
     EditorModule // 这就是编辑器模块
   ],
   providers: [],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
123456789101112131415

在页面上使用,这里需要填上你在 www.tiny.cloud 上注册生成的 apiKey

html
                
 <editor
    apiKey="your-api-key"
   [init]="{
     height: 500,
     menubar: false,
     plugins: [
       'advlist autolink lists link image charmap print preview anchor',
       'searchreplace visualblocks code fullscreen',
       'insertdatetime media table paste code help wordcount'
     ],
     toolbar:
       'undo redo | formatselect | bold italic backcolor | \
       alignleft aligncenter alignright alignjustify | \
       bullist numlist outdent indent | removeformat | help'
   }"
 ></editor>
12345678910111213141516

本地打包

则需要安装 tinymce 编辑器本体

 
npm i tinymce
1

然后配置 angular.json 复制脚本文件,及引用脚本

json
      
 "assets": [
   { "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" }
 ],
  "scripts": [
    "node_modules/tinymce/tinymce.min.js"
  ]
123456

在模块加载

ts
           
  import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
  /* ... */
  @NgModule({
    /* ... */
    imports: [
      EditorModule
    ],
    providers: [
      { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }
    ]
  })
1234567891011

使用

html
    
<editor [init]="{
    base_url: '/tinymce', // Root for resources
    suffix: '.min'        // Suffix to use when loading resources
  }"></editor>
1234

加载语言包

官网 下载语言包

复制 zh_CN.jssrc/assets/tinymce/langs 目录下

然后再使用时加上配置

html
      
<editor [init]="{
    base_url: '/tinymce',
    suffix: '.min',
    language_url: '../../../../../assets/tinymce/langs/zh_CN.js',
    language: 'zh_CN',
  }"></editor>
123456

PS:这里的;路径为根目录,如果为二级目录backend,则需要修改为

    
base_url: '/backend/tinymce',
suffix: '.min',
language_url: '../../../../../backend/assets/tinymce/langs/zh_CN.js',
language: 'zh_CN',
1234

图片上传

更改使用时的配置,添加 imagetools 插件

ts
                             
{
    height: 500,
    base_url: '/backend/tinymce',
    suffix: '.min',
    language_url: '../../../../../backend/assets/tinymce/langs/zh_CN.js',
    language: 'zh_CN',
    plugins: [
      'advlist autolink lists link image imagetools charmap print preview anchor',
      'searchreplace visualblocks code fullscreen',
      'insertdatetime media table paste code help wordcount'
    ],
    toolbar:
       'undo redo | formatselect | bold italic backcolor | \
       alignleft aligncenter alignright alignjustify | \
       bullist numlist outdent indent | removeformat | help',
    image_caption: true,
    paste_data_images: true,
    imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions',
    images_upload_handler: (blobInfo, success: (url: string) => void, failure: (error: string) => void) => {
      const form = new FormData();
      form.append('file', blobInfo.blob(), blobInfo.filename());
      // 这里时是上传的具体方法
      this.httpClient.post<any>('upload/image', form).subscribe(res => {
        success(res.url);
      }, err => {
        failure(err.error.message);
      });
    },
}
1234567891011121314151617181920212223242526272829

images_upload_handler 为自定义上传方法,

这样就可以上传图片了

htaccess 搭配 angular 10 放在二级目录

2020-10-13 00:12:01

angular 项目打包放在二级目录

例如:

把一个项目放在 backend 文件夹下

那么 需要设置

index.html

html
 
 <base href="/backend/">
1

最好把所有页面放在一个 backend 的路由下

ts
    
{
    path: 'backend',
    loadChildren: () => import('./backend/backend.module').then(m => m.BackendModule)
}
1234

这样在本地使用时生成的网址时

 
http://localhost:4200/backend/home
1

打包生成的网址也会是

 
http://zodream.cn/backend/home
1

但是这是刷新页面的话并不能指向 angular 程序

需要在网站根目录加上一个 .htaccess 的文件(PS:我用的时apache)

          
<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    RewriteRule ^backend/[^\.]+$ backend/index.html
</IfModule>
12345678910

加上一句 RewriteRule ^backend/[^\.]+$ backend/index.html 就把路径指向 angular 程序了。

再次刷新网址 http://zodream.cn/backend/home 就能争取打开了

微信小程序跨页面传值

2020-10-12 23:57:21

微信小程序跨页面传值

需求:跳转到另一个页面选择一些东西然后显示在页面中 例如:进入筛选项页面,选择,然后回到列表页面刷新页面。

第一种方法

app.js 里的 app.globalData 上设一个属性,进行传值共享

例如:

设置一个可共享的属性 data

ts
     
App({
    globalData: {
        data: {}
    }
})
12345

第二个页面 设置值

ts
       
Page({

    tapBack() {
        getApp().globalData.data = {};
        wx.navigateBack();
    }
})
1234567

第一个页面获取值

ts
      
Page({

    onShow() {
        const data = getApp().globalData.data;
    }
})
123456

这种方法简单常用,但是使用不灵活,而且可能需要清理 getApp().globalData 上的属性,

第二种方法

直接通过 wx.navigateBack 调用页面上的方法进行回调

ts
           
wx.navigateBack({
    success() {
        setTimeout(() => {
            let page = getCurrentPages().pop(); 
            if (!page) {
                return; 
            }
            // TODO
        }, 10);
    }
});
1234567891011

这里需要注意 getCurrentPages().pop() 获取到的页面需要加上延迟,不然在真机上会获取到的是未返回的页面

例如:

第二个页面 返回值

ts
                
Page({

    tapBack() {
        wx.navigateBack({
            success() {
                setTimeout(() => {
                    let page = getCurrentPages().pop(); 
                    if (!page) {
                        return; 
                    }
                    page.selectedBack({});
                }, 10);
            }
        });
    }
})
12345678910111213141516

第一个页面接收值

ts
      
Page({

    selectedBack(data) {
        // TODO
    }
})
123456

js 对 FileList 进行文件过滤上传

2020-10-03 06:45:32

js 对 FileList 进行文件过滤上传

默认 FileList 是只读,无法进行修改

但有时候 前端可以进行一些文件过滤

例如:

当上传多个图片时,以拖拽的形式获取文件,但这时获取到的 FileList 除了图片可能还包含其他类型的文件

这是就可以使用 FormData 进行多文件上传

ts
         
const form = new FormData();
for (let i = 0; i < files.length; i++) {
    const file = files[i];
    if (file.type.indexOf('image/') < 0) {
        continue;
    }
    form.append('file[]', file, file.name);
}
post('upload', form);
123456789

这样通过 post 方式提交,在服务端获取的依然是多个文件

例如:PHP

php
 
$_FILES['file'];  // 获取到的文件就是多文件  ['name' => ['', ''], ...]
1

angular自定义表单组件支持 formControlName

2020-10-01 21:04:20

默认的 ngModel 的实现方式为

ts
                 
@Component({
  selector: 'app-switch',
  template: `
    <div (click)="tapAdd()">{{ value }}</div>
  `,
  styleUrls: ['./switch.component.scss'],
})
export class SwitchComponent { 

    @Input() value = 0;
    @Output() valueChange: EventEmitter<number> = new EventEmitter();

    public tapAdd() {
        this.valueChange.emit(++ this.value);
    }
}
1234567891011121314151617

使用

html
 
<app-switch [(ngModel)]="value"></app-switch>
1

但这种方式是不支持 formControlName 绑定的

会提示No value accessor for form control with name: 错误

修改代码

因此必须换种方式

ts
                                           
@Component({
    selector: 'app-switch',
    template: `
        <div (click)="tapAdd()">{{ value }}</div>
    `,
    styleUrls: ['./switch.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => SwitchComponent),
            multi: true
        }
    ]
})
export class SwitchComponent implements ControlValueAccessor { 

    public value = 0;
    public disable = false;

    onChange: any = () => { };
    onTouch: any = () => { };

    public tapAdd() {
        if (this.disable) {
            return;
        }
        this.onChange(++ this.value);
    }

    writeValue(obj: any): void {
        this.value = obj;
    }
    registerOnChange(fn: any): void {
        this.onChange = fn;
    }
    registerOnTouched(fn: any): void {
        this.onTouch = fn;
    }
    setDisabledState?(isDisabled: boolean): void {
        this.disable = isDisabled;
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243

现在可以就支持两种方式了

html
   
<app-switch [(ngModel)]="value"></app-switch>

<app-switch formControlName="value"></app-switch>
123

例子

Example with Input

Example with Lazy Loaded Input

Example with Button

基于不同形式的json响应处理

2020-09-10 18:20:45

我的json格式

普通json响应

这是普通链接响应json格式,不论是否出错,响应状态码都是 200

json
        
{
    "code": 200, // 200 表示程序处理成功,其他数字自定义
    "status": "success", // success 表示成功 failure 表示失败
    "message": "",      // 成功的消息提示
    "errors": "",     // 失败的信息
    "data": {},       // 成功响应的信息
    "url": "",         // 搭配 code: 302 实现页面跳转
}
12345678

具体实例

成功返回用户信息

json
        
{
    "code": 200,
    "status": "success",
    "data": {
        "id": 1,
        "name": "zodream"
    },
}
12345678

失败返回提示无权限需要登录

json
      
{
    "code": 302,
    "status": "failure",
    "errors": "当前账户无权限,请先登录",
    "url": "/login?redirect_uri=/"
}
123456

RESTful 响应

状态码判断是否处理成功

具体实例

成功返回用户信息

HTTP/1.1 200 OK

json
    
{
    "id": 1,
    "name": "zodream"
}
1234

成功返回用户列表

HTTP/1.1 200 OK

json
        
{
    "data": [
        {
            "id": 1,
            "name": "zodream"
        }
    ]
}
12345678

失败返回提示无权限需要登录

HTTP/1.1 302 Found

json
    
{
    "code": 302,
    "message": "当前账户无权限,请先登录",
}
1234

具体代码实现,

netcore 版本为例

c#
              
    public interface IJsonResponse
    {
        public object Render(object data);

        public object RenderData(object data);

        public object RenderData(object data, string message);

        public object RenderPage<T>(Page<T> page);

        public object RenderFailure(string message, int code);

        public object RenderFailure(string message);
    }
1234567891011121314

普通版本的实现

c#
                                                             
    public class JsonResponse : IJsonResponse
    {
        public object Render(object data)
        {
            return data;
        }

        public object RenderData(object data)
        {
            return Render(new
            {
                code = 200,
                status = "success",
                data
            });
        }

        public object RenderData(object data, string message)
        {
            return Render(new
            {
                code = 200,
                status = "success",
                data,
                message
            });
        }

        public object RenderFailure(string message, int code)
        {
            return new
            {
                code,
                status = "failure",
                message
            };
        }

        public object RenderFailure(string message)
        {
            return RenderFailure(message, 404);
        }

        public object RenderPage<T>(Page<T> page)
        {

            return Render(new
            {
                code = 200,
                status = "success",
                data = page.Items,
                paging = new
                {
                    limit = page.ItemsPerPage,
                    offset = page.CurrentPage,
                    total = page.TotalItems,
                    more = page.CurrentPage < page.TotalPages
                }
            });
        }
    }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061

RESTful 的实现

c#
                                                          
    public class PlatformResponse : IJsonResponse
    {

        public PlatformModel Platform { get; set; }

        public object Render(object data)
        {
            return data;
        }

        public object RenderData(object data)
        {
            return Render(new
            {
                data,
                appid = Platform.Appid
            });
        }

        public object RenderData(object data, string message)
        {
            return Render(new
            {
                data,
                message
            });
        }

        public object RenderFailure(string message, int code)
        {
            return new
            {
                code,
                message
            };
        }

        public object RenderFailure(string message)
        {
            return RenderFailure(message, 404);
        }

        public object RenderPage<T>(Page<T> page)
        {

            return Render(new
            {
                data = page.Items,
                paging = new
                {
                    limit = page.ItemsPerPage,
                    offset = page.CurrentPage,
                    total = page.TotalItems,
                    more = page.CurrentPage < page.TotalPages
                }
            });
        }
    }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758

通过中间件注入当前请求

c#
     
public Task InvokeAsync(HttpContext context)
{
    context.Items.Add("json", new JsonResponse());
    return _next.Invoke(context);
}
12345

再控制器中使用

c#
 
HttpContext.Items["json"] as IJsonResponse
1

在这里有一个问题,即状态码并没有实现,可以考虑拓展方法,把HttpContext当作参数传入,内部做状态码输入

flutter CupertinoPicker 使用不显示

2020-09-03 06:50:17

CupertinoPicker是一个ios风格的齿轮滚动的选择器

dart
           
CupertinoPicker.builder(
    itemExtent: 40,
    scrollController: FixedExtentScrollController(initialItem: 0),
    backgroundColor: Colors.white,
    onSelectedItemChanged: (index) {
        // 选择
    },
    itemBuilder: (context, index) =>
        // items[index] 返回一个部件显示每一项的内容,
    childCount: items.length,
)
1234567891011

注意事项

在使用 CupertinoPicker 写地区选择器时,发现数据显示不出来,但使用 r 更新就能正常显示。

查找原因:

  1. 数据分为三列:即省、市、区,
  2. 数据都是网络请求的,因此第一次初始化的时候,数据长度是零。

刚开始,以为是没有setState(() {}); 更新,但多次设定也没用。

然后发现是因为 CupertinoPicker 接受数据长度为零的数据就没办法正常显示。

因此通过判断直接把数据长度为零的不生成CupertinoPicker,具体原因不知。

最终解决方案:

dart
                                             
  List<List<Region>> regionItems = [];
  List<int> regionIndex = [];
  List<FixedExtentScrollController> regionController = [];

  Widget buildPickers() {
    var items = <Widget>[];
    for (var i = 0; i < regionController.length; i++) {
        // 判断是数据的长度,小于1的那一列就不显示
      if (regionItems[i].length < 1) {
        continue;
      }
      items.add(Expanded(
        child: CupertinoPicker.builder(
          itemExtent: 40,
          scrollController: regionController[i],
          backgroundColor: Colors.white,
          onSelectedItemChanged: (index) {
            changeRegion(i, index);
          },
          itemBuilder: (context, index) =>
              buildTextItem(regionItems[i][index].name),
          childCount: regionItems[i].length,
        ),
      ));
    }

    return Expanded(
      child: Row(
        children: items,
      ),
    );
  }

  Widget buildTextItem(String text) {
    return Container(
      color: Colors.white,
      alignment: Alignment.center,
      padding: const EdgeInsets.symmetric(horizontal: 5.0),
      child: Text(
        text,
        textAlign: TextAlign.center,
        style: const TextStyle(fontSize: 14.0),
      ),
    );
  }
123456789101112131415161718192021222324252627282930313233343536373839404142434445

CC协议

2020-08-26 00:20:36

CC协议即版权协议

主要包括五个方面

  1. 版权
  2. 署名(BY):必须注明来源为创作者
  3. 非商业性运用(NC):非商业性目的
  4. 制止演绎(ND):不允许任何衍生物或作品的改编
  5. 相同方法同享(SA):必须按照相同的条款共享

主要六种不同的许可证类型

  1. CC BY:本许可证允许再使用者以任何媒介或格式分发、混音、改编和建立在材料上,但必须注明来源为创作者。该许可证允许商业使用。
  2. CC BY-SA:本许可证允许再使用者以任何媒介或格式分发、混音、改编和建立在材料上,但必须注明来源为创作者。该许可证允许商业使用。如果您对该材料进行再混合、改编或建立,您必须按照相同的条款对修改后的材料进行许可。
  3. CC BY-NC:本许可证允许再使用者以任何媒介或格式发布、混音、改编和建立在材料基础上的非商业性目的,但前提是必须归功于创作者。
  4. CC BY-NC-SA:本许可证允许再使用者以任何媒介或格式发布、混音、改编和建立在材料基础上的非商业性目的,但前提是必须归功于创作者。如果您对该材料进行再混合、改编或建立,您必须根据相同的条款对修改后的材料进行许可。
  5. CC BY-ND:该许可证允许再使用者以任何媒介或格式复制和分发未经改编的材料,但前提是必须注明出处给创作者。该许可证允许商业使用。
  6. CC BY-NC-ND:该许可证允许再使用者以任何媒介或格式复制和分发该材料,但仅限于非商业目的,且必须注明出处给创作者。

还有一种共享创意公共领域协议

  1. CC0:该许可证是一种公共奉献工具,它允许创作者放弃自己的版权,并将其作品放到全球公共领域。CC0允许再使用者以任何媒介或格式无条件地分发、混音、改编和建立在材料上。

参考

  1. About CC Licenses

flutter margin 负值实现

2020-08-22 04:48:41

本身 Container 上的 margin 是不能设为负值

例如

dart
   
Container(
    margin: EdgeInsets.only(top: -10),
)
123

这样会报错 Failed assertion: line 251: 'margin == null || margin.isNonNegative': is not true.

但可以通过 transform 属性实现负值效果

dart
   
Container(
    transform: Matrix4.translationValues(0, -10, 0),
)
123

win10添加删除开机自启项

2020-08-21 18:09:15

常见形式

  1. 开始菜单启动文件夹
  2. 任务计划程序
  3. 注册表

启动文件夹

win + R 进入 运行 程序,输入 shell:Startup 打开文件夹

添加

直接创建程序的快捷方式

删除

删除快捷方式即可

任务计划程序

使用任务栏的搜索,搜索 任务计划程序

添加

点击右侧的创建任务

新建 触发器 选择 开始任务 启动时

新建 操作 选择要启动的程序

删除

可以点击右侧的结束禁用删除

注册表

win + R 输入 regedit 进入注册表

常见的注册表键值有如下几项

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce

添加

新建 字符串值 输入名称,输入路径(路径请用英文双引号包起来)

在数值数据的最后加上 /background 可以实现后台自启

删除

删除项即可

Wallpager Engine 删除记录

2020-08-04 23:39:44

普通删除

直接选中,右键取消订阅即可,

但是没办法删除其他机器的订阅记录。因为有些记录是不出现在 已安装 列表里的(个人猜测是已下架的壁纸)

导致每一次安装都需要下载大量的文件。

针对这种记录,就需要用特别的方法。

根据文件标题去搜索中心安装取消订阅

按道理是可以的,

但是我没有试过,因为有很多壁纸都搜不到了

这种方法也很麻烦

让这些壁纸出现在已安装列表里即可取消订阅

首先,必须清楚每一个壁纸有一个特定的 编号,删除也是根据这个编号进行删除的

实际上每一个壁纸的文件夹名就是这个编号,只要把这个编号出现在列表里,就能取消订阅了

实际操作方法:

  1. 获取编号
  2. 找到配置文件(实际是在wallpager 的安装路径下的 bin\workshopcache.json 文件)
  3. 打开文件,复制一项,改掉 workshopid 为编号即可,最好 title 也改成编号,不然认不出
  4. 打开Wallpager Engine,选中取消订阅即可
  5. 重启Wallpager Engine,就删除成功了

提供一个脚本批量生成

python
                                                                           
import os
import json

def get_all_dir(dir):
    '''
    :brief: 获取所有的文件夹名
    :param dir: string 父文件夹路径
    :return: string[] 子文件夹名
    '''
    for _, dirs, _ in os.walk(dir):
        return dirs

def get_all_workshopid(dir):
    '''
    :brief:获取所有项目的workshopid
    :param dir: string 父文件夹路径
    :return: string[] id列表
    '''
    data = []
    dirs = get_all_dir(dir)
    for item in dirs:
        data.extend(get_all_dir(dir + r'\\' + item))
    return data

def create_config(items, sample):
    '''
    :brief:根据示例生成新的项
    :param items: string[] workshopid数组
    :param sample: dict 
    :return: dict[] 
    '''
    data = []
    for item in items:
        new_item = sample.copy()
        old = new_item['workshopid']
        for (k, v) in new_item.items():
            if k == 'title' or k == 'workshopid':
                new_item[k] = item
            elif isinstance(v, str):
                new_item[k] = v.replace(old, item)
        data.append(new_item)
    return data

# steam 的安装路径
steam_dir = r'D:\Steam\\'
# steam下载的文件路径
workshop_dir = steam_dir + r'steamapps\workshop\content'
# wallpager 的配置文件路径
wallpager_config_file = steam_dir + r'steamapps\common\wallpaper_engine\bin\workshopcache.json'

id_items = get_all_workshopid(workshop_dir)

print('总文件个数:%d' %(len(id_items)))

with open(wallpager_config_file, 'r+', encoding='utf-8') as fs:
    content = fs.read()
    res = json.loads(content)
    if len(res['wallpapers']) < 1:
        print('请先订阅一个作为示例')
        exit(0)
    sample = res['wallpapers'][0]
    exist_id = []
    for item in res['wallpapers']:
        exist_id.append(item['workshopid'])
    data = list(set(id_items).difference(set(exist_id)))
    if len(data) < 1:
        print('没有缓存的文件')
        exit(0)
    new_items = create_config(data, sample)
    res['wallpapers'].extend(new_items)
    content = json.dumps(res, indent=4, ensure_ascii=False)
    fs.seek(0)
    fs.write(content)
    print('成功生成缓存配置文件,可以进行取消订阅')
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475

全选或框选批量取消订阅即可

注意改掉里面的路径 steam_dir 必须先订阅一个壁纸

本来是想找到接口,通过接口删除的,但能力有限,没找到方法

angular10教程之http 拦截器

2020-08-04 00:46:55

请求拦截器

登录令牌注入

token.interceptor.ts

ts
                        
@Injectable()
export class TokenInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const auth = this.injector.get(AuthService);
    const clonedRequest = request.clone({
      headers: auth.getTokenHeader(request),
      url: this.fixUrl(request.url),
      params: request.params
    });
    return next.handle(clonedRequest);
  }

  private fixUrl(url: string) {
    if (url.indexOf('http://') >= 0 || url.indexOf('https://') >= 0) {
      return url;
    }
    return environment.apiEndpoint + url;
  }
}

123456789101112131415161718192021222324

响应拦截器

例如登录令牌失效

reponse.interceptor.ts

ts
                   
@Injectable()
export class ResponseInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(req).pipe(catchError((event: HttpEvent<any>) => {
      if (event instanceof HttpErrorResponse) {
        if (event.status === 401) {
          const auth = this.injector.get(AuthService);
          auth.logoutUser();
        }
      }
      return throwError(event);
    }));

  }
}
12345678910111213141516171819

使用

ts
             
@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },   // 使用请求拦截器
    { provide: HTTP_INTERCEPTORS, useClass: ResponseInterceptor, multi: true }, // 使用响应拦截器
  ],
})
export class ThemeModule {
  static forRoot(): ModuleWithProviders<ThemeModule> {
    return {
      ngModule: ThemeModule
    };
  }
}
12345678910111213

AuthService

ts
                                                      
const USER_KEY = 'user';

@Injectable()
export class AuthService {

  constructor(
    private http: HttpClient,
    @Inject(PLATFORM_ID) private platformId: any) {}

  /**
   * 清除本地的token
   */
  public logoutUser() {
    if (isPlatformBrowser(this.platformId)) {
      localStorage.clear();
    }
    this.store.dispatch(this.actions.logoutSuccess());
  }

  /**
   * 生成请求头
   * @returns HttpHeaders
   */
  getTokenHeader(request: HttpRequest<any>): HttpHeaders {
    if (this.getUserToken()) {
      return new HttpHeaders({
        'Content-Type': 'application/vnd.api+json',
        Authorization: `Bearer ${this.getUserToken()}`,
        Accept: '*/*'
      });
    }
    return new HttpHeaders({
      'Content-Type': 'application/vnd.api+json',
      Accept: '*/*'
    });
  }

  private setTokenInLocalStorage(user: any, keyName: string): void {
    const jsonData = JSON.stringify(user);
    if (isPlatformBrowser(this.platformId)) {
      localStorage.setItem(keyName, jsonData);
    }
  }

  public getUserToken() {
    if (isPlatformBrowser(this.platformId)) {
      const user: IUser = JSON.parse(localStorage.getItem(USER_KEY));
      return user ? user.token : null;
    } else {
      return null;
    }
  }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354

dpl 文件

2020-08-02 05:46:56

解释:.dpl 主要为Potplayer直播源列表文件

文件格式

文件头

    
DAUMPLAYLIST
playname=
topindex=0
saveplaypos=0
1234

playname 为当前播放的地址

topindex 为当前播放的序号,即在列表中的位置

列表项

  
1*file*地址
1*title*名称
12

序号从 1 开始

地址行为 序号 + *file* + 地址

标题行为 序号 + *title* + 地址

txt 文件转 dpl

txt 文件每一行格式为

 
标题,地址
1
  1. 获取文件编码
  2. 移动到正文开始的位置
  3. 写入文件头
  4. 循环取每一行,转换并写入
c#
 
Converter(saveFile, sourcefile);
1

具体转换代码

c#
                                                                                                                                          
        const string HEADER_TAG = "DAUMPLAYLIST";

        const string FILE_TAG = "*file*";
        const string TITLE_TAG = "*title*";

        const string NEW_LINE = "\n";

        public static readonly string[] Headers = new string[3]{
            "playname=", // 当前播放的网址
            "topindex=0", // 当前播放的序号
            "saveplaypos=0"
        };

        /// <summary>
        /// 转换文件
        /// </summary>
        /// <param name="dist"></param>
        /// <param name="source"></param>
        public static void Converter(string dist, string source)
        {
            using (var sourceStream = new FileStream(source, FileMode.Open))
            {
                using (var distStream = new FileStream(dist, FileMode.Create))
                {
                    Converter(distStream, sourceStream);
                }
            }
        }

        /// <summary>
        /// 转换文件
        /// </summary>
        /// <param name="dist"></param>
        /// <param name="source"></param>
        public static void Converter(FileStream dist, FileStream source)
        {
            Reset(source);
            var encoder = new TxtEncoder();
            var encoding = encoder.GetEncoding(source);
            source.Seek(encoder.Position, SeekOrigin.Begin);
            var line = ReadLine(source, encoding);
            if (line.Trim() == HEADER_TAG)
            {
                Reset(source);
                Copy(dist, source);
                return;
            }
            source.Seek(encoder.Position, SeekOrigin.Begin);
            Write(dist, HEADER_TAG);
            Write(dist, NEW_LINE);
            foreach (var item in Headers)
            {
                Write(dist, item);
                Write(dist, NEW_LINE);
            }
            var i = 1;
            while (null != (line = ReadLine(source, encoding)))
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
                var args = line.Split(',');
                if (args.Length < 2)
                {
                    continue;
                }
                Write(dist, i.ToString());
                Write(dist, FILE_TAG);
                Write(dist, args[1]);
                Write(dist, NEW_LINE);
                Write(dist, i.ToString());
                Write(dist, TITLE_TAG);
                Write(dist, args[0]);
                Write(dist, NEW_LINE);
                i++;
            }
        }

        /// <summary>
        /// 移动指针到开始位置
        /// </summary>
        /// <param name="source"></param>
        private static void Reset(Stream source)
        {
            source.Seek(0, SeekOrigin.Begin);
        }

        /// <summary>
        /// 复制流
        /// </summary>
        /// <param name="dist"></param>
        /// <param name="source"></param>
        private static void Copy(FileStream dist, FileStream source)
        {
            source.CopyTo(dist);
        }

        /// <summary>
        /// 读取一行
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        private static string ReadLine(FileStream source, Encoding encoding)
        {
            var bytes = new List<byte>();
            int code;
            bool hasByte = false;
            while ((code = source.ReadByte()) != -1)
            {
                hasByte = true;
                if (code == 0x0a/* \n */ || code == 0x0d /* \r */)
                {
                    break;
                }
                bytes.Add(Convert.ToByte(code));
            }
            if (!hasByte)
            {
                return null;
            }
            if (bytes.Count  < 1)
            {
                return "";
            }
            return encoding.GetString(bytes.ToArray());
        }

        /// <summary>
        /// 写入字符
        /// </summary>
        /// <param name="dist"></param>
        /// <param name="line"></param>
        private static void Write(FileStream dist, string line)
        {
            var bytes = Encoding.UTF8.GetBytes(line);
            dist.Write(bytes, 0, bytes.Length);
        }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138

获取编码

c#
                                                                                                                      
/// <summary>   
/// 用于取得一个文本文件的编码方式(Encoding)。   
/// </summary>   
public class TxtEncoder
{
    /// <summary>
    /// 正文开始的位置
    /// </summary>
    public int Position { get; private set; } = 0;
    /// <summary>   
    /// 取得一个文本文件的编码方式。如果无法在文件头部找到有效的前导符,Encoding.Default将被返回。   
    /// </summary>   
    /// <param name="fileName">文件名。</param>   
    /// <returns></returns>   
    public Encoding GetEncoding(string fileName)
    {
        return GetEncoding(fileName, Encoding.Default);
    }
    /// <summary>   
    /// 取得一个文本文件流的编码方式。   
    /// </summary>   
    /// <param name="stream">文本文件流。</param>   
    /// <returns></returns>   
    public Encoding GetEncoding(FileStream stream)
    {
        return GetEncoding(stream, Encoding.Default);
    }
    /// <summary>   
    /// 取得一个文本文件的编码方式。   
    /// </summary>   
    /// <param name="fileName">文件名。</param>   
    /// <param name="defaultEncoding">默认编码方式。当该方法无法从文件的头部取得有效的前导符时,将返回该编码方式。</param>   
    /// <returns></returns>   
    public Encoding GetEncoding(string fileName, Encoding defaultEncoding)
    {
        var fs = new FileStream(fileName, FileMode.Open);
        var targetEncoding = GetEncoding(fs, defaultEncoding);
        fs.Close();
        return targetEncoding;
    }

    /// <summary>   
    /// 取得一个文本文件流的编码方式。   
    /// </summary>   
    /// <param name="stream">文本文件流。</param>   
    /// <param name="defaultEncoding">默认编码方式。当该方法无法从文件的头部取得有效的前导符时,将返回该编码方式。</param>   
    /// <returns></returns>   
    public Encoding GetEncoding(FileStream stream, Encoding defaultEncoding)
    {
        var targetEncoding = defaultEncoding;
        if (stream == null || stream.Length < 2) return targetEncoding;
        //保存文件流的前4个字节   
        byte byte3 = 0;
        //保存当前Seek位置   
        var origPos = stream.Seek(0, SeekOrigin.Begin);
        stream.Seek(0, SeekOrigin.Begin);

        var nByte = stream.ReadByte();
        var byte1 = Convert.ToByte(nByte);
        var byte2 = Convert.ToByte(stream.ReadByte());
        if (stream.Length >= 3)
        {
            byte3 = Convert.ToByte(stream.ReadByte());
        }
        //根据文件流的前4个字节判断Encoding   
        //Unicode {0xFF, 0xFE};   
        //BE-Unicode {0xFE, 0xFF};   
        //UTF8 = {0xEF, 0xBB, 0xBF};   
        if (byte1 == 0xFE && byte2 == 0xFF)//UnicodeBe   
        {
            targetEncoding = Encoding.BigEndianUnicode;
            Position = 2;
        }
        else if (byte1 == 0xFF && byte2 == 0xFE && byte3 != 0xFF)//Unicode   
        {
            targetEncoding = Encoding.Unicode;
            Position = 3;
        }
        else if (byte1 == 0xEF && byte2 == 0xBB && byte3 == 0xBF) //UTF8   
        {
            targetEncoding = Encoding.UTF8;
            Position = 3;
        }
        else
        {
            stream.Seek(0, SeekOrigin.Begin);
            int read;
            while ((read = stream.ReadByte()) != -1)
            {
                if (read >= 0xF0)
                    break;
                if (0x80 <= read && read <= 0xBF)
                    break;
                if (0xC0 <= read && read <= 0xDF)
                {
                    read = stream.ReadByte();
                    if (0x80 <= read && read <= 0xBF)
                        continue;
                    break;
                }
                if (0xE0 > read || read > 0xEF) continue;
                read = stream.ReadByte();
                if (0x80 <= read && read <= 0xBF)
                {
                    read = stream.ReadByte();
                    if (0x80 <= read && read <= 0xBF)
                    {
                        targetEncoding = Encoding.UTF8;
                    }
                }
                break;
            }
        }
        //恢复Seek位置         
        stream.Seek(origPos, SeekOrigin.Begin);
        return targetEncoding;
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118

微信小程序开发记录(一)真机无法进入页面

2020-08-01 04:42:32

这是一个以前开发的正常上线项目,今天要更新一下,突然出问题了

具体症状

在微信开发者工具中是正常的,使用真机模拟时,发现一直加载中,而且加载的圆已全,不动了,在控制台也没有任何错误输出,在首页的js文件中 onLoad onShow 方法都正常执行了。

解决

先看是不是代码问题,无论删除多少代码都不行,

在看是不是项目配置版本的问题,改到最新也不行,

最后,查到 app.json 这个文件,删除一些配置,试试,才发现是底部导航栏配置的问题,

在以前,是允许导航栏名称为空的,即以下写法是没有问题的

json
    
{
    "pagePath": "page/index/index",
    "text": ""
}
1234

但是现在不行了,必须有字符,需改成

json
    
{
    "pagePath": "page/index/index",
    "text": " "
}
1234

flutter 跳转页面操作上一页

2020-07-30 00:48:01

跳转到登录页,登录成功后返回上一页,并更新上一页

主要通过 Navigator.pop(context, args); 第二个参数就是要传的值

那么该怎么接受呢

dart
   
Navigator.pushNamed(context, LOGIN_PATH).then((args) {
                      // TODO
              });
123

通过 then 即可接受传过来的值,然后根据值判断是否登录成功等

Regex Generator 使用指南

2020-07-26 01:02:11

操作指南

  1. 输入要匹配的内容
  2. 输入正则表达式
  3. 点击按钮开始匹配,结果会出现在右下角列表里
  4. 输入模板语句
  5. 双击匹配结果任意一项,弹出组合的结果
  6. 点击复制即可
  7. 左上角有个灯泡图标,点击查看规则

模板生成语法

1. {} 直接输出 默认为 0, 可以 int   可以用 . 连接使用第二级的内容  
2. for 语句
    {for} 或 {~} 开始标志 

    for 无参数时,表示循环输出全部

    for(n) 一个参数时,表示从0 开始 共输出 n 个

    for(m,) 第二个参数缺省时,表示从m 开始,输出后面所有的

    for(m,n) 两个参数时,表示从 m 开始,共输出 n 个

循环体 {} 只能是 int 或 标签 ,不再支持 . 连接,暂不支持 for 循环
{end} 或 {!} 结束标志

3. 整数循环
    {1 2 ... 7}  开始标志

    {1 ... 7}  从1循环到7 即输出 1 2 3 4 5 6 7

    {1,3 ... 7} 从1循环到7 每个数隔2 , 即 1 3 5 7

    {16:1 2 ... 15} 从1循环到15的十六进制 即输出 1 2 ... F


{}           取值标志

{end} 或者 {!} 结束标志

4. {m~n} 输出 m到n 的随机数

5. 新增驼峰与下划线转换
    {studly:}    “ ”、“_”、“-” 为分割符,包括首字母转大写成驼峰写法例如 aa_aa 转成 AaAa

    {lstudly:}   首字母为小写的驼峰写法 例如 aa_aa 转成 aaAa

    {unstudly:}  驼峰写法转下划线 例如 AaAa 转成 aa_aa

go init函数

2020-07-24 01:32:57

介绍

init 函数用于包的初始化

init 函数执行在 main 之前

一个包 可以有多个 init 函数

同一个包 的 init 函数执行顺序无明确定义,不同包的init函数是根据包导入的依赖关系决定的

执行

使用 import 就会执行 init

如果仅执行包的 init 函数,不导入其他函数,使用 import _ "package"

通过init仅执行一次

go
         
import "sync"

var once sync.Once

func init() {
    once.Do(func() {
        // TODO
    })
}
123456789

angular 9 升级 angular 10

2020-07-24 00:23:55

单例服务 Singleton services

ts
       
export class ThemeModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: ThemeModule
    };
  }
}
1234567

修改为

ts
     
static forRoot(): ModuleWithProviders<ThemeModule> {
    return {
        ngModule: ThemeModule
    };
}
12345

CommonJS or AMD dependencies can cause optimization bailouts warning

解决办法:

配置 CommonJS 依赖项

修改 angular.json

projects > 项目名 > architect > build > options

添加 allowedCommonJsDependencies 填入依赖名

json
      
"options": {
    "allowedCommonJsDependencies": [
        "lodash",
        "jsonapi-deserializer"
    ],
}
123456

kotlin AndroidManifest 注意事项

2020-07-22 04:28:18

  1. android:value 为数字则无法通过字符串获取

例如:

xml
 
<meta-data android:name="XXX" android:value="11543906547"/>
1

获取

kotlin
    
packageManager.getApplicationInfo(
            packageName,
            PackageManager.GET_META_DATA
        ).metaData?.getString("XXX");
1234

返回的值为 null, 因为 Bundle 自动识别为数字

正确的写法是修改 AndroidManifest,前加上 \ (反斜杠和空格)即可

xml
 
<meta-data android:name="XXX" android:value="\ 11543906547"/>
1

对于zodream 框架的优化的思考

2020-07-22 04:27:26

php 与其他最大的不同是不支持多进程,本身也就没法实现web程序,需要搭配其他服务软件。

因此每一个访问就是一个全新的进程,没法实现一个内容的共享,造成内存浪费,使得并发数少。

优化方向

路由

目前是实时寻找路由,加载多个文件,时间较长

考虑解决方向

使用路由缓存,加载一个文件即可执行最终控制方法。

可以使用 redis,已最终路径为键,保存。

关于路由重写的,可以在生成时做好映射

配置

目前处于每次都得加载三个文件并进行多维数组合并,考虑进行合并成一个文件。

数据库

查询语句考虑抛弃实时拼接方法

终极方法

使用配置文件,配置路由,配置路由需要的参数,配置路由需要的依赖项,直接就访问控制器方法。最大程度减少加载文件。数据库访问全部使用 redis ,同步到mysql通过其他方式,数据可以共享的都存到 redis 中。

flutter 页面滚动条

2020-07-14 21:51:17

GridView、ListView

嵌套使用需要在子列表添加

  
shrinkWrap:true,    // 处理列表嵌套报错
physics: NeverScrollableScrollPhysics(),      // 处理子列表中滑动父级列表无法滑动
12

CustomScrollView

子项可以是列表或其他部件

如果是列表则使用 SliverListSliverFixedExtentListSliverGrid

如果是其他部件则必须使用 SliverToBoxAdapter

SliverListSliverFixedExtentList 的区别,如果知道高度 则推荐用 SliverFixedExtentList

例如

标题栏

dart
              
SliverFixedExtentList(
    itemExtent: 50,                             // 是设置每一个子元素的高度
    delegate: SliverChildListDelegate(
        <Widget>[
            Container(
                color: Colors.white,
                height: 50,
                child: Center(
                    child: Text(title),
                ),
            )
        ],
    ),
)
1234567891011121314

两列商品类别

dart
             
SliverGrid(
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 1.0,
        crossAxisSpacing: 1.0,
        childAspectRatio: 0.7,
    ),
    delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
        return ProductItem(
            item: data[index],
        );
    }, childCount: data.length),
),
12345678910111213

flutter swiper 使用

2020-07-14 19:03:58

添加项目依赖

 
flutter_swiper : any
1

使用

dart
           
Container(
    width: MediaQuery.of(context).size.width,
    height: 200.0,
    child: Swiper(
        itemBuilder: (context, index) =>
            CachedNetworkImage(imageUrl: banners[index].content),
        itemCount: banners.length,
        autoplay: true,
        loop: true,
    ),
)
1234567891011

注意

必须放到 一个父元素了,不能直接放到 <Widget>[] 中作为一个子元素,原因是必须指定尺寸,即宽和高

flutter API请求

2020-07-14 19:03:17

dio 的使用

响应

虽然获取到的数据可以自动进行json解析,但是获取的数据不会自动进行类转换

dart
  
dio.request<T>()
12

T 只能指定为 dynamicMap,无法作为其他类进行转换,例如定义了一个类

dart
                 
@JsonSerializable()
class Site extends Object {
  String name;
  String version;
  String logo;
  int goods;
  int category;
  int brand;
  String currency;

  Site(this.name, this.version, this.logo, this.goods, this.category,
      this.brand, this.currency);

  factory Site.fromJson(Map<String, dynamic> json) => _$SiteFromJson(json);

  Map<String, dynamic> toJson() => _$SiteToJson(this);
}
1234567891011121314151617

而实际服务器响应的是json

json
         
{
"name": "zodream",
"version": "0.1",
"logo": "https://zodream.cn/assets/upload/image/wap_logo.png",
"category": 6,
"brand": 1,
"goods": 133,
"currency": "¥"
}
123456789

使用 dio.request<Site>() 是会报错的,

只能手动转换

dart
   
var response = dio.request<Map<String, dynamic>>();

return Site.fromJson(response.data);
123

flutter 自定义 AppBar

2020-07-14 19:02:31

实现

dart
                              
import 'package:flutter/material.dart';

class SearchAppBar extends StatefulWidget implements PreferredSizeWidget {
  final Widget child;           //从外部指定内容
  final Color backgroundColor;  // 背景颜色
  SearchAppBar({this.child, this.backgroundColor});

  @override
  _SearchAppBarState createState() => _SearchAppBarState();

  @override
  Size get preferredSize => new Size.fromHeight(kToolbarHeight);
}

class _SearchAppBarState extends State<SearchAppBar> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: kToolbarHeight + MediaQuery.of(context).padding.top,
      width: MediaQuery.of(context).size.width,
      color: widget.backgroundColor,
      child: SafeArea(
        top: true,
        bottom: false,
        child: widget.child,
      ),
    );
  }
}
123456789101112131415161718192021222324252627282930

使用

dart
                              
SearchAppBar appBar() {
    return SearchAppBar(
      backgroundColor: Color(0xFF05A6B1),
      child: Row(
        children: <Widget>[
          CachedNetworkImage(
            imageUrl: site.logo,
            width: 100,
          ),
          Expanded(
            child: Container(
              child: Row(
                children: <Widget>[
                  Icon(Icons.search),
                  Text('搜索商品, 共${site.goods}款好物')
                ],
              ),
              decoration: BoxDecoration(
                  color: Color(0xFFededed),
                  borderRadius: BorderRadius.all(const Radius.circular(2))),
            ),
          ),
          Container(
            child: IconButton(icon: Icon(Icons.message), onPressed: () {}),
            width: 54,
          )
        ],
      ),
    );
  }
123456789101112131415161718192021222324252627282930

MediaQuery

MediaQuery.of(context) 获取当前设备的信息

属性 说明
size 逻辑像素,并不是物理像素,类似于Android中的dp,逻辑像素会在不同大小的手机上显示的大小基本一样,物理像素 = size*devicePixelRatio
devicePixelRatio 单位逻辑像素的物理像素数量,即设备像素比。
textScaleFactor 单位逻辑像素字体像素数,如果设置为1.5则比指定的字体大50%。
platformBrightness 当前设备的亮度模式,比如在Android Pie手机上进入省电模式,所有的App将会使用深色(dark)模式绘制。
viewInsets 被系统遮挡的部分,通常指键盘,弹出键盘,viewInsets.bottom表示键盘的高度。
padding 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏。
viewPadding 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏,此值独立于paddingviewInsets,它们的值从MediaQuery控件边界的边缘开始测量。在移动设备上,通常是全屏。
systemGestureInsets 显示屏边缘上系统“消耗”的区域输入事件,并阻止将这些事件传递给应用。比如在Android Q手势滑动用于页面导航(ios也一样),比如左滑退出当前页面。
physicalDepth 设备的最大深度,类似于三维空间的Z轴。
alwaysUse24HourFormat 是否是24小时制。
accessibleNavigation 用户是否使用诸如TalkBackVoiceOver之类的辅助功能与应用程序进行交互,用于帮助视力有障碍的人进行使用。
invertColors 是否支持颜色反转。
highContrast 用户是否要求前景与背景之间的对比度高, iOS上,方法是通过“设置”->“辅助功能”->“增加对比度”。 此标志仅在运行iOS 13的iOS设备上更新或以上。
disableAnimations 平台是否要求尽可能禁用或减少动画。
boldText 平台是否要求使用粗体。
orientation 是横屏还是竖屏。

参考

  1. Flutter 强大的MediaQuery控件

flutter 主题配置

2020-07-14 19:00:07

ThemeData

dart
                                                                   
ThemeData({
    Brightness brightness, 
    VisualDensity visualDensity, 
    MaterialColor primarySwatch,               //备用主题颜色,如果没有设定primaryColor就使用该颜色
    Color primaryColor,                         //主题主色,决定导航栏颜色
    Brightness primaryColorBrightness, 
    Color primaryColorLight, 
    Color primaryColorDark, 
    Color accentColor,                          //主题次级色,决定大多数Widget的颜色,如进度条、开关等。
    Brightness accentColorBrightness, 
    Color canvasColor, 
    Color scaffoldBackgroundColor, 
    Color bottomAppBarColor, 
    Color cardColor,                             //卡片颜色
    Color dividerColor,                          //分割线颜色
    Color focusColor, 
    Color hoverColor, 
    Color highlightColor, 
    Color splashColor, 
    InteractiveInkFeatureFactory splashFactory, 
    Color selectedRowColor, 
    Color unselectedWidgetColor, 
    Color disabledColor, 
    Color buttonColor, 
    ButtonThemeData buttonTheme,                     //按钮主题
    ToggleButtonsThemeData toggleButtonsTheme, 
    Color secondaryHeaderColor, 
    Color textSelectionColor, 
    Color cursorColor, 
    Color textSelectionHandleColor, 
    Color backgroundColor, 
    Color dialogBackgroundColor,                       //对话框背景颜色
    Color indicatorColor, 
    Color hintColor, 
    Color errorColor, 
    Color toggleableActiveColor, 
    String fontFamily, 
    TextTheme textTheme,                              // 字体主题,包括标题、body等文字样式
    TextTheme primaryTextTheme, 
    TextTheme accentTextTheme, 
    InputDecorationTheme inputDecorationTheme, 
    IconThemeData iconTheme, 
    IconThemeData primaryIconTheme, 
    IconThemeData accentIconTheme, 
    SliderThemeData sliderTheme, 
    TabBarTheme tabBarTheme, 
    TooltipThemeData tooltipTheme, 
    CardTheme cardTheme, 
    ChipThemeData chipTheme, 
    TargetPlatform platform, 
    MaterialTapTargetSize materialTapTargetSize, 
    bool applyElevationOverlayColor, 
    PageTransitionsTheme pageTransitionsTheme, 
    AppBarTheme appBarTheme, 
    BottomAppBarTheme bottomAppBarTheme, 
    ColorScheme colorScheme, 
    DialogTheme dialogTheme, 
    FloatingActionButtonThemeData floatingActionButtonTheme, 
    NavigationRailThemeData navigationRailTheme, 
    Typography typography, 
    CupertinoThemeData cupertinoOverrideTheme, 
    SnackBarThemeData snackBarTheme, 
    BottomSheetThemeData bottomSheetTheme, 
    PopupMenuThemeData popupMenuTheme, 
    MaterialBannerThemeData bannerTheme, 
    DividerThemeData dividerTheme, 
    ButtonBarThemeData buttonBarTheme})
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667

使用

dart
 
Theme.of(context).primaryColor
1

配置

main.dart

dart
                            
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Shop',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
        secondaryHeaderColor: Color(0x05a6b1),
        indicatorColor: Color(0xFFB4282D),
        backgroundColor: Color(0xF4F4F4),
      ),
      home: IndexPage(),
    );
  }
}
12345678910111213141516171819202122232425262728

WPF 使用 WebView2

2020-07-03 06:00:23

安装SDK

必须安装 Microsoft Edge (Chromium) Canary channel 浏览器

NuGet 安装 Microsoft.Web.WebView2 预发行版 0.9.538-prerelease,正式版0.9.538安装是没用的

使用

xml
   
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"

<wv2:WebView2 x:Name="Browser" Source="https://www.baidu.com"/>
123

相关事件

SourceChanged 可以获取当前的网址,可以更新网页通过 js 修改的网址

NavigationStarting 网页加载开始

NavigationCompleted 网页加载完成

CoreWebView2Ready CoreWebView2 初始化完成,可以进行 CoreWebView2 上的事件绑定

CoreWebView2.NewWindowRequested 加载新标签之前触发,可以通过 e.Handled = true; 阻止触发,e.Uri 为要跳转的网址

代码跳转

c#
 
Browser.Source = new Uri(url);
1

获取源代码

主要通过 ExecuteScriptAsync 方法执行 js 代码,返回的值都是 JSON 过的。如果 JSON 编码失败则返回 null

c#
        
using Newtonsoft.Json;

var html = await Browser.ExecuteScriptAsync("document.getElementsByTagName('html')[0].innerHTML");
if (string.IsNullOrWhiteSpace(html))
{
    return html;
}
return "<!DOCTYPE html><html>" + JsonConvert.DeserializeObject(html) + "</html>";
12345678

获取 cookie

c#
                                                                        
using System;
using System.ComponentModel;
using System.Net;
using System.Security;
using System.Security.Permissions;
using System.Text;

/// <summary></summary>  
/// 取得WebBrowser的完整Cookie。  
/// 因为默认的webBrowser1.Document.Cookie取不到HttpOnly的Cookie  
///   
public class FullWebBrowserCookie
{

    [SecurityCritical]
    public static string GetCookieInternal(Uri uri, bool throwIfNoCookie)
    {
        uint pchCookieData = 0;
        var url = UriToString(uri);
        const uint flag = (uint)NativeMethods.InternetFlags.INTERNET_COOKIE_HTTPONLY;

        //Gets the size of the string builder  
        if (NativeMethods.InternetGetCookieEx(url, null, null, ref pchCookieData, flag, IntPtr.Zero))
        {
            pchCookieData++;
            var cookieData = new StringBuilder((int)pchCookieData);

            //Read the cookie  
            if (NativeMethods.InternetGetCookieEx(url, null, cookieData, ref pchCookieData, flag, IntPtr.Zero))
            {
                DemandWebPermission(uri);
                return cookieData.ToString();
            }
        }

        var lastErrorCode = 0;

        if (throwIfNoCookie || (lastErrorCode != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS))
        {
            throw new Win32Exception(lastErrorCode);
        }

        return null;
    }

    private static void DemandWebPermission(Uri uri)
    {
        var uriString = UriToString(uri);

        if (uri.IsFile)
        {
            var localPath = uri.LocalPath;
            new FileIOPermission(FileIOPermissionAccess.Read, localPath).Demand();
        }
        else
        {
            new WebPermission(NetworkAccess.Connect, uriString).Demand();
        }
    }

    private static string UriToString(Uri uri)
    {
        if (uri == null)
        {
            throw new ArgumentNullException(nameof(uri));
        }

        UriComponents components = (uri.IsAbsoluteUri ? UriComponents.AbsoluteUri : UriComponents.SerializationInfoString);
        return new StringBuilder(uri.GetComponents(components, UriFormat.SafeUnescaped), 2083).ToString();
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
c#
                                
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;

internal sealed class NativeMethods
{
    #region enums  

    public enum ErrorFlags
    {
        ERROR_INSUFFICIENT_BUFFER = 122,
        ERROR_INVALID_PARAMETER = 87,
        ERROR_NO_MORE_ITEMS = 259
    }

    public enum InternetFlags
    {
        INTERNET_COOKIE_HTTPONLY = 8192, //Requires IE 8 or higher  
        INTERNET_COOKIE_THIRD_PARTY = 131072,
        INTERNET_FLAG_RESTRICTED_ZONE = 16
    }

    #endregion

    #region DLL Imports  

    [SuppressUnmanagedCodeSecurity, SecurityCritical, DllImport("wininet.dll", EntryPoint = "InternetGetCookieExW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
    internal static extern bool InternetGetCookieEx([In] string Url, [In] string cookieName, [Out] StringBuilder cookieData, [In, Out] ref uint pchCookieData, uint flags, IntPtr reserved);

    #endregion
}
1234567891011121314151617181920212223242526272829303132

使用获取cookie

c#
 
FullWebBrowserCookie.GetCookieInternal(Browser.Source, false)
1

网站体检之xss攻击

2020-06-26 19:51:10

今天突然想到,虽然sql 注入不了(因为所有前台参数是使用pdo 自带的过滤,比起自己造的过滤器好多了),但是xss 攻击还是有可能的

注册表单检查

用户名注入

在用户名填入以下字符,

html
 
<script>alert(1);</script>
1

注册成功后,立马显示弹出框,可以注入

解决:直接对用户名保存进行转换

forum发表主题标题可以xss

标题可以 xss 攻击

主要原因:面包屑没有做转换

code标签可以xss

解决:对标签保存前进行转换

MicroBlog 内容可以xss

解决:在保存前进行转换

Blog 可以xss

  1. 标题及简介都可以在显示时转换
  2. 内容在markdown 模式下

调用方法即可

php
 
(new Parsedown())->setSafeMode(true)
1

GitHub 下载慢怎么办

2020-06-26 19:50:01

提高加载速度

码云

码云来作为中转站,但是码云对仓库大小有一定限制,所以太大的仓库用这个方法不行

使用cnpmjs镜像

只需要修改你的路径 github.comgithub.com.cnpmjs.org

但有时必须使用 github 源才行

使用 git clone 时常终止

这时可以使用 github 客户端,虽然对下载速度没有帮助,但是可以保证下载不会中断

小程序商城开发总结

2020-06-18 20:56:03

服务端使用:php

前端使用 ts

bug 修复

  1. 购物删除商品导致无法选中

原因:在服务端使用了 unset() 移除数组的项导致索引数组变成了关联数组,传到前端变成了对象就没法使用 for 循环了

解决:使用 array_values() 再转成索引数组即可

  1. 订单列表删除根据id 移除失败

原因:使用了 === 判断,但两个id 一个是 string 一个是 number 判断失败

解决:改为 == 即可

  1. 选择地区导致收货人空白

原因:收货人并没有进行保存,而在更新地区时使用了 this.setData(this.data) 导致收货人也更新了

解决:要么进行收货人输入保存,要么使用 this.setData({region}),只更新一个

gulp-vue2mini 更新日志

2020-06-13 00:37:48

2020-06-28

版本: 1.1.0

  1. 新增 html 模板开发
  2. 新增 内置ts sass 编译,可以不用gulp
 
vue2mini
1
--watch   监听脚本变动
--input   源码文件或文件夹,默认 `src` 文件夹
--output  目标保存文件夹,默认 `dist` 文件夹
--mini    编译小程序,默认为 编译模板

新增模板html生成(与其他功能不共用)

生效条件 以 @ 开头,前面可以有空格,功能占整行, @ 后面的内容以空格隔开,空格之后为评论内容,不做生成

@@             为注释,放在首行为声明此文件不生成html页面
@...           加载其他引用此文件的文件内容
@layout/main   加载其他文件,拓展名可以省略
@item@5        加载其他文件重复5次

加载的样式及样式文件自动合并到 head 末尾

加载的脚本及脚本文件自动合并到 body 末尾

支持 ts sass 自动编译

  
vue2mini --watch
12

2020-06-12

版本: 1.0.5

  1. 修复因html标签内部换行导致的解析错误
  2. 增加对 @click.stop 的转化
  3. 增加页面直接对方法传递参数的原始参数。即 @click="tap(1)" 允许接收 function tap(i, e: TouchEvent){}
  4. 删除一些不必要的 text 标签生成,例如 a 不再内部生成 text 子标签
  5. 修复 v-show 解析问题
  6. 调整对 vue 模板的解析流程,直接内嵌 rename 文件后缀

最新使用方法

js
               
gulp.task('vue', async() => {
    await gulp.src(getSrcPath('src/**/*.vue'))
        .pipe(template('tpl'))
        .pipe(gulp.dest(getDistFolder('dist/')))
        .pipe(template('json'))
        .pipe(gulp.dest(getDistFolder('dist/')))
        .pipe(template('scss'))
        .pipe(template('presass'))
        .pipe(sass())
        .pipe(template('endsass'))
        .pipe(gulp.dest(getDistFolder('dist/')))
        .pipe(template('ts'))
        .pipe(ts.createProject('tsconfig.json'))
        .pipe(gulp.dest(getDistFolder('dist/')));
});
123456789101112131415

c# 图片处理及ico 生成

2020-06-11 01:02:59

根据路径获取图片

c#
 
var bmp = new Bitmap(fileName);
1

更改图片尺寸

c#
                                   
Image img;

var bmp = new Bitmap(width, height);
bmp.SetResolution(source.HorizontalResolution, source.VerticalResolution);
using (var g = Graphics.FromImage(bmp))
{
    g.CompositingMode = CompositingMode.SourceCopy;
    switch (quality)
    {
        case SmoothingMode.AntiAlias:
            g.CompositingQuality = CompositingQuality.HighQuality;
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            g.PixelOffsetMode = PixelOffsetMode.HighQuality;
            g.SmoothingMode = SmoothingMode.AntiAlias;
            break;
        case SmoothingMode.HighQuality:
            g.CompositingQuality = CompositingQuality.HighQuality;
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            g.PixelOffsetMode = PixelOffsetMode.HighQuality;
            g.SmoothingMode = SmoothingMode.HighQuality;
            break;
        case SmoothingMode.HighSpeed:
            g.CompositingQuality = CompositingQuality.HighSpeed;
            g.InterpolationMode = InterpolationMode.NearestNeighbor;
            g.PixelOffsetMode = PixelOffsetMode.HighSpeed;
            g.SmoothingMode = SmoothingMode.HighSpeed;
            break;
    }
    using (var ia = new ImageAttributes())
    {
        ia.SetWrapMode(WrapMode.TileFlipXY);
        g.DrawImage(source, new System.Drawing.Rectangle(0, 0, width, height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, ia);
    }
}
return bmp;
1234567891011121314151617181920212223242526272829303132333435

png 转 ico

ico 图标是由不同尺寸的png 图片组成。因此可以设置不同尺寸下不同的图。

生成不同尺寸的图

c#
                                           
/// <summary>
/// 修改图片尺寸
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="source"></param>
/// <returns></returns>
public static Bitmap ResizeImage(int width, int height, Image source, SmoothingMode quality)
{
    var bmp = new Bitmap(width, height);
    bmp.SetResolution(source.HorizontalResolution, source.VerticalResolution);
    using (var g = Graphics.FromImage(bmp))
    {
        g.CompositingMode = CompositingMode.SourceCopy;
        switch (quality)
        {
            case SmoothingMode.AntiAlias:
                g.CompositingQuality = CompositingQuality.HighQuality;
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                g.SmoothingMode = SmoothingMode.AntiAlias;
                break;
            case SmoothingMode.HighQuality:
                g.CompositingQuality = CompositingQuality.HighQuality;
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                g.SmoothingMode = SmoothingMode.HighQuality;
                break;
            case SmoothingMode.HighSpeed:
                g.CompositingQuality = CompositingQuality.HighSpeed;
                g.InterpolationMode = InterpolationMode.NearestNeighbor;
                g.PixelOffsetMode = PixelOffsetMode.HighSpeed;
                g.SmoothingMode = SmoothingMode.HighSpeed;
                break;
        }
        using (var ia = new ImageAttributes())
        {
            ia.SetWrapMode(WrapMode.TileFlipXY);
            g.DrawImage(source, new System.Drawing.Rectangle(0, 0, width, height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, ia);
        }
    }
    return bmp;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243

ico 头

c#
           
/// <summary>
/// 写头
/// </summary>
/// <param name="count"></param>
/// <param name="writer"></param>
private static void CreateHeader(int count, BinaryWriter writer)
{
    writer.Write((ushort)0);
    writer.Write((ushort)1);
    writer.Write((ushort)count); // 
}
1234567891011

获取图片数据的长度,及每一个像素,每一个像素又包括 RGBA

c#
    
private static int GetImageSize(Image image)
{
    return image.Height * image.Width * 4;
}
1234

写入每张图的位置

c#
                
 private static void CreateDirectory(int offset, Image image, int imageLength, BinaryWriter writer)
{
    var size = imageLength + 40;
    var width = image.Width >= 256 ? 0 : image.Width;
    var height = image.Height >= 256 ? 0 : image.Height;
    var bpp = Image.GetPixelFormatSize(image.PixelFormat);

    writer.Write((byte)width);
    writer.Write((byte)height);
    writer.Write((byte)0);
    writer.Write((byte)0);
    writer.Write((ushort)1);
    writer.Write((ushort)bpp);
    writer.Write((uint)size);
    writer.Write((uint)offset);
}
12345678910111213141516

写入每张图的数据

c#
                             
private static void CreateBitmap(Image image, int compression, int imageLength, BinaryWriter writer)
{
    writer.Write((uint)40);
    writer.Write((uint)image.Width);
    writer.Write((uint)image.Height * 2);
    writer.Write((ushort)1);
    writer.Write((ushort)Image.GetPixelFormatSize(image.PixelFormat));
    writer.Write((uint)compression);
    writer.Write((uint)imageLength);
    writer.Write(0);
    writer.Write(0);
    writer.Write((uint)0);
    writer.Write((uint)0);
}

private static void CreateDib(Image image, BinaryWriter writer)
{
    for (int i = 0; i < image.Height; i++)
    {
        for (int j = 0; j < image.Width; j++)
        {
            var color = (image as Bitmap).GetPixel(j, i);
            writer.Write(color.B);
            writer.Write(color.G);
            writer.Write(color.R);
            writer.Write(color.A);
        }
    }
}
1234567891011121314151617181920212223242526272829

生成ico

c#
              
IEnumerable<Image> images;
Stream stream;
var bw = new BinaryWriter(stream);
CreateHeader(images.Count(), bw);
var offset = 6 + (16 * images.Count());
foreach (var item in images) {
    var length = GetImageSize(item);
    CreateDirectory(offset, item, length, bw);
}
foreach (var item in images) {
    CreateBitmap(item, colorMode, GetImageSize(item), bw);
    CreateDib(item, bw);
}
bw.Dispose();
1234567891011121314

UWP TextBlock 换行

2020-06-09 06:26:04

开发时,遇到 文字太长未换行,记录一下

手动换行

通过代码设置内容。默认支持 \n 换行

c#
 
TextBlock1.Text = "a\nb";
1

c#
   
TextBlock1.Inlines.Add(new Run("a"));
TextBlock1.Inlines.Add(new LineBreak());
TextBlock1.Inlines.Add(new Run("b"));
123

xaml 上通过转义字符
<LineBreak/> 换行

xml
 
<TextBlock Text="a
b"/>
1
xml
     
<TextBlock>
    <Run>a</Run>
    <LineBreak/>
    <Run>b</Run>
</TextBlock>
12345

备注

  空格 	 Tab 
 回车 
 换行

自动换行

设置属性 TextWrapping="Wrap"

xml
 
<TextBlock TextWrapping="Wrap"/>
1

js 数组排序 sort

2020-06-08 07:03:02

使用方法

ts
         

const items = [3, 1, 12];

const res = items.sort();
// items [1, 12, 3]
res.push(4);

// items [1, 12, 3, 4]
123456789

注意

  1. sort 会改变自身的排序,
  2. 并返回自身
  3. 默认排序是按字符串升序进行排序,不分数字数组

数字按大小升序

ts
      
const items = [3, 1, 12];
items.sort((a: number, b: number) => {
    return a - b;
});

// items [1, 3, 12]
123456

数字按大小降序

ts
    
const items = [3, 1, 12];
items.sort((a: number, b: number) => {
    return b - a;
});
1234

抛弃create.js 改用pixi.js

2020-06-08 05:41:34

今天在 github 看到推荐了 pixi.js,就行进行了了解。

对比

create.js pixi.js
已不更新了 至今更新
js编写的 ts编写的,更喜欢ts
基于canvas 支持canvas和WebGL
支持Adobe Animate图形工具 无图形工具
文档有 文档有(不是最新的)
压缩后235kb 365kb
包含tween 不包含

总结:保持更新及使用 ts 为最重要的选择原因

简单例子

pixie.js

官网

文档

中文文档

 
npm install pixi.js
1

这是官方的例子

ts
                                 
import * as PIXI from 'pixi.js';

// The application will create a renderer using WebGL, if possible,
// with a fallback to a canvas render. It will also setup the ticker
// and the root stage PIXI.Container
const app = new PIXI.Application();

// The application will create a canvas element for you that you
// can then insert into the DOM
document.body.appendChild(app.view);

// load the texture we need
app.loader.add('bunny', 'bunny.png').load((loader, resources) => {
    // This creates a texture from a 'bunny.png' image
    const bunny = new PIXI.Sprite(resources.bunny.texture);

    // Setup the position of the bunny
    bunny.x = app.renderer.width / 2;
    bunny.y = app.renderer.height / 2;

    // Rotate around the center
    bunny.anchor.x = 0.5;
    bunny.anchor.y = 0.5;

    // Add the bunny to the scene we are building
    app.stage.addChild(bunny);

    // Listen for frame updates
    app.ticker.add(() => {
         // each frame we spin the bunny around a bit
        bunny.rotation += 0.01;
    });
});
123456789101112131415161718192021222324252627282930313233

插件

周边插件也挺丰富的,动画(spine/dragonbones),粒子系统,物理引擎等等

相关页面

后台列表页进阶之路

2020-06-07 05:46:19

PS: 这是无聊时,嫌弃列表页不好看,之后的改良之路,这只是代表个人审美。

第一版:基本功能

包括:

  1. 搜索
  2. 列表
  3. 分页

从图上可以看出基本功能都存在了,但是感觉不美观,也空洞。

改进1

从搜索框开始

新增 按钮移动右边

有铺满的感觉了

改进2

增加页面提示

页面提示,也可以作为操作指南。

解决了搜索顶格的不适,让搜索不至于处在视野边缘。

改进3

移动分页到中间,

居中,大屏幕下,视野中间,容易找

列表项改进

抛弃传统 table,自适应方面差及屏幕展示的信息少(不是指那种有横向滚动的,而且横向滚动条很烦)

这样主要信息都能查看,而且自适应方便,主题也鲜明了

局域网文件传输工具

2020-06-07 05:45:47

一个用于局域网传输文件的小工具

源码

File-Pass

程序

功能介绍

简单的局域网内部文件传送。

  1. 支持文件夹及多文件添加。
  2. 支持多文件同时传送,但默认为单进程
  3. 支持自动获取本地ip
  4. 支持自动获取其他设备ip

缺陷

  1. 局域网内部所有ip获取有问题,并不能获取所有ip
  2. 文件无具体进度显示

使用方法

  1. 接收方

点击 监听 按钮即可

点击监听接收界面

接收文件页面

  1. 发送方

选择或填写 目标IP(即接收方ip)

点击 选择文件 按钮选择文件夹即可

发送文件页面

angular9教程之PullToRefresh

2020-06-02 19:03:15

使用

html
            
<app-pull-to-refresh class="items-box" (refreshChange)="tapRefresh()" (moreChange)="tapMore()" [more]="hasMore" [loading]="isLoading">
    <ng-container *ngFor="let item of items">
        <dl class="book-item" (click)="tapItem(item)">
            <dt>
                <a >{{ item.title }}</a>
                <span class="book-time">{{ item.created_at }}</span></dt>
                <dd>
                    <p>{{ item.description }}</p></span>
            </dd>
        </dl>
    </ng-container>
</app-pull-to-refresh>
123456789101112

refreshChange 下拉刷新事件

moreChange 加载更多事件

more 是否有更多

loading 是否加载中

distance 触底距离

ts
                                 
  @ViewChild(PullToRefreshComponent)
  public pullBox: PullToRefreshComponent;

    public tapRefresh() {
    this.goPage(1);
  }

  public tapMore() {
    if (!this.hasMore) {
        return;
    }
    this.goPage(this.page + 1);
  }

  public goPage(page: number) {
    if (this.isLoading) {
        return;
    }
    this.isLoading = true;
    this.pullBox?.startLoad();
    this.service.getPage({
      page
    }).subscribe(res => {
      this.page = page;
      this.hasMore = res.paging.more;
      this.isLoading = false;
      this.items = page < 2 ? res.data : [].concat(this.items, res.data);
      this.pullBox?.endLoad();
    }, () => {
      this.isLoading = false;
      this.pullBox?.endLoad();
    });
  }
123456789101112131415161718192021222324252627282930313233

推荐使用方法通知,虽然 loading 也可以,但是如果加载没有时间间隔的话,是不起作用的。

startLoad 指定开始加载

endLoad 指定加载完成

监听滚动事件

ts
                
@HostListener('scroll', [
    '$event.target.scrollTop',
    '$event.target.scrollHeight',
    '$event.target.offsetHeight',
  ])
  public onDivScroll(
    scrollY: number,
    scrollheight: number,
    offsetHeight: number,
  ): void {
    const height = scrollheight;
    const y = scrollY + offsetHeight;
    if (this.more && y + this.distance > height) {
      // 触发加载更多
    }
  }
12345678910111213141516

缺点

第一次不能自动加载,如果有更多,但没有滚动条,也不会自动加载更多

具体代码

请查看【GITHUB

c# 添加防火墙例外端口

2020-05-29 06:21:47

必须添加 NetFwTypeLib ,在 引用 > 添加引用 > COM

ts
                                    
/// <summary>
/// 添加防火墙例外端口
/// </summary>
/// <param name="name">名称</param>
/// <param name="port">端口</param>
/// <param name="protocol">协议(TCP、UDP)</param>
public static void NetFwAddPorts(string name, int port, string protocol)
{
    INetFwMgr netFwMgr = (INetFwMgr)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwMgr"));

    INetFwOpenPort objPort = (INetFwOpenPort)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwOpenPort"));

    objPort.Name = name;
    objPort.Port = port;
    if (protocol.ToUpper() == "TCP")
    {
        objPort.Protocol = NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_TCP;
    }
    else
    {
        objPort.Protocol = NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_UDP;
    }
    objPort.Scope = NET_FW_SCOPE_.NET_FW_SCOPE_ALL;
    objPort.Enabled = true;

    bool exist = false;
    foreach (INetFwOpenPort mPort in netFwMgr.LocalPolicy.CurrentProfile.GloballyOpenPorts)
    {
        if (objPort == mPort)
        {
            exist = true;
            break;
        }
    }
    if (!exist) netFwMgr.LocalPolicy.CurrentProfile.GloballyOpenPorts.Add(objPort);
}
123456789101112131415161718192021222324252627282930313233343536

c# 获取局域网ip

2020-05-29 06:21:21

获取本机局域网ip

AddressList必须倒序查找

c#
                            
/// <summary>
/// 获取本机IP地址
/// </summary>
/// <returns>本机IP地址</returns>
public static string GetLocalIP()
{
    try
    {
        var hostName = Dns.GetHostName(); //得到主机名
        var ipEntry = Dns.GetHostEntry(HostName);
        var ips = ipEntry.AddressList;
        for (int i = ips.Length - 1; i >= 0; i--)
        {
            //从IP地址列表中筛选出IPv4类型的IP地址
            //AddressFamily.InterNetwork表示此IP为IPv4,
            //AddressFamily.InterNetworkV6表示此地址为IPv6类型
            if (ips[i].AddressFamily == AddressFamily.InterNetwork)
            {
                return ips[i].ToString();
            }
        }
        return "";
    }
    catch (Exception ex)
    {
        return '';
    }
}
12345678910111213141516171819202122232425262728

获取局域网所有ip

这里用的是 ping 方法 ping 255 个地址

ts
                              

private List<string> ipItems = new List<string>();

/// <summary>
/// 获取局域网的其他ip
/// </summary>
/// <param name="baseIp"></param>
/// <param name="exsits"></param>
private void LoadAllIp(string baseIp, string exsits)
{
    for (int i = 1; i <= 255; i++)
    {
        var ip = baseIp + i;
        if (ip == exsits)
        {
            continue;
        }
        var ping = new Ping();
        ping.PingCompleted += Ping_PingCompleted;
        ping.SendAsync(ip, 2000, null);
    }
}

private void Ping_PingCompleted(object sender, PingCompletedEventArgs e)
{
    if (e.Reply.Status == IPStatus.Success)
    {
        ipItems.Add(e.Reply.Address.ToString());
    }
}
123456789101112131415161718192021222324252627282930

获取的时间为 2-3 秒,获取的也不一定准确

angular9教程之自定义部件及获取页面元素

2020-05-28 06:20:47

组件接受传值

使用 @Input() 定义

ts
          
@Component({
    selector: 'app-page-tip',
    template: './page-tip.component.html',
    styleUrls: ['./page-tip.component.scss']
})
export class PageTipComponent {

    @Input() public title = '提示';

}
12345678910

使用

html
 
<app-page-tip [title]="'标题'"></app-page-tip>
1

组件事件通知

通过 @Output() 定义事件,使用 .emit() 触发事件通知

ts
     
@Output() public valueChange = new EventEmitter();

public tap() {
    this.valueChange.emit();
}
12345

使用

html
 
<app-page-tip (valueChange)="tapValue()"></app-page-tip>
1

双向绑定

使用 [()] 进行双向绑定,那么应该修改事件,保证 事件名为 参数名 + Change

例如

ts
                
@Component({
  selector: 'app-page-tip',
  template: './page-tip.component.html',
  styleUrls: ['./page-tip.component.scss']
})
export class PageTipComponent {

    @Input() public title = '提示';


    @Output() public titleChange = new EventEmitter();

    public tap() {
        this.titleChange.emit();
    }
}
12345678910111213141516

使用

html
 
<app-page-tip [(title)]="title"></app-page-tip>
1

获取页面元素

例如

html
  
<div>12312</div>
<app-page-tip [(title)]="title"></app-page-tip>
12

获取 div,必须在div 上加 #+命名

html
 
<div #app>12312</div>
1

然后通过 @ViewChild('命名') 获取

ts
  
    @ViewChild('app')
    private box: ElementRef;
12

这是 box.nativeElement 就是 HTMLDivElement,可以想普通元素一样操作。

如果要绑定事件,那么请注意先等页面已经生成,不然获取不到。

ngAfterViewInit 就是在页面初始化后触发。

ts
            
export class HomeComponent implements AfterViewInit {

  @ViewChild('app')
  private box: ElementRef;

  ngAfterViewInit(): void {
    const div = this.box.nativeElement as HTMLDivElement;
    div.addEventListener('click', (event) => {
        // TODO
    });
  }
}
123456789101112

获取页面上的自定义组件

例如

html
 
<app-page-tip [(title)]="title"></app-page-tip>
1

通过 @ViewChild(组件类名) 获取

ts
  
    @ViewChild(PageTipComponent)
    private box: PageTipComponent;
12

angular9教程之表单验证及确认密码验证

2020-05-27 03:01:13

FormGroup 的使用

这是一个注册表单,

ts
                       
export class RegisterComponent {
    public registerForm = this.fb.group({
        name: ['', Validators.required],
        email: ['', [Validators.required, Validators.email]],
        password: ['', [Validators.required, passwordValidator]],
        confirm_password: ['', [Validators.required]],
        agree: [false, Validators.requiredTrue]
    }, {
        validators: confirmValidator()
    });

    constructor(
        private fb: FormBuilder
    ) { }

    get email() {
        return this.registerForm.get('email');
    }

    get password() {
        return this.registerForm.get('password');
    }
}
1234567891011121314151617181920212223

通过 [formGroup] 绑定表单数据源

通过 formControlName 绑定表单项

html
                                  
<form class="form-ico login-form" [formGroup]="registerForm" (ngSubmit)="tapSignUp()">
  <div class="input-group">
        <input type="text" formControlName="name" placeholder="请输入昵称" required="">
        <i class="iconfont icon-user" aria-hidden="true"></i>
    </div>
    <div class="input-group" [class]="{error: email.invalid}">
        <input type="email" formControlName="email" placeholder="请输入邮箱" required="">
        <i class="iconfont icon-at" aria-hidden="true"></i>
    </div>
    <div class="input-group" [class]="{error: password.invalid}">
        <input type="password" formControlName="password" placeholder="请输入密码" required="">
        <i class="iconfont icon-lock" aria-hidden="true"></i>
    </div>
    <div class="input-group" [class]="{error: registerForm.errors && registerForm.errors.confirm}">
        <input type="password" formControlName="confirm_password" placeholder="请确认密码" required="">
        <i class="iconfont icon-check" aria-hidden="true"></i>
    </div>

    <div class="input-group">
        <div class="checkbox">
            <input type="checkbox" formControlName="agree" value="1" id="checkboxInput">
            <label for="checkboxInput"></label>
        </div>
        同意《
        <a href="https://zodream.cn/agreement">本站协议</a>
        》
    </div>

    <button type="submit" class="btn" [disabled]="registerForm.invalid">注册</button>
    <div class="other-box">
        <a routerLink="../">返回登录</a>
    </div>

</form>
12345678910111213141516171819202122232425262728293031323334

获取错误信息

AbstractControl.invalid 获取是否有误

AbstractControl.errors. 获取具体错误信息,注意 .errors 有可能为 undefined 需要先判断

在这里,自定义了两个验证器:

密码验证

  1. passwordValidator 可以验证密码的复杂程度。
ts
     
export const passwordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
  return control.value && control.value.length < 6 ? {
    password_simple: true
  } : null;
};
12345

确认密码验证

  1. confirmValidator 可以验证确认密码是否一致
ts
       
export const confirmValidator = (key: string = 'password', confirmKey: string = 'confirm_password'): ValidatorFn => {
  return (control: FormGroup): ValidationErrors | null => {
    return control.get(key).value !== control.get(confirmKey).value ? {
      confirm : true
    } : null;
  };
};
1234567

1). 接受两个参数,为需要比较的两个字段名,

2). 必须放在 FormGroup 上,不然获取不到两个值。

3). 通过 FormGroup.errors && FormGroup.errors.confirm 获取有误信息

netcore 依赖注入 AddTransient、AddScoped、AddSingleton

2020-05-25 05:19:07

使用

Startup.cs

c#
        
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IDatabase>(x =>
    {
        return new Database(Configuration.GetConnectionString("Default"), DatabaseType.MySQL, MySql.Data.MySqlClient.MySqlClientFactory.Instance);
    });
    services.AddScoped(typeof(UserRepository));
}
12345678

区别

AddTransient 每次注入的都是新实例,相当于手动 new

AddScoped 每次请求,都获取一个新的实例,在一个请求中,获取多次会得到相同的实例。

AddSingleton 每次都获取同一个实例

createjs 实现封装 drawImage

2020-05-24 04:42:03

问题

createjs 中,默认是没有封装 canvas 的 drawImage 方法,

想 添加图片到 canvas 上,只能使用

ts
   
const img: HTMLImageElement;
const box = new createjs.Shape();
box.graphics.beginBitmapFill(img).drawRect(0, 0, img.width, img.height);
123

如果想移动图片的位置,只能使用 box.xbox.y

而改变 drawRect 上的值,就是在图片上裁剪。默认 img 是无限重复的,

.drawRect(10, 10, img.width, img.height) 并不是移动到 10,10 再开始画整张图,而是先从 0,0 画,横向重复一次,纵向再重复一次,这是成 字排列四张图,然后 选取 10,10img.width + 10, img.height + 10 矩形区域显示

那么,想从 10,10 开始画整张图该怎么办?

解决方法

canvas 默认是有 drawImage 方法,调用就行了

ts
                   
class ImgFill {
    constructor(
        private img: HTMLImageElement,
        private x: number,
        private y: number,
        private width: number,
        private height: number
    ) {

    }

    public exec(ctx: CanvasRenderingContext2D) {
        ctx.save();
        ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, this.x, this.y, this.width, this.height);
        ctx.restore();
    }
}

box.graphics.append(new ImgFill(img, 10, 10, img.width, img.height));
12345678910111213141516171819

实现一个封装的对象,exec 方法是这个对象必须有的,有这个方法,才能被调用,实现绘制。

笔记(200521)游戏及玩家分类

2020-05-22 01:58:39

本文为阅读 《平衡掌控者游戏数值战斗设计》做的笔记。

游戏分类

  1. 角色扮演

RPG

包括:动作角色扮演、模拟角色扮演、策略角色扮演、角色扮演冒险、恋爱角色扮演、角色扮演解谜

  1. 动作

ACT

包括:射击(STG)、格斗(FTG)、动作冒险、动作角色扮演(APRG)

  1. 冒险

AVG

包括:动作冒险、文字冒险、恋爱冒险

  1. 模拟

SIM 或 SLG

包括:策略模拟、模拟经营、模拟养成、战争、飞行、载具模拟

  1. 策略

包括:回合战略、回合战术、即时战略、即时战术、解谜

  1. 其他

包括:音乐、休闲、体育(SPT)、竞速(RAC)等

玩家分类

基于 MMORPG (大型多人在线角色扮演)

  1. 杀手型

通过虚拟世界发泄现实生活压力,包括语言攻击和虐杀对其他人造成心理或精神伤害而获取成就感。

  1. 成就型

以提升装备和等级、完成任务为目的。

  1. 探索型

包括审美型和学习型,游戏就是获取新素材分享出去。

  1. 社交型

游戏就是为了交朋友。

关于 MasterDetail 模式页面收集

2020-05-22 00:29:37

介绍

基本结构

  1. 左侧为列表,右边为内容
  2. 默认右侧无内容,显示空白或背景
  3. 小屏下,默认显示列表,点击列表项才只显示内容,返回回复列表

适用场景

以列表和详情为主的程序

  1. 邮件类,参考例子: windows 10 自带的邮件 app

  1. 新闻、博客类

  2. 消息聊天类

  1. 手机版界面转pc,这种感觉上还是不太适合,但这种模式更有感觉,界面不会太变形

实现思路

  1. 左右两边有明显分割;
  2. 左右宽度最好右边宽,右边才是主体;
  3. 右边可以是 iframe 这种内嵌,但是页面少的就不用了,直接放到一个页面就好了,设置这类页面可以直接右侧弹窗

怎么写日记(一)格式规范

2020-05-22 00:26:14

基本格式

必须内容

  1. 日期,基本上精确到天,年月日,年可以省略,连续记日记,在进入新的一年时,要写清楚是×年,之后便可以只写×月×日
  2. 星期几
  3. 天气
  4. 正文

可选内容

  1. 标题

示例

5月19日   星期二      天气晴
今天做了什么。

日记内容

  1. 记的事件要真实。

要多记自己看到或经历过的事和人,多记身边发生过的现实生活中的事和人。只有真实的最能反映事件和心情,真实地叙述问题,才有参考价值,才是有效的积累,才能反映规律。

  1. 记的事件内容要具体些,少用概括性叙述,有一定的描写。

写日记主要是练笔。它不同于一般的记录。因此,要通过仔细回忆和重新观察,把事件或人物的细节写出来。

  1. 要有选择地记。

一则日记一般只记一件事,不能太杂,不能拖泥带水地在一则日记中什么都级。一则日记要为荣一个中心。这个中心可以是一件事、一个场景、一段对话、一处风景、一个外貌、一种心情、一个动作等。切记不要流水账。

  1. 记事件的感受不要牵强附会。

每个人都会对身边发生的事产生一些感受,都会有自己的想法。在记叙过程中,可以穿插自己的感受。但这种感受一定要真实,自己是怎么想的就怎么写,不要老是考虑这个想法对不对。

关于 "简书" "提示浏览器版本过低" 探究

2020-05-20 19:12:09

事先声明

本文只是个人观点,不涉及任何利益之争。

情况

经测试,在手机上使用 edge 浏览器、火狐浏览器、谷歌浏览器,都会出现 您的xx手机浏览器版本过低,请立即免费升级。

垃圾广告既视感!

猜测

先来猜测一下,为什么会这么做?有哪些可能原因

  1. 为自己的APP 拉流量
  2. 垃圾广告注入,这是主动放的广告
  3. 被攻击了,这是被动放的广告

探究

点击弹窗的按钮 确定 进入一个网址页面

那么点击一下 取消 试试,弹出 您确定取消升级吗?确定 关闭弹窗,取消 进入下载页面

关键是,每次进入都有弹窗。

  1. 先找到弹窗脚本

使用电脑打开谷歌浏览器,切换到手机模拟模式,修改手机型号,苹果手机不会弹出,找个安卓的,Moto G4会弹窗。

找到工具 性能 页,进行录制,刷新页面,关闭弹窗,

停止录制,找到执行脚本

打开脚本 jian.js 发现里面加密了,

下载脚本到本地,根据据关键词 手机浏览器版本过低 可以搜索到,加密了就不继续了,

这个脚本文件是 jian.t58b.com 这个域名下的

按照简书被劫持了吗 说法:这是 简书的广告主

经过三重域名跳转,跳转到一个 APP 下载网站,

关于 最终下载的APP 就不知道是什么了,不敢安装

通过域名查询 发现

通过备案查询发现 t58b.com 为个人,而最终 跳转域名为 微型企业

到此为止,不想继续了

总结

做网站还是得注意选择合适的广告主,虽然要恰饭,但也不能随便恰,逮着狗屎啃,惹了一身骚,导致网站体验不好就不值得了。

选择广告也应该考虑用户的体验,影响用户体验,就不好了。

angular9教程之路由

2020-05-20 02:48:35

主要写在模块的 -routing.module.ts 文件里

ts
  
const routes: Routes = [
];
12

routes 就是放路由的。

基本写法

显示页面,例如: home 路径显示 HomeComponent 的内容

ts
      
const routes: Routes = [
    {
        path: 'home',
        component: HomeComponent,
    },
];
123456

未匹配到指定 未找到 页面

ts
      
const routes: Routes = [
    {
        path: '**',
        component: NotFoundComponent,
    },
];
123456

指定路由重定向到新的路由

ts
       
const routes: Routes = [
    {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
    },
];
1234567

指定路由到指定模块

ts
       
const routes: Routes = [
    {
        path: 'blog',
        loadChildren: () => import('./blog/blog.module').then(m => m.BlogModule)
    },
];
1234567

在模块中可以指定路由是否要加载模块公共模板

ts
                
const routes: Routes = [
    {
        path: '',
        component: FrontendComponent,
        children: [
            {
                path: 'home',
                component: HomeComponent,
            },
        ]
    },
    {
        path: 'blog',
        component: HomeComponent,
    },
]
12345678910111213141516

注意 frontend.component.html 必须有 <router-outlet></router-outlet> 才能加载子路由

这样 /home 就会先加载 FrontendComponent 再加载 HomeComponent 并把 HomeComponent 的内容 放在 frontend.component.html<router-outlet></router-outlet> 位置

/blog 就只会加载 HomeComponent,这样就可以实现模块内不同页面有不同的框架结构。

也可以实现 不同的页面分别由不同的公共模板

ts
                      
const routes: Routes = [
    {
        path: '',
        component: FrontendComponent,
        children: [
            {
                path: 'home',
                component: HomeComponent,
            },
        ]
    },
    {
        path: 'blog',
        component: BlogComponent,
        children: [
            {
                path: '',
                component: HomeComponent,
            },
        ]
    },
]
12345678910111213141516171819202122

PHP 常用框架

2020-05-20 02:02:50

php发展到今天为止有很多框架,一般开发一个项目都是先选一个熟悉的框架开始,各种PHP开发框架也让程序开发变的简单有效。每一个开发者都知道,拥有一个强大的框架可以让开发工作变得更加快捷、安全和有效。

对于框架的选择往往从几方面考虑:

  1. 文档,完整的文档。
  2. 使用难易程度。
  3. 配套功能,例如ORM、路由、模板解析。
  4. 活跃的社区。

Laravel

  1. Github 开源
  2. 文档全
  3. 有一定的难度
  4. 相对更快使用PHP新版本特性
  5. 有相关视频教程

对小项目没有加成。

官网:https://laravel.com/

Yii

一款快速、安全和专业的PHP框架。

本身提供一个可视化界面生成代码

官网:https://www.yiiframework.com/

CodeIgniter

一款非常敏捷的开源PHP框架。适合开发一个简单而优雅的工具包。

适合小型项目。

官网:https://www.codeigniter.com/

Zend Framework

顶尖的PHP框架,文档丰富

官网:https://framework.zend.com/

Symfony

是一款可重用的PHP组件,例如 laravel 就基于 Symfony 开发的

官网:https://symfony.com/

Swoole

PHP 协程框架,是一个面向生产环境的 PHP 异步网络通信引擎,使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务

目前,国内很多PHP招聘都需要了解swoole

官网:https://www.swoole.com/

Phalcon

运行速度最快的一个PHP框架。主要代码使用 C语言编写。所以配置安装相对麻烦,不一定能适配最新 php 版本(除非你能自己编译c语言编写的php拓展)。

官网:https://phalcon.io/

ThinkPHP

主要用户为国内开发者,作为国内初学者入门框架。

官网:http://www.thinkphp.cn/

css3 实现汉堡式菜单栏

2020-05-18 06:10:54

阅读本文需要了解 scss angular

核心代码

scss
        
.nav-bar {
    display:flex;
    flex-direction:column;
    .bar-top {
        overflow-y: auto;
        flex: 1;
    }
}
12345678

这样就实现中间菜单超长滚动,底部菜单自动增高

效果

代码

注意这个代码是使用在 angular 上的,没有做静态页面,可以考虑自己转化一下

使用的图标是 iconfont 图标

navToggle顶部汉堡按钮 控制菜单收缩用的

router-outletangular 用来加载右侧内容的标签

html
                                                                                                   
<div class="page-box" [ngClass]="{'nav-toggle': navToggle}">
    <div class="nav-bar">
        <i class="iconfont icon-bars nav-toggle-icon" (click)="navToggle = !navToggle"></i>
        <ul class="bar-top">
            <li class="bar-item active">
                <a routerLink="/disk">
                    <i class="iconfont icon-home"></i>
                    <span class="bar-name">首页</span>
                </a>
            </li>
            <li class="bar-item">
                <a routerLink="/disk/catalog">
                    <i class="iconfont icon-folder-open-o"></i>
                    <span class="bar-name">全部文件</span>
                </a>
                <ul class="bar-children">
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-file-image-o"></i>
                            <span class="bar-name">图片</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-file-word-o"></i>
                            <span class="bar-name">文档</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-file-movie-o"></i>
                            <span class="bar-name">视频</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-gift"></i>
                            <span class="bar-name">种子</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-music"></i>
                            <span class="bar-name">音乐</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-APP"></i>
                            <span class="bar-name">应用</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-file-archive-o"></i>
                            <span class="bar-name">压缩包</span>
                        </a>
                    </li>
                    <li class="bar-item">
                        <a routerLink="/disk/catalog">
                            <i class="iconfont icon-file-o"></i>
                            <span class="bar-name">其他</span>
                        </a>
                    </li>
                </ul>
            </li>
            <li class="bar-item">
                <a routerLink="/disk/share">
                    <i class="iconfont icon-share-alt"></i>
                    <span class="bar-name">我的分享</span>
                </a>
            </li>
            <li class="bar-item">
                <a routerLink="/disk/trash">
                    <i class="iconfont icon-trash"></i>
                    <span class="bar-name">回收站</span>
                </a>
            </li>
        </ul>
        <ul class="bar-bottom">
            <li class="bar-item">
                <a routerLink="/disk/my">
                    <i class="iconfont icon-user"></i>
                    <span class="bar-name">zodream</span>
                </a>
            </li>
            <li class="bar-item">
                <a routerLink="/disk/setting">
                    <i class="iconfont icon-cog"></i>
                    <span class="bar-name">设置</span>
                </a>
            </li>
        </ul>
    </div>

    <div class="page-body">
        <router-outlet></router-outlet>
    </div>
</div>
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899

下面是 css 样式

scrollbar() 这个是设置滚动条样式的,默认的太大了

scss
                                                                                                                                                               
@mixin scrollbar() {
    &::-webkit-scrollbar{
        height:6px;
        width:6px;
        margin-right:5px;
        background: #f5f5f5;
        transition:all 0.3s ease-in-out;
        border-radius:0px
    }
    &::-webkit-scrollbar-track { 
        -webkit-border-radius: 0px;
        border-radius: 0px;
    }
    &::-webkit-scrollbar-thumb{
        -webkit-border-radius: 0px;
        border-radius: 0px;
        background: rgba(0,0,0,0.5); 
        &:hover{
            background:rgba(0,0,0,0.6);
        }
        &:active{
            background:rgba(0,0,0,0.8);
        }
        &:window-inactive {
            background: rgba(0,0,0,0.4);
        }
    }
}

ul,
li {
    margin: 0;
    padding: 0;
}
.nav-bar {
    position: fixed;
    left: 0;
    width: 200px;
    bottom: 0;
    background-color: #eee;
    top: 0;
    z-index: 99;
    box-shadow: rgba(51,51,51,.7) 0 0 10px;
    display:flex;
    flex-direction:column;
    .nav-toggle-icon {
        font-size: 30px;
        padding: 0 10px;
        display: inline-block;
        &:hover {
            background-color: #ccc;
        }
    }
    .bar-top {
        overflow-y: auto;
        flex: 1;
        @include scrollbar();
    }
    a {
        text-decoration: none;
        color: #333;
    }
    .bar-item {
        list-style: none;
        line-height: 40px;
        text-align: center;
        font-size: 16px;
        a {
            display: block;
            box-sizing: border-box;
            position: relative;
        }
        .iconfont {
            position: absolute;
            left: 10px;
            top: 0;
            font-size: 30px;
            display: block;
        }
        &:hover {
            >a {
                background-color: #ccc;
            }
        }
    }
    .bar-children {
        box-shadow: inset 0 5px 5px -5px #000, inset 0 -5px 5px -5px #000;
    }
    .bar-item {
        &.active {
            >a {
                &::before {
                    content: " ";
                    display: block;
                    position: absolute;
                    left: 0;
                    width: 5px;
                    height: 40px;
                    background-color: red;
                }
            }
        }
    }
}

.page-body {
    margin-left: 200px;
}

.nav-toggle {
    .nav-bar {
        width: 50px;
        .bar-name {
            display: none;
        }
        .bar-item {
            .iconfont {
                position: static;
            }
        }

    }
    .page-body {
        margin-left: 50px;
    }
}

@media screen and (max-width: 769px) {
    .nav-bar {
        width: 50px;
        .bar-name {
            display: none;
        }
        .bar-item {
            .iconfont {
                position: static;
            }
        }

    }
    .page-body {
        margin-left: 50px;
    }

    .nav-toggle {
        .nav-bar {
            width: 200px;
            .bar-name {
                display: inline-block;
            }
            .bar-item {
                .iconfont {
                    position: absolute;
                }
            }

        }
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159

整体项目代码:Angular-ZoDream

当前项目未使用服务端数据对接,所以可以查看实际效果

angular9教程之监听子路由变化

2020-05-17 06:14:18

需求

根据下级路由改变父组件的菜单选中

解决方法

主要通过 this.router.events.subscribe 获取路由的变化

ts
           
export class FrontendComponent {
    constructor(
        private router: Router) {
        this.router.events.subscribe(event => {
            if (event instanceof NavigationEnd) {
                // event.url  '/blog'
                this.routerChanged(event.url);
            }
        });
    }
}
1234567891011

获取当前路由网址进行切换

angular9教程之添加启动加载动画

2020-05-16 03:34:08

第一步

src/index.html 加上动画效果

<app-root></app-root> 的后面

html
         
<style>@-webkit-keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@-moz-keyframes spin{0%{-moz-transform:rotate(0)}100%{-moz-transform:rotate(360deg)}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.spinner{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1003;background: #000000;overflow:hidden}  .spinner div:first-child{display:block;position:relative;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;box-shadow:0 3px 3px 0 rgba(255,56,106,1);transform:translate3d(0,0,0);animation:spin 2s linear infinite}  .spinner div:first-child:after,.spinner div:first-child:before{content:'';position:absolute;border-radius:50%}  .spinner div:first-child:before{top:5px;left:5px;right:5px;bottom:5px;box-shadow:0 3px 3px 0 rgb(255, 228, 32);-webkit-animation:spin 3s linear infinite;animation:spin 3s linear infinite}  .spinner div:first-child:after{top:15px;left:15px;right:15px;bottom:15px;box-shadow:0 3px 3px 0 rgba(61, 175, 255,1);animation:spin 1.5s linear infinite}</style>
  <div id="zo-global-spinner" class="spinner">
    <div class="blob blob-0"></div>
    <div class="blob blob-1"></div>
    <div class="blob blob-2"></div>
    <div class="blob blob-3"></div>
    <div class="blob blob-4"></div>
    <div class="blob blob-5"></div>
  </div>
123456789

这是一个加载动画

第二步

当加载完需要隐藏

需要在第一个 Component 初始化的时候隐藏

例如 src/app/app.component.ts

ts
      
export class AppComponent {
    constructor() {
        // 隐藏
        document.getElementById('zo-global-spinner').style.display = 'none';
    }
}
123456

angular9教程之分模块开发

2020-05-16 03:33:23

为什么分模块

每一个模块都是分开打包的,只有用到模块才会加载模块的资源,因此可以加快第一次打开的时间,减少不必要的加载

生成

生成一个 frontend 模块,并创建路由模块

 
ng g module frontend --route frontend --module app.module
1

在 frontend 模块下继续添加模板

 
ng g module frontend/blog --route blog --module frontend.module
1

分配路由到模块

src/app/app-routing.module.ts

ts
               
const routes: Routes = [
    {
        path: 'frontend',
        loadChildren: () => import('./frontend/frontend.module').then(m => m.FrontendModule)
    },
    {
        path: '', // 默认跳转到 frontend 模块
        redirectTo: 'frontend',
        pathMatch: 'full'
    },
    {
        path: '**',  // 其他没有匹配到的路径都跳转到 frontend 模块
        redirectTo: 'frontend'
    },
];
123456789101112131415

添加公共页面

修改 src/app/frontend/frontend.component.html

html
         
<header>
    导航栏
</header>
<router-outlet></router-outlet>
<footer>
    <div class="copyright">
        <p>Copyright ©zodream.cn, All Rights Reserved.</p>
    </div>
</footer>
123456789

<router-outlet></router-outlet> 是必须的,是根据子路由加载页面的

添加首页

 
ng g component frontend/home
1

绑定路由

修改 src/app/frontend/frontend-routing.module.ts

ts
                   
const routes: Routes = [
    {
        path: '',
        component: FrontendComponent,
        children: [
            {
                path: 'home',
                component: HomeComponent,
            }, {
                path: '',
                redirectTo: 'home',
                pathMatch: 'full',
            }, {
                path: '**',
                component: HomeComponent,
            }
        ]
    },
];
12345678910111213141516171819

测试

  
ng serve --open
12

出现一下内容则表示正常了

angular9教程之使用

2020-05-16 03:32:44

安装Angular CLI

 
npm install -g @angular/cli
1

生成初始程序

 
ng new my-app
1

必须有一个程序名,没办法生成到当前文件夹

运行

  
cd my-app
ng serve --open
12

使用 bootstrap

 
npm i bootstrap
1

angular.json 修改 projects > my-app > architect > build > options > styles 节点

最前面加上 "node_modules/bootstrap/dist/css/bootstrap.css",

json
     
"styles": [
    "node_modules/bootstrap/dist/css/bootstrap.css",
    "node_modules/@fortawesome/fontawesome-free/css/all.css",
    "src/styles.scss"
],
12345

或者使用 ng-bootstrap

 
ng add @ng-bootstrap/ng-bootstrap
1

这样就不需要手动去添加样式了

使用 fontawesome

 
npm i @fortawesome/fontawesome-free
1

引用 css 同上

引用其他包的样式和脚本

json
              
"assets": [
    "src/favicon.ico",
    "src/assets"
],
"styles": [
    "node_modules/bootstrap/dist/css/bootstrap.css",
    "node_modules/@fortawesome/fontawesome-free/css/all.css",
    "node_modules/pace-js/templates/pace-theme-flash.tmpl.css",
    "node_modules/ngx-toastr/toastr.css",
    "src/styles.scss"
],
"scripts": [
    "node_modules/pace-js/pace.min.js"
]
1234567891011121314

css

angular.json 修改 projects > my-app > architect > build > options > styles 节点加上 css 文件相对路径

js

angular.json 修改 projects > my-app > architect > build > options > scripts 节点加上 js 文件相对路径

其他文件例如图片

angular.json 修改 projects > my-app > architect > build > options > assets 节点加上文件相对路径

301跳转https和www

2020-05-15 05:52:12

强制使用https访问

Apache 做法

修改网站根目录下的 .htaccess 文件

   
RewriteEngine On
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://zodream.cn/$1 [R,L]
123

nginx 做法

     
server {
    listen       80;
    server_name  zodream.cn;
    return 301 https://$server_name$request_uri;
}
12345

iis 做法

修改网站根目录下的 web.config 文件

xml
                
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="HTTP to HTTPS redirect" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
12345678910111213141516

无www 跳转带www

Apache 做法

修改网站根目录下的 .htaccess 文件

   
RewriteEngine on
RewriteCond %{HTTP_HOST} ^zodream.cn [NC]
RewriteRule ^(.*)$ http://www.zodream.cn/$1 [L,R=301,NC]
123

nginx 做法

     
server {
    listen       80;
    server_name  zodream.cn;
    return 301 http://www.zodream.cn$request_uri;
}
12345

iis 做法

修改 无www 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能

xml
      
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpRedirect enabled="true" destination="http://www.zodream.cn" httpResponseStatus="Permanent" />
  </system.webServer>
</configuration>
123456

www 跳转无www

Apache 做法

修改网站根目录下的 .htaccess 文件

   
RewriteEngine on
RewriteCond %{HTTP_HOST} ^www.zodream.cn [NC]
RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC]
123

nginx 做法

     
server {
    listen       80;
    server_name  www.zodream.cn;
    return 301 http://zodream.cn$request_uri;
}
12345

iis 做法

修改 带www 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能

xml
      
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpRedirect enabled="true" destination="http://zodream.cn" httpResponseStatus="Permanent" />
  </system.webServer>
</configuration>
123456

强制其他域名都跳转到一个域名

Apache 做法

修改网站根目录下的 .htaccess 文件

   
RewriteEngine on
RewriteCond %{HTTP_HOST} !^zodream.cn [NC]
RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC]
123

nginx 做法

     
server {
    listen       80 default_server;
    server_name  _;
    return 301 http://zodream.cn$request_uri;
}
12345

iis 做法

修改 默认 * 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能

xml
      
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpRedirect enabled="true" destination="http://zodream.cn" httpResponseStatus="Permanent" />
  </system.webServer>
</configuration>
123456

gulp-vue2mini 使用教程

2020-05-14 21:13:23

资源

npm gulp-vue2mini

 
npm i gulp-vue2mini
1

示例

前言

本插件开发初衷,以类似于 vue 的代码语法开发微信小程序

但是,本程序不能直接把vue 程序转化为小程序代码。

那么,这样做有必要吗?

有!

  1. 熟悉vue语法的开发者能快速上手
  2. 小程序代码本身是 jscsshtmljson 分离的,这样看上出去文件繁多,一个小项目干嘛要那么麻烦
  3. 本程序是使用 typescript ,有很好的代码提示

使用

本程序提供配套示例代码,这是一个初始化版本,

直接下载本代码,

vscode 中打开

 
npm i
1

安装依赖

src/app.vue 为程序入口

src/pages/index 为首页

编译程序

 
npm run build
1

如果你的 vscode 安装了 Code Runner 扩展,支持右键编译当前单个文件

功能介绍

支持 ts sass

支持拆解html js ts sass css 写在一个文件上的情况

sass 支持ttf文件自动转化为 base64

sass 引用模式自动处理

自动转化html 为 wxml, 自动转化 v-if v-for v-else v-show

支持json自动生成,支持 属性合并

注意:span 标签下不能包含其他标签,否则会自动转换为view

标签属性转化列表

属性名 目标属性
v-if wx:if="{{ }}"
v-elseif wx:elif="{{ }}"
v-else wx:else
v-bind:src src
href url
@click bindtap
v-on:click bindtap
(click) bindtap
@touchstart bindtouchstart
@touchmove bindtouchmove
@touchend bindtouchend
:key
v-show hidden="{{! }}"
v-for wx:for="{{ }}" wx:for-index=" " wx:for-item=""
v-model value="{{ }}" bind:input=" Changed"
第一个字符为@且值不为空 bind:
第一个字符为: ={{ }}
其他包含@

支持 对 picker switch slider 执行 v-model 值绑定

支持 :class 数组形式及 {active: true} 形式自动会合并 class

支持 @click 直接赋值及直接传参数 @click="i = 1" @click="tap(i, a)"

定义WxPage WxCommpent WxApp 三个类,增强 setData 的智能提示,

export 是为了避免提示未使用,编译时会自动去除

增加自动添加 Page(new Index()) Commpent(new Index()) App(new Index()) 到末尾

增加json配置生成

ts
           

@WxJson({
    usingComponents: {
        MenuLargeItem: "/components/MenuLargeItem/index",
        MenuItem: "/components/MenuItem/index"
    },
    navigationBarTitleText: "个人中心",
    navigationBarBackgroundColor: "#05a6b1",
    navigationBarTextStyle: "white"
})
1234567891011

自动合并页面相关的json文件

支持自动合并 methods lifetimes pageLifetimes, 如果已有 属性会自动合并

methods  @WxMethod
lifetimes @WxLifeTime
pageLifetimes @WxPageLifeTime

自定义部件自动合并方法到methods属性中

ts
          
methods = {
    aa() {

    }
}

@WxMethod()
tapChange(mode: number) {
}
12345678910

最终生成

ts
       
methods = {
    tapChange(mode: number) {
    },
    aa() {

    }
}
1234567

标准模板

index.vue

html
                               
<template>
    <div>

    </div>
</template>
<script lang="ts">
import {
    IMyApp
} from '../../app';

const app = getApp<IMyApp>();

interface IPageData {
    items: number[],
}

export class Index extends WxPage<IPageData> {
    public data: IPageData = {
        items: []
    };

    onLoad() {
        this.setData({
            items: []
        });
    }
}
</script>
<style lang="scss" scoped>

</style>
12345678910111213141516171819202122232425262728293031

最终会处理为3个文件

index.wxml

html
   

<view></view>
123

index.wxss

css
 
1

index.js

js
                 
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var app = getApp();
var Index = (function () {
    function Index() {
        this.data = {
            items: [],
        };
    }
    Index.prototype.onLoad = function () {
        this.setData({
            items: []
        });
    };
    return Index;
}());
Page(new Index());
1234567891011121314151617

注意

新增了一些指定的声明请参考

create.js 开发小游戏(二)蒙版

2020-05-14 06:07:23

createjs 中物体是不会自动超出边界自动隐藏的,

例如,负坐标也不会在视野内隐藏

里面的每一个 DisplayObject 都是没有宽和高限制的

例如 createjs.Container 上面加个会动的 createjs.Shape, 我想让她 在 x: 0, y: 0, width: 600, height: 600 这个范围内可见,跑到其他地方不可见,该怎么办?

下面代码就可以实现,

ts
        

const box = new createjs.Container();
const shape = new createjs.Shape();

box.addChild(shape);
// 添加蒙版
const mask = new createjs.Shape(new createjs.Graphics().beginFill("#ffffff").drawRect(box.x, box.y, 600, 600));
box.mask = mask;
12345678

但请注意 mark drawRect 使用的时 box 的父坐标系统,不是 box 的坐标系统,

这并不能遮住 box 里面坐标 x 或 y 为负的,可以考虑 mark drawRect(box.x + x, box.y + y, 600 - x, 600 - y) 扩大蒙版的范围

createjs 游戏结束后重新开始 Tween 补间动画无效

2020-05-13 06:08:01

问题描述

当游戏结束了,重新开始发现使用 Tween 的补间动画不起作用了。

解决方法

在 github 的 issues 找到同样的问题

 
Tween._inited does not reset if scene is being destroyed or removed inside the game and rebuild
1

意思就是 Tween 上的一个 _inited 私有属性没有更新,只要增加一句代码就行了

js
 
Tween._inited = undefined;
1

但这种方法肯定是不友好的,但是 Tween 又没有提供 reset 方法,

最后通过查看源码,发现是 createjs.Ticker 引起的

当执行 createjs.Ticker.reset() 会清空所有 tick 事件监听,而 Tween 又没有收到通知,没法更新 _inited 属性,导致 Tween 不会重新添加事件监听 createjs.Ticker.addEventListener("tick", Tween)

所以,要么不要使用 createjs.Ticker.reset(),要么使用

js
  
createjs.Ticker.reset();
createjs.Tween._inited = undefined;
12

create.js 开发小游戏(一)搭建项目

2020-05-12 20:27:55

本文使用 typescript 开发

环境准备

node

安装一些依赖

  
npm i @types/createjs createjs --save
npm i gulp gulp-concat gulp-typescript typescript --save-dev
12

增加文件 tsconfig.json ts 的编译配置信息

json
                           
{
    "compilerOptions": {
        "baseUrl": "./",
        "target": "es5",
        "strictNullChecks": true,
        "noImplicitAny": true,
        "allowJs": false,
        "experimentalDecorators": true,
        "noImplicitThis": true,
        "noImplicitReturns": true,
        "alwaysStrict": true,
        "noFallthroughCasesInSwitch": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "strict": true,
        "removeComments": true,
        "pretty": true,
        "strictPropertyInitialization": true,
    },
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules",
        "dist",
    ]
}
123456789101112131415161718192021222324252627

gulpfile.js gulp 编译方式

js
                  
var gulp = require('gulp'),
    ts = require('gulp-typescript'),
    concat = require('gulp-concat'),
    tsProject = ts.createProject('tsconfig.json');

gulp.task('copy', async() => {
    // 复制 createjs 到目标文件夹
    await gulp.src('node_modules/createjs/builds/1.0.0/createjs.min.js')
        .pipe(gulp.dest('dist/'));
});

gulp.task('default', gulp.series('copy', async() => {
    // 合并ts 文件并编译
    await gulp.src(['src/core/*.ts', 'src/scene/*.ts', 'src/*.ts'])
        .pipe(concat('zodream.ts'))
        .pipe(tsProject())
        .pipe(gulp.dest('dist/'));
}));
123456789101112131415161718

开始

新增 src 文件夹 里面放 主控制

新建 src/core 文件夹 里面放基本定义

新建 src/scene 文件夹 里面放每一个场景

具体代码示例查看 示例

主要代码

初始化场景

ts
 
let stage = new createjs.Stage(arg: string | HTMLCanvasElement);
1

设置启用触摸

ts
 
createjs.Touch.enable(stage);
1

设置场景的尺寸

ts
  
(<HTMLCanvasElement>stage.canvas).width = width;
(<HTMLCanvasElement>stage.canvas).height = height;
12

设置场景刷新频率

ts
  
createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED;
createjs.Ticker.framerate = 60;
12

清空场景,更换场景时使用

ts
 
stage.removeAllChildren();
1

添加物体

ts
 
stage.addChild(...arg);
1

刷新生效

ts
   
createjs.Ticker.addEventListener('tick', function() {
    stage.update();
});
123

运行

新增 index.html

html
                       
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Catch Cat</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body {
                margin: 0;
                padding: 0;
            }
        </style>
    </head>
    <body>
        <canvas id="stage" height="600" width="600"></canvas>

        <script src="dist/createjs.min.js"></script>
        <script src="dist/zodream.js"></script>
        <script>
            App.main('stage');
        </script>
    </body>
</html>
1234567891011121314151617181920212223

打开 index.html

有的浏览器直接打开文件无法加载资源文件,请使用其他程序,例如 iis,

node
 
npm i static-server
1

server.js

js
         
var StaticServer = require('static-server');
var server = new StaticServer({
    rootPath: '.',            // required, the root of the server file tree
    port: 8081,               // required, the port to listen
});

server.start(function () {
    console.log('Server listening to', server.port);
});
123456789

启动

 
node server.js
1

打开 http://localhost:8081 即可查看效果

SEO(二)关键词优化试验

2020-05-12 20:27:07

  1. 可以使用 站长之家百度关键词挖掘,寻找关键字
  2. 可以使用 站长之家SEO综合查询,寻找同类型网址的关键字

长尾词

长尾关键词是指网站上的非目标关键词但与目标关键词相关的也可以带来搜索流量的组合型关键词。

长尾词为几个关键词的组合,通常在搜索引擎搜索时出现的智能提示就是长尾词。

首页关键词优化,增加相关关键字,并进行相关文章展示

我的做法

  1. 先确定本站的发展方向,选择几个关键词
  2. 通过 站长之家-百度关键词挖掘 搜索相关关键词,分别选择几个合适的 长尾词
  3. 长尾词 进行文章添加

目前效果

未知

关键词

关键词来源:文章内容重点名称的提取

我的做法

  1. 对所有文章增加关键词
  2. 增加相应的标签进行归类,方便进行文章关联
  3. 通过 SEO综合查询 参考其他网址获取关键词

目前效果

未知

SEO(一)内链优化试验

2020-05-12 20:26:36

事先说明

本文为参考网上的相关文章,进行对本站的优化,效果暂时不知道,重点:效果不知道

外部链接是别人对你的投票,内部链接优化建设是自己对自己页面进行投票,页面被投票越多,说明这个页面内容越重要

网站地图

最好给自己的网站建立一个完整的网站地图,把网站地图的链接放在网站首页,这样能更利于让搜索引擎蜘蛛发现地图,抓取网站内容。

一般存放在根目录下并命名sitemap,为爬虫指路,增加网站重要内容页面的收录。

当然,在一些搜索引擎是允许主动提交 sitemap 的。

我的做法

通过程序自动生成sitemap.xml 并把她主动提交到搜索引擎,

目前效果

百度、谷歌、360 都有收录,必应没收录

网站权重传递

一般来说网站结构建设良好,网站首页的权重是最高的,栏目页第二,内容页最差。如果有站长朋友有一些重点推广的页面,可以通过网页的链接提升权重的,使这些页面的权重升高。

  1. 保障网站权重在重要页面进行清楚传递
  2. 为重点页面制造多个进口传递更多权重
  3. 要去掉与网站权重传递无关的元素内容

我的做法

已修改首页,增加多个模块展示更多的文章

目前效果

未知

页面层数

对一般的网站而已,最好能确保从首页开始算,点击2-3次就能到达网站的任何一个页面,最多不要超过4次,点击次数越少越好,更有利于网站优化

网址越大,层数就不好控制,但要保证重点内容能符合标准。

我的做法

文章为中心,所以增加首页文章展示

目前效果

未知

网页相互链接

很多站长会对网站的树形结构有误解,正确的理解是,在不同栏目的网页上链接到其他栏目的相关网页,整个网站的链接最后看起来像蜘蛛网一样既有主干也有页面之间的相互链接。

  1. 首页增加文章分类展示
  2. 增加文章统计和标签归类
  3. 增加文章内容相关文章展示
  4. 增加文章内容内链

我的做法

增加标签聚合,增加归档,增加文章相关文章展示

目前效果

未知

Select2使用

2020-05-12 07:11:59

Select2是一款基于JQuery的下拉列表插件,主要用来优化select,支持单选和多选,同时也支持分组显示、列表检索、远程获取数据等功能。

下载

官网

准备

JQuery

select2.min.css

select2.min.js

使用

普通使用

html
                        

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="select2.min.css">
</head>
<body>
    <select name="name">
        <option value="1">1</option>
        <option value="2" selected>2</option>
    </select>
    <script src="jquery-3.5.1.min.js"></script>
    <script src="select2.min.js"></script>
    <script>
        $(function() {
            $('select').select2();
        });
    </script>
</body>
</html>
123456789101112131415161718192021222324

提示

js
   
$('select').select2({
    placeholder: '请选择'
});
123

加载本地数据

需要两个数据属性idtext

js
      
var data = [
    {id: 1, text: '1'}
];
$('select').select2({
    data: data
});
123456

加载远程数据

js
                          
$('select').select2({
    ajax: {
        url: 'tags',
        data: function (params) {
            return {
                keywords: params.term, // keywords为发送到服务端的参数名,params.term表示输入框中输入的内容
                page: params.page || 1
            };
        },
        processResults: function (data) {
            data = data.data;
            return {
                results: data.data.map(item => {
                    return {
                        id: item.id,
                        text: item.name,
                    }
                }),
                pagination: {
                    // 是否还有下一页
                    more: data.paging.more
                }
            };
        }
    }
});
1234567891011121314151617181920212223242526

设置默认选中

html
    
<select name="name[]" multiple>
    <option value="1" selected>1</option>
    <option value="2" selected>2</option>
</select>
1234

WordPress使用

2020-05-11 20:57:49

预先准备

网站的域名

国内的 【万网

国外的 【Godadday

网站空间

分为 虚拟主机 和 服务器

买国内的空间需要给域名备案,不然无法打开。

个人的话对网站内容有限制。

虚拟主机

价格相对便宜,系统和环境不能自定义,只能选指定的,灵活度低,但对新手友好

服务器

系统能更改

运行环境需要自己搭建。

本地软件要求

代码的编辑器: 要注意文件的编码格式,推荐 Vim、NotePad++、Vs Code

FTP上传工具:Filezilla

常用博客软件

WordPress

下载

环境要求:php7.3 + mysql 5.6

  1. 安装

有的虚拟主机包括 wordpress,但版本可能不是最新

从官网下载 WordPress,解压

通过FTP工具将 wordpress 文件夹里面的文件全部上转至你的网站根目录。

  1. 配置

浏览器访问你的域名,默认会跳转到 /wp-admin/setup-config.php

第一步,选择语言,wordpress 是只支持单语言

第二步,说明,直接下一步

第三步,配置数据库信息,数据库必须存在,不存在的话请先创建,可以使用 phpMyAdmin 创建

第四步,配置站点信息及管理员账号密码

  1. 使用

/wp-login.php 这是登录后台网址

显示语言在 左边菜单 Settings 修改 Site Language 点击 Save Changes 保存

Posts 中发布新的文章

AppearanceThemes 可以更改主题模板

Plugins 可以安装各种插件,但安装的越多,打开速度越慢,初期不建议使用

Redis 使用

2020-05-09 01:55:38

windows 安装 Redis

  1. 在任务栏搜索 启用或关闭Windows功能
  2. 选择 适用于Linux的Windows子系统
  3. 在应用商店安装 Ubuntu
  4. 安装编译环境
shell
   
sudo apt-get update
sudo apt-get install make
sudo apt-get install gcc
123
  1. 安装
shell
     
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
sudo make install
sudo cp redis.conf /etc/redis.conf
12345
  1. 启动
shell
     
cd /usr/local/bin
redis-server /etc/redis.conf


redis-server /etc/redis.conf &     #指定配置文件启动redis,且后台启动
12345

安全配置

redis.conf

conf
    
protected-mode yes   #打开保护模式
port 6380  #更改默认启动端口
requirepass xxxxxx   #设置redis启动密码,xxxx是自定义的密码
1234

来禁用远程修改 DB 文件地址

   
rename-command FLUSHALL ""
rename-command CONFIG   ""
rename-command EVAL     ""
123

禁止外网访问 Redis

 
bind 127.0.0.1
1

使用

字符串(String)

可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等,最多可以容纳的数据长度是512M

      
set mykey "this is a test"   //通过set命令为键设置新值,并覆盖原有值。
get mykey

incr mykey                 //该Key的值递增1
decr mykey                 //该Key的值递减1
del mykey                  //删除已有键
123456

哈希(Hash)

键值对字典结构

列表(List)

简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

集合(Set)

是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

有序集合(sorted set)

有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

使用问题

雪崩

指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。

解决办法:

  1. 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。
  2. 简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。

缓存穿透

指访问不存在的数据,导致跳过缓存,直接访问数据库。

例如:数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

解决办法:

  1. 每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

缓存击穿

就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

解决办法:

  1. 可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

WEB攻防(一)基础

2020-05-08 18:44:38

本文为阅读 《黑客攻防技术宝典:Web实战篇(第2版)》 的笔记

黑名单过滤

根据已知的字符串确认数据,排除在黑名单之上的数据。

大小写切换攻击

SELECT 可以尝试 SeleCt

数字更改

or 1=1-- 可以尝试 or 2=2--

同类函数更换

alert('xss') 可以尝试 prompt('xss')

sql 注释

sql
 
select/*foo*/username,password/*foo*/from/*foo*/users
1

非标准字符

html
 
<img%09onerror=alert(1) src=a>
1

空字节攻击

在表达式之前插入空字节

html
 
%00<script>alert(1)</script>
1

删除表达式

通过删除特定表达式,过滤内容

删除 <script> 可以尝试 <scr<script>ipt>

递归删除

  1. 删除 ../
  2. 删除 ..\

可以尝试 ....\/

URI 编码

删除省略号

可以尝试 %2527

%%2727

HTML 编码

html
  
<iframe src=javascript:alert(1) >
12

PHP 转义函数

2020-05-06 23:03:29

函数名 释义 介绍 使用
htmlspecialchars 将与、单双引号、大于和小于号化成HTML格式 &转成&<br>"转成"<br>' 转成'<br><转成<<br>>转成> html页面输出用户输入内容<br> sitemap.xml 转义链接
htmlentities 所有字符都转成HTML格式 除上面htmlspecialchars字符外,还包括双字节字符显示成编码等。
addslashes 单双引号、反斜线及NULL加上反斜线转义 被改的字符包括单引号 (')、双引号(")、反斜线 backslash (\) 以及空字符NULL
stripslashes 去掉反斜线字符 去掉字符串中的反斜线字符。若是连续二个反斜线,则去掉一个,留下一个。若只有一个反斜线,就直接去掉。
quotemeta 加入引用符号 将字符串中含有 . \ + * ? [ ^ ] ( $ )等字符的前面加入反斜线 "\" 符号。
nl2br 将换行字符转成<br>
strip_tags 去掉HTML及PHP标记 去掉字符串中任何 HTML标记和PHP标记,包括标记封堵之间的内容。注意如果字符串HTML及PHP标签存在错误,也会返回错误。
mysql_real_escape_string 转义SQL字符串中的特殊字符 转义 \x00 \n \r 空格 \ ' " \x1a,针对多字节字符处理很有效。mysql_real_escape_string会判断字符集,mysql_escape_string则不用考虑。
base64_decode base64解码 对使用 MIME base64 编码的数据进行解码
base64_encode base64编码 使用 MIME base64 对数据进行编码
rawurldecode URL解码 对已编码的 URL 字符串进行解码
rawurlencode URL编码 按照 RFC 1738 对 URL 进行编码
urldecode URL解码 解码已编码的 URL 字符串
urlencode URL编码 编码 URL 字符串

apache自定义文件类型响应头header

2020-05-06 20:46:04

第一种修改 httpd.conf

打开配置文件 httpd.conf

找到 IfModule mime_module 配置模块

conf
   
<IfModule mime_module>

    AddType application/x-compress .Z
123

添加方法 AddType + 响应头 + 文件拓展名

apache自定义header

第二种修改 mime.types

修改方式

 
application/vnd.dart                dart
1

响应头 + 文件拓展名

apache自定义header

怎么设置文件编码

添加这个全局设置,但只作用文件类型响应头为 text/plaintext/html

 
AddDefaultCharset utf-8
1

其他类型需要在 IfModule mime_module 下使用

 
AddCharset utf8 .json
1

vscode 输出中文乱码

2020-05-06 19:04:01

vscode 输出中文乱码的解决方法

直接在任务栏中搜索 区域设置开始->设置->时间和语言右上角相关设置中的 日期、时间和区域格式设置 进入 区域设置

右上角 相关设置中的 其他日期、时间和区域格式设置

区域下的 更改日期、时间或数字格式

管理 选项卡 更改系统区域设置

勾选 使用 Unicode UTF-8 提供全球语言支持

重启电脑即可

参考

  1. vscode输出窗口中文乱码

网站体检之sqlmap注入

2020-04-30 02:44:30

本次使用环境

kali

sqlmap

先更新一下软件

shell
          
sudo su

#更新软件源中的所有软件列表
apt-get update

#更新软件
apt-get upgrade

#更新系统版本
apt-get dist-upgrade
12345678910

测试

sqlmap 基本参数

-u 网址 ""

--dbms= 数据库类型 mysql

--cookie= 设置cookie 登录信息 ""

--method= 请求方式 POST

--headers= 设置请求头

 
sqlmap -u "http://zodream.localhost/blog?id=19"
1

结果

 
parameter 'id' does not seem to be injectable
1

暂时找不到注入

phpMyAdmin配置文件中的密文(blowfish_secret)太短。

2020-04-26 21:39:29

phpMyAdmin配置文件中的密文(blowfish_secret)太短。

需要修改配置文件

config.inc.php

php
 
$cfg['blowfish_secret'] = '5d01588e401875b15374662a96261262';/* YOU SHOULD CHANGE THIS FOR A MORE SECURE COOKIE AUTH! */
1

随便输入一个32位的字符串即可。

“cookie”身份验证类型使用AES算法加密密码。如果您使用的是“cookie”身份验证类型,请在此输入您选择的随机密码短语。AES算法将在内部使用它:不会提示您输入此密码短语。

这个密钥应该有32个字符长。使用较短的会导致加密cookie的安全性较弱,使用较长的不会造成危害。

hashcat(一)找回office文件密码

2020-04-26 17:53:09

下载地址

hashcat

安装

解压到文件夹即可

使用

具体参数 见【文档

找回office文件密码

  1. 需要下载脚本office2john.py 提取 hash
  2. 执行脚本office2john.py获取hash
shell
 
python office2john.py a.xlsx
1

获得hash $office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e

这是offile 2010

  1. 运行程序

直接运行

shell
  
.\hashcat64.exe -m 9500 '$office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e17d' -a 3 ?a?a?a?a?a?a?a
12

或把hash 保存到文a.txt

shell
  
.\hashcat64.exe -m 9500 a.txt -a 3 ?a?a?a?a?a?a?a
12

-m 哈希类别

9500 表示 MS Office 2010

-a 0字典攻击,-a 1 组合攻击;-a 3掩码攻击

?a 表示大小写字母数字符号 包含?l?u?d?s,重复几次表示密码有几位

?d 表示数字

?l 小写字母

?u 大写字母

?s 特殊字符 «space»!"#$%&'()*+,-./:;<=>?@[]^_`{|}~

?b 0x00 - 0xff

?h 十六进制字符 0123456789abcdef

?H 大写的十六进制字符 0123456789ABCDEF

--increment --increment-min 1 --increment-max 8 指定密码范围最小1位最多8位

  1. 查看运行进度

s 回车即可

Status...........: Running 表示正在运行中

Status...........: Cracked 表示解密成功

Status...........: Exhausted 表示程序已结束,但没有找到密码

  1. 查看密码

Status...........: Cracked 出现后,程序运行结束

上一行就会出现破解密码

:123 最后的冒号后的就是密码了

问题

  1. 报错 CL_OUT_OF_RESOURCES

解决方法:

新建文本 wddm_timeout_patch.reg,点击即可

    
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers]
"TdrLevel"=dword:00000000
1234
  1. Hash '$office$20131000002561642f7509323c3d047371ef44fbb0c47b8e7707349': Token length exception No hashes loaded.

这是因为在 powershell 命令行中直接输入,导致hash被当作命令

解决方法:

1. 把hash保存到文件,以文件形式传入
2. 用英文单引号把hash 当作字符串输入,双引号是不行的
  1. INFO: All hashes found in potfile!

表示密码已经找到过了,通过 --show 即可查看

panic: reflect: call of reflect.Value.Interface on zero Value

2020-04-25 22:33:12

panic: reflect: call of reflect.Value.Interface on zero Value

出现场景

在使用 reflect 报错

go
          
func Dump(vals ...interface{}) {
    for _, v := range vals {
        val := reflect.ValueOf(v)
        fmt.Printf("[%T]", val.Interface())
    }
}

u := &User{}

Dump(u)
12345678910

解决方案

修改成

go
 
Dump(&u)
1

因为反射看不到未导出的函数

这个错误是没办法通过方法内部解决的,是写代码时的错误,只能修改使用方法的代码。

同类错误 reflect: call of reflect.Value.Call on zero Value

第一个App终于上线了

2020-04-24 06:08:14

体验地址

Win10 UWP

主要功能

本程序开发目的

一开始只有web版,用来约束自己,每次做事都有一个时间统计,合理安排工作与休息时间。

目前已有功能

任务添加

任务计时

任务记录回顾

其他附加功能

扫码登录web (这是我一直想实现的功能,每次登录太麻烦了)

签到 (目前没有签到奖励)

修改个人信息

查看博客文章

下个版本功能

任务包含子任务

任务记录图表统计

加入个人财务管理

具体使用

  1. 打开 应用商店 Microsoft Store
  2. 搜索 我的时间回忆薄
  3. 进入详情,点击获取
  4. 点击安装
  5. 打开应用
  6. 点击左侧 我的
  7. 点击头像 进入登录界面
  8. 点击注册账户(注:邮箱在注册时可以随意填写,但找回密码和更换邮箱需要发送邮件)
  9. 回到首页
  10. 点击底部按钮 + 进入 任务列表
  11. 点击底部按钮 + 进入添加任务
  12. 添加

任务名

任务说明

每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止

  1. 点右上角 进行保存
  2. 回到任务列表页
  3. 点击底部 🖊 编辑按钮
  4. 选择几个任务
  5. 点击底部 + 添加到今日任务 就会把选中的任务添加到今日任务,可以连续点击,添加多次任务
  6. 点击底部的 完成 按钮就会退出编辑模式
  7. 底部的 结束任务 可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了
  8. 回到首页
  9. 点击 一个任务,进入任务执行页面
  10. 点击底部的 开始 按钮,开始任务
  11. 。。。
  12. 等到圈转完 表示此轮任务执行完一次,可以停下来休息3-5分钟
  13. 在执行时,可以点击 暂停,表示有其他事打扰,需要暂停一下,处理完后点 开始 继续执行任务
  14. 在执行时,可以点击 停止,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务
  15. 左侧时间薄可以查看每一天执行的记录

聊聊开发过程

本来,第一个版本是微信小程序版的,当时审核直接被打回,好像时个人无法发布包含用户系统的小程序。

那么由此,我的程序做成 app 也将无法在国内的应用市场上架了,所以只能暂缓flutter 开发 App

UWP 是由于几年前就注册了开发者的,但那时没有发布什么app,而且本人业余时间也对 c# 比较熟悉,所以就拿这个程序作为实验版。

UWP 版有想法兼容已经淘汰的Win10 Mobile,因为手上那个还有几个 lumia 手机

node-sass 安装不了怎么办

2020-04-24 00:13:13

今天把 node.js 升到 14.0.0,发现使用 npm update 安装不了 node-sass

使用淘宝镜像

shell
 
npm install -g cnpm --registry=https://registry.npm.taobao.org
1

不行,node-sass 依然是从GitHub 下不下来。

这时想到是不是网络问题导致访问不了 github

使用淘宝镜像的 node-sass

shell
 
npm config set sass_binary_site=http://npm.taobao.org/mirrors/node-sass
1

发现还是不行,点击进去发现没有最新的node-sass

这是回过头看第一种方法,发现是 node-sass 还没有发布适配 node.js 14.0.0 的包

下载 win32-x64-83_binding.node到本地

github 上找 node-sass 最新的有一个 适配的 win32-x64-83_binding.node

下载到本地

shell
   
npm config set sass_binary_path=F:\Downloads\win32-x64-83_binding.node

npm i
123

这样就行了

当然 node-sass 里面有个判断 node.js 版本的报错需要删除

这是临时解决方法,还是等 node-sass 更新吧

删除配置

shell
   
npm config delete sass_binary_site

npm config delete sass_binary_path
123

最佳方案换成dart-sass

shell
 
npm install --save-dev sass
1

gulp-sass 替换方法

js
  
var sass = require("gulp-sass");
sass.compiler = require('sass');
12

UWP 使用语言包

2020-04-16 20:44:41

UWP 使用语言包

我的程序默认语言为 zh-CN

则新建文件夹 Strings\zh-CN

再添加新建项 资源文件(.resw) 新建文件 Resources.resw

添加 xaml 控件的文字

例如要设置 TextBlockText

xml
 
<TextBlock Text="开"/>
1
  1. TextBlock 添加属性 x:Uid
xml
 
<TextBlock x:Uid="OnLabel"/>
1
  1. Resources.resw 添加一行
名称
OnLabel.Text

名称的组成为 {x:Uid}.{属性名}

如果要给一个控件设置多个属性

名称
OnLabel.Text
OnLabel.Content

注意:

  1. x:Uid 属性是没法在代码中获取到的

使用代码使用语言包

先添加在一个类上增加一个公开方法: 例如 类名 AppResource

c#
                              
#region 语言包获取文字

        private static ResourceLoader CurrentResourceLoader
        {
            get { return _loader ?? (_loader = ResourceLoader.GetForCurrentView("Resources")); }
        }

        private static ResourceLoader _loader;
        private static readonly Dictionary<string, string> ResourceCache = new Dictionary<string, string>();

        /// <summary>
        /// 获取资源字典的值
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static string GetString(string key)
        {
            if (ResourceCache.TryGetValue(key, out string s))
            {
                return s;
            }
            else
            {
                s = CurrentResourceLoader.GetString(key);
                ResourceCache[key] = s;
                return s;
            }
        }

 #endregion
123456789101112131415161718192021222324252627282930

Resources.resw 添加一行

名称
appName 开始

使用

c#
 
var name = AppResource::GetString("appName");
1

注意:

  1. 使用 AppResource::GetString("OnLabel.Text")AppResource::GetString("OnLabel") 这种方式是不行,也就是 xaml 上使用的名称没法在代码中使用,只能多增加一条
  2. 名称 OnLabel.TextOnLabel 添加是冲突的

多语言

通过 vs 的扩展添加 Multilingual App Toolkit 能自动生成其他语言的翻译

参考

  1. Win10 UWP 开发系列:使用多语言工具包让应用支持多语言

UWP 上传图片

2020-04-16 20:44:13

场景

需要一个修改头像的功能,

  1. 第一种,直接选择文件上传即可
  2. 第二种,选择文件后进行裁剪再上传

选择图片文件

这是第一种,直接选择文件进行上传

c#
             
using Windows.Web.Http;

var filePicker = new FileOpenPicker
{
    ViewMode = PickerViewMode.Thumbnail,
    SuggestedStartLocation = PickerLocationId.PicturesLibrary,
    FileTypeFilter =
    {
        ".png", ".jpg", ".jpeg"
    }
};
var file = await filePicker.PickSingleFileAsync();
var stream  = new HttpStreamContent(await file.OpenReadAsync());
12345678910111213

开始上传

这里是进行具体上传的地方

c#
       
using Windows.Web.Http;

var httpClient = new HttpClient();
var form = new HttpMultipartFormDataContent();
form.Add(stream, "file", "avatar.png");
await httpClient.PostAsync(new Uri(), form);
1234567

file 为POST表单中文件的键

avatar.png 为文件名

注意:

  1. 使用了 HttpMultipartFormDataContent 就不要设置 请求头中 Content-Type,不然服务器端无法解析表单内容

处理图片并进行上传

这是使用 Microsoft.Toolkit.Uwp.UI.Controls 里面的裁剪控件 ImageCropper

c#
        
using Microsoft.Toolkit.Uwp.UI.Controls;
using Windows.Web.Http;
using Windows.Storage.Streams;

var inputStream = new InMemoryRandomAccessStream();
await ImageCropper.SaveAsync(stream, BitmapFileFormat.Png);

var stream = new HttpStreamContent(inputStream);
12345678

小程序Mini-Timer开发(四)使用说明

2020-03-30 21:26:31

使用此程序必须先登录,支持微信登录

添加任务

  1. 首页点右下角 + 进入所有任务
  2. 在任务列表 点右下角 + 进入任务添加任务页面
  3. 添加

任务名

任务说明

每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止

  1. 点右上角 进行保存

添加今日任务

  1. 首页点右下角 + 进入所有任务
  2. 在任务列表,点击左下角 的 编辑 按钮 进入编辑模式
  3. 选择一个或多个
  4. 点击底部的 添加到今日任务 就会把选中的任务添加到今日任务,可以连续点击,添加多次任务
  5. 点击底部的 完成 按钮就会退出编辑模式
  6. 底部的 结束任务 可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了

执行任务

  1. 首页点击 一个任务,进入任务执行页面

  2. 点击底部的 开始 按钮,开始任务

  3. 这时可以把手机放一边了,做事

  4. 等时间归 0,手机会震动,表示此轮任务执行完一次,可以停下来休息3-5分钟

  5. 在执行时,可以点击 暂停,表示有其他事打扰,需要暂停一下,处理完后点 开始 继续执行任务

  6. 在执行时,可以点击 停止,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务

小程序Mini-Timer开发(三)正式测试

2020-03-30 21:24:49

小程序Mini-Timer开发(三)正式测试

问题及解决方法

服务端 请求头 CONTENT_TYPE 接受不到

原因 $_SERVER['HTTP_CONTENT_TYPE'] 没有值

加上一个判断 允许 $_SERVER['HTTP_CONTENT_TYPE] $_SERVER['CONTENT_TYPE'] 两种获取方式

服务端 请求头 Authorization 接受不到

原因 apache 下重写 认是不能获取Authorization信息的

可以使用 getallheaders() 获取 不要从 $_SERVER 上提取,上一个问题也可以用此方法

注意 getallheaders() 获取的是原始请求头 键未大写 ,- 也未处理为 _ 示例 ['Content-Type' => 'application/json;charset=utf-8']

token 突然失效

用户信息 返回的token有问题

birthday 保存不上

服务端未进行 birthday 保存

在任务页面添加了任务返回首页未显示

onLoad 方法只第一执行了, 需要把执行的内容放到 onShow 中,这样每次显示页面都会刷新

任务执行完了但验证失败

原因 服务器时间与本地时间对不上,改为通过已完成时间重新计算开始时间

无法下拉刷新

enablePullDownRefresh: true 未设置

错误响应未显示弹窗

wx.showLoadingwx.showToast 是冲突的,必须先关闭 Loading

小程序Mini-Timer开发(二)http封装

2020-03-30 21:24:13

小程序Mini-Timer开发(二)http封装

基本请求封装 Promise

  1. 普通请求
ts
                                                                                
interface IRequestOption {
    headers?: any;
    mask?: boolean;
    loading?: boolean;
    guest?: boolean; // token失效不自动跳转
}

interface IRequest extends IRequestOption {
    url: string;
    params?: any;  // 拼接到url上
    data?: any;    // post 数据
}

export function request<T>(method: 'OPTIONS'| 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT', requestHandler: IRequest, option?: IRequestOption) {
    if (option) {
        // 多加一个 option 是方便进一步封装传入自定义配置
        requestHandler = Object.assign(requestHandler, option);
    }
    let { url, params, data, headers, mask, loading, guest } = requestHandler;
    if (loading === undefined || loading) {
        // 自动显示加载中
      wx.showLoading && wx.showLoading({title: 'Loading...', mask: mask ? mask : false})
    }
    if (!params) {
        params = {};
    }
    if (!headers) {
        headers = {};
    }
    // 这里可以加入自定义接口appid 和签名
    const token = wx.getStorageSync(TOKEN_KEY)
    if (token) {
        // 加入登录令牌
        headers.Authorization = 'Bearer ' + token;
    }
    return new Promise<T>((resolve, reject) => {
        wx.request({
            url: util.uriEncode(util.apiEndpoint + url, params),
            data: data,
            method: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'].indexOf(method) > -1 ? method : 'GET',
            header: Object.assign({
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            }, headers),
            success: function (res) {
                const { data, statusCode } = res;
                if (statusCode === 200) {
                    resolve(data as any);
                    return;
                }
                if (statusCode !== 401 || !guest) {
                    // 自动显示错误提示
                    wx.showToast({
                        title: (data as any).message,
                        icon: 'none',
                        duration: 2000
                    });
                }
                if (statusCode === 401) {
                    app && app.setToken();
                    if (!guest) {
                        // 自动跳转到登录页
                        wx.navigateTo({
                            url: '/pages/member/login'
                        });
                    }
                }
                // 处理数据
                reject(res)
            },
            fail: function () {
                reject('Network request failed')
            },
            complete: function () {
                wx.hideLoading && wx.hideLoading()
            }
        })
    });
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. 上传文件
ts
                                                             
/**
 * 上传文件
 * @param file 要上传文件资源的路径 (本地路径)
 * @param requestHandler 
 * @param name 上传文件的对应的 key
 */
export function uploadFile<T>(file: string, requestHandler: IRequest, name: string = 'file'): Promise<T> {
    let { url, params, data, headers, mask, loading } = requestHandler;
    if (loading === undefined || loading) {
      wx.showLoading && wx.showLoading({title: 'Loading...', mask: mask ? mask : false})
    }
    if (!params) {
        params = {};
    }
    if (!headers) {
        headers = {};
    }
    // 这里可以加入自定义接口appid 和签名
    const token = wx.getStorageSync(TOKEN_KEY)
    if (token) {
        // 加入登录令牌
        headers.Authorization = 'Bearer ' + token;
    }
    return new Promise<T>((resolve, reject) => {
        wx.uploadFile({
            url: util.uriEncode(util.apiEndpoint + url, params),
            formData: data,
            filePath: file,
            name,
            header: Object.assign({
                'Accept': 'application/json',
            }, headers),
            success: function (res) {
                const { data, statusCode } = res;
                if (statusCode === 200) {
                    resolve(data as any);
                    return;
                }
                wx.showToast({
                    title: (data as any).message,
                    icon: 'none',
                    duration: 2000
                });
                if (statusCode === 401) {
                    app && app.setToken();
                    wx.navigateTo({
                        url: '/pages/member/login'
                    });
                }
                // 处理数据
                reject(res)
            },
            fail: function () {
                reject('Network request failed')
            },
            complete: function () {
                wx.hideLoading && wx.hideLoading()
            }
        })
    });
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061

需要的几种请求方式

  1. GET
ts
              
/**
 * 封装get方法
 * @param url
 * @param data
 * @param loading 是否显示加载中
 * @returns {Promise}
 */
export function fetch<T>(url: string, params = {}, option?: IRequestOption): Promise<T> {
    return request<T>('GET', {
        url,
        params,
    }, option);
}
1234567891011121314
  1. POST
ts
             
/**
 * 封装post请求
 * @param url
 * @param data
 * @param loading 是否显示加载中
 * @returns {Promise}
 */
export function post<T>(url: string, data = {}, option?: IRequestOption): Promise<T> {
    return request<T>('POST', {
        url,
        data,
    });
}
12345678910111213
  1. PUT
ts
               

/**
 * 封装put请求
 * @param url
 * @param data
 * @param loading 是否显示加载中
 * @returns {Promise}
 */
export function put<T>(url: string, data = {}, option?: IRequestOption) {
    return request<T>('PUT', {
        url,
        data,
    }, option);
}
123456789101112131415
  1. DELETE
ts
           
/**
 * 删除请求
 * @param url 
 * @param loading 是否显示加载中
 */
export function deleteRequest<T>(url: string, option?: IRequestOption): Promise<T> {
    return request<T>('DELETE', {
        url,
    }, option);
}
1234567891011

请求头添加

自动添加登录令牌

ts
      
const token = wx.getStorageSync(TOKEN_KEY)
if (token) {
    // 加入登录令牌
    headers.Authorization = 'Bearer ' + token;
}
123456

响应处理

  1. 对令牌过期的处理

清除token 并跳转登录页面,但是一些页面又不想自动跳转到登录页,所以应该能传入设置

ts
      
if (statusCode === 401) {
    app && app.setToken();
    wx.navigateTo({
        url: '/pages/member/login'
    });
}
123456
  1. 登录推出的处理

  2. 相应失败的信息弹窗

  3. 请求加载进度的显示

有些地方不需要加载进度条,比如下拉加载更多,静默加载更好,不影响阅读

小程序Mini-Timer开发(一)功能介绍

2020-03-30 21:23:19

小程序Mini-Timer开发(一)功能介绍

主要功能

  1. 添加任务

有标题

有说明

有每次执行的时间

  1. 所有任务

支持批量终止

  1. 今日任务

从所有任务页面点击编辑进入编辑模式,可以多选添加到今日任务,可以连续点击添加多次

  1. 执行任务

开始:开始任务,并进行计时

暂停:暂停任务,停止计时

终止:中途有事要终止此次任务,计时重置,本次任务恢复

待开发功能

  1. 大任务及小任务
  2. 任务数据统计

用户相关

  1. 注册登录

支持小程序快捷登录

  1. 找回密码
  2. 个人信息及修改
  3. 第三方账户关联

小程序里只支持小程序的绑定,对其他绑定无法进行操作

  1. 账户注销
  2. 签到
  3. 消息列表

只支持查看列表,无法进入查看

  1. 扫一扫

支持扫码登录网页端

  1. 帮助

文章的搜索

文章的列表

文章的详情

  1. 反馈

支持反馈留言

  1. 设置

震动开启的设置

屏幕常亮的设置

typescript 开发小程序编译当前文件

2020-03-26 05:50:29

typescript 开发小程序编译当前文件

需求

typescript 开发微信小程序时,每次查看效果都很麻烦,编译过程太长了,怎么只编译当前编辑的文件呢?

工具

  1. vs code
  2. Code Runner 插件
  3. gulp

第一步

要把当前文件的路径传给 gulp

要在当前工作区增加一个设置

.vscode\settings.json

json
     
{
    "code-runner.executorMapByFileExtension": {
        ".ts": "gulp build --file=$fullFileName",
    },
}
12345

以下一些语言必须用以下方式才有用,用拓展名是没有用的

java, c, cpp, javascript, php, python, perl, ruby, go, lua, groovy, powershell, bat, shellscript, fsharp, csharp, vbscript, typescript, coffeescript, swift, r, clojure, haxe, objective-c, rust, racket, ahk, autoit, kotlin, dart, pascal, haskell, nim, d, lisp

json
     
{
    "code-runner.executorMap": {
        "typescript": "gulp build --file=$fullFileName",
    }
}
12345

Code Runner 有右键菜单项 Run Code 就是执行当前文件的

这样在 ts 文件就会运行命令 gulp build --file=

$fullFileName 表示文件的绝对路径

第二步

guipfile.js 中增加 build 任务

js
            
var gulp = require('gulp'),
    ts = require("gulp-typescript"),
    tsProject = ts.createProject('tsconfig.json');

gulp.task('build', async() => {
    // 这里必须取得相对路径,不然最终的生成文件就会在 dist 文件夹下加全路径
    var file = process.argv[3].substr(7).replace(__dirname + '\\', '');
    await gulp.src(file)
        .pipe(tsProject())
        .pipe(gulp.dest('dist'));
});
123456789101112

第三步

右键执行 ts 测试以下

增强版

如果我使用一个方法既可以编译所有的,又可以编译一个文件,该怎么做

修改guipfile.js

js
                               
var gulp = require('gulp'),
    ts = require("gulp-typescript"),
    tsProject = ts.createProject('tsconfig.json');

// 获取输入的路径
function getSrcPath(src) {
    if (process.argv.length < 4) {
        return src;
    }
    if (process.argv[2] !== 'build') {
        return src;
    }
    return process.argv[3].substr(7).replace(__dirname + '\\', '').replace('\\', '/');
}
// 获取输出的路径
function getDistPath(dist) {
    if (process.argv.length < 4) {
        return dist;
    }
    if (process.argv[2] !== 'build') {
        return dist;
    }
    return 'dist';
}

gulp.task('build', async() => {
    await gulp.src(getSrcPath('src/**/*.ts'))
        .pipe(tsProject())
        .pipe(gulp.dest(getDistPath('dist/')));
});
12345678910111213141516171819202122232425262728293031

这样执行 gulp build 即可编译所有 ts 文件

右键 Run Code 只编译当前 ts 文件

再也不用等很长时间了

VS2019 开发PHP 拓展(五)创建 class

2020-03-24 22:20:25

VS2019 开发PHP 拓展(五)创建 class

增加一个 application 类

zodream_application.h

定义类的方法

                   
#ifndef ZODREAM_APPLICATION_H
#define ZODREAM_APPLICATION_H

#ifndef ZO_BOOTED
#define ZO_BOOTED ZEND_STRL("booted")
#endif // !ZO_BOOTED


PHP_METHOD(ZodreamApplication, __construct);
PHP_METHOD(ZodreamApplication, __destruct);
PHP_METHOD(ZodreamApplication, version);
PHP_METHOD(ZodreamApplication, handle);

extern zend_class_entry* zo_application_ce;

ZEND_MINIT_FUNCTION(zodream_appliation);

#endif //ZODREAM_APPLICATION_H
12345678910111213141516171819

zodream_application.c

实现类的方法,并实现初始化

                                                           
#include "zodream_application.h"

zend_class_entry* zo_application_ce;

/* {{{ proto ZodreamApplication ZodreamApplication::__construct()
        Public constructor */
PHP_METHOD(ZodreamApplication, __construct)
{
    // 修改属性
    zend_update_property_bool(zo_application_ce, getThis(), ZO_BOOTED, TRUE);
}
/* }}} */

/* {{{ proto void ZodreamApplication::__destruct()
*/
PHP_METHOD(ZodreamApplication, __destruct)
{

}
/* }}} */

/* {{{ proto string ZodreamApplication::version()
*/
PHP_METHOD(ZodreamApplication, version)
{
    zend_string *retval = strpprintf(0, PHP_ZODREAM_VERSION);
    RETURN_STR(retval);
}
/* }}} */

/* {{{ proto void ZodreamApplication::handle()
*/
PHP_METHOD(ZodreamApplication, handle)
{
    zend_bool* booted = (zend_bool *)zend_read_property(zo_application_ce, getThis(), ZO_BOOTED, 0, NULL);
    RETURN_BOOL(booted);
}
/* }}} */

zend_function_entry zodream_application_methods[] = {
     PHP_ME(ZodreamApplication, __destruct, arginfo_void, ZEND_ACC_PUBLIC | ZEND_ACC_DTOR )
     PHP_ME(ZodreamApplication, __construct, arginfo_void, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
     PHP_ME(ZodreamApplication, version, arginfo_void, ZEND_ACC_PUBLIC)
     PHP_ME(ZodreamApplication, handle, arginfo_void, ZEND_ACC_PUBLIC)
     PHP_FE_END
};

ZEND_MINIT_FUNCTION(zodream_application)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Zodream\\Application", zodream_application_methods);
    zo_application_ce = zend_register_internal_class_ex(&ce, NULL);
    zo_application_ce->ce_flags |= ZEND_ACC_FINAL;

    zend_declare_property_bool(zo_application_ce, ZO_BOOTED, FALSE, ZEND_ACC_PROTECTED);
    zend_declare_class_constant_string(zo_application_ce, ZEND_STRL("VERSION"), PHP_ZODREAM_VERSION);

    return SUCCESS;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859

zodream.c

进行类的初始化

      
PHP_MINIT_FUNCTION(zodream)
{
    ZEND_MODULE_STARTUP_N(zodream_application)(INIT_FUNC_ARGS_PASSTHRU)
    return SUCCESS;
}
123456

VS2019 开发PHP 拓展(四)创建 function

2020-03-24 00:16:40

VS2019 开发PHP 拓展(四)创建 function

无参无返回

                
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test1, 0, 0, IS_VOID, 0)
ZEND_END_ARG_INFO()

/* {{{ void test1()
 */
PHP_FUNCTION(test1)
{
    ZEND_PARSE_PARAMETERS_NONE();

    php_printf("The extension %s is loaded and working!\r\n", "zodream");
}
/* }}} */
static const zend_function_entry zodream_functions[] = {
    PHP_FE(test1,       arginfo_test1)
    PHP_FE_END
};
12345678910111213141516

1参无返回

                       
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test2, 0, 0, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0)
ZEND_END_ARG_INFO()

/* {{{ void test2()
 */
PHP_FUNCTION(test2)
{
    char *var = "World";
    size_t var_len = sizeof("World") - 1;

    ZEND_PARSE_PARAMETERS_START(0, 1)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(var, var_len)
    ZEND_PARSE_PARAMETERS_END();

    php_printf("Hello %s\r\n", var);
}
/* }}} */
static const zend_function_entry zodream_functions[] = {
    PHP_FE(test2,       arginfo_test2)
    PHP_FE_END
};
1234567891011121314151617181920212223

多参无返回

                                  
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test3, 0, 0, IS_VOID, 2)
ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

/* {{{ void test3(string $str, array $config)
 */
PHP_FUNCTION(test3)
{
    zval* config, *entry;
    zend_ulong num_idx;
    zend_string* str_idx;
    char* var = "World";
    size_t var_len = sizeof("World") - 1;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STRING(var, var_len)
        Z_PARAM_ARRAY(config)
    ZEND_PARSE_PARAMETERS_END();

    php_printf("Hello %s\r\n", var);
    // 循环打印为字符串的值
    ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(config), num_idx, str_idx, entry) {
        ZVAL_DEREF(entry);
        if (Z_TYPE_P(entry) == IS_STRING) {
            php_printf("Hello %s\r\n", Z_STRVAL_P(entry));
        }
    } ZEND_HASH_FOREACH_END();
}
/* }}} */
static const zend_function_entry zodream_functions[] = {
    PHP_FE(test3,       arginfo_test3)
    PHP_FE_END
};
12345678910111213141516171819202122232425262728293031323334

无参有返回

                 
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test4, 0, 0, IS_STRING, 0)
ZEND_END_ARG_INFO()

/* {{{ void test4()
 */
PHP_FUNCTION(test4)
{
    ZEND_PARSE_PARAMETERS_NONE();

    zend_string* retval = strpprintf(0, "test 4");
    RETURN_STR(retval);
}
/* }}} */
static const zend_function_entry zodream_functions[] = {
    PHP_FE(test4,       arginfo_test4)
    PHP_FE_END
};
1234567891011121314151617

有参有返回

                          
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test5, 0, 0, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0)
ZEND_END_ARG_INFO()

/* {{{ void test5()
 */
PHP_FUNCTION(test5)
{
    char* var = "World";
    size_t var_len = sizeof("World") - 1;
    zend_string* retval;

    ZEND_PARSE_PARAMETERS_START(0, 1)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(var, var_len)
    ZEND_PARSE_PARAMETERS_END();

    retval = strpprintf(0, "Hello %s", var);

    RETURN_STR(retval);
}
/* }}} */
static const zend_function_entry zodream_functions[] = {
    PHP_FE(test5,       arginfo_test5)
    PHP_FE_END
};
1234567891011121314151617181920212223242526

VS2019 开发PHP 拓展(三)一些PHP定义

2020-03-24 00:15:37

VS2019 开发PHP 拓展(三)一些PHP定义

PHP_FUNCTION 定义一个方法

RETURN_NULL() 返回null

RETURN_LONG(l) 返回整型

RETURN_DOUBLE(d) 返回浮点型

RETURN_STR(s) 返回一个字符串。参数是一个zend_string * 指针

RETURN_STRING(s) 返回一个字符串。参数是一个char * 指针

RETURN_STRINGL(s, l) 返回一个字符串。第二个参数是字符串长度。

RETURN_EMPTY_STRING() 返回一个空字符串。

RETURN_ARR(r) 返回一个数组。参数是zend_array *指针。

RETURN_OBJ(r) 返回一个对象。参数是zend_object *指针。

RETURN_ZVAL(zv, copy, dtor) 返回任意类型。参数是 zval *指针。

RETURN_FALSE 返回false

RETURN_TRUE 返回true

PHP_MINIT_FUNCTION 初始化module时运行

PHP_MINIT

PHP_MSHUTDOWN_FUNCTION 当module被卸载时运行

PHP_MSHUTDOWN

PHP_RINIT_FUNCTION 当一个REQUEST请求初始化时运行, return SUCCESS; 返回FALIURE就不会加载这个扩展了

PHP_RINIT

PHP_RSHUTDOWN_FUNCTION 当一个REQUEST请求结束时运行

PHP_RSHUTDOWN

PHP_MINFO_FUNCTION 这个是设置phpinfo中这个模块的信息

PHP_MINFO

PHP_GINIT_FUNCTION 初始化全局变量时

PHP_GSHUTDOWN_FUNCTION 释放全局变量时

ZEND_PARSE_PARAMETERS_NONE 声明方法无参数值

ZEND_PARSE_PARAMETERS_START 获取方法的参数值,第一个参数表示必传的参数个数,第二个参数表示最多传入的参数个数。

Z_PARAM_OPTIONAL 在这个之后的参数都是可选参数

Z_PARAM_STRING 以字符串的形式获取参数值

ZEND_PARSE_PARAMETERS_END 获取参数值结束

方法

php_printf 打印字符串

未完待续

Git 及 Github 相关操作

2020-03-23 18:23:01

Git pull request

问题描述

在 github 上提交了一个 pull request,在作者进行操作前,发现自己某处错了,进行了修改。

这时是关闭这条 pull request 重新发一条,还是有什么操作可以覆盖这次发送的 pull request?

解决方案

push 更新那个分支就行,pull request只和分支名绑定。

直接 push 就会自动追加到到 PR 后面。当然,如果你不希保留旧的 commit 记录,还可以选择本地 git reset 之后 push -f 强行覆盖掉你远程的commit,PR会一并更新。

  1. 回退一个版本
shell
 
git reset HEAD~1
1
  1. 修改代码

  2. 强制提交

shell
 
git push -f
1

问题描述

如何把已经更新到最新的代码保持跟自己Fork下来的仓库代码保持一致

解决方法

  1. 命令查看此时本地仓库对应的远程仓库
     
    git remote -v
    1
  2. 为本地仓库添加一个新的远程仓库,指定名字为upstream,指向原项目仓库
     
    git remote add upstream https://github.com/php/php-src.git
    1

这是使用 git remote -v 则可以查看到已经更新了

  1. 保持同步更新
  
git pull upstream master          //从原仓库更新代码到的本地master分支
git push origin master            //将master推到自己的远程仓库
12

参考

  1. 从github上fork了一个仓库后保持与原仓库代码同步的操作方法
  2. Git实用技巧 pull request修改

GITHUB 客户端一些操作

撤回

  1. 未commit

Changes 下,右击想要操作的文件,点击菜单 Discard changes 即可,此操作会删掉对此文件的所有的修改

Discard changes

  1. 已commit,但是未push

History 下, 右击想要撤回的 commit,

github history

Amend commit 是修改本次提交的 描述信息

Undo commit 取消本次提交,可以重新修改文件和描述信息,不会留下记录

Revert changes in commit 撤回本次 commit, 实际上在反向提交一次 commit 删除了上次修改的内容,上次 commit修改的文件就恢复初始状态, 会丢失上次修改的内容,但是会留下记录

  1. 已commit,已push

只能 Revert changes in commit, 删除了上次修改的内容,会丢失上次修改的内容,同时会留下记录

VS2019 开发PHP 拓展(二)创建空拓展

2020-03-22 21:31:44

VS2019 开发PHP 拓展(二)创建空拓展

生成空的拓展

命令行进入 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext

这里zodream代表你的php扩展名

shell
 
php ext_skel.php --ext zodream
1

vs2019 载入项目

  1. 打开 Visual Studio 2019
  2. 选择 继续但无需代码
  3. 打开菜单 文件 -> 新建 -> 从现有代码创建项目
  4. 选择 Visual C++ 下一步
  5. 项目文件位置(拓展的目录D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\zodream),项目名称 phpzodream,下一步
  6. 项目类型选择 动态链接库(DLL)项目,完成
  7. 菜单栏选配置 Release x64
  8. 右键项目属性 -> C/C++ -> 常规 -> 附加包含目录 -> 编辑,加入目录
    
D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src
D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\main
D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\TSRM
D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\Zend
1234

属性 -> C/C++ -> 预处理器 -> 预处理器定义 -> 编辑 加入以下变量(其中ZODREAM替换成php扩展名

      
ZEND_DEBUG=0
PHP_EXTENSION
PHP_WIN32
ZEND_WIN32
HAVE_ZODREAM=1
COMPILE_DL_ZODREAM
123456

如果为开启线程安全 则加上

 
ZTS
1
  1. 生成

可能报错

  1. 如果提示LINK 1561: 必须定义入口点

属性 -> 常规 -> 配置类型 选择 动态库(.dll)

  1. E0020: 未定义标识符arginfo_test1 或者 缺少 zodream_arginfo.h C1083 无法打开包括文件: “zodream_arginfo.h”: No such file or directory

请复制 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\skeleton\skeleton_arginfo.hzodream_arginfo.h 即可

或者在 ext_skel.php 文件中的 copy_sources 方法 加上 'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h'

php
            
function copy_sources() {
    global $options;

    $files = [
            'skeleton.c'         => $options['ext'] . '.c',
            'skeleton.stub.php'  => $options['ext'] . '.stub.php',
            'php_skeleton.h'     => 'php_' . $options['ext'] . '.h',
            'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h'  // 加上这行就会自动生成
            ];

}
123456789101112
  1. LNK2019 无法解析的外部符号 __imp_zend_strpprintf

属性 -> 连接器 -> 输入 -> 附加依赖项 -> 编辑 加入一个php.lib,此文件在正式发布的PHP程序中 例如 D:\zodream\php\php-7.4-nts\dev\php7.lib

  1. E0020 未定义标识符 "zif_test2"

因为定义的方式 PHP_FUNCTION(zodream_test2) 但是引入时用的是 PHP_FE(test2, arginfo_test2)

改为

    
PHP_FUNCTION(test2)
{

1234

或者改 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\skeleton\skeleton.c

                  
/* {{{ string test2( [ string $var ] )
 */
PHP_FUNCTION(test2)
{
    char *var = "World";
    size_t var_len = sizeof("World") - 1;
    zend_string *retval;

    ZEND_PARSE_PARAMETERS_START(0, 1)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(var, var_len)
    ZEND_PARSE_PARAMETERS_END();

    retval = strpprintf(0, "Hello %s", var);

    RETURN_STR(retval);
}
123456789101112131415161718

使用

这是在正式时使用

把生成的 phpzodream.dll 复制到正式环境 ext 文件夹下,

修改 php.ini 引入插件

  
extension=zodream
12

重启 iis

当前为测试时

按【上一章】编译 php 即可,不需要在ini配置,直接使用即可

在php 代码加入

php
    
test1();

echo test2();
1234

查看效果

VS2019 开发PHP 拓展(一)环境准备

2020-03-22 21:30:37

VS2019 开发PHP 拓展(一)环境准备

准备工作

  1. 下载 “php-sdk-binary-tools

在右边的“Clone or download”点击,选择下方的“Download ZIP

  1. 下载 “PHP源码

此时最新版本是7.4.4,选择“php-7.4.4-src.zip”下载,

根据 Build your own PHP on Windows

Visual C++ 14.0 (Visual Studio 2015) for PHP 7.0 or PHP 7.1.
Visual C++ 15.0 (Visual Studio 2017) for PHP 7.2, PHP 7.3 or PHP 7.4.
Visual C++ 16.0 (Visual Studio 2019) for master.

即 vs2019 只能编译 php-src 的 master 分支,所以需要下载 master 分支源码,在右边的“Clone or download”点击,选择下方的“Download ZIP

  1. 下载 “Visual Studio 2019

选择 “社区” 下的 “免费下载” 进行下载,

  1. php-sdk-binary-tools-master.zip 解压到 D:\zodream\php-sdk-binary-tools

  2. 按住shift在编译目录内点击右键,选择“在此处打开Powershell窗口”;

  3. 执行”.\phpsdk-vs16-x64.bat”,成功后提示符从“>”变成“$”;

  4. 执行“phpsdk_buildtree phpdev”,成功后目录中会多一个“phpdev”目录,命令行的目录自动切换到“phpdev/vc16/x64”;

  5. 在“phpdev/vc16/x64”目录下新建php-src文件夹,将PHP源码复制到此目录;

  6. 切换到php-src目录(cd php-src),执行“phpsdk_deps -u”;

  7. 在“phpdev/vc16/x64”下建立pecl目录(与PHP源码目录同级),此目录放拓展的源码。如果为自己开发的拓展,请参考【下一章】。

编译流程

  1. 将拓展源码复制到该目录下(D:\zodream\php-sdk-binary-toolsphpdev\vc16\x64\pecl);

如果为自己开发的拓展,则不需要这一步,下一步会自动发现这些拓展,直接配置编译选项则可以

  1. 在PHP源码目录内(D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src)执行”buildconf”;
  2. configure --help 查看能够使用的配置选项,包括你自己的插件什么的
  3. 执行“configure –一些选项”命令配置编译选项,例如”configure –-disable-all –-enable-cli –-enable-cgi –-enable-zlib –-enable-session –-without-gd -–with-bz2 –-enable-yourext”;

编译 php

shell
 
configure --disable-all --enable-cli --enable-$remains
1

我的编译php 参数

shell
 
configure --disable-all --enable-cli --with-mysqlnd -–enable-cgi –-enable-zlib –-enable-session --with-bz2 --with-mysqli --enable-pdo --with-pdo-mysql --enable-redis --enable-zodream --enable-fileinfo --with-curl --with-gettext --enable-mbstring --with-openssl --with-imagick --with-pthreads
1

编译 PECL 拓展, 例如: apcu

shell
  
configure --disable-all --enable-cli --enable-apcu
12
  1. 执行nmake命令编译PHP及拓展,
  2. 如果希望压缩生成的 PHP 生成和扩展,则"nmake"之后也会运行:nmake snap
  3. 完成一些更改后重新编译, 清理旧的编译二进制文件 nmake clean, 如果需要更新"configure"脚本 buildconf --force, 然后重复编译 3、 4 步

编译成功后,在源码的X64目录下会生成“Release”或“Release_TS”目录,编译好的php.exe及生成的拓展dll均在此目录下。dll的文件名为php_xxxx.dll,例如“php_zodream.dll”。

默认编译出来的拓展是TS(线程安全)的版本(位于Release_TS目录中),如果要编译非线程安全版本,configure时加入“–disable-zts”选项。

XPath 语法

2020-03-15 18:34:02

XPath 语法

选取节点

/ 如果是在最前面,代表从根节点选取,否则选择某节点下的某个节点.只查询子一辈的节点

/html   查询到一个结果
/div    查询到0个结果,因为根节点以下只有一个html子节点
/html/body  查询到1个结果

// 查询所有子孙节点

//head/script
//div

./ 选取当前节点

../ 选取当前节点的父节点

@ 选取属性

//div[@id]  选择所有带有id属性的div元素
html
 
    <div id="s" class="left">
1
运算符/特殊字符 说明
/ 此路径运算符出现在模式开头时,表示应从根节点选择。
// 从当前节点开始递归下降,此路径运算符出现在模式开头时,表示应从根节点递归下降。
. 当前上下文。
.. 当前上下文节点父级。
* 通配符;选择所有元素节点与元素名无关。(不包括文本,注释,指令等节点,如果也要包含这些节点请用node()函数)
@ 属性名的前缀。
@* 选择所有属性,与名称无关。
: 命名空间分隔符;将命名空间前缀与元素名或属性名分隔。
( ) 括号运算符(优先级最高),强制运算优先级。
[ ] 应用筛选模式(即谓词,包括"过滤表达式"和"轴(向前/向后)")。
[1] 下标运算符;用于在集合中编制索引。1 表示数字
| 两个节点集合的联合,如://messages/message/to
- 减法。
div, 浮点除法。
and, or 逻辑运算。
mod 求余。
not() 逻辑非
= 等于
!= 不等于
特殊比较运算符 < 或者 <<= 或者 <=> 或者 >>= 或者 >=需要转义的时候必须使用转义的形式,如在XSLT中,而在XMLDOM的scripting中不需要转义。

谓语

谓语是用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中。

//body/div[1] body下的第一个div元素

//body/div[last()] body下的最后一个div元素

//body/div[position()<3] body下的位置小于3的元素

//div[@id] div下带id属性的元素

html
 
    <div id="s" class="left">
1

//input[@id="serverTime"] input下id="serverTime"的元素

||| |---|----| ancestor|选取当前节点的所有先辈(父、祖父等) ancestor-or-self|选取当前节点的所有先辈(父、祖父等)以及当前节点本身 attribute|选取当前节点的所有属性 child|选取当前节点的所有子元素。 descendant|选取当前节点的所有后代元素(子、孙等)。 descendant-or-self|选取当前节点的所有后代元素(子、孙等)以及当前节点本身。 following|选取文档中当前节点的结束标签之后的所有节点。 namespace|选取当前节点的所有命名空间节点 parent|选取当前节点的父节点。 preceding|直到所有这个节点的父辈节点,顺序选择每个父辈节点前的所有同级节点 preceding-sibling|选取当前节点之前的所有同级节点。 self|选取当前节点。

模糊匹配

//div[contains(@class,'f1')] div的class属性带有f1的

通配符 *

//body/* body下面所有的元素

//div[@*] 只要有用属性的div元素

//div[@id='footer'] //div 带有id='footer'属性的div下的所有div元素

//div[@class='job_bt'] //dd[@class='job-advantage']

运算符

//div[@class='job_detail'] and @id='job_tent'

//book/title | //book/price 选取 book 元素的所有 title 和 price 元素。

.//a/text() 当前标签下所有a标签的文字内容

//tr[position()>1 and position()<11] 位置大于1小于11

需要注意的知识点

1./和//的区别:/代表子节点,//代表子孙节点,//用的比较多

2.contains有时候某个属性中包含了多个值,那么使用contains函数 //div[contains(@class,'lg')]

3.谓语中的下标是从1开始的,不是从0开始的

常用表达式实例:

||| |----|----| /|Document Root文档根. /|选择文档根下面的所有元素节点,即根节点(XML文档只有一个根节点) /node()|根元素下所有的节点(包括文本节点,注释节点等) /text()|查找文档根节点下的所有文本节点 /messages/message|messages节点下的所有message节点 /messages/message[1]|messages节点下的第一个message节点 /messages/message[1]/self::node()|第一个message节点(self轴表示自身,node()表示选择所有节点) /messages/message[1]/node()|第一个message节点下的所有子节点 /messages/message[1]/[last()]|第一个message节点的最后一个子节点 /messages/message[1]/[last()]|Error,谓词前必须是节点或节点集 /messages/message[1]/node()[last()]|第一个message节点的最后一个子节点 /messages/message[1]/text()|第一个message节点的所有子节点 /messages/message[1]//text()|第一个message节点下递归下降查找所有的文本节点(无限深度) |/messages/message[1] /child::node()| /messages/message[1] /node()| /messages/message[position()=1]/node()| //message[@id=1] /node()|第一个message节点下的所有子节点 //message[@id=1] //child::node()|递归所有子节点(无限深度) //message[position()=1]/node()|选择id=1的message节点以及id=0的message节点 /messages/message[1] /parent::|Messages节点 /messages/message[1]/body/attachments/parent::node()| /messages/message[1]/body/attachments/parent:: | /messages/message[1]/body/attachments/..|attachments节点的父节点。父节点只有一个,所以node()和 返回结果一样。(..也表示父节点. 表示自身节点) //message[@id=0]/ancestor::|Ancestor轴表示所有的祖辈,父,祖父等。向上递归 //message[@id=0]/ancestor-or-self::|向上递归,包含自身 //message[@id=0]/ancestor::node()|对比使用,多一个文档根元素(Document root) /messages/message[1]/descendant::node()| //messages/message[1]//node()|递归下降查找message节点的所有节点 /messages/message[1]/sender/following::|查找第一个message节点的sender节点后的所有同级节点,并对每一个同级节点递归向下查找。 //message[@id=1]/sender/following-sibling::|查找id=1的message节点的sender节点的所有后续的同级节点。 //message[@id=1]/datetime/@date|查找id=1的message节点的datetime节点的date属性 //message[@id=1]/datetime[@date]| //message/datetime[attribute::date]|查找id=1的message节点的所有含有date属性的datetime节点 //message[datetime]|查找所有含有datetime节点的message节点 //message/datetime/attribute::| //message/datetime/attribute::node()| //message/datetime/@|返回message节点下datetime节点的所有属性节点 //message/datetime[attribute::]| //message/datetime[attribute::node()]| //message/datetime[@]| //message/datetime[@node()]|选择所有含有属性的datetime节点 //attribute::|选择根节点下的所有属性节点 //message[@id=0]/body/preceding::node()|顺序选择body节点所在节点前的所有同级节点。(查找顺序为:先找到body节点的顶级节点(根节点),得到根节点标签前的所有同级节点,执行完成后继续向下一级,顺序得到该节点标签前的所有同级节点,依次类推。)注意:查找同级节点是顺序查找,而不是递归查找。 //message[@id=0]/body/preceding-sibling::node()|顺序查找body标签前的所有同级节点。(和上例一个最大的区别是:不从最顶层开始到body节点逐层查找。我们可以理解成少了一个循环,而只查找当前节点前的同级节点) //message[@id=1]//[namespace::amazon]|查找id=1的所有message节点下的所有命名空间为amazon的节点。 //namespace::|文档中的所有的命名空间节点。(包括默认命名空间xmlns:xml) //message[@id=0]//books/[local-name()='book']|选择books下的所有的book节点,注意:由于book节点定义了命名空间<amazone:book>.若写成//message[@id=0]//books/book则查找不出任何节点。 //message[@id=0]//books/[local-name()='book' and namespace-uri()='http://www.amazon.com/books/schema']|选择books下的所有的book节点,(节点名和命名空间都匹配) //message[@id=0]//books/[local-name()='book'][year>2006]|选择year节点值>2006的book节点 //message[@id=0]//books/*[local-name()='book'][1]/year>2006|指示第一个book节点的year节点值是否大于2006.返回xs:boolean: true

参考来源

  1. Xpath语法格式整理

WPF/UWP 下图标字体文件使用

2020-03-13 02:52:35

WPF/UWP 下图标字体文件使用

WPF 下使用FontAwesome

  1. 先把 fontawesome.ttf 放到 Fonts 文件夹中,设置文件属性“如果较新则复制”
  2. 在 xaml 中声明一个字体资源
xml
   
<FontFamily x:Key="FontAwesome">
        pack://application:,,,/Fonts/#FontAwesome
</FontFamily>
123
  1. 绑定属性
xml
 
<Setter Property="FontFamily" Value="{StaticResource FontAwesome}"/>
1
  1. 使用
xml
 
<TextBlock Text="" Style="{DynamicResource FontAwesome}" />
1
c#
   

tb.Text = "\uf000";//Char('\uf15b');
123

或者直接合并234步,按照 /命名空间;component/[路径]#[字体名称] 这个格式写

xml
 
<TextBlock Text="" FontFamily="/ZoDream;component/Fonts#FontAwesome" />
1

UWP 下使用FontAwesome

xml
 
<TextBlock Text="" FontFamily="Fonts/FontAwesome.ttf#FontAwesome" />
1

iconfont 字体使用同理

#字体名称 改成 #iconfont 即可

UWP 下iconfont

xml
 
<TextBlock Text="" FontFamily="Fonts/iconfont.ttf#iconfont" />
1

登录“记住我”的实现

2020-03-12 01:16:51

登录“记住我”的实现

原理

用户登录时勾选 记住我,服务器生成随机token 加用户id 组成字符串永久保存到浏览器cookie 中,下一次从浏览器获取并提取token和id 获取用户登录即可

数据

用户表需要两个关键字段 idremember_token

id 是查询用户的主键 remember_token 为随机生成的字符串

程序

需要方法

go
            
bool login(User user, bool remember);// 登录

bool logout();// 登出并清除cookie

bool setRememberToken(User user);// 生成token 保存到数据库并设置cookie

[int, string] getRememberToken();// 从cookie获取 id 和token


User findByIdentity(int id);// 根据id 获取用户

User findByRememberToken(int id, string token);// 根据id 和token获取用户
123456789101112

wordpress 学习(四) 主循环

2020-03-12 01:00:32

wordpress 学习(四) 主循环

have_posts() 判断是否有文章

get_post_format() 获取文章的类型 aside chat gallery link image quote status video audio 标准 日志 链接 相册 状态 引语 图像

the_post() the_post()函数则调用 $wp_query->the_post()成员函数前移循环计数器,并且创建一个全局变量$post(不是$posts),把当前的post的所有信息都填进这个$post变量中,以备接下来使用。

简单使用用 the_content 正文 the_title 文章标题 the_time 时间 the_category 文章的分类

the_author_posts_link() 输出作者及链接

the_permalink() 输出文章的网址

The loop starts here:

<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>

and ends here:

<?php endwhile; else : ?> <p><?php _e( 'Sorry, no posts matched your criteria.' ); ?></p> <?php endif; ?>

隐藏排除显示特定类别 这里排除 3和 8类

<?php $query = new WP_Query( 'cat=-3,-8' ); ?>

<?php if ( $query->have_posts() ) : while ( $query->have_posts() ) : $query->the_post(); ?>

the_time('Y年m月j日')

a代表小写的英语的上下午,如am、pm

A代表大写的英语的上下午,如AM、PM

d代表英语的日期(小于10仍为两位数写法),如05、12

D代表中文的星期,如五、七

F代表中文的月份(包括“月”这个字),如五月、十二月

g代表英语的小时(小于10为一位数写法),如5、12

G代表英语的小时(小于10仍为两位数写法),如05、12

h代表英语的分钟(小于10为一位数写法),如5、12

H代表英语的分钟(小于10仍为两位数写法),如05、12

j代表英语的日期(小于10为一位数写法),如05、12

l代表中文的星期(包括“星期”这两个字),如星期五、星期七

m代表英语的月份(小于10仍为两位数写法),如05、12

M代表英语的月份(以单词的形式显示),如Jun

n代表英语的月份(小于10为一位数写法),如5、12

O代表英语的时区,如+0800

r代表完整的日期时间,如Tue, 06 Jun 2006 18:37:11 +0800

S代表日期的序数后缀,如st、th

T代表英语的时区(以单词的形式显示),如CST

w代表英语的星期,如5、7

W代表周数,如23

y代表两位数年份,如07、08

Y代表四位数年份,如2007、2008

z代表天数,如156

<?php the_content( $more_link_text, $strip_teaser, $more_file ); ?> get_the_content( $more_link_text, $stripteaser, $more_file )

参数

$more_link_text

(字符串)(可选)“more”链接的链接文本

默认值: '(more...)'

$strip_teaser

(布尔型)(可选)显示(FALSE)或隐藏(TRUE)more链接前的文本。

默认值:FALSE

$more_file

(字符串)(可选)more链接所指向的文件

默认值:当前文件

the_title( $before, $after, $echo ); get_the_title() 通过文章ID返回文章标题

$before 字符串型,标题之前放置的文本,默认是空

$after 字符串型,标题之后放置的文件,默认是空

$echo 逻辑型,true表示显示标题,false表示返回它并用在PHP中,默 认为true.

the_title_attribute() 也是取出标题,但会过滤一些特殊字符

global $post;

echo $post->post_title;

通过post这个全局变量还可以让你获取:ID,post_author,post_date,post_excerpt,comment_count 和其他。

the_category( $separator, $parents, $post_id )

$separator 每个类别的链接之间显示的文本或字符。默认情况下,链接放置在HTML的无序列表。一个空字符串将导致默认行为。默认:空字符串

$parents

'multiple'显示单独指向父表和子类别,展示"父项/子项"关系。▪'single'--仅显示子类别链接,链接文本展示"父项/子项"关系。默认:空字符串注意:默认是一个链接到子类别,没有显示关系。

$post_id 帖子ID来检索类别。 在当前职位的类别列表中的默认值false结果。

comments_popup_link( $zero, $one, $more, $css_class, $none ); 添加一个链接

$zero 没有评论时显示

$one 只有一条评论是显示

$more 用“ % ”显示条数,有多条评论时显示

$css_class class=" "

$none 当评论功能被关闭时 显示

wordpress 学习(三) 添加js

2020-03-12 00:59:52

wordpress 学习(三) 添加js

wp_enqueue_script($handle,$src,$deps,$ver,$in_footer)

$handle (必需)命名

$src 路径

$deps 依赖

$ver 版本号

$in_footer 默认false 放在<head>里, true放在body下方 必需有 wp_footer()函数

wp_localize_script($handle,$object_name,$l10n) 脚本本地化

$handle 脚本名

$object_name 脚本变量名

$l10n 要本地化的数据本身 array()

例如

php
     
wp_localize_script( 'nways-script', 'screenReaderText', array(
        'expand'   => '<span class="screen-reader-text">' . __( 'expand child menu', 'nways' ) . '</span>',
        'collapse' => '<span class="screen-reader-text">' . __( 'collapse child menu', 'nways' ) . '</span>',
    ) );
12345

wordpress 学习(二)添加css

2020-03-12 00:56:47

wordpress 学习(二)添加css

wq_enqueue_style($handle,$src,$deps,$ver,$media);

$handle (必需)给样式命名

$src 路径 get_stylesheet_uri() 默认会加载 style.css

$deps 依赖关系,默认false 不存在依赖, array();接受依赖样式名

$ver 版本号 避免用户端的缓存而使样式不刷新 例如:zx.css?ver=3.2

$media 为指定媒体制定的样式 all
braille embossed handheld print projection screen speech tty tv 例如

css
      
@media screen and min width 400{
    .hh{
        color:#fff;
    }
}
123456

wp_register_style( $handle, $src, $deps, $ver, $media ); 注册样式 再使用wp_enqueue_style($handle); 排队加载

也可以使用 function add_stylesheet_to_head() { echo "<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>"; }

add_action( 'wp_head', 'add_stylesheet_to_head' ); 但无法检查CSS文件是否已经被包含在页面中

wp_enqueue_scripts 用来在网站前台加载脚本和CSS admin_enqueue_scripts 用来在后台加载脚本和CSS login_enqueue_scripts 用来在WP登录页面加载脚本和CSS

例如:function nways(){ wp_enqueue_style(); }

add_action("wp_enqueue_scripts","nways");

添加动态内联样式:wp_add_inline_style()

如果你的主题有选项可自定义主题的样式,你可以使用 wp_add_inline_style() 函数来打印内置的样式:

<?php

function mytheme_custom_styles() { wp_enqueue_style( 'custom-style', get_template_directory_uri() . '/css/custom-style.css' ); $bold_headlines = get_theme_mod( 'headline-font-weight' ); // 比方说,它的值是粗体“bold” $custom_inline_style = '.headline { font-weight: ' . $bold_headlines . '; }'; wp_add_inline_style( 'custom-style', $custom_inline_style ); } add_action( 'wp_enqueue_scripts', 'mytheme_custom_styles' );

?>

wp_style_is( $handle, $state ); 检查样式的排队状况

wp_style_add_data($handle,$key,$value) 添加元数据到你的样式中,包括条件注释、RTL的支持和更多!

注销样式文件:wp_deregister_style() 例如 if(wp_style_is("boostop.min","regitered")) { wp_deregister_style('boostop.min'); }

wp_dequeue_style() 取消已经排列的样式表

wordpress学习(一)

2020-03-08 06:22:16

wordpress学习(一)

__() ,翻译,如果不能就返回原始文本

esc_url() 检查url

home_url() 加上域名组成完整网址,

<?php bloginfo(’name’); ?> : 博客名称(Title)

<?php bloginfo(’stylesheet_url’); ?> : CSS文件路径

<?php bloginfo(’pingback_url’); ?> : PingBack Url

<?php bloginfo(’template_url’); ?> : 模板文件路径

<?php bloginfo(’version’); ?> : WordPress版本

<?php bloginfo(’atom_url’); ?> : Atom Url

<?php bloginfo(’rss2_url’); ?> : RSS 2.o Url

<?php bloginfo(’url’); ?> : 博客 Url

<?php bloginfo(’html_type’); ?> : 博客网页Html类型

<?php bloginfo(’charset’); ?> : 博客网页编码

<?php bloginfo(’description’); ?> : 博客描述

get_bloginfo() 返回博客的信息

||| |---|--| |_e() | 显示翻译文字 |has_nav_menu() | 注册的导航菜单是否已经分配了位置 |wp_nav_menu() | 展示一个导航菜单 |is_active_sidebar() | 判断导航区是否正在使用 |dynamic_sidebar() | 显示动态栏 |get_template_directory() | 得到当前主题的路径

get_permalink() 用来根据固定连接返回文章或者页面的链接。在获取链接时 get_permalink() 函数需要知道要获取的文章的 ID,如果在循环中则自动默认使用当前文章。

用法: <?phpthe_title( $before, $after, $echo ); ?>

参数:

$before 字符串型,标题之前放置的文本,默认是空

$after 字符串型,标题之后放置的文件,默认是空

$echo 逻辑型,true表示显示标题,false表示返回它并用在PHP中,默认为true.

the_excerpt()函数获取文章摘要

get_post_type() 函数用来获取文章的文章类型

edit_post_link( __( '编辑', 'nways' ), '<span class="edit-link">', '</span>' ); ?>编辑当前文章的连接

网页缓存考虑整理

2020-02-28 01:17:02

网页缓存考虑整理

缓存只是为了保存更新频率不高的页面,以减少对数据库的读写压力及显示效率

根据页面更新频率进行缓存

每一个页面更新的频率不一样,比如文章基本不进行内容修改,这可以进行缓存

局部缓存及整页缓存

局部缓存缺点:无法做到 MVC 分离

整页缓存:一些属性无法更新,比如阅读量

对资源引用

整页缓存需要注意 http 及 https 的变化,https 下浏览器无法使用 http 资源样式

域名变化,比如 同一个顶级域名不带 www 和 带www 页面和资源样式是不能使用的

对路径及影响参数需要加入不同缓存条件

比如 是否登录 是否影响页面

不同语言是否影响多语言的使用

不同id 是否调用不同的文章

评论

评论一定要使用 ajax 异步调用

vscode 搭配 gcc 进行c语言学习

2020-02-27 19:28:27

vscode 搭配 gcc 进行c语言学习

通过 MinGW 安装 gcc

版本较老

  1. 安装MinGW

进入 http://www.mingw.org 下载 mingw-get-setup.exe 并进行安装

  1. 安装 gcc

在 MinGW Installation Manager 中选择 mingw32-gcc-g++-bin 点击菜单 Installation -> Apply Changes -> Apply

搜索 编辑系统环境变量 在 Path 中添加 MinGW 的bin文件夹, 例如 C:\MinGW\bin

在 cmd 中输入 gcc -v 查看是否安装成功

通过 Msys2 安装 gcc

版本最新

进入 https://www.msys2.org/ 下载 msys2-x86_64

修改pacman源,参考 https://mirrors.tuna.tsinghua.edu.cn/help/msys2/

pacman基本命令

     
pacman -Sy 更新软件包数据 
pacman -Syu 更新所有 
pacman -Ss xx 查询软件xx的信息 
pacman -S xx 安装软件xx
pacman -R xx 删除软件xx
12345

下载make

 
pacman -S make
1

安装gcc、g++编译器

    
#查询并找到msys/gcc
pacman -Ss gcc
#安装
pacman -S msys/gcc
1234

vscode 安装相应插件

Code Runner

配置

json
     
{
    "code-runner.executorMap": {
        "c": "cd $dir && gcc $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt"
    }
}
12345

C/C++

截取html

2019-12-22 22:31:10

截取html

需求

从一段html 中截取指定长度的内容,

要求:

  1. 长度为不包括html标签的内容长度,即 innerTEXT 长度
  2. 保留相关标签,并进行闭合

代码

简单版代码

php
                                                                              

/**
 * 截取html, 标签不计入长度,自动闭合标签
 * @param string $html
 * @param int $length
 * @param string $endWith
 * @bug 本方法缺陷: 未进行严格标签判断 例如 < <gg data="<a>"
 * @example ::substr('<p>1111<div/>111<br>111<i class="444">11</i>111 55555</p>', 12)
 * @return string
 */
public static function substr(string $html, int $length, string $endWith = '...'): string {
    if ($length < 1) {
        return $endWith;
    }
    $maxLength = mb_strlen($html);
    if ($maxLength < $length) {
        return $html;
    }
    $result = '';
    $n = 0;
    $unClosedTags = [];
    $isCode = false; // 是不是HTML代码
    $isHTML = false; // 是不是HTML特殊字符,如
    $notClosedTags = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'link', 'meta', 'param', 'embed', 'command', 'keygen', 'source', 'track', 'wbr'];
    $tag = '';
    for ($i = 0; $i < $maxLength; $i++) {
        $char = mb_substr($html, $i, 1);
        if ($char == '<') {
            // 进入标签
            $isCode = true;
            $tag = '';
        }
        else if ($char == '&') {
            $isHTML = true;
        }
        else if ($char == '>' && $isCode) {
            $n = $n - 1;
            $isCode = false;
            $tag = explode(' ', $tag, 2)[0];
            if (substr($tag, 0, 1) === '/') {
                // 判断是否时结束标签, 倒序找到邻近开始标签,进行移除
                for ($j = count($unClosedTags) - 1; $j >= 0; $j --) {
                    if ($tag === $unClosedTags[$j]) {
                        $unClosedTags = array_splice($unClosedTags, 0, $j - 1);
                        break;
                    }
                }
                $tag = '';
            }
            if (!empty($tag) &&
                substr($tag, strlen($tag) - 1, 1) !== '/'
                && !in_array(strtolower($tag), $notClosedTags)) {
                // 不是结束标签且不是自闭合且不是无需闭合把标签加入
                $unClosedTags[] = $tag;
            }
            $tag = '';
        }
        else if ($char == ';' && $isHTML) {
            $isHTML = false;
        }
        if ($isCode && ($tag !== '' || $char !== '<')) {
            $tag .= $char;
        }
        if (!$isCode && !$isHTML && $char !== ' ') {
            $n = $n + 1;
        }
        $result .= $char;
        if ($n >= $length) {
            break;
        }
    }
    $result .= $endWith;
    for ($j = count($unClosedTags) - 1; $j >= 0; $j --) {
        $result .= sprintf('</%s>', $unClosedTags[$j]);
    }
    return $result;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778

此代码存在的问题

  1. 未对 < 进行验证是否真的为标签开始
  2. 未对标签属性值中可能出现的标记 进行过滤

相关需求

  1. 指定行的截取

MySql 数据库引擎 MyISAM与InnoDB 怎么选择

2019-12-22 22:27:10

MySql 数据库引擎 MyISAM与InnoDB 怎么选择

介绍

InnoDB

Mysql 5.5 版本开始, InnoDB是默认的表存储引擎, 其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU

MyISAM

基于传统的ISAM类型, ISAM是Indexed Sequential Access Method (有索引的顺序访问方法) 的缩写,它是存储记录和文件的标准方法

比较

MyISAM InnoDB
支持事务、回滚 不支持 支持,默认封装成事务提交,多条最好使用一个事务
支持外键 不支持 支持
表级锁 表、行(默认)级锁,行锁是实现在索引上,可能会导致“死锁”
全文索引 支持 5.6 以后支持(使用sphinx插件更好)
count(*) 事先保存表的总行数 遍历(加了wehre则一样)
索引、主键 允许没有索引和主键,索引都是保存行的地址 没有则生成一个不可见的6字节的主键
安全性 更安全
高并发 容易表损坏 效率更好
巨大数据量 利用CPU效率更高
查询、更新、插入的效率 更高
加索引查询 更快
加索引更新 慢1/2 慢1/30
内存、空间 占用更大
存储文件 frm是表定义文件,myd是数据文件,myi是索引文件 frm是表定义文件,ibd是数据文件

测试:

测试环境:win10 MySQL8.0 php7.4

CPU 占用差不多、内存占用不高、机械硬盘写入跑满

sql
       

CREATE TABLE `test_log` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255) NOT NULL,
  `created_at` INT(10) NULL,
  PRIMARY KEY (`id`));
1234567

|引擎类型|MyISAM|InnoDB|性能相差 -------|-----|----|----|---| 循环插入1万记录|324.83178091049【√】|712.46580886841|1倍 事务插入1万记录|313.85579895973|1.7757570743561【√】|300倍 主键更新100次|2.9796991348267【√】|7.0310151576996|1倍 非主键更新100次|8.5254480838776|9.1009860038757|差不多 查询所有count100次|0.016912937164307【√】|0.25129389762878|20倍 where查询count100次|0.017408847808838|0.021618127822876|差不多 查询单条主键100次|0.018386125564575|0.018260955810547|差不多 查询单条非主键100次|0.27979707717896【√】|0.45667195320129|1倍 like查询100次|0.31684398651123【√】|0.49412107467651|0.5倍 删除单条主键100次|3.5270810127258【√】|7.0672898292542|1倍 删除单条非主键100次|4.6953358650208【√】|11.668270111084|2倍 删除所有|0.23814415931702【√】|0.81685304641724|3倍

php
                                                                         
use Zodream\Database\DB;
use Zodream\Debugger\Domain\Timer;

DB::getEngine();
$timer = new Timer();
// 循环插入 10 万 记录
for ($i = 0; $i < 10000; $i ++) {
    DB::insert(sprintf('INSERT INTO `test_log` (`name`, `created_at`) VALUES (\'test%s\', \'%s\');', $i, time()));
}
$timer->record('insert 10000');
// 事务插入 10 万 记录
DB::transaction(function (\Zodream\Database\Engine\Pdo $pdo) {
    for ($i = 0; $i < 10000; $i ++) {
        $pdo->getDriver()->exec(sprintf('INSERT INTO `test_log` (`name`, `created_at`) VALUES (\'test%s\', \'%s\');', $i, time()));
    }
});
$timer->record('insert trans 10000');

$data = DB::select('SHOW TABLE STATUS where Name=\'test_log\';');
$size = $data[0]['Data_length'] + $data[0]['Index_length'];
$timer->record('free '. $size);
echo $size;
// 更新 主键
for ($i = 0; $i < 100; $i ++) {
    DB::update(sprintf('UPDATE `test_log` SET `name`=\'test_ttt_%s\' WHERE `id`=\'%s\';', $i, $i * 50 + 5));
}
$timer->record('update');
// 更新 非主键
for ($i = 0; $i < 100; $i ++) {
    DB::update(sprintf('UPDATE `test_log` SET `created_at`=\'%s\' WHERE `name`=\'test%s\';', $i, $i * 30 + 4));
}
$timer->record('update name');
// 查询 所有 count
for ($i = 0; $i < 100; $i ++) {
    DB::select('SELECT count(*) as count FROM test_log;');
}
$timer->record('select count');
// 查询 where count
for ($i = 0; $i < 100; $i ++) {
    DB::select(sprintf('SELECT count(*) as count FROM test_log WHERE `id`=\'%s\';', $i * 40 + 3));
}
$timer->record('select count where');
// 查询 单条主键
for ($i = 0; $i < 100; $i ++) {
    DB::select(sprintf('SELECT * FROM test_log WHERE `id`=\'%s\';', $i * 40 + 3));
}
$timer->record('select one');
// 查询 单条非主键
for ($i = 0; $i < 100; $i ++) {
    DB::select(sprintf('SELECT * FROM test_log WHERE `name`=\'test%s\';', $i * 40 + 3));
}
$timer->record('select one name');
// 查询 like
for ($i = 0; $i < 100; $i ++) {
    DB::select(sprintf('SELECT * FROM test_log WHERE `name` like \'%%%s%%\';', $i * 40 + 3));
}
$timer->record('select like');
// 删除 单条主键
for ($i = 0; $i < 100; $i ++) {
    DB::delete(sprintf('DELETE FROM `test_log` WHERE `id`=\'%s\';', $i * 60 + 3));
}
$timer->record('delete');
// 删除 单条非主键
for ($i = 0; $i < 100; $i ++) {
    DB::delete(sprintf('DELETE FROM `test_log` WHERE `name`=\'test%s\';', $i * 60 + 3));
}
$timer->record('delete name');
// 删除所有
DB::delete('DELETE FROM `test_log` WHERE 1');
$timer->record('delete all');
$timer->end();
$timer->log();
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273

实际选择

思考方向:

  1. 数据库是否有外键:innodb 支持
  2. 是否需要事务支持:innodb 支持
  3. 用什么样的查询模式:如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB
  4. 数据有多大
  5. MyISAM恢复起来更困难

日志【MyISAM】

只插入,不修改

小型的应用或项目【MyISAM】

实在不会选,那么直接按项目的大小来选择

参考

  1. MyISAM与InnoDB两者之间区别与选择,详细总结,性能对比
  2. MySQL 8.0 Reference Manual: The MyISAM Storage Engine
  3. MySQL 8.0 Reference Manual: Chapter 15 The InnoDB Storage Engine
  4. MyISAM与InnoDB 的区别(9个不同点)

centos + nginx + php + vsftp 实现不同路径

2019-12-09 18:50:45

centos + nginx + php + vsftp 实现不同路径

nginx 配置

具体配置请参考 【nginx 子目录匹配不同地方的文件夹

必须提升权限

nginx.conf

conf
 
user root;
1

这样访问 vsftp 中的文件 html 或 js 是正常的

但访问 php 文件就会报 File not found 404 错误

开启 nginx 的 error_log 错误日志 会有一条这样的日志

 
FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream.
1

php-fpm 配置

此时应该提升 php-fpm 的权限

php-fpm.conf

conf
  
user = root
group = root
12

但是启动 php-fpm 启动不了

 
please specify user and group other than root
1

不允许使用 root 权限,但她有一个启动参数 -R 允许使用 root

bat
 
 /etc/init.d/php-fpm start -R
1

这样还是不行

找到 /etc/init.d/php-fpm 找到 start() 方法, 加上 -R 就行了

bat
  
daemon --pidfile ${pidfile} /usr/local/php/sbin/php-fpm -R --daemonize
12

重启 /etc/init.d/php-fpm restart

再次访问php文件,正常了

php soap 访问.net Web 服务

2019-12-05 05:29:39

php soap 访问.net Web 服务

一开始使用soap 连接是报 Couldn't load from 'xxxx' : Premature end of data in tag html line

发现是参数设的不对,SoapClient第一个参数使用的是正式请求网址,SoapHeader 第一个参数设的namespace,不能使用默认的 http://tempuri.org/, 不然 headerbodynamespace 相同 ,header 就不会生成加入到xml请求内容中。

__soapCall 的第二个参数传递的是方法的所有参数,类似于 ...args, 所以必须用数组包起来 [参数1, 参数2, ...]

php
      
$client = new SoapClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]);
$header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [
    'User' => 'zodream',
    'Password' => 'zodream'
]);
$res = $client->__soapCall($path, [$data], null, $header);
123456

但是老是报错 Could not connect to host

按照搜到的方法改了 php.ini

ini
   
soap.wsdl_cache_enabled=0
soap.wsdl_cache_ttl=0
soap.wsdl_cache_limit = 0
123

发现还不行,再次修改代码

php
         
$client = new SoapClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]);
$client->soap_defencoding = 'utf-8';  
$client->decode_utf8 = false;   
$client->xml_encoding = 'utf-8'; 
$header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [
    'User' => 'zodream',
    'Password' => 'zodream'
]);
$res = $client->__soapCall($path, [$data], null, $header);
123456789

还是不行,有尝试过使用 fsockopen 发现连接直接超时

然后用 postman 测试发现需要加上 Content-Type: text/xml;charset=utf-8 服务端才能接收,否则响应 服务器无法为请求提供服务,因为不支持该媒体类型。, 最后没办法只能修改请求方法

php
                           
$client = new ZoClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]);
$client->soap_defencoding = 'utf-8';  
$client->decode_utf8 = false;   
$client->xml_encoding = 'utf-8'; 
$header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [
    'User' => 'zodream',
    'Password' => 'zodream'
]);
$res = $client->__soapCall($path, [$data], null, $header);

class ZoClient extends SoapClient {
    public function __doRequest($request, $location, $action, $version, $one_way = 0) {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $location);
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_HTTPHEADER, [
            'Content-Type: text/xml;charset=utf-8',
            'SoapAction: '.$action
        ]);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
        $response = curl_exec($curl);
        curl_close($curl);
        return $response;
    }
}
123456789101112131415161718192021222324252627

虽然可以直接使用curl自己写,但拼接soapxml有点麻烦,转化响应也麻烦,直接使用soap插件就不麻烦了。

可视化编辑之组件思路

2019-12-01 06:49:29

先进行首页组件化测试

页面增加静态化设置,是否静态化,允许设置更新时间,对缓存页面设置过期时间,这样就不需要访问数据库查询是否过期。

组件内容更新,通过事件通知,更新时通过组件反推相关的页面,进行页面更新。

vue 返回上一页回到原先滚动位置

2019-11-16 00:57:07

vue 返回上一页回到原先滚动位置

在一些页面,比如滚动加载的页面默认无法后退保存之前的滚动位置。但需要这个功能该怎么做?

App.vue

html
    
<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
1234

router

在路由中需加上 keepAlive: true

js
        
{
    name: 'search',
    path: '/search',
    component: Search,
    meta: {
        keepAlive: true, // 需要缓存
    },
}, 
12345678

页面上

在需要的页面 .vue 里加上事件,mounted 事件只在新进入时触发,或离开页面后缓存页面时触发(这时是获取不到任何页面元素的)

js
                
data() {
    return {
        scrollTop: 0,
    };
},
beforeRouteLeave (to, from, next) {
    // this.scrollTop = document.querySelector('.scroll-box').scrollTop; // div内部滚动的
    this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    next();
},
beforeRouteEnter(to, from, next) {
    next(vm => {
        // document.querySelector('.scroll-box').scrollTop = vm.scrollTop;// div内部滚动的
        document.body.scrollTop = vm.scrollTop;
    });
},
12345678910111213141516

注意:以上事件只能放到页面上面,不能放到页面上的部件里

参考

  1. vue返回上一页面时回到原先滚动的位置

nginx 子目录匹配不同地方的文件夹

2019-11-02 02:41:24

nginx 子目录匹配不同地方的文件夹

要求

/ 对应 /data/www

/shop 对应 /home/www/shop

/task 对应 /home/task1

/shop/h5 对应 /data/www/shop/h5

/bbs/index.php 对应 /data/bbs/index.php

第一个

                                                          
server
{
    listen       80 default;
    server_name  zodream.cn;
    rewrite ^(.*)$ https://${server_name}$1 permanent; # 强制使用https 访问
}

server
{
    listen       443 ssl;
    server_name  zodream.cn;
    index index.html index.htm index.php;
    root  /data/www;
    ssl_certificate /data/ssl/zodream.cn.pem;
    ssl_certificate_key /data/ssl/zodream.cn.key;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ALL:!DH:!EXPORT:!RC4:+HIGH:+MEDIUM:!LOW:!aNULL:!eNULL;

    location / {
    }

    location ~ ^/shop/h5/.+\.php {
        root /data/www;
        include php_fcgi.conf;
    }


    location ~ ^/shop/h5.* {
        root /data/www;
    }

    location /shop {
        root /home/www; # 会访问 /home/www/shop 文件下的所有html文件,不能访问其他类型文件
    }

    location ~ ^/shop {
        root /home/www; # 会访问 /home/www/shop 文件下的所有类型的文件
    }


    location ~ ^/task.* {
        root /home/task1;
        rewrite ^/task(.*)$ /$1 break;    # 这种方法跟上一种方法效果一样但不需要保持文件名一致
    }

    location ~ ^/bbs/.*\.php.* {       # 此方法存在一个问题即默认的 /bbs php程序提示找不到文件   
        root /data/bbs/;
        include php_fcgi.conf;
        set $real_script_name $fastcgi_script_name;
        if ($fastcgi_script_name ~ "^/bbs/(.+?\.php)(.*)$") {
            set $real_script_name $1;
            set $path_info $2;
        }
        fastcgi_param SCRIPT_FILENAME /data/bbs/$real_script_name;
        fastcgi_param SCRIPT_NAME $real_script_name;
        fastcgi_param PATH_INFO $path_info;
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758

php_fcgi.conf

                            
fastcgi_pass  127.0.0.1:9000;
fastcgi_index index.php;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx;

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

12345678910111213141516171819202122232425262728

win10 查看已保存的wifi 密码

2019-10-26 17:49:33

win10 查看已保存的wifi 密码

右键左下角 徽标 ,点击 Windows PowerShell(管理员)

查看已保存的所有wifi

 
netsh wlan show profile
1

基本显示

所有用户配置文件 : zodream

后面的就是wifi 名(zodream 即为wifi名替换下面

 
netsh wlan export profile name="zodream" folder=. key=clear
1

提示

接口配置文件“zodream”已成功保存在文件“.\WLAN-zodream.xml”中。

输入

  
cat WLAN-zodream.xml
12

找到 passPhrase 往下两行就是

xml
 
<keyMaterial>密码</keyMaterial>
1

这就是密码了

第三方支付对接

2019-10-16 21:53:12

第三方支付对接

支付

显示支付按钮

  1. 直接显示隐藏的表单,点击按钮就提交跳转到支付页面
  2. 只显示一个按钮,点击触发 【调起支付】

调起支付

参数: 订单id 支付方式 币种?

服务端生成表单html或调起参数或跳转链接

支付通知

处理通知,完成支付

  1. 获取订单信息
  2. 验证金额
  3. 处理订单并保存第三方支付单号(退款可能用到)

退款

点击退款

  1. 退回余额,实时处理
  2. 通过订单号生成退款请求,退款的链接或http请求结果,此时退款不能判断是否处理成功

退款通知

  1. 获取订单信息
  2. 验证金额
  3. 处理订单保存第三方退款单号

订单支付状态

  1. 未支付
  2. 支付中
  3. 支付完成

订单退款状态

  1. 退款中
  2. 退款完成

支付流水号

最好不要直接使用订单号去支付,因为如果订单金额发生变动或支付金额受实时汇率影响的话,会导致第三方支付不成功

net core 正确自定义 TagHelper

2019-10-15 19:47:11

net core 正确自定义 TagHelper

自定义一个 友情链接 的tag

创建一个数据源

c#
                    
namespace NetDream.Models
{
    public class FriendLinkModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }

        public static List<FriendLinkModel> All()
        {
            var data = new List<FriendLinkModel>();
            data.Add(new FriendLinkModel()
            {
                Name = "ZoDream",
                Url = "https://zodream.cn",
            });
            return data;
        }
    }
}
1234567891011121314151617181920

定义 TagHelper

c#
                        
namespace NetDream.Base.TagHelpers
{
    public class FriendLinkTagHelper : TagHelper
    {
        public string Title = "友情链接";

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            output.Attributes.Add("class", "friend-link");
            var html = new StringBuilder();
            html.AppendFormat("<div>{0}</div><div>", Title);
            var items = FriendLinkModel.All();
            foreach (var item in items)
            {
                html.AppendFormat("<a href=\"{0}\" target=\"_blank\" rel=\"noopener noreferrer\">{1}</a>", item.Url, item.Name);
            }

            html.Append("</div>");
            output.Content.SetHtmlContent(html.ToString());
        }
    }
}
123456789101112131415161718192021222324

使用

先在 _ViewImports.cshtml 添加以下代码

html
  
@addTagHelper *, netdream
12

注意 netdream 是本项目的程序集名称,并不是命名空间

没看源码,大致猜测这个是动态引用的即使用时临时检索 程序的dll 引入tagHelper

程序集名称查看方法:

右键项目-> 属性 -> 应用程序 -> 程序集名称(N):

然后在 Home.cshtml 使用

html
  
<friend-link title="友情链接"></friend-link>
12

最终就会输出友情链接了

源码

【Vue Shop】代码优化

2019-10-11 00:01:04

代码优化

  1. 变量写法统一
  2. 空格控制
  3. 类型转化规范

由于使用了 typescript 编写,所以引入了 TSLint 作为代码规范检查工具,在编写过程中并没有太注重代码的规范,导致每次编译的代码提示都刷刷的好几页。

在基本功能完成后进行代码优化工作。

  1. 类型转化提示,例如 parseInt 第二个参数必填 10 表示转化为十进制。页面获取传值 this.$route.querythis.$route.params 返回多个类型的值,如果需要string 则使用 as string 做类型转化声明,实际在js 代码中不做处理。如果需要 number 则需要使用 parseInt(this.$route.query.a as string, 10) 进行转化
  2. 变量统一使用驼峰写法,默认的 ts 规范也是这么规范的,当然也可以改,反正我是习惯用驼峰写法
  3. 字符串和引入文件路径使用单引号,当然这也可以改,vscode 默认生成的引用可以在设置里面设置或在 tsconfig.json 中配置
  4. 清除空白行中的空格
  5. 补全 , 和 ;
  6. 匿名函数如果只有一个参数可以不用括号和声明类型,多个参数必须括号和声明类型
  7. 一行文字不超过120个,超过截断换行
  8. 同意代码风格,页面都使用类的写法
  9. 类的所有成员包括属性,方法都加上 publicprivate 修饰符
  10. 变量声明,尽量使用 const

【Vue Shop】个人信息修改、实名制、账户注销

2019-10-11 00:00:32

个人信息修改、实名制、账户注销

个人信息修改

  1. 头像修改,可以增加图片的裁剪
  2. 昵称修改,通过底部弹出框修改
  3. 生日修改,调用日期选择组件

实名制

此功能主要是用于用户身份证图片上传,审核是后台人工进行审核

账户注销

此功能分为三步:

  1. 进入时弹出请用户确认正在操作注销流程
  2. 选择注销的理由
  3. 提交后台审核,审核通过删除相关订单地址账户记录

【Vue Shop】订单评价

2019-10-11 00:00:06

订单评价

功能介绍

当订单签收后可以进行评价,订单商品为单个评价,当订单中商品都评价后,订单状态更改为已完成

【Vue Shop】退换货

2019-10-10 23:59:42

退换货

功能介绍

  1. 退款,已支付未发货订单可以执行退款流程
  2. 退货,已发货、已签收、已完成
  3. 维修,订单完成之后申请售后
  4. 换货,
  5. 价格保护,

【Vue Shop】发票

2019-10-10 23:59:12

发票

发票申请有两种方式:

  1. 购物车结算时填写发票信息
  2. 订单完成后集中申请开票

发票类型:

  1. 普票
  2. 增票

发票抬头

  1. 个人:只能开普票
  2. 企业: 需填写税号等

发票方式

  1. 纸质发票:需选择收货地址
  2. 电子发票: 需填邮箱

【Vue Shop】优惠券

2019-10-10 23:58:40

优惠券

可领取优惠券

  1. 显示领取数量
  2. 显示是否已领取

我的优惠券

  1. 拆分不同状态,显示不同样式
  2. 显示优惠券图片及信息

【Vue Shop】订单及账户开发

2019-10-10 23:58:10

订单及账户开发

订单

  1. 分状态查询
  2. 订单详情
  3. 物流信息查询

账户

  1. 显示账户余额
  2. 账户明细
  3. 添加银行卡

【Vue Shop】支付开发

2019-10-10 23:57:37

支付开发

  1. 支付方式更改
  2. 支付跳转
  3. 支付成功失败

【Vue Shop】收货地址及结算流程

2019-10-10 23:57:09

收货地址及结算流程

收货地址

  1. 列表的状态:可以选择、不可选择
  2. 选择或保存回调地址
  3. 地区选择组件的使用

结算

  1. 收货地址
  2. 分组商品及配送方式
  3. 支付方式
  4. 优惠券
  5. 发票
  6. 备注

【Vue Shop】个人中心页

2019-10-10 23:56:34

个人中心页

结构

  1. 顶部退出
  2. 头像名称
  3. 菜单
  4. 订单统计
  5. 账户统计
  6. 帮助

本页面相对简单,一个注意登录与未登录用户头像的显示

特别功能

可以弄一个页面滑动顶部保留头像的功能

【Vue Shop】购物车加入及显示

2019-10-02 22:58:11

加入购物车弹出框的显示

  1. 首页及商品列表
  2. 商品详情

购物车中的商品

  1. 商品分组
  2. 优惠
  3. 数量加减
  4. 商品删除
  5. 商品选择

选中商品传递到结算页

【Vue Shop】商品详情页面开发

2019-10-02 22:57:39

商品详情页面开发

页面结构

  1. 头部导航
  2. 轮播图
  3. 商品信息
  4. 收藏
  5. 商品活动
  6. 最新评论
  7. 推荐商品
  8. 详情
  9. 规格
  10. 售后保障
  11. 底部购买按钮

特别要求

页面滚动时有相应的头部导航切换

【Vue Shop】登录注册开发及第三方登陆

2019-09-30 20:05:33

登录注册开发及第三方登陆

手机号验证码登录

基本需求:

  1. 一个输入手机号,必须验证码手机号的正确性
  2. 输入验证码,无特殊要求,只是必填
  3. 点击发送验证码,发送时有时间限制,显示倒计时,倒计时期间无法点击

手机号密码登录

使用手机号和密码登录,

邮箱登录

邮箱的话就只能密码登录,当然也可以邮箱加邮件验证码登录,不过让用户使用比短信验证码麻烦,不推荐使用

邮箱注册

邮箱注册,填邮箱、用户名、密码等基本信息就行了,可以价格邮箱验证功能,发送验证链接邮件,点击链接就验证成功

手机号注册

手机号注册,就手机号、用户名、密码、短信验证码

邮箱密码找回

第一步填写邮箱,判断是否注册,然后发送找回链接到邮箱中,那这样就跳转到pc 了,没意义了,改为邮件验证码

第三方登陆

大致流程:

  1. 点击跳转到服务端
  2. 继续重定向到第三方
  3. 第三方返回到服务端
  4. 服务端获取信息保存登录信息到 cookie
  5. 跳转到前端,前端在router 监听事件中获取cookie中的信息

【Vue Shop】搜索开发

2019-09-30 19:57:34

搜索开发

搜索页面可以分两个状态

准备搜索

当搜索条件为空进入显示此页面:

  1. 搜索框:包括搜索关键词提示,回车搜索
  2. 搜索历史记录
  3. 热门搜索关键词

搜索结果

  1. 搜索框显示本次搜索内容
  2. 搜索筛选
  3. 商品列表

涉及接口:

  1. 热门关键词接口
  2. 关键词提示接口
  3. 商品搜索接口:返回商品数据、分页数据、筛选项

技术相关:

  1. 搜索历史记录的保存与读取及删除
  2. 搜索状态的切换,例如:点搜索结果进入搜索自动填入搜索词,取消显示回原有搜索词
  3. 使用上拉刷新下拉加载更多

【Vue Shop】首页及分类页开发

2019-09-30 19:56:59

首页及分类页开发

首页

本次只做一个简单的首页,共分为6部分:

  1. 头部搜索框:不是真正的可输入的搜索框,作用是点击跳转到搜索功能页面
  2. 轮播图:这里使用的是 mint-ui 中的轮播组件
  3. 一级分类菜单栏
  4. 新品
  5. 热门
  6. 精品

涉及到的 API 接口:

  1. 商品统计接口:显示在搜索框中,显示商城有多少个商品正在出售
  2. 轮播广告图接口
  3. 分类接口
  4. 商品首页推荐接口:包含新品、热门、精品的商品

技术相关

  1. 商品统计在其他页面也需要,可以使用 store 进行缓存
  2. 三个推荐商品部分样式一致,可以提取成为组件,传递标题、商品数据即可
  3. 头部有一个登录状态的判断,未登录显示登录入口,登录显示消息入口

公共底部导航栏组件

分类页

本页面分三部分:

  1. 头部搜索栏,同样是起点击跳转作用,样式与首页有区别,不需要做成组件
  2. 左侧一级分类
  3. 右侧为左侧选择的分类详情

右侧分为

  1. 当前分类的banner
  2. 当前分类的名称
  3. 当前分类的商品推荐
  4. 当前分类的所有子孙分类

涉及到的接口:

  1. 分类接口
  2. 商品统计接口
  3. 分类详情接口:因为本接口允许通过拓展参数设置获取推荐商品及子分类,不需要单独接口获取推荐商品和子分类了

技术相关

  1. 左边分类切换,可以考虑缓存当前选中
  2. 可以缓存分类详情,

公共底部导航栏组件

【Vue Shop】客户端身份验证及注入API请求

2019-09-29 18:36:57

客户端身份验证及注入API请求

  
npm i ts-md5
12

服务端提供

appid

secret

公共请求参数

参数名 字段类型 说明
appid string
timestamp string 当前时间例如:2019-05-01 01:01:01
sign string 签名

登录的 token 使用headers

Authorization: Bearer token

签名方式

md5 签名

ts
  
Md5.hashStr(appid + timestamp + secret)
12

注入

ts
  
axios.interceptors.request
12

响应过滤

ts
  
axios.interceptors.response
12

所有错误提示响应状态码都不是200

响应状态码401 时表示登录过期,需重新登录

【Vue Shop】API请求构建

2019-09-29 18:34:01

API请求构建

本次使用 axios 作为网络请求底层

 
npm i axios
1

请求头

post 数据为json格式

接受json格式的数据

   
Content-Type: application/vnd.api+json

Accept: application/json
123

GET 请求

ts
            
export function fetch<T>(url: string, params = {}): Promise<T> {
    return new Promise((resolve, reject) => {
        axios.get(url, {
            params,
        }).then((response) => {
            resolve(response.data)
        }).catch((err) => {
            reject(err)
        })
    })
}
123456789101112

POST 请求

ts
          
export function post<T>(url: string, data = {}): Promise<T> {
    return new Promise((resolve, reject) => {
        axios.post(url, data)
            .then((response) => {
                resolve(response.data)
            }, (err) => {
                reject(err)
            })
    })
}
12345678910

DELETE 请求

ts
           
export function deleteRequest<T>(url: string): Promise<T> {
    return new Promise<T>((resolve, reject) => {
        axios.delete(url)
            .then((response) => {
                resolve(response.data)
            }, (err) => {
                reject(err)
            })
    })
}
1234567891011

【Vue Shop】页面部件确定及开发

2019-09-29 18:34:00

页面部件确定及开发

底部导航

主要功能,根据页面自动判断选中一项

设计思路

  1. 定义菜单项结构
    ts
         
    interface IMenu {
     name: string, // 显示的文字
     icon: string, // 显示的图标
     url: string   // 链接
    }
    12345
  2. 定义可接受参数 menus: IMenu[]
  3. 通过 $route.name 判断当前的路由,选中菜单

下拉刷新上拉加载

大致分为六个状态

  1. 下拉过程中 显示下拉刷新
  2. 到达下拉距离限制 显示释放刷新
  3. 向上脱离下拉 显示停止刷新
  4. 直接释放 显示刷新中
  5. 底部 显示加载更多
  6. 触底 显示加载中
  7. 加载完成 显示加载完成 1s 后自动恢复状态

左滑删除

大致思路

  1. 一般的分为左滑和右滑
  2. 滑动的距离不能大于可显示的宽度,例如 左滑删除,当删除按钮完全显示后不再继续向左滑
  3. 左滑到底是可以继续回撤的(即右滑),如果支持左右滑,那个应该已开始滑的方向为准,不能左滑开始,以实际右滑结束
  4. 收起状态,
  5. 滑动距离大于1/3可以动画补全但是完成滑动,小于1/3需动画补全恢复原始状态
  6. 点击事件,需恢复原始无滑动状态
  7. 排异模式,一个开始滑动,应该把其他的恢复到无滑动状态,动画过渡

时间选择

首先确定输入和输出,输入字符串就要输出字符串,输入Date 就要输出Date

既然要输出字符串,那么就要支持格式化,就要有输入格式化定义 format

一个日期,应该是 年月日时分秒, 那个 年月日 是必须的,时分秒可选 通过 format 判断就行了

还需要有一个选择范围 min max

多语言?暂不考虑

还需要一个触发显示框,不需要外部代码手动触发,

html
     

<div @click="showCalendar" class="datepicker__input-container">
    <slot></slot>
</div>
12345

这样就好了,使用方法

html
        
<DatePicker v-model="user.birthday" format="yyyy-mm-dd">
    <div class="line-item">
        <span>生日</span>
        <span>{{user.birthday}}</span>
        <i class="fa fa-chevron-right"></i>
    </div>
</DatePicker>
12345678
ts
        
import DatePicker from '@/components/DatePicker.vue';

@Component({
    components: {
        DatePicker,
    },
})
12345678

地区选择(多级滑动选择)

为什么不用多个select 联动?

  1. 为了同意样式,全部仿照 ios 底部弹出滑动选择
  2. 为了方便,多个 select 占地方而且获取不方便,因为地区是不确定几级的,而且我只要最后的选择id

不确定几级,那么就用一个 v-for 就行了

要通过上下滑动进行选择,加上滑动事件 @touchstart='touchStart' @touchmove='touchMove'

【Vue Shop】基本架构

2019-09-28 05:56:07

基本架构

项目结构

src

api 定义实现api接口

model.ts 定义接口数据结构

assets 样式及图片资源

components 公共基本部件

pages 页面

Home

Child 本页面用的部件

pipes 自定义过滤器

router 注册的页面路由

store 保存的状态及跨页面传递的数据

utils 辅助方法,http 请求

代码约定

  1. 页面及部件文件夹名使用首字母大写的驼峰写法
  2. 函数方法名使用驼峰写法
  3. 页面及部件全部使用class 类的写法,属性使用驼峰命名
  4. 语句结束必须使用;
  5. 代码一行不超过120 个字符
  6. 对象内部必须使用,结束
  7. html 标签class 类名使用 小写加 -
  8. css 尽量不使用 !important
  9. scss 嵌套层数不超过5层

具体typescript 规范请查看 tslint.json

网络请求

先定义相应数据模型,基本的模型

ts
                                    
// 分页数据
export interface IPaging {
    limit: number;
    offset: number;
    total: number;
    more: boolean;
}
// 页面数据
export interface IPage<T> {
    paging: IPaging;
    data: T[];
}
// 全局相应
export interface IBaseResponse {
    appid?: string;
    sign?: string;
    sign_type?: string;
    timestamp?: string;
    encrypt?: string;
    encrypt_type?: string;
}
// 失败响应
export interface IErrorResponse {
    code: number;
    message?: string;
    errors?: any;
    description?: string;
}

export interface IData<T> extends IBaseResponse {
    data?: T[];
}

export interface IDataOne<T> extends IBaseResponse {
    data?: T;
}
123456789101112131415161718192021222324252627282930313233343536

接着封装一个http 请求,包括注入appid 及sign,设置请求头,处理一些全局响应包括token 过期

暴露几个常用的请求方式 get post delete 最后返回一个 Promise<T>

具体页面

[√] 首页

[√] 分类页

[√] 搜索页

[√] 商品详情页

[√] 商品评论显示页

[√] 购物车页

[√] 结算页

[√] 支付页

[√] 个人中心页

[√] 浏览记录页

[√] 个人账户页(包含提现、充值弹窗)

[√] 个人账户记录页

[√] 银行卡页

[√] 银行卡绑定页

[√] 发票页

[√] 发票申请页

[√] 发票抬头页

[√] 发票抬头编辑页

[√] 发票记录页

[√] 登录注册页

[√] 订单列表页

[√] 订单详情页

[√] 商品收藏页

[√] 消息页

[√] 账号关联页

[√] 个人信息页

[√] 账户注销页

[√] 登陆设备管理页

[√] 修改密码页

[√] 实名认证页

[√] 收货地址页

[√] 收货地址编辑页

[√] 评论商品页

[√] 发表评论页

[√] 退换货页

[√] 退换货申请页

[√] 文章列表页

[√] 文章分类页

[√] 文章详情页

[√] 推荐页

[√] 推荐规则页

[√] 推荐订单页

[√] 推荐会员页

[√] 推荐二维码页

[√] 优惠券领取页

[√] 我的优惠券页

[√] 签到页

【Vue Shop】环境准备

2019-09-28 05:46:28

环境准备

项目语言选择

Typescript 本人最为熟悉,特别喜欢她的强类型,加入 vscode 这个开发工具的智能提示,简直不要太方便。

scss 喜欢她的嵌套

开发工具

node.js

vscode

插件

Paste JSON as Code 根据 json 生成 ts 代码

Vetur vue 的语言服务

Vue VSCode Snippets vue 代码块

安装环境

安装全局vue-cli脚手架

 
npm install --global @vue/cli
1

创建项目

 
vue create my-project-name
1

选择 Manually select features

选中(上下键+空格键) Babel TypeScript Router Vuex Linter / Formatter

输入三次 y

直接默认回车即可

安装 sass 支持

 
npm install -D sass-loader node-sass
1

运行

 
npm run serve
1

浏览器打开 http://localhost:8080

手机版商城客户端开发大纲

2019-09-28 05:45:02

手机版商城客户端开发大纲

本项目大致分为 vue、小程序、UWP 版、Flutter版,其他版本暂无时间开发,如果有好的 kotlin、swift 框架推荐请留言。

目前已基本完成 vue 及小程序版,正在开发UWP版及Flutter版,本教程分四部分预计完成时间为三个月,特别纠结,特别时网络请求这一块,必须设计好才能继续下去。

  1. 环境准备 【Vue】 【小程序】 【UWP】【Flutter
  2. 基本架构 【Vue】 【小程序】 【UWP】【Flutter
  3. 页面部件确定及开发 【Vue】 【小程序】 【UWP】【Flutter
  4. API请求构建 【Vue】 【小程序】 【UWP】【Flutter
  5. 客户端身份验证及注入API请求 【Vue】 【小程序】 【UWP】【Flutter
  6. 首页及分类页开发 【Vue】 【小程序】 【UWP】【Flutter
  7. 搜索开发 【Vue】 【小程序】 【UWP】【Flutter
  8. 登录注册开发及第三方登陆 【Vue】 【小程序】 【UWP】【Flutter
  9. 商品详情页面开发 【Vue】 【小程序】 【UWP】【Flutter
  10. 购物车加入及显示 【Vue】 【小程序】 【UWP】【Flutter
  11. 个人中心页 【Vue】 【小程序】 【UWP】【Flutter
  12. 收货地址及结算流程 【Vue】 【小程序】 【UWP】【Flutter
  13. 支付开发 【Vue】 【小程序】 【UWP】【Flutter
  14. 订单及账户开发 【Vue】 【小程序】 【UWP】【Flutter
  15. 优惠券 【Vue】 【小程序】 【UWP】【Flutter
  16. 发票 【Vue】 【小程序】 【UWP】【Flutter
  17. 退换货 【Vue】 【小程序】 【UWP】【Flutter
  18. 订单评价 【Vue】 【小程序】 【UWP】【Flutter
  19. 个人信息修改、实名制、账户注销 【Vue】 【小程序】 【UWP】【Flutter
  20. 代码优化 【Vue】 【小程序】 【UWP】【Flutter

代码

Vue

小程序

UWP

Flutter

本项目初衷

本人主要从事于电商开发及电商系统二次开发,本项目为工作经验的总结前端篇。

AB 压力测试

2019-07-31 06:27:17

今天使用`google search console` 查看网站收录情况发现有很多链接处于 `发现未收录`状态,才想起用工具进行并发测试。

AB (ApacheBench) 是 Apache 自带的一款功能强大的测试工具,可以快速测试基于 HTTP 协议所有 Web 页面的最大负载压力。

下载地址www.apachelounge.com Apache 2.4.39 Win64

本文以 windows 10 为例,使用 PowerShell 执行命令行

加压下载文件到指定文件夹

打开 PowerShell ,通过 cd 进入 bin 目录

cmd
 
cd D:\zodream\Apache24\bin
1

主要用到 ab.exe 这个程序

cmd
 
.\ab
1
-n 总共发起几次请求
-c 并发数
-t 总共执行时间,到时结束所有请求,
-s 最大超时时间,默认30秒
-b TCP 请求发送和接受的字节数
-p POST发送文件
-u PUT发送文件
-T 设置请求头内容类型
-k keep-alive保持连接

最后输入完整网址 带http/https

例如

cmd
 
.\ab -t 10 -c 100 http://zodream.cn/
1
cmd
                                                       

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking zodream.cn (be patient)
Finished 18 requests 
# 完成 18个请求

Server Software:        Apache
Server Hostname:        zodream.cn
Server Port:            80

Document Path:          /
Document Length:        24125 bytes
# 请求页面大小

Concurrency Level:      100
# 并发请求数量
Time taken for tests:   10.226 seconds
# 整个测试耗时
Complete requests:      18
# 完成的请求数
Failed requests:        0
# 失败的请求
Total transferred:      1146475 bytes
# 网络传输总量
HTML transferred:       1131655 bytes
# HTML 内容传输量
Requests per second:    1.76 [#/sec] (mean)
# 每秒的请求平均数
Time per request:       56812.761 [ms] (mean)
# 每个请求的平均时间
Time per request:       568.128 [ms] (mean, across all concurrent requests)
# 服务器处理请求的平均时间
Transfer rate:          109.48 [Kbytes/sec] received
# 网络平均转移率

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       11   14   3.8     13      23
Processing:  5051 6250 1257.6   5386    8892
Waiting:       47 1733 922.1   1300    3888
Total:       5063 6264 1256.8   5398    8906

Percentage of the requests served within a certain time (ms)
  50%   5398
  66%   6787
  75%   7830
  80%   7840
  90%   7854
  95%   8906
  98%   8906
  99%   8906
 100%   8906 (longest request)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455

发现问题

cmd
 
.\ab -n 100 -c 100 http://zodream.cn/blog.html
1

使用此命令,发现出现 ...apr_pollset_poll: The timeout specified has expired (70007) 这样的错误,大致意思是请求超时了,可以增加 -k 保持连接

cmd
 
.\ab -n 100 -c 100 -k http://zodream.cn/blog.html
1

npm 包开发

2019-07-12 21:44:09

准备

nodejs

vscode

流程

注册npm账号

新建文件夹 test

vscode 中打开

ctrl + ` 打开 cmd 模式下登录

  
npm login
12

输入账号密码

输入 npm init

输入项目名及说明生成 package.json 文件

  
npm i @types/node gulp gulp-typescript typescript --save-dev
12

新建文件夹 src

src 下添加 index.ts

根目录添加 gulpfile.js

输入内容

js
          
var gulp = require('gulp'),
    ts = require("gulp-typescript"),
    tsProject = ts.createProject('tsconfig.json');

gulp.task('default', async() => {
    await gulp.src('src/**/*.ts')
        .pipe(tsProject())
        .pipe(gulp.dest('dist/'));
});
12345678910

根目录添加 tsconfig.json

json
                 
{
  "compilerOptions": {
    "baseUrl": "./",
    "declaration": true,
    "typeRoots": [
      "./node_modules/@types"
    ]
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.spec.ts"
  ]
}
1234567891011121314151617

修改 package.json

json
 
"main": "dist/index.js",
1

将入口指向编译生成的 index.js 文件

添加

json
   
"files": [
    "dist"
],
123

只需要 dist 文件夹下的文件就行了,其他源码就不用发布了

src/index.ts

ts
        

function test() {
    console.log('test');
}

export default test;

module.exports = test;
12345678

执行 gulp 命令

dist 下会自动生成 index.jsindex.d.ts 两文件

  
npm publish
12

发布成功

在其他项目

  
npm i test
12

安装此包

使用

ts
 
import test from 'test';
1

js
 
var test = require('test);
1

添加 bin

添加 bin 文件夹

新建文件 cli.js(文件名随意)

js
   
#!/usr/bin/env node

console.log(1);
123

然后在 package.json 配置

json
   
"bin": {
  "command-name": "./bin/cli.js"  //告诉package.json,我的bin叫 command-name ,它可执行的文件路径是bin/cli.js
}
123

bin 命令必须全局安装才能用

 
npm install . -g
1

 
npm link
1

gorm 使用

2019-06-20 23:07:32

开启日志调试sql语句

go
  
var db *gorm.DB
db.LogMode(true)
12

去除自带的软删除查询

当 struct 结构中带有 DeletedAt 属性,在查询的时候就会默认加上 deleted_at IS NULL,但是我的数据表 deleted_at 的默认值是 0,因此需要使用 , 去除默认查询加上自己的查询

go
 
db.Unscoped().Where("deleted_at=0")
1

默认查询的表会自动使用 结构名 的复数

可以定义 TableName 方法自定义表名

go
       
type User struct {
    ID uint
}

func (User) TableName() string {
    return "blog"
}
1234567

关联查询

使用 Preload("User") 进行关联查询

go
         
type Blog struct {
    ID uint
    User User
    UserID int
}

var data []Blog

db.Preload("User").Find(&data)
123456789

使用 Related 进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 FOREIGNKEY 去查找 User, 如果没定义, 则调用时需要指定

go
  
db.First(&data)
db.Model(&data).Related(&data.User).Find(&data.User)
12

使用 Association 进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 AssociationForeignKey 去查找 User, 必须定义

go
  
db.First(&data)
db.Model(&data).Association("User").Find(&data.User)
12

使用 Android Studio 制作 .9.png 图片

2019-06-19 23:42:56

重要:四边黑线表示可以伸缩的区域

步骤

  1. 将项目的.png图片放到资源文件夹drawable下面,右键 .png 的图片 选择 Create 9-Patch file...,命名默认就行
  2. 双击 .9.png 的图片打开,在 9-Patch 窗口下,会出现两个窗口,左边为编辑窗口,右边为预览窗口(拉伸状态下的图片), Zoom 为编辑窗口中的放大比例(无法缩小),Patch scale 为预览窗口中图片放大比例,Show lock鼠标放到原图上,会显示红色斜线部分。表示点9图锁定的区域; Show content:是预览窗口蓝色部分,蓝色表示可以填充内容,白色便是不可填充内容,移动原图中右边和下边的修改可填充内容的区域,规则如上; Show patches:显示编辑窗口中可以缩放的区域,绿色和紫色部分,原图颜色为不伸缩部分;Show bad patches:显示原图中不规范的缩放区域。比如带弧度中部分是不应该缩放的,如下图中红线标记的区域。遇到下面情况,需要向右稍微移动上边的黑线,不标记弧度部分,红线就会取消。
  3. 从顶边拉出的线表示线以下的为缩放区域,从底边拉出的线表示线以上的为缩放区域,从左边拉出的线表示线以右的为缩放区域,从右边拉出的线表示线以左的为缩放区域;
  4. 在边的区域可以使用鼠标拖拽框选缩放区域
  5. 内容居中的做法是,两边的缩放区域等长
  6. 内容间距自适应,在间距之间都拉出缩放区域
  7. 拉伸四周最快速的方法, zoom 放大3倍,顶边框选两个区域,左边框选两个区域,移动线控制区域到指定位置就行了

Go 笔记(学习iris)

2019-06-18 05:21:07

评论规范写法

go
       
// 方法名 说明

// Example 示例
func Example() {

}
1234567

iris 模板语法

公共模版

使用 yield 输出内容

html
  
{{ yield }}
12

传其他部位的内容

先在公共模版声明 此处使用 header 的内容

html
  
{{ part "header" }}
12

再在页面中定义 命名规则 相对于模板根目录的相对路径 -header

文件 blog/index.html

html
   
{{ define "blog/index-header"}}
<link type="text/css" href="/assets/css/blog.css" rel="stylesheet" media="all">
{{ end }}
123

根据路由名生成链接

go
   

homeRoute := app.Get("/", controllers.Index)
homeRoute.Name = "home"
123

使用

 
{{ urlpath "home" }}
1

生成链接自动加上域名

go
  
rv := router.NewRoutePathReverser(app, router.WithHost("zodream.cn:443"))
tmpl.AddFunc("url", rv.URL)
12

使用

 
{{ url "home" }}
1

项目从vue转微信小程序体验

2019-06-03 20:10:50

基于项目Vue-ShopMini-Shop的开发总结

页面修改

底部导航栏修改

图标全部使用图片,在app.json 中配置

头部标题栏修改

移除后退加标题样式,通过自带的标题加背景颜色的json 配置方式

轮播图修改

使用自带的swiper 进行修改

页面标签及事件修改

标签名 目标标签
img image
a navigator
i span strong font em b text
其他 view
属性名 目标属性
v-if wx:if="{{ }}"
v-elseif wx:elif="{{ }}"
v-else wx:else
v-bind:src src
href url
@click bindtap
v-on:click bindtap
(click) bindtap
@touchstart bindtouchstart
@touchmove bindtouchmove
@touchend bindtouchend
:key
v-show hidden="{{! }}"
v-for wx:for="{{ }}" wx:for-index=" " wx:for-item=""
v-model value="{{ }}" bind:input=" Changed"
第一个字符为@且值不为空 bind:
第一个字符为: ={{ }}
其他包含@

样式修改

图标字体修改

将ttf的字体文件转化成base64 放入样式中

ts
     
import * as fs from 'fs';
export function ttfToBase64(file: string): string {
    const content = fs.readFileSync(file);
    return 'url(\'data:font/truetype;charset=utf-8;base64,'+ content.toString('base64') +'\') format(\'truetype\')';
}
12345

标签样式修改

将文件中的标签选择器进行相应转化

程序修改

页面方法修改

created() 转化为 onLoad

$route 全部转化为 onLoad(query) 的参数

修改属性值,只能通过 setData 修改

接口调用修改

axios 改为 wx.request

store 修改

将所有的store 全部放到 app.ts 中 globalData

组件修改

左滑删除

vue中可以通过$refs 进行排异(只有一个左滑状态,其他自动恢复原状),小程序中可以通过新增自定义组件加入关联关系进行排异

下拉刷新上拉加载更多

启用自定义组件,使用自带的 enablePullDownRefresh: true

地区选择

使用自带的地区选择picker,微信的网络请求有数据大小限制,自定义组件实现无法一次传入所有省市区,

自带地区选择只能获取到省市区名称,因此需要后台接口进行修改

弹出选择

自定义实现监听属性变化

ts
                
public observe(key: string, callback: (newVal: any, oldVal: any) => void) {
    let val = this.data[key];
    Object.defineProperty(this.data, key, {
        configurable: true,
        enumerable: true,
        set: function(value) {
            // 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值
            callback.call(this, value, val); // value是新值,val是旧值
            val = value;
        },
        get: function() {
            return val;
        }     
    })
}
12345678910111213141516

总结

总的来说,转化过程没有太大的技术难度,就是每个页面都得改,比较繁琐。

windows下设置Git 区分文件名大小写

2019-04-21 08:18:39

问题

windows 环境 git 下修改文件名大小写无效,虽然不影响读取,但是在一些(比如小程序路径)就无法识别

解决方法

bash
 
git config core.ignorecase false
1

字体反爬与反反爬

2019-04-16 02:34:53

说明

一些公司会把一些关键数据进行保密,而网站进行保密(对用户显示,对爬虫隐藏)一种方式就是自定义的字体文件

主要用到的工具

python

fonttools

反爬

主要原理

从一个已有的字体文件提取需要加密的文字字形,更改字形索引,保存为新的字体文件,使用新的文字索引使用

反反爬

主要原理

获取字体文件,根据索引获取字形,再根据字形获取真正的文字(前提必须有一个足够丰富的字形库)

晋级

反爬使用指定义的字形(手动修改字形)

反反爬使用类似图像识别进行字形识别

字体处理工具

理想化操作

bash
   

generate --input font.ttf --font 1234567890 --out new
123

自动生成 节选的字体文件ttf wof svg 及css html使用例子

Gulp 插件开发

2019-04-06 06:50:02

基本模板

js
              
'use strict';

var Transform = require('readable-stream/transform');

module.exports = function (options) {
    return new Transform({
        objectMode: true,
        transform: function (file, enc, callback) {
            // TODO
            callback(null, file)
        }
    });
};
1234567891011121314

file 说明

file.path 完整路径
file.contents 内容 Buffer 或 Stream
file.basename 文件名 file.txt
file.extname 文件拓展名 .txt
file.isNull() 是否为空
file.isBuffer() 是否为Buffer 可以使用 String(file.contents) 转化为字符串, file.contents = Buffer.from(''); 可以修改内容
file.isStream() 是否为Stream

更多请参考 【vinylv

callback 使用

正常返回

js
  
callback(null, file);
12

返回报错(会中断后续所有文件任务)

js
  
callback({stack: 'error file'}, file)
12

中断本次任务,继续操作其他文件

js
 
callback()
1

使用插件

js
        
var gulp = require('gulp');
var my = require('./my.js');

gulp.task('default', async() => {
    await gulp.src('src/**/*')
        .pipe(my())
        .pipe(gulp.dest('dist/'));
});
12345678

微信小程序自用框架开发

2019-04-04 06:15:53

注意

【项目地址】GITHUB

本程序不做任何引入其他代码做为底层,仅支持原生代码

本框架基于 typescript gulp sass 开发,自动生成原生小程序代码

本框架优化 ts 智能提示,自动转换 html 为 wxml 自动编译 sass 为 wcss

本项目自带转化工具

转化核心

支持 ts sass

支持拆解html js ts sass css 写在一个文件上的情况

sass 引用模式未做处理

自动转化html 为 wxml, 自动转化 v-if v-for v-else v-show

支持json自动生成,支持 属性合并

更新

定义WxPage WxCommpent 两个类,增强 setData 的智能提示,

export 是为了避免提示未使用,编译时会自动去除

增加自动添加 Page(new Index()) Commpent(new Index()) 到末尾

增加json配置生成

ts
           

@WxJson({
    usingComponents: {
        MenuLargeItem: "/components/MenuLargeItem/index",
        MenuItem: "/components/MenuItem/index"
    },
    navigationBarTitleText: "个人中心",
    navigationBarBackgroundColor: "#05a6b1",
    navigationBarTextStyle: "white"
})
1234567891011

自动合并页面相关的json文件

支持自动合并 methods lifetimes pageLifetimes, 如果已有 属性会自动合并

methods  @WxMethod
lifetimes @WxLifeTime
pageLifetimes @WxPageLifeTime

自定义部件自动合并方法到methods属性中

ts
          
methods = {
    aa() {

    }
}

@WxMethod()
tapChange(mode: number) {
}
12345678910

最终生成

ts
       
methods = {
    tapChange(mode: number) {
    },
    aa() {

    }
}
1234567

标准模板

index.vue

html
                               
<template>
    <div>

    </div>
</template>
<script lang="ts">
import {
    IMyApp
} from '../../app';

const app = getApp<IMyApp>();

interface IPageData {
    items: number[],
}

export class Index extends WxPage<IPageData> {
    public data: IPageData = {
        items: []
    };

    onLoad() {
        this.setData({
            items: []
        });
    }
}
</script>
<style lang="scss" scoped>

</style>
12345678910111213141516171819202122232425262728293031

最终会处理为3个文件

index.wxml

html
   

<view></view>
123

index.wxss

css
 
1

index.js

js
                 
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var app = getApp();
var Index = (function () {
    function Index() {
        this.data = {
            items: [],
        };
    }
    Index.prototype.onLoad = function () {
        this.setData({
            items: []
        });
    };
    return Index;
}());
Page(new Index());
1234567891011121314151617

兴业银行支付PHP-SDK 踩坑

2019-04-04 06:13:15

需全局使用RSA签名

支付方法默认没有传证书路径及密码给签名方法

Signature 需添加传递 $this->epay_config['mrch_cert'], $this->epay_config['mrch_cert_pwd']

验签需手动传递证书路径

返回的参数必须去除自定义参数,VerifyMac 方法需传递证书 $this->epay_config['isDevEnv'] ? $this->epay_config['epay_cert_test'] : $this->epay_config['epay_cert_prod'];

理解Redux

2019-04-01 17:37:31

理解Redux

Reducer

(纯函数)拿到下一个State和之前的State来计算一个新的State。 初始化更改State中的值,并返回State ,接受两个参数 ,第一个为旧的state,第二个为action 根据type 更改state并返回

ts
        
function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}
12345678

可以使用combineReducers合并

ts
        
import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp
12345678

Store

Store 就是把它们联系到一起的对象

ts
               
let store = createStore(todoApp)
// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 更新
unsubscribe();
123456789101112131415

Action

描述State的改变,必须是一个包含type键的对象

ts
     
export interface Action { 
  type: string; 
  payload?: any; // 这个值可有可无
}
12345

可以封装一个方法来生成action

ts
   
export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}
123

重新规划数据库读取

2019-04-01 17:36:34

大致分为三层

第一层数据库实体(Entity)

文件夹位置 Domain\Entities

主要内容:包含数据库名,表名,主键、字段名、字段验证

保存最原始数据,直接读取的也是最原始数据

第二层数据实体(Model)继承数据库实体

文件夹位置 Domain\Models

主要内容:关联表、追加字段、隐藏显示字段

值转化,

第三层数据仓库(Repository)获取数据实体

文件夹位置 Domain\Repositories

主要内容:读取方法,更新实体

包含获取哪些值,返回数据实体

问题

这样文件变得更复杂、传出来的值都会固定、适应不同接口场景

思考

到底在哪一层操作关联数据库?

使用

php
                          
<?php
namespace Domain\Entities;
/**
 * 
 * @property integer $id
 * @property string $name
 * */
class User {
    public static function tableName() {
        return 'user';
    }

    protected function rules() {
        return [
            'name' => 'required|string:0,100',
        ];
    }

    protected function labels() {
        return [
            'id' => 'Id',
            'name' => '昵称',
        ];
    }
}
1234567891011121314151617181920212223242526
php
          
<?php
namespace Domain\Models;

use Domain\Entities\User as UserEntity;

class User extends UserEntity {

    protected $hidden = ['password'];
}
12345678910
php
            
<?php
namespace Domain\Repositories;

use Domain\Models\User;

class UserRepository {

    public function get($id) {
        return 
    }
}
123456789101112
php
                  
<?php
namespace Service;

use Domain\Models\User;

class UserController {

    protected $user;

    public function __construct(UserRepository $user) {
        $this->user = $user;
    }

    public function indexAction($id) {
        return $this->render($this->user->get($id));
    }
}
123456789101112131415161718

SVN Skipped '' -- Node remains in conflict

2019-03-19 00:14:18

SVN Skipped '' -- Node remains in conflict

SVN 使用过程中遇到的问题(主要是命令行还不习惯用),这里是把正确的方法记录一下

问题

拉取SVN 更新时遇到

Skipped 'xx' -- Node remains in conflict

解决方法

svn revert --depth=infinity index.html

index.html为冲突的文件名

【方法来源】

  1. Node remains in conflict,svn在服务器上显示冲突

IIS劫持CORS preflight OPTIONS请求

2019-03-15 19:09:52

发现问题

刚开机,启动vue 程序,发现所有请求都被浏览器禁止了,提示禁止跨域

解决问题

明明昨天都正常的,而且服务端程序也启用的允许跨域响应的,再一查看现在的响应头,不对劲,响应头不对了,而且响应服务也变成了 ASP.NET,再一调试服务端,发现请求根本没到程序里来,被iis拦截了

打开iis管理器,选中根节点,进入处理程序映射,点击“查看经过排序的列表”,把PHP(我用的是php环境)上移到`OPTIONSVerbHandler`前面即可,重启iis

【其他参考】

  1. IIS劫持CORS preflight OPTIONS请求(IIS hijacks CORS Preflight OPTIONS request)

vue 实现左滑删除

2019-03-15 00:46:05

更新

支持 :index 为数值或字符串

演示效果

源码

实现原理

  1. 需要记录初始位置,滑动方向,是否是滑动(点击不触发touchmove事件)
ts
             

oldLeft: 0,       // 记录初始位置 > 0 显示左边控件 < 0 显示右边控件  0 原始状态
left: 0,         // 控制滑动位置
startX: 0,       // 记录滑动的初始位置
isTouch: false,  //判断是否是滑动


touchStart(e: TouchEvent) {
    this.oldLeft = this.left;
    this.isTouch = false;
    this.startX = e.targetTouches[0].clientX;
},
12345678910111213
  1. 滑动中不能超过可显示的隐藏区域宽度
ts
                                

touchMove(e: TouchEvent) {
    this.isTouch = true;
    // 获取滑动距离
    const diff = e.targetTouches[0].clientX - this.startX;
    if (this.oldLeft == 0) {
        if (diff < 0) {
            // 左滑显示右边控件
            this.left = Math.max(diff, -this.getRightWidth());
            return;
        }
        // 右滑显示左边控件
        this.left = Math.min(diff, this.getLeftWidth());
        return;
    }
    if (this.oldLeft > 0) {
        if (diff > 0) {
            // 已显示左边控件,不能继续右滑
            return;
        }
        // 左滑隐藏左边控件
        this.left =  Math.max(this.oldLeft + diff, 0);
        return;
    }
    if (diff < 0) {
        // 已显示右边控件,不能继续左滑
        return;
    }
    // 右滑隐藏右边控件
    this.left = Math.min(this.oldLeft + diff, 0);
}
1234567891011121314151617181920212223242526272829303132
  1. 滑动结束,判断总滑动距离,不超过1/3自动复原
ts
                    

touchEnd(e: TouchEvent) {
    if (!this.isTouch) {
        // 点击,并复原
        this.animation(this.left, 0);
        this.$emit('click');
        return;
    }
    if (this.left == 0) {
        return;
    }
    if (this.left > 0) {
        const width = this.getLeftWidth();
        this.animation(this.left, this.left * 3 > width ? width : 0);
        return;
    }
    const width = - this.getRightWidth();
    this.animation(this.left, this.left * 3 < width ? width : 0);
}
1234567891011121314151617181920
  1. 点击事件需要复原

关于操作一个隐藏其他实现设想

  1. 通过父控件记录所有子控件引用,并标记子空间顺序,在父控件调用子元素复原方法或子控件内部调用

  2. 子控件在初始化的时刻,生成一个自增唯一标识,然后根据这个挂载到父控件或全局属性上

完整代码

html
                                                                                                                                                     

<template>
    <div class="swipe-row" :style="{left: left + 'px'}">
        <div class="actions-left" ref="left">
            <slot name="left"></slot>
        </div>
        <div :class="['swipe-content', name]"  
            @touchstart='touchStart'
            @touchmove='touchMove'
            @touchend='touchEnd'>
            <slot></slot>
        </div>
        <div class="actions-right" ref="right">
            <slot name="right">
                <i class="fa fa-trash" @click="tapRemove"></i>
            </slot>
        </div>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator';

@Component
export default class SwipeRow extends Vue {
    @Prop([String, Array]) readonly name!: string| string[];
    @Prop([Number, String]) readonly index!: number|string;
    oldLeft: number = 0;
    left = 0;
    startX = 0;
    isTouch = false;
    leftBox: HTMLDivElement | null = null;
    rightBox: HTMLDivElement | null = null;

    mounted() {
        this.leftBox = this.$refs.left as HTMLDivElement;
        this.rightBox = this.$refs.right as HTMLDivElement;
    }

    getLeftWidth(): number {
        if (!this.leftBox) {
            return 0;
        }
        return this.leftBox.clientWidth || this.leftBox.offsetWidth;
    }

    getRightWidth(): number {
        if (!this.rightBox) {
            return 0;
        }
        return this.rightBox.clientWidth || this.rightBox.offsetWidth;
    }

    tapRemove(item: any) {
        this.$emit('remove', item);
    }

    touchStart(e: TouchEvent) {
        this.resetOther();
        this.oldLeft = this.left;
        this.isTouch = false;
        this.startX = e.targetTouches[0].clientX;
    }

    touchMove(e: TouchEvent) {
        this.isTouch = true;
        const diff = e.targetTouches[0].clientX - this.startX;
        if (this.oldLeft == 0) {
            if (diff < 0) {
                this.left = Math.max(diff, -this.getRightWidth());
                return;
            }
            this.left = Math.min(diff, this.getLeftWidth());
            return;
        }
        if (this.oldLeft > 0) {
            if (diff > 0) {
                return;
            }
            this.left =  Math.max(this.oldLeft + diff, 0);
            return;
        }
        if (diff < 0) {
            return;
        }
        this.left = Math.min(this.oldLeft + diff, 0);
    }

    touchEnd(e: TouchEvent) {
        if (!this.isTouch) {
            this.animation(this.left, 0);
            this.$emit('click');
            return;
        }
        //const diff = e.changedTouches[0].clientX - this.startX;
        if (this.left == 0) {
            return;
        }
        if (this.left > 0) {
            const width = this.getLeftWidth();
            this.animation(this.left, this.left * 3 > width ? width : 0);
            return;
        }
        const width = - this.getRightWidth();
        this.animation(this.left, this.left * 3 < width ? width : 0);
    }
    // 补间动画
    animation(
        start: number, end: number, endHandle?: Function) {
        const diff = start > end ? -1 : 1;
        let step = 1;
        let handle = setInterval(() => {
            start += (step ++) * diff;
            if ((diff > 0 && start >= end) || (diff < 0 && start <= end)) {
                clearInterval(handle);
                this.left = end;
                endHandle && endHandle();
                return;
            }
            this.left = start;
        }, 16);
    }

    // 暴露出来给外部调用
    public reset() {
        if (this.left === 0) {
            return;
        }
        this.animation(this.left, 0);
    }

    // 复原其他
    resetOther() {
        if (typeof this.index == 'undefined') {
            return;
        }
        const items: SwipeRow[] = this.$parent.$refs.swiperow as SwipeRow[];
        if (!items || items.length < 1) {
            return;
        }
        for (let i = 0; i < items.length; i++) {
            if (items[i].index == this.index) {
                continue;
            }
            items[i].reset();
        }
    }
}
</script>
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149

使用

html
        
<div class="swipe-box">
    <SwipeRow v-for="(item, index) in items" :key="index" @remove="tapRemove(item)" :index="index" ref="swiperow">
        <div>
            这是内容
        </div>
    </SwipeRow>
</div>
12345678

样式

scss
                                               

.swipe-box {
    overflow: hidden;
    .swipe-row {
        width: 100%;
        position: relative;
        height: 5rem; 
        padding: 0;
        margin: 0;
        transition: left .5s;
        .swipe-content {
            width: 100%;
            display: block;
            height: 5rem;
        }
        .actions-left,
        .actions-right {
            position: absolute;
            height: 5rem; 
            min-height: 5rem;
            max-height: 5rem;
            text-align: center;
            font-size: 1.375rem;
            top: 0;
            white-space: nowrap;
            .fa {
                font-size: 1.875rem;
                padding: 1.5625rem 0.9375rem;
            }
            a {
                color: #fff;
                text-decoration: none;
            }
        }
        .actions-left {
            color: #fff;
            background-color: rgb(0, 187, 72);
            right: 100%;
        }
        .actions-right {
            color: #fff;
            background-color: #BB0000;
            left: 100%;
        }
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

Vuex 使用心得

2019-03-13 23:23:06

为什么用

实现多页面同步共享数据,全局状态管理,也可以当作内存缓存来用

怎么用

简单使用

通过 state 存储数据

通过 computed 实现方法获取 state 中的值或定义getters 获取

定义 mutations 更新 state 数据, 通过 store.commit 触发 mutation

定义 actions 封装 store.commit 触发

多模块

modules 每一个模块实现 state getters mutations actions

如果模块内方法重名,则需要 在模块内加上 namespaced: true, 使用时需要加上模块名才能访问指定模块 state.模块名.属性名;反之直接访问即可

注意

state 并不能默认请求内容,要先 store.commit 设置内容,也可以定义action 异步获取并设置

ts
            
getCategories(context: {commit: Commit; state: State}) {
    return new Promise((resolve, reject) => {
        if (context.state.categories && context.state.categories.length > 0) {
            resolve(context.state.categories);
            return;
        }
        getCategories().then(res => {
            context.commit(SET_CATEGORIES, res.data);
            resolve(res.data);
        }).catch(reject);
    });
},
123456789101112

完整例子

ts
                                                                  
import Vue from 'vue'
import Vuex, { Commit, Dispatch } from 'vuex'
import {
    getCategories,
} from '@/api'

Vue.use(Vuex)

export const SET_CATEGORIES = 'SET_CATEGORIES';

export interface State {
    categories: ICategory[],
};

export interface ICategory {
    id: number,
    name: string,
}

interface IActionContext {
    commit: Commit;
    state: State;
}

// initial state
const initState: State = {
    categories: [],
};

const getters = {};

const mutations = {
    [SET_CATEGORIES](state: State, categories: ICategory[]) {
        state.categories = categories;
    },
};

// actions
const actions = {
    getCategories(context: IActionContext) {
        return new Promise((resolve, reject) => {
            if (context.state.categories && context.state.categories.length > 0) {
                resolve(context.state.categories);
                return;
            }
            getCategories().then((res: ICategory[]) => {
                context.commit(SET_CATEGORIES, res.data);
                resolve(res.data);
            }).catch(reject);
        });
    },
};

const store = new Vuex.Store({
    state: initState,
    getters,
    actions,
    mutations,
});

/// 方便typscript 类型推导
export const dispatchCategories = (): Promise<ICategory[]> => store.dispatch('getCategories');

export default store;

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

在“应用和功能”中重置了VS2017部署Debug版本UWP,应用消失后无法再部署、安装

2019-02-19 04:19:40

问题详情

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 DEP0700: 应用程序注册失败。[0x80073CFB] 另一个用户已安装此应用的未打包版本。当前用户无法将该版本替换为打包版本。冲突程序包为 22bdd128-9c57-4102-b7a4-53d890e28a07,由 CN=ZoDream 发布。 ZoDream

解决方法

  1. 已管理员的身份运行 PowerShell

  2. 输入命令

batch
 
Get-AppxPackage "22bdd128-9c57-4102-b7a4-53d890e28a07" -AllUsers | Remove-AppxPackage
1

参考

  1. Cannot deploy app from VS2017 after resetting app from 'Apps & Features'

.NET Core 多模块

2019-01-11 07:04:53

说明

最近准备把本站源码从PHP 迁移到 .NET Core。遇到一个首要问题就是本站是分模块开发的,我也想过分成多个项目来做,但又涉及本站的基础框架,必须所有模块都能随着基础升级,我怕麻烦就整合到一起了。

做法

Areas 是 ASP.NET MVC 功能,在官方文档有介绍【Areas in ASP.NET Core

第一步

Startup.cs 中添加配置

c#
            
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMvc(routes =>
    {
        routes.MapRoute(
                name: "areaRoute",
                template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");  // 
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
123456789101112

第二步

新建文件夹 Areas/Blog/Controllers

新建控制器 HomeController.cs

c#
        
[Area("Blog")]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}
12345678

文件夹名可以用其他,关键 使用 Area 声明 区域,使路由能指向这里,改了文件夹也要记得改视图的文字

第三步

注册视图位置

c#
       
services.Configure<RazorViewEngineOptions>(options =>
{
    options.AreaViewLocationFormats.Clear();
    options.AreaViewLocationFormats.Add("/Categories/{2}/Views/{1}/{0}.cshtml");
    options.AreaViewLocationFormats.Add("/Categories/{2}/Views/Shared/{0}.cshtml");
    options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});
1234567

默认会自动从这些位置找

html
   
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
123

链接生成

HtmlHelper 语法

c#
 
@Html.ActionLink("Go to Services Home Page", "Index", "Home", new { area = "Services" })
1

TagHelper 语法

html
 
<a asp-area="Services" asp-controller="Home" asp-action="Index">Go to Services Home Page</a>
1

.NetCore 中间件学习

2019-01-11 06:29:50

中间件类写法

c#
                
    public class TestMiddleware
    {
        private readonly RequestDelegate _next;

        public TestMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext httpContext)
        {
            // TO DO
            return _next(httpContext);
        }
    }
12345678910111213141516

注册到宿主上

c#
         

    public static class TestMiddlewareExtensions
    {
        public static IApplicationBuilder UseTestMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<TestMiddleware>();
        }
    }
123456789

使用

Startup.cs

c#
       

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //app.Use<TestMiddleware>();
    app.UseTestMiddleware();
}
1234567

内联中间件

c#
         
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        // TO DO
        await next();
    });
}
123456789

vs 2017 使用中间件模板

右键项目》添加》新建项》已安装》ASP.NET Core》WEB》ASP.NET》中间件类

隐藏Apache版本号、PHP 版本信息

2019-01-09 06:24:25

隐藏apache版本号

文件位置:默认是在 httpd.conf 中,xampp 是在 httpd-default.conf 中

ini
        

# 找到ServerTokens和ServerSignature并修改为:

ServerTokens  Prod
ServerSignature  off

# 如果没有找到ServerTokens和ServerSignature可以在最后一行添加
12345678

隐藏Apache版本号

PS: 响应头还是会有 Server: Apache,只是去除了版本号

隐藏php版本号

文件位置:php.ini

ini
   

expose_php=Off
123

隐藏PHP版本信息

最后重启环境

参考

  1. Apache、PHP 隐藏版本信息

wow.js 搭配 animate.css 实现网页动起来

2019-01-06 05:36:29

原由

这篇教程只是给前端看的,简单介绍如何简单实现网页动起来效果。

准备

WOW.js官方示例

Animate.css官方示例

使用

  1. 在页面引入js、css文件
html
   
<link rel="stylesheet" href="animate.min.css">

<script src="wow.min.js"></script>
123
  1. 启动wow.js,配置样式
html
         
<style>
.wow { 
    visibility: hidden; 
}
</style>

<script>
new WOW().init();
</script>
123456789

说明:class wow 是wow.js 默认的标记,加上了这个,那这个元素就会动起来,必须设置样式为不可见,才能起到滚动到视窗出现突然出现的效果。

  1. 修改需要动画的元素
html
   

<section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s"></section>
123

说明:

class wow 是wow.js 默认的标记,可以修改。

class slideInLeft 是 Animate.css 的其中一种动画绑定的class。

data-wow-duration 是指动画执行时间,可以不设,Animate.css 自带默认

data-wow-delay 是指延迟多久执行动画,可以不设,Animate.css 自带默认

最终页面

html
               
<link rel="stylesheet" href="animate.min.css">
<style>
.wow { 
    visibility: hidden; 
}
</style>

<section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s"></section>

<section class="wow slideInLeft"></section>

<script src="wow.min.js"></script>
<script>
new WOW().init();
</script>
123456789101112131415

ORM 关联查询设计

2019-01-06 05:03:27

使用方法

php
       

$this->hasOne(class, 'local_key', 'foreign_key');
$this->hasOne(table, table_id, id);

$this->hasMany(class, 'local_key', 'foreign_key');
$this->hasMany(table, id, table_id);
1234567

流程

php
                  
with('w')
with(['w' => function($query) { $query->select('a') }])
with('w:a')
    eagerLoad['w'] = function(){}
    eagerLoad['w'] = function($query) { $query->select('a') }
    eagerLoad['w'] = function($query) { $query->select('a') }

get()
    $model->w()
        $this->hasOne(table, table_id, id)
        $this->hasOne(table, table_id, id)->select('');
            Relation{table, local_key, foreign_key} : Query

    eagerLoad['w'](Relation)
    Relation(id[])->getResult()

    w => array
123456789101112131415161718

商城活动构思

2019-01-06 05:02:57

单个商品活动显示

  1. 获取所有活动

  2. 判断是否属于此商品

购物活动显示

  1. 获取所有活动

  2. 判断是否属于此商品

准备参加活动

  1. 获取所有活动

  2. 根据购物车中商品能参加的活动

  3. 判断用户等级

  4. 判断参加活动的金额

  5. 判断是否已存在购物车

  6. 判断是否参加过此活动

  7. 加入活动

参加中的活动

  1. 获取购物车中的活动

  2. 判断是否过期

  3. 根据购物车商品数量改删,判断金额是否还符合

确定参加活动(生成订单)

  1. 获取选择的活动

  2. 判断是否过期

  3. 判断是否含有触发活动的商品

  4. 判断选择的商品的金额

优化构思

  1. 根据最近活动过期时间缓存

  2. 缓存所有的数据,比如活动匹配的商品id,活动的等级,活动的金额,活动的时间

RBAC 权限管理(一)

2019-01-05 04:48:17

介绍

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限-资源”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,权限与资源之间一般是多对多的关系。

概念性模型

1.RBAC0的模型中包括用户(U)、角色(R)和许可权(P)等3类实体集合。

2.RBAC1,基于RBAC0模型,引入角色间的继承关系,即角色上有了上下级的区别,角色间的继承关系可分为一般继承关系和受限继承关系。一般继承关系仅要求角色继承关系是一个绝对偏序关系,允许角色间的多继承。而受限继承关系则进一步要求角色继承关系是一个树结构,实现角色间的单继承。

3.RBAC2,基于RBAC0模型的基础上,进行了角色的访问控制。添加了责任分离关系。

4.基于RBAC0的基础上,将RBAC1和RBAC2进行整合了。

多语言网站思考

2019-01-03 18:35:53

静态内容的翻译

这些内容基本上是写死在网页文件里的,主要是通过基于 i18n 调用不同语言包实现。

实现原理,通过中间语句去语言包中匹配,然后输出匹配到的目标语言语句。

动态内容的翻译

这些内容是通过后台添加的内容,

  1. 可以直接调用翻译接口实时翻译,但是一般翻译不准确
  2. 建立一个专门的翻译表,所有需要翻译的内容都放里面。优点可以选择翻译翻译部分,拓展性好
  3. 建立附加字段,进行列的增加,一列内容对应一列翻译内容。
  4. 分站点。缺点各站点数据关联性差。

Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.

2018-12-29 06:23:47

安装 PHPMyAdmin 4.8 以后,使用浏览器登录不上,报错

text
 
Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.
1

临时解决方法

  1. 换回phpMyAdmin 4.7
  2. 使用浏览器的隐私模式访问,谷歌的无痕窗口访问亲测有效

TCP 连接过程

2018-12-29 06:22:23

开始连接(三次握手)

首先确认双方收发功能是否正常

第一次握手,服务端确认客户端的发送能力、服务端的接收能力。客户端发送一个SYN段,并指明客户端的初始序列号,即ISN(c).

第二次握手,客服端确认服务端的接收、发送能力,客户端的接收、发送能力。服务端发送自己的SYN段作为应答,同样指明自己的ISN(s)。为了确认客户端的SYN,将ISN(c)+1作为ACK数值。这样,每发送一个SYN,序列号就会加1. 如果有丢失的情况,则会重传。

第三次握手,服务端确认客户端的接收、发送能力,服务端的发送、接收能力。为了确认服务器端的SYN,客户端将ISN(s)+1作为返回的ACK数值。

确认之后,正式收发数据

结束连接(四次挥手)

第一次挥手,客户端发送一个FIN段,并包含一个希望接收者看到的自己当前的序列号K. 同时还包含一个ACK表示确认对方最近一次发过来的数据。

第二次挥手,服务端将K值加1作为ACK序号值,表明收到了上一个包。这时上层的应用程序会被告知另一端发起了关闭操作,通常这将引起应用程序发起自己的关闭操作。

第三次挥手,服务端发起自己的FIN段,ACK=K+1, Seq=L

第四次挥手,客户端确认。ACK=L+1

DOS 攻击

最基本的DoS攻击就是利用合理的服务请求(发送大量的SYN包)来占用过多的服务资源,从而使合法用户无法得到服务的响应。

DDOS 攻击

分布式拒绝服务攻击采取的攻击手段就是分布式的。

按照TCP/IP协议的层次可将DDOS攻击分为基于ARP的攻击、基于ICMP的攻击、基于IP的攻击、基于UDP的攻击、基于TCP的攻击和基于应用层的攻击。

参考

1.“三次握手,四次挥手”你真的懂吗?

JSON RPC

2018-12-28 04:26:35

基本请求结构

json
      
{
    "jsonrpc": "2.0", 
    "method": "subtract", 
    "params": [42, 23], 
    "id": 1
}
123456

jsonrpc 为固定参数

method 需要调用的方法

params 方法值,非必须

id 可以是数值或字符串,与响应相对应

基本响应结构

json
     
{
    "jsonrpc": "2.0",
    "result": 19, 
    "id": 1
}
12345

通知类型响应为空

批量调用请求结构

json
        
[
    {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
    {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
    {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
    {"foo": "boo"},
    {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
    {"jsonrpc": "2.0", "method": "get_data", "id": "9"} 
]
12345678

响应结构

json
       
[
    {"jsonrpc": "2.0", "result": 7, "id": "1"},
    {"jsonrpc": "2.0", "result": 19, "id": "2"},
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
    {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
    {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
]
1234567

根据 id 确定响应的值

响应错误

code message meaning
-32700 Parse error Invalid JSON was received by the server.An error occurred on the server while parsing the JSON text.
-32600 Invalid Request The JSON sent is not a valid Request object.
-32601 Method not found The method does not exist / is not available.
-32602 Invalid params Invalid method parameter(s).
-32603 Internal error Internal JSON-RPC error.
-32000 to -32099 Server error Reserved for implementation-defined server-errors.

JSON-RPC 与 RESTFUL 比较

json rpc 适用与服务器内部通信,例如分布式架构

restful 适用于对外通信,例如与浏览器,APP 等

参考

1.JSON-RPC 2.0 Specification

VSCode 遭遇typescript language server 初始化失败

2018-12-25 07:16:45

症状

同一个项目,在笔记本上打开是正常的,到了台式机上一直在初始化 typscript 语言服务,

输出显示

text
               

[Error  - 9:59:09 PM] ReaderError
RangeError: out of range index
    at Buffer.copy (buffer.js:602:18)
    at l.tryReadContent (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:161851)
    at t.Reader.onLengthData (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:162417)
    at Socket.t.Reader.i.Disposable.constructor.e.on.e (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:162184)
    at emitOne (events.js:116:13)
    at Socket.emit (events.js:211:7)
    at addChunk (_stream_readable.js:263:12)
    at readableAddChunk (_stream_readable.js:250:11)
    at Socket.Readable.push (_stream_readable.js:208:10)
    at Pipe.onread (net.js:594:20)
[Error  - 9:59:09 PM] TSServer exited with code: 3
123456789101112131415

解决方法

  1. 在设置里增加 "typescript.tsserver.log": "verbose",
  2. 重启vscode,再次查看输出,会在第二行出现日志文件路径
  3. 等到输出报错后,打开日志文件,查看报错原因
  4. 才发现是有.ts视频文件也被识别进去
  5. 删除.ts视频文件或在 tsconfig.json 加入排除文件夹 "exclude": []

报错原因有多种具体请用1、2、3步自行查看或到vscode 提交issues并附上日志内容

【说明】关于本站《圣诞》主题

2018-12-24 07:22:38

《圣诞》主题上线

已知问题

雪花插件未做适应浏览器尺寸变化处理

素材来源

1.背景图片

2.雪花图片来自逐梦博客百度图片

重新思考框架架构

2018-12-24 00:19:47

当前版本

php
                            

$app = new Web();
    $app->register();
$app->autoResponse();
    $response = $app->handle($uri);
        $app->isAllowDomain();  // 这里会判断是否允许域名访问
        $uri = $app->fomat($uri);
            $app['url']->deRewrite($uri);
        $route = $app[Router]->handle($uri)
            new Router()
                $router->get('');
            reurn new Route();
        $route->handle($app['request'], $app['response'])
            $module = $router->getMoudle()
            $module->boot();
            $module->invoke()
                return
            $package = $module->getControlerNamespace();
            $controller = $route->getController($package)
            $controller->init();
            $controller->invoke($action);
                $controller->beforeFilter()
                $controller->$action()
                    $app['view']->render()
    $response->send();
        $response->sendHeader();
        $response->sendContent();
12345678910111213141516171819202122232425262728

中间件版本

php
                          

$app = new Web()
$app->middleware(Middleware::class)
$app->middleware(function($req, $resp, $next))
$response = $app->handle($uri)
    DomainMiddleware       // 是否允许当前域名
        CORSMiddleware     // 跨域验证
            CacheMiddleware    // 静态缓存检测
                GZIPMiddleware   // 压缩输出
                    RewriteMiddleware   // 重写解析还原真实
                        RouterMiddleware   // 路由解析,下一步到控制器
                            MatchRouteMiddle
                                [any]Middleware
                            ModuleMiddleware
                            DefaultRouteMiddle
                                Controller
                                    CSRFMiddleware
                                        RuleMiddleware  // 控制器中的规则过滤
                                            AuthMiddleware        // 登陆验证
                                            HttpMethodMiddleware  // 请求方式验证
                                            RoleMiddleware      // 角色验证
                                                UriParameterMiddleware // 方法参数过滤
                                                    ResponseBodyMiddleware   // 加密解密

$response->send();
1234567891011121314151617181920212223242526

目前还不确定是否升级

页面部件注册思路

2018-12-24 00:15:46

php
                                           

app('page')->node('feedback', ['limit' => 12]);  // 使用,获取部件对象(单例)

// app('page')->feedback(['limit' => 12]);      // 另一种使用方式

    $page = new Page();                          // 未启动是启动

        $page->loadNodes();                      // 注册所有部件  

            $page->register('feedback', Feedback::class);  // 注册单个部件

    $page->__call('feedback', $attrs);          // 第二种方式调用

        $page->node('feedback', $attrs);

            $feedback = $page->instance('feedback')  // 获取部件单例(返回克隆的对象)

                $feedback = new Feedback();         //  初始化

                    $page->on('feedback_list', function () {return $data;});                      // 注册需要的资源,避免重复请求数据库

                return $feedback

            $feedback->attr($attrs);               // 配置参数 

            return this;

        return;

    return;


$feedback->__toString()                           // <?= ?> 使用时自动调用方法

    $feedback->render();                          // 开始输出数据

        $data = $page->trigger('feedback_list');  // 获取已注册的资源

        return $feedback->renderHtml($data);

    return;

12345678910111213141516171819202122232425262728293031323334353637383940414243

Canvas 绘图回顾

2018-12-22 07:26:47

基本

获取页面元素

js
  
let canvas = document.getElementById("canvas"),
    context = canvas.getContext("2d");
12

虚拟缓存(不显示在页面,主要是用于缓存部分场景)

js
  
let cacheCanvas = document.createElement("canvas"),
    cacheContext = cacheCanvas.getContext("2d");
12

应用缓存

js
 
context.drawImage(cacheCanvas, 0, 0); 
1

绘制基本图形

绘制文字

js
      
context.font = 'bold 35px Arial';
context.textAlign = 'center';
context.textBaseline = 'bottom';
context.fillStyle = '#ccc';
context.strokeText("Hello Canvas", 150, 100, 200);
context.fillText("Hello Canvas", 180, 140);
123456

绘制图片

js
            
let img = new Image();
img.src = '';
if(img.complete) {
    context.drawImage(img, 0, 0); 
} else {
    img.onload = function(){
     context.drawImage(img, 0, 0); 
   };
   img.onerror = function(){
     alert('加载失败,请重试');
   };
}
123456789101112

高级绘图

绘制游戏画面,先把背景、元素都分成不同的缓存,设定刷新时间,并组成不同的场景

css3 学习之 attr()

2018-12-21 05:41:52

说明

接受两个参数, 第一个参数时主元素的属性名,可以跟转化类型,第二个是默认值,

例如:

css
   
a::before {
    color: attr(data-color color, red);
}
123

字符串拼接

css
   
a::before {
    content: "Hi " attr(data-hover);
}
123

一个切换例子

css
                                    
.box {
    font-size: 55px;
    overflow: hidden;
    position: relative;
}
.box span {
    display: inline-block;
    position: relative;
    transition: transform 0.3s;
}
.box span:first-child {
    color: #666;
}
.box span:nth-child(2) {
    color: #daa520;
}
.box span:nth-child(2)::before {
    bottom: 105%;
    color: #666;
}
.box span:first-child::before {
    top: 105%;
    color: #daa520;
}
.box span:first-child::before,
.box span:nth-child(2)::before {
    position: absolute;
    content: attr(data-hover);
}
.box:hover span:first-child {
    transform: translate3d(0, -105%, 0);
}
.box:hover span:nth-child(2) {
    transform: translate3d(0, 105%, 0);
}
123456789101112131415161718192021222324252627282930313233343536
html
      

<div class="box">
    <span data-hover="my">my</span>
    <span data-hover="friend">friend</span>
</div>
123456

Ueditor 与textarea切换 及 与pjax 结合

2018-12-21 04:12:24

页面代码

html
  

<textarea id="container">
12

Gulp 实现根据参数处理不同任务

2018-12-20 05:25:15

需求来源

Gulp 一般使用一个默认的任务,如果需要处理不同的文件就多建几个任务,但是这是一般的做法。

比如本站源码就分成多个模块,项目结构都一样,如果按一般的做法就是要建无数个任务,关键是项目是不断开发中的,不可能每次都新建,所以就想怎么自动切换处理不同文件夹。

解决方法

Gulp 调用不同的任务是通过 gulp task 这个调用task任务的,如果不存在task任务就会报错,那有没有可能 通过 gulp task 调用处理 task 这个文件夹呢?

1.首先获取 参数

js
    
var name = '';
if (process.argv && process.argv.length > 2) {
    name = process.argv[2]; // 这就获取到了参数
}
1234

2.然后根据参数改变目标文件夹

js
 
gulp.src(name + '/*.css')
1

但是 gulp task 是会调用 task 任务的,那么可以

3.临时生成 task 任务

js
 
gulp.task(name || 'z', build);
1

name 可能为空,但不能生成一个空命名的任务

4.最终结果

js
               
var gulp = require('gulp'),
    minCss = require('gulp-clean-css'),
    name = '';
if (process.argv && process.argv.length > 2) {
    name = process.argv[2]; // 这就获取到了参数
}
function cssTask() {
    return gulp.src('src/' name + "/*.css")
        .pipe(minCss())
        .pipe(gulp.dest('dist/'));
}
exports.cssTask = cssTask;
var build = gulp.series(gulp.parallel(cssTask));
gulp.task(mo, build);
gulp.task('default', build);
123456789101112131415

测试 压缩 test 文件夹下的css

bash
 
gulp test
1

box-shadow实现四周阴影

2018-12-20 05:24:00

第一种

这个只是简单的进行四个方向的阴影,会出现阴影效果不真实,缺角不连贯

css
              

/*说明:(以上部边为例进行说明)
1. 对于上边,沿x轴方向的偏移量显然没有意义,设为0px;
2. 沿y轴正方向阴影进入div内部,不显示,因此写为负数;
3. 扩展半径不要写,或者写成0px,这样就不会影响其他的边;
4. 颜色自定;
5. 模糊程度按需要自定;
6. 下、左、右边阴影按规律类推。
*/
 box-shadow:    0px -10px 0px 0px #ff0000,   /*上边阴影  红色*/
                -10px 0px 0px 0px #3bee17,   /*左边阴影  绿色*/
                10px 0px 0px 0px #2279ee,    /*右边阴影  蓝色*/
                0px 10px 0px 0px #eede15;    /*下边阴影  黄色*/
1234567891011121314

第二种

真正意义上的全阴影,但是阴影的效果相对于单边阴影距离减半,所以要设得更大

css
      
div{
    width:250px;
    height:250px;
    background:greenyellow;
    box-shadow:black 0px 0px 10px;//将颜色提到前面,且将h-shadow,v-shadow设为0px,实现四周阴影
}
123456

【参考】

1.box-shadow实现四周阴影

2.DIV四个边框分别设置阴影样式

那些可以一用的浏览器api

2018-12-20 05:22:31

page lifecycle(网页生命周期)

document.visibitilityState 来监听网页可见度,是否卸载

focus 事件

focus事件在页面获得输入焦点时触发。

blur 事件

blur事件在页面失去输入焦点时触发。

visibilitychange 事件

visibilitychange事件在网页可见状态发生变化时触发.

js
         
window.addEventListener('visibilitychange',() => {
    // 通过这个方法来获取当前标签页在浏览器中的激活状态。
    switch(document.visibilityState){
        case'prerender': // 网页预渲染 但内容不可见
        case'hidden':    // 内容不可见 处于后台状态,最小化,或者锁屏状态
        case'visible':   // 内容可见
        case'unloaded':  // 文档被卸载
    }
});
123456789

freeze 事件

freeze事件在网页进入挂起状态时触发。

resume 事件

resume事件在网页离开挂起,恢复时触发。

pageshow 事件

pageshow事件在用户加载网页时触发。这时,有可能是全新的页面加载,也可能是从缓存中获取的页面。如果是从缓存中获取,则该事件对象的event.persisted属性为true,否则为false

pagehide 事件

pagehide事件在用户离开当前网页、进入另一个网页时触发。它的前提是浏览器的 History 记录必须发生变化,跟网页是否可见无关。

beforeunload 事件

beforeunload事件在窗口或文档即将卸载时触发。

unload 事件

unload事件在页面正在卸载时触发

online state(网络状态)

js
   
window.addEventListener('online',onlineHandler)

window.addEventListener('offline',offlineHandler)
123

Vibration(震动)

js
      
// 可以传入一个大于0的数字,表示让手机震动相应的时间长度,单位为ms
navigator.vibrate(100)
// 也可以传入一个包含数字的数组,比如下面这样就是代表震动300ms,暂停200ms,震动100ms,暂停400ms,震动100ms
navigator.vibrate([300,200,100,400,100])
// 也可以传入0或者一个全是0的数组,表示暂停震动
navigator.vibrate(0)
123456

device orientation(陀螺仪)

js
     
window.addEventListener('deviceorientation',e => {
    console.log('Gamma:',e.gamma);  // 设备沿着Y轴的旋转角度
    console.log('Beta:',e.beta);    //设备沿着X轴的旋转角度
    console.log('Alpha:', e.alpha);  //设备沿着Z轴的旋转角度
})
12345

注意

alpha 是以手机自带指南针为标准,及手机认为的南方为 0 ,需要注意手机不一定支持指南针,所以会出现偏差

battery status(电量)

js
                  
// 通过这个方法来获取battery对象
navigator.getBattery().then(battery => {
    // battery 对象包括中含有四个属性
    // charging 是否在充电
    // level   剩余电量
    // chargingTime 充满电所需事件
    // dischargingTime  当前电量可使用时间
    const { charging, level, chargingTime, dischargingTime } = battery;
    // 同时可以给当前battery对象添加事件  对应的分别时充电状态变化 和 电量变化
    battery.onchargingchange = ev => {
        const { currentTarget } = ev;
        const { charging } = currentTarget;
    };
    battery.onlevelchange = ev => {
        const { currentTarget } = ev;
        const { level } = ev;
    }
})
123456789101112131415161718

【参考】

  1. 你(可能)不知道的web api
  2. Page Lifecycle API 教程

Angular CORS 跨域请求解决方法

2018-12-07 07:04:06

CORS:跨域资源共享

实际分两步请求:

第一步.预检:

(无论响应什么数据都不会传到js程序里,只是浏览器做判断用,可以根据 请求方式 OPTIONSOrigin 请求头判断是否是预检)

请求头:OPTIONS

   
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
Origin: http://localhost:4200
123

Access-Control-Request-Headers 告诉服务端第二步将带的请求头

Access-Control-Request-Method 第二步将使用的请求方式

Origin 当前的域名

允许的响应头:

     
Access-Control-Allow-Credentials:
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 0
12345

Access-Control-Allow-Credentials 告诉浏览器第二步是否允许带上cookie 默认不带, true 为带上

Access-Control-Allow-Headers 允许带的请求头,必须全匹配才行, * 表示允许任何请求头

Access-Control-Allow-Methods 允许的请求方式 * 都允许

Access-Control-Allow-Origin 允许的请求域名 * 不限制

Access-Control-Max-Age 这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 提供的信息) 可以被缓存多久。

注:需要注意的是Access-Control-Max-Age的设置针对完全一样的url,如果url加上路径参数,其中一个urlAccess-Control-Max-Age设置对另一个url没有效果

第二步.正式请求:

(如果第一步没有获取到允许的响应头就不会发生第二步)

本次依然需要加上允许的跨域的响应头,js程序才能接收到相应的数据

.NetCore 使用 Mysql

2018-12-07 06:33:10

准备

NUGET 添加依赖项

c#
 
MySql.Data.EntityFrameworkCore
1

使用

创建数据库映射

c#
          
public class user
{
    public int id { get; set; }

    public string name { get; set; }

    public string email { get; set; }

    public string avatar { get; set; }
}
12345678910

新建 DbContext类

c#
             
public class DBContext : DbContext
{
    public DBContext(DbContextOptions options)
    : base(options)
    {
    }

    public DbSet user { get; set; }

    //string str = @"Data Source=;Database=;User ID=;Password=;pooling=true;CharSet=utf8;port=3306;sslmode=none";
    //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
    //    optionsBuilder.UseMySQL(str);
}
12345678910111213

appsettings.json里添加连接字符串

json
           
{
    "Logging": {
        "LogLevel": {
            "Default": "Warning"
        }
    },
    "AllowedHosts": "*",
    "ConnectionStrings": {
        "MysqlConnection": "Data Source=localhost;Database=zodream;User ID=root;Password=root;pooling=true;CharSet=utf8;port=3306;sslmode=none"
    }
}
1234567891011

Startup.cs文件的ConfigureServices方法注册

c#
      
public void ConfigureServices(IServiceCollection services)
{
    var connection = Configuration.GetConnectionString("MysqlConnection");
    services.AddDbContext(options => options.UseMySQL(connection));
    services.AddMvc();
}
123456

使用

c#
           
private readonly DBContext _db;
//通过.NET Core框架自动为我们做构造函数依赖注入IOC。
public HomeController(DBContext db)
{
    _db = db;
}
public ActionResult Index()
{
    var list= _db.student.ToList();
    return View();
}
1234567891011

【参考】

1.《.NetCore中EFCore for MySql

windows server 2016 安装 IIS 、PHP 7.2、Mysql 8.0

2018-10-22 04:42:42

准备:

windows server 2016

Notepad++ 下载

PHP 下载

MYSQL 下载

iis rewrite 下载

HeidiSQL 下载

cacert.pem 下载

安装

第一步,安装notepad++

第二步,安装PHP

解压PHP 到指定文件夹, 例如:C:\Program Files\PHP

修改文件名为 php.ini

配置

修改拓展文件夹路径

 
extension_dir = "ext"
1

启用插件

        
extension=curl
extension=fileinfo
extension=gd2
extension=gettext
extension=mbstring
extension=mysqli
extension=openssl
extension=pdo_mysql
12345678

配置时区

 
date.timezone = PRC
1

配置openssl证书

 
openssl.cafile=cacert.pem
1

xdebug配置(需下载php_xdebug.dll

           
[Xdebug]
zend_extension="ext/php_xdebug.dll"
xdebug.auto_trace=1
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.trace_output_dir="D:/zodream/xdebug/trace"
xdebug.profiler_enable=1
xdebug.profiler_output_dir="D:/zodream/xdebug/profiler" 
xdebug.var_display_max_children=1280
xdebug.var_display_max_data=5120
xdebug.var_display_max_depth=200
1234567891011

第三步,安装IIS

启用“web服务器(IIS)”,启用“iis 可承载web核心”,启用“CGI

加载PHP

在iis主页 点击“处理程序映射”,右击空白处点击“添加模块映射”,配置

添加默认文档:index.php

安装 iis rewrite

第四步,安装MySQL

解压 mysql.zip 文件到指定文件夹,例如:C:\Program Files\MYSQL

添加文件 my.ini(把{path}全部替换为当前文件夹路径)

                         
[mysqld]
# 设置3306端口
port=3306
# 设置mysql的安装目录
basedir="{path}" 
# 设置mysql数据库的数据的存放目录
datadir="{path}\data"
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统
max_connect_errors=10
# 服务端使用的字符集默认为UTF8
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8
12345678910111213141516171819202122232425

以管理员打开CMD,进入mysqlbin 文件夹下

初始化

 
mysqld --initialize --console
1

安装服务

 
mysqld -install
1

启动服务

 
net start mysql
1

第五步,安装HeidiSQL

解压到任意文件,双击运行 heidisql.exe 运行

登录mysql, 第一次登录会要求新设密码

第六步,测试 在 C:\inetpub\wwwroot 新建 index.php

php
   
<?php
phpinfo();
123

使用浏览器访问 http://localhost

【注意】

不同phpmysql 版本依赖不同的vc,大致需要 vc9vc11vc12vc14,也分x86x64,本次安装不需要额外安装

GO Methods 中指针和引用

2018-09-22 02:56:44

go
                                         
package main

import (
    "fmt"
)

type Hub struct {
    width, height int
}

func (r Hub) size() int  {
    return r.width * r.height
}

func (r *Hub) bound() int  {
    return r.width * r.height
}

func (r Hub) setX(x int)  {
    r.width = x
}

func (r *Hub) setY(y int)  {
    r.height = y
}

func main()  {
    hub := Hub{width: 1, height: 2}

    fmt.Println(hub)
    fmt.Println(hub.size())
    fmt.Println(hub.bound())

    hub.setX(10)
    hub.setY(20)

    fmt.Println(hub)
    fmt.Println(hub.size())
    fmt.Println(hub.bound())
}
1234567891011121314151617181920212223242526272829303132333435363738394041

输出

       
{1 2}
2
2
{1 20}
20
20
1234567

struct 的方法中尽可能使用 指针 ,除非不希望方法中改变影响主体内容才使用引用

引用实际上是复制主体,会花费相对较多的系统开销(内存和时间)

在方法中改变参数会改变指针主体参数,不改变引用主体参数

架构、框架、模式、模块、组件、插件、控件、中间件

2018-07-04 07:11:32

架构

软件架构,也成称为软件体系结构,简单地说就是一种设计方案,将用户的不同需求抽象成组件,且能够描述组件之间的通信和调用。软件架构会分析工程中的问题,针对问题设计解决方案,针对解决方案分析应具有的功能,针对功能设计软件系统的层次和模块及层次模块之间的逻辑交互关系,确定各个功能如何由这些逻辑实现。开发人员可以根据软件架构分析出来的层次和架构进行软件编写。

【理解】:综合需求和能力对有关软件整体结构与组件的抽象描述

框架

软件框架,是软件开发过程中提取软件的共性部分形成的体系结构。框架不是现成可用的应用系统,而是一个半成品,是一个提供了诸多服务,供开发人员进行二次开发,实现具体功能的程序实体。

框架与架构的关系:框架不是架构,框架比架构更具体,更偏重于技术,而架构更偏重于设计;架构可以通过多种框架来实现。

【理解】:对普遍使用的方法进行的归类、封装,不进行具体业务处理实现,但提供全面的处理方法

模式

设计模式强调的是一个设计问题的解决方法,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

框架与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,一个模式可应用于不同的框架和被不同的程序语言所实现;而框架则是一个应用的体系结构,是一种或多种设计模式和代码的混合体。设计模式的思想可以在框架设计中进行应用。

架构与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,范畴比较小;而架构是高层次的针对体系结构的一种设计思路,范畴比较大。一个架构中可能会出现多个设计模式的思想。

【理解】:对程序的整体结构的概括

模块

根据不同的标准,通常会说程序模块或功能模块,程序模块指的是一段能够实现某个目标的成员代码段,功能模块则用来说明一个功能所包含的系统行为。定义模块的原则是:高内聚和低耦合。

【理解】:实现大型软件系统的一部分功能的程序

组件

组件是封装了一个或多个程序模块的实体。组件强调的是封装,利用接口进行交互。组件也称为构建。插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。

【理解】:对数据和方法简单封装的对象

插件

插件属于组件,插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。插件是一种电脑程序,通过和应用程序的互动,来为应用程序增加一些特定的功能,仅靠插件是无法正常运行的,需要依赖于应用程序才能发挥自身功能。插件和应用程序之间通过接口进行交互。

【理解】:根据规范的接口编写的不能脱离平台单独运行的程序

控件

对数据和方法封装的可视化组件。

【理解】:接受输入数据的组件

中间件

主要解决异构网络环境下分布式应用软件的互连与互操作问题,提供标准接口、协议,屏蔽实现细节,提高应用系统易移植性。

【理解】:从不同的输入来源转化成标准的数据,通过标准的接口进入不同的底层执行处理

CentOs 7 安装apche php mysql

2018-06-20 20:47:23

Apache

  1. yum 安装Apache
 
yum install httpd
1
  1. 设置服务器开机自动启动Apache
 
systemctl enable httpd.service
1
  1. 手动启动Apache
 
systemctl start httpd.service
1

在浏览器中输入IP地址即可验证是否启动成功

  1. 手动重启Apache
 
systemctl restart httpd.service
1
  1. 手动停止Apache
 
systemctl stop httpd.service
1
  1. 开放80端口

开启端口

 
firewall-cmd --zone=public --add-port=80/tcp --permanent
1

命令含义:

--zone #作用域

--add-port=80/tcp #添加端口,格式为:端口/通讯协议

--permanent #永久生效,没有此参数重启后失效

重启防火墙

 
firewall-cmd --reload
1

查看状态

 
firewall-cmd --state
1

MySQL

首先检查 MySQL 是否已安装

 
yum list installed | grep mysql
1

如果有的话 就全部卸载

 
yum -y remove +数据库名称
1

MySQL 依赖 libaio,所以先要安装 libaio

 
yum search libaio
1

检索相关信息

 
yum install libaio
1

安装依赖包

下载 MySQL Yum Repository

地址为 http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm

 
wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
1

PS:如果提示-bash: wget: 未找到命令,请先执行 yum install wget 安装 wget

添加 MySQL Yum Repository 添加 MySQL Yum Repository 到你的系统 repository 列表中,执行

 
yum localinstall mysql-community-release-el7-5.noarch.rpm
1

验证下是否添加成功

 
yum repolist enabled | grep "mysql.*-community.*"
1

选择要启用 MySQL 版本

查看 MySQL 版本,执行

 
yum repolist all | grep mysql
1

可以看到 5.5, 5.7 版本是默认禁用的,因为现在最新的稳定版是 5.6

 
yum repolist enabled | grep mysql
1

查看当前的启动的 MySQL 版本

通过 Yum 来安装 MySQL

执行

 
yum install mysql-community-server
1

Yum 会自动处理 MySQL 与其他组件的依赖关系.

启动和关闭 MySQL Server

启动 MySQL Server

 
systemctl start  mysqld
1

查看 MySQL Server 状态

 
systemctl status  mysqld
1

关闭 MySQL Server

 
systemctl stop mysqld
1

MySQL 安全设置

服务器启动后,可以执行

 
mysql_secure_installation;
1

安装Mysql 8

  1. yum仓库下载MySQL:
 
sudo yum localinstall https://repo.mysql.com//mysql80-community-release-el7-1.noarch.rpm
1
  1. yum安装MySQL:
 
sudo yum install mysql-community-server
1
  1. 启动MySQL服务:
 
sudo service mysqld start
1
  1. 检查MySQL服务状态:
 
sudo service mysqld status
1
  1. 查看初始密码(如无内容直接跳过):
 
sudo grep 'temporary password' /var/log/mysqld.log
1
  1. 本地MySQL客户端登录:
 
mysql -uroot -p
1
  1. 输入密码为第5步查出的,如果没有,直接回车,然后输入命令
 
flush privileges
1
  1. 修改root登录密码:
 
ALTER USER 'root'@'localhost' IDENTIFIED BY '密码';
1

(注意要切换到mysql数据库,使用use mysql)

防火墙设置

远程访问 MySQL, 需开放默认端口号 3306.

执行

  
firewall-cmd --permanent --zone=public --add-port=3306/tcp
firewall-cmd --permanent --zone=public --add-port=3306/udp
12

这样就开放了相应的端口。

执行

 
firewall-cmd --reload
1

PHP

  1. 安装epel-release
 
yum -y install epel-release
1
  1. 安装PHP7
 
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm
1

成功获取PHP7的yum源,然后再执行:

 
yum install php70w
1

源码安装PHP7

下载源码

 
wget http://php.net/get/php-7.2.6.tar.gz/from/a/mirror
1
  1. 解压
 
tar -zxvf php-7.2.6.tar.gz
1
  1. 进入解压包安装一些必要的依赖
 
yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel
1
  1. 安装gcc
 
yum install gcc
1
  1. 安装
     
yum -y install libxslt-devel* 
yum -y install perl* 
yum -y install httpd-devel
find / -name apxs
12345

得到的路径是:/usr/bin/apxs

于是得到--with-apsx2的路径是/usr/bin/apxs

  1. 配置
 
./configure --prefix=/usr/local/php7 --with-curl --with-freetype-dir --with-gd --with-gettext --with-iconv-dir --with-kerberos --with-libdir=lib64 --with-libxml-dir --with-mysqli --with-openssl --with-pcre-regex --with-pdo-mysql --with-pdo-sqlite --with-pear --with-png-dir --with-xmlrpc --with-xsl --with-zlib --enable-fpm --enable-bcmath -enable-inline-optimization --enable-gd-native-ttf --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-shmop --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-zip --enable-pcntl --with-curl --with-fpm-user=nginx --enable-ftp --enable-session --enable-xml --with-apxs2=/usr/bin/apxs
1
  1. 编译
 
make
1
  1. 安装
 
make install
1
  1. 添加环境变量
 
vi /etc/profile
1

在末尾加入:

  
PATH=$PATH:/usr/local/php7/bin
export PATH
12
  1. 使改动立即生效
 
source /etc/profile
1
  1. 查看php版本
 
php -v
1

(如果有问题 请检查添加的环境变量是否是PHP安装目录里的bin目录)

  1. 生成必要文件
    
cp php.ini-production /usr/local/php7/etc/php.ini
cp sapi/fpm/php-fpm /usr/local/php7/etc/php-fpm
cp /usr/local/php7/etc/php-fpm.conf.default /usr/local/php7/etc/php-fpm.conf
cp /usr/local/php7/etc/php-fpm.d/www.conf.default /usr/local/php7/etc/php-fpm.d/www.conf
1234
  1. 配置

如果报错 请敲这行查报错信息 可以查到哪个文件第几行出错:

 
systemctl status httpd.service
1

修改Apache默认欢迎页:

 
vi /etc/httpd/conf.d/welcome.conf
1

将/usr/share/httpd/noindex 修改为/var/www

修改Apache配置:

  
vi /etc/httpd/conf/httpd.conf
DocumentRoot "/var/www/"
12

(请注意,/var/www这个路径是自定义,在配置文件中有好几处这个路径,如果更改,请全局搜索一下都改掉)

找到

  
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
12

在后面添加

  
AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .php7
12

搜索<IfModule dir_module>下面这一块添加上index.php

    
<IfModule dir_module>
 DirectoryIndex index.html index.php
</IfModule>
1234

搜索有没有下面这一行:

 
LoadModule php7_module modules/libphp7.so
1

如果没有 请手动添加 否则 会出现运行php文件变成下载

在最下面配置域名

conf
           
<VirtualHost *:80>
 DocumentRoot /var/www
 ServerName www.你的域名.com
 ServerAlias 你的域名.com
 <Directory /phpstudy/www>
    Options +Indexes +FollowSymLinks +ExecCGI
    AllowOverride All
    Order Deny,Allow
    Allow from all
 </Directory>
</VirtualHost>
1234567891011
  1. 测试

【参考】

  1. Centos7 下安装Apache2 + MySQL + PHP7

  2. CentOS7使用yum安装MySQL8.0

  3. CentOS 7.3下配置 Apache2.4 + MySQL5.7 + PHP7.1.8

  4. centos7下源码编译配置 apache2.4+mysql5.6+php7.1

vs 2017 编写第一个 C 程序

2018-03-30 07:56:32

正式开始学习C语言。

准备:

《C Primer Plus》第六版

windows 10 + vs2017 社区版

第一次写的程序当然是 《Hello World》

第一步打开vs2017

第二步新建项目

编写C程序新建项目

编写C程序

第三步添加源文件

编写C程序

编写C程序

第四步写代码

      
#include <stdio.h>

int main(void) {
    printf("Hello World!");
    return 0;
}
123456

第五步编译

控制台会一闪而过,就表示编译成功!

好了,第一个c程序就这么简单完成了。

input file控件限制上传文件类型

2018-12-23 21:55:30

直接设置文件拓展名

多个用英文逗号隔开

html
 
<input type="file"  accept=".xls,.doc,.txt,.pdf"  />
1

通过文件类型

文件 类型
*.3gpp audio/3gpp
*.ac3 audio/ac3
*.asf allpication/vnd.ms-asf
*.au audio/basic
*.css text/css
*.csv text/csv
*.doc application/msword
*.dot application/msword
*.dtd application/xml-dtd
*.dwg image/vnd.dwg
*.dxf image/vnd.dxf
*.gif image/gif
*.htm text/html
*.html text/html
*.jp2 image/jp2
*.jpe image/jpeg
*.jpeg image/jpeg
*.jpg image/jpeg
*.js text/javascript
*.json application/json
*.mp2 audio/mpeg
*.mp3 audio/mpeg
*.mp4 audio/mp4
*.mpeg video/mpeg
*.mpg video/mpeg
*.mpp application/vnd.ms-project
*.ogg application/ogg
*.pdf application/pdf
*.png image/png
*.pot application/vnd.ms-powerpoint
*.pps application/vnd.ms-powerpoint
*.ppt application/vnd.ms-powerpoint
*.rtf application/rtf
*.svf image/vnd.svf
*.tif image/tiff
*.tiff image/tiff
*.txt text/plain
*.wdb application/vnd.ms-works
*.wps application/vnd.ms-works
*.xhtml application/xhtml+xml
*.xlc application/vnd.ms-excel
*.xlm application/vnd.ms-excel
*.xls application/vnd.ms-excel
*.xlt application/vnd.ms-excel
*.xlw application/vnd.ms-excel
*.xml text/xml
*.zip aplication/zip

当前还有一些多文件的通配符

文件 类型
image/* 匹配所有类型图片
audio/* 匹配所有类型音频
video/* 匹配所有类型视频

注意

这都只是针对普通用户有效的显示,

比如直接修改html代码可以避开

或直接通过其他工具上传也可以避开

或修改文件拓展名也可以避开

后端程序还是需要做其他验证

kindeditor 加入七牛云上传

2017-11-11 22:58:15

七牛云上传主要有两种:

服务端上传

前端上传,前端又分两种返回方式:

1).重定向返回,可以解决ajax跨域的问题

2).回调返回,七牛云先向服务端要返回数据,再由七牛云返回前端,解决不支持重定向的请求方式,比如小程序上传

本次使用的是 七牛云 php sdk;

shell
  
composer require qiniu/php-sdk
12

在Kindeditor/php 下添加 config.php 主要是配置参数

php
              
<?php
error_reporting(0);

defined('ROOT_PATH') || define('ROOT_PATH', dirname(__DIR__).'/');
defined('QINIU_ACCESS_KEY') || define('QINIU_ACCESS_KEY', '');
defined('QINIU_SECRET_KEY') || define('QINIU_SECRET_KEY', '');
defined('QINIU_TEST_BUCKET') || define('QINIU_TEST_BUCKET', '七牛云空间名');
defined('QINIU_BUCKET_DOMAIN') || define('QINIU_BUCKET_DOMAIN', '七牛云空间网址');

defined('CALLBACK_URL') || define('CALLBACK_URL', '域名/kindeditor/php/callBack.php');
defined('RETURN_URL') || define('RETURN_URL', '域名/kindeditor/php/returnBack.php');

require_once ROOT_PATH."vendor/autoload.php";
1234567891011121314

在Kindeditor/php 下添加 qiniu_token.php 主要是生成上传用的 token

php
                         
<?php
use Qiniu\Auth;

require_once __DIR__."/config.php";

// 构建鉴权对象
$auth = new Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);

$data = [
    'returnUrl' => RETURN_URL,
];
if (isset($_REQUEST['is_call'])) {
   $data = [
      'callbackUrl' => CALLBACK_URL,
      'callbackBody' => 'key=$(key)&hash=$(etag)&w=$(imageInfo.width)&h=$(imageInfo.height)'
   ];
}
// 生成上传 Token
$token = $auth->uploadToken(QINIU_TEST_BUCKET, null, 3600, $data);

echo json_encode([
   'error' => 0,
   'token' => $token
]);
12345678910111213141516171819202122232425

在Kindeditor/php 下添加 callBack.php 主要是回调用

php
                                 
<?php
use Qiniu\Auth;

require_once __DIR__."/config.php";
$_body = file_get_contents('php://input');
$auth = new Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
//回调的contentType
$contentType = 'application/x-www-form-urlencoded';
//回调的签名信息,可以验证该回调是否来自七牛
$authorization = $_SERVER['HTTP_AUTHORIZATION'];
$isQiniuCallback = $auth->verifyCallback($contentType, $authorization, CALLBACK_URL, $_body);
if (!$isQiniuCallback) {
    echo json_encode([
        'error' => 2,
        'message' => '验证失败'
    ]);
    die();
}

$body = $_POST;
$qiniu_url = QINIU_BUCKET_DOMAIN;
if (!empty($body['key'])) {
    echo json_encode([
        'error' => 0,
        'url' => $qiniu_url.$body['key']
    ]);
    die();
}
echo json_encode([
    'error' => 1,
    'message' => '视频上传出错'
]);
123456789101112131415161718192021222324252627282930313233

在Kindeditor/php 下添加 returnBack.php 主要是重定向接收地址

php
                   
<?php
use Qiniu\Auth;

require_once __DIR__."/config.php";
$upload_ret = base64_decode($_GET['upload_ret']);
$upload_ret = json_decode($upload_ret, true);
$qiniu_url = QINIU_BUCKET_DOMAIN;
if (!empty($upload_ret['key'])) {
    echo json_encode([
        'error' => 0,
        'url' => $qiniu_url.$upload_ret['key']
    ]);
    die();
}
echo json_encode([
    'error' => 1,
    'message' => '视频上传出错'
]);
12345678910111213141516171819

接下来是前端更改,我改的时视频上传

Kindeditor/plugins/media/media.js

js
                                
KindEditor.plugin('media', function(K) {
    var self = this, name = 'media', lang = self.lang(name + '.'),
        allowMediaUpload = K.undef(self.allowMediaUpload, true),
        allowFileManager = K.undef(self.allowFileManager, false),
        formatUploadUrl = K.undef(self.formatUploadUrl, true),
        extraParams = K.undef(self.extraFileUploadParams, {
            'token': ''//添加token 
        }),
        filePostName = K.undef(self.filePostName, 'file'), //更改文件上传名
        uploadJson = K.undef(self.uploadJson, ' //更改上传地址,我用的时华东区的空间使用https 


        ....................

            function getQToken() {
                $.getJSON('/includes/kindeditor/php/qiniu_token.php', function (data) {
                    K('[name="token"]', div).val(data.token);
                });
            }
                        // 获取设置上传token
            getQToken();

            if (allowMediaUpload) {
                var uploadbutton = K.uploadbutton({
                    button : K('.ke-upload-button', div)[0],
                    fieldName : filePostName,
                    extraParams : extraParams,
                    url : uploadJson,//去除添加参数
                    afterUpload : function(data) {
            ...
});
1234567891011121314151617181920212223242526272829303132

这要就可以上传视频到七牛云了。

centos7 xampp 配置Let's Encrypt 证书

2017-10-29 05:40:38

centos7 xampp 配置Let's Encrypt 证书

本次使用的是 Let's Encrypt 推荐的快速安装工具 Certbot

尝试

一开始使用文档中的安装方法

shell
  
yum install certbot-apache
12

但是发现报错,原来是程序不是最新的

然后根据 certbot: ImportError: ‘pyOpenSSL’ module missing required functionality, 使用pip 安装

清除之前操作

shell
  
yum remove certbot pyOpenSSL
12

准备

安装epel扩展源, 安装pip并更新到最新

shell
     
yum -y install epel-release

yum -y install python-pip

pip install --upgrade pip
12345

安装

安装最新版

shell
   
pip install pyOpenSSL

pip install certbot
123

申请ssl 绑定路径

shell
 
certbot certonly --webroot -w /data/www/html -d zodream.cn
1

如果有多个域名

那么只要在加上 -d 域名即可,即使是临时增加一个域名,也要把所有域名都加上才行,

shell
 
certbot certonly --webroot -w /data/www/html -d zodream.cn -d test.zodream.cn
1

如果不同域名在不同文件夹,只需要设一个 -w 作为验证权限即可

更改 apache 站点配置,重启xampp

conf
                    
 <VirtualHost *:443>  
    ServerName zodream.cn
    DocumentRoot "/data/www/html"
    ServerAdmin [email protected]
    ServerName zodream.cn
    ServerAlias www.zodream.cn
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/zodream.cn/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/zodream.cn/privkey.pem
    ErrorLog "logs/dummy-www.zodream.cn-error_log"
    CustomLog "logs/dummy-www.zodream.cn-access_log" common
 </VirtualHost>

<Directory "/data/www/html">  
    DirectoryIndex index.php index.html index.htm 
    Options FollowSymLinks Includes ExecCGI  
    AllowOverride All  
    Order allow,deny  
    Allow from all  
</Directory>
1234567891011121314151617181920

使用

shell
 
certbot renew --dry-run
1

安装自动更新证书

也可以手动更新证书

shell
 
certbot renew
1

参考:

  1. certbot.eff.org

  2. certbot-importerror-pyopenssl-module-missing-required-functionality

jquery插件系列之延迟加载

2017-10-29 05:40:38

思路

  1. 当浏览器滚动到指定元素进行加载
  2. 可以加载多次

先上代码

ts
                                                                                                                                                                                                                                      
enum LazyMode {
    once,
    every
}

class LazyItem {
    constructor(
       public element: JQuery,
       public callback: Function,
       public mode: LazyMode = LazyMode.once,
       public diff: number|Function = 0
    ) {
       element.on('lazy-refresh', () => {
           this.refresh();
       });
    }

    private _lastHeight: number; // 上次执行的高度
    /**
     * 重新刷新
     */
    public refresh() {
        this._lastHeight = undefined;
    }
    /**
     * 判断能否执行
     * @param height 
     * @param bottom 
     */
    public canRun(height: number, bottom: number): boolean {
       if (this.mode == LazyMode.once && this._lastHeight != undefined) {
           return false;
       }
       if (this.element.parent().length < 1) {
           // 判断元素是否被移除
           return false;
       }
       if (typeof this.diff == 'function') {
           return this.diff.call(this, height, bottom);
       }
       let top = this.element.offset().top;
       return top + this.diff >= height && top < bottom;
    }

    public run(height: number, bottom: number, index: number = 0): boolean {
       // if (!this.canRun(height, bottom)) {
       //     return false;
       // }
       this.callback.call(this, this.element, height, bottom, index);
       this._lastHeight = height;
       return true;
    }
}

class Lazy {
    constructor(
       public element: JQuery,
       options ? : LazyOptions
    ) {
       this.options = $.extend({}, new LazyDefaultOptions(), options);
       let $window = $(window);
       let instance = this;
       this._init();
       $window.scroll(function () {
           instance.scrollInvote();
       });
       // 首次执行
       this.scrollInvote();
    }

    public options: LazyOptions;

    private _data: Array<LazyItem>;

    /**
     * 页面滚动触发更新
     */
    public scrollInvote() {
       let $window = $(window);
       let height = $window.scrollTop();
       let bottom = $window.height() + height;
       this.run(height, bottom);
    }

    public run(height: number, bottom: number) {
       if (!this._data) {
           return;
       }
       let index: number = 0;
       for (let i = 0, length = this._data.length; i < length; i ++) {
           let item = this._data[i];
           if (item.canRun(height, bottom)) {
               item.run(height, bottom, index ++);
           }
           // if (item.run(height, bottom) && item.mode == LazyMode.once) {
           //     this._data.splice(i, 1);
           // }
       }
    }
    // 暂时只做一次
    private _init() {
       this._data = [];
       let instance = this;
       this.element.each(function (i, ele) {
           let item = new LazyItem(
               $(ele), 
               typeof instance.options.callback != 'function' ? Lazy.getMethod(instance.options.callback) : instance.options.callback,
               instance.options.mode, 
               instance.options.diff);
           instance._data.push(item);
       });
       $.each(this.options.data, (i, item: any) => {
           if (item instanceof LazyItem) {
               this._data.push(item);
               return;
           }
           if (typeof i == 'string') {
               item['tag'] = i;
           }
           $(item.tag).each(function (i, ele) {
               let lazyItem = new LazyItem(
                   $(ele), 
                   typeof item.callback != 'function' ? Lazy.getMethod(item.callback) : item.callback, 
                   item.mode || LazyMode.once, 
                   item.diff || 0 );
               instance._data.push(lazyItem);
           })
       });
    }

    /**
     * 全局方法集合
     */
    public static methods: {[name: string]: Function} = {};

    /**
     * 添加方法
     * @param name 
     * @param callback 
     */
    public static addMethod(name: string, callback: Function) {
        this.methods[name] = callback;
    }

    /**
     * 获取方法
     * @param name 
     */
    public static getMethod(name: string): Function {
        return this.methods[name];
    }
}
/**
 * 加载图片,如需加载动画控制请自定义
 */
Lazy.addMethod('img', function (imgEle: JQuery) {
   let img = imgEle.attr('data-src');
   $("<img />")
       .bind("load", function () {
           if (imgEle.is('img') || imgEle.is('video')) {
               imgEle.attr('src', img);
               return;
           }
           imgEle.css('background-image', 'url(' + img + ')');
       }).attr('src', img);
});
/**
 * 加载模板,需要引用 template 函数
 */
Lazy.addMethod('tpl', function (tplEle: JQuery) {
   let url = tplEle.attr('data-url');
   tplEle.addClass('lazy-loading');
   let templateId = tplEle.attr('data-tpl');
   $.get(url, data => {
       let html = '';
       if (typeof data === 'object') {
            if (data.code != 200) {
                return;
            }
            html = typeof data.data != 'string' ? template(templateId, data.data) : data.data;
       } else {
           html = data;
       }
       tplEle.removeClass('lazy-loading');
       tplEle.html(html);
       tplEle.trigger('lazyLoaded');
   }, typeof templateId === 'undefined' ? null : 'json');
});
/**
 * 滚动加载模板,需要引用 template 函数
 */
Lazy.addMethod('scroll', function (moreEle: JQuery) {
   let page: number = parseInt(moreEle.attr('data-page') || '0') + 1;
   let url = moreEle.attr('data-url');
   let templateId = moreEle.attr('data-tpl');
   let target = moreEle.attr('data-target');
   $.getJSON(url, {
       page: page
   }, function (data) {
       if (data.code != 200) {
           return;
       }
       if (typeof data.data != 'string') {
           data.data = template(templateId, data.data);
       }
       $(target).html(data.data);
       moreEle.attr('data-page', page);
   });
});

interface LazyOptions {
   [setting: string]: any,
   data ? : {[tag: string]: string | Object} | Array <Object> | Array < Lazy > ,
   tag ? : string | JQuery,
   callback ? : string | Function, // 回调
   mode ? : LazyMode, //执行模式
   diff ? : number|Function, //距离可视化区域的距离
}

class LazyDefaultOptions implements LazyOptions {
    mode: LazyMode = LazyMode.once;
    diff: number = 0
}

;
(function ($: any) {
    $.fn.lazyload = function (options ? : LazyOptions) {
        return new Lazy(this, options);
    };
})(jQuery);
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230

本插件使用 typescript 编写, js 请查看 GITHUB

本插件内置两个方法:

1.简单的图片加载。可以参考增加加载动画 给 img 的 src 设置一张 加载动态图片

html
       
<img src="loading.gif" data-src="image.jpg" class="lazy">

<script>
$("img.lazy").lazyload({
    callback: 'img'
});
</script>
1234567

2.局部加载。依赖 template 函数(参考 art-template

html
  
<div class="templateLazy lazy-loading" data-url="1" data-tpl="temp_tpl">div>
12

主要有两个参数 :

data-url   请求网址

data-tpl 模板元素id

lazy-loading 为加载动画)

html
   
<script id="temp_tpl" type="text/template">
<div>{{ id }}</div>
</script>
123
ts
    
$(".templateLazy").lazyload({
    callback: 'tpl'
});
1234

请注意,本插件依赖 jquery

使用 typescript 写 jQuery 插件

2017-10-29 05:36:59

思路

  1. 可以接受的参数
  2. 默认的参数
  3. 挂载在 JQuery

步骤

  1. 参数接口

配置的接口,在js中无用,这里只是为了以后 TS 使用方便,方便智能提示和书写,

ts
           
interface CarouselOptions {
     range?: number, // 每次移动的距离,默认一个item宽度
     itemTag?: string, // 子代的标签
     boxTag?: string, // 盒子的标签
     spaceTime?: number, // 停顿的时间
     animationTime?: string|number, // 动画执行时间
     animationMode?: string, // 动画效果
     previousTag?: string, // 可点击向前的元素
     nextTag?: string, // 可点击向后的元素
     thumbMode?: string // 缩略图模式
}
1234567891011
  1. 默认参数
ts
          
class CarouselDefaultOptions implements CarouselOptions {
     itemTag: string = 'li';
     boxTag: string = '.carousel-box';
     spaceTime: number = 3000;
     animationTime: string|number = 1000;
     animationMode: string = "swing";
     previousTag: string = '.carousel-previous';
     nextTag: string = '.carousel-next';
}
12345678910

插件具体功能实现

ts
                     
/// 
class Carousel {
    constructor(
        public element: JQuery,
        options?: CarouselOptions
    ) {
        this.options = $.extend({}, new CarouselDefaultOptions(), options); // 合并参数
        this._init(); // 初始化,包括长度不足循环补足
        this._addEvent(); // 绑定事件
    }

    // 下一张
    public next(range: number = this.options.range) {
        this.goLeft(this._left - range);
    }

    // 上一张
    public previous(range: number = this.options.range) {
        this.goLeft(this._left + range);
    }
}
123456789101112131415161718192021
  1. 挂载在 JQuery
ts
     
;(function($: any) {
    $.fn.carousel = function(options ?: CarouselOptions) {
        return new Carousel(this, options); 
    };
})(jQuery);
12345

--- 用typescript 写 js 一目了然

UWP 解压 GZIP

2017-10-29 05:36:59

准备工作:

通过 NUGET 安装 Microsoft.Bcl.Compression ;

使用命名空间

c#
                            
using System.IO.Compression ;

public static async Task Get(string url)
{
    WebRequest request = WebRequest.CreateHttp(new Uri(url)); //创建WebRequest对象              
    request.Method = "GET";    //设置请求方式为GET
    request.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0";
    request.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate"; //设置接收的编码 可以接受 gzip            
    var response = await request.GetResponseAsync();
    Stream stream = null;
    stream = response.Headers[HttpRequestHeader.ContentEncoding].Equals("gzip",
        StringComparison.CurrentCultureIgnoreCase) ? 
        new GZipStream(response.GetResponseStream(), CompressionMode.Decompress) : response.GetResponseStream();            
    var ms = new MemoryStream();            
    var buffer = new byte[1024];            
    while (true)
    {                
        if (stream == null) continue;                
        var sz = stream.Read(buffer, 0, 1024);                
        if (sz == 0) break;
        ms.Write(buffer, 0, sz);
    }            
    var bytes = ms.ToArray();
    var html = GetEncoding(bytes, response.Headers[HttpRequestHeader.ContentType]).GetString(bytes);            
    await stream.FlushAsync();            
    return html;
}
12345678910111213141516171819202122232425262728

获取编码:

c#
                        
public static Encoding GetEncoding(byte[] bytes, string charSet)
{
    var html = Encoding.UTF8.GetString(bytes);
    var regCharset = new Regex(@"charset\b\s*=\s*""*(?<charset>[^""]*)");
    if (regCharset.IsMatch(html))
    {
        return Encoding.GetEncoding(regCharset.Match(html).Groups["charset"].Value);
    }
    if (string.IsNullOrEmpty(charSet))
    {
        return Encoding.UTF8;
    }
    try
    {
        // 解决 gbk gb2312
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        return Encoding.GetEncoding(charSet);
    }
    catch (Exception)
    {
        return Encoding.UTF8;
    }
}
123456789101112131415161718192021222324

虽然使用 HttpClient 更简单

c#
      
var http = new HttpClient();
http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0");
http.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate");            
var response = await http.GetAsync(new Uri(url));
response.EnsureSuccessStatusCode();//确保请求成功
123456

但是它的响应头并没有 Content-Encoding ,所以无法直接判断需不需要 Gzip 解压。

第六课 Model介绍

2017-10-29 05:33:10

最新文档

具体文档请查看【zodream

介绍

主要用于连接数据库,默认使用单例模式通过PDO连接,其他连接方式有MYSQL、MYSQLI。

使用

默认通过继承 Zodream\Database\Model\Model 扩展,添加具体的方法

php
                                          
<?php
namespace Domain\Model;

use Domain\Model\Model;
/**
* Class LogModel
* @property integer $id
* @property integer $type
* @property float $number
* @property string $remark
* @property integer $created_at
* @property integer $updated_at
*/
class LogModel extends Model {
    public static function tableName() {
        return 'log';
    }


    protected function rules() {
        return [
            'id' => 'required|int',
            'type' => 'int:0,9',
            'number' => '',
            'remark' => '',
            'created_at' => 'int',
            'updated_at' => 'int',
        ];
    }

    protected function labels() {
        return [
            'id' => 'Id',
            'type' => 'Type',
            'number' => 'Number',
            'remark' => 'Remark',
            'created_at' => 'Created At',
            'updated_at' => 'Updated At',
        ];
    }

}
123456789101112131415161718192021222324252627282930313233343536373839404142

一般是控制器调用方法传给视图。

ZoDream 主程序下载

2017-10-29 05:12:39

主程序地址: zodream

Demo地址: PHP-ZoDream ,

文档地址:zodream

其他获取方式:

composer:

shell
  
composer require zodream/zodream
12

第五课 生成功能介绍

2017-10-17 05:00:15

最新文档

具体文档请查看【zodream

介绍

自动生成程序,主要分为三部分:获取数据库、模板、生成程序。其中生成程序又分为:ConfigControllerModelModuleView;

函数介绍

make() 主入口,启动程序    
getDatabase() 获取所有数据库名    
getTable() 获取数据库下的所有表名    
getColumn() 获取表下的所有列名    

makeController($name) 生成控制器 $name 为控制器名    
makeModel($name, $columns) 生成数据 $name 数据名 $columns 所有列名及详细信息    
makeModule($module, $table) 生成模块名及使用的数据表    
makeConfig($configs, $module = APP_MODULE) 生成配置信息文件,$configs 配置信息数组,$module 文件名    
makeView($name = 'Home', $column = array()) 生成视图模板文件,$name 文件夹名,$column 所有列名    

使用

在配置文件中手动添加,在本地运行时自动开启

php
    
'modules' = array(   //模块
    'gzo' => 'Zodream\Module\Gzo'
)
1234

输入网址 http://localhost/gzo

已完成功能

web 界面操作

通过简单的操作就能自动生成对应的代码    

登陆后立即掉线

2016-03-03 22:20:00

session 经常变动,导致登录不了,

原因: session_id 获取不到,cookie 中的PHPSESSIONID 变成了 ,_PHPSESSIONID

解决方法: session_name('ZoDream'); 更改cookie中的SESSION_NAME

第四课 视图模板

2017-10-29 05:12:39

最新文档

具体文档请查看【zodream

位置

位于 UserInterface 下,以文件夹区分模块

使用

在控制中 
$this->show();
表示使用 UserInterface/模块/控制器/方法.php

$this->show('index');
表示使用 UserInterface/模块/index.php

$this->show('home.index');
表示使用 UserInterface/模块/home/index.php

主要函数

$this->ech($name, $default);      输出$name的值,如果为空则输出 $default ,默认为 null ,当$name为 null 时,以字符串的形式输出数组;

$this->get($name, $default);      同理返回;

$this->set($name, $value);        传递参数值

$this->extend($view, $script);    加载其他共享视图,并传递脚本路径;

$this->jcs($arg, ...);            生成js css引用, 可以为 匿名函数,以 @ 开头是绝对路径

$this->url($url);                 输出绝对路径

$this->asset($file);              输出绝对资源路径

例如

(所有文件在 UserInterface/模块 文件夹下)

layouts/header.php

php
                
<?php
defined('APP_DIR') or exit();
use Zodream\Template\View;
/** @var $this View */
?>
<!DOCTYPE html>
<html lang="<?=$this->get('language', 'zh-CN')?>">
   <head>
       <meta name="viewport" content="width=device-width, initial-scale=1"/>
       <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
       <meta name="Description" content="<?=$this->description?>" />
       <meta name="keywords" content="<?=$this->keywords?>" />
       <title><?=$this->title?></title>
       <?=$this->header();?>
   </head>
   <body>
12345678910111213141516

layouts/footer.php

php
        
<?php
defined('APP_DIR') or exit();
use Zodream\Template\View;
/** @var $this View */
?>
   <?=$this->footer()?>
   </body>
</html>
12345678

index.php

php
         
<?php
defined('APP_DIR') or exit();
use Zodream\Template\View;
/** @var $this View */
$this->title = 'ZoDream';
$this->extend('layouts/header');
?>
Alert body ...
<?php $this->extend('layouts/footer');?>
123456789

第三课 控制器使用

2016-03-03 22:33:31

最新文档

具体文档请查看【zodream

步骤

第一步

在 Service 文件夹下新建 Home 文件夹,并新建 HomeController.php 文件作为默认控制器

第二步

php
          
<?php
namespace Service\Home;

use Zodream\Route\Controller\Controller;

class HomeController extends Controller {
    public function indexAction() {
        return $this->show();
    }
}
12345678910

解释

参数说明

$this->show($name, $data);
$name 为空或 null 时,根据路由得到的控制器名和方法名自动生成路径 Home/index.php
      为数组时,此时得到的值作为要传递的参数,路径同上;
      为匿名方法,直接执行,当有返回值则输出匿名函数的返回值;
      为 @ 开头的字符串,直接输出字符串;
      其他则作为路径解析
$data 为空
      为字符串是,参数则以 data 命名;
      为数组,并入参数

第二课 安装及入口配置

2016-03-03 22:33:31

最新文档

具体文档请查看【zodream

基本骨架

请下载 PHP-ZoDream 中的 starter 分支,这个分支只包含基本的骨架,适合新项目开发

主分支为框架配套的开发项目,包括本站的源码和一些开发中的模块

搭建完整架构

请配合composer.phar 在命令行执行

cmd
 
php composer.phar install
1

入口配置

html/index.php

php
      
<?php
use Zodream\Service\Web;

require_once dirname(__DIR__).'/Service/Bootstrap.php';
$app = new Web(APP_DIR);
$app->autoResponse();
123456

解释 这里主要加载主程序 ,具体引入文件在

Service/Bootstrap.php

php
       
<?php
if (version_compare(PHP_VERSION, '7.1.0', '<'))  {
    die('require PHP > 7.1.0 !');
}
defined('DEBUG') or define('DEBUG', true);                  //是否开启测试模式
define('APP_DIR', dirname(__DIR__));            //定义路径
require_once APP_DIR.'/vendor/autoload.php';
1234567

第一课 介绍

2016-03-03 22:33:31

最新文档

具体文档请查看【zodream

整体说明

在本程序中,主要使用的是领域驱动设计模式,在主程序中主要分为四部分:用户层、服务层、领域层、基础层。

用户层主要面向用户,包括界面,负责展示给用户看,是面向浏览器端。

服务层主要负责接收用户信息,分配领域层执行具体操作,并将结果返回给用户。

领域层负责具体执行流程,是整个模式的核心。

基础层主要负责底层方法,提供基础设施,为领域层的执行提供底层方法。

基本步骤

在本程序中,

从浏览器接受网址,

启动指定的脚本文件(例如index.php),启动主程序(Zodream/Service/Web.php),进入路由导航(Zodream/Route/Router.php -> Route.php),根据配置文件信息路由驱动解析网址(例如:优雅链接),分配到具体的控制器(Service/Home/HomeCcontroller.php),执行控制器中的指定方法(indexAction),返回具体的界面($this->show('index') -> UserInterface/Home/index.php

路由介绍

在整个流程中,路由的原理是:

第一步,判断是否是首页;

第二步,判断是否是在配置文件中注册的路由;

第三步,进行自动判断,先判断网址中包含控制器和方法,再判断只包控制器(方法为默认 index),再判断只包含方法(控制器为默认Home),最后报错;

其中网址可能包含参数,参数以数字分割,如果提取的参数是单数,第一个数字则为分隔符,忽略,然后把参数进行配对,奇数为参数名,偶数为参数值。

控制器介绍

控制器中包括变量和方法,变量是 $rules ,指定方法的规则,基本规则有 
* 无要求 
* ? 必须是游客, 
* @ 必须已经登录, 
* p 必须POST提交, 
* ! 未开放不能访问, 
* 其他 要求通过验证权限;

方法,方法名必须加上APP_ACTION定义的后缀(避免与普通方法混淆),

加载数据或插件,

可以通过 use、include、include_once、require、require_once ,

也可通过 $this->loader->model()、$this->loader->library()、$this->loader->plugin() 加载,

然后通过 $this->__() 使用;

通过 $this->show() 指向界面,

如果第一个参数不是 string 则,根据路由解析出来的控制器名和方法名自动加载界面(例如 HomeController::indexAction -> Home/index.php),

如果是指定则可以用户 . 代替 / ,对界面传参数,

可以用 $this->send() 传任意值(如果不是数组则自动加在 data 下)或 $this->show() 第一个参数为数组或第二个参数;

View介绍

主要函数

$this->ech($name, $default);      输出$name的值,如果为空则输出 $default ,默认为 null ,当$name为 null 时,以字符串的形式输出数组;

$this->get($name, $default);      同理返回;

$this->set($name, $value);        传递参数值

$this->extend($view, $script);    加载其他共享视图,并传递脚本路径;

$this->jcs($arg, ...);            生成js css引用, 可以为 匿名函数,以 @ 开头是绝对路径

$this->url($url);                 输出绝对路径

$this->asset($file);              输出绝对资源路径

结束语

基本介绍就这么多。