前端必备知识库之ES6篇

声明 let、const

解构赋值

声明类与继承:class、extend

Promise的使用与实现

generator(异步编程、yield、next()、await 、async)

箭头函数this指向问题、拓展运算符

map和set有没有用过,如何实现一个数组去重,map数据结构有什么优点?

ES6怎么编译成ES5,css-loader原理,过程

ES6转成ES5的常见例子

使用es5实现es6的class

缓慢补充中~

前端必备知识库之Js篇

原型/原型链/构造函数/实例/继承

有几种方式可以实现继承

用原型实现继承有什么缺点,怎么解决

arguments

数据类型判断

作用域链、闭包、作用域

Ajax的原生写法

对象深拷贝、浅拷贝

图片懒加载、预加载

实现页面加载进度条

this关键字

函数式编程

手动实现parseInt

为什么会有同源策略

怎么判断两个对象是否相等

事件模型

事件委托、代理

如何让事件先冒泡后捕获

window的onload事件和domcontentloaded

for…in迭代和for…of有什么区别

函数柯里化

call apply区别,原生实现bind

call,apply,bind 三者用法和区别:角度可为参数、绑定规则(显示绑定和强绑定),运行效率、运行情况。

async/await

立即执行函数和使用场景

设计模式(要求说出如何实现,应用,优缺点)/单例模式实现

iframe的缺点有哪些

数组问题

数组去重

数组常用方法

查找数组重复项

扁平化数组

按数组中各项和特定值差值排序

BOM属性对象方法

服务端渲染

垃圾回收机制

eventloop

进程和线程

任务队列

如何快速让字符串变成已千为精度的数字

缓慢补充中~

前端必备知识库之CSS篇

盒模型

盒子模型有两种,标准模型和IE盒模型

标准模型
IE模型

标准模型:宽高部分只包含content部分
IE模型: 宽高包含content + padding + border

flex

css单位

css选择器

bfc 清除浮动

层叠上下文

常见页面布局

响应式布局

css预处理,后处理

css3新特性

animation和transiton的相关属性

animate和translate

display哪些取值

相邻的两个inline-block节点为什么会出现间隔,该如何解决

meta viewport 移动端适配

CSS实现宽度自适应100%,宽高16:9的比例的矩形

rem布局的优缺点

画三角形

1像素边框问题

缓慢补充中~

浏览器缓存中的HTTP缓存

缓存是一把双刃剑,既能帮你,也会害你。正确地使用缓存策略,能够让用户访问网页的速度大幅提升。

cache

浏览器缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。

缓存带来的优点

  • 减少网络请求延迟,提高页面访问速度
  • 降低服务器请求压力
  • 减少网络带宽的消耗,比如资源存放cdn的情况,减少带宽的使用,有效降低成本。

HTTP缓存

常见的HTTP缓存只能缓存 GET 响应,对于其他类型的响应比如 (post, put, delete) 则无能为力。

强缓存

强缓存是指通过使用Http的响应头 Expires 或 Cache-Control 对资源进行缓存有效期进行设置。两者都是通过控制资源的有效期来实现缓存策略,当对应的资源在有效期内命中,浏览器则不向后端发送通信,直接使用缓存策略命中的资源,一般浏览器会显示 from dist cache 和 from memory cache 两种。

这两者该如何去使用呢?有什么区别?

Expires

Expires是Http 1.0的产物,其实用法是后端在Http的响应头设置如下:

1
expires: Apr, 22 2019 23:58:12 GMT

其中是通过设置了一个时间戳,浏览器下次进行资源获取的时候会对比当前时间和此时间戳,如果当前时间小于该时间戳,就表示缓存仍在有效期中命中。如果要让缓存失效,则配置如下:

1
expires: -1

由以上可发现问题,Expires依赖客户端本地时间,如果客户端的本地时间错误,就会让缓存策略失效,于是Http 1.1就新增了 Cache-Control 来替代 Expires。

Cache-Control

Cache-Control 可配置的字段比较多,我们逐个来了解

no-cache: 强制确认缓存,浏览器获取资源时,浏览器都要向服务器进行通信验证缓存是否命中。

no-store: 禁止进行缓存, 强制浏览器在任何情况下都不要保留任何副本,也就是不进行任何缓存策略。

public: 公共缓存,任何路径的缓存者(CDN、代理服务器),可以无条件的缓存改资源。

private: 私有缓存,只针对单个用户或者实体(不同用户、窗口)缓存资源。

max-age=: 通过设置时间长度 max-age,表示距离请求发起的时间的秒数,判断该资源在此时间范围内以内是否命中,避免时间戳带来的问题。

s-maxage=: s-maxage 优先级高于 max-age,s-maxage 用于表示 cache 服务器上(比如 CDN)的缓存的有效时间,并只对 public 缓存有效。

可见 Cache-Control 的功能比 Expires 完善,而且优先级更高。

协商缓存

协商缓存依赖于浏览器于服务器端进行通信,浏览器需要向服务器询问当前资源是否命中缓存有效期。当请求讯问的资源命中缓存,则服务器会返回Http响应码304务端告知浏览器缓存资源命中,请求会被重定向到浏览器缓存。

Last-Modified

服务器在首次返回资源的响应中通过设置响应头 Last-Modified 当前的时间戳

1
Last-Modified: Apr, 22 2019 23:58:12 GMT

浏览器下次重新获取资源时与服务器进行通信进行协商缓存命中确认,会带上请求头 If-Modified-Since

1
If-Modified-Since: Apr, 22 2019 23:58:12 GMT

