网站搭建

nginx

https://juejin.cn/post/6887135998099062792

https://blog.csdn.net/Charissa2017/article/details/105886521

Nginx是一款轻量级的Web服务器,具有内存占用少,启动极快,高并发能力强的优势,采用事件驱动的异步非阻塞处理方式框架,IO性能好,时常用于服务端的反向代理和负载均衡。

正向代理与反向代理

反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。同时,用户不需要知道目标服务器的地址,也无须在用户端作任何设定。反向代理服务器通常可用来作为Web加速,即使用反向代理作为Web服务器的前置机来降低网络和服务器的负载,提高访问效率

正向代理是什么东东?反向代理又是啥?
「「正向代理」」
「「举个栗子」」 因为防火墙的原因, 直接访问谷歌是无法访问的, 这时可以借助梯子,即vpn,那么这个vpn(代理)就是代替客户端去访问服务器,然后将数据返回给客户端。服务器并不知道目标客户端,只是与vpn建立联系。
「「反向代理」」
「「举个栗子」」 你(客户端)去租房时,真正的房东(服务器)将房租给二手房东(代理服务器),而你通过二手房东租到的房子,也就是你与二手房东建立联系, 租到房子,并非真正的房东。
一句话: 正向代理 “代理”客户端, 反向代理“代理”服务器

nginx文件

配置文件

ps aux|grep nginx 查看 配置文件conf目录

nginx配置文件的基本结构

img

1
2
3
4
5
6
「「main」」:nginx的全局配置,对全局生效。
「「events」」:配置影响nginx服务器或与用户的网络连接。
「「http」」:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。
「「server」」:配置虚拟主机的相关参数,一个http中可以有多个server。
「「location」」:配置请求的路由,以及各种页面的处理情况。
「「upstream」」:配置后端服务器具体地址,负载均衡配置不可或缺的部分。
server
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
server {
listen 80;
server_name localhost;
root html;
index index.html index.htm;
}
##引入/etc/nginx/conf.d中的server配置
include /etc/nginx/conf.d/*.conf;

## /etc/nginx/conf.d/index.conf
server {
#端口号
listen 80;
#域名或者ip
#server_name 121.40.61.99;
server_name tomorrowlm.xyz;
#文件的路径
root /root/www;
#配置默认访问的页面
index index.html;
#配置多个项目
#location注意要和项目中配置的base一样
location /vue-demo {
try_files $uri $uri/ /vue-demo/index.html;
}
location /react-demo {
try_files $uri $uri/ /react-demo/index.html;
}

# location / {
# #当输入错误路径时,跳转到index.html页面
# try_files $uri $uri/ =404 /index.html;
# }
}

nginx默认监听的就是80端口, 也可以监听其他端口号

server_name就是你服务器的名称,可以精准匹配,也可以使用通配符或正则匹配,没有顺序,一般是先到先得。

root 你项目文件存放路径, 一般是放在html下面,也可放在其他地方, 如/var/local/marking-h5,n那么就改为 root /var/local/marking-h5(build打包后的dist文件在服务器上的路径)

index 你的项目入口, 通常是index.html.

1
2
3
4
5
6
7
8
9
#点击刷新后,页面就会显示(404),使用try_files(进行内部重定向)
try_files $uri $uri/ /react-demo/index.html;
#$uri 请求文件的路径
#$uri/ 请求目录的路径

try_files是nginx中http_core核心模块所带的指令,主要是能替代一些rewrite的指令,提高解析效率。
当用户请求 http://localhost/example 时,这里的 $uri 就是 /example。
try_files 会到硬盘里尝试找这个文件。如果存在名为 /$root/example(其中 $root 是项目代码安装目录)的文件,就直接把这个文件的内容发送给用户。
显然,目录中没有叫 example 的文件。然后就看 $uri/,增加了一个 /,也就是看有没有名为/$root/example/ 的目录。

部署多个网站

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
#配置代理
server {
listen 80;
server_name name1;
root /root/www;
#配置默认访问的页面
index index.html;
autoindex on;
#实现公用80端口 部署多个网站
location / {
proxy_pass http://localhost:4300;
}
}
server {
listen 4300;
server_name localhost;
root /root/www;
autoindex on;
location /vue-demo {
try_files $uri $uri/ /index.html;
}
}
server {
listen 80;
server_name name2;
root /root/www;
#配置默认访问的页面
index index.html;
autoindex on;
#实现公用80端口 部署多个网站
location / {
proxy_pass http://localhost:4400;
}
}
server {
listen 4400;
server_name localhost;
root /root/www;
autoindex on;
location /vue-demo {
try_files $uri $uri/ /index.html;
}
}

其他文件

1
2
3
4
/run 		nginx.pid
/etc/nginx nginx配置
/root 页面
/var/log/nginx/access.log 报错信息

nginx命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nginx 							#打开 nginx
nginx -t    #测试配置文件是否有语法错误
nginx -s reopen #重启Nginx
nginx -s reload #重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s stop  #强制停止Nginx服务
nginx -s quit  #停止Nginx服务(即处理完所有请求后再停止服务)
systemctl start nginx #开启防火墙
service nginx start #启动nginx
nginx [-?hvVtq] [-s signal] [-c filename] [-p prefix] [-g directives]

-?,-h           : 打开帮助信息
-v              : 显示版本信息并退出
-V              : 显示版本和配置选项信息,然后退出
-t              : 检测配置文件是否有语法错误,然后退出
-q              : 在检测配置文件期间屏蔽非错误信息
-s signal       : 给一个 nginx 主进程发送信号:stop(强制停止), quit(优雅退出), reopen(重启), reload(重新加载配置文件)
-p prefix       : 设置前缀路径(默认是:/usr/share/nginx/)
-c filename     : 设置配置文件(默认是:/etc/nginx/nginx.conf)
-g directives   : 设置配置文件外的全局指令

tip:

  • nginx: [error] open() “/run/nginx.pid” failed (2: No such file or directory)

    1
    nginx -c /etc/nginx/nginx.conf

PM2

PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。

1
pm2 start ./bin/www --watch --name my-api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用法
$ npm install pm2 -g # 命令行安装 pm2
$ pm2 start app.js --name my-api # 命名进程
$ pm2 list # 显示所有进程状态
$ pm2 monit # 监视所有进程
$ pm2 logs # 显示所有进程日志
$ pm2 stop all # 停止所有进程
$ pm2 restart all # 重启所有进程
$ pm2 reload all # 0秒停机重载进程 (用于 NETWORKED 进程)
$ pm2 stop 0 # 停止指定的进程
$ pm2 restart 0 # 重启指定的进程
$ pm2 startup # 产生 init 脚本 保持进程活着
$ pm2 web # 运行健壮的 computer API endpoint (http://localhost:9615)
$ pm2 delete 0 # 杀死指定的进程
$ pm2 delete all # 杀死全部进程

express

https://help.aliyun.com/document_detail/50775.html

https://www.jianshu.com/p/175558881f19

前端⼯程化

构建历史

https://mp.weixin.qq.com/s/vhkAhBJ2mok43lIlHpu8Gg

构建历史

前端模块化/构建工具从最开始的基于浏览器运行时加载的 RequireJs/Sea.js 到将所有资源组装依赖打包 webpack/rollup/parcelbundle类模块化构建工具,再到现在的bundleless基于浏览器原生 ES 模块的 snowpack/vite,前端的模块化/构建工具发展到现在已经快 10 年了。

  • Grunt

  • browserify

    browserify致力于在浏览器端使用CommonJs,他使用跟 NodeJs 一样的模块化语法,然后将所有依赖文件编译到一个bundle文件,在浏览器通过<script>标签使用的,并且支持 npm 库。

  • Gulp

  • webpack

    webpack1支持CommonJsAMD模块化系统,优化依赖关系,支持分包,支持多种类型 script、image、file、css/less/sass/stylus、mocha/eslint/jshint 的打包,丰富的插件体系。webpack的概念更偏向于工程化

    以上的 3 个库 Grunt/Gulp/browserify 都是偏向于工具,而 webpack将以上功能都集成到一起,相比于工具它的功能大而全。

  • rollup

    rollup编译ES6模块,提出了Tree-shaking,根据ES module静态语法特性,删除未被实际使用的代码,支持导出多种规范语法,并且导出的代码非常简洁,如果看过 vuedist 目录代码就知道导出的 vue 代码完全不影响阅读。

    rollup的插件系统支持:babelCommonJstersertypescript等功能。

    相比于browserifyCommonJsrollup专注于ES module
    相比于webpack大而全的前端工程化,rollup专注于纯javascript,大多被用作打包tool工具或library库。

    react、vue 等库都使用rollup打包项目,并且下面说到的vite也依赖rollup用作生产环境打包 js。

  • snowpack 和 vite

    因为 snowpack基于浏览器的模块化vite 比较类似,都是bundleless所以一起拿来说。bundleless类运行时打包工具的启动速度是毫秒级的,因为不需要打包任何内容,只需要起两个server,一个用于页面加载,另一个用于HMRWebSocket,当浏览器发出原生的ES module请求,server收到请求只需编译当前文件后返回给浏览器不需要管依赖。bundleless工具在生产环境打包的时候依然bundle构建所以依赖视图的方式,vite 是利用 rollup 打包生产环境的 js 的。

基于浏览器的模块化

CommonJS 和 EsModule

https://es6.ruanyifeng.com/#docs/module-loader

CommonJS

一切的开始要从CommonJS规范说起。

CommonJS 本来叫ServerJs,其目标本来是为浏览器之外javascript代码制定规范,在那时NodeJs还没有出生,有一些零散的应用于服务端的JavaScript代码,但是没有完整的生态。

之后就是 NodeJsCommonJS 社区的规范中吸取经验创建了本身的模块系统。

核心思想:允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exportsmodule.exports 来导出需要暴露的接口。

1
2
3
4
5
6
7
8
9
10
// a.js
//本质上是将要导出的对象赋值给module对象的export属性,然后在其他文件中通过require这个方法访问该属性
exports.a = 'Hello world'; // 相当于:module.exports.a = 'Hello world';
module.exports = {
a: 'Hello world'
}
}
// b.js
var moduleA = require('./a.js');
console.log(moduleA.a); // 打印出hello world

优点:服务器端模块重用,NPM中模块包多,有将近20万个。

缺点:加载模块是同步的,只有加载完成后才能执行后面的操作,也就是当要用到该模块了,现加载现用,不仅加载速度慢,而且还会导致性能、可用性、调试和跨域访问等问题。Node.js主要用于服务器编程,加载的模块文件一般都存在本地硬盘,加载起来比较快,不用考虑异步加载的方式,因此,CommonJS规范比较适用。然而,这并不适合在浏览器环境,同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。

require的加载过程是同步的,所以必须等到引入的文件(模块)加载完成之后,才会继续执行其它代码,会产生阻塞现象,因为引入一个文件,则该文件内部的所有代码都会被执行一次。

环境

  • 服务器端的 Node.js
  • Browserify,浏览器端的 CommonJS 实现,可以使用 NPM 的模块,但是编译打包后的文件体积可能很大
  • modules-webmake,类似Browserify,还不如 Browserify 灵活
  • wreq,Browserify 的前身

ES Module

ECMAScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require

  • 命名导出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // B.js
    /*-------- 单个变量或函数导出 ----------*/
    export function show() { console.log('show方法被调用') }

    export let count = 3

    /*-------- 批量导出 ----------*/
    function show() { console.log('show方法被调用') }
    let count = 3
    export {show, count} // 解构赋值语法(as关键字在这里表示将newA作为a的数据接口暴露给外部,外部不能直接访问a)

    第一种是单个的变量或函数导出,只需要直接在开头使用 export 关键字即可;

    第二种情况是批量地把多个变量或函数导出,只需要把它们储存到一个对象中即可

  • 默认导出

    1
    2
    3
    4
    5
    // B.js
    function show() { console.log('show方法被调用') }

    // 默认导出函数show
    export default show

    默认导出是在 export 关键词后面再跟上一个 default 表示导出的该变量或函数是匿名的

    注意: 一个模块只能默认导出一次,否则就会报错,具体原因会在后面讲解

  • 导入

    ES6 Module 的导入用到的关键字是 import ,具体代码如下

    1
    2
    3
    4
    5
    6
    // A.js
    import {show, count} from './B.js'

    show() // show方法被调用

    console.log(count) // 3

    ES6 Module的导入需要用一对 {} 大括号来接收我们需要导入的方法或函数

    注意: 大括号中的变量或函数名必须与导出时的名称一模一样

    那么如果我们想修改导入的变量或函数的名称,可以通过 as 关键词来命名,代码如下

    1
    2
    3
    4
    5
    6
    // A.js
    import {show as print, count as number} from './B.js'

    print() // show方法被调用

    console.log(number) // 3

    如果我们要想将所有的变量或函数都导入,可以通过 * 来整体导入,代码如下

    1
    2
    3
    4
    5
    import * as bModule from './B.js'

    bModule.show() // show方法被调用

    console.log(bModule.count) // 3

    * 表示全部的意思,我们将其全部导入,并赋值给 bModule,这样我们就可以通过 bModule 获取想要的变量或对象了

    以上所说的都是针对命名导出的变量或函数,那么如何导入一个默认导出的变量或函数呢?

    1
    2
    3
    4
    // 将通过 export default 导出的变量导入
    import print from './B.js'

    print() // show方法被调用

    命名导出的变量都是通过 {} 来接收的,那么去掉 {} ,接收的就是默认导出的变量了,因为导出的变量是匿名的,因此我们可以随意地起个变量名用于接收

    补充: 这里特别提一下,与CommonJS不同,ES6 Module 的导入文件路径是不支持表达式的

优点:

  • 容易进行静态分析
  • 面向未来的 ECMAScript 标准

缺点:

  • 原生浏览器端还没有实现该标准
  • 全新的命令字,新版的 Node.js才支持
加载规则

浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

1
<script type="module" src="./foo.js"></script>

上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

1
2
3
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

1
<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
1
<script type="module" src="./foo.js" async></script>

一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

CommonJS 与ES Module的区别

https://mp.weixin.qq.com/s/1wUU-i3W4RlR2hf86lZqEA

这两者的主要区别主要有以下两点:

  1. 对于模块的依赖,CommonJS是动态的,ES6 Module 是静态的

    对于模块的依赖,何为动态?何为静态

    动态是指对于模块的依赖关系建立在代码执行阶段;静态是指对于模块的依赖关系建立在代码编译阶段;

    • CommonJS

      • CommonJS导入时,require 的路径参数是支持表达式的

        1
        2
        3
        // A.js
        let fileName = 'example.js'
        const bModule = require('./' + fileName)

        因为该路径在代码执行时是可以动态改变的,所以如果在代码编译阶段就建立各个模块的依赖关系,那么一定是不准确的,只有在代码运行了以后,才可以真正确认模块的依赖关系,因此说CommonJS是动态的。

      • CommonJs支持动态导入,可以在语句中,使用require语法

        1
        2
        3
        4
        5
        6
        let lists = ["./index.js", "./config.js"]
        lists.forEach((url) => require(url)) // 动态导入

        if (lists.length) {
        require(lists[0]) // 动态导入
        }
    • Es Module

      只能声明在该文件的最顶部,不能动态加载语句

      1
      2
      3
      if (true) {
      import xxx from 'XXX' // 报错
      }
  2. CommonJS导入的是值的拷贝,ES6 Module导入的是值的引用

    首先来验证CommonJS,代码如下

    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
    // B.js
    let count = 3

    function change() {
    count ++ // 变量count + 1
    console.log('原count值为:', count); // 打印B.js模块中count的值
    }

    module.exports = {
    count,
    change
    }

    // A.js
    let count = require('./B.js').count
    let change = require('./B.js').change

    console.log('改变前:', count);
    change() // 调用模块B.js中的change方法,将原来的count + 1
    console.log('改变后:', count);

    // 运行A.js文件的结果
    改变前:3
    原count值为:4
    改变后:3

    在上述代码中我们可以看到,在 A.js 文件中导入了 B.js 文件中的变量 count 和 函数 change,因为导入的 count 只是对原有值的一个拷贝,因此尽管我们调用了函数 change 改变了 B.js 文件中变量 count 的值,也不会影响到 A.js 文件中的变量 count

    根据这个结果得出结论:CommonJS导入的变量是对原值的拷贝

  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

RequireJs 和 AMD

CommonJs 是一套同步模块导入规范,但是在浏览器上还没法实现同步加载,这一套规范在浏览器上明显行不通,所以基于浏览器的异步模块 AMD规范诞生。AMD规范(Asynchronous Module Definition,异步模块定义)是RequireJS在推广模块化开发的过程中提出的一种规范。

其核心接口是:define(id, 『dependencies』, factory) ,它要在声明模块的时候指定所有的依赖 dependencies ,并且还要当做形参传到factory 中,对于依赖的模块提前执行依赖前置

AMD规范采用依赖前置,先把需要用到的依赖提前写在 dependencies 数组里,在所有依赖下载完成后再调用factory回调,通过传参来获取模块,同时也支持require("beta")的方式来获取模块,但实际上这个require只是语法糖,模块并非在require的时候导入,而是跟前面说的一样在调用factory回调之前就被执行,关于依赖前置和执行时机这点在当时有很大的争议,被 CommonJs社区所不容。

1
2
3
4
define("module", ["dep1", "dep2"], function(d1, d2) {
return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });

优点:在浏览器环境中异步加载模块;并行加载多个模块;

缺点:开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;不符合通用的模块化思维方式,是一种妥协的实现;

Sea.js 和 CMD

在不断给 RequireJs 提建议,但不断不被采纳后,玉伯结合RequireJsmodule/2.0规范写出了基于 CMD(Common Module Definition)规范的Sea.js

Common Module Definition 规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。CMD规范(Common Module Definition,通用模块定义)是SeaJS在推广模块化开发的过程中提出的一种规范。

1
2
3
4
5
6
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})

优点:依赖就近,延迟执行(对于依赖的模块延迟执行,即只在需要用到某个模块的时候再require) 可以很容易在 Node.js 中运行;
缺点:依赖 SPM 打包,模块的加载逻辑偏重;
实现:Sea.js ;coolie

rollup

概念

2015 年,前端的ES module发布后,rollup应声而出。

rollup编译ES6模块,提出了Tree-shaking,根据ES module静态语法特性,删除未被实际使用的代码,支持导出多种规范语法,并且导出的代码非常简洁。react、vue 等库都使用rollup打包项目,并且下面说到的vite也依赖rollup用作生产环境打包 js。

  • 相比于browserifyCommonJsrollup专注于ES module

  • 相比于webpack大而全的前端工程化,rollup专注于纯javascript,可以生成轻量、快速以及低复杂度的library和应用程序。

  • rollup的插件系统支持:babelCommonJstersertypescript等功能。

Rollup VS Webpack

源码img

webpack打包后的文件 img

rollup打包后的文件 img

webpack致力于复杂SPA的模块化构建,优势在于:

  1. 通过loader处理各种各样的资源依赖
  2. HMR模块热替换
  3. 按需加载
  4. 提取公共模块

rollup致力于打造性能出众的类库,有如下优势:

  1. 编译出来的代码可读性好
  2. rollup打包后生成的bundle内容十分干净,没有什么多余的代码,只是将各个模块按照依赖顺序拼接起来,所有模块构建在一个函数内(Scope Hoisting), 执行效率更高。相比webpack(webpack打包后会生成__webpack_require__等runtime代码),rollup拥有无可比拟的性能优势,这是由依赖处理方式决定的,编译时依赖处理(rollup)自然比运行时依赖处理(webpack)性能更好
  3. 对于ES模块依赖库,rollup会静态分析代码中的 import,并将排除任何未实际使用的代码:tree-shaking
  4. 支持程序流分析,能更加正确的判断项目本身的代码是否有副作用(配合tree-shaking)
  5. 支持导出es模块文件(webpack不支持导出es模块) 但是模块过于静态化,HMR很难实现

通过以上的对比可以得出,构建App应用时,webpack比较合适,如果是类库(纯js项目),rollup更加适合。

