在看的和待解决的

在看的

React Router定义登录验证后才可以访问的页面

网上搜到的定义PrivateRoute的方法大同小异,例如以下代码来自React Training提供的示例

function PrivateRoute({ children, ...rest }) {
  return (
    <Route
      {...rest}
      render={({ location }) =>
        fakeAuth.isAuthenticated ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location }
            }}
          />
        )
      }
    />
  );
}

这种基于重定义的Route组件的render属性的做法并不靠谱,如果不小心用了一下component属性,就会发现检查用户是否登录验证的代码失效了:

// 从
<PrivateRoute path="/protected">
  <ProtectedPage />
</PrivateRoute>

// 改为
<PrivateRoute path="/protected" component={ProtectedPage}>
  <ProtectedPage />
</PrivateRoute>
// 或者
<PrivateRoute path="/protected" component={ProtectedPage} />

这是因为render属性的优先级是最低的,见React Router源码

let { children, component, render } = this.props;

// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
  children = null;
}

// 优先级:children > component > render
return (
  <RouterContext.Provider value={props}>
    {props.match
      ? children
        ? typeof children === "function"
          ? __DEV__
            ? evalChildrenDev(children, props, this.props.path)
            : children(props)
          : children
        : component
        ? React.createElement(component, props)
        : render
        ? render(props)
        : null
      : typeof children === "function"
      ? __DEV__
        ? evalChildrenDev(children, props, this.props.path)
        : children(props)
      : null}
  </RouterContext.Provider>
);

如果非要在自定义的render函数里完成所有工作,需要全面考虑PrivateRoute里定义了childrencomponentrender的情况(在传给Route组件的自定义render函数里把以上源码中复杂的逻辑再重复一遍),才能实现和Route兼容的API。不如把判断用户是否登录的逻辑放到Route组件外面,让Route组件去处理不需要关心的内容:

function PrivateRoute(props) {
  if (fakeAuth.isAuthenticated) {
    return (<Route {...props} />)
  } else {
    const { children, component, render, ...rest } = props;
    return (
      <Route
        {...rest}
        render={({ location }) =>
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location }
            }}
          />
        }
      />
    );
  }
}

URL编码参考

w3school整理了一个参考页面,可以看到,同一文本,不同的字符集对应的URL编码可能不一样,因此它是在 GBK、UTF-8 编码后的字节串的基础上进行的再编码:

     字符集编码          URL编码
文本 ----------> 字节串 ---------> URL编码后的ASCII字符串

                      URL解码          字符集解码
URL编码后的ASCII字符串 ---------> 字节串 ----------> 文本

TypScript 模块定义和模块扩展

在 Vue 项目中使用 TypeScript 时,会用到下面的定义:

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

这里如果把 import 语句放到 declare 语句块外部

import Vue from 'vue'
declare module '*.vue' {
  export default Vue
}

便不起作用,因为这两种语义是不同的,最外层的 import 语句表示整个源文件是一个模块,因此这里模块定义(module declaration)就变成了模块扩展(module argumentation),这种用法常用于往现有模块中添加内容,例如定义浏览器中的全局 Vue 对象 window.Vue

import { VueConstructor } from 'vue';

declare global {
  interface Window {
    Vue: VueConstructor;
  }
}

GRIB 无效值

目前掌握的信息,GRIB格式的文件没有用无效值(missing/undef/fill value)来编码。

The GRIB format does keep track of missing values but through the use of a bitmap It does not allow the specification of a missing value.

A default value of 9999 is set for the missing value in the library (not the GRIB message!). That means that when retrieving the values from a message without having set the missing value key, all missing values in the data will be replaced with the default value of 9999.

至于解码后这些无效的数据被设置成什么值,不同工具有不同的处理方法:

wgrib使用9.999e20

/* undefined value -- if bitmap */
#define UNDEFINED       9.999e20

// lengthy code

/* 1996             wesley ebisuzaki
 *
 * Unpack BDS section
 *
 * input: *bits, pointer to packed integer data
 *        *bitmap, pointer to bitmap (undefined data), NULL if none
 *        n_bits, number of bits per packed integer
 *        n, number of data points (includes undefined data)
 *        ref, scale: flt[] = ref + scale*packed_int
 * output: *flt, pointer to output array
 *        undefined values filled with UNDEFINED
 *
 * note: code assumes an integer > 32 bits
 *
 * 7/98 v1.2.1 fix bug for bitmaps and nbit >= 25 found by Larry Brasfield
 * 2/01 v1.2.2 changed jj from long int to double
 * 3/02 v1.2.3 added unpacking extensions for spectral data
 *             Luis Kornblueh, MPIfM
 * 7/06 v.1.2.4 fixed some bug complex packed data was not set to undefined
 */

grib2ctl遵从这个约定:

print "undef 9.999E+20\ntitle $file\n*  produced by grib2ctl v$version\n";

NCL目前使用1e20

The value assigned for the _FillValues used to be the default fill value for float data in NCL, -999.0, but because this value occasionally collided with real data values, as of 5.1.0 it has been changed to 1e20 for GRIB data.

这封邮件列表也提到某些GRIB扩展允许指定无效值,本处不作深究。

Maven

阿里云镜像:

在 ~/.m2/settings.xml 中添加如下内容:

<mirrors>
  <mirror>
    <!-- Source: https://help.aliyun.com/document_detail/102512.html?spm=a2c40.aliyun_maven_repo.0.0.361865e9NMmjcb#h2-u4EE3u7406u7684u4ED3u5E93u5217u88681 -->
    <!-- Refer: https://maven.aliyun.com/mvn/view -->
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里云公共仓库</name>
    <url>https://maven.aliyun.com/repository/public</url>
  </mirror>
</mirrors>

来自Maven 配置指南,Maven 配置拆分为3部分:

Maven 用户配置文件选项的解释参考 $MAVEN_DISTRIBUTION_DIRECTORY/conf/settings.xml

Maven Wrapper的作用是帮助用户快速建立项目的 Maven 环境,包括入口脚本 mvnw(类Unix环境)、mvnw.cmd(Windows环境),以及 .mvn/wrapper/,其应用类文件 maven-wrapper.jar 位于该目录中,如果项目代码库允许包括 .jar 文件,可以直接把该类文件提交到代码库中,否则可以通过编译该目录下的 MavenWrapperDownloader.java 来下载 maven-wrapper.jar,maven-wrapper.jar 以及 Maven 的下载地址都可以写在 maven-wrapper.properties 文件中。

MySQL 备份与恢复

备份:

mysqldump --add-drop-table -h ${host} -u ${user} -p ${database} >${dumped_sql_file}

恢复:

mysql -h ${host} -u ${user} -p ${database} <${dumped_sql_file}

MySQL 字符集设置

参考这篇问答

MySQL链接到非标准端口的服务器

使用 mysql 命令连接不在3306端口监听的服务器时,只输入 -P $port 参数是不够的,一定要配合 -h $ip 参数使用:

mysql -h $ip -P $port -u $user -p

因为不指定host参数或者host参数是 localhost 的时候,客户端会尝试连接本地的unix socket,因此,以下命令

mysql -h $ip -P $port -u $user -p

不会按照期待的方式运行,如果本地有MySQL服务在监听unix socket,会连接到该服务,否则会报错

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

CentOS 7 安装设置 phpMyAdmin

首先需要安装必要的软件:

yum install httpd php phpMyAdmin

修改 /etc/phpMyAdmin/config.inc.php 确保 phpMyAdmin 可以正确连接上 MySQL 数据库,phpMyAdmin 默认连接 127.0.0.1 的 3306 端口,如果 mysqld 没有在此端口监听,则需要修改 $cfg['Servers'][$i]['host']$cfg['Servers'][$i]['port'] 配置。如果使用 UNIX socket,则需要设置 $cfg['Servers'][$i]['socket'] 选项,并且 $cfg['Servers'][$i]['connect_type'] 配置需要设为 'socket'

启动 httpd 服务:

systemctl start httpd

在浏览器中打开 http://127.0.0.1/phpMyAdmin/ 就可以使用了。

MySQL 命令

列出服务器上所有的数据库(以下两条命令等价):

SHOW DATABASES;
SHOW SCHEMAS;

列出数据库中所有数据表(或者参考“显示数据库中各数据表的记录数”):

USE <your_db>;
SHOW TABLES;

显示数据库中各数据表的记录数:

SELECT TABLE_NAME, TABLE_ROWS
     FROM INFORMATION_SCHEMA.TABLES
     WHERE TABLE_SCHEMA = '<your_db>';

删除数据库:

DROP DATABASE [IF EXISTS] <database_name>;

清空数据表

USE <your_db>;
DELETE FROM <table>;

查看数据表的设计

DESCRIBE [db_name.]table_name;

查看创建数据表的 SQL 语句

SHOW CREATE TABLE [db_name.]table_name;

删除数据表的字段

ALTER TABLE tbl_Country DROP COLUMN IsDeleted;
ALTER TABLE tbl_Country
  DROP COLUMN IsDeleted,
  DROP COLUMN CountryName;

添加数据

INSERT INTO `django_migrations` (`app`, `name`, `applied`) VALUES ('api', '0003_auto_20190524_0716', '2019-05-24 07:18:00');

修改数据

UPDATE table_name
SET
    field1 = value1,
    field2 = value2
WHERE
    condition_expression;

把字段设置为 NULL

UPDATE your_table
SET
    your_column = NULL
WHERE
    condition_expression;

筛选字段为 NULL 的记录

SELECT * FROM your_table WHERE your_column IS NULL;

Selinux 添加HTTP端口

