JackySummer

JackySummer 查看完整档案

香港岛编辑  |  填写毕业院校  |  填写所在公司/组织 jacky-summer.github.io/ 编辑
编辑

微信公众号【前端精神时光屋】

个人动态

JackySummer 发布了文章 · 1月3日

ES6 系列之 Proxy

本文同步发表在 Github 个人博客:ES6 系列之 Proxy

前言

前几天模拟实现了 MobX 的两个函数 —— 手写实现 MobX 的 observable 和 autorun 方法,其中用到了 Proxy,所以打算再对 Proxy 深入了解一下,做个笔记。

Proxy 是什么

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

const p = new Proxy(target, handler)
  • target: 使用 Proxy 包装的目标对象(可以是任何类型的 JavaScript 对象,包括原生数组,函数,甚至另一个代理)。
  • handler: 一个通常以函数作为属性的对象,用来定制拦截行为。

在支持 Proxy 的浏览器环境中,Proxy 是一个全局对象,可以直接使用。Proxy(target, handler)是一个构造函数,target是被代理的对象,最终返回一个代理对象。

为什么需要 Proxy

学习一样东西之前我们先要想想为什么需要它,在我看来,一般几种情况。

  1. 被代理的对象不想直接被访问
  2. 控制和修改被代理对象的行为(调用属性、属性赋值、方法调用等等),使之可以进行访问控制和增加功能。

API

