0%

初始化

创建空文件夹,然后运行:

1
npx lerna init

这行命令会创建一个空的packages文件夹,一个package.jsonlerna.json

1
2
3
4
|-- mono-repo
|-- packages
|-- lerna.json
|-- package.json

package.json有一点需要注意,他的private必须设置为true,因为本身并不是一个项目,而是承载多个子项目的项目,所以他自己不能直接发布,发布的应该是 packages/下面的各个子项目。

1
"private": true

lerna.json 初始化长这样:

1
2
3
4
{
"packages": ["packages/*"],
"version": "0.0.0"
}

packages字段就是标记你子项目的位置,默认就是packages/文件夹,他是一个数组,所以是支持多个不同位置的。

另外一个需要特别注意的是version字段,这个字段有两个类型的值,一个是像上面的0.0.0这样一个具体版本号,还可以是independent这个关键字。如果是具体版本号,那lerna管理的所有子项目都会有相同的版本号,如果你设置为independent,那各个子项目可以有自己的版本号。

因为我们的组件都是需要独立版本号,所以直接将version设置为independent

1
2
3
4
{
"packages": ["packages/*"],
"version": "independent"
}

创建子项目

创建子项目可以使用 lerna 的命令来创建:

1
lerna create <name>

通过 create 创建的子项目目录:

1
2
3
4
5
6
7
8
9
10
|-- mono-repo
|-- packages
|-- @mono-repo/project_1 # 推荐使用 `@<项目名>/<子项目名>` 的方式命名
|-- __test__
|-- lib
|-- @mono-repo/project_2
|-- __test__
|-- lib
|-- lerna.json
|-- package.json

这个是使用lerna create默认生成的目录结构,__test__文件夹下面放得是单元测试内容,lib下面放得是代码。实际使用过程中可以进行调整。

安装依赖项

lerna bootstrap

packages/下面的每个子项目有自己的依赖包,可使用命令:

1
2
3
lerna bootstrap

lerna bootstrap --hoist

删除已经安装的子项目node_modules可以手动删除,也可以使用:

1
lerna clean

具体命令含义可参考


yarn workspace

lerna bootstrap --hoist虽然可以将子项目的依赖提升到顶层,但是他的方式比较粗暴:先在每个子项目运行npm install,等所有依赖都安装好后,将他们移动到顶层的node_modules。这会导致一个问题,如果多个子项目依赖同一个第三方库,但是需求的版本不同怎么办?比如我们三个子项目都依赖 antd,但是他们的版本不完全一样:

1
2
3
4
5
6
7
8
// @mono-repo/project_1
"antd": "3.1.0"

// @mono-repo/project_2
"antd": "3.1.0"

// @mono-repo/project_3
"antd": "4.9.4"

这时候就需要介绍yarn workspace了,他可以解决前面说的版本不一致的问题,lerna bootstrap --hoist会把所有子项目用的最多的版本移动到顶层, 从而导致某些子项目依赖不正确,而yarn workspace 则会检查每个子项目里面依赖及其版本,如果版本不一样则会留在子项目自己的node_modules里面,只有完全一样的依赖才会提升到顶层。

还是以上面这个antd为例,使用yarn workspace的话,会把project1project2的 3.1.0 版本移动到顶层,而project3项目下会保留自己 4.9.4 的 antd,这样每个子项目都可以拿到自己需要的依赖了。

yarn workspace使用也很简单,yarn 1.0以上的版本默认就是开启workspace的,所以我们只需要在顶层的package.json加一个配置就行:

1
2
3
4
// 顶层package.json
{
"workspaces": ["packages/*"]
}

在 lerna.json 里面指定npmClientyarn,并将useWorkspaces设置为true,稍稍改动变成这样:

1
2
3
4
5
6
{
"packages": ["packages/*"],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true
}

使用了yarn workspace,我们就不用lerna bootstrap来安装依赖了,而是像以前一样yarn install就行了,他会自动帮我们提升依赖,这里的yarn install无论在顶层运行还是在任意一个子项目运行效果都是一样的。

更多请参考

启动子项目

我们可以到子项目的目录运行 start 命令, 但是频繁切换文件是在太麻烦,lerna 提供了相应的命令以帮助我们直接在顶层运行:

1
lerna run [script]

比如我们在顶层运行了 lerna run start,这相当于去每个子项目下面都去执行 yarn run start 或者 npm run start,具体是 yarn 还是 npm,取决于你在 lerna.json 里面的这个设置:

1
"npmClient": "yarn"

如果我只想在其中一个子项目运行命令,应该怎么办呢?加上--scope就行了,比如我就在顶层的package.json里面加了这么一行命令:

1
2
3
4
5
6
// 顶层package.json
{
"scripts": {
"start:project1": "lerna --scope @mono-repo/project_1 run start"
}
}

注意scope后面的项目名称不是目录名,而是子项目package.jsonname

1
2
3
4
// 子项目project_1的package.json
{
"name": "@mono-repo/project_1"
}

引入公共组件

当我们的@mono-repo/project_2要引用@mono-repo/project_1的组件,我们需要先在@mono-repo/project_2package.json里面将依赖加上,我们可以去手动修改他,也可以使用lerna命令:

1
lerna add @mono-repo/project_1 --scope @mono-repo/project_2

这样我们可以在project2中引入project1的组件,但是需要注意多个项目引用时,要避免各个子项目之间的循环引用。

组件库相关请参考组件库构建与编写

发布

发布直接使用 lerna publish,因为此前我们已经将 version 修改为 independent,所以在发布时,只会自动更新有变动的子项目以及依赖该子项目的子项目的版本号。

更多发布命令参数及解释请参考lerna/publish

背景

公司有一些前端代码在不同的托管平台上,为了统一管理和维护的效率,所以要将其他托管平台的代码全部快捷平迁到公司的私有 gitlab 上。

迁移方式

在你把原来的仓库推到你的仓库的新副本或镜像之前,你必须在新的托管平台上创建新的仓库。
在以下的这些示例中,exampleuser/new-repository 或 exampleuser/mirrored 是镜像。

镜像存储库

  1. 创建存储库的裸克隆
1
$ git clone --bare https://github.com/EXAMPLE-USER/OLD-REPOSITORY.git
  1. 镜像推送到新的存储库
1
2
$ cd OLD-REPOSITORY.git
$ git push --mirror https://github.com/EXAMPLE-USER/NEW-REPOSITORY.git
  1. 删除前面创建的临时本地存储库
1
2
$ cd ..
$ rm -rf OLD-REPOSITORY.git
  1. 拉取新存储库或者更改本地项目的源为新的存储库的地址
1
$ git remote set-url origin https://github.com/EXAMPLE-USER/NEW-REPOSITORY.git

其它方式

由于上面的方式可以满足目前已有的迁移需求,其它方式可参考文档

前言

项目中一直用的都是 webpack,前一段需要开发几个类库, 看过很多开源库源码都是用 rollup。这次通过开发类库,于是就快速上手了 rollup。

定位

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。

与 Webpack 偏向于应用打包的定位不同,rollup.js 更专注于 Javascript 类库打包。

我们熟知的 Vue2、React 等诸多知名框架或类库都是通过 rollup.js 进行打包的。

常用配置

入口文件

Rollup 既然是打包类库文件,那么它的入口也就只能是 JS 文件了(通过第三方插件可以支持 Html,这里不作展开),因此我们新建一个 main.js 作为入口文件,打包出来的文件我们命名为 bundle.js,我们可以简单的通过命令行进行打包:

1
2
3
4
5
# 针对浏览器环境打包
rollup main.js --file bundle.js --format iife

# 针对Nodejs环境打包
rollup main.js --file bundle.js --format cjs

推荐使用配置文件进行打包:

1
2
3
4
5
# 默认使用rollup.config.js配置文件
rollup --config

# 使用自定义my.config.js配置文件
$ rollup --config my.config.js

一个基础的配置文件如下

1
2
3
4
5
6
7
8
// rollup.config.js
export default {
input: "main.js",
output: {
file: "bundle.js",
format: "cjs"
}
};

这里的 format 字段有六种选项:

  • amd: 异步模块定义,用于像 RequireJS 这样的模块加载器
  • cjs:CommonJS,适用于 Node 和 Browserify/Webpack
  • es:ES 模块文件
  • iife:自执行模块,适用于浏览器环境 script 标签
  • umd:通用模块定义,以 amd,cjs 和 iife 为一体
  • system:SystemJS 加载器格式

Rollup 也支持配置多个文件入口,我们新建 foo.js 和 bar.js 两个入口文件:

1
2
3
4
5
6
7
8
9
10
export default {
input: {
foo: './foo.js',
bar: './bar.js',
},
output: {
dir: 'dist',
format: 'cjs',
},
}

这样打包出来的两个文件就放入 dist 中。


插件(plugins)

  • @rollup/plugin-json
    • 可以读取 json 文件
  • @rollup/plugin-commonjs
    • 第三方的模块为了能够直接使用,往往不是 ES6 模块而是用 commonjs 的模块方式编写的,将 commonjs 的模块转化为 ES6 模块
  • @rollup/plugin-node-resolve
    • 帮助 rollup 查找 node_modules 里的三方模块
  • @rollup-plugin-alias
    • 提供 modules 名称的 alias 和 reslove 功能
  • @rollup/plugin-babel
    • 打包的时候使用 babel 编译 js 代码
  • @rollup/plugin-replace
    • 类似 Webpack 的 DefinePlugin, 可在源码中通过 process.env.NODE_ENV 用于构建区分环境.
  • rollup-plugin-typescript2
    • 这是对原始 rollup-plugin-typescript 的重写,这个版本比原始版本慢一些,但是它将打印出打字稿的句法和语义诊断消息(毕竟使用打字稿的主要原因)。使用该插件还有一个重要的原因,该插件能生成 声明文件 首先需要提供基础安装环境, 除了 typescript 基础环境 该插件需要依赖 tslib 去编译 ts 代码
  • rollup-plugin-terser
    • 可以打包压缩 es6 的 js 代码

外链(external)