在selinux启用的情况下,httpd只能在被允许的端口监听请求,用下面的命令查看目标端口是否被允许:

semanage port -l | grep http

用下面的命令将目标端口添加到被允许的HTTP端口列表:

semanage port -a -t http_port_t -p tcp $target_port

来源:serverfault

Webpack 在编译时自动注入变量

// webpack.config.js
const webpack = require('webpack')

{
  // other settings
  plugins: [
    new webpack.DefinePlugin({
      __WEBPACK_INJECT_VERSION__: JSON.stringify(require('package.json').version)
    })
  ]
}

也可以直接在源代码中写 require('./package.json').version,但这样会导入 package.json 的全部内容。

JavaScript 数组添加/删除元素

详见这篇讨论

Django 操作

启用令环验证,确保以下两点:

INSTALLED_APPS = (
    # other apps
    'rest_framework.authtoken'
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        # other authentication classes
    ),
    # other configurations
}

自定义 ViewSetqueryset,把 queryset 类属性删除,用 get_queryset 方法代替:

def get_queryset(self):
    return self.request.user.accounts.all()

与此同时,因为 queryset 属性的缺失,必须在注册路由表的时候提供 basename 参数,因为 DRF 无法根据 queryset 属性自动判断 basename 的值,basename 会被用于生成路由名称

获得空的 querysetMyModel.objects.none()

让 ESLint 检查 TypeScript 源文件的代码风格

下面说的 .ts 文件中不存在 no-unused-vars 假警报的问题,这是因为 ESLint 根本没有检查 .ts 文件。开启对 *.ts 文件的代码风格检查:

javascript test: /\.(js|ts|vue)$/

json "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ]

解决 Vue 单文件组件中 TypeScript 类型 no-unused-vars 假警报的问题

在 *.ts 文件中没有这个问题,原因不清楚,有问题的.eslintrc.js内容如下:

module.exports = {
  root: true,
  parserOptions: {
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  env: {
    browser: true
  },
  extends: [
    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
    'plugin:vue/essential',
    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
    'standard'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  globals: {
    'ga': true, // Google Analytics
    'cordova': true,
    '__statics': true
  },
  // add your custom rules here
  'rules': {
    // allow async-await
    'generator-star-spacing': 'off',

    // allow paren-less arrow functions
    'arrow-parens': 0,
    'one-var': 0,

    'import/first': 0,
    'import/named': 2,
    'import/namespace': 2,
    'import/default': 2,
    'import/export': 2,
    'import/extensions': 0,
    'import/no-unresolved': 0,
    'import/no-extraneous-dependencies': 0,

    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}

解决的办法是用用 @typescript-eslint/no-unused-vars 规则替换 no-unused-vars 规则,在 plugins 部分添加:

'@typescript-eslint'

rules 部分明确以下规则:

'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error'

Nuxt.js TypeScript 文档 的启发:

module.exports = {
  plugins: ['@typescript-eslint'],
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs'
  ],
  rules: {
    '@typescript-eslint/no-unused-vars': 'error'
  }
}

@typescript-eslint/no-unused-vars 是 eslint no-unused-vars 规则的的复制扩展,其控制选项是一样的,只是规则的名称要从 no-unused-vars 改为 @typescript-eslint/no-unused-vars,如果两个规则同时打开,设置特例的时候要把设置特例的规则写两遍:

/* eslint @typescript-eslint/no-unused-vars: ["error", { "varsIgnorePattern": "^someVar$" }] */
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^someVar$" }] */

在 TypeScript 中传递构造函数

构造函数和普通函数的类型不一样的地方在于其签名前面有一个 new 关键字:

class MyClass {
  constructor (public x: number) {}
}

function createInstance (Constructor: new (x: number) => MyClass) {
  return new Constructor(0)
}

如果不知道构造函数的参数,用 new (...args: any[]) => any 表示任意构造函数。

讨论链接

SQLite3 命令行不支持 readline 的处理

sh rlwrap sqlite3 "$args"

确保Vue重新渲染组件

给Vue组件添加 key 绑定,确保数据发生变化时 key 的值也发生变化。

git SSH代码库路径的两种格式

基于SSH的代码库路径有两种格式,URL形式 ssh://[user@]host.xz[:port]/path/to/repo.git/ 可以指定SSH服务监听端口,SCP形式 [user@]host.xz:path/to/repo.git/ 没有地方指定端口,需要在 ~/.ssh/config 中配置SSH服务器的信息。然后在地址中主机域名的位置填入主机别名: my_git_host:path/to/repo.git/

Host my_git_host
HostName git.some.host.org
Port 24589
User not_a_root_user

Supervisord 技巧

修改了配置文件,可以运行 supervisorctl reread 命令加载修改后的配置文件,但只有运行 supervisorctl update 才会应用这些修改。在添加了新程序的情况下,不想中断旧程序的运行,可以用这种方法运行新程序。

如果修改了旧程序的配置文件 supervisorctl update 会导致旧程序重启,如果在远程服务器上运行此程序,并且退出旧程序的时候连接断开(例如远程连接建立在旧程序维持的 SSH 隧道上),则其不会重启,如果没有其它渠道登录远程服务器,便会失去和远程服务器的连接。

Supervisor 配置的 startretries 参数默认为3,据说(这是后面推论的基础) supervisor 每次多延迟1秒钟尝试启动程序,这意味着如果3-6秒(取决于第一次尝试重启是0秒还是1秒)内程序因故无法启动,便会进入 FATAL 状态,被 supervisor 放弃,增加这个数字有助于推迟程序被放弃的时间,如果把 startretries 设为300,则这个时间延长到750分钟,达12个半小时之久,并且最长的尝试启动程序的间隔不超过5分钟。

autorestart=unexpectedautorestart=true,这两种设置的区别是,程序从 RUNNING 状态中退出时,如果 exit code 在 exitcodes 列表(从 4.0 版本开始,exitcodes 的默认值是 0,之前是 0,2)中,前者不会重新启动任务,后者会重新启动任务。如果程序没有进入 RUNNING 状态,默认要持续运行 startsecs(默认值是1)秒,autorestart 选项不起作用,任务总会重启。

SSH 发布端口到公网服务器

运行下面的命令可以把端口发布到公网服务器上:

ssh -R 8022:localhost:22 $remote_ip

这个命令在目标服务器的22端口和公网服务器的8022端口(loopback 网络界面 127.0.0.1,如需绑定其它 IP 地址,参考 man ssh 帮助)之间建立了隧道,任何时候都可以登录到公网服务器,然后运行 ssh -p 8022 localhost 登录到目标服务器。

部署一条目标服务器到公网服务器的端口需要满足3个条件:

把命令改为 ssh -nNT -R 8022:localhost:22 $remote_ip 可以建立一条除了隧道之外没有其它功能的 SSH 连接。

这里用公钥实现 SSH 免密登录,在目标服务器上运行一下命令生成 SSH 密钥:

sh-keygen -f ~/.ssh/tunnel -t rsa

在公网服务器上建立专供隧道连接的 tunnel 账户:

useradd -m tunnel

在公网服务器上

这时 ssh tunnel@remote_ip 命令应该可以借助公钥免密登录到公网服务器的 tunnel 账户,在后台运行 ssh 命令之前务必在前台运行一遍该命令(或者其它任何登录到该公网服务器的命令),以记录公网服务器提供的指纹,否则后台运行的 ssh 命令会因为公网服务器指纹提示得不到确认而失败。

最后用 supervisord 监控提供隧道连接的 ssh 命令,安装 supervisord,并且添加如下配置文件 /etc/supervisord.d/tunnel.ini:

[program:tunnel]
autorestart=true
startretries=300

command=ssh -nNT -i /root/.ssh/tunnel -R 8022:localhost:22 tunnel@$remote_ip

stdout_logfile=/var/log/supervisor/tunnel.out
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=10

stderr_logfile=/var/log/supervisor/tunnel.err
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=10

把 $remote_ip 替换成公网服务器的 IP 地址或域名,启动 supervisord 服务:

systemctl start supervisord

最好把该服务设为开机启动:

systemctl enable supervisord

如果不想每次都先登录公网服务器,然后再运行 ssh -p 8022 localhost 登录到目标服务器,可以再建立一条本地服务器到公网服务器的 SSH 隧道,不过这次运行的是 ssh -L 8022:localhost:8022 tunnel@$remote_ip,表示访问本地的8022端口相当于访问公网服务器的8022端口,也就相当于访问目标服务器的22端口。与 ssh -R 0.0.0.0:8022:localhost:22 $remote_ip 的方法相比,这种方法不需要公网服务器的 root 权限(修改 SSH 配置的 GatewayPorts 参数),也没有端口暴露到公网,因此更加安全。

TypeScript 类型

使用 TypeScript 3.5.1,打开 --strict 选项。

在 TypeScript 中,类型只是一种标记,用于类型推断和类型匹配检查,不影响程序的运行。如果类型注解有误,类型检查可以通过,但运行时错误在所难免。

类型名称和 JavaScript 关键字的名称可能一样,并且相互之间存在对应关系,如 undefined,也有可能使用 JavaScript 中没有的名称,如 any。JavaScript 内置类和用户自定义类的名称可以用作类型名,并且可以匹配该类的实例,但也存在例外,如 Array 在 TypeScript 中是泛型,必须包含参数。

booleannumberstringsymbol 分别匹配布尔、数值、字符串、符号这些原始类型的变量。与之对应的 JavaScript 类 BooleanNumberStringSymbol 也可以作为类型使用。除了 Symbolsymbol 匹配的类型相同(因为 Symbol 类不能构造实例),这些 JavaScript 内置类代表的类型是对应原始类型的超集。如 Boolean 可以匹配 truefalse,但是 boolean 不能匹配 Boolean 的实例。

let a: boolean = new Boolean(true)  // TS2322: Type 'Boolean' is not assignable to type 'boolean'.
let b: Boolean = true  // OK

nullundefined 类型分别匹配 nullundefined

object 类型匹配所有对象,但不能匹配原始类型的值,如 Object.create 的第一个参数的类型就可以精确地表达为 nullObject 类型也可以匹配所有的值,包括原始类型的值。Object 类型的值可以访问 Object.prototype 的属性,但 object 类型的值不可以访问任何属性。

let a: object = 4  // TS2322: Type '4' is not assignable to type 'object'.
let b: object = new Number(4)
console.log(a.toFixed(2))  // TS2339: Property 'toFixed' does not exist on type 'object'.
let c: Object = 4
c = Symbol.for('4')

{} 类型也可以匹配所有的值,目前发现的其和 Object 类型的唯一区别是,Visual Studio Code 1.35.1 提示 Object 类型的属性自动完成,但不提示 {} 类型的属性自动完成,没有看到 tsc 在什么地方区分这两种类型。

let a: Object = []
console.log(a.hasOwnProperty('length'))
let b: {} = []
console.log(b.hasOwnProperty('length'))

越一般的类型,其匹配的范围越广,但可以使用的属性也越少,例如 Object 类型的变量,即使把字符串赋值给它,也不能直接访问 length 属性,可以用类型断言解决这个问题。

let a: Object = 'hello'
console.log(a.length)  // TS2339: Property 'length' does not exist on type 'Object'.
console.log((<any[]>a).length)
console.log((a as any[]).length)  // 类型断言的另一种写法和上面的语句等价
console.log((a as number).toFixed(2))  // 抛出异常,TypeError: a.toFixed is not a function

再次强调 TypeScript 类型不影响代码的运行,类型断言只用于把程序员推断的类型告诉 TypeScript 编译器,不进行类型转换。

类型断言并非只是告诉编译器此处的值是什么类型,也可以告诉编译器此处的值不是什么类型,如表达式后的 ! 告诉编译器此处的值不可能是 undefinednull

function getStringLength (s: string | undefined) : number | undefined {
  if (arguments.length) {
    // return s.length  // TS2532: Object is possibly 'undefined'.
    return s!.length
  }
}

function getAge (person: { name: string | null, age: number | null }): number {
  if (person.name) {
    // return person.age  // TS2322: Type 'number | null' is not assignable to type 'number'.
    return (person.age)!
  } else {
    return 0
  }
}

any 用于关闭类型检查,主要用于和 JavaScript 代码交互。any 类型可以匹配任意值,也可以访问任意属性,很特殊,其魔法来自于 TypeScript 不对涉及 any 类型的运算进行类型检查。

数组:T[] 或者 Array<T>,如 Array<number> 可以匹配数值元素组成的数组,Array<any> 可以匹配任意数组。

元祖类型(tuple)用于长度固定,并且每个位置的数组元素的类型可以确定的数组,如 [string, number] 可以匹配 ['PI', 3.14159]

枚举类型(enum)用于表示确定的选项,如 enum Color { Red=1, Green, Blue}

void 表示函数没有返回值,可以把它理解为 undefined 类型的别名,但有一处差别,void 类型可以匹配 undefined 类型的变量,但是反过来不可以:

let a: void = undefined;
let b: undefined = a;  // TS2322: Type 'void' is not assignable to type 'undefined'.

TypeScript 文档上说 void 类型可以匹配 undefinednull,但实际情况并非如此,参考这个解释

never 表示没有值,如函数不返回(死循环或者抛出异常):

function die(message: string): never {
  throw new Error(message)
}

function getStringLength (s: string | undefined) : number {
  if (arguments.length) {
    return s!.length
  } else {
    return die('Must provide a string')
  }
}

function messageLoop (): never {
  while (true) {
    // processMessage()
  }
}

或者空数组:

let a: Array<never> = Object.freeze([])

可以为 this 添加类型注解,放在函数的第一个参数之前:

class Person {
  constructor (public firstName: string, public lastName: string) {

  }
}

interface Person {
  getFullName: () => string;
}

Person.prototype.getFullName = function (this: Person) {
  return `${this.firstName} ${this.lastName}`
}

JavaScript 对象属性

这里讨论的属性都是对象自身的属性,即 Object.getOwnPropertyNames()Object.getOwnPropertySymbols() 可以列出的属性。

JavaScript 对象的属性不是一个简单的值,还具有其它性质,这些性质包括:(configurableenumerablevaluewritable)或者(configurableenumerablegetset)。这些属性也可以用一个 JavaScript 对象来描述,这个对象称为描述符 descriptor。

可以看到属性的性质分为两种,valuewritable 用于描述数据属性(data property),其描述符可称为数据描述符(data descriptor),getset 用于描述访问器属性(accessor property),其描述符称为访问器描述符(accessor descriptor),两类属性共享 configurableenumerable。这两类属性只能二选一,不能混用。

这些性质的作用可以参考 Object.defineProperty() 的 MDN 文档。下面列出对单项属性进行操作需要的条件(假设对象本身没有限制属性的添加、删除和设置操作):

原型链上的属性可能会限制通过赋值操作创建对象的同名属性,如:

var a = {}
Object.defineProperty(a, 'x', { value: 1, writable: false })
var b = Object.create(a)
b.x = 2  // 无效,或在 strict mode 下抛出 TypeError
b.x  // 1

// b.x 不再继承自 a
Object.defineProperty(b, 'x', { value: 2, writable: true })
b.x  // 2
b.x = 3
b.x  // 3

简单地理解就是,如果属性 configurable === true,则对于该项属性的操作是完全自由的,否则对该项属性进行操作的权限只能降低(把 writabletrue 设置为 false),不能升高。

可以用 Object.preventExtensions()Object.seal()Object.freeze() 限制操作对象属性,这些操作都是不可逆的,其效果如下:

上述对于对象属性的操作都限制为对象自身,不包括子对象(类型为对象的数据属性),也不包括原型:

var a = {}
var b = Object.create(a)
b.x = {}
Object.freeze(b)
b.x = 1
b.x  // {}
a.y = 2
b.y  // 2
b.x.z = 3
b.x.z  // 3

Vue 会监测 JavaScript 对象哪些属性的变化

以下代码来自 Vue 2.6.10 的 src/core/observer/index.js:

Vue 只为数组和普通对象添加 __ob__ 属性。

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Vue 只监测对象 Object.keys() 可以列出的(继承自原型链、可列举(Enumerable)、非 Symbol )属性的变化。

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /* The rest of class Observer */
}

yarn 全局安装路径

yarn global命令和npm install -g命令的安装位置存在差异,从npm转到yarn,有可能会遇到找不到yarn安装的全局命令的问题。

使用yarn global bin可以输出yarn安装的全局命令的可执行文件的位置,使用yarn global dir可以找到这些全局命令的软件包根目录位置(内含node_modules目录和yarn.lock文件)。在Linux系统中,非root用户的可执行文件位置和软件包根目录的位置分别为~/.yarn/bin~/.config/yarn/global

可以通过设置prefix参数来指定yarn全局命令的可执行目录的位置,如使用yarn config set prefix ~/node命令会把全局命令可执行文件位置设置为~/node/bin

用 yarn 更新依赖

单个依赖:yarn add ${package} 全部依赖:yarn upgrade

yarn 离线软件库

yarn 还支持离线软件库功能,目的是解决项目构建的可重复性和可靠性,线上软件库不能保证这两点。

离线软件库很容易设置,运行下面的命令在项目目录中创建 .yarnrc:

yarn config set yarn-offline-mirror ./npm-offline-repo
yarn config set yarn-offline-mirror-pruning true
mv ~/.yarnrc .

然后把项目所需的软件包下载到离线软件库里:

yarn cache clean
rm -rf node_modules/ yarn.lock
yarn install

离线软件库有如下好处:

简单的说,离线软件库赢在轻量、简单、工具无关。

yarn 修改次级依赖的软件包版本号

quasar-cli 把 Vue 的版本号写死为 2.5.17,而想要使用新的 slot 语法,必须将 Vue 升级到 2.6,yarn 通过 Selective dependency resolutions 优雅地解决了这个问题。

首先把软件依赖管理工具从 npm 切换到 yarn,在项目目录下运行 yarn import,然后删除 package-lock.json 即可。yarn 默认使用 http://registry.yarnpkg.com/,可以通过 --registry 参数修改为其它 npm registry。

最后在 package.json 中添加 "resolutions" 记录:

"resolutions": {
  "quasar-cli/vue": "^2.6.0",
  "quasar-cli/vue-server-renderer":"^2.6.0",
  "quasar-cli/vue-template-compiler": "^2.6.0"
}

然后运行 yarn install 命令即可。

理解 Vue slot

把 Vue 模板理解为渲染函数(Vue 内部实现就是把模板编译成渲染函数,因此这种相似是天然的),而不是 HTML。在这个类比下,slot 就是传递给子渲染函数的一个类型为渲染函数的参数,因此是高阶渲染函数。

在 Vue 子模板中,通过 <slot> 标签调用 slot 渲染函数,当存在多个 slot 的时候,用 name 属性进行区分,没有 name 属性的是 default slot。

<slot></slot>

相当于 render(slots.default)

<slot name="slot1"></slot>

相当于 render(slots.slot1)

调用 slot 渲染函数的时候,通过 v-bind 指令把当前作用域中的变量传递给它。

<slot :key="value"></slot>

相当于 render(slots.default, { key: value })

在 Vue 父模板中,在子模板元素中定义和传递 default slot 渲染函数,非 default slot 需要定义在 <template> 元素中,用 v-slot 指令标记 slot 的名字和传递给 slot 的参数。

<child>
  <template v-slot:slot1>
    {{ key }}
  </template>
</child>

相当于

function slot1 () {
  render(key)
}

child({ slot1 })

如果需要使用传递给 slot 渲染函数的变量,也在 v-slot 中列出:

<child>
  <template v-slot:slot1="{ key }">
    {{ key }}
  </template>
</child>

相当于

function slot1 ({ key }) {
  render(key)
}

child({ slot1 })

在 Vue 2.6 之前,定义 slot 的时候,用 slotslot-scope 两个属性标记 slot 的名称和传递给 slot 的参数。上述最后一个例子等价为:

<child>
  <template slot="slot1" slot-scope="{ key }">
    {{ key }}
  </template>
</child>

slot 可以用于分离数据逻辑和渲染逻辑,见这篇文章,也可以用高阶渲染组件(HOC)混入来实现。Vue 3计划添加基于函数的组件API,为这类应用场景提供更好的支持。

常见的 Windows 硬件信息查询命令

扩展笔记本电脑内存

操作系统是 Windows 10,在不安装外部硬件检测软件的条件下,可以用 wmic 命令查询购买何种型号的内存条的信息。

wmic memphysical 用于显示内存插槽的详细信息,要读懂其输出结果,这篇文档必不可少。其输出的几个关键信息如下:

wmic memorychip 用于显示内存条的信息,每个内存条显示1条记录,只查到1条记录,说明电脑上还有一个空的内存插槽可用,其参数的含义可参考该文档。关键参数:

因此,得到一些信息,电脑上理想情况下可以插2条16GiB 的内存,现在已经安装了一条8GiB 的内存,可以再安装一条16GiB 的 DDR4 SODIMM 内存,频率2400MHZ,电压1.2V。这些信息可以和相应型号笔记本电脑的规格说明中的内存信息作对比,后者主要用于增强对于通过软件方法获取的信息的信心。

修改 quasar 项目的版本

直接修改 package.json 会导致 package-lock.json 中的版本信息和 package.json 中的不一致,应该使用 npm version 命令:

npm version  # 列出当前项目的版本信息
npm version patch  # 更新 patch 版本
npm version $new_version  # 更新到新版本
# 在 git 项目中运行此命令,更新版本的同时 npm version 会创建 git commit 和 tag
# 用 --no-git-tag-version 关闭此行为
npm version --no-git-tag-version $args

在编译 Cordova 应用之前,Quasar 会更新 Cordova config.xml 中的版本信息,其代码在 quasar-cli/lib/cordova/cordova-config.js 中:

prepare (cfg) {
  this.doc = et.parse(fs.readFileSync(filePath, 'utf-8'))
  this.pkg = require(appPaths.resolve.app('package.json'))

  /* other stuff */
  root.set('version', cfg.cordova.version || this.pkg.version)

  this.__save()
}

据此可以写自己的同步脚本:

// tools/sync_cordova_version.js

const
  fs = require('fs'),
  path = require('path'),
  et = require('elementtree')

const filePath = path.resolve(__dirname, '../src-cordova/config.xml')
const doc = et.parse(fs.readFileSync(filePath, 'utf-8'))
const pkg = require('../package.json')
const root = doc.getroot()
root.set('version', pkg.version)
const content = doc.write({ indent: 4 })
fs.writeFileSync(filePath, content, 'utf8')
console.log('Updated Cordova config.xml')

Axios 在 HTTP 请求中附加验证信息

正确的做法是设置 axios 实例的默认参数

axiosInstance.defaults.headers.common.Authorization = 'Token ' + token

设置 axios 实例的拦截器应该也有效果,但没有测试过,设置 axios 全局拦截器肯定没有效果,因为 axios 实例不能调用全局拦截器

程序退出状态码(Exit Status)

当程序需要作为子进程运行时,可以用退出状态码表示其退出状态,但只是子进程和父进程之间约定,如何定义状态码最终取决于父进程将如何使用它们。以下内容仅供参考:

Cordova Android 版本代码

如果没有设置 android-versionCode,Cordova 会根据 version 来计算:

versionCode = MAJOR * 10000 + MINOR * 100 + PATCH

但这并非编译得到的 Android apk 文件中的版本代码,设置 cdvBuildMultipleApks 会把以上设置的或计算得到的版本代码乘10,最后一位用来表示目标平台的架构。如果需要描述 apk 文件的版本代码,应该直接从 apk 文件读取:

aapt dump badging $apk_file

Cordova 拍摄和上传视频

安装和 Cordova 版本匹配的 cordova-plugin-media-capture cordova-plugin-file 插件:

cordova plugin add cordova-plugin-media-capture
cordova plugin add cordova-plugin-file

拍摄视频

const captureSuccess = (mediaFiles) => {
  path = mediaFiles[0].fullPath
  dealWith(path)
};

const captureError = (error) => {
  console.error(error)
};

navigator.device.capture.captureVideo(captureSuccess, captureError, { limit: 1 })

这里 mediaFiles[0].fullPath 是以 file:/// 开头的 URL,用 resolveLocalFileSystemURL(由 cordova-plugin-file 提供 API 支持)获取文件对象:

const getLocalFile = (url, onSuccess, onError) => {
  /* global resolveLocalFileSystemURL */
  resolveLocalFileSystemURL(`${url}`, (entry) => {
    entry.file((file) => {
      const fileType = file.type
      const fileReader = new FileReader()
      fileReader.onload = function () {
        onSuccess(new Blob([new Uint8Array(this.result)], fileType))
      }
      fileReader.onerror = function () {
        onError(this.error)
      }
      fileReader.readAsArrayBuffer(file)
    }, (err) => { onError(err) })
  }, (err) => { onError(err) })
}

其中 onSuccess 以 FormData 的方式上传文件:

const uploadVideo = (file) => {
  const formData = new FormData()
  formData.append('file', file)
  return axios.post(postURL, formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}

如果 resolveLocalFileSystemURL 调用失败,onError 会让用户选择需要上传的视频:

const onError = (err) => {
  console.error('Can not get File object from URL:' + JSON.stringify(err))
  videoFileInput.addEventListener('change', function () {
    uploadVideo(this.files[0])
  })
  videoFileInput.click()
}

其中 videoFileInput 是 DOM 对象:

<input type="file" style="opacity:0">

扩展阅读:

Cordova 开发环境变量设置

Android 编译及其它工具

export ANDROID_HOME=$HOME/Android/Sdk

export PATH=$ANDROID_HOME/tools:$PATH
export PATH=$ANDROID_HOME/platform-tools:$PATH
export PATH=$ANDROID_HOME/build-tools/28.0.3:$PATH
export PATH=$PATH:$HOME/gradle-5.1/bin

node

export PATH=$PATH:$HOME/node/bin

修改 Linux 用户名

重命名登录名,该步骤会处理 /etc/passwd、/etc/shadow、/etc/group、/etc/gshadow 中的用户名的替换,但不会处理 /etc/subuid 和 /etc/subgid:

usermod -l newname oldname

移动用户主目录:

usermod -d newhome username

需要注意的是,系统中涉及用户名的配置覆盖面可能非常广,基于用户主目录的配置和缓存文件也非常多,要把它们全部找出来几乎是不可能的事,修改用户名和用户主目录就要做好系统中出现各种问题的心理准备。可以排查一下常见的地方:

修改 Linux hostname

Systemd 的 hostnamectl set-hostname new_hostname 差不多等价于 hostname new_hostname 和编辑 /etc/hostname。除此之外,还需要编辑 /etc/hosts,确保新的 hostname 映射到正确的 IP 地址。

HTTP POST 参数在请求的哪个部分

POST 参数可以放在 URL 里面,但数据应该放在请求的 Body 里,其类型可能是 application/x-www-form-urlencodedapplication/json 或者 multipart/form-data 等其它格式,如果是 application/x-www-form-urlencoded,则和 query parameter 差不多,如下所示:

POST /path/script.cgi HTTP/1.0
From: frog@jmarshall.com
User-Agent: HTTPTool/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 32

home=Cosby&favorite+flavor=flies

这篇回答

模拟 Django migration

如果外部应用修改了 Django 使用的数据表的结构,修改了 Django 应用的模型代码以反映其变化,运行 makemigrations 之后不要运行 migrate 命令,而是手动在 django_migrations 表中插入一行记录,让 Django 认为该数据库迁移已经运行了,如下所示:

INSERT INTO `django_migrations` (`app`, `name`, `applied`) VALUES ('api', '0003_auto_20190524_0716', '2019-05-24 07:18:00');

这样 Django 既不会抱怨有数据库迁移没有运行,也避免了 Django 修改数据表可能引起的问题。

Linux fontconfig 常用命令

fc-list  # 列出系统中的所有字体
fc-list -f '%{file}\n' :lang=zh  # 列出系统中的所有中文字体文件
fc-match -s monospace:charset=1F4A9  # 列出 Unicode 字符为1F4A9 匹配的所有等宽字体

pandoc docx 文档模板

使用 pandoc 把文档转化成 docx 格式的时候,可以使用 --reference-docx 指定模板文档,通过修改模板文档的样式改变输出的 docx 文档的样式。下面的描述和官方文档(见 --reference-doc=FILE 部分)中的说法不一致,有可能是因为我使用的 pandoc 是 1.19.2.4(没有找到文档),而官方文档是 2.x 版的。

生成 reference.docx:

# -o 参数没有作用,必须重定向,这一点和 manpage 里的描述也不一致
pandoc --print-default-data-file reference.docx >custom-reference.docx

打开 custom-reference.docx 修改文档中各类内容(如标题1、标题2、正文、超链接、表格)的样式,不是文档的内容,不清楚的参考 Word 官方文档。在 pandoc 命令行加上 --reference-docx=custom-reference.docx 就可以了。

npm random 软件包需要浏览器支持 ES6 Proxy 特性

random 软件包的依赖包 ow-lite 使用了 ES6 Proxy 特性,该特性无法 polyfill,需要浏览器支持。

Pelican 小结

如果站点的页面较多,Pelican 的 page hierarchy 插件可以使生成的 pages 页面不维持其源文件的文件结构,并且支持父页面和子页面功能。用 Google 搜索“关键字 site:站点名”或者 git grep 源码目录 要比自己在站点上的 JavaScript 插件靠谱,因此这里基本没有 Sphinx 的用武之地。

所有的文章的 URL 都是 posts/<slug> 的形式,如果有两篇文章的 slug 碰巧相同导致生成的 HTML 文件会互相覆盖,pelican 会给出警告信息。但如果文章和页面的 HTML 文件会互相覆盖,pelican 不会提示任何信息,因此所有页面的 URL 都是 pages/<path>/<to>/<page>,和文章的根路径隔开。

Python 软件包管理

从 setuptools 到 easy_install 一直到 pip,都没有像样的软件包依赖关系的管理,直到 pipenv 的出现,其依赖关系的管理才赶上 npm。pipenv 创建依赖关系的时候好像会把所有的软件包检查下载一遍,导致这个过程非常慢,而且没有用户反馈,因此用 pipenv 安装软件包的时候最好使用国内的 pypi 镜像,万一卡了基本可以排除网络原因,最重要的是不使用镜像的时候我从来没有等到 pipenv 命令运行结束。

软件包依赖管理的一个重要需求是查询软件包之间的依赖关系,可以通过 pipenv graph 命令来实现或者使用 pipdeptree

在 Python 软件包管理的问题上,PYPA写了一个指南,建议阅读。

Development mode 是一种把正在开发的软件包安装到 Python 运行环境中的技术,如果有多个存在依赖环境的 Python 软件包在开发中,这种方式可以使下游软件包实时看到上游的软件包的改动,也可以用于消除同一个软件包中的 test 和 lib 模块之间的路径依赖。

Linux 测试服务安装小结

Docker 的版本有些混乱,从 1.x 直接变成 17.x,在官方文档中就找不到 17.x 之前的历史了,其实他们只是切换到基于年份的版本号官方文档建议删除其它版本的 docker,安装 docker-ce,个人认为发行版自带的 docker 用得挺好,没有必要多此一举。

CentOS 软件源如 EPEL 可以直接从 yum 安装,因为 yum install 支持 URL 形式的参数。

yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum install -y https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm

yum whatprovides <path> 可以用来哪个软件包中存在指定的路径,支持通配符,例如下面的命令可以查到 yumdb 命令来自于 yum-utils 软件包。

yum whatprovides '*bin/yumdb'

但是这个命令搜索的是软件源中所有的软件包,而非系统中已经安装的软件包,例如 yum whatprovides $(which nginx),能查到 nginx 包中包含该路径,但也许系统中的 nginx 是用户自己编译安装的,要确切的查询系统中的 nginx 来自于哪个已安装的包,应该使用 rpm -qf $(which nginx)

rpm -qf相反,rpm -ql <package>用来列出系统中已安装的软件包中的所有文件。要列出没有安装的软件包中的内容,需要使用 yum-utils 的 repoquery 命令,或者 dnf:

repoquery -l time
dnf repoquery -l time

yum list <package>用来列出软件包的信息,如果软件包已经被安装,则还会出现在 Installed Packages 中,否则会出现在Available Packages 中,如果软件包不存在,会显示 No matching Packages to list。加入 --showduplicates 参数后,该命令会列出启用的软件源中所有版本的软件包。

yum list --show-duplicates docker

会显示:

docker.x86_64  2:1.13.1-75.git8633870.el7.centos  extras
docker.x86_64  2:1.13.1-84.git07f3374.el7.centos  extras
...
docker.x86_64  2:1.13.1-96.gitb2f74b2.el7.centos  extras

然后用户可以输入命令选择安装非最新版本的软件包:

yum install docker-1.13.1-84.git07f3374.el7.centos

注意这里的软件版本既不包括第1列 docker 后面的 CPU 架构 .x86_64,也不包括第2列版本信息最前面的 2:

Oracle JDK 和 OpenJDK 有什么区别?根据这篇文章,Oracle JDK 基于 OpenJDK,Oracle JDK 比 OpenJDK 性能好,至于兼容性,两者都通过了 TCK 验证,应该不会有问题。

禅道官方提供的 rpm 安装包和 CentoOS 7的系统文件存在路径冲突,并且它依赖于 Apache httpd,其实未必,个人偏好源码安装,然后像其它 php 应用(如 wordpress)一样配置。

安装扩展模块的时候,可以用 php -m 命令列出当前系统中已存在的模块,因为有的模块已经编译进 php 中,不需要再找安装包。

HTML 表格内文字换行

如果表格内某列有很长的文字,但不希望该列占用太多宽度,以免把后面的列挤到页面之外,可以指定该列文字换行:

white-space: normal;

这篇回答给出了兼容旧浏览器的解决方案:

.wrapword {
  white-space: -moz-pre-wrap !important;  /* Mozilla, since 1999 */
  white-space: -webkit-pre-wrap; /*Chrome & Safari */
  white-space: -pre-wrap;      /* Opera 4-6 */
  white-space: -o-pre-wrap;    /* Opera 7 */
  white-space: pre-wrap;       /* css-3 */
  word-wrap: break-word;       /* Internet Explorer 5.5+ */
  word-break: break-all;
  white-space: normal;
}
<table style="table-layout:fixed; width:400px">
  <tr>
    <td class="wrapword">
    </td>
  </tr>
</table>

Python 3 str 和 bytes 类型互相转化

通过 str.encode()bytes.decode()

Node.js 只有异步代码运行时会不会退出

取决于是否有定时器、异步 IO 在运行,如果有,node.js 不会退出,否则即使有异步代码运行,node.js 也会退出。见这个 issue这篇回答这篇回答

Vue router 导航

此处共有3个 API:router.push()router.replace()router.go(),这3个函数分别对应于 window.history.pushState()window.history.replaceState()window.history.go()

router.go() 和另外两个 API 有显著的差别,即后者会改变浏览器的历史栈,前者不会,它仅在浏览器的历史栈中前后移动。其参数也有显著的差异,router.push()router.replace() 第一个参数是目标位置的确切信息,router.go() 的唯一一个参数是目标栈相对于当前栈的位移。

当然还有2个 API:router.back()router.forward(),它们其实是 router.go(-1)router.go(1) 更友好的名字,对应于 window.history.back()window.history.forward(),或者浏览器的“后退”和“前进”按钮。只要熟悉浏览器历史操作 API,理解上述内容便是小菜一碟。

Django 修改用户密码

下面是错误的做法:

user = some_way_to_get_user()
user.password = new_password
user.save()

上面的做法会把 new\_password 明文存储在数据库中,但是 Django 不明文存储用户密码。这里要把 new\_password 转化成 Django 支持的存储格式,因此必须调用 set_password()

user = some_way_to_get_user()
user.set_password(new_password)
user.save()

产生这一错误印象的源头可能是 Django 在创建用户时,通过 password 参数传递密码:

User.objects.create_user(username=username, password=password)

注意这并不等同于 user.password = password

如果对密码存储和用户验证有更高的要求,建议阅读:更新用户密码的存储方式DRF 自定义用户验证Django 用户验证

开源简谱输入方案

剑桥大学的 Silas S. Brown 写了一个将简谱记号转化成 LilyPond 输入文件的 Python 脚本,并且写了一个介绍网页,国内的皮波迪网友写了中文介绍,并且提供了一个很好的例子,对照这个例子和用法的介绍,即使像我这样没有音乐基础的人,也能输入简单的乐谱。

用这种方法生成的简谱,拍子是放在音符的前面,并且和音符位于同一行,有时候我们需要把拍子和大调放在同一行,在新行开始音符,这时可以修改生成的 LilyPond 输入文件,在 \\time 命令的同一段中添加下面的命令隐藏音符前面的拍子:

\omit Staff.TimeSignature

\\mark 命令改为:

\mark \markup {
    \pad-around #3 \line {
        1=D
        \hspace #1
        \raise #1.0 \number \fontsize #-1 \column {
            \override #'(thickness . 2) \underline 4
            4
        }
    }
}

