ccrsky


  • 首页

  • 标签

  • 分类

  • 归档

  • 站点地图

解决Ubuntu中文乱码

发表于 2018-05-07 | 分类于 linux |

最近使用ubuntu系统搭建CI环境,发现不支持中文,中文显示???,解决步骤如下:

  1. 安装中文支持包language-pack-zh-hans

    1
    sudo apt-get install language-pack-zh-hans
  2. 修改/etc/environment

    1
    2
    3
    # 文件追加如下内容
    LANG="zh_CN.UTF-8"
    LANGUAGE="zh_CN:zh:en_US:en"
  3. 修改/var/lib/locales/supported.d/local

    1
    2
    3
    4
    en_US.UTF-8 UTF-8
    zh_CN.UTF-8 UTF-8
    zh_CN.GBK GBK
    zh_CN GB2312
  4. 执行命令

    1
    sudo locale-gen

至此中文乱码问题即刻解决~

koa实战

发表于 2018-04-19 | 分类于 node |

koa实战-快速打造一个CMS系统

koa介绍

基于Node.js平台的下一代web开发框架

由 Express 原班人马打造的 koa,致力于成为一个更小、更富有表现力、更健壮的 web 开发框架。 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升常用错误处理效率。Koa 不在内核中打包任何中间件,它仅仅提供了一套优雅的函数库, 使得编写 Web 应用变得得心应手。

node.js环境 版本v7.6以上,koa2

快速开始

1.koa安装

1
npm install koa

2.编写app.js

1
2
3
4
5
6
7
8
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
ctx.body = 'hello koa2'
})

app.listen(3000)

3.启动web服务

1
node app.js

koa中间件

koa对网络请求采用了中间件的形式处理,中间件可以介入请求和相应的处理,是一个轻量级的模块,每个中间负责完成某个特定的功能。中间件的通过next函数联系,执行next()后会将控制权交给下一个中间件,如果没有有中间件没有执行next后将会沿路折返,将控制权交换给前一个中间件

中间件

创建中间件

新建 md.js

1
2
3
4
5
6
7
module.exports = function () {
return async function(ctx, next) {
const startDate = new Date();
next();
console.log(`method: ${ctx.method} code: ${ctx.status} time:${new Date() -startDate}ms`);
}
}
使用中间件
1
2
3
4
5
6
7
8
9
10
11
12
const Koa = require('koa') 
const logger = require('./md')
const app = new Koa()

app.use(logger())

app.use(( ctx ) => {
ctx.body = 'hello world!'
})

app.listen(3000)
console.log('the server is starting at port 3000')

路由

原生实现路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use( async ( ctx ) => {
let url = ctx.request.url

if(url == 'index.html'){
fs.readFile('/view/index.html', 'utf8', ( err, data ) => {
if ( err ) {
ctx.body ='500';
} else {
ctx.body = data;
}
})
}

})

// 也可安装模板中间件koa-views,使用其他js模板引擎

页面渲染可使用ejs, Jade,Nunjucks等模板引擎。

使用路由中间件

1.安装koa-router

1
npm install --save koa-router

2.router使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');

// 定义路由规则
let home = new Router();
home.get('/', async ( ctx )=>{
ctx.body ='Hello world';
})

// 加载路由
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
console.log('server is starting at port 3000')
})

cookie/session

cookie使用

koa提供了从上下文直接读取、写入cookie的方法

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

koa2 中操作的cookies是使用了npm的cookies模块

1
2
3
4
5
6
7
8
9
10
11
12
// 响应头写入cookie
ctx.cookies.set('sid', 'xmly',{
domain: 'a.ximalaya.com', // 写cookie所在的域名
path: '/index', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date('2018-04-25'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
});

// 设置响应内容
ctx.body = 'response body';
koa-session

koa2原生功能只提供了cookie的操作,但是没有提供session操作,只能自己实现或通过第三方中间件实现,这里我们用到了koa-session。

1
npm install  koa-session
使用cookie存储session数据
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
// session config
const sessionCfg = {
// store,
key: COOKIE_LABEL, //1&_token
// maxAge: 86400000,
path: COOKIE_PATH,
maxAge:'session',
overwrite: true,
httpOnly: true,
signed: false,
rolling: true,
renew: false,
};


// 使用session中间件
app.use(session(sessionCfg, app));


// 设置session 值 , koa-session提供一个一种hack方法修改cookie的过期时间 _maxAge
ctx.session.user = _.assign(user,{_maxAge:7 * 24 * 3600 * 1000})
ctx.session.isLogin = true;

// 清除session数据
ctx.session = null;
使用外部store存储数据
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
// 外部store必须实现以下3个方法
let memoryStore = {
storage: {},
async get(key, maxAge) {
return this.storage[key]
},
async set(key, sess, maxAge) {
this.storage[key] = sess
},
async destroy(key) {
delete this.storage[key]
}
}

// 修改session配置
const sessionCfg = {
store: memoryStore, // 使用外部store
key: COOKIE_LABEL,
// maxAge: 86400000,
path: COOKIE_PATH,
maxAge:'session',
overwrite: true,
httpOnly: true,
signed: false,
rolling: true,
renew: false,
};

如果session数据量很小,可以直接存在内存中,如果session数据量很大,则需要存储介质存放session数据,比如redis,mysql等。

数据库

一个完整的系统当然少不了数据库,这里我们使用mysql。
ORM安装

1
npm install sequelize
数据库连接配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Sequelize = require('sequelize');

// 创建数据库访问实例
const sequelize = new Sequelize('database', 'username', 'password');

// 定义模型
const User = sequelize.define('user', {
username: Sequelize.STRING,
description: Sequelize.DATE
});

// 自动建表
sequelize.sync({ force: true });

// 数据持久化
User.create({ username: 'jason.chen', description: '全栈开发' }).then(function(user) {
// user 是一个持久化实例
})
Model操作
1
2
3
4
5
6
7
8
9
10
11
// 新增
User.create({ username: 'jason.chen', description: '全栈开发' })

// 删除
User.destroy({where:{id:1}})

// 更新
User.update({description:'全栈开发'},{where:{id:1}})

// 查询
User.findOne({where:{id:1}});

开发调试

node调试方式有三种

  • VSCode调试模块
  • Chrome 调试
  • 打印日志调试(debug模块)

这里使用Chrome调试,依赖环境如下

  • node环境8.x+
  • chrome 60+
1
node --inspect app.js

debug模块可以根据debug环境变量,控制不同命名空间的日志输出,开发过程中可使用nodemon根据文件修改自动重启服务。

项目框架搭建

开发环境

  • git
  • node (v8.11.1)
  • mysql (v5.6+)
  • redis

目录结构

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
.
├── LICENSE
├── README.md
├── api.md // api接口文档
├── ecosystem.config.js // pm2配置
├── logs // 应用日志
├── package-lock.json
├── package.json
├── src
│   ├── app.js // 应用入口文件
│   ├── config
│   │   ├── app.js
│   │   ├── db.js // db配置文件
│   │   ├── middleware.js // 中间件
│   │   ├── redis.js // redis配置文件
│   │   └── session.js
│   ├── controllers
│   │   ├── sys.js
│   │   └── user.js // 用户模块控制器
│   ├── models
│   │   ├── post.js
│   │   └── user.js // 用户模型
│   ├── routers
│   │   ├── sys.js
│   │   └── user.js // 用户模型
│   ├── run.js // 系统初始化脚本(初始化数据+mock数据)
│   ├── test // 测试demo
│   │   ├── db.js
│   │   └── test.js
│   └── utils
│   ├── db.js
│   ├── index.js
│   ├── log.js
│   ├── mail.js
│   ├── redis.js
│   └── store.js
└── package.json

功能模块

  • 多级路由
  • 应用日志
  • 参数校验
  • 权限控制
  • 负责均衡
  • 邮件通知
  • 钉钉通知
多级路由

全局路由模块

1
2
3
4
5
6
7
let router = new Router();

const user = require('./user')
const post = require('./post')

router.use('/user', user.routes(), user.allowedMethods())
router.use('/post', post.routes(), post.allowedMethods())

具体路由模块

1
2
3
const routers = router
.post('/login', validate(user.v.login), user.login)
.post('/logout', user.logout)
日志
应用日志

应用日志这里使用了log4js

log4js配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
"appenders": {
"access": {
"type": "dateFile",
"filename": "/var/log/myApp/access.log",
"pattern": "-yyyy-MM-dd",
"category": "http"
},
"app": {
"type": "file",
"filename": "/var/log/myApp/app.log",
"maxLogSize": 10485760,
"backups": 3
}
},
"pm2": true,
"categories": {
"default": { "appenders": [ "app"], "level": "DEBUG" },
"http": { "appenders": [ "access"], "level": "DEBUG" }
}
}