服务器接收到 If-Modified-Since 的时间戳后,比对次时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,则命中缓存,返回 304 响应,并且响应头不会再添加 Last-Modified 字段。

Last-Modified 存在一定的弊端,一个是,当编辑后的资源内容没有改变,但服务器端会认为资源被更新,因为其判断是根据文件最后编辑时间。另一个是,当修改文件速度很快,而 If-Modified-Since 的时间戳和与最后修改时间的差距无法被计算出来,所以会认为资源没有进行更新。

Etag

服务器首次返回资源设置响应头 ETag

1
ETag: W/"2123-420d462f951"

浏览器下次重新获取资源时与服务器进行通信进行协商缓存命中确认,会带上请求头 If-None-Match

1
If-None-Match: W/"2123-420d462f951"

服务器接收到 If-None-Match 的资源标识后,比对当前服务器资源的标识和 If-None-Match 的资源标识是否一样,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 ETag 值;否则,则命中缓存,返回 304 响应,并且响应头不会再添加 ETag 字段。

Etag通过对资源进行内容的HASH计算,生成资源的标识。这样可以有效的避免 Last-Modified 的弊端,但 Etag 依赖于服务器端进行计算,会影响服务器性能。

最后用一张图标总结所有:

cache-process

2019我和前端有一个约定

规范简介

该规范目的是保证前端代码的整洁性和代码风格一致性,提高代码的可读性和可维护性。

项目结构

项目命名

全部采用小写方式,使用中划线 - 进行分隔

目录结构规范

1
2
3
4
5
6
7
8
9
---
|-- src
| |-- libs // 公用的库文件,例如:主题样式,鉴权插件,定制化的图表库
| |-- assets // 资源目录,比如png,jpg,svg图片等
| | |-- images
| | |-- svg // svg的图片单独放一个目录
| |-- components // 通用的自定义组件
| |-- pages // 页面
|-- config // webpack等前端配置文件

目录文件命名规范(推荐2种)

  • 目录和文件都使用小写单词并使用中划线 - 进行分隔
  • 目录和文件使用小写单词并使用中划线 - 进行分隔,唯独.vue组件使用大驼峰进行命名

    package.json管理

  • 对于项目中代码需要依赖引入的使用

    1
    $ npm i vue -S
  • 对于项目中开发环境下进行运行调试压缩编译的使用

    1
    $ npm i wepack -D
  • 对于npm包的管理一定要规范,不需要引入的不要随便安装并写入package.json文件,当需要引入新的依赖需要告知所有的项目成员。

  • npm的依赖包的版本需要谨慎操作,不要随便进行任意升级。中间版本号和小版本号可以进行定期升级,并commit对应的修改以及通知项目成员。而大版本号需要确定其版本是否兼容目前使用的版本并进行仔细的测试才可以提交修改。

    HTML书写规范

    语法格式

  • 使用2个空格进行缩进
  • 嵌套的节点应该使用缩进
  • 节点属性使用双引号,而不用单引号
  • 非自动闭合的HTML元素一定要使用斜线进行闭合!!!,自动闭合HTML元素则可选, 举例:

    1
    <img src="images/logo.png" alt="logo">
  • 对于HTML 元素属性大于3个,推荐使用如下方式(适用于vue模板),避免代码横向增长:

    1
    2
    3
    4
    5
    <i class="switch-btn"
    :class="switchBtnIcon"
    :style="switchBtnLeftDistance"
    @click="changeVisible">
    </i>

语义化

  • 根据使用场景选择正确的 HTML 元素。例如,使用 h1 元素创建标题,p 元素创建段落,a 元素创建链接,i 元素创建图标等等。正确的使用 HTML 元素对于可访问性、可重用性以及编码效率都很重要。

    资源引入

  • 引入资源使用相对路径,不要指定资源所带的具体协议 ( http:,https: ) ,除非这两者协议都不可用。
    示例:
    1
    <script src="//ticket1000-1253841380.file.myqcloud.com/assets/release/vue/vue.js"></script>

