Skip to main content

· 8 min read
石晓波

原则基本概念

SOLID

程序设计领域, SOLID (单⼀一功能、开闭原则、⾥里里⽒氏替换、接⼝口隔离以及依赖反转)是由罗伯特·C·⻢马丁在21世纪早期 引⼊入的记忆术⾸首字⺟母缩略略字,指代了了⾯面向对象编程和⾯面向对象设计的五个基本原则。当这些原则被⼀一起应⽤用时,它们使得⼀一个程序员开发⼀一个容易易进⾏行行软件维护和扩展的系统变得更更加可能SOLID被典型的应⽤用在测试驱动开发上,并且是敏敏捷开发以及⾃自适应软件开发的基本原则的重要组成部分。

IOC

依赖倒置原则(Dependency Inversion Principle,DIP)规定:代码应当取决于抽象概念,⽽而不不是具体实现。⾼高层模块不不应该依赖于低层模块,⼆二者都应该依赖于抽象抽象不不应该依赖于细节,细节应该依赖于抽象 (总结解耦)类可能依赖于其他类来执⾏行行其⼯工作。但是,它们不不应当依赖于该类的特定具体实现,⽽而应当是它的抽象。这个原则实在是太重要了了,社会的分⼯工化,标准化都 是这个设计原则的体现。显然,这⼀一概念会⼤大⼤大提⾼高系统的灵活性。如果类只关⼼心它们⽤用于⽀支持特定契约⽽而不不是特定类型的组件,就可以快速⽽而轻松地修改这些低级 服务的功能,同时最⼤大限度地降低对系统其余部分的影响。

AOP

在软件业,AOP为Aspect Oriented Programming的缩写,意为:⾯面向切⾯面编程,通过预编译⽅方式和运⾏行行期动态代理理实现程序功能的统⼀一维护的⼀一种技 术。AOP是OOP的延续,是软件开发中的⼀一个热点,也是Spring框架中的⼀一个重要内容,是函数式 编程的⼀一种衍⽣生范型。利利⽤用AOP可以对业务逻辑的各个部分进⾏行行隔离,从⽽而使得业务逻辑各部分之间的耦合度降低,提⾼高程序的可重⽤用性,同时提⾼高了了开发的效率

编码实现

model

定义数据类型以User为例,定义User

/src/models/user.ts
export namespace Models {
export class User {
email: string;
name:string;
}
}

interface

定义接口,service需要实现接口,代码如下

/src/interface/IIndex.ts
import { Models } from "../models/User";

export interface IIndex {
getUser(id:number): Models.User;
}

service

已经定义了数据类型和接口,接下来完成service类,implements 接口。使用装饰器的方式自动将service绑定到container中。同时使用symbol为每个service定义唯一标识符,完成如上代码只是完成了对service的标记,代表当前service可以注入到container中。如果要在controller中注入service查看接下来的步骤。

src/services/indexService.ts
import { IIndex } from "../interface/IIndex";
import { Models } from "../models/User";
import { provide, buildProviderModule } from "inversify-binding-decorators";
import { TAGS } from "../constant/tags";

@provide(TAGS.IndexService)
export class IndexService implements IIndex {
constructor() {}
private userStorage: Models.User[] = [
{
email: "123123@qq.com",
name: "小名",
},
{
email: "12312312@qq.com",
name: "小同",
},
];
getUser(id: number) {
let result: Models.User;
result = this.userStorage[id];
return result;
}
}

controller

定义controller,controller中使用inversify库提供的inject装饰器注入要使用的service,使用inversify-koa-utils库提供的controller装饰器绑定路由和controller的关联关系。controller中的action使用httpGet装饰器绑定具体的路由和响应操作。这里需要注意一点controller需要按需加载,只有路由匹配了才能加载对应的controller而不能像service一样一次性加入到container中,这里就需要使用inversify-binding-decorators提供的fluentProvide流式provider。代码如下:

src/controller/indexController.ts
import { inject, injectable } from "inversify";
import { interfaces, controller, httpGet,TYPE} from "inversify-koa-utils";
import { TAGS } from "../constant/tags";
import { IIndex } from "../interface/IIndex";
import {IRouterContext} from 'koa-router';
import { provideThrowable } from "../ioc";
// import {BaseContext} from 'koa';
//service一次性加载到容器中,controller需要在遇到这个路由的时候才会加载对应的controller 流式的provider
@provideThrowable(TYPE.Controller,'IndexController')
@controller('/')
export default class IndexController implements interfaces.Controller {
private indexService: IIndex;
constructor(@inject(TAGS.IndexService) indexService) {
this.indexService = indexService;
}

@httpGet('/')
private async indexAction(ctx:IRouterContext, next:Promise<unknown>):Promise<any> {
const data = this.indexService.getUser(1);
ctx.body = {
data,
}
}
}
src/ioc/index.ts
import { fluentProvide } from "inversify-binding-decorators"

//别名和名称
let provideThrowable = (identifier,name) => {
return fluentProvide(identifier).whenTargetNamed(name).done();
}

export {
provideThrowable
};

入口文件

通过如上编码已经实现了controller、service、model接下来需要完成入口文件编写,注意因为使用了元编程所以需要在入口文件的最顶部引入reflect-metadata,按需引入controller和service虽然定义了装饰器在类上但是没有手动引入需要使用的controller和service,所以可以定义loader.ts文件 手动引入所有要使用的controller和service文件然后在入口文件中引入loader即可。这些准备工作都完成后开始创建容器container,使用inversify提供的Container类创建实例即可。bind容器和service,可以通过手动的方式一个一个绑定也可以通过inversify-binding-decorators库提供的buildProviderModule方法自动绑定。使用inversify-koa-utils库提供的InversifyKoaServer类创建server。具体代码如下:

src/app.ts
import "reflect-metadata";
import "./ioc/loader";

import { InversifyKoaServer } from "inversify-koa-utils";
import { Container } from "inversify";
import { buildProviderModule } from "inversify-binding-decorators";

const container = new Container();
container.load(buildProviderModule());
const server = new InversifyKoaServer(container);

server
.setErrorConfig((app) => {
//错误处理中间件
})
.setConfig((app) => {
//其他中间件
});
const app = server.build();

app.listen(3001, () => {
console.log("inversify server 启动成功");
});

总结

如上只是完成了一个简单的基于SOLID的BFF架构代码,可以引入一些其他的中间件丰富工程,也可以配置webpack编译出可以在生产环境的代码。完整代码点击查看

· 3 min read
石晓波

bigpipe定义

bigpipe是由facebook提出的一种动态网页加载技术,简单来说就是将服务器端的响应分成块分多次传输,这么做的主要原因是可以提升首屏渲染时间,可以将网页的框架和主要内容先传输过来进行显示。

bigpipe案例

案例内容是,先将页面分位两块核心页面和动态渲染两部分,案例的话就是简单的innerHTML,html模板如下:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bigPipe</title>
</head>
<body>
<div id="app">
核心页面
</div>

<section>
<div id="part1">loading</div>
<div id="part2">loading</div>
</section>

<script>
function addHtml(id, content){
document.querySelector(`#${id}`).innerHTML=content;
}
</script>
</body>
</html>

接着使用koa@koa/router新建一个简单服务器,根路由下首先读取模板index.html文件,然后将读取的文件通过ctx.res.write传输到客户端,新建两个异步的js标签,通过js动态修改模板中的内容,传输完成记得ctx.res.end()。刷新页面看到动态内容随着传输已经有了内容,查看响应头发现多了两个属性Transfer-Encoding: chunkedKeep-Alive: timeout=5,前者可以证明内容是通过分片进行传输的,因为写了延迟两秒的函数发现传输时间为4.01sWaterfal为4s。需要注意的是需要手动修改tx.status =200ctx.type="html"

服务端代码如下:

const Koa = require('koa');
var Router = require('@koa/router');
const fs = require('fs');
const app = new Koa();
const router = new Router();
const task1 = () => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve(`<script> addHtml("part1", "第一次传输的内容")</script>`)
}, 2000)
})
}

const task2 = () => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve(`<script> addHtml("part2", "第二次传输的内容")</script>`)
}, 2000)
})
}

router.get('/', async (ctx,next) => {
const file = fs.readFileSync("index.html", "utf-8");
ctx.status =200;
ctx.type="html";
ctx.res.write(file);
const part1 = await task1();
ctx.res.write(part1)
const part2 = await task2();
ctx.res.write(part2)
ctx.res.end();
})

