MEAN实践——LAMP的新时代替代方案(下)

【编者按】在九十年代,Linux+Apache+Mysql+PHP 架构曾风靡一时,直到现在仍然是众多 Web 应用程序的基本架构。然而随着需求的变迁和数据流量的激增,LAMP 已不可避免的走下神坛。近日,在 MongoDB Blog 中,Dana Groce 介绍了一个基于新时代架构的实践——MEAN,MongoDB/Mongoose.js、Express.js、Angular.js 和 Node.js。本文系 OneAPM 工程师编译整理:

MEAN实践——LAMP的新时代替代方案(上)

在本系列文章的第一部分旨在介绍一些应用程序的基础技术细节和如何进行数据建模,而这个部分文章将着手建立验证应用程序行为的测试,并会指出如何启动和运行应用程序。

首先,编写测试

首先定义一些小型配置库。文件名:test/config/test_config.js

module.exports = {
    url : ‘http://localhost:8000/api/v1.0‘
}

服务器运行端口是 localhost 8000 ,对于初始的测试来说非常适合。之后,如果改变产品系统的位置或者端口号,只需要简单地修改这个文件就可以。为了良好地测试,首先应该建立 1 个好的测试环境,这点可以通过下面的代码保证。首先,连接到数据库。

文件名: est/setup_tests.js 。

function connectDB(callback) {
    mongoClient.connect(dbConfig.testDBURL, function(err, db) {
        assert.equal(null, err);
        reader_test_db = db;
        console.log("Connected correctly to server");
        callback(0);
    });
}

下一步,drop user collection,这么做可以了解数据库状态。

function dropUserCollection(callback) {
        console.log("dropUserCollection");
        user = reader_test_db.collection(‘user‘);
        if (undefined != user) {
            user.drop(function(err, reply) {
                console.log(‘user collection dropped‘);
                callback(0);
            });
        } else {
            callback(0);
        }
    },

下一步,drop user feed entry collection。

function dropUserFeedEntryCollection(callback) {
    console.log("dropUserFeedEntryCollection");
    user_feed_entry = reader_test_db.collection(‘user_feed_entry‘);
    if (undefined != user_feed_entry) {
        user_feed_entry.drop(function(err, reply) {
            console.log(‘user_feed_entry collection dropped‘);
            callback(0);
        });
    } else {
        callback(0);
    }
}

下一步,连接到Stormpath,随后删点所有测试应用程序中的用户。

function getApplication(callback) {
    console.log("getApplication");
    client.getApplications({
        name: SP_APP_NAME
    }, function(err, applications) {
        console.log(applications);
        if (err) {
            log("Error in getApplications");
            throw err;
        }
        app = applications.items[0];
        callback(0);
    });
},
function deleteTestAccounts(callback) {
    app.getAccounts({
        email: TU_EMAIL_REGEX
    }, function(err, accounts) {
        if (err) throw err;
        accounts.items.forEach(function deleteAccount(account) {
            account.delete(function deleteError(err) {
                if (err) throw err;
            });
        });
        callback(0);
    });
}

下一步,关闭数据库。

function closeDB(callback) {
    reader_test_db.close();
}

最终,调用 async.series 来保证所有函数都按次序运行。

async.series([connectDB, dropUserCollection,    dropUserFeedEntryCollection, dropUserFeedEntryCollection, getApplication, deleteTestAccounts, closeDB]);

Frisby 在初期就被建立,这里将使用它定义测试用例,如下:

文件名:test/createaccountserror_spec.js

TU1_FN = "Test";
TU1_LN = "User1";
TU1_EMAIL = "[email protected]";
TU1_PW = "testUser123";
TU_EMAIL_REGEX = ‘testuser*‘;
SP_APP_NAME = ‘Reader Test‘;

var frisby = require(‘frisby‘);
var tc = require(‘./config/test_config‘);

下面代码将从 enroll route 开始。这个用例故意丢掉了 first name 字段,因此获得 1 个 400 与 1 个 JSON error(显示 first name 未定义)返回,下面就 toss that frisby:

frisby.create(‘POST missing firstName‘)
.post(tc.url + ‘/user/enroll‘,
      { ‘lastName‘ : TU1_LN,
        ‘email‘ : TU1_EMAIL,
        ‘password‘ : TU1_PW })
