如何更改chrome页面编码设置 页面信息

使用 Headless Chrome 进行页面渲染6 months ago笔者为了部署方便,使用来进行快速部署,如果你本地存在 Docker 环境,可以使用如下命令快速启动:docker run -d -p
justinribeiro/chrome-headless
如果是在 Mac 下本地使用的话我们还可以创建命令别名:alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
alias chromium="/Applications/Chromium.app/Contents/MacOS/Chromium"
如果是在 Ubuntu 环境下我们可以使用 deb 进行安装:# Install Google Chrome
# /questions/79280/how-to-install-chrome-browser-properly-via-command-line
sudo apt-get install libxss1 libappindicator1 libindicator7
wget /linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome*.deb
# Might show "errors", fixed by next line
sudo apt-get install -f
chrome 命令行也支持丰富的命令行参数,--dump-dom 参数可以将 document.body.innerHTML 打印到标准输出中:chrome --headless --disable-gpu --dump-dom /
而 --print-to-pdf 标识则会将网页输出位 PDF:chrome --headless --disable-gpu --print-to-pdf /
初次之外,我们也可以使用 --screenshot 参数来获取页面截图:chrome --headless --disable-gpu --screenshot /
# Size of a standard letterhead.
chrome --headless --disable-gpu --screenshot --window-size= /
# Nexus 5x
chrome --headless --disable-gpu --screenshot --window-size=412,732 /
如果我们需要更复杂的截图策略,譬如进行完整页面截图则需要利用代码进行远程控制。代码控制启动在上文中我们介绍了如何利用命令行来手动启动 Chrome,这里我们尝试使用 Node.js 来启动 Chrome,最简单的方式就是使用 child_process 来启动:const exec = require('child_process').
function launchHeadlessChrome(url, callback) {
// Assuming MacOSx.
const CHROME = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';
exec(`${CHROME} --headless --disable-gpu --remote-debugging-port=9222 ${url}`, callback);
launchHeadlessChrome('', (err, stdout, stderr) =& {
远程控制这里我们使用
来远程控制 Chrome ,实际上 chrome-remote-interface 是对于
的远程封装,我们可以参考协议文档了解详细的功能与参数。使用 npm 安装完毕之后,我们可以用如下代码片进行简单控制:const CDP = require('chrome-remote-interface');
CDP((client) =& {
// extract domains
const {Network, Page} =
// setup handlers
Network.requestWillBeSent((params) =& {
console.log(params.request.url);
Page.loadEventFired(() =& {
client.close();
// enable events then start!
Promise.all([
Network.enable(),
Page.enable()
]).then(() =& {
return Page.navigate({url: ''});
}).catch((err) =& {
console.error(err);
client.close();
}).on('error', (err) =& {
// cannot connect to the remote endpoint
console.error(err);
我们也可以使用 chrome-remote-interface 提供的命令行功能,譬如我们可以在命令行中访问某个界面并且记录所有的网络请求:$ chrome-remote-interface inspect
&&& Network.enable()
{ result: {} }
&&& Network.requestWillBeSent(params =& params.request.url)
{ 'Network.requestWillBeSent': 'params =& params.request.url' }
&&& Page.navigate({url: 'https://www.wikipedia.org'})
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/' }
{ result: { frameId: '5530.1' } }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia_wordmark.png' }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png' }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/js/index-3b68787aa6.js' }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/js/gt-ie9-c84bf66d33.js' }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/sprite-bookshelf_icons.png?16ed124e8ca7c5ce9d463e8f99b0' }
{ 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/sprite-project-logos.png?9afc01c5efe0a8fbe2ad3eb48fbca' }
我们也可以直接查看内置的接口文档:&&& Page.navigate
{ [Function]
category: 'command',
parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
[ { name: 'frameId',
'$ref': 'FrameId',
hidden: true,
description: 'Frame id that will be navigated.' } ],
description: 'Navigates current page to the given URL.',
handlers: [ 'browser', 'renderer' ] }&&& Page.navigate
{ [Function]
category: 'command',
parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
[ { name: 'frameId',
'$ref': 'FrameId',
hidden: true,
description: 'Frame id that will be navigated.' } ],
description: 'Navigates current page to the given URL.',
handlers: [ 'browser', 'renderer' ] }
我们在上文中还提到需要以代码控制浏览器进行完整页面截图,这里需要利用 Emulation 模块控制页面视口缩放:const CDP = require('chrome-remote-interface');
const argv = require('minimist')(process.argv.slice(2));
const file = require('fs');
// CLI Args
const url = argv.url || '';
const format = argv.format === 'jpeg' ? 'jpeg' : 'png';
const viewportWidth = argv.viewportWidth || 1440;
const viewportHeight = argv.viewportHeight || 900;
const delay = argv.delay || 0;
const userAgent = argv.userA
const fullPage = argv.
// Start the Chrome Debugging Protocol
CDP(async function(client) {
// Extract used DevTools domains.
const {DOM, Emulation, Network, Page, Runtime} =
// Enable events on domains we are interested in.
await Page.enable();
await DOM.enable();
await Network.enable();
// If user agent override was specified, pass to Network domain
if (userAgent) {
await Network.setUserAgentOverride({userAgent});
// Set up viewport resolution, etc.
const deviceMetrics = {
width: viewportWidth,
height: viewportHeight,
deviceScaleFactor: 0,
mobile: false,
fitWindow: false,
await Emulation.setDeviceMetricsOverride(deviceMetrics);
await Emulation.setVisibleSize({width: viewportWidth, height: viewportHeight});
// Navigate to target page
await Page.navigate({url});
// Wait for page load event to take screenshot
Page.loadEventFired(async () =& {
// If the `full` CLI option was passed, we need to measure the height of
// the rendered page and use Emulation.setVisibleSize
if (fullPage) {
const {root: {nodeId: documentNodeId}} = await DOM.getDocument();
const {nodeId: bodyNodeId} = await DOM.querySelector({
selector: 'body',
nodeId: documentNodeId,
const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});
await Emulation.setVisibleSize({width: viewportWidth, height: height});
// This forceViewport call ensures that content outside the viewport is
// rendered, otherwise it shows up as grey. Possibly a bug?
await Emulation.forceViewport({x: 0, y: 0, scale: 1});
setTimeout(async function() {
const screenshot = await Page.captureScreenshot({format});
const buffer = new Buffer(screenshot.data, 'base64');
file.writeFile('output.png', buffer, 'base64', function(err) {
if (err) {
console.error(err);
console.log('Screenshot saved');
client.close();
}, delay);
}).on('error', err =& {
console.error('Cannot connect to browser:', err);
赞赏还没有人赞赏,快来当第一个赞赏的人吧!64收藏分享举报文章被以下专栏收录我的技术世界,Web前端、服务端应用架构、数据挖掘推荐阅读{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&isPending&:false,&contributes&:[{&sourceColumn&:{&lastUpdated&:,&description&:&让知识在它该在的地方&,&permission&:&COLUMN_PUBLIC&,&memberId&:7301234,&contributePermission&:&COLUMN_PRIVATE&,&translatedCommentPermission&:&all&,&canManage&:true,&intro&:&我的技术世界,Web前端、服务端应用架构、数据挖掘&,&urlToken&:&wxyyxc1992&,&id&:24518,&imagePath&:&v2-3cfe2e19.jpg&,&slug&:&wxyyxc1992&,&applyReason&:&0&,&name&:&某熊的全栈之路&,&title&:&某熊的全栈之路&,&url&:&https:\u002F\\u002Fwxyyxc1992&,&commentPermission&:&COLUMN_ALL_CAN_COMMENT&,&canPost&:true,&created&:,&state&:&COLUMN_NORMAL&,&followers&:6686,&avatar&:{&id&:&v2-3cfe2e19&,&template&:&https:\u002F\\u002F{id}_{size}.jpg&},&activateAuthorRequested&:false,&following&:false,&imageUrl&:&https:\u002F\\u002Fv2-3cfe2e19_l.jpg&,&articlesCount&:134},&state&:&accepted&,&targetPost&:{&titleImage&:&https:\u002F\\u002Fv2-d97aee407_r.png&,&lastUpdated&:,&imagePath&:&v2-d97aee407.png&,&permission&:&ARTICLE_PUBLIC&,&topics&:[101,6445,98],&summary&:&\u003Ca href=\&https:\u002F\\u002Fp\u002F\& data-editable=\&true\& data-title=\&使用 Headless Chrome 进行页面渲染\& class=\&\&\u003E使用 Headless Chrome 进行页面渲染\u003C\u002Fa\u003E 从属于笔者的\u003Ca href=\&https:\u002F\u002Fparg.co\u002FbMe\& data-editable=\&true\& data-title=\&Web 开发基础与工程实践\&\u003E Web 开发基础与工程实践\u003C\u002Fa\u003E系列文章,主要介绍了使用 Node.js 利用 Chrome Remote Protocol 远程控制 Headless Chrome 渲染界面的基础用法。本文涉及的参考与引用资料统一列举在\u003Ca href=\&https:\u002F\u002Fparg.co\u002Fbtv\& data-editable=\&true\& data-title=\&这里\&\u003E这里\u003C\u002Fa\u003E。近日笔者在为 \u003Ca href=\&https:\\u002FwxyyxcFdeclarative-crawler\& data-editable=\&true\& data-title=\&declarative-crawler\&\u003Edeclarativ…\u003C\u002Fa\u003E&,&copyPermission&:&ARTICLE_COPYABLE&,&translatedCommentPermission&:&all&,&likes&:0,&origAuthorId&:0,&publishedTime&:&T21:24:15+08:00&,&sourceUrl&:&&,&urlToken&:,&id&:2911136,&withContent&:false,&slug&:,&bigTitleImage&:false,&title&:&使用 Headless Chrome 进行页面渲染&,&url&:&\u002Fp\u002F&,&commentPermission&:&ARTICLE_ALL_CAN_COMMENT&,&snapshotUrl&:&&,&created&:,&comments&:0,&columnId&:24518,&content&:&&,&parentId&:0,&state&:&ARTICLE_PUBLISHED&,&imageUrl&:&https:\u002F\\u002Fv2-d97aee407_r.png&,&author&:{&bio&:&王下邀月熊&,&isFollowing&:false,&hash&:&ed4cd6b92a003a0ce8e801ae74196e19&,&uid&:36,&isOrg&:false,&slug&:&wxyyxc1992&,&isFollowed&:false,&description&:&https:\\u002Fu\u002Fwxyyxc1992\n\nhttps:\\u002Fwxyyxc1992&,&name&:&王下邀月熊&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fwxyyxc1992&,&avatar&:{&id&:&v2-a627d79d2ed03fe6f83a&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&memberId&:7301234,&excerptTitle&:&&,&voteType&:&ARTICLE_VOTE_CLEAR&},&id&:640983}],&title&:&使用 Headless Chrome 进行页面渲染&,&author&:&wxyyxc1992&,&content&:&\u003Cblockquote\u003E\u003Cp\u003E\u003Ca href=\&https:\u002F\\u002Fp\u002F\& class=\&internal\&\u003E使用 Headless Chrome 进行页面渲染\u003C\u002Fa\u003E 从属于笔者的\u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\u002Fparg.co\u002FbMe\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E Web 开发基础与工程实践\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E系列文章,主要介绍了使用 Node.js 利用 Chrome Remote Protocol 远程控制 Headless Chrome 渲染界面的基础用法。本文涉及的参考与引用资料统一列举在\u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\u002Fparg.co\u002Fbtv\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E这里\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E近日笔者在为 \u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002FwxyyxcFdeclarative-crawler\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003Edeclarative-crawler\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E 编写动态页面的蜘蛛,即在\u003Ca href=\&https:\u002F\\u002Fp\u002F\& class=\&internal\&\u003E使用 declarative-crawler 爬取知乎美图\u003C\u002Fa\u003E 一文中介绍的 HeadlessChromeSpider 时,需要选择某个无界面浏览器以执行 JavaScript 代码来动态生成页面。之前笔者往往是使用 PhantomJS 或者 Selenium 执行动态页面渲染,而在 Chrome 59 之后 Chrome 提供了 Headless 模式,其允许在命令行中使用 Chromium 以及 Blink 渲染引擎提供的完整的现代 Web 平台特性。需要注意的是,Headless Chrome 仍然存在一定的局限,相较于 Nightmare 或 Phantom 这样的工具, Chrome 的远程接口仍然无法提供较好的开发者体验。我们在下文介绍的代码示例中也会发现,目前我们仍需要大量的模板代码进行控制。\u003C\u002Fp\u003E\u003Ch1\u003E安装与启动\u003C\u002Fh1\u003E\u003Cp\u003E在 Chrome 安装完毕后我们可以利用其包体内自带的命令行工具启动:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E$ chrome --headless --remote-debugging-port=9222 https:\u002F\u002Fchromium.org\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E笔者为了部署方便,使用\u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\\u002Fr\u002Fjustinribeiro\u002Fchrome-headless\u002F\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E Docker 镜像\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E来进行快速部署,如果你本地存在 Docker 环境,可以使用如下命令快速启动:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Edocker run -d -p
justinribeiro\u002Fchrome-headless\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E如果是在 Mac 下本地使用的话我们还可以创建命令别名:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Ealias chrome=\&\u002FApplications\u002FGoogle\\ Chrome.app\u002FContents\u002FMacOS\u002FGoogle\\ Chrome\&\nalias chrome-canary=\&\u002FApplications\u002FGoogle\\ Chrome\\ Canary.app\u002FContents\u002FMacOS\u002FGoogle\\ Chrome\\ Canary\&\nalias chromium=\&\u002FApplications\u002FChromium.app\u002FContents\u002FMacOS\u002FChromium\&\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E如果是在 Ubuntu 环境下我们可以使用 deb 进行安装:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E# Install Google Chrome\n# https:\\u002Fquestions\u002F7Fhow-to-install-chrome-browser-properly-via-command-line\nsudo apt-get install libxss1 libappindicator1 libindicator7\nwget https:\u002F\\u002Flinux\u002Fdirect\u002Fgoogle-chrome-stable_current_amd64.deb\nsudo dpkg -i google-chrome*.deb
# Might show \&errors\&, fixed by next line\nsudo apt-get install -f\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003Echrome 命令行也支持丰富的命令行参数,--dump-dom 参数可以将 document.body.innerHTML 打印到标准输出中:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Echrome --headless --disable-gpu --dump-dom https:\u002F\\u002F\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E而 --print-to-pdf 标识则会将网页输出位 PDF:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Echrome --headless --disable-gpu --print-to-pdf https:\u002F\\u002F\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E初次之外,我们也可以使用 --screenshot 参数来获取页面截图:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Echrome --headless --disable-gpu --screenshot https:\u002F\\u002F\n\n# Size of a standard letterhead.\nchrome --headless --disable-gpu --screenshot --window-size= https:\u002F\\u002F\n\n# Nexus 5x\nchrome --headless --disable-gpu --screenshot --window-size=412,732 https:\u002F\\u002F\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E如果我们需要更复杂的截图策略,譬如进行完整页面截图则需要利用代码进行远程控制。\u003C\u002Fp\u003E\u003Ch1\u003E代码控制\u003C\u002Fh1\u003E\u003Ch2\u003E启动\u003C\u002Fh2\u003E\u003Cp\u003E在上文中我们介绍了如何利用命令行来手动启动 Chrome,这里我们尝试使用 Node.js 来启动 Chrome,最简单的方式就是使用 child_process 来启动:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Econst exec = require('child_process').\n\nfunction launchHeadlessChrome(url, callback) {\n
\u002F\u002F Assuming MacOSx.\n
const CHROME = '\u002FApplications\u002FGoogle\\ Chrome.app\u002FContents\u002FMacOS\u002FGoogle\\ Chrome';\n
exec(`${CHROME} --headless --disable-gpu --remote-debugging-port=9222 ${url}`, callback);\n}\n\nlaunchHeadlessChrome('https:\u002F\', (err, stdout, stderr) =& {\n
...\n});\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Ch2\u003E远程控制\u003C\u002Fh2\u003E\u003Cp\u003E这里我们使用 \u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fcyrus-and\u002Fchrome-remote-interface\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003Echrome-remote-interface\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E 来远程控制 Chrome ,实际上 chrome-remote-interface 是对于 \u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\u002Fchromedevtools.github.io\u002Fdevtools-protocol\u002Ftot\u002FInput\u002F\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EChrome DevTools Protocol\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E 的远程封装,我们可以参考协议文档了解详细的功能与参数。使用 npm 安装完毕之后,我们可以用如下代码片进行简单控制:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Econst CDP = require('chrome-remote-interface');\n\nCDP((client) =& {\n
\u002F\u002F extract domains\n
const {Network, Page} =\n
\u002F\u002F setup handlers\n
Network.requestWillBeSent((params) =& {\n
console.log(params.request.url);\n
Page.loadEventFired(() =& {\n
client.close();\n
\u002F\u002F enable events then start!\n
Promise.all([\n
Network.enable(),\n
Page.enable()\n
]).then(() =& {\n
return Page.navigate({url: 'https:\'});\n
}).catch((err) =& {\n
console.error(err);\n
client.close();\n
});\n}).on('error', (err) =& {\n
\u002F\u002F cannot connect to the remote endpoint\n
console.error(err);\n});\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们也可以使用 chrome-remote-interface 提供的命令行功能,譬如我们可以在命令行中访问某个界面并且记录所有的网络请求:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E$ chrome-remote-interface inspect\n&&& Network.enable()\n{ result: {} }\n&&& Network.requestWillBeSent(params =& params.request.url)\n{ 'Network.requestWillBeSent': 'params =& params.request.url' }\n&&& Page.navigate({url: 'https:\u002F\u002Fwww.wikipedia.org'})\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002F' }\n{ result: { frameId: '5530.1' } }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fimg\u002FWikipedia_wordmark.png' }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fimg\u002FWikipedia-logo-v2.png' }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fjs\u002Findex-3b68787aa6.js' }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fjs\u002Fgt-ie9-c84bf66d33.js' }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fimg\u002Fsprite-bookshelf_icons.png?16ed124e8ca7c5ce9d463e8f99b0' }\n{ 'Network.requestWillBeSent': 'https:\u002F\u002Fwww.wikipedia.org\u002Fportal\u002Fwikipedia.org\u002Fassets\u002Fimg\u002Fsprite-project-logos.png?9afc01c5efe0a8fbe2ad3eb48fbca' }\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们也可以直接查看内置的接口文档:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E&&& Page.navigate\n{ [Function]\n
category: 'command',\n
parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },\n
returns:\n
[ { name: 'frameId',\n
'$ref': 'FrameId',\n
hidden: true,\n
description: 'Frame id that will be navigated.' } ],\n
description: 'Navigates current page to the given URL.',\n
handlers: [ 'browser', 'renderer' ] }&&& Page.navigate\n{ [Function]\n
category: 'command',\n
parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },\n
returns:\n
[ { name: 'frameId',\n
'$ref': 'FrameId',\n
hidden: true,\n
description: 'Frame id that will be navigated.' } ],\n
description: 'Navigates current page to the given URL.',\n
handlers: [ 'browser', 'renderer' ] }\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们在上文中还提到需要以代码控制浏览器进行完整页面截图,这里需要利用 Emulation 模块控制页面视口缩放:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Econst CDP = require('chrome-remote-interface');\nconst argv = require('minimist')(process.argv.slice(2));\nconst file = require('fs');\n\n\u002F\u002F CLI Args\nconst url = argv.url || 'https:\u002F\';\nconst format = argv.format === 'jpeg' ? 'jpeg' : 'png';\nconst viewportWidth = argv.viewportWidth || 1440;\nconst viewportHeight = argv.viewportHeight || 900;\nconst delay = argv.delay || 0;\nconst userAgent = argv.userA\nconst fullPage = argv.\n\n\u002F\u002F Start the Chrome Debugging Protocol\nCDP(async function(client) {\n
\u002F\u002F Extract used DevTools domains.\n
const {DOM, Emulation, Network, Page, Runtime} =\n\n
\u002F\u002F Enable events on domains we are interested in.\n
await Page.enable();\n
await DOM.enable();\n
await Network.enable();\n\n
\u002F\u002F If user agent override was specified, pass to Network domain\n
if (userAgent) {\n
await Network.setUserAgentOverride({userAgent});\n
\u002F\u002F Set up viewport resolution, etc.\n
const deviceMetrics = {\n
width: viewportWidth,\n
height: viewportHeight,\n
deviceScaleFactor: 0,\n
mobile: false,\n
fitWindow: false,\n
await Emulation.setDeviceMetricsOverride(deviceMetrics);\n
await Emulation.setVisibleSize({width: viewportWidth, height: viewportHeight});\n\n
\u002F\u002F Navigate to target page\n
await Page.navigate({url});\n\n
\u002F\u002F Wait for page load event to take screenshot\n
Page.loadEventFired(async () =& {\n
\u002F\u002F If the `full` CLI option was passed, we need to measure the height of\n
\u002F\u002F the rendered page and use Emulation.setVisibleSize\n
if (fullPage) {\n
const {root: {nodeId: documentNodeId}} = await DOM.getDocument();\n
const {nodeId: bodyNodeId} = await DOM.querySelector({\n
selector: 'body',\n
nodeId: documentNodeId,\n
const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});\n\n
await Emulation.setVisibleSize({width: viewportWidth, height: height});\n
\u002F\u002F This forceViewport call ensures that content outside the viewport is\n
\u002F\u002F rendered, otherwise it shows up as grey. Possibly a bug?\n
await Emulation.forceViewport({x: 0, y: 0, scale: 1});\n
setTimeout(async function() {\n
const screenshot = await Page.captureScreenshot({format});\n
const buffer = new Buffer(screenshot.data, 'base64');\n
file.writeFile('output.png', buffer, 'base64', function(err) {\n
if (err) {\n
console.error(err);\n
} else {\n
console.log('Screenshot saved');\n
client.close();\n
}, delay);\n
});\n}).on('error', err =& {\n
console.error('Cannot connect to browser:', err);\n});\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E&,&updated&:new Date(&T13:24:15.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:10,&collapsedCount&:0,&likeCount&:64,&state&:&published&,&isLiked&:false,&slug&:&&,&lastestTipjarors&:[],&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\\u002Fv2-d97aee407_r.png&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&reviewers&:[],&topics&:[{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&Google Chrome&},{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&Node.js&},{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&Web 开发&}],&adminClosedComment&:false,&titleImageSize&:{&width&:1141,&height&:603},&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&column&:{&slug&:&wxyyxc1992&,&name&:&某熊的全栈之路&},&tipjarState&:&activated&,&tipjarTagLine&:&大爷赏点呗~&,&sourceUrl&:&&,&pageCommentsCount&:10,&tipjarorCount&:0,&annotationAction&:[],&hasPublishingDraft&:false,&snapshotUrl&:&&,&publishedTime&:&T21:24:15+08:00&,&url&:&\u002Fp\u002F&,&lastestLikers&:[{&bio&:&大学公益社团老兵、开源爱好者、Web 前端工程师&,&isFollowing&:false,&hash&:&494f1f3620beb7a14767&,&uid&:32,&isOrg&:false,&slug&:&Tech_Query&,&isFollowed&:false,&description&:&小吃货、特困生、渣情商……&,&name&:&水歌&,&profileUrl&:&https:\u002F\\u002Fpeople\u002FTech_Query&,&avatar&:{&id&:&1cdf30d88&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&典型码农&,&isFollowing&:false,&hash&:&1e6a84a5f3eecc&,&uid&:08,&isOrg&:false,&slug&:&richard-shi-51&,&isFollowed&:false,&description&:&&,&name&:&richard&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Frichard-shi-51&,&avatar&:{&id&:&&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&https:\\u002Fjiangtao&,&isFollowing&:false,&hash&:&63ad9aafd81eed11fe340e&,&uid&:92,&isOrg&:false,&slug&:&Jerret321&,&isFollowed&:false,&description&:&坚持沉淀,坚持深入,突破边界。&,&name&:&jiangtao&,&profileUrl&:&https:\u002F\\u002Fpeople\u002FJerret321&,&avatar&:{&id&:&aa9b4c68dc440bfdc5a6a5&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.png&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&程序猿&,&isFollowing&:false,&hash&:&a936b5a2ce37cf3fd37497&,&uid&:390100,&isOrg&:false,&slug&:&star-82-25-55&,&isFollowed&:false,&description&:&&,&name&:&star&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fstar-82-25-55&,&avatar&:{&id&:&v2-16c193bbd2170dbde294e&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&IT,nodejs&,&isFollowing&:false,&hash&:&03d8ba04ac09fe2b4b35a48ea438ca36&,&uid&:298700,&isOrg&:false,&slug&:&ding-ge-wen-nuan&,&isFollowed&:false,&description&:&&,&name&:&定格温暖&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fding-ge-wen-nuan&,&avatar&:{&id&:&da8e974dc&,&template&:&https:\u002F\\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false}],&summary&:&\u003Ca href=\&https:\u002F\\u002Fp\u002F\& data-editable=\&true\& data-title=\&使用 Headless Chrome 进行页面渲染\& class=\&\&\u003E使用 Headless Chrome 进行页面渲染\u003C\u002Fa\u003E 从属于笔者的\u003Ca href=\&https:\u002F\u002Fparg.co\u002FbMe\& data-editable=\&true\& data-title=\&Web 开发基础与工程实践\&\u003E Web 开发基础与工程实践\u003C\u002Fa\u003E系列文章,主要介绍了使用 Node.js 利用 Chrome Remote Protocol 远程控制 Headless Chrome 渲染界面的基础用法。本文涉及的参考与引用资料统一列举在\u003Ca href=\&https:\u002F\u002Fparg.co\u002Fbtv\& data-editable=\&true\& data-title=\&这里\&\u003E这里\u003C\u002Fa\u003E。近日笔者在为 \u003Ca href=\&https:\\u002FwxyyxcFdeclarative-crawler\& data-editable=\&true\& data-title=\&declarative-crawler\&\u003Edeclarativ…\u003C\u002Fa\u003E&,&reviewingCommentsCount&:0,&meta&:{&previous&:{&isTitleImageFullScreen&:true,&rating&:&none&,&titleImage&:&https:\u002F\\u002F50\u002Fv2-dbd7e577c4d7e250d0a021_xl.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&topics&:[{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&iOS&},{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&Web 开发&},{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&Android&}],&adminClosedComment&:false,&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&author&:{&bio&:&王下邀月熊&,&isFollowing&:false,&hash&:&ed4cd6b92a003a0ce8e801ae74196e19&,&uid&:36,&isOrg&:false,&slug&:&wxyyxc1992&,&isFollowed&:false,&description&:&https:\\u002Fu\u002Fwxyyxc1992\n\nhttps:\\u002Fwxyyxc1992&,&name&:&王下邀月熊&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fwxyyxc1992&,&avatar&:{&id&:&v2-a627d79d2ed03fe6f83a&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&column&:{&slug&:&wxyyxc1992&,&name&:&某熊的全栈之路&},&content&:&\u003Cblockquote\u003E\u003Cp\u003E随着现代浏览器的日渐流行,Web 以及混合开发技术的发展,大前端的概念日渐成为某种共识;而无论 iOS、Android、Web 这样的端开发还是 React Native、Weex 这样的跨端开发,其术不同而道相似。笔者在日前总结的\u003Ca href=\&https:\u002F\\u002Fp\u002F\& class=\&internal\&\u003E泛前端知识图谱(Web\u002FiOS\u002FAndroid\u002FRN)\u003C\u002Fa\u003E 一文中就对泛前端开发学习中可能会涉及到的知识点进行了总结与盘点,近日笔者打算为该知识图谱编写附带的简略说明,因此先对旧文\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fa\u002F6817\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E GUI 应用程序架构的十年变迁:MVC、MVP、MVVM、Unidirectional、Clean \u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E进行了重制与补充,从属于笔者的\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002FwxyyxcFFrontendTechnology-Handbook\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E大前端开发技术相关仓库\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E,同样向 Martin Fowler 及其撰写的 \u003Ca href=\&http:\u002F\\u002F?target=http%3A\\u002FeaaDev\u002FuiArchs.html\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EGUI Architectures\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E 致敬。其他关联文章建议阅读 \u003Ca href=\&https:\u002F\\u002Fp\u002F\& class=\&internal\&\u003E2016-我的前端之路:工具化与工程化 By 王下邀月熊\u003C\u002Fa\u003E、\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fa\u002F2245\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E2015-我的前端之路:数据流驱动的界面 By 王下邀月熊\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E。另外,本文所有参考引用的文章统一声明在\u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\u002Fparg.co\u002Fbt3\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E这里\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Ch1\u003E前言\u003C\u002Fh1\u003E\u003Cblockquote\u003E\u003Cp\u003E\u003Cstrong\u003EMake everything as simple as possible, but not simpler — Albert Einstein\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E十年前,Martin Fowler 撰写了\u003Ca href=\&http:\u002F\\u002F?target=http%3A\\u002FeaaDev\u002FuiArchs.html\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E GUI Architectures \u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E一文,至今被奉为经典。本文所谈的所谓架构二字,核心即是对于对于富客户端的\u003Cstrong\u003E代码组织\u002F职责划分\u003C\u002Fstrong\u003E。纵览这十年内的架构模式变迁,大概可以分为 MV* 与 Unidirectional 两大类,而 Clean Architecture 则是以严格的层次划分独辟蹊径。从笔者的认知来看,从 MVC 到 MVP 的变迁完成了对于 View 与 Model 的解耦合,改进了职责分配与可测试性。而从 MVP 到 MVVM,添加了 View 与 ViewModel 之间的数据绑定,使得 View 完全的无状态化。最后,整个从 MV* 到 Unidirectional 的变迁即是采用了消息队列式的数据流驱动的架构,并且以 Redux 为代表的方案将原本 MV* 中碎片化的状态管理变为了统一的状态管理,保证了状态的有序性与可回溯性。\u003Cbr\u003E笔者在撰写本文的时候也不可避免的带了很多自己的观点,在漫长的 GUI 架构模式变迁过程中,很多概念其实是交错复杂,典型的譬如 MVP 与 MVVM 的区别,笔者按照自己的理解强行定义了二者的区分边界,不可避免的带着自己的主观想法。另外,鉴于笔者目前主要进行的是Web 方面的开发,因此在整体倾向上是支持 Unidirectional Architecture 并且认为集中式的状态管理是正确的方向(注:MobX 也是极好的)。但是必须要强调,GUI 架构本身是无法脱离其所依托的平台,下文笔者也会浅述由于 Android 与 iOS 本身 SDK API 的特殊性,生搬硬套其他平台的架构模式也是邯郸学步,沐猴而冠。不过总结而言,它山之石,可以攻玉,本身我们所处的开发环境一直在不断变化,对于过去的精华自当应该保留,并且与新的环境相互印证,触类旁通。\u003C\u002Fp\u003E\u003Ch1\u003EGUI 应用程序架构\u003C\u002Fh1\u003E\u003Cp\u003EGraphical User Interfaces一直是软件开发领域的重要组成部分,从当年的MFC,到WinForm\u002FJava Swing,再到WebAPP\u002FAndroid\u002FiOS引领的智能设备潮流,以及未来可能的AR\u002FVR,GUI应用开发中所面临的问题一直在不断演变,但是从各种具体问题中抽象而出的可以复用的模式恒久存在。而这些模式也就是所谓应用架构的核心与基础。对于所谓应用架构,空谈误事,不谈误己,笔者相信不仅仅只有自己想把那一团糟的代码给彻底抛弃。往往对于架构的认知需要一定的大局观与格局眼光,每个有一定经验的客户端程序开发者,无论是Web、iOS还是Android,都会有自己熟悉的开发流程习惯,但是笔者认为架构认知更多的是道,而非术。当你能够以一种指导思想在不同的平台上能够进行高效地开发时,你才能真正理解架构。这个有点像张三丰学武,心中无招,方才达成。笔者这么说只是为了强调,尽量地可以不拘泥于某个平台的具体实现去审视GUI应用程序架构模式,会让你有不一样的体验。譬如下面这个组装Android机器人的图:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-4e135469aad230c077cd6cec474e9d34_b.jpg\& data-rawwidth=\&600\& data-rawheight=\&622\& class=\&origin_image zh-lightbox-thumb\& width=\&600\& data-original=\&http:\u002F\\u002Fv2-4e135469aad230c077cd6cec474e9d34_r.jpg\&\u003E\u003Cp\u003E怎么去焊接两个组件,属于具体的术实现,而应该焊接哪两个组件就是术,作为合格的架构师总不能把脚和头直接焊接在一起,而忽略中间的连接模块。对于软件开发中任何一个方面,我们都希望能够寻找到一个抽象程度适中,能够在接下来的4,5年内正常运行与方便维护扩展的开发模式。引申下笔者在\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fa\u002F2245\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E我的编程之路\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E中的论述,目前在GUI架构模式中,无论是Android、iOS还是Web,都在经历着从命令式编程到声明式\u002F响应式编程,从Passive Components到Reactive Components,从以元素操作为核心到以数据流驱动为核心的变迁(关于这几句话的解释可以参阅下文的Declarative vs. Imperative这一小节)。\u003C\u002Fp\u003E\u003Ch2\u003E基础概念\u003C\u002Fh2\u003E\u003Cp\u003E正文之前,我们先对一些概念进行阐述:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EUser Events\u002F用户事件:即是来自于可输入设备上的用户操作产生的数据,譬如鼠标点击、滚动、键盘输入、触摸等等。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EUser Interface Rendering\u002F用户界面渲染:View这个名词在前后端开发中都被广泛使用,为了明晰该词的含义,我们在这里使用用户渲染这个概念,来描述View,即是以HTML或者JSX或者XAML等等方式在屏幕上产生的图形化输出内容。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EUI Application:允许接收用户输入,并且将输出渲染到屏幕上的应用程序,该程序能够长期运行而不只是渲染一次即结束\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch3\u003EPassive Module & Reactive Module\u003C\u002Fh3\u003E\u003Cp\u003E箭头表示的归属权实际上也是Passive Programming与Reactive Programming的区别,譬如我们的系统中有Foo与Bar两个模块,可以把它们当做OOP中的两个类。如果我们在Foo与Bar之间建立一个箭头,也就意味着Foo能够影响Bar中的状态:\u003Cbr\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-dce5d8e544f_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&174\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-dce5d8e544f_r.jpg\&\u003E譬如Foo在进行一次网络请求之后将Bar内部的计数器加一操作:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F This is inside the Foo module\n\nfunction onNetworkRequest() {\n
\u002F\u002F ...\n
Bar.incrementCounter();\n
\u002F\u002F ...\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E在这里将这种逻辑关系可以描述为Foo拥有着网络请求完成之后将Bar内的计数器加一这个关系的控制权,也就是Foo占有主导性,而Bar相对而言是Passive被动的:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-abbab419f685a581e759fa08d5e49e3f_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&217\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-abbab419f685a581e759fa08d5e49e3f_r.jpg\&\u003E\u003Cp\u003EBar是Passive的,它允许其他模块改变其内部状态。而Foo是主动地,它需要保证能够正确地更新Bar的内部状态,Passive模块并不知道谁会更新到它。而另一种方案就是类似于控制反转,由Bar完成对于自己内部状态的更新:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-b88b88b39bdfe1be7bfe1c6_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&228\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-b88b88b39bdfe1be7bfe1c6_r.jpg\&\u003E\u003Cp\u003E在这种模式下,Bar监听来自于Foo中的事件,并且在某些事件发生之后进行内部状态更新:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F This is inside the Bar module\nFoo.addOnNetworkRequestListener(() =& {\n
self.incrementCounter(); \u002F\u002F self is Bar\n});\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E此时Bar就变成了Reactive Module,它负责自己的内部的状态更新以响应外部的事件,而Foo并不知道它发出的事件会被谁监听。\u003C\u002Fp\u003E\u003Ch3\u003EDeclarative & Imperative\u003C\u002Fh3\u003E\u003Cp\u003E形象地来描述命令式编程与声明式编程的区别,就好像C#\u002FJavaScript与类似于XML或者HTML这样的标记语言之间的区别。命令式编程关注于how to do what you want to do,即事必躬亲,需要安排好每个要做的细节。而声明式编程关注于what you want to do without worrying about how,即只需要声明要做的事情而不用将具体的过程再耦合进来。对于开发者而言,声明式编程将很多底层的实现细节向开发者隐藏,而使得开发者可以专注于具体的业务逻辑,同时也保证了代码的解耦与单一职责。譬如在Web开发中,如果你要基于jQuery将数据填充到页面上,那么大概按照命令式编程的模式你需要这么做:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evar options = $(\&#options\&);\n$.each(result, function() {\n
options.append($(\&&option \u002F&\&).val(this.id).text(this.name));\n});\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E而以Angular 1声明式的方式进行编写,那么是如下的标记模样:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E&div ng-repeat=\&item in items\& ng-click=\&select(item)\&&{{item.name}}\n&\u002Fdiv&\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E而在iOS和Android开发中,近年来函数响应式编程(Functional Reactive Programming)也非常流行,参阅笔者关于响应式编程的介绍可以了解,响应式编程本身是基于流的方式对于异步操作的一种编程优化,其在整个应用架构的角度看更多的是细节点的优化。以\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002FReactiveX\u002FRxSwift\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003ERxSwift\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E为例,通过响应式编程可以编写出非常优雅的用户交互代码:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Elet searchResults = searchBar.rx_text\n
.throttle(0.3, scheduler: MainScheduler.instance)\n
.distinctUntilChanged()\n
.flatMapLatest { query -& Observable&[Repository]& in\n
if query.isEmpty {\n
return Observable.just([])\n
return searchGitHub(query)\n
.catchErrorJustReturn([])\n
.observeOn(MainScheduler.instance)\nsearchResults\n
.bindTo(tableView.rx_itemsWithCellIdentifier(\&Cell\&)) {\n
(index, repository: Repository, cell) in\n
cell.textLabel?.text = repository.name\n
cell.detailTextLabel?.text = repository.url\n
.addDisposableTo(disposeBag)\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E其直观的效果大概如下图所示:\u003Cbr\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-ef92d0b354a_b.gif\& data-rawwidth=\&388\& data-rawheight=\&692\& data-thumbnail=\&http:\u002F\\u002Fv2-ef92d0b354a_b.jpg\& class=\&content_image\& width=\&388\&\u003E到这里可以看出,无论是从命令式编程与声明式编程的对比还是响应式编程的使用,我们开发时的关注点都慢慢转向了所谓的数据流。便如MVVM,虽然它还是双向数据流,但是其使用的Data-Binding也意味着开发人员不需要再去以命令地方式寻找元素,而更多地关注于应该给绑定的对象赋予何值,这也是数据流驱动的一个重要体现。而Unidirectional Architecture采用了类似于Event Source的方式,更是彻底地将组件之间、组件与功能模块之间的关联交于数据流操控。\u003C\u002Fp\u003E\u003Ch2\u003E何谓架构\u003C\u002Fh2\u003E\u003Cp\u003E当我们谈论所谓客户端开发的时候,我们首先会想到怎么保证向后兼容、怎么使用本地存储、怎么调用远程接口、如何有效地利用内存\u002F带宽\u002FCPU等资源,不过最核心的还是怎么绘制界面并且与用户进行交互。而当我们提纲挈领、高屋建瓴地以一个较高的抽象的视角来审视总结这个知识点的时候会发现,我们希望的好的架构,便如在引言中所说,即是有好的代码组织方式\u002F合理的职责划分粒度。笔者脑中会出现如下这样的一个层次结构,可以看出,最核心的即为View与ViewLogic这两部分:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-93a1bd16a010fc7e9cebeee_b.jpg\& data-rawwidth=\&681\& data-rawheight=\&800\& class=\&origin_image zh-lightbox-thumb\& width=\&681\& data-original=\&http:\u002F\\u002Fv2-93a1bd16a010fc7e9cebeee_r.jpg\&\u003E\u003Cp\u003E实际上,对于富客户端的\u003Cstrong\u003E代码组织\u002F职责划分\u003C\u002Fstrong\u003E,从具体的代码分割的角度,即是\u003Cstrong\u003E功能的模块化\u003C\u002Fstrong\u003E、\u003Cstrong\u003E界面的组件化\u003C\u002Fstrong\u003E、\u003Cstrong\u003E状态管理\u003C\u002Fstrong\u003E这三个方面。最终呈献给用户的界面,笔者认为可以抽象为如下等式:View=f(State,Template)。而 ViewLogic 中对于类\u002F模块之间的依赖关系,即属于代码组织,譬如 MVC 中的 View 与 Controller 之间的从属关系。而对于动态数据,即所谓应用数据的管理,属于状态管理这一部分,譬如APP从后来获取了一系列的数据,如何将这些数据渲染到用户界面上使得用户可见,这样的不同部分之间的协同关系、整个数据流的流动,即属于状态管理。\u003C\u002Fp\u003E\u003Ch3\u003E不断衍化的架构\u003C\u002Fh3\u003E\u003Cp\u003E兵无常势,水无常形。实际上从MVC、MVP到MVVM,一直围绕的核心问题就是如何分割ViewLogic与View,即如何将负责界面展示的代码与负责业务逻辑的代码进行分割。所谓分久必合,合久必分,从笔者自我审视的角度,发现很有趣的一点。Android与iOS中都是从早期的用代码进行组件添加与布局到专门的XML\u002FNib\u002FStoryBoard文件进行布局,Android中的Annotation\u002FDataBinding、iOS中的IBOutlet更加地保证了View与ViewLogic的分割(这一点也是从元素操作到以数据流驱动的变迁,我们不需要再去编写大量的 findViewById。而Web的趋势正好有点相反,无论是WebComponent还是ReactiveComponent都是将ViewLogic与View置于一起,特别是JSX的语法将JavaScript与HTML混搭,很像当年的PHP\u002FJSP与HTML混搭。这一点也是由笔者在上文提及的Android\u002FiOS本身封装程度较高的、规范的API决定的。对于Android\u002FiOS与Web之间开发体验的差异,笔者感觉很类似于静态类型语言与动态类型语言之间的差异。(注:使用 TypeScript 与 Flow 同样能为 Web 开发引入静态类型语言的优势)\u003C\u002Fp\u003E\u003Ch3\u003E功能的模块化\u003C\u002Fh3\u003E\u003Cp\u003E老实说在AMD\u002FCMD规范之前,或者说在ES6的模块引入与Webpack的模块打包出来之前,功能的模块化依赖一直也是个很头疼的问题。\u003Cbr\u003ESOLID中的接口隔离原则,大量的IOC或者DI工具可以帮我们完成这一点,就好像Spring中的@Autowire或者Angular 1中的@Injection,都给笔者很好地代码体验。\u003Cbr\u003E在这里笔者首先要强调下,从代码组织的角度来看,项目的构建工具与依赖管理工具会深刻地影响到代码组织,这一点在功能的模块化中尤其显著。譬如笔者对于Android\u002FJava构建工具的使用变迁经历了从Eclipse到Maven再到Gradle,笔者会将不同功能逻辑的代码封装到不同的相对独立的子项目中,这样就保证了子项目与主项目之间的一定隔离,方便了测试与代码维护。同样的,在Web开发中从AMD\u002FCMD规范到标准的ES6模块与Webpack编译打包,也使得代码能够按照功能尽可能地解耦分割与避免冗余编码。而另一方面,依赖管理工具也极大地方便我们使用第三方的代码与发布自定义的依赖项,譬如Web中的NPM与Bower,iOS中的CocoaPods都是十分优秀的依赖发布与管理工具,使我们不需要去关心第三方依赖的具体实现细节即能够透明地引入使用。因此选择合适的项目构建工具与依赖管理工具也是好的GUI架构模式的重要因素之一。不过从应用程序架构的角度看,无论我们使用怎样的构建工具,都可以实现或者遵循某种架构模式,笔者认为二者之间也并没有必然的因果关系。\u003C\u002Fp\u003E\u003Ch3\u003E界面的组件化与无状态组件\u003C\u002Fh3\u003E\u003Cblockquote\u003E\u003Cp\u003EA component is a small piece of the user interface of our application, a view, that can be composed with other components to make more advanced components.\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E何谓组件?一个组件即是应用中用户交互界面的部分组成,组件可以通过组合封装成更高级的组件。组件可以被放入层次化的结构中,即可以是其他组件的父组件也可以是其他组件的子组件。根据上述的组件定义,笔者认为像Activity或者UIViewController都不能算是组件,而像ListView或者UITableView可以看做典型的组件。\u003Cbr\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-5d45aabd1babea_b.png\& data-rawwidth=\&800\& data-rawheight=\&699\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-5d45aabd1babea_r.png\&\u003E我们强调的是界面组件的Composable&Reusable,即可组合性与可重用性。当我们一开始接触到Android或者iOS时,因为本身SDK的完善度与规范度较高,我们能够很多使用封装程度较高的组件。譬如ListView,无论是Android中的RecycleView还是iOS中的UITableView或者UICollectionView,都为我们提供了。凡事都有双面性,这种较高程度的封装与规范统一的API方便了我们的开发,但是也限制了我们自定义的能力。同样的,因为SDK的限制,真正意义上可复用\u002F组合的组件也是不多,譬如你不能将两个ListView再组合成一个新的ListView。在React中有所谓的controller-view的概念,即意味着某个React组件同时担负起MVC中Controller与View的责任,也就是JSX这种将负责ViewLogic的JavaScript代码与负责模板的HTML混编的方式。\u003Cbr\u003E界面的组件化还包括一个重要的点就是路由,譬如Android中的\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fcampusappcn\u002FAndRouter\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EAndRouter\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E、iOS中的\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fjoeldev\u002FJLRoutes\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EJLRoutes\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E都是集中式路由的解决方案,不过集中式路由在Android或者iOS中并没有大规模推广。iOS中的StoryBoard倒是类似于一种集中式路由的方案,不过更偏向于以UI设计为核心。笔者认为这一点可能是因为Android或者iOS本身所有的代码都是存放于客户端本身,而Web中较传统的多页应用方式还需要用户跳转页面重新加载,而后在单页流行之后即不存在页面级别的跳转,因此在Web单页应用中集中式路由较为流行而Android、iOS中反而不流行。\u003C\u002Fp\u003E\u003Cp\u003E无状态的组件的构建函数是纯函数(pure function)并且引用透明的(refferentially transparent),在相同输入的情况下一定会产生相同的组件输出,即符合View=f(State,Template)公式。笔者觉得Android中的ListView\u002FRecycleView,或者iOS中的UITableView,也是无状态组件的典型。譬如在Android中,可以通过动态设置Adapter实例来为RecycleView进行源数据的设置,而作为View层以IoC的方式与具体的数据逻辑解耦。\u003Cbr\u003E组件的可组合性与可重用性往往最大的阻碍就是状态,一般来说,我们希望能够重用或者组合的组件都是\u003Cbr\u003EGeneralization,而状态往往是Specification,即领域特定的。同时,状态也会使得代码的可读性与可测试性降低,在有状态的组件中,我们并不能通过简单地阅读代码就知道其功能。如果借用函数式编程的概念,就是因为副作用的引入使得函数每次回产生不同的结果。函数式编程中存在着所谓Pure Function,即纯函数的概念,函数的返回值永远只受到输入参数的影响。譬如(x)=>x?2这个函数,输入的x值永远不会被改变,并且返回值只是依赖于输入的参数。而Web开发中我们也经常会处于带有状态与副作用的环境,典型的就是Browser中的DOM,之前在jQuery时代我们会经常将一些数据信息缓存在DOM树上,也是典型的将状态与模板混合的用法。这就导致了我们并不能控制到底应该何时去进行重新渲染以及哪些状态变更的操作才是必须的,\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evar Header = component(function (data) {\n
\u002F\u002F First argument is h1 metadata\n
return h1(null, data.text);\n});\n\n\u002F\u002F Render the component to our DOM\nrender(Header({text: 'Hello'}), document.body);\n\n\u002F\u002F Some time later, we change it, by calling the\n\u002F\u002F component once more.\nsetTimeout(function () {\n
render(Header({text: 'Changed'}), document.body);\n}, 1000);\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evar hello = Header({ text: 'Hello' }); var bye = Header({ text: 'Good Bye' });\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Ch3\u003E状态管理\u003C\u002Fh3\u003E\u003Cblockquote\u003E\u003Cp\u003E可变的与不可预测的状态是软件开发中的万恶之源\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E上文提及,我们尽可能地希望组件的无状态性,那么整个应用中的状态管理应该尽量地放置在所谓High-Order Component或者Smart Component中。在React以及Flux的概念流行之后,Stateless Component的概念深入人心,不过其实对于MVVM中的View,也是无状态的View。通过双向数据绑定将界面上的某个元素与ViewModel中的变量相关联,笔者认为很类似于HOC模式中的Container与Component之间的关联。随着应用的界面与功能的扩展,状态管理会变得愈发混乱。这一点,无论前后端都有异曲同工之难,笔者在\u003Ca href=\&http:\u002F\\u002F?target=https%3A\\u002Fa\u002F9478\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E基于Redux思想与RxJava的SpringMVC中Controller的代码风格实践\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E一文中对于服务端应用程序开发中的状态管理有过些许讨论。\u003C\u002Fp\u003E\u003Ch2\u003E何谓好的架构\u003C\u002Fh2\u003E\u003Ch3\u003EBalanced Distribution of Responsibilities: 合理的职责划分\u003C\u002Fh3\u003E\u003Cp\u003E合理的职责划分即是保证系统中的不同组件能够被分配合理的职责,也就是在复杂度之间达成一个平衡,职责划分最权威的原则就是所谓Single Responsibility Principle,单一职责原则。\u003C\u002Fp\u003E\u003Ch3\u003ETestability: 可测试性\u003C\u002Fh3\u003E\u003Cp\u003E可测试性是保证软件工程质量的重要手段之一,也是保证产品可用性的重要途径。在传统的GUI程序开发中,特别是对于界面的测试常常设置于状态或者运行环境,并且很多与用户交互相关的测试很难进行场景重现,或者需要大量的人工操作去模拟真实环境。\u003C\u002Fp\u003E\u003Ch3\u003EEase of Use: 易用性\u003C\u002Fh3\u003E\u003Cp\u003E代码的易用性保证了程序架构的简洁与可维护性,所谓最好的代码就是永远不需要重写的代码,而程序开发中尽量避免的代码复用方法就是复制粘贴。\u003C\u002Fp\u003E\u003Ch3\u003EFractal: 碎片化,易于封装与分发\u003C\u002Fh3\u003E\u003Cblockquote\u003E\u003Cp\u003EIn fractal architectures, the whole can be naively packaged as a component to be used in some larger \u003Ca href=\&http:\u002F\\u002F?target=http%3A\u002F\u002Fapplication.In\& class=\& external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E\u003Cspan class=\&invisible\&\u003Ehttp:\u002F\u002F\u003C\u002Fspan\u003E\u003Cspan class=\&visible\&\u003Eapplication.In\u003C\u002Fspan\u003E\u003Cspan class=\&invisible\&\u003E\u003C\u002Fspan\u003E\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E non-fractal architectures, the non-repeatable parts are said to be orchestrators over the parts that have hierarchical composition.\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EBy André Staltz\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E所谓的Fractal Architectures,即你的应用整体都可以像单个组件一样可以方便地进行打包然后应用到其他项目中。而在Non-Fractal Architectures中,不可以被重复使用的部分被称为层次化组合中的Orchestrators。譬如你在Web中编写了一个登录表单,其中的布局、样式等部分可以被直接复用,而提交表单这个操作,因为具有应用特定性,因此需要在不同的应用中具有不同的实现。譬如下面有一个简单的表单:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E&form action=\&form_action.asp\& method=\&get\&&\n
&p&First name: &input type=\&text\& name=\&fname\& \u002F&&\u002Fp&\n
&p&Last name: &input type=\&text\& name=\&lname\& \u002F&&\u002Fp&\n
&input type=\&submit\& value=\&Submit\& \u002F&\n&\u002Fform&\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E因为不同的应用中,form的提交地址可能不一致,那么整个form组件是不可直接重用的,即Non-Fractal Architectures。而form中的 input组件是可以进行直接复用的,如果将 input看做一个单独的GUI架构,即是所谓的Fractal Architectures,form就是所谓的Orchestrators,将可重用的组件编排组合,并且设置应用特定的一些信息。\u003C\u002Fp\u003E\u003Ch1\u003EMV*: 碎片化的状态与双向数据流\u003C\u002Fh1\u003E\u003Cp\u003EMVC模式将有关于渲染、控制与数据存储的概念有机分割,是GUI应用架构模式的一个巨大成就。但是,MVC模式在构建能够长期运行、维护、有效扩展的应用程序时遇到了极大的问题。MVC模式在一些小型项目或者简单的界面上仍旧有极大的可用性,但是在现代富客户端开发中导致职责分割不明确、功能模块重用性、View的组合性较差。作为继任者MVP模式分割了View与Model之间的直接关联,MVP模式中也将更多的ViewLogic转移到Presenter中进行实现,从而保证了View的可测试性。而最年轻的MVVM将ViewLogic与View剥离开来,保证了View的无状态性、可重用性、可组合性以及可测试性。总结而言,MV*模型都包含了以下几个方面:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EModels:负责存储领域\u002F业务逻辑相关的数据与构建数据访问层,典型的就是譬如Person、PersonDataProvider。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EViews:负责将数据渲染展示给用户,并且响应用户输入\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EController\u002FPresenter\u002FViewModel:往往作为Model与View之间的中间人出现,接收View传来的用户事件并且传递给Model,同时利用从Model传来的最新模型控制更新View\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch2\u003EMVC: 巨石型控制器\u003C\u002Fh2\u003E\u003Cp\u003E相信每一个程序猿都会宣称自己掌握MVC,这个概念浅显易懂,并且贯穿了从GUI应用到服务端应用程序。MVC的概念源自Gamma, Helm, Johnson 以及Vlissidis这四人帮在讨论设计模式中的Observer模式时的想法,不过在那本经典的设计模式中并没有显式地提出这个概念。我们通常认为的MVC名词的正式提出是在1979年5月Trygve Reenskaug发表的Thing-Model-View-Editor这篇论文,这篇论文虽然并没有提及Controller,但是Editor已经是一个很接近的概念。大概7个月之后,Trygve Reenskaug在他的文章Models-Views-Controllers中正式提出了MVC这个三元组。上面两篇论文中对于Model的定义都非常清晰,Model代表着an abstraction in the form of data in a computing system.,即为计算系统中数据的抽象表述,而View代表着capable of showing one or more pictorial representations of the Model on screen and on hardcopy.,即能够将模型中的数据以某种方式表现在屏幕上的组件。而Editor被定义为某个用户与多个View之间的交互接口,在后一篇文章中Controller则被定义为了a special controller ... that permits the user to modify the information that is presented by the view.,即主要负责对模型进行修改并且最终呈现在界面上。从我的个人理解来看,Controller负责控制整个界面,而Editor只负责界面中的某个部分。Controller协调菜单、面板以及像鼠标点击、移动、手势等等很多的不同功能的模块,而Editor更多的只是负责某个特定的任务。后来,Martin Fowler在2003开始编写的著作Patterns of Enterprise Application Architecture中重申了MVC的意义:Model View Controller (MVC) is one of the most quoted (and most misquoted) patterns around.,将Controller的功能正式定义为:响应用户操作,控制模型进行相应更新,并且操作页面进行合适的重渲染。这是非常经典、狭义的MVC定义,后来在iOS以及其他很多领域实际上运用的MVC都已经被扩展或者赋予了新的功能,不过笔者为了区分架构演化之间的区别,在本文中仅会以这种最朴素的定义方式来描述MVC。\u003Cbr\u003E根据上述定义,我们可以看到MVC模式中典型的用户场景为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E用户交互输入了某些内容\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EController将用户输入转化为Model所需要进行的更改\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel中的更改结束之后,Controller通知View进行更新以表现出当前Model的状态\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-b5baca1a4c359c07a4b94c5_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&392\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-b5baca1a4c359c07a4b94c5_r.jpg\&\u003E根据上述流程,我们可知经典的MVC模式的特性为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EView、Controller、Model中皆有ViewLogic的部分实现\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EController负责控制View与Model,需要了解View与Model的细节。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView需要了解Controller与Model的细节,需要在侦测用户行为之后调用Controller,并且在收到通知后调用Model以获取最新数据\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel并不需要了解Controller与View的细节,相对独立的模块\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch3\u003EObserver Pattern:自带观察者模式的MVC\u003C\u002Fh3\u003E\u003Cp\u003E上文中也已提及,MVC滥觞于Observer模式,经典的MVC模式也可以与Observer模式相结合,其典型的用户流程为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E用户交互输入了某些内容\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EController将用户输入转化为Model所需要进行的更改\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView作为Observer会监听Model中的任意更新,一旦有更新事件发出,View会自动触发更新以展示最新的Model状态\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-559c2db8abc0eabcc5fddc4_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&361\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-559c2db8abc0eabcc5fddc4_r.jpg\&\u003E\u003Cbr\u003E\u003Cp\u003E可知其与经典的MVC模式区别在于不需要Controller通知View进行更新,而是由Model主动调用View进行更新。这种改变提升了整体效率,简化了Controller的功能,不过也导致了View与Model之间的紧耦合。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Ch2\u003EMVP: 将视图与模型解耦\u003C\u002Fh2\u003E\u003Cp\u003E维基百科将\u003Ca href=\&http:\u002F\\u002F?target=http%3A\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FModel%25E2%view%25E2%presenter\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EMVP\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E称为MVC的一个推导扩展,观其渊源而知其所以然。对于MVP概念的定义,Microsoft较为明晰,而Martin Fowler的定义最为广泛接受。MVP模式在WinForm系列以Visual-XXX命名的编程语言与Java Swing等系列应用中最早流传开来,不过后来ASP.NET以及JFaces也广泛地使用了该模式。在MVP中用户不再与Presenter进行直接交互,而是由View完全接管了用户交互,譬如窗口上的每个控件都知道如何响应用户输入并且合适地渲染来自于Model的数据。而所有的事件会被传输给Presenter,Presenter在这里就是View与Model之间的中间人,负责控制Model进行修改以及将最新的Model状态传递给View。这里描述的就是典型的所谓Passive View版本的MVP,其典型的用户场景为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E用户交互输入了某些内容\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView将用户输入转化为发送给Presenter\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EPresenter控制Model接收需要改变的点\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel将更新之后的值返回给Presenter\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EPresenter将更新之后的模型返回给View\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-f12fdc688ddfdb4945dc2_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&347\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-f12fdc688ddfdb4945dc2_r.jpg\&\u003E根据上述流程,我们可知Passive View版本的MVP模式的特性为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EView、Presenter、Model中皆有ViewLogic的部分实现\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EPresenter负责连接View与Model,需要了解View与Model的细节。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView需要了解Presenter的细节,将用户输入转化为事件传递给Presenter\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel需要了解Presenter的细节,在完成更新之后将最新的模型传递给Presenter\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView与Model之间相互解耦合\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch3\u003ESupervising Controller MVP\u003C\u002Fh3\u003E\u003Cp\u003E简化Presenter的部分功能,使得Presenter只起到需要复杂控制或者调解的操作,而简单的Model展示转化直接由View与Model进行交互:\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-77cc5cc7ea620149fecc7a4e_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&382\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-77cc5cc7ea620149fecc7a4e_r.jpg\&\u003E\u003Ch2\u003EMVVM: 数据绑定与无状态的视图\u003C\u002Fh2\u003E\u003Cp\u003EModel View View-Model模型是MV*家族中最年轻的一位,也是由Microsoft提出,并经由Martin Fowler布道传播。MVVM源于Martin Fowler的Presentation Model,Presentation Model的核心在于接管了View所有的行为响应,View的所有响应与状态都定义在了Presentation Model中。也就是说,View不会包含任意的状态。举个典型的使用场景,当用户点击某个按钮之后,状态信息是从Presentation Model传递给Model,而不是从View传递给Presentation Model。任何控制组件间的逻辑操作,即上文所述的ViewLogic,都应该放置在Presentation Model中进行处理,而不是在View层,这一点也是MVP模式与Presentation Model最大的区别。MVVM模式进一步深化了Presentation Model的思想,利用Data Binding等技术保证了View中不会存储任何的状态或者逻辑操作。在WPF中,UI主要是利用XAML或者XML创建,而这些标记类型的语言是无法存储任何状态的,就像HTML一样(因此JSX语法其实是将View又有状态化了),只是允许UI与某个ViewModel中的类建立映射关系。渲染引擎根据XAML中的声明以及来自于ViewModel的数据最终生成呈现的页面。因为数据绑定的特性,有时候MVVM也会被称作MVB:Model View Binder。总结一下,MVVM利用数据绑定彻底完成了从命令式编程到声明式编程的转化,使得View逐步无状态化。一个典型的MVVM的使用场景为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E用户交互输入\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView将数据直接传送给ViewModel,ViewModel保存这些状态数据\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E在有需要的情况下,ViewModel会将数据传送给Model\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel在更新完成之后通知ViewModel\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EViewModel从Model中获取最新的模型,并且更新自己的数据状态\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView根据最新的ViewModel的数据进行重新渲染\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-b94a224f58fae5c7bc00f93ab30d1856_b.jpg\& data-rawwidth=\&800\& data-rawheight=\&325\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-b94a224f58fae5c7bc00f93ab30d1856_r.jpg\&\u003E根据上述流程,我们可知MVVM模式的特性为:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003EViewModel、Model中存在ViewLogic实现,View则不保存任何状态信息\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EView不需要了解ViewModel的实现细节,但是会声明自己所需要的数据类型,并且能够知道如何重新渲染\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EViewModel不需要了解View的实现细节(非命令式编程),但是需要根据View声明的数据类型传入对应的数据。ViewModel需要了解Model的实现细节。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EModel不需要了解View的实现细节,需要了解ViewModel的实现细节\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch1\u003EMV* 在端开中的实践\u003C\u002Fh1\u003E\u003Ch2\u003EMV* in iOS\u003C\u002Fh2\u003E\u003Ch3\u003EMVC\u003C\u002Fh3\u003E\u003Cp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-a05e152e56e197c289c3bafa_b.png\& data-rawwidth=\&800\& data-rawheight=\&256\& class=\&origin_image zh-lightbox-thumb\& width=\&800\& data-original=\&http:\u002F\\u002Fv2-a05e152e56e197c289c3bafa_r.png\&\u003ECocoa MVC中往往会将大量的逻辑代码放入ViewController中,这就导致了所谓的Massive ViewController,而且很多的逻辑操作都嵌入到了View的生命周期中,很难剥离开来。或许你可以将一些业务逻辑或者数据转换之类的事情放到Model中完成,不过对

我要回帖

更多关于 chrome更改页面编码 的文章

 

随机推荐