实例化logger

1
2
3
4
5
6
7
8
9
const log4js = require('log4js');

let config = require('../config/log');
log4js.configure(config);

module.exports = {
appLogger:log4js.getLogger(),
httpLogger:log4js.getLogger('http')
};

日志记录

1
2
3
4
5
const { appLogger , httpLogger} = require('./utils/log');

app.on('error', function(err, ctx) {
appLogger.error(err)
});
访问日志

access日志这里使用了koa-logger

1
2
3
4
5
6
7
8
9
const logger = require('koa-logger')

app.use(logger((str, args) => {
if(useFileLogger()){
httpLogger.info(`${args[1]} ${args[2]} ${args[3] || ''} ${args[4] || ''} ${args[5] || ''}`)
}else{
console.log(str)
}
}))
参数校验

web前端输入内容都是不可信的,后端必须校验请求参数,这里用到了joi。

joi
Object schema description language and validator for JavaScript objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Joi = require('joi');

const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email()
});

// Return result.
const result = Joi.validate({ username: 'abc', birthyear: 1994 }, schema);
// result.error === null -> valid

// You can also pass a callback which will be called synchronously with the validation result.
Joi.validate({ username: 'abc', birthyear: 1994 }, schema, function (err, value) { }); // err === null -> valid
权限控制

鉴权中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let auth = (authority)=>{
return async(ctx, next) => {
if(!ctx.session || (ctx.session && !ctx.session.isLogin)){
const error = new Error('请先登录');
error.status = 401;
throw error;
}

if(authority == 'admin' && !ctx.session.isAdmin){
const error = new Error('权限不足');
error.status = 403;
throw error;
}

await next();
};
}

鉴权中间件使用

1
2
3
4
const routers = router
.post('/login', validate(user.v.login), user.login)
.get('/getCurrent', auth(), user.getCurrent)
.get('/getUser', auth('admin'),validate(user.v.getUserById), user.getUserById)
负责均衡
pm2多进程

负载均衡
通过pm2进程管理工具管理应用,实现多进程负载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
apps: [{
name: 'cms-enterprise-helper',
script: './src/app.js',
instances : 1, // 'max',
// instances : 1,
exec_mode: 'cluster',
merge_logs : true,
log_file: '/var/log/myApp/app.log',
error_file: '/var/log/myApp/app-err.log',
out_file : '/var/log/myApp/app-out.log',
log_date_format : 'YYYY-MM-DD HH:mm Z'
}]
}
nginx负载均衡

nginx部分配置

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
#设定负载均衡的服务器列表
upstream mysvr {
server 127.0.0.1:3000 weight=1;
server 192.168.3.0.60 weight=1;
# server backend1.example.com weight=5;
}

server {
listen 80;
server_name cc.ximalaya.com ;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;

location /api/ {
proxy_pass http://mysvr/;
}


}
邮件通知

nodemailer

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
let nodemailer  = require('nodemailer');

let {
host,
secure,
username,
password
} = require('../config/mail');

let transporter = nodemailer.createTransport({
host: host,
secure: secure,
auth: {
user: username,
pass: password
}
});

module.exports = function sentMail(mail) {
return transporter.sendMail(Object.assign(mail,{from: '"系统管理员" <rsky163@163.com>'}));
}


// 发送邮件
let mail = {
from: '"系统管理员" <rsky163@163.com>', // sender address
to: 'jason.chen@ximalaya.com', // list of receivers
subject: 'Hello ✔', // Subject line
text: 'Hello world -- text?', // plain text body
html: '<b>Hello world == html</b>', // html body
};

sendMail(mail)
钉钉通知

通过request封装钉钉机器人接口请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ddUtil = {
send(url, obj){
return request({
method: 'POST',
uri: url,
json:true,
headers: [{
name: 'content-type',
value: 'application/json'
}],
body: obj
})
}
}

ddUtil.send(url_with_token,{
"msgtype": "text",
"text": {
"content": "Hello ximalaya !"
}
})

钉钉机器人,更多用法参见 https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.karFPe&treeId=257&articleId=105735&docType=1

上线部署

部署构图如下:
项目部署图

Node服务进程管理工具:

  • pm2 推荐
  • forever

pm2快速入门

发表于 2018-03-19 | 分类于 node |

pm2是一个高级node进程管理工具,通过它不仅可以管理node应用,也可以管理Python等其他语言的应用;本文是看完官网后做的笔录,适合快速入门pm2命令行工具的使用,具体细节可参考官网介绍

pm2使用

快速使用

安装

1
npm install pm2@latest -g

使用

1
pm2 start app.js

通过配置文件管理多个应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建 process.yml内容如下:
apps:
- script : app.js
instances: 4
exec_mode: cluster
- script : worker.js
watch : true
env :
NODE_ENV: development
env_production:
NODE_ENV: production


# 运行应用
pm2 start process.yml

# 随系统启动
pm2 startup

与pm2相关的所有配置位于$HOME/.pm2下

  • logs 应用log
  • pids 应用pid
  • sock 套接字文件
  • conf.js 配置文件

更新

1
2
3
4
5
# 全局安装
npm install pm2@latest -g

# 内存更新
pm2 update

命令清单

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# Fork mode
pm2 start app.js --name my-api # Name process

# Cluster mode
pm2 start app.js -i 0 # Will start maximum processes with LB depending on available CPUs
pm2 start app.js -i max # Same as above, but deprecated.

# Listing

pm2 list
# Or
pm2 [list|ls|l|status]

# 显示进程详情
pm2 show 0

pm2 list # Display all processes status
pm2 jlist # Print process list in raw JSON
pm2 prettylist # Print process list in beautified JSON

pm2 describe 0 # Display all informations about a specific process

pm2 monit # Monitor all processes

# Logs

pm2 logs [--raw] # Display all processes logs in streaming
pm2 flush # Empty all log files
pm2 reloadLogs # Reload all logs

# Actions

pm2 stop all # Stop all processes
pm2 restart all # Restart all processes

pm2 reload all # Will 0s downtime reload (for NETWORKED apps)

pm2 stop 0 # Stop specific process id
pm2 restart 0 # Restart specific process id

# 重启服务
pm2 restart myapp # Restart specific process id

# 根据应用名称删除
pm2 delete myapp
pm2 delete 0 # Will remove process from pm2 list
pm2 delete all # Will remove all processes from pm2 list

# Misc

pm2 reset <process> # Reset meta data (restarted time...)
pm2 updatePM2 # Update in memory pm2
pm2 ping # Ensure pm2 daemon has been launched
pm2 sendSignal SIGUSR2 my-app # Send system signal to script
pm2 start app.js --no-daemon
pm2 start app.js --no-vizion
pm2 start app.js --no-autorestart

# 给应用传递参数
pm2 start app.js -- -a 23 # Pass arguments '-a 23' argument to app.js script
pm2 start app.js --node-args="--debug=7001" # --node-args to pass options to node V8

pm2 start app.js --log-date-format "YYYY-MM-DD HH:mm Z" # Log will be prefixed with custom time format

# 以json文件方式启动
pm2 start app.json # Start processes with options declared in app.json

# 指定多应用中的具体应用
pm2 reload process.json --only api

# 自定义日志文件
pm2 start app.js -e err.log -o out.log # Start and specify error and out log

# PM2 2.4.0后 可以通过正则批量操作
pm2 restart/delete/stop/reload /http-[1,2]/

启动其他语言应用

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
# 启动其他语言的进程
pm2 start echo.php
pm2 start echo.py
pm2 start echo.sh

# 映射关系 - 解释器
{
".sh": "bash",
".py": "python",
".rb": "ruby",
".coffee" : "coffee",
".php": "php",
".pl" : "perl",
".js" : "node"
}
# 通过配置文件的方式启动,非js应用必须指定exec_mode: 'fork_mode' and exec_interpreter至对应的解释器
{
"apps" : [{
"name" : "bash-worker",
"script" : "./a-bash-script",
"exec_interpreter": "bash",
"exec_mode" : "fork_mode"
}, {
"name" : "ruby-worker",
"script" : "./some-ruby-script",
"exec_interpreter": "ruby",
"exec_mode" : "fork_mode"
}]
}

控制应用内存大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 启动应用 显示内存大小
pm2 start big-array.js --max-memory-restart 20M

# JSON
{
"name" : "max_mem",
"script" : "big-array.js",
"max_memory_restart" : "20M"
}