优化Dom结构

  • 尽量避免多余的父节点,通过迭代和重构来使HTML元素变得更少。

    属性顺序

  • class
  • id
  • name
  • data-*
  • src,for,type,href,value,max-length,max,min,pattern
  • placeholder,title,alt
  • aria-*,role
  • required,readonly,disabled

    CSS书写规范

    命名

  • 类名使用小写字母,以中划线分隔
  • id采用驼峰式命名
  • less, scss中的变量、函数、混合、placeholder采用驼峰式命名

    语法

  • 每个属性声明末尾都要加分号
  • 去掉小数点前面的0
  • 去掉数字中不必要的小数点和末尾的0
  • 属性值’0’后面不要加单位
  • 用border: 0;代替border: none;
  • 选择器不要超过4层
  • 尽量少用’*’选择器

    需要加空格

  • 属性值前
  • 选择器 ‘>’, ‘+’, ‘~’前后
  • ‘{‘前
  • !important’!’前
  • @else前后
  • 属性值中的’,’后
  • 注释’/‘后和’/‘前

    需要换行

  • ‘{‘后和’}’前
  • 每个属性独占一行
  • 多个规则的分隔符’,’后

    颜色

  • 尽量使用16进制的颜色,当不需要控制透明度,避免使用rgba的写法。
  • 16进制的颜色尽量使用简写代替

    属性顺序

  • 简单示例:

    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
    .order {
    display: block;
    float: right;

    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 100;

    border: 1px solid #e5e5e5;
    border-radius: 3px;
    width: 100px;
    height: 100px;

    font: normal 13px "Helvetica Neue", sans-serif;
    line-height: 1.5;
    text-align: center;

    color: #333;
    background-color: #f5f5f5;

    opacity: 1;
    }
  • 完整顺序:

    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
    [
    [
    "display",
    "visibility",
    "float",
    "clear",
    "overflow",
    "clip",
    "zoom"
    ],
    [
    "position",
    "top",
    "right",
    "bottom",
    "left",
    "z-index"
    ],
    [
    "margin",
    "box-sizing",
    "border",
    "border-radius",
    "padding",
    "width",
    "height",
    ],
    [
    "font",
    "line-height",
    "text-align",
    "vertical-align",
    "white-space",
    "text-decoration",
    "word-spacing",
    "text-overflow",
    "word-wrap",
    "word-break"
    ],
    [
    "color",
    "background",
    "background-color",
    "background-image",
    "background-repeat",
    "background-attachment",
    "background-position",
    "background-clip",
    "background-origin",
    "background-size"
    ],
    [
    "outline",
    "opacity",
    "box-shadow",
    "text-shadow"
    ],
    [
    "transition",
    "transition-delay",
    "transition-timing-function",
    "transition-duration",
    "transition-property",
    "transform",
    "transform-origin",
    "animation",
    "animation-duration",
    "animation-delay",
    "animation-direction"
    ],
    [
    "content",
    "resize",
    "cursor",
    "user-select",
    "tab-size",
    ]
    ]

JavaScript规范

  • 遵循standardjs规范
    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
    // http://eslint.org/docs/user-guide/configuring

    module.exports = {
    root: true,
    parser: 'babel-eslint',
    parserOptions: {
    sourceType: 'module'
    },
    env: {
    browser: true,
    },
    // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
    extends: 'standard',
    // required to lint *.vue files
    plugins: [
    'html'
    ],
    // add your custom rules here
    'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    // allow semi
    'semi': 0,
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
    }
    }

Vue规范

Vue和TypeScript的Webpack4.+尝鲜

banner

静态类型系统能帮助你有效防止许多潜在的运行时错误,而且随着你的应用日渐丰满会更加显著。这就是为什么 Vue 不仅仅为 Vue core 提供了针对 TypeScript 的官方类型声明,还为 Vue Router 和 Vuex 也提供了相应的声明文件

TsConfig配置

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
{
"compilerOptions": {
// ts 文件编译成 js 文件的时候,同时生成对应的 map 文件
"sourceMap": true,
"strict": true,
"strictNullChecks": true,
// 当表达式和申明 类型为any时,是否需要发出警告,设置true,则不警告
"noImplicitAny": true,
// 设置为true时,如果不是函数中的所有路径都有返回值,则提示Error
"noImplicitReturns": true,
// module 用于指定模块的代码生成规则,可以使用 commonjs 、 amd 、 umd 、 system 、 es6 、 es2015 、 none 这些选项。
// 选择commonJS,会生成符合commonjs规范的文件,使用amd,会生成满足amd规范的文件,使用system会生成使用ES6的
// system.import的代码。使用es6或者是es2015会生产包含ES6特性的代码。
"module": "es2015",
"moduleResolution": "node",
// 设置为true时,则允许从没有默认导出的模块中默认导入(也就是不做检查)
"allowSyntheticDefaultImports": true,
// 设置为true,则支持ES7的装饰器特性
"experimentalDecorators": true,
// target 用于指定生成代码的兼容版本,可以从es3,es5,es2015,es6中选择一个,如果不设置,默认生产的代码兼容到es3
"target": "es5"
},
"include": [
"./src/**/*"
]
}

配置参考:

Webpack的基础配置一览

每个项目最重要的一部分个人感觉是webpack的配置,只有配置好webpack部分后续才能顺利进行开发

这里webpack使用了4.+的版本,所以算是体验了较为新的webpack,其中和旧版的有些区别,这里不做介绍

先贴出webpack的配置代码

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
const path = require('path')
const webpack = require('webpack')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
}
}
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
transpileOnly: true,
appendTsSuffixTo: [/.vue$/]
}
}
]
},
resolve: {
extensions: ['.ts', '.js', '.vue', '.josn'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
devServer: {
contentBase: './public',
host: 'localhost',
port: '8080',
open: true,
hot: true,
inline: true,
historyApiFallback: true,
noInfo: true
},
performance: {
hints: false
},
devtool: '#eval-source-map',
plugins: [
new VueLoaderPlugin()
]
}

if (process.env.NODE_ENV === 'production') {
module.exports.devtool = '#source-map'
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
])
} else {
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.HotModuleReplacementPlugin()
])
}

注意点:

  • vue-loader v15需要在webpack插件中添加VueLoaderPlugin插件

  • webpack4.+需要指定mode,开发模式还是生产模式

  • 注意ts-loader的配置

这里只是简单进行webpack配置,没有太完整地根据完整的项目来进行配置,只是简单配置了生产环境下的代码混淆压缩,以及对应的开发服务器和热更新等,有需要其他功能扩展的自行配置。

Vue环境搭建配置

vue-shims.d.ts的添加

这个是比较重要的一个配置,该文件需要放到vue的入口文件中,具体的d.ts代码如下:

1
2
3
4
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

目的是让ts能够识别到vue的静态类型

vue的入口文件

index.ts:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
// vuex部分
import store from './store'

new Vue({
el: '#app',
store,
render: h => h(App),
})

入口文件跟普通的js写法没有太多的区别,只是文件类型为ts。

