Skip to content

博客

使用Golang基于MongoDB构建Restful服务

近来使用Golang来构建Restful服务变得越发流行。我发现有些时候使用MongoDB作为持久存储,这篇文档中, 我会使用Golang和MongoDB来构建一个简单的用户管理为服务。

MongoDB因为极简、灵活、高可用以及面向文档的特性得到越来越多市场上的青睐。根据MongoDB之父的解释,它被用来设计组合键值对存储和关系数据库存储的最佳特性。MongoDB在两者之间做妥协,具备了二者的某些有用的功能。

MongoDB的应用场景:Web应用、分析应用的首要数据库,以及弱数据类型的数据,也就是无schema数据。

文档就是键值对集合。文档中的键用字符串表示,文档中的值可以是基础的数据类型(字符串、数字、日期等)、数组,也可以是另一个文档。在MongDB内部以二进制JSON格式存储文档数据,也就做BSON。BSON有相似的结构,但专为文档存储而设计。

下面是一个文档数据示例:

{
name: '张三',
age: '11',
address: '湖北省武汉市光谷一路'
}

集合是结构或者概念上相似文档的容器。例如,我们会把用户(user)文档存储到(users)集合(collection)中。这里集合的概念就非常类似于关系数据库(RDMS)中表(table)的概念。两者的不同是,集合中的数据是无schema的,是不强制数据结构的,可以是任意的。

MongoDB不是用SQL,而是使用自己的JSON查询语言。

例如:使用SQL语句查询名叫“张三”的用户

SELECT * from users
WHERE name = '张三'

而在MongoDB中,查询的是:

db.users.find({name: 'hello'})

mgo(发音:mango)是一个Go语言实现的MongoDB驱动程序,这个驱动提供了一个非常简洁易于使用、并经过充分测试API。接下来,在介绍如何通过mgo来实现CRUD(create、react、update、delete)操作之前,将简单介绍下会话管理(session manager)。

获取会话

session, err := mgo.Dial("localhsot")

单个的会话不允许进行并发处理,所以通常需要使用多个会话。新建一个会话的最快方式是从现有的session中复制一个新的会话:

newSession := session.Copy()
defer newSession.Close()

新生成的这个会话会使用相同的集群信息和连接池(connection pool)。每一个新建的session必须在生命周期结束时调用Close方法,该会话的资源会视情况而定,是被放回连接池,还是被回收。

mgo需要和bson一同使用,bson使编写查询更加简单。

  • 获取集合中所有的文档
c := session.DB("store").C("users")
var users []User
err := c.Find(bson.M{}).All(&books)
  • 查询单个文档
c := session.DB("store").C("users")
var user User
err := c.Find(bson.M{"name": "张三"}).One(&user)
  • 新建文档
c := session.DB("store").C("users")
err = c.Insert(&User{"Ale"})
  • 更新文档
c := session.DB("store").C("users")
err = c.Update(bson.M{"name": "张三"}, &book)
  • 删除文档
c := session.DB("store").C("users")
err = c.Remove(bson.M{"name": "张三"})

Echo是一个高性能、极简的Go语言Web框架。

  • 优化的 HTTP 路由。
  • 创建可靠并可伸缩的RESTful API。
  • 基于标准的HTTP服务器。
  • 组 APIs.
  • 可扩展的middleware框架。
  • Define middleware at root, group or route level.
  • 为JSON, XML进行数据绑定,产生负荷。
  • 提供便捷的方法来发送各种HTTP相应。
  • 对HTTP错误进行集中处理。
  • Template rendering with any template engine.
  • 定义属于你的日志格式。
  • 高度个性化。
  • Automatic TLS via Let’s Encrypt
  • 支持HTTP/2

performance.png

具体实现中基于Echo框架来开发,代码在github.com