这一段代码需要参考的文档有:\\mark\\pad-around\\line\\hspace\\raise\\column\\number\\fontsize\\underline

这里给标注加了 padding,但所有音符之间的行距都会加大,可能所有音符本质上是同一行,只是因为排版的原因被分成了多行。

JavaScript 数组排序

Array.prototype.sort() 在不提供 compareFunction 参数的情况下,把数组中的每一个元素转化成字符串,然后按照字符串 UTF-16 编码升序排列(除了 undefined)。如果需要降序排列,可以结合 Array.prototype.reverse(),或者提供 compareFunction 参数。

如果提供 compareFunction 参数,compareFunction(a, b) 必须返回 3 种数值,如果 ab 之后,返回大于0的值,如果 ab 之前,返回小于0的值,如果 ab 的次序相同,返回0。我搞错过,很多其他人也搞错了。因为排序的重要性,贴出几篇值得参考的回答([(a, b) =&gt; (a&lt;b)-(b&lt;a)](https://stackoverflow.com/a/39751889/9634290),compareFunction 详解)。

总之,如果 compareFunction 不能正常工作,就不要指望 Array.prototype.sort() 能按照用户的意愿把数组排序好,尤其是 compareFunction 可能遇到本质上无法排序的参数,如 undefinedNaN,如果数组会包含这样的值,写出能正常工作的 compareFunction 可以算作一项挑战。

支持该特性的浏览器都遵循规范,但规范中并没有规定次序相同的元素之间的顺序必须如何,因此即使 compareFunction 可以正常工作,不同浏览器如 Firefox 和 Chrome 排序的结果仍可能有差异,甚至同一浏览器(Chrome)对不同长度(10个元素以内和10个元素以上)的数组的排序结果看起来都不一样。参见stable sort and unstable sort

这个函数会改变原来的 array,如果不想改变原来的 array,先调用 Array.prototype.slice() 复制 array 再排序。

很多情况下,数组元素的排序取决于元素经过计算得到的有序值,这种计算称为 keyFunction,lodash 的 sortBy() 函数要比 Array.prototype.sort() 更适合这种用途。

清空 Django 数据表

CSS 封装

构建 JavaScript 对象

一个 JavaScript 对象可以看作一组键值映射,一个 JavaScript 对象中的所有键都是互不相同的字符串。写成代码的形式就是3条规则:

例如:

{
    'a': 1,
    '11': 2,
    '011': 3,
    'x//y': 4,
    '3.142': 5
}

在这里,因为键 'a' 的名称是合法的变量名,其引号可以省略,键 '11' 和键 '3.142' 可以由数值11和3.142转化成字符串得到,也可以写成数值的形式,而键 '011''x//y' 其名称不是合法的变量名,也不能由任何数值转化成字符串得到,必须写成字符串的形式,因此上述表达式等价于:

{
    a: 1,
    11: 2,
    '011': 3,
    'x//y': 4,
    3.142: 5
}

es6 在键中引入了变量名的简写形式,如果键的名称是当前作用域中的变量,并且对应的值恰好也是该变量的值,该键值对就可以直接写成变量名,即把 name: name 简写成 name

es6 还允许通过计算生成的键名,只要用方括号 \[\] 把相应的表达式括起来,放在键的位置上。即用定义对象的时候直接输入表达式键名(var object = { \[expression\]: value })代替先定义对象(var object = {})再赋值(object\[expression\] = value)的二步法。

最后,不能在 JavaScript 语句的位置上放一个光秃秃的对象表达式,因为 JavaScript 会把它解释成语句块,在 REPL 中输入 { a: 1 } 相当于:

{
    a:
    1
}

会得到1,输入 { a: 1, b: 2 } 会得到语法错误。这种情况下,应该用圆括号 () 把对象表达式括起来

elm 调查

elm 没有类似于 redux、vuex 的 store pattern,因为 elm 的核心不是组件,而是函数,这篇讨论引用了一段原来存在于 elm 文档中的话(现在已经不在了):

If you are coming from JavaScript, you are probably wondering “where are my reusable components?” and “how do I do parent-child communication between them?” A great deal of time and effort is spent on these questions in JavaScript, but it just works different in Elm. We do not think in terms of reusable components. Instead, we focus on reusable functions. It is a functional language after all!

如果 elm 中的任何部分需要拆分,作者也给出了建议

elm 的不足:

批评及其反驳

elm 并非没有 Runtime Error,其实无所谓,个人认为,没有 Runtime Error,只能当理想,不能当真。

npm 技巧

以 @ 开头的包称为 scoped packages,有两个好处,明确发布者和避免命名冲突。见这篇回答

npm 安装的时候指定包的版本 \^1.0.0,\~1.2.1 等属于 npm semantic versioning,\^1.0.0 表示 major version 匹配,minor version >= 0 的包,patch version 任意,而\~1.2.1 表示 major version 和 minor version 匹配,patch version >= 1 的包,npm semver 计算器提供了很形象的交互式界面。输入命令为 npm install <package>@<semver> 可选择安装特定版本的包。

npm list 可以查看本地安装的包的信息,包括版本和被依赖的路径:

npm list <package>  # 列出安装在项目中的 <package> 的信息
npm list --depth=0  # 列出项目直接依赖的包
npm list -g <package>  # 列出全局安装的 <package> 的信息

npm peerDependencies 一般用于插件指定宿主的版本,详见这篇文章

data url 格式

完整格式如下:

data:[<mime type>][;charset=<charset>][;base64],<encoded data>

常用的前缀是:

data:image/gif;base64,

qpdf 从文件读取加密参数

命令行中的加密参数没有秘密可言的,qpdf 提供了从文件读取加密参数的方式

qpdf @encrypt_params.txt test_input.pdf test_passwd.pdf

但文件中的加密参数必须换行,每一行放置一个参数,如下所示。

--encrypt
helloworld12345
helloworld12345
40
--

阻止访问特定 IP

Linux 中可以用 iptables 阻止对特定 IP 的访问,分为2种,一种是拦截发送往指定 IP 的包:

sudo iptables -A OUTPUT -d <address> -j DROP

一种是拦截接收自指定 IP 的包:

sudo iptables -A INTPUT -s <address> -j DROP

上述命令中将 -A(添加规则)改为 -D(删除规则)可以取消对特定 IP 的访问限制。其余参数 -d 表示目标站点,-s 表示来源站点,-j 表示对网络包采取的动作(DROP 表示丢弃网络包,即拦截访问),INPUT 表示流入,OUTPUT 表示流出。

Apache2 mod_rewrite

RewriteRule 和 RewriteCond 的匹配规则是,一条 RewriteRule 和它前面的一条或多条 RewriteCond 匹配,RewriteCond 不能匹配多条 RewriteRule。证据之一是文档中 RewriteCond 部分的这段话:

The RewriteCond directive defines a rule condition. One or more RewriteCond can precede a RewriteRule directive. The following rule is then only used if both the current state of the URI matches its pattern, and if these conditions are met.

以及 mod_rewrite 介绍中对于正则表达式反向引用的说明。

最有力的证据是下面的例子:

RewriteCond  "%{HTTP_USER_AGENT}"  "(iPhone|Blackberry|Android)"
RewriteRule  "^/$"                 "/homepage.mobile.html"  [L]

RewriteRule  "^/$"                 "/homepage.std.html"     [L]

只有 RewriteCond 只能匹配一条 RewriteRule 的情况下才能解释得通。

由于 RewriteRule 是 mod_rewrite 中真正干活的部分,可以把它看作语句,RewriteCond 则是语句的修饰,而非控制流。一个 RewriteEngine 就是一条条 RewriteRule 不停执行的过程,直到执行完毕或者某一条 RewriteRule 触发了 END,终止这个过程。

socket 文件不可放在 /tmp 目录中

用于进程间通信的 unix socket 文件不可以放在 /tmp 目录中,至少在 Fedora 和 CentOS 中,进程只能看到自己创建的 socket,其余进程访问的时候就找不到文件。详见这篇回答这篇回答

开始一个 Quasar 项目

开始一个 Quasar 项目非常简单,假设 node 已经安装,只要运行两个命令,按照提示操作就可以了。

npm install -g quasar-cli
quasar init <folder-name>

在 Android 环境已经准备好的情况下,添加 Android 编译功能也不难:

npm install -g cordova
quasar mode -a cordova
cd src-cordova
cordova platform add android

Vue 调试插件因为没有签名被禁用

最近的火狐浏览器禁用了没有签名的浏览器扩展,包括 Vue 调试插件,在 Vue 发布签名版的调试插件之前,只能在火狐浏览器中输入 about:config,搜索 xpinstall.signatures.required 选项,并且将其设置为 false,才可以使用 Vue 调试插件。

使用 ImageMagick 转化 SVG 注意事项

生成的图像和浏览器中看到的不一致,安装 Inkscape 解决了这个问题,原因不明。

用户登录时启动 VirtualBox 虚拟机

把虚拟机快捷方式添加到启动文件夹

在 VirtualBox 虚拟机中配置 Samba 服务

首先要为虚拟机添加一块 Host-Only 连接的网络适配器,默认的网络连接是 NAT,主机中看不到(参考这条回答)。

在虚拟机中安装 samba server 软件(虚拟机操作系统为 Ubuntu 18.04 amd64):

sudo apt update
sudo apt install samba

在 /etc/samba/smb.conf 配置文件末尾添加如下共享目录设置

[sambashare]
    comment = Samba on Ubuntu
    path = /home/<username>/sambashare
    read only = no
    browsable = yes

重启 samba 服务,应用配置:

systemctl restart smbd

为 samba 共享服务添加用户

sudo smbpasswd -a <username>

此处的 username 应该为系统中存在的用户,因为前面把共享目录放在了某位用户的主目录中,这里也应该把同一位用户添加到 samba 用户中,以免折腾文件和目录的权限设置,该命令运行完毕就可以通过该用户名和新添加的密码来访问共享目录了。

urljoin 使用技巧

要在 base url 后面添加路径内容,确保 base url 以“/”结尾。

>>> urljoin('http://a.b.c/d', 'e')
'http://a.b.c/e'
>>> urljoin('http://a.b.c/d/', 'e')
'http://a.b.c/d/e'

Selenium 使用小结(Python)

连接 Firefox 浏览器:

from selenium import webdriver
driver = webdriver.Firefox()

浏览页面:

driver.get("http://www.python.org")

查找单个元素(使用 find_element_by_*):

toc = driver.find_element_by_class_name('toc-B9k')

查找子元素:

link_root = toc.find_element_by_class_name('tree-view_container')

查找多个元素(使用 find_elements_by_*):

parts = driver.find_elements_by_css_selector('[data-text="true"]')

选择 Firefox profile(指定 firefox_profile 选项为 profile 目录的绝对路径或者为一个 FirefoxProfile 对象):

fp = webdriver.FirefoxProfile('/Users/<username>/Library/Application Support/Firefox/Profiles/71v1uczn.default')

driver = webdriver.Firefox(fp)

# 或者
# driver = webdriver('/Users/<username>/Library/Application Support/Firefox/Profiles/71v1uczn.default')

获取元素的属性值:

href = link.get_attribute('href')

获取元素的内部文本(仍使用 get_attribute):

title = link.get_attribute('innerText')

Selenium 注意事项

Ubuntu 中需要下载安装 geckodriver 才可以使用 webdriver.Firefox(),仅安装 firefoxdriver 安装包(不确定其用途是什么)是不够的。

Selenium 无法准确判断 JavaScript 脚本的执行情况,如果要等待 JavaScript 执行完成,可以调用 sleep(),或者使用 Selenium explicit wait不推荐使用 implicit wait)。

pandoc 无法把 pdf 文件转化成其它格式

根本原因在于 pdf 是一种布局格式,而非结构化的文档格式。参考这个回答

冷盘冷电和热盘热电

冷盘冷电指服务器更换硬盘设备需要关机重启,热盘热电指服务器可以在运行的状态更换硬盘设备。

LOM 网卡技术

LOM 全称 LAN on Motherboard,由 Intel 推出,直接在主板上集成网卡,无需占用一个插槽安装网卡,兼容传统以太网的 10M/100M 接口,也可以直接升级到 1000M 的高速网络接口,支持 Windows、Linux 等众多操作系统。

禁用 q-input 用户输入

使用 disable 属性而非 disabled,后者可以改变外观,但用户仍可输入内容。

Seahorse 无法导入 openssh 密钥

表现是点击导入密钥,可以打开,输入 passphrase(如果有)后会显示密钥信息,但导入按钮显示为禁用。临时解决方法是把密钥的私钥和公钥复制到 .ssh 目录下,seahorse 就会自动导入该密钥。

DRF 自动表单验证

写入 DRF 的 SlugRelatedField 的值会被自动转化成目标模型的一个实例,如果找不到与写入值对应的实例,DRF 会返回 400 错误,无需写 validator 来实现这一功能。

如果一个 Field 指定了 many=True,DRF 只会接受数组,如果不是数组,便会返回 400 错误。

规避 Adobe Flash Player 大陆版

升级了 Adobe Flash Player 后,第二天就收到一条国内网民司空见惯的骚扰弹窗,查了一下,原来是 Adobe Flash Player 在中国大陆发行后,已经变成了流氓软件,这年头基本上用不到 Adobe Flash Player,卸载,需要时再想办法安装国际版的,以免中招。

Django CharField max_length 的含义

max_length 指的是字符数,不是字节数,例如在 MySQL中,CharField(max_length=100) 被转化成 varchar(100),即100个字节的字符串。max_length 的值被 Django 用来验证字符串的长度,同时也会影响数据库中可实际存储字符串的长度。

Django Field 属性

以下是 Django Field 的常用属性:

下面的流程会对如何使用这些属性有所帮助。

创建 Django Model 时应该考虑是否需要自定义主键,如果需要,创建 Field 并指定 primary_key=True。

如若不需要,Model 会自己创建一个:

id = models.AutoField(primary_key=True)

创建 Django Model Field 的时候,首先设置 verbose_name,起到注释的作用,接下来提一些问题:

如果这个 Field 指向其它 Model,需要考虑本模型和目标模型的对应关系:

对于 ForeignKey 和 OneToOneField,需要设置 on_delete 属性,一般设置为 on_delete=models.CASCADE,明确本模型从属于目标模型,如果目标实例被删除,也删除本实例。

还需要考虑反向引用的问题,反向引用指的是,根据已定义的对应关系,在目标模型中自动创建到本模型的引用。如果不需要在目标模型中创建反向引用,可以明确设置 related_name='+',否则 related_name 的设置取决于本模型是否是抽象模型,如果是抽象模型,最好设置为 '%(app_label)s_%(class)s_xxx',避免不同子模型反向引用的命名冲突,如果不是抽象模型,直接设置成想要的名字就可以了。

Django 禁用 admin 站点的思路

从上往下越来越彻底。注:只是思路,没有实验验证,也没有上网验证。

Vue Template 中慎用表达式

在不该出现表达式的地方使用了表达式会导致模板编译错误,如果是在运行的时候编译模板(quasar dev 环境),则表现为调试 console 迟迟不能准备好,陷入死循环,但 CPU 占用未达到 100%。

HTML 中插入 JavaScript 的建议

原因是浏览器支持不好,在《JavaScript 高级编程》中详细讨论了使用内嵌脚本可能引起的 HTML 解析问题,以及相应的 hack。明确指出不是所有浏览器都支持 defer 的顺序加载,async 则明确了不按顺序加载。

修改 wsgi 站点代码

如果想让修改立即反映到站点,最靠谱的方法是重启 apache

DRF 创建额外的 _url 链接

如果使用 HyperlinkedModelSerializer,所有指向其它模型的字段都显示为 url,如果要把这些显示为链接的字段名改为以 _url 结尾,可以自定义 HyperlinkedRelatedField 的 source 参数:

user_url = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True, source='user')