开始写vue的单文件页面和组件

单文件页面模板

1
2
3
4
5
6
7
8
9
10
11
<template>
...
</template>

<script lang="ts">
...
</script>

<style>
...
</style>

主要是在script项中把lang写为ts类型

使用装饰器来实现组件和页面

这里我们主要使用两个装饰器库vue-property-decorator 和 vuex-class, vue-property-decorator其是基于vue-class-得component的基础扩展修改的。

  1. 大致了解一下vue-property-decorator的装饰器的用法

一共有七个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (exported from vue-class-component)

这里使用vue-property-decorator的例子来做解析

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
import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

const s = Symbol('baz')

@Component
export class MyComponent extends Vue {

@Emit()
addToCount(n: number){ this.count += n }

@Emit('reset')
resetCount(){ this.count = 0 }

@Inject() foo: string
@Inject('bar') bar: string
@Inject({from: 'optional', default: 'default'}) optional: string
@Inject(s) baz: string

@Model('change') checked: boolean

@Prop()
propA: number

@Prop({ default: 'default value' })
propB: string

@Prop([String, Boolean])
propC: string | boolean

@Provide() foo = 'foo'
@Provide('bar') baz = 'bar'

@Watch('child')
onChildChanged(val: string, oldVal: string) { }

@Watch('person', { immediate: true, deep: true })
onPersonChanged(val: Person, oldVal: Person) { }
}

相当于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
49
50
51
52
53
54
55
56
57
58
59
60
const s = Symbol('baz')

export const MyComponent = Vue.extend({
name: 'MyComponent',
inject: {
foo: 'foo',
bar: 'bar',
'optional': { from: 'optional', default: 'default' },
[s]: s
},
model: {
prop: 'checked',
event: 'change'
},
props: {
   checked: Boolean,
   propA: Number,
propB: {
type: String,
default: 'default value'
},
propC: [String, Boolean],
},
data () {
return {
foo: 'foo',
baz: 'bar'
}
},
provide () {
return {
foo: this.foo,
bar: this.baz
}
},
methods: {
addToCount(n){
this.count += n
this.$emit("add-to-count", n)
},
resetCount(){
this.count = 0
this.$emit("reset")
},
onChildChanged(val, oldVal) { },
onPersonChanged(val, oldVal) { }
},
watch: {
'child': {
handler: 'onChildChanged',
immediate: false,
deep: false
},
'person': {
handler: 'onPersonChanged',
immediate: true,
deep: true
}
}
})

相信通过以上的例子我们很容易就看出各个装饰器如何去使用,这里就不再做太多的解释。

  1. 再看一下vuex-class的使用方法

同样举例官方的使用列子

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
import Vue from 'vue'
import Component from 'vue-class-component'
import {
State,
Getter,
Action,
Mutation,
namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
@State('foo') stateFoo
@State(state => state.bar) stateBar
@Getter('foo') getterFoo
@Action('foo') actionFoo
@Mutation('foo') mutationFoo
@someModule.Getter('foo') moduleGetterFoo

@State foo
@Getter bar
@Action baz
@Mutation qux

created () {
this.stateFoo // -> store.state.foo
this.stateBar // -> store.state.bar
this.getterFoo // -> store.getters.foo
this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
this.moduleGetterFoo // -> store.getters['path/to/module/foo']
}
}

Vuex的配置

store的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Vuex, { StoreOptions } from 'vuex'
import { RootState } from './modules/types'
import { profile } from './modules/profile'

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
state: {
version: 'v1.0.0'
},
modules: {
profile
}
}

export default new Vuex.Store<RootState>(store);

这里RootState只是用于留空,目的是为了注入全局的store,区别于modules的状态

vuex的modules的配置

  1. 写一个全局类型声明
1
2
3
export interface RootState {
version: string;
}

version字段就是我们刚才在RootState中定义的字段

  1. 定义模板profile

profile模块的类型声明:

1
2
3
4
export interface ProfileState {
firstName: string
lastName: string
}

profile的模块实现:

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
import { RootState } from '../types'
import { Module } from 'vuex'
import { ProfileState } from './types'
import { GetterTree, ActionTree, MutationTree } from 'vuex'
import axios, { AxiosPromise } from 'axios'

const state: ProfileState = {
firstName: '',
lastName: ''
}

const getters: GetterTree<ProfileState, RootState> = {
firstName(state) : string {
return state.firstName
},
lastName(state) : string {
return state.lastName
}
}

const actions: ActionTree<ProfileState, RootState> = {
fetchName({ commit }, id: number): AxiosPromise<ProfileState> {
console.log('action:', id)
return axios.request({
url: 'https://www.apiopen.top/satinCommentApi?id=27610708&page=1'
}).then(res => {
commit('setProfile', {
firstName: 'lin',
lastName: 'guangyu'
})
return res
}).catch(err => {
return err
})
}
}

const mutations: MutationTree<ProfileState> = {
setProfile(state, payload: ProfileState) {
state.firstName = payload.firstName
state.lastName = payload.lastName
}
}

const namespaced: boolean = true;

export const profile: Module<ProfileState, RootState> = {
namespaced,
state,
getters,
actions,
mutations
};

这里我们就完成了Vuex的配置了,就可以结合装饰器对vuex进行调用,而且具有静态类型提示,十分方便。

完成了这一系列的配置我们的尝试已经完成,自己写了个简单的demo,有兴趣可以观看github怎么配置。

React结合TypeScript和Mobx初体验

banner

为什么要使用TypeScript

侦测错误