app.use(router.routes()).use(router.allowedMethods());
app.listen(8085,() => {
console.l

小优化

如果首页文件很大,文件读取需要一定时间,现在的实现是读取了首页文件以后才会开始传输,这里可以进行优化,使用的方式传输文件,读一点传输一点以提高效率。修改部分代码如下:

    const filename = resolve(__dirname, 'index.html');
const stream = fs.createReadStream(filename);
ctx.status =200;
ctx.type="html";
stream.on('data',(chunk)=>{
ctx.res.write(chunk);
})
const part1 = await task1();
ctx.res.write(part1)
const part2 = await task2();
ctx.res.write(part2)
ctx.res.end();

· 4 min read
石晓波

CI&CD基础介绍

CI:持续集成,CD:持续交付和持续部署。现代软件开发的需求加上部署到不同基础设施的复杂性使得创建应⽤程序成为⼀个繁琐的过程。当应⽤程序出现规模性增⻓,开发团队⼈员变得更分散时,快速且不断地⽣产和发布软件的流程将会变得更加困难。为了解决这些问题,开发团队开始探索新的策略来使他们的构建、测试和发布流程⾃动化,以帮助其更快地部署新的⽣产。这就是持续交付和持续集成发展的由来。

持续集成(CI)是⼀个让开发⼈员将⼯作集成到共享分⽀中的过程,从⽽增强了协作开发。频繁的集成有助于解决隔离,减少每次提交的⼤⼩,以降低合并冲突的可能性。

手动实现一个简单的CI/CD

为了了解CI/CD的工作原理,让我们来实现一个简单的CI/CD。实现CI/CD需要借助于几个库chokidar 监听文件变化,shelljs执行shell命令。

index.js
const shell = require('shelljs');
const path = require('path');
const chokidar = require('chokidar')

const form = "./profile1.html*";
const to = "root@106.52.73.93";
const password = "1234567890";
const expectPath = path.join(__dirname, './expect.exp');

//const test = shell.exec('npm run test')
//if(test !== 0){
// process.exit(1);
//}

//const status = shell.exec('npm run build')
//if(status !== 0){
// process.exit(1);
//}

const watch = chokidar.watch(process.cwd())

//shell.exec(`scp ${form} ${to}`);


//加入expect.ecp
watcher.on('change', function(filePath){
shell.exec(`expect ${expectPath} ${filePath} ${to} ${password}`);
})

如上实现了基础的自动测试,自动编译打包和部署到服务器。但是在本机配置了免密登录的基础上才能正常运行否则执行scp命令的时候需要输入用户密码,阻塞自动化流程,并且所有的机器我们不一定有所有的控制权不可能给每台机器都配置免密登录,所以加入expect.exp可以监听到命令行密码的输入,可以命令的方式填入密码实现自动交互。

expect.exp
#!/usr/bin/expect
set from [lindex $argv 0]
set to [lindex $argv 1]
set password [lindex $argv 2]

set timeout 30
spawn bash -c "scp $from $to"

expect{
"*password:" {send "$password\r"}
}

interact

思考和优化点

如果有多台机器,如何实现动态部署到多台机器上?

答:可以使用数组保存from和to,循环遍历,将生产的结果文件使用scp命令部署到不同的机器。密码需要写入配置文件中,对密码配置文件使用加密保存。

如何读取超大日志文件?

答:使用stream+多线程处理

· 3 min read
石晓波

本地开发项目如果要实现内部部署测试,我们前端开发者每次都需要将打包后的文件交给后端开发人员,然后经过后端开发者部署到服务器。为了简化开发和减少交流成本,也为了提高前端开发者的位置,我们可以编写本地脚本自动化实现打包、测试、部署。也提高我们的工作效率。

如下是实现本地脚本自动化部署流程需要使用的包:

  • shelljs js执行shell脚本
  • rsync 使用 Node.js 构建和执行 rsync 命令的类。
  • colors
  • yargs
scripts/deploy.js
const shell = require('shelljs');
const Rsync = require('rsync');
const path = require('path');
const colors = require('colors');
const argv = require('yargs').argv;
const { exit } = require('process');

//获取执行的进程参数
const [targetName] = argv._;

//设置多台测试机,给每台测试机起代号,对应不同的IP
const host_map = {
staging001: 'onePiece:/root/build'
}

//判断如果不传参数 没有选择要部署的主机 给出提示
if (!host_map[targetName]) {
shell.echo("目标主机不存在!");
shell.exit(1);
}

//设置通知消息 以企业微信为例
function sendNotify(message) {
shell.exec(`curl 'https//qyapi.weixin.qq.com - H 'Content-type:application' - d '{"message": ${message}}`)
}

//0代表成功
// sendNotify('安装依赖')
// console.log(colors.yellow('☕️ 安装依赖'))
// if(shell.exec("npm install").code !== 0){
// shell.echo("error:npm install error.");
// shell.exit(1)
// }

//测试
// sendNotify('进行测试')
// console.log(colors.yellow('☕️ 进行测试'))
// if(shell.exec("npm run test").code !== 0){
// shell.echo("error:npm run test error.");
// shell.exit(1)
// }
//构建
sendNotify('开始构建')
console.log(colors.yellow('☕️ 开始构建'))
if(shell.exec("npm run build").code !== 0){
shell.echo("error:npm run build error.");
shell.exit(1)
}

//部署
sendNotify("开始部署")
console.log(colors.yellow('☕️ 开始部署'));
const rsync = Rsync.build({
source:path.join(__dirname,'../','/./build/*'),
destination: host_map[targetName],
flags:'avz',
shell:'ssh'
})

rsync.execute(function(error, code, cmd) {
console.log(error,code,cmd,)
console.log(colors.yellow('☕️ 部署完成'))
sendNotify('部署完成')
});

如上所示注释了安装依赖的步骤,本地开发环境不需要重新安装依赖,可以节省掉安装依赖的时间。需要注意的是建议配置免密登录,要不然需要手动输入密码体验差。

执行优化

如上要运行脚本需要使用node命令去执行对应的脚本文件,可以配置package.json的scripts

"scripts":{
"deploy": "node ./scripts/deploy.js"
}

扩展:也可以将本地脚本自动化部署写成npm包,通过配置主机的方式,命令执行部署

· 18 min read
石晓波

项目基础构建

本项目使用的是 MVC 基础机构模式

项目根目录下执行如下命令创建基础的 package.json:

npm init -y

根目录下创建 src 文件,src 下分别创建 client 和 server 文件夹,用来保存客户端代码和服务器代码。使用基础的 MVC 架构模式创建文件夹,完整主文件目录结构如下:

├── src
├──── web
├─────── assets
├─────── components
├─────── views
├──── server
├─────── assets
├─────── controllers
├─────── middlewares
├─────── models
├── node_modules
├── scripts
├── logs
├── dist
├── config
├── tests
├── README.md #帮助项目开发文件
├── app.js #项目启动文件
├── package-lock.json
└── package.json

基础代码实现

服务端代码

创建 app.js 为入口文件,引入 koa web 开发框架,初始化 app。编写配置文件,配置文件需要根据进程中的参数判断开发环境开始生产环境,配置文件代码如下:

/src/server/config/index.js
import { extend } from "lodash";
import { join } from "path";

let config = {
//配置后续资源路径和其他公共配置
viewDir: join(__dirname, "..", "views"),
staticDir: join(__dirname, "..", "assets"),
};

if (process.env.NODE_ENV === "development") {
let localConfig = {
port: 8081,
memoryFlag: false,
};

config = extend(config, localConfig);
}

if (process.env.NODE_ENV === "production") {
let localConfig = {
port: 8082,
memoryFlag: true,
};

config = extend(config, localConfig);
}

export default config;

初始化 koa,并引入 swig 模板引擎。

/src/server/app.js
import Koa from "koa";
import render from "koa-swig";
import { wrap } from "co";
import { port, viewDir, memoryFlag } from "./config";

const app = new Koa();

app.context.render = wrap(
render({
root: viewDir,
autoescape: true,
cache: memoryFlag,
ext: "html",
varControls: ["[[", "]]"],
writeBody: false,
})
);

app.listen(port, () => {
console.log("🍺服务器启动成功", port);
});

Controller

根据 MVC 架构模式定义 Controller,首先定义 Controller 基类,包含基础的 log 信息打印等业务信息,然后定义子类 IndexController、BooksController。

/src/server/controllers/Controller
class Controller {
constructor() {}

log() {
console.log("父类Controller");
}
}

BooksController 中整合了 Model 和 View

/src/server/controllers/BooksController.js
import Controller from "./Controller";
import Books from "../models/Books";
class BooksController extends Controller {
constructor() {
super();
}

//服务端渲染 直出
async actionIndex(ctx, next) {
const book = new Book();
const data = await book.getData();
ctx.body = await ctx.render("books/pages/list", data);
}

async actionCreate(ctx, next) {
ctx.body = await ctx.render("books/pages/create");
}
}
/src/server/controllers/IndexController.js
import Controller from "./Controller";
class IndexController extends Controller {
constructor() {
super();
}
async actionIndex(ctx, next) {
ctx.body = "石晓波🏮";
}
}
export default IndexController;

Router

定义了 Controller 和 Action 后需要将这些方法和路由进行整合,所以加入 koa-simple-router 的使用,代码如下:

/src/server/controllers/index.js
import router from "koa-simple-router";
import BooksController from "./BooksController";
import IndexController from "./IndexController";
const bookController = new BooksController();
const indexController = new IndexController();

export default (app) => {
app.use(
router((_) => {
_.get("/", indexController.actionIndex);
_.get("/books/list", bookController.actionIndex);
_.get("/books/create", bookController.actionCreate);
})
);
};

同时在 app.js 中添加对如上函数的引用并传入 app

/src/server/app.js
import controllers from './controllers';
...
controllers(app)
...

Model

model 定义数据,作为中间代理层,Model 层主要是做数据交互,数据获取、数据整合。如上 Controller 中使用了 Books,如下是 Books 的代码:

/src/server/Model/Books.js
import { get } from "axios";
class Books {
getData() {
return get("http://localhost/basic/web/index.php?r=books");
}
}

错误处理

程序运行难免会遇到错误,我们需要将错误信息收集并做持久化处理,所以自定义处理处理中间件并使用 log4j 对错误信息进行持久化处理。

首先在 app 文件中增加对 log4j 的配置以及引入错误处理中间件,需要注意的是错误处理中间件需要在业务代码前才能捕获到业务代码的错误。

/src/server/app.js
import { configure, getLogger } from "log4js";
import errorHandler from "./middlewares/errorHandler.js";
configure({
appenders: { cheese: { type: "file", filename: "logs/yd.log" } },
categories: { default: { appenders: ["cheese"], level: "error" } },
});
const logger = getLogger("cheese");

errorHandler.error(app, logger);

如下是自定义错误处理中间件代码:

/src/server/middleWares/errorHandler.js
class errorHandler{
static error(app,logger){
app.use(async (ctx, next)=>{
try{
await next();
} catch(e){
logger.error(e);
ctx.body = "500请求啦~恢复中.'"
}
})

app.use(async (ctx, next) => {
await next();
if(404 !== ctx.status){
return;
}
ctx.body = '<script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script>';
})
}
}

客户端代码

swig模板引擎

首先需要创建模板公共布局文件,后续所有页面都可以基于公共文件进行块内容填充,如下是构建公共布局模板:

/src/web/views/layouts/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{% block title %}{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<div>
{% block content %}{% endblock %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>

swig使用了块占位符,以此模板为基础的页面可以只写填充标记相关的代码,模板会自行编译出对应的html文件。

创建模块页面

创建books模块继承自基础模块,并引入定义的组件

/src/web/views/books/pages/list.html
{% extends '@layouts/layout.html' %}

{% block title %} 图书列表页📚 {%endblock %}

{% block head %}
<!--injectcss-->
{% endblock %}

{% block content %}
{% include "@components/banner/banner.html" %}
<h1>展示图书</h1>
{% endblock %}

{% block scripts %}
<!--injectjs-->
{% endblock %}
/src/web/views/books/pages/create.html
{% extends '@layouts/layout.html' %}

{% block title %} 图书列表页📚 {%endblock %}

{% block head %}
<!--injectcss-->
{% endblock %}

{% block content %}
{% include "@components/banner/banner.html" %}
<h1>展示图书</h1>
{% endblock %}

{% block scripts %}
<!--injectjs-->
{% endblock %}

定义公共组件

定义公共组件banner

/src/web/components/banner/banner.html
<div class="baner">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/books/list">展示图书</a></li>
<li><a href="/books/create">添加图书</a></li>
</ul>
</div>
/src/web/components/banner/banner.js
const banner = {
init() {
console.log('banner');
},
};
export default banner;

webpack入口文件

webpack是以js为入口文件的,所以给每个页面都定义一个入口文件,这里设置一种规则模块下的入口文件定义为[模块名]-[页面名].entry.js,webpack打包时可以根据制定的规则解析入口js文件名,将打包后的文件插入到对应的html页面中。

/src/web/views/books/boosk-create.entry.js
import banner from '@/components/banner/banner.js';
banner.init();
/src/web/views/books/boosk-list.entry.js
import banner from '@/components/banner/banner.js';
banner.init();

编译环境构建

编译环境区分生产环境和开发环境webpack通过使用webpack-merge对不同环境的配置文件和基础文件进行合并,gulp通过使用process.env.NODE_ENV来区分开发环境和生产环境。

客户端代码编译

客户端代码使用webpack进行编译,因为是MPA应用,所以需要定义多入口,使用glob使用正则匹配的方式获取多入口的js,获取的js为上面根据命名规则定义的入口文件,循环遍历这些入口文件解析出模块和文件名称,将获取的html文件为webpack打包插入的目标文件也就是template属性,并将以template为模板打包后的文件输出到dist下同等的目录下,这里需要注意两点,第一如果不设置html-webpack-plugin的chunk属性那么会将所有的js文件都插入到模板中,所以需要设置chunk属性['runtime', entryKey],entryKey为[模块名]-[文件名]的组合,为什么要使用这个名称的文件呢,因为多入口打包是以该名称为key,目标js文件为目标定义的。完成入口和循环生成html-webpack-plugin的实例配置后将插件的实例写入webpack的plugins中。如上插件中使用了chunks的配置那么我们就需要将webpack打包中的runtime单独提出来,配置optimization.runtimeChunk即可。

如下是webpack公共部分的配置:

const argv = require('yargs-parser')(process.argv.slice(2));
const _mode = argv.mode || 'development';
const _mergeConfig = require(`./config/webpack.${_mode}.js`);
const { merge } = require('webpack-merge');
const { sync } = require('glob');
const { resolve } = require('path');
const files = sync('./src/web/views/**/*.entry.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlAfterPlugin = require('./config/HtmlAfterPlugin');

let _entry = {};
let _plugins = [];
for (let item of files) {
// console.log(item)
if (/.+\/([a-zA-Z]+-[a-zA-Z]+)(\.entry\.js)/g.test(item) == true) {
const entryKey = RegExp.$1;
_entry[entryKey] = item;
const [dist, template] = entryKey.split('-');
_plugins.push(
new HtmlWebpackPlugin({
filename: `../views/${dist}/pages/${template}.html`,
template: `src/web/views/${dist}/pages/${template}.html`,
chunks: ['runtime', entryKey],
inject: false,
})
);
} else {
console.log('🐖', '项目配置匹配失败');
process.exit(-1);
}
}
const webpackConfig = {
entry: _entry,
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
plugins: [..._plugins, new HtmlAfterPlugin()],
resolve: {
alias: {
'@': resolve('src/web'),
},
},
};
module.exports = merge(webpackConfig, _mergeConfig);

对于入口文件来说,开发环境和生产环境略微有些区别,生产环境建议给js文件名加hash的方式防止文件缓存问题,配置如下

//开发环境
output: {
path: join(__dirname, '../dist/assets'),
publicPath: '/',
filename: 'scripts/[name].bundle.js',
},
//生产环境
output: {
path: join(__dirname, '../dist/assets'),
publicPath: '/',
filename: 'scripts/[name].[contenthash:5].bundle.js',
},

配置运行命令后执行编译,发现虽然打包成功但是我们的components和layout在dist下没有,所以需要将文件拷贝到dist下,使用copy-wepbkac-plugin配置如下:

  new CopyPlugin({
patterns: [
{
from: join(__dirname, '../', 'src/web/views/layouts/layout.html'),
to: '../views/layouts/layout.html',
},
],
}),
new CopyPlugin({
patterns: [
{
from: 'src/web/components/**/*.html',
to: '../components',
transformPath(targetPath, absolutePath) {
//win环境
return targetPath.replace('src\\web\\components\\', '');
//mac环境
// return targetPath.replace('src/web/components/', '');
},
},
],
}),

自定义 html-after-plugin 插件

如上配置都完成后发现还是不能运行,查看html文件后发现打包虽然完成,但是js文件插入的位置有问题,未能按照我们在模板中定义的位置将js文件插入进去,而是将js文件都追加在了最后面,导致运行的时候生成html文件中没有引入js,所以需要我们自定义webpack插件,因为自定义的webpack插件是基于html-webpack-plugin的所以需要关闭该插件的默认插入操作,设置inject为false。具体的API可以查看npm.js中html-webpack-plugin中的介绍,自定义的webpack插件中主要使用到了两个生命周期beforeAssetTagGenerationbeforeEmit,第一个生命周期是资源生成前可以保存所有的资源路径,第二个生命周期是插入到html之前所以可以在该生命周期对html内容进行替换,具体实现如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const pluginName = 'HtmlAfterPlugin';

const assetsHelp = (data) => {
let js = [];
const getAssetsName = {
js: (item) => `<script src="${item}"></script>`,
};
for (let jsitem of data.js) {
js.push(getAssetsName.js(jsitem));
}
return {
js,
};
};

class HtmlAfterPlugin {
constructor() {
this.jsarr = [];
}
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
pluginName,
(htmlPligunData, cb) => {
const { js } = assetsHelp(htmlPligunData.assets);
this.jsarr = js;
cb(null, htmlPligunData);
}
);
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
pluginName,
(data, cb) => {
let _html = data.html;
_html = _html.replace('<!--injectjs-->', this.jsarr.join(''));
_html = _html.replace(/@components/g, '../../../components');
_html = _html.replace(/@layouts/g, '../../layouts');
data.html = _html;
cb(null, data);
}
);
});
}
}