webpack构建App的优势体现在以下几方面:

  1. 强大的插件生态,主流前端框架都有对应的loader
  2. 面向App的特性支持,比如之前提到的HMR按需加载公共模块提取等都是开发App应用必要的特性
  3. 简化Web开发各个环节,包括图片自动base64,[资源缓存](https://www.zhihu.com/search?q=%E8%B5%84%E6%BA%90%E7%BC%93%E5%AD%98&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra=%7B%22sourceType%22%3A%22article%22%2C%22sourceId%22%3A75717476%7D)(chunkId),按路由做代码拆分,懒加载
  4. 可靠的依赖模块处理,不像rollup那样仅面向ES module,面临cjs的问题(webpack通过__webpack_require__实现各种类型的模块依赖问题)

rollup的优势在于构建高性能的bundle,这正是类库所需要的。

副作用

https://blog.csdn.net/zz_jesse/article/details/108289318

Tree-shaking

图片

以上代码最终打包后 b 的声明就会被删除掉。

这依赖ES module的静态语法,在编译阶段就可以确定模块的导入导出有哪些变量。

CommonJs 因为是基于运行时的模块导入,其导出的是一个整体,并且require(variable)内容可以为变量,所以在ast编译阶段没办法识别为被使用的依赖。

webpack4中也开始支持tree-shaking,但是因为历史原因,有太多的基于CommonJS代码,需要额外的配置。

parcel

上面提到过webpack的两个缺点,而parcel的诞生就是为了解决这两个缺点,parcel 主打极速零配置

打包工具 时间
browserify 22.98s
webpack 20.71s
parcel 9.98s
parcel - with cache 2.64s

以上是 parcel 官方的一个数据,基于一个合理大小的应用,包含 1726 个模块,6.5M 未压缩大小。在一台有 4 个物理核心 CPU 的 2016 MacBook Pro 上构建。

parcel 使用 worker 进程去启用多核编译,并且使用文件缓存。

parcel 支持 0 配置,内置了 html、babel、typescript、less、sass、vue等功能,无需配置,并且不同于webpack只能将 js 文件作为入口,在 parcel 中万物皆资源,所以 html 文件 css 文件都可以作为入口来打包。

所以不需要webpack的复杂配置,只需要一个parcel index.html命令就可以直接起一个自带热更新的server来开发vue/react项目。

parcel 也有它的缺点:

  • 0 配置的代价,0 配置是好,但是如果想要配置一些复杂的配置就很麻烦。
  • 生态,相比于webpack比较小众,如果遇到错误查找解决方案比较麻烦。

原理

  1. commander 获取命令
  2. 启动 server 服务,启动 watch监听文件,启动 WebSocket 服务用于 hmr,启动多线程
  3. 如果是第一次启动,针对入口文件开始编译
  4. 根据扩展名生成对应asset资源,例如jsAssetcssAssetvueAsset,如果parcel识别 less 文件后项目内如果没有 less 库会自动安装
  5. 读取缓存,如果有缓存跳到第 7 步
  6. 多线程编译文件,调用 asset 内方法parse -> ast -> 收集依赖 -> transform(转换代码) -> generate(生成代码),在这个过程中收集到依赖,编译完结果写入缓存
  7. 编译依赖文件,重复第 4 步开始
  8. createBundleTree 创建依赖树,替换 hash 等,package打包生成最终代码
  9. watch文件发生变化,重复第 4 步,并将结果 7 通过WebSocket发送到浏览器,进行热更新。

一个完整的模块化打包工具就以上功能和流程。

webpack

概念

webpack是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。

网页中常见的静态资源

js–js,jax,coffee,ts(TypeScript,需要编译为js)

css– css,less,sass

image–jpg,png,gif,bmp,svg

字体文件(Fonts)–svg,ttf,eot,woff,woff2

模板文件–ejs,jade,vue(这是在webpack中定义的组件的方式)

什么是webpack

特点

  • 模块化开发

    在没有各个 webpack 搭建的脚手架(create-react-app、vue-cli 等等)之前,我们通过在 HTML5 文件里引入一个个 Javascript 文件来进行开发,这就可能导致并行请求数量过多、存在重复代码等问题。

    而通过 webpack,我们可以使用 import、require 来进行模块化开发

    在 webpack 中一切皆模块,js、css、图片、字体都是模块,而且支持静态解析、按需打包、动态加载、代码分离等功能,帮助我们优化代码,提升性能。

  • 新语法

    Javascript、CSS 的语法规范在不断更新,但是浏览器的兼容性却不能同步的更新,开发者可以通过 webpack 预处理器进行编译,自由的使用 JS、CSS 等语言的新语法

    webpack 使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源,如 Less、Sass、ES6、TypeScript。

  • 主流框架脚手架

    Vue 脚手架 vue-cli、React 脚手架 creact-react-app、Taro 脚手架 taro-cli 都是使用 webpack,开发者掌握 webpack 后,可以自由配置脚手架,根据项目需要,调整 webpack 配置,以提高项目性能。

文件搭建

https://juejin.cn/post/6844903968405979144

1
2
npm init
npm install webpack

webpack.base.conf.js ==> webpack基本配置;

webpack.dev.conf.js ==> webpack开发环境配置;

webpack.prod.conf.js ==> webpack生产环境配置;

配置

entry

1
entry: path.resolve(__dirname, "./src/main.js"),

output

1
2
3
4
5
6
7
8
9
output: {
//指定打包好的文件,输出到哪个目录中去
path: path.resolve(__dirname, "./dist"),
//输出文件名
filename: "js/bundle.[hash].js",//打包同步代码
chunkFilename:"js/bundle.chunk.[hash].js",//打包异步代码,动态导入
//静态文件打包存放的目录.静态文件是指 img 的src ,link ,script 标签等所指向的文件。静态资源最终访问路径 =output.publicPath + 资源loader或插件等配置路径
publicPath: '',
},

mode

webpack5 提供了模式选择,包括开发模式、生产模式、空模式,并对不同模式做了对应的内置优化。可通过配置模式让项目性能更优。

Sourcemap

https://mp.weixin.qq.com/s/g5GcZ10G89Xb9hZ1SibVrA

Sourcemap 本质上是一个信息文件(储存着代码转换前后的对应位置信息),关联编译后的代码和源码的,通过一个个行列号的映射。比如编译后代码的第 3 行第 4 列,对应着源码里的第 8 行第 5 列这种,这叫做一个mapping。简单说 Sourcemap 构建了处理前以及处理后的代码之间的一座桥梁,方便定位生产环境中出现 bug 的位置。

sourcemap 的格式如下:

1
2
3
4
5
6
7
8
9
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["a", "b"],
mappings: "AAgBC,SAAQ,CAAEA;AAAEA",
sourcesContent: ['const a = 1; console.log(a)', 'const b = 2; console.log(b)']
}

version 是版本号,file 是文件名,sourceRoot 是源码根目录,names 是转换前的变量名,sources 是源码文件,sourcesContent 是每个 sources 对应的源码的内容,mappings 就是一个个位置映射了。

为什么 sources 可以有多个呢?

因为可能编译产物是多个源文件合并的,比如打包,一个 bundle.js 就对应了 n 个 sources 源文件。

为什么要把变量名单独摘出来到 names 里呢?

因为这样就可以通过下标来索引了,mapping 里面就不用保存变量名,只保留 names 的索引就行。

重点是 mappings 部分:

mappings 部分是通过分号; 和逗号 , 分隔的:

1
mappings:"AAAAA,BBBBB;CCCCC"

一个分号就代表一行,这样就免去了行的映射。

然后每一行可能有多个位置的映射,用 , 分隔

那具体的每一个 mapping 都是啥呢?

比如 AAAAA 一共五位,分别有不同的含义:

  • 第一位:转换后代码的第几列(行数通过分号 ; 来确定)
  • 第二位:对应转换前的哪个源码文件,保存在 sources 里的,这里通过下标索引
  • 第三位:对应转换前的源码的第几行
  • 第四位:对应转换前的源码的第几列
  • 第五位:对应转换前的源码的哪个变量名,保存在 names 里的,这里通过下标索引
基础配置
  • eval:浏览器 devtool 支持通过 sourceUrl 来把 eval 的内容单独生成文件,还可以进一步通过 sourceMappingUrl 来映射回源码,webpack 利用这个特性来简化了 sourcemap 的处理,可以直接从模块开始映射,不用从 bundle 级别。
  • cheap:只映射到源代码的某一行,不精确到列,可以提升 sourcemap 生成速度
  • source-map:生成 sourcemap 文件,可以配置 inline,会以 dataURL 的方式内联,可以配置 hidden,只生成 sourcemap,不和生成的文件关联。
  • nosources:不生成 sourceContent 内容,可以减小 sourcemap 文件的大小
  • module:sourcemap 生成时会关联每一步 loader 生成的 sourcemap,配合 sourcemap-loader 可以映射回最初的源码

理解了这些基础配置项,根据 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$ 的规律来进行组合,就可以实现各种需求下的 sourcemap 配置。

当然,这种 sourcemap 配置还不够细致,比如 sourcemap 的 url 怎么生成,文件名是什么。如果想对这些做配置,可以关掉 devtool,启用 SourceMapDevToolPlugin 来配置。

虽然 webapck 的 sourcemap 配置方式比较多,但最底层也就是浏览器支持的文件级别的 sourcemap 还有 eval 代码的 source 映射和 sourcemap 这两种机制。其余的方式都是基于这两种机制的封装。

devtool

Sourcemap 本质上是一个信息文件关联编译后的代码和源码的,通过一个个行列号的映射。比如编译后代码的第 3 行第 4 列,对应着源码里的第 8 行第 5 列这种,这叫做一个mapping。

mapping里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。简单说 Sourcemap 构建了处理前以及处理后的代码之间的一座桥梁,方便定位生产环境中出现 bug 的位置。

在这里插入图片描述

externals

防止将某些 import 的包打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些*扩展依赖(external dependencies)*。

例如,从 CDN 引入 jQuery,而不是把它打包

在这里插入图片描述

通过script方式引入后,在文件中直接使用。这里是没有问题的,如果你使用了eslint,它会提示你该变量未定义。但是如果你想在文件中使用import $ from ‘jquery’;不好意思不行,因为你没有npm install jquery,那么如何让我们像安装了jquery一样使用这个东西呢?这就需要配置externals配置

proxy

该配置是为了解决前后端联调时出现的跨域问题,将后端域名下的请求代理到本地,从而避免跨域请求;

1
2
3
4
5
6
7
8
9
10
11
12
devServer: {
proxy: {
'/mgr': {
target: 'https://api.douban.com',
ws: true,
changeOrigin: true,
pathRewrite:{
'^/mgr': ''
}
}
}
},

接口是https://api.douban.com/v2/music/search?q=周杰伦

调用时就可以将接口写成这样/mgr/v2/music/search?q=周杰伦

那这个pathRewrite到底是干嘛用的,为什么有时候需要写,有时候不需要写

其实很简单,用代理, 首先你得有一个标识, 告诉node, 我接口只要是’/mgr’开头的才用代理.所以你的接口就要这么写 /mgr/xx/xx. 最后代理的路径就是 http://xxx.xx.com/mgr/xx/xx.

可是不对啊, 我正确的接口路径里面没有/mgr啊. 所以就需要 pathRewrite,用'^/mgr':'', 把’/mgr’去掉, 这样既能有正确标识, 又能在请求接口的时候去掉mgr

Resolve

resolve用来解析模块路径,帮助 webpack 找到 bundle 中以require/import引入的模块代码

  • extensions:extensions扩展名选项在resolve追踪到的文件如果没有扩展名时,会尝试在其提供的扩展名选项里进行匹配
  • alias:通过 resolve.alias 来自定义模块路径的映射
  • symlinks:用于配置 npm link 是否生效,禁用可提升编译速度。
1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js 
const path = require('path');
module.exports = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm'],//
alias: {
"@": path.resolve(__dirname, './src')
},
},
};

optimization

optimization 用于自定义 webpack 的内置优化配置,一般用于生产模式提升性能,常用配置项如下:

  • minimize:是否需要压缩 bundle;
  • minimizer:配置压缩工具,如 TerserPlugin、OptimizeCSSAssetsPlugin;
  • splitChunks:拆分 bundle;
  • runtimeChunk:是否需要将所有生成 chunk 之间共享的运行时文件拆分出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
optimization: {
runtimeChunk: 'single',//针对下面多个入口文件,相同的模块是不会共享的解决方案,
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin(),
],
splitChunks: {// 代码分割,提取模块。但是有多个入口文件的话。文件中引入相同的模块是不会共享
// include all types of chunks
chunks: 'all',
// 重复打包问题
cacheGroups:{
vendors:{ //node_modules里的代码
test: /[\\/]node_modules[\\/]/,
chunks: "all",
name: 'vendors', //chunks name
priority: 10, //优先级
enforce: true
}
}
},
},
}

loader

webpack默认只能打包处理 JS类型的文件,无法处理其他的非JS类型的文件

使用 loader

在你的应用程序中,有两种使用 loader 的方式:

  • 配置方式(推荐):在 webpack.config.js 文件中指定 loader。
  • 内联方式:在每个 import 语句中显式指定 loader。
配置参数
  • test 属性,识别出哪些文件会被转换。
  • use 属性,定义出在进行转换时,应该使用哪个 loader
  • type,资源模块类型
  • generator
资源模块类型

https://blog.csdn.net/qq_41887214/article/details/121631683

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。

  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。

  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。

  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

css

js代码里如果使用import导入一个样式文件style.less(代码如下),webpack碰到.less后缀的文件不知所措.因为它默认只能处理以.js.json结尾的文件.

1
2
//js文件
import "./style.less";

有了loader的赋能,webpack便有能力处理.less文件.

比如上面的配置代码,项目中一旦碰到导入以.less为后缀的样式文件,webpack会先将文件内容发送给less-loader处理,less-loader将所有less语法的样式转变成普通的css样式.

普通的css样式继续发送给css-loader处理,css-loader最主要的功能是解析css语法中的@import和图片路径,处理完后导入的css合并在了一起.

合并后的css文件再继续传递,发送给html-loader处理,它最终将样式内容插入到了html头部的style标签下,页面也因此添加了样式.

1
2
3
4
5
6
7
8
9
//loader加载器模块配置
module: {
rules: [
{
test: /\.less$/,//使用正则匹配
use: ["style-loader", "css-loader", "less-loader"],//从后往前调用的
}, //如果想要启用 CSS 模块化,可以为 css-loader 添加 modules 参数即可
],
},

loader在上面配置use数组中的执行顺序是从后往前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// sass-loader
// https://vue-loader.vuejs.org/zh/guide/pre-processors.html#sass
css: {
loaderOptions: {
sass: {
// webpack.docschina.org/loaders/sass-loader/#options
// webpack.docschina.org/loaders/sass-loader/#additionaldata
// cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders
additionalData: '@import "~@/style/variables.scss";'
}
}
},
}
img
1
2
3
4
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},

css加入img

js中加入img

font
1
2
3
4
5
6
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name]_[hash][ext]'
}
加载数据

JSON 文件,CSV、TSV 和 XML,可以使用 csv-loaderxml-loader

自定义输出文件名

1.output.assetModuleFilename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png$/,
type: 'asset/resource'
}
]
},
};

2.generator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png$/,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]'
}
}
]
},
};

使用此配置,所有 png 文件都将被发送到输出目录中的 images 目录中。

Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型。

但Rule.generator.filename 优先级高于 output.assetModuleFilename

自定义loader

https://zhuanlan.zhihu.com/p/397174187

背景:项目团队要为每个项目部署监控系统,一旦生产环境下js出现异常,要将报错信息及时上传到后台日志服务器.

在项目文件夹下创建一个文件error-loader.js,编写下面的测试代码(代码如下).

loader本质上是一个函数,参数content是一段字符串,存储着文件的内容,最后将loader函数导出就可以提供给webpack使用了.

webpack的配置文件在设置rules时(代码如下),只需要将use里的loader指向上面导出的loader函数的文件路径,这样webpack就能顺利引用loader了.另外我们还可以添加options属性给loader函数传参.

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
//error-loader.js
//loader函数
module.exports = function (content){
console.log(this.query); // { name: 'hello' }
return content;
}

//webpack.config.js
//webpack配置
module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:[
{
loader:path.resolve(__dirname,"./error-loader.js"),
options:{
name:"hello"
}
}
]
}
]
}
}

项目一旦启动打包,webpack检测到.js文件,它就会把文件的代码字符串传递给error-loader.js导出的loader函数执行.

我们上面编写的loader函数并没有对代码字符串content做任何操作,直接返回了结果.那么我们自定义loader的目的就是为了对content源代码做各种数据操作,再将操作完的结果返回.

比如我们可以使用正则表达式将content中所有的console.log语句全部去掉,那么最后我们生成的打包文件里就不会包含console.log.

插件

webpack

一个模块打包器,根据entry指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到output字段指定的文件中

webpack-dev-server

webpack-dev-server:一个服务器插件,相当于webpack+apache,启动一个web服务并实时更新修改
启动webpack-dev-server后,在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。

区别

  • webpack不会实时更新修改,就只是一个打包工具,webpack-dev-server会实时自动更新修改
  • webpack打包输出路径,output字段为path,webpack-dev-server打包输出路径,output字段为publicPath(此值为空时默认是项目根目录, contentBase:’src’,//指定托管的根目录)
  • webpack打包输出的文件,是真的存在于物理地址path中,而webpack-dev-server打包输出的文件,是保存在内存中的,在项目目录中是找不到的。

模块热更新(Hot Module Replacement)是指在浏览器运行过程中,替换、添加或删除模块,而无需重新加载整个页面。

html-webpack-plugin

webpack-dev-server实现了自动编译刷新浏览器,让编译出来的bundle.js托关于服务器根路径(电脑内存)中去。

html-webpack-plugin会创建一个在内存中生成一个html的插件,帮我们自动引入在内存中打包好的bundle.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
属性
title 生成html的标题
filename 生成html的文件名,默认是index.html,可以添加路径比如:src/index.html
template 模版的路径
templateParameters 模版需要的参数
inject

inject有四个值: true body head false

true 默认值,script标签位于html文件的 body 底部

body script标签位于html文件的 body 底部

head script标签位于html文件的 head中

false 不插入生成的js文件,这个几乎不会用到的

favicon html的favicon路径
meta 插入的meta标签内容 例如 meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}
minify 使用minify会对生成的html文件进行压缩
hash 对所有css 和 js文件追加webpack生成的hash值
cache 缓存,只有内容变化的时候生成新文件
showErrors 是否把错误输出到html文件
chunks chunks主要用于多入口文件,当你有多个入口文件,那就回编译后生成多个打包后的文件,那么chunks 就能选择你要使用那些js文件
excludeChunks 排除掉一些js
xhtml 一个布尔值,默认值是 false ,如果为 true ,则以兼容 xhtml 的模式引用文件。
chunksSortMode script的顺序,默认四个选项: none auto dependency {function}

'dependency' 不用说,按照不同文件的依赖关系来排序。

'auto' 默认值,插件的内置的排序方式,具体顺序....

'none' 无序?

{function} 提供一个函数?

webpack-manifest-plugin

webpack 通过 manifest,可以追踪所有模块到输出 bundle 之间的映射。通过 WebpackManifestPlugin 插件,可以将 manifest 数据提取为一个 json 文件以供使用。

cross-env

1、什么是cross-env?

它是运行跨平台设置和使用环境变量的脚本。

2、为什么需要cross-env?

针对相同的语句和命令,我们希望这条语句能够同时在 Windows 和 Linux 上使用。

这个问题主要是因为不同的操作系统平台对 Shell 脚本的支持情况不一样导致的。

例如,如果你希望在 Windows 中使用命令NODE_ENV=production 来设置环境变量的话,大多数Windows 命令提示符都没有办法进行操作。

同样的,Windows 和 POSIX 命令使用环境变量的方式也有所不同。

对于POSIX,您可以使用:$ENV_VAR ,但是在 Windows 上需要使用 %ENV_VAR% 来设置环境变量。

上面的情况就是针对不同的操作系统平台,设置环境变量中使用的变量引用是不同的。

例如,我们常常用到的设置环境变量。

针对不同的操作系统环境

1
2
3
4
windows
set NODE_ENV=production
linux
export NODE_ENV=production

process

  • process

    在node中,有全局变量process表示的是当前的node进程。

  • process.env

    process.env 属性返回的是一个包含用户环境信息的对象,它是区分开发环境或正式环境的依据。打开终端,输入node,输入process,就可以看到对应的描述信息

cross-env添加自定义变量