通过静态类型检测可以尽早检测出程序中隐藏的的逻辑错误,对于JavaScript动态的弱类型语言,虽然灵活性高,但是对于初学者来说,如果不熟悉JavaScript内部的语言机制,很容易造成隐藏的事故。但是通过TypeScript的静态类型检测可以规避这些问题,因为其能够约束变量产生的类型。结合IDE编辑器可以推导变量对应的类型以及内部的结构,提高代码的健壮性和可维护性。

抽象

类型系统能够强化规范编程,TypeScript提供定义接口。在开发大型复杂的应用软件时十分重要,一个系统模块可以抽象的看做一个TypeScript定义的接口。让设计脱离实现,最终体现出一种 IDL(接口定义语言,Interface Define Language),让程序设计回归本质。

文档

TypeScript可以自动根据类型标注生成文档,对于简单的功能实现都不需要编写注释。

为什么要使用Mobx

MobX 和 Redux 的比较

先要明白 mobx 和 redux 的定位是不同的。redux 管理的是 (STORE -> VIEW -> ACTION) 的整个闭环,而 mobx 只关心 STORE -> VIEW 的部分。

Redux优缺点:

  • 数据流流动很自然,因为任何 dispatch 都会触发广播,依据对象引用是否变化来控制更新粒度。

  • 通过充分利用时间回溯的特征,可以增强业务的可预测性与错误定位能力。

  • 时间回溯代价高,因为每次都要更新引用,除非增加代码复杂度,或使用 immutable。

  • 时间回溯的另一个代价是 action 与 reducer 完全脱节,原因是可回溯必然不能保证引用关系。

  • 引入中间件,解决异步带来的副作用,业务逻辑或多或少参杂着 magic。

  • 灵活利用中间件,可以通过约定完成许多复杂的工作。

  • 对 typescript 支持困难。

Mobx优缺点:

  • 数据流流动不自然,只有用到的数据才会引发绑定,局部精确更新,但避免了粒度控制烦恼。

  • 没有时间回溯能力,因为数据只有一份引用。自始至终一份引用,不需要 immutable,也没有复制对象的额外开销。

  • 数据流动由函数调用一气呵成,便于调试。

  • 业务开发不是脑力活,而是体力活,少一些 magic,多一些效率。

  • 由于没有 magic,所以没有中间件机制,没法通过 magic 加快工作效率(这里 magic 是指 action 分发到 reducer 的过程)。

  • 完美支持 typescript。

SO: 前端数据流不太复杂的情况,使用 Mobx,因为更加清晰,也便于维护;如果前端数据流极度复杂,建议谨慎使用 Redux,通过中间件减缓巨大业务复杂度

使用Create-React-App来建立TypeScript的环境

1
2
3
4
5
npm i -g create-react-app
create-react-app tinylog-ui --scripts-version=react-scripts-ts
cd tinylog-ui/
npm start
npm run eject

TPS: 最后一个命令使用eject将所有内建的配置暴露出来

通过create-react-app可以很方便地对整个项目完成环境初始化,如果愿意折腾TypeScript和webpack的环境可以试试,这里忽略webpack和TypeScript的环境搭建过程,而是使用create-react-app来实现环境搭建。

加入React-Router

单页应用怎么可以没有前端路由呢,所以我们要加入React-Rotuer, 这里使用的React-Router的版本是v4.2.0

路由配置使用姿势

对于React-Router,这里使用到的模块有Router, Route, Switch

React Router 是建立在 history 之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。

代码如下:

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
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router';
import { createBrowserHistory } from 'history';
import registerServiceWorker from './registerServiceWorker';
import { Root } from './containers/Root';
import './index.css';
import Container from './containers/Container';
import SignIn from './containers/Auth/signIn';
import SignUp from './containers/Auth/signUp';

const history = createBrowserHistory();