module.exports = HtmlAfterPlugin;

在wepbpack中引入我们自定义的插件,发现js插入到了理想中的位置。

服务端代码编译

gulp小巧且容易使用,因为我们这里只对ES Module进行编译,编译目标是Commonjs的规范,使用gulp不会产生其他冗余的代码,代码也比较简洁,需要注意的是我们使用的编译插件是@babel/plugin-transform-modules-commonjs所以这里不能使用wepback中使用的babel配置,所以需要设置babelrc属性为false,开发环境需要使用gulp-watch对文件进行监听,当文件发生变化时自动编译。为了优化运行环境引入gulp-rollup的tree-shaking,对无用代码进行清洗,编译环境自动删除无用代码。完整配置如下:

gulpfile.js
const gulp = require('gulp');
const watch = require('gulp-watch');
const entry = './src/server/**/*.js';
const plumber = require('gulp-plumber');
const cleanEntry = './src/server/config/index.js';
const rollup = require('gulp-rollup');
const babel = require('gulp-babel');
const replace = require('@rollup/plugin-replace');
const prepack = require('gulp-prepack');
function builddev() {
return watch(entry, { ignoreInitial: false }, () => {
gulp
.src(entry)
.pipe(plumber())
.pipe(
babel({
babelrc: false,
plugins: ['@babel/plugin-transform-modules-commonjs'],
})
)
.pipe(gulp.dest('dist'));
});
}