.expectStatus(400)
.expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
.expectJSON({‘error‘ : ‘Undefined First Name‘})
.toss()

下面用例将测试不包含小写字母,这同样会导致 Stormpath 返回错误,以及返回400 状态。

下面将测试一个无效邮箱地址。因此,期望返回的是未发现 @ 标志,以及 emali地址缺少域名,同时也会获得 1 个 400 状态。

文件名:test/createaccountsspec.js

frisby.create(‘POST invalid email address‘)
.post(tc.url + ‘/user/enroll‘,
      { ‘firstName‘ : TU1_FN,
        ‘lastName‘ : TU1_LN,
        ‘email‘ : "invalid.email",
        ‘password‘ : ‘testUser‘ })
.expectStatus(400)
.expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
.expectJSONTypes({‘error‘ : String})
.toss()

下面着眼一些可以运行的例子,首先需要定义 3 个用户。

文件名:test/createaccountsspec.js

TEST_USERS = [{‘fn‘ : ‘Test‘, ‘ln‘ : ‘User1‘,
         ‘email‘ : ‘[email protected]‘, ‘pwd‘ : ‘testUser123‘},
          {‘fn‘ : ‘Test‘, ‘ln‘ : ‘User2‘,
           ‘email‘ : ‘[email protected]‘, ‘pwd‘ : ‘testUser123‘},
          {‘fn‘ : ‘Test‘, ‘ln‘ : ‘User3‘,
           ‘email‘ : ‘[email protected]‘, ‘pwd‘ : ‘testUser123‘}]

SP_APP_NAME = ‘Reader Test‘;

var frisby = require(‘frisby‘);
var tc = require(‘./config/test_config‘);

下面用例将发送 1 个包含上文已定义 3 个用户的数组,当然期望获得代表成功的 201 状态。返回的 JSON document 将展示已建立的用户对象,因此这里可以检查测试数据匹配与否。

TEST_USERS.forEach(function createUser(user, index, array) {
    frisby.create(‘POST enroll user ‘ + user.email)
    .post(tc.url + ‘/user/enroll‘,
          { ‘firstName‘ : user.fn,
            ‘lastName‘ : user.ln,
            ‘email‘ : user.email,
            ‘password‘ : user.pwd })
    .expectStatus(201)
    .expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
    .expectJSON({ ‘firstName‘ : user.fn,
                  ‘lastName‘ : user.ln,
                  ‘email‘ : user.email })
    .toss()
});

下一步将测试重复用户。下例将验证这个用户注册的 email 地址已经被使用。

frisby.create(‘POST enroll duplicate user ‘)
    .post(tc.url + ‘/user/enroll‘,
      { ‘firstName‘ : TEST_USERS[0].fn,
        ‘lastName‘ : TEST_USERS[0].ln,
        ‘email‘ : TEST_USERS[0].email,
        ‘password‘ : TEST_USERS[0].pwd })
.expectStatus(400)
.expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
.expectJSON({‘error‘ : ‘Account with that email already exists.  Please choose another email.‘})
.toss()

这里存在一个重要问题,无法知道 Stormpath 会优先返回哪个 API key。因此,这里需要建立一个动态文件。随后可以使用这个对文件来验证测试用例——用户身份验证组件。

文件名称: /tmp/readerTestCreds.js

TEST_USERS =
[{    "_id":"54ad6c3ae764de42070b27b1",
"email":"[email protected]",
"firstName":"Test",
"lastName":"User1",
"sp_api_key_id":”<API KEY ID>",
"sp_api_key_secret":”<API KEY SECRET>”
},
{    "_id":"54ad6c3be764de42070b27b2”,
    "email":"[email protected]",
    "firstName":"Test",
    "lastName":"User2”,
    "sp_api_key_id":”<API KEY ID>",
    "sp_api_key_secret":”<API KEY SECRET>”
}];
module.exports = TEST_USERS;

为了建立上面这个临时文件,这里需要连接 MongoDB 从而检索用户信息。代码如下:

文件名:tests/writeCreds.js

TU_EMAIL_REGEX = new RegExp(‘^testuser*‘);
SP_APP_NAME = ‘Reader Test‘;
TEST_CREDS_TMP_FILE = ‘/tmp/readerTestCreds.js‘;

