0%

首先感谢开源项目UnblockNeteaseMusic

能听被屏蔽的歌曲靠的就是它,这篇教程只是让使用变得更为方便而已。

解锁的大致原理就是依靠代理的形式,嫁接可用的音频源到网易云音乐里被屏蔽的请求地址上。所以它其实可以适用在任何终端,只不过移动端的不太便利,代理设置只能全局,理想的方式是在路由器上搭建服务,或者发布到自己的云服务器上。

这里不讨论移动端的解决方案,只说PC端的,这个比较简单。

1 安装nodejs

照例node环境还是必需的,安装过程略。

2 打开命令行,执行以下脚本

命令行可以这样打开:右键单击屏幕左下方的windows图标,选择Windows Powershell

然后输入以下内容回车:

1
npx @nondanee/unblockneteasemusic -p 16300

其中,-p 16300是修改服务端口为16300,因为默认的8080很容易在写代码时冲突,端口号可以自选。

3 在网易云音乐客户端中设置代理

在设置菜单中找到自定义代理-HTTP代理,设置服务器为localhost,端口为16300,点击确定即可。

此时会提示重启,重启后已经可以看到周杰伦的歌都不再是灰色了,开始享受吧。


但是很明显有个体验问题:

每次重启电脑,难道都要这么走一遍吗?太麻烦了,开始优化吧。

优化方案一

在合适的位置(比如桌面)新建一个bat文件(新建文本文档,写完以下内容并保存后,把后缀名txt改为bat),比如unlockncm.bat,内容如下:

1
2
start /d "C:\Program Files (x86)\Netease\CloudMusic\cloudmusic.exe" cloudmusic.exe
npx @nondanee/unblockneteasemusic -p 16300

这个批处理文件的作用就是启动网易云音乐的同时启动解锁服务。这样只需要双击bat文件,就可以像之前双击启动网易云音乐那样听歌了。

不过bat文件的图标有点丑,所以可以在bat文件上单击右键然后创建快捷方式。

重命名快捷方式为“网易云音乐”,右键属性——更改图标——找到C:\Program Files (x86)\Netease\CloudMusic\cloudmusic.exe即可将快捷方式的图标变成网易云音乐。

这样一来就跟原来的网易云音乐图标一样,看不出区别了。


但是这样还有个体验问题:powershell这个窗口不能关,否则服务就会关闭,挂了代理的网易云音乐就会瘫痪。

而这个碍眼的窗口挂在那里实在是很碍眼,所以还得优化。

优化方案二

(使用方案二时,将方案一废弃,不要再用这个bat启用网易云音乐了)

在左下角Windows图标上右键,然后点击计算机管理,依次展开系统工具-任务计划程序-任务计划程序库

这时应该可以看到右边有一堆已存在的计划任务,为了避免日后维护麻烦,右键点击任务计划程序库,选择新文件夹,创建一个自己的文件夹,比如user

当然,你可以跳过这一步,直接创建任务。

user上右键选择创建任务。

名称随意,但是用户账户一定要选SYSTEM,操作如图。这样做的目的是让脚本执行时不要弹出窗口。

新建触发器,设置为登录时。

操作为启动程序,内容如下:

1
npx @nondanee/unblockneteasemusic -p 16300

你可以直接把这段命令整个复制到程序或脚本这栏,保存时会自动将参数切割到添加参数里。

条件和设置看需求决定。

这样,每次电脑重启时,服务就都已经存在于系统中了,可以随意打开网易云音乐听歌,感知不到代理服务的存在。

微信小程序的wxss、阿里旗下淘宝、支付宝小程序的acss等等语法很类似原生css,但是在web开发里用惯了动态css语言,再写回原生css很不习惯,尤其是父子样式的嵌套写法非常繁琐。

因此,我希望能有一个自动化构建方案,能够简单地将scss转换成小程序的样式语言。

方案1

以前写微信小程序的依赖库时用过,使用gulp编译,将源码和编译后的代码分别放到src和dist两个目录。gulp会处理src下面的所有文件,将其中的scss转换成css,并将其他所有文件原封不动挪到dist下相应位置。

这里就不详细说了,代码参考Wux

方案2

非常简单直接,使用Webstorm/IDEAFile Watchers功能实时转换。

安装Ruby和sass