function buildprod() {
return gulp
.src(entry)
.pipe(
babel({
babelrc: false,
ignore: [cleanEntry],
plugins: ['@babel/plugin-transform-modules-commonjs'],
})
)
.pipe(gulp.dest('dist'));
}

//清理环境变量
function buildconfig() {
return (
gulp
.src(entry)
.pipe(
rollup({
input: cleanEntry,
output: {
format: 'cjs',
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
})
)
// .pipe(prepack({}))
.pipe(gulp.dest('./dist'))
);
}

let build = gulp.series(builddev);
if (process.env.NODE_ENV == 'production') {
build = gulp.series(buildprod, buildconfig);
}

gulp.task('default', build);

落地页和切页优化

完成所有的配置后页面可以正常显示也可以正常的切换页面以及刷新。接下来我们给layout中增加一些公共库的使用比如jquery,发现每次刷新切换都会重新加载jquery

pjax

修改公共的layout.html,增加如下代码:

  <div id="app">
{% block content %}{% endblock %}
</div>
<script>
<script src="https://cdn.staticfile.org/jquery/3.5.1/jquery.js"></script>
<script src="https://cdn.staticfile.org/jquery.pjax/2.0.1/jquery.pjax.min.js"></script>
$(document).pjax('a', '#app');
</script>

为了优化如上这种情况,避免静态公共资源被重复加载,造成资源的浪费,这里选择加入pjax,pjax=ajax+pushState实现不刷新页面而修改页面内容。首先需要做的就是拦截所有<a>标签的默认事件,然后传入一个容器,容器就是需要替换的内容展示的位置。

修改代码重新切换页面发现http://localhost:8081/books/list?_pjax=%23app多了这么一条请求,因为服务器没有对这条请求进行响应所以还是走原来的路线,发起默认的list请求,所以需要增加服务器对pjax请求的响应处理。需要增加对header头中的x-pjax为判断条件,返回内容,注意要主动设置ctx.status=200ctx.type = 'html',那么到底要返回什么内容呢,这里就需要注意页面内容的加载分为落地页站内切,落地页需要返回整体所有的内容,站内切则只需要返回部分内容,到底返回那些内容呢,我们可以给需要动态返回的内容都增加一个classpjaxcontent,通常会给所有的组件都会添加。这里为了服务器端对html进行解析和DOM操作引入一个库cheerio类似服务器端的jquery,动态获取到所有的pjaxcontent元素通过流的方式分片发送给客户端。代码如下:

async actionIndex(ctx, next) {
// const book = new Book();
// const { data } = await book.getData();
const data = '123';
// console.log('🐻', data);
// ctx.body = {
// data,
// };
const html = await ctx.render('books/pages/list', {
data,
});
if (ctx.request.header['x-pjax']) {
console.log('站内切');
const $ = cheerio.load(html);
ctx.status = 200;
ctx.type = 'html';
//每个组件都需要加 否则切页动态替换的时候不能加载所有的组件
$('.pjaxcontent').each(function () {
ctx.res.write($(this).html());
});
//因为有缓存的原因原地切虽然没有重新加载js但是已经在栈中,当两个页面来回切就会发现js没有运行所以要加如下代码
$('.lazyload-js').each(function () {
ctx.res.write(
`<script class="lazyload-js" src="${$(this).attr('src')}"></script>`
);
});
ctx.res.end();
} else {
function createSSRStreamPromise() {
console.log('落地页');
return new Promise((resolve, reject) => {
const htmlStream = new Readable()
htmlStream.push(html);
htmlStream.push(null);
ctx.type = 'html';
ctx.status = 200;
htmlStream.on('error', (err) => {
reject(err)
})
.pipe(ctx.res)
})
}

await createSSRStreamPromise();
}
ctx.body = await ctx.render('books/pages/list');
}

总结

BFF基础架构已经全部实现也实现了简单的性能优化,站内切只加载部分资源,刷页全部加载,即保证了落地页的直出又保证了切页速度,结合了SSR的优点和SPA的优点。如上文章中使用了分片传输到客户端使用了bigpie,如需了解bigpie的详细信息可以查看bigpie初探

· 4 min read
石晓波

开发的node应用程序要发布,需要重新启动node应用程序进程,如果刚好有用户正在访问可能会返回502错误码,体验会很差所以就需要Node热部署。

参考链接smart-node-reload

如下是Node热部署实现的方式:

const fs = require('fs');
const path = require('path');
const vm = require('vm');

const handlerMap = {};
const hotsPath = path.join(__dirname, 'hots');

//加载文件代码并监听指定文件夹目录文件内容变动
const loadHandlers = async () => {
//遍历出指定文件夹下的所有文件
const files = await new Promise((resolve, reject) => {
fs.readdir(hotsPath, (error, files) => {
if (error) {
reject(error);
} else {
resolve(files)
}
})
})

//初始化加载所有文件 把每个文件结果缓存到handlerMap变量当中
for (let f in files) {
handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
}

//监听指定文件夹的文件内容变动
await watchHandlers();
};

//监听指定文件夹下的文件变动
const watchHandlers = async () => {
//这里建议用chokidar的npm包代替文件夹监听
fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
//获取每个文件的绝对路径
//包一层require.resolve的原因是 拼接好路径后 它会帮你判断这个路径下的文件是否存在
const targetFile = require.resolve(hotsPath, filename);
//当使用require加载一个模块后,模块的数据就会缓存到require.cache中,下次再加载相同模块,就会从缓存中获取文件
//所以热加载部署,首先要做的就是清除require.cache中对应文件的缓存

const cacheModule = require.cache[targetFile];
//去除掉在require.cache缓存中parent对当前模块的引用,否则会引起内存泄露,具体查看下面的文档
//《记录一次由一行代码引发的“血案》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
//《一行 delete require.cache 引发的内存泄露血案》https://zhuanlan.zhihu.com/p/34702356
// 其他文件引用还是会有缓存 上面只是删除了当前文件的引用
if (cacheModule.parent) {
cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1)
}

//清除指定路径对应模块的require.cache缓存
require.cache[targetFile] = null;

//重新加载发生变动后的模块文件,实现热加载部署效果,并将重新加载后的结果,更新到handlerMap变量中
const code = await loadHandler(targetFile);
handlerMap[filename] = code;
console.log("热部署文件:", filename, ",执行结果:", handlerMap);
})
}

//加载指定文件的代码
const loadHandler = filename => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
resolve(null)
} else {
try {
//使用vm模块的script方法来预编译发生变化后的文件代码,检查语法错误,提前发现是否存在的语法错误
new vm.Script(data);
} catch (e) {
//语法错误,编译错误
reject(e);
return;
}

//编译通过后,重新require加载最新的代码
resolve(require(filename));
}
})
})
}

loadHandlers();

· 13 min read
石晓波

ie兼容一直是个让人头疼的问题,很多新的API都不能运行在IE上,但是IE有时候又不得不去兼容他,接下里是解决ie兼容的踩坑过程。

本文使用的是wepback5去编译工程代码,如果是单纯的使用babel或者typescript编译器也可以参考后续文章对应的部分。