package main
import (
"log"
"net/http"
"fmt"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type User struct {
ID string `json:"id" bson:"_id,omitempty"`
Name string `json:"name,omitempty"`
Phone string `json:"phone,omitempty"`
Age int `json:"age,omitempty"`
}
var session *mgo.Session
func init() {
s, err := mgo.Dial("localhost")
if err != nil {
log.Fatal(err)
}
session = s
}
func main() {
defer session.Close()
ensureIndex(session)
session.SetMode(mgo.Monotonic, true)
e := echo.New()
e.Use(middleware.Logger())
e.GET("/users", allUsers)
e.GET("/user/:id", getUser)
e.PUT("/user", updateUser)
e.DELETE("/user/:id", deleteUser)
e.POST("/user", saveUser)
e.Logger.Fatal(e.Start(":1424"))
}
func ensureIndex(s *mgo.Session) {
session := s.Copy()
defer session.Close()
c := session.DB("store").C("users")
index := mgo.Index{
Key: []string{"id"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
err := c.EnsureIndex(index)
if err != nil {
panic(err)
}
}
func saveUser(e echo.Context) error {
u := new(User)
if err := e.Bind(u); err != nil {
return e.JSON(http.StatusBadRequest, err)
}
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
err := c.Insert(u)
if err != nil {
log.Println("Failed insert user", u)
if mgo.IsDup(err) {
return e.JSON(http.StatusBadRequest, "User with this id alread exists.")
}
return e.JSON(http.StatusInternalServerError, "Database error")
}
return e.JSON(http.StatusCreated, "SUCCESS")
}
func getUser(e echo.Context) error {
s := session.Copy()
defer s.Clone()
c := s.DB("store").C("users")
var u User
id := e.Param("id")
fmt.Println("userid", id)
err := c.Find(bson.M{"_id": id}).One(&u)
if err != nil {
log.Println("Failed get user", err)
return e.JSON(http.StatusNotFound, "Database error")
}
return e.JSON(http.StatusOK, u)
}
func updateUser(e echo.Context) error {
u := new(User)
if err := e.Bind(u); err != nil {
return e.JSON(http.StatusBadRequest, err)
}
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
err := c.Update(bson.M{"_id": u.ID}, &u)
if err != nil {
switch err {
default:
log.Fatalln("Failed update user: ", err)
return e.JSON(http.StatusInternalServerError, "Database error")
case mgo.ErrNotFound:
return e.JSON(http.StatusNotFound, "Not found")
}
}
return e.JSON(http.StatusOK, u)
}
func deleteUser(e echo.Context) error {
s := session.Copy()
defer s.Close()
id := e.Param("id")
c := s.DB("store").C("users")
err := c.Remove(bson.M{"_id": id})
if err != nil {
switch err {
default:
e.JSON(http.StatusInternalServerError, "Database error")
log.Fatalln("Failed delete user: ", err)
return err
case mgo.ErrNotFound:
e.JSON(http.StatusInternalServerError, "User not found")
return err
}
}
return e.JSON(http.StatusOK, "Sucess")
}
func allUsers(e echo.Context) error {
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
var users []User
err := c.Find(bson.M{}).All(&users)
if err != nil {
e.JSON(http.StatusInternalServerError, "Database Error")
return err
}
return e.JSON(http.StatusOK, users)
}

curl对于构建和测试RESTful服务来说是一个非常好用的工具,在其他RESTful 服务API的文档中,常常可以看到curl的身影,这里也不例外。

  • 请求
Terminal window
curl -X POST -H 'Content-Type: application/json' -d @body.json http://localhsot:1424/user
body.json
{
"id": "5",
"name": "李四",
"age": 11
}
  • 响应
Terminal window
SUCCESS
  • 请求
Terminal window
curl -X PUT -H 'Content-Type: application/json' -d @body.json http://localhost:1424/user
body.json
{
"id": "1",
"title": "天一",
"age": "-1"
}
  • 响应
Terminal window
{"id":"1","name":"天一","age":-1}
  • 请求

使用python -m json.tool将服务返回的json,进行格式化处理。

Terminal window
curl http://localhost:1424/users | python -m json.tool
  • 响应
Terminal window
[
{
"id": "YE/\ufffd\ufffdDj\ufffd\ufffd\u0004\ufffd-",
"name": "xiwang"
},
{
"id": "2",
"name": "1"
},
{
"age": -1,
"id": "1",
"name": "\u5929\u4e00"
},
{
"id": "YE7\u001d\ufffdDj\ufffd\ufffd\u0004\ufffd.",
"name": "bug"
}
]
  • 请求
Terminal window
curl http://localhost:1424/user/1
  • 响应
{"id":"1","name":"天一","age":-1}
  • 请求
Terminal window
curl -X DELETE http://localhost:1424/user/1
  • 响应
Terminal window
SUCCESS

Git基本使用

团队开发中,遵循一个合理、清晰的Git使用流程,是非常重要的。 否则,每个人都提交一堆杂乱无章的commit,项目很快就会变得难以协调和维护。 image

首先,每次开发新功能,都应该新建一个单独的分支(这方面可以参考《Git分支管理策略》)。

# 获取主干最新代码
$ git checkout master
$ git pull
# 新建一个开发分支myfeature
$ git checkout -b myfeature

分支修改后,就可以提交commit了。

$ git add --all
$ git status
$ git commit --verbose

git add 命令的all参数,表示保存所有变化(包括新建、修改和删除)。从Git 2.0开始,all是 git add 的默认参数,所以也可以用 git add . 代替。 git status 命令,用来查看发生变动的文件。 git commit 命令的verbose参数,会列出 diff 的结果。

提交commit时,必须给出完整扼要的提交信息,下面是一个范本。

Present-tense summary under 50 characters
* More information about commit (under 72 characters).
* More information about commit (under 72 characters).
http://project.management-system.com/ticket/123

第一行是不超过50个字的提要,然后空一行,罗列出改动原因、主要变动、以及需要注意的问题。最后,提供对应的网址(比如Bug ticket)。

分支的开发过程中,要经常与主干保持同步。

$ git fetch origin
$ git rebase origin/master

分支开发完成后,很可能有一堆commit,但是合并到主干的时候,往往希望只有一个(或最多两三个)commit,这样不仅清晰,也容易管理。 那么,怎样才能将多个commit合并呢?这就要用到 git rebase 命令。

$ git rebase -i origin/master

git rebase命令的i参数表示互动(interactive),这时git会打开一个互动界面,进行下一步操作。 下面采用Tute Costa的例子,来解释怎么合并commit。

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend
# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

上面的互动界面,先列出当前分支最新的4个commit(越下面越新)。每个commit前面有一个操作命令,默认是pick,表示该行commit被选中,要进行rebase操作。 4个commit的下面是一大堆注释,列出可以使用的命令。

pick:正常选中
reword:选中,并且修改提交信息;
edit:选中,rebase时会暂停,允许你修改这个commit(参考这里)
squash:选中,会将当前commit与上一个commit合并
fixup:与squash相同,但不会保存当前commit的提交信息
exec:执行其他shell命令

上面这6个命令当中,squash和fixup可以用来合并commit。先把需要合并的commit前面的动词,改成squash(或者s)。

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

这样一改,执行后,当前分支只会剩下两个commit。第二行和第三行的commit,都会合并到第一行的commit。提交信息会同时包含,这三个commit的提交信息。

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage
# This is the 2nd commit message:
Fix PostChecker::Post#urls
# This is the 3rd commit message:
Hey kids, stop all the highlighting

如果将第三行的squash命令改成fixup命令。

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

运行结果相同,还是会生成两个commit,第二行和第三行的commit,都合并到第一行的commit。但是,新的提交信息里面,第三行commit的提交信息,会被注释掉。

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage
# This is the 2nd commit message:
Fix PostChecker::Post#urls
# This is the 3rd commit message:
# Hey kids, stop all the highlighting

Pony Foo提出另外一种合并commit的简便方法,就是先撤销过去5个commit,然后再建一个新的。

$ git reset HEAD~5
$ git add .
$ git commit -am "Here's the bug fix that closes #28"
$ git push --force

squash和fixup命令,还可以当作命令行参数使用,自动合并commit。

$ git commit --fixup
$ git rebase -i --autosquash

这个用法请参考这篇文章,这里就不解释了。

合并commit后,就可以推送当前分支到远程仓库了。

$ git push --force origin myfeature

git push命令要加上force参数,因为rebase以后,分支历史改变了,跟远程分支不一定兼容,有可能要强行推送。

提交到远程仓库以后,就可以发出 Pull Request 到master分支,然后请求别人进行代码review,确认可以合并到master。

React方法绑定的几种实现方式

翻译参考自:React and ES6 - Part 3, Binding to methods of React class (ES7 included)

在React进行事件方法绑定的时,如下面的代码:

import React , {Component} from 'react'
class user extends Component{
render(){
return <button onClick={this.sayhello} >open hello</button>
}
sayhello(){
console.log('hello world')
console.log(this.props)
}
}
export default user

我们将会得到“TypeError: Cannot read property ‘props’ of null“的错误,如下图所示: error

这是因为当我们调用以这种方式绑定到this的方法时,没有将函数上下文绑定到组件的实例上。这是Javascript的默认行为而且是可以明确预期到的。

React团队在实现支持ES6时不支持自动绑定,你在这篇博客中找到更多关于这样做的原因。

下面将介绍几种不同的方式实现,在JSX中使用ES6语法调用class method。

方法一、 使用Function.prototype.bind()

Section titled “方法一、 使用Function.prototype.bind()”

如下面的代码:

// 使用bind()
class user extends Component{
render(){
return <button onClick={this.sayHello.bind(this)} >open hello</button>
}
sayHello(){
console.log(this.props)
}
}

任何ES6类的方法都是纯JavaScript函数,并从Function prototype继承了bind()方法,所以当我们调用sayHello方法时,this将会指向我们的组件实例。在MDN article可以找到更多的相关信息。

方法二、 使用在constructor中定义的方法

Section titled “方法二、 使用在constructor中定义的方法”
// 使用构造函数定义的方法
class user1 extends Component{
constructor(props){
super(props)
this._sayHello = ()=> this.sayHello();
}
sayHello(){
console.log(this.props)
}
render(){
return (<button onClick={this.sayHello}>open hello 1</button>)
}
}

这种方式中,不再需要在JSX中使用bind(),但会使构造函数更加膨胀。

方法三、使用箭头操函数和构造函数

Section titled “方法三、使用箭头操函数和构造函数”

ES6箭头函数会保留定义时的this上下文,即当调用箭头函数时,函数体内的this对象,就是定义时所在的对象,而不是调用时所在的对象。所以可以利用这个特性,并在构造函数中重新定义sayHello方法。

// 箭头函数 + 构造函数
class user extends Component{
constructor(props){
super(props)
this._sayHello = ()=> this._sayHello()
}
sayHello(){
console.log(this.props)
}
render(){
return <button onClick={this._sayHello.bind(this)} >open hello3</button>
}
}

方法四、使用箭头函数和ES6类属性

Section titled “方法四、使用箭头函数和ES6类属性”

可以通过使用箭头函数和ES6类属性语法:

// 箭头函数 + ES属性语法
class user extends Component{
sayHello = ()=>{
console.log(this.props)
}
render(){
return <button onClick={this.sayHello} >open hello4</button>
}
}

上面出现的代码下在地址: github

MySql JOIN

SQL中的连接(JOIN)语句用于将数据库中的两个表或者多个表组合起来。有“连接”生成的集合,可以被保存为表,或者当成表来使用,JOIN语句的含义是把两张表的属性通过它们的值组合在一起。基于ANSI标准的SQL列出五种JOIN方式:

  • 内连接(INNER)
  • 全外连接(FULL OUTER)
  • 左外连接(LEFT OUTER)
  • 右外连接(RIGHT OUTER)
  • 交叉连接(CROSS)

在特定的情况下,一张表(基本表,视图或连接表)可以和自身进行连接,成为自连接(self-join)。 其中, MYSQL不支持全外连接(FULL OUTER)。这里只关注Mysql的剩下的四种连接(JOIN)。

下面有两张表雇员表(Employee)和部门表(Department),ID是主键。

雇员表(Employee)

IDNameDeptID
1张三2
2李四3
3王五3
4老李5

部门表(Department)

IDName
2研发部
3测试部
4运维部

MySQL中,内连接,即等值连接。

select * from employee, department
where employee.deptID = department.id;

等效于

select * from
employee inner join department
on employee.deptID = department.id;

查询结果是:

+----+--------+--------+----+-----------+
| id | name | deptID | id | name |
+----+--------+--------+----+-----------+
| 1 | 张三 | 2 | 2 | 测试部 |
| 2 | 李四 | 3 | 3 | 运维部 |
| 3 | 王五 | 3 | 3 | 运维部 |
+----+--------+--------+----+-----------+
3 rows in set (0.00 sec)

Mysql中, left outer join 等效于 left join。左外连接结果中将会保留“左表”中的所有记录,即使右表中没有匹配连接条件,右表中没有匹配条件的列值为NULL。

select * from employee left join department on employee.deptId = department.id;

或者

select * from employee left outer join department on employee.deptId = department.id;

查询结果:

+----+--------+--------+------+-----------+
| id | name | deptID | id | name |
+----+--------+--------+------+-----------+
| 1 | 张三 | 2 | 2 | 测试部 |
| 2 | 李四 | 3 | 3 | 运维部 |
| 3 | 王五 | 3 | 3 | 运维部 |
| 4 | 老李 | 5 | NULL | NULL |
+----+--------+--------+------+-----------+
4 rows in set (0.00 sec)

右外连接,它和左外连接完全类似,如果 A 表右连接 B 表, 那么”右表” B 中的每一行在连接表中至少会出现一次. 如果 B 表的记录在”左表” A 中未找到匹配行, 连接表中来源于 A 的列的值设为 NULL。right outer join 等效 right join 。

select * from employee right join department on employee.deptId = department.id;

或者

select * from employee right outer join department on employee.deptId = department.id;

查询结果:

+------+--------+--------+----+-----------+
| id | name | deptID | id | name |
+------+--------+--------+----+-----------+
| 1 | 张三 | 2 | 2 | 测试部 |
| 2 | 李四 | 3 | 3 | 运维部 |
| 3 | 王五 | 3 | 3 | 运维部 |
| NULL | NULL | NULL | 1 | 研发部 |
+------+--------+--------+----+-----------+
4 rows in set (0.00 sec)

交叉连接(cross join),又称笛卡尔连接(cartesian join)或叉乘(Product),它是所有类型的内连接的基础。把表视为行记录的集合,交叉连接即返回这两个集合的笛卡尔积。这其实等价于内连接的链接条件为”永真”,或连接条件不存在. 如果 A 和 B 是两个集合,它们的交叉连接就记为: A × B。

select * from employee cross join department;

查询结果:

+----+--------+--------+----+-----------+
| id | name | deptID | id | name |
+----+--------+--------+----+-----------+
| 1 | 张三 | 2 | 1 | 研发部 |
| 1 | 张三 | 2 | 2 | 测试部 |
| 1 | 张三 | 2 | 3 | 运维部 |
| 2 | 李四 | 3 | 1 | 研发部 |
| 2 | 李四 | 3 | 2 | 测试部 |
| 2 | 李四 | 3 | 3 | 运维部 |
| 3 | 王五 | 3 | 1 | 研发部 |
| 3 | 王五 | 3 | 2 | 测试部 |
| 3 | 王五 | 3 | 3 | 运维部 |
| 4 | 老李 | 5 | 1 | 研发部 |
| 4 | 老李 | 5 | 2 | 测试部 |
| 4 | 老李 | 5 | 3 | 运维部 |
+----+--------+--------+----+-----------+
12 rows in set (0.00 sec)

MYSQL不支持全连接,但可以模拟。全连接是左右外连接的并集,连接表包含左右连接表的所有记录,如果缺少匹配的记录,就以NULL填充。

下面使用left join 和 right join 实现 full join:

select * from employee e left join department d on e.deptId = d.id
union
select * from employee e right join department d on e.deptid= d.id

查询结果:

+------+--------+--------+------+-----------+
| id | name | deptID | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 2 | 2 | 测试部 |
| 2 | 李四 | 3 | 3 | 运维部 |
| 3 | 王五 | 3 | 3 | 运维部 |
| 4 | 老李 | 5 | NULL | NULL |
| NULL | NULL | NULL | 1 | 研发部 |
+------+--------+--------+------+-----------+
5 rows in set (0.00 sec)

使用React写一个简单的Chrome插件

当你手中只有一把锤子的时候,你就会把所有的问题都看成钉子。

Google Chrome,Google公司开发的一款设计简单、高效的Web浏览器。Chrome不仅页面渲染速度快,Javascript执行速度快,而且更重要的是支持开发者为其编写各种各样的扩展来扩充其功能。

这里将要描述的就是为Chrome编写一个插件:使用当前网页地址生成二维码。

给chrome编写插件还是非常容易的事情,这里会使用到react.js(使用react,仅仅是因为,我最近对它很感兴趣)。

使用create-react-app来新建React应用是一个非常不错的选择。

$ create-ract-app react-qr
Creating a new React app in /Users/xiaoxiwang/Documents/demo/react-qr.
Installing packages. This might take a couple minutes.
Installing react, react-dom, and react-scripts...
yarn add v0.24.5
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
success Saved lockfile.
success Saved 878 new dependencies.
...

新建完成后生成的文件目录如下:

➜ react-qr tree -L 1
.
├── README.md
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

此时,运行命令:

$ cd react-qr
$ yarn start

如果顺利将可以在浏览器中,看到一个最简单的Create APP。

清除react-react-app生成的默认样式和内容,为下一步做准备。

Chrome扩展是用于扩充Chrome浏览器功能的程序,是一系列文件的集合,这些文件包括HTML文件、CSS样式文件、JavaScript脚本文件、图片等静态文件以及manifest.json。扩展被安装后,Chrome就会读取扩展中的manifest.json文件。这个文件的文件名固定为manifest.json,内容是按照一定格式描述的扩展相关信息,如扩展名称、版本、更新地址、请求的权限、扩展的UI界面入口等等

在public文件夹下新建一个名为manifest.json的文件, 如下:

{
"manifest_version": 2,
"name": "QR Code",
"description": "Generate QR Code",
"version": "0.1",
"icons": {
"16": "./favicon.png",
"48": "./favicon.png",
"128": "./favicon.png"
},
"browser_action": {
"default_icon": {
"19": "./favicon.png",
"38": "./favicon.png"
},
"default_popup": "index.html",
"default_title": "Generate QRCode"
},
"permissions": [
"activeTab"
]
}

上面的配置中,name定义的扩展名称,version定义扩展版本,description定义扩展的描述,icons则定义的扩展使用的图标及其位置。borowser_action指定了扩展在工具栏中的行为,工具栏中的图标(default_icon)、鼠标悬停插件时的标题(default_title)以及用户点击扩展时显示的页面所在位置(deafult_popup)。

qrcode.react是一个生成二维码的React组件。 添加依赖

$ yarn add qrcode.react

现在编辑App.js, 添加二维码。

import React, { Component } from "react";
import "./App.css";
import QRCode from "qrcode.react";
class App extends Component {
constructor(props) {
super(props);
this.state = { url: "" };
}
renderStatus(url) {
this.setState({ url });
}
render() {
const { url } = this.state;
return (
<div>
<div>QRCode</div>
<div className="url">{url}</div>
<QRCode value={url} size={270} />
</div>
);
}
}
export default App;

二维码的初始值是为空, 大小为270px。文本显示的URL地址,这里使用了一个样式‘url’,定义在APP.css文件中:

.url {
width: 270px;
text-overflow: ellipsis;
white-space: pre;
overflow: hidden;
font-size: 14px;
color: #888;
}

url宽度设置为270px,超出部分自动使用‘…’替代。

现在生成的二维码是一个空字符串,可以在插件页面渲染成功后,获取当前页面的URL地址。这里会是使用到React的componentDidMount方法,该方法会在页面渲染完成时调用一次,此时的DOM结构都已经渲染完成。为了获取到当前页面的URL,可以使用chrome.tabs.query(object queryIfon, function callback)接口。

chrome.tabs.query(object queryInfo, function callback)
Gets all tabs that have the specified properties, or all tabs if no properties are specified.

通过查询当前窗口中激活状态的tab,并选取第一个tab,即获取到了当前页面所在chrome tab。

var queryInfo = {
active: true,
currentWindow: true,
};
chrome.tabs.query(queryInfo, (tabs) => {
const tab = tabs[0];
const url = tab.url;
this.setState({ url });
});

获取到当前页面的url后,使用setState更新状态。

运行yarn buid命令,将打包react app,在build文件夹下生成相应输出文件。

$ yarn build

此时build,会出现chrome is not defined的错误。

➜ react-qr yarn build
yarn build v0.24.5
$ react-scripts build
Creating an optimized production build...
Failed to compile.
./src/App.js
Line 34: 'chrome' is not defined no-undef
Search for the keywords to learn more about each error.
error Command failed with exit code 1.

因为ESLint无法识别chrome, 可以在App.js文件的第一行添加”/global chrome/“,来告知ESLint将chrome识别为一个全局变量。

/*global chrome*/
import React, { Component } from "react";
import "./App.css";
import QRCode from "qrcode.react";

在地址栏中输入”//chrome:extensions”进入chrome插件管理UI, 勾选开发者模式, 并点击“Load upacked extension…”按钮,选择插件的编译输出build目录, 此时就可以在工具栏中,看到一个新增的插件图标,点击这个图标即可生成当前页面地址的二维码。

Demo