我们在自己的库中需要使用第三方库,例如 vue 等,又不想在最终生成的打包文件中出现 vue,这个时候我们就需要 external 属性。

  1. 外部依赖的名称
  2. 一个已被找到路径的 ID(像文件的绝对路径)
1
2
3
4
5
6
7
8
9
10
// rollup.config.js
import path from 'path';

export default {
...,
external: [
'some-externally-required-library',
path.resolve( './src/some-local-file-that-should-not-be-bundled.js' )
]
};

全局模块(globals)

Object 形式的 id: name 键值对,用于umd/iife包。例如在这样的情况下

1
import $ from 'jquery';

我们想告诉 Rollup jquery 模块的 id 等同于 $ 变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// rollup.config.js
export default {
...,
format: 'iife',
name: 'MyBundle',
globals: {
jquery: '$'
}
};

/*
var MyBundle = (function ($) {
// 代码到这里
}(window.jQuery));
*/.

Tree Shaking

由于 Rollup 本身支持 ES6 模块化规范,因此不需要额外配置即可进行 Tree Shaking


代码分割

import()实现了按需加载, rollup 内部会进行代码拆分,注意 format 不能用 iife 模式,因为 iife 会把所有模块放在同一个函数中


更多可参考大选项列表

总结

以上就是在开发类库的过程中对 rollup 的简单总结,接下来会进一步剖析 rollup 打包流程和原理。

概述

从我们接触前端开始,每个项目的根目录下一般都会有一个 package.json 文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。

当然大部分人其实并不关心 package.json 的配置,业务中应用的更多的是 dependencies 或 devDependencies 配置。

但当开发库并上传 npm 仓库时,package.json 的配置就至关重要了,如果理解关键属性并加以应用会使得开发和解决问题的效率大大提升。

接下来本文会详细解释一下每个字段的真实含义。

tips:可参考ant-design的 package.json 进行阅读

官方属性

package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。对包的更改应该与对版本的更改一起进行。

1.name

name必须小于等于 214 个字符,不能以.或_开头,不能有大写字母,因为名称最终成为 URL 的一部分因此不能包含任何非 URL 安全字符。 npm官方建议我们不要使用与核心节点模块相同的名称。不要在名称中加jsnode。如果需要可以使用engines来指定运行环境。

该名称会作为参数传递给 require,因此它应该是简短的,但也需要具有合理的描述性。


2.version

version必须可由 node-semver 解析,因为它会与 npm 作为依赖项捆绑在一起。


3.description

description是一个字符串,用于编写描述信息。
有助于在npm库中搜索的时候发现你的模块。


4.keywords

keywords是一个字符串组成的数组,有助于在npm库中搜索的时候发现你的模块。


5.homepage

项目的主页地址


6.bugs

用于项目问题的反馈 issue 地址或者一个邮箱。

1
2
3
4
"bugs": {
"url" : "https://github.com/owner/project/issues",
"email" : "project@hostname.com"
}

7.license

license是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制


8.author & contributors

author是具体一个人,contributors表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name字段和可选的urlemail字段。

1
2
3
4
5
"author": {
"name" : "xxx",
"email" : "xxx@xx.com",
"url" : "https://xxx.com/"
}

也可以写成一个字符串

1
"author": "xxx xxx@xx.com"

9.files

files属性的值是一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)

可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,这个文件的写法与.gitignore类似。


10.main

定义了 npm 包的入口文件,browser 环境和 node 环境均可使用


11.module

定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用

优先级:module > main


12.browser

定义 npm 包在 browser 环境下的入口文件


13.bin

bin项用来指定每个内部命令对应的可执行文件的位置。如果你编写的是一个 node 工具的时候一定会用到 bin 字段。
当我们编写一个 cli 工具的时候,需要指定工具的运行命令,比如常用的 webpack 模块,他的运行命令就是

1
2
3
webpack。"bin": {
"webpack": "./bin/index.js",
}

当我们执行webpack命令的时候就会执行./bin/index.js文件中的代码。

在模块以依赖的方式被安装,如果存在bin选项。在node_modules/.bin/生成对应的文件, Npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。

所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按 tab 键,就会显示所有可以使用的命令。


14.repository

指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助

1
2
3
4
"repository" : {
"type" : "git",
"url" : "https://github.com/npm/npm.git"
}

15.script

scripts 指定了运行脚本命令的 npm 命令行缩写,比如 start 指定了运行 npm run start 时,所要执行的命令。

1
2
3
"scripts": {
"start": "node ./start.js"
}

使用scripts字段可以快速的执行 shell 命令,可以理解为alias
scripts可以直接使用node_modules中安装的模块,这区别于直接运行需要使用npx命令。

1
2
3
4
5
6
"scripts": {
"build": "webpack"
}

// npm run build
// npx webpack

16.config

config字段用于添加命令行的环境变量。

1
2
3
{
"config": { "port": "8080" }
}

然后,在server.js脚本就可以引用 config 字段的值。

1
console.log(process.env.npm_package_config_port) // 8080

用户可以通过 npm config set 来修改这个值。

1
npm config set {name}:port 8000

17.dependencies & devDependencies

dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。

它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。