webpack5编译分为两部分,webpack5的 运行时代码和我们编写的业务代码,webpack5自身可以通过配置将自身的运行时代码编译成可兼容ie运行的代码,但是业务代码webpack5只会处理模块和依赖关系代码并将业务代码本身不会优化为兼容性较高的版本,并且不能直接给ie运行,这时候就需要我们去配置loader去解决代码的编译问题了。

webpack5运行时代码兼容

默认情况下不配置webpack,生成的runtime使用的箭头函数,如果要讲webpack5运行时代码编译为兼容低版本的代码,需要配置target,target可配置为['web', 'es5'],target默认使用的是package.json中的browserslinst可以配置browserslists属性也能达到目的

babel编译

使用babel编译源代码需要配置babel-loader,babel可以将新版本的JavaScript编译为稳定版本以提高兼容性。首先需要安装babel-loader

npm install babel-loader @babel/core @babel/preset-env -D

webpack需要使用babel-loader来调用babel,而@babel/core和@babel/preset-env是核心插件集

danger

babel配置文件需要命名为.babelrc,千万不要命名为babel.config.json,后续配置babel-runtime会报错,模块解析失败的问题,如果添加了模块解析的plugin同样不能实现根据需要引入babel-runtime

首先配置webpack,使用babel-loader解析js文件,babel的配置单独配置到.babelrc中

webpack.common.js
            {
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
}
]
}
{
"presets": [
[
"@babel/preset-env"
]
]
}

编写一些测试代码

const a = 1;
let b = 2;


class myClass{

}

const mine = new myClass();
console.log('mine',mine);

export const aFunc = () => {
return a + b;
}

export const arrInclude = () => {
const arr = [1, 2, 3, 4];
return arr.includes(a)
}

export const promiseFunc = () => {
return new Promise((resolve) => {
setTimeout(() => {
document.body.style.backgroundColor = 'green';
}, 3000)
})
}

const asyncFunc = async () => {
await promiseFunc();
console.log('success!!!!!!!!!!')
}



console.log('aFunc', aFunc())

console.log('arrInclude', arrInclude())

document.body.style.backgroundColor = 'red';

asyncFunc();

启动编译,查看bundle.js文件发现babel默认只对Syntax做转换,对于Promise它未转换,可以使用@babel/runtime@babel/polyfill

info

babel默认只针对Syntal做转换,例如箭头函数、es6、class语法糖等,而自带的API需要原生内置的方法需要透过polyfill才能在浏览器正常运行。

@babel/runtime使用

@babel/runtime是由Babel提供的polyfill套件,由core.js和regenerator组成,core.js是用于JavaScript的组合标准化库,它包含了各种版本的polyfill的实现,而regenerator是来自facebook的一个函数库,主要用户实现generator/yeild,async.await等特性。

安装@babel/runtime

yarn add @babel/runtime -D

使用@babel-runtime还需要安装依赖的@babel/plugin-transform-runtime

yarn add @babel/plugin-transform-runtime -D

修改.babelrc

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false
}
]
]
}

从编译结果来看Class语法糖全局污染的问题已经结果,通过@babel/plugin-transform-runtime插件,它会帮助分析是否有polyfill的需求,并自动通过require的方式向@babel-runtime拿去polyfill,简单来说@babe/runtime提供了丰富的polyfill,开发者可自行使用require来导入自己需要的polyfill,但是谈过与繁琐和麻烦所以需要使用@babel/plugin-transform-runtime自动分析添加@babel/runtime中的polyfill,仔细观察编译结果来看Promise includes,接下来解锁@babel/runtime的所有功能配置。

corejs选项安装命令
falseyarn add @babel/runtime
2yarn add @babel/runtime-corejs2
3yarn add @babel/runtime-corejs3

修改babelrc文件

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
]
]
}

查看编译结果

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "aFunc": function() { return /* binding */ aFunc; },
/* harmony export */ "arrInclude": function() { return /* binding */ arrInclude; },
/* harmony export */ "promiseFunc": function() { return /* binding */ promiseFunc; }
/* harmony export */ });
/* harmony import */ var _babel_runtime_corejs2_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs2/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs2/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs2_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs2/helpers/classCallCheck */ "./node_modules/@babel/runtime-corejs2/helpers/esm/classCallCheck.js");
/* harmony import */ var _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs2/regenerator */ "./node_modules/@babel/runtime-corejs2/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babel/runtime-corejs2/core-js/promise */ "./node_modules/@babel/runtime-corejs2/core-js/promise.js");
/* harmony import */ var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3__);




// import "core-js/stable";
// import "regenerator-runtime/runtime";
var a = 1;
var b = 2;

var myClass = function myClass() {
(0,_babel_runtime_corejs2_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__["default"])(this, myClass);
};

var mine = new myClass();
console.log('mine', mine);
var aFunc = function aFunc() {
return a + b;
};
var arrInclude = function arrInclude() {
var arr = [1, 2, 3, 4];
return arr.includes(a);
};
var promiseFunc = function promiseFunc() {
return new (_babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3___default())(function (resolve) {
setTimeout(function () {
document.body.style.backgroundColor = 'green';
}, 3000);
});
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = (0,_babel_runtime_corejs2_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee() {
return _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return promiseFunc();

case 2:
console.log('success!!!!!!!!!!');

case 3:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

console.log('aFunc', aFunc());
console.log('arrInclude', arrInclude());
document.body.style.backgroundColor = 'red';
asyncFunc();

修改babelrc文件

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

编译结果如下

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "aFunc": function() { return /* binding */ aFunc; },
/* harmony export */ "arrInclude": function() { return /* binding */ arrInclude; },
/* harmony export */ "promiseFunc": function() { return /* binding */ promiseFunc; }
/* harmony export */ });
/* harmony import */ var _babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/classCallCheck */ "./node_modules/@babel/runtime-corejs3/helpers/esm/classCallCheck.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs3/regenerator */ "./node_modules/@babel/runtime-corejs3/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/includes */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/promise */ "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/set-timeout */ "./node_modules/@babel/runtime-corejs3/core-js-stable/set-timeout.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5__);






// import "core-js/stable";
// import "regenerator-runtime/runtime";
var a = 1;
var b = 2;

var myClass = function myClass() {
(0,_babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__["default"])(this, myClass);
};

var mine = new myClass();
console.log('mine', mine);
var aFunc = function aFunc() {
return a + b;
};
var arrInclude = function arrInclude() {
var arr = [1, 2, 3, 4];
return _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3___default()(arr).call(arr, a);
};
var promiseFunc = function promiseFunc() {
return new (_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4___default())(function (resolve) {
_babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5___default()(function () {
document.body.style.backgroundColor = 'green';
}, 3000);
});
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = (0,_babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee() {
return _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return promiseFunc();

case 2:
console.log('success!!!!!!!!!!');

case 3:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

console.log('aFunc', aFunc());
console.log('arrInclude', arrInclude());
document.body.style.backgroundColor = 'red';
asyncFunc();

从上面的结果可知corejs2版本主要针对底层API做编译如:Promise、Fetch等等;corejs3版本主要针对底层API和一些方法如Array.prototype.filter、Array.prototype.includes,简单来说如果要彻底解决兼容性的问题就需要使用corejs3的版本。

note

使用@babel/runtime能够在不污染全局环境的情况下提供相对的的polyfil,拥有自动识别的功能,一般情况下编译出来的文件要比使用@babel/polyfill要小,适合开发组件库或者对环境较为严格的方案。

@babel/polyfill

当babel版本小于7.0.4可以使用@babel/polyfill,最新版本已经废弃,@babel/polyfill是由stable版本的core-js和regenerator-runtime组成,可以直接下在这两个组件库当做@babel/polyfill来使用官方也推荐这样使用。还需要注意的是regenerator-runtime为@babel/runtime的相依套件,检查是否正确安装

修改.babelrc如下所示

{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": false,
"corejs": 3 // 当前 core-js 版本
}
]
]
}

使用同样的JavaScript源码进行测试结果发现和单纯使用babel一样,只针对语法(Syntax)做编译,那是因为尚未开启polyfill的功能,可通过useBuniltIns来修改模式,模式可选为false usage entry,如果使用usage模式结果和@babel/runtime-corejs3一样,自动识别需要require的语法,将兼容性问题彻底解决,不同的地方在于@babel/runtime在不污染全局变量环境的情况下提供了polyfill,而@babel/polyfill则是将需要兼容的新语法挂在到全局对象上,这样的做法造成了所谓的全局污染,如果使用entry模式。就比较简单了,没有使用任何主动识别,直接将整个关于ES环境代码挂在到全局对象,确保浏览器可以兼容所有的新特性,但这样缺陷也显而易见,会导致源代码体积变大,所以为什么要有entry模式,其实最主要的原因是babel默认不会检测第三方的依赖库,所以使用usage选项时可能会因为第三方库的源代码问题导致不能兼容,这事就有了使用entry模式的必要。