确保命令行输入sass -v能出现版本号,安装过程略。

安装File Watchers

到插件市场上搜索并安装(已安装则跳过)

添加scss的转换脚本

现在安装完插件打开项目会自动弹出scss转css的向导,方便了很多。但还需要做一些修改,配置如下:

首先要将生成文件的后缀名改掉,比如这里我的淘宝小程序就得是acss

其次,将Arguments改为:

1
$FileName$:$FileNameWithoutExtension$.acss --no-cache --sourcemap=none --default-encoding utf-8 --style expanded

如果不加--no-cache,scss文件同目录下会出现一个.sass-cache目录。

如果不加--sourcemap=none, scss文件同目录下会出现一个.map文件。

如果不加--default-encoding utf-8, scss文件如果有中文注释转换就会报错。

style可不加,这里用的是无缩进和压缩的风格,反正小程序打包发布时还会压,这里保持可读性。

现在这个scss转换是单独作用于项目的,如果新建一个小程序项目,就需要重新添加(不建议设置成global,容易误伤)。

注意到File Watchers列表的右侧操作栏下方有导入导出按钮,可以将现在配好的设置导出保存,将来新建项目时只要导入一下就行了。


之后还有一个问题,如果我手动将编译后的css(即wxss或者acss,下略)文件删除,scss文件不改动的话,就不会重新编译出css文件。
或者万一监听失效或者不够及时,css还有可能是旧的。
所以还需要一个命令,用来将整个目录下的scss文件统一转换,确保没有遗漏和保持代码最新。

不过我看了半天sasssass-convert的文档,没有找到一个可用的写法,能让命令行遍历指定目录下的所有scss文件,将其转换成css放到源文件所在目录,并且将后缀名改为wxss或者acss

所以遍历这个行为只能交给nodejs来实现,代码如下:

创建编译脚本build/scss-convert.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var path = require("path")
var fs = require("fs")
const { exec } = require('child_process')

const basePath = path.resolve(__dirname, '../')

function mapDir(dir, callback, finish) {
fs.readdir(dir, function(err, files) {
if (err) {
console.error(err)
return
}
files.forEach((filename, index) => {
let pathname = path.join(dir, filename)
fs.stat(pathname, (err, stats) => { // 读取文件信息
if (err) {
console.log('获取文件stats失败')
return
}
if (stats.isDirectory()) {
mapDir(pathname, callback, finish)
} else if (stats.isFile()) {
if (!['.scss'].includes(path.extname(pathname))) {
return
}
callback(pathname)
}
})
if (index === files.length - 1) {
finish && finish()
}
})
})
}

mapDir(
basePath,
function (file) {
const newFileWithoutExt = path.basename(file, '.scss')
if (newFileWithoutExt.startsWith('_')) {
return // 按照scss规则,下划线开头的文件不会生成css
}
// exec可以让nodejs执行外部命令
exec(`sass --no-cache --sourcemap=none --default-encoding utf-8 --style expanded ${file}:${newFileWithoutExt}.acss`, {
cwd: path.dirname(file) // 不写这个会导致生成的文件出现在根目录
}, (err, stdout, stderr) => {
if (err) {
console.log(err)
return
}
console.log(`stdout: ${stdout}`)
})
},
function() {
// console.log('xxx文件目录遍历完了')
}
)

package.json里添加一条script:

1
2
3
"scripts": {
"scss": "node build/scss-convert",
},

最近尝试用TypeScript写一个工具库,需要实现这样一个场景:

  1. 声明一个抽象类Parent

  2. 声明一组子类ChildA、ChildB继承这个Parent,实现它的抽象方法

  3. 实现一个方法,根据参数返回对应的子类

  4. 用拿到的子类创建实例

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Animal {
abstract makeSound(): void
}
class Dog extends Animal {
makeSound(): void {
console.log('woof')
}
}
class Cat extends Animal {
makeSound(): void {
console.log('meow')
}
}
const getAnimal = (name: string) => {
if (name === 'cat') return Cat
return Dog
}

const animal = new (getAnimal('dog'))()
animal.makeSound() // woof

首先注意new后面getAnimal方法的执行需要用括号包起来,否则将得到以下错误:

1
2
TS2350: Only a void function can be called with the 'new' keyword.
ESLint: A constructor name should not start with a lowercase letter.