如果这个字段对应着多个外部模型的实例,不想显示多个链接,只显示一个通过 query string 进行筛选的链接,则需要使用 SerializerMethodField 自定义链接字段:

class TeamSerializer(HyperlinkedModelSerializer):
    ...
    members_url = serializers.SerializerMethodField()

    def get_members_url(self, obj):
        return get_query_url(
            view_name='staff-list',
            request=self.context['request'],
            query_params={
                'team': obj.id,
            },
        )

def get_query_url(view_name, request, query_params):
    return '{url}?{qs}'.format(
        url=reverse(viewname=view_name, request=request),
        qs=urlencode(query_params),
    )

调试 Cordova 应用的 JavaScript 错误

Chrome 远程调试需要 debug 版本的应用,目前在 AndroidManifest.xml 的 application 标签中加上 android:debuggable="true" 没有用,而且还会触发 HardcodedDebugMode 错误,Cordova 会提示这么写容易不小心在发布的应用中混入程序的调试信息。正确的做法是在编译命令中加入 --debug 选项,quasar build -m cordova -T android --debug 会调用 cordova build --debug android,这样编译出来的应用的 WebView 窗口才可以被 Chrome 看到。但是 Chrome 远程调试的效果不理想,在 adb logcat 中输出的 JavaScript 错误信息并不能在远程调试的 console 中看到。