https://blog.csdn.net/qq_43277404/article/details/120902111

PWA

https://www.jianshu.com/p/7845a13a67d7

https://carljin.com/how-to-add-pwa-on-existed-project

PWA化主要解决了两大问题: 1)使web app有沉浸式体验,也就是更靠近原生体验。如:去掉浏览器的地址栏和底部工具栏;在桌面上生成图标,方便再次进入。 2)提供独立于浏览器的缓存,并且可以接收服务器的推送。如:在没有网络,或者网络状态较差的时候,仍可访问缓存在本地的数据.

编译优化

https://jelly.jd.com/article/61179aa26bea510187770aa3

https://www.psvmc.cn/article/2022-08-25-vue-cli-optimization.html

编译分析插件

webpack-bundle-analyzer

webpack-bundle-analyzer可以生成代码分析报告,可以直观地分析打包出的文件有哪些,及它们的大小、占比情况、各文件 Gzipped 后的大小、模块包含关系、依赖项等

image-20220825103406352

npm i -D webpackbar webpack-bundle-analyzer

1
2
3
4
5
6
7
8
9
10
11
12
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
// ...
plugins: [
new BundleAnalyzerPlugin(),
]
}
//package.json
"scripts": {
"analyz": "webpack-bundle-analyzer --port 8888 ./build/stats.json",
}

新版的 vue-cli 也内置了webpack-bundle-analyzer

1
2
3
"scripts": {
"analyz": "vue-cli-service build --report",
},

配置:

  • analyzerMode:server / static / json / disabled

    默认值:server。 在server 模式下,分析器将启动 HTTP 服务器以显示 bundle 报告。 在 static 模式下,将生成带有 bundle 报告的单个 HTML 文件。 在 json 模式下,将生成带有捆绑报告的单个 JSON 文件。 在 disable 模式下,您可以使用此插件通过将 generateStatsFile 设置为 true 来生成 Webpack Stats JSON 文件。

  • analyzerHost:默认值:127.0.0.1。 在 server 模式下用于启动 HTTP 服务器的主机。

  • analyzerPort:默认值:8888。在 server 模式下用于启动 HTTP 服务器的端口

  • reportFilename:默认值:report.html。 在 static 模式下生成的捆绑报告文件的路径。 它可以是绝对路径,也可以是相对于 bundle 文件输出目录的路径(在 webpack 配置中是 output.path)。

  • defaultSizes:stat / parsed / gzip

    默认值:parsed。 默认情况下在报告中显示的模块大小。

    stat:这是文件的“输入”大小,在进行任何转换(如缩小)之前。之所以称为“stat size”,是因为它是从 Webpack 的 stats 对象中获取的。

    parsed:这是文件的“输出”大小。 如果你使用的是 Uglify 之类的 Webpack 插件,那么这个值将反映代码的缩小后的大小。

    gzip:这是通过 gzip 压缩运行解析的包/模块的大小。

  • openAnalyzer:默认值:true。 在默认浏览器中自动打开报告。

  • genarateStatsFile:默认值:false。 如果为 true,将在 bundle 输出目录中生成 webpack stats JSON 文件

webpackbar

webpackbar提供了友好的编译进度提示

1
2
3
4
5
6
7
8
const WebpackBar = require('webpackbar');

module.exports = {
// ...
plugins: [
new WebpackBar(),
]
}

speed-measure-webpack-plugin

优化 webpack 构建速度,首先需要知道是哪些插件、哪些 loader 耗时长,方便我们针对性的优化。通过 speed-measure-webpack-plugin 插件进行构建速度分析,可以看到各个 loader、plugin 的构建时长,后续可针对耗时 loader、plugin 进行优化。

1
npm i -D speed-measure-webpack-plugin

构建速度优化

缓存

Webpack

Webpack 中几种缓存方式:

  • cache-loader
  • hard-source-webpack-plugin

以上这些缓存方式都有首次启动时的开销,即它们会让 “冷启动” 时间会更长,但是二次启动能够节省很多时间

babel-loader

babel-loader的options设置中增加cacheDirectory属性,属性值为true。表示:开启babel缓存,第二次构建时会读取之前的缓存,构建速度会更快一点。

1
2
3
4
5
6
7
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
cache-loader

https://webpack.docschina.org/loaders/cache-loader/

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
//...

module: {
//我的项目中,babel-loader耗时比较长,所以我给它配置了`cache-loader`
rules: [
{
test: /\.jsx?$/,
use: ['cache-loader','babel-loader']
}
]
}
}

如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory。

持久化缓存

通过配置 webpack 持久化缓存 cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度。

简单来说,通过 cache: filesystem 可以将构建过程的 webpack 模板进行缓存,大幅提升二次构建速度、打包速度,当构建突然中断,二次进行构建时,可以直接从缓存中拉取,可提速 90% 左右。

1
2
3
4
5
module.exports = {
cache: {
type: 'filesystem', // 使用文件缓存
},
}
hard-source-webpack-plugin

HardSourceWebpackPlugin 和 speed-measure-webpack-plugin 不能一起使用

vue
cache-loader

Vue-Cli自带cache-loader ,会默认为 Vue/Babel/TypeScript 编译开启。文件会缓存在 node_modules/.cache 中。

cache-loader 进行以下两个的缓存了

  • babel-loader 的 cacheDirectory 标志
  • vue-loader 的 cacheDirectory 标志
hard-source-webpack-plugin

这个插件能正常使用的版本是webpack5以下的版本。

npm install --save-dev hard-source-webpack-plugin

为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
configureWebpack: config => {
config.plugin.push(
// 为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source
new HardSourceWebpackPlugin({
root: process.cwd(),
directories: [],
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package.json', 'yarn.lock']
}
})
// 配置了files的主要原因是解决配置更新,cache不生效了的问题,配置后有包的变化,plugin会重新构建一部分cache
)
}
}
hash缓存

防止编译文件名字重复,部署版本的时候,浏览器使用缓存文件。同时,如果编译时文件未改动,不会改变文件名和文件的

hash、chunkhash、contenthash

hash是一整个项目,一次打包,只有一个hash值,是项目级的

chunhash是从入口entry出发,到它的依赖,以及依赖的依赖,依赖的依赖的依赖,等等,一直下去,所打包构成的代码块(模块的集合)叫做一个chunk,也就是说,入口文件和它的依赖的模块构成的一个代码块,被称为一个chunk。

contenthash是哈希只跟内容有关系,内容不变,哈希值不变。与chunkhash的区别可以举上面contenthash的例子,同时可以说明contenthash跟内容有关,但是chunkhash会考虑很多因素,比如模块路径、模块名称、模块大小、模块id等等。

1
2
3
4
5
6
7
8
9
output: {
filename: '[name].[contenthash].js', // contenthash 只有在内容发生改变才会变
path: path.resolve(__dirname, 'dist'), //输出路径 __dirname 代表当前文件的绝对路径
clean: true, //在生成文件之前清空 output 目录
},
vue-cli
configureWebpack ->
config.output.filename = `js/[name].[contenthash].js`;
config.output.chunkFilename = `js/[name].[contenthash].js`;

在提取css时我们也可以这么命名文件名

1
2
3
4
5
6
// css 提取
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:10].css',
}),
]
dll

将我们项目中的依赖使用dll插件进行动态链接,这样依赖就不会进行编译,从而极大地提高编译速度

webpack5 开箱即用的持久缓存是比 dll 更优的解决方案

将dll和缓存进行对比可以发现:

缓存 DLL
把常用的文件存储到内存或硬盘中 把公共代码打包为dll文件放到硬盘中
再次打包时,直接取读取缓存 再次打包时,读取dll文件,不重新打包
加载时间减少 打包时间减少

autodll-webpack-plugin

多线程

将文件解析任务分解成多个子进程并发执行,发挥多核 CPU 电脑的威力。子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构建速度

happypack

happypack 同样是用来设置多线程,但是在 webpack5 就不要再使用 happypack 了,官方也已经不再维护了,推荐使用 thread-loader。

npm install happypack -D

HappyPack 参数

  • id: String 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件.
  • loaders: Array 用法和 webpack Loader 配置中一样.
  • threads: Number 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
  • verbose: Boolean 是否允许 HappyPack 输出日志,默认是 true。
  • threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
  • verboseWhenProfiling: Boolean 开启webpack --profile ,仍然希望HappyPack产生输出。
  • debug: Boolean 启用debug 用于故障排查。默认 false
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
//提升 Webpack 构建速度
const HappyPack = require('happypack');
//安装 OS 模块 这个主要是拿到当前电脑的CPU核数
const os = require('os');
//这个是设置共享线程池中的数量 size 控制设置数量 类型 只能是 整数类型
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
module: {
rules: [
{
test: /\.js$/,
//把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
loader: 'happypack/loader?id=happyBabel',
//排除node_modules 目录下的文件
exclude: /node_modules/
},
{
test: /\.(css|less)$/,
use: 'happypack/loader?id=styles'
},
]
},
plugins: [
new HappyPack({
//用id来标识 happypack处理那里类文件
id: 'happyBabel',
//用法和loader 的配置一样
loaders: [{
loader: 'babel-loader?cacheDirectory=true',
}],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
}),
new HappyPack({
id: 'styles',
loaders: [ 'style-loader', 'css-loader', 'less-loader' ],
//共享进程池
threadPool: happyThreadPool,
});
]
}

vue

1
2
3
4
5
//把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
// config.module.rule('js').test(/\.js$/)
// .include.add('/src/').end()
// .exclude.add('/node_modules/').end()
// .use().loader('happypack/loader?id=happyBabel').end()
thread-loader
  • Webpack

    npm install –save-dev thread-loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const path = require("path");
    module.exports = {
    module: {
    rules: [
    {
    test: /\.js$/,
    include: path.resolve('src'),
    use: [
    "thread-loader",
    // 耗时的 loader (例如 babel-loader)
    ],
    },
    ],
    },
    };
  • Vue-Cli已经内置thread-loaderthread-loader 会在多核 CPU 的机器上为 Babel/TypeScript 转译开启。

    1
    2
    3
    module.exports = {
    parallel: true,
    }
    • Type: boolean

    • Default: require('os').cpus().length > 1

      是否为 Babel 或 TypeScript 使用 thread-loader

      该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。

缩小文件检索解析范围

alias
  • 为避免无用的检索与递归遍历,可以使用alias指定引用时候的模块
extensions
  • extensions 表示需要解析的文件类型列表。

    根据项目中的文件类型,定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度。

    由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 tsx 放在最左侧

    1
    2
    3
    4
    5
    module.exports = {
    resolve: {
    extensions: ['.tsx', '.js'], // 因为我的项目只有这两种类型的文件,如果有其他类型,需要添加进去。
    }
    }
noParse
  • noParse,对不依赖本地代码的第三方依赖不进行解析,比如CDN引用的第三方依赖。
include
  • 为 loader 指定 include,减少 loader 应用范围,仅应用于最少数量的必要模块,。

import优化

运用这个插件能在代码使用了import语法的情况下,大大提高代码的编译速度。

安装 babel-plugin-dynamic-import-node

1
npm install --save-dev babel-plugin-dynamic-import-node

vue-cli3

修改babel.config.js文件

1
2
3
4
5
6
7
8
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
env: {
development: {
plugins: ["dynamic-import-node"]
}
}
};

vue.cli2

.babelrc文件

1
2
3
4
5
6
7
8
9
"env": {
"test": {
"plugins": []
},
"development":{
"presets": ["env", "stage-2"],
"plugins": ["dynamic-import-node"]
}
}

打包/上线运行优化

某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来 minify(压缩) 和 mangle(混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:

  • TerserPlugin
  • [fullhash]/[chunkhash]/[contenthash]

代码分离

https://www.cnblogs.com/Mr-Hou88888/p/16217467.html

https://blog.csdn.net/qq_41887214/article/details/121646392

代码分离code splitting是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,加快打包速度,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

多入口起点

入口起点(entry points)

src/index.js

1
console.log('Hello world!');

src/another-module.js

1
2
3
import _ from 'lodash'

console.log(_.join(['another', 'module', 'chunk'], ' '));

这个模块依赖了 lodash ,需要安装一下:

1
npm install lodash

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
mode: 'development',
entry: { // 配置多入口文件
index: './src/index.js',
another: './src/another_module.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
},
}

执行webpack命令,可以看到报错了 ̄□ ̄||
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another_module.js'
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
}

执行webpack命令,可以看到不报错了,并且dist输出了两个js文件
在这里插入图片描述
在这里插入图片描述

文件another.bundle.js来源于entry.another,即src/another.js,文件大小为554kb,因为被lodash被打包进去了

文件index.bundle.js来源于entry.index,即src/index.js,文件大小为1.21kb

但是,如果我们的其他入口也需要使用lodash呢?

1
2
3
4
src/index.js
import _ from 'lodash'

console.log(_.join(['index', 'module', 'chunk'], ' '));

在这里插入图片描述

lodash在两个引用文件中都被打包了,我们期望lodash应该是公用的

配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js', // 启动时需加载的模块
dependOn: 'common_chunk', // 当前入口所依赖的入口
},
another: {
import: './src/another_module.js',
dependOn: 'common_chunk',
},

common_chunk: 'lodash' // 当上面两个模块有lodash这个模块时,就提取出来并命名为shared chunk
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
}

执行webpack命令,可以看到打包结果
在这里插入图片描述

已经提取出来common_chunk.bundle.js,即为提取打包了lodash公用模块

index.bundle.js another.bundle.js体积也变小

分离 Vendor

chunk-vendors.js :顾名思义,chunk-vendors.js 是捆绑所有不是自己的模块,而是来自其他方的模块的捆绑包,它们称为第三方模块或供应商模块。

通常,它意味着(仅和)来自项目 /node_modules 目录的所有模块,会将所有 /node_modules 中的第三方包打包到 chunk-vendors.js 中。

将所有的第三方包集中到一个文件,自然也会出现文件过大的问题。

img

可以看到,当前只有一个 chunk 也就是 app.js ,他是一个 entry chunk 。因为我们的 webpack 配置是这样子的:

1
2
3
4
5
6
// webpack.config.js
module.exports = {
entry: {
app: './src/main.js', // entry chunk
}
}

app.js 包含了我们的第三方库 vue 和 axios ,以及我们的业务代码 src 。

分离 Vendor,最简单方法就是:加一个 entry ( File Changes ):

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
entry: {
app: './src/main.js',
vendor: ['vue', 'axios'],
},
}
img

虽然 vendor.js 这个 entry chunk 包含了我们想要的 vue 和 axios ,但是细心的同学会发现, app.js 也包含了他们!为什么!?

其实这是很正常的事情:每个 entry 都包含了他自己的依赖,这样他才能作为一个入口,独立地跑起来。

很难受,事实上我们并不想 app.js 还包含了 vue 和 axios 。如果可以把他们俩相同的依赖提取出来就好了,就像这样:

img
SplitChunksPlugin

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash 模块去除:

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
entry: { // 多入口
index: './src/index.js',
another: './src/another_module.js',
},
output: {
filename: '[name].bundle.js', // 对应多个出口文件名
path: path.resolve(__dirname, './dist'),
},
optimization: {
splitChunks: { // 代码分割
// include all types of chunks
chunks: 'all'
}
},
}

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.jsanother.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小
在这里插入图片描述

CommonsChunkPlugin(已废弃)

现在,修改我们的 webpack 配置文件( File Changes ):

1
2
3
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
})

但是!随着业务的增长,我们依赖的第三方库代码很可能会越来越多,这时候我们的 webpack.config.js 就变成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
entry: {
app: './src/main.js',
vendor: [
'vue',
'axio',
'vue-router',
'vuex',
'element-ui',
// 很长很长
],
},
}

vendor entry 会变成很长很长,更糟糕的是,我们每次引入了新的第三方库,都需要在 vendor 手动增加对应的包名。

动态导入懒加载
  • import() 为动态加载脚本,webpack 会生成类似以上动态创建 script 标签的代码
  • import 里的注释为特殊含义的魔法注释,如果不设置 webpackChunkName,加载的脚本将被按数字次序命名

如果我们想「按需加载」路由组件的话,只要改几行代码就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//index.js
setTimeout(function () {
//文件会等5秒后加载,实现懒加载
// webpackChunkName: "dynamicImport":这是webpack动态导入模块命名的方式。
//浏览器看到的文件名:dynamicImport.chunk-test.js。output.chunkFilename配置命名格式
const add = () => import(
/* webpackChunkName: "dynamicImport" */
'./dynamicImport.js')
console.log(add(1, 2));
}, 5000)

//dynamicImport.js
export default function add(a,b){
return a+b;
}

动态import使用最多的一个场景是懒加载(比如路由懒加载)

  • 封装一个component.js,返回一个component对象;
  • 我们可以在一个按钮点击时,加载这个对象;

image-20220331133000083image-20220331133028694

如果你用了 Babel ,就需要装上这个插件@babel/plugin-syntax-dynamic-import:babel plugin syntax dynamic import 来解析 import() 语法。

预加载

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源。只会缓存资源不会解析
  • preload(预加载):当前导航下可能需要资源。会缓存资源并解析
prefetch

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

LoginButton.js

1
2
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

只要父 chunk 完成加载,webpack 就会添加 prefetch hint(预取提示)。

不同

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

babel

动态导入

如果你用了 Babel ,就需要装上这个插件@babel/plugin-syntax-dynamic-import:babel plugin syntax dynamic import 来解析 import() 语法。

1
2
3
4
// .babelrc 
{
"plugins": ["syntax-dynamic-import"]
}
console移除

babel-plugin-transform-remove-console插件,配置在babel.config.js中,vue-cli5实测可行,vue-cli3,4也可行。(尝试后,谷歌浏览器控制台仅websocket的打印输出未清除,其他打印输出都是清除干净了的)

下载依赖

npm install babel-plugin-transform-remove-console -D
or
yarn add babel-plugin-transform-remove-console -D

babel.config.js中

1
2
3
4
5
6
7
8
const proPlugins = [];
// 判断环境
if (process.env.NODE_ENV === 'production') {
proPlugins.push('transform-remove-console');
}
module.exports = {
plugins: [...proPlugins],
};
辅助代码

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!默认情况下会被添加到每一个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入。

@babel/plugin-transform-runtiome:禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtiome 并且使所有辅助代码从这里引用

先下载包:

npm i @babel/plugin-transform-runtime -D

img

资源模块

使用 webpack 资源模块 (asset module) 代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。

配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
include: [
paths.appSrc,
],
type: 'asset/resource',
},
]
}

SourceMap

最佳选择是 eval-cheap-module-source-map详细区分可至 webpack devtool 查看。

css抽离

mini-css-extract-plugin插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载

1
2
3
4
5
6
7
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[hash].css", // 定义抽离的入口文件的文件名
chunkFilename: "css/[name].[hash].css", // 定义非入口块文件的名称,如动态导入的文件
})
],

css压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
module: {
rules: [
{
test: /.(css|less)$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
},
],
},
+ optimization: {
+ minimizer: [
+ new CssMinimizerPlugin(),
+ ],
+ },
plugins: [new MiniCssExtractPlugin()],
};

这将仅在mode: production 生产环境开启 CSS 优化

如果还想在开发环境下启用 CSS 优化,optimization.minimize 设置为 true

gzip压缩

前端将文件打包成 .gz 文件,然后通过 nginx 的配置,让浏览器直接解析 .gz 文件,可以大大提升文件加载的速度,浏览器可以直接解析 .gz 文件并解压。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
启用gzip压缩(需要配置nginx,可以看出压缩后的文件大小明显变化)
highlighter- PHP

const CompressionWebpackPlugin = require('compression-webpack-plugin')
chainWebpack(config) {
// 生产模式下启用gzip压缩 需要配置nginx支持gzip
if (process.env.NODE_ENV === 'production') {
config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
{
filename: '[path][base].gz',
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
// 只处理大于xx字节 的文件,默认:0
threshold: 10240,
// 示例:一个1024b大小的文件,压缩后大小为768b,minRatio : 0.75
minRatio: 0.8, // 默认: 0.8
// 是否删除源文件,默认: false
deleteOriginalAssets: false
}
])
}
}

js压缩

terser-webpack-plugin

关于 source maps 说明