# 使用脚本方式控制内存大小
pm2.start({
name : "max_mem",
script : "big-array.js",
max_memory_restart : "20M"
}, function(err, proc) {
// Processing
});

# 单位 K(ilobyte), M(egabyte), G(igabyte)
50M
50K
1G

集群模式

根据cpus个数生成多个应用实例,提高应用的性能及可靠性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pm2 start app.js -i max
# max:pm2自动检测cpus个数

# 通过配置 js/yaml/json file
{
"apps" : [{
"script" : "api.js",
"instances" : "max",
"exec_mode" : "cluster"
}]
}
pm2默认不会负载均衡,需要制定 exec_mode为 cluster

-i 参数项
0/max: = CPUs
-1 : CPUs - 1
number: <= CPUS

应用平滑关闭

1
2
3
4
process.on('SIGINT', function() {
db.stop(function(err) {
process.exit(err ? 1 : 0);
});

});

启动配置文件

配置文件格式

  • javascript
  • json
  • yaml

生成配置文件

1
2
3
4
5
# 生成 ecosystem.config.js
pm2 ecosystem

# 生成文件不带注释
pm2 ecosystem simple

JavaScript格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
apps : [{
name : "worker",
script : "./worker.js",
watch : true,
env: {
"NODE_ENV": "development",
},
env_production : {
"NODE_ENV": "production"
}
},{
name : "api-app",
script : "./api.js",
instances : 4,
exec_mode : "cluster"
}]
}

json格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"apps" : [{
"name" : "worker",
"script" : "./worker.js",
"watch" : true,
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
},{
"name" : "api-app",
"script" : "./api.js",
"instances" : 4,
"exec_mode" : "cluster"
}]
}

yaml格式

1
2
3
4
5
6
7
8
9
10
11
12
apps:
- script : ./api.js
name : 'api-app'
instances: 4
exec_mode: cluster
- script : ./worker.js
name : 'worker'
watch : true
env :
NODE_ENV: development
env_production:
NODE_ENV: production

根据配置文件管理应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pm2 start ecosystem.config.js

# 启动配置文件中具体某个应用
pm2 start ecosystem.config.js --only worker-app

# 停止配置文件中指定的所有应用
pm2 stop ecosystem.config.js

# 重启所有应用
pm2 start ecosystem.config.js
## Or
pm2 restart ecosystem.config.js

# 重新加载所有应用
pm2 reload ecosystem.config.js

# 删除所有应用
pm2 delete ecosystem.config.js

更新环境变量信息

1
2
3
4
5
pm2 restart ecosystem.config.js --update-env
pm2 startOrReload ecosystem.config.js --update-env

# 根据参数自动注入环境变量 env_<name>
pm2 start process.json --env production

配置参数列表

配置参数列表



























































































































































































































Field Type Example Description
name (string) “my-api” application name (default to script filename without extension)
script (string) ”./api/app.js” script path relative to pm2 start
cwd (string) “/var/www/” the directory from which your app will be launched
args (string) “-a 13 -b 12” string containing all arguments passed via CLI to script
interpreter (string) “/usr/bin/python” interpreter absolute path (default to node)
interpreter_args (string) ”–harmony” option to pass to the interpreter
node_args (string)   alias to interpreter_args
instances number -1 number of app instance to be launched
exec_mode string “cluster” mode to start your app, can be “cluster” or “fork”, default fork
watch boolean or [] true enable watch & restart feature, if a file change in the folder or subfolder, your app will get reloaded
ignore_watch list [”[\/\]./”, “node_modules”] list of regex to ignore some file or folder names by the watch feature
max_memory_restart string “150M” your app will be restarted if it exceeds the amount of memory specified. human-friendly format : it can be “10M”, “100K”, “2G” and so on…
env object {“NODE_ENV”: “development”, “ID”: “42”} env variables which will appear in your app
env_<env_name></env_name> object {“NODE_ENV”: “production”, “ID”: “89”} inject <env_name> when doing pm2 restart app.yml –env <env_name></env_name></env_name>
source_map_support boolean true default to true, [enable/disable source map file]
instance_var string “NODE_APP_INSTANCE” see documentation
log_date_format (string) “YYYY-MM-DD HH:mm Z” log date format (see log section)
error_file (string)   error file path (default to $HOME/.pm2/logs/XXXerr.log)
out_file (string)   output file path (default to $HOME/.pm2/logs/XXXout.log)
combine_logs boolean true if set to true, avoid to suffix logs file with the process id
merge_logs boolean true alias to combine_logs
pid_file (string)   pid file path (default to $HOME/.pm2/pid/app-pm_id.pid)
min_uptime (string)   min uptime of the app to be considered started
listen_timeout number 8000 time in ms before forcing a reload if app not listening
kill_timeout number 1600 time in milliseconds before sending a final SIGKILL
wait_ready boolean false Instead of reload waiting for listen event, wait for process.send(‘ready’)
max_restarts number 10 number of consecutive unstable restarts (less than 1sec interval or custom time via min_uptime) before your app is considered errored and stop being restarted
restart_delay number 4000 time to wait before restarting a crashed app (in milliseconds). defaults to 0.
autorestart boolean false true by default. if false, PM2 will not restart your app if it crashes or ends peacefully
cron_restart string “1 0 *” a cron pattern to restart your app. Application must be running for cron feature to work
vizion boolean false true by default. if false, PM2 will start without vizion features (versioning control metadatas)
post_update list [“npm install”, “echo launching the app”] a list of commands which will be executed after you perform a Pull/Upgrade operation from Keymetrics dashboard
force boolean true defaults to false. if true, you can start the same script several times which is usually not allowed by PM2

使用json配置文件的方式,命令行可选参数会无效

日志文件

1
2
3
4
5
# 不保存日志 
设置 /dev/null to error_file or out_file 不保存日志文件.

# 同一应用 日志内容放一起
使用 merge_logs: true 禁止日志文件自动加上进程id后缀 (e.g. app-name-ID.log)

状态管理

平滑停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Graceful restart/reload/stop
process.on('SIGINT', function() {
db.stop(function(err) {
process.exit(err ? 1 : 0);
});
});

# 延时 3000ms
pm2 start app.js --kill-timeout 3000

{
"apps" : [{
"name" : "api",
"script" : "app.js",
"kill_timeout" : 3000
}]
}

平滑启动

某些应用运行需要等待DB或者workers建立好链接

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
# 应用发出ready正式更新状态
pm2 start app.js --wait-ready

# 应用链接处理完毕发送ready消息
var http = require('http');
var app = http.createServer(function(req, res) {
res.writeHead(200);
res.end('hey');
})
var listener = app.listen(0, function() {
console.log('Listening on port ' + listener.address().port);
// Here we send the ready signal to PM2
process.send('ready');
});

# 默认超时时间为 3000ms
pm2 start app.js --wait-ready --listen-timeout 3000

# json方式指定
{
"apps" : [{
"name" : "api",
"script" : "app.js",
"listen_timeout" : 3000
}]
}

环境变量

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
# 1.PM2 默认注入当前shell的环境变量
# 2.PM2注入配置文件中声明的环境变量,重名则覆盖

module.exports = {
apps : [
{
name: "myapp",
script: "./app.js",
watch: true,
env: {
"PORT": 3000,
"NODE_ENV": "development"
},
env_production: {
"PORT": 80,
"NODE_ENV": "production",
}
}
]
}

# 根据运行环境,选择不同的环境变量
ecosystem.config.js --env production

# 默认 restarting or reloading 不会更新环境变量,如果需要更新可以使用如下方式
pm2 reload ecosystem.json --update-env

# 指定实例个数的环境变量
NODE_APP_INSTANCE

# 修改环境变量的名称
instance_var: 'INSTANCE_ID',

应用日志管理

PM2日志可以合并,也可以存放不同的日志文件中。

实时显示日志

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
# 查看logs命令帮助
pm2 logs -h

# 显示所有应用日志
pm2 logs

# 指定正则匹配的应用的日志
pm2 logs /api/

pm2 logs /server_[12]/

# 显示应用api的日志
pm2 logs api

# 显示应用api日志的 X 行
pm2 logs big-api --lines 1000

# 格式化日志显示
pm2 logs --json pm2 logs --format

# 应用启动指定显示方式
pm2 start --log-type json app.json
pm2 start echo.js --merge-logs --log-date-format="YYYY-MM-DD HH:mm Z"

# 或者配置文件中中指定 "log_type": "json"

# 清除日志 (或者使用 pm2 install pm2-logrotate )
pm2 flush