好在 debug 版本的应用的 JavaScript 没有经过最小化和压缩处理,非常容易定位到出错的位置,现在确定是 vendor.js 中的箭头函数引发了语法错误(Android 4.4 的 WebView 还是 Chromium 30)。现在在尝试让 babel 把 vendor.js 中的代码转译一下。

后来安装了 targets_webpack_plugin,并在 webpack 配置的 plugins 列表中添加

new TargetsPlugin({
    browsers: ['last 2 versions', 'chrome >= 30']
})

但又遇到 Proxy not defined 的问题,到现在为止遇到的问题都是 Luxon 引起的。

JavaScript Promise

Promise 最重要的属性是 then,把它做对了,Promise 也就做对了。这篇文章指出了 Promise 的目的,以及 Promise 实现的问题。Promise A+ 则给出了关于什么是 Promise 的严格标准。

应该使用哪一种 Promise,ES6es6-promisersvpq、Bluebird?

应该了解使用 Promise 的 Pattern,见这篇文章MDN 讲解

Vue 组件的插槽

如果想在组件中插入一段 HTML 代码:

<component>
  <div>More html content</div>
</component>

可以使用插槽来实现,在组件的 HTML 模板中添加一对 <slot> 标签(可以在其中添加默认内容):

<template>
  <slot>Default content</slot>