var async = require(‘async‘);
var dbConfig = require(‘./config/db.js‘);
var mongodb = require(‘mongodb‘);
assert = require(‘assert‘);

var mongoClient = mongodb.MongoClient
var reader_test_db = null;
var users_array = null;

function connectDB(callback) {
     mongoClient.connect(dbConfig.testDBURL, function(err, db) {
     assert.equal(null, err);
     reader_test_db = db;
     callback(null);
     });
 }

 function lookupUserKeys(callback) {
     console.log("lookupUserKeys");
     user_coll = reader_test_db.collection(‘user‘);
     user_coll.find({email :    TU_EMAIL_REGEX}).toArray(function(err, users) {
         users_array = users;
         callback(null);
     });
 }

function writeCreds(callback) {
     var fs = require(‘fs‘);
     fs.writeFileSync(TEST_CREDS_TMP_FILE, ‘TEST_USERS = ‘);
     fs.appendFileSync(TEST_CREDS_TMP_FILE,   JSON.stringify(users_array));
     fs.appendFileSync(TEST_CREDS_TMP_FILE, ‘; module.exports =  TEST_USERS;‘);
     callback(0);
 }

 function closeDB(callback) {
     reader_test_db.close();
 }

 async.series([connectDB, lookupUserKeys, writeCreds, closeDB]);

着眼下面代码,上文建立的临时文件在第一行就会被使用。同时,有多个 feeds 被建立,比如 Dilbert 和 the Eater Blog 。

文件名:tests/feed_spec.js

TEST_USERS = require(‘/tmp/readerTestCreds.js‘);

var frisby = require(‘frisby‘);
var tc = require(‘./config/test_config‘);
var async = require(‘async‘);
var dbConfig = require(‘./config/db.js‘);

var dilbertFeedURL = ‘http://feeds.feedburner.com/DilbertDailyStrip‘;
var nycEaterFeedURL = ‘http://feeds.feedburner.com/eater/nyc‘;

首先,一些用户会被建立,当然他们并没有订阅任何 feeds。下面代码将测试 feeds 的订阅。请注意,这里同样需要进行身份验证,通过使用 .auth 和 Stormpath API keys 完成。

function addEmptyFeedListTest(callback) {
     var user = TEST_USERS[0];
     frisby.create(‘GET empty feed list for user ‘ + user.email)
             .get(tc.url + ‘/feeds‘)
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(200)
         .expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
         .expectJSON({feeds : []})
         .toss()
         callback(null);
}

下面用例将为第一个测试用户订阅 Dilbert feed 。

这个用例将尝试为用户 feed 重复订阅。