随后按照严谨的做法,我尝试给这个getAnimal方法添加类型约束:

1
2
3
4
const getAnimal = (name: string): Animal => {
if (name === 'cat') return Cat
return Dog
}

马上得到了错误提示:

1
2
TS2351: This expression is not constructable. 
Type 'Animal' has no construct signatures.

这样写的错误在于,Animal描述的应当是一个由其创建的实例的类型(或者说类)。

比如改写成下面这样就没有问题了:

1
2
3
4
const getAnimalInstance = (name: string): Animal => {
if (name === 'cat') return new Cat()
return new Dog()
}

而上面的getAnimal方法返回的不是实例,是类(构造器)本身。

描述这种类型,需要用到TypeScript的new ()语法

1
2
3
4
const getAnimal = (name: string): { new (): Animal } => {
if (name === 'cat') return Cat
return Dog
}

或者这样写:

1
2
3
4
const getAnimal = (name: string): new () => Animal => {
if (name === 'cat') return Cat
return Dog
}

表示这个方法返回的是一个构造器,这个构造器可以创造出一个类型是Animal的实例。

最后的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class Animal {
abstract makeSound(): void
}
class Dog extends Animal {
makeSound(): void {
console.log('woof')
}
}
class Cat extends Animal {
makeSound(): void {
console.log('meow')
}
}
const getAnimal = (name: string): { new (): Animal } => {
if (name === 'cat') return Cat
return Dog
}

const getAnimalInstance = (name: string): Animal => {
if (name === 'cat') return new Cat()
return new Dog()
}

console.log('sound:', new (getAnimal('dog'))().makeSound())
console.log('sound:', getAnimalInstance('cat').makeSound())

打开在线示例查看执行结果:

https://codepen.io/mirari/pen/xxbrvVd

点击Console打开控制台

参考文档:

TypeScript-泛型-在泛型里使用类类型

之前遇到了一个遮罩层级的奇怪问题。在dev环境能在弹窗前正常显示的全屏动画,在编译打包以后实际运行时跑到了弹窗的后面。
弹窗的z-index是500,按理说将动画放到501就应该能显示了,结果发现实际编译出来的z-index仅仅是1。
最后发现这个z-index被重新计算是cssnano的杰作。因为它只处理了项目本身的样式,而不包括第三方库的样式。

cssnano的配置如下:

1
2
3
4
5
"cssnano": {
"preset": "advanced",
"autoprefixer": false,
"postcss-zindex": false
}

可见z-index自动重新计算的配置应该是关闭的。
但是这配置实际上是从webpack2的旧项目上拷过来的,而新版本的cssnano已经悄悄修改了配置方式,这个写法等于什么都没配。
修改成以下配置就可以解决问题了:

1
2
3
4
5
6
"cssnano": {
"cssnano-preset-advanced": {
"zindex": false,
"autoprefixer": false
}
}

最近有个转盘抽奖的需求,搜了一下现有的轮子,有的是用jQuery的动画函数实现的,有的是用canvas绘图然后再用高频率的setTimeout调用旋转方法,前者太老了没法简单移植到vue项目,后者感觉性能表现可能不会太好。也有一些用CSS动画的方案,设计了加速-匀速-减速三个动画,再计算偏转角度让三个动画尽可能无缝衔接,但我感觉绕了大远路,应该有更简单轻量的实现方案。个人更倾向于用transition来实现,不过网上的例子感觉还不够好,有的倾斜文字都没有对齐,最后还是自己手写了一个。核心思路是用transition以及rotate实现旋转动画,使用transition-origin和rotate绘制出定位较为精确的轮盘奖项,同时支持动态设置奖品数量。

许多项目脚手架默认就会把src目录添加一个@别名,项目中实际引入时,虽然可以精简路径,但也带来一个很麻烦的问题:
IDE无法识别这些别名,因此导致无法自动完成路径、无法识别引用资源的输出、出现不必要的告警等情况。

偶然发现vscode的web项目里有一个jsconfig.json文件,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

只要有这个文件,vscode就可以正常识别出别名了。

后来发现JetBrains家的IDE更简单,配置指定一下就行:

在项目设置的webpack标签页里,将配置文件指向<projectRoot>/node_modules/@vue/cli-service/webpack.config.js即可。