# 不记录日志信息
{
"out_file": "/dev/null",
"error_file": "/dev/null"
}

# 指定日志类型
pm2 logs main

更新PM2

1
2
3
4
5
6
7
8
# 保存当前进程信息
pm2 save

# 安装最新版pm2
npm install pm2 -g

# 在内存中更新pm2
pm2 update

部署

本地我们可以直接通过 pm2 start/stop管理本地应用,通过deploy命令,在本地可以很轻松的将本地的应用部署远程服务器上。

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
58
{
"apps" : [{
"name" : "HTTP-API",
"script" : "http.js"
}],
"deploy" : {
// "production" is the environment name
"production" : {
// 已授权的公钥路径
"key" : "/path/to/some.pem", // 用户名
"user" : "ubuntu",
// 主机
"host" : ["212.83.163.1", "212.83.163.2", "212.83.163.3"],
// 分支
"ref" : "origin/master",
// 代码库
"repo" : "git@github.com:Username/repository.git",
// 远程服务器存放应用的路径
"path" : "/var/www/my-repository",
// ssh 配置项 可以是字符串或者数组字符串
"ssh_options": "StrictHostKeyChecking=no",
// 安装依赖,可以是命令,多个命令用分号;分割,也可以用本地脚本文件的路径
"pre-setup" : "apt-get install git",
// 代码库克隆完成执行 使用方式和 pre-setup一样
"post-setup": "ls -la",
// 本地要执行的代码
"pre-deploy-local" : "echo 'This is a local executed command'"
// 代码库克隆完成,远程执行
"post-deploy" : "npm install; grunt dist"
},
"dev" : {
"user" : "ubuntu_dev",
"host" : ["192.168.0.12"],
"ref" : "origin/master",
"repo" : "git@github.com:Username/repository.git",
"path" : "/var/www/my-repository",
"post-deploy" : "npm install; grunt dist"
}
}
}

# 本地生成公钥
ssh-keygen -t rsa
ssh-copy-id node@myserver.com

# 远程部署,初始化应用目录
pm2 deploy production setup

# 更新远程版本
pm2 deploy production update

# 版本回退 Revert to -1 deployment
pm2 deploy production revert 1

# 远程执行命令
pm2 deploy production exec "pm2 reload all"

pm2 startOrRestart all.json

Docker部署

将pm2设置成系统服务

  • systemd: Ubuntu >= 16, CentOS >= 7, Arch, Debian >= 7
  • upstart: Ubuntu <= 14
  • launchd: Darwin, MacOSx
  • openrc: Gentoo Linux, Arch Linux
  • rcd: FreeBSD
  • systemv: Centos 6, Amazon Linux
1
2
3
4
5
6
7
8
9
10
11
12
13
pm2 startup

# 指定具体平台,不加参数会自动检测
pm2 startup [ubuntu | ubuntu14 | ubuntu12 | centos | centos6 | arch | oracle | amazon | macos | darwin | freebsd | systemd | systemv | upstart | launchd | rcd | openrc]

# disable 自动启动
pm2 unstartup

# 保存当前进程列表
pm2 save

# 恢复之前保存的进程列表
pm2 resurrect

检测自启服务是否设置好

1
2
3
4
5
6
7
8
# Check if pm2-<USER> service has been added
$ systemctl list-units
# Check logs
$ journalctl -u pm2-<USER>
# Cat systemd configuration file
$ systemctl cat pm2-<USER>
# Analyze startup
$ systemd-analyze plot > output.svg

自动重启服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 当前目录或子目录文件改动自动重启服务
pm2 start app.js --watch

# 不会停止检测文件改动
pm2 stop 0

# 停止检测文件改动
pm2 stop --watch 0

# 配置文件 详细配置检测文件路径
{
"watch": ["server", "client"],
"ignore_watch" : ["node_modules", "client/img"],
"watch_options": {
"followSymlinks": false
}
}

sourcemap

当使用babeljs,typescript时,有了sourcemap,异常了可以找到具体错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 命令行指定 
pm2 start app.js --source-map-support
pm2 start app.js --disable-source-map

# 配置文件指定
{
"apps" : [{
"name" : "my-app",
"script" : "script.js",
"source_map_support": true
}]
}


# 监测cpu及memory
pm2 monit

静态服务

pm2 2.4.0 后可使用静态服务命令

1
2
3
4
5
6
7
8
9
10
11
# 命令行方式
pm2 serve <path> <port>

# 或者配置文件
{
"script": "serve",
"env": {
"PM2_SERVE_PATH": '.',
"PM2_SERVE_PORT": 8080
}
}

其他

  • 进程性能指标
  • 非root使用authbind绑定80端口
  • pm2 api
  • 云服务使用pm2
  • pm2模块

mysql之修改sql_mode

发表于 2018-03-19 | 分类于 数据库 |

起因

使用node ORM sequelizejs进行聚合查询,出现了一下报错,最终发现是sql_mode中的“ONLY_FULL_GROUP_BY”导致的。

1
2
Error: In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'user.id'; this is incompatible with sql_mode=only_full_group_by
mac 下安装mysql默认开启了”ONLY_FULL_GROUP_BY”模式,可通过 select @@sql_mode查看。

sql_mode配置项

几种常见的mode项

  • ONLY_FULL_GROUP_BY:出现在select语句、HAVING条件和ORDER BY语句中的列,必须是GROUP BY的列或者依赖于GROUP BY列的函数列。

  • NO_AUTO_VALUE_ON_ZERO:该值影响自增长列的插入。默认设置下,插入0或NULL代表生成下一个自增长值。如果用户希望插入的值为0,而该列又是自增长的,那么这个选项就有用了。

  • STRICT_TRANS_TABLES:在该模式下,如果一个值不能插入到一个事务表中,则中断当前的操作,对非事务表不做限制

  • NO_ZERO_IN_DATE:这个模式影响了是否允许日期中的月份和日包含0。如果开启此模式,2016-01-00是不允许的,但是0000-02-01是允许的。它实际的行为受到 strict mode是否开启的影响1。

  • NO_ZERO_DATE:设置该值,mysql数据库不允许插入零日期。它实际的行为受到 strictmode是否开启的影响2。

  • ERROR_FOR_DIVISION_BY_ZERO:在INSERT或UPDATE过程中,如果数据被零除,则产生错误而非警告。如果未给出该模式,那么数据被零除时MySQL返回NULL

  • NO_AUTO_CREATE_USER:禁止GRANT创建密码为空的用户

  • NO_ENGINE_SUBSTITUTION:如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常

  • PIPES_AS_CONCAT:将”||”视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似

  • ANSI_QUOTES:启用ANSI_QUOTES后,不能用双引号来引用字符串,因为它被解释为识别符

修改mysql配置

1.全局修改

1
2
3
4
5
# 查看全局sql_mode
SELECT @@global.sql_mode; 可以查看全局sql_mode的值

# 设置sql_mode
SET @@global.sql_mode="modes";

2.会话修改

1
2
3
4
# 查看会话sql_mode
SELECT @@SESSION.sql_mode; 可以查看全局sql_mode的值
# 设置sql_mode
SET @@SESSION.sql_mode="modes";

3.配置文件修改(推荐)

1
2
[mysqld]
sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"

mysql配置

发表于 2018-03-19 | 分类于 数据库 |

mysql官网安装包中(5.7.18之后版本)不再提供my-default.cnf文件,可以从https://dev.mysql.com/doc/refman/5.7/en/option-files.html找到配置项进行配置,看也可以按如下配置(参考http://www.fx114.net/qa-220-164752.aspx)

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
[client]
port = 3306
socket = /tmp/mysql.sock

[mysqld]

###############################基础设置#####################################

#Mysql服务的唯一编号 每个mysql服务Id需唯一
server-id = 1

#服务端口号 默认3306
port = 3306

#mysql安装根目录
basedir = /opt/mysql

#mysql数据文件所在位置
datadir = /opt/mysql/data

#临时目录 比如load data infile会用到
tmpdir = /tmp

#设置socke文件所在目录
socket = /tmp/mysql.sock

#主要用于MyISAM存储引擎,如果多台服务器连接一个数据库则建议注释下面内容
skip-external-locking

#只能用IP地址检查客户端的登录,不用主机名
skip_name_resolve = 1

#事务隔离级别,默认为可重复读,mysql默认可重复读级别(此级别下可能参数很多间隙锁,影响性能)
transaction_isolation = READ-COMMITTED

#数据库默认字符集,主流字符集支持一些特殊表情符号(特殊表情符占用4个字节)
character-set-server = utf8mb4