API 概览如下:

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy['foo'] 。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v ,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、 Object.keys(proxy) 、 for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc) 、
  • Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截 Object.preventExtensions(proxy) ,返回一个布尔值。
  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象 。
  • isExtensible(target):拦截 Object.isExtensible(proxy) ,返回一个布尔值。
  • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)`。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args) 。

最常用的方法就是getset

例子

get

const target = {
  name: 'jacky',
  sex: 'man',
}
const handler = {
  get(target, key) {
    console.log('获取名字/性别')
    return Reflect.get(target, key)
  },
}
const proxy = new Proxy(target, handler)
console.log(proxy.name)

运行,打印台输出:

获取名字/性别
jacky

在获取name属性是先进入get方法,在get方法里面打印了获取名字/性别,然后通过Reflect.get(target, key)的返回值拿到属性值,相当于target[key]

set

const target = {
  name: 'jacky',
  sex: 'man',
}
const handler = {
  get(target, key) {
    console.log('获取名字/性别')
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    return Reflect.set(target, key, `强行设置为 ${value}`)
  },
}
const proxy = new Proxy(target, handler)
proxy.name = 'monkey'
console.log(proxy.name)

运行输出:

获取名字/性别
强行设置 monkey

设置proxy.name = 'monkey',这是修改属性的值,则会触发到set方法, 然后我们强行更改设置的值再返回,达到拦截对象属性的设置的目的。


Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。
Reflect.get(target, name, receiver) :查找并返回target对象的name属性,如果没有该属性,则返回undefined。
Reflect.set(target, name, value, receiver) :设置target对象的name属性等于value。

this 指向

proxy 会改变 target 中的 this 指向,一旦 Proxy 代理了 target,target 内部的 this 则指向了 Proxy 代理

const target = new Date('2021-01-03')
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate(target)
    }
    return Reflect.get(target, prop)
  },
}
const proxy = new Proxy(target, handler)

console.log(proxy.getDate())

运行代码,会发现报错,提示TypeError: proxy.getDate is not a function,即 this 不是 Date 对象的实例,这时需要我们手动绑定原始对象即可解决:

const target = new Date('2021-01-03')
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target) // 绑定
    }
    return Reflect.get(target, prop)
  },
}
const proxy = new Proxy(target, handler)

console.log(proxy.getDate()) // 3

应用场景

  • 警告或阻止特定操作
  • get 方法取不到对应值可以返回我们想指定的其它值
  • 数据校验。判断数据是否满足条件

等等...

参考


ps:

觉得不错的话赏个 star,给我持续创作的动力吧!

查看原文

赞 0 收藏 0 评论 0

JackySummer 发布了文章 · 2020-12-25

搭建 Next.js + TS + Antd + Redux + Storybook 企业级项目脚手架

前言

Nextjs-TS-Antd-Redux-Storybook-Jest-Starter

之所以有该项目呢,是因为日常可能自己需要练手其他 Next.js 项目,又不想每次都重新配置一遍,但基于强迫症正常企业级项目该有的配置觉得不能少了,于是就想开搞一个通用脚手架模板。

说起 Next.js,8 月份写了一篇文章手把手带你入门 NextJs(v9.5),主要是因为网上大部分 Next.js 是旧版本 v7.x 的教程,于是写个较新的 9.5 版,没想到 10 月就出了 Next.js 10,措手不及,不过更新部分主要是图片优化等,可以照样看。

该项目也是想把日常工作中我觉得实践比较好的点加进来,也打算根据该项目持续跟进良好规范和最新库版本。当然,到具体业务场景的话脚手架肯定多少需要改,但目标希望能降低修改的成本,起码基本配置得搞好。

该脚手架主要库和版本:

Next.js 10.x
React 17.x
TypeScript 4.x
Ant Design 4.x
Styled-components 5.x
Storybook 6.x

初始化 Next.js 模板

npx create-next-app nextjs-ts-redux-antd-starter

添加 TypeScript 支持

根目录下新建tsconfig.json文件,此时运行yarn dev,会看到它提示我们安装类型库

yarn add --dev typescript @types/react @types/node

顺便把@types/react-dom也装上

安装之后,再运行yarn dev, 会在根目录自动生成next-env.d.ts文件,且tsconfig.json有了默认配置,这里我再对配置稍加改动。

具体可以参考 TS 官网看配置项

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "src/*": [
        "src/*"
      ]
    },
    "target": "es5",
    "module": "esnext",
    "strict": true,
    "allowJs": true, // 允许编译js文件
    "jsx": "preserve", // 在 .tsx文件里支持JSX
    "noEmit": true, // 不输出文件
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], // TS需要引用的库,即声明文件
    "esModuleInterop": true, // 允许export=导出,由import from导入
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入
    "isolatedModules": true, // 将每个文件作为单独的模块
    "resolveJsonModule": true, // 允许把json文件当做模块进行解析
    "skipLibCheck": true, // 跳过所有声明文件的类型检查
    "forceConsistentCasingInFileNames": true // 不允许对同一文件使用不一致大小写的引用
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules",
    ".next",
    "dist"
  ]
}

然后清除干净目录,把styles, pages只留下一个index.js即可, 并将index.js重命名为index.tsx

import { NextPage } from 'next'

const Home: NextPage = () => {
  return <div>Hello nextjs-ts-redux-antd-starter</div>
}

export default Home

EditorConfig

作为项目代码风格的统一规范,我们需要借助第三方工具来强制

.editorconfig 是跨编辑器维护一致编码风格的配置文件,在 VSCode 中需要安装相应插件 EditorConfig for VS Code,安装完毕之后, 可以通过输入 Generate .editorcofig 即可快速生成 .editorconfig 文件,也可以自己新建文件。

.editorcofig文件,就可以大家根据不同来设置文件了,比如我的是这样:

# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

Prettier

yarn add prettier -D

同样也需要安装 VSCode 插件Prettier - Code formatter

新建文件.prettierrc

{
  "singleQuote": true,
  "tabWidth": 2,
  "endOfLine": "lf",
  "trailingComma": "all",
  "printWidth": 100,
  "arrowParens": "avoid",
  "semi": false,
  "bracketSpacing": true,
  "overrides": [
    {
      "files": ".prettierrc",
      "options": { "parser": "json" }
    }
  ]
}

再添加忽略文件.prettierignore

**/*.png
**/*.svg
**/*.ico
package.json
lib/
es/
dist/
.next/
coverage/
LICENSE
yarn.lock
yarn-error.log
*.sh
.gitignore
.npmignore
.prettierignore
.DS_Store
.editorconfig
.eslintignore
**/*.yml

ESLint

yarn add eslint -D

安装完后运行 npx eslint --init,运行后有选项,选择如下(自行根据需要):

  • To check syntax, find problems, and enforce code style
  • JavaScript modules (import/export)
  • React
  • TypeScript Yes
  • Browser Node
  • Use a popular style guide
  • Airbnb: https://github.com/airbnb/jav...
  • JavaScript
  • Would you like to install them now with npm (Yes)

npm 安装后会出现package-lock.json,如果你默认想用yarn.lock,为了避免冲突就删掉它。

安装自动生成.eslintrc文件,还没完,为了写出来的代码更好更符合社区规范,我们再加一些不错的 eslint 插件

  • eslint-plugin-unicorn:提供了循环依赖检测,文件名大小写风格约束等非常实用的规则集合。
  • eslint-config-prettier:eslint 和 prettier 混合使用时候,需要修改规则,以防止重复或冲突;该插件即为解决此问题的存在,可以使用它关闭所有可能引起冲突的规则。
  • eslint-plugin-import:能够正确解析 .tsx, .ts, .js, .json 后缀名(还需指定允许的后缀名,添加到 setttings 字段)
  • eslint-import-resolver-alias: eslint 能识别 alias 别名自定义路径
  • eslint-import-resolver-typescript:让 eslint-plugin-import 能够正确解析 tsconfig.json 中的 paths 映射,需要安装它。

我的配置如下,rules忽略规则自己添加因人而异

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'airbnb',
    'plugin:react/recommended',
    'plugin:import/typescript',
    'plugin:@typescript-eslint/recommended',
    'prettier/react',
  ],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.tsx', '.ts', '.js', '.json'],
      },
      alias: [['src', './src']],
    },
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint', 'react-hooks', 'unicorn'],
  rules: {
    semi: 0,
    indent: 0,
    'react/jsx-filename-extension': 0,
    'react/prop-types': 0,
    'react/jsx-props-no-spreading': 0,

    'jsx-a11y/click-events-have-key-events': 0,
    'jsx-a11y/no-static-element-interactions': 0,
    'jsx-a11y/no-noninteractive-element-interactions': 0,

    'no-use-before-define': 0,
    'no-unused-vars': 0,
    'implicit-arrow-linebreak': 0,
    'consistent-return': 0,
    'arrow-parens': 0,
    'object-curly-newline': 0,
    'operator-linebreak': 0,
    'import/no-extraneous-dependencies': 0,
    'import/extensions': 0,
    'import/no-unresolved': 0,
    'import/prefer-default-export': 0,
  },
}

新建文件.eslintignore,忽略一些文件的检查

/node_modules
/public
/dist
/.next
/coverage

StyleLint

sass/less/css

  • eslint-config-prettier: 利用插件禁用与 Prettier 起冲突的规则
  • stylelint-config-rational-order: 对关联属性进行分组和排序
  • stylelint-declaration-block-no-ignored-properties: 矛盾样式忽略
  • stylelint-order:强制你按照某个顺序编写 css

.stylelintrc

{
  extends: [
    'stylelint-config-standard',
    'stylelint-config-rational-order',
    'stylelint-config-prettier',
  ],
  plugins: ['stylelint-order', 'stylelint-declaration-block-no-ignored-properties'],
}

styled-components

以上是使用 sass 或 less 可以完全照搬配置的,至于该脚手架我决定采用的 CSS 方案为styled-components,stylelint 配置 styled-components 目前有关库尚未实现自动修复,所以--fix目前是无效的,且需要安装另外的 stylelint 规则插件

yarn add styled-components
yarn add -D @types/styled-components stylelint-processor-styled-components stylelint-config-styled-components

.stylelintrc

{
  "processors": [
    "stylelint-processor-styled-components"
  ],
  "plugins": [
    "stylelint-order"
  ],
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-styled-components"
  ]
}

再新建文件babel.config.js

{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

你可以分development,test,production,对 styled-components 进行区分设置,比如babel.config.js

新建文件pages/_document.tsx,来自定义 Document 的方式来改写代码。它只有在服务器端渲染的时候才会被调用,主要用来修改服务器端渲染的文档内容,一般用来配合第三方 css-in-js 方案使用。

import React from 'react'
import Document, { DocumentContext } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage
    try {
      const initialProps = await Document.getInitialProps(ctx)

      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        })

      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}

export default MyDocument

.vscode

在根目录下新建文件夹.vscode,在该文件夹下新建文件 settings.json,该文件的配置优先于你自己 VSCode 全局的配置,不会因为团队不同成员的 VSCode 全局配置不同而导致格式化不同。

settings.json

{
  "search.exclude": {
    "**/node_modules": true,
    "dist": true,
    ".next": true,
    "yarn.lock": true
  },
  "editor.formatOnSave": true,
  "editor.tabSize": 2,
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  }
}

husky 与 lint-staged

每次提交代码都要对代码先进行 lint 和格式化,确保代码风格统一。于是我们安装husky来解决这个事情,可我们想每次 lint 格式化的时候,只处理我们修改的代码(暂存区),可以选择lint-staged

yarn add -D husky lint-staged

package.json配置 git commit 钩子操作:

 "husky": {
    "hooks": {
      "commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS",
      "pre-commit": "lint-staged && yarn tsc"
    }
  },
  "lint-staged": {
    "*.{tsx,ts,js,jsx}": [
      "stylelint",
      "prettier --write",
      "eslint --fix"
    ],
    "*.{css,less,scss}": [
      "stylelint",
      "prettier --write"
    ],
    "*.{md,json,yaml,yml}": [
      "prettier --write"
    ]
  },

prettier --write中的--write表示将格式化后的代码写到源文件,不加的话会输出文件。

上面"pre-commit": "lint-staged && yarn tsc"我还加了yarn tsc,ts 检查类型有问题,那当然不给你提交,及早发现错误。

另外需不需要强制--fix看个人,因为有人会顾虑强制的话相当于黑盒,你不知道它对你代码做了什么。

commitlint

我们提交的前文件已经会自动格式化了,接下来要搞搞 commit 提交规范问题。

yarn add @commitlint/cli @commitlint/config-conventional -D

默认类型 git commit 类型有如下几种,这是 angular 风格的 commitlint 配置,我自己平时习惯这一套规则。

[
  'build',
  'ci',
  'chore',
  'docs',
  'feat',
  'fix',
  'perf',
  'refactor',
  'revert',
  'style',
  'test'
];

在根目录新建.commitlintrc.js

module.exports = {
  extends: ['@commitlint/config-conventional'],
}

那如果团队刚来的人没用过也记不住上面那些开头单词怎么办,于是我们弄个命令可以让他自己选择,安装插件

  • cz-conventional-changelog:是一个适配器,一个符合 Angular 团队规范的 preset
yarn add cz-conventional-changelog -D

package.json中配置

{
    "scripts": {
        "commit": "git-cz"
    },
    "config": {
        "commitizen": {
          "path": "node_modules/cz-conventional-changelog"
        }
    }
}

运行yarn commit,即出现该页面,供我们选择

Redux

基本项目规范配置就差不多了,接下来是做项目的状态管理工具,我这里选择了最经典的 Redux,异步处理选择redux-saga

yarn add redux react-redux redux-saga
yarn add -D @types/react-redux @types/redux-saga redux-devtools-extension next-redux-wrapper

社区还有其他 redux 简化方案,比如使用 redux-actions,但该项目维护似乎出现困难,就不加入使用了;还有 dva 等等或者采用其他状态管理库例如 mobx,各位可以自行考虑替换,这里只是给个常用方案。

src/redux/index.ts

import { createWrapper, MakeStore } from 'next-redux-wrapper'
import { applyMiddleware, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'

import rootReducer from 'src/redux/rootReducers'
import rootSaga from 'src/redux/rootSagas'

const makeStore: MakeStore<Store.RootState> = () => {
  const sagaMiddleware = createSagaMiddleware()
  const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))
  sagaMiddleware.run(rootSaga)
  return store
}

export const wrapper = createWrapper < Store.RootState > makeStore

再新建文件pages/_app.tsx引入 redux,覆盖 Next.js 默认的 App

import React, { FC } from 'react'
import { AppProps } from 'next/app'
import { wrapper } from 'src/redux'
import Layout from 'src/components/Layout'

const WrappedApp: FC<AppProps> = ({ Component, pageProps }) => (
  <Layout>
    <Component {...pageProps} />
  </Layout>
)

export default wrapper.withRedux(WrappedApp)

其他代码及例子演示请看源代码

redux 的项目结构有几种,哪种好视乎项目大小和复杂程度选择,该脚手架只是展示一种,按模块来划分 redux 数据结构,并不是说此种方式有多好,具体还是依据项目来调整目录结构。

我做个小 demo 是 saga 请求用户数据,返回并展示在页面上,关于 redux State的类型定义,我放在了根目录types文件夹里。

当然这也只是一种参照方式,也可以在 redux 目录模块里新建types文件放置State类型定义。

Ant Desgin 支持

yarn add antd
yarn add -D babel-plugin-import

babel.config.js

module.exports = {
  plugins: [
    [
      'import',
      {
        libraryName: 'antd',
        libraryDirectory: 'lib',
        style: 'index.css',
      },
    ],
  ],
}

src/_app.tsx中引入 antd 样式:

import 'antd/dist/antd.css'

Travis 自动化部署

默认先这样设置了(后面加了 Jest 后再加入脚本yarn test)。不解释,不懂的看我这篇文章 手把手带你入门 Travis 自动化部署

language: node_js

node_js:
  - "stable"

cache: yarn

install:
  - yarn
script:
  - yarn build

Storybook 搭建组件文档

Storybook 是在开发模式下与应用程序一起运行的. 它可以帮助您构建 UI 组件,并与应用程序的业务逻辑和上下文隔离开来
npx -p @storybook/cli sb init

安装完毕,运行即开启

yarn storybook

然后会有初始一些组件例子,看看就可以删了。

How to write stories 通过给组件写 stories,可以让我们对整个项目用到的组件有大致了解,比如长什么样等等,还有包括是否 UI 改变,下面会写。

国际化

yarn add react-i18next i18next

当然也可以直接使用 next-i18next

src/i18n/index.ts

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import zhCN from './locales/zh_CN.json'
import enUS from './locales/en_US.json'

i18n.use(initReactI18next).init({
  lng: 'zh_CN',
  fallbackLng: 'zh_CN',
  resources: {
    zh_CN: {
      translation: zhCN,
    },
    en_US: {
      translation: enUS,
    },
  },
  debug: false,
  interpolation: {
    escapeValue: false,
  },
})

export default i18n

具体还是直接看代码了,这里就介绍这么多;然后就可以切换语言,把项目用到的一些词语句子都集中到zh_CN.jsonen_US.json等等写。

Jest 单元测试

为了代码的健壮性,当然是加入单元测试。如果不懂单元测试,请先看我这篇 一文带你了解 Jest 单元测试

yarn add -D jest @types/jest eslint-plugin-jest babel-jest @storybook/addon-storyshots

配置.eslintrc.js

module.exports = {
  extends: ['plugin:jest/recommended'],
  plugins: ['jest'],
}

在根目录下新建文件jest.config.js

module.exports = {
  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
  testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/node_modules/', '<rootDir>/.next/'],
  moduleNameMapper: {
    '^src(.*)$': '<rootDir>/src$1',
    '^server(.*)$': '<rootDir>/server$1',
    '^pages(.*)$': '<rootDir>/pages$1',
  },
  collectCoverageFrom: [
    './{src,server}/**/*.{ts,tsx,js,jsx}',
    '!**/node_modules/**',
    '!**/dist/**',
    '!**/coverage/**',
    '!**/*.stories.{ts,tsx,js,jsx}',
    '!**/{config,constants,styles,types,__fixtures__}/**',
  ],
  watchPathIgnorePatterns: ['dist'],
}

Storybook 和 Jest 的 Snapshots 结合

Jest 可以生成快照测试(Snapshot),通过 snapshot 变化给我们判断页面元素是否异常,缺失或增加或配置文件是否更改等等。上面安装了 storybook,如果是 react 组件的 snapshot,需要借助其他插件,这里我们转为依靠 storybook 的 stories 生成。

针对 react,Jest 将为虚拟 DOM 拍摄快照,将其转化为 json 数据,在下一次运行时比对两张快照是否有偏差。

yarn add -D @storybook/addon-storyshots

在根目录新建jest.config.js,针对 snapshot 的配置如下,其它配置按项目配置了,参考 jest.config.js

module.exports = {
  transform: {
    '^.+\\.stories\\.[tj]sx?$': '@storybook/addon-storyshots/injectFileName',
    '^.+\\.[tj]sx?$': 'babel-jest',
  },
}

新建文件src/__tests__/storyshot.test.ts

import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'

initStoryshots({
  test: multiSnapshotWithOptions(),
})

之后组件中有写stories的地方,使用yarn jest,除了运行测试,也会自动为*.stories.tsx比对/生成 snapshot。

对于生成的 snapshot 你会看到

比如我写了Footer组件的,只有 HTML 标签和对应属性,这样检测还不够,因为不知道 css 类的属性做了什么改变,由于我用的 css 方案是styled-components,所以需要再进行配置:

yarn add -D jest-specific-snapshot jest-styled-components

配置src/__tests__/storyshot.test.ts

import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'
import 'jest-styled-components'
import { addSerializer } from 'jest-specific-snapshot'
import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'

addSerializer(styleSheetSerializer)

initStoryshots({
  test: multiSnapshotWithOptions(),
})

再来看现在的 snapshot,已经有了一堆样式可以参考对比了,这样细微组件样式修改都可以被捕捉到了;

Enzyme

Jest 可以对函数,类等等有充足的 API 来测试,但对于 React 组件,想详细进行测试,则需要安装其他插件来支持,如react-test-libraryenzyme,这里我就选我用过相对多一点的 enzyme (出自 Airbnb 公司),同时需要安装它的适配器。

这里由于 React 已经升级到 17 版本了,但是 enzyme 官方适配器还没有升级到对应 17 版本的,有些测试方法可能会报错,所以暂时使用目前 Github 使用较多的代替版本的这个库@wojtekmaj/enzyme-adapter-react-17,等官方更新了再替换。这个只是供测试用,不会影响到线上环境,只要 enzyme 自带所有方法能按预期运行不报错就行,这样就能好好写我们的测试用例了。

yarn add enzyme @wojtekmaj/enzyme-adapter-react-17 -D

在根目录新建文件jest.setup.ts

import Enzyme from 'enzyme'
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'

Enzyme.configure({ adapter: new Adapter() })

同时在jest.config.js中导入

module.exports = {
  setupFiles: ['<rootDir>/jest.setup.ts'],
}

通常情况,测试 React 组件是意义不大的,比较需要测试的就是比如 UI 组件,用得较多的通用组件,还有一些组件如一改动全身的容易有 bug 行为的来针对测试。

生成 CHANGELOG 和自动化版本管理

这里我使用standard-version

yarn add -D standard-version
standard-version 是一款遵循语义化版本( semver)和 commit message 标准规范 的版本和 changlog 自动化工具。
"bump-version": "standard-version --skip.commit --skip.tag"

运行yarn bump-version后,会发现 package.json 的版本号变了(前提你有了 feat 或 fix 等等 commit 的改动),还有自动生成 CAHNGEALOG.md,这些都可以自定义配置

Github 打 tag 版本

点击 Github 项目页面右边创建 release

然后填入版本号,详细信息我把 CHANGELOG.md 的内容直接搬过来,然后按Publish release就可以了。

完善 script 命令

package.json

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "commit": "git-cz",
  "test": "jest",
  "coverage": "yarn jest --coverage",
  "lint": "yarn lint:eslint && yarn lint:css",
  "lint:eslint": "eslint --ext js,jsx,ts,tsx .",
  "lint:css": "stylelint **/*.{ts,tsx}",
  "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
  "tsc:client": "tsc --noEmit -p tsconfig.json",
  "storybook": "start-storybook -p 6006",
  "build-storybook": "build-storybook -o ./dist_storybook",
  "bump-version": "standard-version --skip.commit --skip.tag"
},

LICENSE

添加个开源协议,我选择宽松的 MIT 协议

MIT License

Copyright (c) 2020 nextjs-ts-redux-antd-starter

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

运行项目

如上演示还略过一些细节其他配置,需要详细的就看源码吧。

整个脚手架我是不打算加入太多东西的,如下图所示,毕竟做为模板脚手架,加太多功能反而要用的时候要删除一大堆麻烦,因为做的不是某类型业务网站,有一些只能尽量有个 Demo 参考就行。所以我会尽量保持简洁,之后维护我会倾向于 Next.js 配置和前端工程化及性能优化的角度进行完善,然后就是一些通用的函数和功能。

结语

脚手架到这里就完了?还没有,还有很多没加,比如整理 Next.js 的 config 配置,优化 SEO,发布到线上网站和 npm,一些兼容,特殊页面处理,响应式等等。

当然,在我写这篇文章时的脚手架多少也有写的不好或不完善的地方,因为刚起步,所以该脚手架会持续维护,把工作实践和学习到的最佳实践运行到该项目里,不断保持更新,敬请关注,欢迎 star 🌟🌟🌟 https://github.com/Jacky-Summer/nextjs-ts-antd-redux-storybook-starter


觉得不错的话赏个 star,给我持续创作的动力吧!下次继续...

查看原文

赞 0 收藏 0 评论 2

JackySummer 发布了文章 · 2020-12-09

回顾 HTTP1.0 HTTP1.1 HTTP2.0 的区别

HTTP1.0 和 HTTP1.1 的一些区别

缓存处理

在 HTTP1.0 中主要使用 header 里的 If-Modified-Since(比较资源最后的更新时间是否一致),Expires(资源的过期时间(取决于客户端本地时间)) 来做为缓存判断的标准。

HTTP1.1 则引入了更多的缓存控制策略:

  • Entity tag:资源的匹配信息
  • If-Unmodified-Since:比较资源最后的更新时间是否不一致
  • If-Match:比较 ETag 是否一致
  • If-None-Match:比较 ETag 是否不一致

等更多可供选择的缓存头来控制缓存策略。

带宽优化

HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。

HTTP 1.1默认支持断点续传。

Host 头处理

在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。

长连接

HTTP1.0 需要使用keep-alive参数来告知服务器端要建立一个长连接,而 HTTP1.1 默认支持长连接,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。

HTTP 是基于 TCP/IP 协议的,创建一个 TCP 连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接,可以用个长连接来发多个请求。

HTTP1.1 支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。

错误通知的管理

在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

新增请求方式

  • PUT:请求服务器存储一个资源
  • DELETE:请求服务器删除标识的资源
  • OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求
  • CONNECT:保留请求以供将来使用
  • TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断

HTTP2.0 与 HTTP1.X 的区别

HTTP1.X 版本的缺陷概括来说是:线程阻塞,在同一时间,同一域名的请求有一定的数量限制,超过限制数目的请求会被阻塞。

二进制分帧

HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮。

HTTP2.0 在 应用层(HTTP2.0)和传输层(TCP/UDP)之间增加一个二进制分帧层。在不改动 HTTP1.X 的语义、方法、状态码、URI 以及首部字段的情况下, 解决了 HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层中,HTTP2.0 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.X 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。

  • 帧:HTTP2.0 数据通信的最小单位消息:指 HTTP2.0 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。
  • 流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数 ID。

多路复用(MultiPlexing)

多路复用允许同时通过单一的 HTTP2.0 连接发起多重的请求-响应消息。即是连接共享,提高了连接的利用率,降低延迟。即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面。

在 HTTP1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源 CDN 域名的原因之一。

当然 HTTP1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建 TCP 连接本身也是有开销的。

TCP 连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。

HTTP2.0 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗。HTTP2.0 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。

header 压缩

HTTP1.x 的 header 带有大量信息,而且每次都要重复发送,HTTP2.0 使用 HPACK 算法对 header 的数据进行压缩,减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,差量更新 HTTP 头部,既避免了重复 header 的传输,又减小了需要传输的大小。

header 采取的压缩策略:

  • HTTP2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在 HTTP2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。

服务端推送(server push)

服务端推送是一种在客户端请求之前发送数据的机制。

服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 时再发送这些请求。

服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。

参考文章:


查看原文

赞 0 收藏 0 评论 0

JackySummer 发布了文章 · 2020-12-03

前端安全-XSS和CSRF

XSS

概述

XSS(Cross Site Script),指跨站脚本攻击。原本缩写为 CSS,但因为与层叠样式表缩写(CSS)重名要做区分,所以改为 XSS。

攻击方式

攻击者通过向网站页面注入恶意脚本(一般是 JavaScript),通过恶意脚本对客户端网页进行篡改,达到窃取信息等目的,本质是数据被当作程序执行。

XSS 的注入点

  • HTML 的节点内容或属性,存在读取可输入数据
  • javascript 代码,存在由后台注入的变量或用户输入的信息
  • 富文本

XSS 危害

  • 通过 document.cookie 盗取 cookie
  • 使用 js 或 css 破坏页面正常的结构与样式
  • 流量劫持(通过访问某段具有 window.location.href 定位到其他页面:<script>window.location.href="www.baidu.com";</script>
  • Dos 攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应
  • 利用 iframe、frame、XMLHttpRequest 或上述 Flash 等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动
  • 偷取网站任意数据、用户资料等等

XSS 的类型

反射型(非持久)

反射型 XSS,也叫非持久型 XSS,是指发生请求时,XSS 代码出现在请求 URL 中,作为参数提交到服务器,服务器解析并响应。响应内容包含 XSS 代码,返回给浏览器解析并执行,这个过程就像一次反射,所以叫反射型 XSS。

这种攻击方式通常需要攻击者诱使用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。

该方式只会经过服务器,不会经过数据库。

例子:
比如用户进行搜索时,点击搜索按钮访问到如下链接

http://xxx.com/search?keyword="><script>alert('XSS攻击');</script>

当浏览器请求时,服务器解析参数 keyword,得到"><script>alert('XSS攻击');</script>,拼接到 HTML 中返回给浏览器,如下:

<input type="text" value="" />
<script>
  alert('XSS攻击')
</script>
">
<button>搜索</button>
<div>
  您搜索的关键词是:">
  <script>
    alert('XSS攻击')
  </script>
</div>

因此将其执行了。

存储型(持久)

存储型 XSS,也叫持久型 XSS,主要是将 XSS 代码发送到服务器,当浏览器请求数据时,脚本从服务器上传回并执行。与反射型 XSS 的差别在于,提交的代码会存储在服务器端,下次请求时目标页面时不用再提交 XSS 代码。

比较常见的场景就是网页的留言板,攻击者在留言板写下包含攻击性的脚本代码,发表之后所有访问该留言的用户,留言内容从服务器解析之后发现有 XSS 代码于是当做正常的 HTML 和 JS 解析执行,就发生了 XSS 攻击。

该方式会经过服务器,也会经过数据库。

DOM 型

基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,将攻击脚本写在 URL 中,诱导用户点击该 URL,如果 URL 被解析,那么攻击脚本就会被运行,和前两者 XSS 攻击区别是:取出和执行恶意代码由浏览器端完成,不经过服务端,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞主要在于 DOM 型攻击。

例子引用自:DOM 型攻击例子

<h2>XSS:</h2>
<input type="text" id="input" />
<button id="btn">Submit</button>
<div id="div"></div>
<script>
  const input = document.getElementById('input')
  const btn = document.getElementById('btn')
  const div = document.getElementById('div')

  let val

  input.addEventListener(
    'change',
    e => {
      val = e.target.value
    },
    false
  )

  btn.addEventListener(
    'click',
    () => {
      div.innerHTML = `<a href=${val}>testLink</a>`
    },
    false
  )
</script>

点击 Submit 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:

'' onclick=alert(/xss/)

用户提交之后,页面代码就变成了:

<a href onlick="alert(/xss/)">testLink</a>

此时,用户点击生成的链接,就会执行对应的脚本。

如何防范 XSS

总体就是不能将用户的输入直接存到服务器,需要对一些数据进行特殊处理

设置 HttpOnly

HttpOnly 是一个设置 cookie 是否可以被 javasript 脚本读取的属性,浏览器会禁止页面的 Javascript 访问带有 HttpOnly 属性的 Cookie。

严格来说,这种方式不是防御 XSS,而是在用户被 XSS 攻击之后,不被获取 Cookie 数据。

CSP 内容安全策略

CSP(content security policy),是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。

CSP 可以通过 HTTP 头部(Content-Security-Policy)或<meta>元素配置页面的内容安全策略,以控制浏览器可以为该页面获取哪些资源。比如一个可以上传文件和显示图片页面,应该允许图片来自任何地方,但限制表单的 action 属性只可以赋值为指定的端点。

现在主流的浏览器内置了防范 XSS 的措施,开启 CSP,即开启白名单,可阻止白名单以外的资源加载和运行

输入检查

对于用户的任何输入要进行编码、解码和过滤:

  • 编码:不能对用户输入的内容都保持原样,对用户输入的数据进行字符实体编码转义
  • 解码:原样显示内容的时候必须解码,不然显示不到内容了
  • 过滤:把输入的一些不合法的东西都过滤掉,从而保证安全性。如移除用户上传的 DOM 属性,如 onerror,移除用户上传的 Style 节点、iframe、script 节点等

对用户输入所包含的特殊字符或标签进行编码或过滤,如 <>script,防止 XSS 攻击

function escHTML(str) {
  if (!str) return ''
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/x27/g, '&#039;')
    .replace(/x22/g, '&quto;')
}

输出检查

用户的输入会存在问题,服务端的输出也会存在问题。一般来说,除富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。例如利用 sanitize-html 对输出内容进行有规则的过滤之后再输出到页面中。

输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

验证码

防止脚本冒充用户提交危险操作。

CSRF

概述

CSRF(Cross Site Request Forgery)指的是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。跨域指的是请求来源于其他网站,伪造指的是非用户自身的意愿。

攻击方式

攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

与 XSS 攻击不同的是:XSS 是攻击者直接对我们的网站 A 进行注入攻击,CSRF 是通过网站 B 对我们的网站 A 进行伪造请求。

例子:你登录购物网站 A 之后点击一个恶意链接 B,B 请求了网站 A 的下单接口,结果是在网站 A 的帐号生成一个订单。其背后的原理是:网站 B 通过表单、get 请求来伪造网站 A 的请求,这时候请求会带上网站 A 的 cookies,若登录态是保存在 cookies 中,则实现了伪造攻击。

跨站请求可以用各种方式:图片 URL、超链接、CORS、Form 提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中。

CSRF 危害

  • 用户的登录态被盗用
  • 冒充用户完成操作或修改数据

CSRF 的类型

GET 类型的 CSRF

例子引入自:GET CSRF 例子

假设有这样一个场景:目标网站 A(www.a.com),恶意网站 B(www.b.com)

两个网站的域名不一样,目标网站 A 上有一个删除文章的功能,通常是用户单击'删除文章'链接时才会删除指定的文章。这个链接是www.a.com/blog/del?id=1, id 代表不同的文章。实际上就是发起一个 GET 请求

  • 无法使用 Ajax 发起 GET 请求。因为 CSRF 请求是跨域的,而 Ajax 有同源策略的限制
  • 可以通过在恶意网站 B 上静态或者动态创建 img,script 等标签发起 GET 请求。将其 src 属性指向www.a.com/blog/del?id=1。通过标签的方式发起的请求不受同源策略的限制
  • 最后欺骗已经登录目标网站 A 的用户访问恶意网站 B,那么就会携带网站 A 源的登录凭证向网站 A 后台发起请求,这样攻击就发生了

CSRF 攻击有以下几个关键点:

  • 请求是跨域的,可以看出请求是从恶意网站 B 上发出的
  • 通过 img, script 等标签来发起一个 GET 请求,因为这些标签不受同源策略的限制
  • 发出的请求是身份认证后的

POST 类型的 CSRF

假如目标网站 A 上有发表文章的功能,那么我们就可以动态创建 form 标签,然后修改文章的题目。

在网站 B 中:

function setForm() {
  var form = document.createElement('form')
  form.action = 'www.a.com/blog/article/update'
  form.methods = 'POST'
  var input = document.createElement('input')
  input.type = 'text'
  input.value = 'csfr攻击啦!'
  input.id = 'title'
  form.appendChild(input)
  document.body.appendChild(form)
  form.submit()
}
setForm()

上面代码可以看出,动态创建了 form 表单,然后调用 submit 方法,就可以通过跨域的伪造请求来实现修改目标网站 A 的某篇文章的标题了。

通常是利用自动提交的表单

<form action="http://xxx.com/money" method="post">
  <input type="hidden" name="account" value="jacky" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>
  document.forms[0].submit()
</script>

链接类型的 CSRF

链接类型的 CSRF 并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!! </a>

如何防范 CSRF

验证码

由于 CSRF 攻击伪造请求不会经过受攻击的网站,所以我们可以在网站加入验证码,这样必须通过验证码之后才能进行请求,有效的遏制了 CSRF 请求。

但是,这种方式不是万能的,并不是每个请求都加验证码,那样用户体验会非常不好,只能在部分请求添加,作为一种辅助的防御手段。

验证 Referer

在 HTTP 协议中,头部有个 Referer 字段,他记录了该 HTTP 请求的来源地址,在服务端设置该字段的检验,通过检查该字段,就可以知道该请求是否合法,不过请求头也容易伪造。

cookie 设置 SameSite

设置 SameSite:设置 cookie 的 SameSite 值为 strict,这样只有同源网站的请求才会带上 cookie。这样 cookies 就不能被其他域名网站使用,达到了防御的目的。

添加 token 验证

浏览器请求服务器时,服务器返回一个 token,每个请求都需要同时带上 token 和 cookie 才会被认为是合法请求

这是一种相对成熟的解决方案。要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。在服务端随机生成 token,在 HTTP 请求中以参数的形式加入这个 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

更换登录态方案

因为 CSRF 本质是伪造请求携带了保存在 cookies 中的信息,所以对 session 机制的登录态比较不利,如果更换 JWT(JSON Web Token)方案,其 token 信息一般设置到 HTTP 头部的,所以可以防御 CSRF 攻击。

参考

查看原文

赞 0 收藏 0 评论 0

JackySummer 发布了文章 · 2020-11-30

谈谈对 React 新旧生命周期的理解

前言

在写这篇文章的时候,React 已经出了 17.0.1 版本了,虽说还来讨论目前 React 新旧生命周期有点晚了,React 两个新生命周期虽然出了很久,但实际开发我却没有用过,因为 React 16 版本后我们直接 React Hook 起飞开发项目。

但对新旧生命周期的探索,还是有助于我们更好理解 React 团队一些思想和做法,于是今天就要回顾下这个问题和理解总结,虽然还是 React Hook 写法香,但是依然要深究学习类组件的东西,了解 React 团队的一些思想与做法。

本文只讨论 React17 版本前的。

React 16 版本后做了什么

首先是给三个生命周期函数加上了 UNSAFE:

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

这里并不是表示不安全的意思,它只是不建议继续使用,并表示使用这些生命周期的代码可能在未来的 React 版本(目前 React17 还没有完全废除)存在缺陷,如 React Fiber 异步渲染的出现。

同时新增了两个生命周期函数:

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

UNSAFE_componentWillReceiveProps

UNSAFE_componentWillReceiveProps(nextProps)

先来说说这个函数,componentWillReceiveProps

该子组件方法并不是父组件 props 改变才触发,官方回答是:

如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

先来说说 React 为什么废除该函数,废除肯定有它不好的地方。

componentWillReceiveProps函数的一般使用场景是:

  • 如果组件自身的某个 state 跟父组件传入的 props 密切相关的话,那么可以在该方法中判断前后两个 props 是否相同,如果不同就根据 props 来更新组件自身的 state。
    类似的业务需求比如:一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。

但该方法缺点是会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。

而在新版本中,官方将更新 state 与触发回调重新分配到了 getDerivedStateFromPropscomponentDidUpdate 中,使得组件整体的更新逻辑更为清晰。

新生命周期方法static getDerivedStateFromProps(props, state)怎么用呢?

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

从函数名字就可以看出大概意思:使用 props 来派生/更新 state。这就是重点了,但凡你想使用该函数,都必须出于该目的,使用它才是正确且符合规范的。

getDerivedStateFromProps不同的是,它在挂载和更新阶段都会执行(componentWillReceiveProps挂载阶段不会执行),因为更新 state 这种需求不仅在 props 更新时存在,在 props 初始化时也是存在的。

而且getDerivedStateFromProps在组件自身 state 更新也会执行而componentWillReceiveProps方法执行则取决于父组件的是否触发重新渲染,也可以看出getDerivedStateFromProps并不是 componentWillReceiveProps方法的替代品.

引起我们注意的是,这个生命周期方法是一个静态方法,静态方法不依赖组件实例而存在,故在该方法内部是无法访问 this 的。新版本生命周期方法能做的事情反而更少了,限制我们只能根据 props 来派生 state,官方是基于什么考量呢?

因为无法拿到组件实例的 this,这也导致我们无法在函数内部做 this.fetch()请求,或者不合理的 this.setState()操作导致可能的死循环或其他副作用。有没有发现,这都是不合理不规范的操作,但开发者们都有机会这样用。可如果加了个静态 static,间接强制我们都无法做了,也从而避免对生命周期的滥用。

React 官方也是通过该限制,尽量保持生命周期行为的可控可预测,根源上帮助了我们避免不合理的编程方式,即一个 API 要保持单一性,做一件事的理念。

如下例子:

// before
componentWillReceiveProps(nextProps) {
  if (nextProps.isLogin !== this.props.isLogin) {
    this.setState({
      isLogin: nextProps.isLogin,
    });
  }
  if (nextProps.isLogin) {
    this.handleClose();
  }
}

// after
static getDerivedStateFromProps(nextProps, prevState) {
  if (nextProps.isLogin !== prevState.isLogin) { // 被对比的props会被保存一份在state里
    return {
      isLogin: nextProps.isLogin, // getDerivedStateFromProps 的返回值会自动 setState
    };
  }
  return null;
}

componentDidUpdate(prevProps, prevState) {
  if (!prevState.isLogin && this.props.isLogin) {
    this.handleClose();
  }
}

UNSAVE_componentWillMount

UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。

我们应该避免在此方法中引入任何副作用或事件订阅,而是选用componentDidMount()

在 React 初学者刚接触的时候,可能有这样一个疑问:一般都是数据请求放在componentDidMount里面,但放在componentWillMount不是会更快获取数据吗?

因为理解是componentWillMount在 render 之前执行,早一点执行就早拿到请求结果;但是其实不管你请求多快,都赶不上首次 render,页面首次渲染依旧处于没有获取异步数据的状态。

还有一个原因,componentWillMount是服务端渲染唯一会调用的生命周期函数,如果你在此方法中请求数据,那么服务端渲染的时候,在服务端和客户端都会分别请求两次相同的数据,这显然也我们想看到的结果。

特别是有了 React Fiber,更有机会被调用多次,故请求不应该放在componentWillMount中。

还有一个错误的使用是在componentWillMount中订阅事件,并在componentWillUnmount中取消掉相应的事件订阅。事实上只有调用componentDidMount后,React 才能保证稍后调用componentWillUnmount进行清理。而且服务端渲染时不会调用componentWillUnmount,可能导致内存泄露。

还有人会将事件监听器(或订阅)添加到 componentWillMount 中,但这可能导致服务器渲染(永远不会调用 componentWillUnmount)和异步渲染(在渲染完成之前可能被中断,导致不调用 componentWillUnmount)的内存泄漏。

对于该函数,一般情况,如果项目有使用,则是通常把现有 componentWillMount 中的代码迁移至 componentDidMount 即可。

UNSAFE_componentWillUpdate

当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。

注意,不能在该方法中调用 this.setState();在 componentWillUpdate 返回之前,你也不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新。

首先跟上面两个函数一样,该函数也发生在 render 之前,也存在一次更新被调用多次的可能,从这一点上看就依然不可取了。

其次,该方法常见的用法是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate 中进行相应的处理。但 React 16 版本后有 suspense、异步渲染机制等等,render 过程可以被分割成多次完成,还可以被暂停甚至回溯,这导致 componentWillUpdatecomponentDidUpdate 执行前后可能会间隔很长时间,这导致 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。而且足够使用户进行交互操作更改当前组件的状态,这样可能会导致难以追踪的 BUG。

为了解决这个问题,于是就有了新的生命周期函数:

getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为第三个参数传入componentDidUpdate(prevProps, prevState, snapshot)

componentWillUpdate 不同,getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。

虽然 getSnapshotBeforeUpdate 不是一个静态方法,但我们也应该尽量使用它去返回一个值。这个值会随后被传入到 componentDidUpdate 中,然后我们就可以在 componentDidUpdate 中去更新组件的状态,而不是在 getSnapshotBeforeUpdate 中直接更新组件状态。避免了 componentWillUpdatecomponentDidUpdate 配合使用时将组件临时的状态数据存在组件实例上浪费内存,getSnapshotBeforeUpdate 返回的数据在 componentDidUpdate 中用完即被销毁,效率更高。

来看官方的一个例子:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们是否在 list 中添加新的 items?
    // 捕获滚动位置以便我们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

如果项目中有用到componentWillUpdate的话,升级方案就是将现有的 componentWillUpdate 中的回调函数迁移至 componentDidUpdate。如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。

除了这些,React 16 版本的依然还有大改动,其中引人注目的就是 Fiber,之后我还会抽空写一篇关于 React Fiber 的文章,可以关注我的个人技术博文 Github 仓库,觉得不错的话欢迎 star,给我一点鼓励继续写作吧~

参考:

查看原文

赞 0 收藏 0 评论 0

JackySummer 发布了文章 · 2020-11-23

HTTP 和 HTTPS 协议

前言

HTTP 是前端开发人员必须知道的知识,也是日常处理请求都会接触的,但如果要构成知识体系,则需要一点点填充知识,今天就要讲下 HTTP 和 HTTPS 协议。

HTTP 协议是什么

HTTP 协议是超文本传输协议的缩写,英文是Hyper Text Transfer Protocol。它是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议。设计 HTTP 的初衷是为了提供一种发布和接收 HTML 页面的方法。

HTTP 有多个版本,目前广泛使用的是HTTP/1.1版本。

HTTP 特点

  • 请求和响应:基本的特性,由客户端发起请求,服务端响应
  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。
  • 灵活:HTTP 允许传输任意类型的数据对象。传输的类型由Content-Type以标记。
  • 无连接:限制每次连接只处理一个请求。服务器处理完请求,并收到客户的应答后,即断开连接,但是却不利于客户端与服务器保持会话连接,为了弥补这种不足,产生了两项记录 HTTP 状态的技术,一个叫做 Cookie,一个叫做 Session。
  • 无状态:无状态是指协议对于事务处理没有记忆,后续处理需要前面的信息,则必须重传。

常见的请求方法

  • GET:请求指定的页面信息,并返回实体主体。
  • POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。
  • HEAD:类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
  • PUT:从客户端向服务器传送的数据取代指定的文档的内容。
  • DELETE:请求服务器删除指定的页面。

HTTP 的缺点

  • 明文传输:数据完全肉眼可见,能够方便地研究和分析,但也容易被窃听
  • HTTP 是不安全的,无法验证通信双方的身份,也不能判断报文是否被修改

为什么要用 HTTPS

HTTP 协议以明文方式传递信息,不提供任何方式的数据加密,不适合传输一些敏感信息,比如:各种账号、密码等信息,使用 HTTP 协议传输隐私信息非常不安全。

为了解决上述 HTTP 存在的问题,就出现了 HTTPS。

HTTPS 协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性;解决了 HTTP 存在的缺点问题。

什么是 HTTPS?

HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS,通过 SSL 证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

HTTPS 是一种通过计算机网络进行安全通信的传输协议,经由 HTTP 进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS 使用的主要目的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性。

SSL 是什么?

SSL(Secure Socket Layer,安全套接字层):SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。

TLS(Transport Layer Security,传输层安全):其前身是 SSL,它最初的几个版本(SSL 1.0、SSL 2.0、SSL 3.0)由网景公司开发,目前使用最广泛的是TLS 1.1TLS 1.2

浏览器在使用 HTTPS 传输数据的流程是什么?

  • 首先客户端通过 URL 访问服务器建立 SSL 连接。
  • 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端。
  • 客户端的服务器开始协商 SSL 连接的安全等级,也就是信息加密的等级。
  • 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
  • 服务器利用自己的私钥解密出会话密钥。
  • 服务器利用会话密钥加密与客户端之间的通信。

HTTPS 的特点

  1. 内容加密:采用混合加密技术,中间者无法直接查看明文内容
  2. 验证身份:通过证书认证客户端访问的是自己的服务器(确认网站的真实性)
  3. 保护数据完整性:防止传输的内容被中间人冒充或者篡改

对称加密

也叫做共享密钥加密。通信的双方使用同一个密钥对信息进行加密和解密,优点是加解密速度快。运用对称加密有一个前提就是在发送信息前双方都必须知道加密的规则,但是在互联网的环境下我们每天可能跟不同人发送信息,很多人我们之前根本没认识过,这种情况下我们根本不可能事先就约定好加密规则,那么我们就只能通过信息把加密规则(密钥)发送给对方,然后我们再根据加密规则来加密聊天信息,很显然这肯定是不可取的,不经过加密的数据在网络传输是没有任何安全性可言的。

非对称加密

又称作公开密钥加密。使用一对非对称的密钥,私钥保存在本地,公钥可以公开。发送密文的一方使用对方的公钥进行加密,接收方收到后,使用自己的私钥进行解密。优点是解密的私钥不需要进行分发。缺点是加解密速度慢,同时公钥可能被替换。

混合加密

使用混合加密的原因:非对称秘钥加密会比较慢,但是私钥不用传输安全,而对称秘钥方式则相反,为了充分利用两者优缺点,产生了混合加密机制
解决方案:即通信建立之初采用公开加密的方式传输对称秘钥方式中的同一秘钥,这样确保了共享秘钥的安全性,然后以后的通信均采用对称加密方式,这样提升了通信速度。

数字摘要

一段信息,经过摘要算法得到一串哈希值,就是摘要。通过单向 hash 函数对原文进行哈希,将需加密的明文“摘要”成一串固定长度(如 128bit)的密文,不同的明文摘要成的密文其结果总是不相同,同样的明文其摘要必定一致,并且即使知道了摘要也不能反推出明文。

数字签名

数字签名建立在公钥加密体制基础上,是公钥加密技术的另一类应用。摘要经过私钥的加密后,便有了一个新的名字 —— 数字签名。

数字证书

非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构 CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书(
实际就是一个 .crt 文件)被放到服务端,具有服务器身份验证和数据传输加密功能。

HTTPS 的缺点

  • HTTPS 协议多次握手,导致页面的加载时间延长近 50%;
  • HTTPS 连接缓存不如 HTTP 高效,会增加数据开销和功耗;
  • 申请 SSL 证书需要钱,功能越强大的证书费用越高。
  • SSL 证书通常需要绑定 IP,不能在同一 IP 上绑定多个域名,IPv4 资源不可能支撑这个消耗。
  • SSL 涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。

总结 HTTPS 和 HTTP 的区别

  • 安全性:HTTP 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
  • 连接端口:HTTP 标准端口是 80,而 HTTPS 的标准端口是 443。
  • 费用:HTTPS 协议需要到 CA 申请证书,一般免费证书较少,因而需要一定费用
  • 传输方式:HTTP 是超文本传输协议,信息是明文传输,而 HTTPS 是 SSL 加密传输协议。
  • 工作层:在 OSI 网络模型中,HTTP 工作于应用层,而 HTTPS 工作在传输层。
  • 工作耗时:HTTP 耗时=TCP 握手,而 HTTPS 耗时=TCP 握手+SSL 握手。
  • 显示形式:HTTP 的 URL 以http://开头,而 HTTPS 的 URL 以https://开头。

参考


查看原文

赞 0 收藏 0 评论 0

JackySummer 发布了文章 · 2020-11-16

手把手带你入门 Travis 自动化部署

前言

本文主要讲如何用 Travis 来实现自动化部署,并参照 Github 真实项目开发简单场景来介绍。

CI/CD

在说 Travis 之前,先了解一下 CI/CD 的概念。

CI

CI(Continuous integration)—— 持续集成。持续集成是指频繁地(一天多次)将代码集成到主干,注重将各个开发者的工作集合到一个代码仓库中,在源代码变更后会自动检测、拉取、构建和进行单元测试,其目的在于让产品快速迭代,同时保证高质量。

比如日常工作中,向development分支提交一个 PR,配置好 Travis 就会进行自动测试等等工作,通过并被 review 后才能允许合并到开发分支。一旦自动测试脚本有错误,则不允许合并。

CD

CD 可对应多个英文名称,一个是持续交付(Continuous delivery),一个是持续部署(Continuous deployment)

持续交付指频繁地将软件的新版本交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。
目标是拥有一个可随时部署到生产环境的代码库。好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。

持续部署指持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。目标是代码在任何时刻都是可部署的,可以进入生产阶段。

持续交付并不是指软件每一个改动都要尽快部署到产品环境中,它指的是任何的代码修改都可以在任何时候实施部署。

持续交付表示的是一种能力,而持续部署表示的则一种方式。持续部署是持续交付的最高阶段

Travis 是什么

Travis CI 提供的是持续集成服务,它可以绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。当然这只是其中一种 CI 工具,另外还有Jenkins等。

本文仅仅做的抛砖引玉,让你对这些概念和 Travis 有初步了解和使用,不再陌生,剩余的就靠你自己去学习了。也许你觉得这是公司企业级项目才配上的东西,但其实 Travis 是免费的,个人开发者也可以进行使用,而且是挺好用。

注册 Travis

进入https://travis-ci.com

比如我选择 Github 方式登录

登录之后 Github 的 repo 授权给 travis,我直接选 All,开启对项目的监控

选完之后回到主页就能看到你的仓库页面了,可以看到有一个项目我是配置了 travis 的,所以显示绿色状态。

这项目是最近在自己动手做的 React 组件库项目 monki-ui,尽量参照企业级标准搭建的,技术栈是React + TypeScript + Dumi + Jest,觉得不错可供借鉴学习的话可以 ✨ star 一下哦...

获取 Github Access Token

接着进入 Github -> Developer settings -> Personal access tokens -> Generate new token

勾选repo下的所有项,以及user下的user:email后,会生成一个 token,然后记得复制保存好,因为生成后以后就不会再显示了,如果忘记那必须重新生成token才可以

配置 Travis

我自己用 create-react-app 初始化了一个 Github 项目 travis-demo,删除文件到最简单的项目结构。

然后模拟一下日常开发场景,让大家体会工作中的应用,我先是从master分支 checkout 出development分支,并把development分支设置为default分支,这个就当做我们开发中的 dev 分支,日常 PR 都会合并到该分支上。

然后从development分支 checkout 出一条分支docs/update-readme,接着修改 readme 的内容,git add 和 git commit 后就待 push 了。

我们上面生成的 token 已经说了要先保存好的,接着去travis-ci 官网,找到刚才创建的项目Jacky-Summer/travis-demo,点进去选择Settings

  • 设置全局变量(Environment Variables)

NAME一般规范是大写和下划线来命名,这里我取为GITHUB_TOKENVALUE为我们保存的 token,然后选择Add添加

项目新建配置文件

在项目根目录配置.travis.yml配置文件(当前在docs/udpate-readne

language: node_js  # 设置语言

node_js:
  - 12 # 指定node.js版本

cache: yarn # 开启缓存

install:
  - yarn
script:
  - yarn build

接着就 push 到远程分支,此时打开 github 项目仓库,点击Create pull request

就会看到此时 Travis 正在运行了,它在干什么事,可以点进Details去看看,它就是从头来一遍我们项目,git clone开始,然后yarn installyarn build,整个过程没有错误,那就通过了,通过之后看到此时 PR 这样显示的:

通过说明我们可以 Merge 了,这里我选择Squash and merge合并,合并后会再次跑一次 Travis(但日常开发我司是要先有其他同事 review 通过才能 merge 的)。此时我们的 PR 就合并到development分支上了,你的邮箱整个过程中也会有邮件通知你。

但其实你会发现你不用等它 Travis 运行完成,我们就可以点Merge按钮,按道理它应该暂时禁用才对,等到 Travis 通过才允许按。这个在Github该项目目录 -> Settings -> Branches -> Add rules,这里只是简单举个例子如下:

仅仅这样可能并感受不到什么作用,现在我们切换回development分支,git pull最新代码,再 checkout 一条新分支test/test-case-example,添加个测试文件src/index.test.js

如果不知道什么是单元测试的可以看我这篇文章:一文带你了解 Jest 单元测试

describe('单元测试', () => {
  it('测试用例', () => {
    const foo = true
    expect(foo).toBeTruthy() // 期望 foo 变量的值为 true
  })
})

当然这是个白痴没有意义的测试,运行yarn test,测试用例通过,如果我们修改代码

describe('单元测试', () => {
  it('测试用例', () => {
    const foo = true
    expect(foo).toBeFalsy() // 测试不会通过
  })
})

此时yarn test不会通过,这就对应我们日常修改代码后,假设你没有重新运行测试(但测试用例实际是运行错误的),你却 push 上去了,又合并了就麻烦了,因为测试不过说明你代码可能存在问题。

接着需要 Travis 帮忙了,修改.travis.yml文件

language: node_js

node_js:
  - 12

cache: yarn

install:
  - yarn
script:
  - yarn test # 运行测试
  - yarn build
  • 接着同上步骤,开个 PR,此时显示如下,此时就看到 Travis 显示Required,但因为我是项目唯一拥有者兼管理员,所以依然可以直接 merge,但如果其他人 PR 上来,则是不行的要等 Travis 通过才允许按 merge 按钮

等一会你会发现它挂了,这里就显示×号了,此时失败不能合并(非管理员)。

我们去Details里面看看哪里出错了,看到是测试用例出错导致 Travis 无法通过:

  • 于是此时我们应该是去修改代码,再提交,这就是 Travis 的其中一个常见的作用了。
  • 其后它还有很多其它可以玩的,比如自动化部署到 Github page 页面,在我现在的 React 开源组件库 monki-ui - travis 配置 作为例子
deploy:
  provider: pages  # 指定部署到Github Pages,即 gh-pages分支
  github_token: $GITHUB_TOKEN # 名字对应我们上方的 token
  skip_cleanup: true # 指定保留构建后的文件
  keep-history: true # 指定每次部署会新增一个提交记录再推送,而不是使用 git push --force
  local_dir: docs-dist # 指定构建后要部署的目录
  on:
    branch: master # 指定 master 分支有提交行为时,将触发构建后部署


查看原文

赞 0 收藏 0 评论 0

JackySummer 收藏了文章 · 2020-11-13

开发和维护个人开源项目之代码仓库管理

开发和维护个人开源项目之代码仓库管理

我将代码仓库管理分为以下几个部分:

  • 分支管理策略
  • 工作流程
  • tag版本管理
  • 提交格式、日志获取

代码仓库管理

分支管理策略

  • master(稳定分支)(保护分支)

    • master+tag(发布版本,里程碑)
    • hotfix(临时分支,补丁分支)
  • develop(稳定分支)(保护分支)

    • feature(临时分支,功能分支)
    • release(临时分支,预发布分支)

masterdevelop是固定受保护、不能直接push的分支。两者区别:

  • master始终是最后一次发布的稳定版本
  • develop上会有未发布的功能

每个分支的功能独立,便于理解。

工作流程

  1. 创建开发分支

    git checkout -b develop master
  2. 功能开发

    git checkout -b feature-x develop
  3. 功能开发完成,分支合并到develop分支

    git checkout develop
    git merge --no-ff feature-x
    git branch -d feature-x
  4. 创建预发布分支

    git checkout -b release-0.1 develop
  5. 将预发布合并到master和开发分支

    git checkout master
    git merge --no-ff release-0.1
    git tag -a 0.1
    git checkout develop
    git merge --no-ff release-0.1
    git branch -d release-0.1
  6. 修补bug

    git checkout -b fixbug-0.1 master
    git checkout master //合并到主线
    git merge --no-ff fixbug-0.1
    git tag -a 0.1.1
    git checkout develop //合并到开发分支
    git merge --no-ff fixbug-0.1
    git branch -d fixbug-0.1
  7. fork代码,pull requestdevelop

tag版本管理

上一节提到了用tag打版本,版本号的命名规则:

  1. 项目立项

    0.0.0 //主版本.次版本号.修正版本号
    • 主版本号:0表示正在开发阶段;
    • 次版本号:增加新的功能时增加;
    • 修订号:修复bug等改动
  2. 开发完成

    1.0.0
    • 主版本号:全盘重构时增加;重大功能或方向改变时增加;大范围不兼容之前的时增加;
    • 次版本号:增加新功能时增加;
    • 修订号:修复bug、功能调整等改动

提交格式、日志获取

规范化的提交对后续的整理、回溯是很友好的,比如:realse的时候进行一轮日志获取就能生成版本变更信息(版本开发之前应有计划)。

  • 规范化commit message

    • 提交类型(友好提醒)
    • 提交信息格式
    • 提交信息验证
  • changelog生成

    • conventional-changelog-cli 工具

我是详细实践,请点我:Git commit message和工作流规范

总结

本文主要对代码仓库的管理作了整理,这个也是每个项目启动之时就应该设计好的。

参考链接

Git 工作流程
Git分支管理策略
团队协作中的 Github flow 工作流程
Commit message 和 Change log 编写指南
如何写好 Git commit messages
优雅的提交你的 Git Commit Message
接口(Api)版本号命名规则

查看原文

JackySummer 收藏了文章 · 2020-11-12

?Github集成TravisCI:自动发布

前言

已经有阮一峰老师的持续集成服务 Travis CI 教程,为什么还要写这篇文章?

原因有二:

  1. 文章内容有些过时
  2. 文章覆盖度不够,有些实践细节没写出来

由于以上原因,纵然可以笔者很快在Github集成Travis CI并成功构建,但在发布时却踩了一些坑,折腾一波才终于发布成功。故写下此文,旨在补充更多的细节,帮助他人少走弯路。

正文

免费购买Travis CI应用

点击 https://github.com/marketplace/travis-ci,登录后免费购买(开源项目集成Travis CI不收费)。

选择关联仓库

选择个人或组织名下需要关联Travis CI的Github仓库。

已经设置过的,想进行修改,可以在Github的 Personal settings-> Applications 中进入。
image.png

编写CI文件

在项目根目录下新建 .travis.yml 文件

touch .travis.yml

发布到github pages

下面展示一个可以发布到gh-pages的例子,可以稍做修改,复制粘贴使用。

该示例包含了:

  • 指定node.js版本
  • 使用yarn进行安装依赖及构建
  • 对安装需要的依赖进行了缓存
  • 设置了两个不含敏感信息的环境变量
  • 设置了一个含有敏感信息的环境变量
  • 把构建生成的文件部署至github pages
language: node_js
node_js:
- lts/*
env:
- API_SERVER=https://easy-mock.com/mock/5c1b3895fe5907404e654045/femessage-mock PUBLIC_PATH=http://levy.work/nuxt-element-dashboard/
# 默认是yarn, 如果有yarn.lock的话
install:
- yarn
# 默认是 yarn test
script:
- yarn build
cache: yarn
deploy:
  provider: pages
  skip-cleanup: true
  keep-history: true
  local-dir: dist
  on:
    branch: master
  github-token: $GITHUB_TOKEN

下面对文件进行说明。

language: node_js
node_js:
- lts/*
  • 第1行指定了构建环境为node.js
  • 第2、3行指定使用node.js最新的LTS版本
env:
- API_SERVER=xxx PUBLIC_PATH=xxx

上面是设置两个环境变量。

注意,一次构建中传多个环境变量,必须写在同一行,使用空格分开。

env:
- API_SERVER=xxx 
- PUBLIC_PATH=xxx

如果写成上面的形式,则会变成两个构建,每一个构建中只有一个环境变量。

install:
- yarn
script:
- yarn build
cache: yarn

上面指定使用yarn进行安装依赖,安装好后执行 yarn build 命令; 为yarn的依赖加速安装,开启了缓存。

下面是最关键的部署配置。

deploy:
  provider: pages
  github-token: $GITHUB_TOKEN
  skip-cleanup: true
  keep-history: true
  local-dir: dist
  on:
    branch: master
  • 第2行指定部署到Github Pages,即仓库的 gh-pages 分支,请确保仓库的pages分支是 gh-pages , 相关操作可以看这里
  • 第3行指定保留构建后的文件
  • 第4行指定每次部署会新增一个提交记录再推送,而不是使用 git push --force
  • 第5行指定构建后要部署的目录
  • 第6、7行指定 master 分支有提交行为时,将触发构建后部署
  • 第8行是部署需要用到的github-token,其中$GITHUB_TOKEN是变量,它可以在Travis CI个人仓库的setting页里设置,相关操作可以看这里

发布到npm

再给出把node.js模块发布到npm的例子

主要是 deploy 这里有所不同

deploy:
  provider: npm
  email: <your_email>
  # api_key: travis encrypt NPM_TOKEN --add deploy.api_key --com
  on:
    branch: master
  skip-cleanup: true

api_key指的的npm的token,可以登录npm后,在个人中心生成

因为不能泄露,所以要通过travis ci的命令行工具进行加密,执行以下命令

travis encrypt NPM_TOKEN --add deploy.api_key --com

复杂例子

下面是一个复杂的例子,也是实际用到的配置,主要是

  • master分支才会触发构建
  • 执行script命令前先读取shell中的环境变量,并生成.env文件
  • 构建成功后

    • 把模块发布到npm
    • 把文档发布到gh-pages
branches:
  only:
    - master
language: node_js
node_js:
- lts/*
git:
  depth: 3
install:
- yarn --frozen-lockfile
before_script: echo OSS_KEY=$OSS_KEY\\nOSS_SECRET=$OSS_SECRET\\nOSS_BUCKET=$=OSS_BUCKET\\nOSS_REGION=$OSS_REGION > .env
script:
- yarn build
cache: yarn
deploy:
- provider: pages
  local-dir: docs
  github-token: $GITHUB_TOKEN
  skip-cleanup: true
  keep-history: true
- provider: npm
  email: levy9527@qq.com
  api_key: $NPM_TOKEN
  skip-cleanup: true

相关操作

使用travis命令行工具加密

加密要用到travis命令行工具,如果是在travis ci web界面设置环境变量,则可直接跳过。

下面给出mac环境下操作需要注意的点

1.安装命令:

brew install travis

否则很可能会出现问题

2.确保在 https://travis-ci.org/ sign in with github

3.然后在项目根目录里,执行命令

travis login —auto

4.修改git设置

vi .git/config

确保

[travis]
  slug = 是你在travis关联的仓库

5.添加加密环境变量

travis encrypt github-token=xxx --add deploy.github-token --com

因为笔者登录的travis ci域名是 https://travis-ci.com,所以要带参数 --com , 默认是 https://travis-ci.org

通过环境变量设置GITHUB_TOKEN

首先为Travis CI新建一个token

点击生成新token

设置权限

image.png
复制生成的token。(记得先不要刷新或离开当前页面,否则token就看不见了,只能重新生成)

登录Travis CI, 进入要集成的项目设置页。

image.png

添加环境变量GITHUB_TOKEN

注意,这里的环境变量是通过bash设置、并在.yml里读取的,所以变量名是大写加下划线形式,这是bash的最佳实践,千万别写成github-token

image.png

GitHub Pages

查看gh-pages分支的部署情况

进入仓库 Settings -> Options

image.png

往下翻看,可以看到效果
image.png
因为笔者自定义了域名,所以地址不是默认的 https://xxx.github.io/xxx

查看原文

JackySummer 发布了文章 · 2020-11-11

escape、encodeURI、encodeURIComponent区别

前言

JS 中有三个可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent

escape()

通常用于对字符串编码,不适用于对 URL 编码

除了 ASCII 字母、数字和特定的符号外,对传进来的字符串全部进行转义编码,因此如果想对 URL 编码,最好不要使用此方法。

escape 不会编码的字符有 69 个:* + - . / @ _ 0-9 a-z A-Z

当然如果没有必要,不要使用 escape。

encodeURI()

encodeURI()不会进行编码的字符有 82 个 : ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # 0-9 a-z A-Z

使用encodeURI()编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了%20,encodeURI主要用于直接赋值给地址栏。

encodeURIComponent()

encodeURIComponent:不会进行编码的字符有 71 个:! ' ( ) * - . _ ~ 0-9 a-z A-Z

encodeURIComponentencodeURI编码的范围更大,如encodeURIComponent会把 http:// 编码成 http%3A%2F%2FencodeURI不解码维持http://

encodeURIComponent() 方法在编码单个 URIComponent(指请求参数)应当是最常用的,它可以将参数中的中文、特殊字符进行转义,而不会影响整个 URL

如何选择和使用三个函数

  • 如果只是编码字符串,和 URL 没有关系,才可以用escape。(但它已经被废弃,尽量避免使用,应用encodeURIencodeURIComponent
  • 如果需要编码整个 URL,然后需要使用这个 URL,那么用encodeURI
  • 如果需要编码 URL 中的参数的时候,那么encodeURIComponent是最好方法。
encodeURI("https://github.com/Jacky-Summer/test params");

编码后变成

https://github.com/Jacky-Summer/test%20params

其中空格被编码成了%20,而如果是用encodeURIComponent

https%3A%2F%2Fgithub.com%2FJacky-Summer%2Ftest%20params

/ 都被编码了,整个 URL 已经没法用了。

当编码 URL 的特殊参数时:

// 参数的 / 是需要编码的,而如果是 encodeURI 编码则不编码 / 就会出问题
let param = "https://github.com/Jacky-Summer/";
param = encodeURIComponent(param);
const url = "https://github.com?param=" + param;
console.log(url) // "https://github.com?param=https%3A%2F%2Fgithub.com%2FJacky-Summer%2F"

参考:https://www.zhihu.com/questio...


查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 67 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2019-07-18
个人主页被 1.6k 人浏览