note

使用entry选项记得在代码的最前面添加import core-js/stableregenerator-runtime/runtime组件库

@babel/polyfill具有一次性导入或自动和别导入polyfill的功能,使用挂在全局对象的方法兼容新特性,适合开发应用。不太适合开发组件库或者第三方工具包,存在污染全局环境的问题。

小总结

  1. Babel版本<7.4.0>
    • 开发组件库、工具包选择@babel/runtime
    • 开发应用类项目选择@babel/polyfill
  2. Babel版本>=7.4.0
    • 配置较简单,会污染全局环境,选择@babel/polyfill
    • 配置较繁琐,不会污染全局环境选择@babel/runtime

ts-loader

首先修改webpack的配置文件

webpack.common.js
  module: {
rules: [{
test: /\.ts$/,
use: ['ts-loader'],
}],
},

修改tsconfig.json

{
"compilerOptions":{
"target": "es3",
"strict":true,
"lib": [
"dom",
"es2015",
"scripthost"
],
}
}

async await为例,首先需要安装Promise的polyfill

yarn add es6-promise -D

源代码最前面需要手动引入polyfill

import "es6-promise/auto";
...

编译结果中添加了promise的polyfill,所有的类似需要兼容的API都需要添加对应的polyfill,比较繁琐。

总结

Babel的生态和编译提供的polyfill更为丰富,配置更方便,推荐使用Babel,后续文章中也会完成在React中使用typescript并且使用babel编译源代码。

· 31 min read
石晓波

交叉类型

交叉类型是将多个类型合并为⼀个类型。 这让我们可以把现有的多种类型叠加到⼀起成为⼀种类型, 它包含了所需的所有类型的特性。
在 JavaScript 中,混⼊是⼀种⾮常常⻅的模式,在这种模式中,你可以从两个对象中创建⼀个新对 象,新对象会拥有着两个对象所有的功能。

function mixin<T extends object, U extends object>(first: T, second: U): T & U {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}

return result;
}

const x = mixin({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
console.log(x.a);
console.log(x.b);

联合类型

JavaScript 中,希望属性为多种类型之⼀,如字符串或者数组。 这就是联合类型所能派上⽤场的地⽅(它使⽤ | 作为标记,如 string | number)。

function formatCommandline(command: string[] | string) {
let line = '';
if (typeof command === 'string') {
line = command.trim();
} else {
line = command.join(' ').trim();
}
}

类型别名

type some = boolean | string
const b: some = true // ok
const c: some = 'hello' // ok
const d: some = 123 // 不能将类型“123”分配给类型“some”
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}

typeinterface的区别:
interface 只能⽤于定义对象类型,⽽ type 的声明⽅式除了对象之外还可以定义交叉、联合、原始类 型等,类型声明的⽅式适⽤范围显然更加⼴泛。
但是interface也有其特定的⽤处:

  • interface ⽅式可以实现接⼝的 extends 和 implements
  • interface 可以实现接⼝合并声明
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

接⼝创建了⼀个新的名字,可以在其它任何地⽅使⽤,类型别名并不创建新名字,⽐如,错误信息就不会使⽤别名。

可辨识联合类型

先假设⼀个场景,现在⼜两个功能,⼀个是创建⽤⼾即 create ,⼀个是删除⽤⼾即 delete . 我们先定义⼀下这个接⼝,由于创建⽤⼾不需要id,是系统随机⽣成的,⽽删除⽤⼾是必须⽤到 id 的,那么 代码如下:

interface Info {
username: string
}
interface UserAction {
id?: number
action: 'create' | 'delete'
info: Info
}

上⾯的接⼝是不是有什么问题? 是的,当我们创建⽤⼾时是不需要 id 的,但是根据上⾯接⼝产⽣的情况,以下代码是合法的:

const action:UserAction = {
action:'create',
id: 111,
info: {
username: 'xiaomuzhu'
}

}

但是我们明明不需要 id 这个字段,因此我们得⽤另外的⽅法,这就⽤到了上⾯提到的「字⾯量类型」了:

interface Info {
username: string
}

type UserAction = {
id: number
action: 'delete'
info: Info
} |
{
action: 'create'
info: Info
}
const UserReducer = (userAction: UserAction) => {
switch (userAction.action) {
case 'delete':
console.log(userAction.id);

break;
default:

break;
}
}
// 我们上面提到了 userAction.action 就是辨识的关键, 被称为可辨识的标签, 我们发现上面这种模式要想实现必须要三个要素:

// 具有普通的单例类型属性—可辨识的特征, 上文中就是 delete 与 create 两个有唯一性的字符串字面量
// 一个类型别名包含联合类型
// 类型守卫的特性, 比如我们必须用 if switch 来判断 userAction.action 是属于哪个类型作用域即 delete 与 create

interface Person {
name: string;
age: number;
}
const person = {} as Person;
person.name = 'xiaomuzhu';
person.age = 20;

字面量类型

字⾯量(Literal Type)主要分为 真值字⾯量类型(boolean literal types),数字字⾯量类型 (numeric literal types),枚举字⾯量类型(enum literal types),⼤整数字⾯量类型(bigInt literal types)和字符串字⾯量类型(string literal types)。

const a: 2333 = 2333 // ok
const ab : 0b10 = 2 // ok
const ao : 0o114 = 0b1001100 // ok
const ax : 0x514 = 0x514 // ok
const b : 0x1919n = 6425n // ok
const c : 'xiaomuzhu' = 'xiaomuzhu' // ok
const d : false = false // ok
const g: 'github' = 'pronhub' // 不能将类型“"pronhub"”分配给类型“"github"”

当字⾯量类型与联合类型结合的时候,⽤处就显现出来了,它可以模拟⼀个类似于枚举的效果:

type Direction = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: Direction) {
//...
}

类型字面量

类型字⾯量(Type Literal)不同于字⾯量类型(Literal Type),它跟 JavaScript 中的对象字⾯量的语法很 相似:

type Foo = {
baz: [
number,
'xiaomuzhu'
];
toString(): string;
readonly [Symbol.iterator]: 'github';
0x1: 'foo';
"bar": 12n;
};

类型断言

初学者经常会遇到的⼀类问题:

 const person = {};
person.name = 'xiaomuzhu'; // Error: 'name' 属性不存在于 ‘{}’
person.age = 20; // Error: 'age' 属性不存在于 ‘{}’

这个时候该怎么办?由于类型推断,这个时候 person 的类型就是 {} ,根本不存在后添加的那些属 性,虽然这个写法在js中完全没问题,但是开发者知道这个 person 实际是有属性的,只是⼀开始没 有声明⽽已,但是 typescript 不知道啊,所以就需要类型断⾔了:

interface Person {
name: string;
age: number;
}
const person = {} as Person;
person.name = 'xiaomuzhu';
erson.age = 20;

双重断言

interface Person {
name: string;
age: number;
}
const person = 'xiaomuzhu' as Person; // Error
const person = 'xiaomuzhu' as any as Person; // ok

类型守卫

intanceof、in

class Person {
name = 'xiaomuzhu';
age = 20;
}

class Animal {
name = 'petty';
color = 'pink';
}

function getSometing(arg: Person | Animal) {
if (arg instanceof Person) {
console.log(arg.color); // Error
console.log(arg.age); // ok
}
if (arg instanceof Animal) {
console.log(arg.color); // ok
console.log(arg.age); // Error
}
}

function getSometing(arg: Person | Animal) {
if ('age' in arg) {
console.log(arg.color); // Error
console.log(arg.age); // ok
}
if ('color' in arg) {
console.log(arg.age); // Error
console.log(arg.color); // ok
}
}

类型兼容性

结构类型

TypeScript ⾥的类型兼容性是基于「结构类型」的,结构类型是⼀种只使⽤其成员来描述类型的⽅ 式,其基本规则是,如果 x 要兼容 y,那么 y ⾄少具有与 x 相同的属性。x=y 我做⼀个简单的实验,构建⼀个类 Person ,然后声明⼀个接⼝ DogDog 的属性 Person 都拥有,⽽且还多了其他属性,这种情况下 Dog 兼容了 Person

 class Person {
constructor(public weight: number, public name: string, public born: string) {
}
}

interface Dog {
name: string
weight: number
}
let x: Dog
x = new Person(120, 'cxk', '1996-12-12') // OK

但反过来就不行,总结小的兼容大的

函数的兼容性

函数类型的兼容性判断,要查看 x 是否能赋值给 y,⾸先看它们的参数列表。 x 的每个参数必须能在 y ⾥找到对应类型的参数,注意的是参数的名字相同与否⽆所谓,只看它们的类 型。

let q = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = q; // OK
q = y; // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。

let foo = (x: number, y: number) => { };
let bar = (x?: number, y?: number) => { };
let bas = (...args: number[]) => { };