只对 devtool 选项的 source-mapinline-source-maphidden-source-mapnosources-source-map 有效
为何如此?

  • eval 会包裹 modules,通过 eval("string"),而 minimizer 不会处理字符串。
  • cheap 不存在列信息,minimizer 只产生单行,只会留下一个映射。
    使用支持的 devtool 值可以生成 source dmap。

parallel

使用多进程并发运行以提高构建速度。 并发运行的默认数量: os.cpus().length - 1 。并发运行可以显著提高构建速度,因此强烈建议添加此配置。对于实际环境,可以使用is-wsl插件来判断是否支持多进程。d

webpack

terser-webpack-plugin和uglifyjs-webpack-plugin(不推荐)

不再维护 uglify-es ,并且 uglify-js 不支持 ES6 +。

terser 是 uglify-es 的一个分支,主要保留了与 uglify-es 和 uglify-js@3 的 API 和 CLI 兼容性。

webpack5 自带最新的 terser-webpack-plugin,无需手动安装。

terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量: os.cpus().length - 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
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
parallel: true,
extractComments: true,
sourceMap: config.build.productionSourceMap,
terserOptions: {
output: {
// 是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,可以设置为false
beautify: false,
// 是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false
comments: false
},
compress: {
// 是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用不大的警告
warnings: false,
// 是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句
drop_console: true,
drop_debugger: true,
// 是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 5, 默认为不转换,为了达到更好的压缩效果,可以设 置为false
collapse_vars: true,
// 是否提取出现了多次但是没有定义成变量去引用的静态值,比如将 x = 'xxx'; y = 'xxx' 转换成var a = 'xxxx'; x = a; y = a; 默认为 不转换,为了达到更好的压缩效果,可以设置为false
reduce_vars: true,
pure_funcs: ['console.log'] // 移除console
}
}
}),
]
}
}
vue-cli
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
config.optimization.minimize(true)// 开启压缩js代码
config.optimization.splitChunks({ // 开启代码分割
chunks: 'all'
})

const TerserPlugin = require("terser-webpack-plugin")
chainWebpack: config => {

config.optimization.minimize(true);// 开启压缩js代码
config.optimization.minimize(new TerserPlugin({
terserOptions:{
compress:{
warnings: false,
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log"]
}
}
}));
config.optimization.splitChunks({// 开启代码分割
chunks: 'all',
});

}

img压缩

image-minimizer-webpack-plugin:用来压缩图片的插件

npm i image-minimizer-webpack-plugin imagemin -D
还有剩下包需要下载,有两种模式:

无损压缩
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
有损压缩
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

配置CDN

线上使用 cdn ,如何库有问题,项目就会有问题,除非公司有自己的 cdn 库,不过这确实也是一种优化方案,效果也还不错。它的配置也很简单,在 externals 中配置,例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 配置 cdn,这里将 vue,vue-router 和 axios 三个包配置成 cdn 引入
// 其中 Vue,VueRouter 等名称是该库暴露在全局中的变量名
config.externals = {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios'
}
}
}
}

然后在 public/index.html 模板文件中引入 cdn 地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<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" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title></title>
<!-- 引入 cdn 地址 -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.10/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.18.0/axios.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

我这里使用的是 bootcdn 的地址,需要注意版本问题。

也可以借助 HtmlWebpackPlugin 插件来方便插入 cdn 的引入。

使用 cdn 引入的方式虽然能极大改善网页加载速度,但我还是不会用这个功能,项目还不需要非得这样的优化,也怕 cdn 不稳定。

借助 HtmlWebpackPlugin 插件来方便插入 cdn 的引入

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
//生产环境标记
const IS_PRODUCTION = process.env.NODE_ENV === "production";
const path = require("path");
// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};

module.exports = {
configureWebpack: {
externals: {
vue: "Vue",
},
},
chainWebpack: config => {
config.plugin("html").tap(args => {
args[0].cdn = IS_PRODUCTION ? cdn_production : cdn_development;
return args;
});
}
};

index.html中添加

1
2
3
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>

插件按需加载

1. lodash

类似 import { throttle } from 'lodash' 就属于有副作用的引用,会将整个 lodash 文件进行打包。

优化方式是使用 import { throttle } from 'lodash-es' 代替 import { throttle } from 'lodash'lodash-es Lodash 库导出为 ES 模块,支持基于 ES modules 的 tree shaking,实现按需引入。

2. ant-design

ant-design 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 就会有按需加载的效果。

假如项目中仅引入少部分组件,import { Button } from 'antd' 也属于有副作用,webpack不能把其他组件进行tree-shaking。这时可以缩小引用范围,将引入方式修改为 import { Button } from 'antd/lib/button' 来进一步优化。

未引用代码移除

Tree Shaking(已经配置)

tree shaking是一个术语,用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如importexport

Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。

  • ESM: export + import
  • Common JS: module.exports + require

sideEffects 有三种情况

  • sideEffects:true 所有文件都有副作用,全都不可 tree-shaking

  • sideEffects:false 有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件

  • sideEffects:[] 部分 tree-shaking , 除了数组外都 tree-shaking

“side effect(副作用)” 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

如果你的代码确实有一些副作用,可以改为提供一个数组:

1
2
3
4
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}

所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
//或者
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
sideEffects: true
}
]
IgnorePlugin
  1. 这是webpack内置插件
  2. 这个插件的作用是:忽略第三方包指定目录,让这些指定目录不要被打包进去
1
2
3
4
//虽然我设置了语言为中文,但是在打包的时候,是会将所有语言都打包进去的。这样就导致包很大,打包速度又慢
plugins:[
new Webpack.IgnorePlugin(/\.\/locale/,/moment/),//moment这个库中,如果引用了./locale/目录的内容,就忽略掉,不会打包进去
]
css

打包时把没有用的 CSS 代码摇走,可以大幅减少打包后的 CSS 文件大小。使用 purgecss-webpack-plugin 对 CSS Tree Shaking。

插件

autoprefixer兼容css

webpak 引入autoprefixer,自动加上各种前缀让不同的浏览器得以支持

tip

报错hardsource:a3a09f18: Could not freeze ./node_modules/mini-css-extract-plugin/dist/loader.js

https://blog.csdn.net/weixin_43840202/article/details/124006937

babel

https://zhuanlan.zhihu.com/p/498461706

https://juejin.cn/post/7051555571451265038#heading-10

https://mp.weixin.qq.com/s/qT0y0JlDP7bxESRKip-f5w

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

简单来说就是把 JavaScript 中 es2015+ 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。比如在代码中使用了 ES6 的箭头函数,但是这种写法会在 IE 浏览器中报错,为了让代码能在IE中运行,就需要将代码编译成IE支持的写法,这就是babel的主要工作。

img

babel 在转译的时候,会将源代码分成 syntax 和 api 两部分来处理:

  • syntax:类似于展开对象、optional chain、let、const 等语法
  • api:类似于 [1,2,3].includes 等函数、方法

工作原理

babel的主要工作流程分为三个阶段,解析(parse),转换(transform),生成(generate)

如下图所示:

img

解析

通过 @babel/parser 把源代码字符串转成抽象语法树(AST),在解析过程中主要是两个阶段:词法分析语法分析

词法分析

词法分析阶段把字符串形式的代码解析成一个个具有实际意义的语法单元组成的数据,这种数据被称之为令牌(tokens)流。

语法分析

语法解析器把 Tokens 转换为抽象语法树 AST

被解析语法当中具备实际意义的最小单元,举个例子:’2008年奥运会在北京举行’,这句话不论词性主谓等关系,会把这句话拆分成: 2018年、奥运会、在、北京、举行,这就是分词,把整句话拆分成有意义的最小颗粒,这些最小颗粒不能再继续拆分,否则会失去表达意义。

JavaScript 中常见的语法单元如下:

  • 空白:JS中连续的空格、换行、缩进等这些如果不在字符串里,就没有任何实际逻辑意义,所以把连续的空白符直接组合在一起作为一个语法单元。
  • 注释:行注释或块注释,虽然对于人类来说有意义,但是对于计算机来说知道这是个“注释”就行了,并不关心内容,所以直接作为一个不可再拆的语法单元
  • 字符串:对于机器而言,字符串的内容只是会参与计算或展示,里面再细分的内容也是没必要分析的
  • 数字:JS语言里就有16、10、8进制以及科学表达法等数字表达语法,数字也是个具备含义的最小单元
  • 标识符:没有被引号扩起来的连续字符,可包含字母、_、$、及数字(数字不能作为开头)。标识符可能代表一个变量,或者true、false这种内置常量、也可能是if、return、function这种关键字
  • 运算符:+、-、*、/、>、<等等
  • 括号:(…)可能表示运算优先级、也可能表示函数调用,分词阶段并不关注是哪种语义,只把“(”或“)”当做一种基本语法单元
  • 还有其他:如中括号、大括号、分号、冒号、点等等不再一一列举
什么是 AST(Abstract Syntax Tree)?

抽象语法树 ,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

举例:

console.log(‘你好,babel’)解析成 AST 后的 json格式如下(转换可通过https://astexplorer.net/#/KJ8AjD6maa进行在线转换):

img

img

类似于

1
2
3
4
5
6
{
"type": "Program",
"start": 0,
"end": 23,
"body": [...]
}

类似于这样的结构叫做节点,一个 AST 是由单个或者多个这样的节点构成的,节点内部还可以有子节点,构成一棵语法树。详细请见:https://juejin.cn/post/6844904035271573511

转换

通过@babel/traverse遍历抽象语法树(AST),并调用Babel配置文件中的插件,对抽象语法树(AST)进行增删改

生成

通过@babel/generator把转换后的抽象语法书(AST)生成目标代码

babel插件

babel/core

是 babel的核心,主要作用就是根据我们的配置文件转换代码,配置文件一般是.babelrc(静态文件)或 babel.config.js(可编程),主要作用如下:

  • 加载和处理配置(config)
  • 加载插件
  • 调用 Parser 进行语法解析,生成 AST
  • 调用 Traverser 遍历AST,并使用访问者模式应用’插件’对 AST 进行转换
  • 生成代码,包括SourceMap转换和源代码生成

核心周边

  • Parser( @babel/parser ) : 将源代码解析为 AST 。

  • Traverser( @babel/traverse ) : 对 AST 进行遍历,转换插件会通过它获取感兴趣的AST节点,对节点继续操作

  • Generator( @babel/generator ) : 将 AST 转换为源代码

    • babel-loader: 负责 es6 语法转化

    • babel-preset-env: 包含 es6、7 等版本的语法转化规则

babel的插件分类

  • 语法插件:

    语法插件仅允许 babel 解析语法,不做转换操作;当添加语法插件后,使得babel能够解析更多的语法

    例如:当我们定义或者调用方法时,最后一个参数之后是不允许增加逗号的,如 func(param1, param2,) 就是非法的。如果源码是这种写法,经过 babel 之后就会提示语法错误。

    但最近的 JS 提案中已经允许了这种新的写法(让代码 diff 更加清晰)。为了避免 babel 报错,就需要增加语法插件 babel-plugin-syntax-trailing-function-commas

  • 转译插件:

    当添加转译插件后,会将源代码进行转译输出。

    例如:箭头函数 (a) => a 就会转化为 function (a) {return a}。完成这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions

babel-loader

负责 es6 语法转化

babel/preset-env

包含 es6、7 等版本的语法转化规则

babel-polyfill

babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:

  1. 全局对象:Promise、WeakMap 等。
  2. 全局静态函数:Array.from、Object.assign 等。
  3. 实例方法:比如 Array.prototype.includes 等。

此时,需要引入@babel/polyfill来模拟实现这些对象、方法。需要安装在生产依赖中

主要缺点:

  1. 使用 babel-polufill 导致打出来的包体积比较大,因为 babel-polyfill 是一个整体,把所有方法都会加到原型链上。比如使用了 Array.from,但它会把 Object.defineProperty也给加上,属于一种浪费,要解决这个问题,可以通过单独使用 core-js 的某个类库解决,core-js 是分开的
  2. babel-polyfill 会污染全局变量,给很多的原型链上作出修改,所以会比较倾向于使用 babel-plugin-transform-runtime

注意:如果代码中使用了较高版本的 js实例方法, 比如 [1,2,3].includes(1),此时还是需要使用 polyfill

babel-runtime

提供各种 helper 函数

babel-plugin-transform-runtime

https://blog.windstone.cc/es6/babel/@babel/plugin-transform-runtime.html

该插件会开启对 Babel 注入的helper codehelper可译为辅助函数)的复用,以节省代码体积。

Babel 使用了一些很小的helpers作为通用函数比如_extend。默认情况下,这些helpers会被添加到需要的每一个文件里。这种代码重复有时是不需要的,尤其是你的应用分散在多个文件里。这也是@babel/plugin-transform-runtime插件出现的原因:所有的helpers将引用@babel/runtime模块以避免在编译输出文件里的代码重复。@babel/runtime将编译到构建输出文件里。

@babel/runtime: 提供各种 helper 函数

@babel/plugin-transform-runtime: 自动引入 helper 函数

预设

插件只对单个功能进行转换,当配置插件比较多时,就可以封装成预设(presets)以此来简化插件的使用,预设简单说就是一组原先设定好的插件,是一组插件的集合,比如 @babel/preset-react 包含以下插件:

比如 es2015 是一套规范,包含很多转译插件。如果每次要开发者一个个添加并安装,配置文件很长不说,npm install 的时间也会很长,为了解决这个问题,babel 还提供了一组插件的集合。因为常用,所以不必重复定义 & 安装。

常见的preset 分为以下几种

官网常推荐的 详见:https://babel.docschina.org/docs/en/babel-preset-env/

@babel/preset-env。主要可以根据配置的目标浏览器或者运行环境来自动将ES2015+的代码转换为es5。

@babel/preset-react 。react框架需要的

@babel/preset-flow flow需要的。Flow 是一个静态类型检测工具,进行类型检查,类似于ts

@babel/preset-typescript typescript。需要的

执行顺序原则如下:

  • Plugin 会运行在 Preset 之前。
  • Plugin 会从前到后顺序执行。
  • Preset 的顺序则从后向前。

preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']。这样必须先执行 stage-0 才能确保 babel 不报错。因为低一级的 stage 会包含所有高级 stage 的内容

@babel/preset-env

可以根据配置的目标浏览器或者运行环境来自动将 ES5+的代码转换为 ES5

ESM

ESM:基于浏览器 ES 模块的构建工具

browserifywebpackrollupparcel这些工具的思想都是递归循环依赖,然后组装成依赖树,优化完依赖树后生成代码。
但是这样做的缺点就是慢,需要遍历完所有依赖,即使 parcel 利用了多核,webpack 也支持多线程,在打包大型项目的时候依然慢可能会用上几分钟,存在性能瓶颈。所以基于浏览器原生 ESM 的运行时打包工具出现:

仅打包屏幕中用到的资源,而不用打包整个项目,开发时的体验相比于 bundle类的工具只能用极速来形容。(实际生产环境打包依然会构建依赖方式打包)

图片 图片

bundleless打包原理

bundleless类运行时打包工具的启动速度是毫秒级的,因为不需要打包任何内容,只需要起两个server,一个用于页面加载,另一个用于HMRWebSocket,当浏览器发出原生的ES module请求,server收到请求只需编译当前文件后返回给浏览器不需要管依赖。

bundleless工具在生产环境打包的时候依然bundle构建所有依赖视图的方式,vite 是利用 rollup 打包生产环境的 js 的。

原理拿 vite 举例:

vite在启动服务器后,会预先以所有 html 为入口,使用 esbuild 编译一遍,把所有的 node_modules 下的依赖编译并缓存起来,例如vue缓存为单个文件。

当打开在浏览器中输入链接,渲染index.html文件的时候,利用浏览器自带的ES module来请求文件。

图片

vite 收到一个src/main.jshttp 文件请求,使用esbuild开始编译main.js,这里不进行main.js里面的依赖编译。

图片

浏览器获取到并编译main.js后,再次发出 2 个请求,一个是 vue 的请求,因为前面已经说了 vue 被预先缓存下来,直接返回缓存给浏览器,另一个是App.vue文件,这个需要@vitejs/plugin-vue来编译,编译完成后返回结果给浏览器(@vitejs/plugin-vue会在脚手架创建模板的时候自动配置)。

因为是基于浏览器的ES module,所以编译过程中需要把一些 CommonJsUMD 的模块都转成 ESM

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求,即使缓存失效只要服务没有被杀死,编译结果依然保存在程序内存中也会很快返回。

上面多次提到了esbuildesbuild使用 go 语言编写,所以在 i/o 和运算运行速度上比解释性语言 NodeJs 快得多,esbuild 号称速度是 node 写的其他工具的 10~100 倍。

图片

ES module 依赖运行时编译的概念 + esbuild + 缓存 让 vite 的速度远远甩开其他构建工具。

snowpack

vite

https://juejin.cn/post/7004784113706090526

vite配置多入口,多页面应用:https://juejin.cn/post/6985500915969032222

  • @vitejs/plugin-vue:

    通过@vitejs/plugin-vue这个插件来支持Vue

  • vite-plugin-node-polyfills:https://juejin.cn/post/7145383283038093319

    项目使用的两个第三方库中有使用到 node环境的apivite本身并不会自动引入这些polyfills,所以需要自己添加他们

比较

Webpack与Vite对⽐

Vite特点: 轻量、按需打包rollup 、HMR (热渲染依赖)

Webpack:由于需要预先编译打包所以启动速度慢,但⽣态成熟。

对⽐

webpack :强调对web开发的⽀持,尤其是内置了HMR的⽀持,插件系统⽐较强⼤,对各种模块系统兼容性最佳

(amd,cjs,umd,esm等,兼容性好的有点过分了,这实际上有利有弊,导致⾯向webpack编程),有丰富的⽣态,缺

点是产物不够⼲净,产物不⽀持⽣成esm格式, 插件开发上⼿较难,不太适合库的开发。

rollup: 强调对库开发的⽀持,基于ESM模块系统,对tree shaking有着良好的⽀持,产物⾮常⼲净,⽀持多种输出

格式,适合做库的开发,插件api⽐较友好,缺点是对cjs⽀持需要依赖插件,且⽀持效果不佳需要较多的hack,不

⽀持HMR,做应⽤开发时需要依赖各种插件。

esbuild: 强调性能,内置了对css、图⽚、react、typescript等内置⽀持,编译速度特别快(是webpack和rollup速

度的100倍+),缺点是⽬前插件系统较为简单,⽣态不如webpack和rollup成熟。

微前端

single-spa

概念

Single-spa 从现代框架组件生命周期中获得灵感,将生命周期应用于整个应用程序。 它脱胎于 Canopy 使用 React + React-router 替换 AngularJS + ui-router 的思考,避免应用程序被束缚。

Single-spa 包括以下内容:

  1. Applications,每个应用程序本身就是一个完整的 SPA (某种程度上)。 每个应用程序都可以响应 url 路由事件,并且必须知道如何从 DOM 中初始化、挂载和卸载自己。 传统 SPA 应用程序和 Single SPA 应用程序的主要区别在于,它们必须能够与其他应用程序共存,而且它们没有各自的 html 页面

    例如,React 或 Angular spa 就是应用程序。 当激活时,它们监听 url 路由事件并将内容放在 DOM上。 当它们处于非活动状态时,它们不侦听 url 路由事件,并且完全从 DOM 中删除。

  2. 一个 single-spa-config配置, 这是html页面和向Single SPA注册应用程序的JavaScript。每个应用程序都注册了三件东西

    • A name
    • A function (加载应用程序的代码)
    • A function (确定应用程序何时处于活动状态/非活动状态)
主题 应用程序 沙箱 公共模块
路由 有多个路由 无路由 无路由
API 声明API 必要的API 没有single-spa API
渲染UI 渲染UI 渲染UI 不直接渲染UI
生命周期 single-spa管理生命周期 自定义管理生命周期 没有生命周期
什么时候使用 核心构建模块 仅在多个框架中需要 共享通用逻辑时使用

创建

创建类型

https://zh-hans.single-spa.js.org/docs/separating-applications

初始化

  1. 创建一个 html 文件:
1
2
3
4
5
<html>
<body>
<script src="single-spa-config.js"></script>
</body>
</html>
  1. 创建一个single-spa-config。查看文档以获取更多详细信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//main.js

import * as singleSpa from 'single-spa';

const name = 'app1';