</template>

如果 <component> 和 </component> 之间有内容,则 <slot> 标签之间的部分被该内容替换,否则显示默认内容。

Android WebView 兼容性

从 Android 4.4 KitKat(sdk version 19)开始,使用 chromium 作为其默认的 webview,KitKat 将使用 chromium 30,并且不会升级。见这篇报道

Django INSTALLED_APPS

在 Django 中使用一个应用 myapp,需要把它放在 settings 模块的 INSTALLED_APPS 列表中,这里可以直接写应用的名字:

INSTALLED_APPS = [
    ...
    'myapp',
    ...
]

也可以写应用的一个配置类(django.apps.AppConfig 的子类):

INSTALLED_APPS = [
    ...
    'myapp.apps.NiceConfig',
    ...
]

对于第一种情况,如果 myapp 的 __init__.py 中定义了 default_app_config:

default_app_config = PrettyConfig

则使用 default_app_config 指定的配置类 PrettyConfig。否则使用 AppConfig。这是旧版的写法,这种情况下应用的配置是固定的。

Django 1.7 以来就推荐第二种写法,用户可以根据其需求自由地选择 PrettyConfig 或者 NiceConfig,

筛选 adb logcat 的输出

如果不写筛选参数,adb logcat 会输出所有内容,写了筛选参数,按照筛选参数进行筛选后输出。

adb logcat 的输出可以有许多不同的频道,每个频道都用一个 tag 表示,写到筛选参数中的 tag,会按照选项指定的方式进行筛选,没有出现在筛选参数中的 tag 会输出其全部内容。