#数据库字符集对应一些排序等规则,注意要和character-set-server对应
collation-server = utf8mb4_general_ci

#设置client连接mysql时的字符集,防止乱码
init_connect='SET NAMES utf8mb4'

#是否对sql语句大小写敏感,1表示不敏感
lower_case_table_names = 1

#最大连接数
max_connections = 400

#最大错误连接数
max_connect_errors = 1000

#TIMESTAMP如果没有显示声明NOT NULL,允许NULL值
explicit_defaults_for_timestamp = true

#SQL数据包发送的大小,如果有BLOB对象建议修改成1G
max_allowed_packet = 128M

#MySQL连接闲置超过一定时间后(单位:秒)将会被强行关闭
#MySQL默认的wait_timeout 值为8个小时, interactive_timeout参数需要同时配置才能生效
interactive_timeout = 1800
wait_timeout = 1800

#内部内存临时表的最大值 ,设置成128M。
#比如大数据量的group by ,order by时可能用到临时表,
#超过了这个值将写入磁盘,系统IO压力增大
tmp_table_size = 134217728
max_heap_table_size = 134217728

#禁用mysql的缓存查询结果集功能
#后期根据业务情况测试决定是否开启
#大部分情况下关闭下面两项
query_cache_size = 0
query_cache_type = 0

#####################用户进程分配到的内存设置BEGIN#############################

##每个session将会分配参数设置的内存大小
#用于表的顺序扫描,读出的数据暂存于read_buffer_size中,当buff满时或读完,将数据返回上层调用者
#一般在128kb ~ 256kb,用于MyISAM
#read_buffer_size = 131072
#用于表的随机读取,当按照一个非索引字段排序读取时会用到,
#一般在128kb ~ 256kb,用于MyISAM
#read_rnd_buffer_size = 262144
#order by或group by时用到

#建议先调整为2M,后期观察调整
sort_buffer_size = 2097152

#一般数据库中没什么大的事务,设成1~2M,默认32kb
binlog_cache_size = 524288

########################用户进程分配到的内存设置END############################

#在MySQL暂时停止响应新请求之前的短时间内多少个请求可以被存在堆栈中
#官方建议back_log = 50 + (max_connections / 5),封顶数为900
back_log = 130

############################日志设置##########################################

#数据库错误日志文件
log_error = error.log

#慢查询sql日志设置
slow_query_log = 1
slow_query_log_file = slow.log

#检查未使用到索引的sql
log_queries_not_using_indexes = 1

#针对log_queries_not_using_indexes开启后,记录慢sql的频次、每分钟记录的条数
log_throttle_queries_not_using_indexes = 5

#作为从库时生效,从库复制中如何有慢sql也将被记录
log_slow_slave_statements = 1

#慢查询执行的秒数,必须达到此值可被记录
long_query_time = 8

#检索的行数必须达到此值才可被记为慢查询
min_examined_row_limit = 100

#mysql binlog日志文件保存的过期时间,过期后自动删除
expire_logs_days = 5

############################主从复制设置#####################################

#开启mysql binlog功能
log-bin=mysql-bin

#binlog记录内容的方式,记录被操作的每一行
binlog_format = ROW

#对于binlog_format = ROW模式时,减少记录日志的内容,只记录受影响的列
binlog_row_image = minimal

#master status and connection information输出到表mysql.slave_master_info中
master_info_repository = TABLE

#the slave's position in the relay logs输出到表mysql.slave_relay_log_info中
relay_log_info_repository = TABLE

#作为从库时生效,想进行级联复制,则需要此参数
log_slave_updates

#作为从库时生效,中继日志relay-log可以自我修复
relay_log_recovery = 1

#作为从库时生效,主从复制时忽略的错误
slave_skip_errors = ddl_exist_errors

#####################redo log和binlog的关系设置BEGIN#########################

#(步骤1) prepare dml相关的SQL操作,然后将redo log buff中的缓存持久化到磁盘
#(步骤2)如果前面prepare成功,那么再继续将事务日志持久化到binlog
#(步骤3)如果前面成功,那么在redo log里面写上一个commit记录
#当innodb_flush_log_at_trx_commit和sync_binlog都为1时是最安全的,
#在mysqld服务崩溃或者服务器主机crash的情况下,binary log只有可能丢失最多一个语句或者一个事务。
#但是都设置为1时会导致频繁的io操作,因此该模式也是最慢的一种方式。
#当innodb_flush_log_at_trx_commit设置为0,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。
#当innodb_flush_log_at_trx_commit设置为2,只有在操作系统崩溃或者系统掉电的情况下,上一秒钟所有事务数据才可能丢失。

#commit事务时,控制redo log buff持久化磁盘的模式 默认为1
innodb_flush_log_at_trx_commit = 2

#commit事务时,控制写入mysql binlog日志的模式 默认为0
#innodb_flush_log_at_trx_commit和sync_binlog都为1时,mysql最为安全但性能上压力也是最大
sync_binlog = 1

####################redo log和binlog的关系设置END############################

############################Innodb设置#####################################

#数据块的单位8k,默认是16k,16kCPU压力稍小,8k对select的吞吐量大
#innodb_page_size的参数值也影响最大索引长度,8k比16k的最大索引长度小
#innodb_page_size = 8192

#一般设置物理存储的60% ~ 70%
innodb_buffer_pool_size = 1G

#5.7.6之后默认16M
#innodb_log_buffer_size = 16777216

#该参数针对unix、linux,window上直接注释该参数.默认值为NULL
#O_DIRECT减少操作系统级别VFS的缓存和Innodb本身的buffer缓存之间的冲突
innodb_flush_method = O_DIRECT

#此格式支持压缩, 5.7.7之后为默认值
innodb_file_format = Barracuda

#CPU多核处理能力设置,假设CPU是2颗4核的,设置如下
#读多,写少可以设成2:6的比例
innodb_write_io_threads = 4
innodb_read_io_threads = 4

#提高刷新脏页数量和合并插入数量,改善磁盘I/O处理能力
#默认值200(单位:页)
#可根据磁盘近期的IOPS确定该值
innodb_io_capacity = 500

#为了获取被锁定的资源最大等待时间,默认50秒,超过该时间会报如下错误:
# ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
innodb_lock_wait_timeout = 30

#调整buffer pool中最近使用的页读取并dump的百分比,通过设置该参数可以减少转储的page数
innodb_buffer_pool_dump_pct = 40

#设置redoLog文件所在目录, redoLog记录事务具体操作内容
innodb_log_group_home_dir = /opt/mysql/redolog/

#设置undoLog文件所在目录, undoLog用于事务回滚操作
innodb_undo_directory = /opt/mysql/undolog/

#在innodb_log_group_home_dir中的redoLog文件数, redoLog文件内容是循环覆盖写入。
innodb_log_files_in_group = 3

#MySql5.7官方建议尽量设置的大些,可以接近innodb_buffer_pool_size的大小
#之前设置该值较大时可能导致mysql宕机恢复时间过长,现在恢复已经加快很多了
#该值减少脏数据刷新到磁盘的频次
#最大值innodb_log_file_size * innodb_log_files_in_group <= 512GB,单文件<=256GB
innodb_log_file_size = 1024M

#设置undoLog文件所占空间可以回收
#5.7之前的MySql的undoLog文件一直增大无法回收
innodb_undo_log_truncate = 1
innodb_undo_tablespaces = 3
innodb_undo_logs = 128

#5.7.7默认开启该参数 控制单列索引长度最大达到3072
#innodb_large_prefix = 1

#5.7.8默认为4个, Inodb后台清理工作的线程数
#innodb_purge_threads = 4

#通过设置配置参数innodb_thread_concurrency来限制并发线程的数量,
#一旦执行线程的数量达到这个限制,额外的线程在被放置到对队列中之前,会睡眠数微秒,
#可以通过设定参数innodb_thread_sleep_delay来配置睡眠时间
#该值默认为0,在官方doc上,对于innodb_thread_concurrency的使用,也给出了一些建议:
#(1)如果一个工作负载中,并发用户线程的数量小于64,建议设置innodb_thread_concurrency=0;
#(2)如果工作负载一直较为严重甚至偶尔达到顶峰,建议先设置innodb_thread_concurrency=128,
###并通过不断的降低这个参数,96, 80, 64等等,直到发现能够提供最佳性能的线程数
#innodb_thread_concurrency = 0

#强所有发生的死锁错误信息记录到error.log中,之前通过命令行只能查看最近一次死锁信息
innodb_print_all_deadlocks = 1

############################其他设置########################################