// foo = bar = bas;
// bas = bar = foo;
//当我们把 strictNullChecks 设置为 false 时上述代码是兼容的。

let foo2 = (x: number, y: number) => { };
let bar2 = (x?: number) => { };

// foo2 = bar // ok
// bar2 = foo2 //报错

参数多的兼容参数少的,也就是参数少的可以赋值给参数多的

类的类型兼容性

//仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查:
class Animal {
feet: number;
constructor(name: string, numFeet: number) {
this.feet = numFeet
}
}

class Size {
feet: number;
constructor(meters: number) {
this.feet = meters
}
}

let a: Animal = new Animal('a', 2);
let s: Size = new Size(1);

a = s; // OK
s = a; // OK

泛型的类型兼容性

泛型本⾝就是不确定的类型,它的表现根据是否被成员使⽤⽽不同.

interface Person<T> {

}
let x : Person<string>
let y : Person<number>
x = y // ok
y = x // ok

由于没有被成员使⽤泛型,所以这⾥是没问题的。
接着看如下案例:

interface Person<T> {
name: T
}
let x : Person<string>
let y : Person<number>
x = y // 不能将类型“Person<number>”分配给类型“Person<string>”。
y = x // 不能将类型“Person<string>”分配给类型“Person<number>”。

is关键字

function isString(test: any): test is string {
return typeof test === 'string';
}

function example(foo: number | string) {
if (isString(foo)) {
console.log('it is a string' + foo);
console.log(foo.length); // string function
} else {
console.log(foo)
}
}
example('hello world');

可调⽤类型注解

//我们已经可以用静态类型注解我们的函数、参数等等,但是假设我们有一个接口,我们如何操作才能让它被注解为可执行的:
interface ToString {
(): string
new(): string
}
declare const sometingToString: ToString;
sometingToString() // This expression is not callable. Type 'ToString' has no call signatures.ts(2349)
new sometingToString()

高级类型之索引类型、映射类型、条件类型

索引类型

先看⼀个场景,现在我们需要⼀个 pick函数,这个函数可以从对象上取出指定的属性,类似于 lodash.pick ⽅法。
javascript:

function pick(o, names) {
return names.map(n => o[n]);
}
const user = {
username: 'Jessica Lee',
id: 460000201904141743,
token: '460000201904141743',
avatar: 'http://dummyimage.com/200x200',
role: 'vip'
}
const res = pick(user, ['id'])
console.log(res) // [ '460000201904141743' ]

typescript简陋版:

interface Obj {
[key: string]: any
}
function pick(o: Obj, names: string[]) {
return names.map(n => o[n]);
}

高级框架版:

type key = keyof T === 'username' | 'id' ...

function pick<T, K extends keyof T>(o:T,names:K[]):T[K][]{
return names.map(n => o[n])
}
const res = pick(user, ['token', 'id', ])

映射类型

有⼀个User接⼝,现在有⼀个需求是把User接⼝中的成员全部变成可选的,我们应该怎么做?难 道要重新⼀个个 : 前⾯加上 ? ,有没有更便捷的⽅法?
这个时候映射类型就派上⽤场了,映射类型的语法是 [K in Keys] :

  • K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
  • Keys:字符串字⾯量构成的联合类型,表⽰⼀组属性名(的类型)
type partial<T> = { [K in keyof T]?: T[K] }
interface User3 {
username: string
id: number
token: string
avatar: string
role: string
}
type Keyof = keyof User3
type partial<T> = { [K in keyof T]?: T[K] }
type partialUser = partial<User3>
type readonlyUser = Readonly<User3>
//Required Pick Record Exclude Extract NonNullable

declare function f<T extends boolean>(x: T): T extends true ? string : number;

const x3 = f(Math.random() < 0.5)
const y3 = f(false)
const z = f(true)

条件类型

条件类型够表⽰⾮统⼀的类型,以⼀个条件表达式进⾏类型关系检测,从⽽在两种类型中选择其⼀:

T extends U ? X : Y

上⾯的代码可以理解为: 若 T 能够赋值给 U ,那么类型是 X,否则为 Y ,有点类似于JavaScript中的 三元条件运算符.

⽐如声明⼀个函数 f ,它的参数接收⼀个布尔类型,当布尔类型为 true 时返回 string 类型,否 则返回 number 类型:

declare function f<T extends boolean>(x: T): T extends true ? string : number;

const x = f(Math.random() < 0.5)
const y = f(false)
const z = f(true)

条件类型就是这样,只有类型系统中给出充⾜的条件之后,它才会根据条件推断出类型结果.

条件类型与联合类型

条件类型有⼀个特性,就是「分布式有条件类型」,但是分布式有条件类型是有前提的,条件类型⾥待检 查的类型必须是 naked type parameter . naked type parameter 指的是裸类型参数,怎么理解?这个「裸」是指类型参数没有被包装在其 他类型⾥,⽐如没有被数组、元组、函数、Promise等等包裹.

// 裸类型参数,没有被任何其他类型包裹即T
type NakedUsage<T> = T extends boolean ? "YES" : "NO"

// 类型参数被包裹的在元组内即[T]
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO";

这⼀部分⽐较难以理解,可以把「分布式有条件类型」粗略得理解为类型版的 map()⽅法 ,然后我 们再看⼀些实⽤案例加深理解.

/ 裸类型参数,没有被任何其他类型包裹即T
type NakedUsage<T> = T extends boolean ? "YES" : "NO"
// 类型参数被包裹的在元组内即[T]
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO";

type Distributed = NakedUsage<number | boolean> // = NakedUsage<number> | NakedUsage<boolean> = "NO" | "YES"
type NotDistributed = WrappedUsage<number | boolean> // "NO"



type Diff<T, U> = T extends U ? never : T;

type R = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
type Temp2 = never| "b"| never| "d"
// "b" | "d"|

type Filter<T, U> = T extends U ? T : never;
type R1 = Filter<string | number | (() => void), Function>;

// 剔除 null和undefined
type NonNullable2<T> = Diff<T, null | undefined>;

type R2 = NonNullable2<string | number | undefined>; // string | number

条件类型与映射类型

在⼀些有要求TS基础的公司,设计⼯具类型是⼀个⽐较⼤的考点.

//我有一个interface Part, 现在需要编写一个工具类型将interface中函数类型的名称取出来, 在这个题目示例中, 应该取出的是:
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
interface Part2 {
id2: number;
name2: string;
subparts3: Part[];
updatePart111(newName: string): void;
updatePart222(newName: string): void;
}

//这种问题我们应该换个思路,比如我们把interface看成js中的对象字面量,用js的思维你会如何取出?
//这个时候问题就简单了, 遍历整个对象, 找出value是函数的部分取出key即可.
//在TypeScript的类型编程中也是类似的道理, 我们要遍历interface, 取出类型为Function的部分找出key即可:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type R3 = FunctionPropertyNames<Part>;
type R89 = FunctionPropertyNames<Part2>;
// 1假设我们把Part代入泛型T, [K in keyof T]相当于遍历整个interface
// 2这时K相当于interface的key, T[K]相当于interface的value
// 3接下来, 用条件类型验证value的类型, 如果是Function那么将value作为新interface的key保留下来, 否则为never
// 4到这里我们得到了遍历修改后的新interface即:
// type R7 = {
// id: never;
// name: never;
// subparts: never;
// updatePart: "updatePart";
// }[keyof Part]
// type T = keyof Part
//但是我们的的要求是取出老interface Part的key, 这个时候再次用[keyof T]作为key依次取出新interface的value,
//但是由于id name和subparts的value为never就不会返回任何类型了, 所以只返回了'updatePart'.

强⼤的infer关键字

infer 是⼯具类型和底层库中⾮常常⽤的关键字,表⽰在 extends 条件语句中待推断的类型变量,相对 ⽽⾔也⽐较难理解,我们不妨从⼀个 typescript ⾯试题开始: 之前学过 ReturnType ⽤于获取函数的返回类型,那么如何设计⼀个 ReturnType ?
infer ⾮常强⼤,由于它的存在可以做出⾮常多的骚操作. tupleunion,⽐如[string, number] -> string | number:

type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number];
type ToUnion = ElementOf<ATuple>; // string | number
class TestClass {
constructor(public name: string, public age: number) { }
}
type ConstructorParameters5<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any
? P
: never;
type R4 = ConstructorParameters5<typeof TestClass> // [string, number]

//new (...args: any[]) => any指构造函数, 因为构造函数是可以被实例化的.
//infer P代表待推断的构造函数参数, 如果接受的类型T是一个构造函数, 那么返回构造函数的参数类型P, 否则什么也不返回, 即never类型

常用的工具类型解读