首先加上 *:S 筛选参数,* 匹配所有的 tag,S 表示没有输出,合起来就是默认不显示任何 tag 的输出。然后只对关心的 tag 写参数,如 chromium:I 表示只显示 chromium tag 的 I 级别以上的输出,完整的命令就是 adb logcat chromium:I *:S。

django-cors-headers 设置的注意事项

CORS_ORIGIN_WHITELIST 和 CORS_ORIGIN_REGEX_WHITELIST 有明显的区别,前者匹配 源站的域名和端口号(不包括 http 或 https 部分),如果要匹配 http://127.0.0.1:8080,必须写成 127.0.0.1:8080,不存在只匹配 http://127.0.0.1:8080 但不匹配 https://127.0.0.1:8080 的情况,后者匹配源站的源(包括 http 或 https 部分),如果要匹配 http://127.0.0.1:8080,则至少有一个正则表达式匹配 http://127.0.0.1:8080 的全部。

判断源是否在白名单中的代码如下:

    def origin_found_in_white_lists(self, origin, url):
        return (
            url.netloc in conf.CORS_ORIGIN_WHITELIST
            or (origin == 'null' and origin in conf.CORS_ORIGIN_WHITELIST)
            or self.regex_domain_match(origin)
        )

Django 部署

有关 CSRF 的几个链接收藏

在代码的一些区域关闭质量检查的方法

pylint:参照这篇回答,在代码块中单独插入一行注释(关闭代码块中该行之后的代码检查)或者在代码行后添加注释(关闭该行的代码检查),注释内容如下:

# pylint: disable=no-member, line-too-long

eslint:把需要关闭检查的代码区域用两条注释包起来:

/* eslint-disable no-self-compare */
const isNaN = v => v !== v
/* eslint-enable no-self-compare */

Django 字符集设置

Django 完全支持 unicode 文本,因此,在 Django 程序内部使用 Python3 str 类型传递文本总是没有错的。任何地方遇到非 unicode 文本(bytestring),第一时间把它们转化成 unicode 文本(str),如果应用不这么做,Django 就会代劳,但它会假定该 bytestring 是 utf-8 编码。

至于 Django 应用和数据库的接口,只要确保创建的数据库可以存储 unicode 文本

如果需要向客户端返回非 utf-8 编码的文本,应该修改 DEFAULT_CHARSET 设置

URL、URI 和 URN

简单地说,URL 包含了如何定位这个资源的全部信息,URN 不包含此类信息。URL 和 URN 都是 URI。

URI 和通常意义上的资源的对应关系比较松散:

URL、URN 和 URI 的全部作用只是定义了一套语法,规定了要按照什么方式来书写资源的标识符,至于语义,则由具体的实现(http、mailto、isbn)来负责。一种 URI 对用户的帮助有多大,完全取决于实现的优劣。

跨域资源共享(CORS)

浏览器支持:IE10 以上的所有主流浏览器,关键是服务器支持,判断的依据是服务器受到跨域请求的时候,在返回的消息头中包含 Access-Control-Allow-Origin 字段。

浏览器对简单请求和非简单请求的处理是不一样的,对于简单请求,浏览器会在请求的消息头中包含一个 Origin 字段,标明发送请求的站点。对于复杂请求,除了在正式请求中包含 Origin 字段之外。还会发送一次 OPTIONS 预检请求(preflight),目的是询问服务器它即将发送的请求是否被允许。

以上服务器和浏览器对跨域共享的处理是自动完成的,对用户透明。

如果浏览器不支持 CORS,可以使用 JSONP 等其它方式

建议看阮一峰的博客文章,把原理讲得很清楚。以及这篇 MDN 文章

跨域共享时,发送请求的利用客户浏览器非法获取第三方站点上的资源,甚至攻击第三方站点,而第三方站点也可能通过返回恶意的内容来攻击发送请求的站点。跨域请求如果处理不好,会引发许多安全问题,具体见这篇文章

最后略提一下 Cordova,由于 Cordova 默认打开了 <access origin="*" />,可以向任何域发送请求,在服务端看 Cordova 应用发送过来的请求的消息头中也没有 Origin 字段。因此默认不需要为 Cordova 设置 CORS。

JavaScript 札记

转化成 number 或者 int32

o.length >>> 0 的含义

x - 0 // 把 x 转换成 number 类型
~~x  // 把 x 转换成32位整数
x >>> 0  // 把 x 转换成31位非负整数

转换成 boolean 类型

!!x  // 等价于 Boolean(x)

MDN 文档有相关代码,看到许多举例 !!"false"反对的,不明白为什么这么多人希望一个长度不为0的字符串是 false。JavaScript 中有6个值是假值:false、0、NaN、空字符串("")、undefined、null,其余均为真值,包括空数组([])、空对象({})、new Boolean(false) 等所有对象。

Number(null)  // 0
Number(undefined)  // NaN

以上运算规则见 JavaScript ToNumber

~~undefined  // 0
undefined >>> 0  // 0
undefined | 0

因为进行位运算之前,先把 undefined 转化成 number,结果是 NaN

~~NaN  // 0
NaN >>> 0  // 0
NaN | 0

然后把 NaN 转化成32位整数,规则见 JavaScript ToInt32,结果是 +0

Number.isNaN(x) 表示 x 的值是 NaNisNaN(x) 表示 x 不能被转化成合法的 number 类型。

String.prototype.replace

oldString.replace(pattern, string) 将 oldString 中和 pattern 匹配的部分替换成 string,返回替换后的结果,不改变 oldString 的内容(JavaScript string is immutable)。

Webpack alias

Webpack alias 会影响绝对路径导入的代码的位置,按照 alias 的源和目标的不同可分为多种情况,具体见 Webpack 文档提供的一张示例表。

简单的说,如果 alias 的源以 \\( 结尾,如 xyz\\),只改变 import 'xyz' 的含义,不改变 import 'xyz/some/module' 等语句的含义。如果 import 语句的含义没有被改变,则从 node_modules 目录下的相应模块中导入,如果 import 语句的含义被改变,则从 alias 目标所在的代码文件、或模块目录中导入。

Visual Studio Code alias 配置

配置 compilerOptions.paths:

  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "components/*": [
        "src/components/*"
      ],
      "@/*": [
        "src/*"
      ]
    }
  }

因为 paths 的是值相对于 baseUrl 的路径,因此 node_modules 中的模块路径中也要加上 node_modules

"_": [ "node_modules/lodash" ]

Jest alias 的配置参考这篇文章

Quasar 自动生成的代码的位置

Quasar 自动生成的代码位于项目根目录的 .quasar 文件夹中。

查看 Quasar 使用的 webpack 配置

quasar build 和 quasar dev 不提供输出类似配置信息的选项,用户可以在扩展 webpack 配置的钩子函数中输出配置信息。

Vue 发布的模块

在 Vue 的 dist 目录中可以看到许多发布文件,但 README 文档对它们做了比较详尽的说明,规则是名字中带 runtime 只包含运行时,不包含编译器,不带 runtime 的同时包含运行时和编译器,名字中带 common 的是 CommonJS 模块,带 esm 的是 ES 模块,两者都不带的是 UMD 模块。

在 Vue 的 package.json 中,定义了一系列模块接口:

  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js",
  "unpkg": "dist/vue.js",
  "jsdelivr": "dist/vue.js",
  "typings": "types/index.d.ts",

其中 main 是 CommonJS 模块 require() 导入的接口,module 是 ES 模块 import 导入的接口。

待解决的

Vetur 暂时还不支持重命名变量(#610#873),如果不使用 SFC ,Scoped CSS 是可以解决的,通过 BEM 或 css-in-js 库,但应该会失去 template 部分的自动完成功能,因此可以探索不使用 SFC 的方式,但使用时要权衡利弊,最好是只在 SFC 中包含 UI 的逻辑,其余逻辑移到 JavaScript 模块中完成。

是否可以让 vscode(vetur)检查不存在的 vue 组件,如果在 <template> 部分使用的 Vue 组件没有在 components 部分注册,代码运行时 Vue 会产生运行时错误,可否提示用户该组件没有注册?

Vetur 借助 eslint-plugin-vue 来检查 <template> 部分的标记,根据其文档,它能检查注册的组件是否没有被使用(vue/no-unused-components),不知道能不能检查使用的组件有没有注册。

vscode 不能识别导入的 Vue SFC 组件的类型,在输入导入组件的模块时,也不会给出 *.vue 文件路径的提示。这是 Vetur 的 bug

vscode 自动插入的 import 语句带有“;”,和 eslint 的代码风格检查冲突,能否控制 vscode 插入的 import 语句是否以分号结尾?

应该不行,根据文档,目前 Visual Studio Code 有3个选项控制自动导入:

vscode 自动插入的 import 语句有时候会位于 <template> 部分,正确的做法是在 <script> 部分的最前面插入 import 语句。

不确定如何解决。

和 vscode、Vetur 的模块引入问题还有,vscode 好像不能识别名称中带有 - 的模块,如 quasar-framework、quasar-cli。Vetur 能在 SFC 中识别导入的 SFC 的路径,但不能识别其类型。

不确定如何解决。

如果在 webpack 中使用了 alias,能否让 vscode 识别从 alias 模块中导入的内容?

可以,在 jsconfig.json 的 compilerOptions.paths 中定义相应的内容,分为2种,xxx 定义导入模块 xxx 时的 alias,xxx/* 定义导入模块 xxx 的子模块时的 alias。

建议参考 Vetur 文档内容处理与 Vetur 有关的问题。