[mysqldump]
quick
max_allowed_packet = 128M

[mysql]
no-auto-rehash

[myisamchk]
key_buffer_size = 20M
sort_buffer_size = 256k
read_buffer = 2M
write_buffer = 2M

[mysqlhotcopy]
interactive-timeout

[mysqld_safe]
#增加每个进程的可打开文件数量
open-files-limit = 28192

[RN]真机调试及内部发布

发表于 2017-09-11 | 分类于 js , react native |

一般我们开发RN都是在模拟器上调试的,那么真机是如何调试的呢?

真机调试

  1. 配置好签名证书,并将包环境设置成Debug模式
  2. 将AppDelegate.m中的localhost修改成对应的IP地址,将调试真机插在电脑上,Xcode模拟器选择真机。
  3. 运行编译命令,真机摇一摇手机即可调出调试面板

内部发布

内部发布是上App Store之前的一步,开发完RN项目后可供内部人员安装和使用

  1. 修改AppDelegate.m中jsCodeLocation为 jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
  2. 在项目下运行 curl http://localhost:8081/index.ios.bundle -o main.jsbundle
    或者运行react-native bundle

此时就是在项目目录下生成main.jsbundle文件,这个文件将所有js文件都打包在一起了,然后配置好证书,再编译得到ipa离线包,安装后即可在真机跑起来了。

[RN]修改包服务器端口

发表于 2017-09-11 | 分类于 js , react native |

开发React Native时,启动包服务器的默认端口是8081,如果跟现有服务器端口冲突的话,可以修改成想要的端口,修改内容如下:

修改包服务器端口号

包服务器启动监听端口,在项目的node_modules/react-native/local-cli/server/server.js中

1
2
3
4
5
6
7
8
options: [{
command: '--port [number]',
default: 8081,
parse: (val) => Number(val),
},

...
]

由此可见我可以在运行时react-native start --port=8082动态指定端口

修改应用程序启动时,读取逻辑代码的的地址

在ios/Demo01/AppDelegate.m中找到下面代码

1
2
3
// 修改后代码
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];
// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

修改websocket监听端口

在项目的node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor中修改下端口。

1
2
3
4
5
6
7
8
- (void)setUp
{
if (!_url) {
NSInteger port = [[[_bridge bundleURL] port] integerValue] ?: 8081;
NSString *host = [[_bridge bundleURL] host] ?: @"localhost";
NSString *URLString = [NSString stringWithFormat:@"http://%@:%zd/debugger-proxy?role=client", host, port];
_url = [RCTConvert NSURL:URLString];
}

next快速入门

发表于 2017-08-17 | 分类于 js |

Next

Next是react服务端渲染框架。

特点

  • 默认服务端渲染
  • 自动切分代码
  • 基于webpack开发,支持HMR
  • 可以集成Express或其他Node server
  • 可以自定义Babel和Webpack配置

快速入门

初始化项目

1
2
3
4
5
6
mkdir hello-next
cd hello-next
npm init -y
npm install --save react react-dom next
# pages是页面路由目录,必须有,路由默认是基于该目录下的文件
mkdir pages

npm script 添加如下代码

1
2
3
4
5
6
7
{
"scripts": {
"dev": "next"
}
}

// npm run dev 即可运行开发服务器 http://localhost:3000

目前没有路由,访问会出现404页面,接下来添加路由。

添加路由

在pages目录下添加index.js(pages/index.js),内容如下:

1
2
3
4
5
6
const Index = () => (
<div>
<p>Hello Next.js</p>
</div>
)
export default Index

React组件要 默认 导出;默认情况下pages为路由目录,static为静态文件目录。

页面导航

添加about页面

1
2
3
4
5
export default () => (
<div>
<p>This is the about page</p>
</div>
)

通过 a 标签可以很容易做到页面跳转,但那是通过服务端导航,不是我们想要的,我们可以通过 next/link支持客户端导航。

1
2
3
4
5
6
7
8
9
10
11
12
import Link from 'next/link'

const Index = () => (
<div>
<Link href="/about">
<a>About Page</a>
</Link>
<p>Hello Next.js</p>
</div>
)

export default Index

或者

1
2
3
4
// 包裹按钮
<Link href="/about">
<button>Go to About Page</button>
</Link>

Link组件包裹a标签,处理页面跳转,无论是a,button还是其他组件(比如div),只要Link子组件接收onClick属性,都可以跳转。

链接添加样式

1
2
3
4
5
6
7
8
9
// 生效
<Link href="/about">
<a style={{ fontSize: 20 }}>About Page</a>
</Link>

// 不生效
<Link href="/about" style={{ fontSize: 20 }}>
<a>About Page</a>
</Link>

Link是高阶组件,直接收 href等一些其他属性,不支持style。

共享组件

多个页面经常会有公用部分(比如Head),我们可以新建components/Header.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Link from 'next/link'

const linkStyle = {
marginRight: 15
}

const Header = () => (
<div>
<Link href="/">
<a style={linkStyle}>Home</a>
</Link>
<Link href="/about">
<a style={linkStyle}>About</a>
</Link>
</div>
)
export default Header

在首页中导入Head组件

1
2
3
4
5
6
7
8
import Header from '../components/Header'

export default () => (
<div>
<Header />
<p>Hello Next.js</p>
</div>
)

组件可以放在任何目录中,除了 pages 目录,pages下的文件跟服务端路由相关。

Layout组件

可以将整个页面框架做成layout组件(components/MyLayout.js),各个页面复用

Layout组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Header from './Header'

const layoutStyle = {
margin: 20,
padding: 20,
border: '1px solid #DDD'
}

const Layout = (props) => (
<div style={layoutStyle}>
<Header />
{props.children}
</div>
)

export default Layout

index页面

1
2
3
4
5
6
7
8
9
// pages/index.js

import Layout from '../components/MyLayout.js'

export default () => (
<Layout>
<p>Hello Next.js</p>
</Layout>
)

about页面

1
2
3
4
5
6
7
8
9
pages/about.js

import Layout from '../components/MyLayout.js'

export default () => (
<Layout>
<p>about</p>
</Layout>
)

layout除了使用{props.children}还可以使用属性传值,高级组件的方式。

1
2
3
4
5
6
import withLayout from '../lib/layout'

const Page = () => (
<p>This is the about page</p>
)
export default withLayout(Page)
1
2
3
4
5
6
// pages/index.js
const Page = () => (
<p>This is the about page</p>
)

export default () => (<Layout page={Page}/>)

动态页面

前边我们实现了静态页面,现在我们通过 query string 实现动态页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// pages/index.js

import Layout from '../components/MyLayout.js'
import Link from 'next/link'

const PostLink = (props) => (
<li>
<Link href={`/post?title=${props.title}`}>
<a>{props.title}</a>
</Link>
</li>
)
// 通过Link组件href属性向下一个页面传参

export default () => (
<Layout>
<h1>My Blog</h1>
<ul>
<PostLink title="Hello Next.js"/>
<PostLink title="Learn Next.js is awesome"/>
<PostLink title="Deploy apps with Zeit"/>
</ul>
</Layout>
)

处理查询参数

1
2
3
4
5
6
7
8
import Layout from '../components/MyLayout.js'

export default (props) => (
<Layout>
<h1>{props.url.query.title}</h1>
<p>This is the blog post content.</p>
</Layout>
)

只有default导出的组件才有url属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Layout from '../components/MyLayout.js'

const Content = (props) => (
<div>
<h1>{props.url.query.title}</h1>
<p>This is the blog post content.</p>
</div>
)

export default () => (
<Layout>
<Content />
</Layout>
)

// 页面会抛出异常

// 可以通过组件传值的方式将url参数带到子组件中去
<Layout>
<Content url={props.url} />
</Layout>

每个页面都会获取url属性,可以props.url.query.title拿到相关的数据。

自定义路由

Link组件有个as属性,我们可以通过它自定义路由格式,它会显示在浏览器的地址栏中,但实际下个页面接收的数据还是href属性上的参数。

1
2
3
4
5
6
7
const PostLink = (props) => (
<li>
<Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
<a>{props.title}</a>
</Link>
</li>
)

虽然可以自定义客户端路由显示,但是刷新页面可能会404,服务端没有对应的路由。

自定义服务器

安装Express

1
npm install --save express

根目录下创建server.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
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
const server = express()

// 处理对应路由
server.get('/p/:id', (req, res) => {
const actualPage = '/post'
// 这里拿到的title是路由中的id,而客户端路由拿到的是Link href中的title,有点区别,但实际生产环境中都会使用资源id去请求数据。
const queryParams = { title: req.params.id }
// 定义了一个路由映射到已存在页面,同时附带查询参数
app.render(req, res, actualPage, queryParams)
})

server.get('*', (req, res) => {
return handle(req, res)
})

server.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
.catch((ex) => {
console.error(ex.stack)
process.exit(1)
})

更新下npm脚本

1
2
3
4
5
{
"scripts": {
"dev": "node server.js"
}
}

远程获取数据

可以通过一个异步方法getInitialProps(同时支持客户端和服务端)来获取远程数据。

安装 isomorphic-unfetch 请求模块

1
npm install --save isomorphic-unfetch

修改首页内容

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
import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'

const Index = (props) => (
<Layout>
<h1>Batman TV Shows</h1>
<ul>
{props.shows.map(({show}) => (
<li key={show.id}>
<Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
<a>{show.name}</a>
</Link>
</li>
))}
</ul>
</Layout>
)

Post.getInitialProps = async function (context) {
// 可以通过context上的query拿到查询参数
const { id } = context.query
const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
const show = await res.json()

console.log(`Fetched show: ${show.name}`)

return { show }
}

export default Index

getInitialProps receives a context object with the following properties:

1
2
3
4
5
6
7
pathname - path section of URL
query - query string section of URL parsed as an object
asPath - the actual url path
req - HTTP request object (server only)
res - HTTP response object (server only)
jsonPageRes - Fetch Response object (client only)
err - Error object if any error is encountered during the rendering

组件样式

给组件添加样式有如下两种方法:

  • 外部引入css文件
  • css in js (next推荐)

next.js使用 styled-jxs控制局部样式。

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
58
59
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

function getPosts () {
return [
{ id: 'hello-nextjs', title: 'Hello Next.js'},
{ id: 'learn-nextjs', title: 'Learn Next.js is awesome'},
{ id: 'deploy-nextjs', title: 'Deploy apps with ZEIT'},
]
}

export default () => (
<Layout>
<h1>My Blog</h1>
<ul>
{getPosts().map((post) => (
<li key={post.id}>
<Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}>
<a>{post.title}</a>
</Link>
<!-- prefetch 会预加载下个页面的脚本,不会加载数据-->
<li><Link prefetch href='/'><a>Home</a></Link></li>
</li>
))}
</ul>
<!-- 除了 Link 添加prefech属性,还可以通过脚本预加载-->
<a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }>
A route transition will happen after 100ms
</a>
{
// but we can prefetch it!
Router.prefetch('/dynamic')
}
// 样式必须用反引号包裹,必须放在模板字符串中,Styled jsx仅仅作为一个babel插件
<style jsx>{`
h1, a {
font-family: "Arial";
}

ul {
padding: 0;
}

li {
list-style: none;
margin: 5px 0;
}

a {
text-decoration: none;
color: blue;
}

a:hover {
opacity: 0.6;
}
`}</style>
</Layout>
)