当安装依赖的时候使用--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性。


18.peerDependencies

有时,你的项目和所依赖的模块,都会同时依赖另一个模块,但是所依赖的版本不一样。比如,你的项目依赖 A 模块和 B 模块的 1.0 版,而 A 模块本身又依赖 B 模块的 2.0 版。

大多数情况下,这不构成问题,B 模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。

最典型的场景就是插件,比如 A 模块是 B 模块的插件。用户安装的 B 模块是 1.0 版本,但是 A 插件只能和 2.0 版本的 B 模块一起使用。这时,用户要是将 1.0 版本的 B 的实例传给 A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果 A 和 B 一起安装,那么 B 必须是 2.0 模块。

peerDependencies 字段,就是用来供插件指定其所需要的主工具的版本。

1
2
3
4
5
6
{
"name": "chai-as-promised",
"peerDependencies": {
"chai": "1.x"
}
}

上面代码指定,安装 chai-as-promised 模块时,主程序 chai 必须一起安装,而且 chai 的版本必须是 1.x。如果你的项目指定的依赖是 chai 的 2.0 版本,就会报错。

注意,从 npm 3.0 版开始,peerDependencies 不再会默认安装了。


19.bundledDependencies

bundledDependencies指定发布的时候会被一起打包的模块.


20.optionalDependencies

如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时npm继续运行,你可以把这个模块依赖放到optionalDependencies配置中。这个配置的写法和dependencies的写法一样,不同的是这里边写的模块安装失败不会导致npm install失败。


21.engines

engines字段指明了该模块运行的平台,比如Node或者npm的某个版本或者浏览器。

1
{ "engines": { "node": ">=0.10.3 <0.12", "npm": "~1.0.20" } }

22.os

可以指定你的模块只能在哪个操作系统上运行

1
"os" : [ "darwin", "linux", "win32" ]

23.cpu

限制模块只能在某种架构的cpu下运行

1
"cpu" : [ "x64", "ia32" ]

24.private

如果这个属性被设置为truenpm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。

1
"private": true

25.publishConfig

这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置 tag 或仓库地址。

通常publishConfig会配合private来使用,如果你只想让模块被发布到一个特定的npm仓库,如一个内部的仓库。

1
2
3
4
5
6
"private": true,
"publishConfig": {
"tag": "1.0.0",
"registry": "https://registry.npmjs.org/",
"access": "public"
}

26.preferGlobal

preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用–global 参数),要不要显示警告,表示该模块的本意就是安装为全局模块。

1
"preferGlobal": false

更多请参考package.json 的官方属性

额外属性

1.types | typings

定义一个针对 TypeScript 的入口文件


2.unpkg

让 npm 上所有的文件都开启 cdn 服务。

1
2
3
{
"unpkg": "dist/antd.min.js"
}

当你使用省略的 url https://unpkg.com/antd 时,便会按照如下的方式获取文件:

1
2
3
4
5
6
7
# [latestVersion] 指最新版本号,pkg 指 package.json

# 定义了 unpkg 属性时
https://unpkg.com/jquery@[latestVersion]/[pkg.unpkg]

# 未定义 unpkg 属性时,将回退到 main 属性
https://unpkg.com/jquery@[latestVersion]/[pkg.main]

3.browserslist

设置项目的浏览器兼容情况。

1
2
3
{
"browserslist": ["> 1%", "last 2 versions"]
}

4.webpack

1
2
3
{
"sideEffects": true | false
}

声明该模块是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。


5.exports

exports 字段提供了一种方法来为不同的环境和 JavaScript 风格公开您的包模块,同时限制对其内部部分的访问。
具体用法请参考官方文档


更多请参考package.json 的额外属性

参考资料

前言

由于业务快速发展,业务组件库也在快速迭代。当组件Props等发生变化时,开发人员需要额外的消耗一定精力去保持代码和文档的统一。
此时,我们就可以利用TypeScript的静态类型检查与代码提示能力,通过自动生成文档工具,来增强开发的生产力,解放双手,提高工作效率。

背景

团队内部组件库文档年久失修,组件规范不统一,导致对很多组件的修改是牵一发而动全身,痛定思痛,于是经过小组商议后决定从组件文档着手,逐 步统一业务组件风格规范和组件文档。

调研

通过调研以及过往经验,发现市面上有几款插件可供选择,分别是Docz、StoryBook、dumi、react-docgen、react-docgen-typescript,根据与目前项目匹配度最终dumi和react-docgen-typescript进入了决赛圈,最终根据灵活的和对目前的项目整体的副作用,选择了react-docgen-typescriptreact-styleguidist配合生成文档。

dumi

dumi,中文发音嘟米,是一款为组件开发场景而生的文档工具,与 father 一起为开发者提供一站式的组件开发体验,father 负责构建,而 dumi 负责组件开发及组件文档生成。

  • 特性
    • 开箱即用,将注意力集中在组件开发和文档编写上
    • 基于 TypeScript 类型定义,自动生成组件 API
    • 丰富的 Markdown 扩展,不止于渲染组件 demo
    • 支持移动端组件库研发,内置移动端高清渲染方案
    • 一行命令将组件资产数据化,与下游生产力工具串联