⽤ JavaScript 编写中⼤型程序是离不开 lodash 这种⼯具集的,⽽⽤ TypeScript 编程同样离不开类型 ⼯具的帮助,类型⼯具就是类型版的 lodash.
如下会介绍⼀些类型⼯具的设计与实现,如果项⽬不是⾮常简单的 demo 级项⽬,那么在开发过程中⼀定会⽤到它们。
起初,TypeScript 没有这么多⼯具类型,很多都是社区创造出来的,然后 TypeScript 陆续将⼀些常 ⽤的⼯具类型纳⼊了官⽅基准库内。
⽐如 ReturnType 、 Partial 、 ConstructorParameters 、 Pick 都是官⽅的内置⼯具类型. 其实上述的⼯具类型都可以被我们开发者⾃⼰模拟出来,本节学习⼀下如何设计⼯具类型.

泛型

可以把⼯具类型类⽐ js 中的⼯具函数,因此必须有输⼊和输出,⽽在TS的类型系统中能担当 类型⼊⼝的只有泛型.
⽐如 Partial ,它的作⽤是将属性全部变为可选.

type Partial<T> = { [P in keyof T]?: T[P] };

这个类型⼯具中,需要将类型通过泛型 T 传⼊才能对类型进⾏处理并返回新类型,可以说,⼀切类型 ⼯具的基础就是泛型.

类型递归

interface Company {
id: number
name: string
}

interface Person {
id: number
name: string
adress: string
company: Company
}

type R0 = Partial<Person>

type DeepPartial<T> = {
[U in keyof T]?: T[U] extends object
? DeepPartial<T[U]>
: T[U]
};

type R9 = DeepPartial<Person>

关键字

keyoftypeof 这种常⽤关键字我们已经了解过了,当然还有很常⽤的 Type inference infer 关键字的使⽤,还有之前的 Conditional Type 条件类型,现在主要谈⼀下另外⼀些常⽤关键字. + - 这两个关键字⽤于映射类型中给属性添加修饰符,⽐如 -? 就代表将可选属性变为必选, - readonly 代表将只读属性变为⾮只读. ⽐如TS就内置了⼀个类型⼯具 Required<T>,它的作⽤是将传⼊的属性变为必选项:

type Required<T> = { [P in keyof T]-?: T[P] };

常⻅⼯具类型

Omit

Omit 这个⼯具类型在开发过程中⾮常常⻅,以⾄于官⽅在3.5版本正式加⼊了 Omit 类型.
要了解之前先看⼀下另⼀个内置类型⼯具的实现 Exclude<T> :

type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<1 | 2, 1 | 3> // -> 2

Exclude 的作⽤是从 T 中排除出可分配给 U 的元素. 这⾥的可分配即 assignable ,指可分配的, T extends U 指T是否可分配给U Omit = Exclude + Pick

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

Omit<T, K> 的作⽤是忽略 T 中的某些属性.

Merge

Merge<O1, O2> 的作⽤是将两个对象的属性合并:

type O1 = {
name: string
id: number
}
type O2 = {
id: number
from: string
}
type R2 = Merge<O1, O2>

这个类型⼯具也⾮常常⽤,他主要有两个部分组成: Merge<O1, O2> = Compute<A> + Omit<U, T>
Compute 的作⽤是将交叉类型合并.即:

type Compute<A extends any> =
A extends Function
? A
: { [K in keyof A]: A[K] }
type R1 = Compute<{x: 'x'} & {y: 'y'}>

Merge的最终实现如下:

type Merge<O1 extends object, O2 extends object> =  
Compute<O1 & Omit<O2, keyof O1>>

Intersection

Intersection<T, U> 的作⽤是取 T 的属性,此属性同样也存在与 U.

type Props = { name: string; age: number; visible: boolean };
type DefaultProps = { age: number };

// Expect: { age: number; }
type DuplicatedProps = Intersection<Props, DefaultProps>;

实现

type Intersection<T extends object, U extends object> = Pick<
T,
Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>;

Overwrite

Overwrite<T, U>顾名思义,是⽤ U 的属性覆盖 T 的相同属性.

type Props = { name: string; age: number; visible: boolean };
type NewProps = { age: string; other: string };

// Expect: { name: string; age: string; visible: boolean; }
type ReplacedProps = Overwrite<Props, NewProps>

即:

type Overwrite<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;

Mutable

T 的所有属性的 readonly 移除

type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}

Record

Record 允许从 Union 类型中创建新类型,Union 类型中的值⽤作新类型的属性。

type Car = 'Audi' | 'BMW' | 'MercedesBenz'
type CarList = Record<Car, {age: number}>

const cars: CarList = {
Audi: { age: 119 },
BMW: { age: 113 },
MercedesBenz: { age: 133 },
}

在实战项⽬中尽量多⽤ Record,它会帮助规避很多错误,在 vue 或者 react 中有很多场景选择 Record 是更优解。

巧⽤类型约束

在 .tsx ⽂件⾥,泛型可能会被当做 jsx 标签

const toArray = <T>(element: T) => [element]; // Error in .tsx file.

加 extends 可破

const toArray = <T extends {}>(element: T) => [element]; // No errors.

模块与命名空间

模块系统

TypeScript 与 ECMAScript 2015 ⼀样,任何包含顶级 import 或者 export 的⽂件都被当成⼀个 模块。
相反地,如果⼀个⽂件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可⻅的。

模块语法

可以⽤ export 关键字导出变量或者类型,⽐如:

// export.ts
export const a = 1
export type Person = {
name: String
}

如果想⼀次性导出,那么你可以:

const a = 1
type Person = {
name: String
}
export { a, Person }
import { a, Person } from './export';

同样的也可以重命名导⼊的模块:

import { Person as P } from './export';

如果不想⼀个个导⼊,想把模块整体导⼊,可以这样:

import * as P from './export';

甚⾄可以导⼊后导出模块:

export { Person as P } from './export';

当然,除了上⾯的⽅法之外还有默认的导⼊导出:

 export default (a = 1)export default () => 'function'

命名空间

命名空间⼀个最明确的⽬的就是解决重名问题。
TypeScript 中命名空间使⽤ namespace 来定义,语法格式如下:

namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}

以上定义了⼀个命名空间 SomeNameSpaceName,如果需要在外部可以调⽤ SomeNameSpaceName 中的类和接⼝,则需要在类和接⼝添加 export 关键字. 其实⼀个 命名空间 本质上⼀个 对象 ,它的作⽤是将⼀系列相关的全局变量组织到⼀个对象的属性 你在⼿动构建⼀个命名空间,但是在 ts 中, namespace 提供了⼀颗语法糖。上述可⽤语法糖改写 成:

namespace Letter {
export let a = 1;
export let b = 2;
export let c = 3;
// ...
export let z = 26;

编辑成 js

var Letter;
(function (Letter) {
Letter.a = 1;
Letter.b = 2;
Letter.c = 3;
// ...
Letter.z = 26;
})(Letter || (Letter = {}));

命名空间的⽤处

命名空间在现代TS开发中的重要性并不⾼,主要原因是ES6引⼊了模块系统,⽂件即模块的⽅式使得开发 者能更好的得组织代码,但是命名空间并⾮⼀⽆是处,通常在⼀些⾮ TypeScript 原⽣代码的 .d.ts ⽂ 件中使⽤,主要是由于 ES Module 过于静态,对 JavaScript 代码结构的表达能⼒有限。 因此在正常的TS项⽬开发过程中并不建议⽤命名空间。

使⽤第三⽅ d.ts

Github 上有⼀个库 DefinitelyTyped 它定义了市⾯上主流的JavaScript 库的 d.ts ,⽽且我们可以很⽅ 便地⽤ npm 引⼊这些 d.ts。

编写 d.ts ⽂件

关键字 declare 表⽰声明的意思,我们可以⽤它来做出各种声明:

  • declare var 声明全局变量
  • declare function 声明全局⽅法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有⼦属性的)全局对象
  • interface 和 type 声明全局类型

TypeScript 的编译原理

编译器的组成

TypeScript有⾃⼰的编译器,这个编译器主要有以下部分组成:

  • Scanner 扫描器
  • Parser 解析器
  • Binder 绑定器
  • Emitter 发射器
  • Checker 检查器

编译器的处理

扫描器通过扫描源代码⽣成token流:

SourceCode(源码)+ 扫描器 --> Token 流

解析器将token流解析为抽象语法树(AST):

Token 流 + 解析器 --> AST(抽象语法树)

绑定器将AST中的声明节点与相同实体的其他声明相连形成符号(Symbols),符号是语义系统的主要构 造块:

AST + 绑定器 --> Symbols(符号)

检查器通过符号和AST来验证源代码语义:

AST + 符号 + 检查器 --> 类型验证

最后我们通过发射器⽣成JavaScript代码:

AST + 检查器 + 发射器 --> JavaScript 代码

编译器处理流程

TypeScript 的编译流程也可以粗略得分为三步:

  • 解析
  • 转换
  • 生成

结合上部分的编译器各个组成部分,流程如下图:

Locale Dropdown

验证成果

一道题 题目地址