styled-jsx 样式默认不作用于子组件,如果要子组件生效,可以单独定义子组件样式,或者使用全局样式。

styled-jsx 全局样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style jsx global>{`
.markdown {
font-family: 'Arial';
}

.markdown a {
text-decoration: none;
color: blue;
}

.markdown a:hover {
opacity: 0.6;
}

.markdown h3 {
margin: 0;
padding: 0;
text-transform: uppercase;
}
`}</style>

应用部署

####部署步骤

  1. build代码
  2. 启动服务

修改下npm脚本

1
2
3
4
"scripts": {
"build": "next build",
"start": "next start"
}

启动多个服务

为了水平扩展,通常会使用多个app实例

1
2
3
4
5
6
//$PORT 为环境变量
"scripts": {
"start": "next start -p $PORT"
}

//window下用 "next start -p %PORT%"

调用

1
2
PORT=8000 npm start
PORT=9000 npm start

可以通过 cross-env 设置环境变量。

高阶使用

导出静态网站

在根目录下创建文件next.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
exportPathMap: function () {
return {
'/': { page: '/' },
'/about': { page: '/about' },
'/p/hello-nextjs': { page: '/post', query: { title: "Hello Next.js" } },
'/p/learn-nextjs': { page: '/post', query: { title: "Learn Next.js is awesome" } }
}
}
}

修改npm脚本

1
2
3
4
5
6
{
"scripts": {
"build": "next build",
"export": "next export"
}
}

执行导出命令

1
npm run export

导入内容会放在out目录中,可放在任何静态服务器上使用。

☞官网

Hexo插入媒体资源

发表于 2017-08-15 |

通常我们会在Hexo文章中插入媒体资源如图片,音频,视频,那么让我们来看看如何插入

插入图片

本地图片

在/hexo/source目录下新建文件夹,命名为images或其他

1
![图片描述](/images/图片名字.png)

使用远程图片

这种是大家常用的方法