/* loading 是一个返回 promise 的函数,用于 加载/解析 应用代码。
* 它的目的是为延迟加载提供便利 —— single-spa 只有在需要时才会下载应用程序的代码。
* 在这个示例中,在 webpack 中支持 import ()并返回 Promise,但是 single-spa 可以使用任何返回 Promise 的加载函数。
*/
const app = () => import('./app1/app1.js');

/* Single-spa 配置顶级路由,以确定哪个应用程序对于指定 url 是活动的。
* 您可以以任何您喜欢的方式实现此路由。
* 一种有用的约定是在url前面加上活动应用程序的名称,以使顶层路由保持简单。
*/
const activeWhen = '/app1';

singleSpa.registerApplication({ name, app, activeWhen });

singleSpa.start();
  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
//app1.js

let domEl;

export function bootstrap(props) {
return Promise
.resolve()
.then(() => {
domEl = document.createElement('div');
domEl.id = 'app1';
document.body.appendChild(domEl);
});
}

export function mount(props) {
return Promise
.resolve()
.then(() => {
// 在这里通常使用框架将ui组件挂载到dom。请参阅https://single-spa.js.org/docs/ecosystem.html。
domEl.textContent = 'App 1 is mounted!'
});
}

export function unmount(props) {
return Promise
.resolve()
.then(() => {
// 在这里通常是通知框架把ui组件从dom中卸载。参见https://single-spa.js.org/docs/ecosystem.html
domEl.textContent = '';
})
}

注册应用的生命周期

在一个 single-spa 页面,注册的应用会经过下载(loaded)、初始化(initialized)、被挂载(mounted)、卸载(unmounted)和unloaded(被移除)等过程。single-spa会通过“生命周期”为这些过程提供钩子函数。

生命周期函数是 single-spa 在注册的应用上调用的一系列函数,single-spa 会在各应用的主文件中,查找对应的函数名并进行调用。

注:

  • bootstrap, mount, and unmount的实现是必须的,unload则是可选的
  • 生命周期函数必须有返回值,可以是Promise或者async函数
  • 如果导出的是函数数组而不是单个函数,这些函数会被依次调用,对于promise函数,会等到resolve之后再调用下一个函数
  • 如果 single-spa 未启动,各个应用会被下载,但不会被初始化、挂载或卸载。

single-spa 生态中有各个主流框架对于生命周期函数的实现,这些文档有助于理解这些helper执行的操作,也有助于你自己实现生命周期函数。

生命周期参数

生命周期函数使用”props” 传参,这个对象包含single-spa相关信息和其他的自定义属性。

1
2
3
4
5
6
7
8
9
function bootstrap(props) {
const {
name, // 应用名称
singleSpa, // singleSpa实例
mountParcel, // 手动挂载的函数
customProps // 自定义属性
} = props; // Props 会传给每个生命周期函数
return Promise.resolve();
}

内置参数

每个生命周期函数的入参都会保证有如下参数:

  • name: 注册到 single-spa 的应用名称
  • singleSpa: 对singleSpa 实例的引用, 方便各应用和类库调用singleSpa提供的API时不再导入它。 可以解决有多个webpack配置文件构建时无法保证只引用一个singleSpa实例的问题。
  • mountParcel: mountParcel 函数.

自定义参数

除single-spa提供的内置参数外,还可以指定自定义参数,在调用各个生命周期函数时传入。指定方法是在调用registerApplication时,传入第4个参数。

root.application.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
singleSpa.registerApplication({
name: 'app1',
activeWhen,
app,
customProps: { authToken: "d83jD63UdZ6RS6f70D0" }
});

singleSpa.registerApplication({
name: 'app1',
activeWhen,
app,
customProps: (name, location) => {
return { authToken: "d83jD63UdZ6RS6f70D0" };
}
});

app1.js

1
2
3
4
export function mount(props) {
console.log(props.authToken); // 可以在 app1 中获取到authToken参数
return reactLifecycles.mount(props);
}

可能使用到的场景:

  • 各个应用共享一个公共的 access token
  • 下发初始化信息,如渲染目标
  • 传递对事件总线(event bus)的引用,方便各应用之间进行通信

注意如果没有提供自定义参数,则props.customProps默认会返回一个空对象。

下载(load)

注册的应用会被懒加载,这指的是该应用的代码会从服务器端下载并执行。注册的应用在activity function 第一次返回真值(truthy value)时,下载动作会发生。在下载过程中,建议尽可能执行少的操作,可以在bootstrap生命周期之后再执行各项操作。若确实有在下载时需要执行的操作,可将代码放入子应用入口文件中,但要放在各导出函数的外部。例如:

1
2
3
4
5
console.log("The registered application has been loaded!");

export async function bootstrap(props) {...}
export async function mount(props) {...}
export async function unmount(props) {...}

初始化

这个生命周期函数会在应用第一次挂载前执行一次

1
2
3
4
5
6
7
8
export function bootstrap(props) {
return Promise
.resolve()
.then(() => {
// One-time initialization code goes here
console.log('bootstrapped!')
});
}

挂载

每当应用的activity function返回真值,但该应用处于未挂载状态时,挂载的生命周期函数就会被调用。调用时,函数会根据URL来确定当前被激活的路由,创建DOM元素、监听DOM事件等以向用户呈现渲染的内容。任何子路由的改变(如hashchangepopstate等)不会再次触发mount,需要各应用自行处理。

1
2
3
4
5
6
7
8
export function mount(props) {
return Promise
.resolve()
.then(() => {
// Do framework UI rendering here
console.log('mounted!')
});
}

卸载

每当应用的activity function返回假值,但该应用已挂载时,卸载的生命周期函数就会被调用。卸载函数被调用时,会清理在挂载应用时被创建的DOM元素、事件监听、内存、全局变量和消息订阅等。

1
2
3
4
5
6
7
8
export function unmount(props) {
return Promise
.resolve()
.then(() => {
// Do framework UI unrendering here
console.log('unmounted!');
});
}

移除

“移除”生命周期函数的实现是可选的,它只有在unloadApplication被调用时才会触发。如果一个已注册的应用没有实现这个生命周期函数,则假设这个应用无需被移除。

移除的目的是各应用在移除之前执行部分逻辑,一旦应用被移除,它的状态将会变成NOT_LOADED,下次激活时会被重新初始化。

移除函数的设计动机是对所有注册的应用实现“热下载”,不过在其他场景中也非常有用,比如想要重新初始化一个应用,且在重新初始化之前执行一些逻辑操作时。

1
2
3
4
5
6
7
8
export function unload(props) {
return Promise
.resolve()
.then(() => {
// Hot-reloading implementation goes here
console.log('unloaded!');
});
}

超时

默认情况下,所有注册的应用遵循全局超时配置,但对于每个应用,也可以通过在主入口文件导出一个timeouts对象来重新定义超时时间。如:

app-1.main-entry.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
export function bootstrap(props) {...}
export function mount(props) {...}
export function unmount(props) {...}

export const timeouts = {
bootstrap: {
millis: 5000,
dieOnTimeout: true,
warningMillis: 2500,
},
mount: {
millis: 5000,
dieOnTimeout: false,
warningMillis: 2500,
},
unmount: {
millis: 5000,
dieOnTimeout: true,
warningMillis: 2500,
},
unload: {
millis: 5000,
dieOnTimeout: true,
warningMillis: 2500,
},
};

注意millis指的是最终控制台输出警告的毫秒数,warningMillis指的是将警告打印到控制台(间隔)的毫秒数。

切换应用时过渡

如果你想为应用在挂载和卸载时加一些过渡效果(动画效果等),则需要将其和bootstrap, mount, 和 unmount等生命周期函数关联。这个single-spa 过渡仓库是个小demo,展示了生命周期之间切换时如何过渡。

对于已经挂载的应用,各个页面之间的过渡效果可由应用本身自行处理,如基于React创建的项目可使用using react-transition-group实现过渡效果。

Edit this page

API

https://zh-hans.single-spa.js.org/docs/api

registerApplication注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
singleSpa.registerApplication({
name: 'myApp',
app: () => import('src/myApp/main.js'),
activeWhen: ['/myApp', (location) => location.pathname.startsWith('/some/other/path')],
customProps: {
some: 'value',
},
});
singleSpa.registerApplication({
name: 'myApp',
app: () => import('src/myApp/main.js'),
activeWhen: ['/myApp', (location) => location.pathname.startsWith('/some/other/path')],
customProps: (name, location) => ({
some: 'value',
}),
})
name

registerApplication的第一个参数表示应用名称,name必须是string类型

Loading Function or Application

registerApplication的第二个参数可以是一个Promise类型的 加载函数,也可以是一个已经被解析的应用。

加载函数

registerApplication的第二个参数必须是返回promise的函数((或“async function”方法)。这个函数没有入参,会在应用第一次被下载时调用。返回的Promise resolve之后的结果必须是一个可以被解析的应用。常见的实现方法是使用import加载:() => import(‘/path/to/application.js’)

Application

你可以选择将一个已经被解析过的应用作为registerApplication的第二个参数,这个应用其实是一个包含各个生命周期函数的对象。我们既可以从另外一个文件中引入该对象,也可以在single-spa的配置文件中定义这个对象。

1
2
3
4
5
6
const application = {
bootstrap: () => Promise.resolve(), //bootstrap function
mount: () => Promise.resolve(), //mount function
unmount: () => Promise.resolve(), //unmount function
}
registerApplication('applicationName', application, activityFunction)
激活函数

registerApplication的第三个参数需要是一个纯函数,window.location会作为第一个参数被调用,当函数返回的值为真(true)值时,应用会被激活。通常情况下,Activity function会根据window.location/后面的path来决定该应用是否需要被激活。

另外一种场景是single-spa根据顶级路由查找应用,而每个应用会处理自身的子路由。 在以下场景,single-spa会调用应用的activity function

在以下情况下,single-spa将调用每个应用的活动函数:

  • hashchange or popstate事件触发时
  • pushState or replaceState被调用时
  • 在single-spa上手动调用[triggerAppChange] 方法
  • checkActivityFunctions方法被调用时

路径前缀会匹配url,允许以下每一种前缀:

自定义属性

registerApplication函数可选的第四个参数是 custom props。这个参数会传递给 single-spa 的 lifecycle 函数。自定义属性可以是一个对象,也可以是一个返回Object的函数。如果自定属性是一个函数,函数的参数是应用的名字(application name)和当前window.location

start

start()方法 必须被single-spa配置文件的js调用,这时应用才会被真正挂载。在start被调用之前,应用先被下载,但不会初始化/挂载/卸载。start方法可以协助我们更好提升应用的性能。举个例子,我们可能会马上注册一个应用(为了立刻下载代码),但不能马上就在DOM节点上挂载该应用,而是需要等一个AJAX请求(可能会获取用户的登录信息)完成后,再根据结果进行挂载。这种情况下,最佳实践是先调用registerApplication,等AJAX请求完成后再调用start

1
2
3
4
5
//single-spa-config.js
import { start } from 'single-spa';
/*在注册应用之前调用start意味着single-spa可以立即安装应用,无需等待单页应用的任何初始设置。*/
start();
// 注册应用。。。。

tip

公共模块

https://zh-hans.single-spa.js.org/docs/recommended-setup#%E6%A6%82%E8%BF%B0

同时注册两个路由??

emm…也是可以的。 如果你能用正确的方式来实现,这件事并不可怕。一旦你正确实现了,这将非常非常厉害。一种实现方式是为每个app创建一个<div id="app name"></div>,这样这两个应用就不会同时修改相同的DOM节点了。[考虑一个path变动,同时有两个应用被激活的场景,译者注]

需要一个id,这个id的以single-spa-application前缀开头,后面接着你的应用的名字。比如,如果你的应用名字叫做app-name,就创建一个id为 single-spa-application:app-name的div。

一个多应用的的例子看着就像这样:

1
2
<div id="single-spa-application:app-name"></div>
<div id="single-spa-application:other-app"></div>

项目构建

husky + lint-staged

使用husky + lint-staged助力团队编码规范, husky&lint-staged 安装推荐使用 mrm, 它将根据 package.json 依赖项中的代码质量工具来安装和配置 husky 和 lint-staged,因此请确保在此之前安装并配置所有代码质量工具,如 Prettier 和 ESlint

首先安装 mrm

1
npm i mrm -D --registry=https://registry.npm.taobao.org

husky 是一个为 git 客户端增加 hook 的工具。安装后,它会自动在仓库中的 .git/ 目录下增加相应的钩子;比如 pre-commit 钩子就会在你执行 git commit 的触发。

那么我们可以在 pre-commit 中实现一些比如 lint 检查单元测试代码美化等操作。当然,pre-commit 阶段执行的命令当然要保证其速度不要太慢,每次 commit 都等很久也不是什么好的体验。

lint-staged,一个仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具;这个很实用,因为我们如果对整个项目的代码做一个检查,可能耗时很长,如果是老项目,要对之前的代码做一个代码规范检查并修改的话,这可能就麻烦了呀,可能导致项目改动很大。

所以这个 lint-staged,对团队项目和开源项目来说,是一个很好的工具,它是对个人要提交的代码的一个规范和约束

安装 lint-staged

mrm 安装 lint-staged自动husky 一起安装下来

1
npx mrm lint-staged

TS

配置

https://blog.csdn.net/weixin_50763257/article/details/125755802

https://zhuanlan.zhihu.com/p/285270177

  • npm install -g typescript

  • tsc –init //生成 tsconfig.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "compilerOptions": {
    "outDir": "./built/",
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "module": "es2015",
    "moduleResolution": "node",
    "target": "es5",
    },
    "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
    }

alias

1
2
3
4
5
6
//tsconfig.json
"compilerOptions": {
"paths": {
"@/*":["src/*"]
}
},

.d.ts声明文件

声明文件也叫做描述文件,以d.ts结尾的文件名,比如xxx.d.ts。声明文件主要给ts编译器用的。

开发中不可避免要引用其它第三方的 js库。这时TS就对引入变量的具体类型不明确了,为了告诉TS变量的类型,因此就有了.d.ts (d即declare),ts的声明文件。

顶级声明declare

  1. .d.ts的顶级声明必须以declare开头

  2. 以declare声明的变量和模块后,其他地方不需要引入,就可以直接使用了

    注意我们需要在配置文件下,引入声明文件

    1
    2
    3
    4
    5
    6
    {
    "compilerOptions": {
    ...
    "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
    ...
    }
普通类型声明
1
2
3
4
5
6
7
8
declare let age: number;
declare function getAge(): number | string;
declare class Person { };

使用普通类型声明
console.log(age);
getAge();
new Person()
声明类型
1
2
3
declare type Asd {
name: string;
}

在include包含的文件范围内可以直接使用Asd这个type

外部枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
declare enum Seasons {
Spring,
Summer,
Autumn,
Winter
}
使用枚举 Seasons
let seasons = [
Seasons.Spring,
Seasons.Summer,
Seasons.Autumn,
Seasons.Winter
]
命名空间
  • 如果一个全局变量有很多子属性,就可以使用namespace。
  • 声明文件里的namespace表示一个全局变量,包含很多个子属性
  • 在命名空间内部不需要再使用declare
1
2
3
4
5
6
7
8
9
10
11
12
13
// 模拟 jQuery 的 $
declare namespace $ {
function ajax(url: string, method: string, data: object): void;
let userName: string;
namespace getName {
function onClick(): void;
}
}

使用 $
$.ajax('/login', 'post', {});
$.userName;
$.getName.onClick();
declare声明模块
1
2
3
declare module '*.css';
declare module '*.less';
declare module '*.png';

这样,我们可以在ts中引入相关的文件而不报错了

注意
  • declareexport 不要同级使用,不然的话,声明文件就需要导入了
  • 在声明文件中 typeinterface 也可以不用加declare ,效果相同

给Window增加自定义属性

给Window增加一个简单的类型声明

创建一个 xxx.d.ts 文件, 使用 declare 声明类型 <注意:**此文件中不可以具有 import 等导入方法**>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<env.d.ts>

/// <reference types="vite/client" />

declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

declare interface Window {
canvas: {
e: boolean[],
x: number,
y: string
}
}
给Window增加一个复杂的类型声明

因为在声明文件中使用 import 会导致被当作一个模块导致类型声明失效,如果我们要给Window增加一个已经声明好的类型就需要先创建一个文件用于定义全局命名空间,我们可以在命名空间中引入类型

创建 xxx.d.ts 文件 -> 创建命名空间 -> 在Window声明文件中使用命名空间定义的类型

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
<window.d.ts>

import { CanvasPlus, Canvas } from '@/declare'

declare namespace WindowCanvas {
interface CanvasInterface extends Canvas {
canvas: CanvasPlus
}
}

export = WindowCanvas
export as namespace WindowCanvas

<env.d.ts>
/// <reference types="vite/client" />

declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

declare interface Window {
windowCanvas: WindowCanvas.CanvasInterface
}

tip

  • 在script 标签中引入ts后,会产生JSX语法错误

    “jsx”: “preserve”,

  • 找不到模块“XXX.vue”或其相应的类型声明

    • 在根目录中创建 shims.d.ts文件

      1
      2
      3
      4
      5
      6
      declare module '*.vue' {
      import { ComponentOptions } from 'vue'
      const componentOptions: ComponentOptions
      export default componentOptions
      }
      declare module '*'
    • 也可以修改tsconfig.json 中替换 declare module ‘*’

      1
      2
      "noImplicitAny": false,
      "allowJs": true,

    路由文件

    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
    // 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
    hidden: true // (默认 false)

    //当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
    redirect: 'noRedirect'

    // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
    // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
    // 若你想不管路由下面的 children 声明的个数都显示你的根路由
    // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
    alwaysShow: true

    name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
    meta: {
    roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
    title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
    icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon
    noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
    breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
    affix: true // 如果设置为true,它则会固定在tags-view中(默认 false)

    // 当路由设置了该属性,则会高亮相对应的侧边栏。
    // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list
    // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置
    activeMenu: '/article/list'
    }

scp2自动化部署

https://blog.csdn.net/weixin_41305441/article/details/107108429

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const client = require('scp2');
// const ora = require('ora');

// console.log(123,ora);
const server = {
host: '', //服务器IP
port: 22, //服务器端口
username: 'root', //服务器ssh登录用户名
password: '', //服务器ssh登录密码
path: '', //服务器web目录
};

// const loading = ora('正在部署至 ' + server.host)
// loading.start()
client.scp('dist/', server, err => {
// loading.stop()
if (err) {
console.log('部署失败');
throw err;
} else {
console.log('部署成功');
}
});

axios构建

概念

增post 删delete 改put 查get

OPTIONS请求方法的主要用途有两个:

1、获取服务器支持的HTTP请求方法;也是黑客经常使用的方法。

2、当发起跨域请求时,由于安全原因,触发一定条件时浏览器会在正式请求之前自动先发起OPTIONS请求,即CORS预检请求,服务器若接受该跨域请求,浏览器才继续发起正式请求。

重复调用

https://www.jb51.net/article/221311.htm

搭建

vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import Vue from 'vue'
import axios from 'axios'
import { Toast, Dialog } from 'vant'
import { VueAxios } from './axios'

//单例模式,即同一时间只会存在一个 Toast
Toast.allowMultiple();

// 创建 axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL, // api base_url
//代理
// baseURL: '/dev',
timeout: 6000 // 请求超时时间
})

//code信息
const codeMessage = {
200: "服务器成功返回请求的数据。",
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
405: "请求方法不被允许。",
406: "请求的格式不可得。",
410: "请求的资源被永久删除,且不会再得到的。",
422: "当创建一个对象时,发生一个验证错误。",
500: "服务器发生错误,请检查服务器。",
502: "网关错误。",
503: "服务不可用,服务器暂时过载或维护。",
504: "网关超时。",
};

let showLoading = null
const failToast = (msg) => {
Toast.fail({
duration: 2000,
message: msg
})
}
const err = (error) => {
console.log(error);
if (error.response) {
const data = error.response.data
if (error.response.status === 403) {
failToast('Forbidden')
}
if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
failToast('Unauthorized')
}
} else {
// 请求超时状态
if (error.message.includes('timeout')) {
console.log('超时了')
failToast('请求超时,请检查网络是否连接正常')
} else {
// 可以展示断网组件
console.log('断网了')
failToast('请求失败,请检查网络是否已连接')
}
}
showLoading && showLoading.clear()
showLoading = null
return Promise.reject(error)
}
/**
* 处理参数
* @param {*} config
*/
const handleParams = (config) => {
console.log(config);
const token = Vue.ls.get('token')
const { method } = config
config.headers.authorization =
"Bearer " + token;
return config
}
// request interceptor
service.interceptors.request.use(config => {
return handleParams(config)
}, err)
// response interceptor
service.interceptors.response.use((response) => {
console.log(response);
return response.data
}, err)

