[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"es2015\", { \"modules\": false }],\n    \"stage-3\"\n  ],\n  \"plugins\": [\n    \"transform-object-rest-spread\",\n    \"transform-decorators-legacy\",\n    \"transform-class-properties\"\n  ]\n}\n"
  },
  {
    "path": ".eslintignore",
    "content": "**/assets/**"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"ecmaFeatures\": {\n\t\"modules\": true,\n\t\"experimentalObjectRestSpread\": true,\n\t\"jsx\": true\n  },\n\n  \"env\": {\n\t\"browser\": true,\n\t\"es6\": true,\n\t\"node\": true\n  },\n\n  \"plugins\": [\n\t\"standard\"\n  ],\n\n  \"globals\": {\n\t\"document\": true,\n\t\"window\": true,\n    \"DISQUS\": true,\n    \"DISQUSWIDGETS\": true\n  },\n\n  \"parser\": \"babel-eslint\",\n\n  \"rules\": {\n\t//在定义对象的时候，getter/setter需要同时出现\n\t\"accessor-pairs\": 2,\n\t//箭头函数中的箭头前后需要留空格\n\t\"arrow-spacing\": [2, { \"before\": true, \"after\": true }],\n\t// 箭头函数中，在需要的时候，在参数外使用小括号（只有一个参数时，可以不适用括号，其它情况下都需要使用括号）\n\t\"arrow-parens\": [2, \"as-needed\"],\n\t//如果代码块是单行的时候，代码块内部前后需要留一个空格\n\t\"block-spacing\": [2, \"always\"],\n\t//大括号语法采用『1tbs』,允许单行样式\n\t\"brace-style\": [2, \"1tbs\", { \"allowSingleLine\": true }],\n\t//在定义对象或数组时，最后一项不能加逗号\n\t\"comma-dangle\": [2, \"never\"],\n\t//在写逗号时，逗号前面不需要加空格，而逗号后面需要添加空格\n\t\"comma-spacing\": [2, { \"before\": false, \"after\": true }],\n\t//如果逗号可以放在行首或行尾时，那么请放在行尾\n\t\"comma-style\": [2, \"last\"],\n\t//在constructor函数中，如果classes是继承其他class，那么请使用super。否者不使用super\n\t\"constructor-super\": 2,\n\t//在if-else语句中，如果if或else语句后面是多行，那么必须加大括号。如果是单行就应该省略大括号。\n\t\"curly\": [2, \"multi-line\"],\n\t//该规则规定了.应该放置的位置，\n\t\"dot-location\": [2, \"property\"],\n\t//该规则要求代码最后面需要留一空行，（仅需要留一空行）\n\t\"eol-last\": 2,\n\t//使用=== !== 代替== != .\n\t\"eqeqeq\": [2, \"allow-null\"],\n\t//该规则规定了generator函数中星号两边的空白。\n\t\"generator-star-spacing\": [2, { \"before\": true, \"after\": true }],\n\t// 规定callback 如果有err参数，只能写出err 或者 error .\n\t\"handle-callback-err\": [2, \"^(err|error)$\" ],\n\t//这个就是关于用什么来缩进了，规定使用tab 来进行缩进，switch中case也需要一个tab .\n\t\"indent\": [2, \"tab\", { \"SwitchCase\": 1 }],\n\t//该规则规定了在对象字面量语法中，key和value之间的空白，冒号前不要空格，冒号后面需要一个空格\n\t\"key-spacing\": [2, { \"beforeColon\": false, \"afterColon\": true }],\n\t//构造函数首字母大写\n\t\"new-cap\": [2, { \"newIsCap\": true, \"capIsNew\": false }],\n\t//在使用构造函数时候，函数调用的圆括号不能够省略\n\t\"new-parens\": 2,\n\t//禁止使用Array构造函数\n\t\"no-array-constructor\": 2,\n\t//禁止使用arguments.caller和arguments.callee\n\t\"no-caller\": 2,\n\t//禁止覆盖class命名，也就是说变量名不要和class名重名\n\t\"no-class-assign\": 2,\n\t//在条件语句中不要使用赋值语句\n\t\"no-cond-assign\": 2,\n\t//const申明的变量禁止修改\n\t\"no-const-assign\": 2,\n\t//在正则表达式中禁止使用控制符（详见官网）\n\t\"no-control-regex\": 2,\n\t//禁止使用debugger语句\n\t\"no-debugger\": 2,\n\t//禁止使用delete删除var申明的变量\n\t\"no-delete-var\": 2,\n\t//函数参数禁止重名\n\t\"no-dupe-args\": 2,\n\t//class中的成员禁止重名\n\t\"no-dupe-class-members\": 2,\n\t//在对象字面量中，禁止使用重复的key\n\t\"no-dupe-keys\": 2,\n\t//在switch语句中禁止重复的case\n\t\"no-duplicate-case\": 2,\n\t//禁止使用不匹配任何字符串的正则表达式\n\t\"no-empty-character-class\": 2,\n\t//禁止使用eval函数\n\t\"no-eval\": 2,\n\t//禁止对catch语句中的参数进行赋值\n\t\"no-ex-assign\": 2,\n\t//禁止扩展原生对象\n\t\"no-extend-native\": 2,\n\t//禁止在不必要的时候使用bind函数\n\t\"no-extra-bind\": 2,\n\t//在一个本来就会自动转化为布尔值的上下文中就没必要再使用!! 进行强制转化了。\n\t\"no-extra-boolean-cast\": 2,\n\t//禁止使用多余的圆括号\n\t\"no-extra-parens\": [2, \"functions\"],\n\t//这条规则，简单来说就是在case语句中尽量加break，避免不必要的fallthrough错误，如果需要fall through，那么看官网。\n\t\"no-fallthrough\": 2,\n\t//简单来说不要写这样的数字.2 2.。应该写全，2.2 2.0 .\n\t\"no-floating-decimal\": 2,\n\t//禁止对函数名重新赋值\n\t\"no-func-assign\": 2,\n\t//禁止使用类eval的函数。\n\t\"no-implied-eval\": 2,\n\t//禁止在代码块中定义函数（下面的规则仅限制函数）\n\t\"no-inner-declarations\": [2, \"functions\"],\n\t//RegExp构造函数中禁止使用非法正则语句\n\t\"no-invalid-regexp\": 2,\n\t//禁止使用不规则的空白符\n\t\"no-irregular-whitespace\": 2,\n\t//禁止使用__iterator__属性\n\t\"no-iterator\": 2,\n\t//label和var申明的变量不能重名\n\t\"no-label-var\": 2,\n\t//禁止使用label语句\n\t\"no-labels\": 2,\n\t//禁止使用没有必要的嵌套代码块\n\t\"no-lone-blocks\": 2,\n\t//不要把空格和tab混用\n\t\"no-mixed-spaces-and-tabs\": 2,\n\t//顾名思义，该规则保证了在逻辑表达式、条件表达式、\n\t//申明语句、数组元素、对象属性、sequences、函数参数中不使用超过一个的空白符。\n\t\"no-multi-spaces\": 2,\n\t//该规则保证了字符串不分两行书写。\n\t\"no-multi-str\": 2,\n\t//空行不能够超过2行\n\t\"no-multiple-empty-lines\": [2, { \"max\": 2 }],\n\t//该规则保证了不重写原生对象。\n\t\"no-native-reassign\": 2,\n\t//在in操作符左边的操作项不能用! 例如这样写不对的：if ( !a in b) { //dosomething }\n\t\"no-negated-in-lhs\": 2,\n\t//当我们使用new操作符去调用构造函数时，需要把调用结果赋值给一个变量。\n\t\"no-new\": 2,\n\t//该规则保证了不使用new Function(); 语句。\n\t\"no-new-func\": 2,\n\t//不要通过new Object（），来定义对象\n\t\"no-new-object\": 2,\n\t//禁止把require方法和new操作符一起使用。\n\t\"no-new-require\": 2,\n\t//当定义字符串、数字、布尔值就不要使用构造函数了，String、Number、Boolean\n\t\"no-new-wrappers\": 2,\n\t//禁止无意得把全局对象当函数调用了，比如下面写法错误的：Math(), JSON()\n\t\"no-obj-calls\": 2,\n\t//不要使用八进制的语法。\n\t\"no-octal\": 2,\n\t//用的少，见官网。http://eslint.org/docs/rules/\n\t\"no-octal-escape\": 2,\n\t//不要使用__proto__\n\t\"no-proto\": 2,\n\t//不要重复申明一个变量\n\t\"no-redeclare\": 2,\n\t//正则表达式中不要使用空格\n\t\"no-regex-spaces\": 2,\n\t//return语句中不要写赋值语句\n\t\"no-return-assign\": 2,\n\t//不要和自身作比较\n\t\"no-self-compare\": 2,\n\t//不要使用逗号操作符，详见官网\n\t\"no-sequences\": 2,\n\t//禁止对一些关键字或者保留字进行赋值操作，比如NaN、Infinity、undefined、eval、arguments等。\n\t\"no-shadow-restricted-names\": 2,\n\t//函数调用时，圆括号前面不能有空格\n\t\"no-spaced-func\": 2,\n\t//禁止使用稀疏数组\n\t\"no-sparse-arrays\": 2,\n\t//在调用super之前不能使用this对象\n\t\"no-this-before-super\": 2,\n\t//严格限制了抛出错误的类型，简单来说只能够抛出Error生成的错误。但是这条规则并不能够保证你只能够\n\t//抛出Error错误。详细见官网\n\t\"no-throw-literal\": 2,\n\t//行末禁止加空格\n\t\"no-trailing-spaces\": 2,\n\t//禁止使用没有定义的变量，除非在／＊global＊／已经申明\n\t\"no-undef\": 2,\n\t//禁止把undefined赋值给一个变量\n\t\"no-undef-init\": 2,\n\t//禁止在不需要分行的时候使用了分行\n\t\"no-unexpected-multiline\": 2,\n\t//禁止使用没有必要的三元操作符，因为用些三元操作符可以使用其他语句替换\n\t\"no-unneeded-ternary\": [2, { \"defaultAssignment\": false }],\n\t//没有执行不到的代码\n\t\"no-unreachable\": 2,\n\t//没有定义了没有被使用到的变量\n\t\"no-unused-vars\": 2,\n\t//禁止在不需要使用call（）或者apply（）的时候使用了这两个方法\n\t\"no-useless-call\": 2,\n\t//不要使用with语句\n\t\"no-with\": 2,\n\t//在某些场景只能使用一个var来申明变量\n\t\"one-var\": [2, { \"initialized\": \"never\" }],\n\t//在进行断行时，操作符应该放在行首还是行尾。并且还可以对某些操作符进行重写。\n\t\"operator-linebreak\": [2, \"after\", { \"overrides\": { \"?\": \"before\", \":\": \"before\" } }],\n\t//在使用parseInt() 方法时，需要传递第二个参数，来帮助解析，告诉方法解析成多少进制。\n\t\"radix\": 2,\n\t//这就是分号党和非分号党关心的了，我们还是选择加分号\n\t\"semi\": [2, \"always\"],\n\t//该规则规定了分号前后的空格，具体规定如下。\n\t\"semi-spacing\": [2, { \"before\": false, \"after\": true }],\n\t//关键词前后面需要加空格\n\t\"keyword-spacing\": 2,\n\t//代码块前面需要加空格\n\t\"space-before-blocks\": [2, \"always\"],\n\t//函数圆括号前面需要加空格\n\t\"space-before-function-paren\": [2, \"never\"],\n\t//圆括号内部不需要加空格\n\t\"space-in-parens\": [2, \"never\"],\n\t//操作符前后需要加空格\n\t\"space-infix-ops\": 2,\n\t//一元操作符前后是否需要加空格，单词类操作符需要加，而非单词类操作符不用加\n\t\"space-unary-ops\": [2, { \"words\": true, \"nonwords\": false }],\n\t//评论符号｀／*｀ ｀／／｀，后面需要留一个空格\n\t\"spaced-comment\": [2, \"always\", { \"markers\": [\"global\", \"globals\", \"eslint\", \"eslint-disable\", \"*package\", \"!\", \",\"] }],\n\t//推荐使用isNaN方法，而不要直接和NaN作比较\n\t\"use-isnan\": 2,\n\t//在使用typeof操作符时，作比较的字符串必须是合法字符串eg:'string' 'object'\n\t\"valid-typeof\": 2,\n\t//立即执行函数需要用圆括号包围\n\t\"wrap-iife\": [2, \"any\"],\n\t//yoda条件语句就是字面量应该写在比较操作符的左边，而变量应该写在比较操作符的右边。\n\t//而下面的规则要求，变量写在前面，字面量写在右边\n\t\"yoda\": [2, \"never\"],\n\n\t\"standard/object-curly-even-spacing\": [2, \"either\"],\n\t\"standard/array-bracket-even-spacing\": [2, \"either\"],\n\t\"standard/computed-property-even-spacing\": [2, \"even\"]\n  }\n}"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n\n# subscribe data\n/data\n\n# build source\nbuild\n\n# production log\nlog\n\n# lets encrypt certification\nletsencrypt\n"
  },
  {
    "path": ".prettierrc",
    "content": "printWidth: 120\nsingleQuote: true\ntrailingComma: es5"
  },
  {
    "path": "Dockerfile",
    "content": "# can use node version tag like :onbuild, :latest, but which is not stable version may cause update error\n# detail on https://hub.docker.com/_/node/\nFROM node:8\n\nMAINTAINER Disciple.Ding <disciple.ding@gmail.com>\n\n# Create app work directory\nRUN mkdir -p /usr/app\nWORKDIR /usr/app\n\n# Use the cache as long as contents of package.json hasn't changed.\nCOPY package.json /usr/app/\n\nRUN npm install\n\n# Bundle app source\nCOPY . /usr/app\n\n# Build Source\nRUN npm run build\n\nEXPOSE 8080\n\nVOLUME /usr/app\n\nCMD [ \"npm\", \"run\", \"start:server\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Disciple Ding\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "Disciple.Ding blog\n====\n\nThe source code for my blog, [discipled.me](https://discipled.me)\n\nI'm constantly rewriting / refactoring this silly little blog using\nthe latest and buzziest tech, so that I can stay up to date on these\nlibraries and frameworks.\n\nCurrent buzzwords:\n\n* main tech\n    - Vue 2 & vue-router & vuex \n    - TypeScript\n    - ES2015\n    - Koa 2\n    - GraphQL\n    - SSR(Server side render)\n    - PWA(progressive web apps)\n* style & template\n    - bootstrap v4\n    - [Start Bootstrap](http://startbootstrap.com/) - [Clean Blog](http://startbootstrap.com/template-overviews/clean-blog/)\n    - scss\n    - postcss (Autoprefixer)\n* package\n    - Webpack 3\n* publish\n    - docker\n    - docker-compose\n\n### Branch State\nAs a result of rewriting / refactoring, there're several versions code using different framwork or strategy, and that will on different branches. If you have any interest on that, you can checkout it easily.(Except `master`, other branches will not be updated.)\n\n* master: Vue + TypeScript + SSR\n* vue-js-ssr: Vue + ES6+ + SSR\n* vue-js-spa: Vue + ES6+ + SPA\n\n### Dev env\nNeed node 8 above.\n\n#### INSTALL\nnpm i\n\n#### RUN\nnpm start\n\n### Production env\n#### INSTALL\nnpm i\n\n#### BUILD\nnpm run build\n\n#### Start server\nnpm run start:server\n\n#### Stop server\nnpm run stop:server\n"
  },
  {
    "path": "config/nginx/default.conf",
    "content": "access_log /var/log/nginx/access.log main;\n\nupstream node_server  {\n    server   node:8080 max_fails=2 fail_timeout=30s;\n}\n\n# redirect host www.domain to domain\nserver {\n    listen 80;\n    listen [::]:80;\n    listen 443 ssl;\n    listen [::]:443 ssl;\n\n    server_name www.discipled.me;\n\n    ssl_certificate /etc/letsencrypt/live/www.discipled.me/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/www.discipled.me/privkey.pem;\n\n    # letsencrypt challenge file location\n    location /.well-known {\n        root /usr/share/nginx/html;\n\n        access_log  /var/log/nginx/challenge-access.log  main;\n        allow all;\n    }\n\n    return 301 $scheme://discipled.me$request_uri;\n}\n\n# redirect host http://domain to https://domain\nserver {\n    listen 80;\n    listen [::]:80;\n\n    server_name discipled.me;\n\n    # letsencrypt challenge file location\n    location /.well-known {\n        root /usr/share/nginx/html;\n\n        access_log  /var/log/nginx/challenge-access.log  main;\n        allow all;\n    }\n\n    location / {\n        return 301 https://discipled.me$request_uri;\n    }\n}\n\n# https://domain server\nserver {\n    listen 443 ssl http2;\n    listen [::]:443 ssl http2;\n\n    server_name discipled.me;\n    charset utf-8;\n\n    gzip on;\n    gzip_types    text/plain application/javascript application/x-javascript text/javascript text/xml text/css;\n    root /usr/app/build/client/;\n\n    ssl_certificate /etc/letsencrypt/live/discipled.me/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/discipled.me/privkey.pem;\n\n    ssl_session_cache shared:SSL:1m;\n    ssl_session_timeout 1h;\n\n    location / {\n        try_files $uri @node;\n    }\n\n    location @node {\n        proxy_pass http://node_server;\n        proxy_redirect off;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n\n# David Blog Server\nserver {\n    listen 80;\n    listen [::]:80;\n\n    server_name alighters.com;\n    charset utf-8;\n\n    gzip on;\n    gzip_types    text/plain application/javascript application/x-javascript text/javascript text/xml text/css;\n\n    location / {\n        root /var/www/blog;\n    }\n}\n"
  },
  {
    "path": "config/webpack/base.js",
    "content": "/**\n * Created by jack on 16-11-27.\n */\n\nconst webpack = require('webpack');\nconst autoprefixer = require('autoprefixer');\nconst ExtractTextPlugin = require('extract-text-webpack-plugin');\n\nconst PATH = require('./setting');\n\nconst webpackConfig = {\n\t// http://mp.weixin.qq.com/s?__biz=MzI3NTE2NjYxNw==&mid=2650600472&idx=1&sn=d4bf85c1bb26a32aff144e81d652582f\n\tdevtool: 'source-map',\n\toutput: {\n\t\tpath: PATH.DIST_PATH + '/client',\n\t\tpublicPath: PATH.PUBLIC_PATH\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t'vue': 'vue/dist/vue.js',\n\t\t\t'@': PATH.SOURCE_PATH + '/client'\n\t\t},\n\t\textensions: [\".ts\", \".js\", \".json\"]\n\t},\n\tplugins: [\n\t\t// the plugin need be added in loader\n\t\tnew ExtractTextPlugin('style-[contenthash:8].css'),\n\t\tnew webpack.optimize.ModuleConcatenationPlugin(),\n\t\tnew webpack.NoEmitOnErrorsPlugin()\n\t],\n\tmodule: {\n\t\trules: [\n\t\t\t{\n\t\t\t\ttest: /\\.tsx?$/,\n\t\t\t\tenforce: 'pre',\n\t\t\t\tloader: 'tslint-loader'\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.jsx?$/,\n\t\t\t\tloader: 'eslint-loader',\n\t\t\t\tenforce: 'pre',\n\t\t\t\texclude: /node_modules/,\n\t\t\t\toptions: {\n\t\t\t\t\temitWarning: true,\n\t\t\t\t\temitError: true,\n\t\t\t\t\tformatter: require('eslint-friendly-formatter')\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.tsx?$/,\n\t\t\t\tloader: \"awesome-typescript-loader\"\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.jsx?$/,\n\t\t\t\tloader: 'babel-loader',\n\t\t\t\texclude: /node_modules/\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.html$/,\n\t\t\t\tloader: 'html-loader?interpolate',\n\t\t\t\texclude: /node_modules/\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.(sc|c)ss$/,\n\t\t\t\t// extract css file from js file, that will reduce the js file size and optimize page loading.\n\t\t\t\t// but it will increase the package time, so it should be only used in build file.\n\t\t\t\tuse: ExtractTextPlugin.extract({\n\t\t\t\t\tfallback: 'style-loader',\n\t\t\t\t\tuse: [\n\t\t\t\t\t\t'css-loader?sourceMap',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tloader: 'postcss-loader?sourceMap',\n\t\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\t\tplugins: () => [autoprefixer({\n\t\t\t\t\t\t\t\t\tbrowsers: ['last 2 versions']\n\t\t\t\t\t\t\t\t})]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'sass-loader'\n\t\t\t\t\t]\n\t\t\t\t})\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.(jpe?g|png|gif|svg)$/i,\n\t\t\t\tuse: [\n\t\t\t\t\t'file-loader?hash=sha512&digest=hex&name=[path][name]-[hash:8].[ext]',\n\t\t\t\t\t'image-webpack-loader?bypassOnDebug&optimizationLevel=7&interlaced=false'\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.(woff|woff2)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n\t\t\t\tloader: 'url-loader?limit=10000&mimetype=application/font-woff&prefix=fonts'\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.ttf(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n\t\t\t\tloader: 'url-loader?limit=10000&mimetype=application/octet-stream&prefix=fonts'\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.eot(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n\t\t\t\tloader: 'url-loader?limit=10000&mimetype=application/vnd.ms-fontobject&prefix=fonts'\n\t\t\t}\n\t\t]\n\t}\n};\n\nmodule.exports = webpackConfig;"
  },
  {
    "path": "config/webpack/client.js",
    "content": "/**\n * Created by jack on 16-4-16.\n */\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst CleanPlugin = require('clean-webpack-plugin');\nconst CopyWebpackPlugin = require('copy-webpack-plugin');\nconst VueSSRClientPlugin = require('vue-server-renderer/client-plugin')\n\nconst PATH = require('./setting');\nconst baseWebpackConfig = require('./base');\nconst isProduction = process.env.NODE_ENV === 'production';\n\nconst webpackConfig = Object.assign({}, baseWebpackConfig, {\n\tdevtool: isProduction ? 'cheap-source-map' : 'module-source-map',\n\tentry: {\n\t\tcommon: ['vue', 'vue-router', 'vuex'],\n\t\tapp: [PATH.SOURCE_PATH + '/client-entry.ts']\n\t},\n\toutput: Object.assign({}, baseWebpackConfig.output, {\n\t\tfilename: '[name].[hash:8].js',\n\t\t// The JSONP function used by webpack for asnyc loading of chunks.\n\t\t// Must Using different identifier, when having multiple webpack instances on a single page.\n\t\t// If not, that will cause reference error.\n\t\tjsonpFunction: 'blogJsonp'\n\t}),\n\tplugins: baseWebpackConfig.plugins.concat([\n\t\t// Common Chunk Plugin should be used when project has several entries for common lib file.\n\t\tnew webpack.optimize.CommonsChunkPlugin({\n\t\t\tname: 'common',\n\t\t\tfilename: 'common.[hash:8].js'\n\t\t}),\n\t\t/*\n\t\t * DllReferencePlugin is used to package project library file, which will not be compile every time.\n\t\t * That plugin will improve local build efficiency.\n\t\t * But that cause another problem that file can't be automatically injected into index.html.\n\t\t * That problem causes the plugin is useless.\n\t\t new webpack.DllReferencePlugin({\n\t\t context: path.join(__dirname),\n\t\t manifest: require(PATH.DIST_PATH + '/VueStuff.manifest.json')\n\t\t }),*/\n\t\tnew VueSSRClientPlugin(),\n\t\t/* replace by VueSSRClientPlugin\n\t\tnew HtmlWebpackPlugin({\n\t\t\tfavicon: PATH.SOURCE_PATH + '/client/assets/img/favicon.ico',\n\t\t\tfilename: 'index.temp.html',\n\t\t\ttemplate: PATH.SOURCE_PATH + '/index.html'\n\t\t}), */\n\t\tnew CopyWebpackPlugin([\n\t\t\t{\n\t\t\t\tfrom: PATH.SOURCE_PATH + '/client/assets/img/logo',\n\t\t\t\tto: 'assets/img/logo'\n\t\t\t}\n\t\t]),\n\t\tnew CopyWebpackPlugin([\n\t\t\t{ from: PATH.SOURCE_PATH + '/manifest.json' }\n\t\t]),\n\t\tnew CopyWebpackPlugin([\n\t\t\t{ from: PATH.SOURCE_PATH + '/service-worker.js' }\n\t\t]),\n\t\t// Define NODE_ENV\n\t\tnew webpack.DefinePlugin({\n\t\t\t'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')\n\t\t})\n\t])\n});\n\nif (isProduction) {\n\twebpackConfig.plugins.unshift(new CleanPlugin([`${PATH.DIST_PATH}/client`], { root: process.cwd() }));\n\twebpackConfig.plugins.push(new webpack.DefinePlugin({\n\t\t'process.env': {\n\t\t\tNODE_ENV: '\"production\"'\n\t\t}\n\t}));\n\twebpackConfig.plugins.push(new webpack.LoaderOptionsPlugin({\n\t\tminimize: true\n\t}));\n\twebpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({\n\t\tsourceMap: true\n\t}));\n} else {\n\twebpackConfig.entry['app'].unshift('webpack-hot-middleware/client?path=/__webpack_hmr&reload=true&timeout=20000');\n\twebpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());\n}\n\nmodule.exports = webpackConfig;\n"
  },
  {
    "path": "config/webpack/dll.js",
    "content": "/**\n * Created by jack on 16-8-3.\n */\n\nconst webpack = require('webpack');\n\nconst PATH = require('config/webpack/setting');\n\nmodule.exports = {\n\tentry: {\n\t\t'VueStuff': [\n\t\t\t'vue',\n\t\t\t'vue-router',\n\t\t\t'vuex'\n\t\t]\n\t},\n\toutput: {\n\t\tfilename: '[name].[hash:8].dll.js',\n\t\tpath: PATH.DIST_PATH,\n\t\tlibrary: '[name]_library'\n\t},\n\n\tplugins: [\n\t\tnew webpack.DllPlugin({\n\t\t\tname: '[name]_library',\n\t\t\tpath: PATH.DIST_PATH + '/[name].manifest.json'\n\t\t})\n\t]\n};\n"
  },
  {
    "path": "config/webpack/server.js",
    "content": "/**\n * Created by jack on 16-11-27.\n */\nconst webpack = require('webpack');\nconst VueSSRServerPlugin = require('vue-server-renderer/server-plugin');\n\nconst PATH = require('./setting');\nconst baseWebpackConfig = require('./base');\n\nconst webpackConfig = Object.assign({}, baseWebpackConfig, {\n\ttarget: 'node',\n\tentry: PATH.SOURCE_PATH + '/server-entry.js',\n\toutput: Object.assign({}, baseWebpackConfig.output, {\n\t\tfilename: 'server.bundle.js',\n\t\tlibraryTarget: 'commonjs2'\n\t}),\n\texternals: Object.keys(require(PATH.ROOT + 'package.json').dependencies),\n\t// VueSSRServerPlugin work fail with webpack-middleware\n\tplugins: baseWebpackConfig.plugins.concat([\n\t\tnew VueSSRServerPlugin()\n\t])\n});\n\nmodule.exports = webpackConfig;\n"
  },
  {
    "path": "config/webpack/setting.js",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 13/05/2017\n */\n\nconst path = require('path');\n\nconst ROOT = path.join(__dirname, '../../');\nconst SOURCE_PATH = ROOT + 'src';\nconst DIST_PATH = ROOT + 'build';\nconst PUBLIC_PATH = '/';\n\nconst indexTemplatePath = path.join(SOURCE_PATH, '/index.html');\nconst clientManifestFileName = 'vue-ssr-client-manifest.json';\nconst serverBundleFileName = 'vue-ssr-server-bundle.json';\n\nmodule.exports = {\n\tROOT,\n\tSOURCE_PATH,\n\tPUBLIC_PATH,\n\tDIST_PATH,\n\tindexTemplatePath,\n\tclientManifestFileName,\n\tserverBundleFileName\n};\n"
  },
  {
    "path": "deploy.sh",
    "content": "# git operation\ngit reset HEAD --hard\ngit fetch\ngit pull\n\n# TAG_NAME used to set docker image tag\nexport TAG_NAME=`git tag -l | sort -r | head -n 1`\n\n# docker operation\ndocker-compose down --volumes\n\ndocker-compose up --build -d\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '2'\n\nservices:\n  node:\n    build: .\n    image: \"blog:${TAG_NAME}\"\n    container_name: node\n    # node service port export for test\n    ports:\n     - \"8080:8080\"\n    volumes:\n     - ./log/node:/var/log/node\n     - ./data:/usr/app/data\n\n  nginx:\n    image: nginx:alpine\n    container_name: nginx\n    depends_on:\n      - node\n    volumes:\n      - ./config/nginx:/etc/nginx/conf.d:ro\n      - ./letsencrypt/etc/letsencrypt:/etc/letsencrypt\n      - ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt\n      - ./letsencrypt/challenge:/usr/share/nginx/html\n      - ./log/nginx:/var/log/nginx\n      # David Blog\n      - /var/www/blog:/var/www/blog\n    volumes_from:\n      - node:ro\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    restart: always\n\n  letsencrypt:\n    image: deliverous/certbot\n    container_name: certbot\n    depends_on:\n      - nginx\n    volumes:\n      - ./letsencrypt/etc/letsencrypt:/etc/letsencrypt\n      - ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt\n      - ./letsencrypt/challenge:/usr/share/nginx/html\n      - ./log/letsencrypt:/var/log/letsencrypt\n    command: certonly --webroot --agree-tos --force-renewal -n -w /usr/share/nginx/html -d discipled.me -d www.discipled.me -m discipled.ding@gmail.com\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"disciple.ding-blog\",\n  \"title\": \"Disciple Ding Blog\",\n  \"version\": \"3.0.3\",\n  \"homepage\": \"https://github.com/DiscipleD/blog\",\n  \"author\": \"Discipe.Ding\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"prettier\": \"^1.12.1\",\n    \"memory-fs\": \"^0.3.0\",\n    \"stream\": \"0.0.2\",\n    \"webpack-dev-middleware\": \"^1.12.2\",\n    \"webpack-hot-middleware\": \"^2.21.0\"\n  },\n  \"dependencies\": {\n    \"@types/graphql\": \"^0.10.2\",\n    \"@types/koa\": \"^2.0.43\",\n    \"@types/koa-router\": \"^7.0.27\",\n    \"@types/koa-static\": \"^3.0.2\",\n    \"@types/lodash\": \"^4.14.95\",\n    \"@types/marked\": \"^0.3.0\",\n    \"@types/node\": \"^8.5.9\",\n    \"autoprefixer\": \"^6.7.7\",\n    \"awesome-typescript-loader\": \"^3.4.1\",\n    \"babel-cli\": \"^6.26.0\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-eslint\": \"^6.0.2\",\n    \"babel-loader\": \"^6.4.1\",\n    \"babel-plugin-transform-class-properties\": \"^6.24.1\",\n    \"babel-plugin-transform-decorators-legacy\": \"^1.3.4\",\n    \"babel-plugin-transform-object-rest-spread\": \"^6.26.0\",\n    \"babel-plugin-transform-runtime\": \"^6.23.0\",\n    \"babel-polyfill\": \"^6.26.0\",\n    \"babel-preset-es2015\": \"^6.24.1\",\n    \"babel-preset-stage-3\": \"^6.24.1\",\n    \"clean-webpack-plugin\": \"^0.1.17\",\n    \"co-body\": \"^5.1.1\",\n    \"copy-webpack-plugin\": \"^4.3.1\",\n    \"css-loader\": \"^0.23.1\",\n    \"es6-promise\": \"^4.2.4\",\n    \"eslint\": \"^2.8.0\",\n    \"eslint-friendly-formatter\": \"^2.0.7\",\n    \"eslint-loader\": \"^1.8.0\",\n    \"eslint-plugin-standard\": \"^1.3.2\",\n    \"extract-text-webpack-plugin\": \"^2.1.2\",\n    \"file-loader\": \"^0.8.5\",\n    \"graphql\": \"^0.6.2\",\n    \"html-loader\": \"^0.4.5\",\n    \"html-webpack-plugin\": \"^2.29.0\",\n    \"husky\": \"^0.14.3\",\n    \"image-webpack-loader\": \"^1.7.0\",\n    \"jsdom\": \"^9.12.0\",\n    \"koa\": \"^2.4.1\",\n    \"koa-compress\": \"^2.0.0\",\n    \"koa-convert\": \"^1.2.0\",\n    \"koa-graphql\": \"^0.5.6\",\n    \"koa-mount\": \"^2.0.0\",\n    \"koa-router\": \"^7.3.0\",\n    \"koa-static\": \"^2.1.0\",\n    \"lint-staged\": \"^7.0.4\",\n    \"lodash\": \"^4.17.4\",\n    \"lru-cache\": \"^4.1.1\",\n    \"marked\": \"^0.3.12\",\n    \"node-fetch\": \"^1.7.3\",\n    \"node-sass\": \"^4.7.2\",\n    \"nodemon\": \"^1.14.11\",\n    \"pm2\": \"^2.9.3\",\n    \"postcss-loader\": \"^1.3.3\",\n    \"sass-loader\": \"^6.0.6\",\n    \"serialize-javascript\": \"^1.3.0\",\n    \"source-map-loader\": \"^0.2.3\",\n    \"style-loader\": \"^0.13.2\",\n    \"tslint\": \"^5.9.1\",\n    \"tslint-loader\": \"^3.5.3\",\n    \"typescript\": \"~2.4.1\",\n    \"url-loader\": \"^0.5.9\",\n    \"vue\": \"~2.3.4\",\n    \"vue-class-component\": \"^5.0.2\",\n    \"vue-loader\": \"^9.9.5\",\n    \"vue-router\": \"^2.8.1\",\n    \"vue-server-renderer\": \"~2.3.4\",\n    \"vuex\": \"^2.5.0\",\n    \"vuex-router-sync\": \"^4.3.2\",\n    \"web-push\": \"^3.2.5\",\n    \"webpack\": \"^3.10.0\",\n    \"whatwg-fetch\": \"^1.1.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/DiscipleD/blog.git\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -rf build\",\n    \"createDir\": \"mkdir build && mkdir build/server && mkdir build/server/data && mkdir build/server/data/posts\",\n    \"copy\": \"npm run copy:posts && npm run copy:404\",\n    \"copy:posts\": \"cp src/server/data/posts/*.md build/server/data/posts/\",\n    \"copy:404\": \"cp src/404.html build/\",\n    \"ready\": \"npm run clean && npm run createDir && npm run copy\",\n    \"watch\":\n      \"tsc -w -p tsconfig-server.json & babel src/server -w -d build/server --no-babelrc --presets=es2015,stage-3\",\n    \"start\": \"npm run ready && npm run watch & nodemon --watch build/server --delay 2 build/server/server.js\",\n    \"build\": \"npm run ready && NODE_ENV=production npm run build:server && NODE_ENV=production npm run build:client\",\n    \"build:client\":\n      \"webpack --config config/webpack/client.js --display-optimization-bailout && webpack --config config/webpack/server.js\",\n    \"build:clientDll\": \"webpack --config config/webpack/dll.js\",\n    \"build:server\":\n      \"tsc -p tsconfig-server.json && babel src/server -d build/server --no-babelrc --presets=es2015,stage-3\",\n    \"start:server\":\n      \"NODE_ENV=production pm2 start build/server/server.js --name blog -i 2 --output /var/log/node/out.log --error /var/log/node/error.log --no-daemon\",\n    \"stop:server\": \"pm2 stop blog\",\n    \"precommit\": \"lint-staged\",\n    \"log\": \"pm2 logs\"\n  },\n  \"lint-staged\": {\n    \"*.{json,scss,css}\": [\"prettier --write\", \"git add\"]\n  }\n}\n"
  },
  {
    "path": "src/404.html",
    "content": "<!DOCTYPE HTML>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\t<meta name=\"description\" content=\"Disciple.Ding Blog\">\n\t<meta name=\"author\" content=\"disciple ding\">\n\t<title>Forgotten Land</title>\n\t<link href='//fonts.googleapis.com/css?family=Capriola' rel='stylesheet' type='text/css'>\n\t<style type=\"text/css\">\n\t\tbody{\n\t\t\tfont-family: 'Capriola', sans-serif;\n\t\t\tfont-size: 16px;\n\t\t}\n\t\t.wrap{\n\t\t\tmargin:0 auto;\n\t\t\twidth: 100%;\n\t\t}\n\t\t.logo h1{\n\t\t\tfont-size: 6rem;\n\t\t\tcolor:#404040;\n\t\t\ttext-align:center;\n\t\t\tmargin-bottom: .125rem;\n\t\t\ttext-shadow: .25rem .25rem .1rem gray;\n\t\t}\n\t\t@media only screen and (min-width: 768px){\n\t\t\t.logo h1{\n\t\t\t\tfont-size: 14rem;\n\t\t\t}\n\t\t}\n\t\t.logo p{\n\t\t\tcolor:gray;;\n\t\t\tfont-size: 1.5rem;\n\t\t\ttext-align:center;\n\t\t}\n\t\t.sub a{\n\t\t\tfont-size: 1rem;\n\t\t\tcolor:#404040;\n\t\t\ttext-decoration:none;\n\t\t\tpadding: .5rem;\n\t\t\tfont-family: arial, serif;\n\t\t\tfont-weight:bold;\n\t\t}\n\t\t.sub a:hover{\n\t\t\tcolor: #0085A1;\n\t\t\ttext-shadow:\n\t\t\t\t\t0 0 5px #fff,\n\t\t\t\t\t0 0 10px #fff,\n\t\t\t\t\t0 0 25px #0085A1;\n\t\t}\n\t</style>\n\t<script>\n\t\t(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n\t\t\t\t\t(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\n\t\t\t\tm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n\t\t})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');\n\n\t\tga('create', 'UA-78065426-1', 'auto');\n\t\tga('send', 'pageview');\n\t</script>\n</head>\n<body>\n<div class=\"wrap\">\n\t<div class=\"logo\">\n\t\t<h1>404</h1>\n\t\t<p> Sorry - Page not Found!</p>\n\t\t<div class=\"sub\">\n\t\t\t<p><a href=\"/\"> Back to Home</a></p>\n\t\t</div>\n\t</div>\n</div>\n\n</body>"
  },
  {
    "path": "src/client/app.ts",
    "content": "/**\n * Created by jack on 16-4-16.\n */\n\nimport Vue from 'vue';\nimport { sync } from 'vuex-router-sync';\n\n// Clean-blog less transform to Clean-blog cass\nimport '@/assets/scss/clean-blog.scss';\n\n// Fetch service polyfill\nimport 'whatwg-fetch';\nimport 'core-js/modules/es6.promise';\n\nimport createStore from './vuex';\nimport createRouter from './router';\nimport '@/containers/blog';\nimport '@/components';\n\nconst createApp = () => {\n\tconst store = createStore();\n\tconst router = createRouter();\n\n\tsync(store, router);\n\n\tconst app = new Vue({\n\t\tstore,\n\t\trouter,\n\t\trender: (h) =>\n\t\t\th(\n\t\t\t\t'div',\n\t\t\t\t{\n\t\t\t\t\tattrs: {\n\t\t\t\t\t\tid: 'app',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t[h('blog')],\n\t\t\t),\n\t});\n\n\treturn {app, router, store};\n};\n\nexport default createApp;\n"
  },
  {
    "path": "src/client/assets/scss/animation.scss",
    "content": "@keyframes circle-dash {\n  0% {\n    stroke-dasharray: 1, 125;\n    stroke-dashoffset: 0;\n  }\n  50% {\n    stroke-dasharray: 100, 125;\n    stroke-dashoffset: -25px;\n  }\n  100% {\n    stroke-dasharray: 100, 125;\n    stroke-dashoffset: -125px;\n  }\n}\n\n@keyframes rotate {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/client/assets/scss/clean-blog.scss",
    "content": "@import 'variables';\n@import 'animation';\n\n// Global Components\n\nhtml,\nbody {\n  height: 100%;\n}\n\nbody {\n  font-family: 'Lora', 'Times New Roman', serif;\n  font-size: 20px;\n  color: $gray-dark;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  // font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  font-weight: 800;\n}\n\na img {\n  &:hover,\n  &:focus {\n    cursor: zoom-in;\n  }\n}\n\nblockquote {\n  color: $gray;\n  font-style: italic;\n}\n\nhr.small {\n  max-width: 100px;\n  margin: 15px auto;\n  border-width: 4px;\n  border-color: white;\n}\n\n// Contact Form Styles\n\n.floating-label-form-group {\n  font-size: 14px;\n  position: relative;\n  margin-bottom: 0;\n  padding-bottom: 0.5em;\n  border-bottom: 1px solid $gray-light;\n  input,\n  textarea {\n    z-index: 1;\n    position: relative;\n    padding-right: 0;\n    padding-left: 0;\n    border: none;\n    border-radius: 0;\n    font-size: 1.5em;\n    background: none;\n    box-shadow: none !important;\n    resize: none;\n  }\n  label {\n    display: block;\n    z-index: 0;\n    position: relative;\n    top: 2em;\n    margin: 0;\n    font-size: 0.85em;\n    line-height: 1.764705882em;\n    vertical-align: baseline;\n    opacity: 0;\n    transition: top 0.3s ease, opacity 0.3s ease;\n  }\n  &::not(:first-child) {\n    padding-left: 14px;\n    border-left: 1px solid $gray-light;\n  }\n}\n\n.floating-label-form-group-with-value {\n  label {\n    top: 0;\n    opacity: 1;\n  }\n}\n\n.floating-label-form-group-with-focus {\n  label {\n    color: $brand-primary;\n  }\n}\n\nform .row:first-child .floating-label-form-group {\n  border-top: 1px solid $gray-light;\n}\n\n// Button Styles\n\n.btn {\n  font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  text-transform: uppercase;\n  font-size: 14px;\n  font-weight: 800;\n  letter-spacing: 1px;\n  border-radius: 0;\n  padding: 15px 25px;\n}\n\n.btn-lg {\n  font-size: 16px;\n  padding: 25px 35px;\n}\n\n.btn-default {\n  &:hover,\n  &:focus {\n    background-color: $brand-primary;\n    border: 1px solid $brand-primary;\n    color: white;\n  }\n}\n\n// -- Highlight Color Customization\n\n::selection {\n  color: white;\n  text-shadow: none;\n  background: $brand-primary;\n}\n\nimg::selection {\n  color: white;\n  background: transparent;\n}\n\nbody {\n  webkit-tap-highlight-color: $brand-primary;\n}\n"
  },
  {
    "path": "src/client/assets/scss/variables.scss",
    "content": "// Variables\n\n$brand-primary: #0085a1;\n$gray-dark: lighten(black, 25%);\n$gray: lighten(black, 50%);\n$white-faded: rgba(255, 255, 255, 0.8);\n$gray-light: #ddd;\n"
  },
  {
    "path": "src/client/common/constant/server.ts",
    "content": "/**\n * Created by jack on 16-12-3.\n */\nconst SERVER = {\n\tHOST: 'http://localhost:8080',\n};\n\nif (process.env.NODE_ENV === 'production') {\n\tSERVER.HOST = 'https://discipled.me';\n}\n\nexport default SERVER;\n"
  },
  {
    "path": "src/client/common/constant/site.ts",
    "content": "/**\n * Created by jack on 16-12-17.\n */\n\nexport const BLOG_TITLE: string = 'D.D Blog';\n\nexport const IMAGE_SERVER_PREFIX: string = 'https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/';\n"
  },
  {
    "path": "src/client/common/service/CommonService.ts",
    "content": "/**\n * Created by jack on 16-12-17.\n */\n\nimport {BLOG_TITLE} from '@/common/constant/site';\nimport {setPageTitle} from '@/common/util/dom';\n\nexport const getBlogTitle = (str: string) => {\n\tif (!str || str === BLOG_TITLE) return BLOG_TITLE;\n\telse return `${str} | ${BLOG_TITLE}`;\n};\n\nexport const setBlogTitle = (str: string) => {\n\tsetPageTitle(getBlogTitle(str));\n};\n"
  },
  {
    "path": "src/client/common/service/FetchService.ts",
    "content": "/**\n * Created by jack on 16-8-24.\n */\n\nimport fetchUtil from '../util/fetch';\nimport SERVER from '../constant/server';\n\nexport const generatorUrl = (url: string = '', params: string | { [key: string]: string } = '') =>\n\tparams ? `${url}?${generatorQueryString(params)}` : url;\n\nexport const generatorQueryString = (params: string | { [key: string]: string }) =>\n\ttypeof params === 'object'\n\t\t? Object.keys(params).map((key: string) => `${key}=${encodeURIComponent(params[key])}`).join('&')\n\t\t: params;\n\n// TODO\nconst httpFetch = (url: RequestInfo, options?: RequestInit) => {\n\turl = SERVER.HOST + url;\n\treturn fetchUtil(url, options);\n};\n\nexport default httpFetch;\n"
  },
  {
    "path": "src/client/common/service/PostService.ts",
    "content": "/**\n * Created by jack on 16-4-27.\n */\n\nimport httpFetch, * as FetchService from './FetchService';\nimport { IPostPage } from 'types/post';\nimport { IPager } from 'types/pager';\n\nexport interface IQueryPostsResponse {\n\tposts: IPostPage[];\n}\n\nexport interface IQueryPostResponse {\n\tpost: IPostPage;\n}\n\nconst GRAPHQL_URL_PREFIX = '/graphql';\n\nexport default class PostService {\n\tconstructor() {}\n\n\tpublic getLatestPost(): Promise<GraphQLResponse<IQueryPostsResponse>> {\n\t\tconst GET_LATEST_POST_GRAPHQL =\n\t\t `query={posts(pager:{number:0,size:1}){id,name,createdTime,title,subtitle,headerImgName,tags{name,label}}}`;\n\t\treturn httpFetch(FetchService.generatorUrl(GRAPHQL_URL_PREFIX, GET_LATEST_POST_GRAPHQL));\n\t}\n\n\tpublic queryPostList({ num = 0, size = 5 }: IPager): Promise<GraphQLResponse<IQueryPostsResponse>> {\n\t\tconst QUERY_POST_LIST_GRAPHQL =\n\t\t\t`query={posts(pager:{number:${num},size:${size}}){id,name,createdTime,title,subtitle,tags{name,label}}}`;\n\t\treturn httpFetch(FetchService.generatorUrl(GRAPHQL_URL_PREFIX, QUERY_POST_LIST_GRAPHQL));\n\t}\n\n\tpublic getPostByName(postName: string): Promise<GraphQLResponse<IQueryPostResponse>> {\n\t\tconst GET_POST_BY_NAME_GRAPHQL = `query={post(name: \"${postName}\"){id,name,createdTime,title,subtitle,headerImgName,\n\t\t\tcontent,prevPost{name,title},nextPost{name,title},tags{name,label}}}`;\n\t\treturn httpFetch(FetchService.generatorUrl(GRAPHQL_URL_PREFIX, GET_POST_BY_NAME_GRAPHQL));\n\t}\n}\n"
  },
  {
    "path": "src/client/common/service/TagService.ts",
    "content": "/**\n * Created by jack on 16-8-27.\n */\n\nimport httpFetch, * as FetchService from './FetchService';\n\nimport { ITagPage } from 'types/tag';\n\nexport interface IQueryTagsResponse {\n\ttags: ITagPage[];\n}\n\nconst GRAPHQL_URL_PREFIX = '/graphql';\n\nclass TagService {\n\tconstructor() {}\n\n\tpublic queryTagsList(tagName = ''): Promise<GraphQLResponse<IQueryTagsResponse>> {\n\t\tconst QUERY_POST_LIST_GRAPHQL = `query={tags(name: \"${tagName}\"){id,name,createdTime,label,posts{name,title}}}`;\n\t\treturn httpFetch(FetchService.generatorUrl(GRAPHQL_URL_PREFIX, QUERY_POST_LIST_GRAPHQL));\n\t}\n}\n\nexport default new TagService();\n"
  },
  {
    "path": "src/client/common/service/disqus/DisqusService.ts",
    "content": "/**\n * Created by jack on 16-5-19.\n */\n\nimport Server from '../../constant/server';\n\ndeclare const DISQUS: any;\ndeclare const DISQUSWIDGETS: any;\n\nclass DisqusService {\n\t/**\n\t * load Disqus js file\n\t */\n\tpublic static loadDisqusPlugin() {\n\t\tif (typeof DISQUS === 'undefined') {\n\t\t\tconst d = document;\n\t\t\tconst s = d.createElement('script');\n\n\t\t\ts.src = '//discipled.disqus.com/embed.js';\n\n\t\t\ts.setAttribute('data-timestamp', `${+new Date()}`);\n\t\t\t(d.head || d.body).appendChild(s);\n\t\t}\n\t}\n\n\tconstructor() { }\n\n\tpublic resetDisqusCountPlugin() {\n\t\tif (typeof DISQUSWIDGETS === 'undefined') {\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.resetDisqusCountPlugin();\n\t\t\t}, 1000);\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tDISQUSWIDGETS.getCount({ reset: true });\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic resetDisqusPlugin(identifier: string, title: string) {\n\t\tif (typeof DISQUS === 'undefined') {\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.resetDisqusPlugin(identifier, title);\n\t\t\t}, 1000);\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tDISQUS.reset({\n\t\t\t\t\treload: true,\n\t\t\t\t\tconfig() {\n\t\t\t\t\t\tthis.page.identifier = identifier;\n\t\t\t\t\t\tthis.page.title = title;\n\t\t\t\t\t\tthis.page.url = `${Server.HOST}${identifier}`;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport default DisqusService;\n"
  },
  {
    "path": "src/client/common/service/pwa/NotificationService.ts",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 13/02/2017\n */\n\nconst NOTIFICATION_API = 'Notification';\nconst PERMISSION_GRANTED = 'granted';\nconst NOTIFICATION_START_TIME = 23;\nconst NOTIFICATION_END_TIME = 6;\nconst DELAY_MINUTES = 5;\nconst NOTIFICATION = {\n\ttitle: '夜深了',\n\tdelay: DELAY_MINUTES * 60 * 1000, // 5 minutes\n\toptions: {\n\t\tbody: '亲，工作之余，也要注意身体噢...',\n\t\ticon: '/favicon.ico',\n\t},\n};\n\nconst isSupportNotification = () => NOTIFICATION_API in window;\nconst getPermission = () => Notification.prototype.permission;\nconst isPermissionGranted = (permission: NotificationPermission) => permission === PERMISSION_GRANTED;\n\nconst registerNotification = () => {\n\tconst now = new Date();\n\tconst nowHour = now.getHours();\n\t// Time in the notification time block\n\tif (nowHour <= NOTIFICATION_END_TIME || nowHour >= NOTIFICATION_START_TIME) {\n\t\t// Show notification 5 minutes later\n\t\tsetTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), NOTIFICATION.delay);\n\t} else {\n\t\t// Show notification at 11 o'clock.\n\t\tconst start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), NOTIFICATION_START_TIME, DELAY_MINUTES);\n\t\tsetTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), start.valueOf() - now.valueOf());\n\t}\n};\n\nif (isSupportNotification()) {\n\tif (isPermissionGranted(getPermission())) {\n\t\tregisterNotification();\n\t} else {\n\t\tNotification\n\t\t\t.requestPermission()\n\t\t\t.then(isPermissionGranted)\n\t\t\t.then((granted: boolean) => granted && registerNotification());\n\t}\n} else {\n\tconsole.info('Browser not support Notification.');\n}\n"
  },
  {
    "path": "src/client/common/service/pwa/ServiceWorkerService.ts",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 20/02/2017\n */\n\nimport SubscriptionService from './SubscriptionService';\n\nconst SERVICE_WORKER_API = 'serviceWorker';\nconst SERVICE_WORKER_FILE_PATH = '/service-worker.js';\n\nconst isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;\nconst sendMessageToSW = (msg: string) => new Promise((resolve, reject) => {\n\tconst messageChannel = new MessageChannel();\n\tmessageChannel.port1.onmessage = (event: MessageEvent) => {\n\t\tif (event.data.error) {\n\t\t\treject(event.data.error);\n\t\t} else {\n\t\t\tresolve(event.data);\n\t\t}\n\t};\n\n\tnavigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);\n});\n\nif (isSupportServiceWorker()) {\n\tconst sw = navigator.serviceWorker;\n\n\tsw.addEventListener('message', (e: ServiceWorkerMessageEvent) => console.log(e.data));\n\n\tsw.register(SERVICE_WORKER_FILE_PATH)\n\t\t.then((registration: ServiceWorkerRegistration) =>\n\t\t\tregistration\n\t\t\t\t.pushManager\n\t\t\t\t.getSubscription()\n\t\t\t\t.then((subscription: PushSubscription) =>\n\t\t\t\t\tsubscription || registration.pushManager.subscribe({ userVisibleOnly: true })))\n\t\t.then((subscription: PushSubscription) => SubscriptionService.subscript(subscription))\n\t\t.catch((error: Error) => console.error('Subscribe Failure: ', error.message))\n\t\t.then(() => sendMessageToSW('Hello, service worker.'))\n\t\t.catch(() => console.error('Send message error.'));\n} else {\n\tconsole.info('Browser not support Service Worker.');\n}\n"
  },
  {
    "path": "src/client/common/service/pwa/ShareService.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\nexport const isSupportShareAPI = () => !!navigator.share;\n\nexport const sharePage = () => {\n\tnavigator\n\t\t.share({\n\t\t\ttitle: document.title,\n\t\t\ttext: document.title,\n\t\t\turl: window.location.href,\n\t\t})\n\t\t.then(() => console.info('Successful share.'))\n\t\t.catch((error: Error) => console.log('Error sharing:', error));\n};\n"
  },
  {
    "path": "src/client/common/service/pwa/SubscriptionService.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\nimport fetchRequest from '../../util/fetch';\nimport Server from '../../constant/server';\n\nconst SUBSCRIBE_API = '/publish/subscribe';\n\nconst encodeStr = (str: ArrayBuffer) => btoa(String.fromCharCode.apply(null, new Uint8Array(str)));\nconst getEncodeSubscriptionInfo = (subscription: PushSubscription, type: PushEncryptionKeyName) => {\n\tconst buffer = subscription.getKey(type);\n\treturn buffer ? encodeStr(buffer) : '';\n};\n\nclass SubscriptionService {\n\tpublic subscript(subscription: PushSubscription) {\n\t\tconst endpoint = subscription.endpoint;\n\t\tconst p256dh = getEncodeSubscriptionInfo(subscription, 'p256dh');\n\t\tconst auth = getEncodeSubscriptionInfo(subscription, 'auth');\n\n\t\tconst clientSubscription = { endpoint, keys: { p256dh, auth } };\n\n\t\tconst options = {\n\t\t\tmethod: 'post',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify(clientSubscription),\n\t\t};\n\n\t\treturn fetchRequest(Server.HOST + SUBSCRIBE_API, options);\n\t}\n}\n\nexport default new SubscriptionService();\n"
  },
  {
    "path": "src/client/common/util/dom.ts",
    "content": "/**\n * Created by jack on 16-11-17.\n */\n\nexport const getDocumentScrollTop = () => {\n\treturn window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;\n};\n\nexport const setPageTitle = (title: string) => {\n\tdocument.title = title;\n};\n"
  },
  {
    "path": "src/client/common/util/fetch.ts",
    "content": "/**\n * Created by jack on 16-12-3.\n */\n\nexport const status = (response: Response) => {\n\tif (response.status >= 200 && response.status < 300) {\n\t\treturn Promise.resolve(response);\n\t} else {\n\t\treturn Promise.reject(new Error(response.statusText));\n\t}\n};\n\nexport const json = (response: Response) => response.json();\n\nexport const error = (err: Error, url: RequestInfo, options?: RequestInit) => {\n\tconsole.log('Fetch Error:');\n\tconsole.log('Message: ', err);\n\tconsole.log('Url: ', url);\n\tconsole.log('Options: ', options);\n};\n\nconst fetchRequest = (url: RequestInfo, options?: RequestInit) => fetch(url, options)\n\t.then(status)\n\t.then(json)\n\t.catch((err: Error) => error(err, url, options));\n\nexport default fetchRequest;\n"
  },
  {
    "path": "src/client/common/util/url.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\nexport const queryUrlParams = (url: string = '') => {\n\tconst reg = /([^\\/&=]+)(=([^\\/&=]+)?)/g;\n\tconst params: { [key: string]: string } = {};\n\tconst searchIndex: number = url.indexOf('?');\n\tif (searchIndex < 0) return params;\n\turl.replace(reg, (s, k, e, v) => {\n\t\tparams[k] = decodeURIComponent(v);\n\t\treturn s;\n\t});\n\treturn params;\n};\n\nexport const setUrlParams = (url: string, params: { [key: string]: string } = {}) => {\n\tconst searchIndex: number = url.indexOf('?');\n\tconst path: string = searchIndex < 0 ? url : url.slice(0, searchIndex);\n\tconst newParams: { [key: string]: string } = {\n\t\t...queryUrlParams(url),\n\t\t...params,\n\t};\n\treturn `${path}?` + Object.keys(newParams).reduce((str: string, key: string) => {\n\t\tstr += `&${key}=${encodeURI(newParams[key])}`;\n\t\treturn str;\n\t}, '');\n};\n"
  },
  {
    "path": "src/client/components/about/index.ts",
    "content": "/**\n * Created by jack on 16-8-21.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport './style.scss';\nimport template from './template.html';\n\n@Component({\n\tprops: ['introduction'],\n\ttemplate,\n})\nclass AboutMe extends Vue {}\n\nexport default Vue.component('aboutMe', AboutMe);\n"
  },
  {
    "path": "src/client/components/about/style.scss",
    "content": "@import '~@/assets/scss/variables';\n\n.about-me-block {\n  .about-me-ul {\n    list-style: none;\n    padding: 0;\n  }\n\n  .about-me-label {\n    font-size: 1rem;\n    font-weight: 800;\n    margin: 0;\n    color: $gray;\n  }\n\n  .about-me-p {\n    font-weight: 800;\n    font-size: 1.5rem;\n    color: $gray-dark;\n  }\n\n  .about-me-skill {\n    display: flex;\n    flex-flow: column wrap;\n\n    .about-me-skill__dl {\n      margin: 5px;\n      padding: 5px;\n      border: 2px solid $gray;\n      border-radius: 0.5rem;\n    }\n\n    .about-me-skill__dd {\n      margin: 0;\n      display: flex;\n      justify-content: space-between;\n    }\n\n    .skill-link {\n      margin: 0;\n      color: $gray;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .skill-score {\n      flex: none;\n    }\n\n    @media only screen and (min-width: 768px) {\n      max-height: 400px;\n\n      .about-me-skill__dl {\n        width: 180px;\n      }\n\n      .skill-score {\n        .skill-score__span {\n          margin: -2px;\n          font-size: 18px;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/about/template.html",
    "content": "<div class=\"about-me-block\">\n\t<ul class=\"about-me-ul\">\n\t\t<li v-for=\"(item, key) of introduction\" track-by=\"key\">\n\t\t\t<label class=\"about-me-label\">{{item.label}}</label><br>\n\t\t\t<p class=\"about-me-p\" v-if=\"item.type !== 'list'\">{{item.value}}</p>\n\t\t\t<div class=\"about-me-skill\" v-if=\"item.type === 'list'\">\n\t\t\t\t<dl class=\"about-me-skill__dl\" v-for=\"(skillSet, skillSet_key) of item.value\" track-by=\"skillSet_key\">\n\t\t\t\t\t<dt>{{skillSet.label}}</dt>\n\t\t\t\t\t<dd class=\"about-me-skill__dd\" v-for=\"(skill, skill_key) of skillSet.value\" track-by=\"skill_key\">\n\t\t\t\t\t\t<a class=\"skill-link\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\t\t\t\t\t   :title=\"skill.label\" :href=\"skill.link\">{{ skill.label }}</a>\n\t\t\t\t\t\t<div class=\"skill-score\">\n\t\t\t\t\t\t\t<span class=\"skill-score__span\" v-for=\"value in 5\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-star\" v-if=\"skill.value >= value\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-star-half-o\" v-if=\"skill.value < value && skill.value > value - 1\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-star-o\" v-if=\"Math.ceil(skill.value) < value\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</dd>\n\t\t\t\t</dl>\n\t\t\t</div>\n\t\t</li>\n\t</ul>\n</div>\n"
  },
  {
    "path": "src/client/components/footer/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport './style.scss';\nimport template from './template.html';\n\n@Component({\n\tprops: ['socialLinkList'],\n\ttemplate,\n})\nclass PageFooter extends Vue {}\n\nexport default Vue.component('pageFooter', PageFooter);\n"
  },
  {
    "path": "src/client/components/footer/style.scss",
    "content": "@import '~@/assets/scss/variables.scss';\n\n.page-footer {\n  padding: 1rem 0;\n\n  .social-link-list {\n    display: flex;\n    flex-flow: row wrap;\n    justify-content: center;\n  }\n\n  .social-link-item {\n    padding: 0.225rem;\n\n    > a {\n      background-color: $gray-dark;\n      border-radius: 50%;\n      display: block;\n      width: 3rem;\n      height: 3rem;\n      padding: 0.75rem;\n\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        background-color: $gray;\n      }\n    }\n\n    .social-svg-item {\n      display: block;\n      width: 1.5rem;\n      height: 1.5rem;\n      fill: #fff;\n    }\n  }\n\n  .copyright {\n    font-size: 14px;\n    text-align: center;\n    margin-bottom: 0;\n  }\n}\n"
  },
  {
    "path": "src/client/components/footer/template.html",
    "content": "<footer class=\"page-footer\">\n\t<div class=\"container\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\">\n\t\t\t\t<ul class=\"list-inline social-link-list\">\n\t\t\t\t\t<li v-for=\"item of socialLinkList\" class=\"social-link-item\">\n\t\t\t\t\t\t<a :href=\"item.link\" target=\"_blank\">\n\t\t\t\t\t\t\t<!-- 我了个擦, vue的一个坑爹的坑 ———— xlink:href不能使用拼接的方式, 即=\"{{svgPath}}#github\", 这会发生两次请求,\n\t\t\t\t\t\t\t\t一次会请求'/%7B%7BsvgPath%7D%7D',即{{svgPath}}的uri转译, 这会导致请求不到 404 error, 第二次是正常并获得svg图.\n\t\t\t\t\t\t\t\t全部使用变量则只发送一次正常的请求\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t<svg class=\"social-svg-item\"><use :xlink:href=\"item.svgPath\"></use></svg>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<p class=\"copyright text-muted\"><a href=\"https://github.com/DiscipleD/blog\" target=\"_blank\">find source code here</a></p>\n\t\t\t\t<p class=\"copyright text-muted\">Copyright &copy; Disciple Ding 2016</p>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</footer>"
  },
  {
    "path": "src/client/components/header/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport './style.scss';\nimport template from './template.html';\nimport _defaultImg from '@/assets/img/tags-bg.jpg';\n\n@Component({\n\ttemplate,\n\tprops: {\n\t\tboardImg: {\n\t\t\ttype: String,\n\t\t\tdefault: _defaultImg,\n\t\t},\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\t\tsubtitle: {\n\t\t\ttype: String,\n\t\t},\n\t},\n})\nclass Header extends Vue {}\n\nexport default Vue.component('contentHeader', Header);\n"
  },
  {
    "path": "src/client/components/header/style.scss",
    "content": "@import '~@/assets/scss/variables';\n\n.intro-header {\n  background-color: $gray;\n  background: no-repeat center center;\n  background-attachment: scroll;\n  background-size: cover;\n  margin-bottom: 2rem;\n\n  .page-heading {\n    padding: 100px 0 50px;\n    color: white;\n    @media only screen and (min-width: 768px) {\n      padding: 150px 0;\n    }\n  }\n\n  .page-heading {\n    text-align: center;\n    h1 {\n      margin-top: 0;\n      font-size: 50px;\n    }\n    .subheading {\n      font-size: 24px;\n      line-height: 1.1;\n      display: block;\n      font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n      font-weight: 300;\n      margin: 10px 0 0;\n    }\n    @media only screen and (min-width: 768px) {\n      h1 {\n        font-size: 80px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/header/template.html",
    "content": "<header class=\"intro-header\" :style=\"{ backgroundImage: 'url(' + boardImg + ')' }\">\n\t<div class=\"container\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\">\n\t\t\t\t<div class=\"page-heading\">\n\t\t\t\t\t<h1>{{ title }}</h1>\n\t\t\t\t\t<hr class=\"small\">\n\t\t\t\t\t<span class=\"subheading\">{{ subtitle }}</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</header>\n"
  },
  {
    "path": "src/client/components/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Header from './header';\nimport Nav from './nav';\nimport MainContent from './main-content';\nimport Footer from './footer';\nimport AboutMe from './about';\nimport Post from './post';\nimport PostList from './post-list';\nimport Tags from './tags';\nimport Pager from './pager';\nimport Loading from './loading';\nimport LazyLoading from './lazy-loading';\n\nconst Components = {\n\tHeader,\n\tNav,\n\tMainContent,\n\tFooter,\n\tAboutMe,\n\tPost,\n\tPostList,\n\tTags,\n\tPager,\n\tLoading,\n\tLazyLoading,\n};\n\nexport default Components;\n"
  },
  {
    "path": "src/client/components/lazy-loading/index.ts",
    "content": "/**\n * Created by jack on 16-9-11.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\nimport throttle from 'lodash/throttle';\n\nimport * as DOMUtil from '@/common/util/dom';\nimport './style.scss';\nimport template from './template.html';\n\n@Component({\n\tprops: {\n\t\tloadFn: {\n\t\t\ttype: Function,\n\t\t\trequire: true,\n\t\t},\n\t\tisLoading: {\n\t\t\ttype: Boolean,\n\t\t\trequire: true,\n\t\t},\n\t\tisFinished: {\n\t\t\ttype: Boolean,\n\t\t},\n\t\tlistenerTargetSelector: {\n\t\t\ttype: String,\n\t\t},\n\t\tfinishedMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: '没有更多了...',\n\t\t},\n\t},\n\twatch: {\n\t\tisLoading(newValue) {\n\t\t\t// when load finished, call scrollFn to test element is filled the target element\n\t\t\t// if not call loadFn another time\n\t\t\tif (newValue === false) {\n\t\t\t\t// give vue time to render element.\n\t\t\t\tsetTimeout((this as LazyLoading).scrollFn, 0);\n\t\t\t}\n\t\t},\n\t},\n\ttemplate,\n})\nclass LazyLoading extends Vue {\n\tpublic loadFn: () => void;\n\tprivate listener: () => void;\n\tprivate isLoading: boolean;\n\tprivate isFinished: boolean;\n\tprivate listenerElement: Element | Document;\n\tprivate listenerTargetSelector: string;\n\n\t/*\n\t * lifesycle start\n\t */\n\tprotected mounted() {\n\t\tthis.listenerElement = this.listenerTargetSelector ?\n\t\t\tdocument.querySelector(this.listenerTargetSelector) || document :\n\t\t\tthis.$el;\n\t\tthis.addListener(this.listenerElement);\n\t\tthis.scrollFn();\n\t}\n\tprotected destroyed() {\n\t\tthis.removeListener(this.listenerElement);\n\t}\n\t/*\n\t * lifesycle end\n\t */\n\n\t/*\n\t * methods start\n\t */\n\tprotected addListener(element: Element | Document) {\n\t\tthis.listener = throttle(this.scrollFn, 200);\n\t\telement.addEventListener('scroll', this.listener);\n\t}\n\tprotected removeListener(element: Element | Document) {\n\t\telement.removeEventListener('scroll', this.listener);\n\t}\n\tprotected isScrollBottom(element: HTMLElement | Document) {\n\t\tlet scrollTop: number;\n\t\tif (element === document) {\n\t\t\telement = document.body;\n\t\t\tscrollTop = DOMUtil.getDocumentScrollTop();\n\t\t} else {\n\t\t\tscrollTop = (element as HTMLElement).scrollTop;\n\t\t}\n\t\treturn scrollTop + (element as HTMLElement).offsetHeight >= (element as HTMLElement).scrollHeight - 50;\n\t}\n\tprotected scrollFn() {\n\t\t// when loading, don't call loadFn again\n\t\tif (this.isLoading) return;\n\t\t!this.isFinished && this.isScrollBottom(this.listenerElement as HTMLElement) && this.loadFn();\n\t}\n\t/*\n\t * methods end\n\t */\n}\n\nexport default Vue.component('lazyLoading', LazyLoading);\n"
  },
  {
    "path": "src/client/components/lazy-loading/style.scss",
    "content": ".lazy-loading-block {\n  overflow: auto;\n\n  .lazy-loading-container {\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "src/client/components/lazy-loading/template.html",
    "content": "<section class=\"lazy-loading-block\">\n\t<slot></slot>\n\t<div class=\"lazy-loading-container\" v-if=\"isLoading\">\n\t\t<loading></loading>\n\t</div>\n\t<div class=\"lazy-loading-container\" v-if=\"isFinished\">{{finishedMessage}}</div>\n</section>\n"
  },
  {
    "path": "src/client/components/loading/index.ts",
    "content": "/**\n * Created by jack on 16-9-7.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport template from './template.html';\nimport './style.scss';\n\n@Component({\n\ttemplate,\n})\nclass Loading extends Vue {}\n\nexport default Vue.component('loading', Loading);\n"
  },
  {
    "path": "src/client/components/loading/style.scss",
    "content": ".loader {\n  width: 50px;\n  position: relative;\n  display: inline-block;\n\n  &:before {\n    content: '';\n    display: block;\n    padding-top: 100%;\n  }\n\n  .circular {\n    position: absolute;\n    top: 0;\n    left: 0;\n    animation: rotate 2s linear infinite;\n  }\n\n  circle {\n    animation: circle-dash 1.5s ease-in-out infinite;\n  }\n}\n"
  },
  {
    "path": "src/client/components/loading/template.html",
    "content": "<div class=\"loader\">\n\t<svg class=\"circular\" viewBox=\"0 0 50 50\">\n\t\t<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\" stroke=\"#106CFA\" stroke-width=\"5%\" stroke-linecap=\"round\"/>\n\t</svg>\n</div>\n"
  },
  {
    "path": "src/client/components/main-content/index.ts",
    "content": "/**\n * Created by jack on 16-8-21.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport template from './template.html';\n\n@Component({\n\ttemplate,\n})\nclass MainContent extends Vue {}\n\nexport default Vue.component('mainContent', MainContent);\n"
  },
  {
    "path": "src/client/components/main-content/template.html",
    "content": "<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\">\n\t\t\t<!-- Content -->\n\t\t\t<slot></slot>\n\t\t</div>\n\t</div>\n</div>"
  },
  {
    "path": "src/client/components/nav/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\nimport throttle from 'lodash/throttle';\n\nimport './style.scss';\nimport * as DOMUtil from '@/common/util/dom';\nimport template from './template.html';\n\nconst DESKTOP_MODE = 'desktop';\n\n@Component({\n\tprops: ['navList', 'mode'],\n\ttemplate,\n})\nclass Navigation extends Vue {\n\tpublic mode: string;\n\tpublic isShowList: boolean = false;\n\tprivate isVisible: boolean = false;\n\tprivate isFixed: boolean = false;\n\tprivate navHeight: number = 0;\n\tprivate prevScrollTop: number = 0;\n\tprivate listener: () => void;\n\n\t/*\n\t * lifesycle start\n\t */\n\tprotected mounted() {\n\t\tthis.initNav(this.mode);\n\t}\n\tprotected destroyed() {\n\t\tthis.mode === DESKTOP_MODE && document.removeEventListener('scroll', this.listener);\n\t}\n\t/*\n\t * lifesycle start\n\t */\n\n\t/*\n\t * methods start\n\t */\n\tprivate initNav(mode = DESKTOP_MODE) {\n\t\tif (mode === DESKTOP_MODE) {\n\t\t\tthis.navHeight = this.$el.clientHeight;\n\n\t\t\tthis.listener = throttle(this.bodyScrollListener, 200);\n\t\t\tdocument.addEventListener('scroll', this.listener);\n\t\t}\n\t}\n\tprivate bodyScrollListener() {\n\t\tconst currScrollTop = DOMUtil.getDocumentScrollTop();\n\n\t\tif (currScrollTop < this.prevScrollTop) {\n\t\t\t// if scrolling up...\n\t\t\tif (currScrollTop > 0 && this.isFixed) {\n\t\t\t\tthis.isVisible = true;\n\t\t\t} else {\n\t\t\t\t// scroll to the top\n\t\t\t\tthis.isVisible = false;\n\t\t\t\tthis.isFixed = false;\n\t\t\t}\n\t\t} else if (currScrollTop > this.prevScrollTop) {\n\t\t\t// if scrolling down...\n\t\t\tthis.isVisible = false;\n\t\t\tcurrScrollTop > this.navHeight && (this.isFixed = true);\n\t\t}\n\n\t\tthis.prevScrollTop = currScrollTop;\n\t}\n\tprivate toggleNavShown() {\n\t\tthis.isShowList = !this.isShowList;\n\t}\n\t/*\n\t * methods end\n\t */\n}\n\nexport default Vue.component('navigation', Navigation);\n"
  },
  {
    "path": "src/client/components/nav/style.scss",
    "content": "$brand-primary: #8060ff;\n$gray-dark: lighten(black, 25%);\n$gray: lighten(black, 50%);\n$white-faded: rgba(255, 255, 255, 0.7);\n$black-faded: rgba(0, 0, 0, 0.8);\n$gray-light: #eee;\n\n.navbar-custom {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  z-index: 3;\n  font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  background-color: $white-faded;\n  border-color: #e7e7e7;\n  .navbar-brand {\n    font-weight: 800;\n  }\n  .navbar-toggler {\n    border-color: #bbb;\n  }\n  .navbar-nav {\n    clear: both;\n\n    .nav-item {\n      float: none;\n    }\n\n    .nav-item + .nav-item {\n      margin: 0;\n    }\n  }\n  /* .{name}-transition vue transition setting */\n  .navbar-nav.nav-expand-transition {\n    transition: all 0.3s ease;\n    overflow: hidden;\n    height: 6rem;\n  }\n  /* .{name}-enter 定义进入的开始状态 */\n  /* .{name}-leave 定义离开的结束状态 */\n  .navbar-nav.nav-expand-enter,\n  .navbar-nav.nav-expand-leave {\n    height: 0; // 展开效果\n    padding: 0 1rem; // 字插入效果\n    opacity: 0; // 渐入效果\n  }\n  .nav-link {\n    text-transform: uppercase;\n    font-size: 12px;\n    font-weight: 800;\n    letter-spacing: 1px;\n  }\n  .nav-link.router-link-active {\n    color: rgba(0, 0, 0, 0.6);\n  }\n  @media only screen and (min-width: 768px) {\n    background: transparent;\n    border-bottom: 1px solid transparent;\n    padding: 0.75rem 1rem;\n\n    .navbar-nav {\n      clear: initial;\n      display: flex !important;\n      margin-top: 0.425rem;\n\n      .nav-item + .nav-item {\n        margin-left: 1rem;\n      }\n    }\n\n    .navbar-nav.nav-expand-transition {\n      height: inherit;\n    }\n\n    .navbar-brand,\n    .navbar-nav .nav-link {\n      color: white;\n\n      &.router-link-active,\n      &:hover,\n      &:focus {\n        color: $white-faded;\n      }\n    }\n    .nav-item {\n      color: white;\n\n      &.router-link-active,\n      &:hover,\n      &:focus {\n        background: transparent;\n        color: $white-faded;\n      }\n\n      &::after {\n        content: '';\n        background-color: $white-faded;\n        display: block;\n        height: 2px;\n        width: 100%;\n        transform: scaleX(0);\n        transition: all 0.2s ease-in-out;\n      }\n\n      &:hover::after {\n        transform: scaleX(1);\n      }\n    }\n    .nav-link {\n      padding: 0.225rem 0;\n    }\n\n    transition: background-color 0.3s;\n    /* Force Hardware Acceleration in WebKit */\n    transform: translate3d(0, 0, 0);\n    backface-visibility: hidden;\n    &.is-fixed {\n      /* when the user scrolls down, we hide the header right above the viewport */\n      position: fixed;\n      top: -61px;\n      background-color: rgba(255, 255, 255, 0.9);\n      border-bottom: 1px solid darken(white, 5%);\n      transition: transform 0.3s;\n      .navbar-brand,\n      .nav-link {\n        color: $gray-dark;\n\n        &.router-link-active,\n        &:hover,\n        &:focus {\n          color: $brand-primary;\n        }\n      }\n      .nav-item::after {\n        background-color: $brand-primary;\n      }\n    }\n    &.is-visible {\n      /* if the user changes the scrolling direction, we show the header */\n      transform: translate3d(0, 100%, 0);\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/nav/template.html",
    "content": "<nav class=\"navbar navbar-light navbar-custom\" :class=\"{ 'is-visible': isVisible, 'is-fixed': isFixed }\">\n\t<button class=\"navbar-toggler pull-xs-right hidden-md-up\" type=\"button\" @click=\"toggleNavShown()\"> &#9776; </button>\n\t<div>\n\t\t<router-link class=\"navbar-brand\" to=\"/\" property=\"url\" exact>\n\t\t\t<span property=\"alternateName\">Disciple.Ding Blog</span><meta property=\"name\" content=\"D.D Blog\">\n\t\t</router-link>\n\t\t<ul class=\"nav navbar-nav pull-md-right\" v-show=\"isShowList\" transition=\"nav-expand\">\n\t\t\t<li class=\"nav-item\" v-for=\"item of navList\">\n\t\t\t\t<router-link class=\"nav-link\" v-if=\"item.path\" :to=\"{ path: item.path}\" exact>{{ item.title }}</router-link>\n\t\t\t\t<span class=\"nav-link\" v-else-if=\"typeof item.event === 'function'\" @click=\"item.event\">{{ item.title }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</nav>\n"
  },
  {
    "path": "src/client/components/pager/index.ts",
    "content": "/**\n * Created by jack on 16-9-4.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport template from './template.html';\nimport './style.scss';\n\n@Component({\n\tprops: ['prev', 'next'],\n\ttemplate,\n})\nclass Pager extends Vue {}\n\nexport default Vue.component('pager', Pager);\n"
  },
  {
    "path": "src/client/components/pager/style.scss",
    "content": "@import '~@/assets/scss/variables';\n\n.pager {\n  .prev {\n    float: left;\n  }\n\n  .next {\n    float: right;\n  }\n\n  .prev,\n  .next {\n    > a,\n    > span {\n      font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n      text-transform: uppercase;\n      font-size: 14px;\n      font-weight: 800;\n      letter-spacing: 1px;\n      padding: 15px 25px;\n      background-color: $white-faded;\n      border: 1px solid $gray-light;\n      border-radius: 0;\n      color: $gray-dark;\n    }\n\n    > a:hover {\n      color: $white-faded;\n      background-color: $brand-primary;\n      border: 1px solid $brand-primary;\n    }\n  }\n\n  .disabled {\n    > a,\n    > a:hover,\n    > span {\n      color: $gray;\n      background-color: $gray-dark;\n      cursor: not-allowed;\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/pager/template.html",
    "content": "<div class=\"pager\">\n\t<span class=\"prev\" v-if=\"prev\">\n\t\t<router-link class=\"nav-link\" :to=\"{ path: prev.name}\" :title=\"prev.title\">&larr; {{prev.text}}</router-link>\n\t</span>\n\t<span class=\"next\" v-if=\"next\">\n\t\t<router-link class=\"nav-link\" :to=\"{ path: next.name}\" :title=\"next.title\">{{next.text}} &rarr;</router-link>\n\t</span>\n</div>\n"
  },
  {
    "path": "src/client/components/post/index.ts",
    "content": "/**\n * Created by jack on 16-4-25.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport { IPostPage } from 'types/post';\nimport './post-header';\nimport template from './template.html';\nimport './style.scss';\nimport { IMAGE_SERVER_PREFIX } from '@/common/constant/site';\nimport DisqusService from '@/common/service/disqus/DisqusService';\n\n@Component({\n\tprops: ['post'],\n\ttemplate,\n})\nclass Post extends Vue {\n\tpublic post: IPostPage;\n\n\tprotected mounted() {\n\t\tDisqusService.loadDisqusPlugin();\n\t\tconst disqueService = new DisqusService();\n\t\tdisqueService.resetDisqusPlugin(this.post.name, this.post.title);\n\t}\n\n\t/*\n\t * computer start\n\t */\n\tget headerUrl() {\n\t\treturn IMAGE_SERVER_PREFIX + this.post.name + '/' + this.post.headerImgName;\n\t}\n\tget prev() {\n\t\treturn this.post.prevPost\n\t\t\t? { ...this.post.prevPost, text: 'prev post' }\n\t\t\t: null;\n\t}\n\tget next() {\n\t\treturn this.post.nextPost\n\t\t\t? { ...this.post.nextPost, text: 'next post' }\n\t\t\t: null;\n\t}\n\t/*\n\t * computer end\n\t */\n}\n\nexport default Vue.component('post', Post);\n"
  },
  {
    "path": "src/client/components/post/post-header/index.ts",
    "content": "/**\n * Created by jack on 16-4-27.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport './style.scss';\nimport template from './post-header.html';\nimport _defaultImg from '@/assets/img/tags-bg.jpg';\n\n@Component({\n\tprops: {\n\t\tboardImg: {\n\t\t\ttype: String,\n\t\t\tdefault: _defaultImg,\n\t\t},\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t},\n\t\tsubtitle: {\n\t\t\ttype: String,\n\t\t},\n\t\ttags: {\n\t\t\ttype: Array,\n\t\t},\n\t\tcreatedTime: {\n\t\t\ttype: String,\n\t\t},\n\t},\n\ttemplate,\n})\nclass PostHeader extends Vue {}\n\nexport default Vue.component('postHeader', PostHeader);\n"
  },
  {
    "path": "src/client/components/post/post-header/post-header.html",
    "content": "<header class=\"intro-header\" :style=\"{ backgroundImage: 'url(' + boardImg + ')' }\">\n\t<div class=\"container\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\">\n\t\t\t\t<div class=\"post-heading\">\n\t\t\t\t\t<h1 property=\"headline\">{{title}}</h1>\n\t\t\t\t\t<h2 class=\"subheading\" property=\"alternativeHeadline\">{{subtitle}}</h2>\n\t\t\t\t\t<span class=\"meta\">\n\t\t\t\t\t\t<span property=\"keywords\">\n\t\t\t\t\t\t\t<span v-for=\"(tag, index) of tags\" track-by=\"index\">\n\t\t\t\t\t\t\t\t<span v-if=\"index > 0\">, </span>\n\t\t\t\t\t\t\t\t<router-link class=\"tag-link\" :to=\"{ path: '/tags/' + tag.name }\">{{tag.label}}</router-link>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span><br>Posted on <span property=\"datePublished\">{{createdTime}}</span></span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</header>"
  },
  {
    "path": "src/client/components/post/post-header/style.scss",
    "content": ".intro-header {\n  .post-heading {\n    padding: 100px 0 50px;\n    color: white;\n    @media only screen and (min-width: 768px) {\n      padding: 150px 0;\n    }\n  }\n\n  .post-heading {\n    h1 {\n      font-size: 35px;\n    }\n    .subheading,\n    .meta {\n      line-height: 1.1;\n      display: block;\n    }\n    .subheading {\n      font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n      font-size: 24px;\n      margin: 10px 0 30px;\n      font-weight: 600;\n    }\n    .meta {\n      font-family: 'Lora', 'Times New Roman', serif;\n      font-style: italic;\n      font-weight: 300;\n      font-size: 20px;\n      a {\n        color: white;\n      }\n    }\n    @media only screen and (min-width: 768px) {\n      h1 {\n        font-size: 55px;\n      }\n      .subheading {\n        font-size: 30px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/post/style.scss",
    "content": ".post-content {\n  font-family: 'Lora', 'Times New Roman', serif, 'Microsoft YaHei';\n  font-size: 18px;\n\n  p {\n    line-height: 1.5;\n    margin: 1rem 0;\n  }\n\n  pre {\n    padding: 1rem;\n    background-color: #f5f5f5;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    font-size: 80%;\n  }\n\n  img {\n    max-width: 92.5%; // fix img too large bug\n  }\n\n  blockquote {\n    border-left: 5px solid #eee;\n    padding: 0.75rem 1.5rem;\n    margin: 0 0 1.5rem;\n\n    p {\n      margin: 0;\n    }\n  }\n\n  table {\n    width: 100%;\n    margin-bottom: 20px;\n    border: 1px solid #dddddd;\n    border-collapse: collapse;\n    border-left: none;\n\n    th,\n    td {\n      padding: 6px;\n      border-top: 1px solid #dddddd;\n      border-left: 1px solid #dddddd;\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/post/template.html",
    "content": "<article property=\"blogPost\" typeof=\"BlogPosting\">\n\t<span typeof=\"ImageObject\" property=\"image\">\n\t\t<meta :content=\"headerUrl\" property=\"url\">\n\t\t<meta content=\"1366\" property=\"width\">\n\t\t<meta content=\"768\" property=\"height\">\n\t</span>\n\t<span typeof=\"Person\" property=\"author\">\n\t\t<meta content=\"Disciple.Ding\" property=\"name\">\n\t</span>\n\n\t<post-header :board-img=\"headerUrl\" :title=\"post.title\" :subtitle=\"post.subtitle\"\n\t             :tags=\"post.tags\" :created-time=\"post.createdTime\"></post-header>\n\t<div class=\"container\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 post-content\" v-html=\"post.content\" property=\"articleBody\"></div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<div id=\"disqus_thread\" class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\"></div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<pager class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\" :prev=\"prev\" :next=\"next\"></pager>\n\t\t</div>\n\t</div>\n</article>"
  },
  {
    "path": "src/client/components/post-list/index.ts",
    "content": "/**\n * Created by jack on 16-4-25.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport template from './template.html';\nimport './style.scss';\nimport DisqusService from '@/common/service/disqus/DisqusService';\n\n@Component({\n\tprops: ['postList'],\n\ttemplate,\n})\nclass PostList extends Vue {\n\tprotected mounted() {\n\t\tnew DisqusService().resetDisqusCountPlugin();\n\t}\n}\n\nexport default Vue.component('postList', PostList);\n"
  },
  {
    "path": "src/client/components/post-list/style.scss",
    "content": "@import '~@/assets/scss/variables.scss';\n\n// Post Preview Pages\n.post-list {\n  list-style: none;\n  padding: 0;\n\n  .post-preview {\n    .title-link {\n      color: $gray-dark;\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        color: $brand-primary;\n      }\n      .post-title {\n        font-size: 30px;\n        margin-top: 30px;\n        margin-bottom: 10px;\n      }\n      .post-subtitle {\n        margin-bottom: 10px;\n        font-weight: 300;\n      }\n    }\n    .post-meta {\n      color: $gray;\n      display: inline-block;\n      font-size: 18px;\n      font-style: italic;\n      line-height: 1.25;\n      margin: 0;\n      .tag-link {\n        text-decoration: none;\n        color: $gray;\n        &:hover,\n        &:focus {\n          color: $brand-primary;\n          text-decoration: underline;\n        }\n      }\n    }\n    @media only screen and (min-width: 768px) {\n      .title-link {\n        .post-title {\n          font-size: 36px;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/components/post-list/template.html",
    "content": "<ul class=\"post-list\" typeof=\"ItemList\">\n\t<li v-for=\"(post, index) of postList\" track-by=\"index\">\n\t\t<div class=\"post-preview\" property=\"itemListElement\" typeof=\"CreativeWork\">\n\t\t\t<router-link class=\"title-link\" :to=\"{ path: '/posts/' + post.name }\" property=\"url\">\n\t\t\t\t<h2 class=\"post-title\" property=\"headline\">{{ post.title }}</h2>\n\t\t\t\t<h3 class=\"post-subtitle\" property=\"alternativeHeadline\">{{ post.subtitle }}</h3>\n\t\t\t</router-link>\n\t\t\t<p class=\"post-meta\">\n\t\t\t\t<span property=\"keywords\">\n\t\t\t\t\t<span v-for=\"(tag, index) of post.tags\" track-by=\"index\">\n\t\t\t\t\t\t<span v-if=\"index > 0\">, </span>\n\t\t\t\t\t\t<router-link class=\"tag-link\" :to=\"{ path: '/tags/' + tag.name }\">{{tag.label}}</router-link>\n\t\t\t\t\t</span>\n\t\t\t\t</span><br>Posted on <span property=\"dateCreated\">{{ post.createdTime }}</span><br>\n\t\t\t\t<span class=\"disqus-comment-count\" :data-disqus-identifier=\"post.name\"></span>\n\t\t\t\t<meta property=\"position\" :content=\"index\">\n\t\t\t</p>\n\t\t</div>\n\t\t<hr>\n\t</li>\n</ul>"
  },
  {
    "path": "src/client/components/tags/index.ts",
    "content": "/**\n * Created by jack on 16-8-27.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\n\nimport template from './template.html';\nimport './style.scss';\n\n@Component({\n\tprops: ['tagsList'],\n\ttemplate,\n})\nclass Tags extends Vue {}\n\nexport default Vue.component('tags', Tags);\n"
  },
  {
    "path": "src/client/components/tags/style.scss",
    "content": "@import '~@/assets/scss/variables';\n\n.tags-block {\n  .post-title__link {\n    color: $gray;\n  }\n}\n"
  },
  {
    "path": "src/client/components/tags/template.html",
    "content": "<div class=\"tags-block\">\n\t<dl v-for=\"(tag, index) of tagsList\" track-by=\"index\">\n\t\t<dt><h2>{{tag.label}}</h2></dt>\n\t\t<dd v-for=\"(post, index) of tag.posts\" track-by=\"index\">\n\t\t\t<router-link class=\"post-title__link\" :to=\"{ path: '/posts/' + post.name }\">{{post.title}}</router-link>\n\t\t</dd>\n\t\t<hr>\n\t</dl>\n</div>\n"
  },
  {
    "path": "src/client/containers/about/about.html",
    "content": "<section class=\"about-section\">\n\t<!-- Content Header -->\n\t<content-header :board-img=\"header.image\" :title=\"header.title\" :subtitle=\"header.subtitle\"></content-header>\n\n\t<main-content>\n\t\t<about-me :introduction=\"introduction\"></about-me>\n\t</main-content>\n</section>"
  },
  {
    "path": "src/client/containers/about/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue, { ComponentOptions } from 'vue';\nimport Component from 'vue-class-component';\nimport { mapState, mapActions, Store } from 'vuex';\n\nimport { getActionContext } from '@/vuex/common/actionHelper';\nimport { IRootState } from '@/vuex/module/index';\nimport { AboutMeState } from '@/vuex/module/about-me';\nimport aboutActions from '@/vuex/module/about-me/actions';\nimport template from './about.html';\n\nexport interface IAboutContainer extends Vue {\n\tinitAboutPage: () => void;\n}\n\n@Component({\n\ttemplate,\n\tcomputed: mapState({\n\t\theader: (state: IRootState) => state.aboutMe.header,\n\t\tintroduction: (state: IRootState) => state.aboutMe.introduction,\n\t}),\n\tmethods: mapActions(['initAboutPage']),\n})\nexport default class AboutMeContainer extends Vue {\n\tprivate initAboutPage: () => void;\n\n\tpublic created() {\n\t\tthis.initAboutPage();\n\t}\n\n\tpublic preFetch(store: Store<IRootState>) {\n\t\tconst actionContext = getActionContext<AboutMeState, IRootState>('aboutMe', store);\n\t\treturn aboutActions.initAboutPage(actionContext);\n\t}\n}\n"
  },
  {
    "path": "src/client/containers/blog/blog.html",
    "content": "<main>\n\t<navigation :nav-list=\"navList\" :mode=\"isDesktop ? 'desktop' : 'mobile'\"></navigation>\n\n\t<router-view></router-view>\n\n\t<hr>\n\n\t<page-footer :social-link-list=\"socialLinkList\"></page-footer>\n</main>"
  },
  {
    "path": "src/client/containers/blog/index.ts",
    "content": "/**\n * Created by jack on 16-8-14.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\nimport { mapGetters, mapActions } from 'vuex';\n\nimport { setBlogTitle } from '@/common/service/CommonService';\nimport template from './blog.html';\n\n@Component({\n\tcomputed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']),\n\tmethods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']),\n\ttemplate,\n\twatch: {\n\t\ttitle() {\n\t\t\tsetBlogTitle((this as BlogContainer).title);\n\t\t},\n\t},\n})\nclass BlogContainer extends Vue {\n\tpublic title: string;\n\tpublic loadBrowserSetting: () => void;\n\tpublic loadNavList: () => void;\n\tpublic loadSocialLink: () => void;\n\n\tpublic created() {\n\t\tthis.loadBrowserSetting();\n\t\tthis.loadNavList();\n\t\tthis.loadSocialLink();\n\t}\n}\n\nexport default Vue.component('blog', BlogContainer);\n"
  },
  {
    "path": "src/client/containers/home/home.html",
    "content": "<section>\n\t<!-- Content Header -->\n\t<content-header :board-img=\"header.image\" :title=\"header.title\" :subtitle=\"header.subtitle\"></content-header>\n\n\t<main-content>\n\t\t<lazy-loading\n\t\t\t\t:load-fn=\"loadPostList\"\n\t\t\t\t:is-loading=\"posts.isLoading\"\n\t\t\t\t:is-finished=\"posts.isFinished\"\n\t\t\t\tlistener-target-selector=\"document\">\n\t\t\t<post-list :post-list=\"posts.list\"></post-list>\n\t\t</lazy-loading>\n\t</main-content>\n</section>"
  },
  {
    "path": "src/client/containers/home/index.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue, { ComponentOptions } from 'vue';\nimport Component from 'vue-class-component';\nimport { mapState, mapGetters, mapActions, Store } from 'vuex';\n\nimport { getActionContext } from '@/vuex/common/actionHelper';\nimport template from './home.html';\nimport { IRootState } from '@/vuex/module/index';\nimport { HomeState } from '@/vuex/module/home';\nimport homeActions from '@/vuex/module/home/actions';\n\n@Component({\n\tcomputed: {\n\t\t...mapState({\n\t\t\theader: (state: IRootState) => state.home.header,\n\t\t}),\n\t\t...mapGetters(['posts']),\n\t},\n\tmethods: mapActions(['initHomePage', 'loadPostList']),\n\ttemplate,\n\tpreFetch(store: Store<IRootState>) {\n\t\tconst actionContext = getActionContext<HomeState, IRootState>('home', store);\n\t\treturn homeActions.loadPostList(actionContext);\n\t},\n})\nexport default class HomeContainer extends Vue {\n\tpublic initHomePage: () => void;\n\n\tpublic mounted() {\n\t\tthis.initHomePage();\n\t}\n}\n"
  },
  {
    "path": "src/client/containers/post/index.ts",
    "content": "/**\n * Created by jack on 16-4-25.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\nimport { mapActions, mapState, Store} from 'vuex';\nimport VueRouter from 'vue-router';\n\nimport Post from 'types/post';\nimport { getActionContext } from '@/vuex/common/actionHelper';\nimport template from './post.html';\nimport { IRootState } from '@/vuex/module/index';\nimport { PostState } from '@/vuex/module/post';\nimport postActions, { IPostQueryParam } from '@/vuex/module/post/actions';\n\n@Component({\n\tcomputed: mapState({\n\t\tpost: (state: IRootState) => state.post.post,\n\t\tisLoading: (state: IRootState) => state.post.isLoading,\n\t\tpostName: (state: IRootState) => state.route.params.postName,\n\t}),\n\tmethods: mapActions(['getPost']),\n\twatch: {\n\t\tpostName() {\n\t\t\t(this as PostContainer).getPost({\n\t\t\t\tpostName: (this as PostContainer).postName,\n\t\t\t\trouter: this.$router,\n\t\t\t});\n\t\t},\n\t},\n\ttemplate,\n\tpreFetch(store: Store<IRootState>, router: VueRouter) {\n\t\tconst actionContext = getActionContext<PostState, IRootState>('post', store);\n\t\treturn postActions.getPost(actionContext, {\n\t\t\tpostName: store.state.route.params.postName,\n\t\t\tenableLoading: false,\n\t\t\trouter,\n\t\t});\n\t},\n})\nexport default class PostContainer extends Vue {\n\tpublic post: Post;\n\tpublic postName: string;\n\tpublic getPost: (params: IPostQueryParam) => void;\n\n\tpublic mounted() {\n\t\tthis.getPost({\n\t\t\tpostName: this.postName,\n\t\t\trouter: this.$router,\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/client/containers/post/post.html",
    "content": "<section>\n\t<div style=\"text-align: center\" v-if=\"isLoading\">\n\t\t<loading></loading>\n\t</div>\n\t<post v-if=\"!isLoading\" :post=\"post\"></post>\n</section>"
  },
  {
    "path": "src/client/containers/tags/index.ts",
    "content": "/**\n * Created by jack on 16-8-27.\n */\n\nimport Vue from 'vue';\nimport Component from 'vue-class-component';\nimport { mapState, mapActions, Store } from 'vuex';\nimport VueRouterstore from 'vue-router';\n\nimport template from './tags.html';\nimport { getActionContext } from '@/vuex/common/actionHelper';\nimport { IRootState } from '@/vuex/module/index';\nimport { TagsState } from '@/vuex/module/tags';\nimport tagsActions, { ITagQueryParam } from '@/vuex/module/tags/actions';\n\n@Component({\n\tcomputed: mapState({\n\t\theader: (state: IRootState) => state.tags.header,\n\t\ttagsList: (state: IRootState) => state.tags.list,\n\t\tisLoading: (state: IRootState) => state.tags.isLoading,\n\t\ttagName: (state: IRootState) => state.route.params.tagName,\n\t}),\n\tmethods: mapActions(['initTagsPage', 'queryTagsList']),\n\ttemplate,\n\twatch: {\n\t\ttagName() {\n\t\t\t(this as TagsContainer).queryTagsList({\n\t\t\t\ttagName: (this as TagsContainer).tagName,\n\t\t\t\trouter: this.$router,\n\t\t\t});\n\t\t},\n\t},\n\tpreFetch(store: Store<IRootState>, router: VueRouterstore) {\n\t\tconst actionContext = getActionContext<TagsState, IRootState>('tags', store);\n\t\treturn tagsActions.queryTagsList(actionContext, {\n\t\t\ttagName: store.state.route.params.tagName,\n\t\t\tenableLoading: false,\n\t\t\trouter,\n\t\t});\n\t},\n})\nexport default class TagsContainer extends Vue {\n\tpublic tagName: string;\n\tpublic initTagsPage: () => void;\n\tpublic queryTagsList: (params: ITagQueryParam) => void;\n\n\tpublic mounted() {\n\t\tthis.initTagsPage();\n\t\tthis.queryTagsList({\n\t\t\ttagName: this.tagName,\n\t\t\trouter: this.$router,\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/client/containers/tags/tags.html",
    "content": "<section class=\"about-section\">\n\t<!-- Content Header -->\n\t<content-header :board-img=\"header.image\" :title=\"header.title\" :subtitle=\"header.subtitle\"></content-header>\n\n\t<main-content>\n\t\t<div style=\"text-align: center\" v-if=\"isLoading\">\n\t\t\t<loading></loading>\n\t\t</div>\n\t\t<tags v-else :tags-list=\"tagsList\"></tags>\n\t</main-content>\n</section>\n"
  },
  {
    "path": "src/client/router.ts",
    "content": "/**\n * Created by jack on 16-4-21.\n */\n\nimport Vue from 'vue';\nimport VueRouter, { Route, RouterOptions } from 'vue-router';\n\nimport Home from '@/containers/home';\nimport About from '@/containers/about';\nimport Post from '@/containers/post';\nimport Tags from '@/containers/tags';\n\n// Inject vue plugin\nVue.use(VueRouter);\n\nconst ROUTER_SETTING: RouterOptions = {\n\tmode: 'history', // default value 'hash'\n\troutes: [\n\t\t{path: '/', component: Home},\n\t\t{path: '/about', component: About},\n\t\t{path: '/posts/:postName', component: Post},\n\t\t{path: '/tags', component: Tags},\n\t\t{path: '/tags/:tagName', component: Tags},\n\t\t// Using 404 page, when page not found.\n\t\t// catch all redirect, not matched path will be redirected to the home path\n\t\t// {path: '*', redirect: '/'}\n\t],\n};\n\nconst createRouter = () => {\n\tconst router = new VueRouter(ROUTER_SETTING);\n\n\t// manually hook: page not scroll to top when router changes\n\t// github issue: https://github.com/vuejs/vue-router/issues/173\n\trouter.beforeEach((route, redirect, next) => {\n\t\twindow.scrollTo(0, 0);\n\t\tnext();\n\t});\n\n\trouter.afterEach((route: Route) => {\n\t\tconsole.info(`${new Date()}: ${route.path}`);\n\t});\n\n\treturn router;\n};\n\nexport default createRouter;\n"
  },
  {
    "path": "src/client/vuex/common/actionHelper.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { Store, ActionContext } from 'vuex';\n\nimport { IRootState } from '../module';\n\nexport interface IMutation {\n\ttype: string;\n\tpayload: any;\n}\n\nconst createAction = (typeName: string = '', data?: any): IMutation => ({ type: typeName, payload: data });\n\nconst getActionContext = <T, S>(module: string, store: any): ActionContext<T, S> => {\n\treturn {\n\t\tdispatch: (key: string, payload: any) => store.dispatch(key, payload, module),\n\t\tcommit: (key: string, payload: any) => store.commit(key, payload, module),\n\t\tstate: store.state[module],\n\t\tgetters: store.getters,\n\t\trootState: store.state,\n\t\trootGetters: store.getters,\n\t};\n};\n\nexport { createAction, getActionContext };\n"
  },
  {
    "path": "src/client/vuex/index.ts",
    "content": "/**\n * Created by jack on 16-8-9.\n */\n\nimport Vue from 'vue';\nimport Vuex from 'vuex';\n// import createLogger from 'vuex/dist/logger';\n\nimport createModules from './module';\n\nVue.use(Vuex);\n\nconst createStore = () =>\n\tnew Vuex.Store({\n\t\t// plugins: process.env.NODE_ENV !== 'production' ? [createLogger()] : [],\n\t\tmodules: createModules(),\n\t\tstrict: true,\n\t});\n\nexport default createStore;\n"
  },
  {
    "path": "src/client/vuex/module/about-me/actions.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { ActionContext } from 'vuex';\n\nimport { createAction } from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { AboutMeState } from './index';\nimport { SET_BLOG_TITLE } from '../site/actions';\nimport image from '@/assets/img/about-bg.jpg';\nimport introduction from './introductions.json';\n\nexport const INIT_ABOUT_ME_PAGE = 'INIT_ABOUT_ME_PAGE';\n\nconst initAboutPage = ({ commit }: ActionContext<AboutMeState, IRootState>) => {\n\tcommit(createAction(SET_BLOG_TITLE, 'About D.D'));\n\tcommit(createAction(INIT_ABOUT_ME_PAGE, {\n\t\theader: {\n\t\t\timage,\n\t\t\ttitle: 'About D.D',\n\t\t\tsubtitle: 'Disciple.Ding',\n\t\t},\n\t\tintroduction,\n\t}));\n};\n\nexport default { initAboutPage };\n"
  },
  {
    "path": "src/client/vuex/module/about-me/index.ts",
    "content": "/**\n * Created by jack on 16-8-15.\n */\n\nimport { Module, ActionTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport mutations from './mutations';\nimport actions from './actions';\nimport { ITitle } from 'types/page';\n\nexport class AboutMeState {\n\tpublic header: ITitle;\n\tpublic introduction: any[];\n}\n\nexport default class AboutMeModule implements Module<AboutMeState, IRootState> {\n\tpublic state: AboutMeState;\n\tpublic actions: ActionTree<AboutMeState, IRootState>;\n\tpublic mutations: MutationTree<AboutMeState>;\n\tconstructor() {\n\t\tthis.state = new AboutMeState();\n\t\tthis.actions = actions;\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/about-me/introductions.json",
    "content": "[\n    {\n        \"name\": \"motto\",\n        \"label\": \"Motto\",\n        \"value\": \"Keep on moving forward to glimpse the end.\"\n    },\n    {\n        \"name\": \"email\",\n        \"label\": \"Email\",\n        \"value\": \"disciple.ding@gmail.com\"\n    },\n    {\n        \"name\": \"hobby\",\n        \"label\": \"Hobby\",\n        \"value\": \"Ball games, Swimming, Travelling, Reading\"\n    },\n    {\n        \"name\": \"skill\",\n        \"label\": \"Technology stack\",\n        \"value\": [\n            {\n                \"name\": \"javascript\",\n                \"label\": \"JavaScript\",\n                \"value\": [\n                    {\n                        \"name\": \"es6\",\n                        \"label\": \"ES 6+\",\n                        \"value\": 4,\n                        \"link\": \"https://babeljs.io/docs/learn-es2015/\"\n                    },\n                    {\n                        \"name\": \"ts\",\n                        \"label\": \"TypeScript\",\n                        \"value\": 2.5,\n                        \"link\": \"http://www.typescriptlang.org/docs/home.html\"\n                    },\n                    {\n                        \"name\": \"angular1.x\",\n                        \"label\": \"Angular 1.x\",\n                        \"value\": 3,\n                        \"link\": \"https://docs.angularjs.org/api\"\n                    },\n                    {\n                        \"name\": \"react\",\n                        \"label\": \"React\",\n                        \"value\": 3.5,\n                        \"link\": \"https://facebook.github.io/react/docs/getting-started.html\"\n                    },\n                    {\n                        \"name\": \"vue\",\n                        \"label\": \"Vue\",\n                        \"value\": 3,\n                        \"link\": \"https://vuejs.org/api/\"\n                    },\n                    {\n                        \"name\": \"jquery\",\n                        \"label\": \"jQuery\",\n                        \"value\": 2,\n                        \"link\": \"http://api.jquery.com/\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"state-management\",\n                \"label\": \"State management\",\n                \"value\": [\n                    {\n                        \"name\": \"redux\",\n                        \"label\": \"Redux\",\n                        \"value\": 3.5,\n                        \"link\": \"http://redux.js.org/index.html\"\n                    },\n                    {\n                        \"name\": \"vuex\",\n                        \"label\": \"vuex\",\n                        \"value\": 3,\n                        \"link\": \"http://vuex.vuejs.org/en/intro.html\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"css\",\n                \"label\": \"CSS\",\n                \"value\": [\n                    {\n                        \"name\": \"sass\",\n                        \"label\": \"Sass\",\n                        \"value\": 3,\n                        \"link\": \"http://sass-lang.com/guide\"\n                    },\n                    {\n                        \"name\": \"postcss\",\n                        \"label\": \"Postcss(Autoprefix only)\",\n                        \"value\": 1,\n                        \"link\": \"http://postcss.org/\"\n                    },\n                    {\n                        \"name\": \"bootstrap\",\n                        \"label\": \"Bootstrap\",\n                        \"value\": 2.5,\n                        \"link\": \"http://v4-alpha.getbootstrap.com/getting-started/introduction/\"\n                    },\n                    {\n                        \"name\": \"angular-material\",\n                        \"label\": \"Angular Material\",\n                        \"value\": 2.5,\n                        \"link\": \"https://material.angularjs.org/latest\"\n                    },\n                    {\n                        \"name\": \"ionic\",\n                        \"label\": \"Ionic\",\n                        \"value\": 1,\n                        \"link\": \"http://ionicframework.com/docs/overview/\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"package\",\n                \"label\": \"Package\",\n                \"value\": [\n                    {\n                        \"name\": \"gulp\",\n                        \"label\": \"Gulp\",\n                        \"value\": 3,\n                        \"link\": \"https://github.com/gulpjs/gulp/blob/master/docs/API.md\"\n                    },\n                    {\n                        \"name\": \"webpack\",\n                        \"label\": \"webpack\",\n                        \"value\": 4,\n                        \"link\": \"http://webpack.github.io/docs/\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"node\",\n                \"label\": \"Node\",\n                \"value\": [\n                    {\n                        \"name\": \"express\",\n                        \"label\": \"Express\",\n                        \"value\": 2,\n                        \"link\": \"http://expressjs.com/en/4x/api.html\"\n                    },\n                    {\n                        \"name\": \"koa\",\n                        \"label\": \"Koa\",\n                        \"value\": 2,\n                        \"link\": \"http://koajs.com/\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"api-design\",\n                \"label\": \"API Design\",\n                \"value\": [\n                    {\n                        \"name\": \"rest\",\n                        \"label\": \"REST\",\n                        \"value\": 3,\n                        \"link\": \"https://zh.wikipedia.org/wiki/REST\"\n                    },\n                    {\n                        \"name\": \"graphql\",\n                        \"label\": \"GraphQL\",\n                        \"value\": 3.5,\n                        \"link\": \"https://github.com/graphql/graphql-js\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"release-tool\",\n                \"label\": \"Release tool\",\n                \"value\": [\n                    {\n                        \"name\": \"docker\",\n                        \"label\": \"Docker\",\n                        \"value\": 3,\n                        \"link\": \"https://www.docker.com/\"\n                    }\n                ],\n                \"type\": \"list\"\n            },\n            {\n                \"name\": \"static-server\",\n                \"label\": \"Static server\",\n                \"value\": [\n                    {\n                        \"name\": \"nginx\",\n                        \"label\": \"Nginx\",\n                        \"value\": 2,\n                        \"link\": \"https://nginx.org/en/docs/\"\n                    }\n                ],\n                \"type\": \"list\"\n            }\n        ],\n        \"type\": \"list\"\n    }\n]"
  },
  {
    "path": "src/client/vuex/module/about-me/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { AboutMeState } from './index';\nimport { IMutation } from '../../common/actionHelper';\nimport { INIT_ABOUT_ME_PAGE } from './actions';\n\nconst mutations = {\n\t[INIT_ABOUT_ME_PAGE](state: AboutMeState, mutation: IMutation) {\n\t\tObject.assign(state, mutation.payload);\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client/vuex/module/browser/actions.ts",
    "content": "/**\n * Created by jack on 16-8-20.\n */\n\nimport { ActionContext } from 'vuex';\n\nimport { createAction } from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { BrowserState } from './index';\n\nexport const LOAD_BROWSER_SETTING = 'LOAD_BROWSER_SETTING';\n\nconst loadBrowserSetting = ({ commit }: ActionContext<BrowserState, IRootState>) => {\n\tconst browser = {\n\t\tclientWidth: document.body.clientWidth,\n\t};\n\tcommit(createAction(LOAD_BROWSER_SETTING, browser));\n};\n\nexport default {loadBrowserSetting};\n"
  },
  {
    "path": "src/client/vuex/module/browser/index.ts",
    "content": "/**\n * Created by jack on 16-8-20.\n */\n\nimport { Module, ActionTree, GetterTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport class BrowserState {\n\tpublic clientWidth: number;\n\tconstructor() {\n\t\tthis.clientWidth = 0;\n\t}\n}\n\nconst MIN_SCREEN_WIDTH: number = 768;\n\nexport default class BrowserModule implements Module<BrowserState, IRootState> {\n\tpublic state: BrowserState;\n\tpublic actions: ActionTree<BrowserState, IRootState>;\n\tpublic getters: GetterTree<BrowserState, IRootState>;\n\tpublic mutations: MutationTree<BrowserState>;\n\tconstructor() {\n\t\tthis.state = new BrowserState();\n\t\tthis.actions = actions;\n\t\tthis.getters = {\n\t\t\tisDesktop: (state: BrowserState) => state.clientWidth >= MIN_SCREEN_WIDTH,\n\t\t};\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/browser/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-20.\n */\n\nimport { BrowserState } from './index';\nimport { IMutation } from '../../common/actionHelper';\nimport { LOAD_BROWSER_SETTING } from './actions';\n\nconst mutations = {\n\t[LOAD_BROWSER_SETTING](state: BrowserState, mutation: IMutation) {\n\t\tstate.clientWidth = mutation.payload.clientWidth;\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client/vuex/module/home/actions.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { ActionContext } from 'vuex';\n\nimport image from '@/assets/img/home-bg.jpg';\nimport PostService from '@/common/service/PostService';\nimport { createAction } from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { HomeState } from './index';\nimport { SET_BLOG_TITLE } from '../site/actions';\n\nexport const INIT_HOME_PAGE = 'INIT_HOME_PAGE';\nexport const QUERY_POSTS_LIST = 'QUERY_POSTS_LIST';\nexport const RECEIVE_POSTS_LIST = 'RECEIVE_POSTS_LIST';\n\nconst initHomePage = ({ commit }: ActionContext<HomeState, IRootState>) => {\n\tcommit(createAction(SET_BLOG_TITLE));\n\tcommit(createAction(INIT_HOME_PAGE, {\n\t\theader: {\n\t\t\timage,\n\t\t\ttitle: 'D.D Blog',\n\t\t\tsubtitle: 'Share More, Gain More.',\n\t\t},\n\t}));\n};\n\nconst loadPostList = ({ state, commit }: ActionContext<HomeState, IRootState>) => {\n\tcommit(QUERY_POSTS_LIST);\n\tconst pager = {\n\t\t...state.posts.pager,\n\t\tnum: state.posts.pager.num,\n\t};\n\treturn new PostService().queryPostList(pager)\n\t\t.then((result = {}) => {\n\t\t\tcommit(createAction(RECEIVE_POSTS_LIST, {\n\t\t\t\tpostsList: result.data ? result.data.posts : [],\n\t\t\t}));\n\t\t});\n};\n\nexport default {initHomePage, loadPostList};\n"
  },
  {
    "path": "src/client/vuex/module/home/index.ts",
    "content": "/**\n * Created by jack on 16-8-15.\n */\n\nimport { Module, ActionTree, GetterTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport { ITitle } from 'types/page';\nimport { IPager } from 'types/pager';\nimport Post from 'types/post';\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport class HomeState {\n\tpublic header: ITitle;\n\tpublic posts: {\n\t\tlist: Post[],\n\t\tpager: IPager,\n\t\tisFinished: boolean,\n\t\tisLoading: boolean,\n\t};\n\tconstructor() {\n\t\tthis.header = {\n\t\t\timage: '',\n\t\t\ttitle: '',\n\t\t};\n\t\tthis.posts = {\n\t\t\tisFinished: false,\n\t\t\tisLoading: false,\n\t\t\tlist: [],\n\t\t\tpager: {\n\t\t\t\tnum: 0,\n\t\t\t\tsize: 5,\n\t\t\t},\n\t\t};\n\t}\n}\n\nexport default class HomeModule implements Module<HomeState, IRootState> {\n\tpublic state: HomeState;\n\tpublic actions: ActionTree<HomeState, IRootState>;\n\tpublic getters: GetterTree<HomeState, IRootState>;\n\tpublic mutations: MutationTree<HomeState>;\n\tconstructor() {\n\t\tthis.state = new HomeState();\n\t\tthis.actions = actions;\n\t\tthis.getters = {\n\t\t\tposts: (state: HomeState) => state.posts,\n\t\t};\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/home/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { HomeState } from './index';\nimport { IMutation } from '../../common/actionHelper';\nimport { INIT_HOME_PAGE, QUERY_POSTS_LIST, RECEIVE_POSTS_LIST } from './actions';\n\nconst mutations = {\n\t[INIT_HOME_PAGE](state: HomeState, mutation: IMutation) {\n\t\tstate.header = mutation.payload.header;\n\t},\n\n\t[QUERY_POSTS_LIST](state: HomeState) {\n\t\tstate.posts.isLoading = true;\n\t},\n\n\t[RECEIVE_POSTS_LIST](state: HomeState, mutation: IMutation) {\n\t\tif (mutation.payload.postsList.length) {\n\t\t\tstate.posts.pager.num++;\n\t\t} else {\n\t\t\tstate.posts.isFinished = true;\n\t\t}\n\t\tstate.posts.list = state.posts.list.concat(mutation.payload.postsList);\n\t\tstate.posts.isLoading = false;\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client/vuex/module/index.ts",
    "content": "/**\n * Created by jack on 16-8-27.\n */\nimport { Route } from 'vue-router';\n\nimport BrowserModule, { BrowserState } from './browser';\nimport HomeModule, { HomeState } from './home';\nimport AboutMeModule, { AboutMeState } from './about-me';\nimport PostModule, { PostState } from './post';\nimport SiteModule, { SiteState } from './site';\nimport TagsModule, { TagsState } from './tags';\n\nexport interface IRootState {\n\tbrowser: BrowserState;\n\thome: HomeState;\n\taboutMe: AboutMeState;\n\tpost: PostState;\n\tsite: SiteState;\n\ttags: TagsState;\n\troute: Route;\n}\n\nexport default () => ({\n\tbrowser: new BrowserModule(),\n\tsite: new SiteModule(),\n\taboutMe: new AboutMeModule(),\n\thome: new HomeModule(),\n\tpost: new PostModule(),\n\ttags: new TagsModule(),\n});\n"
  },
  {
    "path": "src/client/vuex/module/post/actions.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { ActionContext, Store } from 'vuex';\nimport VueRouter from 'vue-router';\n\nimport PostService, { IQueryPostResponse } from '@/common/service/PostService';\n\nimport { createAction } from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { PostState } from './index';\nimport { SET_BLOG_TITLE } from '../site/actions';\n\nexport const GET_POST = 'GET_POST';\nexport const RECEIVE_POST = 'RECEIVE_POST';\n\nexport interface IPostQueryParam {\n\tpostName: string;\n\trouter?: VueRouter;\n\tenableLoading?: boolean;\n}\n\nconst getPost =\n\t({ commit }: ActionContext<PostState, IRootState>, { postName, enableLoading = true, router }: IPostQueryParam) => {\n\t\tenableLoading && commit(GET_POST);\n\t\treturn new PostService().getPostByName(postName)\n\t\t\t.then((result: GraphQLResponse<IQueryPostResponse>) => {\n\t\t\t\tif (result.data && result.data.post) {\n\t\t\t\t\treturn result.data;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error('Post not found!');\n\t\t\t\t}\n\t\t\t})\n\t\t\t.then((blog: IQueryPostResponse) => {\n\t\t\t\tcommit(createAction(RECEIVE_POST, blog));\n\t\t\t\tcommit(createAction(SET_BLOG_TITLE, blog.post.title));\n\t\t\t})\n\t\t\t.catch((err: Error) => {\n\t\t\t\tcommit(RECEIVE_POST);\n\t\t\t\tconsole.error(err + ' Page will redirect to the Home page.');\n\t\t\t\trouter && router.replace('/');\n\t\t\t});\n\t};\n\nexport default { getPost };\n"
  },
  {
    "path": "src/client/vuex/module/post/index.ts",
    "content": "/**\n * Created by jack on 16-8-15.\n */\n\nimport { Module, ActionTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport Post from '../../../../types/post'; // ts module bug, it should work well with 'types/post', but not\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport class PostState {\n\tpublic post: Post;\n\tpublic isLoading: boolean;\n\tconstructor() {\n\t\tthis.isLoading = false;\n\t\tthis.post = new Post({\n\t\t\tid: -1,\n\t\t\tname: '',\n\t\t\ttitle: '',\n\t\t\tcontent: '',\n\t\t\tcreatedTime: '',\n\t\t\ttags: [],\n\t\t});\n\t}\n}\n\nexport default class PostModule implements Module<PostState, IRootState> {\n\tpublic state: PostState;\n\tpublic actions: ActionTree<PostState, IRootState>;\n\tpublic mutations: MutationTree<PostState>;\n\tconstructor() {\n\t\tthis.state = new PostState();\n\t\tthis.actions = actions;\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/post/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { IMutation } from '../../common/actionHelper';\nimport { PostState } from './index';\nimport { GET_POST, RECEIVE_POST } from './actions';\n\nconst mutations = {\n\t[GET_POST](state: PostState) {\n\t\tstate.isLoading = true;\n\t},\n\n\t[RECEIVE_POST](state: PostState, mutation: IMutation) {\n\t\tstate.isLoading = false;\n\t\tmutation && (state.post = mutation.payload.post);\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client/vuex/module/site/actions.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { ActionContext } from 'vuex';\n\nimport PostService from '@/common/service/PostService';\nimport {createAction} from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { SiteState } from './index';\nimport SocialLinkSetting from './setting';\n\nexport const LOAD_NAV_LIST = 'LOAD_NAV_LIST';\nexport const LOAD_SOCIAL_LINK = 'LOAD_SOCIAL_LINK';\nexport const SET_BLOG_TITLE = 'SET_BLOG_TITLE';\n\nconst setBlogTitle = ({ commit }: ActionContext<SiteState, IRootState>, title: string) =>\n commit(createAction(SET_BLOG_TITLE, title));\n\nconst loadNavList = ({ commit }: ActionContext<SiteState, IRootState>) => {\n\treturn new PostService().getLatestPost()\n\t\t.then((result = {}) => {\n\t\t\tcommit(createAction(LOAD_NAV_LIST, result.data ? result.data.posts[0] : {}));\n\t\t});\n};\n\nconst loadSocialLink = ({ commit }: ActionContext<SiteState, IRootState>) =>\n commit(createAction(LOAD_SOCIAL_LINK, SocialLinkSetting));\n\nexport default {loadNavList, loadSocialLink, setBlogTitle};\n"
  },
  {
    "path": "src/client/vuex/module/site/index.ts",
    "content": "/**\n * Created by jack on 16-8-15.\n */\n\nimport { Module, ActionTree, GetterTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport { BLOG_TITLE } from '@/common/constant/site';\nimport { Item } from 'types/nav';\nimport { ISocialLink } from './setting';\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport class SiteState {\n\tpublic title: string;\n\tpublic navList: Item[];\n\tpublic socialLinkList: ISocialLink[];\n\tconstructor(title: string) {\n\t\tthis.title = title;\n\t}\n}\n\nexport default class SiteModule implements Module<SiteState, IRootState> {\n\tpublic state: SiteState;\n\tpublic actions: ActionTree<SiteState, IRootState>;\n\tpublic getters: GetterTree<SiteState, IRootState>;\n\tpublic mutations: MutationTree<SiteState>;\n\tconstructor() {\n\t\tthis.state = new SiteState(BLOG_TITLE);\n\t\tthis.actions = actions;\n\t\tthis.getters = {\n\t\t\ttitle: (state: SiteState) => state.title,\n\t\t\tnavList: (state: SiteState) => state.navList,\n\t\t\tsocialLinkList: (state: SiteState) => state.socialLinkList,\n\t\t};\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/site/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { Item } from '../../../../types/nav'; // ts module bug, it should work well with 'types/nav', but not\nimport svgPath from './social-link.svg';\nimport { IMutation } from '../../common/actionHelper';\nimport { SiteState } from './index';\nimport { ISocialLink } from './setting';\nimport { LOAD_NAV_LIST, LOAD_SOCIAL_LINK, SET_BLOG_TITLE } from './actions';\nimport { isSupportShareAPI, sharePage } from '@/common/service/pwa/ShareService';\n\nconst initNavList = () => {\n\tconst navList: Item[] = [];\n\tnavList.push(new Item('home', 'Home', '/'));\n\tnavList.push(new Item('aboutMe', 'About', '/about'));\n\tnavList.push(new Item('tags', 'Tags', '/tags'));\n\tisSupportShareAPI() && navList.push(new Item('share', 'Share', '', sharePage));\n\treturn navList;\n};\n\nconst mutations = {\n\t[LOAD_NAV_LIST](state: SiteState, mutation: IMutation) {\n\t\tconst navList = initNavList();\n\t\tnavList.push(new Item('latestPost', 'Latest Post', `/posts/${mutation.payload.name}`));\n\t\tstate.navList = navList;\n\t},\n\n\t[LOAD_SOCIAL_LINK](state: SiteState, mutation: IMutation) {\n\t\tstate.socialLinkList = mutation.payload\n\t\t\t.filter((item: ISocialLink) => !!item.link)\n\t\t\t.map((item: ISocialLink) => ({\n\t\t\t\t...item,\n\t\t\t\tsvgPath: svgPath + '#' + item.name,\n\t\t\t}));\n\t},\n\n\t[SET_BLOG_TITLE](state: SiteState, mutation: IMutation) {\n\t\tstate.title = mutation.payload;\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client/vuex/module/site/setting.ts",
    "content": "/**\n * Created by jack on 16-5-15.\n */\n\nexport interface ISocialLink {\n\tname: string;\n\tlink: string;\n}\n\nconst SocialLinkSetting: ISocialLink[] = [{\n\tname: 'douban',\n\tlink: 'https://book.douban.com/mine?icn=index-nav',\n}, {\n\tname: 'facebook',\n\tlink: '',\n}, {\n\tname: 'github',\n\tlink: 'https://github.com/DiscipleD',\n}, {\n\tname: 'gmail',\n\tlink: '',\n}, {\n\tname: 'jianshu',\n\tlink: 'http://www.jianshu.com/users/6ed7563919d4/latest_articles',\n}, {\n\tname: 'linkedin',\n\tlink: '',\n}, {\n\tname: 'medium',\n\tlink: '',\n}, {\n\tname: 'sina',\n\tlink: '',\n}, {\n\tname: 'twitter',\n\tlink: '',\n}, {\n\tname: 'xitujuejin',\n\tlink: '',\n}, {\n\tname: 'youtube',\n\tlink: '',\n}, {\n\tname: 'zhihu',\n\tlink: 'https://www.zhihu.com/people/discipled',\n}];\n\nexport default SocialLinkSetting;\n"
  },
  {
    "path": "src/client/vuex/module/tags/actions.ts",
    "content": "/**\n * Created by jack on 16-8-27.\n */\nimport { ActionContext } from 'vuex';\nimport VueRouter from 'vue-router';\n\nimport TagService, { IQueryTagsResponse } from '@/common/service/TagService';\nimport image from '@/assets/img/tags-bg.jpg';\nimport { createAction } from '../../common/actionHelper';\nimport { IRootState } from '../index';\nimport { TagsState } from './index';\nimport { SET_BLOG_TITLE } from '../site/actions';\n\nexport interface ITagQueryParam {\n\ttagName: string;\n\trouter: VueRouter;\n\tenableLoading?: boolean;\n}\n\nexport const INIT_TAGS_PAGE = 'INIT_TAGS_PAGE';\nexport const QUERY_TAGS = 'QUERY_TAGS';\nexport const RECEIVE_TAGS = 'RECEIVE_TAGS';\n\nconst initTagsPage = ({ commit }: ActionContext<TagsState, IRootState>) => {\n\tcommit(createAction(INIT_TAGS_PAGE, {\n\t\theader: {\n\t\t\timage,\n\t\t\ttitle: 'Tags',\n\t\t\tsubtitle: '',\n\t\t},\n\t}));\n};\n\nconst queryTagsList =\n\t({ commit }: ActionContext<TagsState, IRootState>, { tagName, router, enableLoading = true }: ITagQueryParam) => {\n\t\tenableLoading && commit(QUERY_TAGS);\n\t\treturn TagService.queryTagsList(tagName)\n\t\t\t.then((result: GraphQLResponse<IQueryTagsResponse>) => {\n\t\t\t\tif (result.data && result.data.tags && result.data.tags.length > 0) {\n\t\t\t\t\treturn result.data;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error('Tag not found!');\n\t\t\t\t}\n\t\t\t})\n\t\t\t.then((data: IQueryTagsResponse) => {\n\t\t\t\tcommit(createAction(RECEIVE_TAGS, data));\n\t\t\t\tcommit(createAction(SET_BLOG_TITLE, data.tags.length === 1 ? data.tags[0].label : 'Tags'));\n\t\t\t})\n\t\t\t.catch((err: Error) => {\n\t\t\t\tcommit(createAction(RECEIVE_TAGS));\n\t\t\t\tconsole.error(err + ' Page will redirect to the Home page.');\n\t\t\t\trouter.replace('/');\n\t\t\t});\n\t};\n\nexport default { initTagsPage, queryTagsList };\n"
  },
  {
    "path": "src/client/vuex/module/tags/index.ts",
    "content": "/**\n * Created by jack on 16-8-15.\n */\n\nimport { Module, ActionTree, MutationTree } from 'vuex';\n\nimport { IRootState } from '../index';\nimport { ITagPage } from 'types/tag';\nimport { ITitle } from 'types/page';\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport class TagsState {\n\tpublic header: ITitle;\n\tpublic list: ITagPage[];\n\tpublic isLoading: boolean;\n\tconstructor() {\n\t\tthis.header = {\n\t\t\timage: '',\n\t\t\ttitle: '',\n\t\t};\n\t\tthis.isLoading = false;\n\t}\n}\n\nexport default class TagsModule implements Module<TagsState, IRootState> {\n\tpublic state: TagsState;\n\tpublic actions: ActionTree<TagsState, IRootState>;\n\tpublic mutations: MutationTree<TagsState>;\n\tconstructor() {\n\t\tthis.state = new TagsState();\n\t\tthis.actions = actions;\n\t\tthis.mutations = mutations;\n\t}\n}\n"
  },
  {
    "path": "src/client/vuex/module/tags/mutations.ts",
    "content": "/**\n * Created by jack on 16-8-16.\n */\n\nimport { TagsState } from './index';\nimport { IMutation } from '../../common/actionHelper';\nimport { INIT_TAGS_PAGE, QUERY_TAGS, RECEIVE_TAGS } from './actions';\n\nconst mutations = {\n\t[INIT_TAGS_PAGE](state: TagsState, mutation: IMutation) {\n\t\tObject.assign(state, mutation.payload);\n\t},\n\n\t[QUERY_TAGS](state: TagsState) {\n\t\tstate.isLoading = true;\n\t},\n\n\t[RECEIVE_TAGS](state: TagsState, mutation: IMutation) {\n\t\tstate.isLoading = false;\n\t\tmutation && (state.list = mutation.payload.tags);\n\t},\n};\n\nexport default mutations;\n"
  },
  {
    "path": "src/client-entry.ts",
    "content": "/**\n * Created by jack on 16-11-27.\n */\n\nimport Vue, { ComponentOptions } from 'vue';\nimport createApp from '@/app';\nimport '@/common/service/pwa/ServiceWorkerService';\nimport '@/common/service/pwa/NotificationService';\n\nconst { app, router, store } = createApp();\n\nstore.replaceState(window.__INITIAL_STATE__);\n\nrouter.onReady(() => {\n\t// Add router hook for handling asyncData.\n\t// Doing it after initial route is resolved so that we don't double-fetch\n\t// the data that we already have. Using router.beforeResolve() so that all\n\t// async components are resolved.\n\trouter.beforeResolve((to, from, next) => {\n\t\tconst matched = router.getMatchedComponents(to);\n\t\tconst prevMatched = router.getMatchedComponents(from);\n\n\t\t// we only care about none-previously-rendered components,\n\t\t// so we compare them until the two matched lists differ\n\t\tlet diffed = false;\n\t\tconst activated = matched.filter((c, i) => {\n\t\t\treturn diffed || (diffed = (prevMatched[i] !== c));\n\t\t});\n\n\t\tif (!activated.length) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// this is where we should trigger a loading indicator if there is one\n\n\t\tPromise.all(activated.map((component: ComponentOptions<Vue>) => {\n\t\t\tif (component.preFetch) {\n\t\t\t\treturn component.preFetch(store);\n\t\t\t}\n\t\t})).then(() => {\n\t\t\tnext();\n\t\t}).catch(next);\n\t});\n\n\tapp.$mount('#app');\n});\n"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\n\t<meta charset=\"utf-8\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<!-- Origin Trial Token, feature = Web Share, origin = https://discipled.me, expires = 2017-04-17 -->\n\t<meta http-equiv=\"origin-trial\" data-feature=\"Web Share\" data-expires=\"2017-04-17\" content=\"ApDazutYjrfIItAUFfHZS60a8G7/vaNYyZXfKiQSYI0xXVFjo/P191rNCqc9Eeb6Fj7drfzDlhYJ1X3DiQNCdAYAAABOeyJvcmlnaW4iOiJodHRwczovL2Rpc2NpcGxlZC5tZTo0NDMiLCJmZWF0dXJlIjoiV2ViU2hhcmUiLCJleHBpcnkiOjE0OTI0NzM2MDB9\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\t<meta name=\"description\" content=\"Share More, Gain More. - D.D Blog\">\n\t<meta name=\"author\" content=\"Disciple.Ding\">\n\n\t<title>{{title}}</title>\n\n\t<link href=\"https://cdn.bootcss.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css\" rel=\"stylesheet\">\n\t<link href=\"https://cdn.bootcss.com/tether/1.3.2/css/tether.min.css\" rel=\"stylesheet\">\n\t<link rel=\"manifest\" href=\"/manifest.json\">\n\t<link rel=\"icon\" href=\"assets/img/logo/size-32.png\" sizes=\"32x32\">\n\t<link rel=\"icon\" href=\"assets/img/logo/size-48.png\" sizes=\"48x48\">\n\n\t<!-- Custom Fonts -->\n\t<link href=\"https://cdn.bootcss.com/font-awesome/4.5.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n\n\t<!-- google analytics -->\n\t<script>\n\t\t(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n\t\t\t\t\t(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\n\t\t\t\tm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n\t\t})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');\n\n\t\tga('create', 'UA-78065426-2', 'auto');\n\t\tga('send', 'pageview');\n\t</script>\n</head>\n<body vocab=\"http://schema.org/\" typeof=\"Blog\">\n\n<!--vue-ssr-outlet-->\n\n<!-- disqus count js lib -->\n<script id=\"dsq-count-scr\" src=\"//discipled.disqus.com/count.js\" async></script>\n<!-- Custom Theme JavaScript -->\n</body>\n</html>\n"
  },
  {
    "path": "src/manifest.json",
    "content": "{\n  \"dir\": \"ltr\",\n  \"lang\": \"en\",\n  \"name\": \"D.D Blog\",\n  \"scope\": \"/\",\n  \"display\": \"standalone\",\n  \"start_url\": \"/\",\n  \"short_name\": \"D.D Blog\",\n  \"theme_color\": \"transparent\",\n  \"description\": \"Share More, Gain More. - D.D Blog\",\n  \"orientation\": \"any\",\n  \"background_color\": \"transparent\",\n  \"related_applications\": [],\n  \"prefer_related_applications\": false,\n  \"icons\": [\n    {\n      \"src\": \"assets/img/logo/size-32.png\",\n      \"sizes\": \"32x32\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-48.png\",\n      \"sizes\": \"48x48\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-72.png\",\n      \"sizes\": \"72x72\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-96.png\",\n      \"sizes\": \"96x96\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-144.png\",\n      \"sizes\": \"144x144\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-168.png\",\n      \"sizes\": \"168x168\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/img/logo/size-192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"gcm_sender_id\": \"445598565171\",\n  \"applicationServerKey\":\n    \"AAAAZ7--gzM:APA91bH2YhYvmlO-NVzzz2_Ya0a4Gc2WoGwykLvf0bZ72RrLUogJwW01d1NZLyftRpCjvguJcRRn_FBeFwiwJQt6gLxYxeNkpekTn9wXL1qWWMrWV8-L_KMG05FveDy0zZ86MuCUNwnD\"\n}\n"
  },
  {
    "path": "src/server/common/DataService.ts",
    "content": "/**\n * Created by jack on 16-8-22.\n */\n\nimport fs = require('fs');\nimport { promisify } from 'util';\nimport marked = require('marked');\n\ninterface IFileOptions {\n\tencoding?: string;\n}\n\ntype TSortFunc = (params: any) => void;\ntype TSortKey = TSortFunc | string;\n\nconst readFilePromisify = promisify(fs.readFile);\nconst writeFilePromisify = promisify(fs.writeFile);\n\nconst readFile = (path: string, options?: IFileOptions) => readFilePromisify(path, options) as Promise<Buffer>;\nconst writeFile = (path: string, data: any, options?: IFileOptions) => writeFilePromisify(path, data, options);\n\nconst readMarkdownFile = (path: string, encoding: IFileOptions = { encoding: 'utf8' }) =>\n\treadFile(path, encoding).then((data: Buffer) => marked(data.toString()));\n\nconst sortFn = (key: TSortKey, order: number = 1) => (curr: any, next: any) =>\n\t(typeof key === 'function' ? key(curr) > key(next) : curr[key] > next[key]) ? +order : -order;\n\ninterface IObject {\n\tid: string | number;\n}\n\nconst normalize = <T extends IObject>(data: T[]): { [key: string]: T } => Array.isArray(data)\n\t? data.reduce((prev, curr) => ({...prev, [curr.id]: curr}), {}) : data;\n\nexport { IFileOptions, readFile, writeFile, readMarkdownFile, sortFn, normalize };\n"
  },
  {
    "path": "src/server/config.ts",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 10/03/2017\n */\n/* tslint:disable */\nexport const gcmAPIKey = `AAAAZ7--gzM:APA91bH2YhYvmlO-NVzzz2_Ya0a4Gc2WoGwykLvf0bZ72RrLUogJwW01d1NZLyftRpCjvguJcRRn_FBeFwiwJQt6gLxYxeNkpekTn9wXL1qWWMrWV8-L_KMG05FveDy0zZ86MuCUNwnD`;\n"
  },
  {
    "path": "src/server/data/index.ts",
    "content": "/**\n * Created by jack on 16-4-26.\n */\n\nimport path = require('path');\nimport * as DataService from '../common/DataService';\n\nimport Post, { IPostBase } from '../../types/post';\nimport Tag, { ITagBase } from '../../types/tag';\nimport POSTS from './posts';\nimport TAGS from './tags';\n\ninterface IData {\n\tposts: {\n\t\t[key: string]: Post,\n\t};\n\ttags: {\n\t\t[key: string]: Tag,\n\t};\n}\n\nconst POST_DICTIONARY = path.join(__dirname, '/posts/');\n\nconst Data: IData = {\n\tposts: {},\n\ttags: DataService.normalize(TAGS.map((tag: ITagBase, index: number) => new Tag({ ...tag, id: index }))),\n};\n\n// read .md file\nPromise.all(POSTS.map((post: IPostBase) => DataService.readMarkdownFile(POST_DICTIONARY + post.name + '.md')))\n\t.then((postContentList: string[]) =>\n\t\tPOSTS.map((config, index) =>\n\t\t\tnew Post({\n\t\t\t\t...config,\n\t\t\t\tid: index,\n\t\t\t\tcontent: postContentList[index],\n\t\t\t})))\n\t.then(DataService.normalize)\n\t.then((posts) => Object.assign(Data.posts, posts))\n\t.catch(console.error);\n\nexport default Data;\n"
  },
  {
    "path": "src/server/data/posts/angular-provide.md",
    "content": "使用 `Angular` 开发项目已经有了不短的时间，在最近搭建一个项目的前端时遇到了**问题**。\n\n随着项目的增大，通过 `angular-ui` 处理的路由配置的不断增加，使得 module.config 的内容不断膨胀，这时通常的做法是将所有的 router 配置抽出到一个文件中去统一配置，而 module.config 中只需做一个简单的路由 mapping，这样既方便代码的维护，又增加了代码的易读性。每当想建这样一个跨 `scope` 的单例数据源或者一个服务时同城就会很直接想到建一个 `factory` 或 `service` 去处理，于是我也建立了一个这样的 factory 来作为单例数据源通过 angular 注入的方式注入到 module.config 中，然而问题出现了，页面直接出现如下错误。\n\n![页面错误](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/angular-provide/inject-error.png)\n\n我再三仔细查看代码，发现语法上没有任何错误，最后从错误提示上的 Unknown provider 想起——**在 module.config 中只能注入 provider，而不能注入 service 或 factory**。\n\n在重新查阅 API 文档和一些其他资料之后，我对 Angular $provide 有了全新的认识，纠正了我一些原有的错误想法。\n\n首先，所有 Angular 的服务都是**单例**，这里的服务不单单指我前面提到的 `provider`, `service` 和 `factory`，还包括另外3个以前我并不知道的 `constant``, `value` 和 `decorator``。\n\n然后再分别看看这6个方法的不同之处：\n\n1. `provider`：`provider` 是一个构造器用来返回一个服务实例。需要注意的是，`provider` 的参数可以是一个构造函数也可以是对象，如果是一个对象，那这个对象必须提供 `$get` 属性，当 `provider` 被注入时调用 `$get` 属性返回所需要的实例；还有一点是，**在使用 `provider` 注入时，需在你定义的 `provider` 名后添加 Provider 后缀**，即 module.provider(**'listen'**, function(){})，在注入时就需要使用 xxService.$inject = [**'listenProvider'**];\n2. `factory`: `factory` 就是通过 `provider` 第一个参数为对象的方法实现，`factory` 底层通过调用 $provide.provider(name, {$get: $getFn})，而 $getFn 就是自定义 `factory` 的参数，即 `factory` 所传的方法需返回一个对象，这个对象会绑定到 `provider` 的 `$get` 属性上。\n3. `service`: `service` 也是对 `provider` 的一种封装，`service` 的第二个参数是一个构造函数，当service被注入时，会通过 `provider` 来返回一个服务实例。\n4. `value` & `constant`：`value` 和 `constant` 两个方法的参数可以是任意的类型，当它被注入时返回一个包裹了这个值的服务。两者的不同之处在于，**`constant` 可以在 module.config 里被注入，而 `value` 不能，与此同时，`constant` 的值是常量不能修改也无法被 `decorator` 装饰**。\n5. `decorator`：即装饰器，用于在 `service` 创建时对 `service` 进行重写或修改。\n\n显然，使用 `constant` 服务来建立这个配置信息来解决之前提到的问题是最恰当的。"
  },
  {
    "path": "src/server/data/posts/angular1.5-with-ES6-styleguide.md",
    "content": "说到关于 Angular Styleguide，很多人可能会想到[这篇](https://github.com/johnpapa/angular-styleguide/tree/master/a1)经典的文章。的确，它是一篇非常棒的文章，甚至已经被翻译成许多种语言（包括[中文](https://github.com/johnpapa/angular-styleguide/blob/master/a1/i18n/zh-CN.md)），在 github 上更是拥有将近 1.9w 个 star。\n\n然而，这次谈论的不是它。因为随着 ES6 的广泛应用，以及 Angular 1.5 的发布，它有那么一点点不够时髦（也谈不上过时哈~）。\n\n本文的大部分观点都来自这篇[文章](https://github.com/toddmotto/angular-styleguide)（以下简称原文），但个人根据工作上积累的一些经验添并不是完全认同原文的所有想法，并想去除些繁冗的例子，于是就没有直接翻译原文。\n\n言归正传，下面就来看看使用 ES6 来编写基于 Angular 1.5 的代码有哪些最佳实践。\n\n### 模块架构\n在 Angular 体系中，所有代码都是基于模块的，它来封装模块内部的逻辑、模板、路由和子模块。\n\n#### 模块划分\n原文将模块分为 3 大类，分别是：root, component 和 common，并创建相应的文件夹来储存。  \n\n* root：根模块组件，用来启动应用和相应模板\n* component：包含所有可重用的模块，模块中可以包含 components, controllers, services, directives, filters and tests\n* common：包含所有业务的模块（即不可重用，和 component 最大的区别），它可以是页面布局、导航和页脚等等。\n\n[原文](https://github.com/toddmotto/angular-styleguide#root-module)中有详细的例子，但就如文章开头所说，在这里就不贴了。\n\n但是，我并不完全认同原文观点。\n\n因为，common 的翻译是公共的，在 common 中存放业务代码也和我们一直以来的做法相悖；其次是，在 Angular 的开发过程中，还是存在一些可以在业务逻辑中公用的代码，比如 service 和 filter。所以，我更倾向于将它分为 4 部分，分别是 root, app, component 和 common。\n\n* root：和原文的作法一样，依旧是用来启动应用，并包含了应用的模板（并不一定要一个文件夹，可以是根目录下的一个 app.js 文件）\n* app：类似于之前的 common 模块，包含所有的业务模块组件\n* component：同原文的一样，包含所有可重用的模块组件\n* common：公用代码模块，包含可公用的代码，如 service 和 filter\n\n![附一张项目中的代码结构图](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/angular1.5-with-ES6-styleguide/module-file-structure.jpg)\n\n#### 模块导出\n使用 ES6 肯定会使用强大的模块语法，在同 Angular 一同使用时，一定要注意导出的是模块的名字，而非是 Angular 的模块对象，这样才能再另一处被其他模块注入。\n\n```Javascript\n// 精简了原文的代码，去除了一些和这节无关的代码\nimport angular from 'angular';\nimport CalendarComponent from './calendar.component';\n\nconst calendar = angular\n  .module('calendar', [])\n  .component('calendar', CalendarComponent)\n  .name;\n\nexport default calendar;\n```\n\n#### 文件命名\n首先，为每个模块添加 `index.js` 文件来定义整个模块，这样再别的模块中可以通过文件夹直接引入。\n\n原文使用`模块名.文件内容.文件类型`的方式来命名一个文件，如 calendar.controller.js 等。\n\n我完全同意第一个观点，但第二个中的模块名就没有添加的必要，因为文件夹名已经很好的体现了模块名这个含义。\n\n![再附一张项目中的模块结构图](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/angular1.5-with-ES6-styleguide/component-file-structure.jpg)\n\n### 组件(Component)\n组件是 Angular 1.5 新提出的，是一种特殊的指令，Augular 的源码中也彰显了这一点。\n\n它相比指令更多的是数据的单向绑定和生命周期钩子，尽管我认为所谓的生命周期钩子只是语法糖，甚至组件它本身就是个语法糖，但这不妨碍它成为 Angular 体系中重要的一部分。因为，它的推出明确的区分了指令和组件，解决了原先指令划分不清、承担过多工作的问题。\n\n#### 组件属性\nProperty | Support \n--- | ---\nbindings | Yes, 只使用 `@`, `<`, `&`，避免使用 `=`\ncontroller | Yes\ncontrollerAs | Yes, 默认为 `$ctrl`\nrequire | Yes\ntemplate | Yes\ntemplateUrl |Yes\ntransclude | Yes\n\n#### 控制器(controller)\n控制器只应在组件中使用，如果你只想创建一个控制器，那你应创建一个无状态组件来管理它。\n\n使用 `class` 关键字来创建控制器时要注意以下几点：\n\n* 使用 `constructor` 处理依赖注入\n* 之前提到过，导出模型名，而并不是直接导出模型\n* 使用箭头函数\n* 使用 `$onInit`, `$onChanges`, `$postLink` 和 `$onDestroy` 生命周期  \n\t（注意：`$onChanges` 会在 `$onInit`之前被调用）\n* 使用默认的控制器 `$ctrl`，不使用 `controllerAs` 修改控制器的别名\n\n#### 单向数据流\n* 总是使用 `<` 单向数据绑定来代替 `=` 双向数据绑定\n* 使用 `$onChanges` 来监听数据的变化\n* 父组件的方法使用 `$event` 作为参数传递的名字\n* 子组件调用时返回一个包含有 `$event` 属性的对象\n\n这是不是看上去很像 [Redux](http://redux.js.org/)？没错，原文的作者也是推荐使用 [Angular Redux](https://github.com/angular-redux/ng-redux) 来管理状态。\n\n#### 状态组件(Stateful components)和无状态组件(Stateless components)\n状态组件和无状态组件其实分别对应了 [Redux](http://redux.js.org/docs/basics/UsageWithReact.html) 中的容器组件（Smart/Container Components）和展示组件（Dumb/Presentational Components），这部分原作者主要也是表达了在 Angular 中实现单向数据流的理念，但原作者提供的例子并不是完整的 Redux，它没有单一的 Store 和 Reducer。\n\n### 指令（Directive）\n相信指令大家都很熟悉了，但自从 Angular 1.5 提供了组件，指令的选择就应当慎重考虑，它应当只在装饰 DOM 时使用。\n\n* 不使用 `template`, `templateUrl`, `scope`, `bindToController` 或 `controller` 等相关的属性，如果想用，考虑是不是它可以用 `component` 来实现\n* 总是使用 `restrict: 'A'`\n\n#### 指令属性\nProperty | 是否使用 | Why\n--- | --- | ---\nbindToController | No | 使用组件替代\ncompile | Yes | DOM 操作/事件的预处理\ncontroller | No | 使用组件替代\ncontrollerAs | No | 使用组件替代\nlink functions | Yes | DOM 操作/事件的处理\nmultiElement | Yes | [See docs](https://docs.angularjs.org/api/ng/service/$compile#-multielement-)\npriority | Yes | [See docs](https://docs.angularjs.org/api/ng/service/$compile#-priority-)\nrequire | No | 使用组件替代\nrestrict | Yes| 总是使用 `restrict: 'A'`\nscope | No | 使用组件替代\ntemplate | No | 使用组件替代\ntemplateNamespace | Yes (如果必须) | [See docs](https://docs.angularjs.org/api/ng/service/$compile#-templatenamespace-)\ntemplateUrl | No | 使用组件替代\ntransclude | No | 使用组件替代\n\n### 服务（Service）\n服务主要用于封装一些不应在组件中处理的业务逻辑和请求。\n\nAngular 提供 2 种创建服务的方式 `service` 和 `factory`。在 ES6 引入了 `class` 关键字后，它能非常友好地同 `service`一起工作，所以，无论何时都使用 `service` 来创建服务。\n\n### 类 or 方法\n原文的标题是[常量或类（Constants or Classes）](https://github.com/toddmotto/angular-styleguide#constants-or-classes)，容许我自作主张的修改一下标题，因为我认为原文的实现的区别更主要的在于是使用**类或方法**去定义一个服务或控制器等。\n\n当然这两种方法都可以，因为类它本身就是方法的一个语法糖。但是，Angular 2 是重度依赖 `class` 关键字的，所以，我认为还是全部统一使用 `class` 关键字来声明服务、控制器、过滤器、指令和组件的定义等。\n\n值得注意的是，Angular 组件和指令定义的参数是一个对象，所以在使用 `class` 定义时，要手动实例化它。\n\n### 工具\n最后，原文作者还推荐了一些工具\n\n* [Babel](https://babeljs.io/)：编译工具，这就不多说了，必备神器\n* [TypeScript](http://www.typescriptlang.org/)：还是为了 A2\n* [Webpack](https://webpack.github.io/)：打包工具，用过都说好\n* [ngAnnotate](https://github.com/olov/ng-annotate)：自动依赖注入，和打包工具一起服用效果更好\n* [Angular Redux](https://github.com/angular-redux/ng-redux)：状态管理\n\n以上为个人观点，欢迎交流。"
  },
  {
    "path": "src/server/data/posts/apologize-letter.md",
    "content": "#### 亲爱的读者，\n\n今天下午，由于不明人士的请求，本站发出了大量无意义的推送，对此对各位的生活或工作等所带来的不便深表歉意。本人将立即下线推送功能，并在近期完成校验工作后再次上线，希望各位读者继续订阅本站。\n\n同时，还是要感谢这位捣蛋的不明人士，他让我知道我的小站还是有读者的，提醒我要坚持写下去。从去年底开始，因为工作以及私人的一些关系已经有一段时间没有发布新文章了，今年本人将继续以一个月一篇以上的频率来更新，欢迎大家监督。\n\n最后，还是想吐槽一下这位不明人士，代码都发 [Github](https://github.com/DiscipleD/blog) 了，相信你也研究了半天了（不然，也不会找到推送的地址），为啥不自己搭个服务器捣鼓着玩哪~\n\n谢谢\n"
  },
  {
    "path": "src/server/data/posts/autoprefixer.md",
    "content": "众所周知为兼容所有浏览器，有的 CSS 属性需要对不同的浏览器加上前缀，然而有时添加一条属性，需要添加 3~4 条类似的属性只是为了满足浏览器的兼容，这不仅会增加许多的工作量，还会使得你的思路被打断。\n\n如何解决这个问题？最近写项目时，就发现了一个处理 CSS 前缀问题的神器——**AutoPrefixer**。\n\n![AutoPrefixer](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/autoprefixer/autoprefixer.png)\n\n### What is AutoPrefixer\nAutoprefixer 是一个后处理程序，它可以同 Sass，Stylus 或 LESS 等预处理器共通使用。它适用于普通的 CSS，而你无需关心要为哪些浏览器加前缀，只需关注于使用 W3C 最新的规范。\n\n### How to use AutoPrefixer\n介绍了这么多，如果用起来很麻烦，那还不如直接手写，而 **AutoPrefixer** 的另一大特点就是使用简便，现在来说说怎么用。\n\n**AutoPrefixer** 可以简单的通过下载 plugin 配置到 `Sublime`，`Brackets` 或 `Atom` 等 IDE 里，而在 `WebStorm` 中无法通过 plugin 直接安装和使用 AutoPrefixer，需要通过 External Tools 或 File Watchers 来实现，在 `WebStorm` 中详细的安装方法可以参考[这篇文章](http://www.css88.com/archives/5670)。\n\n如果单单只能通过 IDE 才能使用这个功能，那它远称不上神器，真正让其拥有神器之名的原因是：它可以很简单、有效地同现有的打包工具（`gulp`, `webpack` 等）一同使用，来完成对项目中所有的 `css` 文件中的属性添加前缀。\n\n下面，我们就分别来看在这两种打包工具下如何使用 **AutoPrefixer**。\n\n* gulp\n\n在 `gulp` 中，可以使用 [AutoPrefixer官网](https://github.com/postcss/autoprefixer) 推荐的 `postcss` + `autoprefixer` 两个插件的组合，也可以通过 `gulp-autoprefixer` 这一个插件。\n```JavaScript\n// Method 1: postcss + autoprefixer\ngulp.task('autoprefixer', function () {\n    var postcss = require('gulp-postcss');\n    var sourcemaps = require('gulp-sourcemaps');\n    var autoprefixer = require('autoprefixer');\n\n    return gulp.src('./src/*.css')\n      .pipe(sourcemaps.init())\n      .pipe(postcss([ autoprefixer({ browsers: ['last 2 versions'] }) ]))\n      .pipe(sourcemaps.write('.'))\n      .pipe(gulp.dest('./dest'));\n});\n\n// Method 2: gulp-autoprefixer\ngulp.task('autoprefixer', function () {\n    var autoprefixer = require('gulp-autoprefixer');\n\n    return gulp.src('./src/*.css')\n      .pipe([ autoprefixer({ browsers: ['last 2 versions'] }) ])\n      .pipe(gulp.dest('./dest'));\n});\n```\n* Webpack\n\n而在最近很火的 `webpack` 中使用 **AutoPrefixer** 更是轻而易举、如虎添翼。\n使用 `webpack` 可以通过简单的配置将本文开头提到的 sass 这样的预处理器同 `autoprefixer` 这样的后处理程序结合在一起。\n\n```javascript\nvar autoprefixer = require('autoprefixer');\nmodule.exports = {\n    module: {\n      loaders: [\n        { test: /\\.css$/, loader: \"style!css!postcss\" },\n        { test: /\\.scss$/, loader: \"style!css!postcss!sass\" }\n      ]\n    },\n    postcss: [ autoprefixer({ browsers: ['last 2 versions'] })\n]}\n```\n\n注： 另外 `webpack` 还有一个 `autoprefixer-loader`，但 npm 官网已将其标为【deprecated】，推荐使用上面示例中通过 `postcss-loader` 的方式使用 `autoprefixer`。"
  },
  {
    "path": "src/server/data/posts/browsersync.md",
    "content": "随着前端技术的飞速发展，前端的工程化构建工具也随着这股浪潮不断更迭，从 grunt 到 gulp，而 ant 已经淹没在了潮流之中。然而，不单单是构建工具变化飞快，连构建工具的插件变化也是日新月异，最近项目使用 gulp 构建的过程中就尝试使用了 **Browsersync** 这个插件来替代 gulp-livereload。\n\n### Why Browsersync？\n\n首先，既然它能替代 gulp-livereload，那么它就能实现 gulp-livereload 的主要功能：实时刷新——当你在 IDE 编辑文件保存时，插件会自动应用你的修改并自动刷新浏览器页面，其中文件不单包括 html, js, css，还包括 sass, less 等类型的文件。\n\n其次，如果 Browsersync 只是单单实现 gulp-livereload 的功能，那它不值一书。它当然还有其他优势，**Browsersync 可以同时在 PC、平板、手机 等设备下进项调试**，这就意味着任何一次改动都会实时地应用到这些设备中，这将大大提升多设备开发的效率。\n\n![官网示例1](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/browsersync/browsersync-in-different-browser.gif)\n\n还不仅如此，它还能**在不同的浏览器不同的设备上同步所有页面上的操作**，这绝对是多浏览器兼容性测试的福音啊！\n\n![官网示例2](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/browsersync/browsersync-in-different-divice.gif)\n\nAmazing？\n\n### How to use Browsersync?\n\n想要实现这些神奇的效果配置起来相当便捷。\n\n1. 安装 Node.js(https://nodejs.org/en/)\n2. 项目中添加 Browsersync 依赖（package.json推荐）或安装 Browsersync\n3. 在 gulpfile.js 中配置\n\n```JavaScript\nvar gulp = require('gulp');\nvar browserSync = require('browser-sync').create();\n// 静态服务器\ngulp.task('browser-sync',function(){\n    browserSync.init({\n      server: {\n        baseDir:\"./\"\n      }\n    });\n});\n// 代理\ngulp.task('browser-sync', function() {\n    browserSync.init({\n     proxy: \"你的域名或IP\"\n   });\n});\n// 静态服务器（server)和代理（proxy）模式不能同时使用\n```\n\n这样就简单地启动了服务器，而要实现同步刷新就要通过 gulp watch 来调用 Browsersync 的 reload 方法。\n\n```JavaScript\n// 打包js\ngulp.task('js', function () {\n    return gulp.src('app/js/*.js')\n      .pipe(browserify())\n      .pipe(uglify())\n      .pipe(gulp.dest('dist/js'));\n});\n// 确保js文件打包完成后，再调用reload方法\ngulp.task('js-watch', ['js'], browserSync.reload);\ngulp.task('browser-sync',function(){\n    browserSync.init({\n      server: {\n        baseDir:\"./\"\n      }\n    });\n    // 当js目录下js文件发生变化时调用browserSync.reload\n    gulp.watch(\"app/js/*.js\", ['js-watch']);\n});\n```\n\n应用 js file 需要重新刷新页面，而应用 CSS 样式并不用重新加载页面。从示例图1就可以看到，当我们修改 CSS file 的时候页面及时响应了这些修改而并没有刷新页面，因为 Browsersync 可以通过配置将修改后的 CSS 文件直接注入到浏览器中。\n\n```JavaScript\nvar sass = require('gulp-sass');\n// scss编译后的css将注入到浏览器里实现更新\ngulp.task('sass', function() {\n    return gulp.src(\"app/scss/*.scss\")\n      .pipe(sass().on('error', sass.logError))\n      .pipe(gulp.dest(\"app/css\"))\n      .pipe(browserSync.stream()); // stream method returns a transform stream\n});\n// 修改上面的browser-sync task\ngulp.task('browser-sync',function(){\n    browserSync.init({\n      server: {\n        baseDir:\"./\"\n      }\n    });\n    // 当js目录下js文件发生变化时调用browserSync.reload\n    gulp.watch(\"app/js/*.js\", ['js-watch']);\n    // 当scss目录下scss文件发生变化时调用sass task\n    gulp.watch(\"app/scss/*.scss\", ['sass']);\n});\n```\n项目中，开发时常前端和后端分离，而当各自接口开发完成后，进行联调测试时，前端会因为跨域问题无法请求到后台的数据，跨域当然可以通过现有的一些解决方案，如 CORS 等，但用 Browsersync 可以通过设置 proxy 的方式，简单的解决跨域问题而不需要修改业务代码。\n\n```JavaScript\n// 修改上面的browser-sync task\ngulp.task('browser-sync', function () {\n    browserSync.init({\n      proxy: \"http://172.18.2.30\", //后端服务器地址\n      serveStatic: ['./'] // 本地文件目录，proxy同server不能同时配置，需改用serveStatic代替\n    });\n    // 当js目录下js文件发生变化时调用browserSync.reload\n    gulp.watch(\"app/js/*.js\", ['js-watch']);\n    // 当scss目录下scss文件发生变化时调用sass task\n    gulp.watch(\"app/scss/*.scss\", ['sass']);\n});\n```"
  },
  {
    "path": "src/server/data/posts/ci-solution.md",
    "content": "前段时间读到一篇优秀的文章[《前端开源项目持续集成三剑客》](http://efe.baidu.com/blog/front-end-continuous-integration-tools/)，就想试着运用到自己的项目中去。（好吧，老实说，我只是个徽章收集爱好者。）\n\n## 持续集成\n持续集成，这个概念对后端来说应该并不陌生，甚至可以说是司空见惯吧。但是，这对曾经（除了那些大厂）单元测试都不一定要写的前端来说，或许是个陌生的词。\n\n然而，随着前端飞速地发展，不断吸取后端长久以来积累的经验，以及前端对单元测试越来越重视，持续集成作为前端工程化中的一项也渐渐进入人们的视野。\n\n那么，持续集成究竟是什么？\n\n> 持续集成（英语：Continuous integration，缩写为 CI），一种软件工程流程，将所有工程师对于软件的工作复本，每天集成数次到共用主线（mainline）上。 —— [wikipedia](https://zh.wikipedia.org/wiki/%E6%8C%81%E7%BA%8C%E6%95%B4%E5%90%88)\n\n简单来说，就是以一定的频率将代码整合到一起。\n\n使用持续集成能使项目：\n\n* 保持可测试和可发布的状态\n* 易于追踪错误，当集成产生错误时，能将错误产生的缩小范围到上次成功集成之后的提交\n* 版本回滚也变得轻而易举\n\n## Travis-CI vs CircleCI\n在[《前端开源项目持续集成三剑客》](http://efe.baidu.com/blog/front-end-continuous-integration-tools/)中，作者推荐了 2 个集成工具，分别是：[travis-ci](https://travis-ci.org/) 和 [circleci](https://circleci.com/)。\n\n额...该选哪个哪？\n\n![选择困难啊~](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/hard-to-choice.jpeg)\n\n分别粗略地了解了这两个产品，它俩的网站的都非常简洁，文档也很清晰，功能上也大致相同。虽然，circleci 比 travis-ci 多了 Bitbucket 源码库的支持，但是，有一大硬伤 circleci 只对**一个** container 免费，而且，若使用 OS X 需要**额外收费**。与之相反，travis-ci 只要是 Github 上的开源项目**全部免费**，且支持在 OS X 运行。\n\n![决定是你了](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/choose-you.png)\n\nTravis-ci。\n\n注册 travis 只需一步，点击 Sign In 按钮绑定 Github。登录后，执行 travis 只需以下 3 步：\n\n1. 添加需要 travis 管理的项目\n2. 为项目添加 .travis.yml 配置文件\n3. 提交代码\n\n与此同时，travis 的配置也极其简单。如果没有什么特别的需求，那么，只需配置运行语言类型及其版本就行。\n\n```yml\n// .travis.yml\nlanguage: node_js\nnode_js:\n  - \"6\"\n```\n\n这样，一个简单、可用的 travis 配置就完成了。\n\nTravis 构建过程主要分为两步：\n\n* install：安装依赖，在 node 环境下，默认运行 npm install\n* stript：运行构建命令，在 node 环境下，默认运行 npm test\n\n那么，上面的代码就等价于：\n\n```yml\nlanguage: node_js\nnode_js:\n  - \"6\"\ninstall: npm install\nscript: npm test\n```\n\n当然，travis 不止这两个生命周期，额外的配置需求都可以到官网[查看](https://docs.travis-ci.com/user/customizing-the-build/)。\n\nOK。提交代码试试吧。\n\ntravis 的运行信息都可以在 Job log 中看到。\n\n如果运行成功，你就可以通过 https://img.shields.io/travis/USER/REPO.svg 或 https://img.shields.io/travis/USER/REPO/BRANCH.svg 来给你的项目添加 badge 了，就像这样 [![Build Status](https://img.shields.io/travis/DiscipleD/react-redux-antd-starter.svg)](https://travis-ci.org/DiscipleD/react-redux-antd-starter)。\n\nTips：其中的 USER, REPO, BRANCH 都要替换成个人信息。\n\n## Codecov vs Coveralls\n有了构建的徽章，接着再弄一个测试覆盖率的徽章。三剑客文章中用的是 coveralls，但进入它的[官网](https://coveralls.io)发现，它和当今网站那种简洁风格不同，画风有点 classic 啊~文档也不太详细，比较简单，就查了下有没有其他更好的？\n\n于是，发现了 [codecov](https://codecov.io)。\n\n> 干净、免费，我喜欢。\n\n[文档](http://docs.codecov.io/docs)也相对于 [coveralls](https://coveralls.zendesk.com/hc/en-us) 更清晰、详细。在尝试之后，更是觉得我的选择是明智的。^_^\n\ncodecov 的使用相当简单，甚至不用看文档就可以轻易配置。\n\n首先，登录[首页](https://codecov.io)，根据自己源码的存储位置选择相应的登录按钮，这里我选择 Github，第一次登录会需要你的授权。\n\n授权成功之后，就能看到类似下面的图，分别对应你的个人账户以及你所加入的组织。\n\n![codecov dashboard](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/codecov-dashboard.png)\n\n第一次使用时，默认是没有 repository 的，需要通过点击 `+ Add my first repository` 来添加需要 codecov 管理的 repository。\n\n选择相应的 repository 之后，你可以看到一个类似下面的页面。当然，数据什么肯定是没有的。\n\n![codecov repository detail](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/codecov-repository-detail.png)\n\n前几个 tab 是用来展示信息的，在配置完成并运行之前是没有信息的，配置的时候只需要看最后一个 setting tab。\n\n![codecov repository setting](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/codecov-setting.png)\n\n切换左侧的菜单，就能分别看到 setting 和 badge 的信息，是不是超级赞？\n\n无论 codecov 还是 coveralls，它自身都不会去运行测试用例来获得项目代码的覆盖率，而是通过收集覆盖率报告及其他关键信息来静态分析。\n\ncodecov 可以接收 lcov, gcov 以及正确的 json 数据格式作为输入信息。\n\n于是，如果你使用 JEST 作为测试框架，并开启测试覆盖率（collectCoverage），由于，JEST 使用 istanbul 生成覆盖率报告，即 lcov。那么，上传报告就异常简单了。只需安装 codecov\n\n```bash\nnpm install codecov --save-dev\n```\n\n然后，在 CI 执行之后，上传报告就行。比如，像这样\n\n```yml\nlanguage: node_js\nnode_js:\n  - \"6\"\ncache:\n  directories: node_modules\nscript:\n  - npm run test:coverage\n  # 这里我没有全局安装 codecov，所以要通过 npm 来运行 codecov\n  - npm run codecov\nos:\n  - linux\n  - osx\n```\n\n这次的 badge 如何获取上面有写到，这里就不再展示了。\n\n## SAUCELABS vs BrowserStack\n跨浏览器测试同样有 2 个选择，这次我同三剑客的作者站在了同一战线，选择使用 [SAUCELABS](https://saucelabs.com/)。\n\n> SAUCELABS 开源免费账号注册方式隐藏得比较好，找不到的可以点[这里](https://saucelabs.com/beta/signup/OSS/None)。\n\n不过，由于 JEST 不支持 end-to-end 测试，所以，为了做跨浏览器测试我们不得不寻求其他的测试框架来帮助完成这一工作。这里我并不打算使用 [karma](https://karma-runner.github.io/1.0/index.html)，即使是 karma 同 SAUCELABS 有现成的集成插件 [karma-sauce-launcher](https://github.com/karma-runner/karma-sauce-launcher) 可以使用。\n\n不要问我为什么，就是这么任(jue)性(jiang)。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/not-ask-me-why.jpg)\n\n你真不问么？那我就说了吧。因为现有的测试框架 JEST 已经可以完成 karma 的大部分工作，单纯为 end-to-end 测试单独引入 karma 就没有必要了。\n\n经过一番资料收集和比较之后，我选择 [Nightwatch](http://nightwatchjs.org/) 来解决跨浏览器测试的问题。\n\n> What's Nightwatch?\n> \n> Nightwatch.js is an automated testing framework for web applications and websites, written in Node.js and using the W3C WebDriver API (formerly Selenium WebDriver).\n> \n> It is a complete browser (End-to-End) testing solution which aims to simplify the process of setting up Continuous Integration and writing automated tests. \n\n可以从官网的介绍中看到，Nightwatch 对我们当前想解决的问题简直是正中下怀啊！(如果你的项目使用的是 Angular，那么，你也可以试试 [Protractor](http://www.protractortest.org/#/))\n\n在查资料时，发现 nightwatch 的第一个 [issue](https://github.com/nightwatchjs/nightwatch/issues/1) 竟然是[尤大大](https://github.com/yyx990803)提的。\n\n> 走得越远，越是发现一路都是大大们留下的足迹。\n\n膜拜大大。\n\n回到正题，使用 nightwatch 建立 e2e 测试也是相当容易的，这里就简要说一下流程。\n\n首先，使用 npm 进行安装，这就不多说了。  \n然后，在根目录下添加配置文件，可以是 nightwatch.conf.js，也可以是 nightwatch.json。  \n接着，写对应的测试，API 参考[官网](http://nightwatchjs.org/api)。  \n最后，跑测试命令就好了。\n\n主要是来看看，怎么将 nightwatch 的测试同 saucelabs 以及 travis-ci 整合到一起。先看看测试文件。\n\n```JavaScript\n// nightwatch.conf.js\nmodule.exports = {\n\tsrc_folders: ['tests/e2e'], // 测试文件目录\n\toutput_folder: 'tests/reports', // 测试报告地址\n\tcustom_commands_path: 'tests/saucelabs', // 自定义命令，这里用来更新测试信息到 saucelabs\n\tcustom_assertions_path: '',\n\tpage_objects_path: '',\n\tglobals_path: '',\n\n\ttest_workers: {\n\t\tenabled: true,\n\t\tworkers: 'auto'\n\t},\n\n\ttest_settings: {\n\t\tdefault: {\n\t\t\tlaunch_url: 'http://localhost:8080', // 目标地址，用于测试中读取\n\t\t\tselenium_port: 4445, // selenium server 的端口(selenium server 由 saucelabs 提供)\n\t\t\tselenium_host: 'localhost', // selenium server 的地址(selenium server 由 saucelabs 提供)\n\t\t\tusername: process.env.SAUCE_USERNAME,\n\t\t\taccess_key: process.env.SAUCE_ACCESS_KEY,\n\t\t\tsilent: true,\n\t\t\tscreenshots: {\n\t\t\t\tenabled: false,\n\t\t\t\tpath: ''\n\t\t\t},\n\t\t\tglobals: {\n\t\t\t\twaitForConditionTimeout: 15000\n\t\t\t},\n\t\t\t// 以下重要！！！\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbuild: `build-${process.env.TRAVIS_JOB_NUMBER}`,\n\t\t\t\tpublic: 'public',\n\t\t\t\t'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER\n\t\t\t}\n\t\t},\n\n\t\t// 以下是不同环境的配置\n\t\tchrome: {\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbrowserName: 'chrome'\n\t\t\t}\n\t\t},\n\n\t\tfirefox: {\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbrowserName: 'firefox'\n\t\t\t}\n\t\t},\n\n\t\tinternet_explorer_10: {\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbrowserName: 'internet explorer',\n\t\t\t\tversion: '10'\n\t\t\t}\n\t\t},\n\n\t\tinternet_explorer_11: {\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbrowserName: 'internet explorer',\n\t\t\t\tversion: '11'\n\t\t\t}\n\t\t},\n\n\t\tedge: {\n\t\t\tdesiredCapabilities: {\n\t\t\t\tbrowserName: 'MicrosoftEdge'\n\t\t\t}\n\t\t}\n\t}\n};\n```\n\n这里要注意以下几点：（重要！！！这些折磨了我近一周）\n\n* 运行 localhost 测试，要开启 [sauce connect](https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy)\n* 开启 sauce connect 之后，设置运行环境 `selenium_port: 4445`, `selenium_host: 'localhost'`\n\n以上几点是本地测试时需注意的，下面是连通 travis 时需注意的：\n\n* 配置 `'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER`，其中 `process.env.TRAVIS_JOB_NUMBER` 是 travis 运行时的全局变量\n* 配置 `process.env.SAUCE_USERNAME` 和 `process.env.SAUCE_ACCESS_KEY`，后面细讲\n* 配置 `build` 和 `public` 属性，分别用于标识测试和查看权限，这两点对最后生成 browser matrix badge 有用，这两点在[三剑客](http://efe.baidu.com/blog/front-end-continuous-integration-tools/)的文章中也有提到\n\n配置好了 nightwatch 同 saucelabs，再修改下 travis 的配置，将 saucelabs 整合进去。\n\n```yml\n// .travis.yml\nlanguage: node_js\nnode_js:\n- '6'\ncache:\n  directories: node_modules\n# 用于打包，并在 travis 上启动本地服务，用于 e2e test\nbefore_script:\n- npm run build\n- node server.js &\nscript:\n- npm run test:coverage\n- npm run codecov\n- npm run test:e2e\nos:\n- linux\n- osx\nenv:\n  global:\n  - secure: v6CRj4CKMqxEQ9MSYKAkbmrBgIBZvoppICx6JyjQXhexPOVQKBvboCgdL0lOOZdGZ9rEqSMXvud97kBAFYd1sdP/kSwXdUct5BOMIT3a5GLtY5aQfOocBwR6IvmZpO2U+4VhrCwkzdaq2Ehq0fAXF1pkxDj9YkJZmwDNhTdfDGkib+AwDyr4TLQFC1QrD/4vmrULb3NZdW1KadFYjLzVF8FMa2tDSYMFFVymYu5nuCa/Z0dqSfFy8McYwBMzThDkDRHMT/sf4zKDPyxUwN7xGfC6T88xzCEaltN6K7MGMGKvl7Y0p7VjYW/+rO38936kj6xuPU6J7Vh2yKPJhhT2LtM7ucuo0XSpIxCxaKXWeEmYl2KkCMWNHgrWACE//WBFRNx/JQHimw+abr1Zt/3V9QmSEvnB3hHB0NQgJ2nVrVDjk51RSVaiP4sfQ8GVqEwr1+wJqe4wz7fV+jvRB9uUGgGsjsBbZi6ZycoMtOBoJ+miviRCjZvf9sOZKfIDjcuE5vETQcE37d/++yplCG0N83Kx+q67mbWXirfNj2CfXp7pwHTN+n21v1BSicXqQ6+jaNzD/pcN/GTHgZ5A+VkdcjSmEziuQTO035i1nnCB9TQdFeRdGdfo6DAiq8YOfyVkQ1lml6lWqbPqa4QWokRUD2yA/hAIzNWe5BeLF2JFQBc=\n  - secure: S0vWVM74eiAHhk+kqqvym9aIgqaaGyGz9H3rfmEZoG4iuvXjXRaHOOSHxIRVsh5RYXr0PWHAj24fpN5AyUOlu5NQiwACBqmpw9KZBgVekWFshA5uYmpNpCG9w5/UAQa9q2+EcndOCM4lAyuT2wVJ5WfsHRzIA5jUpK1YmUYtuVICTSkumRoEaxfPkwzcGLF7f6aP7mG1YRKeO1F9+RhBfaGN1kYordxIk/fniH8OFB0XiLZ5OIovaAIYFKic0P1wUFwa78jU2fovdObS8JySl2LP19eaLX0MgAFoPB7oLFPxFBN7FCID41TEodDdZtcNnKJT4uQ/iWRqww2BOwVQM9whyBTg8J4kJZALicR4CzGCuUbdyQd2kh/hNZ9d9SKb6YXdcZElFmh3FY6zgfgv5PAx+jDlkfzmgBh7OD5OM4GVrsCsjnaAlmTUNtRPx9B4ps0gbr25F1PxuNy+MXfwSYJdliL+N01BTpiGyts/EXAraWvEm5YkhWfTnbgc8osd3cX9vwB0QHksK+BpkaEs6XCwU6kGMxAJIlafRv6RslREdTPBpYaXB4sGqdYXWY+YFqNxsAwTB3KWIq/uhZmSkou1jZfZa2QonMuVot68U11U7afmPzX8KOVeO2IEcUjt6I4eCYQ+31xO/wSLIQ1uoRySQ2S9VCzr+yzDpu0KVps=\naddons:\n  sauce_connect: true\n```\n\n你肯定会诧异 `global` 下面的那两串长🐜是什么东西。它们其实就是在 nightwatch.conf.js 中用到的 `process.env.SAUCE_USERNAME` 和 `process.env.SAUCE_ACCESS_KEY`。\n\n那它们是怎么来的哪？\n\n首先，安装 travis 工具 `gem install travis`；  \n然后，使用 github 账户登录 `travis login`；  \n登录后，就可以分别使用 `travis encrypt SAUCE_USERNAME=saucelabs用户名 --add`\n和 `travis encrypt SAUCE_ACCESS_KEY=saucelabs的access_key --add`\n 将 username 和 access_key 加密，`--add` 参数会自动将结果追加到 .travis.yml 文件中。所以，已完全不用担心字符贴错或贴漏。 \n\n这样整个跨浏览器测试就同 CI 集成好了，配置信息比较多，有兴趣的可以结合项目一起看。（[点这里](https://github.com/DiscipleD/react-redux-antd-starter/tree/real-world)）\n\n最后，不要忘(tian)了(jia)初(hui)衷(zhang)。这可以在 saucelabs 的 Dashboard -> Automated Builds 下看到。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/saucelabs-badge.png)\n\n总的来说，nigthwatch + saucelabs + travis 来做跨浏览器自动测试还是比较方便的，只是一开始不熟悉，相应的资料也比较少，saucelabs 的文档也不够友好，耗费了些时间。覆盖率测试时， JEST 占的那点小便宜全都还回来了。\n\n## Automatically Publish\n看到这里，你是不是以为 CI 只是帮你跑跑测试、显示覆盖率？那你就错了。\n\nCI 并不是单单只能帮你跑测试，它还可以将构建成功的代码发布到服务器上。试想一下，当你将代码合并到主分支之后，CI 不但帮你运行测试，还将测试通过之后的代码发布到了你的服务器上，而不需要你人工进行额外的操作。这是不是很 cool！\n\n这里就举一个通过 Travis-ci 将代码发布到 github.io 上的例子。\n\n再修改一下上面 .travis.yml 文件。\n\n```yml\nlanguage: node_js\nnode_js:\n- '6'\ncache:\n  directories: node_modules\nbefore_script:\n- npm run build\n- node server.js &\nscript:\n- npm run test:coverage\n- npm run codecov\n- npm run test:e2e\nafter_success:\n- bash ./deploy.sh\nos:\n- linux\n- osx\nenv:\n  global:\n  - USER_NAME: Disciple_D\n  - USER_EMAIL: disciple.ding@gmail.com\n  - GIT_DEPLOY_KEY: XXXXXXXX\n  - secure: v6CRj4CKMqxEQ9MSYKAkbmrBgIBZvoppICx6JyjQXhexPOVQKBvboCgdL0lOOZdGZ9rEqSMXvud97kBAFYd1sdP/kSwXdUct5BOMIT3a5GLtY5aQfOocBwR6IvmZpO2U+4VhrCwkzdaq2Ehq0fAXF1pkxDj9YkJZmwDNhTdfDGkib+AwDyr4TLQFC1QrD/4vmrULb3NZdW1KadFYjLzVF8FMa2tDSYMFFVymYu5nuCa/Z0dqSfFy8McYwBMzThDkDRHMT/sf4zKDPyxUwN7xGfC6T88xzCEaltN6K7MGMGKvl7Y0p7VjYW/+rO38936kj6xuPU6J7Vh2yKPJhhT2LtM7ucuo0XSpIxCxaKXWeEmYl2KkCMWNHgrWACE//WBFRNx/JQHimw+abr1Zt/3V9QmSEvnB3hHB0NQgJ2nVrVDjk51RSVaiP4sfQ8GVqEwr1+wJqe4wz7fV+jvRB9uUGgGsjsBbZi6ZycoMtOBoJ+miviRCjZvf9sOZKfIDjcuE5vETQcE37d/++yplCG0N83Kx+q67mbWXirfNj2CfXp7pwHTN+n21v1BSicXqQ6+jaNzD/pcN/GTHgZ5A+VkdcjSmEziuQTO035i1nnCB9TQdFeRdGdfo6DAiq8YOfyVkQ1lml6lWqbPqa4QWokRUD2yA/hAIzNWe5BeLF2JFQBc=\n  - secure: S0vWVM74eiAHhk+kqqvym9aIgqaaGyGz9H3rfmEZoG4iuvXjXRaHOOSHxIRVsh5RYXr0PWHAj24fpN5AyUOlu5NQiwACBqmpw9KZBgVekWFshA5uYmpNpCG9w5/UAQa9q2+EcndOCM4lAyuT2wVJ5WfsHRzIA5jUpK1YmUYtuVICTSkumRoEaxfPkwzcGLF7f6aP7mG1YRKeO1F9+RhBfaGN1kYordxIk/fniH8OFB0XiLZ5OIovaAIYFKic0P1wUFwa78jU2fovdObS8JySl2LP19eaLX0MgAFoPB7oLFPxFBN7FCID41TEodDdZtcNnKJT4uQ/iWRqww2BOwVQM9whyBTg8J4kJZALicR4CzGCuUbdyQd2kh/hNZ9d9SKb6YXdcZElFmh3FY6zgfgv5PAx+jDlkfzmgBh7OD5OM4GVrsCsjnaAlmTUNtRPx9B4ps0gbr25F1PxuNy+MXfwSYJdliL+N01BTpiGyts/EXAraWvEm5YkhWfTnbgc8osd3cX9vwB0QHksK+BpkaEs6XCwU6kGMxAJIlafRv6RslREdTPBpYaXB4sGqdYXWY+YFqNxsAwTB3KWIq/uhZmSkou1jZfZa2QonMuVot68U11U7afmPzX8KOVeO2IEcUjt6I4eCYQ+31xO/wSLIQ1uoRySQ2S9VCzr+yzDpu0KVps=\naddons:\n  sauce_connect: true\n```\n\n可以看到，我又给它添加了一个 after_success 的配置，只有当之前的测试运行成功之后，才运行之后的命令。当然你也可以选用其他的配置，比如：`deploy`。\n\n要将代码发布到 github.io 上，就势必要 push 代码至仓库的 gh-pages 分支。然而，如果要通过 travis-ci 向 github 提交代码，那么，就要首先建立 ssh 链接。因为，这里是发布特定的仓库代码，所以，我推荐大家通过给 repository 设置 deploy key 的方式来给 travis-ci 授权，而不是 access token。\n\n那么，如何设置 deploy key？\n\n1. 本地新建一个 ssh key（不清楚的点[这里](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/)）\n2. 进入 github 你要发布的仓库中，选择 settings -> Deploy keys -> Add deploy key，并将你刚刚生成的 key.pub 文件中的内容复制到输入框中，记得勾选 Allow write access，再点击 Add key。这样就设置好了 deploy key，但肯定不能将 key 直接放到 github 上，需要先加密。\n3. 使用 travis 工具加密 deploy key `travis encrypt-file key`，这会生成一个 key.enc 文件，将这个文件加入到代码仓库中就行，不要向代码库提交生成的 key 和 key.pub 文件\n4. 加密完成后，控制台会输出一串日志，其中有类似这样的一条 `openssl aes-256-cbc -K $encrypted_c7881d9cb8b5_key -iv $encrypted_c7881d9cb8b5_iv -in key.enc -out key -d`，这就是用来建立 ssh 链接的。将其中 `$encrypted_..._key` 之间的字符提取出来，作为系统运行变量，也就是之前 .travis.yml 中的 `GIT_DEPLOY_KEY: XXXXXXXX`，这样发布脚步中就能使用这个变量\n\nOK。这样 deploy key 就准备好了，下面是发布脚本。\n\n```Bash\n#!/bin/bash\nset -e # Exit with nonzero exit code if anything fails\n\n# Git variables\nTARGET_PATH=\"build/\"\nTARGET_BRANCH=\"gh-pages\"\n\n# Travis encrypt variables\nENCRYPTED_KEY=\"encrypted_${GIT_DEPLOY_KEY}_key\"\nENCRYPTED_IV=\"encrypted_${GIT_DEPLOY_KEY}_iv\"\n\n# Save some useful information\nREPO=`git config remote.origin.url`\nSSH_REPO=${REPO/https:\\/\\/github.com\\//git@github.com:}\nSHA=`git rev-parse --verify HEAD`\n\n# Build source\nnpm run build\n\n# Set committer git info\ngit config user.name $USER_NAME\ngit config user.email $USER_EMAIL\n\n# Force add build folder to git\ngit add -f $TARGET_PATH\n\n# Commit the build code, that is a local commit for git subtree split\ngit commit -m \"Deploy to GitHub Pages: ${SHA}\"\n\n# Split build file as a $TARGET_BRANCH of git\ngit subtree split -P $TARGET_PATH -b $TARGET_BRANCH\n\n# Add ssh authorization\nopenssl aes-256-cbc -K ${!ENCRYPTED_KEY} -iv ${!ENCRYPTED_IV} -in deploy_key.enc -out deploy_key -d\n\n# Change the deploy_key mod to fix ssh permissions too open error\nchmod 600 deploy_key\neval `ssh-agent -s`\nssh-add deploy_key\n\n# Push code to git\ngit push -f $SSH_REPO $TARGET_BRANCH\n```\n\n这个脚本只需简单的变量改动就能适应你的项目，当然，你也可以为自己的项目编写自己的发布脚本。\n\n## Jenkins\n以上说的都是源代码放在 Github 上的开源代码，但我相信大家接触得更多的应该是自己公司的私有代码，比如和 Jira 相关的 Stash。\n\n首先，Stash 现已改名为之前提到过的 Bitbucket，那么，只要将 travis-ci 替换成 circleci 就可以了，其余两个插件都是支持 Bitbucket 的。\n\n其次，如果项目仓库，既不是 Github, 也不是 Bitbucket 或 Gitlab，不要着急，这时候就需要祭出万金油 Jenkins 了。\n\nJenkins 那成千上万的 Plugin，相信总有一款适合你。比如，老版的 stash 就可以参照这篇[文章](https://blog.mikesir87.io/2013/04/continuous-integration-with-stash-and-jenkins/)来配置。\n\n## 最后\n最后，回顾一下整个 CI 流程。\n\n当代码被提交到 github 分支上时，travis-ci 会被触发开始整套的测试及发布。\n\n首先，安装项目依赖；  \n然后，运行测试，其中包括 UT 和 e2e test；  \n测试无误后，自动将打包后的代码发布到 gh-pages 分支；\n于是，就可以通过 [https://用户名.github.io/项目名](https://discipled.github.io/react-redux-antd-starter) 访问项目了。\n\n完成~\n\n来看看成(hui)果(zhang)吧。查看源码点[这里](https://github.com/DiscipleD/react-redux-antd-starter/tree/real-world)。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/readme.png)\n\n### 关于徽章\n所有的徽章信息都可以在 [shields.io](http://shields.io/) 中查看，甚至可以自定义徽章，就像这样 ![custom badge](https://img.shields.io/badge/Disciple-D-blue.svg)。哈哈哈~\n\n少年们，想要集徽章么？快把测试补起来吧~\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ci-solution/study.jpg)\n\n**参考文章：**\n\n1. [前端开源项目持续集成三剑客](http://efe.baidu.com/blog/front-end-continuous-integration-tools/)\n2. [一个靠谱的前端开源项目需要什么？](http://web.jobbole.com/86858/)\n3. [Zero to Hero with End-to-End tests using Nightwatch, SauceLabs and Travis](https://medium.com/@mikaelberg/zero-to-hero-with-end-to-end-tests-using-nightwatch-saucelabs-and-travis-e932c8deb695#.7z40jm3ss)\n4. [Auto-deploying built products to gh-pages with Travis](https://gist.github.com/domenic/ec8b0fc8ab45f39403dd)\n5. [Continuous Integration with Stash and Jenkins](https://blog.mikesir87.io/2013/04/continuous-integration-with-stash-and-jenkins/)\n"
  },
  {
    "path": "src/server/data/posts/css-flex.md",
    "content": "#### What is Flex?\nFlex 是 Flexible Box 的缩写，意为\"弹性布局\"，用来为盒状模型提供最大的灵活性。\n\nW3C 于 2009 年提出了这一方案，时至今日，常用的浏览器已经全部都提供了对它的支持（当然不包括 IE8）。\n\n![Flex浏览器支持情况](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/css-flex/browser-support.jpg)\n\n#### Why to use Flex?\n简便的实现页面布局。\n\n#### How to use Flex?\n为一个元素简单地设置 display: flex; 就使得其成为 Flex 容器（flex container），其内部的所有子元素自动成为容器中的成员（flex item）。\n\n容器默认存在两根轴：水平的主轴（`main axis`）和垂直的交叉轴（`cross axis`）。主轴的开始位置（与边框的交叉点）叫做（`main start`），结束位置叫做 `main end` ；交叉轴的开始位置叫做 `cross start`，结束位置叫做 `cross end`。\n\n项目默认沿主轴水平排列。单个项目占据的主轴空间叫做 `main size`，占据的交叉轴空间叫做 `cross size`。\n\n![Flex基本概念](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/css-flex/flex-box.png)\n\n**注意：**当一个元素设置为 display: flex; 后，其子元素（即flex item）的 float，clear 和 vertical-align 属性将无效。\n\n对于 Webkit 内核的浏览器需要加上 `-webkit` 前缀。\n\n### Flex Container Attributes\n----\n1. `flex-direction`: row | row-reverse | column | column-reverse;\n该属性决定 flex item 在容器中的排列方向，默认为 row，即水平从左 → 右排列；column为从 上 ↓ 下排列；加 -reverse 后缀，即和原先排列顺序相反。\n2. `flex-wrap`: nowrap | wrap | wrap-reverse;\n该属性决定 flex item 在容器中是否换行，换行的方式又是什么，默认为 nowrap，即不换行。wrap 为换行，当 `flex-direction` 为row时，内容从 上 ↓ 下按行排列；当 `flex-direction` 为 column 时，内容从 左 → 右按列排列；加 -reverse 后缀，即和原先排列顺序相反。\n3. `flex-flow`: <flex-direction> || <flex-wrap>\n该属性是 `flex-direction` 和 `flex-wrap` 的简写形式，默认值是原属性 `flex-direction` 和 `flex-wrap` 的默认值，即row nowrap。\n4. `justify-content`: flex-start | flex-end | center | space-between | space-around;\n该属性决定 flex item 在行内的水平对齐方式或列内的垂直对齐方式，默认值是 flex-start。\nflex-start： 与轴的 start 对齐，即左对齐(flex-direction: row)，上对齐(flex-direction: column)\nflex-end：与轴的 end 对齐，即右对齐(flex-direction:row)，下对齐(flex-direction:column)\ncenter： 与轴的的中点对齐\nspace-between：与轴的两端对齐，flex-item 之间的间隔都相等，头尾的 flex item 紧贴轴的 start 位置\nspace-around：每个 flex item 两侧的间隔相等。所以，flex item 之间的间隔比 flex item 与轴的 start 之间的间隔大一倍\n**注意：**\nflex item 默认是没有间距的，间距是由 flex container 的宽度或高度与 flex item 的宽度或高度之间的差产生的，即如果 flex container 的宽度为1000px，flex item 的宽度为100px，container 下有 10 个 item，那无论 justify-content 设任何的值，展示都将是 10 个 item 紧贴地并列排列，item 与 item 之间没有任何间隙。\n5. `align-items`: flex-start | flex-end | center | baseline | stretch;\n该属性与 justify-content 相反，决定 flex item 在行内的垂直对齐方式或列内的水平对齐方式，默认值是 stretch。\nflex-start：与轴的 start 对齐\nflex-end：与轴的 end 对齐\ncenter：与轴的的中点对齐\nbaseline： 与 flex item 的第一行文字的 baseline 对齐\nstretch：如果 flex item 未设置宽度或高度或设为 auto，将占满这行的高度或这列的宽度\n**注意：**\nbaseline 属性在 container 的 flex-direction 设置为 column 时无效。\n当 align-items 属性值设置为 stretch 时，如一个 flex item 设置了宽度或高度，则这个 flex item 应用flex-start，且只对该 flex item 生效。\n6. `align-content`: flex-start | flex-end | center | space-between | space-around | stretch;\n该属性类似于 `justify-content` 属性，与之不同的是，该属性决定 flex item 每行或每列在 flex container 下的对齐方式，如果 flex item 只有一行或一列，则该属性无效，默认值为 stretch。\nflex-start：与轴的 start 对齐\nflex-end：与轴的 end 对齐\ncenter：与轴的中点对齐\nspace-between：与轴的两端对齐，轴线之间的间隔都相等\nspace-around：每根轴线两侧的间隔都相等。所以，轴线之间的间隔比轴线与边框的间隔大一倍\nstretch：轴线占满整个交叉轴\n**注意：**\n当 `align-content` 属性设定为 flex-start、flex-end 或 center时，轴与轴之间默认是没有间隔的。\n\n### Flex Item Attributes\n----\n1. `order`: \\<integer\\>;\n该属性定义 flex item 的排列顺序，数值越小，排列越靠前，默认值为0。\n**注意：**数值可以为负数。\n2. `flex-grow`: \\<number\\>;\n该属性定义 flex item 的放大比例，默认值为 0，即使有空余空间也不放大该元素。\n**注意：**数值可以为小数，但不能为负数。\n3. `flex-shrink`: \\<number\\>;\n该属性与 `flex-grow` 相反，定义 flex item 的缩小比例，默认值为1，即空间不足时，等比例缩小元素；flex-grow 为 0，则空间不足时也不缩小该元素。\n**注意：**数值可以为小数，但不能为负数。\n4. `flex-basis`: \\<length\\> | auto;\n该属性定义在分配剩余空间之前，flex item 占所在轴的大小，默认值为 auto，即原有元素大小。\n**注意：**该属性设定的大小为未分配剩余空间之前的大小，flex item 最终显示的大小会受 flex-grow 或 flex-shrink 的影响。\n5. `flex`: auto | none | [ <`flex-grow`> <`flex-shrink`>? || <`flex-basis`> ];\n该属性是 `flex-grow`、`flex-shrink` 和 `flex-basis` 的简写，默认值为各属性的默认值，0 1 auto。\n该属性还有2个快捷值：auto(1 1 auto), 即 flex item 根据 container 的内容大小自动缩放；none(0 0 auto)，即 flex item 保持自身元素大小，不进行缩放。\n6. `align-self`: auto | flex-start | flex-end | center | baseline | stretch;\n该属性用来设置只用于自身的对齐方式，将覆盖 container 的 `align-items` 属性，默认值为 auto，即继承父属性的 `align-items` 属性。\n\n### TRY\n----\n俗话说的好，光说不练假把式，既然已经清楚了概念，我就尝试使用这些特性，看到阮老师的另一篇文章后，自己也尝试做了一遍，通过 flex 完成了骰子的6个面。\n\n![骰子的六面](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/css-flex/dice.png)\n\n[点击查看源码](http://plnkr.co/edit/BthfuHwFAlZiOUxrU99v?p=preview)\n\n如果理解了 flex 容器的特性，那么上面的列子尝试起来并不难，只有在第 5 点的时候遇到一些小障碍，如何画中间那个点，最后是通过给第 3 个点增加两边的margin，使元素的宽度增加来处理。如果你也对这个有兴趣可以参考[这里](https://davidwalsh.name/flexbox-dice)，里面也有几种不同的实现，或许对你也有所启发，如果你有更好的想法，欢迎留言交流。\n\n另外，在查资料时还发现 CSS3 box-flex，一看描述和内容，完全和 flex 是同一个东西啊。\n\n* `display: box`：弹性模型第一版，不推荐使用（适用于老版本浏览器）。\n* `display: flexbox`：box升级版，不推荐使用（适用于老版本浏览器）。\n* `display: flex`：最新的弹性模型版本，推荐使用。\n\n参考资料：\n1. [阮一峰 Flex 布局教程：语法篇](http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html?utm_source=tuicool)\n2. [A Complete Guide to Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/#flexbox-basics)\n3. [阮一峰 Flex 布局教程：实例篇](http://www.ruanyifeng.com/blog/2015/07/flex-examples.html)\n4. [Getting Dicey With Flexbox](https://davidwalsh.name/flexbox-dice)"
  },
  {
    "path": "src/server/data/posts/decorator-design-pattern.md",
    "content": "### 嗯？这都是怎么一回事哪？\n最近我有机会研究使用不同的方法在JavaScript中实现[装饰者模式（又称为包装模式）](https://en.wikipedia.org/wiki/Decorator_pattern)。我觉得有必要分享我所学到的，关于使用这些技术来实现装饰者模式的利弊。\n\n**\"当然不是这种装饰者...\"**\n![当然不是这种装饰者...](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/decorator.png)\n\n这5种不同的实现方式分别是：\n\n1. 闭包\n2. 猴子补丁\n3. 原型继承\n4. 代理（ES6）\n5. 中间件\n\n如果你想要知道本文 **a) 为什么使用ES6语法**，**b) 为什么不使用class**， **c) 源文件列表**，为了不打乱阅读顺序，我已经把这些都记在了附录中，你可以到这篇文章的最后查看。\n\n### 首先，需要被装饰的组件\n```JavaScript\n'use strict'\n\nfunction myComponentFactory() {\n    let suffix = ''\n\n    return {\n        setSuffix: suf => suffix = suf,\n        printValue: value => console.log(`value is ${value + suffix}`)\n    }\n}\n\nconst component = myComponentFactory()\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\n```\n这是个简单的组件，含有一个 `printValue(val)` 方法，用来在值的最后添加尾缀并在控制台输出，尾缀可以通过 `setSuffix(val)` 方法设置。\n\n我准备用一个验证输入的装饰器，以及一个将值转换为小写的验证器来展示装饰链的情景。创建 `setSuffix(val)` 方法是为了添加一些复杂性，用来满足组件拥有除装饰方法以外还有其他成员。\n\n值得注意的是，除了最后一个以外的所有例子都是使用独立的函数对目标对象进行装饰，而不是添加对象的一个成员。\n\n#### 如何装饰这个组件\n\n下图显示我准备如何装饰这个组件，先将初始的 `printValue(val)` 方法先用 'lower case' 装饰器包装，然后再用 'validate' 装饰器包装。当一个被装饰过的组件调用 `printValue(val)` 方法时，首先它会验证它的值，然后会将值转为小写，最后打印它。\n\n（注意：下图表明，我们可以在原始调用之后返回过程时，给我们的装饰器添加额外的行为，而本文并没有涉及这些。）\n\n![组件装饰设计图](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/design-picture.png)\n\n### 方法一： 闭包\n我能想到最原生实现装饰者模式的方法就是用一个对象来包装需要被装饰的对象，并返回一个新对象，在这个新对象中执行一些处理后再调用原始的方法。\n\n![简单](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/simple.png)\n> “简单！”\n\n**上代码！**\n\n首先，我会展示这些代码作为一个整体，然后我会带你一步一步地分析它。\n\n```JavaScript\nfunction myComponentFactory() {\n    let suffix = ''\n\n    return {\n        setSuffix: suf => suffix = suf,\n        printValue: value => console.log(`value is ${value + suffix}`)\n    }\n}\n\nfunction toLowerDecorator(inner) {\n    return {\n        setSuffix: inner.setSuffix,\n        printValue: value => inner.printValue(value.toLowerCase())\n    }\n}\n\nfunction validatorDecorator(inner) {\n    return {\n        setSuffix: inner.setSuffix,\n        printValue: value => {\n            const isValid = ~value.indexOf('My')\n\n            setTimeout(() => {\n                if (isValid) inner.printValue(value)\n                else console.log('not valid man...')\n            }, 500)\n        }\n    }\n}\n\nconst component = validatorDecorator(toLowerDecorator(myComponentFactory()))\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n这些都做了什么？\n\n组件工厂还是和之前的一样。不过，我们通过用装饰工厂包裹它的创建来装饰它。\n\n```JavaScript\nconst component = validatorDecorator(toLowerDecorator(myComponentFactory()))\n```\n原始对象将作为参数传入装饰工厂中，并返回一个经过包装的对象，它会将除了被装饰的方法以外的调用直接传递给初始对象。\n\n装饰工厂接受原始对象作为参数，并返回一个经过包装后的对象，这个对象会将除了需要被装饰的方法以外的调用直接传递给原始对象。\n\n```JavaScript\nfunction toLowerDecorator(inner) {\n    return {\n        setSuffix: inner.setSuffix,\n        printValue: value => inner.printValue(value.toLowerCase())\n    }\n}\n```\n装饰器会将值转换为小写，并把这个“装饰”（或“包装”）后的值传给了内部函数。\n\n然后，我们可以继续在对象的创建上添加装饰工厂方法并等待调用，这就像是在打开一个俄罗斯套娃。\n\n![俄罗斯套娃](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/Matryoshka-doll.jpg)\n\n在完成了对象的创建和装饰之后，我们运行我们的测试代码：\n\n```JavaScript\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n结果是：\n\n```JavaScript\nvalue is my value!\nnot valid man...\n```\n最外层的装饰器将会第一个被执行。在这个例子中是验证方法。第一次调用是合法的，所以结果会被传递给第二个方法，值将被转换为小写，然后再按顺序调用原始方法给经过小写处理后的值添加尾缀，并在控制台中输出结果。\n\n第二次调用没有通过验证，所以值没有被修改，它展示了如何停止装饰链。\n\n#### 验证装饰器为何要设置定时？\n\n![[(服务生比喻是解释异步代码最好的方法)](http://www.roidna.com/blog/what-is-node-js-benefits-overview/)\n](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/waiters.jpg)\n\n我在包装方法中添加一些异步的代码，因为我们在 JavaScript 的世界里：一个单线程，无阻塞，异步为王的语言世界。如果你的代码无法处理异步，那么它就失去了大部分 JavaScript 语言设计的特点。\n\n验证方法通过设置定时来模拟去数据库验证值的合法性，然后在回调函数中去调用内部函数的方法。这样我们能测试我们的实现方式在处理异步代码时是否依旧能正常工作。\n\n#### 该如何使用闭包？\n为了之后的调用，我们可以将内部对象存储到一个新对象上。但我们为什么不这样做？因为这会使得它成为公共的，那时，我该调用 `instance.setSuffix()`，还是 `instance._original.setSuffix()`？这会变得非常奇怪，会混淆对象的使用，使对象成为一个私有成员这会好得多。\n>“然而JavaScript并没有私有成员，糟糕！”\n\n但我们可以使用闭包来达到这个效果。\n\n**官方：什么是闭包？**\n> “即使函数在变量的作用域之外被调用，闭包允许函数访问闭包引用的变量。”（我稍微重新措辞从[维基百科](https://en.wikipedia.org/wiki/Closure_(computer_programming))的定义）\n\n一个简单的例子:\n\n```JavaScript\nfunction wow() {\n    const val = 5\n    return () => console.log(val)\n}\n\nwow()()\n```\n这是一个我能想到用  JavaScript实现最简单的例子。“wow” 方法返回一个打印 “val” 的方法，然而，一旦 “wow” 返回，“val” 变量就不在作用域之中。\n\n然而，它会正常显示，因为闭包在方法返回时就被创建了，它已经记录了作用域里的变量（在这个例子中是 “val”），即使离开了当前作用域，闭包依旧可以访问它内部的变量，\n\n#### 回到我们的装饰器\n\n再来看看之前的装饰器：\n\n```JavaScript\nfunction toLowerDecorator(inner) {\n    return {\n        setSuffix: inner.setSuffix,\n        printValue: value => inner.printValue(value.tolowercase())\n    }\n}\n```\n它返回一个包含以下方法的对象：\n\n```JavaScript\nvalue => inner.printValue(value.tolowercase())\n```\n这个方法引用 “inner” 对象，当装饰方法被返回时，“inner” 对象就已经在作用域之外了。但因为，它在方法内部是一个被使用的变量，所以内部方法会记录这个变量，一旦这个方法被返回，那么闭包就形成了。\n\n这意味着为了嵌套的方法能在之后正常调用，变量的生命周期被我们的内部方法给延长了。\n\n因为闭包，我们的方法能使用“inner”对象，但它是私有变量，并不是公共的。\n\n闭包是 JavaScript 最重要和实用的特性之一，所以确保你现在已经领悟它了。\n\n![私有](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/soldiers_privates.jpg)\n\n#### 优缺点\n\n在这介绍闭包，虽然它和上面的包装方法有一点关系，但事实上，本文所展示的技术都使用了闭包来隐藏私有变量。\n\n除此之外，这是一个非常简单的实现，但有一个非常明显的缺点：我们必须包装内部对象的每个方法，而非装饰那一个目标方法。就像这样：\n\n```JavaScript\nreturn {\n    setSuffix: inner.setSuffix,\n    ...\n```\n这既丑陋又痛苦。可不可以我们的装饰器只定义装饰行为而不去关心剩下的？幸运的是有不少技术能这样做。让我们看看猴子补丁是如何做的。\n\n### 方法二：猴子补丁\n\n**什么是猴子补丁？**\n>“动态修改一个类或模型。” -[维基百科](https://en.wikipedia.org/wiki/Monkey_patch)\n\n简单地在当前情景下解释一下：\n>“我将要采用 JavaScript 的动态性并结合对象可变性的特点来用我的方法取代你的方法！” -（那时的我）\n\n那该如何用猴子补丁来装饰？\n>“我准备用我的方法替换你的，然后我会从我的方法内部包装并调用你的方法。” -（依旧是我）\n\n**该怎么做！**\n你问该怎么做？好，我会像你展示。首先，我会展示这些代码作为一个整体，然后我会带你一步一步地分析它：（译者注：这里是原作者的一处幽默，看原文更能体会。）\n\n```JavaScript\nfunction myComponentFactory() {\n    let suffix = ''\n\n    return {\n        setSuffix: suf => suffix = suf,\n        printValue: value => console.log(`value is ${value + suffix}`)\n    }\n}\n\nfunction decorateWithToLower(inner) {\n    const originalPrintValue = inner.printValue\n    inner.printValue = value => originalPrintValue(value.toLowerCase())\n}\n\nfunction decorateWithValidator(inner) {\n    const originalPrintValue = inner.printValue\n\n    inner.printValue = value => {\n        const isValid = ~value.indexOf('My')\n\n        setTimeout(() => {\n            if (isValid) originalPrintValue(value)\n            else console.log('not valid man...')\n        }, 500)\n    }\n}\n\nconst component = myComponentFactory()\ndecorateWithToLower(component)\ndecorateWithValidator(component)\n\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n这都做了些什么？\n\n组件还是那个组件，装饰器变了，并且调用方式也变了。我们通过在现有对象上进行处理的方式，来代替通过工厂方法传递对象的方式来实现我们的装饰方法：\n\n```JavaScript\ndecorateWithToLower(component)\n```\n这个装饰方法通过保存初始 “printValue” 方法到一个本地变量的办法来实现猴子补丁：\n\n```JavaScript\nconst originalPrintValue = inner.printValue\n```\n然后用一个方法覆盖原始方法，这个方法先将值转换为小写，再将值传递给之前储存的原始方法的副本去调用：\n\n```JavaScript\ninner.printValue = value => originalPrintValue(value.toLowerCase())\n```\n我们和之前一样创建我们的装饰器。我们先用一个转换小写装饰器包装 `printValue()`，再用一个验证装饰器来包装它：\n\n```JavaScript\nconst component = myComponentFactory()\ndecorateWithToLower(component)\ndecorateWithValidator(component)\n```\n注意这里依旧使用了闭包来用于内部函数链的存储。与例一真正的区别在于我们只替换了现有对象中的一个方法，而不是返回一个全新包装后的对象。\n\n**猴子补丁的优缺点**\n\n人们讨厌猴子补丁通常有着好的理由。\n\n![猴子](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/monkeys.jpg)\n\n额......\n\n为什么所有人都讨厌？因为当我调用一个库函数时，我不希望功能因为我引入了一些其他完全无关的“巨坑”库而被修改了。\n\n不幸的是，如果那个愚蠢的人类决定去猴子补丁一些原生方法或一些共享的依赖，那对我来说就没有惊喜，只剩惊吓了。\n\n如果猴子补丁只是现在用于我自己的代码，它可能并不那么糟糕，但它依旧有点古怪，一些人依旧会对它说“不”。\n\n尽管如此，它比我们之前的方法还是有一个优势。我们的装饰方法只需处理我们想要装饰的方法，组件其余的部分保持不变。这意味着我们的装饰方法只有一个职责：用新的行为包装去方法。\n\n所以，如果你不介意猴子补丁，你的基础对象又拥有需要被额外维护的公共方法，而且你希望保持代码简洁，那么这项技术可能适合你。\n\n好，那有关原型继承是怎样的？\n\n### 方法三：原型继承\n**什么是原型继承？**\n\n大多数开发者习惯于 Java 或 C# 这种一个类基于另一个类的经典继承方式。简单来说，原型继承就都是用对象代替类：“一个对象从其他对象继承上属性。”\n\n它的实现机制在 JavaScript 中也十分简单。所有对象都有同一个原型。事实上，所有原型链的终点都指向 “Object”，它也是所有原型链的基础。\n\n**委托**\n\n![委托](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/delegation.jpg)\n\n你可以通过设置对象的原型声明一个对象基于另一个对象。这就意味着：如果需要访问一个对象成员，对象首先会在自身之中查找，但如果没有找到，它会去它的原型上继续查找，并一直按照这个方式查找到原型链的终点。\n\n我不喜欢使用 JavaScript 中的 `new` 关键字，我不在这深入说为什么，如果你感兴趣，可以到文章的最后查看。而在我看来，实现原型继承最好的方法是使用 `Object.create(prototype)`：\n\n```JavaScript\nconst myBaseObject = { myProperty: 'oh hai' }\n\nconst myNewObject = Object.create(myBaseObject)\nmyNewObject.newMethod = () => { console.log(myBaseObject.myProperty) }\n```\n这里我们不仅设置 myBaseObject 作为 myNewObject 的原型来继承，还展示了如何访问基类的成员。这里没有受保护的或私有的作用域，也没有抽象成员需要我们考虑。如果你想只暴露新的对象而不显示基类对象，只需通过一个函数包裹，然后返回所有你想要的。函数总是能处理你在 JavaScript 中遇到的任何问题。\n\n**看代码**\n\n```JavaScript\nfunction myComponentFactory() {\n    let suffix = ''\n\n    return {\n        setSuffix: suf => suffix = suf,\n        printValue: value => console.log(`value is ${value + suffix}`)\n    }\n}\n\nfunction toLowerDecorator(inner) {\n    const instance = Object.create(inner)\n    instance.printValue = value => inner.printValue(value.toLowerCase())\n    return instance\n}\n\nfunction validatorDecorator(inner) {\n    const instance = Object.create(inner)\n    instance.printValue = value => {\n        const isValid = ~value.indexOf('My')\n\n        setTimeout(() => {\n            if (isValid) inner.printValue(value)\n            else console.log('not valid man...')\n        }, 500)\n    }\n    return instance\n}\n\nconst component = validatorDecorator(toLowerDecorator(myComponentFactory()))\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n这个例子里我构造了一个新的对象来访问内部的对象，这和第一个例子十分地相似。然而，第一个例子中有个缺陷就是为了确保初始对象的每个成员都可访问，需要将每个成员变量复制到新的包装对象上。在这个例子中，我们发挥对象继承的优势，新对象创建时使用初始对象作为它的原型，这样我们就不必在新对象上定义 “setSuffix” 方法，当这个方法被调用时，原型链会检查这个成员是否存在。\n\n在 JavaScript 中使用继承来实现一个装饰器是一个显而易见并高效的方式。有趣的是，装饰者模式的最初设计目的之一就是解决传统继承的一些局限性。也就是说，采用传统的继承，无法将不同的行为联系起来，必须事先定义类的继承关系，这会导致它成为一个僵化的层次结构（译者并不赞同这个观点）。幸运的是，原型继承没有这个限制，从上面的例子就可以看到，我可以选择任何对象作为原型。\n\n这使得用原型继承来实现装饰器是一个极好的选择。\n\n### 方法四：代理\nES6中增加了代理模块，它看上去有希望去完成一些关于面向切片的编程技术。让我们来看看，它能不能帮我们创建一个装饰器。\n\n**什么是代理？**\n>“代理对象通常用来为基本操作定义自定义行为（例如：属性查找，赋值，枚举或函数调用等）。 -[MDN](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy)\n\n哇~我们可以在属性查找和函数调用时注入自定义行为？听起来很强大？没错，很强大。\n\n**看代码**\n\n```JavaScript\nrequire('harmony-reflect')\n\nfunction myComponentFactory() {\n    let suffix = ''\n\n    return {\n        setSuffix: suff => suffix = suff,\n        printValue: value => console.log(`value is ${value + suffix}`)\n    }\n}\n\nfunction toLowerDecorator(inner) {\n    return new Proxy(inner, {\n        get: (target, name) => {\n            return (name === 'printValue')\n                ? value => target.printValue(value.toLowerCase())\n                : target[name]\n        }\n    })\n}\n\nfunction validatorDecorator(inner) {\n    return new Proxy(inner, {\n        get: (target, name) => {\n            return (name === 'printValue')\n                ? value => {\n                    const isValid = ~value.indexOf('my')\n\n                    setTimeout(() => {\n                        if (isValid) target.printValue(value)\n                        else console.log('not valid man...')\n                    }, 500)\n                }\n                : target[name]\n        }\n    })\n}\n\nconst component = toLowerDecorator(validatorDecorator(myComponentFactory()))\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n首先，这是什么？\n\n```JavaScript\nrequire('harmony-reflect')\n```\n因为，我用 node.js 运行代码，然而 node.js 暂时还不支持代理模块。如果你想要在 node 中使用代理需要使用以下代码：\n\n```JavaScript\nnode.exe --harmony-proxies\n```\n即使这样，在写这篇博客时，node 中的代理模块依旧不是ES6的标准模块。然而，如果你：\n\n```JavaScript\nnpm install harmony-reflect\n```\n并向之前一样在代码中引入该模块，那么你会得到一个接近最新 ES6 标准的代理对象来使用，而你现在仍必须使用上面的方法。（我猜测 npm 模块的底层使用的仍是不符合ES6规范的代理对象。）\n\n接下来你会发现组件依旧没有变化，而装饰方法变得不同了：\n\n```JavaScript\nfunction toLowerDecorator(inner) {\n    return new Proxy(inner, {\n        get: (target, name) => {\n            return (name === 'printValue')\n                ? value => target.printValue(value.toLowerCase())\n                : target[name]\n        }\n    })\n}\n```\n代理赋予你无比强大的力量，值得你阅读 [MDN](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 上代理部分。\n\n![代理](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/decorator-design-pattern/spiderman_proxies.jpg)\n\n在这里，装饰器将内部对象作为参数输入，并返回它的代理。在代理中，我们只处理一件事：属性访问。我们通过为 “get” 处理程序添加自定义行为来做到这点。\n\n我们测试一下看看属性是否是我们想装饰的属性，如果是则返回新的装饰器方法（在这个例子中，方法就是将值转换为小写并传递值给内部对象的 printValue 方法）；如果属性名不符合就直接返会内部对象的成员。\n\n细心的你一定会发现我们这里又使用到了闭包。\n\n**代理模式的优劣势**\n\n这里的关键点是，虽然为了创建我们的代理对象不得不做一些额外的工作，但无论装饰对象中有多少个成员，装饰器都不会变的更复杂。所以，代理模式有2个优点：\n\n1. 它不是猴子补丁\n2. 不必手动重新定义内部对象的每个成员\n\n然而，这可能有点杀鸡用牛刀了，原型继承有着相同的优势。代理的实现是被用来处理面向切片风格的东西，而不是装饰器。\n\n还有，就像我之前说的，支持还不够好。如果你在 node 环境中，那你可以用我之前的方法 polyfill。然而，如果你在浏览器环境中，现在所有的 IE 版本都不支持， Chrome 也只在 49 版本支持。不幸的是，从我的理解看来，可能在浏览器中 ployfill 这个特性将会很困难，很可能会[造成严重的性能问题](https://www.npmjs.com/package/babel-plugin-proxy)。\n\n### 方法五：中间件\n之前的那些例子都有一个非常棒的特性，那就是初始的对象不必知道它被装饰了。通过闭包，猴子补丁，继承或者代理来扩展初始对象的行为而不必修改它，这就是[面向对象设计](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design))的[开闭原则](http://c2.com/cgi/wiki?OpenClosedPrinciple)。\n\n假设基础对象一开始就知道自己的创建过程中会被一个特定的方法装饰，会怎么样？还有，假设想在基础功能和装饰器之间增加更多影响，会怎么样？假设通过把一些装饰器的逻辑放到基础对象中使装饰器的代码更为简单,会怎么样？\n\n**看代码**\n\n```JavaScript\nfunction myComponentFactory() {\n    let suffix = ''\n    const instance = {\n        setSuffix: suff => suffix = suff,\n        printValue: value => console.log(`value is ${value + suffix}`),\n        addDecorators: decorators => {\n            let printValue = instance.printValue\n            decorators.slice().reverse().forEach(decorator => printValue = decorator(printValue))\n            instance.printValue = printValue\n        }\n    }\n    return instance\n}\n\nfunction toLowerDecorator(inner) {\n    return value => inner(value.toLowerCase())\n}\n\nfunction validatorDecorator(inner) {\n    return value => {\n        const isValid = ~value.indexOf('My')\n\n        setTimeout(() => {\n            if (isValid) inner(value)\n            else console.log('not valid man...')\n        }, 500)\n    }\n}\n\nconst component = myComponentFactory()\ncomponent.addDecorators([toLowerDecorator, validatorDecorator])\ncomponent.setSuffix('!')\ncomponent.printValue('My Value')\ncomponent.printValue('Invalid Value')\n```\n\n注意到主要的区别了么？我们的初始对象知道它会被装饰，并提供了一个特别的方法来添加装饰器。这里组件设置自己的装饰链，你只需提供装饰方法的列表：\n\n```JavaScript\ncomponent.addDecorators([toLowerDecorator, validatorDecorator])\n```\n\n“addDecorators” 方法会遍历传入到方法中的装饰器，然后将最后一个装饰器方法执行后的结果赋给公共成员变量。这就是基础对象给自身设置装饰链。值得注意的是，方法里翻转了装饰器调用的顺序，为了参数传递时更具可读性：\n\n```JavaScript\naddDecorators: decorators => {\n    let printValue = instance.printValue\n    decorators.slice().reverse().forEach(decorator => printValue = decorator(printValue))\n    instance.printValue = printValue\n}\n```\n装饰器方法本身将便的十分简单，它所要做的全部就是更具需要装饰传入的方法并返回这个方法。\n\n```JavaScript\nfunction toLowerDecorator(inner) {\n    return value => inner(value.toLowerCase())\n}\n```\n**中间件的优劣势**\n通过基础对象自身来创建装饰链，能够获得装饰器更多的控制权。在这个例子中，它被用来通过 `reverse()` 方法来改变装饰器数组的顺序。\n\n在创建装饰链时获得更多的控制权也导致了装饰方法便得极其简单。\n\n因此，通过将对象设置为可以被装饰和完成建立装饰链的工作，我们达成了这些目标：\n\n1. 简单的装饰方法\n2. 建立装饰链时，更多的控制权\n3. 简单地建立装饰列表，只需传递一个有顺序的装饰器数组的方法，而不必关心特殊装饰者模式实现的构造机制\n4. 依旧符合开闭原则，基本实现允许在不修改原始对象的情况下完成装饰\n5. 它不是猴子补丁，也不依赖于代理\n\n这是最复杂的实现方式，如果你设置了一些重量级的装饰器，需要更多的管理而不是简单的包装，那么这个实现可能适合你。\n\n我称呼它为“中间件”实现，是因为：\n\n1. 我想不出比这个更好的（译者：- -||）\n2. Dan Abramov 使用相同的方法在他的 [redux 中间件](http://redux.js.org/docs/advanced/Middleware.html)实现中\n\n### 结论\n我们着眼于用5个不同的技术来实现装饰者模式，在这过程中我们学到了不少。\n\n1. 一个原始的做法，它需要手动地从内部对象复制每个成员到装饰对象上。但我们从中学到了通过闭包来遮盖变量，就好像它们是私有的。\n2. 用猴子补丁的方法解决了“复制每个成员”的问题，但是它也有相当大的副作用。\n3. 用原型继承的方式解决了“复制每个成员”的问题，似乎完美无缺。\n4. 使用 ES6 代理对象又一次解决了之前的问题。然而，代理对象还没有被很好的支持，虽然它是无比强大的，但也强大到超出了这个使用场景。在当前场景下，它并不能比原型继承做得更多。\n5. 在“中间件”实现中，基础对象设置了自身的装饰链，这使得它和简单的装饰方法一起成为一个强大并灵活的实现。\n\n####从中我们能总结哪些结论？\n**每个实现**方式都使用了**闭包**。这应该能让你明白它在 JavaScript 中有多重要。如果你仍不理解它，退回去再读一次，或者去阅读一些别人更好的描述。\n\n哪个是明显的赢家？当我开始写这篇的时候，我期望每个技术都有它的优势和劣势，取决于使用场景。事实上，写到最后我认为只有2种实现方式值得被使用：\n\n1. 原型继承\n2. 中间件\n\n只有当需要对装饰链进行更多控制的时候才使用中间件的方式，否则，原型继承似乎对我来说就是最终赢家。它具有所有的优点：\n\n1. 不需要修改基础对象\n2. 不需要复制每个成员到新的对象\n3. 不是猴子补丁\n4. 不支持差的代码\n5. 相对简单的装饰方法\n\n### 附录\n#### ES6\n我在本文中使用 ES6 语法出于以下多种原因的：\n\n1. 我爱上了 ES6 中的许多事，尤其是箭头函数和 const 关键字\n2. 最新的 node.js 中已经原生支持它的大部分功能，也可以通过一个简单的 babel 转换在浏览器中运行 ES6\n3. 用它更容易写文章中的例子（缺点是，这对没有学过 ES6 的读者并不是这样）\n4. 最近我花了些时间在读和写一些 React 的代码，它所有都是用 ES6 的，所以我们是时候都上这条船了（译者：歪果仁动不动就开船，果仁一言不合就开车）\n5. 我开始学习 ES6 在 [babel 的官网](https://babeljs.io/docs/learn-es2015/)，如果你也想开始学习 ES6 可以从这里开始\n\n#### 类\n为什么我使用 ES6 却不适用 `class` 关键字哪？有两个非常重要的原因：\n\n1. 类在 ES6 中有很多问题，这有一整篇[文章](https://medium.com/javascript-scene/how-to-fix-the-es6-class-keyword-2d42bb3f4caf#.osnwj4xq5)关于它。（对我来说，真正的烦恼是缺乏私有变量和这样做的“意义是什么”，这个关键字比一个简单的工厂方法做得更少。）\n2. 也许更重要的是为了这篇文章：我们谈论的是装饰器，然而装饰器很难和 ES6 class 语法一起工作。正因为此有项提议在 ES7 中应当解决装饰器的问题。然而，正如我希望你看到这篇文章，如果你继续使用函数语法，通过简单的 JavaScript 语法就能创建功能强大的装饰器。\n3. 无论是 `new` 关键字还是 `class` 关键字在 ES6 中都让人迷惑。这看上去让 JavaScript 便得\n把它们加进 JavaScript 中看上去会让从传统语言，像 Java，转过来的人感觉更舒适，但结果是笨重的，只会掩盖原型的真正能力和简单。这里是另一篇优秀的[文章](http://aaditmshah.github.io/why-prototypal-inheritance-matters/)关于刚刚所提到的。\n\n#### 源文件\n\n1. [闭包](http://nickmeldrum.com/scripts/decorator-wrapper.js)\n2. [猴子补丁](http://nickmeldrum.com/scripts/decorator-monkeypatching.js)\n3. [原型继承](http://nickmeldrum.com/scripts/decorator-inheritance.js)\n4. [代理](http://nickmeldrum.com/scripts/decorator-proxy.js)\n5. [中间件](http://nickmeldrum.com/scripts/decorator-middleware.js)\n\n原文：[The decorator pattern in JavaScript using closures, monkey patching, prototypes, proxies and 'middleware'](http://nickmeldrum.com/blog/decorators-in-javascript-using-monkey-patching-closures-prototypes-proxies-and-middleware?utm_source=javascriptweekly&utm_medium=email)\n\n------------ 华丽的分割线 ------------\n\n最后译者推荐[飞狐系列](https://segmentfault.com/u/feihu/articles)，对理解JS设计模式很有帮助。\n\nPPS：翻译的好坏的确是由语文水平决定的，而非外语水平。"
  },
  {
    "path": "src/server/data/posts/docker-compose.md",
    "content": "首先，祝各位新年快乐，万事如意，鸡年大吉。\n\n这次要来说说一个和前端并不太相关的东西——docker compose，一个整合发布应用的利器。\n\n如果，你对 docker 有一些耳闻，那么，你可能知道它是什么。\n\n不过，你不了解也没有关系，在作者眼中，docker 就类似于一个沙箱，而你的应用起在这个沙箱里，不受服务器系统环境的影响，同时也不污染服务器，配置完成之后往服务器部署或移除应用都相当方便。\n\n而 compose 就如同它的字面意思组合，它就好像是一个大箱子，可以把几个不相关的沙箱给组合起来，变成一个整体，就如同小时候动画片中变形金刚的合体变身。\n\n![Awesome?](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/docker-compose/transformer.gif)\n\n理论知识就没有什么比[官方文档](https://docs.docker.com/compose/overview/)更好的了，这里就不讲了，主要来看看如何应用。本文主要包含以下几个部分：\n\n* [安装](#Install)\n* [Hello world](#HelloWorld)\n* [常用命令](#Command)\n* [Real world](#RealWorld)\n\t* [docker 到 docker-compose 的转换](#Transform)\n\t* [引入 nginx](#Nginx)\n\t* [Letsencrypt 镜像生成 SSL 证书](#Letsencrypt)\n\t* [最后](#Conclusion)\n\n如果，你只对前端技术感兴趣，那么，这篇文章可能不适合你。\n\n> 常言道：一个不懂运维的设计，不是一个好前端。\n\n<a name=\"Install\"></a>\n## 安装\nWindows 和 Mac 装了 Docker 之后已经自带 docker-compose，其他环境根据 Docker [官网](https://docs.docker.com/compose/install/)介绍，简单几步也能完成安装。\n\n这里要提一下，在亚马逊 aws 上安装 docker-compose，由于没有 root 权限会遇到官网上所提到的 `Permission denied` 错误，加了 sudo 也是无法直接下载到 /usr/local/bin 目录下的。\n\n硬来不行，还可以曲线救国嘛~\n\n先将文件下载到 aws 服务器上，再将文件移动到 `/usr/local/bin` 目录就可以了。\n\n```Bash\ncurl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > docker-compose\nsudo chown root docker-compose\nsudo mv docker-compose /usr/local/bin\nsudo chmod +x /usr/local/bin/docker-compose\n```\n\n验证是否安装成功，试试 `docker-compose version`。如果有输出版本信息，就说明 docker-compose 已经安装好了。\n\ndocker-compose 虽然安装好了，但并不一定能用，因为 docker 和 docker-compose 是分开安装，即使它俩各自运行正常，在一起就不一定合拍了。\n\n那怎么知道它俩合不合拍？答案很简单，hello world~\n\n<a name=\"HelloWorld\"></a>\n## Hello world\n在任意的目录下，创建一个 docker-compose.yml 文件，并添加下面的内容。\n\n```yml\nversion: '2'\nservices:\n  helloworld:\n    image: 'hello-world'\n```\n\n然后，在当前目录下使用 `docker-compose up` 启动 docker-compose。\n\n启动时，如遇到\n\n> client and server don't have same version (client : 1.22, server: 1.18)\n\n类似这样的错误，可以通过设置 docker-compose 的 api 版本来解决。\n\n```Bash\nCOMPOSE_API_VERSION=auto\n```\n\n不要尝试通过一次次安装不同的 docker-compose 版本来解决，你会 😭 的。如果，还遇到\n\n> docker.errors.InvalidVersion: inspect_network is not available for version < 1.21\n\n这是 Ubuntu 14.04 LTS 默认的 docker 版本太低引起的，需要升级 docker。然而，在 aws 的服务器上升级 docker 版本时，需要先创建 `/etc/apt/sources.list.d/docker.list` 文件，并添加\n\n```\ndeb https://packages.docker.com/1.12/apt/repo ubuntu-trusty main\n```\n\n再运行 \n\n```Bash\nsudo apt-get update && sudo apt-get upgrade docker-engine\n```\n\n就能升级成功。看到👇这样的结果，就表示 docker 和 docker-compose 都安装成功，而且它俩很搭。\n\n![Hello world result](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/docker-compose/hello_world.png)\n\n<a name=\"Command\"></a>\n## 常用命令\ndocker-compose 的命令很简单，它已经将一些 docker 常用关于 image, container & volume 的命令都整合在了一起，使发布变得极其简单。比如，之前刚刚提到的 `docker-compose up`，就类似于 docker build & run，用来创建并启动 container。\n\n其他常用的命令有：\n\n* `build`：构建或重新构建 services\n* `config`：验证 docker-compose 配置文件\n* `create`：创建 services\n* `down`：与 `up` 相对，停止并删除 container, image, volumn 等\n* `kill`：杀死某个 container\n* `logs`：查看 container 日志\n* `ps`：查看 container 信息\n* `restart`：重启 services\n* `rm`：删除已经停止的 container\n* `start`：启动 services\n* `stop`：停止 service\n* `version`：显示 docker-compose 版本\n\n是不是发现有几个命令和 docker 的命令一样？的确，但就如同之前的安装过程一样，docker-compose 是依赖于 docker 的，docker 命令更底层。比如 `docker-compose ps` 这个命令，它只会显示由 docker-compose 启动的容器信息，但不包含 docker 启动的容器信息，相反 `docker ps` 可以查看由 docker-compose 启动的容器信息。\n\n还剩几个命令没有列出来，有兴趣的童鞋可以通过 `docker-compose help` 命令或上官网查看[更多信息](https://docs.docker.com/compose/reference/overview/)。\n\n光说不练假把式。docker-compose 究竟好不好用，只有用了才知道。\n\n<a name=\"RealWorld\"></a>\n## Real world\n之前，个人博客的静态资源一直都是通过 node 提供服务。这的确可以，但这不是 node 的强项。\n\n> 专业的事交给专业的人去做。 - by S(ome)B(ody)\n\n这个专业的人就是 nginx。\n\n除此之外，2017 年起水果和古哥都强推 https，升级 https 也是箭在弦上（虽然一直有这个打算，也拖到了现在\u0005\u0005彡(-_-;)彡）。\n\n于是，程序不再是原先单一的 node 服务，而是，变成了一系列密切相关的服务。如果，通过基础的 docker 命令来一个个启动、停止服务的话，那么，就需要额外添加一个复杂的脚本来控制。\n\ndocker-compose 就是用来处理类似的问题。它可以做到通过一条命令来控制一个应用相关的一系列服务的启动、停止等，并且不依赖于机器环境，作到随时可以将应用迁移至其他的机器上发布。\n\n知道了准备做什么，先看看最终设计的应用结构和之前的对比。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/docker-compose/architecture.png)\n\n直接看这张图可能有点蒙圈，没事，一点点来看。\n\n<a name=\"Transform\"></a>\n### docker 到 docker-compose 的转换\n本文一开始就有提到，docker 可以看做是一个小箱子，而 docker-compose 是一个大箱子用来装这些小箱子。\n\n那么，如何将小箱子放入这个大箱子里哪？\n\n非常简单！只需告诉 docker-compose 如何启动你的应用就可以了，那就先看看原先的启动命令。\n\n```Bash\ndocker run -d -p 80:8080 --name blog\n```\n\n启动命令中，主要配置了一个端口的映射 `-p`，以及命名了容器名，用于方便地启动、停止应用。清楚了这些，那么改成 docker-compose 的文件也就轻而易举了。\n\n```yml\nversion: '2'\nservices:\n  node:\n    build: .\n    container_name: node\n    ports:\n     - \"80:8080\"\n```\n\ndocker 到 docker-compose 的转换就这样完成了，这些更新都不需要修改任何的业务逻辑或者打包配置。\n\n试着使用 `docker-compose up -d` 启动服务验证看看。\n\n启动正常之后，还是一步步来，先引入 nginx。\n\n<a name=\"Nginx\"></a>\n### 引入 Nginx\nNginx 是一个高性能的 Web 服务器，它具有配置简单、运行稳定和负载均衡等特点，常被作为静态资源服务器。（详细的 Nginx 信息，请自行查询资料，这方面本人也不是行家）\n\nNginx 在 docker hub 上有现成的[官方镜像](https://hub.docker.com/_/nginx/)，直接拿来用就可以了。\n\n```yml\nversion: '2'\nservices:\n  # ...\n\n  nginx:\n    image: nginx:stable\n    container_name: nginx\n    ports:\n      - \"80:80\"\n    restart: always\n```\n\n此时，启动服务会失败并报错，因为 nginx 和原有的 node 容器都绑定到了 80 端口。docker-comopse 各个容器之间是相互独立的，容器内部的接口相互之间不影响，但对外暴露的接口不能相同，不然就会引起冲突。\n\n从之前的结构图可以看到，请求全部由 nginx 接受并转发到 node 服务，也就是说，node 不直接对外提供服务。那么，docker-compose 中也就可以移除 ports 部分（这里便于测试 node 服务依旧暴露 8080 端口）。\n\n其次，静态文件是由 node 打包后生成的，也就是说需要将 node 服务中的数据共享给 nginx 服务，这就需要用到 [volume](https://docs.docker.com/engine/tutorials/dockervolumes/)（数据卷）。数据卷可以将数据在宿主机和容器之间、容器和容器之间共享，即使容器被删除了，数据卷依旧存在。\n\n这里就需要将服务器上的 nginx 配置文件和 node 构建之后的静态文件共享给 nginx。\n\n```yml\nversion: '2'\n\nservices:\n  node:\n    build: .\n    container_name: node\n    # node service port export for test\n    ports:\n     - \"8080:8080\"\n    volumes:\n     - ./log/node:/var/log/node\n\n  nginx:\n    image: nginx:stable\n    container_name: nginx\n    depends_on:\n      - node\n    volumes:\n      - ./config/nginx:/etc/nginx/conf.d:ro\n      - ./log/nginx:/var/log/nginx\n    volumes_from:\n      - node:ro\n    ports:\n      - \"80:80\"\n    restart: always\n```\n\nvolume 是 docker 中相当重要及常用的一部分，理解它对使用 docker 解决问题有巨大的帮助。推荐一篇关于 docker volume 的[文章](http://dockone.io/article/128)，有助于理解 volume。\n\n#### 负载均衡\ndocker-compose 配置完了，再来看看 nginx 配置。本章一开始有提到 nginx 可以做负载均衡，那该如何配置哪？\n\n在 nginx 中配置负载均衡相当简单，只需在 `upstream` 里配置一下目标服务器。\n\n然而，这里就会遇到一个问题。由于，容器之间是相互独立的，于是，localhost 便无法在容器之间相互访问。不过，由同一 docker-compose 所起的容器之间可以通过**容器名**相互访问，这里就是\n\n```\nupstream node_server  {\n    server node:8080 max_fails=2 fail_timeout=30s;\n}\n```\n\n如果要额外再起一个服务，只需在 docker-compose 文件中再启动一个容器（可以依赖同一套代码），并将之前所配的 `upstream` 中额外多添加一条 server 信息，比如：\n\n```\nupstream node_server  {\n\tserver node:8080 max_fails=2 fail_timeout=30s;\n\tserver node-backup:8080 max_fails=2 fail_timeout=30s;\n}\n```\n\n这样即使一个服务挂了，只要另一个服务还运行正常，nginx 会将请求转发给运行正常的服务。一个最简单的复杂均衡就做好了，所有这些都不需要修改任何功能性的代码。\n\n知道了 nginx 可以提供负载均衡，但也不要忘了老朋友 pm2。\n\npm2 通过命令行参数 -i，或配置文件通过起多个实例来做负载均衡（本人的小博客也是用的这个方式）。\n\n引入 nginx 之后，将全站升级成 https 就轻而易举了，只需在配置文件中标明证书及秘钥文件的位置就可以了。接下去，就看看如何生成证书和秘钥。\n\n<a name=\"Letsencrypt\"></a>\n### 使用 Letsencrypt 生成 SSL 证书\n获取 ssl 证书的方式有许多种，有的买域名就送证书，这里介绍一下用 [letsencrypt](https://certbot.eff.org/)（现已更名为 `certbot`）获取免费 ssl 证书。\n\n> 常言道：前人栽树，后人乘凉。\n\n同样的，letsencrypt 在 docker hub 上也有现成的[镜像](https://hub.docker.com/r/deliverous/certbot/)。镜像有了，剩下的就只需根据不同的场景来生成证书。\n\n`certbot` 支持 5 种生成证书的模式，分别是：`apache`, `nginx`, `webroot`, `standalone` 和 `manual`，分别用于[不同的场景](https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins)。这里 nginx 和 certbot 使用的是不同的镜像，所以选用的模式是 `webroot`。\n\n选定了镜像和模式，那么参照 certbot 的[文档](https://certbot.eff.org/docs/using.html#certbot-command-line-options)就能够简单地生成证书了。\n\n```Bash\ndocker run -it --rm --name certbot \\\n  -v /letsencrypt/etc/letsencrypt:/etc/letsencrypt \\\n  -v /letsencrypt/lib/letsencrypt:/var/lib/letsencrypt \\\n  -v /letsencrypt/challenge:/usr/share/nginx/html \\\n  -v /var/log/letsencrypt:/var/log/letsencrypt \\\n  deliverous/certbot \\\n  certonly --webroot -w /usr/share/nginx/html\n```\n\n需要注意的是，在 `webroot` 模式下申请证书，需要向 certbot 证明服务器能被访问。certbot 验证程序会访问 web root 目录（这里是 /usr/share/nginx/html）来验证。这里又要用到之前提到的 volume 将目录共享给 nginx，让 nginx 能够访问到目录内部的文件。\n\n```\nserver {\n    listen 80;\n    listen [::]:80;\n\n    server_name discipled.me;\n\n    # ...\n    \n    # letsencrypt challenge file location\n    location /.well-known {\n        root /usr/share/nginx/html;\n\n        access_log  /var/log/nginx/challenge-access.log  main;\n        allow all;\n    }\n    \n    ...\n}\n```\n\n修改 nginx 配置之后，别忘重启 nginx 服务。\n\n```Bash\ndocker-compose restart nginx\n```\n\n重启 nginx 之后，然后再运行上面生成证书的命令就能生成证书了。\n\n![ssl 证书生成成功](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/docker-compose/create-certificate-success.png)\n\n看到 `Congratulations！`，证书就生成成功了。\n\n再一次修改 nginx 配置，添加 ssl 证书信息，并监听 443 端口。\n\n```\n# redirect host http://domain to https://domain\nserver {\n    listen 80;\n    listen [::]:80;\n\n    server_name discipled.me;\n\n    # letsencrypt challenge file location\n    location /.well-known {\n        root /usr/share/nginx/html;\n\n        access_log  /var/log/nginx/challenge-access.log  main;\n        allow all;\n    }\n\n    location / {\n        return 301 https://discipled.me$request_uri;\n    }\n}\n\n# https://domain server\nserver {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n\n    server_name discipled.me;\n    charset utf-8;\n\n    gzip on;\n    gzip_types    text/plain application/javascript application/x-javascript text/javascript text/xml text/css;\n    root /usr/app/build/client/;\n\n    ssl_certificate /etc/letsencrypt/live/discipled.me/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/discipled.me/privkey.pem;\n\n    location / {\n        try_files $uri @node;\n    }\n\n    location @node {\n        proxy_pass http://node_server;\n        proxy_redirect off;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n```\n\n重启 nginx 服务后，访问网站就可以看到\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/docker-compose/https-home-page.png)\n\n小锁加上，大功告成。\n\n> 七牛的图床用 https 还要实名认证，为了保护(pa)个(cha)人(shui)隐(biao)私，就暂时用 Github 来救一下急。(谁知道有啥好用的图床麻烦推荐一下，像七牛一样支持 qrsync 用脚本批量上传的就最好了~先谢过...)\n\n#### 证书更新\nletsencrypt 生成的证书有效期是 3 个月，所以，至少 3 个月内需要更新一次证书。\n\ncertbot 提供了 renew 命令可以方便地更新证书，使用 `--dry-run` 参数可以验证证书更新命令是否正确。\n\n```Bash\ndocker run -it --rm --name certbot \\\n  -v /letsencrypt/etc/letsencrypt:/etc/letsencrypt \\\n  -v /letsencrypt/lib/letsencrypt:/var/lib/letsencrypt \\\n  -v /letsencrypt/challenge:/usr/share/nginx/html \\\n  -v /var/log/letsencrypt:/var/log/letsencrypt \\\n  deliverous/certbot \\\n  renew --dry-run\n```\n\n同样，看到 `Congratulations` 说明证书更新成功了。\n\n由于，本人每月都会发布文章并重启服务，就可以把证书更新一起交由 docker-compose 管理。（这里偷了个懒，增加了证书同应用之间的耦合关系，还是建议大家证书是通过系统定时任务来更新，省得哪天忘更新证书，证书就过期了）。\n\n<a name=\"Conclusion\"></a>\n### 最后\n看一下最终的 docker-compose 配置文件和发布脚本。\n\n```yml\n# docker-compose.yml\nversion: '2'\n\nservices:\n  node:\n    build: .\n    image: \"blog:${TAG_NAME}\"\n    container_name: node\n    # node service port export for test\n    ports:\n     - \"8080:8080\"\n    volumes:\n     - ./log/node:/var/log/node\n\n  nginx:\n    image: nginx:stable\n    container_name: nginx\n    depends_on:\n      - node\n      - letsencrypt\n    volumes:\n      - ./config/nginx:/etc/nginx/conf.d:ro\n      - ./letsencrypt/etc/letsencrypt:/etc/letsencrypt\n      - ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt\n      - ./letsencrypt/challenge:/usr/share/nginx/html\n      - ./log/nginx:/var/log/nginx\n    volumes_from:\n      - node:ro\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    restart: always\n\n  letsencrypt:\n    image: deliverous/certbot\n    container_name: certbot\n    volumes:\n      - ./letsencrypt/etc/letsencrypt:/etc/letsencrypt\n      - ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt\n      - ./letsencrypt/challenge:/usr/share/nginx/html\n      - ./log/letsencrypt:/var/log/letsencrypt\n    command: renew\n```\n发布脚本主要用来更新代码，以及获取应用版本号。\n\n```Bash\n# deploy.sh\n# git operation\ngit reset HEAD --hard\ngit fetch\ngit pull\n\n# TAG_NAME used to set docker image tag\nexport TAG_NAME=`git tag -l | sort -r | head -n 1`\n\n# docker operation\ndocker-compose down --volumes\n\ndocker-compose up --build -d\n```\n\n其他配置可以上 [github 查看](https://github.com/DiscipleD)。\n\n一扯似乎又扯远了，欢迎提意见和建议，顺便再问一下有啥好的图床推荐。\n"
  },
  {
    "path": "src/server/data/posts/does-curry-help.md",
    "content": "自从我写[为什么使用柯里化？（译）](#!/posts/why-curry-helps)——一篇描述柯里化函数在 JavaScript 中强大能力的文章，已经有两年半的时间了。它是我阅读量最多的一篇文章，每月都为我带来数百个读者。\n\n但随着时光流逝，世界变了，我也变了。通过柯里化来使你的代码更可读，依旧是个好主意么？\n\n我不再那么肯定了。\n\n### “这不是 [Haskell](https://www.haskell.org/)”\n\n当我最初提出柯里化作为我们工作中一个额外的工具时，我的同事威廉（非真名）坚决坚持：\n\n> 这不是 Haskell。\n\n我同样固执的认为我们应使用好的技术。然而，我花了一段时间才意识到他是多么正确。\n\n### 简单很重要，但易用也同样重要\n\n在 Rich Hickey [简单成就易用](http://www.infoq.com/presentations/Simple-Made-Easy)的演讲中，他区分了简单和易用的概念。\n\n他提出“简单”意味着逻辑清晰，而“易用”，则是接近于你当前的的理解。\n\n但是，如果非常简单的代码会造成工作中突出的困难，而你的团队从中却获得很少。那此时，你就需要一个平衡，是编写简单的代码来避免 bug 和不断变化的需求；还是编写足够你的团队理解易读的代码。\n\n这是 Haskell 和 JavaScript 第一个不同点。在 Haskell 中，柯里化是基本概念，每个 Haskell 开发者都理解它。\n\n在 JavaScript 中，这个概念就像一个外星人。我谈论过的大多数 JavaScript 开发者发现它难以理解和阅读。虽然，你可能认为柯里化会使代码变的简单，但它并不能让身边所有的团队得益。\n\n### 症状及原因\n\nHaskell 有一个能够在编译时捕获许多 bugs 的类型系统。当我卡住了，我经常编译程序，并让编译器指引我下一步。\n\n而 JavaScript 采用了相反的做法，编译时不作限制。这样做的好处是惊人的灵活性，不足之处就是错误很久才能被发现。\n\n柯里化函数的参数太少是一个常犯的错误，而且它通常很晚才会被发现。\n\n```JavaScript\nvar curry = require('curry');\nvar add = curry(function(a, b, c){ return a + b + c });\n\n// 这里的 threeP 并不返回我们想要的 3 的 Promise，而是返回一个一元函数的 Promise。\n// 调用 threeP 函数的代码可能不会预料到这个结果，而造成一个错误。\nvar threeP = Promise.resolve(1)\n  .then(add(2))\n```\n\n在大多数更复杂的应用场景中，它会导致你或你的同事浪费宝贵的时间来寻找出错的根源。\n\n### 箭头函数\n几个月前 [Josh Habdas](https://disqus.com/by/jhabdas/) 评论了之前那篇[文章]():\n\n> 在示例中，使用 [ES2015] 箭头函数将显著简化数据的访问。\n\n他是对的。\n\n毫无疑问，[为什么使用柯里化？（译）](#!/posts/why-curry-helps)的压轴案列是很棒的。它展示了使用 Promise 和一些工具函数来提取用户文章标题的列表。\n\n```JavaScript\nfetchFromServer()\n    .then(JSON.parse)\n    .then(get('posts'))\n    .then(map(get('title')))\n```\n\n在之前的文章中，我尝试[多少种场景可以使用箭头函数替代？](https://hughfdjackson.com/javascript/arrow-function-syntax/)，并应用这个新语言的特性来代替柯里化函数带来的大部分的好处：\n\n```JavaScript\nfetchFromServer()\n    .then(JSON.parse)\n    .then(data => data.posts)\n    .then(posts => posts.map(p => p.title))\n```\n\n### 我错了么？\n我是不是翻脸比翻书快？是啊，快多了。\n\n虽然[为什么使用柯里化？（译）](#!/posts/why-curry-helps)中并没有足够重视在实践中使用该技术，但我依旧认为文章中所述的柯里化所带来的好处依然存在。现在 ES2015 已经发布，在 JavaScript 中，箭头函数在大多数情况下是一个更自然的方式来优化代码。\n\n现在，我很少在 JavaScript 中使用柯里化。\n\n在过去 2 年半的时间里，虽然我仍试图使用柯里化，但我发现使用和团队成员水准相匹配的技术更为重要。\n\n#### 原文链接：[does-curry-help](https://hughfdjackson.com/javascript/does-curry-help/)\n"
  },
  {
    "path": "src/server/data/posts/es2015.md",
    "content": "主要介绍 `ECMAScript 6` 新引入的语法特性以及一些个人认为比较重要，以后开发时会遇到的一些特性和实例，更多特性和实例请移步[原著](http://es6.ruanyifeng.com/#README)。\n\n<a name=\"catalog\"></a>\n## 目录\n1. [ECMAScript 简介](#Introduction)\n* [**let & const**](#let)\n* [**变量的解构赋值**](#Destructuring)\n* [字符串的扩展](#String)\n* [正则的扩展](#Regular)\n* [数值的扩展](#Number)\n* [数组的扩展](#Array)\n* [**函数的扩展**](#Function)\n* [**对象的扩展**](#Object)\n* [Symbol](#Symbol)\n* [**Proxy和Reflect**](#Proxy)\n* [二进制数组](#BinaryArray)\n* [Set和Map数据结构](#SetMap)\n* [Iterator和for...of循环](#Iterator)\n* [Generator函数](#Generator)\n* [**Promise对象**](#Promise)\n* [异步操作和Async函数](#Async)\n* [**Class**](#Class)\n* [Decorator](#Decorator)\n* [**Module**](#Module)\n* [**编程风格**](#Style)\n\n<a name=\"Introduction\"></a>\n### [ECMAScript 简介](#catalog)\n\n#### What is ECMAScript?\n总结来说，JavaScript 是 ECMAScript 的一种实现，而 ECMAScript 是一种 `浏览器脚本语言` 的国际标准。\n今天分享的 ECMAScript6，是由 2013 年 6 月草案冻结，当年 12 月草案发布，各方讨论，直到今年 6 月正式通过，成为国际标准。\n\n[**How about the browse support now?**](http://kangax.github.io/compat-table/es6/)  \n从表中可以看到，尽管有很大一部分浏览器版本还没有实现 ES6，但各个浏览器的最新版已经支持大部分的 ES6 特性。相信随着时间的推移，浏览器 ES6 的支持度将会越来越好。\nNodeJs 对 ES6 的支持最好，大家有兴趣的可以使用 Node 来体验更多的 ES6 特性。\n\n#### Why to learn it?\n\n既然浏览器还没实现对它特性的全部支持，为什么要学它？  \n首先，它已经成为公认的标准，那么浏览器提供商一定会渐渐提供 ES6 所有特性的支持；\n其次，浏览器暂时的不支持是可以通过转码器来克服的。\n\n现在的 JS 代码也能实现业务需求，为什么还要学它？\n首先，也是最重要的一点，ES6 提供了模块化的支持，它使得大型项目的开发更得心应手；\n其次，ES6 提供的新特性使得JS的编写更轻松，更规范。\n\nPS:以上都是个人观点。\nPPS：个人当时学 ES6 的原因一是理解最新的规范，二是为学习 React 做好准备（React 支持部分 ES6 特性）。\n\n**转码器**  \n既然浏览器还不支持 ES6 那我们是不是现在就无法使用 ES6 的新特性哪？\n答案是否定的。Babel 转码器和 Traceur 转码器会是一个很好的解决方案，让你在使用 ES6 特性的同时，又能转换为浏览器可以理解的 ES5 编码。\n\n<a name=\"let\"></a>\n### [let & const](#catalog)\n\nES6 新增了 `let` 命令，用来声明变量。它的用法类似于 `var`，但是所声明的变量，只在 `let` 命令所在的代码块内有效，即 `let` 的作用域是块作用域。代码块也是 ES6 的新特性，类似于 java 的块。\n\n到此为止，大家会觉得 `let` 和 `var` 没有什么区别嘛，那我们就来看看 let 的神奇之处\n```JavaScript\n    var a = [];\n    for (var i = 0; i < 10; i++) {\n      a[i] = function () {\n        console.log(i);\n      };\n    }\n    a[6]();\n\n\tlet a = [];\n\tfor (let i = 0; i < 10; i++) {\n\t  a[i] = function () {\n\t    console.log(i);\n\t  };\n\t}\n\ta[6]();\n```\n\n大家来说说最后的输出分别是什么吧？\n\n我们来看看上述 ES6 代码解析成 ES5 代码的样子。\n```JavaScript\n\tvar a = [];\n\tvar $__0 = function(i) {\n\t  a[i] = function() {\n\t    console.log(i);\n\t  };\n\t};\n\tfor (var i = 0; i < 10; i++) {\n\t  $__0(i);\n\t}\n\ta[6]();\n```\n\n说了 `let` 的好处，那再看看使用 `let` 还要注意什么！\n1. 不存在变量提升  \n2. 暂时性死区  \n3. 不允许重复声明  \n**以上这些都可以通过事先声明来规避。所以，使用 `let`，`const` 一定要事先声明再使用！在函数开头声明所有变量也是 js 开发的最佳实践之一。**\n\n前面也说到了，ES6 新增的特性块级作用域。我们再看看下面的例子：\n```JavaScript\n\tfunction f() { console.log('I am outside!'); }\n\t(function () {\n\t  if(false) {\n\t    // 重复声明一次函数f\n\t    function f() { console.log('I am inside!'); }\n\t  }\n\t\n\t  f();\n\t}());\n```\n\n上面代码在 ES5 语法下，会得到 “I am inside!”，但是在 ES6 语法下，会得到 “I am outside!”。这是因为 ES5 存在函数提升，不管会不会进入 if 代码块，函数声明都会提升到当前作用域的顶部，得到执行；而 ES6 支持块级作用域，不管会不会进入 if 代码块，其内部声明的函数皆不会影响到作用域的外部。\n\n`const` 的用法基本和 `let` 相同，但它是用来声明的是常量。一旦声明，常量的值就不能改变。\n`const` 的作用域与 `let` 命令相同：只在声明所在的块级作用域内有效。\n\n**在 ES6 下，使用 `let`, `const` 替代 `var` 声明变量也是最佳实践。**\n\nES6 引入了块级作用域是完全可以替代立即执行函数。（个人认为，可讨论）\n\n<a name=\"Destructuring\"></a>\n### [变量的解构赋值](#catalog)\nES6 允许按照一定模式，从数组和对象中提取值，对变量进行赋值，这被称为解构（Destructuring）。\n\n##### 1. 数组的解构赋值\nES6 可以从数组中提取值，按照对应位置，对变量赋值。本质上，这种写法属于“模式匹配”，只要等号两边的模式相同，左边的变量就会被赋予对应的值。但如果等号的右边不是数组（或者严格地说，不是可遍历的结构，参见《Iterator》一章），那么将会报错。\n```JavaScript\n\tlet [foo, [[bar], baz]] = [1, [[2], 3]];\n\tfoo // 1\n\tbar // 2\n\tbaz // 3\n\n\tlet [head, ...tail] = [1, 2, 3, 4];\n\thead // 1\n\ttail // [2, 3, 4]\n\n\t// 报错\n\tlet [foo] = 1;\n\tlet [foo] = false;\n\tlet [foo] = NaN;\n\tlet [foo] = undefined;\n\tlet [foo] = null;\n```\n##### 2. 对象的解构赋值\n解构不仅可以用于数组，还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的，变量的取值由它的位置决定；而对象的属性没有次序，变量必须与属性同名，才能取到正确的值。\n```JavaScript\n\tvar { bar, foo } = { foo: \"aaa\", bar: \"bbb\" };\n\tfoo // \"aaa\"\n\tbar // \"bbb\"\n\t\n\tvar { baz } = { foo: \"aaa\", bar: \"bbb\" };\n\tbaz // undefined\n```\n\n如果变量名与属性名不一致，必须写成下面这样。其中 foo, first, last, loc & start 都是模式，没有具体值。\n```JavaScript\n\tvar { foo: baz } = { foo: \"aaa\", bar: \"bbb\" };\n\tbaz // \"aaa\"\n\t\n\tlet obj = { first: 'hello', last: 'world' };\n\tlet { first: f, last: l } = obj;\n\tf // 'hello'\n\tl // 'world'\n\n\tvar node = {\n\t  loc: {\n\t    start: {\n\t      line: 1,\n\t      column: 5\n\t    }\n\t  }\n\t};\n\t\n\tvar { loc: { start: { line }} } = node;\n\tline // 1\n\tloc  // error: loc is undefined\n\tstart // error: start is undefined\n```\n\n##### 3. 字符串的解构赋值\n字符串的解构赋值可以看成为数组解构赋值的一种。\n```JavaScript\n\tlet {length : len} = 'hello';\n\tlen // 5\n```\n\n##### 4. 函数参数的解构赋值\n函数的参数也可以使用解构，同样也可以使用默认值。\n```JavaScript\n\tfunction move({x = 0, y = 0} = {}) {\n\t  return [x, y];\n\t}\n\t\n\tmove({x: 3, y: 8}); // [3, 8]\n\tmove({x: 3}); // [3, 0]\n\tmove({}); // [0, 0]\n\tmove(); // [0, 0]\n```\n\n```JavaScript\n\tfunction move({x, y} = { x: 0, y: 0 }) {\n\t  return [x, y];\n\t}\n\t\n\tmove({x: 3, y: 8}); // [3, 8]\n\tmove({x: 3}); // [3, undefined]\n\tmove({}); // [undefined, undefined]\n\tmove(); // [0, 0]\n```\n大家能说出上面两段代码执行不同的区别吗？\n\n##### 5. 用途\n用途也就是解构的亮点。  \n（1）交换变量的值\n```JavaScript\n\t[x, y] = [y, x];\n```\n\n（2）从函数返回多个值\n```JavaScript\n\t// 返回一个数组\n\t\n\tfunction example() {\n\t  return [1, 2, 3];\n\t}\n\tvar [a, b, c] = example();\n\t\n\t// 返回一个对象\n\t\n\tfunction example() {\n\t  return {\n\t    foo: 1,\n\t    bar: 2\n\t  };\n\t}\n\tvar { foo, bar } = example();\n```\n\n（3）函数参数的定义\n```JavaScript\n\t// 参数是一组有次序的值\n\tfunction f([x, y, z]) { ... }\n\tf([1, 2, 3])\n\t\n\t// 参数是一组无次序的值\n\tfunction f({x, y, z}) { ... }\n\tf({x:1, y:2, z:3})\n```\n\n（4）提取 JSON 数据\n```JavaScript\n\tvar jsonData = {\n\t  id: 42,\n\t  status: \"OK\",\n\t  data: [867, 5309]\n\t}\n\t\n\tlet { id, status, data: number } = jsonData;\n\t\n\tconsole.log(id, status, number)\n\t// 42, OK, [867, 5309]\n```\n\n（5）函数参数的默认值\n```JavaScript\n\tjQuery.ajax = function (url, {\n\t  async = true,\n\t  beforeSend = function () {},\n\t  cache = true,\n\t  complete = function () {},\n\t  crossDomain = false,\n\t  global = true,\n\t  // ... more config\n\t}) {\n\t  // ... do stuff\n\t};\n```\n\n（6）遍历 Map 结构\n```JavaScript\n\t// 获取键名\n\tfor (let [key] of map) {\n\t  // ...\n\t}\n\t\n\t// 获取键值\n\tfor (let [,value] of map) {\n\t  // ...\n\t}\n```\n\n（7）输入模块的指定方法\n```JavaScript\n\tlet { log, sin, cos } = Math;\n\tconst { SourceMapConsumer, SourceNode } = require(\"source-map\");\n```\n\n<a name=\"String\"></a>\n### [字符串的扩展](#catalog)\n字符串扩展这一章节主要阐述了 ES6 对 Unicode 的支持。\n\nJavaScript 允许采用 `\\uxxxx` 形式表示一个字符，其中 “xxxx” 表示字符的码点。这种表示法只限于 `\\u0000` —— `\\uFFFF` 之间的字符。超出这个范围的字符，必须用两个双字节的形式表达（即 \\uD83D\\uDE80，ES6 可以通过大括号显示超过 FFFF 的字符，\\u{1F680}）。\n\nES6 之前，字符串函数对字符码点超过 FFFF 字符无法返回正确结果，比如 charAt 等。此次对的扩展也就是主要扩展这一方面的内容：\n\n根据字符在字符串的位置返回字符 charAt => at     \n根据字符的位置返回字符的码点 charCodeAt => codePointAt  \n根据字符的码点返回对应字符 fromCharCode => fromCodePoint\n\n另外的一大部分是 ES6 给字符串对象又添加了一些新的方法。\n\n- **includes()**：返回布尔值，表示是否找到了参数字符串。  \n- **startsWith()**：返回布尔值，表示参数字符串是否在源字符串的头部。  \n- **endsWith()**：返回布尔值，表示参数字符串是否在源字符串的尾部。  \nPS:这三个方法都支持第二个参数，表示开始搜索的位置。\n\n```JavaScript\n\tvar s = 'Hello world!';\n\t\n\ts.startsWith('Hello') // true\n\ts.endsWith('!') // true\n\ts.includes('o') // true\n\t\n\ts.startsWith('world', 6) // true\n\ts.endsWith('Hello', 5) // true\n\ts.includes('Hello', 6) // false\n```\n\n- **repeat()**:方法返回一个新字符串，表示将原字符串重复 n 次。\n\n另一大特色是**`模板字符串`**。  \n模板字符串（template string）是增强版的字符串，用反引号（**`**）标识。它可以当作普通字符串使用，也可以用来定义多行字符串，或者在字符串中嵌入变量。\n\n```JavaScript\n\t$(\"#result\").append(\n\t  \"There are <b>\" + basket.count + \"</b> \" +\n\t  \"items in your basket, \" +\n\t  \"<em>\" + basket.onSale +\n\t  \"</em> are on sale!\"\n\t);\n\n\t//使用模板字符串\n\t$(\"#result\").append(`\n\t  There are <b>${basket.count}</b> items\n\t   in your basket, <em>${basket.onSale}</em>\n\t  are on sale!\n\t`);\n```\n\n大括号内部可以放入任意的 JavaScript 表达式，可以进行运算，以及引用对象属性，当然包括运行函数。\n\n模板字符串的功能，不仅仅是上面这些。它可以紧跟在一个函数名后面，该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能（tagged template）。\n\n```JavaScript\n\tvar a = 5;\n\tvar b = 10;\n\t\n\tfunction tag(s, v1, v2) {\n\t  console.log(s[0]);\n\t  console.log(s[1]);\n\t  console.log(s[2]);\n\t  console.log(v1);\n\t  console.log(v2);\n\t\n\t  return \"OK\";\n\t}\n\t\n\ttag`Hello ${ a + b } world ${ a * b}`; //等于调用tag(['Hello ', ' world ', ''], 15, 50)\n\t// \"Hello \"\n\t// \" world \"\n\t// \"\"\n\t// 15\n\t// 50\n\t// \"OK\"\n```\n\n“标签模板”的一个重要应用，就是过滤HTML字符串，防止用户输入恶意内容。\n\n```JavaScript\n\tvar message =\n\t  SaferHTML`<p>${sender} has sent you a message.</p>`;\n\t\n\tfunction SaferHTML(templateData) {\n\t  var s = templateData[0];\n\t  for (var i = 1; i < arguments.length; i++) {\n\t    var arg = String(arguments[i]);\n\t\n\t    // Escape special characters in the substitution.\n\t    s += arg.replace(/&/g, \"&amp;\")\n\t            .replace(/</g, \"&lt;\")\n\t            .replace(/>/g, \"&gt;\");\n\t\n\t    // Don't escape special characters in the template.\n\t    s += templateData[i];\n\t  }\n\t  return s;\n\t}\n```\n上面代码中，经过 SaferHTML 函数处理，HTML 字符串的特殊字符都会被转义。\n\n<a name=\"Regular\"></a>\n### [正则的扩展](#catalog)\n在 ES5 中，RegExp 构造函数只能接受字符串作为参数。ES6 允许 RegExp 构造函数接受正则表达式作为参数，这时会返回一个原有正则表达式的拷贝。如果使用 RegExp 构造函数的第二个参数指定修饰符，则返回的正则表达式会忽略原有的正则表达式的修饰符，只使用新指定的修饰符。\n\n```JavaScript\n\tvar regex = new RegExp(\"xyz\", \"i\");\n\t// 等价于\n\tvar regex = new RegExp(/xyz/i);\n\n\tnew RegExp(/abc/ig, 'i').flags\n\t// \"i\"\n```\n\nES6 为正则表达式新增了 flags 属性，会返回正则表达式的修饰符。\n\n字符串对象共有 4 个方法，可以使用正则表达式：match()、replace()、search() 和 split()。\n\nES6 将这 4 个方法，在语言内部全部调用 RegExp 的实例方法，从而做到所有与正则相关的方法，全都定义在 RegExp 对象上（只是实现更好的`模块化`，对调用没有任何影响）。\n\nES6 新增 `u` 修饰符和 `y` 修饰符，`u` 修饰符用来解决 Unicode 大于 FFFF 时的匹配；`y`（“粘连”sticky）修饰符与 `g` 修饰符类似，也是全局匹配，不同之处在于，`g` 修饰符只要剩余位置中存在匹配就可，而 `y` 修饰符确保匹配必须从剩余的第一个位置开始，进一步说，`y` 修饰符号隐含了头部匹配的标志 `ˆ`。\n**注：**如果同时使用 `g` 修饰符和 `y` 修饰符，则 `y` 修饰符覆盖 `g` 修饰符。\n\n<a name=\"Number\"></a>\n### [数值的扩展](#catalog)\nES6 提供了二进制和八进制数值的新的写法，分别用前缀 0b（或0B）和 0o（或0O）表示。\n如果要将 0b 和 0x 前缀的字符串数值转为十进制，要使用 Number 方法。\n```JavaScript\n\tNumber('0b111')  // 7\n\tNumber('0o10')  // 8\n```\n\nES6 在 Number 对象上，新提供了一些方法，首先是 `Number.isFinite()` 和 `Number.isNaN()` 这两个方法，用来检查 Infinite 和 NaN 这两个特殊值。这两个方法也是为了更好的`模块化`，将原先属于 global 下的 `isFinite()` 和 `isNaN` 放入了 Number 对象下，此时需注意，Number 下的这两个方法是首先将值转换为数值，如果不是数值直接返回 false。\n```JavaScript\n\tisFinite(25) // true\n\tisFinite(\"25\") // true\n\tNumber.isFinite(25) // true\n\tNumber.isFinite(\"25\") // false\n\t\n\tisNaN(NaN) // true\n\tisNaN(\"NaN\") // true\n\tNumber.isNaN(NaN) // true\n\tNumber.isNaN(\"NaN\") // false\n```\n\nES6 将全局方法 `parseInt()` 和 `parseFloat()`，移植到 `Number` 对象上面，行为完全保持不变。目的也是逐步减少全局性方法，使得语言逐步`模块化`。\n\nES6 添加方法 `Number.isInteger()` 用来判断一个值是否为整数。需要注意的是，在 JavaScript 内部，整数和浮点数是同样的储存方法，所以 3 和 3.0 被视为同一个值。\n\n`Number.EPSILON` 是 ES6 在 `Number` 对象上新增的一个极小的常量，用来设置一个误差范围，如果小于这个误差范围，我们就认为得到了正确的结果。\n\nJavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间（不含两个端点），超过这个范围，无法精确表示这个值。为此，ES6 同时引入了 `Number.MAX_SAFE_INTEGER` 和 `Number.MIN_SAFE_INTEGER` 这两个常量，用来表示这个范围的上下限，以及一个方法 `Number.isSafeInteger()` 用来判断一个数值是否在这个范围之内。\n\n**谨记：**在 `Number` 对象下的对象首先必须是数值型，字符型调用都会有问题（parse除外）。\n\nES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法，只能在 Math 对象上调用。\n\n* `Math.trunc()` 方法用于去除一个数的小数部分，返回整数部分。\n* `Math.sign()` 方法用来判断一个数到底是正数、负数、还是零。\n* `Math.cbrt()` 方法用于计算一个数的立方根。\n* `Math.clz32()` 方法返回一个数的 32 位无符号整数形式有多少个前导 0。\n* `Math.imul()` 方法返回两个数以 32 位带符号整数形式相乘的结果，返回的也是一个 32 位的带符号整数。\n* `Math.fround()` 方法返回一个数的单精度浮点数形式。\n* `Math.hypot()` 方法返回所有参数的平方和的平方根。\n* `Math.expm1(x)` 返回 ex - 1，即 `Math.exp(x)` - 1。\n* `Math.log1p(x)` 方法返回 1 + x 的自然对数，即 `Math.log`(1 + x)。如果 x 小于 -1，返回 NaN\n* `Math.log10(x)` 返回以 10 为底的 x 的对数。如果 x 小于 0，则返回 NaN。\n* `Math.log2(x)` 返回以 2 为底的 x 的对数。如果 x 小于 0，则返回 NaN。\n* `Math.sinh(x)` 返回 x 的双曲正弦（hyperbolic sine）\n* `Math.cosh(x)` 返回 x 的双曲余弦（hyperbolic cosine）\n* `Math.tanh(x)` 返回 x 的双曲正切（hyperbolic tangent）\n* `Math.asinh(x)` 返回 x 的反双曲正弦（inverse hyperbolic sine）\n* `Math.acosh(x)` 返回 x 的反双曲余弦（inverse hyperbolic cosine）\n* `Math.atanh(x)` 返回 x 的反双曲正切（inverse hyperbolic tangent）\n\n<a name=\"Array\"></a>\n### [数组的扩展](#catalog)\n#### Array.from\n`Array.from` 方法用于将两类对象转为真正的数组：类似数组的对象（array-like object）和可遍历（iterable）的对象（包括 ES6 新增的数据结构 Set 和 Map）。\n```JavaScript\n\tArray.from('hello')\n\t// ['h', 'e', 'l', 'l', 'o']\n\t\n\tArray.from([1, 2, 3])\n\t// [1, 2, 3]\n\t\n\tlet namesSet = new Set(['a', 'b'])\n\tArray.from(namesSet) // ['a', 'b']\n\t\n\tlet ps = document.querySelectorAll('p');\n\tArray.from(ps).forEach(function (p) {\n\t  console.log(p);\n\t});\n```\n\n上面代码中，querySelectorAll 方法返回的是一个类似数组的对象，只有将这个对象转为真正的数组，才能使用 forEach 方法。\n\n值得提醒的是，扩展运算符（`...`）也可以将某些数据结构转为数组。\n```JavaScript\n\tlet ps = [...document.querySelectorAll('p')];\n```\n\n`Array.from` 还可以接受第二个参数，作用类似于数组的 map 方法，用来对每个元素进行处理。\n```JavaScript\n\tfunction typesOf () {\n\t  return Array.from(arguments, value => typeof value)\n\t}\n\ttypesOf(null, [], NaN)\n\t// ['object', 'object', 'number']\n```\n\n#### Array.of\n`Array.of` 方法用于将一组值，转换为数组，主要目的是弥补数组构造函数 `Array()` 的不足。因为参数个数的不同，会导致 `Array()` 的行为有差异。\n```JavaScript\n\tArray.of(3, 11, 8) // [3,11,8]\n\tArray.of(3) // [3]\n\tArray.of(3).length // 1\n\n\tArray() // []\n\tArray(3) // [undefined, undefined, undefined]\n\tArray(3, 11, 8) // [3, 11, 8]\n```\n\n#### copyWithin()\n数组实例的 `copyWithin` 方法，在当前数组内部，将指定位置的成员复制到其他位置（会覆盖原有成员），然后返回当前数组。也就是说，使用这个方法，会修改当前数组。\n```JavaScript\n\tArray.prototype.copyWithin(target, start = 0, end = this.length)\n\t\n\t[1, 2, 3, 4, 5].copyWithin(0, 3)\n\t// [4, 5, 3, 4, 5]\n```\n\n#### find()和findIndex()\n数组实例的 `find` 方法，用于找出第一个符合条件的数组成员。它的参数是一个回调函数，所有数组成员依次执行该回调函数，直到找出第一个返回值为 `true` 的成员，然后返回该成员。如果没有符合条件的成员，则返回 `undefined`。\n\n数组实例的 `findIndex` 方法的用法与 `find` 方法非常类似，返回第一个符合条件的数组成员的位置，如果所有成员都不符合条件，则返回 -1。\n```JavaScript\n\t[1, 5, 10, 15].find(function(value, index, arr) {\n\t  return value > 9;\n\t}) // 10\n\t\n\t[1, 5, 10, 15].findIndex(function(value, index, arr) {\n\t  return value > 9;\n\t}) // 2\n```\n\n这两个方法都可以发现 `NaN`，弥补了数组的 `IndexOf` 方法的不足。`indexOf` 方法无法识别数组的 `NaN` 成员，但是 `findIndex` 方法可以借助 `Object.is` 方法做到。\n```JavaScript\n\t[NaN].indexOf(NaN)\n\t// -1\n\t\n\t[NaN].findIndex(y => Object.is(NaN, y))\n\t// 0\n```\n\n#### fill()\n`fill` 方法使用给定值，填充一个数组。fill 方法还可以接受第二个和第三个参数，用于指定填充的起始位置和结束位置。\n\n#### entries()，keys()和values()\n这 3 个方法都是用来遍历数组，都返回一个 `Iterator` 对象，可以用 `for...of` 循环进行遍历，唯一的区别是 `keys()` 是对键名的遍历, `values()` 是对键值的遍历，`entries()` 是对键值对的遍历。\n\n#### includes()(该方法属于 ES7)\n`includes` 方法返回一个布尔值，表示某个数组是否包含给定的值，与字符串的 `includes` 方法类似。该方法的第二个参数表示搜索的起始位置，默认为 0。如果第二个参数为负数，则表示倒数的位置，如果这时它大于数组长度，则会重置为从 0 开始。通过 `inculdes` 方法也可以判断 `NaN`.\n\n<a name=\"Function\"></a>\n### [函数的扩展](#catalog)\n#### 函数默认值\n```JavaScript\n\tfunction fetch(url, { body = '', method = 'GET', headers = {} }){\n\t  console.log(method);\n\t}\n\t\n\tfetch('http://example.com', {})\n\t// \"GET\"\n\t\n\tfetch('http://example.com')\n\t// 报错\n```\n**注：**值为`undefined`会使用默认值，但`null`不会。\n\n#### rest\nES6 引入 `rest` 参数（形式为“...变量名”），用于获取函数的多余参数，这样就不需要使用 `arguments` 对象了。`rest` 参数搭配的变量是一个数组，该变量将多余的参数放入数组中。\n注意，`rest` 参数之后不能再有其他参数（即只能是最后一个参数），否则会报错，且函数的 `length` 属性，不包括 `rest` 参数。\n```JavaScript\n\tfunction add(...values) {\n\t  let sum = 0;\n\t\n\t  for (var val of values) {\n\t    sum += val;\n\t  }\n\t\n\t  return sum;\n\t}\n\t\n\tadd(2, 5, 3) // 10\n\n\t(function(a) {}).length  // 1\n\t(function(...a) {}).length  // 0\n\t(function(a, ...b) {}).length  // 1\n```\n\n#### 扩展运算符\n扩展运算符（`spread`）是三个点（`...`）。它好比 `rest` 参数的逆运算，将一个数组转为用逗号分隔的参数序列。\n```JavaScript\n\tfunction push(array, ...items) {\n\t  array.push(...items);\n\t}\n\t\n\tfunction add(x, y) {\n\t  return x + y;\n\t}\n\t\n\tvar numbers = [4, 38];\n\tadd(...numbers) // 42\n```\n\n扩展运算符内部调用的是数据结构的 `Iterator` 接口，因此只要具有 `Iterator` 接口的对象，都可以使用扩展运算符，比如 Map 结构。\n\n#### name属性\n函数的`name`属性，返回该函数的函数名。\n\n#### 箭头函数\nES6 允许使用“箭头”（`=>`）定义函数。\n```JavaScript\n\tvar sum = (num1, num2) => num1 + num2;\n\t// 等同于\n\tvar sum = function(num1, num2) {\n\t  return num1 + num2;\n\t};\n```\n\n箭头函数可以与变量解构结合使用。\n```JavaScript\n\tconst full = ({ first, last }) => first + ' ' + last;\n\t\n\t// 等同于\n\tfunction full( person ){\n\t  return person.first + ' ' + person.name;\n\t}\n```\n\n**注意：**  \n1. 函数体内的 `this` 对象，就是定义时所在的对象，而不是使用时所在的对象。\n2. 不可以当作构造函数，也就是说，不可以使用 `new` 命令，否则会抛出一个错误。\n3. 不可以使用 `arguments` 对象，该对象在函数体内不存在。如果要用，可以用`Rest`参数代替。\n4. 不可以使用 `yield` 命令，因此箭头函数不能用作 `Generator` 函数。\n\n#### 尾调优化\n尾调用（Tail Call）是函数式编程的一个重要概念，本身非常简单，一句话就能说清楚，就是指某个函数的最后一步是调用另一个函数。\n```JavaScript\n\tfunction f(x){\n\t  return g(x);\n\t}\n\n\t//以下三种情况，都不属于尾调用。\n\t// 情况一\n\tfunction f(x){\n\t  let y = g(x);\n\t  return y;\n\t}\n\t\n\t// 情况二\n\tfunction f(x){\n\t  return g(x) + 1;\n\t}\n\t\n\t// 情况三\n\tfunction f(x){\n\t  g(x);\n\t}\n```\n\n函数调用会在内存形成一个“调用记录”，又称“调用帧”（call frame），保存调用位置和内部变量等信息。所有的调用帧，就形成一个“调用栈”（call stack）。\n\n而尾调用由于是函数的最后一步操作，所以不需要保留外层函数的调用帧，因为调用位置、内部变量等信息都不会再用到了，只要直接用内层函数的调用帧，取代外层函数的调用帧就可以了。\n\n如果所有函数都是尾调用，就只保留内层函数的调用帧，那么每次执行时调用帧只有一项，这将大大节省内存。这就是“尾调用优化”。\n\n如果在函数的尾部调用函数自身就称为尾递归。对于尾递归来说，由于只存在一个调用帧，所以永远不会发生“栈溢出”错误，释放了内存的同时，优化了性能。\n\n<a name=\"Object\"></a>\n### [对象的扩展](#catalog)\n#### 属性的简洁表示法\nES6 允许如果对象的值等于对象的属性名，则值可以省略。\n```JavaScript\n\tvar ms = {};\n\t\n\tfunction getItem (key) {\n\t  return key in ms ? ms[key] : null;\n\t}\n\t\n\tfunction setItem (key, value) {\n\t  ms[key] = value;\n\t}\n\t\n\tfunction clear () {\n\t  ms = {};\n\t}\n\t\n\tmodule.exports = { getItem, setItem, clear };\n```\n\nES6 允许表达式作为对象的属性名，须把表达式放在方括号内，但属性名表达式与简洁表示法，不能同时使用。\n\n#### name属性\n函数的 `name` 属性，返回函数名。对象方法也是函数，因此也有 name 属性。\n\n有一些特殊情况，如使用了取值(`get`)函数，则会在方法名前加上 get；如存值(`set`)函数，方法名的前面会加上 set；如 `bind` 方法创造的函数，name 属性返回 “bound” 加上原函数的名字；Function 构造函数创造的函数，name 属性返回 “anonymous”。\n\n#### Object.is\n`Object.is` 用来比较两个值是否严格相等。它与严格比较运算符（`===`）的行为基本一致。不同之处只有两个：一是 +0 不等于 -0，二是 NaN 等于自身。\n```JavaScript\n\t+0 === -0 //true\n\tNaN === NaN // false\n\t\n\tObject.is(+0, -0) // false\n\tObject.is(NaN, NaN) // true\n```\n\n#### Object.assign\n`Object.assign` 方法用来将源对象（source）的**所有可枚举属性**，复制到目标对象（target）。它至少需要两个对象作为参数，第一个参数是目标对象，后面的参数都是源对象。只要有一个参数不是对象，就会抛出 TypeError 错误。如果目标对象与源对象有同名属性，或多个源对象有同名属性，则后面的属性会覆盖前面的属性，且 `Object.assign` 只拷贝自身属性，不可枚举的属性（`enumerable`为false）和继承的属性不会被拷贝。\n```JavaScript\n\tvar target = { a: 1, b: 1 };\n\t\n\tvar source1 = { b: 2, c: 2 };\n\tvar source2 = { c: 3 };\n\n\tObject.assign(target, source1, source2);\n\ttarget // {a:1, b:2, c:3}\n```\n\n**注意**：对于嵌套的对象，`Object.assign` 的处理方法是替换，而不是添加，即此方法不适用深拷贝，如果目标对象的值不是原始类型就可能会存在引用问题。同时，`Object.assign` 可以用来处理数组，但是会把数组视为对象。\n\n```JavaScript\n\tvar target = { a: { b: 'c', d: 'e' } }\n\tvar source = { a: { b: 'hello' } }\n\tObject.assign(target, source)\n\t// { a: { b: 'hello' } }\n\t\n\tObject.assign([1, 2, 3], [4, 5])\n\t// [4, 5, 3]\n```\n\n`Object.assign` 方法有很多用处。\n\n1.给对象添加属性\n```JavaScript\n\tclass Point {\n\t  constructor(x, y) {\n\t    Object.assign(this, {x, y});\n\t  }\n\t}\n```\n\n2.给对象添加方法\n```JavaScript\n\tObject.assign(SomeClass.prototype, {\n\t  someMethod(arg1, arg2) {\n\t    ···\n\t  },\n\t  anotherMethod() {\n\t    ···\n\t  }\n\t});\n```\n\n3.克隆对象（非深克隆）\n```JavaScript\n\tfunction clone(origin) {\n\t  return Object.assign({}, origin);\n\t}\n```\n\n4.合并多个对象\n```JavaScript\n\tconst merge = (target, ...sources) => Object.assign(target, ...sources);\n\t\n\t// 一个新对象\n\tconst merge = (...sources) => Object.assign({}, ...sources);\n```\n\n5.为属性指定默认值\n```JavaScript\n\tconst DEFAULTS = {\n\t  logLevel: 0,\n\t  outputFormat: 'html'\n\t};\n\t\n\tfunction processContent(options) {\n\t  let options = Object.assign({}, DEFAULTS, options);\n\t}\n```\n\n#### 对象的可枚举属性\n对象的每个属性都有一个描述对象（Descriptor），用来控制该属性的行为。`Object.getOwnPropertyDescriptor` 方法可以获取该属性的描述对象。\n```JavaScript\n\tlet obj = { foo: 123 };\n\t Object.getOwnPropertyDescriptor(obj, 'foo')\n\t //   { value: 123,\n\t //     writable: true,\n\t //     enumerable: true,\n\t //     configurable: true }\n```\n\n描述对象的 `enumerable` 属性，称为`可枚举性`，如果该属性为 false，就表示某些操作会忽略当前属性。\n\nES5 有三个操作会忽略 `enumerable` 为 false 的属性。\n\n* `for...in` 循环：只遍历对象自身的和继承的可枚举的属性  \n* `Object.keys()`：返回对象自身的所有可枚举的属性的键名  \n* `JSON.stringify()`：只串行化对象自身的可枚举的属性\n\nES6 新增了两个操作，会忽略 enumerable 为 false 的属性。\n\n* `Object.assign()`：只拷贝对象自身的可枚举的属性\n* `Reflect.enumerate()`：返回所有for...in循环会遍历的属性  \n\n引入 `enumerable` 的最初目的，就是让某些属性可以规避掉 `for...in` 操作。比如，对象原型的 `toString` 方法，以及数组的 `length` 属性，就通过这种手段，不会被 `for...in` 遍历到。\n\nES6 规定，所有 Class 的原型的方法都是不可枚举的。\n\n#### 对象原型\n众所周知，`__proto__` 属性是用来读取或设置当前对象的 `prototype` 属性。由于该属性是有下划线开头，故为私有属性，不建议直接去控制其的值。而是使用 `Object.setPrototypeOf()`（写操作）、`Object.getPrototypeOf()`（读操作）、`Object.create()`（生成操作）代替。\n\n#### `Object.observe()`，`Object.unobserve()`\n`Object.observe` 方法用来监听对象（以及数组）的变化。一旦监听对象发生变化，就会触发回调函数。\n\n`Object.observe` 方法接受两个参数，第一个参数是监听的对象，第二个参数是一个回调函数，第三个参数用来指定监听的事件种类。\n```JavaScript\n\tObject.observe(o, observer, eventType);\n```\n\n`Object.observe` 方法目前共支持监听六种变化。\n\n- add：添加属性  \n- update：属性值的变化\n- delete：删除属性\n- setPrototype：设置原型\n- reconfigure：属性的 attributes 对象发生变化\n- preventExtensions：对象被禁止扩展（当一个对象变得不可扩展时，也就不必再监听了）\n\n`Object.unobserve` 方法用来取消监听。\n```JavaScript\n\tObject.unobserve(o, observer);\n```\n\n**注意：** `Object.observe` 和 `Object.unobserve` 这两个方法不属于 ES6，而是属于 ES7 的一部分。\n\n#### 对象的扩展运算符\nES7 有一个提案，将 rest 参数/扩展运算符（...）引入对象。\n\n**注意：** `Rest` 参数的拷贝是浅拷贝，即如果一个键的值是复合类型的值（数组、对象、函数）、那么 `Rest` 参数拷贝的是这个值的引用，而不是这个值的副本。\n\n<a name=\"Symbol\"></a>\n### [Symbol](#catalog)\nES6 引入了一种新的**原始数据类型** `Symbol`，表示独一无二的值。它是 JavaScript 语言的第七种数据类型，前六种是：`Undefined`、`Null`、布尔值（`Boolean`）、字符串（`String`）、数值（`Number`）、对象（`Object`）。\n\n**注意：**`Symbol` 函数前不能使用 new 命令，否则会报错。这是因为生成的 `Symbol` 是一个原始类型的值，不是对象。也就是说，由于 `Symbol` 值不是对象，所以不能添加属性。基本上，它是一种类似于字符串的数据类型。\n\n`Symbol` 函数可以接受一个字符串作为参数，表示对 `Symbol` 实例的描述，主要是为了在控制台显示，或者转为字符串时，比较容易区分。\n\n**注意：** `Symbol`函数的参数只是表示对当前 Symbol 值的描述，因此相同参数的 Symbol 函数的返回值是不相等的。\n```JavaScript\n\t// 没有参数的情况\n\tvar s1 = Symbol();\n\tvar s2 = Symbol();\n\t\n\ts1 === s2 // false\n\t\n\t// 有参数的情况\n\tvar s1 = Symbol(\"foo\");\n\tvar s2 = Symbol(\"foo\");\n\t\n\ts1 === s2 // false\n```\n\n`Symbol` 值不能与其他类型的值进行运算，会报错，但是，`Symbol` 值可以显式转为字符串。\n```JavaScript\n\tvar sym = Symbol('My symbol');\n\t\n\t\"your symbol is \" + sym\n\t// TypeError: can't convert symbol to string\n\t`your symbol is ${sym}`\n\t// TypeError: can't convert symbol to string\n\n\tString(sym) // 'Symbol(My symbol)'\n\tsym.toString() // 'Symbol(My symbol)'\n```\n\n由于每一个 `Symbol` 值都是不相等的，这意味着 `Symbol` 值可以作为标识符，用于对象的属性名，就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用，能防止某一个键被不小心改写或覆盖。\n```JavaScript\n\tvar mySymbol = Symbol();\n\t\n\t// 第一种写法\n\tvar a = {};\n\ta[mySymbol] = 'Hello!';\n\t\n\t// 第二种写法\n\tvar a = {\n\t  [mySymbol]: 'Hello!'\n\t};\n\t\n\t// 第三种写法\n\tvar a = {};\n\tObject.defineProperty(a, mySymbol, { value: 'Hello!' });\n\t\n\t// 以上写法都得到同样结果\n\ta[mySymbol] // \"Hello!\"\n```\n\n**注意：** `Symbol` 值作为对象属性名时，不能用点运算符。为点运算符后面总是字符串，所以不会读取标识名所指代的那个值。同理，在对象的内部，使用 `Symbol` 值定义属性时，`Symbol` 值必须放在方括号之中。\n\n`Symbol` 作为属性名，该属性不会出现在 `for...in`、`for...of` 循环中，也不会被 `Object.keys()`、`Object.getOwnPropertyNames()` 返回。但是，它也不是私有属性，有一个 `Object.getOwnPropertySymbols` 方法，可以获取指定对象的所有 `Symbol` 属性名。\n\n`Object.getOwnPropertySymbols` 方法返回一个数组，成员是当前对象的所有用作属性名的 `Symbol` 值。\n```JavaScript\n\tvar obj = {};\n\t\n\tvar foo = Symbol(\"foo\");\n\t\n\tObject.defineProperty(obj, foo, {\n\t  value: \"foobar\",\n\t});\n\t\n\tfor (var i in obj) {\n\t  console.log(i); // 无输出\n\t}\n\t\n\tObject.getOwnPropertyNames(obj)\n\t// []\n\t\n\tObject.getOwnPropertySymbols(obj)\n\t// [Symbol(foo)]\n```\n\n**注意：** `Reflect.ownKeys` 方法可以返回所有类型的键名，包括常规键名和 `Symbol` 键名。\n\n#### Symbol.for()，Symbol.keyFor()\n`Symbol.for` 接受一个字符串作为参数，然后搜索有没有以该参数作为名称的 `Symbol` 值。如果有，就返回这个 `Symbol` 值，否则就新建并返回一个以该字符串为名称的 `Symbol` 值。\n\n**注意：** `Symbol.for()` 与 `Symbol()` 这两种写法，都会生成新的 `Symbol`。它们的区别是，前者会被登记在全局环境中供搜索，后者不会。`Symbol.for()` 不会每次调用就返回一个新的 `Symbol` 类型的值，而是会先检查给定的key是否已经存在，如果不存在才会新建一个值。\n```JavaScript\n\tSymbol.for(\"bar\") === Symbol.for(\"bar\")\n\t// true\n\n\tSymbol(\"bar\") === Symbol(\"bar\")\n\t// false\n```\n\n`Symbol.keyFor` 方法返回一个已登记的 `Symbol` 类型值的 `key`。\n```JavaScript\n\tvar s1 = Symbol.for(\"foo\");\n\tSymbol.keyFor(s1) // \"foo\"\n\t\n\tvar s2 = Symbol(\"foo\");\n\tSymbol.keyFor(s2) // undefined\n```\n#### 内置的 `Symbol` 值\n除了定义自己使用的 Symbol 值以外，ES6 还提供了 11 个内置的 Symbol 值，指向语言内部使用的方法。\n\n1. **Symbol.hasInstance：**对象的 `Symbol.hasInstance` 属性，指向一个内部方法。该对象使用 `instanceof` 运算符时，会调用这个方法，判断该对象是否为某个构造函数的实例。\n2. **Symbol.isConcatSpreadable:**对象的 `Symbol.isConcatSpreadable` 属性等于一个布尔值，表示该对象使用 `Array.prototype.concat()` 时，是否可以展开。\n3. **Symbol.species**对象的 `Symbol.species` 属性，指向一个方法。该对象作为构造函数创造实例时，会调用这个方法。\n4. **Symbol.match**对象的 `Symbol.match` 属性，指向一个函数。当执行 str.match(myObject) 时，如果该属性存在，会调用它，返回该方法的返回值。\n5. **Symbol.replace**对象的 `Symbol.replace` 属性，指向一个方法，当该对象被 String.prototype.replace 方法调用时，会返回该方法的返回值。\n6. **Symbol.search**对象的 `Symbol.search` 属性，指向一个方法，当该对象被 String.prototype.search 方法调用时，会返回该方法的返回值。\n7. **Symbol.split**对象的 `Symbol.split` 属性，指向一个方法，当该对象被 String.prototype.split 方法调用时，会返回该方法的返回值。\n8. **Symbol.iterator**对象的 `Symbol.iterator` 属性，指向该对象的默认遍历器方法，即该对象进行 `for...of` 循环时，会调用这个方法，返回该对象的默认遍历器。\n9. **Symbol.toPrimitive**对象的 `Symbol.toPrimitive` 属性，指向一个方法。该对象被转为原始类型的值时，会调用这个方法，返回该对象对应的原始类型值。\n10. **Symbol.toStringTag**对象的 `Symbol.toStringTag` 属性，指向一个方法。在该对象上面调用 Object.prototype.toString 方法时，如果这个属性存在，它的返回值会出现在 toString 方法返回的字符串之中，表示对象的类型。\n11. **Symbol.unscopables**对象的 `Symbol.unscopables` 属性，指向一个对象。该对象指定了使用 `with` 关键字时，哪些属性会被 `with` 环境排除。\n\n<a name=\"Proxy\"></a>\n### [Proxy和Reflect](#catalog)\n#### Proxy\n`Proxy` 用于修改某些操作的默认行为，等同于在语言层面做出修改，所以属于一种“元编程”（meta programming），即对编程语言进行编程。\n\n`Proxy` 可以理解成，在目标对象之前架设一层“拦截”，外界对该对象的访问，都必须先通过这层拦截，因此提供了一种机制，可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理，用在这里表示由它来“代理”某些操作，可以译为“代理器”。\n\nES6原生提供 `Proxy` 构造函数，用来生成 `Proxy` 实例。\n```JavaScript\n\tvar proxy = new Proxy(target, handler)\n```\n\n`new Proxy()` 表示生成一个 `Proxy` 实例，`target` 参数表示所要拦截的目标对象，`handler` 参数也是一个对象，用来定制拦截行为。\n\n`Proxy` 实例也可以作为其他对象的原型对象。\n`Proxy` 支持设置以下拦截操作：\n\n1. **get(target, propKey[, receiver])：**拦截对象属性的读取，返回类型不限。最后一个参数 receiver 可选，当 target 对象设置了 propKey 属性的 get 函数时，receiver 对象会绑定 get 函数的 this 对象。\n2. **set(target, propKey, value[, receiver])：**拦截对象属性的设置，返回一个布尔值。  \n3. **has(target, propKey)：**拦截 `in` 的操作，返回一个布尔值。\n4. **deleteProperty(target, propKey)：**拦截 `delete` 的操作，返回一个布尔值。\n5. **enumerate(target)：**拦截 `for` (var x in proxy)，返回一个遍历器。\n6. **hasOwn(target, propKey)：**拦截 `hasOwnProperty` 的操作，返回一个布尔值。\n7. **ownKeys(target)：**拦截 `Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`，返回一个数组。\n8. **getOwnPropertyDescriptor(target, propKey)：**拦截 `Object.getOwnPropertyDescriptor(proxy, propKey)`，返回属性的描述对象。\n9. **defineProperty(target, propKey, propDesc)：**拦截 `Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`，返回一个布尔值。\n10. **preventExtensions(target)：**拦截 `Object.preventExtensions(proxy)`，返回一个布尔值。\n11. **getPrototypeOf(target)：**拦截 `Object.getPrototypeOf(proxy)`，返回一个对象。\n12. **isExtensible(target)：**拦截 `Object.isExtensible(proxy)`，返回一个布尔值。\n13. **setPrototypeOf(target, proto)：**拦截 `Object.setPrototypeOf(proxy, proto)`，返回一个布尔值。\n14. **apply(target, object, args)：**目标对象是函数，拦截 `Proxy` 实例作为函数调用的操作。\n15. **construct(target, args, proxy)：**拦截 `Proxy` 实例作为构造函数调用的操作。\n\n```JavaScript\n\tvar handler = {\n\t  get: function(target, name) {\n\t    if (name === 'prototype') return Object.prototype;\n\t    return 'Hello, '+ name;\n\t  },\n\t  apply: function(target, thisBinding, args) { return args[0]; },\n\t  construct: function(target, args) { return args[1]; }\n\t};\n\t\n\tvar fproxy = new Proxy(function(x,y) {\n\t  return x+y;\n\t},  handler);\n\t\n\tfproxy(1,2); // 1\n\tnew fproxy(1,2); // 2\n\tfproxy.prototype; // Object.prototype\n\tfproxy.foo; // 'Hello, foo'\n```\n\n**Example:**\n \n#### get()\n```JavaScript\n\tvar person = {\n\t  name: \"张三\"\n\t};\n\t\n\tvar proxy = new Proxy(person, {\n\t  get: function(target, property) {\n\t    if (property in target) {\n\t      return target[property];\n\t    } else {\n\t      throw new ReferenceError(\"Property \\\"\" + property + \"\\\" does not exist.\");\n\t    }\n\t  }\n\t});\n\t\n\tproxy.name // \"张三\"\n\tproxy.age // 抛出一个错误\n```\n\n上面代码表示，如果访问目标对象不存在的属性，会抛出一个错误。如果没有这个拦截函数，访问不存在的属性，只会返回 undefined。\n\n#### set()\n```JavaScript\n\tlet validator = {\n\t  set: function(obj, prop, value) {\n\t    if (prop === 'age') {\n\t      if (!Number.isInteger(value)) {\n\t        throw new TypeError('The age is not an integer');\n\t      }\n\t      if (value > 200) {\n\t        throw new RangeError('The age seems invalid');\n\t      }\n\t    }\n\t\n\t    // 对于age以外的属性，直接保存\n\t    obj[prop] = value;\n\t  }\n\t};\n\t\n\tlet person = new Proxy({}, validator);\n\t\n\tperson.age = 100;\n\t\n\tperson.age // 100\n\tperson.age = 'young' // 报错\n\tperson.age = 300 // 报错\n```\n\n上面代码中，由于设置了存值函数 set，任何不符合要求的 age 属性赋值，都会抛出一个错误。\n\n```JavaScript\n\tvar handler = {\n\t  get (target, key) {\n\t    invariant(key, 'get');\n\t    return target[key];\n\t  },\n\t  set (target, key, value) {\n\t    invariant(key, 'set');\n\t    return true;\n\t  }\n\t}\n\tfunction invariant (key, action) {\n\t  if (key[0] === '_') {\n\t    throw new Error(`Invalid attempt to ${action} private \"${key}\" property`);\n\t  }\n\t}\n\tvar target = {};\n\tvar proxy = new Proxy(target, handler);\n\tproxy._prop\n\t// Error: Invalid attempt to get private \"_prop\" property\n\tproxy._prop = 'c'\n\t// Error: Invalid attempt to set private \"_prop\" property\n```\n\n上面代码中，只要读写的属性名的第一个字符是下划线，一律抛错，从而达到禁止读写内部属性的目的。\n\n更多例子，请移步[原著](http://es6.ruanyifeng.com/#docs/proxy)。\n\n#### Proxy.revocable()\n\n`Proxy.revocable` 方法返回一个对象，该对象的 proxy 属性是 `Proxy` 实例，revoke 属性是一个函数，可以取消 `Proxy` 实例。\n```JavaScript\n\tlet target = {};\n\tlet handler = {};\n\t\n\tlet {proxy, revoke} = Proxy.revocable(target, handler);\n\t\n\tproxy.foo = 123;\n\tproxy.foo // 123\n\t\n\trevoke();\n\tproxy.foo // TypeError: Revoked\n```\n\n#### Reflect\n`Reflect` 是 ES6 新增的 API 用来操作对象，将 `Object` 对象的一些明显属于语言层面的方法，放到 `Reflect` 对象上，使 `Object` 操作都变成函数行为。\n\n`Reflect` 对象的方法还与 `Proxy` 对象的方法一一对应，只要是 `Proxy` 对象的方法，就能在 `Reflect` 对象上找到对应的方法。这就让 `Proxy` 对象可以方便地调用对应的 `Reflect` 方法，完成默认行为，作为修改行为的基础。也就是说，不管 `Proxy` 怎么修改默认行为，你总可以在 `Reflect` 上获取默认行为。\n\n```JavaScript\n\tvar loggedObj = new Proxy(obj, {\n\t  get: function(target, name) {\n\t    console.log(\"get\", target, name);\n\t    return Reflect.get(target, name);\n\t  }\n\t});\n\n\tProxy(target, {\n\t  set: function(target, name, value, receiver) {\n\t    var success = Reflect.set(target,name, value, receiver);\n\t    if (success) {\n\t      log('property ' + name + ' on ' + target + ' set to ' + value);\n\t    }\n\t    return success;\n\t  }\n\t});\n```\n\n#### Object，Proxy 和 Reflect 对象对照表\n\n| Object | Reflect | Proxy |\n| ------ | ------- | ----- |\n| Object.apply(target,thisArg,args) | Reflect.apply(target,thisArg,args) | apply(target, object, args) |\n| Object.construct(target,args) | Reflect.construct(target,args) | construct(target, args) |\n| Object.defineProperty(target, name, desc) | Reflect.defineProperty(target,name,desc) | defineProperty(target, propKey, propDesc) |\n| Object.deleteProperty(target,name) | Reflect.deleteProperty(target,name) | deleteProperty(target, propKey) |\n| Object.enumerate(target) | Reflect.enumerate(target) | enumerate(target) |\n| Object.freeze(target) | Reflect.freeze(target) | - |\n| Object.get(target,name) | Reflect.get(target,name[, receiver]) | get(target, propKey[, receiver]) |\n| Object.getOwnPropertyDescriptor(target,name) | Reflect.getOwnPropertyDescriptor(target,name) | getOwnPropertyDescriptor(target, propKey) |\n| Object.getOwnPropertyNames(target) | Reflect.getOwnPropertyNames(target) | ownKeys(target) |\n| Object.getPrototypeOf(target) | Reflect.getPrototypeOf(target) | getPrototypeOf(target) |\n| Object.has(target,name) | Reflect.has(target,name) | has(target, propKey) |\n| Object.hasOwnProperty(target,name) | Reflect.hasOwnProperty(target,name) | hasOwn(target, propKey) |\n| Object.isExtensible(target) | Reflect.isExtensible(target) | isExtensible(target) |\n| Object.isFrozen(target) | Reflect.isFrozen(target) | - |\n| Object.isSealed(target) | Reflect.isSealed(target) | - |\n| Object.keys(target) | Reflect.keys(target) | ownKeys(target) |\n| Object.preventExtensions(target) | Reflect.preventExtensions(target) | preventExtensions(target) |\n| Object.seal(target) | Reflect.seal(target) | - |\n| Object.set(target,name,value) | Reflect.set(target,name,value[, receiver]) | set(target, propKey, value[, receiver]) |\n| Object.setPrototypeOf(target, prototype) | Reflect.setPrototypeOf(target, prototype) | setPrototypeOf(target, proto) |\n\n<a name=\"BinaryArray\"></a>\n### [二进制数组](#catalog)\n二进制数组（`ArrayBuffer` 对象、`TypedArray` 视图和`DataView` 视图）是 JavaScript 操作二进制数据的一个接口。这些对象早就存在，属于独立的规格，ES6 将它们纳入了 ECMAScript 规格，并且增加了新的方法。\n\n这个接口的原始设计目的，与 WebGL 项目有关。所谓 WebGL，就是指浏览器与显卡之间的通信接口，为了满足 JavaScript 与显卡之间大量的、实时的数据交换，它们之间的数据通信必须是二进制的，而不能是传统的文本格式。文本格式传递一个 32 位整数，两端的 JavaScript 脚本与显卡都要进行格式转化，将非常耗时。这时要是存在一种机制，可以像 C 语言那样，直接操作字节，将 4 个字节的 32 位整数，以二进制形式原封不动地送入显卡，脚本的性能就会大幅提升。\n\n二进制数组就是在这种背景下诞生的。它允许开发者以数组下标的形式，直接操作内存，大大增强了 JavaScript 处理二进制数据的能力，使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信。\n\n二进制数组由三类对象组成。\n\n1. **`ArrayBuffer` 对象：**代表内存之中的一段二进制数据，可以通过“视图”进行操作。“视图”部署了数组接口，这意味着，可以用数组的方法操作内存。\n\n2. **`TypedArray` 视图：**共包括 9 种类型的视图，比如 Uint8Array（无符号8位整数）数组视图, Int16Array（16位整数）数组视图, Float32Array（32位浮点数）数组视图等等。\n\n3. **`DataView` 视图：**可以自定义复合格式的视图，比如第一个字节是Uint8（无符号8位整数）、第二、三个字节是Int16（16位整数）、第四个字节开始是Float32（32位浮点数）等等，此外还可以自定义字节序。\n\n简单说，`ArrayBuffer` 对象代表原始的二进制数据，`TypedArray` 视图用来读写简单类型的二进制数据，`DataView` 视图用来读写复杂类型的二进制数据。`TypedArray` 视图支持的数据类型一共有 9 种（DataView 视图支持除 Uint8C 以外的其他 8 种）。\n\n| 数据类型 | 字节长度 | 含义 | 对应的C语言类型 |\n| ------- | ------- | ---- | ------------- |\n| Int8\t  | 1       | 8位带符号整数   \t         | signed char   |\n| Uint8\t  | 1       | 8位不带符号整数\t             | unsigned char |\n| Uint8C  | 1       | 8位不带符号整数（自动过滤溢出）| unsigned char |\n| Int16\t  | 2       | 16位带符号整数\t             | short         |\n| Uint16  |\t2       | 16位不带符号整数   \t         | unsigned short|\n| Int32   |\t4       | 32位带符号整数\t             | int           |\n| Uint32  |\t4       | 32位不带符号的整数\t         | unsigned int  |\n| Float32 |\t4       | 32位浮点数                  | float         |\n| Float64 |\t8       | 64位浮点数                  | double        |\n\n\n#### 1. ArrayBuffer对象\n`ArrayBuffer` 对象代表储存二进制数据的一段内存，它不能直接读写，只能通过视图（`TypedArray` 视图和 `DataView` 视图)来读写，视图的作用是以指定格式解读二进制数据。\n\n`ArrayBuffer` 的构造函数会分配一段可以存放数据的连续内存区域，参数是所需要的内存大小（单位字节），每个字节的值默认都是 0。\n\n```JavaScript\n\tvar buf = new ArrayBuffer(32);\n\t// DataView\n\tvar dataView = new DataView(buf);\n\tdataView.getUint8(0) // 0\n\n\t// TypedArray\n\tvar x1 = new Int32Array(buffer);\n\tx1[0] = 1;\n\tvar x2 = new Uint8Array(buffer);\n\tx2[0]  = 2;\n\t\n\tx1[0] // 2\n```\n\n`ArrayBuffer` 实例的 `byteLength` 属性，返回所分配的内存区域的字节长度。如果要分配的内存区域很大，有可能分配失败（因为没有那么多的连续空余内存），所以有必要检查是否分配成功。\n\n```JavaScript\n\tvar buffer = new ArrayBuffer(32);\n\tbuffer.byteLength\n\t// 32\n\t\n\tif (buffer.byteLength === n) {\n\t  // 成功\n\t} else {\n\t  // 失败\n\t}\n```\n\n`ArrayBuffer` 实例有一个 `slice` 方法，允许将内存区域的一部分，拷贝生成一个新的 `ArrayBuffer` 对象。`slice` 方法接受两个参数，第一个参数表示拷贝开始的字节序号（含该字节），第二个参数表示拷贝截止的字节序号（不含该字节）。如果省略第二个参数，则默认到原 ArrayBuffer 对象的结尾。\n\n**注意：**除了 `slice` 方法，`ArrayBuffer` 对象不提供任何直接读写内存的方法，只允许在其上方建立视图，然后通过视图读写。\n\n`ArrayBuffer` 有一个静态方法 `isView`，返回一个布尔值，表示参数是否为 `ArrayBuffer` 的视图实例。这个方法大致相当于判断参数，是否为 `TypedArray` 实例或 `DataView` 实例。\n\n#### 2. TypedArray视图\n`ArrayBuffer` 对象作为内存区域，可以存放多种类型的数据。同一段内存，不同数据有不同的解读方式，这就叫做“视图”（`view`）。`TypedArray` 视图共有包括9种类型（参见第一节表格），每一种类型的视图都是一种构造函数。由这9个构造函数生成的数组，统称为 `TypedArray` 视图，它们很像普通数组，都有 `length` 属性，都能用方括号运算符（[]）获取单个元素，所有数组的方法，在它们上面都能使用。**不同点**有一下几处：\n\n* `TypedArray` 数组的所有成员，都是同一种类型。\n* `TypedArray` 数组的成员是连续的，不会有空位。\n* `TypedArray` 数组成员的默认值为0。\n* `TypedArray` 数组只是一层视图，本身不储存数据，它的数据都储存在底层的 `ArrayBuffer` 对象之中，要获取底层对象必须使用 `buffer` 属性。\n\n**注意：** `TypedArray` 数组没有 `concat` 方法。\n\n每一种视图的构造函数，都有一个 `BYTES_PER_ELEMENT` 属性，表示这种数据类型占据的字节数。\n\n#### 3. DataView 视图\n如果一段数据包括多种类型（比如服务器传来的 HTTP 数据），这时除了建立 `ArrayBuffer` 对象的复合视图以外，还可以通过 `DataView` 视图进行操作。\n\n`DataView` 视图提供更多操作选项，而且支持设定字节序。本来，在设计目的上，`ArrayBuffer` 对象的各种 `TypedArray` 视图，是用来向网卡、声卡之类的本机设备传送数据，所以使用本机的字节序就可以了；而 `DataView` 视图的设计目的，是用来处理网络设备传来的数据，所以大端字节序或小端字节序是可以自行设定的。\n\n```JavaScript\n\tDataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);\n```\n\n`DataView` 实例有以下属性，含义与 `TypedArray` 实例的同名方法相同。\n\n* `DataView.prototype.buffer`：返回对应的 `ArrayBuffer` 对象\n* `DataView.prototype.byteLength`：返回占据的内存字节长度\n* `DataView.prototype.byteOffset`：返回当前视图从对应的 `ArrayBuffer` 对象的开始字节\n\n#### 4. 应用\n很多浏览器操作的API，用到了二进制数组操作二进制数据，下面是其中的几个。\n\n- File API\n- XMLHttpRequest\n- Fetch API\n- Canvas\n- WebSockets\n例子，请移步[原著](http://es6.ruanyifeng.com/#docs/arraybuffer)。\n\n<a name=\"SetMap\"></a>\n### [Set & Map](#catalog)\n#### 1. Set\n\nES6 提供了新的数据结构 `Set`。它类似于数组，但是成员的值都是唯一的，没有重复的值。\n\n**注意：**`Set` 内部判断两个值是否不同，使用的算法类似于精确相等运算符（===），这意味着，两个对象总是不相等的，唯一的例外是NaN，即 `Set` 中至多只有一个 NaN。\n```JavaScript\n\tvar set = new Set([1, 2, 3, 4, 4])\n\t[...set]\n\t// [1, 2, 3, 4]\n\t\n\tvar items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);\n\titems.size // 5\n```\n\n`Set` 结构的实例有以下属性。\n\n* `Set.prototype.constructor`：构造函数，默认就是 Set 函数。\n* `Set.prototype.size`：返回 Set 实例的成员总数。\n\n`Set` 实例的方法分为两大类：操作方法（用于操作数据）和遍历方法（用于遍历成员）。\n\n**操作方法:**\n\n* `add(value)`：添加某个值，返回 Set 结构本身。\n* `delete(value)`：删除某个值，返回一个布尔值，表示删除是否成功。\n* `has(value)`：返回一个布尔值，表示该值是否为 Set 的成员。\n* `clear()`：清除所有成员，没有返回值。\n```JavaScript\n\ts.add(1).add(2).add(2);\n\t\n\ts.size // 2\n\t\n\ts.has(1) // true\n\ts.has(2) // true\n\ts.has(3) // false\n\t\n\ts.delete(2);\n\ts.has(2) // false\n```\n\n由于 `Array.from` 方法可以将 `Set` 结构转为数组，那么这就提供了一种去除数组的重复元素的方法。\n```JavaScript\n\tvar items = new Set([1, 2, 3, 4, 5]);\n\tvar array = Array.from(items);\n\t\n\tfunction dedupe(array) {\n\t  return Array.from(new Set(array));\n\t}\n\t\n\tdedupe([1,1,2,3]) // [1, 2, 3]\n```\n\n##### 遍历方法:\n\n* `keys()`：返回一个键名的遍历器\n* `values()`：返回一个键值的遍历器\n* `entries()`：返回一个键值对的遍历器\n* `forEach()`：使用回调函数遍历每个成员\n\n由于 `Set` 结构没有键名，只有键值（或者说键名和键值是同一个值），所以 `keys` 方法和 `values` 方法的行为完全一致。\n```JavaScript\n\tlet set = new Set(['red', 'green', 'blue']);\n\t\n\tfor ( let item of set.keys() ){\n\t  console.log(item);\n\t}\n\t// red\n\t// green\n\t// blue\n\t\n\tfor ( let item of set.values() ){\n\t  console.log(item);\n\t}\n\t// red\n\t// green\n\t// blue\n\t\n\tfor ( let item of set.entries() ){\n\t  console.log(item);\n\t}\n\t// [\"red\", \"red\"]\n\t// [\"green\", \"green\"]\n\t// [\"blue\", \"blue\"]\n```\n\n数组的 map 和 filter 方法也可以用于 Set 了，因此使用 Set，可以很容易地实现并集（Union）和交集（Intersect）。\n```JavaScript\n\tlet a = new Set([1, 2, 3]);\n\tlet b = new Set([4, 3, 2]);\n\t\n\tlet union = new Set([...a, ...b]);\n\t// [1, 2, 3, 4]\n\t\n\tlet intersect = new Set([...a].filter(x => b.has(x)));\n\t// [2, 3]\n````\n\n#### 2. WeakSet\n`WeakSet` 结构与 `Set` 类似，也是不重复的值的集合。但是，它与 `Set` 有两个区别。\n\n首先，`WeakSet`的成员只能是**对象**，而不能是其他类型的值。\n\n其次，`WeakSet`中的对象都是弱引用，即垃圾回收机制不考虑 `WeakSet` 对该对象的引用，也就是说，如果其他对象都不再引用该对象，那么垃圾回收机制会自动回收该对象所占用的内存，不考虑该对象还存在于 `WeakSet` 之中。这个特点意味着，**无法引用 `WeakSet` 的成员，因此 `WeakSet` 是不可遍历的。**\n\n`WeakSet` 结构有以下三个方法。\n\n* `WeakSet.prototype.add(value)`：向 `WeakSet` 实例添加一个新成员。\n* `WeakSet.prototype.delete(value)`：清除 `WeakSet` 实例的指定成员。\n* `WeakSet.prototype.has(value)`：返回一个布尔值，表示某个值是否在 `WeakSet` 实例之中。\n\n个人暂没有想到 `WeakSet` 的用法，书中介绍 `WeakSet` 可以用来储存DOM节点，而不用担心这些节点从文档移除时，会引发内存泄漏。\n\n#### 3. Map\nJavaScript 的对象（`Object`），本质上是键值对的集合（Hash 结构），但是只能用字符串当作键，而 `Map` 结构提供了“值—值”的对应。\n\n`Map` 结构的 `Key` 类型类似于 Set 结构（书中没有这样说，个人总结，例外请留言。）\n\n##### `Map`属性和方法：\n\n* `size`属性：返回 `Map` 结构的成员总数。\n* `has(key)`方法：方法返回一个布尔值，表示某个键是否在 `Map` 数据结构中。\n* `get(key)`方法：读取 key 对应的键值，如果找不到 key，返回 undefined。\n* `set(key, value)`方法：设置 key 所对应的键值，然后返回整个 `Map` 结构，为此可以采用链式写法。\n* `delete(key)`方法：删除某个 key，返回 true。如果删除失败，返回 false。\n* `clear()`方法：清除所有成员，没有返回值。\n* `keys()`方法：返回键名的遍历器。\n* `values()`方法：返回键值的遍历器。\n* `entries()`方法：返回所有成员的遍历器。\n* `forEach()`：遍历 `Map` 的所有成员。\n\n```JavaScript\n\tlet map = new Map([\n\t  [1, 'one'],\n\t  [2, 'two'],\n\t  [3, 'three'],\n\t]);\n\t\n\t[...map.keys()]\n\t// [1, 2, 3]\n\t\n\t[...map.values()]\n\t// ['one', 'two', 'three']\n\t\n\t[...map.entries()]\n\t// [[1,'one'], [2, 'two'], [3, 'three']]\n\t\n\t[...map]\n\t// [[1,'one'], [2, 'two'], [3, 'three']]\n```\n\n#### 4. WeakMap\n`WeakMap` 结构与 `Map` 结构基本类似，唯一的区别是它只接受对象作为键名（null除外），不接受其他类型的值作为键名，而且键名所指向的对象，不计入垃圾回收机制。\n\n应用场景参照 `WeakSet`。\n\n<a name=\"Iterator\"></a>\n### [Iterator](#catalog)\n#### Iterator\n由于 ES6 新加入 Set 和 Map 这两种数据属性，就有了 4 种不同的数据结构。**遍历器（Iterator）**就提供一个统一的接口为各种不同的数据结构提供统一的访问机制，任何数据结构只要部署 Iterator 接口，就可以完成遍历操作。\n\nIterator 的遍历主要通过指针对象，通过不断的调用 next 方法来遍历数据成员。\n\n#### 数据结构的默认 Iterator 接口\n\nES6 规定，默认的 Iterator 接口部署在数据结构的 `Symbol.iterator` 属性，或者说，一个数据结构只要具有 `Symbol.iterator` 属性，就可以认为是“可遍历的”（iterable）。调用 `Symbol.iterator` 方法，就会得到当前数据结构默认的遍历器生成函数。\n\n在 ES6 中，有三类数据结构原生具备 Iterator 接口：数组、某些类似数组的对象、Set 和 Map 结构。\n\n对象（Object）之所以没有默认部署 Iterator 接口，是因为对象的哪个属性先遍历，哪个属性后遍历是不确定的，需要开发者手动指定。本质上，遍历器是一种线性处理，对于任何非线性的数据结构，部署遍历器接口，就等于部署一种线性转换。\n\n#### 默认调用 Iterator 接口的场合\n1. 解构赋值\n2. 扩展运算符\n3. yield*（Generator章详述）\n4. 其他：由于数组的遍历会调用遍历器接口，所以任何接受数组作为参数的场合都调用了遍历器接口\n\n#### 遍历器对象的 return()，throw()\n遍历器对象除了具有 `next` 方法，还可以具有 `return` 方法和 `throw` 方法。如果你自己写遍历器生成函数，那么 `next` 方法是必须部署的，`return` 方法和 `throw` 方法是否部署是可选的。\n\n`return` 方法的使用场合是，如果 for...of 循环提前退出（通常是因为出错，或者有 break 语句或 continue 语句），就会调用 `return` 方法。如果一个对象在完成遍历前，需要清理或释放资源，就可以部署 `return` 方法。\n\n`throw` 方法主要是配合Generator函数使用，一般的遍历器对象用不到这个方法。请参阅《[Generator函数](#Generator)》一章。\n\n#### for...of循环\n\nES6 引入了 `for...of` 循环，作为遍历所有数据结构的统一的方法。一个数据结构只要部署了 `Symbol.iterator` 属性，就被视为具有 `iterator` 接口，就可以用 `for...of` 循环遍历它的成员。\n\n`for...of` 循环可以使用的范围包括数组、`Set` 和 `Map` 结构、某些类似数组的对象（比如 arguments 对象、DOM NodeList 对象）、`Generator` 对象，以及字符串。\n\n#### 与其他遍历语法的比较\n以数组为例，JavaScript 提供多种遍历语法。最原始的写法就是 for 循环。\n```JavaScript\n\tfor (var index = 0; index < myArray.length; index++) {\n\t  console.log(myArray[index]);\n\t}\n```\n这种写法比较麻烦，因此数组提供内置的 forEach 方法。\n```JavaScript\n\tmyArray.forEach(function (value) {\n\t  console.log(value);\n\t});\n```\n这种写法的问题在于，**无法中途跳出 forEach 循环，break 命令或 return 命令都不能奏效**。\n\n`for...in` 循环可以遍历数组的键名。\n```JavaScript\n\tfor (var index in myArray) {\n\t  console.log(myArray[index]);\n\t}\n```\n\n`for...in` 循环有几个缺点：\n\n1. 数组的键名是数字，但是 for...in 循环是以字符串作为键名“0”、“1”、“2”等等。\n2. for...in 循环不仅遍历数字键名，还会遍历手动添加的其他键，甚至包括原型链上的键。\n\n`for...of` 循环相比上面几种做法，有一些显著的优点。\n\n* 有着同 for...in 一样的简洁语法，但是没有 for...in 那些缺点。\n* 不同用于 forEach 方法，它可以与 break、continue 和 return 配合使用。\n* 提供了遍历所有数据结构的统一操作接口。\n\n```JavaScript\n\tfor (var n of fibonacci) {\n\t  if (n > 1000)\n\t    break;\n\t  console.log(n);\n\t}\n```\n\n<a name=\"Generator\"></a>\n### [Generator函数](#catalog)\n`Generator` 函数是 ES6 提供的一种异步编程解决方案，语法行为与传统函数完全不同。\n\n`Generator` 函数有多种理解角度。从语法上，首先可以把它理解成，`Generator` 函数是一个状态机，封装了多个内部状态。\n\n执行 `Generator` 函数会返回一个遍历器对象，也就是说，`Generator` 函数除了状态机，还是一个遍历器对象生成函数。返回的遍历器对象，可以依次遍历 `Generator` 函数内部的每一个状态。\n\n形式上，`Generator` 函数是一个普通函数，但是有两个特征。一是，`function` 命令与函数名之间有一个星号；二是，函数体内部使用 `yield` 语句，定义不同的内部状态。`Generator` 函数的调用方法与普通函数一样，也是在函数名后面加上一对圆括号。不同的是，调用 `Generator` 函数后，该函数并不执行，返回的也不是函数运行结果，而是一个指向内部状态的指针对象(即Iterator对象)。\n```JavaScript\n\tfunction* helloWorldGenerator() {\n\t  yield 'hello';\n\t  yield 'world';\n\t  return 'ending';\n\t}\n\t\n\tvar hw = helloWorldGenerator();\n\t\n\thw.next()\n\t// { value: 'hello', done: false }\n\t\n\thw.next()\n\t// { value: 'world', done: false }\n\t\n\thw.next()\n\t// { value: 'ending', done: true }\n\t\n\thw.next()\n\t// { value: undefined, done: true }\n```\n\n#### yield语句\n由于 `Generator` 函数返回的遍历器对象，只有调用 `next` 方法才会遍历下一个内部状态，所以其实提供了一种可以暂停执行的函数。`yield` 语句就是暂停标志。\n\n如果 `Generator` 函数不使用 `yield` 语句，这时就变成了一个单纯的暂缓执行函数。\n\n**注意：**\n\n* `yield` 语句不能用在普通函数中，否则会报错。\n* `yield` 语句如果用在一个表达式之中，必须放在圆括号里面。\n* `yield` 语句用作函数参数或赋值表达式的右边，可以不加括号。\n\n#### `next` 方法的参数\n`yield` 句本身没有返回值，或者说总是返回 undefined。`next` 方法可以带一个参数，该参数就会被当作上一个 `yield` 语句的返回值。\n\n```JavaScript\n\tfunction* foo(x) {\n\t  var y = 2 * (yield (x + 1));\n\t  var z = yield (y / 3);\n\t  return (x + y + z);\n\t}\n\t\n\tvar a = foo(5);\n\t\n\ta.next() // Object{value:6, done:false}\n\ta.next() // Object{value:NaN, done:false}\n\ta.next() // Object{value:NaN, done:false}\n\n\tvar it = foo(5);\n\t\n\tit.next()\n\t// { value:6, done:false }\n\tit.next(12)\n\t// { value:8, done:false }\n\tit.next(13)\n\t// { value:42, done:true }\n```\n\n#### for...of 循环\nfor...of 循环可以自动遍历 Generator 函数，且此时不再需要调用 `next` 方法。\n\n但有一点需要注意，一旦 `next` 方法的返回对象的 done 属性为 true，for...of 循环就会中止，且不包含该返回对象，即 `return` 语句的返回值是无法通过 for...of 循环返回的。\n```JavaScript\n\tfunction *foo() {\n\t  yield 1;\n\t  yield 2;\n\t  yield 3;\n\t  yield 4;\n\t  yield 5;\n\t  return 6;\n\t}\n\t\n\tfor (let v of foo()) {\n\t  console.log(v);\n\t}\n\t// 1 2 3 4 5\n```\n\n#### throw()\n`Generator` 函数返回的遍历器对象，都有一个 throw 方法，可以在函数体外抛出错误，然后在 `Generator` 函数体内捕获。\n\n如果 `Generator` 函数内部部署了 try...catch 代码块，那么遍历器的 throw 方法抛出的错误，不影响下一次遍历，否则遍历直接终止。\n```JavaScript\n\tvar g = function* () {\n\t  while (true) {\n\t    try {\n\t      yield;\n\t    } catch (e) {\n\t      if (e != 'a') throw e;\n\t      console.log('内部捕获', e);\n\t    }\n\t  }\n\t};\n\t\n\tvar i = g();\n\ti.next();\n\t\n\ttry {\n\t  i.throw('a');\n\t  i.throw('b');\n\t} catch (e) {\n\t  console.log('外部捕获', e);\n\t}\n\t// 内部捕获 a\n\t// 外部捕获 b\n```\n\n#### return()\n`Generator` 函数返回的遍历器对象，还有一个 `return` 方法，可以返回给定的值，并且终结遍历 `Generator` 函数。如果 `Generator` 函数内部有 `try...finally` 代码块，那么 `return` 方法会推迟到 `finally` 代码块执行完再执行。\n```JavaScript\n\tfunction* numbers () {\n\t  yield 1;\n\t  try {\n\t    yield 2;\n\t    yield 3;\n\t  } finally {\n\t    yield 4;\n\t    yield 5;\n\t  }\n\t  yield 6;\n\t}\n\tvar g = numbers()\n\tg.next() // { done: false, value: 1 }\n\tg.next() // { done: false, value: 2 }\n\tg.return(7) // { done: false, value: 4 }\n\tg.next() // { done: false, value: 5 }\n\tg.next() // { done: true, value: 7 }\n```\n\n#### yield*\n如果在 `Generater` 函数内部，调用另一个 `Generator` 函数，默认情况下是没有效果的。这时需要使用 yield* 来执行另一个 `Generator` 函数。\n```JavaScript\n\tfunction* foo() {\n\t  yield 'a';\n\t  yield 'b';\n\t}\n\t\n\tfunction* bar() {\n\t  yield 'x';\n\t  foo();\n\t  yield 'y';\n\t}\n\t\n\tfor (let v of bar()){\n\t  console.log(v);\n\t}\n\t// \"x\"\n\t// \"y\"\n\n\tfunction* bar() {\n\t  yield 'x';\n\t  yield* foo();\n\t  yield 'y';\n\t}\n\t\n\tfor (let v of bar()){\n\t  console.log(v);\n\t}\n\t// \"x\"\n\t// \"a\"\n\t// \"b\"\n\t// \"y\"\n```\n\n#### 应用\n1. 异步操作的同步化  \n`Generator` 函数的暂停执行的效果，意味着可以把异步操作写在 `yield` 语句里面，等到调用 `next` 方法时再往后执行。\n2. 控制流管理  \n多个任务按顺序一个接一个执行时，`yield` 语句可以按顺序排列。多个任务需要并列执行时（比如只有 A 任务和 B 任务都执行完，才能执行 C 任务），可以采用数组的写法。\n3. 部署 iterator 接口\n利用 `Generator` 函数，可以在任意对象上部署 `iterator` 接口。\n4. 作为数据结构  \n`Generator` 可以看作是数据结构，更确切地说，可以看作是一个数组结构，因为 `Generator` 函数可以返回一系列的值，这意味着它可以对任意表达式，提供类似数组的接口。\n\n<a name=\"Promise\"></a>\n### [Promise对象](#catalog)\nES6 正式将 `Promise` 写进了语言标准，统一了用法，原生提供了 `Promise` 对象。\n\n所谓 `Promise`，就是一个对象，用来传递异步操作的消息。它代表了某个未来才会知道结果的事件（通常是一个异步操作），并且这个事件提供统一的 API，可供进一步处理。\n\n`Promise` 对象有以下两个特点。\n（1）对象的状态不受外界影响。Promise 对象代表一个异步操作，有三种状态：`Pending`（进行中）、`Resolved`（已完成，又称Fulfilled）和`Rejected`（已失败）。只有异步操作的结果，可以决定当前是哪一种状态，任何其他操作都无法改变这个状态。\n（2）一旦状态改变，就不会再变，任何时候都可以得到这个结果。`Promise` 对象的状态改变，只有两种可能：从 `Pending` 变为 `Resolved` 和从 `Pending` 变为 `Rejected`。只要这两种情况发生，状态就凝固了，不会再变了，会一直保持这个结果。\n\nES6 规定，`Promise` 对象是一个构造函数，用来生成 `Promise` 实例。`Promise` 构造函数接受一个函数作为参数，该函数的两个参数分别是 `resolve` 和 `reject`。它们是两个函数，由 JavaScript 引擎提供，不用自己部署。\n\n`resolve` 函数的作用是，将 `Promise` 对象的状态从 `Pending` 变为 `Resolved`，在异步操作成功时调用，并将异步操作的结果，作为参数传递出去；`reject` 函数的作用是，将 `Promise` 对象的状态从 `Pending` 变为 `Rejected`，在异步操作失败时调用，并将异步操作报出的错误，作为参数传递出去。\n\n`Promise` 实例生成以后，可以用 `then` 方法分别指定 `Resolved` 状态和 `Reject` 状态的回调函数。\n```JavaScript\n\tvar promise = new Promise(function(resolve, reject) {\n\t  // ... some code\n\t\n\t  if (/* 异步操作成功 */){\n\t    resolve(value);\n\t  } else {\n\t    reject(error);\n\t  }\n\t});\n\t\n\tpromise.then(function(value) {\n\t  // success\n\t}, function(value) {\n\t  // failure\n\t});\n```\n\n`Promise` 实例具有 `then` 方法，`then` 方法返回的是一个新的 `Promise` 实例，即可以采用链式写法。\n```JavaScript\n\tgetJSON(\"/post/1.json\").then(\n\t  post => getJSON(post.commentURL)\n\t).then(\n\t  comments => console.log(\"Resolved: \", comments),\n\t  err => console.log(\"Rejected: \", err)\n\t);\n```\n\n`Promise` 实例还提供 catch 方法，即是 .then(null, rejection) 的别名，用于指定发生错误时的回调函数。尽量不要在 `then` 方法里面定义 `Rejection` 状态的回调函数（即 `then` 的第二个参数），总是使用 `catch` 方法，这可以使得错误可以被统一处理。\n\n由于 `catch` 方法返回的还是一个 `Promise` 对象，因此后面还可以接着调用 `then` 方法。\n\n`Promise.all` 方法用于将多个 `Promise` 实例，包装成一个新的 `Promise` 实例。\n\n`Promise.all` 方法接受一个具有 iterator 接口的对象作为参数，且对象的每个成员都是 `Promise` 实例，如果对象不是 `Promise` 实例，会先调用 `Promise.resolve` 方法将对象转换为 `Promise` 实例。\n\n`Promise.race` 方法同 `Promise.all` 方法是将多个 Promise 实例，包装成一个新的 `Promise` 实例。它与 `Promise.all` 方法不同之处在于，`Promise.race` 只要一个 `Promise` 实例的状态变化，新的包装实例的状态就随之变化（即如方法名，谁跑的快听谁的）。\n\n`Promise.resolve()` 方法用来将现有对象转为 `Promise` 对象。\n\n**注意：**如果 `Promise.resolve` 方法的参数，不是具有 `then` 方法的对象（又称 `thenable` 对象），则返回一个新的 `Promise` 对象，且它的状态为 `Resolved`，基于 `Promise` 的回调函数将被立即执行。\n\n`Promise.reject()` 方法也会返回一个新的 `Promise` 实例，该实例的状态为 `rejected`。`Promise.reject` 方法的参数 reason，会被传递给实例的回调函数。\n\n<a name=\"Async\"></a>\n### [异步操作和Async函数](#catalog)\n所谓\"异步\"，简单说就是一个任务分成两段，先执行第一段，然后转而执行其他任务，等做好了准备，再回过头执行第二段。\n\nES6 诞生以前，异步编程的方法，大概有下面四种。\n\n* 回调函数\n* 事件监听\n* 发布/订阅\n* Promise 对象\n\nES6 通过 `Generator` 函数将 JavaScript 异步编程带入了一个全新的阶段（原理已在[Generator](#Generator)章节中阐述，更多案例移步[原著](http://es6.ruanyifeng.com/#docs/async)），ES7 提出的 `Async` 函数更是在 ES6 的基础上的升级。\n\n`async` 函数是什么？原著作者认为 `async` 函数就是 `Generator` 函数的语法糖。通过一个读取文件的案例来看看它们之间的区别：\n```JavaScript\n\tvar fs = require('fs');\n\t\n\tvar readFile = function (fileName){\n\t  return new Promise(function (resolve, reject){\n\t    fs.readFile(fileName, function(error, data){\n\t      if (error) reject(error);\n\t      resolve(data);\n\t    });\n\t  });\n\t};\n\t\n\t// use Generator\n\tvar gen = function* (){\n\t  var f1 = yield readFile('/etc/fstab');\n\t  var f2 = yield readFile('/etc/shells');\n\t  console.log(f1.toString());\n\t  console.log(f2.toString());\n\t};\n\t\n\t// use Async\n\tvar asyncReadFile = async function (){\n\t  var f1 = await readFile('/etc/fstab');\n\t  var f2 = await readFile('/etc/shells');\n\t  console.log(f1.toString());\n\t  console.log(f2.toString());\n\t};\n```\n\n比较就会发现，`async` 函数就是将 `Generator` 函数的星号（*）替换成 `async`，将 `yield` 替换成 `await`，仅此而已。\n\n`async` 函数对 `Generator` 函数的改进主要体现在以下三个方面：\n1. 内置执行器。`async` 函数自带执行器，也就是说，async 函数的执行，与普通函数一模一样。\n2. 更好的语义。`async` 表示函数里有异步操作，`await` 表示紧跟在后面的表达式需要等待结果。\n3. 更广的适用性。`async` 函数的 `await` 命令后面，可以跟 `Promise` 对象和原始类型的值（数值、字符串和布尔值，但这时等同于同步操作）。\n\n`await` 函数返回一个 `Promise` 对象，可以使用 `then` 方法添加回调函数。当函数执行的时候，一旦遇到 `await` 就会先返回，等到触发的异步操作完成，再接着执行函数体内后面的语句。\n\n<a name=\"Class\"></a>\n### [Class](#catalog)\nES6 引入了 Class（类）这个概念，通过 `class` 关键字，可以定义类作为对象的模板代替原先通过构造函数来定义和生成新对象。\n\n基本上，ES6 的 `class` 可以看作只是一个语法糖，完全可以看作构造函数的另一种写法。\n\n```JavaScript\n\tfunction Point(x,y){\n\t  this.x = x;\n\t  this.y = y;\n\t}\n\t\n\tPoint.prototype.toString = function () {\n\t  return '(' + this.x + ', ' + this.y + ')';\n\t}\n\n\t//定义类\n\tclass Point {\n\t\n\t  constructor(x, y) {\n\t    this.x = x;\n\t    this.y = y;\n\t  }\n\t\n\t  toString() {\n\t    return '('+this.x+', '+this.y+')';\n\t  }\n\t\n\t}\n\n\ttypeof Point // \"function\"\n```\n\n类的内部所有定义的方法，都是不可枚举的（`enumerable`）。\n\n`Class` 之间通过 `extends` 关键字实现继承。\n\n**注意：**\n\n子类必须在 `constructor` 方法中显式地调用 `super` 方法（js 不像 java 一样默认隐式调用 `super` 方法），否则新建实例时会报错。因为子类没有自己的 `this` 对象，而是继承父类的 `this` 对象，然后对其进行加工。如果不调用 `super` 方法，子类就得不到 `this` 对象。\n\n如果子类没有定义 `constructor` 方法，`constructor` 方法会被默认添加。\n\n```JavaScript\n\tclass Point {\n\t  constructor(x, y) {\n\t    this.x = x;\n\t    this.y = y;\n\t  }\n\t}\n\t\n\tclass ColorPoint extends Point {\n\t  constructor(x, y, color) {\n\t    this.color = color; // ReferenceError\n\t    super(x, y);\n\t    this.color = color; // 正确\n\t  }\n\t}\n\t\n\tlet cp = new ColorPoint(25, 8, 'green');\n\t\n\tcp instanceof ColorPoint // true\n\tcp instanceof Point // true\n```\n\n`Class` 作为构造函数的语法糖，同时有 `prototype` 属性和 `__proto__` 属性，因此同时存在两条继承链。\n\n（1）子类的 `__proto__` 属性，表示构造函数的继承，总是指向父类。\n（2）子类 `prototype` 属性的 `__proto__` 属性，表示方法的继承，总是指向父类的 `prototype` 属性。\n\n在子类中，`super` 关键字代表父类实例。\n\n`extends` 关键字不仅可以用来继承类，还可以用来**继承原生的构造函数**。因此可以在原生数据结构的基础上，定义自己的数据结构。\n\n在 `class` 内部可以使用 `get` 和 `set` 关键字，对某个属性设置存值函数和取值函数，拦截该属性的存取行为。\n\n在 `class` 的某个方法之前加上星号（*），就表示该方法是一个 `Generator` 函数。\n```JavaScript\n\tclass Foo {\n\t  constructor(...args) {\n\t    this.args = args;\n\t  }\n\t  * [Symbol.iterator]() {\n\t    for (let arg of this.args) {\n\t      yield arg;\n\t    }\n\t  }\n\t}\n\t\n\tfor (let x of new Foo('hello', 'world')) {\n\t  console.log(x);\n\t}\n```\n\n#### Class 的静态方法\n在一个方法前，加上 `static` 关键字，就表示该方法不会被实例化，而是直接通过类来调用，这就称为“静态方法”。\n```JavaScript\n\tclass Foo {\n\t  static classMethod() {\n\t    return 'hello';\n\t  }\n\t}\n\t\n\tFoo.classMethod() // 'hello'\n\t\n\tvar foo = new Foo();\n\tfoo.classMethod()\n\t// TypeError: undefined is not a function\n```\n\n父类的静态方法，可以被子类继承。\n\nES6 为 `new` 命令引入了一个 `new.target` 属性，（在构造函数中）返回 `new` 命令作用于的那个构造函数。如果构造函数不是通过 new 命令调用的，`new.target` 会返回undefined。\n\n子类继承父类时，`new.target` 会返回子类。利用这个特点，可以写出不能独立使用、必须继承后才能使用的类。\n```JavaScript\n\tclass Shape {\n\t  constructor() {\n\t    if (new.target === Shape) {\n\t      throw new Error('本类不能实例化');\n\t    }\n\t  }\n\t}\n\t\n\tclass Rectangle extends Shape {\n\t  constructor(length, width) {\n\t    super();\n\t    // ...\n\t  }\n\t}\n\t\n\tvar x = new Shape();  // 报错\n\tvar y = new Rectangle(3, 4);  // 正确\n```\n\nMixin 模式指的是，将多个类的接口“混入”（mix in）另一个类（多继承）。\n```JavaScript\n\tclass DistributedEdit extends mix(Loggable, Serializable) {\n\t  // ...\n\t}\n```\n\n<a name=\"Decorator\"></a>\n### [修饰器](#catalog)\n修饰器（Decorator）是一个表达式，用来修改类的行为。这是ES7的一个提案。修饰器对类的行为的改变，是代码编译时发生的，而不是在运行时。这意味着，修饰器能在编译阶段运行代码。\n```JavaScript\n\tfunction testable(target) {\n\t  target.isTestable = true;\n\t}\n\t\n\t@testable\n\tclass MyTestableClass {}\n\t\n\tconsole.log(MyTestableClass.isTestable) // true\n\t// PS：上面虽然为类的静态属性，但Decorator为ES7提案，并不属于ES6\n```\n\n修饰器函数可以接受三个参数，依次是目标函数、属性名和该属性的描述对象，后两个参数可省略，即 function Decorator(target[, attr, descriptor])。\n```JavaScript\n\t// mixins.js\n\texport function mixins(...list) {\n\t  return function (target) {\n\t    Object.assign(target.prototype, ...list)\n\t  }\n\t}\n\t\n\t// main.js\n\timport { mixins } from './mixins'\n\t\n\tconst Foo = {\n\t  foo() { console.log('foo') }\n\t}\n\t\n\t@mixins(Foo)\n\tclass MyClass {}\n\t\n\tlet obj = new MyClass()\n\tobj.foo() // 'foo'\n```\n\n修饰器不仅可以修饰类，还可以修饰类的方法。\n```JavaScript\n\tfunction testable(target) {\n\t  target.prototype.isTestable = true;\n\t}\n\t\n\tfunction readonly(target, name, descriptor){\n\t  // descriptor对象原来的值如下\n\t  // {\n\t  //   value: specifiedFunction,\n\t  //   enumerable: false,\n\t  //   configurable: true,\n\t  //   writable: true\n\t  // };\n\t  descriptor.writable = false;\n\t  return descriptor;\n\t}\n\t\n\tfunction nonenumerable(target, name, descriptor) {\n\t  descriptor.enumerable = false;\n\t  return descriptor;\n\t}\n\t\n\t\n\t@testable\n\tclass Person {\n\t  @readonly\n\t  @nonenumerable\n\t  name() { return `${this.first} ${this.last}` }\n\t}\n```\n\n修饰器只能用于类和类的方法，不能用于函数。\n\n#### core-decorators.js\ncore-decorators.js 是一个第三方模块，提供了几个常见的修饰器，通过它可以更好地理解修饰器。（案例请移步[原著](http://es6.ruanyifeng.com/#docs/decorator)）\n\n1. @autobind：使方法中的this对象，绑定原始对象。\n2. @readonly：使属性或方法不可写。\n3. @override：检查子类的方法，是否正确覆盖了父类的同名方法，如果不正确会报错。\n4. @deprecate 或deprecated：在控制台显示一条警告，表示该方法将废除。\n5. @suppressWarnings：抑制decorated修饰器导致的console.warn()调用。\n\n#### Mixin\n所谓Mixin模式，就是对象继承的一种替代方案，中文译为“混入”（mix in），意为在一个对象之中混入另外一个对象的方法。\n```JavaScript\n\tconst Foo = {\n\t  foo() { console.log('foo') }\n\t};\n\t\n\tclass MyClass {}\n\t\n\tObject.assign(MyClass.prototype, Foo);\n\t\n\tlet obj = new MyClass();\n\tobj.foo() // 'foo'\n\n\texport function mixins(...list) {\n\t  return function (target) {\n\t    Object.assign(target.prototype, ...list);\n\t  };\n\t}\n\t\n\timport { mixins } from './mixins'\n\t\n\tconst Foo = {\n\t  foo() { console.log('foo') }\n\t};\n\t\n\t@mixins(Foo)\n\tclass MyClass {}\n\t\n\tlet obj = new MyClass();\n\tobj.foo() // \"foo\"\n```\n<a name=\"Module\"></a>\n### [Module](#catalog)\n历史上，JavaScript 一直没有模块（module）体系，无法将一个大程序拆分成互相依赖的小文件，这对开发大型的、复杂的项目形成了巨大障碍。\n\n在 ES6 之前，为人熟知的模块加载方案最主要有 `CommonJS`（同步）和 `AMD`（异步）两种。在这次 ES6 标准中实现了模块功能，而且实现得相当简单，完全可以取代现有的 `CommonJS` 和 `AMD` 规范，成为浏览器和服务器通用的模块解决方案。\n```JavaScript\n\t// CommonJS\n\tlet { stat, exists, readFile } = require('fs');\n\t\n\t// Module\n\timport { stat, exists, readFile } from 'fs';\n```\n\nES6 模块不是对象，而是通过 `export` 命令显式指定输出的代码，输入时也采用静态命令的形式。上面代码的实质是从 fs 模块加载 3 个方法，其他方法不加载。这种加载称为“编译时加载”，即 ES6 可以在编译时就完成模块编译，效率要比 `CommonJS` 模块的加载方式高。\n\n#### 严格模式\nES6的模块自动采用严格模式，不管你有没有在模块头部加上 \"use strict\"。\n\n严格模式主要有以下限制。\n\n* 变量必须声明后再使用\n* 函数的参数不能有同名属性，否则报错\n* 不能使用 with 语句\n* 不能对只读属性赋值，否则报错\n* 不能使用前缀 0 表示八进制数，否则报错\n* 不能删除不可删除的属性，否则报错\n* 不能删除变量 delete prop，会报错，只能删除属性 delete global[prop]\n* eval 不会在它的外层作用域引入变量\n* eval 和 arguments 不能被重新赋值\n* arguments 不会自动反映函数参数的变化\n* 不能使用 arguments.callee\n* 不能使用 arguments.caller\n* 禁止 this 指向全局对象\n* 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈\n* 增加了保留字（比如protected、static和interface）\n\n#### export命令\n模块功能主要由两个命令构成：`export` 和 `import`。`export` 命令用于规定模块的对外接口，`import` 命令用于输入其他模块提供的功能。\n\n一个模块就是一个独立的文件。该文件内部的所有变量，外部无法获取。如果你希望外部能够读取模块内部的某个变量，就必须使用 `export` 关键字输出该变量。\n\n`export` 输出的变量就是本来的名字，但是可以使用 `as` 关键字重命名。\n\n```JavaScript\n\tfunction v1() { ... }\n\tfunction v2() { ... }\n\t\n\texport {\n\t  v1 as streamV1,\n\t  v2 as streamV2,\n\t  v2 as streamLatestVersion\n\t};\n```\n\n`export` 语句输出的值是动态绑定，绑定其所在的模块。\n```JavaScript\n\texport var foo = 'bar';\n\tsetTimeout(() => foo = 'baz', 500);\n```\n\n上面代码输出变量 foo，值为 bar，500 毫秒之后变成 baz。\n\n#### import 命令\n使用 `export` 命令定义了模块的对外接口以后，其他JS文件就可以通过 `import` 命令加载这个模块（文件）。\n\n`import` 命令同 `export` 命令一样可以使用 `as` 关键字重命名。\n```JavaScript\n\timport { lastName as surname } from './profile';\n```\n\n#### 模块的整体加载 & module 命令\n除了指定加载某个输出值，还可以使用整体加载，即用星号（*）指定一个对象，所有输出值都加载在这个对象上面。\n\n也可以通过 `module` 命令代替 `import` 语句，达到整体输入模块的作用。\n```JavaScript\n\texport function area(radius) {\n\t  return Math.PI * radius * radius;\n\t}\n\t\n\texport function circumference(radius) {\n\t  return 2 * Math.PI * radius;\n\t}\n\t\n\t// 以下方法结果是相同的\n\timport { area, circumference } from './circle';\n\t\n\timport * as circle from './circle';\n\t\n\tmodule circle from './circle';\n\t\n\tconsole.log(\"圆面积：\" + circle.area(4));\n\tconsole.log(\"圆周长：\" + circle.circumference(14));\n```\n#### export default命令\n使用 `export default` 命令为模块指定默认输出，一个模块只能有一个默认输出，因此 export deault 命令只能使用一次。需要注意的是，当要使用  export default时，`import` 命令后面不使用大括号，因为只可能对应一个方法。\n\n如果想在一条 import 语句中，同时输入默认方法和其他变量，可以写成下面这样。\n```JavaScript\n\timport customName, { otherMethod } from './export-default';\n```\n\n#### 模块的继承\n模块之间也可以继承。\n```JavaScript\n\texport * from 'circle';\n\texport var e = 2.71828182846;\n\texport default function(x) {\n\t    return Math.exp(x);\n\t}\n```\n\n<a name=\"Style\"></a>\n### [编程风格](#catalog)\n#### 1. 块级作用域\n1. let 取代 var\nES6 提出了两个新的声明变量的命令：let 和 const。其中，let 完全可以取代 var，因为两者语义相同，而且 let 没有副作用。\n2. 全局常量和线程安全  \n在 let 和 const 之间，建议优先使用 const，尤其是在全局环境，不应该设置变量，只应设置常量。这符合函数式编程思想，有利于将来的分布式运算。\n**所有的函数都应该设置为常量。**\n3. 严格模式\nV8 引擎只在严格模式之下，支持 let 和 const。\n\n#### 2. 字符串\n静态字符串一律使用单引号或反引号，不使用双引号。动态字符串使用反引号。\n```JavaScript\n\t// bad\n\tconst a = \"foobar\";\n\tconst b = 'foo' + a + 'bar';\n\t\n\t// acceptable\n\tconst c = `foobar`;\n\t\n\t// good\n\tconst a  = 'foobar';\n\tconst b = `foo${a}bar`;\n```\n\n#### 3. 解构赋值\n函数的参数如果是对象的成员，优先使用解构赋值。\n如果函数返回多个值，优先使用对象的解构赋值，而不是数组的解构赋值。这样便于以后添加返回值，以及更改返回值的顺序。\n```JavaScript\n\t// bad\n\tfunction getFullName(user) {\n\t  const firstName = user.firstName;\n\t  const lastName = user.lastName;\n\t}\n\t\n\t// good\n\tfunction getFullName(obj) {\n\t  const { firstName, lastName } = obj;\n\t}\n\t\n\t// best\n\tfunction getFullName({ firstName, lastName }) {\n\t}\n```\n\n如果函数返回多个值，优先使用对象的解构赋值，而不是数组的解构赋值。这样便于以后添加返回值，以及更改返回值的顺序。\n```JavaScript\n\t// bad\n\tfunction processInput(input) {\n\t  return [left, right, top, bottom];\n\t}\n\t\n\t// good\n\tfunction processInput(input) {\n\t  return { left, right, top, bottom };\n\t}\n\t\n\tconst { left, right } = processInput(input);\n```\n\n#### 4. 对象\n对象尽量静态化，一旦定义，就不得随意添加新的属性。如果添加属性不可避免，要使用 `Object.assign` 方法。\n```JavaScript\n\t// bad\n\tconst a = {};\n\ta.x = 3;\n\t\n\t// if reshape unavoidable\n\tconst a = {};\n\tObject.assign(a, { x: 3 });\n\t\n\t// good\n\tconst a = { x: null };\n\ta.x = 3;\n```\n\n对象的属性和方法，尽量采用简洁表达法，这样易于描述和书写。\n\n#### 5. 数组\n使用扩展运算符（...）拷贝数组。使用 Array.from 方法，将类似数组的对象转为数组。\n```JavaScript\n\t// bad\n\tconst len = items.length;\n\tconst itemsCopy = [];\n\tlet i;\n\t\n\tfor (i = 0; i < len; i++) {\n\t  itemsCopy[i] = items[i];\n\t}\n\t\n\t// good\n\tconst itemsCopy = [...items];\n\n\tconst foo = document.querySelectorAll('.foo');\n\tconst nodes = Array.from(foo);\n```\n\n#### 6. 函数\n立即执行函数可以写成箭头函数的形式，另外那些需要使用函数表达式的场合，尽量用箭头函数代替。\n\n箭头函数取代 Function.prototype.bind，不应再用 self/_this/that 绑定 this。\n```JavaScript\n\t// bad\n\tconst self = this;\n\tconst boundMethod = function(...params) {\n\t  return method.apply(self, params);\n\t}\n\t\n\t// acceptable\n\tconst boundMethod = method.bind(this);\n\t\n\t// best\n\tconst boundMethod = (...params) => method.apply(this, params);\n```\n\n所有配置项都应该集中在一个对象，放在最后一个参数，布尔值不可以直接作为参数，并使用默认值语法设置函数参数的默认值。\n```JavaScript\n\t// bad\n\tfunction divide(a, b, option = false ) {\n\t}\n\t\n\t// good\n\tfunction divide(a, b, { option = false } = {}) {\n\t}\n```\n\n不要在函数体内使用 arguments 变量，使用 `rest` 运算符（...）代替。因为 rest 运算符显式表明你想要获取参数，而且 `arguments` 是一个类似数组的对象，而 `rest` 运算符可以提供一个真正的数组。\n```JavaScript\n\t// bad\n\tfunction concatenateAll() {\n\t  const args = Array.prototype.slice.call(arguments);\n\t  return args.join('');\n\t}\n\t\n\t// good\n\tfunction concatenateAll(...args) {\n\t  return args.join('');\n\t}\n```\n\n#### 7. Map结构\n注意区分 `Object` 和 `Map`，只有**模拟实体对象**时，才使用 `Object`。如果只是需要 key:value 的数据结构，使用 `Map`。因为 `Map` 有内建的遍历机制。\n\n#### 8. Class\n总是用 class，取代需要 prototype 操作；使用 extends 实现继承，因为这样更简单，不会有破坏 instanceof 运算的危险。\n```JavaScript\n\t// bad\n\tfunction Queue(contents = []) {\n\t  this._queue = [...contents];\n\t}\n\tQueue.prototype.pop = function() {\n\t  const value = this._queue[0];\n\t  this._queue.splice(0, 1);\n\t  return value;\n\t}\n\n\tconst inherits = require('inherits');\n\tfunction PeekableQueue(contents) {\n\t  Queue.apply(this, contents);\n\t}\n\tinherits(PeekableQueue, Queue);\n\tPeekableQueue.prototype.peek = function() {\n\t  return this._queue[0];\n\t}\n\t\n\t// good\n\tclass Queue {\n\t  constructor(contents = []) {\n\t    this._queue = [...contents];\n\t  }\n\t  pop() {\n\t    const value = this._queue[0];\n\t    this._queue.splice(0, 1);\n\t    return value;\n\t  }\n\t}\n\t\n\tclass PeekableQueue extends Queue {\n\t  peek() {\n\t    return this._queue[0];\n\t  }\n\t}\n```\n#### 9. 模块\n首先，Module 语法是 JavaScript 模块的标准写法，坚持使用这种写法。使用 `import` 取代 `require`，使用 `export` 取代 `module.exports`。\n\n不要在模块输入中使用通配符。因为这样可以确保你的模块之中，有一个默认输出（export default）。如果模块默认输出一个函数，函数名的首字母应该小写；如果模块默认输出一个对象，对象名的首字母应该大写。\n\n#### 10. ESLint\n**ESLint 是一个语法规则和代码风格的检查工具，可以用来保证写出语法正确、风格统一的代码。**"
  },
  {
    "path": "src/server/data/posts/functional-mixins.md",
    "content": "> 原文链接：[Functional Mixins](https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c)  \n> 译者注：在编程中，mixin 类似于一个固有名词，可以理解为混合或混入，通常不进行直译，本文也是同样。\n\n\n> 这是“软件构建”系列教程的一部分，该系列主要从 JavaScript ES6+ 中学习函数式编程，以及软件构建技术。敬请关注。  \n> [上一篇](https://medium.com/javascript-scene/functors-categories-61e031bac53f) | [第一篇](https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea)\n\n**Mixin 函数** 是指能够给对象添加属性或行为，并可以通过管道连接在一起的组合工厂函数，就如同流水线上的工人。Mixin 函数不依赖或要求一个基础工厂或构造函数：简单地将任意一个对象传入一个 mixin，就会得到一个增强之后的对象。\n\nMixin 函数的特点：\n\n* 数据封装\n* 继承私有状态\n* 多继承\n* 覆盖重复属性\n* 无需基础类\n\n### 动机\n现代软件开发的核心就是组合：我们将一个庞大复杂的问题，分解成更小，更简单的问题，最终将这些问题的解决办法组合起来就变成了一个应用程序。\n\n组合的最小单位就是以下两者之一：\n\n* 函数\n* 数据结构\n\n他们的组合就定义了应用的结构。\n\n通常，组合对象由类继承实现，其中子类从父类继承其大部分功能，并扩展或覆盖部分。这种方法导致了 **is-a** 问题，比如：管理员是一名员工，这引发了许多设计问题：\n\n* 高耦合：由于子类的实现依赖于父类，所以类继承是面向对象设计中最紧密的耦合。\n* 脆弱的子类：由于高耦合，对父类的修改可能会破坏子类。软件作者可能在不知情的情况下破坏了第三方管理的代码。\n* 层次不灵活：根据单一祖先分类，随着长时间的演变，最终所有的类都将不适用于新用例。\n* 重复问题：由于层次不灵活，新用例通常是通过重复而不是扩展来实现的，这导致不同的类有着相似的类结构。而一旦重复创建，在创建其子类时，该继承自哪个类以及为什么继承于这个类就不清晰了。\n* 大猩猩和香蕉问题：“...面向对象语言的问题是他们会获得所有与之相关的隐含环境。比如你想要一个香蕉，但你得到的会是一只拿着香蕉的大猩猩，以及一整片丛林。” - Joe Armstrong([Coders at Work](https://www.amazon.com/gp/product/1430219483?ie=UTF8&camp=213733&creative=393185&creativeASIN=1430219483&linkCode=shr&tag=eejs-20&linkId=3MNWRRZU3C4Q4BDN))\n\n假设管理员是一名员工，你如何处理聘请外部顾问暂时行使管理员职务的情况？（译者：木知啊~）如果你事先知道所有的需求，类继承可能有效，但我从没有看到过这种情况。随着不断地使用，新问题和更有效的流程将会被发现，应用程序和需求不可避免地随着时间的推移而发展和演变。\n\nMixin 提供了更灵活的方法。\n\n### 什么是 Mixin？\n\n> “组合优于继承。” - [设计模式：可重用面向对象软件的元素](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)\n\n**Mixin** 是对象组合的一种，它将部分特性混入复合对象中，使得这些属性成为复合对象的属性。\n\n面向对象编程中的 \"mixin\" 一词来源于冰激凌店。不同于将不同口味的冰激凌预先混合，每个顾客可以自由混合各种口味的冰激凌，从而创造出属于自己的冰激凌口味。\n\n对象 mixin 与之类似：从一个空对象开始，然后一步步扩展它。由于 JavaScript 支持动态对象扩展，所以在 JavaScript 中使用对象 mixin 是非常简单的。它也是 JavaScript 中最常见的继承形式，来看一个例子：\n\n```JavaScript\nconst chocolate = {\n  hasChocolate: () => true\n};\nconst caramelSwirl = {\n  hasCaramelSwirl: () => true\n};\nconst pecans = {\n  hasPecans: () => true\n};\nconst iceCream = Object.assign({}, chocolate, caramelSwirl, pecans);\n/*\n// 支持对象扩展符的话也可以写成这样...\nconst iceCream = {...chocolate, ...caramelSwirl, ...pecans};\n*/\nconsole.log(`\n  hasChocolate: ${ iceCream.hasChocolate() }\n  hasCaramelSwirl: ${ iceCream.hasCaramelSwirl() }\n  hasPecans: ${ iceCream.hasPecans() }\n`);\n\n/* 输出\n  hasChocolate: true\n  hasCaramelSwirl: true\n  hasPecans: true\n*/\n```\t\n\n### 什么是函数继承？\n函数继承是指通过函数来增强对象实例实现特性继承的过程。该函数建立一个闭包使得部分数据是私有的，并通过动态对象扩展使得对象实例拥有新的属性和方法。\n\n来看一下这个词的创造者 Douglas Crockford 所给出的例子。\n\n```JavaScript\n// 父类\nfunction base(spec) {\n    var that = {}; // Create an empty object\n    that.name = spec.name; // Add it a \"name\" property\n    return that; // Return the object\n}\n// 子类\nfunction child(spec) {\n    // 调用父类构造函数\n    var that = base(spec); \n    that.sayHello = function() { // Augment that object\n        return 'Hello, I\\'m ' + that.name;\n    };\n    return that; // Return it\n}\n// Usage\nvar result = child({ name: 'a functional object' });\nconsole.log(result.sayHello()); // \"Hello, I'm a functional object\"\n```\n\n由于 `child()` 同 `base()` 紧密耦合在一起，当你想添加 `grandchild()`, `greatGrandchild()` 等时，你将面对类继承中许多常见的问题。\n\n### 什么是 Mixin 函数?\nMixin 函数是一系列将新的属性或行为混入特定对象的组合函数。它不依赖或需要一个基础工厂方法或构造器，只需将任意对象传入一个 mixin 方法，它就会被扩展。\n\n来看下面的例子。\n\n```JavaScript\nconst flying = o => {\n  let isFlying = false;\n  return Object.assign({}, o, {\n    fly () {\n      isFlying = true;\n      return this;\n    },\n    isFlying: () => isFlying,\n    land () {\n      isFlying = false;\n      return this;\n    }\n  });\n};\nconst bird = flying({});\nconsole.log( bird.isFlying() ); // false\nconsole.log( bird.fly().isFlying() ); // true\n```\n\n这里需要注意，当调用 `flying()` 时需要传递一个被扩展的对象。Mixin 函数被设计用来实现函数组合，继续看下去。\n\n```JavaScript\nconst quacking = quack => o => Object.assign({}, o, {\n  quack: () => quack\n});\nconst quacker = quacking('Quack!')({});\nconsole.log( quacker.quack() ); // 'Quack!'\n```\n\n### 组合 Mixin 函数\n通过简单的函数组合就可以将 mixin 函数组合起来。\n\n```JavaScript\nconst createDuck = quack => quacking(quack)(flying({}));\nconst duck = createDuck('Quack!');\nconsole.log(duck.fly().quack());\n```\n\n但是，这看上去有点丑陋，调试或重新排列组合顺序也有点困难。\n\n当然，这只是标准的函数组合，而我们可以通过一些好的办法来将它们组合起来，比如 `compose()` 或 `pipe()`。如果，使用 `pipe()` 就需反转函数的调用顺序，才能保持相同的执行顺序。当属性冲突时，最后的属性生效。\n\n```JavaScript\nconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);\n// OR...\n// import pipe from `lodash/fp/flow`;\nconst createDuck = quack => pipe(\n  flying,\n  quacking(quack)\n)({});\nconst duck = createDuck('Quack!');\nconsole.log(duck.fly().quack());\n```\n\n### Mixin 函数的使用场景\n你应当总是使用最简单的抽象来解决问题。从纯函数开始。如果需要一个持久化状态的对象，就试试工厂方法。如果你需要构建更复杂的对象，那就试试 Mixin 函数。\n\n以下是一些使用 Mixin 函数很棒的例子：\n\n* 应用状态管理，比如，Redux\n* 某些横向服务，比如，集中日志处理\n* 组件生命周期函数\n* 功能可组合的数据类型，比如，JavaScript `Array` 类实现了 [`Semigroup`](https://en.wikipedia.org/wiki/Semigroup), [`Functor`](https://en.wikipedia.org/wiki/Functor), [`Foldable`](https://en.wikibooks.org/wiki/Haskell/Foldable)\n\n一些代数结构可以根据其他代数结构得出，这意味着新的数据类型可以通过某些推导组合而成，而不需要定制。\n\n### 注意事项\n大部分问题都可以使用纯函数优雅地解决。然而，mixin 函数同类继承一样，会造成一些问题。事实上，使用 mixin 函数能够完全复制类继承的优缺点。\n\n你应当遵循以下的建议来避免这些问题。\n\n* 使用最简单的实现。从左边开始，根据需要移到右边。纯函数 > 工厂方法 > mixin 函数 > 类继承\n* 避免创建对象，mixin，或数据类型之间的 is-a 关系\n* 避免 mixins 之间的隐含依赖关系，mixin 函数应当是独立的\n* mixin 函数并不意味着函数式编程\n\n### 类继承\n在 JavaScript 中，类继承在极少情况下（也许永远不）会是最佳方案，但这通常是一些不由你控制的库或框架。在这种场景下，类有时是实用的。\n\n1. 无需扩展你自己的类（不需要你建立多层次的类结构）\n2. 无需使用 `new` 关键字，也就是说，框架会替你实例化\n\nAngular 2+ 和 React 满足这些需求，所以你无需扩展你自己的类，而是放心地使用它们的类。在 React 中，你可以不使用类，不过这样你的组件将不会获得 React 的优化，并且你的组件也会同文档中的例子不同。但无论如何，使用函数构建 React 组件总是你的首选。\n\n#### 性能\n在一些浏览器中，类会获得 JavaScript 引擎的优化，其他的则无法直接使用。在几乎所有情况下，这些优化都不会对程序产生决定性的影响。事实上，在接下去的几年中，你都无需关心类在性能上的不同。无论你如何构建对象，对象创建和属性访问总是非常快的（每秒百万次）。\n\n也就是说，类似 RxJS，Lodash 等公共库的作者应该研究使用 `class` 创建对象实例可能的性能优势。除非你能够证明通过类能够解决性能瓶颈，否则，你就应当使你的代码保持干净、灵活，而不必担心性能。\n\n### 隐式依赖\n你可能打算创建一些计划用于一同工作的 mixin 函数。试想一下，你想要为你的应用添加一个配置管理器，当你访问不存在的配置属性时，它会提示警告，像这样：\n\n```JavaScript\n// log 模块\nconst withLogging = logger => o => Object.assign({}, o, {\n  log (text) {\n    logger(text)\n  }\n});\n\n// 确认配置项存在模块，同 log 模块无关，这里只是确保 log 存在\nconst withConfig = config => (o = {\n  log: (text = '') => console.log(text)\n}) => Object.assign({}, o, {\n  get (key) {\n    return config[key] == undefined ?\n      // vvv 隐式依赖! vvv\n      this.log(`Missing config key: ${ key }`) :\n      // ^^^ 隐式依赖! ^^^\n      config[key]\n    ;\n  }\n});\n// 模块封装\nconst createConfig = ({ initialConfig, logger }) =>\n  pipe(\n    withLogging(logger),\n    withConfig(initialConfig)\n  )({})\n;\n// 调用\nconst initialConfig = {\n  host: 'localhost'\n};\nconst logger = console.log.bind(console);\nconst config = createConfig({initialConfig, logger});\nconsole.log(config.get('host')); // 'localhost'\nconfig.get('notThere'); // 'Missing config key: notThere'\n```\n\n也可以是这样，\n\n```JavaScript\n// 引入 log 模块\nimport withLogging from './with-logging';\nconst addConfig = config => o => Object.assign({}, o, {\n  get (key) {\n    return config[key] == undefined ? \n      this.log(`Missing config key: ${ key }`) :\n      config[key]\n    ;\n  }\n});\nconst withConfig = ({ initialConfig, logger }) => o =>\n  pipe(\n    // vvv 明确的依赖! vvv\n    withLogging(logger),\n    // ^^^ 明确的依赖! ^^^\n    addConfig(initialConfig)\n  )(o)\n;\n// 工厂方法\nconst createConfig = ({ initialConfig, logger }) =>\n  withConfig({ initialConfig, logger })({})\n;\n\n// 另一模块\nconst initialConfig = {\n  host: 'localhost'\n};\nconst logger = console.log.bind(console);\nconst config = createConfig({initialConfig, logger});\nconsole.log(config.get('host')); // 'localhost'\nconfig.get('notThere'); // 'Missing config key: notThere'\n```\n\n选择隐式还是显式取决于很多因素。Mixin 函数作用的数据类型必须是有效的，这就需要 API 文档中的函数签名非常清晰。\n\n这就是隐式依赖版本中为 `o` 添加默认值的原因。由于 JavaScript 缺少类型注释功能，但我们可以通过默认值来代替它。\n\n```JavaScript\nconst withConfig = config => (o = {\n  log: (text = '') => console.log(text)\n}) => Object.assign({}, o, {\n  // ...\n```\n\n如果你使用 TypeScript 或 Flow，最好为你的对象参数定义一个明确的接口。\n\n### Mixin 函数与函数式编程\nMixin 函数并不像函数式编程那样纯。Mixin 函数通常是面向对象编程风格，具有副作用。许多 Mixin 函数会改变传入的参数对象。注意！\n\n出于同样的原因，一些开发者更喜欢函数式编程风格，不修改传入的对象。在编写 mixin 时，你应当适当地使用这两种编码风格。\n\n这意味着，如果你要返回对象的实例，则始终返回 `this`，而不是闭包中对象实例的引用。因为在函数式编程中，很有可能这些引用指向的并不是同一个对象。另外，总是使用 `Object.assign()` 或 `{...object, ...spread}` 语法进行复制。但需要注意的是，非枚举的属性将不会存在于最终的对象上。\n\n```JavaScript\nconst a = Object.defineProperty({}, 'a', {\n  enumerable: false,\n  value: 'a'\n});\nconst b = {\n  b: 'b'\n};\nconsole.log({...a, ...b}); // { b: 'b' }\n```\n\n出于同样的原因，如果你使用的 mixin 函数不是自己构建的，就不要认为它就是纯的。假设基础对象会被改变，假设它可能会产生副作用，不保证参数不会改变，即由 mixin 函数组合而成的记录工厂通常是不安全的。\n\n### 结论\nMixin 函数是可组合的工厂方法，它能够为对象添加属性和行为，就如同装配线上的站。它是将多个来源的功能（has-a, uses-a, can-do）组合成行为的好方法，而不是从一个类上继承所有功能（is-a）。\n\n记住，“mixin 函数” 并不意味着“函数式编程”。Mixin 函数可以用函数式编程风格编写，避免副作用并不修改参数，但这并不保证。第三方 mixin 可能存在副作用和不确定性。\n\n* 不同于对象 mixin，mixin 函数支持正真的私有数据（封装），包括继承私有数据的能力。\n* 不同于单继承，mixin 函数还支持继承多个祖先的能力，类似于类装饰器或多继承。\n* 不同于 C++ 中的多继承，JavaScript 中很少出现属性冲突问题，当属性冲突发生时，总是最后添加的 mixin 有效。\n* 不同于类装饰器或多继承，不需要基类\n\n总是从最简单的实现方式开始，只根据需要使用更复杂的实现方式：\n\n**纯函数 > 工厂方法 > mixin 函数 > 类继承**\n"
  },
  {
    "path": "src/server/data/posts/getting-started-with-redux.md",
    "content": "> 系列文章:\n> 1. Redux 入门(本文)\n> 2. [Redux 进阶](http://discipled.me/posts/redux-advanced)\n> 3. [番外篇: Vuex — The core of Vue application](http://discipled.me/posts/vuex-core-of-vue-application)\n\n状态管理，第一次听到这个词要追溯到去年年底。那时，[Flux](https://facebook.github.io/flux/) 红透半边天，而 [Reflux](https://github.com/reflux/refluxjs) 也是风华正茂。然而，前一阵一直在忙其他的事，一直没时间学学这两个库，到现在 [Redux](http://redux.js.org/) 似乎又有一统天下的趋势。\n\n那就来看看，Redux 是凭借什么做到异军突起的。\n\n### What's Redux\nRedux 是一个 JavaScript 应用状态管理的库，它帮助你编写行为一致，并易于测试的代码，而且它非常迷你，只有 2KB。\n\nRedux 有一点和别的前端库或框架不同，它不单单是一套类库，它更是一套方法论，告诉你如何去构建一个状态可预测的应用。\n\n### Why using Redux\n随着单页应用变得越来越复杂，前端代码需要管理各种各样的状态，它可以是服务器的响应，也可能是前端界面的状态。当这个状态变得任意可变，那么你就可能在某个时间点失去对整个应用状态的控制。\n\nRedux 就是为了解决这个问题而诞生的。\n\n简短地说，Redux 为整个应用创建并管理一棵状态树，并通过限制更新发生的时间和方式，而使得整个应用状态的变化变得可以被预测。\n\n除此之外，Redux 有着一整套丰富的生态圈，包括教程、中间件、开发者工具及文档，这些都可以在[官方文档](http://redux.js.org/docs/introduction/Ecosystem.html)中找到。\n\n### How to use Redux\n#### 三大原则\n在使用 Redux 之前，你必须要谨记它的三大原则：单一数据源、`state` 是只读的和使用纯函数执行修改。\n\n* 单一数据源\n\n\t> 整个应用的 `state` 都被储存在一棵树中，并且这棵状态树只存在于**唯一**一个 `store` 中。\n\t\n\t这使得来自服务端的 `state` 可以轻易地注入到客户端中；并且，由于是单一的 `state` 树，代码调试、以及“撤销/重做”这类功能的实现也变得轻而易举。\n* 只读的 `state`\n\n\t> 唯一改变 `state` 的方法就是触发 `action`，`action` 是一个用于描述已发生事件的普通对象。\n\t\n\t这就表示无论是用户操作或是请求数据都不能直接修改 `state`，相反它们只能通过触发 `action` 来变更当前应用状态。其次，`action` 就是普通对象，因此它们可以被日志打印、序列化、储存，以及用于调试或测试的后期回放。\n* 使用纯函数执行修改\n\n\t> 为每个 `action` 用**纯函数**编写 `reducer` 来描述如何修改 `state` 树\n\t\n\t或许你是第一次听到纯函数这个概念，但它是函数化编程的基础。\n\t\n\t纯函数在[维基百科](https://en.wikipedia.org/wiki/Pure_function)上的解释简单来说是满足以下两项：\n\t1. 函数在有相同的输入值时，产生相同的输出\n\t2. 函数中不包含任何会产生副作用的语句\n\t\n\t在这里，`reducer` 要做到**只要传入参数相同，返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用，没有 API 请求、没有变量修改，只进行单纯执行计算。**\n\t\n知道了三大原则之后，那就可以开始了解如何创建一个基于 Redux 的应用。\n\n#### Action\n就如之前提到的，`action` 是一个描述事件的简单对象，它是改变 `store` 中 `state` 的唯一方法，它通过 `store.dispatch()` 方法来将 `action` 传到 `store` 中。\n\n下面就是一个 `action` 的例子，它表示添加一个新的 todo 项。\n\n```JavaScript\nconst ADD_TODO = 'ADD_TODO'\n// action\n{\n  type: ADD_TODO,\n  text: 'Build my first Redux app'\n}\n```\n可以看到 `action` 就是一个简单的 JavaScript 对象。\n\n用一个字符串类型的 `type` 字段来表示将要执行的动作，`type` 最好用常量来定义，当应用扩大时，可以使用单独的模块来存放 `action`。\n\n除了 `type` 字段外，`action` 对象的结构完全由你自己决定（也可以借鉴 [flux-standard-action](https://github.com/acdlite/flux-standard-action) 来构建你的 `action`）。\n\n在现实场景中，`action` 所传递的值很少会是一个固定的值，都是动态产生的。所以，要为每个 `action` 创建它的工厂方法，工厂方法返回一个 `action` 对象。\n\n上面的那个例子就会变为：\n\n```JavaScript\nfunction addTodo(text) {\n  return {\n    type: ADD_TODO,\n    text\n  }\n}\n```\n`Action` 的创建工厂可以是异步非纯函数。牵扯到异步的问题内容就比较多，放到下一篇再分享了。\n\n#### Reducer\n`Action` 只是一个描述事件的简单对象，并没有告诉应用该如何更新 `state`，而这正是 `reducer` 的工作。\n\n在 Redux 应用中，所有的 `state` 都被保存在一个单一对象中。所以，建议在写代码前先确定这个对象的结构。如何才能以最简的形式把应用的 `state` 用对象描述出来？\n\n在设计过程中，你会发现你有时需要在 `state` 中存储一些如 UI 的 `state`，尽量将应用数据和 UI `state` 分开存放。\n\n```JavaScript\n{\n  todos: [\n    {\n      text: 'Consider using Redux',\n      completed: true,\n    },\n    {\n      text: 'Keep all state in a single tree',\n      completed: false\n    }\n  ]\n}\n```\n\n**注意：**在处理复杂应用时，建议尽可能地把 `state` 范式化，把所有数据放到一个对象里，每个数据以 ID 为主键，不同实体或列表间通过 ID 相互引用数据，这种方法在 [normalizr](https://github.com/paularmstrong/normalizr) 文档里有详细阐述。\n\n现在我们已经确定了 `state` 对象的结构，就可以开始开发 `reducer`。`reducer` 是一个纯函数，它接收旧的 `state` 和 `action`，返回新的 `state`，就像这样\n\n```JavaScript\n(previousState, action) => newState\n```\n还记不记得**三大原则**？\n\n没错，最后一点**使用纯函数进行修改**，所以，**永远不要**在 `reducer` 里做这些操作：\n\n* 修改传入的参数（即之前的 `state` 或 `action` 对象）\n* 执行有副作用的操作，如 API 请求或路由跳转\n* 调用非纯函数，如 `Date.now()` 或 `Math.random()` 等\n\n将这些铭记于心后，就能创建对应之前 `action` 的 `reducer` 了。\n\n```JavaScript\nconst initialState = {\n  todos: []\n}\n\nfunction todoApp(state = initialState, action) {\n  switch (action.type) {\n    case ADD_TODO:\n      return {\n        ...state,\n        todos: [\n          ...state.todos,\n          {\n            text: action.text,\n            completed: false\n          }\n        ]\n      }\n    default:\n      return state\n  }\n}\n```\n**注意：**\n\n1. 不要修改传入的 `state`，否则它就不是个纯函数\n2. 在遇到未知 `action` type 的时候，默认返回之前的 `state`\n\n这样一个 `reducer` 就创建好了，是不是很简单？多个 `action` 也是如此，我们再来添加一个\n\n```JavaScript\ncase TOGGLE_TODO:\n  return {\n    ...state,\n    todos: state.todos.map((todo, index) => {\n      if (index === action.index) {\n        return {\n          ...todo,\n          completed: !todo.completed\n        } // 时刻谨记不要修改 state，保证 reducer 是纯函数\n      }\n      return todo\n    })\n  }\n```\n\n从例子中可以发现，当对 `state` 的一部分进行操作时，不会影响 `state` 的其他部分，但仍需复制 `state` 树的其他部分。当项目的规模成长时，`state` 树的层次也会随之增长，对树深层节点的操作将会带来大量的复制。\n\n此时，我们就可以将这些相互独立的 `reducer` 拆分开来，我们之前的例子就可以改成这样(官网的例子更能体现这一点，为了缩减篇幅我这里省略了另一个 `reducer`)。\n\n```JavaScript\n// todos reducer\nfunction todos(state = [], action) {\n  switch (action.type) {\n    case ADD_TODO:\n      return [\n        ...state,\n        {\n          text: action.text,\n          completed: false\n        }\n      ]\n    case TOGGLE_TODO:\n      return state.map((todo, index) => {\n        if (index === action.index) {\n          return {\n            ...todo,\n            completed: !todo.completed\n          } // 时刻谨记不要修改 state，保证 reducer 是纯函数\n        }\n        return todo\n      })\n    default:\n      return state\n  }\n}\n\n// main reducer\nfunction todoApp(state = initialState, action) {\n  switch (action.type) {\n    case ADD_TODO:\n    case TOGGLE_TODO:\n      return {\n        ...state,\n        todos: todos(state.todos, action)\n      }\n   default:\n      return state\n  }\n}\n```\n这就是所谓的 `reducer` 合成，它是开发 Redux 应用的基础。\n\n**注意：**每个 `reducer` 应当只负责管理全局 `state` 中它负责的一部分；并且，每个 `reducer` 的 `state` 参数分别对应它管理的那部分 `state`。\n\n由于，每个 `reducer` 应当只负责管理全局 `state` 中它负责的一部分，那么上面的 main `reducer` 就能改为\n\n```JavaScript\n// main reducer\nfunction todoApp(state = initialState, action) {\n  return {\n    todos: todos(state.todos, action)\n  }\n}\n```\n最后，Redux 提供了 `combineReducers()` 工具类，它能帮我们减少很多重复的模板代码。\n\n`combineReducers()` 就像一个工厂，它根据传入对象的 key 来筛选出 `state` 中 key 所对应的值传给对应的 `reducer`，最终它返回一个符合规范的 reducer 函数。 \n\n最终，我们的 main `reducer` 就变为\n\n```JavaScript\n// main reducer\nconst todoApp = combineReducers({\n  todos // 等价于 todos: todos(state.todos, action)\n})\n```\n随着应用的膨胀，你可以将拆分后的 `reducer` 放到不同的文件中, 以保持其独立性。然后，你的代码就可以变成这样...\n\n```JavaScript\nimport { combineReducers } from 'redux'\nimport * as reducers from './reducers'\n\nconst todoApp = combineReducers(reducers)\n\nexport default todoApp\n```\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/getting-started-with-redux/to_heaven.jpeg)\n\n#### Store\n`Store` 用来存放整个应用的 `state`，并将 `action` 和 `reducer` 联系起来。它主要有以下几个职能：\n\n* 存储整个应用的 `state`\n* 提供 `getState()` 方法获取 `state`\n* 提供 `dispatch(action)` 方法更新 `state`\n* 提供 `subscribe(listener)` 来注册、取消监听器\n\n根据已有的 `reducer` 来创建 `store` 非常容易，只需将 `reducer` 作为参数传递给 `createStore()` 方法。\n\n```JavaScript\nimport { createStore } from 'redux'\nimport todoApp from './reducers'\nlet store = createStore(todoApp)\n```\n这样，整个应用的 `store` 就创建完成了。虽然还没有界面，但我们已经可以测试数据处理逻辑了。\n\n```JavaScript\nimport { addTodo, toggleTodo } from './actions'\n\n// 打印初始状态\nconsole.log(store.getState())\n\n// 注册监听器，在每次 state 更新时，打印日志\nconst unsubscribe = store.subscribe(() =>\n  console.log(store.getState())\n)\n\n// 发起 actions\nstore.dispatch(addTodo('Learn about actions'))\nstore.dispatch(addTodo('Learn about reducers'))\nstore.dispatch(addTodo('Learn about store'))\nstore.dispatch(actions.toggleTodo(0))\nstore.dispatch(actions.toggleTodo(1))\n\n// 停止监听\nunsubscribe();\n```\n运行代码，控制台中就能看到下面的输出。\n\n![控制台输出](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/getting-started-with-redux/redux_console_output.png)\n\n### Data flow\n时刻谨记一点：**严格的单向数据流是 Redux 架构的设计核心**。\n\n也就是说，对 `state` 树的任何修改都该通过 `action` 发起，然后经过一系列 `reducer` 组合的处理，最后返回一个新的 `state` 对象。\n\n### Take a try with Angular\n之前的举例已经将 redux 最基本的一套生命周期处理展示完毕了，但没有个界面显示总是不那么令人信服。Redux 官网的例子是将 Redux 同 React 一起使用，但如同一开始说的，Redux 更是一套方法论，它不单可以和 React 一同使用，也可以和 Angular 等其他框架一同使用。\n\n虽然，同官网用的是不同的框架，但概念是相通的。\n\n首先，页面都是由组件构成，组件又分为两大类：**容器组件（Smart/Container Components）**和**展示组件（Dumb/Presentational Components）**。\n\n|  | 容器组件 | 展示组件 |\n| --- | :---: | :---: |\n| 目的 | 数据处理，state 更新 | 界面展示 |\n| 受 redux 影响 | 是 | 否 |\n| 数据来源 | `store.subscribe()` | 组件属性传递 |\n| 修改数据 | `store.dispatch()` | 调用通过组件属性传递的方法 |\n\n简单来说，容器组件就是通过 `store.subscribe()` 这个方法监听 `store` 中 `state` 的变化，而展示组件，就是平常使用的普通的组件，只有一点需要注意的是，所有数据修改都是通过父组件中传递下来的 `store.dispatch()` 方法来修改。\n\n可以说，容器组件是整个界面显示的核心。\n\n```JavaScript\n// todos/index.js\nimport angular from 'angular'\nimport template from './todos.html'\nimport controller from './todos'\n\nconst todoContainer = {\n\tcontroller,\n\ttemplate\n}\n\nexport default angular.module('todoContainer', [])\n\t.component('todoContainer', todoContainer)\n\t.name\n\t\n// todos/todos.js\nimport store from '../../store'\nimport actions from '../../actions'\n\nexport default class TodosContainController {\n\n\t$onInit() {\n\t\t// 注册监听器，在每次 state 更新时，更新页面绑定内容\n\t\tthis.unsubscribe = store.subscribe(() => {\n\t\t\t\tconsole.log(store.getState())\n\t\t\t\tthis.todos = store.getState().todos\n\t\t})\n\t}\n\n\taddTodoItem(text) {\n\t\tstore.dispatch(actions.addTodo(text))\n\t}\n\n\ttoggleTodoItem(index) {\n\t\tstore.dispatch(actions.toggleTodo(index))\n\t}\n\n\t$onDistory() {\n\t\t// 销毁监听器\n\t\tthis.unsubscribe()\n\t}\n}\t\n\n// todos/todos.html\n<div>\n\t<add-todo add-todo-fn=\"$ctrl.addTodoItem(text)\"></add-todo>\n\t<todo-list todo-list=\"$ctrl.todos\" toggle-todo-fn=\"$ctrl.toggleTodoItem(index)\"></todo-list>\n</div>\n```\n\nRedux 官网并不建议直接这样使用 `store.subscribe()` 来监听数据的变化，而是调用 React Redux 库的 `connect()` 方法，因为 `connect` 方法做了许多性能上的优化。相对于 Angular，也有 [ng-redux](https://github.com/angular-redux/ng-redux) 和 [ng2-redux](https://github.com/angular-redux/ng2-redux) 提供了相同的方法。\n\n鉴于展示组件与 redux 并没有太大的相关，就不在这里赘述了，有兴趣可以去 [github](https://github.com/DiscipleD/angular-redux-todoMVC) 上查看。\n\n至此，一个简单的基于 Angular 并运用 Redux 的 todo MVC 应用就完成了。\n\n### 最后\n如果你熟悉 Flux，那么这篇图文并茂的[文章](https://github.com/jasonslyvia/a-cartoon-intro-to-redux-cn)获取会对你有很大的帮助。\n\n如果你是和我一样直接接触 Redux，那[官方文档](http://redux.js.org/)是你的首选。\n\n当然，你一定得看看 Redux 作者 Dan Abramov 自己录制的[视频](https://egghead.io/courses/getting-started-with-redux)，它会对你理解 Redux 有极大的帮助。\n"
  },
  {
    "path": "src/server/data/posts/graphql-core-concepts.md",
    "content": "> 系列文章：\n>\n> 1. GraphQL 核心概念(本文)\n> 2. [graphql-js 浅尝](http://discipled.me/posts/graphql-js-entry)\n\n最近因为工作上新产品的需要，让我有机会了解和尝试 [GraphQL](https://github.com/facebook/graphql)。按照套路，在介绍一项新技术的时候总要回答 3 个问题：What, Why & How。\n\n![tradition](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/graphql-core-concepts/traditional.jpg)\n\n### What is GraphQL?\n正如副标题所说，GraphQL 是由 Facebook 创造的用于描述复杂数据模型的一种查询语言。这里查询语言所指的并不是常规意义上的类似 sql 语句的查询语言，而是一种用于前后端数据查询方式的规范。\n\n### Why using GraphQL?\n当今客户端和服务端主要的交互方式有 2 种，分别是 REST 和 ad hoc 端点。[GraphQL](http://graphql.org/) 官网指出了它们的不足之处主要在于：当需求或数据发生变化时，它们都需要建立新的接口来适应变化，而不断添加的接口，会造成服务器代码的不断增长，即使通过增加接口版本，也并不能够完全限制服务器代码的增长。（更多不足，前往[官网](http://graphql.org/)查看）\n\n既然，GraphQL 指出了它们的缺点，那么它自然解决了这些问题。\n\n如何解决的哪？那就得说说 GraphQL 的 3 大特性。\n\n* 首先，它是**声明式的**。查询的结果格式由请求方（即客户端）决定而非响应方（即服务器端）决定，也就是说，一个 GraphQL 查询结果的返回是同客户端请求时的结构一样的，不多不少，不增不减。\n* 其次，它是**可组合的**。一个 GraphQL 的查询结构是一个有层次的字段集，它可以任意层次地进行嵌套或组合，也就是说它可以通过对字段进行组合、嵌套来满足需求。\n* 第三，它是**强类型的**。强类型保证，只有当一个 GraphQL 查询满足所设定的查询类型，那么查询的结果才会被执行。\n\n回到之前的问题，也就是说，当需求或数据发生变化时，客户端可以根据需求来改变查询的结构，只要查询结构满足之前的定义，服务器端代码甚至不需要做任何的修改；即使不满足，也只需修改服务器端的查询结构，而不必额外添加新的接口来满足需求。\n\n### Core Concepts\n可能你会问，按套路这节不该是 HOW to use GraphQL，怎么变成了 Core Concepts？\n\n由于，GraphQL 是一种规范，于是，它的实现不限制某种特定语言，每种语言对 GraphQL 都可以有自己的实现，比如相对 JavaScript 就有 [graphql-js](https://github.com/graphql/graphql-js)。既然，实现都不相同，那么，使用的方法也会不同，所以便不在这里细述了。\n\n这篇文章主要分享的是 GraphQL 的核心概念，主要分为：`Type System`, `Query Syntax`, `Validation ` 和 `Introspection ` 四部分。\n\n#### Type System\n类型系统是整个 GraphQL 的核心，它用来定义每个查询对象和返回对象的类型，将所有定义的对象组合起来就形成了一整个 GraphQL Schema。\n\n这个概念比较抽象，空说很难理解，还是拿例子来边看边说。个人博客相信大家都很熟悉，这里就尝试用一个简单的博客系统的例子来说明，这会比[官网](http://graphql.org/)星战的例子简单一点。\n\nLet's go!\n\n既然是一个博客，那么，文章肯定少不了，我们首先来建立一个文章的类型。\n\n```\ntype Post {\n\tid: String,\n\tname: String,\n\tcreateDate: String,\n\ttitle: String,\n\tsubtitle: String,\n\tcontent: String\n}\n```\n\n这样，一个简单的文章类型就定义好了，它是一个自定义的类型，包含了一系列的字段，巧合的是这些字段的类型正好都是 `String`（字符串类型）。\n\n`String` 没有定义过，为什么可以直接使用哪？因为，`String` 是 GraphQL 支持的 scalar type(标量类型)，默认的标量类型还包括 `Int`，`Float`, `Boolean` 和 `ID`。\n\n许多的博客网站都支持给每篇文章打标签，那么我们在来建立一个标签的类型。\n\n```\ntype Tag {\n\tid: String,\n\tname: String,\n\tlabel: String,\n\tcreateDate: String\n}\n```\n\n标签类型和文章类型怎么整合到一起哪？\n\nGraphQL 不单单支持简单类型，还支持一些[其他类型](http://graphql.org/docs/api-reference-type-system/#overview)，如 `Object`, `Enum`, `List`, `NotNull` 这些常见的类型，还有 `Interface`, `Union`, `InputObject` 这几个特殊类型。\n\nPS：一直没搞明白 `Interface` 和 `Union` 的区别在哪，它们分别适用于什么场景？谷歌了一下，还真有篇[文章](https://medium.com/the-graphqlhub/graphql-tour-interfaces-and-unions-7dd5be35de0d#.4ywdt7kj4)说它们的区别，不过恕我愚钝，还是没能领悟，还望大神点拨...\n\n再修改一下之前的文章类型，使一个文章可以包含多个标签。\n\n```\ntype Post {\n\tid: String,\n\tname: String,\n\tcreateDate: String,\n\ttitle: String,\n\tsubtitle: String,\n\tcontent: String,\n\ttags: [Tag]\n}\n```\n通常在博客网站的标签列表中会显示该标签下的一些文章，由于 GraphQL 是以**产品为中心**的，那么在标签类型下也可以有文章类型。于是，标签类就变成了\n\n```\ntype Tag {\n\tid: String,\n\tname: String,\n\tlabel: String,\n\tcreateDate: String,\n\tposts: [Post]\n}\n```\n可能你会疑惑，文章类型和标签类型这样相互嵌套会不会造成死循环？我可以负责任的告诉你：不会。你可以尽情地嵌套、组合类型结构来满足你的需求。\n\n最后，根据整个博客网站的需求，组合嵌套刚刚定义的文章类型和标签类型，建立一个根类型作为查询的 schema。\n\n```\ntype Blog {\n\tpost: Post,\t\t// 查询一篇文章\n\tposts: [Post],\t// 用于博客首页，查询一组文章\n\ttag: Tag,\t\t// 查询一个标签\n\ttags: [Tag],\t// 用于博客标签页，查询所有标签\n}\n```\n\nOK，我们的类型和 schema 都定义好了，就可以开始查询了。怎么查哪？那我们来看看 GraphQL 的查询语法。\n\n#### Query Syntax\nGraphQL 的查询语法同我们现在所使用的有一大不同是，传输的数据结构并不是 JSON 对象，而是一个字符串，这个字符串描述了客户端希望服务端返回数据的具体结构。\n\n知道了概念，那么一个 GraphQL 的查询到底长什么样哪？继续我们的例子，假设，我们现在要查询一篇文章，那么，GraphQL 的查询语句就可以是这样。\n\n```\nquery FetchPostQuery {\n\tpost {\n\t\tid,\n\t\tname,\n\t\tcreateDate,\n\t\ttitle,\n\t\tsubtitle,\n\t\tcontent,\n\t\ttags {\n\t\t\tname,\n\t\t\tlabel\n\t\t}\n\t}\n}\n```\n它相对应的返回就会是类似这样的一个 JSON 数据。\n\n```\n{\n\t\"data\": {\n\t\t\"post\": {\n\t\t\t\"id\": \"3\",\n\t\t\t\"name\": \"graphql-core-concepts\",\n\t\t\t\"createDate\": \"2016-08-01\",\n\t\t\t\"title\": \"GraphQL 核心概念\",\n\t\t\t\"subtitle\": \"A query language created by Facebook for decribing data requirements on complex application data models\",\n\t\t\t\"content\": \"省略...\",\n\t\t\t\"tags\": [{\n\t\t\t\t\"name\": \"graphql\",\n\t\t\t\t\"label\": \"GraphQL\"\n\t\t\t}]\n\t\t}\n\t}\n}\n```\n从中我们可以看到，数据返回了整个文章的属性以及部分的标签属性。其中，标签属性并没有返回全部的字段，而是只返回了 name 和 label 字段的属性，做到了返回数据的结构完成同请求数据的结构相同，没有冗余的数据。\n\n查询添加参数的需求也非常基本，在 GraphQL 的查询语法中也相当简单，就拿刚刚的例子，要查询特定的文章就可以把它改成这样。\n\n```\nquery FetchPostQuery {\n\tpost(name: 'graphql-core-concepts') {\n\t\tid,\n\t\tname,\n\t\tcreateDate,\n\t\ttitle,\n\t\tsubtitle,\n\t\tcontent,\n\t\ttags {\n\t\t\tname,\n\t\t\tlabel\n\t\t}\n\t}\n}\n```\n返回的结果会是和之前的一样。查询关键字只有在多个查询时才必须，在单个查询时可以省略。同时，也可以对查询的返回起别名，再来看看博客的首页希望展示一个粗略的文章列表，那么这样的一个查询语句可以是\n\n```\n{\n\tpostList: posts {\n\t\tid,\n\t\tname,\n\t\tcreateDate,\n\t\ttitle,\n\t\tsubtitle,\n\t\ttags {\n\t\t\tname,\n\t\t\tlabel\n\t\t}\n\t}\n}\n```\n这里，我们省略了查询关键字，并将 `posts` 起了一个别名为 `postList`，返回的结果就会是\n\n```\n{\n\t\"data\": {\n\t\t\"postList\": [{\n\t\t\t\"id\": \"3\",\n\t\t\t\"name\": \"graphql-core-concepts\",\n\t\t\t\"createDate\": \"2016-08-01\",\n\t\t\t\"title\": \"GraphQL 核心概念\",\n\t\t\t\"subtitle\": \"A query language created by Facebook for decribing data requirements on complex application data models\",\n\t\t\t\"tags\": [{\n\t\t\t\t\"name\": \"graphql\",\n\t\t\t\t\"label\": \"GraphQL\"\n\t\t\t}]\n\t\t}, {\n\t\t\t\"id\": \"2\",\n\t\t\t\"name\": \"redux-advanced\",\n\t\t\t\"createDate\": \"2016-07-23\",\n\t\t\t\"title\": \"Redux 进阶\",\n\t\t\t\"subtitle\": \"Advanced skill in Redux\",\n\t\t\t\"tags\": [{\n\t\t\t\t\"name\": \"javascript\",\n\t\t\t\t\"label\": \"JavaScript\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"redux\",\n\t\t\t\t\"label\": \"Redux\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"state-management\",\n\t\t\t\t\"label\": \"State management\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"angular-1.x\",\n\t\t\t\t\"label\": \"Angular 1.x\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"ui-router\",\n\t\t\t\t\"label\": \"ui-router\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"redux-ui-router\",\n\t\t\t\t\"label\": \"redux-ui-router\"\n\t\t\t}]\n\t\t}, {\n\t\t\t\"id\": \"1\",\n\t\t\t\"name\": \"getting-started-with-redux\",\n\t\t\t\"createDate\": \"2016-07-06\",\n\t\t\t\"title\": \"Redux 入门\",\n\t\t\t\"subtitle\": \"A tiny predictable state management lib for JavaScript apps\",\n\t\t\t\"tags\": [{\n\t\t\t\t\"name\": \"javascript\",\n\t\t\t\t\"label\": \"JavaScript\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"redux\",\n\t\t\t\t\"label\": \"Redux\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"state-management\",\n\t\t\t\t\"label\": \"State management\"\n\t\t\t}, {\n\t\t\t\t\"name\": \"angular-1.x\",\n\t\t\t\t\"label\": \"Angular 1.x\"\n\t\t\t}]\n\t\t}]\n\t}\n}\n```\n同样，查询所有标签的语句就可以是这样\n\n```\n{\n\ttags {\n\t\tid,\n\t\tname,\n\t\tlabel,\n\t\tposts {\n\t\t\tname,\n\t\t\ttitle\n\t\t}\n\t}\n}\n```\n\n这样，一个 GraphQL 的接口，满足了一个简单博客网站的所有需求，是不是很神奇？\n\n![萌呆](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/graphql-core-concepts/tim.png)\n\n#### Validation\n由于 GraphQL 是一个强类型语言，所以它可以在执行查询之前检查每个查询语句是否满足事先设定的 schema，符合则合法，如果查询语句不合法则不进行查询。\n\n以上所举的都是合法的例子，[官网](http://graphql.org/docs/validation/)上举了一些例子，这里就不贴了，我们就总结看看要注意的有哪几点。\n\n1. `fragment` 不能引用自己从而形成一个循环\n2. 不能查询类型中不存在的字段\n3. 查询的字段如果不是 scalar type(标量类型)或 enum type（枚举类型），则需要明确该字段下所包含的字段\n4. 同上一条相对，如果查询字段是 scalar type(标量类型)，那么它就不能再有子字段\n\n#### Introspection\nIntrospection 这个词的意思是内省，自我检查（第一次发现英语有语义如此丰富的词，又暴露词汇量少了-_-||）。\n\n不扯远了，在 GraphQL 中 Introspection 是一个非常有用的功能，它可以用来查询当前 GraphQL 的 schema，从而得知服务器端支持何种类型的查询。\n\n这是一个非常强大且有用的功能，可以想象一下，现在大型公司的开发基本上都是前后端分离的，客户端并不知道服务器端提供的 schema 结构，但通过 Introspection，客户端就能获得当前服务器端所提供的 schema，这无论对开发，还是调试错误都很有帮助。\n\n还是拿刚刚的博客系统来做例子，我们可以通过查询 `__schema` 字段来获得当前所支持的查询类型。\n\n```\n// query string\n{\n\t__schema {\n\t\ttypes {\n\t\t\tname\n\t\t}\n\t}\n}\n\n// response data\n{\n  \"data\": {\n    \"__schema\": {\n      \"types\": [\n        {\n          \"name\": \"String\"\n        },\n        {\n          \"name\": \"BlogType\"\n        },\n        {\n          \"name\": \"PostType\"\n        },\n        {\n          \"name\": \"ID\"\n        },\n        {\n          \"name\": \"TagType\"\n        },\n        {\n          \"name\": \"__Schema\"\n        },\n        {\n          \"name\": \"__Type\"\n        },\n        {\n          \"name\": \"__TypeKind\"\n        },\n        {\n          \"name\": \"Boolean\"\n        },\n        {\n          \"name\": \"__Field\"\n        },\n        {\n          \"name\": \"__InputValue\"\n        },\n        {\n          \"name\": \"__EnumValue\"\n        },\n        {\n          \"name\": \"__Directive\"\n        },\n        {\n          \"name\": \"__DirectiveLocation\"\n        }\n      ]\n    }\n  }\n}\n```\n从返回的数据中可以看到，我们自定义的 BlogType, PostType 和 TagType 类，剩下的都是 GraphQL 内部类型，其中又分为两类：一类是 ID, String 和 Bealoon 所表示的标量类型，另一类以双下划线开头的是用于自我检查的类型。\n\n知道了自定义类，假设，还想知道自定义类中包含哪些属性以及属性的类型，就可以这样查询\n\n```\n// query string\n{\n\t__type(name: \"PostType\") {\n\t\tname\n\t\tfields {\n\t\t\tname,\n\t\t\ttype {\n\t\t\t\tname,\n\t\t\t\tkind\n\t\t\t}\n\t\t}\n\t}\n}\n\n// response result\n{\n  \"data\": {\n    \"__type\": {\n      \"name\": \"PostType\",\n      \"fields\": [\n        {\n          \"name\": \"id\",\n          \"type\": {\n            \"name\": null,\n            \"kind\": \"NON_NULL\"\n          }\n        },\n        {\n          \"name\": \"name\",\n          \"type\": {\n            \"name\": null,\n            \"kind\": \"NON_NULL\"\n          }\n        },\n        {\n          \"name\": \"createDate\",\n          \"type\": {\n            \"name\": null,\n            \"kind\": \"NON_NULL\"\n          }\n        },\n        {\n          \"name\": \"title\",\n          \"type\": {\n            \"name\": null,\n            \"kind\": \"NON_NULL\"\n          }\n        },\n        {\n          \"name\": \"subtitle\",\n          \"type\": {\n            \"name\": \"String\",\n            \"kind\": \"SCALAR\"\n          }\n        },\n        {\n          \"name\": \"content\",\n          \"type\": {\n            \"name\": \"String\",\n            \"kind\": \"SCALAR\"\n          }\n        },\n        {\n          \"name\": \"tags\",\n          \"type\": {\n            \"name\": null,\n            \"kind\": \"LIST\"\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\n### 最后\n总结一下，GraphQL 是一种客户端同服务端之间数据交互的概念，具有强大、灵活、易扩展等的特点。既然，它是一种概念，那么，不同的语言就可以有各种不同的实现方式。\n\n概念并不多，在于灵活运用。\n\n> PS：再次强调，本文主要讲的是 GraphQL 的核心概念，Type System 中所定义的类，都是设计类，并不是具体实现代码。实现请听下回分解。\n"
  },
  {
    "path": "src/server/data/posts/graphql-js-entry.md",
    "content": "> 系列文章：\n>\n> 1. [GraphQL 核心概念](http://discipled.me/posts/graphql-core-concepts)\n> 2. graphql-js 浅尝(本文)\n\n**常言道，实践是检验真理的唯一标准。**\n\n[上一篇文章](http://discipled.me/posts/graphql-core-concepts)讲了 GraphQL 的核心概念，所提到的一些例子都是理论化的，并没有实际代码做支撑，就好像在画一个大饼，总是让人不那么信服。\n\n它真的有那么神奇吗？那就同我一起看下去，用事实说话。\n\n之前那篇文章一直有提到 GraphQL 是一个概念，每个语言可以有自己实现它的方式。因为，我是搞前端的，对 JavaScript 比较熟悉，所以，这里就用 graphql-js（GraphQL 的 JavaScript 实现）来举例。\n\n### Hello World\n遵循传统，第一个例子必须是 Hello World。\n\n首先，安装就不用多说了。\n\n```Bash\nnpm install graphql-js --save\n```\n\n那这个例子该怎么设计哪？假设，查询一个 `hello` 字符串，就返回一个 `world` 字符串，很明显 type 的结构就该是这样\n\n```\ntype HelloWorld {\n\thello: String\n}\n```\n如何实现这个 HelloWorld 类型哪？graphql-js 已经定义好了[基础类](http://graphql.org/docs/api-reference-type-system/)，我们直接调用就行。那么，这个 type 实现起来也就非常简单了\n\n```JavaScript\nimport {\n\tGraphQLString,\n\tGraphQLObjectType,\n} from 'graphql';\n\nconst HelloWorldType = new GraphQLObjectType({\n\tname: 'HelloWorldType',\n\tfields: () => ({\n\t\thello: {\n\t\t\ttype: GraphQLString,\n\t\t}\n\t})\n});\n```\n简单分析一下上面的代码，可以看到 `HelloWorldType` 是一个 `GraphQLObjectType` 的实例，它包含一个 `fields` 是 hello，这个 hello 所对应的返回类型是字符串。\n\n那如何返回 world 字符串？那就给它个 `resolve` 方法\n\n```JavaScript\nconst HelloWorldType = new GraphQLObjectType({\n\tname: 'HelloWorldType',\n\tfields: () => ({\n\t\thello: {\n\t\t\ttype: GraphQLString,\n\t\t\tresolve() {\n\t\t\t\treturn 'world';\n\t\t\t},\n\t\t}\n\t})\n});\n```\n\n这样类型就定义好了，还记不记得上篇文章提到的类型定义完成后该怎么办？\n\n对，创建查询的 schema。\n\n```JavaScript\nimport {\n\tGraphQLString,\n\tGraphQLObjectType,\n\tGraphQLSchema,\n} from 'graphql';\n\nconst HelloWorldType = new GraphQLObjectType({\n\tname: 'HelloWorldType',\n\tfields: {\n\t\thello: {\n\t\t\ttype: GraphQLString,\n\t\t\tresolve() {\n\t\t\t\treturn 'world';\n\t\t\t},\n\t\t}\n\t}\n});\n\nconst schema = new GraphQLSchema({\n\tquery: HelloWorldType\n});\n```\nschema 设置好了，是不是想查询看看哪？\n\n![万事俱备，只欠东风](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/graphql-js-entry/sanguo.jpeg)\n\n东风当然是服务器啦。GraphQL 官方提供 [express-graphql](https://github.com/graphql/express-graphql) 这个中间件来支持基于 GraphQL 的查询，所以，这里选用 [Express](http://expressjs.com/) 作为服务器。\n\n安装就不再重复了，只需将刚刚建立的 schema 添加到 express 的中间件中就可以了。\n\n```JavaScript\nconst app = express();\n\napp\n\t.use('/graphql', graphqlHTTP({ schema, pretty: true }))\n\t.listen(3000, () => {\n\t\tconsole.log('GraphQL server running on http://localhost:3000/graphql');\n\t});\n```\n当当当当~完成，去 Postman 里查询 `http://localhost:3000/graphql?query={hello}` 看看吧。\n\n### Blog System\n在[上一篇文章](http://discipled.me/posts/graphql-core-concepts)里，我们设计了一个博客的查询 schema，这次我们就来动手实现它。（下面就开始讲例子啦，不愿听我唠叨的可以直接看[代码](https://github.com/DiscipleD/graphql-demo)）\n\n前面 HelloWorld 的例子讲的比较详细，现在大家熟悉了语法，接下来的案例就会过得快一些。\n\n首先是 PostType，这里对 Posttype 做了一点小修改，给几个字段添加了不能为空的设计。\n\n```JavaScript\n/**\n * type Post {\n *   id: ID!,\n *   name: String!,\n *   createDate: String!,\n *   title: String!,\n *   subtitle: String,\n *   content: String,\n *   tags: [Tag]\n * }\n */\nconst Post = new GraphQLObjectType({\n\tname: 'PostType',\n\tfields: () => ({\n\t\tid: {\n\t\t\ttype: new GraphQLNonNull(GraphQLID)\n\t\t},\n\t\tname: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\tcreateDate: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\ttitle: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\tsubtitle: {\n\t\t\ttype: GraphQLString\n\t\t},\n\t\tcontent: {\n\t\t\ttype: GraphQLString\n\t\t},\n\t\ttags: {\n\t\t\ttype: new GraphQLList(TagType),\n\t\t\tresolve: post => post.tags.map(tagName => getTagByName(tagName))\n\t\t}\n\t})\n});\n```\n然后是另一个主要的 type: Tag type。\n\n```JavaScript\n/**\n * type Tag {\n *   id: ID!,\n *   name: String!,\n *   label: String!,\n *   createDate: String!,\n *   posts: [Post]\n * }\n */\nconst Tag = new GraphQLObjectType({\n\tname: 'TagType',\n\tfields: () => ({\n\t\tid: {\n\t\t\ttype: new GraphQLNonNull(GraphQLID)\n\t\t},\n\t\tname: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\tlabel: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\tcreateDate: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString)\n\t\t},\n\t\tposts: {\n\t\t\ttype: new GraphQLList(PostType),\n\t\t\tresolve: tag => getPostsList().filter(post => ~post.tags.indexOf(tag.name))\n\t\t}\n\t})\n});\n```\n两个主要的类型已经定义好了，把它们俩整合起来就是博客类型了。\n\n```JavaScript\n/**\n * type Blog {\n *   post: Post,\t// 查询一篇文章\n *   posts: [Post],\t// 查询一组文章，用于博客首页\n *   tag: Tag,\t\t// 查询一个标签\n *   tags: [Tag],\t// 查询所有标签，用于博客标签页\n * }\n */\nconst BlogType = new GraphQLObjectType({\n\tname: 'BlogType',\n\tfields: () => ({\n\t\tpost: {\n\t\t\ttype: PostType,\n\t\t\targs: {\n\t\t\t\tname: {\n\t\t\t\t\ttype: GraphQLString\n\t\t\t\t}\n\t\t\t},\n\t\t\tresolve: (blog, { name }) => getPostByName(name),\n\t\t},\n\t\tposts: {\n\t\t\ttype: new GraphQLList(PostType),\n\t\t\tresolve: () => getPostsList(),\n\t\t},\n\t\ttag: {\n\t\t\ttype: TagType,\n\t\t\targs: {\n\t\t\t\tname: {\n\t\t\t\t\ttype: GraphQLString\n\t\t\t\t}\n\t\t\t},\n\t\t\tresolve: (blog, { name }) => getTagByName(name),\n\t\t},\n\t\ttags: {\n\t\t\ttype: new GraphQLList(TagType),\n\t\t\tresolve: () => getTagsList(),\n\t\t}\n\t})\n});\n```\n这里有一个新东西，就是 `arg` 字段，用来获取查询参数，如果在没有设置过 `arg` 字段的属性上添加变量进行查询，graphql-js 的验证系统会报错。\n\n最后，将之前的 helloworld 类型稍微修饰一下，独立出来，然后和 blog type 整合到一起成为根查询类。\n\n```JavaScript\nconst queryType = new GraphQLObjectType({\n\tname: 'RootQueryType',\n\tfields: () => ({\n\t\thello: WorldType,\n\t\tblog: {\n\t\t\ttype: BlogType,\n\t\t\tresolve: () => ({})\n\t\t},\n\t})\n});\n\nconst schema = new GraphQLSchema({\n\tquery: queryType\n});\n```\nOK。这样整个 Demo 就完成了([查看源码戳这里](https://github.com/DiscipleD/graphql-demo))，快去 Postman 试试各种查询，体验 GraphQL 的神奇吧。（不知道怎么写查询语句的就看[上一篇](http://discipled.me/posts/graphql-core-concepts)吧）\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/graphql-js-entry/convinced.jpeg)\n\n### 最后\n如果，你不喜欢 GET 方法或查询字符串过长，express-graphql 也支持 POST 方法，服务器会先查看请求的 URL 中是否包含查询字符串，如果不包含就会去 request body 中获取，只需在 request header 中将 `Content-Type` 设置为 `application/graphql` 就可以了。\n\n全文一直在说查询，或许你会疑惑，那我修改怎么做哪？graphql 中的修改称之为 `mutation`。`mutation` 可以定义自己的接口解析类，它在 graphql 的 schema 中是一个可选项，其他的和查询并无两样，只是最后在 `resolve` 方法中的处理方式不同而已。\n\n```JavaScript\nconst schema = new GraphQLSchema({\n\tquery: queryType，\n\tmutation: mutationType\n});\n```\n\n最后的最后提一句，[nodemon](http://nodemon.io/) 很好用，谁用谁知道。"
  },
  {
    "path": "src/server/data/posts/how-to-use-colors-in-ui.md",
    "content": "> 原文链接：[How to use colors in UI Design](https://blog.prototypr.io/how-to-use-colors-in-ui-design-16406ec06753#.b50ipi6w7)\n\n## 实用技巧\n颜色就如同其他事物一样，要适度使用。你应当坚持你的配色方案中最多使用 3 种基色，这样会得到更好的效果。每为你的设计添加一种颜色，就需要做许多来使你的设计达到颜色上的一种平衡，而你选择的颜色越多，这种平衡就越难达成。\n\n> 颜色不为设计添加令人愉快的感觉——它只是加强它。 - Pierre Bonnard（[皮尔·波纳尔](https://zh.wikipedia.org/wiki/%E7%9A%AE%E7%88%BE%C2%B7%E6%B3%A2%E7%B4%8D%E7%88%BE)）\n\n假如，你需要使用配色方案以外的颜色，请先考虑通过改变基色的明度和饱和度来提供不同的色调。\n\n### 60-30-10 原则\n在室内设计中，有一个经久不衰的原则——60-30-10，它指导你用 60% + 30% + 10% 的比例将颜色组合在一起。这个原则如此有效，是因为它给你一种平衡的感觉，让你的眼神舒适地在重点之间来回移动。同时，它还极其简单。\n\n> 60％ 是主色，30％ 是辅色，还有 10％ 是着重色。\n\n![墙漆，家具，配件](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/60-30-10-rule.png)\n\n### 颜色的意义\n几个世纪以来，科学家研究了某些颜色对人生理产生的影响。颜色不仅给人美的感觉，同时影响人的情感。颜色会随着文化和场景的不同产生不同的含义。这就是你为何会看到以黑白为主的时装商店，他们就想体现优雅和高尚。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/black-white-shore.png)\n\n* 红色：激情、爱情、危险\n* 蓝色：平静、负责、安全\n* 黑色：神秘、优雅、邪恶\n* 白色：纯洁、安静、整洁\n* 绿色：新潮、新鲜、自然\n\n[点此查看](http://seopressor.com/wp-content/uploads/2015/06/colour-culture1.png)更多颜色的所代表的含义。\n\n## 灰度第一\n我们总喜欢在设计初期就调颜色和色调，但通常你很快就会发现，刚刚花几个小时所定的主色，并不适用。虽然，这种工作方式的确很诱人，但你应当避免这种方式。\n\n与之相反，你应当强制自己专注于间距和布局元素。这种约束是非常有效的，它会为你省下大把的时间。同时，你可以试着使用不同的灰度来美化元素。\n\n![简单的单色](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/simple-monochromatic-color.png)\n\n### 远离纯灰和黑色\n我所学到的最重要的颜色技巧之一是避免使用不添加饱和度的灰色。在现实生活中，纯灰色几乎从不存在，黑色也是一样。\n\n![图中最暗的不是 #000，而是 #0A0A10](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/no-pure-grayscale.png)\n\n记住永远给你的颜色添加一些饱和度，这样看上去更自然和熟悉。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/gray-with-saturation.png)\n\n### 相信自然\n最佳的颜色组合总是来自自然。从周边环境中寻找设计方案时，最棒的事就是总能从中获得无穷的灵感。\n\n> 寻找灵感，我们只需看看四周。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/believe-in-nature.png)\n\n### 形成对比\n有些颜色组合起来效果很好，另一些则不是。颜色之间的相互作用，存在一些确定的规则，在色轮上能够查看这些规则。你应当知道这些方法，但不必人为设定它。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/contrast-on-colar-wheel.jpeg)\n\n如果，你想知道更多关于颜色的理论，可以查看这篇文章——[设计师的色彩理论：创建属于自己的调色板](https://www.smashingmagazine.com/2010/02/color-theory-for-designer-part-3-creating-your-own-color-palettes/)\n\n### 收集灵感\ndribbble 是收集 UI 灵感的最佳地点。它提供一个可以按颜色搜索的[工具](https://dribbble.com/colors/)，你可以查看其他设计师如何使用某种颜色。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/dribbble-color.png)\n\n视频、平面广告设计、室内设计、时装...如此多可以收集灵感的地方。不需要犹豫，保存任何你觉得有趣的配色方案。\n\n## 工具\n为了使设计变得更简单，我整理了一些 2017 可用的选取配色方案最佳工具。它们会为你节省大量的时间。\n\n### [Coolors.co](https://coolors.co/)\n绝对是我最喜欢的颜色挑选工具。你可以简单地锁定选中的颜色，然后按空格来生成配色方案。Coolors 还支持通过上传的文件获取配色方案，更酷的是你可以在它产生的结果上进行修改，从而生成自己的配色方案。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/pick-color-from-image.png)\n\n### [Kuler](https://color.adobe.com/)\n这个来自 Adobe 的工具已经陪伴了我很长一段时间了。它既有浏览器版，也有桌面版，桌面版还可以将配色方案导出到 Photoshop 中。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/kuler.png)\n\n### [Paletton](http://paletton.com/)\n它类似于 Kuler，不同之处在于，它不仅限于 5 中色调。当你已经定了主色想要额外的色调时，它是一个很棒的工具。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/paletton.png)\n\n### [Designspiration.net](http://designspiration.net/)\n试想一下，你已经有了一个配色方案，但你想要看一下它们整合在一起的结果。Designspiration 适用于这种场景。你最多可以选取 5 中颜色来查询对应的图片。真的很棒，不仅可以用于查找特定配色方案的图片，甚至是真实的设计实现。\n\n### [Shutterstock Lab Spectrum](https://www.shutterstock.com/labs/spectrum/)\n你可能会问：如果我想用选择的颜色搜索照片怎么办？Shutterstock 有一个工具 Spectrum，它可以根据特定的色调来搜索照片。你不必订阅，因为即使带有水印的小预览图都足以生成配色方案。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/how-to-use-colors-in-ui/shutterstock-lab-spectrum.png)\n\n### [Tineye Multicolr](https://labs.tineye.com/multicolr/)\n但如果你想在图片中搜索某些颜色的混合，甚至指定每一种颜色的数量，那么 Tineye 会帮到你。这个网站使用来自 Flickr 数据库中的 1000 万张 Creative Commons 图片。\n\n## 最后\n颜色是一个难以掌握的概念，特别是在数字时代。上述提到的提示会帮助你找到正确的颜色。学习创造令人惊叹的配色方案的最好方法就是练习，所以行动起来。"
  },
  {
    "path": "src/server/data/posts/index.ts",
    "content": "/**\n * Created by jack on 16-8-23.\n */\n\nimport { IPostBase } from '../../../types/post';\nimport { sortFn } from '../../common/DataService';\n\nconst POSTS_LIST: IPostBase[] = [{\n\tname: 'angular1.5-with-ES6-styleguide',\n\ttitle: 'Angular 1.5 Styleguide (ES2015)',\n\tsubtitle: '使用 ES2015 在 Angular 1.5 中的最佳实践',\n\tcreatedTime: '2016-06-22',\n\ttags: ['javascript', 'es6', 'angular-1.x', 'styleguide'],\n}, {\n\tname: 'angular-provide',\n\ttitle: 'Angular $provide',\n\tcreatedTime: '2015-12-22',\n\ttags: ['javascript', 'angular-1.x'],\n}, {\n\tname: 'autoprefixer',\n\ttitle: 'AutoPrefixer',\n\tsubtitle: '一个处理CSS前缀问题的神器',\n\tcreatedTime: '2016-02-25',\n\ttags: ['css', 'postcss', 'autoprefixer', 'tool'],\n}, {\n\tname: 'browsersync',\n\ttitle: 'Browsersync',\n\tcreatedTime: '2015-11-30',\n\ttags: ['browsersync', 'tool'],\n}, {\n\tname: 'css-flex',\n\ttitle: 'Css Flex',\n\tcreatedTime: '2016-01-29',\n\ttags: ['css'],\n}, {\n\tname: 'decorator-design-pattern',\n\ttitle: 'JS 5种不同的方法实现装饰者模式（译）',\n\tsubtitle: '为了自身乐趣和加强理解使用闭包、猴子补丁、原型、代理和中间件5种不同方式在 javascript 中实现装饰者模式。',\n\tcreatedTime: '2016-04-13',\n\ttags: ['javascript', 'design-pattern', 'translate'],\n}, {\n\tname: 'does-curry-help',\n\ttitle: '柯里化还好用么？（译）',\n\tcreatedTime: '2016-05-18',\n\ttags: ['javascript', 'translate'],\n}, {\n\tname: 'es2015',\n\ttitle: 'ES 6',\n\tsubtitle: 'ECMAScript 6 学习总结',\n\tcreatedTime: '2015-10-30',\n\ttags: ['javascript', 'es6'],\n}, {\n\tname: 'getting-started-with-redux',\n\ttitle: 'Redux 入门',\n\tsubtitle: 'A tiny predictable state management lib for JavaScript apps',\n\tcreatedTime: '2016-07-06',\n\ttags: ['javascript', 'es6', 'redux', 'state-management', 'angular-1.x'],\n}, {\n\tname: 'graphql-core-concepts',\n\ttitle: 'GraphQL 核心概念',\n\tsubtitle: 'A query language created by Facebook for describing data requirements on complex application data models',\n\tcreatedTime: '2016-08-01',\n\ttags: ['graphql'],\n}, {\n\tname: 'graphql-js-entry',\n\ttitle: 'graphql-js 浅尝',\n\tsubtitle: 'A JavaScript implementation for GraphQL',\n\tcreatedTime: '2016-08-03',\n\ttags: ['graphql', 'javascript', 'graphql-js'],\n}, {\n\tname: 'js-doc',\n\ttitle: 'JSDoc',\n\tsubtitle: '前端代码文档化势在必行',\n\tcreatedTime: '2016-03-26',\n\ttags: ['document', 'tool'],\n}, {\n\tname: 'ocLazyLoad',\n\ttitle: 'ocLazyLoad',\n\tsubtitle: 'Angular.js 模块按需懒加载',\n\tcreatedTime: '2016-05-28',\n\ttags: ['javascript', 'angular-1.x', 'ui-router'],\n}, {\n\tname: 'private-npm-server',\n\ttitle: '企业私有 npm 服务器',\n\tsubtitle: 'cnpm OR sinopia',\n\tcreatedTime: '2016-04-27',\n\ttags: ['npm', 'cnpm', 'sinopia', 'tool'],\n}, {\n\tname: 'redux-advanced',\n\ttitle: 'Redux 进阶',\n\tsubtitle: 'Advanced skill in Redux',\n\tcreatedTime: '2016-07-23',\n\ttags: ['javascript', 'redux', 'state-management', 'angular-1.x', 'ng-redux', 'ui-router', 'redux-ui-router'],\n}, {\n\tname: 'troubleshooting-of-upgrading-vue',\n\ttitle: 'Vue 2.0 升（cai）级（keng）之旅',\n\tsubtitle: 'Troubleshooting of upgrading Vue from 1.0 to 2.0',\n\tcreatedTime: '2016-08-14',\n\ttags: ['javascript', 'vue1', 'vue2', 'vue-router'],\n}, {\n\tname: 'vuex-core-of-vue-application',\n\ttitle: 'Vuex — The core of Vue application',\n\tsubtitle: '随着 Vue 2.0 的发布，Vuex 也伴随着推出了最新版，本文就带你对照 Redux 来看看刚刚出炉的 Vuex 2.0',\n\tcreatedTime: '2016-08-21',\n\ttags: ['javascript', 'vue2', 'vue-router', 'vuex', 'redux', 'state-management'],\n}, {\n\tname: 'why-curry-helps',\n\ttitle: '为什么使用柯里化？（译）',\n\tcreatedTime: '2016-05-05',\n\ttags: ['javascript', 'es6', 'translate'],\n}, {\n\tname: 'remote-debugging-devices',\n\ttitle: 'Remote Debugging Devices',\n\tcreatedTime: '2016-09-03',\n\ttags: ['tool', 'browsersync', 'debug', 'wechat'],\n}, {\n\tname: 'material-loading',\n\ttitle: 'Loading of Material Design',\n\tsubtitle: 'Imitate Material Design implement loading component with SVG',\n\tcreatedTime: '2016-09-11',\n\ttags: ['css', 'material-design'],\n}, {\n\tname: 'you-might-not-need-redux',\n\ttitle: '【译】也许你不必使用 Redux',\n\tcreatedTime: '2016-09-23',\n\ttags: ['redux', 'react', 'translate'],\n}, {\n\tname: 'ci-solution',\n\ttitle: '前端持续集成解决方案',\n\tcreatedTime: '2016-10-19',\n\ttags: ['ci', 'travis', 'codecov', 'nightwatch', 'saucelabs'],\n}, {\n\tname: 'ssr',\n\ttitle: 'From SPA to SSR',\n\tsubtitle: '从单页应用到服务器渲染',\n\tcreatedTime: '2016-11-30',\n\ttags: ['ssr', 'seo', 'vue2', 'koa2'],\n}, {\n\tname: 'structure-data',\n\ttitle: '结构化数据让 SEO 更上一层楼',\n\tcreatedTime: '2016-12-21',\n\ttags: ['seo', 'structure-data', 'rdfa-lite'],\n}, {\n\tname: 'docker-compose',\n\ttitle: 'Transformer: Docker Compose',\n\tsubtitle: '整合发布应用相关全部服务',\n\tcreatedTime: '2017-01-30',\n\ttags: ['docker', 'docker-compose', 'nginx', 'https', 'certbot'],\n}, {\n\tname: 'how-to-use-colors-in-ui',\n\ttitle: '[译] UI 设计中颜色正确的打开方式',\n\tcreatedTime: '2017-02-16',\n\ttags: ['translate', 'ui', 'design'],\n}, {\n\tname: 'service-workers',\n\ttitle: 'Service Workers 和离线缓存',\n\tcreatedTime: '2017-02-25',\n\ttags: ['pwa', 'service-workers'],\n}, {\n\tname: 'notification-with-sw-push-events',\n\ttitle: 'Notification with Service Workers push events',\n\tcreatedTime: '2017-03-21',\n\ttags: ['pwa', 'service-workers', 'notification'],\n}, {\n\tname: 'pwa-installable-and-share',\n\ttitle: 'PWA：添加应用至桌面及分享',\n\tcreatedTime: '2017-04-01',\n\ttags: ['pwa', 'installable', 'webshare'],\n}, {\n\tname: 'simple-chess-ai-step-by-step',\n\ttitle: '[译]手把手教你创建国际象棋 AI',\n\tcreatedTime: '2017-04-04',\n\ttags: ['translate', 'minimax', 'alpha-beta'],\n\theaderImageType: '.jpeg',\n}, {\n\tname: 'upgrade-to-webpack2',\n\ttitle: '升级 webpack 至 v2.2.x',\n\tcreatedTime: '2017-04-09',\n\ttags: ['webpack'],\n}, {\n\tname: 'upgrade-ssr-of-vue',\n\ttitle: 'Vue v2.3.0 ssr 升级手册',\n\tcreatedTime: '2017-05-10',\n\ttags: ['vue2', 'ssr', 'webpack'],\n}, {\n\tname: 'functional-mixins',\n\ttitle: '[译]Mixin 函数',\n\tsubtitle: '软件构建系列',\n\tcreatedTime: '2017-06-21',\n\ttags: ['translate', 'javascript', 'fp'],\n}, {\n\tname: 'webpack3-release',\n\ttitle: 'Webpack3 正式版发布',\n\tsubtitle: '继 Node, React, Angular 版本失控之后，Webpack 的版本也坐上了🚀',\n\tcreatedTime: '2017-06-27',\n\ttags: ['webpack'],\n}, {\n\tname: 'npm-package-locks',\n\ttitle: 'Npm 5 package locks',\n\tcreatedTime: '2017-07-31',\n\ttags: ['npm'],\n}, {\n\tname: 'vue-with-typescript',\n\ttitle: 'Vue with TypeScript',\n\tsubtitle: '如果说，2017 年计算机领域的潮流是人工智能的话，那么前端界的潮流想必就是 TypeScript 了',\n\tcreatedTime: '2017-08-11',\n\ttags: ['typescript', 'tslint', 'vue2', 'ssr', 'webpack'],\n}, {\n\tname: 'translate-react-high-performance-tools',\n\ttitle: '[译]使用 3 个工具加速你的 React 应用',\n\tcreatedTime: '2017-09-04',\n\ttags: ['react'],\n}, {\n\tname: 'apologize-letter',\n\ttitle: '致歉信',\n\tcreatedTime: '2018-01-24',\n\ttags: [],\n}, {\n\tname: 'wechat-miniprogram-basic',\n\ttitle: '微信小程序基础',\n\tcreatedTime: '2018-01-31',\n\ttags: ['wechat', 'miniprogram'],\n}, {\n\tname: 'wechat-minigame-try',\n\ttitle: '微信小游戏初试',\n\tsubtitle: '相信每个程序猿都有过这样一个梦想，梦想有一天自己能做一个游戏，如今微信小游戏让这个梦唾手可得',\n\tcreatedTime: '2018-02-25',\n\ttags: ['wechat', 'minigame'],\n}, {\n\tname: 'webpack-alias-in-css',\n\ttitle: 'Webpack Alias in Css',\n\tcreatedTime: '2018-04-22',\n\ttags: ['webpack'],\n}, {\n\tname: 'trouble-with-babelrc',\n\ttitle: 'babelrc 两三事',\n\tcreatedTime: '2018-08-04',\n\ttags: ['babel'],\n}];\n\nexport default POSTS_LIST.sort(sortFn('createdTime'));\n"
  },
  {
    "path": "src/server/data/posts/js-doc.md",
    "content": "随着 ES2015 的定稿，模块化已经成为前端开发的规范被执行，清晰的模块化使得开发者与开发者之间的依赖便的更小，当项目还小时，可以通过查找一下模块源文件中的声明就能大致了解模块的功用。然而，随着项目的不断增长以及各项目之间的整合，开发者对其他模块的内容知之甚少，如果来源于不同项目，只是项目间的依赖的话，那么源代码有可能也无法在当前开发环境下找到，此时，开发者都会想到有个 API 文档那该多好啊。\n\n所以，**前端代码文档化势在必行**。\n\n前端代码主要以js为主，主流的文档生成器便是 JSDoc，最近项目是使用的 ES2105 编写的，JSDoc3.4.0 之后已经提供了对 ES2015 的支持。\n\n## install JSDoc\n\n```Bash\nnpm i jsdoc -g\n```\n\n## How to use JSDoc\n\n同其他语言一样，文档生成工具的原理还是通过代码注释去解析并根据一定的 tag 来生成文档。在 JSDoc 文档中明确说明了，只有以 `/**` 为开始的注释才会被 JSDoc 识别，其他的注释格式都会被忽略。\n\n额外，JSDoc 默认还会将项目中的 README.md 文件一同生成到 JSDoc 最后生成的文档文件中，或通过命令 --R/-readme 指定个别文件，将其添加至所生成的文档文件中，但文件格式必须是 Markdown，此时，项目中的 README.md 将被忽略。\n\n### JSDoc命令行参数\nJSDoc 命令行几个常用参数有以下几个：\n* -c, --configure 指定 configuration file\n* -d, --destination 指定输出路径，默认 ./out\n* -e, --encoding 设定 encoding，默认utf8\n* -p, --private 将 private 注释输出到文档，默认不输出\n* -P, --package 指定 package.json file\n* -r, --recurse 查询子目录\n* -t, --template 指定输出文档 template\n* -u, --tutorials 指定教程路径，默认无\n\n### JSDoc配置文件\n同许多 js 工具一样，JSDoc 也有配置文件，可以通过设定配置文件来定制 JSDoc。如果没有指定 configuration file，将会使用一下配置。\n\n```JavaScript\n{\n    \"tags\": {\n        \"allowUnknownTags\": true, // 允许使用自定义tag\n        \"dictionaries\": [\"jsdoc\",\"closure\"] // 定义tag集\n    },\n    \"source\": {\n        \"includePattern\": \".+\\\\.js(doc)?$\", // 将以.js, .jsdoc结尾的文件作为源文件\n        \"excludePattern\": \"(^|\\\\/|\\\\\\\\)_\" // 忽略以_开头的文件夹及文件\n    },\n    \"plugins\": [],\n    \"templates\": {\n        \"cleverLinks\": false,\n        \"monospaceLinks\": false\n    }\n}\n```\n以上这个是默认配置，下面解释几个常用配置。\n\n- source：顾名思义是用来指定源文件的，在其之下包含了4个属性，其中两个已经在默认配置中出现过了。\n  |- include: [ array of paths to files to generate documentation for ], // 源文件路径数组\n  |- exclude: [ array of paths to exclude ], // 排除文件路径数组\n  |- includePattern: a regular expression, // 接受一个正则表达式，当文件名匹配这个正则时，执行JSDoc\n  |- excludePattern: a regular expression, // 接受一个正则表达式，当文件名匹配这个正则时，JSDoc忽略该文件\n  JSDoc以以下的顺序执行这些属性：\n  1. 根据include获取目标文件\n  2. 根据includePattern筛选由第一步得到的目标文件\n  3. 根据excludePattern筛选由第二步得到的文件\n  4. 最后根据exclude属性，排除由第三步得到的文件结果集，排除之后的文件便是JSDoc需要执行的源文件。\n- tags: 用来指定tag库，tags下面有2个属性，分别是\n  |- allowUnknownTags: 用来告诉JSDoc如何处理标签库以外的tag，设为false时，JSDoc不会处理标签库以外的tag，但会记录一个警告，默认为true\n  |- dictionaries: 数组格式，指定标签库，标签库越靠前，优先度越高\n- opts: 命定行参数可以在此属性下配置，列如：\n```JavaScript\n  \"opts\": {\n    \"template\": \"templates/default\",  // same as -t templates/default\n    \"encoding\": \"utf8\",               // same as -e utf8\n    \"destination\": \"./out/\",          // same as -d ./out/\n    \"recurse\": true,                  // same as -r\n    \"tutorials\": \"path/to/tutorials\", // same as -u path/to/tutorials\n}\n```\n- plugins: 配置额外的插件，如 markdown 插件，与此同时，JSDoc 也可以编写[自定义插件](http://usejsdoc.org/about-plugins.html)做额外的处理。\n- templates: 可以用来配置默认 template 的格式，或另外指定自定义的 template\n\n### Tags\n上文说了那么多，主要说的都是 JSDoc 如何使用和配置，和平时的编码过程中注释怎么写，要使用哪些标签并没什么联系，现在就来讲讲最重要的 **Tag**。\n\nJSDoc 中将 tag 分为两类，`Block tag` 和 `Inline tag`。\n\n* Block tag: 在 JSDoc 中是最高级别的注释，通常用来提供代码的详细信息。它以 `@` 开头，除了位于注释最后的 `Block tag`，其他 `Block tag` 必须紧跟换行符\n* Inline tag: 通常是 `Block tag` 的文字内容或描述，它用一对 `{}` 包裹。\n\n`Block tag` 也就是我们平时最常用的注释标签，在此列举一些常用的 tag\n\n- [@abstact](http://usejsdoc.org/tags-abstract.html): 抽象\n- [@access](http://usejsdoc.org/tags-access.html): 也可以直接使用 @private, @protect, @public 来替代\n- [@alias](http://usejsdoc.org/tags-alias.html): 别名\n- [@augments | @extends](http://usejsdoc.org/tags-augments.html): 继承\n- [@author](http://usejsdoc.org/tags-author.html): 作者\n- [@borrows](http://usejsdoc.org/tags-borrows.html): 引用，用来引用文档中的另一个记录\n- [@callback](http://usejsdoc.org/tags-callback.html): 回调\n- [@class | @constructor](http://usejsdoc.org/tags-class.html): 类，ES2015 规范下不用显示添加该 tag，JSDoc 会默认将注释第一段转换为 @class\n- [@classdesc](http://usejsdoc.org/tags-classdesc.html): 类描述\n- [@const | @constant](http://usejsdoc.org/tags-constant.html): 常量，ES2015 规范下使用 const 定义变量，不用显示添加该 tag\n- [@copyright](http://usejsdoc.org/tags-copyright.html): 版权\n- [@default | @defaultvalue](http://usejsdoc.org/tags-default.html): 默认值，JSDoc 会自动识别简单类型的值：string, number, boolean and null.\n- [@deprecated](http://usejsdoc.org/tags-deprecated.html): 废弃\n- [@desc | @description](http://usejsdoc.org/tags-description.html): 描述\n- [@emits | @fires](http://usejsdoc.org/tags-fires.html): 发出，函数内部会触发自定义事件，即包含 `@event` tag\n- [@enum](http://usejsdoc.org/tags-enum.html): 枚举\n- [@event](http://usejsdoc.org/tags-event.html): 事件，自定义事件触发处，父方法应添加 `@fires` tag\n- [@example](http://usejsdoc.org/tags-example.html): 举例\n- [@exports](http://usejsdoc.org/tags-exports.html): 导出，ES2015 规范下使用 exports 不用显示添加该 tag\n- [@external | @host](http://usejsdoc.org/tags-external.html): 外部引用\n- [@file | @fileoverview | @overview](http://usejsdoc.org/tags-file.html): 文件\n- [@func | @function | @method](http://usejsdoc.org/tags-function.html): 方法\n- [@global](http://usejsdoc.org/tags-global.html): 全局变量\n- [@license](http://usejsdoc.org/tags-license.html): 许可证\n- [@member | @var](http://usejsdoc.org/tags-member.html): 成员变量\n- [@mixin](http://usejsdoc.org/tags-mixes.html): 混合\n- [@module](http://usejsdoc.org/tags-module.html): 模型\n- [@name](http://usejsdoc.org/tags-name.html): 名称，用于抽象方法或匿名函数，变更现有方法的方法名使用 `@alias` tag\n- [@namespace](http://usejsdoc.org/tags-namespace.html): 命名空间\n- [@override](http://usejsdoc.org/tags-override.html): 重写\n- [@param | @arg | @argument](http://usejsdoc.org/tags-param.html): 参数\n- [@property | @prop](http://usejsdoc.org/tags-property.html): 属性，多用于静态对象，区别于 `@enum` 标签，`property` 标签可以设定不用类型，而 `@enum` 标签是同一类型的值的集合\n- [@readonly](http://usejsdoc.org/tags-readonly.html): 只读\n- [@requires](http://usejsdoc.org/tags-requires.html): 依赖\n- [@return | @returns](http://usejsdoc.org/tags-returns.html): 返回\n- [@see](http://usejsdoc.org/tags-see.html): 参阅，ref\n- [@since](http://usejsdoc.org/tags-since.html): 添加版本\n- [@summary](http://usejsdoc.org/tags-summary.html): 总结\n- [@this](http://usejsdoc.org/tags-this.html): 声明方法中的this指代\n- [@throws | @exception](http://usejsdoc.org/tags-throws.html): 异常\n- [@type](http://usejsdoc.org/tags-type.html): 类型\n\n`Inline tag`\n\n- {@link} 生成一个链接指向定义的 `namepath` 或者 URL\n\n### Namepaths\n`namepath` 在 JSDoc 中起着至关重要的作用，JSDoc namepath 会提供一个唯一的标识给任意一个变量，这使得你在使用 inline tag 时，可以方便的找到任何一个变量，从而提供一个指向该变量的链接。\n\n```JavaScript\nMyConstructor                // 父元素\nMyConstructor#instanceMember // 成员变量使用#\nMyConstructor.staticMember   // 静态变量使用.\nMyConstructor~innerMember    // 内部成员使用~\n                             // module使用:\n```"
  },
  {
    "path": "src/server/data/posts/material-loading.md",
    "content": "![material loading](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/material-loading/material-loading.gif)\n\n相信这个 loading 的标志大家都很熟悉，是不是很和谐？\n\n![对着发呆...](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/material-loading/trance.jpg)\n\n额...有毒，看得停不下来。既然，那么神奇，我就好奇地研(goo)究(gle)了一下。\n\n原来它是 [Material Design Progress](https://material.google.com/components/progress-activity.html#progress-activity-types-of-indicators)（谷歌网站，你懂得）的一种 —— Circular。\n\n在研究的过程中，发现有大神用 CSS + SVG 在 [codePen](https://codepen.io/jczimm/pen/vEBpoL) 上实现了它。接着，就一步步来看这个魔性的 loading 是如何实现的。\n\n## SVG\n既然，它是一个页面元素，那么，就先看看它的 dom 结构。\n\n```HTML\n<div class=\"loader\">\n\t<svg class=\"circular\" viewBox=\"0 0 50 50\">\n\t\t<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\"/>\n\t</svg>\n</div>\n```\n\n可以看到，结构很简单，是一个 `div` 标签包裹一个 `svg` 标签（`circle` 是 `svg` 中的一个预定义形状，后面再讲）。`div` 大家都很熟悉，那么，`svg` 是什么哪？\n\n> 可缩放矢量图形（英语：Scalable Vector Graphics，SVG）是一种基于可扩展标记语言（XML），用于描述二维矢量图形的图形格式。SVG由W3C制定，是一个开放标准。 ——[wikipedia](https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%B8%AE%E6%94%BE%E5%90%91%E9%87%8F%E5%9C%96%E5%BD%A2)\n\n同其他图像格式相比，svg 的主要优势在于：它是可伸缩的，即缩小、放大都不会影响显示的质量。\n\n知道了，svg 标签是什么，那其中的 viewBox 属性又是用来干什么的？\n\n### viewBox\n> The viewBox attribute allows you to specify that a given set of graphics stretch to fit a particular container element. ——[MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox)\n\n我的理解就是，选中 svg 中的一部分作为内容的显示区域来进行放大或缩小来适应整个 svg 的大小。（如果还是不太明白的，可以查看张鑫旭大神的[文章](http://www.zhangxinxu.com/wordpress/2014/08/svg-viewport-viewbox-preserveaspectratio/)，形象生动）\n\nviewBox 的值是 4 个数字并用逗号分割，分别对应原 svg 图的 x 坐标，y 坐标，宽度，高度。通过这 4 个值就能在原 svg 图中划出一个矩形，然后将它缩放至现有 svg 的大小。\n\n### circle\n明白了 `svg` 是用于描述图形，那该如何将图形画于其中哪？\n\n`svg` 提供了一些预定义形状，除了之前用到的 `circle`，还有：\n\n* [矩形 &lt;rect&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)\n* [椭圆 &lt;ellipse&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse)\n* [线 &lt;line&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)\n* [折线 &lt;polyline&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)\n* [多边形 &lt;polygon&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)\n* [路径 &lt;path&gt;](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)\n\n这里只用到了 [`circle`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)，对其他有兴趣的可以直接点链接了解。\n\n`circle` 的属性很简单，`cx`, `cy` 和 `r`，对应圆心的 x 坐标，y 坐标和半径。\n\n那例子中的就是画一个以 （25, 25）为圆心，半径为 20 的圆。\n\n```HTML\n<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\"/>\n```\n\n`fill` 属性用来填充，这里 `fill=\"none\"` 就是没有填充色。\n\nok。这样圆就完成了，但如果你也在一边尝试的话会发现，界面上依旧是一片空白。\n\n别着急，刚刚只是前戏，正戏现在才开始。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/material-loading/666.jpg)\n\n### stroke\n从最初的图中可以看到，并不是要画一个圆，而是画一段线，这段线围绕一个圆来运动。\n\n画好了圆，给它加上外边线不就有了一个围绕圆的线了么，这就要用到 `stroke`。\n\n> The stroke attribute defines the color of the outline on a given graphical element. The default value for the stroke attribute is none. ——[MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke)\n\n也就是给图形的外线框添加颜色。\n\n```HTML\n<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\" stroke=\"#106CFA\"/>\n```\n\n这时，你就能看到一个蓝色的细环了。但是，太细了，可以通过 `stroke-width` 调整。\n\n> the stroke-width attribute specifies the width of the outline on the current object. Its default value is 1. If a <percentage> is used, the value represents a percentage of the current viewport. If a value of 0 is used the outline will never be drawn. ——[MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-width)\n\n刚才，觉得太细就是因为 `stroke-width` 的默认值是 1。这里将 `stroke-width` 设定成 5%，使用百分比的好处是：当它做成组件后，只需控制 svg viewport 的大小，线宽会自动调整粗细。\n\n于是，代码又变成了这样\n\n```HTML\n<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\"\n\t\tstroke=\"#106CFA\" stroke-width=\"5%\" />\n```\n\nloading 中的线段并不是一直保持环装，而是长短会变化，这该如何控制哪？\n\n答案是：[`stroke-dasharray`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray)。\n\n> the stroke-dasharray attribute controls the pattern of dashes and gaps used to stroke paths. ——[MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray)\n\n也就是说，它是用来一组值来表示设置环绕在形状外部的虚线间隔。当这组值是偶数时，那么，它就分别表示线段长，间距长...，并以此类推。当它为奇数时，系统会默认追加相同的设置到末尾，使它成为偶数，然后再按偶数时的处理方式，如 5,3,2 就是 5,3,2,5,3,2。\n\n你会想，虚线和 loading 有啥关系，loading 是一条线啊？没错，一开始我也是这样想的。\n\n但，大神就想到了，如果将虚线的第一部分设定的足够长，那么它在可视范围内就是一条实线。于是，通过控制第一段实线的长度，也就控制了整条线段的长度。\n\n因为，线段变长变短是一个过程，这就要用到动画。\n\n```SCSS\n.loader{\n  // 省略...\n  circle{\n    animation: circle-dash 2s ease-in-out infinite;\n  }\n}\n\n@keyframes circle-dash{\n  0% {\n    stroke-dasharray: 1, 125;\n  }\n  100% {\n    stroke-dasharray: 125, 125;\n  }\n}\n```\n\n这时，你就能看到线段周而复始地从一根细线变为一个圆圈。但这有突然闪屏的感觉，和所要的结果不同，再修改一下动画，让线段成为圆圈后再退回成一根细线。\n\n```SCSS\n@keyframes circle-dash{\n  0% {\n    stroke-dasharray: 1, 125;\n  }\n  50% {\n    stroke-dasharray: 125, 125;\n  }\n  100% {\n    stroke-dasharray: 1, 125;\n  }\n}\n```\n\n是不是和结果越来越像了，但还是不对，loading 中的线段没有给人有倒退的感觉，那该如何做？\n\n那就要使用 `stroke-dashoffset`，通过设定该属性线段的开始位置，来作出线段在不断前行的假象。\n\n> the stroke-dashoffset attribute specifies the distance into the dash pattern to start the dash. ——[MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset)\n\n再修改一下，刚刚的动画。\n\n```SCSS\n@keyframes circle-dash{\n  0% {\n    stroke-dasharray: 1, 125;\n    stroke-dashoffset: 0;\n  }\n  50% {\n    stroke-dasharray: 100, 125;\n    stroke-dashoffset: -25px;\n  }\n  100% {\n    stroke-dasharray: 100, 125;\n    stroke-dashoffset: -125px;\n  }\n}\n```\n\n这次感觉是不是很相像了，只是现在它的开口一直处于一个位置，就没什么魔性了。可以通过让整个圆形旋转起来，这样圆的开口的位置也就会不断变化了。\n\n```SCSS\n.circular{\n  animation: rotate 2s linear infinite;\n}\n\n@keyframes rotate {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n```\n\nFinish！来看看最后的结果。\n\n<iframe height='265' scrolling='no' src='//codepen.io/discipled/embed/XjbNvW/?height=265&theme-id=0&default-tab=result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen <a href='http://codepen.io/discipled/pen/XjbNvW/'>Loading of Material Design</a> by Disciple_D (<a href='http://codepen.io/discipled'>@discipled</a>) on <a href='http://codepen.io'>CodePen</a>.\n</iframe>\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/material-loading/interview.jpeg)\n\n`stroke` 还有几个其他相关的属性，比如，[`stroke-linecap`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap) 可以用来改变线头的形状，其他还有 [stroke-linejoin](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin), [stroke-miterlimit](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit), [stroke-opacity](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-opacity)。\n\n### 最后\n模拟 Material Design 的 loading 就这样完成了，并应用到了我的[博客](http://discipled.me/)中，比如，首页的文章列表的懒加载。\n\n> 最近，因工作需要搭了一个 React 全家桶 + Ant.Design 的脚手架，有兴趣的可以[看看](https://github.com/DiscipleD/react-redux-antd-starter)。\n\n最后不得不吐槽一句，React + Redux 相对于 vue 2 + vuex 用起来真心累..."
  },
  {
    "path": "src/server/data/posts/notification-with-sw-push-events.md",
    "content": "> 系列文章：\n> \n> 1. [Service Workers 和离线缓存](https://discipled.me/posts/service-workers)\n> 2. Notification with Service Workers push events (本文)\n> 3. [PWA：添加应用至桌面及分享](https://discipled.me/posts/pwa-installable-and-share)\n>\n\n## Notification\nHTML5 Notification 已经推出挺久了，它可以用来给用户发送通知提示。\n\n一直想试一试给自己的博客用上这个功能。[上一篇](https://discipled.me/posts/docker-compose#Letsencrypt)成功升级 https 之后，终于可以来捣鼓一下了。捣鼓之前，还是先来看一下浏览器支持情况。\n\n### Notification 浏览器支持情况\n![Can I use Notification](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/can-i-use-notification.png)\n\n从上图中可以看到，除了我行我素的 IE 之外，其他桌面浏览器都已经支持 Notification；与之相反，移动端一片血红，几乎全军覆没。自己玩就不用在意这些了，而且 Notification 已加入标准，移动端浏览器最终也会响应号召的🙃。\n\nSo, JUST DO IT.\n\n虽然，桌面浏览器已经基本支持 Notification，但 Notification 之中还有很多配置项。之前，有看到过大神写的一篇关于 [Notification 的文章](http://www.zhangxinxu.com/wordpress/2016/07/know-html5-web-notification/)，上面列举了 Notification 的属性，比如，`sound`, `vibrate`, `image` 等。于是，上 MDN 看了下它们的支持情况，\n\n![Notification API support](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/notification-api-support.png)\n\n同样也是一大片血红，普遍也就只支持最基础的功能。\n\n### 小试牛刀\n想要尝试 Notification 非常方便，打开浏览器的 console 就可以了。\n\n首先，申请推送的权限，在 console 中输入\n\n```JavaScript\nNotification.requestPermission();\n```\n就可以看到浏览器左上角弹出提示问你是否允许推送。\n\n权限有 3 种状态：`granted`（同意）, `denied`（拒绝）和 `default`，默认是 `default`。默认权限浏览器行为和拒绝相同，不会发起推送，只有在获得用户同意后，浏览器才会发起推送。\n\n`requestPermission` 会返回一个 Promise，当用户选择后，会将用户所做的决定（即`granted`, `denied`, `default`）作为参数传递给 `then` 方法。\n\n获得了用户同意的授权之后，就可以发起推送了。发起推送也很简单，只需创建一个 Notification 对象。\n\n```JavaScirpt\n// new Notification(title[, options]);\nnew Notification('Hello world.');\n```\n\n通常情况下，这时你就能看到屏幕右上角会弹出个小框。不过总会遇到一些特例：mac 下 chrome 满屏状态下 Notification 不会实时弹出，只有切换到桌面状态下才可能弹出。然而，它就像个磨人的小妖精，你不知道它会在什么时候弹出，可能是下一秒，可能是一个小时以后，也可能是明天...（firefox 和 safiri 满屏下没有这个问题）\n\n其他具体的一些 API 就不细讲了，有兴趣的可以看 [MDN 的文档](https://developer.mozilla.org/en-US/docs/Web/API/notification)，或者之前提到的那篇文章。\n\n试过了最基本的 Hello world，那么，再进一步试着搞到项目中看看。\n\n当今，人们都睡得比较晚，有的是工作原因，有的是因为有[晚睡强迫症](https://www.zhihu.com/question/19761485)，也有时是专注于什么一下子忘了时间。\n\n这时就可以用 Notification 来做个提示，提醒自己早点休息。\n\n```JavaScript\nconst NOTIFICATION_API = 'Notification';\nconst PERMISSION_GRANTED = 'granted';\nconst NOTIFICATION_START_TIME = 23;\nconst NOTIFICATION_END_TIME = 6;\nconst DELAY_MINUTES = 5;\nconst NOTIFICATION = {\n\ttitle: '夜深了',\n\tdelay: DELAY_MINUTES * 60 * 1000, // 5 minutes\n\toptions: {\n\t\tbody: '亲，工作之余，也要注意身体噢...',\n\t\ticon: '/favicon.ico'\n\t}\n};\n\nconst isSupportNotification = () => NOTIFICATION_API in window;\nconst getPermission = () => Notification.permission;\nconst isPermissionGranted = permission => permission === PERMISSION_GRANTED;\n\nconst registerNotification = () => {\n\tconst now = new Date();\n\tconst nowHour = now.getHours();\n\t// Time in the notification time block\n\tif (nowHour <= NOTIFICATION_END_TIME || nowHour >= NOTIFICATION_START_TIME) {\n\t\t// Show notification 5 minutes later\n\t\tsetTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), NOTIFICATION.delay);\n\t} else {\n\t\t// Show notification at 11 o'clock.\n\t\tconst start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), NOTIFICATION_START_TIME, DELAY_MINUTES);\n\t\tsetTimeout(() => new Notification(NOTIFICATION.title, NOTIFICATION.options), start.valueOf() - now.valueOf());\n\t}\n};\n\nif (isSupportNotification()) {\n\tif (isPermissionGranted(getPermission())) {\n\t\tregisterNotification();\n\t} else {\n\t\tNotification\n\t\t\t.requestPermission()\n\t\t\t.then(isPermissionGranted)\n\t\t\t.then(granted => granted && registerNotification());\n\t}\n} else {\n\tconsole.info('Browser not support Notification.');\n}\n```\n\n有兴趣的话，你还可以多捣鼓几个。但是，这些提示说起来都是程序写死的，当页面加载之后就决定了它显示的时间，而不是动态产生的。如果，想要发送动态提示，这就需要客户端与服务器端的配合，还是先来看客户端。\n\n## 通过 service workers push events 来接收消息\n[上一篇中](https://discipled.me/posts/service-workers)已经成功地在客户端注册了 service workers，通过它来获取服务端发送的消息就很简单了。\n\n监听 Service workers 中的 `push` 事件，就能获取来自推送服务器的消息，再通过 `registration.showNotification` 方法就能发出 Notification 了。\n\n```JavaScript\n// service-worker.js\n// ...\nconst onPush = function(event) {\n\tevent.waitUntil(_self.registration.showNotification('New Post Arrival', {\n\t\ticon: '/logo.png'\n\t}));\n};\n\n_self.addEventListener('push', onPush);\n```\n\n现在就可以打开 firefox 试一试了，打开 service workers 调试页，点击推送就可以预览效果了。（为什么不用 chrome？这个问题后面会说...）\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/first-attempt-in-firefox.png)\n\n是不是以为这样就完成了？那就错了，这才刚刚完成了一半，服务器怎么知道是给你发推送，而不是隔壁老王？\n\n### 客户端订阅\n这就需要客户端将自己与其他客户端区分的信息告诉服务器，而这个信息就是订阅信息，在 service workers 注册时可以拿到。我们再修改一下之前的代码...\n\n```JavaScript\n// ServiceWorkerService.js\n// ...\nsw.register(SERVICE_WORKER_FILE_PATH)\n\t\t.catch(() => console.error('Load service worker fail'))\n\t\t.then(registration =>\n\t\t\tregistration\n\t\t\t\t.pushManager\n\t\t\t\t.getSubscription()\n\t\t\t\t.then(subscription => subscription || registration.pushManager.subscribe({ userVisibleOnly: true })))\n\t\t.then(subscription => {\n\t\t\tconst endpoint = subscription.endpoint;\n\n\t\t\tconst options = {\n\t\t\t\tmethod: 'post',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ endpoint })\n\t\t\t};\n\n\t\t\treturn httpFetch(SUBSCRIBE_API, options);\n\t\t})\n\t\t.catch(error => console.error('Subscribe Failure: ', error.message))\n\t\t.then(() => sendMessageToSW('Hello, service worker.'))\n\t\t.catch(() => console.error('Send message error.'));\n```\n\n在注册 service worker 时，先通过 `pushManager.getSubscription` 方法获取当前客户端是否已经订阅过了，没有订阅则通过 `pushManager.subsribe` 方法来获取一个订阅；接着就将订阅信息发送给后端，交由后端储存起来，服务端的接口这里就不贴了，有兴趣的看 Github 上的[代码](https://github.com/DiscipleD/blog/blob/master/src/server/publish/index.js#L60)吧。\n\n> 订阅信息是最重要的资料，需要妥善保存，一旦泄露别人就能轻易冒充你了。\n\n订阅信息会过期，所以不要忘了在 servier worker 中监听 `pushsubscriptionchange` 事件，当订阅过期后自动重新订阅。\n\n拿到了订阅信息，接着就可以来推送消息了。不过得先说明一点，这里所说的服务器推送与 http2 的 server push 没有任何关系（虽然，之前我一直是这么认为的...彡(-_-;)彡）。\n\n#### 打个岔\n说到 http2，就顺便说一个 nginx 升级 http2 时遇到的问题。ubuntu 14.04 下需要将 [OpenSSL 升级至 1.0.2](http://www.miguelvallejo.com/updating-to-openssl-1-0-2g-on-ubuntu-server-12-04-14-04-lts-to-stop-cve-2016-0800-drown-attack/)，nginx 才能开启 http2。\n\n> Note that accepting HTTP/2 connections over TLS requires the “Application-Layer Protocol Negotiation” (ALPN) TLS extension support, which is available only since OpenSSL version 1.0.2. Using the “Next Protocol Negotiation” (NPN) TLS extension for this purpose (available since OpenSSL version 1.0.1) is not guaranteed.\n\n但如果，和我一样使用 nginx docker 镜像的话，使用 `alpine` 版本就能开启 http2，而不必操心上面所提的了。\n\n### 服务器推送\n言归正传，这里的服务器推送是基于发布/订阅模式构成的一套体系，通过客户端的订阅行为向服务器注册，当服务器广播消息时，将消息传递给推送服务，再由推送服务器给客户端推送消息。\n\n你可能会像我一样纳闷，推送服务是什么鬼？自己的服务器支持 http2，可以 server push 那是不是可以直接推送消息，而不通过推送服务哪？答案是，No way。这里的推送服务（Push Service）指的是 google 的 [fcm](https://firebase.google.com/docs/cloud-messaging/) (以前叫 [gcm](https://developers.google.com/cloud-messaging/?hl=zh-Cn))，或者 apple 的 APNs（苹果现在还不支持 webpush）等。这点可以从上面 firefox 截图中的推送服务后的字符串看出端倪 *https://updates.push.services.mozilla.com/wpush/...*，同时，它也是客户端提交给服务器的订阅信息。\n\n知道了这些就能理解规范上的 webpush 架构了。\n\n![Webpush Architecture](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/webpush-architecture.png)\n\n要发送通知时，服务器端取出之前客户端上传的订阅信息，即刚提到的 url 地址，往这个地址发一个 post 请求就可以了，剩下的事推送服务会替你完成。\n\n```JavaScript\n// publish.js\n// ...\n\t.post('/broadcast', async ctx => {\n\t\tawait readEndpoints()\n\t\t\t.then(endpoints => {\n\t\t\t\tctx.status = 200;\n\t\t\t\tctx.body = {};\n\n\t\t\t\tendpoints.forEach(endpoint => {\n\t\t\t\t\twebPush.sendNotification({ endpoint })\n\t\t\t\t\t\t.catch(console.error);\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch(err => {\n\t\t\t\tctx.status = 500;\n\t\t\t\tctx.body = err;\n\t\t\t});\n\t});\n```\n\n### web-push & payload\n给客户端发送推送内容时，需要对推送内容进行加密，这里使用了 [web-push](https://github.com/web-push-libs/web-push) 这个库来帮加密内容，并将消息传递给推送服务器。实现了推送服务后，就不用再通过控制台去模拟推送服务了。\n\n上面是最基础的用法，如果要带上 message，就需要在客户端注册时向后端传递 `p256dh` 和 `auth`。服务器发送消息时，通过这两个值来给 message 加密，当然加密的过程都交给 `web-push` 来做。通过 Postman 发个消息👀\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/firefox-server-push-notification.png)\n\n同之前测试一样，在系统右上角弹出了提示，通常通知都可以被点击，这点 web notification 也可以做到...\n\n### 响应点击事件\n在 service workers 中可以监听 notification 的 click 事件，再通过 clients 操作，就能达到一些诸如打开一个新页面等类似的效果。\n\n```JavaScript\n// service-worker.js\n// ...\nconst onNotificationClick = function(event) {\n\tevent.notification.close();\n\n\tevent.waitUntil(clients.openWindow(event.notification.data));\n};\n\n_self.addEventListener('notificationclick', onNotificationClick);\n```\n现在点击推送，就会打开我的网站啦~😁\n\n可惜的是，当浏览器关闭时，推送就接收不到了。\n\n搞完了 firefox，但也不能忘了老朋友 chrome，之前有提到现在这套代码在 chrome 下无法成功订阅，如果想要 chrome 支持，那么还得用上古哥服务。\n\n### 配置古哥服务\n> 重要提示：使用 google 服务需要科学上网...\n\n想要 chrome 下 service workers 能够发出 Notification 并不复杂，只需以下几步：\n\n1. 由于，GCM 已经被 FCM(Firebase Cloud Messaging) 替代，所以，先要先开通 [firebase](https://console.firebase.google.com)\n2. 创建一个项目\n3. 查看 setting（⚙）中的 cloud messaging 信息(`Server key` 和 `Sender ID`)\n4. 客户端根目录下添加 manifest.json，并设置 `gcm_sender_id` 和 `applicationServerKey`，分别对应项目的`Sender ID` 和 `Server key`\n6. 服务器端，在使用 web-push 调用 `sendNotification` API 时添加 `gcmAPIKey`（填 `Server key`）\n\n通过这几步，chrome 就和 firefox 一样可以接受通知消息了。因为，项目之前没有使用 Firebase，所以，个人没有直接使用它所提供的 API 来发送通知。如果，你的项目中已经用到了 Firebase，那么，你可以根据[手册](https://firebase.google.com/docs/cloud-messaging/js/client)直接使用 firebase 封装后的 API 来接收消息，那样可能会更简单一点。\n\n配置成功之后，就可以试试 Notification 在移动端 chrome 下的效果。\n\n![notification on mobile](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/notification-on-mobile.jpeg)\n\n服务器发出消息后，notification 就会出现在系统的消息提示栏里，点击通知也会打开新的页面。（和桌面端一样，浏览器彻底关闭后就无法接收到消息了）\n\n是不是很酷~\n\n> 再次提示：使用 google 服务需要科学上网...\n\n![摊手](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/notification-with-sw-push-events/tanshou.jpeg)\n\n（google 翻译摊手竟然是 tanshou...\u0005😂）\n\n## 最后\n至此，从客户端订阅，到服务器发送推送消息，再到客户端接收推送消息一整套的功能就完成了。尽管，无论是桌面端还是移动端在浏览器关闭的情况下，Service Workers 都无法接收到推送消息，但这个功能还是能够极大得增加用户的粘性，尤其在桌面端（大多情况都会打开着浏览器）。\n\nTips: 开发时，记得勾选 `Application` -> `Service Workers` 下的 `Update on reload` 和 `Bypass for network`，这样 service worker 的更新会被立即应用。\n\n同时，推荐 mozilla 的 [Service Workers Cookbook](https://serviceworke.rs) 真的很棒!论文档、Demo，M 家优势明显。\n\n如果，你喜欢我的文章，欢迎来[我的博客](http://discipled.me)并开启通知，这样每当有新的文章，你就会第一时间收到通知啦~相信有些小伙伴已经收到了😎\n\n有了 SW 和 Notification 还要啥 R(自)S(行)S(车)...[手动滑稽]\n\n内容如有不妥之处，请指出，谢谢..."
  },
  {
    "path": "src/server/data/posts/npm-package-locks.md",
    "content": "上一篇文章中提到了几个前端界的版本大佬，这不，上个月 Node 又发布了 [8.0 版本](https://nodejs.org/en/blog/release/v8.0.0/)。\n\nNode 8 这次升级有哪些令人眼前一亮的新特性？\n\n* 新增了 [Node.js API (N-API)](https://nodejs.org/api/n-api.html)\n* 新增了 `util.promisify()`，用于将原有的 callback 形式的函数 Promise 化（相信是个神器...）\n\n不过，这些都不是今天的重点，今天的主角另有其人。\n\n## Npm 5\n最近，正好有一个小项目需要用到 node 服务，也就正好升级一波尝尝鲜。\n\nNode 的升级通常会伴随着 npm 的升级，这次也没有例外。升级至 node 8 以后，npm 也自动升级至了 5。\n\n一开始也没有在意，但当安装完依赖之后，我们的主角登场了...\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/npm-package-locks/debut.gif)\n\n项目下面多了一个文件 `package-lock.json`。\n\n心中瞬间迸发出一个想法：窝艹，这不是 [yarn](https://github.com/yarnpkg/yarn) 嘛~\n\n赶紧学习一波看看。\n\n这不看不知道，一看才发现这次 npm 5 还是有着许多的[变化](http://blog.npmjs.org/post/161081169345/v500)。\n\n首先，重写了 cache，不推荐手动清除 npm cache。以前那种安装不成功，跑下 `npm cache clean`，再来一次的日子一去不复返了~\n\n其次，一个小帮助。现在 `install` 后，会将依赖直接添加到 `package.json` 文件中，也就是默认添加 `--save` 参数。\n\n最大的变化就是引入了 package lock 这个新特性。剩下一些没用到过，也就不乱说了。\n\n## Npm package locks\n相信童鞋们多多少少遇到过，一个项目在自己机器上跑得好好的，又来一个新同事，或另一台机器安装项目就跑出问题的情况。调查了半天发现，原来是某个依赖包安装的版本不一致引起的。这或许可以怪你，谁让你不在 `package.json` 里把版本号定死的。（定死版本号同样也会带来升级库文件不方便的问题。）\n\n以后，你就不必再为此操心了。因为，npm 引入了 [package locks](https://docs.npmjs.com/files/package-locks)。\n\nNpm 5 之后使用 `npm install`，npm 不再是直接根据 `package.json` 中定义的依赖版本进行安装，而是先去尝试查看 `package-lock.json` 文件。如果 `package-lock.json` 文件不存在，则仍像之前一样按 `package.json` 安装，并同时自动生成 `package-lock.json` 文件。反之，则就根据文件中保存的依赖版本信息进行安装。\n\n这样就能保证，每次安装都会得到相同的依赖树，也就再也不会发生之前那种情况了。\n\n### `package-lock.json`\n`package-lock.json` 是一个安装依赖时，由 npm 自动生成的一个文件，里面保存的是依赖的版本信息，以及依赖之间的树状关系。当所安装的依赖不存在于 `package-lock.json` 文件中时，npm 会自动修改该文件。\n\n既然，它用于记录依赖树，那么，为了保证团队各成员之间使用相同的依赖树，它需要被提交到代码仓库。与此同时，它的改动也代表了项目依赖版本的改动，同样 npm 建议将 `package-lock.json` 的修改单独进行提交。\n\n除此以外，使用 `package-lock.json` 还需注意以下几点：\n\n* 只能存放于项目根目录，其他位置无效\n* 不随包一同发布\n* 与 `npm-shrinkwrap.json` 同时使用时，`npm-shrinkwrap.json` 优先级更高\n\n提到了 `npm-shrinkwrap.json`，自然也要了解一下。\n\n### `npm-shrinkwrap.json`\n`npm-shrinkwrap.json` 是通过命令 `npm shrinkwrap` 生成。\n\n它与 `package-lock.json` 有着同样的数据结构。不同之处在于，它没有刚刚 `package-lock.json` 所提到的这些限制。\n\n所以，`npm-shrinkwrap.json` 的使用场景是用于定义发布包所需的确切依赖版本信息。与之相反，`package-lock.json` 的使用场景就是用于定义协同开发时，项目所安装的依赖版本信息。\n\n最后，还是强烈推荐将 node 升级到 8，这样就能用到 npm package locks 这个功能啦。\n\nPS: 快写完了发现一篇相同主题的好文：[npm5 新版功能特性解析及与 yarn 评测对比](https://cloud.tencent.com/community/article/171211)。（和大佬一比，高下立见😞还是要多学习...）\n\n7 月都不知道干了点啥，忙忙碌碌地就过了...（差点就坏了规矩，警醒~）"
  },
  {
    "path": "src/server/data/posts/ocLazyLoad.md",
    "content": "> “又到了月底，不得不逼自己写一篇 blog 了，不然底线一旦破了，以后就没有底线了...”\n\n随着公司规模不断地扩大，公司的产品线也可能会随之增加，如果使用的是 AngularJS 的体系，那么产品线之间的整合势必就会遇到这样一个问题——模块加载的问题。\n\n#### 这是个怎样的问题？\n公司的产品原先通过 iframe 的形式整合在一起，iframe 的各种弊端就不再这里重复，所以公司准备做一次大的产品升级，移除所有的 iframe，将产品组合成一个单页应用来增强用户体验。\n\n那么假设，我们将所有产品已整合成一个单页应用，显而易见，这个单页应用需要有个主路由来管理各个产品模块之间的切换，而这时问题就出现了。各个模块的代码如何引入，如果简单粗暴地在首页将各个模块的代码全部引入，那么首页就会加载很多无用的代码，从而造成首屏加载时间过长，首页加载时间超长我想没有一个用户能忍的吧，直接 GG 了。\n\n那么，可不可以做到，首页只引入首屏需要显示的必要代码，而在必要的时候再去加载各个模块的代码，做到**按需懒加载**哪？\n\n答案是，肯定的。\n\n那是不是直接简单地在路由切换的 templateUrl 中，注入各自的模块代码就行了哪？\n\n```JavaScript\n// index.html\n// 引入啥的就不写了\n<body ng-app=\"mainApp\">\n  <nav>\n    <ul>\n      <li ui-sref=\"appA\">app A</li>\n      <li ui-sref=\"appB\">app B</li>\n    </ul>\n  </nav>\n  <ui-view></ui-view>\n<script>\nangular.module('mainApp', ['ui.router'])\n  .config(function($stateProvider) {\n    $stateProvider\n      .state('appA', {url: '/appA', templateUrl: './pageA.html', controller: 'appActrl'})\n      .state('appB', {url: '/appB', template: '<div>appB</div>'});\n});\n</script>\n</body>\n\n// pageA.html\n<section>{{text}}</section>\n\n<script src=\"./appA.js\"></script>\n\n// appA.js\nangular.module('mainApp')\n.controller('appActrl', ['$scope', function($scope){\n  $scope.text='App A';\n}]);\n```\n可惜没这么简单，你会发现当你点击路由 app A 的时候，页面没有正常显示 App A, 而是报了一个错误 appActrl 无法找到，因为 angular 体系下，单单加载 js 文件是没有作用的，必须将代码模块注入到主模块之中，模块代码才会被找到。\n\n#### 那该如何做？\n而这时，老司机祭出了关键法宝 [ocLazyLoad](https://oclazyload.readme.io/)。（我们年轻人还是太年轻了...）\n\nocLazyLoad 可以为你 load angular module，这个 module 可以是新建的，也可以是现有的 module。根据[官网上的例子](https://oclazyload.readme.io/docs/with-your-router)，对之前的代码稍作修改就能实现按需懒加载了。\n\n```JavaScript\n// index.html\n// 以上不变省略...\nangular.module('mainApp', ['ui.router', \"oc.lazyLoad\"])\n  .config(function($stateProvider) {\n    $stateProvider\n      .state('appA', {\n        url: '/appA',\n        controller: 'appActrl',\n        templateUrl: 'pageA.html',\n        resolve: {\n          loadMyService: ['$ocLazyLoad', function($ocLazyLoad) {\n            return $ocLazyLoad.load('./appA.js'); // 按需加载目标 js file\n          }]\n        }\n      })\n// 以下也不变...\n\n// pageA.html\n<section>{{text}}</section>\n\n// appA.js\nangular.module('appA', []) // 此处也可以是原 module angular.module('mainApp')\n  .controller('appActrl', ['$scope', function($scope){\n    $scope.text='App A';\n  }]);\n```\n\nOK，大功告成([在线例子可以看这里](http://plnkr.co/edit/Y6bCd5?p=info))，打开控制台，你可以看到，只有当点击了 app A，appA.js 的请求才会被发出。\n\n#### 写在最后\nAngular 按需懒加载就完成了，剩下的就是分模块开发了，但每次在主路由里还要关心如何加载子路由的文件，还存在着模块之间的耦合，这对模块的独立性来说，还是有点丑陋。\n\n最后再吹一波[老司机](https://github.com/kuitos)，老司机造了个轮子 [ui-router-require-polyfill](https://github.com/kuitos/angular-utils/blob/1.3.1/polyfills/ui-router-require-polyfill.js)，这个轮子用来在 router 切换的时候去拿 template 中的 script 标签去做 lazy load，那么在 roter 定义中就不用加以上 resolve 中的代码了，减小了耦合，相当好用。\n\n[Angular 1.5 & ocLazyLoad](https://github.com/ocombe/ocLazyLoad/issues/138)，原本 Angular 1.5 有可能会提供 lazy load 的功能，最终还是被无情的丢弃了。"
  },
  {
    "path": "src/server/data/posts/private-npm-server.md",
    "content": "为何需要搭建企业私有 npm 服务器，主要有以下 2 点：\n\n1. 网络因素（下载速度不佳，企业内网等）\n2. 私有包的发布与管理\n\n本文主要致力于如何搭建和运用企业私有 npm 服务器，以下所有案例都将私有 npm 服务器搭建在 VMware 虚拟机下的 linux 系统中，来模拟服务器环境。\n\n## cnpm OR sinopia\n### 1. cnpm\n\n#### cnpm 的安装\n\n1. 下载 cnpm: git clone git://github.com/fengmk2/cnpmjs.org.git $HOME/cnpmjs.org\n2. cd $HOME/cnpmjs.org\n3. 进入 mysql\n    1. CREATE DATABASE privateNPM;\n    2. use privateNPM; \n    3. source docs/db.sql\n4. 添加配置信息  \n    建议在 config 目录下新建 `config.js` 文件来配置 cnpm，而不是直接修改 config 目录下的 `index.js`，因为 `index.js 会读取 config 目录下的 `config.js`，如果存在就会覆盖默认配置。\n\n```JavaScript\n\n    // index.js\n    if (process.env.NODE_ENV !== 'test') {\n      var customConfig;\n      if (process.env.NODE_ENV === 'development') {\n        customConfig = path.join(root, 'config', 'config.js');\n      } else {\n        // 1. try to load `$dataDir/config.json` first, not exists then goto 2.\n        // 2. load config/config.js, everything in config.js will cover the same key in index.js\n        customConfig = path.join(dataDir, 'config.json');\n        if (!fs.existsSync(customConfig)) {\n          customConfig = path.join(root, 'config', 'config.js');\n        }\n      }\n      if (fs.existsSync(customConfig)) {\n        copy(require(customConfig)).override(config);\n      }\n    }\n    module.exports = config;\n\n    // config.js\n    module.exports = {\n      debug: false,\n      database: {\n        db: 'privateNPM',     // 数据库名，默认为cnpmjs_test\n        host: '127.0.0.1',    // 服务器地址\n        port: 3306,           // 端口\n        username: 'root',     // 用户名\n        password: '      ',   // 密码\n        dialect: 'mysql'      // 使用mysql，默认为sqlite, 还支持postgres,mariadb,暂时不支持oracle\n      },\n      syncModel: 'exist'         // 同步已存在的模块, 默认为none，即不同步, 还有个选项为all，同步所有模块\n    }; \n```\n\n配置字段含义在 `index.js` 内都有详细注释。 \n\n注意：**重要提示**如果搭建的 cnpm 希望被其他电脑访问，一定要将 `index.js` 中的\n\n```JavaScript\n    bindingHost: '127.0.0.1', // only binding on 127.0.0.1 for local access\n```\n\n这行注释。\n\ncnpm 默认的两个访问端口是：\n\n　　1) 7001是 registry 端口，对应 registryPort 配置项\n\n　　2) 7002是 web 端口，对应 webPort 配置项\n\n这两项都可以通过修改 `config.js` 文件来配置。\n \n5. npm install\n6. npm start // 启动cnpm服务\n    1. 启动成功  \n    ![success log](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/cnpm-server-start.jpg)\n    1. 验证：访问 http://localhost:7001/\n    ![cnpm 的 registry 信息](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/cnpm-repositry.jpg)\n\n    2. 验证：访问 http://localhost:7002/\n    ![web访问](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/cnpm-web.jpg)\n\n至此，cnpm 的安装已经完成了。\n\n#### cnpm 的使用\n\n服务器搭建完成之后就可以使用了，该如何使用哪？\n\n最简单的方式就是下载 npm 包时修改下载依赖的目标地址。\n\n```bash\n    npm install webpack -g --registry=http://192.168.80.130:7001 // IP为你搭建服务器的地址，注意不要忘了端口号\n```\n\n第一次下载会同步许多包，比较慢，喝杯咖啡吧。（安装过程中可能会发生找不到对应版本的安装错误 **No compatible version found:** ，那是因为安装顺序的问题，多装几次，或者手动先安装指定版本就能修复这个问题）\n\n安装完成后可以访问 cnpm 的 web 端口可以看到 downloads 已发生了变化，然后在其他机器上尝试下载 webpack，那你就能感觉那飞一般的感觉。\n\n如果，觉得每次在 npm 后面加 --registry=http://192.168.80.130:7001 很麻烦，也可以通过\n\n```Bash\n    npm config set registry http://192.168.80.130:7001\n```\n\n设置默认的资源地址。\n\n#### 发布私有 npm 包\n\n在发布 npm 包之前，需要先将原先的 `config/config.js` 中添加一些配置属性：\n\n```JavaScript\n    enablePrivate: true, // 只有管理员可以发布 npm 包，默认为 false，即任何人都可以发布包\n    admins: {\n      admin: 'test@mycompany.com' // 管理员权限\n    },\n    scopes: ['@mycompany'], // 私有包必须依附于 scope 下\n```\n\n重新启动 cnpm 来应用这些配置。\n\n然后，新建一个简单的项目来测试发布，在 package.json 文件中加入代码：\n\n```JavaScript\n     \"name\": \"@mycompany/testjs\", // 包名，之前必须加入 scope 名\n     \"version\": \"1.0.2\"           // 版本信息\n```\n\n一切准备就绪就可以发布了。\n\n```Bash\n    npm login --registry=http://192.168.80.130:7001 // publish 之前需要登录用户\n    Username: admin // 管理员名\n    Password: whateveryoulike\n    Email: test@mycompany.com // 一定要填管理员账户邮箱地址，不然无法发布，因为设置了 enablePrivate: true\n\n    npm publish --registry=http://192.168.80.130:7001\n```\n\n看到以下信息就发布成功了。\n\n![发布成功](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/cnpm-publish-package.jpg)\n\n当然，其他机器也已经能通过\n\n```Bash\n   npm install @mycompany/test -registry=http://192.168.80.130:7001\n```\n\n来安装私有包了。\n\n与此同时，还可以访问 http://192.168.80.130:7002/package/@mycompany/test 来查看发布包的详细信息。\n\n![npm 包的基本信息和下载信息](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/cnpm-package-web.jpg)\n\n至此，cnpm 的搭建和使用已经全部完成，接下来我们看看 sinopia。\n\n### 2. sinopia\n\n#### sinopia 的安装\n\nsinopia 的安装极其简单，只需一行代码：\n\n```Bash\n    npm install -g sinopia\n```\n\nOk. 这就安装好啦，就这么简单。启动服务：\n\n```Bash\n    sinopia\n```\n\n看到以下图就启动成功：\n\n![启动成功](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/sinopia-server-start.jpg)\n\n**注意：**上面输出的两条信息相当重要\n\n1. 服务器中 sinopia 的配置文件存放的位置，后期的配置都需要修改这个文件\n2. sinopia 提供服务的地址，默认4873\n\n![访问localhost:4873](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/private-npm-server/sinopia-web.jpg)\n\nsinopia 就搭建完成了，还能更简单么？No。\n\n#### 配置 sinopia\n\nsinopia 搭建完成后就可以发布 npm 包了，上文已经叙述过如何发布，这里就不再重复了，主要还是说说如何个性化配置 sinopia。\n\n编辑刚刚 log 中输出的配置文件，可以配置 sinopia。\n\n生成的默认配置文件中的配置都是些常用配置：\n\n```\n    // config.yaml\n    storage: ./storage // npm 包存放的仓库位置\n    auth:   \n        htpasswd:  \n            file: ./htpasswd  \n            max_users: -1 // 允许注册用户的最大数，-1则不允许添加用户\n    uplinks:\n        npmjs:\n            url: https://registry.npmjs.org/ // 当资源本地不存在时，下载资源的地址，此处可以改为淘宝的镜像https://registry.npm.taobao.org\n    packages:\n      '@*/*':                           // 包通配符，这里匹配私有包\n        # scoped packages\n        access: $all                    // 下载权限，权限分为3种，分别是 $all 任何人，$anonymous 未登陆，$authenticated 登陆用户\n        publish: $authenticated         // 发布权限，登陆用户\n    \n      '*':\n        access: $all\n        publish: $authenticated\n        proxy: npmjs                    // 当资源本地不存在时，下载资源的地址，对应 uplinks 的配置\n\n```\n\n如果想要查看详细的可配置项，可以到 sinopia 的存储位置中查看，也可以上 [github](https://github.com/rlidwka/sinopia/blob/master/conf/full.yaml) 查看，每个配置项都有详细的注释。\n\n### 3. cnpm VS sinopia\n\n|   -   | cnpm | sinopia |\n| :---: | :---: | :---: |\n| 系统支持 | 非windows | 全系统 |\n| 安装 | 复杂 | 简单 |\n| 配置 | 较多，适合个性化需求较多的 | 较少 |\n| 配置——修改默认镜像 | 不支持 | 支持 |\n| 存储 | mysql | 文件格式，直观 |\n| 服务托管 | 默认后台运行 | pm2, doker, forever |\n\n#### 到底谁比较好？\n\n有言道：脱离业务场景谈解决方案，都是耍流氓。\n\n在此，我就不耍流氓了。\n\n#### 参考资料：\n\n1. [github cnpm](https://github.com/cnpm/cnpmjs.org)\n2. [Deploy a private npm registry in 5 minutes](https://github.com/cnpm/cnpmjs.org/wiki/Deploy-a-private-npm-registry-in-5-minutes)\n3. [MacPro 使用cnpmjs搭建私有npm服务](http://www.cnblogs.com/wyzfzu/p/4149310.html)\n4. [CNPM搭建私有的NPM服务](http://blog.fens.me/nodejs-cnpm-npm/)\n5. [github sinopia](https://github.com/rlidwka/sinopia)\n6. [Sinopia | 从零开始搭建npm仓库](http://mp.weixin.qq.com/s?__biz=MzA5Nzk5MzE3Ng==&mid=401510950&idx=1&sn=f775d53fa36e2a7284eb6399e0a0f6c1&scene=4#wechat_redirect)"
  },
  {
    "path": "src/server/data/posts/pwa-installable-and-share.md",
    "content": "> 系列文章：\n> \n> 1. [Service Workers 和离线缓存](https://discipled.me/posts/service-workers)\n> 2. [Notification with Service Workers push events](https://discipled.me/posts/notification-with-sw-push-events) \n> 3. PWA：添加应用至桌面及分享(本文)\n>\n\n继上两篇[离线缓存](https://discipled.me/posts/notification-with-sw-push-events)和[发送通知](https://discipled.me/posts/service-workers)之后，这篇是 PWA([progressive web apps](https://developer.mozilla.org/en-US/docs/Web/Apps/Progressive)) 相关的第三篇，也是计划中的最后一篇。\n\n这篇将讲述如何为应用添加两个小功能——添加应用至桌面和分享。虽然，这两个功能实现起来相当简单，可以说是没有什么代码量，但是，不要小看了这两个小功能，它们有可能会改变大格局。\n\n本篇主要包含以下内容：\n\n* [添加应用至桌面](#add-to-home-screen)\n* [Web Share API](#web-share-api)\n* [Bullshit or Prediction](#bullshit-or-prediction)\n\n<a name=\"add-to-home-screen\"></a>\n## 添加应用至桌面\n如果，你想要为你的网站添加添加到桌面这个功能，那么，你的网站只需满足以下 3 项就足够了：\n\n* 包含一个 `manifest.json` 文件，其中包含 `short_name` 以及 `icons` 字段\n* 包含 service sorkers\n* 使用 HTTPS（这个好像是废话，既然使用了 service workers，那肯定已经基于 https了）\n\n除此之外，chrome 会替你处理。\n\n从上面 3 点可以看到，如果你的应用已经是个 PWA 应用的话，那么，第二，第三点就已经满足了，添加至桌面的功能其实只需为项目添加一个描述性的配置文件 `manifest.json` 就可以了。\n\n那 `manifest.json` 这东西到底是啥？\n\n它是 PWA 的一部分，是一个 JSON 格式的文件用来描述应用相关的信息，目的是提供将应用添加至桌面的功能，从而使用户可以无需下载就可以如应用一般从桌面打开 web 应用，大大增强用户体验和粘性。\n\n[manifest](https://w3c.github.io/manifest/) 正处于 W3C 的草案阶段，并且 Chrome 和 Firefox 已经实现了这个功能，微软系也在开发中，只剩苹果系还在考虑。（大致和 service workers 的进程一样）\n\n知道了 manifest 是什么，接着就来看一下它怎么用，也就是可以配置哪些字段：\n\n* `short_name`: 应用展示的名字\n* `icons`: 定义不同尺寸的应用图标\n* `start_url`: 定义桌面启动的 URL\n* `description`: 应用描述，可以参考 meta 中的 description\n* `display`: 定义应用的显示方式，有 4 种显示方式，分别为：\n\t* `fullscreen`: 全屏\n\t* `standalone`: 应用\n\t* `minimal-ui`: 类似于应用模式，但比应用模式多一些系统导航控制元素，但又不同于浏览器模式\n\t* `browser`: 浏览器模式，默认值\n* `name`: 应用名称\n* `orientation`: 定义默认应用显示方向，竖屏、横屏\n* `prefer_related_applications`: 是否设置对应移动应用，默认为 `false`\n* `related_applications`: 获取移动应用的方式\n* `background_color`: 应用加载之前的背景色，用于应用启动时的过渡\n* `theme_color`: 定义应用默认的主题色\n* `dir`: 文字方向，3 个值可选 `ltr`(left-to-right), `rtl`(right-to-left) 和 `auto`(浏览器判断)，默认为 `auto`\n* `lang`: 语言\n* `scope`: 定义应用模式下的路径范围，超出范围会已浏览器方式显示\n\n需要注意的是自 `background_color` 开始的属性只有[部分浏览器支持](https://developer.mozilla.org/en-US/docs/Web/Manifest)。\n\n如果，你不知如何设置这些值，你可以试一试 [Manifest Generator](http://brucelawson.github.io/manifest/)，它会一步步指引你生成一个包含应用主要信息的 `manifest.json` 文件。\n\n除了以上列举的这些值，可能还包含其他一些某些浏览器特定的值，比如[上一篇](https://discipled.me/posts/notification-with-sw-push-events)中提到的 `gcm_sender_id`, `applicationServerKey` 用于 chrome 下订阅服务器消息。\n\n下面就是项目 `manifest.json` 最终的样子。\n\n```\n// manifest.json\n{\n  \"dir\": \"ltr\",\n  \"lang\": \"en\",\n  \"name\": \"D.D Blog\",\n  \"scope\": \"/\",\n  \"display\": \"standalone\",\n  \"start_url\": \"/\",\n  \"short_name\": \"D.D Blog\",\n  \"theme_color\": \"transparent\",\n  \"description\": \"Share More, Gain More. - D.D Blog\",\n  \"orientation\": \"any\",\n  \"background_color\": \"transparent\",\n  \"related_applications\": [],\n  \"prefer_related_applications\": false,\n  \"icons\": [{\n    \"src\": \"assets/img/logo/size-32.png\",\n    \"sizes\": \"32x32\",\n    \"type\": \"image/png\"\n  }, {\n    \"src\": \"assets/img/logo/size-48.png\",\n    \"sizes\": \"48x48\",\n    \"type\": \"image/png\"\n  } //...\n  ],\n  \"gcm_sender_id\": \"...\",\n  \"applicationServerKey\": \"...\"\n}\n```\n\n生成后的文件可以通过 [Web Manifest Validator](https://manifest-validator.appspot.com/) 进行验证。验证通过后，把它加入到项目，再次访问就会有添加到桌面的提示。\n\n![Add to home screen](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/pwa-installable-and-share/add-to-home-screen.jpeg)\n\n确定之后就能在桌面上看到了应用图标了。失手点了关闭也没有关系，可以通过 chrome 右上角的 `...` -> `Add to Home sceen` 手动添加。\n\n![Home screen](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/pwa-installable-and-share/home-screen.jpeg)\n\n点击添加桌面后，如果发现桌面没有应用图标，确认 chrome 是否有添加桌面快捷方式的权限。\n\n另外，通过[媒体查询](https://developer.mozilla.org/zh-CN/docs/Web/CSS/%40media/display-mode)可以根据不同的 `display` 模式来应用不同的 CSS 样式。\n\n还有一点需要特别注意，用户将应用添加到桌面后，你修改 `minifest.json` 文件将不会生效，除非用户重新将它添加到桌面，所以，尽量还是一步到位。\n\n如果这篇到这里就结束就未免有点太短了，有点不太符合我有事没事往长里写的风格。前一阵正好看到一篇关于 web 分享 API 的[文章](https://github.com/xitu/gold-miner/blob/master/TODO/why-do-we-need-a-new-api.md)，虽然，它不属于 PWA 的一系列技术中，但它实现的功能和理念与 PWA 相当相似——渐进式地提供功能。这里就放在一起讲一讲，也顺便给自己的博客添加这个功能。\n\n<a name=\"web-share-api\"></a>\n## Web Share API\nWeb Share API 和 PWA 一样是一项由古哥提出的[草案](https://github.com/WICG/web-share)，现还未被纳入 W3C。通过 Web Share API，用户可以方便地将内容或数据分享到应用中。\n\n不过，现在只有安卓 Chrome 55 以上支持 Web Share API。另外，要使用分享功能，还要满足以下几点：\n\n* 网站必须基于 HTTPS\n* 注册 [Origin Trial](https://github.com/jpchase/OriginTrials/blob/gh-pages/developer-guide.md)，并将生成的 token 加入页面 meta 中\n* 提供 `text` 或 `url` 中的一项，且值必须为字符串\n* 分享事件必须由用户事件触发\n\n满足了这些剩下的就很简单了，只需监听用户事件，然后将需要分享的内容传递给 Web Share API 就可以了。\n\n```\n// CommonService.js\nexport const isSupportShareAPI = () => !!navigator.share;\n\nexport const sharePage = () => {\n\tnavigator\n\t\t.share({\n\t\t\ttitle: document.title,\n\t\t\ttext: document.title,\n\t\t\turl: window.location.href\n\t\t})\n\t\t.then(() => console.info('Successful share.'))\n\t\t.catch(error => console.log('Error sharing:', error));\n};\n```\n\n![Web share](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/pwa-installable-and-share/web-share.jpeg)\n\n如果，你的网站设有[元数据](https://discipled.me/posts/structure-data)，那么，分享的内容可以从网页元数据中获取。\n\n由于，Web Share 是由 chrome 团队单方面提出，即使是在 chrome 下也是实验性支持，之后是否会永久支持尚未定论，不排除以后不再支持的可能。\n\n这次分享的两个功能：添加到桌面和分享至应用就这样搞定了，加之前两次分享的离线缓存以及推送通知，就完成了现有 PWA 应用所包含的全部功能。\n\n<a name=\"bullshit-or-prediction\"></a>\n## Bullshit or Prediction\n总体来说，实现 PWA 的功能并不困难，甚至可以说是简单。但就如同文章之初所说，这可能是一个影响移动端格局的技术。在此之前，web 技术只用应用于浏览器中，无论做什么之前都得先打开浏览器。然而，PWA 所提供添加至桌面、推送消息及离线缓存这些功能，使得对用户来说网页应用和移动应用真的是分不清楚，也不必分清楚...\n\n当然，要使用户有使用应用的感觉，这里就得提一提另一个东西，那就是设计。在将网站转换为 PWA 的同时，这个转变不应只发生在 JS 方面，用户感受最明显的还是网站的外观，也就是用户界面。界面设计也应随着网站转换成 PWA 而进行重新设计，从而给用户真正带来类应用的体验。我个人认为如果 PWA 顺利推行，那么，网站的界面设计同时也会迎来一次巨大革新，就如同之前 jsp 到单页应用般巨大的改变。\n\n不过，这里还是得浇一盆冷水，鉴于我国网络现状，我同[这篇回答](https://www.zhihu.com/question/46690207)中的观点基本一致，就我国苹果机的占比来说，如果苹果不支持 PWA，那么，它也就只有自己拿来玩玩了。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/pwa-installable-and-share/ditou.jpg)\n\n倘若，苹果也加入到 PWA 的行列，浏览器兼容性不再成为障碍时，PWA 必然会改变前端与移动端之间的格局，再加之 [AOT(ahead-of-time)](http://asmjs.org/spec/latest/) 与 [WebAssembly](http://webassembly.org/) 为 JS 带来的性能上的突破，JS 将撼动所有领域，从移动端（PWA），到[桌面应用](https://github.com/electron/electron)，[物联网](https://www.postscapes.com/javascript-and-the-internet-of-things/)，[VR](https://github.com/mrdoob/three.js)，[AR](https://github.com/aframevr/aframe)，[游戏](http://www.jianshu.com/p/0469cd7b1711)，乃至[人工智能](https://github.com/karpathy/convnetjs)[等等](https://www.zhihu.com/question/20796866)，画美不看。\n\n妄言或许会成预言。\n\n> Atwood's Law: any application that can be written in JavaScript, will eventually be written in JavaScript.\n\n我们正处于一个前端最好的时代，未来可期...\n\n扯了这么多，最后当然还是希望对[本人博客](https://discipled.me)有兴趣的小伙伴可以试一试这次分享的两个功能，把[本人博客](https://discipled.me)添加到桌面并分享给自己的小伙伴们。\u0011\u0005\u0005\u0005🤗\n\n支持离线查看噢（得先访问过），没网的时候也能涨姿势了哪...(不用连啥花生 wifi 之类的了[手动滑稽])"
  },
  {
    "path": "src/server/data/posts/redux-advanced.md",
    "content": "> 系列文章:\n> 1. [Redux 入门](http://discipled.me/posts/getting-started-with-redux)\n> 2. Redux 进阶(本文)\n> 3. [番外篇: Vuex — The core of Vue application](http://discipled.me/posts/vuex-core-of-vue-application)\n\n在之前的[文章](http://discipled.me/posts/getting-started-with-redux)中，我们已经了解了 Redux 到底是什么，用来处理什么样的问题，并创建了一个简单的 [TodoMVC Demo](https://github.com/DiscipleD/angular-redux-todoMVC)。但是，我们同样遗留了一些问题没有处理，比如：异步处理、中间件、模板绑定等，这些问题我们将在这篇文章中通过一个简单的天气预报 Demo 来一一梳理（[查看源码点这里](https://github.com/DiscipleD/Redux-demo/tree/master/src/weather-forecast)）。\n\n![Demo preview](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/redux-advanced/weather-forecast-demo.png)\n\n在开始新的内容之前，先快速回顾一下[上一篇](http://discipled.me/posts/getting-started-with-redux)的内容。\n\n### Action, Reducer & Store\n创建一个基于 Redux 状态管理的应用时，我们还是从创建 Redux 的核心开始。\n\n首先，建立 Action。假设，发出请求和收到请求之间有一个 loading 的状态，那么，我们将查询天气这个行为划分为 2 个 action，并为此创建 2 个工厂函数。\n\n```JavaScript\nexport const QUERY_WEATHER_TODAY = 'QUERY_WEATHER_TODAY'\nexport const RECEIVE_WEATHER_TODAY = 'RECEIVE_WEATHER_TODAY'\n\nexport function queryWeatherToday(city) {\n\treturn {\n\t\ttype: QUERY_WEATHER_TODAY,\n\t\tcity\n\t}\n}\n\nexport function receiveWeatherToday(weatherToday) {\n\treturn {\n\t\ttype: RECEIVE_WEATHER_TODAY,\n\t\tweatherToday\n\t}\n}\n```\n然后，为 Action 创建相应的 Reducer，不要忘了 Reducer 必须是一个纯函数。\n\n```JavaScript\nexport default function WeatherTodayReducer(state = {}, action) {\n\tswitch (action.type) {\n\t\tcase QUERY_WEATHER_TODAY:\n\t\t\treturn { load: true, city: action.city }\n\t\tcase RECEIVE_WEATHER_TODAY:\n\t\t\treturn { ...state, load: false, detail: action.weatherToday}\n\t\tdefault:\n\t\t\treturn state\n\t}\n}\n```\n最后是 Store。\n\n```JavaScript\nimport { createStore } from 'redux'\nimport WeatherForecastReducer from '../reducers'\nimport actions from '../actions'\n\nlet store = createStore(WeatherForecastReducer)\n// Log the initial state\nconsole.log('init store', store.getState())\n\nstore.dispatch(actions.queryWeatherToday('shanghai'))\n\nconsole.log(store.getState())\n\nstore.dispatch(actions.receiveWeatherToday({}))\n\nconsole.log(store.getState())\n\nexport default store\n```\n启动应用之后，就能在控制台中看到一下的输出。\n\n![控制台输出](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/redux-advanced/base_redux_console.png)\n\n回顾了之前的内容以后，那我们就进入正题，来看一些新概念。\n\n### 中间件\n相信大家对中间件这个词并不陌生，Redux 中的中间件和其他的中间件略微有些不同。它并不是对整个 Redux 进行包装，而是对 `store.dispatch` 方法进行的封装，是 action 与 reducer 之间的扩展。\n\n[Redux 官网](http://redux.js.org/docs/advanced/Middleware.html)一步一步详细地演示了中间件产生的原因及其演变过程，在此我就不再多做赘述了。\n\n中间件在真正应用中是必不可少的一环，或许你不需要写一个中间件，但理解它会对你运用 Redux 编写代码会有很大的帮助。\n\n### 异步请求\n在上一篇文章中有提到，为了保证 reducer 的纯净，Redux 中的异步请求都是由 action 处理。\n\n但是，reducer 需要接收一个普通的 JS 对象，action 工厂返回一个描述事件的简单对象，那我们的异步方法该怎么处理哪？这就需要我们刚才提到的中间件来帮忙了，添加 [redux-thunk](https://github.com/gaearon/redux-thunk) 这个中间件，使我们的 action 得到增强，使得 action 不单能返回对象，还能返回函数，在这个函数中还可以发起其他的 action。\n\n其实，redux-thunk 这个中间件也没有什么特别之处，在 [Redux 官网](http://redux.js.org/docs/advanced/Middleware.html)的案例最后已经简单地实现了它。\n\n```JavaScript\n/**\n * 虽然，中间件是对 store.dispatch 的封装，但它是添加在整个 store 上\n * 所以，函数能传递 `dispatch` 和 `getState` 作为参数\n *\n * redux-thunk 的逻辑就是判断当前的 action 是不是一个函数，是就执行函数，不是就继续传递 action 给下一个中间件\n */\nconst thunk = store => next => action =>\n  typeof action === 'function' ?\n    action(store.dispatch, store.getState) :\n    next(action)\n```\n于是，我们就修改一下之前的 action，给它添加一个异步请求。\n\n```JavaScript\nexport const QUERY_WEATHER_TODAY = 'QUERY_WEATHER_TODAY'\nexport const RECEIVE_WEATHER_TODAY = 'RECEIVE_WEATHER_TODAY'\n\nconst queryWeatherToday = city => ({\n\ttype: QUERY_WEATHER_TODAY,\n\tcity\n})\n\nconst receiveWeatherToday = weatherToday => ({\n\ttype: RECEIVE_WEATHER_TODAY,\n\tweatherToday\n})\n\nexport function fetchWeatherToday(city) {\n\treturn dispatch => {\n\t\tdispatch(queryWeatherToday(city))\n\n\t\treturn fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${CONFIG.APPID}`)\n\t\t\t.then(response => response.json())\n\t\t\t.then(data => dispatch(receiveWeatherToday(data)))\n\t}\n}\n```\n\n既然，我们用了中间件，那就要在 createStore 的时候装载中间件。\n\n```JavaScript\nimport { createStore, applyMiddleware } from 'redux'\nimport thunkMiddleware from 'redux-thunk'\nimport createLogger from 'redux-logger'\n\nimport WeatherForecastReducer from '../reducers'\nimport actions from '../actions'\n\nconst loggerMiddleware = createLogger()\n\nconst store = createStore(\n\tWeatherForecastReducer,\n\tapplyMiddleware(\n\t\tthunkMiddleware,\n\t\tloggerMiddleware\n\t)\n)\n\nstore.dispatch(actions.fetchWeatherToday('shanghai'))\n\nexport default store\n```\n\n这时，再看看应用的控制台。\n\n![添加中间件后，控制台输出](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/redux-advanced/middleware_redux_console.png)\n\nOK，Redux 核心的功能我们基本完成，我们继续看看如何将它同界面绑定在一起。\n\n### 模板绑定\n官网的例子都是 Redux 搭配 React，用的是 [react-redux](https://github.com/gaearon/react-redux)；然而，本文一直是以 Angular 来写的例子，所以，这里就用到另一个 redux 生态圈中的项目 [angular-redux](https://github.com/angular-redux)。它其中包含了 2 个不同的库，ng-redux 和 ng2-redux，分别对应 Angular 1.x 和 Angular 2 两个版本。\n\n当然，我们这里使用 [ng-redux](https://github.com/angular-redux/ng-redux)。之前那些章节和官网讲述的可能相差不大，但这部分就有所区分了。\n\nreact-redux 提供一个特殊的 React 组件 `Provider`，它通过 React [Context](https://facebook.github.io/react/docs/context.html) 特性使每个组件不用显示地传递 store 就能使用它。\n\nng-redux 当然不能使用这种方式，但它可以使用 angular 自己的方式——依赖注入。\n\nng-redux 是一个 `provider`，它包含了所有 Redux store 所有的 API，额外只有 2 个 API，分别是 `createStoreWith` 和 `connect`。\n\n其中，`createStoreWith` 显而易见是用来创建一个 store，参数同 Redux 的 `createStore` 方法差不多，原有创建 store 的方法就用不到了，之前的 store.js 也就被合并到了应用启动的 index.js 里。\n\n```JavaScript\nimport angular from 'angular'\nimport ngRedux from 'ng-redux'\nimport thunkMiddleware from 'redux-thunk'\nimport createLogger from 'redux-logger'\n\nimport './assets/main.css'\nimport WeatherForecastReducer from './reducers'\nimport Components from './components'\n\nconst loggerMiddleware = createLogger()\n\nangular.module('WeatherForecastApp', [ngRedux, Components])\n\t.config($ngReduxProvider => {\n\t\t$ngReduxProvider.createStoreWith(\n\t\t\tWeatherForecastReducer,\n\t\t\t[thunkMiddleware, loggerMiddleware]\n\t\t)\n\t})\n```\n这样应用的 store 就建立好了。\n\n另一个 API `connect` 的用法同 react-redux 的 `connect` 方法差不多，用于将 props 和 actions 绑定到 template 上。\n\nAPI 签名是 `connect(mapStateToTarget, [mapDispatchToTarget])(target)`。\n\n其中，`mapStateToTarget` 是一个 `function`，`function` 的参数是 state，返回 state 的一部分，即 select；`mapDispatchToTarget` 可以是**对象或函数**，如果是对象，那么它的每个属性都必须是 actions 工厂方法，这些方法会自动地绑定到 `target` 对象上，也就是说，如果用之前定义好的 action，这边就不需要做任何的修改；如果是函数，那么这个函数会被传递 dispatch 作为参数，而且这个函数需要返回一个对象，如何 dispatch action 就由你自己设定，同时这个对象的属性也会绑定到 `target` 对象上。\n\n最后的 `target` 就是目标对象了，也可以是函数，如果是函数的话，前面所传的 2 个参数会作为 `target` 函数的参数。\n\n好了，扯了这么多概念，估计你也晕了。\nTalk is sxxt，show me the code!\n\n```JavaScript\n// query-city/controller.js\nimport actions from '../../actions'\n\nexport default class QueryCity {\n\tconstructor($ngRedux, $scope) {\n\t\tconst unsubscribe = $ngRedux.connect(null, actions)(this)\n\t\t$scope.$on('$destroy', unsubscribe)\n\t}\n}\n\n// today-weather-board/controller.js\nexport default class TodayWeatherBoardCtrl {\n\tconstructor($ngRedux, $scope) {\n\t\tconst unsubscribe = $ngRedux.connect(this.mapStateToThis)(this);\n\t\t$scope.$on('$destroy', unsubscribe);\n\t}\n\n\tmapStateToThis(state) {\n\t\treturn {\n\t\t\tweatherToday: state.weatherToday\n\t\t};\n\t}\n}\n```\n这样，controller 是不是变得很简洁？\n\n![上天咯](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/redux-advanced/go_to_heaven.png)\n\nWeather Forecast 部分基本和之前的部分相同，唯一的一处小修改就是把 QueryCity 控制器里添加一个方法，在方法里调用 2 个不同的 action 来替换之前按钮上直接绑定的 action。\n\n于是，我们的天气预报应用就成了这样。\n\n![应用预览](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/redux-advanced/connect_template.png)\n\n### 路由切换\n一个真实的项目肯定会用到路由切换，路由状态也是应用状态的一部分，那么它也应当由 Redux 来统一管理。\n\n谈到 Angular 的路由，那必须提到 ui-router。那 ui-router 怎么整合到由 Redux 管理的项目中哪？答案是：[redux-ui-router](https://github.com/neilff/redux-ui-router)。\n\n使用 redux-ui-router 同样也有 3 点要注意：\n\n* 使用 store 来管理应用的路由状态\n* 使用 action 代替 $state 来触发路由的变更\n* 使用 state 代替 $stateParams 来作为路由参数\n\n记住这些就可以动手开工了。首先，安装依赖：\n\n```Bash\nnpm install angular-ui-router redux-ui-router --save\n```\n这里有一点要注意，redux-ui-router 虽然依赖 angular-ui-router，但它不会帮你自动安装，需要你自己额外手动安装，虽然你项目里不需要引入 angular-ui-router 模块。\n\n安装完依赖之后，就把它引入到我们项目中，项目的 index.js 就变为了\n\n```JavaScript\nimport angular from 'angular'\nimport ngRedux from 'ng-redux'\nimport ngReduxUiRouter from 'redux-ui-router'\nimport thunkMiddleware from 'redux-thunk'\nimport createLogger from 'redux-logger'\n\nimport './assets/main.css'\nimport { current, forecast } from './Router'\nimport App from './app/app'\nimport WeatherForecastReducer from './reducers'\nimport Components from './components'\n\nconst loggerMiddleware = createLogger()\n\nangular.module('WeatherForecastApp', [ngReduxUiRouter, ngRedux, App, Components])\n\t.config(($urlRouterProvider, $stateProvider) => {\n\t\t$urlRouterProvider\n\t\t\t.otherwise('/current')\n\n\t\t$stateProvider\n\t\t\t.state('current', current)\n\t\t\t.state('forecast', forecast)\n\t})\n\t.config($ngReduxProvider => {\n\t\t$ngReduxProvider.createStoreWith(\n\t\t\tWeatherForecastReducer,\n\t\t\t[thunkMiddleware, loggerMiddleware, 'ngUiRouterMiddleware']\n\t\t)\n\t})\n```\n项目中只需引入 `ngReduxUiRouter` 模块，而不用再引入 ui-router 模块到应用中。ui-router 的路由声明就不在这里赘述了，网上的资料也是大把大把的。\n\n接着，将 `'ngUiRouterMiddleware'` 添加到中间件中，这样距离完工就只剩最后一步了。\n\n那就是修改主 Reducer 文件，将路由的 Reducer 合并到主 Reducer中，\n\n```JavaScript\nimport { combineReducers } from 'redux'\nimport { router } from 'redux-ui-router'\nimport weatherToday from './WeatherToday'\nimport weatherForecast from './WeatherForecast'\n\nexport default combineReducers({\n\tweatherToday,\n\tweatherForecast,\n\trouter\n})\n```\n\nOK，大工告成。现在，如果你刷新界面就应该能看到控制台中已经输出了 `type` 为 `@@reduxUiRouter/$stateChangeStart` 和 `@@reduxUiRouter/$stateChangeSuccess` 的 action log。此时，如果页面上使用 `ui-sref` 来切换应用路由状态的话，同样也能看到 redux-logger 输出的日志。\n\n在这个 Demo 里，我就不直接使用 `ui-sref`，而是用例子来说明刚刚提到的 3 点中的第二点：**使用 action 代替 $state 来触发路由的变更**。\n\n```JavaScript\nimport { stateGo } from 'redux-ui-router'\n\nexport default class NavBarCtrl {\n\tconstructor($ngRedux, $scope) {\n\t\tconst routerAction = { stateGo }\n\t\tconst unsubscribe = $ngRedux.connect(this.mapStateToThis, routerAction)(this)\n\t\t$scope.$on('$destroy', unsubscribe)\n\t}\n\n\tmapStateToThis(state) {\n\t\treturn {\n\t\t\trouter: state.router\n\t\t}\n\t}\n}\n```\n\n从代码中可以看到，先从 redux-ui-router 里引入了 `stateGo` 方法，然后通过上一节所说的模板绑定，将这个方法绑定到当前的模板上，于是在模板中就可以使用 `$ctrl.stateGo()` 方法来跳转路由。\n\n那为什么说这就满足了刚刚的第二点哪？查看[源码](https://github.com/neilff/redux-ui-router/tree/master/src)就可以发现，redux-ui-router 提供的 `stateGo(to, params, options)`等 API 也只是个再普通不过的 action 工厂方法，返回一个特定 type 的 action。\n\n路由的切换是在之前添加的中间件中，做了一个类似 reducer 的处理，根据不同的 action type 触发不同的路由事件。\n\n举一反三，通过模板绑定我们可以获得当前应用的 state。那么，我们同样可以用过调用 `$ctrl.stateGo()` 等方法给路由切换添加参数来做到**使用 state 代替 $stateParams 来作为路由参数**。\n\n顺便说一句，redux-ui-router 似乎还没有支持 angular-ui-router 中的 View Load Events，如果你看懂了我刚刚所说的，那么 pr 走起。\n\n### 写在最后\n\n一不小心写了那么长，文笔又不是很好，不知有多少人看完了，希望大家都有所收获。\n\n其中，也有不少细节也没有细说，有疑问的就留言吧。\n\n在学习的过程中发现还有不少相关的知识可以扩展，应该还会有下一篇。\n\n最后，最重要的当然是附上[源码](https://github.com/DiscipleD/Redux-demo/tree/master/src/weather-forecast)。"
  },
  {
    "path": "src/server/data/posts/remote-debugging-devices.md",
    "content": "做过移动端开发的童鞋相信一定遇到过，页面在自己电脑上模拟各种手机都跑的好好的，但当程序正真在真机上运行时，总会遇到一些问题。\n\n有了问题就得要解决啊，这时你肯定想手机上要是能打开控制台该有多好啊~\n\n办法当然是有滴。\n\n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/go-heaven.jpg)\n\n首先，当然是来看看土豪们用的机器 iphone。\n\n### Safari\niOS 系统默认的浏览器是 safari，调试 safari 只需一下简单几步。\n\n1. 打开手机上的 web 检查器  \n通过【设置】>【Safari】>【高级】>【Web检查器】打开  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/ios-open-inspect.png)\n2. 链接手机到电脑。链接上了以后，手机直接访问网页就可以了。\n3. 电脑上打开 safari，点击 【开发】 菜单栏  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/ios-connect.jpg)  \n然后就能看到手机上访问的页面内容了  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/ios-inspect-result.png)\n注：如果 safari 菜单栏上没有 【开发】，可以通过【偏好设置】>【高级】来设置  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/ios-safari-menu.png)\n\n如果是本地调试的话，有一点要注意。iphone 通过 ip 地址访问会链接不上，需要通过`用户名.local:端口号`的方式访问，我这里就是 david-2.local:8080。\n\n看完 iOS，当然就要看看 Android。\n\n### Chrome\n打开 Android 机上的 Chrome 控制台同上面的方法大同小异。\n\n1. 手机进入开发者模式，启用 USB 调试\n2. 链接手机到电脑\n3. 打开 Chrome，访问网页\n4. 电脑上同样打开 Chrome，打开控制台，通过【···】>【More tools】>【Inspect devices】  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/chrome-open-inspect.png)  \n5. 选择 Devices，然后 Inspect 就可以了  \n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/chrome-choose-devices.png)\n\n注意：安卓机访问本地服务器，用 ip 地址就可以了，即 192.168.1.4:8080。\n\nSafari 和 Chrome 都了解完了，是不是就结束了？当然不是，别忘了国内一大浏览渠道微信。\n\n### 微信浏览器\n要调试微信浏览器，就需要额外下一个软件——[微信 web 开发者工具](http://mp.weixin.qq.com/wiki/10/e5f772f4521da17fa0d7304f68b97d7e.html)。\n\n安装完成后打开软件，切换到【移动调试】，根据提示操作就可以了。\n\n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/wechat-tool.png)\n\n> 如果，控制台中出现 `weixin://preInjectJSBridge/fail` 的错误，可能是使用了不兼容的语法，加入相应 polyfill 可以解决。\n\n如果，你以为到此就结束了，那就图样图森破了。\n\n![](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/naive.jpg)\n\n国内**绝大部分**安卓机用户都用的不是 chrome，用的都是 UC、QQ 或者自带浏览器之类，水太深啊。\n\n### Weinre\n\n刚刚使用微信开发工具的时候，文档上有提到它是基于 weinre 的，那 weinre 是什么，能帮我们解决问题么？\n\n> weinre was built in an age when there were no remote debuggers available for mobile devices.\n\n它几乎支持各种新老浏览器，而且，安装和使用也很方便，具体安装方法在这篇[文章](http://yujiangshui.com/multidevice-frontend-debug/#使用-Weinre-调试)中写得很详细了。\n\n但，再如何方便不还得装么，而且还要修改当前的代码，那能不能有更好的办法？\n\n### BrowserSync\nBrowserSync 或许大家都有所了解，不了解的可以看一下我之前介绍它的[文章](http://discipled.me/posts/browsersync)。\n\nBrowserSync v2.0.0 之后就默认提供了对 weinre 的支持，当你使用 BrowserSync 启动 server 时，可以访问 browsersync 的系统面板来开启 remote debugger。\n\n控制面板的地址在 server 启动后的控制台上看到，默认为当前 server 端口号 +1，即 server 端口是 3000，那么，browsersync 系统面板的端口就是 3001。\n\n![BrowserSync Remote Debug config](//o7nu3cbe9.bkt.clouddn.com/blog/remote-debugging-devices/browser-sync.png)\n\n这样，既不用修改任何一行代码，又能在任何机器的任何浏览器上使用，是不是很完美？\n\n### 写在最后\n最后，当然还是继续安利下自己的 [Blog](http://discipled.me/)。\n\n在之前将 vue 升级到 vue 2.0 并加入 vuex 之后，现又加入 graphql-js，并将 vue-router 切换到了 `history` 模式（点击查看[源码](https://github.com/DiscipleD/blog)）。\n\n#### 参考资料\n1. [如何在移动设备上调试网页](http://www.codingserf.com/index.php/2014/05/debug-on-devices/)\n2. [Remote Debugging Android Devices](https://developers.google.com/web/tools/chrome-devtools/debug/remote-debugging/remote-debugging?hl=en)\n3. [微信页面开发遇到preInjectJSBridge错误](https://segmentfault.com/q/1010000004605740)\n4. [移动端前端开发调试](http://yujiangshui.com/multidevice-frontend-debug/)\n5. [Weinre和Browsersync - 跨设备前端调试](http://andward.github.io/weinre/browsersync/%E5%89%8D%E7%AB%AF/2015/09/17/weinre-and-browsersync.html)"
  },
  {
    "path": "src/server/data/posts/service-workers.md",
    "content": "> 系列文章：\n> \n> 1. Service Workers 和离线缓存 (本文)\n> 2. [Notification with Service Workers push events](https://discipled.me/posts/notification-with-sw-push-events)\n> 3. [PWA：添加应用至桌面及分享](https://discipled.me/posts/pwa-installable-and-share)\n>\n\n第一次听到 Service Workers 这个词还是在去年 Google 来安利 Angular 2 的时候，那时就觉得很惊艳，想搞一搞，但是因为没把网站升级成 https 一直拖到现在。[不久前](https://discipled.me/posts/docker-compose)，把网站升级成了 https，终于可以搞一发了。\n\n本篇主要包含以下内容：\n\n* [What's Service Workers?](#Whats-service-workers)\n* [小试 Service Workers](#try-service-workers)\n* [调试 Service Workers](#debug-service-workers)\n* [通过 postMessage 与主窗口通信](#postmessage)\n* [为应用添加离线缓存](#offline-cache)\n* [Service workers 的生命周期与更新](#lifecycle-and-update)\n\n当然，还是先来看看 Service Workers 究竟是什么？\n\n<a name=\"Whats-service-workers\"></a>\n## What's Service Workers?\nService Workers 是谷歌 chrome 团队提出并大力推广的一项 web 技术。在 2015 年，它加入到 W3C 标准，进入[草案阶段](https://www.w3.org/TR/service-workers/)。W3C 标准中对 Service Workers 的解释太细致，相对而言，我更喜欢 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) 上的解释，更简练，更易于理解。\n\n> Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs. - MDN\n\n简单翻译一下：Service workers 基本上充当应用同服务器之间的**代理服务器**，可以用于拦截请求，也就意味着可以在离线环境下响应请求，从而提供更好的离线体验。同时，它还可以接收服务器推送和后台同步 API。\n\n那么，这项技术的浏览器支持情况是什么样，还是来看一眼 Can I use?\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/can-i-use.png)\n\n可以从看到，Chrome 和 Firefox, Opera 都已经支持 Service Workers，底下的备注也写到 Edge 在开发中，Safari 也考虑支持。至于 IE，[船长都跳船了](https://www.microsoft.com/en-us/WindowsForBusiness/End-of-IE-support)。看了 PC 端，再来看看移动端。移动端的支持率并不尽如人意，不过在安卓 4.4 之后，安卓原生浏览器，以及安卓版的 Chrome 都已经开始支持 Service Workers。\n\n说句题外话，突然发现在 Can I use 中选择导入我国数据时，竟出现了 UC 和 QQ 浏览器的支持情况，口以口以\u0005\u0005\u0017\u0017👍...\n\n言归正传，在真正开始使用 Service Workers 之前，还有几点要注意：\n\n1. Service Workers 基于 Https，这是硬性条件（如何升级 https 可以参考[上一篇文章](https://discipled.me/posts/docker-compose#Letsencrypt)）\n2. 每个 Service Worker 都有自己的作用域，它只会处理自己作用域下的请求，而 Service Worker 的存放位置就是它的最大作用域\n3. Service Workder 是 Web Worker 的一种，它不能够直接操作 DOM\n\nGithub 上有一个[非常棒的资源](https://github.com/delapuente/service-workers-101)，它用图片的方式展示了 Servic Workers 的一些核心要点。\n\n搞定这些基础就可以正式开搞了...\n\n<a name=\"try-service-workers\"></a>\n## 小试 Service Workers\n和其他 worker 一样，service worker 有一个独自的文件。由于之前所提到的 service worker 只能作用在自己存放位置之下的文件，所以，一般在应用根目录下存放 service worker 文件。\n\n首先，先写一个最简单的来看看浏览器是不是支持，以及能否正确地安装并运行 service worker。\n\n```JavaScript\n// service-worker.js\nconst _self = this;\n\nconsole.log('In service worker.');\n\n_self.addEventListener('install', function () {\n\tconsole.log('Install success');\n});\n\n_self.addEventListener('activate', function () {\n\tconsole.log('Activated');\n});\n```\n\n虽然，service worker 是 web worker 其中的一种，但它有些不同，它有自己的注册方式。\n\n```JavaScript\n// ServiceWorkerService.js\nconst SERVICE_WORKER_API = 'serviceWorker';\nconst SERVICE_WORKER_FILE_PATH = 'service-worker.js';\n\nconst isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;\n\nif (isSupportServiceWorker()) {\n\tnavigator\n\t\t.serviceWorker\n\t\t.register(SERVICE_WORKER_FILE_PATH)\n\t\t.then(() => console.log('Load service worker Success.'))\n\t\t.catch(() => console.error('Load service worker fail'));\n} else {\n\tconsole.info('Browser not support Service Worker.');\n}\n```\n\n重启程序之后，你应该就能在控制台中看到 `Load service worker Success.`。然而，却没有另两句的输出，难道加载失败了？但是，控制台不是显示加载成功了么？不要担心，程序没有出错，只是 service worker 中的日志信息有它自己的输出位置，而并非输出在主日志之中。\n\n接下去，先来看看如何调试 service worker。\n\n<a name=\"debug-service-workers\"></a>\n## 调试 Service Workers\n在 Chrome 中，service worker 的信息显示在 `Application -> Service Workers` 中，就像这样\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/chrome-console-application-service-worker.png)\n\n里面会显示注册的 service worker，以及它当前的状态。还能通过切换最上面的选项来模拟不同的网络环境，测试在不同环境下 service worker 的响应，它们分别是：\n\n* Offline: 离线\n* Update on reload: 加载时更新\n* Bypass for network: 使用网络内容\n\n回到之前的问题，如何查看 service worker 之中的日志哪？只需点击图中的 `inspect` 链接，它会弹出另一个开发者窗口，在里面可以查看 service worker 的日志。是不是觉得需要那么多步有点麻烦，别担心，Chrome 已经替我们解决了这个烦恼。重新刷新页面后，Chrome 的开发者工具中已经能够查看 service workers 的信息了，比如：在 console 选项卡勾选 `Show all messages` 就能显示 service workers 中控制台的信息；在 source 选项卡也能看到 service workers 的代码，当然也可以打断点啦~\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/chrome-console-show-all-messages.png)\n\n在 firefox 中，默认会将 service worker 中的日志输出到主控制台中，但要打开 service worker 的调试器就有点麻烦了。有两种方法查看，一个是在地址栏中输入 `about:debugging#workers`，另一种就是通过菜单栏中选择 `Tools -> Web Developer -> Service Workers`。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/firefox-debugging-service-workers.png)\n\n更多关于在 firefox 中调试 service workers 的信息可以[点此查看](https://hacks.mozilla.org/2016/03/debugging-service-workers-and-push-with-firefox-devtools/)。\n\n虽然，已经将日志输出到主控制台了，可这里就有个疑问了，主页能不能获取 service workers 中的信息哪？答案是肯定的，那就是通过 `postMessage`。\n\n<a name=\"postmessage\"></a>\n## 通过 postMessage 与主窗口通信\n和 web worker 一样，service worker 与主窗口通讯也需要通过 `postMessage`，但它的语法又有些许不同。\n\n首先，是主页面给 service worker 发消息。\n\n```JavaScript\n// ServiceWorkerService.js\nconst sendMessageToSW = msg => navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);\n\nif (isSupportServiceWorker()) {\n\tconst sw = navigator.serviceWorker;\n\n\tsw.register(SERVICE_WORKER_FILE_PATH)\n\t\t.then(() => console.log('Load service worker Success.'))\n\t\t.catch(() => console.error('Load service worker fail'))\n\t\t.then(() => sendMessageToSW('Hello, service worker.'))\n\t\t.catch(() => console.error('Send message error.'));\n} else {\n\tconsole.info('Browser not support Service Worker.');\n}\n```\n\n可以看到，`postMessage` 方法并不在 worker 实例下，而是在 serviceWorker 下的 controller 对象下。这里需要注意一下，当 service worker 还没有注册成功时，`navigator.serviceWorker.controller` 对象的值是 `null`，所以，在调用 `postMessage` 之前需要确保 `controller` 对象已经存在。在 service worker 这边就没有什么区别了\n\n```JavaScript\n// service-worker.js\n_self.addEventListener('message', function(event) {\n\tconsole.log(event.data);\n});\n```\n\n是不是很简单？不过，反过来 service worker 给主页面发消息就要复杂一点了。在 service worker 里发送信息需要通过 [`Client`](https://developer.mozilla.org/en-US/docs/Web/API/Client) 对象的 `postMessage` 方法。获取 `Client` 的方法有很多，比如，刚从主页面发来的消息，事件的来源就是一个 `Client` 对象，即 `event.source`。不过，这只能向来源发消息，但如果你开了几个网页，或者不是通过主页消息发来的该怎么办哪？方法还是有的，在 service workers 中可以通过 `clients` 来获取所有的页面对象或其他的 service workers。\n\n```JavaScript\n// service-worker.js\n_self.clients.matchAll().then(function(clients) {\n\tclients.forEach(function(client) {\n\t\tclient.postMessage('Service worker attached.');\n\t})\n});\n```\n\n不过，如果你发出一个消息需要等到另一方的返回的消息做处理，上述的办法就做不到了。这时就需要建立一个通道来处理了，修改一下之前的 `sendMessageToSW` 方法。\n\n```JavaScript\n// ServiceWorkerService.js\nconst sendMessageToSW = msg => new Promise((resolve, reject) => {\n\tconst messageChannel = new MessageChannel();\n\tmessageChannel.port1.onmessage = event => {\n\t\tif (event.data.error) {\n\t\t\treject(event.data.error);\n\t\t} else {\n\t\t\tresolve(event.data);\n\t\t}\n\t};\n\n\tnavigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);\n});\n```\n\n这样信息发送出去后会返回一个 `promise`，然后就可以优雅地链式调用了。\n\n```JavaScript\n// ServiceWorkerService.js\nif (isSupportServiceWorker()) {\n\tconst sw = navigator.serviceWorker;\n\n\tsw.register(SERVICE_WORKER_FILE_PATH)\n\t\t.then(() => console.log('Load service worker Success.'))\n\t\t.catch(() => console.error('Load service worker fail'))\n\t\t.then(() => sendMessageToSW('Hello, service worker.'))\n\t\t.then(console.log)\n\t\t.catch(() => console.error('Send message error.'));\n} else {\n\tconsole.info('Browser not support Service Worker.');\n}\n```\n\n了解了如何在浏览器中调试  service workers 和与主页面通信这些基础之后，就可以搞一些正真功能性的东西，比如创造 service workers 最初的动机——提供更好的离线体验。\n\n<a name=\"offline-cache\"></a>\n## 为应用添加离线缓存\n为应用添加缓存的方式有很多，但能够提供**离线**缓存的，据我所知，那就只有 service workers 一家了。这就好比已经安装了的应用，无论是否有网络连接都可以随时打开使用（google 所推的 PWA 最终目的就是这个）。你可能会怀疑，听起来这么高大上实现起来会不会很复杂？然而并没有，使用 service workers 为应用添加离线缓存还是相当简单的。\n\n就如同文章开头 MDN 中所提到的，service workers 可以充当应用与服务器之前的代理服务器，它通过监听 `fetch` 事件来捕捉自己作用域下发出的网络请求，并通过 `event.respondWith` 来返回请求结果，过程中可以对返回结果做任何的修改（所以必须 https 啊）。\n\n```JavaScript\n// service-worker.js\nconst handleFetchRequest = function(request) {\n\treturn fetch(request);\n};\n\nconst onFetch = function(event) {\n\tevent.respondWith(handleFetchRequest(event.request));\n};\n\n_self.addEventListener('fetch', onFetch);\n```\n\n上面这段代码就是捕获请求最基本的方式，然后直接将请求发送出去，并将请求的结果返回，没有做其他额外的操作。如果，你这时观察控制台的网络请求，会发现所有请求的 `size` 都不再是原先的文件大小或来自缓存，而是 `from ServiceWorker`。\n\n接下去，就来给应用添加离线缓存。既然，所有的请求都是手动发出的，而且能够拿到返回的结果，那么，缓存这些结果就变得轻而易举了。\n\n不过，这里要先讲另一个知识点——[`Cache Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Cache)。它作为 service worker 的一部分写在[草案中](https://www.w3.org/TR/service-workers/#cache-objects)。通过它，我们可以方便地把请求，以及请求结果一同缓存起来。了解了 `Cache Storage`，那就把上面的代码改一下，让它能够缓存请求。\n\n```JavaScript\n// service-worker.js\nconst handleFetchRequest = function(request) {\n\treturn caches.match(request)\n\t\t.then(function(response) {\n\t\t\treturn response || fetch(request)\n\t\t\t\t\t.then(function(response) {\n\t\t\t\t\t\tconst clonedResponse = response.clone();\n\n\t\t\t\t\t\tcaches.open(CACHE_NAME)\n\t\t\t\t\t\t\t.then(function(cache) {\n\t\t\t\t\t\t\t\tcache.put(request, clonedResponse);\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn response;\n\t\t\t\t\t});\n\t\t});\n};\n```\n\n这里主要修改了如何处理请求的方法，先判断这个请求是否已经被缓存过了，缓存过了就直接返回结果，没有的话就去请求，并把结果添加到缓存中，以便下次请求来时可以直接返回。\n\n离线缓存就这样添加好了，来看看效果怎么样。这就要用到之前调试时所提到的模拟不同环境，不记得的童鞋可以往上翻一翻。（提示关键词：控制台, `Application`, `Service Workers`, `Offline`）这里模拟离线环境，设置好后再刷新页面。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/offline-page-view.png)\n\nAwesome~\u0005\u0017😁\n\n虽然已实现了离线缓存，但是，使用 `Cache Storage` 还需要注意以下几点：\n\n1. 它只能缓存 `GET` 请求；\n2. 每个站点只能缓存属于自己域下的请求，同时也能缓存跨域的请求，比如 CDN，不过无法对跨域请求的请求头和内容进行修改\n3. 缓存的更新需要自行实现；\n4. 缓存不会过期，除非将缓存删除，而浏览器对每个网站 `Cache Storage` 的大小有硬性的限制，所以需要清理不必要的缓存。\n\n上面的代码并没有做缓存的清除和更新，所以，还要更新一下。同时，通过给跨域请求添加 `{mode: 'cors'}` 属性来使请求支持跨域，从而拿到响应头信息。\n\n```JavaScript\nconst HOST_NAME = location.host;\nconst VERSION_NAME = 'CACHE-v1';\nconst CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;\nconst CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];\n\nconst isNeedCache = function(url) {\n\treturn CACHE_HOST.some(function(host) {\n\t\treturn url.search(host) !== -1;\n\t});\n};\n\nconst isCORSRequest = function(url, host) {\n\treturn url.search(host) === -1;\n};\n\nconst isValidResponse = function(response) {\n\treturn response && response.status >= 200 && response.status < 400;\n};\n\nconst handleFetchRequest = function(req) {\n\tif (isNeedCache(req.url)) {\n\t\tconst request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;\n\t\treturn caches.match(request)\n\t\t\t.then(function(response) {\n\t\t\t\t// Cache hit - return response directly\n\t\t\t\tif (response) {\n\t\t\t\t\t// Update Cache for next time enter\n\t\t\t\t\tfetch(request)\n\t\t\t\t\t\t.then(function(response) {\n\n\t\t\t\t\t\t\t// Check a valid response\n\t\t\t\t\t\t\tif(isValidResponse(response)) {\n\t\t\t\t\t\t\t\tcaches\n\t\t\t\t\t\t\t\t\t.open(CACHE_NAME)\n\t\t\t\t\t\t\t\t\t.then(function (cache) {\n\t\t\t\t\t\t\t\t\t\tcache.put(request, response);\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsentMessage('Update cache ' + request.url + ' fail: ' + response.message);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(function(err) {\n\t\t\t\t\t\t\tsentMessage('Update cache ' + request.url + ' fail: ' + err.message);\n\t\t\t\t\t\t});\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Return fetch response\n\t\t\t\treturn fetch(request)\n\t\t\t\t\t.then(function(response) {\n\t\t\t\t\t\t// Check if we received an unvalid response\n\t\t\t\t\t\tif(!isValidResponse(response)) {\n\t\t\t\t\t\t\treturn response;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst clonedResponse = response.clone();\n\n\t\t\t\t\t\tcaches\n\t\t\t\t\t\t\t.open(CACHE_NAME)\n\t\t\t\t\t\t\t.then(function(cache) {\n\t\t\t\t\t\t\t\tcache.put(request, clonedResponse);\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn response;\n\t\t\t\t\t});\n\t\t\t});\n\t} else {\n\t\treturn fetch(req);\n\t}\n};\n```\n\n升级之后，还是有缓存先拿缓存，这样比较快，但依旧会在后台发出请求，如果返回合法的请求，就更新 cache 中的值，那么，下次访问时就是这次访问返回的结果了。\n\nservice worker 的 `install` 和 `activite` 事件对象都包含一个 `waitUntil` 方法，方法接受一个 promise，当 promise 被 `resolve` 后才会继续执行到下一个状态。如果，想要强制更新缓存，就可以通过这个方法在 service worker 激活时除旧版本缓存。\n\n```JavaScript\n// service-worker.js\nconst onActive = function(event) {\n\tevent.waitUntil(\n\t\tcaches\n\t\t\t.keys()\n\t\t\t.then(function(cacheNames) {\n\t\t\t\treturn Promise.all(\n\t\t\t\t\tcacheNames.map(function(cacheName) {\n\t\t\t\t\t\t// Remove expired cache response\n\t\t\t\t\t\tif (CACHE_NAME.indexOf(cacheName) === -1) {\n\t\t\t\t\t\t\treturn caches.delete(cacheName);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t})\n\t);\n};\n\n_self.addEventListener('activate', onActive);\n```\n这样请求的缓存就能随时更新了，不过，你可能会和我有同样的疑问——那 service workers 怎么更新呢？\n\n<a name=\"lifecycle-and-update\"></a>\n## Service workers 的生命周期与更新\n事实上，service workers 的更新并不需要我们操心，只要 service workers 文件有任何一点的修改，浏览器就会立即装载它。然而，它还是有需要注意的地方，不然也就不值一提了。\n\n虽然，浏览器立即装载它，但它并没有立即生效，这和它的生命周期有关。下面这张图来自 [Service Workers 101](https://github.com/delapuente/service-workers-101)，非常形象地展示了 service workers 的生命周期。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/service-workers/sw-lifecycle.png)\n\n先看图的右边，它展示了 service workers 的 3 种状态：`Installing`, `Waiting` 和 `Active`；左边是 service workers 的生命周期，两者结合在一起，直观地展现了在 service workers 不同的生命周期时，service workers 所处的状态。可以看到，`install` 与 `activate` 2 个时间中间，service workers 是处于 `Waiting` 的状态。\n\n回到刚才提到的 service workers 更新，浏览器虽然会立即装载最新的 service workers，但只是让它 `install`，并进入 `Waiting` 的状态，而并没有立即 `activate`。只有当用户将浏览器关闭后，重新打开页面时，旧的 service workers 才会被新的 service workers 替换。不过，图中也有提到，可以在 `install` 事件中 `self.skipWaiting` 方法来跳过等待，直接进入 `activate` 状态。同样的，可以在 `activate` 事件中调用 `self.clients.claim` 方法来更新所有客户端上的 service works。\n\n为 service workers 添加上述两个方法就能较好地处理更新问题。代码改动很小，这里就不再重复贴了，所有的代码都已上传 [Github](https://github.com/DiscipleD/blog)。\n\n下次准备捣鼓 service workers 相关的服务器推送，敬请关注...😏"
  },
  {
    "path": "src/server/data/posts/simple-chess-ai-step-by-step.md",
    "content": "> 原文链接：[A step-by-step guide to building a simple chess AI](https://medium.freecodecamp.com/simple-chess-ai-step-by-step-1d55a9266977)\n\n我们先来了解一下，在我们创建一个简单的国际象棋 AI 过程中所会接触到的一些基本概念：\n\n* 棋子的移动\n* 绘制棋盘\n* Minimax（极小化极大算法）\n* Alpha-beta 剪枝\n\n我们将一步一步将这些加入最终的算法中，并分别展示它们对算法所产生的影响。\n\n你可以在 Github 上查看[最终版本](https://github.com/lhartikk/simple-chess-ai)。\n\n> 译者试了下最终版本，一不小心就被吊打了...😂\n\n## 第一步：棋子的移动和绘制棋盘\n这里我们使用 [chess.js](https://github.com/jhlywa/chess.js) 和 [chessboard.js](https://github.com/oakmac/chessboardjs/) 分别来控制棋子的移动和绘制棋盘。chess.js 库实现了所有棋子的移动规则，基于此我们可以根据棋局状态得到棋子所有可能的移动。\n\n![根据输入的棋盘状态生成所有可能的棋子移动](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/possible-moves-according-input.png)\n\n有了以上两个类库，我们就能将精力放在最有趣的事上——创建一个能够找到最佳移动的 AI。\n\n接下来就开始创建这样一个 AI，我们先创建一个方法，它会在所有合法的移动中随机选取一个。\n\n```JavaScript\nvar calculateBestMove =function(game) {\n    //generate all the moves for a given position\n    var newGameMoves = game.ugly_moves();\n    return newGameMoves[Math.floor(Math.random() * newGameMoves.length)];\n};\n```\n\n尽管，这个 AI 像一个刚懂规则的新手，但是，我们已经可以和它下棋了，这是一个好的开始。\n\n![随机移动，[点击试玩](https://jsfiddle.net/lhartikk/m14epfwb/4)](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/play-with-random-moves.gif)\n\n## 第二步：棋盘状态评估\n现在，我们试着计算在棋局某一状态下哪边更具优势，最简单的方法就是根据下表来统计棋局剩余棋子权重。\n\n![棋子对应权重表](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/chess-position-table.png)\n\n根据这个方法，我们就能让我们的 AI 选择在棋局某一状态下使棋局权重最高的移动了。\n\n```JavaScript\nvar calculateBestMove = function (game) {\n\n    var newGameMoves = game.ugly_moves();\n    var bestMove = null;\n    //use any negative large number\n    var bestValue = -9999;\n\n    for (var i = 0; i < newGameMoves.length; i++) {\n        var newGameMove = newGameMoves[i];\n        game.ugly_move(newGameMove);\n\n        //take the negative as AI plays as black\n        var boardValue = -evaluateBoard(game.board())\n        game.undo();\n        if (boardValue > bestValue) {\n            bestValue = boardValue;\n            bestMove = newGameMove\n        }\n    }\n\n    return bestMove;\n\n};\n```\n\n加入了计算权重后，我们的 AI 就会尽可能地去吃对方的棋子。\n\n![尽可能地吃子，[点击试玩](https://jsfiddle.net/lhartikk/m5q6fgtb/1/)](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/play-with-simple-evaluation.gif)\n\n## 第三步：使用极小化极大算法来探索树\n下一步，我们使用 [极小化极大算法(Minimax)](https://en.wikipedia.org/wiki/Minimax) 来使我们的 AI 能从探索树中选出最优移动。\n\n首先，我们先根据给定深度递归构建棋子所有可能移动的树，并用上一节的方法来计算所有子节点的权重\n\n然后，依据不同的行棋颜色，父节点取子节点的最大或最小值，若白子则取子节点的最大值返回给父节点，反之返回最小值。\n\n![深度为 2 的情况下，极小化极大算法图解](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/minimax-algorithm.jpeg)\n\n```JavaScript\nvar minimax = function (depth, game, isMaximisingPlayer) {\n    if (depth === 0) {\n        return -evaluateBoard(game.board());\n    }\n    var newGameMoves = game.ugly_moves();\n    if (isMaximisingPlayer) {\n        var bestMove = -9999;\n        for (var i = 0; i < newGameMoves.length; i++) {\n            game.ugly_move(newGameMoves[i]);\n            bestMove = Math.max(bestMove, minimax(depth - 1, game, !isMaximisingPlayer));\n            game.undo();\n        }\n        return bestMove;\n    } else {\n        var bestMove = 9999;\n        for (var i = 0; i < newGameMoves.length; i++) {\n            game.ugly_move(newGameMoves[i]);\n            bestMove = Math.min(bestMove, minimax(depth - 1, game, !isMaximisingPlayer));\n            game.undo();\n        }\n        return bestMove;\n    }\n};\n```\n\n加入了极小化极大算法之后，我们的 AI 已经不再是任人宰割了。\n\n![加入了极小化极大算法，[点击试玩](https://jsfiddle.net/lhartikk/m5q6fgtb/1/)](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/play-with-minimax.gif)\n\n极小化极大算法很大程度上取决于我们能够探索深度，下一步我们就来优化它。\n\n## 第四步：Alpha-beta 剪枝\n[Alpha-beta 剪枝](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning) 是对极小化极大算法的一种优化，用于减少搜索树中需要探索的节点数。这样在同样的资源条件下，就增加了探索树的搜索深度。\n\n当探索路径的结果比之前探索的更糟时，Alpha-beta 剪枝就不再搜索该子树。它并不影响极小化极大算法的计算结果，而是加快极小化极大算法运算速度。无论何种情况，Alpha-beta 剪枝总是能优化计算效率，即使，我们最初探索的就是最优解。\n\n![alpha-beta 剪枝用于极小化极大算法](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/alpha-beta-pruning.jpeg)\n\n如下图所示，通过 alpha-beta 剪枝，我们能显著减少极小化极大算法的计算次数。\n\n![深度为 4 时，使用或不使用 alpha-beta 剪枝时的计算次数](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/using-alpha-beta-or-not.png)\n\n```JavaScript\nvar minimax = function (depth, game, alpha, beta, isMaximisingPlayer) {\n    positionCount++;\n    if (depth === 0) {\n        return -evaluateBoard(game.board());\n    }\n\n    var newGameMoves = game.ugly_moves();\n\n    if (isMaximisingPlayer) {\n        var bestMove = -9999;\n        for (var i = 0; i < newGameMoves.length; i++) {\n            game.ugly_move(newGameMoves[i]);\n            bestMove = Math.max(bestMove, minimax(depth - 1, game, alpha, beta, !isMaximisingPlayer));\n            game.undo();\n            alpha = Math.max(alpha, bestMove);\n            if (beta <= alpha) {\n                return bestMove;\n            }\n        }\n        return bestMove;\n    } else {\n        var bestMove = 9999;\n        for (var i = 0; i < newGameMoves.length; i++) {\n            game.ugly_move(newGameMoves[i]);\n            bestMove = Math.min(bestMove, minimax(depth - 1, game, alpha, beta, !isMaximisingPlayer));\n            game.undo();\n            beta = Math.min(beta, bestMove);\n            if (beta <= alpha) {\n                return bestMove;\n            }\n        }\n        return bestMove;\n    }\n};\n```\n\n## 第五步：升级计算权重方法\n最初计算权重的方法相当简单就是通过计算棋盘上棋子所对应的权重，单凭这一点无法判断棋的局势。为了改善这一点，我们需要将棋子在棋盘中的位置因素计算在内。比如，骑士在棋盘中间就比在棋盘边缘位置更优，这样它可以有更多的选择。\n\n这里我们在 chess-programming-wiki 所提供的表格的基础上稍作修改已适应我们的程序。\n\n![棋子位置所对应的权重表](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/location-of-the-piece.png)\n\n这样我们的 AI 就已经像模像样了，至少从业余玩家的角度来说。\n\n![优化后，[点击试玩](https://jsfiddle.net/lhartikk/m5q6fgtb/1/)](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/simple-chess-ai-step-by-step/play-with-evaluation-improved.gif)\n\n## 结语\n总的来说，我们所创造的这个简单的 AI 不会犯一些愚蠢的错误，但依旧缺乏大局观。\n\n通过以上我介绍的方法，已经能够使我们的 AI 进行基本的对战。最终 AI 部分的代码（不包括移动棋子）不足 200 行，这意味着它实现来非常简单。你可以在 Github 上查看[最终版本](https://github.com/lhartikk/simple-chess-ai)。\n\n我们还可以继续优化我们的 AI，比如：\n\n* [落子排序，在 alpha-beta 剪枝的过程中，将可能的最优解先进行探测，从而减少计算量](https://chessprogramming.wikispaces.com/Move+Ordering)\n* [加快遍历所有落子可能的计算](https://chessprogramming.wikispaces.com/Move+Generation)\n* [胜负判定](https://chessprogramming.wikispaces.com/Endgame)\n\n如果你对此感兴趣，你可以到 [chess programming wiki](https://chessprogramming.wikispaces.com/) 中发现更多内容。\n\n感谢阅读。\n"
  },
  {
    "path": "src/server/data/posts/ssr.md",
    "content": "> 系列文章:\n> \n> 1. [Vue 2.0 升（cai）级（keng）之旅](http://discipled.me/posts/troubleshooting-of-upgrading-vue)\n> 2. [Vuex — The core of Vue application](http://discipled.me/posts/vuex-core-of-vue-application)\n> 3. From SPA to SSR (本文)\n\n个人博客之前已经将 vue-router 的模式改为了 `history`，即 url 中不包含 `hash`，再通过将所有的静态请求转发到 index.html，使它看上去似乎像一个静态多页的网站。\n\n然而，它其实和其他的 SPA (Single Page Application 单页应用)来说没有任何的区别，最终是通过前端的路由去控制页面的显示。单页应用虽然在交互体验上比传统多页更友好，但它也有一个天生的缺陷，就是对搜索引擎不友好，不利于爬虫爬取数据。\n\n正所谓成也萧何，败也萧何。\n\n讲人话就是，搜索引擎搜不到我的博客啊~哭...\n\n那什么对搜索引擎和爬虫友好的哪？答案就是静态页，而非浏览器渲染，这就需要服务器直接渲染，也就 SSR(Server Side Render)。\n\n![当然不是这个 SSR](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ssr/ssr.jpg) \n\nSSR，服务器渲染。简单来说就是，服务器将每个要展示的页面都运行完成后，将整个相应流传送给浏览器，所有的运算在服务器端都已经完成，浏览器只需要解析 HTML 就行。\n\n说起来简单，那到底该如何着手将项目改造成 SSR，和曾经的多页又有什么区别哪？既然自己在 SSR 方面是个小白，自然要先从查资料看文档入手，Vue 2.0 的文档中有一章就是关于 [SSR](https://vuejs.org/v2/guide/ssr.html)。\n\n看了文档之后，它给了我一个新思路，可以在无须大幅修改原先代码的情况下做到 SSR，又不失单页良好的体验。\n\n听上去很酷是不是，具体怎么做继续看下去。\n\n## SSR Architecture\n\n一个普通的单页应用通常是通过 webpack 将源代码打包后插入到 html 中，当页面请求时，返回 html 再加载打包后的 js 文件，也就是下图中的 Application Code，Webpack build 和 browser 这三大块。\n\n![SSR Architecture](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ssr/ssr-architecture.png)\n\n剩下的那几部分就是 SSR 需要额外新加的部分，一个个来看。\n\n### Server entry & Client entry\n\nServer entry & client entry 两者的有共同的词尾 entry，对应的是 webpack.config 中的 entry，即打包入口文件，也就是分别代表服务器端所运行代码的入口和浏览器端所运行代码的入口文件。\n\n入口文件自然不用多复杂。\n\n* server entry: 根据路由状态，返回渲染完成后相应的组件\n* clinet entry: 将应用直接挂载到 DOM 上\n\nOK。它俩的事就做完啦，是不是很简单。\n\n### Webpack build\n\n有了不同的 entry，打包的内容也有不同，自然就要两套配置。\n\n配置 webpack 的配置文件的确很麻烦，但有个好消息就是原先的打包文件不需要修改，只需加一个 server 端的配置文件就可以了。server 端的配置文件也相当简单，基本可以沿用客户端的配置，改改 `entry` 和 `output` 基本就差不多了。\n\n不过，有一点要注意，一定要将 `target` 属性设置成 `node`，不然打包完了也没法在 node 环境下跑。还可以将所有依赖都设置成 `externals`（跑在服务器本地嘛，依赖自然都拿得到），这只是个优化点，不加也没有任何问题。\n\n有了配置文件，也就能生成 Server Bundle 了，只剩下最后一块 Bundle Renderer 了。\n\n### Bundle Renderer\n\n到这里才要用上 vue 为支持 ssr 所依赖的库 `vue-server-renderer`。\n\n通过 `vue-server-renderer` 提供的 [API](https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md) 就能容易地根据 url 生成对应的组件树，然后将它返回给客户端。\n\n这里要注意，因为用的是 webpack 打包后的文件，所以只能用 `createBundleRenderer` 而不能用 `createRenderer` 来创建 renderer。\n\n创建 renderer 的时候还可以为它配置 cache，方法在 [README](https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md) 中也写得很清楚了，由于我个人博客的场景不适合添加 cache 就没有添加。\n\n这样从 SPA 到 SSR 的变更就完成了，通过浏览器访问看看是不是已经将页面整个返回了。\n\n### Tips\n\n* 遇到控制台 ⚠️\n\n> The client-side rendered virtual DOM tree is not matching server-rendered content. \n\n当然，可能是你的标签不对应，也有可能是 text node 中的空格字符长度不对应，我个人遇到的都是空格不对应造成的问题，很是尴尬（可能是使用 template 语法造成的）...\n\n* Memory-fs\n\n在开发环境下，由于使用服务器渲染，自然不能使用 webpack-dev-server，而是要用 webpack-dev-middleware。然而，webpack-dev-middleware 所创建的文件都是在内存里的，server 就无法读到 server bundle 文件，这里就要用到 [memory-fs](https://github.com/webpack/memory-fs) 来从内存中读文件。\n\n* KOA 2\n\n用 koa 2 作为服务器时，在 `renderToString` 或 `renderToStream` 时，记得外面要加 `await`，否则，程序就不等组件渲染好，就直接跑下个 middleware 去了。\n\n(奉劝大家不要用 koa 作 SSR 服务器，koa 和 webpack-dev-middleware 天生水土不服，不要问我为什么~😭)\n\n* document\n\n在 Server 端渲染时，node 环境下是没有 document 对象的。当一个界面的显示依赖于 document 对象（比如，页面滚动监听事件），那么，在 node 端运行时就会报错。\n\n这时，有两个解决的办法。\n\n1. 根据运行时的环境变量，通过添加逻辑来判断是否依赖 document\n2. 使用 jsdom mock document 对象（个人偷懒的做法）\n\n当然，从设计的角度移除对 document 的依赖就最好啦。\n\n* $root._isMounted：组件中可以用这个参数来判断应用是否为第一次挂载\n\n### 完成\n这样当浏览器请求时，返回的页面是服务器渲染之后的，浏览器解析后，页面仍就是一个单页应用。\n\n最后，看效果的戳[这里](http://discipled.me/)，看代码的戳[这里](https://github.com/DiscipleD/blog)，原先 SPA 的代码依旧保留在了 [SPA 分支](https://github.com/DiscipleD/blog/tree/SPA)。\n\n对 Vue SSR 有兴趣的童鞋，一定要看看 [vue hackernews 2.0](https://github.com/vuejs/vue-hackernews-2.0)，大神的水准比我可是高多了。\n\n最后的最后，吐槽下 Daocloud，最近老挂我服务器，枉我一直为它说好话。\n\n自己写完，看看感觉好简单，为什么还搞了那么久...\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/ssr/transfixed.jpg)\n\n常言道：饭不能一日不吃，博客不能一月不发...差点就破例了（🏃\n"
  },
  {
    "path": "src/server/data/posts/structure-data.md",
    "content": "继[上一篇](http://discipled.me/posts/ssr)使用 SSR 来优化搜索引擎之后，为了进一步提高自己的网（zhi）站（ming）排（du）名，就打算进一步优化 SEO。之前有听[朋友](https://github.com/arzyu)提到[结构化数据](https://developers.google.com/search/docs/guides/intro-structured-data)对 SEO 有帮助，便去了解了一下，果然是个好东西。\n\n## 什么是结构化数据\n简单来说，结构化数据就是按一定的结构产生的一系列描述你网站内容的信息，它能帮助搜索引擎的爬虫更好地了解你网页中所要展现的内容，并在搜索结果中有更丰富得展现，而非千篇一律的链接。\n\n不仅如此，结构化数据的设置方式也相当简单，主要分为 3 种：[JSON-LD](http://json-ld.org/),  [Microdata](https://www.w3.org/TR/microdata/) 和 [RDFa](https://rdfa.info/)。\n\n它们又分为 2 派，Microdata 和 RDFa 是通过给 html 标签加属性的方式来设置结构化数据，而 JSON-LD 是通过给页面添加 JavaScript 标签的方式自成一派。\n\n## Microdata, RDFa 孰优孰劣\n既然，Microdata 和 RDFa 都是通过同样的方式生成结构化数据，那么，自然就会比较它俩的优劣。\n\n尽管，它俩总体来说区别不大，并且 [Google Search](https://developers.google.com/search/docs/guides/intro-structured-data) 和 [Schema.org](http://schema.org/) 默认的 DEMO 都是 Microdata 格式，但是，RDFa Lite 还是更胜一筹，起决定性的因素是 [W3C 标准](https://www.w3.org/standards/)。[RDFa Lite](https://www.w3.org/TR/rdfa-lite/#the-attributes) 已被收录进了 W3C 的标准，而 [Microdata](https://en.wikipedia.org/wiki/Microdata_(HTML)) 却没有。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/structure-data/no-compare-no-hurts.jpg)\n\n更详细的对比可以参考这个[问题](http://stackoverflow.com/questions/8957902/microdata-vs-rdfa)的高票回答。\n\n也正如上面回答中提到的，Microdata, RDFa 的孰优孰劣，并不是选边站队，也不像 Angular, React, Vue 之间的有我没他，你可以在你的网站上同时使用这两种不同的方法去实现结构化数据，甚至还可以用上 JSON-LD。\n\n这里主要谈谈 RDFa Lite。\n\nRDFa Lite， 它的结构也很简单，只需在特定的 HTML 元素上添加一些特定的属性就可以了。就像这样\n\n```HTML\n<p vocab=\"http://schema.org/\" typeof=\"Person\">\n   My name is\n   <span property=\"name\">Manu Sporny</span>\n   and you can give me a ring via\n   <span property=\"telephone\">1-800-555-0199</span>\n   or visit \n   <a property=\"url\" href=\"http://manu.sporny.org/\">my homepage</a>.\n</p>\n```\n\n上面这段 html 就告诉了搜索引擎这段描述的是一个人的信息。其中，`vocab`, `typeof` 和 `property` 就是特定的属性，而 RDFa Lite 的基础属性已就这 3 个。\n\n`vocab` 是 vocabulary 的简写，顾名思义是词汇表，用来表示机器能够识别的结构化数据的类型库，比如上例中的 Person。\n\n还有哪些可以使用的类型哪？\n\n## Schema.org\nSchema.org 是由 Google, Microsoft, Yahoo 和 Yandex 共同赞助，为了创建、维护和促进结构化数据在互联网等场景下的应用，而成立的一个社区组织。它可以被之前提到的 3 种设置结构化数据的方式所使用。\n\nSchema.org 包含 583 种类型（`typeof`），以及 846 个属性（`property`），[点击查看全部类型](http://schema.org/docs/full.html)。\n\n知道了语法和词库，根据自己的网站内容就可以设置属于自己网站的结构化数据了。\n\n## 测试\n假设，已经在网站上完成了结构化数据的添加，怎么知道添加的正确与否？\n\n谷哥提供了一个简单的[工具](https://search.google.com/structured-data/testing-tool/u/0/)，只需填上网站的地址或 html 代码就可以知道添加结构化数据是否成功，是否有错误。\n\n![Structure data Test](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/structure-data/structure-data-test.jpg)\n\n点击右边的任何一项，测试工具都会自动定位到设置该属性的位置并高亮显示。\n\n至此，可以算完成了。\n\n> 福利：WordPress 用户有现成的结构化数据[插件](https://srd.wordpress.org/plugins/schema-app-structured-data-for-schemaorg/)可以使用，真是开森。\n\n## 最后\n结构化数据不仅仅可以提高搜索排名，美化搜索结果。它还能够被其他一些应用所读取使用，比如：[Gmail](https://developers.google.com/gmail/markup/overview), Facebook, Twitter 等，甚至还可以是 Siri，可穿戴设备，或是车载导航系统。\n\n看到这里，是不是冒出了很多想法，不要犹豫，开始尝试吧~\n\n### 题外话\n最近 DAOCloud 上服务起一天就被停了，停还不给提醒，好恶心，一毁原先的好印象，到年底原 DAOCloud 上的应用就不再维护了。\n\n个人博客已经从 DAOCloud 搬到自己新买的域名 [discipled.me](http://discipled.me)。\n\n> 如何省钱地买域名可以参考知乎上的这个[回答](https://www.zhihu.com/question/19551906/answer/31986656)。  \n> 当然，不差钱的可以直接点[这里](https://www.domcomp.com/?refcode=5838446c1700002750e1f877)，童叟无欺。\n\n由于，刚换域名，没 google 索引，也不知道这次加的结构化数据有没有效果 (￣▽￣) ~\n\n今天冬至，吃肉去...ㄟ(▔,▔)ㄏ\n"
  },
  {
    "path": "src/server/data/posts/translate-react-high-performance-tools.md",
    "content": "> 原文链接：[High Performance React: 3 New Tools to Speed Up Your Apps](https://medium.freecodecamp.org/make-react-fast-again-tools-and-techniques-for-speeding-up-your-react-app-7ad39d3c1b82)\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/translate-react-high-performance-tools/banner.png)\n\nReact 应用通常非常快，但一些小疏忽同样会造成性能问题。缓慢地组件装载，深层的组件树，以及不必要的渲染很快会使应用感觉上变慢。\n\n幸运的是，有一些工具能够诊断性能问题，其中一些甚至是 React 自带的。在本文中，我将介绍一些加速 React 的工具和技术。每一部分都有一个有趣的例子。\n\n## 工具 1：The Performance Timeline\nReact 15.4.0 推出了一个新特性：性能时间轴。它使你能够确切地知道组件挂载、更新和卸载的时间，以及用可视化的方式让你了解相关组件的生命周期。\n\n注意：由于，它使用了尚未在所有浏览器中实现的 [User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API)，所以到现在为止，这个特性只能在 Chrome, Edge 和 IE 下工作。\n\n### 如何使用\n1. 打开应用，并在地址栏中添加 `react_perf` 查询参数。如：`http://localhost:3000?react_perf`\n2. 打开 Chrome 的开发者工具的 Performance（性能）选项卡，点击 Record（录制）\n3. 运行你想要分析的动作\n4. 停止录制\n5. 查看运行结果\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/translate-react-high-performance-tools/performance-timeline.png)\n\n### 理解结果\n每个色条显示一个组件正在执行某项工作。由于 JavaScript 是单线程的，所以每当一个组件挂载或渲染，它会阻塞主线程并阻止其他的代码运行。\n\n中括号中的文字，像 [update]，描述了组件正处于生命周期的哪一部分。时间轴会根据不同的生命周期分解成各段，使你了解每个周期所消耗的时间，如 [componentDidMount], [componentWillReceiveProps], [ctor]（构造器）和 [render]。\n\n堆叠的条表示组件树。虽然在 React 中通常有着较深的组件树，但当你试图优化一个经常挂载的组件时，它可以帮助减少包装组件的数量，从而减少性能和内存上的消耗。\n\n有一点需要注意，时间轴工具是用于开发环境的 React 工具。事实上，性能时间轴它本身就会拖慢你的应用。但不必担心这个，因为它不会对生产环境造成影响，并且组件与组件之间相对的时间消耗是准确的。\n\n### 例 1\n为了演示，我修改了 TodoMVC 应用使它有一些严重的性能问题。你可以在[这里试试看](https://perf-demo.firebaseapp.com/?react_perf)。（需科学上网）\n\n先打开 Chrome，切换到 `Performance` 选项卡，点击开始录制。接着，添加一个待办事项，然后停止录制，查看结果。试试看看是否能够找到引起性能问题的组件。\n\n## 工具 2：why-did-you-update\nReact 中最常见的性能问题之一就是不必要的渲染。默认情况下，在 React 中当父组件渲染时，即使传入的属性没有变更，子组件也会重新渲染。\n\n比如，假设我有一个简单的组件像这样：\n\n```JavaScript\nclass DumbComponent extends Component {\n  render() {\n    return <div> {this.props.value} </div>;\n  }\n}\n```\n\n它的父组件像这样：\n\n```JavaScript\nclass Parent extends Component {\n  render() {\n    return <div>\n      <DumbComponent value={3} />\n    </div>;\n  }\n}\n```\n\n无论父组件合适渲染，子组件都会重新渲染，即使属性没有发生改变。\n\n通常，因为 `render` 方法应当是纯净的、没有副作用的，所以，虚拟 DOM 不会发生改变，这只是浪费了运行 `render` 方法的性能。在大型 React 应用里检查这种情况非常困难，幸运的是，有一个工具可以处理这个问题。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/translate-react-high-performance-tools/why-did-you-update.png)\n\n### 使用 why-did-you-update\n`why-did-you-update` 是一个 React 工具库，用于检测组件传入属性没有发生变化时，渲染方法是否被调用，从而避免潜在不必要的渲染。\n\n### 安装\n1. npm 安装：`npm i --save-dev why-did-you-update`\n2. 在你的应用中添加\n\n```JavaScript\nimport React from 'react'\nif (process.env.NODE_ENV !== 'production') {\n  const {whyDidYouUpdate} = require('why-did-you-update')\n  whyDidYouUpdate(React)\n}\n```\n\n注意这个工具会拖慢应用，所以确保它只用于生产环境。\n\n### 理解结果\n`why-did-you-update` 监控你的应用并记录可能会引起不必要修改的组件。它能让你知道属性在渲染前后所代表的值，从而确定渲染是否是不必要的。\n\n### 例 2\n为了演示 `why-did-you-update`，我在 Code Sandbox 上创建了 TodoMVC 应用并安装了这个库。打开浏览器控制台，然后添加一些待办事项，最后，查看控制台的输出。\n\n[例子点这里。](https://codesandbox.io/s/xGJP4QExn)\n\n注意应用中的一些组件可能存在不必要的渲染，试着使用下面提到的一些方法来避免不必要的渲染。如果方法正确，那么控制台将不再有警告输出。\n\n## 工具 2：React Developer Tools\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/translate-react-high-performance-tools/react-developer-tools.png)\n\nReact 开发者工具 Chrome 扩展程序具有可视化组件更新的功能，这有助于检测不必要的渲染。\n\n使用前，先请确保安装了此扩展。然后，打开 Chrome 控制台，切换到 `React` 选项卡并勾选 `Highlight Updates`。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/translate-react-high-performance-tools/highlight-updates.png)\n\n接着，使用你的应用程序，并看 React 开发者工具显神威吧~\n\n### 理解结果\nReact 开发者工具在一定时间内会高亮进行重新渲染的组件。根据不同的更新频率，使用不同的颜色，蓝色为不频繁，接着依次是绿色、黄色和红色。\n\n当调整滚动条或其他触发频繁的 UI 控件时，看到黄色或红色不一定是坏事。但是，当你只是点击一个按钮就看到红色时，那就意味着哪里出错了。这个工具的目的是发现不必更新的组件。作为应用的开发者，你应当知道在一定的时间内哪些组件应该更新。\n\n### 例 3\n为了演示该工具，我又修改了 TodoMVC 应用，使它不必要地更新组件。\n\n[想看例子点这里。](https://highlight-demo.firebaseapp.com/) （需科学上网）\n\n点击上面的链接，然后打开 React 开发者工具并启用高亮。当你在输入框内输入时，你会看到所有的待办事项都不必要的被高亮了。当你快速输入时，你会看到颜色变得越来越红。\n\n## 去除不必要的渲染\n一旦你找到了不必要渲染的组件，这里有几个方法可以修复它。\n\n### 使用纯组件\n在上面的例子中，`DumbComponent` 是一个纯函数。这个组件只需当属性发生变化时才需要渲染。React 内置了一个特殊的组件类型 `PureComponent` 就是用于这样的场景。\n\n像这样继承 `React.PureComponent`，而不是 `React.Component`\n\n```JavaScript\nclass DumbComponent extends PureComponent {\n  render() {\n    return <div> {this.props.value} </div>;\n  }\n}\n```\n这样组件只会在属性发生变化时才会重新渲染。\n\n需要注意的是，`PureComponent` 使用浅比较来比较传入的属性，所以，当你传入复杂的数据结构时，你的组件可能遗漏更新。\n\n### 实现 shouldComponentUpdate\n`shouldComponentUpdate` 是组件生命周期的一部分，当 `state` 或 `props` 改变时，会在 `render` 之前运行。如果 `shouldComponentUpdate` 返回 true，`render` 会执行，反之，则不运行。\n\n通过实现此方法，你可以让 React 避免重复渲染组件。\n\n例如，我们可以在上面的组件中实现 `shouldComponentUpdate`\n\n```JavaScript\nclass DumbComponent extends Component {\n  shouldComponentUpdate(nextProps) {\n    if (this.props.value !== nextProps.value) {\n      return true;\n    } else {\n      return false;\n    }\n  }\nrender() {\n    return <div>foo</div>;\n  }\n}\n```\n\n> 译注：最后作者安利了一个自己的[产品](https://logrocket.com/)，类似于线上的打点分析工具，有兴趣的可以看看，这里就不打广告了。\n"
  },
  {
    "path": "src/server/data/posts/trouble-with-babelrc.md",
    "content": "> TL;DR 一个工具包通过 npm 发布时，建议使用 `.npmignore` 忽略项目中的 `.babelrc` 相关设置文件。\n\n为什么要这样设置，且听我娓娓道来。故事的起因是这样的...\n\n公司的一个项目在打包发布时，遇到了 babel plugin 依赖未找到的错误。通过错误信息很快定位到了引起编译错误的依赖包，很不幸这个包是我发布的...\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/trouble-with-babelrc/carry-pot.jpg)\n\n同事找到我时，我当然很(shuai)硬(guo)气，依赖我肯定写全了，别的项目也用得好好的，是不是你姿势不对啊~\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/trouble-with-babelrc/deny-treble.jpg)\n\n既然错误已经产生了，当然先要解决问题。\n\n首先，肯定是想复现场景。本地下载项目，安装依赖，运行得很顺畅。稳~\n\n然而，在 CI 环境下打包还是出错，相比本地环境，CI 上是生产环境，而本地不是，那是不是环境的问题造成的哪？babel 的插件一般设置在 `package.json` 的 `devDependencies` 中，生产环境的确不会安装 `devDependencies`，似乎找到了原因。\n\n但又仔细一想，事实并不是这样。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/trouble-with-babelrc/matter-complicated.jpg)\n\n作为一个工具依赖包，项目引用的是打包后的文件，不需要再次安装依赖、打包。所以，问题不是在生产环境没有安装 `devDependencies` 上。\n\n那问题究竟再哪里？再回头看报错信息，是未找到 babel 的插件，`devDependencies` 中的依赖有那么多，为什么只有这个依赖出了问题，是不是和这个插件有关？研究了半天，得出个结论插件稳得不行，问题究竟出在哪里？感觉走进了一个死胡同，这时就得回到出发点换条路走一走。\n\n之前一直在思考包安装上的问题，但就同之前提到的，在生产环境是直接依赖打包后的文件，那么问题来了，babel 为什么还要安装这个插件？真相渐渐浮出了水面。\n\nbabel 插件的设置是在 `.babelrc` 中，项目在生产环境打包时一定是引用到了公共包的 `.babelrc`。根据 babel 的文档，`.babalrc` 的[应用规则](https://babeljs.io/docs/en/babelrc)是从当前编译的文件往上查找最近的 `.babalrc` 文件。\n\n至此，一切都清楚了。项目在生产环境打包时，会 babel 编译工具包的代码，而此时编译代码时参照的 `.babelrc` 文件是工具包自带的。同时，项目中的 `.babelrc` 设置同工具包中的 `.babelrc` 插件设置不一致引起的。\n\n从上面可以看到，其中有两方面的原因引起了这个问题。\n\n1. 重复打包：项目中对已打过包的文件又进行了一次 babel 的编译\n2. `.babelrc` 文件不一致\n\n找到了问题产生的原因，解决就很容易了，对应的解决方案也有两个。\n\n1. 避免重复打包：在项目的 babel-loader 中 `exclude` 工具包\n2. 通过 `.npmignore` 不上传工具包中的 `.babelrc` 文件，项目打包时就会直接使用项目的 `.babelrc` 文件\n\n第一套方案虽然看起来是**最正确的选择**，但是它增加了工具接入方的接入成本，侵入了项目接入方的打包代码。\n\n第二套方案看起来是一种 hack，但或许是**最好的选择**。因为，它对工具包的接入方来说是无感的。同时，工具包发布的是打包之后的文件，所以即使不上传 `.babelrc` 文件对工具包来说也是完整的。\n\n额外提一下，babel 7 对 `.babelrc` 的查询策略会有变更，会根据文件层级 merge `.babelrc` 设置，但对 `exclude` 之后的文件应该也没有影响。\n\n参考资料：\n\n1. [Babel doc](https://babeljs.io/docs/en/babelrc)\n2. [Babel next doc](https://babeljs.io/docs/en/next/babelrc)"
  },
  {
    "path": "src/server/data/posts/troubleshooting-of-upgrading-vue.md",
    "content": "> 系列文章:\n> 1. Vue 2.0 升（cai）级（keng）之旅 (本文)\n> 2. [Vuex — The core of Vue application](http://discipled.me/posts/vuex-core-of-vue-application)\n> 3. [From SPA to SSR](http://discipled.me/posts/ssr)\n\n> 本文不包含 Vue 2.0 所有新特性，如 SSR 等，本文并没有涉及，本文只包含[个人博客项目](https://github.com/DiscipleD/blog)升级中所遇到的经验分享，如有兴趣，可以查看 Vue 2.0 [changes log](https://github.com/vuejs/vue/issues/2873)。\n\n### 前言\n> 这节净是些唠叨，只想看升(tian)级(keng)的可直接跳过。\n\n从去年年底开始写博客，那时对怎么搞个博客网站一窍不通，看别人用 [Github Pages](https://pages.github.com/) 写博客挺赞的，就也想搞个玩玩。技术选型时，在 [jekyll](https://jekyllrb.com/) 和 [hexo](https://hexo.io/zh-cn/) 中选择了前者，或许你会问为什么？估计当时大脑的供氧量不足了吧...\n\n于是，我的博客就这么诞生了。（jekyll 版的博客已经废弃了，如果你有兴趣，可以查看之前的[提交](https://github.com/DiscipleD/DiscipleD.github.io/commits/master)）\n\n可是，用久了就发现并不怎么好用，虽然支持 markdown，可代码块要转换成 highlighter 标签；其次，[主题](https://github.com/aron-bordin/neo-hpstr-jekyll-theme)模板是挺好看，可换成中文字杂就那么别扭哪；还有，对 jekyll 的模板又不熟，自定义也不方便。\n\n年初有一天，突然想到自己也是搞技术的，为啥不自己搭一个博客网站哪？对，顺带还能学学新技术，何乐而不为。又到了技术选型的时候了，这次摆在我面前又有 2 个选择，[React](https://facebook.github.io/react/) 和 [Vue](https://vuejs.org/)，这次我选择了后者。\n\nWhy？因为，后者更轻量级，也更贴近我熟悉的 [Angular](https://angularjs.org/) 的语法，还有，那时网上就有说今年 4 月 Vue 会升级到 2.0 和 Vue 兼具 React 和 Angular 的优点等等。（好吧，老实说，不选 React 只是因为不喜欢 JSX 而已。-_-||）\n\nSo，我就用 Vue 1.10+ 搭建了自己的新博客——[Disciple.Ding Blog](http://discipled.me/)(点这里看[源码](https://github.com/DiscipleD/blog))，并渐渐地往里添加一些新学到的东西，[ES6](https://babeljs.io/docs/learn-es2015/), [webpack](http://webpack.github.io/docs/), [docker](https://www.docker.com/) 等，并在 [DAOcloud](https://www.daocloud.io/) 上发布了。(免费用了人家那么久的服务，在这里做个硬广也是应该的，DAOcloud 的确很好用，特别和 Github 绑定之后能自动构建，应用更新也及其简单，只是有个缺点就是有带宽限制。)\n\n在不久之前，Vue 如约发布了 2.0 版本。正如计划之初，博客 Vue 的版本也将升级到 2.0。\n\n说了那么多，再不进入正题就要变成标题党了。好，那就开始我们的升(cai)级(keng)之旅。\n\n### 升(tian)级(keng)之旅\n首先，升级依赖。\n\n```Bash\nnpm install vue@next vue-router@next --save\n```\n\n#### import vue\n顺利安装完成并按 [changelog](https://github.com/vuejs/vue/issues/2873) 做了修改之后，启动项目也正常，当我兴致勃勃地打开 Browser，驾轻就熟地输入 localhost，并自然而然地按下 Enter，一切水到渠成。\n\n然而，迎接我的竟是一片白板，控制台里赫然映着一串红字。\n\n> [Vue warn]: You are using the runtime-only build of Vue where the template option is not available. Either pre-compile the templates into render functions, or use the compiler-included build. (found in root instance)\n\nWhat? template 选项不能用了，changelog 没提到啊？但 [vue-router](https://github.com/vuejs/vue-router/tree/43183911dedfbb30ebacccf2d76ced74d998448a/examples) 的例子中都在用啊，什么鬼？甚至我将代码全部替换成例子中的代码依旧无法运行，但在 vue-router 项目里就能跑，什么鬼啊！\n\n但是，我并不妥协，分别打断点运行，发现两者竟然跑的不是同一段代码，纳尼！\n\n```JavaScript\nimport vue from 'vue'\n```\n同样的 `import` 语句，却有不一样的结果，vue-router 中引的是 vue.js，而在我的项目中引的竟然是 vue.common.js...common...mon...n...\n\n![懵逼](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/troubleshooting-of-upgrading-vue/mengbi.jpg)\n\n为什么会引 vue.common.js，`from 'vue'` 不该引的是 vue.js 么？这就要引入另一个知识点：package.json。\n\npackage.json 中的 `main` 属性决定了，当项目被引入时，输出的是哪个文件，而 vue 的 package.json 中的 `main` 指向的是 `dist/vue.common.js`。\n\n> 福利时间：推荐一个网站 [json.is](http://json.is/)，它对 package.json 里的每条属性都有详细的解释。\n\n找到了问题产生的原因，那么解决也就轻而易举了。\n\n```JavaScript\nimport vue from 'vue/dist/vue.js'\n```\n\n每次引用 vue 的时候都要写那么长，一点都不优雅，而且为什么 vue-router 的例子可以用啊？\n\n我要一探究竟。确认了 vue-router 中依赖的 vue 的 package.json 文件中的 `main` 字段指向的也是 `dist/vue.common.js`。那就只有一个可能了，webpack 对引入做了处理，查看 webpack.config.js\n\n```JavaScript\nmodule.exports = {\n\t// 省略...\n\tresolve: {\n\t\talias: {\n\t\t\t'vue': 'vue/dist/vue.js'\n\t\t}\n\t},\n\t...\n```\n\n果然啊~他用 webpack 的别名功能把 `vue/dist/vue.js` 命名成了 vue，防不胜防。\n\n在自己项目的 wepack.config.js 里同样给 vue 起别名，这样就又能愉快地使用 `import vue from 'vue'` 了。\n\n你是不是以为这样就结束了？不，对待一个问题要刨根问底，不能不求甚解。\n\n**为什么 vue 默认导出的是 vue.common.js，它和 vue.js 的区别在哪里，又有什么关系？**\n\n这个问题在囧克斯的[博客](http://jiongks.name/blog/code-review-for-vue-next/)中有提到。\n\n> Vue 最早会打包生成三个文件，一个是 runtime only 的文件 vue.common.js，一个是 compiler only 的文件 compiler.js，一个是 runtime + compiler 的文件 vue.js。\n\n也就是说，`vue.js = vue.common.js + compiler.js`，而如果要使用 `template` 这个属性的话就一定要用 compiler.js，那么，引入 vue.js 是最恰当的。\n\n#### 路由升级\nvue-router 的升级并不困难，参照 [Releases Note](https://github.com/vuejs/vue-router/releases/tag/v2.0.0-beta.1) 上的注释修改应该没有什么大问题，主要的变化有两点：\n\n1. 路由配置从一系列的方法调用，变成了传递一个配置对象\n2. 原先的 `v-link` 指令，变成了 `router-link` Component，路径指向用 `to` 属性\n\n正当你以为会一路顺风顺水，轻松升级路由完成的时候，现实总会给你当头一棒。\n\n之前博客的 vue-router 中使用了 `beforeEach` 和 `afterEach` 方法，根据 [Release Note](https://github.com/vuejs/vue-router/releases/tag/v2.0.0-beta.1) \n\n> * router.beforeEach (replaced by the beforeEach option)\n> * router.afterEach (replaced by the afterEach option)\n\n行，那我把它改到配置里\n\n```JavaScript\nconst ROUTER_SETTING = {\n\troutes: [\n\t\t// 省略...\n\t],\n\tbeforeEach: () => { /* some function */ },\n\tafterEach: () => { /* some function */ }\n}\n```\nBut, not work. What's wrong?\n\n难道我哪里写错了？又经过我一番谷哥和查阅文档之后，发现在下一个版本的 [Release Note](https://github.com/vuejs/vue-router/releases/tag/v2.0.0-beta.2) 中有这么一段\n\n> * beforeEach and afterEach are reverted as router instance methods (options removed). This makes it more convenient for plugins/modules to add hooks after the router instance has been created.\n\n好吧，它又被恢复回路由实例的方法了。那么，改回去\n\n```JavaScript\nconst router = new VueRouter(ROUTER_SETTING);\n\nrouter\n\t.beforeEach(() => { /* some function */ })\n\t.afterEach(() => { /* some function */ });\n```\nOK，这样总好了吧。然而，并没有...console 中报出无法从 `undefined` 中读取 `afterEach`，好吧，我猜这应该是 `beforeEach` 中没有像之前一样返回路由对象，所以不能链式调用。\n\n```JavaScript\nclass VueRouter {\n\t// 省略...\n\tbeforeEach (fn: Function) {\n\t\tthis.beforeHooks.push(fn)\n\t}\n\t\n\tafterEach (fn: Function) {\n\t\tthis.afterHooks.push(fn)\n\t}\n\t// 省略...\n}\n```\n\n看一眼源码，果然如此。\n\n那再将之前的代码稍作修改就可以了。\n\n```JavaScript\nconst router = new VueRouter(ROUTER_SETTING);\n\nrouter.beforeEach(() => { /* some function */ });\nrouter.afterEach(() => { /* some function */ });\n```\n不过，不能链式调用似乎没之前的优雅了哪~\n\n最后，提一下 vue-router 2.0 里所有的 hook（就像之前的 `beforeEach`, `afterEach`，以及每个路由状态中的 `beforeEnter`, `beforeRouteLeave`等）都具有相同的参数签名，这在 [Release Note](https://github.com/vuejs/vue-router/releases/tag/v2.0.0-beta.1) 中也有提到。\n\n```JavaScript\nfn (toRoute, redirect, next) {\n\t// toRoute: {Object} 当前路由对象\n\t// redirect: {Function} 调用跳转至另一路由\n\t// next: {Function} 调用继续当前路由跳转\n\t// 什么都不做，则取消当前跳转\n}\n```\n\n路由升级完成后，如果控制台没有什么报错，那么，路由可以相互切换了，那些不依赖数据读取的组件已经可以正常显示了。\n\n那些依赖数据读取的组件哪？\n\n这就要提到组件的**生命周期钩子（即 lifecycle hooks）**。\n\n#### Lifecycle hooks\n**生命周期钩子**应该算 vue 这次升级中 broken changes 最多的一部分了，对照 1.0 的[文档](https://vuejs.org/api/#Options-Lifecycle-Hooks)和 [release note](https://github.com/vuejs/vue/issues/2873)，作了下面这张表\n\nvue 1.0+ | vue 2.0 | Description\n:---: | :---: | ---\ninit | beforeCreate | 组件实例刚被创建，组件属性计算之前，如 data 属性等\ncreated | created | 组件实例创建完成，属性已绑定，但 DOM 还未生成，`$el` 属性还不存在\nbeforeCompile | beforeMount | 模板编译/挂载之前 \ncompiled | mounted | 模板编译/挂载之后\nready | mounted | 模板编译/挂载之后（不保证组件已在 document 中）\n- | beforeUpdate | 组件更新之前\n- | updated | 组件更新之后\n- | activated | for `keep-alive`，组件被激活时调用\n- | deactivated | for `keep-alive`，组件被移除时调用\nattached | - | 不用了还说啥哪...\ndetached | - | 那就不说了吧...\nbeforeDestory | beforeDestory | 组件销毁前调用\ndestoryed | destoryed | 组件销毁后调用\n\n知道了 hooks 升级前后的对应关系，那么升级起来就轻而易举了，改改组件的属性名就可以了。\n\n那么，改完属性名是不是就完成了？然而并没有。\n\n因为，在 vue 1.0+ 中，如果一个组件和路由相关，那么，它就可能不单单有自己组件的 lifecycle hooks，它还会有基于 vue-router 的 lifecycle hooks。\n\n而在 vue 2.0 中，**router lifecycle hooks 全部被移除了**，因为，这些 hooks 可以通过其他的方式来代替，这样不但简化了配置，还不用在组件中去处理路由相关的业务，降低了耦合。那这些 hooks 该如何替换，我们接下来就来看一下。\n\n* `activate` & `deactivate`：使用组件自身的 lifecycle hook 替代\n* `data`：通过组件 `watch` 属性来监听当前路由 `$route` 的变化\n* `canActivate`：由路由属性 `beforeEnter` 来代替\n* `canDeactivate`：由路由属性 `beforeRouteLeave` 来代替\n* `canReuse`：去除\n\n那个这个是不是也直接改改属性名就好了哪？\n\n恩，差不多。不过需要注意的是，如果原先 hooks 中使用了有关路由信息的 `transition` 参数是肯定不能用了。比如，根据路由参数来进行查询，原先通过 `transition.to.params` 获取路由参数，现在就要通过刚刚提到的**当前路由对象** `this.$route.params` 来获取。\n\n在升级这里的过程中，还遇到一个问题：当用户输入的 URL 满足路由匹配，但根据路由参数无法获得正确的文章时，我想让路由直接跳转到首页。\n\n在 1.0 版本中，我通过 `transition.redirect('/');` 就轻松的回到了首页，由于 2.0 中没有 `transition` 参数，而 `$route` 只包含当前路由的信息，并不包换路由切换的操作。那该怎么做哪？再一次谷哥和查阅文档，然而一无所获。\n\n![i choose death](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/troubleshooting-of-upgrading-vue/i-choose-death.jpg)\n\n最后在 vue-router 的例子中找到了解决问题的钥匙——`$router`。\n\n`$router` 返回的是整个项目路由的实例，它是只读的。于是，刚刚那个问题就可以通过 `this.$router.replace('/');` 来解决。\n\n这里还有一点，在 1.0 版本中组件配置 route 属性时还可以设置一个叫 `waitForData` 的属性。这个在 2.0 中，我还没有找到直接的替换方式，不过，我在整个组件上添加 `v-if` 来处理。从理论和效果的角度上讲，`v-if` 是可以替代原先的 `waitForData` 属性，就似乎不那么优雅。\n\n剩余其他小点，看控制台报错信息，然后查查 [Release Note](https://github.com/vuejs/vue-router/releases/tag/v2.0.0-beta.1) 都能轻松处理啦~\n\n> 至此，我的整个 [Blog](http://discipled.me/) 也升级完成了，欢迎来访。（查看源码戳[这里](https://github.com/DiscipleD/blog)）\n\n### 写在最后\n如果现在再让我选一个技术来搭博客的话，我会选 React。为啥？\n\n因为 vue 我已经玩过啦，哈哈哈~\n\n最后，借用外国网友的一句话：\n\n> I'm constantly rewriting / refactoring this silly little blog using the latest and buzziest tech, so that I can stay up to date on these libraries and frameworks. \n\n这也是我自己搭博客，而不是直接使用博客系统的主要原因。\n\n最后的最后，安利下自己的 [Blog](http://discipled.me/)，以及 [Source Code](https://github.com/DiscipleD/blog)。\n\n欢迎交流，喷子绕道。"
  },
  {
    "path": "src/server/data/posts/upgrade-ssr-of-vue.md",
    "content": "不久前，vue 升级至了 2.3.0 版本，是一个 minor 的版本。[该版本](https://github.com/vuejs/vue/releases/tag/v2.3.0)除了一些组件功能的优化之外，主要是升级 vue 的 ssr 功能，甚至于为之建立了一个独立的 [Git Book](https://ssr.vuejs.org/en/)。\n\n我的博客之前用的就是 ssr，这次升级自然也是要尝试一把。ssr 的优势和实现在这里就不再赘述了，不太了解的可以看[之前的文章](https://discipled.me/posts/ssr)，这里主要还是来看看升级的变化之处。\n\n升级的第一件事自然就是先升级依赖，将 vue, vue-server-renderer 等依赖的版本升级至最新 `npm up -S`（作者 vue 的版本为 v2.3.3）。升级之后，直接启动服务看看，应该是没有问题的，文档也提到可以使用之前的配置，但建议改为新版本的方式。\n\n虽然，依赖升级之后同样能运行，但还是来看看有哪些提升或变化的地方？\n\n* [Renderer Create Options](#renderer-create-options)\n* [Lifesycle & data prefetch](#lifesycle-data-prefetch)\n* [代码结构与同构](#code-structure)\n* [Webpack build plugin](#webpack-build-plugin)\n* [结尾](#concultion)\n\n<a name=\"renderer-create-options\"></a>\n## Renderer Create Options\n更新之后，在创建 renderer 时可以为它添加配置，其中的 `template` 属性可以为我们省去之前的许多繁杂的小工作，比如：\n\n* 在 html 中使用 `<!--vue-ssr-outlet-->`，renderer 会自动将 app 生成的 html 插入此处，而不用自己再进行替换操作\n* 将 `context.state` 插入到 html 中，并自动使用 [serialize-javascript](https://github.com/yahoo/serialize-javascript) 进行转义来防止 XSS 攻击\n* 直接通过 `cache` 属性配置组件缓存\n\n以上这些都是在之前版本中常被使用到的，剩下一些 `clientManifest`, `inject`, `runInNewContext` 等新增的东西后面会再提到。\n\n<a name=\"lifesycle-data-prefetch\"></a>\n## Lifesycle & data prefetch\n由于在 ssr 阶段不会有一系列的变更，所以更新之后 vue 在 ssr 阶段只会执行 `beforeCreate` 和 `created` 这个两个生命周期函数。\n\n相信你一定会问那如果遇到异步请求该怎么办哪？这里同之前并没有变化，仍旧是通过设置组件的自定义方法来获取数据，最终通过 vuex 将数据传递回客户端。没什么变化就不展开了，不清楚的可以看一下[文档](https://ssr.vuejs.org/en/data.html)，写得已经相当详细了。\n\n不过此处有一点优化，由于数据已经在服务器端已准备完成，客户端就无需再像服务器端发送异步请求，而是可以直接从 store 中获取数据。\n\n<a name=\"code-structure\"></a>\n## 代码结构与同构\n文档的[这一节](https://ssr.vuejs.org/en/structure.html)在内容上和之前的文档基本没有区别，不过其中提到一点指出了我原有代码的不足之处，也给了我不少启发。\n\n通常大家的 app.js 会是这样\n\n```JavaScript\n// 省略其他依赖...\nimport store from './vuex';\nimport router from './router';\n\nsync(store, router);\n\nconst app = new Vue({\n\tstore,\n\trouter,\n\trender: h => h(/* ... */)\n});\n\nexport {app, router, store};\n```\n\n这看上去并没有任何问题。在平时的浏览器环境中，每次刷新页面都会重新加载一次文件，是一个全新的环境（或沙盒）。但当同构了代码之后，服务器端同样运行这段代码时，就可能出现问题。\n\n因为 node 端服务启动后，vue 的实例就被初始化完成，所有的请求会公用这同一个实例，这就可能造成混乱。所以为每个请求返回一个新的 vue 的实例是一个比较好的处理方法，router 和 store 同样适用这个道理。\n\n```JavaScript\n// 省略其他依赖...\nimport createStore from './vuex';\nimport createRouter from './router';\n\nconst createApp = () => {\n\tconst store = createStore();\n\tconst router = createRouter();\n\n\tsync(store, router);\n\n\tconst app = new Vue({\n\t\tstore,\n\t\trouter,\n\t\trender: h => h(/* ... */)\n\t});\n\n\treturn {app, router, store};\n};\n\nexport default createApp;\n```\n\n虽然，我至今还没有遇到过实例冲突的问题，不过我还是觉得文档说的很有道理，可能会发生这样的情况。多个实例会克服冲突的问题，但它同时也增加服务器的负担。\n\n这样处理之后，就可能将之前提到的 `runInNewContext` 配置设为 `false`，默认为 `true` 会为每个 bundle 创建新的上下文。\n\n<a name=\"webpack-build-plugin\"></a>\n## Webpack build plugin\n升级的最大变化在于对 webpack 提供更强大的支持，在 `vue-server-renderer` 包中新增了两个 webpack plugin: `server-plugin` 和 `client-plugin`，分别用于服务器端和客户端。\n\n### server-plugin\n`server-plugin` 会默认创建一个名为 `vue-ssr-server-bundle.json` 的文件作为 `createBundleRenderer` 的第一个参数。\n\n这里 webpack 的 `output.filename` 设置还是要定义的，不然打包的时候会报错。\n\n上面这点上一个版本就能做到，使用 `server-plugin` 的好处是在于，它提供了服务端的 `source-map`功能，这可是开发利器。另一大好处是，支持 `hot-reload`，不过我之前使用的是 webpack-middleware 就已经支持该特性了。\n\n熟悉 webpack 的都知道，webpack-middleware 是将文件放在内存里的，而这里的 `createBundleRenderer` 用的是文件访问，所以，直接传路径是有问题的。不过，它也支持传一个对象，所以记得每次服务端代码更新之后要重新创建 renderer，还有读文件之后要将 string 转换为 object 传给 `createBundleRenderer`。\n\n```JavaScript\n// 省略...\nconst updateRenderer = () => {\n\ttry {\n\t\tconst options = {\n\t\t\tclientManifest: JSON.parse(expressDevMiddleware.fileSystem.readFileSync(clientManifestFilePath, 'utf-8'))\n\t\t};\n\t\tcreateRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')), options);\n\t} catch(e) {\n\t\tcreateRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')));\n\t}\n\tconsole.log('Renderer is updated.');\n};\n\n// watch and update server renderer\nconst serverCompiler = webpack(serverConfig);\nserverCompiler.outputFileSystem = mfs;\nserverCompiler.watch({}, (err, stats) => {\n\tif (err) throw err;\n\tstats = stats.toJson();\n\tstats.errors.forEach(err => console.error(err));\n\tstats.warnings.forEach(err => console.warn(err));\n\tupdateRenderer();\n});\n```\n\n### client-plugin\n如果你使用过升级之前 vue ssr 的功能，那你肯定会对一系列有关 html 的操作有映象，比如替换 html，插入 state 等。现在，有了 `client-plugin` 它就能代替原有的 `html-webpack-plugin` 来生成 html，并把之前那些繁杂的事都替你处理了。\n\n上面这些对已经实现 ssr 的你可能不是很有吸引力，不过，下面这点可能会让你感兴趣。这个插件还自带为你的 ccs 或 js 添加 `preload` 和 `prefetch` 功能，它可以加快你网站的加载速度，如果你还不清楚 `prefetch` 和 `preload` 是什么的话，可以先读一下[这篇文章](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf)。\n\n如果你使用的是 webpack-server，那么，你按文档上的例子来应该没什么问题。但如果你和我一样使用的是 webpack-middleware，那么，这里还是有些别扭的，需要和之前一样每次 plugin 生成后去重新构建 renderer。\n\n```JavaScript\n// 省略...\nclientCompiler.plugin('done', updateRenderer);\n```\n\n同 `server-plugin` 一样文件读出来的是 string，你要将它转换为对象。其他基本的配置按文档上的来就行，遇到问题的可以参考下我的[代码](https://github.com/DiscipleD/blog)。\n\n吹了这么多，不足之处还是得指出来，`client-plugin` 还不能像 `html-webpack-plugin` 监听 html 文件，每次修改 html 都得手动重启服务有点麻烦，可以优化一波...\n\n升级所要注意的就差不多就这些了。还有一点，之前 vue 推荐使用 `renderToStream` 来返回页面，如果组件生命周期中有请求的话，使用 stream 可能导致组件还未构建完成就已经发送。所以，更新之后 vue 推荐使用 `renderToString`。\n\n<a name=\"concultion\"></a>\n## 结尾\nvue 的确是非常紧跟潮流，就像这次加入的 `preload` 和 `prefetch` 功能，但因开发团队人员太少（相对于 react 和 angular），导致版本并不是很稳定。\n\n如果，你问我 vue 好不好？我会说，好。  \n如果，你问我要不要学 vue？我会说，学。  \n如果，你问我 vue 能不能上生产？我的建议是，不如咋们半年后再谈...\n\n------------------------------------------\n外公，一路走好..."
  },
  {
    "path": "src/server/data/posts/upgrade-to-webpack2.md",
    "content": "> 本文主要讲述如何将 webpack 版本升级至 v2.2.x，如果你还不了解 webpack，那么推荐你先读一下这篇[文章](https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783)。\n\n今年年初，webpack 2.2.0 版本正式发布，还记得那时已有很多文章来介绍 webpack 2。 但经历过之前，先将默认安装升级至 2.0.x-beta 又退回 1.x 的我来说，吃一堑长一智，决定先观察看看。\n\n经过这几个月的时间，并没有什么大新闻发生，应该不会再闹乌龙了，便把公司项目和自己博客都试着升级一波看看效果。\n\n## 为什么要升级至 Webpack2\n你可能会问 webpack1 用得好好的为啥要去升级成 webpack 2 哪？有啥好处？是不是又在瞎折腾了？\n\n当然不是在瞎折腾，因为 webpack2 最大一个好处就是 Tree Shaking(摇树)，这也是年初 webpack2 火了一把的最大原因。\n\n使用 webpack2 还有没有其他好处哪？当然是有的。\n\n除了 Tree Shaking 之外，webpack2 还支持了 ES6 的模块语法，单就这一点已经不需要 babel 了，当然如果你要用其他一些新特性，还是得加入 babel-loader。\n\n与此同时，webpack2 还支持使用 `System.import` 来动态加载模块。不过，使用此功能时要注意，对不支持 `Promise` 的浏览器需要添加 polyfill。\n\n再加之，webpack 已经正式弃用 webpack1，也就不再维护了。从长期的角度考虑，升级到 webpack2 也更加稳妥。\n\n知道了为啥升级，接着就来看看如何升级。\n\n## 主要变化和注意点\n如何从 v1 升级至 v2，webpack 官网的[升级手册](https://webpack.js.org/guides/migrating/#uglifyjsplugin-sourcemap)已经介绍的非常详细了，基本先过一遍，然后遇到问题再搜索一下就能搞定。（英文不好的童鞋也不用担心，已经有人翻译了[中文版](https://segmentfault.com/a/1190000008181955)）\n\n这里就不再一一赘述升级变更点了，主要按常用功能整理、分享一下 webpack2 升级的变化。\n\n首先，来看看升级前的配置文件。（由于篇幅原因省略了一些重复性的配置，有兴趣的可以到 Github 上[查看详细内容](https://github.com/DiscipleD/blog/commits/master/config/webpack/base.js)）\n\n```JavaScript\n// base.js\nconst path = require('path');\nconst webpack = require('webpack');\nconst autoprefixer = require('autoprefixer');\nconst ExtractTextPlugin = require('extract-text-webpack-plugin');\n\nconst SOURCE_PATH = path.join(__dirname, '../../src');\nconst DIST_PATH = path.join(__dirname, '../../build/client');\n\nconst webpackConfig = {\n\t// http://mp.weixin.qq.com/s?__biz=MzI3NTE2NjYxNw==&mid=2650600472&idx=1&sn=d4bf85c1bb26a32aff144e81d652582f\n\tdevtool: 'source-map',\n\toutput: {\n\t\tpath: DIST_PATH,\n\t\tpublicPath: '/'\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t'vue': 'vue/dist/vue.js',\n\t\t\t'assets': SOURCE_PATH + '/client/assets',\n\t\t\t// 省略其他 alias...\n\t\t},\n\t\textensions: ['', '.js']\n\t},\n\teslint: {\n\t\tconfigFile: '.eslintrc',\n\t\temitWarning: true,\n\t\temitError: true,\n\t\tformatter: require('eslint-friendly-formatter')\n\t},\n\tpostcss: [autoprefixer({browsers: ['last 2 versions']})],\n\tplugins: [\n\t\tnew ExtractTextPlugin('style-[contenthash:8].css'),\n\t\tnew webpack.NoErrorsPlugin()\n\t],\n\tmodule: {\n\t\tpreLoaders: [\n\t\t\t{\n\t\t\t\ttest: /[^(\\.min)]\\.js$/,\n\t\t\t\tloader: 'eslint-loader',\n\t\t\t\texclude: /node_modules/,\n\t\t\t\tinclude: SOURCE_PATH\n\t\t\t}\n\t\t],\n\t\tloaders: [\n\t\t\t{\n\t\t\t\ttest: /[^(\\.min)]\\.js$/,\n\t\t\t\tloaders: ['babel'],\n\t\t\t\texclude: /node_modules/,\n\t\t\t\tinclude: SOURCE_PATH\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.html$/,\n\t\t\t\tloader: 'html',\n\t\t\t\tquery: {interpolate: true},\n\t\t\t\texclude: /node_modules/,\n\t\t\t\tinclude: SOURCE_PATH\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.(sc|c)ss$/,\n\t\t\t\t// extract css file from js file, that will reduce the js file size and optimize page loading.\n\t\t\t\t// but it will increase the package time, so it should be only used in build file.\n\t\t\t\tloader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!sass-loader')\n\t\t\t\t// loaders: ['style', 'css', 'postcss', 'sass']\n\t\t\t},\n\t\t\t// 省略其他 loader...\n\t\t]\n\t}\n};\n```\n\n由于之前博客已[升级成同构应用](https://discipled.me/posts/ssr)，webpack 的配置被分为了客户端和服务器端两套，上面这个文件便是两套配置中共通的部分。除了没有设置 `entry`，基本的 webpack 配置就同它差不多，升级 webpack2 也不需要修改 `entry`，也就正好不用列出了。\n\n接着就一个个来看，这些基本、常用的配置属性，有哪些不要改，有哪些要改的。\n\n一开始就是个好消息，两个属性 `devtool` 和 `output`，它俩同 `entry` 一样不用修改。\n\n### resolve\n接着是 `resolve`，`resolve` 的变化也不大，主要是其中两个字段的变化，`root` 和 `extensions`。\n\n* `root` 改为了 `modules`，用于设置 webpack 查询模块的路径，默认是 `[\"node_modules\"]`。  \n同时，搜索模块的优先级与数组的顺序有关，越靠前的越先匹配，比如 `[path.resolve(__dirname, \"src\"), \"node_modules\"]`，此时，webpack 查找模块时会优先查找本地 src 下的模块，查不到再到 `node_modules` 中查找。\n* `extensions`，用于设置 webpack 处理的扩展名，默认值为 `[\".js\", \".json\"]`。  \n这里就牵扯到 2 个变更点：\n\t1. 升级后就不用像 v1 一样添加一个空字符串 `\"\"` 了；\n\t2. v2 自带 `json-loader` 来处理 `json` 类型的文件，而不须我们自己手动引入。  \n\n这两个配置大部分情况下使用默认值（不配置）就可以了。但在 react 或 vue 的项目中，可能需要在 `extensions` 中添加 `.jsx` 或 `.vue`。 \n\n接着两个是 `eslint` 和 `postcss`，属于自定义属性，在 webpack2 中不支持自定义属性，需要挪到各自的 `loader` 中进行配置。\n\n既然，遇到了 `loader` 相关，下一步就先升级 `loader`。\n\n### module\n在 webpack 中，`module` 下不再有 `preLoaders`, `loaders` 或 `postLoaders` 统一都变成了 `rules`，如需要替换 `preLoaders` 或 `postLoaders` 则需通过设置 `rules.enforce` 属性。\n\n同时，webpack2 不再支持 `loaders`，改为 `rules.use`，`loader` 属性可以继续使用。`loader` 相关的配置，可以通过 `rules.use.options` 设置。\n\n还有一点，在 webpack2 中，已不再默认给 loader 添加 `-loader` 后缀，不过还可以通过将 `resolveLoader` 设置为 `moduleExtensions: [\"-loader\"]` 来给 loader 添加默认后缀。不过，webpack 官方不推荐这么做，还是为每个 loader 都加上 `-loader` 比较好。\n\n这部分可以算是 webpack2 升级过程中改动量最多的地方了。但别担心，这只是个有点麻烦，细心一点就能解决的问题。\n\n还剩下最后一个部分，`plugin` 在来看一下它的变化。\n\n### plugin\n`plugin` 部分 webpack2 有着一下这些变化：\n\n* 新增 `LoaderOptionsPlugin`，用于设置全局的 `loader` 和 `plugin` 属性\n* 默认引入 `OccurrenceOrderPlugin`，也就是可以删了原先的这个配置\n* `DedupePlugin` 也被移除\n* `NoErrorsPlugin` 重命名为 `NoEmitOnErrorsPlugin`\n* `UglifyJsPlugin` 不再默认压缩 js，需在 `LoaderOptionsPlugin` 配置 `minimize: true`\n\n这样升级就基本完成了，再来看一下修改后的配置文件。\n\n```\n// base.js\nconst path = require('path');\nconst webpack = require('webpack');\nconst autoprefixer = require('autoprefixer');\nconst ExtractTextPlugin = require('extract-text-webpack-plugin');\n\nconst SOURCE_PATH = path.join(__dirname, '../../src');\nconst PUBLIC_PATH = '/';\nconst DIST_PATH = path.join(__dirname, '../../build/client');\n\nconst webpackConfig = {\n\t// http://mp.weixin.qq.com/s?__biz=MzI3NTE2NjYxNw==&mid=2650600472&idx=1&sn=d4bf85c1bb26a32aff144e81d652582f\n\tdevtool: 'source-map',\n\toutput: {\n\t\tpath: DIST_PATH,\n\t\tpublicPath: PUBLIC_PATH\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t'vue': 'vue/dist/vue.js',\n\t\t\t'assets': SOURCE_PATH + '/client/assets',\n\t\t\t// 省略其他 alias...\n\t\t}\n\t},\n\tplugins: [\n\t\tnew ExtractTextPlugin('style-[contenthash:8].css'),\n\t\tnew webpack.NoEmitOnErrorsPlugin()\n\t],\n\tmodule: {\n\t\trules: [\n\t\t\t{\n\t\t\t\ttest: /\\.js$/,\n\t\t\t\tloader: 'eslint',\n\t\t\t\tenforce: 'pre',\n\t\t\t\texclude: /node_modules/,\n\t\t\t\toptions: {\n\t\t\t\t\temitWarning: true,\n\t\t\t\t\temitError: true,\n\t\t\t\t\tformatter: require('eslint-friendly-formatter')\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.js$/,\n\t\t\t\tloader: 'babel',\n\t\t\t\texclude: /node_modules/\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.html$/,\n\t\t\t\tloader: 'html?interpolate',\n\t\t\t\texclude: /node_modules/\n\t\t\t},\n\t\t\t{\n\t\t\t\ttest: /\\.(sc|c)ss$/,\n\t\t\t\t// extract css file from js file, that will reduce the js file size and optimize page loading.\n\t\t\t\t// but it will increase the package time, so it should be only used in build file.\n\t\t\t\tuse: ExtractTextPlugin.extract({\n\t\t\t\t\tfallback: 'style-loader',\n\t\t\t\t\tuse: [\n\t\t\t\t\t\t'css-loader?sourceMap',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tloader: 'postcss-loader?sourceMap',\n\t\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\t\tplugins: () => [autoprefixer({browsers: ['last 2 versions']})]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'sass-loader'\n\t\t\t\t\t]\n\t\t\t\t})\n\t\t\t},\n\t\t\t// 省略其他 loader...\n\t\t]\n\t}\n};\n```\n\n不要以为这就完了，勿忘初心。这只是成功升级了 webpack，还没用上 Tree Shaking 哪。\n\n### Tree Shaking & Module\n想要使用 webpack2 的 tree shaking 就需要让 webpack 来管理模块之间的加载，而不是让 `babel-loader` 去处理。\n\n不过，这修改起来也很简单，只需修改 babel 的配置文件 `.babelrc`，将原先的 `es2015` 改为 `[\"es2015\", { \"modules\": false }]` 就可以了。\n\n但是（再次转折...），改起来虽然简单，但修改这个全局的配置会影响到许多方面：\n\n* 假如你和我一样将服务器端和客户端放在一个项目中，那么这个修改会影响到服务器端代码的解析；\n* 假如你的项目中加入了单元测试，比如 Jest，那么，修改这个配置同样会影响到测试代码的运行\n\n这也可以解决的，还是修改 `.babelrc` 配置，根据环境来运用不同的插件，比如运行测试时就可以添加以下配置\n\n```JSON\n// .babelrc\n  \"env\": {\n    \"test\": {\n      \"plugins\": [\"transform-es2015-modules-commonjs\"]\n    }\n  }\n```\n\n在公司项目升级 webpack 修改模块引入方式时，还遇到过 `Module build failed: some file... TypeError: Cannot read property '0' of null` 这样一个问题，折腾了半天。最后发现是因为 `babel-plugin-antd` 报出的问题，`babel-plugin-antd` 前一阵就升级成 `babel-plugin-import` 了，项目里也升级一下问题就解决了.\n\n> 升级 webpack 后，记得同时升级所用到的 loader 和 plugin。\n\n这样升级基本完成了，对比一下升级前后的打包结果。\n\n![build with webpack1](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/upgrade-to-webpack2/build-with-webpack1.png)\n\n![build with webpack2](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/upgrade-to-webpack2/build-with-webpack2.png)\n\n可以看到，app.js 小了不到 8kb，减小了 10%，还是比较可观的，而 common.js 反而大了 12kb，应该是因为升级了依赖的关系~[捂脸]\n\n总得来说，将 webpack 从 v1 升级至 v2，主要修改 `resolve`, `module` 和 `plugin` 这 3 个属性，而且基本是一些字段名的修改，整体结构上没有大的变化，升级还是比较简单的，是个耐心活。\n\n至此，博客 2 代成员（vue2 和 koa2）又多了一位 webpack2...😂\n\n> 相关阅读：[Why Webpack 2's Tree Shaking is not as effective as you think](https://advancedweb.hu/2017/02/07/treeshaking/)"
  },
  {
    "path": "src/server/data/posts/vue-with-typescript.md",
    "content": "* [前言](#preface)\n* [安装 TypeScript](#install)\n* [tsconfig.json 配置](#tsconfig)\n* [Tslint](#tslint)\n* [Vue 中使用 typescript 需要注意的问题](#problems-with-vue)\n* [其他问题](#other-problems)\n* [最后](#conclusion)\n\n<a name=\"preface\"></a>\n## 前言\n大家一听到 ts 是强类型语言，想到 js 要像其他语言那样定义变量类型就头疼，心里多少有些抵触情绪。起初我也是这样认为的，写的时候的确也是这样。但在另一方面，它强大的静态分析功能会使你所写的代码更健壮，从而大大减少 bug 的发生概率，将 bug 掐死在摇篮里。\n\n这样的好东西就想尝试着把它用到自己的项目里。可当要将 ts 加入到现有的 vue 项目中时，突然有无从下手的感觉，总感觉 ts 的类型和 vue 绑定数据的方式无法有效地结合起来。同时，印象中一直听到的都是 react 和 angular 的项目在使用 ts，还没有听说哪个成功的 vue 项目是用 ts 开发的。（[element](https://github.com/ElemeFE/element) 也不是。）\n\n那是不是 vue 就不能同 ts 一起用哪？一度我也这样怀疑过，不过搜了波资料之后，发现 vue 官网已经给出了如何整合 ts 的[教程](https://vuejs.org/v2/guide/typescript.html)。微软这边也有个 [TypeScript-Vue-Starter](https://github.com/Microsoft/TypeScript-Vue-Starter)，但是，这个 starter 也无法解决组件属性上的类型检测。这令 ts 类型检测的能力大大降低，而 vue 则是推荐另一个官方工具 [vue-class-component](https://github.com/vuejs/vue-class-component) 来解决这个问题。\n\n扯了那么多，总结一句话就是：TS 和 Vue 能搞。\n\n那么，下面直接开搞。\n\n<a name=\"install\"></a>\n## 安装 TypeScript\n首先，自然是安装，typescript 和其他依赖没有什么不同，直接通过 npm 安装就可以了。因为项目之前用的是 webpack，所以还要装上另外两个 loader：[`awesome-typescript-loader`](https://github.com/s-panferov/awesome-typescript-loader) 和 [`source-map-loader`](https://github.com/webpack-contrib/source-map-loader)。\n\n```Bash\nnpm i typescript awesome-typescript-loader source-map-loader -S\n```\n\n有了 loader 那么让 webpack 去管理 ts 的文件也就轻而易举了。别忘了在 `resolve` -> `extensions` 中添加 `.ts`，让 webpack 能够识别以 ts 结尾的文件。\n\n```JavaScript\n// ...\n\tresolve: {\n\t\t// ...\n\t\textensions: [\".ts\", \".js\", \".json\"]\n\t},\n\tmodule: {\n\t\trules: [\n\t\t\t{\n\t\t\t\ttest: /\\.tsx?$/,\n\t\t\t\tloader: \"awesome-typescript-loader\"\n\t\t\t},\n\t\t\t// ...\n\t\t]\n\t}\n// ...\n```\n\n这样 webpack 的配置就完成了，接着在根目录下添加 `tsconfig.json` 文件来配置 ts。\n\n<a name=\"tsconfig\"></a>\n## 配置 tsconfig.json\n`tsconfig.json` 所包含的属性并不多，只有 7 个，ms 官方也给出了它的[定义文件](http://json.schemastore.org/tsconfig)。但看起来并不怎么舒服，这里就翻译整理一下。（若有误，还请指出）\n\n* `files`: 数组类型，用于表示由 ts 管理的文件的具体文件路径\n* `exclude`: 数组类型，用于表示 ts 排除的文件（2.0 以上支持 Glob）\n* `include`: 数组类型，用于表示 ts 管理的文件（2.0 以上）\n* `compileOnSave`: 布尔类型，用于 IDE 保存时是否生成编译后的文件\n* `extends`: 字符串类型，用于继承 ts 配置，2.1 版本后支持\n* `compilerOptions`: 对象类型，设置编译的选项，不设置则使用默认配置，配置项比较多，后面再列\n* `typeAcquisition`: 对象类型，设置自动引入库类型定义文件(`.d.ts`)相关，该对象下面有 3 个子属性分别是：\n\t* `enable`: 布尔类型，是否开启自动引入库类型定义文件(`.d.ts`)，默认为 `false`\n\t* `include`: 数组类型，允许自动引入的库名，如：[\"jquery\", \"lodash\"]\n\t* `exculde`: 数组类型，排除的库名\n\n如不设定 `files` 和 `include`，ts 默认是 `exclude` 以外的所有的以 `.ts` 和 `.tsx` 结尾的文件。如果，同时设置 `files` 的优先级最高，`exclude` 次之，`include` 最低。\n\n上面都是文件相关的，编译相关的都是靠 `compilerOptions` 设置的，接着就来看一看。\n\n属性名 | 值类型 | 默认值 | 描述 \n--- | --- | --- | --- \nallowJs | boolean | false | 编译时，允许有 js 文件\nallowSyntheticDefaultImports | boolean | module === \"system\" | 允许引入没有默认导出的模块\nallowUnreachableCode | boolean | false | 允许覆盖不到的代码\nallowUnusedLabels | boolean | false | 允许未使用的标签\nalwaysStrict | boolean | false | 严格模式，为每个文件添加 \"use strict\"\nbaseUrl | string | | 与 `path` 一同定义模块查找的路径，详细参考[这里](http://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url) \ncharset | string | \"utf8\" | 输入文件的编码类型 \ncheckJs | boolean | false | 验证 js 文件，与 `allowJs` 一同使用\ndeclaration | boolean | false | 生成 `.d.ts` 定义文件\ndeclarationDir | string | | 生成定义文件的存放文件夹（2.0 以上）\ndiagnostics | boolean | false | 是否显示诊断信息\ndownlevelIteration | boolean | false | 当 `target` 为 ES5 或 ES3 时，提供对 `for..of`，解构等的支持\nemitBOM | boolean | false | 在输出文件头添加 utf-8 (BOM)字节标记\nemitDecoratorMetadata | boolean | false | 详见 [issue](https://github.com/Microsoft/TypeScript/issues/2577)\nexperimentalDecorators | boolean | false | 允许注解语法\nforceConsistentCasingInFileNames | boolean | false | 不允许不同变量来代表同一文件\nimportHelpers | | boolean | false | 引入帮助（2.1 以上）\ninlineSourceMap | boolean | false | 将 source map 一同生成到输出文件中\ninlineSources | boolean | false | 将 ts 源码生成到 source map 中，需要同时设置 `inlineSourceMap` 或 `sourceMap`\nisolatedModules | boolean | false | 将每个文件作为单独的模块\njsx | string | \"preserve\" | jsx 的[编译方式](http://www.typescriptlang.org/docs/handbook/jsx.html)\njsxFactory | string | \"React.createElement\" | 定义 jsx 工厂方法，`React.createElement` 还是 `h`（2.1 以上）\nlib | string[] | | 引入库定义文件，可以是[\"es5\", \"es6\", \"es2015\", \"es7\", \"es2016\", \"es2017\", \"esnext\", \"dom\", \"dom.iterable\", \"webworker\", \"scripthost\", \"es2015.core\", \"es2015.collection\", \"es2015.generator\", \"es2015.iterable\", \"es2015.promise\", \"es2015.proxy\", \"es2015.reflect\", \"es2015.symbol\", \"es2015.symbol.wellknown\", \"es2016.array.include\", \"es2017.object\", \"es2017.sharedmemory\", \"esnext.asynciterable\"]（2.0 以上）\nlistEmittedFiles | boolean | false | 显示输入文件名\nlistFiles | boolean | false | 显示编译输出文件名\nlocale | string | 随系统 | 错误信息的语言\nmapRoot | string | | 定义 source map 的存放位置\nmaxNodeModuleJsDepth | number | 0 | 检查引入 js 模块的深度，需同 `allowJs` 一同使用\nmodule | string | | 指定模块生成方式，[\"commonjs\", \"amd\", \"umd\", \"system\", \"es6\", \"es2015\", \"esnext\", \"none\"]\nmoduleResolution | string | | 指定模块解析方式，[\"classic\" : \"node\"]\nnewLine | string | 随系统 | 行位换行符，\"crlf\" (windows) 或 \"lf\" (unix)\nnoEmit | boolean | false | 不显示输出\nnoEmitHelpers | boolean | false | 不在输出文件中生成帮助\nnoEmitOnError | boolean | false | 出错后，不输出文件\nnoFallthroughCasesInSwitch | boolean | false | `switch` 语句中，每个 `case` 都要有 `break`\nnoImplicitAny | boolean | false | 不允许隐式 `any`\nnoImplicitReturns | boolean | false | 函数所有路径都必须有显示 `return`\nnoImplicitThis | boolean | false | 不允许 `this` 为隐式 `any`\nnoImplicitUseStrict | boolean | false | 输出中不添加 \"use strict\"\nnoLib | boolean | false | 不引入默认库文件\nnoResolve | boolean | false | 不编译三斜杠或模块引入的文件\nnoUnusedLocals | boolean | false | 未使用的本地变量将报错（2.0 以上）\nnoUnusedParameters | boolean | false | 未使用的参数将报错（2.0 以上）\noutDir | string | | 定义输出文件的文件夹\noutFile | string | | 合并输出到一个文件\npaths | object | | 与 `baseUrl` 一同定义模块查找的路径，详细参考[这里](http://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url) \npreserveConstEnums | boolean | false | 不去除枚举声明\npretty | boolean | false | 美化错误信息\nreactNamespace | string | \"React\" | 废弃。改用`jsxFactory`\nremoveComments | boolean | false | 去除注释\nrootDir | string | 当前目录 | 定义输入文件根目录\nrootDirs | string [] | | 定义输入文件根目录\nskipDefaultLibCheck | boolean | false | 废弃。改用 `skipLibCheck`\nskipLibCheck | boolean | false | 对库定义文件跳过类型检查（2.0 以上）\nsourceMap | boolean | false | 生成对应的 map 文件\nsourceRoot | string | | 调试时源码位置\nstrict | boolean | false | 同时开启 `alwaysStrict`, `noImplicitAny`, `noImplicitThis` 和 `strictNullChecks` (2.3 以上)\nstrictNullChecks | boolean | false | `null` 检查（2.0 以上）\nstripInternal | boolean | false | 不输出 JSDoc 注解\nsuppressExcessPropertyErrors | boolean | false | 不提示对象外属性错误\nsuppressImplicitAnyIndexErrors | boolean | false | 不提示对象索引隐式 any 的错误\ntarget | string | \"es3\" | 输出代码 ES 版本，可以是 [\"es3\", \"es5\", \"es2015\", \"es2016\", \"es2017\", \"esnext\"]\ntraceResolution | boolean | false | 跟踪模块查找信息\ntypeRoots | string [] | | 定义文件的文件夹位置（2.0 以上）\ntypes | string [] | | 设置引入的定义文件（2.0 以上）\nwatch | boolean | false | 监听文件变更\n\n一般情况下，tsconfig.json 文件只需配置 `compilerOptions` 部分。\n\n```Json\n{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"module\": \"es2015\",\n    \"removeComments\": true,\n    \"preserveConstEnums\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"es5\",\n      \"es2015\"\n    ]\n  }\n}\n```\n\n其中，`allowSyntheticDefaultImports` 是使用 vue 必须的，而设置 `module` 则是让模块交由 webpack 处理，从而可以使用 webpack2 的摇树。另外，加上`allowJs`，这样就可以一点点将现有的 js 代码转换为 ts 代码了。\n\n如果，你在 webpack 中设置过 `resolve` -> `alias`，那么，在 ts config 中也需要通过 `baseUrl` + `path` 的方式来定义模块查找的方式。\n\n<a name=\"tslint\"></a>\n## Tslint\n同 js 一样，ts 也有自己的 lint —— `tslint`。\n\n```Bash\nnpm i tslint tslint-loader -S\n```\n\n之前项目是通过 webpack 打包的，所以一并把 `tslint-loader` 也装上，并修改 webpack loader 的配置。\n\n```\n// ...\n\t{\n\t\ttest: /\\.tsx?$/,\n\t\tenforce: 'pre',\n\t\tloader: 'tslint-loader'\n\t},\n// ...\n```\n\n同时，在项目目录下添加 `tslint.json` 文件。\n\n```JSON\n{\n  \"extends\": \"tslint:recommended\",\n  \"rules\": {\n    // ...\n  }\n}\n```\n\n有些[推荐的配置](https://github.com/palantir/tslint/blob/master/src/configs/recommended.ts)和自己的习惯不太一样，可以通过 `rules` 去自定义（[查看所有规则](https://palantir.github.io/tslint/rules/)）。\n\ntslint 默认都是警告类型，这样对做迁移也比较方便，也可以在配置中将提示类型从警告改为错误。\n\n配置差不多完了，剩下就是码代码了。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vue-with-typescript/play.gif)\n\n<a name=\"problems-with-vue\"></a>\n## Vue 中使用 typescript 需要注意的问题\n### 定义组件\n`this` 在 vue 组件中非常常见，但 vue 组件的申明方式无法让 typescript 了解组件实例所包含的属性。\n\n```JavaScript\nexport default Vue.component('blog', {\n\ttemplate,\n\tcreated() {\n\t\tthis.loadBrowserSetting();\n\t\tthis.loadNavList();\n\t\tthis.loadSocialLink();\n\t},\n\tcomputed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']),\n\tmethods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']),\n\twatch: {\n\t\t'title': function() {\n\t\t\tsetBlogTitle(this.title);\n\t\t}\n\t}\n});\n```\n所以，就需要通过继承 vue 提供的 `ComponentOptions` 接口来申明组件所用到的每个属性，比如 `methods`, `getter` 中的属性等。\n\n```TypeScript\nexport interface IBlogContainer extends Vue {\n\ttitle: string;\n\tloadBrowserSetting: () => void;\n\tloadNavList: () => void;\n\tloadSocialLink: () => void;\n}\n\nexport default Vue.component('blog', {\n\ttemplate,\n\tcreated() {\n\t\tthis.loadBrowserSetting();\n\t\tthis.loadNavList();\n\t\tthis.loadSocialLink();\n\t},\n\tcomputed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']),\n\tmethods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']),\n\twatch: {\n\t\ttitle() {\n\t\t\tsetBlogTitle(this.title);\n\t\t},\n\t},\n} as ComponentOptions<IBlogContainer>);\n```\n\n看上去还不错？但这还不是最终的方案，可以更好，那就是一开始提到的 [vue-class-component](https://github.com/vuejs/vue-class-component)。\n\n`vue-class-component` 既可以用于 ts，也能够用于 js。它都让你的组件定义文件变得相当清晰。将生命周期函数，`data`, `methods` 中的方法直接定义在 class 上，而将其他的组件 `options` 传入注解中就可以了。\n\n```TypeScript\n@Component({\n\tcomputed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']),\n\tmethods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']),\n\ttemplate,\n\twatch: {\n\t\ttitle() {\n\t\t\tsetBlogTitle((this as BlogContainer).title);\n\t\t},\n\t},\n})\nclass BlogContainer extends Vue {\n\tpublic title: string;\n\tpublic loadBrowserSetting: () => void;\n\tpublic loadNavList: () => void;\n\tpublic loadSocialLink: () => void;\n\n\tpublic created() {\n\t\tthis.loadBrowserSetting();\n\t\tthis.loadNavList();\n\t\tthis.loadSocialLink();\n\t}\n}\n\nexport default Vue.component('blog', BlogContainer);\n```\n\n需要注意的是，全局组件还是需要在最后调用 `Vue.component` 语法来声明一下。\n\n### 服务器渲染组件服务器端获取数据\nVue 服务器渲染会为某些需要动态获取数据的组件添加额外的方法，并在服务端接受到请求后调用，这个方法的名字可以是任意的（通常是 `preFetch` 或 `asyncData`）。同样的，它并没有在 vue 的定义文件中被定义，所以，需要各自去定义它。\n\n在同一个项目中，组件获取数据的方法是相同的，所以可以扩展现有的 vue 的类型定义，而不用一遍遍的重复申明。\n\n```TypeScript\n// vue.d.ts\nimport Vue from 'vue';\nimport { Store } from 'vuex';\nimport VueRouter from 'vue-router';\n\nimport { IRootState } from 'vuexModule/index';\n\ndeclare global {\n  interface Window {\n    __INITIAL_STATE__: any\n  }\n}\n\ndeclare module 'vue/types/options' {\n  interface ComponentOptions<V extends Vue> {\n    preFetch?: (store: Store<IRootState>, router?: VueRouter) => Promise<any>\n  }\n}\n```\n\n同样的方法也可以用来扩展浏览器的定义文件，比如一些尝试性的 API。\n\n```TypeScript\n// pwa.d.ts\ninterface ShareInfo {\n    title: string,\n    url?: string,\n    text?: string\n}\n\ninterface Navigator {\n    readonly share: (o: ShareInfo) => Promise<void>\n}\n```\n\n再回到刚刚的组件服务器端获取数据。\n\n众所周知，在使用 vuex 管理的系统获取数据通常使用的是调一个 action 方法，然而，action 将变动传递到 mutation。其中，action 需要接受一个对象作为参数，其中包含了 `commit` 和 `dispatch` 方法。在 Redux 中，这个参数是 store，但在 vue 中，它的类型是 `ActionContext<S, R>`。\n\n同时，可以看到刚刚的 `preFetch` 方法的签名是 `store` 和 `router`。尽管，`store` 中也包含 `commit` 和 `dispatch` 方法，但它的类型是 `Store<R>`。这可以在原先的 js 中顺利运行，但在 ts 中，类型不同是会报错的。所以，这时你需要一个中间方法将传入的 `Store<R>` 类型转换为 `ActionContext<S, R>`。\n\n这里推荐大家借鉴 [vuex-typescript](https://github.com/istrib/vuex-typescript) 中 `getStoreAccessors` 的实现方法。(自己写得不太好，不够通用，就不贴出来了)\n\n### 服务端渲染永远返回新实例\n在之前一篇关于 [vue 2.3 SSR 升级手册](https://discipled.me/posts/upgrade-ssr-of-vue)中有提到过，\n\n> 因为 node 端服务启动后，vue 的实例就被初始化完成，所有的请求会公用这同一个实例，这就可能造成混乱。所以为每个请求返回一个新的 vue 的实例是一个比较好的处理方法，router 和 store 同样适用这个道理。\n\n的确，我也这样做了。但在这次升级过程中，我还是发现了原先的一个 bug，甚至可以说是大 issue。\n\n先来看一眼，原先的代码\n\n```JavaScript\n// vuex/index.js\nimport modules from './module';\n\nVue.use(Vuex);\n\nconst createStore = () =>\n\tnew Vuex.Store({\n\t\tmodules,\n\t\tstrict: true\n\t});\n\nexport default createStore;\n```\n\n是不是觉得没问题？返回的是一个方法，方法每次调用会返回一个新的 store 对象。的确！\n\n继续看下去\n\n```JavaScript\n// vuex/module/index.js\nimport browser from './browser';\nimport home from './home';\nimport aboutMe from './about-me';\nimport post from './post';\nimport site from './site';\nimport tags from './tags';\n\nexport default {\n\tbrowser,\n\tsite,\n\taboutMe,\n\thome,\n\tpost,\n\ttags\n};\n```\n\n是不是发现什么了？没错。问题就在于，`store` 的确是新的对象了，但 `modules` 因为是对象引用的关系，所以永远是同一个。以此类推，`modules` 下面的每个模块也有着同样的问题。\n\n> 记住：在服务器渲染中，总是通过方法返回新的实例。\n\n<a name=\"other-problems\"></a>\n## 其他问题\n### IDE\n首先，最直观的体会就是 webstorm 对 typescript 的支持非常差，代码提示做的还不错，但类型检测，错误提示等等可以说是几乎没有。而同是微软出品的 vscode，自然在这些方面都有着良好的表现。\n\n> VScode，你值得拥有。\n\nPS：没用过的童鞋可以用一下试试，真的好用。（用下来除了 git 操作比 ws 用起来麻烦一点，其他都很棒，墙裂安利...）\n\n### 引入 `.ts` 以外类型的文件\n在 webpack 中可以引入各式各样的文件，只要你装了相应的 loader，比如 `json`, `scss`, `jpg` 文件等等。但这些文件在 ts 里引入时，就有问题了，ts 的模块是无法理解这些文件的，ts 的模块只负责对 `.tsx?` 或 `.jsx?` 文件类型的编译。\n\n这时可以添加一个定义文件来 hack 它。\n\n```TypeScript\n// support-loader.d.ts\ndeclare module \"*.json\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.html\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.jpg\" {\n    const value: any;\n    export default value;\n}\n// ...\n```\n\n### `process.env`\n大家肯定很熟悉 `process.env` 这个变量，这里也就不多解释了。虽然大家都熟悉它，但 ts 不了解它，不知道它是什么类型，所以会报错。\n\n遇到这个问题，可以通过安装 `@types/node` 来解决。\n\n```Bash\nnpm install @types/node\n```\nTypescript 2.0 之后，ts 通过 npm 来安装类定义文件（@types）。\n\nTs 会默认读取项目下 node_modules 下面的 @types 中的类定义文件，也可以通过之前提到的 `tsconfig.json` 中的 `typeRoots` 和 `types` 属性就行修改。\n\n`typeRoots` 用于修改查找定义文件的位置，而 `types` 则是选择引入哪些定义文件，不填则默认不设限制，即 `typeRoots` 下所有定义文件。\n\n### export default 无法同 ES6 对象字面量增强同时使用\nES6 中新增了一个特性是对象字面量的键可以为一个变量或一个表达式，像这样\n\n```JavaScript\n{\n\t[key]: 'something'\n}\n```\n\n当它同 ES 6 模块的默认导出同时使用时，`babel-loader` 工作正常，但在 `awesome-typescript-loader` 这里就出了问题。\n\n> You may need an appropriate loader to handle this file type.\n\n直接 export 动态对象字面量就会报错，但将它们拆分开来就可以了。（不是很理解其中的原因，还望大神解惑）\n\n```JavaScript\n// error...\nexport default {\n\t[SomeAction](state) { /* ... */ }\n}\n\n// compile success\nconst mutations = { [SomeAction](state) { /* ... */ } };\n\nexport default mutations;\n```\n\n> ps: `typescript` 版本为 2.4.1，`awesome-typescript-loader` 版本为 3.2.1。\n\n至此，客户端升级至 typescript 就完成了。（服务端因为类型定义的问题没有全部转换完成，还得再琢磨琢磨。）\n\n<a name=\"conclusion\"></a>\n## 最后\n总的来说，就如本文最初讲，ts 从数据类型、结构入手，通过静态类型检测来增强你代码的健壮性，从而避免 bug 的产生。\n\n与此同时，vue 也有解决方案（[vue-class-component](https://github.com/vuejs/vue-class-component)）可以与 ts 结合得非常棒。\n"
  },
  {
    "path": "src/server/data/posts/vuex-core-of-vue-application.md",
    "content": "> 系列文章:\n> \n> 1. [Vue 2.0 升（cai）级（keng）之旅](http://discipled.me/posts/troubleshooting-of-upgrading-vue)\n> 2. Vuex — The core of Vue application (本文)\n> 3. [From SPA to SSR](http://discipled.me/posts/ssr)\n\n> 当今，谈到状态管理首先想到的肯定是 Redux，而随着 Vue 2.0 的发布，Vuex 也伴随着推出了最新版，本文就带你对照 Redux 来看看刚刚出炉的 Vuex 2.0。\n> \n> 有关 Redux 的基础概念在本文中会简要略过，如再一一赘述篇幅就太长了，不了解的可以看一下本人之前写的有关 Redux 的两篇文章：\n> \n> 1. [Redux 入门](http://discipled.me/posts/getting-started-with-redux)\n> 2. [Redux 进阶](http://discipled.me/posts/redux-advanced)\n\n### 为什么说 Vuex 是 Vue 应用的核心？\n众所周知，一个应用的外观可以千变万化，但无论如何变化，它都需要一样东西去支撑，那就是——**数据**。这个数据是广义上的，可以是数据库中的数据，也可以是当前应用所处的状态，甚至可以是 [WebRTC](https://webrtc.org/), [Web Bluetooth](https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web) 等一系列实时数据。\n\n> 在 vue 应用中，vuex 就充当了**数据**提供者的角色，vue 则只需要关注页面的展示与交互。\n\n既然，明确了以 vuex 为核心，那么就来看看如何在 vue 应用中使用 vuex？\n\n随着 Vue 2.0 的发布，Vuex 在近期也随之推出 2.0 版。在[上一篇文章](http://discipled.me/posts/troubleshooting-of-upgrading-vue)中有提到作者的博客是用 vue 2.0 搭建的，但之前并没有添加 vuex，现在正可以借此机会将 vuex 添加到项目中。\n\n本文将介绍 Vuex 2.0 的同时，分享一些本人在这个过程中的一些心得。\n\n首先，当然是核心的核心 Store。\n\n### Store\nStore 用来存放整个应用的 state。\n\n那怎么建立 store 哪？由于，Vuex 2.0 刚刚推出，最新的 API 还得看 [Release Note](https://github.com/vuejs/vuex/releases)。\n\n创建一个 Store 非常简单只需 `new Vuex.Store({ ...options })`，其中，`options` 可以是一下几种：\n\n* state `Object`：存放应用状态\n* actions `Object`：注册 `action`\n* mutations `Object`：注册 `mutation`\n* getters `Object`：注册 `getter`\n* modules `Object`：注册 `module`\n* plugins `Array<Function>`：注册中间件\n* strict `Boolean`：是否开启严格模式，严格模式下所有对 state 的变化必须通过 `mutation` 来修改，反之抛出异常，默认不开启。\n\n或许你不了解这些属性的含义，没关系，之后每个还会分别解释。\n\n明白了属性的含义，那么创建一个 store 的代码就可能会是这样\n\n```JavaScript\n// store.js\nimport Vue from 'vue';\nimport Vuex from 'vuex';\nimport createLogger from 'vuex/logger';\n\nimport blog from './module/blog';\n\n// 在 Vue 中，注册 Vuex\nVue.use(Vuex);\n\nexport default new Vuex.Store({\n\tstate: {},\n\tplugins: process.env.NODE_ENV !== 'production' ? [createLogger()] : [],\n\tmodules: {\n\t\tblog\n\t}\n});\n```\nstore 创建完成之后，就可以在根组件中使用了。\n\n```JavaScript\nimport Vue from 'vue';\nimport store from '../vuex';\nimport router from './router';\nimport './blog';\n\nnew Vue({\n\tstore,\n\trouter,\n\ttemplate: '<blog></blog>'\n}).$mount('#app');\n```\n\n个人看来，一个状态管理的应用，无论是使用 vuex，还是 redux，最困难的部分是在 store 的**设计**。\n\n> 究竟该如何设计一个 store，是根据组件的结构层次设计对应的 store，还是根据应用数据来设计 store？\n\n由于，store 是存放整个应用状态的地方，所以，起初我认为应该是前者按组件的层次结构去设计。这样 store 中分别保存着每个组件的状态，这对大型项目来说或许会造成大量的冗余数据存储在 store 中，以及一些重复的工作，但这也提供了简洁鲜明的层次结构，增强了项目的可维护性，这对大型项目来说更至关重要。\n\n但伴随着写项目时的思考，我渐渐推翻了之前的想法。\n\n假设这样一个场景，项目中有两个互不相关的组件，但它们俩却依赖同一份数据源。如果，这时采用之前的设计方法，那么这同一份数据源会被存放在 store 的两个不同的位置。那么此时，如果一个组件需要对数据源进行操作的话，它不但需要修改自己组件对应的 state，同时还要发起 action 来修改另一个组件的 state，这恰恰违背了组件的单一性。\n\n然而，使用应用数据来设计 store 就不会有这样的问题。鉴于这个原因，我现在更倾向于第二个理念来设计整个应用的 store。\n\n所以，当项目开始时，要考虑到整个应用的数据模型来设计 store 真是相当麻烦啊。\n\n谈完了 store，就再一个个来看刚刚创建 store 时所提到的属性，state 就是用来保存状态的，没啥好说的，直接来看看第二个 `actions`。\n\n### Actions\n`actions` 是一个对象，key 就是 action 的名字，value 就是对应的 action。此处的 action，无论从名字，还是作用都和 redux 中的 action 相同，用于激发 state 的变更。但是，它们的用法却不相同。\n\nRedux 中的 action 需要返回一个 JS 对象，即使加了 thunk 中间件之后，能够返回一个函数，但这个函数最终返回的还是一个 JS 对象，最后通过，`store.dispatch` 该对象来触发 state 的变更。\n\n然而，Vuex 中的 action 它本身就是一个方法，并且这个方法并不需要任何的返回，而是，通过 `store.commit` 来触发 `mutation`。\n\n> Vuex 2.0 中，已将原先的 `store.dispatch` 改名为了 `store.commit` 来触发 `mutation`。  \n> Vuex 2.0 中，并没有移除 `store.dispatch`，而是改为用于触发 `action`。\n\n所有 action 方法接受当前 store 的实例作为第一个参数，调用传递的参数会作为第二个参数传入（暂不支持多参数）。\n\n### Mutations\n`mutations ` 也是一个对象，同 `actions` 类似，key 就是 mutation 的名字，value 就是对应的 mutation。\n\nmutation 用于更新应用的 state。Redux 中虽然没有 mutation 这个词，但从上面的解释就明白，这同 redux 中的 reduce 起着相同的作用。\n\n但两者在写法上又有着不同，由于 vuex 中的 `mutations` 是一个对象，并借用 ES6 对象方法可以**使用变量**和**省略**的特点，调用 `mutation` 可以直接通过命名找到相应的处理方法，这使得它比 redux 的一系列 switch/case 语句要更简单、更优雅。\n\n更大的不同之处在于 redux 的 reduce 是要求返回一个新的 state，而 vuex 就如它的命名 mutations（变异）是对当前 state 进行操作，而不能返回一个新的 state，这里就和 FP 的理念有所冲突了。\n\n```JavaScript\n// mutations.js\nexport default {\n\t// work\n\t[LOAD_SOCIAL_LINK](state = {}, mutation = {}) {\n\t\tstate.socialLinkList = mutation.payload\n\t\t\t.filter(item => !!item.link)\n\t\t\t.map(item => ({\n\t\t\t\t...item,\n\t\t\t\tsvgPath: svgPath + '#' + item.name\n\t\t\t}));\n\t}\n\t\n\t// not work\n\t[LOAD_SOCIAL_LINK](state = {}, mutation = {}) {\n\t\tstate = {\n\t\t\t...state,\n\t\t\tsocialLinkList: mutation.payload\n\t\t\t\t.filter(item => !!item.link)\n\t\t\t\t.map(item => ({\n\t\t\t\t\t...item,\n\t\t\t\t\tsvgPath: svgPath + '#' + item.name\n\t\t\t\t}))\n\t\t};\n\t}\n};\n```\n\n单就这点来看，redux 略胜一筹。\n\n### Getters\n`Getters` 也是一个对象，用于注册 getter，每个 getter 都是一个 `function` 用于返回一部分的 state。\n\ngetter 方法接受 state 作为第一个参数，一个简单的 `getters` 就可能是这样：\n\n```JavaScript\nexport default {\n\t// 省略...\n\tgetters: {\n\t\tsocialLinkList: state => state.socialLinkList\n\t}\n};\n```\n\n> 掌握了 Store, Actions, Mutations 以及 Getters 这几个概念，那你就掌握了 vuex 的核心，已经完全可以创建一个完整的 store，并可以使用了。\n\n但随着项目的增长，你会发现将 Actions, Mutations, Getters 全都写在一起非常难以维护，这时你会想念 Redux 中将 state 划分处理的 `combineReducers`。\n\n![Wake up!](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vuex-core-of-vue-application/wake_up.jpeg)\n\n醒醒！别想 Redux 啦，Vuex 也可以划分处理 state 树，它就是接着就要提到的 `modules`。\n\n### Modules\n`Modules` 的作用就如它的名字，划分模块。\n\n它的属性也是一个对象，key 是对应的 module 名，在 state 中会创建相应的 key，而 value 是一个用于配置如何创建 module 的对象，该对象的属性基本同创建 store 时的 `options` 对象一样，只少了最后 2 个还没有讲到的属性 `plugins` 和 `strict`。这两者是不是有什么关系哪？\n\n```JavaScript\nclass Store {\n  constructor (options = {}) {\n    // 省略...\n    \n    // init root module.\n    // this also recursively registers all sub-modules\n    // and collects all module getters inside this._wrappedGetters\n    installModule(this, state, [], options)\n    \n    // 省略...\n  }\n```\n从 vuex 创建的源码中可以看到，其实，store 它本身就是一个 module。\n\n既然，`modules` 中能配置 `modules` 那就意味着：模块是可以嵌套的。那么，使用 `modules` 就可以将 state 划分为各个模块，同 `combineReducers` 一样可以化繁为简，这对中大型项目来说必不可少。\n\n一个 module 的定义就可以是这样。\n\n```JavaScript\n// nav module\nimport mutations from './mutations';\nimport actions from './actions';\n\nexport default {\n\tstate: {},\n\tgetters: {\n\t\tnavList: state => state.navList\n\t},\n\tactions,\n\tmutations\n};\n```\n\n> 警报！前方第 6 行有坑，请速速绕行。\n\n第 6 行？\n\n`state: {},` 初始化 state 能有什么问题啊？\n\n当你运行你的应用的时候，你会发现，如果 navList 的变化是由一个同步的方法返回的就没有问题，但如果，它是通过异步方法返回的，你会发现虽然控制台上的 mutation log 输出正确，但你的组件中并没有得到正确的值。\n\n![What happened?](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vuex-core-of-vue-application/question.jpeg)\n\n因为，当 action 调用之后会计算一次 getter，如果是同步的，那么此时 getter 的 state 中已经保存着最新的数据。\n\n但如果是异步的，那么此时 getter 中的 state 是一个空对象，那么上例中的 `state.navList` 就会返回一个 `undefined`。然而，`undefined` 就不会进入 vue 的 watch 系统，所以当异步请求结束后，即使 state 中对应字段变为了目标值，但也不会再调用 getter 了，组件中的值自然也不会更新了。\n\n那怎么解决哪？那就是给 state 中的每个属性设初始值，这样在第一次计算 getter 的值时就会返回对应的初始值，而这个初始值是在 vue 的系统中的，所以当异步请求结束后调用 mutation 改变 state 中对应的值后，getter 会自动触发更新，此时，组件中对应的值也就被修改了。\n\n所以，一定要记得：\n\n> 为每个属性设置初始化 state ！！！\n\n> 为每个属性设置初始化 state ！！！\n\n> 为每个属性设置初始化 state ！！！\n\n重要的话，说三遍！！！\n\n> 最后，在使用 `modules` 还需要注意，在不同 `modules` 下，注册的 action 或 mutation 的名字重复并不会报错，但都会被调用，所以要**注意命名**。\n\n好，`modules` 讲完了，继续看下一个属性 `plugins`。\n\n### Plugins\nvuex 自 1.0 版开始就将原先的 `middlewares` 替换成了 `plugins`。也就是说，现在使用的 `plugins` 就是中间件。\n\n`plugins` 的参数终于同之前的有所不同了，是一个数组，数组中的每一项都是一个方法，方法接受一个参数就是当前 store 的实例。\n\n```JavaScript\n\t// vuex source code: apply plugins\n\tplugins.concat(devtoolPlugin).forEach(plugin => plugin(this))\n```\n\nvuex 中间件的编写理解起来也十分容易，就是通过 `store.subscribe` 来订阅 mutation 的变化，这比 redux 中间件的工作原理更容易理解。\n\n最后的 `strict` 属性之前已经提到了，就是用来设置时候开启严格模式的，严格模式下，state 只能通过 mutation 来修改。\n\n至此，创建 vuex store 的所有属性都讲完了，store 也就完成了，那么，vue 的组件该如何和 vuex 的 store 链接起来哪？\n\n### 连接到组件\nvuex 1.0 之前如何将 vuex 连接到组件在这里就不说了，有兴趣可以上[官网](http://vuex.vuejs.org/en/index.html)上看看。\n\n主要来看看如何使用 vue 2.0 新增的 4 个 helper 方法**优雅**地将 vuex 连接到组件。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vuex-core-of-vue-application/move_back.jpeg)\n\n这 4 个 helper 方法，分别是：\n\n* mapState\n* mapMutations\n* mapGetters\n* mapActions\n\n常言道：口说无凭。\n\n我们就来看一个博客升级中的简单例子，没有加入 vuex 前，本人博客的首页是这样设定的：\n\n```JavaScript\n// home.js\nimport Vue from 'vue';\n\nimport PostService from '../../../common/service/PostService';\n\nimport img from '../../../assets/img/home-bg.jpg';\nimport template from './home.html';\n\nconst Home = Vue.extend({\n\ttemplate,\n\tdata: () => {\n\t\treturn {\n\t\t\theader: {\n\t\t\t\timg,\n\t\t\t\ttitle: 'D.D Blog',\n\t\t\t\tsubtitle: 'Share More, Gain More.'\n\t\t\t},\n\t\t\tpostList: []\n\t\t};\n\t},\n\tcreated() {\n\t\tconst postService = new PostService();\n\t\tpostService.queryPostList().then(({postList}) => (this.postList = postList));\n\t}\n});\n```\n\n这里我们回顾一下之前的所讲，为 home 组件创建对应的 store module。\n\n```JavaScript\n// index.js\n// mutation types\nconst INIT_HOME_PAGE = 'INIT_HOME_PAGE';\nconst LOAD_POST_LIST = 'LOAD_POST_LIST';\n\n// actions\nconst initHomePage = ({dispatch, commit}) => {\n\tcommit(createAction(INIT_HOME_PAGE, {\n\t\theader: {\n\t\t\timage,\n\t\t\ttitle: 'D.D Blog',\n\t\t\tsubtitle: 'Share More, Gain More.'\n\t\t}\n\t}));\n\tdispatch('loadPostList');\n};\n\nconst loadPostList = ({commit}) => {\n\tnew PostService().queryPostList()\n\t\t.then((result = {}) => {\n\t\t\tcommit(createAction(LOAD_POST_LIST, {\n\t\t\t\tpostsList: result.postsList\n\t\t\t}));\n\t\t});\n};\n\nconst actions = {initHomePage, loadPostList};\n\n// mutations\nconst mutations = {\n\t[INIT_HOME_PAGE](state = {}, mutation = {}) {\n\t\tstate.header = mutation.payload.header;\n\t},\n\n\t[LOAD_POST_LIST](state = {}, mutation = {}) {\n\t\tstate.postsList = mutation.payload.postsList;\n\t}\n};\n\nexport default {\n\tstate: {\n\t\theader: {},\n\t\tpostsList: []\n\t},\n\tgetters: {\n\t\tpostsList: state => state.postsList\n\t},\n\tactions,\n\tmutations\n};\n```\n\n```\nconst createAction = (typeName = '', data = '') => ({ type: typeName, payload: data });\n```\n这里的 `createAction` 是自己创建的一个简单函数，用于格式化 `mutation` 获得的参数，这并不是必须的，vuex 的 `commit` 方法是接受参数为 `(type, data)` 的。\n\nOK。对应的 store module 也创建好了，就来改组件吧。\n\n首先，应用的状态都来自于 store，那么组件中的 `data` 属性自然就不用了，直接删除。爽~\n\n```JavaScript\nconst Home = Vue.extend({\n\ttemplate,\n\tcreated() {\n\t\tconst postService = new PostService();\n\t\tpostService.queryPostList().then(({postList}) => (this.postList = postList));\n\t}\n});\n```\n\n其次，原先在 created hooks 里直接去查数据，现在用了 vuex 自然要通过调用 action 来获取数据，这里就要用到 4 大金刚之一——`mapActions` 来获取 vuex 中设定好的 action。\n\n`mapActions` 接受一个数组或对象，根据相应的值将对应的 action 绑定到组件上。\n\n```JavaScript\nimport {mapActions} from 'vuex';\n\nconst Home = Vue.extend({\n\ttemplate,\n\tmethods: mapActions(['initHomePage']),\n\tcreated() {\n\t\tthis.initHomePage();\n\t}\n});\n```\n\n数据拿到了，怎么绑定到组件上哪？这就可以用到另两个 helper：`mapState` 和 `mapGetters`。\n\n`mapState` 和 `mapGetters` 同样接受一个数组或对象，并根据相应的值将 store 中的 state 或 getter 绑定到组件上。\n\n```JavaScript\nimport vue from 'vue';\nimport { mapState, mapGetters, mapActions } from 'vuex';\n\nimport template from './home.html';\n\nconst Home = vue.extend({\n\ttemplate,\n\tcomputed: {\n\t\t...mapState({\n\t\t\theader: state => state.home.header\n\t\t}),\n\t\t...mapGetters(['postsList'])\n\t},\n\tmethods: mapActions(['initHomePage']),\n\tcreated() {\n\t\tthis.initHomePage();\n\t}\n});\n```\n\n哈哈，这样模板不用改变一分一毫，升级就完成啦~\n\n是不是很简洁，很优雅~\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vuex-core-of-vue-application/handsome.jpg)\n\n### 容器组件和展示组件\n容器组件和展示组件这个概念在 [Redux 入门](http://discipled.me/posts/getting-started-with-redux)一文中已有提到。然而，这个概念并不只服务于 react，在 vue 中也可以用到。\n\n简单来说，容器组件就是用于包裹展示组件的组件，它和界面展示无关，它负责数据的获取和传递，之前的 home 组件就是一个容器组件，再来看看它的 template，你会发现它除了根元素以外，不包含其他任何的 html 标签。\n\n```Html\n<section>\n\t<!-- Content Header -->\n\t<content-header :board-img=\"header.image\" :title=\"header.title\" :subtitle=\"header.subtitle\"></content-header>\n\n\t<!-- Main Content -->\n\t<main-content>\n\t\t<post-list :post-list=\"postsList\"></post-list>\n\t</main-content>\n</section>\n```\n\n与此相反的是，展示组件单单用于展示，自己不获取任何数据，数据都通过 `props` 传递，比如 content-header。\n\n```JavaScript\nconst template = `<header class=\"intro-header\" :style=\"{ backgroundImage: 'url(' + boardImg + ')' }\">\n\t<div class=\"container\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\">\n\t\t\t\t<div class=\"site-heading\">\n\t\t\t\t\t<h1>{{ title }}</h1>\n\t\t\t\t\t<hr class=\"small\">\n\t\t\t\t\t<span class=\"subheading\">{{ subtitle }}</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</header>`;\n\nexport default Vue.component('contentHeader', {\n\ttemplate,\n\tprops: {\n\t\tboardImg: {\n\t\t\ttype: String,\n\t\t\tdefault: _defaultImg\n\t\t},\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\trequired: true\n\t\t},\n\t\tsubtitle: {\n\t\t\ttype: String\n\t\t}\n\t}\n});\n```\n\n这样明确地区分容器组件和展示组件会使得项目结构变得更清晰，追踪 bug ，以及维护也变得轻而易举。\n\n### 管理路由\n是不是觉得这样就完了？\n\nNo, No, No. 路由系统还没处理，那么如何将 vue-router 纳入到 vuex 的管理中哪？\n\n这里又得感谢尤大大为我们造好了一个小工具 **[vuex-router-sync](https://github.com/vuejs/vuex-router-sync)**。\n\n首先，安装\n\n```Bash\nnpm install vuex-router-sync@next --save\n```\n\n然后，在项目初始化的时候将 router 同 store 联系起来就行，简单到都不知道说啥好。\n\n不知道说啥，就说说原理，看看源码吧。\n\n这个工具的原理也非常好理解，主要是 2 点：\n\n一是，给 vuex 的 store 注册一个 router 的 module。\n\n```JavaScript\nfunction patchStore (store) {\n  // 略...\n  var routeModule = {\n    mutations: {\n      'router/ROUTE_CHANGED': function (state, to) {\n        store.state.route = to\n      }\n    }\n  }\n\n  // add module\n  if (store.registerModule) {\n    store.registerModule('route', routeModule)\n  } else if (store.module) {\n    store.module('route', routeModule)\n  } else {\n    store.hotUpdate({\n      modules: {\n        route: routeModule\n      }\n    })\n  }\n}\n```\n另一个，就是使用 vue-router 的 afterEach hooks 来触发 mutation。\n\n```JavaScript\nexports.sync = function (store, router) {\n  patchStore(store)\n  store.router = router\n\n  var commit = store.commit || store.dispatch\n  // 略...\n  \n  // sync store on router navigation\n  router.afterEach(function (transition) {\n    if (isTimeTraveling) {\n      isTimeTraveling = false\n      return\n    }\n    var to = transition.to\n    currentPath = to.path\n    commit('router/ROUTE_CHANGED', to)\n  })\n}\n```\n项目中使用：\n\n```JavaScript\nimport { sync } from 'vuex-router-sync';\nimport store from '../vuex';\nimport router from './router';\n\nsync(store, router);\n\nnew Vue({\n\tstore,\n\trouter,\n\ttemplate: '<blog></blog>'\n}).$mount('#app');\n```\n\nOK，这样就大功告成了。\n\n### 写在最后\n加入了 vuex 后，我的博客终于让 vue 它们一家子（vue + vuex + vue-router）团圆了。\n\n总的来看，vuex 同 vue 一样使用起来相当方便，集成了许多方法，但似乎缺少了 redux 的那份优雅，而我喜欢比较优雅的...（看在全篇我都在安利 vue 的情面上，尤大大请不要打我~）\n\n![逃~](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/vuex-core-of-vue-application/run.jpg)\n\nPS: 一下把 vuex 有关的一股脑都过了，可能过得太快，如有不明白的就留言吧。\n\n最后的最后，当然是继续安利下自己的 [Blog](http://discipled.me/)，以及 [Source Code](https://github.com/DiscipleD/blog)。\n"
  },
  {
    "path": "src/server/data/posts/webpack-alias-in-css.md",
    "content": "### 基本概念\nAlias 是 `resolve` 下的一个子属性，用于给引入文件的路径起别名，它主要有两个好处\n\n* 文件引入简单：避免引入文件时，相对路径太长、查找复杂\n* 配置归于一处：文件移动时，代码改动量小，无需再计算引入文件位置\n\n这个属性比较常见，[文档](https://webpack.js.org/configuration/resolve/#resolve-alias)也很易懂，就不展开了。\n\n这样引入 js 文件、样式文件、照片等等（有适当的 loader）都没有问题了。在 js 里引入样式文件是没有问题了，但问题还没有完美解决。\n\n## alias in css\n通常项目会有一些样式公有变量或规则等等，每个页面的样式文件可能都会引入。\n\n```javascript\n<style lang=\"less\">\n@import '../../../assets/less/common/index';\n// ...\n</style>\n```\n\n呃，又看到了熟悉的相当路径引用...\n\n有没有办法像 js 那样使用 alias 解决哪？\n\n答案就是 `~`。在 `import` 语句中使用 `~` 作为前缀，webpack 就会根据模块规则查找。\n\n```javascript\n<style lang=\"less\">\n@import '~@/assets/less/common/index';\n// ...\n</style>\n```\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack-alias-in-css/almost-prefect.png)\n\n注：如果还是打包失败，可能是 `css-loader` 版本过低的问题，更新至最新就行。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack-alias-in-css/do-not-ask.jpg)\n\n同时，也可以在 `css-loader` 中添加样式专属的 alias.\n\n```javascript\n// ...\n  use: [{\n    loader: 'css-loader',\n      options: {\n        alias: {\n          '@variable': path.resolve(__dirname, '../src/assets/less/common/variable'),\n        }\n      }\n// ...\n```\n\n这样项目再也见不到一长串的相对路径查找了。\n\n---\n[webpack 4 发布了...](https://medium.com/webpack/webpack-4-released-today-6cdb994702d4)（跟不上，溜了溜了...）\n"
  },
  {
    "path": "src/server/data/posts/webpack3-release.md",
    "content": "在之前的[文章](https://discipled.me/posts/upgrade-to-webpack2)里，就提到了因为年前版本回退的原因，我特意推迟了升级 webpack，就怕它又搞什么大新闻。\n\n然而，没想到还是中了圈套，webpack2 坚挺了还不到半年，就迎来了它的替代者。\n\n就在一周前 [webpack3 正式版发布了](https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b)！\n\n这次版本升级的主要原因有以下几点：\n\n* webpack 内部实现变化\n* 新增了模块串联功能。之前，webpack 会为每个模块创建各自的闭包，使用串联功能将模块连接到一起后，就只需为这真个模块创建一个单独的闭包，从而减少不必要的代码\n* 增加动态加载注释，即可为动态加载定义 chunk 名\n\n最最最重要的一点是不用修改任何配置就能从 webpack2 升级至 webpack3，这总算让我上个月的升级没有白费，至少 98% 的用户是这样。\n\n既然，不用改代码就能升级，又能大幅减小输出文件大小，那就升一波看看效果。先看一眼升级前的打包结果，\n\n![before update](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack3-release/before-update.png)\n\n现在，通过 npm 命令默认安装的已经是 3.0.0 的版本。升级的话，因为是大版本，所以别忘了先改 package.json 里面的依赖版本。\n\n升级之后直接跑命令，顺利打包。（谢天谢地，不是那 2%。）\n\n![after update](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack3-release/after-update.png)\n\n只是多了一个 warning。\n\n```JavaScript\nDeprecationWarning: Chunk.modules is deprecated. Use Chunk.getNumberOfModules/mapModules/forEachModule/containsModule instead.\n```\n\n这是由一些 webpack plugin 引起的，比如：`extract-text-webpack-plugin` 等。不过，不用理它。首先，它不影响打包，其次，已经有人提了 [pr](https://github.com/webpack-contrib/extract-text-webpack-plugin/pull/543)。\n\n结果看上去是不是和之前基本一样？不要着急，那是因为还没有用上模块串联的功能。开启模块串联的功能需要在配置中简单的加一个 plugin。\n\n```JavaScript\n\tplugins: [\n\t\t// ...\n\t\tnew webpack.optimize.ModuleConcatenationPlugin()\n\t]\n```\n\n再看一眼结果，\n\n![build with module concatenation plugin](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack3-release/build-with-module-concatenation-plugin.png)\n\n什么~app.js 只小了 3 kb（5%），广告果然都是骗人的，不管国内还是国外...😔（难道姿势不对，升级了的朋友都说说小了多少）\n\n这样 webpack 3 升级就完成了，也用上了新特性，总得来说这次升级在文件大小以及打包时间上还是有所优化的，再加之升级的 effort 几乎为 0，还是非常值得一试的。\n\nPS：ESlint 也发布了 [4.0 版本](http://eslint.org/blog/2017/06/eslint-v4.0.0-released)。\n（前端界一个个都是版本大佬）\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/webpack3-release/dalao.gif)"
  },
  {
    "path": "src/server/data/posts/wechat-minigame-try.md",
    "content": "> 系列文章\n> \n> 1. [微信小程序基础](https://discipled.me/posts/wechat-miniprogram-basic)\n> 2. 微信小游戏初试（本文）\n\n如果，你有开发 h5 游戏的经验，那么相信你能够直接上手微信小游戏。即使，你和我一样之前没有游戏开发经验也没关系，看了本文之后，相信你也可以试着开发一个简单的小游戏玩一玩了。\n\n### 文件结构\n任何应用都会有一个入口文件，微信小游戏也是如此，小游戏的入口文件是根目录下的 `game.js`。从文件名中可以看到，这个入口文件仍是 js 文件。的确，小游戏在开发语言上没有同小程序那样又另建一套规范，而是依旧采用 js 作为开发语言。\n\n其次，一般而言一个应用的代码除了功能逻辑之外，还会有一些配置文件。对于小游戏而言，它只有一个必要的配置文件 `game.json`，而它的配置项更是不足十个。所以说，如果之前有过 h5 游戏的开发经验再来开发小游戏，可以说是基本没有任何的学习成本。\n\n只要有了上述这两个文件，小游戏就可以正常运行了。\n\n### Adapter\n虽然，微信小游戏使用 js 作为开发语言，但小游戏的运行环境是 [JavaScriptCore](https://developer.apple.com/documentation/javascriptcore)(iOS) 和 [V8](https://developers.google.com/v8/index.html)(Android)，而不是熟悉的浏览器或者 Node，也就没有 BOM, DOM 或者文件操作等 API。\n\n你可能会疑惑，连 DOM 都没有了还怎么玩？不用担心，微信自身提供了一系列 API 来完成创建画布、绘制图形、显示图片以及响应用户交互等基础功能。\n\n> “又有 API，不是说好没有学习成本吗？”\n\n这里就又要吹一波微信了。\n\n微信提供了一个名为 `weapp-adapter` 的非常棒的库文件，用于浏览器或 Node API 到微信 API 之间的适配。只需在入口文件引入它，就可以不用额外学习微信 API，而是直接使用 DOM 或其他（如 Node）API 来编写小游戏了。\n\n注：adapter 会自动创建一个 canvas 并暴露到全局。这个 canvas 也是主画布，之后创建的 canvas 都不会直接显示，如要显示，需将它们画到主画布上。\n\n![小游戏架构](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-minigame-try/minigame-framework.png)\n\n当然，这个 adapter 也不是完美的，它还是有着许多的不足之处。\n\n微信官方对它的定位是一个第三方库，并不属于小游戏的范畴，之后也将不再维护。不过，微信提供现有 adapter 实现的[源码下载](https://mp.weixin.qq.com/debug/wxagame/dev/tutorial/weapp-adapter.zip)，之后可以根据各自需要，自行添加功能进行维护。\n\n其次，所有的适配最终是通过微信提供的 API 实现，所以它对浏览器 API 的模拟是不完整的。\n\n另外，图中的游戏引擎之前没有接触过就不多说了，有兴趣的可以关注[官方文档](https://mp.weixin.qq.com/debug/wxagame/dev/tutorial/base/engine.html)。\n\n小游戏的大致架构就介绍完了，闲话不多说，先搞个简单的小游戏操练起来。\n\n### 敲砖块小游戏\n很久之前学 canvas 的时候，正好跟着 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Advanced_animations) 做过一个[敲砖块的小游戏](https://github.com/DiscipleD/eliminate-bricks)，正好这次拿来试一试。\n\n#### 代码迁移\n原先的代码模块划分没有作好，都写在了一个文件里，但这也方便了这次迁移。\n\n首先，创建一个 `game.js` 文件，在第一行引入 adapter，这很重要。同时，不要忘了创建一个 `game.json` 文件，只需设置一下显示的方向。\n\n然后，将原有的代码从获取 canvas 元素一直到末尾全部复制到 `game.js` 中，保存运行。\n\nNo warning! 一次成功。\n\n不过，现在这个游戏还不能动起来，需要将原先的 mouse 事件转换为 touch 事件。\n\n#### 事件转换\n首先，将原先的一系列控制游戏开始、暂停的 `click`, `mouseenter` 和 `mouseout` 事件收拢成 `touchstart` 事件。\n\n```javascript\n  this.canvas.addEventListener('touchstart', function () {\n    if (!$this.game.start) {\n      $this.game.start = true;\n      $this.ref = window.requestAnimationFrame(function () { $this.draw($this); });\n    } else {\n      $this.game.start = false;\n      window.cancelAnimationFrame($this.ref);\n    }\n  });\n```\n\n接着是控制挡板左右移动的事件，原先是通过鼠标的移动来控制的，在移动端自然没有了鼠标，原本打算还是用 touch 事件来控制。在翻阅了小游戏的 API 之后，我发现了一个更好的选择——重力控制。\n\n```javascript\n  wx.onAccelerometerChange(function (e) {\n    if ($this.game.start && !$this.game.over) {\n      $this.ct.clearRect(0, $this.canvas.height - $this.board.height, $this.canvas.width, $this.canvas.height);\n      var distance = e.x * $this.canvas.width;\n      $this.board.x = $this.getBoardX($this.canvas.width / 2 + distance, $this.board);\n      $this.board.draw();\n    }\n  });\n```\n\n现在就可以通过左右倾斜手机来控制挡板的移动了，是不是更有趣了？\n\n从 `wx.onAccelerometerChange` 方法就可以看到，微信还提供了许多浏览器以外的功能，这里就不一一举例了，有兴趣的同学可以查阅下[文档](https://mp.weixin.qq.com/debug/wxagame/dev/document/render/canvas/wx.createCanvas.html)。微信小游戏的初探就到这里，消除砖块的功能就留给大家自己去尝试了。\n\nPS：截止最新，微信小游戏还未正式开放。 \n\n### 写在最后\n就如[上一篇文章](https://discipled.me/posts/wechat-miniprogram-basic)中所提到的，微信小游戏相较于原生 APP 的主要优势在于：微信——拥有庞大用户数，强社交，易推广。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-minigame-try/bold-idea.jpg)\n\n等小游戏正式开放上线...\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-minigame-try/cool.gif)"
  },
  {
    "path": "src/server/data/posts/wechat-miniprogram-basic.md",
    "content": "> 系列文章\n> \n> 1. 微信小程序基础（本文）\n> 2. [微信小游戏初试](https://discipled.me/posts/wechat-minigame-try)\n\n2018 年过了不到一个月，时间虽短但有一样新东西在这短短时间里就火了起来。从“跳一跳”，到“坦克大战”，再到“头脑王者”，微信小游戏好像突然将时间拉回到了过去。餐桌上、休息时大家不再是各自刷着微博、段子，而是聚在一起开始一场场紧（ge）张（zhong）刺（zhuang）激（bi）的对战。小游戏充分利用了人们零碎的时间，并将娱乐和社交有效地结合起来。\n\n在小游戏推出之前，本人是看衰小程序的，所以一直没有入坑。然而，小游戏狠狠地打了我的脸，它的出现让我眼前一亮，不单单让我觉得小游戏有着很大的想象空间，更让我觉得微信这个平台有着无限的可能。\n\n当前，微信是将小游戏作为小程序的一个分类，所以暂时先亡羊补牢看看什么是小程序。\n\n### 小程序\n要学一样新东西的最好办法自然是看官网教程。\n\n小程序的官网是[微信公众平台](https://mp.weixin.qq.com/)，其他什么乱七八糟的都是外包广告啦~\n\n在官网上可以轻松地找到小程序和小游戏的教程。微信的教程也相当详细，这边就不再赘述了。\n\n如果跟着教程走，其中第二步微信会推荐你安装一个开发者工具，这里要吹一波这个工具。\n\n#### 接近完美的开发者工具\n新版的开发者工具和之前仅能够用于调试的代理工具完全不同，可以说是鸟枪换炮。\n\n接着就来看一下这个工具到底惊艳在哪里？\n\n首次打开工具，你会看到一个类似下图的界面，会让你填一些项目的基础信息。其中的 AppID 可以通过注册获得，不过即使没有 AppID 也可以先创建项目进行开发，这里先选体验小程序。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-miniprogram-basic/wechat-devtool-create.png)\n\n如果，选择一个空文件夹作为项目的目录，那么，在工具的最下方就会出现一个模板项目的选项。勾选它，创建的项目就包含了一个微信的 Demo 项目。这个小功能当然不是这个工具的亮点所在，这里先点确定生成一个 Demo 项目。\n\n登登登等~\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-miniprogram-basic/wechat-devtool-project.png)\n\n有没有被惊艳到？\n\n工具左上角的 3 个按钮分别控制模拟器、编辑器和调试器区域的显示与否。模拟器和调试器的模样是不是非常熟悉？[滑稽]\n\n这个开发者工具可以说是集成了浏览器和 IDE，以及代理等工具于一身，所有的开发工作几乎可以在这一个工具中完成，再也不用在一个个应用之间来回切换了。\n\n整洁、干净、完美！（要被吸粉了...）\n\nPS: 虽然，可以在设置里修改编辑器的配置，不过和真正的 IDE 比样子还是丑了一点。\n\n开发工具就说到这，更多功能等你自己去探索。看完了酷炫的工具，平复一下心情，继续来看 Demo 项目。\n\n#### WXML, WXSS 和 WXS\n在 Demo 项目中，你会看到两种新类型的文件：.wxml 和 .wxss。这也是我之前看（xian）衰（qi）小程序的主要原因，它并没有使用标准的文件类型及语法，而是创立了一套微信自己的标准。\n\n##### WXML\n先看一下 wxml 里面究竟有什么名堂？\n\n```wxml\n<!--index.wxml-->\n<view class=\"container\">\n  <view class=\"userinfo\">\n    <button wx:if=\"{{!hasUserInfo && canIUse}}\" open-type=\"getUserInfo\" bindgetuserinfo=\"getUserInfo\"> 获取头像昵称 </button>\n    <block wx:else>\n      <image bindtap=\"bindViewTap\" class=\"userinfo-avatar\" src=\"{{userInfo.avatarUrl}}\" background-size=\"cover\"></image>\n      <text class=\"userinfo-nickname\">{{userInfo.nickName}}</text>\n    </block>\n  </view>\n  <view class=\"usermotto\">\n    <text class=\"user-motto\">{{motto}}</text>\n  </view>\n</view>\n```\n\n是不是又很熟悉？这不就是多了些默认组件的 vue 嘛。\n\n需要注意的是，`{{}}` 与引号之间不能有空格，否则会解析为字符串。其他语法层面就没有什么难点了，再撸一遍[基础组件文档](https://mp.weixin.qq.com/debug/wxadoc/dev/component/)就差不多了。\n\n##### WXSS\nwxss 的变化就更小了，就多提供了一个单位 `rpx`。\n\n`1rpx` 等于屏幕尺寸的 1/750。（UI 出 750 的图就很好做啦...）\n\n剩下就提供了一些简单的选择器，类、Id、元素和前后的伪类，没有其他的学习成本。\n\n最后说一下 wxs（Demo 项目中没有用到）。\n\n##### WXS\n什么是 WXS？微信官方是这样说的：\n\n> WXS（WeiXin Script）是小程序的一套脚本语言，结合 WXML，可以构建出页面的结构。\n> \n> wxs 与 javascript 是不同的语言，有自己的语法，并不和 javascript 一致。\n\n但是，整个[文档](https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxs/)看下来，除了在模块的处理上有些许的不同之外，其他可以说是破产版的 js 吧。既然，它是 js 的子集，那么，都用 js 来写也没什么毛病，暂时也没有看出什么场景必须使用它。\n\n~~感觉整套都是 kpi 的产物哪...~~\n\n小程序其他的配置文件[文档](https://mp.weixin.qq.com/debug/wxadoc/dev/framework/config.html)里已经写得很清晰了。至此，小程序的基础就暂告一段落，下篇将关于小游戏相关内容，敬请期待。\n\n![](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/wechat-miniprogram-basic/end.jpg)\n"
  },
  {
    "path": "src/server/data/posts/why-curry-helps.md",
    "content": "编写的代码能被毫不费力地重复使用是程序员的一个白日梦。首先，它是有含义的，因为代码都是根据需求用某种方式所写的；并且，它是可重用的，因为你打算重用它。你还想要什么？\n\n[柯里化](https://npmjs.org/package/curry)能帮上忙。\n\n### 什么是柯里化？它又棒在哪里？\n普通的 JavaScript 调用会像这样：\n\n```JavaScript\n\tvar add = function(a, b){ return a + b }\n\tadd(1, 2) //= 3\n```\n\n一个函数有多个参数，并有一个返回值。调用的参数可以比定义的少（会出错），或多（被忽视）：\n\n```JavaScript\n\tadd(1) //= NaN\n\tadd(1, 2, 'IGNORE ME') //= 3\n```\n\n柯里化后的函数是将多个参数由一系列单参数函数表示的函数。比如，柯里化后的加法会是这样：\n\n```JavaScript\n\tvar add = curry(function(a, b){ return a + b })\n\tvar add100 = add(100)\n\tadd100(1) //= 101\n```\n\n如果柯里化函数需要接受多个参数，需要这样写：\n\n```JavaScript\n\tvar sum3 = curry(function(a, b, c){ return a + b + c })\n\tsum3(1)(2)(3) //= 6\n```\n\n由于这在 JavaScript 语法中相当丑陋，柯里化接受一次调用时有多个参数：\n\n```JavaScript\n\tvar sum3 = curry(function(a, b, c){ return a + b + c })\n\tsum3(1, 2, 3) //= 6\n\tsum3(1)(2, 3) //= 6\n\tsum3(1, 2)(3) //= 6\n```\n\n### 所以？\n\n如果你不习惯柯里化函数是语言的常规部分，那么柯里化可能对你没有什么明显的优势，而在我看来，它有 2 大优势：\n\n* 程序片段越小越容易被配置\n* 尽可能地函数化\n\n### 小片段\n举个显而易见的例子，获取集合每个成员的 id：\n\n```JavaScript\n\tvar objects = [{ id: 1 }, { id: 2 }, { id: 3 }]\n\tobjects.map(function(o){ return o.id })\n```\n\n如果你想知道第二行的逻辑，让我来告诉你：\n\n> **循环**遍历**对象**来获得 **id**\n\n就在这行里就有许多不佳的实现，让我带你一个个清楚它。首先，在函数定义中，\n\n```JavaScript\n\tvar get = curry(function(property, object){ return object[property] })\n\tobjects.map(get('id')) //= [1, 2, 3]\n```\n\n以上创建的 get 函数是一个可配置的函数。\n\n如果我们想重用获取对象 id 的功能，就可以这样简单地使用它：\n\n```JavaScript\n\tvar getIDs = function(objects){\n\t    return objects.map(get('id'))\n\t}\n\tgetIDs(objects) //= [1, 2, 3]\n```\n\n嗯，这似乎又从优雅简洁回到了呆板。我们还能做些什么？那如果，遍历方法也可以被配置哪？\n\n```JavaScript\n\tvar map = curry(function(fn, value){ return value.map(fn) })\n\tvar getIDs = map(get('id'))\n\n\tgetIDs(objects) //= [1, 2, 3]\n```\n\n从上面的代码中可以看到，如果代码的基础模块是柯里化函数，就可以轻松地更新它，或给它添加新的功能。更令人兴奋的是，代码读起来就如同真正的逻辑。\n\n### 尽可能地函数化\n\n这种方法的另一个优点是，它鼓励创建函数，而非方法。虽然方法具有多态和易读的优势，但它们并不总是最适合的工具，比如在大量异步代码的情景下。\n\n在这个小例子中，我们试着从服务器中获取一些数据，并用一些有用的方式来处理它。数据看起来会像这样：\n\n```JavaScript\n\t{\n\t    \"user\": \"hughfdjackson\",\n\t    \"posts\": [\n\t        { \"title\": \"why curry?\", \"contents\": \"...\" },\n\t        { \"title\": \"prototypes: the short(est possible) story\", \"contents\": \"...\" }\n    \t]\n\t}\n```\n\n目标就是获取每个 posts 中的 title。\n\n```JavaScript\n\tfetchFromServer()\n\t\t.then(JSON.parse)\n\t\t.then(function(data){ return data.posts })\n\t\t.then(function(posts){\n\t\t\treturn posts.map(function(post){ return post.title })\n\t\t})\n```\n\n由于 Promise 链（或者你更倾向于回调）本质上是函数调用，你不能轻易的映射一个从服务器中获取但尚未经过加工的值。这整个无论从视觉（或心理）上都很粗糙。\n\n那试试用刚刚定义好的工具来看看：\n\n```JavaScript\n\tfetchFromServer()\n\t\t.then(JSON.parse)\n\t\t.then(get('posts'))\n\t\t.then(map(get('title')))\n```\n\n如果没有将函数经过柯里化，就无法如此轻易地使代码逻辑变得如此精炼和易于表达。\n\n### 概括\n\n柯里化会给你写代码带来神奇的力量。\n\n我建议你掌握并熟练地运用它。如果你已经熟悉它的概念，我猜你会对它的 API 很满意，如果不是，你和你的同事应该试着考虑它。\n\n#### 原文链接：[why-curry-helps](https://hughfdjackson.com/javascript/why-curry-helps/)"
  },
  {
    "path": "src/server/data/posts/you-might-not-need-redux.md",
    "content": "> 原文链接：[You Might Not Need Redux](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367#.a98d3x6e7)\n\n人们常常在正真需要 Redux 之前，就选择使用它。“如果不使用 Redux，我们的应用无法扩展怎么办？”应用接入 Redux 之后，开发者就开始头疼了。“为什么为了开发一个简单的功能需要创建 3 个文件？”为什么！\n\n![为什么！(配图译者加)](https://raw.githubusercontent.com/DiscipleD/image-storage/master/blog/you-might-not-need-redux/wtf.png)\n\n人们痛苦地抱怨 Redux, React, FP, 不可变数据和一些别的东西，但我理解他们。那些不需要一系列代码来更新应用状态的方法自然比使用 Redux 更为简单。这说的没错，设计上也是如此。\n\nRedux 提供了一种权衡。它要求你：\n\n* 用简单的对象和数组来描述应用状态\n* 用简单对象来描述应用中的变更\n* 用纯函数来描述处理变更的逻辑\n\n无论是不是 React 应用，这些限制都不是创建一个应用所必须的。事实上，这些都是非常强的约束，在把它们加入应用之前，你应当慎重考虑。\n\n你有没有一些好的理由来使用 Redux？\n\n当然是有的。这些限制吸引我是因为它同时也能够使应用拥有以下的特性：\n\n* [持久化应用状态到 LocalStorage，之后应用可以根据该状态启动。](https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage?course=building-react-applications-with-idiomatic-redux)（视频预警）\n* [服务器初始化应用状态，客户端直接根据该状态启动。](http://redux.js.org/docs/recipes/ServerRendering.html)\n* [序列化用户操作，并将它们连同应用状态的快照一同添加到错误报告中，使得产品开发人员能够通过重放用户操作来重现错误。](https://github.com/dtschust/redux-bug-reporter)\n* [无需要对代码做巨大的修改，就能通过网络传递简单的 Action 对象来实现协作环境。](https://github.com/philholden/redux-swarmlog)\n* [无需要对代码做巨大的修改，就能实现撤销或重做。](http://redux.js.org/docs/recipes/ImplementingUndoHistory.html)\n* [在开发环境中，可以在应用状态历史中旅行，并且当代码修改时，会根据操作历史重新计算当前状态。](https://github.com/gaearon/redux-devtools)\n* [开发工具提供了全面的检查和控制能力，使产品开发人员能为他们的应用构建自定义工具。](https://github.com/romseguy/redux-devtools-chart-monitor)\n* [当 UI 变更时，可以重用大部分业务逻辑。](https://www.youtube.com/watch?v=gvVpSezT5_M&feature=youtu.be&t=11m51s)（视频预警）\n\n如果，你正在开发一个[可扩展的终端](https://hyperterm.org/)、[JavaScript 调试器](https://hacks.mozilla.org/2016/09/introducing-debugger-html/)，或是[某些类型的应用](https://twitter.com/necolas/status/727538799966715904)，那么，Redux 也许值得一试。至少，它是值得考虑的。（顺便说一句，[Elm](https://github.com/evancz/elm-architecture-tutorial) 和 [Om](https://github.com/omcljs/om) 并不是新技术。）\n\n然而，如果你只是学习 React，那么，Redux 并不是你的首选。\n\n与之相反，你该看看[理解 React](https://facebook.github.io/react/docs/thinking-in-react.html)。当你有真正的需要或想玩一些新东西的时候，才去尝试 Redux。然而，就像你使用其他强限制的工具一样，谨慎地选择是否使用它。\n\n如果，你觉得用 Redux 的方式编码有压力，那可能意味着你或你的伙伴对此太较真了。Redux 只是你工具箱中的[一件工具](https://www.youtube.com/watch?v=xsSnOQynTHs)，[一种尝试](https://www.youtube.com/watch?v=uvAXVMwHJXU)。\n\n最后，记住你可以将 Redux 的理念运用到你的应用中，但不使用 Redux。试想一下，一个拥有本地状态的 React 组件：\n\n```JavaScript\nimport React, { Component } from 'react';\n\nclass Counter extends Component {\n  state = { value: 0 };\n\n  increment = () => {\n    this.setState(prevState => ({\n      value: prevState.value + 1\n    }));\n  };\n\n  decrement = () => {\n    this.setState(prevState => ({\n      value: prevState.value - 1\n    }));\n  };\n  \n  render() {\n    return (\n      <div>\n        {this.state.value}\n        <button onClick={this.increment}>+</button>\n        <button onClick={this.decrement}>-</button>\n      </div>\n    )\n  }\n}\n```\n\n这是非常合理的。认真地说，它值得重复使用。\n\n> 本地状态是好的。\n\nRedux 提供的权衡是通过增加中间环节来将“发生了什么”和“该如何变化”之间进行解耦。\n\n这样做是不是总是正确的哪？不，这是一种权衡。\n\n比如，我们可以从组件中将 reducer 抽出：\n\n```JavaScript\nimport React, { Component } from 'react';\n\nconst counter = (state = { value: 0 }, action) => {\n  switch (action.type) {\n    case 'INCREMENT':\n      return { value: state.value + 1 };\n    case 'DECREMENT':\n      return { value: state.value - 1 };\n    default:\n      return state;\n  }\n}\n\nclass Counter extends Component {\n  state = counter(undefined, {});\n  \n  dispatch(action) {\n    this.setState(prevState => counter(prevState, action));\n  }\n\n  increment = () => {\n    this.dispatch({ type: 'INCREMENT' });\n  };\n\n  decrement = () => {\n    this.dispatch({ type: 'DECREMENT' });\n  };\n  \n  render() {\n    return (\n      <div>\n        {this.state.value}\n        <button onClick={this.increment}>+</button>\n        <button onClick={this.decrement}>-</button>\n      </div>\n    )\n  }\n}\n```\n\n发现没有，我们不必运行 npm install 就能使用 Redux。酷！\n\n状态组件能不能也这样做？可能不行。也就是说，除非你有打算从额外的中间环节中受益。在当前这个时代，想法才是关键。\n\nRedux 库它本身只是一系列的助手将 reducers “挂载”到全局唯一的 store 对象上。你可以根据你的喜好来选择是尽可能少，或尽可能多得使用 Redux。\n\n但是，如果你付出了一些，确保你同时也能获得一些回报。\n\n译者注：如果你对本文感兴趣，你或许也会对这篇[文章](https://medium.freecodecamp.com/where-do-i-belong-a-guide-to-saving-react-component-data-in-state-store-static-and-this-c49b335e2a00)感兴趣。"
  },
  {
    "path": "src/server/data/tags/index.ts",
    "content": "/**\n * Created by jack on 16-8-22.\n */\n\nimport { ITagBase } from '../../../types/tag';\nimport { sortFn } from '../../common/DataService';\n\nconst TAGS_LIST: ITagBase[] = [{\n\tname: 'angular-1.x',\n\tlabel: 'Angular 1.x',\n\tcreatedTime: '2015-12-22',\n}, {\n\tname: 'styleguide',\n\tlabel: 'Style Guide',\n\tcreatedTime: '2016-06-22',\n}, {\n\tname: 'es6',\n\tlabel: 'ECMAScript 2015',\n\tcreatedTime: '2015-10-30',\n}, {\n\tname: 'javascript',\n\tlabel: 'JavaScript',\n\tcreatedTime: '2015-10-30',\n}, {\n\tname: 'css',\n\tlabel: 'CSS',\n\tcreatedTime: '2016-01-29',\n}, {\n\tname: 'postcss',\n\tlabel: 'Postcss',\n\tcreatedTime: '2016-02-25',\n}, {\n\tname: 'autoprefixer',\n\tlabel: 'Autoprefixer',\n\tcreatedTime: '2016-02-25',\n}, {\n\tname: 'tool',\n\tlabel: 'Tool',\n\tcreatedTime: '2015-11-30',\n}, {\n\tname: 'browsersync',\n\tlabel: 'Browsersync',\n\tcreatedTime: '2015-11-30',\n}, {\n\tname: 'design-pattern',\n\tlabel: 'Design Pattern',\n\tcreatedTime: '2016-04-13',\n}, {\n\tname: 'translate',\n\tlabel: 'Translate',\n\tcreatedTime: '2016-04-13',\n}, {\n\tname: 'redux',\n\tlabel: 'Redux',\n\tcreatedTime: '2016-07-06',\n}, {\n\tname: 'state-management',\n\tlabel: 'State Management',\n\tcreatedTime: '2016-07-06',\n}, {\n\tname: 'graphql',\n\tlabel: 'GraphQL',\n\tcreatedTime: '2016-08-01',\n}, {\n\tname: 'graphql-js',\n\tlabel: 'GraphQL JS',\n\tcreatedTime: '2016-08-03',\n}, {\n\tname: 'document',\n\tlabel: 'Document',\n\tcreatedTime: '2016-03-26',\n}, {\n\tname: 'npm',\n\tlabel: 'npm',\n\tcreatedTime: '2016-04-27',\n}, {\n\tname: 'cnpm',\n\tlabel: 'cnpm',\n\tcreatedTime: '2016-04-27',\n}, {\n\tname: 'sinopia',\n\tlabel: 'Sinopia',\n\tcreatedTime: '2016-04-27',\n}, {\n\tname: 'ui-router',\n\tlabel: 'ui-router',\n\tcreatedTime: '2016-05-28',\n}, {\n\tname: 'redux-ui-router',\n\tlabel: 'redux-ui-router',\n\tcreatedTime: '2016-07-23',\n}, {\n\tname: 'ng-redux',\n\tlabel: 'ng-redux',\n\tcreatedTime: '2016-07-23',\n}, {\n\tname: 'vue1',\n\tlabel: 'Vue 1.0',\n\tcreatedTime: '2016-08-14',\n}, {\n\tname: 'vue2',\n\tlabel: 'Vue 2.0',\n\tcreatedTime: '2016-08-14',\n}, {\n\tname: 'vue-router',\n\tlabel: 'vue-router',\n\tcreatedTime: '2016-08-14',\n}, {\n\tname: 'vuex',\n\tlabel: 'vuex',\n\tcreatedTime: '2016-08-21',\n}, {\n\tname: 'debug',\n\tlabel: 'Debug',\n\tcreatedTime: '2016-09-03',\n}, {\n\tname: 'wechat',\n\tlabel: 'wechat',\n\tcreatedTime: '2016-09-03',\n}, {\n\tname: 'material-design',\n\tlabel: 'Material Design',\n\tcreatedTime: '2016-09-11',\n}, {\n\tname: 'react',\n\tlabel: 'React',\n\tcreatedTime: '2016-09-23',\n}, {\n\tname: 'ci',\n\tlabel: 'CI',\n\tcreatedTime: '2016-10-19',\n}, {\n\tname: 'travis',\n\tlabel: 'Travis-ci',\n\tcreatedTime: '2016-10-19',\n}, {\n\tname: 'codecov',\n\tlabel: 'Code',\n\tcreatedTime: '2016-10-19',\n}, {\n\tname: 'nightwatch',\n\tlabel: 'Nightwatch',\n\tcreatedTime: '2016-10-19',\n}, {\n\tname: 'saucelabs',\n\tlabel: 'Sauce Labs',\n\tcreatedTime: '2016-10-19',\n}, {\n\tname: 'ssr',\n\tlabel: 'Server Side Render',\n\tcreatedTime: '2016-11-30',\n}, {\n\tname: 'seo',\n\tlabel: 'SEO',\n\tcreatedTime: '2016-11-30',\n}, {\n\tname: 'koa2',\n\tlabel: 'KOA 2',\n\tcreatedTime: '2016-11-30',\n}, {\n\tname: 'structure-data',\n\tlabel: 'Structure data',\n\tcreatedTime: '2016-12-21',\n}, {\n\tname: 'rdfa-lite',\n\tlabel: 'RDFa Lite',\n\tcreatedTime: '2016-12-21',\n}, {\n\tname: 'docker',\n\tlabel: 'Docker',\n\tcreatedTime: '2017-01-30',\n}, {\n\tname: 'docker-compose',\n\tlabel: 'Docker Compose',\n\tcreatedTime: '2017-01-30',\n}, {\n\tname: 'nginx',\n\tlabel: 'Nginx',\n\tcreatedTime: '2017-01-30',\n}, {\n\tname: 'https',\n\tlabel: 'Https',\n\tcreatedTime: '2017-01-30',\n}, {\n\tname: 'certbot',\n\tlabel: 'Certbot / Let’s Encrypt',\n\tcreatedTime: '2017-01-30',\n}, {\n\tname: 'ui',\n\tlabel: 'UI',\n\tcreatedTime: '2017-02-16',\n}, {\n\tname: 'design',\n\tlabel: 'Design',\n\tcreatedTime: '2017-02-16',\n}, {\n\tname: 'pwa',\n\tlabel: 'Progressive web apps',\n\tcreatedTime: '2017-02-25',\n}, {\n\tname: 'service-workers',\n\tlabel: 'Service Workers',\n\tcreatedTime: '2017-02-25',\n}, {\n\tname: 'notification',\n\tlabel: 'Notification',\n\tcreatedTime: '2017-03-21',\n}, {\n\tname: 'installable',\n\tlabel: 'Add to home screen',\n\tcreatedTime: '2017-04-01',\n}, {\n\tname: 'webshare',\n\tlabel: 'Web Share',\n\tcreatedTime: '2017-04-01',\n}, {\n\tname: 'minimax',\n\tlabel: 'Minimax',\n\tcreatedTime: '2017-04-04',\n}, {\n\tname: 'alpha-beta',\n\tlabel: 'Alpha beta pruning',\n\tcreatedTime: '2017-04-04',\n}, {\n\tname: 'webpack',\n\tlabel: 'Webpack',\n\tcreatedTime: '2017-04-09',\n}, {\n\tname: 'fp',\n\tlabel: 'Functional Programming',\n\tcreatedTime: '2017-06-21',\n}, {\n\tname: 'typescript',\n\tlabel: 'TypeScript',\n\tcreatedTime: '2017-08-11',\n}, {\n\tname: 'tslint',\n\tlabel: 'Tslint',\n\tcreatedTime: '2017-08-11',\n}, {\n\tname: 'wechat',\n\tlabel: 'Wechat',\n\tcreatedTime: '2018-01-31',\n}, {\n\tname: 'miniprogram',\n\tlabel: 'Miniprogram',\n\tcreatedTime: '2018-01-31',\n}, {\n\tname: 'minigame',\n\tlabel: 'Minigame',\n\tcreatedTime: '2018-02-25',\n}, {\n\tname: 'babel',\n\tlabel: 'Babel',\n\tcreatedTime: '2018-08-04',\n}];\n\nexport default TAGS_LIST.sort(sortFn('createdTime'));\n"
  },
  {
    "path": "src/server/graphql/index.ts",
    "content": "/**\n * Created by jack on 16-7-30.\n */\nimport { GraphQLSchema } from 'graphql';\n\nimport query from './query';\n\nconst schema = new GraphQLSchema({\n\tquery,\n});\n\nexport default schema;\n"
  },
  {
    "path": "src/server/graphql/query/Pager.ts",
    "content": "/**\n * Created by jack on 16-9-11.\n */\n\nimport {\n\tGraphQLInputObjectType,\n\tGraphQLFloat,\n} from 'graphql';\n\n/**\n * type Pager {\n *   pageNumber: Number,\n *   pageSize: Number\n * }\n */\nconst Pager = new GraphQLInputObjectType({\n\tname: 'PagerInputType',\n\tfields: {\n\t\tnumber: {\n\t\t\ttype: GraphQLFloat,\n\t\t\tdefaultValue: 0,\n\t\t},\n\t\tsize: {\n\t\t\ttype: GraphQLFloat,\n\t\t\tdefaultValue: 5,\n\t\t},\n\t},\n});\n\nexport default Pager;\n"
  },
  {
    "path": "src/server/graphql/query/Post.ts",
    "content": "/**\n * Created by jack on 16-7-30.\n */\n\nimport {\n\tGraphQLObjectType,\n\tGraphQLString,\n\tGraphQLID,\n\tGraphQLNonNull,\n\tGraphQLList,\n} from 'graphql';\n\nimport PostType from '../../../types/post';\nimport Tag from './Tag';\nimport PostService from '../../queries/PostService';\nimport TagService from '../../queries/TagService';\n\n/**\n * type Post {\n *   id: String!,\n *   name: String!,\n *   createdTime: String,\n *   title: String!,\n *   subtitle: String,\n *   headerImgName: String,\n *   content: String,\n *   prevPost: Post,\n *   nextPost: Post,\n *   tags: [Tag]\n * }\n */\nconst Post = new GraphQLObjectType({\n\tname: 'PostType',\n\tfields: (): any => ({\n\t\tid: {\n\t\t\ttype: new GraphQLNonNull(GraphQLID),\n\t\t},\n\t\tname: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString),\n\t\t},\n\t\tcreatedTime: {\n\t\t\ttype: GraphQLString,\n\t\t},\n\t\ttitle: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString),\n\t\t},\n\t\tsubtitle: {\n\t\t\ttype: GraphQLString,\n\t\t},\n\t\theaderImgName: {\n\t\t\ttype: GraphQLString,\n\t\t},\n\t\tcontent: {\n\t\t\ttype: GraphQLString,\n\t\t},\n\t\tprevPost: {\n\t\t\ttype: Post,\n\t\t\tresolve: (post: PostType) => PostService.getPreviousPost(post.id),\n\t\t},\n\t\tnextPost: {\n\t\t\ttype: Post,\n\t\t\tresolve: (post: PostType) => PostService.getNextPost(post.id),\n\t\t},\n\t\ttags: {\n\t\t\ttype: new GraphQLList(Tag),\n\t\t\tresolve: (post: PostType) => TagService.queryTagsByPostId(post.id),\n\t\t},\n\t}),\n});\n\nexport default Post;\n"
  },
  {
    "path": "src/server/graphql/query/Tag.ts",
    "content": "/**\n * Created by jack on 16-7-30.\n */\n\nimport {\n\tGraphQLObjectType,\n\tGraphQLString,\n\tGraphQLID,\n\tGraphQLNonNull,\n\tGraphQLList,\n} from 'graphql';\n\nimport Tag from '../../../types/tag';\nimport PostType from './Post';\nimport PostService from '../../queries/PostService';\nimport { sortFn } from '../../common/DataService';\n\n/**\n * type Tag {\n *   id: ID!,\n *   name: String!,\n *   label: String!,\n *   createdTime: String,\n *   posts: [Post]\n * }\n */\nexport default new GraphQLObjectType({\n\tname: 'TagType',\n\tfields: (): any => ({\n\t\tid: {\n\t\t\ttype: new GraphQLNonNull(GraphQLID),\n\t\t},\n\t\tname: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString),\n\t\t},\n\t\tlabel: {\n\t\t\ttype: new GraphQLNonNull(GraphQLString),\n\t\t},\n\t\tcreatedTime: {\n\t\t\ttype: GraphQLString,\n\t\t},\n\t\tposts: {\n\t\t\ttype: new GraphQLList(PostType),\n\t\t\tresolve: (tag: Tag) => PostService.queryPostsListByTagName(tag.name).sort(sortFn('createdTime', -1)),\n\t\t},\n\t}),\n});\n"
  },
  {
    "path": "src/server/graphql/query/index.ts",
    "content": "/**\n * Created by jack on 16-7-30.\n */\n\nimport {\n\tGraphQLObjectType,\n\tGraphQLString,\n\tGraphQLList,\n} from 'graphql';\n\nimport Post from './Post';\nimport Tag from './Tag';\nimport PagerInput from './Pager';\nimport PostService from '../../queries/PostService';\nimport TagService from '../../queries/TagService';\n\n/**\n * type RootQueryType {\n *   post: Post,\t// 查询一篇文章\n *   posts: [Post],\t// 查询一组文章，用于博客首页\n *   tags: [Tag],\t// 查询标签，用于博客标签页\n * }\n */\nconst rootQueryType = new GraphQLObjectType({\n\tname: 'RootQueryType',\n\tfields: () => ({\n\t\tpost: {\n\t\t\ttype: Post,\n\t\t\targs: {\n\t\t\t\tname: {\n\t\t\t\t\ttype: GraphQLString,\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolve: (blog, { name }) => PostService.getPostByName(name),\n\t\t},\n\t\tposts: {\n\t\t\ttype: new GraphQLList(Post),\n\t\t\targs: {\n\t\t\t\tpager: {\n\t\t\t\t\ttype: PagerInput,\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolve: (blog, { pager }) => PostService.queryPostsList(pager),\n\t\t},\n\t\ttags: {\n\t\t\ttype: new GraphQLList(Tag),\n\t\t\targs: {\n\t\t\t\tname: {\n\t\t\t\t\ttype: GraphQLString,\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolve: (blog, { name }) => !name ? TagService.queryTags() : [TagService.getTagByName(name)],\n\t\t},\n\t}),\n});\n\nexport default rootQueryType;\n"
  },
  {
    "path": "src/server/middleware/index.js",
    "content": "/**\n * Created by jack on 16-8-22.\n */\n\nimport path from 'path';\n\nimport { readFile } from '../common/DataService';\nimport serverRender from './server-render';\n\nlet _404HTML;\nreadFile(path.join(__dirname, '../../404.html')).then(buffer => { _404HTML = buffer.toString(); });\n\n// server error catcher\nconst serverErrorHandler = async (ctx, next) => {\n\ttry {\n\t\tawait next();\n\t} catch (err) {\n\t\terr.status = err.statusCode || err.status || 500;\n\t\tthrow err;\n\t}\n};\n\n// 404 handler\nconst pageNotFound = async (ctx, next) => {\n\tawait next();\n\n\tif (404 != ctx.status) return;\n\n\t// we need to explicitly set 404 here\n\t// so that koa doesn't assign 200 on body=\n\tctx.status = 404;\n\n\tswitch (ctx.accepts('html', 'json')) {\n\t\tcase 'html':\n\t\t\tctx.type = 'text/html';\n\t\t\tctx.body = _404HTML;\n\t\t\tbreak;\n\t\tcase 'json':\n\t\t\tctx.body = {\n\t\t\t\tmessage: 'Page Not Found'\n\t\t\t};\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tctx.type = 'text';\n\t\t\tctx.body = 'Page Not Found';\n\t}\n};\n\n// x-response-time\nconst responseTime = async (ctx, next) => {\n\tconst start = new Date();\n\tawait next();\n\tconst ms = new Date() - start;\n\tctx.set('X-Response-Time', `${ms}ms`);\n};\n\n// logger\nconst logger = async (ctx, next) => {\n\tconst start = new Date();\n\tawait next();\n\tconst ms = new Date() - start;\n\tconsole.log('%s %s - %s', ctx.method, ctx.url, `${ms}ms`);\n};\n\nexport {serverRender, serverErrorHandler, pageNotFound, responseTime, logger};\n"
  },
  {
    "path": "src/server/middleware/server-render.js",
    "content": "/**\n * Created by jack on 16-11-27.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { createBundleRenderer } from 'vue-server-renderer';\nimport LRU from 'lru-cache';\n\nimport { indexTemplatePath, clientManifestFileName, serverBundleFileName } from '../../../config/webpack/setting';\nimport serverConfig from '../../../config/webpack/server';\n\nconst template = fs.readFileSync(indexTemplatePath, 'utf8');\nlet renderer;\n\nexport const createRenderer = (bundle, options = {}) => {\n\trenderer = createBundleRenderer(bundle, Object.assign({\n\t\ttemplate,\n\t\tcache: LRU({\n\t\t\tmax: 1000\n\t\t}),\n\t\trunInNewContext: false\n\t}, options));\n};\n\nif (process.env.NODE_ENV === 'production') {\n\tconst serverBundlePath = path.join(serverConfig.output.path, serverBundleFileName);\n\tcreateRenderer(require(serverBundlePath), {\n\t\tclientManifest: require(`${serverConfig.output.path}/${clientManifestFileName}`),\n\t});\n}\n\nconst renderServer = async ctx => {\n\tconst context = { url: ctx.url };\n\t// Have to create a promise, because koa don't wait for render callback\n\tawait new Promise((resolve, reject) => {\n\t\trenderer.renderToString(\n\t\t\tcontext,\n\t\t\t(error, vueApp) => {\n\t\t\t\tif (error) {\n\t\t\t\t\tif (error.code === '404') {\n\t\t\t\t\t\tctx.status = 404;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tctx.type = 'text/html';\n\t\t\t\t\tctx.body = vueApp;\n\t\t\t\t}\n\t\t\t\tresolve();\n\t\t\t});\n\t});\n};\n\nexport default renderServer;\n"
  },
  {
    "path": "src/server/middleware/webpack-middleware.js",
    "content": "/**\n * Created by jack on 16-11-28.\n */\n\nimport path from 'path';\nimport webpack from 'webpack';\nimport webpackDevMiddleware from 'webpack-dev-middleware';\nimport webpackHotMiddleware from 'webpack-hot-middleware';\nimport MFS from 'memory-fs';\nimport { PassThrough } from 'stream';\n\nimport { serverBundleFileName, clientManifestFileName } from '../../../config/webpack/setting';\nimport clientConfig from '../../../config/webpack/client';\nimport serverConfig from '../../../config/webpack/server';\nimport { createRenderer } from './server-render';\n\nconst mfs = new MFS();\nconst clientManifestFilePath = path.join(clientConfig.output.path, clientManifestFileName);\nconst serverBundleFilePath = path.join(serverConfig.output.path, serverBundleFileName);\nlet expressDevMiddleware;\nlet serverBundleComplete = false;\n\n/**\n * setRenderer\n * whenever client file or server file change, renderer should be update\n */\nconst updateRenderer = () => {\n\tif (!serverBundleComplete) return;\n\ttry {\n\t\tconst options = {\n\t\t\tclientManifest: JSON.parse(expressDevMiddleware.fileSystem.readFileSync(clientManifestFilePath, 'utf-8'))\n\t\t};\n\t\tcreateRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')), options);\n\t} catch (e) {\n\t\tcreateRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')));\n\t}\n};\n\n// watch and update server renderer\nconst serverCompiler = webpack(serverConfig);\nserverCompiler.outputFileSystem = mfs;\nserverCompiler.watch({}, (err, stats) => {\n\tif (err) throw err;\n\tstats = stats.toJson();\n\tstats.errors.forEach(err => console.error(err));\n\tstats.warnings.forEach(err => console.warn(err));\n\tif (!serverBundleComplete) serverBundleComplete = true;\n\tsetImmediate(updateRenderer);\n});\n\nconst koaWebpackDevMiddleware = (compiler, opts) => {\n\texpressDevMiddleware = webpackDevMiddleware(compiler, opts);\n\treturn async (ctx, next) => {\n\t\tawait new Promise(resolve =>\n\t\t\texpressDevMiddleware(ctx.req, {\n\t\t\t\tend: (content) => {\n\t\t\t\t\tctx.body = content;\n\t\t\t\t\tresolve();\n\t\t\t\t},\n\t\t\t\tsetHeader: ctx.set.bind(ctx)\n\t\t\t}, () => resolve(next()))\n\t\t);\n\t};\n};\n\nconst koaWebpackHotMiddleware = (compiler, opts) => {\n\tconst expressMiddleware = webpackHotMiddleware(compiler, opts);\n\treturn async (ctx, next) => {\n\t\tlet stream = new PassThrough();\n\t\tctx.body = stream;\n\t\tawait expressMiddleware(ctx.req, {\n\t\t\twrite: stream.write.bind(stream),\n\t\t\twriteHead: (state, headers) => {\n\t\t\t\tctx.state = state;\n\t\t\t\tctx.set(headers);\n\t\t\t}\n\t\t}, next);\n\t}\n};\n\nconst clientCompiler = webpack(clientConfig);\n\nconst devMiddleware = koaWebpackDevMiddleware(clientCompiler, {\n\t// display no info to console (only warnings and errors)\n\tnoInfo: false,\n\tstats: {\n\t\tcolors: true,\n\t\tcached: false\n\t},\n\tcontentBase: clientConfig.output.path,\n\tpublicPath: clientConfig.output.publicPath\n});\n\nclientCompiler.plugin('done', updateRenderer);\n\nconst hotMiddleware = koaWebpackHotMiddleware(clientCompiler, {});\n\nexport {devMiddleware, hotMiddleware};\n"
  },
  {
    "path": "src/server/publish/index.js",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 05/03/2017\n */\n\nimport path from 'path';\nimport Koa from 'koa';\nimport Router from 'koa-router';\nimport bodyParser from 'co-body';\nimport webPush from 'web-push';\n\nimport { readFile, writeFile } from '../common/DataService';\nimport { gcmAPIKey } from '../config';\n\nconst publishApp = new Koa();\nconst router = new Router();\n\nconst SUBSCRIPTION_FILE = path.resolve(__dirname, '../../../data/subscriptions.txt');\nconst TOKEN_FILE_PATH = path.resolve(__dirname, '../../../data/token.txt');\n\nconst parseSubscriptions = string => string.split('\\n').filter(item => !!item).map(item => JSON.parse(item));\nconst stringifySubscriptions = subscriptions => subscriptions.map(item => JSON.stringify(item)).join('\\n');\n\nconst isVerifyMessage = async message => {\n\tconst token = await readFile(TOKEN_FILE_PATH).then(b => b.toString().replace(/\\s/g, ''));\n\treturn message.token === token;\n};\n\nconst readSubscriptions = () => readFile(SUBSCRIPTION_FILE, { encoding: 'utf8' })\n\t.then(buffer => parseSubscriptions(buffer.toString()))\n\t.catch(err => {\n\t\tconsole.error(err);\n\t\treturn [];\n\t});\n\nconst writeSubscription = subscriptions => writeFile(SUBSCRIPTION_FILE, stringifySubscriptions(subscriptions), { encoding: 'utf8' });\n\nconst addSubscription = subscription =>\n\treadSubscriptions()\n\t\t.then(subscriptions => {\n\t\t\tif (!subscriptions.find(item => item.endpoint === subscription.endpoint)) {\n\t\t\t\tsubscriptions.push(subscription);\n\t\t\t}\n\t\t\treturn subscriptions;\n\t\t})\n\t\t.then(writeSubscription);\n\nconst removeSubscriptions = subs =>\n\treadSubscriptions()\n\t\t.then(subscriptions => subscriptions.filter(subscription => !subs.find(item => item.endpoint === subscription.endpoint)))\n\t\t.then(writeSubscription);\n\nrouter\n\t.post('/subscribe', async ctx => {\n\t\tconst body = await bodyParser(ctx.request);\n\n\t\tawait addSubscription(body)\n\t\t\t.then(() => {\n\t\t\t\tctx.status = 200;\n\t\t\t\tctx.body = {};\n\t\t\t})\n\t\t\t.catch(err => {\n\t\t\t\tctx.status = 500;\n\t\t\t\tctx.body = err;\n\t\t\t});\n\t})\n\t.post('/unsubscribe', async ctx => {\n\t\tconst body = await bodyParser(ctx.request);\n\n\t\tawait removeSubscriptions([body])\n\t\t\t.then(() => {\n\t\t\t\tctx.status = 200;\n\t\t\t\tctx.body = {};\n\t\t\t})\n\t\t\t.catch(err => {\n\t\t\t\tctx.status = 500;\n\t\t\t\tctx.body = err;\n\t\t\t});\n\t})\n\t.post('/broadcast', async ctx => {\n\t\tconst body = await bodyParser(ctx.request);\n\t\tconst isVerified = await isVerifyMessage(body);\n\n\t\tif (isVerified) {\n\t\t\tawait readSubscriptions()\n\t\t\t.then(subscriptions => new Promise(resolve => {\n\t\t\t\tif (!subscriptions || subscriptions.length === 0) resolve([]);\n\t\t\t\tlet i = 0;\n\t\t\t\tconst errorSubscriptions = [];\n\t\t\t\tconst resolveErrorSubsriptions = (len, subscribes) => subscriptions.length === len && resolve(subscribes);\n\n\t\t\t\tsubscriptions.forEach(subscription => {\n\t\t\t\t\twebPush.sendNotification(subscription, JSON.stringify(body), { gcmAPIKey })\n\t\t\t\t\t\t.then(() => resolveErrorSubsriptions(++i, errorSubscriptions))\n\t\t\t\t\t\t.catch(err => {\n\t\t\t\t\t\t\tconsole.error(err);\n\t\t\t\t\t\t\t// retain the subscription, if the error cause by network not access (GREAT WALL)\n\t\t\t\t\t\t\tif (err.code !== 'ETIMEDOUT') errorSubscriptions.push(subscription);\n\n\t\t\t\t\t\t\tresolveErrorSubsriptions(++i, errorSubscriptions);\n\t\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}))\n\t\t\t.then(subscriptions => {\n\t\t\t\tctx.status = 200;\n\t\t\t\tctx.body = subscriptions;\n\t\t\t\tremoveSubscriptions(subscriptions);\n\t\t\t})\n\t\t\t.catch(err => {\n\t\t\t\tctx.status = 500;\n\t\t\t\tctx.body = err;\n\t\t\t});\n\t\t} else {\n\t\t\tctx.status = 400;\n\t\t\tctx.body = 'Buddy, you forgot the password! (:';\n\t\t}\n\t});\n\npublishApp\n\t.use(router.routes())\n\t.use(router.allowedMethods());\n\nexport default publishApp;\n"
  },
  {
    "path": "src/server/queries/PostService.ts",
    "content": "/**\n * Created by jack on 16-4-27.\n */\n\nimport Post from '../../types/post';\nimport Data from '../data';\nimport * as DataService from '../common/DataService';\n\nclass PostService {\n\tpublic posts: { [key: string]: Post };\n\tconstructor() {\n\t\tthis.posts = Data.posts;\n\t}\n\n\tpublic getPostById(id: string) {\n\t\treturn this.posts[id];\n\t}\n\n\tpublic getPostByName(name: string) {\n\t\treturn Object.values(this.posts).filter((post: Post) => post.name === name)[0];\n\t}\n\n\tpublic getPreviousPost(id: number) {\n\t\treturn id > 0 ? this.getPostById(`${id - 1}`) : null;\n\t}\n\n\tpublic getNextPost(id: number) {\n\t\treturn id < Object.keys(this.posts).length - 1 ? this.getPostById(`${id + 1}`) : null;\n\t}\n\n\tpublic queryPostsList({number: pageNumber = 0, size: pageSize = 5} = {}) {\n\t\tconst postsList = Object.values(this.posts).sort(DataService.sortFn('createdTime', -1));\n\t\tconst startIndex = pageNumber * pageSize;\n\t\tconst endIndex = startIndex + pageSize > postsList.length ? postsList.length : startIndex + pageSize;\n\t\treturn postsList.slice(startIndex, endIndex);\n\t}\n\n\tpublic queryPostsListByTagName(tagName = '') {\n\t\treturn Object.values(this.posts).filter((post: Post) => post.tags.indexOf(tagName) > -1);\n\t}\n}\n\nexport default new PostService();\n"
  },
  {
    "path": "src/server/queries/TagService.ts",
    "content": "/**\n * Created by jack on 16-8-22.\n */\n\nimport Tag from '../../types/tag';\nimport Data from '../data';\nimport * as DataService from '../common/DataService';\nimport PostService from './PostService';\n\nclass TagService {\n\tpublic tags: { [key: string]: Tag };\n\tconstructor() {\n\t\tthis.tags = Data.tags;\n\t}\n\n\tpublic getTagByName(name: string) {\n\t\treturn Object.values(this.tags).filter((tag: Tag) => tag.name === name)[0] || {};\n\t}\n\n\tpublic queryTags() {\n\t\treturn Object\n\t\t\t.values(this.tags)\n\t\t\t.sort(DataService.sortFn((tag: Tag) => PostService.queryPostsListByTagName(tag.name).length, -1));\n\t}\n\n\tpublic queryTagsByPostId(postId = 1) {\n\t\treturn PostService.getPostById(`${postId}`).tags.map((name: string) => this.getTagByName(name));\n\t}\n}\n\nexport default new TagService();\n"
  },
  {
    "path": "src/server/server.js",
    "content": "/**\n * Created by jack on 16-4-16.\n */\nimport 'babel-polyfill';\n\nimport path from 'path';\nimport Koa from 'koa';\nimport mount from 'koa-mount';\nimport graphQLHTTP from 'koa-graphql';\nimport convert from 'koa-convert';\nimport serve from 'koa-static';\n\nimport * as middleware from './middleware';\nimport schema from './graphql';\nimport publish from './publish';\n\nconst app = new Koa();\n\nconst PORT = Number.parseInt(process.env.PORT || '8080', 10);\nconst PUBLIC_PATH = path.resolve(__dirname, '../client');\nconst staticServer = serve(PUBLIC_PATH);\n\napp.use(middleware.serverErrorHandler);\napp.use(middleware.pageNotFound);\napp.use(middleware.responseTime);\napp.use(middleware.logger);\n\n// koa graphql\napp.use(mount('/graphql', convert(graphQLHTTP({ schema, pretty: true }))));\n\n// Publish service\napp.use(mount('/publish', publish));\n\nif (process.env.NODE_ENV !== 'production') {\n\t// koa static\n\tapp.use(staticServer);\n\n\tconst devMiddleware = require('./middleware/webpack-middleware').devMiddleware;\n\tconst hotMiddleware = require('./middleware/webpack-middleware').hotMiddleware;\n\n\tapp.use(devMiddleware);\n\tapp.use(hotMiddleware);\n}\n\n// server render\napp.use(middleware.serverRender);\n\napp.on('error', err => {\n\tconsole.log('server error', err);\n});\n\napp.listen(PORT, () => {\n\tconsole.log(`Blog is running, port: ${PORT}`);\n});\n"
  },
  {
    "path": "src/server-entry.js",
    "content": "/**\n * Created by jack on 16-11-27.\n */\n\nimport createApp from './client/app';\nimport { getBlogTitle } from '@/common/service/CommonService';\nimport siteActions from '@/vuex/module/site/actions';\n\n// Add global variables for node environment.\nconst jsdom = require('jsdom').jsdom;\nglobal.document = jsdom('<!doctype html><html><body></body></html>');\nglobal.window = document.defaultView;\nglobal.navigator = window.navigator;\nglobal.fetch = require('node-fetch');\n\nexport default context => {\n\tconst { app, router, store } = createApp();\n\t// set router's location\n\trouter.push(context.url);\n\tconst matchedComponents = router.getMatchedComponents();\n\n\t// no matched routes\n\tif (!matchedComponents.length) {\n\t\treturn Promise.reject({code: '404'});\n\t}\n\n\t// Call preFetch hooks on components matched by the route.\n\t// A preFetch hook dispatches a store action and returns a Promise,\n\t// which is resolved when the action is complete and store state has been\n\t// updated.\n\treturn Promise.all(matchedComponents.map(component => {\n\t\tif (component.options.preFetch) {\n\t\t\treturn component.options.preFetch(store, router);\n\t\t}\n\t}))\n\t\t// special handle nav state load, which can't be added in preFetch hook\n\t\t.then(() => siteActions.loadNavList(store))\n\t\t.then(() => {\n\t\t\tcontext.title = getBlogTitle(store.state.site.title);\n\t\t\t// After all preFetch hooks are resolved, our store is now\n\t\t\t// filled with the state needed to render the app.\n\t\t\t// Expose the state on the render context, and let the request handler\n\t\t\t// inline the state in the HTML response. This allows the client-side\n\t\t\t// store to pick-up the server-side state without having to duplicate\n\t\t\t// the initial data fetching on the client.\n\t\t\tcontext.state = store.state;\n\t\t\treturn app;\n\t\t});\n\n};\n"
  },
  {
    "path": "src/service-worker.js",
    "content": "/**\n * @author Disciple_D\n * @homepage https://github.com/discipled/\n * @since 20/02/2017\n */\n\nconst _self = this;\nconst HOST_NAME = location.host;\nconst VERSION_NAME = 'CACHE-v3';\nconst CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;\nconst CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];\nconst SUBSCRIBE_API = '/publish/subscribe';\n\nconst sentMessage = function(msg) {\n\t_self.clients.matchAll().then(function(clients) {\n\t\tclients.forEach(function(client) {\n\t\t\tclient.postMessage(msg);\n\t\t});\n\t});\n};\n\nconst onInstall = function(event) {\n\tevent.waitUntil(\n\t\tcaches\n\t\t\t.open(CACHE_NAME)\n\t\t\t.then(function() { _self.skipWaiting(); })\n\t\t\t.then(function() { console.log('Install success'); })\n\t);\n};\n\nconst onActive = function(event) {\n\tevent.waitUntil(\n\t\tcaches\n\t\t\t.keys()\n\t\t\t.then(function(cacheNames) {\n\t\t\t\treturn Promise.all(\n\t\t\t\t\tcacheNames.map(function(cacheName) {\n\t\t\t\t\t\t// Remove expired cache response\n\t\t\t\t\t\tif (CACHE_NAME.indexOf(cacheName) === -1) {\n\t\t\t\t\t\t\treturn caches.delete(cacheName);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t})\n\t\t\t.then(function() {\n\t\t\t\t_self.clients.claim();\n\t\t\t})\n\t);\n};\n\nconst onMessage = function(event) {\n\tconsole.log(event.data);\n\n\tevent.ports[0].postMessage('Hi, buddy.');\n};\n\nconst isNeedCache = function(req) {\n\tconst { method, url } = req;\n\treturn method.toUpperCase() === 'GET' && CACHE_HOST.some(function(host) {\n\t\treturn url.search(host) !== -1;\n\t});\n};\n\nconst isCORSRequest = function(url, host) {\n\treturn url.search(host) === -1;\n};\n\nconst isValidResponse = function(response) {\n\treturn response && response.status >= 200 && response.status < 400;\n};\n\nconst handleFetchRequest = function(req) {\n\tif (isNeedCache(req)) {\n\t\tconst request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;\n\t\treturn caches.match(request)\n\t\t\t.then(function(response) {\n\t\t\t\t// Cache hit - return response directly\n\t\t\t\tif (response) {\n\t\t\t\t\t// Update Cache for next time enter\n\t\t\t\t\tfetch(request)\n\t\t\t\t\t\t.then(function(response) {\n\n\t\t\t\t\t\t\t// Check a valid response\n\t\t\t\t\t\t\tif (isValidResponse(response)) {\n\t\t\t\t\t\t\t\tcaches\n\t\t\t\t\t\t\t\t\t.open(CACHE_NAME)\n\t\t\t\t\t\t\t\t\t.then(function(cache) {\n\t\t\t\t\t\t\t\t\t\tcache.put(request, response);\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsentMessage('Update cache ' + request.url + ' fail: ' + response.message);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(function(err) {\n\t\t\t\t\t\t\tsentMessage('Update cache ' + request.url + ' fail: ' + err.message);\n\t\t\t\t\t\t});\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Return fetch response\n\t\t\t\treturn fetch(request)\n\t\t\t\t\t.then(function(response) {\n\t\t\t\t\t\t// Check if we received an valid response\n\t\t\t\t\t\tif (isValidResponse(response)) {\n\t\t\t\t\t\t\tconst clonedResponse = response.clone();\n\n\t\t\t\t\t\t\tcaches\n\t\t\t\t\t\t\t\t.open(CACHE_NAME)\n\t\t\t\t\t\t\t\t.then(function(cache) {\n\t\t\t\t\t\t\t\t\tcache.put(request, clonedResponse);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn response;\n\t\t\t\t\t});\n\t\t\t});\n\t} else {\n\t\treturn fetch(req);\n\t}\n};\n\nconst onFetch = function(event) {\n\tevent.respondWith(handleFetchRequest(event.request));\n};\n\nconst onPush = function(event) {\n\tconst payload = event.data ? event.data.text() : '{}';\n\tconst { body, link } = JSON.parse(payload);\n\n\tevent.waitUntil(_self.registration.showNotification('New Post Arrival', {\n\t\tbody,\n\t\tdata: link,\n\t\ticon: '/assets/img/logo/size-48.png'\n\t}));\n};\n\nconst encodeStr = str => btoa(String.fromCharCode.apply(null, new Uint8Array(str)));\nconst getEncodeSubscriptionInfo = (subscription, type) => subscription.getKey ? encodeStr(subscription.getKey(type)) : '';\nconst onPushSubscriptionChange = function(event) {\n\tevent.waitUntil(\n\t\t_self.registration.pushManager.subscribe({ userVisibleOnly: true })\n\t\t\t.then(function(subscription) {\n\t\t\t\tconst endpoint = subscription.endpoint;\n\t\t\t\tconst p256dh = getEncodeSubscriptionInfo(subscription, 'p256dh');\n\t\t\t\tconst auth = getEncodeSubscriptionInfo(subscription, 'auth');\n\n\t\t\t\tconst clientSubscription = { endpoint, keys: { p256dh, auth } };\n\n\t\t\t\tconst options = {\n\t\t\t\t\tmethod: 'post',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(clientSubscription)\n\t\t\t\t};\n\n\t\t\t\treturn fetch(SUBSCRIBE_API, options);\n\t\t\t})\n\t);\n};\n\nconst onNotificationClick = function(event) {\n\tevent.notification.close();\n\n\tevent.waitUntil(clients.openWindow(event.notification.data));\n};\n\n_self.addEventListener('install', onInstall);\n\n_self.addEventListener('activate', onActive);\n\n_self.addEventListener('message', onMessage);\n\n_self.addEventListener('fetch', onFetch);\n\n_self.addEventListener('push', onPush);\n\n_self.addEventListener('notificationclick', onNotificationClick);\n\n_self.addEventListener('pushsubscriptionchange', onPushSubscriptionChange);\n"
  },
  {
    "path": "src/types/graphql-request.d.ts",
    "content": "interface GraphQLResponseError {\n  message: string\n}\n\ninterface GraphQLResponse<T> {\n  data?: T,\n  error?: GraphQLResponseError[]\n}\n"
  },
  {
    "path": "src/types/koa.d.ts",
    "content": "declare module 'web-push' {\n  export var sendNotification: (subscription: SubscriptionRecord, data: any, options?: any) => Promise<any>\n}\n"
  },
  {
    "path": "src/types/nav.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\nconst noon = () => { };\nexport class Item {\n\tpublic name: string;\n\tpublic title: string;\n\tpublic path: string;\n\tpublic event?: () => void;\n\n\tconstructor(name = '', title = '', path = '/', event = noon) {\n\t\tthis.name = name;\n\t\tthis.title = title;\n\t\tthis.path = path;\n\t\tthis.event = event;\n\t}\n}\n"
  },
  {
    "path": "src/types/page.ts",
    "content": "/**\n * Created by d.d on 25/07/2017.\n */\nexport interface ITitle {\n\timage: string;\n\ttitle: string;\n\tsubtitle?: string;\n}\n"
  },
  {
    "path": "src/types/pager.ts",
    "content": "export interface IPager {\n\tnum: number;\n\tsize: number;\n}\n"
  },
  {
    "path": "src/types/post.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\nexport interface IPostBase {\n\tname: string;\n\ttitle: string;\n\tsubtitle?: string;\n\tcreatedTime: string;\n\theaderImageType?: string;\n\ttags: string[];\n}\n\nexport interface IPost extends IPostBase {\n\tid: number;\n\tcontent: string;\n}\n\nexport default class Post {\n\tpublic id: number;\n\tpublic name: string;\n\tpublic title: string;\n\tpublic content: string;\n\tpublic subtitle?: string;\n\tpublic createdTime: string;\n\tpublic headerImgName: string;\n\tpublic tags: string[];\n\tconstructor({\n\t\tid = -1,\n\t\tname = '',\n\t\ttitle = '',\n\t\tcontent = '',\n\t\tsubtitle = '',\n\t\tcreatedTime = '',\n\t\theaderImageType = '.jpg',\n\t\ttags = [] }: IPost) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.title = title;\n\t\tthis.subtitle = subtitle;\n\t\tthis.createdTime = createdTime;\n\t\tthis.content = content;\n\t\tthis.headerImgName = createdTime + headerImageType;\n\t\tthis.tags = tags;\n\t}\n}\n\nexport interface IPostShort {\n\tname: string;\n\ttitle: string;\n}\n\nexport interface IPostPage extends Post {\n\tprevPost?: IPostShort;\n\tnextPost?: IPostShort;\n}\n"
  },
  {
    "path": "src/types/pwa.d.ts",
    "content": "interface ShareInfo {\n\ttitle: string,\n\turl?: string,\n\ttext?: string\n}\n\ninterface Navigator {\n\treadonly share: (o: ShareInfo) => Promise<void>\n}\n\ninterface SubscriptionRecord {\n\tendpoint: string;\n\tkey: {\n\t\tp256dh: string,\n\t\tauth: string,\n\t};\n}\n"
  },
  {
    "path": "src/types/support-loader.d.ts",
    "content": "/**\n * Created by d.d on 18/07/2017.\n */\n\ndeclare module \"*.json\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.html\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.md\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.css\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.scss\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.jpg\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.jpeg\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.png\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.gif\" {\n    const value: any;\n    export default value;\n}\n\ndeclare module \"*.svg\" {\n    const value: any;\n    export default value;\n}\n"
  },
  {
    "path": "src/types/tag.ts",
    "content": "import { IPostShort } from './post';\n\nexport interface ITagShort {\n\tname: string;\n\tlabel: string;\n}\n\nexport interface ITagBase {\n\tname: string;\n\tcreatedTime: string;\n\tlabel: string;\n}\n\nexport default class Tag implements ITagBase {\n\tpublic id: number;\n\tpublic name: string;\n\tpublic createdTime: string;\n\tpublic label: string;\n\tconstructor({ id = -1, name = '', label = '', createdTime = '' } = {}) {\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.label = label;\n\t\tthis.createdTime = createdTime;\n\t}\n}\n\nexport interface ITagPage extends Tag {\n\tposts: IPostShort[];\n}\n"
  },
  {
    "path": "src/types/vue.d.ts",
    "content": "import Vue from 'vue';\nimport { Store } from 'vuex';\nimport VueRouter from 'vue-router';\n\nimport { IRootState } from '@/vuex/module/index';\n\ndeclare global {\n  interface Window {\n    __INITIAL_STATE__: any\n  }\n}\n\ndeclare module 'vue/types/options' {\n  interface ComponentOptions<V extends Vue> {\n    preFetch?: (store: Store<IRootState>, router?: VueRouter) => Promise<any>\n  }\n}\n"
  },
  {
    "path": "tsconfig-server.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \"./src\",\n    \"paths\": {\n      \"@/*\": [\"client/*\"]\n    },\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"removeComments\": true,\n    \"preserveConstEnums\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"es5\",\n    \"outDir\": \"./build\",\n    \"lib\": [\"dom\", \"es5\", \"es2015\", \"es2017.object\", \"esnext.asynciterable\"]\n  },\n  \"include\": [\"src/server/**/*.ts\", \"src/types/*\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \"./src\",\n    \"paths\": {\n      \"@/*\": [\"client/*\"]\n    },\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"removeComments\": true,\n    \"preserveConstEnums\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"es5\", \"es2015\", \"es2017.object\", \"esnext.asynciterable\"]\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"src/server/**/*.ts\"]\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"extends\": \"tslint:recommended\",\n  \"rules\": {\n    \"curly\": [true, \"ignore-same-line\"],\n    \"quotemark\": [true, \"single\", \"jsx-double\"],\n    \"indent\": [true, \"tabs\", 2],\n    \"ordered-imports\": [false],\n    \"object-literal-sort-keys\": false,\n    \"max-classes-per-file\": [true, 2],\n    \"no-console\": [false],\n    \"no-empty\": false,\n    \"no-unused-expression\": false,\n    \"no-var-requires\": false\n  }\n}\n"
  }
]