react-docgen

  • 来自Facebook开源
  • 基于Babel解析源码,对propTypes支持良好
  • 虽然新版本支持 TypeScript,但从其它文件导入的类型信息无法被获取
  • 不解析JSDoc部分,整个注释都作为描述部分,不过可以添加自己handler来补充解析

react-docgen-typescript

  • 来自styleguidist开源,主要目标是服务TS React组件的API文档生成
  • 基于TS解析源码,不支持propTypes,Props interface继承的类型都可以拿到
  • 会读取JSDoc的@type、@default作为类型和默认值信息

业务实践阶段

安装

1
2
3
4
5
6
7
8
9
10
11
// package.json
"devDependencies": {
"react-docgen-typescript": "^1.9.0",
"react-styleguidist": "^7.3.7",
}

// 可根据配置文件,自行设置、执行启动和构建命令
"scripts": {
"styleguide:dev": "cross-env STYLEGUIDE_ENV=development styleguidist server --config styleguide.config.js",
"styleguide:build": "cross-env STYLEGUIDE_ENV=production styleguidist build --config styleguide.config.js"
}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// styleguide.config.js
const path = require('path');
const glob = require('glob');
const STYLEGUIDE_ENV = process.env.STYLEGUIDE_ENV

module.exports = {
title: 'components',
styleguideDir: 'dist',
components: function () {
return glob.sync(path.resolve(__dirname, 'src/componets/**/*.tsx'))
.filter(function (module) {
return /\/[A-Za-z]\w*\.tsx$/.test(module);
});
},
resolver: require('react-docgen').resolver.findAllComponentDefinitions,
propsParser: require('react-docgen-typescript').withDefaultConfig({ propFilter: { skipPropsWithoutDoc: true } }).parse,
webpackConfig: Object.assign({}, STYLEGUIDE_ENV === 'production' ? require('./config/styleguide.webpack.config.prod') : require('./config/styleguide.webpack.config.dev')),
dangerouslyUpdateWebpackConfig: function (config, env) {
return {
...config,
output: {
...config.output,
filename: 'build/[name].bundle.js', // 默认是生成hash,公司部署平台要求入口文件不带hash
chunkFilename: 'build/[name].js',
},
}
}
};

组件内部配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from 'react';

// 在原有interface基础上,每个属性添加上jsdoc即可

/**
* Select properties.
*/
export interface ISelectProps {
/**
* 传入的数据源,可以动态渲染子项
*/
dataSource: string[];
/**
* Select发生改变时触发的回调
*/
onChange?: (item: string) => void;
/**
* 是否只读,只读模式下可以展开弹层但不能选
*/
readOnly?: boolean;
/**
* 选择器尺寸
*/
size?: 'small' | 'medium' | 'large';
/**
* 当前值,用于受控模式
*/
value?: string | number;
}

/**
* Component is described here.
*
* @visibleName Select
* @version
* @author
*/
class Select extends React.Component<ISelectProps> {
static defaultProps = {
readOnly: false,
size: 'medium',
};

render() {
return <div>Test</div>;
}
}
export default Select;

部署

执行build命令后,会生成dist文件,可根据自己的需求部署到指定位置。

总结

TypeScript给js带来了类型,做到静态类型检查和代码提示,经过上面一顿的折腾,把类型又用了一遍。整个过程中,我们已经拿到了组件中的所有想要拿到的数据并生成文档,不足的是子属性的类型并不能很好的显示,还需要后续完善,但是已经满足基础的需求。

diff算法大致流程

  • vue2参考snabbdom实现vdom和diff,vue3重写了vdom,优化了性能
  • 用js模拟dom结构,新旧vnode对比,得出最小的更新范围,最后更新dom
  • 只比较同一层级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag和key都相同,则认为是相同节点, 继续去比较子节点

vdom结构

1
2
3
4
5
6
7
8
9
10
11
vnode (
sel: string | undefined, // 标签 selector div#container.two
data: any | undefined, // 属性 onCLick style ...
children: Array<VNode | string> | undefined, // 子元素
text: string | undefined, // 文本
elm: Element | Text | undefined // 对应的dom节点
): VNode {
// key 唯一标识
let key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}

一些判断方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : '';
const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等
// 都不传key undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
function isUndef (s: any): boolean { return s === undefined; }
function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }

hook

hook名称 触发时间 回调参数
pre patch开始 none
init vnode被添加的时候 vnode
create DOM元素被从create创建 ode, vnode
insert 一个元素被插入了DOM vnode
prepatch 元素即将被patch oldVnode, vnode
update 元素被更新 oldVnode, vnode
postpatch 元素被patch后 oldVnode, vnode
destroy 元素被直接或者间接移除 vnode
remove 元素直接从DOM被移除 vnode, removeCallback
post patch操作结束 none

核心算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// patch函数 通过.init()函数返回patch函数
// oldVnode可能使一个dom(第一次进来)或者旧的vdom
// vnode是一个新的vdom
patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 执行 pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 第一个参数不是 vnode
if (!isVnode(oldVnode)) {
// 创建一个空的 vnode ,关联到这个 DOM 元素
oldVnode = emptyNodeAt(oldVnode);
}