保存并重新打开项目以后,不只src,所有的别名比如utils等等都可以被正常识别。

其实这都已经写在vue-cli3文档里了,只是原始表述不太直观,被我一直忽略了。

IOS下的webview页面,内嵌iframe元素,将其样式指定为宽高100%:

1
2
3
4
.iframe {
width: 100%;
height: 100%;
}

在安卓下运行均无问题,但是在IOS下会出现异常。

具体表现为iframe页面内的子元素一旦超出原先的边界,只要能影响到html元素的宽高,就会自动撑开iframe,即使html元素设置了overflow:hidden也没用。
比如一个body元素下的弹层需要从下往上滑动进场,这个弹层的位置就会导致html高度的变化,因此页面底部的tabbar就会在弹层运动期间先消失再出现。

解决方法就是使用具体的宽高数值锁定iframe元素:

1
2
3
4
5
6
7
8
function onLoadIFrame (index) {
// 修复IOS下轮播图初始化瞬间会让iframe宽度自行扩大问题
if (this.ENV.isIOS) {
const iframe = this.$el.querySelector('#iframe' + index)
iframe.style.width = iframe.clientWidth + 'px'
iframe.style.height = iframe.clientHeight + 'px'
}
}

babel-preset-env是现在最主流的babel解决方案,但是前阵子踩到了一个坑。
部分机型在执行到ES6的for of语句时就会报错。

原因有两个。

一是babel-preset-env默认只转换语法(syntax),而不会转换全局对象(IteratorGeneratorSetMapsProxyReflectSymbolPromise等)和全局对象上的新增方法(Object.assign等)。

二是for of会被babel编译,编译之后其中会出现Symbol关键字。

所以表面上看for of似乎可以使用,但实际上涉及到了新全局对象,从而导致不支持Symbol的浏览器报错。

因此,babel-preset-env并不像宣传(你需要的唯一Babel插件)描述的那样简单,它仍旧离不开那个笨重但好用的babel-polyfill

最终的解决方案是在.babelrc中的env下添加useBuiltIns属性, 值为entry:

1
2
3
4
5
6
"presets": [
["env", {
"modules": false,
"useBuiltIns": "entry"
}]
],

并在项目的主入口顶部添加:

1
import 'babel-polyfill'

这样env会自动精简没有使用到的polyfill,比单纯直接引入polyfill要小一些。

useBuiltIns还可以设置为usage,也就是在未兼容的环境使用新对象或方法时在当前脚本顶部自动添加polyfill。

以下问题均出现在windows环境,使用IntelliJ IDEA中的npm选项卡双击执行命令时。

使用npm的gh-pages工具包,可以很方便地一键发布指定目录到项目的gh-pages分支。
以前用着好好的,但是最近使用的时候,忽然发现以下报错信息:

1
2
No such device or address: '/dev/tty'
fatal: could not read Username for 'https://github.com': No error

网上搜到类似的问题都是hexo的发布,但这里并不像hexo一样用到config.yml,流传的处理方式也只是治标不治本(暴露了账号密码到配置文件中)。
关键问题还是执行过程中出现了windows不支持的语句。
最终处理方式是,重新安装git for windows,在安装过程这一步选择第二项:

这样可以使cmd支持git命令。应该是之前手贱升级git的时候点错了。

随后尝试一键发布到npm仓库时又出现了如下错误:

1
2
 Error: EPERM: operation not permitted, unlink '...\Temp\npm-xxxxx-xxxxxxxx\tmp\fromDir-xxxxxxxx\package.tgz'
npm ERR! at Error (native)

解决方式是打开Idea设置,找到Node.js and npm,将Package Manager从yarn改为npm。

9月初Chrome发布了最新的正式版,版本号61,升级以后却发现所有网页都变成了奶白清新配色。
不管CSS原本设置是什么颜色,最终显示效果都会比设置得更白一些。
如图:

操作系统是win10 x64,浏览器是chrome 61 x64。

去google了一下发现是普遍现象,解决方法如下:

  1. 在chrome地址栏输入chrome://flags/
  2. 找到配置项Color correct rendering,将其禁用并重启浏览器即可。

经验证问题已解决。

据说先行版本63没有此问题,也许后续版本中会修复,到时候可以将设置恢复默认。