1
![图片描述](http://ouonaooa5.bkt.clouddn.com/demo.jpg)

可使用七牛存储自己的图片资源

插入音频

插入网易云音乐

1
2
3
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86   
src="//music.163.com/outchain/player?type=2&id=25706282&auto=0&height=66">
</iframe>

插入喜马拉雅

1
<iframe height="36" width="260" src="//www.ximalaya.com/swf/sound/red.swf?id=36371956" frameborder=0 allowfullscreen></iframe>

插入视频

1
<iframe class="ql-video ql-align-center" frameborder="0" allowfullscreen="true" src="https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0" height="238" width="560"></iframe>

Handlebars模板使用

发表于 2017-08-15 | 分类于 js |

Handlebars 是 JavaScript 一个语义模板库,通过对view和data的分离来快速构建Web模板。

安装

  • 直接下载 Handlebars.js 和 Handlebar Runtime
  • 通过npm下载
1
2
3
4
5
6
7
8
# 安装
npm install --save handlebars

# 引用
require('handlebars');

# 如果模板预编译后,可直接使用运行时环境
require('handlebars/runtime');

快速入门

Handlebar表达式

Handlebars会从当前上下文中查找title和body属性

1
2
3
4
5
6
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>

路径查询

Handlebars会从当前上下文中查找article属性,然后在查找结果中找title属性

1
2
3
<h1>{{article.title}}</h1>
<!-- 两者等价,下面方法deprecated-->
<h1>{{article/title}}</h1>

标识符不能包含 Whitespace ! “ # % & ‘ ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~

如果出现不合法的属性,可以使用中括号的方式进行引用

1
2
3
4
{{#each articles.[10].[#comments]}}
<h1>{{subject}}</h1>
{{{body}}}
{{/each}}

默认情况下 Handlebars为了安全,会对内容进行转义,如需原样输出可以使用`{{{ html }}}`

Helper模板方法

1
2
3
4
5
6
7
8
9
10
{{{link story}}}

Handlebars.registerHelper('link', function(object, options) {
var url = Handlebars.escapeExpression(object.url),
text = Handlebars.escapeExpression(object.text);

return new Handlebars.SafeString(
"<a href='" + url + "'>" + text + "</a>"
);
});

Handlebars helper支持接收一个可选的键值对参数作为它的最后一个参数传入(hash)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{{link "See more..." href=story.url class="story"}}}
Handlebars.registerHelper('link', function(text, options) {
var attrs = [];

for (var prop in options.hash) {
attrs.push(
Handlebars.escapeExpression(prop) + '="'
+ Handlebars.escapeExpression(options.hash[prop]) + '"');
}

return new Handlebars.SafeString(
"<a " + attrs.join(" ") + ">" + Handlebars.escapeExpression(text) + "</a>"
);
});

Helper 支持嵌套

1
{{outer-helper (inner-helper 'abc') 'def'}}

去掉空格

去掉多余空格(在大括号边添加 ~)

1
2
3
4
5
6
7
8
9
{{#each nav ~}}
<a href="{{url}}">
{{~#if test}}
{{~title}}
{{~^~}}
Empty
{{~/if~}}
</a>
{{~/each}}

标签转义

使用标签原内容

1
2
3
4
5
6
7
<!-- 在前边添加 \ -->
\{{escaped}}

<!--使用4个大括号-->
{{{{raw}}}}
{{escaped}}
{{{{/raw}}}}

编译模板

模板字符串

1
2
3
4
5
6
7
8
<script id="entry-template" type="text/x-handlebars-template">
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
</script>

使用 Handlebars.compile编译模板输出模板函数

1
2
3
4
var source   = $("#entry-template").html();
var template = Handlebars.compile(source);

// 如果预先生成模板函数了,则可以直接引用runtime库,不必将handlebar全部引入

命令行工具

1
2
3
4
5
6
7
8
9
10
11
npm install handlebars -g

# 语法
handlebars <input> -f <output>

# 查看更多使用方法
handlebars --help

#如果输入文件是 person.handlebars,Handlebar会将文件内容作为compile方法的第一个参数,生成一个模板方法,插入到 Handlebars.templates.person上,之后可直接使用 Handlebars.templates.person(context,options)

# 若预编译,直接使用runtime库 <script src="/libs/handlebars.runtime.js"></script>

高级使用

complile 函数

1
2
3
4
模板函数接收第二个可选参数,传以下参数:
data:传入私有变量 @variable
helpers:传入局部helper,而不是全局定义,重名会覆盖全局。
partials: 自定义模板片段partials,与helper类似

块级表达式

块级表达式可以改变模板局部context,块级helper以#开口,结尾以/开发,如`{{#hi}} {{/hi}}`

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{#bold}}{{body}}{{/bold}}
{{#noop}} hello {{/noop}}
</div>
</div>

Handlebars.registerHelper('noop', function(options) {
return options.fn(this);
});
// 其中this执行当前上下文 options.fn 就像普通的模板函数一样,如果块级helper含有else部分,和fn类似的还有一个 options.reverse 渲染else里的模板。

Handlebars.registerHelper('bold', function(options) {
return new Handlebars.SafeString(
'<div class="mybold">'
+ options.fn(this)
+ '</div>');
});


<!-- with 会改变当前上下文 -->
{{#with story}}
<div class="intro">{{{intro}}}</div>
<div class="body">{{{body}}}</div>
{{/with}}



<!-- 使用each 迭代 -->
{{#each comments}}
<div class="comment">
<h2>{{subject}}</h2>
{{{body}}}
</div>
{{/each}}

// each 原理
Handlebars.registerHelper('each', function(context, options) {
var ret = "";
for(var i=0, j=context.length; i<j; i++) {
ret = ret + options.fn(context[i]);
}
return ret;
});



<!-- 条件渲染 -->
{{#if isActive}}
<img src="star.gif" alt="Active">
{{else}}
<img src="cry.gif" alt="Inactive">
{{/if}}

Handlebars.registerHelper('if', function(conditional, options) {
if(conditional) {
return options.fn(this);
} else {
return options.inverse(this); // 使用else里边的模板
}
});


<!-- hash 参数 -->
{{#list nav id="nav-bar" class="top"}}
<a href="{{url}}">{{title}}</a>
{{/list}}

Handlebars.registerHelper('list', function(context, options) {
var attrs = Em.keys(options.hash).map(function(key) {
return key + '="' + options.hash[key] + '"';
}).join(" ");

return "<ul " + attrs + ">" + context.map(function(item) {
return "<li>" + options.fn(item) + "</li>";
}).join("\n") + "</ul>";
});


<!-- block helper 支持向子模板里注入自己的私有变量,如@index -->
{{#list array}}
{{@index}}. {{title}}
{{/list}}
Handlebars.registerHelper('list', function(context, options) {
var out = "<ul>", data;

if (options.data) {
data = Handlebars.createFrame(options.data);
}

for (var i=0; i<context.length; i++) {
if (data) {
data.index = i;
}

out += "<li>" + options.fn(context[i], { data: data }) + "</li>";
}

out += "</ul>";
return out;
});

// 访问父作用域中的index 可以使用 @../index

// Handlebars 3.0 部分内置helper 支持命名参数 user为当前this,userId为index
{{#each users as |user userId|}}
Id: {{userId}} Name: {{user.name}}
{{/each}}

Handlebars.registerHelper('block-params', function() {
var args = [],
options = arguments[arguments.length - 1];
for (var i = 0; i < arguments.length - 1; i++) {
args.push(arguments[i]);
}

return options.fn(this, {data: options.data, blockParams: args});
});
{{#block-params 1 2 3 as |foo bar baz|}}
{{foo}} {{bar}} {{baz}}
{{/block-params}}

<!-- 纯渲染,不做任何处理 -->
{{{{raw-helper}}}}
{{bar}}
{{{{/raw-helper}}}}

路径查找

可以通过 `../name` 或者`{{./name}} or {{this/name}} or {{this.name}}`

1
2
3
4
5
6
7
8
9
{{permalink}}
{{#each comments}}
{{../permalink}}

{{#if title}}
{{../permalink}}
{{/if}}
{{/each}}
<!-- permalink 查找上级作用域中的Permalink属性 -->

模板注释

使用`{{!-- --}} or {{! }}`注释内容

1
2
3
{{!-- This comment will not be in the output --}} 

{{! This comment will not be in the output }}

Helper

使用 Handlebars.registerHelper注册模板助手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Handlebars.registerHelper('fullName', function(person) {
return person.firstName + " " + person.lastName;
});

Handlebars.registerHelper('agree_button', function() {
var emotion = Handlebars.escapeExpression(this.emotion),
name = Handlebars.escapeExpression(this.name);

// 如果不需要转义内容,使用Handlebars.SafeString
return new Handlebars.SafeString(
"<button>I agree. I " + emotion + " " + name + "</button>"
);
});

Handlebars.registerHelper('if', function(conditional, options) {
if(conditional) {
return options.fn(this);
} else {
return options.inverse(this); // 使用else里边的模板
}
});

模板片段Partials

使用 Handlebars.registerPartial注册模板片段

1
Handlebars.registerPartial('myPartial', '{{name}}')

使用`{{> myPartial }}`调用

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
<div class="post">
{{> userMessage tagName="h1" }}
<h1>Comments</h1>
</div>

<!-- 动态模板片段 -->
{{> (whichPartial) }}

<!-- 更改上下文 -->
{{> myPartial myOtherContext }}

<!-- hash 参数,模板中可直接使用该参数-->
{{> myPartial parameter=value }}

<!-- Partial Blocks 如果partial 没有注册就直接调用,就会抛出异常,使用块级helper可解决 -->
{{#> myPartial }}
Failover content {{! myPartial 没有注册时才渲染 }}
{{/myPartial}}

<!-- 可以通过@partial-block拿到默认内容 -->
{{#> layout }}
My Content
{{/layout}}

Site Content
{{> @partial-block }}

<!-- 模板内声明partial,当前block和子模板中可用 -->
{{#*inline "myPartial"}}
My Content
{{/inline}}

{{#each children}}
{{> myPartial}}
{{/each}}

内置Helper

if 如果值为 false, undefined, null, "", 0, or [] 就会渲染else部分

1
2
3
4
5
6
7
<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{else}}
<h1>Unknown Author</h1>
{{/if}}
</div>

unless 相当于if的else情况

1
2
3
{{#unless license}}
<h3 class="warning">WARNING: This entry does not have a license!</h3>
{{/unless}}

each遍历数组,会改变当前context,执行遍历的元素

1
2
3
4
5
6
7
8
<ul class="people_list">
{{#each people}}
<li>{{this}}</li>

{{else}}
暂无数据
{{/each}}
</ul>

遍历对象,相当于变量的key;遍历数组,为array的index

with 会改变当前context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

<div class="entry">
<h1>{{title}}</h1>

{{#with author}}
<h2>By {{firstName}} {{lastName}}</h2>
{{else}}
<p class="empty">No content</p>
{{/with}}
</div>
```

`log` 渲染时打印日志

```html
{{log "Look at me!"}}

<!-- level 支持的值为 debug, info, warn, and error -->
{{log "Log!" level="error"}}

处理helper异常
如果调用一个不存在的helper,就会异常,options.name会自动注入为helper名称

1
2
3
4
Handlebars.registerHelper('helperMissing', function(/* [args, ] options */) {
var options = arguments[arguments.length - 1];
throw new Handlebars.Exception('Unknown field: ' + options.name);
});

API

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
// 注册 partial
Handlebars.registerPartial('foo', partial);

Handlebars.registerPartial({
foo: partial,
bar: partial
});
// 注销 partial
Handlebars.unregisterPartial('foo');

// 注册 helper
Handlebars.registerHelper('foo', function() {
});

Handlebars.registerHelper({
foo: function() {
},
bar: function() {
}
});
// 注销helper
Handlebars.unregisterHelper('foo');

// 创建块级作用域 child data
Handlebars.createFrame(data)

// 顶级对象
@root

// 数组第一个元素
@first

// 数组最后一个元素
@last

// 对象的key
@key

// 数组的index
@index

API更多请参考

123
ccrsky

ccrsky

聚沙成塔,集腋成裘

26 日志
9 分类
16 标签
RSS
GitHub 知乎 微博
© 2020 ccrsky
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4