// 相同的 vnode(key 和 sel 都相等)
if (sameVnode(oldVnode, vnode)) {
// vnode 对比
patchVnode(oldVnode, vnode, insertedVnodeQueue);

// 不同的 vnode ,直接删掉重建
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm);

// 重建
createElm(vnode, insertedVnodeQueue);

if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
}

function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);

// 设置 vnode.elem
const elm = vnode.elm = oldVnode.elm!;

// 旧 children
let oldCh = oldVnode.children as VNode[];
// 新 children
let ch = vnode.children as VNode[];

if (oldVnode === vnode) return;

// hook 相关
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}

// vnode.text === undefined (vnode.children 一般有值, 因为text和children不能共存)
if (isUndef(vnode.text)) {
// 新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 新 children 有,旧 children 无 (旧 text 有)
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 旧 child 有,新 child 无
} else if (isDef(oldCh)) {
// 移除 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 旧 text 有
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}

// else : vnode.text !== undefined (vnode.children 一般无值)
} else if (oldVnode.text !== vnode.text) {
// 移除旧 children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新 text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}

// oldVnode.children 和 vnode.children 对比
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 进行null的验证,避免空值
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];

// 开始和开始对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];

// 结束和结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];

// 开始和结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];

// 结束和开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];

// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string];

// 没对应上
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];