ReactDOM.render(
<Root>
<Router history={history}>
<Switch>
<Route
path="/signIn"
component={SignIn}
/>
<Route
path="/signUp"
component={SignUp}
/>
<Route
path="/"
component={Container}
/>
</Switch>
</Router>
</Root>,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();

页面的编写

这里描述一写Container这个组件的编写

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
import * as React from 'react';
import Header from '../../layout/Header';
import { IAuth } from '../../interfaces';
import { Route, Switch } from 'react-router';
import App from '../App';
import Website from '../Website';

// 这部分是坑点,一开始不知道配置,后发现react-rotuer的4.0版本下需要配置prop的接口
interface Container extends RouteComponentProps<{}> {
}

class Container extends React.Component<Container, {}> {
render () {
return (
<div>
<Header {...this.props} />
<Switch>
<Route path="/website" component={Website}/>
<Route path="/" component={App}/>
</Switch>
</div>
)
}
}

export default Container;

这样,当我们访问url为’/‘的时候,默认会进入Container,其中Container里面是一层子页面,会匹配url,如果url为’/website’, 则进入Website页面,若为’/‘,则进入App页面。

具体关于React-Router的使用请阅读React-Router文档

加入Mobx

1
npm i mobx react-mobx mobx-react-router -S

重新修改index.tsx的入口配置

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
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router';
import { createBrowserHistory } from 'history';
import { useStrict } from 'mobx';
import { Provider } from 'mobx-react';
import { RouterStore, syncHistoryWithStore } from 'mobx-react-router';
// 定义需要使用到的store来进行数据状态的管理
import {
TokenStore,
AuthStore,
HostStore,
OverViewStore,
AssetsStore,
CommonDataStore,
PageStore,
RealTimeStore
} from './stores';
import registerServiceWorker from './registerServiceWorker';
import { Root } from './containers/Root';
import './index.css';
import Container from './containers/Container';
import SignIn from './containers/Auth/signIn';
import SignUp from './containers/Auth/signUp';
// 引入Echarts
import './macarons';
import 'echarts/map/js/world';

// 开启mobx的严格模式,规范数据修改操作只能在action中进行
useStrict(true);

const browserHistory = createBrowserHistory();
const routerStore = new RouterStore();
// 同步路由与mobx的数据状态
const history = syncHistoryWithStore(browserHistory, routerStore);
const rootStore = {
token: new TokenStore(),
auth: new AuthStore(),
host: new HostStore(),
overview: new OverViewStore(),
assets: new AssetsStore(),
commmon: new CommonDataStore(),
page: new PageStore(),
realtime: new RealTimeStore(),
router: routerStore
};

ReactDOM.render(
<Provider {...rootStore}>
<Root>
<Router history={history}>
<Switch>
<Route
path="/signIn"
component={SignIn}
/>
<Route
path="/signUp"
component={SignUp}
/>
<Route
path="/"
component={Container}
/>
</Switch>
</Router>
</Root>
</Provider>,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();

Container容器的修改

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
import * as React from 'react';
import Header from '../../layout/Header';
import { IAuth } from '../../interfaces';
import { Route, Switch } from 'react-router';
// 使用inject和observer来进行数据监听和数据依赖声明
import { inject, observer } from 'mobx-react';
import App from '../App';
import Website from '../Website';

interface Container extends IAuth {
}

@inject('router', 'auth')
@observer
class Container extends React.Component<Container, {}> {
render () {
return (
<div>
<Header {...this.props} />
<Switch>
<Route path="/website" component={Website}/>
<Route path="/" component={App}/>
</Switch>
</div>
)
}
}

export default Container;

@observable 可以在实例字段和属性 getter 上使用。 对于对象的哪部分需要成为可观察的,@observable 提供了细粒度的控制。

@inject 相当于Provider 的高阶组件。可以用来从 React 的context中挑选 store 作为 prop 传递给目标组件

组件的接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
import { RouteComponentProps } from 'react-router';
import {
RouterStore,
AuthStore
} from '../stores';

export interface IBase extends RouteComponentProps<{}> {
router: RouterStore;
}

export interface IAuth extends IBase {
auth: AuthStore;
}

Store的配置

先看一下RouterStore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { History } from 'history';
import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router';

// 路由状态同步
class RouterStore extends BaseRouterStore {
public history;
constructor(history?: History) {
super();
if (history) {
this.history = syncHistoryWithStore(history, this);
}
}
}

export default RouterStore;

然后是AuthStore:

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
import { ISignIn, ISignUp } from './../interfaces/index';
import { observable, action } from 'mobx';
import api from '../api/auth';
import { IUser } from '../models';

// 登录注册状态
class AuthStore {
@observable token;
@observable id;
@observable email;
constructor () {
this.id = '';
this.token = '';
this.email = '';
}
setLocalStorage ({ id, token, email }: IUser) {
localStorage.setItem('id', id);
localStorage.setItem('token', token);
localStorage.setItem('email', email);
}
clearStorage () {
localStorage.clear();
}
@action async signIn (data: ISignIn) {
try {
const { data: res } = await api.signIn(data);
this.id = res.data.id;
this.token = res.data.token;
this.email = res.data.email;
this.setLocalStorage({
id: this.id,
token: this.token,
email: this.email
});
return res;
} catch (error) {
return error;
}
}

@action async signUp (data: ISignUp) {
try {
const { data: res } = await api.signUp(data);
this.id = res.data.id;
this.token = res.data.token;
this.email = res.data.email;
this.setLocalStorage({
id: this.id,
token: this.token,
email: this.email
});
return res;
} catch (error) {
return error;
}
}

@action signOut () {
this.id = '';
this.token = '';
this.email = '';
this.clearStorage()
}
}

export default AuthStore;

Auth是用于网站的登录注册事件以及对应的Token的数据状态保存,登录注册事件的接口请求等操作。

具体的有关Mobx的用法请阅读Mobx文档

目录结构

1
2
3
4
5
6
7
8
9
10
11
app
├── api 后端提供的接口数据请求
├── components 编写的可复用组件
├── config 侧边栏以及导航栏配置
├── constants 常量编写
├── interfaces 接口编写
├── layout 布局外框
├── stores mobx的数据状态管理
├── index.css 全局样式
├── index.tsx 页面入口
├── reset.css 浏览器重置样式

本项目使用了Ant-Design来作为依赖的组件库,具体怎么使用以及配置请参考Ant-Design

到这里其实以及完成对React下TypeScript结合React-Router和Mobx的配置。具体的业务模块如何编写有兴趣可以参阅项目tinylog-ui

个人表达能力有限,无法描述得太清晰,请见谅!

高举 Vue-SSR

将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。


SSR的目的

To solve

  • 首屏渲染问题

  • SEO问题


项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
vue-ssr
├── build (webapck编译配置)
├── components (vue 页面)
├── dist (编译后的静态资源目录)
├── api.js (请求接口,模拟异步请求)
├── app.js (创建Vue实例入口)
├── App.vue (Vue页面入口)
├── entry-client.js (前端执行入口)
├── entry-server.js (后端执行入口)
├── index.template.html (前端渲染模板)
├── router.js (Vue路由配置)
├── server.js (Koa服务)
├── store.js (Vuex数据状态中心配置)

原理概览

vue-ssr

这张图相信很多大佬们都看过N遍了,每个人理解不同,我发表一下自己个人的理解,如果有什么理解错误请原谅我。

先看Source部分,Source部分先由app.js引入Vue全家桶,至于Vue全家桶如何配置后面会说明。app.js其实就是创建一个注册好各种依赖的Vue对象实例,在SPA单页环境下,我们只需要拿到这个Vue实例,然后指定挂载到模板特定的dom结点,然后丢给webpack处理就完事了。但是SSR在此分为两部分,一部分是前端单页,一部分是后端直出。于是,Client entry的作用是挂载Vue对象实例,并由webpack进行编译打包,最后在浏览器渲染。Server entry的作用是拿到Vue对象实例,并处理收集页面中的asynData,获取对应的数据上下文,然后再由webpack解析处理。最后Node Server端中使用weback编译好的两个bundle文件( 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。),当用户请求页面时候,这时候服务端会先使用SSR来生成对应的页面文档结构,而在用户切换路由则是使用了SPA的模式。


搭建环境

项目依赖说明

Koa2 + Vue2 + Vue-router + Vuex

一切都从路由开始

先来配置vue-router, 生成router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Router from 'vue-router'
import Bar from './components/Bar.vue'
import Baz from './components/Baz.vue'
import Foo from './components/Foo.vue'
import Item from './components/Item.vue'

Vue.use(Router)

export const createRouter = () => {
return new Router({
mode: 'history',
routes: [
{ path: '/item/:id', component: Item },
{ path: '/bar', component: Bar },
{ path: '/baz', component: Baz },
{ path: '/foo', component: Foo }
]
})
}

为每个请求创建一个新的Vue实例,路由也是如此,通过一个工厂函数来保证每次都是新创建一个Vue路由的新实例。

Vuex 配置

配置Vuex, 生成store.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
import Vue from 'vue'
import Vuex from 'vuex'
import { fetchItem } from './api'

Vue.use(Vuex)

export const createStore = () => {
return new Vuex.Store({
state: {
items: {}
},
actions: {
fetchItem ({ commit }, id) {
return fetchItem(id).then(item => {
commit('setItem', { id, item })
})
}
},
mutations: {
setItem (state, { id, item }) {
Vue.set(state.items, id, item)
}
}
})
}

同样也是通过一个工厂函数,来创建一个新的Vuex实例并暴露该方法

生成一个Vue的根实例

创建Vue实例,生成app.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
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

export const createApp = ssrContext => {
const router = createRouter()
const store = createStore()

sync(store, router)

const app = new Vue({
router,
store,
ssrContext,
render: h => h(App)
})
return {
app,
store,
router
}
}

通过使用我们编写的createRouter, createStore来每次都创建新的Vue-router和Vuex实例,保证和Vue的实例一样都是重新创建过的,接着挂载注册router和store到Vue的实例中,提供createApp传入服务端渲染对应的数据上下文。

到此我们已经基本完成source部分的工作了。接着就要考虑如何去编译打包这些文件,让浏览器和Node服务端去运行解析。

先从前端入口文件开始

前端打包入口文件: entry-client.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
import { createApp } from './app'

const {
app,
store,
router
} = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
next()
}).catch(next)
})
app.$mount('#app')
})