// Vue.prototype.service = service
// export { service as axios }

const installer = {
vm: {},
install(Vue) {
Vue.use(VueAxios, service)
}
}

export {
installer as VueAxios,
service as axios
}

react

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import axios from "axios";
import store from "../store";
import { message, notification, Button, Space, Spin } from 'antd';
import ReactDOM from 'react-dom';
import React from 'react';
// import {HashRouter} from 'react-router-dom' //如果使用的是hash路由类型,使用这个
// const router = new HashRouter()

import { BrowserRouter } from 'react-router-dom'
const router = new BrowserRouter()

const API_BASE_URLS = {
development: "http://121.40.61.99:3600/",
production: "http://121.40.61.99:3600/"
};

const request = axios.create({
baseURL: API_BASE_URLS[process.env.NODE_ENV],
});

const codeMessage = {
200: "服务器成功返回请求的数据。",
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
405: "请求方法不被允许。",
406: "请求的格式不可得。",
410: "请求的资源被永久删除,且不会再得到的。",
422: "当创建一个对象时,发生一个验证错误。",
500: "服务器发生错误,请检查服务器。",
502: "网关错误。",
503: "服务不可用,服务器暂时过载或维护。",
504: "网关超时。",
};
// 当前正在请求的数量
let requestCount = 0
// 显示loading
function showLoading() {
if (requestCount === 0) {
const dom = document.createElement('div')
dom.setAttribute('id', 'loading')
dom.setAttribute('style', {
position: "absolute",

})
document.body.appendChild(dom)
ReactDOM.render(<Spin tip="努力加载中..." delay={2000} size="large" />, dom)
}
requestCount += 1;
}

// 隐藏loading
function hideLoading() {
requestCount -= 1;
if (requestCount === 0) {
const loading = document.getElementById('loading');
document.body.removeChild(loading);
}
}
/**
* 异常处理程序
*/
export const errorHandler = (error) => {
hideLoading();
if (error.response) {
const {
response: {
status,
data
},
} = error;
if (status) {
const errorText = data || codeMessage[status];
if (status === 500) {
message.error(errorText);
} else if (status === 400) {
message.info(errorText);
} else if (status === 401) {
window.localStorage.removeItem("token");
localStorage.removeItem('token');
window.location.hash = "user/login"
message.info(errorText);
}
}
if (!status) {
notification.error({
description: "您的网络发生异常,无法连接服务器",
message: "网络异常",
});
}
} else if (error.message.includes("timeout")) {
// 请求超时状态
notification.error({
description: "求超时,请检查网络是否连接正常",
message: "网络异常",
});
} else {
// 可以展示断网组件
notification.error({
description: "请求失败,请检查网络是否已连接",
message: "网络异常",
});
}
return error;
};
request.interceptors.request.use((config) => {
//调用接口时 设置axios(ajax)请求头Authorization的格式为`Bearer ` +token
config.headers.authorization =
"Bearer " + window.localStorage.getItem("token");
showLoading();
return config;
});
request.interceptors.response.use(
(resp) => {
hideLoading();
return resp;
},
(respError) => {
// console.log(respError, respError.response)
errorHandler(respError)
return respError.response;
}
);

export default request;

国际化 i18n

https://mp.weixin.qq.com/s/NcKVhpKNTnX6E8Ei3Y5LAw

换肤

css

使用var,var() 函数用于插入自定义的属性值

:root是一个伪类,表示文档根元素(html),所有主流浏览器均支持 :root 选择器,除了 IE8 及更早的版本。

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
<!DOCTYPE html>
<html lang="en">
<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>Document</title>
</head>
<style>
html {
--bg: #fff;
--bg-panel: #ebebeb;
--color-heading: rgb(27, 168, 14);
--color-text: #333333;
}/* 可以用 :root代替html */
html[data-theme='dark'] {
--bg: #333333;
--bg-panel: #434343;
--color-heading: #0077ff;
--color-text: #b5b5b5;
}
body {
background-color: var(--bg); /* background color variable */
}
</style>
<body>
<button class="changeThemeToDark">点击</button>
</body>
<script>
document.getElementsByClassName('changeThemeToDark')[0].addEventListener('click', function() {
document.documentElement.setAttribute('data-theme', 'dark'); //set theme to light
});
</script>
</html>

less

lessOptions配置modifyVars,globalVars

scss

注入全局样式

1
2
3
sass-loader v8-,这个选项名是 "data"
sass-loader v8 中,这个选项名是 "prependData"
sass-loader v10+,这个选项名是 "additionalData"

图片

https://shengchangwei.github.io/optimizing-images/

概念

  • 有损(Lossless)和无损(Lossy):一般我们聊到图片的时候经常会听到别人提到有损或者无损的字眼,那么什么是有损和无损呢?无损就是图片在压缩保存后虽然占用的存储更小了,但是图像的呈现质量依然如旧;而有损则是相反,图像随着一次又一次的压缩后,质量会变得越来越差。
  • 索引色(Indexed color)和直接色(Direct color):按照不同的颜色深度可以将颜色分为索引色和直接色。索引色就是图像作者指定图片的用色总共不超过256种;而直接色则是对颜色使用没有限制,可以存在成千上万的颜色值。
  • 光栅格式(raster)和矢量格式(vector):图像根据信息的表示方式可以分为光栅图和矢量图。光栅图也叫作位图,点阵图或者像素图,图的最小单位是由一个个带颜色的像素组合而成,在 Photoshop 里把图片放到最大,将看到许许多多的像素方块,所以光栅图在伸缩的时候图像可能会失真;而矢量图则是以由点、线和一些几何图形为基础,通过数学计算来排列组合而成,这种图在伸缩的时候能完好的保护质量。
JPEG/JPG
  • 关键字

    有损压缩、体积小、加载快、不支持透明

  • 优点

    优点:JPG 最大的特点是有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉——前提是你用对了业务场景。

  • 缺点

    有损压缩在在大图,如背景图、轮播图确实很难露出马脚,但当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。

    此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现。

  • 应用场景

    JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积,是当下比较推崇的一种方案。

PNG
  • 关键字

    无损压缩、质量高、体积大、支持透明

  • 优点

    PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。

    PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的 BUG 就是体积太大。

  • 缺点

    PNG 体积较大,较旧的浏览器和程序可能不支持 PNG 文件。

  • 应用场景

    复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。

  • 扩展

    • PNG-8 与 PNG-24 的选择题

      什么时候用 PNG-8,什么时候用 PNG-24,这是一个问题。

      理论上来说,当你追求最佳的显示效果、并且不在意文件体积大小时,是推荐使用 PNG-24 的。

      但实践当中,为了规避体积的问题,我们一般不用PNG去处理较复杂的图像。当我们遇到适合 PNG 的场景时,也会优先选择更为小巧的 PNG-8。

      如何确定一张图片是该用 PNG-8 还是 PNG-24 去呈现呢?好的做法是把图片先按照这两种格式分别输出,看 PNG-8 输出的结果是否会带来肉眼可见的质量损耗,并且确认这种损耗是否在我们(尤其是你的 UI 设计师)可接受的范围内,基于对比的结果去做判断。

SVG
  • 关键字

    文本文件、体积小、不失真、兼容性好

  • 优点

    和性能关系最密切的一点就是:SVG 与 PNG 和 JPG 相比,文件体积更小,可压缩性更强。 当然,作为矢量图,它最显著的优势还是在于图片可无限放大而不失真这一点上。这使得 SVG 即使是被放到视网膜屏幕上,也可以一如既往地展现出较好的成像品质——1 张 SVG 足以适配 n 种分辨率。此外,SVG 是文本文件。我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。这使得 SVG 文件可以被非常多的工具读取和修改,具有较强的灵活性。

  • 缺点

    一方面是它的渲染成本比较高,这点对性能来说是很不利的。另一方面,SVG 存在着其它图片格式所没有的学习成本(它是可编程的)。

  • 应用场景

    SVG 是可编程的,因此可以在WEB项目中的平面图绘制,如需要绘制线,多边形,图片等。 SVG也可作为iocn图标,个人认为作为图标使用的话, 在线矢量图形库已经满足大部分的业务需求了。

svg改颜色

Base64
  • 关键字

    文本文件、依赖编码、小图标解决方案

  • 优点

    base64就是一串字符串码表示的图片,在加载页面和js时一块加载出来,减少了加载图片时的http请求。加载一张图片时会发起一次http请求,http请求每次建立都会需要一定的时间,对于加载一张小图来说,下载图片所需的时间会比建立http请求的时间要短。

  • 缺点

    既然 Base64 这么棒,我们何不把大图也换成 Base64 呢?

    这是因为,Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的)。如果我们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增加,即便我们减少了 HTTP 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失。 在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 HTTP 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势。 因此,Base64 并非万全之策,我们往往在一张图片满足以下条件时会对它应用 Base64 编码:

    • 图片的实际尺寸很小(大家可以观察一下掘金页面的 Base64 图,几乎没有超过 2kb 的)

    • 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)

    • 图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)

  • 应用场景

    Base64应用场景往往是比较小的图片,如:页面的Logo。对于一些体积较大的图片,由于Base64的缺点不建议转成Base64的格式。

    如果你使用vue框架,就应该知道, vue-cli创建的webpack模板默认会将10K以下的图片和字体文件转为base64。好处带来了更快的渲染,不会因为页面切换时还有加载图片的延迟感。

WebP
  • 关键字

    年轻的全能型选手

    WebP 是今天在座各类图片格式中最年轻的一位,它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

  • 优点

    WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。WebP 的官方介绍:

    与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类
    JPEG 图像小 25-34%。 无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。
    对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。图片优化是质量与性能的博弈,从这个角度看,WebP 无疑是真正的赢家。

  • 缺点

    WebP 纵有千般好,但它毕竟太年轻。我们知道,任何新生事物,都逃不开兼容性的大坑。此外,WebP 还会增加服务器的负担——和编码 JPG 文件相比,编码同样质量的 WebP 文件会占用更多的计算资源。

  • 应用场景

    现在限制我们使用 WebP 的最大问题不是“这个图片是否适合用 WebP 呈现”的问题,而是“浏览器是否允许 WebP”的问题,即我们上文谈到的兼容性问题。具体来说,一旦我们选择了 WebP,就要考虑在 Safari 等浏览器下它无法显示的问题,也就是说我们需要准备方案B,准备降级方案。

svg

1
2
3
4
5
6
7
8
9
10
<img
class="mar-r-8"
:style="{
width: '16px',
height: '16px',
filter: 'drop-shadow(red 100px 0)',
translate: '-100px',
}"
:src="iconPeople"
/>

多页面

配置文件

package.json

https://es6.ruanyifeng.com/#docs/module-loader#package-json-%E7%9A%84-exports-%E5%AD%97%E6%AE%B5

https://nodejs.cn/api/packages.html

main

项目发布时,默认会包括 package.json,license,README 和main 字段里指定的文件,因为 main 字段里指定的是项目的入口文件,在 browser 和 Node 环境中都可以使用。

如果不设置 main 字段,那么入口文件就是根目录下的 index.js。

比如 packageA 的 main 字段指定为 index.js。

1
"main": "./index.js"

我们引入 packageA 时,实际上引入的就是 node_modules/packageA/index.js。

name

项目的名称,如果是第三方包的话,其他人可以通过该名称使用 npm install 进行安装。

1
"name": "react"

keywords

一组项目的技术关键词,比如 Ant Design 组件库的 keywords 如下:

1
2
3
4
5
6
7
8
9
10
11
"keywords": [
"ant",
"component",
"components",
"design",
"framework",
"frontend",
"react",
"react-component",
"ui"
],

repository/homepage

repository项目的仓库地址以及版本控制信息。

homepage项目主页的链接,通常是项目 github 链接,项目官网或文档首页。

1
2
3
4
5
6
7
{
"repository": {
"type": "git",
"url": "git+https://github.com/jerrywu001/sandpack-vue3.git"
},
"homepage": "https://sandpack-vue3.netlify.app",
}

bugs

项目 bug 反馈地址,通常是 github issue 页面的链接。

1
"bugs": "http://github.com/vuejs/core/issues"

license

项目的开源许可证。项目的版权拥有人可以使用开源许可证来限制源码的使用、复制、修改和再发布等行为。常见的开源许可证有 BSD、MIT、Apache 等,它们的区别可以参考:如何选择开源许可证?

type

在 node 支持 ES 模块后,要求 ES 模块采用 .mjs 后缀文件名。只要遇到 .mjs 文件,就认为它是 ES 模块。如果不想修改文件后缀,就可以在 package.json文件中,指定 type 字段为 module。

“type”: “module”
这样所有 .js 后缀的文件,node 都会用 ES 模块解释。

使用 ES 模块规范

$ node index.js

exports

使用 "exports" 字段可以防止包的使用者使用包中其他未定义的入口点,指定只能引用包文件的范围

exports定义了自定义导出规则,可以理解为路径映射防止其他未被定义的内容被访问

若export不起作用:https://stackoverflow.com/questions/58990498/package-json-exports-field-not-working-with-typescript

子目录别名

package.json文件的exports字段可以指定脚本或子目录的别名。

1
2
3
4
5
6
// ./node_modules/es-module-package/package.json
{
"exports": {
"./submodule": "./src/submodule.js"
}
}

上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。

1
2
import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js

如果没有指定别名,就不能用“模块+脚本名”这种形式加载脚本。

1
2
3
4
5
// 报错
import submodule from 'es-module-package/private-module.js';

// 不报错
import submodule from './node_modules/es-module-package/private-module.js';
main 的别名

exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。

1
2
3
4
5
6
7
8
9
10
{
"exports": {
".": "./main.js"
}
}

// 等同于
{
"exports": "./main.js"
}

由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。

1
2
3
4
5
6
{
"main": "./main-legacy.cjs",
"exports": {
".": "./main-modern.cjs"
}
}

上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs

条件加载

利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。

1
2
3
4
5
6
7
8
9
{
"type": "module",
"exports": {
".": {
"require": "./main.cjs",
"default": "./main.js"
}
}
}

上面代码中,别名.require条件指定require()命令的入口文件(即 CommonJS 的入口),default条件指定其他情况的入口(即 ES6 的入口)

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
{
"include": [
"./index.ts"
], //只编译的文件
"exclude": [
"./test.ts"
], //除了[]中的文件,编译其他所有文件
// 编译过程中一些编译的属性或者编译的配置
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* 启用增量编译 */
// "composite": true, /* 启用允许将类型脚脚本项目与项目引用一起使用的约束 */
// "tsBuildInfoFile": "./", /* 指定.tsbuildinfo增量编译文件的文件夹。 */
// "disableSourceOfProjectReferenceRedirect": true, /* 在引用复合项目时,将禁用首选的源文件,而不是声明文件 */
// "disableSolutionSearching": true, /* 在编辑时,选择一个项目退出多项目引用检查。 */
// "disableReferencedProjectLoad": true, /* 减少通过类型脚本自动加载的项目数量。 */
/* Language and Environment */
"target": "es2016", /* 为发出的JavaScript设置JavaScript语言版本,并包含兼容的库声明 */
// "lib": [], /* 指定一组描述目标运行时环境的捆绑库声明文件 */
// "jsx": "preserve", /* 指定生成的JSX代码 */
// "experimentalDecorators": true, /* 为TC39第二阶段的草稿装饰器提供实验支持。 */
// "emitDecoratorMetadata": true, /* 为源文件中的修饰声明的设计类型元数据 */
// "jsxFactory": "", /* 指定针对ReactJSX发射时使用的JSX工厂函数,例如“React.createElement”或“h” */
// "jsxFragmentFactory": "", /* 指定用于片段的JSX片段引用例如:React.Fragment' or 'Fragment. */
// "jsxImportSource": "", /* 指定用于在使用`jsx时导入JSX工厂函数的模块说明符:反应-jsx*`.` */
// "reactNamespace": "", /* 指定为`创建元素`调用的对象。这只适用于针对`,`JSX emit。*/
// "noLib": true, /* 禁用包括任何库文件,包括默认的lib.d.ts。 */
// "useDefineForClassFields": true, /* emit ECMAScript-符合标准的类字段。 */
/* Modules */
"module": "commonjs", /* 指定所生成的模块代码。 */
// "rootDir": "./", /* 在源文件中指定根文件夹。 */
// "moduleResolution": "node", /* 指定TypeScript如何从给定的模块指定符中查找文件。 */
// "baseUrl": "./", /* 指定要解析非相对模块名称的基本目录。 */
// "paths": {}, /* 指定一组将导入重新映射到其他查找位置的条目。 */
// "rootDirs": [], /* 在解析模块时,允许将多个文件夹视为一个文件夹。*/
// "typeRoots": [], /* 指定多个类似于`./node_modules/@的`类型的文件夹。 */
// "types": [], /* 指定要在源文件中引用的类型包名。 */
// "allowUmdGlobalAccess": true, /* 允许从模块访问UMD全局文件。 */
// "resolveJsonModule": true, /* 启用导入.json文件 */
// "noResolve": true, /* 不允许`import`,`require`或`reference`来扩展类型应该添加到项目的文件数量。*/
/* JavaScript Support */
// "allowJs": true, /* 允许JavaScript文件成为您的程序的一部分。使用`检查JS`选项从这些文件中获取错误。 */
// "checkJs": true, /* 在已检查类型的JavaScript文件中启用错误报告。 */
// "maxNodeModuleJsDepth": 1, /* 指定用于从`node_modules`中检查JavaScript文件的最大文件夹深度。仅适用于`允许的Js`。 */
/* Emit */
// "declaration": true, /* 从项目中的typeScript和JavaScript文件生成.d.ts文件。 */
// "declarationMap": true, /* 为d.ts文件创建源集映射。 */
// "emitDeclarationOnly": true, /* 只输出d.ts文件,而不输出JavaScript文件。 */
// "sourceMap": true, /* 为发出的JavaScript文件创建源映射文件。 */
// "outFile": "./", /* 指定一个将所有输出捆绑到一个JavaScript文件中的文件。如果`声明`为true,则还指定一个捆绑所有.d.ts输出的文件。 */
// "outDir": "./", /* 为所有发出的文件指定一个输出文件夹。 */
//去除注释
// "removeComments": true, /* 禁用注释 */
// "noEmit": true, /* 从编译中禁用emit文件。 */
// "importHelpers": true, /* 允许从每个项目的tslib中导入一次助手函数,而不是为每个文件包含它们。 */
// "importsNotUsedAsValues": "remove", /* 为仅用于类型的导入指定发射/检查行为 */
// "downlevelIteration": true, /* Emit 更兼容,但冗长和性能较差的JavaScript的迭代。 */
// "sourceRoot": "", /* 为调试器指定根路径以查找引用源代码的位置。 */
// "mapRoot": "", /* 指定调试器应该定位映射文件的位置,而不是生成的位置。 */
// "inlineSourceMap": true, /* 在发出的JavaScript中包含源源映射文件。*/
// "inlineSources": true, /* 在发出的JavaScript中的源映射中包含源代码。 */
// "emitBOM": true, /* 在输出文件的开头发出UTF-8字节顺序标记(BOM)。 */
// "newLine": "crlf", /* 设置发射文件的换行符。 */
// "stripInternal": true, /* 禁用在其JSDoc注释中具有`@internal`的发射声明。 */
// "noEmitHelpers": true, /* 禁用在编译输出中生成自定义助手函数。 */
// "noEmitOnError": true, /* 如果报告了任何类型检查错误,则禁用发射文件。 */
// "preserveConstEnums": true, /* 禁用擦除生成的代码中的`常数枚举`声明。 */
// "declarationDir": "./", /* 为生成的声明文件的输出目录。 */
// "preserveValueImports": true, /* 在JavaScript输出中保留未使用的导入值,否则将被删除。 */
/* Interop Constraints */
// "isolatedModules": true, /* 确保每个文件都可以安全地传输,而不依赖于其他导入。 */
// "allowSyntheticDefaultImports": true, /* 当模块没有默认导出时,允许“从y导入x”。 */
"esModuleInterop": true, /* 发出额外的JavaScript,以简化对导入CommonJS模块的支持。这使得`允许合成默认导入`以实现类型兼容性。 */
// "preserveSymlinks": true, /* 禁用对符号链接到其实际路径的解析。这与节点中的同一标志相关联。 */
"forceConsistentCasingInFileNames": true, /* 确保进口时外壳正确。 */
/* Type Checking */
//strict为true,代表下面的都是true
"strict": true,
// /* Enable all strict type-checking options. */
//不要求必须显示的设置any
// "noImplicitAny": true, /*为隐含`任何`类型的表达式和声明启用错误报告 */
//不强制进行null监测
// "strictNullChecks": true, /* 在类型检查时,考虑`null`和`未定义`。 */
// "strictFunctionTypes": true, /* 在分配函数时,请检查以确保参数和返回值与子类型兼容 */
// "strictBindCallApply": true, /* 检查`绑定`、`调用`和`应用`方法的参数是否与原始函数匹配。 */
// "strictPropertyInitialization": true, /* 检查在构造函数中已声明但未设置的类属性。 */
// "noImplicitThis": true, /* 当`this`具有类型`any`时,启用错误报告。*/
// "useUnknownInCatchVariables": true, /* 将catch子句变量类型化为“unknown”,而不是“any”。 */
// "alwaysStrict": true, /* 确保始终发出“严格使用”信号。 */
// "noUnusedLocals": true, /* 在未读取局部变量时启用错误报告。*/
// "noUnusedParameters": true, /* 在未读取函数参数时引发错误 */
// "exactOptionalPropertyTypes": true, /* 将可选的属性类型解释为已写入的,而不是添加“未定义的”。*/
// "noImplicitReturns": true, /* 为不在函数中显式返回的代码路径启用错误报告。 */
// "noFallthroughCasesInSwitch": true, /* 为开关语句中的故障情况启用错误报告。*/
// "noUncheckedIndexedAccess": true, /* 在索引签名结果中包含“未定义的”*/
// "noImplicitOverride": true, /* 确保在派生类中标记覆盖成员。 */
// "noPropertyAccessFromIndexSignature": true, /* 对使用索引类型声明的密钥强制使用索引访问器 */
// "allowUnusedLabels": true, /* 禁用对未使用的标签的错误报告。 */
// "allowUnreachableCode": true, /* 禁用对不可达代码的错误报告。 */
/* Completeness */
// "skipDefaultLibCheck": true, /* 跳过类型检查。d.ts类型脚本中包含的ts文件。 */
"skipLibCheck": true /* 跳过类型检查所有。d.ts文件。 */
}
}