// 对应上了
} else {
// 对应上 key 的节点
elmToMove = oldCh[idxInOld];

// sel 是否相等(sameVnode 的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);

// sel 相等,key 相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}

Web 标准是由各大标准组织制定,由浏览器和其他 Web 底层框架或工具来实现,再提供给开发者能以最小成本开发适用于多平台的 Web 应用,这些标准是我们能访问无数网站的前提。

Web 标准计划

在 Web 发展的早期,浏览器各自为政,技术无一致实现,这直接损害了设计师、开发者、用户和行业的利益。为了解决这些问题,Web 标准计划 (Web Standards Project, WaSP) 于 1998 年成立,目标便是促进核心的 Web 标准的推广,鼓励浏览器对标准的支持,为大家寻求一条简单而便利之路。
得益于前人努力,如今的现代浏览器表现已经越来越一致,进而催生出更多标准,有了这些标准我们可以开发出体验更好的 Web 应用。
这个是所有浏览器相关的技术标准: The Web platform: Browser technologies,从中能了解到健全发展的 Web 技术生态。

Web 标准组织

W3C

W3C 组织为 Web 开发领域提出了很多建议,比如为 XHTML、XML、DOM、CSS 和 Web API 等技术实现提出了建议。你可能会注意到为什么说是提出建议,而不是标准呢?那是因为 W3C 自认为不是标准组织,他们只是组织了 Web 相关领域的专家,这些专家组成一支工作小组,工作小组就如何实现 Web 技术提出建议。尽管 W3C 对其建议的实现方案没有任何强制权力,但他们大多数的建议都被视为事实上的标准。
W3C 组织关注 DOM、CSS、HTTP、媒体、性能、安全、图形学、可访问性和用户隐私等方方面面的技术,在这里可以搜索相关技术: All Standards and Drafts。
从 W3C 组织成员的工作手册可以看到,一项技术从提出到成为标准,需要经过 4 个阶段。

WD (Working Drafts):草案阶段
CR (Candidate Recommendation):候选阶段
PR (Proposed Recommendation):提议阶段
REC (W3C Recommendation):正式建议阶段

WHATWG

WHATWG 工作小组成立于 2004 年,起因是 W3C 组织对 HTML 不再感兴趣,转而关注 XHTML 技术,部分 W3C 成员对此行为不满,因此他们决定建立一个新组织推动 HTML 发展,制定相关标准。如今 HTML5 技术能发展起来,也是得助于 WHATWG 小组。
WHATWG 小组因 HTML 而生,负责的 Web 标准主要是 HTML 相关技术,也涉猎一些 Web API,比如: HTML、DOM、浏览器兼容性、XHR、Fetch、Storage 和 URL 等标准
WHATWG 组织没有明确说明,一项技术成为标准要经过哪些阶段,他们实行的是现行标准 (Living Standard),标准由相关负责人维护升级,并由开发者或浏览器厂商提议将新功能加入标准,这一协作过程通过 Github 的 Issues 来讨论。
WHATWG 和 W3C 制定的标准会有一些重叠,比如 DOM 标准。有一些标准会在开头说明:“该标准已经不由我们来维护,请查看某某组织的最新标准”。

ECMA

ECMA 组织负责很多与信息化相关的技术标准,其中应用最广的就是 TC39 委员会负责的 ECMAScript 标准,这标准的实现就是 JavaScript。

对 ECMAScript 标准的更新,需要经过 5 个阶段。
Strawman (Stage 0):提案纳入考虑中Proposal (Stage 1):明确提案的好处,以及可能带来的风险
Draft (Stage 2):使用正式的规范语言描述语法和语义
Candidate (Stage 3):根据使用者反馈进行改良
Finished (Stage 4):准备正式加入 ECMAScript 标准

khronos

khronos 是一个由成员资助的,专注于制定开放标准(Open standard)的行业协会,重点制定免费的API,使在各种平台和设备上创作或播放的多媒体可以得到硬件加速
主要负责OpenGL,WebGL等方面标准的指定

IETF

IETF (The Internet Engineering Task Force) 组织主要负责制定互联网基础架构的标准,比如 TCP/IP 和 FTP 协议。

总结

对一般 Web 开发来说,我们用不上晦涩难懂的标准文档。但学习标准我们可以收获很多,也可以获取到第一手的学习资源,了解技术的发展前沿,全面深入地理解相关技术。

web安全之前端安全

概述

随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点。 在移动互联网时代,前端人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇网络劫持、非法调用 Hybrid API 等新型安全问题。当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要前端技术人员不断进行“查漏补缺”。

常见的攻击方式

XSS(跨站脚本攻击 Cross-Site Scripting)

  • 反射型
    • 漏洞原理
      • 应用程序或API包括未经验证和未转义的用户输入,直接作为HTML输出的一部分。
      • 一个成功的攻击可以让攻击者在受害者额的浏览器中执行任意的HTML和javascript
    • 特点
      • 非持久化,必须用户点击带有特定参数的链接才能引起
      • 影响范围是仅执行脚本的用户
    • 攻击步骤
      • 攻击者构造出特殊的 URL,其中包含恶意代码。
      • 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
      • 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
      • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
    • 防御措施
      • 危害相对较小,多为一次性点击触发,对于用户端来说陌生的链接不要打开

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

  • 存储型
    • 漏洞原理

      • 程序通过web请求获取不可信赖的数据,在未检校是否存在XSS代码的情况下,便将其存入数据库
      • 当下次从数据库中获取该数据时程序未为对其进行过滤,页面再次执行XSS代码
      • 可以持续攻击用户
    • 攻击步骤:

      • 攻击者将恶意代码提交到目标网站的数据库中。
      • 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
      • 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
      • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
    • 防御措施

      • 这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等
      • 针对用户的输入合理验证,对特殊字符(< 、>、’、”等)以及script javascript等进行过滤
      • 对数据输出HTML上下文中不同位置进行恰当的输出编码
      • 设置httponly 属性 避免攻击者利用跨站脚本漏洞进行cookie劫持
  • DOM型
    • 漏洞原理

      • 基于DOM,文档对象模型的一种漏洞,也是一种特殊的反射型XSS
      • 通过JS操作DOM树,动态的输出数据到页面,而不依赖将数据提交给服务端
    • 攻击步骤:

      • 攻击者构造出特殊的 URL,其中包含恶意代码。
      • 用户打开带有恶意代码的 URL。
      • 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
      • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
    • 案例

    • 防御措施

      • 在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。
      • 如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTMLouterHTML 的 XSS 隐患。
      • DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

三种类型的比较

  • 反射型 跟存储型 的区别是:存储型 的恶意代码存在数据库里,反射型 的恶意代码存在 URL 里。
  • DOM型和反射型相同点:都是没有控制好输入,并且把JavaScript脚本输入作为输入 插入到HTML页面
  • DOM型和反射型不同点:反射型是通过后端后,页面引用后端输出后生效。DOM 是经过JS对DOM树直接操作后插入到页面

小游戏

CSRF(跨站请求伪造 cross-site request forgery)

  • 漏洞原理

    • 数据包中的cookie的值是浏览器从本地存储中取出的,并自动填充到数据包中
    • 如果攻击者控制了受害者的浏览器并窃取了cookie
    • 浏览器会自动完成cookie的填充,目标网站会误认为该数据包就是管理员发送的,会以受害者的权限进行操作
  • 案例 & 攻击步骤

    • 受害者登录a.com,并保留了登录凭证(Cookie)。
    • 攻击者引诱受害者访问了b.com。
    • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
    • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
    • a.com以受害者的名义执行了act=xx。
    • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
  • 防御措施

    • Token

      • Token - 是由web应用程序添加到数据包中
  • HTTP Referer

    • 站点可以对一些敏感操作限制其Referer字段的值

第二周 | 浏览器工作原理

1. 浏览器工作原理总论

我们看到的页面都是一个图片形式,专业点的说法叫做位图(Bitmap),然后经过显卡转换为我们可以识别的光信号。

整个的过程就是从 URL 转换为 Bitmap 的过程,先发送请求到服务器,然后服务器返回 HTML,浏览器解析 HTML,然后构建 DOM 树,计算 CSS 属性,然后进行排版,最后渲染成位图,然后经过操作系统或硬件的 API 完成视图的显示

2. 状态机

  • 有限状态机
    • 每一个状态都是一个机器
      • 在每个机器里都可以做计算、存储、输出
      • 所有的机器接受的输入时一致的
      • 状态机的每一个机器本省没有状态,如果我们用函数来表示的话,应该是纯函数(无副作用)
    • 每一个机器都知道下一个状态
      • 每个机器都有确定的下一个状态(Moore)
      • 每个机器根据输入决定下一个状态(Mealy)
  • 之前看winter老师的课,讲到状态机,并没有怎么理解,但是通过这次winter老师的讲解以及使用和不适用状态机堆字符进行处理后,对状态机的概念和简单的运用都有了理解

3. HTTP

  • HTTP协议解析

    • ISO-OSI七层网络模型

      对应
      应用层 HTTP
      表示层 HTTP
      会话层 HTTP
      传输层 TCP / UDP
      网络层 Internet
      数据链路层 4G/5G/Wi-Fi
      物理层 4G/5G/Wi-Fi
  • HTTP请求的实现

    • HTTP请求总结
      • 设计一个HTTP请求的类
      • content type 是一个必要字段 要有默认值
      • body是key value 格式
      • 不同的content-type影响body的格式
    • 发送请求 && send函数的编写
      • 设计支持已有的connection或者自己新建connection
      • 收到数据传递给parser
      • 根据parser的状态resolve Promise
    • response解析
      • Response必须分段构造,用ResponseParse来装配
      • ResponseParse分段处理ResponseText,我们用状态机来分析文本的结构
    • response body 解析
      • Response的body可能根据content-type有不同的结构,采用子parser的结构来解决问题
      • 以TrunkedBodyParser为例,同样适用状态机来处理body的格式

一、前言

Hexo是一个快速、简洁且高效的博客框架,最近有搭建个人博客的想法,便动手尝试了。

二、初始化

1.安装Node.js

Node.js官网下载相应的安装包,一路安装即可。

2.安装Git

Mac下安装Xcode就自带Git。

3.注册Github账号并新建仓库

注册过程就不多说了,注册完成之后需要新建一个仓库。需要注意的是新创建的仓库的名字,必须是username.github.io。例如我的username是rile,那么新创建的仓库的名字便是rile.github.io。

4.配置SSH Key

这一步不是必须的,配置SSH Key的话之后每次更新博客就不用都输入用户名和密码,可以方便一些。

以下是详细配置过程。
(1)检查本机上是否已经存在SSH Key
打开终端,输入如下命令

1
2
cd .ssh
ls -la

检查终端输出的文件列表中是否已经存在id_rsa.pub 或 id_dsa.pub 文件,如果文件已经存在,则直接进入第三步。

(2)创建一个SSH Key
在终端输入如下命令

1
ssh-keygen -t rsa -C "your_email@example.com"

按下回车,让你输入文件名,直接回车会创建使用默认文件名的文件(推荐使用默认文件名),然后会提示你输入两次密码,可以为空。

(3)添加SSH Key到Github

如果你没有指定文件名(也就是使用的默认文件名),那么你的.ssh文件夹下,应该有一个id_rsa.pub文件了,打开该文件,复制里面的文本。然后登录Github,点击右上角头像右边的三角图标,点击Settings,然后在左边菜单栏点击SSH and GPG keys,点击New SSH key,Title 随便填一个,在Key栏填入你复制的内容,点击Add SSH key,就添加成功了。

(4)检验SSH Key是否配置成功
在终端输入如下命令

1
ssh -T git@github.com

如果出现

1
Are you sure you want to continue connecting (yes/no)?

请输入yes再按回车。

如果最后出现

1
Hi username! You've successfully authenticated, but GitHub does not provide shell access.

就说明你的SSH Key配置成功了。

5.安装Hexo

前面我们已经安装了Node.js,这里我们使用npm命令来安装Hexo

1
npm install -g hexo

等待一会就会完成下载安装。

接着在任意位置创建一个文件夹,如Blog,cd到该路径下执行以下命令

1
hexo init

该命令会在目标文件夹内建立网站所需要的所有文件。接下来是安装依赖包

1
npm install

到这里本地博客就搭建好了。执行以下命令(在你博客的对应文件夹路径下)

1
2
hexo generate
hexo server

在浏览器输入http://localhost:4000/ 就可以进行查看了。
当然这个博客是本地的,别人是无法访问的,之后我们需要部署到GitHub上。

6.同步本地博客到Github

编辑自己创建的本地博客文件夹中的_config.yml中的deploy节点

1
2
3
4
deploy:
type: git
repo: git@github.com:username/username.github.io.git
branch: master

注意:repo为这种形式的是配置了SSH Key之后的,如果没有配置则使用Https形式的地址。

为了能够使Hexo部署到GitHub上,需要安装一个插件

1
npm install hexo-deployer-git --save

然后输入以下命令

1
2
3
hexo clean
hexo generate
hexo deploy

在浏览器输入username.github.io就可以访问你的博客了。

三、配置主题

Hexo主题在Github上有很多,如

挑选自己喜欢的一款。当然,自己能够DIY也是极好的。

四、发布新文章

执行以下命令

1
hexo new post '文章标题'

这样会在本地博客的source->_posts路径下看到新建的文章,是md格式的,推荐使用Typora进行编辑。

编辑完成之后,与上面一样,执行以下命令

1
2
3
hexo clean
hexo generate
hexo deploy

即可更新到Github上。

最后晒上我的博客地址:https://rile14929.github.io/
祝大家也早日拥有自己的博客~

参考链接