function subDuplicateFeed(callback) {
 var user = TEST_USERS[0];
 frisby.create(‘PUT Add duplicate feed sub for user ‘ + user.email)
         .put(tc.url + ‘/feeds/subscribe‘,
              {‘feedURL‘ : dilbertFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
         .expectJSONLength(‘user.subs‘, 1)
         .toss()
 callback(null);
}

下一步,将为测试用户添加一个新的 feed,返回的结果应该是用户当下已经订阅了 2 个 feed。

function subSecondFeed(callback) {
 var user = TEST_USERS[0];
 frisby.create(‘PUT Add second feed sub for user ‘ + user.email)
         .put(tc.url + ‘/feeds/subscribe‘,
              {‘feedURL‘ : nycEaterFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
         .expectJSONLength(‘user.subs‘, 2)
         .toss()
 callback(null);
 }

下一步,将使用第 2 个测试用户来订阅 1 个 feed 。

function subOneFeedSecondUser(callback) {
     var user = TEST_USERS[1];
 frisby.create(‘PUT Add one feed sub for second user ‘ + user.email)
         .put(tc.url + ‘/feeds/subscribe‘,
              {‘feedURL‘ : nycEaterFeedURL})
         .auth(user.sp_api_key_id, user.sp_api_key_secret)
         .expectStatus(201)
         .expectHeader(‘Content-Type‘, ‘application/json; charset=utf-8‘)
         .expectJSONLength(‘user.subs‘, 1)
         .toss()
 callback(null);
}

async.series([addEmptyFeedListTest, subOneFeed, subDuplicateFeed, subSecondFeed, subOneFeedSecondUser]);

REST API

在开始编写 REST API 代码之前,首先需要定义一些实用工具库。首先,需求定义应用程序如何连接到数据库。将这个信息写入一个独立的文件允许应用程序灵活地添加新数据库 URL,以应对开发或者生产系统。

文件名:config/db.js

如果期望打开数据库验证,这里需要将信息存入 1 个文件,如下文代码所示。出于多个原因,这个文件不应该被置入源代码控制。

文件名称:config/security.js

module.exports = {
 stormpath_secret_key : ‘YOUR STORMPATH APPLICATION KEY’;
}

Stormpath API 和 Secret keys 应该被保存到属性文件,如下文代码所示,同事还需要严加注意。

文件名:config/stormpath_apikey.properties

apiKey.id = YOUR STORMPATH API KEY ID
apiKey.secret = YOUR STORMPATH API KEY SECRET

Express.js 简述

在 Express.js 中会建立应用程序(APP)。这个应用程序会监听制定的端口来响应 HTTP 请求。当请求涌入,它们会被传输到 1 个中间件链。中间件链中的每个 link 都会被给予 1 个请求和 1 个响应对象用以存储结果。link 分为两种类型,工作或者传递到下一个 link 。这里会通过 app.use() 来添加新的中间件。主中间件被称为「router(路由器)」,它会监听 URL,并将 URL/ 动作传递到 1 个指定的处理函数。

建立应用程序

现在开始聚焦应用程序代码,鉴于可以在独立文件中为不同的 routes 嵌入处理器,所以应用程序的体积非常小。

文件名:server.js

在 chain 中末尾定义中间件来处理坏 URLs。

现在,应用程序就会监听 8000 端口。

在控制台将消息打印给用户。

console.log(‘Magic happens on port ‘ + port);

exports = module.exports = app;

定义 Mongoose 数据模型

这里会使用 Mongoose 将 Node.js 上的对象映射成 MongoDB 文档。如上文所述,这里将建立 4 个 collections:

  • Feed collection。
  • Feed entry collection。
  • User collection。
  • User feed-entry-mapping collection。

下一步,将为 4 个 collections 定义 schema。首先,从 user schema 开始。注意,这里同样可以格式化数据,比如讲字母都转换成小写,使用 trim 消除首/末空格。

文件名:app/routes.js

var userSchema = new mongoose.Schema({
         active: Boolean,
     email: { type: String, trim: true, lowercase: true },
     firstName: { type: String, trim: true },
     lastName: { type: String, trim: true },
     sp_api_key_id: { type: String, trim: true },
     sp_api_key_secret: { type: String, trim: true },
     subs: { type: [mongoose.Schema.Types.ObjectId], default: [] },
     created: { type: Date, default: Date.now },
     lastLogin: { type: Date, default: Date.now },
 },
 { collection: ‘user‘ }
);

下面代码将告诉 Mongoose 需要哪些索引。当索引不存在于 MongoDB 数据库中时,Mongoose 将会负责索引的建立。唯一性约束保障将去除重复出现的可能。「email : 1」 将以升序的方式维护地址,而「email : -1」则是降序。

在其他 3 个 collections 上重复这个步骤。

var UserModel = mongoose.model( ‘User‘, userSchema );

var feedSchema = new mongoose.Schema({
     feedURL: { type: String, trim:true },
     link: { type: String, trim:true },
     description: { type: String, trim:true },
     state: { type: String, trim:true, lowercase:true, default: ‘new‘ },
     createdDate: { type: Date, default: Date.now },
     modifiedDate: { type: Date, default: Date.now },
 },
 { collection: ‘feed‘ }
);

feedSchema.index({feedURL : 1}, {unique:true});
feedSchema.index({link : 1}, {unique:true, sparse:true});

var FeedModel = mongoose.model( ‘Feed‘, feedSchema );

var feedEntrySchema = new mongoose.Schema({
     description: { type: String, trim:true },
     title: { type: String, trim:true },
     summary: { type: String, trim:true },
     entryID: { type: String, trim:true },
     publishedDate: { type: Date },
     link: { type: String, trim:true  },
     feedID: { type: mongoose.Schema.Types.ObjectId },
     state: { type: String, trim:true, lowercase:true, default: ‘new‘ },
     created: { type: Date, default: Date.now },
 },
 { collection: ‘feedEntry‘ }
);

feedEntrySchema.index({entryID : 1});
feedEntrySchema.index({feedID : 1});

var FeedEntryModel = mongoose.model( ‘FeedEntry‘, feedEntrySchema     );

var userFeedEntrySchema = new mongoose.Schema({
     userID: { type: mongoose.Schema.Types.ObjectId },
     feedEntryID: { type: mongoose.Schema.Types.ObjectId },
     feedID: { type: mongoose.Schema.Types.ObjectId },
     read : { type: Boolean, default: false },
 },
 { collection: ‘userFeedEntry‘ }
 );

下面是复合索引实例,每个索引都以升序维护。

userFeedEntrySchema.index({userID : 1, feedID : 1, feedEntryID : 1, read : 1});

var UserFeedEntryModel = mongoose.model(‘UserFeedEntry‘, userFeedEntrySchema );

每个用于 GET、POST、PUT 和 DELETE 的请求需要拥有 1 个正确的内容类型,也就是 application/json。然后下一个 link 会被调用。

下一步需要为每个 URL/verb 定义处理器。参考资料部分附上了所有代码,下面只是代码片段。在这些代码中,Stormpath 带来的便捷一览无余。此外,这里定义的是 /api/v1.0 ,举个例子,这里客户端可以调用的是 /api/v1.0/user/enroll。如果使用 /api/v2.0,/api/v2.0 则可以被使用,当然向下兼容。

启动服务器并运行测试

要启动服务器和运行测试,这里需要遵循几个步骤。

  1. 保证 MongoDB 实例运行,mongod。
  2. 安装 Node 库,npm install。
  3. 开启 REST API 服务器,node server.js。
  4. 运行测试用例:node setuptests.js;jasmine-node createaccountserrorspec.js;jasmine-node createaccountsspec.js;node writecreds.js;jasmine-node feedspec.js。

原文链接:Building your first application with MongoDB: Creating a REST API using the MEAN Stack - Part 2

参考文献:

MEAN实践——LAMP的新时代替代方案(上)

本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

时间: 2024-10-21 19:40:34

MEAN实践——LAMP的新时代替代方案(下)的相关文章

MEAN实践——LAMP的新时代替代方案(上)

[编者按]在九十年代,Linux+Apache+Mysql+PHP 架构曾风靡一时,直到现在仍然是众多 Web 应用程序的基本架构.然而随着需求的变迁和数据流量的激增,LAMP 已不可避免的走下神坛.近日,在 MongoDB Blog 中,Dana Groce 介绍了一个基于新时代架构的实践——MEAN,MongoDB/Mongoose.js.Express.js.Angular.js 和 Node.js.本文系 OneAPM 工程师编译整理: 本系列博客的两篇文章主要关注 MEAN 技术堆栈的

新时代 DevOps 需求下,我们该如何保障服务的安全?

[编者按]时下,传统安全策略显然已无法支撑 DevOps 环境的敏捷需求.那么,对于一个决策者来说,你又该如何实现 DevOps 速度与安全的兼得?本篇译自 Dzone 的一篇运维文章,题为「Security Breaks DevOps – Here's How to Fix It」,由 OneAPM 工程师编译整理. 现在,通信.协作.抽象.自动化.流程等理念已成为快速 DevOps 操作的基础.同时,在虚拟基础设施和 IaaS(基础设施即服务)的冲击下,手动的配置和管理已然不再可行--它需要

马云的阿里正在实践《失控》里人类下一轮进化:连接

1994年写成的<失控>讲了一件事情:连接.按<失控>的核心观点,连接是人类下一轮的进化方式,从以个人为单元的社会进化到以群体为单元的社会,连接是主要的技术实现手段.<失控>还有一个观点,就是现在的人们通过连接成群体来实践下一轮进化,但进化之后的群体智能是什么样子,这超越了现在人们的想象.因此,不能用今天的经验来预测明天的世界,连接之后的群体智能将在连接的过程中自然而然展现出来. 可以说腾讯率先在消费人群中实践了<失控>的思想,这也成就了微信的成功.后来,马

用友财务云,引领智能财务新时代

在中国经济新常态."一带一路"全球化.产业结构升级以及科技日新月异的新时代背景下,每一个企业转型升级都迫在眉睫.企业的发展.扩张.商业模式调整.竞争力提升给财务和业务部门均带来了挑战,大企业财务转型升级势在必行.8月19日,由用友网络主办的"2017中国企业互联网大会"在乌镇召开.以"财务云,引领智能财务新时代"为主题的大企业财务管理转型高峰论坛上,来自中国铝业.北京国家会计学院.山东信发集团.新钢联冶金有限公司.鞍钢集团.天士力控股集团.延长石

欢迎你,企业架构CCIE,RS CCIEv5.0的升级版新时代迎合自动化运维的网工顶级认证

欢迎你,企业架构CCIE,作者乾颐堂安德 或许,这就是能力!想起一句电影台词,"股票是什么,股票是人类对未来的预测,预测对了你就是赢家,预测错了你就是输家",没错预测就是一种能力,2019年6月10日下午乾颐堂茶话会公开课,名字为"思科一出,谁与争锋!",其中的小道消息主要概括为"思科将发布新的CCIE,时间为2020年2月",果然在今天思科就发布了Glance,这个发布时间这么紧凑倒是我没想到的.好了,让我们来看看企业架构CCIE的不同吧,这是

写在新时代伊始

2019年的最后一天稀松平常,但年代之交这种人类自定义的巧合,也给许久没吐槽的我一些落笔的动力.我早就忘了十二岁的我曾对未来有何期待,但仍想给三十岁的自己一些回忆.又十个轮回后,明天或意外,还有那么大的分别吗? 大环境下的焦虑和愿景 在即将走出校园的时刻,我对社会大环境有了前所未有的焦虑.扩展了自己的认知边际后,感触越来越深刻的,无非是自己在社会中的渺小和无力.作为理科生,少有工科能直接改变周围环境的手段或途径,逐渐羡慕起投身科技行业内的变革者:同时对历史和政治的批判,加深了自身在制度和历史问题

刷机的新时代,浅谈,刷机必备神器!

论述:在刷机的新时代,Rom助手被刷机者们简称为"刷机神器",为何会被赋予这么神圣的名字? 刷机的狂潮总是掀起波澜,让我们不得不倾听身边刷机的佳话.数不胜数的刷机软件更让我们看的眼花缭乱,力求简单,快捷,全方位的刷机神器,也因此成为刷机爱好者们共同追求的新目标.在刷机者们看来,这就像是在追求身边简单的奢侈品,总会存在一个简单而不失格调的刷机软件.而在恰当的时间,准确的人群中, rom助手的横空出世,也就名副其实的成为刷机爱好者共同推崇的稀世珍宝. 成功总需付出努力,没有汗水的实践怎会被

新时代的“网盘模型”——够快云库

2013年12月18日,对于够快网络科技有限公司来说,是一个意义非凡的日子.这一天,是够快正式发布其颠覆性产品--"够快云库"的日子.够快云库不同于其他云库,它具有非常明显的两大优势: 1)够快云库采用一种叫"索引同步"的技术,可以让每个独立的云库,实现同步结构和文件的基本属性,而不用像Dropbox一样同步文件本身.够快云库的这种同步方式操作非常迅速,且不占用本地空间,真正做到本地硬盘"零"占用. 2)够快云库的智能算法也帮助用户缓存了一些常用

从自拍到运动摄像、拍摄领域正在引来一个新时代

拍摄到底有多重要?这么说吧,如果国内的OPPO.vivo等手机厂商不能通过拍摄能力"照亮你的美",或许销量就没那么高--毕竟性价比并不占优.此外,iPhone之所以备受摄影爱好者的青睐,就在于iPhone是手机拍摄的标杆之作.在当下,包括手机在内的智能设备已经将拍摄功能提升到最高位置.当然,变革也始终在持续中. 如果说此前拍摄追求的是像素.广角.光圈.算法等,那么现在流行的则是"视角".从传统的前置.后置拍摄,到运动相机带来的多角度拍摄,再到近日谷歌推出的AI夹子相