files 配置项值是一个数组,用来指定了待编译文件,即入口文件
当入口文件依赖其他文件时,不需要将被依赖文件也指定到 files 中,因为编译器会自动将所有的依赖文件归纳为编译对象,即 index.ts 依赖 user.ts 时,不需要在 files 中指定 user.tsuser.ts 会自动纳入待编译文件。

.browserslistrc

.commitlintrc.json

.editorconfig

EditorConfig 主要用于统一不同 IDE 编辑器的编码风格。

在项目根目录下添加 .editorconfig 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 表示是最顶层的 EditorConfig 配置文件
root = true

# 表示所有文件适用
[*]
# 缩进风格(tab | space)
indent_style = space
# 控制换行类型(lf | cr | crlf)
end_of_line = lf
# 设置文件字符集为 utf-8
charset = utf-8
# 去除行首的任意空白字符
trim_trailing_whitespace = true
# 始终在文件末尾插入一个新行
insert_final_newline = true

# 表示仅 md 文件适用以下规则
[*.md]
max_line_length = off
trim_trailing_whitespace = false

# 表示仅 ts、js、vue、css 文件适用以下规则
[*.{ts,js,vue,css}]
indent_size = 2

.eslintignore

.eslintrc.js

.gitignore

.npmrc

.prettierrc

.stylelintrc.json

stylelint.config.js

babel.config.js

vue项目搭建

项目搭建

vue脚手架

https://cli.vuejs.org/zh/guide/

  • CLI

    CLI (@vue/cli) 是一个全局安装的 npm 包,提供了终端里的 vue 命令。它可以通过 vue create 快速搭建一个新项目,也可以通过 vue ui 通过一套图形化界面管理你的所有项目

  • CLI 服务

    CLI 服务 (@vue/cli-service) 是一个开发环境依赖。它是一个 npm 包,局部安装在每个 @vue/cli 创建的项目中。

    CLI 服务是构建于 webpackwebpack-dev-server 之上的。它包含了:

    • 加载其它 CLI 插件的核心服务;
    • 一个针对绝大部分应用优化过的内部的 webpack 配置;
    • 项目内部的 vue-cli-service 命令,提供 servebuildinspect 命令。

vue2

https://blog.csdn.net/weixin_44882488/article/details/124220864

  1. 安装脚手架:npm install -g @vue/clinpm i -g webpack webpack-cli

  2. 生成项目模板-可以自定义

    vue create 文件夹名称

vue3

https://juejin.cn/post/7156957907890733063

1.使用 vue-cli 创建

官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

1
2
3
4
5
6
7
8
9
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue_test
## 启动
cd vue_test
npm run serve

2.使用 vite 创建

官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite

vite官网:https://vitejs.cn

https://zhuanlan.zhihu.com/p/654327710

  • 什么是vite?—— 新一代前端构建工具。
  • 优势如下:
    • 开发环境中,无需打包操作,可快速的冷启动。
    • 轻量快速的热重载(HMR)。
    • 真正的按需编译,不再等待整个应用编译完成。
  • 传统构建 与 vite构建对比图
1
2
3
4
5
6
7
8
## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev

配置

https://www.jianshu.com/p/d8d819cc607f

1
2
3
4
5
项目根目录控制台执行 vue inspect > webapck.config.js
(webapck.config.js名字随便起就行)
执行完 ,根目录会多出一个webapck.config.js 的文件,
他就是vue最后生成的webpack配置

环境配置

1
2
3
4
5
6
7
8
9
10
11
12
13
用法:vue-cli-service build [options] [entry|pattern]

选项:

--mode 指定环境模式 (默认值:production)
--dest 指定输出目录 (默认值:dist)
--modern 面向现代浏览器带自动回退地构建应用
--target app | lib | wc | wc-async (默认值:app)
--name 库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名)
--no-clean 在构建项目之前不清除目标目录
--report 生成 report.html 以帮助分析包内容
--report-json 生成 report.json 以帮助分析包内容
--watch 监听文件变化

模式

可以通过传递 --mode 选项参数为命令行覆写默认的模式

  • development 模式用于 vue-cli-service serve
  • test 模式用于 vue-cli-service test:unit
  • production 模式用于 vue-cli-service buildvue-cli-service test:e2e

如果你想要在构建命令中使用开发环境变量:

1
vue-cli-service build --mode development

当运行 vue-cli-service 命令时,所有的环境变量都从对应的环境文件中载入

环境变量

项目根目录中放置下列环境文件来指定环境变量:

1
2
3
4
.env                # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略

只有 NODE_ENVBASE_URL 和以 VUE_APP_ 开头的变量将通过 webpack.DefinePlugin 静态地嵌入到客户端侧的代码中

使用环境变量:console.log(process.env.VUE_APP_SECRET)

插件

vscode插件

  • ref sugar语法快捷改动支持

业务工具类

循环滚动列表

  • vue-seamless-scroll

图片资源懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

(1)安装插件

1
npm install vue-lazyload --save-dev

(2)在入口文件 man.js 中引入并使用

1
import VueLazyload from 'vue-lazyload'

然后再 vue 中直接使用

1
Vue.use(VueLazyload)

或者添加自定义选项

1
2
3
4
5
6
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})

(3)在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:

以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址。

展示动态图片

PWA

  • vue/cli-plugin-pwa只能在https,或者本地生产环境使用

框架功能类

  • ant-design-vue:vue2.0使用版本1,vue3.0使用版本2
  • Muse https://www.cnblogs.com/randomlee/p/10782710.html
  • vue-class-component: 用类的方式编写组件 , vue2.x 对 TS 的支持并不友好,所以 vue2.x 跟 TS 的整合,通常需要基于 vue-class-component 来用基于 class(类) 的组件书写方式。

存储

  • vue-ls:用于从Vue上下文中使用本地Storage,会话Storage和内存Storage

tip

  • 组件内部引入第三方的js文件只在当前组件生效的办法

    https://www.cnblogs.com/qianxiaox/p/13826334.html

  • 缓存导致编译问题

    cache-loader 会默认为 Vue/Babel/TypeScript 编译开启。文件会缓存在 node_modules/.cache 中——如果你遇到了编译方面的问题,记得先删掉缓存目录之后再试试看。

react项目搭建

脚⼿架create-react-app

全局安装create-react-app

1
$ npm install -g create-react-app

创建⼀个项⽬

1
2
3
$ create-react-app your-app 注意命名⽅式
cd your-app
npm i

插件

底层编译类

react

react 这个包,是专门用来创建React组件、组件生命周期等这些东西的;
react-dom 里面主要封装了和 DOM 操作相关的包,要把组件渲染到页面上

1
2
import React from 'react'
import ReactDOM from 'react-dom'

要使用 JSX 语法,必须先运行 cnpm i babel-preset-react -D,然后再 .babelrc 中添加 语法配置;

框架功能类

  • pubsub利用JavaScript进行发布/订阅的库
  • React 中保存页面状态vue/react->keep-alive/React Activation

样式动画类

CSS-in-JS

CSS-in-JS就是将应用的CSS样式写在JavaScript文件里面,而不是独立为一些.css.scss或者less之类的文件,这样你就可以在CSS中使用一些属于JS的诸如模块声明,变量定义,函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。

1
2
// 用 npm 安装
npm install @material-ui/styles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles({
root: {
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
border: 0,
borderRadius: 3,
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
color: 'white',
height: 48,
padding: '0 30px',
},
});