客户端的entry只需创建应用程序,并且将其挂载到 DOM 中, 需要注意的是,任然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,(如果你有使用异步组件的话,本项目没有使用到异步组件,但后续考虑加入) 才能正确地调用组件中可能存在的路由钩子。通过添加路由钩子函数,用于处理 asyncData,在初始路由 resolve 后执行,以便我们不会二次预取(double-fetch)已有的数据。使用 router.beforeResolve(),以便确保所有异步组件都 resolve,并对比之前没有渲染的组件找出两个匹配列表的差异组件,如果没有差异表示无需处理直接next输出。

再看服务端渲染解析入口文件

服务端渲染的执行入口文件: entry-server.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
import { createApp } from './app'

export default context => {
return new Promise((resolve, reject) => {
const {
app,
store,
router
} = createApp(context)

router.push(context.url)

router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}

Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}

服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,创建和返回应用程序实例之外,还在此执行服务器端路由匹配(server-side route matching)和数据预取逻辑(data pre-fetching logic)。在所有预取钩子(preFetch hook) resolve 后,我们的 store 现在已经填充入渲染应用程序所需的状态。当我们将状态附加到上下文,并且 template 选项用于 renderer 时,状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML。

激动人心的来写webpack

直接上手weback4.x版本

webpack配置分为3个配置,公用配置,客户端配置,服务端配置。

三个配置文件以此如下:

base config:

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
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
devtool: '#cheap-module-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
filename: '[name]-[chunkhash].js'
},
resolve: {
alias: {
'public': path.resolve(__dirname, '../public'),
'components': path.resolve(__dirname, '../components')
},
extensions: ['.js', '.vue']
},
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: 'css-loader'
}
]
},
performance: {
maxEntrypointSize: 300000,
hints: 'warning'
},
plugins: [
new ExtractTextPlugin({
filename: 'common.[chunkhash].css'
})
]
}

改配置只是简单的配置vue, css, babel等loader的使用,接着ExtractTextPlugin提取css资源文件,指定输出的目录,而入口文件则分别在client和server的config中配置。

client config

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
const webpack = require('webpack')
const merge = require('webpack-merge')
const path = require('path')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')


