2024-04-06 06:10:10
本文主要从简单到详情讲解会员系统的设计逻辑,不涉及任何编程,主要通过图文方式通俗的讲述会员系统的前后端逻辑。
本文适合以下人:
不适合的人:
会员系统是一切网站的基础,不论是后台管理。
主要就两个字段: 账号、密码
使用于小型网站:就只有一个管理员管理发布内容的系统。
权限根据详细程度又可以细分不同的权限模型
设计会员等级体系:普通会员、VIP会员、钻石会员等
2024-04-02 19:06:44
周刊相对普通博客,提供了更多有趣新知识。
weekly
基本都是关于软件技术的周刊每周分享潮流技术和潮流工具,标题什么的不重要。
https://hellogithub.com/periodical
每月分享 GitHub 上有趣、入门级的开源项目。
周刊月刊这种文章主要还是用来找乐子的,随便发现点有用的东西,当作放松方式还是不错的,至少能够拣点有用的东西。
不过,也不要寄希望于挖到什么大宝贝,因为写的人也不一定经常用里面推荐的工具,当时可能只是抱着“哟,看起不错哦”的心态记录一下。
总之,写写周报,就是闲时炫耀一下自己发现的“大宝贝”。
2024-03-30 05:36:27
解释说明:
()
添加网址{}
添加起始行号或高亮行号,使用 -
表示范围,,
只能用于高亮,表示多个 ```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;
}
```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;
}
2024-03-29 23:39:35
本周还是在思考网站内容的发展方向。
使用 ClaudeAI 学习了下怎么写技术类的教程文章。
准备朝着这个标准写。同时也想改改已有的文章,让文章都超这个标准靠拢。
一篇优秀的技术教程文章应当具备以下几个基本结构和要素:
标题
标题应该简洁明了,能够准确表达教程主题,吸引读者阅读。
前言
前言部分通常会简单介绍所要讲解的技术/工具是什么,解决什么问题,在哪些场景下会使用。让读者对整体内容有一个基本认知。
先决条件
列出学习和实践本教程所需要具备的基础知识技能、环境配置等前提条件。
概念讲解
对教程涉及的关键概念、理论原理进行深入浅出的讲解,融入适量的图示和案例,帮助读者理解和消化。
分步指导
教程实战部分最为关键。通过分步骤的形式,引导读者一步一步操作实践。文字通俗易懂,操作简明流畅。可以附带运行输出截图。
常见问题
总结实践过程中可能遇到的常见问题,并给出解决方法和建议。
扩展知识
对教程内容进行适度延伸阐述,如进阶技巧、高级玩法、场景应用等,满足不同层次读者的需求。
总结
全文总结并重申教程主题和重点,同时可以指出一些注意事项和最佳实践。
参考资源
列出撰写教程时参考的权威资料出处,供读者进一步了解和学习。
此外,代码示例尽量保持简洁,重点突出,避免过多冗余。插图和动图可以辅助说明。层次分明的标题结构也有助于读者查阅和理解。交错适度的互动性语句会增加教程的亲和力。良好的教程文章不仅传递知识,还需要关注知识传递的体验。
2024-03-25 04:53:59
使用 flex 布局中,经常会出现父容器的尺寸被子元素撑开,而不是子元素自适应父容器的尺寸。
例如:
在子元素的样式上设置 width
为 0
即可
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能提供文字、图像、视频的生成,但是还是需要人的主导。
2024-03-10 22:15:00
在使用 Edge 浏览器 浏览网页时查看图片,默认会调用 Edge Image Viewer
打开图片,增加了图片编辑等功能,但是实际是跳转了一个网址,导致加载显示图片太慢。
所以怎么关闭 Edge Image Viewer
呢?
在浏览器右上角打开进入 设置
在设置页面点击 下载
关闭 “允许边缘图像查看器打开图像” 即可
2024-03-06 22:44:41
完全基于个人兴趣写作,记录关于个人的生活学习工作内容。
这类文章完全取决个人的兴趣爱好,当兴趣爱好跟浏览者的喜好一致时,获得的流量就多,反之,就没什么点击量。
优点:
缺点:
从其他平台转载内容。例如:从国内平台搬运至外网。
优点:
缺点:
案例:
俗称蹭热度,查看搜索引擎流量热点,进行相关创作。这是最好的方法。
这种相对比搬运高级一点,但不多,因为核心内容不变。
案例:
这种方法跟原创几乎没有区别,一样的需要花费大量时间进行创作,但是这种方法比原创更能把握流量热点。
用计算机术语来讲就是:面向搜索引擎创作。
步骤:
使用 Google Search Console
和 Google Analytics
挖掘网站关键词
选取关键词进行搜索
2024-02-27 22:44:09
双因素身份认证,简单理解就是使用账户密码登录后需要使用一个动态码确认,账户密码
加 动态码
两种方式登录,多一步就多一点安全性,
但是,这种方式也牺牲了方便。因此,有多种形式的动态码确认,常见的就有:基于TOTP验证APP,例如Google Authenticator、微软的 Authenticator;网上银行的U盾,这类第三方专属物理设备验证。
今天,需要实现的是基于 TOTP (基于时间的一次性密码)实现的两步验证。
需要实现的步骤如下:
use RobThree\Auth\TwoFactorAuth;
$provider = new TwoFactorAuth('你的域名');
$secret_key = $provider->createSecret();
$qr = $provider->getQRCodeImageAsDataUri('用户的名称获取ID', $secret_key);
显示二维码即可,当然要保存 $secret_key
跟用户关联上;
基本原理: 每30秒生成一个动态码
use RobThree\Auth\TwoFactorAuth;
$provider = new TwoFactorAuth('你的域名');
$provider->verifyCode($secret_key, $_POST['code']); // bool
2023-11-21 01:35:22
在 MainWindow.xaml
添加
<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>
在 MainWindow.xaml.cs
中添加
public MainWindow()
{
this.InitializeComponent();
// 自定义标题栏
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
}
如果只是在左侧添加按钮,可以直接使用 xaml 定义
在 MainWindow.xaml
添加
<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>
如果需要在中间添加可点击可输入内容,那么就需要这样做
在 MainWindow.xaml
添加
<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>
在 MainWindow.xaml.cs
中添加
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)
)]);
}
使用 Command={TemplateBinding BackCommand}
可能无效
可以使用 Command="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=BackCommand}"
2023-11-21 01:33:59
使用 SelectedIndex
或 SelectedItem
都可以
<ListBox x:Name="dataGrid1" SelectedIndex="{Binding SelectedIndex}" SelectedItem="{Binding SelectedItem}">
</ListBox>
要先添加依赖项 System.Windows.Interactivity.dll
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>
2023-10-15 02:47:48
解决方法:使用 base64 编解码;
接下来 需要解决 base64 算法不一致问题,
例如:浏览器自带的 js
例如:php 自带的
实际,webAuthn
一些数据是 ArrayBuffer
所以默认的就不行了
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);
}
}
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);
}
一个 php 的 CBOR 解码库
一个 php 的 pem 转换库, 因为从浏览器获取的公钥是没法直接通过 openssl_get_publickey
加载的
$('.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);
passkey/register_option
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
]
];
passkey/register
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 保存公钥
}
$('.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);
passkey/login_option
return [
'challenge' => $challenge, // 防止重复操作的随机的字符串
'timeout' => $timeout * 1000,
'rpId' => request()->host(),
'allowCredentials' => [],
'userVerification' => 'preferred'
];
passkey/login
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);
}
2023-10-15 02:46:15
开启 使用代理服务器
地址和端口 填 Burp Suite
Proxy settings
的代理地址
点击保存即可
Burp Suite
Proxy -> HTTP history 查看所有的请求响应内容第1、2步 同上
PortSwigger CA
, 导出新的证书,再导入新的证书Burp Suite
Proxy -> HTTP history 查看所有HTTPS的请求响应内容2023-06-28 22:54:32
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
输入 mysql root 账户的密码
选择 mysql 和 php 的版本
等待安装完成
添加 站点
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;
}
编辑 /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
编辑 /usr/local/lnmp2.0/tools/check502.sh
编辑 /usr/local/lnmp2.0/tools/cut_nginx_logs.sh
######~保存日志的文件夹~######
log_files_path="/home/wwwlogs/"
######~多个日志文件名~######
log_files_name=(access vpser licess)
确定安装了 crontab
添加任务
# 五分钟检测一次
*/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
主要查看 nginx
的 error_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";
2023-05-16 01:36:13
textarea
最简单,但是可以编辑的内容也少。
需要的了解的知识
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();
textarea
只接受字符串,所以图片,超链接等需要使用 markdown 格式转成对应的字符串
把div 设置为可编辑模式
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) // 返回新创建的后一部分字符串的节点, 自动会添加到页面上的
相对来说,代码编辑器相对简单,就只要 显示对应行号、对部分字符串加样式即可,当然更高级的需要加代码提示候选就更复杂点了。
let isComposition = false; // 判断是否是输入法输入
element.addEventListener('keydown', e => {
});
element.addEventListener('keyup', () => {
if (isComposition) {
return;
}
});
element.addEventListener('compositionstart', () => {
isComposition = true;
});
element.addEventListener('compositionend', () => {
isComposition = false;
});
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);
2023-04-18 06:59:16
https//www.bing.com/indexnow
,只要不使用就不会生效2023-04-11 20:56:07
新增一个控件 例如 IconLabl
新建一个场景,继承至用户界面,命名为 IconLabel
增加一个子控件 Label
设置Font .ttf
增加脚本
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);
}
}
支持 Xaml 和 十六进制写法
2023-04-02 03:58:24
原本的访问控制是通过实现 CanActivate
接口完成的,当时并不支持 Observable
异步返回判断结果,现在终于实现了!
/**
* 需要登录才能访问的页面控制
* @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 {}
/**
* 需要有某种权限才能访问的页面控制
* @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 {}
@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;
}));
}
}
2023-03-31 22:54:06
使用 column-count
实现瀑布流时,出现内容被分割,大部分都在同一列,但是在一列的最后一个会出现部分内容被分割到了另一列。
关于column-count多列布局内容被切割在下列的解决方法以及瀑布流实现方式
2023-03-31 04:39:25
需求:登录界面,输入账户后,按下回车键,焦点移动到密码输入框,再次按下回车键,直接提交表单
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();
}
}
<input appFocusNext>
<input appFocusNext>
<button appFocusNext>提交</button>
<input appFocusNext="1">
<button appFocusNext="1" [order]="1">提交</button>
<input appFocusNext="1">
appFocusNext
属性值是作为分组使用
order
作为排序用,数字越大排在后面,最后触发,默认按照初始化顺序
HTMLElement
2023-03-18 04:48:12
需要在页面上做一个搜索框,单独加一个按钮显得多余,所以直接使用确认键跳转
这种做法就只有一个输入框,通过代码实现按键完成事件触发
在PC浏览器中是可以触发的,但在手机端没有触发事件,
通过单独调试,在手机端依然执行了方法,也获取到了event.key
, 但是在项目就是没有触发的事件
当页面存在多个表单项时,手机端的确认键会自动移动焦点到下一个表单项,只有最后一个才会有效执行,如果不在form
标签中,则时自动查找整个网页,
所以,实际上只有最后一个表单项,才能实际接受到 event.key === 'Enter'
这样就可以保证手机也可以支持,这个实际就是:表示 input
就是最后一项,可以确认了
html5 自带一个搜索输入框,可以在手机键盘确认显示为搜索
自带一个清除图标和功能
通过 css 可以移除这个
同样的还有 密码框的 小眼睛
2023-03-05 05:00:42
// 本机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);
}
数据必须一次发送,不能分段发送,不然顺序会混乱,数据大小也要注意,因为接收时只能接收一次,多出的数据就会接收不到了
// 本机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);
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;
}
}
请注意,发送数据可以分多次,因为是保持顺序的,不会乱的,接收端也可以分多次接收,但是,不能保证一次发送全部数据,最好封装一个方法,保证数据全部发送了
第一步接收每一个连接
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();
}
第二步,从每一个连接中接收数据
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;
}
接收数据也会出现一次性接收不完,要分几次才能接收一段数据,最好是保持一问一答的方式发送和接收数据,保证数据的完整,不会出现丢包的问题
2023-02-28 02:04:26
首先打开文件夹 Platforms/Windows
在文件 app.manifest
中添加
<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>
然后再文件 Package.appxmanifest
(右键->查看代码) 中添加
2023-02-28 02:02:27
在 Maui 中自定义控件分为两种方式。
cs
文件中xaml
和cs
代码文件第一种直接通过代码创建控件,并赋予属性,没法在外部修改模板
第二种分为xaml
和cs
文件,在xaml
中定义模板,cs
写属性,类似于WPF中的 UserControl
但又更像是 CustomControl
具体说明
<?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>
这里分为两部分,一部分是放在 ContentView.ControlTemplate
中,通过 TemplateBinding
获取自定义属性,类似于 WPF
中 CustomControl
定义在 Themes/Generic.xaml
中 Style.Template
, 用法也类似
特别注意,直接作为 ContentView.Content
的 Button
,这里写的控件是无法获取当前控件的属性的,如果在 ContentView.ControlTemplate
中没有使用 ContentPresenter
接收,就不会显示,默认 ContentView.ControlTemplate
就有一个 ContentPresenter
在 cs 中通过 BindableProperty
创建定义属性
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);
2023-02-20 01:22:42
和 CentOS
的命令一样
一些其他辅助命令
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 # 安全套接字层密码库,用于通信加密
下载 pcre2
下载 zlib
源码包放到 /usr/local/src
下解压
解压三个源码压缩包
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 #安装
如果后面遇到某个功能没有,只要修改 configure
中的参数就行了,重复执行 make
make install
即可
/usr/local/nginx/sbin/nginx #启动服务
/usr/local/nginx/sbin/nginx -s reload #重新加载服务
/usr/local/nginx/sbin/nginx -s stop #停止服务
ps -ef | grep nginx #查看服务进程
更改配置
开始设置开启启动项
[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
启用开机启动服务
最终的启动运行命令
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
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
下载 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
安装完成后,查看是否存在/usr/local/lib64/pkgconfig目录,如果存在,执行如下命令来设置PKG_CONFIG_PATH:
cd /usr/local/src
wget https://www.php.net/distributions/php-8.2.3.tar.gz # 下载
tar -zxvf php-8.2.3.tar.gz # 解压缩
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 #安装
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
将 ;cgi.fix_pathinfo=1
改为 cgi.fix_pathinfo=0
将 expose_php = On
改为 expose_php = Off
隐藏版本号
将 [global]
下的 pid = run/php-fpm.pid
启用, 后面 php-fpm.service
中的 PIDFile
必须设成一样的路径,否则会出现找不到php-fpm.pid
的情况
将
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
[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
可以使用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
配置 Nginx
在 http
下添加
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压缩,直接在本地修改成其他图片格式效果更好
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
}
修改完保存文件并重启Nginx服务器
如果本地已安装 mayql 则会出现冲突 增加 参数即可 --allowerasing
如果出现 找不到 nginx
赋予权限
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
php.ini
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
现在,我们新建目录 /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
启动
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
php.ini
cd /usr/local/src
wget https://www.php.net/distributions/php-7.4.33.tar.gz # 下载
tar -zxvf php-7.4.33.tar.gz # 解压缩
-–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 #安装
同原本的配置方法 [PHP8配置]()
注意将 /usr/local/php7/etc/php-fpm.d/www.conf
中的端口改成其他的,不要和原本的冲突
[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
可以使用systemctl命令管理php7-fpm:
/usr/local/nginx/conf/nginx.conf
将 fastcgi_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;
}
}
启动PHP7
,重启nginx
即可
2022-08-16 06:18:22
当然是使用两个自定义控件
<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>
了解 ng-template
使用,需要搭配 *ngTemplateOutlet
使用
具体:
ng-template
的数据传递,通过 *ngTemplateOutlet
的 context:
传递对象, 其中 $implicit
没默认名称ng-template
的数据获取,通过 let-
获取,例如: let-file
的意思就是:在 template
中 const file = $implicit
, 相当于完整写法 let-file="$implicit"
2022-06-30 21:27:29
移植弹出框到 angular
项目中。
比较常用的有以下两个
但是通过查看源码,发现都使用使用 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.
没有了 ComponentFactoryResolver
, 那么就使用 ViewContainerRef.createComponent
主要思路:
新建一个容器组件,获取到 ViewContainerRef
新建一个全局的服务提供者,
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();
}
}
作为对比,ViewContainerRef.createComponent
比 ComponentFactoryResolver
使用更简单,而且更符合 angular
特色
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);
export class DialogMessageComponent implements OnDestroy {
constructor(
private data: DialogPackage<DialogMessageOption>,
private service: DialogService,
) {
}
@HostListener('click')
public close() {
this.service.remove(this.data.dialogId);
}
}
@NgModule({
imports: [
CommonModule,
],
declarations: [
...COMPONENTS
],
exports: [
...COMPONENTS
],
})
export class DialogModule {
static forRoot(): ModuleWithProviders<DialogModule> {
return {
ngModule: DialogModule,
providers: [
DialogService
]
};
}
}
第一步,在 app.module.ts
中导入
第二步,在 app.component.ts
页面中添加容器
第三步,使用
ViewContainerRef.createComponent
比 ComponentFactoryResolver
使用更简单,而且更符合 angular
特色,始终保持所有创建的内容在框架内。
完整代码请查看 Angular-ZoDream
2022-06-15 22:10:13
自动加载指定文件夹下的所有dll文件
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 获取需要的类
}
Q: 在主程序中使用 typeof(IRule).IsAssignableFrom(types[0])
判断是否继承至公共接口,会出现 false
的情况
A:说明 LoadDll
加载dll 时又引入了公共类库,只需要在 dll 解决方案下的依赖项属性中设置 复制本地设为否
(CopyLocal=false
) 即可,或者不要把公共类库复制放到dll所在额文件夹下
2022-05-14 19:02:34
在WPF中使用
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();
}
虽然保证了动画看上去更丝滑,但这个是按帧执行的,具体一秒多少帧取决于电脑支持的最大帧数。
但是很占用GPU,一个小的移动的动画使用CompositionTarget.Rendering就直接占用60%的GPU
CompositionTarget
比 DispatcherTimer
的动画更流畅,DispatcherTimer
有明显的卡顿感。DispatcherTimer
支持自定义时间间隔,可以减少帧数来减少GPU占用2022-05-14 18:54:09
c++ 数组定义
转成c# 数组
c++ 数组定义
转成c# 数组
2022-04-24 00:11:26
操作系统:windows 11
开发工具:TortoiseGit、Visual Studio 2022 Preview
Skia-Windows-Release-x64.zip
,解压到一个文件夹即可aseprite
的源码文件夹下打开cmdlaf
third_party
文件夹下是否只有一个 .git
文件,删除重试命令即可
aseprite
下新建文件夹 build
获取使用命令,在 build
文件夹下打开 cmd
工具
菜单进入命令行是 x86模式,注意:只要找到 vs 的安装目录下的 Common7\Tools\VsDevCmd.bat
文件即可,
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 ..
这一步基本不会出什么问题,如果有问题那就是 第五步 安装模块时不完整,删除出错目录下的文件,执行第五步的命令即可
这一步可能出错,我的是失败在 FIALLED json11
,需要更改 third_party/json11/CMakeLists.txt
文件,删除第27行或者改为
重新执行ninja aseprite
命令即可,
build/bin
下的 aseprite.exe
主程序 和 data
文件夹即可,这两个文件就是aseprite
的运行文件2022-04-21 18:51:39
今天才发现不同编程语言对字符串的 split
是有差距的
是先把所有的都拆分成数组,然后取前面的几个
是拆分前几个,剩余的都原样放在最后一个
2022-04-06 18:43:16
使用NuGet
安装
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(); //解码结果
gzuncompress
比较gzuncompress
主要是不必考虑有无gzip头,都能进行解码
而 SharpZipLib
中默认的解码方法 GZipInputStream
并不支持无头内容,
2022-03-05 23:51:51
通过自带的任务管理器
查看详细信息
即可。
即在任务管理器
中进程
占用的内存之和远小于实际被占用的内存。
这是查看某一个进程已提交虚拟内存类型的明细。
准确地了解 Windows 如何分配物理内存、在 RAM 中缓存的文件数据量,或者内核和设备驱动程序使用了多少内存。
可以完整的看出内存用到哪里去了!
可以查看哪些驱动使用的内存情况。
每次使用Steam 或 Epic 下载游戏时,内存占用越来越高,最后占用99%之后电脑卡死,关闭程序无用,只能重启。
任务管理器
中进程
查看不到占用大量内存的进程。RAMMap
查看到大量内存被 Nonpaged Pool
占用。poolmon
执行命令 poolmon.exe /p /d
发现被一个 Tag
为 wfpn
的驱动占用了大量内存。Bing
搜索 poolmon wfpn
找到了 wfpn
为 Killer Network Manager
网络驱动更新 Killer Network Manager
驱动即可;或禁用 NDNB
2021-10-22 19:47:06
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);
}
无论内容是什么输出的时一个黑色的框框带阴影效果,
应该加代码输出原版的图像,即可
2021-09-20 01:45:17
c++ 程序的效率比 c# 的效率高很多
struct KeyItem
{
std::uint32_t x, y, z;
};
extern "C" _declspec(dllexport)
KeyItem FindKey(char* zipFile, char* zipFileName, char* plainFile, char* plainFileName)
{
}
复制 dll 到生成目录
[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);
}
生成平台必须选择一样的 x64
或 x86
,不能使用 Any CPU
,否则会报错
2021-09-20 01:43:19
c++
转 c#
遍历c++
转 c#
倒序遍历c++
using rit = std::reverse_iterator<std::vector<byte>::const_iterator>;
for(rit p = rit(items.begin()); p != items.rend(); ++p)
{
*p
}
转 c#
我的理解:
std::reverse_iterator
实际上是把输入的位置往前移一位,并把 +
转成 -
,方向反一下
2021-09-07 04:47:12
VS 2022
一个应用程序分:桌面版和UWP版,实现一个类库能被两个版本使用
WPF 应用程序(用于创建.NET Core WPF 应用程序的项目)
,Framework 选择.NET 6.0
空白应用(通用Windows)
,选择最低版本 17763
类库(一个创建用于面向NET Standard 或.NET Core的类库的项目)
,Framework 选择.NET Standard 2.0
Framework 选择.NET Standard 2.0
支持 .NET Core 、UWP 、 NET Framework 4.8
则需要手动修改第三个项目的 .csproj
文件
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="'$(LibraryFrameworks)'==''">net48;netstandard2.0</TargetFrameworks>
<TargetFrameworks Condition="'$(LibraryFrameworks)'!=''">$(LibraryFrameworks)</TargetFrameworks>
修改 TargetFrameworks
内容,
然后就可以添加不同的程序集引用了
2021-08-25 23:22:34
rar 分卷
功能,任意一卷也是可以测试密码的,不用全部下载才发现密码不正确
下载 rar2john
下载地址 点击 1.9.0-jumbo-1 64-bit Windows binaries
下载
执行命令 rar2john.exe
在压缩包的 run
文件夹下
输出密码哈希
1.rar:$rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe
$rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe
就是密码哈希值了
hashcat.exe -m 13000 -a3 $rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe ?d?d?d?d
具体规则请查看 【上一篇:hashcat(一)找回office文件密码】
2021-08-15 22:57:07
选择 Manually select features
勾选
Choose Vue version
Babel
Typescript
Progressive Web App (PWA) Support
Router
Vuex
Css Pre-processors
使用了Scss 必选
Linter/Formatter
Unit Testing
和 E2E 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
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 使用
复制一些文件和资源
修改 @Component
为 @Options
axios
注册全局
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
},
}
VueRouter
改为 import { Router } from 'vue-router';
const routes: Array<RouteRecordRaw> = [];
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';
@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');
$children
被删除了,需要改动使用 setup()
获取子元素filter
已经删除了,所以只能通过方法调用2021-07-16 06:34:12
主要有两种写法格式:
LaTeX
AsciiMath
这里使用的是 KaTeX
,默认支持 LaTeX
,如果需要支持 AsciiMath
则需要 安装转化工具 asciimath2tex
默认是有 angular
版本的 KaTeX
ng-katex
默认根据 $
和 $$
来识别处理公式的
例如
但是我不知需要显示公式,还需要处理一些其他的,所以直接使用 KaTeX
进行处理
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;
}
import * as katex from 'katex';
import AsciiMathParser from 'asciimath2tex';
katex.renderToString(parser.parse(content));
根据提取的公式转化成 html 代码
2021-07-09 20:05:57
一个输入框
主要事件
onkeydown
按下一个按键时执行
onkeypress
按下键盘按钮时执行,不是适用于所有按键(如: ALT, CTRL, SHIFT, ESC)
onkeyup
释放键盘按钮时执行
监听所有按键事件
监听输入框按键事件
KeyboardEvent
的主要属性
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 键
}
使用建议
获取 字母按键 请使用 event.code
获取其他特殊按键(Enter、Tab) 请使用 event.key
例如:输入框确认事件
document.querySelector('input').addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
// TODO 确认
}
});
监听复制快捷键
2021-07-04 02:21:52
a.scss
ng-deep
表示里面的样式是公共的,需要影响子组件的样式。
可以理解为 vue
中的不带 scoped
属性的 style
标签
上面的样式表示,当打开 a
组件之后,样式开始生效,如果从 a
离开了样式依然会起作用,
::ng-deep
里面的样式是影响全局的,如果想只作用与一个模块下
请使用一个规则名放在最外面
2021-07-03 07:08:59
在页面中生成一个确认弹窗
@Injectable({
providedIn: 'root'
})
export class DialogService {
public containerRef: ViewContainerRef; //需要从Component中获取,然后传递进来
constructor(
private injector: Injector,
) { }
}
第一种在 Component
初始化时获取
在template
中通过 ng-container
获取
@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;
}
}
主要区别:就是增加的元素节点为兄弟节点,所以,第一种添加的弹窗是在外面的,不受 app-dialog-container
中的样式影响;第二种是作为子元素添加的,受样式影响
component
为组件的类名
const dialogRef = this.containerRef.createComponent(component, {
injector: this.injector
});
// dialogRef.instance 就是组件的实例
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);
}
}
则第二步
需要修改
const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector);
const dialogRef = this.containerRef.createComponent(component, {
injector: dialogInjector
});
// dialogRef.instance 就是组件的实例
在组件中接收值
export class DialogConfirmComponent {
constructor(
private data: DialogPackage<DialogConfirmOption>,
private service: DialogService,
) {
}
}
export class DialogService {
constructor(
private resolver: ComponentFactoryResolver,
private applicationRef: ApplicationRef,
private injector: Injector,
@Inject(DOCUMENT) private document: Document,
) { }
}
component
为组件的类名
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 就是组件的实例
this.applicationRef.detachView(dialogRef.hostView);
this.document.body.removeChild(dialogRef.location.nativeElement);
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);
}
}
则第二步
需要修改
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 就是组件的实例
在组件中接收值
2021-07-03 06:21:59
在执行完关闭动画移除组件
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')
]),
]);
@Component({
selector: 'app-dialog-confirm',
templateUrl: './dialog-confirm.component.html',
styleUrls: ['./dialog-confirm.component.scss'],
animations: [
DialogAnimation,
],
})
export class DialogConfirmComponent {
public visible = true;
}
<div class="dialog-box" [@dialogOpen]="visible ? 'open' : 'closed'" (@dialogOpen.done)="animationDone($event)">
</div>
(@动画名.start)
表示动画开始执行
(@动画名.done)
表示动画执行完成
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;
}
// 表示关闭动画已经执行完成
}
}
2021-06-30 06:00:42
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;
}
}
export class ThemeModule {
static forRoot(): ModuleWithProviders<ThemeModule> {
return {
ngModule: ThemeModule,
providers: [
SearchService,
]
};
}
}
<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>
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);
}
}
其他页面,根据搜索关键词跳转搜索页面或详情页。
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();
}
}
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);
}
搜索事件并不会自动清除,需要添加 ngOnDestroy
进行清除
2021-06-30 05:59:04
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class SearchService {
}
providedIn: 'root'
表明当前 service
为全局有效的单例模式
必须在 AppModule
注册,或者 在任意一个 module 中注册。
但是需要注意,这个 module 只能导入一次,
例如:有一个 module ThemeModule
为公共核心组件,很多其他 module 都会依赖她,
导致很多地方都使用了
进行导入
而 单例 service SearchService
是注册在 ThemeModule
中的,
这样就会导致单例不生效,每一个 SearchService
都不同,
这是可以给 ThemeModule
增加一个方法 forRoot()
export class ThemeModule {
static forRoot(): ModuleWithProviders<ThemeModule> {
return {
ngModule: ThemeModule,
providers: [
SearchService,
]
};
}
}
把 SearchService
仅注册到 forRoot()
中。
然后在 AppModule
中导入 ThemeModule
这样就能生效
2021-06-05 02:12:24
/**
* 正则匹配替换
* @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('');
}
Q:为什么不在匹配时边匹配边替换?
A:原本我这样做的,发现匹配结果出错了,有些没匹配到。因为exec 匹配时正则表达式会记住最后的匹配位置,如果原本的内容长度变化;,就会导致这个位置不正确。
Q:为什么要截断字符串?
A: 使用字符串替换会搜索全部,多一些不必要的操作;截断存入数组,就是想最后一起做拼接。
Q: 为什么要倒序替换?
A: 因为正序截取的话会导致匹配结果中的位置要进行改变。
2021-05-12 05:53:57
通过 Canvas.Invalidate();
触发重绘事件
通过 Canvas_Draw
进行绘制
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);
}
最后必须手动销毁
2021-04-29 06:02:58
Custom Control 又名 Templated Control 模板控件
分为两个文件
一个资源文件 Themes/Generic.xaml
里面,主要放默认的模板及初始化属性
一个cs 文件 控件名.cs
,主要放 声明属性及事件
一个简单的控件,由一个图标和文字组成的控件
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));
}
<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>
ControlTemplate
就是放默认模板,
Setter Property=
就是声明一些初始化的属性
必须先使用 x:Name
声明名称
<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>
必须声明模板必须包含 x:Name="Content"
且 类型为 TextBlock
[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;
}
}
通过 GetTemplateChild
获取控件,而且必须等控件加载完了才能获取到。
2021-04-29 05:44:43
在项目里加了一个css 样式文件,
需要把这个样式引用到 webview 中
在项目资源文件里放入样式文件
Assets/markdown.css
选中文件右键“属性”
更改文件属性
复制到输出目录: 始终复制
生成操作:内容
然后和html源码合并给webview
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));
2021-04-22 03:41:58
使用 ShouldBindQuery
或者 ShouldBind
需要注意结构体的格式
例如一个查询分页的参数获取
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
}
}
form:"page" json:"page"
这个就是结构体的解析说明又名struct tag
这里的 form
指查询参数或表单提交的参数,如果是 GET
或 表单POST
的数据必须有这个标记,不然会绑定失败
如果是 POST
的 json
则用 json:"page"
如果是 POST
的 xml
则用 xml:"page"
ShouldBind
是会自动判断内容的格式,是GET
会匹配网址上的参数,其他则根据 请求头 Content-Type
自动判断
reflect: reflect.Value.SetUint using unaddressable value
只要出现 using unaddressable value
就表明代码中该传引用的地方传了值
对与跨域的处理中间件必须放在全局即 *gin.Engine
上面,不能放在某一个路由组上
原因是跨域请求会产生一个前置 OPTIONS
请求,而这个请求实际是不需要响应内容的,如果没有匹配的路由就会响应404
影响下一步浏览器发出真正的请求
不能添加请求头
所有登录信息没法通过请求头传递,而网址传递并不安全,所以只能先建立连接,然后通过发送消息传递
2021-04-18 20:49:26
路由 ` 和
/` 可以分开作为两个路由, 但是如果只有一个,就会把没注册的那个重定向到注册的那个
这种方法时有效的
如果只注册一个
浏览器访问 http://localhost/home/
就会响应 301
并跳转到 http://localhost/home
可以通过 ` ` 进行更精确的分组
r := gin.Default()
blog := r.Group("/blog")
blog.GET("", Index)
g := blog.Group("")
g.Use(middleware.CORS)
g.GET("/count", Count)
gin.HandlerFunc
只能通过 c.Abort
进行中断,否则会继续执行下去
r := gin.Default()
r.Use(func(c *gin.Context) {
if false {
c.AbortWithStatusJSON(400, json.RenderFailure(err.Error()))
return
}
})
c.Next()
不是必须调用,如果需要对输出结果进行操作,这是才需要在内部调用
在中间件中传值
c.Keys["key"] = val
c.ShouldBindQuery
可以绑定查询值到模型
c.Get()
c.GetInt()
c.GetString()
等是获取 c.Keys
注册的值
c.Query
获取查询的值
c.ShouldBind
是绑定post 提交的值到模型
c.PostForm
获取post的值
获取网址中匹配的值
r.GET("/user/:id", func(c *gin.Context) {
// a GET request to /user/john
id := c.Param("id") // id == "john"
})
r := gin.Default()
r.Static("/assets", configs.Config.Asset) // 指定资源文件的路径及文件夹
r.StaticFile("/favicon.ico", configs.Config.Favicon) // 指定网站图标
r.LoadHTMLGlob("templates/**/*")
特别注意:
templates/**/*
会匹配 templates
文件夹下的子文件夹中的文件,但是只会注册文件名
例如
有 templates/blog/index.html
和 templates/auth/index.html
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{})
2021-04-14 04:11:39
事实事件有两种定义方式
angular
通常方式,使用 @Output()
方式调用组件
不需要考虑事件是否有接收,同时可以被接收多次,而且可以接受父页面上的值。
没办法判断事件是否注册了,传递值只能传一个,可以把多个值合并成一个 object
传递
// 在组件声明一个事件
@Output() public tapped = new EventEmitter<number>();
// 组件中触发事件
this.tapped.emit(1);
调用组件
@Input()
方式// 在组件声明一个值
@Input() public tapped: () => void;
// 组件中触发事件
this.tapped && this.tapped();
调用组件
可以同时传递多个值,可以判断是否有事件
没法同时接受父页面上的值,而且这个方法的内部 this
为子组件,所以就没法调用父组件的方法或属性。
2021-04-13 22:42:34
在 angular
实现文件下载功能, 默认只能在前端代码中手动添加文件类型及文件名。
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'));
});
}
}
使用
private downloadService: DownloadService
this.downloadService.export('http://localhost/export', {}, '流水记录.xlsx');
突然想到
在响应头中已经有了文件类型和文件名,那么是否可以直接获取呢?
关键是响应头中的 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
列出了哪些首部可以作为响应的一部分暴露给外部。
默认情况下,只有七种 simple response headers (简单响应首部)可以暴露给外部:
如果想要让客户端可以访问到其他的首部信息,可以将它们在 Access-Control-Expose-Headers
里面列出来。
多个用英文逗号分隔
【来源】
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编码
}
}
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>
在 .htaccess
中加代码是没有用的。
2021-04-07 04:55:28
有这个需要的页面基本是从列表页点击详情或新建编辑页面。在返回需要回到上一次的列表页面,同时保持页面内容不变
例如:在列表已经翻到了第100页,需要修改某一项值(当然可以做一个弹窗修改,这里讨论的是有必要新增页面去修改的情况),
这是点进去修改之后返回,发现到了第一页,这就麻烦了,再放到第100页就需要浪费时间了。
这时可能想到在网址上加一个可接受的分页属性。
但是这也要手动去输入。
如果还有其他查询参数呢?难道还一个个去输入,这也麻烦。
private route: ActivatedRoute
this.route.queryParams.subscribe(params => {
this.goPage(params.page || 1);
});
private route: ActivatedRoute
this.route.queryParams.subscribe(params => {
this.goPage(params);
});
不灵活
localStorage
保存页面数据如果所有的列表页面都使用一个值。这样就会发现页面之间会混乱。
例如:
访问到了第10页,在访问其他页面
发现一进去也到了第10页。
那就分开保存,但是 localStorage
是由存储限制的,页面一多。就会发现localStorage
存满了,
不优雅,会受限制
每访问一页就进行保存历史。
/**
* 遍历对象属性或数组
*/
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;
};
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);
});
}
2021-04-06 06:16:31
本播放器适用场景: 播客类型的文章,需要一个播放器,但是不需要预加载资源文件的。可以尽可能的减少不必要的加载及访客流量浪费。
本播放器有两个版本:
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);
.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;
}
}
}
}
<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>
src 为资源文件的网络路径
type 可选值 video|audio|iframe ;默认为 video ; audio 即音频;iframe 为其他网址的视频,例如 bilibili 分享的链接
2021-04-04 06:29:14
[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\">&laquo;</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\">&raquo;</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;
}
}
这是控制器中的方法,需要传递当前网址到分页类中。
public IActionResult Index(int page = 1)
{
ViewData["items"] = _repository.GetPage(page);
ViewData["fullUrl"] = $"{HttpContext.Request.Path}{HttpContext.Request.QueryString}";
ViewData["pageIndex"] = page;
return View();
}
NetDream.Web
为当前项目命名空间
Page
为 NPoco
查询获取到的分页数据
2021-03-30 20:33:21
直接生成 html 加上链接,这种也分预处理和实时处理
但是这种对多平台应用并不友好,
例如web 端和 app 端就不能统一,必须在应用内部再处理。
这就增加了一些变化。不能统一
例如:
输出的内容为
web 端快速
不能适应多平台应用,增加了其他平台的难度
参考搜索对关键词的标注
也可以分预处理和实时处理
返回一段原始文本,附加一些标注参数
例如:
输出
{
"content": "@user 这是一个召唤用户 #每天一次#",
"rules": [
{
"word": "@user",
"rule": "user",
"id": 1,
},
{
"word": "#每天一次#",
"rule": "topic",
"id": 1,
},
]
}
这样由各自应用自行组合,不必进行解析步骤。
方便多平台应用使用
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
2021-03-19 19:37:29
开始菜单
进入 设置
搜索进入 图形设置
点击 浏览
选择软件的 exe 文件
点击 软件 选项
选择 节能
保存即可
可以给 软件 加上 以管理员身份运行
兼容属性
2021-02-17 20:08:39
export class EditComponent {
public form = this.fb.group({
name: ['', Validators.required],
});
constructor(
private fb: FormBuilder,
) { }
}
<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>
FormArray
必须为 FormGroup
的子项
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);
}
}
<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>
2021-02-08 00:05:27
我的理解:这个模块就是把网络请求获取数据,然后更新数据状态的操作合并到了一起。依然需要手动触发,既不会启动时自动加载,也不会获取时只加载一次。
例子:
有一个全局的数据:站点信息
通常的方法是:
app-component.ts
ngOnInit() {
this.service.site().subscribe(site => {
this.store.dispatch(setSite({site}));
});
}
app.actions.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');
@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,
) {
}
}
app.moudule.ts
还必须请触发
app-component.ts
大致流程
dispatch(getSite())
--> AppEffects
.loadSite$
--> service 网络请求 --> setSite({site})
更新到Store --> 响应 select(selectSite)
2021-02-08 00:04:39
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);
}
定义action 用于修改Store中的数据
export const setCart = createAction('[shop]SET_CART', props<{cart: ICart}>());
export const setSite = createAction('[shop]SET_SITE', props<{site: ISite}>());
相当于
定义 selector 用于获取数据
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
);
注册根
app.module.ts
这是必须的如果没有全局数据,则
也可以为某一个模块注册局部数据
export class AppComponent {
constructor(
private store: Store<AppState>,
) {
}
setSite() {
this.store.dispatch(setSite({site: {}}));
}
}
export class AppComponent {
constructor(
private store: Store<AppState>,
) {
this.store.select(selectSite).subscribe(site => {
// TODO
});
}
}
获取数据是一直实时更新的,除非取消订阅了。
更新的频率是依赖 reducer
的,把 reducer
看作一个对象,对象上的某一个属性更新了,会触发其他的属性更新事件
例如:
2021-01-04 01:08:39
在一些情况下只需要简单的获取表单的值,而不需要做一些不必要的值定义
例如:
当然可以一个一个的值进行定义,通过绑定
但这种情况值是始终保持和输入一直,但并不能同时保持和搜索结果的条件一直。
这时我想到了直接获取搜索表单
<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>
public keywords = '';
public tapSearch(form: any) {
this.keywords = form.keywords || '';
// TODO
}
这里需要注意
#searchForm="ngForm"
是获取表单对象
ngModel
进行绑定键值,必须的,不然在 searchForm.value
中无法获取到值
searchForm.value 的具体值为
2020-10-13 00:36:32
即真正的 tinymce 编辑器脚本都放到 他们的服务器上,
但访问速度真的不行
使用方法
然后在模块里加载
import { EditorModule } from '@tinymce/tinymce-angular';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
EditorModule // 这就是编辑器模块
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
在页面上使用,这里需要填上你在 www.tiny.cloud
上注册生成的 apiKey
<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>
则需要安装 tinymce 编辑器本体
然后配置 angular.json
复制脚本文件,及引用脚本
"assets": [
{ "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" }
],
"scripts": [
"node_modules/tinymce/tinymce.min.js"
]
在模块加载
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
/* ... */
@NgModule({
/* ... */
imports: [
EditorModule
],
providers: [
{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }
]
})
使用
<editor [init]="{
base_url: '/tinymce', // Root for resources
suffix: '.min' // Suffix to use when loading resources
}"></editor>
从 官网 下载语言包
复制 zh_CN.js
到 src/assets/tinymce/langs
目录下
然后再使用时加上配置
<editor [init]="{
base_url: '/tinymce',
suffix: '.min',
language_url: '../../../../../assets/tinymce/langs/zh_CN.js',
language: 'zh_CN',
}"></editor>
base_url: '/backend/tinymce',
suffix: '.min',
language_url: '../../../../../backend/assets/tinymce/langs/zh_CN.js',
language: 'zh_CN',
更改使用时的配置,添加 imagetools
插件
{
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);
});
},
}
images_upload_handler
为自定义上传方法,
这样就可以上传图片了
2020-10-13 00:12:01
把 angular 项目打包放在二级目录
例如:
把一个项目放在 backend 文件夹下
那么 需要设置
index.html
最好把所有页面放在一个 backend 的路由下
{
path: 'backend',
loadChildren: () => import('./backend/backend.module').then(m => m.BackendModule)
}
这样在本地使用时生成的网址时
打包生成的网址也会是
但是这是刷新页面的话并不能指向 angular
程序
需要在网站根目录加上一个 .htaccess 的文件(PS:我用的时apache)
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
RewriteRule ^backend/[^\.]+$ backend/index.html
</IfModule>
加上一句 RewriteRule ^backend/[^\.]+$ backend/index.html
就把路径指向 angular 程序了。
再次刷新网址 http://zodream.cn/backend/home
就能争取打开了
2020-10-12 23:57:21
需求:跳转到另一个页面选择一些东西然后显示在页面中 例如:进入筛选项页面,选择,然后回到列表页面刷新页面。
在 app.js
里的 app.globalData
上设一个属性,进行传值共享
例如:
设置一个可共享的属性 data
第二个页面 设置值
第一个页面获取值
这种方法简单常用,但是使用不灵活,而且可能需要清理 getApp().globalData
上的属性,
直接通过 wx.navigateBack
调用页面上的方法进行回调
wx.navigateBack({
success() {
setTimeout(() => {
let page = getCurrentPages().pop();
if (!page) {
return;
}
// TODO
}, 10);
}
});
这里需要注意 getCurrentPages().pop()
获取到的页面需要加上延迟,不然在真机上会获取到的是未返回的页面
例如:
第二个页面 返回值
Page({
tapBack() {
wx.navigateBack({
success() {
setTimeout(() => {
let page = getCurrentPages().pop();
if (!page) {
return;
}
page.selectedBack({});
}, 10);
}
});
}
})
第一个页面接收值
2020-10-03 06:45:32
默认 FileList
是只读,无法进行修改
但有时候 前端可以进行一些文件过滤
例如:
当上传多个图片时,以拖拽的形式获取文件,但这时获取到的 FileList
除了图片可能还包含其他类型的文件
这是就可以使用 FormData 进行多文件上传
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);
这样通过 post
方式提交,在服务端获取的依然是多个文件
例如:PHP
2020-10-01 21:04:20
默认的 ngModel 的实现方式为
@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);
}
}
使用
但这种方式是不支持 formControlName 绑定的
会提示No value accessor for form control with name:
错误
因此必须换种方式
@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;
}
}
现在可以就支持两种方式了
<app-switch [(ngModel)]="value"></app-switch>
<app-switch formControlName="value"></app-switch>
2020-09-10 18:20:45
我的json格式
这是普通链接响应json格式,不论是否出错,响应状态码都是 200
{
"code": 200, // 200 表示程序处理成功,其他数字自定义
"status": "success", // success 表示成功 failure 表示失败
"message": "", // 成功的消息提示
"errors": "", // 失败的信息
"data": {}, // 成功响应的信息
"url": "", // 搭配 code: 302 实现页面跳转
}
成功返回用户信息
失败返回提示无权限需要登录
状态码判断是否处理成功
成功返回用户信息
HTTP/1.1 200 OK
成功返回用户列表
HTTP/1.1 200 OK
失败返回提示无权限需要登录
HTTP/1.1 302 Found
以 netcore
版本为例
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);
}
普通版本的实现
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
}
});
}
}
RESTful 的实现
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
}
});
}
}
public Task InvokeAsync(HttpContext context)
{
context.Items.Add("json", new JsonResponse());
return _next.Invoke(context);
}
再控制器中使用
在这里有一个问题,即状态码并没有实现,可以考虑拓展方法,把HttpContext
当作参数传入,内部做状态码输入
2020-09-03 06:50:17
CupertinoPicker
是一个ios风格的齿轮滚动的选择器
CupertinoPicker.builder(
itemExtent: 40,
scrollController: FixedExtentScrollController(initialItem: 0),
backgroundColor: Colors.white,
onSelectedItemChanged: (index) {
// 选择
},
itemBuilder: (context, index) =>
// items[index] 返回一个部件显示每一项的内容,
childCount: items.length,
)
在使用 CupertinoPicker
写地区选择器时,发现数据显示不出来,但使用 r
更新就能正常显示。
查找原因:
刚开始,以为是没有setState(() {});
更新,但多次设定也没用。
然后发现是因为 CupertinoPicker
接受数据长度为零的数据就没办法正常显示。
因此通过判断直接把数据长度为零的不生成CupertinoPicker
,具体原因不知。
最终解决方案:
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),
),
);
}
2020-08-26 00:20:36
CC协议即版权协议
主要包括五个方面
主要六种不同的许可证类型
还有一种共享创意公共领域协议
2020-08-22 04:48:41
本身 Container
上的 margin
是不能设为负值
例如
这样会报错 Failed assertion: line 251: 'margin == null || margin.isNonNegative': is not true.
但可以通过 transform
属性实现负值效果
2020-08-21 18:09:15
启动
文件夹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 可以实现后台自启
删除项即可
2020-08-04 23:39:44
直接选中
,右键取消订阅
即可,
但是没办法删除其他机器的订阅记录。因为有些记录是不出现在 已安装
列表里的(个人猜测是已下架的壁纸)
导致每一次安装都需要下载大量的文件。
针对这种记录,就需要用特别的方法。
按道理是可以的,
但是我没有试过,因为有很多壁纸都搜不到了
这种方法也很麻烦
首先,必须清楚每一个壁纸有一个特定的 编号,删除也是根据这个编号进行删除的
实际上每一个壁纸的文件夹名就是这个编号,只要把这个编号出现在列表里,就能取消订阅了
实际操作方法:
bin\workshopcache.json
文件)workshopid
为编号即可,最好 title
也改成编号,不然认不出Wallpager Engine
,选中取消订阅
即可Wallpager Engine
,就删除成功了提供一个脚本批量生成
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('成功生成缓存配置文件,可以进行取消订阅')
全选或框选批量取消订阅即可
本来是想找到接口,通过接口删除的,但能力有限,没找到方法
2020-08-04 00:46:55
登录令牌注入
token.interceptor.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;
}
}
例如登录令牌失效
reponse.interceptor.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);
}));
}
}
@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
};
}
}
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;
}
}
}
2020-08-02 05:46:56
解释:.dpl
主要为Potplayer
直播源列表文件
文件头
playname
为当前播放的地址
topindex
为当前播放的序号,即在列表中的位置
列表项
序号从 1
开始
地址行为 序号 + *file*
+ 地址
标题行为 序号 + *title*
+ 地址
txt 文件每一行格式为
具体转换代码
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);
}
获取编码
/// <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;
}
}
2020-08-01 04:42:32
这是一个以前开发的正常上线项目,今天要更新一下,突然出问题了
在微信开发者工具中是正常的,使用真机模拟时,发现一直加载中,而且加载的圆已全,不动了,在控制台也没有任何错误输出,在首页的js文件中 onLoad
onShow
方法都正常执行了。
先看是不是代码问题,无论删除多少代码都不行,
在看是不是项目配置版本的问题,改到最新也不行,
最后,查到 app.json
这个文件,删除一些配置,试试,才发现是底部导航栏配置的问题,
在以前,是允许导航栏名称为空的,即以下写法是没有问题的
但是现在不行了,必须有字符,需改成
2020-07-30 00:48:01
主要通过 Navigator.pop(context, args);
第二个参数就是要传的值
那么该怎么接受呢
通过 then 即可接受传过来的值,然后根据值判断是否登录成功等
2020-07-26 01:02:11
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
2020-07-24 01:32:57
init
函数用于包的初始化
init
函数执行在 main
之前
一个包 可以有多个 init
函数
同一个包 的 init
函数执行顺序无明确定义,不同包的init函数是根据包导入的依赖关系决定的
使用 import
就会执行 init
如果仅执行包的 init
函数,不导入其他函数,使用 import _ "package"
2020-07-24 00:23:55
export class ThemeModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ThemeModule
};
}
}
修改为
解决办法:
配置 CommonJS 依赖项
修改 angular.json
projects
> 项目名
> architect
> build
> options
添加 allowedCommonJsDependencies
填入依赖名
2020-07-22 04:28:18
android:value
为数字则无法通过字符串获取例如:
获取
packageManager.getApplicationInfo(
packageName,
PackageManager.GET_META_DATA
).metaData?.getString("XXX");
返回的值为 null
, 因为 Bundle
自动识别为数字
正确的写法是修改 AndroidManifest
,前加上 \
(反斜杠和空格)即可
2020-07-22 04:27:26
php 与其他最大的不同是不支持多进程,本身也就没法实现web程序,需要搭配其他服务软件。
因此每一个访问就是一个全新的进程,没法实现一个内容的共享,造成内存浪费,使得并发数少。
目前是实时寻找路由,加载多个文件,时间较长
使用路由缓存,加载一个文件即可执行最终控制方法。
可以使用 redis
,已最终路径为键,保存。
关于路由重写的,可以在生成时做好映射
目前处于每次都得加载三个文件并进行多维数组合并,考虑进行合并成一个文件。
查询语句考虑抛弃实时拼接方法
使用配置文件,配置路由,配置路由需要的参数,配置路由需要的依赖项,直接就访问控制器方法。最大程度减少加载文件。数据库访问全部使用 redis
,同步到mysql通过其他方式,数据可以共享的都存到 redis
中。
2020-07-14 21:51:17
嵌套使用需要在子列表添加
子项可以是列表或其他部件
如果是列表则使用 SliverList
、 SliverFixedExtentList
、 SliverGrid
如果是其他部件则必须使用 SliverToBoxAdapter
SliverList
和 SliverFixedExtentList
的区别,如果知道高度 则推荐用 SliverFixedExtentList
例如
标题栏
SliverFixedExtentList(
itemExtent: 50, // 是设置每一个子元素的高度
delegate: SliverChildListDelegate(
<Widget>[
Container(
color: Colors.white,
height: 50,
child: Center(
child: Text(title),
),
)
],
),
)
两列商品类别
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),
),
2020-07-14 19:03:58
添加项目依赖
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,
),
)
必须放到 一个父元素了,不能直接放到 <Widget>[]
中作为一个子元素,原因是必须指定尺寸,即宽和高
2020-07-14 19:03:17
虽然获取到的数据可以自动进行json解析,但是获取的数据不会自动进行类转换
即
T
只能指定为 dynamic
或 Map
,无法作为其他类进行转换,例如定义了一个类
@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);
}
而实际服务器响应的是json
{
"name": "zodream",
"version": "0.1",
"logo": "https://zodream.cn/assets/upload/image/wap_logo.png",
"category": 6,
"brand": 1,
"goods": 133,
"currency": "¥"
}
使用 dio.request<Site>()
是会报错的,
只能手动转换
2020-07-14 19:02:31
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,
),
);
}
}
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,
)
],
),
);
}
MediaQuery.of(context)
获取当前设备的信息
属性 | 说明 |
---|---|
size | 逻辑像素,并不是物理像素,类似于Android中的dp,逻辑像素会在不同大小的手机上显示的大小基本一样,物理像素 = size*devicePixelRatio 。 |
devicePixelRatio | 单位逻辑像素的物理像素数量,即设备像素比。 |
textScaleFactor | 单位逻辑像素字体像素数,如果设置为1.5则比指定的字体大50%。 |
platformBrightness | 当前设备的亮度模式,比如在Android Pie手机上进入省电模式,所有的App将会使用深色(dark)模式绘制。 |
viewInsets | 被系统遮挡的部分,通常指键盘,弹出键盘,viewInsets.bottom 表示键盘的高度。 |
padding | 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏。 |
viewPadding | 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏,此值独立于padding 和viewInsets ,它们的值从MediaQuery 控件边界的边缘开始测量。在移动设备上,通常是全屏。 |
systemGestureInsets | 显示屏边缘上系统“消耗”的区域输入事件,并阻止将这些事件传递给应用。比如在Android Q手势滑动用于页面导航(ios也一样),比如左滑退出当前页面。 |
physicalDepth | 设备的最大深度,类似于三维空间的Z轴。 |
alwaysUse24HourFormat | 是否是24小时制。 |
accessibleNavigation | 用户是否使用诸如TalkBack 或VoiceOver 之类的辅助功能与应用程序进行交互,用于帮助视力有障碍的人进行使用。 |
invertColors | 是否支持颜色反转。 |
highContrast | 用户是否要求前景与背景之间的对比度高, iOS上,方法是通过“设置”->“辅助功能”->“增加对比度”。 此标志仅在运行iOS 13的iOS设备上更新或以上。 |
disableAnimations | 平台是否要求尽可能禁用或减少动画。 |
boldText | 平台是否要求使用粗体。 |
orientation | 是横屏还是竖屏。 |
2020-07-14 19:00:07
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})
在 main.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(),
);
}
}
2020-07-03 06:00:23
必须安装 Microsoft Edge (Chromium) Canary channel 浏览器
NuGet 安装 Microsoft.Web.WebView2
预发行版 0.9.538-prerelease
,正式版0.9.538
安装是没用的
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
<wv2:WebView2 x:Name="Browser" Source="https://www.baidu.com"/>
SourceChanged
可以获取当前的网址,可以更新网页通过 js 修改的网址
NavigationStarting
网页加载开始
NavigationCompleted
网页加载完成
CoreWebView2Ready
CoreWebView2
初始化完成,可以进行 CoreWebView2
上的事件绑定
CoreWebView2.NewWindowRequested
加载新标签之前触发,可以通过 e.Handled = true;
阻止触发,e.Uri
为要跳转的网址
主要通过 ExecuteScriptAsync
方法执行 js 代码,返回的值都是 JSON
过的。如果 JSON
编码失败则返回 null
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>";
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();
}
}
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
}
使用获取cookie
2020-06-26 19:51:10
今天突然想到,虽然sql 注入
不了(因为所有前台参数是使用pdo 自带的过滤,比起自己造的过滤器好多了),但是xss 攻击还是有可能的
用户名注入
在用户名填入以下字符,
注册成功后,立马显示弹出框,可以注入
解决:直接对用户名保存进行转换
标题可以 xss 攻击
主要原因:面包屑没有做转换
解决:对标签保存前进行转换
解决:在保存前进行转换
调用方法即可
2020-06-26 19:50:01
码云来作为中转站,但是码云对仓库大小有一定限制,所以太大的仓库用这个方法不行
只需要修改你的路径 github.com
为 github.com.cnpmjs.org
使用 git clone
时常终止
这时可以使用 github 客户端
,虽然对下载速度没有帮助,但是可以保证下载不会中断
2020-06-18 20:56:03
服务端使用:php
前端使用 ts
原因:在服务端使用了 unset()
移除数组的项导致索引数组变成了关联数组,传到前端变成了对象就没法使用 for
循环了
解决:使用 array_values()
再转成索引数组即可
原因:使用了 ===
判断,但两个id 一个是 string
一个是 number
判断失败
解决:改为 ==
即可
原因:收货人并没有进行保存,而在更新地区时使用了 this.setData(this.data)
导致收货人也更新了
解决:要么进行收货人输入保存,要么使用 this.setData({region})
,只更新一个
2020-06-13 00:37:48
版本: 1.1.0
--watch 监听脚本变动
--input 源码文件或文件夹,默认 `src` 文件夹
--output 目标保存文件夹,默认 `dist` 文件夹
--mini 编译小程序,默认为 编译模板
生效条件 以 @
开头,前面可以有空格,功能占整行, @
后面的内容以空格隔开,空格之后为评论内容,不做生成
@@ 为注释,放在首行为声明此文件不生成html页面
@... 加载其他引用此文件的文件内容
@layout/main 加载其他文件,拓展名可以省略
@item@5 加载其他文件重复5次
加载的样式及样式文件自动合并到 head
末尾
加载的脚本及脚本文件自动合并到 body
末尾
支持 ts
sass
自动编译
版本: 1.0.5
@click.stop
的转化@click="tap(1)"
允许接收 function tap(i, e: TouchEvent){}
text
标签生成,例如 a
不再内部生成 text
子标签v-show
解析问题 vue
模板的解析流程,直接内嵌 rename
文件后缀最新使用方法
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/')));
});
2020-06-11 01:02:59
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;
ico 图标是由不同尺寸的png 图片组成。因此可以设置不同尺寸下不同的图。
生成不同尺寸的图
/// <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;
}
ico 头
/// <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); //
}
获取图片数据的长度,及每一个像素,每一个像素又包括 RGBA
写入每张图的位置
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);
}
写入每张图的数据
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);
}
}
}
生成ico
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();
2020-06-09 06:26:04
开发时,遇到 文字太长未换行,记录一下
通过代码设置内容。默认支持 \n
换行
或
TextBlock1.Inlines.Add(new Run("a"));
TextBlock1.Inlines.Add(new LineBreak());
TextBlock1.Inlines.Add(new Run("b"));
xaml
上通过转义字符&#x000A;
或 <LineBreak/>
换行
<TextBlock>
<Run>a</Run>
<LineBreak/>
<Run>b</Run>
</TextBlock>
&#x0020;
空格
&#x0009;
Tab
&#x000D;
回车
&#x000A;
换行
设置属性 TextWrapping="Wrap"
2020-06-08 07:03:02
使用方法
const items = [3, 1, 12];
const res = items.sort();
// items [1, 12, 3]
res.push(4);
// items [1, 12, 3, 4]
const items = [3, 1, 12];
items.sort((a: number, b: number) => {
return a - b;
});
// items [1, 3, 12]
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 为最重要的选择原因
这是官方的例子
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;
});
});
周边插件也挺丰富的,动画(spine/dragonbones),粒子系统,物理引擎等等
2020-06-07 05:46:19
PS: 这是无聊时,嫌弃列表页不好看,之后的改良之路,这只是代表个人审美。
包括:
从图上可以看出基本功能都存在了,但是感觉不美观,也空洞。
从搜索框开始
新增
按钮移动右边
有铺满的感觉了
增加页面提示
页面提示,也可以作为操作指南。
解决了搜索顶格的不适,让搜索不至于处在视野边缘。
移动分页到中间,
居中,大屏幕下,视野中间,容易找
抛弃传统 table
,自适应方面差及屏幕展示的信息少(不是指那种有横向滚动的,而且横向滚动条很烦)
这样主要信息都能查看,而且自适应方便,主题也鲜明了
2020-06-07 05:45:47
一个用于局域网传输文件的小工具
简单的局域网内部文件传送。
点击 监听
按钮即可
选择或填写 目标IP
(即接收方ip)
点击 选择文件
按钮选择文件夹即可
2020-06-02 19:03:15
<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>
refreshChange
下拉刷新事件
moreChange
加载更多事件
more
是否有更多
loading
是否加载中
distance
触底距离
@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();
});
}
推荐使用方法通知,虽然 loading
也可以,但是如果加载没有时间间隔的话,是不起作用的。
startLoad
指定开始加载
endLoad
指定加载完成
@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) {
// 触发加载更多
}
}
第一次不能自动加载,如果有更多,但没有滚动条,也不会自动加载更多
请查看【GITHUB】
2020-05-29 06:21:47
必须添加 NetFwTypeLib
,在 引用
> 添加引用
> COM
/// <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);
}
2020-05-29 06:21:21
AddressList
必须倒序查找
/// <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 '';
}
}
这里用的是 ping
方法 ping
255 个地址
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());
}
}
获取的时间为 2-3 秒,获取的也不一定准确
2020-05-28 06:20:47
使用 @Input()
定义
@Component({
selector: 'app-page-tip',
template: './page-tip.component.html',
styleUrls: ['./page-tip.component.scss']
})
export class PageTipComponent {
@Input() public title = '提示';
}
使用
通过 @Output()
定义事件,使用 .emit()
触发事件通知
使用
使用 [()]
进行双向绑定,那么应该修改事件,保证 事件名为 参数名 + Change
例如
@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();
}
}
使用
例如
获取 div
,必须在div
上加 #+命名
然后通过 @ViewChild('命名')
获取
这是 box.nativeElement
就是 HTMLDivElement
,可以想普通元素一样操作。
如果要绑定事件,那么请注意先等页面已经生成,不然获取不到。
ngAfterViewInit
就是在页面初始化后触发。
export class HomeComponent implements AfterViewInit {
@ViewChild('app')
private box: ElementRef;
ngAfterViewInit(): void {
const div = this.box.nativeElement as HTMLDivElement;
div.addEventListener('click', (event) => {
// TODO
});
}
}
例如
通过 @ViewChild(组件类名)
获取
2020-05-27 03:01:13
这是一个注册表单,
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');
}
}
通过 [formGroup]
绑定表单数据源
通过 formControlName
绑定表单项
<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>
获取错误信息
AbstractControl.invalid
获取是否有误
AbstractControl.errors.
获取具体错误信息,注意 .errors
有可能为 undefined
需要先判断
在这里,自定义了两个验证器:
passwordValidator
可以验证密码的复杂程度。export const passwordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
return control.value && control.value.length < 6 ? {
password_simple: true
} : null;
};
confirmValidator
可以验证确认密码是否一致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;
};
};
1). 接受两个参数,为需要比较的两个字段名,
2). 必须放在 FormGroup
上,不然获取不到两个值。
3). 通过 FormGroup.errors && FormGroup.errors.confirm
获取有误信息
2020-05-25 05:19:07
Startup.cs
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));
}
AddTransient
每次注入的都是新实例,相当于手动 new
。
AddScoped
每次请求,都获取一个新的实例,在一个请求中,获取多次会得到相同的实例。
AddSingleton
每次都获取同一个实例
2020-05-24 04:42:03
在 createjs 中,默认是没有封装 canvas 的 drawImage 方法,
想 添加图片到 canvas 上,只能使用
const img: HTMLImageElement;
const box = new createjs.Shape();
box.graphics.beginBitmapFill(img).drawRect(0, 0, img.width, img.height);
如果想移动图片的位置,只能使用 box.x
和 box.y
,
而改变 drawRect
上的值,就是在图片上裁剪。默认 img
是无限重复的,
即 .drawRect(10, 10, img.width, img.height)
并不是移动到 10,10
再开始画整张图,而是先从 0,0
画,横向重复一次,纵向再重复一次,这是成 田
字排列四张图,然后 选取 10,10
到 img.width + 10, img.height + 10
矩形区域显示
那么,想从 10,10 开始画整张图该怎么办?
canvas 默认是有 drawImage 方法,调用就行了
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));
实现一个封装的对象,exec
方法是这个对象必须有的,有这个方法,才能被调用,实现绘制。
2020-05-22 01:58:39
本文为阅读 《平衡掌控者游戏数值战斗设计》做的笔记。
RPG
包括:动作角色扮演、模拟角色扮演、策略角色扮演、角色扮演冒险、恋爱角色扮演、角色扮演解谜
ACT
包括:射击(STG)、格斗(FTG)、动作冒险、动作角色扮演(APRG)
AVG
包括:动作冒险、文字冒险、恋爱冒险
SIM 或 SLG
包括:策略模拟、模拟经营、模拟养成、战争、飞行、载具模拟
包括:回合战略、回合战术、即时战略、即时战术、解谜
包括:音乐、休闲、体育(SPT)、竞速(RAC)等
基于 MMORPG (大型多人在线角色扮演)
通过虚拟世界发泄现实生活压力,包括语言攻击和虐杀对其他人造成心理或精神伤害而获取成就感。
以提升装备和等级、完成任务为目的。
包括审美型和学习型,游戏就是获取新素材分享出去。
游戏就是为了交朋友。
2020-05-22 00:29:37
以列表和详情为主的程序
新闻、博客类
消息聊天类
2020-05-22 00:26:14
5月19日 星期二 天气晴
今天做了什么。
要多记自己看到或经历过的事和人,多记身边发生过的现实生活中的事和人。只有真实的最能反映事件和心情,真实地叙述问题,才有参考价值,才是有效的积累,才能反映规律。
写日记主要是练笔。它不同于一般的记录。因此,要通过仔细回忆和重新观察,把事件或人物的细节写出来。
一则日记一般只记一件事,不能太杂,不能拖泥带水地在一则日记中什么都级。一则日记要为荣一个中心。这个中心可以是一件事、一个场景、一段对话、一处风景、一个外貌、一种心情、一个动作等。切记不要流水账。
每个人都会对身边发生的事产生一些感受,都会有自己的想法。在记叙过程中,可以穿插自己的感受。但这种感受一定要真实,自己是怎么想的就怎么写,不要老是考虑这个想法对不对。
2020-05-20 19:12:09
本文只是个人观点,不涉及任何利益之争。
经测试,在手机上使用 edge 浏览器、火狐浏览器、谷歌浏览器,都会出现 您的xx手机浏览器版本过低,请立即免费升级。
垃圾广告既视感!
先来猜测一下,为什么会这么做?有哪些可能原因
点击弹窗的按钮 确定
进入一个网址页面
那么点击一下 取消
试试,弹出 您确定取消升级吗?
, 确定
关闭弹窗,取消
进入下载页面
关键是,每次进入都有弹窗。
使用电脑打开谷歌浏览器,切换到手机模拟模式,修改手机型号,苹果手机不会弹出,找个安卓的,Moto G4
会弹窗。
找到工具 性能
页,进行录制,刷新页面,关闭弹窗,
停止录制,找到执行脚本
打开脚本 jian.js
发现里面加密了,
下载脚本到本地,根据据关键词 手机浏览器版本过低
可以搜索到,加密了就不继续了,
这个脚本文件是 jian.t58b.com
这个域名下的
按照简书被劫持了吗 说法:这是 简书
的广告主
经过三重域名跳转,跳转到一个 APP 下载网站,
关于 最终下载的APP 就不知道是什么了,不敢安装
通过域名查询 发现
通过备案查询发现 t58b.com 为个人,而最终 跳转域名为 微型企业
到此为止,不想继续了
做网站还是得注意选择合适的广告主,虽然要恰饭,但也不能随便恰,逮着狗屎啃,惹了一身骚,导致网站体验不好就不值得了。
选择广告也应该考虑用户的体验,影响用户体验,就不好了。
2020-05-20 02:48:35
主要写在模块的 -routing.module.ts
文件里
routes
就是放路由的。
显示页面,例如: home
路径显示 HomeComponent
的内容
未匹配到指定 未找到
页面
指定路由重定向到新的路由
指定路由到指定模块
const routes: Routes = [
{
path: 'blog',
loadChildren: () => import('./blog/blog.module').then(m => m.BlogModule)
},
];
在模块中可以指定路由是否要加载模块公共模板
const routes: Routes = [
{
path: '',
component: FrontendComponent,
children: [
{
path: 'home',
component: HomeComponent,
},
]
},
{
path: 'blog',
component: HomeComponent,
},
]
注意 frontend.component.html
必须有 <router-outlet></router-outlet>
才能加载子路由
这样 /home
就会先加载 FrontendComponent
再加载 HomeComponent
并把 HomeComponent
的内容 放在 frontend.component.html
的 <router-outlet></router-outlet>
位置
而 /blog
就只会加载 HomeComponent
,这样就可以实现模块内不同页面有不同的框架结构。
也可以实现 不同的页面分别由不同的公共模板
2020-05-20 02:02:50
php发展到今天为止有很多框架,一般开发一个项目都是先选一个熟悉的框架开始,各种PHP开发框架也让程序开发变的简单有效。每一个开发者都知道,拥有一个强大的框架可以让开发工作变得更加快捷、安全和有效。
对于框架的选择往往从几方面考虑:
对小项目没有加成。
一款快速、安全和专业的PHP框架。
本身提供一个可视化界面生成代码
官网:https://www.yiiframework.com/
一款非常敏捷的开源PHP框架。适合开发一个简单而优雅的工具包。
适合小型项目。
官网:https://www.codeigniter.com/
顶尖的PHP框架,文档丰富
官网:https://framework.zend.com/
是一款可重用的PHP组件,例如 laravel 就基于 Symfony 开发的
PHP 协程框架,是一个面向生产环境的 PHP 异步网络通信引擎,使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务
目前,国内很多PHP招聘都需要了解swoole
运行速度最快的一个PHP框架。主要代码使用 C语言编写。所以配置安装相对麻烦,不一定能适配最新 php 版本(除非你能自己编译c语言编写的php拓展)。
主要用户为国内开发者,作为国内初学者入门框架。
2020-05-18 06:10:54
阅读本文需要了解 scss
angular
这样就实现中间菜单超长滚动,底部菜单自动增高
使用的图标是 iconfont
图标
navToggle
是 顶部汉堡按钮
控制菜单收缩用的
router-outlet
是 angular
用来加载右侧内容的标签
<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>
下面是 css 样式
scrollbar()
这个是设置滚动条样式的,默认的太大了
@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;
}
}
}
}
}
整体项目代码:Angular-ZoDream
当前项目未使用服务端数据对接,所以可以查看实际效果
2020-05-17 06:14:18
根据下级路由改变父组件的菜单选中
主要通过 this.router.events.subscribe
获取路由的变化
export class FrontendComponent {
constructor(
private router: Router) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
// event.url '/blog'
this.routerChanged(event.url);
}
});
}
}
获取当前路由网址进行切换
2020-05-16 03:34:08
在 src/index.html
加上动画效果
在 <app-root></app-root>
的后面
<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>
这是一个加载动画
当加载完需要隐藏
需要在第一个 Component 初始化的时候隐藏
例如 src/app/app.component.ts
2020-05-16 03:33:23
每一个模块都是分开打包的,只有用到模块才会加载模块的资源,因此可以加快第一次打开的时间,减少不必要的加载
生成一个 frontend 模块,并创建路由模块
在 frontend 模块下继续添加模板
src/app/app-routing.module.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'
},
];
修改 src/app/frontend/frontend.component.html
<header>
导航栏
</header>
<router-outlet></router-outlet>
<footer>
<div class="copyright">
<p>Copyright ©zodream.cn, All Rights Reserved.</p>
</div>
</footer>
<router-outlet></router-outlet>
是必须的,是根据子路由加载页面的
修改 src/app/frontend/frontend-routing.module.ts
const routes: Routes = [
{
path: '',
component: FrontendComponent,
children: [
{
path: 'home',
component: HomeComponent,
}, {
path: '',
redirectTo: 'home',
pathMatch: 'full',
}, {
path: '**',
component: HomeComponent,
}
]
},
];
出现一下内容则表示正常了
2020-05-16 03:32:44
必须有一个程序名,没办法生成到当前文件夹
在 angular.json
修改 projects
> my-app
> architect
> build
> options
> styles
节点
最前面加上 "node_modules/bootstrap/dist/css/bootstrap.css",
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.css",
"node_modules/@fortawesome/fontawesome-free/css/all.css",
"src/styles.scss"
],
这样就不需要手动去添加样式了
引用 css
同上
"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"
]
在 angular.json
修改 projects
> my-app
> architect
> build
> options
> styles
节点加上 css 文件相对路径
在 angular.json
修改 projects
> my-app
> architect
> build
> options
> scripts
节点加上 js 文件相对路径
在 angular.json
修改 projects
> my-app
> architect
> build
> options
> assets
节点加上文件相对路径
2020-05-15 05:52:12
修改网站根目录下的 .htaccess
文件
修改网站根目录下的 web.config
文件
<?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>
修改网站根目录下的 .htaccess
文件
RewriteEngine on
RewriteCond %{HTTP_HOST} ^zodream.cn [NC]
RewriteRule ^(.*)$ http://www.zodream.cn/$1 [L,R=301,NC]
修改 无www
域名 网站根目录下的 web.config
文件,必须安装 HTTP重定向
功能
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpRedirect enabled="true" destination="http://www.zodream.cn" httpResponseStatus="Permanent" />
</system.webServer>
</configuration>
修改网站根目录下的 .htaccess
文件
RewriteEngine on
RewriteCond %{HTTP_HOST} ^www.zodream.cn [NC]
RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC]
修改 带www
域名 网站根目录下的 web.config
文件,必须安装 HTTP重定向
功能
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpRedirect enabled="true" destination="http://zodream.cn" httpResponseStatus="Permanent" />
</system.webServer>
</configuration>
修改网站根目录下的 .htaccess
文件
RewriteEngine on
RewriteCond %{HTTP_HOST} !^zodream.cn [NC]
RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC]
修改 默认 *
域名 网站根目录下的 web.config
文件,必须安装 HTTP重定向
功能
2020-05-14 21:13:23
npm gulp-vue2mini
本插件开发初衷,以类似于 vue
的代码语法开发微信小程序
但是,本程序不能直接把vue
程序转化为小程序代码。
那么,这样做有必要吗?
有!
js
、css
、html
、json
分离的,这样看上出去文件繁多,一个小项目干嘛要那么麻烦typescript
,有很好的代码提示本程序提供配套示例代码,这是一个初始化版本,
直接下载本代码,
在 vscode
中打开
安装依赖
src/app.vue
为程序入口
src/pages/index
为首页
编译程序
如果你的 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自动生成,支持 属性合并
属性名 | 目标属性 |
---|---|
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配置生成
@WxJson({
usingComponents: {
MenuLargeItem: "/components/MenuLargeItem/index",
MenuItem: "/components/MenuItem/index"
},
navigationBarTitleText: "个人中心",
navigationBarBackgroundColor: "#05a6b1",
navigationBarTextStyle: "white"
})
自动合并页面相关的json文件
支持自动合并 methods
lifetimes
pageLifetimes
, 如果已有 属性会自动合并
methods @WxMethod
lifetimes @WxLifeTime
pageLifetimes @WxPageLifeTime
自定义部件自动合并方法到methods
属性中
最终生成
index.vue
<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>
index.wxml
index.wxss
index.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());
新增了一些指定的声明请参考
2020-05-14 06:07:23
在 createjs 中物体是不会自动超出边界自动隐藏的,
例如,负坐标也不会在视野内隐藏
里面的每一个 DisplayObject
都是没有宽和高限制的
例如 createjs.Container
上面加个会动的 createjs.Shape
, 我想让她 在 x: 0, y: 0, width: 600, height: 600
这个范围内可见,跑到其他地方不可见,该怎么办?
下面代码就可以实现,
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;
但请注意 mark
drawRect
使用的时 box
的父坐标系统,不是 box
的坐标系统,
这并不能遮住 box 里面坐标 x 或 y 为负的,可以考虑 mark
drawRect(box.x + x, box.y + y, 600 - x, 600 - y)
扩大蒙版的范围
2020-05-13 06:08:01
当游戏结束了,重新开始发现使用 Tween 的补间动画不起作用了。
在 github 的 issues 找到同样的问题
意思就是 Tween 上的一个 _inited
私有属性没有更新,只要增加一句代码就行了
但这种方法肯定是不友好的,但是 Tween
又没有提供 reset
方法,
最后通过查看源码,发现是 createjs.Ticker
引起的
当执行 createjs.Ticker.reset()
会清空所有 tick
事件监听,而 Tween
又没有收到通知,没法更新 _inited
属性,导致 Tween
不会重新添加事件监听 createjs.Ticker.addEventListener("tick", Tween)
所以,要么不要使用 createjs.Ticker.reset()
,要么使用
2020-05-12 20:27:55
本文使用 typescript 开发
node
安装一些依赖
增加文件 tsconfig.json
ts 的编译配置信息
{
"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",
]
}
gulpfile.js
gulp 编译方式
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/'));
}));
新增 src
文件夹 里面放 主控制
新建 src/core
文件夹 里面放基本定义
新建 src/scene
文件夹 里面放每一个场景
具体代码示例查看 示例
初始化场景
设置启用触摸
设置场景的尺寸
(<HTMLCanvasElement>stage.canvas).width = width;
(<HTMLCanvasElement>stage.canvas).height = height;
设置场景刷新频率
清空场景,更换场景时使用
添加物体
刷新生效
新增 index.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>
打开 index.html
有的浏览器直接打开文件无法加载资源文件,请使用其他程序,例如 iis,
或
server.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);
});
启动
打开 http://localhost:8081 即可查看效果
2020-05-12 20:27:07
站长之家
的 百度关键词挖掘
,寻找关键字站长之家
的 SEO综合查询
,寻找同类型网址的关键字长尾关键词是指网站上的非目标关键词但与目标关键词相关的也可以带来搜索流量的组合型关键词。
长尾词为几个关键词的组合,通常在搜索引擎搜索时出现的智能提示就是长尾词。
首页关键词优化,增加相关关键字,并进行相关文章展示
站长之家-百度关键词挖掘
搜索相关关键词,分别选择几个合适的 长尾词
长尾词
进行文章添加未知
关键词来源:文章内容重点名称的提取
SEO综合查询
参考其他网址获取关键词未知
2020-05-12 20:26:36
本文为参考网上的相关文章,进行对本站的优化,效果暂时不知道,重点:效果不知道
外部链接是别人对你的投票,内部链接优化建设是自己对自己页面进行投票,页面被投票越多,说明这个页面内容越重要
最好给自己的网站建立一个完整的网站地图,把网站地图的链接放在网站首页,这样能更利于让搜索引擎蜘蛛发现地图,抓取网站内容。
一般存放在根目录下并命名sitemap,为爬虫指路,增加网站重要内容页面的收录。
当然,在一些搜索引擎是允许主动提交 sitemap 的。
通过程序自动生成sitemap.xml 并把她主动提交到搜索引擎,
百度、谷歌、360 都有收录,必应没收录
一般来说网站结构建设良好,网站首页的权重是最高的,栏目页第二,内容页最差。如果有站长朋友有一些重点推广的页面,可以通过网页的链接提升权重的,使这些页面的权重升高。
已修改首页,增加多个模块展示更多的文章
未知
对一般的网站而已,最好能确保从首页开始算,点击2-3次就能到达网站的任何一个页面,最多不要超过4次,点击次数越少越好,更有利于网站优化
网址越大,层数就不好控制,但要保证重点内容能符合标准。
文章为中心,所以增加首页文章展示
未知
很多站长会对网站的树形结构有误解,正确的理解是,在不同栏目的网页上链接到其他栏目的相关网页,整个网站的链接最后看起来像蜘蛛网一样既有主干也有页面之间的相互链接。
增加标签聚合,增加归档,增加文章相关文章展示
未知
2020-05-12 07:11:59
Select2是一款基于JQuery的下拉列表插件,主要用来优化select,支持单选和多选,同时也支持分组显示、列表检索、远程获取数据等功能。
<!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>
需要两个数据属性id
和text
$('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
}
};
}
}
});
设置默认选中
2020-05-11 20:57:49
国内的 【万网】
国外的 【Godadday】
分为 虚拟主机 和 服务器
买国内的空间需要给域名备案,不然无法打开。
个人的话对网站内容有限制。
价格相对便宜,系统和环境不能自定义,只能选指定的,灵活度低,但对新手友好
系统能更改
运行环境需要自己搭建。
代码的编辑器: 要注意文件的编码格式,推荐 Vim、NotePad++、Vs Code
FTP上传工具:Filezilla
【下载】
环境要求:php7.3 + mysql 5.6
有的虚拟主机包括 wordpress,但版本可能不是最新
从官网下载 WordPress,解压
通过FTP工具将 wordpress 文件夹里面的文件全部上转至你的网站根目录。
浏览器访问你的域名,默认会跳转到 /wp-admin/setup-config.php
第一步,选择语言,wordpress 是只支持单语言
第二步,说明,直接下一步
第三步,配置数据库信息,数据库必须存在,不存在的话请先创建,可以使用 phpMyAdmin 创建
第四步,配置站点信息及管理员账号密码
/wp-login.php
这是登录后台网址
显示语言在 左边菜单 Settings
修改 Site Language
点击 Save Changes
保存
Posts
中发布新的文章
Appearance
下 Themes
可以更改主题模板
Plugins
可以安装各种插件,但安装的越多,打开速度越慢,初期不建议使用
2020-05-09 01:55:38
启用或关闭Windows功能
适用于Linux的Windows子系统
Ubuntu
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
cd /usr/local/bin
redis-server /etc/redis.conf
redis-server /etc/redis.conf & #指定配置文件启动redis,且后台启动
redis.conf
来禁用远程修改 DB 文件地址
禁止外网访问 Redis
可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等,最多可以容纳的数据长度是512M
set mykey "this is a test" //通过set命令为键设置新值,并覆盖原有值。
get mykey
incr mykey //该Key的值递增1
decr mykey //该Key的值递减1
del mykey //删除已有键
键值对字典结构
简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。
解决办法:
指访问不存在的数据,导致跳过缓存,直接访问数据库。
例如:数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决办法:
就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决办法:
2020-05-08 18:44:38
本文为阅读 《黑客攻防技术宝典:Web实战篇(第2版)》 的笔记
根据已知的字符串确认数据,排除在黑名单之上的数据。
SELECT
可以尝试 SeleCt
or 1=1--
可以尝试 or 2=2--
alert('xss')
可以尝试 prompt('xss')
在表达式之前插入空字节
通过删除特定表达式,过滤内容
删除 <script>
可以尝试 <scr<script>ipt>
../
..\
可以尝试 ....\/
删除省略号
可以尝试 %2527
或 %%2727
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 字符串 |
2020-05-06 20:46:04
httpd.conf
打开配置文件 httpd.conf
找到 IfModule mime_module
配置模块
添加方法 AddType
+ 响应头
+ 文件拓展名
mime.types
修改方式
响应头
+ 文件拓展名
添加这个全局设置,但只作用文件类型响应头为 text/plain
或 text/html
其他类型需要在 IfModule mime_module
下使用
2020-05-06 19:04:01
直接在任务栏中搜索 区域设置
或 开始
->设置
->时间和语言
右上角相关设置
中的 日期、时间和区域格式设置
进入 区域设置
右上角 相关设置
中的 其他日期、时间和区域格式设置
区域
下的 更改日期、时间或数字格式
管理
选项卡 更改系统区域设置
勾选 使用 Unicode UTF-8 提供全球语言支持
重启电脑即可
2020-04-30 02:44:30
本次使用环境
sqlmap
-u
网址 ""
--dbms=
数据库类型 mysql
--cookie=
设置cookie 登录信息 ""
--method=
请求方式 POST
--headers=
设置请求头
暂时找不到注入
2020-04-26 21:39:29
需要修改配置文件
config.inc.php
$cfg['blowfish_secret'] = '5d01588e401875b15374662a96261262';/* YOU SHOULD CHANGE THIS FOR A MORE SECURE COOKIE AUTH! */
随便输入一个32位的字符串即可。
“cookie”身份验证类型使用AES算法加密密码。如果您使用的是“cookie”身份验证类型,请在此输入您选择的随机密码短语。AES算法将在内部使用它:不会提示您输入此密码短语。
这个密钥应该有32个字符长。使用较短的会导致加密cookie的安全性较弱,使用较长的不会造成危害。
2020-04-26 17:53:09
解压到文件夹即可
具体参数 见【文档】
获得hash $office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e
这是offile 2010
直接运行
.\hashcat64.exe -m 9500 '$office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e17d' -a 3 ?a?a?a?a?a?a?a
或把hash 保存到文a.txt
-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位
s
回车即可
Status...........: Running
表示正在运行中
Status...........: Cracked
表示解密成功
Status...........: Exhausted
表示程序已结束,但没有找到密码
当 Status...........: Cracked
出现后,程序运行结束
上一行就会出现破解密码
:123
最后的冒号后的就是密码了
CL_OUT_OF_RESOURCES
解决方法:
新建文本 wddm_timeout_patch.reg
,点击即可
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers]
"TdrLevel"=dword:00000000
这是因为在 powershell 命令行中直接输入,导致hash被当作命令
解决方法:
1. 把hash保存到文件,以文件形式传入
2. 用英文单引号把hash 当作字符串输入,双引号是不行的
表示密码已经找到过了,通过 --show
即可查看
2020-04-25 22:33:12
panic: reflect: call of reflect.Value.Interface on zero Value
在使用 reflect 报错
func Dump(vals ...interface{}) {
for _, v := range vals {
val := reflect.ValueOf(v)
fmt.Printf("[%T]", val.Interface())
}
}
u := &User{}
Dump(u)
修改成
因为反射看不到未导出的函数
这个错误是没办法通过方法内部解决的,是写代码时的错误,只能修改使用方法的代码。
同类错误 reflect: call of reflect.Value.Call on zero Value
2020-04-24 06:08:14
一开始只有web版,用来约束自己,每次做事都有一个时间统计,合理安排工作与休息时间。
任务添加
任务计时
任务记录回顾
扫码登录web (这是我一直想实现的功能,每次登录太麻烦了)
签到 (目前没有签到奖励)
修改个人信息
查看博客文章
任务包含子任务
任务记录图表统计
加入个人财务管理
Microsoft Store
我的时间回忆薄
获取
安装
我的
+
进入 任务列表+
进入添加任务任务名
任务说明
每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止
√
进行保存🖊
编辑按钮+
添加到今日任务
就会把选中的任务添加到今日任务,可以连续点击,添加多次任务完成
按钮就会退出编辑模式结束任务
可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了开始
按钮,开始任务暂停
,表示有其他事打扰,需要暂停一下,处理完后点 开始
继续执行任务停止
,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务本来,第一个版本是微信小程序版的,当时审核直接被打回,好像时个人无法发布包含用户系统的小程序。
那么由此,我的程序做成 app 也将无法在国内的应用市场上架了,所以只能暂缓flutter 开发 App
UWP 是由于几年前就注册了开发者的,但那时没有发布什么app,而且本人业余时间也对 c# 比较熟悉,所以就拿这个程序作为实验版。
UWP 版有想法兼容已经淘汰的Win10 Mobile,因为手上那个还有几个 lumia 手机
2020-04-24 00:13:13
今天把 node.js
升到 14.0.0
,发现使用 npm update
安装不了 node-sass
不行,node-sass
依然是从GitHub
下不下来。
这时想到是不是网络问题导致访问不了 github
发现还是不行,点击进去发现没有最新的node-sass
这是回过头看第一种方法,发现是 node-sass
还没有发布适配 node.js 14.0.0
的包
到 github
上找 node-sass
最新的有一个 适配的 win32-x64-83_binding.node
下载到本地
这样就行了
当然 node-sass
里面有个判断 node.js
版本的报错需要删除
这是临时解决方法,还是等 node-sass
更新吧
gulp-sass 替换方法
2020-04-16 20:44:41
我的程序默认语言为 zh-CN
则新建文件夹 Strings\zh-CN
再添加新建项 资源文件(.resw)
新建文件 Resources.resw
例如要设置 TextBlock
的 Text
TextBlock
添加属性 x:Uid
Resources.resw
添加一行 名称 | 值 |
---|---|
OnLabel.Text | 开 |
名称的组成为 {x:Uid}.{属性名}
如果要给一个控件设置多个属性
名称 | 值 |
---|---|
OnLabel.Text | 开 |
OnLabel.Content | 开 |
注意:
x:Uid
属性是没法在代码中获取到的先添加在一个类上增加一个公开方法: 例如 类名 AppResource
#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
在 Resources.resw
添加一行
名称 | 值 |
---|---|
appName | 开始 |
使用
注意:
AppResource::GetString("OnLabel.Text")
或 AppResource::GetString("OnLabel")
这种方式是不行,也就是 xaml
上使用的名称没法在代码中使用,只能多增加一条OnLabel.Text
和 OnLabel
添加是冲突的通过 vs 的扩展添加 Multilingual App Toolkit
能自动生成其他语言的翻译
2020-04-16 20:44:13
需要一个修改头像的功能,
这是第一种,直接选择文件进行上传
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());
这里是进行具体上传的地方
using Windows.Web.Http;
var httpClient = new HttpClient();
var form = new HttpMultipartFormDataContent();
form.Add(stream, "file", "avatar.png");
await httpClient.PostAsync(new Uri(), form);
file
为POST表单中文件的键
avatar.png
为文件名
注意:
HttpMultipartFormDataContent
就不要设置 请求头中 Content-Type
,不然服务器端无法解析表单内容这是使用 Microsoft.Toolkit.Uwp.UI.Controls
里面的裁剪控件 ImageCropper
2020-03-30 21:26:31
使用此程序必须先登录,支持微信登录
+
进入所有任务+
进入任务添加任务页面任务名
任务说明
每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止
√
进行保存+
进入所有任务编辑
按钮 进入编辑模式添加到今日任务
就会把选中的任务添加到今日任务,可以连续点击,添加多次任务完成
按钮就会退出编辑模式结束任务
可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了首页点击 一个任务,进入任务执行页面
点击底部的 开始
按钮,开始任务
这时可以把手机放一边了,做事
等时间归 0,手机会震动,表示此轮任务执行完一次,可以停下来休息3-5分钟
在执行时,可以点击 暂停
,表示有其他事打扰,需要暂停一下,处理完后点 开始
继续执行任务
在执行时,可以点击 停止
,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务
2020-03-30 21:24:49
问题及解决方法
原因 $_SERVER['HTTP_CONTENT_TYPE']
没有值
加上一个判断 允许 $_SERVER['HTTP_CONTENT_TYPE]
$_SERVER['CONTENT_TYPE']
两种获取方式
原因 apache 下重写 认是不能获取Authorization信息的
可以使用 getallheaders()
获取 不要从 $_SERVER 上提取,上一个问题也可以用此方法
注意 getallheaders()
获取的是原始请求头 键未大写 ,-
也未处理为 _
示例 ['Content-Type' => 'application/json;charset=utf-8']
用户信息 返回的token有问题
服务端未进行 birthday 保存
onLoad
方法只第一执行了, 需要把执行的内容放到 onShow
中,这样每次显示页面都会刷新
原因 服务器时间与本地时间对不上,改为通过已完成时间重新计算开始时间
enablePullDownRefresh: true
未设置
wx.showLoading
与 wx.showToast
是冲突的,必须先关闭 Loading
2020-03-30 21:24:13
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()
}
})
});
}
/**
* 上传文件
* @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()
}
})
});
}
/**
* 封装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);
}
/**
* 封装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,
});
}
/**
* 封装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);
}
/**
* 删除请求
* @param url
* @param loading 是否显示加载中
*/
export function deleteRequest<T>(url: string, option?: IRequestOption): Promise<T> {
return request<T>('DELETE', {
url,
}, option);
}
自动添加登录令牌
const token = wx.getStorageSync(TOKEN_KEY)
if (token) {
// 加入登录令牌
headers.Authorization = 'Bearer ' + token;
}
清除token 并跳转登录页面,但是一些页面又不想自动跳转到登录页,所以应该能传入设置
if (statusCode === 401) {
app && app.setToken();
wx.navigateTo({
url: '/pages/member/login'
});
}
登录推出的处理
相应失败的信息弹窗
请求加载进度的显示
有些地方不需要加载进度条,比如下拉加载更多,静默加载更好,不影响阅读
2020-03-30 21:23:19
有标题
有说明
有每次执行的时间
支持批量终止
从所有任务页面点击编辑进入编辑模式,可以多选添加到今日任务,可以连续点击添加多次
开始:开始任务,并进行计时
暂停:暂停任务,停止计时
终止:中途有事要终止此次任务,计时重置,本次任务恢复
支持小程序快捷登录
小程序里只支持小程序的绑定,对其他绑定无法进行操作
只支持查看列表,无法进入查看
支持扫码登录网页端
文章的搜索
文章的列表
文章的详情
支持反馈留言
震动开启的设置
屏幕常亮的设置
2020-03-26 05:50:29
用 typescript
开发微信小程序时,每次查看效果都很麻烦,编译过程太长了,怎么只编译当前编辑的文件呢?
要把当前文件的路径传给 gulp
要在当前工作区增加一个设置
.vscode\settings.json
以下一些语言必须用以下方式才有用,用拓展名是没有用的
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
Code Runner 有右键菜单项 Run Code
就是执行当前文件的
这样在 ts 文件就会运行命令 gulp build --file=
$fullFileName
表示文件的绝对路径
在 guipfile.js
中增加 build
任务
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'));
});
右键执行 ts 测试以下
如果我使用一个方法既可以编译所有的,又可以编译一个文件,该怎么做
修改guipfile.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/')));
});
这样执行 gulp build
即可编译所有 ts 文件
右键 Run Code
只编译当前 ts 文件
再也不用等很长时间了
2020-03-24 22:20:25
增加一个 application 类
定义类的方法
#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
实现类的方法,并实现初始化
#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;
}
进行类的初始化
2020-03-24 00:16:40
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
};
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
};
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
};
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
};
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
};
2020-03-24 00:15:37
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
打印字符串
2020-03-23 18:23:01
在 github 上提交了一个 pull request,在作者进行操作前,发现自己某处错了,进行了修改。
这时是关闭这条 pull request 重新发一条,还是有什么操作可以覆盖这次发送的 pull request?
push 更新那个分支就行,pull request只和分支名绑定。
直接 push 就会自动追加到到 PR 后面。当然,如果你不希保留旧的 commit 记录,还可以选择本地 git reset 之后 push -f 强行覆盖掉你远程的commit,PR会一并更新。
修改代码
强制提交
如何把已经更新到最新的代码保持跟自己Fork下来的仓库代码保持一致
这是使用 git remote -v
则可以查看到已经更新了
在 Changes
下,右击想要操作的文件,点击菜单 Discard changes
即可,此操作会删掉对此文件的所有的修改
在 History
下, 右击想要撤回的 commit
,
Amend commit
是修改本次提交的 描述信息
Undo commit
取消本次提交,可以重新修改文件和描述信息,不会留下记录
Revert changes in commit
撤回本次 commit
, 实际上在反向提交一次 commit
删除了上次修改的内容,上次 commit
修改的文件就恢复初始状态, 会丢失上次修改的内容,但是会留下记录
只能 Revert changes in commit
, 删除了上次修改的内容,会丢失上次修改的内容,同时会留下记录
2020-03-22 21:31:44
命令行进入 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext
这里zodream代表你的php扩展名
Visual Studio 2019
继续但无需代码
文件
-> 新建
-> 从现有代码创建项目
Visual C++
下一步D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\zodream
),项目名称 phpzodream
,下一步Release
x64
属性
-> 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
属性
-> C/C++
-> 预处理器
-> 预处理器定义
-> 编辑
加入以下变量(其中ZODREAM
替换成php扩展名
)
如果为开启线程安全 则加上
LINK 1561: 必须定义入口点
属性
-> 常规
-> 配置类型
选择 动态库(.dll)
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.h
为 zodream_arginfo.h
即可
或者在 ext_skel.php
文件中的 copy_sources
方法 加上 'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h'
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' // 加上这行就会自动生成
];
}
LNK2019 无法解析的外部符号 __imp_zend_strpprintf
属性
-> 连接器
-> 输入
-> 附加依赖项
-> 编辑
加入一个php.lib
,此文件在正式发布的PHP程序中 例如 D:\zodream\php\php-7.4-nts\dev\php7.lib
E0020 未定义标识符 "zif_test2"
因为定义的方式 PHP_FUNCTION(zodream_test2)
但是引入时用的是 PHP_FE(test2, arginfo_test2)
改为
或者改 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);
}
把生成的 phpzodream.dll
复制到正式环境 ext
文件夹下,
修改 php.ini
引入插件
重启 iis
按【上一章】编译 php 即可,不需要在ini配置,直接使用即可
在php 代码加入
查看效果
2020-03-22 21:30:37
在右边的“Clone or download
”点击,选择下方的“Download ZIP
”
此时最新版本是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
”
选择 “社区
” 下的 “免费下载
” 进行下载,
将 php-sdk-binary-tools-master.zip
解压到 D:\zodream\php-sdk-binary-tools
按住shift在编译目录内点击右键,选择“在此处打开Powershell窗口
”;
执行”.\phpsdk-vs16-x64.bat
”,成功后提示符从“>
”变成“$
”;
执行“phpsdk_buildtree phpdev
”,成功后目录中会多一个“phpdev
”目录,命令行的目录自动切换到“phpdev/vc16/x64
”;
在“phpdev/vc16/x64
”目录下新建php-src
文件夹,将PHP源码复制到此目录;
切换到php-src
目录(cd php-src
),执行“phpsdk_deps -u
”;
在“phpdev/vc16/x64
”下建立pecl
目录(与PHP源码目录同级),此目录放拓展的源码。如果为自己开发的拓展,请参考【下一章】。
D:\zodream\php-sdk-binary-toolsphpdev\vc16\x64\pecl
);如果为自己开发的拓展,则不需要这一步,下一步会自动发现这些拓展,直接配置编译选项则可以
D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src
)执行”buildconf
”;configure --help
查看能够使用的配置选项,包括你自己的插件什么的configure –一些选项
”命令配置编译选项,例如”configure –-disable-all –-enable-cli –-enable-cgi –-enable-zlib –-enable-session –-without-gd -–with-bz2 –-enable-yourext
”;编译 php
我的编译php 参数
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
编译 PECL 拓展, 例如: apcu
nmake
命令编译PHP及拓展, nmake snap
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
”选项。
2020-03-15 18:34:02
/
如果是在最前面,代表从根节点选取,否则选择某节点下的某个节点.只查询子一辈的节点
/html 查询到一个结果
/div 查询到0个结果,因为根节点以下只有一个html子节点
/html/body 查询到1个结果
//
查询所有子孙节点
//head/script
//div
./
选取当前节点
../
选取当前节点的父节点
@
选取属性
//div[@id] 选择所有带有id属性的div元素
运算符/特殊字符 | 说明 |
---|---|
/ | 此路径运算符出现在模式开头时,表示应从根节点选择。 |
// | 从当前节点开始递归下降,此路径运算符出现在模式开头时,表示应从根节点递归下降。 |
. | 当前上下文。 |
.. | 当前上下文节点父级。 |
* | 通配符;选择所有元素节点与元素名无关。(不包括文本,注释,指令等节点,如果也要包含这些节点请用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属性的元素
//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
2020-03-13 02:52:35
fontawesome.ttf
放到 Fonts
文件夹中,设置文件属性“如果较新则复制”<FontFamily x:Key="FontAwesome">
pack://application:,,,/Fonts/#FontAwesome
</FontFamily>
或者直接合并234步,按照 /命名空间;component/[路径]#[字体名称]
这个格式写
将 #字体名称
改成 #iconfont
即可
2020-03-12 01:16:51
用户登录时勾选 记住我
,服务器生成随机token 加用户id 组成字符串永久保存到浏览器cookie 中,下一次从浏览器获取并提取token和id 获取用户登录即可
用户表需要两个关键字段 id
和 remember_token
id 是查询用户的主键 remember_token 为随机生成的字符串
需要方法
2020-03-12 01:00:32
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 当评论功能被关闭时 显示
2020-03-12 00:59:52
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()
例如
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>',
) );
2020-03-12 00:56:47
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
例如
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() 取消已经排列的样式表
2020-03-08 06:22:16
__() ,翻译,如果不能就返回原始文本
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 异步调用
2020-02-27 19:28:27
进入 http://www.mingw.org 下载 mingw-get-setup.exe 并进行安装
在 MinGW Installation Manager 中选择 mingw32-gcc-g++-bin 点击菜单 Installation -> Apply Changes -> Apply
搜索 编辑系统环境变量 在 Path 中添加 MinGW 的bin文件夹, 例如 C:\MinGW\bin
在 cmd 中输入 gcc -v
查看是否安装成功
进入 https://www.msys2.org/ 下载 msys2-x86_64
修改pacman源,参考 https://mirrors.tuna.tsinghua.edu.cn/help/msys2/
pacman基本命令
下载make
安装gcc、g++编译器
Code Runner
配置
{
"code-runner.executorMap": {
"c": "cd $dir && gcc $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt"
}
}
C/C++
2019-12-22 22:31:10
从一段html 中截取指定长度的内容,
要求:
简单版代码
/**
* 截取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;
}
此代码存在的问题
2019-12-22 22:27:10
Mysql 5.5 版本开始, InnoDB是默认的表存储引擎, 其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU
基于传统的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 占用差不多、内存占用不高、机械硬盘写入跑满
CREATE TABLE `test_log` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`created_at` INT(10) NULL,
PRIMARY KEY (`id`));
|引擎类型|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倍
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();
思考方向:
只插入,不修改
实在不会选,那么直接按项目的大小来选择
2019-12-09 18:50:45
具体配置请参考 【nginx 子目录匹配不同地方的文件夹】
必须提升权限
nginx.conf
这样访问 vsftp 中的文件 html 或 js 是正常的
但访问 php 文件就会报 File not found
404 错误
开启 nginx 的 error_log 错误日志 会有一条这样的日志
此时应该提升 php-fpm 的权限
php-fpm.conf
但是启动 php-fpm 启动不了
不允许使用 root
权限,但她有一个启动参数 -R
允许使用 root
这样还是不行
找到 /etc/init.d/php-fpm
找到 start()
方法, 加上 -R
就行了
重启 /etc/init.d/php-fpm restart
再次访问php文件,正常了
2019-12-05 05:29:39
一开始使用soap 连接是报 Couldn't load from 'xxxx' : Premature end of data in tag html line
发现是参数设的不对,SoapClient
第一个参数使用的是正式请求网址,SoapHeader
第一个参数设的namespace
,不能使用默认的 http://tempuri.org/
, 不然 header
和 body
的namespace
相同 ,header
就不会生成加入到xml请求内容中。
__soapCall
的第二个参数传递的是方法的所有参数,类似于 ...args
, 所以必须用数组包起来 [参数1, 参数2, ...]
$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);
但是老是报错 Could not connect to host
按照搜到的方法改了 php.ini
发现还不行,再次修改代码
$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);
还是不行,有尝试过使用 fsockopen
发现连接直接超时
然后用 postman
测试发现需要加上 Content-Type: text/xml;charset=utf-8
服务端才能接收,否则响应 服务器无法为请求提供服务,因为不支持该媒体类型。
, 最后没办法只能修改请求方法
$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;
}
}
虽然可以直接使用curl
自己写,但拼接soap
的xml
有点麻烦,转化响应也麻烦,直接使用soap
插件就不麻烦了。
2019-12-01 06:49:29
先进行首页组件化测试
页面增加静态化设置,是否静态化,允许设置更新时间,对缓存页面设置过期时间,这样就不需要访问数据库查询是否过期。
组件内容更新,通过事件通知,更新时通过组件反推相关的页面,进行页面更新。
2019-11-16 00:57:07
在一些页面,比如滚动加载的页面默认无法后退保存之前的滚动位置。但需要这个功能该怎么做?
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
在路由中需加上 keepAlive: true
在需要的页面 .vue 里加上事件,mounted 事件只在新进入时触发,或离开页面后缓存页面时触发(这时是获取不到任何页面元素的)
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;
});
},
注意:以上事件只能放到页面上面,不能放到页面上的部件里
2019-11-02 02:41:24
/ 对应 /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;
}
}
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;
2019-10-26 17:49:33
右键左下角 徽标 ,点击 Windows PowerShell(管理员)
查看已保存的所有wifi
基本显示
所有用户配置文件 : zodream
后面的就是wifi 名(zodream 即为wifi名替换下面
)
提示
接口配置文件“zodream”已成功保存在文件“.\WLAN-zodream.xml”中。
输入
找到 passPhrase
往下两行就是
这就是密码了
2019-10-16 21:53:12
参数: 订单id 支付方式 币种?
服务端生成表单html或调起参数或跳转链接
处理通知,完成支付
最好不要直接使用订单号去支付,因为如果订单金额发生变动或支付金额受实时汇率影响的话,会导致第三方支付不成功
2019-10-15 19:47:11
自定义一个 友情链接 的tag
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;
}
}
}
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());
}
}
}
先在 _ViewImports.cshtml
添加以下代码
netdream
是本项目的程序集名称,并不是命名空间没看源码,大致猜测这个是动态引用的即使用时临时检索 程序的dll 引入tagHelper
右键项目-> 属性 -> 应用程序 -> 程序集名称(N):
然后在 Home.cshtml
使用
2019-10-11 00:01:04
由于使用了 typescript
编写,所以引入了 TSLint
作为代码规范检查工具,在编写过程中并没有太注重代码的规范,导致每次编译的代码提示都刷刷的好几页。
在基本功能完成后进行代码优化工作。
parseInt
第二个参数必填 10 表示转化为十进制。页面获取传值 this.$route.query
或 this.$route.params
返回多个类型的值,如果需要string
则使用 as string
做类型转化声明,实际在js 代码中不做处理。如果需要 number
则需要使用 parseInt(this.$route.query.a as string, 10)
进行转化public
、private
修饰符const
2019-10-11 00:00:32
此功能主要是用于用户身份证图片上传,审核是后台人工进行审核
此功能分为三步:
2019-10-10 23:59:42
2019-10-10 23:59:12
2019-10-10 23:57:09
2019-10-10 23:56:34
本页面相对简单,一个注意登录与未登录用户头像的显示
可以弄一个页面滑动顶部保留头像的功能
2019-10-02 22:58:11
2019-10-02 22:57:39
页面滚动时有相应的头部导航切换
2019-09-30 20:05:33
基本需求:
使用手机号和密码登录,
邮箱的话就只能密码登录,当然也可以邮箱加邮件验证码登录,不过让用户使用比短信验证码麻烦,不推荐使用
邮箱注册,填邮箱、用户名、密码等基本信息就行了,可以价格邮箱验证功能,发送验证链接邮件,点击链接就验证成功
手机号注册,就手机号、用户名、密码、短信验证码
第一步填写邮箱,判断是否注册,然后发送找回链接到邮箱中,那这样就跳转到pc 了,没意义了,改为邮件验证码
大致流程:
2019-09-30 19:57:34
搜索页面可以分两个状态
当搜索条件为空进入显示此页面:
涉及接口:
技术相关:
2019-09-30 19:56:59
本次只做一个简单的首页,共分为6部分:
mint-ui
中的轮播组件涉及到的 API
接口:
技术相关
公共底部导航栏组件
本页面分三部分:
右侧分为
涉及到的接口:
技术相关
公共底部导航栏组件
2019-09-29 18:36:57
appid
secret
参数名 | 字段类型 | 说明 |
---|---|---|
appid | string | |
timestamp | string | 当前时间例如:2019-05-01 01:01:01 |
sign | string | 签名 |
Authorization: Bearer token
md5 签名
所有错误提示响应状态码都不是200
响应状态码401 时表示登录过期,需重新登录
2019-09-29 18:34:01
本次使用 axios 作为网络请求底层
post 数据为json格式
接受json格式的数据
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)
})
})
}
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)
})
})
}
2019-09-29 18:34:00
主要功能,根据页面自动判断选中一项
menus: IMenu[]
$route.name
判断当前的路由,选中菜单大致分为六个状态
下拉刷新
释放刷新
停止刷新
刷新中
加载更多
加载中
加载完成
1s 后自动恢复无
状态首先确定输入和输出,输入字符串就要输出字符串,输入Date
就要输出Date
。
既然要输出字符串,那么就要支持格式化,就要有输入格式化定义 format
一个日期,应该是 年月日时分秒, 那个 年月日 是必须的,时分秒可选 通过 format
判断就行了
还需要有一个选择范围 min
max
多语言?暂不考虑
还需要一个触发显示框,不需要外部代码手动触发,
<div @click="showCalendar" class="datepicker__input-container">
<slot></slot>
</div>
这样就好了,使用方法
<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>
import DatePicker from '@/components/DatePicker.vue';
@Component({
components: {
DatePicker,
},
})
为什么不用多个select
联动?
select
占地方而且获取不方便,因为地区是不确定几级的,而且我只要最后的选择id不确定几级,那么就用一个 v-for
就行了
要通过上下滑动进行选择,加上滑动事件 @touchstart='touchStart' @touchmove='touchMove'
2019-09-28 05:56:07
src
api 定义实现api接口
model.ts 定义接口数据结构
assets 样式及图片资源
components 公共基本部件
pages 页面
Home
Child 本页面用的部件
pipes 自定义过滤器
router 注册的页面路由
store 保存的状态及跨页面传递的数据
utils 辅助方法,http 请求
;
,
结束具体typescript 规范请查看 tslint.json
先定义相应数据模型,基本的模型
// 分页数据
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;
}
接着封装一个http 请求,包括注入appid 及sign,设置请求头,处理一些全局响应包括token 过期
暴露几个常用的请求方式 get post delete 最后返回一个 Promise<T>
[√] 首页
[√] 分类页
[√] 搜索页
[√] 商品详情页
[√] 商品评论显示页
[√] 购物车页
[√] 结算页
[√] 支付页
[√] 个人中心页
[√] 浏览记录页
[√] 个人账户页(包含提现、充值弹窗)
[√] 个人账户记录页
[√] 银行卡页
[√] 银行卡绑定页
[√] 发票页
[√] 发票申请页
[√] 发票抬头页
[√] 发票抬头编辑页
[√] 发票记录页
[√] 登录注册页
[√] 订单列表页
[√] 订单详情页
[√] 商品收藏页
[√] 消息页
[√] 账号关联页
[√] 个人信息页
[√] 账户注销页
[√] 登陆设备管理页
[√] 修改密码页
[√] 实名认证页
[√] 收货地址页
[√] 收货地址编辑页
[√] 评论商品页
[√] 发表评论页
[√] 退换货页
[√] 退换货申请页
[√] 文章列表页
[√] 文章分类页
[√] 文章详情页
[√] 推荐页
[√] 推荐规则页
[√] 推荐订单页
[√] 推荐会员页
[√] 推荐二维码页
[√] 优惠券领取页
[√] 我的优惠券页
[√] 签到页
2019-09-28 05:46:28
Typescript
本人最为熟悉,特别喜欢她的强类型,加入 vscode 这个开发工具的智能提示,简直不要太方便。
scss
喜欢她的嵌套
Paste JSON as Code 根据 json 生成 ts 代码
Vetur vue 的语言服务
Vue VSCode Snippets vue 代码块
选择 Manually select features
选中(上下键+空格键) Babel
TypeScript
Router
Vuex
Linter / Formatter
输入三次 y
直接默认回车即可
浏览器打开 http://localhost:8080
2019-09-28 05:45:02
本项目大致分为 vue、小程序、UWP 版、Flutter版,其他版本暂无时间开发,如果有好的 kotlin、swift 框架推荐请留言。
目前已基本完成 vue 及小程序版,正在开发UWP版及Flutter版,本教程分四部分预计完成时间为三个月,特别纠结,特别时网络请求这一块,必须设计好才能继续下去。
本人主要从事于电商开发及电商系统二次开发,本项目为工作经验的总结前端篇。
2019-07-31 06:27:17
今天使用`google search console` 查看网站收录情况发现有很多链接处于 `发现未收录`状态,才想起用工具进行并发测试。
AB (ApacheBench) 是 Apache 自带的一款功能强大的测试工具,可以快速测试基于 HTTP 协议所有 Web 页面的最大负载压力。
下载地址www.apachelounge.com Apache 2.4.39 Win64
加压下载文件到指定文件夹
打开 PowerShell ,通过 cd 进入 bin 目录
主要用到 ab.exe 这个程序
-n 总共发起几次请求
-c 并发数
-t 总共执行时间,到时结束所有请求,
-s 最大超时时间,默认30秒
-b TCP 请求发送和接受的字节数
-p POST发送文件
-u PUT发送文件
-T 设置请求头内容类型
-k keep-alive保持连接
最后输入完整网址 带http/https
例如
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)
使用此命令,发现出现 ...apr_pollset_poll: The timeout specified has expired (70007)
这样的错误,大致意思是请求超时了,可以增加 -k
保持连接
2019-07-12 21:44:09
注册npm
账号
新建文件夹 test
在vscode
中打开
ctrl
+ ` 打开 cmd 模式下登录
输入账号密码
输入 npm init
输入项目名及说明生成 package.json
文件
新建文件夹 src
src
下添加 index.ts
根目录添加 gulpfile.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/'));
});
根目录添加 tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"declaration": true,
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts"
]
}
修改 package.json
将入口指向编译生成的 index.js
文件
添加
只需要 dist
文件夹下的文件就行了,其他源码就不用发布了
src/index.ts
执行 gulp
命令
在 dist
下会自动生成 index.js
和 index.d.ts
两文件
发布成功
在其他项目
安装此包
使用
或
添加 bin
文件夹
新建文件 cli.js
(文件名随意)
然后在 package.json
配置
"bin": {
"command-name": "./bin/cli.js" //告诉package.json,我的bin叫 command-name ,它可执行的文件路径是bin/cli.js
}
bin 命令必须全局安装才能用
或
2019-06-20 23:07:32
当 struct 结构中带有 DeletedAt 属性,在查询的时候就会默认加上 deleted_at IS NULL
,但是我的数据表 deleted_at
的默认值是 0,因此需要使用 , 去除默认查询加上自己的查询
可以定义 TableName 方法自定义表名
使用 Preload("User") 进行关联查询
type Blog struct {
ID uint
User User
UserID int
}
var data []Blog
db.Preload("User").Find(&data)
使用 Related
进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 FOREIGNKEY 去查找 User, 如果没定义, 则调用时需要指定
使用 Association
进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 AssociationForeignKey 去查找 User, 必须定义
2019-06-19 23:42:56
重要:四边黑线表示可以伸缩的区域
Create 9-Patch file...
,命名默认就行Zoom
为编辑窗口中的放大比例(无法缩小),Patch scale
为预览窗口中图片放大比例,Show lock
鼠标放到原图上,会显示红色斜线部分。表示点9图锁定的区域; Show content
:是预览窗口蓝色部分,蓝色表示可以填充内容,白色便是不可填充内容,移动原图中右边和下边的修改可填充内容的区域,规则如上; Show patches
:显示编辑窗口中可以缩放的区域,绿色和紫色部分,原图颜色为不伸缩部分;Show bad patches
:显示原图中不规范的缩放区域。比如带弧度中部分是不应该缩放的,如下图中红线标记的区域。遇到下面情况,需要向右稍微移动上边的黑线,不标记弧度部分,红线就会取消。2019-06-18 05:21:07
使用 yield
输出内容
先在公共模版声明 此处使用 header
的内容
再在页面中定义 命名规则 相对于模板根目录的相对路径 -header
文件 blog/index.html
{{ define "blog/index-header"}}
<link type="text/css" href="/assets/css/blog.css" rel="stylesheet" media="all">
{{ end }}
使用
rv := router.NewRoutePathReverser(app, router.WithHost("zodream.cn:443"))
tmpl.AddFunc("url", rv.URL)
使用
2019-06-03 20:10:50
图标全部使用图片,在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 放入样式中
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\')';
}
将文件中的标签选择器进行相应转化
created()
转化为 onLoad
$route
全部转化为 onLoad(query)
的参数
修改属性值,只能通过 setData
修改
将 axios
改为 wx.request
将所有的store 全部放到 app.ts 中 globalData
在vue中可以通过$refs 进行排异(只有一个左滑状态,其他自动恢复原状),小程序中可以通过新增自定义组件加入关联关系进行排异
启用自定义组件,使用自带的 enablePullDownRefresh: true
使用自带的地区选择picker
,微信的网络请求有数据大小限制,自定义组件实现无法一次传入所有省市区,
自带地区选择只能获取到省市区名称,因此需要后台接口进行修改
自定义实现监听属性变化
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;
}
})
}
总的来说,转化过程没有太大的技术难度,就是每个页面都得改,比较繁琐。
2019-04-21 08:18:39
windows 环境 git 下修改文件名大小写无效,虽然不影响读取,但是在一些(比如小程序路径)就无法识别
2019-04-16 02:34:53
一些公司会把一些关键数据进行保密,而网站进行保密(对用户显示,对爬虫隐藏)一种方式就是自定义的字体文件
python
从一个已有的字体文件提取需要加密的文字字形,更改字形索引,保存为新的字体文件,使用新的文字索引使用
获取字体文件,根据索引获取字形,再根据字形获取真正的文字(前提必须有一个足够丰富的字形库)
反爬使用指定义的字形(手动修改字形)
反反爬使用类似图像识别进行字形识别
自动生成 节选的字体文件ttf wof svg 及css html使用例子
2019-04-06 06:50:02
'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)
}
});
};
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】
正常返回
返回报错(会中断后续所有文件任务)
中断本次任务,继续操作其他文件
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配置生成
@WxJson({
usingComponents: {
MenuLargeItem: "/components/MenuLargeItem/index",
MenuItem: "/components/MenuItem/index"
},
navigationBarTitleText: "个人中心",
navigationBarBackgroundColor: "#05a6b1",
navigationBarTextStyle: "white"
})
自动合并页面相关的json文件
支持自动合并 methods
lifetimes
pageLifetimes
, 如果已有 属性会自动合并
methods @WxMethod
lifetimes @WxLifeTime
pageLifetimes @WxPageLifeTime
自定义部件自动合并方法到methods
属性中
最终生成
index.vue
<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>
index.wxml
index.wxss
index.js
2019-04-04 06:13:15
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']
;
2019-04-01 17:37:31
(纯函数)拿到下一个State和之前的State来计算一个新的State。 初始化更改State中的值,并返回State ,接受两个参数 ,第一个为旧的state,第二个为action 根据type 更改state并返回
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
可以使用combineReducers
合并
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
Store 就是把它们联系到一起的对象
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();
描述State的改变,必须是一个包含type键的对象
可以封装一个方法来生成action
2019-04-01 17:36:34
文件夹位置 Domain\Entities
主要内容:包含数据库名,表名,主键、字段名、字段验证
保存最原始数据,直接读取的也是最原始数据
文件夹位置 Domain\Models
主要内容:关联表、追加字段、隐藏显示字段
值转化,
文件夹位置 Domain\Repositories
主要内容:读取方法,更新实体
包含获取哪些值,返回数据实体
这样文件变得更复杂、传出来的值都会固定、适应不同接口场景
到底在哪一层操作关联数据库?
<?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' => '昵称',
];
}
}
<?php
namespace Domain\Models;
use Domain\Entities\User as UserEntity;
class User extends UserEntity {
protected $hidden = ['password'];
}
<?php
namespace Domain\Repositories;
use Domain\Models\User;
class UserRepository {
public function get($id) {
return
}
}
2019-03-19 00:14:18
SVN 使用过程中遇到的问题(主要是命令行还不习惯用),这里是把正确的方法记录一下
拉取SVN 更新时遇到
Skipped 'xx' -- Node remains in conflict
svn revert --depth=infinity index.html
index.html
为冲突的文件名
2019-03-15 19:09:52
刚开机,启动vue 程序,发现所有请求都被浏览器禁止了,提示禁止跨域
。
明明昨天都正常的,而且服务端程序也启用的允许跨域响应的,再一查看现在的响应头,不对劲,响应头不对了,而且响应服务也变成了 ASP.NET
,再一调试服务端,发现请求根本没到程序里来,被iis拦截了
打开iis管理器,选中根节点,进入处理程序映射,点击“查看经过排序的列表”,把PHP(我用的是php环境)上移到`OPTIONSVerbHandler`前面即可,重启iis
2019-03-15 00:46:05
支持 :index 为数值或字符串
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;
},
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;
}
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);
}
通过父控件记录所有子控件引用,并标记子空间顺序,在父控件调用子元素复原方法或子控件内部调用
子控件在初始化的时刻,生成一个自增唯一标识,然后根据这个挂载到父控件或全局属性上
<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>
<div class="swipe-box">
<SwipeRow v-for="(item, index) in items" :key="index" @remove="tapRemove(item)" :index="index" ref="swiperow">
<div>
这是内容
</div>
</SwipeRow>
</div>
.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%;
}
}
}
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
异步获取并设置
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);
});
},
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;
2019-02-19 04:19:40
严重性 代码 说明 项目 文件 行 禁止显示状态 错误 DEP0700: 应用程序注册失败。[0x80073CFB] 另一个用户已安装此应用的未打包版本。当前用户无法将该版本替换为打包版本。冲突程序包为 22bdd128-9c57-4102-b7a4-53d890e28a07,由 CN=ZoDream 发布。 ZoDream
已管理员的身份运行 PowerShell
输入命令
2019-01-11 07:04:53
最近准备把本站源码从PHP 迁移到 .NET Core。遇到一个首要问题就是本站是分模块开发的,我也想过分成多个项目来做,但又涉及本站的基础框架,必须所有模块都能随着基础升级,我怕麻烦就整合到一起了。
Areas 是 ASP.NET MVC 功能,在官方文档有介绍【Areas in ASP.NET Core】
在 Startup.cs
中添加配置
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?}");
});
}
新建文件夹 Areas/Blog/Controllers
新建控制器 HomeController.cs
[Area("Blog")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
文件夹名可以用其他,关键 使用 Area
声明 区域,使路由能指向这里,改了文件夹也要记得改视图的文字
注册视图位置
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");
});
默认会自动从这些位置找
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
HtmlHelper 语法
TagHelper 语法
2019-01-11 06:29:50
public class TestMiddleware
{
private readonly RequestDelegate _next;
public TestMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
// TO DO
return _next(httpContext);
}
}
public static class TestMiddlewareExtensions
{
public static IApplicationBuilder UseTestMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TestMiddleware>();
}
}
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//app.Use<TestMiddleware>();
app.UseTestMiddleware();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (context, next) =>
{
// TO DO
await next();
});
}
右键项目》添加》新建项》已安装》ASP.NET Core》WEB》ASP.NET》中间件类
2019-01-09 06:24:25
文件位置:默认是在 httpd.conf 中,xampp 是在 httpd-default.conf 中
# 找到ServerTokens和ServerSignature并修改为:
ServerTokens Prod
ServerSignature off
# 如果没有找到ServerTokens和ServerSignature可以在最后一行添加
PS: 响应头还是会有 Server: Apache
,只是去除了版本号
文件位置:php.ini
2019-01-06 05:36:29
这篇教程只是给前端看的,简单介绍如何简单实现网页动起来效果。
<link rel="stylesheet" href="animate.min.css">
<script src="wow.min.js"></script>
<style>
.wow {
visibility: hidden;
}
</style>
<script>
new WOW().init();
</script>
说明:class wow
是wow.js 默认的标记,加上了这个,那这个元素就会动起来,必须设置样式为不可见,才能起到滚动到视窗出现突然出现的效果。
<section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s"></section>
说明:
class wow
是wow.js 默认的标记,可以修改。
class slideInLeft
是 Animate.css 的其中一种动画绑定的class。
data-wow-duration
是指动画执行时间,可以不设,Animate.css 自带默认
data-wow-delay
是指延迟多久执行动画,可以不设,Animate.css 自带默认
<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>
2019-01-06 05:03:27
$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);
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
2019-01-06 05:02:57
获取所有活动
判断是否属于此商品
获取所有活动
判断是否属于此商品
获取所有活动
根据购物车中商品能参加的活动
判断用户等级
判断参加活动的金额
判断是否已存在购物车
判断是否参加过此活动
加入活动
获取购物车中的活动
判断是否过期
根据购物车商品数量改删,判断金额是否还符合
获取选择的活动
判断是否过期
判断是否含有触发活动的商品
判断选择的商品的金额
根据最近活动过期时间缓存
缓存所有的数据,比如活动匹配的商品id,活动的等级,活动的金额,活动的时间
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 调用不同语言包实现。
实现原理,通过中间语句去语言包中匹配,然后输出匹配到的目标语言语句。
这些内容是通过后台添加的内容,
2018-12-29 06:23:47
安装 PHPMyAdmin 4.8 以后,使用浏览器登录不上,报错
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攻击就是利用合理的服务请求(发送大量的SYN包)来占用过多的服务资源,从而使合法用户无法得到服务的响应。
分布式拒绝服务攻击采取的攻击手段就是分布式的。
按照TCP/IP协议的层次可将DDOS攻击分为基于ARP的攻击、基于ICMP的攻击、基于IP的攻击、基于UDP的攻击、基于TCP的攻击和基于应用层的攻击。
2018-12-28 04:26:35
jsonrpc
为固定参数
method
需要调用的方法
params
方法值,非必须
id
可以是数值或字符串,与响应相对应
[
{"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"}
]
[
{"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"}
]
根据 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 适用于对外通信,例如与浏览器,APP 等
2018-12-25 07:16:45
同一个项目,在笔记本上打开是正常的,到了台式机上一直在初始化 typscript
语言服务,
输出显示
[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
"typescript.tsserver.log": "verbose",
.ts视频文件
也被识别进去tsconfig.json
加入排除文件夹 "exclude": []
2018-12-24 00:19:47
$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();
$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();
2018-12-24 00:15:46
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;
2018-12-22 07:26:47
获取页面元素
虚拟缓存(不显示在页面,主要是用于缓存部分场景)
应用缓存
绘制文字
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);
绘制图片
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('加载失败,请重试');
};
}
绘制游戏画面,先把背景、元素都分成不同的缓存,设定刷新时间,并组成不同的场景
2018-12-21 05:41:52
接受两个参数, 第一个参数时主元素的属性名,可以跟转化类型,第二个是默认值,
例如:
.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);
}
2018-12-20 05:25:15
Gulp 一般使用一个默认的任务,如果需要处理不同的文件就多建几个任务,但是这是一般的做法。
比如本站源码就分成多个模块,项目结构都一样,如果按一般的做法就是要建无数个任务,关键是项目是不断开发中的,不可能每次都新建,所以就想怎么自动切换处理不同文件夹。
Gulp 调用不同的任务是通过 gulp task
这个调用task任务的,如果不存在task任务就会报错,那有没有可能 通过 gulp task
调用处理 task 这个文件夹呢?
1.首先获取 参数
var name = '';
if (process.argv && process.argv.length > 2) {
name = process.argv[2]; // 这就获取到了参数
}
2.然后根据参数改变目标文件夹
但是 gulp task
是会调用 task 任务的,那么可以
3.临时生成 task 任务
name 可能为空,但不能生成一个空命名的任务
4.最终结果
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);
测试 压缩 test 文件夹下的css
2018-12-20 05:24:00
这个只是简单的进行四个方向的阴影,会出现阴影效果不真实,缺角不连贯
/*说明:(以上部边为例进行说明)
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; /*下边阴影 黄色*/
真正意义上的全阴影,但是阴影的效果相对于单边阴影距离减半,所以要设得更大
div{
width:250px;
height:250px;
background:greenyellow;
box-shadow:black 0px 0px 10px;//将颜色提到前面,且将h-shadow,v-shadow设为0px,实现四周阴影
}
【参考】
2018-12-20 05:22:31
focus事件在页面获得输入焦点时触发。
blur事件在页面失去输入焦点时触发。
visibilitychange事件在网页可见状态发生变化时触发.
window.addEventListener('visibilitychange',() => {
// 通过这个方法来获取当前标签页在浏览器中的激活状态。
switch(document.visibilityState){
case'prerender': // 网页预渲染 但内容不可见
case'hidden': // 内容不可见 处于后台状态,最小化,或者锁屏状态
case'visible': // 内容可见
case'unloaded': // 文档被卸载
}
});
freeze事件在网页进入挂起状态时触发。
resume事件在网页离开挂起,恢复时触发。
pageshow事件在用户加载网页时触发。这时,有可能是全新的页面加载,也可能是从缓存中获取的页面。如果是从缓存中获取,则该事件对象的event.persisted属性为true,否则为false
pagehide事件在用户离开当前网页、进入另一个网页时触发。它的前提是浏览器的 History 记录必须发生变化,跟网页是否可见无关。
beforeunload事件在窗口或文档即将卸载时触发。
unload事件在页面正在卸载时触发
window.addEventListener('online',onlineHandler)
window.addEventListener('offline',offlineHandler)
// 可以传入一个大于0的数字,表示让手机震动相应的时间长度,单位为ms
navigator.vibrate(100)
// 也可以传入一个包含数字的数组,比如下面这样就是代表震动300ms,暂停200ms,震动100ms,暂停400ms,震动100ms
navigator.vibrate([300,200,100,400,100])
// 也可以传入0或者一个全是0的数组,表示暂停震动
navigator.vibrate(0)
window.addEventListener('deviceorientation',e => {
console.log('Gamma:',e.gamma); // 设备沿着Y轴的旋转角度
console.log('Beta:',e.beta); //设备沿着X轴的旋转角度
console.log('Alpha:', e.alpha); //设备沿着Z轴的旋转角度
})
alpha 是以手机自带指南针为标准,及手机认为的南方为 0 ,需要注意手机不一定支持指南针,所以会出现偏差
// 通过这个方法来获取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;
}
})
【参考】
2018-12-07 07:04:06
CORS:跨域资源共享
实际分两步请求:
(无论响应什么数据都不会传到js程序里,只是浏览器做判断用,可以根据 请求方式 OPTIONS
和 Origin
请求头判断是否是预检)
请求头:OPTIONS
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
Origin: http://localhost:4200
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
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-Methods
和Access-Control-Allow-Headers
提供的信息) 可以被缓存多久。
注:需要注意的是Access-Control-Max-Age
的设置针对完全一样的url
,如果url
加上路径参数,其中一个url
的Access-Control-Max-Age
设置对另一个url
没有效果
(如果第一步没有获取到允许的响应头就不会发生第二步)
本次依然需要加上允许的跨域的响应头,js程序才能接收到相应的数据
2018-12-07 06:33:10
NUGET 添加依赖项
创建数据库映射
public class user
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string avatar { get; set; }
}
新建 DbContext类
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);
}
appsettings.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"
}
}
Startup.cs文件的ConfigureServices方法注册
public void ConfigureServices(IServiceCollection services)
{
var connection = Configuration.GetConnectionString("MysqlConnection");
services.AddDbContext(options => options.UseMySQL(connection));
services.AddMvc();
}
使用
private readonly DBContext _db;
//通过.NET Core框架自动为我们做构造函数依赖注入IOC。
public HomeController(DBContext db)
{
_db = db;
}
public ActionResult Index()
{
var list= _db.student.ToList();
return View();
}
【参考】
2018-10-22 04:42:42
cacert.pem 下载
notepad++
PHP
解压PHP
到指定文件夹, 例如:C:\Program Files\PHP
修改文件名为 php.ini
修改拓展文件夹路径
启用插件
extension=curl
extension=fileinfo
extension=gd2
extension=gettext
extension=mbstring
extension=mysqli
extension=openssl
extension=pdo_mysql
配置时区
配置openssl
证书
附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
启用“web服务器(IIS)
”,启用“iis 可承载web核心
”,启用“CGI
”
加载PHP
在iis主页 点击“处理程序映射
”,右击空白处点击“添加模块映射
”,配置
添加默认文档:index.php
安装 iis rewrite
解压 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
以管理员打开CMD,进入mysql
的bin
文件夹下
初始化
安装服务
启动服务
解压到任意文件,双击运行 heidisql.exe
运行
登录mysql
, 第一次登录会要求新设密码
第六步,测试
在 C:\inetpub\wwwroot
新建 index.php
,
使用浏览器访问 http://localhost
不同php
、mysql
版本依赖不同的vc
,大致需要 vc9
、vc11
、vc12
、vc14
,也分x86
和x64
,本次安装不需要额外安装
2018-09-22 02:56:44
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())
}
输出
在 struct
的方法中尽可能使用 指针 ,除非不希望方法中改变影响主体内容才使用引用
引用实际上是复制主体,会花费相对较多的系统开销(内存和时间)
在方法中改变参数会改变指针主体参数,不改变引用主体参数
2018-07-04 07:11:32
软件架构,也成称为软件体系结构,简单地说就是一种设计方案,将用户的不同需求抽象成组件,且能够描述组件之间的通信和调用。软件架构会分析工程中的问题,针对问题设计解决方案,针对解决方案分析应具有的功能,针对功能设计软件系统的层次和模块及层次模块之间的逻辑交互关系,确定各个功能如何由这些逻辑实现。开发人员可以根据软件架构分析出来的层次和架构进行软件编写。
【理解】:综合需求和能力对有关软件整体结构与组件的抽象描述
软件框架,是软件开发过程中提取软件的共性部分形成的体系结构。框架不是现成可用的应用系统,而是一个半成品,是一个提供了诸多服务,供开发人员进行二次开发,实现具体功能的程序实体。
框架与架构的关系:框架不是架构,框架比架构更具体,更偏重于技术,而架构更偏重于设计;架构可以通过多种框架来实现。
【理解】:对普遍使用的方法进行的归类、封装,不进行具体业务处理实现,但提供全面的处理方法
设计模式强调的是一个设计问题的解决方法,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
框架与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,一个模式可应用于不同的框架和被不同的程序语言所实现;而框架则是一个应用的体系结构,是一种或多种设计模式和代码的混合体。设计模式的思想可以在框架设计中进行应用。
架构与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,范畴比较小;而架构是高层次的针对体系结构的一种设计思路,范畴比较大。一个架构中可能会出现多个设计模式的思想。
【理解】:对程序的整体结构的概括
根据不同的标准,通常会说程序模块或功能模块,程序模块指的是一段能够实现某个目标的成员代码段,功能模块则用来说明一个功能所包含的系统行为。定义模块的原则是:高内聚和低耦合。
【理解】:实现大型软件系统的一部分功能的程序
组件是封装了一个或多个程序模块的实体。组件强调的是封装,利用接口进行交互。组件也称为构建。插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。
【理解】:对数据和方法简单封装的对象
插件属于组件,插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。插件是一种电脑程序,通过和应用程序的互动,来为应用程序增加一些特定的功能,仅靠插件是无法正常运行的,需要依赖于应用程序才能发挥自身功能。插件和应用程序之间通过接口进行交互。
【理解】:根据规范的接口编写的不能脱离平台单独运行的程序
对数据和方法封装的可视化组件。
【理解】:接受输入数据的组件
主要解决异构网络环境下分布式应用软件的互连与互操作问题,提供标准接口、协议,屏蔽实现细节,提高应用系统易移植性。
【理解】:从不同的输入来源转化成标准的数据,通过标准的接口进入不同的底层执行处理
2018-06-20 20:47:23
在浏览器中输入IP地址即可验证是否启动成功
开启端口
命令含义:
--zone
#作用域
--add-port=80/tcp
#添加端口,格式为:端口/通讯协议
--permanent
#永久生效,没有此参数重启后失效
重启防火墙
查看状态
首先检查 MySQL 是否已安装
如果有的话 就全部卸载
MySQL 依赖 libaio
,所以先要安装 libaio
下载 MySQL Yum Repository
地址为 http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
PS:如果提示-bash: wget: 未找到命令,请先执行 yum install wget
安装 wget
添加 MySQL Yum Repository
添加 MySQL Yum Repository
到你的系统 repository
列表中,执行
验证下是否添加成功
选择要启用 MySQL 版本
查看 MySQL 版本,执行
可以看到 5.5, 5.7 版本是默认禁用的,因为现在最新的稳定版是 5.6
查看当前的启动的 MySQL 版本
通过 Yum 来安装 MySQL
执行
Yum 会自动处理 MySQL 与其他组件的依赖关系.
启动和关闭 MySQL Server
启动 MySQL Server
查看 MySQL Server 状态
关闭 MySQL Server
MySQL 安全设置
服务器启动后,可以执行
(注意要切换到mysql数据库,使用use mysql)
防火墙设置
远程访问 MySQL, 需开放默认端口号 3306.
执行
firewall-cmd --permanent --zone=public --add-port=3306/tcp
firewall-cmd --permanent --zone=public --add-port=3306/udp
这样就开放了相应的端口。
执行
成功获取PHP7的yum源,然后再执行:
源码安装PHP7
下载源码
yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel
得到的路径是:/usr/bin/apxs
于是得到--with-apsx2的路径是/usr/bin/apxs
./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
在末尾加入:
(如果有问题 请检查添加的环境变量是否是PHP安装目录里的bin目录)
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
如果报错 请敲这行查报错信息 可以查到哪个文件第几行出错:
修改Apache默认欢迎页:
将/usr/share/httpd/noindex 修改为/var/www
修改Apache配置:
(请注意,/var/www这个路径是自定义,在配置文件中有好几处这个路径,如果更改,请全局搜索一下都改掉)
找到
在后面添加
搜索<IfModule dir_module>
下面这一块添加上index.php
搜索有没有下面这一行:
如果没有 请手动添加 否则 会出现运行php文件变成下载
在最下面配置域名
<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>
2018-03-30 07:56:32
正式开始学习C语言。
准备:
《C Primer Plus》第六版
windows 10 + vs2017 社区版
第一次写的程序当然是 《Hello World》
第一步打开vs2017
第二步新建项目
第三步添加源文件
第四步写代码
第五步编译
控制台会一闪而过,就表示编译成功!
好了,第一个c程序就这么简单完成了。
2018-12-23 21:55:30
多个用英文逗号隔开
文件 | 类型 |
---|---|
*.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 |
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代码可以避开
或直接通过其他工具上传也可以避开
或修改文件拓展名也可以避开
后端程序还是需要做其他验证
2017-11-11 22:58:15
七牛云上传主要有两种:
服务端上传
前端上传,前端又分两种返回方式:
1).重定向返回,可以解决ajax跨域的问题
2).回调返回,七牛云先向服务端要返回数据,再由七牛云返回前端,解决不支持重定向的请求方式,比如小程序上传
本次使用的是 七牛云 php sdk;
在Kindeditor/php 下添加 config.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";
在Kindeditor/php 下添加 qiniu_token.php
主要是生成上传用的 token
<?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
]);
在Kindeditor/php 下添加 callBack.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' => '视频上传出错'
]);
在Kindeditor/php 下添加 returnBack.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' => '视频上传出错'
]);
接下来是前端更改,我改的时视频上传
Kindeditor/plugins/media/media.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) {
...
});
这要就可以上传视频到七牛云了。
2017-10-29 05:40:38
本次使用的是 Let's Encrypt 推荐的快速安装工具 Certbot
一开始使用文档中的安装方法
但是发现报错,原来是程序不是最新的
然后根据 certbot: ImportError: ‘pyOpenSSL’ module missing required functionality, 使用pip 安装
清除之前操作
安装epel扩展源, 安装pip并更新到最新
安装最新版
申请ssl 绑定路径
那么只要在加上 -d 域名即可
,即使是临时增加一个域名,也要把所有域名都加上才行,
如果不同域名在不同文件夹,只需要设一个 -w
作为验证权限即可
更改 apache 站点配置,重启xampp
<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>
使用
安装自动更新证书
也可以手动更新证书
参考:
2017-10-29 05:40:38
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);
本插件使用 typescript 编写, js 请查看 GITHUB
本插件内置两个方法:
1.简单的图片加载。可以参考增加加载动画 给 img 的 src 设置一张 加载动态图片
<img src="loading.gif" data-src="image.jpg" class="lazy">
<script>
$("img.lazy").lazyload({
callback: 'img'
});
</script>
2.局部加载。依赖 template 函数(参考 art-template)
主要有两个参数 :
data-url 请求网址
data-tpl 模板元素id
(lazy-loading
为加载动画)
<script id="temp_tpl" type="text/template">
<div>{{ id }}</div>
</script>
请注意,本插件依赖 jquery
2017-10-29 05:36:59
JQuery
上配置的接口,在js中无用,这里只是为了以后 TS 使用方便,方便智能提示和书写,
interface CarouselOptions {
range?: number, // 每次移动的距离,默认一个item宽度
itemTag?: string, // 子代的标签
boxTag?: string, // 盒子的标签
spaceTime?: number, // 停顿的时间
animationTime?: string|number, // 动画执行时间
animationMode?: string, // 动画效果
previousTag?: string, // 可点击向前的元素
nextTag?: string, // 可点击向后的元素
thumbMode?: string // 缩略图模式
}
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';
}
插件具体功能实现
///
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);
}
}
JQuery
上;(function($: any) {
$.fn.carousel = function(options ?: CarouselOptions) {
return new Carousel(this, options);
};
})(jQuery);
--- 用typescript 写 js 一目了然
2017-10-29 05:36:59
通过 NUGET 安装 Microsoft.Bcl.Compression
;
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;
}
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;
}
}
虽然使用 HttpClient 更简单
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();//确保请求成功
但是它的响应头并没有 Content-Encoding
,所以无法直接判断需不需要 Gzip 解压。
2017-10-29 05:33:10
具体文档请查看【zodream】
主要用于连接数据库,默认使用单例模式通过PDO连接,其他连接方式有MYSQL、MYSQLI。
默认通过继承 Zodream\Database\Model\Model
扩展,添加具体的方法
<?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',
];
}
}
一般是控制器调用方法传给视图。
2017-10-29 05:12:39
主程序地址: zodream ,
Demo地址: PHP-ZoDream ,
文档地址:zodream
其他获取方式:
composer:
2017-10-17 05:00:15
具体文档请查看【zodream】
自动生成程序,主要分为三部分:获取数据库、模板、生成程序。其中生成程序又分为:Config
、Controller
、Model
、Module
、View
;
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 所有列名
在配置文件中手动添加,在本地运行时自动开启
输入网址 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
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>
layouts/footer.php
<?php
defined('APP_DIR') or exit();
use Zodream\Template\View;
/** @var $this View */
?>
<?=$this->footer()?>
</body>
</html>
index.php
2016-03-03 22:33:31
具体文档请查看【zodream】
在 Service 文件夹下新建 Home 文件夹,并新建 HomeController.php 文件作为默认控制器
<?php
namespace Service\Home;
use Zodream\Route\Controller\Controller;
class HomeController extends Controller {
public function indexAction() {
return $this->show();
}
}
解释
参数说明
$this->show($name, $data);
$name 为空或 null 时,根据路由得到的控制器名和方法名自动生成路径 Home/index.php
为数组时,此时得到的值作为要传递的参数,路径同上;
为匿名方法,直接执行,当有返回值则输出匿名函数的返回值;
为 @ 开头的字符串,直接输出字符串;
其他则作为路径解析
$data 为空
为字符串是,参数则以 data 命名;
为数组,并入参数
2016-03-03 22:33:31
具体文档请查看【zodream】
请下载 PHP-ZoDream 中的 starter 分支,这个分支只包含基本的骨架,适合新项目开发
主分支为框架配套的开发项目,包括本站的源码和一些开发中的模块
请配合composer.phar 在命令行执行
入口配置
html/index.php
<?php
use Zodream\Service\Web;
require_once dirname(__DIR__).'/Service/Bootstrap.php';
$app = new Web(APP_DIR);
$app->autoResponse();
解释 这里主要加载主程序 ,具体引入文件在
Service/Bootstrap.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';
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() 第一个参数为数组或第二个参数;
主要函数
$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); 输出绝对资源路径
基本介绍就这么多。