export default function Hook() {
const classes = useStyles();
return <Button className={classes.root}>Hook</Button>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { styled } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const MyButton = styled(Button)({
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
border: 0,
borderRadius: 3,
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
color: 'white',
height: 48,
padding: '0 30px',
});

export default function StyledComponents() {
return <MyButton>Styled Components</MyButton>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const useStyles = makeStyles({
// style rule
foo: props => ({
backgroundColor: props.backgroundColor,
}),
bar: {
// CSS property
color: props => props.color,
},
});

function MyComponent() {
// 为了示例,我们模拟了这个属性
const props = { backgroundColor: 'black', color: 'white' };
// 将 props 作为 useStyles() 的第一个属性传入
const classes = useStyles(props);

return <div className={`${classes.foo} ${classes.bar}`} />
}

样式动画库

https://zhuanlan.zhihu.com/p/361065034

工具类

拖拽react-sortable-hoc

高阶函数(Higher Order Function)=> 参数或返回值为函数组件

react-sortable-hoc 提供了两个特别重要的 API ,分别是SortableContainer 和 SortableElement 看英文的意思也知道,SortableElement用来装饰每个要拖拽的组件,相对于SortableContainer 要装饰的组件,SortableElement 装饰的组件是子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const SortableItem = SortableElement((props: any) => <div>{props.children}</div>);
const SortableList1 = SortableContainer(({ items }: any) => {
return (
<div>
{items.map((item: any, index: number) => (
<SortableItem key={`item-${item}-${index}`} index={index} value={item} >
<RuleOne
></RuleOne>
</SortableItem>
))}
</div>
);
});

<SortableList1 items={submitData['rule1'].ruleGroup} useDragHandle disableAutoscroll
onSortEnd={({ oldIndex, newIndex }: any) => {
if (oldIndex !== newIndex) {
saveRules(submitData, () => {
submitData['rule1'].ruleGroup = arrayMoveImmutable(submitData['rule1'].ruleGroup, oldIndex, newIndex)
})
}
}}
/>

array-move 就一个 API,它的主要作用就是用来交换数组中元素的位置。我们用 node 进行调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引包
const arrayMove = require('array-move');
// 创建一个数组
const input = ['a', 'b', 'c'];

// 以下三个排序实例
console.log(arrayMove(input, 1, 2));
//=> ['a', 'c', 'b']

console.log(arrayMove(input, -1, 0));
//=> ['c', 'a', 'b']

console.log(arrayMove(input, -2, -3));
//=> ['b', 'a', 'c']

优化

  • react:usememo减少渲染
  • react,lazy路由懒加载,代码的分片

tip

Theia

介绍

Eclipse Theia 不是一个 IDE,而是一个用来开发 IDE 的框架。是Eclipse 基金会打造的云端及桌面IDE框架,该产品旨在替代微软的 Visual Studio Code

Theia 能够是一个桌面应用,也能够在浏览器和远程服务中运行。为了用同一套代码,支持桌面应用和webIDE,Theia 运行在两个独立的进程中。这个进程分别被称为前端和后端,它们经过WebSockets上的JSON-RPC消息或HTTP上的REST api进行通讯。在桌面应用中,后端和前端都在本地运行,而在远程上下文中后端将在远程主机上运行。web

前端和后端流程都有它们的依赖注入容器,能够贡献扩展。express

  • 前端

前端进程表明客户端而且担任呈现UI的职责。在浏览器中,它简单的在渲染循环中运行,而在Electron中,它在Electron 的窗口中运行,Electron 窗口是一个基本的带有附加Electron和Node.js api的浏览器。而任何前端代码均可以假设浏览器是一个平台,而不用关联Node.js。后端

  • 后端

后端进程运行在Node.js上。咱们使用express做为HTTP服务器。它可能不须要使用任何依赖浏览器做为平台的代码(DOM api)。后端应用程序的启动将首先加载全部贡献扩展的DI模块,而后得到BackendApplication的实例并在其上调用start(portNumber)。默认状况下,后台的express服务器也为前台提供代码。api

  • 平台分离

在扩展程序的顶层文件夹中,咱们还有一个附加的文件夹层,能够按平台分开:浏览器

  • common文件夹包含不依赖于任何运行时的代码。服务器
  • 浏览器文件夹包含要求使用现代浏览器做为平台(DOM API)的代码。架构
  • 电子浏览器文件夹包含须要DOM API以及Electron渲染器过程特定的API的前端代码。eclipse
  • 节点文件夹包含(后端)代码,须要Node.js做为平台。
  • node-electron文件夹包含特定于Electron的(后端)代码。

Cloud IDE 架构

Cloud IDE 主要包含 Client、Server、Container Pool 三部分。

  • Client: 客户端也是最重要的端,将代码编辑等本地功能移植到浏览器中。
  • Server: 服务端也是控制端,包括管理数据交互及资源调度。
  • Container Pool: 运行时,用户代码真正运行的容器环境。

三者之间最典型的架构如下图所示:

img

环境配置

https://www.cnblogs.com/fanqisoft/p/13171657.html

https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md

1
2
3
4
5
6
7
8
9
10
win11 - 从磁盘到项目文件的路径中不能有中文。因为一些引用路径是用的绝对路径,如果用中文,一些方法是无法识别文件路径的(PS:我猜主要还是国外的人写的,并没有适配语言)

--pyenv
--Python 3.9.13
--nvm
--node v18.14.0
--npm 8.3.1
--yarn 1.22.19
vs 2022/2017(具体看下面配置详情,我用的是2022)
node-gyp: v9.3.1

下载node-gyp

node-gyp查找VS安装路径简单解析:https://juejin.cn/post/6949529951284494344(**PS:通过find-visualstudio.js文件,需要在配置环境用户变量VSINSTALLDIR。这个能让node-gyp找到vs的位置**)

1
2
环境变量-》用户变量
VCINSTALLDIR=C:\Program Files\Microsoft Visual Studio\2022\Community\VC

github: https://github.com/nodejs/node-gyp#on-windows

1
2
3
4
npm i -g node-gyp@last

node-gyp configure --msvs_version=2017
node-gyp configure --msvs_version=2022

如果项目文件中依赖的node-gyp不是最新版本,node_modules/node-gyp/lib/find-visualstudio.js文件中可能没有针对2022版本的处理。

下载pyhon

配置pyenv:https://blog.csdn.net/weixin_42289080/article/details/127997003

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
where python

# 查看当前版本
pyenv version

# 查看所有版本
pyenv versions

# 查看所有可安装的版本
pyenv install --list

# 安装指定版本
pyenv install 3.6.5
# 安装新版本后rehash一下
pyenv rehash

# 删除指定版本
pyenv uninstall 3.5.2

# 指定全局版本
pyenv global 3.6.5

# 指定多个全局版本, 3版本优先
# 实际上当你切换版本后, 相应的pip和包仓库都是会自动切换过去的
pyenv global 3.6.5 2.7.14

# 创建一个3.6.5版本的虚拟环境, 命名为v365env, 然后激活虚拟环境
$ pyenv virtualenv 3.6.5 v365env
$ pyenv activate v365env
# 关闭虚拟环境
$ pyenv deactivate v365env
1
2
3
两种方法:
npm config set python python3.9.13 --global
npm config set python "D:\Python27\python.exe"

安装C/C++编译工具

  • 安装Visual Studio软件以及 C++ 相应的编译工具

    1
    2
    3
    4
    5
    6
    7
    2022
    npm config set msbuild_path "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"

    2017
    npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe"

    npm config set msvs_version 2022 --global

    win11系统,如果SDK报错,需要安装win10SDK

  • 纯前端开发者,只需要安装C/C++编译器工具即可

    1
    以管理员身份执行 npm install -global -production windows-build-tools

配置环境变量或者npm配置

配置npm

1
2
3
4
npm config set VCINSTALLDIR "C:\Program Files\Microsoft Visual Studio\2022\Community\VC" --global
npm config set VCINSTALLDIR "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community" --global
npm config delete msvs_version --global
npm config get msvs_version
1
2
3
4
5
//.npmrc
python=D:\software\pyenv-win-master\pyenv-win\versions\3.9.13\python.exe
msvs_version=2022
GYP_MSVS_VERSION=2022
msbuild_path=C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe120

依赖下载启动问题

  • 下载慢,将资源地址设置为淘宝镜像:yarn config set registry https://registry.npm.taobao.org/

  • 每次更改依赖后下载或者启动有问题,最好删除node_module,以及yarn的缓存(PS:我就是没有去删除yarn缓存,启动服务启动失败)

  • 从磁盘到项目文件的路径中不能有中文。因为一些引用路径是用的绝对路径,如果用中文,一些方法是无法识别文件路径的(PS:我猜主要还是国外的人写的,并没有适配语言)

  • THEIA_ELECTRON_SKIP_REPLACE_FFMPEG=1 yarn,忽略FFMPEG去下载,我的win11找不到FFMPEG的共享库

  • Module did not self-register drivelist

    cd ./node_modules/drivelist && ../.bin/electron-rebuild

启动项目

由于 Theia 及其扩展是 Node.js 包,而 Theia 应用程序是包的集合,因此启动 Theia 及所选扩展的一个非常简单的方法就是创建一个 package.json。

参考文档:Build your own IDE

theia-blueprint

整个工程使用 Lerna 配置 mono-repo构建

目录

1
2
3
4
5
6
7
8
9
10
11
12
resources:electron打包常用一些资源,像应用的图标。
scripts:存放打包使用的脚本,像签名应用程序的脚本。
test:存放测试脚本
electron-builder.yml:electron-builder打包的配置文件
webpack.config.js:webpack的相关配置


执行theia build生成文件
lib:构建生成的Bundle包
plugins:执行download:plugins时下载的插件包
src-gen:theia命令自动生成的工程文件
gen-webpack.config.js:theia自动生成的webpack配置文件,由webpack.config.js引入

安装插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
// ... others
"scripts": {
"prepare": "yarn run clean && yarn build && yarn run download:plugins",
"clean": "theia clean",
"build": "theia build --mode development",
"start": "theia start --plugins=local-dir:plugins",
"download:plugins": "theia download:plugins"
},
"theiaPluginsDir": "plugins",
"theiaPlugins": {
"vscode-builtin-css": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/css-1.39.1-prel.vsix",
"vscode-builtin-html": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/html-1.39.1-prel.vsix",
"vscode-builtin-javascript": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/javascript-1.39.1-prel.vsix",
"vscode-builtin-json": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/json-1.39.1-prel.vsix",
"vscode-builtin-markdown": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/markdown-1.39.1-prel.vsix"
}
}

添加了 theiaPluginsDirtheiaPlugins 这两个属性。theiaPluginsDir 是用来设置我们的插件存放地址的,theiaPlugins 就是我们要安装的插件了

运行项目之前,我们要先运行 yarn prepare 来准备环境,我们会在日志中看到插件的下载情况:

img

这些插件都会放在当前目录下的 plugins 文件夹下

命令

theia这个命令是来自devDependencies配置的@theia/cli

  1. yarn

  2. 构建项目

    yarn theia build

    这个命令主要是用来生成项目代码的,包含源码,webpack 配置文件以及 webpack 打包后的文件。运行成功的结果如下:

    img
    • lib:构建生成的Bundle包
    • plugins:执行download:plugins时下载的插件包
    • src-gen:theia命令自动生成的工程文件
    • gen-webpack.config.js:theia自动生成的webpack配置文件,由webpack.config.js引入
  3. 运行 Theia IDE

    yarn theia start

  • theia rebuild:browser:构建环境切换到 browser
  • theia rebuild:electron:构建环境切换到 electron

theia包

  • application-manager:应用工程管理器,提供 Frontend、Backend、Webpack 代码生成
  • application-package:应用 package.json 配置解析,管理 Application、Extensions
  • cli:是一个命令行工具,用于管理基于 Theia 的应用程序,为扩展和应用程序开发提供了有用的脚本和命令
  • ffmpeg:是一个Node Native 插件,用于动态链接到 Electronffmpeg.dll 并获取包含的编解码器列表
  • localization-manager:用于为不同语言创建 Theia 和 Theia 扩展的本地化
  • ovsx-client:该包用于通过其 REST API@theia/ovsx-client 进行交互。open-vsx 该包允许客户端获取扩展及其元数据、搜索注册表,并包含必要的逻辑来根据提供的支持的 API 版本确定兼容性
  • private-eslint-plugin:对 Eclipse Theia 开发有用的 @theia/eslint-plugin 贡献规则。该插件通过静态分析帮助识别开发过程中的问题,包括代码质量、潜在问题和代码异常。
  • private-ext-scripts:是一个命令行工具,用于在 Theia 包中运行共享的 npm 脚本
  • private-re-exports:用于重新导出依赖项
  • request:发送代理请求的库
  • search-in-workspace` 负责搜索模块
  • terminal` 负责终端模块等

Monorepo

什么是 Monorepo ?

Monorepo是管理项目代码的方式之一,指在一个大的项目仓库(repo)中 管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理。大概结构如下:

1
2
3
4
5
6
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json

目前很很多大型项目采用这样的结构,比如:Babelvue3vite等。

Monorepo 的好处在哪里嘞?

  • 统一管理。比如微前端项目,多个子应用可以放在同一个monorepo中方便管理;后端用node.js的项目放在monorepo中也可以使用同一套技术栈管理。在CI/CD等流水线过程中,方便统一迭代或升级版本,也方便做通用化的配置,适用到多个子项目当中。
  • 依赖提升。如果多个项目都依赖了诸如reactvueTypeScript等常用库,那可以通过lerna或者yarn workspace将依赖提升到最外层,多个子模块/包复用同一个依赖,减小项目体积。

lerna

命令

https://gitcode.net/mirrors/lerna/lerna?utm_source=csdn_github_accelerator

https://blog.csdn.net/a357951314/article/details/111314875

lerna init

1
2
3
4
5
6
7
8
9
10
11
12
lerna.json
{
"lerna": "4.0.0",
"version": "0.0.0",
"useWorkspaces": true,
"npmClient": "yarn",//定义我们需要的包管理工具为yarn
"command": {
"run": {
"stream": true
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//package.json
{
"name": "root",
//因为mono-repo本身的这个Git仓库并不是一个项目,他是多个项目,所以他自己不能直接发布
"private": true,
//用来指定你需要管理的package的目录,默认配置是packages下面的第一级目录下的项目文件夹
"workspaces": [
"packages/*"
],
//如果需要添加其他目录,例如前后端一起的monorepo,可以修改为
"packages": [
"servers/*", "frontend/*"
]
"devDependencies": {
"lerna": "^6.6.1"
}
}

lerna create

lerna create 添加package

lerna add

执行 lerna add 为特定package或这所有package添加依赖

注意默认lerna add会在每个子package中添加依赖,执行 lerna bootstrap --hoist重新提取公共依赖

lerna run

我们可以使用lerna run指令,去运行每个包中包含有相关package script的相关命令

lerna run –parallel start

--parallel参数就是指为项目的中需要一直的进程,打印所有子进程的输出。说得有点绕,理解为需要一直运行的就加上这个参数就可以了

lerna bootstrap

使用下面命令,可以安装所有包的package.json中的dependencies

lerna clean

删除已经安装的子项目node_modules

公共的包

我们可以将公共的包安装在根目录下的package.json下,例如,你是前端微服务的项目而且是使用react作为主要技术栈的,可以把reactreact-dom等安装在根目录下,packages都是可以直接引用的

更常用的是一些项目规范的配置,例如eslintprettiertsconfig之类的,也可以直接安装在根目录下

把公共包提取出来的好处有:

  • 所有包用的依赖包版本都是一致的
  • 做一些包的升级和像github进行包检查时,更方便地进行升级
  • 安装依赖包的时间可以更少
  • 需要更少的存储空间

项目中包与包之间的引用

如果我们packages里面需要进行互相引用

我们使用lerna add命令,可以为指定的包安装第三方的或者本地的包,这个指令和yarn add或者npm install实质上是类似的,下面是一些来自文档的一些例子

构建

https://blog.csdn.net/dennis_jiang/article/details/112181925

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── lerna.json
├── package.json
└── packages
├── vue1
│ ├── __tests__
│ ├── lib
│ ├── README.md
│ └── package.json
└── vue2
├── __tests__
├── lib
├── README.md
└── package.json

  • lerna init

  • packages目录下创建子项目模块

    按照mono-repo的惯例,这几个子项目的名称最好命名为@<主项目名称>/<子项目名称>,这样当别人引用你的时候,你的这几个项目都可以在node_modules的同一个目录下面,目录名字就是@<主项目名称>,所以我们手动改下三个子项目package.json里面的name

    这里我按照vue项目流程创建

  • lerna bootstrap

    packages/下面的每个子项目有自己的node_modules,如果将它打开,会发现很多重复的依赖包,这会占用我们大量的硬盘空间。lerna提供了另一个强大的功能:将子项目的依赖包都提取到最顶层,我们只需要先删除子项目的node_modules再跑下面这行命令就行了:lerna bootstrap –hoist

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

    vue1和vue2项目需要的element版本都是3.1.0,但是common需要的版本却是4.9.4,如果使用lerna bootstrap –hoist来进行提升,lerna会提升用的最多的版本,也就是3.1.0到顶层,然后把子项目的node_modules里面的antd都删了。也就是说common去访问element的话,也会拿到3.1.0的版本,这可能会导致common项目工作不正常

    yarn workspace可以解决前面说的版本不一致的问题。lerna bootstrap --hoist会把所有子项目用的最多的版本移动到顶层,而yarn workspace 则会检查每个子项目里面依赖及其版本,如果版本不一样则会留在子项目自己的node_modules里面,只有完全一样的依赖才会提升到顶层。

    1
    2
    3
    4
    5
    6
    // 顶层package.json
    {
    "workspaces": [
    "packages/*"
    ]
    }
    1
    2
    3
    4
    5
    // lerna.json
    {
    "npmClient": "yarn",
    "useWorkspaces": true
    }

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

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

    1
    "npmClient": "yarn"    

    只想在其中一个子项目运行命令

    1
    2
    3
    4
    5
    6
    // 顶层package.json
    {
    "scripts": {
    "start:vue": "lerna --scope @mono-repo-demo/vue run start"//--scope来指定在管理员子项目下运行
    }
    }
  • 引入公共组件:lerna add @mono-repo-demo/common –scope @mono-repo-demo/vue1

common 组件写好了,我们就在vue1里面引用下他,要引用上面的组件,我们需要先在vue1的package.json里面将这个依赖加上,我们可以去手动修改他,也可以使用lerna命令:

1
2
3
4
//packages\vue\package
"dependencies": {
"@mono-repo-demo/common": "^0.0.0",
},

mono-repo管理electron和web

electron和web一致

编码规范

规范的意义

每个程序员都有自己的编码习惯,最常见的莫过于:

  • 有的人写代码一行代码结尾必须加分号 ;,有的人觉得不加分号 ; 更好看;
  • 有的人写代码一行代码不会超过 80 个字符,认为这样看起来简洁明了,有的人喜欢把所有逻辑都写在一行代码上,觉得别人看不懂的代码很牛逼;
  • 有的人使用变量必然会先定义 var a = 10;,而粗心的人写变量可能没有定义过就直接使用 b = 10;

如果你写自己的项目怎么折腾都没关系,但是在公司中老板希望每个人写出的代码都要符合一个统一的规则,这样别人看源码就能够看得懂,因为源码是符合统一的编码规范制定的。

那么问题来了,总不能每个人写的代码老板都要一行行代码去检查吧,这是一件很蠢的事情。凡是重复性的工作,都应该被制作成工具来节约成本。这个工具应该做两件事情:

  • 提供编码规范;
  • 提供自动检验代码的程序,并打印检验结果:告诉你哪一个文件哪一行代码不符合哪一条编码规范,方便你去修改代码。

Lint 因此而诞生。Lint 是检验代码格式工具的一个统称,具体的工具有 JslintEslint 等等

检测方式

检测可分为两种方式

  • 使用编辑器的扩展

    • 项目中的.vscode/settings.json
    • 本地环境中的 settings.json
  • 使用脚本的方式

    • prettier
    • eslint

上面两种方式如果同时存在的话,会有优先级的问题。**.prettierrc 的优先级会高于在vscode全局配置settings.json中格式化配置的优先级(同理eslint)**

也就是说,如果你在一个项目中有 .prettierrc 配置文件,然后你又在settings.json也配置了格式化规则,那么当你在vscode编辑器中对一个文件点击鼠标右键[格式化文档]的时候,格式化规则会以 .prettierrc 为准

Eslint(代码质量)

【eslint英文文档】https://eslint.org/docs/user-guide/configuring

【eslint中文文档】http://eslint.cn/docs/rules/ https://zh-hans.eslint.org/

含义

ESLint 是什么呢?
是一个开源的 JavaScript 的 linting 工具,使用 espree 将 JavaScript 代码解析成抽象语法树 (AST),然后通过AST 来分析我们代码,从而给予我们两种提示:

  1. 代码质量问题
  2. 代码风格问题(ESLint 之类的 Linters 对于代码格式化的能力是有限的,不如 Prettier 那么专业。通过 Prettier 执行格式化代码的工作,而代码质量的控制由 ESLint 处理)

获得如下收益:

  • 在执行代码之前发现并修复语法错误,减少调试耗时和潜在 bug
  • 保证项目的编码风格统一,提高可维护性
  • 督促团队成员在编码时遵守约定的最佳实践,提高代码质量

ESLint 可以让程序员在编码的过程中发现问题而不是在执行的过程中,还可以让让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插拔的。为了便于人们使用,ESLint 内置了一些规则,当然,你可以在使用过程中自定义规则。所有的规则默认都是禁用的。

安装

生成流程:https://blog.csdn.net/shenxianhui1995/article/details/103035013

如果你想你所有项目都使用eslint,请全局安装;如果你想当前项目使用,请局部安装。

  • 全局安装:

    • npm install -g eslint
    • eslint --init
  • 项目安装:

    • npm install eslint --save-dev
    • ./node_modules/.bin/eslint --init
1
2
3
4
5
6
7
? How would you like to use ESLint? (Use arrow keys) // 你想怎样使用eslint
To check syntax only
//只检查语法
> To check syntax and find problems
//检查语法、发现问题
To check syntax, find problems, and enforce code style
//检查语法、发现问题并执行代码样式

命令

“lint”: “eslint . –ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts –fix”

原理

ESLint 的原理,是遍历语法树然后检验,其核心的 Linter,是不依赖 node 环境的,并且官方也进行了单独的打包输出,具体可以通过 clone官方代码 后,执行 npm run webpack 拿到核心的打包后的 ESLint.js。其本质是对 linter.js 文件的打包。

同时官方也基于该打包产物,提供了 ESLint 的官方 demo

eslint配置

https://www.cnblogs.com/jiaoshou/p/11218526.html

eslint文件搜索

  1. 一般都采用.eslintrc.的配置文件进行配置, 如果放在项目的根目录中,则会作用于整个项目。如果在项目的子目录中也包含着.eslintrc文件,则对于子目录中文件的检查会忽略掉根目录中的配置,而直接采用子目录中的配置,这就能够在不同的目录范围内应用不同的检查规则,显得比较灵活。ESLint采用逐级向上查找的方式查找.eslintrc.文件,当找到带有”root”: true配置项的.eslintrc.文件时,将会停止向上查找。
  2. 在 package.json文件里的 eslintConfig 字段进行配置。

跳过检测

eslint可以具体文件中设置特定代码的规则,常用于跳过某条语句的检测。

1
2
3
4
5
注销全部规则,在代码前新建一行,添加注销 /* eslint-disable *\/  。如果没有恢复设置的语句,则下列全部代码都会跳过检测。
恢复eslint,在代码前新建一行,添加注销 /* eslint-enable *\/
指定忽略的规则,/* eslint-disable no-alert, no-console *\/
在特定行禁用,// eslint-disable-line
在下一行禁用,// eslint-disable-next-lin

root

root:是否以当前目录为根目录。 告诉 ESLint 不要再往上级目录查找,利用此属性配置,项目级和目录级的配置都可以不受上级目录以及祖先目录的配置影响,通常项目根目录应该设置为 true

举个例子,比如这个项目 vue-project1,默认情况下 root 为 false,而且该项目上层目录下还有 eslint 配置文件的话,这个更上一层的配置就会对你的项目文件的代码产生作用,直到到达根目录才会停止。这是我们不愿意看到的,所以就需要我们在当前项目目录下设置 root: true,告诉 ESLint 这里就是根目录了,别再往上查找其他的配置文件了!

env运行环境

env,这是对环境定义的一组全局变量的预设。

使用 env 属性来指定要启用的环境,将其设置为 true,以保证在进行代码检测时不会把这些环境预定义的全局变量识别成未定义的变量而报错

http://eslint.cn/docs/user-guide/configuring#specifying-environments

常见的运行环境

  • browser - 浏览器环境中的全局变量。
  • node - Node.js 全局变量和 Node.js 作用域。
  • es6 - 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)。
  • amd - 将 require() 和 define() 定义为像 amd 一样的全局变量。
  • commonjs - CommonJS 全局变量和 CommonJS 作用域 (用于 Browserify/WebPack 打包的只在浏览器中运行的代码)。
  • jquery - jQuery 全局变量。
  • mongo - MongoDB 全局变量。
  • worker - Web Workers 全局变量。
  • serviceworker - Service Worker 全局变量。
1
2
3
4
5
6
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"jquery": true
}

globals全局变量

ESLint会检测未声明的额外的全局变量,并发出报错,比如node环境中的process,浏览器环境下的全局变量console,以及我们通过cdn引入的jQuery定义的$等;我们可以在globals中进行变量声明:

http://eslint.cn/docs/user-guide/configuring#specifying-globals

  • 定义额外的全局,开发者自定义的全局变量,让其跳过no-undef 规则
  • key值就是额外添加的全局变量
  • value值用于标识该变量能否被重写,类似于const的作用。true为允许变量被重写
  • 注意:要启用no-global-assign规则来禁止对只读的全局变量进行修改。
1
2
3
4
5
6
7
8
{
"globals": {
// true表示该变量可读写,false表示变量是只读
"$": true,
"console": false,
"gTool": true, // 例如定义gTool这个全局变量,且这个变量可以被重写
}
}

但是node或者浏览器中的全局变量很多,如果我们一个个进行声明显得繁琐,因此就需要用到我们的**env,这是对环境定义的一组全局变量的预设。**

parserOptions解析器

http://eslint.cn/docs/user-guide/configuring#specifying-parser

ESLint 解析器将代码转换为 ESLint 可以评估的抽象语法树(AST, abstract syntax tree)。默认情况下,ESLint 使用内置的与标准 JavaScript运行时和版本兼容的 Espree 解析器。

自定义解析器让 ESLint 可以解析非标准的 JavaScript 语法。通常自定义解析器会被包含在可共享配置或插件中,这样你就不需要直接使用它们了。

比如用于让 ESLint 可以解析 TypeScript 代码的 @typescript-eslint/parser 解析器就被包含在 typescript-eslint 项目中。

要指定 npm 模块作为解析,需要在 .eslintrc 文件中使用 parser 选项指定。比如,下方就指定使用 babel-eslint 代替 Espree:

1
2
3
4
5
6
parserOptions: {

parser: 'babel-eslint',
// 指定js版本。语法上的支持
ecmaVersion: 6
}

下列解析器与 ESLint 兼容:

babel-eslint是一个与 Babel 和 ESLint 配合使用的插件。它允许 ESLint 在语法转换器(Babel)处理代码之前对代码进行检查,从而使 ESLint 能够正确识别 Babel 转换后的语法。

通俗地说,如果您使用 Babel 转换 JavaScript 代码,并且希望使用 ESLint 检查代码质量,则可以使用 babel-eslint 插件来确保 ESLint 正确识别转换后的语法。

  • @typescript-eslint/parser(没有它,vue中ts不会检测报错):ESLint 的解析器,用于解析 typescript,从而检查和规范 Typescript 代码
  • @typescript-eslint/eslint-plugin:包含了各类定义好的检测 Typescript 代码的规范。
Vue
  • vue-eslint-parser
  • eslint-plugin-vue包含常用的 vue 规范
    • Configurations for using Vue.js 3.x.
      • "plugin:vue/vue3-essential"base, plus rules to prevent errors or unintended behavior.
      • "plugin:vue/vue3-strongly-recommended" … Above, plus rules to considerably improve code readability and/or dev experience.
      • "plugin:vue/vue3-recommended" … Above, plus rules to enforce subjective community defaults to ensure consistency.
    • Configurations for using Vue.js 2.x.
      • "plugin:vue/essential"base, plus rules to prevent errors or unintended behavior.
      • "plugin:vue/strongly-recommended" … Above, plus rules to considerably improve code readability and/or dev experience.
      • "plugin:vue/recommended" … Above, plus rules to enforce subjective community defaults to ensure consistency

overrides

overrides:用指定配置覆盖指定后缀的文件的规则配置

1
2
3
4
5
6
7
8
9
overrides: [
{
files: ["*.vue"],
rules: {
...
'max-len': 'off' // disables line length check
}
}
]

extend规则继承

http://eslint.cn/docs/user-guide/configuring#extending-configuration-files

一个配置文件可以被基础配置中的已启用的规则继承。

extends 属性值可以是:

  • 指定配置的字符串(配置文件的路径、可共享配置的名称、eslint:recommendedeslint:all)
  • 字符串数组:每个配置继承它前面的配置

ESLint递归地扩展配置,因此基本配置也可以具有 extends 属性。extends 属性中的相对路径和可共享配置名从配置文件中出现的位置解析。

rules 属性可以做下面的任何事情以扩展(或覆盖)规则:

  • 启用额外的规则
  • 改变继承的规则级别而不改变它的选项:
    • 基础配置:"eqeqeq": ["error", "allow-null"]
    • 派生的配置:"eqeqeq": "warn"
    • 最后生成的配置:"eqeqeq": ["warn", "allow-null"]
  • 覆盖基础配置中的规则的选项
    • 基础配置:"quotes": ["error", "single", "avoid-escape"]
    • 派生的配置:"quotes": ["error", "single"]
    • 最后生成的配置:"quotes": ["error", "single"]

plugins插件

http://eslint.cn/docs/user-guide/configuring#configuring-plugins

插件同样需要在node_module中下载

注意插件名忽略了「eslint-plugin-」前缀,所以在package.json中,对应的项目名是「eslint-plugin-vue」

插件的作用类似于解析器,用以扩展解析器的功能,用于检测非常规的js代码。也可能会新增一些特定的规则。

如 eslint-plugin-vue,是为了帮助我们检测.vue文件中