module.exports = merge(baseConfig, {
entry: path.resolve(__dirname, '../entry-client.js'),
plugins: [
new VueSSRClientPlugin()
],
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: 'initial',
minChunks: 2, maxInitialRequests: 5,
minSize: 0
},
vendor: {
test: /node_modules/,
chunks: 'initial',
name: 'vendor',
priority: 10,
enforce: true
}
}
},
runtimeChunk: true
}
})

客户端的入口文件,使用VueSSRClientPlugin生成对应的vue-ssr-client-manifest.json的映射文件,然后添加vendor的chunk分离。

server config

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
const merge = require('webpack-merge')
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')


module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: path.resolve(__dirname, '../entry-server.js'),
// 允许 webpack Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
target: 'node',
// 提供 source map 支持
devtool: 'source-map',
// 使用 Node 风格导出模块(Node-style exports)
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
externals: nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})

到此打包的流程已经结束了,server端配置参考了官网的注释。

使用Koa2

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
const { createBundleRenderer } = require('vue-server-renderer')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const fs = require('fs')
const path = require('path')

const Koa = require('koa')
const KoaRuoter = require('koa-router')
const serve = require('koa-static')

const app = new Koa()
const router = new KoaRuoter()

const template = fs.readFileSync(path.resolve('./index.template.html'), 'utf-8')

const renderer = createBundleRenderer(serverBundle, {
// 推荐
runInNewContext: false,
// (可选)页面模板
template,
// (可选)客户端构建 manifest
clientManifest
})

app.use(serve(path.resolve(__dirname, './dist')))

router.get('*', (ctx, next) => {
ctx.set('Content-Type', 'text/html')
return new Promise((resolve, reject) => {
const handleError = err => {
if (err && err.code === 404) {
ctx.status = 404
ctx.body = '404 | Page Not Found'
} else {
ctx.status = 500
ctx.body = '500 | Internal Server Error'
console.error(`error during render : ${ctx.url}`)
console.error(err.stack)
}
resolve()
}
console.log(ctx.url)
const context = { url: ctx.url, title: 'Vue SSR' }

// 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
// 现在我们的服务器与应用程序已经解耦!
renderer.renderToString(context, (err, html) => {
// 处理异常……
if (err) {
handleError(err)
}
ctx.body = html
resolve()
})
})
})

app.use(router.routes()).use(router.allowedMethods())

const port = 3000
app.listen(port, '127.0.0.1', () => {
console.log(`server running at localhost:${port}`)
})

最后效果当然是这样的了:

预览

参考文档:

vue-ssr官方文档

代码仓库:

github链接

This浅析

你不知道的js

概念

  • this的绑定和函数的声明位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

调用位置

  • 调用位置就是函数在代码中被调用的位置(而不是声明位置)

绑定规则

  • 默认绑定
1
2
3
4
5
function foo () {
console.log(this.a)
}
var a = 2
foo() // 2

当函数调用直接使用不带任何修饰的函数引用进行调用时,在非严格模式下,这时this会绑定到全局对象window/global,而在严格模式下,就会绑定到undefined

  • 隐式绑定
1
2
3
4
5
6
7
8
9
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}

obj.foo() // 2

当函数引用拥有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。

  • 隐式丢失
1
2
3
4
5
6
7
8
9
10
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函数别名
var a = 'lgybetter'
bar() // 'lgybetter'

虽然bar是obj.foo的一个引用,但是它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,使用了默认绑定(非严格模式)

  • 参数传递引起的隐式绑定
1
2
3
4
5
6
7
8
9
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = 'lgybetter'
setTimeout(obj.foo, 100) // 'lgybetter'

JavaScript环境内置的setTimeout()函数实现和以下伪代码类似:

1
2
3
function setTimeout (fn, delay) {
fn() // <--- 调用位置
}
  • 显示绑定:

    • call 和 apply:
1
2
3
4
5
6
7
8
function foo () {
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
foo.apply(obj) // 2

简单使用call和apply显示绑定无法解决丢失绑定问题

  • 硬绑定

    • 手动创建函数强制绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo () {
console.log(this.a)
}
var obj = {
a: 2
}
var bar = function () {
foo.call(obj)
}

bar() // 2
setTimeout(bar, 100) // 2

// 硬绑定的bar不可能再修改它的this
bar.call(window) // 2

通过创建bar(),并在它的内部手动调用foo.call(obj),因此强制把foo的this绑定到了obj

  • 创建一个可以重复使用的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo (something) {
console.log(this.a, something)
return this.a + something
}

function bind (fn, obj) {
return function () {
return fn.apply(obj, arguments)
}
}

var obj = {
a: 2
}

var bar = bind(foo, obj)
var b = bar(3) // 2 3
console.log(b) // 5

由于硬绑定式一种非常常用的模式,于是ES5提供了内置方法Function.prototype.bind,bind会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。

  • API调用的”上下文”:
1
2
3
4
5
6
7
8
function foo (el) {
console.log(el, this.id)
}
var obj = {
id: 'lgybetter'
}
[1,2,3].forEach(foo, obj)
// 1 lgybetter 2 lgybetter 3 lgybetter
  • new绑定:

JavaScript中构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。它们就是被new操作符调用的普通函数而已。
实际上,不存在所谓的构造函数,只有对函数的“构造调用”。

new调用函数操作:

  1. 创建一个全新的对象
  2. 新对象会被执行[[Prototype]]连接
  3. 新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
1
2
3
4
5
function Foo (a) {
this.a = a
}
var bar = new Foo(2)
console.log(bar.a) // 2

使用new来调用Foo(…), 会构造一个新对象并把它绑定到Foo(…)调用中的this上。